Files
pdfcraft/crates/render
b36bdaeac6 M2: a shrunk image on a turned or mirrored page snaps to whole pixels (#5530) (#5640)
* M2: a shrunk image on a turned or mirrored page snaps to whole pixels (#5530)

hayro patch 14 (`snap_minified_image`) shrinks an interpolated image once
to its whole-pixel device rectangle and draws that 1:1, instead of
shrinking to a rounded-up size and resampling again at a fractional
scale and offset. It only accepted an upright transform (a > 0, d > 0,
b = c = 0), so every image on a page under /Rotate 90, 180 or 270 -- and
any image placed turned or mirrored by its own matrix -- still took the
double resample. That is #5530: a scanned page that matches PDFium
upright drifts and softens when the same page is rotated.

The snap now accepts every transform whose axes lie along the device's
(the eight quarter-turns and mirrors) and returns the 1:1 transform
that draws the shrunk raster from the snapped corner, one source-axis
step a whole device pixel, in whichever direction that axis runs.
Skewed and freely rotated images keep the old path.

On a synthetic page (crop box and scale on whole pixels, image at
fractional coordinates), /Rotate 90, 180 and 270 renders turned back
differed from the upright render in 72% of pixels, by up to 200 grey
levels; now they are identical. Non-interpolated images were already
identical and stay so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M2: snap only when the dropped skew is invisible, and only a resize that succeeded

Review of #5640:

- The snap judged an image axis-aligned by a tolerance on its
  coefficients (1e-4 of the scale). For a very wide image a skew that
  small still moves the far edge by more than a pixel (40 000 samples
  at 4e-5: 1.6 px), and the snap flattened it. It now measures what it
  drops: how far each off-axis term moves the far edges, at most a
  hundredth of a device pixel (SNAP_SKEW). Upright and turned alike.
- A resize that fails hands back the source raster at source size; the
  1:1 snapped transform then drew it at that size. The snapped
  transform is now used only when the shrink produced the snapped
  size; otherwise the full transform, as before.
- The mirrored case of the edge test drew a uniform image, which looks
  the same mirrored; it now also mirrors a half-black image and checks
  which side the black lands on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M2: the turned-page test draws every kind of image the snap reaches

The /Rotate oracle test drew one 8-bit grey image. Stencil masks (CCITT
scans often are one), 1-bit images and images with a soft mask reach
the snap through other paths, so it now draws all five: 8-bit and
1-bit grey, a stencil mask, a soft mask of the image's size and one of
another size. On main, every interpolated kind is off by 113-217 levels
over 63-73% of the image when turned, and so is a non-interpolated
image with a smaller soft mask (110 levels, 16%): hayro patch 15
filters such a mask whatever /Interpolate says, so it is snapped
upright and was not when turned. All are identical with the fix; the
other non-interpolated kinds were already identical and stay as guards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 15:07:29 -05:00
..