From c3487320b2d1de657b860b3c8abb44e1f2b32bd8 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Tue, 13 Oct 2026 12:29:12 -0600 Subject: [PATCH 1/6] feat(pdf): add shared guarded writer foundation --- ATTRIBUTION.md | 20 + Cargo.lock | 513 +- Cargo.toml | 8 + NOTICE | 14 + README.md | 2 + crates/pdf/Cargo.toml | 30 + crates/pdf/README.md | 115 + crates/pdf/src/lib.rs | 8 + crates/pdf/src/write.rs | 179 + crates/pdf/tests/fonts.json | 40 + crates/pdf/tests/unicode_writing.rs | 139 + crates/pdf/tests/writing.rs | 319 ++ licenses/pdf-writer/LICENSE-ICC.txt | 116 + licenses/pdf-writer/LICENSE_APACHE | 202 + licenses/pdf-writer/LICENSE_MIT | 21 + vendor/krilla/Cargo.toml | 165 + vendor/krilla/README.craft.md | 28 + vendor/krilla/README.md | 156 + vendor/krilla/SOURCE.json | 384 ++ vendor/krilla/examples/basic.rs | 117 + vendor/krilla/examples/empty_document.rs | 25 + vendor/krilla/examples/parley.rs | 174 + vendor/krilla/examples/simple_text.rs | 118 + vendor/krilla/examples/stream_builder.rs | 93 + vendor/krilla/icc/LICENSE | 116 + vendor/krilla/icc/README.md | 1 + vendor/krilla/icc/sGrey-v2-magic.icc | Bin 0 -> 616 bytes vendor/krilla/icc/sGrey-v4.icc | Bin 0 -> 360 bytes vendor/krilla/icc/sRGB-v2-magic.icc | Bin 0 -> 736 bytes vendor/krilla/icc/sRGB-v4.icc | Bin 0 -> 480 bytes vendor/krilla/src/chunk_container.rs | 489 ++ vendor/krilla/src/configure/PDF_A1.md | 227 + vendor/krilla/src/configure/PDF_A2.md | 269 + vendor/krilla/src/configure/PDF_A3.md | 31 + vendor/krilla/src/configure/PDF_A4.md | 211 + vendor/krilla/src/configure/PDF_UA1.md | 202 + vendor/krilla/src/configure/README.md | 23 + vendor/krilla/src/configure/mod.rs | 197 + vendor/krilla/src/configure/validate.rs | 1237 +++++ vendor/krilla/src/configure/version.rs | 134 + vendor/krilla/src/content.rs | 1352 +++++ vendor/krilla/src/data.rs | 49 + vendor/krilla/src/document.rs | 160 + vendor/krilla/src/error.rs | 173 + vendor/krilla/src/geom.rs | 341 ++ vendor/krilla/src/graphics/blend.rs | 46 + vendor/krilla/src/graphics/color.rs | 552 ++ vendor/krilla/src/graphics/graphic.rs | 30 + vendor/krilla/src/graphics/graphics_state.rs | 251 + vendor/krilla/src/graphics/icc.rs | 218 + vendor/krilla/src/graphics/image.rs | 835 +++ vendor/krilla/src/graphics/mask.rs | 128 + vendor/krilla/src/graphics/mod.rs | 14 + vendor/krilla/src/graphics/paint.rs | 434 ++ vendor/krilla/src/graphics/separation.rs | 86 + .../krilla/src/graphics/shading_function.rs | 865 +++ vendor/krilla/src/graphics/shading_pattern.rs | 60 + vendor/krilla/src/graphics/tiling_pattern.rs | 119 + vendor/krilla/src/graphics/xobject.rs | 150 + vendor/krilla/src/interactive/action.rs | 68 + vendor/krilla/src/interactive/annotation.rs | 283 + vendor/krilla/src/interactive/destination.rs | 289 + vendor/krilla/src/interactive/mod.rs | 3 + vendor/krilla/src/interchange/embed.rs | 240 + vendor/krilla/src/interchange/metadata.rs | 477 ++ vendor/krilla/src/interchange/mod.rs | 4 + vendor/krilla/src/interchange/outline.rs | 263 + vendor/krilla/src/interchange/tagging/fmt.rs | 733 +++ .../src/interchange/tagging/generate.toml | 474 ++ .../src/interchange/tagging/generated.rs | 4754 +++++++++++++++++ vendor/krilla/src/interchange/tagging/mod.rs | 1324 +++++ vendor/krilla/src/interchange/tagging/tag.rs | 686 +++ vendor/krilla/src/lib.rs | 185 + vendor/krilla/src/num.rs | 26 + vendor/krilla/src/page.rs | 623 +++ vendor/krilla/src/pdf.rs | 313 ++ vendor/krilla/src/resource.rs | 436 ++ vendor/krilla/src/serialize.rs | 1104 ++++ vendor/krilla/src/stream.rs | 368 ++ vendor/krilla/src/surface.rs | 675 +++ vendor/krilla/src/text/cid.rs | 488 ++ vendor/krilla/src/text/font.rs | 351 ++ vendor/krilla/src/text/glyph/bitmap.rs | 81 + vendor/krilla/src/text/glyph/colr.rs | 581 ++ vendor/krilla/src/text/glyph/mod.rs | 205 + vendor/krilla/src/text/glyph/outline.rs | 72 + vendor/krilla/src/text/glyph/svg.rs | 34 + vendor/krilla/src/text/group.rs | 463 ++ vendor/krilla/src/text/mod.rs | 232 + vendor/krilla/src/text/shape.rs | 102 + vendor/krilla/src/text/type3.rs | 514 ++ vendor/krilla/src/util.rs | 286 + 92 files changed, 28705 insertions(+), 18 deletions(-) create mode 100644 crates/pdf/Cargo.toml create mode 100644 crates/pdf/README.md create mode 100644 crates/pdf/src/lib.rs create mode 100644 crates/pdf/src/write.rs create mode 100644 crates/pdf/tests/fonts.json create mode 100644 crates/pdf/tests/unicode_writing.rs create mode 100644 crates/pdf/tests/writing.rs create mode 100644 licenses/pdf-writer/LICENSE-ICC.txt create mode 100644 licenses/pdf-writer/LICENSE_APACHE create mode 100644 licenses/pdf-writer/LICENSE_MIT create mode 100644 vendor/krilla/Cargo.toml create mode 100644 vendor/krilla/README.craft.md create mode 100644 vendor/krilla/README.md create mode 100644 vendor/krilla/SOURCE.json create mode 100644 vendor/krilla/examples/basic.rs create mode 100644 vendor/krilla/examples/empty_document.rs create mode 100644 vendor/krilla/examples/parley.rs create mode 100644 vendor/krilla/examples/simple_text.rs create mode 100644 vendor/krilla/examples/stream_builder.rs create mode 100644 vendor/krilla/icc/LICENSE create mode 100644 vendor/krilla/icc/README.md create mode 100644 vendor/krilla/icc/sGrey-v2-magic.icc create mode 100644 vendor/krilla/icc/sGrey-v4.icc create mode 100644 vendor/krilla/icc/sRGB-v2-magic.icc create mode 100644 vendor/krilla/icc/sRGB-v4.icc create mode 100644 vendor/krilla/src/chunk_container.rs create mode 100644 vendor/krilla/src/configure/PDF_A1.md create mode 100644 vendor/krilla/src/configure/PDF_A2.md create mode 100644 vendor/krilla/src/configure/PDF_A3.md create mode 100644 vendor/krilla/src/configure/PDF_A4.md create mode 100644 vendor/krilla/src/configure/PDF_UA1.md create mode 100644 vendor/krilla/src/configure/README.md create mode 100644 vendor/krilla/src/configure/mod.rs create mode 100644 vendor/krilla/src/configure/validate.rs create mode 100644 vendor/krilla/src/configure/version.rs create mode 100644 vendor/krilla/src/content.rs create mode 100644 vendor/krilla/src/data.rs create mode 100644 vendor/krilla/src/document.rs create mode 100644 vendor/krilla/src/error.rs create mode 100644 vendor/krilla/src/geom.rs create mode 100644 vendor/krilla/src/graphics/blend.rs create mode 100644 vendor/krilla/src/graphics/color.rs create mode 100644 vendor/krilla/src/graphics/graphic.rs create mode 100644 vendor/krilla/src/graphics/graphics_state.rs create mode 100644 vendor/krilla/src/graphics/icc.rs create mode 100644 vendor/krilla/src/graphics/image.rs create mode 100644 vendor/krilla/src/graphics/mask.rs create mode 100644 vendor/krilla/src/graphics/mod.rs create mode 100644 vendor/krilla/src/graphics/paint.rs create mode 100644 vendor/krilla/src/graphics/separation.rs create mode 100644 vendor/krilla/src/graphics/shading_function.rs create mode 100644 vendor/krilla/src/graphics/shading_pattern.rs create mode 100644 vendor/krilla/src/graphics/tiling_pattern.rs create mode 100644 vendor/krilla/src/graphics/xobject.rs create mode 100644 vendor/krilla/src/interactive/action.rs create mode 100644 vendor/krilla/src/interactive/annotation.rs create mode 100644 vendor/krilla/src/interactive/destination.rs create mode 100644 vendor/krilla/src/interactive/mod.rs create mode 100644 vendor/krilla/src/interchange/embed.rs create mode 100644 vendor/krilla/src/interchange/metadata.rs create mode 100644 vendor/krilla/src/interchange/mod.rs create mode 100644 vendor/krilla/src/interchange/outline.rs create mode 100644 vendor/krilla/src/interchange/tagging/fmt.rs create mode 100644 vendor/krilla/src/interchange/tagging/generate.toml create mode 100644 vendor/krilla/src/interchange/tagging/generated.rs create mode 100644 vendor/krilla/src/interchange/tagging/mod.rs create mode 100644 vendor/krilla/src/interchange/tagging/tag.rs create mode 100644 vendor/krilla/src/lib.rs create mode 100644 vendor/krilla/src/num.rs create mode 100644 vendor/krilla/src/page.rs create mode 100644 vendor/krilla/src/pdf.rs create mode 100644 vendor/krilla/src/resource.rs create mode 100644 vendor/krilla/src/serialize.rs create mode 100644 vendor/krilla/src/stream.rs create mode 100644 vendor/krilla/src/surface.rs create mode 100644 vendor/krilla/src/text/cid.rs create mode 100644 vendor/krilla/src/text/font.rs create mode 100644 vendor/krilla/src/text/glyph/bitmap.rs create mode 100644 vendor/krilla/src/text/glyph/colr.rs create mode 100644 vendor/krilla/src/text/glyph/mod.rs create mode 100644 vendor/krilla/src/text/glyph/outline.rs create mode 100644 vendor/krilla/src/text/glyph/svg.rs create mode 100644 vendor/krilla/src/text/group.rs create mode 100644 vendor/krilla/src/text/mod.rs create mode 100644 vendor/krilla/src/text/shape.rs create mode 100644 vendor/krilla/src/text/type3.rs create mode 100644 vendor/krilla/src/util.rs diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 5504fb05..7a6e0537 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -9,3 +9,23 @@ author, source and license. Library code and original assets are under the repos | `crates/cms/profiles/photocraft-coated-cmyk.icc` | PhotoCraft contributors; generated by `craft_cms::synth::coated_cmyk` | [PhotoCraft `crates/cms`](https://github.com/storytold/photocraft/tree/b3222ee8e7e6bcb4799c37f904c595f20f9baf5e/crates/cms) | CC0-1.0 | | `crates/cms/profiles/craft-coated-fogra39.icc` | craft-libs contributors; generated by `crates/cms/tests/fogra39.rs` from the FOGRA39 data below | this repository | CC0-1.0 | | `crates/cms/tests/data/FOGRA39.txt` (unmodified) | Fogra | [Fogra characterisation data](https://fogra.org/en/downloads/work-tools/characterisation-data), `FOGRA39_to_FOGRA60_v2.zip` | Fogra terms: free use and redistribution, unmodified, with Fogra identified as the source; "FOGRA39" identifies the reference data only and implies no certification, approval or endorsement by Fogra (full text in [NOTICE](NOTICE)) | + +Original assets are covered by the repository licence. + +## Assets embedded by the optional PDF writer + +Krilla 0.8.2 bundles the following Compact ICC Profiles by Clinton Ingram, under CC0-1.0. +The immutable source revision is `3ffdf0588cf98050aad6edba51ca70162e1fb5b5`, recorded by the +registry package's `.cargo_vcs_info.json`. These are the same dependency profiles already +used by the apps' Krilla exporters. The local backend snapshot retains these four profiles; no font binary is copied into this repository. +The profile licence is retained in `licenses/pdf-writer/LICENSE-ICC.txt`. + +| Crate / file | Author | Source | Licence | SHA-256 | Use | +| --- | --- | --- | --- | --- | --- | +| `krilla 0.8.2 / icc/sGrey-v2-magic.icc` | Clinton Ingram | https://github.com/LaurenzV/krilla/blob/3ffdf0588cf98050aad6edba51ca70162e1fb5b5/crates/krilla/icc/sGrey-v2-magic.icc | CC0-1.0 | `4110169287e5f1b5149ca9f1da1814d32943905bba8030e849d1a3b729a19481` | PDF writer colour profiles | +| `krilla 0.8.2 / icc/sGrey-v4.icc` | Clinton Ingram | https://github.com/LaurenzV/krilla/blob/3ffdf0588cf98050aad6edba51ca70162e1fb5b5/crates/krilla/icc/sGrey-v4.icc | CC0-1.0 | `00c0f94e09127520a17dc0e1d9264b5702081d96dbdb1549ee88e3631ce42a9d` | PDF writer colour profiles | +| `krilla 0.8.2 / icc/sRGB-v2-magic.icc` | Clinton Ingram | https://github.com/LaurenzV/krilla/blob/3ffdf0588cf98050aad6edba51ca70162e1fb5b5/crates/krilla/icc/sRGB-v2-magic.icc | CC0-1.0 | `af4efe28f6d311799865f325ba39184a2e978b113ca74124d60af7de22b105f4` | PDF writer colour profiles | +| `krilla 0.8.2 / icc/sRGB-v4.icc` | Clinton Ingram | https://github.com/LaurenzV/krilla/blob/3ffdf0588cf98050aad6edba51ca70162e1fb5b5/crates/krilla/icc/sRGB-v4.icc | CC0-1.0 | `c56e1685d888f5edb92fe07f2750f387f8fe8e91b32ff8fb0b56bfbbb9458353` | PDF writer colour profiles | + +The four Krilla ICC entries above are also stored unchanged in `vendor/krilla/icc`. +The vendored source patch and per-file hashes are recorded in `vendor/krilla/README.craft.md` and `vendor/krilla/SOURCE.json`. diff --git a/Cargo.lock b/Cargo.lock index ecf87d0e..bda86020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -120,6 +135,17 @@ dependencies = [ "libloading", ] +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -228,6 +254,27 @@ dependencies = [ "objc2 0.6.5", ] +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -304,6 +351,12 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.6.0" @@ -415,6 +468,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.8" @@ -496,6 +555,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -529,6 +597,20 @@ dependencies = [ "rayon", ] +[[package]] +name = "craft-pdf" +version = "0.1.0" +dependencies = [ + "hayro-cmap", + "hayro-interpret", + "hayro-syntax", + "krilla", + "kurbo 0.13.1", + "thiserror 2.0.21", + "ttf-parser", + "wasm-bindgen-test", +] + [[package]] name = "craft-print" version = "0.1.0" @@ -970,14 +1052,14 @@ dependencies = [ "ecolor", "emath", "epaint_default_fonts", - "font-types", + "font-types 0.12.6", "harfrust", "log", "nohash-hasher", "parking_lot", "profiling", "self_cell", - "skrifa", + "skrifa 0.44.0", "smallvec", "unicode-general-category", "unicode-segmentation", @@ -1003,7 +1085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1048,6 +1130,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" +[[package]] +name = "fearless_simd" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3296eb97d869113eaf148a6bdf01884e90b3828ad7ae2340b0f93362fc158611" + [[package]] name = "filetime" version = "0.2.29" @@ -1086,6 +1174,9 @@ name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] [[package]] name = "fmt-derive" @@ -1121,6 +1212,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + [[package]] name = "font-types" version = "0.12.6" @@ -1268,6 +1368,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -1290,7 +1400,7 @@ dependencies = [ "hashbrown 0.17.1", "log", "peniko", - "skrifa", + "skrifa 0.44.0", "smallvec", "vello_common", ] @@ -1378,7 +1488,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ "bitflags 2.13.2", "bytemuck", - "read-fonts", + "read-fonts 0.41.0", "smallvec", ] @@ -1402,6 +1512,99 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hayro-ccitt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" + +[[package]] +name = "hayro-ccitt" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2866f0d976e76497480b853d5e258dab6e4b89ecfe991124e08f15dfa8fd03ad" + +[[package]] +name = "hayro-cmap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d285dc30731c8485de5fa732fbdf2b3affdf01e4da7c2135022ca6fa664bf6" +dependencies = [ + "brotli", + "hayro-postscript", +] + +[[package]] +name = "hayro-interpret" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2613d0406898995042d0794c4245b2f2fba1246490c8ac593769bba6551129d" +dependencies = [ + "bitflags 2.13.2", + "hayro-cmap", + "hayro-syntax", + "kurbo 0.13.1", + "moxcms", + "phf", + "rustc-hash 2.1.3", + "siphasher", + "skrifa 0.42.1", + "smallvec", + "yoke", +] + +[[package]] +name = "hayro-jbig2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000e025850f4592ddcaee85640db70704151f2bafd43b6f431aa5b6a2c6a35a9" +dependencies = [ + "fearless_simd 1.1.0", + "hayro-ccitt 0.4.0", +] + +[[package]] +name = "hayro-jpeg2000" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" +dependencies = [ + "fearless_simd 0.4.1", +] + +[[package]] +name = "hayro-postscript" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" + +[[package]] +name = "hayro-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edeafd70aa2db743de8ede8637d07ec87db05efe69cde371d03f1b185fcef27" +dependencies = [ + "flate2", + "hayro-ccitt 0.3.0", + "hayro-jbig2", + "hayro-jpeg2000", + "memchr", + "rustc-hash 2.1.3", + "smallvec", + "zune-jpeg", +] + +[[package]] +name = "hayro-write" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b3db92c4917aad24ff30d61413a9a7509ae88b27aee1ae34eff881e1d2723a" +dependencies = [ + "flate2", + "hayro-syntax", + "pdf-writer", +] + [[package]] name = "heck" version = "0.5.0" @@ -1663,12 +1866,28 @@ dependencies = [ "tiff", ] +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "imagesize" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + [[package]] name = "indexmap" version = "2.14.2" @@ -1816,6 +2035,33 @@ dependencies = [ "accesskit_consumer", ] +[[package]] +name = "krilla" +version = "0.8.2" +dependencies = [ + "base64 0.22.1", + "bumpalo", + "flate2", + "float-cmp", + "gif", + "hayro-write", + "image-webp", + "imagesize 0.14.0", + "indexmap", + "pdf-writer", + "png 0.18.1", + "rustc-hash 2.1.3", + "rustybuzz", + "siphasher", + "skrifa 0.42.1", + "smallvec", + "subsetter", + "tiny-skia-path 0.12.0", + "xmp-writer", + "yoke", + "zune-jpeg", +] + [[package]] name = "kurbo" version = "0.11.3" @@ -1945,6 +2191,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2001,7 +2257,7 @@ dependencies = [ "objc2-foundation 0.3.2", "png 0.18.1", "thiserror 2.0.21", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2084,6 +2340,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2432,6 +2697,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -2486,6 +2757,18 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pdf-writer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e456864a7a304047bff84977dc6fb162bd956475d40ba50b2dcecaada7f753" +dependencies = [ + "bitflags 2.13.2", + "itoa", + "memchr", + "ryu", +] + [[package]] name = "pem" version = "3.0.6" @@ -2524,6 +2807,48 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -2776,7 +3101,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2870,6 +3195,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types 0.11.3", +] + [[package]] name = "read-fonts" version = "0.41.0" @@ -2877,7 +3212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", - "font-types", + "font-types 0.12.6", "once_cell", ] @@ -3054,7 +3389,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3111,7 +3446,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3138,6 +3473,24 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.23" @@ -3524,6 +3877,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts 0.39.2", +] + [[package]] name = "skrifa" version = "0.44.0" @@ -3531,7 +3894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.41.0", ] [[package]] @@ -3609,7 +3972,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3673,6 +4036,18 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "subsetter" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38803281d1c23166c5ebcb455439a5d2afe711cc909cf88af72448c297756ad6" +dependencies = [ + "kurbo 0.13.1", + "rustc-hash 2.1.3", + "skrifa 0.42.1", + "write-fonts", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3783,7 +4158,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3861,7 +4236,7 @@ dependencies = [ "cfg-if", "log", "png 0.17.16", - "tiny-skia-path", + "tiny-skia-path 0.11.4", ] [[package]] @@ -3875,6 +4250,17 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -4085,6 +4471,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + [[package]] name = "type-map" version = "0.5.1" @@ -4100,6 +4495,18 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-general-category" version = "1.1.0" @@ -4112,6 +4519,18 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -4157,7 +4576,7 @@ dependencies = [ "base64 0.22.1", "data-url", "flate2", - "imagesize", + "imagesize 0.13.0", "kurbo 0.11.3", "log", "pico-args", @@ -4166,7 +4585,7 @@ dependencies = [ "siphasher", "strict-num", "svgtypes", - "tiny-skia-path", + "tiny-skia-path 0.11.4", "xmlwriter", ] @@ -4189,7 +4608,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2" dependencies = [ "bytemuck", - "fearless_simd", + "fearless_simd 0.4.1", "guillotiere", "log", "peniko", @@ -4296,6 +4715,45 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae7499dfd45780a0a91d7ee6bb9ac51970a4479a41a89da443fdda5a39547d42" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b84b5ac638bfb168196a1a461fcc8f46a294a18b1b6be52133b4e0db122cc9f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f692aa943ccd88363733b77063f32cfed5bc6cbea8e6e8b251b302f881606fe" + [[package]] name = "wayland-backend" version = "0.3.17" @@ -4642,7 +5100,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5007,6 +5465,19 @@ version = "0.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef0ffc427f045c0cc9ebffd6f4f91153dcd2a1547b5c3c29ee3f541e16e95c6" +[[package]] +name = "write-fonts" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb731d4c4d93eacc69a1ad2f270f905788a98e4a3438267bcafbe08d3431c8d8" +dependencies = [ + "font-types 0.11.3", + "indexmap", + "kurbo 0.13.1", + "log", + "read-fonts 0.39.2", +] + [[package]] name = "writeable" version = "0.6.4" @@ -5108,6 +5579,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xmp-writer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" + [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index 8664090b..665729b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "3" members = ["crates/*"] +exclude = ["vendor/krilla"] [workspace.package] version = "0.1.0" @@ -11,6 +12,9 @@ repository = "https://github.com/storytold/craft-libs" [workspace.dependencies] # internal: add each crate here as `craft- = { path = "crates/" }` +craft-pdf = { path = "crates/pdf", version = "=0.1.0" } +krilla = "=0.8.2" +thiserror = "2" craft-atomic = { path = "crates/atomic" } craft-cms = { path = "crates/cms" } craft-print = { path = "crates/print" } @@ -22,7 +26,11 @@ rayon = "1" [workspace.lints.rust] unsafe_code = "forbid" +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(craft_pdf_unicode_fixtures)"] } [workspace.lints.clippy] dbg_macro = "warn" todo = "warn" + +[patch.crates-io] +krilla = { path = "vendor/krilla" } diff --git a/NOTICE b/NOTICE index bf247173..353bde3d 100644 --- a/NOTICE +++ b/NOTICE @@ -20,6 +20,8 @@ crates/cms/tests/data/FOGRA39.txt: FOGRA39 characterisation data, distributed un FOGRAxx may be used solely to identify the respective reference data. Such use does not imply certification, approval or endorsement by Fogra." +The optional PDF writer bundles four CC0 ICC profiles by Clinton Ingram. See ATTRIBUTION.md for per-file sources and hashes. + Source provenance The SVG icon preparation in crates/ui is adapted from: @@ -79,3 +81,15 @@ The colour management module in crates/cms is extracted from: Copyright (c) 2026 ArtCraft Team and the PhotoCraft contributors. Licensed MIT OR Apache-2.0. Its generated ICC profile is dedicated to the public domain (CC0-1.0); see ATTRIBUTION.md. + +Optional PDF writer + +Krilla 0.8.2 (https://github.com/LaurenzV/krilla), MIT OR Apache-2.0. +Upstream licence texts are retained in licenses/pdf-writer. +The optional writer embeds Compact ICC Profiles by Clinton Ingram +(https://github.com/saucecontrol/Compact-ICC-Profiles), under CC0-1.0. +See licenses/pdf-writer/LICENSE-ICC.txt and ATTRIBUTION.md for sources and hashes. +No font files are copied into this repository. The four existing writer ICC profiles are retained under vendor/krilla/icc. + +Krilla is vendored with a native unwind-cleanup patch; see vendor/krilla/README.craft.md +and vendor/krilla/SOURCE.json. Its upstream MIT/Apache-2.0 licences and CC0 profiles are unchanged. diff --git a/README.md b/README.md index 3e7ad3e1..cd1da872 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ craft- = { git = "https://github.com/storytold/craft-libs", rev = "), + #[error("PDF export failed internally: {0}")] + BackendPanic(String), + #[error("font exceeds the {MAX_FONT_BYTES}-byte embedding limit")] + FontLimit, + #[error("font variation coordinates must be finite and number at most {MAX_FONT_VARIATIONS}")] + FontVariations, + #[error("font data could not be allocated")] + Allocation, + #[error("font could not be read for embedding")] + InvalidFont, +} + +/// Run an entire app export, including drawing and postprocessing, behind a native +/// unwind guard. Existing application errors pass through unchanged. +/// +/// Discard any writer or drawing state captured by the closure after a panic. Prefer +/// constructing the writer inside the closure. WASM aborts and process-wide allocation +/// failures cannot be caught; this is not a memory or execution-time budget. +pub fn guard>(operation: impl FnOnce() -> Result) -> Result { + catch_unwind(AssertUnwindSafe(operation)).map_err(|payload| { + let message = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .unwrap_or("non-string panic payload"); + E::from(WriteError::BackendPanic(message.chars().take(1024).collect())) + })? +} + +/// Build a version/archival configuration without changing application export policy. +pub fn configuration(version: PdfVersion, archival: Option) -> Result { + guard(|| { + let mut builder = ConfigurationBuilder::new().with_version(version); + if let Some(archival) = archival { + builder = builder.with_archival_validator(archival); + } + builder.finish().map_err(WriteError::Configuration) + }) +} + +/// Common document information. Callers decide which values to provide and whether to +/// trim them. Dates are supplied explicitly; this crate never consults a platform clock. +#[derive(Clone, Debug, Default)] +pub struct Metadata { + pub creator: String, + pub producer: String, + pub title: Option, + pub authors: Vec, + pub description: Option, + pub keywords: Vec, + pub created: Option, +} + +impl Metadata { + fn into_backend(self) -> backend::metadata::Metadata { + let mut metadata = + backend::metadata::Metadata::new().creator(self.creator).producer(self.producer).authors(self.authors).keywords(self.keywords); + if let Some(title) = self.title { + metadata = metadata.title(title); + } + if let Some(description) = self.description { + metadata = metadata.description(description); + } + if let Some(created) = self.created { + metadata = metadata.creation_date(created); + } + metadata + } +} + +/// A writer whose serialization is fallible and consumes the document on every outcome. +pub struct Writer { + document: backend::Document, +} + +impl Writer { + /// Keep all backend settings, including colour profiles, compression and validators. + pub fn new(settings: backend::SerializeSettings, metadata: Metadata) -> Self { + Self::new_with_language(settings, metadata, None) + } + + /// Set an optional document language without changing the common metadata fields. + /// Empty language is treated as absent. Callers validate nonempty tags, which pass through unchanged. + pub fn new_with_language(settings: backend::SerializeSettings, metadata: Metadata, language: Option) -> Self { + let mut metadata = metadata.into_backend(); + if let Some(language) = language.filter(|language| !language.is_empty()) { + metadata = metadata.language(language); + } + let mut document = backend::Document::new_with(settings); + document.set_metadata(metadata); + Self { document } + } + + /// Advanced drawing, resource, outline and tag-tree access for an application's adapter. + /// Use [`guard`] around the whole export to cover drawing as well as serialization. + pub fn document_mut(&mut self) -> &mut backend::Document { + &mut self.document + } + + /// Serialize, preserving validation failures and catching native backend unwinds. + /// The returned byte vector is not a bound on peak serialization memory. + pub fn finish(self) -> Result, WriteError> { + guard(|| self.document.finish().map_err(|error| WriteError::Serialization(Box::new(error)))) + } +} + +fn check_font_input(size: usize, variations: Option<&[([u8; 4], f32)]>) -> Result<(), WriteError> { + if size > MAX_FONT_BYTES { + return Err(WriteError::FontLimit); + } + if variations.is_some_and(|coords| coords.len() > MAX_FONT_VARIATIONS || coords.iter().any(|(_, value)| !value.is_finite())) { + return Err(WriteError::FontVariations); + } + Ok(()) +} + +/// Copy and prepare an already selected, licensed font for Krilla's subset embedding. +/// `None` retains static-font construction; `Some` retains variable-font construction, +/// including an empty coordinate list. Selection, caching, shaping and fallback belong +/// to the app. An error never substitutes another font or drops text silently. +pub fn font(data: &[u8], index: u32, variations: Option<&[([u8; 4], f32)]>) -> Result { + check_font_input(data.len(), variations)?; + let mut owned = Vec::new(); + owned.try_reserve_exact(data.len()).map_err(|_| WriteError::Allocation)?; + owned.extend_from_slice(data); + guard(|| { + let font = match variations { + None => Font::new(owned.into(), index), + Some(coords) => { + let coords: Vec<_> = coords.iter().map(|(tag, value)| (Tag::new(tag), *value)).collect(); + Font::new_variable(owned.into(), index, &coords) + } + }; + font.ok_or(WriteError::InvalidFont) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn font_bounds_are_checked_without_allocating_the_input() { + assert!(check_font_input(MAX_FONT_BYTES, None).is_ok()); + assert!(matches!(check_font_input(MAX_FONT_BYTES + 1, None), Err(WriteError::FontLimit))); + assert!(matches!(check_font_input(usize::MAX, None), Err(WriteError::FontLimit))); + let coords = vec![(*b"wght", 400.0); MAX_FONT_VARIATIONS + 1]; + assert!(matches!(check_font_input(0, Some(&coords)), Err(WriteError::FontVariations))); + } +} diff --git a/crates/pdf/tests/fonts.json b/crates/pdf/tests/fonts.json new file mode 100644 index 00000000..a19b3a81 --- /dev/null +++ b/crates/pdf/tests/fonts.json @@ -0,0 +1,40 @@ +[ + { + "name": "NotoSansThai[wdth,wght].ttf", + "source": "https://raw.githubusercontent.com/google/fonts/8b0a1d0f5983c89bc2b93f1b5fb55f9e252744b5/ofl/notosansthai/NotoSansThai%5Bwdth%2Cwght%5D.ttf", + "sha256": "5a1c559bb539583c8a1fd99d1c5b9491e5e14478c9cd2bd0970d5c3096cc9ef8", + "commit": "8b0a1d0f5983c89bc2b93f1b5fb55f9e252744b5", + "license": "OFL-1.1" + }, + { + "name": "OFL.txt", + "source": "https://raw.githubusercontent.com/google/fonts/8b0a1d0f5983c89bc2b93f1b5fb55f9e252744b5/ofl/notosansthai/OFL.txt", + "sha256": "2e98fd23a52d253db8612cd5942c8f2ff4111b21d2367050fdca91d8ccc374a0", + "commit": "8b0a1d0f5983c89bc2b93f1b5fb55f9e252744b5", + "license": "OFL-1.1" + }, + { + "name": "NotoSansArabic.ttf", + "source": "https://raw.githubusercontent.com/storytold/craft-fonts/8dcdacd5153e64560d109541a47d806f26f048c0/fonts/noto-sans-arabic/NotoSansArabic.ttf", + "sha256": "63111b5b2e074dd48cc67692e0a2726d86ee94c1c37fe8598257b7b4e87e869e", + "license": "OFL-1.1" + }, + { + "name": "OFL-Arabic.txt", + "source": "https://raw.githubusercontent.com/storytold/craft-fonts/8dcdacd5153e64560d109541a47d806f26f048c0/fonts/noto-sans-arabic/OFL.txt", + "sha256": "07fc70bfeb985cc1a87a8587d0a0c80bab11c86c9dc3fd95b6f0cb332f983e96", + "license": "OFL-1.1" + }, + { + "name": "Inter-Regular.ttf", + "source": "https://raw.githubusercontent.com/storytold/pdfcraft/a6bc63a497954af87c1cd5a2a1e72e928a11c039/assets/fonts/Inter-Regular.ttf", + "sha256": "40d692fce188e4471e2b3cba937be967878f631ad3ebbbdcd587687c7ebe0c82", + "license": "OFL-1.1" + }, + { + "name": "OFL-Inter.txt", + "source": "https://raw.githubusercontent.com/storytold/pdfcraft/a6bc63a497954af87c1cd5a2a1e72e928a11c039/assets/fonts/OFL-Inter.txt", + "sha256": "262481e844521b326f5ecd053e59b98c8b2da78c8ee1bdbb6e8174305e54935a", + "license": "OFL-1.1" + } +] diff --git a/crates/pdf/tests/unicode_writing.rs b/crates/pdf/tests/unicode_writing.rs new file mode 100644 index 00000000..0f362289 --- /dev/null +++ b/crates/pdf/tests/unicode_writing.rs @@ -0,0 +1,139 @@ +//! Opt-in contracts with externally supplied, licensed fonts; see the crate README. +#![cfg(feature = "write")] +#![allow(clippy::unwrap_used, clippy::panic)] + +use backend::geom::Point; +use backend::page::PageSettings; +use backend::text::{GlyphId, KrillaGlyph}; +use craft_pdf::write::{self, Metadata, Writer, backend}; +use hayro_cmap::BfString; +use hayro_interpret::{ + BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, SoftMask, TransformExt, + font::Glyph, +}; +use kurbo::{Affine, BezPath, Rect}; + +#[cfg(craft_pdf_unicode_fixtures)] +const INTER: &[u8] = include_bytes!(concat!(env!("CRAFT_PDF_TEST_FONT_DIR"), "/Inter-Regular.ttf")); +#[cfg(craft_pdf_unicode_fixtures)] +const ARABIC: &[u8] = include_bytes!(concat!(env!("CRAFT_PDF_TEST_FONT_DIR"), "/NotoSansArabic.ttf")); +#[cfg(craft_pdf_unicode_fixtures)] +const THAI: &[u8] = include_bytes!(concat!(env!("CRAFT_PDF_TEST_FONT_DIR"), "/NotoSansThai[wdth,wght].ttf")); + +#[derive(Default)] +struct TextDevice { + text: String, + visible_glyphs: usize, + outlines: Vec, +} + +impl Device<'_> for TextDevice { + fn set_soft_mask(&mut self, _: Option>) {} + fn set_blend_mode(&mut self, _: BlendMode) {} + fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {} + fn push_clip_path(&mut self, _: &ClipPath) {} + fn push_transparency_group(&mut self, _: f32, _: Option>, _: BlendMode) {} + fn draw_glyph(&mut self, glyph: &Glyph<'_>, _: Affine, _: Affine, _: &Paint<'_>, _: &GlyphDrawMode) { + let Glyph::Outline(outline) = glyph else { panic!("expected an embedded outline font") }; + assert_ne!(outline.glyph_id().to_u32(), 0, "reopened page must not draw .notdef"); + assert!(outline.font_data().is_some(), "expected an embedded font"); + let text = match glyph.as_unicode().unwrap() { + BfString::Char(c) => c.to_string(), + BfString::String(s) => s, + }; + if !text.chars().all(char::is_whitespace) { + assert!(!outline.outline().elements().is_empty(), "non-space text must have a visible outline"); + self.visible_glyphs += 1; + self.outlines.push(outline.outline()); + } + self.text.push_str(&text); + } + fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {} + fn pop_clip_path(&mut self) {} + fn pop_transparency_group(&mut self) {} +} + +fn mapped_document(font_bytes: &[u8], text: &str, variations: Option<&[([u8; 4], f32)]>) -> Vec { + let mut face = ttf_parser::Face::parse(font_bytes, 0).unwrap(); + if let Some(coords) = variations { + for (tag, value) in coords { + face.set_variation(ttf_parser::Tag::from_bytes(tag), *value).unwrap(); + } + } + // Deliberately test explicit scalar-to-glyph mappings, not shaping or bidi layout. + // Applications own those operations and supply the final glyph run to the writer. + let glyphs: Vec<_> = text + .char_indices() + .map(|(start, c)| { + let id = face.glyph_index(c).unwrap(); + assert_ne!(id.0, 0, "fixture font must cover {c}"); + let advance = f32::from(face.glyph_hor_advance(id).unwrap()) / f32::from(face.units_per_em()); + KrillaGlyph::new(GlyphId::new(u32::from(id.0)), advance, 0.0, 0.0, 0.0, start..start + c.len_utf8(), None) + }) + .collect(); + let font = write::font(font_bytes, 0, variations).unwrap(); + let settings = backend::SerializeSettings { compress_content_streams: false, ..Default::default() }; + // Do not put the source string in metadata: only real page text may satisfy the assertion. + let mut writer = Writer::new(settings, Metadata::default()); + let mut page = writer.document_mut().start_page_with(PageSettings::from_wh(600.0, 100.0).unwrap()); + let mut surface = page.surface(); + surface.draw_glyphs(Point::from_xy(20.0, 50.0), &glyphs, font, text, 18.0, false); + surface.finish(); + page.finish(); + writer.finish().unwrap() +} + +#[cfg_attr(not(craft_pdf_unicode_fixtures), ignore = "requires hash-verified external fonts and craft_pdf_unicode_fixtures cfg")] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn embedded_unicode_mappings_survive_serialization_and_reopen_as_page_text() { + let [inter, arabic, thai] = fixture_fonts(); + for (font, text) in [(inter, "Café Ελληνικά Русский Tiếng Việt"), (arabic, "اللغة العربية"), (thai, "ภาษาไทย")] + { + let bytes = mapped_document(font, text, None); + assert!(String::from_utf8_lossy(&bytes).contains("/ToUnicode")); + let device = reopened_text(&bytes); + assert_eq!(device.text, text, "saved page text must preserve every requested character"); + assert_eq!(device.visible_glyphs, text.chars().filter(|c| !c.is_whitespace()).count()); + } +} + +fn reopened_text(bytes: &[u8]) -> TextDevice { + let pdf = hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.to_vec())).unwrap(); + let page = &pdf.pages()[0]; + let cache = InterpreterCache::new(); + let mut context = + Context::new(page.initial_transform(true).to_kurbo(), Rect::new(0.0, 0.0, 600.0, 100.0), &cache, page.xref(), InterpreterSettings::default()); + let mut device = TextDevice::default(); + hayro_interpret::interpret_page(page, &mut context, &mut device); + device +} + +#[cfg_attr(not(craft_pdf_unicode_fixtures), ignore = "requires hash-verified external fonts and craft_pdf_unicode_fixtures cfg")] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn variable_coordinates_change_embedded_outlines_and_preserve_real_text() { + let [_, _, thai] = fixture_fonts(); + let text = "ภาษาไทย"; + let default = reopened_text(&mapped_document(thai, text, None)); + let empty = reopened_text(&mapped_document(thai, text, Some(&[]))); + let light = reopened_text(&mapped_document(thai, text, Some(&[(*b"wght", 100.0)]))); + let bold = reopened_text(&mapped_document(thai, text, Some(&[(*b"wght", 900.0)]))); + for device in [&default, &empty, &light, &bold] { + assert_eq!(device.text, text); + assert_eq!(device.visible_glyphs, text.chars().count()); + } + assert_eq!(default.outlines, empty.outlines, "empty coordinates preserve the default instance"); + assert_ne!(light.outlines, bold.outlines, "weight coordinates must affect the embedded glyph outlines"); +} + +fn fixture_fonts() -> [&'static [u8]; 3] { + #[cfg(craft_pdf_unicode_fixtures)] + { + [INTER, ARABIC, THAI] + } + #[cfg(not(craft_pdf_unicode_fixtures))] + { + panic!("enable craft_pdf_unicode_fixtures and supply CRAFT_PDF_TEST_FONT_DIR to run this contract") + } +} diff --git a/crates/pdf/tests/writing.rs b/crates/pdf/tests/writing.rs new file mode 100644 index 00000000..da611b61 --- /dev/null +++ b/crates/pdf/tests/writing.rs @@ -0,0 +1,319 @@ +#![cfg(feature = "write")] +#![allow(clippy::unwrap_used, clippy::panic)] + +use backend::configure::{Archival, PdfVersion}; +use backend::geom::{PathBuilder, Rect, Size}; +use backend::page::PageSettings; +use backend::paint::Fill; +use craft_pdf::write::{self, Metadata, WriteError, Writer, backend}; + +fn document(compress: bool) -> Vec { + let metadata = Metadata { + creator: "Craft writer contract".into(), + producer: "Craft writer contract".into(), + title: Some("Synthetic export".into()), + authors: vec!["Test author".into()], + description: Some("Original synthetic artwork".into()), + keywords: vec!["craft".into(), "pdf".into()], + created: Some(backend::metadata::DateTime::new(2026).month(10).day(9)), + }; + let settings = backend::SerializeSettings { + compress_content_streams: compress, + configuration: write::configuration(PdfVersion::Pdf16, None).unwrap(), + ..Default::default() + }; + let mut writer = Writer::new(settings, metadata); + for (width, height) in [(80.0, 40.0), (30.0, 60.0)] { + let mut page = writer.document_mut().start_page_with(PageSettings::new(Size::from_wh(width, height).unwrap())); + let mut surface = page.surface(); + let mut path = PathBuilder::new(); + path.push_rect(Rect::from_xywh(2.0, 3.0, 15.0, 12.0).unwrap()); + surface.set_fill(Some(Fill { paint: backend::color::rgb::Color::new(20, 80, 160).into(), ..Default::default() })); + surface.draw_path(&path.finish().unwrap()); + surface.finish(); + page.finish(); + } + writer.finish().unwrap() +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn writes_pages_metadata_and_selected_version_without_a_renderer() { + for compress in [false, true] { + let bytes = document(compress); + assert!(bytes.starts_with(b"%PDF-1.6")); + let text = String::from_utf8_lossy(&bytes); + for value in ["Synthetic export", "Test author", "Craft writer contract", "Original synthetic artwork"] { + assert!(text.contains(value), "missing {value}"); + } + let pdf = hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.clone())).unwrap(); + let sizes: Vec<_> = pdf.pages().iter().map(|page| page.render_dimensions()).collect(); + assert_eq!(sizes, vec![(80.0, 40.0), (30.0, 60.0)]); + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn fixed_input_does_not_read_a_clock_or_change_pdf_bytes() { + assert_eq!(document(false), document(false)); + assert_eq!(document(true), document(true)); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn document_language_is_preserved_in_catalog_and_xmp_when_supplied() { + use hayro_syntax::object::{Dict, String as PdfString}; + + for language in [None, Some("es-CR"), Some("ja")] { + let mut writer = Writer::new_with_language(Default::default(), Metadata::default(), language.map(str::to_owned)); + writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()).finish(); + let bytes = writer.finish().unwrap(); + let pdf = hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.clone())).unwrap(); + let catalog = pdf.xref().get::>(pdf.xref().root_id()).unwrap(); + let actual = catalog.get::>(b"Lang"); + assert_eq!(actual.as_ref().map(|value| value.as_bytes()), language.map(str::as_bytes)); + + let xmp = String::from_utf8_lossy(&bytes); + if let Some(language) = language { + assert!(xmp.contains("")); + assert!(xmp.contains(&format!("{language}"))); + } else { + assert!(!xmp.contains("")); + } + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn incompatible_archival_version_is_a_configuration_error() { + assert!(matches!(write::configuration(PdfVersion::Pdf17, Some(Archival::A1_B)), Err(WriteError::Configuration(_)))); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn serialization_preserves_archival_validation_diagnostics() { + let settings = + backend::SerializeSettings { configuration: write::configuration(PdfVersion::Pdf17, Some(Archival::A2_B)).unwrap(), ..Default::default() }; + // Deliberately missing the metadata required by the selected archival standard. + let mut writer = Writer::new(settings, Metadata::default()); + writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()).finish(); + let Err(WriteError::Serialization(error)) = writer.finish() else { panic!("expected archival validation errors") }; + assert!(matches!(*error, backend::error::KrillaError::Validation(_))); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn invalid_fonts_and_variations_are_fallible() { + assert!(matches!(write::font(b"not a font", 0, None), Err(WriteError::InvalidFont))); + for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + assert!(matches!(write::font(&[], 0, Some(&[(*b"wght", value)])), Err(WriteError::FontVariations))); + } +} + +#[derive(Debug, PartialEq, Eq)] +enum AppError { + InvalidPage, + Panic, + Writer, +} + +impl From for AppError { + fn from(error: WriteError) -> Self { + if matches!(error, WriteError::BackendPanic(_)) { Self::Panic } else { Self::Writer } + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn export_guard_preserves_application_errors() { + let result: Result<(), AppError> = write::guard(|| Err(AppError::InvalidPage)); + assert_eq!(result, Err(AppError::InvalidPage)); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn a_native_drawing_panic_is_an_error_and_a_fresh_export_still_works() { + let result: Result<(), AppError> = write::guard(|| { + let mut writer = Writer::new(Default::default(), Metadata::default()); + let _page = writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()); + panic!("synthetic app drawing failure"); + }); + assert_eq!(result, Err(AppError::Panic)); + assert!(!document(false).is_empty()); +} + +#[cfg(feature = "write-pdf")] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn embedded_pages_can_be_written_without_interpretation_or_rasterization() { + let source = backend::pdf::Pdf::new(std::sync::Arc::new(document(false))).unwrap(); + let source = backend::pdf::PdfDocument::new(std::sync::Arc::new(source)); + let mut writer = Writer::new(Default::default(), Metadata::default()); + let size = Size::from_wh(80.0, 40.0).unwrap(); + let mut page = writer.document_mut().start_page_with(PageSettings::new(size)); + let mut surface = page.surface(); + surface.draw_pdf_page(&source, size, 0); + surface.finish(); + page.finish(); + let bytes = writer.finish().unwrap(); + let pdf = hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.clone())).unwrap(); + assert_eq!(pdf.pages().len(), 1); + assert_eq!(pdf.pages()[0].render_dimensions(), (80.0, 40.0)); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn native_unbalanced_surface_cleanup_survives_in_a_subprocess() { + const CHILD: &str = "CRAFT_PDF_SURFACE_PANIC_PROBE"; + if std::env::var_os(CHILD).is_some() { + let result: Result<(), AppError> = write::guard(|| { + let mut writer = Writer::new(Default::default(), Metadata::default()); + let mut page = writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()); + let mut surface = page.surface(); + surface.push_transform(&backend::geom::Transform::from_translate(2.0, 3.0)); + let mut clip = PathBuilder::new(); + clip.push_rect(Rect::from_xywh(0.0, 0.0, 20.0, 10.0).unwrap()); + surface.push_clip_path(&clip.finish().unwrap(), &backend::paint::FillRule::NonZero); + surface.push_isolated(); + surface.start_tagged(backend::tagging::ContentTag::Other); + panic!("synthetic app drawing failure with open surface state"); + }); + assert_eq!(result, Err(AppError::Panic)); + assert!(!document(false).is_empty()); + return; + } + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "native_unbalanced_surface_cleanup_survives_in_a_subprocess", "--nocapture"]) + .env(CHILD, "1") + .output() + .unwrap(); + assert!(output.status.success(), "child failed: {:?}\n{}", output.status, String::from_utf8_lossy(&output.stderr)); +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn native_panic_diagnostics_preserve_bounded_unicode_text() { + let result: Result<(), WriteError> = write::guard(|| panic!("synthetic app export failure")); + assert!(matches!(result, Err(WriteError::BackendPanic(message)) if message == "synthetic app export failure")); + let result: Result<(), WriteError> = write::guard(|| std::panic::panic_any("界".repeat(2048))); + assert!(matches!(result, Err(WriteError::BackendPanic(message)) if message == "界".repeat(1024))); + let result: Result<(), WriteError> = write::guard(|| std::panic::panic_any(7_u32)); + assert!(matches!(result, Err(WriteError::BackendPanic(message)) if message == "non-string panic payload")); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn empty_metadata_stays_absent_and_populated_metadata_stays_present() { + let export = |metadata| { + let mut writer = Writer::new(Default::default(), metadata); + writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()).finish(); + writer.finish().unwrap() + }; + let empty = export(Metadata { title: Some(String::new()), description: Some(String::new()), ..Default::default() }); + assert_eq!(empty, export(Metadata::default())); + let empty = String::from_utf8_lossy(&empty); + for key in [ + "/Title", + "/Author", + "/Keywords", + "/Creator", + "/Producer", + "/Subject", + "/CreationDate", + "", + "", + "", + "", + "", + ] { + assert!(!empty.contains(key), "unexpected empty metadata {key}"); + } + let full = document(false); + let full = String::from_utf8_lossy(&full); + for key in [ + "/Title", + "/Author", + "/Keywords", + "/Creator", + "/Producer", + "/Subject", + "/CreationDate", + "", + "", + "", + "", + "", + ] { + assert!(full.contains(key), "missing populated metadata {key}"); + } + for title in [None, Some(String::new())] { + let settings = backend::SerializeSettings { + configuration: write::configuration(PdfVersion::Pdf17, Some(Archival::A2_B)).unwrap(), + ..Default::default() + }; + let mut writer = Writer::new(settings, Metadata { title, ..Default::default() }); + writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()).finish(); + assert!(matches!(writer.finish(), Err(WriteError::Serialization(_))), "empty title must not bypass archival validation"); + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn retaining_a_writer_after_page_unwind_cannot_serialize_missing_pages() { + for mode in ["page", "surface", "explicit finish"] { + let settings = backend::SerializeSettings { enable_tagging: false, ..Default::default() }; + let mut writer = Writer::new(settings, Metadata::default()); + writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()).finish(); + let result: Result<(), WriteError> = if mode == "page" { + write::guard(|| { + let _page = writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()); + panic!("synthetic failed middle page"); + }) + } else { + // Keep the page outside the narrow guard: successful later page cleanup + // must not clear poison from an unwound or explicitly failed surface. + let mut page = writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()); + let result = write::guard(|| { + let mut surface = page.surface(); + surface.push_transform(&backend::geom::Transform::from_translate(2.0, 3.0)); + if mode == "explicit finish" { + surface.finish(); + return Ok(()); + } + panic!("synthetic failed middle surface"); + }); + page.finish(); + result + }; + assert!(matches!(result, Err(WriteError::BackendPanic(_)))); + writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()).finish(); + assert!( + matches!(writer.finish(), Err(WriteError::Serialization(error)) if matches!(*error, backend::error::KrillaError::Poisoned)), + "a retained failed writer must reject serialization after {mode}" + ); + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn empty_language_is_absent_and_keeps_required_language_validation() { + let mut outputs = Vec::new(); + for language in [None, Some(String::new())] { + let mut writer = Writer::new_with_language(Default::default(), Metadata::default(), language.clone()); + writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()).finish(); + outputs.push(writer.finish().unwrap()); + let settings = backend::SerializeSettings { + configuration: write::configuration(PdfVersion::Pdf17, Some(Archival::A2_A)).unwrap(), + ..Default::default() + }; + let mut writer = Writer::new_with_language(settings, Metadata::default(), language); + writer.document_mut().start_page_with(PageSettings::from_wh(40.0, 20.0).unwrap()).finish(); + let Err(WriteError::Serialization(error)) = writer.finish() else { panic!("expected archival errors") }; + let backend::error::KrillaError::Validation(errors) = *error else { panic!("expected validation diagnostics") }; + assert!( + errors.iter().any(|(error, _)| matches!(error, backend::configure::ValidationError::NoDocumentLanguage)), + "empty language must not bypass archival language validation" + ); + } + assert_eq!(outputs[0], outputs[1], "empty language and absent language must serialize identically"); +} diff --git a/licenses/pdf-writer/LICENSE-ICC.txt b/licenses/pdf-writer/LICENSE-ICC.txt new file mode 100644 index 00000000..550e190e --- /dev/null +++ b/licenses/pdf-writer/LICENSE-ICC.txt @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/ \ No newline at end of file diff --git a/licenses/pdf-writer/LICENSE_APACHE b/licenses/pdf-writer/LICENSE_APACHE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/licenses/pdf-writer/LICENSE_APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/licenses/pdf-writer/LICENSE_MIT b/licenses/pdf-writer/LICENSE_MIT new file mode 100644 index 00000000..63b1625f --- /dev/null +++ b/licenses/pdf-writer/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Laurenz Stampfl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/krilla/Cargo.toml b/vendor/krilla/Cargo.toml new file mode 100644 index 00000000..9895fedc --- /dev/null +++ b/vendor/krilla/Cargo.toml @@ -0,0 +1,165 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.92" +name = "krilla" +version = "0.8.2" +authors = ["Laurenz Stampfl "] +build = false +exclude = ["src/tests"] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A high-level crate for creating PDF files." +readme = "README.md" +keywords = ["pdf"] +categories = [ + "graphics", + "multimedia", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/LaurenzV/krilla" + +[features] +comemo = ["dep:comemo"] +default = [ + "raster-images", + "simple-text", +] +pdf = ["dep:hayro-write"] +raster-images = [ + "dep:png", + "dep:zune-jpeg", + "dep:gif", + "dep:image-webp", + "dep:imagesize", +] +rayon = ["dep:rayon"] +simple-text = ["dep:rustybuzz"] + +[lib] +name = "krilla" +path = "src/lib.rs" + +[[example]] +name = "basic" +path = "examples/basic.rs" + +[[example]] +name = "empty_document" +path = "examples/empty_document.rs" + +[[example]] +name = "parley" +path = "examples/parley.rs" + +[[example]] +name = "simple_text" +path = "examples/simple_text.rs" + +[[example]] +name = "stream_builder" +path = "examples/stream_builder.rs" + +[dependencies.base64] +version = "0.22.1" + +[dependencies.bumpalo] +version = "3.16.0" + +[dependencies.comemo] +version = "0.5.0" +optional = true + +[dependencies.flate2] +version = "1.1.0" + +[dependencies.float-cmp] +version = "0.9.0" + +[dependencies.gif] +version = "0.14.2" +optional = true + +[dependencies.hayro-write] +version = "0.7.0" +optional = true + +[dependencies.image-webp] +version = "0.2.1" +optional = true + +[dependencies.imagesize] +version = "0.14.0" +optional = true + +[dependencies.indexmap] +version = "2.10.0" + +[dependencies.pdf-writer] +version = "0.15.0" + +[dependencies.png] +version = "0.18.1" +optional = true + +[dependencies.rayon] +version = "1.10.0" +optional = true + +[dependencies.rustc-hash] +version = "2.1.1" + +[dependencies.rustybuzz] +version = "0.20.1" +optional = true + +[dependencies.siphasher] +version = "1.0.1" + +[dependencies.skrifa] +version = "0.42.0" + +[dependencies.smallvec] +version = "1.15.1" +features = [ + "union", + "const_new", + "write", +] + +[dependencies.subsetter] +version = "0.2.6" +features = ["variable-fonts"] + +[dependencies.tiny-skia-path] +version = "0.12.0" + +[dependencies.xmp-writer] +version = "0.3.3" + +[dependencies.yoke] +version = "0.8.0" +features = ["derive"] + +[dependencies.zune-jpeg] +version = "0.5.5" +optional = true + +[dev-dependencies.parley] +version = "0.8.0" + +[dev-dependencies.pretty_assertions] +version = "1.4.1" diff --git a/vendor/krilla/README.craft.md b/vendor/krilla/README.craft.md new file mode 100644 index 00000000..c2e6e7cc --- /dev/null +++ b/vendor/krilla/README.craft.md @@ -0,0 +1,28 @@ +# Local Krilla 0.8.2 patch + +This is the crates.io Krilla 0.8.2 package (MIT OR Apache-2.0), upstream commit +`3ffdf0588cf98050aad6edba51ca70162e1fb5b5`. `SOURCE.json` records each retained +upstream file hash and the patched hash. Registry bookkeeping files, +`Cargo.lock` and the redundant original manifest are omitted. Upstream MIT/Apache licence texts are retained under `../../licenses/pdf-writer`; +the registry package itself omits those texts. Its examples and four CC0 ICC +profiles are retained without changes. + +The patch changes two destructors: `Surface::drop` and `Page::drop` return early +when `std::thread::panicking()` is true. Drawing can panic with an unfinished +transform, clip, isolated group or tagged section. Finalizing that state during +unwind can panic a second time and abort before an outer catch can recover. +Discarding the unfinished state permits native unwind recovery. Cleanup marks the +serialization context poisoned until it completes successfully, preserving any prior +poison. Document::finish returns KrillaError::Poisoned after failed cleanup, including +explicit surface finalization, even if a caller retains the writer after a narrow +catch. A caller must still discard other captured app state after a panic. Normal +finalization, including checks for unbalanced state, remains unchanged. + +Public marker constants, `CRAFT_UNWIND_SAFE_DROP` and `CRAFT_UNWIND_POISONING`, let craft-pdf reject an accidentally +unpatched dependency at compile time. Consumers must copy the Krilla root patch +from the craft-pdf README because Cargo ignores dependency workspaces' patches. + +The shared writer's native subprocess regression covers the previously aborting +cleanup and a successful fresh export afterward. Native and WASM export tests +cover the unchanged successful path. WASM aborts, aborting panic configurations +and process-wide allocation failures remain outside unwind recovery. diff --git a/vendor/krilla/README.md b/vendor/krilla/README.md new file mode 100644 index 00000000..62fccf5a --- /dev/null +++ b/vendor/krilla/README.md @@ -0,0 +1,156 @@ +# krilla + +[![Crates.io](https://img.shields.io/crates/v/krilla.svg)](https://crates.io/crates/krilla) +[![Documentation](https://docs.rs/krilla/badge.svg)](https://docs.rs/krilla) + + + +A high-level, ergonomic Rust library creating PDF documents. + +[krilla] is a high-level Rust crate that allows for the creation of PDF files. It builds +on top of the [pdf-writer] crate, but abstracts away all complexities that are +involved in creating a PDF file, instead providing an interface with high-level primitives, such +as fills, strokes, gradient, glyphs and images which can be used and combined easily +without having to worry about low-level details. + +To get started, take a look at the [`document`] module that explains how you can create +a document using krilla. + +## Example + +The following example shows some of the features of krilla in action. + +The example creates a PDF file with two pages. On the first page, +we add two small pieces of text, and on the second page we draw a triangle +with a gradient fill. + +For more examples, feel free to take a look at the [examples] directory of the GitHub repository. + +```rust +use std::path::{self, PathBuf}; + +use krilla::color::rgb; +use krilla::text::Font; +use krilla::geom::{Point, PathBuilder}; +use krilla::paint::{SpreadMethod, LinearGradient, Stop, FillRule}; +use krilla::text::TextDirection; +use krilla::paint::Fill; +use krilla::Document; +use krilla::page::PageSettings; +use krilla::num::NormalizedF32; + +// Create a new document. +let mut document = Document::new(); +// Load a font. +let font = { + let path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/fonts/NotoSans-Regular.ttf"); + let data = std::fs::read(&path).unwrap(); + Font::new(data.into(), 0).unwrap() +}; + +// Add a new page with dimensions 200x200. +let mut page = document.start_page_with(PageSettings::from_wh(200.0, 200.0).unwrap()); +// Get the surface of the page. +let mut surface = page.surface(); +// Draw some text. +surface.draw_text( + Point::from_xy(0.0, 25.0), + font.clone(), + 14.0, + "This text has font size 14!", + false, + TextDirection::Auto +); + +surface.set_fill(Some(Fill { + paint: rgb::Color::new(255, 0, 0).into(), + opacity: NormalizedF32::new(0.5).unwrap(), + rule: Default::default(), +})); +// Draw some more text, in a different color with an opacity and bigger font size. +surface.draw_text( + Point::from_xy(0.0, 50.0), + font.clone(), + 16.0, + "This text has font size 16!", + false, + TextDirection::Auto +); + +// Finish the page. +surface.finish(); +page.finish(); + +// Start a new page. +let mut page = document.start_page_with(PageSettings::from_wh(200.0, 200.0).unwrap()); +// Create the triangle. +let triangle = { + let mut pb = PathBuilder::new(); + pb.move_to(100.0, 20.0); + pb.line_to(160.0, 160.0); + pb.line_to(40.0, 160.0); + pb.close(); + + pb.finish().unwrap() +}; + +// Create the linear gradient. +let lg = LinearGradient { + x1: 60.0, + y1: 0.0, + x2: 140.0, + y2: 0.0, + transform: Default::default(), + spread_method: SpreadMethod::Repeat, + stops: vec![ + Stop { + offset: NormalizedF32::new(0.2).unwrap(), + color: rgb::Color::new(255, 0, 0).into(), + opacity: NormalizedF32::ONE, + }, + Stop { + offset: NormalizedF32::new(0.8).unwrap(), + color: rgb::Color::new(255, 255, 0).into(), + opacity: NormalizedF32::ONE, + }, + ], + anti_alias: false, +}; +let mut surface = page.surface(); + +// Set the fill. +surface.set_fill(Some(Fill { + paint: lg.into(), + rule: FillRule::EvenOdd, + opacity: NormalizedF32::ONE +})); + +// Fill the path. +surface.draw_path(&triangle); + +// Finish up and write the resulting PDF. +surface.finish(); +page.finish(); + +let pdf = document.finish().unwrap(); +let path = path::absolute("basic.pdf").unwrap(); +eprintln!("Saved PDF to '{}'", path.display()); + +// Write the PDF to a file. +std::fs::write(path, &pdf).unwrap(); +``` + +## Minimum Supported Rust Version (MSRV) + +The minimum supported Rust version is **1.92**. + +[krilla]: https://github.com/LaurenzV/krilla +[pdf-writer]: https://github.com/typst/pdf-writer +[examples]: https://github.com/LaurenzV/krilla/tree/main/crates/krilla/examples +[`document`]: https://docs.rs/krilla/latest/krilla/document/ + + + +## License +This crate is dual-licensed under the MIT and Apache 2.0 licenses. diff --git a/vendor/krilla/SOURCE.json b/vendor/krilla/SOURCE.json new file mode 100644 index 00000000..3b01eaf6 --- /dev/null +++ b/vendor/krilla/SOURCE.json @@ -0,0 +1,384 @@ +{ + "crate": "krilla", + "version": "0.8.2", + "source": "https://crates.io/api/v1/crates/krilla/0.8.2/download", + "upstream_commit": "3ffdf0588cf98050aad6edba51ca70162e1fb5b5", + "files": [ + { + "path": "Cargo.toml", + "upstream_sha256": "edc07e480c43e421bf098371c01aba2ab66a2c97e0ec0bf03d513eabc074fa87", + "sha256": "edc07e480c43e421bf098371c01aba2ab66a2c97e0ec0bf03d513eabc074fa87" + }, + { + "path": "README.md", + "upstream_sha256": "399b3e1b3b7759ae5a1ad19b89004e5faa8b97c12534ca2248ba27b1f5649a98", + "sha256": "399b3e1b3b7759ae5a1ad19b89004e5faa8b97c12534ca2248ba27b1f5649a98" + }, + { + "path": "examples/basic.rs", + "upstream_sha256": "d675eb0ddfe9c99f7b1969b9dec0b6bf0b7828c2c35cd475577fa6e04476530f", + "sha256": "d675eb0ddfe9c99f7b1969b9dec0b6bf0b7828c2c35cd475577fa6e04476530f" + }, + { + "path": "examples/empty_document.rs", + "upstream_sha256": "60488167a3cd7009876d14226010388d0fde62e41342b17e0ac3e57a0c191605", + "sha256": "60488167a3cd7009876d14226010388d0fde62e41342b17e0ac3e57a0c191605" + }, + { + "path": "examples/parley.rs", + "upstream_sha256": "49ff160a05b5f920ea8a5bb703982cf26d0fe18af5ae206bb24fd933e56a8a1b", + "sha256": "49ff160a05b5f920ea8a5bb703982cf26d0fe18af5ae206bb24fd933e56a8a1b" + }, + { + "path": "examples/simple_text.rs", + "upstream_sha256": "20cc5ebc39f9f35daec3e20d4b760ab9a421e7a7b12851843cac1cd18937f173", + "sha256": "20cc5ebc39f9f35daec3e20d4b760ab9a421e7a7b12851843cac1cd18937f173" + }, + { + "path": "examples/stream_builder.rs", + "upstream_sha256": "f3b174c1aa63596d7c16a85052c9af3cf59a1bf987958d19759711a2645b0989", + "sha256": "f3b174c1aa63596d7c16a85052c9af3cf59a1bf987958d19759711a2645b0989" + }, + { + "path": "icc/LICENSE", + "upstream_sha256": "469d87b5fa48f8aaf69e25f4fcac3a309c97dbb7d561b917638abdb640e004d9", + "sha256": "469d87b5fa48f8aaf69e25f4fcac3a309c97dbb7d561b917638abdb640e004d9" + }, + { + "path": "icc/README.md", + "upstream_sha256": "e5c937572fbbd1cd2717892956db7c3582ec4385373131fa06f631b1258e1844", + "sha256": "e5c937572fbbd1cd2717892956db7c3582ec4385373131fa06f631b1258e1844" + }, + { + "path": "icc/sGrey-v2-magic.icc", + "upstream_sha256": "4110169287e5f1b5149ca9f1da1814d32943905bba8030e849d1a3b729a19481", + "sha256": "4110169287e5f1b5149ca9f1da1814d32943905bba8030e849d1a3b729a19481" + }, + { + "path": "icc/sGrey-v4.icc", + "upstream_sha256": "00c0f94e09127520a17dc0e1d9264b5702081d96dbdb1549ee88e3631ce42a9d", + "sha256": "00c0f94e09127520a17dc0e1d9264b5702081d96dbdb1549ee88e3631ce42a9d" + }, + { + "path": "icc/sRGB-v2-magic.icc", + "upstream_sha256": "af4efe28f6d311799865f325ba39184a2e978b113ca74124d60af7de22b105f4", + "sha256": "af4efe28f6d311799865f325ba39184a2e978b113ca74124d60af7de22b105f4" + }, + { + "path": "icc/sRGB-v4.icc", + "upstream_sha256": "c56e1685d888f5edb92fe07f2750f387f8fe8e91b32ff8fb0b56bfbbb9458353", + "sha256": "c56e1685d888f5edb92fe07f2750f387f8fe8e91b32ff8fb0b56bfbbb9458353" + }, + { + "path": "src/chunk_container.rs", + "upstream_sha256": "7317162f659d7c756ee170146d360e2c757cef953dc5a2a62f32daea90fc0c3f", + "sha256": "7317162f659d7c756ee170146d360e2c757cef953dc5a2a62f32daea90fc0c3f" + }, + { + "path": "src/configure/PDF_A1.md", + "upstream_sha256": "e6ea127125ec0984f3fcd57f9886fc960aa90c9f751dd59cd61fe9cbc2cdb903", + "sha256": "e6ea127125ec0984f3fcd57f9886fc960aa90c9f751dd59cd61fe9cbc2cdb903" + }, + { + "path": "src/configure/PDF_A2.md", + "upstream_sha256": "47c87d3b29aff6e87703ba6f6021a2cfa88b4faf7f2c35101719b8b9f1bb9705", + "sha256": "47c87d3b29aff6e87703ba6f6021a2cfa88b4faf7f2c35101719b8b9f1bb9705" + }, + { + "path": "src/configure/PDF_A3.md", + "upstream_sha256": "24c5ee9df0666fb208e96d08924372facb15c8492eab25c07c639e5f87974ce8", + "sha256": "24c5ee9df0666fb208e96d08924372facb15c8492eab25c07c639e5f87974ce8" + }, + { + "path": "src/configure/PDF_A4.md", + "upstream_sha256": "27710cf95e6ef83714cfa372e09796c9ec0d5455ceb31c101a7dccfdd8ee1b5b", + "sha256": "27710cf95e6ef83714cfa372e09796c9ec0d5455ceb31c101a7dccfdd8ee1b5b" + }, + { + "path": "src/configure/PDF_UA1.md", + "upstream_sha256": "f3932749a56db38eb302e05b59b6a18312b9eda2e473b2b6e99645a29d3dc670", + "sha256": "f3932749a56db38eb302e05b59b6a18312b9eda2e473b2b6e99645a29d3dc670" + }, + { + "path": "src/configure/README.md", + "upstream_sha256": "b16e444c68eb7cdc33e07119acc3eac9664bdb418859597a2d10d84f179c421c", + "sha256": "b16e444c68eb7cdc33e07119acc3eac9664bdb418859597a2d10d84f179c421c" + }, + { + "path": "src/configure/mod.rs", + "upstream_sha256": "940d285eb0df625c458da4693125c23d6d3f3ec810ffb2407a94609a32d8efea", + "sha256": "940d285eb0df625c458da4693125c23d6d3f3ec810ffb2407a94609a32d8efea" + }, + { + "path": "src/configure/validate.rs", + "upstream_sha256": "7e2a0ac6d7ecaac1ed2158cdc9e94411c2499c98e541ffb471f2bb06d5069c33", + "sha256": "7e2a0ac6d7ecaac1ed2158cdc9e94411c2499c98e541ffb471f2bb06d5069c33" + }, + { + "path": "src/configure/version.rs", + "upstream_sha256": "a5635080803f8efcfd04d803cb1c26162b21ef316e4b329f9892e33bf98ca378", + "sha256": "a5635080803f8efcfd04d803cb1c26162b21ef316e4b329f9892e33bf98ca378" + }, + { + "path": "src/content.rs", + "upstream_sha256": "9e59b2c77b73b36caedacfe80894d1cce7fd6fe6f7438ea24fda2019391a980c", + "sha256": "9e59b2c77b73b36caedacfe80894d1cce7fd6fe6f7438ea24fda2019391a980c" + }, + { + "path": "src/data.rs", + "upstream_sha256": "76fe20faa43a098519325ec01d0f6952a066ada0a775275cc6c232d45055d2d2", + "sha256": "76fe20faa43a098519325ec01d0f6952a066ada0a775275cc6c232d45055d2d2" + }, + { + "path": "src/document.rs", + "upstream_sha256": "61d2ad0df23da4b5207d492f175d7e18da0e755fa0e16410436b5d64db6e428b", + "sha256": "871645ae9a9b87eb5faa958c877b36dc2a335d631ba037bf35cd0f275d5f5dd3" + }, + { + "path": "src/error.rs", + "upstream_sha256": "656b260bd99d250535099f17f2db8e6e6135e72c74fb6dff304371db9f646be5", + "sha256": "340791663a19e56a421968aaab1e127b69fdf7e3b91647e2807efb88c393f376" + }, + { + "path": "src/geom.rs", + "upstream_sha256": "36078aec49332870797e6a929f977a82fc6bea937627d0598c35dd6e98ece41e", + "sha256": "36078aec49332870797e6a929f977a82fc6bea937627d0598c35dd6e98ece41e" + }, + { + "path": "src/graphics/blend.rs", + "upstream_sha256": "f2629743d9e2ea1440763231f3d4cc914224ca238d88fa0630b7c7e9628b58dd", + "sha256": "f2629743d9e2ea1440763231f3d4cc914224ca238d88fa0630b7c7e9628b58dd" + }, + { + "path": "src/graphics/color.rs", + "upstream_sha256": "e5843d33ef184ab5c458025d1b1dc53df6ee6aaf085595ea14604d080b98f8a4", + "sha256": "e5843d33ef184ab5c458025d1b1dc53df6ee6aaf085595ea14604d080b98f8a4" + }, + { + "path": "src/graphics/graphic.rs", + "upstream_sha256": "1fb321782c09f135de69cfd43c0a5c54f7edf749c4a3720930813f3f58e6bb0f", + "sha256": "1fb321782c09f135de69cfd43c0a5c54f7edf749c4a3720930813f3f58e6bb0f" + }, + { + "path": "src/graphics/graphics_state.rs", + "upstream_sha256": "77537e134e8d2ed3602730d2b3637421e9dbc6ac615085d45c680b1e6d5eb5bb", + "sha256": "77537e134e8d2ed3602730d2b3637421e9dbc6ac615085d45c680b1e6d5eb5bb" + }, + { + "path": "src/graphics/icc.rs", + "upstream_sha256": "b078282bbe072f85118aabfb1c469a1a480cbc4dc5effba6c5cb6ac8c202f1b8", + "sha256": "b078282bbe072f85118aabfb1c469a1a480cbc4dc5effba6c5cb6ac8c202f1b8" + }, + { + "path": "src/graphics/image.rs", + "upstream_sha256": "61324d6426ac79aa5322d0d33a95532934cd1e41797a9dad6302210a2b799d5d", + "sha256": "61324d6426ac79aa5322d0d33a95532934cd1e41797a9dad6302210a2b799d5d" + }, + { + "path": "src/graphics/mask.rs", + "upstream_sha256": "d1dbc7d4d28388ee43c43294bfcbeec79136a65ac2816bdaf19eac4b88669661", + "sha256": "d1dbc7d4d28388ee43c43294bfcbeec79136a65ac2816bdaf19eac4b88669661" + }, + { + "path": "src/graphics/mod.rs", + "upstream_sha256": "9f82d56c24f2fd51736e68b9c182cae90617816d00e814102f45996705b13224", + "sha256": "9f82d56c24f2fd51736e68b9c182cae90617816d00e814102f45996705b13224" + }, + { + "path": "src/graphics/paint.rs", + "upstream_sha256": "c3de4b7056f3f952009f39f0e0d0311bc9af54807660f9e46f67c177572edf45", + "sha256": "c3de4b7056f3f952009f39f0e0d0311bc9af54807660f9e46f67c177572edf45" + }, + { + "path": "src/graphics/separation.rs", + "upstream_sha256": "8c2f506da3c232ea45fb06929f264f29159b25ff2dd47e43062ed403dc289b32", + "sha256": "8c2f506da3c232ea45fb06929f264f29159b25ff2dd47e43062ed403dc289b32" + }, + { + "path": "src/graphics/shading_function.rs", + "upstream_sha256": "0e0fc8bb1a32272457ef560fa80933972d0f5ef5fb7046424c6de48093cfd978", + "sha256": "0e0fc8bb1a32272457ef560fa80933972d0f5ef5fb7046424c6de48093cfd978" + }, + { + "path": "src/graphics/shading_pattern.rs", + "upstream_sha256": "bace5d192085e955e433bf2e5614fc77f2f121413f7d5b058a1b711ba1e21f54", + "sha256": "bace5d192085e955e433bf2e5614fc77f2f121413f7d5b058a1b711ba1e21f54" + }, + { + "path": "src/graphics/tiling_pattern.rs", + "upstream_sha256": "e38ed5243b4b3d2d4fc23c623c6de9c7b813cceb580ec1ea4d0a5b449433d2f6", + "sha256": "e38ed5243b4b3d2d4fc23c623c6de9c7b813cceb580ec1ea4d0a5b449433d2f6" + }, + { + "path": "src/graphics/xobject.rs", + "upstream_sha256": "4f6db246a626d39367660e1b089bc83a828fe97ba6e69d5527bbede876ae4a66", + "sha256": "4f6db246a626d39367660e1b089bc83a828fe97ba6e69d5527bbede876ae4a66" + }, + { + "path": "src/interactive/action.rs", + "upstream_sha256": "f01eaa605d70fca85bd3964b26671e106dd1d9fec6d4e89feca716deb185bda3", + "sha256": "f01eaa605d70fca85bd3964b26671e106dd1d9fec6d4e89feca716deb185bda3" + }, + { + "path": "src/interactive/annotation.rs", + "upstream_sha256": "45b01acfef124a5ab9b6799ab4c83734f929b3573fdb28c218b108e220afad39", + "sha256": "45b01acfef124a5ab9b6799ab4c83734f929b3573fdb28c218b108e220afad39" + }, + { + "path": "src/interactive/destination.rs", + "upstream_sha256": "fbe7ce81ce195dbc86502e8555268c026a7c795cfe8a94a975e23cab16b657c3", + "sha256": "fbe7ce81ce195dbc86502e8555268c026a7c795cfe8a94a975e23cab16b657c3" + }, + { + "path": "src/interactive/mod.rs", + "upstream_sha256": "d6810b138d7d6db4b55add15da79e8d2401f129925986af52bbf1189622c484b", + "sha256": "d6810b138d7d6db4b55add15da79e8d2401f129925986af52bbf1189622c484b" + }, + { + "path": "src/interchange/embed.rs", + "upstream_sha256": "20a69cb06a81649d7e1905f6f32ea67306a993e47a20d6c7321116fd56b86158", + "sha256": "20a69cb06a81649d7e1905f6f32ea67306a993e47a20d6c7321116fd56b86158" + }, + { + "path": "src/interchange/metadata.rs", + "upstream_sha256": "14fab986f2436a4275e7cca13e065763b14b938ae180632294455ad0199380ec", + "sha256": "14fab986f2436a4275e7cca13e065763b14b938ae180632294455ad0199380ec" + }, + { + "path": "src/interchange/mod.rs", + "upstream_sha256": "3164a9bdaa048d30fe4bd732a0ce100266e7b35fa3499845390358bed8d99b00", + "sha256": "3164a9bdaa048d30fe4bd732a0ce100266e7b35fa3499845390358bed8d99b00" + }, + { + "path": "src/interchange/outline.rs", + "upstream_sha256": "7e33d6166a8e6fff75843a6cfc8e5bdc22b3943e65661f189ed06ec95f8dc030", + "sha256": "7e33d6166a8e6fff75843a6cfc8e5bdc22b3943e65661f189ed06ec95f8dc030" + }, + { + "path": "src/interchange/tagging/fmt.rs", + "upstream_sha256": "81dba7794077e93340c2e3d14e78457c0fd66b47b98a3291b458a4792e31ca54", + "sha256": "81dba7794077e93340c2e3d14e78457c0fd66b47b98a3291b458a4792e31ca54" + }, + { + "path": "src/interchange/tagging/generate.toml", + "upstream_sha256": "3e72b8e92997cd9d33c5d894c9abf3e6ffe73675b06a4896130c69efbab18557", + "sha256": "3e72b8e92997cd9d33c5d894c9abf3e6ffe73675b06a4896130c69efbab18557" + }, + { + "path": "src/interchange/tagging/generated.rs", + "upstream_sha256": "5aa6865ce77ddf6209163f88be21aaf3d5c83ba95ab89edeff6ccfe9a27c24d9", + "sha256": "5aa6865ce77ddf6209163f88be21aaf3d5c83ba95ab89edeff6ccfe9a27c24d9" + }, + { + "path": "src/interchange/tagging/mod.rs", + "upstream_sha256": "3ca36fab92b870d0b96a74601192269bb248d3880a99ea6cca740347bebed4ea", + "sha256": "3ca36fab92b870d0b96a74601192269bb248d3880a99ea6cca740347bebed4ea" + }, + { + "path": "src/interchange/tagging/tag.rs", + "upstream_sha256": "d2777f89eeca52893eb55c079bb460369a80be0d6c923b4d7a994bc1111c9be7", + "sha256": "d2777f89eeca52893eb55c079bb460369a80be0d6c923b4d7a994bc1111c9be7" + }, + { + "path": "src/lib.rs", + "upstream_sha256": "7533b14f1cef6749c961a7c1ba95d3274d6584e60febfccc03b9d63b4dc2abeb", + "sha256": "8da1abd3331575d5d67ce276aabc492ad00fbb3bfa5901867c0ab78e230751d9" + }, + { + "path": "src/num.rs", + "upstream_sha256": "79778f8254067a04d954fdfd25ff73d208a8a22a8cc68608c660cb93bf303483", + "sha256": "79778f8254067a04d954fdfd25ff73d208a8a22a8cc68608c660cb93bf303483" + }, + { + "path": "src/page.rs", + "upstream_sha256": "ef85a771013896b185c9393414175ee91e5541c9a3fad4760c161764088ac177", + "sha256": "9684b5e0256c212be77b992687cb3774030f8e7c0c49a26ad520f5c81a1b8532" + }, + { + "path": "src/pdf.rs", + "upstream_sha256": "de10d523ecf07ec9c9d17a17f5616786cb179168d0356d8182ffcf047b473db1", + "sha256": "de10d523ecf07ec9c9d17a17f5616786cb179168d0356d8182ffcf047b473db1" + }, + { + "path": "src/resource.rs", + "upstream_sha256": "56a062eb37e5da88a78a129c6391103caddf7a2c7ca9fcfdb577319412c97107", + "sha256": "56a062eb37e5da88a78a129c6391103caddf7a2c7ca9fcfdb577319412c97107" + }, + { + "path": "src/serialize.rs", + "upstream_sha256": "4f2c439dbd20006eea112e056b2f1a5ba98a1ede9227a45f66a6df46fb1a07e8", + "sha256": "f0b1d921badbd8f2e8b006b39d3f6522f672fede176f512c6c0739f832872244" + }, + { + "path": "src/stream.rs", + "upstream_sha256": "a999960d2ad0ee48c24711793da86c3a33d8c0f80ac4d53c61efdbc65af2ca22", + "sha256": "a999960d2ad0ee48c24711793da86c3a33d8c0f80ac4d53c61efdbc65af2ca22" + }, + { + "path": "src/surface.rs", + "upstream_sha256": "94de0a0384fc69c9785c6595b3f2c7ed8319635c901b8dc12894e252ab3ef8da", + "sha256": "936dfeb978add9d4443658c209f2d72034583a7771179587d8135ab870ded905" + }, + { + "path": "src/text/cid.rs", + "upstream_sha256": "5332282160b75e2a24345b546ead219476768829bef881562f5658dd50ce97f2", + "sha256": "5332282160b75e2a24345b546ead219476768829bef881562f5658dd50ce97f2" + }, + { + "path": "src/text/font.rs", + "upstream_sha256": "c58ca2cac24f6ca310b2a2ee6b359c10f299f6a5a6db461ee1aed6234b5a418b", + "sha256": "c58ca2cac24f6ca310b2a2ee6b359c10f299f6a5a6db461ee1aed6234b5a418b" + }, + { + "path": "src/text/glyph/bitmap.rs", + "upstream_sha256": "d22d396d31193adac8a7f684c7d8812030b107c7ebabb6d79e0c2ae3a21c8edc", + "sha256": "d22d396d31193adac8a7f684c7d8812030b107c7ebabb6d79e0c2ae3a21c8edc" + }, + { + "path": "src/text/glyph/colr.rs", + "upstream_sha256": "cde9bb33f7b87fe026aa9abe24dc375d8fbdce01a5d9aa6764f93fe6febef676", + "sha256": "cde9bb33f7b87fe026aa9abe24dc375d8fbdce01a5d9aa6764f93fe6febef676" + }, + { + "path": "src/text/glyph/mod.rs", + "upstream_sha256": "e500dccad0e6a254bc8cc911cfce111dae787b263f73f0ee50f5b1763c00b109", + "sha256": "e500dccad0e6a254bc8cc911cfce111dae787b263f73f0ee50f5b1763c00b109" + }, + { + "path": "src/text/glyph/outline.rs", + "upstream_sha256": "0739d195e81d1af69fd9be1a43cf39aea4cd84b1378c3d01f2bd9f9ace05d813", + "sha256": "0739d195e81d1af69fd9be1a43cf39aea4cd84b1378c3d01f2bd9f9ace05d813" + }, + { + "path": "src/text/glyph/svg.rs", + "upstream_sha256": "67d0bc7e67db7685de44810fb4a0b6eca73b45a2d0a1b3aed88630bab3787894", + "sha256": "67d0bc7e67db7685de44810fb4a0b6eca73b45a2d0a1b3aed88630bab3787894" + }, + { + "path": "src/text/group.rs", + "upstream_sha256": "28b6b8d98c214d033b6732f1554408032c6f1fb218500570de9e243ade4d5f05", + "sha256": "28b6b8d98c214d033b6732f1554408032c6f1fb218500570de9e243ade4d5f05" + }, + { + "path": "src/text/mod.rs", + "upstream_sha256": "68fd8668f1a0a738f3c29ca8dd2d23c500a356503062bbfec003384abaceec1b", + "sha256": "68fd8668f1a0a738f3c29ca8dd2d23c500a356503062bbfec003384abaceec1b" + }, + { + "path": "src/text/shape.rs", + "upstream_sha256": "bf32f98ddfc792f35c4cb78a719c834cb04cedd9068f33e8340e9a50c17079f2", + "sha256": "bf32f98ddfc792f35c4cb78a719c834cb04cedd9068f33e8340e9a50c17079f2" + }, + { + "path": "src/text/type3.rs", + "upstream_sha256": "7ac9b8529369a55fa8e9eb2163909bac36212b057eec72710ea8b7a2455e9bd7", + "sha256": "7ac9b8529369a55fa8e9eb2163909bac36212b057eec72710ea8b7a2455e9bd7" + }, + { + "path": "src/util.rs", + "upstream_sha256": "3aed339d6a22a31e963022f891b7d9a25babe7fd442dae91932115540373d8d0", + "sha256": "3aed339d6a22a31e963022f891b7d9a25babe7fd442dae91932115540373d8d0" + } + ], + "crate_sha256": "27da593198b20eeba65caeb73c2bbeec3e53ab08fa549898312ce81c4fce5e33" +} diff --git a/vendor/krilla/examples/basic.rs b/vendor/krilla/examples/basic.rs new file mode 100644 index 00000000..b2ccb937 --- /dev/null +++ b/vendor/krilla/examples/basic.rs @@ -0,0 +1,117 @@ +//! A basic example of a PDF file created with krilla. + +use std::path; +use std::path::PathBuf; + +use krilla::color::rgb; +use krilla::geom::{PathBuilder, Point}; +use krilla::num::NormalizedF32; +use krilla::page::PageSettings; +use krilla::paint::Fill; +use krilla::paint::{FillRule, LinearGradient, SpreadMethod, Stop}; +use krilla::text::Font; +use krilla::text::TextDirection; +use krilla::Document; + +fn main() { + // Create a new document. + let mut document = Document::new(); + // Load a font. + let font = { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../assets/fonts/NotoSans-Regular.ttf"); + let data = std::fs::read(&path).unwrap(); + Font::new(data.into(), 0).unwrap() + }; + + // Add a new page with dimensions 200x200. + let mut page = document.start_page_with(PageSettings::from_wh(200.0, 200.0).unwrap()); + // Get the surface of the page. + let mut surface = page.surface(); + // Draw some text. + surface.draw_text( + Point::from_xy(0.0, 25.0), + font.clone(), + 14.0, + "This text has font size 14!", + false, + TextDirection::Auto, + ); + + surface.set_fill(Some(Fill { + paint: rgb::Color::new(255, 0, 0).into(), + opacity: NormalizedF32::new(0.5).unwrap(), + rule: Default::default(), + })); + // Draw some more text, in a different color with an opacity and bigger font size. + surface.draw_text( + Point::from_xy(0.0, 50.0), + font.clone(), + 16.0, + "This text has font size 16!", + false, + TextDirection::Auto, + ); + + // Finish the page. + surface.finish(); + page.finish(); + + // Start a new page. + let mut page = document.start_page_with(PageSettings::from_wh(200.0, 200.0).unwrap()); + // Create the triangle. + let triangle = { + let mut pb = PathBuilder::new(); + pb.move_to(100.0, 20.0); + pb.line_to(160.0, 160.0); + pb.line_to(40.0, 160.0); + pb.close(); + + pb.finish().unwrap() + }; + + // Create the linear gradient. + let lg = LinearGradient { + x1: 60.0, + y1: 0.0, + x2: 140.0, + y2: 0.0, + transform: Default::default(), + spread_method: SpreadMethod::Repeat, + stops: vec![ + Stop { + offset: NormalizedF32::new(0.2).unwrap(), + color: rgb::Color::new(255, 0, 0).into(), + opacity: NormalizedF32::ONE, + }, + Stop { + offset: NormalizedF32::new(0.8).unwrap(), + color: rgb::Color::new(255, 255, 0).into(), + opacity: NormalizedF32::ONE, + }, + ], + anti_alias: false, + }; + let mut surface = page.surface(); + + // Set the fill. + surface.set_fill(Some(Fill { + paint: lg.into(), + rule: FillRule::EvenOdd, + opacity: NormalizedF32::ONE, + })); + + // Fill the path. + surface.draw_path(&triangle); + + // Finish up and write the resulting PDF. + surface.finish(); + page.finish(); + + let pdf = document.finish().unwrap(); + let path = path::absolute("basic.pdf").unwrap(); + eprintln!("Saved PDF to '{}'", path.display()); + + // Write the PDF to a file. + std::fs::write(path, &pdf).unwrap(); +} diff --git a/vendor/krilla/examples/empty_document.rs b/vendor/krilla/examples/empty_document.rs new file mode 100644 index 00000000..45f71490 --- /dev/null +++ b/vendor/krilla/examples/empty_document.rs @@ -0,0 +1,25 @@ +//! This example contains the most basic document you can create: A document +//! with a single empty page. + +use std::path; + +use krilla::page::PageSettings; +use krilla::Document; + +fn main() { + // First, we create a new document. This represents a single PDF document. + let mut document = Document::new(); + // We can now successively add new pages by calling `start_page`, or `start_page_with` + // if we want to pass custom page settings. + let page = document.start_page_with(PageSettings::from_wh(300.0, 600.0).unwrap()); + page.finish(); + + // Create the PDF + let pdf = document.finish().unwrap(); + + let path = path::absolute("empty_document.pdf").unwrap(); + eprintln!("Saved PDF to '{}'", path.display()); + + // Write the PDF to a file. + std::fs::write(path, &pdf).unwrap(); +} diff --git a/vendor/krilla/examples/parley.rs b/vendor/krilla/examples/parley.rs new file mode 100644 index 00000000..47e51b83 --- /dev/null +++ b/vendor/krilla/examples/parley.rs @@ -0,0 +1,174 @@ +//! This example shows how to use parley to create advanced layouted text. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::path; + +use krilla::color::rgb; +use krilla::geom::Point; +use krilla::num::NormalizedF32; +use krilla::page::PageSettings; +use krilla::paint::Fill; +use krilla::text::Font; +use krilla::text::{GlyphId, KrillaGlyph}; +use krilla::Document; +use parley::layout::Alignment; +use parley::style::{FontFamily, FontFamilyName, FontWeight, LineHeight, StyleProperty}; +use parley::{FontContext, LayoutContext}; + +fn main() { + // The text that we want to insert into the PDF. + let text = String::from( + "This is a long text. We want it to not be wider than 200pt, \ + so that it fits on the page. Let's intersperse some emojis 💩👻💀emojis🦩🌚😁😆\ + as well as complex scripts: हैलो वर्ल्ड and مرحبا بالعالم", + ); + + // Set up the properties of the text. See the documentation of parley for more information + // on how you can configure its behavior. + let max_advance = Some(200.0); + let text_color = rgb::Color::new(0, 0, 0); + let mut font_cx = FontContext::default(); + let mut layout_cx = LayoutContext::new(); + let mut builder = layout_cx.ranged_builder(&mut font_cx, &text, 1.0, false); + let brush_style = StyleProperty::Brush(text_color); + builder.push_default(brush_style); + + let font_stack = FontFamily::List(Cow::Borrowed(&[ + FontFamilyName::Named(Cow::Borrowed("Noto Sans")), + FontFamilyName::Named(Cow::Borrowed("Noto Sans Arabic")), + FontFamilyName::Named(Cow::Borrowed("Noto Sans Devanagari")), + FontFamilyName::Named(Cow::Borrowed("Noto Color Emoji")), + ])); + let font_stack_style = StyleProperty::FontFamily(font_stack); + builder.push_default(font_stack_style); + builder.push_default(StyleProperty::LineHeight(LineHeight::FontSizeRelative(1.3))); + builder.push_default(StyleProperty::FontSize(16.0)); + + // In our case, we set the first four characters to bold and also make some + // part of the text red. + let bold = FontWeight::new(600.0); + let bold_style = StyleProperty::FontWeight(bold); + builder.push(bold_style, 0..4); + + let color_style = StyleProperty::Brush(rgb::Color::new(255, 0, 0)); + builder.push(color_style, 2..12); + + let mut layout = builder.build(&text); + layout.break_all_lines(max_advance); + layout.align(max_advance, Alignment::Start, Default::default()); + + // After setting up everything, now starts the actual part where we use krilla to write + // the text to a PDF. + // We need to set up a font cache that converts from parley fonts to krilla fonts. + let mut font_cache = HashMap::new(); + + // The usual page setup. + let mut document = Document::new(); + let mut page = document.start_page_with(PageSettings::from_wh(200.0, 300.0).unwrap()); + let mut surface = page.surface(); + + for line in layout.lines() { + let y = line.metrics().baseline; + let mut x = 0.0; + for run in line.runs() { + let mut cur_x = x; + let font = run.font().clone(); + let (font_data, id) = font.data.into_raw_parts(); + // Get the krilla font. + let krilla_font = font_cache + .entry(id) + .or_insert_with(|| Font::new(font_data.into(), font.index).unwrap()); + let font_size = run.font_size(); + + // This is part is somewhat convoluted, the reason being that each glyph might + // have a different style than the previous one. So we always need to keep track + // of the current style, and whenever we encounter a new style we "flush" all + // current glyphs into the PDF, and build the next sequence of consecutive + // glyphs. + let mut cur_style = None; + let mut glyphs = Vec::::new(); + + for cluster in run.visual_clusters() { + if cluster.is_ligature_continuation() { + if let Some(glyph) = glyphs.last_mut() { + glyph.text_range.end = cluster.text_range().end; + } + // Ligature continuations have no glyphs of their own. + continue; + } + + for glyph in cluster.glyphs() { + let glyph_style = glyph.style_index; + + if let Some(style) = cur_style { + if style != glyph_style { + // If style doesn't match, flush all glyphs up to now. + cur_style = Some(glyph_style); + let style = layout.styles()[style as usize].brush; + surface.set_fill(Some(Fill { + paint: style.into(), + opacity: NormalizedF32::ONE, + rule: Default::default(), + })); + + surface.draw_glyphs( + Point { x: cur_x, y }, + &glyphs, + krilla_font.clone(), + &text, + font_size, + false, + ); + glyphs.clear(); + cur_x = x; + } + } else { + cur_style = Some(glyph_style); + } + + // Add the current glyph to our buffer of glyphs. + glyphs.push(KrillaGlyph::new( + GlyphId::new(glyph.id as u32), + glyph.advance / font_size, + glyph.x / font_size, + glyph.y / font_size, + 0.0, + cluster.text_range(), + None, + )); + // And make sure keep track of the current x position. + x += glyph.advance; + } + } + + // Flush all remaining glyphs, if existing. + if !glyphs.is_empty() { + surface.set_fill(Some(Fill { + paint: layout.styles()[cur_style.unwrap() as usize].brush.into(), + opacity: NormalizedF32::ONE, + rule: Default::default(), + })); + surface.draw_glyphs( + Point::from_xy(cur_x, y), + &glyphs, + krilla_font.clone(), + &text, + font_size, + false, + ); + } + } + } + + surface.finish(); + page.finish(); + + let pdf = document.finish().unwrap(); + + let path = path::absolute("parley.pdf").unwrap(); + eprintln!("Saved PDF to '{}'", path.display()); + + // Write the PDF to a file. + std::fs::write(path, &pdf).unwrap(); +} diff --git a/vendor/krilla/examples/simple_text.rs b/vendor/krilla/examples/simple_text.rs new file mode 100644 index 00000000..8ca44392 --- /dev/null +++ b/vendor/krilla/examples/simple_text.rs @@ -0,0 +1,118 @@ +//! This example shows how to use `simple_text` API to draw simple text in a single line. It +//! also demonstrates how you can use non-default variation coordinates. +//! +//! Note that simple text in this case does not mean that complex scripts aren't supported +//! (they are, including RTL text!), but the text itself must not contain mixed scripts. And +//! the font must contain all necessary glyphs, otherwise the `.notdef` glyph will be emitted +//! instead of font fallback. + +use std::path; +use std::path::PathBuf; +use std::sync::{Arc, LazyLock}; + +use krilla::color::rgb; +use krilla::geom::Point; +use krilla::num::NormalizedF32; +use krilla::page::PageSettings; +use krilla::paint::{Fill, LinearGradient, SpreadMethod, Stop, Stroke}; +use krilla::text::{Font, TextDirection}; +use krilla::Document; +pub(crate) static WORKSPACE_PATH: LazyLock = + LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../")); + +pub(crate) static ASSETS_PATH: LazyLock = LazyLock::new(|| WORKSPACE_PATH.join("assets")); + +fn main() { + // The usual page setup. + let mut document = Document::new(); + let mut page = document.start_page_with(PageSettings::from_wh(600.0, 280.0).unwrap()); + let mut surface = page.surface(); + + let noto_font = Font::new( + Arc::new(std::fs::read(ASSETS_PATH.join("fonts/NotoSans-Regular.ttf")).unwrap()).into(), + 0, + ) + .unwrap(); + + let gradient = LinearGradient { + x1: 30.0, + y1: 0.0, + x2: 50.0, + y2: 0.0, + transform: Default::default(), + spread_method: SpreadMethod::Reflect, + stops: vec![ + Stop { + offset: NormalizedF32::new(0.2).unwrap(), + color: rgb::Color::new(255, 0, 0).into(), + opacity: NormalizedF32::ONE, + }, + Stop { + offset: NormalizedF32::new(0.8).unwrap(), + color: rgb::Color::new(255, 255, 0).into(), + opacity: NormalizedF32::ONE, + }, + ], + anti_alias: true, + }; + + surface.set_fill(Some(Fill { + paint: gradient.into(), + opacity: NormalizedF32::new(0.5).unwrap(), + rule: Default::default(), + })); + + // Let's first write some red-colored text with some English text. + surface.draw_text( + Point::from_xy(0.0, 25.0), + noto_font.clone(), + 25.0, + "z͈̤̭͖̉͑́a̳ͫ́̇͑̽͒ͯlͨ͗̍̀̍̔̀ģ͔̫̫̄o̗̠͔͆̏̓͢", + false, + TextDirection::Auto, + ); + + surface.set_fill(None); + surface.set_stroke(Some(Stroke { + paint: rgb::Color::new(0, 255, 0).into(), + ..Default::default() + })); + // Instead of applying fills, we can also apply strokes! + surface.draw_text( + Point::from_xy(0.0, 50.0), + noto_font.clone(), + 25.0, + "This text is stroked green!", + false, + TextDirection::Auto, + ); + + let noto_arabic_font = Font::new( + Arc::new(std::fs::read(ASSETS_PATH.join("fonts/NotoSansArabic-Regular.ttf")).unwrap()) + .into(), + 0, + ) + .unwrap(); + + surface.set_fill(None); + // As mentioned above, complex scripts are supported, you just can't mix them in + // one run. + surface.draw_text( + Point::from_xy(0.0, 75.0), + noto_arabic_font.clone(), + 25.0, + "هذا هو السطر الثاني من النص.", + false, + TextDirection::Auto, + ); + + surface.finish(); + page.finish(); + let pdf = document.finish().unwrap(); + + let path = path::absolute("simple_text.pdf").unwrap(); + eprintln!("Saved PDF to '{}'", path.display()); + + // Write the PDF to a file. + std::fs::write(path, &pdf).unwrap(); +} diff --git a/vendor/krilla/examples/stream_builder.rs b/vendor/krilla/examples/stream_builder.rs new file mode 100644 index 00000000..1fe75c12 --- /dev/null +++ b/vendor/krilla/examples/stream_builder.rs @@ -0,0 +1,93 @@ +//! This example introduces the concept of a `StreamBuilder`, which allows you to +//! define graphics content on a separate context than the main page surface. This is necessary +//! when defining patterns or mask. + +use std::path; + +use krilla::color::rgb; +use krilla::geom::{Path, PathBuilder, Rect, Transform}; +use krilla::page::PageSettings; +use krilla::paint::{Fill, Pattern, Stroke}; +use krilla::Document; + +fn main() { + let mut document = Document::new(); + let mut page = document.start_page_with(PageSettings::from_wh(200.0, 200.0).unwrap()); + let mut surface = page.surface(); + + // We want to define a pattern with a red rectangle on the top-left and a + // blue rectangle on the bottom-right. Then we want to apply this pattern to + // a rectangle that is rotated by 45 degrees in the center. + + // First, let's define the pattern by creating a new stream builder. + let mut stream_builder = surface.stream_builder(); + let mut pattern_surface = stream_builder.surface(); + + pattern_surface.set_fill(Some(Fill { + paint: rgb::Color::new(255, 0, 0).into(), + ..Default::default() + })); + // Draw the top-left rectangle. + pattern_surface.draw_path(&rect_to_path(0.0, 0.0, 10.0, 10.0)); + + pattern_surface.set_fill(Some(Fill { + paint: rgb::Color::new(0, 0, 255).into(), + ..Default::default() + })); + // Draw the bottom-right rectangle. + pattern_surface.draw_path(&rect_to_path(10.0, 10.0, 20.0, 20.0)); + pattern_surface.finish(); + + // Get the pattern stream + let pattern_stream = stream_builder.finish(); + + // Define the actual pattern + let pattern = Pattern { + stream: pattern_stream, + transform: Default::default(), + width: 20.0, + height: 20.0, + }; + + // Now we draw the actual transformed rectangle. + // First, push a transform so that the rectangle will be rotated. + surface.push_transform(&Transform::from_rotate_at(45.0, 100.0, 100.0)); + + let rect_path = rect_to_path(30.0, 30.0, 170.0, 170.0); + + surface.set_fill(Some(Fill { + paint: pattern.into(), + ..Default::default() + })); + // Draw the rectangle. + surface.draw_path(&rect_path); + + surface.set_fill(None); + surface.set_stroke(Some(Stroke { + paint: rgb::Color::black().into(), + ..Default::default() + })); + // Let's also add a stroke, makes it look a bit nicer. + surface.draw_path(&rect_path); + + // Don't forget to pop! Each `push_` method must have a corresponding pop. + surface.pop(); + + surface.finish(); + page.finish(); + + let pdf = document.finish().unwrap(); + + let path = path::absolute("stream_builder.pdf").unwrap(); + eprintln!("Saved PDF to '{}'", path.display()); + + // Write the PDF to a file. + std::fs::write(path, &pdf).unwrap(); +} + +// A simple convenience function that allow us to generate rectangle paths. +pub fn rect_to_path(x1: f32, y1: f32, x2: f32, y2: f32) -> Path { + let mut builder = PathBuilder::new(); + builder.push_rect(Rect::from_ltrb(x1, y1, x2, y2).unwrap()); + builder.finish().unwrap() +} diff --git a/vendor/krilla/icc/LICENSE b/vendor/krilla/icc/LICENSE new file mode 100644 index 00000000..550e190e --- /dev/null +++ b/vendor/krilla/icc/LICENSE @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/ \ No newline at end of file diff --git a/vendor/krilla/icc/README.md b/vendor/krilla/icc/README.md new file mode 100644 index 00000000..bd045f36 --- /dev/null +++ b/vendor/krilla/icc/README.md @@ -0,0 +1 @@ +ICC profiles are from: https://github.com/saucecontrol/Compact-ICC-Profiles \ No newline at end of file diff --git a/vendor/krilla/icc/sGrey-v2-magic.icc b/vendor/krilla/icc/sGrey-v2-magic.icc new file mode 100644 index 0000000000000000000000000000000000000000..1ea54310ec58bfcf955e1a5f9fb701c49500979a GIT binary patch literal 616 zcmZQzV9Llz&MjsVU|`72D=Bgha*T|Kj8b5K#K6oT!obPE#~_=STwLHA>=wcR1jUKv z#mOZ_IUqIye7nZL2;yDV%}C5k={=?M-A{Y(l?yN1t<2Rvb0gGYWwE5B7AFH`wg7Q_ zazRlEkiG!KJmnYP{9r($>}q)UDB5rGL**%qYlsipfng74u?? zV^#{*H8%I{TF@Pp6^;jWPkQTt+A;(QXM z6W=EvPhFPYnOT?}pBtF(Rp?giRT@|xS6NuyQM;`EMB|4Rxwe3g&aR`qoc)0l=TH7T z&3ne`*?e;==09H)z2w^Ru$8yhq_6wFv44x!wlh0R_9*VVda(D1&vAj1x6Z6SUw=wcR1jUKv z#mOZ_IUqIye7nZL2;yDV%}C5k*}W@?WB0NR{3ok2x2G?Adk~=xD~mNHwKy3la{!1{ zk_(DTfb=_tX@BaV)S&e~# zeF6i+-%0=f|6ch2|KCLn49s_cPA@D-EJ_5cWd@Q=X_*WRpO!N)@J2E)2wz}e;EDzS D*d9sj literal 0 HcmV?d00001 diff --git a/vendor/krilla/icc/sRGB-v2-magic.icc b/vendor/krilla/icc/sRGB-v2-magic.icc new file mode 100644 index 0000000000000000000000000000000000000000..b29f729dbff1d6eae55aa62418f79b42b1fce9e8 GIT binary patch literal 736 zcmZQzV0w^~oLkH!z`&53S5g$@?xYYA8KuDfh=G|wgn^TRk3lvuxwybL*e!$s2#OQS zi<3)=azJbZ_;!tf5yZQ!n~|88GI^89O1=$BT{Hg9TE95Gb2dU9Ru*SUYH>19<^vGN zCl?fzFfcIk0NFg{B?Tb11duIK1auUTtpj4GL)Z==b`pdQa+^p|NRTs7O$HEGq$9DD zkl0YWfXW$Ii-Dd6F<_u1wE}1#1B0`(0Rt!uAmX1xzU%w5_dnW^f#4UuF3Q!$?D+9x}Bgv&jWf1ciwgG7w1`CD|h608jh7}A)7#=b*Fe)*+ zG3GK(Wjx6Efk}ZWl&PEP2s0zIJ#!QDF%~|SNS5U+KUsZPm$3e0i(%WxuE^fS{)Ho% z<0_{o=V2~;uKnCj+$VWLcpmbW@Nx1j;&e!5TP-eaZJj{f8ogEe_YB31 zf{dq_+%!`$FSa;lrC?oSbKlO@euJZ;(=-Q*XRMyhH@9N`^F`51t}PE+d3#Ozy6+qNw`grUv$JH6;=Zc~dyn`W v7dUzA%f&j?%JgIr1!9bb=h-Dax8G;zx8JuvaVFRmiW^iUOU|;}+ z7myDGh;S%QGd2PXG4j|mFtFbJ|NpZZ0|Wa628O?r{{R2I@c;k6ix?P~?=UbNC`9Pb zU%lYv3v7D7w~sE)stfnnQ`g2bXkuszH`k|`~df#K6~1_s_p M1_t2^3=CY+0OR6cu>b%7 literal 0 HcmV?d00001 diff --git a/vendor/krilla/src/chunk_container.rs b/vendor/krilla/src/chunk_container.rs new file mode 100644 index 00000000..0530dd2f --- /dev/null +++ b/vendor/krilla/src/chunk_container.rs @@ -0,0 +1,489 @@ +use pdf_writer::{Chunk, Finish, Name, Pdf, Ref, Str, TextStr}; +use std::collections::HashMap; +use std::sync::OnceLock; +use xmp_writer::{RenditionClass, XmpWriter}; + +use crate::configure::{PdfVersion, ValidationError}; +use crate::error::KrillaResult; +use crate::interchange::metadata::Metadata; +use crate::metadata::PageLayout; +use crate::serialize::SerializeContext; +use crate::util::{stable_hash_base64, Deferred}; + +type DChunk = Deferred; + +/// Collects all chunks that we create while building +/// the PDF and then writes them out in an orderly manner. +pub(crate) struct ChunkContainer { + pub(crate) streams: StreamChunks, + pub(crate) mixed: MixedChunks, + pub(crate) metadata: Option, + pub(crate) non_stream: NonStreamChunks, +} + +pub(crate) struct StreamChunks { + pub(crate) fonts: Vec, + pub(crate) shading_functions: Vec, + pub(crate) patterns: Vec, + pub(crate) pages: Vec, + pub(crate) embedded_files: Vec, + pub(crate) icc_profiles: Vec, + pub(crate) x_objects: Vec, + pub(crate) images: Vec>>, +} + +pub(crate) struct MixedChunks { + pub(crate) embedded_pdfs: Vec>>, +} + +pub(crate) struct NonStreamChunks { + pub(crate) page_tree: Option<(Ref, Chunk)>, + pub(crate) outline: Option<(Ref, Chunk)>, + pub(crate) page_label_tree: Option<(Ref, Chunk)>, + pub(crate) destination_profiles: Option<(Ref, Chunk)>, + pub(crate) struct_tree_root: Option<(Ref, Chunk)>, + pub(crate) struct_elements: Option, + pub(crate) page_labels: Chunk, + pub(crate) annotations: Chunk, + pub(crate) color_spaces: Chunk, + pub(crate) destinations: Chunk, + pub(crate) ext_g_states: Chunk, + pub(crate) resource_dictionaries: Chunk, + pub(crate) masks: Chunk, + pub(crate) fonts: Chunk, + pub(crate) shading_functions: Chunk, + pub(crate) patterns: Chunk, + pub(crate) pages: Chunk, + pub(crate) embedded_files: Chunk, +} + +impl ChunkContainer { + pub(crate) fn new(sc: &SerializeContext) -> Self { + Self { + streams: StreamChunks { + fonts: vec![], + shading_functions: vec![], + patterns: vec![], + pages: vec![], + embedded_files: vec![], + icc_profiles: vec![], + x_objects: vec![], + images: vec![], + }, + mixed: MixedChunks { + embedded_pdfs: vec![], + }, + metadata: None, + non_stream: NonStreamChunks { + page_tree: None, + outline: None, + page_label_tree: None, + destination_profiles: None, + struct_tree_root: None, + struct_elements: None, + page_labels: sc.new_chunk(), + annotations: sc.new_chunk(), + color_spaces: sc.new_chunk(), + destinations: sc.new_chunk(), + ext_g_states: sc.new_chunk(), + resource_dictionaries: sc.new_chunk(), + masks: sc.new_chunk(), + fonts: sc.new_chunk(), + shading_functions: sc.new_chunk(), + patterns: sc.new_chunk(), + pages: sc.new_chunk(), + embedded_files: sc.new_chunk(), + }, + } + } + + pub(crate) fn finish(self, sc: &mut SerializeContext) -> KrillaResult { + let mut remapped_ref = Ref::new(1); + let mut remapper = HashMap::new(); + + // Allows us to estimate the capacity we will need for the new PDF. + let mut chunks_byte_len = 0; + + // This traverses the chunks in the order that we will write them to the PDF and assigns new + // references as we go. This gives us the advantage that the PDF will be numbered with + // monotonically increasing numbers, which, while it is not a strict requirement for a valid + // PDF, makes it a lot cleaner and might make implementing features like object streams + // easier down the road. + // + // It also allows us to estimate the capacity we will need for the new PDF. + self.visit(sc, &mut |chunk| { + for object_ref in chunk.refs() { + let existing = remapper.insert(object_ref, remapped_ref.bump()); + debug_assert!(existing.is_none()); + } + chunks_byte_len += chunk.len(); + })?; + + // Chunk length is not an exact number because the length might change as we renumber, + // so we add a bit of a padding by multiplying with 1.1. The 200 is additional padding + // for the document catalog. This hopefully allows us to avoid re-alloactions in the general + // case, and thus give us better performance. + let capacity = (chunks_byte_len as f32 * 1.1 + 200.0) as usize; + let mut pdf = sc.new_pdf_with_capacity(capacity); + sc.serialize_settings().pdf_version().set_version(&mut pdf); + + if sc.serialize_settings().ascii_compatible + && !sc + .serialize_settings() + .validators() + .requires_binary_header() + { + pdf.set_binary_marker(b"AAAA") + } + + // Write the chunks in all the fields. + self.visit(sc, &mut |chunk| { + chunk.renumber_into(&mut pdf, |old| remapper[&old]); + })?; + + let missing_title = self.metadata.as_ref().is_none_or(|m| m.title.is_none()); + + if missing_title { + sc.register_validation_error(ValidationError::NoDocumentTitle); + } + + // Write the PDF document info metadata. + if let Some(metadata) = &self.metadata { + metadata.serialize_document_info( + &mut remapped_ref, + &mut pdf, + sc.serialize_settings().configuration, + ); + } + + let instance_id = stable_hash_base64(pdf.as_bytes()); + + let document_id = if let Some(metadata) = &self.metadata { + if let Some(document_id) = &metadata.document_id { + stable_hash_base64(&(sc.serialize_settings().pdf_version().as_str(), document_id)) + } else if metadata.title.is_some() && metadata.authors.is_some() { + stable_hash_base64(&( + sc.serialize_settings().pdf_version().as_str(), + &metadata.title, + &metadata.authors, + )) + } else { + instance_id.clone() + } + } else { + instance_id.clone() + }; + + let mut xmp = XmpWriter::new(); + if let Some(metadata) = &self.metadata { + metadata.serialize_xmp_metadata(&mut xmp, sc, &instance_id); + } + + let settings = sc.serialize_settings(); + let validators = settings.validators(); + validators.write_xmp(&mut xmp); + + xmp.num_pages(sc.page_infos().len() as u32); + xmp.format("application/pdf"); + xmp.instance_id(&instance_id); + xmp.document_id(&document_id); + pdf.set_file_id(( + document_id.as_bytes().to_vec(), + instance_id.as_bytes().to_vec(), + )); + + xmp.rendition_class(RenditionClass::Proof); + sc.serialize_settings().pdf_version().write_xmp(&mut xmp); + + let named_destinations = sc.global_objects.named_destinations.take(); + let embedded_files = sc.global_objects.embedded_files.take(); + + // We only write a catalog if a page tree exists. Every valid PDF must have one + // and krilla ensures that there always is one, but for snapshot tests, it can be + // useful to not write a document catalog if we don't actually need it for the test. + if self.non_stream.page_tree.is_some() + || self.non_stream.outline.is_some() + || self.non_stream.page_label_tree.is_some() + || self.non_stream.destination_profiles.is_some() + || self.non_stream.struct_tree_root.is_some() + { + let meta_ref = if sc.serialize_settings().xmp_metadata { + let meta_ref = remapped_ref.bump(); + let xmp_buf = xmp.finish(None); + pdf.stream(meta_ref, xmp_buf.as_bytes()) + .pair(Name(b"Type"), Name(b"Metadata")) + .pair(Name(b"Subtype"), Name(b"XML")); + Some(meta_ref) + } else { + None + }; + + let catalog_ref = remapped_ref.bump(); + + let mut catalog = pdf.catalog(catalog_ref); + + if let Some(pt) = &self.non_stream.page_tree { + catalog.pages(remapper[&pt.0]); + } + + if let Some(meta_ref) = meta_ref { + catalog.metadata(meta_ref); + } + + if let Some(pl) = &self.non_stream.page_label_tree { + catalog.pair(Name(b"PageLabels"), remapper[&pl.0]); + } + + if let Some(oi) = &self.non_stream.destination_profiles { + catalog.pair(Name(b"OutputIntents"), remapper[&oi.0]); + } + + if let Some(lang) = self.metadata.as_ref().and_then(|m| m.language.as_ref()) { + catalog.lang(TextStr(lang)); + } else { + sc.register_validation_error(ValidationError::NoDocumentLanguage); + } + + if let Some(st) = &self.non_stream.struct_tree_root { + catalog.pair(Name(b"StructTreeRoot"), remapper[&st.0]); + let mut mark_info = catalog.mark_info(); + mark_info.marked(true); + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf16 + && sc.serialize_settings().pdf_version() < PdfVersion::Pdf20 + { + // We always set suspects to false because it's required by PDF/UA. + mark_info.suspects(false); + } + mark_info.finish(); + } + + let write_doc_title = sc + .serialize_settings() + .validators() + .requires_display_doc_title(); + let text_direction = self.metadata.as_ref().and_then(|m| m.text_direction); + + if write_doc_title || text_direction.is_some() { + let mut vp = catalog.viewer_preferences(); + + if write_doc_title { + vp.display_doc_title(true); + } + + if let Some(dir) = text_direction { + vp.direction(dir.to_pdf()); + } + } + + let page_layout = self.metadata.as_ref().and_then(|m| m.page_layout); + if let Some(layout) = page_layout { + // TwoPageLeft and TwoPageRight are only available PDF 1.5+ + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf15 + || !matches!(layout, PageLayout::TwoPageLeft | PageLayout::TwoPageRight) + { + catalog.page_layout(layout.to_pdf()); + } + } + + if let Some(ol) = &self.non_stream.outline { + catalog.outlines(remapper[&ol.0]); + } + + let settings = sc.serialize_settings(); + let validators = settings.validators(); + let write_embedded_files = self.non_stream.embedded_files.len() != 0 + || validators.requires_embedded_files_when_empty(); + + if !named_destinations.is_empty() || write_embedded_files { + // Cannot use pdf-writer API here because it requires Ref's, while + // we write our destinations directly into the array. + let mut names = catalog.names(); + + if !named_destinations.is_empty() { + let mut dest_name_tree = names.destinations(); + let mut dest_name_entries = dest_name_tree.names(); + + // "The Names entries in the leaf (or root) nodes shall + // contain the tree’s keys and their associated values, + // arranged in key-value pairs and shall be sorted lexically + // in ascending order by key. Shorter keys shall appear + // before longer ones beginning with the same byte sequence. + // Any encoding of the keys may be used as long as it is + // self-consistent; keys shall be compared for equality on + // a simple byte-by-byte basis." + let mut sorted = named_destinations.into_iter().collect::>(); + // Note that named destinations are guaranteed to be unique, + // hence just comparing by the name is enough. + sorted.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes())); + + for (name, (dest_ref, _)) in sorted { + dest_name_entries.insert(Str(name.as_bytes()), remapper[&dest_ref]); + } + + dest_name_entries.finish(); + dest_name_tree.finish(); + } + + if write_embedded_files { + let mut embedded_files_name_tree = names.embedded_files(); + let mut embedded_name_entries = embedded_files_name_tree.names(); + + for (name, _ref) in &embedded_files { + embedded_name_entries.insert(Str(name.as_bytes()), remapper[_ref]); + } + } + } + + if !embedded_files.is_empty() && settings.supports_associated_files() { + let mut associated_files = catalog.insert(Name(b"AF")).array().typed(); + for _ref in embedded_files.values() { + associated_files.item(remapper[_ref]).finish(); + } + } + + catalog.finish(); + } + + Ok(pdf) + } +} + +pub(crate) struct EmbeddedPdfChunk { + pub(crate) original_chunk: Chunk, + pub(crate) root_ref_mappings: HashMap, + pub(crate) new_chunk: OnceLock, +} + +/// Visits all chunks in a type. +trait Visit { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()>; +} + +impl Visit for EmbeddedPdfChunk { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + // Now, we have a chunk that contains everything we need to fully embed the PDF, including + // the pages we wanted to extract into, as well as all their dependencies. The + // problem is: during the document creation, we already assigned references to the + // pages (stored in `SerializerContex::page_infos`), but `hayro_write` created new references + // for those (stored in `result.root_refs`). + + // Because of this, embedded PDF chunks will be renumbered twice: First, we preprocess the + // chunk such that page/XObjects are reassigned their original references from the serialize + // context, and all other objects are assigned new, unique references provided by the + // serialize context. Then, we renumber them once again by treating them like any other chunk. + + // Since we are calling `visit` twice, we also cache the renumbered chunk. + + let renumbered = self.new_chunk.get_or_init(|| { + let mut remapper = self.root_ref_mappings.clone(); + + self.original_chunk + .renumber(|old| *remapper.entry(old).or_insert_with(|| sc.new_ref())) + }); + + renumbered.visit(sc, f) + } +} + +impl Visit for ChunkContainer { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + self.non_stream.visit(sc, f)?; + self.mixed.visit(sc, f)?; + self.streams.visit(sc, f)?; + Ok(()) + } +} + +impl Visit for StreamChunks { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + self.fonts.visit(sc, f)?; + self.shading_functions.visit(sc, f)?; + self.patterns.visit(sc, f)?; + self.pages.visit(sc, f)?; + self.embedded_files.visit(sc, f)?; + self.icc_profiles.visit(sc, f)?; + self.x_objects.visit(sc, f)?; + self.images.visit(sc, f)?; + + Ok(()) + } +} + +impl Visit for MixedChunks { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + self.embedded_pdfs.visit(sc, f)?; + + Ok(()) + } +} + +impl Visit for NonStreamChunks { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + self.page_tree.visit(sc, f)?; + self.outline.visit(sc, f)?; + self.page_label_tree.visit(sc, f)?; + self.destination_profiles.visit(sc, f)?; + self.struct_tree_root.visit(sc, f)?; + self.struct_elements.visit(sc, f)?; + self.page_labels.visit(sc, f)?; + self.annotations.visit(sc, f)?; + self.color_spaces.visit(sc, f)?; + self.destinations.visit(sc, f)?; + self.ext_g_states.visit(sc, f)?; + self.resource_dictionaries.visit(sc, f)?; + self.masks.visit(sc, f)?; + self.fonts.visit(sc, f)?; + self.shading_functions.visit(sc, f)?; + self.patterns.visit(sc, f)?; + self.pages.visit(sc, f)?; + self.embedded_files.visit(sc, f)?; + + Ok(()) + } +} + +impl Visit for Chunk { + fn visit(&self, _: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + f(self); + Ok(()) + } +} + +impl Visit for Option { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + if let Some(chunk) = self { + chunk.visit(sc, f)?; + } + Ok(()) + } +} + +impl Visit for Option<(Ref, Chunk)> { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + if let Some((_, chunk)) = self { + chunk.visit(sc, f)?; + } + Ok(()) + } +} + +impl Visit for Deferred { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + self.wait().visit(sc, f) + } +} + +impl Visit for KrillaResult { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + self.as_ref().map_err(|e| e.clone())?.visit(sc, f) + } +} + +impl Visit for Vec { + fn visit(&self, sc: &mut SerializeContext, f: &mut impl FnMut(&Chunk)) -> KrillaResult<()> { + for field in self { + field.visit(sc, f)?; + } + Ok(()) + } +} diff --git a/vendor/krilla/src/configure/PDF_A1.md b/vendor/krilla/src/configure/PDF_A1.md new file mode 100644 index 00000000..2ade3274 --- /dev/null +++ b/vendor/krilla/src/configure/PDF_A1.md @@ -0,0 +1,227 @@ +# Description +PDF/A-1 requires PDF version <= 1.4 and defines two conformance levels, +in the following order from less strict to more strict: +- Level B +- Level A + +Level B is a subset of level A. + +See `README.md` for the meaning of each color. + +# Level B + +## 6.1 File structure + +6.1.2: `pdf-writer` always write the file header as described in the spec. Even if the +user enables `ascii_compatible`, we still write a binary header marker. 🟢 + +6.1.3: +- We always set the file ID. 🟢 +- We do not support encryption. 🔵 +- We do not support linearization. 🔵 + +6.1.4: `pdf-writer` always write the xref section as described in the spec. 🟢 + +6.1.5: krilla ensures that the document information dictionary is consistent with +the XMP metadata. 🟢 + +6.1.6: `pdf-writer` always writes hex strings +with an even number of characters and without whitespaces. 🟢 + +6.1.7: +- `pdf-writer` always write streams as described in the spec. 🟢 +- krilla does never write streams referencing external files. 🔵 +- krilla does never use `LZWDecode` or `Crypt`. 🔵 +- krilla never writes F, FFilter or FDecodeParams in streams. 🔵 + +6.1.8: `pdf-writer` always writes indirect objects as described. 🟢 + +6.1.9: krilla doesn't support linearization. 🔵 + +6.1.10: krilla doesn't support `LZWDecode`. 🔵 + +6.1.11: krilla forbids embedded files for this export mode. 🟢 + +6.1.12: +- `pdf-writer` uses i32 for integers. 🟢 +- krilla checks the that the maximum/minimum float number has not been exceeded. 🟢 +- krilla does not round close real numbers to 0. 🟠 +- krilla does not write at most 5 decimal places for real numbers. 🟠 +- krilla always uses the `new_str` and `new_text_str` methods of the SerializeContext to create them, + which returns a validation error in case it's too long. 🔵 +- krilla trims the names of fonts, and all other names cannot be longer than 127. 🔵 +- krilla does check the maximum length of arrays. 🟢 +- krilla does check the maximum entries of a dictionary. 🟢 +- krilla fails export if more than 8388607 indirect objects exist. 🟢 +- krilla fails export if a higher nesting-level than 28 exists. 🔵 +- krilla does not use the DeviceN color space. 🟢 +- krilla only uses u16 for CIDs. 🟢 + +6.1.13: krilla does not support optional content. 🔵 + +## 6.2 Graphics + +6.2.2: +- krilla always writes a valid output intent. 🟢 +- krilla always only writes one output intent. 🟢 + +6.2.3.1: krilla overrides the `no_device_cs` property if PDF/A is selected, and +in case CMYK is used but no profile was provided, export fails. 🟢 + +6.2.3.2: +- sRGB/sGrey ICC profiles conform to ICC v2 specification. 🟢 + +6.2.4.3: +- krilla always uses sRGB for RGB, and in addition also embeds an sRGB output intent. 🟢 +- krilla always uses sGrey (except for encoding the alpha channel in images, where DeviceGray + is required), and in addition always embeds an output intent. 🟢 +- krilla always uses an CMYK ICC profile, and always sets CMYK as the output intent. 🟢 + It fails export if no CMYK ICC profile was provided. 🟢 + +6.2.3.4: +- krilla ensures the Alternate space in Separation color spaces obeys the restrictions + in the applicable clauses 🟢 +- krilla does not support DeviceN color spaces. 🔵 + +6.2.4: +- krilla does not use the `Alternates`/`Intent` keys for images. 🔵 +- krilla does check whether the `Interpolate` key is used. 🟢 + +6.2.5: krilla does not use the `OPI`/`Subtype2`/`PS` keys for XObjects. 🔵 + +6.2.6: krilla does not use reference XObjects. 🔵 + +6.2.7: The spec only talks about PostScript XObjects, which we don't really use. +We only use PostScript functions. In any case, to be on the safe side, krilla fails exports +when a PostScript function is used. 🟢 + +6.2.8: krilla does not use the transfer functions, halftones, TR/HTP/RI/FL keys. 🔵 + +6.2.9: krilla does never define a rendering intent. 🔵 + +6.2.10: krilla only uses operators defined in the reference and never uses BX/EX. 🔵 + +# 6.3 Fonts + +6.3.2: krilla follows the specification when defining fonts. 🟢 + +6.3.3.1: krilla always writes the same Registry/Ordering for fonts. 🟢 + +6.3.3.2: krilla always includes a `CIDToGIDMap` for Type2 CID fonts. 🟢 + +6.3.3.3: krilla always writes the `WMode` entry for cmaps and never references any other ones. 🟢 + +6.3.4: +- krilla embeds all fonts that are used. 🟢 +- krilla checks the OpenType fsType field to ensure that fonts are legally embeddable. 🟢 + +6.3.5: +- krilla doesn't support Type1 fonts and thus doesn't need the `CharSet` attribute. 🔵 +- krilla always writes the `CIDSet` attribute. 🟢 + +6.3.6: krilla always adds the `Widths` entry and makes them consistent to the font program. 🟢 + +6.3.7: krilla only embeds CID fonts. 🔵 + + +# 6.4 Transparency + +- krilla forbids soft masks in this export mode. 🟢 +- krilla forbids the S key in this export mode. 🟢 +- krilla forbids non-default CA/ca/BM in this export mode. 🟢 + +## 6.5 Annotations + +6.5.2: krilla does not support any non-standard annotation types, nor `FileAttachment`, `Sound` or `Movie`. 🔵 + +6.5.3: +- krilla never sets the CA key of annotation dictionaries. 🔵 +- krilla always sets the `F` flag for annotations. 🟢 +- krilla does not support text annotations. 🔵 +- krilla does not set the C key of annotations. 🔵 +- krilla does never write an appearence dictionary. 🔵 + + +## 6.6 Action + +6.6.1: +- krilla does not support the `Launch`, `Sound`, `Movie`, `ResetForm`, `ImportData`, `Hide`, + `JavaScript` actions. 🔵 +- krilla does not use the `set-state` and `no-op` actions. 🔵 +- krilla does not support named actions. 🔵 +- krilla does not support interactive forms. 🔵 + +6.6.2: +- krilla does not support widget annotations. 🔵 +- krilla does not write an AA entry in the document catalog. 🟢 +- krilla does not write an AA entry for pages. 🟢 + +6.6.3: - + +## 6.7 Metadata + +6.7.2: +- krilla overrides the `xmp_metadata` attribute for PDF/A exports so that it's always contained. 🟢 +- `xmp-writer` always creates conforming XMP streams. 🟢 +- We never apply a filter to the metadata stream dictionary. + +6.7.3: +- krilla ensures that XMP metadata and document info dictionary are consistent. 🟢 +- Authors are encoded as a length-1 text array. 🟢 +- krilla ensures consistency between PDF dates and XMP dates. + +6.7.4: Normalization is currently not checked. + +6.7.5: We never include the `bytes` or `encoding` attributes. 🟢 + +6.7.6: krilla writes pdfaid:conformance and pdfaid:part as specified. 🟢 + +6.7.7: krilla writes a minimal `xmpMM:History` entry when a creation date is +provided. 🟢 + +6.7.8: krilla writes the extension schemas. 🟢 + +6.7.9: `xmp-writer` always produces valid XMP packets. 🟢 + +6.7.10: krilla doesn't use Type 0, Type 1 or TrueType fonts directly, thus no metadata needs to be provided. 🔵 + +6.7.11: krilla writes the conformance level identification. 🟢 + +# Level A + +6.3.8: krilla always embeds a unicode character map. 🟢 + +# 6.8 Logical Structure + +6.8.2.1: General conformance to tagged PDF cannot be checked, but is documented. 🟣 + +6.8.2.2: krilla always writes the mark info dictionary for this export mode. 🟢 + +6.8.3.1: The need to specify artifacts is documented. 🟣 + +6.8.3.2: The need to specify word boundaries is documented. 🟣 + +6.8.3.3: +- The presence of a structure tree is enforced. 🟢 +- The need to specify a granular structure hierarchy is documented. 🟣 + +6.8.3.4: krilla maps all non-standard structure types. 🟢 + +6.8.4: +- krilla requires the user to set the document language in that export mode. 🟢 +- krilla allows users to specify the language on spans. 🟢 +- The need for correctness of language tags is documented. 🟣 + +6.8.5: The fact that an alternate text should be provided to figures and formulas is checked. 🟢 + +6.8.6: krilla ensures that annotations have an alternate text. 🟢 + +6.8.7: The requirement to specify replacement text is documented. 🟣 + +6.8.8: The requirement to specify the expansions of abbreviations is documented. 🟣 +NOTE: When inserting expansions of abbreviations, you must set them on the +leaf `SpanTag` struct passed to `Surface::start_tagged` method instead of the +`Tag` struct in the structure tree + +# 6.9 Interactive Forms +krilla does not support interactive forms. 🔵 diff --git a/vendor/krilla/src/configure/PDF_A2.md b/vendor/krilla/src/configure/PDF_A2.md new file mode 100644 index 00000000..a4e4377a --- /dev/null +++ b/vendor/krilla/src/configure/PDF_A2.md @@ -0,0 +1,269 @@ +# Description +PDF/A-2 requires PDF version <= 1.7 and defines three conformance levels, +in the following order from less strict to more strict: +- Level B +- Level U +- Level A + +Level U is a subset of level A, and level B is a subset of level U. + +See `README.md` for the meaning of each color. + +## Level B + +## 6.1 File structure + +6.1.2: `pdf-writer` always write the file header as described in the spec. Even if the +user enables `ascii_compatible`, we still write a binary header marker. 🟢 + +6.1.3: +- We always set the file ID. 🟢 +- We do not support encryption. 🔵 + +6.1.4: `pdf-writer` always write the xref section as described in the spec. 🟢 + +6.1.5: - + +6.1.6: `pdf-writer` always writes hex strings with an even number of characters. 🟢 + +6.1.7: +- `pdf-writer` always write streams as described in the spec. 🟢 +- krilla does never write streams referencing external files. 🔵 +- krilla does never use `LZWDecode` or `Crypt`. 🔵 +- krilla never writes F, FFIlter or FDecodeParams in streams. 🔵 + +6.1.8: krilla only ever writes UTF-8 strings as names. 🟢 + +6.1.9: `pdf-writer` always writes indirect objects as described in the spec. 🟢 + +6.1.10: krilla does never use inline image dictionaries. 🔵 + +6.1.11: - + +6.1.12: krilla doesn't support permissions. 🔵 + +6.1.13: +- `pdf-writer` uses i32 for integers. 🟢 +- `pdf-writer` uses f32 for real numbers. 🟢 +- krilla always uses the `new_str` and `new_text_str` methods of the SerializeContext to create them, + which returns a validation error in case it's too long. 🔵 +- krilla trims the names of fonts, and all other names cannot be longer than 127. 🔵 +- krilla fails export if more than 8388607 indirect objects exist. 🟢 +- krilla fails export if a higher nesting-level than 28 exists. 🟢 +- krilla does not use the DeviceN color space. 🟢 +- krilla only uses u16 for CIDs. 🟢 + +## 6.2 Graphics + +6.2.2: +- krilla doesn't use non-standard operators. 🟢 +- krilla doesn't use the `ri` or `i` operator. 🟢 +- krilla ensure that content stream has their own associated resource dictionary. 🟢 + +6.2.3: krilla always write an `sRGB` output intent for PDF/A. 🟢 + +6.2.4.1: krilla overrides the `no_device_cs` property if PDF/A is selected, and +in case CMYK is used but no profile was provided, export fails. + +6.2.4.2: +- sRGB/sGrey ICC profiles conform to ICC v4 specification. 🟢 +- krilla does not support overprinting. 🔵 + +6.2.4.3: +- krilla always uses sRGB for RGB, and in addition also embeds an sRGB output intent. 🟢 +- krilla always uses sGrey (except for encoding the alpha channel in images, where DeviceGray + is required), and in addition always embeds an output intent. 🟢 +- krilla always uses an CMYK ICC profile, and always sets CMYK as the output intent. 🟢 + It fails export if no CMYK ICC profile was provided. 🟢 + +6.2.4.4: +- krilla ensures the Alternate space in Separation color spaces obeys the restrictions + in the applicable clauses 🟢 +- krilla does not support DeviceN color spaces. 🔵 +- krilla fails export if a Separation colorant is associated with multiple different + fallback color spaces 🟢 +- krilla manages the `tintTransform` function and will always write the same function for + the same color space 🟢 + +6.2.4.5: Fulfilled because patterns are treated the same as all other elements in krilla. 🟢 + +6.2.5: krilla does not use the transfer functions, halftones, TR/HTP/RI/FL keys. 🔵 + +6.2.6: krilla does never define a rendering intent. 🔵 + +6.2.7: krilla is not a reader. 🔵 + +6.2.8.1: +- krilla does not use the `Alternates`/`Intent` keys for images. 🔵 +- krilla does check whether the `Interpolate` key is used. 🟢 + +6.2.8.2: krilla does not support thumbnails. 🔵 + +6.2.8.3: krilla doesn't support JPEG2000 images. 🔵 + +6.2.9.1: krilla does not use the `OPI`/`Subtype2`/`PS` keys for XObjects. 🔵 + +6.2.9.2: krilla does not use reference XObjects. 🔵 + +6.2.9.3: The spec only talks about PostScript XObjects, which we don't really use. +We only use PostScript functions. In any case, to be on the safe side, krilla fails exports +when a PostScript function is used. 🟢 + +6.2.10: krilla always includes an OutputIntent for PDF/A, so the /G attribute is not +always required. 🟢 + +6.2.11.1: krilla fails export in PDF/A when the .notdef glyph is referenced. 🟢 + +6.2.11.2: krilla has made sure that the spec is followed in this regard. 🟢 + +6.2.11.3.1: krilla always uses `Identity-H` as encoding. 🟢 + +6.2.11.3.2: krilla always writes the `CIDToGidMap` entry. 🟢 + +6.2.11.3.3: krilla always writes the `WMode` entry for cmaps and never references any other ones. 🟢 + +6.2.11.4.1: +- krilla embeds all fonts that are used. 🟢 +- krilla checks the OpenType fsType field to ensure that fonts are legally embeddable. 🟢 + +6.2.11.4.2: +- krilla never writes the `CharSet` attribute. 🔵 +- krilla always includes all CIDs in `CIDSet`. 🔵 + +6.2.11.5: krilla copies the font metrics directly from the font. 🟢 + +6.2.11.6: +- krilla embeds all fonts as symbolic. 🟢 +- krilla does not write TrueType fonts directly. 🔵 +- krilla only writes CIDFonts instead of TrueType fonts directly, so cmap is not needed. 🟢 + +6.2.11.8: krilla fails export when the .notdef glyph is referenced. 🟢 + +## 6.3 Annotations + + +6.3.1: krilla does not support any non-standard annotation types, nor `3D`, `Sound`, `Screen` or `Movie`. 🔵 + +6.3.2: +- krilla always sets the `F` flag for annotations. 🟢 +- krilla does not support text annotations. 🔵 + +6.3.3: krilla only supports the Link subtype for annotations, which doesn't require an appearence stream. 🔵 + +6.3.4: - + +## 6.4 Interactive forms + +6.4.1: krilla does not support interactive forms. 🔵 + +6.4.2: krilla does not support interactive forms. 🔵 + +6.4.3: krilla does not support digital signatures. 🔵 + +## 6.5 Action + +6.5.1: +- krilla does not support the `Launch`, `Sound`, `Movie`, `ResetForm`, `ImportData`, `Hide`, `SetOCGState`, `Rendition`, `Trans`, + `GoTo3DView` and `JavaScript` actions. 🔵 +- krilla does not use the `set-state` and `no-op` actions. 🔵 +- krilla does not support named actions. 🔵 + +6.5.2: +- krilla does not support widget annotations. 🔵 +- krilla does not write an AA entry in the document catalog. 🟢 +- krilla does not write an AA entry for pages. 🟢 + +6.5.3: - + +## 6.6 Metadata + +6.6.1: - + +6.6.2.1: +- krilla overrides the `xmp_metadata` attribute for PDF/A exports so that it's always contained. 🟢 +- `xmp-writer` always creates conforming XMP streams. 🟢 +- We never include the `bytes` or `encoding` attributes. 🟢 + +6.6.2.2: - + +6.6.2.3.1: krilla doesn't use any non-standard properties. 🟢 + +6.6.2.3.2: krilla writes the extension schemas. 🟢 + +6.6.2.3.3: krilla writes the extension schemas. 🟢 + +6.6.3: krilla ensures that XMP metadata and document info dictionary are consistent. 🟢 + +6.6.4: krilla writes pdfaid:conformance and pdfaid:part as specified. 🟢 + +6.6.5: +- krilla writes a document and instance ID. 🟢 +- krilla writes a minimal `xmpMM:History` entry when a creation date is provided. 🟢 + +6.6.6: krilla writes a minimal `xmpMM:History` entry when a creation date is +provided. 🟢 + +# 6.8 Embedding files + +krilla prohibits embedding files in this export mode. 🟢 + +# 6.9 Optional content + +krilla does not support optional content. 🔵 + +# 6.10 Use of alternate presentations and transitions + +- krilla never writes `AlternatePresentations` in the name dictionary. 🔵 +- krilla never writes `PresSteps` into page dictionaries. 🔵 + +# 6.11 Document requirements + +krilla never writes the `Requirements` key in the document dictionary. 🔵 + +## Level U + +6.2.11.7.1: - + +6.2.11.7.2: +- krilla always includes a `ToUnicode` mapping. 🟢 +- For levels U and A, krilla checks that all glyphs have a codepoint mapping that + does not contain 0x0, 0xFEFF or 0xFFFE. 🟢 + + +# Level A + +6.2.11.7.3: krilla forbids all codepoints in the unicode private area for this export mode. 🟢 + +# 6.7 Logical Structure + +6.7.1: - + +6.7.2.1: General conformance to tagged PDF cannot be checked, but is documented. 🟣 + +6.7.2.2: krilla always writes the mark info dictionary for this export mode. 🟢 + +6.7.3.1: The need to specify artifacts is documented. 🟣 + +6.7.3.2: The need to specify word boundaries is documented. 🟣 + +6.7.3.3: +- The presence of a structure tree is enforced. 🟢 +- The need to specify a granular structure hierarchy is documented. 🟣 + +6.7.3.4: krilla maps all non-standard structure types. 🟢 + +6.7.4: +- krilla requires the user to set the document language in that export mode. 🟢 +- krilla allows users to specify the language on spans. 🟢 +- The need for correctness of language tags is documented. 🟣 + +6.7.5: The fact that an alternate text should be provided to figures and formulas is checked. 🟢 + +6.7.6: krilla ensures that annotations have an alternate text. 🟢 + +6.7.7: The requirement to specify replacement text is documented. 🟣 + +6.7.8: The requirement to specify the expansions of abbreviations is documented. 🟣 +NOTE: When inserting expansions of abbreviations, you must set them on the +leaf `SpanTag` struct passed to `Surface::start_tagged` method instead of the +`Tag` struct in the structure tree diff --git a/vendor/krilla/src/configure/PDF_A3.md b/vendor/krilla/src/configure/PDF_A3.md new file mode 100644 index 00000000..823be41a --- /dev/null +++ b/vendor/krilla/src/configure/PDF_A3.md @@ -0,0 +1,31 @@ +# Description +PDF/A-3 requires a PDF version <= 1.7 and defines three conformance levels, +in the following order from less strict to more strict: +- Level B +- Level U +- Level A + +It is exactly the same as PDF/A-2, except for clause 6.8. + +See `README.md` for the meaning of each subclause. + +## Level B + +## 6.8 Embedded files + +- krilla allows embedded files in this export mode. 🟢 +// TODO: Decide whether UF should be written for < PDF 1.7. +- krilla writes `F` and `AFRelationship` for embedded files, and writes `UF` for PDF 1.7 + exports. 🟠 +- krilla requires modification date, description and MIME subtype for embedded files in this + export mode. 🟢 +- The fact that embedded file relationships should correctly describe the relationship to + the document is documented. 🟣 + +## Level U + +- + +# Level A + +- \ No newline at end of file diff --git a/vendor/krilla/src/configure/PDF_A4.md b/vendor/krilla/src/configure/PDF_A4.md new file mode 100644 index 00000000..44a294f6 --- /dev/null +++ b/vendor/krilla/src/configure/PDF_A4.md @@ -0,0 +1,211 @@ +# Description +PDF/A-4 requires PDF version 2.0. + +See `README.md` for the meaning of each color. + +## 5.1 Conforming files +- krilla does not use any deprecated features for PDF 2.0. 🟢 + +## 6.1 File structure +- + +### 6.1.2 File header +- krilla only allows exporting with PDF 2.0 in this mode. 🟢 +- krilla always writes the binary marker as written. 🟢 + +### 6.1.3 File trailer +- The trailer is written as defined. 🟢 +- krilla does not support encryption. 🟢 +- krilla does not write the `Info` attribute into the trailer. 🟢 + +### 6.1.4 Cross reference table +- The cross-reference table is always written as defined. 🟢 + +### 6.1.5 String objects +- pdf-writer always writes an even amount of digits for hexadecimal strings. 🟢 + +### 6.1.6 Stream objects +- krilla writes streams as mandated. 🟢 +- krilla does not use the `F`, `FFilter` or `FDecodeParams` attributes. 🟢 +- krilla does not use LZWDecode. 🟢 +- krilla does not support the `Crypt` filter. 🟢 + +### 6.1.7 Name objects +- krilla does not use non-UTF8 names. 🟢 (TODO: what about user supplied ones?) + +### 6.1.8 Indirect objects +- krilla always writes indirect objects as defined. 🟢 + +### 6.1.9 Inline image dictionaries +- krilla does not use inline images. 🟢 + +### 6.1.10 Linearized PDF +- krilla does not support linearization. 🟢 + +### 6.1.11 Permissions +- krilla does not support permissions. 🟢 + +### 6.1.12 Document catalog dictionary +- krilla writes the PDF file header version as 2.0 and does not write a conflicting catalog + `Version` entry. 🟢 + +## 6.2 Graphics + +### 6.2.1 General + +### 6.2.2 Content streams +- krilla does not use custom content stream operators. 🟢 + +### 6.2.3 Output intent +- krilla always writes the output intent for this export mode. 🟢 +- krilla does not use the `DestOutputProfileRef` attribute. 🟢 +- krilla does not write multiple output intents (since PDF/X and PDF/E are not supported). 🟢 +- krilla only uses a Display ICC profile for output intent. 🟢 +- krilla only uses RGB for output intents. 🟢 + +### 6.2.4 Colour spaces +- krilla only uses device-independent colors in this export mode. 🟢 +- krilla uses compatible ICC profiles. 🟢 +- krilla does not use the OPM entry. 🟢 +- krilla never uses CMYK profiles as the destination profile. 🟢 +- krilla does not use device color spaces in this mode. 🟢 +- krilla ensures the Alternate space in Separation color spaces obeys the restrictions + in the applicable clauses 🟢 +- krilla does not support DeviceN color spaces. 🔵 +- krilla fails export if a Separation colorant is associated with multiple different + fallback color spaces 🟢 +- krilla manages the `tintTransform` function and will always write the same function for + the same color space 🟢 + +### 6.2.5 Extended graphics state +- krilla does not use the `TR`, `HTO`, `TR2` or `HT` keys. 🟢 +- krilla does not use halftones. 🟢 +- krilla does not use the `FL`, `BG`, `BG2`, `UCR` or `URC2` functions. 🟢 + +### 6.2.6 Flatness +- krilla does not use the flatness parameter. 🟢 + +### 6.2.7 Images +- krilla does not use the `Alternates` or `OPI` key. 🟢 +- krilla disallows the `Interpolate` key in this mode. 🟢 +- krilla does not support thumbnails. 🟢 +- krilla does not support JPEG2000 images. 🟢 + +### 6.2.8 XObjects +- krilla does not use the `OPI` key in FormXObjects. 🟢 +- krilla does not use reference XObjects. 🟢 + +### 6.2.9 Transparency +- krilla uses transparency as mandated. 🟢 + +### 6.2.10 Fonts +- krilla uses fonts as described in the spec. 🟢 +- krilla always uses the IDENTITY-H encoding. 🟢 +- krilla always embeds a CIDtoGIDMap for Type2 CID fonts. 🟢 +- krilla always embeds cmaps and adds the WMode entry. 🟢 +- krilla always embeds the font programs. 🟢 +- krilla only uses glyphs referenced in the font. 🟢 +- krilla derives the glyph width information from the font program, also for Type3 fonts. 🟢 +- krilla does not use fonts in vertical writing mode. 🟢 +- krilla only writes symbolic TrueType fonts. 🟢 +- krilla does not use the `Encoding` entry in the font dictionary. 🟢 +- krilla always writes the `ToUnicode` entry. 🟢 +- krilla ensures Unicode values are always greater than 0 and not equal to U+FEFF or U+FFFE. 🟢 +- krilla straight out forbids characters in the private use area. 🟢 +- krilla disallows the .notdef glyph in this export mode. 🟢 +- krilla checks the OpenType fsType field to ensure that fonts are legally embeddable. 🟢 + +## 6.3 Annotations + +### 6.3.1 Annotation types +- krilla only supports link annotations. 🟢 + +### 6.3.2 Annotation dictionaries +- Annotation dictionaries always contain the `F` key and sets the values accordingly. 🟢 +- krilla does not support text annotations. 🟢 + +### 6.3.3 Annotation appearances +- This section only contains provisions for readers. 🟢 + +### 6.3.4 Display of annotation contents +- krilla does not use appearence dictionaries. 🟢 + +## 6.4 Interactive forms + +- krilla does not support interactive forms. 🟢 + +## 6.5 Digital signatures + +- krilla does not support digital signatures. 🟢 + +## 6.6 Action + +### 6.6.1 General +- krilla does not support any of the named actions. 🟢 + +### 6.6.2 Handling of JavaScript actions +- krilla does not support JavaScript actions. 🟢 + +### 6.6.3 Trigger events +- krilla does not use the `AA` entry anywhere. 🟢 + +### 6.6.4 Handling of GoToR, GoToE, URI and SubmitForm actions +- This section only contains provisions for readers. 🟢 + +## 6.7 Metadata + +### 6.7.1 General + +### 6.7.2 Metadata streams +- krilla always requires metadata in this export mode. 🟢 +- krilla does not use the `bytes` and `encoding` attributes. 🟢 +- krilla uses the outlined namespaces and prefixes. 🟢 +- krilla does not currently have an associated file containing the embedded file specification. However, + it seems like there is an issue with the spec because I can't find that table entry in the PDF 2.0 spec? + And no validator I tried seems to complain about this.🔴 + +### 6.7.3 Version identification +- krilla always writes the required version identification parts. 🟢 + +### 6.7.4 File identifiers +- krilla always adds a document identifier to the metadata. 🟢 +- krilla writes a minimal `xmpMM:History` entry when a creation date is provided. 🟢 + +### 6.7.5 File provenance information +- krilla writes a minimal `xmpMM:History` entry when a creation date is provided. 🟢 + +## 6.8 Logical structure +- Logical structure is only optional, so krilla does not make any enforcements here, but the + recommendation is documented. 🟢 + +## 6.9 Embedded files +- krilla straight out forbids embedding files in this export mode. 🟢 + +## 6.10 Optional content +- krilla does not support optional content. 🟢 + +## 6.11 Use of alternate presentations and transitions +- krilla does not use the `AlternatePresentations` entry. 🟢 + +## 6.12 Document requirements +- krilla does not use the `Requirements` key. 🟢 + +## 6.13 Print scaling +- This section only contains provisions for processors. 🟢 + +## 6.14 Geospatial +- krilla does not support any of the geospatial features. 🟢 + +## 6.15 Measurement Properties +- krilla does not use any of the measurement properties. 🟢 + +# A4-F +- krilla allows embedding any files in this export mode. 🟢 +- krilla always writes the `AFRelationship`, `Desc`, `UF` and `F` strings in this case. 🟢 +- krilla always writes the corresponding file identification. 🟢 + +# A4-E +- krilla does not support any of the 3D features. 🟢 +- krilla deals similarly with embedded files similarly to A4-F. 🟢 +- krilla always writes the corresponding file identification. 🟢 + diff --git a/vendor/krilla/src/configure/PDF_UA1.md b/vendor/krilla/src/configure/PDF_UA1.md new file mode 100644 index 00000000..16959bbe --- /dev/null +++ b/vendor/krilla/src/configure/PDF_UA1.md @@ -0,0 +1,202 @@ +# Description +PDF/UA-1 requires a version <= PDF 1.7. + +See `README.md` for the meaning of each color. + +# 6.1 General + +- krilla defaults to PDF 1.7 for this mode, but allows any PDF version up to PDF 1.7. 🟢 + +# 6.2 Conforming files + +- krilla writes the `pdfuaid:part` attribute. 🟢 +- krilla does adhere to the file format provisions. 🟢 + +# 6.3 Conforming reader + +- Not applicable to krilla as a writer. - + +# 6.4 Conforming assistive technology + +- Not applicable to krilla as a writer. - + +# 7.1 General + +- The fact that real content should be tagged is documented. 🟣 +- The fact that artifacts should be marked is documented. 🟣 +- krilla never includes artifacts in the structure tree. 🟢 +- krilla role maps all non-standard structure types. 🟢 +- krilla does not overwrite non-standard structure types. 🟢 +- krilla doesn't support any elements that flicker, flash or blink. 🟢 +- The fact that information shall not be conveyed by contrast, colour, format or layout is documented. 🟣 +- krilla does not support sounds. 🔵 +- krilla forces the user to provide a document title. 🟢 +- krilla always sets `DisplayDocTitle` to true for this mode. 🟢 +- krilla can't really control if the user provides raster-based images as content. 🟠 +- krilla always writes the `Suspects` value as false. 🟢 + +# 7.2 Text +- The fact that logical reading order should be followed is documented. 🟣 +- krilla checks that every character is mapped to a codepoint. 🟢 +- The fact that the user should make use of the natural language attributes is documented. 🟣 +- The fact that stretchable characters should be marked with `ActualText` is documented. 🟣 +- krilla allows setting the language of spans. 🟢 + +# 7.3 Graphics +- The fact that figures should be tagged (as a figure or an artifact) is documented. 🟣 +- The fact that figures should be followed by a caption is documented. 🟣 +- The fact that an alternate text should be provided to figures is checked. 🟢 +- The fact graphics that possess semantic value only in combination with other graphics should be tagged with a single Figure tag for each group is documented. 🟣 +- The fact that a more accessible representation should be used if it exists is documented. 🟣 + +# 7.4 Headings + +7.4.1: +- The fact that headings should be tagged is documented. 🟣 +- krilla supports the `T` key for headings. 🟢 + +7.4.2: +- The information there is hardly enforceable in an automated way, so not documented yet. 🟠 + +7.4.3: +- krilla adds tags for heading levels higher than 6 as prescribed. 🟢 + +7.4.4: +- The information there is hardly enforceable in an automated way, so not documented yet. 🟠 + +# 7.5 Tables +- The fact that tables should include headers is documented. 🟣 +- krilla always requires the user to provide a table header scope. 🟢 +- The fact that table tagging structures should only be used to tag content presented within logical row and/or column relationships is documented. 🟣 + +# 7.6 Lists +- The fact that lists should be tagged is documented. 🟣 +- The fact that Li, Lbl and LBody should be used is documented. 🟣 +- krilla always forces writing the `ListNumbering` attribute for lists. 🟢 + +# 7.7 Mathematical expressions +- The fact that mathematical expressions should be wrapped in `Formula` is documented. 🟣 +- The fact that mathematical expressions should have an alternate text is checked. 🟢 + +# 7.8 Page headers and footers +- The fact that headers and footers should be tagged is documented. 🟣 + +# 7.9 Notes and references +- The fact that footnotes, endnotes, note labels and references should be tagged is documented. 🟣 +- The fact that footnotes and endnotes should be tagged with `Note` is documented. 🟣 +- krilla always generates an ID for notes. 🟢 + +# 7.10 Optional content +- krilla does currently not support optional content. 🔵 + +# 7.11 Embedded files +- krilla supports embedded files, and requires a description in this export mode. 🟢 +- The fact that embedded files should be accessible in their own right is documented. 🟣 + +# 7.12 Article threads +- The fact that the logical reading order should be preserved is documented. 🟣 + +# 7.13 Digital signatures +- krilla does not support digital signatures. 🔵 + +# 7.14 Non-interactive forms +- krilla does not support forms. 🔵 + +# 7.15 XFA +- krilla does not support forms. 🔵 + +# 7.16 Security +- krilla does not support encryption. 🔵 + +# 7.17 Navigation +- krilla enforces setting a document outline. 🟢 +- The fact that the outline should reflect the reading order is + documented. 🟣 +- The fact that page labels should be semantically appropriate is documented. 🟣 + +# 7.18 Annotations +7.18.1: +- The fact that annotations should reflect the reading order is documented. 🟣 +- The fact that for visual formatting, annotations should + be tagged according to their semantic function is not documented. 🟠 +- krilla ensures that annotations have an alternate text. 🟢 + +7.18.2: +- krilla only supports the default annotation types. 🟢 +- krilla does not use the `TrapNet` annotation. 🔵 + +7.18.3 +- krilla always writes the `TabOrder` property for pages that have a struct parent. 🟢 + +7.18.4 +- krilla does not support widget annotations. 🔵 + +7.18.5 +- The best practices for link tagging are documented. 🟣 +- krilla enforces an alt text for all annotations. 🟢 +- krilla never writes the `IMap` key for URIs. 🔵 + +7.18.6 +- krilla does not support media annotations. 🔵 + +7.18.7 +- krilla does not support file attachment annotations. 🔵 + +7.18.8 +- krilla does not support printer mark annotations. 🔵 + +# 7.19 Actions +- krilla does not support adding (JavaScript) scripts. 🔵 + +# 7.20 XObjects +- krilla never creates reference XObjects. 🔵 +- krilla doesn't allow references to XObjects for tagging. 🔵 + +# 7.21 Fonts + +7.21.1: - + +7.21.2: +- krilla ensures to always conform to the PDF specification. 🟢 + +7.21.3.1: +- krilla always uses Identity-H for encoding. 🟢 + +7.21.3.2: +- krilla always includes a `CIDToGIDMap`. 🟢 + +7.21.3.3 +- krilla always includes a cmap. 🟢 +- krilla always writes the `WMode` entry in cmaps. 🟢 +- krilla never references other cmaps in a cmap. 🟢 + +7.21.4.1 +- krilla always embeds a cmap for all used fonts. 🟢 +- The fact that only legally embeddable fonts should be used is + documented. 🟣 +- krilla export only succeeds if all glyphs are available in the font (otherwise subsetting fails). 🟢 + +7.21.4.2 +- krilla doesn't include a CharSet in font descriptors. 🟢 +- krilla always includes all CIDs in `CIDSet`. 🟢 + +7.21.5 +- Font metrics should (hopefully) be as consistent as possible. 🟢 + +7.21.6 +- krilla only uses CID fonts and not (PDF) TrueType fonts. 🟢 + +7.21.7 +- krilla always includes the `ToUnicode` entry with corresponding mappings. 🟢 +- krilla will fail export if 0, U+FEFF or U+FFFE is included. 🟢 + +7.21.8 +- krilla will fail export if the .notdef glyph is included. 🟢 + +# 8 Reader Requirements + +- Not applicable to krilla as a writer. - + +# 9 Assistive Technology Requirements + +- Not applicable to krilla as a writer. - diff --git a/vendor/krilla/src/configure/README.md b/vendor/krilla/src/configure/README.md new file mode 100644 index 00000000..267a9acb --- /dev/null +++ b/vendor/krilla/src/configure/README.md @@ -0,0 +1,23 @@ +# Introduction + +The files in this folder track the properties that are required to achieve +different compliance level, as well as a description of how/why krilla achieves those. +As krilla evolves and new features are added, these documents +might have to be updated, and we always need to ensure that all invariants are still enforced, +when adding new features. + +# Legend +🟢: This means that krilla actively checks that this property is enforced, either by an +invariant in the code, or by returning an error to the user in case it's not fulfillable. + +🔵: This means that krilla fulfills this property because it's not supported. + +🟣: This means that this property cannot be enforced by krilla, and thus is only documented. +It is upon the user of the library to enforce it. + +🔴: This means that the property is currently not enforced by krilla. + +🟠: This means that the property should perhaps be documented/checked, but is +currently not because it seems unnecessary or hard to implement. + +-: This means that the clause is not applicable to krilla (for example because it's not a reader application) \ No newline at end of file diff --git a/vendor/krilla/src/configure/mod.rs b/vendor/krilla/src/configure/mod.rs new file mode 100644 index 00000000..187cd55f --- /dev/null +++ b/vendor/krilla/src/configure/mod.rs @@ -0,0 +1,197 @@ +//! Configuring PDF version and export mode. + +pub mod validate; +mod version; + +pub use validate::{Accessibility, Archival, ValidationError, Validator, Validators}; +pub use version::PdfVersion; + +use crate::configure::validate::ValidatorsBuilder; + +/// A configuration of validator and PDF version. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct Configuration { + validators: Validators, + version: PdfVersion, +} + +impl Configuration { + /// Return the validators of the configuration. + pub fn validators(self) -> Validators { + self.validators + } + + /// Return the PDF version of the configuration. + pub fn version(self) -> PdfVersion { + self.version + } +} + +/// A configuration of validator and PDF version. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct ConfigurationBuilder { + validators: ValidatorsBuilder, + version: Option, +} + +impl ConfigurationBuilder { + /// Create a new `ConfigurationBuilder` with default settings. + pub fn new() -> Self { + Self::default() + } + + /// Set the PDF version, overwriting the current one if already set. + pub fn with_version(mut self, version: PdfVersion) -> Self { + self.version = Some(version); + self + } + + /// Set a validator, overwriting the current one if the same standard family is already set. + pub fn set_validator(mut self, validator: Validator) -> Self { + self.validators = self.validators.set_validator(validator); + self + } + + /// Set the PDF/A validator, overwriting the current one if already set. + pub fn with_archival_validator(mut self, archival: Archival) -> Self { + self.validators = self.validators.with_archival_validator(archival); + self + } + + /// Set the PDF/UA accessibility validator, overwriting the current one if already set. + pub fn with_accessibility_validator(mut self, ua: Accessibility) -> Self { + self.validators = self.validators.with_accessibility_validator(ua); + self + } + + /// Build the [`Configuration`], returning an error if the validators and version are incompatible. + pub fn finish(self) -> Result { + let validators = self + .validators + .finish() + .map_err(ConfigurationError::NoOverlappingValidatorsRange)?; + + let validator_range = validators.min().unwrap_or(PdfVersion::MIN)..=validators.max(); + match self.version { + Some(version) if validator_range.contains(&version) => Ok(Configuration { + validators, + version, + }), + Some(version) => Err(ConfigurationError::VersionDoesNotMatchValidatorsRange( + version, validators, + )), + None if !validators.is_empty() => Ok(Configuration { + validators, + version: *validator_range.end(), + }), + None => { + let version = PdfVersion::default(); + debug_assert!(validator_range.contains(&version)); + Ok(Configuration { + validators, + version, + }) + } + } + } +} + +/// An error that occurred while building a [`Configuration`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ConfigurationError { + /// The selected validators have no overlapping valid PDF version range. + NoOverlappingValidatorsRange(Validators), + /// The explicitly set PDF version falls outside the range allowed by the validators. + VersionDoesNotMatchValidatorsRange(PdfVersion, Validators), +} + +#[cfg(test)] +mod tests { + use crate::configure::{ + Accessibility, Archival, Configuration, ConfigurationBuilder, ConfigurationError, + PdfVersion, + }; + + #[test] + fn invalid_combination_1() { + // A1_B max is PDF 1.4; explicit PDF 1.7 is out of range. + assert!(matches!( + ConfigurationBuilder::new() + .with_version(PdfVersion::Pdf17) + .with_archival_validator(Archival::A1_B) + .finish(), + Err(ConfigurationError::VersionDoesNotMatchValidatorsRange( + PdfVersion::Pdf17, + _ + )) + )); + } + + #[test] + fn invalid_combination_2() { + // A4 requires PDF 2.0; UA1 max is PDF 1.7 → no overlapping range. + assert!(matches!( + ConfigurationBuilder::new() + .with_archival_validator(Archival::A4) + .with_accessibility_validator(Accessibility::UA1) + .finish(), + Err(ConfigurationError::NoOverlappingValidatorsRange(_)) + )); + } + + #[test] + fn invalid_combination_3() { + // A1_B max is PDF 1.4; UA1 max is PDF 1.7 → intersection is PDF14..=PDF14. + // Explicitly setting PDF 1.7 is out of range. + assert!(matches!( + ConfigurationBuilder::new() + .with_archival_validator(Archival::A1_B) + .with_accessibility_validator(Accessibility::UA1) + .with_version(PdfVersion::Pdf17) + .finish(), + Err(ConfigurationError::VersionDoesNotMatchValidatorsRange( + PdfVersion::Pdf17, + _ + )) + )); + } + + #[test] + fn multi_validator_pdf_a3b_pdf_ua1() { + let config = ConfigurationBuilder::new() + .with_archival_validator(Archival::A3_B) + .with_accessibility_validator(Accessibility::UA1) + .finish() + .unwrap(); + assert_eq!(config.validators().archival(), Some(Archival::A3_B)); + assert_eq!( + config.validators().accessibility(), + Some(Accessibility::UA1) + ); + assert_eq!(config.version(), PdfVersion::Pdf17); + } + + #[test] + fn multi_validator_pdfa2a_pdfua1() { + assert!(ConfigurationBuilder::new() + .with_archival_validator(Archival::A2_A) + .with_accessibility_validator(Accessibility::UA1) + .finish() + .is_ok()); + } + + #[test] + fn empty_validators() { + let config = ConfigurationBuilder::new().finish().unwrap(); + assert!(config.validators().is_empty()); + assert_eq!(config.version(), PdfVersion::Pdf17); + } + + #[test] + fn default_config() { + assert_eq!( + ConfigurationBuilder::new().finish().unwrap(), + Configuration::default() + ); + } +} diff --git a/vendor/krilla/src/configure/validate.rs b/vendor/krilla/src/configure/validate.rs new file mode 100644 index 00000000..e92da05b --- /dev/null +++ b/vendor/krilla/src/configure/validate.rs @@ -0,0 +1,1237 @@ +//! Exporting with a specific PDF conformance level. +//! +//! PDF defines a number of additional conformance levels that restrict the features of PDF that +//! can be used to a specific subset. +//! +//! You can use a [`Validator`] by creating a corresponding [`Configuration`] +//! you want to build the document with. There are three important aspects that play into this: +//! - krilla will internally write the file in a way that conforms to the given standard, i.e. +//! by settings appropriate metadata. This happens under-the-hood and is completely abstracted +//! away from the user. +//! - For aspects that are out of control of krilla and dependent on the input, krilla will perform +//! a validation that the input is compatible with the standard. krilla will record all violations, +//! and when calling `document.finish()`, in case there is at least one violation, krilla will +//! return them as an error, instead of returning the finished document. See [`ValidationError`]. +//! - Finally, some standards have requirements that cannot possibly be validated by krilla, as +//! they are semantic in nature. It is upon you, as a user of that library, to ensure that those +//! requirements are fulfilled. Therefore, while krilla tries to make it as easy as possible +//! to generate compliant PDFs, it is still highly recommended that you familiarize yourself +//! with the PDF specification as well as the specifications for the substandards. This is +//! especially true for standards related to universal accessibility. +//! +//! You can find some requirements below **Requirements** for each [`Validator`]. +//! +//! [`Configuration`]: crate::configure::Configuration + +use std::collections::HashMap; +use std::fmt::Debug; + +use pdf_writer::types::OutputIntentSubtype; +use xmp_writer::pdfa::PdfAExtSchemasWriter; +use xmp_writer::XmpWriter; + +use crate::color::separation::SeparationColorant; +use crate::color::separation::SeparationSpace; +use crate::color::RegularColor; +use crate::configure::PdfVersion; +use crate::interchange::embed::EmbedError; +use crate::surface::Location; +use crate::text::Font; +use crate::text::GlyphId; + +/// An error that occurred during validation/ +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ValidationError { + /// There was a string that was longer than the maximum allowed length (32767). + /// + /// Can for example occur if you set a title or an author that is longer than + /// the given length. + TooLongString, + /// There was a name that was longer than the maximum allowed length (127). + /// + /// Can for example occur if the font name is too long. + TooLongName, + /// There was an array that was longer than the maximum allowed length (8191). + /// Can only occur for PDF 1.4. + /// + /// Can for example occur if a text too long was written. + TooLongArray, + /// There was a dictionary with more entries than the maximum allowed (4095). + /// Can only occur for PDF 1.4. + /// + /// Can for example occur if too many annotations are added to a page. + TooLongDictionary, + /// There was a float that is higher than the maximum allowed (32767). + /// Can only occur for PDF 1.4. + TooLargeFloat, + /// The PDF exceeds the upper limit for indirect objects (8388607). + /// + /// Occurs if the PDF is simply too long. + TooManyIndirectObjects, + /// The PDF contains a content stream that exceeds maximum allowed q/Q nesting level (28). + /// + /// Can only occur if the user stacks many clip paths. + TooHighQNestingLevel, + /// The PDF contains PostScript code, which is forbidden by some export formats. + /// + /// Occurs if a gradient with spread method `Repeat`/`Reflect` or a sweep gradient was used. + ContainsPostScript(Option), + /// No CMYK ICC profile was provided, even though one is necessary. + /// + /// Occurs if the export format requires a device-independent color representation, + /// and a CMYK color was used in the document. + MissingCMYKProfile, + /// The same Separation colorant was used with multiple different fallback colors. + /// + /// Occurs if the user specified multiple Separation color spaces with the same colorant but a different fallback color. + InconsistentSeparationFallback(SeparationColorant), + /// The `.notdef` glyph was used, which is forbidden by some export formats. + /// + /// Can occur if a glyph could not be found in the font for a corresponding codepoint + /// in the input text, or if it was explicitly mapped that way. + /// + /// The third argument contains the text range of the glyph. + ContainsNotDefGlyph(Font, Option, String), + /// A glyph was mapped to no codepoint at all, which is forbidden by some + /// standards. + NoCodepointMapping(Font, GlyphId, Option), + /// A glyph was mapped either to the codepoint 0x0, 0xFEFF or 0xFFFE, which + /// is forbidden by some standards. + /// + /// Can occur if those codepoints appeared in the input text, or were + /// explicitly mapped to that glyph. + InvalidCodepointMapping(Font, GlyphId, char, Option), + /// A glyph was mapped to a codepoint in the Unicode private use area, which is forbidden + /// by some standards, like for example PDF/A-2a. + // Note that the standard doesn't explicitly forbid it, but instead requires an ActualText + // attribute to be present. But we just completely forbid it, for simplicity. + UnicodePrivateArea(Font, GlyphId, char, Option), + /// A font has a license that requires explicit permission of the legal owner for embedding + /// but the standard requires font programs to be legally embeddable for universal rendering. + RestrictedLicense(Font), + /// No document language was set via the metadata, even though it is required + /// by the standard. + NoDocumentLanguage, + /// No title was provided for the document, even though it is required by + /// the standard. + NoDocumentTitle, + /// A figure or formula is missing an alt text. + MissingAltText(Option), + /// A heading is missing a title. + MissingHeadingTitle, + /// The document does not contain an outline. + MissingDocumentOutline, + /// An annotation is missing an alt text. + MissingAnnotationAltText(Option), + /// The date of the document is missing. + // We need this because for some standards we need to add the + // xmp:History attribute. + MissingDocumentDate, + /// The PDF contains transparency, which is forbidden by some standards (e.g. PDF/A-1). + Transparency(Option), + /// The PDF contains an image with `interpolate` set to `true`. + ImageInterpolation(Option), + /// The PDF contains an embedded file. + EmbeddedFile(EmbedError, Option), + /// The PDF contains no tagging. + MissingTagging, + /// The PDF contains another embedded PDF. + /// + /// This is currently forbidden in validated export because we cannot manually verify + /// whether the file actually fulfills all the criteria for the export mode. + EmbeddedPDF(Option), + /// A feature only available in a later PDF version was required. + RequiresNewerPdfVersion(VersionedFeature, Option), +} + +/// Features that may require a later PDF version than the current one. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum VersionedFeature { + /// Tabbing through the document according to the structure order. + StructureOrderTabbing, + /// Header and footer artifact subtypes. + HeaderFooterArtifactSubtypes, + /// Scope attribute for table header cells. + TableHeaderScope, +} + +impl VersionedFeature { + /// Get the minimum PDF version required for this feature. + pub fn minimum_pdf_version(&self) -> PdfVersion { + match self { + VersionedFeature::StructureOrderTabbing => PdfVersion::Pdf15, + VersionedFeature::HeaderFooterArtifactSubtypes => PdfVersion::Pdf17, + VersionedFeature::TableHeaderScope => PdfVersion::Pdf15, + } + } +} + +/// Collection of validators with at most one validator for each standard. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)] +pub struct Validators { + a: Option, + ua: Option, +} + +impl Validators { + /// Returns a filtered `Validators` containing only validators that prohibit the given error, + /// or `None` if no validator prohibits it. + pub fn prohibits(self, error: &ValidationError) -> Option { + let a = self.a.filter(|v| v.prohibits(error)); + let ua = self.ua.filter(|v| v.prohibits(error)); + + let any = a.is_some() || ua.is_some(); + any.then_some(Self { a, ua }) + } + + /// Returns `true` if no validators are set. + pub fn is_empty(self) -> bool { + self.a.is_none() && self.ua.is_none() + } + + /// Returns the number of set validators. + pub fn len(self) -> usize { + (if self.a.is_some() { 1 } else { 0 }) + (if self.ua.is_some() { 1 } else { 0 }) + } + + /// Returns the PDF/A validator, if set. + pub fn archival(self) -> Option { + self.a + } + + /// Returns the PDF/UA accessibility validator, if set. + pub fn accessibility(self) -> Option { + self.ua + } + + /// Whether the font must supply valid Unicode code points for each of the + /// drawn glyphs. + pub(crate) fn requires_codepoint_mappings(self) -> bool { + self.into_iter().any(Validator::requires_codepoint_mappings) + } + + /// Force the `DisplayDocTitle` flag set. + pub(crate) fn requires_display_doc_title(self) -> bool { + self.ua + .is_some_and(Accessibility::requires_display_doc_title) + } + + /// Force sRGB profiles for `DeviceGray` and `DeviceRgb` colorspaces. + pub(crate) fn requires_no_device_cs(self) -> bool { + self.a.is_some_and(Archival::requires_no_device_cs) + } + + /// Force the `Print` flag set and the `Hidden`, `Invisible`, + /// `ToggleNoView`, and `NoView` flags unset. + pub(crate) fn requires_annotation_flags(self) -> bool { + self.a.is_some_and(Archival::requires_annotation_flags) + } + + /// Whether Tagged PDF must be enabled. + pub(crate) fn requires_tagging(self) -> bool { + self.into_iter().any(Validator::requires_tagging) + } + + /// Whether XMP metadata must be written. + pub(crate) fn requires_xmp_metadata(self) -> bool { + self.into_iter().any(Validator::requires_xmp_metadata) + } + + /// Whether any extension schemata should be descibed using the "pdfaSchema" + /// namespace. + pub(crate) fn requires_xmp_metadata_extension_schema(self) -> bool { + self.a + .is_some_and(Archival::requires_xmp_metadata_extension_schema) + } + + /// Whether the `instanceID` field is allowed in XMP. + pub(crate) fn prohibits_instance_id_in_xmp_metadata(self) -> bool { + self.a + .is_some_and(Archival::prohibits_instance_id_in_xmp_metadata) + } + + /// Whether the xmpMM:History entry is required. + pub(crate) fn requires_file_provenance_information(self) -> bool { + self.a + .is_some_and(Archival::requires_file_provenance_information) + } + + /// Whether the `/Info` dictionary is allowed in the file trailer. + pub(crate) fn prohibits_info_dict(self) -> bool { + self.a.is_some_and(Archival::prohibits_info_dict) + } + + /// Whether a non-printable file header is mandatory. + pub(crate) fn requires_binary_header(self) -> bool { + self.a.is_some_and(Archival::requires_binary_header) + } + + /// Whether the `EmbeddedFiles` key in the name dictionary of the document + /// catalog dictionary should be written even if empty. + pub(crate) fn requires_embedded_files_when_empty(self) -> bool { + self.a + .is_some_and(Archival::requires_embedded_files_when_empty) + } + + /// Whether any of these standards explicitly specifies the `/AF` key. + /// + /// The `/AF` key may be supported by the underlying PDF version instead: + /// Starting at PDF 2.0, the key is specified by ISO 32000 and does not need + /// to be added by PDF/A. + pub(crate) fn specifies_associated_files(self) -> bool { + self.a.is_some_and(Archival::specifies_associated_files) + } + + pub(crate) fn output_intent(self) -> Option> { + self.a.map(Archival::output_intent) + } + + pub(crate) fn write_xmp(self, xmp: &mut XmpWriter) { + if self.requires_xmp_metadata_extension_schema() { + let mut extension_schemas = xmp.extension_schemas(); + if let Some(a) = self.a { + a.write_xmp_extension_schema_description(&mut extension_schemas); + } + if let Some(ua) = self.ua { + ua.write_xmp_extension_schema_description(&mut extension_schemas); + } + } + + if let Some(a) = self.a { + a.write_xmp(xmp); + } + + if let Some(ua) = self.ua { + ua.write_xmp(xmp); + } + } + + /// Returns the maximum PDF version allowed by all active validators. + pub fn max(self) -> PdfVersion { + self.a + .map_or(PdfVersion::MAX, |v| v.max()) + .min(self.ua.map_or(PdfVersion::MAX, |v| v.max())) + } + + /// Returns the minimum PDF version required by all active validators, if any. + pub fn min(self) -> Option { + self.a + .and_then(|v| v.min()) + .max(self.ua.and_then(|v| v.min())) + } +} + +impl IntoIterator for Validators { + type Item = Validator; + type IntoIter = std::iter::Flatten, 2>>; + + fn into_iter(self) -> Self::IntoIter { + [self.a.map(Validator::A), self.ua.map(Validator::Ua)] + .into_iter() + .flatten() + } +} + +/// A builder for constructing a [`Validators`] collection. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)] +pub struct ValidatorsBuilder(Validators); + +impl ValidatorsBuilder { + /// Set a validator, overwriting the current one if the same standard family is already set. + pub fn set_validator(self, validator: Validator) -> Self { + match validator { + Validator::A(a) => self.with_archival_validator(a), + Validator::Ua(ua) => self.with_accessibility_validator(ua), + } + } + + /// Set the PDF/A validator, overwriting the current one if already set. + pub fn with_archival_validator(mut self, archival: Archival) -> Self { + self.0.a = Some(archival); + self + } + + /// Set the PDF/UA accessibility validator, overwriting the current one if already set. + pub fn with_accessibility_validator(mut self, accessibility: Accessibility) -> Self { + self.0.ua = Some(accessibility); + self + } + + pub(crate) fn finish(self) -> Result { + let min = self.0.min().unwrap_or(PdfVersion::MIN); + let max = self.0.max(); + + if min > max { + Err(self.0) + } else { + Ok(self.0) + } + } +} + +/// A PDF validator for a specific conformance standard. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Validator { + /// A PDF/A validator. + A(Archival), + /// A PDF/UA accessibility validator. + Ua(Accessibility), +} + +impl Validator { + fn requires_codepoint_mappings(self) -> bool { + match self { + Self::A(a) => a.requires_codepoint_mappings(), + Self::Ua(ua) => ua.requires_codepoint_mappings(), + } + } + + fn requires_tagging(self) -> bool { + match self { + Self::A(a) => a.requires_tagging(), + Self::Ua(ua) => ua.requires_tagging(), + } + } + + fn requires_xmp_metadata(self) -> bool { + match self { + Self::A(a) => a.requires_xmp_metadata(), + Self::Ua(ua) => ua.requires_xmp_metadata(), + } + } + + /// Minimum PDF version required to use this validator, if any. + pub fn min(self) -> Option { + match self { + Self::A(a) => a.min(), + Self::Ua(ua) => ua.min(), + } + } + + /// Maximum PDF version this standard can be used with. + pub fn max(self) -> PdfVersion { + match self { + Self::A(a) => a.max(), + Self::Ua(ua) => ua.max(), + } + } + + /// Returns a human-readable string representation of the validator. + pub fn as_str(self) -> &'static str { + match self { + Self::A(a) => a.as_str(), + Self::Ua(ua) => ua.as_str(), + } + } +} + +impl From for Validator { + fn from(a: Archival) -> Self { + Self::A(a) + } +} + +impl From for Validator { + fn from(ua: Accessibility) -> Self { + Self::Ua(ua) + } +} + +/// A PDF/A conformance level. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[allow(non_camel_case_types)] +pub enum Archival { + /// The validator for the PDF/A-1a standard. + /// + /// **Requirements**: + /// - All requirements of PDF/A-1b. + /// - You need to follow all requirements outlined in the _Other Notes_ section of the + /// [`tagging`] module. + /// - You need to follow all best practices when using [tags](`crate::interchange::tagging::Tag`), as outlined in the documentation + /// of each tag. + /// - Artifacts such as page numbers, backgrounds, cut marks and color bars should be specified + /// correspondingly as artifacts. + /// - Word boundaries need to be explicitly specified with a space. The same applies to words at + /// the end of a line that are not followed by punctuation. + /// - To the fullest extent possible, the logical structure of the document should be encoded + /// correspondingly in the tag tree using appropriate grouping tags. + /// - Language identifiers used must be valid according to RFC 3066. + /// - You should provide an alternate text to span content tags, if applicable. + /// - You should provide the expansion of abbreviations to span content tags, if applicable. + /// + /// [`tagging`]: crate::interchange::tagging + A1_A, + /// The validator for the PDF/A-1b standard. + /// + /// **Requirements**: - + A1_B, + /// The validator for the PDF/A-2a standard. + /// + /// **Requirements**: + /// - All requirements of PDF/A-2b. + /// - You need to follow all requirements outlined in the _Other Notes_ section of the + /// [`tagging`] module. + /// - You need to follow all best practices when using [tags](`crate::interchange::tagging::Tag`), as outlined in the documentation + /// of each tag. + /// - Artifacts such as page numbers, backgrounds, cut marks and color bars should be specified + /// correspondingly as artifacts. + /// - Word boundaries need to be explicitly specified with a space. The same applies to words at + /// the end of a line that are not followed by punctuation. + /// - To the fullest extent possible, the logical structure of the document should be encoded + /// correspondingly in the tag tree using appropriate grouping tags. + /// - Language identifiers used must be valid according to RFC 3066. + /// - You should provide an alternate text to span content tags, if applicable. + /// - You should provide the expansion of abbreviations to span content tags, if applicable. + /// + /// [`tagging`]: crate::interchange::tagging + A2_A, + /// The validator for the PDF/A-2b standard. + /// + /// **Requirements**: + /// - You should only use fonts that are legally embeddable in a file for unlimited, + /// universal rendering. + A2_B, + /// The validator for the PDF/A-2u standard. + /// + /// **Requirements**: + /// - All requirements of PDF/A-2b + A2_U, + /// The validator for the PDF/A-3a standard. + /// + /// **Requirements**: + /// - All requirements of PDF/A-2a + A3_A, + /// The validator for the PDF/A-3b standard. + /// + /// **Requirements**: + /// - All requirements of PDF/A-2b + A3_B, + /// The validator for the PDF/A-3u standard. + /// + /// **Requirements**: + /// - All requirements of PDF/A-2b + A3_U, + /// The validator for the PDF/A-4 standard. + /// + /// **Requirements**: + /// - While not required, it's recommended to enable tagging. + A4, + /// The validator for the PDF/A-4f standard. + /// + /// **Requirements**: + /// - All requirements of PDF/A-4 + A4F, + /// The validator for the PDF/A-4e standard. + /// + /// **Requirements**: + /// - All requirements of PDF/A-4 + A4E, +} + +impl Archival { + fn prohibits(self, error: &ValidationError) -> bool { + match (self, error) { + // Forbidden under all PDF/A-1 profiles. + ( + Self::A1_A | Self::A1_B, + ValidationError::TooLongString + | ValidationError::TooLongName + | ValidationError::TooLongArray + | ValidationError::TooLongDictionary + | ValidationError::TooLargeFloat + | ValidationError::TooManyIndirectObjects + | ValidationError::TooHighQNestingLevel + | ValidationError::ContainsPostScript(_) + | ValidationError::MissingCMYKProfile + | ValidationError::RestrictedLicense(_) + | ValidationError::MissingDocumentDate + | ValidationError::Transparency(_) + | ValidationError::ImageInterpolation(_) + | ValidationError::EmbeddedFile(EmbedError::Existence, _) + | ValidationError::EmbeddedPDF(_), + ) => true, + // Allowed under all PDF/A-1 profiles. + ( + Self::A1_A | Self::A1_B, + ValidationError::InconsistentSeparationFallback(_) + | ValidationError::InvalidCodepointMapping(_, _, _, _) + | ValidationError::UnicodePrivateArea(_, _, _, _) + | ValidationError::NoDocumentTitle + | ValidationError::MissingHeadingTitle + | ValidationError::MissingDocumentOutline + | ValidationError::EmbeddedFile(_, _) + | ValidationError::RequiresNewerPdfVersion( + VersionedFeature::HeaderFooterArtifactSubtypes + | VersionedFeature::StructureOrderTabbing + | VersionedFeature::TableHeaderScope, + _, + ), + ) => false, + // Forbidden under PDF/A-1a but allowed under PDF/A-1b. + ( + Self::A1_A | Self::A1_B, + ValidationError::ContainsNotDefGlyph(_, _, _) + | ValidationError::NoCodepointMapping(_, _, _) + | ValidationError::NoDocumentLanguage + | ValidationError::MissingAltText(_) + | ValidationError::MissingAnnotationAltText(_) + | ValidationError::MissingTagging, + ) => self == Self::A1_A, + + // Forbidden under all PDF/A-2 and PDF/A-3 profiles. + ( + Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U, + ValidationError::TooLongString + | ValidationError::TooLongName + | ValidationError::TooManyIndirectObjects + | ValidationError::TooHighQNestingLevel + | ValidationError::ContainsPostScript(_) + | ValidationError::MissingCMYKProfile + | ValidationError::InconsistentSeparationFallback(_) + | ValidationError::ContainsNotDefGlyph(_, _, _) + | ValidationError::RestrictedLicense(_) + | ValidationError::MissingDocumentDate + | ValidationError::ImageInterpolation(_) + | ValidationError::EmbeddedPDF(_), + ) => true, + // Allowed under all PDF/A-2 and PDF/A-3 profiles. + ( + Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U, + ValidationError::TooLongArray + | ValidationError::TooLongDictionary + | ValidationError::TooLargeFloat + | ValidationError::NoDocumentTitle + | ValidationError::Transparency(_) + | ValidationError::MissingHeadingTitle + | ValidationError::MissingDocumentOutline + | ValidationError::RequiresNewerPdfVersion( + VersionedFeature::HeaderFooterArtifactSubtypes + | VersionedFeature::StructureOrderTabbing + | VersionedFeature::TableHeaderScope, + _, + ), + ) => false, + // Forbidden under PDF/A-2 but allowed under PDF/A-3. + ( + Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U, + ValidationError::EmbeddedFile(EmbedError::Existence, _), + ) => self == Self::A2_A || self == Self::A2_B || self == Self::A2_U, + // Forbidden under PDF/A-3 but allowed under PDF/A-2. + ( + Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U, + ValidationError::EmbeddedFile( + EmbedError::MissingDate + | EmbedError::MissingDescription + | EmbedError::MissingMimeType, + _, + ), + ) => self == Self::A3_A || self == Self::A3_B || self == Self::A3_U, + // Forbidden under PDF/A-2 and PDF/A-3 accessible profiles. + ( + Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U, + ValidationError::UnicodePrivateArea(_, _, _, _) + | ValidationError::NoDocumentLanguage + | ValidationError::MissingAltText(_) + | ValidationError::MissingAnnotationAltText(_) + | ValidationError::MissingTagging, + ) => self == Self::A2_A || self == Self::A3_A, + // Forbidden under PDF/A-2 and PDF/A-3 accessible and Unicode profiles. + ( + Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U, + ValidationError::NoCodepointMapping(_, _, _) + | ValidationError::InvalidCodepointMapping(_, _, _, _), + ) => { + self == Self::A2_A || self == Self::A2_U || self == Self::A3_A || self == Self::A3_U + } + + // Forbidden under all PDF/A-4 profiles. + ( + Self::A4 | Self::A4F | Self::A4E, + ValidationError::MissingCMYKProfile + | ValidationError::InconsistentSeparationFallback(_) + | ValidationError::ContainsNotDefGlyph(_, _, _) + | ValidationError::NoCodepointMapping(_, _, _) + | ValidationError::InvalidCodepointMapping(_, _, _, _) + | ValidationError::UnicodePrivateArea(_, _, _, _) + | ValidationError::RestrictedLicense(_) + | ValidationError::MissingDocumentDate + | ValidationError::ImageInterpolation(_) + | ValidationError::EmbeddedPDF(_), + ) => true, + // Allowed under all PDF/A-4 profiles. + ( + Self::A4 | Self::A4F | Self::A4E, + ValidationError::TooLongString + | ValidationError::TooLongName + | ValidationError::TooLongArray + | ValidationError::TooLongDictionary + | ValidationError::TooLargeFloat + | ValidationError::TooManyIndirectObjects + | ValidationError::TooHighQNestingLevel + | ValidationError::ContainsPostScript(_) + | ValidationError::NoDocumentLanguage + | ValidationError::NoDocumentTitle + | ValidationError::MissingAltText(_) + | ValidationError::MissingHeadingTitle + | ValidationError::MissingDocumentOutline + | ValidationError::MissingAnnotationAltText(_) + | ValidationError::Transparency(_) + | ValidationError::EmbeddedFile( + EmbedError::MissingDate | EmbedError::MissingMimeType, + _, + ) + | ValidationError::MissingTagging + | ValidationError::RequiresNewerPdfVersion( + VersionedFeature::HeaderFooterArtifactSubtypes + | VersionedFeature::StructureOrderTabbing + | VersionedFeature::TableHeaderScope, + _, + ), + ) => false, + // Forbidden under PDF/A-4 but allowed under other PDF/A-4 profiles. + ( + Self::A4 | Self::A4F | Self::A4E, + ValidationError::EmbeddedFile(EmbedError::Existence, _), + ) => self == Self::A4, + // Allowed under PDF/A-4 but forbidden under other profiles. + ( + Self::A4 | Self::A4F | Self::A4E, + ValidationError::EmbeddedFile(EmbedError::MissingDescription, _), + ) => self == Self::A4, + } + } + + fn requires_codepoint_mappings(self) -> bool { + match self { + Self::A1_A + | Self::A2_A + | Self::A2_U + | Self::A3_A + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => true, + Self::A1_B | Self::A2_B | Self::A3_B => false, + } + } + + fn requires_no_device_cs(self) -> bool { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => true, + } + } + + fn requires_annotation_flags(self) -> bool { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => true, + } + } + + fn requires_tagging(self) -> bool { + match self { + Self::A1_A | Self::A2_A | Self::A3_A => true, + Self::A1_B + | Self::A2_B + | Self::A2_U + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => false, + } + } + + fn requires_xmp_metadata(self) -> bool { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => true, + } + } + + fn requires_xmp_metadata_extension_schema(self) -> bool { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U => true, + // Clause 6.7.2.3 of PDF/A-4 recommends ("should") a RELAX NG + // definition of its metadata contents to be embedded as an + // associated file. It no longer uses the inline schema definition + // using the "pdfaSchema" namespaces for extension schemata. + Self::A4 | Self::A4F | Self::A4E => false, + } + } + + fn prohibits_instance_id_in_xmp_metadata(self) -> bool { + match self { + Self::A1_A | Self::A1_B => true, + Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => false, + } + } + + fn requires_file_provenance_information(self) -> bool { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => true, + } + } + + fn prohibits_info_dict(self) -> bool { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U => false, + Self::A4 | Self::A4F | Self::A4E => true, + } + } + + fn requires_binary_header(self) -> bool { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => true, + } + } + + fn requires_embedded_files_when_empty(self) -> bool { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4E => false, + Self::A4F => true, + } + } + + /// Whether this standard explicitly specifies the `/AF` key. + /// + /// The `/AF` key may be supported by the underlying PDF version instead: + /// Starting at PDF 2.0, the key is specified by ISO 32000 and does not need + /// to be added by PDF/A. + fn specifies_associated_files(self) -> bool { + match self { + Self::A3_A | Self::A3_B | Self::A3_U => true, + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A4 + | Self::A4F + | Self::A4E => false, + } + } + + fn output_intent(self) -> OutputIntentSubtype<'static> { + match self { + Self::A1_A + | Self::A1_B + | Self::A2_A + | Self::A2_B + | Self::A2_U + | Self::A3_A + | Self::A3_B + | Self::A3_U + | Self::A4 + | Self::A4F + | Self::A4E => OutputIntentSubtype::PDFA, + } + } + + fn write_xmp(self, xmp: &mut XmpWriter) { + match self { + Self::A1_A => { + xmp.pdfa_part(1); + xmp.pdfa_conformance("A"); + } + Self::A1_B => { + xmp.pdfa_part(1); + xmp.pdfa_conformance("B"); + } + Self::A2_A => { + xmp.pdfa_part(2); + xmp.pdfa_conformance("A"); + } + Self::A2_B => { + xmp.pdfa_part(2); + xmp.pdfa_conformance("B"); + } + Self::A2_U => { + xmp.pdfa_part(2); + xmp.pdfa_conformance("U"); + } + Self::A3_A => { + xmp.pdfa_part(3); + xmp.pdfa_conformance("A"); + } + Self::A3_B => { + xmp.pdfa_part(3); + xmp.pdfa_conformance("B"); + } + Self::A3_U => { + xmp.pdfa_part(3); + xmp.pdfa_conformance("U"); + } + Self::A4 => { + xmp.pdfa_part(4); + xmp.pdfa_rev(2020); + } + Self::A4F => { + xmp.pdfa_part(4); + xmp.pdfa_rev(2020); + xmp.pdfa_conformance("F"); + } + Self::A4E => { + xmp.pdfa_part(4); + xmp.pdfa_rev(2020); + xmp.pdfa_conformance("E"); + } + } + } + + fn write_xmp_extension_schema_description( + self, + extension_schemas: &mut PdfAExtSchemasWriter<'_, '_>, + ) { + if !self.requires_xmp_metadata_extension_schema() { + return; + } + + extension_schemas + .xmp_media_management() + .properties() + .describe_instance_id(); + extension_schemas.pdf().properties().describe_all(); + } + + /// Returns a human-readable string representation of the conformance level. + pub fn as_str(self) -> &'static str { + match self { + Self::A1_A => "PDF/A-1a", + Self::A1_B => "PDF/A-1b", + Self::A2_A => "PDF/A-2a", + Self::A2_B => "PDF/A-2b", + Self::A2_U => "PDF/A-2u", + Self::A3_A => "PDF/A-3a", + Self::A3_B => "PDF/A-3b", + Self::A3_U => "PDF/A-3u", + Self::A4 => "PDF/A-4", + Self::A4F => "PDF/A-4f", + Self::A4E => "PDF/A-4e", + } + } + + /// Minimum PDF version required to use this standard, if any. + pub const fn min(self) -> Option { + match self { + // PDF/A-1 through 3 require XMP `/Metadata` streams, which require PDF 1.4. + Self::A1_A | Self::A1_B => Some(PdfVersion::Pdf14), + Self::A2_A | Self::A2_B | Self::A2_U => Some(PdfVersion::Pdf14), + Self::A3_A | Self::A3_B | Self::A3_U => Some(PdfVersion::Pdf14), + Self::A4 | Self::A4F | Self::A4E => Some(PdfVersion::Pdf20), + } + } + + /// Maximum PDF version this standard can be used with. + pub const fn max(self) -> PdfVersion { + match self { + Self::A1_A | Self::A1_B => PdfVersion::Pdf14, + Self::A2_A | Self::A2_B | Self::A2_U | Self::A3_A | Self::A3_B | Self::A3_U => { + PdfVersion::Pdf17 + } + Self::A4 | Self::A4F | Self::A4E => PdfVersion::Pdf20, + } + } +} + +/// A validator for exporting PDF documents to a specific subset of PDF. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[allow(non_camel_case_types)] +pub enum Accessibility { + /// The validator for the PDF/UA-1 standard. + /// + /// **Requirements**: + /// + /// General: + /// - All real content should be tagged accordingly. + /// - All artifacts should be marked accordingly. + /// - The tag tree should reflect the logical reading order of the + /// document. + /// - Information should not be conveyed by contrast, color, format + /// or layout. + /// - All "best practice" notes in [`TagKind`] need to be complied with. + /// + /// Text: + /// - You should make use of the `Alt`, `ActualText`, `Lang` and `Expansion` attributes + /// whenever possible. + /// - Usually, you can provide an empty string as `Lang` to indicate that a language is unknown. + /// You should not do that in PDF/UA. + /// - Stretchable characters (such as brackets, which often consist of several glyphs) + /// should be marked accordingly with `ActualText`. + /// + /// Graphics: + /// - Graphics should be tagged as figures (unless they are an artifact). + /// - Graphics need to be followed by a caption. + /// - Graphics that possess semantic values only in combination with other graphics + /// should be tagged with a single Figure tag for each figure. + /// - If a more accessible representation exists, it should be used over graphics. + /// + /// Headings: + /// - Headings should be tagged as such. + /// - For not strongly structured documents, H1 should be the first + /// heading. + /// + /// Tables: + /// - Tables should include headers and be tagged accordingly. + /// - Tables should only be used to represent content within logical row/column relationship. + /// + /// Lists: + /// - List items should be tagged with Li tags, if necessary also with + /// Lbl and LBody tags. + /// - Lists should only be used when the content is intended to be read + /// as a list. + /// + /// Mathematical expressions: + /// - All mathematical expressions should be enclosed with + /// a `Formula` tag. + /// + /// Headers and footers: + /// - Headers and footers should be marked as corresponding + /// artifacts. + /// + /// Notes and references: + /// - Footnotes, endnotes, note labels and references should be + /// tagged accordingly and use tagged annotations. + /// - Footnotes and end notes should use the `Note` tag. + /// + /// Navigation: + /// - The document must contain an outline, and it should reflect + /// the reading order of the document. + /// - Page labels should be semantically appropriate. + /// + /// Annotations: + /// - Annotations should be present in the tag tree in the correct + /// reading order. + /// + /// Fonts: + /// - You should only use fonts that are legally embeddable in a file for unlimited, + /// universal rendering. + /// + /// [`TagKind`]: crate::interchange::tagging::TagKind + UA1, +} + +impl Accessibility { + fn prohibits(self, error: &ValidationError) -> bool { + match (self, error) { + ( + Self::UA1, + ValidationError::ContainsNotDefGlyph(_, _, _) + | ValidationError::NoCodepointMapping(_, _, _) + | ValidationError::InvalidCodepointMapping(_, _, _, _) + | ValidationError::RestrictedLicense(_) + | ValidationError::NoDocumentTitle + | ValidationError::MissingAltText(_) + | ValidationError::MissingHeadingTitle + | ValidationError::MissingDocumentOutline + | ValidationError::MissingAnnotationAltText(_) + | ValidationError::EmbeddedFile(EmbedError::MissingDescription, _) + | ValidationError::MissingTagging + | ValidationError::EmbeddedPDF(_) + | ValidationError::RequiresNewerPdfVersion( + VersionedFeature::HeaderFooterArtifactSubtypes + | VersionedFeature::StructureOrderTabbing + | VersionedFeature::TableHeaderScope, + _, + ), + ) => true, + ( + Self::UA1, + ValidationError::TooLongString + | ValidationError::TooLongName + | ValidationError::TooLongArray + | ValidationError::TooLongDictionary + | ValidationError::TooLargeFloat + | ValidationError::TooManyIndirectObjects + | ValidationError::TooHighQNestingLevel + | ValidationError::ContainsPostScript(_) + | ValidationError::MissingCMYKProfile + | ValidationError::InconsistentSeparationFallback(_) + | ValidationError::UnicodePrivateArea(_, _, _, _) + | ValidationError::NoDocumentLanguage + | ValidationError::Transparency(_) + | ValidationError::ImageInterpolation(_) + | ValidationError::EmbeddedFile( + EmbedError::Existence | EmbedError::MissingDate | EmbedError::MissingMimeType, + _, + ) + | ValidationError::MissingDocumentDate, + ) => false, + } + } + + fn requires_codepoint_mappings(self) -> bool { + match self { + Self::UA1 => true, + } + } + + fn requires_display_doc_title(self) -> bool { + match self { + Self::UA1 => true, + } + } + + const fn requires_tagging(self) -> bool { + true + } + + fn requires_xmp_metadata(self) -> bool { + match self { + Self::UA1 => true, + } + } + + fn write_xmp(self, xmp: &mut XmpWriter) { + match self { + Self::UA1 => { + xmp.pdfua_part(1); + } + } + } + + fn write_xmp_extension_schema_description( + self, + extension_schemas: &mut PdfAExtSchemasWriter<'_, '_>, + ) { + // Needs to be updated if [`Self::write_xmp`] gains more properties. + extension_schemas.pdfua_id().properties().describe_part(); + } + + /// Returns a human-readable string representation of the accessibility level. + pub fn as_str(self) -> &'static str { + match self { + Self::UA1 => "PDF/UA-1", + } + } + + /// Minimum PDF version required to use this standard, if any. + pub const fn min(self) -> Option { + match self { + // PDF/UA-1 requires Tagged PDF and XMP `/Metadata` streams, which both require PDF 1.4. + Self::UA1 => Some(PdfVersion::Pdf14), + } + } + + /// Maximum PDF version this standard can be used with. + pub const fn max(self) -> PdfVersion { + match self { + // PDF/UA-1 is specified against PDF 1.7. + Self::UA1 => PdfVersion::Pdf17, + } + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub(crate) struct ValidationStore { + /// Maps from the name of a Separation colorant to a hash of its fallback + /// color. Used to track that a name is only ever matched with a single + /// fallback color. Since Krilla manages the `tintTransform` functions, + /// those are always equivalent. + separation_fallback_map: HashMap, +} + +impl ValidationStore { + pub(crate) fn new() -> Self { + Default::default() + } + + /// Register a colorant and its fallback and raise an error if it already + /// exists. + pub(crate) fn validate_separation( + &mut self, + separation: &SeparationSpace, + ) -> Result<(), ValidationError> { + if self + .separation_fallback_map + .entry(separation.colorant.clone()) + .or_insert(separation.fallback) + == &separation.fallback + { + Ok(()) + } else { + Err(ValidationError::InconsistentSeparationFallback( + separation.colorant.clone(), + )) + } + } +} diff --git a/vendor/krilla/src/configure/version.rs b/vendor/krilla/src/configure/version.rs new file mode 100644 index 00000000..d16b09ee --- /dev/null +++ b/vendor/krilla/src/configure/version.rs @@ -0,0 +1,134 @@ +use pdf_writer::Pdf; +use std::sync::LazyLock; +use xmp_writer::XmpWriter; + +#[cfg(feature = "raster-images")] +use crate::graphics::icc::ICCMetadata; +use crate::graphics::icc::ICCProfile; +#[cfg(feature = "raster-images")] +use crate::image::BitsPerComponent; + +/// The version of a PDF document. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum PdfVersion { + /// PDF 1.4. + Pdf14, + /// PDF 1.5. + Pdf15, + /// PDF 1.6. + Pdf16, + /// PDF 1.7. + #[default] + Pdf17, + /// PDF 2.0. + Pdf20, +} + +impl PdfVersion { + /// The minimum supported PDF version. + pub const MIN: Self = Self::Pdf14; + /// The maximum supported PDF version. + pub const MAX: Self = Self::Pdf20; + + pub(crate) fn write_xmp(&self, xmp: &mut XmpWriter) { + match self { + PdfVersion::Pdf14 => xmp.pdf_version("1.4"), + PdfVersion::Pdf15 => xmp.pdf_version("1.5"), + PdfVersion::Pdf16 => xmp.pdf_version("1.6"), + PdfVersion::Pdf17 => xmp.pdf_version("1.7"), + PdfVersion::Pdf20 => xmp.pdf_version("2.0"), + }; + } + + /// Get a string representation of the PDF version. + pub fn as_str(self) -> &'static str { + match self { + PdfVersion::Pdf14 => "PDF 1.4", + PdfVersion::Pdf15 => "PDF 1.5", + PdfVersion::Pdf16 => "PDF 1.6", + PdfVersion::Pdf17 => "PDF 1.7", + PdfVersion::Pdf20 => "PDF 2.0", + } + } + + pub(crate) fn rgb_icc(&self) -> ICCProfile<3> { + match self { + PdfVersion::Pdf14 => SRGB_V2_ICC.clone(), + PdfVersion::Pdf15 => SRGB_V2_ICC.clone(), + PdfVersion::Pdf16 => SRGB_V2_ICC.clone(), + PdfVersion::Pdf17 => SRGB_V4_ICC.clone(), + PdfVersion::Pdf20 => SRGB_V4_ICC.clone(), + } + } + + pub(crate) fn grey_icc(&self) -> ICCProfile<1> { + match self { + PdfVersion::Pdf14 => GREY_V2_ICC.clone(), + PdfVersion::Pdf15 => GREY_V2_ICC.clone(), + PdfVersion::Pdf16 => GREY_V2_ICC.clone(), + PdfVersion::Pdf17 => GREY_V4_ICC.clone(), + PdfVersion::Pdf20 => GREY_V4_ICC.clone(), + } + } + + #[cfg(feature = "raster-images")] + pub(crate) fn supports_icc(&self, metadata: &ICCMetadata) -> bool { + match self { + PdfVersion::Pdf14 => metadata.major <= 2 && metadata.minor <= 2, + PdfVersion::Pdf15 => metadata.major <= 4, + PdfVersion::Pdf16 => metadata.major <= 4 && metadata.minor <= 1, + PdfVersion::Pdf17 => metadata.major <= 4 && metadata.minor <= 2, + PdfVersion::Pdf20 => metadata.major <= 4 && metadata.minor <= 2, + } + } + + #[cfg(feature = "raster-images")] + pub(crate) fn supports_bit_depth(&self, bits_per_component: BitsPerComponent) -> bool { + match bits_per_component { + BitsPerComponent::Eight => true, + BitsPerComponent::Sixteen => *self >= PdfVersion::Pdf15, + } + } + + /// Whether the version supports the `/AF` key. + /// + /// Note that support for the `/AF` key may instead be provided by the + /// PDF/A-3 standard for 1.x series files. + pub(crate) fn specifies_associated_files(&self) -> bool { + match self { + PdfVersion::Pdf14 | PdfVersion::Pdf15 | PdfVersion::Pdf16 | PdfVersion::Pdf17 => false, + PdfVersion::Pdf20 => true, + } + } + + pub(crate) fn set_version(&self, pdf: &mut Pdf) { + match self { + PdfVersion::Pdf14 => pdf.set_version(1, 4), + PdfVersion::Pdf15 => pdf.set_version(1, 5), + PdfVersion::Pdf16 => pdf.set_version(1, 6), + PdfVersion::Pdf17 => pdf.set_version(1, 7), + PdfVersion::Pdf20 => pdf.set_version(2, 0), + }; + } + + pub(crate) fn deprecates_proc_sets(&self) -> bool { + *self >= PdfVersion::Pdf20 + } + + pub(crate) fn deprecates_cid_set(&self) -> bool { + *self >= PdfVersion::Pdf20 + } +} + +/// The ICC v4 profile for the SRGB color space. +static SRGB_V4_ICC: LazyLock> = + LazyLock::new(|| ICCProfile::new(include_bytes!("../../icc/sRGB-v4.icc")).unwrap()); +/// The ICC v2 profile for the SRGB color space. +static SRGB_V2_ICC: LazyLock> = + LazyLock::new(|| ICCProfile::new(include_bytes!("../../icc/sRGB-v2-magic.icc")).unwrap()); +/// The ICC v4 profile for the sgray color space. +static GREY_V4_ICC: LazyLock> = + LazyLock::new(|| ICCProfile::new(include_bytes!("../../icc/sGrey-v4.icc")).unwrap()); +/// The ICC v2 profile for the sgray color space. +static GREY_V2_ICC: LazyLock> = + LazyLock::new(|| ICCProfile::new(include_bytes!("../../icc/sGrey-v2-magic.icc")).unwrap()); diff --git a/vendor/krilla/src/content.rs b/vendor/krilla/src/content.rs new file mode 100644 index 00000000..dd5b1cb4 --- /dev/null +++ b/vendor/krilla/src/content.rs @@ -0,0 +1,1352 @@ +//! A low-level abstraction over a single content stream. + +use std::cell::RefCell; +use std::collections::HashSet; +use std::rc::Rc; +use std::sync::Arc; + +use float_cmp::approx_eq; +use pdf_writer::types::TextRenderingMode; +use pdf_writer::{Content, Finish, Name, Ref, Str, TextStr}; +use tiny_skia_path::{Path, PathSegment, PathVerb}; + +use crate::chunk_container::ChunkContainer; +use crate::color::rgb; +use crate::configure::ValidationError; +#[cfg(feature = "raster-images")] +use crate::geom::Size; +use crate::geom::{Point, Rect, Transform}; +use crate::graphics::color::{Color, ColorSpace}; +use crate::graphics::graphics_state::{ExtGState, GraphicsStates}; +#[cfg(feature = "raster-images")] +use crate::graphics::image::Image; +use crate::graphics::mask::Mask; +use crate::graphics::paint::{Fill, FillRule, InnerPaint, LineCap, LineJoin, Paint, Stroke}; +use crate::graphics::shading_function::{ + GradientProperties, GradientPropertiesExt, ShadingFunction, +}; +use crate::graphics::shading_pattern::ShadingPattern; +use crate::graphics::tiling_pattern::TilingPattern; +use crate::graphics::xobject::XObject; +use crate::interchange::tagging::ContentTag; +use crate::num::NormalizedF32; +use crate::resource; +use crate::resource::{Resource, ResourceDictionaryBuilder}; +use crate::serialize::{MaybeDeviceColorSpace, SerializeContext}; +use crate::stream::Stream; +use crate::text::group::{use_text_spanner, GlyphGroup, GlyphGrouper, GlyphSpan, GlyphSpanner}; +use crate::text::type3::ColoredGlyph; +use crate::text::{Font, FontContainer, FontIdentifier, PdfFont, PDF_UNITS_PER_EM}; +use crate::text::{Glyph, GlyphId}; +use crate::util::{calculate_stroke_bbox, NameExt}; + +pub(crate) struct ContentBuilder { + rd_builder: ResourceDictionaryBuilder, + content: Content, + validation_errors: HashSet, + root_transform: Transform, + graphics_states: GraphicsStates, + bbox: Option, + uses_mask: bool, + // Calculating the bbox of text is expensive, so we should avoid doing it if not needed. + // The only time we really need it is if we are currently inside of an XObject, where we + // need to provide a bbox of all its contents. If we are on the main page stream, we only + // need it if automatic size detection is enabled. + bbox_important: bool, + /// A temporary buffer that's reused across the builder. + scratch: Vec, + pub(crate) active_marked_content: bool, +} + +/// Stores either a device-specific color space, +/// or the name of a different colorspace (e.g. ICCBased) stored in +/// the current resource dictionary +enum ContentColorSpace { + Device, + Named(String), +} + +impl ContentBuilder { + pub(crate) fn new( + root_transform: Transform, + bbox_important: bool, + sc: &SerializeContext, + ) -> Self { + Self { + rd_builder: ResourceDictionaryBuilder::new(), + validation_errors: HashSet::new(), + content: sc.new_content(), + root_transform, + uses_mask: false, + bbox_important, + graphics_states: GraphicsStates::new(), + bbox: None, + scratch: Vec::new(), + active_marked_content: false, + } + } + + pub(crate) fn content_save_state(&mut self) { + self.content.save_state(); + + if self.content.state_nesting_depth() > 28 { + self.validation_errors + .insert(ValidationError::TooHighQNestingLevel); + } + } + + pub(crate) fn finish(self, sc: &mut SerializeContext) -> Stream { + let buf = self.content.finish(); + sc.register_limits(buf.limits()); + + Stream::new( + buf.to_vec(), + self.bbox + .unwrap_or(Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()), + self.validation_errors.into_iter().collect(), + self.rd_builder.finish(), + self.uses_mask, + ) + } + + fn start_marked_content_prelude(&mut self) { + if self.active_marked_content { + panic!("can't start marked content twice"); + } + + self.active_marked_content = true; + } + + #[track_caller] + pub(crate) fn start_marked_content(&mut self, name: Name) { + self.start_marked_content_prelude(); + self.content.begin_marked_content(name); + } + + #[track_caller] + pub(crate) fn start_marked_content_with_properties( + &mut self, + sc: &mut SerializeContext, + mcid: Option, + tag: ContentTag, + ) { + self.start_marked_content_prelude(); + + let mut mc = self + .content + .begin_marked_content_with_properties(tag.name()); + let mut properties = mc.properties(); + + if let Some(mcid) = mcid { + properties.pairs([(Name(b"MCID"), mcid)]); + } + + // Page height extracted from transform and passed to function to allow + // its dependants to flip the y-axis, mirroring Krilla conventions. + let page_height = self.root_transform.ty(); + tag.write_properties(sc, properties, page_height); + } + + pub(crate) fn end_marked_content(&mut self) { + if !self.active_marked_content { + panic!("can't end marked content when none has been started"); + } + + self.content.end_marked_content(); + self.active_marked_content = false; + } + + pub(crate) fn concat_transform(&mut self, transform: &Transform) { + self.graphics_states.transform(*transform); + } + + fn cur_transform_with_root_transform(&self) -> Transform { + self.root_transform.pre_concat(self.cur_transform()) + } + + pub(crate) fn cur_transform(&self) -> Transform { + self.graphics_states.cur().transform() + } + + pub(crate) fn save_graphics_state(&mut self) { + self.graphics_states.save_state(); + } + + pub(crate) fn restore_graphics_state(&mut self) { + self.graphics_states.restore_state(); + } + + pub(crate) fn set_blend_mode(&mut self, blend_mode: pdf_writer::types::BlendMode) { + if blend_mode != pdf_writer::types::BlendMode::Normal { + let state = ExtGState::new().blend_mode(blend_mode); + self.graphics_states.combine(&state); + } + } + + pub(crate) fn expand_bbox(&mut self, new_bbox: Rect) { + let new_bbox = self.graphics_states.transform_bbox(new_bbox); + if let Some(bbox) = &mut self.bbox { + bbox.expand(&new_bbox); + } else { + self.bbox = Some(new_bbox); + } + } + + pub(crate) fn draw_path( + &mut self, + path: &Path, + mut fill: Option<&Fill>, + stroke: Option<&Stroke>, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) { + if fill.is_none() && stroke.is_none() { + return; + } + + // Zero-size geometry, don't draw. + if path.bounds().width() == 0.0 && path.bounds().height() == 0.0 { + return; + } + + // See issue 199. + let is_line = path.verbs().len() == 2 + && path.verbs()[0] == PathVerb::Move + && path.verbs()[1] == PathVerb::Line; + let dont_fill = path.bounds().width() == 0.0 || path.bounds().height() == 0.0 || is_line; + + if dont_fill { + match stroke.is_some() { + // Some PDF viewers have bugs where they slightly fill zero-sized lines, so + // don't draw them in the first place. + false => return, + // Don't fill, but we still want to draw the stroke, so don't return. + true => { + fill = None; + } + } + } + + let bbox_important = self.bbox_important; + let calculate_bbox = |is_solid: bool| bbox_important || !is_solid; + + let stroke_bbox = |stroke: &Stroke| { + if calculate_bbox(matches!(&stroke.paint.0, InnerPaint::Color(_))) { + calculate_stroke_bbox(stroke, path).unwrap_or(Rect::from_tsp(path.bounds())) + } else { + Rect::from_tsp(path.bounds()) + } + }; + + let fill_prep = |sb: &mut ContentBuilder, fill: &Fill| { + let has_pattern = matches!(fill.paint.0, InnerPaint::Pattern(_)); + let fill_opacity = fill.opacity; + sb.expand_bbox(Rect::from_tsp(path.bounds())); + + // PDF viewers don't show patterns with fill/stroke opacities consistently. + // Because of this, the opacity is accounted for in the pattern itself. + if !has_pattern { + sb.set_fill_opacity(fill_opacity); + } + }; + + let stroke_prep = |sb: &mut ContentBuilder, stroke: &Stroke, stroke_bbox: Rect| { + let is_pattern = matches!(stroke.paint.0, InnerPaint::Pattern(_)); + let stroke_opacity = stroke.opacity; + sb.expand_bbox(stroke_bbox); + + // See comment in `set_fill_properties` + if !is_pattern { + sb.set_stroke_opacity(stroke_opacity); + } + }; + + let fill_op = |sb: &mut ContentBuilder, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + fill: &Fill| { + let fill_rule = fill.rule; + sb.content_set_fill_properties( + Rect::from_tsp(path.bounds()), + fill, + sc, + chunk_container, + ); + sb.content_draw_path(path.segments()); + + match fill_rule { + FillRule::NonZero => sb.content.fill_nonzero(), + FillRule::EvenOdd => sb.content.fill_even_odd(), + }; + }; + + let stroke_op = |sb: &mut ContentBuilder, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + stroke: &Stroke, + stroke_bbox: Rect| { + sb.content_set_stroke_properties(stroke_bbox, stroke, sc, chunk_container); + sb.content_draw_path(path.segments()); + sb.content.stroke(); + }; + + let fill_stroke_op = |sb: &mut ContentBuilder, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + fill: &Fill, + stroke: &Stroke, + stroke_bbox: Rect| { + let fill_rule = fill.rule; + sb.content_set_fill_properties( + Rect::from_tsp(path.bounds()), + fill, + sc, + chunk_container, + ); + sb.content_set_stroke_properties(stroke_bbox, stroke, sc, chunk_container); + sb.content_draw_path(path.segments()); + + match fill_rule { + FillRule::NonZero => sb.content.fill_nonzero_and_stroke(), + FillRule::EvenOdd => sb.content.fill_even_odd_and_stroke(), + }; + }; + + match (fill, stroke) { + (Some(fill), None) => { + self.apply_isolated_op( + |sb, _, _| { + fill_prep(sb, fill); + }, + |sb, sc, chunk_container| { + fill_op(sb, sc, chunk_container, fill); + }, + sc, + chunk_container, + ); + } + (None, Some(stroke)) => { + let stroke_bbox = stroke_bbox(stroke); + + self.apply_isolated_op( + |sb, _, _| { + stroke_prep(sb, stroke, stroke_bbox); + }, + |sb, sc, chunk_container| { + stroke_op(sb, sc, chunk_container, stroke, stroke_bbox); + }, + sc, + chunk_container, + ); + } + (Some(fill), Some(stroke)) => { + let stroke_bbox = stroke_bbox(stroke); + + self.apply_isolated_op( + |sb, _, _| { + fill_prep(sb, fill); + stroke_prep(sb, stroke, stroke_bbox); + }, + |sb, sc, chunk_container| { + fill_stroke_op(sb, sc, chunk_container, fill, stroke, stroke_bbox); + }, + sc, + chunk_container, + ); + } + (None, None) => unreachable!(), + } + } + + pub(crate) fn push_clip_path(&mut self, path: &Path, clip_rule: &FillRule) { + self.content_save_state(); + self.content_draw_path( + path.clone() + .transform(self.cur_transform_with_root_transform().to_tsp()) + .unwrap() + .segments(), + ); + + match clip_rule { + FillRule::NonZero => self.content.clip_nonzero(), + FillRule::EvenOdd => self.content.clip_even_odd(), + }; + + self.content.end_path(); + } + + pub(crate) fn pop_clip_path(&mut self) { + self.content.restore_state(); + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn draw_glyphs( + &mut self, + start: Point, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + fill: Option<&Fill>, + stroke: Option<&Stroke>, + context_color: rgb::Color, + glyphs: &[impl Glyph], + font: Font, + text: &str, + font_size: f32, + ) { + if fill.is_none() && stroke.is_none() { + return; + } + + let (x, y) = (start.x, start.y); + self.graphics_states.save_state(); + + // Calculating the glyphs bbox is very expensive but not always necessary, so omit + // if not needed. + let bbox_important = self.bbox_important; + let calculate_bbox = |is_solid: bool| bbox_important || !is_solid; + + let fill_action = |sb: &mut ContentBuilder, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + fill: &Fill| { + let bbox = if calculate_bbox(matches!(&fill.paint.0, InnerPaint::Color(_))) { + let bbox = get_glyphs_bbox(glyphs, x, y, font_size, font.clone()); + sb.expand_bbox(bbox); + bbox + } else { + Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap() + }; + + sb.content_set_fill_properties(bbox, fill, sc, chunk_container) + }; + + let stroke_action = |sb: &mut ContentBuilder, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + stroke: &Stroke| { + let bbox = if calculate_bbox(matches!(&stroke.paint.0, InnerPaint::Color(_))) { + // TODO: Bbox should also account for stroke. + let bbox = get_glyphs_bbox(glyphs, x, y, font_size, font.clone()); + sb.expand_bbox(bbox); + bbox + } else { + Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap() + }; + + sb.content_set_stroke_properties(bbox, stroke, sc, chunk_container); + }; + + let set_fill_opacity = |sb: &mut ContentBuilder, fill: &Fill| { + // PDF viewers don't show patterns with fill/stroke opacities consistently. + // Because of this, the opacity is accounted for in the pattern itself. + if !matches!(&fill.paint.0, &InnerPaint::Pattern(_)) { + sb.set_fill_opacity(fill.opacity); + } + }; + + let set_stroke_opacity = |sb: &mut ContentBuilder, stroke: &Stroke| { + // PDF viewers don't show patterns with fill/stroke opacities consistently. + // Because of this, the opacity is accounted for in the pattern itself. + if !matches!(&stroke.paint.0, &InnerPaint::Pattern(_)) { + // PDF viewers don't show patterns with fill/stroke opacities consistently. + // Because of this, the opacity is accounted for in the pattern itself. + sb.set_stroke_opacity(stroke.opacity); + + // See the comment in `stroke-action` for why we also set the fill opacity. + sb.set_fill_opacity(stroke.opacity); + } + }; + + match (fill, stroke) { + (Some(f), Some(s)) => { + set_fill_opacity(self, f); + set_stroke_opacity(self, s); + + self.fill_stroke_glyph_run( + x, + y, + sc, + chunk_container, + TextRenderingMode::FillStroke, + |sb, sc, chunk_container| { + fill_action(sb, sc, chunk_container, f); + stroke_action(sb, sc, chunk_container, s); + }, + glyphs, + font.clone(), + context_color, + text, + font_size, + ); + } + (Some(f), None) => { + set_fill_opacity(self, f); + + self.fill_stroke_glyph_run( + x, + y, + sc, + chunk_container, + TextRenderingMode::Fill, + |sb, sc, chunk_container| { + fill_action(sb, sc, chunk_container, f); + }, + glyphs, + font.clone(), + context_color, + text, + font_size, + ); + } + (None, Some(s)) => { + set_stroke_opacity(self, s); + + self.fill_stroke_glyph_run( + x, + y, + sc, + chunk_container, + TextRenderingMode::Stroke, + |sb, sc, chunk_container| { + stroke_action(sb, sc, chunk_container, s); + }, + glyphs, + font.clone(), + context_color, + text, + font_size, + ); + } + (None, None) => unreachable!(), + } + + self.graphics_states.restore_state(); + } + + /// Encode a successive sequence of glyphs that share the same properties and + /// can be encoded with one text showing operator. + #[allow(clippy::too_many_arguments)] + #[inline(always)] + fn encode_consecutive_glyph_run( + &mut self, + sc: &mut SerializeContext, + cur_x: &mut f32, + cur_y: f32, + font_identifier: FontIdentifier, + pdf_font: &dyn PdfFont, + size: f32, + context_color: rgb::Color, + glyphs: &[impl Glyph], + text: &str, + ) { + let font_name = self + .rd_builder + .register_resource(sc.register_font_identifier(font_identifier)); + self.content.set_font(font_name.to_pdf_name(), size); + self.content.set_text_matrix( + Transform::from_row(1.0, 0.0, 0.0, -1.0, *cur_x, cur_y).to_pdf_transform(), + ); + + for glyph in glyphs { + if glyph.glyph_id() == GlyphId::new(0) + || pdf_font.font().postscript_name() == Some("LastResort") + { + sc.register_validation_error(ValidationError::ContainsNotDefGlyph( + pdf_font.font(), + glyph.location(), + text[glyph.text_range()].to_string(), + )); + } + } + + // If it's just a single glyph and without an x_offset, there is no need for individual + // positioning, so we can use `Tj` instead of `TJ`. The reason we're doing this is because + // Acrobat's copy-paste is a bit buggy if `/ActualText` is used at the end of a line, but + // only if `TJ` is used. With `Tj`, it's fine. + if let [glyph] = glyphs { + if approx_eq!(f32, glyph.x_offset(1.0), 0.0, epsilon = 0.001) { + self.encode_single_glyph(cur_x, pdf_font, size, context_color, glyph); + return; + } + } + + self.encode_glyphs_with_individual_positioning( + cur_x, + pdf_font, + size, + context_color, + glyphs, + ); + } + + fn encode_single_glyph( + &mut self, + cur_x: &mut f32, + pdf_font: &dyn PdfFont, + size: f32, + context_color: rgb::Color, + glyph: &impl Glyph, + ) { + let pdf_glyph = pdf_font + .get_gid(ColoredGlyph::new(glyph.glyph_id(), context_color)) + .unwrap(); + self.scratch.clear(); + let encoded = &mut self.scratch; + pdf_glyph.encode_into(encoded); + self.content.show(Str(encoded)); + *cur_x += glyph.x_advance(size); + } + + fn encode_glyphs_with_individual_positioning( + &mut self, + cur_x: &mut f32, + pdf_font: &dyn PdfFont, + size: f32, + context_color: rgb::Color, + glyphs: &[impl Glyph], + ) { + self.scratch.clear(); + let encoded = &mut self.scratch; + + let mut adjustment = 0.0; + let mut positioned = self.content.show_positioned(); + let mut items = positioned.items(); + + for glyph in glyphs { + let pdf_glyph = pdf_font + .get_gid(ColoredGlyph::new(glyph.glyph_id(), context_color)) + .unwrap(); + + let scale = |val| val * pdf_font.units_per_em(); + + let x_advance = scale(glyph.x_advance(1.0)); + let font_advance = pdf_font + .font() + .advance_width(glyph.glyph_id()) + .map(|n| scale(n / pdf_font.font().units_per_em())); + let x_offset = scale(glyph.x_offset(1.0)); + + adjustment += x_offset; + + // Make sure we don't write miniscule adjustments + if !approx_eq!(f32, adjustment, 0.0, epsilon = 0.001) { + if !encoded.is_empty() { + items.show(Str(encoded)); + encoded.clear(); + } + + // Adjustment is always in 1000 units, even for Type3 fonts. + items.adjust(-(adjustment / pdf_font.units_per_em() * PDF_UNITS_PER_EM)); + adjustment = 0.0; + } + + pdf_glyph.encode_into(encoded); + + if let Some(font_advance) = font_advance { + adjustment += x_advance - font_advance; + } + + adjustment -= x_offset; + // cur_x/cur_y and glyph metrics are in user space units. + *cur_x += glyph.x_advance(size); + } + + if !encoded.is_empty() { + items.show(Str(encoded)); + } + + items.finish(); + positioned.finish(); + } + + #[allow(clippy::too_many_arguments)] + fn fill_stroke_glyph_run( + &mut self, + x: f32, + ys: f32, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + fill_render_mode: TextRenderingMode, + action: impl FnOnce(&mut ContentBuilder, &mut SerializeContext, &mut ChunkContainer), + glyphs: &[impl Glyph], + font: Font, + context_color: rgb::Color, + text: &str, + font_size: f32, + ) { + if glyphs.is_empty() { + return; + } + + self.apply_isolated_op( + |_, _, _| {}, + |sb, sc, chunk_container| { + let mut cur_x = x; + let mut cur_y = ys; + + // When the glyph ranges are monotonically decreasing, the glyphs are in reverse + // order and we should write `/ReversedChars` marked content. + let reversed = glyphs.len() > 1 + && glyphs + .windows(2) + .all(|w| w[0].text_range().start >= w[1].text_range().end); + + action(sb, sc, chunk_container); + sb.content.begin_text(); + + let font_container = sc.register_font_container(font.clone()); + let do_text_span = reversed + || use_text_spanner( + glyphs, + text, + context_color, + &mut font_container.borrow_mut(), + ); + + if do_text_span { + // Separate into distinct glyph runs that either are encoded using actual text, or are + // not. + let spanned = GlyphSpanner::new( + glyphs, + text, + reversed, + sc.serialize_settings() + .validators() + .requires_codepoint_mappings(), + context_color, + font_container.clone(), + ); + + for fragment in spanned { + sb.fill_stroke_glyph_span( + &mut cur_x, + &mut cur_y, + fragment, + sc, + fill_render_mode, + font_container.clone(), + context_color, + text, + font_size, + reversed, + ) + } + } else { + let glyph_span = GlyphSpan::Unspanned(glyphs); + + sb.fill_stroke_glyph_span( + &mut cur_x, + &mut cur_y, + glyph_span, + sc, + fill_render_mode, + font_container.clone(), + context_color, + text, + font_size, + reversed, + ) + } + + sb.content.end_text(); + }, + sc, + chunk_container, + ) + } + + #[allow(clippy::too_many_arguments)] + fn fill_stroke_glyph_span( + &mut self, + cur_x: &mut f32, + cur_y: &mut f32, + fragment: GlyphSpan<'_, impl Glyph>, + sc: &mut SerializeContext, + fill_render_mode: TextRenderingMode, + font_container: Rc>, + context_color: rgb::Color, + text: &str, + font_size: f32, + reversed: bool, + ) { + if let Some(text) = fragment.actual_text() { + let mut actual_text = self + .content + .begin_marked_content_with_properties(Name(b"Span")); + actual_text.properties().actual_text(TextStr(text)); + } + + if reversed { + self.content.begin_marked_content(Name(b"ReversedChars")); + } + + // Segment into glyph runs that can be encoded in one go using a PDF + // text showing operator (i.e. no y shift, same Type3 font, etc.) + let segmented = GlyphGrouper::new(font_container.clone(), context_color, fragment.glyphs()); + + for glyph_group in segmented { + self.fill_stroke_glyph_group( + cur_x, + cur_y, + glyph_group, + sc, + fill_render_mode, + font_container.clone(), + context_color, + text, + font_size, + ) + } + + if reversed { + self.content.end_marked_content(); + } + + if fragment.actual_text().is_some() { + self.content.end_marked_content(); + } + } + + #[allow(clippy::too_many_arguments)] + fn fill_stroke_glyph_group( + &mut self, + cur_x: &mut f32, + cur_y: &mut f32, + glyph_group: GlyphGroup<'_, impl Glyph>, + sc: &mut SerializeContext, + fill_render_mode: TextRenderingMode, + font_container: Rc>, + context_color: rgb::Color, + text: &str, + font_size: f32, + ) { + let borrowed = font_container.borrow(); + let pdf_font = borrowed + .get_from_identifier(glyph_group.font_identifier.clone()) + .unwrap(); + + if fill_render_mode == TextRenderingMode::Fill || pdf_font.force_fill() { + self.content + .set_text_rendering_mode(TextRenderingMode::Fill); + } else if fill_render_mode == TextRenderingMode::FillStroke { + self.content + .set_text_rendering_mode(TextRenderingMode::FillStroke); + } else { + self.content + .set_text_rendering_mode(TextRenderingMode::Stroke); + } + + self.encode_consecutive_glyph_run( + sc, + cur_x, + *cur_y - glyph_group.y_offset * font_size, + glyph_group.font_identifier, + pdf_font, + font_size, + context_color, + glyph_group.glyphs, + text, + ); + + *cur_y -= glyph_group.y_advance * font_size; + } + + pub(crate) fn draw_xobject( + &mut self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + x_object: XObject, + state: &ExtGState, + ) { + let bbox = x_object.bbox(); + self.apply_isolated_op( + |sb, _, _| { + sb.graphics_states.combine(state); + sb.expand_bbox(bbox); + }, + move |sb, sc, chunk_container| { + let x_object_name = sb + .rd_builder + .register_resource(sc.register_resourceable(chunk_container, x_object)); + sb.content.x_object(x_object_name.to_pdf_name()); + }, + sc, + chunk_container, + ); + } + + #[allow(dead_code)] + pub(crate) fn draw_xobject_by_reference( + &mut self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + bbox: Rect, + x_object: Ref, + ) { + // TODO: Consider bbox of XObject somehow? + self.apply_isolated_op( + |sb, _, _| { + sb.expand_bbox(bbox); + }, + move |sb, _, _| { + let x_object_name = sb + .rd_builder + .register_resource(resource::XObject::new(x_object)); + sb.content.x_object(x_object_name.to_pdf_name()); + }, + sc, + chunk_container, + ); + } + + pub(crate) fn draw_masked( + &mut self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + mask: Mask, + stream: Stream, + ) { + let state = ExtGState::new().mask(mask, sc, chunk_container); + self.uses_mask = true; + let x_object = XObject::new(stream, false, true, None); + self.draw_xobject(sc, chunk_container, x_object, &state); + } + + pub(crate) fn draw_opacified( + &mut self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + opacity: NormalizedF32, + stream: Stream, + ) { + let state = ExtGState::new() + .stroking_alpha(opacity) + .non_stroking_alpha(opacity); + let x_object = XObject::new(stream, true, false, None); + self.draw_xobject(sc, chunk_container, x_object, &state); + } + + pub(crate) fn draw_isolated( + &mut self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + stream: Stream, + ) { + let state = ExtGState::new(); + let x_object = XObject::new(stream, true, false, None); + self.draw_xobject(sc, chunk_container, x_object, &state); + } + + #[cfg(feature = "raster-images")] + pub(crate) fn draw_image( + &mut self, + image: Image, + size: Size, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) { + self.apply_isolated_op( + |sb, _, _| { + // Scale the image from 1x1 to the actual dimensions. + let transform = + Transform::from_row(size.width(), 0.0, 0.0, -size.height(), 0.0, size.height()); + sb.concat_transform(&transform); + sb.expand_bbox(Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap()); + }, + move |sb, sc, chunk_container| { + let image_name = sb.rd_builder.register_resource(resource::XObject::new( + sc.register_image(chunk_container, image), + )); + + sb.content.x_object(image_name.to_pdf_name()); + }, + sc, + chunk_container, + ); + } + + pub(crate) fn draw_shading( + &mut self, + shading: &ShadingFunction, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) { + self.apply_isolated_op( + |_, _, _| {}, + move |sb, sc, chunk_container| { + let sh = sb + .rd_builder + .register_resource(sc.register_resourceable(chunk_container, shading.clone())); + sb.content.shading(sh.to_pdf_name()); + }, + sc, + chunk_container, + ) + } + + fn set_fill_opacity(&mut self, alpha: NormalizedF32) { + if alpha.get() != 1.0 { + let state = ExtGState::new().non_stroking_alpha(alpha); + self.graphics_states.combine(&state); + } + } + + fn set_stroke_opacity(&mut self, alpha: NormalizedF32) { + if alpha.get() != 1.0 { + let state = ExtGState::new().stroking_alpha(alpha); + self.graphics_states.combine(&state); + } + } + + fn apply_isolated_op( + &mut self, + prep: impl FnOnce(&mut Self, &mut SerializeContext, &mut ChunkContainer), + op: impl FnOnce(&mut Self, &mut SerializeContext, &mut ChunkContainer), + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) { + self.save_graphics_state(); + self.content_save_state(); + + prep(self, sc, chunk_container); + + let transform = self.cur_transform_with_root_transform(); + + if transform != Transform::identity() { + self.content.transform(transform.to_pdf_transform()); + } + + let state = self.graphics_states.cur().ext_g_state().clone(); + + if !state.empty() { + let ext = self.rd_builder.register_resource::( + sc.register_resourceable(chunk_container, state), + ); + self.content.set_parameters(ext.to_pdf_name()); + } + + op(self, sc, chunk_container); + + self.content.restore_state(); + self.restore_graphics_state(); + } + + #[allow(clippy::too_many_arguments)] + fn content_set_fill_stroke_properties( + &mut self, + bounds: Rect, + paint: &Paint, + opacity: NormalizedF32, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + mut set_pattern_fn: impl FnMut(&mut Content, String), + mut set_solid_fn: impl FnMut(&mut Content, ContentColorSpace, &Color), + ) { + let pattern_transform = |transform: Transform| -> Transform { + transform.post_concat(self.cur_transform_with_root_transform()) + }; + + let mut write_gradient = + |gradient_props: GradientProperties, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + transform: Transform, + content_builder: &mut ContentBuilder| { + if let Some((color, opacity)) = gradient_props.single_stop_color() { + // Write gradients with one stop as a solid color fill. + content_builder.set_fill_opacity(opacity); + let cs = color.color_space(sc); + let color_space_resource = + Self::cs_to_content_cs(content_builder, sc, chunk_container, cs); + set_solid_fn(&mut content_builder.content, color_space_resource, color); + } else { + let shading_mask = Mask::new_from_shading( + gradient_props.clone(), + transform, + bounds, + sc, + chunk_container, + ); + + let shading_pattern = ShadingPattern::new( + gradient_props, + content_builder + .cur_transform_with_root_transform() + .pre_concat(transform), + ); + let color_space = content_builder + .rd_builder + .register_resource::( + sc.register_resourceable(chunk_container, shading_pattern), + ); + + if let Some(shading_mask) = shading_mask { + let state = ExtGState::new().mask(shading_mask, sc, chunk_container); + content_builder.uses_mask = true; + + let ext = content_builder + .rd_builder + .register_resource::( + sc.register_resourceable(chunk_container, state), + ); + content_builder.content.set_parameters(ext.to_pdf_name()); + } + + set_pattern_fn(&mut content_builder.content, color_space); + } + }; + + match &paint.0 { + InnerPaint::Color(c) => { + let cs = c.color_space(sc); + let color_space_resource = Self::cs_to_content_cs(self, sc, chunk_container, cs); + set_solid_fn(&mut self.content, color_space_resource, c); + } + InnerPaint::LinearGradient(lg) => { + let (gradient_props, transform) = lg.clone().gradient_properties(bounds); + write_gradient(gradient_props, sc, chunk_container, transform, self); + } + InnerPaint::RadialGradient(rg) => { + let (gradient_props, transform) = rg.clone().gradient_properties(bounds); + write_gradient(gradient_props, sc, chunk_container, transform, self); + } + InnerPaint::SweepGradient(sg) => { + let (gradient_props, transform) = sg.clone().gradient_properties(bounds); + write_gradient(gradient_props, sc, chunk_container, transform, self); + } + InnerPaint::Pattern(pat) => { + let mut pat = Arc::unwrap_or_clone(pat.clone()); + pat.transform = pattern_transform(pat.transform); + + let tiling_pattern = TilingPattern::new( + pat.stream, + pat.transform, + opacity, + pat.width, + pat.height, + sc, + chunk_container, + ); + + let color_space = self.rd_builder.register_resource::( + sc.register_resourceable(chunk_container, tiling_pattern), + ); + set_pattern_fn(&mut self.content, color_space); + } + } + } + + fn cs_to_content_cs( + content_builder: &mut ContentBuilder, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + cs: ColorSpace, + ) -> ContentColorSpace { + match sc.register_colorspace(chunk_container, cs) { + MaybeDeviceColorSpace::ColorSpace(s) => { + ContentColorSpace::Named(content_builder.rd_builder.register_resource(s)) + } + MaybeDeviceColorSpace::DeviceGray => ContentColorSpace::Device, + MaybeDeviceColorSpace::DeviceRgb => ContentColorSpace::Device, + MaybeDeviceColorSpace::DeviceCMYK => ContentColorSpace::Device, + } + } + + fn content_set_fill_properties( + &mut self, + bounds: Rect, + fill: &Fill, + serializer_context: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) { + fn set_pattern_fn(content: &mut Content, color_space: String) { + content.set_fill_color_space(pdf_writer::types::ColorSpaceOperand::Pattern); + content.set_fill_pattern(None, color_space.to_pdf_name()); + } + + fn set_solid_fn(content: &mut Content, color_space: ContentColorSpace, color: &Color) { + match color_space { + ContentColorSpace::Device => match color { + Color::Regular(crate::color::RegularColor::Rgb(r)) => { + let comps = r.to_pdf_color(); + content.set_fill_rgb(comps[0], comps[1], comps[2]); + } + Color::Regular(crate::color::RegularColor::Luma(l)) => { + content.set_fill_gray(l.to_pdf_color()); + } + Color::Regular(crate::color::RegularColor::Cmyk(c)) => { + let comps = c.to_pdf_color(); + content.set_fill_cmyk(comps[0], comps[1], comps[2], comps[3]); + } + Color::Special(_) => { + panic!("Device color space cannot be used with special colors") + } + }, + ContentColorSpace::Named(n) => { + content.set_fill_color_space(n.to_pdf_name()); + content.set_fill_color(color.to_pdf_color()); + } + } + } + + self.content_set_fill_stroke_properties( + bounds, + &fill.paint, + fill.opacity, + serializer_context, + chunk_container, + set_pattern_fn, + set_solid_fn, + ); + } + + fn content_set_stroke_properties( + &mut self, + bounds: Rect, + stroke: &Stroke, + serializer_context: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) { + fn set_pattern_fn(content: &mut Content, color_space: String) { + content.set_stroke_color_space(pdf_writer::types::ColorSpaceOperand::Pattern); + content.set_stroke_pattern(None, color_space.to_pdf_name()); + } + + fn set_solid_fn(content: &mut Content, color_space: ContentColorSpace, color: &Color) { + match color_space { + ContentColorSpace::Device => match color { + Color::Regular(crate::color::RegularColor::Rgb(r)) => { + let comps = r.to_pdf_color(); + content.set_stroke_rgb(comps[0], comps[1], comps[2]); + } + Color::Regular(crate::color::RegularColor::Luma(l)) => { + content.set_stroke_gray(l.to_pdf_color()); + } + Color::Regular(crate::color::RegularColor::Cmyk(c)) => { + let comps = c.to_pdf_color(); + content.set_stroke_cmyk(comps[0], comps[1], comps[2], comps[3]); + } + Color::Special(_) => { + panic!("Device color space cannot be used with special colors") + } + }, + ContentColorSpace::Named(n) => { + content.set_stroke_color_space(n.to_pdf_name()); + content.set_stroke_color(color.to_pdf_color()); + } + } + } + + self.content_set_fill_stroke_properties( + bounds, + &stroke.paint, + stroke.opacity, + serializer_context, + chunk_container, + set_pattern_fn, + set_solid_fn, + ); + + // Only write if they don't correspond to the default values as defined in the + // PDF specification. + if stroke.width != 1.0 { + self.content.set_line_width(stroke.width); + } + + if stroke.miter_limit != 10.0 { + self.content.set_miter_limit(stroke.miter_limit); + } + + if stroke.line_cap != LineCap::Butt { + self.content.set_line_cap(stroke.line_cap.to_pdf_line_cap()); + } + + if stroke.line_join != LineJoin::Miter { + self.content + .set_line_join(stroke.line_join.to_pdf_line_join()); + } + + if let Some(stroke_dash) = &stroke.dash { + self.content + .set_dash_pattern(stroke_dash.array.iter().copied(), stroke_dash.offset); + } + } + + fn content_draw_path(&mut self, path_data: impl Iterator) { + // Taken from resvg + fn calc(n1: f32, n2: f32) -> f32 { + (n1 + n2 * 2.0) / 3.0 + } + + let mut p_prev = None; + + for operation in path_data { + match operation { + PathSegment::MoveTo(p) => { + self.content.move_to(p.x, p.y); + p_prev = Some(p); + } + PathSegment::LineTo(p) => { + self.content.line_to(p.x, p.y); + p_prev = Some(p); + } + PathSegment::QuadTo(p1, p2) => { + // Since PDF doesn't support quad curves, we need to convert them into + // cubic. + let prev = p_prev.unwrap(); + self.content.cubic_to( + calc(prev.x, p1.x), + calc(prev.y, p1.y), + calc(p2.x, p1.x), + calc(p2.y, p1.y), + p2.x, + p2.y, + ); + p_prev = Some(p2); + } + PathSegment::CubicTo(p1, p2, p3) => { + self.content.cubic_to(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + p_prev = Some(p3); + } + PathSegment::Close => { + self.content.close_path(); + } + }; + } + } +} + +// Note that this isn't a 100% accurate calculation, it can overestimate (and in a few cases +// even underestimate), but it should be good enough for the majority of the cases. +// TODO: Improve this so that `zalgo_text` test case shows up fully in the reference image. +fn get_glyphs_bbox(glyphs: &[impl Glyph], x: f32, y: f32, size: f32, font: Font) -> Rect { + let font_bbox = font.bbox(); + let (mut bl, mut bt, mut br, mut bb) = font_bbox + .transform(Transform::from_scale( + size / font.units_per_em(), + -size / font.units_per_em(), + )) + .and_then(|b| b.transform(Transform::from_translate(x, y))) + .map(|b| (b.left(), b.top(), b.right(), b.bottom())) + .unwrap_or((x, y, x + 1.0, y + 1.0)); + + let mut x = x; + let mut y = y; + + for glyph in glyphs { + let xo = glyph.x_offset(size); + let xa = glyph.x_advance(size); + let yo = glyph.y_offset(size); + let ya = glyph.y_advance(size); + + x += xa; + y -= ya; + + bl = bl.min(x + xo); + br = br.max(x + xo); + bt = bt.min(y - yo); + bb = bb.max(y - yo); + } + + Rect::from_ltrb(bl, bt, br, bb).unwrap() +} diff --git a/vendor/krilla/src/data.rs b/vendor/krilla/src/data.rs new file mode 100644 index 00000000..d6dfa940 --- /dev/null +++ b/vendor/krilla/src/data.rs @@ -0,0 +1,49 @@ +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +/// A type that holds some bytes. +#[derive(Clone)] +pub struct Data(pub(crate) Arc + Send + Sync>); + +impl AsRef<[u8]> for Data { + fn as_ref(&self) -> &[u8] { + self.0.as_ref().as_ref() + } +} + +impl Hash for Data { + fn hash(&self, state: &mut H) { + self.as_ref().hash(state); + } +} + +impl From + Send + Sync>> for Data { + fn from(value: Arc + Send + Sync>) -> Self { + Self(value) + } +} + +impl From<&'static [u8]> for Data { + fn from(value: &'static [u8]) -> Self { + Self(Arc::new(value)) + } +} + +impl From> for Data { + fn from(value: Vec) -> Self { + Self(Arc::new(value)) + } +} + +impl From>> for Data { + fn from(value: Arc>) -> Self { + Self(value) + } +} + +impl Debug for Data { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Data {{..}}") + } +} diff --git a/vendor/krilla/src/document.rs b/vendor/krilla/src/document.rs new file mode 100644 index 00000000..ef1ab37a --- /dev/null +++ b/vendor/krilla/src/document.rs @@ -0,0 +1,160 @@ +//! Creating new PDF documents. +//! +//! When using krilla, the starting point is always the creation of a [`Document`]. A document +//! represents _one_ PDF document, to which you can add pages or configure them in any +//! other way you want. +//! +//! Unfortunately, creating PDFs always requires some kind of global state to keep track +//! of different aspects in the creation process, meaning that it is not possible to +//! generate multiple pages at the same time. Instead, you need to add pages separately +//! by calling the [`Document::start_page`] method, which returns a new [`Page`] object that mutably +//! borrows the global state from the document. Once the page is dropped, the global +//! state is passed back to the original document, which you can then use to add even +//! more pages. +//! +//! [`Page`]: Page + +use crate::chunk_container::ChunkContainer; +use crate::destination::NamedDestination; +use crate::error::{KrillaError, KrillaResult}; +use crate::interchange::embed::EmbeddedFile; +use crate::interchange::metadata::Metadata; +use crate::interchange::outline::Outline; +use crate::interchange::tagging::TagTree; +use crate::page::{Page, PageSettings}; +#[cfg(feature = "pdf")] +use crate::pdf::PdfDocument; +use crate::serialize::{SerializeContext, SerializeSettings}; +use crate::surface::Location; + +/// A PDF document. +pub struct Document { + pub(crate) serializer_context: SerializeContext, + pub(crate) chunk_container: ChunkContainer, +} + +impl Default for Document { + fn default() -> Self { + Self::new() + } +} + +impl Document { + /// Create a new document with default serialize settings. + pub fn new() -> Self { + let serializer_context = SerializeContext::new(SerializeSettings::default()); + let chunk_container = ChunkContainer::new(&serializer_context); + + Self { + serializer_context, + chunk_container, + } + } + + /// Create a new document with custom serialize settings. + pub fn new_with(serialize_settings: SerializeSettings) -> Self { + let serializer_context = SerializeContext::new(serialize_settings); + let chunk_container = ChunkContainer::new(&serializer_context); + + Self { + serializer_context, + chunk_container, + } + } + + /// Start a new page with default settings. + pub fn start_page(&mut self) -> Page<'_> { + let page_index = self.serializer_context.page_infos().iter().len(); + Page::new( + &mut self.serializer_context, + &mut self.chunk_container, + page_index, + PageSettings::default(), + ) + } + + /// Start a new page with specific page settings. + pub fn start_page_with(&mut self, page_settings: PageSettings) -> Page<'_> { + let page_index = self.serializer_context.page_infos().iter().len(); + Page::new( + &mut self.serializer_context, + &mut self.chunk_container, + page_index, + page_settings, + ) + } + + /// Embed the pages (0-indexed) from the given + /// PDF document. + #[cfg(feature = "pdf")] + pub fn embed_pdf_pages(&mut self, pdf: &PdfDocument, page_indices: &[usize]) { + self.serializer_context.embed_pdf_pages(pdf, page_indices); + } + + /// Set the location that should be assumed for subsequent operations. + pub fn set_location(&mut self, location: Location) { + self.serializer_context.set_location(location); + } + + /// Reset the location that should be assumed for subsequent operations. + pub fn reset_location(&mut self) { + self.serializer_context.reset_location(); + } + + /// Set the outline of the document. + pub fn set_outline(&mut self, outline: Outline) { + self.serializer_context.set_outline(outline); + } + + /// Set the metadata of the document. + pub fn set_metadata(&mut self, metadata: Metadata) { + self.chunk_container.metadata = Some(metadata); + } + + /// Set the tag tree of the document. + pub fn set_tag_tree(&mut self, tag_tree: TagTree) { + self.serializer_context.set_tag_tree(tag_tree); + } + + /// Embed a new file in the PDF document. + /// + /// Returns `None` if the file couldn't be embedded because a file + /// with the same name has already been embedded. + pub fn embed_file(&mut self, file: EmbeddedFile) -> Option<()> { + self.serializer_context + .embed_file(&mut self.chunk_container, file) + } + + /// Manually register a global named destination. + /// + /// Named destinations used in link annotations are automatically registered, so you don't need + /// to call this function for them. + /// + /// Returns `None` if a named destination with the same name and a different destination has + /// already been registered, and therefore could not be registered again. + #[must_use] + pub fn register_named_destination(&mut self, dest: NamedDestination) -> Option<()> { + self.serializer_context + .register_named_destination(dest) + .map(|_| ()) + } + + /// Attempt to export the document to a PDF file. + pub fn finish(mut self) -> KrillaResult> { + if self.serializer_context.poisoned { + return Err(KrillaError::Poisoned); + } + + // Write empty page if none has been created yet. + if self.serializer_context.page_infos().is_empty() { + self.start_page(); + } + + let Self { + serializer_context, + chunk_container, + } = self; + + Ok(serializer_context.finish(chunk_container)?.finish()) + } +} diff --git a/vendor/krilla/src/error.rs b/vendor/krilla/src/error.rs new file mode 100644 index 00000000..26cd7677 --- /dev/null +++ b/vendor/krilla/src/error.rs @@ -0,0 +1,173 @@ +//! Error handling. +//! +//! There are a lot of things that can go wrong when writing a PDF, like for example when +//! invalid fonts are provided. This module provides the basic error types krilla uses. + +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::sync::Arc; + +use crate::configure::{ValidationError, Validators}; +#[cfg(feature = "raster-images")] +use crate::graphics::image::Image; +#[cfg(feature = "pdf")] +use crate::pdf::{PdfDocument, PdfError}; +use crate::surface::Location; +use crate::tagging::TagId; +use crate::text::Font; + +/// A wrapper type for krilla errors. +pub type KrillaResult = Result; + +/// An error in krilla. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum KrillaError { + /// A page or surface failed during cleanup; discard the complete document. + Poisoned, + /// An error while attempting to embed a font. + Font(Font, String), + /// A list of validation errors. Can only occur if you set the `validator` in + /// the [`SerializeSettings`] to something else than the dummy validator. + /// + /// [`SerializeSettings`]: crate::SerializeSettings + Validation(Vec<(ValidationError, Validators)>), + /// A hard limit of the selected PDF version was exceeded. + Limit(LimitError), + /// The same destination name has been associated with two different destinations, + /// which is prohibited. + DuplicateNamedDestination(Arc), + /// A duplicate [`Tag::id`] was provided. + /// + /// [`Tag::id`]: crate::interchange::tagging::Tag::id + DuplicateTagId(TagId, Option), + /// A [`TagId`] was not found in the [`TagTree`]. + /// + /// [`TagTree`]: crate::interchange::tagging::TagTree + UnknownTagId(TagId, Option), + /// An image couldn't be processed properly. + /// + /// The third argument contains the error message. + #[cfg(feature = "raster-images")] + Image(Image, Option, String), + /// An embedded PDF document couldn't be processed properly. + #[cfg(feature = "pdf")] + Pdf(PdfDocument, PdfError, Option), + /// A sixteen bit image was used, even though it isn't + /// supported by the used PDF version (only available in PDF 1.5+). + #[cfg(feature = "raster-images")] + SixteenBitImage(Image, Option), +} + +impl Display for KrillaError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + KrillaError::Poisoned => f.write_str("document contains a failed page or surface"), + KrillaError::Font(_, message) => write!(f, "failed to embed font: {message}"), + KrillaError::Validation(errors) => { + let count = errors.len(); + write!( + f, + "validation failed with {count} {}", + if count == 1 { "error" } else { "errors" } + ) + } + KrillaError::Limit(error) => write!(f, "PDF version limit exceeded: {error}"), + KrillaError::DuplicateNamedDestination(name) => { + write!(f, "duplicate named destination: {name}") + } + KrillaError::DuplicateTagId(id, location) => { + write!(f, "duplicate tag id {id:?}")?; + write_location(f, *location) + } + KrillaError::UnknownTagId(id, location) => { + write!(f, "unknown tag id {id:?}")?; + write_location(f, *location) + } + #[cfg(feature = "raster-images")] + KrillaError::Image(_, location, message) => { + write!(f, "failed to process image")?; + write_location(f, *location)?; + write!(f, ": {message}") + } + #[cfg(feature = "pdf")] + KrillaError::Pdf(_, error, location) => { + write!(f, "failed to process embedded PDF")?; + write_location(f, *location)?; + write!(f, ": {error}") + } + #[cfg(feature = "raster-images")] + KrillaError::SixteenBitImage(_, location) => { + write!( + f, + "sixteen bit images require PDF 1.5 or newer, but the selected PDF version does not support them" + )?; + write_location(f, *location) + } + } + } +} + +impl Error for KrillaError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + KrillaError::Limit(error) => Some(error), + #[cfg(feature = "pdf")] + KrillaError::Pdf(_, error, _) => Some(error), + _ => None, + } + } +} + +/// A limit imposed by the selected PDF version. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum LimitError { + /// A float exceeded the maximum allowed size for the PDF version. + TooLargeFloat, + /// An array exceeded the maximum allowed length for the PDF version. + TooLongArray, + /// A dictionary exceeded the maximum allowed number of entries for the PDF version. + TooLongDictionary, +} + +impl Display for LimitError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + LimitError::TooLargeFloat => write!(f, "a float exceeded the maximum allowed size"), + LimitError::TooLongArray => write!(f, "an array exceeded the maximum allowed length"), + LimitError::TooLongDictionary => { + write!( + f, + "a dictionary exceeded the maximum allowed number of entries" + ) + } + } + } +} + +impl Error for LimitError {} + +fn write_location(f: &mut Formatter<'_>, location: Option) -> fmt::Result { + if let Some(location) = location { + write!(f, " at location {location}") + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::error::Error as _; + + use super::{KrillaError, LimitError}; + + #[test] + fn krilla_error_implements_error() { + let error = KrillaError::Limit(LimitError::TooLongArray); + + assert_eq!( + error.to_string(), + "PDF version limit exceeded: an array exceeded the maximum allowed length" + ); + assert!(error.source().is_some()); + } +} diff --git a/vendor/krilla/src/geom.rs b/vendor/krilla/src/geom.rs new file mode 100644 index 00000000..fa571e4b --- /dev/null +++ b/vendor/krilla/src/geom.rs @@ -0,0 +1,341 @@ +//! Path geometry. + +use std::hash::{Hash, Hasher}; + +/// A point. +#[allow(missing_docs)] +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Point { + pub x: f32, + pub y: f32, +} + +impl Point { + /// Create a new point with the given x and y coordinates. + pub fn from_xy(x: f32, y: f32) -> Self { + Self { x, y } + } + + pub(crate) fn to_tsp(self) -> tiny_skia_path::Point { + tiny_skia_path::Point::from_xy(self.x, self.y) + } +} + +/// A size. +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Size(tiny_skia_path::Size); + +impl Size { + /// Creates a new `Size` from width and height. + /// + /// Returns `None` if either the width or the height is not > 0. + pub fn from_wh(width: f32, height: f32) -> Option { + Some(Self(tiny_skia_path::Size::from_wh(width, height)?)) + } + + /// Returns the width of the size. + pub fn width(&self) -> f32 { + self.0.width() + } + + /// Returns the height of the size. + pub fn height(&self) -> f32 { + self.0.height() + } +} + +/// A rectangle defined by left, top, right and bottom edges. +#[allow(missing_docs)] +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Rect(tiny_skia_path::Rect); + +impl Eq for Rect {} + +impl Hash for Rect { + fn hash(&self, state: &mut H) { + self.0.left().to_bits().hash(state); + self.0.top().to_bits().hash(state); + self.0.right().to_bits().hash(state); + self.0.bottom().to_bits().hash(state); + } +} + +impl Rect { + /// Creates new `Rect`. + pub fn from_ltrb(left: f32, top: f32, right: f32, bottom: f32) -> Option { + Some(Self(tiny_skia_path::Rect::from_ltrb( + left, top, right, bottom, + )?)) + } + + /// Apply a transform to the rect. + pub fn transform(self, transform: Transform) -> Option { + Some(Self(self.0.transform(transform.to_tsp())?)) + } + + /// Creates new `Rect`. + pub fn from_xywh(x: f32, y: f32, w: f32, h: f32) -> Option { + Some(Self(tiny_skia_path::Rect::from_xywh(x, y, w, h)?)) + } + + /// Returns the left edge. + pub fn left(&self) -> f32 { + self.0.left() + } + + /// Returns the top edge. + pub fn top(&self) -> f32 { + self.0.top() + } + + /// Returns the right edge. + pub fn right(&self) -> f32 { + self.0.right() + } + + /// Returns the bottom edge. + pub fn bottom(&self) -> f32 { + self.0.bottom() + } + + /// Returns rect's width. + pub fn width(&self) -> f32 { + self.0.width() + } + + /// Returns rect's height. + pub fn height(&self) -> f32 { + self.0.height() + } + + pub(crate) fn to_tsp(self) -> tiny_skia_path::Rect { + self.0 + } + + pub(crate) fn from_tsp(rect: tiny_skia_path::Rect) -> Self { + Self(rect) + } + + pub(crate) fn expand(&mut self, other: &Rect) { + let left = self.left().min(other.left()); + let top = self.top().min(other.top()); + let right = self.right().max(other.right()); + let bottom = self.bottom().max(other.bottom()); + *self = Rect::from_ltrb(left, top, right, bottom).unwrap(); + } + + pub(crate) fn to_pdf_rect(self) -> pdf_writer::Rect { + pdf_writer::Rect::new( + self.left(), + self.top(), + self.left() + self.width(), + self.top() + self.height(), + ) + } +} + +/// A quadrilateral. +/// +/// The points should be given in the following order: +/// 1. bottom-left +/// 2. bottom-right +/// 3. top-right +/// 4. top-left +/// +/// Where the coordinate system is Y-down: +/// ```md +/// | +/// | +/// -----+----> X +/// | +/// | +/// v +/// +/// Y +/// ``` +#[derive(Clone, Copy, PartialEq)] +pub struct Quadrilateral(pub [Point; 4]); + +impl From for Quadrilateral { + fn from(r: Rect) -> Self { + Self([ + Point::from_xy(r.left(), r.bottom()), + Point::from_xy(r.right(), r.bottom()), + Point::from_xy(r.right(), r.top()), + Point::from_xy(r.left(), r.top()), + ]) + } +} + +/// An affine transformation matrix. +/// +/// Unlike other types, doesn't guarantee to be valid. This is Skia quirk. +/// Meaning Transform(0, 0, 0, 0, 0, 0) is ok, while it's technically not. +/// Non-finite values are also not an error. +#[allow(missing_docs)] +#[derive(Copy, Clone, PartialEq, Debug, Default)] +pub struct Transform(tiny_skia_path::Transform); + +impl Transform { + /// Creates an identity transform. + pub fn identity() -> Self { + Self(tiny_skia_path::Transform::default()) + } + + /// Creates a new `Transform`. + pub fn from_row(sx: f32, ky: f32, kx: f32, sy: f32, tx: f32, ty: f32) -> Self { + Self(tiny_skia_path::Transform::from_row(sx, ky, kx, sy, tx, ty)) + } + + /// Creates a new translating `Transform`. + pub fn from_translate(tx: f32, ty: f32) -> Self { + Self(tiny_skia_path::Transform::from_translate(tx, ty)) + } + + /// Creates a new scaling `Transform`. + pub fn from_scale(sx: f32, sy: f32) -> Self { + Self(tiny_skia_path::Transform::from_scale(sx, sy)) + } + + /// Creates a new skewing `Transform`. + pub fn from_skew(kx: f32, ky: f32) -> Self { + Self(tiny_skia_path::Transform::from_skew(kx, ky)) + } + + /// Creates a new rotating `Transform`. + /// + /// `angle` in degrees. + pub fn from_rotate(angle: f32) -> Self { + Self(tiny_skia_path::Transform::from_rotate(angle)) + } + + /// Creates a new rotating `Transform` at the specified position. + /// + /// `angle` in degrees. + pub fn from_rotate_at(angle: f32, tx: f32, ty: f32) -> Self { + Self(tiny_skia_path::Transform::from_rotate_at(angle, tx, ty)) + } + + /// Return the `sx` component + pub fn sx(&self) -> f32 { + self.0.sx + } + + /// Return the `sy` component + pub fn sy(&self) -> f32 { + self.0.sy + } + + /// Return the `kx` component + pub fn kx(&self) -> f32 { + self.0.kx + } + + /// Return the `kx` component + pub fn ky(&self) -> f32 { + self.0.ky + } + + /// Return the `tx` component + pub fn tx(&self) -> f32 { + self.0.tx + } + + /// Return the `ty` component + pub fn ty(&self) -> f32 { + self.0.ty + } + + /// Returns the inverted transform. + pub fn invert(&self) -> Option { + Some(Self(self.0.invert()?)) + } + + pub(crate) fn pre_concat(&self, other: Self) -> Self { + Self(self.0.pre_concat(other.0)) + } + + pub(crate) fn post_concat(&self, other: Self) -> Self { + Self(self.0.post_concat(other.0)) + } + + pub(crate) fn to_tsp(self) -> tiny_skia_path::Transform { + self.0 + } + + pub(crate) fn from_tsp(ts: tiny_skia_path::Transform) -> Self { + Self(ts) + } + + pub(crate) fn to_pdf_transform(self) -> [f32; 6] { + [ + self.0.sx, self.0.ky, self.0.kx, self.0.sy, self.0.tx, self.0.ty, + ] + } +} + +impl Hash for Transform { + fn hash(&self, state: &mut H) { + self.0.tx.to_bits().hash(state); + self.0.ty.to_bits().hash(state); + self.0.sx.to_bits().hash(state); + self.0.sy.to_bits().hash(state); + self.0.kx.to_bits().hash(state); + self.0.ky.to_bits().hash(state); + } +} + +/// A path. +pub struct Path(pub(crate) tiny_skia_path::Path); + +impl Path { + /// Apply a transformation to the path. + pub fn transform(self, transform: Transform) -> Option { + Some(Self(self.0.transform(transform.to_tsp())?)) + } +} + +/// A path builder. +#[derive(Default)] +pub struct PathBuilder(tiny_skia_path::PathBuilder); + +impl PathBuilder { + /// Create a new path. + pub fn new() -> Self { + Self::default() + } + + /// Adds beginning of a contour. + pub fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to(x, y) + } + + /// Adds a line from the last point. + pub fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to(x, y) + } + + /// Adds a quad curve from the last point to `x`, `y`. + pub fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.0.quad_to(x1, y1, x, y) + } + /// Adds a cubic curve from the last point to `x`, `y`. + pub fn cubic_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.0.cubic_to(x1, y1, x2, y2, x, y) + } + + /// Close the current contour. + pub fn close(&mut self) { + self.0.close() + } + + /// Push a rectangle to the path. + pub fn push_rect(&mut self, rect: Rect) { + self.0.push_rect(rect.to_tsp()) + } + + /// Finish the current path. + pub fn finish(self) -> Option { + Some(Path(self.0.finish()?)) + } +} diff --git a/vendor/krilla/src/graphics/blend.rs b/vendor/krilla/src/graphics/blend.rs new file mode 100644 index 00000000..9d0e98df --- /dev/null +++ b/vendor/krilla/src/graphics/blend.rs @@ -0,0 +1,46 @@ +//! Blending. + +/// How to blend source and backdrop. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[allow(missing_docs)] +pub enum BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, +} + +impl BlendMode { + pub(crate) fn to_pdf(self) -> pdf_writer::types::BlendMode { + match self { + BlendMode::Normal => pdf_writer::types::BlendMode::Normal, + BlendMode::Multiply => pdf_writer::types::BlendMode::Multiply, + BlendMode::Screen => pdf_writer::types::BlendMode::Screen, + BlendMode::Overlay => pdf_writer::types::BlendMode::Overlay, + BlendMode::Darken => pdf_writer::types::BlendMode::Darken, + BlendMode::Lighten => pdf_writer::types::BlendMode::Lighten, + BlendMode::ColorDodge => pdf_writer::types::BlendMode::ColorDodge, + BlendMode::ColorBurn => pdf_writer::types::BlendMode::ColorBurn, + BlendMode::HardLight => pdf_writer::types::BlendMode::HardLight, + BlendMode::SoftLight => pdf_writer::types::BlendMode::SoftLight, + BlendMode::Difference => pdf_writer::types::BlendMode::Difference, + BlendMode::Exclusion => pdf_writer::types::BlendMode::Exclusion, + BlendMode::Hue => pdf_writer::types::BlendMode::Hue, + BlendMode::Saturation => pdf_writer::types::BlendMode::Saturation, + BlendMode::Color => pdf_writer::types::BlendMode::Color, + BlendMode::Luminosity => pdf_writer::types::BlendMode::Luminosity, + } + } +} diff --git a/vendor/krilla/src/graphics/color.rs b/vendor/krilla/src/graphics/color.rs new file mode 100644 index 00000000..559305c5 --- /dev/null +++ b/vendor/krilla/src/graphics/color.rs @@ -0,0 +1,552 @@ +//! Dealing with colors and color spaces. +//! +//! # Color spaces +//! +//! krilla currently supports four color models: +//! - RGB +//! - Luma +//! - CMYK +//! - Separation (also known as Spot) +//! +//! Each color space is associated with its specific color type, which you can use to create new +//! instances of a specific color in that color space. +//! +//! # Representation of colors +//! +//! When specifying colors in the process color spaces RGB, Luma, and CMYK, it is important +//! to understand the distinction between device-dependent and decide-independent color +//! specification. What follows is only a very brief explanation, if you want to dive into +//! more details, please look for appropriate resources on the web. +//! +//! When specifying colors in a *device-dependent way*, if I instruct the program to draw +//! the RGB color (145, 120, 45), then the program will use these literal values to activate +//! the R/G/B lights to achieve displaying a certain color. The problem is that specifying +//! colors in such a way can lead to slightly different results when actually displaying it, +//! depending on the screen that is used, since each screen is calibrated differently and +//! based on different display technologies. This is especially critical for printers, where +//! different values for CMYK colors might result in different-looking colors when being printed. +//! +//! This is why there is also the option to specify colors in a *device-independent* way, +//! which basically means that the color value (145, 120, 45) is represented in a well-specified +//! color space, and each device can then convert the colors to their native color space +//! so that they match the representation in the given color space as closely as possible. +//! This should lead to a more accurate color representation across different screens. +//! +//! In 90% of the cases, it is totally fine to just use a device-dependent colorspace, and it's +//! what krilla does by default. However, if you do care about that, then you can set the +//! `no_device_cs` property of [`SerializeSettings`] to true, in which case krilla will embed an ICC profile for the +//! sGrey and sRGB color spaces (for Luma and RGB colors, respectively). If a CMYK profile +//! was provided to the serialize settings, this will be used for CMYK colors. Otherwise, +//! it will fall back to device CMYK. +//! +//! # Separations +//! +//! An alternative way to achieve exact color reproduction in print are Separation colors. +//! Using these colors, you can request a specific colorant through its well-known name +//! (e.g. through a color registry such as PANTONE or RAL). Your production shop can then +//! supply that exact pigment. +//! +//! It is important to recognize that, when viewed on a computer or printed on a home printer, +//! the requested colorant won't be available. For that reason, you will need to supply a fallback +//! process color. +//! +//! Pay attention to remain consistent: Both in your use of colorant names and fallback colors. A +//! given colorant should not be referred to by multiple, slightly different names. Just the same +//! for fallback colors: For a given colorant, there should be only one fallback color. Using multiple +//! Separation color spaces with the same colorant and distinct fallback colors is considered a bad +//! practice and forbidden in PDF/A-2 and later. +//! +//! [`SerializeSettings`]: crate::SerializeSettings + +use std::fmt::Debug; +use std::hash::Hash; + +use crate::configure::ValidationError; +use crate::graphics::icc::ICCBasedColorSpace; +use crate::serialize::SerializeContext; + +/// The PDF name for the device RGB color space. +pub(crate) const DEVICE_RGB: &str = "DeviceRGB"; +/// The PDF name for the device gray color space. +pub(crate) const DEVICE_GRAY: &str = "DeviceGray"; +/// The PDF name for the device CMYK color space. +pub(crate) const DEVICE_CMYK: &str = "DeviceCMYK"; + +/// A wrapper for storing colors from different color spaces. +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub enum Color { + /// A device or CIE-based color. + Regular(RegularColor), + /// A special color space. + Special(SpecialColor), +} + +/// A device or CIE-based color. +#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] +pub enum RegularColor { + /// An RGB-based color. + Rgb(rgb::Color), + /// A luma-based color. + Luma(luma::Color), + /// A device CMYK color. + Cmyk(cmyk::Color), +} + +/// A special color space color. +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub enum SpecialColor { + /// A separation color. + Separation(separation::Color), +} + +impl Color { + pub(crate) fn to_pdf_color(&self) -> Vec { + match self { + Color::Regular(RegularColor::Rgb(rgb)) => rgb.to_pdf_color().to_vec(), + Color::Regular(RegularColor::Luma(l)) => vec![l.to_pdf_color()], + Color::Regular(RegularColor::Cmyk(cmyk)) => cmyk.to_pdf_color().to_vec(), + Color::Special(SpecialColor::Separation(spot)) => vec![spot.to_pdf_color()], + } + } + + pub(crate) fn color_space(&self, sc: &mut SerializeContext) -> ColorSpace { + match self { + Color::Regular(c) => c.color_space(sc).into(), + Color::Special(c) => c.color_space().into(), + } + } + + /// Convert a color to a regular color for use with constructs like tags or + /// annotations that don't support special color spaces + pub(crate) fn to_regular(&self) -> RegularColor { + match self { + Color::Regular(c) => *c, + Color::Special(SpecialColor::Separation(c)) => c.space.fallback, + } + } +} + +impl RegularColor { + pub(crate) fn color_space(&self, sc: &mut SerializeContext) -> RegularColorSpace { + match self { + Self::Rgb(r) => r.color_space(sc.serialize_settings().no_device_cs), + Self::Luma(_) => luma::color_space(sc.serialize_settings().no_device_cs), + Self::Cmyk(_) => match cmyk::color_space(&sc.serialize_settings()) { + None => { + sc.register_validation_error(ValidationError::MissingCMYKProfile); + DeviceColorSpace::Cmyk.into() + } + Some(cs) => cs, + }, + } + } + + /// Return the current color as RGB for use with colored glyphs (SVG and + /// COLR). + pub(crate) fn as_rgb(self) -> Option { + Some(match self { + Self::Rgb(r) => r, + Self::Luma(l) => rgb::Color::new(l.0, l.0, l.0), + Self::Cmyk(_) => return None, + }) + } + + /// Returns true if this is a subtractive color space (CMYK), false otherwise (RGB, Luma). + /// Used for determining the correct tint transform behavior in Separation color spaces. + pub(crate) fn is_subtractive(self) -> bool { + matches!(self, Self::Cmyk(_)) + } +} + +impl SpecialColor { + pub(crate) fn color_space(&self) -> SpecialColorSpace { + match self { + Self::Separation(spot) => spot.color_space().into(), + } + } +} + +/// Gray-scale colors. +pub mod luma { + use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace}; + + /// A luma color. + #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] + pub struct Color(pub(crate) u8); + + impl Color { + /// Create a new luma color. + pub fn new(lightness: u8) -> Color { + Color(lightness) + } + + /// Create a black luma color. + pub fn black() -> Self { + Self::new(0) + } + + /// Create a white RGB color. + pub fn white() -> Self { + Self::new(255) + } + + pub(crate) fn to_pdf_color(self) -> f32 { + self.0 as f32 / 255.0 + } + } + + impl From for super::RegularColor { + fn from(val: Color) -> Self { + super::RegularColor::Luma(val) + } + } + + impl From for super::Color { + fn from(val: Color) -> Self { + RegularColor::from(val).into() + } + } + + impl Default for Color { + fn default() -> Self { + Color::new(0) + } + } + + pub(crate) fn color_space(no_device_cs: bool) -> RegularColorSpace { + if no_device_cs { + CieBasedColorSpace::Luma.into() + } else { + DeviceColorSpace::Gray.into() + } + } +} + +impl From for Color { + fn from(value: RegularColor) -> Self { + Self::Regular(value) + } +} + +impl From for Color { + fn from(value: SpecialColor) -> Self { + Self::Special(value) + } +} + +/// CMYK colors. +pub mod cmyk { + use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace}; + use crate::graphics::icc::ICCBasedColorSpace; + use crate::SerializeSettings; + + /// A CMYK color. + #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] + pub struct Color(pub(crate) u8, pub(crate) u8, pub(crate) u8, pub(crate) u8); + + impl Color { + /// Create a new CMYK color. + pub fn new(cyan: u8, magenta: u8, yellow: u8, black: u8) -> Color { + Color(cyan, magenta, yellow, black) + } + + pub(crate) fn to_pdf_color(self) -> [f32; 4] { + [ + self.0 as f32 / 255.0, + self.1 as f32 / 255.0, + self.2 as f32 / 255.0, + self.3 as f32 / 255.0, + ] + } + } + + impl From for super::RegularColor { + fn from(val: Color) -> Self { + super::RegularColor::Cmyk(val) + } + } + + impl From for super::Color { + fn from(val: Color) -> Self { + super::RegularColor::from(val).into() + } + } + + impl Default for Color { + fn default() -> Self { + Color::new(0, 0, 0, 255) + } + } + + pub(crate) fn color_space(ss: &SerializeSettings) -> Option { + if ss.no_device_cs { + ss.clone() + .cmyk_profile + .map(|p| CieBasedColorSpace::Cmyk(ICCBasedColorSpace::<4>(p.clone())).into()) + } else { + Some(DeviceColorSpace::Cmyk.into()) + } + } +} + +/// RGB colors. +pub mod rgb { + use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace}; + + /// An RGB color. + #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] + pub struct Color(pub(crate) u8, pub(crate) u8, pub(crate) u8); + + impl Default for Color { + fn default() -> Self { + Color::black() + } + } + + impl Color { + /// Create a new RGB color. + pub fn new(red: u8, green: u8, blue: u8) -> Self { + Color(red, green, blue) + } + + /// Create a new linear RGB color. + pub fn new_linear(red: u8, green: u8, blue: u8) -> Self { + Color(red, green, blue) + } + + /// Create a black RGB color. + pub fn black() -> Self { + Self::new(0, 0, 0) + } + + /// Create a white RGB color. + pub fn white() -> Self { + Self::new(255, 255, 255) + } + + /// The `red` component of the color. + pub fn red(&self) -> u8 { + self.0 + } + + /// The `green` component of the color. + pub fn green(&self) -> u8 { + self.1 + } + + /// The `blue` component of the color. + pub fn blue(&self) -> u8 { + self.2 + } + + pub(crate) fn to_pdf_color(self) -> [f32; 3] { + [ + self.0 as f32 / 255.0, + self.1 as f32 / 255.0, + self.2 as f32 / 255.0, + ] + } + + pub(super) fn color_space(&self, no_device_cs: bool) -> RegularColorSpace { + color_space(no_device_cs) + } + } + + impl From for super::RegularColor { + fn from(val: Color) -> Self { + super::RegularColor::Rgb(val) + } + } + + impl From for super::Color { + fn from(val: Color) -> Self { + super::RegularColor::from(val).into() + } + } + + pub(crate) fn color_space(no_device_cs: bool) -> RegularColorSpace { + if no_device_cs { + CieBasedColorSpace::Srgb.into() + } else { + DeviceColorSpace::Rgb.into() + } + } +} + +/// Separation (spot) colors. +pub mod separation { + use crate::color::RegularColor; + + /// A spot color. + #[derive(Debug, Hash, Eq, PartialEq, Clone)] + pub struct Color { + pub(crate) tint: u8, + pub(crate) space: SeparationSpace, + } + + impl Color { + /// Create a new spot color. + pub fn new(tint: u8, space: SeparationSpace) -> Self { + Self { tint, space } + } + + pub(crate) fn to_pdf_color(&self) -> f32 { + self.tint as f32 / 255.0 + } + + pub(crate) fn color_space(&self) -> SeparationSpace { + self.space.clone() + } + } + + impl Default for Color { + fn default() -> Self { + Color::new(0, SeparationSpace::default()) + } + } + + impl From for super::SpecialColor { + fn from(val: Color) -> Self { + super::SpecialColor::Separation(val) + } + } + + impl From for super::Color { + fn from(val: Color) -> Self { + super::SpecialColor::from(val).into() + } + } + + /// A Separation color space (also known as spot colors). + /// + /// Separation color spaces use a single, subtractive colorant and allow + /// achieving exact color reproduction in print. + /// + /// Krilla automatically linearly scales the fallback color with the separation + /// tint. + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + pub struct SeparationSpace { + pub(crate) colorant: SeparationColorant, + pub(crate) fallback: RegularColor, + } + + impl SeparationSpace { + /// Create a new Separation space. + /// + /// To export PDF/A-2 and later, make sure that a single Separation Colorant + /// is always used with the same fallback color. + pub fn new(colorant: SeparationColorant, fallback: RegularColor) -> Self { + Self { colorant, fallback } + } + } + + impl From for super::SpecialColorSpace { + fn from(value: SeparationSpace) -> Self { + Self::Separation(value) + } + } + + impl Default for SeparationSpace { + fn default() -> Self { + Self { + colorant: SeparationColorant::default(), + fallback: super::rgb::Color::default().into(), + } + } + } + + /// What colorant to use for colors in this space. + #[derive(Debug, Eq, PartialEq, Hash, Clone, Default)] + pub enum SeparationColorant { + /// Don't apply colorant at all. Sometimes used to indicate other production + /// info, such as cuts. + #[default] + NoColorant, + /// Apply the same amount of each available colorants. + AllColorants, + /// Specify a colorant name. + Custom(String), + } + + impl SeparationColorant { + pub(crate) fn to_pdf<'a>(&'a self) -> pdf_writer::Name<'a> { + match self { + Self::AllColorants => pdf_writer::Name(b"All"), + Self::NoColorant => pdf_writer::Name(b"None"), + Self::Custom(s) => pdf_writer::Name(s.as_bytes()), + } + } + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) enum ColorSpace { + Device(DeviceColorSpace), + CieBased(CieBasedColorSpace), + Special(SpecialColorSpace), +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) enum RegularColorSpace { + Device(DeviceColorSpace), + CieBased(CieBasedColorSpace), +} + +impl From for ColorSpace { + fn from(value: RegularColorSpace) -> Self { + match value { + RegularColorSpace::Device(s) => Self::Device(s), + RegularColorSpace::CieBased(s) => Self::CieBased(s), + } + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) enum DeviceColorSpace { + Rgb, + Gray, + Cmyk, +} + +impl From for ColorSpace { + fn from(value: DeviceColorSpace) -> Self { + Self::Device(value) + } +} + +impl From for RegularColorSpace { + fn from(value: DeviceColorSpace) -> Self { + Self::Device(value) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) enum CieBasedColorSpace { + Srgb, + Luma, + Cmyk(ICCBasedColorSpace<4>), +} + +impl From for ColorSpace { + fn from(value: CieBasedColorSpace) -> Self { + Self::CieBased(value) + } +} + +impl From for RegularColorSpace { + fn from(value: CieBasedColorSpace) -> Self { + Self::CieBased(value) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) enum SpecialColorSpace { + /// A Separation color space with its colorant and fallback. + Separation(separation::SeparationSpace), +} + +impl From for ColorSpace { + fn from(value: SpecialColorSpace) -> Self { + Self::Special(value) + } +} diff --git a/vendor/krilla/src/graphics/graphic.rs b/vendor/krilla/src/graphics/graphic.rs new file mode 100644 index 00000000..d8c38e82 --- /dev/null +++ b/vendor/krilla/src/graphics/graphic.rs @@ -0,0 +1,30 @@ +//! Drawing graphics. This allows you to reuse the same graphical content +//! multiple times in the PDF without incurring any overhead in terms of file size. + +use crate::graphics::xobject::XObject; +use crate::stream::Stream; + +/// A cacheable graphic. You can use this for large graphics objects which you +/// want to reuse in multiple locations in the same document. Embedding the same +/// graphic multiple times will ensure that it is only actually written once in the +/// PDF document, which might lead to better file sizes. +/// +/// IMPORTANT: Note that you must only use a graphic in the document that you created it with! +/// If you use it in a different document, you will end up with an invalid PDF file. +#[derive(PartialEq, Eq, Debug, Hash, Clone)] +pub struct Graphic { + /// The stream of the graphic. + pub(crate) x_object: XObject, +} + +impl Graphic { + /// Create a new graphic. + /// `stream` contains the content description of the graphic. + /// `isolated` indicates whether the contents of the graphic should be isolated + /// from wherever the graphic is invoked. + pub fn new(stream: Stream, isolated: bool) -> Self { + Self { + x_object: XObject::new(stream, isolated, false, None), + } + } +} diff --git a/vendor/krilla/src/graphics/graphics_state.rs b/vendor/krilla/src/graphics/graphics_state.rs new file mode 100644 index 00000000..918f0591 --- /dev/null +++ b/vendor/krilla/src/graphics/graphics_state.rs @@ -0,0 +1,251 @@ +use std::hash::Hash; +use std::sync::Arc; + +use pdf_writer::types::BlendMode; +use pdf_writer::{Finish, Name, Ref}; + +use crate::chunk_container::ChunkContainer; +use crate::configure::ValidationError; +use crate::geom::{Rect, Transform}; +use crate::graphics::mask::Mask; +use crate::num::NormalizedF32; +use crate::resource; +use crate::resource::Resourceable; +use crate::serialize::{Cacheable, SerializeContext}; + +/// The inner representation of an external graphics state. +#[derive(Debug, Hash, PartialEq, Eq, Default, Clone)] +struct Repr { + /// The non-stroking alpha. + non_stroking_alpha: Option, + /// The stroking alpha. + stroking_alpha: Option, + /// The blend mode. + blend_mode: Option, + /// An active mask. + mask: Option, +} + +/// A graphics state containing information about +/// - The current stroking alpha. +/// - The current non-stroking alpha. +/// - The current blend mode. +/// - The current mask. +/// +/// This struct provides exposes a builder pattern for setting the various properties +/// individually. +/// +/// This type is cheap to clone. +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +pub(crate) struct ExtGState(Arc); + +impl ExtGState { + /// Create a new, empty graphics state. + pub(crate) fn new() -> Self { + Self::default() + } + + /// Create a new graphics state with a stroking alpha. + #[must_use] + pub(crate) fn stroking_alpha(mut self, stroking_alpha: NormalizedF32) -> Self { + Arc::make_mut(&mut self.0).stroking_alpha = Some(stroking_alpha); + self + } + + /// Create a new graphics state with a non-stroking alpha. + #[must_use] + pub(crate) fn non_stroking_alpha(mut self, non_stroking_alpha: NormalizedF32) -> Self { + Arc::make_mut(&mut self.0).non_stroking_alpha = Some(non_stroking_alpha); + self + } + + /// Create a new graphics state with a blend mode. + #[must_use] + pub(crate) fn blend_mode(mut self, blend_mode: BlendMode) -> Self { + Arc::make_mut(&mut self.0).blend_mode = Some(blend_mode); + self + } + + /// Create a new graphics state with a mask. + #[must_use] + pub(crate) fn mask( + mut self, + mask: Mask, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) -> Self { + let mask_ref = sc.register_cacheable(chunk_container, mask); + Arc::make_mut(&mut self.0).mask = Some(mask_ref); + self + } + + /// Check whether the graphics state is empty. + pub(crate) fn empty(&self) -> bool { + self.0.mask.is_none() + && self.0.stroking_alpha.is_none() + && self.0.non_stroking_alpha.is_none() + && self.0.blend_mode.is_none() + } + + /// Integrate another graphics state into the current one. This is done by replacing + /// all active properties of the other graphics state in the current graphics state, while + /// leaving the inactive ones unchanged. + pub(crate) fn combine(&mut self, other: &ExtGState) { + if let Some(stroking_alpha) = other.0.stroking_alpha { + Arc::make_mut(&mut self.0).stroking_alpha = Some(stroking_alpha); + } + + if let Some(non_stroking_alpha) = other.0.non_stroking_alpha { + Arc::make_mut(&mut self.0).non_stroking_alpha = Some(non_stroking_alpha); + } + + if let Some(blend_mode) = other.0.blend_mode { + Arc::make_mut(&mut self.0).blend_mode = Some(blend_mode); + } + + if let Some(mask) = other.0.mask { + Arc::make_mut(&mut self.0).mask = Some(mask); + } + } +} + +impl Cacheable for ExtGState { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let chunk = &mut chunk_container.non_stream.ext_g_states; + + let mut ext_st = chunk.ext_graphics(root_ref); + if let Some(nsa) = self.0.non_stroking_alpha { + if nsa != NormalizedF32::ONE { + sc.register_validation_error(ValidationError::Transparency(sc.location)); + } + + ext_st.non_stroking_alpha(nsa.get()); + } + + if let Some(sa) = self.0.stroking_alpha { + if sa != NormalizedF32::ONE { + sc.register_validation_error(ValidationError::Transparency(sc.location)); + } + + ext_st.stroking_alpha(sa.get()); + } + + if let Some(bm) = self.0.blend_mode { + if bm != BlendMode::Normal { + sc.register_validation_error(ValidationError::Transparency(sc.location)); + } + + ext_st.blend_mode(bm); + } + + if let Some(mask_ref) = self.0.mask { + sc.register_validation_error(ValidationError::Transparency(sc.location)); + + ext_st.pair(Name(b"SMask"), mask_ref); + } + + ext_st.finish(); + } +} + +impl Resourceable for ExtGState { + type Resource = resource::ExtGState; +} + +/// A simulation of the PDF graphics state, so that we +/// can write our transforms/graphics state all at once +/// when adding an image/path instead of having to +/// use `save_state`/`restore_state` excessively. +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct GraphicsState { + ext_g_state: ExtGState, + ctm: Transform, +} +impl Eq for GraphicsState {} + +impl Hash for GraphicsState { + fn hash(&self, state: &mut H) { + self.ext_g_state.hash(state); + self.ctm.hash(state); + } +} + +impl Default for GraphicsState { + fn default() -> Self { + Self { + ext_g_state: ExtGState::new(), + ctm: Transform::identity(), + } + } +} + +impl GraphicsState { + pub(crate) fn combine(&mut self, other: &ExtGState) { + self.ext_g_state.combine(other); + } + + pub(crate) fn concat_transform(&mut self, transform: Transform) { + self.ctm = self.ctm.pre_concat(transform); + } + + pub(crate) fn transform(&self) -> Transform { + self.ctm + } + + pub(crate) fn ext_g_state(&self) -> &ExtGState { + &self.ext_g_state + } +} + +/// A collection of graphics states, simulates the stack-like +/// structure used. +#[derive(Debug, Hash, Eq, PartialEq)] +pub struct GraphicsStates { + graphics_states: Vec, +} + +impl GraphicsStates { + pub(crate) fn new() -> Self { + GraphicsStates { + graphics_states: vec![GraphicsState::default()], + } + } + + pub(crate) fn cur(&self) -> &GraphicsState { + self.graphics_states.last().unwrap() + } + + pub(crate) fn cur_mut(&mut self) -> &mut GraphicsState { + self.graphics_states.last_mut().unwrap() + } + + pub(crate) fn save_state(&mut self) { + let state = self.cur(); + self.graphics_states.push(state.clone()) + } + + pub(crate) fn restore_state(&mut self) { + self.graphics_states.pop(); + } + + pub(crate) fn combine(&mut self, other: &ExtGState) { + self.cur_mut().combine(other); + } + + pub(crate) fn transform(&mut self, transform: Transform) { + self.cur_mut().concat_transform(transform); + } + + pub(crate) fn transform_bbox(&self, bbox: Rect) -> Rect { + // Important: This does not take the root transform of the + // corresponding ContentBuilder into account, because we + // want it to be in krilla coordinates, not in PDF + // coordinates. + bbox.transform(self.cur().transform()).unwrap() + } +} diff --git a/vendor/krilla/src/graphics/icc.rs b/vendor/krilla/src/graphics/icc.rs new file mode 100644 index 00000000..5457235c --- /dev/null +++ b/vendor/krilla/src/graphics/icc.rs @@ -0,0 +1,218 @@ +//! Using ICC profiles. + +use std::ops::Deref; +use std::ops::DerefMut; +use std::sync::Arc; + +use pdf_writer::{Finish, Name, Ref}; + +use crate::chunk_container::ChunkContainer; +use crate::resource; +use crate::resource::Resourceable; +use crate::serialize::{Cacheable, SerializeContext}; +use crate::stream::{deflate_encode, FilterStreamBuilder}; +use crate::util::Prehashed; + +/// An ICC profile. +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub struct ICCProfile(Arc>); + +impl ICCProfile { + /// Create a new ICC profile. + /// + /// Returns `None` if the metadata of the profile couldn't be read or if the + /// number of channels of the underlying data does not correspond to the one + /// indicated in the constant parameter. + pub fn new(data: &[u8]) -> Option { + let metadata = ICCMetadata::from_data(data)?; + + if metadata.color_space.num_components() != C { + return None; + } + + Some(Self(Arc::new(Prehashed::new(Repr { + data: deflate_encode(data), + metadata, + })))) + } + + pub(crate) fn metadata(&self) -> &ICCMetadata { + &self.0.metadata + } +} + +impl Cacheable for ICCProfile { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let mut chunk = sc.new_chunk(); + let icc_stream = FilterStreamBuilder::new_from_deflated(&self.0.deref().data) + .finish(&sc.serialize_settings()); + + let mut icc_profile = chunk.icc_profile(root_ref, icc_stream.encoded_data()); + icc_profile.n(C as i32).range([0.0, 1.0].repeat(C as usize)); + icc_stream.write_filters(icc_profile.deref_mut().deref_mut()); + icc_profile.finish(); + chunk_container.streams.icc_profiles.push(chunk); + } +} + +#[cfg(feature = "raster-images")] +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) enum GenericICCProfile { + Luma(ICCProfile<1>), + Rgb(ICCProfile<3>), + Cmyk(ICCProfile<4>), +} + +#[cfg(feature = "raster-images")] +impl GenericICCProfile { + pub(crate) fn metadata(&self) -> &ICCMetadata { + match self { + GenericICCProfile::Luma(l) => l.metadata(), + GenericICCProfile::Rgb(r) => r.metadata(), + GenericICCProfile::Cmyk(c) => c.metadata(), + } + } +} + +#[cfg(feature = "raster-images")] +impl Cacheable for GenericICCProfile { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + match self { + GenericICCProfile::Luma(l) => l.serialize(sc, chunk_container, root_ref), + GenericICCProfile::Rgb(r) => r.serialize(sc, chunk_container, root_ref), + GenericICCProfile::Cmyk(c) => c.serialize(sc, chunk_container, root_ref), + } + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct ICCBasedColorSpace(pub(crate) ICCProfile); + +impl Cacheable for ICCBasedColorSpace { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let icc_ref = sc.register_cacheable(chunk_container, self.0.clone()); + + let chunk = &mut chunk_container.non_stream.color_spaces; + + let mut array = chunk.indirect(root_ref).array(); + array.item(Name(b"ICCBased")); + array.item(icc_ref); + array.finish(); + } +} + +impl Resourceable for ICCBasedColorSpace { + type Resource = resource::ColorSpace; +} + +#[derive(Clone, Hash, Debug, Eq, PartialEq)] +pub(crate) enum ICCColorSpace { + Xyz, + Lab, + Luv, + Ycbr, + Yxy, + Lms, + Rgb, + Gray, + Hsv, + Hls, + Cmyk, + Cmy, + OneClr, + ThreeClr, + FourClr, + // There are more, but those should be the most important + // ones. +} + +impl ICCColorSpace { + pub(crate) fn num_components(&self) -> u8 { + match self { + ICCColorSpace::Xyz => 3, + ICCColorSpace::Lab => 3, + ICCColorSpace::Luv => 3, + ICCColorSpace::Ycbr => 3, + ICCColorSpace::Yxy => 3, + ICCColorSpace::Lms => 3, + ICCColorSpace::Rgb => 3, + ICCColorSpace::Gray => 1, + ICCColorSpace::Hsv => 3, + ICCColorSpace::Hls => 3, + ICCColorSpace::Cmyk => 4, + ICCColorSpace::Cmy => 3, + ICCColorSpace::OneClr => 1, + ICCColorSpace::ThreeClr => 3, + ICCColorSpace::FourClr => 4, + } + } +} + +impl TryFrom for ICCColorSpace { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + 0x58595A20 => Ok(ICCColorSpace::Xyz), + 0x4C616220 => Ok(ICCColorSpace::Lab), + 0x4C757620 => Ok(ICCColorSpace::Luv), + 0x59436272 => Ok(ICCColorSpace::Ycbr), + 0x59787920 => Ok(ICCColorSpace::Yxy), + 0x4C4D5320 => Ok(ICCColorSpace::Lms), + 0x52474220 => Ok(ICCColorSpace::Rgb), + 0x47524159 => Ok(ICCColorSpace::Gray), + 0x48535620 => Ok(ICCColorSpace::Hsv), + 0x484C5320 => Ok(ICCColorSpace::Hls), + 0x434D594B => Ok(ICCColorSpace::Cmyk), + 0x434D5920 => Ok(ICCColorSpace::Cmy), + 0x31434C52 => Ok(ICCColorSpace::OneClr), + 0x33434C52 => Ok(ICCColorSpace::ThreeClr), + 0x34434C52 => Ok(ICCColorSpace::FourClr), + _ => Err(()), + } + } +} + +#[derive(Clone, Hash, Debug, Eq, PartialEq)] +pub(crate) struct ICCMetadata { + pub(crate) major: u8, + pub(crate) minor: u8, + pub(crate) color_space: ICCColorSpace, +} + +impl ICCMetadata { + pub(crate) fn from_data(data: &[u8]) -> Option { + let major = *data.get(8)?; + let minor = *data.get(9)? >> 4; + let color_space = { + let marker = u32::from_be_bytes(data.get(16..20)?.try_into().ok()?); + ICCColorSpace::try_from(marker).ok()? + }; + Some(Self { + major, + minor, + color_space, + }) + } +} + +#[derive(Clone, Hash, Debug)] +struct Repr { + data: Vec, + metadata: ICCMetadata, +} diff --git a/vendor/krilla/src/graphics/image.rs b/vendor/krilla/src/graphics/image.rs new file mode 100644 index 00000000..22a68ca9 --- /dev/null +++ b/vendor/krilla/src/graphics/image.rs @@ -0,0 +1,835 @@ +//! Creating and using bitmap images. +//! +//! krilla allows you to add bitmap images to your PDF very easily. +//! The currently supported formats include +//! - PNG +//! - JPG +//! - GIF +//! - WEBP +//! - Custom image formats via [`CustomImage`] + +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::io::Cursor; +use std::ops::DerefMut; +use std::sync::Arc; + +use pdf_writer::{Finish, Name, Ref}; +use png::{BitDepth, ColorType, Transformations}; +use zune_jpeg::zune_core::colorspace::ColorSpace; +use zune_jpeg::JpegDecoder; + +use crate::chunk_container::ChunkContainer; +use crate::configure::ValidationError; +use crate::error::KrillaError; +use crate::graphics::color::DEVICE_GRAY; +use crate::graphics::color::{cmyk, luma, rgb}; +use crate::graphics::icc::{GenericICCProfile, ICCBasedColorSpace, ICCProfile}; +use crate::serialize::SerializeContext; +use crate::stream::{deflate_encode, FilterStreamBuilder}; +use crate::util::{set_colorspace, Deferred, NameExt, SipHashable}; +use crate::Data; + +/// The number of bits per color component. +#[derive(Debug, Hash, Eq, PartialEq, Copy, Clone)] +pub enum BitsPerComponent { + /// Eight bits per component. + Eight, + /// Sixteen bits per component. + Sixteen, +} + +impl BitsPerComponent { + fn as_u8(&self) -> u8 { + match self { + BitsPerComponent::Eight => 8, + BitsPerComponent::Sixteen => 16, + } + } +} + +/// The color space of the image. +#[derive(Debug, Hash, Eq, PartialEq, Copy, Clone)] +pub enum ImageColorspace { + /// The RGB color space. + Rgb, + /// The luma color space. + Luma, + /// The CMYK color space. + Cmyk, +} + +impl ImageColorspace { + fn num_components(&self) -> u8 { + match self { + ImageColorspace::Luma => 1, + ImageColorspace::Rgb => 3, + ImageColorspace::Cmyk => 4, + } + } + + fn matches_icc_profile(&self, profile: &GenericICCProfile) -> bool { + match self { + ImageColorspace::Rgb => matches!(profile, GenericICCProfile::Rgb(_)), + ImageColorspace::Luma => matches!(profile, GenericICCProfile::Luma(_)), + ImageColorspace::Cmyk => matches!(profile, GenericICCProfile::Cmyk(_)), + } + } +} + +impl TryFrom for ImageColorspace { + type Error = (); + + fn try_from(value: ColorSpace) -> Result { + match value { + ColorSpace::RGB => Ok(ImageColorspace::Rgb), + ColorSpace::RGBA => Ok(ImageColorspace::Rgb), + ColorSpace::YCbCr => Ok(ImageColorspace::Rgb), + ColorSpace::Luma => Ok(ImageColorspace::Luma), + ColorSpace::LumaA => Ok(ImageColorspace::Luma), + ColorSpace::YCCK => Ok(ImageColorspace::Cmyk), + ColorSpace::CMYK => Ok(ImageColorspace::Cmyk), + _ => Err(()), + } + } +} + +struct SampledRepr { + color_channel: Vec, + alpha_channel: Option>, + bits_per_component: BitsPerComponent, +} + +struct JpegRepr { + data: Data, + bits_per_component: BitsPerComponent, + invert_cmyk: bool, +} + +enum Repr { + Sampled(SampledRepr), + Jpeg(JpegRepr), +} + +impl Repr { + fn bits_per_component(&self) -> BitsPerComponent { + match self { + Repr::Sampled(s) => s.bits_per_component, + Repr::Jpeg(j) => j.bits_per_component, + } + } +} + +/// A trait for custom images, which you can use if the +/// current methods provided by krilla (JPEG/PNG/WEBP/GIF) images +/// are not suitable for your own purpose. +/// +/// Note that a struct implementing this trait should be cheap to +/// hash and clone, otherwise performance might be bad! +pub trait CustomImage: Hash + Clone + Send + Sync + 'static { + /// Return the raw bytes of the color channel. + fn color_channel(&self) -> &[u8]; + /// Return the raw bytes of the alpha channel, if available. + fn alpha_channel(&self) -> Option<&[u8]>; + /// Return the bits per component of the image. + fn bits_per_component(&self) -> BitsPerComponent; + /// Return the dimensions of the image. + fn size(&self) -> (u32, u32); + /// Return the ICC profile of the image, if available. + fn icc_profile(&self) -> Option<&[u8]>; + /// Return the color space of the image. + fn color_space(&self) -> ImageColorspace; +} + +struct ImageMetadata { + size: (u32, u32), + color_space: ImageColorspace, + has_alpha: bool, + bits_per_component: BitsPerComponent, + icc: Option, +} + +struct ImageRepr { + inner: Deferred>, + metadata: ImageMetadata, + sip: u128, + interpolate: bool, +} + +impl ImageRepr { + fn size(&self) -> (u32, u32) { + self.metadata.size + } + + fn icc(&self) -> Option { + self.metadata.icc.clone() + } + + fn color_space(&self) -> ImageColorspace { + self.metadata.color_space + } +} + +impl Debug for ImageRepr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ImageRepr {{..}}") + } +} + +impl Hash for ImageRepr { + fn hash(&self, state: &mut H) { + self.sip.hash(state); + } +} + +impl PartialEq for ImageRepr { + fn eq(&self, other: &Self) -> bool { + self.sip == other.sip + } +} + +impl Eq for ImageRepr {} + +/// A bitmap image. +/// +/// This type is cheap to hash and clone, but expensive to create. +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub struct Image(Arc); + +impl Image { + /// Create a new bitmap image from a `.png` file. + pub fn from_png(data: Data, interpolate: bool) -> Result { + let hash = data.as_ref().sip_hash(); + let metadata = png_metadata(data.as_ref())?; + + Ok(Self(Arc::new(ImageRepr { + inner: Deferred::new(move || decode_png(data.as_ref())), + metadata, + sip: hash, + interpolate, + }))) + } + + /// Create a new bitmap image from a `.jpg` file. + pub fn from_jpeg(data: Data, interpolate: bool) -> Result { + let hash = data.as_ref().sip_hash(); + let metadata = jpeg_metadata(data.as_ref())?; + + Ok(Self(Arc::new(ImageRepr { + inner: Deferred::new(move || decode_jpeg(data)), + metadata, + sip: hash, + interpolate, + }))) + } + + /// Create a new bitmap image from a `.jpg` file with custom ICC profile. + #[doc(hidden)] + pub fn from_jpeg_with_icc( + data: Data, + icc_profile: Option, + interpolate: bool, + ) -> Result { + let hash = data.as_ref().sip_hash(); + let mut metadata = jpeg_metadata(data.as_ref())?; + let icc_profile = + icc_profile.and_then(|d| get_icc_profile_type(d.as_ref(), metadata.color_space)); + metadata.icc = icc_profile; + + Ok(Self(Arc::new(ImageRepr { + inner: Deferred::new(move || decode_jpeg(data)), + metadata, + sip: hash, + interpolate, + }))) + } + + /// Create a new bitmap image from a `.gif` file. + pub fn from_gif(data: Data, interpolate: bool) -> Result { + let hash = data.as_ref().sip_hash(); + let metadata = gif_metadata(data.as_ref())?; + + Ok(Self(Arc::new(ImageRepr { + inner: Deferred::new(move || decode_gif(data)), + metadata, + sip: hash, + interpolate, + }))) + } + + /// Create a new bitmap image from a `.webp` file. + /// + /// Returns `None` if krilla was unable to parse the file. + pub fn from_webp(data: Data, interpolate: bool) -> Result { + let hash = data.as_ref().sip_hash(); + let metadata = webp_metadata(data.as_ref())?; + + Ok(Self(Arc::new(ImageRepr { + inner: Deferred::new(move || decode_webp(data)), + metadata, + sip: hash, + interpolate, + }))) + } + + /// Create a new image from a custom image. + /// + /// Panics if the dimensions of the image and the length of the + /// data doesn't match. + pub fn from_custom(image: T, interpolate: bool) -> Result { + let hash = (image.clone(), interpolate).sip_hash(); + let metadata = ImageMetadata { + size: image.size(), + color_space: image.color_space(), + has_alpha: image.alpha_channel().is_some(), + bits_per_component: image.bits_per_component(), + icc: image + .icc_profile() + .and_then(|d| get_icc_profile_type(d, image.color_space())), + }; + + Ok(Self(Arc::new(ImageRepr { + inner: Deferred::new(move || { + let bytes_per_component = (image.bits_per_component().as_u8() / 8) as u32; + let color_channel_len = bytes_per_component + * image.color_space().num_components() as u32 + * metadata.size.0 + * metadata.size.1; + let color_channel = image.color_channel(); + assert_eq!(color_channel.len(), color_channel_len as usize); + + let alpha_channel_len = bytes_per_component * metadata.size.0 * metadata.size.1; + let alpha_channel = image.alpha_channel(); + if let Some(alpha_channel) = alpha_channel { + assert_eq!(alpha_channel.len(), alpha_channel_len as usize); + } + + Ok(Repr::Sampled(SampledRepr { + color_channel: deflate_encode(color_channel), + alpha_channel: image.alpha_channel().map(deflate_encode), + bits_per_component: image.bits_per_component(), + })) + }), + metadata, + sip: hash, + interpolate, + }))) + } + + /// Create a new RGB image from raw RGBA pixels. + pub fn from_rgba8(data: Vec, width: u32, height: u32) -> Self { + let hash = data.sip_hash(); + let metadata = ImageMetadata { + has_alpha: true, + size: (width, height), + bits_per_component: BitsPerComponent::Eight, + color_space: ImageColorspace::Rgb, + icc: None, + }; + + Self(Arc::new(ImageRepr { + inner: Deferred::new(move || { + let (color_channel, alpha_channel, bits_per_component) = + handle_u8_image(&data, ColorSpace::RGBA); + + Ok(Repr::Sampled(SampledRepr { + color_channel, + alpha_channel, + bits_per_component, + })) + }), + metadata, + sip: hash, + interpolate: false, + })) + } + + /// Return the size of the image. + pub fn size(&self) -> (u32, u32) { + self.0.size() + } + + fn icc(&self) -> Option { + self.0.icc() + } + + fn color_space(&self) -> ImageColorspace { + self.0.color_space() + } + + pub(crate) fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let soft_mask_id = self.0.metadata.has_alpha.then(|| { + sc.register_validation_error(ValidationError::Transparency(sc.location)); + sc.new_ref() + }); + + let icc_ref = self.icc().and_then(|ic| { + if sc + .serialize_settings() + .pdf_version() + .supports_icc(ic.metadata()) + && self.color_space().matches_icc_profile(&ic) + { + let ref_ = match ic { + GenericICCProfile::Luma(l) => { + sc.register_cacheable(chunk_container, ICCBasedColorSpace(l)) + } + GenericICCProfile::Rgb(r) => { + sc.register_cacheable(chunk_container, ICCBasedColorSpace(r)) + } + GenericICCProfile::Cmyk(c) => { + sc.register_cacheable(chunk_container, ICCBasedColorSpace(c)) + } + }; + + Some(ref_) + } else { + // Don't embed ICC profiles from images if the current + // PDF version does not support it. + None + } + }); + + if self.0.interpolate { + sc.register_validation_error(ValidationError::ImageInterpolation(sc.location)); + } + + let serialize_settings = sc.serialize_settings().clone(); + + let cs = { + let cs = match self.color_space() { + ImageColorspace::Rgb => { + rgb::color_space(sc.serialize_settings().no_device_cs).into() + } + ImageColorspace::Luma => { + luma::color_space(sc.serialize_settings().no_device_cs).into() + } + ImageColorspace::Cmyk => match cmyk::color_space(&sc.serialize_settings()) { + None => { + sc.register_validation_error(ValidationError::MissingCMYKProfile); + crate::color::DeviceColorSpace::Cmyk.into() + } + Some(cs) => cs.into(), + }, + }; + + sc.register_colorspace(chunk_container, cs) + }; + + let supports_bit_depth = sc + .serialize_settings() + .configuration + .version() + .supports_bit_depth(self.0.metadata.bits_per_component); + let location = sc.location; + let mut chunk = sc.new_chunk(); + + let chunk = Deferred::new(move || { + if !supports_bit_depth { + return Err(KrillaError::SixteenBitImage(self.clone(), location)); + } + + let repr = self + .0 + .inner + .wait() + .as_ref() + .map_err(|e| KrillaError::Image(self.clone(), location, e.clone()))?; + + let alpha_mask = match repr { + Repr::Sampled(sampled) => sampled.alpha_channel.as_ref().map(|mask_data| { + let soft_mask_id = soft_mask_id.unwrap(); + let mask_stream = FilterStreamBuilder::new_from_deflated(mask_data) + .finish(&serialize_settings); + let mut s_mask = chunk.image_xobject(soft_mask_id, mask_stream.encoded_data()); + mask_stream.write_filters(s_mask.deref_mut().deref_mut()); + s_mask.width(self.size().0 as i32); + s_mask.height(self.size().1 as i32); + s_mask.pair( + Name(b"ColorSpace"), + // Mask color space must be device gray -- see Table 145. + DEVICE_GRAY.to_pdf_name(), + ); + + if self.0.interpolate { + s_mask.interpolate(true); + } + + s_mask.bits_per_component(repr.bits_per_component().as_u8() as i32); + soft_mask_id + }), + Repr::Jpeg(_) => None, + }; + + let filter_stream = match repr { + Repr::Sampled(s) => FilterStreamBuilder::new_from_deflated(&s.color_channel) + .finish(&serialize_settings), + Repr::Jpeg(j) => FilterStreamBuilder::new_from_jpeg_data(j.data.as_ref()) + .finish(&serialize_settings), + }; + + let mut image_x_object = chunk.image_xobject(root_ref, filter_stream.encoded_data()); + filter_stream.write_filters(image_x_object.deref_mut().deref_mut()); + image_x_object.width(self.size().0 as i32); + image_x_object.height(self.size().1 as i32); + + if let Some(icc_ref) = icc_ref { + image_x_object.pair(Name(b"ColorSpace"), icc_ref); + } else { + set_colorspace(cs, image_x_object.deref_mut()); + } + + if self.0.interpolate { + image_x_object.interpolate(true); + } + + // Photoshop CMYK images need to be inverted, see + // https://github.com/sile-typesetter/libtexpdf/blob/1891bee5e0b73165e4a259f910d3ea3fe1df0b42/jpegimage.c#L25-L51 + // I'm not sure if this applies to all JPEG CMYK images out there, but for now we just + // always do it. In libtexpdf, they only seem to do it if they can find the Adobe APP + // marker. + if let Repr::Jpeg(j) = repr { + if j.invert_cmyk { + image_x_object + .decode([1.0, 0.0].repeat(self.color_space().num_components() as usize)); + } + } + + image_x_object.bits_per_component(repr.bits_per_component().as_u8() as i32); + if let Some(soft_mask_id) = alpha_mask { + image_x_object.s_mask(soft_mask_id); + } + image_x_object.finish(); + + Ok(chunk) + }); + + chunk_container.streams.images.push(chunk); + } +} + +const PNG_TRANSFORMATIONS: Transformations = Transformations::EXPAND; + +fn png_metadata(data: &[u8]) -> Result { + let cursor = Cursor::new(data); + let mut decoder = png::Decoder::new(cursor); + decoder.set_transformations(PNG_TRANSFORMATIONS); + let reader = decoder + .read_info() + .map_err(|e| e.to_string().to_ascii_lowercase())?; + let info = reader.info(); + + let size = (info.width, info.height); + let (color_type, bit_depth) = reader.output_color_type(); + let bits_per_component = match bit_depth { + // We will normalize to 8 when decoding. + BitDepth::One | BitDepth::Two | BitDepth::Four | BitDepth::Eight => BitsPerComponent::Eight, + BitDepth::Sixteen => BitsPerComponent::Sixteen, + }; + + let (image_color_space, has_alpha) = match color_type { + ColorType::Grayscale => (ImageColorspace::Luma, false), + ColorType::GrayscaleAlpha => (ImageColorspace::Luma, true), + ColorType::Rgb => (ImageColorspace::Rgb, false), + ColorType::Rgba => (ImageColorspace::Rgb, true), + ColorType::Indexed => { + return Err("image uses an indexed color space, which is unsupported".to_string()) + } + }; + let icc = info + .icc_profile + .as_ref() + .and_then(|i| get_icc_profile_type(i, image_color_space)); + + Ok(ImageMetadata { + has_alpha, + size, + bits_per_component, + color_space: image_color_space, + icc, + }) +} + +fn decode_png(data: &[u8]) -> Result { + let cursor = Cursor::new(data); + let mut decoder = png::Decoder::new(cursor); + decoder.set_transformations(PNG_TRANSFORMATIONS); + let mut reader = decoder + .read_info() + .map_err(|e| e.to_string().to_ascii_lowercase())?; + let mut img_data = vec![0; reader.output_buffer_size().ok_or("image is too large")?]; + let _ = reader + .next_frame(&mut img_data) + .map_err(|e| e.to_string())?; + let (color_type, bit_depth) = reader.output_color_type(); + + let color_space = match color_type { + ColorType::Rgb => ColorSpace::RGB, + ColorType::Rgba => ColorSpace::RGBA, + ColorType::Grayscale => ColorSpace::Luma, + ColorType::GrayscaleAlpha => ColorSpace::LumaA, + _ => unreachable!(), + }; + + let (color_channel, alpha_channel, bits_per_component) = match bit_depth { + BitDepth::Eight => handle_u8_image(&img_data, color_space), + BitDepth::Sixteen => handle_u16_image(&img_data, color_space), + _ => return Err("image has an unsupported bit-depth".to_string()), + }; + + Ok(Repr::Sampled(SampledRepr { + color_channel, + alpha_channel, + bits_per_component, + })) +} + +fn jpeg_metadata(data: &[u8]) -> Result { + let reader = Cursor::new(data); + let mut decoder = JpegDecoder::new(reader); + decoder + .decode_headers() + .map_err(|e| e.to_string().to_ascii_lowercase())?; + + let size = { + let dimensions = decoder + .dimensions() + .ok_or("failed to read image dimensions".to_string())?; + (dimensions.0 as u32, dimensions.1 as u32) + }; + + let image_color_space = decoder + .input_colorspace() + .and_then(|c| c.try_into().ok()) + .ok_or("failed to read image colorspace".to_string())?; + + let icc = decoder + .icc_profile() + .and_then(|d| get_icc_profile_type(&d, image_color_space)); + + Ok(ImageMetadata { + has_alpha: false, + size, + bits_per_component: BitsPerComponent::Eight, + color_space: image_color_space, + icc, + }) +} + +fn decode_jpeg(data: Data) -> Result { + let reader = Cursor::new(data.as_ref()); + let mut decoder = JpegDecoder::new(reader); + decoder + .decode_headers() + .map_err(|e| e.to_string().to_ascii_lowercase())?; + + let input_color_space = decoder + .input_colorspace() + .ok_or("failed to read image colorspace".to_string())?; + + if matches!( + input_color_space, + ColorSpace::Luma + | ColorSpace::YCbCr + | ColorSpace::RGB + | ColorSpace::CMYK + | ColorSpace::YCCK + ) { + Ok(Repr::Jpeg(JpegRepr { + data, + bits_per_component: BitsPerComponent::Eight, + invert_cmyk: matches!(input_color_space, ColorSpace::YCCK | ColorSpace::CMYK), + })) + } else { + // JPEGs shouldn't be able to have a different color space? + Err("image has an unknown color space".to_string()) + } +} + +fn decode_gif(data: Data) -> Result { + let mut decoder = gif::DecodeOptions::new(); + decoder.set_color_output(gif::ColorOutput::RGBA); + let mut decoder = decoder + .read_info(data.as_ref()) + .map_err(|e| e.to_string().to_ascii_lowercase())?; + let first_frame = decoder + .read_next_frame() + .map_err(|e| e.to_string())? + .ok_or("GIF image seems to be empty".to_string())?; + + let (color_channel, alpha_channel, bits_per_component) = + handle_u8_image(first_frame.buffer.as_ref(), ColorSpace::RGBA); + + Ok(Repr::Sampled(SampledRepr { + color_channel, + alpha_channel, + bits_per_component, + })) +} + +fn gif_metadata(data: &[u8]) -> Result { + let size = imagesize::blob_size(data).map_err(|e| e.to_string().to_ascii_lowercase())?; + + Ok(ImageMetadata { + // We always decode GIFs using RGBA, see `decode_gif` + has_alpha: true, + bits_per_component: BitsPerComponent::Eight, + size: (size.width as u32, size.height as u32), + color_space: ImageColorspace::Rgb, + icc: None, + }) +} + +fn webp_metadata(data: &[u8]) -> Result { + let mut decoder = image_webp::WebPDecoder::new(std::io::Cursor::new(data)) + .map_err(|e| e.to_string().to_ascii_lowercase())?; + let size = decoder.dimensions(); + let color_space = ImageColorspace::Rgb; + let icc = decoder + .icc_profile() + .map_err(|e| e.to_string().to_ascii_lowercase())? + .and_then(|d| get_icc_profile_type(&d, color_space)); + + Ok(ImageMetadata { + has_alpha: decoder.has_alpha(), + bits_per_component: BitsPerComponent::Eight, + size, + color_space, + icc, + }) +} + +fn decode_webp(data: Data) -> Result { + let mut decoder = image_webp::WebPDecoder::new(std::io::Cursor::new(data.as_ref())) + .map_err(|e| e.to_string().to_ascii_lowercase())?; + let mut first_frame = vec![0; decoder.output_buffer_size().ok_or("image is too large")?]; + decoder + .read_image(&mut first_frame) + .map_err(|e| e.to_string().to_ascii_lowercase())?; + + let color_space = if decoder.has_alpha() { + ColorSpace::RGBA + } else { + ColorSpace::RGB + }; + + let (color_channel, alpha_channel, bits_per_component) = + handle_u8_image(&first_frame, color_space); + + Ok(Repr::Sampled(SampledRepr { + color_channel, + alpha_channel, + bits_per_component, + })) +} + +fn handle_u8_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, BitsPerComponent) { + let mut alphas = if cs.has_alpha() { + Vec::with_capacity(data.len() / cs.num_components()) + } else { + Vec::new() + }; + + let color_channel = match cs { + ColorSpace::RGB => deflate_encode(data), + ColorSpace::RGBA => { + let mut buf = Vec::with_capacity(data.len() * 3 / 4); + data.chunks_exact(4).for_each(|data| { + buf.extend_from_slice(&data[0..3]); + alphas.push(data[3]); + }); + deflate_encode(&buf) + } + ColorSpace::Luma => deflate_encode(data), + ColorSpace::LumaA => { + let mut buf = Vec::with_capacity(data.len() / 2); + data.chunks_exact(2).for_each(|data| { + buf.push(data[0]); + alphas.push(data[1]); + }); + deflate_encode(&buf) + } + // PNG/WEBP/GIF only support those three, so should be enough? + _ => unimplemented!(), + }; + + let alpha_channel = if !alphas.is_empty() && alphas.iter().any(|v| *v != 255) { + Some(deflate_encode(&alphas)) + } else { + None + }; + + (color_channel, alpha_channel, BitsPerComponent::Eight) +} + +fn handle_u16_image(data: &[u8], cs: ColorSpace) -> (Vec, Option>, BitsPerComponent) { + let mut alphas = if cs.has_alpha() { + Vec::with_capacity(data.len() / cs.num_components()) + } else { + Vec::new() + }; + + let encoded_image = match cs { + ColorSpace::RGB => deflate_encode(data), + ColorSpace::RGBA => { + let mut buf = Vec::with_capacity(data.len() * 3 / 4); + data.chunks_exact(8).for_each(|data| { + buf.extend_from_slice(&data[0..6]); + alphas.extend_from_slice(&data[6..]); + }); + deflate_encode(&buf) + } + ColorSpace::Luma => deflate_encode(data), + ColorSpace::LumaA => { + let mut buf = Vec::with_capacity(data.len() / 2); + data.chunks_exact(4).for_each(|data| { + buf.extend_from_slice(&data[0..2]); + alphas.extend_from_slice(&data[2..]); + }); + deflate_encode(&buf) + } + // PNG/WEBP/GIF only support those three, so should be enough? + _ => unimplemented!(), + }; + + let encoded_mask = if !alphas.is_empty() { + Some(deflate_encode(&alphas)) + } else { + None + }; + + (encoded_image, encoded_mask, BitsPerComponent::Sixteen) +} + +fn get_icc_profile_type(data: &[u8], color_space: ImageColorspace) -> Option { + let wrapper = match color_space { + ImageColorspace::Rgb => GenericICCProfile::Rgb(ICCProfile::new(data)?), + ImageColorspace::Luma => GenericICCProfile::Luma(ICCProfile::new(data)?), + ImageColorspace::Cmyk => GenericICCProfile::Cmyk(ICCProfile::new(data)?), + }; + + Some(wrapper) +} + +#[cfg(test)] +mod tests { + use crate::image::Image; + use std::sync::Arc; + + #[test] + fn invalid_png_image() { + let e = Image::from_png(Arc::new(b"dfngiudfg".to_vec()).into(), false); + assert!(e.is_err()); + } + + #[test] + fn invalid_jpeg_image() { + let e = Image::from_jpeg(Arc::new(b"dfngiudfg".to_vec()).into(), false); + assert!(e.is_err()); + } +} diff --git a/vendor/krilla/src/graphics/mask.rs b/vendor/krilla/src/graphics/mask.rs new file mode 100644 index 00000000..ffbb3de2 --- /dev/null +++ b/vendor/krilla/src/graphics/mask.rs @@ -0,0 +1,128 @@ +//! Alpha and luminosity masks. + +use pdf_writer::{Finish, Name, Ref}; + +use crate::chunk_container::ChunkContainer; +use crate::geom::{Rect, Transform}; +use crate::graphics::shading_function::{GradientProperties, ShadingFunction}; +use crate::graphics::xobject::XObject; +use crate::resource; +use crate::resource::Resourceable; +use crate::serialize::{Cacheable, SerializeContext}; +use crate::stream::Stream; +use crate::stream::StreamBuilder; + +/// A mask. Can be a luminance mask or an alpha mask. +/// +/// IMPORTANT: Note that you must only use a mask in the document that you created it with! +/// If you use it in a different document, you will end up with an invalid PDF file. +// TODO: Maybe we can enfore the above somehow? +#[derive(PartialEq, Eq, Debug, Hash)] +pub struct Mask { + /// The stream of the mask. + stream: Stream, + /// The type of the mask. + mask_type: MaskType, + /// A custom bbox of the mask. The only reason we need this is that for gradients with + /// transparencies, we create a custom mask where we call the shading operator. In this case, + /// we want to manually set the bbox of the underlying XObject to match the shape that the + /// gradient is being applied to. + custom_bbox: Option, +} + +impl Mask { + /// Create a new mask. `stream` contains the content description + /// of the mask, and `mask_type` indicates the type of mask. + pub fn new(stream: Stream, mask_type: MaskType) -> Self { + Self { + stream, + mask_type, + custom_bbox: None, + } + } + + /// Create a new mask for a shading to encode the opacity channels. + pub(crate) fn new_from_shading( + gradient_properties: GradientProperties, + shading_transform: Transform, + bbox: Rect, + serializer_context: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) -> Option { + match &gradient_properties { + GradientProperties::RadialAxialGradient(rag) => { + if rag.stops.iter().all(|s| s.opacity.get() == 1.0) { + return None; + } + } + GradientProperties::PostScriptGradient(psg) => { + if psg.stops.iter().all(|s| s.opacity.get() == 1.0) { + return None; + } + } + } + + let shading_function = ShadingFunction::new(gradient_properties, true); + + let shading_stream = { + let mut builder = StreamBuilder::new(serializer_context, chunk_container); + let mut surface = builder.surface(); + surface.push_transform(&shading_transform); + surface.draw_shading(&shading_function); + surface.pop(); + surface.finish(); + builder.finish() + }; + + Some(Self { + stream: shading_stream, + mask_type: MaskType::Luminosity, + custom_bbox: Some(bbox), + }) + } +} + +/// A mask type. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum MaskType { + /// A luminosity mask. + Luminosity, + /// An alpha mask. + Alpha, +} + +impl MaskType { + /// Return the PDF name of the mask type. + pub(crate) fn to_name(self) -> Name<'static> { + match self { + MaskType::Alpha => Name(b"Alpha"), + MaskType::Luminosity => Name(b"Luminosity"), + } + } +} + +impl Cacheable for Mask { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let x_object = sc.register_cacheable( + chunk_container, + XObject::new(self.stream, false, true, self.custom_bbox), + ); + + let chunk = &mut chunk_container.non_stream.masks; + let mut dict = chunk.indirect(root_ref).dict(); + dict.pair(Name(b"Type"), Name(b"Mask")); + dict.pair(Name(b"S"), self.mask_type.to_name()); + dict.pair(Name(b"G"), x_object); + + dict.finish(); + } +} + +impl Resourceable for Mask { + type Resource = resource::XObject; +} diff --git a/vendor/krilla/src/graphics/mod.rs b/vendor/krilla/src/graphics/mod.rs new file mode 100644 index 00000000..fa15fdab --- /dev/null +++ b/vendor/krilla/src/graphics/mod.rs @@ -0,0 +1,14 @@ +pub mod blend; +pub mod color; +pub mod graphic; +pub(crate) mod graphics_state; +pub mod icc; +#[cfg(feature = "raster-images")] +pub mod image; +pub mod mask; +pub mod paint; +pub(crate) mod separation; +pub(crate) mod shading_function; +pub(crate) mod shading_pattern; +pub(crate) mod tiling_pattern; +pub(crate) mod xobject; diff --git a/vendor/krilla/src/graphics/paint.rs b/vendor/krilla/src/graphics/paint.rs new file mode 100644 index 00000000..cd4413c4 --- /dev/null +++ b/vendor/krilla/src/graphics/paint.rs @@ -0,0 +1,434 @@ +//! Paints that can be used for filling and stroking text or paths. + +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use pdf_writer::types::{LineCapStyle, LineJoinStyle}; + +use crate::color::SpecialColor; +use crate::geom::Transform; +use crate::graphics::color::{cmyk, luma, rgb, Color}; +use crate::num::NormalizedF32; +use crate::stream::Stream; + +/// A linear gradient. +#[derive(Debug, Clone, PartialEq)] +pub struct LinearGradient { + /// The x coordinate of the first point. + pub x1: f32, + /// The y coordinate of the first point. + pub y1: f32, + /// The x coordinate of the second point. + pub x2: f32, + /// The y coordinate of the second point. + pub y2: f32, + /// A transform that should be applied to the linear gradient. + pub transform: Transform, + /// The spread method of the linear gradient. + pub spread_method: SpreadMethod, + /// The color stops of the linear gradient. + /// + /// Note that all stops need to be in the same color space. + pub stops: Vec, + /// Whether the gradient should be anti-aliased. + pub anti_alias: bool, +} + +impl Eq for LinearGradient {} + +impl Hash for LinearGradient { + fn hash(&self, state: &mut H) { + self.x1.to_bits().hash(state); + self.y1.to_bits().hash(state); + self.x2.to_bits().hash(state); + self.y2.to_bits().hash(state); + self.transform.hash(state); + self.spread_method.hash(state); + self.stops.hash(state); + self.anti_alias.hash(state); + } +} + +/// A radial gradient. +#[derive(Debug, Clone, PartialEq)] +pub struct RadialGradient { + /// The x coordinate of the start circle. + pub fx: f32, + /// The y coordinate of the start circle. + pub fy: f32, + /// The radius of the start circle. + pub fr: f32, + /// The x coordinate of the end circle. + pub cx: f32, + /// The y coordinate of the end circle. + pub cy: f32, + /// The radius of the end circle. + pub cr: f32, + /// A transform that should be applied to the radial gradient. + pub transform: Transform, + /// The spread method of the radial gradient. + /// + /// _Note_: The spread methods `Repeat`/`Reflect` are currently not supported + /// for radial gradients, and will fall back to `Pad`. + pub spread_method: SpreadMethod, + /// The color stops of the radial gradient. + /// + /// Note that all stops need to be in the same color space. + pub stops: Vec, + /// Whether the gradient should be anti-aliased. + pub anti_alias: bool, +} + +impl Eq for RadialGradient {} + +impl Hash for RadialGradient { + fn hash(&self, state: &mut H) { + self.fx.to_bits().hash(state); + self.fy.to_bits().hash(state); + self.fr.to_bits().hash(state); + self.cx.to_bits().hash(state); + self.cy.to_bits().hash(state); + self.cr.to_bits().hash(state); + self.transform.hash(state); + self.spread_method.hash(state); + self.stops.hash(state); + self.anti_alias.hash(state); + } +} + +/// A sweep gradient. +/// +/// Angles start from the right and go counter-clockwise with increasing values. +#[derive(Debug, Clone, PartialEq)] +pub struct SweepGradient { + /// The x coordinate of the center. + pub cx: f32, + /// The y coordinate of the center. + pub cy: f32, + /// The start angle. + pub start_angle: f32, + /// The end angle. + pub end_angle: f32, + /// A transform that should be applied to the sweep gradient. + pub transform: Transform, + /// The spread method of the sweep gradient. + pub spread_method: SpreadMethod, + /// The color stops of the sweep gradient. + /// + /// Note that all stops need to be in the same color space. + pub stops: Vec, + /// Whether the gradient should be anti-aliased. + pub anti_alias: bool, +} + +impl Eq for SweepGradient {} + +impl Hash for SweepGradient { + fn hash(&self, state: &mut H) { + self.cx.to_bits().hash(state); + self.cy.to_bits().hash(state); + self.start_angle.to_bits().hash(state); + self.end_angle.to_bits().hash(state); + self.transform.hash(state); + self.spread_method.hash(state); + self.stops.hash(state); + self.anti_alias.hash(state); + } +} + +/// A pattern. +/// +/// IMPORTANT: Note that you must only use a mask in the document that you created it with! +/// If you use it in a different document, you will end up with an invalid PDF file. +#[derive(Debug, PartialEq, Clone)] +pub struct Pattern { + /// The stream of the pattern. + pub stream: Stream, + /// A transform that should be applied to the pattern. + pub transform: Transform, + /// The width of the pattern. + pub width: f32, + /// The height of the pattern. + pub height: f32, +} + +impl Eq for Pattern {} + +impl Hash for Pattern { + fn hash(&self, state: &mut H) { + self.stream.hash(state); + self.transform.hash(state); + self.width.to_bits().hash(state); + self.height.to_bits().hash(state); + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub(crate) enum InnerPaint { + Color(Color), + LinearGradient(LinearGradient), + RadialGradient(RadialGradient), + SweepGradient(SweepGradient), + Pattern(Arc), +} + +/// A paint. +/// +/// You cannot construct this type directly, but instead can convert +/// into it by calling `into` on the various types of paint, such as linear +/// gradients and patterns. +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub struct Paint(pub(crate) InnerPaint); + +impl Paint { + pub(crate) fn as_rgb(&self) -> Option { + match &self.0 { + InnerPaint::Color(c) => match c { + Color::Regular(c) => c.as_rgb(), + Color::Special(SpecialColor::Separation(c)) => c.space.fallback.as_rgb(), + }, + _ => None, + } + } +} + +impl From for Paint { + fn from(value: rgb::Color) -> Self { + Paint(InnerPaint::Color(value.into())) + } +} + +impl From for Paint { + fn from(value: luma::Color) -> Self { + Paint(InnerPaint::Color(value.into())) + } +} + +impl From for Paint { + fn from(value: cmyk::Color) -> Self { + Paint(InnerPaint::Color(value.into())) + } +} + +impl From for Paint { + fn from(value: Color) -> Self { + Paint(InnerPaint::Color(value)) + } +} + +impl From for Paint { + fn from(value: LinearGradient) -> Self { + Paint(InnerPaint::LinearGradient(value)) + } +} + +impl From for Paint { + fn from(value: RadialGradient) -> Self { + Paint(InnerPaint::RadialGradient(value)) + } +} + +impl From for Paint { + fn from(value: SweepGradient) -> Self { + Paint(InnerPaint::SweepGradient(value)) + } +} + +impl From for Paint { + fn from(value: Pattern) -> Self { + Paint(InnerPaint::Pattern(Arc::new(value))) + } +} + +/// A spread method. +#[derive(Debug, Hash, Eq, PartialEq, Copy, Clone, Default)] +pub enum SpreadMethod { + /// The pad spread method. + #[default] + Pad, + /// The reflect spread method. + Reflect, + /// The repeat spread method. + Repeat, +} + +/// A color stop in a gradient. +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +#[allow(private_bounds)] +pub struct Stop { + /// The normalized offset of the stop. + pub offset: NormalizedF32, + /// The color of the stop. + pub color: Color, + /// The opacity of the stop. + pub opacity: NormalizedF32, +} + +/// A line cap. +#[derive(Eq, PartialEq, Debug, Clone, Copy, Default, Hash)] +pub enum LineCap { + /// The butt line cap. + #[default] + Butt, + /// The round line cap. + Round, + /// The square line cap. + Square, +} + +impl LineCap { + pub(crate) fn to_pdf_line_cap(self) -> LineCapStyle { + match self { + LineCap::Butt => LineCapStyle::ButtCap, + LineCap::Round => LineCapStyle::RoundCap, + LineCap::Square => LineCapStyle::ProjectingSquareCap, + } + } +} + +/// A line join. +#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Hash)] +pub enum LineJoin { + /// The miter line join. + #[default] + Miter, + /// The round line join. + Round, + /// The bevel line join. + Bevel, +} + +impl LineJoin { + pub(crate) fn to_pdf_line_join(self) -> LineJoinStyle { + match self { + LineJoin::Miter => LineJoinStyle::MiterJoin, + LineJoin::Round => LineJoinStyle::RoundJoin, + LineJoin::Bevel => LineJoinStyle::BevelJoin, + } + } +} + +/// A stroke dash. +#[derive(Debug, Clone, PartialEq)] +pub struct StrokeDash { + /// The dash array. + pub array: Vec, + /// The offset of the dash. + pub offset: f32, +} + +impl Eq for StrokeDash {} + +impl Hash for StrokeDash { + fn hash(&self, state: &mut H) { + for el in &self.array { + el.to_bits().hash(state); + } + + self.offset.to_bits().hash(state); + } +} + +/// A stroke. +#[derive(Debug, Clone, PartialEq)] +pub struct Stroke { + /// The paint of the stroke. + pub paint: Paint, + /// The width of the stroke. + pub width: f32, + /// The miter limit of the stroke. + pub miter_limit: f32, + /// The line cap of the stroke. + pub line_cap: LineCap, + /// The line join of the stroke. + pub line_join: LineJoin, + /// The opacity of the stroke. + pub opacity: NormalizedF32, + /// The (optional) dash of the stroke. + pub dash: Option, +} + +impl Eq for Stroke {} + +impl Hash for Stroke { + fn hash(&self, state: &mut H) { + self.paint.hash(state); + self.width.to_bits().hash(state); + self.miter_limit.to_bits().hash(state); + self.line_cap.hash(state); + self.line_join.hash(state); + self.opacity.hash(state); + self.dash.hash(state); + } +} + +impl Default for Stroke { + fn default() -> Self { + Stroke { + paint: luma::Color::black().into(), + width: 1.0, + miter_limit: 10.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + opacity: NormalizedF32::ONE, + dash: None, + } + } +} + +impl Stroke { + pub(crate) fn into_tiny_skia(self) -> tiny_skia_path::Stroke { + let mut stroke = tiny_skia_path::Stroke { + width: self.width, + miter_limit: self.miter_limit, + line_cap: match self.line_cap { + LineCap::Butt => tiny_skia_path::LineCap::Butt, + LineCap::Round => tiny_skia_path::LineCap::Round, + LineCap::Square => tiny_skia_path::LineCap::Square, + }, + line_join: match self.line_join { + LineJoin::Miter => tiny_skia_path::LineJoin::Miter, + LineJoin::Round => tiny_skia_path::LineJoin::Round, + LineJoin::Bevel => tiny_skia_path::LineJoin::Bevel, + }, + dash: None, + }; + + if let Some(stroke_dash) = self.dash { + stroke.dash = tiny_skia_path::StrokeDash::new(stroke_dash.array, stroke_dash.offset); + } + + stroke + } +} + +/// A fill rule. +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, Default)] +pub enum FillRule { + /// The `non-zero` fill rule. + #[default] + NonZero, + /// The `even-odd` fill rule. + EvenOdd, +} + +/// A fill. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Fill { + /// The paint of the fill. + pub paint: Paint, + /// The opacity of the fill. + pub opacity: NormalizedF32, + /// The fill rule that should be used when applying the fill. + pub rule: FillRule, +} + +impl Default for Fill { + fn default() -> Self { + Fill { + paint: luma::Color::black().into(), + opacity: NormalizedF32::ONE, + rule: FillRule::default(), + } + } +} diff --git a/vendor/krilla/src/graphics/separation.rs b/vendor/krilla/src/graphics/separation.rs new file mode 100644 index 00000000..275cff46 --- /dev/null +++ b/vendor/krilla/src/graphics/separation.rs @@ -0,0 +1,86 @@ +use pdf_writer::writers::ExponentialFunction; +use pdf_writer::{Finish, Name, Ref, Writer}; + +use crate::chunk_container::ChunkContainer; +use crate::color::separation::SeparationSpace; +use crate::color::{DEVICE_CMYK, DEVICE_GRAY, DEVICE_RGB}; +use crate::resource::{self, Resource, Resourceable}; +use crate::serialize::{Cacheable, MaybeDeviceColorSpace, SerializeContext}; + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct SeparationColorSpace { + space: SeparationSpace, +} + +impl SeparationColorSpace { + pub fn new(space: SeparationSpace) -> Self { + Self { space } + } +} + +impl Cacheable for SeparationColorSpace { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + // Delegate fallback color space registration to existing logic + let fallback_cs = self.space.fallback.color_space(sc); + let fallback_cs_resource = sc.register_colorspace(chunk_container, fallback_cs.into()); + + // Get fallback color components for tint function + let fallback_color = crate::color::Color::from(self.space.fallback).to_pdf_color(); + let num_components = fallback_color.len(); + + // Determine C0 based on whether the fallback is subtractive or additive + // - Additive (RGB, Luma): tint 0.0 = white = [1.0, 1.0, 1.0] + // - Subtractive (CMYK): tint 0.0 = white = [0.0, 0.0, 0.0, 0.0] + let c0_value = if self.space.fallback.is_subtractive() { + 0.0 + } else { + 1.0 + }; + + if let Err(validation_error) = sc.validation_store().validate_separation(&self.space) { + sc.register_validation_error(validation_error); + } + + let chunk = &mut chunk_container.non_stream.color_spaces; + + // Write Separation color space array: [/Separation name alternateSpace tintTransform] + let mut array = chunk.indirect(root_ref).array(); + array.item(Name(b"Separation")); + + // Colorant name + array.item(self.space.colorant.to_pdf()); + + // Fallback color space - write as name for device CS, or ref for others + match fallback_cs_resource { + MaybeDeviceColorSpace::DeviceRgb => array.item(Name(DEVICE_RGB.as_bytes())), + MaybeDeviceColorSpace::DeviceGray => array.item(Name(DEVICE_GRAY.as_bytes())), + MaybeDeviceColorSpace::DeviceCMYK => array.item(Name(DEVICE_CMYK.as_bytes())), + MaybeDeviceColorSpace::ColorSpace(cs) => { + // Use get_ref() to extract the underlying Ref + array.item(cs.get_ref()) + } + }; + + // Write Type 2 (Exponential) function for tint transform + // Maps tint [0.0-1.0] from white (no ink) to fallback color (full ink) + ExponentialFunction::start(array.push()) + .domain([0.0, 1.0]) + .range([0.0, 1.0].repeat(num_components)) + // C0: white/no ink (tint = 0.0) - value depends on color space + .c0(vec![c0_value; num_components]) + // C1: fallback color (tint = 1.0) + .c1(fallback_color) + .n(1.0); + + array.finish(); + } +} + +impl Resourceable for SeparationColorSpace { + type Resource = resource::ColorSpace; +} diff --git a/vendor/krilla/src/graphics/shading_function.rs b/vendor/krilla/src/graphics/shading_function.rs new file mode 100644 index 00000000..0b652a8f --- /dev/null +++ b/vendor/krilla/src/graphics/shading_function.rs @@ -0,0 +1,865 @@ +use std::hash::{Hash, Hasher}; +use std::ops::DerefMut; +use std::sync::Arc; + +use bumpalo::Bump; +use pdf_writer::types::{FunctionShadingType, PostScriptOp}; +use pdf_writer::{Chunk, Finish, Ref}; +use tiny_skia_path::Point; + +use crate::chunk_container::ChunkContainer; +use crate::configure::ValidationError; +use crate::geom::{Rect, Transform}; +use crate::graphics::color::luma; +use crate::graphics::color::{Color, ColorSpace}; +use crate::graphics::paint::{LinearGradient, RadialGradient, SweepGradient}; +use crate::graphics::paint::{SpreadMethod, Stop}; +use crate::num::NormalizedF32; +use crate::resource; +use crate::resource::Resourceable; +use crate::serialize::{Cacheable, MaybeDeviceColorSpace, SerializeContext}; +use crate::stream::FilterStreamBuilder; +use crate::util::set_colorspace; + +#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)] +pub(crate) enum GradientType { + Sweep, + Linear, +} + +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct RadialAxialGradient { + pub(crate) coords: Vec, + pub(crate) shading_type: FunctionShadingType, + pub(crate) stops: Vec, + pub(crate) anti_alias: bool, +} + +impl Eq for RadialAxialGradient {} + +impl Hash for RadialAxialGradient { + fn hash(&self, state: &mut H) { + for el in &self.coords { + el.to_bits().hash(state); + } + + self.shading_type.hash(state); + self.stops.hash(state); + self.anti_alias.hash(state); + } +} + +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct PostScriptGradient { + pub(crate) min: f32, + pub(crate) max: f32, + // Only used for sweep gradient + pub(crate) cx: f32, + // Only used for sweep gradient + pub(crate) cy: f32, + pub(crate) stops: Vec, + pub(crate) domain: Rect, + pub(crate) spread_method: SpreadMethod, + pub(crate) gradient_type: GradientType, + pub(crate) anti_alias: bool, +} + +impl Eq for PostScriptGradient {} + +impl Hash for PostScriptGradient { + fn hash(&self, state: &mut H) { + self.min.to_bits().hash(state); + self.max.to_bits().hash(state); + self.stops.hash(state); + self.domain.hash(state); + self.spread_method.hash(state); + self.gradient_type.hash(state); + self.anti_alias.hash(state); + } +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub(crate) enum GradientProperties { + RadialAxialGradient(RadialAxialGradient), + PostScriptGradient(PostScriptGradient), +} + +impl GradientProperties { + // Check if the gradient could be encoded as a solid fill instead. + pub(crate) fn single_stop_color(&self) -> Option<(&Color, NormalizedF32)> { + match self { + GradientProperties::RadialAxialGradient(rag) => { + if rag.stops.len() == 1 { + return Some((&rag.stops[0].color, rag.stops[0].opacity)); + } + } + GradientProperties::PostScriptGradient(psg) => { + if psg.stops.len() == 1 { + return Some((&psg.stops[0].color, psg.stops[0].opacity)); + } + } + } + + None + } +} + +pub(crate) trait GradientPropertiesExt { + fn gradient_properties(self, bbox: Rect) -> (GradientProperties, Transform); +} + +fn get_expanded_bbox(mut bbox: Rect, shading_transform: Transform) -> Rect { + // We need to make sure the shading covers the whole bbox of the object after + // the transform as been applied. In order to know that, we need to calculate the + // resulting bbox from the inverted transform. + bbox.expand(&bbox.transform(shading_transform.invert().unwrap()).unwrap()); + bbox +} + +/// When writing a PostScript shading, we assume that both points are on a horizontal +/// line. Here, we calculate by how much we need to rotate the second point so that it +/// is horizontal to the first point, as well as the position of the rotated point. +fn get_point_ts(start: Point, end: Point) -> (Transform, f32, f32) { + let dist = start.distance(end); + + let dx = end.x - start.x; + let dy = end.y - start.y; + let angle = dy.atan2(dx).to_degrees(); + + ( + Transform::from_rotate_at(angle, start.x, start.y), + start.x, + start.x + dist, + ) +} + +impl GradientPropertiesExt for LinearGradient { + fn gradient_properties(self, bbox: Rect) -> (GradientProperties, Transform) { + if self.spread_method == SpreadMethod::Pad { + ( + GradientProperties::RadialAxialGradient(RadialAxialGradient { + coords: vec![self.x1, self.y1, self.x2, self.y2], + shading_type: FunctionShadingType::Axial, + stops: self.stops, + anti_alias: self.anti_alias, + }), + self.transform, + ) + } else { + let p1 = Point::from_xy(self.x1, self.y1); + let p2 = Point::from_xy(self.x2, self.y2); + + let (ts, min, max) = get_point_ts(p1, p2); + ( + GradientProperties::PostScriptGradient(PostScriptGradient { + min, + max, + cx: 0.0, + cy: 0.0, + stops: self.stops, + domain: get_expanded_bbox(bbox, self.transform.pre_concat(ts)), + spread_method: self.spread_method, + gradient_type: GradientType::Linear, + anti_alias: self.anti_alias, + }), + self.transform.pre_concat(ts), + ) + } + } +} + +impl GradientPropertiesExt for SweepGradient { + fn gradient_properties(self, bbox: Rect) -> (GradientProperties, Transform) { + let min = self.start_angle; + let max = self.end_angle; + + let transform = self.transform; + + ( + GradientProperties::PostScriptGradient(PostScriptGradient { + min, + max, + cx: self.cx, + cy: self.cy, + stops: self.stops, + domain: get_expanded_bbox(bbox, transform), + spread_method: self.spread_method, + gradient_type: GradientType::Sweep, + anti_alias: self.anti_alias, + }), + transform, + ) + } +} + +impl GradientPropertiesExt for RadialGradient { + fn gradient_properties(self, _: Rect) -> (GradientProperties, Transform) { + // TODO: Support other spread methods + ( + GradientProperties::RadialAxialGradient(RadialAxialGradient { + coords: vec![self.fx, self.fy, self.fr, self.cx, self.cy, self.cr], + shading_type: FunctionShadingType::Radial, + stops: self.stops, + anti_alias: self.anti_alias, + }), + self.transform, + ) + } +} + +#[derive(Debug, Hash, Eq, PartialEq)] +struct Repr { + pub(crate) properties: GradientProperties, + pub(crate) use_opacities: bool, +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub(crate) struct ShadingFunction(Arc); + +impl ShadingFunction { + pub(crate) fn new(properties: GradientProperties, use_opacities: bool) -> Self { + Self(Arc::new(Repr { + properties, + use_opacities, + })) + } +} + +impl Cacheable for ShadingFunction { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let mut stream_chunk = sc.new_chunk(); + + match &self.0.properties { + GradientProperties::RadialAxialGradient(rag) => { + let shading_cs = + shading_color_space(sc, rag.stops[0].color.clone(), self.0.use_opacities); + let registered_cs = sc.register_colorspace(chunk_container, shading_cs); + let chunk = &mut chunk_container.non_stream.shading_functions; + serialize_axial_radial_shading( + sc, + chunk, + root_ref, + rag, + self.0.use_opacities, + registered_cs, + ) + } + GradientProperties::PostScriptGradient(psg) => { + sc.register_validation_error(ValidationError::ContainsPostScript(sc.location)); + let shading_cs = + shading_color_space(sc, psg.stops[0].color.clone(), self.0.use_opacities); + let registered_cs = sc.register_colorspace(chunk_container, shading_cs); + let chunk = &mut chunk_container.non_stream.shading_functions; + serialize_postscript_shading( + sc, + chunk, + &mut stream_chunk, + root_ref, + psg, + self.0.use_opacities, + registered_cs, + ) + } + } + + // Note: The stream chunk might be empty. + chunk_container.streams.shading_functions.push(stream_chunk); + } +} + +impl Resourceable for ShadingFunction { + type Resource = resource::Shading; +} + +fn shading_color_space(sc: &mut SerializeContext, color: Color, use_opacities: bool) -> ColorSpace { + if use_opacities { + luma::color_space(sc.serialize_settings().no_device_cs).into() + } else { + color.color_space(sc) + } +} + +fn serialize_postscript_shading( + sc: &mut SerializeContext, + chunk: &mut Chunk, + stream_chunk: &mut Chunk, + root_ref: Ref, + post_script_gradient: &PostScriptGradient, + use_opacities: bool, + cs: MaybeDeviceColorSpace, +) { + let domain = post_script_gradient.domain; + + let bump = Bump::new(); + let function_ref = + select_postscript_function(post_script_gradient, stream_chunk, sc, &bump, use_opacities); + let mut shading = chunk.function_shading(root_ref); + shading.shading_type(FunctionShadingType::Function); + + set_colorspace(cs, shading.deref_mut()); + + // Write the identity matrix, because ghostscript has a bug where + // it thinks the entry is mandatory. + shading.matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]); + shading.anti_alias(post_script_gradient.anti_alias); + shading.function(function_ref); + + shading.domain([domain.left(), domain.right(), domain.top(), domain.bottom()]); + shading.finish(); +} + +fn serialize_axial_radial_shading( + sc: &mut SerializeContext, + chunk: &mut Chunk, + root_ref: Ref, + radial_axial_gradient: &RadialAxialGradient, + use_opacities: bool, + cs: MaybeDeviceColorSpace, +) { + let function_ref = + select_axial_radial_function(radial_axial_gradient, chunk, sc, use_opacities); + let mut shading = chunk.function_shading(root_ref); + if radial_axial_gradient.shading_type == FunctionShadingType::Radial { + shading.shading_type(FunctionShadingType::Radial); + } else { + shading.shading_type(FunctionShadingType::Axial); + } + + set_colorspace(cs, shading.deref_mut()); + + shading.anti_alias(radial_axial_gradient.anti_alias); + shading.function(function_ref); + shading.coords(radial_axial_gradient.coords.iter().copied()); + shading.extend([true, true]); + shading.finish(); +} + +fn select_axial_radial_function( + properties: &RadialAxialGradient, + chunk: &mut Chunk, + sc: &mut SerializeContext, + use_opacities: bool, +) -> Ref { + debug_assert!(properties.stops.len() > 1); + + let mut stops = properties.stops.clone(); + + if let Some(first) = stops.first() { + if first.offset.get() != 0.0 { + let mut new_stop = first.clone(); + new_stop.offset = NormalizedF32::ZERO; + stops.insert(0, new_stop); + } + } + + if let Some(last) = stops.last() { + if last.offset.get() != 1.0 { + let mut new_stop = last.clone(); + new_stop.offset = NormalizedF32::ONE; + stops.push(new_stop); + } + } + + if stops.len() == 2 { + if use_opacities { + serialize_exponential( + vec![stops[0].opacity.get()], + vec![stops[1].opacity.get()], + chunk, + sc, + ) + } else { + serialize_exponential( + stops[0] + .color + .to_pdf_color() + .into_iter() + .collect::>(), + stops[1] + .color + .to_pdf_color() + .into_iter() + .collect::>(), + chunk, + sc, + ) + } + } else { + serialize_stitching(&stops, chunk, sc, use_opacities) + } +} + +fn select_postscript_function( + properties: &PostScriptGradient, + chunk: &mut Chunk, + sc: &mut SerializeContext, + bump: &Bump, + use_opacities: bool, +) -> Ref { + debug_assert!(properties.stops.len() > 1); + + if properties.gradient_type == GradientType::Linear { + serialize_linear_postscript(properties, chunk, sc, use_opacities) + } else if properties.gradient_type == GradientType::Sweep { + serialize_sweep_postscript(properties, chunk, sc, bump, use_opacities) + } else { + todo!(); + } +} + +// Not working yet +// fn serialize_radial_postscript( +// properties: &GradientProperties, +// sc: &mut SerializeContext, +// bbox: &Rect, +// ) -> Ref { +// let root_ref = sc.new_ref(); +// +// let start_code = [ +// "{".to_string(), +// // Stack: x y +// "80 exch 80 sub dup mul 3 1 roll sub dup mul add sqrt 120 div 0 0".to_string(), +// ]; +// +// let end_code = ["}".to_string()]; +// +// let mut code = Vec::new(); +// code.extend(start_code); +// // code.push(encode_spread_method(min, max, properties.spread_method)); +// // code.push(encode_stops(&properties.stops, min, max)); +// code.extend(end_code); +// +// let code = code.join(" ").into_bytes(); +// let mut postscript_function = sc.chunk_mut().post_script_function(root_ref, &code); +// postscript_function.domain([bbox.left(), bbox.right(), bbox.top(), bbox.bottom()]); +// postscript_function.range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]); +// +// root_ref +// } + +fn serialize_sweep_postscript( + properties: &PostScriptGradient, + chunk: &mut Chunk, + sc: &mut SerializeContext, + bump: &Bump, + use_opacities: bool, +) -> Ref { + use pdf_writer::types::PostScriptOp::*; + + let root_ref = sc.new_ref(); + + let min: f32 = properties.min; + let max: f32 = properties.max; + + let mut code = vec![]; + code.extend([ + // Stack: x y + // Shift by cy, so gradient actually starts from the center + Real(properties.cy), + Sub, + Exch, + // y x + // Shift by cx, so gradient actually starts from the center + Real(properties.cx), + Sub, + // Make sure x is never 0. + Dup, + Dup, + Real(0.0001), + Lt, + Exch, + Real(-0.0001), + Gt, + And, + If(bump.alloc([Pop, Real(0.0001)])), + // Get the angle + Atan, + ]); + + encode_spread_method(min, max, &mut code, bump, properties.spread_method); + encode_postscript_stops(&properties.stops, min, max, &mut code, bump, use_opacities); + + let encoded = PostScriptOp::encode(&code); + sc.register_limits(encoded.limits()); + let encoded = FilterStreamBuilder::new_from_content_stream(&encoded, &sc.serialize_settings()) + .finish(&sc.serialize_settings()); + let mut postscript_function = chunk.post_script_function(root_ref, encoded.encoded_data()); + encoded.write_filters(postscript_function.deref_mut().deref_mut()); + postscript_function.domain([ + properties.domain.left(), + properties.domain.right(), + properties.domain.top(), + properties.domain.bottom(), + ]); + + if use_opacities { + postscript_function.range([0.0, 1.0]); + } else { + postscript_function.range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]); + } + + root_ref +} + +const MAX_POSTSCRIPT_STOPS: usize = 97; + +fn trim_stops(stops: &[Stop]) -> Vec { + let len = stops.len(); + let factor = len as f32 / MAX_POSTSCRIPT_STOPS as f32; + let mut cur_index: f32 = 0.0; + + let get_index = |i: f32| i.round() as usize; + + let mut new_stops = vec![]; + + while get_index(cur_index) < stops.len() { + new_stops.push(stops[get_index(cur_index)].clone()); + cur_index += factor; + } + + new_stops +} + +fn serialize_linear_postscript( + properties: &PostScriptGradient, + chunk: &mut Chunk, + sc: &mut SerializeContext, + use_opacities: bool, +) -> Ref { + use pdf_writer::types::PostScriptOp::*; + + let bump = Bump::new(); + let root_ref = sc.new_ref(); + + let min: f32 = properties.min; + let max: f32 = properties.max; + + let mut code = vec![]; + code.extend([ + // Stack: x y + // Ignore the y coordinate. We account for it in the gradient transform. + Pop, + // x + ]); + + encode_spread_method(min, max, &mut code, &bump, properties.spread_method); + encode_postscript_stops(&properties.stops, min, max, &mut code, &bump, use_opacities); + + let encoded = PostScriptOp::encode(&code); + sc.register_limits(encoded.limits()); + let encoded = FilterStreamBuilder::new_from_content_stream(&encoded, &sc.serialize_settings()) + .finish(&sc.serialize_settings()); + let mut postscript_function = chunk.post_script_function(root_ref, encoded.encoded_data()); + encoded.write_filters(postscript_function.deref_mut().deref_mut()); + postscript_function.domain([ + properties.domain.left(), + properties.domain.right(), + properties.domain.top(), + properties.domain.bottom(), + ]); + + if use_opacities { + postscript_function.range([0.0, 1.0]); + } else { + postscript_function.range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]); + } + + root_ref +} + +/// Postscript code that, given an arbitrary x coordinate, normalizes it to an x coordinate +/// between min and max that yields the correct color, depending on the spread mode. In the case +/// of the `Pad` spread methods, the coordinate will not be normalized since the Postscript functions +/// assign the correct value by default. +fn encode_spread_method<'a>( + min: f32, + max: f32, + code: &mut Vec>, + bump: &'a Bump, + spread_method: SpreadMethod, +) { + use pdf_writer::types::PostScriptOp::*; + + if spread_method == SpreadMethod::Pad { + return; + } + + let length = max - min; + + code.extend([ + // We do the following: + // 1. Normalize by doing n = x - min. + // 2. Calculate the "interval" we are in by doing i = floor(n / length) + // 3. Calculate the offset by doing o = n - i * length + // 4. If the spread method is repeat, we additionally calculate o = length - 0 if i % 2 == 1 + // 5. Calculate the final value with x_new = min + o. + + // Current stack: + // x + Real(length), + Real(min), + // x length min + Integer(2), + Index, + // x length min x + Integer(1), + Index, + // x length min x min + Sub, + // x length min n + Dup, + // x length min n n + Integer(3), + Index, + // x length min n n length + Div, + // x length min n {n/length} + Floor, + // x length min n i + Exch, + // x length min i n + Integer(1), + Index, + // x length min i n i + Integer(4), + Index, + // x length min i n i length + Mul, + // x length min i n {i * length} + Sub, + // x length min i o + Exch, + // x length min o i + Cvi, + Abs, + // x length min o abs(i) + Integer(2), + Mod, + // x length min o {abs(i) % 2} + // See https://github.com/google/skia/blob/645b77ce61449951cb9f3cf754b47d4977b68e1a/src/pdf/SkPDFGradientShader.cpp#L402-L408 + // for why we check > 0 instead of == 1. + Integer(0), + Gt, + // x length min o {(abs(i) % 2) > 0} + if spread_method == SpreadMethod::Reflect { + If(bump.alloc([Integer(2), Index, Exch, Sub])) + } else { + Pop + }, + // x length min o + Add, + // x length x_new + Integer(3), + Integer(1), + Roll, + // x_new x length + Pop, + Pop, + // x_new + ]); +} + +/// Postscript code that, given an x coordinate between the min and max +/// of a gradient, returns the interpolated color value depending on where it +/// lies within the stops. +fn encode_postscript_stops<'a>( + stops: &[Stop], + min: f32, + max: f32, + code: &mut Vec>, + bump: &'a Bump, + use_opacities: bool, +) { + // Our algorithm requires the stops to be padded. + let mut stops = stops.to_vec(); + + // Most viewers have a nesting depth on how many `elseif` can be nested (100 for mupdf, + // 128 for Chrome and 256 for Acrobat), so we trim the stops if they are too large. + // this is of course a bit unfortunate, but it should be pretty rare to have that many + // stops, and if we do, the stops are most likely going to be very similar, so it's + // safe to just sample from in-between. + if stops.len() > MAX_POSTSCRIPT_STOPS { + stops = trim_stops(&stops); + } + + if let Some(first) = stops.first() { + let mut first = first.clone(); + first.offset = NormalizedF32::ZERO; + stops.insert(0, first); + } + + if let Some(last) = stops.last() { + let mut last = last.clone(); + last.offset = NormalizedF32::ONE; + stops.push(last); + } + + encode_stops_impl(&stops, min, max, code, bump, use_opacities); +} + +fn encode_stops_impl<'a>( + stops: &[Stop], + min: f32, + max: f32, + code: &mut Vec>, + bump: &'a Bump, + use_opacities: bool, +) { + use pdf_writer::types::PostScriptOp::*; + + let encode_two_stops = + |c0: &[f32], c1: &[f32], min: f32, max: f32, code: &mut Vec| { + if min == max { + code.push(Pop); + code.extend(c0.iter().map(|n| Real(*n))); + return; + } + + // Sanity check that both stops have the same number of components. + assert_eq!( + c0.len(), + c1.len(), + "cannot create gradient with stops from different color spaces" + ); + + // Normalize the x coordinate to be between 0 and 1. + code.extend([Real(min), Sub, Real(max), Real(min), Sub, Div]); + + for i in 0..c0.len() { + // Interpolate each color component c0 + x_norm * (x1 - c0). + code.extend([ + Integer(i as i32), + Index, + Real(c0[i]), + Exch, + Real(c1[i]), + Real(c0[i]), + Sub, + Mul, + Add, + ]); + // x_norm, c0, c1, ... + } + // Remove x_norm from the stack. + code.extend([Integer((c0.len() + 1) as i32), Integer(-1), Roll, Pop]); + // c0, c1, c2, ... + }; + + if stops.len() == 1 { + if use_opacities { + code.push(Real(stops[0].opacity.get())); + } else { + code.extend(stops[0].color.to_pdf_color().into_iter().map(Real)); + } + } else { + let length = max - min; + let stops_min = min + length * stops[0].offset.get(); + let stops_max = min + length * stops[1].offset.get(); + // Write the if conditions to find the corresponding set of two stops. + + let if_stops = bump.alloc(vec![]); + if use_opacities { + encode_two_stops( + &[stops[0].opacity.get()], + &[stops[1].opacity.get()], + stops_min, + stops_max, + if_stops, + ) + } else { + encode_two_stops( + &stops[0] + .color + .to_pdf_color() + .into_iter() + .collect::>(), + &stops[1] + .color + .to_pdf_color() + .into_iter() + .collect::>(), + stops_min, + stops_max, + if_stops, + ) + }; + let else_stops = bump.alloc(vec![]); + encode_stops_impl(&stops[1..], min, max, else_stops, bump, use_opacities); + + code.extend([Dup, Real(stops_max), Le, IfElse(if_stops, else_stops)]); + } +} + +fn serialize_stitching( + stops: &[Stop], + chunk: &mut Chunk, + sc: &mut SerializeContext, + use_opacities: bool, +) -> Ref { + let root_ref = sc.new_ref(); + let mut functions = vec![]; + let mut bounds = vec![]; + let mut encode = vec![]; + let mut count = 0; + + for window in stops.windows(2) { + let (first, second) = (&window[0], &window[1]); + bounds.push(second.offset.get()); + + let (c0_components, c1_components) = if use_opacities { + (vec![first.opacity.get()], vec![second.opacity.get()]) + } else { + ( + first.color.to_pdf_color().into_iter().collect::>(), + second.color.to_pdf_color().into_iter().collect::>(), + ) + }; + + count = c0_components.len(); + + let exp_ref = serialize_exponential(c0_components, c1_components, chunk, sc); + + functions.push(exp_ref); + encode.extend([0.0, 1.0]); + } + + bounds.pop(); + let mut stitching_function = chunk.stitching_function(root_ref); + stitching_function.domain([0.0, 1.0]); + stitching_function.range([0.0, 1.0].repeat(count)); + stitching_function.functions(functions); + stitching_function.bounds(bounds); + stitching_function.encode(encode); + + root_ref +} + +fn serialize_exponential( + c0: Vec, + c1: Vec, + chunk: &mut Chunk, + sc: &mut SerializeContext, +) -> Ref { + let root_ref = sc.new_ref(); + assert_eq!( + c0.len(), + c1.len(), + "cannot create gradient with stops from different color spaces" + ); + let num_components = c0.len(); + + let mut exp = chunk.exponential_function(root_ref); + + exp.range([0.0, 1.0].repeat(num_components)); + exp.c0(c0); + exp.c1(c1); + exp.domain([0.0, 1.0]); + exp.n(1.0); + exp.finish(); + root_ref +} diff --git a/vendor/krilla/src/graphics/shading_pattern.rs b/vendor/krilla/src/graphics/shading_pattern.rs new file mode 100644 index 00000000..90115b28 --- /dev/null +++ b/vendor/krilla/src/graphics/shading_pattern.rs @@ -0,0 +1,60 @@ +use std::hash::Hash; +use std::sync::Arc; + +use pdf_writer::{Finish, Name, Ref}; + +use crate::chunk_container::ChunkContainer; +use crate::geom::Transform; +use crate::graphics::shading_function::{GradientProperties, ShadingFunction}; +use crate::resource; +use crate::resource::Resourceable; +use crate::serialize::{Cacheable, SerializeContext}; + +#[derive(Debug, PartialEq)] +struct Repr { + shading_function: ShadingFunction, + shading_transform: Transform, +} + +impl Eq for Repr {} + +impl Hash for Repr { + fn hash(&self, state: &mut H) { + self.shading_function.hash(state); + self.shading_transform.hash(state); + } +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub(crate) struct ShadingPattern(Arc); + +impl ShadingPattern { + pub fn new(gradient_properties: GradientProperties, shading_transform: Transform) -> Self { + Self(Arc::new(Repr { + // CTM doesn't need to be included to calculate the domain of the shading function + shading_function: ShadingFunction::new(gradient_properties, false), + shading_transform, + })) + } +} + +impl Cacheable for ShadingPattern { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let shading_ref = sc.register_cacheable(chunk_container, self.0.shading_function.clone()); + let chunk = &mut chunk_container.non_stream.patterns; + let mut shading_pattern = chunk.shading_pattern(root_ref); + shading_pattern.pair(Name(b"Shading"), shading_ref); + shading_pattern.matrix(self.0.shading_transform.to_pdf_transform()); + + shading_pattern.finish(); + } +} + +impl Resourceable for ShadingPattern { + type Resource = resource::Pattern; +} diff --git a/vendor/krilla/src/graphics/tiling_pattern.rs b/vendor/krilla/src/graphics/tiling_pattern.rs new file mode 100644 index 00000000..e26f1d8c --- /dev/null +++ b/vendor/krilla/src/graphics/tiling_pattern.rs @@ -0,0 +1,119 @@ +//! Tiling patterns. + +use std::hash::{Hash, Hasher}; +use std::ops::DerefMut; + +use pdf_writer::types::{PaintType, TilingType}; +use pdf_writer::{Finish, Ref}; + +use crate::chunk_container::ChunkContainer; +use crate::geom::Transform; +use crate::num::NormalizedF32; +use crate::resource; +use crate::resource::Resourceable; +use crate::serialize::{Cacheable, SerializeContext}; +use crate::stream::StreamBuilder; +use crate::stream::{FilterStreamBuilder, Stream}; + +#[derive(Debug, PartialEq)] +pub(crate) struct TilingPattern { + stream: Stream, + transform: Transform, + base_opacity: NormalizedF32, + width: f32, + height: f32, +} + +impl Eq for TilingPattern {} + +impl Hash for TilingPattern { + fn hash(&self, state: &mut H) { + self.stream.hash(state); + self.transform.hash(state); + self.base_opacity.hash(state); + self.width.to_bits().hash(state); + self.height.to_bits().hash(state); + } +} + +impl TilingPattern { + pub(crate) fn new( + stream: Stream, + transform: Transform, + base_opacity: NormalizedF32, + width: f32, + height: f32, + serializer_context: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) -> Self { + // stroke/fill opacity doesn't work consistently across different viewers for patterns, + // so instead we simulate it ourselves. + let pattern_stream = if base_opacity == NormalizedF32::ONE { + stream + } else { + let stream = { + let mut builder = StreamBuilder::new(serializer_context, chunk_container); + let mut surface = builder.surface(); + surface.draw_opacified_stream(base_opacity, stream); + surface.finish(); + builder.finish() + }; + + stream + }; + + Self { + stream: pattern_stream, + transform, + base_opacity, + width, + height, + } + } +} + +impl Cacheable for TilingPattern { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let mut chunk = sc.new_chunk(); + + for validation_error in self.stream.validation_errors { + sc.register_validation_error(validation_error); + } + + let pattern_stream = FilterStreamBuilder::new_from_content_stream( + &self.stream.content, + &sc.serialize_settings(), + ) + .finish(&sc.serialize_settings()); + let mut tiling_pattern = chunk.tiling_pattern(root_ref, pattern_stream.encoded_data()); + pattern_stream.write_filters(tiling_pattern.deref_mut().deref_mut()); + + self.stream.resource_dictionary.to_pdf_resources( + &mut tiling_pattern, + sc, + &mut chunk_container.non_stream.resource_dictionaries, + ); + + let final_bbox = pdf_writer::Rect::new(0.0, 0.0, self.width, self.height); + + tiling_pattern + .tiling_type(TilingType::ConstantSpacing) + .paint_type(PaintType::Colored) + .bbox(final_bbox) + .matrix(self.transform.to_pdf_transform()) + .x_step(final_bbox.x2 - final_bbox.x1) + .y_step(final_bbox.y2 - final_bbox.y1); + + tiling_pattern.finish(); + chunk_container.streams.patterns.push(chunk); + } +} + +impl Resourceable for TilingPattern { + type Resource = resource::Pattern; +} diff --git a/vendor/krilla/src/graphics/xobject.rs b/vendor/krilla/src/graphics/xobject.rs new file mode 100644 index 00000000..28d6f388 --- /dev/null +++ b/vendor/krilla/src/graphics/xobject.rs @@ -0,0 +1,150 @@ +use std::ops::DerefMut; +use std::sync::Arc; + +use pdf_writer::{Finish, Name, Ref}; + +use crate::chunk_container::ChunkContainer; +use crate::configure::ValidationError; +use crate::geom::Rect; +use crate::graphics::color::{rgb, DEVICE_RGB}; +use crate::resource; +use crate::resource::{Resource, Resourceable}; +use crate::serialize::{Cacheable, MaybeDeviceColorSpace, SerializeContext}; +use crate::stream::{FilterStreamBuilder, Stream}; +use crate::util::{NameExt, Prehashed}; + +#[derive(Debug, Hash, Eq, PartialEq)] +struct Repr { + stream: Stream, + isolated: bool, + transparency_group_color_space: bool, + custom_bbox: Option, +} + +#[derive(Debug, Hash, Clone, Eq, PartialEq)] +pub(crate) struct XObject(Arc>); + +impl XObject { + pub(crate) fn new( + stream: Stream, + isolated: bool, + mut transparency_group_color_space: bool, + custom_bbox: Option, + ) -> Self { + // In case a mask was invoked in the content stream, we _always_ create + // a new transparency group. Please see . + // Just to provide a brief explanation: I have not found any mention + // of a transparency group being required when using a soft mask in the + // PDF spec. However, Apple Preview (and only them!) have a weird bug + // where if you transform an XObject with a soft mask but not transparency + // group, the transform gets applied twice to the mask, resulting in + // rendering issues. Using a transparency group in this case seems to + // fix the issue. + // TODO: Apply group transparency to page as well. + transparency_group_color_space |= stream.uses_mask; + + Self(Arc::new(Prehashed::new(Repr { + stream, + isolated, + transparency_group_color_space, + custom_bbox, + }))) + } + + pub(crate) fn is_empty(&self) -> bool { + self.0.stream.is_empty() + } + + pub(crate) fn bbox(&self) -> Rect { + self.0.custom_bbox.unwrap_or(self.0.stream.bbox) + } +} + +impl Cacheable for XObject { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let mut chunk = sc.new_chunk(); + + for validation_error in &self.0.stream.validation_errors { + sc.register_validation_error(validation_error.clone()); + } + + let use_transparency_group = self.0.isolated || self.0.transparency_group_color_space; + + if use_transparency_group { + sc.register_validation_error(ValidationError::Transparency(sc.location)); + } + + let serialize_settings = sc.serialize_settings(); + + // "Ordinarily, the CS entry may be present only for isolated transparency groups (those + // for which I is true), and even then it is optional. However, this entry shall be present in + // the group attributes dictionary for any transparency group XObject that has no parent + // group or page from which to inherit." + // + // So while we don't technically always need to write it, let's just play it save + // and always add a group color space in case we have a transparency group. + let transparency_group_cs = if use_transparency_group { + Some(sc.register_colorspace( + chunk_container, + rgb::color_space(serialize_settings.no_device_cs).into(), + )) + } else { + None + }; + + let x_object_stream = FilterStreamBuilder::new_from_content_stream( + &self.0.stream.content, + &serialize_settings, + ) + .finish(&serialize_settings); + let mut x_object = chunk.form_xobject(root_ref, x_object_stream.encoded_data()); + x_object_stream.write_filters(x_object.deref_mut().deref_mut()); + + self.0.stream.resource_dictionary.to_pdf_resources( + &mut x_object, + sc, + &mut chunk_container.non_stream.resource_dictionaries, + ); + x_object.bbox( + self.0 + .custom_bbox + .unwrap_or(self.0.stream.bbox) + .to_pdf_rect(), + ); + + if use_transparency_group { + let mut group = x_object.group(); + let transparency = group.transparency(); + + if self.0.isolated { + transparency.isolated(self.0.isolated); + } + + if let Some(transparency_group_cs) = transparency_group_cs { + let pdf_cs = transparency.insert(Name(b"CS")); + + match transparency_group_cs { + MaybeDeviceColorSpace::DeviceRgb => pdf_cs.primitive(DEVICE_RGB.to_pdf_name()), + // Can only be SRGB + MaybeDeviceColorSpace::ColorSpace(cs) => pdf_cs.primitive(cs.get_ref()), + _ => unreachable!(), + } + } + + transparency.finish(); + group.finish(); + } + + x_object.finish(); + chunk_container.streams.x_objects.push(chunk); + } +} + +impl Resourceable for XObject { + type Resource = resource::XObject; +} diff --git a/vendor/krilla/src/interactive/action.rs b/vendor/krilla/src/interactive/action.rs new file mode 100644 index 00000000..69e6fea2 --- /dev/null +++ b/vendor/krilla/src/interactive/action.rs @@ -0,0 +1,68 @@ +//! PDF actions, allowing you to add interactivity to the document. +//! +//! PDF has the concept of "actions", which encompass things like navigating to a URL, +//! opening some file on the system, and so on. The PDF reference defines a whole bunch +//! of actions, but krilla does not expose nearly all of them, and never will. As of right now, +//! the only available action is the link action, which allows you to specify a link that +//! should be opened, when activating the action. + +use pdf_writer::types::ActionType; +use pdf_writer::{Name, Str}; + +use crate::error::KrillaResult; +use crate::interactive::destination::Destination; +use crate::serialize::SerializeContext; + +/// A type of action. +pub enum Action { + /// A link action. + Link(LinkAction), + /// A go-to action. + Goto(Destination), +} + +impl Action { + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + mut action: pdf_writer::writers::Action, + ) -> KrillaResult<()> { + match self { + Action::Link(link) => { + link.serialize(action); + + Ok(()) + } + Action::Goto(dest) => { + let dest_entry = action.action_type(ActionType::GoTo).insert(Name(b"D")); + dest.serialize(sc, dest_entry) + } + } + } +} + +/// A link action. Will open a link when clicked. +pub struct LinkAction { + uri: String, +} + +impl From for Action { + fn from(value: LinkAction) -> Self { + Action::Link(value) + } +} + +impl LinkAction { + /// Create a new link action that will open a URI when clicked. + pub fn new(uri: String) -> Self { + Self { uri } + } +} + +impl LinkAction { + fn serialize(&self, mut action: pdf_writer::writers::Action) { + action + .action_type(ActionType::Uri) + .uri(Str(self.uri.as_bytes())); + } +} diff --git a/vendor/krilla/src/interactive/annotation.rs b/vendor/krilla/src/interactive/annotation.rs new file mode 100644 index 00000000..91c4cff6 --- /dev/null +++ b/vendor/krilla/src/interactive/annotation.rs @@ -0,0 +1,283 @@ +//! PDF annotations, allowing you to add extra "content" to specific pages. +//! +//! PDF has the concept of annotations, which allow you to associate certain regions of +//! a page with an "annotation". The PDF reference defines many different actions, however, +//! krilla does not and never will expose all of them. As of right now, the only annotations +//! that are supported are "link annotations", which allow you associate a certain region of +//! the page with a link. + +use core::f32; + +use pdf_writer::types::AnnotationFlags; +use pdf_writer::{Finish, Name, Ref, TextStr}; + +use crate::chunk_container::ChunkContainer; +use crate::color::Color; +use crate::configure::{PdfVersion, ValidationError}; +use crate::error::KrillaResult; +use crate::geom::{Quadrilateral, Rect}; +use crate::interactive::action::Action; +use crate::interactive::destination::Destination; +use crate::page::page_root_transform; +use crate::serialize::SerializeContext; +use crate::surface::Location; + +/// An annotation. +pub struct Annotation { + pub(crate) annotation_type: AnnotationType, + pub(crate) alt: Option, + pub(crate) struct_parent: Option, + pub(crate) location: Option, +} + +impl Annotation { + /// Create a new link annotation with some alt text. + /// + /// Note that the alt text might be required in some cases, for example + /// when exporting to PDF/UA. + pub fn new_link(annotation: LinkAnnotation, alt_text: Option) -> Self { + Self { + annotation_type: AnnotationType::Link(annotation), + alt: alt_text, + struct_parent: None, + location: None, + } + } + + /// Sets the location of the annotation. + pub fn with_location(mut self, location: Option) -> Self { + self.location = location; + self + } +} + +impl From for Annotation { + fn from(value: LinkAnnotation) -> Self { + Self { + annotation_type: AnnotationType::Link(value), + alt: None, + struct_parent: None, + location: None, + } + } +} + +impl Annotation { + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + page_height: f32, + ) -> KrillaResult<()> { + let chunk = &mut chunk_container.non_stream.annotations; + let mut annotation = chunk + .indirect(root_ref) + .start::(); + + self.annotation_type + .serialize_type(sc, &mut annotation, page_height)?; + + let AnnotationType::Link(l) = &self.annotation_type; + // Only set the print flag when really necessary (only PDF/A). Don't + // set it by default, so annotations with color borders will be shown + // on a screen but not printed. + // TODO: No need to write the print flag even if it is `None`, + // only for PDF/A. + if l.border.is_none() + || sc + .serialize_settings() + .configuration + .validators() + .requires_annotation_flags() + { + annotation.flags(AnnotationFlags::PRINT); + } + + if let Some(struct_parent) = self.struct_parent { + annotation.struct_parent(struct_parent); + } + + if let Some(alt_text) = &self.alt { + annotation.contents(TextStr(alt_text)); + } + + if self.alt.as_ref().is_none_or(String::is_empty) { + sc.register_validation_error(ValidationError::MissingAnnotationAltText(self.location)); + } + + annotation.finish(); + + Ok(()) + } +} + +/// A type of annotation. +pub enum AnnotationType { + /// A link annotation. + Link(LinkAnnotation), +} + +impl AnnotationType { + fn serialize_type( + &self, + sc: &mut SerializeContext, + annotation: &mut pdf_writer::writers::Annotation, + page_height: f32, + ) -> KrillaResult<()> { + match self { + AnnotationType::Link(l) => l.serialize_type(sc, annotation, page_height), + } + } +} + +/// An annotation target. +pub enum Target { + /// A destination within the document. + Destination(Destination), + /// An action to be performed. + Action(Action), +} + +/// Border of a link annotation. +pub struct LinkBorder { + pub(crate) width: f32, + pub(crate) color: Color, +} + +impl LinkBorder { + /// Create a new link annotation border. + /// + /// `width`: The width of the border in pt. + /// `color`: The color of the border. + pub fn new(width: f32, color: Color) -> Self { + Self { width, color } + } +} + +/// A link annotation. +pub struct LinkAnnotation { + pub(crate) rect: Rect, + pub(crate) quad_points: Option>, + pub(crate) target: Target, + pub(crate) border: Option, +} + +impl LinkAnnotation { + /// Create a new link annotation. + /// + /// `rect`: The bounding box of the link annotation that it should cover on the page. + /// `target`: The target of the link annotation. + pub fn new(rect: Rect, target: Target) -> Self { + Self { + rect, + quad_points: None, + target, + border: None, + } + } + + /// Create a new link annotation. + /// + /// `target`: The target of the link annotation. + /// `quad_points`: An array of quadrilaterals that define where the link + /// annotation should be activated. This is useful if you for example have + /// a link annotation that is broken to one or multiple lines. + pub fn new_with_quad_points(quad_points: Vec, target: Target) -> Self { + assert!(!quad_points.is_empty()); + + let mut min_x = f32::INFINITY; + let mut min_y = f32::INFINITY; + let mut max_x = f32::NEG_INFINITY; + let mut max_y = f32::NEG_INFINITY; + + for point in quad_points.iter().flat_map(|q| q.0) { + min_x = min_x.min(point.x); + min_y = min_y.min(point.y); + max_x = max_x.max(point.x); + max_y = max_y.max(point.y); + } + + // Expand the bounding box by a little. There is a bug in adobe acrobat + // that sometimes prevents the quadpoints from being used if the quad + // points lie exactly on the bounding rectangle. + const EPSILON: f32 = 0.001; + let rect = Rect::from_ltrb( + min_x - EPSILON, + min_y - EPSILON, + max_x + EPSILON, + max_y + EPSILON, + ) + .unwrap(); + + Self { + rect, + quad_points: Some(quad_points), + target, + border: None, + } + } + + /// Set a border for this link annotation. The border will be visible on + /// screen but not when printed, unless when exporting with PDF/A standard. + pub fn with_border(self, border: LinkBorder) -> Self { + Self { + border: Some(border), + ..self + } + } + + fn serialize_type( + &self, + sc: &mut SerializeContext, + annotation: &mut pdf_writer::writers::Annotation, + page_height: f32, + ) -> KrillaResult<()> { + annotation.subtype(pdf_writer::types::AnnotationType::Link); + + let actual_rect = self + .rect + .transform(page_root_transform(page_height)) + .unwrap(); + annotation.rect(actual_rect.to_pdf_rect()); + annotation.border( + 0.0, + 0.0, + self.border.as_ref().map_or(0.0, |x| x.width), + None, + ); + + if let Some(border) = &self.border { + match border.color.to_regular() { + crate::color::RegularColor::Rgb(rgb) => { + let [r, g, b] = rgb.to_pdf_color(); + annotation.color_rgb(r, g, b); + } + crate::color::RegularColor::Cmyk(cmyk) => { + let [c, m, y, k] = cmyk.to_pdf_color(); + annotation.color_cmyk(c, m, y, k); + } + crate::color::RegularColor::Luma(gray) => { + annotation.color_gray(gray.to_pdf_color()); + } + } + } + + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf16 { + self.quad_points.as_ref().map(|p| { + annotation.quad_points(p.iter().flat_map(|q| q.0).flat_map(|p| { + let mut p = p.to_tsp(); + page_root_transform(page_height).to_tsp().map_point(&mut p); + [p.x, p.y] + })) + }); + } + + match &self.target { + Target::Destination(destination) => { + destination.serialize(sc, annotation.insert(Name(b"Dest"))) + } + Target::Action(action) => action.serialize(sc, annotation.action()), + } + } +} diff --git a/vendor/krilla/src/interactive/destination.rs b/vendor/krilla/src/interactive/destination.rs new file mode 100644 index 00000000..de40085d --- /dev/null +++ b/vendor/krilla/src/interactive/destination.rs @@ -0,0 +1,289 @@ +//! Destinations in a PDF document. +//! +//! In some cases, you might want to refer to other locations within the same document, for +//! example when defining the outline, or when link to a different section in the document +//! from a link. To achieve this, you can use destinations, which are associated with a page +//! and a specific location on that page. + +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use pdf_writer::{Obj, Ref, Str}; +use tiny_skia_path::Transform; + +use crate::chunk_container::ChunkContainer; +use crate::error::{KrillaError, KrillaResult}; +use crate::geom::Point; +use crate::serialize::{PageInfo, SerializeContext}; + +/// The type of destination. +#[derive(Hash)] +pub enum Destination { + /// An XYZ destination. + Xyz(XyzDestination), + /// A named destination. + Named(NamedDestination), +} + +impl Destination { + pub(crate) fn serialize(&self, sc: &mut SerializeContext, buffer: Obj) -> KrillaResult<()> { + match self { + Destination::Xyz(xyz) => { + let ref_ = sc.register_xyz_destination(xyz.clone()); + buffer.primitive(ref_); + + Ok(()) + } + Destination::Named(named) => named.serialize(sc, buffer), + } + } +} + +/// A destination associated with a name. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct NamedDestination { + pub(crate) name: Arc, + pub(crate) xyz_dest: Arc, +} + +impl From for Destination { + fn from(val: NamedDestination) -> Self { + Destination::Named(val) + } +} + +impl NamedDestination { + /// Create a new named destination. + /// + /// When used as part of a link annotation, the destination will be automatically registered + /// with the [`Document`](crate::Document). + /// + /// That said, you can also manually register a destination without linking to it by calling + /// [`Document::register_named_destination`](crate::Document::register_named_destination). + pub fn new(name: String, xyz_dest: XyzDestination) -> Self { + Self { + name: Arc::new(name), + xyz_dest: Arc::new(xyz_dest), + } + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + destination: Obj, + ) -> KrillaResult<()> { + sc.register_named_destination(self.clone()) + .ok_or_else(|| KrillaError::DuplicateNamedDestination(Arc::clone(&self.name)))?; + destination.primitive(Str(self.name.as_bytes())); + + Ok(()) + } +} + +#[derive(Debug)] +struct XyzDestRepr { + page_index: usize, + point: Point, +} + +impl Hash for XyzDestRepr { + fn hash(&self, state: &mut H) { + self.page_index.hash(state); + self.point.x.to_bits().hash(state); + self.point.y.to_bits().hash(state); + } +} + +impl PartialEq for XyzDestRepr { + fn eq(&self, other: &Self) -> bool { + self.page_index == other.page_index + && self.point.x == other.point.x + && self.point.y == other.point.y + } +} + +impl Eq for XyzDestRepr {} + +/// A destination pointing to a specific location at a specific page. +#[derive(Clone, Hash, PartialEq, Eq, Debug)] +pub struct XyzDestination(Arc); + +impl From for Destination { + fn from(val: XyzDestination) -> Self { + Destination::Xyz(val) + } +} + +impl XyzDestination { + /// Create a new XYZ destination. `page_index` should be the index (i.e. number) of the + /// target page, and point indicates the specific location on that page that should be + /// targeted. If the `page_index` is out of range, export will panic. + pub fn new(page_index: usize, point: Point) -> Self { + Self(Arc::new(XyzDestRepr { page_index, point })) + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let chunk = &mut chunk_container.non_stream.destinations; + let destination = chunk.destination(root_ref); + + let page_info = sc.page_infos().get(self.0.page_index).unwrap_or_else(|| { + panic!( + "attempted to link to page {}, but document only has {} pages", + self.0.page_index + 1, + sc.page_infos().len() + ) + }); + + let (ref_, surface_size) = match page_info { + PageInfo::Krilla { + ref_, surface_size, .. + } => (ref_, surface_size), + PageInfo::Pdf { ref_, size, .. } => (ref_, size), + }; + + let page_ref = *ref_; + let page_size = surface_size.height(); + + let mut mapped_point = self.0.point.to_tsp(); + // Convert to PDF coordinates + let invert_transform = Transform::from_row(1.0, 0.0, 0.0, -1.0, 0.0, page_size); + invert_transform.map_point(&mut mapped_point); + + destination + .page(page_ref) + .xyz(mapped_point.x, mapped_point.y, None); + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::annotation::{LinkAnnotation, Target}; + use crate::error::KrillaError; + use crate::geom::{Point, Rect}; + use crate::Document; + + use super::{NamedDestination, XyzDestination}; + + #[test] + fn named_duplicate_rejected() { + let mut document = Document::new(); + assert_eq!( + document.register_named_destination(NamedDestination::new( + "same".to_string(), + XyzDestination::new(0, Point::from_xy(0.0, 0.0)), + )), + Some(()) + ); + assert_eq!( + document.register_named_destination(NamedDestination::new( + "same".to_string(), + XyzDestination::new(0, Point::from_xy(100.0, 100.0)), + )), + None + ); + } + + #[test] + fn named_duplicate_same_location_allowed() { + let mut document = Document::new(); + assert_eq!( + document.register_named_destination(NamedDestination::new( + "same".to_string(), + XyzDestination::new(0, Point::from_xy(0.0, 0.0)), + )), + Some(()) + ); + assert_eq!( + document.register_named_destination(NamedDestination::new( + "same".to_string(), + XyzDestination::new(0, Point::from_xy(0.0, 0.0)), + )), + Some(()) + ); + + assert!(document.finish().is_ok()); + } + + #[test] + fn named_duplicate_annotation_rejected() { + let mut document = Document::new(); + let mut page = document.start_page(); + + page.add_annotation( + LinkAnnotation::new( + Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(), + Target::Destination( + NamedDestination::new( + "same".to_string(), + XyzDestination::new(0, Point::from_xy(0.0, 0.0)), + ) + .into(), + ), + ) + .into(), + ); + page.add_annotation( + LinkAnnotation::new( + Rect::from_xywh(0.0, 100.0, 100.0, 100.0).unwrap(), + Target::Destination( + NamedDestination::new( + "same".to_string(), + XyzDestination::new(0, Point::from_xy(100.0, 100.0)), + ) + .into(), + ), + ) + .into(), + ); + drop(page); + + assert_eq!( + document.finish(), + Err(KrillaError::DuplicateNamedDestination(Arc::new( + "same".to_string() + ))) + ); + } + + #[test] + fn named_duplicate_manual_then_annotation_rejected() { + let mut document = Document::new(); + assert_eq!( + document.register_named_destination(NamedDestination::new( + "same".to_string(), + XyzDestination::new(0, Point::from_xy(0.0, 0.0)), + )), + Some(()) + ); + + let mut page = document.start_page(); + page.add_annotation( + LinkAnnotation::new( + Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(), + Target::Destination( + NamedDestination::new( + "same".to_string(), + XyzDestination::new(0, Point::from_xy(100.0, 100.0)), + ) + .into(), + ), + ) + .into(), + ); + drop(page); + + assert_eq!( + document.finish(), + Err(KrillaError::DuplicateNamedDestination(Arc::new( + "same".to_string() + ))) + ); + } +} diff --git a/vendor/krilla/src/interactive/mod.rs b/vendor/krilla/src/interactive/mod.rs new file mode 100644 index 00000000..c886e437 --- /dev/null +++ b/vendor/krilla/src/interactive/mod.rs @@ -0,0 +1,3 @@ +pub mod action; +pub mod annotation; +pub mod destination; diff --git a/vendor/krilla/src/interchange/embed.rs b/vendor/krilla/src/interchange/embed.rs new file mode 100644 index 00000000..8e4f9ffb --- /dev/null +++ b/vendor/krilla/src/interchange/embed.rs @@ -0,0 +1,240 @@ +//! Embedding attachments to a PDF file. + +use std::ops::DerefMut; + +use pdf_writer::{Finish, Name, Ref, Str, TextStr}; + +use crate::chunk_container::ChunkContainer; +use crate::configure::{PdfVersion, ValidationError}; +use crate::interchange::metadata::pdf_date; +use crate::metadata::DateTime; +use crate::serialize::{Cacheable, SerializeContext}; +use crate::stream::FilterStreamBuilder; +use crate::surface::Location; +use crate::util::NameExt; +use crate::Data; + +/// An error while embedding the file. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum EmbedError { + /// The selected standard does not support embedding files. + Existence, + /// The document doesn't contain a modification date, which is required for embedded files + /// in some export modes. + MissingDate, + /// The embedded file is missing a human-readable description. + MissingDescription, + /// The mime type of the embedded file is missing. + MissingMimeType, +} + +/// An embedded file. +#[derive(Debug, Clone, Hash)] +pub struct EmbeddedFile { + /// The name of the embedded file. + pub path: String, + /// The mime type of the embedded file. + pub mime_type: Option, + /// A description of the embedded file. + pub description: Option, + /// The association kind of the embedded file. + pub association_kind: AssociationKind, + /// The raw data of the embedded file. + pub data: Data, + /// The modification date of the embedded file. + pub modification_date: Option, + /// Whether the embedded file should be compressed (recommended to turn off if the + /// original file already has compression). If `None`, krilla will use its own logic + /// for determining whether to compress the file or not. + pub compress: Option, + /// The location of the embedded file. + pub location: Option, +} + +impl Cacheable for EmbeddedFile { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + sc.register_validation_error(ValidationError::EmbeddedFile( + EmbedError::Existence, + self.location, + )); + + let chunk = &mut chunk_container.non_stream.embedded_files; + let mut stream_chunk = sc.new_chunk(); + let stream_ref = sc.new_ref(); + + let file_stream = match self.compress { + Some(true) => FilterStreamBuilder::new_from_binary_data(self.data.as_ref()), + Some(false) => FilterStreamBuilder::new_from_uncompressed(self.data.as_ref()), + None => FilterStreamBuilder::new_auto_compressed(self.data.as_ref()), + } + .finish(&sc.serialize_settings()); + + let mut embedded_file_stream = + stream_chunk.embedded_file(stream_ref, file_stream.encoded_data()); + file_stream.write_filters(embedded_file_stream.deref_mut().deref_mut()); + + if let Some(mime_type) = &self.mime_type { + embedded_file_stream.subtype(mime_type.0.to_pdf_name()); + } else { + sc.register_validation_error(ValidationError::EmbeddedFile( + EmbedError::MissingMimeType, + self.location, + )); + } + + let mut params = embedded_file_stream.params(); + params.size(self.data.as_ref().len() as i32); + + if let Some(date_time) = &self.modification_date { + let date = pdf_date(*date_time); + params.modification_date(date); + } else { + sc.register_validation_error(ValidationError::EmbeddedFile( + EmbedError::MissingDate, + self.location, + )); + } + + params.finish(); + embedded_file_stream.finish(); + + let mut file_spec = chunk.file_spec(root_ref); + file_spec.path(Str(self.path.as_bytes())); + + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf17 { + file_spec.unic_file(TextStr(&self.path)); + } + + let mut ef = file_spec.insert(Name(b"EF")).dict(); + ef.pair(Name(b"F"), stream_ref); + + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf17 { + ef.pair(Name(b"UF"), stream_ref); + } + + ef.finish(); + + if sc.serialize_settings().supports_associated_files() { + file_spec.association_kind(self.association_kind.to_pdf()); + } + + if let Some(description) = self.description { + file_spec.description(TextStr(&description)); + } else { + sc.register_validation_error(ValidationError::EmbeddedFile( + EmbedError::MissingDescription, + self.location, + )); + } + + file_spec.finish(); + chunk_container.streams.embedded_files.push(stream_chunk); + } +} + +/// How an embedded file relates to the PDF document it is embedded in. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum AssociationKind { + /// The PDF document was created from this source file. + Source, + /// This file was used to derive a visual presentation in the PDF. + Data, + /// An alternative representation of this document. + Alternative, + /// Additional resources for this document. + Supplement, + /// There is no clear relationship or it is not known. + Unspecified, +} + +impl AssociationKind { + fn to_pdf(self) -> pdf_writer::types::AssociationKind { + match self { + AssociationKind::Source => pdf_writer::types::AssociationKind::Source, + AssociationKind::Data => pdf_writer::types::AssociationKind::Data, + AssociationKind::Alternative => pdf_writer::types::AssociationKind::Alternative, + AssociationKind::Supplement => pdf_writer::types::AssociationKind::Supplement, + AssociationKind::Unspecified => pdf_writer::types::AssociationKind::Unspecified, + } + } +} + +/// A mime type. +#[derive(Debug, Clone, Hash)] +pub struct MimeType(String); + +impl MimeType { + /// Create a new mime type. + /// + /// Returns `None` if the mime type is invalid. + pub fn new(mime_type: &str) -> Option { + if valid_mime_type(mime_type) { + Some(Self(mime_type.to_string())) + } else { + None + } + } +} + +fn valid_mime_type(mime_type: &str) -> bool { + let parts: Vec<&str> = mime_type.split('/').collect(); + if parts.len() != 2 { + return false; + } + + let (type_part, subtype_part) = (parts[0], parts[1]); + + valid_mime_part(type_part) && valid_mime_part(subtype_part) +} + +fn valid_mime_part(part: &str) -> bool { + if part.is_empty() { + return false; + } + + part.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '+' || c == '.') +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_mime_types() { + assert!(valid_mime_type("text/plain")); + assert!(valid_mime_type("application/pdf")); + assert!(valid_mime_type("image/jpeg")); + assert!(valid_mime_type("application/octet-stream")); + assert!(valid_mime_type("text/html")); + assert!(valid_mime_type("application/json")); + assert!(valid_mime_type("image/png")); + assert!(valid_mime_type("video/mp4")); + assert!(valid_mime_type("audio/mpeg")); + assert!(valid_mime_type("application/vnd.ms-excel")); + assert!(valid_mime_type("text/css")); + assert!(valid_mime_type("application/x-www-form-urlencoded")); + assert!(valid_mime_type("multipart/form-data")); + assert!(valid_mime_type("application/xml+svg")); + assert!(valid_mime_type("text/plain+charset")); + } + + #[test] + fn invalid_mime_types() { + assert!(!valid_mime_type("source")); + assert!(!valid_mime_type("text/")); + assert!(!valid_mime_type("/plain")); + assert!(!valid_mime_type("")); + assert!(!valid_mime_type("text")); + assert!(!valid_mime_type("text/plain/extra")); + assert!(!valid_mime_type("text plain")); + assert!(!valid_mime_type("text\\plain")); + assert!(!valid_mime_type("text@plain")); + assert!(!valid_mime_type("text/plain*")); + } +} diff --git a/vendor/krilla/src/interchange/metadata.rs b/vendor/krilla/src/interchange/metadata.rs new file mode 100644 index 00000000..18dde72c --- /dev/null +++ b/vendor/krilla/src/interchange/metadata.rs @@ -0,0 +1,477 @@ +//! Setting document metadata. +//! +//! PDF allows for the inclusion of metadata in a PDF document. To do so in krilla, +//! you can simply create a [`Metadata`] object, set the data, and then include it +//! in the document via [`Document::set_metadata`]. +//! +//! [`Document::set_metadata`]: crate::document::Document::set_metadata +use pdf_writer::{Finish, Pdf, Ref, TextStr}; +use std::cell::LazyCell; +use xmp_writer::{LangId, Timezone, XmpWriter}; + +use crate::configure::{Configuration, PdfVersion, ValidationError}; +use crate::serialize::SerializeContext; + +/// Metadata for a PDF document. +#[derive(Default, Clone, Debug)] +pub struct Metadata { + pub(crate) title: Option, + pub(crate) description: Option, + pub(crate) creator: Option, + pub(crate) producer: Option, + pub(crate) keywords: Option>, + pub(crate) authors: Option>, + pub(crate) document_id: Option, + pub(crate) language: Option, + pub(crate) creation_date: Option, + pub(crate) text_direction: Option, + pub(crate) page_layout: Option, +} + +impl Metadata { + /// Create new metadata. + pub fn new() -> Self { + Self { + ..Default::default() + } + } + + /// The title of the document. + pub fn title(mut self, title: String) -> Self { + if !title.is_empty() { + self.title = Some(title); + } + self + } + + /// The description of the document. + /// + /// This should be a short, human-readable abstract, summary, or description + /// of the topic of the document. + pub fn description(mut self, description: String) -> Self { + if !description.is_empty() { + self.description = Some(description); + } + self + } + + /// The keywords that describe the document. + pub fn keywords(mut self, keywords: Vec) -> Self { + if !keywords.is_empty() { + self.keywords = Some(keywords); + } + self + } + + /// The main language of the document, as an RFC 3066 language tag. + /// + /// This property is required for some export modes, like for example PDF/A-3a. + pub fn language(mut self, language: String) -> Self { + self.language = Some(language); + self + } + + /// The creator tool of the document. + pub fn creator(mut self, creator: String) -> Self { + if !creator.is_empty() { + self.creator = Some(creator); + } + self + } + + /// The producer tool of the document. + pub fn producer(mut self, producer: String) -> Self { + if !producer.is_empty() { + self.producer = Some(producer); + } + self + } + + /// The authors of the document. + pub fn authors(mut self, authors: Vec) -> Self { + if !authors.is_empty() { + self.authors = Some(authors); + } + self + } + + /// The creation date of the document. + pub fn creation_date(mut self, creation_date: DateTime) -> Self { + self.creation_date = Some(creation_date); + self + } + + /// A document ID. + /// + /// This attribute will be used as an identifier for identifying + /// different versions of the same document. + pub fn document_id(mut self, document_id: String) -> Self { + self.document_id = Some(document_id); + self + } + + /// The main text direction of the document. + pub fn text_direction(mut self, text_direction: TextDirection) -> Self { + self.text_direction = Some(text_direction); + self + } + + /// How the viewer should lay out the pages. + pub fn page_layout(mut self, page_layout: PageLayout) -> Self { + self.page_layout = Some(page_layout); + self + } + + pub(crate) fn has_document_info(&self) -> bool { + self.title.is_some() + || self.producer.is_some() + || self.keywords.is_some() + || self.authors.is_some() + || self.creator.is_some() + || self.creation_date.is_some() + || self.description.is_some() + } + + pub(crate) fn serialize_xmp_metadata( + &self, + xmp: &mut XmpWriter, + sc: &mut SerializeContext, + instance_id: &str, + ) { + if let Some(title) = &self.title { + xmp.title([(None, title.as_str())]); + } + + if let Some(description) = &self.description { + xmp.description([(None, description.as_str())]); + } + + if let Some(keywords) = &self.keywords { + let joined = keywords.join(", "); + xmp.pdf_keywords(joined.as_str()); + } + + match &self.authors { + Some(authors) if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf20 => { + // PDF 2.0+ deprecates the document information dictionary, so + // we can use the array here. + xmp.creator(authors.iter().map(String::as_str)); + } + Some(authors) => { + // Turns out that if the authors are given in both the document + // information dictionary and the XMP metadata, Acrobat takes a + // little bit of both: The first author from the document + // information dictionary and the remaining authors from the XMP + // metadata. + // + // To fix this for Acrobat, we could omit the remaining authors + // or all metadata from the document information catalog (it is + // optional) and only write XMP. However, not all other tools + // (including Apple Preview) read the XMP data. This means we do + // want to include all authors in the document information + // dictionary. + // + // Thus, the only alternative is to fold all authors into a + // single `` in the XMP metadata. This is, in fact, + // exactly what the PDF/A spec Part 1 section 6.7.3 has to say + // about the matter. It's a bit weird to not use the array (and + // it makes Acrobat show the author list in quotes), but there's + // not much we can do about that. + let joined = authors.join(", "); + xmp.creator([joined.as_str()]); + } + None => {} + } + + if let Some(creator) = &self.creator { + xmp.creator_tool(creator); + } + + if let Some(producer) = &self.producer { + xmp.producer(producer); + } + + if let Some(lang) = &self.language { + xmp.language([LangId(lang)]); + } + + if let Some(date) = self.creation_date.map(xmp_date) { + xmp.modify_date(date); + xmp.create_date(date); + + if sc + .serialize_settings() + .validators() + .requires_file_provenance_information() + { + let mut history = xmp.history(); + let mut saved = history.add_event(); + + saved + .action(xmp_writer::ResourceEventAction::Saved) + .when(date); + + if !sc + .serialize_settings() + .validators() + .prohibits_instance_id_in_xmp_metadata() + { + saved.instance_id(&format!("{instance_id}_source")); + } + + saved.finish(); + + let mut converted = history.add_event(); + + converted + .action(xmp_writer::ResourceEventAction::Converted) + .when(date); + + if let Some(creator) = &self.creator { + converted.software_agent(creator); + } + + if !sc + .serialize_settings() + .validators() + .prohibits_instance_id_in_xmp_metadata() + { + converted.instance_id(&format!("{instance_id}_source")); + } + } + } else { + sc.register_validation_error(ValidationError::MissingDocumentDate); + } + } + + pub(crate) fn serialize_document_info( + &self, + ref_: &mut Ref, + pdf: &mut Pdf, + config: Configuration, + ) { + if config.validators().prohibits_info_dict() { + return; + } + + if self.has_document_info() { + let ref_ = ref_.bump(); + let mut document_info = LazyCell::new(|| pdf.document_info(ref_)); + + // ALl of those are deprecated in PDF 2.0 and will only be written + // to the XMP metadata. + if config.version() < PdfVersion::Pdf20 { + if let Some(title) = &self.title { + document_info.title(TextStr(title)); + } + + if let Some(description) = &self.description { + document_info.subject(TextStr(description)); + } + + if let Some(keywords) = &self.keywords { + let joined = keywords.join(", "); + document_info.keywords(TextStr(&joined)); + } + + if let Some(authors) = &self.authors { + let joined = authors.join(", "); + document_info.author(TextStr(&joined)); + } + + if let Some(creator) = &self.creator { + document_info.creator(TextStr(creator)); + } + + if let Some(producer) = &self.producer { + document_info.producer(TextStr(producer)); + } + } + + if let Some(date_time) = self.creation_date { + document_info.modified_date(pdf_date(date_time)); + document_info.creation_date(pdf_date(date_time)); + } + } + } +} + +/// A datetime. Invalid values will be clamped. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct DateTime { + /// The year (0-9999). + pub(crate) year: u16, + /// The month (0-11). + pub(crate) month: Option, + /// The day (0-31). + pub(crate) day: Option, + /// The hour (0-23). + pub(crate) hour: Option, + /// The minute (0-59). + pub(crate) minute: Option, + /// The second (0-59). + pub(crate) second: Option, + /// The hour offset from UTC (-23 through 23). + pub(crate) utc_offset_hour: Option, + /// The minute offset from UTC (0-59). Will carry over the sign from + /// `utc_offset_hour`. + pub(crate) utc_offset_minute: u8, +} + +impl DateTime { + /// Create a new, minimal date. The year will be clamped within the range + /// 0-9999. + #[inline] + pub fn new(year: u16) -> Self { + Self { + year: year.min(9999), + month: None, + day: None, + hour: None, + minute: None, + second: None, + utc_offset_hour: None, + utc_offset_minute: 0, + } + } + + /// Add the month field. It will be clamped within the range 1-12. + #[inline] + pub fn month(mut self, month: u8) -> Self { + self.month = Some(month.clamp(1, 12)); + self + } + + /// Add the day field. It will be clamped within the range 1-31. + #[inline] + pub fn day(mut self, day: u8) -> Self { + self.day = Some(day.clamp(1, 31)); + self + } + + /// Add the hour field. It will be clamped within the range 0-23. + #[inline] + pub fn hour(mut self, hour: u8) -> Self { + self.hour = Some(hour.min(23)); + self + } + + /// Add the minute field. It will be clamped within the range 0-59. + #[inline] + pub fn minute(mut self, minute: u8) -> Self { + self.minute = Some(minute.min(59)); + self + } + + /// Add the second field. It will be clamped within the range 0-59. + #[inline] + pub fn second(mut self, second: u8) -> Self { + self.second = Some(second.min(59)); + self + } + + /// Add the offset from UTC in hours. If not specified, the time will be + /// assumed to be local to the viewer's time zone. It will be clamped within + /// the range -23-23. + #[inline] + pub fn utc_offset_hour(mut self, hour: i8) -> Self { + self.utc_offset_hour = Some(hour.clamp(-23, 23)); + self + } + + /// Add the offset from UTC in minutes. This will have the same sign as set in + /// [`Self::utc_offset_hour`]. It will be clamped within the range 0-59. + #[inline] + pub fn utc_offset_minute(mut self, minute: u8) -> Self { + self.utc_offset_minute = minute.min(59); + self + } +} + +/// Converts a datetime to a pdf-writer date. +pub(crate) fn pdf_date(date_time: DateTime) -> pdf_writer::Date { + // We always assume a full date with all fields because for some reason + // Acrobat doesn't like PDF/A-1 files without everything set. + pdf_writer::Date::new(date_time.year) + .month(date_time.month.unwrap_or(1)) + .day(date_time.day.unwrap_or(1)) + .hour(date_time.hour.unwrap_or(0)) + .minute(date_time.minute.unwrap_or(0)) + .second(date_time.second.unwrap_or(0)) + .utc_offset_hour(date_time.utc_offset_hour.unwrap_or(0)) + .utc_offset_minute(date_time.utc_offset_minute) +} + +/// Converts a datetime to an xmp-writer datetime. +fn xmp_date(datetime: DateTime) -> xmp_writer::DateTime { + let timezone = match (datetime.utc_offset_hour, datetime.utc_offset_minute) { + (Some(h), m) => Some(Timezone::Local { + hour: h, + minute: m as i8, + }), + _ => Some(Timezone::Utc), + }; + + // We always assume a full date with all fields because for some reason + // Acrobat doesn't like PDF/A-1 files without everything set. + xmp_writer::DateTime { + year: datetime.year, + month: Some(datetime.month.unwrap_or(1)), + day: Some(datetime.day.unwrap_or(1)), + hour: Some(datetime.hour.unwrap_or(0)), + minute: Some(datetime.minute.unwrap_or(0)), + second: Some(datetime.second.unwrap_or(0)), + timezone, + } +} + +/// The main text direction of the document. +#[allow(missing_docs)] +#[derive(Copy, Clone, Debug)] +pub enum TextDirection { + LeftToRight, + RightToLeft, +} + +impl TextDirection { + pub(crate) fn to_pdf(self) -> pdf_writer::types::Direction { + match self { + TextDirection::LeftToRight => pdf_writer::types::Direction::L2R, + TextDirection::RightToLeft => pdf_writer::types::Direction::R2L, + } + } +} + +/// How the viewer should lay out the pages. +#[derive(Copy, Clone, Debug)] +pub enum PageLayout { + /// Only a single page at a time. + SinglePage, + /// A single, continuously scrolling column of pages. + OneColumn, + /// Two continuously scrolling columns of pages, laid out with odd-numbered + /// pages on the left. + TwoColumnLeft, + /// Two continuously scrolling columns of pages, laid out with odd-numbered + /// pages on the right (like in a left-bound book). + TwoColumnRight, + /// Only two pages are visible at a time, laid out with odd-numbered pages + /// on the left. PDF 1.5+. + TwoPageLeft, + /// Only two pages are visible at a time, laid out with odd-numbered pages + /// on the right (like in a left-bound book). PDF 1.5+. + TwoPageRight, +} + +impl PageLayout { + pub(crate) fn to_pdf(self) -> pdf_writer::types::PageLayout { + match self { + PageLayout::SinglePage => pdf_writer::types::PageLayout::SinglePage, + PageLayout::OneColumn => pdf_writer::types::PageLayout::OneColumn, + PageLayout::TwoColumnLeft => pdf_writer::types::PageLayout::TwoColumnLeft, + PageLayout::TwoColumnRight => pdf_writer::types::PageLayout::TwoColumnRight, + PageLayout::TwoPageLeft => pdf_writer::types::PageLayout::TwoPageLeft, + PageLayout::TwoPageRight => pdf_writer::types::PageLayout::TwoPageRight, + } + } +} diff --git a/vendor/krilla/src/interchange/mod.rs b/vendor/krilla/src/interchange/mod.rs new file mode 100644 index 00000000..439a99b7 --- /dev/null +++ b/vendor/krilla/src/interchange/mod.rs @@ -0,0 +1,4 @@ +pub mod embed; +pub mod metadata; +pub mod outline; +pub mod tagging; diff --git a/vendor/krilla/src/interchange/outline.rs b/vendor/krilla/src/interchange/outline.rs new file mode 100644 index 00000000..ebe865a5 --- /dev/null +++ b/vendor/krilla/src/interchange/outline.rs @@ -0,0 +1,263 @@ +//! Building outlines for the navigation of the document. +//! +//! An outline is a tree-like structure that stores the hierarchical structure of a document. +//! In particular, in most cases it is used to define the hierarchy of headings in the document. +//! For example, your document might consist of two first-level headings, which in turn have +//! more nested headings, which in turn might be nested even further, and so on. The [`Outline`] +//! allows you to encode this behavior, and makes it easier to navigate the document in PDF +//! viewers that support this feature. +//! +//! The intended usage is to create a new, empty outline in the very beginning. It represents +//! the root of the tree. Then, you can create new outline nodes, which you can recursively +//! nest in any way you wish while processing your document. In the end, you need to push +//! the first layer of children to the [`Outline`] object. +//! +//! Finally, once you are done building your outline tree, you can use the [`Document::set_outline`] +//! function of [`Document`] to store the outline in the document. +//! +//! [`Document`]: crate::Document +//! [`Document::set_outline`]: crate::Document::set_outline + +use pdf_writer::writers::OutlineItem; +use pdf_writer::{Chunk, Finish, Name, Ref, TextStr}; + +use crate::chunk_container::ChunkContainer; +use crate::interactive::destination::XyzDestination; +use crate::serialize::SerializeContext; + +/// An outline. +/// +/// This represents the root of the outline tree. +#[derive(Debug, Clone)] +pub struct Outline { + children: Vec, +} + +impl Outline { + pub(crate) fn is_empty(&self) -> bool { + self.children.is_empty() + } +} + +impl Default for Outline { + fn default() -> Self { + Self::new() + } +} + +trait Outlineable { + fn first(&mut self, item: Ref) -> &mut Self; + fn last(&mut self, item: Ref) -> &mut Self; + fn count(&mut self, count: i32) -> &mut Self; +} + +impl Outlineable for pdf_writer::writers::Outline<'_> { + fn first(&mut self, item: Ref) -> &mut Self { + self.first(item) + } + + fn last(&mut self, item: Ref) -> &mut Self { + self.last(item) + } + + fn count(&mut self, count: i32) -> &mut Self { + self.count(count) + } +} + +impl Outlineable for OutlineItem<'_> { + fn first(&mut self, item: Ref) -> &mut Self { + self.first(item) + } + + fn last(&mut self, item: Ref) -> &mut Self { + self.last(item) + } + + fn count(&mut self, count: i32) -> &mut Self { + self.count(count) + } +} + +struct SerializedChildren { + first: Ref, + last: Ref, + visible_count: usize, +} + +impl SerializedChildren { + fn write(&self, outlineable: &mut impl Outlineable, negate_count: bool) { + outlineable.first(self.first); + outlineable.last(self.last); + + let mut count = i32::try_from(self.visible_count).unwrap(); + if negate_count { + count = -count; + } + outlineable.count(count); + } +} + +impl Outline { + /// Create a new, empty outline. + pub fn new() -> Self { + Self { children: vec![] } + } + + /// Push a new child (which may in turn contain other children) to the outline. + pub fn push_child(&mut self, node: OutlineNode) { + self.children.push(node) + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root: Ref, + ) { + let mut chunk = sc.new_chunk(); + let children = serialize_children(&self.children, root, &mut chunk, sc); + + let mut outline = chunk.outline(root); + if let Some(children) = &children { + children.write(&mut outline, false); + } + outline.finish(); + + chunk_container.non_stream.outline = Some((root, chunk)); + } +} + +/// An outline node. +/// +/// This represents either an intermediate node in the outline tree, or a leaf node +/// if it does not contain any further children itself. +#[derive(Debug, Clone)] +pub struct OutlineNode { + /// The children of the outline node. + children: Vec, + /// The text of the outline entry. + text: String, + /// The destination of the outline entry. + destination: XyzDestination, + /// Whether this node is initially open (children shown expanded). + open: bool, +} + +impl OutlineNode { + /// Create a new outline node. + /// + /// `text` is the string that should be displayed in the outline tree, and + /// `destination` is the destination that should be jumped to when clicking on + /// the outline entry. + pub fn new(text: String, destination: XyzDestination) -> Self { + Self { + children: vec![], + text, + destination, + open: false, + } + } + + /// Set whether this node is initially open (children shown expanded). + /// + /// If `open` is `true`, the node's children will be shown expanded when the + /// document is opened in a PDF viewer; if `false`, the children will be + /// collapsed. Leaf nodes (nodes without children) are unaffected by this flag. + /// + /// By default, this flag is set to `false`. + pub fn with_open(mut self, open: bool) -> Self { + self.open = open; + self + } + + /// Add a new child to the outline node. + pub fn push_child(&mut self, node: OutlineNode) { + self.children.push(node) + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + parent: Ref, + root: Ref, + next: Option, + prev: Option, + chunk: &mut Chunk, + ) -> usize { + let children = serialize_children(&self.children, root, chunk, sc); + let mut outline_entry = chunk.outline_item(root); + outline_entry.parent(parent); + + if let Some(next) = next { + outline_entry.next(next); + } + + if let Some(prev) = prev { + outline_entry.prev(prev); + } + + if let Some(children) = &children { + children.write(&mut outline_entry, !self.open); + } + + outline_entry.title(TextStr(&self.text)); + + let dest_ref = sc.register_xyz_destination(self.destination.clone()); + outline_entry.pair(Name(b"Dest"), dest_ref); + + outline_entry.finish(); + + // See the algorithm described in the PDF spec. When recursing down, we + // do not go into nodes that whose count is negative (i.e. nodes that are + // closed). Therefore, in case the node is closed, the visible count is + // just the node itself, so 1. + + if self.open { + 1 + children.map_or(0, |children| children.visible_count) + } else { + 1 + } + } +} + +fn serialize_children( + children: &[OutlineNode], + parent: Ref, + chunk: &mut Chunk, + sc: &mut SerializeContext, +) -> Option { + let mut visible_count = 0; + + if !children.is_empty() { + let first = sc.new_ref(); + let mut last = first; + + let mut prev = None; + let mut cur = Some(first); + + for i in 0..children.len() { + let next = if i < children.len() - 1 { + Some(sc.new_ref()) + } else { + None + }; + + last = cur.unwrap(); + + let child_visible_count = children[i].serialize(sc, parent, last, next, prev, chunk); + visible_count += child_visible_count; + + prev = cur; + cur = next; + } + + Some(SerializedChildren { + first, + last, + visible_count, + }) + } else { + None + } +} diff --git a/vendor/krilla/src/interchange/tagging/fmt.rs b/vendor/krilla/src/interchange/tagging/fmt.rs new file mode 100644 index 00000000..66c9bdbd --- /dev/null +++ b/vendor/krilla/src/interchange/tagging/fmt.rs @@ -0,0 +1,733 @@ +//! Format the tag tree in a human readable way, in form of a YAML document. +//! YAML is a good fit to represent the hierarchy of the tag tree, and many +//! editors allow folding the sub-trees. + +use std::fmt::Display; + +use crate::tagging::{ + Attr, BBox, BlockAlign, BorderStyle, ColumnDimensions, GlyphOrientationVertical, Identifier, + IdentifierInner, IdentifierType, InlineAlign, LayoutAttr, LineHeight, ListAttr, ListNumbering, + NaiveRgbColor, Node, Placement, Sides, StructAttr, TableAttr, TableHeaderScope, TagGroup, + TagId, TagKind, TagTree, TextAlign, TextDecorationType, WritingMode, +}; + +/// Helper trait for indented output. +pub trait Output { + /// Wrapper around [`Output::output`] with a zero indent; + fn output(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { + self.output_indent(f, Indent(0)) + } + + /// Output data with an indent. + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result; + + /// Create a [`Display`]able wrapper. + fn display<'a>(&'a self) -> Wrapper<'a, Self> { + Wrapper { + inner: self, + indent: Indent(0), + } + } + + /// Create a [`Display`]able wrapper with a specific indent. + fn display_indent<'a>(&'a self, indent: Indent) -> Wrapper<'a, Self> { + Wrapper { + inner: self, + indent, + } + } +} + +/// A [`Display`]able wrapper struct around an [`Output`]. +pub struct Wrapper<'a, T: Output + ?Sized> { + inner: &'a T, + indent: Indent, +} + +impl Display for Wrapper<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.output_indent(f, self.indent) + } +} + +/// A [`Display`]able indentation. +#[derive(Clone, Copy)] +pub struct Indent(pub usize); + +impl Indent { + fn inc(self) -> Self { + Self(self.0 + 1) + } +} + +impl Display for Indent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:indent$}", "", indent = 2 * self.0) + } +} + +impl Output for TagTree { + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result { + for child in self.children.iter() { + child.output_indent(f, indent)?; + } + Ok(()) + } +} + +impl Output for Node { + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result { + match self { + Node::Group(group) => group.output_indent(f, indent), + Node::Leaf(Identifier(IdentifierInner::Real(IdentifierType::PageIdentifier(pi)))) => { + writeln!( + f, + "{indent}- Content: page={} mcid={}", + pi.page_index, pi.mcid + ) + } + Node::Leaf(Identifier(IdentifierInner::Real( + IdentifierType::AnnotationIdentifier(ai), + ))) => { + writeln!( + f, + "{indent}- Annotation: page={} index={}", + ai.page_index, ai.annot_index + ) + } + Node::Leaf(Identifier(IdentifierInner::Dummy)) => writeln!(f, "{indent}- Artifact"), + } + } +} + +impl Output for TagGroup { + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result { + self.tag.output_indent(f, indent)?; + let indent = indent.inc(); + if !self.children.is_empty() { + writeln!(f, "{indent}/K:")?; + let indent = indent.inc(); + for node in self.children.iter() { + node.output_indent(f, indent)?; + } + } + Ok(()) + } +} + +impl Output for TagKind { + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result { + write!(f, "{indent}- Tag: ")?; + match self { + TagKind::Part(_) => write!(f, "Part"), + TagKind::Article(_) => write!(f, "Article"), + TagKind::Section(_) => write!(f, "Section"), + TagKind::Div(_) => write!(f, "Div"), + TagKind::BlockQuote(_) => write!(f, "BlockQuote"), + TagKind::Caption(_) => write!(f, "Caption"), + TagKind::TOC(_) => write!(f, "TOC"), + TagKind::TOCI(_) => write!(f, "TOCI"), + TagKind::Index(_) => write!(f, "Index"), + TagKind::P(_) => write!(f, "P"), + TagKind::Hn(tag) => write!(f, "H{}", tag.level().get()), + TagKind::L(_) => write!(f, "L"), + TagKind::LI(_) => write!(f, "LI"), + TagKind::Lbl(_) => write!(f, "Lbl"), + TagKind::LBody(_) => write!(f, "LBody"), + TagKind::Table(_) => write!(f, "Table"), + TagKind::TR(_) => write!(f, "TR"), + TagKind::TH(_) => write!(f, "TH"), + TagKind::TD(_) => write!(f, "TD"), + TagKind::THead(_) => write!(f, "THead"), + TagKind::TBody(_) => write!(f, "TBody"), + TagKind::TFoot(_) => write!(f, "TFoot"), + TagKind::Span(_) => write!(f, "Span"), + TagKind::InlineQuote(_) => write!(f, "InlineQuote"), + TagKind::Note(_) => write!(f, "Note"), + TagKind::Reference(_) => write!(f, "Reference"), + TagKind::BibEntry(_) => write!(f, "BibEntry"), + TagKind::Code(_) => write!(f, "Code"), + TagKind::Link(_) => write!(f, "Link"), + TagKind::Annot(_) => write!(f, "Annot"), + TagKind::Figure(_) => write!(f, "Figure"), + TagKind::Formula(_) => write!(f, "Formula"), + TagKind::Form(_) => write!(f, "Form"), + TagKind::NonStruct(_) => write!(f, "NonStruct"), + TagKind::Datetime(_) => write!(f, "Datetime"), + TagKind::Terms(_) => write!(f, "Terms"), + TagKind::Title(_) => write!(f, "Title"), + TagKind::Strong(_) => write!(f, "Strong"), + TagKind::Em(_) => write!(f, "Em"), + }?; + writeln!(f)?; + + let indent = indent.inc(); + for attr in self.as_any().attrs.iter() { + attr.output_indent(f, indent)?; + } + + Ok(()) + } +} + +impl Output for Attr { + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result { + use LayoutAttr::*; + use ListAttr::*; + use StructAttr::*; + use TableAttr::*; + + if let Attr::Struct(StructAttr::HeadingLevel(_)) = self { + return Ok(()); + } + + write!(f, "{indent}")?; + match self { + Attr::Struct(struct_attr) => match struct_attr { + Id(id) => writeln!(f, "/Id: {}", id.display()), + Lang(lang) => writeln!(f, "/Lang: {lang:?}"), + AltText(alt) => writeln!(f, "/Alt: {alt:?}"), + Expanded(e) => writeln!(f, "/E: {e:?}"), + ActualText(actual) => writeln!(f, "/ActualText: {actual:?}"), + Title(title) => writeln!(f, "/T: {title:?}"), + + // Not a real attribute, is already displayed in tag kind. + HeadingLevel(_) => Ok(()), + }, + Attr::List(list_attr) => match list_attr { + Numbering(n) => writeln!(f, "/Numbering: {}", n.display()), + }, + Attr::Table(table_attr) => match table_attr { + Summary(summary) => writeln!(f, "/Summary: {summary:?}"), + HeaderScope(scope) => writeln!(f, "/Scope: {}", scope.display()), + CellHeaders(headers) => { + write!(f, "/Headers: [")?; + if let Some((first, remainder)) = headers.split_first() { + first.output(f)?; + for id in remainder.iter() { + write!(f, ", {}", id.display())?; + } + } + writeln!(f, "]") + } + RowSpan(rowspan) => writeln!(f, "/RowSpan: {}", rowspan.get()), + ColSpan(colspan) => writeln!(f, "/ColSpan: {}", colspan.get()), + }, + Attr::Layout(layout_attr) => match layout_attr { + Placement(placement) => writeln!(f, "/Placement: {}", placement.display()), + WritingMode(mode) => writeln!(f, "/WritingMode: {}", mode.display()), + BBox(bbox) => writeln!(f, "/BBox:{}", bbox.display_indent(indent.inc())), + Width(width) => writeln!(f, "/Width: {}", width.display()), + Height(height) => writeln!(f, "/Height: {}", height.display()), + BackgroundColor(color) => writeln!(f, "/BackgroundColor: {}", color.display()), + BorderColor(sides) => { + let space = omit_if(" ", sides.is_multiline()); + let sides = sides.display_indent(indent.inc()); + writeln!(f, "/BorderColor:{space}{sides}") + } + BorderStyle(sides) => { + let space = omit_if(" ", sides.is_multiline()); + let sides = sides.display_indent(indent.inc()); + writeln!(f, "/BorderStyle:{space}{sides}") + } + BorderThickness(sides) => { + let space = omit_if(" ", sides.is_multiline()); + let sides = sides.display_indent(indent.inc()); + writeln!(f, "/BorderThickness:{space}{sides}") + } + Padding(sides) => { + writeln!(f, "/Padding: {}", sides.display_indent(indent.inc())) + } + Color(color) => writeln!(f, "/Color: {}", color.display()), + SpaceBefore(space) => writeln!(f, "/SpaceBefore: {}", space.display()), + SpaceAfter(space) => writeln!(f, "/SpaceAfter: {}", space.display()), + StartIndent(indent) => writeln!(f, "/StartIndent: {}", indent.display()), + EndIndent(indent) => writeln!(f, "/EndIndent: {}", indent.display()), + TextIndent(indent) => writeln!(f, "/TextIndent: {}", indent.display()), + TextAlign(align) => writeln!(f, "/TextAlign: {}", align.display()), + BlockAlign(align) => writeln!(f, "/BlockAlign: {}", align.display()), + InlineAlign(align) => writeln!(f, "/InlineAlign: {}", align.display()), + TableBorderStyle(sides) => { + let space = omit_if(" ", sides.is_multiline()); + let style = sides.display_indent(indent.inc()); + writeln!(f, "/TableBorderStyle:{space}{style}") + } + TablePadding(sides) => { + let space = omit_if(" ", sides.is_multiline()); + let sides = sides.display_indent(indent.inc()); + writeln!(f, "/TablePadding:{space}{sides}") + } + BaselineShift(shift) => writeln!(f, "/BaselineShift: {}", shift.display()), + LineHeight(line_height) => { + writeln!(f, "/LineHeight: {}", line_height.display()) + } + TextDecorationColor(color) => { + writeln!(f, "/TextDecorationColor: {}", color.display()) + } + TextDecorationThickness(thickness) => { + writeln!(f, "/TextDecorationThickness: {}", thickness.display()) + } + TextDecorationType(deco_type) => { + writeln!(f, "/TextDecorationType: {}", deco_type.display()) + } + GlyphOrientationVertical(orientation) => { + writeln!(f, "/GlyphOrientationVertical: {}", orientation.display()) + } + ColumnCount(column_count) => writeln!(f, "/ColumnCount: {}", column_count.get()), + ColumnGap(column_gap) => { + let space = omit_if(" ", column_gap.is_multiline()); + let column_gap = column_gap.display_indent(indent.inc()); + writeln!(f, "/ColumnGap:{space}{column_gap}") + } + ColumnWidths(column_width) => { + let space = omit_if(" ", column_width.is_multiline()); + let column_width = column_width.display_indent(indent.inc()); + writeln!(f, "/ColumnWidths:{space}{column_width}") + } + }, + } + } +} + +trait ValueOutput: Output { + fn is_multiline(&self) -> bool { + false + } +} + +#[derive(Clone, Copy)] +struct OmitText { + text: &'static str, + omit: bool, +} + +fn omit_if(text: &'static str, omit: bool) -> OmitText { + OmitText { text, omit } +} + +impl Display for OmitText { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !self.omit { + f.write_str(self.text)?; + } + Ok(()) + } +} + +impl ValueOutput for TagId {} +impl Output for TagId { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + if self + .as_bytes() + .iter() + .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z')) + { + let str = std::str::from_utf8(self.as_bytes()).unwrap(); + write!(f, "\"{str}\"")?; + } else { + for b in self.as_bytes() { + write!(f, "0x{b:02x}")?; + } + } + Ok(()) + } +} + +impl ValueOutput for ListNumbering {} +impl Output for ListNumbering { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + ListNumbering::None => write!(f, "None"), + ListNumbering::Disc => write!(f, "Disc"), + ListNumbering::Circle => write!(f, "Circle"), + ListNumbering::Square => write!(f, "Square"), + ListNumbering::Decimal => write!(f, "Decimal"), + ListNumbering::LowerRoman => write!(f, "LowerRoman"), + ListNumbering::UpperRoman => write!(f, "UpperRoman"), + ListNumbering::LowerAlpha => write!(f, "LowerAlpha"), + ListNumbering::UpperAlpha => write!(f, "UpperAlpha"), + } + } +} + +impl ValueOutput for TableHeaderScope {} +impl Output for TableHeaderScope { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + TableHeaderScope::Row => write!(f, "Row"), + TableHeaderScope::Column => write!(f, "Column"), + TableHeaderScope::Both => write!(f, "Both"), + } + } +} + +impl ValueOutput for Placement {} +impl Output for Placement { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + Placement::Block => write!(f, "Block"), + Placement::Inline => write!(f, "Inline"), + Placement::Before => write!(f, "Before"), + Placement::Start => write!(f, "Start"), + Placement::End => write!(f, "End"), + } + } +} + +impl ValueOutput for WritingMode {} +impl Output for WritingMode { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + WritingMode::LrTb => write!(f, "LrTb"), + WritingMode::RlTb => write!(f, "RlTb"), + WritingMode::TbRl => write!(f, "TbRl"), + } + } +} + +impl ValueOutput for BBox { + fn is_multiline(&self) -> bool { + true + } +} +impl Output for BBox { + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result { + writeln!(f)?; + writeln!(f, "{indent}page: {}", self.page_idx)?; + writeln!( + f, + "{indent}left: {}", + self.rect.left().display_indent(indent) + )?; + writeln!( + f, + "{indent}top: {}", + self.rect.top().display_indent(indent) + )?; + writeln!( + f, + "{indent}right: {}", + self.rect.right().display_indent(indent) + )?; + write!( + f, + "{indent}bottom: {}", + self.rect.bottom().display_indent(indent) + )?; + Ok(()) + } +} + +impl ValueOutput for Sides { + fn is_multiline(&self) -> bool { + !self.is_uniform() || self.before.is_multiline() + } +} +impl Output for Sides { + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result { + if self.is_uniform() { + self.before.output_indent(f, indent)?; + } else { + writeln!(f)?; + + let space = omit_if(" ", self.before.is_multiline()); + let before = self.before.display_indent(indent.inc()); + writeln!(f, "{indent}before:{space}{before}")?; + + let space = omit_if(" ", self.after.is_multiline()); + let after = self.after.display_indent(indent.inc()); + writeln!(f, "{indent}after:{space}{after}")?; + + let space = omit_if(" ", self.start.is_multiline()); + let start = self.start.display_indent(indent.inc()); + writeln!(f, "{indent}start:{space}{start}")?; + + let space = omit_if(" ", self.end.is_multiline()); + let end = self.end.display_indent(indent.inc()); + write!(f, "{indent}end:{space}{end}")?; + } + Ok(()) + } +} + +impl ValueOutput for NaiveRgbColor {} +impl Output for NaiveRgbColor { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + let r = self.red; + let g = self.green; + let b = self.blue; + write!(f, "#{r:02x}{g:02x}{b:02x}") + } +} + +impl ValueOutput for BorderStyle {} +impl Output for BorderStyle { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + BorderStyle::None => write!(f, "None"), + BorderStyle::Hidden => write!(f, "Hidden"), + BorderStyle::Solid => write!(f, "Solid"), + BorderStyle::Dashed => write!(f, "Dashed"), + BorderStyle::Dotted => write!(f, "Dotted"), + BorderStyle::Double => write!(f, "Double"), + BorderStyle::Groove => write!(f, "Groove"), + BorderStyle::Ridge => write!(f, "Ridge"), + BorderStyle::Inset => write!(f, "Inset"), + BorderStyle::Outset => write!(f, "Outset"), + } + } +} + +impl ValueOutput for TextAlign {} +impl Output for TextAlign { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + TextAlign::Start => write!(f, "Start"), + TextAlign::Center => write!(f, "Center"), + TextAlign::End => write!(f, "End"), + TextAlign::Justify => write!(f, "Justify"), + } + } +} + +impl ValueOutput for BlockAlign {} +impl Output for BlockAlign { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + BlockAlign::Begin => write!(f, "Begin"), + BlockAlign::Middle => write!(f, "Middle"), + BlockAlign::After => write!(f, "After"), + BlockAlign::Justify => write!(f, "Justify"), + } + } +} + +impl ValueOutput for InlineAlign {} +impl Output for InlineAlign { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + InlineAlign::Start => write!(f, "Start"), + InlineAlign::Center => write!(f, "Center"), + InlineAlign::End => write!(f, "End"), + } + } +} + +impl ValueOutput for LineHeight {} +impl Output for LineHeight { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + LineHeight::Normal => write!(f, "Normal"), + LineHeight::Auto => write!(f, "Auto"), + LineHeight::Custom(custom) => custom.output(f), + } + } +} + +impl ValueOutput for TextDecorationType {} +impl Output for TextDecorationType { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + TextDecorationType::None => write!(f, "None"), + TextDecorationType::Underline => write!(f, "Underline"), + TextDecorationType::Overline => write!(f, "Overline"), + TextDecorationType::LineThrough => write!(f, "LineThrough"), + } + } +} + +impl ValueOutput for GlyphOrientationVertical {} +impl Output for GlyphOrientationVertical { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + match self { + GlyphOrientationVertical::Auto => write!(f, "Auto"), + GlyphOrientationVertical::None => write!(f, "0"), + GlyphOrientationVertical::Clockwise90 => write!(f, "90"), + GlyphOrientationVertical::CounterClockwise90 => write!(f, "-90"), + GlyphOrientationVertical::Clockwise180 => write!(f, "180"), + GlyphOrientationVertical::CounterClockwise180 => write!(f, "-180"), + GlyphOrientationVertical::Clockwise270 => write!(f, "270"), + } + } +} + +impl ValueOutput for ColumnDimensions { + fn is_multiline(&self) -> bool { + matches!(self, Self::Specific(_)) + } +} +impl Output for ColumnDimensions { + fn output_indent(&self, f: &mut impl std::fmt::Write, indent: Indent) -> std::fmt::Result { + match self { + ColumnDimensions::All(all) => all.output(f), + ColumnDimensions::Specific(list) => { + let Some((last, remainder)) = list.split_last() else { + return Ok(()); + }; + writeln!(f)?; + for dim in remainder.iter() { + writeln!(f, "{indent}- {}", dim.display())?; + } + write!(f, "{indent}- {}", last.display()) + } + } + } +} + +impl ValueOutput for f32 {} +impl Output for f32 { + fn output_indent(&self, f: &mut impl std::fmt::Write, _: Indent) -> std::fmt::Result { + write!(f, "{self:7.3}") + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU16; + + use pretty_assertions::assert_eq; + + use crate::action::{Action, LinkAction}; + use crate::annotation::{LinkAnnotation, Target}; + use crate::geom::Rect; + use crate::tagging::fmt::{Indent, Output}; + use crate::tagging::{ + BBox, ColumnDimensions, ContentTag, LineHeight, NaiveRgbColor, Sides, Tag, TagGroup, + TagTree, + }; + use crate::Document; + + #[test] + fn display_empty_tag_tree() { + assert_eq!("", TagTree::new().display().to_string()); + } + + #[test] + fn display_tag_tree() { + let mut document = Document::new(); + let mut page = document.start_page(); + let mut tree = TagTree::new(); + + let sec = Tag::Section + .with_lang(Some("de".into())) + .with_column_widths(Some(ColumnDimensions::Specific(vec![17.0, 23.0, 34.0]))) + .with_column_gap(Some(ColumnDimensions::Specific(vec![3.0, 4.0]))); + let mut sec = TagGroup::new(sec); + + let figure_rect = Rect::from_ltrb(12.1, 12.342, 24.789877, 32.0).unwrap(); + let figure = Tag::Figure(Some("figure alt text".into())) + .with_actual_text(Some("THE ACTUAL TEXT".into())) + .with_bbox(Some(BBox::new(0, figure_rect))) + .with_line_height(Some(LineHeight::Normal)); + let mut figure = TagGroup::new(figure); + + let link_rect = Rect::from_ltrb(12.0, 12.0, 24.0, 32.32).unwrap(); + let link_target = + Target::Action(Action::Link(LinkAction::new("https://github.com".into()))); + let link_id = + page.add_tagged_annotation(LinkAnnotation::new(link_rect, link_target).into()); + figure.push(link_id); + sec.push(figure); + + let border_color = Sides::new( + NaiveRgbColor::new(0x1A, 0x66, 0xFF), + NaiveRgbColor::new(0x4D, 0x80, 0x33), + NaiveRgbColor::new(0x4D, 0x66, 0x4D), + NaiveRgbColor::new(0x00, 0xB3, 0x33), + ); + let table = Tag::Table + .with_border_color(Some(border_color)) + .with_line_height(Some(LineHeight::Custom(23.0))); + let table = TagGroup::new(table); + sec.push(table); + + tree.push(sec); + + let yaml = tree.display().to_string(); + let expected = "\ +- Tag: Section + /Lang: \"de\" + /ColumnGap: + - 3.000 + - 4.000 + /ColumnWidths: + - 17.000 + - 23.000 + - 34.000 + /K: + - Tag: Figure + /Alt: \"figure alt text\" + /ActualText: \"THE ACTUAL TEXT\" + /BBox: + page: 0 + left: 12.100 + top: 12.342 + right: 24.790 + bottom: 32.000 + /LineHeight: Normal + /K: + - Annotation: page=0 index=0 + - Tag: Table + /BorderColor: + before: #1a66ff + after: #4d8033 + start: #4d664d + end: #00b333 + /LineHeight: 23.000 +"; + assert_eq!(expected, yaml) + } + + #[test] + fn display_sides_mutliline() { + let sides = Sides::new( + ColumnDimensions::specific(vec![1.0, 5.0]), + ColumnDimensions::specific(vec![2.0, 6.0]), + ColumnDimensions::specific(vec![3.0, 7.0]), + ColumnDimensions::specific(vec![4.0, 8.0]), + ); + let yaml = format!("val:{}\n", sides.display_indent(Indent(1))); + let expected = "\ +val: + before: + - 1.000 + - 5.000 + after: + - 2.000 + - 6.000 + start: + - 3.000 + - 7.000 + end: + - 4.000 + - 8.000 +"; + assert_eq!(expected, yaml); + } + + #[test] + fn display_heading_with_children() { + let mut document = Document::new(); + let mut page = document.start_page(); + let mut tree = TagTree::new(); + + let heading = Tag::Hn(NonZeroU16::new(1).unwrap(), None); + let mut heading = TagGroup::new(heading); + + let mut surface = page.surface(); + + let id1 = surface.start_tagged(ContentTag::Other); + surface.end_tagged(); + heading.push(id1); + + tree.push(heading); + + let yaml = tree.display().to_string(); + let expected = "\ +- Tag: H1 + /K: + - Content: page=0 mcid=0 +"; + assert_eq!(expected, yaml); + } +} diff --git a/vendor/krilla/src/interchange/tagging/generate.toml b/vendor/krilla/src/interchange/tagging/generate.toml new file mode 100644 index 00000000..8e202ecc --- /dev/null +++ b/vendor/krilla/src/interchange/tagging/generate.toml @@ -0,0 +1,474 @@ +# A tag for group nodes. +[Tag] +# A part of a document that may contain multiple articles or sections. +[Tag.Part] +# An article with largely self-contained content. +[Tag.Article] +optional = [ + "Layout::ColumnCount", + "Layout::ColumnGap", + "Layout::ColumnWidths", +] +# Section of a larger document. +[Tag.Section] +optional = [ + "Layout::ColumnCount", + "Layout::ColumnGap", + "Layout::ColumnWidths", +] +# A generic block-level grouping element. +[Tag.Div] +optional = [ + "Layout::ColumnCount", + "Layout::ColumnGap", + "Layout::ColumnWidths", +] +# A paragraph-level quote. +[Tag.BlockQuote] +# An image or figure caption. +# +# **Best Practice**: In the tag tree, this should appear +# as a sibling after the image (or other) content it describes. +[Tag.Caption] +# Table of contents. +# +# **Best Practice**: Should consist of TOCIs or other nested TOCs. +[Tag.TOC] +# Item in the table of contents. +# +# **Best Practice**: Should only appear within a TOC. Should only consist of +# labels, references, paragraphs and TOCs. +[Tag.TOCI] +# Index of the key terms in the document. +# +# **Best Practice**: Should contain a sequence of text accompanied by +# reference elements pointing to their occurrence in the text. +[Tag.Index] +# A paragraph. +[Tag.P] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::TextIndent", + "Layout::TextAlign", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# Heading level `n`, including an optional title of the heading. +# +# The title is required for some export modes, like for example PDF/UA. +[Tag.Hn] +required = ["Struct::HeadingLevel"] +suggested = ["Struct::Title"] +optional = [ + "Struct::Title", + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::TextIndent", + "Layout::TextAlign", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# A list. +# +# **Best practice**: Should consist of an optional caption followed by +# list items. +# List numbering is only required for PDF/UA, but we just enforce it for always. +[Tag.L] +required = ["List::Numbering"] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::TextIndent", + "Layout::TextAlign", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# A list item. +# +# **Best practice**: Should consist of one or more list labels and/or list bodies. +[Tag.LI] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::TextIndent", + "Layout::TextAlign", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# Label for a list item. +[Tag.Lbl] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::TextIndent", + "Layout::TextAlign", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# Description of the list item. +[Tag.LBody] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::TextIndent", + "Layout::TextAlign", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# A table, with an optional summary describing the purpose and structure. +# +# **Best practice**: Should consist of an optional table header row, +# one or more table body elements and an optional table footer. Can have +# caption as the first or last child. +[Tag.Table] +optional = [ + "Table::Summary", + "Layout::BBox", + "Layout::Width", + "Layout::Height", + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::TextIndent", + "Layout::TextAlign", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# A table row. +# +# **Best practice**: May contain table headers cells and table data cells. +[Tag.TR] +# A table header cell. +[Tag.TH] +required = ["Table::HeaderScope"] +optional = [ + "Table::CellHeaders", + "Table::RowSpan", + "Table::ColSpan", + "Layout::Width", + "Layout::Height", + "Layout::TableBorderStyle", + "Layout::TablePadding", + "Layout::BlockAlign", + "Layout::InlineAlign", +] +# A table data cell. +[Tag.TD] +optional = [ + "Table::CellHeaders", + "Table::RowSpan", + "Table::ColSpan", + "Layout::Width", + "Layout::Height", + "Layout::TableBorderStyle", + "Layout::TablePadding", + "Layout::BlockAlign", + "Layout::InlineAlign", +] +# A table header row group. +[Tag.THead] +# A table data row group. +[Tag.TBody] +# A table footer row group. +[Tag.TFoot] +# An inline-level element that does not have a specific meaning. +[Tag.Span] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# An inline quotation. +[Tag.InlineQuote] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# A foot- or endnote, potentially referred to from within the text. +# +# **Best practice**: It may have a label as a child. +[Tag.Note] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# A reference to elsewhere in the document. +# +# **Best practice**: The first child of a tag group with this tag should be a link annotation +# linking to a destination in the document, and the second child should consist of +# the children that should be associated with that reference. +[Tag.Reference] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# A reference to the external source of some cited document. +# +# **Best practice**: It may have a label as a child. +[Tag.BibEntry] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# Computer code. +[Tag.Code] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# A link. +# +# **Best practice**: The first child of a tag group with this tag should be a link annotation +# linking to an URL, and the second child should consist of the children that should +# be associated with that link. +[Tag.Link] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# An association between an annotation and the content it belongs to. PDF +# +# **Best practice**: Should be used for all annotations, except for link annotations and +# widget annotations. The first child should be the identifier of a non-link annotation, +# and all other subsequent children should be content identifiers associated with that +# annotation. +[Tag.Annot] +optional = [ + "Layout::SpaceBefore", + "Layout::SpaceAfter", + "Layout::StartIndent", + "Layout::EndIndent", + "Layout::LineHeight", + "Layout::BaselineShift", + "Layout::TextDecorationColor", + "Layout::TextDecorationThickness", + "Layout::TextDecorationType", +] +# Item of graphical content. +# +# Providing `alt_text` is required in some export modes, like for example PDF/UA-1. +[Tag.Figure] +suggested = ["Struct::AltText"] +optional = [ + "Layout::BBox", + "Layout::Width", + "Layout::Height", +] +# A mathematical formula. +# +# Providing `alt_text` is required in some export modes, like for example PDF/UA-1. +[Tag.Formula] +suggested = ["Struct::AltText"] +optional = [ + "Layout::BBox", + "Layout::Width", + "Layout::Height", +] +# An interactive form field. +[Tag.Form] +# Non-structural element. A grouping element having no inherent structural significance; +# it serves solely for grouping purposes. +[Tag.NonStruct] + +# Non-standard role-mapped tag kinds: + +# A date or time. +[Tag.Datetime] +# A list of terms. +[Tag.Terms] +# A title. +[Tag.Title] +# Encloses content with strong importance, most commonly **bold** text. +[Tag.Strong] +# Encloses content that is emphasized, most commonly *italic* text. +[Tag.Em] + +[Attr.Struct] +# The tag id. +Id = { type = "TagId", global = true } +# The language of this tag. +Lang = { type = "String", global = true, accessor = "AsRef" } +# The optional alternate text that describes the text (for example, if the text +# consists of a star symbol, the alt text should describe that in natural language). +AltText = { type = "String", global = true, accessor = "AsRef" } +# The expanded form of an abbreviation. +# Only applicable if the content of the tag is an abbreviation. +Expanded = { type = "String", global = true, accessor = "AsRef" } +# The actual text represented by the content of this tag, i.e. if it contained +# some curves that artistically represent some word. This should be the exact +# replacement text of the word. +ActualText = { type = "String", global = true, accessor = "AsRef" } +# The title, characterizing a specific tag such as `"Chapter 1"`. +Title = { type = "String", accessor = "AsRef" } + +# The heading level +HeadingLevel = { name = "level", type = "NonZeroU16", accessor = "Copy" } + +[Attr.List] +# The list numbering. +Numbering = { type = "ListNumbering", accessor = "Copy" } + +[Attr.Table] +# The table summary. +Summary = { type = "String", accessor = "AsRef" } +# The table header scope. +HeaderScope = { name = "scope", type = "TableHeaderScope", accessor = "Copy" } +# The list of headers associated with a table cell. +# Table data cells (`TD`) may specify a list of table headers (`TH`), +# which can also specify a list of parent header cells (`TH`), and so on. +# To determine the list of associated headers this list is recursively +# evaluated. +# +# This allows specifying header hierarchies inside tables. +CellHeaders = { name = "headers", type = "SmallVec<[TagId; 1]>", accessor = "Custom" } +# The row span of this table cell. +RowSpan = { type = "NonZeroU32", accessor = "Copy" } +# The column span of this table cell. +ColSpan = { type = "NonZeroU32", accessor = "Copy" } + +[Attr.Layout] +# The placement. +Placement = { type = "Placement", global = true, accessor = "Copy" } +# The writing mode. +WritingMode = { type = "WritingMode", global = true, accessor = "Copy" } +# The bounding box of a tag that encloses its visible content. +# If the content spans multiple pages, this should be omitted. +BBox = { name = "bbox", type = "BBox", accessor = "Copy" } +# The width. +Width = { type = "f32", accessor = "Copy" } +# The height. +Height = { type = "f32", accessor = "Copy" } +# The background color. +BackgroundColor = { type = "NaiveRgbColor", global = true, accessor = "Copy" } +# The border color. +BorderColor = { type = "Sides", global = true, accessor = "Copy" } +# The way the border is drawn. +BorderStyle = { type = "Sides", global = true, accessor = "Copy" } +# The border width. +BorderThickness = { type = "Sides", global = true, accessor = "Copy" } +# The padding inside of an element. +Padding = { type = "Sides", global = true, accessor = "Copy" } +# The color of text, borders, and text decorations. +Color = { type = "NaiveRgbColor", global = true, accessor = "Copy" } +# The spacing before the block-level element. +SpaceBefore = { type = "f32", accessor = "Copy" } +# The spacing after the block-level element. +SpaceAfter = { type = "f32", accessor = "Copy" } +# The spacing between the start inline edge of the element and the parent. +StartIndent = { type = "f32", accessor = "Copy" } +# The spacing between the end inline edge of the element and the parent. +EndIndent = { type = "f32", accessor = "Copy" } +# The amount the first line of text in a block-level element is indented. Only +# applicable to paragraph-like elements with non-block-level elements. +TextIndent = { type = "f32", accessor = "Copy" } +# The text alignment. +TextAlign = { type = "TextAlign", accessor = "Copy" } +# The alignment of block-level elements inside of this block-level element. +BlockAlign = { type = "BlockAlign", accessor = "Copy" } +# The alignment of inline-level elements inside of this block-level element. +InlineAlign = { type = "InlineAlign", accessor = "Copy" } +# The border style of table cells, overriding `BorderStyle`. +TableBorderStyle = { type = "Sides", accessor = "Copy" } +# The padding inside of table cells, overriding `Padding`. +TablePadding = { type = "Sides", accessor = "Copy" } +# The distance by which the baseline shall be shifted from the default position. +BaselineShift = { type = "f32", global = true, accessor = "Copy" } +# The height of each line in an element on the block axis. +LineHeight = { type = "LineHeight", global = true, accessor = "Copy" } +# The color of the text decoration, overriding the fill color. +TextDecorationColor = { type = "NaiveRgbColor", global = true, accessor = "Copy" } +# The width of the text decoration line. +TextDecorationThickness = { type = "f32", global = true, accessor = "Copy" } +# The kind of text decoration. +TextDecorationType = { type = "TextDecorationType", global = true, accessor = "Copy" } +# How the glyphs are rotated in a vertical writing mode. +GlyphOrientationVertical = { type = "GlyphOrientationVertical", global = true, accessor = "Copy" } +# The number of columns in the grouping element. +ColumnCount = { type = "NonZeroU32", accessor = "Copy" } +# The width of the gaps between columns in the grouping element. +ColumnGap = { type = "ColumnDimensions" } +# The width of the columns in the grouping element. +ColumnWidths = { type = "ColumnDimensions" } diff --git a/vendor/krilla/src/interchange/tagging/generated.rs b/vendor/krilla/src/interchange/tagging/generated.rs new file mode 100644 index 00000000..c4c23717 --- /dev/null +++ b/vendor/krilla/src/interchange/tagging/generated.rs @@ -0,0 +1,4754 @@ +// This file is automatically generated! +// +// To update it: +// 1. Edit the `generate.toml` file inside of this directory +// 2. Run `cargo run --bin=codegen` from the repository root + +/// A tag for group nodes. +#[derive(Clone, Debug, PartialEq)] +pub enum TagKind { + /// A part of a document that may contain multiple articles or sections. + Part(Tag), + /// An article with largely self-contained content. + Article(Tag), + /// Section of a larger document. + Section(Tag), + /// A generic block-level grouping element. + Div(Tag), + /// A paragraph-level quote. + BlockQuote(Tag), + /// An image or figure caption. + /// + /// **Best Practice**: In the tag tree, this should appear + /// as a sibling after the image (or other) content it describes. + Caption(Tag), + /// Table of contents. + /// + /// **Best Practice**: Should consist of TOCIs or other nested TOCs. + TOC(Tag), + /// Item in the table of contents. + /// + /// **Best Practice**: Should only appear within a TOC. Should only consist of + /// labels, references, paragraphs and TOCs. + TOCI(Tag), + /// Index of the key terms in the document. + /// + /// **Best Practice**: Should contain a sequence of text accompanied by + /// reference elements pointing to their occurrence in the text. + Index(Tag), + /// A paragraph. + P(Tag), + /// Heading level `n`, including an optional title of the heading. + /// + /// The title is required for some export modes, like for example PDF/UA. + Hn(Tag), + /// A list. + /// + /// **Best practice**: Should consist of an optional caption followed by + /// list items. + /// List numbering is only required for PDF/UA, but we just enforce it for always. + L(Tag), + /// A list item. + /// + /// **Best practice**: Should consist of one or more list labels and/or list bodies. + LI(Tag), + /// Label for a list item. + Lbl(Tag), + /// Description of the list item. + LBody(Tag), + /// A table, with an optional summary describing the purpose and structure. + /// + /// **Best practice**: Should consist of an optional table header row, + /// one or more table body elements and an optional table footer. Can have + /// caption as the first or last child. + Table(Tag), + /// A table row. + /// + /// **Best practice**: May contain table headers cells and table data cells. + TR(Tag), + /// A table header cell. + TH(Tag), + /// A table data cell. + TD(Tag), + /// A table header row group. + THead(Tag), + /// A table data row group. + TBody(Tag), + /// A table footer row group. + TFoot(Tag), + /// An inline-level element that does not have a specific meaning. + Span(Tag), + /// An inline quotation. + InlineQuote(Tag), + /// A foot- or endnote, potentially referred to from within the text. + /// + /// **Best practice**: It may have a label as a child. + Note(Tag), + /// A reference to elsewhere in the document. + /// + /// **Best practice**: The first child of a tag group with this tag should be a link annotation + /// linking to a destination in the document, and the second child should consist of + /// the children that should be associated with that reference. + Reference(Tag), + /// A reference to the external source of some cited document. + /// + /// **Best practice**: It may have a label as a child. + BibEntry(Tag), + /// Computer code. + Code(Tag), + /// A link. + /// + /// **Best practice**: The first child of a tag group with this tag should be a link annotation + /// linking to an URL, and the second child should consist of the children that should + /// be associated with that link. + Link(Tag), + /// An association between an annotation and the content it belongs to. PDF + /// + /// **Best practice**: Should be used for all annotations, except for link annotations and + /// widget annotations. The first child should be the identifier of a non-link annotation, + /// and all other subsequent children should be content identifiers associated with that + /// annotation. + Annot(Tag), + /// Item of graphical content. + /// + /// Providing `alt_text` is required in some export modes, like for example PDF/UA-1. + Figure(Tag), + /// A mathematical formula. + /// + /// Providing `alt_text` is required in some export modes, like for example PDF/UA-1. + Formula(Tag), + /// An interactive form field. + Form(Tag), + /// Non-structural element. A grouping element having no inherent structural significance; + /// it serves solely for grouping purposes. + NonStruct(Tag), + /// A date or time. + Datetime(Tag), + /// A list of terms. + Terms(Tag), + /// A title. + Title(Tag), + /// Encloses content with strong importance, most commonly **bold** text. + Strong(Tag), + /// Encloses content that is emphasized, most commonly *italic* text. + Em(Tag), +} + +impl TagKind { + /// A type erased tag, which allows reading all attributes. + pub fn as_any(&self) -> &AnyTag { + match self { + Self::Part(tag) => tag.as_any(), + Self::Article(tag) => tag.as_any(), + Self::Section(tag) => tag.as_any(), + Self::Div(tag) => tag.as_any(), + Self::BlockQuote(tag) => tag.as_any(), + Self::Caption(tag) => tag.as_any(), + Self::TOC(tag) => tag.as_any(), + Self::TOCI(tag) => tag.as_any(), + Self::Index(tag) => tag.as_any(), + Self::P(tag) => tag.as_any(), + Self::Hn(tag) => tag.as_any(), + Self::L(tag) => tag.as_any(), + Self::LI(tag) => tag.as_any(), + Self::Lbl(tag) => tag.as_any(), + Self::LBody(tag) => tag.as_any(), + Self::Table(tag) => tag.as_any(), + Self::TR(tag) => tag.as_any(), + Self::TH(tag) => tag.as_any(), + Self::TD(tag) => tag.as_any(), + Self::THead(tag) => tag.as_any(), + Self::TBody(tag) => tag.as_any(), + Self::TFoot(tag) => tag.as_any(), + Self::Span(tag) => tag.as_any(), + Self::InlineQuote(tag) => tag.as_any(), + Self::Note(tag) => tag.as_any(), + Self::Reference(tag) => tag.as_any(), + Self::BibEntry(tag) => tag.as_any(), + Self::Code(tag) => tag.as_any(), + Self::Link(tag) => tag.as_any(), + Self::Annot(tag) => tag.as_any(), + Self::Figure(tag) => tag.as_any(), + Self::Formula(tag) => tag.as_any(), + Self::Form(tag) => tag.as_any(), + Self::NonStruct(tag) => tag.as_any(), + Self::Datetime(tag) => tag.as_any(), + Self::Terms(tag) => tag.as_any(), + Self::Title(tag) => tag.as_any(), + Self::Strong(tag) => tag.as_any(), + Self::Em(tag) => tag.as_any(), + } + } + + /// A type erased tag, which allows reading all attributes and additionally + /// writing all global attributes. + pub fn as_any_mut(&mut self) -> &mut AnyTag { + match self { + Self::Part(tag) => tag.as_any_mut(), + Self::Article(tag) => tag.as_any_mut(), + Self::Section(tag) => tag.as_any_mut(), + Self::Div(tag) => tag.as_any_mut(), + Self::BlockQuote(tag) => tag.as_any_mut(), + Self::Caption(tag) => tag.as_any_mut(), + Self::TOC(tag) => tag.as_any_mut(), + Self::TOCI(tag) => tag.as_any_mut(), + Self::Index(tag) => tag.as_any_mut(), + Self::P(tag) => tag.as_any_mut(), + Self::Hn(tag) => tag.as_any_mut(), + Self::L(tag) => tag.as_any_mut(), + Self::LI(tag) => tag.as_any_mut(), + Self::Lbl(tag) => tag.as_any_mut(), + Self::LBody(tag) => tag.as_any_mut(), + Self::Table(tag) => tag.as_any_mut(), + Self::TR(tag) => tag.as_any_mut(), + Self::TH(tag) => tag.as_any_mut(), + Self::TD(tag) => tag.as_any_mut(), + Self::THead(tag) => tag.as_any_mut(), + Self::TBody(tag) => tag.as_any_mut(), + Self::TFoot(tag) => tag.as_any_mut(), + Self::Span(tag) => tag.as_any_mut(), + Self::InlineQuote(tag) => tag.as_any_mut(), + Self::Note(tag) => tag.as_any_mut(), + Self::Reference(tag) => tag.as_any_mut(), + Self::BibEntry(tag) => tag.as_any_mut(), + Self::Code(tag) => tag.as_any_mut(), + Self::Link(tag) => tag.as_any_mut(), + Self::Annot(tag) => tag.as_any_mut(), + Self::Figure(tag) => tag.as_any_mut(), + Self::Formula(tag) => tag.as_any_mut(), + Self::Form(tag) => tag.as_any_mut(), + Self::NonStruct(tag) => tag.as_any_mut(), + Self::Datetime(tag) => tag.as_any_mut(), + Self::Terms(tag) => tag.as_any_mut(), + Self::Title(tag) => tag.as_any_mut(), + Self::Strong(tag) => tag.as_any_mut(), + Self::Em(tag) => tag.as_any_mut(), + } + } + + /// The tag id. + pub fn id(&self) -> Option<&TagId> { + self.as_any().id() + } + + /// Set the tag id. + pub fn set_id(&mut self, id: Option) { + self.as_any_mut().set_id(id); + } + + /// Set the tag id. + pub fn with_id(mut self, id: Option) -> Self { + self.set_id(id); + self + } + + /// The language of this tag. + pub fn lang(&self) -> Option<&str> { + self.as_any().lang() + } + + /// Set the language of this tag. + pub fn set_lang(&mut self, lang: Option) { + self.as_any_mut().set_lang(lang); + } + + /// Set the language of this tag. + pub fn with_lang(mut self, lang: Option) -> Self { + self.set_lang(lang); + self + } + + /// The optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn alt_text(&self) -> Option<&str> { + self.as_any().alt_text() + } + + /// Set the optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn set_alt_text(&mut self, alt_text: Option) { + self.as_any_mut().set_alt_text(alt_text); + } + + /// Set the optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn with_alt_text(mut self, alt_text: Option) -> Self { + self.set_alt_text(alt_text); + self + } + + /// The expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn expanded(&self) -> Option<&str> { + self.as_any().expanded() + } + + /// Set the expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn set_expanded(&mut self, expanded: Option) { + self.as_any_mut().set_expanded(expanded); + } + + /// Set the expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn with_expanded(mut self, expanded: Option) -> Self { + self.set_expanded(expanded); + self + } + + /// The actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn actual_text(&self) -> Option<&str> { + self.as_any().actual_text() + } + + /// Set the actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn set_actual_text(&mut self, actual_text: Option) { + self.as_any_mut().set_actual_text(actual_text); + } + + /// Set the actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn with_actual_text(mut self, actual_text: Option) -> Self { + self.set_actual_text(actual_text); + self + } + + /// The title, characterizing a specific tag such as `"Chapter 1"`. + pub fn title(&self) -> Option<&str> { + self.as_any().title() + } + + /// The heading level + pub fn level(&self) -> Option { + self.as_any().level() + } + + /// The list numbering. + pub fn numbering(&self) -> Option { + self.as_any().numbering() + } + + /// The table summary. + pub fn summary(&self) -> Option<&str> { + self.as_any().summary() + } + + /// The table header scope. + pub fn scope(&self) -> Option { + self.as_any().scope() + } + + /// The list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + pub fn headers(&self) -> Option<&[TagId]> { + self.as_any().headers() + } + + /// The row span of this table cell. + pub fn row_span(&self) -> Option { + self.as_any().row_span() + } + + /// The column span of this table cell. + pub fn col_span(&self) -> Option { + self.as_any().col_span() + } + + /// The placement. + pub fn placement(&self) -> Option { + self.as_any().placement() + } + + /// Set the placement. + pub fn set_placement(&mut self, placement: Option) { + self.as_any_mut().set_placement(placement); + } + + /// Set the placement. + pub fn with_placement(mut self, placement: Option) -> Self { + self.set_placement(placement); + self + } + + /// The writing mode. + pub fn writing_mode(&self) -> Option { + self.as_any().writing_mode() + } + + /// Set the writing mode. + pub fn set_writing_mode(&mut self, writing_mode: Option) { + self.as_any_mut().set_writing_mode(writing_mode); + } + + /// Set the writing mode. + pub fn with_writing_mode(mut self, writing_mode: Option) -> Self { + self.set_writing_mode(writing_mode); + self + } + + /// The bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn bbox(&self) -> Option { + self.as_any().bbox() + } + + /// The width. + pub fn width(&self) -> Option { + self.as_any().width() + } + + /// The height. + pub fn height(&self) -> Option { + self.as_any().height() + } + + /// The background color. + pub fn background_color(&self) -> Option { + self.as_any().background_color() + } + + /// Set the background color. + pub fn set_background_color(&mut self, background_color: Option) { + self.as_any_mut().set_background_color(background_color); + } + + /// Set the background color. + pub fn with_background_color(mut self, background_color: Option) -> Self { + self.set_background_color(background_color); + self + } + + /// The border color. + pub fn border_color(&self) -> Option> { + self.as_any().border_color() + } + + /// Set the border color. + pub fn set_border_color(&mut self, border_color: Option>) { + self.as_any_mut().set_border_color(border_color); + } + + /// Set the border color. + pub fn with_border_color(mut self, border_color: Option>) -> Self { + self.set_border_color(border_color); + self + } + + /// The way the border is drawn. + pub fn border_style(&self) -> Option> { + self.as_any().border_style() + } + + /// Set the way the border is drawn. + pub fn set_border_style(&mut self, border_style: Option>) { + self.as_any_mut().set_border_style(border_style); + } + + /// Set the way the border is drawn. + pub fn with_border_style(mut self, border_style: Option>) -> Self { + self.set_border_style(border_style); + self + } + + /// The border width. + pub fn border_thickness(&self) -> Option> { + self.as_any().border_thickness() + } + + /// Set the border width. + pub fn set_border_thickness(&mut self, border_thickness: Option>) { + self.as_any_mut().set_border_thickness(border_thickness); + } + + /// Set the border width. + pub fn with_border_thickness(mut self, border_thickness: Option>) -> Self { + self.set_border_thickness(border_thickness); + self + } + + /// The padding inside of an element. + pub fn padding(&self) -> Option> { + self.as_any().padding() + } + + /// Set the padding inside of an element. + pub fn set_padding(&mut self, padding: Option>) { + self.as_any_mut().set_padding(padding); + } + + /// Set the padding inside of an element. + pub fn with_padding(mut self, padding: Option>) -> Self { + self.set_padding(padding); + self + } + + /// The color of text, borders, and text decorations. + pub fn color(&self) -> Option { + self.as_any().color() + } + + /// Set the color of text, borders, and text decorations. + pub fn set_color(&mut self, color: Option) { + self.as_any_mut().set_color(color); + } + + /// Set the color of text, borders, and text decorations. + pub fn with_color(mut self, color: Option) -> Self { + self.set_color(color); + self + } + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.as_any().space_before() + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.as_any().space_after() + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.as_any().start_indent() + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.as_any().end_indent() + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.as_any().text_indent() + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.as_any().text_align() + } + + /// The alignment of block-level elements inside of this block-level element. + pub fn block_align(&self) -> Option { + self.as_any().block_align() + } + + /// The alignment of inline-level elements inside of this block-level element. + pub fn inline_align(&self) -> Option { + self.as_any().inline_align() + } + + /// The border style of table cells, overriding `BorderStyle`. + pub fn table_border_style(&self) -> Option> { + self.as_any().table_border_style() + } + + /// The padding inside of table cells, overriding `Padding`. + pub fn table_padding(&self) -> Option> { + self.as_any().table_padding() + } + + /// The distance by which the baseline shall be shifted from the default position. + pub fn baseline_shift(&self) -> Option { + self.as_any().baseline_shift() + } + + /// Set the distance by which the baseline shall be shifted from the default position. + pub fn set_baseline_shift(&mut self, baseline_shift: Option) { + self.as_any_mut().set_baseline_shift(baseline_shift); + } + + /// Set the distance by which the baseline shall be shifted from the default position. + pub fn with_baseline_shift(mut self, baseline_shift: Option) -> Self { + self.set_baseline_shift(baseline_shift); + self + } + + /// The height of each line in an element on the block axis. + pub fn line_height(&self) -> Option { + self.as_any().line_height() + } + + /// Set the height of each line in an element on the block axis. + pub fn set_line_height(&mut self, line_height: Option) { + self.as_any_mut().set_line_height(line_height); + } + + /// Set the height of each line in an element on the block axis. + pub fn with_line_height(mut self, line_height: Option) -> Self { + self.set_line_height(line_height); + self + } + + /// The color of the text decoration, overriding the fill color. + pub fn text_decoration_color(&self) -> Option { + self.as_any().text_decoration_color() + } + + /// Set the color of the text decoration, overriding the fill color. + pub fn set_text_decoration_color(&mut self, text_decoration_color: Option) { + self.as_any_mut().set_text_decoration_color(text_decoration_color); + } + + /// Set the color of the text decoration, overriding the fill color. + pub fn with_text_decoration_color(mut self, text_decoration_color: Option) -> Self { + self.set_text_decoration_color(text_decoration_color); + self + } + + /// The width of the text decoration line. + pub fn text_decoration_thickness(&self) -> Option { + self.as_any().text_decoration_thickness() + } + + /// Set the width of the text decoration line. + pub fn set_text_decoration_thickness(&mut self, text_decoration_thickness: Option) { + self.as_any_mut().set_text_decoration_thickness(text_decoration_thickness); + } + + /// Set the width of the text decoration line. + pub fn with_text_decoration_thickness(mut self, text_decoration_thickness: Option) -> Self { + self.set_text_decoration_thickness(text_decoration_thickness); + self + } + + /// The kind of text decoration. + pub fn text_decoration_type(&self) -> Option { + self.as_any().text_decoration_type() + } + + /// Set the kind of text decoration. + pub fn set_text_decoration_type(&mut self, text_decoration_type: Option) { + self.as_any_mut().set_text_decoration_type(text_decoration_type); + } + + /// Set the kind of text decoration. + pub fn with_text_decoration_type(mut self, text_decoration_type: Option) -> Self { + self.set_text_decoration_type(text_decoration_type); + self + } + + /// How the glyphs are rotated in a vertical writing mode. + pub fn glyph_orientation_vertical(&self) -> Option { + self.as_any().glyph_orientation_vertical() + } + + /// Set how the glyphs are rotated in a vertical writing mode. + pub fn set_glyph_orientation_vertical(&mut self, glyph_orientation_vertical: Option) { + self.as_any_mut().set_glyph_orientation_vertical(glyph_orientation_vertical); + } + + /// Set how the glyphs are rotated in a vertical writing mode. + pub fn with_glyph_orientation_vertical(mut self, glyph_orientation_vertical: Option) -> Self { + self.set_glyph_orientation_vertical(glyph_orientation_vertical); + self + } + + /// The number of columns in the grouping element. + pub fn column_count(&self) -> Option { + self.as_any().column_count() + } + + /// The width of the gaps between columns in the grouping element. + pub fn column_gap(&self) -> Option<&ColumnDimensions> { + self.as_any().column_gap() + } + + /// The width of the columns in the grouping element. + pub fn column_widths(&self) -> Option<&ColumnDimensions> { + self.as_any().column_widths() + } +} + +// Read accessors for all attributes and write accessors for global ones. +impl AnyTag { + #[inline(always)] + fn get_struct(&self, ordinal: usize) -> Option<&StructAttr> { + self.attrs.get(ordinal).map(Attr::unwrap_struct) + } + + #[allow(unused)] + #[inline(always)] + fn set_struct(&mut self, attr: StructAttr) { + self.attrs.set(Attr::Struct(attr)); + } + + #[allow(unused)] + #[inline(always)] + fn set_or_remove_struct(&mut self, ordinal: usize, attr: Option) { + self.attrs.set_or_remove(ordinal, attr.map(Attr::Struct)); + } + + #[inline(always)] + fn get_list(&self, ordinal: usize) -> Option<&ListAttr> { + self.attrs.get(ordinal).map(Attr::unwrap_list) + } + + #[allow(unused)] + #[inline(always)] + fn set_list(&mut self, attr: ListAttr) { + self.attrs.set(Attr::List(attr)); + } + + #[allow(unused)] + #[inline(always)] + fn set_or_remove_list(&mut self, ordinal: usize, attr: Option) { + self.attrs.set_or_remove(ordinal, attr.map(Attr::List)); + } + + #[inline(always)] + fn get_table(&self, ordinal: usize) -> Option<&TableAttr> { + self.attrs.get(ordinal).map(Attr::unwrap_table) + } + + #[allow(unused)] + #[inline(always)] + fn set_table(&mut self, attr: TableAttr) { + self.attrs.set(Attr::Table(attr)); + } + + #[allow(unused)] + #[inline(always)] + fn set_or_remove_table(&mut self, ordinal: usize, attr: Option) { + self.attrs.set_or_remove(ordinal, attr.map(Attr::Table)); + } + + #[inline(always)] + fn get_layout(&self, ordinal: usize) -> Option<&LayoutAttr> { + self.attrs.get(ordinal).map(Attr::unwrap_layout) + } + + #[allow(unused)] + #[inline(always)] + fn set_layout(&mut self, attr: LayoutAttr) { + self.attrs.set(Attr::Layout(attr)); + } + + #[allow(unused)] + #[inline(always)] + fn set_or_remove_layout(&mut self, ordinal: usize, attr: Option) { + self.attrs.set_or_remove(ordinal, attr.map(Attr::Layout)); + } + + + /// The tag id. + pub fn id(&self) -> Option<&TagId> { + self.get_struct(StructAttr::ID).map(StructAttr::unwrap_id) + } + + /// Set the tag id. + pub fn set_id(&mut self, id: Option) { + self.set_or_remove_struct(StructAttr::ID, id.map(StructAttr::Id)); + } + + /// Set the tag id. + pub fn with_id(mut self, id: Option) -> Self { + self.set_id(id); + self + } + + /// The language of this tag. + pub fn lang(&self) -> Option<&str> { + self.get_struct(StructAttr::LANG).map(StructAttr::unwrap_lang) + } + + /// Set the language of this tag. + pub fn set_lang(&mut self, lang: Option) { + self.set_or_remove_struct(StructAttr::LANG, lang.map(StructAttr::Lang)); + } + + /// Set the language of this tag. + pub fn with_lang(mut self, lang: Option) -> Self { + self.set_lang(lang); + self + } + + /// The optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn alt_text(&self) -> Option<&str> { + self.get_struct(StructAttr::ALT_TEXT).map(StructAttr::unwrap_alt_text) + } + + /// Set the optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn set_alt_text(&mut self, alt_text: Option) { + self.set_or_remove_struct(StructAttr::ALT_TEXT, alt_text.map(StructAttr::AltText)); + } + + /// Set the optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn with_alt_text(mut self, alt_text: Option) -> Self { + self.set_alt_text(alt_text); + self + } + + /// The expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn expanded(&self) -> Option<&str> { + self.get_struct(StructAttr::EXPANDED).map(StructAttr::unwrap_expanded) + } + + /// Set the expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn set_expanded(&mut self, expanded: Option) { + self.set_or_remove_struct(StructAttr::EXPANDED, expanded.map(StructAttr::Expanded)); + } + + /// Set the expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn with_expanded(mut self, expanded: Option) -> Self { + self.set_expanded(expanded); + self + } + + /// The actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn actual_text(&self) -> Option<&str> { + self.get_struct(StructAttr::ACTUAL_TEXT).map(StructAttr::unwrap_actual_text) + } + + /// Set the actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn set_actual_text(&mut self, actual_text: Option) { + self.set_or_remove_struct(StructAttr::ACTUAL_TEXT, actual_text.map(StructAttr::ActualText)); + } + + /// Set the actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn with_actual_text(mut self, actual_text: Option) -> Self { + self.set_actual_text(actual_text); + self + } + + /// The title, characterizing a specific tag such as `"Chapter 1"`. + pub fn title(&self) -> Option<&str> { + self.get_struct(StructAttr::TITLE).map(StructAttr::unwrap_title) + } + + /// The heading level + pub fn level(&self) -> Option { + self.get_struct(StructAttr::HEADING_LEVEL).map(StructAttr::unwrap_level) + } + + /// The list numbering. + pub fn numbering(&self) -> Option { + self.get_list(ListAttr::NUMBERING).map(ListAttr::unwrap_numbering) + } + + /// The table summary. + pub fn summary(&self) -> Option<&str> { + self.get_table(TableAttr::SUMMARY).map(TableAttr::unwrap_summary) + } + + /// The table header scope. + pub fn scope(&self) -> Option { + self.get_table(TableAttr::HEADER_SCOPE).map(TableAttr::unwrap_scope) + } + + /// The list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + pub fn headers(&self) -> Option<&[TagId]> { + self.get_table(TableAttr::CELL_HEADERS).map(TableAttr::unwrap_headers) + } + + /// The row span of this table cell. + pub fn row_span(&self) -> Option { + self.get_table(TableAttr::ROW_SPAN).map(TableAttr::unwrap_row_span) + } + + /// The column span of this table cell. + pub fn col_span(&self) -> Option { + self.get_table(TableAttr::COL_SPAN).map(TableAttr::unwrap_col_span) + } + + /// The placement. + pub fn placement(&self) -> Option { + self.get_layout(LayoutAttr::PLACEMENT).map(LayoutAttr::unwrap_placement) + } + + /// Set the placement. + pub fn set_placement(&mut self, placement: Option) { + self.set_or_remove_layout(LayoutAttr::PLACEMENT, placement.map(LayoutAttr::Placement)); + } + + /// Set the placement. + pub fn with_placement(mut self, placement: Option) -> Self { + self.set_placement(placement); + self + } + + /// The writing mode. + pub fn writing_mode(&self) -> Option { + self.get_layout(LayoutAttr::WRITING_MODE).map(LayoutAttr::unwrap_writing_mode) + } + + /// Set the writing mode. + pub fn set_writing_mode(&mut self, writing_mode: Option) { + self.set_or_remove_layout(LayoutAttr::WRITING_MODE, writing_mode.map(LayoutAttr::WritingMode)); + } + + /// Set the writing mode. + pub fn with_writing_mode(mut self, writing_mode: Option) -> Self { + self.set_writing_mode(writing_mode); + self + } + + /// The bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn bbox(&self) -> Option { + self.get_layout(LayoutAttr::B_BOX).map(LayoutAttr::unwrap_bbox) + } + + /// The width. + pub fn width(&self) -> Option { + self.get_layout(LayoutAttr::WIDTH).map(LayoutAttr::unwrap_width) + } + + /// The height. + pub fn height(&self) -> Option { + self.get_layout(LayoutAttr::HEIGHT).map(LayoutAttr::unwrap_height) + } + + /// The background color. + pub fn background_color(&self) -> Option { + self.get_layout(LayoutAttr::BACKGROUND_COLOR).map(LayoutAttr::unwrap_background_color) + } + + /// Set the background color. + pub fn set_background_color(&mut self, background_color: Option) { + self.set_or_remove_layout(LayoutAttr::BACKGROUND_COLOR, background_color.map(LayoutAttr::BackgroundColor)); + } + + /// Set the background color. + pub fn with_background_color(mut self, background_color: Option) -> Self { + self.set_background_color(background_color); + self + } + + /// The border color. + pub fn border_color(&self) -> Option> { + self.get_layout(LayoutAttr::BORDER_COLOR).map(LayoutAttr::unwrap_border_color) + } + + /// Set the border color. + pub fn set_border_color(&mut self, border_color: Option>) { + self.set_or_remove_layout(LayoutAttr::BORDER_COLOR, border_color.map(LayoutAttr::BorderColor)); + } + + /// Set the border color. + pub fn with_border_color(mut self, border_color: Option>) -> Self { + self.set_border_color(border_color); + self + } + + /// The way the border is drawn. + pub fn border_style(&self) -> Option> { + self.get_layout(LayoutAttr::BORDER_STYLE).map(LayoutAttr::unwrap_border_style) + } + + /// Set the way the border is drawn. + pub fn set_border_style(&mut self, border_style: Option>) { + self.set_or_remove_layout(LayoutAttr::BORDER_STYLE, border_style.map(LayoutAttr::BorderStyle)); + } + + /// Set the way the border is drawn. + pub fn with_border_style(mut self, border_style: Option>) -> Self { + self.set_border_style(border_style); + self + } + + /// The border width. + pub fn border_thickness(&self) -> Option> { + self.get_layout(LayoutAttr::BORDER_THICKNESS).map(LayoutAttr::unwrap_border_thickness) + } + + /// Set the border width. + pub fn set_border_thickness(&mut self, border_thickness: Option>) { + self.set_or_remove_layout(LayoutAttr::BORDER_THICKNESS, border_thickness.map(LayoutAttr::BorderThickness)); + } + + /// Set the border width. + pub fn with_border_thickness(mut self, border_thickness: Option>) -> Self { + self.set_border_thickness(border_thickness); + self + } + + /// The padding inside of an element. + pub fn padding(&self) -> Option> { + self.get_layout(LayoutAttr::PADDING).map(LayoutAttr::unwrap_padding) + } + + /// Set the padding inside of an element. + pub fn set_padding(&mut self, padding: Option>) { + self.set_or_remove_layout(LayoutAttr::PADDING, padding.map(LayoutAttr::Padding)); + } + + /// Set the padding inside of an element. + pub fn with_padding(mut self, padding: Option>) -> Self { + self.set_padding(padding); + self + } + + /// The color of text, borders, and text decorations. + pub fn color(&self) -> Option { + self.get_layout(LayoutAttr::COLOR).map(LayoutAttr::unwrap_color) + } + + /// Set the color of text, borders, and text decorations. + pub fn set_color(&mut self, color: Option) { + self.set_or_remove_layout(LayoutAttr::COLOR, color.map(LayoutAttr::Color)); + } + + /// Set the color of text, borders, and text decorations. + pub fn with_color(mut self, color: Option) -> Self { + self.set_color(color); + self + } + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.get_layout(LayoutAttr::TEXT_INDENT).map(LayoutAttr::unwrap_text_indent) + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.get_layout(LayoutAttr::TEXT_ALIGN).map(LayoutAttr::unwrap_text_align) + } + + /// The alignment of block-level elements inside of this block-level element. + pub fn block_align(&self) -> Option { + self.get_layout(LayoutAttr::BLOCK_ALIGN).map(LayoutAttr::unwrap_block_align) + } + + /// The alignment of inline-level elements inside of this block-level element. + pub fn inline_align(&self) -> Option { + self.get_layout(LayoutAttr::INLINE_ALIGN).map(LayoutAttr::unwrap_inline_align) + } + + /// The border style of table cells, overriding `BorderStyle`. + pub fn table_border_style(&self) -> Option> { + self.get_layout(LayoutAttr::TABLE_BORDER_STYLE).map(LayoutAttr::unwrap_table_border_style) + } + + /// The padding inside of table cells, overriding `Padding`. + pub fn table_padding(&self) -> Option> { + self.get_layout(LayoutAttr::TABLE_PADDING).map(LayoutAttr::unwrap_table_padding) + } + + /// The distance by which the baseline shall be shifted from the default position. + pub fn baseline_shift(&self) -> Option { + self.get_layout(LayoutAttr::BASELINE_SHIFT).map(LayoutAttr::unwrap_baseline_shift) + } + + /// Set the distance by which the baseline shall be shifted from the default position. + pub fn set_baseline_shift(&mut self, baseline_shift: Option) { + self.set_or_remove_layout(LayoutAttr::BASELINE_SHIFT, baseline_shift.map(LayoutAttr::BaselineShift)); + } + + /// Set the distance by which the baseline shall be shifted from the default position. + pub fn with_baseline_shift(mut self, baseline_shift: Option) -> Self { + self.set_baseline_shift(baseline_shift); + self + } + + /// The height of each line in an element on the block axis. + pub fn line_height(&self) -> Option { + self.get_layout(LayoutAttr::LINE_HEIGHT).map(LayoutAttr::unwrap_line_height) + } + + /// Set the height of each line in an element on the block axis. + pub fn set_line_height(&mut self, line_height: Option) { + self.set_or_remove_layout(LayoutAttr::LINE_HEIGHT, line_height.map(LayoutAttr::LineHeight)); + } + + /// Set the height of each line in an element on the block axis. + pub fn with_line_height(mut self, line_height: Option) -> Self { + self.set_line_height(line_height); + self + } + + /// The color of the text decoration, overriding the fill color. + pub fn text_decoration_color(&self) -> Option { + self.get_layout(LayoutAttr::TEXT_DECORATION_COLOR).map(LayoutAttr::unwrap_text_decoration_color) + } + + /// Set the color of the text decoration, overriding the fill color. + pub fn set_text_decoration_color(&mut self, text_decoration_color: Option) { + self.set_or_remove_layout(LayoutAttr::TEXT_DECORATION_COLOR, text_decoration_color.map(LayoutAttr::TextDecorationColor)); + } + + /// Set the color of the text decoration, overriding the fill color. + pub fn with_text_decoration_color(mut self, text_decoration_color: Option) -> Self { + self.set_text_decoration_color(text_decoration_color); + self + } + + /// The width of the text decoration line. + pub fn text_decoration_thickness(&self) -> Option { + self.get_layout(LayoutAttr::TEXT_DECORATION_THICKNESS).map(LayoutAttr::unwrap_text_decoration_thickness) + } + + /// Set the width of the text decoration line. + pub fn set_text_decoration_thickness(&mut self, text_decoration_thickness: Option) { + self.set_or_remove_layout(LayoutAttr::TEXT_DECORATION_THICKNESS, text_decoration_thickness.map(LayoutAttr::TextDecorationThickness)); + } + + /// Set the width of the text decoration line. + pub fn with_text_decoration_thickness(mut self, text_decoration_thickness: Option) -> Self { + self.set_text_decoration_thickness(text_decoration_thickness); + self + } + + /// The kind of text decoration. + pub fn text_decoration_type(&self) -> Option { + self.get_layout(LayoutAttr::TEXT_DECORATION_TYPE).map(LayoutAttr::unwrap_text_decoration_type) + } + + /// Set the kind of text decoration. + pub fn set_text_decoration_type(&mut self, text_decoration_type: Option) { + self.set_or_remove_layout(LayoutAttr::TEXT_DECORATION_TYPE, text_decoration_type.map(LayoutAttr::TextDecorationType)); + } + + /// Set the kind of text decoration. + pub fn with_text_decoration_type(mut self, text_decoration_type: Option) -> Self { + self.set_text_decoration_type(text_decoration_type); + self + } + + /// How the glyphs are rotated in a vertical writing mode. + pub fn glyph_orientation_vertical(&self) -> Option { + self.get_layout(LayoutAttr::GLYPH_ORIENTATION_VERTICAL).map(LayoutAttr::unwrap_glyph_orientation_vertical) + } + + /// Set how the glyphs are rotated in a vertical writing mode. + pub fn set_glyph_orientation_vertical(&mut self, glyph_orientation_vertical: Option) { + self.set_or_remove_layout(LayoutAttr::GLYPH_ORIENTATION_VERTICAL, glyph_orientation_vertical.map(LayoutAttr::GlyphOrientationVertical)); + } + + /// Set how the glyphs are rotated in a vertical writing mode. + pub fn with_glyph_orientation_vertical(mut self, glyph_orientation_vertical: Option) -> Self { + self.set_glyph_orientation_vertical(glyph_orientation_vertical); + self + } + + /// The number of columns in the grouping element. + pub fn column_count(&self) -> Option { + self.get_layout(LayoutAttr::COLUMN_COUNT).map(LayoutAttr::unwrap_column_count) + } + + /// The width of the gaps between columns in the grouping element. + pub fn column_gap(&self) -> Option<&ColumnDimensions> { + self.get_layout(LayoutAttr::COLUMN_GAP).map(LayoutAttr::unwrap_column_gap) + } + + /// The width of the columns in the grouping element. + pub fn column_widths(&self) -> Option<&ColumnDimensions> { + self.get_layout(LayoutAttr::COLUMN_WIDTHS).map(LayoutAttr::unwrap_column_widths) + } +} + +impl Tag { + + /// The tag id. + pub fn id(&self) -> Option<&TagId> { + self.inner.get_struct(StructAttr::ID).map(StructAttr::unwrap_id) + } + + /// Set the tag id. + pub fn set_id(&mut self, id: Option) { + self.inner.set_or_remove_struct(StructAttr::ID, id.map(StructAttr::Id)); + } + + /// Set the tag id. + pub fn with_id(mut self, id: Option) -> Self { + self.set_id(id); + self + } + + /// The language of this tag. + pub fn lang(&self) -> Option<&str> { + self.inner.get_struct(StructAttr::LANG).map(StructAttr::unwrap_lang) + } + + /// Set the language of this tag. + pub fn set_lang(&mut self, lang: Option) { + self.inner.set_or_remove_struct(StructAttr::LANG, lang.map(StructAttr::Lang)); + } + + /// Set the language of this tag. + pub fn with_lang(mut self, lang: Option) -> Self { + self.set_lang(lang); + self + } + + /// The optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn alt_text(&self) -> Option<&str> { + self.inner.get_struct(StructAttr::ALT_TEXT).map(StructAttr::unwrap_alt_text) + } + + /// Set the optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn set_alt_text(&mut self, alt_text: Option) { + self.inner.set_or_remove_struct(StructAttr::ALT_TEXT, alt_text.map(StructAttr::AltText)); + } + + /// Set the optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + pub fn with_alt_text(mut self, alt_text: Option) -> Self { + self.set_alt_text(alt_text); + self + } + + /// The expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn expanded(&self) -> Option<&str> { + self.inner.get_struct(StructAttr::EXPANDED).map(StructAttr::unwrap_expanded) + } + + /// Set the expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn set_expanded(&mut self, expanded: Option) { + self.inner.set_or_remove_struct(StructAttr::EXPANDED, expanded.map(StructAttr::Expanded)); + } + + /// Set the expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + pub fn with_expanded(mut self, expanded: Option) -> Self { + self.set_expanded(expanded); + self + } + + /// The actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn actual_text(&self) -> Option<&str> { + self.inner.get_struct(StructAttr::ACTUAL_TEXT).map(StructAttr::unwrap_actual_text) + } + + /// Set the actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn set_actual_text(&mut self, actual_text: Option) { + self.inner.set_or_remove_struct(StructAttr::ACTUAL_TEXT, actual_text.map(StructAttr::ActualText)); + } + + /// Set the actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + pub fn with_actual_text(mut self, actual_text: Option) -> Self { + self.set_actual_text(actual_text); + self + } + + /// The placement. + pub fn placement(&self) -> Option { + self.inner.get_layout(LayoutAttr::PLACEMENT).map(LayoutAttr::unwrap_placement) + } + + /// Set the placement. + pub fn set_placement(&mut self, placement: Option) { + self.inner.set_or_remove_layout(LayoutAttr::PLACEMENT, placement.map(LayoutAttr::Placement)); + } + + /// Set the placement. + pub fn with_placement(mut self, placement: Option) -> Self { + self.set_placement(placement); + self + } + + /// The writing mode. + pub fn writing_mode(&self) -> Option { + self.inner.get_layout(LayoutAttr::WRITING_MODE).map(LayoutAttr::unwrap_writing_mode) + } + + /// Set the writing mode. + pub fn set_writing_mode(&mut self, writing_mode: Option) { + self.inner.set_or_remove_layout(LayoutAttr::WRITING_MODE, writing_mode.map(LayoutAttr::WritingMode)); + } + + /// Set the writing mode. + pub fn with_writing_mode(mut self, writing_mode: Option) -> Self { + self.set_writing_mode(writing_mode); + self + } + + /// The background color. + pub fn background_color(&self) -> Option { + self.inner.get_layout(LayoutAttr::BACKGROUND_COLOR).map(LayoutAttr::unwrap_background_color) + } + + /// Set the background color. + pub fn set_background_color(&mut self, background_color: Option) { + self.inner.set_or_remove_layout(LayoutAttr::BACKGROUND_COLOR, background_color.map(LayoutAttr::BackgroundColor)); + } + + /// Set the background color. + pub fn with_background_color(mut self, background_color: Option) -> Self { + self.set_background_color(background_color); + self + } + + /// The border color. + pub fn border_color(&self) -> Option> { + self.inner.get_layout(LayoutAttr::BORDER_COLOR).map(LayoutAttr::unwrap_border_color) + } + + /// Set the border color. + pub fn set_border_color(&mut self, border_color: Option>) { + self.inner.set_or_remove_layout(LayoutAttr::BORDER_COLOR, border_color.map(LayoutAttr::BorderColor)); + } + + /// Set the border color. + pub fn with_border_color(mut self, border_color: Option>) -> Self { + self.set_border_color(border_color); + self + } + + /// The way the border is drawn. + pub fn border_style(&self) -> Option> { + self.inner.get_layout(LayoutAttr::BORDER_STYLE).map(LayoutAttr::unwrap_border_style) + } + + /// Set the way the border is drawn. + pub fn set_border_style(&mut self, border_style: Option>) { + self.inner.set_or_remove_layout(LayoutAttr::BORDER_STYLE, border_style.map(LayoutAttr::BorderStyle)); + } + + /// Set the way the border is drawn. + pub fn with_border_style(mut self, border_style: Option>) -> Self { + self.set_border_style(border_style); + self + } + + /// The border width. + pub fn border_thickness(&self) -> Option> { + self.inner.get_layout(LayoutAttr::BORDER_THICKNESS).map(LayoutAttr::unwrap_border_thickness) + } + + /// Set the border width. + pub fn set_border_thickness(&mut self, border_thickness: Option>) { + self.inner.set_or_remove_layout(LayoutAttr::BORDER_THICKNESS, border_thickness.map(LayoutAttr::BorderThickness)); + } + + /// Set the border width. + pub fn with_border_thickness(mut self, border_thickness: Option>) -> Self { + self.set_border_thickness(border_thickness); + self + } + + /// The padding inside of an element. + pub fn padding(&self) -> Option> { + self.inner.get_layout(LayoutAttr::PADDING).map(LayoutAttr::unwrap_padding) + } + + /// Set the padding inside of an element. + pub fn set_padding(&mut self, padding: Option>) { + self.inner.set_or_remove_layout(LayoutAttr::PADDING, padding.map(LayoutAttr::Padding)); + } + + /// Set the padding inside of an element. + pub fn with_padding(mut self, padding: Option>) -> Self { + self.set_padding(padding); + self + } + + /// The color of text, borders, and text decorations. + pub fn color(&self) -> Option { + self.inner.get_layout(LayoutAttr::COLOR).map(LayoutAttr::unwrap_color) + } + + /// Set the color of text, borders, and text decorations. + pub fn set_color(&mut self, color: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLOR, color.map(LayoutAttr::Color)); + } + + /// Set the color of text, borders, and text decorations. + pub fn with_color(mut self, color: Option) -> Self { + self.set_color(color); + self + } + + /// The distance by which the baseline shall be shifted from the default position. + pub fn baseline_shift(&self) -> Option { + self.inner.get_layout(LayoutAttr::BASELINE_SHIFT).map(LayoutAttr::unwrap_baseline_shift) + } + + /// Set the distance by which the baseline shall be shifted from the default position. + pub fn set_baseline_shift(&mut self, baseline_shift: Option) { + self.inner.set_or_remove_layout(LayoutAttr::BASELINE_SHIFT, baseline_shift.map(LayoutAttr::BaselineShift)); + } + + /// Set the distance by which the baseline shall be shifted from the default position. + pub fn with_baseline_shift(mut self, baseline_shift: Option) -> Self { + self.set_baseline_shift(baseline_shift); + self + } + + /// The height of each line in an element on the block axis. + pub fn line_height(&self) -> Option { + self.inner.get_layout(LayoutAttr::LINE_HEIGHT).map(LayoutAttr::unwrap_line_height) + } + + /// Set the height of each line in an element on the block axis. + pub fn set_line_height(&mut self, line_height: Option) { + self.inner.set_or_remove_layout(LayoutAttr::LINE_HEIGHT, line_height.map(LayoutAttr::LineHeight)); + } + + /// Set the height of each line in an element on the block axis. + pub fn with_line_height(mut self, line_height: Option) -> Self { + self.set_line_height(line_height); + self + } + + /// The color of the text decoration, overriding the fill color. + pub fn text_decoration_color(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_DECORATION_COLOR).map(LayoutAttr::unwrap_text_decoration_color) + } + + /// Set the color of the text decoration, overriding the fill color. + pub fn set_text_decoration_color(&mut self, text_decoration_color: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_DECORATION_COLOR, text_decoration_color.map(LayoutAttr::TextDecorationColor)); + } + + /// Set the color of the text decoration, overriding the fill color. + pub fn with_text_decoration_color(mut self, text_decoration_color: Option) -> Self { + self.set_text_decoration_color(text_decoration_color); + self + } + + /// The width of the text decoration line. + pub fn text_decoration_thickness(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_DECORATION_THICKNESS).map(LayoutAttr::unwrap_text_decoration_thickness) + } + + /// Set the width of the text decoration line. + pub fn set_text_decoration_thickness(&mut self, text_decoration_thickness: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_DECORATION_THICKNESS, text_decoration_thickness.map(LayoutAttr::TextDecorationThickness)); + } + + /// Set the width of the text decoration line. + pub fn with_text_decoration_thickness(mut self, text_decoration_thickness: Option) -> Self { + self.set_text_decoration_thickness(text_decoration_thickness); + self + } + + /// The kind of text decoration. + pub fn text_decoration_type(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_DECORATION_TYPE).map(LayoutAttr::unwrap_text_decoration_type) + } + + /// Set the kind of text decoration. + pub fn set_text_decoration_type(&mut self, text_decoration_type: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_DECORATION_TYPE, text_decoration_type.map(LayoutAttr::TextDecorationType)); + } + + /// Set the kind of text decoration. + pub fn with_text_decoration_type(mut self, text_decoration_type: Option) -> Self { + self.set_text_decoration_type(text_decoration_type); + self + } + + /// How the glyphs are rotated in a vertical writing mode. + pub fn glyph_orientation_vertical(&self) -> Option { + self.inner.get_layout(LayoutAttr::GLYPH_ORIENTATION_VERTICAL).map(LayoutAttr::unwrap_glyph_orientation_vertical) + } + + /// Set how the glyphs are rotated in a vertical writing mode. + pub fn set_glyph_orientation_vertical(&mut self, glyph_orientation_vertical: Option) { + self.inner.set_or_remove_layout(LayoutAttr::GLYPH_ORIENTATION_VERTICAL, glyph_orientation_vertical.map(LayoutAttr::GlyphOrientationVertical)); + } + + /// Set how the glyphs are rotated in a vertical writing mode. + pub fn with_glyph_orientation_vertical(mut self, glyph_orientation_vertical: Option) -> Self { + self.set_glyph_orientation_vertical(glyph_orientation_vertical); + self + } +} + +/// Tag kind structs. +pub mod kind { + /// A part of a document that may contain multiple articles or sections. + #[derive(Clone, Debug, PartialEq)] + pub struct Part; + + /// An article with largely self-contained content. + #[derive(Clone, Debug, PartialEq)] + pub struct Article; + + /// Section of a larger document. + #[derive(Clone, Debug, PartialEq)] + pub struct Section; + + /// A generic block-level grouping element. + #[derive(Clone, Debug, PartialEq)] + pub struct Div; + + /// A paragraph-level quote. + #[derive(Clone, Debug, PartialEq)] + pub struct BlockQuote; + + /// An image or figure caption. + /// + /// **Best Practice**: In the tag tree, this should appear + /// as a sibling after the image (or other) content it describes. + #[derive(Clone, Debug, PartialEq)] + pub struct Caption; + + /// Table of contents. + /// + /// **Best Practice**: Should consist of TOCIs or other nested TOCs. + #[derive(Clone, Debug, PartialEq)] + pub struct TOC; + + /// Item in the table of contents. + /// + /// **Best Practice**: Should only appear within a TOC. Should only consist of + /// labels, references, paragraphs and TOCs. + #[derive(Clone, Debug, PartialEq)] + pub struct TOCI; + + /// Index of the key terms in the document. + /// + /// **Best Practice**: Should contain a sequence of text accompanied by + /// reference elements pointing to their occurrence in the text. + #[derive(Clone, Debug, PartialEq)] + pub struct Index; + + /// A paragraph. + #[derive(Clone, Debug, PartialEq)] + pub struct P; + + /// Heading level `n`, including an optional title of the heading. + /// + /// The title is required for some export modes, like for example PDF/UA. + #[derive(Clone, Debug, PartialEq)] + pub struct Hn; + + /// A list. + /// + /// **Best practice**: Should consist of an optional caption followed by + /// list items. + /// List numbering is only required for PDF/UA, but we just enforce it for always. + #[derive(Clone, Debug, PartialEq)] + pub struct L; + + /// A list item. + /// + /// **Best practice**: Should consist of one or more list labels and/or list bodies. + #[derive(Clone, Debug, PartialEq)] + pub struct LI; + + /// Label for a list item. + #[derive(Clone, Debug, PartialEq)] + pub struct Lbl; + + /// Description of the list item. + #[derive(Clone, Debug, PartialEq)] + pub struct LBody; + + /// A table, with an optional summary describing the purpose and structure. + /// + /// **Best practice**: Should consist of an optional table header row, + /// one or more table body elements and an optional table footer. Can have + /// caption as the first or last child. + #[derive(Clone, Debug, PartialEq)] + pub struct Table; + + /// A table row. + /// + /// **Best practice**: May contain table headers cells and table data cells. + #[derive(Clone, Debug, PartialEq)] + pub struct TR; + + /// A table header cell. + #[derive(Clone, Debug, PartialEq)] + pub struct TH; + + /// A table data cell. + #[derive(Clone, Debug, PartialEq)] + pub struct TD; + + /// A table header row group. + #[derive(Clone, Debug, PartialEq)] + pub struct THead; + + /// A table data row group. + #[derive(Clone, Debug, PartialEq)] + pub struct TBody; + + /// A table footer row group. + #[derive(Clone, Debug, PartialEq)] + pub struct TFoot; + + /// An inline-level element that does not have a specific meaning. + #[derive(Clone, Debug, PartialEq)] + pub struct Span; + + /// An inline quotation. + #[derive(Clone, Debug, PartialEq)] + pub struct InlineQuote; + + /// A foot- or endnote, potentially referred to from within the text. + /// + /// **Best practice**: It may have a label as a child. + #[derive(Clone, Debug, PartialEq)] + pub struct Note; + + /// A reference to elsewhere in the document. + /// + /// **Best practice**: The first child of a tag group with this tag should be a link annotation + /// linking to a destination in the document, and the second child should consist of + /// the children that should be associated with that reference. + #[derive(Clone, Debug, PartialEq)] + pub struct Reference; + + /// A reference to the external source of some cited document. + /// + /// **Best practice**: It may have a label as a child. + #[derive(Clone, Debug, PartialEq)] + pub struct BibEntry; + + /// Computer code. + #[derive(Clone, Debug, PartialEq)] + pub struct Code; + + /// A link. + /// + /// **Best practice**: The first child of a tag group with this tag should be a link annotation + /// linking to an URL, and the second child should consist of the children that should + /// be associated with that link. + #[derive(Clone, Debug, PartialEq)] + pub struct Link; + + /// An association between an annotation and the content it belongs to. PDF + /// + /// **Best practice**: Should be used for all annotations, except for link annotations and + /// widget annotations. The first child should be the identifier of a non-link annotation, + /// and all other subsequent children should be content identifiers associated with that + /// annotation. + #[derive(Clone, Debug, PartialEq)] + pub struct Annot; + + /// Item of graphical content. + /// + /// Providing `alt_text` is required in some export modes, like for example PDF/UA-1. + #[derive(Clone, Debug, PartialEq)] + pub struct Figure; + + /// A mathematical formula. + /// + /// Providing `alt_text` is required in some export modes, like for example PDF/UA-1. + #[derive(Clone, Debug, PartialEq)] + pub struct Formula; + + /// An interactive form field. + #[derive(Clone, Debug, PartialEq)] + pub struct Form; + + /// Non-structural element. A grouping element having no inherent structural significance; + /// it serves solely for grouping purposes. + #[derive(Clone, Debug, PartialEq)] + pub struct NonStruct; + + /// A date or time. + #[derive(Clone, Debug, PartialEq)] + pub struct Datetime; + + /// A list of terms. + #[derive(Clone, Debug, PartialEq)] + pub struct Terms; + + /// A title. + #[derive(Clone, Debug, PartialEq)] + pub struct Title; + + /// Encloses content with strong importance, most commonly **bold** text. + #[derive(Clone, Debug, PartialEq)] + pub struct Strong; + + /// Encloses content that is emphasized, most commonly *italic* text. + #[derive(Clone, Debug, PartialEq)] + pub struct Em; + +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Part(value) + } +} +impl Tag { + /// A part of a document that may contain multiple articles or sections. + #[allow(non_upper_case_globals)] + pub const Part: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Article(value) + } +} +impl Tag { + /// An article with largely self-contained content. + #[allow(non_upper_case_globals)] + pub const Article: Tag = Tag::new(); + + /// The number of columns in the grouping element. + pub fn column_count(&self) -> Option { + self.inner.get_layout(LayoutAttr::COLUMN_COUNT).map(LayoutAttr::unwrap_column_count) + } + + /// Set the number of columns in the grouping element. + pub fn set_column_count(&mut self, column_count: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_COUNT, column_count.map(LayoutAttr::ColumnCount)); + } + + /// Set the number of columns in the grouping element. + pub fn with_column_count(mut self, column_count: Option) -> Self { + self.set_column_count(column_count); + self + } + + /// The width of the gaps between columns in the grouping element. + pub fn column_gap(&self) -> Option<&ColumnDimensions> { + self.inner.get_layout(LayoutAttr::COLUMN_GAP).map(LayoutAttr::unwrap_column_gap) + } + + /// Set the width of the gaps between columns in the grouping element. + pub fn set_column_gap(&mut self, column_gap: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_GAP, column_gap.map(LayoutAttr::ColumnGap)); + } + + /// Set the width of the gaps between columns in the grouping element. + pub fn with_column_gap(mut self, column_gap: Option) -> Self { + self.set_column_gap(column_gap); + self + } + + /// The width of the columns in the grouping element. + pub fn column_widths(&self) -> Option<&ColumnDimensions> { + self.inner.get_layout(LayoutAttr::COLUMN_WIDTHS).map(LayoutAttr::unwrap_column_widths) + } + + /// Set the width of the columns in the grouping element. + pub fn set_column_widths(&mut self, column_widths: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_WIDTHS, column_widths.map(LayoutAttr::ColumnWidths)); + } + + /// Set the width of the columns in the grouping element. + pub fn with_column_widths(mut self, column_widths: Option) -> Self { + self.set_column_widths(column_widths); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Section(value) + } +} +impl Tag { + /// Section of a larger document. + #[allow(non_upper_case_globals)] + pub const Section: Tag = Tag::new(); + + /// The number of columns in the grouping element. + pub fn column_count(&self) -> Option { + self.inner.get_layout(LayoutAttr::COLUMN_COUNT).map(LayoutAttr::unwrap_column_count) + } + + /// Set the number of columns in the grouping element. + pub fn set_column_count(&mut self, column_count: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_COUNT, column_count.map(LayoutAttr::ColumnCount)); + } + + /// Set the number of columns in the grouping element. + pub fn with_column_count(mut self, column_count: Option) -> Self { + self.set_column_count(column_count); + self + } + + /// The width of the gaps between columns in the grouping element. + pub fn column_gap(&self) -> Option<&ColumnDimensions> { + self.inner.get_layout(LayoutAttr::COLUMN_GAP).map(LayoutAttr::unwrap_column_gap) + } + + /// Set the width of the gaps between columns in the grouping element. + pub fn set_column_gap(&mut self, column_gap: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_GAP, column_gap.map(LayoutAttr::ColumnGap)); + } + + /// Set the width of the gaps between columns in the grouping element. + pub fn with_column_gap(mut self, column_gap: Option) -> Self { + self.set_column_gap(column_gap); + self + } + + /// The width of the columns in the grouping element. + pub fn column_widths(&self) -> Option<&ColumnDimensions> { + self.inner.get_layout(LayoutAttr::COLUMN_WIDTHS).map(LayoutAttr::unwrap_column_widths) + } + + /// Set the width of the columns in the grouping element. + pub fn set_column_widths(&mut self, column_widths: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_WIDTHS, column_widths.map(LayoutAttr::ColumnWidths)); + } + + /// Set the width of the columns in the grouping element. + pub fn with_column_widths(mut self, column_widths: Option) -> Self { + self.set_column_widths(column_widths); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Div(value) + } +} +impl Tag { + /// A generic block-level grouping element. + #[allow(non_upper_case_globals)] + pub const Div: Tag = Tag::new(); + + /// The number of columns in the grouping element. + pub fn column_count(&self) -> Option { + self.inner.get_layout(LayoutAttr::COLUMN_COUNT).map(LayoutAttr::unwrap_column_count) + } + + /// Set the number of columns in the grouping element. + pub fn set_column_count(&mut self, column_count: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_COUNT, column_count.map(LayoutAttr::ColumnCount)); + } + + /// Set the number of columns in the grouping element. + pub fn with_column_count(mut self, column_count: Option) -> Self { + self.set_column_count(column_count); + self + } + + /// The width of the gaps between columns in the grouping element. + pub fn column_gap(&self) -> Option<&ColumnDimensions> { + self.inner.get_layout(LayoutAttr::COLUMN_GAP).map(LayoutAttr::unwrap_column_gap) + } + + /// Set the width of the gaps between columns in the grouping element. + pub fn set_column_gap(&mut self, column_gap: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_GAP, column_gap.map(LayoutAttr::ColumnGap)); + } + + /// Set the width of the gaps between columns in the grouping element. + pub fn with_column_gap(mut self, column_gap: Option) -> Self { + self.set_column_gap(column_gap); + self + } + + /// The width of the columns in the grouping element. + pub fn column_widths(&self) -> Option<&ColumnDimensions> { + self.inner.get_layout(LayoutAttr::COLUMN_WIDTHS).map(LayoutAttr::unwrap_column_widths) + } + + /// Set the width of the columns in the grouping element. + pub fn set_column_widths(&mut self, column_widths: Option) { + self.inner.set_or_remove_layout(LayoutAttr::COLUMN_WIDTHS, column_widths.map(LayoutAttr::ColumnWidths)); + } + + /// Set the width of the columns in the grouping element. + pub fn with_column_widths(mut self, column_widths: Option) -> Self { + self.set_column_widths(column_widths); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::BlockQuote(value) + } +} +impl Tag { + /// A paragraph-level quote. + #[allow(non_upper_case_globals)] + pub const BlockQuote: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Caption(value) + } +} +impl Tag { + /// An image or figure caption. + /// + /// **Best Practice**: In the tag tree, this should appear + /// as a sibling after the image (or other) content it describes. + #[allow(non_upper_case_globals)] + pub const Caption: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::TOC(value) + } +} +impl Tag { + /// Table of contents. + /// + /// **Best Practice**: Should consist of TOCIs or other nested TOCs. + #[allow(non_upper_case_globals)] + pub const TOC: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::TOCI(value) + } +} +impl Tag { + /// Item in the table of contents. + /// + /// **Best Practice**: Should only appear within a TOC. Should only consist of + /// labels, references, paragraphs and TOCs. + #[allow(non_upper_case_globals)] + pub const TOCI: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Index(value) + } +} +impl Tag { + /// Index of the key terms in the document. + /// + /// **Best Practice**: Should contain a sequence of text accompanied by + /// reference elements pointing to their occurrence in the text. + #[allow(non_upper_case_globals)] + pub const Index: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::P(value) + } +} +impl Tag { + /// A paragraph. + #[allow(non_upper_case_globals)] + pub const P: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_INDENT).map(LayoutAttr::unwrap_text_indent) + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn set_text_indent(&mut self, text_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_INDENT, text_indent.map(LayoutAttr::TextIndent)); + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn with_text_indent(mut self, text_indent: Option) -> Self { + self.set_text_indent(text_indent); + self + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_ALIGN).map(LayoutAttr::unwrap_text_align) + } + + /// Set the text alignment. + pub fn set_text_align(&mut self, text_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_ALIGN, text_align.map(LayoutAttr::TextAlign)); + } + + /// Set the text alignment. + pub fn with_text_align(mut self, text_align: Option) -> Self { + self.set_text_align(text_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Hn(value) + } +} +impl Tag { + /// Heading level `n`, including an optional title of the heading. + /// + /// The title is required for some export modes, like for example PDF/UA. + #[allow(non_snake_case)] + pub fn Hn(level: NonZeroU16, title: Option) -> Tag { + let mut tag = Tag::new(); + tag.set_level(level); + tag.set_title(title); + tag + } + + /// The heading level + pub fn level(&self) -> NonZeroU16 { + self.inner.get_struct(StructAttr::HEADING_LEVEL).unwrap().unwrap_level() + } + + /// Set the heading level + pub fn set_level(&mut self, level: NonZeroU16) { + self.inner.set_struct(StructAttr::HeadingLevel(level)); + } + + /// Set the heading level + pub fn with_level(mut self, level: NonZeroU16) -> Self { + self.set_level(level); + self + } + + /// The title, characterizing a specific tag such as `"Chapter 1"`. + pub fn title(&self) -> Option<&str> { + self.inner.get_struct(StructAttr::TITLE).map(StructAttr::unwrap_title) + } + + /// Set the title, characterizing a specific tag such as `"Chapter 1"`. + pub fn set_title(&mut self, title: Option) { + self.inner.set_or_remove_struct(StructAttr::TITLE, title.map(StructAttr::Title)); + } + + /// Set the title, characterizing a specific tag such as `"Chapter 1"`. + pub fn with_title(mut self, title: Option) -> Self { + self.set_title(title); + self + } + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_INDENT).map(LayoutAttr::unwrap_text_indent) + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn set_text_indent(&mut self, text_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_INDENT, text_indent.map(LayoutAttr::TextIndent)); + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn with_text_indent(mut self, text_indent: Option) -> Self { + self.set_text_indent(text_indent); + self + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_ALIGN).map(LayoutAttr::unwrap_text_align) + } + + /// Set the text alignment. + pub fn set_text_align(&mut self, text_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_ALIGN, text_align.map(LayoutAttr::TextAlign)); + } + + /// Set the text alignment. + pub fn with_text_align(mut self, text_align: Option) -> Self { + self.set_text_align(text_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::L(value) + } +} +impl Tag { + /// A list. + /// + /// **Best practice**: Should consist of an optional caption followed by + /// list items. + /// List numbering is only required for PDF/UA, but we just enforce it for always. + #[allow(non_snake_case)] + pub fn L(numbering: ListNumbering) -> Tag { + let mut tag = Tag::new(); + tag.set_numbering(numbering); + tag + } + + /// The list numbering. + pub fn numbering(&self) -> ListNumbering { + self.inner.get_list(ListAttr::NUMBERING).unwrap().unwrap_numbering() + } + + /// Set the list numbering. + pub fn set_numbering(&mut self, numbering: ListNumbering) { + self.inner.set_list(ListAttr::Numbering(numbering)); + } + + /// Set the list numbering. + pub fn with_numbering(mut self, numbering: ListNumbering) -> Self { + self.set_numbering(numbering); + self + } + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_INDENT).map(LayoutAttr::unwrap_text_indent) + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn set_text_indent(&mut self, text_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_INDENT, text_indent.map(LayoutAttr::TextIndent)); + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn with_text_indent(mut self, text_indent: Option) -> Self { + self.set_text_indent(text_indent); + self + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_ALIGN).map(LayoutAttr::unwrap_text_align) + } + + /// Set the text alignment. + pub fn set_text_align(&mut self, text_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_ALIGN, text_align.map(LayoutAttr::TextAlign)); + } + + /// Set the text alignment. + pub fn with_text_align(mut self, text_align: Option) -> Self { + self.set_text_align(text_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::LI(value) + } +} +impl Tag { + /// A list item. + /// + /// **Best practice**: Should consist of one or more list labels and/or list bodies. + #[allow(non_upper_case_globals)] + pub const LI: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_INDENT).map(LayoutAttr::unwrap_text_indent) + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn set_text_indent(&mut self, text_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_INDENT, text_indent.map(LayoutAttr::TextIndent)); + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn with_text_indent(mut self, text_indent: Option) -> Self { + self.set_text_indent(text_indent); + self + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_ALIGN).map(LayoutAttr::unwrap_text_align) + } + + /// Set the text alignment. + pub fn set_text_align(&mut self, text_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_ALIGN, text_align.map(LayoutAttr::TextAlign)); + } + + /// Set the text alignment. + pub fn with_text_align(mut self, text_align: Option) -> Self { + self.set_text_align(text_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Lbl(value) + } +} +impl Tag { + /// Label for a list item. + #[allow(non_upper_case_globals)] + pub const Lbl: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_INDENT).map(LayoutAttr::unwrap_text_indent) + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn set_text_indent(&mut self, text_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_INDENT, text_indent.map(LayoutAttr::TextIndent)); + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn with_text_indent(mut self, text_indent: Option) -> Self { + self.set_text_indent(text_indent); + self + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_ALIGN).map(LayoutAttr::unwrap_text_align) + } + + /// Set the text alignment. + pub fn set_text_align(&mut self, text_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_ALIGN, text_align.map(LayoutAttr::TextAlign)); + } + + /// Set the text alignment. + pub fn with_text_align(mut self, text_align: Option) -> Self { + self.set_text_align(text_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::LBody(value) + } +} +impl Tag { + /// Description of the list item. + #[allow(non_upper_case_globals)] + pub const LBody: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_INDENT).map(LayoutAttr::unwrap_text_indent) + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn set_text_indent(&mut self, text_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_INDENT, text_indent.map(LayoutAttr::TextIndent)); + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn with_text_indent(mut self, text_indent: Option) -> Self { + self.set_text_indent(text_indent); + self + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_ALIGN).map(LayoutAttr::unwrap_text_align) + } + + /// Set the text alignment. + pub fn set_text_align(&mut self, text_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_ALIGN, text_align.map(LayoutAttr::TextAlign)); + } + + /// Set the text alignment. + pub fn with_text_align(mut self, text_align: Option) -> Self { + self.set_text_align(text_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Table(value) + } +} +impl Tag { + /// A table, with an optional summary describing the purpose and structure. + /// + /// **Best practice**: Should consist of an optional table header row, + /// one or more table body elements and an optional table footer. Can have + /// caption as the first or last child. + #[allow(non_upper_case_globals)] + pub const Table: Tag = Tag::new(); + + /// The table summary. + pub fn summary(&self) -> Option<&str> { + self.inner.get_table(TableAttr::SUMMARY).map(TableAttr::unwrap_summary) + } + + /// Set the table summary. + pub fn set_summary(&mut self, summary: Option) { + self.inner.set_or_remove_table(TableAttr::SUMMARY, summary.map(TableAttr::Summary)); + } + + /// Set the table summary. + pub fn with_summary(mut self, summary: Option) -> Self { + self.set_summary(summary); + self + } + + /// The bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn bbox(&self) -> Option { + self.inner.get_layout(LayoutAttr::B_BOX).map(LayoutAttr::unwrap_bbox) + } + + /// Set the bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn set_bbox(&mut self, bbox: Option) { + self.inner.set_or_remove_layout(LayoutAttr::B_BOX, bbox.map(LayoutAttr::BBox)); + } + + /// Set the bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn with_bbox(mut self, bbox: Option) -> Self { + self.set_bbox(bbox); + self + } + + /// The width. + pub fn width(&self) -> Option { + self.inner.get_layout(LayoutAttr::WIDTH).map(LayoutAttr::unwrap_width) + } + + /// Set the width. + pub fn set_width(&mut self, width: Option) { + self.inner.set_or_remove_layout(LayoutAttr::WIDTH, width.map(LayoutAttr::Width)); + } + + /// Set the width. + pub fn with_width(mut self, width: Option) -> Self { + self.set_width(width); + self + } + + /// The height. + pub fn height(&self) -> Option { + self.inner.get_layout(LayoutAttr::HEIGHT).map(LayoutAttr::unwrap_height) + } + + /// Set the height. + pub fn set_height(&mut self, height: Option) { + self.inner.set_or_remove_layout(LayoutAttr::HEIGHT, height.map(LayoutAttr::Height)); + } + + /// Set the height. + pub fn with_height(mut self, height: Option) -> Self { + self.set_height(height); + self + } + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } + + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn text_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_INDENT).map(LayoutAttr::unwrap_text_indent) + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn set_text_indent(&mut self, text_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_INDENT, text_indent.map(LayoutAttr::TextIndent)); + } + + /// Set the amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + pub fn with_text_indent(mut self, text_indent: Option) -> Self { + self.set_text_indent(text_indent); + self + } + + /// The text alignment. + pub fn text_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::TEXT_ALIGN).map(LayoutAttr::unwrap_text_align) + } + + /// Set the text alignment. + pub fn set_text_align(&mut self, text_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::TEXT_ALIGN, text_align.map(LayoutAttr::TextAlign)); + } + + /// Set the text alignment. + pub fn with_text_align(mut self, text_align: Option) -> Self { + self.set_text_align(text_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::TR(value) + } +} +impl Tag { + /// A table row. + /// + /// **Best practice**: May contain table headers cells and table data cells. + #[allow(non_upper_case_globals)] + pub const TR: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::TH(value) + } +} +impl Tag { + /// A table header cell. + #[allow(non_snake_case)] + pub fn TH(scope: TableHeaderScope) -> Tag { + let mut tag = Tag::new(); + tag.set_scope(scope); + tag + } + + /// The table header scope. + pub fn scope(&self) -> TableHeaderScope { + self.inner.get_table(TableAttr::HEADER_SCOPE).unwrap().unwrap_scope() + } + + /// Set the table header scope. + pub fn set_scope(&mut self, scope: TableHeaderScope) { + self.inner.set_table(TableAttr::HeaderScope(scope)); + } + + /// Set the table header scope. + pub fn with_scope(mut self, scope: TableHeaderScope) -> Self { + self.set_scope(scope); + self + } + + /// The list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + pub fn headers(&self) -> Option<&[TagId]> { + self.inner.get_table(TableAttr::CELL_HEADERS).map(TableAttr::unwrap_headers) + } + + /// Set the list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + pub fn set_headers(&mut self, headers: Option>) { + self.inner.set_or_remove_table(TableAttr::CELL_HEADERS, headers.map(|ids| ids.into_iter().collect()).map(TableAttr::CellHeaders)); + } + + /// Set the list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + pub fn with_headers(mut self, headers: Option>) -> Self { + self.set_headers(headers); + self + } + + /// The row span of this table cell. + pub fn row_span(&self) -> Option { + self.inner.get_table(TableAttr::ROW_SPAN).map(TableAttr::unwrap_row_span) + } + + /// Set the row span of this table cell. + pub fn set_row_span(&mut self, row_span: Option) { + self.inner.set_or_remove_table(TableAttr::ROW_SPAN, row_span.map(TableAttr::RowSpan)); + } + + /// Set the row span of this table cell. + pub fn with_row_span(mut self, row_span: Option) -> Self { + self.set_row_span(row_span); + self + } + + /// The column span of this table cell. + pub fn col_span(&self) -> Option { + self.inner.get_table(TableAttr::COL_SPAN).map(TableAttr::unwrap_col_span) + } + + /// Set the column span of this table cell. + pub fn set_col_span(&mut self, col_span: Option) { + self.inner.set_or_remove_table(TableAttr::COL_SPAN, col_span.map(TableAttr::ColSpan)); + } + + /// Set the column span of this table cell. + pub fn with_col_span(mut self, col_span: Option) -> Self { + self.set_col_span(col_span); + self + } + + /// The width. + pub fn width(&self) -> Option { + self.inner.get_layout(LayoutAttr::WIDTH).map(LayoutAttr::unwrap_width) + } + + /// Set the width. + pub fn set_width(&mut self, width: Option) { + self.inner.set_or_remove_layout(LayoutAttr::WIDTH, width.map(LayoutAttr::Width)); + } + + /// Set the width. + pub fn with_width(mut self, width: Option) -> Self { + self.set_width(width); + self + } + + /// The height. + pub fn height(&self) -> Option { + self.inner.get_layout(LayoutAttr::HEIGHT).map(LayoutAttr::unwrap_height) + } + + /// Set the height. + pub fn set_height(&mut self, height: Option) { + self.inner.set_or_remove_layout(LayoutAttr::HEIGHT, height.map(LayoutAttr::Height)); + } + + /// Set the height. + pub fn with_height(mut self, height: Option) -> Self { + self.set_height(height); + self + } + + /// The border style of table cells, overriding `BorderStyle`. + pub fn table_border_style(&self) -> Option> { + self.inner.get_layout(LayoutAttr::TABLE_BORDER_STYLE).map(LayoutAttr::unwrap_table_border_style) + } + + /// Set the border style of table cells, overriding `BorderStyle`. + pub fn set_table_border_style(&mut self, table_border_style: Option>) { + self.inner.set_or_remove_layout(LayoutAttr::TABLE_BORDER_STYLE, table_border_style.map(LayoutAttr::TableBorderStyle)); + } + + /// Set the border style of table cells, overriding `BorderStyle`. + pub fn with_table_border_style(mut self, table_border_style: Option>) -> Self { + self.set_table_border_style(table_border_style); + self + } + + /// The padding inside of table cells, overriding `Padding`. + pub fn table_padding(&self) -> Option> { + self.inner.get_layout(LayoutAttr::TABLE_PADDING).map(LayoutAttr::unwrap_table_padding) + } + + /// Set the padding inside of table cells, overriding `Padding`. + pub fn set_table_padding(&mut self, table_padding: Option>) { + self.inner.set_or_remove_layout(LayoutAttr::TABLE_PADDING, table_padding.map(LayoutAttr::TablePadding)); + } + + /// Set the padding inside of table cells, overriding `Padding`. + pub fn with_table_padding(mut self, table_padding: Option>) -> Self { + self.set_table_padding(table_padding); + self + } + + /// The alignment of block-level elements inside of this block-level element. + pub fn block_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::BLOCK_ALIGN).map(LayoutAttr::unwrap_block_align) + } + + /// Set the alignment of block-level elements inside of this block-level element. + pub fn set_block_align(&mut self, block_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::BLOCK_ALIGN, block_align.map(LayoutAttr::BlockAlign)); + } + + /// Set the alignment of block-level elements inside of this block-level element. + pub fn with_block_align(mut self, block_align: Option) -> Self { + self.set_block_align(block_align); + self + } + + /// The alignment of inline-level elements inside of this block-level element. + pub fn inline_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::INLINE_ALIGN).map(LayoutAttr::unwrap_inline_align) + } + + /// Set the alignment of inline-level elements inside of this block-level element. + pub fn set_inline_align(&mut self, inline_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::INLINE_ALIGN, inline_align.map(LayoutAttr::InlineAlign)); + } + + /// Set the alignment of inline-level elements inside of this block-level element. + pub fn with_inline_align(mut self, inline_align: Option) -> Self { + self.set_inline_align(inline_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::TD(value) + } +} +impl Tag { + /// A table data cell. + #[allow(non_upper_case_globals)] + pub const TD: Tag = Tag::new(); + + /// The list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + pub fn headers(&self) -> Option<&[TagId]> { + self.inner.get_table(TableAttr::CELL_HEADERS).map(TableAttr::unwrap_headers) + } + + /// Set the list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + pub fn set_headers(&mut self, headers: Option>) { + self.inner.set_or_remove_table(TableAttr::CELL_HEADERS, headers.map(|ids| ids.into_iter().collect()).map(TableAttr::CellHeaders)); + } + + /// Set the list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + pub fn with_headers(mut self, headers: Option>) -> Self { + self.set_headers(headers); + self + } + + /// The row span of this table cell. + pub fn row_span(&self) -> Option { + self.inner.get_table(TableAttr::ROW_SPAN).map(TableAttr::unwrap_row_span) + } + + /// Set the row span of this table cell. + pub fn set_row_span(&mut self, row_span: Option) { + self.inner.set_or_remove_table(TableAttr::ROW_SPAN, row_span.map(TableAttr::RowSpan)); + } + + /// Set the row span of this table cell. + pub fn with_row_span(mut self, row_span: Option) -> Self { + self.set_row_span(row_span); + self + } + + /// The column span of this table cell. + pub fn col_span(&self) -> Option { + self.inner.get_table(TableAttr::COL_SPAN).map(TableAttr::unwrap_col_span) + } + + /// Set the column span of this table cell. + pub fn set_col_span(&mut self, col_span: Option) { + self.inner.set_or_remove_table(TableAttr::COL_SPAN, col_span.map(TableAttr::ColSpan)); + } + + /// Set the column span of this table cell. + pub fn with_col_span(mut self, col_span: Option) -> Self { + self.set_col_span(col_span); + self + } + + /// The width. + pub fn width(&self) -> Option { + self.inner.get_layout(LayoutAttr::WIDTH).map(LayoutAttr::unwrap_width) + } + + /// Set the width. + pub fn set_width(&mut self, width: Option) { + self.inner.set_or_remove_layout(LayoutAttr::WIDTH, width.map(LayoutAttr::Width)); + } + + /// Set the width. + pub fn with_width(mut self, width: Option) -> Self { + self.set_width(width); + self + } + + /// The height. + pub fn height(&self) -> Option { + self.inner.get_layout(LayoutAttr::HEIGHT).map(LayoutAttr::unwrap_height) + } + + /// Set the height. + pub fn set_height(&mut self, height: Option) { + self.inner.set_or_remove_layout(LayoutAttr::HEIGHT, height.map(LayoutAttr::Height)); + } + + /// Set the height. + pub fn with_height(mut self, height: Option) -> Self { + self.set_height(height); + self + } + + /// The border style of table cells, overriding `BorderStyle`. + pub fn table_border_style(&self) -> Option> { + self.inner.get_layout(LayoutAttr::TABLE_BORDER_STYLE).map(LayoutAttr::unwrap_table_border_style) + } + + /// Set the border style of table cells, overriding `BorderStyle`. + pub fn set_table_border_style(&mut self, table_border_style: Option>) { + self.inner.set_or_remove_layout(LayoutAttr::TABLE_BORDER_STYLE, table_border_style.map(LayoutAttr::TableBorderStyle)); + } + + /// Set the border style of table cells, overriding `BorderStyle`. + pub fn with_table_border_style(mut self, table_border_style: Option>) -> Self { + self.set_table_border_style(table_border_style); + self + } + + /// The padding inside of table cells, overriding `Padding`. + pub fn table_padding(&self) -> Option> { + self.inner.get_layout(LayoutAttr::TABLE_PADDING).map(LayoutAttr::unwrap_table_padding) + } + + /// Set the padding inside of table cells, overriding `Padding`. + pub fn set_table_padding(&mut self, table_padding: Option>) { + self.inner.set_or_remove_layout(LayoutAttr::TABLE_PADDING, table_padding.map(LayoutAttr::TablePadding)); + } + + /// Set the padding inside of table cells, overriding `Padding`. + pub fn with_table_padding(mut self, table_padding: Option>) -> Self { + self.set_table_padding(table_padding); + self + } + + /// The alignment of block-level elements inside of this block-level element. + pub fn block_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::BLOCK_ALIGN).map(LayoutAttr::unwrap_block_align) + } + + /// Set the alignment of block-level elements inside of this block-level element. + pub fn set_block_align(&mut self, block_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::BLOCK_ALIGN, block_align.map(LayoutAttr::BlockAlign)); + } + + /// Set the alignment of block-level elements inside of this block-level element. + pub fn with_block_align(mut self, block_align: Option) -> Self { + self.set_block_align(block_align); + self + } + + /// The alignment of inline-level elements inside of this block-level element. + pub fn inline_align(&self) -> Option { + self.inner.get_layout(LayoutAttr::INLINE_ALIGN).map(LayoutAttr::unwrap_inline_align) + } + + /// Set the alignment of inline-level elements inside of this block-level element. + pub fn set_inline_align(&mut self, inline_align: Option) { + self.inner.set_or_remove_layout(LayoutAttr::INLINE_ALIGN, inline_align.map(LayoutAttr::InlineAlign)); + } + + /// Set the alignment of inline-level elements inside of this block-level element. + pub fn with_inline_align(mut self, inline_align: Option) -> Self { + self.set_inline_align(inline_align); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::THead(value) + } +} +impl Tag { + /// A table header row group. + #[allow(non_upper_case_globals)] + pub const THead: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::TBody(value) + } +} +impl Tag { + /// A table data row group. + #[allow(non_upper_case_globals)] + pub const TBody: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::TFoot(value) + } +} +impl Tag { + /// A table footer row group. + #[allow(non_upper_case_globals)] + pub const TFoot: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Span(value) + } +} +impl Tag { + /// An inline-level element that does not have a specific meaning. + #[allow(non_upper_case_globals)] + pub const Span: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::InlineQuote(value) + } +} +impl Tag { + /// An inline quotation. + #[allow(non_upper_case_globals)] + pub const InlineQuote: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Note(value) + } +} +impl Tag { + /// A foot- or endnote, potentially referred to from within the text. + /// + /// **Best practice**: It may have a label as a child. + #[allow(non_upper_case_globals)] + pub const Note: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Reference(value) + } +} +impl Tag { + /// A reference to elsewhere in the document. + /// + /// **Best practice**: The first child of a tag group with this tag should be a link annotation + /// linking to a destination in the document, and the second child should consist of + /// the children that should be associated with that reference. + #[allow(non_upper_case_globals)] + pub const Reference: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::BibEntry(value) + } +} +impl Tag { + /// A reference to the external source of some cited document. + /// + /// **Best practice**: It may have a label as a child. + #[allow(non_upper_case_globals)] + pub const BibEntry: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Code(value) + } +} +impl Tag { + /// Computer code. + #[allow(non_upper_case_globals)] + pub const Code: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Link(value) + } +} +impl Tag { + /// A link. + /// + /// **Best practice**: The first child of a tag group with this tag should be a link annotation + /// linking to an URL, and the second child should consist of the children that should + /// be associated with that link. + #[allow(non_upper_case_globals)] + pub const Link: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Annot(value) + } +} +impl Tag { + /// An association between an annotation and the content it belongs to. PDF + /// + /// **Best practice**: Should be used for all annotations, except for link annotations and + /// widget annotations. The first child should be the identifier of a non-link annotation, + /// and all other subsequent children should be content identifiers associated with that + /// annotation. + #[allow(non_upper_case_globals)] + pub const Annot: Tag = Tag::new(); + + /// The spacing before the block-level element. + pub fn space_before(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_BEFORE).map(LayoutAttr::unwrap_space_before) + } + + /// Set the spacing before the block-level element. + pub fn set_space_before(&mut self, space_before: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_BEFORE, space_before.map(LayoutAttr::SpaceBefore)); + } + + /// Set the spacing before the block-level element. + pub fn with_space_before(mut self, space_before: Option) -> Self { + self.set_space_before(space_before); + self + } + + /// The spacing after the block-level element. + pub fn space_after(&self) -> Option { + self.inner.get_layout(LayoutAttr::SPACE_AFTER).map(LayoutAttr::unwrap_space_after) + } + + /// Set the spacing after the block-level element. + pub fn set_space_after(&mut self, space_after: Option) { + self.inner.set_or_remove_layout(LayoutAttr::SPACE_AFTER, space_after.map(LayoutAttr::SpaceAfter)); + } + + /// Set the spacing after the block-level element. + pub fn with_space_after(mut self, space_after: Option) -> Self { + self.set_space_after(space_after); + self + } + + /// The spacing between the start inline edge of the element and the parent. + pub fn start_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::START_INDENT).map(LayoutAttr::unwrap_start_indent) + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn set_start_indent(&mut self, start_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::START_INDENT, start_indent.map(LayoutAttr::StartIndent)); + } + + /// Set the spacing between the start inline edge of the element and the parent. + pub fn with_start_indent(mut self, start_indent: Option) -> Self { + self.set_start_indent(start_indent); + self + } + + /// The spacing between the end inline edge of the element and the parent. + pub fn end_indent(&self) -> Option { + self.inner.get_layout(LayoutAttr::END_INDENT).map(LayoutAttr::unwrap_end_indent) + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn set_end_indent(&mut self, end_indent: Option) { + self.inner.set_or_remove_layout(LayoutAttr::END_INDENT, end_indent.map(LayoutAttr::EndIndent)); + } + + /// Set the spacing between the end inline edge of the element and the parent. + pub fn with_end_indent(mut self, end_indent: Option) -> Self { + self.set_end_indent(end_indent); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Figure(value) + } +} +impl Tag { + /// Item of graphical content. + /// + /// Providing `alt_text` is required in some export modes, like for example PDF/UA-1. + #[allow(non_snake_case)] + pub fn Figure(alt_text: Option) -> Tag { + let mut tag = Tag::new(); + tag.set_alt_text(alt_text); + tag + } + + /// The bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn bbox(&self) -> Option { + self.inner.get_layout(LayoutAttr::B_BOX).map(LayoutAttr::unwrap_bbox) + } + + /// Set the bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn set_bbox(&mut self, bbox: Option) { + self.inner.set_or_remove_layout(LayoutAttr::B_BOX, bbox.map(LayoutAttr::BBox)); + } + + /// Set the bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn with_bbox(mut self, bbox: Option) -> Self { + self.set_bbox(bbox); + self + } + + /// The width. + pub fn width(&self) -> Option { + self.inner.get_layout(LayoutAttr::WIDTH).map(LayoutAttr::unwrap_width) + } + + /// Set the width. + pub fn set_width(&mut self, width: Option) { + self.inner.set_or_remove_layout(LayoutAttr::WIDTH, width.map(LayoutAttr::Width)); + } + + /// Set the width. + pub fn with_width(mut self, width: Option) -> Self { + self.set_width(width); + self + } + + /// The height. + pub fn height(&self) -> Option { + self.inner.get_layout(LayoutAttr::HEIGHT).map(LayoutAttr::unwrap_height) + } + + /// Set the height. + pub fn set_height(&mut self, height: Option) { + self.inner.set_or_remove_layout(LayoutAttr::HEIGHT, height.map(LayoutAttr::Height)); + } + + /// Set the height. + pub fn with_height(mut self, height: Option) -> Self { + self.set_height(height); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Formula(value) + } +} +impl Tag { + /// A mathematical formula. + /// + /// Providing `alt_text` is required in some export modes, like for example PDF/UA-1. + #[allow(non_snake_case)] + pub fn Formula(alt_text: Option) -> Tag { + let mut tag = Tag::new(); + tag.set_alt_text(alt_text); + tag + } + + /// The bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn bbox(&self) -> Option { + self.inner.get_layout(LayoutAttr::B_BOX).map(LayoutAttr::unwrap_bbox) + } + + /// Set the bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn set_bbox(&mut self, bbox: Option) { + self.inner.set_or_remove_layout(LayoutAttr::B_BOX, bbox.map(LayoutAttr::BBox)); + } + + /// Set the bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + pub fn with_bbox(mut self, bbox: Option) -> Self { + self.set_bbox(bbox); + self + } + + /// The width. + pub fn width(&self) -> Option { + self.inner.get_layout(LayoutAttr::WIDTH).map(LayoutAttr::unwrap_width) + } + + /// Set the width. + pub fn set_width(&mut self, width: Option) { + self.inner.set_or_remove_layout(LayoutAttr::WIDTH, width.map(LayoutAttr::Width)); + } + + /// Set the width. + pub fn with_width(mut self, width: Option) -> Self { + self.set_width(width); + self + } + + /// The height. + pub fn height(&self) -> Option { + self.inner.get_layout(LayoutAttr::HEIGHT).map(LayoutAttr::unwrap_height) + } + + /// Set the height. + pub fn set_height(&mut self, height: Option) { + self.inner.set_or_remove_layout(LayoutAttr::HEIGHT, height.map(LayoutAttr::Height)); + } + + /// Set the height. + pub fn with_height(mut self, height: Option) -> Self { + self.set_height(height); + self + } +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Form(value) + } +} +impl Tag { + /// An interactive form field. + #[allow(non_upper_case_globals)] + pub const Form: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::NonStruct(value) + } +} +impl Tag { + /// Non-structural element. A grouping element having no inherent structural significance; + /// it serves solely for grouping purposes. + #[allow(non_upper_case_globals)] + pub const NonStruct: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Datetime(value) + } +} +impl Tag { + /// A date or time. + #[allow(non_upper_case_globals)] + pub const Datetime: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Terms(value) + } +} +impl Tag { + /// A list of terms. + #[allow(non_upper_case_globals)] + pub const Terms: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Title(value) + } +} +impl Tag { + /// A title. + #[allow(non_upper_case_globals)] + pub const Title: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Strong(value) + } +} +impl Tag { + /// Encloses content with strong importance, most commonly **bold** text. + #[allow(non_upper_case_globals)] + pub const Strong: Tag = Tag::new(); +} + +impl From> for TagKind { + fn from(value: Tag) -> Self { + Self::Em(value) + } +} +impl Tag { + /// Encloses content that is emphasized, most commonly *italic* text. + #[allow(non_upper_case_globals)] + pub const Em: Tag = Tag::new(); +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Attr { + Struct(StructAttr), + List(ListAttr), + Table(TableAttr), + Layout(LayoutAttr), +} + +impl Attr { + + #[inline(always)] + fn unwrap_struct(&self) -> &StructAttr { + match self { + Self::Struct(attr) => attr, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_list(&self) -> &ListAttr { + match self { + Self::List(attr) => attr, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_table(&self) -> &TableAttr { + match self { + Self::Table(attr) => attr, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_layout(&self) -> &LayoutAttr { + match self { + Self::Layout(attr) => attr, + _ => unreachable!(), + } + } +} +impl Ordinal for Attr { + fn ordinal(&self) -> usize { + match self { + Self::Struct(a) => a.ordinal(), + Self::List(a) => a.ordinal(), + Self::Table(a) => a.ordinal(), + Self::Layout(a) => a.ordinal(), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum StructAttr { + /// The tag id. + Id(TagId), + /// The language of this tag. + Lang(String), + /// The optional alternate text that describes the text (for example, if the text + /// consists of a star symbol, the alt text should describe that in natural language). + AltText(String), + /// The expanded form of an abbreviation. + /// Only applicable if the content of the tag is an abbreviation. + Expanded(String), + /// The actual text represented by the content of this tag, i.e. if it contained + /// some curves that artistically represent some word. This should be the exact + /// replacement text of the word. + ActualText(String), + /// The title, characterizing a specific tag such as `"Chapter 1"`. + Title(String), + /// The heading level + HeadingLevel(NonZeroU16), +} + +impl StructAttr { + pub(crate) const ID: usize = 0; + pub(crate) const LANG: usize = 1; + pub(crate) const ALT_TEXT: usize = 2; + pub(crate) const EXPANDED: usize = 3; + pub(crate) const ACTUAL_TEXT: usize = 4; + pub(crate) const TITLE: usize = 5; + pub(crate) const HEADING_LEVEL: usize = 6; + + #[inline(always)] + fn unwrap_id(&self) -> &TagId { + match self { + Self::Id(val) => val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_lang(&self) -> &str { + match self { + Self::Lang(val) => val.as_ref(), + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_alt_text(&self) -> &str { + match self { + Self::AltText(val) => val.as_ref(), + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_expanded(&self) -> &str { + match self { + Self::Expanded(val) => val.as_ref(), + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_actual_text(&self) -> &str { + match self { + Self::ActualText(val) => val.as_ref(), + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_title(&self) -> &str { + match self { + Self::Title(val) => val.as_ref(), + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_level(&self) -> NonZeroU16 { + match self { + Self::HeadingLevel(val) => *val, + _ => unreachable!(), + } + } +} + +impl Ordinal for StructAttr { + fn ordinal(&self) -> usize { + match self { + Self::Id(_) => Self::ID, + Self::Lang(_) => Self::LANG, + Self::AltText(_) => Self::ALT_TEXT, + Self::Expanded(_) => Self::EXPANDED, + Self::ActualText(_) => Self::ACTUAL_TEXT, + Self::Title(_) => Self::TITLE, + Self::HeadingLevel(_) => Self::HEADING_LEVEL, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum ListAttr { + /// The list numbering. + Numbering(ListNumbering), +} + +impl ListAttr { + pub(crate) const NUMBERING: usize = 7; + + #[inline(always)] + fn unwrap_numbering(&self) -> ListNumbering { + match self { + Self::Numbering(val) => *val, + } + } +} + +impl Ordinal for ListAttr { + fn ordinal(&self) -> usize { + match self { + Self::Numbering(_) => Self::NUMBERING, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum TableAttr { + /// The table summary. + Summary(String), + /// The table header scope. + HeaderScope(TableHeaderScope), + /// The list of headers associated with a table cell. + /// Table data cells (`TD`) may specify a list of table headers (`TH`), + /// which can also specify a list of parent header cells (`TH`), and so on. + /// To determine the list of associated headers this list is recursively + /// evaluated. + /// + /// This allows specifying header hierarchies inside tables. + CellHeaders(SmallVec<[TagId; 1]>), + /// The row span of this table cell. + RowSpan(NonZeroU32), + /// The column span of this table cell. + ColSpan(NonZeroU32), +} + +impl TableAttr { + pub(crate) const SUMMARY: usize = 8; + pub(crate) const HEADER_SCOPE: usize = 9; + pub(crate) const CELL_HEADERS: usize = 10; + pub(crate) const ROW_SPAN: usize = 11; + pub(crate) const COL_SPAN: usize = 12; + + #[inline(always)] + fn unwrap_summary(&self) -> &str { + match self { + Self::Summary(val) => val.as_ref(), + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_scope(&self) -> TableHeaderScope { + match self { + Self::HeaderScope(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_headers(&self) -> &[TagId] { + match self { + Self::CellHeaders(val) => val.as_ref(), + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_row_span(&self) -> NonZeroU32 { + match self { + Self::RowSpan(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_col_span(&self) -> NonZeroU32 { + match self { + Self::ColSpan(val) => *val, + _ => unreachable!(), + } + } +} + +impl Ordinal for TableAttr { + fn ordinal(&self) -> usize { + match self { + Self::Summary(_) => Self::SUMMARY, + Self::HeaderScope(_) => Self::HEADER_SCOPE, + Self::CellHeaders(_) => Self::CELL_HEADERS, + Self::RowSpan(_) => Self::ROW_SPAN, + Self::ColSpan(_) => Self::COL_SPAN, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum LayoutAttr { + /// The placement. + Placement(Placement), + /// The writing mode. + WritingMode(WritingMode), + /// The bounding box of a tag that encloses its visible content. + /// If the content spans multiple pages, this should be omitted. + BBox(BBox), + /// The width. + Width(f32), + /// The height. + Height(f32), + /// The background color. + BackgroundColor(NaiveRgbColor), + /// The border color. + BorderColor(Sides), + /// The way the border is drawn. + BorderStyle(Sides), + /// The border width. + BorderThickness(Sides), + /// The padding inside of an element. + Padding(Sides), + /// The color of text, borders, and text decorations. + Color(NaiveRgbColor), + /// The spacing before the block-level element. + SpaceBefore(f32), + /// The spacing after the block-level element. + SpaceAfter(f32), + /// The spacing between the start inline edge of the element and the parent. + StartIndent(f32), + /// The spacing between the end inline edge of the element and the parent. + EndIndent(f32), + /// The amount the first line of text in a block-level element is indented. Only + /// applicable to paragraph-like elements with non-block-level elements. + TextIndent(f32), + /// The text alignment. + TextAlign(TextAlign), + /// The alignment of block-level elements inside of this block-level element. + BlockAlign(BlockAlign), + /// The alignment of inline-level elements inside of this block-level element. + InlineAlign(InlineAlign), + /// The border style of table cells, overriding `BorderStyle`. + TableBorderStyle(Sides), + /// The padding inside of table cells, overriding `Padding`. + TablePadding(Sides), + /// The distance by which the baseline shall be shifted from the default position. + BaselineShift(f32), + /// The height of each line in an element on the block axis. + LineHeight(LineHeight), + /// The color of the text decoration, overriding the fill color. + TextDecorationColor(NaiveRgbColor), + /// The width of the text decoration line. + TextDecorationThickness(f32), + /// The kind of text decoration. + TextDecorationType(TextDecorationType), + /// How the glyphs are rotated in a vertical writing mode. + GlyphOrientationVertical(GlyphOrientationVertical), + /// The number of columns in the grouping element. + ColumnCount(NonZeroU32), + /// The width of the gaps between columns in the grouping element. + ColumnGap(ColumnDimensions), + /// The width of the columns in the grouping element. + ColumnWidths(ColumnDimensions), +} + +impl LayoutAttr { + pub(crate) const PLACEMENT: usize = 13; + pub(crate) const WRITING_MODE: usize = 14; + pub(crate) const B_BOX: usize = 15; + pub(crate) const WIDTH: usize = 16; + pub(crate) const HEIGHT: usize = 17; + pub(crate) const BACKGROUND_COLOR: usize = 18; + pub(crate) const BORDER_COLOR: usize = 19; + pub(crate) const BORDER_STYLE: usize = 20; + pub(crate) const BORDER_THICKNESS: usize = 21; + pub(crate) const PADDING: usize = 22; + pub(crate) const COLOR: usize = 23; + pub(crate) const SPACE_BEFORE: usize = 24; + pub(crate) const SPACE_AFTER: usize = 25; + pub(crate) const START_INDENT: usize = 26; + pub(crate) const END_INDENT: usize = 27; + pub(crate) const TEXT_INDENT: usize = 28; + pub(crate) const TEXT_ALIGN: usize = 29; + pub(crate) const BLOCK_ALIGN: usize = 30; + pub(crate) const INLINE_ALIGN: usize = 31; + pub(crate) const TABLE_BORDER_STYLE: usize = 32; + pub(crate) const TABLE_PADDING: usize = 33; + pub(crate) const BASELINE_SHIFT: usize = 34; + pub(crate) const LINE_HEIGHT: usize = 35; + pub(crate) const TEXT_DECORATION_COLOR: usize = 36; + pub(crate) const TEXT_DECORATION_THICKNESS: usize = 37; + pub(crate) const TEXT_DECORATION_TYPE: usize = 38; + pub(crate) const GLYPH_ORIENTATION_VERTICAL: usize = 39; + pub(crate) const COLUMN_COUNT: usize = 40; + pub(crate) const COLUMN_GAP: usize = 41; + pub(crate) const COLUMN_WIDTHS: usize = 42; + + #[inline(always)] + fn unwrap_placement(&self) -> Placement { + match self { + Self::Placement(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_writing_mode(&self) -> WritingMode { + match self { + Self::WritingMode(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_bbox(&self) -> BBox { + match self { + Self::BBox(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_width(&self) -> f32 { + match self { + Self::Width(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_height(&self) -> f32 { + match self { + Self::Height(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_background_color(&self) -> NaiveRgbColor { + match self { + Self::BackgroundColor(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_border_color(&self) -> Sides { + match self { + Self::BorderColor(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_border_style(&self) -> Sides { + match self { + Self::BorderStyle(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_border_thickness(&self) -> Sides { + match self { + Self::BorderThickness(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_padding(&self) -> Sides { + match self { + Self::Padding(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_color(&self) -> NaiveRgbColor { + match self { + Self::Color(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_space_before(&self) -> f32 { + match self { + Self::SpaceBefore(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_space_after(&self) -> f32 { + match self { + Self::SpaceAfter(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_start_indent(&self) -> f32 { + match self { + Self::StartIndent(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_end_indent(&self) -> f32 { + match self { + Self::EndIndent(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_text_indent(&self) -> f32 { + match self { + Self::TextIndent(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_text_align(&self) -> TextAlign { + match self { + Self::TextAlign(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_block_align(&self) -> BlockAlign { + match self { + Self::BlockAlign(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_inline_align(&self) -> InlineAlign { + match self { + Self::InlineAlign(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_table_border_style(&self) -> Sides { + match self { + Self::TableBorderStyle(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_table_padding(&self) -> Sides { + match self { + Self::TablePadding(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_baseline_shift(&self) -> f32 { + match self { + Self::BaselineShift(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_line_height(&self) -> LineHeight { + match self { + Self::LineHeight(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_text_decoration_color(&self) -> NaiveRgbColor { + match self { + Self::TextDecorationColor(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_text_decoration_thickness(&self) -> f32 { + match self { + Self::TextDecorationThickness(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_text_decoration_type(&self) -> TextDecorationType { + match self { + Self::TextDecorationType(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_glyph_orientation_vertical(&self) -> GlyphOrientationVertical { + match self { + Self::GlyphOrientationVertical(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_column_count(&self) -> NonZeroU32 { + match self { + Self::ColumnCount(val) => *val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_column_gap(&self) -> &ColumnDimensions { + match self { + Self::ColumnGap(val) => val, + _ => unreachable!(), + } + } + + #[inline(always)] + fn unwrap_column_widths(&self) -> &ColumnDimensions { + match self { + Self::ColumnWidths(val) => val, + _ => unreachable!(), + } + } +} + +impl Ordinal for LayoutAttr { + fn ordinal(&self) -> usize { + match self { + Self::Placement(_) => Self::PLACEMENT, + Self::WritingMode(_) => Self::WRITING_MODE, + Self::BBox(_) => Self::B_BOX, + Self::Width(_) => Self::WIDTH, + Self::Height(_) => Self::HEIGHT, + Self::BackgroundColor(_) => Self::BACKGROUND_COLOR, + Self::BorderColor(_) => Self::BORDER_COLOR, + Self::BorderStyle(_) => Self::BORDER_STYLE, + Self::BorderThickness(_) => Self::BORDER_THICKNESS, + Self::Padding(_) => Self::PADDING, + Self::Color(_) => Self::COLOR, + Self::SpaceBefore(_) => Self::SPACE_BEFORE, + Self::SpaceAfter(_) => Self::SPACE_AFTER, + Self::StartIndent(_) => Self::START_INDENT, + Self::EndIndent(_) => Self::END_INDENT, + Self::TextIndent(_) => Self::TEXT_INDENT, + Self::TextAlign(_) => Self::TEXT_ALIGN, + Self::BlockAlign(_) => Self::BLOCK_ALIGN, + Self::InlineAlign(_) => Self::INLINE_ALIGN, + Self::TableBorderStyle(_) => Self::TABLE_BORDER_STYLE, + Self::TablePadding(_) => Self::TABLE_PADDING, + Self::BaselineShift(_) => Self::BASELINE_SHIFT, + Self::LineHeight(_) => Self::LINE_HEIGHT, + Self::TextDecorationColor(_) => Self::TEXT_DECORATION_COLOR, + Self::TextDecorationThickness(_) => Self::TEXT_DECORATION_THICKNESS, + Self::TextDecorationType(_) => Self::TEXT_DECORATION_TYPE, + Self::GlyphOrientationVertical(_) => Self::GLYPH_ORIENTATION_VERTICAL, + Self::ColumnCount(_) => Self::COLUMN_COUNT, + Self::ColumnGap(_) => Self::COLUMN_GAP, + Self::ColumnWidths(_) => Self::COLUMN_WIDTHS, + } + } +} + diff --git a/vendor/krilla/src/interchange/tagging/mod.rs b/vendor/krilla/src/interchange/tagging/mod.rs new file mode 100644 index 00000000..60962284 --- /dev/null +++ b/vendor/krilla/src/interchange/tagging/mod.rs @@ -0,0 +1,1324 @@ +//! Creating accessible PDF documents. +//! +//! # Introduction +//! +//! A document usually consists of many smaller semantic building blocks, like for example +//! titles, headings, paragraphs, tables, headers, footers, and so on. However, it is important +//! to understand that, by default, when exporting a document to PDF, all of this semantic +//! information is lost. +//! +//! By default, a PDF document doesn't have a notion of a table or a +//! paragraph. Instead, it consists of very low-level instructions, such as "draw +//! a path at that location" or "draw a line of glyphs at that font size". A table is +//! not encoded as a "table", but instead as a number of rectangle-like paths that happen +//! to surround lines of text. This is what made PDF popular in the first place, as encoding +//! information at such a low level allows to ensure a consistent viewing experience across +//! different platforms and viewers. +//! +//! However, this design has drawbacks, one of the main ones being that it leads to the +//! production of non-accessible documents. Especially in recent years, ensuring the +//! accessibility of documents has become an increasingly important requirement. +//! To address this deficiency, PDF introduced the notion of "tagged PDF", which consists +//! of enriching PDF documents with additional semantic information in a way that can be +//! interpreted by different consumers. krilla supports the creation of such documents. +//! +//! # A word on krilla's implementation +//! +//! As nearly everything in PDF, tagging is a really complex topic, and getting it right +//! is very hard. Because of this, in line with the general philosophy of krilla, some +//! of the potential capabilities of tagged PDF are not directly supported. Instead, only +//! a specific subset has been implemented, with a focus on features that improve the +//! accessibility of documents. +//! +//! Please note that doing tagging 100% correctly as recommended by various PDF standards is very +//! difficult. While the documentation lists many of the recommendations to abstract away +//! as much as possible, users are still expected to consult the given specifications to +//! ensure their implementation matches the specification. A "reference implementation" that +//! could be good to consult is the [`typst-pdf`](https://github.com/typst/typst/tree/main/crates/typst-pdf) +//! crate which implements most of the expected features for well-tagged PDFs. +//! +//! # Basic Principles +//! +//! The way tagged PDFs are created is by attaching a tag tree to the PDF document +//! that encodes the logical structure of the document. As mentioned above, a raw PDF file +//! mainly consists of text- and path-drawing primitives, which are not necessarily +//! drawn in the logical reading order of the document. What the tag tree does is +//! maping the different "snippets" of the PDF file to the tree-like structure in a way +//! that reflects the logical structure of the document, in reading-order. +//! +//! For example, a document can consist of multiple "sections", where each section might contain +//! headings, paragraphs or figures. A figure might consist of a table as well as a caption. +//! A table consists yet again of smaller semantic components, like a header, footer +//! and the data cells, which usually contain some text. These kinds of hierarchical structures +//! can be encoded with the help of tagging. +//! +//! A tag tree consists of two components: +//! - Group nodes, which represents a component with certain semantics. A group node must have +//! at least one child, otherwise it's discarded. +//! - Leaf nodes, which represent the actual pieces on the page that form part of a group. +//! +//! # How to create a tagged document +//! +//! If you want to create a tagged document, you need to follow the following steps: +//! +//! 1) Ensure that you activate the `enable_tagging` attribute in [`SerializeSettings`]. +//! 2) Create a [tag tree](TagTree), which represents the "root" of a tag tree. +//! 3) As you create your document, create new [tag groups](TagGroup) with corresponding [tags](Tag). +//! Nest them with other tag groups, if necessary, by using the `push` method. +//! 4) Populate tag groups with [identifiers](Identifier), which represent the leaf nodes +//! in the tag tree. Identifiers are unique and point to a sequence of content on the +//! page. If you push an identifier to a tag group, then all content that is marked by +//! that identifier belongs semantically to that tag group. There are currently two ways +//! of obtaining an identifier: +//! +//! - Use the `add_tagged_annotation` method on [`Page`], which allows you to associate +//! annotations to the content they correspond to. Currently, krilla only supports link +//! annotations, and a link annotation should always be a child in a tag group with the +//! [`TagKind`] [`Link`](TagKind::Link), with its sibling being an identifier or another tag group that is +//! to be associated with the link. +//! - Use the `start_tagged` command on [`Surface`], which returns an [Identifier], and +//! indicates that all content drawn on the surface should be associated with that +//! identifier, until you call the `end_tagged` method. *Important*: Note that you cannot +//! nest calls to `start_tagged`, and you have to ensure that you always call a corresponding +//! `end_tagged`. Otherwise, krilla will panic. +//! +//! It is very important that each identifier you create has exactly one parent in the tag +//! tree. This means that you cannot create an identifier and not use it at all (0 parents), +//! or use the same identifier in two different parts of the tree (1+ parents). Otherwise, +//! export will fail. +//! +//! 5) Once you have built your tag tree, simply call `set_tag_tree` on [`Document`]. That's it! +//! +//! # Other notes +//! +//! Make sure that you carefully read the documentation of the other parts of this module, as +//! there are some more points as well as best practices you need to keep in mind +//! when creating well-tagged documents. The PDF specification is in some places very vague +//! on how a tagged document should look like, so there is quite a bit of ambiguity. +//! +//! Apart from that, the PDF specification does make a few statements on requirements a well-tagged PDF +//! should follows, although those are not really "strict" requirements in the sense that they can +//! be automatically checked by a PDF validator, so not conforming to some of those points does not +//! suddenly make your document a badly-tagged document! However, if possible, you should still +//! try to comply with the following requirements: +//! +//! - In general, all contents in your file should be tagged, either as an artifact or with +//! Span/Other. +//! - The order of elements in the tag tree should represent the logical reading order, including +//! annotations. +//! - Word breaks in text should be represented explicitly with spaces, instead of implicitly +//! by not including them, but instead positioning text in a way that "simulates" the spaces. +//! - Hyphenation should be represented as a soft hyphen character (U+00AD) instead +//! of a hard hyphen (U+002D). +//! - Tag groups should follow the best-practice of what kind of children they contain. See +//! [`TagKind`] for more information. +//! - You should provide "Alt" descriptions for formulas and images. +//! - In case you have a link annotation that applies to text that wraps into one or multiple +//! new lines, you should use the `quad_points` functionality to indicate the exact covered +//! areas of the link. +//! +//! Once again, the above is only a best-effort summary, if you are interested in creating +//! completely well-tagged PDFs, you are advised to consult the given specifications. +//! +//! [`SerializeSettings`]: crate::SerializeSettings +//! [`Page`]: crate::page::Page +//! [`Surface`]: crate::surface::Surface +//! [`Document`]: crate::Document + +use std::cell::LazyCell; +use std::cmp::PartialEq; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap, HashMap}; +use std::io::Write as _; + +use pdf_writer::types::{RoleMapOpts, StructRole, StructRole2}; +use pdf_writer::writers::{PropertyList, StructElement}; +use pdf_writer::{Chunk, Finish, Name, Ref, Str, TextStr}; +use smallvec::SmallVec; + +use crate::chunk_container::ChunkContainer; +use crate::configure::validate::VersionedFeature; +use crate::configure::{PdfVersion, ValidationError}; +use crate::error::{KrillaError, KrillaResult}; +use crate::geom::Rect; +use crate::page::page_root_transform; +use crate::serialize::SerializeContext; + +pub use tag::*; + +pub mod fmt; +mod tag; + +/// An artifact that should not be part of the accessible structure. +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub struct Artifact { + /// The type of the artifact. + pub kind: ArtifactType, + /// The bounding box that incloses the artifact's visual content. Required + /// for background artifacts. + pub bbox: Option, +} + +impl Artifact { + /// Create a new artifact with a type and an optional BBox. + /// + /// You must provide a bounding box if `kind` is + /// [`ArtifactType::Background`] and you are targetting PDF 1.7, otherwise, + /// Krilla will later panic. + pub fn new(kind: ArtifactType, bbox: Option) -> Self { + Self { kind, bbox } + } + + /// Create a new artifact with a type and no bounding box. + pub fn with_kind(kind: ArtifactType) -> Self { + Self::new(kind, None) + } + + /// Whether the artifacts requires a property list. + pub(crate) fn requires_properties(self, pdf_version: PdfVersion) -> bool { + self.bbox.is_some() + || self + .kind + .map_pdf_version(pdf_version) + .to_pdf_artifact_type() + .is_some() + } + + /// Whether the artifact requires a BBox. + pub(crate) fn requires_bbox(self, pdf_version: PdfVersion) -> bool { + // Background artifacts were introduced in PDF 1.7. In table 330, the + // standard makes bounding boxes mandatory for background artifacts + // only. PDF 2.0 lifts this requirement in table 363. + // + // When changing the below condition, also change the error at the + // callsite. + self.kind == ArtifactType::Background && pdf_version == PdfVersion::Pdf17 + } +} + +/// A type of artifact. +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub enum ArtifactType { + /// The header of a page. + Header, + /// The footer of the page. + Footer, + /// For text in the back- or foreground of all pages. + Watermark, + /// Page numbers. + PageNumber, + /// Numbering artifacts before lines. + LineNumber, + /// Areas where there formerly was content, but which has been removed. + Redaction, + /// Bates numbering. + Bates, + /// Other artifacts arising from pagination not covered by the above variants. + PaginationOther, + /// Purely cosmetic typographical or design elements. + Layout, + /// Page artifacts, such as for example cut marks or color bars. + Page, + /// The background of a page or a graphical element. + Background, + /// Any other type of artifact. + #[default] + Other, +} + +impl ArtifactType { + pub(crate) fn map_pdf_version(self, version: PdfVersion) -> Self { + match self { + Self::PageNumber | Self::LineNumber | Self::Redaction | Self::Bates + if version < PdfVersion::Pdf20 => + { + ArtifactType::PaginationOther + } + Self::Header | Self::Footer | Self::Watermark if version < PdfVersion::Pdf17 => { + ArtifactType::PaginationOther + } + Self::Background if version < PdfVersion::Pdf17 => ArtifactType::Other, + _ => self, + } + } + + pub(crate) fn to_pdf_artifact_type(self) -> Option { + match self { + ArtifactType::Header => Some(pdf_writer::types::ArtifactType::Pagination), + ArtifactType::Footer => Some(pdf_writer::types::ArtifactType::Pagination), + ArtifactType::Watermark => Some(pdf_writer::types::ArtifactType::Pagination), + ArtifactType::PageNumber => Some(pdf_writer::types::ArtifactType::Pagination), + ArtifactType::LineNumber => Some(pdf_writer::types::ArtifactType::Pagination), + ArtifactType::Redaction => Some(pdf_writer::types::ArtifactType::Pagination), + ArtifactType::Bates => Some(pdf_writer::types::ArtifactType::Pagination), + ArtifactType::PaginationOther => Some(pdf_writer::types::ArtifactType::Pagination), + ArtifactType::Layout => Some(pdf_writer::types::ArtifactType::Layout), + ArtifactType::Page => Some(pdf_writer::types::ArtifactType::Page), + ArtifactType::Background => Some(pdf_writer::types::ArtifactType::Background), + ArtifactType::Other => None, + } + } + + pub(crate) fn to_pdf_artifact_subtype( + self, + ) -> Option> { + match self { + ArtifactType::Header => Some(pdf_writer::types::ArtifactSubtype::Header), + ArtifactType::Footer => Some(pdf_writer::types::ArtifactSubtype::Footer), + ArtifactType::Watermark => Some(pdf_writer::types::ArtifactSubtype::Watermark), + ArtifactType::PageNumber => Some(pdf_writer::types::ArtifactSubtype::PageNumber), + ArtifactType::LineNumber => Some(pdf_writer::types::ArtifactSubtype::LineNumber), + ArtifactType::Redaction => Some(pdf_writer::types::ArtifactSubtype::Redaction), + ArtifactType::Bates => Some(pdf_writer::types::ArtifactSubtype::Bates), + _ => None, + } + } +} + +/// A language identifier as specified in RFC 3066. It will not be validated, so +/// it's on the user of the library to ensure the tag is valid. +pub type Lang<'a> = &'a str; +/// An alternate text that describes some element in natural language. +pub type Alt<'a> = &'a str; +/// The actual intended textual content of a span. For example, +/// if you have a hyphenated word, you can use `ActualText` to describe +/// the same word without hyphens. +pub type ActualText<'a> = &'a str; +/// The expanded form of an abbreviation. +pub type Expanded<'a> = &'a str; + +/// A content tag associated with the content it wraps. +#[derive(Clone, Copy, Debug)] +pub enum ContentTag<'a> { + /// Artifacts represent pieces of content that are not really part of the logical structure + /// of a document and should be excluded in the logical tree. These include for example headers, + /// footers, page background and similar. + Artifact(Artifact), + /// A content tag that wraps some text with specific properties. + /// + /// Spans should not be too long. At most, they should contain a single line of text, but they + /// can obviously be shorter, if text within a single line contains text with different styles + /// or different languages. + Span(SpanTag<'a>), + /// Use this tag for anything else that does not semantically fit into `Span` or `Artifact`. + /// This includes for example arbitrary paths, images or a mix of different content that cannot + /// be split up more. + Other, +} + +impl ContentTag<'_> { + pub(crate) fn name(&self) -> Name<'static> { + match self { + ContentTag::Artifact(_) => Name(b"Artifact"), + ContentTag::Span(_) => Name(b"Span"), + ContentTag::Other => Name(b"P"), + } + } + + pub(crate) fn write_properties( + &self, + sc: &mut SerializeContext, + mut properties: PropertyList, + page_height: f32, + ) { + match self { + ContentTag::Artifact(artifact) => { + let at = artifact + .kind + .map_pdf_version(sc.serialize_settings().pdf_version()); + let mut artifact_props = properties.artifact(); + + if let Some(bbox) = artifact.bbox { + let transform = page_root_transform(page_height); + let actual_rect = bbox.transform(transform).unwrap(); + artifact_props.bounding_box(actual_rect.to_pdf_rect()); + } + + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf17 { + if at == ArtifactType::Header { + artifact_props.attached([pdf_writer::types::ArtifactAttachment::Top]); + } + + if at == ArtifactType::Footer { + artifact_props.attached([pdf_writer::types::ArtifactAttachment::Bottom]); + } + + if let Some(subtype) = at.to_pdf_artifact_subtype() { + artifact_props.subtype(subtype); + } + } + + if let Some(artifact_type) = at.to_pdf_artifact_type() { + artifact_props.kind(artifact_type); + } + } + ContentTag::Span(SpanTag { + lang, + alt_text, + expanded, + actual_text, + }) => { + if let Some(lang) = lang { + properties.pair(Name(b"Lang"), TextStr(lang)); + } + + if let Some(alt) = alt_text { + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf15 { + properties.pair(Name(b"Alt"), TextStr(alt)); + } + } + + if let Some(exp) = expanded { + properties.pair(Name(b"E"), TextStr(exp)); + } + + if let Some(actual) = actual_text { + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf15 { + properties.actual_text(TextStr(actual)); + } + } + } + ContentTag::Other => {} + } + } +} + +/// A span tag. +#[derive(Clone, Copy, Debug)] +pub struct SpanTag<'a> { + /// The language of the text. + pub lang: Option>, + /// An optional alternate text that describes the text (for example, if the text consists + /// of a star symbol, the alt text should describe that in natural language). + pub alt_text: Option>, + /// If the content of the span is an abbreviation, the expanded form of the + /// abbreviation should be provided here. + pub expanded: Option>, + /// The actual text represented by the glyphs, i.e. if you have a hyphenated span + /// `row-`, then you can wrap it in an `ActualText` to remove the hyphenation + /// when copy-pasting. + pub actual_text: Option>, +} + +impl<'a> SpanTag<'a> { + /// An empty span tag. + pub fn empty() -> Self { + Self { + lang: None, + alt_text: None, + expanded: None, + actual_text: None, + } + } + + /// Sets [`SpanTag::lang`]. + pub fn with_lang(mut self, lang: Option<&'a str>) -> Self { + self.lang = lang; + self + } + + /// Sets [`SpanTag::alt_text`]. + pub fn with_alt_text(mut self, alt_text: Option<&'a str>) -> Self { + self.alt_text = alt_text; + self + } + + /// Sets [`SpanTag::expanded`]. + pub fn with_expanded(mut self, expanded: Option<&'a str>) -> Self { + self.expanded = expanded; + self + } + + /// Sets [`SpanTag::actual_text`]. + pub fn with_actual_text(mut self, actual_text: Option<&'a str>) -> Self { + self.actual_text = actual_text; + self + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct PageTagIdentifier { + pub(crate) page_index: usize, + pub(crate) mcid: i32, +} + +impl From for IdentifierType { + fn from(value: PageTagIdentifier) -> Self { + IdentifierType::PageIdentifier(value) + } +} + +impl From for Identifier { + fn from(value: PageTagIdentifier) -> Self { + Identifier(IdentifierInner::Real(value.into())) + } +} + +impl PageTagIdentifier { + pub(crate) fn new(page_index: usize, mcid: i32) -> Self { + Self { page_index, mcid } + } + + pub(crate) fn bump(&mut self) -> PageTagIdentifier { + let old = *self; + + self.mcid = self.mcid.checked_add(1).unwrap(); + + old + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub(crate) struct AnnotationIdentifier { + pub(crate) page_index: usize, + pub(crate) annot_index: usize, +} + +impl From for IdentifierType { + fn from(value: AnnotationIdentifier) -> Self { + IdentifierType::AnnotationIdentifier(value) + } +} + +impl From for Identifier { + fn from(value: AnnotationIdentifier) -> Self { + Identifier(IdentifierInner::Real(value.into())) + } +} + +impl AnnotationIdentifier { + pub fn new(page_index: usize, annot_index: usize) -> Self { + Self { + page_index, + annot_index, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum IdentifierType { + PageIdentifier(PageTagIdentifier), + AnnotationIdentifier(AnnotationIdentifier), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum IdentifierInner { + Real(IdentifierType), + Dummy, +} + +/// An identifier for an annotation or certain parts of page content. +/// +/// Need to be used as a leaf node in a tag tree. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct Identifier(pub(crate) IdentifierInner); + +impl Identifier { + pub(crate) fn new_annotation(page_index: usize, annot_index: usize) -> Self { + AnnotationIdentifier::new(page_index, annot_index).into() + } + + pub(crate) fn dummy() -> Self { + Self(IdentifierInner::Dummy) + } +} + +impl TagKind { + pub(crate) fn write_kind(&self, struct_elem: &mut StructElement, sc: &mut SerializeContext) { + let pdf_version = sc.serialize_settings().pdf_version(); + if pdf_version < self.minimum_version() { + // Fall back to P in case the tag is not supported with the current + // PDF version + struct_elem.kind(StructRole::P); + return; + } + + match self { + Self::Part(_) => write_kind_compat(sc, struct_elem, StructRole2::Part), + Self::Article(_) => write_kind_1_7(struct_elem, StructRole::Art), + Self::Section(_) => write_kind_compat(sc, struct_elem, StructRole2::Sect), + Self::Div(_) => write_kind_compat(sc, struct_elem, StructRole2::Div), + Self::BlockQuote(_) => write_kind_1_7(struct_elem, StructRole::BlockQuote), + Self::Caption(_) => write_kind_compat(sc, struct_elem, StructRole2::Caption), + Self::TOC(_) => write_kind_1_7(struct_elem, StructRole::TOC), + Self::TOCI(_) => write_kind_1_7(struct_elem, StructRole::TOCI), + Self::Index(_) => write_kind_1_7(struct_elem, StructRole::Index), + Self::P(_) => write_kind_compat(sc, struct_elem, StructRole2::P), + Self::L(_) => write_kind_compat(sc, struct_elem, StructRole2::L), + Self::LI(_) => write_kind_compat(sc, struct_elem, StructRole2::LI), + Self::Lbl(_) => write_kind_compat(sc, struct_elem, StructRole2::Lbl), + Self::LBody(_) => write_kind_compat(sc, struct_elem, StructRole2::LBody), + Self::Table(_) => write_kind_compat(sc, struct_elem, StructRole2::Table), + Self::TR(_) => write_kind_compat(sc, struct_elem, StructRole2::TR), + Self::TH(_) => write_kind_compat(sc, struct_elem, StructRole2::TH), + Self::TD(_) => write_kind_compat(sc, struct_elem, StructRole2::TD), + Self::THead(_) => write_kind_compat(sc, struct_elem, StructRole2::THead), + Self::TBody(_) => write_kind_compat(sc, struct_elem, StructRole2::TBody), + Self::TFoot(_) => write_kind_compat(sc, struct_elem, StructRole2::TFoot), + Self::Span(_) => write_kind_compat(sc, struct_elem, StructRole2::Span), + Self::InlineQuote(_) => write_kind_1_7(struct_elem, StructRole::Quote), + Self::Note(_) => write_kind_1_7(struct_elem, StructRole::Note), + Self::Reference(_) => write_kind_1_7(struct_elem, StructRole::Reference), + Self::BibEntry(_) => write_kind_1_7(struct_elem, StructRole::BibEntry), + Self::Code(_) => write_kind_1_7(struct_elem, StructRole::Code), + Self::Link(_) => write_kind_compat(sc, struct_elem, StructRole2::Link), + Self::Annot(_) => write_kind_compat(sc, struct_elem, StructRole2::Annot), + Self::Figure(_) => write_kind_compat(sc, struct_elem, StructRole2::Figure), + Self::Formula(_) => write_kind_compat(sc, struct_elem, StructRole2::Formula), + Self::Form(_) => write_kind_compat(sc, struct_elem, StructRole2::Form), + Self::NonStruct(_) => write_kind_compat(sc, struct_elem, StructRole2::NonStruct), + // Custom structure roles that are registered in the `RoleMap`. + Self::Datetime(_) => write_kind_custom(sc, struct_elem, Name(b"Datetime")), + Self::Terms(_) => write_kind_custom(sc, struct_elem, Name(b"Terms")), + Self::Title(_) => write_kind_custom(sc, struct_elem, Name(b"Title")), + // PDF 2.0 structure roles that are conditionally registered. + Self::Hn(tag) => { + let role2 = StructRole2::Heading(tag.level()); + if pdf_version < PdfVersion::Pdf20 { + // Dynamically register custom headings `Hn` if the level + // (`n >= 7`) isn't supported by PDF 1.7 and below. + let compat = role2.compatibility_1_7(RoleMapOpts::default()); + if compat.into_pdf_1_7().is_none() { + sc.global_objects.custom_heading_roles.insert(tag.level()); + } + struct_elem.custom_kind(role2.to_name(&mut [0; 6])); + } else { + struct_elem.kind_2(role2, sc.pdf2_ns.ssn_ref); + } + } + Self::Strong(_) => { + if pdf_version < PdfVersion::Pdf20 { + struct_elem.custom_kind(Name(b"Strong")); + } else { + struct_elem.kind_2(StructRole2::Strong, sc.pdf2_ns.ssn_ref); + } + } + Self::Em(_) => { + if pdf_version < PdfVersion::Pdf20 { + struct_elem.custom_kind(Name(b"Em")); + } else { + struct_elem.kind_2(StructRole2::Em, sc.pdf2_ns.ssn_ref); + } + } + }; + } + + pub(crate) fn minimum_version(&self) -> PdfVersion { + match self { + Self::Part(_) => PdfVersion::Pdf14, + Self::Article(_) => PdfVersion::Pdf14, + Self::Section(_) => PdfVersion::Pdf14, + Self::Div(_) => PdfVersion::Pdf14, + Self::BlockQuote(_) => PdfVersion::Pdf14, + Self::Caption(_) => PdfVersion::Pdf14, + Self::TOC(_) => PdfVersion::Pdf14, + Self::TOCI(_) => PdfVersion::Pdf14, + Self::Index(_) => PdfVersion::Pdf14, + Self::P(_) => PdfVersion::Pdf14, + Self::Hn(_) => PdfVersion::Pdf14, + Self::L(_) => PdfVersion::Pdf14, + Self::LI(_) => PdfVersion::Pdf14, + Self::Lbl(_) => PdfVersion::Pdf14, + Self::LBody(_) => PdfVersion::Pdf14, + Self::Table(_) => PdfVersion::Pdf14, + Self::TR(_) => PdfVersion::Pdf14, + Self::TH(_) => PdfVersion::Pdf14, + Self::TD(_) => PdfVersion::Pdf14, + // TODO: writing `P` tags in PDF 1.4 will break the table structure. + // Instead consider just transparently inserting all children, which + // should be `TR`s anyway. + Self::THead(_) => PdfVersion::Pdf15, + Self::TBody(_) => PdfVersion::Pdf15, + Self::TFoot(_) => PdfVersion::Pdf15, + Self::Span(_) => PdfVersion::Pdf14, + Self::InlineQuote(_) => PdfVersion::Pdf14, + Self::Note(_) => PdfVersion::Pdf14, + Self::Reference(_) => PdfVersion::Pdf14, + Self::BibEntry(_) => PdfVersion::Pdf14, + Self::Code(_) => PdfVersion::Pdf14, + Self::Link(_) => PdfVersion::Pdf14, + Self::Annot(_) => PdfVersion::Pdf15, + Self::Figure(_) => PdfVersion::Pdf14, + Self::Formula(_) => PdfVersion::Pdf14, + Self::Form(_) => PdfVersion::Pdf14, + Self::NonStruct(_) => PdfVersion::Pdf14, + Self::Datetime(_) => PdfVersion::Pdf14, + Self::Terms(_) => PdfVersion::Pdf14, + Self::Title(_) => PdfVersion::Pdf14, + Self::Strong(_) => PdfVersion::Pdf14, + Self::Em(_) => PdfVersion::Pdf14, + } + } + + pub(crate) fn should_have_alt(&self) -> bool { + matches!(self, TagKind::Figure(_) | TagKind::Formula(_)) + } + + pub(crate) fn can_have_title(&self) -> bool { + matches!(self, Self::Hn(_)) + } +} + +fn write_kind_1_7(struct_elem: &mut StructElement, role: StructRole) { + struct_elem.kind(role); +} + +/// If serializing a PDF 2.0 document, write a PDF 2.0 structure role, otherwise +/// fall back to the compatible PDF 1.7 role. +fn write_kind_compat( + sc: &mut SerializeContext, + struct_elem: &mut StructElement, + role: StructRole2, +) { + if sc.serialize_settings().pdf_version() < PdfVersion::Pdf20 { + let compat = role.compatibility_1_7(RoleMapOpts::default()); + struct_elem.kind(compat.role()); + } else { + struct_elem.kind_2(role, sc.pdf2_ns.ssn_ref); + } +} + +/// Write a custom role-mapped structure role. If serializing a PDF 2.0 document +/// also write the custom krilla namespace. +fn write_kind_custom(sc: &mut SerializeContext, struct_elem: &mut StructElement, name: Name) { + struct_elem.custom_kind(name); + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf20 { + struct_elem.namespace(sc.pdf2_ns.krilla_ref); + } +} + +/// A node in a tag tree. +#[derive(Debug, Clone, PartialEq)] +pub enum Node { + /// A group node. + Group(TagGroup), + /// A leaf node. + Leaf(Identifier), +} + +impl Node { + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + parent_tree_map: &mut HashMap, + id_tree: &mut BTreeMap, + parent: Ref, + note_id: &mut u32, + struct_elems: &mut Chunk, + ) -> KrillaResult> { + match self { + Node::Group(g) => Ok(Some(g.serialize( + sc, + parent_tree_map, + id_tree, + parent, + note_id, + struct_elems, + )?)), + Node::Leaf(ci) => match ci.0 { + IdentifierInner::Real(rci) => Ok(Some(Reference::ContentIdentifier(rci))), + IdentifierInner::Dummy => Ok(None), + }, + } + } +} + +impl From for Node { + fn from(value: TagGroup) -> Self { + Node::Group(value) + } +} + +impl From for Node { + fn from(value: Identifier) -> Self { + Node::Leaf(value) + } +} + +#[derive(Clone, Copy)] +pub(crate) enum Reference { + Ref(Ref), + ContentIdentifier(IdentifierType), +} + +/// A tag group. +#[derive(Debug, Clone, PartialEq)] +pub struct TagGroup { + /// The tag of the tag group. + pub tag: TagKind, + /// The children of the tag group. + pub children: Vec, +} + +impl TagGroup { + /// Create a new tag group with a specific tag. + pub fn new(tag: impl Into) -> Self { + Self { + tag: tag.into(), + children: vec![], + } + } + + /// Create a new tag group with a specific tag and a list of children. + pub fn with_children(tag: impl Into, children: Vec) -> Self { + Self { + tag: tag.into(), + children, + } + } + + /// Append a new child to the tag group. + pub fn push(&mut self, child: impl Into) { + self.children.push(child.into()) + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + parent_tree_map: &mut HashMap, + id_tree: &mut BTreeMap, + parent_ref: Ref, + note_id: &mut u32, + struct_elems: &mut Chunk, + ) -> KrillaResult { + let elem_ref = sc.new_ref(); + let mut children_refs = vec![]; + + for child in &self.children { + let serialized = child.serialize( + sc, + parent_tree_map, + id_tree, + elem_ref, + note_id, + struct_elems, + )?; + if let Some(ref_) = serialized { + children_refs.push(ref_); + } + } + + let mut struct_elem = struct_elems.struct_element(elem_ref); + self.tag.write_kind(&mut struct_elem, sc); + struct_elem.parent(parent_ref); + + let tag = self.tag.as_any(); + let pdf_version = sc.serialize_settings().pdf_version(); + + if let Some(id) = tag.id() { + match id_tree.entry(id.clone()) { + Entry::Vacant(vacant) => { + struct_elem.id(Str(id.as_bytes())); + vacant.insert(elem_ref); + } + Entry::Occupied(_) => { + return Err(KrillaError::DuplicateTagId(id.clone(), tag.location)); + } + } + } else if matches!(self.tag, TagKind::Note(_)) { + // Explicitly don't use `TagId::from_bytes` to disambiguate note IDs + // from user provided IDs. + let mut id = TagId(SmallVec::new()); + _ = write!(&mut id.0, "Note {note_id}"); + struct_elem.id(Str(id.as_bytes())); + id_tree.insert(id, elem_ref); + + *note_id += 1; + } + + if self.tag.can_have_title() && tag.title().is_none_or(str::is_empty) { + sc.register_validation_error(ValidationError::MissingHeadingTitle); + } + + if self.tag.should_have_alt() && tag.alt_text().is_none_or(str::is_empty) { + sc.register_validation_error(ValidationError::MissingAltText(tag.location)); + } + + // TODO: Once we have a generalized mechanism for validating tag trees, + // validate TH-outside-THead structure for PDF < 1.5. + + for attr in tag.attrs.iter() { + let Attr::Struct(attr) = attr else { + continue; + }; + match attr { + StructAttr::Id(_) => (), // Handled above + StructAttr::Title(title) => { + struct_elem.title(TextStr(title)); + } + StructAttr::Lang(lang) => { + if pdf_version >= PdfVersion::Pdf14 { + struct_elem.lang(TextStr(lang)); + } + } + StructAttr::AltText(alt) => { + struct_elem.alt(TextStr(alt)); + } + StructAttr::Expanded(expanded) => { + if pdf_version >= PdfVersion::Pdf15 { + struct_elem.expanded(TextStr(expanded)); + } + } + StructAttr::ActualText(actual_text) => { + if pdf_version >= PdfVersion::Pdf14 { + struct_elem.actual_text(TextStr(actual_text)); + } + } + + // Not really an attribute + StructAttr::HeadingLevel(_) => (), + } + } + + let mut attributes = LazyCell::new(|| struct_elem.attributes()); + + // Lazily initialize the list attributes to avoid an empty array. + let mut list_attributes = LazyCell::new(|| attributes.push().list()); + for attr in tag.attrs.iter() { + let Attr::List(attr) = attr else { + continue; + }; + match attr { + ListAttr::Numbering(numbering) => { + list_attributes.list_numbering(numbering.to_pdf()); + } + } + } + list_attributes.finish(); + + // Lazily initialize the table attributes to avoid an empty array. + let mut table_attributes = LazyCell::new(|| attributes.push().table()); + for attr in tag.attrs.iter() { + let Attr::Table(attr) = attr else { + continue; + }; + match attr { + TableAttr::Summary(summary) => { + if pdf_version >= PdfVersion::Pdf17 { + table_attributes.summary(TextStr(summary)); + } + } + TableAttr::HeaderScope(scope) => { + if pdf_version >= VersionedFeature::TableHeaderScope.minimum_pdf_version() { + table_attributes.scope(scope.to_pdf()); + } else if scope == &TableHeaderScope::Row { + // Without `Scope`, the correct cell to point to is ambiguous. + sc.register_validation_error(ValidationError::RequiresNewerPdfVersion( + VersionedFeature::TableHeaderScope, + tag.location, + )); + } + } + TableAttr::CellHeaders(headers) => { + if pdf_version >= PdfVersion::Pdf15 { + let id_strs = headers.iter().map(|id| Str(id.as_bytes())); + table_attributes.headers().items(id_strs); + } + } + TableAttr::RowSpan(n) => { + table_attributes.row_span(n.get() as i32); + } + TableAttr::ColSpan(n) => { + table_attributes.col_span(n.get() as i32); + } + } + } + table_attributes.finish(); + + // Lazily initialize the list attributes to avoid an empty array. + let mut layout_attributes = LazyCell::new(|| attributes.push().layout()); + for attr in tag.attrs.iter() { + let Attr::Layout(attr) = attr else { + continue; + }; + match attr { + LayoutAttr::Placement(placement) => { + layout_attributes.placement(placement.to_pdf()); + } + LayoutAttr::WritingMode(writing_mode) => { + layout_attributes.writing_mode(writing_mode.to_pdf()); + } + &LayoutAttr::BBox(BBox { page_idx, rect }) => { + let Some(page_info) = sc.page_infos().get(page_idx) else { + panic!( + "tag tree contains bounding box with page index {page_idx}, \ + but document only has {} pages", + sc.page_infos().len() + ); + }; + let transform = page_root_transform(page_info.size().height()); + let actual_rect = rect.transform(transform).unwrap(); + layout_attributes.bbox(actual_rect.to_pdf_rect()); + } + &LayoutAttr::Width(width) => { + layout_attributes.width(width); + } + &LayoutAttr::Height(height) => { + layout_attributes.height(height); + } + &LayoutAttr::BackgroundColor(color) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.background_color(color.into()); + } + } + LayoutAttr::BorderColor(sides) => { + if pdf_version >= PdfVersion::Pdf15 { + let sides = sides.map_pdf(NaiveRgbColor::into_f32_array); + layout_attributes.border_color(sides); + } + } + LayoutAttr::BorderStyle(sides) => { + if pdf_version >= PdfVersion::Pdf15 { + let sides = sides.map_pdf(BorderStyle::to_pdf); + layout_attributes.border_style(sides); + } + } + LayoutAttr::BorderThickness(sides) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.border_thickness(sides.into_pdf()); + } + } + LayoutAttr::Padding(sides) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.padding(sides.into_pdf()); + } + } + &LayoutAttr::Color(color) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.color(color.into()); + } + } + &LayoutAttr::SpaceBefore(margin) => { + layout_attributes.space_before(margin); + } + &LayoutAttr::SpaceAfter(margin) => { + layout_attributes.space_after(margin); + } + &LayoutAttr::StartIndent(margin) => { + layout_attributes.start_indent(margin); + } + &LayoutAttr::EndIndent(margin) => { + layout_attributes.end_indent(margin); + } + &LayoutAttr::TextIndent(indent) => { + layout_attributes.text_indent(indent); + } + LayoutAttr::BlockAlign(alignment) => { + layout_attributes.block_align(alignment.to_pdf()); + } + LayoutAttr::InlineAlign(alignment) => { + layout_attributes.inline_align(alignment.to_pdf()); + } + LayoutAttr::TextAlign(alignment) => { + layout_attributes.text_align(alignment.to_pdf()); + } + LayoutAttr::TableBorderStyle(sides) => { + if pdf_version >= PdfVersion::Pdf15 { + let sides = sides.map_pdf(BorderStyle::to_pdf); + layout_attributes.table_border_style(sides); + } + } + LayoutAttr::TablePadding(sides) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.table_padding(sides.into_pdf()); + } + } + &LayoutAttr::BaselineShift(shift) => { + layout_attributes.baseline_shift(shift); + } + LayoutAttr::LineHeight(height) => { + layout_attributes.line_height(height.to_pdf()); + } + &LayoutAttr::TextDecorationColor(color) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.text_decoration_color(color.into()); + } + } + &LayoutAttr::TextDecorationThickness(thickness) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.text_decoration_thickness(thickness); + } + } + LayoutAttr::TextDecorationType(style) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.text_decoration_type(style.to_pdf()); + } + } + &LayoutAttr::GlyphOrientationVertical(orientation) => { + if pdf_version >= PdfVersion::Pdf15 { + layout_attributes.glyph_orientation_vertical(orientation.to_pdf()); + } + } + LayoutAttr::ColumnCount(columns) => { + if pdf_version >= PdfVersion::Pdf16 { + layout_attributes.column_count(columns.get() as i32); + } + } + LayoutAttr::ColumnGap(gap) => { + if pdf_version >= PdfVersion::Pdf16 { + let sizes = layout_attributes.column_gap(); + match gap { + ColumnDimensions::All(gap) => sizes.uniform(*gap), + ColumnDimensions::Specific(values) => { + sizes.individual().items(values.iter().copied()); + } + } + } + } + LayoutAttr::ColumnWidths(width) => { + if pdf_version >= PdfVersion::Pdf16 { + let sizes = layout_attributes.column_widths(); + match width { + ColumnDimensions::All(width) => sizes.uniform(*width), + ColumnDimensions::Specific(values) => { + sizes.individual().items(values.iter().copied()); + } + } + } + } + } + } + layout_attributes.finish(); + + attributes.finish(); + + serialize_children( + sc, + elem_ref, + children_refs, + parent_tree_map, + &mut struct_elem, + )?; + struct_elem.finish(); + + Ok(Reference::Ref(elem_ref)) + } + + fn validate(&self, id_tree: &BTreeMap) -> KrillaResult<()> { + if let Some(headers) = self.tag.headers() { + for id in headers.iter() { + if !id_tree.contains_key(id) { + return Err(KrillaError::UnknownTagId(id.clone(), self.tag.location())); + } + } + } + + for child in self.children.iter() { + if let Node::Group(group) = child { + group.validate(id_tree)?; + } + } + Ok(()) + } +} + +/// A tag tree. +#[derive(Default)] +pub struct TagTree { + /// The children of the tag tree. + pub children: Vec, + /// Language attribute for the auto-generated Document root structure element. + pub lang: Option, +} + +impl From> for TagTree { + fn from(children: Vec) -> Self { + Self { + children, + lang: None, + } + } +} + +impl TagTree { + /// Create a new tag tree. + pub fn new() -> Self { + Self { + children: vec![], + lang: None, + } + } + + /// Set the language attribute on the Document root structure element. + /// + /// This sets `/Lang` on the auto-generated Document structure element, + /// which is distinct from the catalog-level `/Lang` set via + /// [`Metadata::language`](crate::metadata::Metadata::language). + pub fn with_lang(mut self, lang: Option) -> Self { + self.lang = lang; + self + } + + /// Append a new child to the tag tree. + pub fn push(&mut self, child: impl Into) { + self.children.push(child.into()) + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + parent_tree_map: &mut HashMap, + id_tree_map: &mut BTreeMap, + struct_tree_ref: Ref, + ) -> KrillaResult { + let root_ref = sc.new_ref(); + let mut struct_elems = sc.new_chunk(); + + // Keeps track of the ID of notes in the IDTree. We currently only write IDs for notes, + // which is why we use this simple variable, but this should be refactored if we write + // the IDs for multiple types of struct elements in the future. + let mut note_id = 1; + + let mut children_refs = vec![]; + + for child in &self.children { + let serialized = child.serialize( + sc, + parent_tree_map, + id_tree_map, + root_ref, + &mut note_id, + &mut struct_elems, + )?; + + if let Some(ref_) = serialized { + children_refs.push(ref_); + } + } + + let mut struct_elem = struct_elems.indirect(root_ref).start::(); + struct_elem.kind(StructRole::Document); + struct_elem.parent(struct_tree_ref); + if let Some(lang) = &self.lang { + if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf14 { + struct_elem.lang(TextStr(lang)); + } + } + serialize_children( + sc, + root_ref, + children_refs, + parent_tree_map, + &mut struct_elem, + )?; + + struct_elem.finish(); + chunk_container.non_stream.struct_elements = Some(struct_elems); + + Ok(root_ref) + } + + pub(crate) fn validate(&self, id_tree: &BTreeMap) -> KrillaResult<()> { + for child in self.children.iter() { + if let Node::Group(group) = child { + group.validate(id_tree)?; + } + } + Ok(()) + } +} + +fn serialize_children( + sc: &mut SerializeContext, + parent_ref: Ref, + children_refs: Vec, + parent_tree_map: &mut HashMap, + struct_elem: &mut StructElement, +) -> KrillaResult<()> { + // We can define a /Pg element on the struct element. If a marked content reference + // is part of the same page as that entry, we can just write the mcid, otherwise, we + // need to write a full marked content reference. + // In our case, we just use the first marked content reference we can find as the + // entry in the /Pg dict. + let mut struct_page_ref = None; + let mut struct_children = struct_elem.children(); + + for child in children_refs { + match child { + Reference::Ref(r) => { + struct_children.struct_element(r); + } + Reference::ContentIdentifier(it) => match it { + IdentifierType::PageIdentifier(pi) => { + let page_ref = sc + .page_infos() + .get(pi.page_index) + .unwrap_or_else(|| panic!("tag tree contains identifier from page {}, but document only has {} pages", + pi.page_index + 1, + sc.page_infos().len())) + .ref_(); + + if struct_page_ref.is_none() { + struct_page_ref = Some(page_ref); + } + + if parent_tree_map.contains_key(&pi.into()) { + panic!("the identifier {pi:?} appears twice in the tag tree"); + } + + parent_tree_map.insert(pi.into(), parent_ref); + + if struct_page_ref == Some(page_ref) { + struct_children.marked_content_id(pi.mcid); + } else { + struct_children + .marked_content_ref() + .marked_content_id(pi.mcid) + .page(page_ref); + } + } + IdentifierType::AnnotationIdentifier(ai) => { + let Some(page_info) = sc.page_infos_mut().get_mut(ai.page_index) else { + panic!( + "tag tree contains identifier from page {}, but document only has {} pages", + ai.page_index + 1, + sc.page_infos().len() + ); + }; + + let page_ref = page_info.ref_(); + let Some((annotation_ref, struct_parent)) = + page_info.annotations_mut().get_mut(ai.annot_index) + else { + panic!( + "tag tree contains identifier from annotation {} on page {}, but page only has {} annotations", + ai.annot_index + 1, + ai.page_index + 1, + page_info.annotations().len() + ); + }; + + if parent_tree_map.contains_key(&ai.into()) { + panic!("identifier {ai:?} appears twice in the tag tree"); + } + parent_tree_map.insert(ai.into(), *annotation_ref); + + struct_parent.set(parent_ref).expect("only one parent"); + + struct_children + .object_ref() + .page(page_ref) + .object(*annotation_ref); + } + }, + } + } + struct_children.finish(); + + if let Some(spr) = struct_page_ref { + struct_elem.page(spr); + } + + Ok(()) +} + +/// Where a layout artifact is attached to the page edge. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum ArtifactAttachment { + Left, + Top, + Right, + Bottom, +} diff --git a/vendor/krilla/src/interchange/tagging/tag.rs b/vendor/krilla/src/interchange/tagging/tag.rs new file mode 100644 index 00000000..7d03ed43 --- /dev/null +++ b/vendor/krilla/src/interchange/tagging/tag.rs @@ -0,0 +1,686 @@ +use std::marker::PhantomData; +use std::num::{NonZeroU16, NonZeroU32}; + +use smallvec::SmallVec; + +use crate::geom::Rect; +use crate::surface::Location; + +include!("generated.rs"); + +impl TagKind { + /// Get the location. + pub fn location(&self) -> Option { + self.as_any().location + } + + /// Set the location. + pub fn set_location(&mut self, location: Option) { + self.as_any_mut().location = location; + } + + /// Set the location. + pub fn with_location(mut self, location: Option) -> Self { + self.as_any_mut().location = location; + self + } +} + +/// A specific tag which allows accessing attributes specific to this [`TagKind`]. +/// +/// # Example +/// ``` +/// use std::num::NonZeroU32; +/// use krilla::tagging::{TagGroup, TagTree, TableHeaderScope, Tag, TagId}; +/// +/// let tag = Tag::TH(TableHeaderScope::Row) +/// .with_id(Some(TagId::from(*b"this id"))) +/// .with_col_span(Some(NonZeroU32::new(3).unwrap())) +/// .with_headers(Some([TagId::from(*b"parent id")])) +/// .with_width(Some(250.0)) +/// .with_height(Some(100.0)); +/// let group = TagGroup::new(tag); +/// +/// let mut tree = TagTree::new(); +/// tree.push(group); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Tag { + inner: AnyTag, + /// Compile time marker for a type-safe API. + pub(crate) ty: PhantomData, +} + +impl Tag { + /// This can't be public, otherwise tags could be constructed without + /// providing required attributes. + pub(crate) const fn new() -> Self { + Self { + inner: AnyTag::new(), + ty: PhantomData, + } + } + + /// A raw tag, which allows reading all attributes. + pub fn as_any(&self) -> &AnyTag { + &self.inner + } + + /// A raw tag, which allows reading all attributes and additionally writing + /// all global ones. + pub fn as_any_mut(&mut self) -> &mut AnyTag { + &mut self.inner + } + + /// Get the location. + pub fn location(&self) -> Option { + self.as_any().location + } + + /// Set the location. + pub fn set_location(&mut self, location: Option) { + self.as_any_mut().location = location; + } + + /// Set the location. + pub fn with_location(mut self, location: Option) -> Self { + self.as_any_mut().location = location; + self + } +} + +/// A raw tag, which allows reading all attributes and additionally writing all +/// global ones. +#[derive(Clone, Debug, PartialEq)] +pub struct AnyTag { + /// The location of the tag. + pub location: Option, + pub(crate) attrs: OrdinalSet, +} + +impl AnyTag { + pub(crate) const fn new() -> Self { + Self { + attrs: OrdinalSet::new(), + location: None, + } + } +} + +/// An ordered set using ordinal numbers to sort and identify elements. +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct OrdinalSet { + items: SmallVec<[A; 1]>, +} + +impl OrdinalSet { + pub(crate) const fn new() -> Self { + Self { + items: SmallVec::new_const(), + } + } +} + +impl OrdinalSet { + pub(crate) fn iter(&self) -> impl Iterator { + self.items.iter() + } + + pub(crate) fn set(&mut self, attr: A) { + for (i, item) in self.items.iter().enumerate() { + if item.ordinal() == attr.ordinal() { + self.items[i] = attr; + return; + } + if item.ordinal() > attr.ordinal() { + self.items.insert(i, attr); + return; + } + } + self.items.push(attr); + } + + pub(crate) fn remove(&mut self, ordinal: usize) { + for (i, item) in self.items.iter().enumerate() { + if item.ordinal() == ordinal { + self.items.remove(i); + return; + } + if item.ordinal() > ordinal { + break; + } + } + } + + pub(crate) fn get(&self, ordinal: usize) -> Option<&A> { + for item in self.items.iter() { + if item.ordinal() == ordinal { + return Some(item); + } + if item.ordinal() > ordinal { + break; + } + } + None + } + + pub(crate) fn set_or_remove(&mut self, ordinal: usize, attr: Option) { + match attr { + Some(attr) => self.set(attr), + None => self.remove(ordinal), + } + } +} + +/// Identifies elements using an ordinal number. +pub(crate) trait Ordinal { + fn ordinal(&self) -> usize; +} + +/// An identifier of a [`Tag`]. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct TagId(pub(crate) SmallVec<[u8; 16]>); + +impl> From for TagId { + fn from(value: I) -> Self { + // Disambiguate ids provided by the user from ids automatically assigned + // to notes by prefixing them with a `U`. + let bytes = std::iter::once(b'U').chain(value).collect(); + TagId(bytes) + } +} + +impl TagId { + /// Returns the identifier as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + self.0.as_slice() + } +} + +/// The list numbering type. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ListNumbering { + /// No numbering. + None, + /// Solid circular bullets. + Disc, + /// Open circular bullets. + Circle, + /// Solid square bullets. + Square, + /// Decimal numbers. + Decimal, + /// Lowercase Roman numerals. + LowerRoman, + /// Uppercase Roman numerals. + UpperRoman, + /// Lowercase letters. + LowerAlpha, + /// Uppercase letters. + UpperAlpha, +} + +impl ListNumbering { + pub(crate) fn to_pdf(self) -> pdf_writer::types::ListNumbering { + match self { + ListNumbering::None => pdf_writer::types::ListNumbering::None, + ListNumbering::Disc => pdf_writer::types::ListNumbering::Disc, + ListNumbering::Circle => pdf_writer::types::ListNumbering::Circle, + ListNumbering::Square => pdf_writer::types::ListNumbering::Square, + ListNumbering::Decimal => pdf_writer::types::ListNumbering::Decimal, + ListNumbering::LowerRoman => pdf_writer::types::ListNumbering::LowerRoman, + ListNumbering::UpperRoman => pdf_writer::types::ListNumbering::UpperRoman, + ListNumbering::LowerAlpha => pdf_writer::types::ListNumbering::LowerAlpha, + ListNumbering::UpperAlpha => pdf_writer::types::ListNumbering::UpperAlpha, + } + } +} + +/// The scope of a table header cell. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TableHeaderScope { + /// The header cell refers to the row. + Row, + /// The header cell refers to the column. + Column, + /// The header cell refers to both the row and the column. + Both, +} + +impl TableHeaderScope { + pub(crate) fn to_pdf(self) -> pdf_writer::types::TableHeaderScope { + match self { + TableHeaderScope::Row => pdf_writer::types::TableHeaderScope::Row, + TableHeaderScope::Column => pdf_writer::types::TableHeaderScope::Column, + TableHeaderScope::Both => pdf_writer::types::TableHeaderScope::Both, + } + } +} + +/// The positioning of the element with respect to the enclosing reference area +/// and other content. +/// When applied to an ILSE, any value except Inline shall cause the element to +/// be treated as a BLSE instead. +/// +/// Default value: [`Placement::Inline`]. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum Placement { + /// Stacked in the block-progression direction within an enclosing reference + /// area or parent BLSE. + Block, + /// Packed in the inline-progression direction within an enclosing BLSE. + #[default] + Inline, + /// Placed so that the before edge of the element’s allocation rectangle. + /// (see “Content and Allocation Rectangles” in 14.8.5.4, “Layout Attributes”) + /// coincides with that of the nearest enclosing reference area. The element + /// may float, if necessary, to achieve the specified placement. The element + /// shall be treated as a block occupying the full extent of the enclosing + /// reference area in the inline direction. Other content shall be stacked + /// so as to begin at the after edge of the element’s allocation rectangle. + Before, + /// Placed so that the start edge of the element’s allocation rectangle + /// (see “Content and Allocation Rectangles” in 14.8.5.4, “Layout Attributes”) + /// coincides with that of the nearest enclosing reference area. The element + /// may float, if necessary, to achieve the specified placement. Other + /// content that would intrude into the element’s allocation rectangle + /// shall be laid out as a runaround. + Start, + /// Placed so that the end edge of the element’s allocation rectangle + /// (see “Content and Allocation Rectangles” in 14.8.5.4, “Layout Attributes”) + /// coincides with that of the nearest enclosing reference area. The element + /// may float, if necessary, to achieve the specified placement. Other + /// content that would intrude into the element’s allocation rectangle + /// shall be laid out as a runaround. + End, +} + +impl Placement { + pub(crate) fn to_pdf(self) -> pdf_writer::types::Placement { + match self { + Placement::Block => pdf_writer::types::Placement::Block, + Placement::Inline => pdf_writer::types::Placement::Inline, + Placement::Before => pdf_writer::types::Placement::Before, + Placement::Start => pdf_writer::types::Placement::Start, + Placement::End => pdf_writer::types::Placement::End, + } + } +} + +/// The directions of layout progression for packing of ILSEs (inline progression) +/// and stacking of BLSEs (block progression). +/// The specified layout directions shall apply to the given structure element +/// and all of its descendants to any level of nesting. +/// +/// Default value: [`WritingMode::LrTb`]. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum WritingMode { + /// Inline progression from left to right; block progression from top to + /// bottom. This is the typical writing mode for Western writing systems. + #[default] + LrTb, + /// Inline progression from right to left; block progression from top to + /// bottom. This is the typical writing mode for Arabic and Hebrew writing + /// systems. + RlTb, + /// Inline progression from top to bottom; block progression from right to + /// left. This is the typical writing mode for Chinese and Japanese writing + /// systems. + TbRl, +} + +impl WritingMode { + pub(crate) fn to_pdf(self) -> pdf_writer::types::WritingMode { + match self { + WritingMode::LrTb => pdf_writer::types::WritingMode::LtrTtb, + WritingMode::RlTb => pdf_writer::types::WritingMode::RtlTtb, + WritingMode::TbRl => pdf_writer::types::WritingMode::TtbRtl, + } + } +} + +/// The bounding box of a tag that encloses its visible content. +/// If the content spans multiple pages, this should be omitted. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BBox { + /// The page index of the bounding box. + pub page_idx: usize, + /// The rectangle that encloses the content. + pub rect: Rect, +} + +impl BBox { + /// Create a new bounding box. + pub fn new(page_idx: usize, rect: Rect) -> Self { + Self { page_idx, rect } + } +} + +/// An RGB color within the tag tree. The color space of this color is not +/// specified. Each component is in the range [0.0, 1.0]. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NaiveRgbColor { + /// The red component of the color. + pub red: u8, + /// The green component of the color. + pub green: u8, + /// The blue component of the color. + pub blue: u8, +} + +impl NaiveRgbColor { + /// Create a new RGB color. + pub fn new(red: u8, green: u8, blue: u8) -> Self { + Self { red, green, blue } + } + + /// Create a new RGB color from normalized floating point values. + pub fn new_f32(red: f32, green: f32, blue: f32) -> Self { + if !(0.0..=1.0).contains(&red) + || !(0.0..=1.0).contains(&green) + || !(0.0..=1.0).contains(&blue) + { + panic!("RGB color components must be in the range [0.0, 1.0]"); + } + Self { + red: (255.0 * red).round() as u8, + green: (255.0 * green).round() as u8, + blue: (255.0 * blue).round() as u8, + } + } + + /// Convert the color into an array of f32 components for PDF serialization. + pub fn into_f32_array(self) -> [f32; 3] { + let normalize = |n| n as f32 / 255.0; + [self.red, self.green, self.blue].map(normalize) + } +} + +impl From for crate::graphics::color::rgb::Color { + fn from(color: NaiveRgbColor) -> Self { + crate::graphics::color::rgb::Color::new(color.red, color.green, color.blue) + } +} + +impl From for [f32; 3] { + fn from(color: NaiveRgbColor) -> Self { + color.into_f32_array() + } +} + +/// The border style of an element. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum BorderStyle { + /// No border. + None, + /// Hidden border. + Hidden, + /// Solid border. + Solid, + /// Dashed border. + Dashed, + /// Dotted border. + Dotted, + /// Double border. + Double, + /// Groove border. + Groove, + /// Ridge border. + Ridge, + /// Inset border. + Inset, + /// Outset border. + Outset, +} + +impl BorderStyle { + pub(super) fn to_pdf(self) -> pdf_writer::types::LayoutBorderStyle { + match self { + BorderStyle::None => pdf_writer::types::LayoutBorderStyle::None, + BorderStyle::Hidden => pdf_writer::types::LayoutBorderStyle::Hidden, + BorderStyle::Solid => pdf_writer::types::LayoutBorderStyle::Solid, + BorderStyle::Dashed => pdf_writer::types::LayoutBorderStyle::Dashed, + BorderStyle::Dotted => pdf_writer::types::LayoutBorderStyle::Dotted, + BorderStyle::Double => pdf_writer::types::LayoutBorderStyle::Double, + BorderStyle::Groove => pdf_writer::types::LayoutBorderStyle::Groove, + BorderStyle::Ridge => pdf_writer::types::LayoutBorderStyle::Ridge, + BorderStyle::Inset => pdf_writer::types::LayoutBorderStyle::Inset, + BorderStyle::Outset => pdf_writer::types::LayoutBorderStyle::Outset, + } + } +} + +/// The text alignment. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TextAlign { + /// At the start of the inline advance direction. + Start, + /// Centered. + Center, + /// At the end of the inline advance direction. + End, + /// Justified. + Justify, +} + +impl TextAlign { + pub(super) fn to_pdf(self) -> pdf_writer::types::TextAlign { + match self { + TextAlign::Start => pdf_writer::types::TextAlign::Start, + TextAlign::Center => pdf_writer::types::TextAlign::Center, + TextAlign::End => pdf_writer::types::TextAlign::End, + TextAlign::Justify => pdf_writer::types::TextAlign::Justify, + } + } +} + +/// The block alignment. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum BlockAlign { + /// At the start of the block advance direction. + Begin, + /// Centered. + Middle, + /// At the end of the block advance direction. + After, + /// Justified. + Justify, +} + +impl BlockAlign { + pub(super) fn to_pdf(self) -> pdf_writer::types::BlockAlign { + match self { + BlockAlign::Begin => pdf_writer::types::BlockAlign::Before, + BlockAlign::Middle => pdf_writer::types::BlockAlign::Middle, + BlockAlign::After => pdf_writer::types::BlockAlign::After, + BlockAlign::Justify => pdf_writer::types::BlockAlign::Justify, + } + } +} + +/// The inline alignment. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum InlineAlign { + /// At the start of the inline advance direction. + Start, + /// Centered. + Center, + /// At the end of the inline advance direction. + End, +} + +impl InlineAlign { + pub(super) fn to_pdf(self) -> pdf_writer::types::InlineAlign { + match self { + InlineAlign::Start => pdf_writer::types::InlineAlign::Start, + InlineAlign::Center => pdf_writer::types::InlineAlign::Center, + InlineAlign::End => pdf_writer::types::InlineAlign::End, + } + } +} + +/// The height of a line. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum LineHeight { + /// Adjust the line height automatically, taking `/BaselineShift` into + /// account. + Normal, + /// Adjust the line height automatically. + Auto, + /// Set a fixed line height. + Custom(f32), +} + +impl LineHeight { + pub(super) fn to_pdf(self) -> pdf_writer::types::LineHeight { + match self { + LineHeight::Auto => pdf_writer::types::LineHeight::Auto, + LineHeight::Normal => pdf_writer::types::LineHeight::Normal, + LineHeight::Custom(height) => pdf_writer::types::LineHeight::Custom(height), + } + } +} + +/// The text decoration type (over- and underlines). +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum TextDecorationType { + /// No decoration. + None, + /// Underlined. + Underline, + /// Line over the text. + Overline, + /// Strike the text. + LineThrough, +} + +impl TextDecorationType { + pub(super) fn to_pdf(self) -> pdf_writer::types::TextDecorationType { + match self { + Self::None => pdf_writer::types::TextDecorationType::None, + Self::Underline => pdf_writer::types::TextDecorationType::Underline, + Self::Overline => pdf_writer::types::TextDecorationType::Overline, + Self::LineThrough => pdf_writer::types::TextDecorationType::LineThrough, + } + } +} + +/// The rotation of glyphs in vertical writing modes. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum GlyphOrientationVertical { + /// Determine the rotation based on whether the text is full-width. + Auto, + /// No rotation. + None, + /// Rotate 90 degrees clockwise. + Clockwise90, + /// Rotate 90 degrees counter-clockwise. + CounterClockwise90, + /// Rotate 180 degrees clockwise. + Clockwise180, + /// Rotate 180 degrees counter-clockwise. + CounterClockwise180, + /// Rotate 270 degrees clockwise. + Clockwise270, +} + +impl GlyphOrientationVertical { + /// Convert the rotation to a number. If the rotation is `Auto`, returns + /// `None`. + pub(super) fn to_pdf(self) -> pdf_writer::types::GlyphOrientationVertical { + let angle = match self { + GlyphOrientationVertical::Auto => { + return pdf_writer::types::GlyphOrientationVertical::Auto + } + GlyphOrientationVertical::None => 0, + GlyphOrientationVertical::Clockwise90 => 90, + GlyphOrientationVertical::CounterClockwise90 => -90, + GlyphOrientationVertical::Clockwise180 => 180, + GlyphOrientationVertical::CounterClockwise180 => -180, + GlyphOrientationVertical::Clockwise270 => 270, + }; + pdf_writer::types::GlyphOrientationVertical::Angle(angle) + } +} + +/// An attribute value that can apply to all sides of the element, or have a specific value for each side. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Sides { + /// The start of the element on the block axis. + pub before: T, + /// The end of the element on the block axis. + pub after: T, + /// The start of the element on the inline axis. + pub start: T, + /// The end of the element on the inline axis. + pub end: T, +} + +impl Sides { + /// Construct a new `Sides` value with specific values for each side. + pub fn new(before: T, after: T, start: T, end: T) -> Self { + Self { + before, + after, + start, + end, + } + } + + /// Construct a new `Sides` value with the same value for all sides. + pub fn uniform(value: T) -> Self + where + T: Copy, + { + Sides { + before: value, + after: value, + start: value, + end: value, + } + } + + pub(crate) fn is_uniform(&self) -> bool + where + T: PartialEq, + { + self.before == self.after && self.before == self.start && self.before == self.end + } + + /// Returns an array for all sides. + pub(super) fn into_array(self) -> [T; 4] { + [self.before, self.after, self.start, self.end] + } + + /// Convert into [`pdf_writer::types::Sides`]. + pub(super) fn into_pdf(self) -> pdf_writer::types::Sides { + pdf_writer::types::Sides::from_array(self.into_array()) + } + + /// Convert into [`pdf_writer::types::Sides`] by each side value. + pub(super) fn map_pdf

(self, to_pdf: impl Fn(T) -> P) -> pdf_writer::types::Sides

{ + pdf_writer::types::Sides::from_array(self.into_array().map(to_pdf)) + } +} + +/// Widths related to columns, either for all columns or +/// with specific values for each. +#[derive(Debug, Clone, PartialEq)] +pub enum ColumnDimensions { + /// The same value applies to all columns. + All(f32), + /// The value varies for each column or column gap. + Specific(Vec), +} + +impl ColumnDimensions { + /// Construct a new `ColumnDimensions` with the same value for all columns. + pub fn all(value: f32) -> Self { + ColumnDimensions::All(value) + } + + /// Construct a new `ColumnDimensions` with specific values for each column. + pub fn specific(values: Vec) -> Self { + ColumnDimensions::Specific(values) + } +} diff --git a/vendor/krilla/src/lib.rs b/vendor/krilla/src/lib.rs new file mode 100644 index 00000000..efbe6f84 --- /dev/null +++ b/vendor/krilla/src/lib.rs @@ -0,0 +1,185 @@ +/*! +A high-level, ergonomic Rust library creating PDF documents. + +[krilla] is a high-level Rust crate that allows for the creation of PDF files. It builds +on top of the [pdf-writer] crate, but abstracts away all complexities that are +involved in creating a PDF file, instead providing an interface with high-level primitives, such +as fills, strokes, gradient, glyphs and images which can be used and combined easily +without having to worry about low-level details. + +To get started, take a look at the [`document`] module that explains how you can create +a document using krilla. + +# Example + +The following example shows some of the features of krilla in action. + +The example creates a PDF file with two pages. On the first page, +we add two small pieces of text, and on the second page we draw a triangle +with a gradient fill. + +For more examples, feel free to take a look at the [examples] directory of the GitHub repository. + +``` +use std::path::{self, PathBuf}; + +use krilla::color::rgb; +use krilla::text::Font; +use krilla::geom::{Point, PathBuilder}; +use krilla::paint::{SpreadMethod, LinearGradient, Stop, FillRule}; +use krilla::text::TextDirection; +use krilla::paint::Fill; +use krilla::Document; +use krilla::page::PageSettings; +use krilla::num::NormalizedF32; + +// Create a new document. +let mut document = Document::new(); +// Load a font. +let font = { + let path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/fonts/NotoSans-Regular.ttf"); + let data = std::fs::read(&path).unwrap(); + Font::new(data.into(), 0).unwrap() +}; + +// Add a new page with dimensions 200x200. +let mut page = document.start_page_with(PageSettings::from_wh(200.0, 200.0).unwrap()); +// Get the surface of the page. +let mut surface = page.surface(); +// Draw some text. +surface.draw_text( + Point::from_xy(0.0, 25.0), + font.clone(), + 14.0, + "This text has font size 14!", + false, + TextDirection::Auto +); + +surface.set_fill(Some(Fill { + paint: rgb::Color::new(255, 0, 0).into(), + opacity: NormalizedF32::new(0.5).unwrap(), + rule: Default::default(), +})); +// Draw some more text, in a different color with an opacity and bigger font size. +surface.draw_text( + Point::from_xy(0.0, 50.0), + font.clone(), + 16.0, + "This text has font size 16!", + false, + TextDirection::Auto +); + +// Finish the page. +surface.finish(); +page.finish(); + +// Start a new page. +let mut page = document.start_page_with(PageSettings::from_wh(200.0, 200.0).unwrap()); +// Create the triangle. +let triangle = { + let mut pb = PathBuilder::new(); + pb.move_to(100.0, 20.0); + pb.line_to(160.0, 160.0); + pb.line_to(40.0, 160.0); + pb.close(); + + pb.finish().unwrap() +}; + +// Create the linear gradient. +let lg = LinearGradient { + x1: 60.0, + y1: 0.0, + x2: 140.0, + y2: 0.0, + transform: Default::default(), + spread_method: SpreadMethod::Repeat, + stops: vec![ + Stop { + offset: NormalizedF32::new(0.2).unwrap(), + color: rgb::Color::new(255, 0, 0).into(), + opacity: NormalizedF32::ONE, + }, + Stop { + offset: NormalizedF32::new(0.8).unwrap(), + color: rgb::Color::new(255, 255, 0).into(), + opacity: NormalizedF32::ONE, + }, + ], + anti_alias: false, +}; +let mut surface = page.surface(); + +// Set the fill. +surface.set_fill(Some(Fill { + paint: lg.into(), + rule: FillRule::EvenOdd, + opacity: NormalizedF32::ONE +})); + +// Fill the path. +surface.draw_path(&triangle); + +// Finish up and write the resulting PDF. +surface.finish(); +page.finish(); + +let pdf = document.finish().unwrap(); +let path = path::absolute("basic.pdf").unwrap(); +eprintln!("Saved PDF to '{}'", path.display()); + +// Write the PDF to a file. +std::fs::write(path, &pdf).unwrap(); +``` + +# Minimum Supported Rust Version (MSRV) + +The minimum supported Rust version is **1.92**. + +[krilla]: https://github.com/LaurenzV/krilla +[pdf-writer]: https://github.com/typst/pdf-writer +[examples]: https://github.com/LaurenzV/krilla/tree/main/crates/krilla/examples +[`document`]: crate::document +*/ + +#![deny(missing_docs)] +#![forbid(unsafe_code)] + +mod chunk_container; +mod graphics; +mod interactive; +mod interchange; +mod resource; +mod serialize; +mod util; + +pub(crate) mod content; +pub(crate) mod data; + +pub mod configure; +pub mod document; +pub mod error; +pub mod geom; +pub mod num; +pub mod page; +#[cfg(feature = "pdf")] +pub mod pdf; +pub mod stream; +pub mod surface; +pub mod text; + +pub use data::*; +pub use document::*; +pub use graphics::*; +pub use interactive::*; +pub use interchange::*; +pub use serialize::SerializeSettings; + +/// Identifies the local patch that discards unfinished page/surface state during unwind. +pub const CRAFT_UNWIND_SAFE_DROP: bool = true; + +/// Identifies the patch that rejects serialization after failed page/surface cleanup. +pub const CRAFT_UNWIND_POISONING: bool = true; diff --git a/vendor/krilla/src/num.rs b/vendor/krilla/src/num.rs new file mode 100644 index 00000000..f91247c5 --- /dev/null +++ b/vendor/krilla/src/num.rs @@ -0,0 +1,26 @@ +//! Numerical types. + +/// An immutable, finite `f32` in a 0..=1 range. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[repr(transparent)] +pub struct NormalizedF32(tiny_skia_path::NormalizedF32); + +impl NormalizedF32 { + /// A `NormalizedF32` value initialized with zero. + pub const ZERO: Self = NormalizedF32(tiny_skia_path::NormalizedF32::ZERO); + /// A `NormalizedF32` value initialized with one. + pub const ONE: Self = NormalizedF32(tiny_skia_path::NormalizedF32::ONE); + + /// Create a new normalized f32. + /// + /// Panics if the number is not normalized. + pub fn new(num: f32) -> Option { + Some(Self(tiny_skia_path::NormalizedF32::new(num)?)) + } + + /// Returns the value as a primitive type. + #[inline] + pub const fn get(self) -> f32 { + self.0.get() + } +} diff --git a/vendor/krilla/src/page.rs b/vendor/krilla/src/page.rs new file mode 100644 index 00000000..ed6b1e29 --- /dev/null +++ b/vendor/krilla/src/page.rs @@ -0,0 +1,623 @@ +//! Working with pages of a PDF document. + +use std::cell::OnceCell; +use std::num::NonZeroU32; +use std::ops::DerefMut; + +use pdf_writer::types::TabOrder; +use pdf_writer::writers::NumberTree; +use pdf_writer::{Chunk, Finish, Ref, TextStr}; + +use crate::chunk_container::ChunkContainer; +use crate::configure::validate::VersionedFeature; +use crate::configure::ValidationError; +use crate::content::ContentBuilder; +use crate::error::KrillaResult; +use crate::geom::{Rect, Size, Transform}; +use crate::interactive::annotation::Annotation; +use crate::interchange::tagging::{Identifier, PageTagIdentifier}; +use crate::resource::ResourceDictionary; +use crate::serialize::{PageInfo, SerializeContext}; +use crate::stream::{FilterStreamBuilder, Stream}; +use crate::surface::Surface; +use crate::tagging::AnnotationIdentifier; +use crate::util::Deferred; + +#[derive(Clone, Debug)] +/// The settings of a page. +pub struct PageSettings { + /// The media box of the page, which defines the visible area of the surface. + media_box: Option, + /// The page label of the page. + page_label: PageLabel, + /// The size of the surface. + surface_size: Size, + /// The crop box of the page + crop_box: Option, + /// The bleed box of the page + bleed_box: Option, + /// The trim box of the page + trim_box: Option, + /// The actual content boundaries + art_box: Option, +} + +impl PageSettings { + /// Create new page settings and define the size of the page surface. + pub fn new(size: Size) -> Self { + Self { + media_box: Some(Rect::from_xywh(0.0, 0.0, size.width(), size.height()).unwrap()), + surface_size: size, + ..Default::default() + } + } + + /// Try to create new page settings with the specified width and height. + /// + /// Returns `None` if either the width or the height is not > 0. + pub fn from_wh(width: f32, height: f32) -> Option { + Some(Self::new(Size::from_wh(width, height)?)) + } + + /// Change the media box. + /// + /// The media box defines the visible area of the page when opening the PDF, + /// so it can be distinct from the size of the surface, but in the majority + /// of the cases you want them to match in size and align the media box + /// at the origin of the coordinate system. + /// + /// If set to `None`, the dimensions will be chosen in such a way that all + /// contents fit on the page. + pub fn with_media_box(mut self, media_box: Option) -> PageSettings { + self.media_box = media_box; + self + } + + /// Change the page label. + pub fn with_page_label(mut self, page_label: PageLabel) -> PageSettings { + self.page_label = page_label; + self + } + + /// The current media box. + pub(crate) fn media_box(&self) -> Option { + self.media_box + } + + /// The current surface size. + pub(crate) fn surface_size(&self) -> Size { + self.surface_size + } + + /// The current page label. + pub(crate) fn page_label(&self) -> &PageLabel { + &self.page_label + } + + /// Change the crop box. + /// + /// The crop box defines the region to which the page contents are to be clipped + /// when displayed or printed. Default is the media box. + /// + /// If `None`, no /CropBox attribute will be written to the page. + pub fn with_crop_box(mut self, crop_box: Option) -> PageSettings { + self.crop_box = crop_box; + self + } + + /// The current crop box. + pub(crate) fn crop_box(&self) -> Option { + self.crop_box + } + + /// Change the bleed box. + /// + /// The bleed box defines the region to which the page contents needs to be clipped + /// when output in a production environment. It includes any extra bleed area needed + /// for printing. + /// + /// If `None`, no /BleedBox attribute will be written to the page. + pub fn with_bleed_box(mut self, bleed_box: Option) -> PageSettings { + self.bleed_box = bleed_box; + self + } + + /// The current bleed box. + pub(crate) fn bleed_box(&self) -> Option { + self.bleed_box + } + + /// Change the trim box. + /// + /// The trim box defines the intended dimensions of the finished page after trimming. + /// It may be smaller than the media box and bleed box to accommodate bleed for printing. + /// + /// If `None`, no /TrimBox attribute will be written to the page. + pub fn with_trim_box(mut self, trim_box: Option) -> PageSettings { + self.trim_box = trim_box; + self + } + + /// The current trim box. + pub(crate) fn trim_box(&self) -> Option { + self.trim_box + } + + /// Change the art box. + /// + /// The art box defines the extent of the page's meaningful content (including + /// potential white space) as intended by the page's creator. + /// + /// If `None`, no /ArtBox attribute will be written to the page. + pub fn with_art_box(mut self, art_box: Option) -> PageSettings { + self.art_box = art_box; + self + } + + /// The current art box. + pub(crate) fn art_box(&self) -> Option { + self.art_box + } +} + +impl Default for PageSettings { + fn default() -> Self { + // Default for A4. + let width = 595.0; + let height = 842.0; + + Self { + media_box: Some(Rect::from_xywh(0.0, 0.0, width, height).unwrap()), + surface_size: Size::from_wh(width, height).unwrap(), + page_label: PageLabel::default(), + crop_box: None, + bleed_box: None, + trim_box: None, + art_box: None, + } + } +} + +/// A single page. +/// +/// You cannot create an instance of this type yourself. Instead, you should use the +/// [`Document::start_page`] (or a related method) to add a new page to a document. In most cases, all +/// you need to do is to call the [`Page::surface`] method, so you can start drawing on the page. +/// However, there are a few other operations you can perform, such as adding annotations +/// to a page. +/// +/// [`Document::start_page`]: crate::Document::start_page +pub struct Page<'a> { + sc: &'a mut SerializeContext, + chunk_container: &'a mut ChunkContainer, + page_settings: PageSettings, + page_index: usize, + page_stream: Stream, + num_mcids: i32, + annotations: Vec, +} + +impl<'a> Page<'a> { + pub(crate) fn new( + sc: &'a mut SerializeContext, + chunk_container: &'a mut ChunkContainer, + page_index: usize, + page_settings: PageSettings, + ) -> Self { + Self { + sc, + chunk_container, + page_settings, + page_index, + num_mcids: 0, + page_stream: Stream::empty(), + annotations: vec![], + } + } + + pub(crate) fn root_transform(&self) -> Transform { + page_root_transform(self.page_settings.surface_size().height()) + } + + /// Add an annotation to the page. + pub fn add_annotation(&mut self, annotation: Annotation) { + self.annotations.push(annotation); + } + + /// Add a tagged annotation to the page. + pub fn add_tagged_annotation(&mut self, mut annotation: Annotation) -> Identifier { + let annot_index = self.annotations.len(); + let ai = AnnotationIdentifier::new(self.page_index, annot_index); + let struct_parent = self.sc.register_annotation_parent(ai); + annotation.struct_parent = struct_parent; + self.add_annotation(annotation); + + match struct_parent { + None => Identifier::dummy(), + Some(_) => Identifier::new_annotation(self.page_index, annot_index), + } + } + + /// Get the surface of the page to draw on. Calling this multiple times + /// on the same page will reset any previous drawings. + pub fn surface(&mut self) -> Surface<'_> { + let root_builder = ContentBuilder::new( + self.root_transform(), + self.page_settings.media_box.is_none(), + self.sc, + ); + + let finish_fn = Box::new(|stream, num_mcids| { + self.page_stream = stream; + self.num_mcids = num_mcids; + }); + + let page_identifier = if self.sc.serialize_settings().enable_tagging { + Some(PageTagIdentifier::new(self.page_index, 0)) + } else { + None + }; + + Surface::new( + self.sc, + self.chunk_container, + root_builder, + page_identifier, + finish_fn, + ) + } + + /// A shorthand for `std::mem::drop`. + pub fn finish(self) {} +} + +pub(crate) fn page_root_transform(height: f32) -> Transform { + Transform::from_row(1.0, 0.0, 0.0, -1.0, 0.0, height) +} + +impl Drop for Page<'_> { + fn drop(&mut self) { + // Keep the document poisoned if cleanup unwinds, including an invariant failure + // during an explicit finish. Successful cleanup preserves any earlier poison. + let poisoned = std::mem::replace(&mut self.sc.poisoned, true); + // A drawing panic can leave page state incomplete. Discard it while + // unwinding; finalization here could panic again and abort the process. + // The caller must discard the whole document after a caught panic. + if std::thread::panicking() { + return; + } + + // Since we cannot take ownership in `drop`, just make use `mem::take` to pick + // what we need. + let annotations = std::mem::take(&mut self.annotations); + let page_settings = std::mem::take(&mut self.page_settings); + + let struct_parent = self + .sc + .register_page_struct_parent(self.page_index, self.num_mcids); + + let stream = std::mem::replace(&mut self.page_stream, Stream::empty()); + let page = InternalPage::new( + stream, + self.sc, + annotations, + struct_parent, + page_settings, + self.page_index, + ); + self.sc.register_page(page); + self.sc.poisoned = poisoned; + } +} + +/// The numbering style of a page label. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum NumberingStyle { + /// Arabic numerals. + Arabic, + /// Lowercase Roman numerals. + LowerRoman, + /// Uppercase Roman numerals. + UpperRoman, + /// Lowercase letters (a-z, then aa-zz, ...). + LowerAlpha, + /// Uppercase letters (A-Z, then AA-ZZ, ...). + UpperAlpha, +} + +impl NumberingStyle { + fn to_pdf(self) -> pdf_writer::types::NumberingStyle { + match self { + NumberingStyle::Arabic => pdf_writer::types::NumberingStyle::Arabic, + NumberingStyle::LowerRoman => pdf_writer::types::NumberingStyle::LowerRoman, + NumberingStyle::UpperRoman => pdf_writer::types::NumberingStyle::UpperRoman, + NumberingStyle::LowerAlpha => pdf_writer::types::NumberingStyle::LowerAlpha, + NumberingStyle::UpperAlpha => pdf_writer::types::NumberingStyle::UpperAlpha, + } + } +} + +pub(crate) struct InternalPage { + pub stream_ref: Ref, + pub stream_resources: ResourceDictionary, + pub stream_chunk: Deferred, + pub page_settings: PageSettings, + pub page_index: usize, + pub struct_parent: Option, + pub bbox: Rect, + pub annotations: Vec, +} + +impl InternalPage { + pub(crate) fn new( + mut stream: Stream, + sc: &mut SerializeContext, + annotations: Vec, + struct_parent: Option, + page_settings: PageSettings, + page_index: usize, + ) -> Self { + for validation_error in stream.validation_errors { + sc.register_validation_error(validation_error) + } + + let stream_ref = sc.new_ref(); + let serialize_settings = sc.serialize_settings().clone(); + let stream_resources = std::mem::take(&mut stream.resource_dictionary); + let mut chunk = sc.new_chunk(); + + let stream_chunk = Deferred::new(move || { + let page_stream = + FilterStreamBuilder::new_from_content_stream(&stream.content, &serialize_settings) + .finish(&serialize_settings.clone()); + + let mut stream = chunk.stream(stream_ref, page_stream.encoded_data()); + page_stream.write_filters(stream.deref_mut()); + + stream.finish(); + chunk + }); + + Self { + stream_resources, + stream_ref, + stream_chunk, + struct_parent, + bbox: stream.bbox, + annotations, + page_settings, + page_index, + } + } + + pub(crate) fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) -> KrillaResult<()> { + let mut annotation_refs = vec![]; + + if !self.annotations.is_empty() { + for annotation in &self.annotations { + let annot_ref = sc.new_ref(); + + annotation.serialize( + sc, + chunk_container, + annot_ref, + self.page_settings.surface_size().height(), + )?; + annotation_refs.push((annot_ref, OnceCell::new())); + } + } + + let chunk = &mut chunk_container.non_stream.pages; + let mut page = chunk.page(root_ref); + self.stream_resources.to_pdf_resources( + &mut page, + sc, + &mut chunk_container.non_stream.resource_dictionaries, + ); + + let transform_rect = |rect: Rect| { + rect.transform(page_root_transform( + self.page_settings.surface_size().height(), + )) + .unwrap() + }; + + // media box is mandatory, so we need to fall back to the default bbox + let media_box = transform_rect(self.page_settings.media_box().unwrap_or(self.bbox)); + page.media_box(media_box.to_pdf_rect()); + + // the remaining type of box are not mandatory, so we only set them if they are present + if let Some(crop_box) = self.page_settings.crop_box() { + let crop_box = transform_rect(crop_box); + page.crop_box(crop_box.to_pdf_rect()); + } + + if let Some(bleed_box) = self.page_settings.bleed_box() { + let bleed_box = transform_rect(bleed_box); + page.bleed_box(bleed_box.to_pdf_rect()); + } + + if let Some(trim_box) = self.page_settings.trim_box() { + let trim_box = transform_rect(trim_box); + page.trim_box(trim_box.to_pdf_rect()); + } + + if let Some(art_box) = self.page_settings.art_box() { + let art_box = transform_rect(art_box); + page.art_box(art_box.to_pdf_rect()); + } + + if let Some(struct_parent) = self.struct_parent { + page.struct_parents(struct_parent); + } + + // Only required for PDF/UA, but might as well always set it if there + // are annotations. + // + // Since the navigation order of annotations only has an effect if there + // are annotations, we usually only set the key in that case. However, + // the accessibility audit in Adobe Acrobat always [requires the key to + // be set][1], even if there are no annotations (perhaps the rationale + // is that the user can add some). Hence, we check if there is an + // accessibility validator or one requiring tags to decide whether to + // force the key. + // + // Since forcing `/Tabs S` unconditionally for PDF/A and PDF/UA files + // targeting PDF 1.4 and below would unconditionally raise a + // `RequiresNewerPdfVersion(StructureOrderTabbing, _)` error, we also + // check the target version. + // + // [1]: https://helpx.adobe.com/acrobat/using/create-verify-pdf-accessibility.html#TabOrder "Create and verify PDF accessibility (Acrobat Pro): Tab order" + if (!self.annotations.is_empty() + || ((sc + .serialize_settings() + .validators() + .accessibility() + .is_some() + || sc.serialize_settings().validators().requires_tagging()) + && sc.serialize_settings().pdf_version() + >= VersionedFeature::StructureOrderTabbing.minimum_pdf_version())) + && sc.serialize_settings().enable_tagging + { + if sc.serialize_settings().pdf_version() + >= VersionedFeature::StructureOrderTabbing.minimum_pdf_version() + { + page.tab_order(TabOrder::StructureOrder); + } else { + sc.register_validation_error(ValidationError::RequiresNewerPdfVersion( + VersionedFeature::StructureOrderTabbing, + sc.location, + )); + } + } + + page.parent(sc.page_tree_ref()); + page.contents(self.stream_ref); + + if !annotation_refs.is_empty() { + page.annotations(annotation_refs.iter().map(|(r, _)| *r)); + } + + // Populate the refs for each annotation in page infos. + let PageInfo::Krilla { annotations, .. } = &mut sc.page_infos_mut()[self.page_index] else { + unreachable!() + }; + *annotations = annotation_refs; + + page.finish(); + chunk_container.streams.pages.push(self.stream_chunk); + + Ok(()) + } +} + +/// A page label. +#[derive(Debug, Hash, Eq, PartialEq, Default, Clone)] +pub struct PageLabel { + /// The numbering style of the page label. + pub(crate) style: Option, + /// The prefix of the page label. + pub(crate) prefix: Option, + /// The numeric value of the page label. + pub(crate) offset: Option, +} + +impl PageLabel { + /// Create a new page label. + pub fn new( + style: Option, + prefix: Option, + offset: Option, + ) -> Self { + Self { + style, + prefix, + offset, + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.style.is_none() && self.prefix.is_none() && self.offset.is_none() + } + + pub(crate) fn serialize(&self, chunk_container: &mut ChunkContainer, root_ref: Ref) { + let chunk = &mut chunk_container.non_stream.page_labels; + let mut label = chunk + .indirect(root_ref) + .start::(); + + if let Some(style) = self.style { + label.style(style.to_pdf()); + } + + if let Some(prefix) = &self.prefix { + label.prefix(TextStr(prefix)); + } + + if let Some(offset) = self.offset.and_then(|o| i32::try_from(o.get()).ok()) { + label.offset(offset); + } + + label.finish(); + } +} + +#[derive(Hash)] +pub(crate) struct PageLabelContainer<'a> { + labels: &'a [PageLabel], +} + +impl<'a> PageLabelContainer<'a> { + pub(crate) fn new(labels: &'a [PageLabel]) -> Option { + if labels.iter().all(|f| f.is_empty()) { + None + } else { + Some(PageLabelContainer { labels }) + } + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + // Will always contain at least one entry, since we ensured that a PageLabelContainer cannot + // be empty + let mut filtered_entries = vec![]; + let mut prev: Option = None; + + for (i, label) in self.labels.iter().enumerate() { + if let Some(n_prev) = &prev { + if n_prev.style != label.style + || n_prev.prefix != label.prefix + || n_prev.offset.map(|n| n.get()) != label.offset.map(|n| n.get() + 1) + { + filtered_entries.push((i, label.clone())); + prev = Some(label.clone()); + } + } else { + filtered_entries.push((i, label.clone())); + prev = Some(label.clone()); + } + } + + let mut chunk = sc.new_chunk(); + let mut num_tree = chunk.indirect(root_ref).start::>(); + let mut nums = num_tree.nums(); + + for (page_num, label) in filtered_entries { + let label_ref = sc.register_page_label(chunk_container, label); + nums.insert(page_num as i32, label_ref); + } + + nums.finish(); + num_tree.finish(); + chunk_container.non_stream.page_label_tree = Some((root_ref, chunk)); + } +} diff --git a/vendor/krilla/src/pdf.rs b/vendor/krilla/src/pdf.rs new file mode 100644 index 00000000..bbdae8e3 --- /dev/null +++ b/vendor/krilla/src/pdf.rs @@ -0,0 +1,313 @@ +//! Including other PDF files. + +use std::collections::HashMap; +use std::fmt::{self, Debug, Display, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::Deref; +use std::sync::{Arc, OnceLock}; + +use hayro_write::hayro_syntax::page::Page; +use hayro_write::hayro_syntax::PdfVersion as HayroPdfVersion; +use hayro_write::{ExtractionError, ExtractionQuery}; +use pdf_writer::{Name, Ref}; + +pub use hayro_write::hayro_syntax::Pdf; + +use crate::chunk_container::{ChunkContainer, EmbeddedPdfChunk}; +use crate::configure::{PdfVersion, ValidationError}; +use crate::error::{KrillaError, KrillaResult}; +use crate::graphics::color::rgb; +use crate::resource::Resource; +use crate::serialize::{MaybeDeviceColorSpace, SerializeContext}; +use crate::surface::Location; +use crate::util::{Deferred, Prehashed}; + +/// An error that can occur when embedding a PDF document. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PdfError { + /// A page was requested that doesn't exist. + InvalidPage(usize), + /// The PDF version of the embedded PDF is not compatible with the version of the PDF + /// produced by krilla. This happens if the version of the krilla document is lower than + /// the one of the embedded PDF. + /// + /// The argument indicates the version of the embedded PDF document. + VersionMismatch(PdfVersion), +} + +impl Display for PdfError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + PdfError::InvalidPage(index) => write!(f, "invalid page index {index}"), + PdfError::VersionMismatch(version) => write!( + f, + "embedded PDF requires {}, which is newer than the selected PDF version", + version.as_str() + ), + } + } +} + +impl std::error::Error for PdfError {} + +struct PdfDocumentRepr(Arc); + +impl Debug for PdfDocumentRepr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "PdfDocumentRepr {{ .. }}") + } +} + +impl Hash for PdfDocumentRepr { + fn hash(&self, state: &mut H) { + self.0.data().as_ref().as_ref().hash(state); + } +} + +/// An external PDF document. +#[derive(Clone, Hash, PartialEq, Eq, Debug)] +pub struct PdfDocument(Arc>); + +impl PdfDocument { + /// Load a new PDF document. + pub fn new(pdf: Arc) -> PdfDocument { + Self(Arc::new(Prehashed::new(PdfDocumentRepr(pdf)))) + } + + pub(crate) fn pdf(&self) -> &Pdf { + &self.0.deref().0 + } + + pub(crate) fn pages(&self) -> &[Page<'_>] { + self.0.deref().0.pages() + } +} + +#[derive(Default, Debug)] +pub(crate) struct PdfDocumentInfo { + counter: u64, + query_refs: Vec, + cached_xobjects: HashMap, + queries: Vec, + locations: Vec>, +} + +impl PdfDocumentInfo { + pub(crate) fn new(counter: u64) -> Self { + Self { + counter, + ..Self::default() + } + } +} + +#[derive(Default, Debug)] +pub(crate) struct PdfSerializerContext { + infos: HashMap, + counter: u64, +} + +impl PdfSerializerContext { + pub(crate) fn add_page( + &mut self, + document: &PdfDocument, + page_index: usize, + ref_: Ref, + location: Option, + ) { + // Unlike XObject's, we cannot cache page refs since Acrobat doesn't like duplicate page + // refs in the page tree. + let info = self.get_info(document); + + info.query_refs.push(ref_); + info.queries.push(ExtractionQuery::new_page(page_index)); + info.locations.push(location); + } + + fn get_info(&mut self, document: &PdfDocument) -> &mut PdfDocumentInfo { + self.infos.entry(document.clone()).or_insert_with(|| { + let info = PdfDocumentInfo::new(self.counter); + self.counter += 1; + + info + }) + } + + #[must_use = "This method might not use the original ref and could return a different one that \ + was previously cached."] + pub(crate) fn add_xobject( + &mut self, + document: &PdfDocument, + page_index: usize, + ref_: Ref, + location: Option, + ) -> Ref { + let info = self.get_info(document); + + if let Some(cached_ref) = info.cached_xobjects.get(&page_index) { + return *cached_ref; + } + + info.query_refs.push(ref_); + info.queries.push(ExtractionQuery::new_xobject(page_index)); + info.locations.push(location); + + info.cached_xobjects.insert(page_index, ref_); + + ref_ + } + + pub(crate) fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + ) -> KrillaResult<()> { + let page_tree_parent_ref = sc.page_tree_ref(); + let krilla_version = sc.serialize_settings().configuration.version(); + + let mut entries = self.infos.into_iter().collect::>(); + // Make sure we always process them in the same order. + entries.sort_by(|d1, d2| d1.1.counter.cmp(&d2.1.counter)); + + for (doc, info) in entries { + for location in info.locations.iter() { + sc.register_validation_error(ValidationError::EmbeddedPDF(*location)) + } + + // In theory `hayro-write` only requires this for PDFs that are embedded + // as XObject's to write the XObject group color space (embedded pages don't need it), + // but we just always allocate it if we have at least one PDF, + // regardless of how the files are embedded. We can change this in the + // future, but it keeps the code simpler. + let xobject_group_color_space = sc.register_colorspace( + chunk_container, + rgb::color_space(sc.serialize_settings().no_device_cs).into(), + ); + let chunk_settings = sc.chunk_settings(); + + let deferred_chunk = Deferred::new(move || { + // We can't share the serializer context between threads, so each PDF has it's own + // reference, and we remap it later in `ChunkContainer`. + let mut new_ref = Ref::new(1); + // `local_ref` refers to the Ref that will be assigned to it locally + // in the extracted PDF chunk. Note that the colorspace itself + // doesn't actually exist in the extracted PDF, i.e. the Ref + // points to the null object. It just represents a placeholder Ref. + // Later on when actually merging the PDF chunk into the final + // krilla PDF, we replace `local_ref` with `cs_ref` such that it + // actually points to the colorspace that is embedded in the full PDF. + let (assigned_cs_ref, should_cs_ref) = match &xobject_group_color_space { + MaybeDeviceColorSpace::DeviceRgb => (None, None), + MaybeDeviceColorSpace::ColorSpace(cs) => { + (Some(new_ref.bump()), Some(cs.get_ref())) + } + MaybeDeviceColorSpace::DeviceGray | MaybeDeviceColorSpace::DeviceCMYK => { + unreachable!() + } + }; + + let first_location = info.locations.iter().flatten().next().cloned(); + let pdf = doc.pdf(); + + let pdf_version = convert_pdf_version(pdf.version()); + + if krilla_version < pdf_version { + return Err(KrillaError::Pdf( + doc.clone(), + PdfError::VersionMismatch(pdf_version), + first_location, + )); + } + + let extracted = hayro_write::extract( + pdf, + Box::new(|| new_ref.bump()), + chunk_settings, + |group| { + if let Some(local_group_color_space_ref) = assigned_cs_ref { + group + .insert(Name(b"CS")) + .primitive(local_group_color_space_ref); + } else { + // If we aren't using an ICC color space, just use + // the device RGB color space. + group.color_space().device_rgb(); + } + }, + &info.queries, + ); + let result = convert_extraction_result(extracted, &doc, first_location.as_ref())?; + + debug_assert_eq!(info.query_refs.len(), result.root_refs.len()); + + let mut root_ref_mappings = HashMap::new(); + + root_ref_mappings.insert(result.page_tree_parent_ref, page_tree_parent_ref); + + if let (Some(local_group_color_space_ref), Some(group_color_space_ref)) = + (assigned_cs_ref, should_cs_ref) + { + root_ref_mappings.insert(local_group_color_space_ref, group_color_space_ref); + } + + for ((should_ref, extraction_result), location) in info + .query_refs + .iter() + .zip(result.root_refs) + .zip(&info.locations) + { + let assigned_ref = convert_extraction_result( + extraction_result, + &doc, + location.clone().as_ref(), + )?; + + root_ref_mappings.insert(assigned_ref, *should_ref); + } + + Ok(EmbeddedPdfChunk { + root_ref_mappings, + original_chunk: result.chunk, + new_chunk: OnceLock::new(), + }) + }); + + chunk_container.mixed.embedded_pdfs.push(deferred_chunk); + } + + Ok(()) + } +} + +fn convert_pdf_version(version: HayroPdfVersion) -> PdfVersion { + match version { + // Those are obviously not right, but we don't support versions lower than 1.4 in krilla. + // Since we only need this conversion to detect version mismatches (in which case the + // version of the embedded PDF has to be higher than 1.4), this hack is sufficient for our + // purposes. + HayroPdfVersion::Pdf10 => PdfVersion::Pdf14, + HayroPdfVersion::Pdf11 => PdfVersion::Pdf14, + HayroPdfVersion::Pdf12 => PdfVersion::Pdf14, + HayroPdfVersion::Pdf13 => PdfVersion::Pdf14, + + HayroPdfVersion::Pdf14 => PdfVersion::Pdf14, + HayroPdfVersion::Pdf15 => PdfVersion::Pdf15, + HayroPdfVersion::Pdf16 => PdfVersion::Pdf16, + HayroPdfVersion::Pdf17 => PdfVersion::Pdf17, + HayroPdfVersion::Pdf20 => PdfVersion::Pdf20, + } +} + +fn convert_extraction_result( + result: Result, + doc: &PdfDocument, + location: Option<&Location>, +) -> KrillaResult { + result.map_err(|e| { + let pdf_error = match e { + ExtractionError::InvalidPageIndex(i) => PdfError::InvalidPage(i), + }; + + KrillaError::Pdf(doc.clone(), pdf_error, location.cloned()) + }) +} diff --git a/vendor/krilla/src/resource.rs b/vendor/krilla/src/resource.rs new file mode 100644 index 00000000..5ad0585a --- /dev/null +++ b/vendor/krilla/src/resource.rs @@ -0,0 +1,436 @@ +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; +use std::marker::PhantomData; + +use pdf_writer::types::ProcSet; +use pdf_writer::writers; +use pdf_writer::{Chunk, Dict, Finish, Name, Ref}; + +use crate::serialize::{Cacheable, SerializeContext}; +use crate::util::NameExt; + +pub(crate) trait Resource { + fn new(ref_: Ref) -> Self; + fn get_ref(&self) -> Ref; + fn get_dict<'a>(resources: &'a mut writers::Resources) -> Dict<'a>; + fn get_prefix() -> &'static str; + fn get_mapper(b: &mut ResourceDictionaryBuilder) -> &mut ResourceMapper; +} + +pub(crate) trait Resourceable: Cacheable { + type Resource: Resource; +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct ExtGState(Ref); + +impl Resource for ExtGState { + fn new(ref_: Ref) -> Self { + Self(ref_) + } + + fn get_ref(&self) -> Ref { + self.0 + } + + fn get_dict<'a>(resources: &'a mut writers::Resources) -> Dict<'a> { + resources.ext_g_states() + } + + fn get_prefix() -> &'static str { + "g" + } + + fn get_mapper(b: &mut ResourceDictionaryBuilder) -> &mut ResourceMapper { + &mut b.ext_g_states + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct ColorSpace(Ref); + +impl Resource for ColorSpace { + fn new(ref_: Ref) -> Self { + Self(ref_) + } + + fn get_ref(&self) -> Ref { + self.0 + } + + fn get_dict<'a>(resources: &'a mut writers::Resources) -> Dict<'a> { + resources.color_spaces() + } + + fn get_prefix() -> &'static str { + "c" + } + + fn get_mapper(b: &mut ResourceDictionaryBuilder) -> &mut ResourceMapper { + &mut b.color_spaces + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct Shading(Ref); + +impl Resource for Shading { + fn new(ref_: Ref) -> Self { + Self(ref_) + } + + fn get_ref(&self) -> Ref { + self.0 + } + + fn get_dict<'a>(resources: &'a mut writers::Resources) -> Dict<'a> { + resources.shadings() + } + + fn get_prefix() -> &'static str { + "s" + } + + fn get_mapper(b: &mut ResourceDictionaryBuilder) -> &mut ResourceMapper { + &mut b.shadings + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct XObject(Ref); + +impl Resource for XObject { + fn new(ref_: Ref) -> Self { + Self(ref_) + } + + fn get_ref(&self) -> Ref { + self.0 + } + + fn get_dict<'a>(resources: &'a mut writers::Resources) -> Dict<'a> { + resources.x_objects() + } + + fn get_prefix() -> &'static str { + "x" + } + + fn get_mapper(b: &mut ResourceDictionaryBuilder) -> &mut ResourceMapper { + &mut b.x_objects + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct Pattern(Ref); + +impl Resource for Pattern { + fn new(ref_: Ref) -> Self { + Self(ref_) + } + + fn get_ref(&self) -> Ref { + self.0 + } + + fn get_dict<'a>(resources: &'a mut writers::Resources) -> Dict<'a> { + resources.patterns() + } + + fn get_prefix() -> &'static str { + "p" + } + + fn get_mapper(b: &mut ResourceDictionaryBuilder) -> &mut ResourceMapper { + &mut b.patterns + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct Font(Ref); + +impl Resource for Font { + fn new(ref_: Ref) -> Self { + Self(ref_) + } + + fn get_ref(&self) -> Ref { + self.0 + } + + fn get_dict<'a>(resources: &'a mut writers::Resources) -> Dict<'a> { + resources.fonts() + } + + fn get_prefix() -> &'static str { + "f" + } + + fn get_mapper(b: &mut ResourceDictionaryBuilder) -> &mut ResourceMapper { + &mut b.fonts + } +} + +#[derive(Debug)] +pub(crate) struct ResourceDictionaryBuilder { + pub(crate) color_spaces: ResourceMapper, + pub(crate) ext_g_states: ResourceMapper, + pub(crate) patterns: ResourceMapper, + pub(crate) x_objects: ResourceMapper, + pub(crate) shadings: ResourceMapper, + pub(crate) fonts: ResourceMapper, +} + +impl ResourceDictionaryBuilder { + pub(crate) fn new() -> Self { + Self { + color_spaces: ResourceMapper::new(), + ext_g_states: ResourceMapper::new(), + patterns: ResourceMapper::new(), + x_objects: ResourceMapper::new(), + shadings: ResourceMapper::new(), + fonts: ResourceMapper::new(), + } + } + + pub(crate) fn register_resource(&mut self, obj: T) -> String + where + T: Resource, + { + T::get_mapper(self).remap_with_name(obj.get_ref()) + } + + pub(crate) fn finish(self) -> ResourceDictionary { + ResourceDictionary { + color_spaces: self.color_spaces.into_resource_list(), + ext_g_states: self.ext_g_states.into_resource_list(), + patterns: self.patterns.into_resource_list(), + x_objects: self.x_objects.into_resource_list(), + shadings: self.shadings.into_resource_list(), + fonts: self.fonts.into_resource_list(), + } + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +pub(crate) struct ResourceDictionary { + pub(crate) color_spaces: ResourceList, + pub(crate) ext_g_states: ResourceList, + pub(crate) patterns: ResourceList, + pub(crate) x_objects: ResourceList, + pub(crate) shadings: ResourceList, + pub(crate) fonts: ResourceList, +} + +impl Default for ResourceDictionary { + fn default() -> Self { + Self { + color_spaces: ResourceList::empty(), + ext_g_states: ResourceList::empty(), + patterns: ResourceList::empty(), + x_objects: ResourceList::empty(), + shadings: ResourceList::empty(), + fonts: ResourceList::empty(), + } + } +} + +pub(crate) type ResourceNumber = u32; + +impl ResourceDictionary { + pub fn to_pdf_resources( + &self, + parent: &mut T, + sc: &mut SerializeContext, + resources_chunk: &mut Chunk, + ) where + T: ResourcesExt, + { + let write_proc_sets = !sc.serialize_settings().pdf_version().deprecates_proc_sets(); + let has_resource_entries = self.color_spaces.len() > 0 + || self.ext_g_states.len() > 0 + || self.patterns.len() > 0 + || self.x_objects.len() > 0 + || self.shadings.len() > 0 + || self.fonts.len() > 0; + + if !write_proc_sets && !has_resource_entries { + // `Resources` dictionary is mandatory (or rather, it's mandatory if + // there isn't a parent resource dictionary to inherit from, but we just + // assume it is always mandatory), so just write an empty one as a direct + // object instead of an indirect object, for aesthetic reasons. + parent.resources().finish(); + return; + } + + let resources_ref = sc.new_ref(); + let mut resources = resources_chunk + .indirect(resources_ref) + .start::(); + if write_proc_sets { + resources.proc_sets([ + ProcSet::Pdf, + ProcSet::Text, + ProcSet::ImageColor, + ProcSet::ImageGrayscale, + ]); + } + write_resource_type::(&mut resources, &self.color_spaces); + write_resource_type::(&mut resources, &self.ext_g_states); + write_resource_type::(&mut resources, &self.patterns); + write_resource_type::(&mut resources, &self.x_objects); + write_resource_type::(&mut resources, &self.shadings); + write_resource_type::(&mut resources, &self.fonts); + parent.set_resources(resources_ref); + } +} + +fn write_resource_type(resources: &mut writers::Resources, resource_list: &ResourceList) +where + T: Resource, +{ + if resource_list.len() > 0 { + let mut dict = T::get_dict(resources); + + for (name, entry) in resource_list.get_entries() { + dict.pair(name.to_pdf_name(), entry); + } + + dict.finish(); + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Default)] +pub(crate) struct ResourceList { + entries: Vec, + phantom: PhantomData, +} + +impl ResourceList +where + T: Resource, +{ + pub(crate) fn empty() -> ResourceList { + Self { + entries: vec![], + phantom: Default::default(), + } + } + + pub(crate) fn len(&self) -> u32 { + self.entries.len() as u32 + } + + fn name_from_number(num: ResourceNumber) -> String { + format!("{}{}", T::get_prefix(), num) + } + + pub(crate) fn get_entries(&self) -> impl Iterator + '_ { + self.entries + .iter() + .enumerate() + .map(|(i, r)| (Self::name_from_number(i as ResourceNumber), *r)) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct ResourceMapper { + forward: Vec, + backward: HashMap, + phantom: PhantomData, +} + +impl ResourceMapper +where + T: Resource, +{ + pub(crate) fn new() -> Self { + Self { + forward: Vec::new(), + backward: HashMap::new(), + phantom: PhantomData, + } + } + + pub(crate) fn remap(&mut self, ref_: Ref) -> ResourceNumber { + let forward = &mut self.forward; + let backward = &mut self.backward; + + *backward.entry(ref_).or_insert_with(|| { + let old = forward.len(); + forward.push(ref_); + old as ResourceNumber + }) + } + + pub(crate) fn remap_with_name(&mut self, ref_: Ref) -> String { + Self::name_from_number(self.remap(ref_)) + } + + fn name_from_number(num: ResourceNumber) -> String { + format!("{}{}", T::get_prefix(), num) + } + + pub(crate) fn into_resource_list(self) -> ResourceList { + ResourceList { + entries: self.forward, + phantom: Default::default(), + } + } +} + +pub(crate) trait ResourcesExt { + fn resources(&mut self) -> writers::Resources<'_>; + fn set_resources(&mut self, resources_ref: Ref); +} + +impl ResourcesExt for writers::FormXObject<'_> { + fn resources(&mut self) -> writers::Resources<'_> { + self.resources() + } + + fn set_resources(&mut self, resources_ref: Ref) { + self.pair(Name(b"Resources"), resources_ref); + } +} + +impl ResourcesExt for writers::TilingPattern<'_> { + fn resources(&mut self) -> writers::Resources<'_> { + self.resources() + } + + fn set_resources(&mut self, resources_ref: Ref) { + self.pair(Name(b"Resources"), resources_ref); + } +} + +impl ResourcesExt for writers::Type3Font<'_> { + fn resources(&mut self) -> writers::Resources<'_> { + self.resources() + } + + fn set_resources(&mut self, resources_ref: Ref) { + self.pair(Name(b"Resources"), resources_ref); + } +} + +impl ResourcesExt for writers::Pages<'_> { + fn resources(&mut self) -> writers::Resources<'_> { + self.resources() + } + + fn set_resources(&mut self, resources_ref: Ref) { + self.pair(Name(b"Resources"), resources_ref); + } +} + +impl ResourcesExt for writers::Page<'_> { + fn resources(&mut self) -> writers::Resources<'_> { + self.resources() + } + + fn set_resources(&mut self, resources_ref: Ref) { + self.pair(Name(b"Resources"), resources_ref); + } +} diff --git a/vendor/krilla/src/serialize.rs b/vendor/krilla/src/serialize.rs new file mode 100644 index 00000000..d79ce28e --- /dev/null +++ b/vendor/krilla/src/serialize.rs @@ -0,0 +1,1104 @@ +use std::cell::{OnceCell, RefCell}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::num::NonZeroU16; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; +use std::sync::Arc; + +use indexmap::IndexMap; +use pdf_writer::types::{StructRole, StructRole2}; +use pdf_writer::writers::{OutputIntent, StructTreeRoot}; +use pdf_writer::{Chunk, Content, Finish, Limits, Name, Pdf, Ref, Settings, Str, TextStr}; + +use crate::chunk_container::ChunkContainer; +use crate::color::{CieBasedColorSpace, DeviceColorSpace, SpecialColorSpace}; +use crate::configure::validate::ValidationStore; +use crate::configure::{Configuration, PdfVersion, ValidationError, Validators}; +use crate::error::{KrillaError, KrillaResult, LimitError}; +use crate::geom::Size; +use crate::graphics::color::{rgb, ColorSpace}; +use crate::graphics::icc::{ICCBasedColorSpace, ICCProfile}; +#[cfg(feature = "raster-images")] +use crate::graphics::image::Image; +use crate::graphics::separation::SeparationColorSpace; +use crate::interactive::destination::{NamedDestination, XyzDestination}; +use crate::interchange::embed::EmbeddedFile; +use crate::interchange::outline::Outline; +use crate::interchange::tagging::{AnnotationIdentifier, PageTagIdentifier, TagTree}; +use crate::page::{InternalPage, PageLabel, PageLabelContainer}; +#[cfg(feature = "pdf")] +use crate::pdf::{PdfDocument, PdfSerializerContext}; +use crate::resource; +use crate::resource::{Resource, Resourceable}; +use crate::surface::{Location, Surface}; +use crate::text::GlyphId; +use crate::text::{Font, FontContainer, FontIdentifier}; +use crate::util::SipHashable; + +const STR_LEN: usize = 32767; +const NAME_LEN: usize = 127; + +// These only apply to PDF 1.4 and PDF/A-1. +const MAX_FLOAT: f32 = 32767.0; +const DICT_LEN: usize = 4095; +const ARRAY_LEN: usize = 8191; + +/// Settings that should be applied when creating a PDF document. +#[derive(Clone, Debug)] +pub struct SerializeSettings { + /// Whether to write PDFs in a way that is easier to inspect manually. This + /// will result in larger file sizes. + pub pretty: bool, + /// Whether content streams should be compressed. Leads to significantly smaller file sizes, + /// but also longer running times. It is highly recommended that you set this to `true`. + pub compress_content_streams: bool, + /// Whether device-independent colors should be used instead of + /// device-dependent ones. + /// + /// Note that this value might be overridden depending on which validator + /// you use. For example, when exporting to PDF/A, this value will be set to + /// true, regardless of what value will be passed. + pub no_device_cs: bool, + /// Whether the PDF should be ASCII-compatible, i.e. only consist of + /// characters in the ASCII range. + /// + /// Note that this only on a best-effort basis. For example, XMP metadata always + /// contains a binary marker. In addition to that, some validators, + /// like PDF/A, require that the file header be a binary marker, meaning + /// that the header itself will not be ASCII-compatible. Finally, embedded PDFs will + /// be embedded as is and not re-encoded with ASCII-compatible encoding. + pub ascii_compatible: bool, + /// Whether the PDF should include XMP metadata. + /// + /// Note that this value might be overridden depending on which validator + /// you use. For example, when exporting to PDF/A, this value will be set to + /// true, regardless of what value will be passed. + pub xmp_metadata: bool, + /// The ICC profile that should be used for CMYK colors + /// when `no_device_cs` is enabled. + /// + /// This is usually not required, but it is for example required when exporting + /// to PDF/A and using a CMYK color, since they have to be device-independent. + pub cmyk_profile: Option>, + /// A validator and PDF version used for export. + /// + /// In case validation fails, export will fail, and a list of validation errors that + /// occurred will be returned instead of the PDF. + /// + /// **Important**: Make sure to carefully read the documentation of the [`validate`] module + /// before using this feature! Just setting a validator might not be enough to ensure that + /// your output conforms to the given standard, as some requirements are semantic in nature + /// and cannot possibly be verified by krilla! + /// + /// However, as long as you carefully read and follow the documentation, + /// you can be certain that the resulting document will conform to the standard (unless there + /// is a bug). + /// + /// [`validate`]: crate::configure::validate + pub configuration: Configuration, + /// Whether to enable the creation of tagged documents. See the module documentation + /// of [`tagging`] for more information about tagged PDF documents. + /// + /// Note that enabling this does not automatically make your documents tagged, as tagging implies + /// enriching the document with semantic information, which krilla cannot do + /// for you, since it's content-agnostic. All this setting does is to allow you + /// to dynamically disable tagging if you wish to do so. This allows you to write + /// your code primarily with tagging in mind, but still allows you to + /// disable it dynamically, without having to make any changes to your code. + /// + /// Note that this value might be overridden depending on which validator + /// you use. For example, when exporting with PDF/UA, this value will always + /// be set to `true`. + /// + /// [`tagging`]: crate::interchange::tagging + pub enable_tagging: bool, + /// A function that should be used to render SVG glyphs. If you don't need this, yu can + /// just use the default function which doesn't render them at all. If you do want this, it + /// is recommended that you use the function provided by the `krilla-svg` crate. + pub render_svg_glyph_fn: RenderSvgGlyphFn, +} + +pub type RenderSvgGlyphFn = fn(&[u8], rgb::Color, GlyphId, (f32, f32), &mut Surface) -> Option<()>; + +impl SerializeSettings { + pub(crate) fn pdf_version(&self) -> PdfVersion { + self.configuration.version() + } + + pub(crate) fn validators(&self) -> Validators { + self.configuration.validators() + } + + /// Whether the `/AF` key is supported, accounting for the PDF version and active standards. + pub(crate) fn supports_associated_files(&self) -> bool { + self.configuration.version().specifies_associated_files() + || self.configuration.validators().specifies_associated_files() + } +} + +impl Default for SerializeSettings { + fn default() -> Self { + Self { + pretty: false, + ascii_compatible: false, + compress_content_streams: true, + no_device_cs: false, + xmp_metadata: true, + cmyk_profile: None, + configuration: Configuration::default(), + enable_tagging: true, + render_svg_glyph_fn: |_, _, _, _, _| None, + } + } +} + +pub(crate) enum PageInfo { + /// A page built with krilla. + Krilla { + /// The reference of the page in the chunk. + ref_: Ref, + /// The page size, necessary so that we can convert from PDF coordinates to + /// krilla coordinates. + surface_size: Size, + /// The refs of the annotations that are used by that page, and optionally + /// a ref to their struct parent in the tag tree. + /// + /// Note that this will be empty be default when adding a new `PageInfo` to + /// `page_infos` in `SerializeContext`, and only once we actually serialize + /// the page will the annotations be populated. + annotations: Vec<(Ref, OnceCell)>, + /// The page label of the page. + page_label: PageLabel, + }, + /// A page embedded from an external PDF file. + #[allow(dead_code)] + Pdf { + ref_: Ref, + size: Size, + page_label: PageLabel, + }, +} + +impl PageInfo { + pub(crate) fn ref_(&self) -> Ref { + match self { + PageInfo::Krilla { ref_, .. } => *ref_, + PageInfo::Pdf { ref_, .. } => *ref_, + } + } + + pub(crate) fn size(&self) -> Size { + match self { + PageInfo::Krilla { surface_size, .. } => *surface_size, + PageInfo::Pdf { size, .. } => *size, + } + } + + pub(crate) fn page_label(&self) -> &PageLabel { + match self { + PageInfo::Krilla { page_label, .. } => page_label, + PageInfo::Pdf { page_label, .. } => page_label, + } + } + + pub(crate) fn annotations(&self) -> &[(Ref, OnceCell)] { + match self { + PageInfo::Krilla { annotations, .. } => annotations, + PageInfo::Pdf { .. } => &[], + } + } + + pub(crate) fn annotations_mut(&mut self) -> &mut [(Ref, OnceCell)] { + match self { + PageInfo::Krilla { annotations, .. } => annotations, + PageInfo::Pdf { .. } => &mut [], + } + } +} + +enum StructParentElement { + /// The index of the page and the number of marked content IDs present on that page. + Page(usize, i32), + /// The index of the page where the annotation is present, as well as the index of the + /// annotation within that one page. + Annotation(AnnotationIdentifier), +} + +#[derive(Debug)] +pub(crate) enum MaybeDeviceColorSpace { + DeviceRgb, + DeviceGray, + DeviceCMYK, + ColorSpace(resource::ColorSpace), +} + +/// The serializer context is more or less the core piece of krilla. It is passed around +/// throughout pretty much the whole conversion process, and contains all mutable state +/// that is needed when writing a PDF file. This includes for example: +/// - Storing all chunks that are produced. +/// - The mappings from OTF fonts to CID/Type 3 fonts. +/// - Annotations used in the document. +/// etc. +pub(crate) struct SerializeContext { + /// An unfinished or failed page/surface must never serialize as a complete document. + pub(crate) poisoned: bool, + /// The ref of the page tree. + page_tree_ref: Ref, + /// PDF 2.0 namespaces. + pub(crate) pdf2_ns: Pdf2Namespaces, + /// All global objects, such as PDF fonts, that are populated over time. + pub(crate) global_objects: GlobalObjects, + /// Information for each page written so far, index by the page index. + page_infos: Vec, + /// Keep track of object hashes and their corresponding reference. This is used for + /// caching, so that for example same images will not be embedded twice in the document. + cached_mappings: HashMap, + /// The current ref in use. All serializers should use the `new_ref` method (which indirectly + /// is based on this field) to generate a new Ref, instead of creating one manually with + /// `Ref::new`. + pub(crate) cur_ref: Ref, + /// All validation errors that are collected as part of the export process + /// alongside the validators that raised the error. + validation_errors: Vec<(ValidationError, Validators)>, + /// Settings used for serialization. + serialize_settings: Arc, + /// Settings used for all PDF object chunks. + chunk_settings: Settings, + /// The limits created as part of the serialization process. In principle, we could + /// just keep track of this in `ChunkContainer`, where all used chunks are stored. + /// The only reason why `SerializeContext` needs to know about them is that we also + /// need to merge limits from postscript functions, which are not directly accessible + /// from the chunk they are written to. + limits: Limits, + /// Additional information stored during serialization that allows us to + /// raise standards errors later. + validation_store: ValidationStore, + /// The current location, if set. + pub(crate) location: Option, +} + +impl SerializeContext { + pub(crate) fn new(mut serialize_settings: SerializeSettings) -> Self { + // Override flags as required by the validator + serialize_settings.no_device_cs |= serialize_settings.validators().requires_no_device_cs(); + serialize_settings.enable_tagging |= serialize_settings.validators().requires_tagging(); + serialize_settings.xmp_metadata |= serialize_settings.validators().requires_xmp_metadata(); + + let mut cur_ref = Ref::new(1); + let page_tree_ref = cur_ref.bump(); + let pdf2_ns = Pdf2Namespaces { + ssn_ref: cur_ref.bump(), + krilla_ref: cur_ref.bump(), + }; + + let chunk_settings = Settings { + pretty: serialize_settings.pretty, + }; + + Self { + poisoned: false, + cached_mappings: HashMap::new(), + pdf2_ns, + global_objects: GlobalObjects::default(), + cur_ref, + page_tree_ref, + page_infos: vec![], + location: None, + validation_errors: vec![], + serialize_settings: Arc::new(serialize_settings), + chunk_settings, + limits: Limits::new(), + validation_store: ValidationStore::new(), + } + } + + pub(crate) fn page_infos(&self) -> &[PageInfo] { + &self.page_infos + } + + pub(crate) fn page_infos_mut(&mut self) -> &mut [PageInfo] { + &mut self.page_infos + } + + pub(crate) fn set_outline(&mut self, outline: Outline) { + // Only set it if it's not empty or if the current validator requires an + // outline. + if !outline.is_empty() + || self + .serialize_settings + .validators() + .prohibits(&ValidationError::MissingDocumentOutline) + .is_some() + { + self.global_objects.outline = MaybeTaken::new(Some(outline)); + } + } + + pub(crate) fn set_location(&mut self, location: Location) { + self.location = Some(location) + } + + pub(crate) fn reset_location(&mut self) { + self.location = None + } + + pub(crate) fn embed_file( + &mut self, + chunk_container: &mut ChunkContainer, + file: EmbeddedFile, + ) -> Option<()> { + let name = file.path.clone(); + let ref_ = self.register_cacheable(chunk_container, file); + if self + .global_objects + .embedded_files + .insert(name, ref_) + .is_some() + { + None + } else { + Some(()) + } + } + + pub(crate) fn set_tag_tree(&mut self, root: TagTree) { + // Only set the tag tree if the user actually enabled tagging. + if self.serialize_settings.enable_tagging { + self.global_objects.tag_tree = MaybeTaken::new(Some(root)) + } + } + + pub(crate) fn new_ref(&mut self) -> Ref { + self.cur_ref.bump() + } + + pub(crate) fn serialize_settings(&self) -> Arc { + self.serialize_settings.clone() + } + + // IMPORTANT: DO NEVER CALL `Chunk::new`, `Pdf::new` or `Content::new` directly! Instead, + // always make sure to use the methods on `SerializeContext`, to ensure the + // flags are applied consistently across all chunks. + + pub(crate) fn new_chunk(&self) -> Chunk { + Chunk::with_settings(self.chunk_settings) + } + + pub(crate) fn new_content(&self) -> Content { + Content::with_settings(self.chunk_settings) + } + + pub(crate) fn new_pdf_with_capacity(&self, capacity: usize) -> Pdf { + Pdf::with_settings_and_capacity(self.chunk_settings, capacity) + } + + #[cfg(feature = "pdf")] + pub(crate) fn chunk_settings(&self) -> Settings { + self.chunk_settings + } + + #[cfg(feature = "pdf")] + pub(crate) fn embed_pdf_pages(&mut self, pdf: &PdfDocument, page_indices: &[usize]) { + for page_idx in page_indices { + let page_ref = self.new_ref(); + let size = pdf + .pages() + .get(*page_idx) + .and_then(|p| { + let (x, y) = p.render_dimensions(); + Size::from_wh(x, y) + }) + // In case the page doesn't exist, we will catch the error later, so just use + // a dummy size. + .unwrap_or(Size::from_wh(1.0, 1.0).unwrap()); + self.global_objects + .pdf_ctx + .add_page(pdf, *page_idx, page_ref, self.location); + self.page_infos.push(PageInfo::Pdf { + ref_: page_ref, + size, + // TODO: Maybe this should be configurable. + page_label: PageLabel::default(), + }); + } + } + + #[cfg(feature = "pdf")] + pub(crate) fn embed_pdf_page_as_xobject(&mut self, pdf: &PdfDocument, page_idx: usize) -> Ref { + let xobj_ref = self.new_ref(); + + // Note that `add_xobject` might return a different ref than the one we created. + self.global_objects + .pdf_ctx + .add_xobject(pdf, page_idx, xobj_ref, self.location) + } + + pub(crate) fn page_tree_ref(&mut self) -> Ref { + self.page_tree_ref + } + + pub(crate) fn register_font_container(&mut self, font: Font) -> Rc> { + self.global_objects + .font_map + .entry(font.clone()) + .or_insert_with(|| Rc::new(RefCell::new(FontContainer::new(font.clone())))) + .clone() + } + + pub(crate) fn validation_store(&mut self) -> &mut ValidationStore { + &mut self.validation_store + } + + pub(crate) fn finish(mut self, mut chunk_container: ChunkContainer) -> KrillaResult { + // We need to be careful here that we serialize the objects in the right order, + // as in some cases we use MaybeTake::take to remove an object, which means that + // no object that is serialized afterwards must depend on it. + + // Serialize all objects that can only be written in the end. + self.serialize_destination_profiles(&mut chunk_container); + self.serialize_page_label_tree(&mut chunk_container); + self.serialize_outline(&mut chunk_container); + self.serialize_fonts(&mut chunk_container)?; + self.serialize_pages(&mut chunk_container)?; + self.serialize_page_tree(&mut chunk_container); + #[cfg(feature = "pdf")] + self.serialize_embedded_pdfs(&mut chunk_container)?; + self.serialize_xyz_destinations(&mut chunk_container)?; + // It is important that we serialize the tags AFTER we have serialized the pages, + // because page serialization will update the annotation refs of the page infos, + // and when serializing the parent tree map we need to know the refs of the annotations + self.serialize_tag_tree(&mut chunk_container)?; + + // Create the final PDF. + let pdf = chunk_container.finish(&mut self)?; + self.register_limits(pdf.limits()); + + self.check_validator_limits(); + + if !self.validation_errors.is_empty() { + // Deduplicate errors, while still preserving order. + let mut errors = vec![]; + let mut seen = HashSet::new(); + + for error in self.validation_errors { + if !seen.contains(&error) { + seen.insert(error.clone()); + errors.push(error); + } + } + + return Err(KrillaError::Validation(errors)); + } + + if let Some(limit_error) = self.check_version_limits() { + return Err(KrillaError::Limit(limit_error)); + } + + // Just a sanity check that we've actually processed all items. + self.global_objects.assert_all_taken(); + + Ok(pdf) + } +} + +/// Various registration methods. +impl SerializeContext { + pub(crate) fn register_validation_error(&mut self, error: ValidationError) { + if let Some(validators) = self.serialize_settings().validators().prohibits(&error) { + self.validation_errors.push((error, validators)) + } + } + + pub(crate) fn register_limits(&mut self, limits: &Limits) { + self.limits.merge(limits); + } + + pub(crate) fn register_page_struct_parent( + &mut self, + page_index: usize, + num_mcids: i32, + ) -> Option { + if self.serialize_settings.enable_tagging { + if num_mcids == 0 { + return None; + } + + let id = self.global_objects.struct_parents.len(); + self.global_objects + .struct_parents + .push(StructParentElement::Page(page_index, num_mcids)); + Some(i32::try_from(id).unwrap()) + } else { + None + } + } + + /// Register the struct parent integer in the parent tree. + /// The annotation parent must be later set using [`Self::set_annotation_parent`]. + pub(crate) fn register_annotation_parent(&mut self, ai: AnnotationIdentifier) -> Option { + if self.serialize_settings.enable_tagging { + let id = self.global_objects.struct_parents.len(); + self.global_objects + .struct_parents + .push(StructParentElement::Annotation(ai)); + Some(i32::try_from(id).unwrap()) + } else { + None + } + } + + pub(crate) fn register_named_destination(&mut self, nd: NamedDestination) -> Option { + if let Some((dest_ref, existing)) = + self.global_objects.named_destinations.get(nd.name.as_ref()) + { + return (existing == nd.xyz_dest.as_ref()).then_some(*dest_ref); + } + + let dest_ref = self.register_xyz_destination((*nd.xyz_dest).clone()); + self.global_objects + .named_destinations + .insert(nd.name.clone(), (dest_ref, (*nd.xyz_dest).clone())); + Some(dest_ref) + } + + pub(crate) fn register_page(&mut self, page: InternalPage) { + let ref_ = self.new_ref(); + self.page_infos.push(PageInfo::Krilla { + ref_, + surface_size: page.page_settings.surface_size(), + // Will be populated when the page is serialized. + annotations: vec![], + page_label: page.page_settings.page_label().clone(), + }); + self.global_objects.pages.push((ref_, page)); + } + + fn register_cached( + &mut self, + item: T, + mut func: impl FnMut(&mut Self, T, Ref), + ) -> Ref { + let hash = item.sip_hash(); + if let Some(_ref) = self.cached_mappings.get(&hash) { + *_ref + } else { + let root_ref = self.new_ref(); + func(self, item, root_ref); + self.cached_mappings.insert(hash, root_ref); + root_ref + } + } + + pub(crate) fn register_cacheable( + &mut self, + chunk_container: &mut ChunkContainer, + object: T, + ) -> Ref + where + T: Cacheable, + { + self.register_cached(object, |sc, object, root_ref| { + object.serialize(sc, chunk_container, root_ref); + }) + } + + pub(crate) fn register_resourceable( + &mut self, + chunk_container: &mut ChunkContainer, + object: T, + ) -> T::Resource + where + T: Resourceable, + { + Resource::new(self.register_cacheable(chunk_container, object)) + } + + #[cfg(feature = "raster-images")] + pub(crate) fn register_image( + &mut self, + chunk_container: &mut ChunkContainer, + image: Image, + ) -> Ref { + self.register_cached(image, |sc, object, root_ref| { + object.serialize(sc, chunk_container, root_ref); + }) + } + + pub(crate) fn register_xyz_destination(&mut self, dest: XyzDestination) -> Ref { + self.register_cached(dest, |sc, dest, root_ref| { + sc.global_objects.xyz_destinations.push((root_ref, dest)); + }) + } + + pub(crate) fn register_page_label( + &mut self, + chunk_container: &mut ChunkContainer, + page_label: PageLabel, + ) -> Ref { + let ref_ = self.new_ref(); + page_label.serialize(chunk_container, ref_); + ref_ + } + + pub(crate) fn register_font_identifier(&mut self, f: FontIdentifier) -> resource::Font { + let hash = f.sip_hash(); + if let Some(_ref) = self.cached_mappings.get(&hash) { + resource::Font::new(*_ref) + } else { + let root_ref = self.new_ref(); + self.cached_mappings.insert(hash, root_ref); + resource::Font::new(root_ref) + } + } + + pub(crate) fn register_colorspace( + &mut self, + chunk_container: &mut ChunkContainer, + cs: ColorSpace, + ) -> MaybeDeviceColorSpace { + match cs { + ColorSpace::CieBased(CieBasedColorSpace::Srgb) => { + MaybeDeviceColorSpace::ColorSpace(self.register_resourceable( + chunk_container, + ICCBasedColorSpace(self.serialize_settings.pdf_version().rgb_icc()), + )) + } + ColorSpace::CieBased(CieBasedColorSpace::Luma) => { + MaybeDeviceColorSpace::ColorSpace(self.register_resourceable( + chunk_container, + ICCBasedColorSpace(self.serialize_settings.pdf_version().grey_icc()), + )) + } + ColorSpace::CieBased(CieBasedColorSpace::Cmyk(cs)) => { + MaybeDeviceColorSpace::ColorSpace(self.register_resourceable(chunk_container, cs)) + } + ColorSpace::Device(DeviceColorSpace::Gray) => MaybeDeviceColorSpace::DeviceGray, + ColorSpace::Device(DeviceColorSpace::Rgb) => MaybeDeviceColorSpace::DeviceRgb, + ColorSpace::Device(DeviceColorSpace::Cmyk) => MaybeDeviceColorSpace::DeviceCMYK, + ColorSpace::Special(SpecialColorSpace::Separation(s)) => { + MaybeDeviceColorSpace::ColorSpace( + self.register_resourceable(chunk_container, SeparationColorSpace::new(s)), + ) + } + } + } +} + +/// Various serialization methods. +/// All methods are supposed to only be called once in `SerializeContext::finish`! +impl SerializeContext { + fn serialize_destination_profiles(&mut self, chunk_container: &mut ChunkContainer) { + let validators = self.serialize_settings.validators(); + chunk_container.non_stream.destination_profiles = + validators.output_intent().map(|subtype| { + let root_ref = self.new_ref(); + let mut chunk = self.new_chunk(); + + let oi_ref = self.new_ref(); + let mut oi = chunk.indirect(oi_ref).start::(); + let icc_profile = self.serialize_settings.pdf_version().rgb_icc(); + + oi.dest_output_profile( + self.register_cacheable(chunk_container, icc_profile.clone()), + ) + .subtype(subtype) + .output_condition_identifier(TextStr("Custom")) + .output_condition(TextStr("sRGB")) + .registry_name(TextStr("")) + .info(TextStr( + format!( + "sRGB v{}.{}", + icc_profile.metadata().major, + icc_profile.metadata().minor + ) + .as_str(), + )); + oi.finish(); + + let mut array = chunk.indirect(root_ref).array(); + array.item(oi_ref); + array.finish(); + + (root_ref, chunk) + }); + } + + fn serialize_page_label_tree(&mut self, chunk_container: &mut ChunkContainer) { + if let Some(container) = PageLabelContainer::new( + &self + .page_infos + .iter() + .map(|page| page.page_label().clone()) + .collect::>(), + ) { + let page_label_tree_ref = self.new_ref(); + container.serialize(self, chunk_container, page_label_tree_ref); + } + } + + fn serialize_outline(&mut self, chunk_container: &mut ChunkContainer) { + let outline = self.global_objects.outline.take(); + if let Some(outline) = &outline { + let outline_ref = self.new_ref(); + outline.serialize(self, chunk_container, outline_ref); + } else { + self.register_validation_error(ValidationError::MissingDocumentOutline); + } + } + + #[cfg(feature = "pdf")] + fn serialize_embedded_pdfs( + &mut self, + chunk_container: &mut ChunkContainer, + ) -> KrillaResult<()> { + let pdf_ctx = self.global_objects.pdf_ctx.take(); + + pdf_ctx.serialize(self, chunk_container) + } + + fn serialize_fonts(&mut self, chunk_container: &mut ChunkContainer) -> KrillaResult<()> { + let fonts = self.global_objects.font_map.take(); + for font_container in fonts.values() { + let borrowed = font_container.borrow(); + + if !borrowed.type3_mapper().is_empty() { + for t3_font in borrowed.type3_mapper().fonts() { + let f = self.register_font_identifier(t3_font.identifier()); + t3_font.serialize(self, chunk_container, f.get_ref()); + } + } + + if !borrowed.cid_font().is_empty() { + let f = self.register_font_identifier(borrowed.cid_font().identifier()); + borrowed + .cid_font() + .serialize(self, chunk_container, f.get_ref())?; + } + } + + Ok(()) + } + + fn serialize_pages(&mut self, chunk_container: &mut ChunkContainer) -> KrillaResult<()> { + let pages = self.global_objects.pages.take(); + for (ref_, page) in pages { + page.serialize(self, chunk_container, ref_)?; + } + + Ok(()) + } + + fn serialize_page_tree(&mut self, chunk_container: &mut ChunkContainer) { + let mut page_tree_chunk = self.new_chunk(); + page_tree_chunk + .pages(self.page_tree_ref) + .count(self.page_infos.len() as i32) + .kids(self.page_infos.iter().map(|i| i.ref_())); + chunk_container.non_stream.page_tree = Some((self.page_tree_ref, page_tree_chunk)); + } + + fn serialize_xyz_destinations( + &mut self, + chunk_container: &mut ChunkContainer, + ) -> KrillaResult<()> { + let xyz_destinations = self.global_objects.xyz_destinations.take(); + for (ref_, dest) in &xyz_destinations { + dest.serialize(self, chunk_container, *ref_); + } + + Ok(()) + } + + fn serialize_tag_tree(&mut self, chunk_container: &mut ChunkContainer) -> KrillaResult<()> { + let tag_tree = self.global_objects.tag_tree.take(); + let struct_parents = self.global_objects.struct_parents.take(); + if let Some(root) = &tag_tree { + let mut parent_tree_map = HashMap::new(); + let mut id_tree_map = BTreeMap::new(); + let struct_tree_root_ref = self.new_ref(); + let document_ref = root.serialize( + self, + chunk_container, + &mut parent_tree_map, + &mut id_tree_map, + struct_tree_root_ref, + )?; + + root.validate(&id_tree_map)?; + + let mut chunk = self.new_chunk(); + let mut tree = chunk + .indirect(struct_tree_root_ref) + .start::(); + + let mut sub_chunks = vec![]; + + if self.serialize_settings.pdf_version() < PdfVersion::Pdf20 { + let mut role_map = tree.role_map(); + // Custom structure elements. + role_map.insert(Name(b"Datetime"), StructRole::Span); + role_map.insert(Name(b"Terms"), StructRole::Part); + + // PDF 2.0 exclusive structure elements. + role_map.insert(Name(b"Title"), StructRole::P); + role_map.insert(Name(b"Strong"), StructRole::Span); + role_map.insert(Name(b"Em"), StructRole::Span); + for level in self.global_objects.custom_heading_roles.iter() { + let role2 = StructRole2::Heading(*level); + role_map.insert(role2.to_name(&mut [0; 6]), StructRole::P); + } + } else { + let mut namespaces = tree.namespaces(); + + // PDF 2.0 standard structure namespace + namespaces.item(self.pdf2_ns.ssn_ref); + let mut ns_chunk = self.new_chunk(); + ns_chunk.namespace(self.pdf2_ns.ssn_ref).pdf_2_ns(); + sub_chunks.push(ns_chunk); + + // Custom krilla namspace + namespaces.item(self.pdf2_ns.krilla_ref); + let mut ns_chunk = self.new_chunk(); + let mut ns = ns_chunk.namespace(self.pdf2_ns.krilla_ref); + ns.ns(TextStr("https://github.com/LaurenzV/krilla")); + + // Custom structure elements. + ns.role_map_ns() + .to_pdf_2_0(Name(b"Datetime"), StructRole2::Span, self.pdf2_ns.ssn_ref) + .to_pdf_2_0(Name(b"Terms"), StructRole2::Part, self.pdf2_ns.ssn_ref); + + ns.finish(); + sub_chunks.push(ns_chunk); + } + tree.children().item(document_ref); + + if !struct_parents.is_empty() { + let mut parent_tree = tree.parent_tree(); + let mut tree_nums = parent_tree.nums(); + + for (index, struct_parent) in struct_parents.iter().enumerate() { + match *struct_parent { + StructParentElement::Page(page_index, num_mcids) => { + let mut list_chunk = self.new_chunk(); + let list_ref = self.new_ref(); + + let mut refs = list_chunk.indirect(list_ref).array(); + + for mcid in 0..num_mcids { + let rci = PageTagIdentifier::new(page_index, mcid); + refs.item(parent_tree_map.get(&rci.into()).unwrap_or_else(|| { + panic!( + "page tag identifier {rci:?} doesn't appear in the tag tree" + ) + })); + } + + refs.finish(); + + sub_chunks.push(list_chunk); + tree_nums.insert(index as i32, list_ref); + } + StructParentElement::Annotation(ai) => { + // Write a reference to the parent structure element. + // From the PDF 1.7 spec (14.7.5.4 Finding structure elements from content items): + // > For an object identified as a content item by means of an object reference + // > (see 14.7.5.3, "PDF objects as content items"), the value shall be an + // > indirect reference to the parent structure element. + let page_annotations = &self.page_infos[ai.page_index].annotations(); + let parent_ref = + *page_annotations[ai.annot_index].1.get().unwrap_or_else(|| { + panic!("annotation identifier {ai:?} doesn't appear in the tag tree") + }); + tree_nums.insert(index as i32, parent_ref); + } + } + } + + tree_nums.finish(); + parent_tree.finish(); + } + + if !id_tree_map.is_empty() { + let mut id_tree = tree.id_tree(); + let mut names = id_tree.names(); + + for (name, ref_) in id_tree_map { + names.insert(Str(name.as_bytes()), ref_); + } + } + + if !struct_parents.is_empty() { + tree.parent_tree_next_key(struct_parents.len() as i32); + } + tree.finish(); + + for sub_chunk in sub_chunks { + chunk.extend(&sub_chunk); + } + + chunk_container.non_stream.struct_tree_root = Some((struct_tree_root_ref, chunk)); + } else { + self.register_validation_error(ValidationError::MissingTagging); + } + + Ok(()) + } + + fn check_validator_limits(&mut self) { + if self.cur_ref > Ref::new(8388607) { + self.register_validation_error(ValidationError::TooManyIndirectObjects) + } + + if self.limits.str_len() > STR_LEN { + self.register_validation_error(ValidationError::TooLongString); + } + + if self.limits.name_len() > NAME_LEN { + self.register_validation_error(ValidationError::TooLongName); + } + + if self.limits.real() > MAX_FLOAT { + self.register_validation_error(ValidationError::TooLargeFloat); + } + + if self.limits.array_len() > ARRAY_LEN { + self.register_validation_error(ValidationError::TooLongArray); + } + + if self.limits.dict_entries() > DICT_LEN { + self.register_validation_error(ValidationError::TooLongDictionary); + } + } + + fn check_version_limits(&self) -> Option { + if self.serialize_settings.pdf_version() != PdfVersion::Pdf14 { + return None; + } + + if self.limits.real() > MAX_FLOAT { + return Some(LimitError::TooLargeFloat); + } + + if self.limits.array_len() > ARRAY_LEN { + return Some(LimitError::TooLongArray); + } + + if self.limits.dict_entries() > DICT_LEN { + return Some(LimitError::TooLongDictionary); + } + + None + } +} + +/// This struct is essentially a thin wrapper around `std::mem::replace`. When finishing the +/// document, we need to take ownership of many of the items in `GlobalObjects` in order to +/// prevent having to clone them. However, the problem is that we cannot easily take ownership +/// of them, because they are part of the SerializeContext. Because of this, what we +/// do is that we `std::mem::replace` the elements step by step and then serialize them. +/// The `MaybeTaken` struct helps us to ensure that once we have taken a value, we do not +/// accidentally attempt to write/read it again. +pub(crate) struct MaybeTaken(Option); + +impl MaybeTaken { + pub(crate) fn new(item: T) -> Self { + Self(Some(item)) + } + + pub(crate) fn is_taken(&self) -> bool { + self.0.is_none() + } +} + +impl MaybeTaken { + #[track_caller] + pub(crate) fn take(&mut self) -> T { + self.0.take().expect("value was already taken before") + } +} + +impl Default for MaybeTaken { + fn default() -> Self { + Self::new(T::default()) + } +} + +impl Deref for MaybeTaken { + type Target = T; + + #[track_caller] + fn deref(&self) -> &Self::Target { + self.0.as_ref().expect("value was taken") + } +} + +impl DerefMut for MaybeTaken { + #[track_caller] + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.as_mut().expect("value was taken") + } +} + +pub(crate) struct Pdf2Namespaces { + /// The ref of the PDF 2.0 standard structure namspace (`https://www.iso.org/pdf2/ssn`). + pub(crate) ssn_ref: Ref, + /// The ref of the custom krilla namespace used for role mapping. + pub(crate) krilla_ref: Ref, +} + +#[derive(Default)] +pub(crate) struct GlobalObjects { + /// All named destinations that have been registered, including a Ref to their destination and + /// the destination itself. + // Needs to be pub(crate) because writing of named destinations happens in `ChunkContainer`. + pub(crate) named_destinations: MaybeTaken, (Ref, XyzDestination)>>, + /// A map from fonts to font container. + font_map: MaybeTaken>>>, + /// All XYZ destinations used in the document. The reason we need to store them + /// separately is that we can only serialize them in the very end, once all pages + /// have been written, so that we know the Ref of the page they belong to. + xyz_destinations: MaybeTaken>, + /// All pages and their corresponding chunks. Similarly to destinations, they need + /// to be written in the very end, because pages might contain annotations which in turn + /// depend on future pages (not written yet), so pages must also only be written in the + /// very end. + pages: MaybeTaken>, + /// Stores the struct parent elements. + struct_parents: MaybeTaken>, + /// Stores the document outline. + outline: MaybeTaken>, + /// Stores the tag tree. + tag_tree: MaybeTaken>, + /// Stores the association of the names of embedded files to their refs, + /// for the catalog dictionary. + pub(crate) embedded_files: MaybeTaken>, + /// A list of custom headings numbers used in the document. + pub(crate) custom_heading_roles: BTreeSet, + /// The context tracking all of the pdfs and their pages that have been inserted. + #[cfg(feature = "pdf")] + pub(crate) pdf_ctx: MaybeTaken, +} + +impl GlobalObjects { + pub(crate) fn assert_all_taken(&self) { + assert!(self.named_destinations.is_taken()); + assert!(self.font_map.is_taken()); + assert!(self.xyz_destinations.is_taken()); + assert!(self.pages.is_taken()); + assert!(self.struct_parents.is_taken()); + assert!(self.outline.is_taken()); + assert!(self.tag_tree.is_taken()); + assert!(self.embedded_files.is_taken()); + #[cfg(feature = "pdf")] + assert!(self.pdf_ctx.is_taken()); + } +} + +pub(crate) trait Cacheable: SipHashable { + fn serialize( + self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ); +} diff --git a/vendor/krilla/src/stream.rs b/vendor/krilla/src/stream.rs new file mode 100644 index 00000000..3d98a4d5 --- /dev/null +++ b/vendor/krilla/src/stream.rs @@ -0,0 +1,368 @@ +//! Drawing to a non-page context. +//! +//! In 90% of the cases, you will not have to use streams. In most cases, +//! all you need to do when using this crate is to first construct a document, +//! and then add new pages to the document and use the [`Page::surface`] method to get +//! access to the drawing context. However, there are cases when you don't want to +//! draw on the main page surface, but instead you want to create a "sub-surface" +//! where you can draw independently of the main page contents. This is what streams +//! are there for. Currently, there are only two situations where you need to do that: +//! +//! - When using masks and defining the contents of the mask. +//! - When using a pattern fill or stroke and defining the contents of the pattern. +//! +//! If you want to do any of the two above, you need to call the [`Surface::stream_builder`] method +//! of the current surface. The stream builder represents a kind of sub-context that is +//! independent of the main surface you are working with. Once you have a stream builder, you +//! can once again invoke the [`StreamBuilder::surface`] method, and use this new surface to define the contents +//! of your mask/pattern. In the end, you can call [`StreamBuilder::finish`] which will return a [`Stream`] object. +//! This [`Stream`] object contains the encoded instructions of the mask/pattern, which you can +//! then use to create new [`Pattern`]/[`Mask`] objects. +//! +//! [`Page::surface`]: crate::page::Page::surface +//! [`Surface::stream_builder`]: crate::surface::Surface::stream_builder +//! [`Pattern`]: crate::graphics::paint::Pattern +//! [`Mask`]: crate::graphics::mask::Mask + +use std::borrow::Cow; +use std::ops::DerefMut; + +use flate2::write::ZlibEncoder; +use flate2::Compression; +use pdf_writer::{Array, Dict, Name}; + +use crate::chunk_container::ChunkContainer; +use crate::configure::ValidationError; +use crate::content::ContentBuilder; +use crate::geom::{Rect, Transform}; +use crate::resource::{ResourceDictionary, ResourceDictionaryBuilder}; +use crate::serialize::SerializeContext; +use crate::surface::Surface; +use crate::SerializeSettings; + +/// A stream. +/// +/// See the module description for an explanation of its purpose. +// The only reason we implement clone for this type is that in some cases, +// we might need to clone a pattern (including its stream) +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub struct Stream { + pub(crate) content: Vec, + pub(crate) bbox: Rect, + // Important: Each object that uses a stream must ensure to pass on the validation + // errors to the `SerializeContext` at some point. Currently, only `Mask`, + // `TilingPattern`, `InternalPage` and `XObject` require that. + pub(crate) validation_errors: Vec, + pub(crate) resource_dictionary: ResourceDictionary, + pub(crate) uses_mask: bool, +} + +impl Stream { + pub(crate) fn new( + content: Vec, + bbox: Rect, + validation_errors: Vec, + resource_dictionary: ResourceDictionary, + uses_mask: bool, + ) -> Self { + Self { + content, + bbox, + validation_errors, + resource_dictionary, + uses_mask, + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.content.is_empty() + } + + pub(crate) fn empty() -> Self { + Self { + content: vec![], + bbox: Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(), + validation_errors: vec![], + resource_dictionary: ResourceDictionaryBuilder::new().finish(), + uses_mask: false, + } + } +} + +/// A builder to create streams. +pub struct StreamBuilder<'a> { + sc: &'a mut SerializeContext, + chunk_container: &'a mut ChunkContainer, + stream: Stream, +} + +impl<'a> StreamBuilder<'a> { + pub(crate) fn new( + sc: &'a mut SerializeContext, + chunk_container: &'a mut ChunkContainer, + ) -> Self { + Self { + sc, + chunk_container, + stream: Stream::empty(), + } + } + + /// Get the surface of the stream builder. + pub fn surface(&mut self) -> Surface<'_> { + // Stream builders cannot have any tags since we always pass a dummy + // identifier. Only main page content streams can have one. + let finish_fn = Box::new(|stream, _| { + self.stream = stream; + }); + + Surface::new( + self.sc, + self.chunk_container, + ContentBuilder::new(Transform::identity(), true, self.sc), + None, + finish_fn, + ) + } + + /// Turn the stream builder into a stream. + pub fn finish(self) -> Stream { + self.stream + } +} + +/// A PDF stream filter. +#[derive(Debug, Copy, Clone)] +pub(crate) enum StreamFilter { + Flate, + FlateMemoized, + AsciiHex, + #[cfg(feature = "raster-images")] + Dct, +} + +impl StreamFilter { + pub(crate) fn to_name(self) -> Name<'static> { + match self { + Self::AsciiHex => Name(b"ASCIIHexDecode"), + Self::Flate => Name(b"FlateDecode"), + Self::FlateMemoized => Name(b"FlateDecode"), + #[cfg(feature = "raster-images")] + Self::Dct => Name(b"DCTDecode"), + } + } + + pub(crate) fn is_binary(&self) -> bool { + match self { + StreamFilter::Flate => true, + StreamFilter::FlateMemoized => true, + StreamFilter::AsciiHex => false, + #[cfg(feature = "raster-images")] + StreamFilter::Dct => true, + } + } +} + +impl StreamFilter { + #[track_caller] + pub(crate) fn apply(&self, content: &[u8]) -> Vec { + match self { + StreamFilter::Flate => deflate_encode(content), + StreamFilter::FlateMemoized => deflate_encode_memoized(content), + StreamFilter::AsciiHex => hex_encode(content), + // Note: We don't actually encode manually with DCT, because + // this is only used for JPEG images which are already encoded, + // so this shouldn't be called at all. + #[cfg(feature = "raster-images")] + StreamFilter::Dct => panic!("can't apply dct decode"), + } + } +} + +/// Allows us to keep track of the filters that a stream has and +/// apply them in an orderly fashion. +#[derive(Debug, Clone)] +pub(crate) enum StreamFilters { + None, + Single(StreamFilter), + Multiple(Vec), +} + +impl StreamFilters { + pub(crate) fn add(&mut self, stream_filter: StreamFilter) { + match self { + StreamFilters::None => *self = StreamFilters::Single(stream_filter), + StreamFilters::Single(cur) => { + *self = StreamFilters::Multiple(vec![*cur, stream_filter]) + } + StreamFilters::Multiple(cur) => cur.push(stream_filter), + } + } + + pub(crate) fn is_binary(&self) -> bool { + match self { + StreamFilters::None => false, + StreamFilters::Single(s) => s.is_binary(), + StreamFilters::Multiple(m) => m.last().unwrap().is_binary(), + } + } +} + +pub(crate) struct FilterStreamBuilder<'a> { + content: Cow<'a, [u8]>, + filters: StreamFilters, + is_binary: bool, +} + +impl<'a> FilterStreamBuilder<'a> { + fn empty(content: &'a [u8]) -> Self { + Self { + content: Cow::Borrowed(content), + filters: StreamFilters::None, + is_binary: false, + } + } + + fn binary(content: &'a [u8]) -> Self { + Self { + is_binary: true, + ..Self::empty(content) + } + } + + pub(crate) fn new_from_content_stream( + content: &'a [u8], + serialize_settings: &SerializeSettings, + ) -> Self { + let mut filter_stream = Self::empty(content); + + if serialize_settings.compress_content_streams { + filter_stream.add_filter(StreamFilter::FlateMemoized); + } + + filter_stream + } + + pub(crate) fn new_from_deflated(content: &'a [u8]) -> Self { + let mut filter_stream = Self::empty(content); + filter_stream.add_unapplied_filter(StreamFilter::Flate); + + filter_stream + } + + pub(crate) fn new_from_binary_data(content: &'a [u8]) -> Self { + let mut filter_stream = Self::binary(content); + filter_stream.add_filter(StreamFilter::Flate); + + filter_stream + } + + pub(crate) fn new_auto_compressed(content: &'a [u8]) -> Self { + let filter_stream = Self::new_from_binary_data(content); + + const MAX_COMPRESSED_SIZE: usize = 75; + if content.is_empty() + || 100 * filter_stream.content.len() / content.len() > MAX_COMPRESSED_SIZE + { + return Self::binary(content); + } + + filter_stream + } + + pub(crate) fn new_from_uncompressed(content: &'a [u8]) -> Self { + Self::binary(content) + } + + #[cfg(feature = "raster-images")] + pub(crate) fn new_from_jpeg_data(content: &'a [u8]) -> Self { + let mut filter_stream = Self::empty(content); + // JPEG data already is DCT encoded. + filter_stream.add_unapplied_filter(StreamFilter::Dct); + + filter_stream + } + + pub(crate) fn finish(mut self, serialize_settings: &SerializeSettings) -> FilterStream<'a> { + if serialize_settings.ascii_compatible + && !self.content.is_empty() + && (self.filters.is_binary() + || (self.is_binary && self.content.iter().any(|byte| !byte.is_ascii()))) + { + self.add_filter(StreamFilter::AsciiHex); + } + + FilterStream { + content: self.content, + filters: self.filters, + } + } + + fn add_filter(&mut self, filter: StreamFilter) { + self.content = Cow::Owned(filter.apply(&self.content)); + self.filters.add(filter); + } + + fn add_unapplied_filter(&mut self, filter: StreamFilter) { + self.filters.add(filter); + } +} + +pub(crate) struct FilterStream<'a> { + content: Cow<'a, [u8]>, + filters: StreamFilters, +} + +impl FilterStream<'_> { + pub(crate) fn encoded_data(&self) -> &[u8] { + &self.content + } + + pub(crate) fn write_filters<'b, T>(&self, mut dict: T) + where + T: DerefMut>, + { + match &self.filters { + StreamFilters::None => {} + StreamFilters::Single(filter) => { + dict.deref_mut().pair(Name(b"Filter"), filter.to_name()); + } + StreamFilters::Multiple(filters) => { + dict.deref_mut() + .insert(Name(b"Filter")) + .start::() + .items(filters.iter().map(|f| f.to_name()).rev()); + } + } + } +} + +#[cfg_attr(feature = "comemo", comemo::memoize)] +fn deflate_encode_memoized(data: &[u8]) -> Vec { + deflate_encode(data) +} + +pub(crate) fn deflate_encode(data: &[u8]) -> Vec { + use std::io::Write; + + const COMPRESSION_LEVEL: u8 = 6; + let mut e = ZlibEncoder::new(Vec::new(), Compression::new(COMPRESSION_LEVEL as u32)); + e.write_all(data).unwrap(); + e.finish().unwrap() +} + +fn hex_encode(data: &[u8]) -> Vec { + use std::fmt::Write; + + const COLUMNS: usize = 35; + let len = data.len(); + let capacity = 2 * len + len / COLUMNS; + let mut output = String::with_capacity(capacity); + for (index, byte) in data.iter().enumerate() { + write!(output, "{byte:02X}").unwrap(); + if index % COLUMNS == COLUMNS - 1 { + output.push('\n'); + } + } + output.into_bytes() +} diff --git a/vendor/krilla/src/surface.rs b/vendor/krilla/src/surface.rs new file mode 100644 index 00000000..990527f8 --- /dev/null +++ b/vendor/krilla/src/surface.rs @@ -0,0 +1,675 @@ +//! Drawing onto a surface. +//! +//! This module contains most core part of krilla: The [`Surface`] struct. A surface +//! represents a drawing area on which you can define the contents of your page. This includes +//! operations such as applying linear transformations, showing text or images and drawing paths. + +use std::num::NonZeroU64; + +use crate::chunk_container::ChunkContainer; +use crate::color::rgb; +use crate::configure::validate::VersionedFeature; +use crate::configure::ValidationError; +use crate::content::ContentBuilder; +use crate::geom::Path; +#[cfg(any(feature = "raster-images", feature = "pdf"))] +use crate::geom::Size; +use crate::geom::{Point, Transform}; +use crate::graphic::Graphic; +use crate::graphics::blend::BlendMode; +use crate::graphics::graphics_state::ExtGState; +#[cfg(feature = "raster-images")] +use crate::graphics::image::Image; +use crate::graphics::mask::Mask; +use crate::graphics::paint::{Fill, FillRule, Stroke}; +use crate::graphics::shading_function::ShadingFunction; +use crate::interchange::tagging::{ContentTag, Identifier, PageTagIdentifier}; +use crate::num::NormalizedF32; +use crate::paint::{InnerPaint, Paint}; +#[cfg(feature = "pdf")] +use crate::pdf::PdfDocument; +use crate::serialize::SerializeContext; +use crate::stream::{Stream, StreamBuilder}; +use crate::tagging::ArtifactType; +use crate::text::Font; +use crate::text::{draw_glyph, Glyph}; +#[cfg(feature = "simple-text")] +use crate::text::{shape::naive_shape, TextDirection}; + +/// Can be used to associate render operations with a unique identifier. +/// This is useful if you want to backtrack a validation error to a specific +/// identifier chosen by you. +pub type Location = NonZeroU64; + +/// A surface. +/// +/// Represents a drawing area for defining graphical content. The origin of the +/// coordinate axis is in the top-left corner. +/// +/// You cannot directly create an instance of a [`Surface`] yourself. +/// Instead, there are two ways of getting access to a surface, which you can then use to draw on: +/// +/// - The first way, and also the most common one you will use, is by creating a new document, +/// adding a page to it and then invoking the [`Page::surface`] method. The surface returned as part of +/// that represents the drawing area of the page. +/// - The second way is by calling the [`Surface::stream_builder`] method on the surface, to create a sub-drawing +/// context. See the documentation of the [`stream`] module for more information. +/// +/// The surface uses a `push` and `pop` based mechanism for applying certain actions. For example, +/// there is a [`Surface::push_transform`] method which allows you to concatenate a new transform to the +/// current transform matrix. There is also a [`Surface::push_clip_path`] method, which allows you to +/// intersect the current drawing area with a clip path. Once you call such a `push` method, +/// the action that it invokes will be in place until you call the `pop` method, which will then +/// revert the last `push` operation. This allows you to, for example, define a clipping path area +/// or a mask to use only for certain objects. +/// +/// It is important that, for each `push` method you invoke, there must be a corresponding `pop` +/// invocation that reverts it. If, at the end of the using the surface, the number of pushes/pops is +/// uneven, the program will panic. +/// +/// [`stream`]: crate::stream +/// [`Page::surface`]: crate::page::Page::surface +pub struct Surface<'a> { + pub(crate) sc: &'a mut SerializeContext, + pub(crate) chunk_container: &'a mut ChunkContainer, + fill: Option, + stroke: Option, + bd: Builders, + push_instructions: Vec, + page_identifier: Option, + finish_fn: Box, +} + +impl<'a> Surface<'a> { + pub(crate) fn new( + sc: &'a mut SerializeContext, + chunk_container: &'a mut ChunkContainer, + root_builder: ContentBuilder, + page_identifier: Option, + finish_fn: Box, + ) -> Surface<'a> { + Self { + sc, + chunk_container, + bd: Builders::new(root_builder), + page_identifier, + fill: None, + stroke: None, + push_instructions: vec![], + finish_fn, + } + } + + /// Return a `StreamBuilder` to allow drawing on a sub-context. + pub fn stream_builder(&mut self) -> StreamBuilder<'_> { + StreamBuilder::new(self.sc, self.chunk_container) + } + + /// Set the fill that should be used for the next drawing operation. + /// + /// You can set it to `None` if you want to disable filling (though + /// if there is no active stroke, than krilla will choose to fill + /// with black by default). + pub fn set_fill(&mut self, fill: Option) { + self.fill = fill; + } + + /// Get the currently active fill. + pub fn get_fill(&mut self) -> Option<&Fill> { + self.fill.as_ref() + } + + /// Set the stroke that should be used for drawing operations. + /// + /// You can set it to `None` if you want to disable stroking. + pub fn set_stroke(&mut self, stroke: Option) { + self.stroke = stroke; + } + + /// Get the currently active stroke. + pub fn get_stroke(&mut self) -> Option<&Stroke> { + self.stroke.as_ref() + } + + /// Draw a path using the currently active fill and/or stroke. + pub fn draw_path(&mut self, path: &Path) { + if self.fill.is_some() || self.stroke.is_some() { + if self.has_complex_fill_or_stroke() { + self.bd.get_mut().draw_path( + &path.0, + self.fill.as_ref(), + None, + self.sc, + self.chunk_container, + ); + self.bd.get_mut().draw_path( + &path.0, + None, + self.stroke.as_ref(), + self.sc, + self.chunk_container, + ); + } else { + self.bd.get_mut().draw_path( + &path.0, + self.fill.as_ref(), + self.stroke.as_ref(), + self.sc, + self.chunk_container, + ); + } + } else { + // Draw with black by default. + self.bd.get_mut().draw_path( + &path.0, + Some(&Fill::default()), + None, + self.sc, + self.chunk_container, + ); + } + } + + /// Start a new tagged content section. + /// + /// # Panics + /// Panics if a tagged section has already been started. + /// Panics if an artifact with a required bbox has none. + pub fn start_tagged(&mut self, tag: ContentTag) -> Identifier { + if let Some(id) = &mut self.page_identifier { + match tag { + // An artifact is actually not really part of tagged PDF and doesn't have + // a marked content identifier, so we need to return a dummy one here. It's just + // the API of krilla that conflates artifacts with tagged content, + // for the sake of simplicity. But the user of the library does not need to know + // about this. + ContentTag::Artifact(artifact) => { + if matches!(artifact.kind, ArtifactType::Header | ArtifactType::Footer) + && self.sc.serialize_settings().pdf_version() + < VersionedFeature::HeaderFooterArtifactSubtypes.minimum_pdf_version() + { + self.sc.register_validation_error( + ValidationError::RequiresNewerPdfVersion( + VersionedFeature::HeaderFooterArtifactSubtypes, + self.sc.location, + ), + ); + } + + if artifact.requires_properties(self.sc.serialize_settings().pdf_version()) { + self.bd + .get_mut() + .start_marked_content_with_properties(self.sc, None, tag); + } else { + self.bd.get_mut().start_marked_content(tag.name()); + } + + if artifact.requires_bbox(self.sc.serialize_settings().pdf_version()) + && artifact.bbox.is_none() + { + panic!("Background artifact must have a bounding box in PDF 1.7"); + } + + Identifier::dummy() + } + ContentTag::Span(_) | ContentTag::Other => { + self.bd.get_mut().start_marked_content_with_properties( + self.sc, + Some(id.mcid), + tag, + ); + id.bump().into() + } + } + } else { + Identifier::dummy() + } + } + + /// End the current tagged section. + /// + /// # Panics + /// Panics if no tagged section has been started. + pub fn end_tagged(&mut self) { + if self.page_identifier.is_some() { + self.bd.get_mut().end_marked_content(); + } + } + + fn outline_glyphs( + &mut self, + glyphs: &[impl Glyph], + context_color: rgb::Color, + start: Point, + font: Font, + font_size: f32, + ) { + let (mut cur_x, y) = (start.x, start.y); + + for glyph in glyphs { + let mut base_transform = tiny_skia_path::Transform::from_translate( + cur_x + glyph.x_offset(font_size), + y - glyph.y_offset(font_size), + ); + base_transform = base_transform.pre_concat(tiny_skia_path::Transform::from_scale( + font_size / font.units_per_em(), + -font_size / font.units_per_em(), + )); + draw_glyph( + font.clone(), + context_color, + glyph.glyph_id(), + Transform::from_tsp(base_transform), + self, + ); + + cur_x += glyph.x_advance(font_size); + } + } + + /// Draw a sequence of glyphs using the currently active fill and/or stroke. + /// + /// This is a very low-level method, which gives you full control over how to place + /// the glyphs that make up the text. This means that you must have your own text processing + /// logic for dealing with bidirectional text, font fallback, text layouting, etc. + pub fn draw_glyphs( + &mut self, + start: Point, + glyphs: &[impl Glyph], + font: Font, + text: &str, + font_size: f32, + outlined: bool, + ) { + let context_color = self.context_color(); + if outlined { + self.outline_glyphs(glyphs, context_color, start, font, font_size); + } else { + match (self.fill.as_ref(), self.stroke.as_ref()) { + (Some(f), Some(s)) => { + if self.has_complex_fill_or_stroke() { + // We need to draw fill and stroke separately. In order to avoid embedding + // text twice, we fill the text as normal text and then stroke it as a path. + self.bd.get_mut().draw_glyphs( + start, + self.sc, + self.chunk_container, + Some(f), + None, + context_color, + glyphs, + font.clone(), + text, + font_size, + ); + + self.outline_glyphs(glyphs, context_color, start, font, font_size); + } else { + self.bd.get_mut().draw_glyphs( + start, + self.sc, + self.chunk_container, + Some(f), + Some(s), + context_color, + glyphs, + font, + text, + font_size, + ); + } + } + (None, Some(s)) => { + self.bd.get_mut().draw_glyphs( + start, + self.sc, + self.chunk_container, + None, + Some(s), + context_color, + glyphs, + font, + text, + font_size, + ); + } + (Some(f), None) => { + self.bd.get_mut().draw_glyphs( + start, + self.sc, + self.chunk_container, + Some(f), + None, + context_color, + glyphs, + font, + text, + font_size, + ); + } + (None, None) => { + // Draw with black by default. + self.bd.get_mut().draw_glyphs( + start, + self.sc, + self.chunk_container, + Some(&Fill::default()), + None, + context_color, + glyphs, + font, + text, + font_size, + ); + } + } + } + } + + /// Draw some text using the currently active fill and/or stroke. + /// + /// This is a high-level method which allows you to just provide some text, which will + /// then be rendered into a single line. However, this approach has restrictions: + /// + /// - It will not perform BIDI resolution and only supports a single script, meaning that you + /// must ensure that your text does not contain multiple scripts. + /// - It will only use the single font you provided to draw the text, no font fallback will + /// be performed. + /// + /// If you need more advanced control over how your text looks, + /// you can use the `fill_glyphs` method. + #[cfg(feature = "simple-text")] + pub fn draw_text( + &mut self, + start: Point, + font: Font, + font_size: f32, + text: &str, + outlined: bool, + direction: TextDirection, + ) { + let glyphs = naive_shape(text, font.clone(), direction); + + self.draw_glyphs(start, &glyphs, font, text, font_size, outlined); + } + + /// Set the location that should be assumed for subsequent operations. + pub fn set_location(&mut self, location: Location) { + self.sc.set_location(location); + } + + /// Reset the location that should be assumed for subsequent operations. + pub fn reset_location(&mut self) { + self.sc.reset_location(); + } + + /// Return the current transformation matrix. + pub fn ctm(&self) -> Transform { + self.bd.get().cur_transform() + } + + /// Concatenate a new transform to the current transformation matrix. + pub fn push_transform(&mut self, transform: &Transform) { + self.push_instructions.push(PushInstruction::Transform); + self.bd.get_mut().save_graphics_state(); + self.bd.get_mut().concat_transform(transform); + } + + /// Push a new blend mode. + pub fn push_blend_mode(&mut self, blend_mode: BlendMode) { + self.push_instructions.push(PushInstruction::BlendMode); + self.bd.get_mut().save_graphics_state(); + self.bd.get_mut().set_blend_mode(blend_mode.to_pdf()); + } + + /// Push a new clip path. + pub fn push_clip_path(&mut self, path: &Path, clip_rule: &FillRule) { + self.push_instructions.push(PushInstruction::ClipPath); + self.bd.get_mut().push_clip_path(&path.0, clip_rule); + } + + /// Push a new mask. + pub fn push_mask(&mut self, mask: Mask) { + self.push_instructions + .push(PushInstruction::Mask(Box::new(mask))); + self.bd + .sub_builders + .push(ContentBuilder::new(Transform::identity(), true, self.sc)); + } + + #[cfg(feature = "pdf")] + /// Embed a single PDF page with the given dimensions. + pub fn draw_pdf_page(&mut self, pdf: &PdfDocument, size: Size, page_idx: usize) { + use crate::geom::Rect; + + let obj_ref = self.sc.embed_pdf_page_as_xobject(pdf, page_idx); + // If the user provided an invalid page index, we will detect this later on anyway, so + // just use dummy dimensions here. + let (page_width, page_height) = pdf + .pages() + .get(page_idx) + .map(|p| p.render_dimensions()) + .unwrap_or((1.0, 1.0)); + let transform = + Transform::from_scale(size.width() / page_width, size.height() / page_height); + // We applied a transform to invert the y-axis initially, but since the embedded PDF also uses + // a y-up coordinate system, we need to "invert" this. + self.push_transform(&transform); + self.push_transform(&Transform::from_row(1.0, 0.0, 0.0, -1.0, 0.0, page_height)); + + self.bd.get_mut().draw_xobject_by_reference( + self.sc, + self.chunk_container, + Rect::from_xywh(0.0, 0.0, page_width, page_height).unwrap(), + obj_ref, + ); + + self.pop(); + self.pop(); + } + + /// Push a new opacity, meaning that each subsequent graphics object will be + /// rendered with a base opacity. + /// + /// This stacks, meaning that if you do `push_opacity(0.5)` twice, the resulting + /// base opacity will be 0.25. + pub fn push_opacity(&mut self, opacity: NormalizedF32) { + self.push_instructions + .push(PushInstruction::Opacity(opacity)); + + if opacity != NormalizedF32::ONE { + self.bd + .sub_builders + .push(ContentBuilder::new(Transform::identity(), true, self.sc)); + } + } + + /// Push a new isolated layer. + pub fn push_isolated(&mut self) { + self.push_instructions.push(PushInstruction::Isolated); + self.bd + .sub_builders + .push(ContentBuilder::new(Transform::identity(), true, self.sc)); + } + + /// Pop the last `push` instruction. + /// + /// # Panics + /// Panics if the there wasn't a corresponding `push` to the `pop`. + pub fn pop(&mut self) { + match self.push_instructions.pop().unwrap() { + PushInstruction::Transform => self.bd.get_mut().restore_graphics_state(), + PushInstruction::Opacity(o) => { + if o != NormalizedF32::ONE { + let stream = self.bd.sub_builders.pop().unwrap().finish(self.sc); + self.bd + .get_mut() + .draw_opacified(self.sc, self.chunk_container, o, stream); + } + } + PushInstruction::ClipPath => self.bd.get_mut().pop_clip_path(), + PushInstruction::BlendMode => self.bd.get_mut().restore_graphics_state(), + PushInstruction::Mask(mask) => { + let stream = self.bd.sub_builders.pop().unwrap().finish(self.sc); + self.bd + .get_mut() + .draw_masked(self.sc, self.chunk_container, *mask, stream) + } + PushInstruction::Isolated => { + let stream = self.bd.sub_builders.pop().unwrap().finish(self.sc); + self.bd + .get_mut() + .draw_isolated(self.sc, self.chunk_container, stream); + } + } + } + + #[cfg(feature = "raster-images")] + /// Draw a new bitmap image. + pub fn draw_image(&mut self, image: Image, size: Size) { + self.bd + .get_mut() + .draw_image(image, size, self.sc, self.chunk_container); + } + + /// Draw a new graphic. + /// + /// Drawing the same graphic multiple times is very cheap in terms of + /// file size. + pub fn draw_graphic(&mut self, graphic: Graphic) { + self.bd.get_mut().draw_xobject( + self.sc, + self.chunk_container, + graphic.x_object, + &ExtGState::new(), + ) + } + + pub(crate) fn draw_shading(&mut self, shading: &ShadingFunction) { + self.bd + .get_mut() + .draw_shading(shading, self.sc, self.chunk_container); + } + + /// A convenience method for `std::mem::drop`. + /// + /// # Panics + /// Panics if the push/pop difference is not 0. + pub fn finish(self) {} + + pub(crate) fn draw_opacified_stream(&mut self, opacity: NormalizedF32, stream: Stream) { + self.bd + .get_mut() + .draw_opacified(self.sc, self.chunk_container, opacity, stream) + } + + /// Return the current transformation matrix of the surface. + pub fn cur_transform(&self) -> Transform { + self.bd.get().cur_transform() + } + + fn context_color(&self) -> rgb::Color { + self.fill + .as_ref() + .and_then(|f| f.paint.as_rgb()) + .or_else(|| self.stroke.as_ref().and_then(|s| s.paint.as_rgb())) + .unwrap_or_default() + } + + fn has_complex_fill_or_stroke(&self) -> bool { + // Things can go wrong and yield inconsistent results in different PDF viewers + // for some fill/stroke combinations. So in some cases, we need to fill and stroke + // separately. + // 1) We have an opacity on the stroke, because they render inconsistently across different + // viewers. + // 2) We have a linear gradient with opacities in at least one stop, because then we need + // to invoke a soft mask, in which case the fill/stroke would both be affected by this. + + let check_paint = |paint: &Paint| match &paint.0 { + InnerPaint::Color(_) => false, + InnerPaint::LinearGradient(l) => { + l.stops.iter().any(|s| s.opacity != NormalizedF32::ONE) + } + InnerPaint::RadialGradient(r) => { + r.stops.iter().any(|s| s.opacity != NormalizedF32::ONE) + } + InnerPaint::SweepGradient(r) => r.stops.iter().any(|s| s.opacity != NormalizedF32::ONE), + InnerPaint::Pattern(_) => false, + }; + + let complex_stroke = self + .stroke + .as_ref() + .is_some_and(|s| s.opacity != NormalizedF32::ONE || check_paint(&s.paint)); + + let complex_fill = self.fill.as_ref().is_some_and(|f| check_paint(&f.paint)); + + complex_fill || complex_stroke + } +} + +impl Drop for Surface<'_> { + fn drop(&mut self) { + // Keep the document poisoned if cleanup unwinds, including an invariant failure + // during an explicit finish. Successful cleanup preserves any earlier poison. + let poisoned = std::mem::replace(&mut self.sc.poisoned, true); + // A drawing panic can leave surface state incomplete. Discard it while + // unwinding; finalization here could panic again and abort the process. + // The caller must discard the whole document after a caught panic. + if std::thread::panicking() { + return; + } + + let root_builder = std::mem::replace( + &mut self.bd.root_builder, + ContentBuilder::new(Transform::identity(), false, self.sc), + ); + let num_mcids = match self.page_identifier { + Some(pi) => pi.mcid, + None => 0, + }; + + assert!(self.bd.sub_builders.is_empty()); + assert!(self.push_instructions.is_empty()); + assert!(!root_builder.active_marked_content); + + (self.finish_fn)(root_builder.finish(self.sc), num_mcids); + self.sc.poisoned = poisoned; + } +} + +/// Holds the different content streams we are currently building. In the usual case, +/// this only contains the current page stream as the root builder, but the sub builders +/// will be used if we are for example creating a mask/pattern, or an XObject. +struct Builders { + pub(crate) root_builder: ContentBuilder, + pub(crate) sub_builders: Vec, +} + +impl Builders { + fn new(root_builder: ContentBuilder) -> Self { + Self { + root_builder, + sub_builders: vec![], + } + } + + fn get_mut(&mut self) -> &mut ContentBuilder { + self.sub_builders + .last_mut() + .unwrap_or(&mut self.root_builder) + } + + fn get(&self) -> &ContentBuilder { + self.sub_builders.last().unwrap_or(&self.root_builder) + } +} + +pub(crate) enum PushInstruction { + Transform, + Opacity(NormalizedF32), + ClipPath, + BlendMode, + Mask(Box), + Isolated, +} diff --git a/vendor/krilla/src/text/cid.rs b/vendor/krilla/src/text/cid.rs new file mode 100644 index 00000000..2c521271 --- /dev/null +++ b/vendor/krilla/src/text/cid.rs @@ -0,0 +1,488 @@ +use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; +use pdf_writer::writers::WMode; +use pdf_writer::{Finish, Name, Ref, Str}; +use rustc_hash::FxHashMap; +use skrifa::instance::Size; +use skrifa::outline::DrawSettings; +use skrifa::prelude::LocationRef; +use skrifa::raw::tables::cff::Cff; +use skrifa::raw::{TableProvider, TopLevelTable}; +use std::hash::Hash; +use std::ops::DerefMut; +use std::sync::Arc; +use subsetter::GlyphRemapper; + +use super::{CIDIdentifier, FontIdentifier, PDF_UNITS_PER_EM}; +use crate::chunk_container::ChunkContainer; +use crate::configure::ValidationError; +use crate::error::{KrillaError, KrillaResult}; +use crate::geom::Rect; +use crate::serialize::SerializeContext; +use crate::stream::FilterStreamBuilder; +use crate::surface::Location; +use crate::text::outline::OutlineBuilder; +use crate::text::Font; +use crate::text::GlyphId; +use crate::util::{stable_hash128, SliceExt}; + +const SUBSET_TAG_LEN: usize = 6; +pub(crate) const IDENTITY_H: &str = "Identity-H"; +pub(crate) const CMAP_NAME: Name = Name(b"Custom"); +pub(crate) const SYSTEM_INFO: SystemInfo = SystemInfo { + registry: Str(b"Adobe"), + ordering: Str(b"Identity"), + supplement: 0, +}; + +pub(crate) type Cid = u16; + +/// A shared function for CID fonts and Type3 fonts to write the cmap entries. +pub(crate) fn write_cmap_entry( + font: &Font, + entry: Option<&(String, Option)>, + sc: &mut SerializeContext, + cmap: &mut UnicodeCmap, + g: G, +) where + G: pdf_writer::types::GlyphId + Into + Copy, +{ + match entry { + None => sc.register_validation_error(ValidationError::NoCodepointMapping( + font.clone(), + GlyphId::new(g.into()), + None, + )), + Some((text, loc)) => { + let mut invalid_codepoint = text.is_empty(); + let mut invalid_code = None; + let mut private_unicode = None; + + for c in text.chars() { + if matches!(c as u32, 0x0 | 0xFEFF | 0xFFFE) { + invalid_code = Some(c); + invalid_codepoint = true; + } + + if matches!(c as u32, 0xE000..=0xF8FF | 0xF0000..=0xFFFFD | 0x100000..=0x10FFFD) { + private_unicode = Some(c); + } + } + + match invalid_code { + Some(c) => sc.register_validation_error(ValidationError::InvalidCodepointMapping( + font.clone(), + GlyphId::new(g.into()), + c, + *loc, + )), + None if invalid_codepoint => sc.register_validation_error( + ValidationError::NoCodepointMapping(font.clone(), GlyphId::new(g.into()), *loc), + ), + _ => {} + } + + if let Some(code) = private_unicode { + sc.register_validation_error(ValidationError::UnicodePrivateArea( + font.clone(), + GlyphId::new(g.into()), + code, + *loc, + )); + } + + if !text.is_empty() { + cmap.pair_with_multiple(g, text.chars()); + } + } + } +} + +/// A CID-keyed font. +#[derive(Debug)] +pub(crate) struct CIDFont { + /// The _actual_ underlying OTF font of the CID-keyed font. + font: Font, + /// A mapper that maps GIDs from the original font to CIDs, i.e. the corresponding GID in the font + /// subset. The subsetter will ensure that for CID-keyed CFF fonts, the CID-to-GID mapping + /// will be the identity mapping, regardless of what the mapping was in the original font. This + /// allows us to index both, CFF and glyf-based fonts, transparently using GIDs, + /// instead of having to distinguish according to the underlying font. See section + /// 9.7.4.2 for more information on how glyphs are indexed in a CID-keyed font. + glyph_remapper: GlyphRemapper, + /// A mapping from CIDs to their string in the original text. + cmap_entries: FxHashMap)>, + /// The widths of the glyphs, _indexed by their CID_. + widths: Vec, + is_empty: bool, +} + +impl CIDFont { + /// Create a new CID-keyed font. + pub(crate) fn new(font: Font) -> CIDFont { + // Always include the .notdef glyph. Will also always be included by the subsetter in + // the glyph remapper. + let widths = vec![font.advance_width(GlyphId::new(0)).unwrap_or(0.0)]; + + Self { + glyph_remapper: GlyphRemapper::new(), + cmap_entries: FxHashMap::default(), + widths, + font, + is_empty: true, + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.is_empty + } + + pub(crate) fn font(&self) -> Font { + self.font.clone() + } + + // Note that this refers to the units per em in PDF (which is always 1000), and not the + // units per em of the underlying font. + pub(crate) fn units_per_em(&self) -> f32 { + PDF_UNITS_PER_EM + } + + #[inline] + pub(crate) fn get_cid(&self, glyph_id: GlyphId) -> Option { + self.glyph_remapper.get(glyph_id.to_u32() as u16) + } + + /// Add a new glyph (if it has not already been added) and return its CID. + #[inline] + pub(crate) fn add_glyph(&mut self, glyph_id: GlyphId) -> Cid { + self.is_empty = false; + + let new_id = self + .glyph_remapper + .remap(u16::try_from(glyph_id.to_u32()).unwrap()); + + // This means that the glyph ID has been newly assigned, and thus we need to add its width. + if new_id as usize >= self.widths.len() { + self.widths + .push(self.font.advance_width(glyph_id).unwrap_or(0.0)); + } + + new_id + } + + #[inline] + pub(crate) fn get_codepoints(&self, cid: Cid) -> Option<&str> { + self.cmap_entries.get(&cid).map(|s| s.0.as_str()) + } + + #[inline] + pub(crate) fn set_codepoints(&mut self, cid: Cid, text: String, location: Option) { + self.cmap_entries.insert(cid, (text, location)); + } + + #[inline] + pub(crate) fn identifier(&self) -> FontIdentifier { + FontIdentifier::Cid(CIDIdentifier(self.font.clone())) + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) -> KrillaResult<()> { + let chunk = &mut chunk_container.non_stream.fonts; + let mut stream_chunk = sc.new_chunk(); + + let cid_ref = sc.new_ref(); + let descriptor_ref = sc.new_ref(); + let cmap_ref = sc.new_ref(); + let cid_set_ref = sc.new_ref(); + let data_ref = sc.new_ref(); + + let glyph_remapper = &self.glyph_remapper; + + let is_glyf = self.font.font_ref().glyf().is_ok(); + let is_cff = self.font.font_ref().cff().is_ok(); + let is_cff2 = self.font.font_ref().cff2().is_ok(); + + if !is_glyf && !is_cff && !is_cff2 { + return Err(KrillaError::Font( + self.font.clone(), + "font is missing an outline table".to_string(), + )); + } + + // OS/2 fsType has a few bits that describe which kind of license the font has. Of + // particular interest is bit 2 "Restricted License embedding", which places restrictions on + // font embedding that are incompatible with some PDF standards. + // + // The OpenType spec intended for the different bits to be mutually exclusive. However, some + // fonts have multiple of the bits sets. The OpenType spec was thus adjusted to require + // mutual exclusion, allowing applications to assume the least-restrictive specified + // variant. Specifically, it also clarifies that "For Restricted License embedding to take + // effect, the Embedding permissions sub-field must have the value 2 (that is, only bit 1 is + // set)." + // + // Hence, we are not checking `fsType & 2 != 0` (as would be usual for bit flags), + // but rather `fsType & 0xF == 2`. + if self + .font + .font_ref() + .os2() + .is_ok_and(|os2| os2.fs_type() & 0xF == 2) + { + sc.register_validation_error(ValidationError::RestrictedLicense(self.font.clone())); + } + + let (subsetted, global_bbox) = subset_font(self.font.clone(), glyph_remapper)?; + let num_glyphs = subsetted.num_glyphs(); + let subsetted_data = subsetted.font_data().0; + + let font_stream = { + let mut data = subsetted_data.as_ref().as_ref(); + + // If we have a CFF font, only embed the standalone CFF program. + let subsetted_ref = skrifa::FontRef::new(data).map_err(|_| { + KrillaError::Font(self.font.clone(), "failed to read font subset".to_string()) + })?; + + if let Some(cff) = subsetted_ref.data_for_tag(Cff::TAG) { + data = cff.as_bytes(); + } + + FilterStreamBuilder::new_from_binary_data(data).finish(&sc.serialize_settings()) + }; + + let base_font = base_font_name(&self.font, &self.glyph_remapper); + let base_font_type0 = if is_cff { + format!("{base_font}-{IDENTITY_H}") + } else { + base_font.clone() + }; + + chunk + .type0_font(root_ref) + .base_font(Name(base_font_type0.as_bytes())) + .encoding_predefined(Name(IDENTITY_H.as_bytes())) + .descendant_font(cid_ref) + .to_unicode(cmap_ref); + + let mut cid = chunk.cid_font(cid_ref); + cid.subtype(if is_cff { + CidFontType::Type0 + } else { + CidFontType::Type2 + }); + cid.base_font(Name(base_font.as_bytes())); + cid.system_info(SYSTEM_INFO); + cid.font_descriptor(descriptor_ref); + cid.default_width(0.0); + + if !is_cff { + cid.cid_to_gid_map_predefined(Name(b"Identity")); + } + + // IN CID fonts, a upem value of 1000 is assumed for all fonts, so we need to convert. + let to_pdf_units = |v: f32| v / self.font.units_per_em() * self.units_per_em(); + + let mut first = 0; + let mut width_writer = cid.widths(); + for (w, group) in self.widths.group_by_key(|&w| w) { + let end = first + group.len(); + if w != 0.0 { + let last = end - 1; + width_writer.same(first as u16, last as u16, to_pdf_units(w)); + } + first = end; + } + + width_writer.finish(); + cid.finish(); + + // The only reason we write this in the first place is that PDF/A-1b requires + // a CIDSet. + if !sc.serialize_settings().pdf_version().deprecates_cid_set() { + let cid_stream_data = { + // It's always guaranteed by the subsetter that CIDs start from 0 and are + // consecutive, so this encoding is very straight-forward. + let mut bytes = vec![]; + bytes.extend([0xFFu8].repeat((num_glyphs / 8) as usize)); + let padding = num_glyphs % 8; + if padding != 0 { + bytes.push(!(0xFF >> padding)) + } + + bytes + }; + + let cid_stream = FilterStreamBuilder::new_from_binary_data(&cid_stream_data) + .finish(&sc.serialize_settings()); + let mut cid_set = stream_chunk.stream(cid_set_ref, cid_stream.encoded_data()); + cid_stream.write_filters(cid_set.deref_mut()); + cid_set.finish(); + cid_stream.finish(); + } + + let mut flags = FontFlags::empty(); + flags.set( + FontFlags::SERIF, + self.font + .postscript_name() + .is_some_and(|n| n.contains("Serif")), + ); + flags.set(FontFlags::FIXED_PITCH, self.font.is_monospaced()); + flags.set(FontFlags::ITALIC, self.font.italic_angle() != 0.0); + flags.insert(FontFlags::SYMBOLIC); + flags.insert(FontFlags::SMALL_CAP); + + let bbox = { + Rect::from_ltrb( + to_pdf_units(global_bbox.left()), + to_pdf_units(global_bbox.top()), + to_pdf_units(global_bbox.right()), + to_pdf_units(global_bbox.bottom()), + ) + .unwrap() + } + .to_pdf_rect(); + + let italic_angle = self.font.italic_angle(); + let ascender = to_pdf_units(self.font.ascent()); + let descender = to_pdf_units(self.font.descent()); + let cap_height = self.font.cap_height().map(to_pdf_units).unwrap_or(ascender); + let stem_v = 10.0 + 0.244 * (self.font.weight() - 50.0); + + let mut font_descriptor = chunk.font_descriptor(descriptor_ref); + font_descriptor + .name(Name(base_font.as_bytes())) + .flags(flags) + .bbox(bbox) + .italic_angle(italic_angle) + .ascent(ascender) + .descent(descender) + .cap_height(cap_height) + .stem_v(stem_v); + + if !sc.serialize_settings().pdf_version().deprecates_cid_set() { + font_descriptor.cid_set(cid_set_ref); + } + + if is_cff { + font_descriptor.font_file3(data_ref); + } else { + font_descriptor.font_file2(data_ref); + } + + font_descriptor.finish(); + + let cmap = { + let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); + + // For the .notdef glyph, it's fine if no mapping exists, since it is included + // even if it was not referenced in the text. + for g in 1..self.glyph_remapper.num_gids() { + let entry = self.cmap_entries.get(&g); + write_cmap_entry(&self.font, entry, sc, &mut cmap, g); + } + + cmap + }; + + let cmap_stream = cmap.finish(); + let cmap_stream = + FilterStreamBuilder::new_from_content_stream(&cmap_stream, &sc.serialize_settings()) + .finish(&sc.serialize_settings()); + let mut cmap = stream_chunk.cmap(cmap_ref, cmap_stream.encoded_data()); + cmap_stream.write_filters(cmap.deref_mut().deref_mut()); + cmap.writing_mode(WMode::Horizontal); + cmap.finish(); + + let mut stream = stream_chunk.stream(data_ref, font_stream.encoded_data()); + font_stream.write_filters(stream.deref_mut()); + if is_cff { + stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C")); + } + + stream.finish(); + chunk_container.streams.fonts.push(stream_chunk); + + Ok(()) + } +} + +/// Create a tag for a font subset. +pub(crate) fn subset_tag(data: &T) -> String { + const BASE: u128 = 26; + let mut hash = stable_hash128(data); + let mut letter = [b'A'; SUBSET_TAG_LEN]; + for l in letter.iter_mut() { + *l = b'A' + (hash % BASE) as u8; + hash /= BASE; + } + std::str::from_utf8(&letter).unwrap().to_string() +} + +pub(crate) fn base_font_name(font: &Font, data: &T) -> String { + const REST_LEN: usize = SUBSET_TAG_LEN + 1 + 1 + IDENTITY_H.len(); + + let postscript_name = font.postscript_name().unwrap_or("unknown"); + let max_len = 127 - REST_LEN; + let trimmed = &postscript_name[..postscript_name.len().min(max_len)]; + let subset_tag = subset_tag(&data); + + format!("{subset_tag}+{trimmed}") +} + +#[cfg_attr(feature = "comemo", comemo::memoize)] +fn subset_font(font: Font, glyph_remapper: &GlyphRemapper) -> KrillaResult<(Font, Rect)> { + let mut bbox: Option = None; + + let variation_coordinates = font + .variation_coordinates() + .iter() + .map(|v| (subsetter::Tag::new(v.0.get()), v.1.get())) + .collect::>(); + let font = subsetter::subset_with_variations( + font.font_data().as_ref(), + font.index(), + &variation_coordinates, + glyph_remapper, + ) + .map_err(|e| KrillaError::Font(font.clone(), format!("failed to subset font: {e}"))) + .and_then(|data| { + Font::new(Arc::new(data).into(), 0).ok_or(KrillaError::Font( + font.clone(), + "failed to subset font".to_string(), + )) + })?; + let global_bbox = font.bbox(); + + for g in 0..font.num_glyphs() { + if let Some(path_bbox) = compute_bbox(&font, skrifa::GlyphId::new(g)) { + bbox = bbox + .map(|mut r| { + r.expand(&path_bbox); + r + }) + .or(Some(path_bbox)); + } + } + + Ok((font, bbox.unwrap_or(global_bbox))) +} + +#[cfg_attr(feature = "comemo", comemo::memoize)] +fn compute_bbox(font: &Font, glyph: skrifa::GlyphId) -> Option { + let outline_glyphs = font.outline_glyphs(); + + if let Some(outline_glyph) = outline_glyphs.get(glyph) { + let mut glyph_builder = OutlineBuilder::new(); + let _ = outline_glyph.draw( + DrawSettings::unhinted(Size::unscaled(), LocationRef::default()), + &mut glyph_builder, + ); + glyph_builder.finish().map(|p| Rect::from_tsp(p.bounds())) + } else { + None + } +} diff --git a/vendor/krilla/src/text/font.rs b/vendor/krilla/src/text/font.rs new file mode 100644 index 00000000..3d3ad7b2 --- /dev/null +++ b/vendor/krilla/src/text/font.rs @@ -0,0 +1,351 @@ +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::ops::Deref; +use std::sync::Arc; + +use skrifa::instance::{Location, LocationRef, Size}; +use skrifa::metrics::GlyphMetrics; +use skrifa::raw::types::NameId; +use skrifa::raw::TableProvider; +use skrifa::{FontRef, MetadataProvider, OutlineGlyphCollection}; +use tiny_skia_path::FiniteF32; +use yoke::{Yoke, Yokeable}; + +use crate::geom::Rect; +use crate::text::GlyphId; +use crate::util::Prehashed; +use crate::Data; + +/// An OpenType font. Can be a TrueType, OpenType font or a TrueType collection. +/// It holds a reference to the underlying data as well as some basic information +/// about the font. +/// +/// Cloning and hashing this type is cheap. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Font(Arc>); + +impl Font { + /// Create a new font from some data. If you want to create a variable font at a specific + /// location, use [`Font::new_variable`] instead. + /// + /// The `index` indicates the index that should be + /// associated with this font for TrueType collections, otherwise this value should be + /// set to 0. + /// + /// Returns `None` if the index is invalid or the font couldn't be read. + pub fn new(data: Data, index: u32) -> Option { + Self::new_variable(data, index, &[]) + } + + /// Like [`Font::new`], creates a new font from some data, but allows you to specify + /// variation coordinates in case the font is variable. + pub fn new_variable(data: Data, index: u32, variation_coords: &[(Tag, f32)]) -> Option { + let font_info = FontInfo::new(data.as_ref(), index, variation_coords)?; + + Font::new_with_info(data.clone(), Arc::new(font_info)) + } + + pub(crate) fn new_with_info(data: Data, font_info: Arc) -> Option { + let yoke_data = YokeData { + data: data.0.clone(), + location: font_info.location.clone(), + }; + + let font_ref_yoke = Yoke::, Box>::attach_to_cart( + Box::new(yoke_data), + |data| { + let font_ref = + FontRef::from_index(data.data.as_ref().as_ref(), font_info.index).unwrap(); + FontRefYoke { + font_ref: font_ref.clone(), + glyph_metrics: font_ref.glyph_metrics(Size::unscaled(), &data.location), + outline_glyphs: font_ref.outline_glyphs(), + } + }, + ); + + Some(Font(Arc::new(Prehashed::new(Repr { + font_data: data, + font_ref_yoke, + font_info, + })))) + } + + pub(crate) fn postscript_name(&self) -> Option<&str> { + self.0.font_info.postscript_name.as_deref() + } + + /// Return the index of the font. + pub(crate) fn index(&self) -> u32 { + self.font_info().index + } + + pub(crate) fn font_info(&self) -> Arc { + self.0.font_info.clone() + } + + pub(crate) fn variation_coordinates(&self) -> &[(Tag, FiniteF32)] { + &self.0.font_info.var_coords + } + + pub(crate) fn cap_height(&self) -> Option { + self.0.font_info.cap_height.map(|n| n.get()) + } + + pub(crate) fn ascent(&self) -> f32 { + self.0.font_info.ascent.get() + } + + pub(crate) fn weight(&self) -> f32 { + self.0.font_info.weight.get() + } + + pub(crate) fn stretch(&self) -> f32 { + self.0.font_info.stretch.get() + } + + pub(crate) fn descent(&self) -> f32 { + self.0.font_info.descent.get() + } + + pub(crate) fn num_glyphs(&self) -> u32 { + self.0.font_info.num_glyphs + } + + pub(crate) fn is_monospaced(&self) -> bool { + self.0.font_info.is_monospaced + } + + pub(crate) fn italic_angle(&self) -> f32 { + self.0.font_info.italic_angle.get() + } + + /// The units per em of the font. + pub fn units_per_em(&self) -> f32 { + self.0.font_info.units_per_em as f32 + } + + pub(crate) fn bbox(&self) -> Rect { + self.0.font_info.global_bbox + } + + pub(crate) fn location_ref(&self) -> LocationRef<'_> { + (&self.0.font_info.location).into() + } + + pub(crate) fn font_ref(&self) -> &FontRef<'_> { + &self.0.font_ref_yoke.get().font_ref + } + + pub(crate) fn glyph_metrics(&self) -> &GlyphMetrics<'_> { + &self.0.font_ref_yoke.get().glyph_metrics + } + + pub(crate) fn outline_glyphs(&self) -> &OutlineGlyphCollection<'_> { + &self.0.font_ref_yoke.get().outline_glyphs + } + + pub(crate) fn font_data(&self) -> Data { + self.0.font_data.clone() + } + + #[inline] + pub(crate) fn advance_width(&self, glyph_id: GlyphId) -> Option { + self.glyph_metrics().advance_width(glyph_id.to_skrifa()) + } +} + +/// A 4-byte OpenType tag. +#[derive(Copy, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Tag([u8; 4]); + +impl Tag { + /// Create a new tag. + pub fn new(tag: &[u8; 4]) -> Self { + Self(*tag) + } + + /// Try to create a new tag from a string. + /// + /// Return `None` if the string is not 4 bytes in size. + pub fn try_from_str(s: &str) -> Option { + let tag: [u8; 4] = s.as_bytes().try_into().ok()?; + + Some(Self(tag)) + } + + /// Return the value of the tag. + pub fn get(&self) -> &[u8; 4] { + &self.0 + } +} + +impl Debug for Font { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Font {{..}}") + } +} + +struct YokeData { + data: Arc + Send + Sync>, + location: Location, +} + +impl Deref for YokeData { + type Target = YokeData; + + fn deref(&self) -> &Self::Target { + self + } +} + +/// `FontInfo` holds basic information about the font which is necessary +/// to distinguish a `Font` object from others. The `Hash` implementation +/// of the `Font` struct solely depends on its `FontInfo` object. The reason +/// we do this is to avoid hashing the whole font, which can be dozens of megabytes. +/// Instead, we parse the most basic information as well as additional distinguishing +/// information, such as the font name and the checksum, and has this instead. +/// This is much faster, and since we also include the checksum, the odds of two +/// different fonts ending up with the same hash is pretty much zero. +#[derive(Debug, Hash, Eq, PartialEq)] +pub(crate) struct FontInfo { + index: u32, + checksum: u32, + data_len: usize, + // `location` is derived from `var_coords`, but we need to store `var_coords` it explicitly + // so we can later pass it to the subsetter. + var_coords: Vec<(Tag, FiniteF32)>, + location: Location, + units_per_em: u16, + global_bbox: Rect, + num_glyphs: u32, + postscript_name: Option, + ascent: FiniteF32, + descent: FiniteF32, + cap_height: Option, + is_monospaced: bool, + italic_angle: FiniteF32, + weight: FiniteF32, + has_glyf: bool, + has_cff: bool, + has_cff2: bool, + stretch: FiniteF32, +} + +struct Repr { + font_info: Arc, + font_data: Data, + font_ref_yoke: Yoke, Box>, +} + +impl Hash for Repr { + fn hash(&self, state: &mut H) { + // We assume that if the font info is distinct, the font itself is distinct as well. This + // doesn't have to be the case: while the font does have a checksum, it's "only" a + // u32. The proper way would be to hash the whole font data, but this is just too expensive. + // However, the odds of the checksum AND all font metrics (including font name) being the same + // with the font being different is diminishingly low. + self.font_info.hash(state); + } +} + +impl FontInfo { + pub(crate) fn new(data: &[u8], index: u32, var_coords: &[(Tag, f32)]) -> Option { + let font_ref = FontRef::from_index(data, index).ok()?; + let location = font_ref.axes().location( + var_coords + .iter() + .map(|i| (skrifa::Tag::new(i.0.get()), i.1)), + ); + let data_len = data.len(); + let checksum = font_ref.head().ok()?.checksum_adjustment(); + let num_glyphs = font_ref.glyph_names().num_glyphs(); + + let metrics = font_ref.metrics(Size::unscaled(), &location); + let os_2 = font_ref.os2().ok(); + let ascent = FiniteF32::new( + os_2.as_ref() + .map(|s| s.s_typo_ascender() as f32) + .unwrap_or(metrics.ascent), + )?; + let descent = FiniteF32::new( + os_2.as_ref() + .map(|s| s.s_typo_descender() as f32) + .unwrap_or(metrics.descent), + )?; + let is_monospaced = metrics.is_monospace; + let cap_height = metrics.cap_height.map(|n| FiniteF32::new(n).unwrap()); + let italic_angle = FiniteF32::new(metrics.italic_angle).unwrap(); + let weight = FiniteF32::new(font_ref.attributes().weight.value())?; + let stretch = FiniteF32::new(font_ref.attributes().stretch.ratio())?; + let units_per_em = metrics.units_per_em; + + let global_bbox = metrics + .bounds + .and_then(|b| Rect::from_ltrb(b.x_min, b.y_min, b.x_max, b.y_max)) + .unwrap_or(Rect::from_xywh( + 0.0, + 0.0, + units_per_em as f32, + units_per_em as f32, + )?); + + let postscript_name = { + if let Ok(name) = font_ref.name() { + name.name_record().iter().find_map(|n| { + if n.name_id.get() == NameId::POSTSCRIPT_NAME { + if let Ok(string) = n.string(name.string_data()) { + return Some(string.to_string()); + } + } + + None + }) + } else { + None + } + }; + + let has_glyf = font_ref.glyf().is_ok(); + let has_cff = font_ref.cff().is_ok(); + let has_cff2 = font_ref.cff2().is_ok(); + + Some(FontInfo { + index, + data_len, + checksum, + var_coords: var_coords + .iter() + .map(|v| (v.0, FiniteF32::new(v.1).unwrap_or_default())) + .collect(), + location, + num_glyphs, + units_per_em, + postscript_name, + ascent, + cap_height, + has_glyf, + has_cff, + has_cff2, + descent, + is_monospaced, + weight, + stretch, + italic_angle, + global_bbox, + }) + } + + pub(crate) fn can_be_cid_font(&self) -> bool { + self.has_cff || self.has_glyf || self.has_cff2 + } +} + +/// A yoke so that we can attach a `FontRef` object to the corresponding `Font`, +/// without running into lifetime issues. +#[derive(Yokeable, Clone)] +struct FontRefYoke<'a> { + pub font_ref: FontRef<'a>, + pub glyph_metrics: GlyphMetrics<'a>, + pub outline_glyphs: OutlineGlyphCollection<'a>, +} diff --git a/vendor/krilla/src/text/glyph/bitmap.rs b/vendor/krilla/src/text/glyph/bitmap.rs new file mode 100644 index 00000000..b6933088 --- /dev/null +++ b/vendor/krilla/src/text/glyph/bitmap.rs @@ -0,0 +1,81 @@ +use skrifa::bitmap::{BitmapData, BitmapFormat, BitmapGlyph, Origin}; +use skrifa::MetadataProvider; + +use crate::geom::{Size, Transform}; +use crate::graphics::image::Image; +use crate::surface::Surface; +use crate::text::Font; +use crate::text::GlyphId; + +pub(crate) fn has_bitmap_data(font: &Font, glyph: GlyphId) -> bool { + // We only support PNG. + get_bitmap_glyph(font, glyph).is_some_and(|b| matches!(b.data, BitmapData::Png(_))) +} + +pub(crate) fn get_bitmap_glyph(font: &Font, glyph: GlyphId) -> Option> { + let bitmap_strikes = font.font_ref().bitmap_strikes(); + bitmap_strikes.glyph_for_size(skrifa::instance::Size::unscaled(), glyph.to_skrifa()) +} + +/// Draw a bitmap-based glyph on a surface. +pub(crate) fn draw_glyph(font: Font, glyph: GlyphId, surface: &mut Surface) -> Option<()> { + let metrics = font + .font_ref() + .metrics(skrifa::instance::Size::unscaled(), font.location_ref()); + + let bitmap_strikes = font.font_ref().bitmap_strikes(); + let bitmap_glyph = + bitmap_strikes.glyph_for_size(skrifa::instance::Size::unscaled(), glyph.to_skrifa())?; + + let upem = metrics.units_per_em as f32; + + match bitmap_glyph.data { + BitmapData::Png(data) => { + let image = Image::from_png(data.to_vec().into(), false).ok()?; + let size = Size::from_wh(image.size().0 as f32, image.size().1 as f32).unwrap(); + + // Adapted from vello. + let scale_factor = upem / (bitmap_glyph.ppem_y); + let mut transform = + Transform::from_translate(-bitmap_glyph.bearing_x, bitmap_glyph.bearing_y) + .pre_concat(Transform::from_scale(scale_factor, scale_factor)) + .pre_concat(Transform::from_translate( + -bitmap_glyph.inner_bearing_x, + -bitmap_glyph.inner_bearing_y, + )); + + transform = match bitmap_glyph.placement_origin { + Origin::TopLeft => transform, + Origin::BottomLeft => { + transform.pre_concat(Transform::from_translate(0.0, -(image.size().1 as f32))) + } + }; + + transform = if let Some(format) = bitmap_strikes.format() { + if format == BitmapFormat::Sbix { + // For unknown reasons, using Apple Color Emoji will lead to a vertical shift on MacOS, but this shift + // doesn't seem to be coming from the font and most likely is somehow hardcoded. On Windows, + // this shift will not be applied. However, if this shift is not applied the emojis are a bit + // too high up when being together with other text, so we try to imitate this. + // See also https://github.com/harfbuzz/harfbuzz/issues/2679#issuecomment-1345595425 + // We approximate this vertical shift that seems to be produced by it. + // This value seems to be pretty close to what is happening on MacOS. + transform + .pre_concat(Transform::from_translate(0.0, 0.128 * upem / scale_factor)) + } else { + transform + } + } else { + transform + }; + + surface.push_transform(&transform); + surface.draw_image(image, size); + surface.pop(); + + Some(()) + } + BitmapData::Bgra(_) => None, + BitmapData::Mask(_) => None, + } +} diff --git a/vendor/krilla/src/text/glyph/colr.rs b/vendor/krilla/src/text/glyph/colr.rs new file mode 100644 index 00000000..35d5071d --- /dev/null +++ b/vendor/krilla/src/text/glyph/colr.rs @@ -0,0 +1,581 @@ +use skrifa::color::{Brush, ColorPainter, ColorStop, CompositeMode}; +use skrifa::outline::DrawSettings; +use skrifa::raw::types::BoundingBox; +use skrifa::raw::TableProvider; +use skrifa::MetadataProvider; +use tiny_skia_path::{Path, PathBuilder}; + +use crate::geom::Transform; +use crate::graphics::blend::BlendMode; +use crate::graphics::color::rgb; +use crate::graphics::mask::{Mask, MaskType}; +use crate::graphics::paint::{ + Fill, FillRule, LinearGradient, RadialGradient, SpreadMethod, Stop, SweepGradient, +}; +use crate::num::NormalizedF32; +use crate::surface::Surface; +use crate::text::outline::OutlineBuilder; +use crate::text::Font; +use crate::text::GlyphId; + +pub(crate) fn has_colr_data(font: &Font, glyph: GlyphId) -> bool { + font.font_ref() + .color_glyphs() + .get(glyph.to_skrifa()) + .is_some() +} + +/// Draw a COLR-based glyph on a surface. +pub(crate) fn draw_glyph( + font: Font, + context_color: rgb::Color, + glyph: GlyphId, + surface: &mut Surface, +) -> Option<()> { + // Drawing COLR glyphs is a bit tricky, because it's possible that an error + // occurs while we are drawing, in which case we cannot revert it anymore since + // we already drew the instructions onto the surface. Because of this, we first + // convert the glyph into a more accessible bytecode representation and only + // if that succeeds do we iterate over the bytecode to draw onto the canvas. + + let colr_glyphs = font.font_ref().color_glyphs(); + let colr_glyph = colr_glyphs.get(glyph.to_skrifa())?; + + let mut colr_canvas = ColrBuilder::new(font.clone(), context_color); + colr_glyph + .paint(font.location_ref(), &mut colr_canvas) + .ok()?; + let instructions = colr_canvas.finish()?; + + surface.push_transform(&Transform::from_scale(1.0, -1.0)); + interpret(instructions, surface); + surface.pop(); + + Some(()) +} + +// Interpret the glyph bytecode +fn interpret(instructions: Vec, surface: &mut Surface) { + for instruction in instructions { + match instruction { + Instruction::Layer(mode, instructions) => { + let blend = mode.to_blend_mode().unwrap_or(BlendMode::Normal); + surface.push_blend_mode(blend); + surface.push_isolated(); + interpret(instructions, surface); + surface.pop(); + surface.pop(); + } + Instruction::Composite { + source, + mode, + backdrop, + } => interpret_composite(source, mode, backdrop, surface), + Instruction::Filled(fill, mut clips) => { + let filled = clips.split_off(clips.len() - 1); + + let num_clips = clips.len(); + for path in clips { + surface.push_clip_path(&crate::geom::Path(path), &FillRule::NonZero); + } + + let old_fill = surface.get_fill().cloned(); + let old_stroke = surface.get_stroke().cloned(); + + surface.set_fill(Some(*fill)); + surface.set_stroke(None); + + surface.draw_path(&crate::geom::Path(filled[0].clone())); + + surface.set_fill(old_fill); + surface.set_stroke(old_stroke); + + for _ in 0..num_clips { + surface.pop(); + } + } + } + } +} + +fn interpret_composite( + source: Vec, + mode: CompositeMode, + backdrop: Vec, + surface: &mut Surface, +) { + match mode { + CompositeMode::Clear => {} + CompositeMode::Src => { + surface.push_isolated(); + interpret(source, surface); + surface.pop(); + } + CompositeMode::Dest => { + surface.push_isolated(); + interpret(backdrop, surface); + surface.pop(); + } + CompositeMode::SrcOver => { + surface.push_isolated(); + interpret(backdrop, surface); + interpret(source, surface); + surface.pop(); + } + CompositeMode::DestOver => { + surface.push_isolated(); + interpret(source, surface); + interpret(backdrop, surface); + surface.pop(); + } + CompositeMode::SrcIn => interpret_masked(source, backdrop, surface), + CompositeMode::DestIn => interpret_masked(backdrop, source, surface), + _ => { + let Some(blend) = mode.to_blend_mode() else { + surface.push_isolated(); + interpret(backdrop, surface); + interpret(source, surface); + surface.pop(); + return; + }; + + surface.push_isolated(); + interpret(backdrop, surface); + surface.push_blend_mode(blend); + surface.push_isolated(); + interpret(source, surface); + surface.pop(); + surface.pop(); + surface.pop(); + } + } +} + +fn interpret_masked(visible: Vec, mask: Vec, surface: &mut Surface) { + let mask_stream = { + let mut builder = surface.stream_builder(); + let mut mask_surface = builder.surface(); + interpret(mask, &mut mask_surface); + mask_surface.finish(); + builder.finish() + }; + + surface.push_mask(Mask::new(mask_stream, MaskType::Alpha)); + interpret(visible, surface); + surface.pop(); +} + +/// The context necessary for creating the bytecode of a COLR-based glyph. +struct ColrBuilder { + font: Font, + context_color: rgb::Color, + clips: Vec>, + stack: Vec>, + layers: Vec, + transforms: Vec, + error: bool, +} + +/// A bytecode instruction for drawing a COLR glyph. +enum Instruction { + Layer(CompositeMode, Vec), + Composite { + source: Vec, + mode: CompositeMode, + backdrop: Vec, + }, + Filled(Box, Vec), +} + +trait CompositeModeExt { + fn to_blend_mode(self) -> Option; +} + +impl CompositeModeExt for CompositeMode { + fn to_blend_mode(self) -> Option { + match self { + CompositeMode::SrcOver => Some(BlendMode::Normal), + CompositeMode::Screen => Some(BlendMode::Screen), + CompositeMode::Overlay => Some(BlendMode::Overlay), + CompositeMode::Darken => Some(BlendMode::Darken), + CompositeMode::Lighten => Some(BlendMode::Lighten), + CompositeMode::ColorDodge => Some(BlendMode::ColorDodge), + CompositeMode::ColorBurn => Some(BlendMode::ColorBurn), + CompositeMode::HardLight => Some(BlendMode::HardLight), + CompositeMode::SoftLight => Some(BlendMode::SoftLight), + CompositeMode::Difference => Some(BlendMode::Difference), + CompositeMode::Exclusion => Some(BlendMode::Exclusion), + CompositeMode::Multiply => Some(BlendMode::Multiply), + CompositeMode::HslHue => Some(BlendMode::Hue), + CompositeMode::HslColor => Some(BlendMode::Color), + CompositeMode::HslLuminosity => Some(BlendMode::Luminosity), + CompositeMode::HslSaturation => Some(BlendMode::Saturation), + _ => None, + } + } +} + +impl ColrBuilder { + pub fn new(font: Font, context_color: rgb::Color) -> Self { + Self { + font, + context_color, + stack: vec![vec![]], + transforms: vec![Transform::identity()], + clips: vec![vec![]], + layers: vec![], + error: false, + } + } + + pub fn finish(mut self) -> Option> { + if self.error { + return None; + } else if let Some(instructions) = self.stack.pop() { + return Some(instructions); + } + + None + } +} + +impl ColrBuilder { + fn palette_index_to_color( + &self, + palette_index: u16, + alpha: f32, + ) -> Option<(rgb::Color, NormalizedF32)> { + if palette_index != u16::MAX { + let color = self + .font + .font_ref() + .cpal() + .ok()? + .color_records_array()? + .ok()?[palette_index as usize]; + + Some(( + rgb::Color::new(color.red, color.green, color.blue), + NormalizedF32::new(alpha * color.alpha as f32 / 255.0)?, + )) + } else { + Some((self.context_color, NormalizedF32::new(alpha)?)) + } + } + + fn stops(&self, stops: &[ColorStop]) -> Option> { + let mut converted_stops = vec![]; + + for stop in stops { + let (color, alpha) = self.palette_index_to_color(stop.palette_index, stop.alpha)?; + + converted_stops.push(Stop { + offset: NormalizedF32::new(stop.offset)?, + color: color.into(), + opacity: alpha, + }) + } + + Some(converted_stops) + } +} + +trait ExtendExt { + fn to_spread_method(&self) -> SpreadMethod; +} + +impl ExtendExt for skrifa::color::Extend { + fn to_spread_method(&self) -> SpreadMethod { + match self { + skrifa::color::Extend::Pad => SpreadMethod::Pad, + skrifa::color::Extend::Repeat => SpreadMethod::Repeat, + skrifa::color::Extend::Reflect => SpreadMethod::Reflect, + skrifa::color::Extend::Unknown => SpreadMethod::Pad, + } + } +} + +impl ColorPainter for ColrBuilder { + fn push_transform(&mut self, transform: skrifa::color::Transform) { + let Some(last_transform) = self.transforms.last() else { + self.error = true; + return; + }; + + let new_transform = last_transform.pre_concat(Transform::from_row( + transform.xx, + transform.yx, + transform.xy, + transform.yy, + transform.dx, + transform.dy, + )); + self.transforms.push(new_transform); + } + + fn pop_transform(&mut self) { + let Some(_) = self.transforms.pop() else { + self.error = true; + return; + }; + } + + fn push_clip_glyph(&mut self, glyph_id: skrifa::GlyphId) { + let Some(mut old) = self.clips.last().cloned() else { + self.error = true; + return; + }; + + let mut glyph_builder = OutlineBuilder::new(); + let outline_glyphs = self.font.outline_glyphs(); + let Some(outline_glyph) = outline_glyphs.get(glyph_id) else { + self.error = true; + return; + }; + + let Ok(_) = outline_glyph.draw( + DrawSettings::unhinted(skrifa::instance::Size::unscaled(), self.font.location_ref()), + &mut glyph_builder, + ) else { + self.error = true; + return; + }; + + let Some(path) = glyph_builder + .finish() + .and_then(|p| p.transform(self.transforms.last()?.to_tsp())) + else { + self.error = true; + return; + }; + + old.push(path); + + self.clips.push(old); + } + + fn push_clip_box(&mut self, clip_box: BoundingBox) { + let Some(mut old) = self.clips.last().cloned() else { + self.error = true; + return; + }; + + let mut path_builder = PathBuilder::new(); + path_builder.move_to(clip_box.x_min, clip_box.y_min); + path_builder.line_to(clip_box.x_min, clip_box.y_max); + path_builder.line_to(clip_box.x_max, clip_box.y_max); + path_builder.line_to(clip_box.x_max, clip_box.y_min); + path_builder.close(); + + let Some(path) = path_builder + .finish() + .and_then(|p| p.transform(self.transforms.last()?.to_tsp())) + else { + self.error = true; + return; + }; + old.push(path); + + self.clips.push(old); + } + + fn pop_clip(&mut self) { + let Some(_) = self.clips.pop() else { + self.error = true; + return; + }; + } + + fn fill(&mut self, brush: Brush<'_>) { + if let Some(fill) = match brush { + Brush::Solid { + palette_index, + alpha, + } => { + let (color, alpha) = match self.palette_index_to_color(palette_index, alpha) { + Some(c) => c, + None => { + self.error = true; + return; + } + }; + Some(Fill { + paint: color.into(), + opacity: alpha, + rule: Default::default(), + }) + } + Brush::LinearGradient { + p0, + p1, + color_stops, + extend, + } => { + let stops = match self.stops(color_stops) { + Some(s) => s, + None => { + self.error = true; + return; + } + }; + + let Some(transform) = self.transforms.last().copied() else { + self.error = true; + return; + }; + + let linear = LinearGradient { + x1: p0.x, + y1: p0.y, + x2: p1.x, + y2: p1.y, + stops, + spread_method: extend.to_spread_method(), + transform, + anti_alias: false, + }; + + Some(Fill { + paint: linear.into(), + opacity: NormalizedF32::ONE, + rule: Default::default(), + }) + } + Brush::RadialGradient { + c0, + r0, + c1, + r1, + color_stops, + extend, + } => { + let stops = match self.stops(color_stops) { + Some(s) => s, + None => { + self.error = true; + return; + } + }; + + let Some(transform) = self.transforms.last().copied() else { + self.error = true; + return; + }; + + let radial = RadialGradient { + fx: c0.x, + fy: c0.y, + fr: r0, + cx: c1.x, + cy: c1.y, + cr: r1, + stops, + spread_method: extend.to_spread_method(), + transform, + anti_alias: false, + }; + + Some(Fill { + paint: radial.into(), + opacity: NormalizedF32::ONE, + rule: Default::default(), + }) + } + Brush::SweepGradient { + c0, + start_angle, + end_angle, + color_stops, + extend, + } => { + let stops = match self.stops(color_stops) { + Some(s) => s, + None => { + self.error = true; + return; + } + }; + + let Some(mut transform) = self.transforms.last().copied() else { + self.error = true; + return; + }; + + // krilla sweep gradients go in a different direction than COLR, so we need + // to invert y-axis. + + transform = transform.pre_concat(Transform::from_scale(1.0, -1.0)); + + let sweep = SweepGradient { + cx: c0.x, + cy: -c0.y, + start_angle, + end_angle, + stops, + spread_method: extend.to_spread_method(), + transform, + anti_alias: false, + }; + + Some(Fill { + paint: sweep.into(), + opacity: NormalizedF32::ONE, + rule: Default::default(), + }) + } + } { + // The proper implementation would be to apply all clip paths and then draw the + // whole "visible" area with the fill. However, this seems to produce artifacts in + // Google Chrome when zooming. So instead, what we do is that we apply all clip paths except + // for the last one, and the last one we use to actually perform the fill. + let Some(clips) = self.clips.last().map(|paths| paths.to_vec()) else { + self.error = true; + return; + }; + + let Some(stack) = self.stack.last_mut() else { + self.error = true; + return; + }; + + stack.push(Instruction::Filled(Box::new(fill), clips)); + } + } + + fn push_layer(&mut self, composite_mode: CompositeMode) { + self.layers.push(composite_mode); + self.stack.push(vec![]); + } + + fn pop_layer(&mut self) { + let (Some(composite), Some(mut instructions)) = (self.layers.pop(), self.stack.pop()) + else { + self.error = true; + return; + }; + + let Some(stack) = self.stack.last_mut() else { + self.error = true; + return; + }; + + // See the skrifa code to see how it handles composite operations. + // https://github.com/googlefonts/fontations/blob/fd0178ce4ea48301a7f06b35a6fce9879c102292/skrifa/src/color/traversal.rs#L566-L593 + // Everything is wrapped in a `SrcOver` base layer. + if composite == CompositeMode::SrcOver { + if let Some(Instruction::Layer(_, _)) = instructions.last() { + let Some(Instruction::Layer(mode, source)) = instructions.pop() else { + unreachable!(); + }; + stack.push(Instruction::Composite { + source, + mode, + backdrop: instructions, + }); + return; + } + } + + stack.push(Instruction::Layer(composite, instructions)); + } +} diff --git a/vendor/krilla/src/text/glyph/mod.rs b/vendor/krilla/src/text/glyph/mod.rs new file mode 100644 index 00000000..83d3f552 --- /dev/null +++ b/vendor/krilla/src/text/glyph/mod.rs @@ -0,0 +1,205 @@ +use std::ops::Range; + +use crate::color::rgb; +use crate::geom::Transform; +use crate::surface::Surface; +use crate::text::Font; + +#[cfg(feature = "raster-images")] +pub(crate) mod bitmap; +pub(crate) mod colr; +pub(crate) mod outline; +pub(crate) mod svg; + +/// A glyph ID. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct GlyphId(u32); + +impl GlyphId { + /// Create a new glyph ID. + pub fn new(id: u32) -> Self { + Self(id) + } + + /// Get the glyph ID as a u32. + pub fn to_u32(&self) -> u32 { + self.0 + } + + pub(crate) fn to_skrifa(self) -> skrifa::GlyphId { + skrifa::GlyphId::new(self.0) + } +} + +/// Draw a color glyph to a surface. +pub(crate) fn draw_color_glyph( + font: Font, + context_color: rgb::Color, + glyph: GlyphId, + base_transform: Transform, + surface: &mut Surface, +) -> Option<()> { + surface.push_transform(&base_transform); + surface.push_transform(&Transform::from_scale(1.0, -1.0)); + + // Note: See the comment in krilla-svg in `render_svg_glyph` for why we prefer + // COLR over SVG. For Noto Color Emoji specifically, using SVG will be slow when + // rendering multiple glyphs. + let drawn = colr::draw_glyph(font.clone(), context_color, glyph, surface) + .or_else(|| svg::draw_glyph(font.clone(), context_color, glyph, surface)) + .or_else(|| { + #[cfg(feature = "raster-images")] + let res = bitmap::draw_glyph(font.clone(), glyph, surface); + + #[cfg(not(feature = "raster-images"))] + let res = None; + + res + }); + + surface.pop(); + surface.pop(); + + drawn +} + +pub(crate) fn should_outline(font: &Font, glyph: GlyphId) -> bool { + let has_svg = svg::has_svg_data(font, glyph); + let has_colr = colr::has_colr_data(font, glyph); + #[cfg(feature = "raster-images")] + let has_bitmap = bitmap::has_bitmap_data(font, glyph); + #[cfg(not(feature = "raster-images"))] + let has_bitmap = false; + font.font_info().can_be_cid_font() && !has_svg && !has_colr && !has_bitmap +} + +/// Draw a color glyph or outline glyph to a surface. +pub(crate) fn draw_glyph( + font: Font, + context_color: rgb::Color, + glyph: GlyphId, + base_transform: Transform, + surface: &mut Surface, +) -> Option<()> { + draw_color_glyph(font.clone(), context_color, glyph, base_transform, surface) + .or_else(|| outline::draw_glyph(font, glyph, base_transform, surface)) +} + +/// A glyph with certain properties. +pub trait Glyph { + /// The glyph ID of the glyph. + fn glyph_id(&self) -> GlyphId; + /// The range of bytes in the original text covered by the cluster that the glyph + /// belongs to. + fn text_range(&self) -> Range; + /// The advance in the x direction of the glyph, at the given font size. + fn x_advance(&self, size: f32) -> f32; + /// The offset in the x direction of the glyph, at the given font size. + fn x_offset(&self, size: f32) -> f32; + /// The offset in the y direction of the glyph, at the given font size. + fn y_offset(&self, size: f32) -> f32; + /// The advance in the y direction of the glyph, at the given font size. + fn y_advance(&self, size: f32) -> f32; + /// A location identifying the glyph. If set, `krilla` will automatically call + /// `set_location` before processing the glyph. + fn location(&self) -> Option; +} + +/// A glyph type that implements `Glyph`. +/// +/// You can use it if you don't have your own type of glyph that you want to use. +#[derive(Debug, Clone)] +pub struct KrillaGlyph { + /// The glyph ID of the glyph. + pub glyph_id: GlyphId, + /// The range in the original text that corresponds to the + /// cluster of the glyph. + pub text_range: Range, + /// The advance of the glyph. + pub x_advance: f32, + /// The x offset of the glyph. + pub x_offset: f32, + /// The y offset of the glyph. + pub y_offset: f32, + /// The y advance of the glyph. + pub y_advance: f32, + /// The location of the glyph. + pub location: Option, +} + +impl Glyph for KrillaGlyph { + fn glyph_id(&self) -> GlyphId { + self.glyph_id + } + + fn text_range(&self) -> Range { + self.text_range.clone() + } + + fn x_advance(&self, size: f32) -> f32 { + self.x_advance * size + } + + fn x_offset(&self, size: f32) -> f32 { + self.x_offset * size + } + + fn y_offset(&self, size: f32) -> f32 { + self.y_offset * size + } + + fn y_advance(&self, size: f32) -> f32 { + self.y_advance * size + } + + fn location(&self) -> Option { + self.location + } +} + +impl KrillaGlyph { + /// Create a new krilla glyph. + /// + /// Important: `x_advance`, `x_offset`, `y_offset` and `y_advance` + /// need to be normalized, i.e. divided by the units per em! + pub fn new( + glyph_id: GlyphId, + x_advance: f32, + x_offset: f32, + y_offset: f32, + y_advance: f32, + range: Range, + location: Option, + ) -> Self { + Self { + glyph_id, + x_advance, + x_offset, + y_offset, + y_advance, + text_range: range, + location, + } + } +} + +/// A glyph that belongs either to a CID font or a Type 3 font. +#[derive(Clone, Copy)] +pub(crate) enum PDFGlyph { + Type3(u8), + Cid(u16), +} + +impl PDFGlyph { + /// Encode the glyph into a content stream. + #[inline(always)] + pub(crate) fn encode_into(&self, slice: &mut Vec) { + match self { + PDFGlyph::Type3(cg) => slice.push(*cg), + PDFGlyph::Cid(cid) => { + slice.push((cid >> 8) as u8); + slice.push((cid & 0xff) as u8); + } + } + } +} diff --git a/vendor/krilla/src/text/glyph/outline.rs b/vendor/krilla/src/text/glyph/outline.rs new file mode 100644 index 00000000..5a76e68a --- /dev/null +++ b/vendor/krilla/src/text/glyph/outline.rs @@ -0,0 +1,72 @@ +use skrifa::outline::{DrawSettings, OutlinePen}; + +use crate::geom::Path; +use crate::geom::Transform; +use crate::surface::Surface; +use crate::text::Font; +use crate::text::GlyphId; + +pub(crate) fn glyph_path(font: Font, glyph: GlyphId) -> Option { + let outline_glyphs = font.outline_glyphs(); + let mut outline_builder = OutlineBuilder::new(); + + if let Some(outline_glyph) = outline_glyphs.get(glyph.to_skrifa()) { + outline_glyph + .draw( + DrawSettings::unhinted(skrifa::instance::Size::unscaled(), font.location_ref()), + &mut outline_builder, + ) + .ok()?; + } + + outline_builder.finish() +} + +/// Draw an outline-based glyph on a surface. +pub(crate) fn draw_glyph( + font: Font, + glyph: GlyphId, + base_transform: Transform, + surface: &mut Surface, +) -> Option<()> { + let path = Path(glyph_path(font, glyph).and_then(|p| p.transform(base_transform.to_tsp()))?); + + surface.draw_path(&path); + + Some(()) +} + +/// A wrapper struct for implementing the `OutlinePen` trait. +pub(crate) struct OutlineBuilder(tiny_skia_path::PathBuilder); + +impl OutlineBuilder { + pub(crate) fn new() -> Self { + Self(tiny_skia_path::PathBuilder::new()) + } + + pub(crate) fn finish(self) -> Option { + self.0.finish() + } +} + +impl OutlinePen for OutlineBuilder { + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to(x, y); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.0.line_to(x, y); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.0.quad_to(cx0, cy0, x, y); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.0.cubic_to(cx0, cy0, cx1, cy1, x, y); + } + + fn close(&mut self) { + self.0.close() + } +} diff --git a/vendor/krilla/src/text/glyph/svg.rs b/vendor/krilla/src/text/glyph/svg.rs new file mode 100644 index 00000000..e902d76f --- /dev/null +++ b/vendor/krilla/src/text/glyph/svg.rs @@ -0,0 +1,34 @@ +use skrifa::raw::TableProvider; + +use crate::color::rgb; +use crate::surface::Surface; +use crate::text::Font; +use crate::text::GlyphId; + +pub(crate) fn has_svg_data(font: &Font, glyph: GlyphId) -> bool { + font.font_ref() + .svg() + .map(|svg_table| svg_table.glyph_data(glyph.to_skrifa()).is_ok()) + .is_ok() +} + +/// Draw an SVG-based glyph on a surface. +pub(crate) fn draw_glyph( + font: Font, + context_color: rgb::Color, + glyph: GlyphId, + surface: &mut Surface, +) -> Option<()> { + let svg_data = font + .font_ref() + .svg() + .and_then(|svg_table| svg_table.glyph_data(glyph.to_skrifa())) + .ok()??; + + let upem = font.units_per_em(); + + let fn_ = surface.sc.serialize_settings().render_svg_glyph_fn; + fn_(svg_data, context_color, glyph, (upem, upem), surface)?; + + Some(()) +} diff --git a/vendor/krilla/src/text/group.rs b/vendor/krilla/src/text/group.rs new file mode 100644 index 00000000..a61490ef --- /dev/null +++ b/vendor/krilla/src/text/group.rs @@ -0,0 +1,463 @@ +use std::cell::{RefCell, RefMut}; +use std::ops::Range; +use std::rc::Rc; + +use crate::color::rgb; +use crate::text::type3::ColoredGlyph; +use crate::text::Glyph; +use crate::text::{FontContainer, FontIdentifier}; + +pub(crate) enum GlyphSpan<'a, T> +where + T: Glyph, +{ + Unspanned(&'a [T]), + Spanned(&'a [T], &'a str), +} + +impl GlyphSpan<'_, T> +where + T: Glyph, +{ + pub(crate) fn glyphs(&self) -> &[T] { + match self { + GlyphSpan::Unspanned(glyphs) => glyphs, + GlyphSpan::Spanned(glyphs, _) => glyphs, + } + } + + pub(crate) fn actual_text(&self) -> Option<&str> { + match self { + GlyphSpan::Unspanned(_) => None, + GlyphSpan::Spanned(_, text) => Some(text), + } + } +} + +/// In PDF, correspondences between glyphs and Unicode codepoints are expressed +/// via a CMAP. In a CMAP, you can assign a sequence of Unicode codepoints to each +/// glyph. There are two issues with this approach: +/// - How to deal with the fact that the same glyph might be assigned two different codepoints +/// in different contexts (i.e. space and NZWJ). +/// - How to deal with complex shaping scenarios, where there is not a one-to-one or +/// one-to-many correspondence between glyphs and codepoints, but instead a many-to-one +/// or many-to-many mapping. +/// +/// The answer to this is the `ActualText` feature of PDF, which allows to define some custom +/// actual text for a number of glyphs, which overrides anything else. Unfortunately, this +/// is seemingly only supported in Acrobat and Chrome, but it's the only proper way of addressing +/// this issue. +/// +/// This is the task of the `TextSpanner`. Given a sequence of glyphs, it segments the +/// sequence into subruns of glyphs that either do need to be wrapped in an actual text +/// attribute, or not. +pub(crate) struct GlyphSpanner<'a, T> +where + T: Glyph, +{ + slice: &'a [T], + context_color: rgb::Color, + reversed: bool, + forbid_invalid_codepoints: bool, + font_container: Rc>, + text: &'a str, +} + +impl<'a, T> GlyphSpanner<'a, T> +where + T: Glyph, +{ + pub(crate) fn new( + slice: &'a [T], + text: &'a str, + reversed: bool, + forbid_invalid_codepoints: bool, + context_color: rgb::Color, + font_container: Rc>, + ) -> Self { + Self { + slice, + context_color, + reversed, + forbid_invalid_codepoints, + text, + font_container, + } + } +} + +impl<'a, T> Iterator for GlyphSpanner<'a, T> +where + T: Glyph, +{ + type Item = GlyphSpan<'a, T>; + + #[inline] + fn next(&mut self) -> Option { + fn func( + g: &U, + context_color: rgb::Color, + previous_range: Option>, + forbid_invalid_codepoints: bool, + mut font_container: RefMut, + text: &str, + ) -> (Range, bool) + where + U: Glyph, + { + let (identifier, pdf_glyph) = + font_container.add_glyph(ColoredGlyph::new(g.glyph_id(), context_color)); + let pdf_font = font_container + .get_from_identifier_mut(identifier.clone()) + .unwrap(); + + let range = g.text_range().clone(); + let text = &text[range.clone()]; + let codepoints = pdf_font.get_codepoints(pdf_glyph); + // Check if the glyph has already been assigned codepoints that don't match the + // one we are seeing right now. + let incompatible_codepoint = codepoints.is_some_and(|c| c != text); + + // Only set the codepoint if there isn't a previous, different mapping. + // + // If we could set it, we only want to insert a codepoint if we are not already + // building a spanned run (which is the case if the previous range is the same). + // If we are building a spanned run, it means that the glyphs are part of the same + // cluster, in which case only the first glyph should be assigned the codepoint, + // while all other glyphs in the same cluster should not be assigned anything. + // Otherwise, when copying text from the PDF, we will get the same codepoint multiple + // times in viewers that don't support `ActualText`. + // + // However, in case we are for example exporting to PDF/UA, every glyph is required + // to have a valid codepoint mapping. So in this case, we still add the codepoints + // to each glyph in the cluster, this will result in worse copy-pasting in viewers + // that don't support `ActualText`. + if !incompatible_codepoint + && (previous_range != Some(range.clone()) || forbid_invalid_codepoints) + && codepoints != Some(text) + { + pdf_font.set_codepoints(pdf_glyph, text.to_string(), g.location()); + } + + (range, incompatible_codepoint) + } + + let mut use_span = None; + let mut count = 1; + + let mut iter = self.slice.iter(); + + // Get the range of the first glyph, as well as whether it's + // incompatible. + let (first_range, first_incompatible) = func( + iter.next()?, + self.context_color, + None, + self.forbid_invalid_codepoints, + self.font_container.borrow_mut(), + self.text, + ); + + let mut prev_range = first_range.clone(); + + for next in iter { + let (next_range, next_incompatible) = func( + next, + self.context_color, + Some(prev_range.clone()), + self.forbid_invalid_codepoints, + self.font_container.borrow_mut(), + self.text, + ); + + match use_span { + // In this case, we just started and we are looking at the first two glyphs. + // This decides whether the current run will be spanned, or not. + None => { + if prev_range == next_range { + // The two glyphs are in the same range, so we definitely want this run + // to be spanned, and also want to include both glyphs in that run. + use_span = Some(true); + } else { + // Else, whether we use a span depends on whether the first glyph + // is incompatible. + use_span = Some(first_incompatible); + + // If either the first glyph or the second glyph are incompatible, they + // need to be in separate runs, since they are not part of the same cluster. + if first_incompatible || next_incompatible { + break; + } + + // If none are incompatible, then `use_span` is false, and we can also + // include the next glyph in that unspanned run. + } + } + // We are currently building a spanned range, and all glyphs + // are part of the same cluster. + Some(true) => { + // If the next glyph is not part of the same cluster, terminate the current + // span and don't include the next one. + if prev_range != next_range { + break; + } + } + // We are currently building an unspanned range, meaning the + // glyphs are not part of the same cluster. + Some(false) => { + // If the previous and next glyph are part of the same range this means + // that they are part of the same cluster. This means that the previous + // AND the next glyph should be part of the upcoming spanned range, not + // the current one. To exclude the next glyph, we need to do + // `count -= 1` before terminating. + if prev_range == next_range { + count -= 1; + break; + } + + // If the next one is incompatible, terminate the + // current run, since the next one needs to be spanned. + if next_incompatible { + break; + } + } + } + + prev_range = next_range; + count += 1; + + // PDF 1.7 § 14.8.2.3.3 says that `/ReversedChars` show strings shall not contain + // interior space glyphs. Thus, we check whether we have a glyph that is not the first + // one and which has a space as its text mapping, and if so, stop after it. That a space + // is in the same cluster as another codepoint should be exceedingly rare, so we do a + // literal comparison against `" "` instead of a containment check and prefer to + // continue otherwise. + if self.reversed && count > 1 && &self.text[prev_range.clone()] == " " { + break; + } + } + + // If we only had one glyph to begin with (and never entered the for loop), then + // it should be spanned if its codepoint is incompatible. + if count == 1 { + use_span = Some(first_incompatible); + } + + let (head, tail) = self.slice.split_at(count); + self.slice = tail; + + let fragment = match use_span.unwrap_or(false) { + true => GlyphSpan::Spanned(head, &self.text[first_range]), + false => GlyphSpan::Unspanned(head), + }; + + Some(fragment) + } +} + +pub(crate) struct GlyphGroup<'a, T> +where + T: Glyph, +{ + pub(crate) font_identifier: FontIdentifier, + pub(crate) glyphs: &'a [T], + // This will be stored in normalized form (i.e. at a font size of 1). + pub(crate) y_offset: f32, + // This will be stored in normalized form (i.e. at a font size of 1). + pub(crate) y_advance: f32, +} + +impl<'a, T> GlyphGroup<'a, T> +where + T: Glyph, +{ + pub fn from_props(glyphs: &'a [T], props: GlyphProps) -> Self { + GlyphGroup { + font_identifier: props.font_identifier, + glyphs, + y_offset: props.y_offset, + y_advance: props.y_advance, + } + } +} + +// The GlyphGrouper further segments glyph runs (that already have been segmented +// by `TextSpanner` into subruns that can be encoded as one consecutive run in PDF. +// This is necessary because: +// - The user provides a font for the whole glyph run, but in PDF, the font might +// have to be switched if the glyph maps to a different Type3 font. +// - The glyph contains a y_offset/y_advance, which cannot be expressed as an adjustment +// and requires us to start a new run with a transformation matrix that takes this +// adjustment into account. +pub(crate) struct GlyphGrouper<'a, T> +where + T: Glyph, +{ + font_container: Rc>, + context_color: rgb::Color, + slice: &'a [T], +} + +impl<'a, T> GlyphGrouper<'a, T> +where + T: Glyph, +{ + pub fn new( + font_container: Rc>, + context_color: rgb::Color, + slice: &'a [T], + ) -> Self { + Self { + font_container, + context_color, + slice, + } + } +} + +impl<'a, T> Iterator for GlyphGrouper<'a, T> +where + T: Glyph, +{ + type Item = GlyphGroup<'a, T>; + + #[inline] + fn next(&mut self) -> Option { + // Guarantees: All glyphs in `head` have the font identifier that is given in + // `props`, the same size and the same y offset. + let (head, tail, props) = { + let mut count = 1; + + let mut iter = self.slice.iter(); + let first = get_glyph_props( + iter.next()?, + self.context_color, + &mut self.font_container.borrow_mut(), + ); + + for next in iter { + let temp_glyph = get_glyph_props( + next, + self.context_color, + &mut self.font_container.borrow_mut(), + ); + + // If either of those is different, we need to start a new subrun. + if first.font_identifier != temp_glyph.font_identifier + || first.y_offset != temp_glyph.y_offset + || first.y_advance != 0.0 + || temp_glyph.y_advance != 0.0 + { + break; + } + + count += 1; + } + + let (head, tail) = self.slice.split_at(count); + (head, tail, first) + }; + + self.slice = tail; + + let glyph_group = GlyphGroup::from_props(head, props); + + Some(glyph_group) + } +} + +pub(crate) struct GlyphProps { + font_identifier: FontIdentifier, + y_offset: f32, + y_advance: f32, +} + +pub(crate) fn get_glyph_props( + g: &U, + context_color: rgb::Color, + font_container: &mut FontContainer, +) -> GlyphProps +where + U: Glyph, +{ + // Safe because we've already added all glyphs in the text spanner. + let font_identifier = font_container + .font_identifier(ColoredGlyph::new(g.glyph_id(), context_color)) + .unwrap(); + + GlyphProps { + font_identifier, + y_offset: g.y_offset(1.0), + y_advance: g.y_advance(1.0), + } +} + +pub fn use_text_spanner( + glyphs: &[impl Glyph], + text: &str, + context_color: rgb::Color, + font_container: &mut FontContainer, +) -> bool { + if glyphs.is_empty() { + return false; + } + + let mut do_text_span = false; + // We _might_ need to group glyphs either if they are mapped to different PDF fonts (which + // can only happen for Type3 fonts, not CID fonts), or if a glyph has a different y/x offset. + // Note that it's of course possible that we always use the same Type3 font, but we need to + // be conservative here. + + let mut check_single = |glyph, do_text_span: &mut bool| { + // As soon as we know that the glyph run requires a text span, we do not insert any codepoints + // anymore, because otherwise we might unnecessarily pollute the cmap with entries that + // wouldn't be necessary. The text spanner will then iterate over the all glyphs again + // and thus take care of inserting cmap entries for any remaining glyphs we ignore now. + // The only reason we keep going and don't early abort is in order to fully + // check the `do_glyph_grouping` property. + if !*do_text_span { + check_text_span_prop(glyph, text, context_color, font_container, do_text_span); + } + }; + + for glyphs in glyphs.windows(2) { + let prev = &glyphs[0]; + let next = &glyphs[1]; + + // If two glyphs are part of the same cluster, we definitely need a text span. + do_text_span |= prev.text_range() == next.text_range(); + + check_single(prev, &mut do_text_span); + } + + // Since windows checks groups of two, we need to manually check the last glyph + check_single(glyphs.last().unwrap(), &mut do_text_span); + + do_text_span +} + +fn check_text_span_prop( + glyph: &impl Glyph, + text: &str, + context_color: rgb::Color, + font_container: &mut FontContainer, + do_text_span: &mut bool, +) { + let (identifier, pdf_glyph) = + font_container.add_glyph(ColoredGlyph::new(glyph.glyph_id(), context_color)); + let pdf_font = font_container + .get_from_identifier_mut(identifier.clone()) + .unwrap(); + + let range = glyph.text_range().clone(); + let text = &text[range.clone()]; + let codepoints = pdf_font.get_codepoints(pdf_glyph); + + if codepoints.is_some_and(|t| t != text) { + // If there already exists an entry, then the codepoints are invalid, and we will have to + // use a text span. + *do_text_span = true; + } else if codepoints.is_none() { + pdf_font.set_codepoints(pdf_glyph, text.to_string(), glyph.location()); + } +} diff --git a/vendor/krilla/src/text/mod.rs b/vendor/krilla/src/text/mod.rs new file mode 100644 index 00000000..3050fbd4 --- /dev/null +++ b/vendor/krilla/src/text/mod.rs @@ -0,0 +1,232 @@ +//! Text and font support. +//! +//! krilla has extensive support for OpenType fonts. It supports CFF-based as well +//! as TTF-based OpenType fonts. In addition to that, krilla also supports +//! all major tables used in color fonts, including the `SVG`, `COLR`, `sbix` and +//! `CBDT`/`EBDT` (only PNG) tables, something that, to the best of my knowledge, no other +//! Rust crates provides. +//! +//! Even better is the fact that you do not need to take care of choosing the right +//! table for drawing glyphs: All you need to do is to provide the [`Font`] object with +//! an appropriate index. + +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; + +use rustc_hash::FxHashMap; + +use crate::text::cid::CIDFont; +use crate::text::type3::{ColoredGlyph, Type3Font, Type3FontMapper, Type3ID}; +pub(crate) mod cid; +pub(crate) mod font; +pub(crate) mod glyph; +pub(crate) mod group; +#[cfg(feature = "simple-text")] +pub(crate) mod shape; +pub(crate) mod type3; + +pub use font::*; +pub use glyph::*; +#[cfg(feature = "simple-text")] +pub use shape::TextDirection; + +pub(crate) const PDF_UNITS_PER_EM: f32 = 1000.0; + +/// A unique CID identifier. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub(crate) struct CIDIdentifier(pub Font); + +/// A unique Type3 font identifier. Type3 fonts can only hold 256 glyphs, which +/// means that we might have to create more than one Type3 font. This is why we +/// additionally store an index that indicates which specific Type3Font we are +/// referring to. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub(crate) struct Type3Identifier(pub Font, pub Type3ID); + +/// A font identifier for a PDF font. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub(crate) enum FontIdentifier { + Cid(CIDIdentifier), + Type3(Type3Identifier), +} + +/// A container that holds all PDF fonts belonging to an OTF font. +pub(crate) struct FontContainer { + font: Font, + type3_mapper: Type3FontMapper, + cid_font: CIDFont, + cid_cache: FxHashMap, + type3_cache: HashMap, +} + +impl FontContainer { + pub(crate) fn new(font: Font) -> Self { + Self { + font: font.clone(), + type3_mapper: Type3FontMapper::new(font.clone()), + cid_font: CIDFont::new(font.clone()), + cid_cache: Default::default(), + type3_cache: Default::default(), + } + } + + pub(crate) fn type3_mapper(&self) -> &Type3FontMapper { + &self.type3_mapper + } + + pub(crate) fn cid_font(&self) -> &CIDFont { + &self.cid_font + } + + #[inline] + pub(crate) fn font_identifier(&self, glyph: ColoredGlyph) -> Option { + let (id, _) = self + .cid_cache + .get(&glyph.glyph_id.to_u32()) + .or_else(|| self.type3_cache.get(&glyph.to_owned()))?; + Some(id.clone()) + } + + #[inline] + pub(crate) fn get_from_identifier_mut( + &mut self, + font_identifier: FontIdentifier, + ) -> Option<&mut dyn PdfFont> { + if self.cid_font.identifier() == font_identifier { + Some(&mut self.cid_font) + } else { + // If the identifier doesn't match either of CID or Type3, this will + // return `None`. + Some(self.type3_mapper.font_mut_from_id(font_identifier)?) + } + } + + #[inline] + pub(crate) fn get_from_identifier( + &self, + font_identifier: FontIdentifier, + ) -> Option<&dyn PdfFont> { + if self.cid_font.identifier() == font_identifier { + Some(&self.cid_font) + } else { + // If the identifier doesn't match either of CID or Type3, this will + // return `None`. + Some(self.type3_mapper.font_from_id(font_identifier)?) + } + } + + #[inline] + pub(crate) fn add_glyph(&mut self, glyph: ColoredGlyph) -> (FontIdentifier, PDFGlyph) { + if let Some(e) = self + .cid_cache + .get(&glyph.glyph_id.to_u32()) + .or_else(|| self.type3_cache.get(&glyph.to_owned())) + { + // We already know whether this glyph uses a CID or Type3 glyph. + e.clone() + } else if should_outline(&self.font, glyph.glyph_id) { + let cid = self.cid_font.add_glyph(glyph.glyph_id); + let res = (self.cid_font.identifier(), PDFGlyph::Cid(cid)); + self.cid_cache.insert(glyph.glyph_id.to_u32(), res.clone()); + res + } else { + let (identifier, gid) = self.type3_mapper.add_glyph(glyph.to_owned()); + let res = (identifier, PDFGlyph::Type3(gid)); + self.type3_cache.insert(glyph.to_owned(), res.clone()); + res + } + } +} + +pub(crate) trait PdfFont { + fn units_per_em(&self) -> f32; + fn font(&self) -> Font; + fn get_codepoints(&self, pdf_glyph: PDFGlyph) -> Option<&str>; + fn set_codepoints( + &mut self, + pdf_glyph: PDFGlyph, + text: String, + location: Option, + ); + fn get_gid(&self, glyph: ColoredGlyph) -> Option; + fn force_fill(&self) -> bool; +} + +impl PdfFont for Type3Font { + fn units_per_em(&self) -> f32 { + self.unit_per_em() + } + + fn font(&self) -> Font { + Type3Font::font(self) + } + + #[track_caller] + fn get_codepoints(&self, pdf_glyph: PDFGlyph) -> Option<&str> { + match pdf_glyph { + PDFGlyph::Type3(t3) => self.get_codepoints(t3), + PDFGlyph::Cid(_) => panic!("attempted to pass cid to type 3 font"), + } + } + + #[track_caller] + fn set_codepoints( + &mut self, + pdf_glyph: PDFGlyph, + text: String, + location: Option, + ) { + match pdf_glyph { + PDFGlyph::Type3(t3) => self.set_codepoints(t3, text, location), + PDFGlyph::Cid(_) => panic!("attempted to pass cid to type 3 font"), + } + } + + fn get_gid(&self, glyph: ColoredGlyph) -> Option { + self.get_gid(&glyph.to_owned()).map(PDFGlyph::Type3) + } + + fn force_fill(&self) -> bool { + true + } +} + +impl PdfFont for CIDFont { + fn units_per_em(&self) -> f32 { + self.units_per_em() + } + + fn font(&self) -> Font { + CIDFont::font(self) + } + + #[track_caller] + fn get_codepoints(&self, pdf_glyph: PDFGlyph) -> Option<&str> { + match pdf_glyph { + PDFGlyph::Type3(_) => panic!("attempted to pass type 3 glyph to cid font"), + PDFGlyph::Cid(cid) => self.get_codepoints(cid), + } + } + + #[track_caller] + fn set_codepoints( + &mut self, + pdf_glyph: PDFGlyph, + text: String, + location: Option, + ) { + match pdf_glyph { + PDFGlyph::Type3(_) => panic!("attempted to pass type 3 glyph to cid font"), + PDFGlyph::Cid(cid) => self.set_codepoints(cid, text, location), + } + } + + fn get_gid(&self, glyph: ColoredGlyph) -> Option { + self.get_cid(glyph.glyph_id).map(PDFGlyph::Cid) + } + + fn force_fill(&self) -> bool { + false + } +} diff --git a/vendor/krilla/src/text/shape.rs b/vendor/krilla/src/text/shape.rs new file mode 100644 index 00000000..0d1d57c3 --- /dev/null +++ b/vendor/krilla/src/text/shape.rs @@ -0,0 +1,102 @@ +use crate::text::Font; +use crate::text::{GlyphId, KrillaGlyph}; +use rustybuzz::ttf_parser::Tag; +use rustybuzz::{Direction, UnicodeBuffer}; + +/// Naively shape some text with a single font. +pub(crate) fn naive_shape(text: &str, font: Font, direction: TextDirection) -> Vec { + let data = font.font_data(); + let mut rb_font = rustybuzz::Face::from_slice(data.as_ref(), font.index()).unwrap(); + + for variation in font.variation_coordinates() { + rb_font.set_variation(Tag::from_bytes(variation.0.get()), variation.1.get()); + } + + let mut buffer = UnicodeBuffer::new(); + buffer.push_str(text); + buffer.guess_segment_properties(); + + match direction { + TextDirection::LeftToRight => buffer.set_direction(Direction::LeftToRight), + TextDirection::RightToLeft => buffer.set_direction(Direction::RightToLeft), + TextDirection::TopToBottom => buffer.set_direction(Direction::TopToBottom), + TextDirection::BottomToTop => buffer.set_direction(Direction::BottomToTop), + TextDirection::Auto => {} + } + + let dir = buffer.direction(); + + let output = rustybuzz::shape(&rb_font, &[], buffer); + + let positions = output.glyph_positions(); + let infos = output.glyph_infos(); + + let mut glyphs = vec![]; + + for i in 0..output.len() { + let pos = positions[i]; + let start_info = infos[i]; + + let start = start_info.cluster as usize; + + let end = if dir == Direction::LeftToRight || dir == Direction::TopToBottom { + let mut e = i.checked_add(1); + loop { + if let Some(index) = e { + if let Some(end_info) = infos.get(index) { + if end_info.cluster == start_info.cluster { + e = index.checked_add(1); + continue; + } + } + } + + break; + } + + e + } else { + let mut e = i.checked_sub(1); + while let Some(index) = e { + if let Some(end_info) = infos.get(index) { + if end_info.cluster == start_info.cluster { + e = index.checked_sub(1); + } else { + break; + } + } + } + + e + } + .and_then(|last| infos.get(last)) + .map_or(text.len(), |info| info.cluster as usize); + + glyphs.push(KrillaGlyph::new( + GlyphId::new(start_info.glyph_id), + pos.x_advance as f32 / font.units_per_em(), + pos.x_offset as f32 / font.units_per_em(), + pos.y_offset as f32 / font.units_per_em(), + pos.y_advance as f32 / font.units_per_em(), + start..end, + None, + )); + } + + glyphs +} + +/// The direction of a text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TextDirection { + /// Determine the direction automatically. + Auto, + /// Left to right. + LeftToRight, + /// Right to left. + RightToLeft, + /// Top to bottom. + TopToBottom, + /// Bottom to top. + BottomToTop, +} diff --git a/vendor/krilla/src/text/type3.rs b/vendor/krilla/src/text/type3.rs new file mode 100644 index 00000000..1ba149cb --- /dev/null +++ b/vendor/krilla/src/text/type3.rs @@ -0,0 +1,514 @@ +use std::collections::HashSet; +use std::hash::Hash; +use std::ops::DerefMut; + +use pdf_writer::types::{FontFlags, UnicodeCmap}; +use pdf_writer::writers::WMode; +use pdf_writer::{Finish, Name, Ref, Str}; +use rustc_hash::FxHashMap; + +use super::{FontIdentifier, Type3Identifier}; +use crate::chunk_container::ChunkContainer; +use crate::color::rgb; +use crate::configure::PdfVersion; +use crate::geom::Path; +use crate::geom::{Rect, Transform}; +use crate::graphics::paint::Fill; +use crate::graphics::xobject::XObject; +use crate::resource::ResourceDictionaryBuilder; +use crate::serialize::SerializeContext; +use crate::stream::{FilterStreamBuilder, StreamBuilder}; +use crate::surface::Location; +use crate::text::cid::write_cmap_entry; +use crate::text::cid::{CMAP_NAME, IDENTITY_H, SYSTEM_INFO}; +use crate::text::outline::glyph_path; +use crate::text::GlyphId; +use crate::text::{self, Font}; +use crate::util::NameExt; + +pub(crate) type Gid = u8; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub(crate) struct ColoredGlyph { + pub(crate) glyph_id: GlyphId, + // Some formats like COLR and SVG allow drawing the same glyph + // with a different context color, so we need to store that as well. + pub(crate) context_color: rgb::Color, +} + +impl ColoredGlyph { + pub(crate) fn new(glyph_id: GlyphId, context_color: rgb::Color) -> ColoredGlyph { + Self { + glyph_id, + context_color, + } + } +} + +#[derive(Debug)] +pub(crate) struct Type3Font { + font: Font, + glyphs: Vec, + widths: Vec, + cmap_entries: FxHashMap)>, + glyph_set: HashSet, + index: usize, +} + +impl Type3Font { + pub(crate) fn new(font: Font, index: usize) -> Self { + Self { + font, + glyphs: Vec::new(), + cmap_entries: FxHashMap::default(), + widths: Vec::new(), + glyph_set: HashSet::new(), + index, + } + } + + // Unlike CID fonts, the units per em of type 3 fonts does not have to be 1000. + pub(crate) fn unit_per_em(&self) -> f32 { + self.font.units_per_em() + } + + pub(crate) fn is_full(&self) -> bool { + self.count() == 256 + } + + pub(crate) fn count(&self) -> u16 { + u16::try_from(self.glyphs.len()).unwrap() + } + + #[inline] + pub(crate) fn covers(&self, glyph: &ColoredGlyph) -> bool { + self.glyph_set.contains(glyph) + } + + #[inline] + pub(crate) fn get_gid(&self, glyph: &ColoredGlyph) -> Option { + self.glyphs + .iter() + .position(|g| g == glyph) + .and_then(|n| u8::try_from(n).ok()) + } + + #[inline] + pub(crate) fn add_glyph(&mut self, glyph: ColoredGlyph) -> u8 { + if let Some(pos) = self.get_gid(&glyph) { + pos + } else { + assert!(self.glyphs.len() < 256); + + self.glyph_set.insert(glyph); + self.glyphs.push(glyph); + self.widths + .push(self.font.advance_width(glyph.glyph_id).unwrap_or(0.0)); + u8::try_from(self.glyphs.len() - 1).unwrap() + } + } + + #[inline] + pub(crate) fn get_codepoints(&self, gid: Gid) -> Option<&str> { + self.cmap_entries.get(&gid).map(|s| s.0.as_str()) + } + + #[inline] + pub(crate) fn set_codepoints(&mut self, gid: Gid, text: String, location: Option) { + self.cmap_entries.insert(gid, (text, location)); + } + + #[inline] + pub(crate) fn font(&self) -> Font { + self.font.clone() + } + + #[inline] + pub(crate) fn identifier(&self) -> FontIdentifier { + FontIdentifier::Type3(Type3Identifier(self.font.clone(), self.index)) + } + + pub(crate) fn serialize( + &self, + sc: &mut SerializeContext, + chunk_container: &mut ChunkContainer, + root_ref: Ref, + ) { + let mut stream_chunk = sc.new_chunk(); + + let mut rd_builder = ResourceDictionaryBuilder::new(); + let mut font_bbox = Rect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(); + + let glyph_streams = self + .glyphs + .iter() + .enumerate() + .map(|(index, glyph)| { + let mut stream_surface = StreamBuilder::new(sc, chunk_container); + let mut surface = stream_surface.surface(); + + // In case this returns `None`, the surface is guaranteed to be empty. + let drawn_color_glyph = text::draw_color_glyph( + self.font.clone(), + glyph.context_color, + glyph.glyph_id, + Transform::default(), + &mut surface, + ); + + if drawn_color_glyph.is_none() { + // If this code path is reached, either we are dealing with a glyph that has no outline + // (like .notdef), or it means we tried to create a Type3 + // font from a font that does not have any (valid) color/bitmap/svg table. + // Therefore, as a last resort we draw the outline glyph as a black glyph. + // PDF does have the concept of shape glyphs which take the color or where + // they are drawn from. + // The problem with those is that they seemingly have very bad viewer support. For + // example, filled glyphs with gradients become broken in some viewers, + // and stroking does not work at all consistently. Because of this, + // we don't bother trying to implement it for now. + + // If this is the case (i.e. no color glyph was drawn, either because no table + // exists or an error occurred, the surface is guaranteed to be empty. + // So we can just safely draw the outline glyph instead without having to + // worry about the surface being "dirty". + if let Some(path) = glyph_path(self.font.clone(), glyph.glyph_id) { + surface.set_fill(Some(Fill::default())); + surface.set_stroke(None); + surface.draw_path(&Path(path)); + } + }; + + surface.finish(); + let stream = stream_surface.finish(); + let mut content = sc.new_content(); + + // I considered writing into the stream directly instead of creating an XObject + // and showing that, but it seems like many viewers don't like that, and emojis + // look messed up. Using XObjects seems like the best choice here. + content.start_color_glyph(self.widths[index]); + let x_object = XObject::new(stream, false, false, None); + if !x_object.is_empty() { + font_bbox.expand(&x_object.bbox()); + let x_name = rd_builder + .register_resource(sc.register_resourceable(chunk_container, x_object)); + content.x_object(x_name.to_pdf_name()); + } + + let stream = content.finish(); + + let font_stream = FilterStreamBuilder::new_from_content_stream( + stream.as_slice(), + &sc.serialize_settings(), + ) + .finish(&sc.serialize_settings()); + + let stream_ref = sc.new_ref(); + let mut stream = stream_chunk.stream(stream_ref, font_stream.encoded_data()); + font_stream.write_filters(stream.deref_mut()); + + stream_ref + }) + .collect::>(); + + let resource_dictionary = rd_builder.finish(); + let chunk = &mut chunk_container.non_stream.fonts; + + let descriptor_ref = if sc.serialize_settings().pdf_version() >= PdfVersion::Pdf15 { + Some(sc.new_ref()) + } else { + None + }; + let cmap_ref = sc.new_ref(); + + let mut flags = FontFlags::empty(); + flags.set( + FontFlags::SERIF, + self.font + .postscript_name() + .is_some_and(|n| n.contains("Serif")), + ); + flags.set(FontFlags::FIXED_PITCH, self.font.is_monospaced()); + flags.set(FontFlags::ITALIC, self.font.italic_angle() != 0.0); + flags.insert(FontFlags::SYMBOLIC); + flags.insert(FontFlags::SMALL_CAP); + + let italic_angle = self.font.italic_angle(); + let ascender = font_bbox.bottom(); + let descender = font_bbox.top(); + + let mut gids = self + .glyph_set + .iter() + .map(|g| g.glyph_id) + .collect::>(); + gids.sort(); + let base_font = base_font_name(&self.font); + + if let Some(descriptor_ref) = descriptor_ref { + // Write the font descriptor (contains metrics about the font). + let mut font_descriptor = chunk.font_descriptor(descriptor_ref); + font_descriptor + .name(Name(base_font.as_bytes())) + .flags(flags) + .bbox(font_bbox.to_pdf_rect()) + .italic_angle(italic_angle) + .ascent(ascender) + .descent(descender) + // Adobe recommends these for tagged PDF for 1.5+ (descriptors for Type3 fonts + // are only written for 1.5+ in the first place, so no additional checks needed) + // so we write them as well. + // Unfortunately we have no way of determining the actual family name, so we just + // take the next best thing + .family(Str(base_font.as_bytes())) + .stretch( + match skrifa::attribute::Stretch::new(self.font().stretch()) { + skrifa::attribute::Stretch::ULTRA_CONDENSED => { + pdf_writer::types::FontStretch::UltraCondensed + } + skrifa::attribute::Stretch::EXTRA_CONDENSED => { + pdf_writer::types::FontStretch::ExtraCondensed + } + skrifa::attribute::Stretch::CONDENSED => { + pdf_writer::types::FontStretch::Condensed + } + skrifa::attribute::Stretch::SEMI_CONDENSED => { + pdf_writer::types::FontStretch::SemiCondensed + } + skrifa::attribute::Stretch::NORMAL => { + pdf_writer::types::FontStretch::Normal + } + skrifa::attribute::Stretch::SEMI_EXPANDED => { + pdf_writer::types::FontStretch::SemiExpanded + } + skrifa::attribute::Stretch::EXPANDED => { + pdf_writer::types::FontStretch::Expanded + } + skrifa::attribute::Stretch::EXTRA_EXPANDED => { + pdf_writer::types::FontStretch::ExtraExpanded + } + skrifa::attribute::Stretch::ULTRA_EXPANDED => { + pdf_writer::types::FontStretch::UltraExpanded + } + // Fallback + _ => pdf_writer::types::FontStretch::Normal, + }, + ) + .weight(match self.font.weight() as i32 { + 150..250 => 200, + 250..350 => 300, + 350..450 => 400, + 450..550 => 500, + 550..650 => 600, + 650..750 => 700, + 750..850 => 800, + other => { + if other < 150 { + 100 + } else { + 900 + } + } + }); + + font_descriptor.finish(); + } + + let mut type3_font = chunk.type3_font(root_ref); + resource_dictionary.to_pdf_resources( + &mut type3_font, + sc, + &mut chunk_container.non_stream.resource_dictionaries, + ); + + // See https://github.com/typst/typst/issues/5067 as to why we write this. + type3_font.name(Name(base_font.as_bytes())); + type3_font.bbox(font_bbox.to_pdf_rect()); + type3_font.to_unicode(cmap_ref); + type3_font.matrix( + Transform::from_scale( + 1.0 / (self.font.units_per_em()), + 1.0 / (self.font.units_per_em()), + ) + .to_pdf_transform(), + ); + type3_font.first_char(0); + type3_font.last_char(u8::try_from(self.glyphs.len() - 1).unwrap()); + type3_font.widths(self.widths.iter().copied()); + if let Some(descriptor_ref) = descriptor_ref { + type3_font.font_descriptor(descriptor_ref); + } + + let mut char_procs = type3_font.char_procs(); + for (gid, ref_) in glyph_streams.iter().enumerate() { + char_procs.pair(format!("g{gid}").to_pdf_name(), *ref_); + } + char_procs.finish(); + + let names = (0..self.glyphs.len() as u16) + .map(|gid| format!("g{gid}")) + .collect::>(); + + type3_font + .encoding_custom() + .differences() + .consecutive(0, names.iter().map(|n| n.to_pdf_name())); + + type3_font.finish(); + + let cmap = { + let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); + + for g in 0..self.glyphs.len() { + let g = u8::try_from(g).unwrap(); + let entry = self.cmap_entries.get(&g); + write_cmap_entry(&self.font, entry, sc, &mut cmap, g); + } + + cmap + }; + + let cmap_stream = cmap.finish(); + let cmap_stream = + FilterStreamBuilder::new_from_content_stream(&cmap_stream, &sc.serialize_settings()) + .finish(&sc.serialize_settings()); + let mut cmap = stream_chunk.cmap(cmap_ref, cmap_stream.encoded_data()); + cmap_stream.write_filters(cmap.deref_mut().deref_mut()); + cmap.writing_mode(WMode::Horizontal); + cmap.finish(); + chunk_container.streams.fonts.push(stream_chunk); + } +} + +pub(crate) type Type3ID = usize; + +// A font can have multiple Type3 fonts, if more than 256 glyphs are used. +// We use this struct to keep track of them. +#[derive(Debug)] +pub(crate) struct Type3FontMapper { + font: Font, + fonts: Vec, +} + +impl Type3FontMapper { + pub(crate) fn new(font: Font) -> Type3FontMapper { + Self { + font, + fonts: Vec::new(), + } + } +} + +impl Type3FontMapper { + /// Given a requested glyph coverage, find the corresponding font identifier of the + /// font that contains it. + pub(crate) fn id_from_glyph(&self, glyph: &ColoredGlyph) -> Option { + self.fonts + .iter() + .position(|f| f.covers(glyph)) + .map(|p| self.fonts[p].identifier()) + } + + /// Given a font identifier, return the corresponding Type3 font if it is part of + /// that type 3 font mapper. + pub(crate) fn font_from_id(&self, identifier: FontIdentifier) -> Option<&Type3Font> { + let pos = self + .fonts + .iter() + .position(|f| f.identifier() == identifier)?; + self.fonts.get(pos) + } + + pub(crate) fn font_mut_from_id( + &mut self, + identifier: FontIdentifier, + ) -> Option<&mut Type3Font> { + let pos = self + .fonts + .iter() + .position(|f| f.identifier() == identifier)?; + self.fonts.get_mut(pos) + } + + pub(crate) fn is_empty(&self) -> bool { + self.fonts.is_empty() + } + + pub(crate) fn fonts(&self) -> &[Type3Font] { + &self.fonts + } + + pub(crate) fn add_glyph(&mut self, glyph: ColoredGlyph) -> (FontIdentifier, Gid) { + // If the glyph has already been added, return the font identifier of + // the type 3 font as well as the Type3 gid in that font. + if let Some(id) = self.id_from_glyph(&glyph) { + let gid = self + .font_from_id(id.clone()) + .unwrap() + .get_gid(&glyph) + .unwrap(); + return (id, gid); + } + + if let Some(last_font) = self.fonts.last_mut() { + if last_font.is_full() { + // If the last Type3 font is full, create a new one. + let mut font = Type3Font::new(self.font.clone(), self.fonts.len()); + let id = font.identifier(); + let gid = font.add_glyph(glyph); + self.fonts.push(font); + (id, gid) + } else { + // Otherwise, insert it into the last Type3 font. + let id = last_font.identifier(); + (id, last_font.add_glyph(glyph)) + } + } else { + // If not Type3 font exists yet, create a new one. + let mut font = Type3Font::new(self.font.clone(), self.fonts.len()); + let id = font.identifier(); + let gid = font.add_glyph(glyph); + self.fonts.push(font); + (id, gid) + } + } +} + +pub(crate) fn base_font_name(font: &Font) -> String { + const REST_LEN: usize = 1 + 1 + IDENTITY_H.len(); + let postscript_name = font.postscript_name().unwrap_or("unknown"); + + let max_len = 127 - REST_LEN; + + let trimmed = &postscript_name[..postscript_name.len().min(max_len)]; + + trimmed.to_string() +} + +#[cfg(test)] +mod tests { + use crate::color::rgb; + use crate::text::type3::{ColoredGlyph, Type3FontMapper}; + use crate::text::Font; + use crate::text::GlyphId; + use crate::util::test_utils::NOTO_COLOR_EMOJI_COLR; + + #[test] + fn type3_more_than_256_glyphs() { + let font = Font::new(NOTO_COLOR_EMOJI_COLR.clone(), 0).unwrap(); + let mut t3 = Type3FontMapper::new(font.clone()); + + for i in 2..258 { + t3.add_glyph(ColoredGlyph::new(GlyphId::new(i), rgb::Color::black())); + } + + assert_eq!(t3.fonts.len(), 1); + assert_eq!( + t3.fonts[0].add_glyph(ColoredGlyph::new(GlyphId::new(20), rgb::Color::black())), + 18 + ); + + t3.add_glyph(ColoredGlyph::new(GlyphId::new(512), rgb::Color::black())); + assert_eq!(t3.fonts.len(), 2); + } +} diff --git a/vendor/krilla/src/util.rs b/vendor/krilla/src/util.rs new file mode 100644 index 00000000..e9f82284 --- /dev/null +++ b/vendor/krilla/src/util.rs @@ -0,0 +1,286 @@ +use std::any::Any; +use std::fmt; +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; +use std::ops::Deref; + +use crate::geom::Rect; +use crate::graphics::color::{DEVICE_CMYK, DEVICE_GRAY, DEVICE_RGB}; +use crate::graphics::paint::Stroke; +use crate::resource::Resource; +use crate::serialize::MaybeDeviceColorSpace; +use base64::Engine; +pub(crate) use deferred::*; +use pdf_writer::{Dict, Name}; +use siphasher::sip128::{Hasher128, SipHasher13}; +use tiny_skia_path::Path; + +pub(crate) trait NameExt { + fn to_pdf_name(&self) -> Name<'_>; +} + +impl NameExt for String { + fn to_pdf_name(&self) -> Name<'_> { + Name(self.as_bytes()) + } +} + +impl NameExt for &str { + fn to_pdf_name(&self) -> Name<'_> { + Name(self.as_bytes()) + } +} + +pub(crate) fn calculate_stroke_bbox(stroke: &Stroke, path: &Path) -> Option { + let stroke = stroke.clone().into_tiny_skia(); + + if let Some(stroked_path) = path.stroke(&stroke, 1.0) { + return Some(Rect::from_tsp(stroked_path.compute_tight_bounds()?)); + } + + None +} + +pub(crate) struct Prehashed { + hash: u128, + value: T, +} + +impl Prehashed { + #[inline] + pub fn new(value: T) -> Self { + let hash = value.sip_hash(); + Self { hash, value } + } +} + +impl Eq for Prehashed {} + +impl PartialEq for Prehashed { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl Deref for Prehashed { + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Clone for Prehashed { + fn clone(&self) -> Self { + Self { + hash: self.hash, + value: self.value.clone(), + } + } +} + +impl Debug for Prehashed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} + +impl Hash for Prehashed { + #[inline] + fn hash(&self, state: &mut H) { + state.write_u128(self.hash); + } +} + +/// Extra methods for [`[T]`](slice). +pub(crate) trait SliceExt { + /// Split a slice into consecutive runs with the same key and yield for + /// each such run the key and the slice of elements with that key. + fn group_by_key(&self, f: F) -> GroupByKey<'_, T, F> + where + F: FnMut(&T) -> K, + K: PartialEq; +} + +impl SliceExt for [T] { + fn group_by_key(&self, f: F) -> GroupByKey<'_, T, F> { + GroupByKey { slice: self, f } + } +} + +/// This struct is created by [`SliceExt::group_by_key`]. +pub(crate) struct GroupByKey<'a, T, F> { + slice: &'a [T], + f: F, +} + +impl<'a, T, K, F> Iterator for GroupByKey<'a, T, F> +where + F: FnMut(&T) -> K, + K: PartialEq, +{ + type Item = (K, &'a [T]); + + fn next(&mut self) -> Option { + let mut iter = self.slice.iter(); + let key = (self.f)(iter.next()?); + let count = 1 + iter.take_while(|t| (self.f)(t) == key).count(); + let (head, tail) = self.slice.split_at(count); + self.slice = tail; + Some((key, head)) + } +} + +pub(crate) trait SipHashable { + fn sip_hash(&self) -> u128; +} + +impl SipHashable for T +where + T: Hash + ?Sized + 'static, +{ + fn sip_hash(&self) -> u128 { + let mut state = SipHasher13::new(); + self.type_id().hash(&mut state); + self.hash(&mut state); + state.finish128().as_u128() + } +} + +/// Create a base64-encoded hash of the value. +pub(crate) fn stable_hash_base64(value: &T) -> String { + base64::engine::general_purpose::STANDARD.encode(stable_hash128(value).to_be_bytes()) +} + +/// Calculate a 128-bit siphash of a value. +pub(crate) fn stable_hash128(value: &T) -> u128 { + /// A hasher that hashes usize as u64 to produce stable hashes between + /// 32-bit and 64-bit architectures. + struct StableHasher(SipHasher13); + + impl Hasher for StableHasher { + fn finish(&self) -> u64 { + self.0.finish() + } + + fn write(&mut self, bytes: &[u8]) { + self.0.write(bytes); + } + + fn write_usize(&mut self, i: usize) { + self.0.write_u64(i as u64); + } + } + + let mut state = StableHasher(SipHasher13::new()); + value.hash(&mut state); + state.0.finish128().as_u128() +} + +pub(crate) fn set_colorspace(cs: MaybeDeviceColorSpace, target: &mut Dict) { + let pdf_cs = target.insert(Name(b"ColorSpace")); + + match cs { + MaybeDeviceColorSpace::DeviceGray => pdf_cs.primitive(DEVICE_GRAY.to_pdf_name()), + MaybeDeviceColorSpace::DeviceRgb => pdf_cs.primitive(DEVICE_RGB.to_pdf_name()), + MaybeDeviceColorSpace::DeviceCMYK => pdf_cs.primitive(DEVICE_CMYK.to_pdf_name()), + MaybeDeviceColorSpace::ColorSpace(cs) => pdf_cs.primitive(cs.get_ref()), + } +} + +#[cfg(not(feature = "rayon"))] +mod deferred { + pub(crate) struct Deferred(T); + + impl Deferred { + pub fn new(f: F) -> Self + where + F: FnOnce() -> T + Send + Sync + 'static, + { + Self(f()) + } + + pub fn wait(&self) -> &T { + &self.0 + } + } +} + +#[cfg(feature = "rayon")] +mod deferred { + /// A value that is lazily executed on another thread. + /// + /// Execution will be started in the background and can be waited on. + pub(crate) struct Deferred(std::sync::Arc>); + + #[cfg(feature = "rayon")] + impl Deferred { + /// Creates a new deferred value. + /// + /// The closure will be called on a secondary thread such that the value + /// can be initialized in parallel. + pub fn new(f: F) -> Self + where + F: FnOnce() -> T + Send + Sync + 'static, + { + let inner = std::sync::Arc::new(std::sync::OnceLock::new()); + let cloned = std::sync::Arc::clone(&inner); + rayon::spawn(move || { + // Initialize the value if it hasn't been initialized yet. + // We do this to avoid panicking in case it was set externally. + cloned.get_or_init(f); + }); + Self(inner) + } + + /// Waits on the value to be initialized. + /// + /// If the value has already been initialized, this will return + /// immediately. Otherwise, this will block until the value is + /// initialized in another thread. + pub fn wait(&self) -> &T { + // Fast path if the value is already available. We don't want to yield + // to rayon in that case. + if let Some(value) = self.0.get() { + return value; + } + + // Ensure that we yield to give the deferred value a chance to compute + // single-threaded platforms (for WASM compatibility). + while let Some(rayon::Yield::Executed) = rayon::yield_now() {} + + self.0.wait() + } + } +} + +#[cfg(test)] +pub(crate) mod test_utils { + use std::path::PathBuf; + use std::sync::Arc; + use std::sync::LazyLock; + + use crate::Data; + + pub(crate) static WORKSPACE_PATH: LazyLock = + LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../")); + + pub(crate) static ASSETS_PATH: LazyLock = + LazyLock::new(|| WORKSPACE_PATH.join("assets")); + + static FONT_PATH: LazyLock = LazyLock::new(|| ASSETS_PATH.join("fonts")); + + macro_rules! lazy_font { + ($name:ident, $path:expr) => { + pub static $name: LazyLock = + LazyLock::new(|| Arc::new(std::fs::read($path).unwrap()).into()); + }; + } + + lazy_font!( + NOTO_COLOR_EMOJI_COLR, + FONT_PATH.join("NotoColorEmoji.COLR.subset.ttf") + ); +} From c07b3fada6c31b5517bea6b49ab5ae64793fcdf8 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Tue, 13 Oct 2026 12:52:32 -0600 Subject: [PATCH 2/6] feat(pdf): share patched reading and rasterization --- .github/workflows/pdf.yml | 86 + ATTRIBUTION.md | 30 +- Cargo.lock | 177 +- Cargo.toml | 11 +- NOTICE | 15 +- README.md | 2 +- crates/pdf/Cargo.toml | 9 +- crates/pdf/README.md | 105 +- crates/pdf/read-only-contract/Cargo.lock | 537 ++ crates/pdf/read-only-contract/Cargo.toml | 23 + crates/pdf/read-only-contract/README.md | 27 + crates/pdf/read-only-contract/src/lib.rs | 123 + crates/pdf/src/lib.rs | 17 +- crates/pdf/src/raster.rs | 178 + crates/pdf/src/read.rs | 105 + crates/pdf/tests/contracts.rs | 363 + crates/pdf/tests/interpretation.rs | 120 + crates/pdf/tests/upstream_raster.rs | 271 + crates/pdf/tests/upstream_reading.rs | 121 + .../pdf-backend/hayro-cmap/LICENSE-APACHE | 177 + licenses/pdf-backend/hayro-cmap/LICENSE-MIT | 7 + licenses/pdf-backend/hayro-cmap/LICENSE.txt | 28 + .../hayro-interpret/LICENSE-APACHE | 177 + .../pdf-backend/hayro-interpret/LICENSE-MIT | 7 + .../hayro-interpret/assets/CGATS_LICENSE.txt | 116 + .../hayro-interpret/assets/LICENSE_FOXIT | 27 + .../pdf-backend/hayro-jbig2/LICENSE-APACHE | 177 + licenses/pdf-backend/hayro-jbig2/LICENSE-MIT | 7 + .../pdf-backend/hayro-syntax/LICENSE-APACHE | 177 + licenses/pdf-backend/hayro-syntax/LICENSE-MIT | 7 + licenses/pdf-backend/hayro/LICENSE-APACHE | 177 + licenses/pdf-backend/hayro/LICENSE-MIT | 7 + scripts/fetch-pdf-test-fonts.py | 38 + scripts/test-pdf-backends.sh | 18 + vendor/README.craft-reader.md | 36 + vendor/hayro-interpret/.cargo-ok | 1 + vendor/hayro-interpret/.cargo_vcs_info.json | 6 + vendor/hayro-interpret/Cargo.lock | 529 ++ vendor/hayro-interpret/Cargo.toml | 139 + vendor/hayro-interpret/Cargo.toml.orig | 38 + vendor/hayro-interpret/LICENSE-APACHE | 177 + vendor/hayro-interpret/LICENSE-MIT | 7 + vendor/hayro-interpret/README.md | 58 + vendor/hayro-interpret/SOURCE.json | 440 + .../assets/CGATS001Compat-v2-micro.icc | Bin 0 -> 8464 bytes .../hayro-interpret/assets/CGATS_LICENSE.txt | 116 + .../hayro-interpret/assets/FoxitDingbats.pfb | Bin 0 -> 29513 bytes vendor/hayro-interpret/assets/FoxitFixed.pfb | Bin 0 -> 17597 bytes .../hayro-interpret/assets/FoxitFixedBold.pfb | Bin 0 -> 18055 bytes .../assets/FoxitFixedBoldItalic.pfb | Bin 0 -> 19151 bytes .../assets/FoxitFixedItalic.pfb | Bin 0 -> 18746 bytes vendor/hayro-interpret/assets/FoxitSans.pfb | Bin 0 -> 15025 bytes .../hayro-interpret/assets/FoxitSansBold.pfb | Bin 0 -> 16344 bytes .../assets/FoxitSansBoldItalic.pfb | Bin 0 -> 16418 bytes .../assets/FoxitSansItalic.pfb | Bin 0 -> 16339 bytes vendor/hayro-interpret/assets/FoxitSerif.pfb | Bin 0 -> 19469 bytes .../hayro-interpret/assets/FoxitSerifBold.pfb | Bin 0 -> 19395 bytes .../assets/FoxitSerifBoldItalic.pfb | Bin 0 -> 20733 bytes .../assets/FoxitSerifItalic.pfb | Bin 0 -> 21227 bytes vendor/hayro-interpret/assets/FoxitSymbol.pfb | Bin 0 -> 16729 bytes vendor/hayro-interpret/assets/LICENSE_FOXIT | 27 + vendor/hayro-interpret/assets/README.md | 13 + .../hayro-interpret/examples/extract_html.rs | 122 + .../examples/extract_images.rs | 147 + vendor/hayro-interpret/src/cache.rs | 279 + vendor/hayro-interpret/src/color.rs | 1185 +++ vendor/hayro-interpret/src/context.rs | 486 + vendor/hayro-interpret/src/convert.rs | 30 + vendor/hayro-interpret/src/device.rs | 92 + vendor/hayro-interpret/src/encode.rs | 520 ++ vendor/hayro-interpret/src/font/blob.rs | 335 + vendor/hayro-interpret/src/font/cid.rs | 645 ++ .../src/font/generated/glyph_names.rs | 8119 +++++++++++++++++ .../src/font/generated/mac_expert.rs | 174 + .../src/font/generated/mac_os_roman.rs | 48 + .../src/font/generated/mac_roman.rs | 432 + .../src/font/generated/metrics.rs | 4215 +++++++++ .../hayro-interpret/src/font/generated/mod.rs | 9 + .../src/font/generated/standard.rs | 157 + .../src/font/generated/symbol.rs | 197 + .../src/font/generated/win_ansi.rs | 230 + .../src/font/generated/zapf_dings.rs | 210 + .../src/font/glyph_simulator.rs | 53 + vendor/hayro-interpret/src/font/mod.rs | 689 ++ vendor/hayro-interpret/src/font/outline.rs | 146 + .../hayro-interpret/src/font/standard_font.rs | 694 ++ vendor/hayro-interpret/src/font/true_type.rs | 496 + vendor/hayro-interpret/src/font/type1.rs | 341 + vendor/hayro-interpret/src/font/type3.rs | 268 + vendor/hayro-interpret/src/function/mod.rs | 157 + vendor/hayro-interpret/src/function/type0.rs | 327 + vendor/hayro-interpret/src/function/type2.rs | 148 + vendor/hayro-interpret/src/function/type3.rs | 247 + vendor/hayro-interpret/src/function/type4.rs | 1103 +++ vendor/hayro-interpret/src/gradient.rs | 361 + vendor/hayro-interpret/src/interpret/mod.rs | 953 ++ vendor/hayro-interpret/src/interpret/path.rs | 178 + vendor/hayro-interpret/src/interpret/state.rs | 440 + vendor/hayro-interpret/src/interpret/text.rs | 197 + vendor/hayro-interpret/src/lib.rs | 64 + vendor/hayro-interpret/src/log.rs | 27 + vendor/hayro-interpret/src/ocg.rs | 179 + vendor/hayro-interpret/src/pattern.rs | 351 + vendor/hayro-interpret/src/shading.rs | 1081 +++ vendor/hayro-interpret/src/soft_mask.rs | 214 + vendor/hayro-interpret/src/types.rs | 366 + vendor/hayro-interpret/src/util.rs | 183 + vendor/hayro-interpret/src/x_object.rs | 1062 +++ vendor/hayro-jbig2/.gitignore | 2 + vendor/hayro-jbig2/Cargo.lock | 824 ++ vendor/hayro-jbig2/Cargo.toml | 129 + vendor/hayro-jbig2/Cargo.toml.orig | 41 + vendor/hayro-jbig2/LICENSE-APACHE | 177 + vendor/hayro-jbig2/LICENSE-MIT | 7 + vendor/hayro-jbig2/README.md | 30 + vendor/hayro-jbig2/SOURCE.json | 194 + vendor/hayro-jbig2/examples/jbig2png.rs | 64 + vendor/hayro-jbig2/src/arithmetic_decoder.rs | 398 + vendor/hayro-jbig2/src/bitmap.rs | 227 + vendor/hayro-jbig2/src/decode/generic.rs | 792 ++ .../src/decode/generic_refinement.rs | 507 + vendor/hayro-jbig2/src/decode/halftone.rs | 425 + vendor/hayro-jbig2/src/decode/mod.rs | 236 + vendor/hayro-jbig2/src/decode/pattern.rs | 221 + vendor/hayro-jbig2/src/decode/symbol.rs | 866 ++ vendor/hayro-jbig2/src/decode/text.rs | 903 ++ vendor/hayro-jbig2/src/error.rs | 322 + vendor/hayro-jbig2/src/file.rs | 214 + vendor/hayro-jbig2/src/gray_scale.rs | 291 + vendor/hayro-jbig2/src/huffman_table.rs | 621 ++ .../src/huffman_tables_generated.rs | 727 ++ vendor/hayro-jbig2/src/integer_decoder.rs | 118 + vendor/hayro-jbig2/src/integration.rs | 151 + vendor/hayro-jbig2/src/lazy.rs | 29 + vendor/hayro-jbig2/src/lib.rs | 692 ++ vendor/hayro-jbig2/src/page_info.rs | 150 + vendor/hayro-jbig2/src/reader.rs | 173 + vendor/hayro-jbig2/src/segment.rs | 419 + vendor/hayro-jbig2/src/simd.rs | 256 + vendor/hayro-jbig2/src/symbol_id_decoder.rs | 40 + vendor/hayro-syntax/Cargo.lock | 272 + vendor/hayro-syntax/Cargo.toml | 160 + vendor/hayro-syntax/Cargo.toml.orig | 46 + vendor/hayro-syntax/LICENSE-APACHE | 177 + vendor/hayro-syntax/LICENSE-MIT | 7 + vendor/hayro-syntax/README.md | 90 + vendor/hayro-syntax/SOURCE.json | 350 + vendor/hayro-syntax/examples/iterate.rs | 26 + vendor/hayro-syntax/src/bit_reader.rs | 623 ++ vendor/hayro-syntax/src/byte_reader.rs | 286 + vendor/hayro-syntax/src/content/gen_ops.py | 222 + vendor/hayro-syntax/src/content/mod.rs | 1017 +++ vendor/hayro-syntax/src/content/ops.rs | 259 + .../hayro-syntax/src/content/ops_generated.rs | 528 ++ vendor/hayro-syntax/src/crypto/aes.rs | 819 ++ vendor/hayro-syntax/src/crypto/md5.rs | 116 + vendor/hayro-syntax/src/crypto/mod.rs | 905 ++ vendor/hayro-syntax/src/crypto/rc4.rs | 125 + vendor/hayro-syntax/src/crypto/sha256.rs | 151 + vendor/hayro-syntax/src/crypto/sha384.rs | 51 + vendor/hayro-syntax/src/crypto/sha512.rs | 237 + vendor/hayro-syntax/src/data.rs | 264 + .../hayro-syntax/src/decode_limits_tests.rs | 195 + vendor/hayro-syntax/src/filter/ascii_85.rs | 91 + vendor/hayro-syntax/src/filter/ascii_hex.rs | 108 + vendor/hayro-syntax/src/filter/ccitt.rs | 156 + vendor/hayro-syntax/src/filter/dct.rs | 181 + vendor/hayro-syntax/src/filter/jbig2.rs | 96 + vendor/hayro-syntax/src/filter/jpx.rs | 191 + vendor/hayro-syntax/src/filter/lzw_flate.rs | 1335 +++ vendor/hayro-syntax/src/filter/mod.rs | 173 + vendor/hayro-syntax/src/filter/run_length.rs | 55 + vendor/hayro-syntax/src/lib.rs | 103 + vendor/hayro-syntax/src/log.rs | 40 + vendor/hayro-syntax/src/math.rs | 36 + vendor/hayro-syntax/src/metadata.rs | 43 + vendor/hayro-syntax/src/object/array.rs | 421 + vendor/hayro-syntax/src/object/bool.rs | 61 + vendor/hayro-syntax/src/object/date.rs | 230 + vendor/hayro-syntax/src/object/dict.rs | 1398 +++ vendor/hayro-syntax/src/object/indirect.rs | 61 + vendor/hayro-syntax/src/object/mod.rs | 399 + vendor/hayro-syntax/src/object/name.rs | 362 + vendor/hayro-syntax/src/object/null.rs | 53 + vendor/hayro-syntax/src/object/number.rs | 596 ++ vendor/hayro-syntax/src/object/rect.rs | 83 + vendor/hayro-syntax/src/object/ref.rs | 187 + vendor/hayro-syntax/src/object/stream.rs | 466 + vendor/hayro-syntax/src/object/string.rs | 548 ++ vendor/hayro-syntax/src/object/tuple.rs | 174 + vendor/hayro-syntax/src/page.rs | 1116 +++ vendor/hayro-syntax/src/pdf.rs | 234 + vendor/hayro-syntax/src/reader.rs | 248 + vendor/hayro-syntax/src/sync.rs | 104 + vendor/hayro-syntax/src/transform.rs | 56 + vendor/hayro-syntax/src/trivia.rs | 83 + vendor/hayro-syntax/src/util.rs | 217 + vendor/hayro-syntax/src/xref.rs | 1370 +++ vendor/hayro/.cargo-ok | 1 + vendor/hayro/.cargo_vcs_info.json | 6 + vendor/hayro/Cargo.lock | 665 ++ vendor/hayro/Cargo.toml | 122 + vendor/hayro/Cargo.toml.orig | 33 + vendor/hayro/LICENSE-APACHE | 177 + vendor/hayro/LICENSE-MIT | 7 + vendor/hayro/README.md | 46 + vendor/hayro/SOURCE.json | 74 + vendor/hayro/examples/render.rs | 149 + vendor/hayro/src/lib.rs | 669 ++ vendor/hayro/src/renderer.rs | 1879 ++++ 210 files changed, 65038 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/pdf.yml create mode 100644 crates/pdf/read-only-contract/Cargo.lock create mode 100644 crates/pdf/read-only-contract/Cargo.toml create mode 100644 crates/pdf/read-only-contract/README.md create mode 100644 crates/pdf/read-only-contract/src/lib.rs create mode 100644 crates/pdf/src/raster.rs create mode 100644 crates/pdf/src/read.rs create mode 100644 crates/pdf/tests/contracts.rs create mode 100644 crates/pdf/tests/interpretation.rs create mode 100644 crates/pdf/tests/upstream_raster.rs create mode 100644 crates/pdf/tests/upstream_reading.rs create mode 100644 licenses/pdf-backend/hayro-cmap/LICENSE-APACHE create mode 100644 licenses/pdf-backend/hayro-cmap/LICENSE-MIT create mode 100644 licenses/pdf-backend/hayro-cmap/LICENSE.txt create mode 100644 licenses/pdf-backend/hayro-interpret/LICENSE-APACHE create mode 100644 licenses/pdf-backend/hayro-interpret/LICENSE-MIT create mode 100644 licenses/pdf-backend/hayro-interpret/assets/CGATS_LICENSE.txt create mode 100644 licenses/pdf-backend/hayro-interpret/assets/LICENSE_FOXIT create mode 100644 licenses/pdf-backend/hayro-jbig2/LICENSE-APACHE create mode 100644 licenses/pdf-backend/hayro-jbig2/LICENSE-MIT create mode 100644 licenses/pdf-backend/hayro-syntax/LICENSE-APACHE create mode 100644 licenses/pdf-backend/hayro-syntax/LICENSE-MIT create mode 100644 licenses/pdf-backend/hayro/LICENSE-APACHE create mode 100644 licenses/pdf-backend/hayro/LICENSE-MIT create mode 100644 scripts/fetch-pdf-test-fonts.py create mode 100644 scripts/test-pdf-backends.sh create mode 100644 vendor/README.craft-reader.md create mode 100644 vendor/hayro-interpret/.cargo-ok create mode 100644 vendor/hayro-interpret/.cargo_vcs_info.json create mode 100644 vendor/hayro-interpret/Cargo.lock create mode 100644 vendor/hayro-interpret/Cargo.toml create mode 100644 vendor/hayro-interpret/Cargo.toml.orig create mode 100644 vendor/hayro-interpret/LICENSE-APACHE create mode 100644 vendor/hayro-interpret/LICENSE-MIT create mode 100644 vendor/hayro-interpret/README.md create mode 100644 vendor/hayro-interpret/SOURCE.json create mode 100644 vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc create mode 100644 vendor/hayro-interpret/assets/CGATS_LICENSE.txt create mode 100644 vendor/hayro-interpret/assets/FoxitDingbats.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitFixed.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitFixedBold.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitFixedItalic.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSans.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSansBold.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSansItalic.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSerif.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSerifBold.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSerifItalic.pfb create mode 100644 vendor/hayro-interpret/assets/FoxitSymbol.pfb create mode 100644 vendor/hayro-interpret/assets/LICENSE_FOXIT create mode 100644 vendor/hayro-interpret/assets/README.md create mode 100644 vendor/hayro-interpret/examples/extract_html.rs create mode 100644 vendor/hayro-interpret/examples/extract_images.rs create mode 100644 vendor/hayro-interpret/src/cache.rs create mode 100644 vendor/hayro-interpret/src/color.rs create mode 100644 vendor/hayro-interpret/src/context.rs create mode 100644 vendor/hayro-interpret/src/convert.rs create mode 100644 vendor/hayro-interpret/src/device.rs create mode 100644 vendor/hayro-interpret/src/encode.rs create mode 100644 vendor/hayro-interpret/src/font/blob.rs create mode 100644 vendor/hayro-interpret/src/font/cid.rs create mode 100644 vendor/hayro-interpret/src/font/generated/glyph_names.rs create mode 100644 vendor/hayro-interpret/src/font/generated/mac_expert.rs create mode 100644 vendor/hayro-interpret/src/font/generated/mac_os_roman.rs create mode 100644 vendor/hayro-interpret/src/font/generated/mac_roman.rs create mode 100644 vendor/hayro-interpret/src/font/generated/metrics.rs create mode 100644 vendor/hayro-interpret/src/font/generated/mod.rs create mode 100644 vendor/hayro-interpret/src/font/generated/standard.rs create mode 100644 vendor/hayro-interpret/src/font/generated/symbol.rs create mode 100644 vendor/hayro-interpret/src/font/generated/win_ansi.rs create mode 100644 vendor/hayro-interpret/src/font/generated/zapf_dings.rs create mode 100644 vendor/hayro-interpret/src/font/glyph_simulator.rs create mode 100644 vendor/hayro-interpret/src/font/mod.rs create mode 100644 vendor/hayro-interpret/src/font/outline.rs create mode 100644 vendor/hayro-interpret/src/font/standard_font.rs create mode 100644 vendor/hayro-interpret/src/font/true_type.rs create mode 100644 vendor/hayro-interpret/src/font/type1.rs create mode 100644 vendor/hayro-interpret/src/font/type3.rs create mode 100644 vendor/hayro-interpret/src/function/mod.rs create mode 100644 vendor/hayro-interpret/src/function/type0.rs create mode 100644 vendor/hayro-interpret/src/function/type2.rs create mode 100644 vendor/hayro-interpret/src/function/type3.rs create mode 100644 vendor/hayro-interpret/src/function/type4.rs create mode 100644 vendor/hayro-interpret/src/gradient.rs create mode 100644 vendor/hayro-interpret/src/interpret/mod.rs create mode 100644 vendor/hayro-interpret/src/interpret/path.rs create mode 100644 vendor/hayro-interpret/src/interpret/state.rs create mode 100644 vendor/hayro-interpret/src/interpret/text.rs create mode 100644 vendor/hayro-interpret/src/lib.rs create mode 100644 vendor/hayro-interpret/src/log.rs create mode 100644 vendor/hayro-interpret/src/ocg.rs create mode 100644 vendor/hayro-interpret/src/pattern.rs create mode 100644 vendor/hayro-interpret/src/shading.rs create mode 100644 vendor/hayro-interpret/src/soft_mask.rs create mode 100644 vendor/hayro-interpret/src/types.rs create mode 100644 vendor/hayro-interpret/src/util.rs create mode 100644 vendor/hayro-interpret/src/x_object.rs create mode 100644 vendor/hayro-jbig2/.gitignore create mode 100644 vendor/hayro-jbig2/Cargo.lock create mode 100644 vendor/hayro-jbig2/Cargo.toml create mode 100644 vendor/hayro-jbig2/Cargo.toml.orig create mode 100644 vendor/hayro-jbig2/LICENSE-APACHE create mode 100644 vendor/hayro-jbig2/LICENSE-MIT create mode 100644 vendor/hayro-jbig2/README.md create mode 100644 vendor/hayro-jbig2/SOURCE.json create mode 100644 vendor/hayro-jbig2/examples/jbig2png.rs create mode 100644 vendor/hayro-jbig2/src/arithmetic_decoder.rs create mode 100644 vendor/hayro-jbig2/src/bitmap.rs create mode 100644 vendor/hayro-jbig2/src/decode/generic.rs create mode 100644 vendor/hayro-jbig2/src/decode/generic_refinement.rs create mode 100644 vendor/hayro-jbig2/src/decode/halftone.rs create mode 100644 vendor/hayro-jbig2/src/decode/mod.rs create mode 100644 vendor/hayro-jbig2/src/decode/pattern.rs create mode 100644 vendor/hayro-jbig2/src/decode/symbol.rs create mode 100644 vendor/hayro-jbig2/src/decode/text.rs create mode 100644 vendor/hayro-jbig2/src/error.rs create mode 100644 vendor/hayro-jbig2/src/file.rs create mode 100644 vendor/hayro-jbig2/src/gray_scale.rs create mode 100644 vendor/hayro-jbig2/src/huffman_table.rs create mode 100644 vendor/hayro-jbig2/src/huffman_tables_generated.rs create mode 100644 vendor/hayro-jbig2/src/integer_decoder.rs create mode 100644 vendor/hayro-jbig2/src/integration.rs create mode 100644 vendor/hayro-jbig2/src/lazy.rs create mode 100644 vendor/hayro-jbig2/src/lib.rs create mode 100644 vendor/hayro-jbig2/src/page_info.rs create mode 100644 vendor/hayro-jbig2/src/reader.rs create mode 100644 vendor/hayro-jbig2/src/segment.rs create mode 100644 vendor/hayro-jbig2/src/simd.rs create mode 100644 vendor/hayro-jbig2/src/symbol_id_decoder.rs create mode 100644 vendor/hayro-syntax/Cargo.lock create mode 100644 vendor/hayro-syntax/Cargo.toml create mode 100644 vendor/hayro-syntax/Cargo.toml.orig create mode 100644 vendor/hayro-syntax/LICENSE-APACHE create mode 100644 vendor/hayro-syntax/LICENSE-MIT create mode 100644 vendor/hayro-syntax/README.md create mode 100644 vendor/hayro-syntax/SOURCE.json create mode 100644 vendor/hayro-syntax/examples/iterate.rs create mode 100644 vendor/hayro-syntax/src/bit_reader.rs create mode 100644 vendor/hayro-syntax/src/byte_reader.rs create mode 100644 vendor/hayro-syntax/src/content/gen_ops.py create mode 100644 vendor/hayro-syntax/src/content/mod.rs create mode 100644 vendor/hayro-syntax/src/content/ops.rs create mode 100644 vendor/hayro-syntax/src/content/ops_generated.rs create mode 100644 vendor/hayro-syntax/src/crypto/aes.rs create mode 100644 vendor/hayro-syntax/src/crypto/md5.rs create mode 100644 vendor/hayro-syntax/src/crypto/mod.rs create mode 100644 vendor/hayro-syntax/src/crypto/rc4.rs create mode 100644 vendor/hayro-syntax/src/crypto/sha256.rs create mode 100644 vendor/hayro-syntax/src/crypto/sha384.rs create mode 100644 vendor/hayro-syntax/src/crypto/sha512.rs create mode 100644 vendor/hayro-syntax/src/data.rs create mode 100644 vendor/hayro-syntax/src/decode_limits_tests.rs create mode 100644 vendor/hayro-syntax/src/filter/ascii_85.rs create mode 100644 vendor/hayro-syntax/src/filter/ascii_hex.rs create mode 100644 vendor/hayro-syntax/src/filter/ccitt.rs create mode 100644 vendor/hayro-syntax/src/filter/dct.rs create mode 100644 vendor/hayro-syntax/src/filter/jbig2.rs create mode 100644 vendor/hayro-syntax/src/filter/jpx.rs create mode 100644 vendor/hayro-syntax/src/filter/lzw_flate.rs create mode 100644 vendor/hayro-syntax/src/filter/mod.rs create mode 100644 vendor/hayro-syntax/src/filter/run_length.rs create mode 100644 vendor/hayro-syntax/src/lib.rs create mode 100644 vendor/hayro-syntax/src/log.rs create mode 100644 vendor/hayro-syntax/src/math.rs create mode 100644 vendor/hayro-syntax/src/metadata.rs create mode 100644 vendor/hayro-syntax/src/object/array.rs create mode 100644 vendor/hayro-syntax/src/object/bool.rs create mode 100644 vendor/hayro-syntax/src/object/date.rs create mode 100644 vendor/hayro-syntax/src/object/dict.rs create mode 100644 vendor/hayro-syntax/src/object/indirect.rs create mode 100644 vendor/hayro-syntax/src/object/mod.rs create mode 100644 vendor/hayro-syntax/src/object/name.rs create mode 100644 vendor/hayro-syntax/src/object/null.rs create mode 100644 vendor/hayro-syntax/src/object/number.rs create mode 100644 vendor/hayro-syntax/src/object/rect.rs create mode 100644 vendor/hayro-syntax/src/object/ref.rs create mode 100644 vendor/hayro-syntax/src/object/stream.rs create mode 100644 vendor/hayro-syntax/src/object/string.rs create mode 100644 vendor/hayro-syntax/src/object/tuple.rs create mode 100644 vendor/hayro-syntax/src/page.rs create mode 100644 vendor/hayro-syntax/src/pdf.rs create mode 100644 vendor/hayro-syntax/src/reader.rs create mode 100644 vendor/hayro-syntax/src/sync.rs create mode 100644 vendor/hayro-syntax/src/transform.rs create mode 100644 vendor/hayro-syntax/src/trivia.rs create mode 100644 vendor/hayro-syntax/src/util.rs create mode 100644 vendor/hayro-syntax/src/xref.rs create mode 100644 vendor/hayro/.cargo-ok create mode 100644 vendor/hayro/.cargo_vcs_info.json create mode 100644 vendor/hayro/Cargo.lock create mode 100644 vendor/hayro/Cargo.toml create mode 100644 vendor/hayro/Cargo.toml.orig create mode 100644 vendor/hayro/LICENSE-APACHE create mode 100644 vendor/hayro/LICENSE-MIT create mode 100644 vendor/hayro/README.md create mode 100644 vendor/hayro/SOURCE.json create mode 100644 vendor/hayro/examples/render.rs create mode 100644 vendor/hayro/src/lib.rs create mode 100644 vendor/hayro/src/renderer.rs diff --git a/.github/workflows/pdf.yml b/.github/workflows/pdf.yml new file mode 100644 index 00000000..2880b831 --- /dev/null +++ b/.github/workflows/pdf.yml @@ -0,0 +1,86 @@ +name: Shared PDF + +on: + push: + branches: [main] + paths: ["crates/pdf/**", "vendor/hayro*/**", "vendor/krilla/**", "scripts/test-pdf-backends.sh", "scripts/fetch-pdf-test-fonts.py", "Cargo.toml", "Cargo.lock", ".github/workflows/pdf.yml"] + pull_request: + paths: ["crates/pdf/**", "vendor/hayro*/**", "vendor/krilla/**", "scripts/test-pdf-backends.sh", "scripts/fetch-pdf-test-fonts.py", "Cargo.toml", "Cargo.lock", ".github/workflows/pdf.yml"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: shared-pdf-${{ github.ref }} + cancel-in-progress: true + +jobs: + native: + name: PDF (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: --cfg craft_pdf_unicode_fixtures + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + key: craft-pdf + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Provision licensed PDF test fonts + shell: bash + run: | + python scripts/fetch-pdf-test-fonts.py "$RUNNER_TEMP/craft-pdf-test-fonts" + echo "CRAFT_PDF_TEST_FONT_DIR=$RUNNER_TEMP/craft-pdf-test-fonts" >> "$GITHUB_ENV" + - run: cargo fmt --check + - run: cargo clippy -p craft-pdf --all-targets --all-features --locked -- -D warnings + - run: cargo test -p craft-pdf --all-features --locked + - run: cargo test --manifest-path crates/pdf/read-only-contract/Cargo.toml --locked + backends: + name: PDF vendored contracts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: craft-pdf-backends + - run: bash scripts/test-pdf-backends.sh + wasm: + name: PDF WASM runtime + runs-on: ubuntu-latest + env: + RUSTFLAGS: --cfg craft_pdf_unicode_fixtures + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER: wasm-bindgen-test-runner + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + with: + key: craft-pdf-wasm + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Provision licensed PDF test fonts + shell: bash + run: | + python scripts/fetch-pdf-test-fonts.py "$RUNNER_TEMP/craft-pdf-test-fonts" + echo "CRAFT_PDF_TEST_FONT_DIR=$RUNNER_TEMP/craft-pdf-test-fonts" >> "$GITHUB_ENV" + - name: Install the matching WASM test runner + shell: bash + run: | + runner_version=$(python3 -c 'import tomllib; d=tomllib.load(open("Cargo.lock", "rb")); print(next(p["version"] for p in d["package"] if p["name"] == "wasm-bindgen"))') + cargo install wasm-bindgen-cli --version "$runner_version" --locked + - run: cargo test -p craft-pdf --all-features --locked --target wasm32-unknown-unknown + - run: cargo test --manifest-path crates/pdf/read-only-contract/Cargo.toml --locked --target wasm32-unknown-unknown diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 7a6e0537..34cd2ee2 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -17,7 +17,7 @@ Original assets are covered by the repository licence. Krilla 0.8.2 bundles the following Compact ICC Profiles by Clinton Ingram, under CC0-1.0. The immutable source revision is `3ffdf0588cf98050aad6edba51ca70162e1fb5b5`, recorded by the registry package's `.cargo_vcs_info.json`. These are the same dependency profiles already -used by the apps' Krilla exporters. The local backend snapshot retains these four profiles; no font binary is copied into this repository. +used by the apps' Krilla exporters. The local backend snapshot retains these four profiles; the optional writer adds no font binaries. The profile licence is retained in `licenses/pdf-writer/LICENSE-ICC.txt`. | Crate / file | Author | Source | Licence | SHA-256 | Use | @@ -29,3 +29,31 @@ The profile licence is retained in `licenses/pdf-writer/LICENSE-ICC.txt`. The four Krilla ICC entries above are also stored unchanged in `vendor/krilla/icc`. The vendored source patch and per-file hashes are recorded in `vendor/krilla/README.craft.md` and `vendor/krilla/SOURCE.json`. + +## Assets retained with the optional PDF reader + +The shared reader centralizes the four existing patched Hayro source trees from PdfCraft. +The standard-14 fonts, CMYK profile and generated metrics listed below remain byte-identical +in `vendor/hayro-interpret`; no application document fonts are added. CMaps remain a pinned +registry dependency. The removed upstream LAB.icc profile is not restored. Source hashes +and the exact transfer revision are recorded in each vendored crate's SOURCE.json. + +| Crate / file | Author | Source | Licence | SHA-256 | Use | +| --- | --- | --- | --- | --- | --- | +| `hayro-interpret 0.7.0 / assets/FoxitDingbats.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitDingbats.pfb | BSD-3-Clause | `845c752392b6c914fb989c75a08b7792b88f542d2499042ef2889f8c814a16ed` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixed.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitFixed.pfb | BSD-3-Clause | `b6c8fe53f134b8b6d4578cd2d544df4cee9624c4efa8d51a560fb40ea296101b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitFixedBold.pfb | BSD-3-Clause | `f1b7159702973f54fc86254ea38bcf3712b2a736eb3a0995751e9f4bb45ad603` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb | BSD-3-Clause | `8a000945843bd31add06aee63bf9fd41b7578b4f3242a4b7cd46349ad9d24d4d` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb | BSD-3-Clause | `5007faf8320fa1fcc08b8894b356ad976f60b992ba29ee5272578bbdebaf3876` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSans.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSans.pfb | BSD-3-Clause | `a02339e95d20db14cbdfc1c6baf359cd36a091d5434925c3a4040f60e1a1af9e` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSansBold.pfb | BSD-3-Clause | `7bdfa36de1d5757dcfaf8b8a4be95c6e41c40fa70dbed9d78bf979a7b9959054` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb | BSD-3-Clause | `555ce45924338e2e4b32821910221ce85da691fd59a9723c4c971a7c5627fc8b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSansItalic.pfb | BSD-3-Clause | `7751cbefeda351708dae8c4a88db043411bc39ab2ae2097685da845d4d16f307` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerif.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSerif.pfb | BSD-3-Clause | `4f57d2b9d884af8f907bf22df6019b52d86cbf6214fdbd02b1ae05472a543f35` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSerifBold.pfb | BSD-3-Clause | `0bdf4b04e964139818d51eda03d566ba999fc3ba2421b1c6c51f9dc969022e80` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb | BSD-3-Clause | `a406cac82583bf98175cb62c87ed5e95c45fbb34eece63a10b0a13e793cb2e10` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb | BSD-3-Clause | `610ae0687198045c4db5d1a6650fd5e92536631706382eb8b72e38df578d0ae9` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSymbol.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSymbol.pfb | BSD-3-Clause | `47967d055530e7357088a08403115425643ec2cdfd6201ba8af0fbd7116c1539` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/CGATS001Compat-v2-micro.icc` | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc | CC0-1.0 | `73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0` | Default CMYK colour conversion (pinned hayro-interpret) | +| `hayro-cmap 0.1.0 / assets/cmaps.brotli` | Adobe | https://crates.io/crates/hayro-cmap/0.1.0 | BSD-3-Clause | `ddf37af521d904d5646c2fc37b2b7e912dc1b56f0d210190b61f598154758e46` | Character-code mapping tables required to read CJK PDFs. Non-visual dependency data; existing embedding unchanged | +| `hayro-interpret 0.7.0 / src/font/generated/metrics.rs` | Adobe (Core 14 AFM metrics, PDF specification encodings); tables by The Hayro Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/src/font/generated/metrics.rs | MIT OR Apache-2.0 | `2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41` | Glyph widths and encodings for non-embedded standard fonts. Non-visual dependency data; existing embedding unchanged | diff --git a/Cargo.lock b/Cargo.lock index bda86020..99d20b36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,6 +601,8 @@ dependencies = [ name = "craft-pdf" version = "0.1.0" dependencies = [ + "bytemuck", + "hayro", "hayro-cmap", "hayro-interpret", "hayro-syntax", @@ -1063,7 +1065,7 @@ dependencies = [ "smallvec", "unicode-general-category", "unicode-segmentation", - "vello_cpu", + "vello_cpu 0.1.0", ] [[package]] @@ -1130,12 +1132,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" -[[package]] -name = "fearless_simd" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3296eb97d869113eaf148a6bdf01884e90b3828ad7ae2340b0f93362fc158611" - [[package]] name = "filetime" version = "0.2.29" @@ -1389,6 +1385,23 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glifo" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +dependencies = [ + "bytemuck", + "foldhash", + "hashbrown 0.17.1", + "log", + "peniko", + "png 0.18.1", + "skrifa 0.42.1", + "smallvec", + "vello_common 0.0.9", +] + [[package]] name = "glifo" version = "0.2.0" @@ -1402,7 +1415,7 @@ dependencies = [ "peniko", "skrifa 0.44.0", "smallvec", - "vello_common", + "vello_common 0.1.0", ] [[package]] @@ -1512,18 +1525,25 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hayro" +version = "0.7.1" +dependencies = [ + "bytemuck", + "hayro-interpret", + "image", + "kurbo 0.13.1", + "log", + "pic-scale", + "vello_cpu 0.0.8", +] + [[package]] name = "hayro-ccitt" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" -[[package]] -name = "hayro-ccitt" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2866f0d976e76497480b853d5e258dab6e4b89ecfe991124e08f15dfa8fd03ad" - [[package]] name = "hayro-cmap" version = "0.1.0" @@ -1537,8 +1557,6 @@ dependencies = [ [[package]] name = "hayro-interpret" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2613d0406898995042d0794c4245b2f2fba1246490c8ac593769bba6551129d" dependencies = [ "bitflags 2.13.2", "hayro-cmap", @@ -1548,19 +1566,17 @@ dependencies = [ "phf", "rustc-hash 2.1.3", "siphasher", - "skrifa 0.42.1", + "skrifa 0.47.0", "smallvec", "yoke", ] [[package]] name = "hayro-jbig2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000e025850f4592ddcaee85640db70704151f2bafd43b6f431aa5b6a2c6a35a9" +version = "0.3.0" dependencies = [ - "fearless_simd 1.1.0", - "hayro-ccitt 0.4.0", + "fearless_simd", + "hayro-ccitt", ] [[package]] @@ -1569,7 +1585,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" dependencies = [ - "fearless_simd 0.4.1", + "fearless_simd", ] [[package]] @@ -1581,16 +1597,15 @@ checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" [[package]] name = "hayro-syntax" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0edeafd70aa2db743de8ede8637d07ec87db05efe69cde371d03f1b185fcef27" dependencies = [ "flate2", - "hayro-ccitt 0.3.0", + "hayro-ccitt", "hayro-jbig2", "hayro-jpeg2000", "memchr", "rustc-hash 2.1.3", "smallvec", + "stringprep", "zune-jpeg", ] @@ -2849,6 +2864,16 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pic-scale" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb0ff645d35accaec252de081d55984a0d6c952cb1e5dc2b9afa7c78f07288" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -3216,6 +3241,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "read-fonts" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a0cf4bb6cbce4c29756270d189a5e400ac05e91192161d83a0bcb6e68492e8" +dependencies = [ + "bytemuck", + "font-types 0.12.6", + "once_cell", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3897,6 +3933,16 @@ dependencies = [ "read-fonts 0.41.0", ] +[[package]] +name = "skrifa" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "526e654b303ff4ad73a5a40b25de2b3ce3d039c5e3b6da5f89ae6e57004cb40f" +dependencies = [ + "bytemuck", + "read-fonts 0.44.0", +] + [[package]] name = "slab" version = "0.4.12" @@ -4015,6 +4061,17 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strum" version = "0.28.0" @@ -4495,6 +4552,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-bidi-mirroring" version = "0.4.0" @@ -4519,6 +4582,15 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-properties" version = "0.1.4" @@ -4601,6 +4673,40 @@ version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +[[package]] +name = "vello_common" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3361bff7f7d82c0c496b92048db83846691f0e844cc28dee92b1c824291b55ee" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown 0.17.1", + "log", + "peniko", + "png 0.18.1", + "smallvec", + "thiserror 2.0.21", +] + +[[package]] +name = "vello_common" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown 0.17.1", + "log", + "peniko", + "png 0.18.1", + "smallvec", + "thiserror 2.0.21", +] + [[package]] name = "vello_common" version = "0.1.0" @@ -4608,7 +4714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2" dependencies = [ "bytemuck", - "fearless_simd 0.4.1", + "fearless_simd", "guillotiere", "log", "peniko", @@ -4616,6 +4722,19 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "vello_cpu" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8ded630e8316bb94a55881256506d1f3b9947b5f66db8a7d32ca7ba02decd0" +dependencies = [ + "bytemuck", + "glifo 0.1.1", + "hashbrown 0.17.1", + "png 0.18.1", + "vello_common 0.0.8", +] + [[package]] name = "vello_cpu" version = "0.1.0" @@ -4623,9 +4742,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" dependencies = [ "bytemuck", - "glifo", + "glifo 0.2.0", "hashbrown 0.17.1", - "vello_common", + "vello_common 0.1.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 665729b0..84f425c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "3" members = ["crates/*"] -exclude = ["vendor/krilla"] +exclude = ["vendor/krilla", "vendor/hayro", "vendor/hayro-interpret", "vendor/hayro-syntax", "vendor/hayro-jbig2"] [workspace.package] version = "0.1.0" @@ -14,6 +14,10 @@ repository = "https://github.com/storytold/craft-libs" # internal: add each crate here as `craft- = { path = "crates/" }` craft-pdf = { path = "crates/pdf", version = "=0.1.0" } krilla = "=0.8.2" +hayro = { version = "=0.7.1", default-features = false } +hayro-interpret = { version = "=0.7.0", default-features = false, features = ["embed-fonts", "embed-cmaps"] } +hayro-syntax = { version = "=0.7.2", default-features = false, features = ["std"] } +bytemuck = "1" thiserror = "2" craft-atomic = { path = "crates/atomic" } craft-cms = { path = "crates/cms" } @@ -34,3 +38,8 @@ todo = "warn" [patch.crates-io] krilla = { path = "vendor/krilla" } + +hayro = { path = "vendor/hayro" } +hayro-interpret = { path = "vendor/hayro-interpret" } +hayro-syntax = { path = "vendor/hayro-syntax" } +hayro-jbig2 = { path = "vendor/hayro-jbig2" } diff --git a/NOTICE b/NOTICE index 353bde3d..9467671f 100644 --- a/NOTICE +++ b/NOTICE @@ -89,7 +89,20 @@ Upstream licence texts are retained in licenses/pdf-writer. The optional writer embeds Compact ICC Profiles by Clinton Ingram (https://github.com/saucecontrol/Compact-ICC-Profiles), under CC0-1.0. See licenses/pdf-writer/LICENSE-ICC.txt and ATTRIBUTION.md for sources and hashes. -No font files are copied into this repository. The four existing writer ICC profiles are retained under vendor/krilla/icc. +The writer adds no font files. Its four existing ICC profiles are retained under vendor/krilla/icc. Krilla is vendored with a native unwind-cleanup patch; see vendor/krilla/README.craft.md and vendor/krilla/SOURCE.json. Its upstream MIT/Apache-2.0 licences and CC0 profiles are unchanged. + +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). +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). +Their licences and per-file hashes are retained in vendor/hayro-interpret/assets, +licenses/pdf-backend and ATTRIBUTION.md. These are existing reader dependency assets; +applications retain their own document-font selection and licensing policy. diff --git a/README.md b/README.md index cd1da872..a5db4f30 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ craft- = { git = "https://github.com/storytold/craft-libs", rev = " Vec { + let mut bytes = b"%PDF-1.7\n".to_vec(); + let mut offsets = vec![0]; + let mut content = format!("<< /Length {} /Filter /FlateDecode >>\nstream\n", FLATE_CONTENT.len()).into_bytes(); + content.extend_from_slice(FLATE_CONTENT); + content.extend_from_slice(b"\nendstream"); + let objects: [&[u8]; 4] = [ + b"<< /Type /Catalog /Pages 2 0 R >>", + b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>", + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 80] /Contents 4 0 R >>", + &content, + ]; + for (index, object) in objects.iter().enumerate() { + offsets.push(bytes.len()); + bytes.extend_from_slice(format!("{} 0 obj\n", index + 1).as_bytes()); + bytes.extend_from_slice(object); + bytes.extend_from_slice(b"\nendobj\n"); + } + let xref = bytes.len(); + bytes.extend_from_slice(b"xref\n0 5\n0000000000 65535 f \n"); + for offset in offsets.iter().skip(1) { + bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes()); + } + bytes.extend_from_slice(format!("trailer\n<< /Root 1 0 R /Size 5 >>\nstartxref\n{xref}\n%%EOF\n").as_bytes()); + bytes + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn read_only_opens_and_decodes_flate_without_image_features() { + let bytes = flate_pdf(); + let pdf = open_slice(&bytes, None).unwrap(); + assert_eq!(pdf.pages().len(), 1); + let page = &pdf.pages()[0]; + let dimensions = page.media_box(); + assert_eq!((dimensions.width(), dimensions.height()), (120.0, 80.0)); + let expected = b"q Q\n".repeat(128); + assert_eq!(page.page_stream().unwrap(), expected.as_slice()); + + // Also use the shipping incremental path and drain once beyond the expected end so + // completion is actually observed, rather than inferred from a valid prefix. + let mut reader = page.content_reader(); + let mut decoded = Vec::new(); + while reader.read_into(&mut decoded, 19) != 0 { + assert!(decoded.len() <= expected.len()); + } + assert_eq!(decoded, expected); + assert!(!reader.truncated()); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn incremental_flate_keeps_its_separate_content_budget() { + let bytes = flate_pdf(); + let expected = b"q Q\n".repeat(128); + for limit in [0, 32] { + let options = OpenOptions { decode_limits: DecodeLimits::default().with_max_decoded_stream(limit), ..Default::default() }; + let pdf = open_slice_with_options(&bytes, None, &options).unwrap(); + let page = &pdf.pages()[0]; + assert_eq!(page.page_stream().unwrap().len(), limit); + let mut reader = page.content_reader(); + let mut decoded = Vec::new(); + while reader.read_into(&mut decoded, 19) != 0 { + assert!(decoded.len() <= expected.len()); + } + assert_eq!(decoded, expected); + assert!(!reader.truncated(), "the separate streaming budget was not exhausted"); + } + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn document_stream_limits_are_lowered_without_affecting_another_open() { + let bytes = Arc::new(flate_pdf()); + let limits = DecodeLimits::default().with_max_decoded_stream(32); + let mut options = OpenOptions { decode_limits: limits, ..Default::default() }; + let low = open_with_options(bytes.clone(), None, &options).unwrap(); + let normal = open(bytes.clone(), None).unwrap(); + // The options value is copied into the document, not consulted later. + options.decode_limits = limits.with_max_decoded_stream(0); + let zero = open_slice_with_options(bytes.as_slice(), None, &options).unwrap(); + let expected = b"q Q\n".repeat(128); + let low_stream = low.xref().get::>(ObjectIdentifier::new(4, 0)).unwrap(); + assert!(low_stream.raw_data().len() < limits.max_decoded_stream()); + assert_eq!(low_stream.decoded().unwrap().as_ref(), &expected[..32]); + assert_eq!(low_stream.decoded_within(7).unwrap().as_ref(), &expected[..7]); + assert_eq!(low.pages()[0].page_stream().unwrap(), &expected[..32]); + assert_eq!(normal.pages()[0].page_stream().unwrap(), expected.as_slice()); + assert_eq!(zero.pages()[0].page_stream().unwrap(), b""); + assert_eq!(low.decode_limits(), limits); + assert_eq!(normal.decode_limits(), DecodeLimits::default()); + assert_eq!(limits.with_max_decoded_stream(usize::MAX), limits); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn caller_input_limit_applies_to_borrowed_and_shared_open() { + let bytes = flate_pdf(); + let limit = bytes.len() - 1; + let options = OpenOptions { max_input_bytes: limit, ..Default::default() }; + assert!(matches!(open_slice_with_options(&bytes, None, &options), Err(OpenError::InputLimit { limit: actual }) if actual == limit)); + assert!(matches!(open_with_options(Arc::new(bytes), None, &options), Err(OpenError::InputLimit { limit: actual }) if actual == limit)); + } +} diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 573719af..e540157a 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -1,8 +1,19 @@ -//! Shared optional PDF writing for Craft applications. +//! Shared optional PDF reading, rendering and writing for Craft applications. //! -//! Applications retain layout, shaping, font policy, tagging and export policy. -//! Reading and rendering are separate capabilities; authoring does not enable them. +//! Applications retain document models, layout, shaping, font policy and scheduling. +//! Advanced backend re-exports are coupled to the pinned backend, not a universal document model. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +#[cfg(feature = "interpret")] +pub use hayro_interpret as interpret; +#[cfg(feature = "read")] +pub use hayro_syntax as syntax; + +#[cfg(feature = "read")] +mod read; +#[cfg(feature = "read")] +pub use read::*; +#[cfg(feature = "raster")] +pub mod raster; #[cfg(feature = "write")] pub mod write; diff --git a/crates/pdf/src/raster.rs b/crates/pdf/src/raster.rs new file mode 100644 index 00000000..bf1fc931 --- /dev/null +++ b/crates/pdf/src/raster.rs @@ -0,0 +1,178 @@ +//! Bounded, premultiplied RGBA8 raster output, with explicit viewport and background. + +use crate::{ + interpret::{InterpreterSettings, InterpreterWarning}, + syntax::page::Page, +}; +pub use hayro::{ColorReplacement, RenderCache}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::{ + Arc, + atomic::{AtomicU8, Ordering}, +}; + +/// Hard bounds on output allocation. These do not bound every backend scratch allocation. +pub const MAX_SIDE: u32 = 8192; +pub const MAX_PIXELS: u64 = 64_000_000; + +#[derive(Clone, Copy, Debug)] +pub struct Options { + pub scale: [f32; 2], + /// Explicit device-pixel viewport. When absent the scaled page size is floored, with + /// at least one pixel per side. Callers that need edge coverage can supply a rounded-up size. + pub size: Option<[u32; 2]>, + /// Top-left viewport origin in device pixels of the scaled page. + pub offset: [f32; 2], + /// Straight-alpha sRGB bytes; output pixels are always premultiplied. + pub background: [u8; 4], + /// Generic solid-colour mapping; reading presets and print bypass policy belong to callers. + pub color_replacement: Option, +} + +impl Default for Options { + fn default() -> Self { + Self { scale: [1.0, 1.0], size: None, offset: [0.0, 0.0], background: [0; 4], color_replacement: None } + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Diagnostics { + pub image_decode_failed: bool, + /// Reserved by the backend, which currently does not emit this warning. + pub unsupported_font: bool, + /// The backend reported omitted, failed, or budget-limited content. + pub content_truncated: bool, +} + +impl Diagnostics { + pub fn is_empty(self) -> bool { + !self.image_decode_failed && !self.unsupported_font && !self.content_truncated + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] +pub enum Error { + #[error("PDF page has an empty or non-finite crop box")] + InvalidPage, + #[error("PDF raster scale or viewport offset is invalid")] + InvalidOptions, + #[error("PDF raster exceeds the output size limit")] + OutputLimit, + #[error("PDF raster buffer length does not match its dimensions")] + BufferLength, + #[error("PDF raster buffer could not be allocated")] + Allocation, + #[error("PDF rendering was cancelled")] + Cancelled, + /// After this error callers must discard the document parser and render cache. + #[error("PDF renderer failed internally")] + BackendPanic, +} + +/// Validate geometry before converting sizes or allocating. A zero-size output is rejected. +pub fn dimensions(page: &Page<'_>, options: &Options) -> Result<[u32; 2], Error> { + let (w, h) = page.render_dimensions(); + if !w.is_finite() || !h.is_finite() || w <= 0.0 || h <= 0.0 { + return Err(Error::InvalidPage); + } + if options.scale.iter().any(|s| !s.is_finite() || *s <= 0.0) || options.offset.iter().any(|s| !s.is_finite()) { + return Err(Error::InvalidOptions); + } + let size = if let Some(size) = options.size { + size + } else { + let scaled = [w * options.scale[0], h * options.scale[1]]; + if scaled.iter().any(|s| !s.is_finite() || *s > MAX_SIDE as f32) { + return Err(Error::OutputLimit); + } + [scaled[0].floor().max(1.0) as u32, scaled[1].floor().max(1.0) as u32] + }; + if size.iter().any(|s| *s == 0 || *s > MAX_SIDE) || u64::from(size[0]) * u64::from(size[1]) > MAX_PIXELS { + return Err(Error::OutputLimit); + } + Ok(size) +} + +/// Render directly into a caller-owned buffer. The buffer must be discarded after any error. +/// Settings preserve caller fonts, annotation policy, OCG identities, cancellation and warning +/// callbacks. Diagnostics report only warnings the backend actually emits, not all fidelity loss. +pub fn render_into<'a>( + page: &'a Page<'a>, + cache: &RenderCache<'a>, + settings: &InterpreterSettings, + options: &Options, + buffer: &mut [u8], +) -> Result { + let [width, height] = dimensions(page, options)?; + let expected = u64::from(width) * u64::from(height) * 4; + if u64::try_from(buffer.len()).ok() != Some(expected) { + return Err(Error::BufferLength); + } + let cancelled = || settings.cancelled.as_ref().is_some_and(|c| c.load(Ordering::Relaxed)); + if cancelled() { + return Err(Error::Cancelled); + } + let warnings = Arc::new(AtomicU8::new(0)); + let sink = warnings.clone(); + let original = settings.warning_sink.clone(); + let settings = InterpreterSettings { + warning_sink: Arc::new(move |warning| { + let bit = match warning { + InterpreterWarning::ImageDecodeFailure => 1, + InterpreterWarning::UnsupportedFont => 2, + InterpreterWarning::ContentTruncated => 4, + }; + sink.fetch_or(bit, Ordering::Relaxed); + original(warning); + }), + ..settings.clone() + }; + let rs = hayro::RenderSettings { + color_replacement: options.color_replacement, + x_scale: options.scale[0], + y_scale: options.scale[1], + width: Some(width as u16), + height: Some(height as u16), + x_offset: options.offset[0], + y_offset: options.offset[1], + bg_color: hayro::vello_cpu::color::AlphaColor::from_rgba8( + options.background[0], + options.background[1], + options.background[2], + options.background[3], + ), + }; + catch_unwind(AssertUnwindSafe(|| hayro::render_into(page, cache, &settings, &rs, buffer))).map_err(|_| Error::BackendPanic)?; + if cancelled() { + return Err(Error::Cancelled); + } + let bits = warnings.load(Ordering::Relaxed); + Ok(Diagnostics { image_decode_failed: bits & 1 != 0, unsupported_font: bits & 2 != 0, content_truncated: bits & 4 != 0 }) +} + +/// Pixel-word aligned premultiplied RGBA8 output; no GUI or image encoding dependency. +#[derive(Debug)] +pub struct Raster { + pub width: u32, + pub height: u32, + words: Vec, + pub diagnostics: Diagnostics, +} +impl Raster { + pub fn bytes(&self) -> &[u8] { + bytemuck::cast_slice(&self.words) + } + pub fn into_words(self) -> Vec { + self.words + } +} + +pub fn render<'a>(page: &'a Page<'a>, cache: &RenderCache<'a>, settings: &InterpreterSettings, options: &Options) -> Result { + let [width, height] = dimensions(page, options)?; + let count = usize::try_from(u64::from(width) * u64::from(height)).map_err(|_| Error::OutputLimit)?; + let mut words = Vec::new(); + words.try_reserve_exact(count).map_err(|_| Error::Allocation)?; + words.resize(count, 0u32); + let diagnostics = render_into(page, cache, settings, options, bytemuck::cast_slice_mut(&mut words))?; + Ok(Raster { width, height, words, diagnostics }) +} diff --git a/crates/pdf/src/read.rs b/crates/pdf/src/read.rs new file mode 100644 index 00000000..509f96bd --- /dev/null +++ b/crates/pdf/src/read.rs @@ -0,0 +1,105 @@ +//! Bounded opening and native interpreter guards. App policy remains with callers. +#[cfg(feature = "interpret")] +use crate::interpret; +use crate::syntax; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; + +/// Maximum input accepted by the default open operation (512 MiB). +pub const MAX_INPUT_BYTES: usize = 512 * 1024 * 1024; + +#[derive(Debug, thiserror::Error)] +pub enum OpenError { + #[error("PDF exceeds the {limit}-byte input limit")] + InputLimit { limit: usize }, + #[error("PDF input could not be allocated")] + Allocation, + #[error("PDF parser rejected the document: {0:?}")] + Load(syntax::LoadPdfError), + #[error("PDF parser failed internally")] + BackendPanic, +} + +/// Check the input bound before an application allocates or starts another PDF reader. +pub fn check_input_size(size: usize) -> Result<(), OpenError> { + check_input_size_with(size, &OpenOptions::default()) +} + +/// Caller policy for accepted input and ordinary stream decoding, fixed for one document. +/// These limits are not a bound on total parser or process memory. +#[derive(Clone, Copy, Debug)] +pub struct OpenOptions { + /// Largest file accepted before copying or parsing. Callers preserve their own file policy. + pub max_input_bytes: usize, + /// Ordinary-stream limits, including xref streams and repair/fallback paths. + /// Structural repair or objects omitted by these limits have no separate warning; + /// a successful open and empty rendering diagnostics do not certify completeness. + /// Image-pixel and streaming page-content budgets remain separate. + /// A bare Flate page uses the streaming budget during interpretation/rasterization; + /// lowering this ordinary limit only bounds its materialized `page_stream` path. + pub decode_limits: syntax::DecodeLimits, +} + +impl Default for OpenOptions { + fn default() -> Self { + Self { max_input_bytes: MAX_INPUT_BYTES, decode_limits: syntax::DecodeLimits::default() } + } +} + +/// Check a caller's input policy before allocation or invoking another reader. +pub fn check_input_size_with(size: usize, options: &OpenOptions) -> Result<(), OpenError> { + if size > options.max_input_bytes { + return Err(OpenError::InputLimit { limit: options.max_input_bytes }); + } + Ok(()) +} + +/// Open a PDF without copying its shared input. Password errors retain the backend's type +/// so applications can distinguish a missing/wrong password and implement owner-password +/// compatibility. Parsing is bounded by the backend's own structural limits too. +/// +/// Native unwinding panics become errors. WASM aborts and process-wide allocation failures +/// cannot be caught; these guards do not replace input and allocation limits. +pub fn open(bytes: Arc>, password: Option<&str>) -> Result { + open_with_options(bytes, password, &OpenOptions::default()) +} + +/// Open shared input with explicit immutable caller limits. +pub fn open_with_options(bytes: Arc>, password: Option<&str>, options: &OpenOptions) -> Result { + check_input_size_with(bytes.len(), options)?; + catch_unwind(AssertUnwindSafe(|| syntax::Pdf::new_with_password_and_limits(bytes, password.unwrap_or(""), options.decode_limits))) + .map_err(|_| OpenError::BackendPanic)? + .map_err(OpenError::Load) +} + +/// Copy a borrowed input only after checking its size and reserving its storage. +pub fn open_slice(bytes: &[u8], password: Option<&str>) -> Result { + open_slice_with_options(bytes, password, &OpenOptions::default()) +} + +/// Copy borrowed input after checking caller limits and reserving storage fallibly. +pub fn open_slice_with_options(bytes: &[u8], password: Option<&str>, options: &OpenOptions) -> Result { + check_input_size_with(bytes.len(), options)?; + let mut owned = Vec::new(); + owned.try_reserve_exact(bytes.len()).map_err(|_| OpenError::Allocation)?; + owned.extend_from_slice(bytes); + open_with_options(Arc::new(owned), password, options) +} + +/// A guarded page interpreter entry point for editable import and text devices. +/// After an error the caller must discard the partially populated device and its cache. +/// This catches native unwinding panics, not WASM aborts. +#[cfg(feature = "interpret")] +pub fn interpret_page<'a>( + page: &'a syntax::page::Page<'a>, + context: &mut interpret::Context<'a>, + device: &mut impl interpret::Device<'a>, +) -> Result<(), ExecutionError> { + catch_unwind(AssertUnwindSafe(|| interpret::interpret_page(page, context, device))).map_err(|_| ExecutionError::BackendPanic) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] +pub enum ExecutionError { + #[error("PDF page interpretation failed internally")] + BackendPanic, +} diff --git a/crates/pdf/tests/contracts.rs b/crates/pdf/tests/contracts.rs new file mode 100644 index 00000000..d0d0f462 --- /dev/null +++ b/crates/pdf/tests/contracts.rs @@ -0,0 +1,363 @@ +#![cfg(feature = "raster")] +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +use craft_pdf::{ + interpret::InterpreterSettings, + raster::{self, Error, Options, RenderCache}, +}; +use std::sync::{ + Arc, + atomic::{AtomicBool, AtomicUsize, Ordering}, +}; + +fn pdf(content: &str, extra: &str) -> Arc> { + let objects = [ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(), + format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] /Contents 4 0 R {extra} >>"), + format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()), + ]; + assemble(&objects) +} + +fn assemble(objects: &[String]) -> Arc> { + let mut bytes = b"%PDF-1.7\n".to_vec(); + let mut offsets = vec![0]; + for (i, body) in objects.iter().enumerate() { + offsets.push(bytes.len()); + bytes.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", i + 1).as_bytes()); + } + let start = bytes.len(); + bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", offsets.len()).as_bytes()); + for offset in offsets.iter().skip(1) { + bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes()); + } + bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{start}\n%%EOF\n", offsets.len()).as_bytes()); + Arc::new(bytes) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn transparent_and_white_backgrounds_are_explicit() { + let doc = craft_pdf::open(pdf("0 0 1 rg 0 0 20 20 re f", ""), None).unwrap(); + let page = &doc.pages()[0]; + let settings = InterpreterSettings::default(); + let a = raster::render(page, &RenderCache::new(), &settings, &Options::default()).unwrap(); + assert_eq!((a.width, a.height), (40, 20)); + assert_eq!(&a.bytes()[0..4], &[0, 0, 255, 255]); + assert_eq!(&a.bytes()[120..124], &[0, 0, 0, 0]); + let b = raster::render(page, &RenderCache::new(), &settings, &Options { background: [255; 4], ..Default::default() }).unwrap(); + assert_eq!(&b.bytes()[120..124], &[255; 4]); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn cropped_rotated_geometry_and_tile_match_full_raster() { + let doc = craft_pdf::open(pdf("1 0 0 rg 0 0 40 20 re f", "/CropBox [10 0 40 20] /Rotate 90"), None).unwrap(); + let page = &doc.pages()[0]; + assert_eq!(raster::dimensions(page, &Options::default()).unwrap(), [20, 30]); + let full = raster::render(page, &RenderCache::new(), &InterpreterSettings::default(), &Options::default()).unwrap(); + let tile = raster::render( + page, + &RenderCache::new(), + &InterpreterSettings::default(), + &Options { size: Some([5, 5]), offset: [2.0, 3.0], ..Default::default() }, + ) + .unwrap(); + for row in 0..5 { + assert_eq!(&tile.bytes()[row * 20..(row + 1) * 20], &full.bytes()[((row + 3) * 20 + 2) * 4..((row + 3) * 20 + 7) * 4]); + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn user_unit_scales_rotated_crop_geometry_and_paint_together() { + let doc = craft_pdf::open(pdf("1 0 0 rg 0 0 40 20 re f", "/CropBox [10 0 40 20] /Rotate 90 /UserUnit 2"), None).unwrap(); + let page = &doc.pages()[0]; + assert_eq!(raster::dimensions(page, &Options::default()).unwrap(), [40, 60]); + let full = raster::render(page, &RenderCache::new(), &InterpreterSettings::default(), &Options::default()).unwrap(); + assert_eq!((full.width, full.height), (40, 60)); + assert!(full.bytes().as_chunks::<4>().0.iter().all(|pixel| *pixel == [255, 0, 0, 255])); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn subpixel_page_extent_still_produces_visible_pixels() { + let doc = craft_pdf::open(pdf("1 0 0 rg 0 0 40 20 re f", "/CropBox [0 0 40 0.2]"), None).unwrap(); + let raster = + raster::render(&doc.pages()[0], &RenderCache::new(), &InterpreterSettings::default(), &Options { scale: [0.5; 2], ..Default::default() }) + .unwrap(); + assert_eq!((raster.width, raster.height), (20, 1)); + assert!(raster.bytes().as_chunks::<4>().0.iter().any(|pixel| pixel[3] > 0)); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn rounded_up_viewport_draws_the_partial_edge_pixel() { + let doc = craft_pdf::open(pdf("0 0 1 rg 0 0 40 20 re f", ""), None).unwrap(); + let raster = raster::render( + &doc.pages()[0], + &RenderCache::new(), + &InterpreterSettings::default(), + &Options { scale: [0.51; 2], size: Some([21, 11]), ..Default::default() }, + ) + .unwrap(); + assert_eq!((raster.width, raster.height), (21, 11)); + let corner = raster.bytes().as_chunks::<4>().0.last().unwrap(); + assert!(corner[3] > 0 && corner[3] < 255, "partial edge pixel: {corner:?}"); + assert_eq!(corner[2], corner[3]); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn hostile_options_are_rejected_before_buffer_changes() { + let doc = craft_pdf::open(pdf("", ""), None).unwrap(); + let page = &doc.pages()[0]; + let cache = RenderCache::new(); + for scale in [f32::NAN, f32::INFINITY, -1.0, 0.0] { + let mut buffer = [123; 4]; + assert_eq!( + raster::render_into(page, &cache, &InterpreterSettings::default(), &Options { scale: [scale, 1.0], ..Default::default() }, &mut buffer), + Err(Error::InvalidOptions) + ); + assert_eq!(buffer, [123; 4]); + } + for size in [[0, 1], [8193, 1], [8192, 8192], [u32::MAX, u32::MAX]] { + assert_eq!(raster::dimensions(page, &Options { size: Some(size), ..Default::default() }), Err(Error::OutputLimit)); + } + assert_eq!(raster::render_into(page, &cache, &InterpreterSettings::default(), &Options::default(), &mut [0; 4]), Err(Error::BufferLength)); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn pre_cancelled_requests_never_draw() { + let doc = craft_pdf::open(pdf("0 0 1 rg 0 0 40 20 re f", ""), None).unwrap(); + let settings = InterpreterSettings { cancelled: Some(Arc::new(AtomicBool::new(true))), ..Default::default() }; + let mut bytes = vec![7; 40 * 20 * 4]; + assert_eq!(raster::render_into(&doc.pages()[0], &RenderCache::new(), &settings, &Options::default(), &mut bytes), Err(Error::Cancelled)); + assert!(bytes.iter().all(|b| *b == 7)); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn injected_font_resolver_is_used() { + let doc = craft_pdf::open( + pdf("BT /F1 12 Tf 2 5 Td (ABC) Tj ET", "/Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >>"), + None, + ) + .unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let count = calls.clone(); + let original = InterpreterSettings::default().font_resolver; + let settings = InterpreterSettings { + font_resolver: Arc::new(move |query| { + count.fetch_add(1, Ordering::Relaxed); + original(query) + }), + ..Default::default() + }; + raster::render(&doc.pages()[0], &RenderCache::new(), &settings, &Options::default()).unwrap(); + assert!(calls.load(Ordering::Relaxed) > 0); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn decode_warnings_reach_caller_and_result() { + let doc = craft_pdf::open(pdf("q 10 0 0 10 0 0 cm BI /W 32769 /H 8192 /BPC 8 /CS /RGB ID x EI Q", ""), None).unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let count = calls.clone(); + let settings = InterpreterSettings { + warning_sink: Arc::new(move |_| { + count.fetch_add(1, Ordering::Relaxed); + }), + ..Default::default() + }; + let result = raster::render(&doc.pages()[0], &RenderCache::new(), &settings, &Options::default()).unwrap(); + assert!(result.diagnostics.image_decode_failed); + assert!(calls.load(Ordering::Relaxed) > 0); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg(not(target_arch = "wasm32"))] +fn backend_callback_panic_is_an_error() { + let doc = craft_pdf::open( + pdf("BT /F1 12 Tf 2 5 Td (ABC) Tj ET", "/Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >>"), + None, + ) + .unwrap(); + let settings = InterpreterSettings { font_resolver: Arc::new(|_| panic!("synthetic backend failure")), ..Default::default() }; + let error = raster::render(&doc.pages()[0], &RenderCache::new(), &settings, &Options::default()).unwrap_err(); + assert_eq!(error, Error::BackendPanic); + assert_eq!(error.to_string(), "PDF renderer failed internally"); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn alpha_is_premultiplied_exactly_once() { + let doc = craft_pdf::open(pdf("/G gs 0 0 1 rg 0 0 40 20 re f", "/Resources << /ExtGState << /G << /ca 0.5 >> >> >>"), None).unwrap(); + let r = raster::render(&doc.pages()[0], &RenderCache::new(), &InterpreterSettings::default(), &Options::default()).unwrap(); + assert_eq!(&r.bytes()[0..4], &[0, 0, 128, 128]); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn annotation_policy_and_duplicate_ocg_names_remain_independent() { + let content = "/OC /Left BDC 1 0 0 rg 0 0 20 20 re f EMC /OC /Right BDC 0 0 1 rg 20 0 20 20 re f EMC"; + let appearance = "0 1 0 rg 0 0 40 20 re f"; + let doc=craft_pdf::open(assemble(&[ + "<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [7 0 R 8 0 R] /D << /BaseState /ON >> >> >>".into(), + "<< /Type /Pages /Count 1 /Kids [3 0 R] >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] /Contents 4 0 R /Annots [5 0 R] /Resources << /Properties << /Left 7 0 R /Right 8 0 R >> >> >>".into(), + format!("<< /Length {} >>\nstream\n{content}\nendstream",content.len()), + "<< /Type /Annot /Subtype /Square /Rect [0 0 40 20] /F 4 /AP << /N 6 0 R >> >>".into(), + format!("<< /Type /XObject /Subtype /Form /BBox [0 0 40 20] /Length {} >>\nstream\n{appearance}\nendstream",appearance.len()), + "<< /Type /OCG /Name (Duplicate) >>".into(), + "<< /Type /OCG /Name (Duplicate) >>".into(), + ]),None).unwrap(); + let page = &doc.pages()[0]; + let settings = InterpreterSettings { render_annotations: false, ocg_overrides: Arc::new(vec![(7, 0, false)]), ..Default::default() }; + let hidden = raster::render(page, &RenderCache::new(), &settings, &Options::default()).unwrap(); + assert_eq!(&hidden.bytes()[0..4], &[0; 4]); + assert_eq!(&hidden.bytes()[120..124], &[0, 0, 255, 255]); + let visible = raster::render(page, &RenderCache::new(), &InterpreterSettings::default(), &Options::default()).unwrap(); + assert_eq!(&visible.bytes()[0..4], &[0, 255, 0, 255]); + let comments_hidden = + raster::render(page, &RenderCache::new(), &InterpreterSettings { hide_comments: true, ..Default::default() }, &Options::default()).unwrap(); + assert_eq!(&comments_hidden.bytes()[0..4], &[255, 0, 0, 255]); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn colour_replacement_is_explicit_and_preserves_alpha() { + let doc = craft_pdf::open(pdf("/G gs 0 0 0 rg 0 0 40 20 re f", "/Resources << /ExtGState << /G << /ca 0.5 >> >> >>"), None).unwrap(); + let page = &doc.pages()[0]; + let cache = RenderCache::new(); + let original = raster::render(page, &cache, &InterpreterSettings::default(), &Options::default()).unwrap(); + assert_eq!(&original.bytes()[0..4], &[0, 0, 0, 128]); + let replacement = raster::ColorReplacement { text: [255, 0, 0], background: [255; 3], only_black: true, line_art: true }; + let changed = + raster::render(page, &cache, &InterpreterSettings::default(), &Options { color_replacement: Some(replacement), ..Default::default() }) + .unwrap(); + assert_eq!(&changed.bytes()[0..4], &[128, 0, 0, 128]); + let restored = raster::render(page, &cache, &InterpreterSettings::default(), &Options::default()).unwrap(); + assert_eq!(original.bytes(), restored.bytes()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn malformed_stream_completion_warning_reaches_raster_and_caller() { + let doc = craft_pdf::open( + assemble(&[ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] /Contents 4 0 R >>".into(), + "<< /Length 1 /Filter /FlateDecode >>\nstream\nx\nendstream".into(), + ]), + None, + ) + .unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let count = calls.clone(); + let settings = InterpreterSettings { + warning_sink: Arc::new(move |w| { + if matches!(w, craft_pdf::interpret::InterpreterWarning::ContentTruncated) { + count.fetch_add(1, Ordering::Relaxed); + } + }), + ..Default::default() + }; + let result = raster::render(&doc.pages()[0], &RenderCache::new(), &settings, &Options::default()).unwrap(); + assert!(result.diagnostics.content_truncated); + assert!(calls.load(Ordering::Relaxed) > 0); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn preview_settings_and_cache_preserve_final_image_quality() { + let content = "q 10 0 0 10 20 5 cm /Im0 Do Q q 0.5 0 0 0.5 2 2 cm /Im0 Do Q"; + let pixels = "A".repeat(4 * 4 * 3); + let doc = craft_pdf::open(assemble(&[ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >>".into(), + format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()), + format!("<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length {} >>\nstream\n{pixels}\nendstream", pixels.len()), + ]), None).unwrap(); + let page = &doc.pages()[0]; + let cache = RenderCache::new(); + let preview = raster::render(page, &cache, &InterpreterSettings { preview: true, ..Default::default() }, &Options::default()).unwrap(); + let final_after_preview = raster::render(page, &cache, &InterpreterSettings::default(), &Options::default()).unwrap(); + let fresh_final = raster::render(page, &RenderCache::new(), &InterpreterSettings::default(), &Options::default()).unwrap(); + assert_ne!(preview.bytes(), fresh_final.bytes(), "the tiny image is skipped only for a preview"); + assert_eq!(final_after_preview.bytes(), fresh_final.bytes(), "preview cache must not degrade final output"); + assert!(fresh_final.bytes().as_chunks::<4>().0.iter().filter(|p| **p == [0x41, 0x41, 0x41, 255]).count() > 50); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn document_decode_limit_warns_for_pages_and_nested_painting() { + // ASCIIHex wraps a RunLength packet of 128 spaces. The valid stream is cut to + // 32 bytes only by the lowered document policy, with no malformed operator. + let filter = "/Filter [/ASCIIHexDecode /RunLengthDecode]"; + let encoded = "812080"; + for kind in ["page", "form", "pattern", "type3"] { + let (content, resources, extras) = match kind { + "page" => (encoded.to_owned(), String::new(), Vec::new()), + "form" => ("/Fm Do".into(), "/Resources << /XObject << /Fm 5 0 R >> >>".into(), vec![ + format!("<< /Type /XObject /Subtype /Form /BBox [0 0 40 20] /Length {} {filter} >>\nstream\n{encoded}\nendstream", encoded.len()) + ]), + "pattern" => ("/Pattern cs /P scn 0 0 40 20 re f".into(), "/Resources << /Pattern << /P 5 0 R >> >>".into(), vec![ + format!("<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 40 20] /XStep 40 /YStep 20 /Resources << >> /Length {} {filter} >>\nstream\n{encoded}\nendstream", encoded.len()) + ]), + _ => ("BT /F 10 Tf 1 0 0 1 10 10 Tm (A) Tj ET".into(), "/Resources << /Font << /F 5 0 R >> >>".into(), vec![ + "<< /Type /Font /Subtype /Type3 /FontBBox [0 0 500 500] /FontMatrix [0.001 0 0 0.001 0 0] /CharProcs << /A 6 0 R >> /Encoding << /Type /Encoding /Differences [65 /A] >> /FirstChar 65 /LastChar 65 /Widths [500] /Resources << >> >>".into(), + format!("<< /Length {} {filter} >>\nstream\n{encoded}\nendstream", encoded.len()), + ]), + }; + let content_filter = if kind == "page" { filter } else { "" }; + let mut objects = vec![ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] /Contents 4 0 R {resources} >>"), + format!("<< /Length {} {content_filter} >>\nstream\n{content}\nendstream", content.len()), + ]; + objects.extend(extras); + let data = assemble(&objects); + for low in [false, true] { + let decode_limits = if low { craft_pdf::syntax::DecodeLimits::default().with_max_decoded_stream(32) } else { Default::default() }; + let options = craft_pdf::OpenOptions { decode_limits, ..Default::default() }; + let doc = craft_pdf::open_with_options(data.clone(), None, &options).unwrap(); + let calls = Arc::new(AtomicUsize::new(0)); + let count = calls.clone(); + let settings = InterpreterSettings { + warning_sink: Arc::new(move |w| { + if matches!(w, craft_pdf::interpret::InterpreterWarning::ContentTruncated) { + count.fetch_add(1, Ordering::Relaxed); + } + }), + ..Default::default() + }; + let result = raster::render(&doc.pages()[0], &RenderCache::new(), &settings, &Options::default()).unwrap(); + assert_eq!(result.diagnostics.content_truncated, low, "{kind}: low={low}"); + assert_eq!(calls.load(Ordering::Relaxed) > 0, low, "{kind}: caller warning"); + } + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn failed_form_decode_reports_incomplete_content() { + let doc = craft_pdf::open( + assemble(&[ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] /Contents 4 0 R /Resources << /XObject << /Fm 5 0 R >> >> >>".into(), + "<< /Length 6 >>\nstream\n/Fm Do\nendstream".into(), + "<< /Type /XObject /Subtype /Form /BBox [0 0 40 20] /Length 1 /Filter /DCTDecode >>\nstream\nx\nendstream".into(), + ]), + None, + ) + .unwrap(); + let result = raster::render(&doc.pages()[0], &RenderCache::new(), &InterpreterSettings::default(), &Options::default()).unwrap(); + assert!(result.diagnostics.content_truncated); +} diff --git a/crates/pdf/tests/interpretation.rs b/crates/pdf/tests/interpretation.rs new file mode 100644 index 00000000..7f86dcf9 --- /dev/null +++ b/crates/pdf/tests/interpretation.rs @@ -0,0 +1,120 @@ +#![cfg(feature = "read")] +#![allow(clippy::unwrap_used, clippy::panic)] + +#[cfg(feature = "interpret")] +mod device { + use craft_pdf::interpret::{ + BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, SoftMask, + TransformExt, font::Glyph, + }; + use kurbo::{Affine, BezPath, Rect}; + + #[derive(Default)] + struct PathDevice { + paths: usize, + fail: bool, + } + + impl Device<'_> for PathDevice { + fn set_soft_mask(&mut self, _: Option>) {} + fn set_blend_mode(&mut self, _: BlendMode) {} + fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) { + assert!(!self.fail, "synthetic device failure"); + self.paths += 1; + } + fn push_clip_path(&mut self, _: &ClipPath) {} + fn push_transparency_group(&mut self, _: f32, _: Option>, _: BlendMode) {} + fn draw_glyph(&mut self, _: &Glyph<'_>, _: Affine, _: Affine, _: &Paint<'_>, _: &GlyphDrawMode) {} + fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {} + fn pop_clip_path(&mut self) {} + fn pop_transparency_group(&mut self) {} + } + + fn execute(device: &mut PathDevice) -> Result<(), craft_pdf::ExecutionError> { + let content = "1 0 0 rg 0 0 40 20 re f"; + let bytes = format!( + "%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] /Contents 4 0 R >> endobj\n4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF", + content.len() + ); + let pdf = craft_pdf::open_slice(bytes.as_bytes(), None).unwrap(); + let page = &pdf.pages()[0]; + let cache = InterpreterCache::new(); + let mut context = Context::new( + page.initial_transform(true).to_kurbo(), + Rect::new(0.0, 0.0, 40.0, 20.0), + &cache, + page.xref(), + InterpreterSettings::default(), + ); + craft_pdf::interpret_page(page, &mut context, device) + } + + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), test)] + fn guarded_interpreter_reaches_the_device_without_raster() { + let mut device = PathDevice::default(); + execute(&mut device).unwrap(); + assert_eq!(device.paths, 1); + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn device_panic_is_an_execution_error_and_fresh_state_still_works() { + assert_eq!(execute(&mut PathDevice { fail: true, ..Default::default() }), Err(craft_pdf::ExecutionError::BackendPanic)); + let mut fresh = PathDevice::default(); + execute(&mut fresh).unwrap(); + assert_eq!(fresh.paths, 1); + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn invalid_pdf_is_a_typed_error() { + assert!(matches!(craft_pdf::open_slice(b"not a pdf", None), Err(craft_pdf::OpenError::Load(_)))); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn input_limit_can_be_checked_before_allocating() { + assert!(craft_pdf::check_input_size(craft_pdf::MAX_INPUT_BYTES).is_ok()); + assert!(matches!( + craft_pdf::check_input_size(craft_pdf::MAX_INPUT_BYTES + 1), + Err(craft_pdf::OpenError::InputLimit { limit: craft_pdf::MAX_INPUT_BYTES }) + )); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn parsing_without_interpretation_exposes_pages_and_objects() { + let bytes = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] >> endobj\ntrailer << /Root 1 0 R >>\n%%EOF"; + let pdf = craft_pdf::open_slice(bytes, None).unwrap(); + assert_eq!(pdf.pages().len(), 1); + let catalog = pdf.xref().get::>(pdf.xref().root_id()).unwrap(); + assert!(catalog.get_ref(b"Pages").is_some()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn caller_input_policy_is_checked_before_parser_and_copy() { + let options = craft_pdf::OpenOptions { max_input_bytes: 3, ..Default::default() }; + assert!(matches!(craft_pdf::open_slice_with_options(b"bad!", None, &options), Err(craft_pdf::OpenError::InputLimit { limit: 3 }))); + assert!(matches!( + craft_pdf::open_with_options(std::sync::Arc::new(b"bad!".to_vec()), None, &options), + Err(craft_pdf::OpenError::InputLimit { limit: 3 }) + )); + assert!(craft_pdf::check_input_size_with(3, &options).is_ok()); + let larger = craft_pdf::OpenOptions { max_input_bytes: usize::MAX, ..Default::default() }; + assert!(craft_pdf::check_input_size_with(craft_pdf::MAX_INPUT_BYTES + 1, &larger).is_ok()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn caller_decode_policy_survives_open_fallback() { + let bytes = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 20] >> endobj\ntrailer << /Root 1 0 R >>\n%%EOF"; + let options = + craft_pdf::OpenOptions { decode_limits: craft_pdf::syntax::DecodeLimits::default().with_max_decoded_stream(32), ..Default::default() }; + let low = craft_pdf::open_slice_with_options(bytes, None, &options).unwrap(); + let normal = craft_pdf::open_slice(bytes, None).unwrap(); + assert_eq!(low.decode_limits().max_decoded_stream(), 32); + assert_eq!(normal.decode_limits(), craft_pdf::syntax::DecodeLimits::default()); +} diff --git a/crates/pdf/tests/upstream_raster.rs b/crates/pdf/tests/upstream_raster.rs new file mode 100644 index 00000000..9f27f989 --- /dev/null +++ b/crates/pdf/tests/upstream_raster.rs @@ -0,0 +1,271 @@ +//! Portable regressions adapted from PdfCraft 7437bde378f43856eada40af25b983812e615e8a (MIT OR Apache-2.0). +#![cfg(feature = "raster")] +use craft_pdf::{ + open_slice, + raster::{self, Options, RenderCache}, +}; + +struct Pixels { + width: u32, + height: u32, + rgba: Vec, +} +fn render(bytes: &[u8]) -> Pixels { + let pdf = open_slice(bytes, None).unwrap(); + let raster = + raster::render(&pdf.pages()[0], &RenderCache::new(), &Default::default(), &Options { background: [255; 4], ..Default::default() }).unwrap(); + assert!(raster.diagnostics.is_empty(), "{:?}", raster.diagnostics); + Pixels { width: raster.width, height: raster.height, rgba: raster.bytes().to_vec() } +} +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02X}")).collect() +} + +// These frozen RGB8 values were computed outside the renderer in f64 from the +// CIE Lab inverse, the full XYZ adaptation matrix B^-1 diag(B*D65 / B*white) B, +// and the standard sRGB matrix/transfer function. No production helper is used. +// Bradford reference: https://www.color.org/chadtag/ (ICC TN 02-2003). +// White points: D50 [0.96422, 1, 0.82521], E [1, 1, 1], +// D65 [0.95047, 1, 1.08883]. D65 adaptation is the identity. +const SAMPLES: [[f64; 3]; 4] = [[50.0, 0.0, 0.0], [40.0, 20.0, -60.0], [70.0, -50.0, 30.0], [100.0, 0.0, 0.0]]; +const D50: [[u8; 3]; 4] = [[119, 119, 119], [67, 86, 194], [66, 194, 113], [255, 255, 255]]; +const E: [[u8; 3]; 4] = [[119, 119, 119], [50, 88, 194], [64, 194, 114], [255, 255, 255]]; +const D65: [[u8; 3]; 4] = [[119, 119, 119], [19, 90, 194], [77, 193, 114], [255, 255, 255]]; + +fn patch(space: &str, operands: &str) -> [u8; 4] { + let content = format!("/C cs {operands} sc 0 0 20 20 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 20 20] /Resources << /ColorSpace << /C {space} >> >> /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 = render(&bytes); + assert_eq!((page.width, page.height), (20, 20)); + page.rgba.get((10 * 20 + 10) * 4..(10 * 20 + 10) * 4 + 4).unwrap().try_into().unwrap() +} + +fn assert_rgb(actual: [u8; 4], expected: [u8; 3], context: &str) { + assert_eq!(actual[3], 255, "{context}: {actual:?}"); + for (got, want) in actual.into_iter().zip(expected) { + assert!(got.abs_diff(want) <= 1, "{context}: got {actual:?}, expected {expected:?}"); + } +} + +fn check_white(white: &str, expected: [[u8; 3]; 4]) { + let space = format!("[/Lab << /WhitePoint {white} /Range [-100 100 -100 100] >>]"); + for (sample, expected) in SAMPLES.into_iter().zip(expected) { + let operands = format!("{} {} {}", sample[0], sample[1], sample[2]); + assert_rgb(patch(&space, &operands), expected, &format!("{white} Lab {sample:?}")); + } +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn lab_bradford_d50_neutral_and_chromatic_oracles() { + check_white("[0.96422 1 0.82521]", D50); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn lab_bradford_equal_energy_neutral_and_chromatic_oracles() { + check_white("[1 1 1]", E); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn lab_bradford_d65_identity_oracles() { + check_white("[0.95047 1 1.08883]", D65); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn lab_bradford_indexed_matches_direct_and_oracle() { + for (white, blue) in [("[0.96422 1 0.82521]", D50[1]), ("[1 1 1]", E[1]), ("[0.95047 1 1.08883]", D65[1])] { + // <66 99 33> maps exactly to L*=40, a*=20, b*=-60. + let lab = format!("[/Lab << /WhitePoint {white} /Range [-100 100 -100 100] >>]"); + let indexed = patch(&format!("[/Indexed {lab} 0 <669933>]"), "0"); + assert_rgb(indexed, blue, &format!("{white} Indexed blue")); + assert_eq!(indexed, patch(&lab, "40 20 -60"), "{white} direct/Indexed blue"); + + // A non-default Range maps <80 80 80> to L*=128/255*100, a*=b*=0. + let lab = format!("[/Lab << /WhitePoint {white} /Range [-128 127 -128 127] >>]"); + let indexed = patch(&format!("[/Indexed {lab} 0 <808080>]"), "0"); + assert_rgb(indexed, [119, 119, 119], &format!("{white} Indexed neutral")); + assert_eq!(indexed, patch(&lab, "50.19607843137255 0 0"), "{white} direct/Indexed neutral"); + } +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn lab_bradford_degenerate_cones_fall_back_to_unadapted_xyz() { + // In f32, B*[1, 1, 7.1964064] has a zero L cone response. + // B*[3e38, 3e38, 3e38] overflows its L/M responses. The guard must + // return the sample XYZ in both cases. Independent unadapted XYZ -> + // sRGB references for Lab (50, 0, 0) are frozen below. + for (white, expected) in [ + ("[1 1 7.1964064]", [0, 130, 255]), + ( + "[300000000000000000000000000000000000000 300000000000000000000000000000000000000 300000000000000000000000000000000000000]", + [255, 255, 255], + ), + ] { + let space = format!("[/Lab << /WhitePoint {white} >>]"); + assert_rgb(patch(&space, "50 0 0"), expected, &format!("{white} degenerate cones")); + } +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn lab_bradford_asymmetric_chromatic_range_matches_direct_and_oracle() { + // With a* [-40, 70] and b* [-90, 20], <80 99 33> maps to + // L*=128/255*100, a*=26, b*=-68. Frozen independent D50 oracle: + // RGB (98, 108, 237). The unequal, asymmetric ranges expose swaps + // and default-Range scaling that a neutral lookup cannot detect. + let lab = "[/Lab << /WhitePoint [0.96422 1 0.82521] /Range [-40 70 -90 20] >>]"; + let indexed = patch(&format!("[/Indexed {lab} 0 <809933>]"), "0"); + assert_rgb(indexed, [98, 108, 237], "D50 asymmetric chromatic Range"); + assert_eq!(indexed, patch(lab, "50.19607843137255 26 -68")); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn shrunk_soft_mask_larger_than_its_image_keeps_thin_strokes() { + let mask: [u8; 8] = [0, 0, 255, 0, 0, 0, 0, 0]; + let content = "q 4 0 0 1 0 0 cm /Im0 Do Q"; + let pdf = format!( + "%PDF-1.4\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 4 1] /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >> endobj\n\ + 4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\ + 5 0 obj << /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /SMask 6 0 R /Filter /ASCIIHexDecode /Length 3 >> stream\n00>\nendstream endobj\n\ + 6 0 obj << /Type /XObject /Subtype /Image /Width 8 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /ASCIIHexDecode /Length {} >> stream\n{}>\nendstream endobj\n\ + trailer << /Root 1 0 R >>\n%%EOF\n", + content.len(), + hex(&mask).len() + 1, + hex(&mask) + ); + let page = render(pdf.as_bytes()); + assert_eq!((page.width, page.height), (4, 1)); + let grays: Vec = page.rgba.as_chunks::<4>().0.iter().map(|px| px[0]).collect(); + // The stroke covers half of device pixel 1, so it is a mid-tone; the far side stays white. + assert!((64..=191).contains(&grays[1]), "the stroke is lost or not a mid-tone: {grays:?}"); + assert_eq!(grays[3], 255, "{grays:?}"); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn text_rendering_modes_other_than_invisible_draw_type3_glyphs_as_filled() { + let render = |glyph: &str, mode: u8| { + let content = format!("BT 1 0 0 RG 6 w 0 g /F1 20 Tf {mode} Tr 10.3 10.3 Td (A) Tj ET"); + let pdf = format!( + "%PDF-1.7 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj +4 0 obj << /Length {} >> stream +{content} +endstream endobj +5 0 obj << /Type /Font /Subtype /Type3 /FontBBox [0 0 1 1] /FontMatrix [1 0 0 1 0 0] /FirstChar 65 /LastChar 65 /Widths [1] + /Encoding << /Differences [65 /a] >> /CharProcs << /a 6 0 R >> >> endobj +6 0 obj << /Length {} >> stream +{glyph} +endstream endobj +trailer << /Root 1 0 R >> +%%EOF", + content.len(), + glyph.len() + ); + let p = render(pdf.as_bytes()); + p.rgba + }; + for glyph in ["1 0 0 0 1 1 d1 0.12 0.12 0.71 0.71 re f", "1 0 d0 0.12 0.12 0.71 0.71 re f"] { + let filled = render(glyph, 0); + assert!(filled.as_chunks::<4>().0.iter().any(|p| p[0] < 128), "{glyph}: the glyph paints"); + for mode in [1, 2, 4, 5, 6, 7] { + assert!(render(glyph, mode) == filled, "{glyph}: {mode} Tr draws as 0 Tr"); + } + assert!(render(glyph, 3).as_chunks::<4>().0.iter().all(|p| p[..3] == [255, 255, 255]), "{glyph}: 3 Tr hides the glyph"); + } +} + +// Fixed zlib streams encode the two known rows in each case. These values are +// independent of the decoder and preserve packed row padding in 1/2/4-bit RGB. +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn tiff_predictor_packed_rgb_rows_keep_their_byte_boundaries() { + use craft_pdf::syntax::object::{FromBytes, Object}; + struct RowCase { + bpc: u8, + columns: u32, + compressed: &'static [u8], + expected: &'static [u8], + masks: &'static [u8], + } + let cases = [ + RowCase { bpc: 1, columns: 1, compressed: &[120, 156, 107, 112, 0, 0, 1, 66, 0, 193], expected: &[128, 64], masks: &[224] }, + RowCase { bpc: 2, columns: 1, compressed: &[120, 156, 59, 96, 0, 0, 1, 178, 0, 241], expected: &[192, 48], masks: &[252] }, + RowCase { + bpc: 4, + columns: 1, + compressed: &[120, 156, 251, 192, 192, 207, 0, 0, 3, 226, 1, 0], + expected: &[240, 0, 15, 0], + masks: &[255, 240], + }, + RowCase { + bpc: 1, + columns: 3, + compressed: &[120, 156, 155, 217, 16, 213, 0, 0, 5, 28, 1, 244], + expected: &[136, 128, 80, 128], + masks: &[255, 128], + }, + RowCase { + bpc: 8, + columns: 1, + compressed: &[120, 156, 251, 207, 192, 192, 240, 159, 1, 0, 7, 254, 1, 255], + expected: &[255, 0, 0, 0, 255, 0], + masks: &[255, 255, 255], + }, + ]; + for RowCase { bpc, columns, compressed, expected, masks } in cases { + let mut bytes = format!( + "<< /Filter /FlateDecode /DecodeParms << /Predictor 2 /Colors 3 /BitsPerComponent {bpc} /Columns {columns} >> /Length {} >> stream\n", + compressed.len() + ) + .into_bytes(); + bytes.extend_from_slice(compressed); + bytes.extend_from_slice(b"\nendstream"); + let Object::Stream(stream) = Object::from_bytes(&bytes).unwrap() else { panic!("stream expected") }; + let decoded = stream.decoded().unwrap(); + assert_eq!(decoded.len(), expected.len()); + for ((actual, expected), mask) in decoded.iter().zip(expected).zip(masks.iter().cycle()) { + assert_eq!(actual & mask, *expected, "{bpc}-bit RGB, {columns} columns"); + } + } +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn tiff_predictor_packed_rgb_image_renders_every_row() { + // Deflated [0x80, 0x40]: one red then one green 1-bit RGB sample, each byte-aligned. + let compressed = [120, 156, 107, 112, 0, 0, 1, 66, 0, 193]; + let content = "q 40 0 0 40 0 0 cm /Im1 Do Q"; + let mut pdf = 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 40 40] /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >> endobj\n\ + 4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\n\ + 5 0 obj << /Type /XObject /Subtype /Image /Width 1 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 1 /Filter /FlateDecode /DecodeParms << /Predictor 2 /Colors 3 /BitsPerComponent 1 /Columns 1 >> /Length {} >> stream\n", + content.len(), compressed.len() + ).into_bytes(); + pdf.extend_from_slice(&compressed); + pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n"); + let page = render(&pdf); + assert_eq!((page.width, page.height), (40, 40)); + let pixel = |y: usize| &page.rgba[((y * 40 + 20) * 4)..][..4]; + assert_eq!(pixel(10), &[255, 0, 0, 255]); + assert_eq!(pixel(30), &[0, 255, 0, 255]); +} diff --git a/crates/pdf/tests/upstream_reading.rs b/crates/pdf/tests/upstream_reading.rs new file mode 100644 index 00000000..778cc2b4 --- /dev/null +++ b/crates/pdf/tests/upstream_reading.rs @@ -0,0 +1,121 @@ +//! Text contracts adapted from PdfCraft #1477, with explicit-encoding precedence checks. +#![cfg(feature = "interpret")] +#![allow(clippy::unwrap_used, clippy::panic)] + +use craft_pdf::interpret::{ + BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, SoftMask, TransformExt, + font::Glyph, +}; +use hayro_cmap::BfString; +use kurbo::{Affine, BezPath, Rect}; + +#[cfg(craft_pdf_unicode_fixtures)] +const INTER: &[u8] = include_bytes!(concat!(env!("CRAFT_PDF_TEST_FONT_DIR"), "/Inter-Regular.ttf")); + +#[derive(Default)] +struct TextDevice { + text: String, + embedded_glyphs: usize, +} + +impl Device<'_> for TextDevice { + fn set_soft_mask(&mut self, _: Option>) {} + fn set_blend_mode(&mut self, _: BlendMode) {} + fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {} + fn push_clip_path(&mut self, _: &ClipPath) {} + fn push_transparency_group(&mut self, _: f32, _: Option>, _: BlendMode) {} + fn draw_glyph(&mut self, glyph: &Glyph<'_>, _: Affine, _: Affine, _: &Paint<'_>, _: &GlyphDrawMode) { + let Glyph::Outline(outline) = glyph else { panic!("expected an embedded TrueType font") }; + assert!(outline.font_data().unwrap().data.as_ref().as_ref() == fixture_font(), "a fallback font must not satisfy this contract"); + self.embedded_glyphs += 1; + match glyph.as_unicode() { + Some(BfString::Char(c)) => self.text.push(c), + Some(BfString::String(s)) => self.text.push_str(&s), + None => self.text.push('\u{fffd}'), + } + } + fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {} + fn pop_clip_path(&mut self) {} + fn pop_transparency_group(&mut self) {} +} + +fn fixture_font() -> &'static [u8] { + #[cfg(craft_pdf_unicode_fixtures)] + { + INTER + } + #[cfg(not(craft_pdf_unicode_fixtures))] + { + panic!("enable craft_pdf_unicode_fixtures with the hash-verified fonts.json fixtures") + } +} + +fn encoded_text(flags: u8, encoding: &str, unicode_map: Option<&str>, codes: &[u8]) -> String { + let font = fixture_font(); + let mut content = b"BT /F1 14 Tf 1 0 0 1 20 200 Tm (".to_vec(); + content.extend_from_slice(codes); + content.extend_from_slice(b") Tj ET"); + let mut pdf = 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 600 400] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj\n\ + 4 0 obj << /Length {} >> stream\n", + content.len() + ) + .into_bytes(); + pdf.extend_from_slice(&content); + pdf.extend_from_slice( + format!( + "\nendstream endobj\n5 0 obj << /Type /Font /Subtype /TrueType /BaseFont /Inter-Regular /FontDescriptor 6 0 R {encoding} {} >> endobj\n\ + 6 0 obj << /Type /FontDescriptor /FontName /Inter-Regular /Flags {flags} /FontFile2 7 0 R >> endobj\n\ + 7 0 obj << /Length {} >> stream\n", + if unicode_map.is_some() { "/ToUnicode 8 0 R" } else { "" }, + font.len() + ) + .as_bytes(), + ); + pdf.extend_from_slice(font); + pdf.extend_from_slice(b"\nendstream endobj\n"); + if let Some(map) = unicode_map { + let cmap = format!( + "/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Test def /CMapType 2 def \ + 1 begincodespacerange <00> endcodespacerange {map} endcmap CMapName currentdict /CMap defineresource pop end end" + ); + pdf.extend_from_slice(format!("8 0 obj << /Length {} >> stream\n{cmap}\nendstream endobj\n", cmap.len()).as_bytes()); + } + pdf.extend_from_slice(b"trailer << /Root 1 0 R >>\n%%EOF\n"); + let pdf = craft_pdf::open_slice(&pdf, None).unwrap(); + let page = &pdf.pages()[0]; + let cache = InterpreterCache::new(); + let mut context = + Context::new(page.initial_transform(true).to_kurbo(), Rect::new(0.0, 0.0, 600.0, 400.0), &cache, page.xref(), InterpreterSettings::default()); + let mut device = TextDevice::default(); + craft_pdf::interpret_page(page, &mut context, &mut device).unwrap(); + assert_eq!(device.embedded_glyphs, codes.len()); + device.text +} + +#[cfg_attr(not(craft_pdf_unicode_fixtures), ignore = "requires hash-verified external fonts and craft_pdf_unicode_fixtures cfg")] +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn truetype_without_encoding_reads_win_ansi_even_with_partial_to_unicode() { + let codes = b"R\xfcckfragen f\xfcr \xdcberhitzungsschutz: k\xf6nnen, wei\xdf, Caf\xe9"; + let expected = "Rückfragen für Überhitzungsschutz: können, weiß, Café"; + let ascii_map = "1 beginbfrange <20> <7E> <0020> endbfrange"; + assert_eq!(encoded_text(4, "", None, codes), expected, "symbolic, no ToUnicode"); + assert_eq!(encoded_text(32, "", Some(ascii_map), codes), expected, "nonsymbolic, ASCII-only ToUnicode"); +} + +#[cfg_attr(not(craft_pdf_unicode_fixtures), ignore = "requires hash-verified external fonts and craft_pdf_unicode_fixtures cfg")] +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn explicit_encoding_differences_and_to_unicode_override_the_fallback() { + for flags in [4, 32] { + assert_eq!(encoded_text(flags, "/Encoding /StandardEncoding", None, b"\xe9"), "Ø"); + assert_eq!(encoded_text(flags, "/Encoding /WinAnsiEncoding", None, b"\xe9"), "é"); + let differences = "/Encoding << /BaseEncoding /StandardEncoding /Differences [233 /C] >>"; + assert_eq!(encoded_text(flags, differences, None, b"\xe9"), "C"); + let map = "1 beginbfchar <03A9> endbfchar"; + assert_eq!(encoded_text(flags, differences, Some(map), b"\xe9"), "Ω"); + } +} diff --git a/licenses/pdf-backend/hayro-cmap/LICENSE-APACHE b/licenses/pdf-backend/hayro-cmap/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/licenses/pdf-backend/hayro-cmap/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-cmap/LICENSE-MIT b/licenses/pdf-backend/hayro-cmap/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/licenses/pdf-backend/hayro-cmap/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-cmap/LICENSE.txt b/licenses/pdf-backend/hayro-cmap/LICENSE.txt new file mode 100644 index 00000000..2552cc99 --- /dev/null +++ b/licenses/pdf-backend/hayro-cmap/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 1990-2023 Adobe. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +Neither the name of Adobe nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/pdf-backend/hayro-interpret/LICENSE-APACHE b/licenses/pdf-backend/hayro-interpret/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/licenses/pdf-backend/hayro-interpret/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-interpret/LICENSE-MIT b/licenses/pdf-backend/hayro-interpret/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/licenses/pdf-backend/hayro-interpret/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-interpret/assets/CGATS_LICENSE.txt b/licenses/pdf-backend/hayro-interpret/assets/CGATS_LICENSE.txt new file mode 100644 index 00000000..550e190e --- /dev/null +++ b/licenses/pdf-backend/hayro-interpret/assets/CGATS_LICENSE.txt @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/ \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-interpret/assets/LICENSE_FOXIT b/licenses/pdf-backend/hayro-interpret/assets/LICENSE_FOXIT new file mode 100644 index 00000000..8b4ed6dd --- /dev/null +++ b/licenses/pdf-backend/hayro-interpret/assets/LICENSE_FOXIT @@ -0,0 +1,27 @@ +// Copyright 2014 PDFium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/pdf-backend/hayro-jbig2/LICENSE-APACHE b/licenses/pdf-backend/hayro-jbig2/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/licenses/pdf-backend/hayro-jbig2/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-jbig2/LICENSE-MIT b/licenses/pdf-backend/hayro-jbig2/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/licenses/pdf-backend/hayro-jbig2/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-syntax/LICENSE-APACHE b/licenses/pdf-backend/hayro-syntax/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/licenses/pdf-backend/hayro-syntax/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-syntax/LICENSE-MIT b/licenses/pdf-backend/hayro-syntax/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/licenses/pdf-backend/hayro-syntax/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/pdf-backend/hayro/LICENSE-APACHE b/licenses/pdf-backend/hayro/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/licenses/pdf-backend/hayro/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro/LICENSE-MIT b/licenses/pdf-backend/hayro/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/licenses/pdf-backend/hayro/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/scripts/fetch-pdf-test-fonts.py b/scripts/fetch-pdf-test-fonts.py new file mode 100644 index 00000000..e21a47a4 --- /dev/null +++ b/scripts/fetch-pdf-test-fonts.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Fetch the pinned, licensed PDF test fixtures outside the checkout.""" + +import argparse +import hashlib +import json +from pathlib import Path +import urllib.request + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("destination", type=Path) + args = parser.parse_args() + root = Path(__file__).resolve().parent.parent + destination = args.destination.resolve() + if destination == root or root in destination.parents: + parser.error("fixtures must be stored outside the checkout") + fixtures = json.loads((root / "crates/pdf/tests/fonts.json").read_text()) + destination.mkdir(parents=True, exist_ok=True) + for fixture in fixtures: + name, url, digest = fixture["name"], fixture["source"], fixture["sha256"] + if Path(name).name != name or name in ("", ".", "..") or not url.startswith("https://"): + raise ValueError("invalid fixture name or source") + path = destination / name + if path.is_file() and hashlib.sha256(path.read_bytes()).hexdigest() == digest: + print(f"Verified {name}") + continue + with urllib.request.urlopen(url, timeout=60) as response: + data = response.read(16 * 1024 * 1024 + 1) + if len(data) > 16 * 1024 * 1024 or hashlib.sha256(data).hexdigest() != digest: + raise ValueError(f"fixture size or SHA-256 mismatch: {name}") + path.write_bytes(data) + print(f"Fetched and verified {name}") + + +if __name__ == "__main__": + main() diff --git a/scripts/test-pdf-backends.sh b/scripts/test-pdf-backends.sh new file mode 100644 index 00000000..de8fae60 --- /dev/null +++ b/scripts/test-pdf-backends.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Run transferred backend tests with the complete local patch set. Test copies +# keep Cargo's standalone lock resolution from modifying the vendored source. +set -euo pipefail +root=$(cd "$(dirname "$0")/.." && pwd) +test_root=$(mktemp -d "${TMPDIR:-/tmp}/craft-pdf-backends.XXXXXX") +trap 'rm -rf "$test_root"' EXIT +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$root/target}" +patches=() +for backend in hayro hayro-interpret hayro-syntax hayro-jbig2; do + cp -R "$root/vendor/$backend" "$test_root/$backend" + patches+=(--config "patch.crates-io.$backend.path=\"$test_root/$backend\"") +done +for backend in hayro-syntax hayro-interpret hayro hayro-jbig2; do + cargo test --manifest-path "$test_root/$backend/Cargo.toml" --lib "${patches[@]}" +done +cargo test --manifest-path "$test_root/hayro-syntax/Cargo.toml" --doc "${patches[@]}" +cargo test --manifest-path "$test_root/hayro-syntax/Cargo.toml" --lib --no-default-features --features std "${patches[@]}" diff --git a/vendor/README.craft-reader.md b/vendor/README.craft-reader.md new file mode 100644 index 00000000..3ce28824 --- /dev/null +++ b/vendor/README.craft-reader.md @@ -0,0 +1,36 @@ +# 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) +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 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. +ContentReader now reports omitted or failed whole streams and incomplete Flate blocks as +known incomplete content. The interpreter also reports failed or budget-reaching nested +forms, patterns and Type 3 procedures. A stream exposes its immutable document limit so +callers can use the same conservative completion check. This diagnostic does not cover +structural repair or every omitted object and is not a rendering-completeness guarantee. +The syntax manifest pins JBIG2 exactly to 0.3.0 so a newer registry release cannot bypass +the transferred decoder patches. +Synthetic unit tests cover these boundaries; two version tests and the introductory doctest +use synthetic PDFs so they do not rely on the absent upstream corpus. No corpus is copied. + +All other source and asset files remain unchanged. Upstream licence texts, Foxit/PDFium +standard-14 fonts and the CMYK ICC profile are retained. Attribution is in the root NOTICE +and ATTRIBUTION.md. The removed LAB.icc profile remains absent. Lopdf is not part of this +snapshot: applications that need its editing or inspection compatibility keep owning it. + +Root Cargo patch entries are required in consuming workspaces. The writer-only feature +remains independent; vendoring reader sources does not make them a writer dependency. + +The transferred snapshot includes the latest Lab Bradford adaptation, Type 3 rendering-mode +handling and filtered shrinking of oversized soft masks. `tests/upstream_raster.rs` adapts +the corresponding portable PdfCraft rendering contracts for native and WASM. +The snapshot also preserves PdfCraft #1477 (WinAnsi Unicode fallback for TrueType +fonts without Encoding) and #1499 (byte-aligned packed TIFF predictor rows). +Synthetic tests cover 1/2/4/8-bit RGB rows and rendering, plus embedded TrueType +text with absent or partial ToUnicode maps and explicit encoding precedence. diff --git a/vendor/hayro-interpret/.cargo-ok b/vendor/hayro-interpret/.cargo-ok new file mode 100644 index 00000000..5f8b7958 --- /dev/null +++ b/vendor/hayro-interpret/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/hayro-interpret/.cargo_vcs_info.json b/vendor/hayro-interpret/.cargo_vcs_info.json new file mode 100644 index 00000000..d89fbc1b --- /dev/null +++ b/vendor/hayro-interpret/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "0c98904d2ced8e37b63ed2bd1e60f95bc0c7c774" + }, + "path_in_vcs": "hayro-interpret" +} \ No newline at end of file diff --git a/vendor/hayro-interpret/Cargo.lock b/vendor/hayro-interpret/Cargo.lock new file mode 100644 index 00000000..c10e1749 --- /dev/null +++ b/vendor/hayro-interpret/Cargo.lock @@ -0,0 +1,529 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76258897e51fd156ee03b6246ea53f3e0eb395d0b327e9961c4fc4c8b2fa151a" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "hayro-ccitt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" + +[[package]] +name = "hayro-cmap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d285dc30731c8485de5fa732fbdf2b3affdf01e4da7c2135022ca6fa664bf6" +dependencies = [ + "brotli", + "hayro-postscript", +] + +[[package]] +name = "hayro-interpret" +version = "0.7.0" +dependencies = [ + "bitflags", + "hayro-cmap", + "hayro-syntax", + "image", + "kurbo", + "log", + "moxcms", + "phf", + "rustc-hash", + "siphasher", + "skrifa", + "smallvec", + "yoke", +] + +[[package]] +name = "hayro-jbig2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69374b3668dd45aeb3d3145cda68f2c7b4f223aaa2511e67d076f1c7d741388d" +dependencies = [ + "fearless_simd", + "hayro-ccitt", +] + +[[package]] +name = "hayro-jpeg2000" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" +dependencies = [ + "fearless_simd", + "log", +] + +[[package]] +name = "hayro-postscript" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" + +[[package]] +name = "hayro-syntax" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cff966952259902703369bf0f565ab757de6e022deb25db3a8734f39eca6d17" +dependencies = [ + "flate2", + "hayro-ccitt", + "hayro-jbig2", + "hayro-jpeg2000", + "log", + "memchr", + "rustc-hash", + "smallvec", + "zune-jpeg", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/vendor/hayro-interpret/Cargo.toml b/vendor/hayro-interpret/Cargo.toml new file mode 100644 index 00000000..8141e63d --- /dev/null +++ b/vendor/hayro-interpret/Cargo.toml @@ -0,0 +1,139 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.92" +name = "hayro-interpret" +version = "0.7.0" +authors = ["Laurenz Stampfl "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A crate for interpreting PDF files." +readme = "README.md" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/LaurenzV/hayro" +resolver = "2" + +[features] +default = [ + "embed-fonts", + "embed-cmaps", +] +embed-cmaps = ["hayro-cmap/embed-cmaps"] +embed-fonts = [] +logging = [ + "dep:log", + "hayro-syntax/logging", +] + +[lib] +name = "hayro_interpret" +path = "src/lib.rs" + +[[example]] +name = "extract_html" +path = "examples/extract_html.rs" + +[[example]] +name = "extract_images" +path = "examples/extract_images.rs" + +[dependencies.bitflags] +version = "2.10" + +[dependencies.hayro-cmap] +version = "0.1.0" +default-features = false + +[dependencies.hayro-syntax] +version = "0.7.0" +features = [ + "std", + "images", + "unsafe", +] +default-features = false + +[dependencies.kurbo] +version = "0.13.0" + +[dependencies.log] +version = "0.4" +optional = true + +[dependencies.moxcms] +version = "0.8.1" + +[dependencies.phf] +version = "0.13.1" +features = ["macros"] +default-features = false + +[dependencies.rustc-hash] +version = "2" + +[dependencies.siphasher] +version = "1" + +[dependencies.skrifa] +version = "0.47.0" + +[dependencies.smallvec] +version = "1" + +[dependencies.yoke] +version = "0.8" +features = ["derive"] + +[dev-dependencies.image] +version = "0.25" +features = ["png"] +default-features = false + +[lints.clippy] +collection_is_never_read = "warn" +dbg_macro = "warn" +debug_assert_with_mut_call = "warn" +default_trait_access = "warn" +doc_markdown = "warn" +fn_to_numeric_cast_any = "warn" +infinite_loop = "warn" +large_stack_arrays = "warn" +mismatching_type_param_order = "warn" +negative_feature_names = "warn" +redundant_feature_names = "warn" +same_functions_in_if_condition = "warn" +semicolon_if_nothing_returned = "warn" +todo = "warn" +too_many_arguments = "allow" +unseparated_literal_suffix = "warn" +use_self = "warn" +wildcard_dependencies = "warn" + +[lints.rust] +elided_lifetimes_in_paths = "warn" +keyword_idents_2024 = "forbid" +missing_docs = "warn" +non_ascii_idents = "forbid" +non_local_definitions = "forbid" +trivial_numeric_casts = "warn" +unnameable_types = "warn" +unreachable_pub = "warn" +unsafe_op_in_unsafe_fn = "forbid" +unused_import_braces = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" diff --git a/vendor/hayro-interpret/Cargo.toml.orig b/vendor/hayro-interpret/Cargo.toml.orig new file mode 100644 index 00000000..4d0abc72 --- /dev/null +++ b/vendor/hayro-interpret/Cargo.toml.orig @@ -0,0 +1,38 @@ +[package] +name = "hayro-interpret" +version = "0.7.0" +description = "A crate for interpreting PDF files." +edition = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +readme = "README.md" + +[dependencies] +hayro-cmap = { workspace = true } +hayro-syntax = { workspace = true, features = ["std", "images", "unsafe"] } +kurbo = { workspace = true } +smallvec = { workspace = true } +log = { workspace = true, optional = true } +moxcms = { workspace = true } +phf = { workspace = true, features = ["macros"] } +skrifa = { workspace = true } +yoke = { workspace = true, features = ["derive"]} +siphasher = { workspace = true } +bitflags = { workspace = true } +rustc-hash = { workspace = true } + +[dev-dependencies] +image = { workspace = true, features = ["png"] } + +[features] +default = ["embed-fonts", "embed-cmaps"] +# Embed a light-weight set of fonts (around 240KB) that can be used as a substitute for the 14 PDF standard fonts. +embed-fonts = [] +# Embed the 61 predefined cmaps into the binary. Adds about ~250KB of brotli-encoded data. +embed-cmaps = ["hayro-cmap/embed-cmaps"] +logging = ["dep:log", "hayro-syntax/logging"] + +[lints] +workspace = true diff --git a/vendor/hayro-interpret/LICENSE-APACHE b/vendor/hayro-interpret/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/vendor/hayro-interpret/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/vendor/hayro-interpret/LICENSE-MIT b/vendor/hayro-interpret/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/vendor/hayro-interpret/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/hayro-interpret/README.md b/vendor/hayro-interpret/README.md new file mode 100644 index 00000000..6536284b --- /dev/null +++ b/vendor/hayro-interpret/README.md @@ -0,0 +1,58 @@ +# hayro-interpret + +[![Crates.io](https://img.shields.io/crates/v/hayro-interpret.svg)](https://crates.io/crates/hayro-interpret) +[![Documentation](https://docs.rs/hayro-interpret/badge.svg)](https://docs.rs/hayro-interpret) + + + +A crate for interpreting PDF files. + +This crate provides an abstraction to interpret the content of a PDF file and render them +into an abstract [`Device`], which clients can implement as needed. This allows you, for +example, to render PDF files to bitmaps (which is what the `hayro` crate does), or other formats +such as SVG. + +It should be noted that this crate is still very much in development. Therefore it currently +lacks pretty much any documentation on how to use it. It's current API also only really makes it +useful for rendering to PNG or SVG, though this will be improved upon in the future. + +## Examples +See the `examples` folder on the GitHub repository. Apart from that, you can also consult +the implementation of `hayro` and `hayro-svg` to get an idea on how to use this crate. + +## Safety +This crate forbids unsafe code via a crate-level attribute. + +## Cargo features +This crate has one optional feature: +- `embed-fonts`: PDF processors are required to support 14 predefined fonts that do not need to be + embedded into a PDF file. If you enable this feature, hayro will embed a (permissively-licensed) + substitute for each font, so that you don't have to implement your custom font loading logic. This + will add around ~240KB to your binary. + + + +## License +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +## PdfCraft function limits + +The PdfCraft patch rejects function construction after 64 nested functions or 10,000 total +function nodes, including repeated references, and detects cycles on the current path. Shared +children in different branches remain valid. Stitching functions reject empty or unresolved +child lists instead of silently truncating them. Calculator functions have a shared limit of 64 +procedure levels and 10,000 parsed tokens (operators and procedure openings); evaluation has +its own 10,000-operator limit across selected branches. These are implementation resource +limits rather than PDF format limits. Rejection returns the existing `None`, and the optional +`logging` feature reports the reason and limit; no new interface warning is introduced. + +Calculator integer division without an integer result is also rejected. Bit shifting discards +bits beyond the 32-bit word and fills with zeroes, including logical right shifts of negative +words; rotation accepts the full signed shift range without negating the minimum integer. +Fixed-size operand and procedure-stack overflow errors propagate instead of truncating work. +Normal function signatures and supported color/transfer-function behavior remain unchanged. diff --git a/vendor/hayro-interpret/SOURCE.json b/vendor/hayro-interpret/SOURCE.json new file mode 100644 index 00000000..335dc081 --- /dev/null +++ b/vendor/hayro-interpret/SOURCE.json @@ -0,0 +1,440 @@ +{ + "crate": "hayro-interpret", + "source_repository": "https://github.com/storytold/pdfcraft", + "source_revision": "7437bde378f43856eada40af25b983812e615e8a", + "source_directory": "vendor/hayro-interpret", + "files": [ + { + "path": ".cargo-ok", + "source_sha256": "afbf9d0f3560b0fd7795e81c42a0a79ee6b6fc67e064f77826aee642cad28d91", + "source_git_blob": "5f8b795830acbab5961c1a28c76a7916c9631493", + "sha256": "afbf9d0f3560b0fd7795e81c42a0a79ee6b6fc67e064f77826aee642cad28d91" + }, + { + "path": ".cargo_vcs_info.json", + "source_sha256": "70d1ae0c04ad449578be4959fe09aab07b50c8e2427b2d8e417292e335a83186", + "source_git_blob": "d89fbc1b6510eed623a10ccc7708298dd621d832", + "sha256": "70d1ae0c04ad449578be4959fe09aab07b50c8e2427b2d8e417292e335a83186" + }, + { + "path": "Cargo.lock", + "source_sha256": "a8a0e3460e43e7305385c01c3060ed34e4fdf13495d61b8a6bd1314d8a1c91a1", + "source_git_blob": "c10e1749882fc27c11322f338b7e7f9568643b3c", + "sha256": "a8a0e3460e43e7305385c01c3060ed34e4fdf13495d61b8a6bd1314d8a1c91a1" + }, + { + "path": "Cargo.toml", + "source_sha256": "cdb6fc309585747b98c029d2e2d48f31bc06595871aa3cce041786e5f78f61bf", + "source_git_blob": "8141e63df329caeebd66392643848cdbdae5fb4e", + "sha256": "cdb6fc309585747b98c029d2e2d48f31bc06595871aa3cce041786e5f78f61bf" + }, + { + "path": "Cargo.toml.orig", + "source_sha256": "ed55d4a11308de617a457449a4af163d6ba9c5bb80efa7b6ccf237f2f03969a1", + "source_git_blob": "4d0abc721ef5f0b1e43706c04eafe794ef98f69f", + "sha256": "ed55d4a11308de617a457449a4af163d6ba9c5bb80efa7b6ccf237f2f03969a1" + }, + { + "path": "LICENSE-APACHE", + "source_sha256": "0cec06e0e55fbc3dc5cee4fca9b607f66cb8f4e4dbcf3b3c013594dd156732e9", + "source_git_blob": "4947287f7b5ccb5d1e8b7b2d3aa5d89f322c160d", + "sha256": "0cec06e0e55fbc3dc5cee4fca9b607f66cb8f4e4dbcf3b3c013594dd156732e9" + }, + { + "path": "LICENSE-MIT", + "source_sha256": "51b50e6120c344dcc146bbd6375ca1c78e079d97c8bea76e67a53d6ff0efebac", + "source_git_blob": "ae3742de4e9ccb463cb46f05fe3ca0ec23fbb0d2", + "sha256": "51b50e6120c344dcc146bbd6375ca1c78e079d97c8bea76e67a53d6ff0efebac" + }, + { + "path": "README.md", + "source_sha256": "582379dedd0bee08cc2173b8b44485626137f17660d50d3f6559fb2064c2070d", + "source_git_blob": "6536284b8f38df120a927f2aafeaad49612f8241", + "sha256": "582379dedd0bee08cc2173b8b44485626137f17660d50d3f6559fb2064c2070d" + }, + { + "path": "assets/CGATS001Compat-v2-micro.icc", + "source_sha256": "73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0", + "source_git_blob": "b5a73495bfc7509837b300800d8197d94e83fab7", + "sha256": "73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0" + }, + { + "path": "assets/CGATS_LICENSE.txt", + "source_sha256": "469d87b5fa48f8aaf69e25f4fcac3a309c97dbb7d561b917638abdb640e004d9", + "source_git_blob": "550e190e5c9ddeff82695fd411ca16edc5e4980e", + "sha256": "469d87b5fa48f8aaf69e25f4fcac3a309c97dbb7d561b917638abdb640e004d9" + }, + { + "path": "assets/FoxitDingbats.pfb", + "source_sha256": "845c752392b6c914fb989c75a08b7792b88f542d2499042ef2889f8c814a16ed", + "source_git_blob": "30d52963e281dcd7e6dd70555340fc987d3568ef", + "sha256": "845c752392b6c914fb989c75a08b7792b88f542d2499042ef2889f8c814a16ed" + }, + { + "path": "assets/FoxitFixed.pfb", + "source_sha256": "b6c8fe53f134b8b6d4578cd2d544df4cee9624c4efa8d51a560fb40ea296101b", + "source_git_blob": "f12dcbce52babb40fb61765aa46ddf47e3846511", + "sha256": "b6c8fe53f134b8b6d4578cd2d544df4cee9624c4efa8d51a560fb40ea296101b" + }, + { + "path": "assets/FoxitFixedBold.pfb", + "source_sha256": "f1b7159702973f54fc86254ea38bcf3712b2a736eb3a0995751e9f4bb45ad603", + "source_git_blob": "cf8e24aee5962ebcf7e4d58d932cc3321f3cdd38", + "sha256": "f1b7159702973f54fc86254ea38bcf3712b2a736eb3a0995751e9f4bb45ad603" + }, + { + "path": "assets/FoxitFixedBoldItalic.pfb", + "source_sha256": "8a000945843bd31add06aee63bf9fd41b7578b4f3242a4b7cd46349ad9d24d4d", + "source_git_blob": "d2880017c25716417f837a1c26044a0342fa3f9a", + "sha256": "8a000945843bd31add06aee63bf9fd41b7578b4f3242a4b7cd46349ad9d24d4d" + }, + { + "path": "assets/FoxitFixedItalic.pfb", + "source_sha256": "5007faf8320fa1fcc08b8894b356ad976f60b992ba29ee5272578bbdebaf3876", + "source_git_blob": "d71697d4b638737a5545dd0cb28cba8f613e4eec", + "sha256": "5007faf8320fa1fcc08b8894b356ad976f60b992ba29ee5272578bbdebaf3876" + }, + { + "path": "assets/FoxitSans.pfb", + "source_sha256": "a02339e95d20db14cbdfc1c6baf359cd36a091d5434925c3a4040f60e1a1af9e", + "source_git_blob": "37f244bd9cf3c60686c11ab9723278040e3675ac", + "sha256": "a02339e95d20db14cbdfc1c6baf359cd36a091d5434925c3a4040f60e1a1af9e" + }, + { + "path": "assets/FoxitSansBold.pfb", + "source_sha256": "7bdfa36de1d5757dcfaf8b8a4be95c6e41c40fa70dbed9d78bf979a7b9959054", + "source_git_blob": "affcf316dc61f489af93f38362d6cc97dd66e211", + "sha256": "7bdfa36de1d5757dcfaf8b8a4be95c6e41c40fa70dbed9d78bf979a7b9959054" + }, + { + "path": "assets/FoxitSansBoldItalic.pfb", + "source_sha256": "555ce45924338e2e4b32821910221ce85da691fd59a9723c4c971a7c5627fc8b", + "source_git_blob": "e1f60b75465d49e186d9109c9082f56d2c66f806", + "sha256": "555ce45924338e2e4b32821910221ce85da691fd59a9723c4c971a7c5627fc8b" + }, + { + "path": "assets/FoxitSansItalic.pfb", + "source_sha256": "7751cbefeda351708dae8c4a88db043411bc39ab2ae2097685da845d4d16f307", + "source_git_blob": "c04b0a5aec805a5354413e45a2122fa0b6001789", + "sha256": "7751cbefeda351708dae8c4a88db043411bc39ab2ae2097685da845d4d16f307" + }, + { + "path": "assets/FoxitSerif.pfb", + "source_sha256": "4f57d2b9d884af8f907bf22df6019b52d86cbf6214fdbd02b1ae05472a543f35", + "source_git_blob": "3fa682efbb4b1d1941194a5dd8b78f39b33b72b0", + "sha256": "4f57d2b9d884af8f907bf22df6019b52d86cbf6214fdbd02b1ae05472a543f35" + }, + { + "path": "assets/FoxitSerifBold.pfb", + "source_sha256": "0bdf4b04e964139818d51eda03d566ba999fc3ba2421b1c6c51f9dc969022e80", + "source_git_blob": "ff7c6ddecf6b689d93823c4c2a8c41e6e058de46", + "sha256": "0bdf4b04e964139818d51eda03d566ba999fc3ba2421b1c6c51f9dc969022e80" + }, + { + "path": "assets/FoxitSerifBoldItalic.pfb", + "source_sha256": "a406cac82583bf98175cb62c87ed5e95c45fbb34eece63a10b0a13e793cb2e10", + "source_git_blob": "460231fb88a0083e577ed534760307d7779fd82f", + "sha256": "a406cac82583bf98175cb62c87ed5e95c45fbb34eece63a10b0a13e793cb2e10" + }, + { + "path": "assets/FoxitSerifItalic.pfb", + "source_sha256": "610ae0687198045c4db5d1a6650fd5e92536631706382eb8b72e38df578d0ae9", + "source_git_blob": "d03a7c781e1a2b91481103dc3227a6bdca4a461c", + "sha256": "610ae0687198045c4db5d1a6650fd5e92536631706382eb8b72e38df578d0ae9" + }, + { + "path": "assets/FoxitSymbol.pfb", + "source_sha256": "47967d055530e7357088a08403115425643ec2cdfd6201ba8af0fbd7116c1539", + "source_git_blob": "c8f9bca78c2f89ce40dff0afe6cdd7068061b3e6", + "sha256": "47967d055530e7357088a08403115425643ec2cdfd6201ba8af0fbd7116c1539" + }, + { + "path": "assets/LICENSE_FOXIT", + "source_sha256": "b578cdd2345840ada550bd12519533812320d5f1d21cf4c1c7e1b1b0a31c98b7", + "source_git_blob": "8b4ed6dddf22b2fcc9f7f520cd23613b49307fa0", + "sha256": "b578cdd2345840ada550bd12519533812320d5f1d21cf4c1c7e1b1b0a31c98b7" + }, + { + "path": "assets/README.md", + "source_sha256": "15f2b46ccef40443b9b815cefb17bdb55bcdd45a286b4caab2384485c54d656b", + "source_git_blob": "3f5e8d4c7ef2cbe058f29149cdc45048c5a4b521", + "sha256": "15f2b46ccef40443b9b815cefb17bdb55bcdd45a286b4caab2384485c54d656b" + }, + { + "path": "examples/extract_html.rs", + "source_sha256": "fd287b7eab4fb1f1efa5a7107342e7466e8e008c088fe528797ecddf10d5d133", + "source_git_blob": "8e5933b1e2942acb5331b608be1549d40f2ac814", + "sha256": "fd287b7eab4fb1f1efa5a7107342e7466e8e008c088fe528797ecddf10d5d133" + }, + { + "path": "examples/extract_images.rs", + "source_sha256": "44ac19abe75918ac3a2a22dbe019091be97bb7a08ab2b7323bcbcc5151f86d1b", + "source_git_blob": "07e4406172c9f8e54ad97bf7a3de572d01f2a0f0", + "sha256": "44ac19abe75918ac3a2a22dbe019091be97bb7a08ab2b7323bcbcc5151f86d1b" + }, + { + "path": "src/cache.rs", + "source_sha256": "a0a46ddbab7ed00b6bd0fb09f10d545c06bb4374ea0f520ae118c8d4f7b3c55d", + "source_git_blob": "2dda2e8a7cc0a2491b253ba1f42adac6074b1b34", + "sha256": "a0a46ddbab7ed00b6bd0fb09f10d545c06bb4374ea0f520ae118c8d4f7b3c55d" + }, + { + "path": "src/color.rs", + "source_sha256": "c78cad4124e7bb203e0763f6bea36c542280fd0a8abfa3f7b3ea0bd728adb73b", + "source_git_blob": "dd2edf241d60f146ddb8ea4cbd4ad12a127fa0a7", + "sha256": "c78cad4124e7bb203e0763f6bea36c542280fd0a8abfa3f7b3ea0bd728adb73b" + }, + { + "path": "src/context.rs", + "source_sha256": "e6898005282196c77bf44357237c79f8c839e74f666e8157756230fd9e2f93ed", + "source_git_blob": "3e83229c5f58cfbbefd15bf3b5e7b71380291c01", + "sha256": "86e838ca19dfc47f5bb0d7a2ffff80709f2fd51bc6b62d65151841bcba212e08" + }, + { + "path": "src/convert.rs", + "source_sha256": "a361962285ef6153b4c10f75c8c7e31a2267b12481f85e5ff520915e79a6f0ba", + "source_git_blob": "e768911415945256824d9759b4aa0490e0c80377", + "sha256": "a361962285ef6153b4c10f75c8c7e31a2267b12481f85e5ff520915e79a6f0ba" + }, + { + "path": "src/device.rs", + "source_sha256": "b95b9c91184dde6bb7dcf2dfcad1244f44ae5c73b02d78ea6cd2c692ac8a5ce4", + "source_git_blob": "72d5a98098cb3544259dcb05df7b5b265310ff2f", + "sha256": "b95b9c91184dde6bb7dcf2dfcad1244f44ae5c73b02d78ea6cd2c692ac8a5ce4" + }, + { + "path": "src/encode.rs", + "source_sha256": "68973a358f38bd6d7f09277e6319506122f586d1cda092b933c2a18680fe6d8f", + "source_git_blob": "498ed909ca65f9765b4f0c3b2ea10386e2e6c48d", + "sha256": "68973a358f38bd6d7f09277e6319506122f586d1cda092b933c2a18680fe6d8f" + }, + { + "path": "src/font/blob.rs", + "source_sha256": "30a9dadf498829ee03eb1ddc6deaeb09a06ee42415f288d46f4a67640582c575", + "source_git_blob": "6eabc26b9bc30bdc51f0b5fdd5e4b556d7583ec5", + "sha256": "30a9dadf498829ee03eb1ddc6deaeb09a06ee42415f288d46f4a67640582c575" + }, + { + "path": "src/font/cid.rs", + "source_sha256": "c1dbe7fe5b7f7092d97b84063008df54aebf87db4ba1fdb3a179fab9fe6674b8", + "source_git_blob": "b99dcbc116ca1169df3a229eafd05ee5b9204626", + "sha256": "c1dbe7fe5b7f7092d97b84063008df54aebf87db4ba1fdb3a179fab9fe6674b8" + }, + { + "path": "src/font/generated/glyph_names.rs", + "source_sha256": "3ae9e18c10206ce893c50572c6a121db69c3f7f7c0f75ea9607b41b4c5d3dc4b", + "source_git_blob": "b2dd9ab05ab2a185e4c9d130281d763ac866f25f", + "sha256": "3ae9e18c10206ce893c50572c6a121db69c3f7f7c0f75ea9607b41b4c5d3dc4b" + }, + { + "path": "src/font/generated/mac_expert.rs", + "source_sha256": "8cee2c1e4957248931f35a813588417b34d6728e24d141e8023044f1bd33c157", + "source_git_blob": "3c8971b0da8f9923e296a476f0f6d4e6da6f888b", + "sha256": "8cee2c1e4957248931f35a813588417b34d6728e24d141e8023044f1bd33c157" + }, + { + "path": "src/font/generated/mac_os_roman.rs", + "source_sha256": "5c605359a58cedb98e9c68e09d0efab20b3298f78165bd6fa5448a95a2ffecb1", + "source_git_blob": "1d71ca4cbb1358c43bed3ccc52f30db684098356", + "sha256": "5c605359a58cedb98e9c68e09d0efab20b3298f78165bd6fa5448a95a2ffecb1" + }, + { + "path": "src/font/generated/mac_roman.rs", + "source_sha256": "0451e6ed0659cda339ef2202f1e4782cac0c4e3ec819446b33c8c5b47575c8e3", + "source_git_blob": "82f66da55e5a06a89feaa51dd9838e77aa7e5782", + "sha256": "0451e6ed0659cda339ef2202f1e4782cac0c4e3ec819446b33c8c5b47575c8e3" + }, + { + "path": "src/font/generated/metrics.rs", + "source_sha256": "2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41", + "source_git_blob": "c9ac725ee125e9f9d236896f615239119ef18a32", + "sha256": "2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41" + }, + { + "path": "src/font/generated/mod.rs", + "source_sha256": "1f462b95c1b427704f34bbe42caca37134c14e0654b85f107a442b8bcc819773", + "source_git_blob": "4edf16a7c8c142c17a4f500d045e8da9dfb7bcb2", + "sha256": "1f462b95c1b427704f34bbe42caca37134c14e0654b85f107a442b8bcc819773" + }, + { + "path": "src/font/generated/standard.rs", + "source_sha256": "9094acd2e4b0404104b48b46b28b34bf2a35ae938685ab4a4d2e17ab26bc999e", + "source_git_blob": "a3972c8e5f963e0020ec8c0d7cfbb2aa4a812b27", + "sha256": "9094acd2e4b0404104b48b46b28b34bf2a35ae938685ab4a4d2e17ab26bc999e" + }, + { + "path": "src/font/generated/symbol.rs", + "source_sha256": "3b0b7cc198c484fed14c867f033b82ff51257380dcaa72add3e87cd800afe615", + "source_git_blob": "ba68a2787626ad172e09f22b097e24e9cc28a515", + "sha256": "3b0b7cc198c484fed14c867f033b82ff51257380dcaa72add3e87cd800afe615" + }, + { + "path": "src/font/generated/win_ansi.rs", + "source_sha256": "b9e4d4251a7c478c56c07ea445ebf179de6c0cbde1f50dff2b9ef9f3ae7f2139", + "source_git_blob": "6db56ba9d83e263b49c62b032f78bc5ce68cc932", + "sha256": "b9e4d4251a7c478c56c07ea445ebf179de6c0cbde1f50dff2b9ef9f3ae7f2139" + }, + { + "path": "src/font/generated/zapf_dings.rs", + "source_sha256": "b3ef29c609b22be4119b4f21a847c9bb7450e2d0155889d9590bca3835d0aba3", + "source_git_blob": "19834cfdb2b34d92b74a042023d14e3143610a91", + "sha256": "b3ef29c609b22be4119b4f21a847c9bb7450e2d0155889d9590bca3835d0aba3" + }, + { + "path": "src/font/glyph_simulator.rs", + "source_sha256": "cc845d34c445e32d7fb63b113dd52c6f95be88c86331ebb25ded3d90bb50316f", + "source_git_blob": "903a6632462f545e4301f14895260ac076023759", + "sha256": "cc845d34c445e32d7fb63b113dd52c6f95be88c86331ebb25ded3d90bb50316f" + }, + { + "path": "src/font/mod.rs", + "source_sha256": "f7a8cfe30893115cc05906cbec747ad368b5645de101c8cbf7bf622f7611973b", + "source_git_blob": "5ccf5c48a6132b7704baff81ece3492a6a7acae2", + "sha256": "f7a8cfe30893115cc05906cbec747ad368b5645de101c8cbf7bf622f7611973b" + }, + { + "path": "src/font/outline.rs", + "source_sha256": "2462d9ae4008c620afb47103bfa55362e75d84a28cb418240439b147de4ada8c", + "source_git_blob": "558d886cad5b9f79c6c1422f604c517a5f98ce0d", + "sha256": "2462d9ae4008c620afb47103bfa55362e75d84a28cb418240439b147de4ada8c" + }, + { + "path": "src/font/standard_font.rs", + "source_sha256": "048bcfb0d7d63ced49cef583c954b7507e5f078313caf5cf565adb97ad4bda46", + "source_git_blob": "c73c75dd22c7937585a0646f49bc89514ce054df", + "sha256": "048bcfb0d7d63ced49cef583c954b7507e5f078313caf5cf565adb97ad4bda46" + }, + { + "path": "src/font/true_type.rs", + "source_sha256": "8e8ffcf8f9749b10262793f1ecaf86ac7c65e6d5608e013d938a03adf210ad84", + "source_git_blob": "40f33590309100f295cb441069c340f9e034b49b", + "sha256": "8e8ffcf8f9749b10262793f1ecaf86ac7c65e6d5608e013d938a03adf210ad84" + }, + { + "path": "src/font/type1.rs", + "source_sha256": "5779b825fa6a5589978ed5b30910b7af0986ace42bac4e3652f7975a479874d4", + "source_git_blob": "32b5e3ddd9a2ca877d760bc5150bd55c3302bb33", + "sha256": "5779b825fa6a5589978ed5b30910b7af0986ace42bac4e3652f7975a479874d4" + }, + { + "path": "src/font/type3.rs", + "source_sha256": "257f7a2c8780c8e565f46dfd7dd1e3d50bef1e2c44aae5777994addb0ca26d62", + "source_git_blob": "ccfeeb766472f16f15b04710bab711f669b94adf", + "sha256": "257f7a2c8780c8e565f46dfd7dd1e3d50bef1e2c44aae5777994addb0ca26d62" + }, + { + "path": "src/function/mod.rs", + "source_sha256": "59bdcc7a8491882c22154621e0d3eac3a1eeada9b572430f7a2f682b0cbfc9c6", + "source_git_blob": "005ab0bfe385724c5c85f524ce628d19a1be2089", + "sha256": "59bdcc7a8491882c22154621e0d3eac3a1eeada9b572430f7a2f682b0cbfc9c6" + }, + { + "path": "src/function/type0.rs", + "source_sha256": "84cb6ccb45536ca5731394558883350f43b5e66330e07eb8d5a0e070c8e20704", + "source_git_blob": "02bbc41b84caddc9b5dfc8f49b2da081eb281199", + "sha256": "84cb6ccb45536ca5731394558883350f43b5e66330e07eb8d5a0e070c8e20704" + }, + { + "path": "src/function/type2.rs", + "source_sha256": "93fc9da845ad3f4090846a49a49ab2de1e3bda74ee4f6f2d039c2043de8237c5", + "source_git_blob": "3e12fdaf54e30519ba2a4f0d955fad06e06d6bfb", + "sha256": "93fc9da845ad3f4090846a49a49ab2de1e3bda74ee4f6f2d039c2043de8237c5" + }, + { + "path": "src/function/type3.rs", + "source_sha256": "2153a517d8365fabf8b43d867572d7463cabcdbcdee28eea7f23e122af1bd0cc", + "source_git_blob": "37950f89c912865e05d849526dbf0c04d4439099", + "sha256": "2153a517d8365fabf8b43d867572d7463cabcdbcdee28eea7f23e122af1bd0cc" + }, + { + "path": "src/function/type4.rs", + "source_sha256": "13181d842c32a53dd93e922388eeb09c59d35520b4709d0d31972e813cb3bb41", + "source_git_blob": "e4c0d1e0f08ebdf5e0f7a04eab0ca47533e5f1e4", + "sha256": "13181d842c32a53dd93e922388eeb09c59d35520b4709d0d31972e813cb3bb41" + }, + { + "path": "src/gradient.rs", + "source_sha256": "fda8ca51d5314731c69d67732611c2e1f994373a1e585128a28c7115849989a3", + "source_git_blob": "1cfa30af8eebcfa77577eae2d69d365f82aa0c6d", + "sha256": "fda8ca51d5314731c69d67732611c2e1f994373a1e585128a28c7115849989a3" + }, + { + "path": "src/interpret/mod.rs", + "source_sha256": "2b286df0c34b8194e1bdd1b63c1e08354d24f8389958fd782f42e2cabf364673", + "source_git_blob": "931b3388cef188cee0a5a1d495b69a6ff04f5e51", + "sha256": "2b286df0c34b8194e1bdd1b63c1e08354d24f8389958fd782f42e2cabf364673" + }, + { + "path": "src/interpret/path.rs", + "source_sha256": "a5defbc4b883b262662f7e7474a00ec1f97dbef8c03b8669859f624f5d02344b", + "source_git_blob": "838ea099f11d918d6e333e32503368be864689f8", + "sha256": "a5defbc4b883b262662f7e7474a00ec1f97dbef8c03b8669859f624f5d02344b" + }, + { + "path": "src/interpret/state.rs", + "source_sha256": "dce778291b13f6df83c8c13aee22f60edd033ec9f7d89309ef148d46d3ba9814", + "source_git_blob": "95b773fb5cc434ab8db362d00e4e5180a8b740c9", + "sha256": "dce778291b13f6df83c8c13aee22f60edd033ec9f7d89309ef148d46d3ba9814" + }, + { + "path": "src/interpret/text.rs", + "source_sha256": "35eafa80147aea635bdef127c33ace20e1695e11e87c7bfe1e035c886806ff30", + "source_git_blob": "ddec0400ea7a639c14769ac9ee7e137d000b3efd", + "sha256": "35eafa80147aea635bdef127c33ace20e1695e11e87c7bfe1e035c886806ff30" + }, + { + "path": "src/lib.rs", + "source_sha256": "ea3fa23b74f99b5382798f90b7e9f0e3edca354ee26ea92fe26a91ee5c0195c9", + "source_git_blob": "24c3be295783d7a53ac6bc39d93e539883480cd4", + "sha256": "ea3fa23b74f99b5382798f90b7e9f0e3edca354ee26ea92fe26a91ee5c0195c9" + }, + { + "path": "src/log.rs", + "source_sha256": "f5cbe9ee0319d1770e1a12dee4af754937effb3b6fe1780f687f2ff57bbf60e1", + "source_git_blob": "fee9828c02c2a65d1c7a16c913954f5aa7fe6600", + "sha256": "f5cbe9ee0319d1770e1a12dee4af754937effb3b6fe1780f687f2ff57bbf60e1" + }, + { + "path": "src/ocg.rs", + "source_sha256": "439455f65b8ad6ed9b0306536c2f75fa5f3fdda42ed4ca351ec3d340c38dc330", + "source_git_blob": "c59b70df4be6a83485360dacd76bead9f80c7f94", + "sha256": "439455f65b8ad6ed9b0306536c2f75fa5f3fdda42ed4ca351ec3d340c38dc330" + }, + { + "path": "src/pattern.rs", + "source_sha256": "17b49a465275c20e6645dea9443f2cb3a64c594925fd860e74b80b44d5344e37", + "source_git_blob": "9295f51709f8915951d7b9afca9411651d65a1f8", + "sha256": "17b49a465275c20e6645dea9443f2cb3a64c594925fd860e74b80b44d5344e37" + }, + { + "path": "src/shading.rs", + "source_sha256": "a28f730ede65bb4e480d0a6ae49a21e69ed23d7b0fd6f8006e487c83faa0756d", + "source_git_blob": "98fbb58310900160d1d4067e0707708ba2f673d0", + "sha256": "a28f730ede65bb4e480d0a6ae49a21e69ed23d7b0fd6f8006e487c83faa0756d" + }, + { + "path": "src/soft_mask.rs", + "source_sha256": "6cd9c59c0cff580879c1f06ff40f3537c04b719bdb3ea59008fa1ab7559db5dd", + "source_git_blob": "4d97d565b53c820fa927fbba8c3dda6acead7f34", + "sha256": "6cd9c59c0cff580879c1f06ff40f3537c04b719bdb3ea59008fa1ab7559db5dd" + }, + { + "path": "src/types.rs", + "source_sha256": "0e66569b8a7dc243b0a384cf1d3615ca62bf660b5324236f2594609bdfddfd46", + "source_git_blob": "64d2b849bdd3fdcd4ebfd76845edcef77b625fbd", + "sha256": "0e66569b8a7dc243b0a384cf1d3615ca62bf660b5324236f2594609bdfddfd46" + }, + { + "path": "src/util.rs", + "source_sha256": "236c0316abe8aa2ddd37b71f04ebd2ec35fb6a03db8476c3a78fb33b8c953718", + "source_git_blob": "d39b450b8d72cbc91848b9072694a7020c1927c8", + "sha256": "236c0316abe8aa2ddd37b71f04ebd2ec35fb6a03db8476c3a78fb33b8c953718" + }, + { + "path": "src/x_object.rs", + "source_sha256": "b325843a8f4088cdb1cc1d471971bd88932fb1e538d06a20a1cc2bee138d5624", + "source_git_blob": "68d97d55554888b9afe6c16d08ab2cbaca61924e", + "sha256": "b325843a8f4088cdb1cc1d471971bd88932fb1e538d06a20a1cc2bee138d5624" + } + ] +} diff --git a/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc b/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc new file mode 100644 index 0000000000000000000000000000000000000000..b5a73495bfc7509837b300800d8197d94e83fab7 GIT binary patch literal 8464 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 U@I8Hy^1FWP|Gv&X!`9FL1Def^p8x;= literal 0 HcmV?d00001 diff --git a/vendor/hayro-interpret/assets/CGATS_LICENSE.txt b/vendor/hayro-interpret/assets/CGATS_LICENSE.txt new file mode 100644 index 00000000..550e190e --- /dev/null +++ b/vendor/hayro-interpret/assets/CGATS_LICENSE.txt @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/ \ No newline at end of file diff --git a/vendor/hayro-interpret/assets/FoxitDingbats.pfb b/vendor/hayro-interpret/assets/FoxitDingbats.pfb new file mode 100644 index 0000000000000000000000000000000000000000..30d52963e281dcd7e6dd70555340fc987d3568ef GIT binary patch literal 29513 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 DiyeX$ literal 0 HcmV?d00001 diff --git a/vendor/hayro-interpret/assets/FoxitFixedBold.pfb b/vendor/hayro-interpret/assets/FoxitFixedBold.pfb new file mode 100644 index 0000000000000000000000000000000000000000..cf8e24aee5962ebcf7e4d58d932cc3321f3cdd38 GIT binary patch literal 18055 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 aP{6{BWwy2LU5CmZYCDT$C?nq``+oq(ZGoi# literal 0 HcmV?d00001 diff --git a/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb b/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb new file mode 100644 index 0000000000000000000000000000000000000000..d2880017c25716417f837a1c26044a0342fa3f9a GIT binary patch literal 19151 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 M96+?GEz$me0Iy=aZvX%Q literal 0 HcmV?d00001 diff --git a/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb b/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb new file mode 100644 index 0000000000000000000000000000000000000000..d71697d4b638737a5545dd0cb28cba8f613e4eec GIT binary patch literal 18746 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 Zni$D~vCIWrQWpk)bz)RW9#i8~{}=hLJf;8u literal 0 HcmV?d00001 diff --git a/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb b/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb new file mode 100644 index 0000000000000000000000000000000000000000..e1f60b75465d49e186d9109c9082f56d2c66f806 GIT binary patch literal 16418 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 literal 0 HcmV?d00001 diff --git a/vendor/hayro-interpret/assets/FoxitSerifBold.pfb b/vendor/hayro-interpret/assets/FoxitSerifBold.pfb new file mode 100644 index 0000000000000000000000000000000000000000..ff7c6ddecf6b689d93823c4c2a8c41e6e058de46 GIT binary patch literal 19395 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