From 15c35620c4048e649a05b8bb3c73776f723e7c8f 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 CADCraft colour, with CADCraft.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; ~21 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 | 34 +++++ packaging/macos/dmg/background.tiff | Bin 0 -> 22304 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, 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 b4e240a..6cb7179 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -37,6 +37,8 @@ Generated-in-code assets are original and have no file to list: | `assets/app-icon/cadcraft-256.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | | | `assets/app-icon/cadcraft-1024.png` | CADCraft contributors | generated | MIT OR Apache-2.0 | | | `assets/app-icon/cadcraft-macos-512.png` | CADCraft contributors | generated (macOS margin variant) | MIT OR Apache-2.0 | | +| `packaging/macos/dmg/background.svg` | @XusBadia | original work for this repository (2026-10-08), "CADCraft macOS DMG window background"; derived from the CADCraft app icon (`assets/app-icon/`, referenced via ``, not copied); text is outlined Inter / JetBrains Mono glyph paths (OFL fonts the project already uses); no third-party material modified | MIT OR Apache-2.0 (same as the code: `LICENSE-MIT`, `LICENSE-APACHE`) | Finder window background of the macOS DMG; `packaging/macos/dmg/generate.py` renders it to `background.tiff` (see `packaging/macos/dmg/README.md`) | +| `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (same as the code: `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") | | `examples/apartment.dxf` | CADCraft contributors | sample drawing made with cadcraft-cli commands | MIT OR Apache-2.0 | Original | | `docs/images/ui-apartment.png` | CADCraft contributors | screenshot of CADCraft itself (examples/apartment.dxf) | MIT OR Apache-2.0 | Original; no Autodesk UI | | `docs/images/ui-layout.png` | CADCraft contributors | screenshot of CADCraft itself (a layout of examples/apartment.dxf) | MIT OR Apache-2.0 | Original; no Autodesk UI | diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md new file mode 100644 index 0000000..feaf4f1 --- /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/cadcraft-1024.png`, linked, not copied) cropped as a cover on the CADCraft colour field (`#14a3c7`), 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, CADCraft.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 `CADCraft`, not `CADCraft `. 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 (`cadcraft--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..67f8699 --- /dev/null +++ b/packaging/macos/dmg/background.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/macos/dmg/background.tiff b/packaging/macos/dmg/background.tiff new file mode 100644 index 0000000000000000000000000000000000000000..d889a9a4e53cd13f1752ef28c1ec671e4168a00b GIT binary patch literal 22304 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 F{{s)xFW3M8 literal 0 HcmV?d00001 diff --git a/packaging/macos/dmg/dmg-layout.DS_Store b/packaging/macos/dmg/dmg-layout.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c35363bcfa2ffd9b62e8589a7d4e226a69f428c5 GIT binary patch literal 16388 zcmeI2%}*0S6u{psphZ5q>QRz>@8RVI%zSXj6+i^~?a(}AFh2oM1xKm>>Y5g-CY zfCvx)B0vO)01+SpM1TnVj|9|z`$==ltC+Sh5y5M-Vq2b{N*yw-rM2zENm(MRdS6yJ zzF|4s&EFGSd8^1x!@bj>-wkx%@T*=X9GN%F1&eP5R`WvG=&^j;a*ARrBdSi(!)XV1 zcE(bvk@%Id$=#9o&cp=jm&eC;cO(5Hle4#0)@^lLSeW;N_NLo}Pb^^4FliMw!w6d1 zYY}|VqhSn@_ER05UENVV7K`=t_Vx9j9vB!L8eTG7*Ko=_v%RYKnnq#0?8<#*g)Q8S zUF8^cQ}1<`b7xiWY3jbki$`p88%CK=+m_+2)Gg7AOyt&jp(9yeM<$Xnu^W2D8g!L69>V>fuWf&-!Ep-elZ2b? z#*A_lOfaAT>re(41XRI+A|$~F3rcuJp$FH69J;hvtrbJ&Licnj~~5I(_I z_{MtKAUmf$(jIHiv=`b-ZC`t(y;0s;?X7V}wQIF@fEvCvHuJ7y3#S~k06H;gG>Ia?IM4*^V~Ygf_!y}U;<5-gp+a7m&!&UDm|%{KW3&QuWrB0vO)01+Sp oM1Tko0U|&IhyW2F0z`la5CI}U1c(3;AOb{y2oM1x@DB<601>(zZ2$lO literal 0 HcmV?d00001 diff --git a/packaging/macos/dmg/generate.py b/packaging/macos/dmg/generate.py new file mode 100644 index 0000000..b2b75c3 --- /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 = "CADCraft" +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 = {"CADCraft.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 9b01e9e..203dc20 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -131,10 +131,15 @@ STAGE="$WORK/dmg" mkdir -p "$STAGE" ditto "$APP" "$STAGE/CADCraft.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 "CADCraft $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 "CADCraft" -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"