mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-13 15:09:07 +00:00
* fix: recognize paragraph drafts as unsaved work (Issue #811) * fix(color): honour the /Lab colour space /WhitePoint (#1203) The old path fed every Lab value to the bundled ICC transform as if the space's white point were D50, so a /Lab space with any other /WhitePoint (e.g. D65 [0.9505 1 1.089]) rendered off by several levels per channel and a D65 blue came out 45 levels too red. Map L*a*b* to XYZ in the space's own white coordinates (ISO 32000-2 §8.6.5.4), Bradford-adapt it to D65 and convert to sRGB directly, as Poppler does. Missing or degenerate white points fall back to a flat white and hostile components are clamped without panicking. Add a render regression that checks the five reported flat patches of the D65 white-point fixture against their CIE sRGB values. * Lab: map normalised Indexed palette components onto L*/Range; drop unused LAB.icc An /Indexed palette over Lab passes its entries as byte/255 (manual_scale), which the new Lab conversion read as raw L*a*b* and drew near black. Map them onto L* 0..100 and the a*/b* /Range, with a regression test. Record the patch as hayro-interpret (25) in vendor/README.md, and remove LAB.icc (no longer referenced) with its ATTRIBUTION, NOTICE and assets README entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Main gates: rustfmt tests/viewer.rs; a type alias for the refresh-steps array (clippy type_complexity) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
10 files changed
+179
-67
No files matched your search
+1
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
|
||||
|
||||
## In this repository (269)
|
||||
## In this repository (268)
|
||||
|
||||
| Asset | Title | Author | Licence | Source | Used for |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -221,7 +221,6 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
||||
| `vendor/hayro-interpret/assets/FoxitSerifItalic.pfb` | FoxitSerifItalic (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
|
||||
| `vendor/hayro-interpret/assets/FoxitSymbol.pfb` | FoxitSymbol (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
|
||||
| `vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc` | CGATS001Compat v2 micro ICC profile | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | CC0-1.0 | https://github.com/saucecontrol/Compact-ICC-Profiles | Default CMYK colour conversion (vendored hayro-interpret) |
|
||||
| `vendor/hayro-interpret/assets/LAB.icc` | Lab abstract ICC profile | The Hayro Authors (generated with LittleCMS 2) | MIT OR Apache-2.0 | https://github.com/LaurenzV/hayro (hayro-interpret 0.7.0 assets/) | Lab colour space conversion (vendored hayro-interpret) |
|
||||
| `docs/brand/artcraft-logo-white.png` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
|
||||
| `docs/brand/artcraft-logo-white.svg` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
|
||||
| `docs/brand/artcraft-logo.png` | ArtCraft brand: artcraft-logo | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
|
||||
|
||||
@@ -2353,17 +2353,6 @@ kind = "data"
|
||||
usage = "Default CMYK colour conversion (vendored hayro-interpret)"
|
||||
sha256 = "73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0"
|
||||
|
||||
[[asset]]
|
||||
path = "vendor/hayro-interpret/assets/LAB.icc"
|
||||
title = "Lab abstract ICC profile"
|
||||
author = "The Hayro Authors (generated with LittleCMS 2)"
|
||||
source = "https://github.com/LaurenzV/hayro (hayro-interpret 0.7.0 assets/)"
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "vendor/hayro-interpret/LICENSE-MIT"
|
||||
kind = "data"
|
||||
usage = "Lab colour space conversion (vendored hayro-interpret)"
|
||||
sha256 = "a6efc5c30ece826eba2a83ddf5aa7026289b0c5ffa90acb129bc59ab8d7f5c59"
|
||||
|
||||
[[bundled]]
|
||||
crate = "epaint_default_fonts"
|
||||
version = "0.36.2"
|
||||
|
||||
@@ -35,8 +35,7 @@ list (author, source, license) is in ATTRIBUTION.md.
|
||||
- lopdf (bootstrap document inspection, via crates.io) — MIT.
|
||||
- egui default fonts (epaint_default_fonts, via eframe) — Ubuntu Light (Ubuntu Font Licence 1.0),
|
||||
Hack (MIT / Bitstream Vera licence), Noto Emoji (SIL OFL 1.1), emoji-icon-font (MIT).
|
||||
- Colour profiles in hayro-interpret — CGATS001Compat-v2-micro.icc (CC0-1.0, Clinton Ingram) and
|
||||
LAB.icc (generated by the hayro authors with LittleCMS 2; MIT OR Apache-2.0).
|
||||
- Colour profile in hayro-interpret — CGATS001Compat-v2-micro.icc (CC0-1.0, Clinton Ingram).
|
||||
|
||||
Required notices:
|
||||
|
||||
|
||||
@@ -3925,7 +3925,8 @@ fn edits_let_go_of_the_previous_working_file() {
|
||||
|| Edit::AddWatermark { pages: vec![0], settings: Watermark { text: "Draft".into(), ..Watermark::default() }, file: None, replace: false };
|
||||
s.apply(id, Edit::RotatePages { pages: vec![1], degrees: 90 }).unwrap();
|
||||
// A full refresh, a comment refresh, a content refresh, then undo and redo.
|
||||
let steps: [(&str, Box<dyn Fn(&mut Session)>); 5] = [
|
||||
type Step<'a> = Box<dyn Fn(&mut Session) + 'a>;
|
||||
let steps: [(&str, Step<'_>); 5] = [
|
||||
("rotate", Box::new(|s: &mut Session| s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 }).unwrap())),
|
||||
("comment", Box::new(move |s: &mut Session| s.apply(id, comment()).unwrap())),
|
||||
("watermark", Box::new(move |s: &mut Session| s.apply(id, watermark()).unwrap())),
|
||||
|
||||
@@ -1905,6 +1905,98 @@ mod tests {
|
||||
assert!(remainder.is_empty());
|
||||
assert!(pixels.iter().all(|pixel| *pixel == [0, 0, 255, 255]));
|
||||
}
|
||||
/// #1203: a /Lab colour space must honour its own /WhitePoint. The old path
|
||||
/// fed every Lab value to the ICC transform as if the white point were D50,
|
||||
/// shifting non-D50 pages several levels per channel (for example a D65
|
||||
/// blue came out 45 levels too red). Conversion and expected values follow
|
||||
/// ISO 32000-2 §8.6.5.4 with the space's own /WhitePoint [0.9505 1 1.089].
|
||||
#[test]
|
||||
fn lab_honours_the_space_white_point() {
|
||||
use super::*;
|
||||
let render = |l: f32, a: f32, b: f32| -> [u8; 4] {
|
||||
let content = format!("/L cs {l} {a} {b} sc 0 0 200 200 re f");
|
||||
let bytes = format!(
|
||||
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /ColorSpace << /L [/Lab << /WhitePoint [0.9505 1 1.089] /Range [-100 100 -100 100] >>] >> >> /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
)
|
||||
.into_bytes();
|
||||
let page = PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
assert!(page.error.is_none(), "Lab ({l} {a} {b}): {:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (200, 200));
|
||||
page.rgba[(100 * page.width as usize + 100) * 4..][..4].try_into().unwrap()
|
||||
};
|
||||
|
||||
for (l, a, b, expected) in [
|
||||
(40.0, 20.0, -60.0, [19, 90, 194, 255]),
|
||||
(70.0, -50.0, 30.0, [77, 193, 114, 255]),
|
||||
(60.0, 0.0, -40.0, [87, 148, 215, 255]),
|
||||
(50.0, 60.0, 40.0, [217, 61, 55, 255]),
|
||||
(30.0, 40.0, 20.0, [130, 37, 42, 255]),
|
||||
] {
|
||||
let got = render(l, a, b);
|
||||
for (channel, (got, want)) in got.iter().zip(expected).enumerate() {
|
||||
assert!((*got).abs_diff(want) <= 2, "Lab ({l} {a} {b}) channel {channel}: got {got:?}, expected {expected:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
/// #1203: an `/Indexed` palette over Lab looks its entries up as
|
||||
/// `byte / 255`, which must map onto L* 0..100 and the a*/b* /Range. They
|
||||
/// were read as raw L*a*b* (L* below 1), so such palettes drew near black.
|
||||
#[test]
|
||||
fn indexed_palettes_over_lab_keep_their_colours() {
|
||||
use super::*;
|
||||
let lab = "[/Lab << /WhitePoint [0.9505 1 1.089] /Range [-100 100 -100 100] >>]";
|
||||
let render = |content: &str| -> [u8; 4] {
|
||||
let bytes = format!(
|
||||
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n\
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /ColorSpace << /L {lab} /I [/Indexed {lab} 1 <000000 80CCB2>] >> >> /Contents 4 0 R >> endobj\n\
|
||||
4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\
|
||||
trailer << /Root 1 0 R >>\n%%EOF",
|
||||
content.len()
|
||||
)
|
||||
.into_bytes();
|
||||
let page = PageRenderer::new(Arc::new(bytes), RenderConfig::default()).render(RenderRequest {
|
||||
page: 0,
|
||||
kind: RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: 1.0,
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
assert!(page.error.is_none(), "{content}: {:?}", page.error);
|
||||
assert_eq!((page.width, page.height), (200, 200));
|
||||
let start = (100 * page.width as usize + 100) * 4;
|
||||
let mut pixel = [0; 4];
|
||||
pixel.copy_from_slice(page.rgba.get(start..start + 4).expect("centre pixel"));
|
||||
pixel
|
||||
};
|
||||
|
||||
// Palette entry 1 is <80 CC B2>: L* = 128/255·100, a* = -100 + 204/255·200,
|
||||
// b* = -100 + 178/255·200, about Lab (50.2 60 39.6): a warm red.
|
||||
let indexed = render("/I cs 1 sc 0 0 200 200 re f");
|
||||
let direct = render(&format!(
|
||||
"/L cs {} {} {} sc 0 0 200 200 re f",
|
||||
128.0 / 255.0 * 100.0,
|
||||
-100.0 + 204.0 / 255.0 * 200.0,
|
||||
-100.0 + 178.0 / 255.0 * 200.0
|
||||
));
|
||||
for (channel, (got, want)) in indexed.iter().zip(direct).enumerate() {
|
||||
assert!(got.abs_diff(want) <= 1, "channel {channel}: indexed {indexed:?}, direct {direct:?}");
|
||||
}
|
||||
assert!(indexed[0] > 180, "the palette red must not render near black: {indexed:?}");
|
||||
}
|
||||
#[test]
|
||||
fn watchdog_skips_a_stuck_page_and_keeps_rendering() {
|
||||
use super::*;
|
||||
|
||||
@@ -193,9 +193,7 @@ fn marking_while_searching_does_not_jitter_or_reset_search_state() {
|
||||
assert!(h.state().views[0].goto.is_none());
|
||||
|
||||
// User draws or marks an annotation on page 0
|
||||
let shape = pdfcraft_engine::Shape::Ink {
|
||||
strokes: vec![vec![[10.0, 10.0], [20.0, 20.0]]],
|
||||
};
|
||||
let shape = pdfcraft_engine::Shape::Ink { strokes: vec![vec![[10.0, 10.0], [20.0, 20.0]]] };
|
||||
let stroke = pdfcraft_engine::Edit::AddAnnotation(pdfcraft_engine::NewAnnotation {
|
||||
page: 0,
|
||||
style: pdfcraft_engine::Style::default_for(&shape),
|
||||
@@ -206,11 +204,7 @@ fn marking_while_searching_does_not_jitter_or_reset_search_state() {
|
||||
h.state_mut().apply_edit(stroke);
|
||||
|
||||
// Right after applying the edit, search matches and current match must still be valid
|
||||
assert_eq!(
|
||||
h.state().views[0].find.as_ref().unwrap().current,
|
||||
current_before,
|
||||
"applying an annotation must not wipe current search match"
|
||||
);
|
||||
assert_eq!(h.state().views[0].find.as_ref().unwrap().current, current_before, "applying an annotation must not wipe current search match");
|
||||
|
||||
// Let the canvas process the reload
|
||||
let reload_deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
|
||||
Vendored
+1
-1
@@ -7,7 +7,7 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001).
|
||||
|
||||
| Crate | Version | Licence | Patches | Test |
|
||||
|---|---|---|---|---|
|
||||
| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.nLine truncated
|
||||
| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end); (18) `MAX_PAGE_CONTENT`: the content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within 512 MiB (`decode_content`; once it runs out, `InterpreterWarning::ContentTruncated` is reported (once per page by pdfcraft-render) so a truncated page is not a clean render; each level gives its bytes back when it finishes): forms nest fifty deep and each level kept its content while the next ran, so a form painting itself around a stream inflating to 256 MiB would have held over 12 GB; and Type 0 sampled functions take at most `MAX_SAMPLE_ENTRIES` (2^21) table points and `MAX_SAMPLE_VALUES` (2^23) samples, read no further than `/Size` asks (256 MiB of 1-bit samples asked for over 8 GB); (19) mesh shadings (types 4–7) are sampled only inside the area being drawn (`ShadingPattern::encode_within`, which the vendored hayro renderer calls with that area plus a pixel), within 8 visits per pixel of it plus 4 per triangle (`TriangleSampler`), and patches are triangulated one at a time: points far off the page were sampled over their whole bounding box, up to 65536 × 65536 entries (a fuzzed 147-byte patch stream; a synthetic page passed 6 GB in six seconds), ten thousand overlapping triangles ran for minutes, and every patch became 722 triangles before any limit; meshes hold at most `MAX_MESH_PATCHES` (2^16) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, a lattice needs two to 2^19 vertices per row (`/VerticesPerRow 0` pushed empty rows for ever; one huge row read the whole stream), and a page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25) mesh triangles in all (a mesh painted a thousand times was triangulated a thousand times); each cut logs a warning; (20) PDF function construction is limited to 64 levels and 10,000 total nodes with cycle detection; calculator parsing and evaluation share procedure depth/work budgets, integer division/bitshift/rotation/index are panic-free, and fixed-size stack errors propagate; (21) a Type 0 font drawn with a substitute that isn't CID-keyed (it isn't embedded, or its program doesn't load) draws `.nLine truncated
|
||||
|
||||
| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) backport of hayro 0.8: shadings that `as_svg_gradient` can represent are painted as a `vello_cpu` linear or two-point radial gradient instead of a texture sampled pixel by pixel through the shading function (InDesign fades are luminosity soft masks of one full-page axial `sh`: 40 on a report page took 15 s at 144 dpi, now under 1 s); without upstream's half-pixel shift, which suits vello_cpu 0.3 but moves 0.0.8's gradients off pixel centres; (11) `may_paint_rect`: an image whose stream-size box, mapped to the device, misses the canvas is skipped before decoding (decoded data can only shrink against that box), so a tile of an image-heavy page no longer decodes, resamples and rasterizes every image on it; (12) preview renders (`InterpreterSettings::preview`: backdrops, thumbnails, grid cells, signature-drag backgrounds) skip images under `PREVIEW_MIN_IMAGE_AREA` device pixels, while final renders always draw them | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over`; (8) `raster::tests::image_resampling_rejects_anisotropic_target_growth`, `raster::tests::image_resampling_bounds_intermediate_before_planning`, `raster::tests::image_resampling_rejects_invalid_sources_and_scales`, `raster::tests::image_resampling_keeps_valid_sizes_and_exact_limits`, `raster::tests::resampling_fallback_preserves_original_geometry_and_pixels`, `raster::tests::image_resampling_wide_sources_shrink_before_backend_side_limits`, `raster::tests::image_resampling_wide_transparent_sources_still_shrink`, `raster::tests::image_resampling_padded_backend_dimensions_stay_checked`, `raster::tests::image_resampling_mismatched_alpha_mask_keeps_pixels_and_placement`; (9) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (10) `raster::tests::axial_and_radial_shadings_draw_as_native_gradients`; (11) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (12) `raster::tests::preview_renders_skip_tiny_images_but_finals_draw_everything` |
|
||||
| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap; (8) checked image source/target/RGBA, alpha-premultiplication scratch and intermediate sizes before resampling, finite transforms and exact source/mask buffers; sources wider than u16 may shrink before backend limits, with a 2^20 source-axis cap and conservative 128 MiB filter-planning estimate; an over-limit or failed resize keeps the original image resolution and geometry when the source fits the backend, otherwise skips it, with a logged warning; staging, resampling output/scratch, RGBA conversion and padding buffers use fallible reserves; (9) `may_paint_canvas`: a fill or stroke whose control-point box, grown by how far its paint reaches past the path (half the stroke width times the larger of the miter limit and √2) plus 0.5 user units and 2 pixels, misses the canvas is not drawn, so a tile no longer strokes and fills every path of its page (item 5 of #623); `set_skip_offscreen_paths` turns this off for the regression test; (10) `/Interpolate` governs the pre-resize too (#624): a minified image that does not request interpolation is pre-resized with nearest-neighbour, planned without the Catmull-Rom filter limits (`image_resampling_size` takes the flag); a Type 3 glyph image is sampled bicubically only when interpolated; a minified image with a matching soft mask is nearest-sampled straight into premultiplied RGBA; (11) `may_paint_rect`: an image whose stream-size box, mapped to the device, misses the canvas is skipped before decoding (decoded data can only shrink against that box), so a tile of an image-heavy page no longer decodes, resamples and rasterizes every image on it; (12) preview renders (`InterpreterSettings::preview`: backdrops, thumbnails, grid cells, signature-drag backgrounds) skip images under `PREVIEW_MIN_IMAGE_AREA` device pixels, while final renders always draw them | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over`; (8) `raster::tests::image_resampling_rejects_anisotropic_target_growth`, `raster::tests::image_resampling_bounds_intermediate_before_planning`, `raster::tests::image_resampling_rejects_invalid_sources_and_scales`, `raster::tests::image_resampling_keeps_valid_sizes_and_exact_limits`, `raster::tests::resampling_fallback_preserves_original_geometry_and_pixels`, `raster::tests::image_resampling_wide_sources_shrink_before_backend_side_limits`, `raster::tests::image_resampling_wide_transparent_sources_still_shrink`, `raster::tests::image_resampling_padded_backend_dimensions_stay_checked`, `raster::tests::image_resampling_mismatched_alpha_mask_keeps_pixels_and_placement`; (9) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (10) `raster::tests::non_interpolated_images_are_not_smoothed_when_minified`, `raster::tests::non_interpolated_gray_images_are_not_smoothed_when_minified`, `raster::tests::non_integer_minification_samples_the_same_columns`, `raster::tests::non_integer_minification_samples_colour_and_mask_from_the_same_columns`, `raster::tests::non_interpolated_alpha_images_keep_their_colours_and_mask_when_minified`, `raster::tests::non_interpolated_type3_glyph_images_are_not_smoothed_when_minified`, `raster::tests::image_resampling_nearest_accepts_wide_sources_the_filter_refuses`, `raster::tests::wide_non_interpolated_sources_draw_after_shrinking`; (11) `raster::tests::skipping_paths_off_the_canvas_changes_no_pixel`; (12) `raster::testLine truncated
|
||||
|
||||
BIN
Binary file not shown.
+1
-3
@@ -10,6 +10,4 @@ Copyright 2014 PDFium Authors. All rights reserved.
|
||||
found in the LICENSE file.
|
||||
|
||||
Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
|
||||
```
|
||||
|
||||
The LAB.icc profile was generated with LCMS2.
|
||||
```
|
||||
Vendored
+79
-39
@@ -11,7 +11,7 @@ use hayro_syntax::object::Stream;
|
||||
use hayro_syntax::object::dict::keys::*;
|
||||
use moxcms::{
|
||||
ColorProfile, DataColorSpace, Layout, Transform8BitExecutor, TransformF32Executor,
|
||||
TransformOptions, Xyzd,
|
||||
TransformOptions,
|
||||
};
|
||||
use smallvec::{SmallVec, ToSmallVec, smallvec};
|
||||
use std::borrow::Cow;
|
||||
@@ -601,11 +601,7 @@ impl CalRgb {
|
||||
Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_flat)
|
||||
}
|
||||
|
||||
fn normalize_white_point_to_d65(
|
||||
&self,
|
||||
source_white_point: &[f32; 3],
|
||||
xyz: &[f32; 3],
|
||||
) -> [f32; 3] {
|
||||
fn normalize_white_point_to_d65(source_white_point: &[f32; 3], xyz: &[f32; 3]) -> [f32; 3] {
|
||||
let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz);
|
||||
let lms_d65 = Self::to_d65(source_white_point, &lms);
|
||||
Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_d65)
|
||||
@@ -637,7 +633,7 @@ impl ToRgb for CalRgb {
|
||||
|
||||
let xyz_flat = self.normalize_white_point_to_flat(&self.white_point, &xyz);
|
||||
let xyz_black = Self::compensate_black_point(&self.black_point, &xyz_flat);
|
||||
let xyz_d65 = self.normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black);
|
||||
let xyz_d65 = Self::normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black);
|
||||
let srgb_xyz = Self::matrix_product(&Self::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65);
|
||||
|
||||
output.copy_from_slice(&[
|
||||
@@ -654,58 +650,93 @@ impl ToRgb for CalRgb {
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Lab {
|
||||
range: [f32; 4],
|
||||
profile: ICCProfile,
|
||||
white_point: [f32; 3],
|
||||
}
|
||||
|
||||
impl Lab {
|
||||
fn new(dict: &Dict<'_>) -> Option<Self> {
|
||||
let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]);
|
||||
// Not sure how this should be used.
|
||||
let _black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]);
|
||||
let range = dict
|
||||
.get::<[f32; 4]>(RANGE)
|
||||
.unwrap_or([-100.0, 100.0, -100.0, 100.0]);
|
||||
|
||||
let mut profile = ColorProfile::new_from_slice(include_bytes!("../assets/LAB.icc")).ok()?;
|
||||
profile.white_point = Xyzd::new(
|
||||
white_point[0] as f64,
|
||||
white_point[1] as f64,
|
||||
white_point[2] as f64,
|
||||
);
|
||||
Some(Self { range, white_point })
|
||||
}
|
||||
|
||||
let profile = ICCProfile::new_from_src_profile(
|
||||
profile, false,
|
||||
// This flag is only used to scale the values to [0.0, 1.0], but
|
||||
// we already take care of this in the `convert_f32` method.
|
||||
// Therefore, leave this as false, even though this is a LAB profile.
|
||||
false, 3,
|
||||
)?;
|
||||
/// The white point the space's L\*a\*b\* values are relative to (ISO 32000-2
|
||||
/// §8.6.5.4). A missing or degenerate entry falls back to a flat white point
|
||||
/// so hostile input cannot break the conversion.
|
||||
fn usable_white_point(&self) -> [f32; 3] {
|
||||
let white_point = self.white_point;
|
||||
if white_point
|
||||
.iter()
|
||||
.all(|component| component.is_finite() && *component > 0.0)
|
||||
{
|
||||
white_point
|
||||
} else {
|
||||
[1.0, 1.0, 1.0]
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self { range, profile })
|
||||
/// Inverse of the CIE f(t) helper that maps relative fX/fY/fZ to absolute
|
||||
/// XYZ as a ratio of the white point.
|
||||
fn inverse_lab_f(t: f32) -> f32 {
|
||||
const DELTA: f32 = 6.0 / 29.0;
|
||||
if t > DELTA {
|
||||
t * t * t
|
||||
} else {
|
||||
3.0 * DELTA * DELTA * (t - 4.0 / 29.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRgb for Lab {
|
||||
fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> {
|
||||
if !manual_scale {
|
||||
// moxcms expects values between 0.0 and 1.0, so we need to undo
|
||||
// the scaling.
|
||||
let [min_a, max_a, min_b, max_b] = self.range;
|
||||
let white_point = self.usable_white_point();
|
||||
let [xw, yw, zw] = white_point;
|
||||
|
||||
let input = input
|
||||
.chunks_exact(3)
|
||||
.flat_map(|i| {
|
||||
let l = i[0] / 100.0;
|
||||
let a = (i[1] + 128.0) / 255.0;
|
||||
let b = (i[2] + 128.0) / 255.0;
|
||||
for (input, output) in input.chunks_exact(3).zip(output.chunks_exact_mut(3)) {
|
||||
// PdfCraft patch: `manual_scale` means the components arrive
|
||||
// normalised to [0, 1] (an `/Indexed` palette over Lab looks its
|
||||
// bytes up as `byte / 255`). Map them linearly onto L* 0..100 and
|
||||
// the a*/b* /Range (ISO 32000-2 §8.6.6.3) before converting.
|
||||
let (l, a, b) = if manual_scale {
|
||||
(
|
||||
clamp_component(input[0], 0.0, 1.0) * 100.0,
|
||||
min_a + clamp_component(input[1], 0.0, 1.0) * (max_a - min_a),
|
||||
min_b + clamp_component(input[2], 0.0, 1.0) * (max_b - min_b),
|
||||
)
|
||||
} else {
|
||||
(input[0], input[1], input[2])
|
||||
};
|
||||
let l = clamp_component(l, 0.0, 100.0);
|
||||
let a = clamp_component(a, min_a, max_a);
|
||||
let b = clamp_component(b, min_b, max_b);
|
||||
|
||||
[l, a, b]
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// L*a*b* is relative to the space's own /WhitePoint. Map it to XYZ
|
||||
// in that white's coordinates, Bradford-adapt to D65 and convert to
|
||||
// sRGB (ISO 32000-2 §8.6.5.4).
|
||||
let fy = (l + 16.0) / 116.0;
|
||||
let fx = fy + a / 500.0;
|
||||
let fz = fy - b / 200.0;
|
||||
let xyz = [
|
||||
xw * Self::inverse_lab_f(fx),
|
||||
yw * Self::inverse_lab_f(fy),
|
||||
zw * Self::inverse_lab_f(fz),
|
||||
];
|
||||
|
||||
self.profile.convert_f32(&input, output, manual_scale)
|
||||
} else {
|
||||
self.profile.convert_f32(input, output, manual_scale)
|
||||
let xyz_d65 = CalRgb::normalize_white_point_to_d65(&white_point, &xyz);
|
||||
let srgb = CalRgb::matrix_product(&CalRgb::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65);
|
||||
|
||||
output.copy_from_slice(&[
|
||||
f32_to_u8(CalRgb::srgb_transfer_function(srgb[0])),
|
||||
f32_to_u8(CalRgb::srgb_transfer_function(srgb[1])),
|
||||
f32_to_u8(CalRgb::srgb_transfer_function(srgb[2])),
|
||||
]);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1029,6 +1060,15 @@ fn f32_to_u8(val: f32) -> u8 {
|
||||
(val * 255.0 + 0.5) as u8
|
||||
}
|
||||
|
||||
/// Clamp an input-derived colour component to `[lo, hi]` without panicking on
|
||||
/// NaN or on a reversed range. Non-finite values map to zero.
|
||||
fn clamp_component(value: f32, lo: f32, hi: f32) -> f32 {
|
||||
if !value.is_finite() {
|
||||
return 0.0;
|
||||
}
|
||||
value.max(lo).min(hi)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// A color.
|
||||
pub struct Color {
|
||||
|
||||
Reference in new issue
Block a user