OpenAI-compatible image provider (Azure + standard OpenAI), Generative Fill model picker, and sharper error messages.
New
- OpenAI-compatible provider — wire up any OpenAI-compatible images/edits endpoint: standard OpenAI (Bearer token) or Azure OpenAI (api-key header + deployment URL). Auth style auto-detected from the base URL, user-overridable. Configured in Settings → AI Providers; available for Reimagine.
- Generative Fill model picker — the fill dialog now shows a Model popup (Replicate · FLUX-Fill or Local FLUX-Fill) so the backend is visible and changeable per-call without opening Settings.
- Generative Fill backend section in Settings → AI Providers — persistent backend preference in one place alongside all other provider config.
Under the hood
ProviderError now conforms to LocalizedError — real human-readable messages instead of "ProviderError error 0" in error dialogs.
GenerativeFillService protocol gains needsPrepFill — separates "needs pre-filled bands" from "needs fixed tile size", letting mask-aware backends opt into edge-stretch pre-conditioning for expand mode.
- OpenAI-compatible provider scoped to Reimagine only: confirmed gpt-image-1 architecture regenerates the whole image rather than preserving non-masked pixels, making it unsuitable as a drop-in inpainting backend.
Reimagine Whole Image — describe a change in plain text, get a new layer. Three AI backends: Google Gemini (cloud), FLUX Kontext (on-device, Apple Silicon), and Qwen-Image-Edit (on-device). Live terminal panel shows generation progress as it runs.
New
- AI → Reimagine Whole Image — composites the canvas, sends it to the chosen provider with your prompt, and lands the result as a new raster layer above the active one. Re-roll unlimited times; each result is independently named and stacked.
- Google Gemini — cloud provider, free tier (500 calls/day on gemini-2.5-flash-image). Live model discovery from your API key; model picker updates when you paste a key. Cost badge tracks free quota vs. paid in real time.
- Local FLUX Kontext — on-device via
mflux-generate-kontext (MLX-native, Apple Silicon). FLUX.1-Kontext-dev quantized to 4-bit = ~4 GB memory footprint. Always free; model downloads once to your HuggingFace cache.
- Local Qwen-Image-Edit — on-device via
qwen-image-mps. Apache-2.0 licensed, always free.
- Live terminal panel in the Reimagine sheet — black-background green monospaced log that streams each step as it completes. Stays open between re-rolls so you can read the full history.
- Per-prompt controls for Local FLUX — Strength slider (0.10–1.00) and Steps picker (4 / 8 / 12) directly in the Reimagine sheet. Lower strength for subtle edits; higher for strong transformations.
- Settings → AI Providers — unified pane for all AI providers: API key fields, model pickers, Test buttons, capability badges, and install status for local providers.
- Cancel actually cancels — hitting Cancel kills the in-flight HTTP request or terminates the local subprocess immediately, no zombie processes.
Under the hood
AIImageProvider protocol + AIProviders registry — new providers plug in by conforming to the protocol; every feature that asks "who can reimagine?" discovers them automatically.
ShellEnv — sniffs HF_HOME, HF_TOKEN, and PATH from the user's login+interactive shell (zsh -i -l) at first use, so local models find HuggingFace caches on external drives without manual env config.
QuotaTracker — per-(provider, model, day) call counter drives the real-time cost badge in the Reimagine sheet.
CloudAudit — append-only structured log of every AI call (provider, model, prompt, duration, outcome).
- tqdm progress streamed live: byte-by-byte pipe reader handles
\r-style terminal updates so each denoising step appears in the panel as it finishes.
A real paint engine and the selection toolset to drive it. Pencil, eraser, lasso, polygonal lasso, magic wand, AI Smart Select, and refine edge — plus a Photoshop-style brush ring that follows the cursor.
Paint
- Pencil + Eraser with a stamp-based brush engine — soft round dabs at 10% spacing, configurable Size / Hardness / Opacity / Flow / Smoothing / Color, all live-editable from the tool options bar with text fields and steppers for precise values.
- Photoshop-correct opacity vs flow. Flow controls per-stamp build-up rate within a stroke; opacity is the maximum alpha the stroke can reach, applied as a single global multiplier at commit time. A 30% opacity stroke caps at 30% no matter how many overlapping stamps land.
- Brush smoothing with a 1-pole low-pass filter on the input pointer. End-of-stroke catch-up runs the filter several iterations against the final raw position so the brush doesn't lag short of where you released.
- Brush-radius cursor ring drawn inside the SwiftUI canvas at the live pointer location, sized exactly to the brush's view-pixel footprint at the current zoom. NSCursor image-cursor scaling is unreliable above ~256pt and below ~16pt; the in-canvas overlay is what every pro paint app actually does.
- Rasterize Layer (⌘⇧E) — bake any layer's full appearance (content + filters + adjustments + mask + styles + composite-time text transforms) into a flat raster you can paint on.
Selection
- Lasso — drag a free-form selection. Path closes on release.
- Polygonal Lasso — click-by-click vertices, double-click or click the start point to close. Esc cancels mid-build.
- Magic Wand — flood-fill from a click within sRGB Euclidean tolerance. Contiguous toggle + Tolerance slider in the options bar.
- Smart Select (AI) — click an object and Apple Vision's foreground-instance segmentation turns it into a selection. No model download, runs on Neural Engine. Best on photos with one or more distinct foreground subjects.
- Refine Edge menu — expand or contract the active selection by 1, 5, or 10 px via CoreImage morphology + contour re-extraction.
- Single canonical selection model (
selectionPath: CGPath?) — every selection-aware feature now reads one source of truth in doc top-down coords.
- Selection clipping — paint and erase strokes are clipped to the actual selection shape, not its bounding box (proven by an algorithmic test using a diamond polygon).
- New Select menu with Deselect (⌘⇧D) and Refine Edge submenu.
Under the hood
- 10 algorithmic tests in
TiramisuTests/ — paint stamping, eraser destination-out, selection clipping (rect + polygon), rasterize visual fidelity, flood-fill orientation regression, mask-to-path round-trip, and refine-edge expand/contract symmetry.
- New ControlServer endpoints:
rasterizeLayer and paintStroke (with full brush settings + selection-aware) for headless agent-driven testing.
SelectionTools.swift centralizes flood-fill, Vision contour extraction, Vision foreground segmentation, and morphological refine-edge under one set of pure functions.
PaintEngine.swift uses two CGContexts per stroke — captured base + scratch accumulator — so opacity composition matches Photoshop semantics regardless of stamp count.
- Tool-aware system cursors: crosshair for selection / paint, I-beam for text, custom eyedropper, scope cursor for HSL TAT.
Layer masks land — non-destructive cutouts, mask-aware drop shadows, mask-aware edge cleanup. Plus per-layer rotation for text and 90° quick-rotate buttons.
New
- Layer masks — every layer can now carry a grayscale mask (white = reveal, black = hide). Persisted in the document; backward-compatible with v0.3.x project files.
- Background removal is now non-destructive. Click "Remove Background" and you get a layer mask, not a baked-in alpha cutout — toggle the mask off any time to recover the original photo. Hits Apple Vision sub-second on Apple Silicon.
- Mask thumbnail in the Layers panel next to each layer's content thumbnail. Right-click the row for Add Layer Mask / Invert Mask / Delete Mask.
- Mask follows the layer through every transform — scale, rotation, flip, recenter, layer offset. The same placement math that draws the photo also rasterizes the mask, so the cutout never floats free of its subject.
- Edge cleanup acts on the mask now — Offset / Feather / Threshold sliders give the mask edge the same matting tools that previously baked into the source's alpha.
- Mask-aware layer styles — drop shadow, stroke, and outer glow trace the masked silhouette instead of the photo's rectangular bounding box. Photoshop semantic.
- Text layers gain rotation — slider plus 90° quick-rotate buttons (↺ / ↻ / 180°). Rotation pivots around the anchor point and is applied at composite time, so dragging the slider doesn't re-rasterize the text.
- 90° quick-rotate buttons for smart objects too — the tiny convenience that becomes muscle memory once you have it.
Under the hood
- 9 new tests across two suites: 4 visual goldens (hard-half mask, gradient mask, shadow-follows-edge, white-no-op) plus 5 algorithmic tests pinning luma→alpha conversion, save/reopen roundtrip, and smart-object transform tracking. The tracking test would have caught the v0.4 prerelease bug where masks floated in canvas space when smart objects scaled or recentered.
- New ControlServer endpoints (
setLayerMask, clearLayerMask, invertLayerMask, removeBackground) for headless smoke testing.
BackgroundRemover.mask(from:) replaces the destructive remove(_:) flow. Vision's mask APIs are synchronous, so no async hop is needed and the HTTP control handler doesn't have to juggle main-actor reentrancy.
HSL Targeted Adjustment Tool — click on the photo to scrub the band under the cursor.
New
- Targeted Adjustment Tool (TAT) for the HSL panel — Lightroom's killer feature. Click the scope icon next to the Hue / Saturation / Luminance sub-tabs, then click on the photo and drag up or down. Tiramisu samples the pixel under the cursor, identifies which 1–2 hue bands it belongs to (via the same band-weighting math the renderer's LUT uses), and scrubs those bands' slider for the active channel. No more "which color was that, again?" — point at a sky, drag down, the blue lum slider moves automatically.
- Three exits, all clearly signposted: click the scope again, press Esc, or switch sub-tabs (auto-deactivates so the visible scope state always matches reality).
- Custom scope cursor matches the panel button — black scope glyph with a white halo so it reads on both light and dark photo content. Cursor switches automatically when TAT mode is active.
Under the hood
- One undo entry per click+drag. Pixel sampling decouples from the source bitmap's byte order by drawing a 1×1 cropped CGImage tile into a known sRGB RGBA8 context. Band weights computed once on click and frozen for the drag, so the target doesn't chase its tail as the slider moves.
HSL sliders feel responsive — removed saturation dampening that was muting moves on photo content.
Fixed
- HSL sliders no longer feel mushy. The renderer was multiplying every per-band effect (hue rotation, sat scale, lum scale) by the pixel's saturation — the design rationale was "preserve neutrals" but the side effect was dampening real photo content. A moderately-saturated foliage / sky / skin pixel only felt half the slider; only fully-saturated content (the v0.3.0 cafe-umbrella red) showed an obvious response. Lightroom doesn't dampen this way: a saturation gate (skip true grays) is enough; band weighting handles the rest. Switched to the Lightroom approach. Now every slider feels as responsive as redSat does.
Under the hood
- HSL snapshot tests retargeted to the bands kodim23 actually has content in (the "blue parrot" plumage classifies as aqua, not blue). Added a yellow-band test. Combined "teal-and-orange" demo pushed to slider extremes for a real "look" rather than subtle correction.
HSL hue rotation matches Lightroom convention. Plus algorithmic band-isolation tests.
Fixed
- HSL Hue direction. Positive Hue slider now rotates the band toward lower hue degrees on the standard wheel — green+ → yellow, red+ → magenta, blue+ → aqua. This matches Lightroom's muscle memory; v0.3.0 had it inverted. Caught by the new algorithmic tests below.
Under the hood
- Algorithmic HSL tests. A new
HSLBandMetrics helper classifies each pixel into its single nearest hue band and aggregates per-band mean saturation, luminance, and hue (via unit-vector averaging for proper hue wraparound). Five new tests in HSLBandIsolationTests assert what snapshots can't: that band-targeted slider moves only affect their target band, that effects are monotonic across the slider range, and that the LUT path with all sliders at zero produces near-byte-exact output. Catches band leakage, monotonicity drift, and identity drift — three classes of regression invisible to snapshot diffing.
- The new tests immediately surfaced two findings: (1) the hue-direction bug fixed in this release, (2) kodim23's "blue parrot" plumage is actually aqua/turquoise (~180°), not pure blue (240°), so blue-band tests on this fixture would barely move anything.
Per-color HSL adjustments — Lightroom-style sub-tabs with colored slider tracks for 8 color ranges.
New
- HSL color correction. Eight color ranges (red, orange, yellow, green, aqua, blue, purple, magenta) with three sliders each — Hue (rotates that band's hue ±60°), Saturation (gray ↔ full color), Luminance (darken ↔ brighten). Lives in Adjust → Color (HSL) under three sub-tabs that match Lightroom's muscle memory.
- Colored slider tracks. Each slider's track is a gradient that visualizes what that slider does — Red Hue's track fades through magenta → red → orange so you feel which way you're shifting. Sat tracks go gray → full color, Lum tracks go black → band → white. Center detent line, double-click to reset to zero.
Under the hood
- Implemented as a 32³ RGBA-float lookup table applied via
CIColorCubeWithColorSpace. One GPU pass per layer at composite time; LUT is generated CPU-side from the 24 slider values and cached by parameter hash. Slider drags don't allocate beyond the first unique value the user lands on.
- 6 new snapshot goldens cover identity (proves the LUT path is a no-op at zero), single-band sat/lum/hue moves, and a composite teal-and-orange look. Total snapshot count: 61 → 67.
- ControlServer's
setAdjust extended with hsl.<band>.<channel> keys (e.g. hsl.red.sat, hsl.blue.lum) for headless smoke testing.
Photographer-grade tone curves — 5 preset shapes with an intensity dial.
New
- Tone curve presets. Five named curve shapes — Gentle S, Strong S, Lifted shadows, Crushed shadows, plus Linear (no-op) — with an intensity slider that lerps between linear and the chosen preset. Lives in Adjust → Lighting → Customize, after Highlights. Interactive draggable graph editor coming in v0.4.
Under the hood
- 5 new snapshot goldens: each preset at full intensity plus a half-intensity test that pins the lerp behavior separately from the preset shape itself. Total snapshot count: 56 → 61.
- ControlServer adds
setCurve {preset} and extends setAdjust to take curveIntensity — used in the manual smoke-test pass.
- This release ran through the new Step 1b (manual smoke test) in
RELEASING.md — caught zero new bugs but proved the runbook is now followable.
Per-layer effects now stay inside the layer. Grain, vignette, and noise no longer bleed across the canvas.
Fixed
- Grain bleed. The new film-grain filter (v0.2.1) was applying its multiply-blend across the layer's full canvas extent — over transparent regions too — which made the grain appear to cover everything beneath the layer instead of just the layer's content. Now alpha-masked to the source layer.
- Vignette bleed. Same root cause for vignette darkening — the corner shadow extended past the layer's actual content. Now confined to the layer.
- Noise bleed. The pre-existing flat noise filter (v0.1) had the same structural bug. Now consistent with the others.
- Grain "lower-left anchored" at large sizes. The grainSize scale transform anchored at CI's (0,0) origin (canvas lower-left), making the pattern look stretched from the corner. Now scales around canvas center.
Three creator-favorite adjustments — Vignette, Film Grain, and Vibrance — shipped as a single patch.
New
- Vignette. Radial darkening at the canvas edges with a separate falloff slider for the soft edge. Lives in Adjust → Filters. The fastest way to focus attention on a thumbnail's subject.
- Film grain. Anisotropic noise overlay distinct from the existing flat noise — uses a multiply-blend so highlights stay bright while shadows pick up texture. "Grain size" slider controls particle chunkiness (1.0 = pixel-fine, 4.0 = vintage-film coarse).
- Vibrance. Lightroom-style smart saturation that boosts low-saturation regions more than already-vivid ones — protects skin tones automatically. Lives in Adjust → Lighting between Saturation and Warmth.
Under the hood
- All three new fields decode with backward-compat defaults — existing
.tiramisu documents load unchanged.
- Layer fingerprint hash extended so the render cache invalidates correctly on any new field.
- 6 new snapshot goldens (vignette × 2 strengths, grain × 2 sizes, vibrance × ±0.7). Total snapshot count: 50 → 56.
Inspector redesign + Adjust presets. Editing the chrome to feel as creator-first as the rest of the app.
New
- Adjust presets. Horizontal chip row with seven curated looks — Punchy, Cinematic, Pastel, Faded, Warm, Cool, B&W — plus an Auto Enhance one-click button. The 7 manual sliders moved into a Customize disclosure (collapsed by default) for power users.
- Inspector primitives. Every panel across Properties / Adjust / Effects rebuilt on a shared component library — InspectorSection, InspectorRow, InspectorSlider, InspectorColorWell. Every slider now has a monospaced readout; every label aligns to the same 72pt column.
- Alignment for text layers. The Move tool's 9-point alignment now operates on text layers, not just Smart Objects. Selection bounding box tracks the aligned text.
- Branded welcome window. Replaces the stock NSAlert with a custom SwiftUI window that picks up the marketing-site palette + typography. Shows the three-layer slice mark in full color.
- Real layer thumbnails. Layers panel now shows the actual rendered image for raster + smart-object layers, gradient previews for gradient layers, and a typed "T" for text layers — no more generic gray placeholders.
- Canvas status bar. Zoom controls + document dimensions live in a dedicated strip below the canvas, so they never overlap content. The checkerboard background fills the whole canvas area edge-to-edge.
Fixed
- Transform handles no longer clip at viewport edges when zoomed in.
- Color picker dot on the Text panel no longer overlaps the bold button.
- Tool-options bar buttons now swap by layer kind instead of grey-disabling — text layers get Fit-width / Reset-size; gradient/solid layers show nothing (alignment is meaningless on a fill layer).
Under the hood
- ControlServer extensions for headless UI testing:
GET /window, setInspectorTab, setSection, selectLayerByName, alignLayer, setZoom, clickAt, keystroke.
- New test suites:
LayerArrangeTextTests (alignment math, regression coverage for the stale-bounds bug) and AdjustPresetTests (preset library invariants). 27 tests now archived across 26 runs.
First public-signed build. The DMG is silent on first mount and the .app is silent on first launch.
New
- Brand-matched app icon. Three-layer slice on parchment, accent-orange dot for the cocoa dust.
- Two-finger trackpad pan for the canvas viewport.
- Tool palette visually distinguishes implemented tools from placeholders (pencil/pen/eraser ship in v0.3+).
Tooling
scripts/release.sh — one-command sign + notarize + DMG + publish.
- Both DMG and .app are notarized and stapled, so Gatekeeper is silent at every step.
The bones. Tiramisu becomes a real layered editor with on-device AI.
New
- Layer types: raster (image), text, gradient, solid color, smart object.
- Smart objects with non-destructive transforms + AI edge feathering / matting via Apple Vision.
- 16 blend modes at Photoshop parity, locked in with snapshot tests.
- Generative fill via Replicate (cloud) or Local FLUX-Fill via mflux (on-device).
- Studio relight — depth-aware light positioning, drag on canvas to aim.
- Skin retouch — face-mask-aware smoothing + even-tone + glow.
- Per-layer adjustments: brightness, contrast, exposure, saturation, warmth, shadows, highlights.
- Per-layer effects: drop shadow, outer glow, stroke, gradient fill.
- Rich-text editing with multi-style runs (RichTextKit-backed).
- Canvas presets — YouTube 1280×720, FHD, 2K, 4K UHD, IG, Story, channel banners, profile pics.
- Smart object external editing — double-click to open in any image editor; live update on save.