diff --git a/NOTICE b/NOTICE index 9467671f..d9e06ba0 100644 --- a/NOTICE +++ b/NOTICE @@ -99,7 +99,7 @@ Optional PDF reader Hayro 0.7.1, hayro-interpret 0.7.0, hayro-syntax 0.7.2 and hayro-jbig2 0.3.0 (https://github.com/LaurenzV/hayro), MIT OR Apache-2.0, retain their upstream licence files. These patched sources are transferred from PdfCraft revision -7437bde378f43856eada40af25b983812e615e8a (https://github.com/storytold/pdfcraft). +ec0a965074f376a5bf610435948e4e5184bf3299 (https://github.com/storytold/pdfcraft). See vendor/README.craft-reader.md and each crate's SOURCE.json for provenance and local changes. The reader retains the existing Foxit/PDFium standard-14 font assets (BSD-3-Clause), Compact ICC Profiles by Clinton Ingram (CC0-1.0), and dependency CMaps (BSD-3-Clause). diff --git a/crates/pdf/README.md b/crates/pdf/README.md index f35c5b1b..c728e716 100644 --- a/crates/pdf/README.md +++ b/crates/pdf/README.md @@ -56,7 +56,7 @@ Cancellation is polled between operators, so a decoder may finish before noticin ## Backend provenance and consumer pins The shared reader vendors the current patched Hayro trees from PdfCraft revision -`7437bde378f43856eada40af25b983812e615e8a`. See +`ec0a965074f376a5bf610435948e4e5184bf3299`. See [`vendor/README.craft-reader.md`](../../vendor/README.craft-reader.md) for the transfer and local patches. Lopdf, COS editing, inspection fallback and application caches remain app-owned. diff --git a/vendor/README.craft-reader.md b/vendor/README.craft-reader.md index 704ce25d..dc956e6e 100644 --- a/vendor/README.craft-reader.md +++ b/vendor/README.craft-reader.md @@ -1,11 +1,37 @@ # Shared Hayro reader snapshot The `hayro`, `hayro-interpret`, `hayro-syntax` and `hayro-jbig2` directories are transferred -from [PdfCraft revision 7437bde378f43856eada40af25b983812e615e8a](https://github.com/storytold/pdfcraft/tree/7437bde378f43856eada40af25b983812e615e8a/vendor). -The upstream [patch ledger](https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/README.md) +from [PdfCraft revision ec0a965074f376a5bf610435948e4e5184bf3299](https://github.com/storytold/pdfcraft/tree/ec0a965074f376a5bf610435948e4e5184bf3299/vendor) +(`main`, 2026-10-19). The upstream [patch ledger](https://github.com/storytold/pdfcraft/blob/ec0a965074f376a5bf610435948e4e5184bf3299/vendor/README.md) describes the inherited patches. Source manifests record every transferred Git blob and SHA-256 alongside the local hash. The source manifests themselves are not self-hashed. +The first transfer was from PdfCraft revision 7437bde378f43856eada40af25b983812e615e8a. +The snapshot was then re-synced to ec0a965 by a three-way merge (base 7437bde, PdfCraft's +current tree, this tree), so every local patch described below is carried unchanged. The +re-sync brings these PdfCraft changes: + +| PdfCraft PR | Crate | Change | +|---|---|---| +| #1691 | hayro-interpret | Patch 28: images with few possible pixel values convert each value once (spot-colour images) | +| #1903, #3316 | hayro-syntax | CCITT images that declare `/Rows 0`, or fewer `/Rows` than `/Height`, decode to their height | +| #1954 | hayro-syntax | ASCII85 decoding stops at the stream's decode limit (here the per-document limit) | +| #1982 | hayro-syntax | `Page::base_dimensions` keeps a one-point floor after `/UserUnit` | +| #2027 | hayro-interpret | Reads a transparency group's `/I` and `/K`; a group that is neither isolated nor knockout composites directly onto its backdrop | +| #2132 | hayro | A soft mask fades a gradient fill | +| #1557 | hayro-interpret, hayro | Patch 29: devices are told where annotation drawing begins and ends | +| 13e3154 | hayro-interpret | Annex D.2 alias codes in standard, TrueType, Type 1 and Type 3 fonts read as the glyphs they draw | +| #2436 | hayro-interpret | Patches 31–33: faster sampled functions, image decodes reused across zoom levels, patch meshes interpolated once per grid point | +| #2812 | hayro-interpret | Patch 34: a per-thread tint-transform counter for deterministic tests | +| #2035 | hayro-syntax | Flate back-references are copied in runs | +| #3356 | hayro | A tiling pattern drawn once shows its cell at full scale | +| #3378 | hayro-syntax | Patch 20: spaces or tabs between `stream` and its line break are accepted | +| #3363 | hayro-syntax | A JP2 image in a colour space the decoder refuses is drawn by its component count | +| #3412 | hayro-interpret | Type 3 glyph bounding boxes are transformed to device space before shading bounds are intersected | + +PdfCraft's vendored `vello_common` patch (clip-intersection performance) is not part of this +reader snapshot. + The shared snapshot adds immutable per-document ordinary decode limits, threaded through initial xrefs, object streams, fallback and repair. Existing constructors retain their previous default. Known-size image budgets and streaming content budgets remain distinct. diff --git a/vendor/hayro-interpret/SOURCE.json b/vendor/hayro-interpret/SOURCE.json index 27811044..2507cd07 100644 --- a/vendor/hayro-interpret/SOURCE.json +++ b/vendor/hayro-interpret/SOURCE.json @@ -1,7 +1,7 @@ { "crate": "hayro-interpret", "source_repository": "https://github.com/storytold/pdfcraft", - "source_revision": "7437bde378f43856eada40af25b983812e615e8a", + "source_revision": "ec0a965074f376a5bf610435948e4e5184bf3299", "source_directory": "vendor/hayro-interpret", "files": [ { @@ -174,21 +174,21 @@ }, { "path": "src/cache.rs", - "source_sha256": "a0a46ddbab7ed00b6bd0fb09f10d545c06bb4374ea0f520ae118c8d4f7b3c55d", - "source_git_blob": "2dda2e8a7cc0a2491b253ba1f42adac6074b1b34", - "sha256": "a0a46ddbab7ed00b6bd0fb09f10d545c06bb4374ea0f520ae118c8d4f7b3c55d" + "source_sha256": "76b5dc066330666a50a32124856306b72c98e646346263bd8a269bc99e5a0d42", + "source_git_blob": "e253a7b8c7dbfbfa2f7dc1f6c582559962060b94", + "sha256": "76b5dc066330666a50a32124856306b72c98e646346263bd8a269bc99e5a0d42" }, { "path": "src/color.rs", - "source_sha256": "c78cad4124e7bb203e0763f6bea36c542280fd0a8abfa3f7b3ea0bd728adb73b", - "source_git_blob": "dd2edf241d60f146ddb8ea4cbd4ad12a127fa0a7", - "sha256": "bc9b67008e8f7e5ba7bd96a9b49b6ec5d2818eedc44e003212342d8bc259cc36" + "source_sha256": "799ce29bb5e59c8fc94db202873e48c2047dd35962e6f26efd215156c47158c1", + "source_git_blob": "56bb04f24c04ba33ab6bf99c02a24735670959ad", + "sha256": "149d5a6b1b9e6212d2b3e9d63e1803a6041eb1798bfb9231ef89fc6bfcb7b968" }, { "path": "src/context.rs", - "source_sha256": "e6898005282196c77bf44357237c79f8c839e74f666e8157756230fd9e2f93ed", - "source_git_blob": "3e83229c5f58cfbbefd15bf3b5e7b71380291c01", - "sha256": "86e838ca19dfc47f5bb0d7a2ffff80709f2fd51bc6b62d65151841bcba212e08" + "source_sha256": "dc2be553958ee3f5e5e644a77a0301723bae9ca027a352c13b9caaf549cc0e56", + "source_git_blob": "d6772baa15ebb95d3ba2d1f54ce5c250ad1e8137", + "sha256": "d2dc5a6e8e4c066667a92892470315e7447b216048125d67b7674d25a6f3a265" }, { "path": "src/convert.rs", @@ -198,15 +198,15 @@ }, { "path": "src/device.rs", - "source_sha256": "b95b9c91184dde6bb7dcf2dfcad1244f44ae5c73b02d78ea6cd2c692ac8a5ce4", - "source_git_blob": "72d5a98098cb3544259dcb05df7b5b265310ff2f", - "sha256": "b95b9c91184dde6bb7dcf2dfcad1244f44ae5c73b02d78ea6cd2c692ac8a5ce4" + "source_sha256": "6f0c1203d1e60724cdd11e9a230249247a70f578c4a7ccb9bf39a23411b32db6", + "source_git_blob": "36c64a23fd23f6c4abad941077cf93296a67841f", + "sha256": "6f0c1203d1e60724cdd11e9a230249247a70f578c4a7ccb9bf39a23411b32db6" }, { "path": "src/encode.rs", - "source_sha256": "68973a358f38bd6d7f09277e6319506122f586d1cda092b933c2a18680fe6d8f", - "source_git_blob": "498ed909ca65f9765b4f0c3b2ea10386e2e6c48d", - "sha256": "68973a358f38bd6d7f09277e6319506122f586d1cda092b933c2a18680fe6d8f" + "source_sha256": "674a97c48cc62e9a218f843246776528173c36c00aa96a1e4c291c1d912c29c6", + "source_git_blob": "b3f830ba2dcdb7069ea73706be43c55be398f670", + "sha256": "674a97c48cc62e9a218f843246776528173c36c00aa96a1e4c291c1d912c29c6" }, { "path": "src/font/blob.rs", @@ -288,9 +288,9 @@ }, { "path": "src/font/mod.rs", - "source_sha256": "f7a8cfe30893115cc05906cbec747ad368b5645de101c8cbf7bf622f7611973b", - "source_git_blob": "5ccf5c48a6132b7704baff81ece3492a6a7acae2", - "sha256": "f7a8cfe30893115cc05906cbec747ad368b5645de101c8cbf7bf622f7611973b" + "source_sha256": "7e6411927d91205ca76c6b59f9d737cd1104d066f8cfcc0a87f481016ef30313", + "source_git_blob": "7c8c8d086d59139df791cdb6952506473ec4b5a7", + "sha256": "7e6411927d91205ca76c6b59f9d737cd1104d066f8cfcc0a87f481016ef30313" }, { "path": "src/font/outline.rs", @@ -300,27 +300,27 @@ }, { "path": "src/font/standard_font.rs", - "source_sha256": "048bcfb0d7d63ced49cef583c954b7507e5f078313caf5cf565adb97ad4bda46", - "source_git_blob": "c73c75dd22c7937585a0646f49bc89514ce054df", - "sha256": "048bcfb0d7d63ced49cef583c954b7507e5f078313caf5cf565adb97ad4bda46" + "source_sha256": "af3c51592c086671cb127745d4351d1380ef1212aee325099f1e933607995d7d", + "source_git_blob": "139454a0b21eb992e9e52bf73c85e965c0f86cda", + "sha256": "af3c51592c086671cb127745d4351d1380ef1212aee325099f1e933607995d7d" }, { "path": "src/font/true_type.rs", - "source_sha256": "8e8ffcf8f9749b10262793f1ecaf86ac7c65e6d5608e013d938a03adf210ad84", - "source_git_blob": "40f33590309100f295cb441069c340f9e034b49b", - "sha256": "8e8ffcf8f9749b10262793f1ecaf86ac7c65e6d5608e013d938a03adf210ad84" + "source_sha256": "278fed0cddcc43cc7d616200dcefb45720db9d560f54e41a82c281c3163b411d", + "source_git_blob": "b36266985942b4d7e2970f9edf7329c4edbcfa11", + "sha256": "278fed0cddcc43cc7d616200dcefb45720db9d560f54e41a82c281c3163b411d" }, { "path": "src/font/type1.rs", - "source_sha256": "5779b825fa6a5589978ed5b30910b7af0986ace42bac4e3652f7975a479874d4", - "source_git_blob": "32b5e3ddd9a2ca877d760bc5150bd55c3302bb33", - "sha256": "5779b825fa6a5589978ed5b30910b7af0986ace42bac4e3652f7975a479874d4" + "source_sha256": "431b1fe86ca51bd993e81084a64c3e8ed8b90a67385a30eb4ffedfdd98c6f96b", + "source_git_blob": "02f76bfb8095041e3d54df014190047de46be4c5", + "sha256": "431b1fe86ca51bd993e81084a64c3e8ed8b90a67385a30eb4ffedfdd98c6f96b" }, { "path": "src/font/type3.rs", - "source_sha256": "257f7a2c8780c8e565f46dfd7dd1e3d50bef1e2c44aae5777994addb0ca26d62", - "source_git_blob": "ccfeeb766472f16f15b04710bab711f669b94adf", - "sha256": "5bd99e9a219c4ed437f964b3b81e10381ad4f0bfdee8b2f9be1a6d3940fcd9b4" + "source_sha256": "bc33784f0dd3e599028b28b3696b9e4380cbff08f4c4e6299dee51f8df5eb08f", + "source_git_blob": "c0e0d0b2df8f38183b8e23e9e8f644c0b81d0529", + "sha256": "1778362c6c4a5cb015e756fd3f36afd7482f20a230dde0206f4339da922eaca4" }, { "path": "src/function/mod.rs", @@ -330,9 +330,9 @@ }, { "path": "src/function/type0.rs", - "source_sha256": "84cb6ccb45536ca5731394558883350f43b5e66330e07eb8d5a0e070c8e20704", - "source_git_blob": "02bbc41b84caddc9b5dfc8f49b2da081eb281199", - "sha256": "84cb6ccb45536ca5731394558883350f43b5e66330e07eb8d5a0e070c8e20704" + "source_sha256": "a10a3a8c2154c19c5c637d5ff70b87b4d28a8bb857d82f2b06de68414ef93be7", + "source_git_blob": "9a82c5978657170c44a2e2b771f2610ade5fb292", + "sha256": "a10a3a8c2154c19c5c637d5ff70b87b4d28a8bb857d82f2b06de68414ef93be7" }, { "path": "src/function/type2.rs", @@ -360,9 +360,9 @@ }, { "path": "src/interpret/mod.rs", - "source_sha256": "2b286df0c34b8194e1bdd1b63c1e08354d24f8389958fd782f42e2cabf364673", - "source_git_blob": "931b3388cef188cee0a5a1d495b69a6ff04f5e51", - "sha256": "6ae30906c8e6d0e869e0fac8ece4d97e0ac08d8abd7042c0242a069d4e609351" + "source_sha256": "2b002323055bc19aa497e92c81ad8e06dc60ee03686cad89366df4ac7c7a06b2", + "source_git_blob": "fe08fc284ba7c914d1414b68a5a346674a6f910f", + "sha256": "1ed42a7777a6265f14efaaa5899dc96b93dc53aff85afacfcac60065e6e490d5" }, { "path": "src/interpret/path.rs", @@ -408,9 +408,9 @@ }, { "path": "src/shading.rs", - "source_sha256": "a28f730ede65bb4e480d0a6ae49a21e69ed23d7b0fd6f8006e487c83faa0756d", - "source_git_blob": "98fbb58310900160d1d4067e0707708ba2f673d0", - "sha256": "a28f730ede65bb4e480d0a6ae49a21e69ed23d7b0fd6f8006e487c83faa0756d" + "source_sha256": "5d541a2d52f69c88e87a40580bf28afa4b8a6a2d62a3385009087264384ead32", + "source_git_blob": "2e619e47f2800d7fc7cbdae77c66c58f012c84c2", + "sha256": "5d541a2d52f69c88e87a40580bf28afa4b8a6a2d62a3385009087264384ead32" }, { "path": "src/soft_mask.rs", @@ -432,9 +432,9 @@ }, { "path": "src/x_object.rs", - "source_sha256": "b325843a8f4088cdb1cc1d471971bd88932fb1e538d06a20a1cc2bee138d5624", - "source_git_blob": "68d97d55554888b9afe6c16d08ab2cbaca61924e", - "sha256": "93539c173b193aa740aa7a839ec05c9676a89cbee404726767cf82b63f19bda6" + "source_sha256": "e1c51e96a335c32c122011b2d05c5020c36a33c2c73342ba173c28a3e2fe0219", + "source_git_blob": "21b134ce8cd9604399cd0776febf923d69c02673", + "sha256": "48d296abf28696c4a3e01e9d12169c754062b0840c9f2b93051435247aba9144" } ], "compatibility_reference": { diff --git a/vendor/hayro-interpret/src/cache.rs b/vendor/hayro-interpret/src/cache.rs index 2dda2e8a..e253a7b8 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/color.rs b/vendor/hayro-interpret/src/color.rs index 4db84631..af359741 100644 --- a/vendor/hayro-interpret/src/color.rs +++ b/vendor/hayro-interpret/src/color.rs @@ -22,6 +22,25 @@ use std::sync::{Arc, LazyLock, OnceLock}; /// A storage for the components of colors. pub type ColorComponents = SmallVec<[f32; 4]>; +std::thread_local! { + // PdfCraft patch (34): see `tint_transform_runs`. + static TINT_TRANSFORM_RUNS: core::cell::Cell = const { core::cell::Cell::new(0) }; +} + +/// PdfCraft patch (34): how many colours this thread has run through a Separation or DeviceN +/// tint transform, for diagnostics and tests. An image converted through patch (28)'s lookup +/// table runs it once per possible sample value, not once per pixel. Per thread, so a caller +/// counting its own render is not disturbed by renders on other threads. +pub fn tint_transform_runs() -> u64 { + TINT_TRANSFORM_RUNS.with(core::cell::Cell::get) +} + +/// PdfCraft patch (34): count `colours` runs of a tint transform (called once per batch). +fn count_tint_transform_runs(colours: usize) { + let colours = u64::try_from(colours).unwrap_or(u64::MAX); + TINT_TRANSFORM_RUNS.with(|runs| runs.set(runs.get().saturating_add(colours))); +} + /// An RGB color with an alpha channel. #[derive(Debug, Copy, Clone)] pub struct AlphaColor { @@ -857,6 +876,7 @@ impl Separation { impl ToRgb for Separation { fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> { + count_tint_transform_runs(input.len()); let evaluated = input .iter() .flat_map(|n| { @@ -911,6 +931,8 @@ impl DeviceN { impl ToRgb for DeviceN { fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> { + let colours = input.len().checked_div(usize::from(self.num_components)); + count_tint_transform_runs(colours.unwrap_or(0)); let evaluated = input .chunks_exact(self.num_components as usize) .flat_map(|n| { diff --git a/vendor/hayro-interpret/src/context.rs b/vendor/hayro-interpret/src/context.rs index 9953b419..3614aee8 100644 --- a/vendor/hayro-interpret/src/context.rs +++ b/vendor/hayro-interpret/src/context.rs @@ -150,6 +150,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/device.rs b/vendor/hayro-interpret/src/device.rs index 72d5a980..36c64a23 100644 --- a/vendor/hayro-interpret/src/device.rs +++ b/vendor/hayro-interpret/src/device.rs @@ -66,6 +66,13 @@ pub trait Device<'a> { fn actual_text(&mut self, _text: &[u8]) {} /// Called at the end of a marked content sequence (EMC). fn end_marked_content(&mut self) {} + /// PdfCraft patch (29): the page's annotations are about to be drawn (until + /// `end_annotations`), as page content finishes. Devices can use this to treat their + /// paints as annotation colours rather than document colours — the reading-colours + /// setting replaces document colours only, never the colours a comment was given. + fn begin_annotations(&mut self) {} + /// PdfCraft patch (29): the page's annotations are done; what follows is page content. + fn end_annotations(&mut self) {} } /// A device that discards all drawing operations. diff --git a/vendor/hayro-interpret/src/encode.rs b/vendor/hayro-interpret/src/encode.rs index 498ed909..b3f830ba 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/font/mod.rs b/vendor/hayro-interpret/src/font/mod.rs index 5ccf5c48..7c8c8d08 100644 --- a/vendor/hayro-interpret/src/font/mod.rs +++ b/vendor/hayro-interpret/src/font/mod.rs @@ -444,6 +444,29 @@ impl Encoding { Self::BuiltIn => None, } } + + /// PdfCraft patch: the glyph name an Annex D.2 *alias code* reads as when the glyph is taken + /// as text rather than looked up in a font program; `None` for every other code. + /// + /// Annex D.2 gives WinAnsiEncoding codes 0xA0 and 0xAD the glyph names `space` and `hyphen`, + /// the same names it gives 0x20 and 0x2D, and MacRomanEncoding 0xCA `space`. The tables above + /// call them `nbspace` and `sfthyphen`, whose Adobe Glyph List values U+00A0 and U+00AD are + /// *formatting* characters, so extraction hid ink the reader can see: a soft hyphen is + /// invisible, and text that drew one matched no search and no redaction pattern -- one corpus + /// file alone produced 104 of them -- while a non-breaking space did not match the space a + /// reader typed. pdftotext reads both codes as the ordinary character. + /// + /// Only the base table is aliased, in the text direction only. A `/Differences` entry naming + /// `sfthyphen` or `nbspace` is a deliberate statement and keeps its glyph list value, which + /// is what pdftotext does too; and glyph *lookup* is untouched, so a font program's own + /// `sfthyphen` program is still found first. + fn text_alias(&self, code: u8) -> Option<&'static str> { + match (self, code) { + (Self::WinAnsi, 0xA0) | (Self::MacRoman, 0xCA) => Some("space"), + (Self::WinAnsi, 0xAD) => Some("hyphen"), + _ => None, + } + } } /// The font stretch. diff --git a/vendor/hayro-interpret/src/font/standard_font.rs b/vendor/hayro-interpret/src/font/standard_font.rs index c73c75dd..139454a0 100644 --- a/vendor/hayro-interpret/src/font/standard_font.rs +++ b/vendor/hayro-interpret/src/font/standard_font.rs @@ -546,6 +546,14 @@ impl StandardKind { } pub(crate) fn char_code_to_unicode(&self, code: u8) -> Option { + // PdfCraft patch: an Annex D.2 alias code reads as the ordinary character; see + // `Encoding::text_alias`. `code_to_ps_name` is shared with glyph lookup and stays as it + // is, so the alias is applied here, after `/Differences` has had its say. + if !self.encodings.contains_key(&code) + && let Some(name) = self.encoding.text_alias(code) + { + return glyph_name_to_unicode(name); + } self.code_to_ps_name(code).and_then(glyph_name_to_unicode) } diff --git a/vendor/hayro-interpret/src/font/true_type.rs b/vendor/hayro-interpret/src/font/true_type.rs index 40f33590..b3626698 100644 --- a/vendor/hayro-interpret/src/font/true_type.rs +++ b/vendor/hayro-interpret/src/font/true_type.rs @@ -286,7 +286,13 @@ impl EmbeddedKind { self.differences .get(&code) .map(|s| s.as_str()) + // PdfCraft patch: an Annex D.2 alias code reads as the ordinary character; see + // `Encoding::text_alias`. + .or_else(|| self.encoding.text_alias(code)) .or_else(|| self.encoding.map_code(code)) + // The last resort reads the code as WinAnsi whatever the font said, so it takes the + // same aliasing. + .or_else(|| Encoding::WinAnsi.text_alias(code)) .or_else(|| win_ansi::get(code)) } diff --git a/vendor/hayro-interpret/src/font/type1.rs b/vendor/hayro-interpret/src/font/type1.rs index 32b5e3dd..02f76bfb 100644 --- a/vendor/hayro-interpret/src/font/type1.rs +++ b/vendor/hayro-interpret/src/font/type1.rs @@ -239,6 +239,14 @@ impl Type1Kind { } fn char_code_to_unicode(&self, code: u8) -> Option { + // PdfCraft patch: an Annex D.2 alias code reads as the ordinary character; see + // `Encoding::text_alias`. `code_to_ps_name` is shared with glyph lookup and stays as it + // is, so the alias is applied here, after `/Differences` has had its say. + if !self.encodings.contains_key(&code) + && let Some(name) = self.encoding.text_alias(code) + { + return glyph_name_to_unicode(name); + } self.code_to_ps_name(code).and_then(glyph_name_to_unicode) } } @@ -336,6 +344,14 @@ impl CffKind { } fn char_code_to_unicode(&self, code: u8) -> Option { + // PdfCraft patch: an Annex D.2 alias code reads as the ordinary character; see + // `Encoding::text_alias`. `code_to_ps_name` is shared with glyph lookup and stays as it + // is, so the alias is applied here, after `/Differences` has had its say. + if !self.encodings.contains_key(&code) + && let Some(name) = self.encoding.text_alias(code) + { + return glyph_name_to_unicode(name); + } self.code_to_ps_name(code).and_then(glyph_name_to_unicode) } } diff --git a/vendor/hayro-interpret/src/font/type3.rs b/vendor/hayro-interpret/src/font/type3.rs index 6824d4ff..216f2618 100644 --- a/vendor/hayro-interpret/src/font/type3.rs +++ b/vendor/hayro-interpret/src/font/type3.rs @@ -108,6 +108,9 @@ impl<'a> Type3<'a> { .encodings .get(&code) .map(|s| s.as_str()) + // PdfCraft patch: an Annex D.2 alias code reads as the ordinary character; + // see `Encoding::text_alias`. + .or_else(|| self.encoding.text_alias(code)) .or_else(|| self.encoding.map_code(code)); name.and_then(crate::font::glyph_name_to_unicode) .or_else(|| (0x20..0x7f).contains(&code).then_some(code as char)) @@ -165,9 +168,10 @@ impl<'a> Type3<'a> { is_shape_glyph }; + // PdfCraft patch: context clips and shading bounds are in device space; FontBBox is in glyph space. let mut context = Context::new_with( state.ctm, - self.font_bbox, + root_transform.transform_rect_bbox(self.font_bbox), &glyph.cache, glyph.xref, glyph.settings.clone(), diff --git a/vendor/hayro-interpret/src/function/type0.rs b/vendor/hayro-interpret/src/function/type0.rs index 02bbc41b..9a82c597 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/interpret/mod.rs b/vendor/hayro-interpret/src/interpret/mod.rs index a0c6308e..b389b6b3 100644 --- a/vendor/hayro-interpret/src/interpret/mod.rs +++ b/vendor/hayro-interpret/src/interpret/mod.rs @@ -175,6 +175,9 @@ pub fn interpret_page<'a>( if context.settings.render_annotations && let Some(annot_arr) = page.raw().get::>(ANNOTS) { + // PdfCraft patch (29): bracket the annotations so devices can tell their paints from + // the document's own (the reading-colours setting recolours the document, not comments). + device.begin_annotations(); for annot in annot_arr.iter::>() { let flags = annot.get::(F).unwrap_or(0); @@ -275,6 +278,7 @@ pub fn interpret_page<'a>( draw_highlight_without_appearance(&annot, context, device); } } + device.end_annotations(); } } diff --git a/vendor/hayro-interpret/src/shading.rs b/vendor/hayro-interpret/src/shading.rs index 98fbb583..2e619e47 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 767a54f3..d7d19dce 100644 --- a/vendor/hayro-interpret/src/x_object.rs +++ b/vendor/hayro-interpret/src/x_object.rs @@ -60,6 +60,9 @@ pub(crate) struct FormXObject<'a> { pub(crate) matrix: Affine, pub(crate) bbox: [f32; 4], is_transparency_group: bool, + // PdfCraft patch: the group's /I and /K (ISO 32000-2 §11.6.6, both false by default). + is_isolated: bool, + is_knockout: bool, pub(crate) dict: Dict<'a>, resources: Option>, } @@ -77,12 +80,17 @@ impl<'a> FormXObject<'a> { .unwrap_or([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]), ); let bbox = dict.get::<[f32; 4]>(BBOX)?; - let is_transparency_group = dict.get::>(GROUP).is_some(); + let group = dict.get::>(GROUP); + let is_transparency_group = group.is_some(); + let is_isolated = group.as_ref().and_then(|g| g.get::(I)).unwrap_or(false); + let is_knockout = group.as_ref().and_then(|g| g.get::(K)).unwrap_or(false); Some(Self { decoded, matrix, is_transparency_group, + is_isolated, + is_knockout, bbox, dict: dict.clone(), resources, @@ -134,7 +142,26 @@ pub(crate) fn draw_form_xobject<'a, 'b>( context.pre_concat_affine(x_object.matrix); context.push_root_transform(); - if x_object.is_transparency_group { + // PdfCraft patch: a transparency group that is neither isolated nor knockout composites + // its elements with the backdrop already on the page (§11.4.7), so when it is entered at + // full opacity with no soft mask and Normal blending there is nothing for a layer of its + // own to add, and drawing straight onto the page is the exact result. A layer here was + // wrong, not just slower: a blend mode inside the group (the `/BM /Multiply` of a highlight + // appearance written by other editors) saw the layer's transparent backdrop instead of the + // text, and the layer then covered the page as an opaque block (#1968). An isolated group + // starts from a blank backdrop by definition, and a knockout group or one entered at partial + // opacity, under a soft mask or with a blend mode keeps its layer, where the group's + // elements still composite with a blank backdrop (an approximation of §11.4.7 for them). + let neutral = !x_object.is_isolated && !x_object.is_knockout && { + let state = &context.get().graphics_state; + state.non_stroke_alpha >= 1.0 + && state.stroke_alpha >= 1.0 + && state.soft_mask.is_none() + && state.blend_mode == BlendMode::Normal + }; + let own_layer = x_object.is_transparency_group && !neutral; + + if own_layer { device.push_transparency_group( context.get().graphics_state.non_stroke_alpha, std::mem::take(&mut context.get_mut().graphics_state.soft_mask), @@ -169,7 +196,7 @@ pub(crate) fn draw_form_xobject<'a, 'b>( device.pop_clip_path(); - if x_object.is_transparency_group { + if own_layer { device.pop_transparency_group(); } @@ -407,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 } @@ -440,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 { @@ -667,23 +725,38 @@ fn decode_raster( ctx.bits_per_component, )?; - let mut f32_data = apply_decode_array( + // PdfCraft patch (28): an image with few possible pixel values converts each of them once. + let mut rgb_data = if let Some(rgb) = rgb_via_lookup( &components, - &ctx.color_space, - ctx.bits_per_component, - &ctx.decode_arr, - )?; - - fix_image_length(&mut f32_data, ctx.width, &mut height, 0.0, &ctx.color_space)?; - - let mut rgb_data = get_rgb_data( - &f32_data, ctx.width, height, ctx.scale_factors, &ctx.color_space, + ctx.bits_per_component, + &ctx.decode_arr, obj.interpolate, - ); + ) { + height = rgb.height; + Some(rgb) + } else { + let mut f32_data = apply_decode_array( + &components, + &ctx.color_space, + ctx.bits_per_component, + &ctx.decode_arr, + )?; + + fix_image_length(&mut f32_data, ctx.width, &mut height, 0.0, &ctx.color_space)?; + + get_rgb_data( + &f32_data, + ctx.width, + height, + ctx.scale_factors, + &ctx.color_space, + obj.interpolate, + ) + }; if let Some(transfer_function) = &obj.transfer_function && let Some(rgb_data) = &mut rgb_data @@ -1034,27 +1107,136 @@ fn apply_decode_array( bits_per_component: u8, decode: &[(f32, f32)], ) -> Option> { - let interpolate = |n: f32, d_min: f32, d_max: f32| { - interpolate( - n, - 0.0, - 2.0_f32.powi(bits_per_component as i32) - 1.0, - d_min, - d_max, - ) - }; - let mut decoded_arr = vec![]; for pixel in components.chunks(color_space.num_components() as usize) { - for (component, (d_min, d_max)) in pixel.iter().zip(decode) { - decoded_arr.push(interpolate(*component as f32, *d_min, *d_max)); + for (component, range) in pixel.iter().zip(decode) { + decoded_arr.push(decode_sample(*component, bits_per_component, *range)); } } Some(decoded_arr) } +/// One sample mapped through its `/Decode` range (ISO 32000-2 §8.9.5.2). PdfCraft patch (28): +/// shared by [`apply_decode_array`] and [`rgb_via_lookup`], so both decode alike. +fn decode_sample(sample: u16, bits_per_component: u8, (d_min, d_max): (f32, f32)) -> f32 { + interpolate( + sample as f32, + 0.0, + 2.0_f32.powi(bits_per_component as i32) - 1.0, + d_min, + d_max, + ) +} + +/// PdfCraft patch (28): the most bits a pixel's samples may hold together for +/// [`rgb_via_lookup`] (a table of 65,536 colours). +const MAX_LOOKUP_BITS: usize = 16; + +/// PdfCraft patch (28): the RGB of an image whose pixels hold at most [`MAX_LOOKUP_BITS`] bits +/// of samples, converted once per possible pixel value rather than once per pixel, at the +/// height `fix_image_length` gives. `None` when the table would not pay off (the image has no +/// more pixels than it has entries) or does not apply; the caller then converts every pixel. +/// +/// The colour of a pixel depends only on its own samples, so the result is the per-pixel one, +/// except where a tint transform gives the wrong number of components. The per-pixel batch then +/// shifts the colours after that pixel. The table converts each entry on its own +/// ([`convert_each`]), so a wrong-sized entry changes only itself, and a value the image never +/// uses cannot change the colour of one it does. +/// A Separation or DeviceN tint transform is a PDF function, often a PostScript calculator +/// program: run once per pixel, an 8-bit spot-colour background of 3304 × 4105 took four +/// seconds and 430 MB to draw; looked up from 256 entries it takes milliseconds. +#[allow(clippy::too_many_arguments)] +fn rgb_via_lookup( + components: &[u16], + width: u32, + height: u32, + scale_factors: (f32, f32), + cs: &ColorSpace, + bits_per_component: u8, + decode: &[(f32, f32)], + interpolate: bool, +) -> Option { + let n = cs.num_components() as usize; + let bpc = usize::from(bits_per_component); + let bits = bpc.checked_mul(n)?; + // `apply_decode_array` decodes only the samples `/Decode` has a range for: leave a short + // array to it, and every sample size it doesn't read. + if n == 0 || bits == 0 || bits > MAX_LOOKUP_BITS || bpc > 16 || decode.len() < n { + return None; + } + // The length rules of `fix_image_length`: extra samples are dropped; missing ones shorten + // the image to the rows begun, the last row padded with decoded zeros. + let row_len = usize::try_from(width).ok()?.checked_mul(n)?; + let wanted = row_len.checked_mul(usize::try_from(height).ok()?)?; + let (height, used) = if wanted <= components.len() { + (height, wanted) + } else { + (u32::try_from(components.len().div_ceil(row_len)).ok()?, components.len()) + }; + let pixels = usize::try_from(width).ok()?.checked_mul(usize::try_from(height).ok()?)?; + let total = pixels.checked_mul(n)?; + let entries = 1usize << bits; + if pixels <= entries { + return None; + } + + // Every possible pixel value, its samples in order, decoded as `apply_decode_array` would. + let mask = (1usize << bpc) - 1; + let mut table_input = Vec::with_capacity(entries * n); + for code in 0..entries { + for (k, range) in decode.iter().take(n).enumerate() { + let sample = ((code >> (bpc * (n - 1 - k))) & mask) as u16; + table_input.push(decode_sample(sample, bits_per_component, *range)); + } + } + let mut table = vec![0; entries * 3]; + convert_each(cs, &table_input, n, &mut table); + + let mut data = vec![0; pixels.checked_mul(3)?]; + let whole = used / n; + let samples = components.get(..whole * n)?; + for (pixel, rgb) in samples.chunks_exact(n).zip(data.chunks_exact_mut(3)) { + let code = pixel.iter().fold(0usize, |code, s| (code << bpc) | (usize::from(*s) & mask)); + if let Some(colour) = table.get(code * 3..code * 3 + 3) { + rgb.copy_from_slice(colour); + } + } + // The pixels past the samples present (at most a row): converted as before, padding and all. + if whole < pixels { + let tail: Vec = (whole * n..total) + .map(|i| match (components.get(i), decode.get(i % n)) { + (Some(s), Some(range)) if i < used => decode_sample(*s, bits_per_component, *range), + _ => 0.0, + }) + .collect(); + let _ = cs.convert_f32(&tail, data.get_mut(whole * 3..)?, false); + } + + Some(RgbData { + data, + width, + height, + interpolate, + scale_factors, + }) +} + +/// PdfCraft patch (28): `input`, `n` decoded samples a pixel, converted into `output` (RGB) one +/// pixel at a time. A Separation or DeviceN tint transform may give more or fewer components +/// than its alternate space takes (a Type 4 program's count is only warned about): converted as +/// one batch, such a result shifts every pixel after it; converted alone, it affects only its +/// own. A failed conversion leaves black, as in `get_rgb_data`. +fn convert_each(cs: &ColorSpace, input: &[f32], n: usize, output: &mut [u8]) { + if n == 0 { + return; + } + for (pixel, rgb) in input.chunks_exact(n).zip(output.chunks_exact_mut(3)) { + let _ = cs.convert_f32(pixel, rgb, false); + } +} + /// PdfCraft patch: whether an image or mask of `width` × `height` may be decoded (at most /// 2^28 pixels, the renderer's `MAX_IMAGE_PIXELS`). pub fn image_size_ok(width: u32, height: u32) -> bool { diff --git a/vendor/hayro-jbig2/SOURCE.json b/vendor/hayro-jbig2/SOURCE.json index 50ec8937..bf51f3bb 100644 --- a/vendor/hayro-jbig2/SOURCE.json +++ b/vendor/hayro-jbig2/SOURCE.json @@ -1,7 +1,7 @@ { "crate": "hayro-jbig2", "source_repository": "https://github.com/storytold/pdfcraft", - "source_revision": "7437bde378f43856eada40af25b983812e615e8a", + "source_revision": "ec0a965074f376a5bf610435948e4e5184bf3299", "source_directory": "vendor/hayro-jbig2", "files": [ { diff --git a/vendor/hayro-syntax/SOURCE.json b/vendor/hayro-syntax/SOURCE.json index 721bac61..6419aee4 100644 --- a/vendor/hayro-syntax/SOURCE.json +++ b/vendor/hayro-syntax/SOURCE.json @@ -1,7 +1,7 @@ { "crate": "hayro-syntax", "source_repository": "https://github.com/storytold/pdfcraft", - "source_revision": "7437bde378f43856eada40af25b983812e615e8a", + "source_revision": "ec0a965074f376a5bf610435948e4e5184bf3299", "source_directory": "vendor/hayro-syntax", "files": [ { @@ -132,9 +132,9 @@ }, { "path": "src/filter/ascii_85.rs", - "source_sha256": "df32c6783b1c1969f9b6b03de3bf204384dfdba767f7beefdd8baa8590387d24", - "source_git_blob": "5a18cf8352d7d5a1770b4ea3068254187ff4b4f9", - "sha256": "df32c6783b1c1969f9b6b03de3bf204384dfdba767f7beefdd8baa8590387d24" + "source_sha256": "550cd5b3ceb7ee9342959ac9cc80c35b176aed3c7f061789e379beb8b5873f48", + "source_git_blob": "943bfeb742ac8cdc8ba64fcc538a044cb812577e", + "sha256": "550cd5b3ceb7ee9342959ac9cc80c35b176aed3c7f061789e379beb8b5873f48" }, { "path": "src/filter/ascii_hex.rs", @@ -144,9 +144,9 @@ }, { "path": "src/filter/ccitt.rs", - "source_sha256": "101e2e7e69ece280a258edcadd29f5ea0442ce6822cf1dd1906dca41a69436bf", - "source_git_blob": "98924f2dbdd942bc6ee64f66749b8d2496d8b45e", - "sha256": "101e2e7e69ece280a258edcadd29f5ea0442ce6822cf1dd1906dca41a69436bf" + "source_sha256": "45d7eb89934d28cff97a00c7b264010bccfd14f3d511e1307ad50a9fef3b53c8", + "source_git_blob": "685d680d2fbadce608a9bd3ca925c2ccc6c5d641", + "sha256": "45d7eb89934d28cff97a00c7b264010bccfd14f3d511e1307ad50a9fef3b53c8" }, { "path": "src/filter/dct.rs", @@ -162,21 +162,21 @@ }, { "path": "src/filter/jpx.rs", - "source_sha256": "a9cab59f286bf3d3c99aa008f8fdede5028117c0f36018f3c02e52ed097c299a", - "source_git_blob": "6a788591e3338c55eccd616e21aa6e683f8d3351", - "sha256": "a9cab59f286bf3d3c99aa008f8fdede5028117c0f36018f3c02e52ed097c299a" + "source_sha256": "f0a2b01c692c086d30a26562d7548f9e837af166be737d2c543de7c886a45f82", + "source_git_blob": "74efd8b31e2b2935ff2bbceda2c1ffd36ea9c20d", + "sha256": "f0a2b01c692c086d30a26562d7548f9e837af166be737d2c543de7c886a45f82" }, { "path": "src/filter/lzw_flate.rs", - "source_sha256": "e2d2196bc47c00474dc8f61d281ba327b88aa0d5f9e5475d29bf63a0572522e3", - "source_git_blob": "f9da1590b27b3e582ddac2fdfe9c1ef80ea72aff", - "sha256": "e1a6125b27423c91d79127ce6f8bfec507bd4d6ab0f959b2740a8a6e392d3e49" + "source_sha256": "db51a57effe90ad9db2d314e16081ce90e3ef99e5a5f46fcc4bdbdacfd13a528", + "source_git_blob": "f1ab1aae73e4c84ec0993a6eb96594018ca7cc91", + "sha256": "00d478a55b0c123d903768186592d138d563543abf46e5c786c67b9fffd69637" }, { "path": "src/filter/mod.rs", - "source_sha256": "5e6d62da5b1fd023f85f79bc8ade928b508214e13668f9fc99f3a08bc4d48a15", - "source_git_blob": "22bebf6d3287f1dc893c67b76a139108b28a8188", - "sha256": "8cc6c2572519046bd27adfc597630045e453829a954eaf1747fee7a447e5c5cb" + "source_sha256": "0af7145ab16b15b7a60c4b57efecb9bd5fd0cfdc9bb5f9388abb695386e63e49", + "source_git_blob": "06bda29487d62c25dc39c1ee109091cb17e3867f", + "sha256": "e33e74ee55dcc850db9eb64811c56b4ffe1932908ab9684dfba07f4277b1eea9" }, { "path": "src/filter/run_length.rs", @@ -276,9 +276,9 @@ }, { "path": "src/object/stream.rs", - "source_sha256": "151721c7bfe558a6942aba323d3185cf6db5bf726bd905326881c70ef480240f", - "source_git_blob": "833610817b4738304a7376cd43ea079cb352bd8c", - "sha256": "e83d7ecb01a038d5a0ae589d77e877f9521097010e5f88db8f8a333fd161aff1" + "source_sha256": "f5229eb2f52c9918830027c93112e042a9e97d34b5fec72f97dcc7de8c5410ac", + "source_git_blob": "a80f0730cba317ed3771cee61d6a7ab18de296d7", + "sha256": "6cd27e9952715244a35d3f2ea610f401ae75fe60cf9740b69257a7ca704cce0b" }, { "path": "src/object/string.rs", @@ -294,9 +294,9 @@ }, { "path": "src/page.rs", - "source_sha256": "19d0e7604a3a692956e523128022d07944253b308793598795e09595cb04e79e", - "source_git_blob": "7d63931a324f5faf90bb37e0a90670643567dace", - "sha256": "681681016eda0d1cdbec10d2cb0305c5f439724ec5d0b3be218a18e99b5e72b8" + "source_sha256": "17be32b2eaa03a3367f1f20040556f7ee5b207588cf44a1af3e5253386fb46f9", + "source_git_blob": "d7f0ec953722a938508f1f31f0adaecc0a181d20", + "sha256": "8993224b8f403c95c32803113139fe4c8fc25338571466b37bad0874b21f8145" }, { "path": "src/pdf.rs", diff --git a/vendor/hayro-syntax/src/filter/ascii_85.rs b/vendor/hayro-syntax/src/filter/ascii_85.rs index 5a18cf83..943bfeb7 100644 --- a/vendor/hayro-syntax/src/filter/ascii_85.rs +++ b/vendor/hayro-syntax/src/filter/ascii_85.rs @@ -4,7 +4,9 @@ use crate::reader::Reader; use crate::trivia::is_white_space_character; use alloc::vec::Vec; -pub(crate) fn decode(data: &[u8]) -> Option> { +/// PdfCraft patch: `limit` is the most bytes produced (see `filter::decode_limit`); each `z` +/// expands one input byte to four. +pub(crate) fn decode(data: &[u8], limit: usize) -> Option> { const POW_85: [u32; 5] = [52200625, 614125, 7225, 85, 1]; let mut reader = Reader::new(data); @@ -50,15 +52,21 @@ pub(crate) fn decode(data: &[u8]) -> Option> { .checked_add(digits[3].checked_mul(POW_85[3])?)? .checked_add(digits[4])?; - decoded.extend_from_slice(&value.to_be_bytes()[..output_len]); + let left = limit.saturating_sub(decoded.len()); + decoded.extend_from_slice(&value.to_be_bytes()[..output_len.min(left)]); group.clear(); Some(()) }; - let mut decoded = Vec::with_capacity(data.len() * 4 / 5); + let mut decoded = Vec::with_capacity((data.len() / 5 * 4).min(limit)); let mut group = Vec::with_capacity(5); loop { + // PdfCraft patch: output cut at the limit is reported, never dropped silently. + if decoded.len() >= limit { + warn!("ASCII85 stream stopped at its decode limit of {limit} bytes"); + return Some(decoded); + } let Some(b) = read_byte() else { // Be lenient and accept what we have (see PDFBOX-5910). flush_group(&mut group, &mut decoded)?; @@ -76,7 +84,8 @@ pub(crate) fn decode(data: &[u8]) -> Option> { } b'z' => { flush_group(&mut group, &mut decoded)?; - decoded.extend_from_slice(&[0, 0, 0, 0]); + let left = limit.saturating_sub(decoded.len()); + decoded.extend(core::iter::repeat_n(0, left.min(4))); } b'~' => { // Technically requires a '>', but there is a PDF where it isn't diff --git a/vendor/hayro-syntax/src/filter/ccitt.rs b/vendor/hayro-syntax/src/filter/ccitt.rs index 98924f2d..685d680d 100644 --- a/vendor/hayro-syntax/src/filter/ccitt.rs +++ b/vendor/hayro-syntax/src/filter/ccitt.rs @@ -15,7 +15,15 @@ pub(crate) fn decode( ) -> Option> { let k = params.get::(K).unwrap_or(0); - let rows = params.get::(ROWS).unwrap_or(image_params.height); + // PdfCraft patch: `/Rows 0` means the height is not predetermined (ISO 32000-2 Table 11, and + // the default), not zero rows. Scanners write it on Group 4 pages, which rendered blank. + // PdfCraft patch (19): fewer `/Rows` than the image's `/Height` doesn't cut the image short: a + // plotter driver wrote `/Rows` equal to `/Columns`, and the rows after it were drawn black + // (#3308). The decoder still stops where the data ends. + let rows = params + .get::(ROWS) + .filter(|&rows| rows != 0) + .map_or(image_params.height, |rows| rows.max(image_params.height)); let end_of_block = params.get::(END_OF_BLOCK).unwrap_or(true); let settings = DecodeSettings { diff --git a/vendor/hayro-syntax/src/filter/jpx.rs b/vendor/hayro-syntax/src/filter/jpx.rs index 6a788591..74efd8b3 100644 --- a/vendor/hayro-syntax/src/filter/jpx.rs +++ b/vendor/hayro-syntax/src/filter/jpx.rs @@ -26,7 +26,18 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option image, + Err(_) => { + repaired = without_refused_enumerated_colour(data)?; + hayro_jpeg2000::Image::new(&repaired, &settings).ok()? + } + }; let width = image.width(); let height = image.height(); @@ -87,6 +98,82 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option Option> { + let header = boxes(data).find(|&(kind, _, _)| kind == *b"jp2h")?; + let (_, start, end) = header; + let inner = data.get(start..end)?; + let (_, ih_start, _) = boxes(inner).find(|&(kind, _, _)| kind == *b"ihdr")?; + // ihdr: height (4), width (4), number of components (2). + let components = u16::from_be_bytes(inner.get(ih_start + 8..ih_start + 10)?.try_into().ok()?); + let replacement: u32 = match components { + 1 => 17, + 3 => 16, + 4 => 12, + _ => return None, + }; + // Where each refused enum is, found before anything is copied: a stream the decoder refused + // for another reason isn't copied for nothing. + let mut refused = Vec::new(); + for (kind, body, body_end) in boxes(inner) { + if kind != *b"colr" { + continue; + } + // colr: METH (1), PREC (1), APPROX (1), then EnumCS (4) when METH is 1. + let colr = inner.get(body..body_end)?; + if colr.first() != Some(&1) { + continue; + } + let enumerated = u32::from_be_bytes(colr.get(3..7)?.try_into().ok()?); + if DECODED_ENUMERATED.contains(&enumerated) { + continue; + } + refused.push(start.checked_add(body)?.checked_add(3)?); + } + if refused.is_empty() { + return None; + } + let mut out = data.to_vec(); + for at in refused { + out.get_mut(at..at.checked_add(4)?)?.copy_from_slice(&replacement.to_be_bytes()); + } + Some(out) +} + +/// The boxes in `data` (ISO 15444-1 I.4): each one's type and the start and end of its contents. +/// Stops at the first box whose length doesn't fit. +fn boxes(data: &[u8]) -> impl Iterator + '_ { + let mut at = 0usize; + core::iter::from_fn(move || { + let length = u32::from_be_bytes(data.get(at..at.checked_add(4)?)?.try_into().ok()?); + let kind: [u8; 4] = data.get(at + 4..at.checked_add(8)?)?.try_into().ok()?; + let (header, size) = match length { + // The rest of the data. + 0 => (8, data.len().checked_sub(at)?), + // An 8-byte length follows the type. + 1 => { + let large = u64::from_be_bytes(data.get(at + 8..at.checked_add(16)?)?.try_into().ok()?); + (16, usize::try_from(large).ok()?) + } + n => (8, usize::try_from(n).ok()?), + }; + let end = at.checked_add(size)?; + if size < header || end > data.len() { + return None; + } + let item = (kind, at + header, end); + at = end; + Some(item) + }) +} + fn scale( data: &[u8], bit_per_component: u8, @@ -134,7 +221,7 @@ fn scale( #[cfg(test)] mod tests { - use super::scale; + use super::{scale, without_refused_enumerated_colour}; use crate::bit_reader::{BitReader, bit_mask}; use alloc::vec::Vec; @@ -188,4 +275,87 @@ mod tests { } } } + + /// A JP2 header (`jp2h`) holding `ihdr` for `components` and the given `colr` bodies, with + /// a codestream box after it. + fn jp2(components: u16, colrs: &[&[u8]]) -> Vec { + let jp2_box = |kind: &[u8], body: &[u8]| { + let mut b = ((8 + body.len()) as u32).to_be_bytes().to_vec(); + b.extend_from_slice(kind); + b.extend_from_slice(body); + b + }; + let mut ihdr = Vec::new(); + ihdr.extend_from_slice(&[0, 0, 0, 1, 0, 0, 0, 1]); + ihdr.extend_from_slice(&components.to_be_bytes()); + ihdr.extend_from_slice(&[7, 7, 0, 0]); + let mut header = jp2_box(b"ihdr", &ihdr); + for c in colrs { + header.extend(jp2_box(b"colr", c)); + } + let mut out = jp2_box(b"ftyp", b"jp2 \0\0\0\0jp2 "); + out.extend(jp2_box(b"jp2h", &header)); + out.extend(jp2_box(b"jp2c", &[0xff, 0x4f])); + out + } + + fn enumerated(e: u32) -> Vec { + let mut c = alloc::vec![1, 0, 0]; + c.extend_from_slice(&e.to_be_bytes()); + c + } + + /// The enumerated colour spaces found in the copy's `colr` boxes, in order. + fn enums_in(data: &[u8]) -> Vec { + data.windows(4) + .enumerate() + .filter(|(_, w)| *w == b"colr") + .filter(|(i, _)| data.get(i + 4) == Some(&1)) + .map(|(i, _)| u32::from_be_bytes(data[i + 7..i + 11].try_into().unwrap())) + .collect() + } + + #[test] + fn refused_enumerated_colour_spaces_become_the_component_counts_default() { + // CIEJab (19) and other refused spaces, by component count. + for (components, refused, expected) in [(3, 19, 16), (3, 22, 16), (1, 15, 17), (4, 13, 12), (3, 1, 16)] { + let data = jp2(components, &[&enumerated(refused)]); + let out = without_refused_enumerated_colour(&data).unwrap(); + assert_eq!(enums_in(&out), [expected], "{refused} with {components} component(s)"); + assert_eq!(out.len(), data.len(), "only the enum changes"); + } + // Several colour specifications: each refused one, and only those. + let out = without_refused_enumerated_colour(&jp2(3, &[&enumerated(19), &enumerated(14), &enumerated(24)])).unwrap(); + assert_eq!(enums_in(&out), [16, 14, 16]); + // An 8-byte box length (LBox 1) is read too. + let mut long = jp2(3, &[&enumerated(19)]); + let at = long.windows(4).position(|w| w == b"jp2h").unwrap() - 4; + let len = u32::from_be_bytes(long[at..at + 4].try_into().unwrap()) as u64 + 8; + long.splice(at..at + 4, [0, 0, 0, 1]); + long.splice(at + 8..at + 8, (len).to_be_bytes()); + assert_eq!(enums_in(&without_refused_enumerated_colour(&long).unwrap()), [16]); + } + + #[test] + fn what_the_decoder_handles_or_cant_be_read_is_left_alone() { + for known in [12, 14, 16, 17, 18, 20, 21] { + assert_eq!(without_refused_enumerated_colour(&jp2(3, &[&enumerated(known)])), None, "{known}"); + } + // An ICC profile (METH 2), a component count with no default, no header at all. + // (Its bytes where an enum would be read say 19: only METH 1 is an enum.) + assert_eq!(without_refused_enumerated_colour(&jp2(3, &[&[2, 0, 0, 0, 0, 0, 19, 0, 0]])), None); + assert_eq!(without_refused_enumerated_colour(&jp2(2, &[&enumerated(19)])), None); + assert_eq!(without_refused_enumerated_colour(&[0xff, 0x4f, 0xff, 0x51]), None); + // Damaged lengths: none of these may panic, and none is changed. + let good = jp2(3, &[&enumerated(19)]); + for cut in 0..good.len() { + let _ = without_refused_enumerated_colour(&good[..cut]); + } + for bad in [0x0000_0004_u32, 0x7fff_ffff, 0xffff_ffff] { + let mut data = good.clone(); + let at = data.windows(4).position(|w| w == b"colr").unwrap() - 4; + data[at..at + 4].copy_from_slice(&bad.to_be_bytes()); + assert_eq!(without_refused_enumerated_colour(&data), None, "{bad:#x}"); + } + } } diff --git a/vendor/hayro-syntax/src/filter/lzw_flate.rs b/vendor/hayro-syntax/src/filter/lzw_flate.rs index e65e83bd..9642a749 100644 --- a/vendor/hayro-syntax/src/filter/lzw_flate.rs +++ b/vendor/hayro-syntax/src/filter/lzw_flate.rs @@ -560,23 +560,27 @@ pub(crate) mod flate { /// Copies `left` bytes from `distance` back in the output, up to the limit. Returns how /// many are still to copy. A copy whose source does not exist writes nothing. - fn copy_back(&mut self, valid: bool, distance: usize, mut left: usize) -> usize { - while left > 0 && self.output.len() < self.limit { - if valid { - let byte = self - .output - .len() - .checked_sub(distance) - .and_then(|index| self.output.get(index)) - .copied(); - if let Some(byte) = byte { - self.output.push(byte); - } + fn copy_back(&mut self, valid: bool, distance: usize, left: usize) -> usize { + let len = self.output.len(); + // Without a source the copy writes nothing, and it runs out only at the limit. + let Some(mut start) = len.checked_sub(distance).filter(|_| valid && distance > 0) else { + return if len < self.limit { 0 } else { left }; + }; + // PdfCraft patch: copy in runs of up to `distance` bytes. Each run reads only bytes + // already in the output, so the bytes are the ones a byte-at-a-time copy writes. + let copy = left.min(self.limit.saturating_sub(len)); + let mut done = 0; + while done < copy { + let end = start.saturating_add((copy - done).min(distance)); + if end > self.output.len() { + break; } - left -= 1; + self.output.extend_from_within(start..end); + done += end - start; + start = end; } - left + left - done } fn read_dynamic_tables(&mut self) -> Option<(HuffmanTable, HuffmanTable)> { @@ -1318,6 +1322,52 @@ mod tests { ); } + /// PdfCraft patch: 300 zero bytes, zlib-compressed: one literal, then copies of distance 1 that + /// are longer than their distance. + const ZEROS_300: [u8; 13] = [0x78, 0xda, 0x63, 0x60, 0x18, 0x05, 0xc4, 0x02, 0x00, 0x01, 0x2c, 0x00, 0x01]; + + #[test] + fn back_reference_longer_than_its_distance_repeats_its_source() { + let mut state = flate::FlateState::new(&ZEROS_300); + let mut out = Vec::new(); + state.decode_into(&ZEROS_300, &mut out, 1000); + + assert_eq!(out, vec![0; 300]); + } + + #[test] + fn back_reference_cut_by_the_limit_resumes_in_the_next_call() { + let mut state = flate::FlateState::new(&ZEROS_300); + let mut out = Vec::new(); + state.decode_into(&ZEROS_300, &mut out, 100); + assert_eq!(out, vec![0; 100]); + + state.decode_into(&ZEROS_300, &mut out, 1000); + assert_eq!(out, vec![0; 300]); + } + + #[test] + fn back_reference_reads_bytes_written_by_an_earlier_call() { + // "abc" repeated 2000 times, zlib-compressed: copies reach 3 bytes back, and the calls below + // stop every 7 bytes, so most copies start in an earlier call's output. + const ABC_2000: [u8; 32] = [ + 0x78, 0xda, 0xed, 0xc2, 0x41, 0x11, 0x00, 0x00, 0x0c, 0x02, 0xa0, 0xac, 0x6a, 0xff, 0x0e, 0x0b, + 0xb1, 0x2f, 0x1c, 0xe9, 0xa2, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xfe, 0x1e, 0x2f, 0x01, 0xf9, 0x59, + ]; + let expected = b"abc".repeat(2000); + let mut state = flate::FlateState::new(&ABC_2000); + let mut out = Vec::new(); + loop { + let before = out.len(); + state.decode_into(&ABC_2000, &mut out, before + 7); + if out.len() == before || out.len() >= expected.len() { + break; + } + } + + assert_eq!(out, expected); + } + #[test] fn predictor_paeth() { predictor_test( diff --git a/vendor/hayro-syntax/src/filter/mod.rs b/vendor/hayro-syntax/src/filter/mod.rs index 90ffd76e..e3622795 100644 --- a/vendor/hayro-syntax/src/filter/mod.rs +++ b/vendor/hayro-syntax/src/filter/mod.rs @@ -95,7 +95,7 @@ impl Filter { Self::AsciiHexDecode => ascii_hex::decode(data) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), - Self::Ascii85Decode => ascii_85::decode(data) + Self::Ascii85Decode => ascii_85::decode(data, limit) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), Self::RunLengthDecode => run_length::decode(data, limit) diff --git a/vendor/hayro-syntax/src/object/stream.rs b/vendor/hayro-syntax/src/object/stream.rs index c975ed9f..ac911e85 100644 --- a/vendor/hayro-syntax/src/object/stream.rs +++ b/vendor/hayro-syntax/src/object/stream.rs @@ -351,14 +351,25 @@ impl FilterResult<'_> { } } +/// PdfCraft patch (20): the end-of-line after the `stream` keyword. Microsoft Reporting Services +/// writes `stream \r\n`, a space before the line break, which the specification does not allow. +/// Without this the content stream was taken to start at the space, failed to inflate, and the +/// page rendered blank (bank statements, invoices) while other viewers showed it. Only spaces and +/// tabs are skipped, and only when a line break follows, so stream data is never swallowed. +fn forward_stream_eol(r: &mut Reader<'_>) -> Option<()> { + r.forward_while(|b| b == b' ' || b == b'\t'); + r.forward_tag(b"\n") + .or_else(|| r.forward_tag(b"\r\n")) + // Technically not allowed, but no reason to not try it. + .or_else(|| r.forward_tag(b"\r")) +} + fn parse_proper<'a>(r: &mut Reader<'a>, dict: &Dict<'a>) -> Option> { let length = dict.get::(LENGTH)?; r.skip_white_spaces_and_comments(); r.forward_tag(b"stream")?; - r.forward_tag(b"\n") - .or_else(|| r.forward_tag(b"\r\n")) - .or_else(|| r.forward_tag(b"\r"))?; + forward_stream_eol(r)?; let data = r.read_bytes(length as usize)?; r.skip_white_spaces(); r.forward_tag(b"endstream")?; @@ -374,11 +385,7 @@ fn parse_fallback<'a>(r: &mut Reader<'a>, dict: &Dict<'a>) -> Option> // A wrong `/Length` still recovers, because that keyword sits directly after the dict. r.skip_white_spaces_and_comments(); r.forward_tag(b"stream")?; - - r.forward_tag(b"\n") - .or_else(|| r.forward_tag(b"\r\n")) - // Technically not allowed, but no reason to not try it. - .or_else(|| r.forward_tag(b"\r"))?; + forward_stream_eol(r)?; let tail = r.tail()?; let endstream_offset = find_needle(tail, b"endstream")?; diff --git a/vendor/hayro-syntax/src/page.rs b/vendor/hayro-syntax/src/page.rs index 20d6adef..7f1740f4 100644 --- a/vendor/hayro-syntax/src/page.rs +++ b/vendor/hayro-syntax/src/page.rs @@ -604,9 +604,17 @@ impl<'a> Page<'a> { { (A4.width() as f32, A4.height() as f32) } else { + // PdfCraft patch: retain a one-point floor after UserUnit, not one raw unit. + // The stored unit is finite and within 1..=75000. Round upward only if the + // reciprocal's f32 product would fall below one and trigger a placeholder. + let mut raw_floor = 1.0 / self.user_unit; + if raw_floor * self.user_unit < 1.0 { + raw_floor = raw_floor.next_up(); + } + let raw_floor = f64::from(raw_floor); ( - crop_box.width().max(1.0) as f32, - crop_box.height().max(1.0) as f32, + crop_box.width().max(raw_floor) as f32, + crop_box.height().max(raw_floor) as f32, ) } } diff --git a/vendor/hayro/SOURCE.json b/vendor/hayro/SOURCE.json index 50e0340a..260e2c55 100644 --- a/vendor/hayro/SOURCE.json +++ b/vendor/hayro/SOURCE.json @@ -1,7 +1,7 @@ { "crate": "hayro", "source_repository": "https://github.com/storytold/pdfcraft", - "source_revision": "7437bde378f43856eada40af25b983812e615e8a", + "source_revision": "ec0a965074f376a5bf610435948e4e5184bf3299", "source_directory": "vendor/hayro", "files": [ { @@ -60,15 +60,15 @@ }, { "path": "src/lib.rs", - "source_sha256": "1ce3e02072acb639c33476618446af80fca29b960a3ca764168c1c2d348d5e5d", - "source_git_blob": "def102b4291b1fe99f2c8975477e5d294c4f955f", - "sha256": "1ce3e02072acb639c33476618446af80fca29b960a3ca764168c1c2d348d5e5d" + "source_sha256": "682db2a32fbf4d875c0e9dd7ac5dc875a6b09ade375efbce6f104de9197de078", + "source_git_blob": "6333ed5808ae0cf9cf158e4730e973c1d7089bab", + "sha256": "682db2a32fbf4d875c0e9dd7ac5dc875a6b09ade375efbce6f104de9197de078" }, { "path": "src/renderer.rs", - "source_sha256": "b74b630b73e86b69b26ef433ee6aabbb684def133e4ae46f0cbf8f9f1fbd4484", - "source_git_blob": "23fc2e36cae7368e1a508bb246f3a1a15bb60a67", - "sha256": "b74b630b73e86b69b26ef433ee6aabbb684def133e4ae46f0cbf8f9f1fbd4484" + "source_sha256": "f7ff7bd088ceb43823ede33151138384af331cf71403f7d23f7b2937212efc3b", + "source_git_blob": "6aa53716ad2d0203a8058b2f7271515b4fc1db22", + "sha256": "f7ff7bd088ceb43823ede33151138384af331cf71403f7d23f7b2937212efc3b" } ] } diff --git a/vendor/hayro/src/lib.rs b/vendor/hayro/src/lib.rs index def102b4..6333ed58 100644 --- a/vendor/hayro/src/lib.rs +++ b/vendor/hayro/src/lib.rs @@ -61,7 +61,7 @@ use vello_cpu::{Level, Pixmap, RenderMode}; mod renderer; /// PdfCraft patch: exported for its regression test. -pub use renderer::{image_resampling_size, tiling_cell_scale}; +pub use renderer::{image_resampling_size, tiling_cell_scale, usable_cell_scale}; /// PdfCraft patch: exported for its regression test. #[doc(hidden)] pub use renderer::set_skip_offscreen_paths; @@ -254,6 +254,16 @@ pub fn render_into<'a>( interpreter_settings.preview, ); + // PdfCraft patch (18): the page renders as one transparency group with the background + // painted *inside* it, as the initial backdrop. A group isolates what it wraps from what + // was painted before it, so with the background outside, a Multiply highlight blended + // only with the group's transparent backdrop and stayed its own colour — over a dark + // reading-colours background the whole quad became one flat bar and the words under it + // vanished. Inside the group the highlight multiplies with the background too: the words + // keep the highlight's colour and the band between them takes the background (white + // backgrounds are unaffected — multiplying with white is the identity). + device.push_transparency_group(1.0, None, BlendMode::Normal); + device.ctx.set_paint(render_settings.bg_color); device .ctx @@ -265,12 +275,10 @@ pub fn render_into<'a>( fill: FillRule::NonZero, }); - device.push_transparency_group(1.0, None, BlendMode::Normal); interpret_page(page, &mut state, &mut device); - device.pop_transparency_group(); - device.pop_clip_path(); + device.pop_transparency_group(); let mut resources = vello_cpu::Resources::default(); device.ctx.render_to_buffer( diff --git a/vendor/hayro/src/renderer.rs b/vendor/hayro/src/renderer.rs index 23fc2e36..6aa53716 100644 --- a/vendor/hayro/src/renderer.rs +++ b/vendor/hayro/src/renderer.rs @@ -432,6 +432,9 @@ pub(crate) struct Renderer { /// PdfCraft patch: when set, solid text and line-art paint colours are remapped /// while rendering (the "reading colours" view setting). color_replacement: Option, + /// PdfCraft patch (17): while the page's annotations are being drawn, paints keep their + /// colours — reading colours replace document colours, never annotation colours. + in_annotations: bool, pub(crate) inside_pattern: bool, /// Soft masks drawn during this render. PdfCraft note: it belongs to one `Renderer` (one /// render, every nested renderer starting empty) and so to one `preview` setting: a mask @@ -466,6 +469,7 @@ impl Renderer { Self { ctx: RenderContext::new_with(width, height, settings), color_replacement, + in_annotations: false, inside_pattern: false, soft_mask_cache: HashMap::default(), outline_cache: cache.outline_cache.clone(), @@ -522,14 +526,7 @@ impl Renderer { }, }; - // A miter limit that isn't finite leaves miters unbounded: never skipped. - let miter = stroke.miter_limit.abs(); - let reach = stroke.width / 2.0 - * if miter.is_finite() { - miter.max(std::f64::consts::SQRT_2) - } else { - f64::INFINITY - }; + let reach = stroke_reach(&stroke); self.ctx.set_stroke(stroke); reach } @@ -573,6 +570,7 @@ impl Renderer { // PdfCraft patch: this renders an image's alpha mask, which must keep its // original values, so it is never remapped by the reading-colours setting. color_replacement: None, + in_annotations: false, inside_pattern: false, soft_mask_cache: HashMap::default(), outline_cache: self.outline_cache.clone(), @@ -1023,6 +1021,11 @@ impl Renderer { let Some(replacement) = self.color_replacement else { return paint.clone(); }; + // PdfCraft patch (17): annotations keep the colours they were given; reading colours + // replace document colours only (issue #1548). + if self.in_annotations { + return paint.clone(); + } match paint { Paint::Color(c) => { let mapped = replacement.map(c.to_rgba().to_rgba8(), is_text); @@ -1075,8 +1078,13 @@ impl Renderer { // become native gradients instead of a texture sampled pixel by pixel. // Upstream also shifts by half a pixel for vello_cpu 0.3; 0.0.8 already // evaluates gradients at pixel centres, like the texture below. - if let Some(native) = - encoded.as_svg_gradient(bbox, NATIVE_GRADIENT_TOLERANCE) + // PdfCraft patch (#2128): vello_cpu 0.0.8 copies an opaque gradient + // over fully covered tiles without applying the mask or blend mode, and + // a gradient can't be flagged transparent like the texture below, so + // under a soft mask or blend mode the shading is sampled instead. + if !self.force_images_may_have_transparency() + && let Some(native) = + encoded.as_svg_gradient(bbox, NATIVE_GRADIENT_TOLERANCE) { self.ctx .set_paint_transform(path_transform.inverse() * native.transform); @@ -1127,6 +1135,65 @@ impl Renderer { }; xs = xs.max(min_x_scale).min(max_x_scale); ys = ys.max(min_y_scale).min(max_y_scale); + // PdfCraft patch (20): a step too large for a cell pixmap, where what is + // painted can only show the first cell (PowerPoint writes `/XStep + // 2147480100` for a picture fill drawn once, #3349): the cell's `/BBox` is + // drawn at full scale with a transparent border and padded, not tiled. + // Shrinking the cell to fit the step left a picture under one pixel. + // How far a stroke's paint reaches past the path, miters included, in + // device pixels: the most the transform stretches any direction (a skew + // stretches some further than either axis), times the reach. + let reach = if is_stroke { + path_transform.spectral_norm() * stroke_reach(self.ctx.stroke()) + } else { + 0.0 + }; + if usable_cell_scale(xs, ys) + && (xs * t.x_step.abs() > MAX_PIXMAP_SIZE || ys * t.y_step.abs() > MAX_PIXMAP_SIZE) + && shows_one_cell(&t.matrix, path_transform, path, reach, bbox, t.x_step, t.y_step) + { + const BORDER: f64 = 1.0; + let pix_width = ((bbox.width() as f32 * xs).ceil() as u16).saturating_add(2); + let pix_height = ((bbox.height() as f32 * ys).ceil() as u16).saturating_add(2); + let mut renderer = Self { + ctx: RenderContext::new_with( + pix_width, + pix_height, + derive_settings(self.ctx.render_settings()), + ), + color_replacement: None, + in_annotations: false, + cur_mask: None, + inside_pattern: true, + soft_mask_cache: HashMap::default(), + outline_cache: self.outline_cache.clone(), + in_type3_glyph: false, + scaler: self.scaler, + preview: self.preview, + image_transparency_stack: Vec::new(), + }; + let initial_transform = Affine::translate((BORDER, BORDER)) + * Affine::scale_non_uniform(xs as f64, ys as f64) + * Affine::translate((-bbox.x0, -bbox.y0)); + t.interpret(&mut renderer, initial_transform, is_stroke); + let mut pix = Pixmap::new(pix_width, pix_height); + renderer.ctx.flush(); + let mut resources = vello_cpu::Resources::default(); + renderer.ctx.render_to_pixmap(&mut resources, &mut pix); + paint_transform = path_transform.inverse() * t.matrix * initial_transform.inverse(); + let image = Image { + image: ImageSource::Pixmap(Arc::new(pix)), + sampler: ImageSampler { + x_extend: peniko::Extend::Pad, + y_extend: peniko::Extend::Pad, + quality: ImageQuality::Medium, + alpha: 1.0, + }, + }; + self.ctx.set_paint_transform(paint_transform); + self.ctx.set_paint(PaintType::Image(image)); + return clip_path; + } // PdfCraft patch: the cell pixmap is `step × scale` pixels, which only the // u16 cast below bounded (a fuzzed /XStep far beyond /BBox gave 65535² cells). xs = tiling_cell_scale(xs, t.x_step, MAX_PIXMAP_SIZE); @@ -1149,6 +1216,7 @@ impl Renderer { // PdfCraft patch: a pattern cell is painted as one artwork, so its // content is never remapped by the reading-colours setting. color_replacement: None, + in_annotations: false, cur_mask: None, inside_pattern: true, soft_mask_cache: HashMap::default(), @@ -1464,6 +1532,7 @@ impl<'a> Device<'a> for Renderer { // coverage values, so its content is never remapped by // the reading-colours setting. color_replacement: None, + in_annotations: false, inside_pattern: false, soft_mask_cache: HashMap::default(), outline_cache: self.outline_cache.clone(), @@ -1608,6 +1677,16 @@ impl<'a> Device<'a> for Renderer { } } + /// PdfCraft patch (17): the reading-colours mapping is suspended for annotations. + fn begin_annotations(&mut self) { + self.in_annotations = true; + } + + /// PdfCraft patch (17): back to document content; reading colours apply again. + fn end_annotations(&mut self) { + self.in_annotations = false; + } + fn draw_rect( &mut self, rect: &Rect, @@ -1749,6 +1828,7 @@ fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, hei // PdfCraft patch: a soft mask must keep its original luminance values, so its // content is never remapped by the reading-colours setting. color_replacement: None, + in_annotations: false, inside_pattern: false, cur_mask: None, soft_mask_cache: HashMap::default(), @@ -1866,6 +1946,46 @@ fn convert_blend_mode(blend_mode: BlendMode) -> peniko::BlendMode { peniko::BlendMode::new(mix, Compose::SrcOver) } +/// PdfCraft patch: how far a stroke's paint reaches past its path, in user units: half the width +/// times the larger of the miter limit and √2 (a miter join or a square cap). A miter limit that +/// isn't finite leaves miters unbounded, so the reach is too. +fn stroke_reach(stroke: &kurbo::Stroke) -> f64 { + let miter = stroke.miter_limit.abs(); + stroke.width / 2.0 + * if miter.is_finite() { + miter.max(std::f64::consts::SQRT_2) + } else { + f64::INFINITY + } +} + +/// PdfCraft patch (20): whether a cell's scale can draw it at full size: finite and positive. A +/// `/BBox` too thin to divide by (1e-40 wide passes the zero-area check) gives an infinite scale, +/// which would send non-finite geometry to the cell renderer; such a cell keeps the old path. +pub fn usable_cell_scale(xs: f32, ys: f32) -> bool { + xs.is_finite() && ys.is_finite() && xs > 0.0 && ys > 0.0 +} + +/// PdfCraft patch (20): whether `path` (drawn with `path_transform`, its paint reaching `reach` +/// device pixels past it), mapped into the space of a tiling pattern with matrix `matrix`, can +/// only meet the first cell's `/BBox`: every other cell's content lies a whole step or more +/// away. False when the matrix can't be inverted or the bounds aren't finite. +pub(crate) fn shows_one_cell(matrix: &Affine, path_transform: &Affine, path: &BezPath, reach: f64, bbox: Rect, x_step: f32, y_step: f32) -> bool { + if matrix.determinant().abs() < 1e-12 || !matrix.determinant().is_finite() { + return false; + } + let device = (*path_transform * path.clone()).bounding_box().inflate(reach, reach); + let bounds = (matrix.inverse() * device.to_path(0.1)).bounding_box(); + let (sx, sy) = (f64::from(x_step.abs()), f64::from(y_step.abs())); + let finite = [bounds.x0, bounds.y0, bounds.x1, bounds.y1, sx, sy].iter().all(|v| v.is_finite()); + // Cell k's content spans `bbox + k × step`; with k = ±1 out of reach, so is every other. + finite + && bounds.x1 < bbox.x0 + sx + && bounds.x0 > bbox.x1 - sx + && bounds.y1 < bbox.y0 + sy + && bounds.y0 > bbox.y1 - sy +} + /// PdfCraft patch: the scale for a tiling cell, lowered so that the cell pixmap (`|step| × /// scale` pixels a side) stays within `max_pixels`. Geometry is unchanged (the pattern transform /// is derived from the scale); an over-large step only renders at lower resolution.