From 617888baebd0fd54cb2822df24da8f207498f0c5 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 SoundCraft colour, with SoundCraft.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; ~28 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 | 1 + docs/releasing.md | 3 + packaging/macos/dmg/README.md | 44 +++++++ packaging/macos/dmg/background.svg | 31 +++++ packaging/macos/dmg/background.tiff | Bin 0 -> 29216 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, 243 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 69f0f0b..084f424 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -38,6 +38,7 @@ and, at runtime, the operating system's own UI fonts when available (never redis | `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | | `docs/brand/artcraft-mark-black.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | | `assets/app-icon/icon.svg` | SoundCraft contributors | original (hand-placed SVG paths; placeholder app icon) | MIT OR Apache-2.0 | +| `packaging/macos/dmg/` (all files) | @XusBadia | original (macOS DMG window background and layout, from the app icon; see `packaging/macos/dmg/README.md`) | MIT OR Apache-2.0 | | `assets/app-icon/soundcraft-1024.png` | SoundCraft contributors | original, rendered from `icon.svg` | MIT OR Apache-2.0 | | `assets/app-icon/soundcraft-macos-512.png` | SoundCraft contributors | original, rendered from `icon.svg` | MIT OR Apache-2.0 | | `assets/app-icon/soundcraft.icns` | SoundCraft contributors | original, rendered from `icon.svg` | MIT OR Apache-2.0 | diff --git a/docs/releasing.md b/docs/releasing.md index 2f7894d..e68b9a5 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -94,6 +94,9 @@ plugins), and WebAudio in the browser. - **Notarization:** the app is zipped and sent with `xcrun notarytool submit --wait`, then the ticket is stapled to the app. The app goes on a DMG (`hdiutil`, with an `Applications` link). The DMG is signed, notarized and stapled too. + Its Finder window (background, icon size and positions) comes from + [`packaging/macos/dmg/`](../packaging/macos/dmg/README.md), and its volume is named `SoundCraft` + without the version, which the window's background needs; the DMG file name keeps the version. - **CLI:** the universal `soundcraft-cli` is signed with the hardened runtime, zipped and notarized. diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md new file mode 100644 index 0000000..5bbc169 --- /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/icon.svg`, linked, not copied) cropped as a cover on the SoundCraft colour field (`#14a9c4`), 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, SoundCraft.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 `SoundCraft`, not `SoundCraft `. 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 (`soundcraft--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..2f07687 --- /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..feeb2f90402a225dc3bdc20b39f37cf2131fa35e GIT binary patch literal 29216 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' 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 = "SoundCraft" +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 = {"SoundCraft.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 60b7ac0..a898e3d 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -129,10 +129,15 @@ STAGE="$WORK/dmg" mkdir -p "$STAGE" ditto "$APP" "$STAGE/SoundCraft.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 "SoundCraft $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 "SoundCraft" -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"