diff --git a/README.md b/README.md
index 1321a5db..447a0ac7 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,29 @@
+
+
# LUTzy
A macOS app that applies `.cube` LUTs to RAW and other images. It can also build a LUT from a RAW + JPEG pair.
SwiftUI and Core Image, no third-party packages. macOS 26 to run, [Xcode 27 to compile](#build).
+A 3D LUT is a table with a colour at every lattice point. The photo's colours are looked up in it, interpolated between the nearest points, and replaced. That is the whole trick, and it is why a LUT can be a look but never a sharpen or a blur.
+
+
+
## Features
- **RAW** via `CIRAWFilter`, not the embedded preview.
DNG, CR2, CR3, NEF, ARW, ORF, RAF, RW2, PEF, SRW, X3F, RAW.
JPEG, PNG, TIFF, BMP, HEIC.
-- **Import** — drop a file, a folder, several files, or pictures straight from Photos. ⌘O file · ⌘⇧I Photos (max 50) · ⌘⌥I source folder.
-- **LUTs** — `.cube` 3D (`LUT_3D_SIZE`, `DOMAIN_MIN` / `DOMAIN_MAX`) through `CIColorCubeWithColorSpace` on Metal. Sidebar scans recursively, groups by subfolder, searchable. Intensity 0–100%. ⌘⇧L picks the folder.
+- **Import** — drop a file, a folder, several files, pictures straight from Photos, or a bitmap from any app. ⌘O file · ⌘⇧I Photos (max 50) · ⌘⌥I source folder.
+- **LUTs** — `.cube` 3D (`LUT_3D_SIZE`, `DOMAIN_MIN` / `DOMAIN_MAX`) through `CIColorCubeWithColorSpace` on Metal. Sidebar scans recursively, groups by subfolder, searchable. Intensity 0–100%, in the Adjust inspector. ⌘⇧L picks the folder.
- **Preview** — V side-by-side / single. Hold Space in single view for the original. ↑↓ walk the library.
- **Inspector** ⌘I
- **Info** — histogram of what's on screen (graded, or original while Space is down) plus EXIF / TIFF / GPS
- **Develop** — the `CIRAWFilter` knobs this file's decoder actually supports
- **Adjust** — nine sliders after develop, before the LUT: exposure, brightness, contrast, saturation, highlights, shadows, temperature, tint, vibrance
- **Filmstrip** when a source folder is open. ←→ or [] step through; the current look stays on. ⌘R rescans.
+- **Window** — Liquid Glass toolbar, customizable (View ▸ Customize Toolbar…). The file and LUT names sit in the title. ⌘, sets launch defaults: side-by-side, source browser, export format.
- **Export** — 16-bit TIFF, JPEG (quality 0.95), or PNG, always full resolution, named `{photo}_{LUT}.ext` (spaces in the LUT name become underscores). ⌘⇧E Export All writes the whole look — develop, adjustments, LUT, intensity — and counts failures instead of aborting.
## Derive LUT from JPG
@@ -25,11 +32,7 @@ SwiftUI and Core Image, no third-party packages. macOS 26 to run, [Xcode 27 to c
The JPEG is treated as a look (the manufacturer's color science, or whatever picture profile was on). LUTzy writes the difference against a neutral RAW develop. Same frame required — aspect within 1%. Pixel size can differ.
-```
-RAW ──► CIRAWFilter (neutral) ─┐
- ├─► align ─► smooth samples ─► 33³ cube ─┬─► .cube
-JPEG ─► decode ─► edge mask ────┘ └─► report
-```
+
The result previews on the current image and stays in memory until **Save to LUT Folder…**.
@@ -81,7 +84,7 @@ swift test
> [!IMPORTANT]
> `swift run` and Run from Xcode both produce a SwiftPM executable, not a sandboxed `.app`. LUT folder and source folder do not persist across launches.
-There is no `.xcodeproj`. `Package.swift` excludes `Assets.xcassets` and `LUTzy.entitlements`; the appiconset is empty; there is no `Info.plist` or bundle identifier. The entitlements file is real (sandbox, user-selected files, app-scoped bookmarks) and unused. An Xcode app target would apply it. This repo doesn't have one.
+There is no `.xcodeproj`. `Package.swift` excludes `Assets.xcassets` and `LUTzy.entitlements`, and a SwiftPM executable has no `Info.plist` or bundle identifier, so `swift run` uses neither the icon in the appiconset nor the entitlements file. The release script below applies the icon and an `Info.plist`. The entitlements (sandbox, user-selected files, app-scoped bookmarks) are real and still unused; wiring them up needs an Xcode app target this repo doesn't have.
- **Run** — macOS 26
- **Compile** — Xcode 27 / macOS 27 SDK
@@ -103,6 +106,18 @@ Swift 6 language mode on every target. No `@unchecked Sendable`, `nonisolated(un
Pipeline notes: [docs/PHASE2_SPEC.md](docs/PHASE2_SPEC.md). Standing review: [docs/CODE_REVIEW.md](docs/CODE_REVIEW.md).
+## Release
+
+```bash
+DEVELOPER_ID_APP="Developer ID Application: Your Name (TEAMID)" \
+NOTARY_PROFILE="your-notarytool-profile" \
+scripts/release-dmg.sh 0.1.1
+```
+
+Does what an Xcode archive would: universal release build, a hand-written `LUTzy.app` with an `Info.plist` and the icon, signed with Developer ID and the hardened runtime (no sandbox yet), notarized and stapled, then packed into a DMG that is notarized and stapled again. Output lands in `build/release/`. Needs `create-dmg` (`brew install create-dmg`) and a notarytool keychain profile.
+
+The icon is drawn by `scripts/render-icon.swift`: the RGB cube a `.cube` file indexes, seen from its green corner, rendered into every slot of the appiconset. Run it again rather than editing the PNGs.
+
## License
[MIT](LICENSE)
diff --git a/docs/images/derive.svg b/docs/images/derive.svg
new file mode 100644
index 00000000..fee08c87
--- /dev/null
+++ b/docs/images/derive.svg
@@ -0,0 +1,29 @@
+
\ No newline at end of file
diff --git a/docs/images/lut-cube.svg b/docs/images/lut-cube.svg
new file mode 100644
index 00000000..cb939c64
--- /dev/null
+++ b/docs/images/lut-cube.svg
@@ -0,0 +1,159 @@
+
\ No newline at end of file
diff --git a/scripts/render-readme-graphics.py b/scripts/render-readme-graphics.py
new file mode 100755
index 00000000..e67fc402
--- /dev/null
+++ b/scripts/render-readme-graphics.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+# Draws docs/images/lut-cube.svg and docs/images/derive.svg for the README. Run from the package
+# root; standard library only. Regenerate rather than hand-editing the SVGs.
+import math, colorsys
+INK="#8E8E93"; INK2="#6E6E73"
+SANS="-apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Arial, sans-serif"
+MONO="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
+
+AZ,EL=math.radians(38),math.radians(24)
+def proj3(x,y,z,cx,cy,e):
+ x,y,z=x-.5,y-.5,z-.5
+ ca,sa=math.cos(AZ),math.sin(AZ); x,z = x*ca+z*sa, -x*sa+z*ca
+ ce,se=math.cos(EL),math.sin(EL); y,z = y*ce-z*se, y*se+z*ce
+ return cx+x*e, cy-y*e, z
+def proj(x,y,z,cx,cy,e):
+ px,py,_=proj3(x,y,z,cx,cy,e); return px,py
+
+def look(r,g,b):
+ # a film-ish look: S-curve, teal shadows, warm highlights, slight desat
+ def s(v): return v*v*(3-2*v)
+ r,g,b = s(r),s(g),s(b)
+ lum = 0.2126*r+0.7152*g+0.0722*b
+ shadow = (1-lum)**2; hi = lum**2
+ r += 0.18*hi - 0.12*shadow; g += 0.06*hi + 0.02*shadow; b += -0.10*hi + 0.22*shadow
+ h,l,sat = colorsys.rgb_to_hls(*[min(1,max(0,v)) for v in (r,g,b)])
+ r,g,b = colorsys.hls_to_rgb(h,l,sat*0.85)
+ return tuple(min(1,max(0,v)) for v in (r,g,b))
+
+def cube(cx,cy,e,n,f,out):
+ V={'000':(0,0,0),'100':(1,0,0),'001':(0,0,1),'101':(1,0,1),'010':(0,1,0),'110':(1,1,0),'011':(0,1,1),'111':(1,1,1)}
+ C={k:proj3(*v,cx,cy,e) for k,v in V.items()}
+ far=min(C,key=lambda k:C[k][2])
+ edges=[(a,b) for a in V for b in V if a')
+ for a,b in edges:
+ if far in (a,b): line(a,b,True)
+ dots=[]
+ for i in range(n):
+ for j in range(n):
+ for k in range(n):
+ r,g,b=f(i/(n-1),j/(n-1),k/(n-1))
+ px,py,depth=proj3(r,g,b,cx,cy,e)
+ dots.append((depth,px,py,r,g,b))
+ dots.sort()
+ for depth,px,py,r,g,b in dots:
+ col='#%02x%02x%02x'%tuple(int(round(v*255)) for v in (r,g,b))
+ rad=4.6+1.6*(depth+0.87)/1.74
+ out.append(f'')
+ for a,b in edges:
+ if far not in (a,b): line(a,b,False)
+
+def lut_cube_svg():
+ W,H=720,318; out=[]
+ out.append(f'')
+ return '\n'.join(out)
+
+def derive_svg():
+ W,H=720,150; out=[]
+ out.append(f'')
+ return '\n'.join(out)
+
+open('docs/images/lut-cube.svg','w').write(lut_cube_svg())
+open('docs/images/derive.svg','w').write(derive_svg())
+print("written")