mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-01 08:16:43 +00:00
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:
1 parent
ae2f796b67
commit
b6f60f25b1
30 files changed
+993
-385
No files matched your search
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Vendored
+28
-2
@@ -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.
|
||||
|
||||
Vendored
+43
-43
@@ -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": {
|
||||
|
||||
Vendored
+11
-1
@@ -72,7 +72,8 @@ pub(crate) struct DecodedImageCache {
|
||||
/// - the dictionary's bytes (`dict_hash`: `/Decode`, `/ColorSpace`, `/SMask`, `/Mask`,
|
||||
/// `/BitsPerComponent`, filters, …) and its object (`obj`: the decryption key depends on it);
|
||||
/// - whether it is decoded as a mask, a stencil mask, raster or mask output, interpolation,
|
||||
/// the decode target size, and preview or final quality.
|
||||
/// the decode target size (only for an image a JPX stream decodes, which is the only
|
||||
/// decode that reads it: PdfCraft patch 32), and preview or final quality.
|
||||
///
|
||||
/// A transfer function (graphics state `/TR`) also changes the pixels and has no cheap key, so
|
||||
/// an image drawn under one is not cached; neither is an inline image, whose dictionary lives in
|
||||
@@ -95,6 +96,8 @@ pub(crate) struct DecodedImageKey {
|
||||
struct DecodedImageCacheInner {
|
||||
map: HashMap<DecodedImageKey, Option<Box<dyn Any + Send + Sync>>>,
|
||||
bytes: usize,
|
||||
/// PdfCraft patch (32): how many keyed decodes ran (cache misses).
|
||||
decodes: usize,
|
||||
}
|
||||
|
||||
impl DecodedImageCache {
|
||||
@@ -105,6 +108,12 @@ impl DecodedImageCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (32): how many decodes the cache was asked to keep (its misses), for
|
||||
/// diagnostics and tests. 0 when the lock is poisoned.
|
||||
pub(crate) fn decodes(&self) -> usize {
|
||||
self.inner.lock().map(|locked| locked.decodes).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The same cache, for a preview (`true`) or a final render.
|
||||
pub(crate) fn for_render(&self, preview: bool) -> Self {
|
||||
Self {
|
||||
@@ -141,6 +150,7 @@ impl DecodedImageCache {
|
||||
let Ok(mut locked) = self.inner.lock() else {
|
||||
return val;
|
||||
};
|
||||
locked.decodes = locked.decodes.saturating_add(1);
|
||||
// Another thread may have decoded it meanwhile: keep the first copy.
|
||||
if let Some(existing) = locked.map.get(&key)
|
||||
&& let Some(existing) = existing.as_ref().and_then(|val| val.downcast_ref::<T>())
|
||||
|
||||
Vendored
+22
@@ -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
@@ -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`].
|
||||
|
||||
Vendored
+7
@@ -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.
|
||||
|
||||
Vendored
+66
-37
@@ -3,7 +3,7 @@
|
||||
use crate::color::{AlphaColor, ColorComponents, ColorSpace};
|
||||
use crate::interpret::state::ActiveTransferFunction;
|
||||
use crate::pattern::ShadingPattern;
|
||||
use crate::shading::{ShadingFunction, ShadingType, Triangle};
|
||||
use crate::shading::{ShadingFunction, ShadingType, Triangle, TriangleVertex};
|
||||
use kurbo::{Affine, Point};
|
||||
use rustc_hash::FxHashMap;
|
||||
use smallvec::{ToSmallVec, smallvec};
|
||||
@@ -102,7 +102,7 @@ impl ShadingPattern {
|
||||
}
|
||||
triangles.clear();
|
||||
patch.to_triangles(&mut triangles);
|
||||
if !sampler.sample(&triangles, full_transform) {
|
||||
if !sampler.sample_owned(&mut triangles, full_transform) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ impl ShadingPattern {
|
||||
}
|
||||
triangles.clear();
|
||||
patch.to_triangles(&mut triangles);
|
||||
if !sampler.sample(&triangles, full_transform) {
|
||||
if !sampler.sample_owned(&mut triangles, full_transform) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -268,47 +268,62 @@ impl TriangleSampler {
|
||||
|
||||
/// Sample `triangles`; `false` once a budget is spent (the rest is left unsampled).
|
||||
fn sample(&mut self, triangles: &[Triangle], transform: Affine) -> bool {
|
||||
if !self.take_page_budget(triangles.len()) {
|
||||
return false;
|
||||
}
|
||||
triangles.iter().all(|t| {
|
||||
self.sample_one(transformed(
|
||||
t.p0.clone(),
|
||||
t.p1.clone(),
|
||||
t.p2.clone(),
|
||||
transform,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// PdfCraft patch (33): [`Self::sample`] for triangles made for this sampling alone (a
|
||||
/// patch's), moving their vertices instead of copying each one's colours (`triangles` is
|
||||
/// left empty).
|
||||
fn sample_owned(&mut self, triangles: &mut Vec<Triangle>, transform: Affine) -> bool {
|
||||
if !self.take_page_budget(triangles.len()) {
|
||||
return false;
|
||||
}
|
||||
triangles
|
||||
.drain(..)
|
||||
.all(|t| self.sample_one(transformed(t.p0, t.p1, t.p2, transform)))
|
||||
}
|
||||
|
||||
/// Take `count` triangles from the page's budget; `false` (and none left) when it is short.
|
||||
fn take_page_budget(&mut self, count: usize) -> bool {
|
||||
let page_left = MESH_TRIANGLES_LEFT.with(core::cell::Cell::get);
|
||||
if page_left < triangles.len() as u64 {
|
||||
if page_left < count as u64 {
|
||||
MESH_TRIANGLES_LEFT.with(|n| n.set(0));
|
||||
warn!("page mesh triangle budget exceeded; the rest of the shading is not drawn");
|
||||
return false;
|
||||
}
|
||||
MESH_TRIANGLES_LEFT.with(|n| n.set(page_left - triangles.len() as u64));
|
||||
for t in triangles {
|
||||
let t = {
|
||||
let p0 = transform * t.p0.point;
|
||||
let p1 = transform * t.p1.point;
|
||||
let p2 = transform * t.p2.point;
|
||||
MESH_TRIANGLES_LEFT.with(|n| n.set(page_left - count as u64));
|
||||
true
|
||||
}
|
||||
|
||||
let mut v0 = t.p0.clone();
|
||||
v0.point = p0;
|
||||
let mut v1 = t.p1.clone();
|
||||
v1.point = p1;
|
||||
let mut v2 = t.p2.clone();
|
||||
v2.point = p2;
|
||||
/// Sample one triangle in device space; `false` once the visit budget is spent.
|
||||
fn sample_one(&mut self, t: Triangle) -> bool {
|
||||
// Only the part of the triangle inside `area`; only such triangles add visits.
|
||||
let bbox = t.bounding_box().intersect(self.area);
|
||||
if bbox.width() <= 0.0 || bbox.height() <= 0.0 {
|
||||
return true;
|
||||
}
|
||||
self.visits_left = self.visits_left.saturating_add(MESH_VISITS_PER_TRIANGLE);
|
||||
|
||||
Triangle::new(v0, v1, v2)
|
||||
};
|
||||
|
||||
// Only the part of the triangle inside `area`; only such triangles add visits.
|
||||
let bbox = t.bounding_box().intersect(self.area);
|
||||
if bbox.width() <= 0.0 || bbox.height() <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
self.visits_left = self.visits_left.saturating_add(MESH_VISITS_PER_TRIANGLE);
|
||||
|
||||
for y in (bbox.y0.floor() as u16)..(bbox.y1.ceil() as u16) {
|
||||
for x in (bbox.x0.floor() as u16)..(bbox.x1.ceil() as u16) {
|
||||
if self.visits_left == 0 {
|
||||
warn!("mesh shading visit budget exceeded; the rest is not drawn");
|
||||
return false;
|
||||
}
|
||||
self.visits_left -= 1;
|
||||
let point = Point::new(x as f64, y as f64);
|
||||
if t.contains_point(point) {
|
||||
self.map.insert((x, y), t.interpolate(point));
|
||||
}
|
||||
for y in (bbox.y0.floor() as u16)..(bbox.y1.ceil() as u16) {
|
||||
for x in (bbox.x0.floor() as u16)..(bbox.x1.ceil() as u16) {
|
||||
if self.visits_left == 0 {
|
||||
warn!("mesh shading visit budget exceeded; the rest is not drawn");
|
||||
return false;
|
||||
}
|
||||
self.visits_left -= 1;
|
||||
let point = Point::new(x as f64, y as f64);
|
||||
if t.contains_point(point) {
|
||||
self.map.insert((x, y), t.interpolate(point));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,6 +331,20 @@ impl TriangleSampler {
|
||||
}
|
||||
}
|
||||
|
||||
/// The triangle `v0`, `v1`, `v2` with its points mapped through `transform` (its colours as
|
||||
/// they are).
|
||||
fn transformed(
|
||||
mut v0: TriangleVertex,
|
||||
mut v1: TriangleVertex,
|
||||
mut v2: TriangleVertex,
|
||||
transform: Affine,
|
||||
) -> Triangle {
|
||||
v0.point = transform * v0.point;
|
||||
v1.point = transform * v1.point;
|
||||
v2.point = transform * v2.point;
|
||||
Triangle::new(v0, v1, v2)
|
||||
}
|
||||
|
||||
fn encode_function_shading(domain: &[f32; 4], function: &ShadingFunction) -> EncodedShadingType {
|
||||
let domain = kurbo::Rect::new(
|
||||
domain[0] as f64,
|
||||
|
||||
+23
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Vendored
+1
-1
@@ -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": [
|
||||
{
|
||||
|
||||
Vendored
+22
-22
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")?;
|
||||
|
||||
Vendored
+10
-2
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+7
-7
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+12
-4
@@ -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(
|
||||
|
||||
Vendored
+130
-10
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user