mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
macOS DMG: generate DS_Store and TIFF from a script, attribute the assets (review of #493)
- 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]>
This commit is contained in:
1 parent
884df22408
commit
d0a99faf1e
6 files changed
+158
-28
No files matched your search
@@ -7,9 +7,10 @@ Finder scripting on CI).
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `background.svg` | Source of the background: the app icon's figure on the VectorCraft colour field (`#e8573f`), Ink and Paper, Inter and JetBrains Mono. |
|
||||
| `background.tiff` | The background at 1x (660 × 400 px, 72 dpi) and 2x (1320 × 800 px, 144 dpi) in one HiDPI TIFF. Goes to `.background/background.tiff`. |
|
||||
| `background.svg` | Source of the background: the app icon (`assets/app-icon/vectorcraft-small.svg`, linked, not copied) cropped as a cover on the VectorCraft colour field (`#e8573f`), Ink and Paper, Inter and JetBrains Mono (from `assets/fonts`, not embedded). |
|
||||
| `background.tiff` | The background at 1x (660 × 400 px, 72 dpi) and 2x (1320 × 800 px, 144 dpi) in one HiDPI TIFF (Deflate, sRGB). Goes to `.background/background.tiff`. |
|
||||
| `DS_Store` | Finder's view settings for the volume: window size, icon size 128, VectorCraft.app at (326, 205), `Applications` at (574, 205), and the background. Goes to `.DS_Store`. |
|
||||
| `generate.py` | Writes `background.tiff` and `DS_Store` from the SVG and the layout above. |
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -23,25 +24,14 @@ Finder scripting on CI).
|
||||
|
||||
## Regenerate
|
||||
|
||||
1. Edit `background.svg` and render it with [resvg](https://github.com/linebender/resvg), with
|
||||
Inter and JetBrains Mono available (`--use-fonts-dir` pointing at a craft-fonts checkout):
|
||||
Edit `background.svg` (or the layout constants in `generate.py`), then run, on any OS:
|
||||
|
||||
```sh
|
||||
resvg --use-fonts-dir ../craft-fonts -w 660 background.svg bg.png
|
||||
resvg --use-fonts-dir ../craft-fonts -w 1320 background.svg [email protected]
|
||||
sips -s dpiWidth 144 -s dpiHeight 144 [email protected]
|
||||
tiffutil -cathidpicheck bg.png [email protected] -out background.tiff
|
||||
```
|
||||
```sh
|
||||
pip install 'pillow>=12' ds_store==1.3.3 mac_alias==2.2.3
|
||||
python3 packaging/macos/dmg/generate.py # needs resvg on PATH, as packaging/icons.sh
|
||||
```
|
||||
|
||||
2. `DS_Store` only changes if the window size, icon positions or background file name change.
|
||||
Recreate it once on a Mac with [create-dmg](https://github.com/create-dmg/create-dmg) and copy it
|
||||
out of the mounted volume:
|
||||
|
||||
```sh
|
||||
create-dmg --volname "VectorCraft" --background background.tiff --window-size 660 432 \
|
||||
--icon-size 128 --icon "VectorCraft.app" 326 205 --hide-extension "VectorCraft.app" \
|
||||
--app-drop-link 574 205 seed.dmg <folder with VectorCraft.app>
|
||||
hdiutil attach seed.dmg -mountpoint /tmp/seed && cp "/tmp/seed/.DS_Store" DS_Store
|
||||
```
|
||||
|
||||
(660 × 432 includes Finder's 32 pt title bar; the content area is 660 × 400.)
|
||||
It renders the SVG with resvg using only the fonts in `assets/fonts`, so the pixels are the same
|
||||
on every machine, and writes `DS_Store` from scratch: the background alias holds the volume name
|
||||
and `/.background/background.tiff`, nothing from the machine that ran it. The window is 660 × 432
|
||||
with Finder's 32 pt title bar; the content area is 660 × 400.
|
||||
@@ -1,6 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="660" height="400" viewBox="0 0 660 400">
|
||||
<defs><clipPath id="panel"><rect width="232" height="400"/></clipPath><pattern id="hatch" width="2.6" height="2.6" patternUnits="userSpaceOnUse" patternTransform="rotate(-38)">
|
||||
<rect width="2.6" height="1.05" fill="#0b0b0c"/></pattern><filter id="grain" x="0" y="0" width="100%" height="100%">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="660" height="400" viewBox="0 0 660 400">
|
||||
<defs><clipPath id="panel"><rect width="232" height="400"/></clipPath><filter id="grain" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7"/>
|
||||
<feColorMatrix values="0 0 0 0 0.04 0 0 0 0 0.04 0 0 0 0 0.05 0.5 0 0 0 -0.225"/>
|
||||
</filter></defs>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,136 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user