Skip to content

Latest commit

 

History

147 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

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 or a folder. ⌘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.
  • PreviewV 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.
  • 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

⌘D — File ▸ Derive LUT from JPG…. Pick the RAW, pick the JPEG, hit Derive.

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….

The report is a tone curve (R/G/B vs identity) plus saturation, sharpening, coverage, samples, alignment, and camera EXIF. Sharpening is measured, not applied — a cube can't sharpen, and there isn't a second stage that does.

How the cube is built

The RAW is developed with the same default CIRAWFilter settings the rest of the app uses, so the LUT applies without a baseline mismatch. Both images are Lanczos-scaled onto a shared working extent (long edge capped at 3000 px — 200k samples don't get better from a 60 MP buffer) and aligned by luma cross-correlation. An edge mask on the JPEG keeps in-camera sharpening out of the color samples. Surviving pixels (~200k, from a 2M draw) fill a 33³ cube; empty cells are pulled from neighbors, then identity.

Shortcuts

Preview

Key Action
previous / next LUT
or [ ] previous / next image
Space (hold) original, in single view
V side-by-side / single
⌘I inspector

File

Key Action
⌘O open image
⌘⇧I Photos
⌘⌥I source folder
⌘R rescan source folder
⌘⇧L LUT folder
⌘D derive
⌘S export
⌘⇧E export all
⌘, settings — launch defaults and the sidebar's collapsed folders

Letter keys go through SwiftUI's .onKeyPress on the split view; the preview canvas is focusable and holds focus by default so the handler always has a focused descendant. shortcuts go through the menu bar.

Build

swift run           # launch
open Package.swift  # same binary, Xcode debugger
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.

  • Run — macOS 26
  • Compile — Xcode 27 / macOS 27 SDK

Deployment target and SDK are different things. The compiler rejects API newer than 26 unless it's #available(macOS 27, *)-guarded, and a macOS 27 symbol has to be in the SDK before it can be referenced at all — #available does not conjure a missing symbol — so Xcode 26 can't build the package. The binary still runs on 26.

swift test generates fixtures into a temp directory. Tests that need a real RAW/JPEG pair look in realworldtest/ (gitignored) and skip if it isn't there. LUTZY_BENCH=1 for the preview-cost tests. CI is debug build → test → release build on GitHub's xcode-27 runner.

Layout and render path

Everything of substance is in Sources/LUTzyKit. Sources/LUTzy is @main plus an AppDelegate that forces .regular activation, because a bare executable otherwise starts as a background process with no Dock icon. Only ContentView and LUTzyCommands are public, so the kit can be @testable imported.

The look is an EditDocument (develop + adjustments + LUT). Preview, histogram, and both export paths render that document through one RenderEngine actor / one CIContext. Preview vs export differs only by scale: 1600×1200 vs full. WorkingSpace (sRGB) is used for both cube interpolation and encoding. Non-RAW files go through ImageDecoder.orientedLoadOptions because CIImage(contentsOf:) ignores EXIF orientation and CIRAWFilter doesn't.

Swift 6 language mode on every target. No @unchecked Sendable, nonisolated(unsafe), or @preconcurrency.

Pipeline notes: docs/PHASE2_SPEC.md. Standing review: docs/CODE_REVIEW.md.

License

MIT

About

Native macOS app for color-grading RAW/DNG photos with .cube LUTs — and reverse-engineering a camera's JPEG look back into one. SwiftUI + Core Image, zero dependencies.

Topics

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages