Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,15 @@ linux/flutter/ephemeral/
!test/fixtures/*.nef
!test/fixtures/*.dng

# But don't commit locally-copied personal photos. The numbered fixtures are
# populated from a developer's own photo library (see CLAUDE.md) to run the
# histogram / brightness regression tests across a variety of real scenes.
# They stay local only because they contain personal EXIF and would bloat the
# repo. The tests that use them skip gracefully when they're missing.
test/fixtures/test_image_*.arw
test/fixtures/test_image_*.raf
test/fixtures/test_image_*.cr2
test/fixtures/test_image_*.nef
test/fixtures/test_image_*.dng

ROADMAP.md
83 changes: 83 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# CLAUDE.md

Context notes for future Claude (or human) sessions on this repo.

## Highlight recovery mode

LibRaw's `params.highlight` is currently hardcoded to `blend` (value 2) at
`lib/models/image_state.dart` via the `_highlightMode` constant. The full
machinery to expose it as a user preference is in place:

- `lib/models/highlight_mode.dart` — `HighlightMode` enum (clip / blend / reconstruct).
- `lib/ffi/raw/raw_processor_common.c` — native setter. For blend/reconstruct,
auto-bright is left **off** (`no_auto_bright = 1`) and a fixed **+1.0 EV
exposure correction** is applied via `params.exp_correc = 1`,
`params.exp_shift = 2.0`, `params.exp_preser = 1.0`.
Rationale (measured across 5 fixtures — Sony ARW + Fuji RAF):
- `params.highlight = 2` (blend) alone darkens the image by ~45% on
average because it pulls clipped-channel pixels below the output max.
Opening a picture that suddenly looks dark is jarring.
- LibRaw's auto-bright with default `auto_bright_thr = 0.01` re-creates
the same 1%-of-pixels pile-up at 255 that blend was meant to eliminate.
- `auto_bright_thr = 0` is scene-adaptive but wildly inconsistent: −6%
on a sky photo, −47% on a dark low-key scene.
- `params.bright` (output-space multiplier) brightens but loses highlight
detail to clipping — an image with a lamp in a dim room gets a new
90k-pixel spike at 255.
- `exp_shift` with `exp_preser = 1.0` acts in linear light *before*
gamma, compressing highlights instead of clipping them. +1.0 EV
(exp_shift = 2.0) brings blend within -15% to +7% of clip's brightness
across all tested fixtures, with zero re-introduced spikes.

An earlier version used +0.7 EV (matching darktable's default), which
left the image still ~25% darker than clip. +1.0 EV is the sweet spot
where most scenes are indistinguishable from clip at import.
- `lib/services/raw_processor.dart` — plumbed through `loadRawFile(..., highlightMode:)`.

When a real settings panel is introduced:

1. Re-add `getHighlightMode` / `setHighlightMode` to `PreferencesService`
(they were removed because nothing called them).
2. Load the preference in `ImageState` and expose a `setHighlightMode()`
method that persists + reloads the current image.
3. Add the selector UI (radio list worked fine).
4. **Reconstruct mode is experimental**: it produces a noticeably darker
image than clip/blend because libraw's auto-bright is computed from raw
data stats and doesn't see reconstruct's downstream attenuation. A fixed
`bright = 2.25` multiplier roughly compensated on the test fixture but is
scene-dependent and therefore unprincipled. If reconstruct is exposed,
either calibrate a per-image gain (measure post-decode max, scale to 255)
or label it clearly as experimental.

## Exposure math

`Exposure` adjustments use linear-light EV: the CPU LUT in
`lib/services/optimized_processor.dart:generateExposureLUT` and the GPU path in
`linux/vulkan_processor/shaders/image_process.comp:applyExposure` both do
`sRGB decode -> * 2^EV -> sRGB encode`. An earlier version did the naive
`byte * 2^EV`, which wildly over-amplified midtones because sRGB is
gamma-encoded. If you touch either function, keep them in sync.

## Native RAW processor build paths

The canonical C source is `lib/ffi/raw/raw_processor_common.{c,h}`. Three
consumers include/compile it:

- Production Linux build (CMake) — `linux/raw_processor/raw_processor_wrapper.c`
which `#include`s `../../lib/ffi/raw/raw_processor_common.c`.
- Test libraries (`scripts/build_test_libs.sh`) — same wrapper.
- Flatpak (`dev.myyc.aks.yaml`) — same wrapper.
- macOS (`macos/raw_processor/raw_processor.c`) — **standalone copy**, not
wrapped. If you change the common source, mirror it here too or convert
macOS to use a wrapper the same way.

All three Linux paths were consolidated onto the wrapper in the
`refactor/cleanup-and-imagestate` branch; prior to that, stale duplicates
at `linux/raw_processor/raw_processor.c` kept drifting.

## Histogram

`lib/widgets/histogram_widget.dart`: the painter caps the 0 / 255 edge bins
at `3 × maxValue` for display so clipped pixels don't dominate the chart.
That's deliberate visualisation, not a bug. The sampling stride is 2D
(`sqrt(cropPixels / target)`), not 1D — see the comment at line 108.
43 changes: 20 additions & 23 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# Analyzer configuration.
# See https://dart.dev/guides/language/analysis-options

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

analyzer:
# Generated FFI bindings contain many unused helper declarations; they are
# not hand-maintained so excluding them keeps the signal-to-noise ratio high.
exclude:
- lib/ffi/raw/libraw_bindings.dart
- lib/ffi/jpeg/jpeg_bindings.dart

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
# Inherited from flutter_lints. Explicit entries below enable extras or
# disable defaults that produce too much noise in this codebase.
unused_import: true
unused_local_variable: true
unused_field: true
unused_element: true
avoid_empty_else: true
prefer_final_locals: true
# Many existing debug log sites use print(). These will be migrated to
# debugPrint over time; silencing avoids drowning real issues.
avoid_print: false
9 changes: 4 additions & 5 deletions dev.myyc.aks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,13 @@ modules:
# Verify the app was pre-built
- test -f build/linux/x64/release/bundle/aks || (echo "Error: Please run 'flutter build linux --release' first" && exit 1)

# Copy raw_processor_common.h to linux directory for Flatpak build
- cp lib/ffi/raw/raw_processor_common.h linux/raw_processor/

# Build raw_processor library with statically linked LibRaw
# Build raw_processor library with statically linked LibRaw.
# Wrapper includes lib/ffi/raw/raw_processor_common.c (canonical source).
- echo "Building raw_processor with static LibRaw..."
- |
gcc -shared -fPIC -o libraw_processor.so \
linux/raw_processor/raw_processor.c \
linux/raw_processor/raw_processor_wrapper.c \
-Ilib/ffi/raw \
-I/app/include \
-L/app/lib \
-Wl,-Bstatic -lraw -Wl,-Bdynamic \
Expand Down
14 changes: 14 additions & 0 deletions lib/ffi/raw/libraw_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ class LibRawBindings {
late final _raw_processor_process = _raw_processor_processPtr
.asFunction<int Function(ffi.Pointer<ffi.Void>)>();

void raw_processor_set_highlight_mode(
ffi.Pointer<ffi.Void> processor,
int mode,
) {
return _raw_processor_set_highlight_mode(processor, mode);
}

late final _raw_processor_set_highlight_modePtr = _lookup<
ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>, ffi.Int)>
>('raw_processor_set_highlight_mode');
late final _raw_processor_set_highlight_mode =
_raw_processor_set_highlight_modePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>, int)>();

ffi.Pointer<RawImageData> raw_processor_get_rgb(
ffi.Pointer<ffi.Void> processor,
) {
Expand Down
124 changes: 0 additions & 124 deletions lib/ffi/raw/raw_processor.dart

This file was deleted.

34 changes: 34 additions & 0 deletions lib/ffi/raw/raw_processor_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,40 @@ int raw_processor_open(void* processor, const char* filename) {
return LIBRAW_SUCCESS;
}

void raw_processor_set_highlight_mode(void* processor, int mode) {
if (!processor) return;
libraw_data_t* lr = (libraw_data_t*)processor;
if (mode < 0) mode = 0;
if (mode > 9) mode = 9;
lr->params.highlight = mode;

// clip (mode 0): no compensation. This is the reference brightness.
//
// blend / reconstruct redistribute clipped channel values below the
// output maximum, which systematically darkens the image (-45% on avg
// without any compensation). Compensate with libraw's exposure
// correction feature: +1.0 EV (exp_shift = 2.0) in linear light with
// exp_preser = 1.0 so highlights are *compressed* rather than re-clipped.
//
// Measured across 5 fixtures (Sony ARW + Fuji RAF, sky / indoor / low-key
// scenes), blend+1EV lands within -15% to +7% of clip's mean brightness,
// with zero re-introduced 255-pile-up. This is close to imperceptible
// compared to the -25% delta we'd get with the darktable-style +0.7 EV.
//
// We use exp_shift rather than params.bright (output-space multiplier)
// because exp_shift acts in linear light before gamma, so exp_preser can
// keep highlights from re-clipping. A plain bright multiplier just
// rescales all byte values and loses highlight detail.
lr->params.no_auto_bright = 1;
if (mode != 0) {
lr->params.exp_correc = 1;
lr->params.exp_shift = 2.0f;
lr->params.exp_preser = 1.0f;
} else {
lr->params.exp_correc = 0;
}
}

int raw_processor_process(void* processor) {
if (!processor) {
snprintf(last_error, sizeof(last_error), "Invalid processor");
Expand Down
10 changes: 10 additions & 0 deletions lib/ffi/raw/raw_processor_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ void raw_processor_free_exif(ExifData* exif);
void raw_processor_cleanup(void* processor);
const char* raw_processor_get_error();

// Configure highlight handling for the next call to raw_processor_process().
// Values map to libraw's params.highlight:
// 0 = clip (default, sharp 255 pile-up on clipped channels)
// 1 = unclip (preserves channel ratios; can produce magenta highlights)
// 2 = blend (lerp clipped channel toward unclipped mean)
// 3-9 = rebuild/reconstruct (iterative; higher = more expensive)
// Any unrecognised value is clamped to [0, 9]; call this after
// raw_processor_open() but before raw_processor_process().
void raw_processor_set_highlight_mode(void* processor, int mode);

#ifdef __cplusplus
}
#endif
Expand Down
2 changes: 0 additions & 2 deletions lib/models/adjustments.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:convert';

/// Base class for all image adjustments
abstract class Adjustment {
final String type;
Expand Down
1 change: 0 additions & 1 deletion lib/models/crop_state.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'dart:ui' as ui;

/// Represents the crop region in normalized coordinates (0.0 to 1.0)
class CropRect {
Expand Down
Loading
Loading