M2: area-average shrunk scans and smooth enlarged ones

Scanned pages looked thin and broken at screen size. An image that does not
ask for /Interpolate (most scans) was pre-shrunk with nearest-neighbour since
about one sample in ten and dropped most strokes of its text, and the nearest
sampler then read the shrunk raster again at a fractional offset.

- Shrinking (vendored hayro patch 21): non-interpolated images outside Type 3
  glyphs are area-averaged (area_shrink: each device pixel the
  coverage-weighted mean of the samples it covers, 16.16 fixed point whose
  weights sum to exactly one, premultiplied when masked) to their snapped
  device rectangle and drawn 1:1. /Interpolate governs smoothing when an image
  is enlarged (ISO 32000-2 8.9.5.3); averaging adds nothing between samples
  and drops none. PicScale's Box/Area filters were not usable: shrinking, they
  average a window four target pixels wide.
- Enlarging (patch 28): an image of page-scan size (512+ samples on its
  shorter side) is drawn bilinearly when enlarged, so a zoomed scan has no
  staircase edges; icons, QR codes and pixel art keep their hard pixels.
- A snapped image whose shrink failed keeps the full transform instead of
  being drawn at its source size (latent in the interpolated path too).

Measured on a 13-page 300 dpi scan at 96, 120 and 192 dpi: every rendered
sample is within 1 of the exact area average (it was up to 255 off, 17-23%
of samples by more than 1). Bilinear zoom costs nothing measurable (four
1024 px tiles at 400%: 50 ms, nearest 48 ms; bicubic was 267 ms).

Review fixes:

- a tile snaps a shrunk image's half-pixel edges where the whole page does
- a scan shrunk one way is smoothed the way it is stretched
- a smaller soft mask doesn't let transparent colour through the area shrink

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kronbiiandClaude Opus 5.5 committed 2026-11-04 11:32:52 +02:00
1 parent a4d148bf48
commit ff227b770c
6 files changed
+571 -66

No files matched your search

+2 -2
View File
@@ -734,8 +734,8 @@ milestone = "M2"
status = "partial"
commands = []
tools = ["page_render"]
evidence = ["crates/render/src/raster/tests.rs::renders_a_blue_rectangle", "crates/automation/tests/it/automation.rs::open_inspect_render_and_find", "crates/render/src/raster/tests.rs::user_unit_scales_the_page_and_everything_on_it"]
notes = "hayro bootstrap renderer; own DisplayList devices in M2.6. Pages with /UserUnit are shown at their size in points, as Acrobat reads the value"
evidence = ["crates/render/src/raster/tests.rs::renders_a_blue_rectangle", "crates/automation/tests/it/automation.rs::open_inspect_render_and_find", "crates/render/src/raster/tests.rs::user_unit_scales_the_page_and_everything_on_it", "crates/render/src/raster/tests.rs::non_interpolated_minification_keeps_thin_strokes", "crates/render/src/raster/tests.rs::minified_image_edges_snap_to_whole_pixels", "crates/render/src/raster/tests.rs::large_non_interpolated_images_are_smoothed_when_enlarged", "crates/render/src/raster/tests.rs::small_non_interpolated_images_keep_hard_pixels_when_enlarged"]
notes = "hayro bootstrap renderer; own DisplayList devices in M2.6. Pages with /UserUnit are shown at their size in points, as Acrobat reads the value. An image drawn below its resolution is shrunk once to whole device pixels: by area averaging unless it asks for /Interpolate (then Catmull-Rom), so the text of a 300 dpi scan at 100% keeps its strokes (it was nearest-neighbour, which dropped them). Enlarged, an image of page-scan size (512 samples or more on its shorter side) is smoothed bilinearly whatever its /Interpolate, so a zoomed scan has no staircases; smaller images keep their hard pixels. Not measured against Acrobat"
[[feature]]
id = "view.render-shadings-patterns"