Peek the negative as loaded - #889
Merged
Merged
Conversation
Every canvas view is a print: the base stage converts the linear source to normalized log density and the exposure transfer curve inverts it. Peek Flat Scan inverts too, so there was no way to look at the negative itself. Judging whether a scan is thin, dense, colour-cast or clipped meant exporting a Linear Output TIFF and opening it elsewhere. Add a Peek Negative toggle (toolbar button, overflow entry, N) that paints AppState.preview_raw: un-inverted, un-metered, un-normalized, none of the tone edits. Geometry is the exception and runs through GeometryProcessor and CropProcessor, the same two the base and crop stages use, so the frame keeps its rotation, flip, straighten, keystone and crop; the crop tool still peeks the uncropped frame, or there would be nothing to frame a crop against. Only working_oetf_encode follows, being a display encode rather than an edit, since a linear buffer would otherwise show as near-black. The buffer is in camera primaries, so it is marked splash to keep the working-to-display matrix and the soft proof off it, and content_rect is cleared because no border stage ran to inset the picture. No pipeline or shader change: no config expresses "do not invert", and a new RenderIntent would mean the CPU engine, the WGSL shaders and a parity test for a diagnostic that needs none of them. This follows the splash path instead. The state is transient, mutually exclusive with the flat peek and the before/after split, and any render with no config override drops it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every canvas view is a print. The base stage converts the linear source to normalized log density and the exposure transfer curve (
features/exposure/transfer.py) inverts it; Peek Flat Scan inverts too, sinceRenderIntent.FLATonly drops the creative stages. So there was no way to look at the negative itself.When a conversion looks wrong, the first question is whether the scan is wrong — thin or dense frame, what colour the mask really is, whether the scanner clipped. Answering it meant exporting a Linear Output TIFF and opening it elsewhere.
What
A Peek Negative toggle: toolbar button, overflow entry,
N. It paintsAppState.preview_raw— un-inverted, un-metered, un-normalized, none of the tone edits — and returns to the normal render when turned off.Geometry is the one thing it does apply, through
GeometryProcessorandCropProcessor, the same two the base and crop stages use, so the frame keeps its rotation, flip, straighten, keystone and crop. The crop tool is the exception and peeks the uncropped frame, mirroringrequest_render, or there would be nothing to frame a crop against.Only
working_oetf_encodefollows. That is a display encode rather than an edit — a linear buffer would otherwise show as near-black.Approach
Not a pipeline change and not a
config_overriderender. No config expresses "do not invert", and adding aRenderIntentwould mean the CPU engine, the WGSL shaders and a parity test for a diagnostic that needs none of them. This follows the splash path (_on_splash_preview) instead: write the buffer intolast_metrics["base_positive"], mark itsplash, emitimage_updated.Two consequences worth calling out:
splash, so the working-to-display matrix and the soft proof stay off it. The buffer is in camera/scanner primaries, not the working space; expect the raw orange cast rather than an accurate one.content_rectcleared. It marks the picture area inside a border/mat, and no finish stage ran, so leaving the previous render's value would inset the frame wrongly once a border is on.The state is transient, mutually exclusive with the flat peek and the before/after split, and any render with no config override drops it.
Verification
make allgreen. Nine controller tests added, plusnegative_peek=Falseon threeSimpleNamespacestate stubs that modelAppState.Driven headless against
samples/20260619SP_EKTAR100_120_1_09_ME_4000PPI.tif, peek vs. print at each geometry setting:The strong negative correlation is the point: same picture, inverted. Also confirmed the orange mask reads in the raw negative (R=0.416, G=0.243, B=0.164) and that any edit drops the peek. Colour Negative and B&W Negative give identical peek buffers, as they must — the peek sits upstream of the pipeline, so process mode cannot reach it.
One unrelated observation
While checking the crop case I found the CPU and GPU engines disagree on a
crop_rectset directly on the config:Still uncropped on the GPU after a fresh load with the crop already in the config, so it is not stale cache in the harness. It may be that poking
crop_rectprogrammatically is not the supported path and the real crop tool sets state the GPU keys on. Pre-existing and untouched by this PR; the peek follows the CPU engine, which is the parity reference. Flagging it rather than chasing it here.Docs
docs/USER_GUIDE.mdgets the panel prose;docs/PIPELINE.mdrecords that the peek is not a stage of its own and what it skips.