80 lines
1.9 KiB
Python
80 lines
1.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Utility to build an .ico file from a list of PNGs of different sizes.
|
|
|
|
Uses only Python's standard library so it can run in restricted environments.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
|
|
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
|
|
|
|
|
def read_png_dimensions(data: bytes) -> tuple[int, int]:
|
|
if not data.startswith(PNG_SIGNATURE):
|
|
raise ValueError("All inputs must be PNG files.")
|
|
width, height = struct.unpack(">II", data[16:24])
|
|
return width, height
|
|
|
|
|
|
def build_icon(png_paths: list[Path], output: Path) -> None:
|
|
png_data = [p.read_bytes() for p in png_paths]
|
|
entries = []
|
|
offset = 6 + 16 * len(png_data) # icon header + entries
|
|
|
|
for data in png_data:
|
|
width, height = read_png_dimensions(data)
|
|
entry = {
|
|
"width": width if width < 256 else 0,
|
|
"height": height if height < 256 else 0,
|
|
"colors": 0,
|
|
"reserved": 0,
|
|
"planes": 1,
|
|
"bit_count": 32,
|
|
"size": len(data),
|
|
"offset": offset,
|
|
"data": data,
|
|
}
|
|
entries.append(entry)
|
|
offset += entry["size"]
|
|
|
|
header = struct.pack("<HHH", 0, 1, len(entries))
|
|
table = bytearray()
|
|
for entry in entries:
|
|
table.extend(
|
|
struct.pack(
|
|
"<BBBBHHII",
|
|
entry["width"],
|
|
entry["height"],
|
|
entry["colors"],
|
|
entry["reserved"],
|
|
entry["planes"],
|
|
entry["bit_count"],
|
|
entry["size"],
|
|
entry["offset"],
|
|
)
|
|
)
|
|
|
|
payload = header + table + b"".join(entry["data"] for entry in entries)
|
|
output.write_bytes(payload)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("output", type=Path)
|
|
parser.add_argument("inputs", nargs="+", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
if not args.inputs:
|
|
raise SystemExit("Provide at least one PNG input.")
|
|
|
|
build_icon(args.inputs, args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|