From be33eba7d1101fe7427a08adb99994c7d76ec9de Mon Sep 17 00:00:00 2001 From: Xus Badia Date: Thu, 8 Oct 2026 15:12:32 +0200 Subject: [PATCH] macOS DMG: branded window background and icon layout The DMG opened as a plain Finder window. It now shows the app icon's figure on the GridCraft colour, with GridCraft.app and the Applications link side by side. Same approach as PhotoCraft's and VectorCraft's (storytold/photocraft#1201, storytold/vectorcraft#493). - packaging/macos/dmg/: background.svg (source; text outlined, no paper grain, the figure an of assets/app-icon), background.tiff (1x + 2x HiDPI, 16-colour palette, Deflate, sRGB; ~26 KB) and dmg-layout.DS_Store (Finder view settings: window, icon size 128, positions, background). - generate.py writes the TIFF and the layout on any OS, byte-for-byte reproducible, with nothing from the machine that ran it (no paths, users, disk names or volume UUIDs). - package.sh copies them into the staging folder; the image is still built with hdiutil makehybrid. Nothing extra runs in the signing job. - The volume name drops the version: the layout finds the background through an alias that includes the volume name. The DMG file name keeps the version. Co-Authored-By: Claude Opus 5.5 --- ATTRIBUTION.md | 2 + docs/releasing.md | 3 + packaging/macos/dmg/README.md | 44 +++++++ packaging/macos/dmg/background.svg | 31 +++++ packaging/macos/dmg/background.tiff | Bin 0 -> 27312 bytes packaging/macos/dmg/dmg-layout.DS_Store | Bin 0 -> 16388 bytes packaging/macos/dmg/generate.py | 158 ++++++++++++++++++++++++ packaging/macos/package.sh | 7 +- 8 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 packaging/macos/dmg/README.md create mode 100644 packaging/macos/dmg/background.svg create mode 100644 packaging/macos/dmg/background.tiff create mode 100644 packaging/macos/dmg/dmg-layout.DS_Store create mode 100644 packaging/macos/dmg/generate.py diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 35a5d7d..36678d8 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -18,6 +18,8 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra | `docs/brand/artcraft-mark-black.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark | | `docs/brand/LICENSE-brand.txt` | (licence text) | craftrules | — | | | `assets/app-icon/gridcraft.svg` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | PLACEHOLDER app icon (ledger page); replaced by the owner's creature art later | +| `packaging/macos/dmg/background.svg` | @XusBadia | original (hand-written SVG, derived from the GridCraft app icon `assets/app-icon/gridcraft.svg`, referenced via ``, not copied; text is outlined glyph paths; `packaging/macos/dmg/generate.py` renders it to `background.tiff`) | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | GridCraft macOS DMG window background for the Finder window; added 2026-10-08 | +| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 1x + 2x HiDPI TIFF for the macOS DMG window; includes the app icon (see its row) and LittleCMS's built-in sRGB profile ("No copyright, use freely") | | `assets/app-icon/gridcraft-1024.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, rendered from gridcraft.svg | | `assets/app-icon/gridcraft-macos-512.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, macOS grid render | | `assets/app-icon/gridcraft.icns` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, macOS bundle icon | diff --git a/docs/releasing.md b/docs/releasing.md index 352409e..f4a28eb 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -101,6 +101,9 @@ and upload their artifacts, the environment refuses macOS and Windows, and no re - **Notarization:** the app is zipped and sent with `xcrun notarytool submit --wait`, then stapled. The DMG (with an `Applications` link) is signed, notarized and stapled too, and checked with `codesign --verify --strict`, `stapler validate` and `spctl -a -vvv`. + Its Finder window (background, icon size and positions) comes from + [`packaging/macos/dmg/`](../packaging/macos/dmg/README.md), and its volume is named `GridCraft` + without the version, which the window's background needs; the DMG file name keeps the version. - **CLI:** the universal `gridcraft-cli` is signed, zipped and notarized. Locally, without certificates, the script signs ad-hoc (`codesign -s -`) and skips notarization: diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md new file mode 100644 index 0000000..0a6233d --- /dev/null +++ b/packaging/macos/dmg/README.md @@ -0,0 +1,44 @@ +# macOS DMG window + +What Finder shows when the DMG opens: a 660 × 400 pt background with the app icon and the +`Applications` link side by side. `package.sh` copies these files into the image; nothing here is +generated at build time, so the DMG still builds with `hdiutil makehybrid` (no mounted device, no +Finder scripting on CI, nothing extra installed in the signing job). + +| File | What | +|---|---| +| `background.svg` | Source of the background: the app icon (`assets/app-icon/gridcraft.svg`, linked, not copied) cropped as a cover on the GridCraft colour field (`#1f9d55`), Ink and Paper. Its text (Inter, JetBrains Mono) is outlined, so rendering it needs no fonts. No paper grain: noise doesn't compress, and this keeps `background.tiff` small. | +| `background.tiff` | The background at 1x (660 × 400 px, 72 dpi) and 2x (1320 × 800 px, 144 dpi) in one HiDPI TIFF (16-colour palette, Deflate, sRGB). Goes to `.background/background.tiff`. | +| `dmg-layout.DS_Store` | Finder's view settings for the volume: window size, icon size 128, GridCraft.app at (326, 205), `Applications` at (574, 205), and the background. Goes to `.DS_Store` in the image; named so it isn't mistaken for (or ignored like) a Finder-generated `.DS_Store`. | +| `generate.py` | Writes `background.tiff` and `dmg-layout.DS_Store` from the SVG and the layout above. | + +## The volume name has no version + +The mounted volume is called `GridCraft`, not `GridCraft `. The layout points at the background +through an alias that includes the volume name, and Finder resolves it by that name: with a +versioned name the window keeps its size and icon positions but shows no background (tested). +The DMG file name (`gridcraft--macos-.dmg`) still carries the version. + +## Rules + +- **Finder draws the icon labels in black in light and dark mode** when a window has a background, + so the area under both icons stays light (Paper). +- **Nothing goes inside the icon boxes:** artwork keeps 10 pt clear of each 128 pt icon box and of + the label strip under it. + +## Regenerate + +Edit `background.svg` (or the layout constants in `generate.py`), then run, on any OS: + +```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 +``` + +Both outputs are byte-for-byte reproducible and carry nothing from the machine that ran it: the +SVG renders without any fonts, and `dmg-layout.DS_Store` is written from scratch, its background +alias holding only the volume name and `/.background/background.tiff`. If you add text to the SVG, +outline it (`usvg` from resvg converts text to paths). The window is 660 × 432 with Finder's 32 pt +title bar; the content area is 660 × 400. + +This follows VectorCraft's `packaging/macos/dmg/` (storytold/vectorcraft#493). diff --git a/packaging/macos/dmg/background.svg b/packaging/macos/dmg/background.svg new file mode 100644 index 0000000..b4c2661 --- /dev/null +++ b/packaging/macos/dmg/background.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/macos/dmg/background.tiff b/packaging/macos/dmg/background.tiff new file mode 100644 index 0000000000000000000000000000000000000000..f7e54fe18234e055a10661d18f1ebfb7a7e0bff5 GIT binary patch literal 27312 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 >uqmDD<7rIyRhqfl(@ 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 literal 0 HcmV?d00001 diff --git a/packaging/macos/dmg/generate.py b/packaging/macos/dmg/generate.py new file mode 100644 index 0000000..a168759 --- /dev/null +++ b/packaging/macos/dmg/generate.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Regenerate background.tiff and dmg-layout.DS_Store for the macOS DMG window (see README.md here). + +Needs resvg on PATH 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__)) + +# Keep in sync with package.sh (volume name) and README.md (layout). +VOLUME = "GridCraft" +WIDTH, HEIGHT = 660, 400 # window content, pt; the background's 1x size +TITLE_BAR = 32 +ICON_SIZE = 128 +PALETTE = 16 # colours in background.tiff +ICONS = {"GridCraft.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): + # The SVG's text is already outlined and no fonts are loaded, so every machine draws the + # same pixels. + subprocess.run( + ["resvg", "--skip-system-fonts", "-w", str(width), svg, out], + check=True, + ) + with Image.open(out) as im: + return im.convert("RGB") + + +def quantize(im): + # No dithering: dither noise compresses badly and the artwork has flat fills. + return im.quantize(PALETTE, method=Image.Quantize.FASTOCTREE, dither=Image.Dither.NONE) + + +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 = ( + quantize(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 16-colour palette, Deflate: the artwork is three inks plus antialiasing, and a palette + # keeps the file under ~100 KB (repository assets must stay small). As PhotoCraft's. + 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)") + strips = [] + for page in (0, 1): + im.seek(page) + strips += zip(im.tag_v2[273], im.tag_v2[279]) # StripOffsets, StripByteCounts + # TIFF keeps directories and tag data on even offsets. When a strip ends on an odd one, libtiff + # leaves the padding byte after it uninitialised, so the file changed from run to run with + # identical pixels. Zero it: nothing refers to that byte. + starts = {o for o, _ in strips} + with open(out, "r+b") as f: + for o, n in strips: + if (o + n) % 2 and o + n not in starts: + f.seek(o + n) + f.write(bytes(1)) + + +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 --locked)") + write_tiff(os.path.join(HERE, "background.svg"), os.path.join(HERE, "background.tiff")) + write_ds_store(os.path.join(HERE, "dmg-layout.DS_Store")) + + +if __name__ == "__main__": + main() diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index 976db94..b128c3c 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -131,10 +131,15 @@ STAGE="$WORK/dmg" mkdir -p "$STAGE" ditto "$APP" "$STAGE/GridCraft.app" ln -s /Applications "$STAGE/Applications" +# Finder window layout: background, icon size and positions (packaging/macos/dmg/README.md). +mkdir -p "$STAGE/.background" +cp "$HERE/dmg/background.tiff" "$STAGE/.background/background.tiff" +cp "$HERE/dmg/dmg-layout.DS_Store" "$STAGE/.DS_Store" rm -f "$DMG" "$WORK/raw.dmg" # makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, # which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions. -hdiutil makehybrid -hfs -hfs-volume-name "GridCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" +# The volume name has no version: .DS_Store finds the background through an alias that includes it. +hdiutil makehybrid -hfs -hfs-volume-name "GridCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" rm -f "$WORK/raw.dmg" sign "$DMG"