Skip to content

Instantly share code, notes, and snippets.

@lukemmtt
Created March 7, 2025 06:19
Show Gist options
  • Select an option

  • Save lukemmtt/8ef5100d58fbd970065cb47709eb89ee to your computer and use it in GitHub Desktop.

Select an option

Save lukemmtt/8ef5100d58fbd970065cb47709eb89ee to your computer and use it in GitHub Desktop.
Generate windows store assets and ICO
#!/usr/bin/env python3
import os
import sys
import subprocess
import io
import json
import math
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter
import numpy as np
# Icon specifications for Windows Store
# Format: name, size, required, apply_rounding
ASSETS = [
# Required assets
# Square44x44Logo (App icon)
{"name": "Square44x44Logo.scale-100.png", "size": (44, 44), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.scale-125.png", "size": (55, 55), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.scale-150.png", "size": (66, 66), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.scale-200.png", "size": (88, 88), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.scale-400.png", "size": (176, 176), "required": True, "apply_rounding": True},
# Target size variants
{"name": "Square44x44Logo.targetsize-16.png", "size": (16, 16), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.targetsize-24.png", "size": (24, 24), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.targetsize-32.png", "size": (32, 32), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.targetsize-48.png", "size": (48, 48), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.targetsize-256.png", "size": (256, 256), "required": True, "apply_rounding": True},
# Unplated variants (still with blue background for brand consistency)
{"name": "Square44x44Logo.altform-unplated_targetsize-16.png", "size": (16, 16), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.altform-unplated_targetsize-24.png", "size": (24, 24), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.altform-unplated_targetsize-32.png", "size": (32, 32), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.altform-unplated_targetsize-48.png", "size": (48, 48), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.altform-unplated_targetsize-256.png", "size": (256, 256), "required": True, "apply_rounding": True},
# Light unplated variants (still with blue background for brand consistency)
{"name": "Square44x44Logo.altform-lightunplated_targetsize-16.png", "size": (16, 16), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.altform-lightunplated_targetsize-24.png", "size": (24, 24), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.altform-lightunplated_targetsize-32.png", "size": (32, 32), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.altform-lightunplated_targetsize-48.png", "size": (48, 48), "required": True, "apply_rounding": True},
{"name": "Square44x44Logo.altform-lightunplated_targetsize-256.png", "size": (256, 256), "required": True, "apply_rounding": True},
# Square150x150Logo (Medium Tile)
{"name": "Square150x150Logo.scale-100.png", "size": (150, 150), "required": True, "apply_rounding": True},
{"name": "Square150x150Logo.scale-125.png", "size": (188, 188), "required": True, "apply_rounding": True},
{"name": "Square150x150Logo.scale-150.png", "size": (225, 225), "required": True, "apply_rounding": True},
{"name": "Square150x150Logo.scale-200.png", "size": (300, 300), "required": True, "apply_rounding": True},
{"name": "Square150x150Logo.scale-400.png", "size": (600, 600), "required": True, "apply_rounding": True},
# StoreLogo
{"name": "StoreLogo.scale-100.png", "size": (50, 50), "required": True, "apply_rounding": True},
{"name": "StoreLogo.scale-125.png", "size": (63, 63), "required": True, "apply_rounding": True},
{"name": "StoreLogo.scale-150.png", "size": (75, 75), "required": True, "apply_rounding": True},
{"name": "StoreLogo.scale-200.png", "size": (100, 100), "required": True, "apply_rounding": True},
{"name": "StoreLogo.scale-400.png", "size": (200, 200), "required": True, "apply_rounding": True},
# Recommended assets
# SmallTile (71x71)
{"name": "SmallTile.scale-100.png", "size": (71, 71), "required": False, "apply_rounding": True},
{"name": "SmallTile.scale-125.png", "size": (89, 89), "required": False, "apply_rounding": True},
{"name": "SmallTile.scale-150.png", "size": (107, 107), "required": False, "apply_rounding": True},
{"name": "SmallTile.scale-200.png", "size": (142, 142), "required": False, "apply_rounding": True},
{"name": "SmallTile.scale-400.png", "size": (284, 284), "required": False, "apply_rounding": True},
# Wide310x150Logo (Wide Tile)
{"name": "Wide310x150Logo.scale-100.png", "size": (310, 150), "required": False, "apply_rounding": True},
{"name": "Wide310x150Logo.scale-125.png", "size": (388, 188), "required": False, "apply_rounding": True},
{"name": "Wide310x150Logo.scale-150.png", "size": (465, 225), "required": False, "apply_rounding": True},
{"name": "Wide310x150Logo.scale-200.png", "size": (620, 300), "required": False, "apply_rounding": True},
{"name": "Wide310x150Logo.scale-400.png", "size": (1240, 600), "required": False, "apply_rounding": True},
# LargeTile (310x310)
{"name": "LargeTile.scale-100.png", "size": (310, 310), "required": False, "apply_rounding": True},
{"name": "LargeTile.scale-125.png", "size": (388, 388), "required": False, "apply_rounding": True},
{"name": "LargeTile.scale-150.png", "size": (465, 465), "required": False, "apply_rounding": True},
{"name": "LargeTile.scale-200.png", "size": (620, 620), "required": False, "apply_rounding": True},
{"name": "LargeTile.scale-400.png", "size": (1240, 1240), "required": False, "apply_rounding": True},
# SplashScreen (620x300)
{"name": "SplashScreen.scale-100.png", "size": (620, 300), "required": False, "apply_rounding": False},
{"name": "SplashScreen.scale-125.png", "size": (775, 375), "required": False, "apply_rounding": False},
{"name": "SplashScreen.scale-150.png", "size": (930, 450), "required": False, "apply_rounding": False},
{"name": "SplashScreen.scale-200.png", "size": (1240, 600), "required": False, "apply_rounding": False},
{"name": "SplashScreen.scale-400.png", "size": (2480, 1200), "required": False, "apply_rounding": False},
# BadgeLogo (24x24)
{"name": "BadgeLogo.scale-100.png", "size": (24, 24), "required": False, "apply_rounding": True},
{"name": "BadgeLogo.scale-125.png", "size": (30, 30), "required": False, "apply_rounding": True},
{"name": "BadgeLogo.scale-150.png", "size": (36, 36), "required": False, "apply_rounding": True},
{"name": "BadgeLogo.scale-200.png", "size": (48, 48), "required": False, "apply_rounding": True},
{"name": "BadgeLogo.scale-400.png", "size": (96, 96), "required": False, "apply_rounding": True},
]
# Standard sizes for Windows ICO file (Microsoft best practices)
ICO_SIZES = [16, 32, 48, 64, 128, 256]
# TimeFinder background color (light blue)
BACKGROUND_COLOR = (0x85, 0xEB, 0xFF, 0xFF) # #85EBFF
def create_consistent_rounded_mask(size, radius):
"""
Create a mask with smooth rounded corners using PIL's ImageDraw.
This creates aesthetically pleasing rounded corners with anti-aliasing.
Args:
size: Tuple of (width, height)
radius: Corner radius in pixels
Returns:
A mask image with rounded corners
"""
width, height = size
# Create a white background (fully opaque)
mask = Image.new("L", size, 0)
draw = ImageDraw.Draw(mask)
# Draw a rounded rectangle with the specified radius
# The rectangle is drawn from (0, 0) to (width-1, height-1)
draw.rounded_rectangle(
[(0, 0), (width-1, height-1)],
radius=radius,
fill=255
)
return mask
def create_asset(
source_image_path,
target_size,
output_path=None,
corner_radius=None,
padding=0,
background_color=None
):
"""
Create an asset from a source image, resizing it to the target size and applying
rounded corners if specified.
Args:
source_image_path: Path to the source image
target_size: Tuple of (width, height) for the target size
output_path: Path to save the resulting image (optional)
corner_radius: Radius of the rounded corners (optional)
padding: Padding to add around the image (optional)
background_color: Background color to use (optional)
Returns:
The resulting image and a dictionary with information about the image
"""
# Open the source image
source = Image.open(source_image_path).convert("RGBA")
# Get the target dimensions
target_width, target_height = target_size
# Calculate the size to resize the source image to
resize_width = target_width - (padding * 2)
resize_height = target_height - (padding * 2)
# Resize the source image while maintaining aspect ratio
source_width, source_height = source.size
source_aspect = source_width / source_height
target_aspect = resize_width / resize_height
if source_aspect > target_aspect:
# Source is wider than target
new_width = resize_width
new_height = int(resize_width / source_aspect)
else:
# Source is taller than target
new_height = resize_height
new_width = int(resize_height * source_aspect)
resized = source.resize((new_width, new_height), Image.LANCZOS)
# Create a new image with the target size
if background_color:
result = Image.new("RGBA", target_size, background_color)
else:
result = Image.new("RGBA", target_size, (0, 0, 0, 0))
# Calculate the position to paste the resized image
paste_x = (target_width - new_width) // 2
paste_y = (target_height - new_height) // 2
# Paste the resized image onto the result
result.paste(resized, (paste_x, paste_y), resized)
# If no corner radius is specified, return the result
if corner_radius == 0:
if output_path:
result.save(output_path)
return {
"size": target_size,
"corner_radius": 0,
"path": output_path
}
return result, {"size": target_size, "corner_radius": 0}
# Calculate corner radius based on Windows guidelines if not specified
if corner_radius is None:
# Scale based on Microsoft's guideline of 2px radius for 48x48 (exterior curves)
scale_factor = min(target_width, target_height) / 48.0
# Use round() instead of int() to properly round the value instead of truncating
corner_radius = max(1, round(2 * scale_factor))
# Create a mask with consistent rounded corners
mask = create_consistent_rounded_mask(target_size, corner_radius)
# Apply the mask to the result
final = Image.new("RGBA", target_size, (0, 0, 0, 0))
final.paste(result, (0, 0), mask)
# Save the resulting image if output path is provided
if output_path:
final.save(output_path)
return {
"size": target_size,
"corner_radius": corner_radius,
"path": output_path
}
# Otherwise return the image and info
return final, {"size": target_size, "corner_radius": corner_radius}
def create_ico_file(source_icon, output_path):
"""
Create a Windows ICO file with multiple sizes according to Microsoft best practices.
Args:
source_icon: Path to the source icon
output_path: Path to save the ICO file
Returns:
Dictionary with information about the created ICO file
"""
print(f"\nGenerating Windows ICO file with {len(ICO_SIZES)} sizes...")
# Create a list to store all the images for the ICO file
ico_images = []
# Create each size for the ICO file using the same asset creation function
for size in ICO_SIZES:
print(f" - Processing {size}x{size} icon size")
# Use the create_asset function to generate the image with consistent styling
image, info = create_asset(
source_icon,
(size, size),
corner_radius=(0 if size < 32 else None),
background_color=BACKGROUND_COLOR
)
# Add the image to our list
ico_images.append(image)
# Create the directory if it doesn't exist
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Save the ICO file using a more reliable approach
# We'll save each size as a separate image in memory and then combine them
# First, create a list of (size, image_bytes) tuples
size_images = []
for img in ico_images:
img_byte_arr = io.BytesIO()
img.save(img_byte_arr, format='PNG')
size_images.append((img.size, img_byte_arr.getvalue()))
# Now write the ICO file manually
with open(output_path, 'wb') as ico_file:
# Write ICO header (6 bytes)
# 0-1: Reserved (0)
# 2-3: Image type (1 for ICO)
# 4-5: Number of images
ico_file.write(bytes([0, 0, 1, 0, len(size_images), 0]))
# Calculate offset to image data
# Header (6 bytes) + (16 bytes per directory entry)
offset = 6 + (16 * len(size_images))
# Write directory entries (16 bytes each)
for i, (size, image_bytes) in enumerate(size_images):
width, height = size
# Width (1 byte, 0 for 256)
ico_file.write(bytes([width if width < 256 else 0]))
# Height (1 byte, 0 for 256)
ico_file.write(bytes([height if height < 256 else 0]))
# Color palette (1 byte, 0 for no palette)
ico_file.write(bytes([0]))
# Reserved (1 byte, 0)
ico_file.write(bytes([0]))
# Color planes (2 bytes, 1 for PNG)
ico_file.write(bytes([1, 0]))
# Bits per pixel (2 bytes, 32 for RGBA PNG)
ico_file.write(bytes([32, 0]))
# Image size in bytes (4 bytes, little-endian)
size_bytes = len(image_bytes).to_bytes(4, byteorder='little')
ico_file.write(size_bytes)
# Image offset from start of file (4 bytes, little-endian)
offset_bytes = offset.to_bytes(4, byteorder='little')
ico_file.write(offset_bytes)
# Update offset for next image
offset += len(image_bytes)
# Write image data
for _, image_bytes in size_images:
ico_file.write(image_bytes)
print(f"✓ Created ICO file with {len(ICO_SIZES)} sizes")
print(f" Saved to: {output_path}")
return {
"sizes": [size for size in ICO_SIZES],
"path": output_path
}
def generate_store_assets(source_icon, assets_dir):
"""
Generate all Windows Store assets from the source icon.
Args:
source_icon: Path to the source icon
assets_dir: Directory to save the assets
"""
# Count total assets
total_assets = len(ASSETS)
required_assets = sum(1 for asset in ASSETS if asset["required"])
print(f"\nGenerating {total_assets} assets ({required_assets} required, {total_assets - required_assets} recommended)")
# Generate each asset
for i, asset in enumerate(ASSETS, 1):
output_path = assets_dir / asset["name"]
size_str = f"{asset['size'][0]}x{asset['size'][1]}"
required_str = "Required" if asset["required"] else "Recommended"
print(f"\n[{i}/{total_assets}] Generating {asset['name']} ({size_str}) - {required_str}")
# Create the asset
try:
# Set corner radius to 0 if rounding should not be applied
corner_radius = 0 if not asset.get('apply_rounding', True) else None
result = create_asset(
source_icon,
asset['size'],
output_path,
corner_radius=corner_radius,
background_color=BACKGROUND_COLOR if not asset.get('transparent', False) else None
)
print(f"✓ Created {asset['name']}")
print(f" Size: {result['size'][0]}x{result['size'][1]}px")
if corner_radius != 0:
print(f" Corner radius: {result['corner_radius']}px")
print(f" Saved to: {result['path']}")
except Exception as e:
print(f"Error generating {asset['name']}: {e}")
if asset["required"]:
print("Warning: Failed to generate a required asset!")
def generate_ico_file(source_icon, output_path):
"""
Generate an ICO file with all required sizes.
Args:
source_icon: Path to the source icon
output_path: Path to save the ICO file
Returns:
True if successful, False otherwise
"""
try:
print(f"Generating Windows ICO file with {len(ICO_SIZES)} sizes...")
# Create a list to store the images for each size
images = []
# Process each size
for size in ICO_SIZES:
print(f" - Processing {size}x{size} icon size")
# Create the asset for this size
img, info = create_asset(
source_icon,
(size, size),
corner_radius=(0 if size < 32 else None),
background_color=BACKGROUND_COLOR
)
# Add the image to the list
images.append(img)
# Save the ICO file with all sizes
images[0].save(
output_path,
format="ICO",
sizes=[(img.width, img.height) for img in images],
append_images=images[1:]
)
print(f"ICO file saved to: {output_path}")
return True
except Exception as e:
print(f"Error generating ICO file: {e}")
return False
def generate_assets(source_icon, assets_dir, resources_dir):
"""
Generate all required and recommended assets for Windows Store.
Args:
source_icon: Path to the source icon
assets_dir: Directory to save the assets
resources_dir: Directory to save the resources
Returns:
True if all required assets were generated, False otherwise
"""
# Create the directories if they don't exist
os.makedirs(assets_dir, exist_ok=True)
os.makedirs(resources_dir, exist_ok=True)
# Generate the ICO file
ico_path = os.path.join(resources_dir, "app_icon.ico")
ico_success = generate_ico_file(source_icon, ico_path)
if not ico_success:
print("Warning: Failed to generate the Windows application icon!")
# Count the number of required and recommended assets
required_count = sum(1 for asset in ASSETS if asset.get("required", False))
recommended_count = len(ASSETS) - required_count
print(f"\nGenerating {len(ASSETS)} assets ({required_count} required, {recommended_count} recommended)")
print()
# Track if all required assets were generated
all_required_generated = True
# Generate each asset
for i, asset in enumerate(ASSETS, 1):
asset_name = asset["name"]
asset_size = f"{asset['size'][0]}x{asset['size'][1]}"
asset_type = "Required" if asset.get("required", False) else "Recommended"
print(f"[{i}/{len(ASSETS)}] Generating {asset_name} ({asset_size}) - {asset_type}")
# Create the output path
output_path = os.path.join(assets_dir, asset_name)
try:
# Create the asset
corner_radius = None
if asset.get("no_rounding", False):
corner_radius = 0
result = create_asset(
source_icon,
asset['size'],
output_path,
corner_radius=corner_radius,
background_color=BACKGROUND_COLOR if not asset.get("transparent", False) else None
)
if isinstance(result, dict):
print(f" - Saved to: {result['path']}")
print(f" - Size: {result['size'][0]}x{result['size'][1]}")
print(f" - Corner radius: {result['corner_radius']}")
except Exception as e:
print(f"Error generating {asset_name}: {e}")
if asset.get("required", False):
all_required_generated = False
print("Warning: Failed to generate a required asset!")
print("\nAssets generation complete!")
print(f"All assets saved to: {assets_dir}")
if ico_success:
print(f"Windows application icon (ICO) saved to: {resources_dir}")
print("\nYour app now has all the necessary assets for Windows Store submission!")
print("The Windows application icon (ICO) has been generated according to Microsoft's best practices.")
return all_required_generated and ico_success
def main():
# Hardcode the path to the source icon
source_icon = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "app_icon", "complete_icon.png")
# Validate the source icon exists
if not os.path.exists(source_icon):
print(f"Error: Source icon '{source_icon}' not found.")
return 1
# Define the assets directory using absolute path
assets_dir = Path("/Users/lukemmtt/Developer/timefinder_app/app/windows/runner/Assets")
# Define the resources directory for the ICO file
resources_dir = Path("/Users/lukemmtt/Developer/timefinder_app/app/windows/runner/resources")
# Create the directories if they don't exist
os.makedirs(assets_dir, exist_ok=True)
os.makedirs(resources_dir, exist_ok=True)
print(f"Assets directory: {assets_dir}")
print(f"Resources directory: {resources_dir}")
# Generate the ICO file
ico_path = resources_dir / "app_icon.ico"
try:
create_ico_file(source_icon, ico_path)
except Exception as e:
print(f"Error generating ICO file: {e}")
print("Warning: Failed to generate the Windows application icon!")
# Generate all Windows Store assets
generate_store_assets(source_icon, assets_dir)
print("\nAssets generation complete!")
print(f"All assets saved to: {assets_dir}")
print(f"Windows application icon (ICO) saved to: {resources_dir}")
print("\nYour app now has all the necessary assets for Windows Store submission!")
print("The Windows application icon (ICO) has been generated according to Microsoft's best practices.")
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment