Export the photograph, not the canvas

Zooming the develop view changed the exported file. `Framing::view` is
kept out of the sidecar, out of `is_active` and out of `output_size`
precisely so that it cannot — but those exclusions keep it out of the
*edit*, and an export is a *render*. `visible_rect` deliberately folds
the view into the single rect the fused shader's prologue samples, so
`render_for_export` inherited it: at 4:1 it wrote the middle of the
frame, magnified to fill the file at the full output size, with the
detail kernels scaled four times over because `render_scale` folds the
view in as well. `render_thumbnail` did the same to the grid.

`render_uncropped` already suspends the view for this exact reason, so
the fix is its pattern: one `render_the_file` that both file-producing
paths go through, composing inside the suspension since the view
reaches the shader as a uniform baked at composition time. Restored
whatever happens — leaving the graph un-zoomed after a failed export
would throw away where the photographer was looking.

Nothing caught it because the guard checked the wrong things.
`zooming_does_not_change_the_exported_image` asserted the output size
and the crop; both held perfectly throughout. Renamed to
`zooming_does_not_change_the_size_or_the_crop`, which is what it
tests, and the pixels are now guarded where pixels exist. The new test
uses a ramp rather than quadrants deliberately: a four-quadrant frame
is self-similar under a centred zoom, and the first version of this
test passed against the bug because of it.

Traces FR-EXP-9, which asks for the full-quality pipeline "regardless
of what the display was showing".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 19:41:30 +02:00
co-authored by Claude Opus 5
parent a1e361e35a
commit 3d248cfb79
4 changed files with 213 additions and 35 deletions
+24 -6
View File
@@ -348,9 +348,19 @@ pub struct Framing {
/// Which part of the framed image the viewport is looking at.
///
/// **Not an edit.** Zooming changes what you are inspecting, never what
/// the file becomes: it is excluded from [`Self::is_active`], from the
/// structure hash, and from the sidecar, so a zoomed view exports exactly
/// as an unzoomed one does.
/// the file becomes, so it is excluded from [`Self::is_active`], from the
/// structure hash, and from the sidecar.
///
/// **That is not on its own enough to make an export ignore it**, and
/// this doc comment used to claim it was. The exclusions keep the view out
/// of the *edit* — out of what is saved, out of the output size, out of
/// the crop. They cannot keep it out of a *render*, because
/// [`Self::visible_rect`] deliberately folds it into the one rect the
/// shader samples. Anything composing this framing and rendering it gets
/// the zoom; a caller that wants the photograph rather than the canvas
/// has to suspend the view first, as `DevelopSession::render_the_file`
/// and `render_uncropped` both do. Exporting at 4:1 wrote the middle of
/// the frame, magnified, until it did.
///
/// It lives here rather than in the UI because it composes with the crop
/// in the same normalised space — nesting one rect inside the other is a
@@ -1252,10 +1262,18 @@ mod tests {
}
#[test]
fn zooming_does_not_change_the_exported_image() {
fn zooming_does_not_change_the_size_or_the_crop() {
// The property that makes zoom a viewing tool rather than an edit: it
// must not reach the output size or the crop. If it did, exporting
// while zoomed would write the zoomed view.
// must not reach the output size or the crop.
//
// **Renamed, because the old name promised more than the body checks
// and the gap was where a real bug lived.** "Does not change the
// exported image" was read as a guarantee about pixels; it is a
// guarantee about two numbers. Both held perfectly while
// `render_for_export` was writing the zoomed view at full size,
// because the view reaches the render through `visible_rect` and
// never through either of these. The pixels are guarded where pixels
// exist — `dr-ui`'s `export_ignores_the_viewport`.
//
// The structure key is deliberately not asserted here — see
// `zooming_from_neutral_changes_the_structure_key` for why it must