NotesRelease
Lumeira 1.2.0
Animated GIF, APNG, and animated WebP
Added
- Animated GIF, APNG, and animated WebP playback at the authoring frame rate. Storage is adaptive: animations whose estimated total decoded size is ≤ 64 MB are pre-decoded for instant playback; larger animations decode lazily with a 5-frame LRU ring buffer. Auto-paused when the window isn't key or the view isn't in a window — no background draw loop.
- 28 additional RAW formats in
SupportedFormat: CR2, CRW (Canon legacy), RAF (Fuji), ORF (Olympus/OM System), RW2 (Panasonic), PEF/PTX (Pentax), 3FR/FFF/HIF (Hasselblad), IIQ/CAP/EIP (Phase One), X3F (Sigma), MRW (Minolta), SR2/SRF (Sony pre-ARW), KDC/DCR/DCS/DRF (Kodak), MEF (Mamiya), MOS (Leaf), RWL (Leica), ERF (Epson), ARI (Arri), BAY (Casio), OBM (Olympus alt). Decode capability is bounded by macOS RAW Camera Support; the extensions are claimed for Finder integration. - Embedded-JPEG fallback in
NativeDecoder— whenCGImageSourceCreateImageAtIndexfails for a RAW extension, Lumeira extracts the embedded full-size preview viaCGImageSourceCreateThumbnailAtIndex. Cameras macOS doesn't fully support still display, from the in-file preview. Result is markedDecodedImage.isPreviewOnly. DecodedImagevalue type inLumeiraRenderingcarryingimage,cgImage,bitsPerPixel,isFloat,colorSpace,isHDR,gainMap,isPreviewOnly, and optionalAnimatedFrames. Foundation for accurate cache accounting and the future HDR display work.- ISO 21496-1 gain-map detection for iPhone HDR HEICs via
CGImageSourceCopyAuxiliaryDataInfoAtIndex(..., kCGImageAuxiliaryDataTypeISOGainMap). Detection only — render path is still 8-bit sRGB; full HDR display is tracked indocs/TODO.md. - Smart thumbnail sidebar. Multi-image highlighting in comparison mode (primary slot gets the full accent background, secondary slots get a lighter accent + 1pt stroke). Smart auto-scroll backed by
onScrollVisibilityChange(macOS 15+): each thumbnail reports its visibility and the sidebar tracks the actualvisibleIndicesset. Auto-scroll fires only when the focused image isn't on screen, so manual sidebar scrolling is fully preserved as long as the focused image remains visible. Position indicator (47 / 312) above the scroll view. - Sidebar lock toggle (small lock icon in the sidebar header, next to the position indicator; also reachable via the View menu and the
⇧Lkeyboard shortcut —toggleSidebarLockis a realKeyBindingaction, so it's customizable and conflict-checked like every other shortcut). When locked, the sidebar never auto-scrolls in response to focused-image changes — supports the "scout while navigating" workflow where photographers manually scroll the sidebar to plan ahead while arrow-keying the main viewer through nearby images. Lock state resets to off automatically when a new folder is loaded (lock is a transient per-folder intent, not a persistent preference). Decision logic lives inSidebarScrollDecision.shouldScroll(..., isLocked:), unit-tested inLumeiraUITests. - Thumbnail badges —
RAWandGIFchips overlay the bottom-right of each thumbnail. Derived from URL extension, no decode cost. Accessibility label announces the badges. FolderWatcher(FSEvents-based) — the active folder reconciles automatically when files are added, removed, or renamed outside the app. New files append to the end of the list (forward-biased prefetch); removed files clear from the image cache and thumbnail generator; the user's current scroll position and focused image are preserved when possible.- Debug HUD cache line —
Cache: 312 / 1024 MB · peak 488 MB · 14 entries, refreshed once per second alongside existing stats. docs/COMPARISON.md— feature comparison vs Preview, Photos, Pixea, Phiewer, Xee³, Lyn, and qView with sources.docs/PITCH.md— single-page marketing positioning doc.docs/TODO.md— consolidated roadmap, verify list, deferred work, and explicit non-goals.- Test fixtures for animated formats: 4-frame
animated-test.gif,animated-test.png(APNG),animated-test.webp(regenerated withgif2webpfrom libwebp — seedocs/TODO.md),zero-delay-test.giffor clamp coverage.
Changed
- Image cache budget raised from 512 MB to 1024 MB. Previously the cost formula hardcoded 4 bytes per pixel, undercounting RAW (16-bit) by 2× and float HDR by 4×. With accurate accounting (
width × height × bitsPerPixel / 8) and the wider RAW format list, the 1 GB headroom keeps prefetch responsive without thrashing. ImageCacheAPI — entries now storeDecodedImagerather thanNSImage.set(_:image:)becomesset(_:decoded:);getreturnsDecodedImage?. Cost is computed once at decode time fromcgImage.bitsPerPixel, not estimated fromNSImage.representations.- Edit menu defaults stripped —
Undo / Redo,Cut / Paste,Select All,Find,Spellingdon't apply to a viewer.Copyis kept and copies the current file path to the pasteboard. Window(singular) instead ofWindowGroupfor the main scene, paired with the newLumeiraDocumentControllerbelow. Single-window UX guaranteed regardless of how many URLs arrive from Finder.- App Sandbox re-enabled with
com.apple.security.files.user-selected.read-writegranting access to user-opened folders.
Fixed
- Black flash on SwiftUI binding swap.
MetalImageView.updateTexturepreviously nil'dcurrentTextureand forced a redraw wheneverNSImage.cgImage(forProposedRect:context:hints:)returned nil — which happened transiently during certain SwiftUI binding transitions, painting solid black for a frame. The new path treats main-thread CGImage extraction as a fast path: on success, the pre-extracted CGImage goes straight to the upload queue; on failure, the NSImage is handed to the upload queue's existing path.currentTextureis never speculatively nil'd. - MetalImageView re-parenting.
viewDidMoveToWindownow re-drivesupdateTexturewhen the view is reattached to a window and already has an image. Covers cases where aMetalImageViewinstance is created mid-transition with a stale binding. ViewerState.open(url:)is idempotent. Returns immediately if already on the requested URL and not loading. Defense against duplicate URL delivery paths.
Known issues
- Two-window on Finder file-open (carried over from earlier builds). When the app is already running and the user double-clicks an image in a different folder via Finder, an empty
NSWindowis spawned alongside the real one. The real window does update to the new image; the stray sits next to it. Caused by AppKit'sNSDocumentControllerdocument-opening flow running in parallel with ourAppDelegate.application(_:open:)handler becauseCFBundleDocumentTypesdeclares image UTIs. Workarounds attempted in this release (customNSDocumentControllersubclass) regressed the file-open flow and were reverted. A proper fix is tracked indocs/TODO.md.
Performance
- Concurrent RAW pre-decode in prefetch continues to use
kCGImageSourceShouldCacheImmediately, so navigation lands on a fully decoded frame instead of a "decoding…" placeholder. - Float-decode preserved through the cache.
kCGImageSourceShouldAllowFloat: truestays on so HDR sources retain precision for the eventual HDR pipeline. Render path still down-converts to 8-bit at upload — known interim cost, tracked indocs/TODO.md.
Tests
- LumeiraCore: 62 → 66. Added
expandedRawFormats,threefrCase,isRawDetectsRawExtensions,isLikelyAnimatedDetectsGIF. - LumeiraRendering: 24 → 56. Added
DecodedImagecost calculation suites (8-bit, 16-bit, float),ImageCache Peak Cost(monotonic advancement + reset semantics), Animation Detection (GIF/APNG/WebP + clamp + loop count),AnimationCursor(advancement, loop exhaustion, finished state), RAW embedded-preview fallback (happy path and unable-to-decode garbage path).