mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
- packaging/macos/dmg/generate.py writes background.tiff and DS_Store on any OS (resvg with the repo's fonts; ds_store and mac_alias, as dmgbuild uses). Both are byte-reproducible and carry nothing from the machine that made them. - DS_Store: same window, icon size, positions and background alias as before, without the bookmark (pBBk/pBB0) and the alias's disk-image record, which held the author's boot volume UUID and name, its creation date and temporary paths. - background.tiff: the same pixels as before (checked), now opaque RGB with Deflate (1.8 MB to 1.0 MB) and LittleCMS's built-in sRGB profile instead of an HP profile with no open licence; no file names or software tags. - background.svg: drop an unused pattern and the xlink namespace (renders the same). - ASSETS.md rows for the three files; README, docs/releasing.md (volume name) and ROADMAP. Co-Authored-By: Claude Opus 5.5 <[email protected]>
137 lines
5.1 KiB
Python
137 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Regenerate background.tiff and DS_Store for the macOS DMG window (see README.md here).
|
|
|
|
Needs resvg on PATH (as packaging/icons.sh) and:
|
|
pip install 'pillow>=12' ds_store==1.3.3 mac_alias==2.2.3
|
|
|
|
Runs on any OS, no Mac needed. Both outputs come from this script alone, so they carry nothing
|
|
from the machine that made them: no local paths, user or disk names, dates or volume UUIDs.
|
|
|
|
python3 packaging/macos/dmg/generate.py
|
|
"""
|
|
|
|
import datetime
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
from ds_store import DSStore
|
|
from mac_alias import Alias, TargetInfo, VolumeInfo
|
|
from PIL import Image, ImageCms
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
FONTS = os.path.join(HERE, "..", "..", "..", "assets", "fonts")
|
|
|
|
# Keep in sync with package.sh (volume name) and README.md (layout).
|
|
VOLUME = "VectorCraft"
|
|
WIDTH, HEIGHT = 660, 400 # window content, pt; the background's 1x size
|
|
TITLE_BAR = 32
|
|
ICON_SIZE = 128
|
|
ICONS = {"VectorCraft.app": (326, 205), "Applications": (574, 205)}
|
|
# A fixed date for the alias and the colour profile. Finder never matches it against the image
|
|
# (each build is a new volume): it finds the background by volume name and path.
|
|
FIXED_DATE = datetime.datetime(2026, 10, 8, tzinfo=datetime.timezone.utc)
|
|
|
|
|
|
def render(svg, width, out):
|
|
# Only the repo's fonts (Inter, JetBrains Mono), so every machine draws the same pixels.
|
|
subprocess.run(
|
|
["resvg", "--skip-system-fonts", "--use-fonts-dir", FONTS, "-w", str(width), svg, out],
|
|
check=True,
|
|
)
|
|
with Image.open(out) as im:
|
|
return im.convert("RGB")
|
|
|
|
|
|
def srgb_profile():
|
|
# LittleCMS's built-in sRGB ("No copyright, use freely"), with its creation date fixed.
|
|
icc = bytearray(ImageCms.ImageCmsProfile(ImageCms.createProfile("sRGB")).tobytes())
|
|
d = FIXED_DATE
|
|
icc[24:36] = b"".join(v.to_bytes(2, "big") for v in (d.year, d.month, d.day, 0, 0, 0))
|
|
if icc[84:100] != bytes(16):
|
|
sys.exit("error: the sRGB profile has an ID; recompute it after fixing the date")
|
|
return bytes(icc)
|
|
|
|
|
|
def write_tiff(svg, out):
|
|
"""1x (72 dpi) and 2x (144 dpi) in one TIFF, as `tiffutil -cathidpicheck` writes them."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
one, two = (render(svg, WIDTH * s, os.path.join(tmp, f"{s}x.png")) for s in (1, 2))
|
|
two.encoderinfo = {"dpi": (144, 144)} # the rest as the first page
|
|
# Opaque RGB, Deflate without a predictor: the paper grain is noise, which a predictor inflates.
|
|
one.save(
|
|
out,
|
|
"TIFF",
|
|
save_all=True,
|
|
append_images=[two],
|
|
compression="tiff_adobe_deflate", # brand-ok: Pillow's name for TIFF compression 8 (Deflate)
|
|
dpi=(72, 72),
|
|
icc_profile=srgb_profile(),
|
|
)
|
|
with Image.open(out) as im:
|
|
im.seek(1)
|
|
if im.info.get("dpi") != (144, 144):
|
|
sys.exit("error: the 2x page isn't 144 dpi (needs Pillow 12 or later)")
|
|
|
|
|
|
def background_alias():
|
|
# What Finder stores for /.background/background.tiff on the volume, minus anything about the
|
|
# machine (dmgbuild writes the same kind of alias from a mounted image). Disk type 5 (ejectable),
|
|
# the volume flags and the folder/file ids (25, 26) are what Finder wrote on a test image; the ids
|
|
# are only hints, as the volume's creation date.
|
|
volume = VolumeInfo(VOLUME, FIXED_DATE, b"H+", 5, 0x0D02, b"\0\0")
|
|
volume.posix_path = f"/Volumes/{VOLUME}"
|
|
target = TargetInfo(0, "background.tiff", 25, 26, FIXED_DATE, b"\0\0\0\0", b"\0\0\0\0")
|
|
target.folder_name = ".background"
|
|
target.cnid_path = [25]
|
|
target.carbon_path = f"{VOLUME}:.background:\0background.tiff"
|
|
target.posix_path = "/.background/background.tiff"
|
|
return Alias(volume=volume, target=target).to_bytes()
|
|
|
|
|
|
def write_ds_store(out):
|
|
if os.path.exists(out):
|
|
os.remove(out)
|
|
with DSStore.open(out, "w+") as d:
|
|
d["."]["bwsp"] = {
|
|
"ShowStatusBar": False,
|
|
"ShowToolbar": False,
|
|
"ShowTabView": False,
|
|
"ContainerShowSidebar": False,
|
|
"ShowSidebar": False,
|
|
"WindowBounds": f"{{{{200, 528}}, {{{WIDTH}, {HEIGHT + TITLE_BAR}}}}}",
|
|
}
|
|
d["."]["icvp"] = {
|
|
"viewOptionsVersion": 1,
|
|
"backgroundType": 2,
|
|
"backgroundImageAlias": background_alias(),
|
|
"backgroundColorRed": 1.0,
|
|
"backgroundColorGreen": 1.0,
|
|
"backgroundColorBlue": 1.0,
|
|
"gridOffsetX": 0.0,
|
|
"gridOffsetY": 0.0,
|
|
"gridSpacing": 100.0,
|
|
"arrangeBy": "none",
|
|
"showIconPreview": True,
|
|
"showItemInfo": False,
|
|
"labelOnBottom": True,
|
|
"textSize": 16.0,
|
|
"iconSize": float(ICON_SIZE),
|
|
}
|
|
d["."]["vSrn"] = ("long", 1)
|
|
for name, pos in ICONS.items():
|
|
d[name]["Iloc"] = pos
|
|
|
|
|
|
def main():
|
|
if not shutil.which("resvg"):
|
|
sys.exit("error: resvg not found (brew install resvg / cargo install resvg)")
|
|
write_tiff(os.path.join(HERE, "background.svg"), os.path.join(HERE, "background.tiff"))
|
|
write_ds_store(os.path.join(HERE, "DS_Store"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|