mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 04:58:34 -06:00
M2: snap shrunk interpolated images to whole pixels so scans stay sharp (#1377)
* M2: snap shrunk interpolated images to whole pixels so scans stay sharp A scanned page (one 300 dpi image with /Interpolate true) was shrunk to a ceil-sized raster and then sampled a second time at a fractional scale and offset, which softened every edge next to Acrobat. An axis-aligned shrunk interpolated image is now shrunk once to its rounded device rectangle and drawn 1:1 at the rounded corner. Rotated, skewed, flipped, Type 3 and non-interpolated images are unchanged. On a synthetic scan page the mean error against an area-average reference falls from 3.4 to 1.6 grey levels at 96 dpi (3.0 to 1.5 at 144 dpi). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * vendor: list the hayro snap patch as (14) in the Patches and Tests columns; correct gaps.md patch counts The patch description had been appended to the Tests column of one of the two hayro rows. gaps.md now counts the distinct patches vendor/README.md lists (26 hayro-interpret, 15 hayro, 3 hayro-jbig2, 17 hayro-syntax). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
186c1dc767
commit
88d96c93a8
@@ -4741,6 +4741,41 @@ trailer << /Root 1 0 R >>
|
||||
assert!(pixels(&enabled).iter().any(|px| px[..3].iter().any(|&c| (32..=223).contains(&c))), "/Interpolate true made no mid-tone");
|
||||
}
|
||||
|
||||
/// A shrunk, interpolated image whose edges fall between device pixels is snapped to whole
|
||||
/// pixels (shrunk once to that size, drawn 1:1). Before, the shrunk raster was sampled a second
|
||||
/// time at the fractional offset, so every edge of a scan was a blended, soft pixel.
|
||||
#[test]
|
||||
fn interpolated_minified_image_edges_snap_to_whole_pixels() {
|
||||
let black = vec![0u8; 40 * 40];
|
||||
let content = "q 10 0 0 10 0.4 0.4 cm /Im0 Do Q";
|
||||
let image = hex(&black);
|
||||
let pdf = format!(
|
||||
"%PDF-1.4\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 11 11] /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
5 0 obj << /Type /XObject /Subtype /Image /Width 40 /Height 40 /ColorSpace /DeviceGray /BitsPerComponent 8 /Interpolate true /Filter /ASCIIHexDecode /Length {} >> stream\n{image}>\nendstream endobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF\n",
|
||||
content.len(),
|
||||
image.len() + 1
|
||||
);
|
||||
let mut renderer = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default());
|
||||
let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, scale: 1.0, ..Default::default() });
|
||||
assert!(page.error.is_none(), "{:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (11, 11));
|
||||
let gray = |x: usize, y: usize| page.rgba[(y * 11 + x) * 4];
|
||||
// Device box 0.4..10.4 × 0.6..10.6 snaps to columns 0..10 and rows 1..11.
|
||||
for y in 1..11 {
|
||||
for x in 0..10 {
|
||||
assert_eq!(gray(x, y), 0, "pixel ({x}, {y}) inside the snapped image");
|
||||
}
|
||||
assert_eq!(gray(10, y), 255, "column 10 is outside the snapped image (row {y})");
|
||||
}
|
||||
for x in 0..11 {
|
||||
assert_eq!(gray(x, 0), 255, "row 0 is outside the snapped image (column {x})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_interpolated_gray_images_are_not_smoothed_when_minified() {
|
||||
let levels = [0u8, 255, 85, 170, 40, 200, 120, 230];
|
||||
|
||||
Reference in New Issue
Block a user