From 21f1703994eeec6d9fa58e0a5a7e73207b63801a 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 DeckCraft colour, with DeckCraft.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; ~18 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 + packaging/macos/dmg/README.md | 44 +++++++ packaging/macos/dmg/background.svg | 31 +++++ packaging/macos/dmg/background.tiff | Bin 0 -> 18736 bytes packaging/macos/dmg/dmg-layout.DS_Store | Bin 0 -> 16388 bytes packaging/macos/dmg/generate.py | 158 ++++++++++++++++++++++++ packaging/macos/package.sh | 7 +- 7 files changed, 241 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 3318f2e..d46a54b 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -18,6 +18,8 @@ deck and UI icons are original work of the DeckCraft contributors (drawn or gene | `docs/brand/artcraft-mark-black.svg`, `docs/brand/artcraft-mark-black.png` | ArtCraft Team | ArtCraft brand kit | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Mark for light backgrounds | | `docs/brand/LICENSE-brand.txt` | ArtCraft Team | craftrules `standards/license-files` | — (licence text) | Terms for the ArtCraft marks | | `assets/app-icon/deckcraft-1024.png`, `assets/app-icon/hicolor/*/apps/ai.storyteller.deckcraft.png` | DeckCraft contributors | rendered by DeckCraft from `assets/app-icon/icon-source.deckcraft` (shapes only) | MIT OR Apache-2.0 | Placeholder app icon (presentation board) until the mascot icon is drawn | +| `packaging/macos/dmg/background.svg` | @XusBadia | DeckCraft macOS DMG window background, original work for this repository (2026-10-08), derived from the DeckCraft app icon `assets/app-icon/deckcraft-1024.png` (referenced via ``, not copied); text is outlined Inter / JetBrains Mono glyph paths | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | Finder window of the macOS DMG; `packaging/macos/dmg/generate.py` renders it to `background.tiff` | +| `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/deckcraft.icns`, `assets/app-icon/deckcraft.ico` | DeckCraft contributors | generated from the PNGs above by `packaging/icons.sh` (sips/iconutil, `cargo xtask ico`) | MIT OR Apache-2.0 | macOS and Windows app icons | | `assets/app-icon/icon-source.deckcraft` | DeckCraft contributors | made with `deckcraft-cli run` (see git history) | MIT OR Apache-2.0 | Source of the placeholder icon | | `docs/images/editor.png`, `docs/images/shape-format.png`, `docs/images/format-pane.png`, `docs/images/slide-sorter.png`, `docs/images/dark-mode.png`, `docs/images/sample-deck.png` | DeckCraft contributors | Screenshots of DeckCraft and slides rendered by `deckcraft-cli` from the built-in sample deck (original content; Inter font, OFL-1.1) | MIT OR Apache-2.0 | README screenshots | diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md new file mode 100644 index 0000000..6f217cf --- /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/deckcraft-1024.png`, linked, not copied) cropped as a cover on the DeckCraft colour field (`#f26b1d`), 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, DeckCraft.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 `DeckCraft`, not `DeckCraft `. 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 (`deckcraft--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..2c0b696 --- /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..99502550c81362b7ebca7807109b87e0d755c332 GIT binary patch literal 18736 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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..1b2ef30 --- /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 = "DeckCraft" +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 = {"DeckCraft.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 26fdbe5..4eed67a 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -131,10 +131,15 @@ STAGE="$WORK/dmg" mkdir -p "$STAGE" ditto "$APP" "$STAGE/DeckCraft.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 "DeckCraft $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 "DeckCraft" -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"