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 — when CGImageSourceCreateImageAtIndex fails for a RAW extension, Lumeira extracts the embedded full-size preview via CGImageSourceCreateThumbnailAtIndex. Cameras macOS doesn't fully support still display, from the in-file preview. Result is marked DecodedImage.isPreviewOnly.
  • DecodedImage value type in LumeiraRendering carrying image, cgImage, bitsPerPixel, isFloat, colorSpace, isHDR, gainMap, isPreviewOnly, and optional AnimatedFrames. 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 in docs/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 actual visibleIndices set. 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 ⇧L keyboard shortcut — toggleSidebarLock is a real KeyBinding action, 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 in SidebarScrollDecision.shouldScroll(..., isLocked:), unit-tested in LumeiraUITests.
  • Thumbnail badges — RAW and GIF chips 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 with gif2webp from libwebp — see docs/TODO.md), zero-delay-test.gif for 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.
  • ImageCache API — entries now store DecodedImage rather than NSImage. set(_:image:) becomes set(_:decoded:); get returns DecodedImage?. Cost is computed once at decode time from cgImage.bitsPerPixel, not estimated from NSImage.representations.
  • Edit menu defaults stripped — Undo / Redo, Cut / Paste, Select All, Find, Spelling don't apply to a viewer. Copy is kept and copies the current file path to the pasteboard.
  • Window (singular) instead of WindowGroup for the main scene, paired with the new LumeiraDocumentController below. Single-window UX guaranteed regardless of how many URLs arrive from Finder.
  • App Sandbox re-enabled with com.apple.security.files.user-selected.read-write granting access to user-opened folders.

Fixed

  • Black flash on SwiftUI binding swap. MetalImageView.updateTexture previously nil'd currentTexture and forced a redraw whenever NSImage.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. currentTexture is never speculatively nil'd.
  • MetalImageView re-parenting. viewDidMoveToWindow now re-drives updateTexture when the view is reattached to a window and already has an image. Covers cases where a MetalImageView instance 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 NSWindow is spawned alongside the real one. The real window does update to the new image; the stray sits next to it. Caused by AppKit's NSDocumentController document-opening flow running in parallel with our AppDelegate.application(_:open:) handler because CFBundleDocumentTypes declares image UTIs. Workarounds attempted in this release (custom NSDocumentController subclass) regressed the file-open flow and were reverted. A proper fix is tracked in docs/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: true stays 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 in docs/TODO.md.

Tests

  • LumeiraCore: 62 → 66. Added expandedRawFormats, threefrCase, isRawDetectsRawExtensions, isLikelyAnimatedDetectsGIF.
  • LumeiraRendering: 24 → 56. Added DecodedImage cost 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).