#!/usr/bin/env python3
"""Compile original generated PNG icons to a deterministic, straight-alpha RGBA atlas.
This is an asset compiler, not an image-generation or retouching tool. It does not
alter the original PNGs or recolor, redraw, crop, remove backgrounds, or repair
pixels. Resampling happens in premultiplied alpha to preserve translucent edges.
ContactSheet.png is a labeled inspection aid and is never a runtime asset.
"""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from PIL import Image, ImageDraw, __version__ as pillow_version
ICON_NAMES = (
"Files", "Editor", "Browser", "Terminal", "Folder", "TextFile",
"CodeFile", "MarkdownFile", "ApplicationFile",
)
ICON_SIZE = 64
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def compile_icons(asset_directory: Path) -> dict:
provenance_bytes = (asset_directory / "Provenance.json").read_bytes()
provenance = json.loads(provenance_bytes.decode("utf-8"))
if tuple(asset["Name"] for asset in provenance["Assets"]) != ICON_NAMES:
raise ValueError("Provenance icon order does not match the runtime contract")
atlas_parts: list[bytes] = []
entries: list[dict] = []
thumbnails: list[Image.Image] = []
for index, name in enumerate(ICON_NAMES):
source_path = asset_directory / "Source" / f"{name}.png"
source_bytes = source_path.read_bytes()
with Image.open(source_path) as original:
if original.format != "PNG" or original.mode != "RGBA":
raise ValueError(f"{name}: expected original PNG with explicit RGBA alpha")
original.load()
width, height = original.size
if width <= 0 or height <= 0 or width * height > 8_388_608:
raise ValueError(f"{name}: unsupported source dimensions")
source = original.copy()
histogram = source.getchannel("A").histogram()
if histogram[0] == 0 or histogram[255] == 0:
raise ValueError(f"{name}: must contain truly transparent and opaque pixels")
if histogram[0] < width * height // 10:
raise ValueError(f"{name}: unexpectedly little transparent background")
# Deterministic pixel-format compilation only. No creative image editing.
thumbnail = source.convert("RGBa").resize(
(ICON_SIZE, ICON_SIZE), Image.Resampling.LANCZOS
).convert("RGBA")
pixels = thumbnail.tobytes("raw", "RGBA")
expected_size = ICON_SIZE * ICON_SIZE * 4
if len(pixels) != expected_size:
raise ValueError(f"{name}: invalid compiled byte length")
atlas_parts.append(pixels)
thumbnails.append(thumbnail)
entries.append({
"Name": name,
"Index": index,
"ByteOffset": index * expected_size,
"ByteLength": expected_size,
"Width": ICON_SIZE,
"Height": ICON_SIZE,
"Stride": ICON_SIZE * 4,
"Source": f"Source/{name}.png",
"SourceWidth": width,
"SourceHeight": height,
"SourceSha256": sha256(source_bytes),
"CompiledSha256": sha256(pixels),
"SourceTransparentPixels": histogram[0],
"SourceOpaquePixels": histogram[255],
"CompiledAlphaExtrema": list(thumbnail.getchannel("A").getextrema()),
})
atlas_bytes = b"".join(atlas_parts)
atlas_path = asset_directory / "Atlas.rgba"
atlas_path.write_bytes(atlas_bytes)
manifest = {
"SchemaVersion": 1,
"Tool": "Tools/BuildDesktopIcons.py",
"Generator": "image_gen.imagegen (built-in tool)",
"CompilerPillowVersion": pillow_version,
"SourcePolicy": "Original generated PNGs are retained unchanged.",
"Resampling": "Premultiplied-alpha LANCZOS, then straight-alpha RGBA8",
"Format": "RGBA8",
"Alpha": "Straight",
"Layout": "IconMajor; each icon is row-major; no header or padding",
"ColorSpace": "sRGB",
"IconSize": ICON_SIZE,
"IconCount": len(ICON_NAMES),
"Atlas": "Atlas.rgba",
"AtlasByteLength": len(atlas_bytes),
"AtlasSha256": sha256(atlas_bytes),
"Provenance": "Provenance.json",
"ProvenanceSha256": sha256(provenance_bytes),
"WebReferences": [],
"Icons": entries,
}
(asset_directory / "Manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
# Contact sheet only: originals remain the sole sources of runtime pixels.
card_width, card_height = 224, 128
contact = Image.new("RGB", (card_width * 3, card_height * 3), "#DCDDEC")
drawing = ImageDraw.Draw(contact)
for index, (name, thumbnail) in enumerate(zip(ICON_NAMES, thumbnails)):
left = index % 3 * card_width
top = index // 3 * card_height
drawing.rectangle((left, top, left + card_width - 2, top + 25), fill="#EEEEFC")
drawing.text((left + 10, top + 7), name, fill="#46465C")
drawing.rectangle((left, top + 26, left + 110, top + card_height - 2), fill="#FFFFFF")
drawing.rectangle((left + 111, top + 26, left + card_width - 2, top + card_height - 2), fill="#09121B")
contact.paste(thumbnail, (left + 23, top + 44), thumbnail)
contact.paste(thumbnail, (left + 135, top + 44), thumbnail)
contact.save(asset_directory / "ContactSheet.png", format="PNG")
return manifest
def main() -> None:
default = Path(__file__).resolve().parents[1] / "assets" / "DesktopIcons"
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--assets", type=Path, default=default)
arguments = parser.parse_args()
manifest = compile_icons(arguments.assets.resolve())
print(json.dumps({
"Atlas": str(arguments.assets.resolve() / "Atlas.rgba"),
"IconCount": manifest["IconCount"],
"ByteLength": manifest["AtlasByteLength"],
"Sha256": manifest["AtlasSha256"],
}, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Compile original generated PNG icons to a deterministic, straight-alpha RGBA atlas.
This is an asset compiler, not an image-generation or retouching tool. It does not
alter the original PNGs or recolor, redraw, crop, remove backgrounds, or repair
pixels. Resampling happens in premultiplied alpha to preserve translucent edges.
ContactSheet.png is a labeled inspection aid and is never a runtime asset.
"""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from PIL import Image, ImageDraw, __version__ as pillow_version
ICON_NAMES = (
"Files", "Editor", "Browser", "Terminal", "Folder", "TextFile",
"CodeFile", "MarkdownFile", "ApplicationFile",
)
ICON_SIZE = 64
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def compile_icons(asset_directory: Path) -> dict:
provenance_bytes = (asset_directory / "Provenance.json").read_bytes()
provenance = json.loads(provenance_bytes.decode("utf-8"))
if tuple(asset["Name"] for asset in provenance["Assets"]) != ICON_NAMES:
raise ValueError("Provenance icon order does not match the runtime contract")
atlas_parts: list[bytes] = []
entries: list[dict] = []
thumbnails: list[Image.Image] = []
for index, name in enumerate(ICON_NAMES):
source_path = asset_directory / "Source" / f"{name}.png"
source_bytes = source_path.read_bytes()
with Image.open(source_path) as original:
if original.format != "PNG" or original.mode != "RGBA":
raise ValueError(f"{name}: expected original PNG with explicit RGBA alpha")
original.load()
width, height = original.size
if width <= 0 or height <= 0 or width * height > 8_388_608:
raise ValueError(f"{name}: unsupported source dimensions")
source = original.copy()
histogram = source.getchannel("A").histogram()
if histogram[0] == 0 or histogram[255] == 0:
raise ValueError(f"{name}: must contain truly transparent and opaque pixels")
if histogram[0] < width * height // 10:
raise ValueError(f"{name}: unexpectedly little transparent background")
# Deterministic pixel-format compilation only. No creative image editing.
thumbnail = source.convert("RGBa").resize(
(ICON_SIZE, ICON_SIZE), Image.Resampling.LANCZOS
).convert("RGBA")
pixels = thumbnail.tobytes("raw", "RGBA")
expected_size = ICON_SIZE * ICON_SIZE * 4
if len(pixels) != expected_size:
raise ValueError(f"{name}: invalid compiled byte length")
atlas_parts.append(pixels)
thumbnails.append(thumbnail)
entries.append({
"Name": name,
"Index": index,
"ByteOffset": index * expected_size,
"ByteLength": expected_size,
"Width": ICON_SIZE,
"Height": ICON_SIZE,
"Stride": ICON_SIZE * 4,
"Source": f"Source/{name}.png",
"SourceWidth": width,
"SourceHeight": height,
"SourceSha256": sha256(source_bytes),
"CompiledSha256": sha256(pixels),
"SourceTransparentPixels": histogram[0],
"SourceOpaquePixels": histogram[255],
"CompiledAlphaExtrema": list(thumbnail.getchannel("A").getextrema()),
})
atlas_bytes = b"".join(atlas_parts)
atlas_path = asset_directory / "Atlas.rgba"
atlas_path.write_bytes(atlas_bytes)
manifest = {
"SchemaVersion": 1,
"Tool": "Tools/BuildDesktopIcons.py",
"Generator": "image_gen.imagegen (built-in tool)",
"CompilerPillowVersion": pillow_version,
"SourcePolicy": "Original generated PNGs are retained unchanged.",
"Resampling": "Premultiplied-alpha LANCZOS, then straight-alpha RGBA8",
"Format": "RGBA8",
"Alpha": "Straight",
"Layout": "IconMajor; each icon is row-major; no header or padding",
"ColorSpace": "sRGB",
"IconSize": ICON_SIZE,
"IconCount": len(ICON_NAMES),
"Atlas": "Atlas.rgba",
"AtlasByteLength": len(atlas_bytes),
"AtlasSha256": sha256(atlas_bytes),
"Provenance": "Provenance.json",
"ProvenanceSha256": sha256(provenance_bytes),
"WebReferences": [],
"Icons": entries,
}
(asset_directory / "Manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
# Contact sheet only: originals remain the sole sources of runtime pixels.
card_width, card_height = 224, 128
contact = Image.new("RGB", (card_width * 3, card_height * 3), "#DCDDEC")
drawing = ImageDraw.Draw(contact)
for index, (name, thumbnail) in enumerate(zip(ICON_NAMES, thumbnails)):
left = index % 3 * card_width
top = index // 3 * card_height
drawing.rectangle((left, top, left + card_width - 2, top + 25), fill="#EEEEFC")
drawing.text((left + 10, top + 7), name, fill="#46465C")
drawing.rectangle((left, top + 26, left + 110, top + card_height - 2), fill="#FFFFFF")
drawing.rectangle((left + 111, top + 26, left + card_width - 2, top + card_height - 2), fill="#09121B")
contact.paste(thumbnail, (left + 23, top + 44), thumbnail)
contact.paste(thumbnail, (left + 135, top + 44), thumbnail)
contact.save(asset_directory / "ContactSheet.png", format="PNG")
return manifest
def main() -> None:
default = Path(__file__).resolve().parents[1] / "assets" / "DesktopIcons"
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--assets", type=Path, default=default)
arguments = parser.parse_args()
manifest = compile_icons(arguments.assets.resolve())
print(json.dumps({
"Atlas": str(arguments.assets.resolve() / "Atlas.rgba"),
"IconCount": manifest["IconCount"],
"ByteLength": manifest["AtlasByteLength"],
"Sha256": manifest["AtlasSha256"],
}, indent=2))
if __name__ == "__main__":
main()