mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 19:48:14 -06:00
fix(render): faster sampled functions, image decodes across zoom levels and patch meshes (vendored hayro-interpret patches 31–33) (#2436)
* 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
99941daa5a
commit
f65f733041
@@ -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.
|
||||
|
||||
@@ -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<u8>| {
|
||||
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<SoftMask<'a>>) {}
|
||||
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<SoftMask<'a>>, _: 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::<Vec<_>>());
|
||||
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<u8>) -> RenderedPage {
|
||||
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+11
-1
@@ -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<DecodedImageKey, Option<Box<dyn Any + Send + Sync>>>,
|
||||
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::<T>())
|
||||
|
||||
+6
@@ -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`].
|
||||
|
||||
Vendored
+66
-37
@@ -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<Triangle>, 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,
|
||||
|
||||
+68
-175
@@ -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<Key, IntVec>,
|
||||
/// 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<u32>,
|
||||
/// 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::<IntVec>();
|
||||
let in_next = key.iter().map(|v| v.ceil() as u32).collect::<IntVec>();
|
||||
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::<SmallVec<_>>();
|
||||
|
||||
self.clamper.clamp_output(&mut out);
|
||||
@@ -157,95 +156,57 @@ type FloatVec = SmallVec<[f32; 4]>;
|
||||
type IntVec = SmallVec<[u32; 4]>;
|
||||
|
||||
// See <https://github.com/apache/pdfbox/blob/bb778d4784f354c36ce032e91a0cee2169a4c598/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java#L252>
|
||||
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<Key, IntVec>) -> Option<FloatVec> {
|
||||
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<Key, IntVec>,
|
||||
) -> Option<FloatVec> {
|
||||
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<HashMap<Key, IntVec>> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
+13
-21
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-1
@@ -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::<Stream<'_>>(SMASK).is_some_and(|s| uses_jpx(&s))
|
||||
|| dict.get::<Stream<'_>>(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::<Name<'_>>(key).is_some_and(|name| is_jpx(&name))
|
||||
|| dict
|
||||
.get::<Array<'_>>(key)
|
||||
.is_some_and(|filters| filters.iter::<Name<'_>>().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 {
|
||||
|
||||
Reference in New Issue
Block a user