vendor: re-sync hayro reader trees from pdfcraft ec0a965

Bring vendor/hayro, hayro-interpret, hayro-syntax and hayro-jbig2 to
storytold/pdfcraft ec0a965074f376a5bf610435948e4e5184bf3299 (main,
2026-10-19), from 7437bde. The local patches (per-document decode
limits, incomplete-content diagnostics, Hayro 0.8 resource scopes,
nesting cap, iterative page tree, TJ fill order, transfer rounding,
safety repairs) are carried by a three-way merge without conflicts.

The re-sync includes transparency group /I and /K reading (pdfcraft
#2027), patches 28, 29 and 31-34, and the reader fixes listed in
vendor/README.craft-reader.md. SOURCE.json manifests, NOTICE and the
craft-pdf README name the new source revision.
This commit is contained in:
aiongg committed 2026-10-20 02:30:13 +00:00
1 parent ae2f796b67
commit b6f60f25b1
30 files changed
+993 -385

No files matched your search

+1 -1
View File
@@ -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).
+1 -1
View File
@@ -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.
+28 -2
View File
@@ -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.
+43 -43
View File
@@ -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": {
+11 -1
View File
@@ -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>())
+22
View File
@@ -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<u64> = 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| {
+6
View File
@@ -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`].
+7
View File
@@ -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.
+66 -37
View File
@@ -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,
+23
View File
@@ -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.
+8
View File
@@ -546,6 +546,14 @@ impl StandardKind {
}
pub(crate) fn char_code_to_unicode(&self, code: u8) -> Option<char> {
// 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)
}
+6
View File
@@ -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))
}
+16
View File
@@ -239,6 +239,14 @@ impl Type1Kind {
}
fn char_code_to_unicode(&self, code: u8) -> Option<char> {
// 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<char> {
// 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)
}
}
+5 -1
View File
@@ -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(),
+68 -175
View File
@@ -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(())
}
}
+4
View File
@@ -175,6 +175,9 @@ pub fn interpret_page<'a>(
if context.settings.render_annotations
&& let Some(annot_arr) = page.raw().get::<Array<'_>>(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::<Dict<'_>>() {
let flags = annot.get::<u32>(F).unwrap_or(0);
@@ -275,6 +278,7 @@ pub fn interpret_page<'a>(
draw_highlight_without_appearance(&annot, context, device);
}
}
device.end_annotations();
}
}
+13 -21
View File
@@ -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)));
}
}
}
+209 -27
View File
@@ -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<Dict<'a>>,
}
@@ -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::<Dict<'_>>(GROUP).is_some();
let group = dict.get::<Dict<'_>>(GROUP);
let is_transparency_group = group.is_some();
let is_isolated = group.as_ref().and_then(|g| g.get::<bool>(I)).unwrap_or(false);
let is_knockout = group.as_ref().and_then(|g| g.get::<bool>(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::<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
}
@@ -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::<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 {
@@ -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<Vec<f32>> {
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<RgbData> {
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<f32> = (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 {
+1 -1
View File
@@ -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": [
{
+22 -22
View File
@@ -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",
+13 -4
View File
@@ -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<Vec<u8>> {
/// 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<Vec<u8>> {
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<Vec<u8>> {
.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<Vec<u8>> {
}
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
+9 -1
View File
@@ -15,7 +15,15 @@ pub(crate) fn decode(
) -> Option<FilterResult<'static>> {
let k = params.get::<i32>(K).unwrap_or(0);
let rows = params.get::<u32>(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::<u32>(ROWS)
.filter(|&rows| rows != 0)
.map_or(image_params.height, |rows| rows.max(image_params.height));
let end_of_block = params.get::<bool>(END_OF_BLOCK).unwrap_or(true);
let settings = DecodeSettings {
+172 -2
View File
@@ -26,7 +26,18 @@ pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option<FilterRe
target_resolution: params.target_dimension,
};
let image = hayro_jpeg2000::Image::new(data, &settings).ok()?;
// PdfCraft patch (21): a JP2 colour specification naming an enumerated colour space the
// decoder refuses (CIEJab, enum 19, in a veraPDF fixture; YCbCr, PhotoYCC, …) made the whole
// image fail, so the page drew blank (#3306). Then it is decoded as if it had no colour
// specification, as MuPDF and Poppler draw it: grey, RGB or CMYK by its component count.
let repaired;
let image = match hayro_jpeg2000::Image::new(data, &settings) {
Ok(image) => 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<FilterRe
})
}
/// The enumerated colour spaces (`EnumCS`, ISO 15444-1 I.5.3.3 and 15444-2 M.11.7.2) the decoder
/// handles: CMYK, CIELab, sRGB, greyscale, sYCC, e-sRGB and ROMM-RGB.
const DECODED_ENUMERATED: [u32; 7] = [12, 14, 16, 17, 18, 20, 21];
/// PdfCraft patch (21): a copy of JP2 file `data` whose colour specification boxes (`colr`, in
/// the `jp2h` header box) that name an enumerated colour space outside [`DECODED_ENUMERATED`]
/// name the space the decoder assumes without one instead: greyscale, sRGB or CMYK for 1, 3 or
/// 4 components (`ihdr`). `None` when there is nothing to change or the boxes can't be read.
pub(crate) fn without_refused_enumerated_colour(data: &[u8]) -> Option<Vec<u8>> {
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<Item = ([u8; 4], usize, usize)> + '_ {
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<u8> {
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<u8> {
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<u32> {
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}");
}
}
}
+64 -14
View File
@@ -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(
+1 -1
View File
@@ -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)
+15 -8
View File
@@ -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<Stream<'a>> {
let length = dict.get::<u32>(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<Stream<'a>>
// 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")?;
+10 -2
View File
@@ -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,
)
}
}
+7 -7
View File
@@ -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"
}
]
}
+12 -4
View File
@@ -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(
+130 -10
View File
@@ -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<ColorReplacement>,
/// 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.