Show the file's own pixels at 1:1 and beyond

Zoomed to 1:1 or past it, the develop canvas showed a smoothed blur
rather than the photograph's pixels, so focus and noise could not be
judged at the magnification meant for judging them.

Two things caused it. The canvas only switched to nearest-neighbour
strictly past 1:1, with a margin, so the 1:1 inspection itself stayed
smooth. And the switch mostly had nothing to act on: the pipeline
rendered a viewport-sized frame at every zoom, so past 1:1 it was the
pipeline doing the enlarging - bilinearly whenever a straightening angle
or lens correction was in the chain - and the detail stage then sharpened
and denoised those invented pixels at radii scaled up to match. The
texture reached the canvas already blurred and was presented 1:1.

Now, from 1:1 on, the visible region is rendered at the source's own
resolution (render::render_size) and the canvas enlarges it with
nearest-neighbour, so the blocks on screen are the pixels an export would
have; it is also less shading. The decision lives in two small
functions, render::magnification and render::shows_source_pixels,
measured in physical pixels like one_to_one_zoom, with a half-percent
tolerance so the inspection zoom counts as 1:1 even where fit() rounded
the other edge. Below 1:1 the render and the smooth filter are unchanged.
This commit is contained in:
2026-09-24 21:24:57 -04:00
parent 5569a066ff
commit 229def0afc
5 changed files with 353 additions and 98 deletions
+24 -11
View File
@@ -146,7 +146,8 @@ export component AppWindow inherits Window {
in property <float> zoom: 1.0;
in property <bool> zoomed: false;
/// Whether one source pixel now covers more than one screen pixel.
/// Whether one source pixel now covers one screen pixel or more — 1:1 or
/// closer, in physical pixels.
///
/// Drives the canvas's filtering, and nothing else. Rust decides it rather
/// than the `zoom` property above, because the two are not the same
@@ -1609,20 +1610,27 @@ in property <bool> panel-visible: true;
height: 100%;
source: root.canvas;
image-fit: contain;
// Past 1:1 there is no detail left to reconstruct, so
// TRACES: FR-UI-4
// From 1:1 on there is no detail left to reconstruct, so
// smoothing only invents values between real pixels — and
// inspecting focus or noise is the whole reason to zoom in
// that far. Below 1:1 it stays smooth, where filtering is
// what keeps the image from aliasing.
//
// This is the only enlargement there is. Past 1:1 the
// pipeline renders the visible region at the source's own
// resolution (`render::render_size`) and `contain`
// stretches it to the box, so the blocks on screen are the
// file's pixels and not ones the pipeline interpolated.
//
// TRACES: FR-DSP-8
// The canvas is rendered at the *physical* pixel size of
// the box this image occupies, so `contain` presents it
// 1:1 and neither filter is reached at all until the
// photographer zooms. That is the point: on a fractionally
// scaled desktop the buffer used to be a logical-sized one
// that the compositor stretched, and no choice of filter
// recovers detail that was never rendered.
// Below 1:1 the canvas is rendered at the *physical* pixel
// size of the box this image occupies, so `contain`
// presents it 1:1 and the filter barely matters. That is
// the point: on a fractionally scaled desktop the buffer
// used to be a logical-sized one that the compositor
// stretched, and no choice of filter recovers detail that
// was never rendered.
image-rendering: root.magnified
? ImageRendering.pixelated
: ImageRendering.smooth;
@@ -1712,7 +1720,9 @@ in property <bool> panel-visible: true;
// pointer, so the gesture reads as magnifying
// the picture rather than sliding it about.
// Double-tap is the way to an exact 1:1; this
// is the way to everything in between.
// is the way to everything in between. Past
// 1:1 the pixels are shown as they are, square
// and unsmoothed; below it, filtered.
//
// TRACES: FR-UI-4
// Pinch to zoom. The wheel gesture below is the desktop's
@@ -2303,7 +2313,10 @@ in property <bool> panel-visible: true;
// the next photograph, which is what
// makes checking the same eye across
// forty portraits forty keystrokes
// rather than forty pans.
// rather than forty pans. From 1:1 on
// the photograph is drawn as its own
// pixels, each a hard-edged square,
// rather than smoothed into a blur.
//
// TRACES: FR-UI-4 | FR-UI-5
// Fit and 1:1 from the keyboard, so inspecting