From f65f7330418e8121ebe59c01003da899340ceee9 Mon Sep 17 00:00:00 2001 From: Piohrun Date: Fri, 16 Oct 2026 01:50:29 +0200 Subject: [PATCH] =?UTF-8?q?fix(render):=20faster=20sampled=20functions,=20?= =?UTF-8?q?image=20decodes=20across=20zoom=20levels=20and=20patch=20meshes?= =?UTF-8?q?=20(vendored=20hayro-interpret=20patches=2031=E2=80=9333)=20(#2?= =?UTF-8?q?436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(render): sampled functions index a flat sample array (vendored hayro-interpret patch 31) A Type 0 sampled function kept its samples in a HashMap keyed by each point of its /Size grid (two SmallVecs per key, SipHash), and the renderer builds a shading's functions again every time it is painted, so every paint hashed one insert per sample. A 2,097,152-sample ramp painted 30 times took 38 s; the pdf.js corpus's bug1721218 (InDesign) spent most of a 1.5 s page on it. The samples now stay in stream order in one Vec, and a grid point is found by strides (first input fastest). The interpolation keeps the same recursion and arithmetic, so results are bit for bit the same, and a point the stream ended before still gives no value. The repeated-paint page draws in well under a second. The maximum sample value no longer computes 2^32 in a u32 (a panic with overflow checks on at 32 bits per sample). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ * fix(render): one decode of an image serves every size it is drawn at (vendored hayro-interpret patch 32) The decoded-image cache (patch 24) keyed each decode by the size the image is drawn at, but only the JPX filter decodes a reduced resolution for a size: every other filter decodes the whole image. So a JPEG or Flate scan missed the cache at every zoom step, was decoded again, and was kept once per zoom level. The key now carries the target size only when JPX decodes the image, its /SMask or its /Mask (read from /Filter or /F, erring towards keeping the target). `InterpreterCache::decoded_image_decodes` counts the cache's decodes; the new test draws a Flate image at five sizes and sees one decode, and a copy whose soft mask is JPX-encoded still decodes per size. Both assertions fail with the old keying. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ * fix(render): patch meshes copy their vertices' colours less (vendored hayro-interpret patch 33) A Coons or tensor-product patch is drawn as 722 triangles over a 20 x 20 grid of points, and every vertex's colours (a SmallVec) were produced or copied up to five times: each grid cell interpolated its four corners itself (1,444 interpolations a patch for 400 points), cloned all six vertices of its two triangles, and the mesh sampler cloned each vertex again to move the triangle into device space. Profiling page 8 of the annual report in #1584 put a quarter of its time in those clones. Each grid point's colours are now interpolated once, a cell's second triangle takes the vertices its first copied, and the sampler moves a patch's freshly made triangles instead of copying them (`TriangleSampler::sample_owned`; a free-form triangle mesh, which the shading keeps, is still copied). The same inputs give the same values: rendering the first three pages of every pdf.js corpus file is byte for byte the same. Release build, Linux: report page 8 773 -> 696 ms, personwithdog.pdf 149 -> 129 ms, issue13520.pdf 104 -> 87 ms. The new test pins the colours near each corner, at the centre and between them for a Coons and a tensor-product patch (taken before the change); swapping two vertices' grid points makes it fail. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ * docs: roadmap entries for faster sampled functions, image decodes and patch meshes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ --------- Co-authored-by: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/render/src/raster.rs | 193 +++++++++++++++ docs/roadmap.md | 2 + vendor/README.md | 2 +- vendor/hayro-interpret/src/cache.rs | 12 +- vendor/hayro-interpret/src/context.rs | 6 + vendor/hayro-interpret/src/encode.rs | 103 +++++--- vendor/hayro-interpret/src/function/type0.rs | 243 ++++++------------- vendor/hayro-interpret/src/shading.rs | 34 +-- vendor/hayro-interpret/src/x_object.rs | 33 ++- 10 files changed, 393 insertions(+), 236 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 66b967756..3ee820247 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -176,6 +176,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-12 (M14, #1395):** XDG desktops use the configuration base for app.ron, safely copying existing preferences without overwriting a destination. Original preferences, logs, recovery and digital IDs are retained; failed migration falls back to the original location. Portable and non-XDG locations remain unchanged; estimates unchanged. - **2026-10-12 (M10, Windows print progress, follow-up to #944):** While a job spools, the progress card counts its sheets ("Sending sheet 2 of 3 to Office…") as craft-print reports them (`submit_with_progress`, storytold/craft-libs#27), and steps aside for another long job's card. Pins craft-libs 666c358. Estimates unchanged. +- **2026-10-15 (M2, renderer speed):** Three vendored hayro-interpret patches (31–33), pixel for pixel the same over the first three pages of every pdf.js corpus file. A sampled (Type 0) function keeps its samples in one flat array instead of a hash map rebuilt every time a shading using it is painted: `bug1721218` (InDesign) draws in 0.49 s (was 1.34 s), and a 2,097,152-sample ramp painted 30 times in under a second (was 38 s). One decode of a JPEG or Flate image serves every size it is drawn at (only JPX decodes for a size), so zooming a 300-dpi scan in and out costs 35–40% less render work. Coons and tensor patch meshes copy their vertices' colours less: mesh-gradient pages draw 10–16% faster (page 8 of #1584's annual report 773 → 696 ms). Estimates unchanged. - 2026-10-12 · M3: selected page text can be copied from Edit and the canvas context menu (#1225). Five focused regressions, full CI and native Unicode clipboard checks pass; floating Copy shares the document permission gate. Estimates unchanged. - **2026-10-13 (M2, page-label prefixes):** Preserve literal leading and trailing whitespace in page-label prefixes without changing generic metadata normalization. Nine original public-inspection cases cover prefix spaces, encodings and empty-prefix controls. Existing empty-prefix fallback remains a compatibility policy, not a conformance claim. Estimates unchanged. diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index 17afd82c7..8d5686dd8 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -4651,6 +4651,76 @@ trailer << /Root 1 0 R >> assert_eq!(&page.rgba[((38 * 40 + 1) * 4)..][..4], &[255, 0, 0, 255], "the rest of the page draws"); } + /// A sampled function kept its samples in a hash map keyed by each point of its grid, built + /// again every time a shading using it was painted (vendored hayro-interpret patch 31). + /// InDesign pages paint 4096-sample gradient ramps dozens of times, and `bug1721218` in the + /// pdf.js corpus spent most of its 1.5 s hashing them. The samples are now one flat array + /// indexed by strides: this page paints a 2,097,152-sample ramp (the most a table may hold) + /// 30 times, which took 38 seconds through the hash map. + #[test] + fn shadings_with_large_sampled_functions_paint_quickly() { + const SAMPLES: usize = 1 << 21; + let samples = zlib_zeros(SAMPLES); + // Black ramp on the left half, 30 times over; a red square after it. + let content = "q 0 0 20 40 re W n /Sh0 sh Q\n".repeat(30) + "1 0 0 rg 36 36 4 4 re f"; + let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /Shading << /Sh0 5 0 R >> >> >> endobj\n".to_vec(); + pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n", content.len()).as_bytes()); + pdf.extend_from_slice(b"5 0 obj << /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 40 0] /Function 6 0 R >> endobj\n"); + pdf.extend_from_slice( + format!("6 0 obj << /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [{SAMPLES}] /BitsPerSample 8 /Filter /FlateDecode /Length {} >> stream\n", samples.len()) + .as_bytes(), + ); + pdf.extend_from_slice(&samples); + pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n"); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let _ = tx.send(render_first_page(pdf)); + }); + let page = + rx.recv_timeout(std::time::Duration::from_secs(10)).expect("painting a large sampled function must not rebuild a hash map each time"); + assert_eq!(rgb_at(&page.rgba, 40, 5, 20), [0, 0, 0], "the ramp is drawn"); + assert_eq!(rgb_at(&page.rgba, 40, 30, 20), [255, 255, 255], "outside the clip"); + assert_eq!(rgb_at(&page.rgba, 40, 38, 2), [255, 0, 0], "the rest of the page draws"); + } + + /// Patch 31's flat table gives every sampled function the values the hash map gave: points + /// on the grid, multilinear interpolation between them (first input varying fastest), and no + /// value where a sample the interpolation needs is missing from a short stream. + #[test] + fn sampled_functions_interpolate_their_grid_as_before() { + use hayro::hayro_interpret::Function; + use hayro_syntax::object::{FromBytes, Object}; + + // Size [3 2], two outputs: out0 = 40 i + 60 j (linear), out1 = 100 i j (bilinear only). + let mut samples = Vec::new(); + for j in 0..2u8 { + for i in 0..3u8 { + samples.extend_from_slice(&[40 * i + 60 * j, 100 * i * j]); + } + } + let function = |data: &[u8]| { + let mut bytes = + format!("<< /FunctionType 0 /Domain [0 1 0 1] /Range [0 1 0 1] /Size [3 2] /BitsPerSample 8 /Length {} >> stream\n", data.len()) + .into_bytes(); + bytes.extend_from_slice(data); + bytes.extend_from_slice(b"\nendstream"); + bytes + }; + let whole = function(&samples); + let f = Function::new(&Object::from_bytes(&whole).expect("a stream object")).expect("a sampled function"); + for (x, y) in [(0.0, 0.0), (1.0, 1.0), (0.5, 0.0), (0.25, 0.5), (0.75, 0.25), (0.1, 0.9), (1.0, 0.3)] { + let (gi, gj) = (2.0 * x, y); + let got = f.eval([x, y].into_iter().collect()).expect("every sample is present"); + let want = [(40.0 * gi + 60.0 * gj) / 255.0, 100.0 * gi.min(1.0) * gj / 255.0 + 100.0 * (gi - 1.0).max(0.0) * gj / 255.0]; + assert!(got.len() == 2 && got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-5), "({x}, {y}): got {got:?}, want {want:?}"); + } + // Only the j = 0 row: points on it evaluate, points needing j = 1 have no value. + let short = function(&samples[..6]); + let f = Function::new(&Object::from_bytes(&short).expect("a stream object")).expect("a sampled function"); + assert_eq!(f.eval([0.5, 0.0].into_iter().collect()).map(|v| v.to_vec()), Some(vec![40.0 / 255.0, 0.0])); + assert_eq!(f.eval([0.5, 0.5].into_iter().collect()), None); + } + /// From `cargo xtask fuzz`: a mesh shading whose points lie far off the page (a 147-byte /// Coons patch stream decoded onto ±40000) was sampled pixel by pixel over its whole /// bounding box, up to 65536 × 65536 entries: a synthetic page passed 6 GB in six seconds. @@ -4743,6 +4813,54 @@ trailer << /Root 1 0 R >> assert!(middle != [255, 255, 255, 255] && middle[3] == 255, "a small patch is shaded: {middle:?}"); } + /// Coons and tensor-product patches put each corner's colour at its corner and blend them + /// in between. Patch 33 interpolates each point of a patch's 20 × 20 triangle grid once + /// instead of once per cell touching it, and moves vertices instead of copying them; these + /// colours (taken before that change) pin that every vertex still gets its own point's colour. + #[test] + fn patch_meshes_keep_their_corner_colours() { + // Points on a 4 × 4 grid over the 40 pt page, in stream order: the 12 boundary points + // (both patch types), then a tensor patch's four interior ones. + let grid = |x: u8, y: u8| [x * 85, y * 85]; + let boundary = [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (2, 3), (3, 3), (3, 2), (3, 1), (3, 0), (2, 0), (1, 0)]; + let interior = [(1, 1), (1, 2), (2, 2), (2, 1)]; + let colours = [255u8, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0]; + let patch = |points: &[(u8, u8)]| { + let mut data = vec![0u8]; + data.extend(points.iter().flat_map(|(x, y)| grid(*x, *y))); + data.extend_from_slice(&colours); + data + }; + let render = |shading_type: u8, data: Vec| { + let content = b"/Sh0 sh"; + let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /Shading << /Sh0 5 0 R >> >> >> endobj\n".to_vec(); + pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n", content.len()).as_bytes()); + pdf.extend_from_slice(content); + pdf.extend_from_slice(format!("\nendstream endobj\n5 0 obj << /ShadingType {shading_type} /ColorSpace /DeviceRGB /BitsPerCoordinate 8 /BitsPerComponent 8 /BitsPerFlag 8 /Decode [0 40 0 40 0 1 0 1 0 1] /Length {} >> stream\n", data.len()).as_bytes()); + pdf.extend_from_slice(&data); + pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n"); + render_first_page(pdf) + }; + let coons = render(6, patch(&boundary)); + let tensor = render(7, patch(&[&boundary[..], &interior[..]].concat())); + // Near each corner (top-left, bottom-left, bottom-right, top-right in the raster), the + // centre, and a point between them. + let want = [ + ((5, 5), [32, 199, 28]), + ((5, 34), [217, 61, 5]), + ((34, 34), [217, 190, 32]), + ((34, 5), [32, 61, 190]), + ((20, 20), [127, 128, 64]), + ((12, 27), [172, 110, 25]), + ]; + for (what, page) in [("Coons", &coons), ("tensor", &tensor)] { + for ((x, y), colour) in want { + let got = rgb_at(&page.rgba, 40, x, y); + assert!(got.iter().zip(colour).all(|(g, w)| g.abs_diff(w) <= 1), "{what} patch at ({x}, {y}): got {got:?}, want {colour:?}"); + } + } + } + /// The other half of the mesh-shading patch: ten thousand triangles that each cover half /// of this 1000 × 1000 canvas walked their bounding boxes ten thousand times over (ten /// billion visits). The visit budget stops that work at eight per pixel plus four per @@ -5979,6 +6097,81 @@ endstream endobj assert_eq!(rgb_at(&out.rgba, w, 100, 35), [255, 0, 0]); } + /// The decoded-image cache keyed every decode by the size the image is drawn at, but only + /// a JPX stream decodes differently for a size: a scan was decoded again at every zoom step + /// and kept once per zoom level (vendored hayro-interpret patch 32). An image drawn at five + /// sizes is now decoded once, unless JPX decodes it or its soft mask. + #[test] + fn decoded_images_serve_every_size_they_are_drawn_at() { + use hayro::hayro_interpret::font::Glyph; + use hayro::hayro_interpret::hayro_syntax::Pdf; + use hayro::hayro_interpret::{ + BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, Paint, PathDrawMode, SoftMask, TransformExt, interpret_page, + }; + use kurbo::{Affine, BezPath, Rect}; + use std::io::Write; + + /// Asks for every raster image at the size it is drawn at, as the renderer does, and + /// counts the decoded images it was handed. + #[derive(Default)] + struct Draws(usize); + impl<'a> Device<'a> for Draws { + fn set_soft_mask(&mut self, _: Option>) {} + fn set_blend_mode(&mut self, _: BlendMode) {} + fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'a>, _: &PathDrawMode) {} + fn push_clip_path(&mut self, _: &ClipPath) {} + fn push_transparency_group(&mut self, _: f32, _: Option>, _: BlendMode) {} + fn draw_glyph(&mut self, _: &Glyph<'a>, _: Affine, _: Affine, _: &Paint<'a>, _: &GlyphDrawMode) {} + fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) { + let [a, b, c, d, ..] = transform.as_coeffs(); + let target = ((a.hypot(b) * f64::from(image.width())).ceil() as u32, (c.hypot(d) * f64::from(image.height())).ceil() as u32); + if let Image::Raster(r) = image { + r.with_rgba(|_, _| self.0 += 1, Some(target)); + } + } + fn pop_clip_path(&mut self) {} + fn pop_transparency_group(&mut self) {} + } + + let flate = |data: &[u8]| { + let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast()); + z.write_all(data).unwrap(); + z.finish().unwrap() + }; + let pixels = flate(&(0..64 * 64 * 3).map(|i| (i % 251) as u8).collect::>()); + let image = |extra: &str| { + format!( + "<< /Type /XObject /Subtype /Image /Width 64 /Height 64 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode {extra}/Length {} >>", + pixels.len() + ) + }; + let (plain, jpx_masked) = (image(""), image("/SMask 7 0 R ")); + let jpx_mask = + "<< /Type /XObject /Subtype /Image /Width 64 /Height 64 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /JPXDecode /Length 4 >>"; + // `name` drawn at five sizes, from 10 to 40 pt square. + let decodes = |name: &str| { + let content = [(10, 0, 0), (20, 12, 0), (30, 34, 0), (40, 66, 0), (25, 100, 40)] + .map(|(s, x, y)| format!("q {s} 0 0 {s} {x} {y} cm /{name} Do Q")) + .join(" "); + let bytes = image_cache_pdf( + &content, + "<< /XObject << /A 5 0 R /B 6 0 R >> >>", + &[(&plain, &pixels), (&jpx_masked, &pixels), (jpx_mask, b"\0\0\0\0")], + ); + let parsed = Pdf::new(Arc::new(bytes)).expect("parses"); + let page = &parsed.pages()[0]; + let cache = InterpreterCache::new(); + let initial = page.initial_transform(true).to_kurbo(); + let mut ctx = Context::new(initial, Rect::new(0.0, 0.0, 130.0, 70.0), &cache, page.xref(), InterpreterSettings::default()); + let mut device = Draws::default(); + interpret_page(page, &mut ctx, &mut device); + assert_eq!(device.0, 5, "every draw of /{name} was handed its pixels"); + cache.decoded_image_decodes() + }; + assert_eq!(decodes("A"), 1, "a Flate image decodes the same at every size"); + assert_eq!(decodes("B"), 5, "a JPX soft mask decodes for the size it is drawn at"); + } + /// Render `pdf`'s first page at 72 dpi with no time limit beyond the test's own. fn render_first_page(pdf: Vec) -> RenderedPage { let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default()); diff --git a/docs/roadmap.md b/docs/roadmap.md index 3148823ea..ef28f3bbd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -3,6 +3,7 @@ > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-14 · **Change:** minor (M14: the Italian catalog has every entry the other catalogs had and the code looks up, 2,528 entries; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (M2/M6: spot-colour images convert once per pixel value; widget placement and the Fields panel scale with the field count; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 candidate: coordinate control clicks on slow frames and coalesced text selection; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-15 · **Change:** minor (M2: sampled functions index a flat table, one image decode serves every zoom level, patch meshes copy less; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 #2080 candidate: explicit search jumps join per-tab history; rail and control-channel Back/Forward; estimates unchanged) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M4 #1310 #1311 #1312: undo, revert and the next save keep the last saved revision; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -190,6 +191,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st | 2026-10-14 | minor | M14: Italian catalog completed (2,528 entries) and dropped from the ≈ 2,000–2,140-entry group; estimates unchanged | | 2026-10-12 | minor | M4 #1310 #1311 #1312: undo back to the opened file is clean, Revert returns to the last saved file, and the next save keeps that revision. Estimates unchanged. | | 2026-10-14 | minor | M5: reconciled PR #1457 with upstream main, preserving comment submission shortcuts and upstream roadmap progress. Estimates unchanged. | +| 2026-10-15 | minor | M2: three vendored hayro-interpret patches (31–33): sampled functions index a flat table (`bug1721218` 1.34 → 0.49 s), one JPEG or Flate decode serves every size an image is drawn at (35–40% less render work zooming a scan), patch meshes copy their vertices less (10–16% on mesh-gradient pages); pixel for pixel the same over the pdf.js corpus. Estimates unchanged. | | 2026-10-13 | minor | M2/M6: images with at most 16 bits of samples a pixel (8-bit Separation/DeviceN, 1-4-bit gray or indexed) convert once per pixel value (hayro-interpret patch 28), so spot-colour form backgrounds draw in milliseconds instead of seconds; widgets without `/P` are placed in one pass; the Fields panel no longer searches the form per row and draws only the rows in view. Estimates unchanged. | | 2026-10-13 | minor | M3: native-open admission preserves modal/picker ownership; regressions cover queued replies and one-shot picker wakes. Estimates unchanged | | 2026-10-12 | minor | M13: CLI check directory identity, with overlap and symlink regressions; estimates unchanged | diff --git a/vendor/README.md b/vendor/README.md index 87f9d8682..62be4cad9 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -9,7 +9,7 @@ A PdfCraft patch in `hayro-syntax::Page::base_dimensions` expresses the existing | Crate | Version | Licence | Patches | Test | |---|---|---|---|---| -| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.notdef` for a CID of an Adobe character collection (Japan1, GB1, CNS1, Korea1) that it can't find by Unicode: hayro took the CID for a glyph index of the substitute, which numbers its glyphs its own way, so the character was drawn as an unrelated one (𠮷 as "Q" in BIZ UDPGothic); (22) a Type 0 font drawn with a substitute finds a character given with a variation selector (`/ToUnicode`, or Adobe-Japan1-UCS2, which maps the JIS X 0208 forms of the kanji JIS X 0213:2004 redrew that way: CID 1247 is 噂 U+E0100) through the substitute's variation sequences, else as the character alone: such a CID found no glyph and was drawn as an unrelated one; (23) backport of hayro 0.8's `gradient` module: `EncodedShadingPattern::as_svg_gradient` turns an axial or radial shading into stops of an sRGB gradient (within 0.01 per channel, a stop at every stitching-function bound via `Function::stitching_bounds`) when a native gradient draws it exactly (extended past the box drawn in; a radial one with no background colour); (24) `DecodedImageCache`: decoded image pixels shared across renders through the interpreter cache within a 64 MiB budget (an image-heavy page decoded its thousands of images again per tile and per scale), keyed by every decode input (`DecodedImageKey`: the stream's place and dictionary bytes, its object, mask/stencil/raster output, interpolation, decode target size, preview or final); images under a transfer function and inline images are not cached, an image over the budget is never kept, and a poisoned lock decodes uncached; (25) `Device::actual_text` hands the device a marked content sequence's `/ActualText` (inline or named in `/Properties`), so text extraction keeps the Thai, Indic and Arabic clusters Chrome marks this way (their shaped tone marks and vowels have no Unicode of their own); (26) non-embedded simple fonts drawn with a standard-font stand-in build the Latin Extended-A letters the stand-in lacks (`/Gbreve`, `/Idotaccent`, `/Scedilla`, `/aogonek`, `/ccaron`…) from the base letter and accent it has (`StandardKind::composite_glyph`, `ACCENTED_LETTERS`): PDFium's Foxit stand-ins hold only the Standard/WinAnsi/Mac Roman glyphs, so iText letters with Times New Roman unembedded showed blanks for Ğ, İ, Ş, ğ and ş; (27) an embedded TrueType font without a base encoding reads its codes as WinAnsi for text extraction (`EmbeddedKind::code_to_unicode_name`), matching pdftotext's output, instead of Standard: OCR text layers of scans write WinAnsi codes with no `/Encoding`, and Standard has no ü, ö or ß and reads é as Ø (glyph selection unchanged); (28) an image whose pixels hold at most 16 bits of samples (an 8-bit Separation or DeviceN image, a 1-, 2- or 4-bit gray or indexed one) is converted once per possible pixel value through a lookup table instead of once per pixel (`rgb_via_lookup`): a spot-colour tint transform is often a PostScript calculator program, run for every pixel, so a fillable character sheet's 3304 × 4105 DeviceN background took four seconds and 430 MB to draw a page; each entry is converted on its own (`convert_each`), so a tint transform giving the wrong number of components for a value the image never uses cannot shift the colours of the values it does; (29) `Device::begin_annotations`/`end_annotations` bracket the page's annotations, so a device can tell comment paints from document paints (reading colours recolour the document, never a comment, #1548); (30) the Annex D.2 alias codes read as the ink they draw: WinAnsiEncoding 0xA0 and 0xAD, and MacRomanEncoding 0xCA, carry the glyph names `space` and `hyphen` -- the names those tables also give 0x20 and 0x2D -- not `nbspace` and `sfthyphen`, whose glyph list values U+00A0 and U+00AD are invisible formatting characters. Extraction hid drawn ink behind them, so search and redaction matched neither (`XiaoBiaoSong.pdf` in the pdf.js corpus drew 104 soft hyphens; across the 989 files the soft hyphens we extract fall from 117 to 13 and the non-breaking spaces from 30 to 26, what remains coming from `/ToUnicode` maps, which say so explicitly). The base table only, in the text direction only: a `/Differences` name keeps its glyph list value and glyph lookup is untouched, both as pdftotext does | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on`; (18) `raster::tests::page_contents_and_nested_forms_share_one_budget`, `raster::tests::content_painted_again_and_again_keeps_drawing`, `raster::tests::sampled_functions_with_billions_of_samples_are_refused`; (19) `raster::tests::mesh_shadings_far_off_the_page_render`, `raster::tests::many_overlapping_mesh_triangles_finish`, `raster::tests::mesh_shadings_with_many_patches_or_no_rows_finish`; (20) `raster::tests::function_limits_calculator_arithmetic_and_depth`, `raster::tests::function_limits_public_construction_work_and_depth`, `raster::tests::function_limits_stitching_cycles_are_refused_and_shared_children_work`, `raster::tests::function_limits_invalid_calculator_keeps_the_page_renderable`; (21) `raster::tests::cids_a_substitute_lacks_are_not_drawn_as_other_glyphs`; (26) `raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts`; (22) `raster::tests::variation_sequences_draw_their_kanji_in_a_substitute`; (23) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (24) `raster::tests::tiles_match_full_render`, `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel` (both decode through the cache), `raster::tests::decoded_images_with_identical_dictionaries_keep_their_own_pixels`, `raster::tests::decoded_images_under_a_transfer_function_are_not_shared`; (25) `text::tests::actual_text_replaces_the_glyphs_it_marks_in_every_writing_direction`, `text::tests::actual_text_follows_the_pdf_rules_for_marked_content`; (27) `text::tests::truetype_without_an_encoding_reads_its_codes_as_win_ansi`; (28) `raster::tests::spot_colour_images_run_their_tint_transform_once_per_sample_value`, `raster::tests::images_converted_through_a_table_keep_every_samples_colour`, `raster::tests::a_malformed_tint_for_an_unused_sample_keeps_the_used_colours`; (29) `raster::tests::reading_colors_keep_highlight_annotations_readable`; (30) `text::tests::the_annex_d2_alias_codes_read_as_an_ordinary_hyphen_and_space` | +| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.notdef` for a CID of an Adobe character collection (Japan1, GB1, CNS1, Korea1) that it can't find by Unicode: hayro took the CID for a glyph index of the substitute, which numbers its glyphs its own way, so the character was drawn as an unrelated one (𠮷 as "Q" in BIZ UDPGothic); (22) a Type 0 font drawn with a substitute finds a character given with a variation selector (`/ToUnicode`, or Adobe-Japan1-UCS2, which maps the JIS X 0208 forms of the kanji JIS X 0213:2004 redrew that way: CID 1247 is 噂 U+E0100) through the substitute's variation sequences, else as the character alone: such a CID found no glyph and was drawn as an unrelated one; (23) backport of hayro 0.8's `gradient` module: `EncodedShadingPattern::as_svg_gradient` turns an axial or radial shading into stops of an sRGB gradient (within 0.01 per channel, a stop at every stitching-function bound via `Function::stitching_bounds`) when a native gradient draws it exactly (extended past the box drawn in; a radial one with no background colour); (24) `DecodedImageCache`: decoded image pixels shared across renders through the interpreter cache within a 64 MiB budget (an image-heavy page decoded its thousands of images again per tile and per scale), keyed by every decode input (`DecodedImageKey`: the stream's place and dictionary bytes, its object, mask/stencil/raster output, interpolation, decode target size, preview or final); images under a transfer function and inline images are not cached, an image over the budget is never kept, and a poisoned lock decodes uncached; (25) `Device::actual_text` hands the device a marked content sequence's `/ActualText` (inline or named in `/Properties`), so text extraction keeps the Thai, Indic and Arabic clusters Chrome marks this way (their shaped tone marks and vowels have no Unicode of their own); (26) non-embedded simple fonts drawn with a standard-font stand-in build the Latin Extended-A letters the stand-in lacks (`/Gbreve`, `/Idotaccent`, `/Scedilla`, `/aogonek`, `/ccaron`…) from the base letter and accent it has (`StandardKind::composite_glyph`, `ACCENTED_LETTERS`): PDFium's Foxit stand-ins hold only the Standard/WinAnsi/Mac Roman glyphs, so iText letters with Times New Roman unembedded showed blanks for Ğ, İ, Ş, ğ and ş; (27) an embedded TrueType font without a base encoding reads its codes as WinAnsi for text extraction (`EmbeddedKind::code_to_unicode_name`), matching pdftotext's output, instead of Standard: OCR text layers of scans write WinAnsi codes with no `/Encoding`, and Standard has no ü, ö or ß and reads é as Ø (glyph selection unchanged); (28) an image whose pixels hold at most 16 bits of samples (an 8-bit Separation or DeviceN image, a 1-, 2- or 4-bit gray or indexed one) is converted once per possible pixel value through a lookup table instead of once per pixel (`rgb_via_lookup`): a spot-colour tint transform is often a PostScript calculator program, run for every pixel, so a fillable character sheet's 3304 × 4105 DeviceN background took four seconds and 430 MB to draw a page; each entry is converted on its own (`convert_each`), so a tint transform giving the wrong number of components for a value the image never uses cannot shift the colours of the values it does; (29) `Device::begin_annotations`/`end_annotations` bracket the page's annotations, so a device can tell comment paints from document paints (reading colours recolour the document, never a comment, #1548); (30) the Annex D.2 alias codes read as the ink they draw: WinAnsiEncoding 0xA0 and 0xAD, and MacRomanEncoding 0xCA, carry the glyph names `space` and `hyphen` -- the names those tables also give 0x20 and 0x2D -- not `nbspace` and `sfthyphen`, whose glyph list values U+00A0 and U+00AD are invisible formatting characters. Extraction hid drawn ink behind them, so search and redaction matched neither (`XiaoBiaoSong.pdf` in the pdf.js corpus drew 104 soft hyphens; across the 989 files the soft hyphens we extract fall from 117 to 13 and the non-breaking spaces from 30 to 26, what remains coming from `/ToUnicode` maps, which say so explicitly). The base table only, in the text direction only: a `/Differences` name keeps its glyph list value and glyph lookup is untouched, both as pdftotext does; (31) a Type 0 sampled function keeps its samples in one flat array indexed by strides (`Type0::entry`) instead of a hash map keyed by each grid point: the map was built again, one hashed insert per sample, every time a shading using the function was painted (a 2,097,152-sample ramp painted 30 times took 38 s; `bug1721218` in the pdf.js corpus spent most of a 1.5 s page on it); the interpolation is unchanged, so results are bit for bit the same, a sample missing from a short stream still gives no value, and the maximum sample value is computed without overflowing at 32 bits per sample; (32) the decoded-image cache (24) keys a decode by the size the image is drawn at only when a JPX stream decodes it or its soft mask or mask (`decode_reads_target`): JPX is the only filter that decodes a reduced resolution for a size, so a JPEG or Flate scan was decoded again at every zoom step and kept once per zoom level; one decode now serves every size; `InterpreterCache::decoded_image_decodes` counts the cache's decodes; (33) Coons and tensor-product patches copy their vertices' colours less: each point of a patch's 20 × 20 triangle grid is interpolated once (`generate_patch_triangles`; each cell interpolated its four corners itself, 1,444 interpolations a patch for 400 points), a cell's second triangle takes the vertices its first copied, and the mesh sampler moves a patch's triangles into device space instead of copying each vertex (`TriangleSampler::sample_owned`); the same values, 10–16% faster on mesh-gradient pages | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on`; (18) `raster::tests::page_contents_and_nested_forms_share_one_budget`, `raster::tests::content_painted_again_and_again_keeps_drawing`, `raster::tests::sampled_functions_with_billions_of_samples_are_refused`; (19) `raster::tests::mesh_shadings_far_off_the_page_render`, `raster::tests::many_overlapping_mesh_triangles_finish`, `raster::tests::mesh_shadings_with_many_patches_or_no_rows_finish`; (20) `raster::tests::function_limits_calculator_arithmetic_and_depth`, `raster::tests::function_limits_public_construction_work_and_depth`, `raster::tests::function_limits_stitching_cycles_are_refused_and_shared_children_work`, `raster::tests::function_limits_invalid_calculator_keeps_the_page_renderable`; (21) `raster::tests::cids_a_substitute_lacks_are_not_drawn_as_other_glyphs`; (26) `raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts`; (22) `raster::tests::variation_sequences_draw_their_kanji_in_a_substitute`; (23) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (24) `raster::tests::tiles_match_full_render`, `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel` (both decode through the cache), `raster::tests::decoded_images_with_identical_dictionaries_keep_their_own_pixels`, `raster::tests::decoded_images_under_a_transfer_function_are_not_shared`; (25) `text::tests::actual_text_replaces_the_glyphs_it_marks_in_every_writing_direction`, `text::tests::actual_text_follows_the_pdf_rules_for_marked_content`; (27) `text::tests::truetype_without_an_encoding_reads_its_codes_as_win_ansi`; (28) `raster::tests::spot_colour_images_run_their_tint_transform_once_per_sample_value`, `raster::tests::images_converted_through_a_table_keep_every_samples_colour`, `raster::tests::a_malformed_tint_for_an_unused_sample_keeps_the_used_colours`; (29) `raster::tests::reading_colors_keep_highlight_annotations_readable`; (30) `text::tests::the_annex_d2_alias_codes_read_as_an_ordinary_hyphen_and_space`; (31) `raster::tests::shadings_with_large_sampled_functions_paint_quickly`, `raster::tests::sampled_functions_interpolate_their_grid_as_before`; (32) `raster::tests::decoded_images_serve_every_size_they_are_drawn_at`; (33) `raster::tests::patch_meshes_keep_their_corner_colours` | | hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.notdef` for a CID of an Adobe character collection (Japan1, GB1, CNS1, Korea1) that it can't find by Unicode: hayro took the CID for a glyph index of the substitute, which numbers its glyphs its own way, so the character was drawn as an unrelated one (𠮷 as "Q" in BIZ UDPGothic); (22) a Type 0 font drawn with a substitute finds a character given with a variation selector (`/ToUnicode`, or Adobe-Japan1-UCS2, which maps the JIS X 0208 forms of the kanji JIS X 0213:2004 redrew that way: CID 1247 is 噂 U+E0100) through the substitute's variation sequences, else as the character alone: such a CID found no glyph and was drawn as an unrelated one; (23) backport of hayro 0.8's `gradient` module: `EncodedShadingPattern::as_svg_gradient` turns an axial or radial shading into stops of an sRGB gradient (within 0.01 per channel, a stop at every stitching-function bound via `Function::stitching_bounds`) when a native gradient draws it exactly (extended past the box drawn in; a radial one with no background colour); (24) `DecodedImageCache`: decoded image pixels shared across renders through the interpreter cache within a 64 MiB budget (an image-heavy page decoded its thousands of images again per tile and per scale), keyed by every decode input (`DecodedImageKey`: the stream's place and dictionary bytes, its object, mask/stencil/raster output, interpolation, decode target size, preview or final); images under a transfer function and inline images are not cached, an image over the budget is never kept, and a poisoned lock decodes uncached; (25) `Device::actual_text` hands the device a marked content sequence's `/ActualText` (inline or named in `/Properties`), so text extraction keeps the Thai, Indic and Arabic clusters Chrome marks this way (their shaped tone marks and vowels have no Unicode of their own); (26) non-embedded simple fonts drawn with a standard-font stand-in build the Latin Extended-A letters the stand-in lacks (`/Gbreve`, `/Idotaccent`, `/Scedilla`, `/aogonek`, `/ccaron`…) from the base letter and accent it has (`StandardKind::composite_glyph`, `ACCENTED_LETTERS`): PDFium's Foxit stand-ins hold only the Standard/WinAnsi/Mac Roman glyphs, so iText letters with Times New Roman unembedded showed blanks for Ğ, İ, Ş, ğ and ş; (27) an embedded TrueType font without a base encoding reads its codes as WinAnsi for text extraction (`EmbeddedKind::code_to_unicode_name`), matching pdftotext's output, instead of Standard: OCR text layers of scans write WinAnsi codes with no `/Encoding`, and Standard has no ü, ö or ß and reads é as Ø (glyph selection unchanged); (28) a transparency group that is neither isolated nor knockout, entered at full opacity with no soft mask and Normal blending, is drawn straight onto the page instead of into a layer of its own (§11.4.7: its elements composite with the backdrop; the layer made a `/BM /Multiply` highlight appearance an opaque block, #1968); (29) `Device::begin_annotations`/`end_annotations` bracket the page's annotations, so a device can tell comment paints from document paints (reading colours recolour the document, never a comment, #1548) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on`; (18) `raster::tests::page_contents_and_nested_forms_share_one_budget`, `raster::tests::content_painted_again_and_again_keeps_drawing`, `raster::tests::sampled_functions_with_billions_of_samples_are_refused`; (19) `raster::tests::mesh_shadings_far_off_the_page_render`, `raster::tests::many_overlapping_mesh_triangles_finish`, `raster::tests::mesh_shadings_with_many_patches_or_no_rows_finish`; (20) `raster::tests::function_limits_calculator_arithmetic_and_depth`, `raster::tests::function_limits_public_construction_work_and_depth`, `raster::tests::function_limits_stitching_cycles_are_refused_and_shared_children_work`, `raster::tests::function_limits_invalid_calculator_keeps_the_page_renderable`; (21) `raster::tests::cids_a_substitute_lacks_are_not_drawn_as_other_glyphs`; (26) `raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts` |; (22) `raster::tests::variation_sequences_draw_their_kanji_in_a_substitute`; (23) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (24) `raster::tests::tiles_match_full_render`, `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel` (both decode through the cache), `raster::tests::decoded_images_with_identical_dictionaries_keep_their_own_pixels`, `raster::tests::decoded_images_under_a_transfer_function_are_not_shared`; (25) `text::tests::actual_text_replaces_the_glyphs_it_marks_in_every_writing_direction`, `text::tests::actual_text_follows_the_pdf_rules_for_marked_content`; (27) `text::tests::truetype_without_an_encoding_reads_its_codes_as_win_ansi`; (28) `raster::tests::a_multiply_highlight_inside_a_transparency_group_keeps_the_text`; (29) `raster::tests::reading_colors_keep_highlight_annotations_readable` | | hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.notdef` for a CID of an Adobe character collection (Japan1, GB1, CNS1, Korea1) that it can't find by Unicode: hayro took the CID for a glyph index of the substitute, which numbers its glyphs its own way, so the character was drawn as an unrelated one (𠮷 as "Q" in BIZ UDPGothic); (22) a Type 0 font drawn with a substitute finds a character given with a variation selector (`/ToUnicode`, or Adobe-Japan1-UCS2, which maps the JIS X 0208 forms of the kanji JIS X 0213:2004 redrew that way: CID 1247 is 噂 U+E0100) through the substitute's variation sequences, else as the character alone: such a CID found no glyph and was drawn as an unrelated one; (23) backport of hayro 0.8's `gradient` module: `EncodedShadingPattern::as_svg_gradient` turns an axial or radial shading into stops of an sRGB gradient (within 0.01 per channel, a stop at every stitching-function bound via `Function::stitching_bounds`) when a native gradient draws it exactly (extended past the box drawn in; a radial one with no background colour); (24) `DecodedImageCache`: decoded image pixels shared across renders through the interpreter cache within a 64 MiB budget (an image-heavy page decoded its thousands of images again per tile and per scale), keyed by every decode input (`DecodedImageKey`: the stream's place and dictionary bytes, its object, mask/stencil/raster output, interpolation, decode target size, preview or final); images under a transfer function and inline images are not cached, an image over the budget is never kept, and a poisoned lock decodes uncached; (25) `Device::actual_text` hands the device a marked content sequence's `/ActualText` (inline or named in `/Properties`), so text extraction keeps the Thai, Indic and Arabic clusters Chrome marks this way (their shaped tone marks and vowels have no Unicode of their own); (26) non-embedded simple fonts drawn with a standard-font stand-in build the Latin Extended-A letters the stand-in lacks (`/Gbreve`, `/Idotaccent`, `/Scedilla`, `/aogonek`, `/ccaron`…) from the base letter and accent it has (`StandardKind::composite_glyph`, `ACCENTED_LETTERS`): PDFium's Foxit stand-ins hold only the Standard/WinAnsi/Mac Roman glyphs, so iText letters with Times New Roman unembedded showed blanks for Ğ, İ, Ş, ğ and ş; (27) text in a Type 3 font is drawn filled whatever the text rendering mode, except mode 3 (invisible): only that mode has any effect on Type 3 fonts (ISO 32000-1 §9.3.6). Stroking ran the glyph procedure a second time, so `2 Tr` made bitmap fonts look bold, `1 Tr` painted an uncoloured glyph in the stroking colour, and `7 Tr` hid it; (29) `Device::begin_annotations`/`end_annotations` bracket the page's annotations, so a device can tell comment paints from document paints (reading colours recolour the document, never a comment, #1548) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on`; (18) `raster::tests::page_contents_and_nested_forms_share_one_budget`, `raster::tests::content_painted_again_and_again_keeps_drawing`, `raster::tests::sampled_functions_with_billions_of_samples_are_refused`; (19) `raster::tests::mesh_shadings_far_off_the_page_render`, `raster::tests::many_overlapping_mesh_triangles_finish`, `raster::tests::mesh_shadings_with_many_patches_or_no_rows_finish`; (20) `raster::tests::function_limits_calculator_arithmetic_and_depth`, `raster::tests::function_limits_public_construction_work_and_depth`, `raster::tests::function_limits_stitching_cycles_are_refused_and_shared_children_work`, `raster::tests::function_limits_invalid_calculator_keeps_the_page_renderable`; (21) `raster::tests::cids_a_substitute_lacks_are_not_drawn_as_other_glyphs`; (26) `raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts` |; (22) `raster::tests::variation_sequences_draw_their_kanji_in_a_substitute`; (23) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (24) `raster::tests::tiles_match_full_render`, `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel` (both decode through the cache), `raster::tests::decoded_images_with_identical_dictionaries_keep_their_own_pixels`, `raster::tests::decoded_images_under_a_transfer_function_are_not_shared`; (25) `text::tests::actual_text_replaces_the_glyphs_it_marks_in_every_writing_direction`, `text::tests::actual_text_follows_the_pdf_rules_for_marked_content`; (27) `raster::tests::text_rendering_modes_other_than_invisible_draw_type3_glyphs_as_filled`; (29) `raster::tests::reading_colors_keep_highlight_annotations_readable` | | hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.notdef` for a CID of an Adobe character collection (Japan1, GB1, CNS1, Korea1) that it can't find by Unicode: hayro took the CID for a glyph index of the substitute, which numbers its glyphs its own way, so the character was drawn as an unrelated one (𠮷 as "Q" in BIZ UDPGothic); (22) a Type 0 font drawn with a substitute finds a character given with a variation selector (`/ToUnicode`, or Adobe-Japan1-UCS2, which maps the JIS X 0208 forms of the kanji JIS X 0213:2004 redrew that way: CID 1247 is 噂 U+E0100) through the substitute's variation sequences, else as the character alone: such a CID found no glyph and was drawn as an unrelated one; (23) backport of hayro 0.8's `gradient` module: `EncodedShadingPattern::as_svg_gradient` turns an axial or radial shading into stops of an sRGB gradient (within 0.01 per channel, a stop at every stitching-function bound via `Function::stitching_bounds`) when a native gradient draws it exactly (extended past the box drawn in; a radial one with no background colour); (24) `DecodedImageCache`: decoded image pixels shared across renders through the interpreter cache within a 64 MiB budget (an image-heavy page decoded its thousands of images again per tile and per scale), keyed by every decode input (`DecodedImageKey`: the stream's place and dictionary bytes, its object, mask/stencil/raster output, interpolation, decode target size, preview or final); images under a transfer function and inline images are not cached, an image over the budget is never kept, and a poisoned lock decodes uncached; (25) Lab colour spaces honour their own `/WhitePoint`: L\*a\*b\* goes to XYZ relative to that white, is Bradford-adapted to D65 and converted to sRGB (ISO 32000-2 §8.6.5.4) instead of through `LAB.icc` as if every white were D50 (a D65 blue came out 45 levels too red; the profile is removed); components normalised to [0, 1] (`manual_scale`, an `/Indexed` palette over Lab) map onto L\* 0..100 and the a\*/b\* `/Range`; (29) `Device::begin_annotations`/`end_annotations` bracket the page's annotations, so a device can tell comment paints from document paints (reading colours recolour the document, never a comment, #1548) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on`; (18) `raster::tests::page_contents_and_nested_forms_share_one_budget`, `raster::tests::content_painted_again_and_again_keeps_drawing`, `raster::tests::sampled_functions_with_billions_of_samples_are_refused`; (19) `raster::tests::mesh_shadings_far_off_the_page_render`, `raster::tests::many_overlapping_mesh_triangles_finish`, `raster::tests::mesh_shadings_with_many_patches_or_no_rows_finish`; (20) `raster::tests::function_limits_calculator_arithmetic_and_depth`, `raster::tests::function_limits_public_construction_work_and_depth`, `raster::tests::function_limits_stitching_cycles_are_refused_and_shared_children_work`, `raster::tests::function_limits_invalid_calculator_keeps_the_page_renderable`; (21) `raster::tests::cids_a_substitute_lacks_are_not_drawn_as_other_glyphs` |; (22) `raster::tests::variation_sequences_draw_their_kanji_in_a_substitute`; (23) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (24) `raster::tests::tiles_match_full_render`, `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel` (both decode through the cache), `raster::tests::decoded_images_with_identical_dictionaries_keep_their_own_pixels`, `raster::tests::decoded_images_under_a_transfer_function_are_not_shared`; (25) `raster::tests::lab_honours_the_space_white_point`, `raster::tests::indexed_palettes_over_lab_keep_their_colours`; (29) `raster::tests::reading_colors_keep_highlight_annotations_readable` | diff --git a/vendor/hayro-interpret/src/cache.rs b/vendor/hayro-interpret/src/cache.rs index 2dda2e8a7..e253a7b8c 100644 --- a/vendor/hayro-interpret/src/cache.rs +++ b/vendor/hayro-interpret/src/cache.rs @@ -72,7 +72,8 @@ pub(crate) struct DecodedImageCache { /// - the dictionary's bytes (`dict_hash`: `/Decode`, `/ColorSpace`, `/SMask`, `/Mask`, /// `/BitsPerComponent`, filters, …) and its object (`obj`: the decryption key depends on it); /// - whether it is decoded as a mask, a stencil mask, raster or mask output, interpolation, -/// the decode target size, and preview or final quality. +/// the decode target size (only for an image a JPX stream decodes, which is the only +/// decode that reads it: PdfCraft patch 32), and preview or final quality. /// /// A transfer function (graphics state `/TR`) also changes the pixels and has no cheap key, so /// an image drawn under one is not cached; neither is an inline image, whose dictionary lives in @@ -95,6 +96,8 @@ pub(crate) struct DecodedImageKey { struct DecodedImageCacheInner { map: HashMap>>, bytes: usize, + /// PdfCraft patch (32): how many keyed decodes ran (cache misses). + decodes: usize, } impl DecodedImageCache { @@ -105,6 +108,12 @@ impl DecodedImageCache { } } + /// PdfCraft patch (32): how many decodes the cache was asked to keep (its misses), for + /// diagnostics and tests. 0 when the lock is poisoned. + pub(crate) fn decodes(&self) -> usize { + self.inner.lock().map(|locked| locked.decodes).unwrap_or(0) + } + /// The same cache, for a preview (`true`) or a final render. pub(crate) fn for_render(&self, preview: bool) -> Self { Self { @@ -141,6 +150,7 @@ impl DecodedImageCache { let Ok(mut locked) = self.inner.lock() else { return val; }; + locked.decodes = locked.decodes.saturating_add(1); // Another thread may have decoded it meanwhile: keep the first copy. if let Some(existing) = locked.map.get(&key) && let Some(existing) = existing.as_ref().and_then(|val| val.downcast_ref::()) diff --git a/vendor/hayro-interpret/src/context.rs b/vendor/hayro-interpret/src/context.rs index 3e83229c5..d6772baa1 100644 --- a/vendor/hayro-interpret/src/context.rs +++ b/vendor/hayro-interpret/src/context.rs @@ -140,6 +140,12 @@ impl<'a> InterpreterCache<'a> { decoded_cache: DecodedImageCache::new(), } } + + /// PdfCraft patch (32): how many images were decoded through this cache (its misses, not + /// counting images that are never cached), for diagnostics and tests. + pub fn decoded_image_decodes(&self) -> usize { + self.decoded_cache.decodes() + } } /// A per-page interpretation context that borrows shared data from an [`InterpreterCache`]. diff --git a/vendor/hayro-interpret/src/encode.rs b/vendor/hayro-interpret/src/encode.rs index 498ed909c..b3f830ba2 100644 --- a/vendor/hayro-interpret/src/encode.rs +++ b/vendor/hayro-interpret/src/encode.rs @@ -3,7 +3,7 @@ use crate::color::{AlphaColor, ColorComponents, ColorSpace}; use crate::interpret::state::ActiveTransferFunction; use crate::pattern::ShadingPattern; -use crate::shading::{ShadingFunction, ShadingType, Triangle}; +use crate::shading::{ShadingFunction, ShadingType, Triangle, TriangleVertex}; use kurbo::{Affine, Point}; use rustc_hash::FxHashMap; use smallvec::{ToSmallVec, smallvec}; @@ -102,7 +102,7 @@ impl ShadingPattern { } triangles.clear(); patch.to_triangles(&mut triangles); - if !sampler.sample(&triangles, full_transform) { + if !sampler.sample_owned(&mut triangles, full_transform) { break; } } @@ -126,7 +126,7 @@ impl ShadingPattern { } triangles.clear(); patch.to_triangles(&mut triangles); - if !sampler.sample(&triangles, full_transform) { + if !sampler.sample_owned(&mut triangles, full_transform) { break; } } @@ -268,47 +268,62 @@ impl TriangleSampler { /// Sample `triangles`; `false` once a budget is spent (the rest is left unsampled). fn sample(&mut self, triangles: &[Triangle], transform: Affine) -> bool { + if !self.take_page_budget(triangles.len()) { + return false; + } + triangles.iter().all(|t| { + self.sample_one(transformed( + t.p0.clone(), + t.p1.clone(), + t.p2.clone(), + transform, + )) + }) + } + + /// PdfCraft patch (33): [`Self::sample`] for triangles made for this sampling alone (a + /// patch's), moving their vertices instead of copying each one's colours (`triangles` is + /// left empty). + fn sample_owned(&mut self, triangles: &mut Vec, transform: Affine) -> bool { + if !self.take_page_budget(triangles.len()) { + return false; + } + triangles + .drain(..) + .all(|t| self.sample_one(transformed(t.p0, t.p1, t.p2, transform))) + } + + /// Take `count` triangles from the page's budget; `false` (and none left) when it is short. + fn take_page_budget(&mut self, count: usize) -> bool { let page_left = MESH_TRIANGLES_LEFT.with(core::cell::Cell::get); - if page_left < triangles.len() as u64 { + if page_left < count as u64 { MESH_TRIANGLES_LEFT.with(|n| n.set(0)); warn!("page mesh triangle budget exceeded; the rest of the shading is not drawn"); return false; } - MESH_TRIANGLES_LEFT.with(|n| n.set(page_left - triangles.len() as u64)); - for t in triangles { - let t = { - let p0 = transform * t.p0.point; - let p1 = transform * t.p1.point; - let p2 = transform * t.p2.point; + MESH_TRIANGLES_LEFT.with(|n| n.set(page_left - count as u64)); + true + } - let mut v0 = t.p0.clone(); - v0.point = p0; - let mut v1 = t.p1.clone(); - v1.point = p1; - let mut v2 = t.p2.clone(); - v2.point = p2; + /// Sample one triangle in device space; `false` once the visit budget is spent. + fn sample_one(&mut self, t: Triangle) -> bool { + // Only the part of the triangle inside `area`; only such triangles add visits. + let bbox = t.bounding_box().intersect(self.area); + if bbox.width() <= 0.0 || bbox.height() <= 0.0 { + return true; + } + self.visits_left = self.visits_left.saturating_add(MESH_VISITS_PER_TRIANGLE); - Triangle::new(v0, v1, v2) - }; - - // Only the part of the triangle inside `area`; only such triangles add visits. - let bbox = t.bounding_box().intersect(self.area); - if bbox.width() <= 0.0 || bbox.height() <= 0.0 { - continue; - } - self.visits_left = self.visits_left.saturating_add(MESH_VISITS_PER_TRIANGLE); - - for y in (bbox.y0.floor() as u16)..(bbox.y1.ceil() as u16) { - for x in (bbox.x0.floor() as u16)..(bbox.x1.ceil() as u16) { - if self.visits_left == 0 { - warn!("mesh shading visit budget exceeded; the rest is not drawn"); - return false; - } - self.visits_left -= 1; - let point = Point::new(x as f64, y as f64); - if t.contains_point(point) { - self.map.insert((x, y), t.interpolate(point)); - } + for y in (bbox.y0.floor() as u16)..(bbox.y1.ceil() as u16) { + for x in (bbox.x0.floor() as u16)..(bbox.x1.ceil() as u16) { + if self.visits_left == 0 { + warn!("mesh shading visit budget exceeded; the rest is not drawn"); + return false; + } + self.visits_left -= 1; + let point = Point::new(x as f64, y as f64); + if t.contains_point(point) { + self.map.insert((x, y), t.interpolate(point)); } } } @@ -316,6 +331,20 @@ impl TriangleSampler { } } +/// The triangle `v0`, `v1`, `v2` with its points mapped through `transform` (its colours as +/// they are). +fn transformed( + mut v0: TriangleVertex, + mut v1: TriangleVertex, + mut v2: TriangleVertex, + transform: Affine, +) -> Triangle { + v0.point = transform * v0.point; + v1.point = transform * v1.point; + v2.point = transform * v2.point; + Triangle::new(v0, v1, v2) +} + fn encode_function_shading(domain: &[f32; 4], function: &ShadingFunction) -> EncodedShadingType { let domain = kurbo::Rect::new( domain[0] as f64, diff --git a/vendor/hayro-interpret/src/function/type0.rs b/vendor/hayro-interpret/src/function/type0.rs index 02bbc41b8..9a82c5978 100644 --- a/vendor/hayro-interpret/src/function/type0.rs +++ b/vendor/hayro-interpret/src/function/type0.rs @@ -3,14 +3,18 @@ use hayro_syntax::bit_reader::BitReader; use hayro_syntax::object::Array; use hayro_syntax::object::Stream; use hayro_syntax::object::dict::keys::{BITS_PER_SAMPLE, DECODE, ENCODE, SIZE}; -use smallvec::{SmallVec, ToSmallVec, smallvec}; -use std::collections::HashMap; +use smallvec::SmallVec; /// A type 0 function (sampled function). #[derive(Debug)] pub(crate) struct Type0 { sizes: IntVec, - table: HashMap, + /// PdfCraft patch (31): the samples in stream order, `range.len()` to a grid point, the + /// first input varying fastest. They were a hash map keyed by each grid point, built again + /// every time a shading using the function was painted. + samples: Vec, + /// PdfCraft patch (31): how many grid points one step along each input moves. + strides: SmallVec<[usize; 4]>, clamper: Clamper, range: TupleVec, bits_per_sample: u8, @@ -85,14 +89,22 @@ impl Type0 { data.truncate(num_expected_entries); } - let table = build_table(&data, &sizes, range.len())?; + // Saturating: the grid has at most `MAX_SAMPLE_ENTRIES` points unless a size is 0, and + // then no point is on it (`entry` checks each coordinate against its size first). + let mut strides = SmallVec::with_capacity(sizes.len()); + let mut stride = 1usize; + for size in &sizes { + strides.push(stride); + stride = stride.saturating_mul(*size as usize); + } Some(Self { sizes, clamper, range, bits_per_sample, - table, + samples: data, + strides, encode, decode, }) @@ -122,29 +134,16 @@ impl Type0 { let in_prev = key.iter().map(|v| v.floor() as u32).collect::(); let in_next = key.iter().map(|v| v.ceil() as u32).collect::(); + let mut coord: IntVec = in_prev.clone(); - let interpolator = Interpolator::new( - key.clone().to_smallvec(), - in_prev, - in_next, - self.sizes.clone(), - self.range.len(), - ); - - let interpolated = interpolator.interpolate(&self.table)?; + let interpolated = self.interpolate(&key, &in_prev, &in_next, &mut coord, 0)?; + // PdfCraft patch (31): `2_u32.pow(32)` overflowed (a panic with overflow checks on). + let max_sample = ((1u64 << self.bits_per_sample) - 1) as f32; let mut out = interpolated .iter() .zip(self.decode.iter()) - .map(|(x, decode)| { - interpolate( - *x, - 0.0, - (2_u32.pow(self.bits_per_sample as u32) - 1) as f32, - decode.0, - decode.1, - ) - }) + .map(|(x, decode)| interpolate(*x, 0.0, max_sample, decode.0, decode.1)) .collect::>(); self.clamper.clamp_output(&mut out); @@ -157,95 +156,57 @@ type FloatVec = SmallVec<[f32; 4]>; type IntVec = SmallVec<[u32; 4]>; // See -struct Interpolator { - input: FloatVec, - sizes: IntVec, - in_prev: IntVec, - in_next: IntVec, - out_len: usize, -} - -impl Interpolator { - fn new( - input: FloatVec, - in_prev: IntVec, - in_next: IntVec, - sizes: IntVec, - out_len: usize, - ) -> Self { - Self { - input, - in_prev, - in_next, - sizes, - out_len, - } - } - - fn interpolate(&self, table: &HashMap) -> Option { - self.interpolate_inner(smallvec![0; self.input.len()], 0, table) - } - - fn interpolate_inner( +impl Type0 { + /// PdfCraft patch (31): the value at `input` between the grid points `in_prev` and + /// `in_next`, interpolating the inputs from `step` on (the earlier ones are placed in + /// `coord`). The same multilinear interpolation, in the same order, as the hash-map + /// version, so the results are bit for bit the same; one level of recursion per input, + /// as before. + fn interpolate( &self, - mut coord: IntVec, + input: &[f32], + in_prev: &[u32], + in_next: &[u32], + coord: &mut IntVec, step: usize, - table: &HashMap, ) -> Option { - if step == self.input.len() - 1 { - if self.in_prev[step] == self.in_next[step] { - coord[step] = self.in_prev[step]; + let (Some(x), Some(prev), Some(next)) = + (input.get(step), in_prev.get(step), in_next.get(step)) + else { + // Every input placed: the grid point itself. + return Some(self.entry(coord)?.iter().map(|n| *n as f32).collect()); + }; + let (x, prev, next) = (*x, *prev, *next); - Some( - table - .get(&Key::from_raw(&self.sizes, &coord))? - .clone() - .iter() - .map(|n| *n as f32) - .collect(), - ) - } else { - coord[step] = self.in_prev[step]; - let val1 = table.get(&Key::from_raw(&self.sizes, &coord))?; - coord[step] = self.in_next[step]; - let val2 = table.get(&Key::from_raw(&self.sizes, &coord))?; - let mut out = smallvec![0.0; self.out_len]; - - for i in 0..self.out_len { - out[i] = interpolate( - self.input[step], - self.in_prev[step] as f32, - self.in_next[step] as f32, - val1[i] as f32, - val2[i] as f32, - ); - } - - Some(out) - } - } else if self.in_prev[step] == self.in_next[step] { - coord[step] = self.in_prev[step]; - self.interpolate_inner(coord, step + 1, table) - } else { - coord[step] = self.in_prev[step]; - let val1 = self.interpolate_inner(coord.clone(), step + 1, table)?; - coord[step] = self.in_next[step]; - let val2 = self.interpolate_inner(coord, step + 1, table)?; - - let mut out = smallvec![0.0; self.out_len]; - - for i in 0..self.out_len { - out[i] = interpolate( - self.input[step], - self.in_prev[step] as f32, - self.in_next[step] as f32, - val1[i], - val2[i], - ); - } - - Some(out) + *coord.get_mut(step)? = prev; + let val1 = self.interpolate(input, in_prev, in_next, coord, step + 1)?; + if prev == next { + return Some(val1); } + *coord.get_mut(step)? = next; + let val2 = self.interpolate(input, in_prev, in_next, coord, step + 1)?; + + Some( + val1.iter() + .zip(val2.iter()) + .map(|(a, b)| interpolate(x, prev as f32, next as f32, *a, *b)) + .collect(), + ) + } + + /// PdfCraft patch (31): the samples of grid point `coord`, or `None` where it is off the + /// grid or the stream ended before it (where the hash map had no entry). + fn entry(&self, coord: &[u32]) -> Option<&[u32]> { + let mut index = 0usize; + for ((c, size), stride) in coord.iter().zip(&self.sizes).zip(&self.strides) { + if c >= size { + return None; + } + index = index.checked_add((*c as usize).checked_mul(*stride)?)?; + } + let n = self.range.len(); + let start = index.checked_mul(n)?; + self.samples.get(start..start.checked_add(n)?) } } @@ -257,71 +218,3 @@ impl Interpolator { /// values, 17^5 points with 4 outputs 5.7 million values. const MAX_SAMPLE_VALUES: usize = 1 << 23; const MAX_SAMPLE_ENTRIES: usize = 1 << 21; - -fn build_table(data: &[u32], sizes: &[u32], n: usize) -> Option> { - let mut key = Key::new(sizes); - let mut table = HashMap::new(); - - let mut first = true; - - for b in data.chunks_exact(n) { - if !first { - key.increment(); - } - - table.insert(key.clone(), b.to_smallvec()); - - first = false; - } - - Some(table) -} - -/// A sampled function consists of a (possibly) multi-dimensional table that we can index -/// into. We do this by representing the entries as a flat list of vectors, where each -/// element in the vector represents the value of the key in that specific dimension. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct Key { - sizes: SmallVec<[u32; 4]>, - parts: SmallVec<[u32; 4]>, -} - -impl Key { - fn new(sizes: &[u32]) -> Self { - let parts = smallvec![0; sizes.len()]; - - Self { - sizes: sizes.to_smallvec(), - parts, - } - } - - fn from_raw(sizes: &[u32], parts: &[u32]) -> Self { - Self { - sizes: sizes.to_smallvec(), - parts: parts.to_smallvec(), - } - } - - fn increment(&mut self) -> Option<()> { - self.increment_index(0) - } - - fn increment_index(&mut self, index: usize) -> Option<()> { - let size = *self.sizes.get(index).or_else(|| { - error!("overflowed key in sampled function"); - - None - })?; - let val = self.parts.get_mut(index)?; - - if *val >= (size - 1) { - *val = 0; - self.increment_index(index + 1)?; - } else { - *val += 1; - } - - Some(()) - } -} diff --git a/vendor/hayro-interpret/src/shading.rs b/vendor/hayro-interpret/src/shading.rs index 98fbb5831..2e619e47f 100644 --- a/vendor/hayro-interpret/src/shading.rs +++ b/vendor/hayro-interpret/src/shading.rs @@ -731,6 +731,10 @@ where { const GRID_SIZE: usize = 20; let mut grid = vec![vec![Point::ZERO; GRID_SIZE]; GRID_SIZE]; + // PdfCraft patch (33): each grid point's colours, interpolated once. Every cell below + // interpolated its four corners itself, so an inner point was interpolated four times + // (1,444 interpolations a patch instead of 400) for the same values. + let mut colors = vec![vec![ColorComponents::new(); GRID_SIZE]; GRID_SIZE]; // Create grid by mapping unit square coordinates. for i in 0..GRID_SIZE { @@ -741,6 +745,7 @@ where // Map unit square coordinate to patch coordinate. let unit_point = Point::new(u, v); grid[i][j] = map_coordinate(unit_point); + colors[i][j] = interpolate(unit_point); } } @@ -751,32 +756,26 @@ where let p01 = grid[i][j + 1]; let p11 = grid[i + 1][j + 1]; - // Calculate unit square coordinates for color interpolation. - let u0 = i as f64 / (GRID_SIZE - 1) as f64; - let u1 = (i + 1) as f64 / (GRID_SIZE - 1) as f64; - let v0 = j as f64 / (GRID_SIZE - 1) as f64; - let v1 = (j + 1) as f64 / (GRID_SIZE - 1) as f64; - - // Create triangle vertices with interpolated colors. + // Create triangle vertices with their grid points' colors. let v00 = TriangleVertex { flag: 0, point: p00, - colors: interpolate(Point::new(u0, v0)), + colors: colors[i][j].clone(), }; let v10 = TriangleVertex { flag: 0, point: p10, - colors: interpolate(Point::new(u1, v0)), + colors: colors[i + 1][j].clone(), }; let v01 = TriangleVertex { flag: 0, point: p01, - colors: interpolate(Point::new(u0, v1)), + colors: colors[i][j + 1].clone(), }; let v11 = TriangleVertex { flag: 0, point: p11, - colors: interpolate(Point::new(u1, v1)), + colors: colors[i + 1][j + 1].clone(), }; let inflate_point = |p: Point, mid: Point| -> Point { @@ -795,16 +794,9 @@ where triangle }; - buffer.push(inflate(Triangle::new( - v00.clone(), - v10.clone(), - v01.clone(), - ))); - buffer.push(inflate(Triangle::new( - v10.clone(), - v11.clone(), - v01.clone(), - ))); + // PdfCraft patch (33): the second triangle takes the vertices the first copied. + buffer.push(inflate(Triangle::new(v00, v10.clone(), v01.clone()))); + buffer.push(inflate(Triangle::new(v10, v11, v01))); } } } diff --git a/vendor/hayro-interpret/src/x_object.rs b/vendor/hayro-interpret/src/x_object.rs index 1138b658f..21b134ce8 100644 --- a/vendor/hayro-interpret/src/x_object.rs +++ b/vendor/hayro-interpret/src/x_object.rs @@ -434,11 +434,28 @@ impl<'a> ImageXObject<'a> { is_mask: self.is_mask, is_stencil_mask: self.is_stencil_mask, interpolate: self.interpolate, - target: target_dimension, + // PdfCraft patch (32): see `decode_reads_target`. + target: if self.decode_reads_target() { + target_dimension + } else { + None + }, preview: cache.preview, }) } + /// PdfCraft patch (32): whether decoding this image depends on the size it is drawn at. + /// Only the JPX filter decodes at a reduced resolution for a target size, in the image + /// itself or in the soft mask or mask decoded with it; every other filter decodes the whole + /// image whatever the target. Keyed by the target, a scan was decoded again at every zoom + /// step and kept once per zoom level; without it, one decode serves them all. + fn decode_reads_target(&self) -> bool { + let dict = self.stream.dict(); + uses_jpx(&self.stream) + || dict.get::>(SMASK).is_some_and(|s| uses_jpx(&s)) + || dict.get::>(MASK).is_some_and(|s| uses_jpx(&s)) + } + pub(crate) fn width(&self) -> u32 { self.width } @@ -467,6 +484,20 @@ fn decoded_mask_size(mask: &DecodedMask) -> usize { mask.luma.width as usize * mask.luma.height as usize } +/// PdfCraft patch (32): whether `stream` names the JPX filter, as a name or in an array, under +/// `/Filter` or its inline abbreviation `/F`. Errs towards `true`: a name the decoder would +/// ignore still counts, which only keeps the target in the key as before. +fn uses_jpx(stream: &Stream<'_>) -> bool { + let dict = stream.dict(); + let is_jpx = |name: &Name<'_>| name.deref() == JPX_DECODE; + [F, FILTER].into_iter().any(|key| { + dict.get::>(key).is_some_and(|name| is_jpx(&name)) + || dict + .get::>(key) + .is_some_and(|filters| filters.iter::>().any(|name| is_jpx(&name))) + }) +} + /// PdfCraft patch: pixel bytes held by a decoded raster image and its mask. fn decoded_raster_size(raster: &DecodedRaster) -> usize { let image = match &raster.image {