diff --git a/.github/workflows/pdf.yml b/.github/workflows/pdf.yml new file mode 100644 index 00000000..e0d9fcf1 --- /dev/null +++ b/.github/workflows/pdf.yml @@ -0,0 +1,88 @@ +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/test-fetch-pdf-test-fonts.py + 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/test-fetch-pdf-test-fonts.py + 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/.github/workflows/ui.yml b/.github/workflows/ui.yml index 9f5e98db..79845a44 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -17,6 +17,7 @@ concurrency: jobs: native: + timeout-minutes: 30 name: UI (${{ matrix.os }}) strategy: fail-fast: false @@ -43,6 +44,7 @@ jobs: - run: cargo test -p craft-ui --locked - run: cargo test -p craft-ui --all-features --locked wasm: + timeout-minutes: 15 name: UI WASM runs-on: ubuntu-latest steps: diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 5504fb05..34cd2ee2 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -9,3 +9,51 @@ 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; 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 | +| --- | --- | --- | --- | --- | --- | +| `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`. + +## 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 669d56ec..22657117 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" @@ -537,6 +605,22 @@ dependencies = [ ] +[[package]] +name = "craft-pdf" +version = "0.1.0" +dependencies = [ + "bytemuck", + "hayro", + "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" @@ -987,18 +1071,18 @@ 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", - "vello_cpu", + "vello_cpu 0.1.0", ] [[package]] @@ -1020,7 +1104,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]] @@ -1103,6 +1187,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" @@ -1138,6 +1225,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" @@ -1285,6 +1381,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" @@ -1296,6 +1402,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" @@ -1307,9 +1430,9 @@ dependencies = [ "hashbrown 0.17.1", "log", "peniko", - "skrifa", + "skrifa 0.44.0", "smallvec", - "vello_common", + "vello_common 0.1.0", ] [[package]] @@ -1395,7 +1518,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ "bitflags 2.13.2", "bytemuck", - "read-fonts", + "read-fonts 0.41.0", "smallvec", ] @@ -1419,6 +1542,101 @@ 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-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 2.13.2", + "hayro-cmap", + "hayro-syntax", + "kurbo 0.13.1", + "moxcms", + "phf", + "rustc-hash 2.1.3", + "siphasher", + "skrifa 0.47.0", + "smallvec", + "yoke", +] + +[[package]] +name = "hayro-jbig2" +version = "0.3.0" +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", +] + +[[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" +dependencies = [ + "flate2", + "hayro-ccitt", + "hayro-jbig2", + "hayro-jpeg2000", + "memchr", + "rustc-hash 2.1.3", + "smallvec", + "stringprep", + "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" @@ -1680,12 +1898,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" @@ -1833,6 +2067,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" @@ -1962,6 +2223,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" @@ -2018,7 +2289,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]] @@ -2101,6 +2372,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" @@ -2449,6 +2729,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" @@ -2503,6 +2789,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" @@ -2541,6 +2839,58 @@ 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 = "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" @@ -2793,7 +3143,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2887,6 +3237,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" @@ -2894,7 +3254,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", - "font-types", + "font-types 0.12.6", + "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", ] @@ -3071,7 +3442,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3128,7 +3499,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3155,6 +3526,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" @@ -3541,6 +3930,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" @@ -3548,7 +3947,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" dependencies = [ "bytemuck", - "read-fonts", + "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]] @@ -3626,7 +4035,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]] @@ -3669,6 +4078,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" @@ -3690,6 +4110,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" @@ -3800,7 +4232,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3878,7 +4310,7 @@ dependencies = [ "cfg-if", "log", "png 0.17.16", - "tiny-skia-path", + "tiny-skia-path 0.11.4", ] [[package]] @@ -3892,6 +4324,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" @@ -4102,6 +4545,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" @@ -4117,6 +4569,24 @@ 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" +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" @@ -4129,6 +4599,27 @@ 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" +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" @@ -4174,7 +4665,7 @@ dependencies = [ "base64 0.22.1", "data-url", "flate2", - "imagesize", + "imagesize 0.13.0", "kurbo 0.11.3", "log", "pico-args", @@ -4183,7 +4674,7 @@ dependencies = [ "siphasher", "strict-num", "svgtypes", - "tiny-skia-path", + "tiny-skia-path 0.11.4", "xmlwriter", ] @@ -4199,6 +4690,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" @@ -4214,6 +4739,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" @@ -4221,9 +4759,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]] @@ -4313,6 +4851,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" @@ -4659,7 +5236,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]] @@ -5024,6 +5601,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" @@ -5125,6 +5715,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 dccac86d..44a69033 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "3" members = ["crates/*"] +exclude = ["vendor/krilla", "vendor/hayro", "vendor/hayro-interpret", "vendor/hayro-syntax", "vendor/hayro-jbig2"] [workspace.package] version = "0.1.0" @@ -11,6 +12,13 @@ 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 = { version = "=0.8.2", default-features = false, features = ["raster-images"] } +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" } craft-print = { path = "crates/print" } @@ -23,7 +31,16 @@ 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" } + +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 6cd47e08..abf8fba5 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 native Windows Installer UI in crates/installer is adapted from: @@ -92,3 +94,28 @@ 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. +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 dd903709..e80d5379 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ 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 new file mode 100644 index 00000000..e540157a --- /dev/null +++ b/crates/pdf/src/lib.rs @@ -0,0 +1,19 @@ +//! Shared optional PDF reading, rendering and writing for Craft applications. +//! +//! 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/src/write.rs b/crates/pdf/src/write.rs new file mode 100644 index 00000000..1c2e0413 --- /dev/null +++ b/crates/pdf/src/write.rs @@ -0,0 +1,179 @@ +//! PDF authoring shared by Craft apps, using Krilla's drawing primitives. +//! +//! Applications supply already laid-out pages and retain their document models, font +//! licensing and fallback policy, export presets, tagging semantics and postprocessing. +//! [`backend`] exposes Krilla explicitly; this is not an independent graphics model. + +use std::panic::{AssertUnwindSafe, catch_unwind}; + +pub use krilla as backend; + +// Require the root workspace patch; an unpatched backend can abort during unwind. +const _: bool = backend::CRAFT_UNWIND_POISONING; + +use backend::configure::{Archival, Configuration, ConfigurationBuilder, ConfigurationError, PdfVersion}; +use backend::text::{Font, Tag}; + +/// Largest individual font copied for embedding (512 MiB). +pub const MAX_FONT_BYTES: usize = 512 * 1024 * 1024; +/// Maximum number of variation coordinates accepted for one font instance. +pub const MAX_FONT_VARIATIONS: usize = 256; + +/// Shared failures retain backend diagnostics instead of silently emitting a partial PDF. +#[derive(Debug, thiserror::Error)] +pub enum WriteError { + #[error("{0:?}")] + Configuration(ConfigurationError), + #[error("{0:?}")] + Serialization(#[source] Box), + #[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/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/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/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/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/upstream_compatibility.rs b/crates/pdf/tests/upstream_compatibility.rs new file mode 100644 index 00000000..38ffa0c7 --- /dev/null +++ b/crates/pdf/tests/upstream_compatibility.rs @@ -0,0 +1,512 @@ +//! Observable resource, object-depth and text-paint compatibility contracts. +#![cfg(feature = "read")] +#![allow(clippy::unwrap_used, clippy::panic)] + +use craft_pdf::syntax::object::{FromBytes, ObjRef, Object}; + +fn stream(dictionary: &str, content: &[u8]) -> Vec { + let mut bytes = format!("<< {dictionary} /Length {} >>\nstream\n", content.len()).into_bytes(); + bytes.extend_from_slice(content); + bytes.extend_from_slice(b"\nendstream"); + bytes +} + +fn object(bytes: &mut Vec, number: usize, body: &[u8]) -> usize { + let offset = bytes.len(); + bytes.extend_from_slice(format!("{number} 0 obj\n").as_bytes()); + bytes.extend_from_slice(body); + bytes.extend_from_slice(b"\nendobj\n"); + offset +} + +fn assemble(objects: &[Vec]) -> Vec { + let mut bytes = b"%PDF-1.7\n".to_vec(); + let offsets: Vec<_> = objects.iter().enumerate().map(|(i, body)| object(&mut bytes, i + 1, body)).collect(); + let start = bytes.len(); + bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", offsets.len() + 1).as_bytes()); + for offset in offsets { + bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes()); + } + bytes.extend_from_slice(format!("trailer << /Size {} /Root 1 0 R >>\nstartxref\n{start}\n%%EOF\n", objects.len() + 1).as_bytes()); + bytes +} + +fn nested_object(depth: usize, kind: usize) -> Vec { + let mut value = "42".to_string(); + for level in 0..depth { + value = if kind == 0 || kind == 2 && level % 2 == 0 { format!("[{value}]") } else { format!("<< /V {value} >>") }; + } + value.into_bytes() +} + +fn assert_nested_leaf(mut value: Object<'_>, depth: usize) { + for _ in 0..depth { + value = match value { + Object::Array(array) => array.iter::>().next().unwrap(), + Object::Dict(dict) => dict.get::>(b"V").unwrap(), + _ => panic!("expected another direct container"), + }; + } + assert_eq!(value.into_i32(), Some(42)); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +fn direct_objects_accept_64_containers_and_reject_65() { + for kind in 0..3 { + let accepted = nested_object(64, kind); + assert_nested_leaf(Object::from_bytes(&accepted).unwrap(), 64); + assert!(Object::from_bytes(&nested_object(65, kind)).is_none(), "container kind {kind}"); + } +} + +// Object 6 exists only inside an ASCIIHex-decoded object stream. A real xref stream +// selects the compressed object, exercising its parser rather than a direct-byte helper. +fn compressed_object(value: &[u8]) -> Vec { + let mut payload = b"6 0 ".to_vec(); + payload.extend_from_slice(value); + let encoded = payload.iter().map(|b| format!("{b:02X}")).collect::() + ">"; + let objects = [ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R >>".to_vec(), + stream("", b""), + stream("/Type /ObjStm /N 1 /First 4 /Filter /ASCIIHexDecode", encoded.as_bytes()), + ]; + let mut bytes = b"%PDF-1.7\n".to_vec(); + let mut offsets = vec![0]; + for (i, body) in objects.iter().enumerate() { + offsets.push(object(&mut bytes, i + 1, body)); + } + let xref = bytes.len(); + let mut rows = Vec::new(); + for number in 0..8 { + let (kind, first, second): (u8, u32, u16) = match number { + 0 => (0, 0, 65535), + 6 => (2, 5, 0), + 7 => (1, u32::try_from(xref).unwrap(), 0), + n => (1, u32::try_from(offsets[n]).unwrap(), 0), + }; + rows.push(kind); + rows.extend_from_slice(&first.to_be_bytes()); + rows.extend_from_slice(&second.to_be_bytes()); + } + object(&mut bytes, 7, &stream("/Type /XRef /Size 8 /Root 1 0 R /W [1 4 2]", &rows)); + bytes.extend_from_slice(format!("startxref\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 decoded_object_streams_keep_the_same_64_container_boundary() { + for kind in 0..3 { + let accepted = craft_pdf::open_slice(&compressed_object(&nested_object(64, kind)), None).unwrap(); + assert_nested_leaf(accepted.xref().get::>(ObjRef::new(6, 0).into()).unwrap(), 64); + let rejected = craft_pdf::open_slice(&compressed_object(&nested_object(65, kind)), None).unwrap(); + assert!(rejected.xref().get::>(ObjRef::new(6, 0).into()).is_none(), "container kind {kind}"); + } +} + +mod page_trees { + use super::assemble; + use craft_pdf::syntax::object::{Array, FromBytes, MaybeRef, Name, Number}; + use craft_pdf::syntax::page::{Page, Rotation}; + + fn assert_page(page: &Page<'_>, id: i32, media: [f64; 4], crop: [f64; 4], rotation: i32, user_unit: f32) { + assert_eq!(page.raw().get::(b"ContractPage"), Some(id)); + let actual = page.media_box(); + assert_eq!([actual.x0, actual.y0, actual.x1, actual.y1], media); + let actual = page.crop_box(); + assert_eq!([actual.x0, actual.y0, actual.x1, actual.y1], crop); + let actual = match page.rotation() { + Rotation::None => 0, + Rotation::Horizontal => 90, + Rotation::Flipped => 180, + Rotation::FlippedHorizontal => 270, + }; + assert_eq!(actual, rotation); + assert_eq!(page.user_unit(), user_unit); + } + + fn deep_page_tree_contract() { + const DEPTH: usize = 1024; + let mut objects = vec![b"<< /Type /Catalog /Pages 2 0 R >>".to_vec()]; + for level in 0..DEPTH { + let number = level + 2; + let parent = if level == 0 { String::new() } else { format!("/Parent {} 0 R", number - 1) }; + let attributes = match level { + 0 => "/MediaBox [0 0 200 300] /CropBox [1 2 190 290] /Rotate 90 /Resources << /Font << /Root << /BaseFont /Helvetica >> >> >>", + 512 => "/MediaBox [0 0 300 400] /Rotate 180 /Resources << /Font << /Middle << /BaseFont /Courier >> >> >>", + _ => "", + }; + objects.push(format!("<< /Type /Pages /Count 1 /Kids [{} 0 R] {parent} {attributes} >>", number + 1).into_bytes()); + } + // An empty page is valid. Omitting Contents also prevents a brute-force scan + // from satisfying this contract after losing the actual page-tree ancestry. + objects.push(format!("<< /Type /Page /Parent {} 0 R /ContractPage 1024 /UserUnit 2 >>", DEPTH + 1).into_bytes()); + let pdf = craft_pdf::open_slice(&assemble(&objects), None).unwrap(); + assert_eq!(pdf.pages().len(), 1, "the valid leaf must survive all 1024 ancestors"); + let page = &pdf.pages()[0]; + assert_page(page, 1024, [0.0, 0.0, 300.0, 400.0], [1.0, 2.0, 190.0, 290.0], 180, 2.0); + let middle = Name::from_bytes(b"/Middle").unwrap(); + let root = Name::from_bytes(b"/Root").unwrap(); + let font = page.resources().get_font(&middle).unwrap(); + assert_eq!(font.get::>(b"BaseFont").unwrap().as_str(), "Courier"); + assert!(page.resources().get_font(&root).is_none(), "the middle dictionary replaces the root scope"); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn deep_page_trees_preserve_leaf_attributes_and_resources() { + // Assert the observable contract before the small-stack check, so an old + // recursive backend fails on missing pages before it can overflow that stack. + deep_page_tree_contract(); + #[cfg(not(target_arch = "wasm32"))] + std::thread::Builder::new().stack_size(128 * 1024).spawn(deep_page_tree_contract).unwrap().join().unwrap(); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn cyclic_page_trees_keep_valid_sibling_order_and_deduplicate_leaves() { + let pdf = craft_pdf::open_slice( + &assemble(&[ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Count 3 /Kids [3 0 R 6 0 R 5 0 R 4 0 R 6 0 R] /MediaBox [0 0 100 200] /CropBox [1 2 90 180] /Rotate 90 /Resources << /Font << /Root << /BaseFont /Helvetica >> >> >> >>".to_vec(), + b"<< /Type /Pages /Parent 2 0 R /Count 2 /Kids [2 0 R 5 0 R 4 0 R 5 0 R] /MediaBox [0 0 300 400] /CropBox [10 20 280 370] /Rotate 180 /Resources << /Font << /Local << /BaseFont /Courier >> >> >> >>".to_vec(), + b"<< /Type /Page /Parent 3 0 R /ContractPage 4 /Rotate 270 /UserUnit 2 /Resources << >> >>".to_vec(), + b"<< /Type /Page /Parent 3 0 R /ContractPage 5 >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /ContractPage 6 >>".to_vec(), + ]), + None, + ) + .unwrap(); + assert_eq!(pdf.pages().len(), 3, "cycles and repeated references must not hide or duplicate valid pages"); + assert_page(&pdf.pages()[0], 5, [0.0, 0.0, 300.0, 400.0], [10.0, 20.0, 280.0, 370.0], 180, 1.0); + assert_page(&pdf.pages()[1], 4, [0.0, 0.0, 300.0, 400.0], [10.0, 20.0, 280.0, 370.0], 270, 2.0); + assert_page(&pdf.pages()[2], 6, [0.0, 0.0, 100.0, 200.0], [1.0, 2.0, 90.0, 180.0], 90, 1.0); + let root = Name::from_bytes(b"/Root").unwrap(); + let local = Name::from_bytes(b"/Local").unwrap(); + assert!(pdf.pages()[0].resources().get_font(&local).is_some()); + assert!(pdf.pages()[0].resources().get_font(&root).is_none()); + assert!(pdf.pages()[1].resources().get_font(&local).is_none()); + assert!(pdf.pages()[1].resources().get_font(&root).is_none()); + assert!(pdf.pages()[2].resources().get_font(&root).is_some()); + assert!(pdf.pages()[2].resources().get_font(&local).is_none()); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn sign_only_numeric_children_preserve_later_valid_pages() { + // Exercise the parser invariant directly: the old raw iterator panicked + // after array validation accepted '+' but parsing could not read it. + let array = Array::from_bytes(b"[+ 3]").unwrap(); + let values: Vec<_> = array + .raw_iter() + .map(|value| match value { + MaybeRef::NotRef(value) => value.into_i32(), + MaybeRef::Ref(_) => None, + }) + .collect(); + assert_eq!(values, [Some(0), Some(3)]); + for token in ["+", "+.", "-"] { + let pdf = craft_pdf::open_slice( + &assemble(&[ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + format!("<< /Type /Pages /Count 1 /Kids [{token} 3 0 R] /MediaBox [0 0 100 200] /CropBox [1 2 90 180] /Rotate 90 >>") + .into_bytes(), + b"<< /Type /Page /Parent 2 0 R /ContractPage 3 >>".to_vec(), + ]), + None, + ) + .unwrap(); + assert_eq!(pdf.pages().len(), 1, "{token}: an ignored numeric child must not hide the valid sibling"); + assert_page(&pdf.pages()[0], 3, [0.0, 0.0, 100.0, 200.0], [1.0, 2.0, 90.0, 180.0], 90, 1.0); + let bytes = format!("{token} "); + assert_eq!(Number::from_bytes(bytes.as_bytes()).unwrap().as_f64(), 0.0, "{token}: keep the reader's zero tolerance"); + } + } +} + +#[cfg(feature = "interpret")] +mod painting { + use super::{assemble, stream}; + use craft_pdf::interpret::{ + BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, SoftMask, + TransformExt, font::Glyph, + }; + use kurbo::{Affine, BezPath, Point, Rect}; + + #[cfg(craft_pdf_unicode_fixtures)] + const INTER: &[u8] = include_bytes!(concat!(env!("CRAFT_PDF_TEST_FONT_DIR"), "/Inter-Regular.ttf")); + + #[derive(Debug)] + struct GlyphEvent { + text: String, + mode: char, + rgba: [f32; 4], + origin: Point, + } + + #[derive(Default)] + struct Events { + paths: Vec<[f32; 4]>, + glyphs: Vec, + clips: Vec, + } + + fn rgba(paint: &Paint<'_>) -> [f32; 4] { + let Paint::Color(color) = paint else { panic!("solid paint expected") }; + color.to_rgba().components() + } + + impl Device<'_> for Events { + fn set_soft_mask(&mut self, _: Option>) {} + fn set_blend_mode(&mut self, _: BlendMode) {} + fn draw_path(&mut self, _: &BezPath, _: Affine, paint: &Paint<'_>, _: &PathDrawMode) { + self.paths.push(rgba(paint)); + } + fn push_clip_path(&mut self, clip: &ClipPath) { + self.clips.push(clip.path.to_svg()); + } + fn push_transparency_group(&mut self, _: f32, _: Option>, _: BlendMode) {} + fn draw_glyph(&mut self, glyph: &Glyph<'_>, _: Affine, glyph_transform: Affine, paint: &Paint<'_>, mode: &GlyphDrawMode) { + let Glyph::Outline(outline) = glyph else { panic!("embedded outline glyph expected") }; + assert!(outline.font_data().unwrap().data.as_ref().as_ref() == fixture_font(), "fallback font cannot satisfy the contract"); + let text = match glyph.as_unicode().unwrap() { + hayro_cmap::BfString::Char(c) => c.to_string(), + hayro_cmap::BfString::String(s) => s, + }; + let mode = match mode { + GlyphDrawMode::Fill => 'F', + GlyphDrawMode::Stroke(_) => 'S', + GlyphDrawMode::Invisible => 'I', + }; + self.glyphs.push(GlyphEvent { text, mode, rgba: rgba(paint), origin: glyph_transform * Point::ORIGIN }); + } + fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {} + fn pop_clip_path(&mut self) {} + fn pop_transparency_group(&mut self) {} + } + + fn events(bytes: &[u8]) -> Events { + let pdf = craft_pdf::open_slice(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, 100.0, 60.0), + &cache, + page.xref(), + InterpreterSettings::default(), + ); + let mut device = Events::default(); + craft_pdf::interpret_page(page, &mut context, &mut device).unwrap(); + device + } + + fn page(resources: &str, content: &[u8], extra: &[Vec]) -> Vec { + let mut objects = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>".to_vec(), + format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources {resources} >>").into_bytes(), + stream("", content), + ]; + objects.extend_from_slice(extra); + assemble(&objects) + } + + fn painted_form(color: &str, side: u32) -> Vec { + stream(&format!("/Type /XObject /Subtype /Form /BBox [0 0 {side} {side}]"), format!("{color} rg 0 0 {side} {side} re f").as_bytes()) + } + + const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0]; + const GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0]; + const GREEN_SCOPE: &str = "/Resources << /XObject << /Target 7 0 R >> >>"; + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn nearest_page_resource_dictionary_replaces_the_whole_ancestor_dictionary() { + for (middle, leaf, expected) in [ + ("", "", vec![RED]), + ("/Resources << >>", "", vec![]), + ("/Resources << /ProcSet [/PDF] >>", "", vec![]), + (GREEN_SCOPE, "", vec![GREEN]), + (GREEN_SCOPE, "/Resources << >>", vec![]), + ("", "/Resources << /XObject << /Unused 7 0 R >> >>", vec![]), + ] { + let bytes = assemble(&[ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Count 1 /Kids [3 0 R] /Resources << /XObject << /Target 6 0 R >> >> >>".to_vec(), + format!("<< /Type /Pages /Parent 2 0 R /Count 1 /Kids [4 0 R] {middle} >>").into_bytes(), + format!("<< /Type /Page /Parent 3 0 R /MediaBox [0 0 40 40] /Contents 5 0 R {leaf} >>").into_bytes(), + stream("", b"/Target Do"), + painted_form("1 0 0", 40), + painted_form("0 1 0", 40), + ]); + assert_eq!(events(&bytes).paths, expected, "middle={middle}, page={leaf}"); + } + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn forms_inherit_only_when_resources_are_absent_and_restore_the_caller_scope() { + for (scope, first) in [("", Some(RED)), ("/Resources << >>", None), ("/Resources << /ProcSet [/PDF] >>", None), (GREEN_SCOPE, Some(GREEN))] { + let bytes = page( + "<< /XObject << /Outer 5 0 R /Target 6 0 R >> >>", + b"/Outer Do /Target Do", + &[ + stream(&format!("/Type /XObject /Subtype /Form /BBox [0 0 40 40] {scope}"), b"/Target Do"), + painted_form("1 0 0", 40), + painted_form("0 1 0", 40), + ], + ); + let expected: Vec<_> = first.into_iter().chain([RED]).collect(); + assert_eq!(events(&bytes).paths, expected, "{scope}"); + } + } + + #[cfg(feature = "raster")] + fn pixels(bytes: &[u8]) -> Vec { + use craft_pdf::raster::{self, Options, RenderCache}; + let pdf = craft_pdf::open_slice(bytes, None).unwrap(); + let image = raster::render(&pdf.pages()[0], &RenderCache::new(), &InterpreterSettings::default(), &Options::default()).unwrap(); + assert!(image.diagnostics.is_empty(), "{:?}", image.diagnostics); + image.bytes().to_vec() + } + + #[cfg(feature = "raster")] + fn center(bytes: &[u8]) -> [u8; 4] { + pixels(bytes)[(20 * 40 + 20) * 4..(20 * 40 + 20) * 4 + 4].try_into().unwrap() + } + + #[cfg(feature = "raster")] + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn type3_procedure_and_font_resources_replace_the_enclosing_scope() { + for (font_scope, procedure_scope, expected) in [ + ("", "", [255, 0, 0, 255]), + ("/Resources << >>", "", [0; 4]), + (GREEN_SCOPE, "", [0, 255, 0, 255]), + (GREEN_SCOPE, "/Resources << >>", [0; 4]), + (GREEN_SCOPE, "/Resources << /ProcSet [/PDF] >>", [0; 4]), + ("/Resources << >>", GREEN_SCOPE, [0, 255, 0, 255]), + ] { + let bytes = page( + "<< /Font << /F1 5 0 R >> /XObject << /Target 6 0 R >> >>", + b"BT /F1 20 Tf 10 10 Td (A) Tj ET", + &[ + format!("<< /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 8 0 R >> {font_scope} >>").into_bytes(), + painted_form("1 0 0", 1), + painted_form("0 1 0", 1), + stream(procedure_scope, b"1 0 d0 /Target Do"), + ], + ); + assert_eq!(center(&bytes), expected, "font={font_scope}, procedure={procedure_scope}"); + } + } + + #[cfg(feature = "raster")] + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn tiling_patterns_do_not_import_missing_resources_from_the_page() { + for (scope, expected) in + [("", [0; 4]), ("/Resources << >>", [0; 4]), ("/Resources << /ProcSet [/PDF] >>", [0; 4]), (GREEN_SCOPE, [0, 255, 0, 255])] + { + let bytes = page( + "<< /Pattern << /P 5 0 R >> /XObject << /Target 6 0 R >> >>", + b"/Pattern cs /P scn 0 0 40 40 re f", + &[ + stream( + &format!("/Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 40 40] /XStep 40 /YStep 40 {scope}"), + b"/Target Do", + ), + painted_form("1 0 0", 40), + painted_form("0 1 0", 40), + ], + ); + assert_eq!(center(&bytes), expected, "{scope}"); + } + } + + fn fixture_font() -> &'static [u8] { + #[cfg(craft_pdf_unicode_fixtures)] + { + INTER + } + #[cfg(not(craft_pdf_unicode_fixtures))] + { + panic!("enable the hash-verified fonts.json fixtures") + } + } + + fn text_pdf(content: &str) -> Vec { + assemble(&[ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 60] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> /ExtGState << /G << /ca 0.25 /CA 0.75 >> >> >> >>".to_vec(), + stream("", content.as_bytes()), + b"<< /Type /Font /Subtype /TrueType /BaseFont /Inter-Regular /Encoding /WinAnsiEncoding /FirstChar 65 /LastChar 67 /Widths [600 600 600] /FontDescriptor 6 0 R >>".to_vec(), + b"<< /Type /FontDescriptor /FontName /Inter-Regular /Flags 32 /FontFile2 7 0 R >>".to_vec(), + stream("", fixture_font()), + ]) + } + + #[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 filled_stroked_tj_runs_preserve_paint_order_alpha_and_text_advances() { + let content = |mode| format!("/G gs 1 0 0 rg 0 0 1 RG 2 w BT /F1 20 Tf {mode} Tr 1 0 0 1 10 20 Tm [(A) 400 (B)] TJ (C) Tj ET"); + for mode in [2, 6] { + let result = events(&text_pdf(&content(mode))); + let expected = [('F', "A", 10.0), ('F', "B", 14.0), ('S', "A", 10.0), ('S', "B", 14.0), ('F', "C", 26.0), ('S', "C", 26.0)]; + assert_eq!(result.glyphs.len(), expected.len()); + for (glyph, (paint, text, x)) in result.glyphs.iter().zip(expected) { + assert_eq!((glyph.mode, glyph.text.as_str()), (paint, text), "{mode} Tr: {:?}", result.glyphs); + assert!((glyph.origin.x - x).abs() < 1e-6 && (glyph.origin.y - 20.0).abs() < 1e-6, "{glyph:?}"); + assert_eq!(glyph.rgba, if paint == 'F' { [1.0, 0.0, 0.0, 64.0 / 255.0] } else { [0.0, 0.0, 1.0, 191.0 / 255.0] }); + } + if mode == 6 { + assert_eq!(result.clips, events(&text_pdf(&content(4))).clips, "stroking must not duplicate text clipping outlines"); + } + } + } + + #[cfg(feature = "raster")] + #[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 overlapping_translucent_glyphs_fill_the_whole_run_before_stroking() { + let reference = pixels(&text_pdf( + "/G gs 1 0 0 rg 0 0 1 RG 2 w BT /F1 20 Tf 0 Tr 1 0 0 1 10 20 Tm [(A) 400 (B)] TJ 1 Tr 1 0 0 1 10 20 Tm [(A) 400 (B)] TJ ET", + )); + assert!(reference.as_chunks::<4>().0.iter().any(|p| p[0] > 0 && p[2] > 0 && p[3] < 255)); + for mode in [2, 6] { + let actual = pixels(&text_pdf(&format!("/G gs 1 0 0 rg 0 0 1 RG 2 w BT /F1 20 Tf {mode} Tr 1 0 0 1 10 20 Tm [(A) 400 (B)] TJ ET"))); + assert!(actual == reference, "{mode} Tr must match explicit whole-run fill followed by whole-run stroke"); + } + } + + #[cfg(feature = "raster")] + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + fn soft_mask_transfer_endpoints_use_rounded_output_bytes() { + for (low, high, left, right) in [("0", "0", 0, 0), ("0.5", "0.5", 128, 128), ("1", "1", 255, 255), ("0.5001", "0.5", 128, 128)] { + let bytes = page( + "<< /ExtGState << /G 5 0 R >> >>", + b"/G gs 0 0 1 rg 0 0 40 40 re f", + &[ + b"<< /SMask << /S /Luminosity /G 6 0 R /TR 7 0 R >> >>".to_vec(), + stream( + "/Type /XObject /Subtype /Form /BBox [0 0 40 40] /Group << /S /Transparency /CS /DeviceGray >>", + b"0 g 0 0 20 40 re f 1 g 20 0 20 40 re f", + ), + format!("<< /FunctionType 2 /Domain [0 1] /C0 [{low}] /C1 [{high}] /N 1 >>").into_bytes(), + ], + ); + let rendered = pixels(&bytes); + for (x, alpha) in [(10, left), (30, right)] { + assert_eq!(&rendered[(20 * 40 + x) * 4..(20 * 40 + x) * 4 + 4], &[0, 0, alpha, alpha], "C0={low}, C1={high}, x={x}"); + } + } + } +} 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..f203a7a9 --- /dev/null +++ b/crates/pdf/tests/upstream_reading.rs @@ -0,0 +1,157 @@ +//! 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, + glyph_ids: 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 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; + self.glyph_ids.push(outline.glyph_id().to_u32()); + 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 interpreted_text(flags: u8, encoding: &str, unicode_map: Option<&str>, codes: &[u8]) -> TextDevice { + 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 +} + +fn encoded_text(flags: u8, encoding: &str, unicode_map: Option<&str>, codes: &[u8]) -> String { + interpreted_text(flags, encoding, unicode_map, codes).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"), "ฮฉ"); + } +} + +#[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 unicode_fallback_preserves_the_existing_nonsymbolic_glyph_selection() { + let fallback = interpreted_text(32, "", None, b"\xe9\xfc"); + let standard = interpreted_text(32, "/Encoding /StandardEncoding", None, b"\xe9\xfc"); + let ansi = interpreted_text(32, "/Encoding /WinAnsiEncoding", None, b"\xe9\xfc"); + assert_eq!(fallback.text, "รฉรผ"); + // The upstream port repairs extraction only. With no encoding, drawing still uses + // StandardEncoding: Oslash for 0xE9 and .notdef for 0xFC. Do not claim a rendering fix. + assert_eq!(fallback.glyph_ids, standard.glyph_ids); + assert_ne!(fallback.glyph_ids[0], 0); + assert_eq!(fallback.glyph_ids[1], 0); + assert_eq!(ansi.text, "รฉรผ"); + assert!(ansi.glyph_ids.iter().all(|id| *id != 0)); + assert_ne!(fallback.glyph_ids[0], ansi.glyph_ids[0]); +} + +#[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 unicode_fallback_uses_the_upstream_win_ansi_bullet_mapping() { + // The upstream fallback uses the complete WinAnsi mapping, including its unused-code + // bullet rule. This is a fallback policy, not proof of the original author's text. + for flags in [4, 32] { + assert_eq!(encoded_text(flags, "", None, b"\x81"), "โ€ข"); + assert_eq!(encoded_text(flags, "/Encoding /WinAnsiEncoding", None, b"\x81"), "โ€ข"); + } +} 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-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/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/scripts/fetch-pdf-test-fonts.py b/scripts/fetch-pdf-test-fonts.py new file mode 100644 index 00000000..821a5e74 --- /dev/null +++ b/scripts/fetch-pdf-test-fonts.py @@ -0,0 +1,57 @@ +#!/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 time +import urllib.error +import urllib.request + + +def fetch(url): + for attempt in range(3): + try: + with urllib.request.urlopen(url, timeout=60) as response: + return response.read(16 * 1024 * 1024 + 1) + except urllib.error.HTTPError as error: + if error.code not in (408, 429) and not 500 <= error.code <= 599: + raise + if attempt == 2: + raise + error.close() + except (urllib.error.URLError, TimeoutError, ConnectionError): + if attempt == 2: + raise + time.sleep(attempt + 1) + raise RuntimeError("fixture download retries exhausted") + + +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 + data = fetch(url) + 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-fetch-pdf-test-fonts.py b/scripts/test-fetch-pdf-test-fonts.py new file mode 100644 index 00000000..94531e62 --- /dev/null +++ b/scripts/test-fetch-pdf-test-fonts.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Exercise fixture-download retries without network access.""" + +import importlib.util +import io +from pathlib import Path +import sys +import unittest +from unittest.mock import patch +import urllib.error + +sys.dont_write_bytecode = True +spec = importlib.util.spec_from_file_location("fetch_pdf_fonts", Path(__file__).with_name("fetch-pdf-test-fonts.py")) +fetcher = importlib.util.module_from_spec(spec) +spec.loader.exec_module(fetcher) +URL = "https://example.invalid/font.ttf" + + +class FetchTests(unittest.TestCase): + def test_success_needs_no_retry(self): + with patch.object(fetcher.urllib.request, "urlopen", return_value=io.BytesIO(b"font")) as request: + with patch.object(fetcher.time, "sleep") as sleep: + self.assertEqual(fetcher.fetch(URL), b"font") + request.assert_called_once_with(URL, timeout=60) + sleep.assert_not_called() + + def test_transient_failures_retry_then_succeed(self): + errors = [ + urllib.error.HTTPError(URL, 429, "rate limit", {}, None), + urllib.error.HTTPError(URL, 503, "unavailable", {}, None), + urllib.error.URLError("temporary DNS failure"), + TimeoutError("timed out"), + ConnectionResetError("reset"), + ] + for error in errors: + if isinstance(error, urllib.error.HTTPError): + self.addCleanup(error.close) + with self.subTest(error=error): + with patch.object(fetcher.urllib.request, "urlopen", side_effect=[error, io.BytesIO(b"font")]) as request: + with patch.object(fetcher.time, "sleep") as sleep: + self.assertEqual(fetcher.fetch(URL), b"font") + self.assertEqual(request.call_count, 2) + sleep.assert_called_once_with(1) + + def test_permanent_http_error_is_not_retried(self): + error = urllib.error.HTTPError(URL, 404, "missing", {}, None) + self.addCleanup(error.close) + with patch.object(fetcher.urllib.request, "urlopen", side_effect=error) as request: + with patch.object(fetcher.time, "sleep") as sleep: + with self.assertRaises(urllib.error.HTTPError): + fetcher.fetch(URL) + self.assertEqual(request.call_count, 1) + sleep.assert_not_called() + + def test_transient_failure_stops_after_three_attempts(self): + error = urllib.error.URLError("unavailable") + with patch.object(fetcher.urllib.request, "urlopen", side_effect=error) as request: + with patch.object(fetcher.time, "sleep") as sleep: + with self.assertRaises(urllib.error.URLError): + fetcher.fetch(URL) + self.assertEqual(request.call_count, 3) + self.assertEqual([call.args[0] for call in sleep.call_args_list], [1, 2]) + + def test_response_read_remains_bounded(self): + class Response(io.BytesIO): + def read(self, size=-1): + self.size = size + return super().read(size) + + response = Response(b"font") + with patch.object(fetcher.urllib.request, "urlopen", return_value=response): + self.assertEqual(fetcher.fetch(URL), b"font") + self.assertEqual(response.size, 16 * 1024 * 1024 + 1) + + +if __name__ == "__main__": + unittest.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..704ce25d --- /dev/null +++ b/vendor/README.craft-reader.md @@ -0,0 +1,58 @@ +# 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. + +Compatibility patches follow the resource scopes and text-showing order in +[Hayro 0.8.0, revision ea9c81dc70cde40c64205986fdcfcd481fe934d8](https://github.com/LaurenzV/hayro/tree/ea9c81dc70cde40c64205986fdcfcd481fe934d8), +under the same MIT OR Apache-2.0 licence. Explicit page and nested-object resource +dictionaries replace the enclosing dictionary as a whole. Missing Form resources inherit +the caller; tiling patterns use only their own resources. Direct arrays and dictionaries +are limited to 64 levels in raw and decoded content. Page-tree traversal is iterative, +retains cycle detection and inheritance, and fails closed when its 100,000-entry work +budget or a fallible frame/page allocation fails. Text-showing operations fill the +whole run before stroking it, including strings separated by adjustments in `TJ`. +The implementation replays bounded source operands without allocating a glyph buffer. +The existing Type 3 single-paint behavior and incomplete-content diagnostics remain. +Raw array iteration now ends safely if validation and object parsing disagree; +standalone sign tokens retain the reader's zero tolerance. +Byte transfer-function samples use rounded 8-bit values; failed or non-finite evaluations +leave the sample unchanged. Historical source hashes remain in each `SOURCE.json`, +alongside local hashes and the compatibility reference. + +This remains the patched 0.7 backend. It retains the existing Bradford Lab conversion, +floating component precision and codec versions, rather than claiming full 0.8 +equivalence. The independent color oracles, backend tests and facade contracts cover +these retained policies separately from the compatibility changes. + +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..27811044 --- /dev/null +++ b/vendor/hayro-interpret/SOURCE.json @@ -0,0 +1,468 @@ +{ + "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": "bc9b67008e8f7e5ba7bd96a9b49b6ec5d2818eedc44e003212342d8bc259cc36" + }, + { + "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": "5bd99e9a219c4ed437f964b3b81e10381ad4f0bfdee8b2f9be1a6d3940fcd9b4" + }, + { + "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": "53a0000544aa56cc753d02ccdead174f61cfae2ff92ac60d87b5144ff4232115" + }, + { + "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": "6ae30906c8e6d0e869e0fac8ece4d97e0ac08d8abd7042c0242a069d4e609351" + }, + { + "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": "239ca1ac3ad2a34bfce3d70935ea3483dbb5c5a689c44cd7c5a22693dab51b75" + }, + { + "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": "24ac7a1cfd9a55b7d40d572120f52743bd390fab0007603aec64034ae7d2fd23" + }, + { + "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": "780ffbf73567a7ae33f2fb673676840e0a29cb6b59f1d8b625034f42c4d17a22" + }, + { + "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": "93539c173b193aa740aa7a839ec05c9676a89cbee404726767cf82b63f19bda6" + } + ], + "compatibility_reference": { + "repository": "https://github.com/LaurenzV/hayro", + "revision": "ea9c81dc70cde40c64205986fdcfcd481fe934d8", + "version": "0.8.0", + "license": "Apache-2.0 OR MIT", + "archive_sha256": "ac9df074ff163a63864cec9b0bf640ccbff8abf4e44481df7369ce0ac172ffbe", + "reference_files": [ + "src/x_object/form.rs", + "src/font/type3.rs", + "src/pattern.rs", + "src/interpret/text.rs", + "src/interpret/mod.rs", + "src/function/mod.rs", + "src/color/mod.rs" + ], + "changes": [ + "Use whole-dictionary resource scopes for forms, Type 3 procedures and tiling patterns.", + "Paint the entire Tj or TJ fill pass before its stroke pass by bounded source replay, retaining the existing Device API.", + "Add rounded byte transfer evaluation with identity fallback on failed or non-finite samples." + ], + "preserved": [ + "Existing Type 3 single-paint rendering-mode behavior, font handling, incomplete-content diagnostics and interpreter budgets.", + "Existing Bradford Lab conversion and full-precision calibrated and tint color conversion; no LAB.icc asset or broad color backend replacement." + ] + }, + "local_test_updates": [ + "Adjust direct stitching-function fixtures to the 32/33-function boundary imposed by the 64-container syntax cap and independently test unchanged 64/65-function construction limits with indirect references." + ] +} 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 00000000..b5a73495 Binary files /dev/null and b/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc differ 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 00000000..30d52963 Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitDingbats.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitFixed.pfb b/vendor/hayro-interpret/assets/FoxitFixed.pfb new file mode 100644 index 00000000..f12dcbce Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitFixed.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitFixedBold.pfb b/vendor/hayro-interpret/assets/FoxitFixedBold.pfb new file mode 100644 index 00000000..cf8e24ae Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitFixedBold.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb b/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb new file mode 100644 index 00000000..d2880017 Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb b/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb new file mode 100644 index 00000000..d71697d4 Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSans.pfb b/vendor/hayro-interpret/assets/FoxitSans.pfb new file mode 100644 index 00000000..37f244bd Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSans.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSansBold.pfb b/vendor/hayro-interpret/assets/FoxitSansBold.pfb new file mode 100644 index 00000000..affcf316 Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSansBold.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb b/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb new file mode 100644 index 00000000..e1f60b75 Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSansItalic.pfb b/vendor/hayro-interpret/assets/FoxitSansItalic.pfb new file mode 100644 index 00000000..c04b0a5a Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSansItalic.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSerif.pfb b/vendor/hayro-interpret/assets/FoxitSerif.pfb new file mode 100644 index 00000000..3fa682ef Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSerif.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSerifBold.pfb b/vendor/hayro-interpret/assets/FoxitSerifBold.pfb new file mode 100644 index 00000000..ff7c6dde Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSerifBold.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb b/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb new file mode 100644 index 00000000..460231fb Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb b/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb new file mode 100644 index 00000000..d03a7c78 Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb differ diff --git a/vendor/hayro-interpret/assets/FoxitSymbol.pfb b/vendor/hayro-interpret/assets/FoxitSymbol.pfb new file mode 100644 index 00000000..c8f9bca7 Binary files /dev/null and b/vendor/hayro-interpret/assets/FoxitSymbol.pfb differ diff --git a/vendor/hayro-interpret/assets/LICENSE_FOXIT b/vendor/hayro-interpret/assets/LICENSE_FOXIT new file mode 100644 index 00000000..8b4ed6dd --- /dev/null +++ b/vendor/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/vendor/hayro-interpret/assets/README.md b/vendor/hayro-interpret/assets/README.md new file mode 100644 index 00000000..3f5e8d4c --- /dev/null +++ b/vendor/hayro-interpret/assets/README.md @@ -0,0 +1,13 @@ +The CMYK profile is from: https://github.com/saucecontrol/Compact-ICC-Profiles?tab=readme-ov-file#cmyk, available under the CC0-1.0 License + +The pfb files in this directory were extracted from Pdfium + +Original copyright notice: + +``` +Copyright 2014 PDFium Authors. All rights reserved. + Use of this source code is governed by a BSD-style license that can be + found in the LICENSE file. + +Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com +``` \ No newline at end of file diff --git a/vendor/hayro-interpret/examples/extract_html.rs b/vendor/hayro-interpret/examples/extract_html.rs new file mode 100644 index 00000000..8e5933b1 --- /dev/null +++ b/vendor/hayro-interpret/examples/extract_html.rs @@ -0,0 +1,122 @@ +//! A small example that shows how you can extract unicode characters from +//! glyphs. Please note that Unicode extraction is still experimental, so it +//! might not work fully correctly in certain cases. Note also that generating +//! div elements for every single character is clearly not desirable and there +//! should be some word/sentence merging algorithm in-place, but this is +//! out-of-scope for this example. + +use hayro_interpret::font::Glyph; +use hayro_interpret::{ + BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, + InterpreterSettings, Paint, PathDrawMode, SoftMask, interpret_page, +}; +use hayro_syntax::Pdf; + +use std::fmt::Write; + +use hayro_cmap::BfString; +use kurbo::{Affine, BezPath, Point, Rect}; +use std::path::PathBuf; + +fn main() { + let args: Vec = std::env::args().collect(); + let relative_path = args + .get(1) + .expect("Please provide a relative path to the PDF file as the first argument"); + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative_path); + let data = std::fs::read(path).unwrap(); + + let pdf = Pdf::new(data).unwrap(); + + let settings = InterpreterSettings::default(); + let cache = InterpreterCache::new(); + // Pass dummy values for bbox and initial transform, since we don't care about those. + let mut context = Context::new( + Affine::IDENTITY, + Rect::new(0.0, 0.0, 1.0, 1.0), + &cache, + pdf.xref(), + settings, + ); + + // Run everything! + let page = &pdf.pages()[0]; + + let mut extractor = TextExtractor { + dimensions: page.render_dimensions(), + ..Default::default() + }; + + writeln!(extractor.text, " ").unwrap(); + writeln!(extractor.text, "", 0).unwrap(); + writeln!( + extractor.text, + "
", + 0, + extractor.dimensions.0, + extractor.dimensions.1 + ).unwrap(); + + interpret_page(page, &mut context, &mut extractor); + + writeln!(extractor.text, "
").unwrap(); + + print!("{}", extractor.text); +} + +#[derive(Default)] +struct TextExtractor { + text: String, + dimensions: (f32, f32), +} + +/// Implement `Device` for `TextExtractor`. We extract Unicode text from glyphs. +impl Device<'_> for TextExtractor { + fn set_soft_mask(&mut self, _: Option>) {} + + 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<'_>, + transform: Affine, + glyph_transform: Affine, + _: &Paint<'_>, + _: &GlyphDrawMode, + ) { + if let Some(unicode_char) = glyph.as_unicode() { + // Apply vertical flip transformation to combined transform + // to place origin at top-left corner. + let flip_transform = Affine::translate((0.0, self.dimensions.1 as f64)) + * Affine::scale_non_uniform(1.0, -1.0); + let transform = flip_transform * transform * glyph_transform; + + let point = Point::new(0.0, 0.0); + let position = transform * point; + + writeln!( + self.text, + "
{}
", + position.x, position.y, 6, match unicode_char { + BfString::Char(c) => c.to_string(), + BfString::String(s) => s + } + ).unwrap(); + } else { + // Fallback for glyphs without Unicode mapping. + self.text.push('๏ฟฝ'); + } + } + + fn pop_clip_path(&mut self) {} + + fn pop_transparency_group(&mut self) {} + + fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {} + + fn set_blend_mode(&mut self, _: BlendMode) {} +} diff --git a/vendor/hayro-interpret/examples/extract_images.rs b/vendor/hayro-interpret/examples/extract_images.rs new file mode 100644 index 00000000..07e44061 --- /dev/null +++ b/vendor/hayro-interpret/examples/extract_images.rs @@ -0,0 +1,147 @@ +//! This example demonstrates how you can extract all images used on a page and save them as +//! PNG. +//! +//! Note that you must have downloaded the corresponding PDF file for the example to work. + +use hayro_interpret::font::Glyph; +use hayro_interpret::{ + BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, ImageData, InterpreterCache, + InterpreterSettings, Paint, PathDrawMode, SoftMask, interpret_page, +}; +use hayro_syntax::Pdf; +use image::{DynamicImage, ImageBuffer}; +use kurbo::{Affine, BezPath, Rect}; +use std::path::PathBuf; + +fn main() { + let data = std::fs::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../hayro-tests/pdfs/custom/image_rgb8.pdf"), + ) + .unwrap(); + + let pdf = Pdf::new(data).unwrap(); + + let mut extractor = ImageExtractor::new(); + let settings = InterpreterSettings::default(); + let cache = InterpreterCache::new(); + // Pass dummy values for bbox and initial transform, since we don't care about those. + let mut context = Context::new( + Affine::IDENTITY, + Rect::new(0.0, 0.0, 1.0, 1.0), + &cache, + pdf.xref(), + settings, + ); + + // Run everything! + let page = &pdf.pages()[0]; + interpret_page(page, &mut context, &mut extractor); + + // Then just save all of the images! + for (idx, img) in extractor.0.iter().enumerate() { + img.save(format!("image_{idx}.png")).unwrap(); + } +} + +struct ImageExtractor(Vec); + +impl ImageExtractor { + fn new() -> Self { + Self(Vec::new()) + } +} + +/// Implement `Device` for `ImageExtractor`. We can ignore most operations and only +/// need to implement `draw_rgba_image` and `draw_stencil_image`. +impl Device<'_> for ImageExtractor { + fn set_soft_mask(&mut self, _: Option>) {} + + 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<'_>, + _: Affine, + _: Affine, + _: &Paint<'_>, + _: &GlyphDrawMode, + ) { + } + + fn pop_clip_path(&mut self) {} + + fn pop_transparency_group(&mut self) {} + + fn draw_image(&mut self, image: Image<'_, '_>, _: Affine) { + match image { + Image::Stencil(s) => { + s.with_stencil( + |stencil, _paint| { + // Stencil images are gray-channel images that should be painted using the color stored in + // `paint`. For simplicity, we just store them as gray-channel for now. + self.0.push(DynamicImage::ImageLuma8( + ImageBuffer::from_raw( + stencil.width, + stencil.height, + stencil.data.clone(), + ) + .unwrap(), + )); + }, + None, + ); + } + Image::Raster(r) => { + // The alpha and RGB channels are provided separately. + r.with_rgba( + |image, alpha| { + let (rgb_data, width, height) = match image { + ImageData::Rgb(rgb) => (rgb.data, rgb.width, rgb.height), + ImageData::Luma(luma) => { + let rgb = luma + .data + .iter() + .flat_map(|g| [*g, *g, *g]) + .collect::>(); + (rgb, luma.width, luma.height) + } + }; + + let image = if let Some(alpha) = alpha { + // This is not complete, as it can in theory happen that the alpha channel has a different + // dimension than the RGB channel. We ignore this edge case for this example. + if alpha.width == width && alpha.height == height { + let interleaved = rgb_data + .chunks(3) + .zip(alpha.data) + .flat_map(|(rgb, a)| [rgb[0], rgb[1], rgb[2], a]) + .collect::>(); + + DynamicImage::ImageRgba8( + ImageBuffer::from_raw(width, height, interleaved).unwrap(), + ) + } else { + DynamicImage::ImageRgb8( + ImageBuffer::from_raw(width, height, rgb_data).unwrap(), + ) + } + } else { + DynamicImage::ImageRgb8( + ImageBuffer::from_raw(width, height, rgb_data).unwrap(), + ) + }; + + self.0.push(image); + }, + None, + ); + } + } + } + + fn set_blend_mode(&mut self, _: BlendMode) {} +} diff --git a/vendor/hayro-interpret/src/cache.rs b/vendor/hayro-interpret/src/cache.rs new file mode 100644 index 00000000..2dda2e8a --- /dev/null +++ b/vendor/hayro-interpret/src/cache.rs @@ -0,0 +1,279 @@ +use crate::util::hash128; +use hayro_syntax::object::{Array, Dict, MaybeRef, Name, Null, ObjRef, Object, Stream}; +use kurbo::Affine; +use std::any::Any; +use std::collections::HashMap; +use std::collections::hash_map::Entry; +use std::sync::{Arc, Mutex}; + +type CacheMap = HashMap>>; +#[derive(Clone)] +pub(crate) struct Cache(Arc>); +impl Default for Cache { + fn default() -> Self { + Self::new() + } +} + +impl Cache { + pub(crate) fn new() -> Self { + Self(Arc::new(Mutex::new(HashMap::new()))) + } + + pub(crate) fn get_or_insert_with( + &self, + id: u128, + f: impl FnOnce() -> Option, + ) -> Option { + let mut locked = self.0.lock().unwrap(); + + // We can't use `get_or_insert_with` here, because if the closure makes another access to the + // cache, we end up with a deadlock. + match locked.entry(id) { + Entry::Occupied(o) => o + .get() + .as_ref() + .and_then(|val| val.downcast_ref::().cloned()), + Entry::Vacant(_) => { + drop(locked); + let val = f(); + self.0.lock().unwrap().insert( + id, + val.clone() + .map(|val| Box::new(val) as Box), + ); + + val + } + } + } +} + +/// PdfCraft patch: a byte-budgeted cache of decoded images, keyed by everything a decode reads +/// ([`DecodedImageKey`]). Image-heavy pages decode thousands of images; without this every tile +/// and every scale decodes them all again. Entries hold owned pixels, so a hit never changes +/// them; when an insert would pass the budget the whole map is dropped (a fresh epoch), which +/// only costs re-decoding, and an image larger than the whole budget is never kept. The budget +/// caps memory per interpreter cache. A poisoned lock decodes without the cache. +#[derive(Clone)] +pub(crate) struct DecodedImageCache { + inner: Arc>, + /// Whether the render this handle serves is a preview (`InterpreterSettings::preview`). + /// Part of every key, so a preview's entries are never handed to a final render. + pub(crate) preview: bool, +} + +/// PdfCraft patch: what a decoded image depends on. Two images share an entry only when all of +/// it is equal: +/// - where the stream's dictionary lies in the document's data (`dict_at`, `dict_len`): a +/// stream's data follows its dictionary, so this tells apart streams whose dictionaries are +/// byte for byte the same (two uncompressed images of one size), which `dict_hash` alone +/// would not; the data stays put for as long as the document (and so this cache) lives; +/// - the dictionary's bytes (`dict_hash`: `/Decode`, `/ColorSpace`, `/SMask`, `/Mask`, +/// `/BitsPerComponent`, filters, โ€ฆ) and its object (`obj`: the decryption key depends on it); +/// - whether it is decoded as a mask, a stencil mask, raster or mask output, interpolation, +/// the decode target size, and preview or final quality. +/// +/// A transfer function (graphics state `/TR`) also changes the pixels and has no cheap key, so +/// an image drawn under one is not cached; neither is an inline image, whose dictionary lives in +/// a content stream and whose colour space can be a name resolved through the resources. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) struct DecodedImageKey { + pub(crate) mask_output: bool, + pub(crate) dict_at: usize, + pub(crate) dict_len: usize, + pub(crate) dict_hash: u128, + pub(crate) obj: Option<(i32, i32)>, + pub(crate) is_mask: bool, + pub(crate) is_stencil_mask: bool, + pub(crate) interpolate: bool, + pub(crate) target: Option<(u32, u32)>, + pub(crate) preview: bool, +} + +#[derive(Default)] +struct DecodedImageCacheInner { + map: HashMap>>, + bytes: usize, +} + +impl DecodedImageCache { + pub(crate) fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(DecodedImageCacheInner::default())), + preview: false, + } + } + + /// The same cache, for a preview (`true`) or a final render. + pub(crate) fn for_render(&self, preview: bool) -> Self { + Self { + inner: self.inner.clone(), + preview, + } + } + + /// The decoded image for `key`, from the cache or from `f`. `None` as the key decodes + /// without the cache. + pub(crate) fn get_or_insert_with( + &self, + key: Option, + budget: usize, + size_of: impl FnOnce(&T) -> usize, + f: impl FnOnce() -> Option, + ) -> Option { + let Some(key) = key else { return f() }; + // Fast path: a hit clones the pixels without decoding. + let hit = match self.inner.lock() { + Ok(locked) => locked + .map + .get(&key) + .and_then(|val| val.as_ref()) + .and_then(|val| val.downcast_ref::().cloned()), + // A thread panicked while holding the lock: decode without the cache. + Err(_) => return f(), + }; + if let Some(hit) = hit { + return Some(hit); + } + // Miss: decode without holding the lock (decoding must never deadlock the cache). + let val = f(); + let Ok(mut locked) = self.inner.lock() else { + return val; + }; + // Another thread may have decoded it meanwhile: keep the first copy. + if let Some(existing) = locked.map.get(&key) + && let Some(existing) = existing.as_ref().and_then(|val| val.downcast_ref::()) + { + return Some(existing.clone()); + } + let size = match &val { + Some(v) => size_of(v), + None => 0, + }; + if size > budget { + return val; + } + if locked.bytes.saturating_add(size) > budget { + locked.map.clear(); + locked.bytes = 0; + } + locked.bytes = locked.bytes.saturating_add(size); + locked.map.insert( + key, + val.clone() + .map(|val| Box::new(val) as Box), + ); + val + } +} + +/// A trait for objects that can generate a unique cache key. +pub trait CacheKey { + /// Returns the cache key for this object. + fn cache_key(&self) -> u128; +} + +impl CacheKey for (T, U) { + fn cache_key(&self) -> u128 { + hash128(&(self.0.cache_key(), self.1.cache_key())) + } +} + +impl CacheKey for Dict<'_> { + fn cache_key(&self) -> u128 { + hash128(self.data()) + } +} + +impl CacheKey for Stream<'_> { + fn cache_key(&self) -> u128 { + self.dict().cache_key() + } +} + +impl CacheKey for Null { + fn cache_key(&self) -> u128 { + hash128(self) + } +} + +impl CacheKey for bool { + fn cache_key(&self) -> u128 { + hash128(self) + } +} + +impl CacheKey for hayro_syntax::object::Number { + fn cache_key(&self) -> u128 { + hash128(&self.as_f64().to_bits()) + } +} + +impl CacheKey for hayro_syntax::object::String<'_> { + fn cache_key(&self) -> u128 { + hash128(self.as_ref()) + } +} + +impl CacheKey for Name<'_> { + fn cache_key(&self) -> u128 { + hash128(self) + } +} + +impl CacheKey for Array<'_> { + fn cache_key(&self) -> u128 { + hash128(self.data()) + } +} + +impl CacheKey for Object<'_> { + fn cache_key(&self) -> u128 { + match self { + Object::Null(n) => n.cache_key(), + Object::Boolean(b) => b.cache_key(), + Object::Number(n) => n.cache_key(), + Object::String(s) => s.cache_key(), + Object::Name(n) => n.cache_key(), + Object::Dict(d) => d.cache_key(), + Object::Array(a) => a.cache_key(), + Object::Stream(s) => s.cache_key(), + } + } +} + +impl CacheKey for ObjRef { + fn cache_key(&self) -> u128 { + hash128(self) + } +} + +impl CacheKey for MaybeRef { + fn cache_key(&self) -> u128 { + match self { + Self::Ref(r) => r.cache_key(), + Self::NotRef(o) => o.cache_key(), + } + } +} + +impl CacheKey for Affine { + fn cache_key(&self) -> u128 { + let c = self.as_coeffs(); + hash128(&[ + c[0].to_bits(), + c[1].to_bits(), + c[2].to_bits(), + c[3].to_bits(), + c[4].to_bits(), + c[5].to_bits(), + ]) + } +} + +impl CacheKey for u128 { + fn cache_key(&self) -> u128 { + hash128(self) + } +} diff --git a/vendor/hayro-interpret/src/color.rs b/vendor/hayro-interpret/src/color.rs new file mode 100644 index 00000000..4db84631 --- /dev/null +++ b/vendor/hayro-interpret/src/color.rs @@ -0,0 +1,1205 @@ +//! PDF colors and color spaces. + +use crate::cache::{Cache, CacheKey}; +use crate::function::Function; +use hayro_syntax::object; +use hayro_syntax::object::Array; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Name; +use hayro_syntax::object::Object; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::*; +use moxcms::{ + ColorProfile, DataColorSpace, Layout, Transform8BitExecutor, TransformF32Executor, + TransformOptions, +}; +use smallvec::{SmallVec, ToSmallVec, smallvec}; +use std::borrow::Cow; +use std::fmt::{Debug, Formatter}; +use std::ops::Deref; +use std::sync::{Arc, LazyLock, OnceLock}; + +/// A storage for the components of colors. +pub type ColorComponents = SmallVec<[f32; 4]>; + +/// An RGB color with an alpha channel. +#[derive(Debug, Copy, Clone)] +pub struct AlphaColor { + components: [f32; 4], +} + +impl AlphaColor { + /// A black color. + pub const BLACK: Self = Self::new([0., 0., 0., 1.]); + + /// A transparent color. + pub const TRANSPARENT: Self = Self::new([0., 0., 0., 0.]); + + /// A white color. + pub const WHITE: Self = Self::new([1., 1., 1., 1.]); + + /// Create a new color from the given components. + pub const fn new(components: [f32; 4]) -> Self { + Self { components } + } + + /// Create a new color from RGB8 values. + pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self { + let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), 1.]; + Self::new(components) + } + + /// Return the color as premulitplied RGBF32. + pub fn premultiplied(&self) -> [f32; 4] { + [ + self.components[0] * self.components[3], + self.components[1] * self.components[3], + self.components[2] * self.components[3], + self.components[3], + ] + } + + /// Create a new color from RGBA8 values. + pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self { + let components = [u8_to_f32(r), u8_to_f32(g), u8_to_f32(b), u8_to_f32(a)]; + Self::new(components) + } + + /// Return the color as RGBA8. + pub fn to_rgba8(&self) -> [u8; 4] { + [ + (self.components[0] * 255.0 + 0.5) as u8, + (self.components[1] * 255.0 + 0.5) as u8, + (self.components[2] * 255.0 + 0.5) as u8, + (self.components[3] * 255.0 + 0.5) as u8, + ] + } + + /// Return the components of the color as RGBF32. + pub fn components(&self) -> [f32; 4] { + self.components + } +} + +const fn u8_to_f32(x: u8) -> f32 { + x as f32 * (1.0 / 255.0) +} + +#[derive(Debug, Clone)] +pub(crate) enum ColorSpaceType { + DeviceCmyk, + DeviceGray, + DeviceRgb, + Pattern(ColorSpace), + Indexed(Indexed), + ICCBased(ICCProfile), + CalGray(CalGray), + CalRgb(CalRgb), + Lab(Lab), + Separation(Separation), + DeviceN(DeviceN), +} + +impl ColorSpaceType { + fn new(object: Object<'_>, cache: &Cache) -> Option { + Self::new_inner(object, cache) + } + + fn new_inner(object: Object<'_>, cache: &Cache) -> Option { + if let Object::Name(name) = object { + return Self::new_from_name(&name); + } else if let Object::Array(color_array) = object { + let mut iter = color_array.flex_iter(); + let name = iter.next::>()?; + + match name.deref() { + ICC_BASED => { + let icc_stream = iter.next::>()?; + let dict = icc_stream.dict(); + let num_components = dict.get::(N)?; + + return cache.get_or_insert_with(icc_stream.cache_key(), || { + if let Some(decoded) = icc_stream.decoded().ok().as_ref() { + ICCProfile::new(decoded, num_components) + .map(|icc| { + // TODO: For SVG and PNG we can assume that the output color space is + // sRGB. If we ever implement PDF-to-PDF, we probably want to + // let the user pass the native color type and don't make this optimization + // if it's not sRGB. + if icc.is_srgb() { + Self::DeviceRgb + } else { + Self::ICCBased(icc) + } + }) + .or_else(|| { + dict.get::>(ALTERNATE) + .and_then(|o| Self::new(o, cache)) + }) + .or_else(|| match dict.get::(N) { + Some(1) => Some(Self::DeviceGray), + Some(3) => Some(Self::DeviceRgb), + Some(4) => Some(Self::DeviceCmyk), + _ => None, + }) + } else { + None + } + }); + } + CALCMYK => return Some(Self::DeviceCmyk), + CALGRAY => { + let cal_dict = iter.next::>()?; + return Some(Self::CalGray(CalGray::new(&cal_dict)?)); + } + CALRGB => { + let cal_dict = iter.next::>()?; + return Some(Self::CalRgb(CalRgb::new(&cal_dict)?)); + } + DEVICE_RGB | RGB => return Some(Self::DeviceRgb), + DEVICE_GRAY | G => return Some(Self::DeviceGray), + DEVICE_CMYK | CMYK => return Some(Self::DeviceCmyk), + LAB => { + let lab_dict = iter.next::>()?; + return Some(Self::Lab(Lab::new(&lab_dict)?)); + } + INDEXED | I => { + return Some(Self::Indexed(Indexed::new(&color_array, cache)?)); + } + SEPARATION => { + return Some(Self::Separation(Separation::new(&color_array, cache)?)); + } + DEVICE_N => { + return Some(Self::DeviceN(DeviceN::new(&color_array, cache)?)); + } + PATTERN => { + let _ = iter.next::>(); + let cs = iter + .next::>() + .and_then(|o| ColorSpace::new(o, cache)) + .unwrap_or(ColorSpace::device_rgb()); + return Some(Self::Pattern(cs)); + } + _ => { + warn!("unsupported color space: {}", name.as_str()); + return None; + } + } + } + + None + } + + fn new_from_name(name: &Name<'_>) -> Option { + match name.deref() { + DEVICE_RGB | RGB => Some(Self::DeviceRgb), + DEVICE_GRAY | G => Some(Self::DeviceGray), + DEVICE_CMYK | CMYK => Some(Self::DeviceCmyk), + CALCMYK => Some(Self::DeviceCmyk), + PATTERN => Some(Self::Pattern(ColorSpace::device_rgb())), + _ => None, + } + } +} + +/// A PDF color space. +#[derive(Debug, Clone)] +pub struct ColorSpace(Arc); + +impl ColorSpace { + /// Create a new color space from the given object. + pub(crate) fn new(object: Object<'_>, cache: &Cache) -> Option { + Some(Self(Arc::new(ColorSpaceType::new(object, cache)?))) + } + + /// Create a new color space from the name. + pub(crate) fn new_from_name(name: &Name<'_>) -> Option { + ColorSpaceType::new_from_name(name).map(|c| Self(Arc::new(c))) + } + + /// Return the device gray color space. + pub(crate) fn device_gray() -> Self { + Self(Arc::new(ColorSpaceType::DeviceGray)) + } + + /// Return the device RGB color space. + pub(crate) fn device_rgb() -> Self { + Self(Arc::new(ColorSpaceType::DeviceRgb)) + } + + /// Return the device CMYK color space. + pub(crate) fn device_cmyk() -> Self { + Self(Arc::new(ColorSpaceType::DeviceCmyk)) + } + + /// Return the pattern color space. + pub(crate) fn pattern() -> Self { + Self(Arc::new(ColorSpaceType::Pattern(Self::device_gray()))) + } + + pub(crate) fn pattern_cs(&self) -> Option { + match self.0.as_ref() { + ColorSpaceType::Pattern(cs) => Some(cs.clone()), + _ => None, + } + } + + /// Return `true` if the current color space is the pattern color space. + pub(crate) fn is_pattern(&self) -> bool { + matches!(self.0.as_ref(), ColorSpaceType::Pattern(_)) + } + + /// Return `true` if the current color space is an indexed color space. + pub(crate) fn is_indexed(&self) -> bool { + matches!(self.0.as_ref(), ColorSpaceType::Indexed(_)) + } + + /// Get the default decode array for the color space. + pub(crate) fn default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> { + match self.0.as_ref() { + ColorSpaceType::DeviceCmyk => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0), (0.0, 1.0)], + ColorSpaceType::DeviceGray => smallvec![(0.0, 1.0)], + ColorSpaceType::DeviceRgb => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)], + ColorSpaceType::ICCBased(i) => smallvec![(0.0, 1.0); i.0.number_components], + ColorSpaceType::CalGray(_) => smallvec![(0.0, 1.0)], + ColorSpaceType::CalRgb(_) => smallvec![(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)], + ColorSpaceType::Lab(l) => smallvec![ + (0.0, 100.0), + (l.range[0], l.range[1]), + (l.range[2], l.range[3]), + ], + ColorSpaceType::Indexed(_) => smallvec![(0.0, 2.0_f32.powf(n) - 1.0)], + ColorSpaceType::Separation(_) => smallvec![(0.0, 1.0)], + ColorSpaceType::DeviceN(d) => smallvec![(0.0, 1.0); d.num_components as usize], + // Not a valid image color space. + ColorSpaceType::Pattern(_) => smallvec![(0.0, 1.0)], + } + } + + pub(crate) fn inverted_default_decode_arr(&self, n: f32) -> SmallVec<[(f32, f32); 4]> { + self.default_decode_arr(n) + .iter() + .map(|(min, max)| (*max, *min)) + .collect() + } + + /// Get the initial color of the color space. + pub(crate) fn initial_color(&self) -> ColorComponents { + match self.0.as_ref() { + ColorSpaceType::DeviceCmyk => smallvec![0.0, 0.0, 0.0, 1.0], + ColorSpaceType::DeviceGray => smallvec![0.0], + ColorSpaceType::DeviceRgb => smallvec![0.0, 0.0, 0.0], + ColorSpaceType::ICCBased(icc) => match icc.0.number_components { + 1 => smallvec![0.0], + 3 => smallvec![0.0, 0.0, 0.0], + 4 => smallvec![0.0, 0.0, 0.0, 1.0], + _ => unreachable!(), + }, + ColorSpaceType::CalGray(_) => smallvec![0.0], + ColorSpaceType::CalRgb(_) => smallvec![0.0, 0.0, 0.0], + ColorSpaceType::Lab(_) => smallvec![0.0, 0.0, 0.0], + ColorSpaceType::Indexed(_) => smallvec![0.0], + ColorSpaceType::Separation(_) => smallvec![1.0], + ColorSpaceType::Pattern(c) => c.initial_color(), + ColorSpaceType::DeviceN(d) => smallvec![1.0; d.num_components as usize], + } + } + + pub(crate) fn is_device_gray(&self) -> bool { + matches!(self.0.as_ref(), ColorSpaceType::DeviceGray) + } + + /// Get the number of components of the color space. + pub(crate) fn num_components(&self) -> u8 { + match self.0.as_ref() { + ColorSpaceType::DeviceCmyk => 4, + ColorSpaceType::DeviceGray => 1, + ColorSpaceType::DeviceRgb => 3, + ColorSpaceType::ICCBased(icc) => icc.0.number_components as u8, + ColorSpaceType::CalGray(_) => 1, + ColorSpaceType::CalRgb(_) => 3, + ColorSpaceType::Lab(_) => 3, + ColorSpaceType::Indexed(_) => 1, + ColorSpaceType::Separation(_) => 1, + ColorSpaceType::Pattern(p) => p.num_components(), + ColorSpaceType::DeviceN(d) => d.num_components, + } + } + + /// Turn the given component values and opacity into an RGBA color. + pub fn to_rgba(&self, c: &[f32], opacity: f32, manual_scale: bool) -> AlphaColor { + self.to_alpha_color(c, opacity, manual_scale) + .unwrap_or(AlphaColor::BLACK) + } +} + +impl ToRgb for ColorSpace { + fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> { + match self.0.as_ref() { + ColorSpaceType::DeviceCmyk => { + if input.len() == 4 { + let converted = [ + f32_to_u8(input[0]), + f32_to_u8(input[1]), + f32_to_u8(input[2]), + f32_to_u8(input[3]), + ]; + CMYK_TRANSFORM.convert_u8(&converted, output) + } else { + let converted = input.iter().copied().map(f32_to_u8).collect::>(); + CMYK_TRANSFORM.convert_u8(&converted, output) + } + } + ColorSpaceType::DeviceGray => { + for (gray, output) in input.iter().zip(output.chunks_exact_mut(3)) { + let gray = f32_to_u8(*gray); + output.copy_from_slice(&[gray, gray, gray]); + } + + Some(()) + } + ColorSpaceType::DeviceRgb => { + for (input, output) in input.iter().copied().zip(output) { + *output = f32_to_u8(input); + } + + Some(()) + } + ColorSpaceType::Pattern(i) => i.convert_f32(input, output, manual_scale), + ColorSpaceType::Indexed(i) => i.convert_f32(input, output, manual_scale), + ColorSpaceType::ICCBased(i) => i.convert_f32(input, output, manual_scale), + ColorSpaceType::CalGray(i) => i.convert_f32(input, output, manual_scale), + ColorSpaceType::CalRgb(i) => i.convert_f32(input, output, manual_scale), + ColorSpaceType::Lab(i) => i.convert_f32(input, output, manual_scale), + ColorSpaceType::Separation(i) => i.convert_f32(input, output, manual_scale), + ColorSpaceType::DeviceN(i) => i.convert_f32(input, output, manual_scale), + } + } + + fn supports_u8(&self) -> bool { + match self.0.as_ref() { + ColorSpaceType::DeviceCmyk => true, + ColorSpaceType::DeviceGray => true, + ColorSpaceType::DeviceRgb => true, + ColorSpaceType::Pattern(i) => i.supports_u8(), + ColorSpaceType::Indexed(i) => i.supports_u8(), + ColorSpaceType::ICCBased(i) => i.supports_u8(), + ColorSpaceType::CalGray(i) => i.supports_u8(), + ColorSpaceType::CalRgb(i) => i.supports_u8(), + ColorSpaceType::Lab(i) => i.supports_u8(), + ColorSpaceType::Separation(i) => i.supports_u8(), + ColorSpaceType::DeviceN(i) => i.supports_u8(), + } + } + + fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> { + match self.0.as_ref() { + ColorSpaceType::DeviceCmyk => CMYK_TRANSFORM.convert_u8(input, output), + ColorSpaceType::DeviceGray => { + for (input, output) in input.iter().zip(output.chunks_exact_mut(3)) { + output.copy_from_slice(&[*input, *input, *input]); + } + + Some(()) + } + ColorSpaceType::DeviceRgb => { + for (input, output) in input.iter().zip(output.iter_mut()) { + *output = *input; + } + + Some(()) + } + ColorSpaceType::Pattern(i) => i.convert_u8(input, output), + ColorSpaceType::Indexed(i) => i.convert_u8(input, output), + ColorSpaceType::ICCBased(i) => i.convert_u8(input, output), + ColorSpaceType::CalGray(i) => i.convert_u8(input, output), + ColorSpaceType::CalRgb(i) => i.convert_u8(input, output), + ColorSpaceType::Lab(i) => i.convert_u8(input, output), + ColorSpaceType::Separation(i) => i.convert_u8(input, output), + ColorSpaceType::DeviceN(i) => i.convert_u8(input, output), + } + } + + fn is_none(&self) -> bool { + match self.0.as_ref() { + ColorSpaceType::Separation(s) => s.is_none(), + ColorSpaceType::DeviceN(d) => d.is_none(), + _ => false, + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct CalGray { + white_point: [f32; 3], + black_point: [f32; 3], + gamma: f32, +} + +// See +impl CalGray { + fn new(dict: &Dict<'_>) -> Option { + let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]); + let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]); + let gamma = dict.get::(GAMMA).unwrap_or(1.0); + + Some(Self { + white_point, + black_point, + gamma, + }) + } +} + +impl ToRgb for CalGray { + fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> { + for (input, output) in input.iter().copied().zip(output.chunks_exact_mut(3)) { + let g = self.gamma; + let (_xw, yw, _zw) = { + let wp = self.white_point; + (wp[0], wp[1], wp[2]) + }; + let (_xb, _yb, _zb) = { + let bp = self.black_point; + (bp[0], bp[1], bp[2]) + }; + + let a = input; + let ag = a.powf(g); + let l = yw * ag; + let val = (0.0_f32.max(295.8 * l.powf(0.333_333_34) - 40.8) + 0.5) as u8; + + output.copy_from_slice(&[val, val, val]); + } + + Some(()) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct CalRgb { + white_point: [f32; 3], + black_point: [f32; 3], + matrix: [f32; 9], + gamma: [f32; 3], +} + +// See +// Completely copied from there without really understanding the logic, but we get the same results as Firefox +// which should be good enough (and by viewing the `calrgb.pdf` test file in different viewers you will +// see that in many cases each viewer does whatever it wants, even Acrobat), so this is good enough for us. +impl CalRgb { + fn new(dict: &Dict<'_>) -> Option { + let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]); + let black_point = dict.get::<[f32; 3]>(BLACK_POINT).unwrap_or([0.0, 0.0, 0.0]); + let matrix = dict + .get::<[f32; 9]>(MATRIX) + .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]); + let gamma = dict.get::<[f32; 3]>(GAMMA).unwrap_or([1.0, 1.0, 1.0]); + + Some(Self { + white_point, + black_point, + matrix, + gamma, + }) + } + + const BRADFORD_SCALE_MATRIX: [f32; 9] = [ + 0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296, + ]; + + const BRADFORD_SCALE_INVERSE_MATRIX: [f32; 9] = [ + 0.9869929, -0.1470543, 0.1599627, 0.4323053, 0.5183603, 0.0492912, -0.0085287, 0.0400428, + 0.9684867, + ]; + + const SRGB_D65_XYZ_TO_RGB_MATRIX: [f32; 9] = [ + 3.2404542, -1.5371385, -0.4985314, -0.969_266, 1.8760108, 0.0415560, 0.0556434, -0.2040259, + 1.0572252, + ]; + + const FLAT_WHITEPOINT: [f32; 3] = [1.0, 1.0, 1.0]; + const D65_WHITEPOINT: [f32; 3] = [0.95047, 1.0, 1.08883]; + + fn decode_l_constant() -> f32 { + ((8.0_f32 + 16.0) / 116.0).powi(3) / 8.0 + } + + fn srgb_transfer_function(color: f32) -> f32 { + if color <= 0.0031308 { + (12.92 * color).clamp(0.0, 1.0) + } else if color >= 0.99554525 { + 1.0 + } else { + ((1.0 + 0.055) * color.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0) + } + } + + fn matrix_product(a: &[f32; 9], b: &[f32; 3]) -> [f32; 3] { + [ + a[0] * b[0] + a[1] * b[1] + a[2] * b[2], + a[3] * b[0] + a[4] * b[1] + a[5] * b[2], + a[6] * b[0] + a[7] * b[1] + a[8] * b[2], + ] + } + + fn to_flat(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] { + [ + lms[0] / source_white_point[0], + lms[1] / source_white_point[1], + lms[2] / source_white_point[2], + ] + } + + fn to_d65(source_white_point: &[f32; 3], lms: &[f32; 3]) -> [f32; 3] { + [ + lms[0] * Self::D65_WHITEPOINT[0] / source_white_point[0], + lms[1] * Self::D65_WHITEPOINT[1] / source_white_point[1], + lms[2] * Self::D65_WHITEPOINT[2] / source_white_point[2], + ] + } + + fn decode_l(l: f32) -> f32 { + if l < 0.0 { + -Self::decode_l(-l) + } else if l > 8.0 { + ((l + 16.0) / 116.0).powi(3) + } else { + l * Self::decode_l_constant() + } + } + + fn compensate_black_point(source_bp: &[f32; 3], xyz_flat: &[f32; 3]) -> [f32; 3] { + if source_bp == &[0.0, 0.0, 0.0] { + return *xyz_flat; + } + + let zero_decode_l = Self::decode_l(0.0); + + let mut out = [0.0; 3]; + for i in 0..3 { + let src = Self::decode_l(source_bp[i]); + let scale = (1.0 - zero_decode_l) / (1.0 - src); + let offset = 1.0 - scale; + out[i] = xyz_flat[i] * scale + offset; + } + + out + } + + fn normalize_white_point_to_flat( + &self, + source_white_point: &[f32; 3], + xyz: &[f32; 3], + ) -> [f32; 3] { + if source_white_point[0] == 1.0 && source_white_point[2] == 1.0 { + return *xyz; + } + let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz); + let lms_flat = Self::to_flat(source_white_point, &lms); + Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_flat) + } + + fn normalize_white_point_to_d65(source_white_point: &[f32; 3], xyz: &[f32; 3]) -> [f32; 3] { + let lms = Self::matrix_product(&Self::BRADFORD_SCALE_MATRIX, xyz); + let lms_d65 = Self::to_d65(source_white_point, &lms); + Self::matrix_product(&Self::BRADFORD_SCALE_INVERSE_MATRIX, &lms_d65) + } +} + +impl ToRgb for CalRgb { + fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> { + for (input, output) in input.chunks_exact(3).zip(output.chunks_exact_mut(3)) { + let input = [ + input[0].clamp(0.0, 1.0), + input[1].clamp(0.0, 1.0), + input[2].clamp(0.0, 1.0), + ]; + + let [r, g, b] = input; + let [gr, gg, gb] = self.gamma; + let [agr, bgg, cgb] = [ + if r == 1.0 { 1.0 } else { r.powf(gr) }, + if g == 1.0 { 1.0 } else { g.powf(gg) }, + if b == 1.0 { 1.0 } else { b.powf(gb) }, + ]; + + let m = &self.matrix; + let x = m[0] * agr + m[3] * bgg + m[6] * cgb; + let y = m[1] * agr + m[4] * bgg + m[7] * cgb; + let z = m[2] * agr + m[5] * bgg + m[8] * cgb; + let xyz = [x, y, z]; + + let xyz_flat = self.normalize_white_point_to_flat(&self.white_point, &xyz); + let xyz_black = Self::compensate_black_point(&self.black_point, &xyz_flat); + let xyz_d65 = Self::normalize_white_point_to_d65(&Self::FLAT_WHITEPOINT, &xyz_black); + let srgb_xyz = Self::matrix_product(&Self::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65); + + output.copy_from_slice(&[ + (Self::srgb_transfer_function(srgb_xyz[0]) * 255.0 + 0.5) as u8, + (Self::srgb_transfer_function(srgb_xyz[1]) * 255.0 + 0.5) as u8, + (Self::srgb_transfer_function(srgb_xyz[2]) * 255.0 + 0.5) as u8, + ]); + } + + Some(()) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Lab { + range: [f32; 4], + white_point: [f32; 3], +} + +impl Lab { + fn new(dict: &Dict<'_>) -> Option { + let white_point = dict.get::<[f32; 3]>(WHITE_POINT).unwrap_or([1.0, 1.0, 1.0]); + let range = dict + .get::<[f32; 4]>(RANGE) + .unwrap_or([-100.0, 100.0, -100.0, 100.0]); + + Some(Self { range, white_point }) + } + + /// The white point the space's L\*a\*b\* values are relative to (ISO 32000-2 + /// ยง8.6.5.4). A missing or degenerate entry falls back to a flat white point + /// so hostile input cannot break the conversion. + fn usable_white_point(&self) -> [f32; 3] { + let white_point = self.white_point; + if white_point + .iter() + .all(|component| component.is_finite() && *component > 0.0) + { + white_point + } else { + [1.0, 1.0, 1.0] + } + } + + /// Bradford adaptation scales cone responses, so both white points must + /// be expressed in LMS before taking their ratios. CalRGB's separate + /// flat-white/black-point pipeline retains its existing normalization. + fn adapt_to_d65(white_point: &[f32; 3], xyz: &[f32; 3]) -> [f32; 3] { + let source_lms = CalRgb::matrix_product(&CalRgb::BRADFORD_SCALE_MATRIX, white_point); + // A malformed but positive XYZ white can still yield a zero or + // overflowing cone response. Leave it unadapted rather than divide by + // zero or propagate a non-finite scale. + if source_lms.iter().any(|v| !v.is_finite() || v.abs() <= f32::EPSILON) { + return *xyz; + } + let destination_lms = CalRgb::matrix_product(&CalRgb::BRADFORD_SCALE_MATRIX, &CalRgb::D65_WHITEPOINT); + let lms = CalRgb::matrix_product(&CalRgb::BRADFORD_SCALE_MATRIX, xyz); + let adapted = [ + lms[0] * destination_lms[0] / source_lms[0], + lms[1] * destination_lms[1] / source_lms[1], + lms[2] * destination_lms[2] / source_lms[2], + ]; + CalRgb::matrix_product(&CalRgb::BRADFORD_SCALE_INVERSE_MATRIX, &adapted) + } + + /// Inverse of the CIE f(t) helper that maps relative fX/fY/fZ to absolute + /// XYZ as a ratio of the white point. + fn inverse_lab_f(t: f32) -> f32 { + const DELTA: f32 = 6.0 / 29.0; + if t > DELTA { + t * t * t + } else { + 3.0 * DELTA * DELTA * (t - 4.0 / 29.0) + } + } +} + +impl ToRgb for Lab { + fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> { + let [min_a, max_a, min_b, max_b] = self.range; + let white_point = self.usable_white_point(); + let [xw, yw, zw] = white_point; + + for (input, output) in input.chunks_exact(3).zip(output.chunks_exact_mut(3)) { + // PdfCraft patch: `manual_scale` means the components arrive + // normalised to [0, 1] (an `/Indexed` palette over Lab looks its + // bytes up as `byte / 255`). Map them linearly onto L* 0..100 and + // the a*/b* /Range (ISO 32000-2 ยง8.6.6.3) before converting. + let (l, a, b) = if manual_scale { + ( + clamp_component(input[0], 0.0, 1.0) * 100.0, + min_a + clamp_component(input[1], 0.0, 1.0) * (max_a - min_a), + min_b + clamp_component(input[2], 0.0, 1.0) * (max_b - min_b), + ) + } else { + (input[0], input[1], input[2]) + }; + let l = clamp_component(l, 0.0, 100.0); + let a = clamp_component(a, min_a, max_a); + let b = clamp_component(b, min_b, max_b); + + // L*a*b* is relative to the space's own /WhitePoint. Map it to XYZ + // in that white's coordinates, Bradford-adapt to D65 and convert to + // sRGB (ISO 32000-2 ยง8.6.5.4). + let fy = (l + 16.0) / 116.0; + let fx = fy + a / 500.0; + let fz = fy - b / 200.0; + let xyz = [ + xw * Self::inverse_lab_f(fx), + yw * Self::inverse_lab_f(fy), + zw * Self::inverse_lab_f(fz), + ]; + + let xyz_d65 = Self::adapt_to_d65(&white_point, &xyz); + let srgb = CalRgb::matrix_product(&CalRgb::SRGB_D65_XYZ_TO_RGB_MATRIX, &xyz_d65); + + output.copy_from_slice(&[ + f32_to_u8(CalRgb::srgb_transfer_function(srgb[0])), + f32_to_u8(CalRgb::srgb_transfer_function(srgb[1])), + f32_to_u8(CalRgb::srgb_transfer_function(srgb[2])), + ]); + } + + Some(()) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Indexed { + values: Vec>, + hival: u8, + base: Box, +} + +impl Indexed { + fn new(array: &Array<'_>, cache: &Cache) -> Option { + let mut iter = array.flex_iter(); + // Skip name + let _ = iter.next::>()?; + let base_color_space = ColorSpace::new(iter.next::>()?, cache)?; + let hival = iter.next::()?.min(u8::MAX as u32) as u8; + + let values = { + let data = iter + .next::>() + .and_then(|s| s.decoded().ok()) + .or_else(|| { + iter.next::>() + .map(|s| Cow::Owned(s.to_vec())) + })?; + + let num_components = base_color_space.num_components(); + + let mut byte_iter = data.iter().copied(); + + let mut vals = vec![]; + for _ in 0..=hival { + let mut temp = vec![]; + + for _ in 0..num_components { + temp.push(byte_iter.next()? as f32 / 255.0); + } + + vals.push(temp); + } + + vals + }; + + Some(Self { + values, + hival, + base: Box::new(base_color_space), + }) + } +} + +impl ToRgb for Indexed { + fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> { + let mut indexed = vec![0.0; input.len() * self.base.num_components() as usize]; + + for (input, output) in input + .iter() + .copied() + .zip(indexed.chunks_exact_mut(self.base.num_components() as usize)) + { + let idx = (input.clamp(0.0, self.hival as f32) + 0.5) as usize; + output.copy_from_slice(&self.values[idx]); + } + + self.base.convert_f32(&indexed, output, true) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct Separation { + alternate_space: ColorSpace, + tint_transform: Function, + is_none_separation: bool, +} + +impl Separation { + fn new(array: &Array<'_>, cache: &Cache) -> Option { + let mut iter = array.flex_iter(); + // Skip `/Separation` + let _ = iter.next::>()?; + let name = iter.next::>()?; + let alternate_space = ColorSpace::new(iter.next::>()?, cache)?; + let tint_transform = Function::new(&iter.next::>()?)?; + // Either I did something wrong, or no other viewers properly handles + // `All`, so let's just ignore it as well. + let is_none_separation = name.as_str() == "None"; + + Some(Self { + alternate_space, + tint_transform, + is_none_separation, + }) + } +} + +impl ToRgb for Separation { + fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> { + let evaluated = input + .iter() + .flat_map(|n| { + self.tint_transform + .eval(smallvec![*n]) + .unwrap_or(self.alternate_space.initial_color()) + }) + .collect::>(); + self.alternate_space.convert_f32(&evaluated, output, false) + } + + fn is_none(&self) -> bool { + self.is_none_separation + } +} + +#[derive(Debug, Clone)] +pub(crate) struct DeviceN { + alternate_space: ColorSpace, + num_components: u8, + tint_transform: Function, + is_none: bool, +} + +impl DeviceN { + fn new(array: &Array<'_>, cache: &Cache) -> Option { + let mut iter = array.flex_iter(); + // Skip `/DeviceN` + let _ = iter.next::>()?; + // Skip `Name`. + let names = iter + .next::>()? + .iter::>() + .collect::>(); + let num_components = u8::try_from(names.len()).ok()?; + let all_none = names.iter().all(|n| n.as_str() == "None"); + let alternate_space = ColorSpace::new(iter.next::>()?, cache)?; + let tint_transform = Function::new(&iter.next::>()?)?; + + if num_components == 0 { + return None; + } + + Some(Self { + alternate_space, + num_components, + tint_transform, + is_none: all_none, + }) + } +} + +impl ToRgb for DeviceN { + fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> { + let evaluated = input + .chunks_exact(self.num_components as usize) + .flat_map(|n| { + self.tint_transform + .eval(n.to_smallvec()) + .unwrap_or(self.alternate_space.initial_color()) + }) + .collect::>(); + self.alternate_space.convert_f32(&evaluated, output, false) + } + + fn is_none(&self) -> bool { + self.is_none + } +} + +struct ICCColorRepr { + src_profile: ColorProfile, + src_layout: Layout, + number_components: usize, + is_srgb: bool, + is_lab: bool, + transform_u8: Arc, + transform_f32: OnceLock>, +} + +#[derive(Clone)] +pub(crate) struct ICCProfile(Arc); + +impl Debug for ICCProfile { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ICCColor {{..}}") + } +} + +impl ICCProfile { + fn new(profile: &[u8], number_components: usize) -> Option { + let src_profile = ColorProfile::new_from_slice(profile).ok()?; + + const SRGB_MARKER: &[u8] = b"sRGB"; + + let is_srgb = profile + .get(52..56) + .map(|device_model| device_model == SRGB_MARKER) + .unwrap_or(false); + let is_lab = src_profile.color_space == DataColorSpace::Lab; + + Self::new_from_src_profile(src_profile, is_srgb, is_lab, number_components) + } + + fn new_from_src_profile( + src_profile: ColorProfile, + is_srgb: bool, + is_lab: bool, + number_components: usize, + ) -> Option { + let src_layout = match number_components { + 1 => Layout::Gray, + 3 => Layout::Rgb, + 4 => Layout::Rgba, + _ => { + warn!("unsupported number of components {number_components} for ICC profile"); + + return None; + } + }; + + let dest_profile = ColorProfile::new_srgb(); + let transform_u8 = src_profile + .clone() + .create_transform_8bit( + src_layout, + &dest_profile, + Layout::Rgb, + TransformOptions::default(), + ) + .ok()?; + + Some(Self(Arc::new(ICCColorRepr { + src_profile, + src_layout, + number_components, + is_srgb, + is_lab, + transform_u8, + transform_f32: OnceLock::new(), + }))) + } + + fn is_srgb(&self) -> bool { + self.0.is_srgb + } + + fn is_lab(&self) -> bool { + self.0.is_lab + } + + fn transform_u8(&self) -> &Arc { + &self.0.transform_u8 + } + + fn transform_f32(&self) -> &Arc { + // From my benchmarking, creating the f32 transforms is usually much + // more expensive than u8. Therefore, we only create it lazily when + // really needed. + self.0.transform_f32.get_or_init(|| { + let dest_profile = ColorProfile::new_srgb(); + self.0 + .src_profile + .clone() + .create_transform_f32( + self.0.src_layout, + &dest_profile, + Layout::Rgb, + TransformOptions::default(), + ) + // Since the u8 version was valid, hopefully this should never panic? + .unwrap() + }) + } +} + +impl ToRgb for ICCProfile { + fn convert_f32(&self, input: &[f32], output: &mut [u8], _: bool) -> Option<()> { + let mut temp = vec![0.0_f32; output.len()]; + + if self.is_lab() { + // moxcms expects normalized values. + let scaled = input + .chunks_exact(3) + .flat_map(|i| { + [ + i[0] * (1.0 / 100.0), + (i[1] + 128.0) * (1.0 / 255.0), + (i[2] + 128.0) * (1.0 / 255.0), + ] + }) + .collect::>(); + self.transform_f32().transform(&scaled, &mut temp).ok()?; + } else { + self.transform_f32().transform(input, &mut temp).ok()?; + }; + + for (input, output) in temp.iter().zip(output.iter_mut()) { + *output = (input * 255.0 + 0.5) as u8; + } + + Some(()) + } + + fn supports_u8(&self) -> bool { + true + } + + fn convert_u8(&self, input: &[u8], output: &mut [u8]) -> Option<()> { + if self.is_srgb() { + output.copy_from_slice(input); + } else { + self.transform_u8().transform(input, output).ok()?; + } + + Some(()) + } +} + +#[inline(always)] +fn f32_to_u8(val: f32) -> u8 { + (val * 255.0 + 0.5) as u8 +} + +/// Clamp an input-derived colour component to `[lo, hi]` without panicking on +/// NaN or on a reversed range. Non-finite values map to zero. +fn clamp_component(value: f32, lo: f32, hi: f32) -> f32 { + if !value.is_finite() { + return 0.0; + } + value.max(lo).min(hi) +} + +#[derive(Debug, Clone)] +/// A color. +pub struct Color { + color_space: ColorSpace, + components: ColorComponents, + opacity: f32, +} + +impl Color { + pub(crate) fn new(color_space: ColorSpace, components: ColorComponents, opacity: f32) -> Self { + Self { + color_space, + components, + opacity, + } + } + + /// Return the color as an RGBA color. + pub fn to_rgba(&self) -> AlphaColor { + self.color_space + .to_rgba(&self.components, self.opacity, false) + } + + /// Create a color from RGBA. + pub fn from_rgba(rgba: AlphaColor) -> Self { + let c = rgba.components(); + Self { + color_space: ColorSpace::device_rgb(), + components: smallvec![c[0], c[1], c[2]], + opacity: c[3], + } + } +} + +static CMYK_TRANSFORM: LazyLock = LazyLock::new(|| { + ICCProfile::new(include_bytes!("../assets/CGATS001Compat-v2-micro.icc"), 4).unwrap() +}); + +pub(crate) trait ToRgb { + fn convert_sample(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()> { + // We prefer using the u8 variant for single samples, which is especially + // important for ICC profiles to avoid constructing the (more expensive) + // f32 variant. + if self.supports_u8() { + let converted = input + .iter() + .copied() + .map(f32_to_u8) + .collect::>(); + + if self.convert_u8(&converted, output).is_some() { + return Some(()); + } + } + + self.convert_f32(input, output, manual_scale) + } + + fn convert_f32(&self, input: &[f32], output: &mut [u8], manual_scale: bool) -> Option<()>; + fn supports_u8(&self) -> bool { + false + } + fn convert_u8(&self, _: &[u8], _: &mut [u8]) -> Option<()> { + unimplemented!(); + } + fn is_none(&self) -> bool { + false + } + fn to_alpha_color( + &self, + input: &[f32], + mut opacity: f32, + manual_scale: bool, + ) -> Option { + let mut output = [0; 3]; + self.convert_sample(input, &mut output, manual_scale)?; + + // For separation color spaces: + // "The special colourant name None shall not produce any visible output. + // Painting operations in a Separation space with this colourant name + // shall have no effect on the current page." + if self.is_none() { + opacity = 0.0; + } + + Some(AlphaColor::from_rgba8( + output[0], + output[1], + output[2], + (opacity * 255.0 + 0.5) as u8, + )) + } +} + +#[cfg(test)] +mod device_color_compatibility_tests { + use super::ColorSpace; + + #[test] + fn device_rgb_rounds_components_and_opacity_to_bytes() { + let space = ColorSpace::device_rgb(); + assert_eq!(space.to_rgba(&[0.0, 0.5, 1.0], 0.5, false).to_rgba8(), [0, 128, 255, 128]); + assert_eq!(space.to_rgba(&[-1.0, 2.0, f32::NAN], 1.0, false).to_rgba8(), [0, 255, 0, 255]); + assert_eq!(space.to_rgba(&[0.5], 1.0, false).to_rgba8(), [128, 0, 0, 255]); + } + + #[test] + fn device_gray_uses_the_same_rounding_and_missing_component_default() { + let space = ColorSpace::device_gray(); + assert_eq!(space.to_rgba(&[0.5], 0.5, false).to_rgba8(), [128, 128, 128, 128]); + assert_eq!(space.to_rgba(&[], 1.0, false).to_rgba8(), [0, 0, 0, 255]); + } +} diff --git a/vendor/hayro-interpret/src/context.rs b/vendor/hayro-interpret/src/context.rs new file mode 100644 index 00000000..9953b419 --- /dev/null +++ b/vendor/hayro-interpret/src/context.rs @@ -0,0 +1,486 @@ +use crate::cache::{Cache, CacheKey, DecodedImageCache}; +use crate::color::ColorSpace; +use crate::convert::convert_transform; +use crate::font::{Font, StandardFont}; +use crate::interpret::state::{ClipType, State, TextStateFont}; +use crate::ocg::OcgState; +use crate::util::{BezPathExt, Float64Ext}; +use crate::{ClipPath, Device, FillRule, InterpreterSettings, StrokeProps}; +use hayro_syntax::content::ops::Transform; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Name; +use hayro_syntax::object::Stream; +use hayro_syntax::page::Resources; +use hayro_syntax::xref::XRef; +use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +/// Maximum nesting depth for interpreting `XObject`'s/patterns/streams. +pub(crate) const MAX_NESTED_INTERPRETATION_DEPTH: u32 = 50; +/// PdfCraft patch: maximum nesting for content that is painted recursively by the device +/// (tiling patterns, Type 3 glyphs). Either can refer back to itself through resources it +/// inherits, which recursed until the stack overflowed (found by fuzzing). +pub(crate) const MAX_PAINT_NESTING: u32 = 16; + +/// PdfCraft patch: how many nested paints (a form inside a form, a Type 3 glyph or tiling +/// pattern painted from inside another one) one page may do. Nesting depth alone doesn't bound +/// the work: a glyph that shows ten glyphs of its own font, nested sixteen deep, is 10^16 +/// paints (a fuzzed file hung). Ordinary pages stay far below this. +pub(crate) const MAX_NESTED_PAINTS: u32 = 50_000; + +std::thread_local! { + static NESTED_PAINTS: core::cell::Cell = const { core::cell::Cell::new(0) }; + static CONTENT_LEFT: core::cell::Cell = const { core::cell::Cell::new(MAX_PAGE_CONTENT) }; + // PdfCraft patch (18): whether this page's content budget ran out, reported as + // `InterpreterWarning::ContentTruncated`. + static CONTENT_TRUNCATED: core::cell::Cell = const { core::cell::Cell::new(false) }; +} + +/// PdfCraft patch: decoded content-stream bytes one page may hold at once: its contents, and +/// the forms, tiling patterns and Type 3 glyph procedures being painted. Each stream already +/// stops at `MAX_DECODED_STREAM`, but forms nest fifty deep and every level keeps its content +/// while the next one runs: a form painting itself around a stream that inflates to 256 MiB +/// would have held over 12 GB. A level gives its bytes back when it finishes, so content +/// painted again and again doesn't use the budget up. Ordinary pages hold a few megabytes. +pub(crate) const MAX_PAGE_CONTENT: usize = 512 << 20; + +/// PdfCraft patch: a content stream decoded within the page's [`MAX_PAGE_CONTENT`]. The bytes +/// it decoded count against the budget until it is dropped; a stream without filters is +/// borrowed from the file and costs nothing. +pub(crate) struct DecodedContent<'a> { + data: Cow<'a, [u8]>, + held: usize, +} + +impl AsRef<[u8]> for DecodedContent<'_> { + fn as_ref(&self) -> &[u8] { + &self.data + } +} + +impl Drop for DecodedContent<'_> { + fn drop(&mut self) { + let held = self.held; + CONTENT_LEFT.with(|c| c.set(c.get().saturating_add(held).min(MAX_PAGE_CONTENT))); + } +} + +/// PdfCraft patch: decode a content stream within what is left of this page's +/// [`MAX_PAGE_CONTENT`]. `None` once the budget is spent (the content is skipped). +pub(crate) fn decode_content<'a>(stream: &Stream<'a>) -> Option> { + let left = CONTENT_LEFT.with(core::cell::Cell::get); + if left == 0 { + CONTENT_TRUNCATED.with(|truncated| truncated.set(true)); + warn!("page content budget exceeded"); + return None; + } + let filtered = !stream.filters().is_empty(); + let limit = if filtered { left.min(stream.decode_limits().max_decoded_stream()) } else { left }; + let Ok(data) = stream.decoded_within(limit) else { + CONTENT_TRUNCATED.with(|truncated| truncated.set(true)); + return None; + }; + // The whole-stream decoders do not expose completion. Conservatively report a + // filtered form, pattern or Type 3 procedure reaching its effective budget. + if filtered && data.len() >= limit { + CONTENT_TRUNCATED.with(|truncated| truncated.set(true)); + } + let held = match &data { + Cow::Owned(v) => v.len(), + Cow::Borrowed(_) => 0, + }; + charge_content(held); + Some(DecodedContent { data, held }) +} + +/// PdfCraft patch: count `len` bytes held for the rest of the page (its own contents). +pub(crate) fn charge_content(len: usize) { + CONTENT_LEFT.with(|c| c.set(c.get().saturating_sub(len))); +} + +/// PdfCraft patch (18): whether content was skipped because the page's budget ran out. +pub(crate) fn content_was_truncated() -> bool { + CONTENT_TRUNCATED.with(core::cell::Cell::get) +} + +/// PdfCraft patch: count a paint at `depth` (top-level ones are free); `false` once this +/// page's budget is spent, and the paint is skipped. +pub(crate) fn take_nested_paint(depth: u32) -> bool { + if depth < 2 { + return true; + } + NESTED_PAINTS.with(|n| { + let used = n.get(); + if used >= MAX_NESTED_PAINTS { + return false; + } + n.set(used + 1); + true + }) +} + +/// A cache used by the interpreter. +/// +/// Ideally, such a cache should be constructed once per PDF and then reused across +/// multiple interpreter invocations on the same document. +#[derive(Clone)] +pub struct InterpreterCache<'a> { + pub(crate) font_cache: Rc>>>>, + pub(crate) object_cache: Cache, + /// PdfCraft patch: decoded images, shared by every render through this cache so a tile + /// no longer decodes its page's images again. + pub(crate) decoded_cache: DecodedImageCache, +} + +impl<'a> Default for InterpreterCache<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> InterpreterCache<'a> { + /// Create a new interpreter cache. + pub fn new() -> Self { + Self { + font_cache: Rc::new(RefCell::new(HashMap::new())), + object_cache: Cache::new(), + decoded_cache: DecodedImageCache::new(), + } + } +} + +/// A per-page interpretation context that borrows shared data from an [`InterpreterCache`]. +pub struct Context<'a> { + states: Vec>, + path: BezPath, + sub_path_start: Point, + last_point: Point, + clip: Option, + root_transforms: Vec, + bbox: Vec, + pub(crate) settings: InterpreterSettings, + pub(crate) interpreter_cache: InterpreterCache<'a>, + pub(crate) xref: &'a XRef, + pub(crate) ocg_state: OcgState, + nesting_depth: u32, +} + +impl<'a> Context<'a> { + /// Create a new context. + pub fn new( + initial_transform: Affine, + bbox: Rect, + cache: &InterpreterCache<'a>, + xref: &'a XRef, + settings: InterpreterSettings, + ) -> Self { + let state = State::new(initial_transform); + // PdfCraft patch: a page (or other top-level content) starts with fresh budgets. + NESTED_PAINTS.with(|n| n.set(0)); + CONTENT_LEFT.with(|c| c.set(MAX_PAGE_CONTENT)); + CONTENT_TRUNCATED.with(|truncated| truncated.set(false)); + crate::encode::reset_mesh_budget(); + + Self::new_with(initial_transform, bbox, cache, xref, settings, state, 0) + } + + pub(crate) fn new_with( + initial_transform: Affine, + bbox: Rect, + cache: &InterpreterCache<'a>, + xref: &'a XRef, + settings: InterpreterSettings, + state: State<'a>, + nesting_depth: u32, + ) -> Self { + let mut ocg_state = { + let root_ref = xref.root_id(); + xref.get::>(root_ref) + .map(|catalog| OcgState::from_catalog(&catalog)) + .unwrap_or_default() + }; + // PdfCraft patch: apply viewer layer toggles. + for (num, generation, visible) in settings.ocg_overrides.iter() { + ocg_state.set_active(hayro_syntax::object::ObjectIdentifier::new(*num, *generation), *visible); + } + + Self { + states: vec![state], + settings, + xref, + root_transforms: vec![initial_transform], + last_point: Point::default(), + sub_path_start: Point::default(), + clip: None, + bbox: vec![bbox], + path: BezPath::new(), + interpreter_cache: cache.clone(), + ocg_state, + nesting_depth, + } + } + + pub(crate) fn save_state(&mut self) { + let Some(cur) = self.states.last().cloned() else { + warn!("attempted to save state without existing state"); + return; + }; + + self.states.push(cur); + } + + pub(crate) fn bbox(&self) -> Rect { + self.bbox.last().copied().unwrap_or_else(|| { + warn!("failed to get a bbox"); + + Rect::new(0.0, 0.0, 1.0, 1.0) + }) + } + + fn push_bbox(&mut self, bbox: Rect) { + let new = self.bbox().intersect(bbox); + self.bbox.push(new); + } + + pub(crate) fn push_clip_path( + &mut self, + clip_path: BezPath, + fill: FillRule, + device: &mut impl Device<'a>, + ) { + if let Some(clip_rect) = path_as_rect(&clip_path) { + let cur_bbox = self.bbox(); + + // If the clip path is a rect and completely covers the current bbox, don't emit it. + if cur_bbox + .min_x() + .is_nearly_greater_or_equal(clip_rect.min_x()) + && cur_bbox + .min_y() + .is_nearly_greater_or_equal(clip_rect.min_y()) + && cur_bbox.max_x().is_nearly_less_or_equal(clip_rect.max_x()) + && cur_bbox.max_y().is_nearly_less_or_equal(clip_rect.max_y()) + { + self.get_mut().clips.push(ClipType::Dummy); + return; + } + } + + let bbox = clip_path.bounding_box(); + device.push_clip_path(&ClipPath { + path: clip_path, + fill, + }); + self.push_bbox(bbox); + self.get_mut().clips.push(ClipType::Real); + } + + pub(crate) fn pop_clip_path(&mut self, device: &mut impl Device<'a>) { + if let Some(ClipType::Real) = self.get_mut().clips.pop() { + device.pop_clip_path(); + self.pop_bbox(); + } + } + + fn pop_bbox(&mut self) { + self.bbox.pop(); + } + + pub(crate) fn push_root_transform(&mut self) { + self.root_transforms.push(self.get().ctm); + } + + pub(crate) fn pop_root_transform(&mut self) { + self.root_transforms.pop(); + } + + pub(crate) fn root_transform(&self) -> Affine { + self.root_transforms + .last() + .copied() + .unwrap_or(Affine::IDENTITY) + } + + pub(crate) fn restore_state(&mut self, device: &mut impl Device<'a>) { + let Some(target_clips) = self + .states + .get(self.states.len().saturating_sub(2)) + .map(|s| s.clips.len()) + else { + warn!("underflowed graphics state"); + return; + }; + + while self.get().clips.len() > target_clips { + self.pop_clip_path(device); + } + + // The first state should never be popped. + if self.states.len() > 1 { + self.states.pop(); + } + + device.set_soft_mask( + self.states + .last() + .and_then(|l| l.graphics_state.soft_mask.clone()), + ); + } + + pub(crate) fn path(&self) -> &BezPath { + &self.path + } + + pub(crate) fn path_mut(&mut self) -> &mut BezPath { + &mut self.path + } + + pub(crate) fn sub_path_start(&self) -> &Point { + &self.sub_path_start + } + + pub(crate) fn sub_path_start_mut(&mut self) -> &mut Point { + &mut self.sub_path_start + } + + pub(crate) fn last_point(&self) -> &Point { + &self.last_point + } + + pub(crate) fn last_point_mut(&mut self) -> &mut Point { + &mut self.last_point + } + + pub(crate) fn clip_mut(&mut self) -> &mut Option { + &mut self.clip + } + + pub(crate) fn get(&self) -> &State<'a> { + self.states.last().unwrap() + } + + pub(crate) fn get_mut(&mut self) -> &mut State<'a> { + self.states.last_mut().unwrap() + } + + pub(crate) fn pre_concat_transform(&mut self, transform: Transform) { + self.pre_concat_affine(convert_transform(transform)); + } + + pub(crate) fn pre_concat_affine(&mut self, transform: Affine) { + self.get_mut().ctm *= transform; + } + + pub(crate) fn get_color_space( + &mut self, + resources: &Resources<'_>, + name: &Name<'_>, + ) -> Option { + let cs_object = resources.get_color_space(name)?; + self.interpreter_cache + .object_cache + .get_or_insert_with(cs_object.cache_key(), || { + ColorSpace::new(cs_object.clone(), &self.interpreter_cache.object_cache) + }) + } + + pub(crate) fn stroke_props(&self) -> StrokeProps { + self.get().graphics_state.stroke_props.clone() + } + + pub(crate) fn num_states(&self) -> usize { + self.states.len() + } + + pub(crate) fn nesting_depth(&self) -> u32 { + self.nesting_depth + } + + pub(crate) fn begin_nested_interpretation(&mut self) -> bool { + if self.nesting_depth >= MAX_NESTED_INTERPRETATION_DEPTH { + warn!("interpreter nesting depth exceeded"); + + return false; + } + if !take_nested_paint(self.nesting_depth + 1) { + warn!("nested paint budget exceeded"); + + return false; + } + + self.nesting_depth += 1; + + true + } + + pub(crate) fn end_nested_interpretation(&mut self) { + self.nesting_depth = self.nesting_depth.saturating_sub(1); + } + pub(crate) fn resolve_font(&mut self, font_dict: &Dict<'a>) -> Option> { + let cache_key = font_dict.cache_key(); + + let resolved = { + let mut font_cache = self.interpreter_cache.font_cache.borrow_mut(); + font_cache + .entry(cache_key) + .or_insert_with(|| { + Font::new( + font_dict, + &self.settings.font_resolver, + &self.settings.cmap_resolver, + ) + }) + .clone() + }; + + if let Some(resolved) = resolved { + Some(TextStateFont::Font(resolved)) + } else { + Font::new_standard(StandardFont::Helvetica, &self.settings.font_resolver) + .map(TextStateFont::Fallback) + } + } +} + +pub(crate) fn path_as_rect(path: &BezPath) -> Option { + // One MoveTo, three LineTo, one ClosePath + if path.elements().len() != 5 { + return None; + } + + let bbox = path.fast_bounding_box(); + let (min_x, min_y, max_x, max_y) = (bbox.min_x(), bbox.min_y(), bbox.max_x(), bbox.max_y()); + let mut corners = [false; 4]; + + let mut check_point = |p: Point| { + corners[0] |= p.x.is_nearly_equal(min_x) && p.y.is_nearly_equal(min_y); + corners[1] |= p.x.is_nearly_equal(min_x) && p.y.is_nearly_equal(max_y); + corners[2] |= p.x.is_nearly_equal(max_x) && p.y.is_nearly_equal(min_y); + corners[3] |= p.x.is_nearly_equal(max_x) && p.y.is_nearly_equal(max_y); + }; + + for (idx, el) in path.elements().iter().enumerate() { + match el { + PathEl::MoveTo(p) => { + if idx != 0 { + return None; + } + + check_point(*p); + } + PathEl::LineTo(l) => check_point(*l), + PathEl::QuadTo(_, _) => return None, + PathEl::CurveTo(_, _, _) => return None, + PathEl::ClosePath => {} + } + } + + if corners[0] && corners[1] && corners[2] && corners[3] { + Some(bbox) + } else { + None + } +} diff --git a/vendor/hayro-interpret/src/convert.rs b/vendor/hayro-interpret/src/convert.rs new file mode 100644 index 00000000..e7689114 --- /dev/null +++ b/vendor/hayro-interpret/src/convert.rs @@ -0,0 +1,30 @@ +use hayro_syntax::content::ops::{LineCap, LineJoin, Transform}; + +pub(crate) fn convert_transform(t: Transform) -> kurbo::Affine { + kurbo::Affine::new([ + t.0.as_f64(), + t.1.as_f64(), + t.2.as_f64(), + t.3.as_f64(), + t.4.as_f64(), + t.5.as_f64(), + ]) +} + +pub(crate) fn convert_line_cap(lc: LineCap) -> kurbo::Cap { + match lc.0.as_i64() { + 0 => kurbo::Cap::Butt, + 1 => kurbo::Cap::Round, + 2 => kurbo::Cap::Square, + _ => kurbo::Cap::Butt, + } +} + +pub(crate) fn convert_line_join(lc: LineJoin) -> kurbo::Join { + match lc.0.as_i64() { + 0 => kurbo::Join::Miter, + 1 => kurbo::Join::Round, + 2 => kurbo::Join::Bevel, + _ => kurbo::Join::Miter, + } +} diff --git a/vendor/hayro-interpret/src/device.rs b/vendor/hayro-interpret/src/device.rs new file mode 100644 index 00000000..72d5a980 --- /dev/null +++ b/vendor/hayro-interpret/src/device.rs @@ -0,0 +1,92 @@ +use crate::font::Glyph; +use crate::soft_mask::SoftMask; +use crate::{BlendMode, ClipPath, Image}; +use crate::{GlyphDrawMode, Paint, PathDrawMode}; +use kurbo::{Affine, BezPath, Rect, Shape}; + +/// A trait for a device that can be used to process PDF drawing instructions. +pub trait Device<'a> { + /// Set the properties for future stroking operations. + /// Set a soft mask to be used for future drawing instructions. + fn set_soft_mask(&mut self, mask: Option>); + /// Set the blend mode that should be used for rendering operations. + fn set_blend_mode(&mut self, blend_mode: BlendMode); + /// Draw a path. + fn draw_path( + &mut self, + path: &BezPath, + transform: Affine, + paint: &Paint<'a>, + draw_mode: &PathDrawMode, + ); + /// Push a new clip path to the clip stack. + fn push_clip_path(&mut self, clip_path: &ClipPath); + /// Push a new transparency group to the blend stack. + fn push_transparency_group( + &mut self, + opacity: f32, + mask: Option>, + blend_mode: BlendMode, + ); + /// Draw a glyph. + fn draw_glyph( + &mut self, + glyph: &Glyph<'a>, + transform: Affine, + glyph_transform: Affine, + paint: &Paint<'a>, + // TODO: Move this into outline glyph. + draw_mode: &GlyphDrawMode, + ); + /// Draw an image. + fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine); + /// Pop the last clip path from the clip stack. + fn pop_clip_path(&mut self); + /// Pop the last transparency group from the blend stack. + fn pop_transparency_group(&mut self); + /// Draw a rectangle directly, without going through the general path pipeline. + fn draw_rect( + &mut self, + rect: &Rect, + transform: Affine, + paint: &Paint<'a>, + draw_mode: &PathDrawMode, + ) { + self.draw_path(&rect.to_path(0.1), transform, paint, draw_mode); + } + /// Called at the beginning of a marked content sequence (BMC/BDC). + /// + /// The tag is the marked content tag (e.g. b"P", b"Span"). The mcid is + /// the marked content identifier from the properties dict, if present. + fn begin_marked_content(&mut self, _tag: &[u8], _mcid: Option) {} + /// PdfCraft patch (25): the `/ActualText` of the marked content sequence that just began + /// (ISO 32000-2 ยง14.9.4), as the text string's raw bytes. Called right after + /// `begin_marked_content`; the text replaces everything drawn up to the matching + /// `end_marked_content`. + fn actual_text(&mut self, _text: &[u8]) {} + /// Called at the end of a marked content sequence (EMC). + fn end_marked_content(&mut self) {} +} + +/// A device that discards all drawing operations. +pub struct DummyDevice; + +impl Device<'_> for DummyDevice { + 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<'_>, + _: Affine, + _: Affine, + _: &Paint<'_>, + _: &GlyphDrawMode, + ) { + } + fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {} + fn pop_clip_path(&mut self) {} + fn pop_transparency_group(&mut self) {} +} diff --git a/vendor/hayro-interpret/src/encode.rs b/vendor/hayro-interpret/src/encode.rs new file mode 100644 index 00000000..498ed909 --- /dev/null +++ b/vendor/hayro-interpret/src/encode.rs @@ -0,0 +1,520 @@ +//! Encoding shading patterns for easy sampling. + +use crate::color::{AlphaColor, ColorComponents, ColorSpace}; +use crate::interpret::state::ActiveTransferFunction; +use crate::pattern::ShadingPattern; +use crate::shading::{ShadingFunction, ShadingType, Triangle}; +use kurbo::{Affine, Point}; +use rustc_hash::FxHashMap; +use smallvec::{ToSmallVec, smallvec}; + +/// A shading pattern that was encoded so it can be sampled. +#[derive(Debug)] +pub struct EncodedShadingPattern { + /// The base transform of the shading pattern. + pub base_transform: Affine, + pub(crate) color_space: ColorSpace, + pub(crate) background_color: AlphaColor, + pub(crate) shading_type: EncodedShadingType, + pub(crate) opacity: f32, + pub(crate) transfer_function: Option, +} + +impl EncodedShadingPattern { + /// Sample the shading at the given position. + #[inline] + pub fn sample(&self, pos: Point) -> [f32; 4] { + self.shading_type + .eval(pos, self.background_color, &self.color_space) + .map(|v| { + let mut components = v.components(); + components[3] *= self.opacity; + + if let Some(tf) = &self.transfer_function { + return tf.apply(&AlphaColor::new(components)).components(); + } + + components + }) + .unwrap_or([0.0, 0.0, 0.0, 0.0]) + } +} + +impl ShadingPattern { + /// Encode the shading pattern. + pub fn encode(&self) -> EncodedShadingPattern { + self.encode_within(None) + } + + /// PdfCraft patch: encode the shading pattern for drawing inside `area` (device space) only. + /// Mesh shadings are sampled pixel by pixel, and only there (see `TriangleSampler`). + pub fn encode_within(&self, area: Option) -> EncodedShadingPattern { + let base_transform; + + let shading_type = match self.shading.shading_type.as_ref() { + ShadingType::FunctionBased { + domain, + matrix, + function, + } => { + base_transform = (self.matrix * *matrix).inverse(); + encode_function_shading(domain, function) + } + ShadingType::RadialAxial { + coords, + domain, + function, + extend, + axial, + } => { + let (encoded, initial_transform) = + encode_axial_shading(*coords, *domain, function, *extend, *axial); + + base_transform = initial_transform * self.matrix.inverse(); + + encoded + } + ShadingType::TriangleMesh { + triangles, + function, + } => { + let full_transform = self.matrix; + let mut sampler = TriangleSampler::new(area); + sampler.sample(triangles, full_transform); + let samples = sampler.map; + + base_transform = Affine::IDENTITY; + + EncodedShadingType::Sampled { + samples, + function: function.clone(), + } + } + ShadingType::CoonsPatchMesh { patches, function } => { + // PdfCraft patch: one patch's triangles at a time (each is 722); collecting all + // of them first held 150 KB per patch before any limit applied. + let full_transform = self.matrix; + let mut sampler = TriangleSampler::new(area); + let mut triangles = vec![]; + for patch in patches { + if !mesh_budget_left() { + break; + } + triangles.clear(); + patch.to_triangles(&mut triangles); + if !sampler.sample(&triangles, full_transform) { + break; + } + } + let samples = sampler.map; + + base_transform = Affine::IDENTITY; + + EncodedShadingType::Sampled { + samples, + function: function.clone(), + } + } + ShadingType::TensorProductPatchMesh { patches, function } => { + // PdfCraft patch: one patch's triangles at a time, as for Coons patches. + let full_transform = self.matrix; + let mut sampler = TriangleSampler::new(area); + let mut triangles = vec![]; + for patch in patches { + if !mesh_budget_left() { + break; + } + triangles.clear(); + patch.to_triangles(&mut triangles); + if !sampler.sample(&triangles, full_transform) { + break; + } + } + let samples = sampler.map; + + base_transform = Affine::IDENTITY; + + EncodedShadingType::Sampled { + samples, + function: function.clone(), + } + } + ShadingType::Dummy => { + base_transform = Affine::IDENTITY; + + EncodedShadingType::Dummy + } + }; + + let color_space = self.shading.color_space.clone(); + + let background_color = self + .shading + .background + .as_ref() + .map(|b| color_space.to_rgba(b, 1.0, false)) + .unwrap_or(AlphaColor::TRANSPARENT); + + EncodedShadingPattern { + color_space, + background_color, + shading_type, + base_transform, + opacity: self.opacity, + transfer_function: self.transfer_function.clone(), + } + } +} + +fn encode_axial_shading( + coords: [f32; 6], + domain: [f32; 2], + function: &ShadingFunction, + extend: [bool; 2], + is_axial: bool, +) -> (EncodedShadingType, Affine) { + let initial_transform; + + let params = if is_axial { + let [x_0, y_0, x_1, y_1, _, _] = coords; + + initial_transform = ts_from_line_to_line( + Point::new(x_0 as f64, y_0 as f64), + Point::new(x_1 as f64, y_1 as f64), + Point::ZERO, + Point::new(1.0, 0.0), + ); + + RadialAxialParams::Axial + } else { + let [x_0, y_0, r0, x_1, y_1, r_1] = coords; + + initial_transform = Affine::translate((-x_0 as f64, -y_0 as f64)); + let new_x1 = x_1 - x_0; + let new_y1 = y_1 - y_0; + + let p1 = Point::new(new_x1 as f64, new_y1 as f64); + let r = Point::new(r0 as f64, r_1 as f64); + + RadialAxialParams::Radial { p1, r } + }; + + ( + EncodedShadingType::RadialAxial { + function: function.clone(), + params, + domain, + extend, + }, + initial_transform, + ) +} + +/// PdfCraft patch: pixel visits one mesh shading may make, per pixel of the area it is drawn in +/// and per triangle. Sampling walks each triangle's bounding box (rounded out to whole pixels), +/// so triangles far off the page (a fuzzed patch decoded onto ยฑ40000) asked for 65536 ร— 65536 +/// samples, and many triangles covering the same pixels repeat the work. A triangle's box is +/// about twice its area, and a sub-pixel triangle's box is at most 2 ร— 2 pixels. +const MESH_VISITS_PER_PIXEL: u64 = 8; +const MESH_VISITS_PER_TRIANGLE: u64 = 4; + +/// PdfCraft patch: the visits allowed when no area is given (the public `encode`). +const MESH_VISITS_WITHOUT_AREA: u64 = 1 << 24; + +/// PdfCraft patch: mesh triangles one page may sample in all, across every shading painted on +/// it. A shading is triangulated again each time it is painted (2^16 patches are 47 million +/// triangles, several seconds), so painting one a thousand times took hours. A 200 ร— 200 +/// gradient mesh is 29 million triangles. +const MAX_PAGE_MESH_TRIANGLES: u64 = 1 << 25; + +std::thread_local! { + static MESH_TRIANGLES_LEFT: core::cell::Cell = + const { core::cell::Cell::new(MAX_PAGE_MESH_TRIANGLES) }; +} + +/// PdfCraft patch: a page starts with a fresh mesh triangle budget (called from +/// `Context::new`). +pub(crate) fn reset_mesh_budget() { + MESH_TRIANGLES_LEFT.with(|n| n.set(MAX_PAGE_MESH_TRIANGLES)); +} + +/// PdfCraft patch: `false` once this page's mesh triangle budget is spent. +fn mesh_budget_left() -> bool { + MESH_TRIANGLES_LEFT.with(|n| n.get() > 0) +} + +/// PdfCraft patch: samples mesh triangles into a map with one entry per pixel, only inside +/// `area` (device space), within the visit budget above. +struct TriangleSampler { + map: FxHashMap<(u16, u16), ColorComponents>, + area: kurbo::Rect, + visits_left: u64, +} + +impl TriangleSampler { + fn new(area: Option) -> Self { + let visits_left = match area { + Some(a) => (a.width().max(0.0).ceil() as u64) + .saturating_mul(a.height().max(0.0).ceil() as u64) + .saturating_mul(MESH_VISITS_PER_PIXEL), + None => MESH_VISITS_WITHOUT_AREA, + }; + Self { + map: FxHashMap::default(), + area: area.unwrap_or(kurbo::Rect::new(0.0, 0.0, 65536.0, 65536.0)), + visits_left, + } + } + + /// Sample `triangles`; `false` once a budget is spent (the rest is left unsampled). + fn sample(&mut self, triangles: &[Triangle], transform: Affine) -> bool { + let page_left = MESH_TRIANGLES_LEFT.with(core::cell::Cell::get); + if page_left < triangles.len() as u64 { + MESH_TRIANGLES_LEFT.with(|n| n.set(0)); + warn!("page mesh triangle budget exceeded; the rest of the shading is not drawn"); + return false; + } + MESH_TRIANGLES_LEFT.with(|n| n.set(page_left - triangles.len() as u64)); + for t in triangles { + let t = { + let p0 = transform * t.p0.point; + let p1 = transform * t.p1.point; + let p2 = transform * t.p2.point; + + let mut v0 = t.p0.clone(); + v0.point = p0; + let mut v1 = t.p1.clone(); + v1.point = p1; + let mut v2 = t.p2.clone(); + v2.point = p2; + + Triangle::new(v0, v1, v2) + }; + + // Only the part of the triangle inside `area`; only such triangles add visits. + let bbox = t.bounding_box().intersect(self.area); + if bbox.width() <= 0.0 || bbox.height() <= 0.0 { + continue; + } + self.visits_left = self.visits_left.saturating_add(MESH_VISITS_PER_TRIANGLE); + + for y in (bbox.y0.floor() as u16)..(bbox.y1.ceil() as u16) { + for x in (bbox.x0.floor() as u16)..(bbox.x1.ceil() as u16) { + if self.visits_left == 0 { + warn!("mesh shading visit budget exceeded; the rest is not drawn"); + return false; + } + self.visits_left -= 1; + let point = Point::new(x as f64, y as f64); + if t.contains_point(point) { + self.map.insert((x, y), t.interpolate(point)); + } + } + } + } + true + } +} + +fn encode_function_shading(domain: &[f32; 4], function: &ShadingFunction) -> EncodedShadingType { + let domain = kurbo::Rect::new( + domain[0] as f64, + domain[2] as f64, + domain[1] as f64, + domain[3] as f64, + ); + + EncodedShadingType::FunctionBased { + domain, + function: function.clone(), + } +} + +#[derive(Debug)] +pub(crate) enum RadialAxialParams { + Axial, + Radial { p1: Point, r: Point }, +} + +#[derive(Debug)] +pub(crate) enum EncodedShadingType { + FunctionBased { + domain: kurbo::Rect, + function: ShadingFunction, + }, + RadialAxial { + function: ShadingFunction, + params: RadialAxialParams, + domain: [f32; 2], + extend: [bool; 2], + }, + Sampled { + samples: FxHashMap<(u16, u16), ColorComponents>, + function: Option, + }, + Dummy, +} + +impl EncodedShadingType { + pub(crate) fn eval( + &self, + pos: Point, + bg_color: AlphaColor, + color_space: &ColorSpace, + ) -> Option { + match self { + Self::FunctionBased { domain, function } => { + if !domain.contains(pos) { + Some(bg_color) + } else { + let out = function.eval(&smallvec![pos.x as f32, pos.y as f32])?; + // TODO: Clamp out-of-range values. + Some(color_space.to_rgba(&out, 1.0, false)) + } + } + Self::RadialAxial { + function, + params, + domain, + extend, + } => { + let (t0, t1) = (domain[0], domain[1]); + + let mut t = match params { + RadialAxialParams::Axial => pos.x as f32, + RadialAxialParams::Radial { p1, r } => { + radial_pos(&pos, p1, *r, extend[0], extend[1]).unwrap_or(f32::MIN) + } + }; + + if t == f32::MIN { + return Some(bg_color); + } + + if t < 0.0 { + if extend[0] { + t = 0.0; + } else { + return Some(bg_color); + } + } else if t > 1.0 { + if extend[1] { + t = 1.0; + } else { + return Some(bg_color); + } + } + + let t = t0 + (t1 - t0) * t; + + let val = function.eval(&smallvec![t])?; + + Some(color_space.to_rgba(&val, 1.0, false)) + } + Self::Sampled { samples, function } => { + let sample_point = (pos.x as u16, pos.y as u16); + + if let Some(color) = samples.get(&sample_point) { + if let Some(function) = function { + let val = function.eval(&color.to_smallvec())?; + Some(color_space.to_rgba(&val, 1.0, false)) + } else { + Some(color_space.to_rgba(color, 1.0, false)) + } + } else { + Some(bg_color) + } + } + Self::Dummy => Some(AlphaColor::TRANSPARENT), + } + } +} + +fn ts_from_line_to_line(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Affine { + let unit_to_line1 = unit_to_line(src1, src2); + let line1_to_unit = unit_to_line1.inverse(); + let unit_to_line2 = unit_to_line(dst1, dst2); + + unit_to_line2 * line1_to_unit +} + +fn unit_to_line(p0: Point, p1: Point) -> Affine { + Affine::new([ + p1.y - p0.y, + p0.x - p1.x, + p1.x - p0.x, + p1.y - p0.y, + p0.x, + p0.y, + ]) +} + +fn radial_pos( + pos: &Point, + p1: &Point, + r: Point, + min_extend: bool, + max_extend: bool, +) -> Option { + let r0 = r.x as f32; + let dx = p1.x as f32; + let dy = p1.y as f32; + let dr = r.y as f32 - r0; + + let px = pos.x as f32; + let py = pos.y as f32; + + let a = dx * dx + dy * dy - dr * dr; + let b = -2.0 * (px * dx + py * dy + r0 * dr); + let c = px * px + py * py - r0 * r0; + + let discriminant = b * b - 4.0 * a * c; + + // No solution available. + if discriminant < 0.0 { + return None; + } + + if a.abs() < 1e-6 { + if b.abs() < 1e-6 { + return None; + } + + let t = -c / b; + + if (!min_extend && t < 0.0) || (!max_extend && t > 1.0) { + return None; + } + + let r_t = r0 + dr * t; + if r_t < 0.0 { + return None; + } + + return Some(t); + } + + let sqrt_d = discriminant.sqrt(); + let t1 = (-b - sqrt_d) / (2.0 * a); + let t2 = (-b + sqrt_d) / (2.0 * a); + + let max = t1.max(t2); + let mut take_max = Some(max); + let min = t1.min(t2); + let mut take_min = Some(min); + + if (!min_extend && min < 0.0) || r0 + dr * min < 0.0 { + take_min = None; + } + + if (!max_extend && max > 1.0) || r0 + dr * max < 0.0 { + take_max = None; + } + + match (take_min, take_max) { + (Some(_), Some(max)) => Some(max), + (Some(min), None) => Some(min), + (None, Some(max)) => Some(max), + (None, None) => None, + } +} diff --git a/vendor/hayro-interpret/src/font/blob.rs b/vendor/hayro-interpret/src/font/blob.rs new file mode 100644 index 00000000..6eabc26b --- /dev/null +++ b/vendor/hayro-interpret/src/font/blob.rs @@ -0,0 +1,335 @@ +use crate::font::UNITS_PER_EM; +use crate::font::outline::OutlinePath; +use kurbo::BezPath; +use skrifa::instance::{LocationRef, Size}; +use skrifa::metrics::GlyphMetrics; +use skrifa::outline::{DrawSettings, Engine, HintingInstance, HintingOptions, Target}; +use skrifa::raw::TableProvider; +use skrifa::raw::ps::cff::{CffFontRef, Subfont, charset::Charset, v1::Cff}; +use skrifa::raw::ps::string::Sid; +use skrifa::raw::ps::type1::Type1Font; +use skrifa::raw::tables::post::DEFAULT_GLYPH_NAMES; +use skrifa::raw::{FontData as ReadFontData, FontRead}; +use skrifa::{FontRef, GlyphId, MetadataProvider, OutlineGlyphCollection}; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; +use yoke::{Yoke, Yokeable}; + +type FontData = Arc + Send + Sync>; +type OpenTypeFontYoke = Yoke, FontData>; +type CffFontYoke = Yoke, FontData>; + +/// A font blob for type 1 fonts. +#[derive(Clone)] +pub(crate) struct Type1FontBlob(Arc); + +impl Debug for Type1FontBlob { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Type1 Font {{ .. }}") + } +} + +impl Type1FontBlob { + pub(crate) fn new(data: FontData) -> Option { + let table = Type1Font::new(data.as_ref().as_ref()).ok()?; + Some(Self(Arc::new(table))) + } + + pub(crate) fn table(&self) -> &Type1Font { + self.0.as_ref() + } + + pub(crate) fn outline_glyph(&self, gid: GlyphId) -> BezPath { + let mut path = OutlinePath::new(); + let _ = self.table().draw(gid, Some(UNITS_PER_EM), &mut path); + + path.take() + } +} + +/// A font blob for CFF-based fonts. +#[derive(Clone)] +pub(crate) struct CffFontBlob(Arc); + +impl Debug for CffFontBlob { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Type1 Font {{ .. }}") + } +} + +impl CffFontBlob { + pub(crate) fn new(data: FontData) -> Option { + let font = CffFontRef::new(data.as_ref().as_ref(), 0, None).ok()?; + let cff = Cff::read(ReadFontData::new(data.as_ref().as_ref())).ok()?; + let charset = font.charset(); + let subfonts = (0..font.num_subfonts()) + .map(|index| font.subfont(index, &[]).ok()) + .collect::>>()?; + + let yoke = Yoke::, FontData>::attach_to_cart(data.clone(), |data| { + let bytes = data.as_ref(); + let font = CffFontRef::new(bytes, 0, None).unwrap(); + let cff = Cff::read(ReadFontData::new(bytes)).unwrap(); + let charset = font.charset(); + let subfonts = (0..font.num_subfonts()) + .map(|index| font.subfont(index, &[]).unwrap()) + .collect(); + CFFYoke { + font, + cff, + charset, + subfonts, + } + }); + + let _ = (cff, charset, subfonts); + Some(Self(Arc::new(yoke))) + } + + pub(crate) fn font_data(&self) -> FontData { + self.0.backing_cart().clone() + } + + pub(crate) fn font(&self) -> &CffFontRef<'_> { + &self.0.as_ref().get().font + } + + fn charset(&self) -> Option<&Charset<'_>> { + self.0.as_ref().get().charset.as_ref() + } + + fn subfont(&self, glyph: GlyphId) -> Option<&Subfont> { + let index = self.font().subfont_index(glyph)? as usize; + self.0.as_ref().get().subfonts.get(index) + } + + pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + let mut path = OutlinePath::new(); + let Some(subfont) = self.subfont(glyph) else { + return BezPath::new(); + }; + + let _ = self + .font() + .draw(subfont, glyph, &[], Some(UNITS_PER_EM), &mut path); + + path.take() + } + + pub(crate) fn glyph_names(&self) -> Vec<(GlyphId, String)> { + let Some(charset) = self.charset() else { + return Vec::new(); + }; + + // TODO: Avoid collecting here. + charset + .iter() + .filter_map(|(gid, sid)| { + let bytes = self.0.as_ref().get().cff.string(sid)?; + let name = std::str::from_utf8(bytes).ok()?.to_string(); + Some((gid, name)) + }) + .collect() + } + + pub(crate) fn glyph_index_by_name(&self, name: &str) -> Option { + // TODO: This is probably slow to do repeatedly? + self.charset()?.iter().find_map(|(gid, sid)| { + let bytes = self.0.as_ref().get().cff.string(sid)?; + (bytes == name.as_bytes()).then_some(gid) + }) + } + + pub(crate) fn glyph_index_by_cid(&self, cid: u16) -> Option { + self.charset()?.glyph_id(Sid::new(cid)) + } + + pub(crate) fn glyph_index(&self, code: u8) -> Option { + self.font() + .encoding() + .and_then(|encoding| encoding.map(code)) + } + + pub(crate) fn num_glyphs(&self) -> u32 { + self.font().num_glyphs() + } + + pub(crate) fn is_cid(&self) -> bool { + self.font().is_cid() + } +} + +/// A font blob for OpenType fonts. +#[derive(Clone)] +pub(crate) struct OpenTypeFontBlob { + yoke: Arc, + cff_blob: Option, +} + +impl Debug for OpenTypeFontBlob { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "OpenType Font {{ .. }}") + } +} + +impl OpenTypeFontBlob { + pub(crate) fn new(data: FontData, index: u32) -> Option { + // Check first whether the font is valid so we can unwrap in the closure. + let f = FontRef::from_index(data.as_ref().as_ref(), index).ok()?; + // Reject fonts with invalid post table version, fixes pdf.js issue 9462. Not sure if there + // is a better fix, for some reason skrifa accepts the font which is completely invalid. + let invalid = f.post().is_ok_and(|p| { + !matches!( + p.version().to_major_minor(), + (1, 0) | (2, 0) | (2, 5) | (3, 0) + ) + }); + + if invalid { + return None; + } + + // We store this separately because we want to be able to cache the subfonts + // of a CFF OpenType font, which is not possible using the current skrifa API. + // Hopefully there will be some way in the future. + let cff_blob = f + .cff() + .ok() + .map(|cff| Arc::new(cff.offset_data().as_ref().to_vec()) as FontData) + .and_then(CffFontBlob::new); + + let font_ref_yoke = + Yoke::, FontData>::attach_to_cart(data.clone(), |data| { + let font_ref = FontRef::from_index(data.as_ref(), index).unwrap(); + + let hinting_instance = if font_ref.outline_glyphs().require_interpreter() { + HintingInstance::new( + &font_ref.outline_glyphs(), + Size::new(UNITS_PER_EM), + LocationRef::default(), + HintingOptions { + engine: Engine::Interpreter, + target: Target::Mono, + }, + ) + .ok() + } else { + None + }; + + OTFYoke { + font_ref: font_ref.clone(), + outline_glyphs: font_ref.outline_glyphs(), + hinting_instance, + glyph_metrics: font_ref + .glyph_metrics(Size::new(UNITS_PER_EM), LocationRef::default()), + } + }); + + Some(Self { + yoke: Arc::new(font_ref_yoke), + cff_blob, + }) + } + + pub(crate) fn font_data(&self) -> FontData { + self.yoke.backing_cart().clone() + } + + pub(crate) fn font_ref(&self) -> &FontRef<'_> { + &self.yoke.as_ref().get().font_ref + } + + pub(crate) fn glyph_metrics(&self) -> &GlyphMetrics<'_> { + &self.yoke.as_ref().get().glyph_metrics + } + + pub(crate) fn glyph_names(&self) -> HashMap { + // Note: We don't call the `glyph_name` method provided by read-fonts because + // calling it repeatedly is very slow. + let mut glyph_names = HashMap::new(); + let Ok(post) = self.font_ref().post() else { + return glyph_names; + }; + + match post.version().to_major_minor() { + (1, 0) => { + for (gid, name) in DEFAULT_GLYPH_NAMES.iter().enumerate() { + glyph_names.insert((*name).to_string(), GlyphId::new(gid as u32)); + } + } + (2, 0) => { + let Some(name_indices) = post.glyph_name_index() else { + return glyph_names; + }; + let Some(string_data) = post.string_data() else { + return glyph_names; + }; + let custom_names = string_data + .iter() + .map(|entry| entry.ok().map(|name| name.as_str())) + .collect::>(); + + for (gid, idx) in name_indices.iter().enumerate() { + let idx = idx.get() as usize; + if let Some(name) = DEFAULT_GLYPH_NAMES.get(idx).copied().or_else(|| { + custom_names + .get(idx.saturating_sub(DEFAULT_GLYPH_NAMES.len())) + .copied() + .flatten() + }) { + glyph_names.insert(name.to_string(), GlyphId::new(gid as u32)); + } + } + } + _ => {} + } + + glyph_names + } + + fn outline_glyphs(&self) -> &OutlineGlyphCollection<'_> { + &self.yoke.as_ref().get().outline_glyphs + } + + pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + if let Some(cff_blob) = self.cff_blob.as_ref() { + return cff_blob.outline_glyph(glyph); + } + + let mut path = OutlinePath::new(); + + let draw_settings = if let Some(instance) = self.yoke.get().hinting_instance.as_ref() { + // Note: We always hint at the font size `UNITS_PER_EM`, which obviously isn't very useful. We don't do this + // for better text quality (right now), but instead because there are some PDFs with obscure fonts that + // actually render wrongly if hinting is disabled! + DrawSettings::hinted(instance, false) + } else { + DrawSettings::unhinted(Size::new(UNITS_PER_EM), LocationRef::default()) + }; + + let Some(outline) = self.outline_glyphs().get(glyph) else { + return BezPath::new(); + }; + + let _ = outline.draw(draw_settings, &mut path); + path.take() + } +} + +#[derive(Yokeable, Clone)] +struct OTFYoke<'a> { + font_ref: FontRef<'a>, + glyph_metrics: GlyphMetrics<'a>, + hinting_instance: Option, + outline_glyphs: OutlineGlyphCollection<'a>, +} + +#[derive(Yokeable, Clone)] +struct CFFYoke<'a> { + font: CffFontRef<'a>, + cff: Cff<'a>, + charset: Option>, + subfonts: Vec, +} diff --git a/vendor/hayro-interpret/src/font/cid.rs b/vendor/hayro-interpret/src/font/cid.rs new file mode 100644 index 00000000..b99dcbc1 --- /dev/null +++ b/vendor/hayro-interpret/src/font/cid.rs @@ -0,0 +1,645 @@ +use crate::font::blob::{CffFontBlob, OpenTypeFontBlob, Type1FontBlob}; +use crate::font::generated::glyph_names; +use crate::font::standard_font::select_standard_font; +use crate::font::{ + FallbackFontQuery, FontFlags, FontQuery, read_to_unicode, stretch_glyph, strip_subset_prefix, +}; +use crate::{CMapResolverFn, CacheKey, FontResolverFn}; +use hayro_cmap::{BfString, CMap, CharacterCollection, CidFamily, WritingMode}; +use hayro_syntax::object; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Name; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::*; +use hayro_syntax::object::{Array, Object}; +use kurbo::{BezPath, Vec2}; +use skrifa::attribute::Style; +use skrifa::charmap::MapVariant; +use skrifa::raw::collections::int_set::Domain; +use skrifa::{FontRef, GlyphId, MetadataProvider}; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::Arc; + +#[derive(Debug)] +pub(crate) struct Type0Font { + font_type: FontType, + horizontal: bool, + cache_key: u128, + dw: f32, + dw2: (f32, f32), + widths: HashMap, + encoding: CMap, + to_unicode: Option, + widths2: HashMap, + cid_to_gid_map: CidToGIdMap, + /// PostScript name from the PDF. + postscript_name: Option, + /// Font flags from the font descriptor. + font_flags: Option, + /// Whether this font is using a fallback (non-embedded) font. + fallback: bool, + /// Whether the `to_unicode` map is a UCS2 `CMap` (CID-indexed) rather than + /// a `ToUnicode` `CMap` (code-indexed). + to_unicode_is_cid_indexed: bool, + /// PdfCraft patch: whether the CIDs belong to an Adobe character collection (Japan1, GB1, + /// CNS1, Korea1), whose glyph numbering no substitute other than a CID-keyed CFF font shares. + adobe_collection: bool, +} + +impl Type0Font { + pub(crate) fn new( + dict: &Dict<'_>, + font_resolver: &FontResolverFn, + cmap_resolver: &CMapResolverFn, + ) -> Option { + let cmap = read_encoding(&dict.get::>(ENCODING)?, cmap_resolver)?; + + let horizontal = cmap.metadata().writing_mode != Some(WritingMode::Vertical); + + let descendant_font = dict + .get::>(DESCENDANT_FONTS)? + .iter::>() + .next()?; + let font_descriptor = descendant_font + .get::>(FONT_DESC) + .unwrap_or_default(); + + let character_collection = cmap + .metadata() + .character_collection + .clone() + .filter(|cc| cc.family != CidFamily::AdobeIdentity) + .or_else(|| read_cid_system_info(&descendant_font)); + + let (font_type, fallback, _is_standard_fallback) = match FontType::new(&font_descriptor) { + Some(ft) => (ft, false, false), + None => { + let (query, is_standard) = + if let Some((standard, _)) = select_standard_font(dict, &font_descriptor) { + (FontQuery::Standard(standard), true) + } else { + let mut query = FallbackFontQuery::new(dict); + query.character_collection = character_collection.clone(); + + warn!( + "unable to load CID font {} ({:?}), attempting fallback", + query.post_script_name.as_deref().unwrap_or("(no name)"), + dict.obj_id() + ); + + (FontQuery::Fallback(query), false) + }; + + let (data, index) = font_resolver(&query)?; + let blob = OpenTypeFontBlob::new(data.clone(), index) + .map(FontType::OpenType) + .or_else(|| CffFontBlob::new(data).map(FontType::Cff))?; + (blob, true, is_standard) + } + }; + + let default_width = descendant_font.get::(DW).unwrap_or(1000.0); + let dw2 = descendant_font + .get::<[f32; 2]>(DW2) + .map(|v| (v[0], v[1])) + .unwrap_or((880.0, -1000.0)); + + let widths = descendant_font + .get::>(W) + .and_then(|a| read_widths(&a)) + .unwrap_or_default(); + let widths2 = descendant_font + .get::>(W2) + .and_then(|a| read_widths2(&a)) + .unwrap_or_default(); + let cid_to_gid_map = CidToGIdMap::new(&descendant_font).unwrap_or_default(); + let cache_key = dict.cache_key(); + + let mut to_unicode = read_to_unicode(dict, cmap_resolver); + let mut to_unicode_is_cid_indexed = false; + + // If there is no ToUnicode map, try to get the UCS2 CMap. + if fallback + && to_unicode.is_none() + && let Some(cc) = character_collection.as_ref() + && let Some(ucs2_name) = cc.family.ucs2_cmap() + && let Some(data) = (cmap_resolver)(ucs2_name) + { + let resolver = cmap_resolver.clone(); + if let Some(ucs2_cmap) = CMap::parse(data, move |n| (resolver)(n)) { + to_unicode = Some(ucs2_cmap); + to_unicode_is_cid_indexed = true; + } + } + + let adobe_collection = character_collection + .as_ref() + .is_some_and(|cc| cc.family.ucs2_cmap().is_some()); + + let postscript_name = dict + .get::>(BASE_FONT) + .map(|n| strip_subset_prefix(n.as_str()).to_string()); + + // Extract font flags from descriptor + let font_flags = font_descriptor + .get::(FLAGS) + .and_then(FontFlags::from_bits); + + Some(Self { + cache_key, + horizontal, + encoding: cmap, + to_unicode, + font_type, + dw: default_width, + dw2, + widths, + widths2, + cid_to_gid_map, + postscript_name, + font_flags, + fallback, + to_unicode_is_cid_indexed, + adobe_collection, + }) + } + + pub(crate) fn map_code(&self, code: u32) -> GlyphId { + let Some(cid) = self.code_to_cid(code) else { + return GlyphId::NOTDEF; + }; + + // UCS2 maps are indexed by CIDs, while embededd `ToUnicode` maps are + // indexed by character code. + let to_unicode_key = if self.to_unicode_is_cid_indexed { + cid + } else { + code + }; + + if self.fallback + && let Some(glyph) = self.map_via_unicode(to_unicode_key) + { + // Yay, Unicode worked! + return glyph; + } + + // PdfCraft patch: a substitute that isn't CID-keyed numbers its glyphs its own way, so + // the CID of an Adobe character collection taken as its glyph index draws an unrelated + // glyph. Draw .notdef, as for a CID the font has no glyph for. + if self.fallback + && self.adobe_collection + && !matches!(&self.font_type, FontType::Cff(c) if c.is_cid()) + { + return GlyphId::NOTDEF; + } + + // At this point, not much we can do anymore. Just hope that the + // selected font has the right glyph order, and map via that. + + match &self.font_type { + FontType::OpenType(_) => self.cid_to_gid_map.map(cid as u16), + FontType::Cff(c) => { + if c.is_cid() { + // Very confusing stuff going on here, see https://github.com/mozilla/pdf.js/pull/15563. + // The PDF spec makes it sounds like cid-to-gid map should only be used for TrueType fonts, + // but Acrobat also seems to support it for CFF fonts with some weird behavior. + if matches!(self.cid_to_gid_map, CidToGIdMap::Identity) { + c.glyph_index_by_cid(cid as u16).unwrap_or(GlyphId::NOTDEF) + } else { + GlyphId::new(self.cid_to_gid_map.inverse_map(GlyphId::new(cid)) as u32) + } + } else { + self.cid_to_gid_map.map(cid as u16) + } + } + // Maybe we need similar processing to CFF fonts? But since + // Type1 fonts are invalid anyway, let's just ignore for now. + FontType::Type1(_) => GlyphId::new(cid), + } + } + + /// Map a character code (or CID) to a glyph ID + /// by first getting its Unicode and then looking up the codepoint in the + /// font's cmap. + fn map_via_unicode(&self, key: u32) -> Option { + let to_unicode = self.to_unicode.as_ref()?; + + let (character, selector) = to_unicode + .lookup_bf_string(key) + .or_else(|| { + for len in 0..4 { + if let Some(code) = to_unicode.lookup_cid_code(key, len) + && let Some(code) = char::from_u32(code.to_u32()) + { + return Some(BfString::Char(code)); + } + } + + None + }) + .and_then(|bf| match bf { + BfString::Char(c) => Some((c, None)), + // PdfCraft patch: a character and a variation selector is that character in a + // given form, as Adobe-Japan1 maps the JIS X 0208 forms of the kanji that + // JIS X 0213:2004 redrew (ๅ™‚ is U+5642 U+E0100). Other strings (ligatures) still + // have no glyph. + BfString::String(s) => { + let mut chars = s.chars(); + let character = chars.next()?; + let selector = chars.next().filter(|c| is_variation_selector(*c))?; + chars.next().is_none().then_some((character, Some(selector))) + } + })?; + + match &self.font_type { + FontType::OpenType(t) => { + let charmap = t.font_ref().charmap(); + // PdfCraft patch: the face's glyph for that form when its `cmap` has one, else its + // glyph for the character, as when a form isn't supported (Unicode FAQ on + // variation sequences). + if let Some(selector) = selector + && let Some(MapVariant::Variant(glyph)) = charmap.map_variant(character, selector) + { + return Some(glyph); + } + charmap.map(character) + } + FontType::Cff(c) => { + // Map codepoint to glyph name via AFL, and then look it up. + if let Some(name) = glyph_names::get_reverse(character) + && let Some(gid) = c.glyph_index_by_name(name) + { + Some(gid) + } else { + None + } + } + FontType::Type1(_) => { + // We could in theory map to a glyph via AFL and then look up + // the glyph index in the encoding, but since skrifa doesn't have + // an API for that and this is only really necessary for fallback + // fonts (which currently only supports OpenType and CFF as fallback), + // so we don't support this for now. + None + } + } + } + + fn code_to_cid(&self, code: u32) -> Option { + for byte_len in 1..=4_u8 { + if let Some(cid) = self.encoding.lookup_cid_code(code, byte_len) { + return Some(cid); + } + } + + None + } + + pub(crate) fn outline_glyph(&self, glyph: GlyphId, code: u32) -> BezPath { + let path = match &self.font_type { + FontType::OpenType(t) => t.outline_glyph(glyph), + FontType::Cff(c) => c.outline_glyph(glyph), + FontType::Type1(t) => t.outline_glyph(glyph), + }; + + if self.fallback + && let FontType::OpenType(t) = &self.font_type + && let Some(cid) = self.code_to_cid(code) + && let Some(actual_width) = t.glyph_metrics().advance_width(glyph) + // Only use an expected width if there is an explicit /W array, + // not the default width, as it leads to weird results from my testing. + && let Some(expected_width) = self.widths.get(&cid).copied() + { + return stretch_glyph(path, expected_width, actual_width); + } + + path + } + + pub(crate) fn font_data(&self) -> Option { + match &self.font_type { + FontType::OpenType(t) => Some(t.font_data()), + FontType::Cff(c) => Some(c.font_data()), + FontType::Type1(_) => None, + } + } + + /// Get the PostScript name. + pub(crate) fn postscript_name(&self) -> Option<&str> { + self.postscript_name.as_deref() + } + + /// Get the font weight (100-900, 400=normal, 700=bold). + /// + /// Returns `None` if weight cannot be determined (CFF fonts or invalid weight). + pub(crate) fn weight(&self) -> Option { + match &self.font_type { + FontType::OpenType(t) => { + let weight = t.font_ref().attributes().weight.value().round() as u32; + if weight > 0 { Some(weight) } else { None } + } + FontType::Cff(_) | FontType::Type1(_) => None, + } + } + + /// Check if font is italic based on font flags or font attributes. + pub(crate) fn is_italic(&self) -> bool { + if let Some(flags) = &self.font_flags + && flags.contains(FontFlags::ITALIC) + { + return true; + } + match &self.font_type { + FontType::OpenType(t) => t.font_ref().attributes().style != Style::Normal, + FontType::Cff(_) | FontType::Type1(_) => false, + } + } + + /// Check if font is serif based on font flags. + pub(crate) fn is_serif(&self) -> bool { + self.font_flags + .as_ref() + .is_some_and(|f| f.contains(FontFlags::SERIF)) + } + + /// Check if font is monospace based on font flags or font metrics. + pub(crate) fn is_monospace(&self) -> bool { + if let Some(flags) = &self.font_flags + && flags.contains(FontFlags::FIXED_PITCH) + { + return true; + } + match &self.font_type { + FontType::OpenType(t) => { + t.font_ref() + .metrics( + skrifa::instance::Size::unscaled(), + skrifa::instance::LocationRef::default(), + ) + .is_monospace + } + FontType::Cff(_) | FontType::Type1(_) => false, + } + } + + pub(crate) fn code_advance(&self, code: u32) -> Vec2 { + let cid = self.code_to_cid(code).unwrap_or(0); + if self.horizontal { + Vec2::new(self.horizontal_width(cid) as f64, 0.0) + } else if let Some([w, _, _]) = self.widths2.get(&cid) { + Vec2::new(0.0, *w as f64) + } else { + Vec2::new(0.0, self.dw2.1 as f64) + } + } + + fn horizontal_width(&self, cid: u32) -> f32 { + self.widths.get(&cid).copied().unwrap_or(self.dw) + } + + pub(crate) fn is_horizontal(&self) -> bool { + self.horizontal + } + + pub(crate) fn read_code(&self, bytes: &[u8], offset: usize) -> (u32, usize) { + let mut code = 0_u32; + let remaining = bytes.len() - offset; + + for n in 0..4.min(remaining) { + code = (code << 8) | bytes[offset + n] as u32; + + if self.encoding.lookup_cid_code(code, (n + 1) as u8).is_some() { + return (code, n + 1); + } + } + + (0, 1) + } + + pub(crate) fn origin_displacement(&self, code: u32) -> Vec2 { + let cid = self.code_to_cid(code).unwrap_or(0); + + if self.is_horizontal() { + Vec2::default() + } else if let Some([_, v1, v2]) = self.widths2.get(&cid) { + Vec2::new(-*v1 as f64, -*v2 as f64) + } else { + Vec2::new(-self.horizontal_width(cid) as f64 / 2.0, -self.dw2.0 as f64) + } + } + + pub(crate) fn char_code_to_unicode(&self, code: u32) -> Option { + if let Some(to_unicode) = &self.to_unicode { + let key = if self.to_unicode_is_cid_indexed { + self.code_to_cid(code).unwrap_or(0) + } else { + code + }; + return to_unicode.lookup_bf_string(key); + } + + None + } +} + +impl CacheKey for Type0Font { + fn cache_key(&self) -> u128 { + self.cache_key + } +} + +#[derive(Debug)] +enum FontType { + /// An OpenType font. + OpenType(OpenTypeFontBlob), + /// A CFF font. + Cff(CffFontBlob), + /// A Type1 font. (Yes, there actually exist PDFs that embed a Type1 font in a + /// CID font :)) + Type1(Type1FontBlob), +} + +impl FontType { + fn new(descriptor: &Dict<'_>) -> Option { + // PDF has a distinction between CIDFontType0 and CIDFontType 2, with + // specific requirements what font type can appear where. However, some + // PDFs use wrong metadata, so we simply bruteforce without even looking + // at the metadata. + + let data = descriptor + .get::>(FONT_FILE2) + .or_else(|| descriptor.get::>(FONT_FILE3)) + // See PDFBOX-2599. Apparently, Acrobat accepts this as well. + .or_else(|| descriptor.get::>(FONT_FILE))? + .decoded() + .ok()?; + + let data = Arc::new(data.into_owned()); + + let parsed = if let Ok(_font_ref) = FontRef::from_index(data.as_ref(), 0) { + // It's an OpenType font, either TrueType or CFF. + Self::OpenType(OpenTypeFontBlob::new(data, 0)?) + } else if let Some(cff) = CffFontBlob::new(data.clone()) { + // It's a CFF font. + Self::Cff(cff) + } else if let Some(t1) = Type1FontBlob::new(data) { + // It's a Type1 (PFB) font. + Self::Type1(t1) + } else { + return None; + }; + + Some(parsed) + } +} + +#[derive(Debug, Default)] +enum CidToGIdMap { + #[default] + Identity, + Mapped { + forward: HashMap, + inverse: HashMap, + }, +} + +impl CidToGIdMap { + fn new(dict: &Dict<'_>) -> Option { + if let Some(name) = dict.get::>(CID_TO_GID_MAP) { + if name.deref() == IDENTITY { + Some(Self::Identity) + } else { + None + } + } else if let Some(stream) = dict.get::>(CID_TO_GID_MAP) { + let decoded = stream.decoded().ok()?; + let mut forward = HashMap::new(); + let mut inverse = HashMap::new(); + + for (cid, gid) in decoded.chunks_exact(2).enumerate() { + let gid = GlyphId::new(u16::from_be_bytes([gid[0], gid[1]]) as u32); + + forward.insert(cid as u16, gid); + inverse.insert(gid, cid as u16); + } + + Some(Self::Mapped { forward, inverse }) + } else { + None + } + } + + fn map(&self, code: u16) -> GlyphId { + match self { + Self::Identity => GlyphId::new(code as u32), + Self::Mapped { forward, .. } => forward.get(&code).copied().unwrap_or(GlyphId::NOTDEF), + } + } + + fn inverse_map(&self, gid: GlyphId) -> u16 { + match self { + Self::Identity => gid.to_u32() as u16, + Self::Mapped { inverse, .. } => { + inverse.get(&gid).copied().unwrap_or(gid.to_u32() as u16) + } + } + } +} + +/// PdfCraft patch: Variation Selectors (U+FE00โ€“U+FE0F) and their supplement (U+E0100โ€“U+E01EF). +fn is_variation_selector(c: char) -> bool { + matches!(c, '\u{FE00}'..='\u{FE0F}' | '\u{E0100}'..='\u{E01EF}') +} + +/// PdfCraft patch: the largest CID (ISO 32000-2 ยง9.7.2; CIDs are at most two bytes). +const MAX_CID: u32 = 65_535; + +fn read_widths(arr: &Array<'_>) -> Option> { + let mut map = HashMap::new(); + let mut iter = arr.flex_iter(); + + loop { + if let Some((mut first, range)) = iter.next::<(u32, Array<'_>)>() { + for width in range.iter::() { + if first > MAX_CID { + break; + } + map.insert(first, width); + first = first.checked_add(1)?; + } + } else if let Some((first, second, width)) = iter.next::<(u32, u32, f32)>() { + // PdfCraft patch: CIDs stop at 65535. A corrupt range such as + // `0 4294967295 500` inserted billions of entries (found by fuzzing). + for i in first..=second.min(MAX_CID) { + map.insert(i, width); + } + } else { + break; + } + } + + Some(map) +} + +fn read_widths2(arr: &Array<'_>) -> Option> { + let mut map = HashMap::new(); + let mut iter = arr.flex_iter(); + + loop { + if let Some((mut first, range)) = iter.next::<(u32, Array<'_>)>() { + let mut iter = range.iter::(); + + while let Some(w) = iter.next() { + let v1 = iter.next()?; + let v2 = iter.next()?; + map.insert(first, [w, v1, v2]); + first = first.checked_add(1)?; + } + } else if let Some((first, second, w, v1, v2)) = iter.next::<(u32, u32, f32, f32, f32)>() { + // PdfCraft patch: as in `read_widths`. + for i in first..=second.min(MAX_CID) { + map.insert(i, [w, v1, v2]); + } + } else { + break; + } + } + + Some(map) +} + +fn read_cid_system_info(descendant_font: &Dict<'_>) -> Option { + let info = descendant_font.get::>(CIDSYSTEMINFO)?; + let registry = info.get::>(REGISTRY)?; + let ordering = info.get::>(ORDERING)?; + let supplement = info.get::(SUPPLEMENT).unwrap_or(0); + let family = CidFamily::from_registry_ordering(registry.deref(), ordering.deref()); + + Some(CharacterCollection { family, supplement }) +} + +fn read_encoding(object: &Object<'_>, cmap_resolver: &CMapResolverFn) -> Option { + // TODO: Support fetching CMaps referenced via `usecmap` in the PDF. + match object { + Object::Name(n) => { + let cmap_type = hayro_cmap::CMapName::from_bytes(n.deref()); + match cmap_type { + hayro_cmap::CMapName::IdentityH => Some(CMap::identity_h()), + hayro_cmap::CMapName::IdentityV => Some(CMap::identity_v()), + _ => { + let data = (cmap_resolver)(cmap_type)?; + let resolver = cmap_resolver.clone(); + CMap::parse(data, move |n| (resolver)(n)) + } + } + } + Object::Stream(s) => { + let decoded = s.decoded().ok()?; + let resolver = cmap_resolver.clone(); + CMap::parse(&decoded, move |n| (resolver)(n)) + } + _ => None, + } +} diff --git a/vendor/hayro-interpret/src/font/generated/glyph_names.rs b/vendor/hayro-interpret/src/font/generated/glyph_names.rs new file mode 100644 index 00000000..b2dd9ab0 --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/glyph_names.rs @@ -0,0 +1,8119 @@ +use phf::phf_map; + +pub(crate) fn get(name: &str) -> Option<&'static str> { + GLYPH_NAMES.get(name).copied() +} + +pub(crate) fn get_reverse(unicode: char) -> Option<&'static str> { + REVERSE_GLYPH_NAMES.get(&unicode).copied() +} + +static GLYPH_NAMES: phf::Map<&'static str, &'static str> = phf_map! { + "A" => "\u{0041}", + "AE" => "\u{00C6}", + "AEacute" => "\u{01FC}", + "AEmacron" => "\u{01E2}", + "AEsmall" => "\u{F7E6}", + "Aacute" => "\u{00C1}", + "Aacutesmall" => "\u{F7E1}", + "Abreve" => "\u{0102}", + "Abreveacute" => "\u{1EAE}", + "Abrevecyrillic" => "\u{04D0}", + "Abrevedotbelow" => "\u{1EB6}", + "Abrevegrave" => "\u{1EB0}", + "Abrevehookabove" => "\u{1EB2}", + "Abrevetilde" => "\u{1EB4}", + "Acaron" => "\u{01CD}", + "Acircle" => "\u{24B6}", + "Acircumflex" => "\u{00C2}", + "Acircumflexacute" => "\u{1EA4}", + "Acircumflexdotbelow" => "\u{1EAC}", + "Acircumflexgrave" => "\u{1EA6}", + "Acircumflexhookabove" => "\u{1EA8}", + "Acircumflexsmall" => "\u{F7E2}", + "Acircumflextilde" => "\u{1EAA}", + "Acute" => "\u{F6C9}", + "Acutesmall" => "\u{F7B4}", + "Acyrillic" => "\u{0410}", + "Adblgrave" => "\u{0200}", + "Adieresis" => "\u{00C4}", + "Adieresiscyrillic" => "\u{04D2}", + "Adieresismacron" => "\u{01DE}", + "Adieresissmall" => "\u{F7E4}", + "Adotbelow" => "\u{1EA0}", + "Adotmacron" => "\u{01E0}", + "Agrave" => "\u{00C0}", + "Agravesmall" => "\u{F7E0}", + "Ahookabove" => "\u{1EA2}", + "Aiecyrillic" => "\u{04D4}", + "Ainvertedbreve" => "\u{0202}", + "Alpha" => "\u{0391}", + "Alphatonos" => "\u{0386}", + "Amacron" => "\u{0100}", + "Amonospace" => "\u{FF21}", + "Aogonek" => "\u{0104}", + "Aring" => "\u{00C5}", + "Aringacute" => "\u{01FA}", + "Aringbelow" => "\u{1E00}", + "Aringsmall" => "\u{F7E5}", + "Asmall" => "\u{F761}", + "Atilde" => "\u{00C3}", + "Atildesmall" => "\u{F7E3}", + "Aybarmenian" => "\u{0531}", + "B" => "\u{0042}", + "Bcircle" => "\u{24B7}", + "Bdotaccent" => "\u{1E02}", + "Bdotbelow" => "\u{1E04}", + "Becyrillic" => "\u{0411}", + "Benarmenian" => "\u{0532}", + "Beta" => "\u{0392}", + "Bhook" => "\u{0181}", + "Blinebelow" => "\u{1E06}", + "Bmonospace" => "\u{FF22}", + "Brevesmall" => "\u{F6F4}", + "Bsmall" => "\u{F762}", + "Btopbar" => "\u{0182}", + "C" => "\u{0043}", + "Caarmenian" => "\u{053E}", + "Cacute" => "\u{0106}", + "Caron" => "\u{F6CA}", + "Caronsmall" => "\u{F6F5}", + "Ccaron" => "\u{010C}", + "Ccedilla" => "\u{00C7}", + "Ccedillaacute" => "\u{1E08}", + "Ccedillasmall" => "\u{F7E7}", + "Ccircle" => "\u{24B8}", + "Ccircumflex" => "\u{0108}", + "Cdot" => "\u{010A}", + "Cdotaccent" => "\u{010A}", + "Cedillasmall" => "\u{F7B8}", + "Chaarmenian" => "\u{0549}", + "Cheabkhasiancyrillic" => "\u{04BC}", + "Checyrillic" => "\u{0427}", + "Chedescenderabkhasiancyrillic" => "\u{04BE}", + "Chedescendercyrillic" => "\u{04B6}", + "Chedieresiscyrillic" => "\u{04F4}", + "Cheharmenian" => "\u{0543}", + "Chekhakassiancyrillic" => "\u{04CB}", + "Cheverticalstrokecyrillic" => "\u{04B8}", + "Chi" => "\u{03A7}", + "Chook" => "\u{0187}", + "Circumflexsmall" => "\u{F6F6}", + "Cmonospace" => "\u{FF23}", + "Coarmenian" => "\u{0551}", + "Csmall" => "\u{F763}", + "D" => "\u{0044}", + "DZ" => "\u{01F1}", + "DZcaron" => "\u{01C4}", + "Daarmenian" => "\u{0534}", + "Dafrican" => "\u{0189}", + "Dcaron" => "\u{010E}", + "Dcedilla" => "\u{1E10}", + "Dcircle" => "\u{24B9}", + "Dcircumflexbelow" => "\u{1E12}", + "Dcroat" => "\u{0110}", + "Ddotaccent" => "\u{1E0A}", + "Ddotbelow" => "\u{1E0C}", + "Decyrillic" => "\u{0414}", + "Deicoptic" => "\u{03EE}", + "Delta" => "\u{2206}", + "Deltagreek" => "\u{0394}", + "Dhook" => "\u{018A}", + "Dieresis" => "\u{F6CB}", + "DieresisAcute" => "\u{F6CC}", + "DieresisGrave" => "\u{F6CD}", + "Dieresissmall" => "\u{F7A8}", + "Digammagreek" => "\u{03DC}", + "Djecyrillic" => "\u{0402}", + "Dlinebelow" => "\u{1E0E}", + "Dmonospace" => "\u{FF24}", + "Dotaccentsmall" => "\u{F6F7}", + "Dslash" => "\u{0110}", + "Dsmall" => "\u{F764}", + "Dtopbar" => "\u{018B}", + "Dz" => "\u{01F2}", + "Dzcaron" => "\u{01C5}", + "Dzeabkhasiancyrillic" => "\u{04E0}", + "Dzecyrillic" => "\u{0405}", + "Dzhecyrillic" => "\u{040F}", + "E" => "\u{0045}", + "Eacute" => "\u{00C9}", + "Eacutesmall" => "\u{F7E9}", + "Ebreve" => "\u{0114}", + "Ecaron" => "\u{011A}", + "Ecedillabreve" => "\u{1E1C}", + "Echarmenian" => "\u{0535}", + "Ecircle" => "\u{24BA}", + "Ecircumflex" => "\u{00CA}", + "Ecircumflexacute" => "\u{1EBE}", + "Ecircumflexbelow" => "\u{1E18}", + "Ecircumflexdotbelow" => "\u{1EC6}", + "Ecircumflexgrave" => "\u{1EC0}", + "Ecircumflexhookabove" => "\u{1EC2}", + "Ecircumflexsmall" => "\u{F7EA}", + "Ecircumflextilde" => "\u{1EC4}", + "Ecyrillic" => "\u{0404}", + "Edblgrave" => "\u{0204}", + "Edieresis" => "\u{00CB}", + "Edieresissmall" => "\u{F7EB}", + "Edot" => "\u{0116}", + "Edotaccent" => "\u{0116}", + "Edotbelow" => "\u{1EB8}", + "Efcyrillic" => "\u{0424}", + "Egrave" => "\u{00C8}", + "Egravesmall" => "\u{F7E8}", + "Eharmenian" => "\u{0537}", + "Ehookabove" => "\u{1EBA}", + "Eightroman" => "\u{2167}", + "Einvertedbreve" => "\u{0206}", + "Eiotifiedcyrillic" => "\u{0464}", + "Elcyrillic" => "\u{041B}", + "Elevenroman" => "\u{216A}", + "Emacron" => "\u{0112}", + "Emacronacute" => "\u{1E16}", + "Emacrongrave" => "\u{1E14}", + "Emcyrillic" => "\u{041C}", + "Emonospace" => "\u{FF25}", + "Encyrillic" => "\u{041D}", + "Endescendercyrillic" => "\u{04A2}", + "Eng" => "\u{014A}", + "Enghecyrillic" => "\u{04A4}", + "Enhookcyrillic" => "\u{04C7}", + "Eogonek" => "\u{0118}", + "Eopen" => "\u{0190}", + "Epsilon" => "\u{0395}", + "Epsilontonos" => "\u{0388}", + "Ercyrillic" => "\u{0420}", + "Ereversed" => "\u{018E}", + "Ereversedcyrillic" => "\u{042D}", + "Escyrillic" => "\u{0421}", + "Esdescendercyrillic" => "\u{04AA}", + "Esh" => "\u{01A9}", + "Esmall" => "\u{F765}", + "Eta" => "\u{0397}", + "Etarmenian" => "\u{0538}", + "Etatonos" => "\u{0389}", + "Eth" => "\u{00D0}", + "Ethsmall" => "\u{F7F0}", + "Etilde" => "\u{1EBC}", + "Etildebelow" => "\u{1E1A}", + "Euro" => "\u{20AC}", + "Ezh" => "\u{01B7}", + "Ezhcaron" => "\u{01EE}", + "Ezhreversed" => "\u{01B8}", + "F" => "\u{0046}", + "Fcircle" => "\u{24BB}", + "Fdotaccent" => "\u{1E1E}", + "Feharmenian" => "\u{0556}", + "Feicoptic" => "\u{03E4}", + "Fhook" => "\u{0191}", + "Fitacyrillic" => "\u{0472}", + "Fiveroman" => "\u{2164}", + "Fmonospace" => "\u{FF26}", + "Fourroman" => "\u{2163}", + "Fsmall" => "\u{F766}", + "G" => "\u{0047}", + "GBsquare" => "\u{3387}", + "Gacute" => "\u{01F4}", + "Gamma" => "\u{0393}", + "Gammaafrican" => "\u{0194}", + "Gangiacoptic" => "\u{03EA}", + "Gbreve" => "\u{011E}", + "Gcaron" => "\u{01E6}", + "Gcedilla" => "\u{0122}", + "Gcircle" => "\u{24BC}", + "Gcircumflex" => "\u{011C}", + "Gcommaaccent" => "\u{0122}", + "Gdot" => "\u{0120}", + "Gdotaccent" => "\u{0120}", + "Gecyrillic" => "\u{0413}", + "Ghadarmenian" => "\u{0542}", + "Ghemiddlehookcyrillic" => "\u{0494}", + "Ghestrokecyrillic" => "\u{0492}", + "Gheupturncyrillic" => "\u{0490}", + "Ghook" => "\u{0193}", + "Gimarmenian" => "\u{0533}", + "Gjecyrillic" => "\u{0403}", + "Gmacron" => "\u{1E20}", + "Gmonospace" => "\u{FF27}", + "Grave" => "\u{F6CE}", + "Gravesmall" => "\u{F760}", + "Gsmall" => "\u{F767}", + "Gsmallhook" => "\u{029B}", + "Gstroke" => "\u{01E4}", + "H" => "\u{0048}", + "H18533" => "\u{25CF}", + "H18543" => "\u{25AA}", + "H18551" => "\u{25AB}", + "H22073" => "\u{25A1}", + "HPsquare" => "\u{33CB}", + "Haabkhasiancyrillic" => "\u{04A8}", + "Hadescendercyrillic" => "\u{04B2}", + "Hardsigncyrillic" => "\u{042A}", + "Hbar" => "\u{0126}", + "Hbrevebelow" => "\u{1E2A}", + "Hcedilla" => "\u{1E28}", + "Hcircle" => "\u{24BD}", + "Hcircumflex" => "\u{0124}", + "Hdieresis" => "\u{1E26}", + "Hdotaccent" => "\u{1E22}", + "Hdotbelow" => "\u{1E24}", + "Hmonospace" => "\u{FF28}", + "Hoarmenian" => "\u{0540}", + "Horicoptic" => "\u{03E8}", + "Hsmall" => "\u{F768}", + "Hungarumlaut" => "\u{F6CF}", + "Hungarumlautsmall" => "\u{F6F8}", + "Hzsquare" => "\u{3390}", + "I" => "\u{0049}", + "IAcyrillic" => "\u{042F}", + "IJ" => "\u{0132}", + "IUcyrillic" => "\u{042E}", + "Iacute" => "\u{00CD}", + "Iacutesmall" => "\u{F7ED}", + "Ibreve" => "\u{012C}", + "Icaron" => "\u{01CF}", + "Icircle" => "\u{24BE}", + "Icircumflex" => "\u{00CE}", + "Icircumflexsmall" => "\u{F7EE}", + "Icyrillic" => "\u{0406}", + "Idblgrave" => "\u{0208}", + "Idieresis" => "\u{00CF}", + "Idieresisacute" => "\u{1E2E}", + "Idieresiscyrillic" => "\u{04E4}", + "Idieresissmall" => "\u{F7EF}", + "Idot" => "\u{0130}", + "Idotaccent" => "\u{0130}", + "Idotbelow" => "\u{1ECA}", + "Iebrevecyrillic" => "\u{04D6}", + "Iecyrillic" => "\u{0415}", + "Ifraktur" => "\u{2111}", + "Igrave" => "\u{00CC}", + "Igravesmall" => "\u{F7EC}", + "Ihookabove" => "\u{1EC8}", + "Iicyrillic" => "\u{0418}", + "Iinvertedbreve" => "\u{020A}", + "Iishortcyrillic" => "\u{0419}", + "Imacron" => "\u{012A}", + "Imacroncyrillic" => "\u{04E2}", + "Imonospace" => "\u{FF29}", + "Iniarmenian" => "\u{053B}", + "Iocyrillic" => "\u{0401}", + "Iogonek" => "\u{012E}", + "Iota" => "\u{0399}", + "Iotaafrican" => "\u{0196}", + "Iotadieresis" => "\u{03AA}", + "Iotatonos" => "\u{038A}", + "Ismall" => "\u{F769}", + "Istroke" => "\u{0197}", + "Itilde" => "\u{0128}", + "Itildebelow" => "\u{1E2C}", + "Izhitsacyrillic" => "\u{0474}", + "Izhitsadblgravecyrillic" => "\u{0476}", + "J" => "\u{004A}", + "Jaarmenian" => "\u{0541}", + "Jcircle" => "\u{24BF}", + "Jcircumflex" => "\u{0134}", + "Jecyrillic" => "\u{0408}", + "Jheharmenian" => "\u{054B}", + "Jmonospace" => "\u{FF2A}", + "Jsmall" => "\u{F76A}", + "K" => "\u{004B}", + "KBsquare" => "\u{3385}", + "KKsquare" => "\u{33CD}", + "Kabashkircyrillic" => "\u{04A0}", + "Kacute" => "\u{1E30}", + "Kacyrillic" => "\u{041A}", + "Kadescendercyrillic" => "\u{049A}", + "Kahookcyrillic" => "\u{04C3}", + "Kappa" => "\u{039A}", + "Kastrokecyrillic" => "\u{049E}", + "Kaverticalstrokecyrillic" => "\u{049C}", + "Kcaron" => "\u{01E8}", + "Kcedilla" => "\u{0136}", + "Kcircle" => "\u{24C0}", + "Kcommaaccent" => "\u{0136}", + "Kdotbelow" => "\u{1E32}", + "Keharmenian" => "\u{0554}", + "Kenarmenian" => "\u{053F}", + "Khacyrillic" => "\u{0425}", + "Kheicoptic" => "\u{03E6}", + "Khook" => "\u{0198}", + "Kjecyrillic" => "\u{040C}", + "Klinebelow" => "\u{1E34}", + "Kmonospace" => "\u{FF2B}", + "Koppacyrillic" => "\u{0480}", + "Koppagreek" => "\u{03DE}", + "Ksicyrillic" => "\u{046E}", + "Ksmall" => "\u{F76B}", + "L" => "\u{004C}", + "LJ" => "\u{01C7}", + "LL" => "\u{F6BF}", + "Lacute" => "\u{0139}", + "Lambda" => "\u{039B}", + "Lcaron" => "\u{013D}", + "Lcedilla" => "\u{013B}", + "Lcircle" => "\u{24C1}", + "Lcircumflexbelow" => "\u{1E3C}", + "Lcommaaccent" => "\u{013B}", + "Ldot" => "\u{013F}", + "Ldotaccent" => "\u{013F}", + "Ldotbelow" => "\u{1E36}", + "Ldotbelowmacron" => "\u{1E38}", + "Liwnarmenian" => "\u{053C}", + "Lj" => "\u{01C8}", + "Ljecyrillic" => "\u{0409}", + "Llinebelow" => "\u{1E3A}", + "Lmonospace" => "\u{FF2C}", + "Lslash" => "\u{0141}", + "Lslashsmall" => "\u{F6F9}", + "Lsmall" => "\u{F76C}", + "M" => "\u{004D}", + "MBsquare" => "\u{3386}", + "Macron" => "\u{F6D0}", + "Macronsmall" => "\u{F7AF}", + "Macute" => "\u{1E3E}", + "Mcircle" => "\u{24C2}", + "Mdotaccent" => "\u{1E40}", + "Mdotbelow" => "\u{1E42}", + "Menarmenian" => "\u{0544}", + "Mmonospace" => "\u{FF2D}", + "Msmall" => "\u{F76D}", + "Mturned" => "\u{019C}", + "Mu" => "\u{039C}", + "N" => "\u{004E}", + "NJ" => "\u{01CA}", + "Nacute" => "\u{0143}", + "Ncaron" => "\u{0147}", + "Ncedilla" => "\u{0145}", + "Ncircle" => "\u{24C3}", + "Ncircumflexbelow" => "\u{1E4A}", + "Ncommaaccent" => "\u{0145}", + "Ndotaccent" => "\u{1E44}", + "Ndotbelow" => "\u{1E46}", + "Nhookleft" => "\u{019D}", + "Nineroman" => "\u{2168}", + "Nj" => "\u{01CB}", + "Njecyrillic" => "\u{040A}", + "Nlinebelow" => "\u{1E48}", + "Nmonospace" => "\u{FF2E}", + "Nowarmenian" => "\u{0546}", + "Nsmall" => "\u{F76E}", + "Ntilde" => "\u{00D1}", + "Ntildesmall" => "\u{F7F1}", + "Nu" => "\u{039D}", + "O" => "\u{004F}", + "OE" => "\u{0152}", + "OEsmall" => "\u{F6FA}", + "Oacute" => "\u{00D3}", + "Oacutesmall" => "\u{F7F3}", + "Obarredcyrillic" => "\u{04E8}", + "Obarreddieresiscyrillic" => "\u{04EA}", + "Obreve" => "\u{014E}", + "Ocaron" => "\u{01D1}", + "Ocenteredtilde" => "\u{019F}", + "Ocircle" => "\u{24C4}", + "Ocircumflex" => "\u{00D4}", + "Ocircumflexacute" => "\u{1ED0}", + "Ocircumflexdotbelow" => "\u{1ED8}", + "Ocircumflexgrave" => "\u{1ED2}", + "Ocircumflexhookabove" => "\u{1ED4}", + "Ocircumflexsmall" => "\u{F7F4}", + "Ocircumflextilde" => "\u{1ED6}", + "Ocyrillic" => "\u{041E}", + "Odblacute" => "\u{0150}", + "Odblgrave" => "\u{020C}", + "Odieresis" => "\u{00D6}", + "Odieresiscyrillic" => "\u{04E6}", + "Odieresissmall" => "\u{F7F6}", + "Odotbelow" => "\u{1ECC}", + "Ogoneksmall" => "\u{F6FB}", + "Ograve" => "\u{00D2}", + "Ogravesmall" => "\u{F7F2}", + "Oharmenian" => "\u{0555}", + "Ohm" => "\u{2126}", + "Ohookabove" => "\u{1ECE}", + "Ohorn" => "\u{01A0}", + "Ohornacute" => "\u{1EDA}", + "Ohorndotbelow" => "\u{1EE2}", + "Ohorngrave" => "\u{1EDC}", + "Ohornhookabove" => "\u{1EDE}", + "Ohorntilde" => "\u{1EE0}", + "Ohungarumlaut" => "\u{0150}", + "Oi" => "\u{01A2}", + "Oinvertedbreve" => "\u{020E}", + "Omacron" => "\u{014C}", + "Omacronacute" => "\u{1E52}", + "Omacrongrave" => "\u{1E50}", + "Omega" => "\u{2126}", + "Omegacyrillic" => "\u{0460}", + "Omegagreek" => "\u{03A9}", + "Omegaroundcyrillic" => "\u{047A}", + "Omegatitlocyrillic" => "\u{047C}", + "Omegatonos" => "\u{038F}", + "Omicron" => "\u{039F}", + "Omicrontonos" => "\u{038C}", + "Omonospace" => "\u{FF2F}", + "Oneroman" => "\u{2160}", + "Oogonek" => "\u{01EA}", + "Oogonekmacron" => "\u{01EC}", + "Oopen" => "\u{0186}", + "Oslash" => "\u{00D8}", + "Oslashacute" => "\u{01FE}", + "Oslashsmall" => "\u{F7F8}", + "Osmall" => "\u{F76F}", + "Ostrokeacute" => "\u{01FE}", + "Otcyrillic" => "\u{047E}", + "Otilde" => "\u{00D5}", + "Otildeacute" => "\u{1E4C}", + "Otildedieresis" => "\u{1E4E}", + "Otildesmall" => "\u{F7F5}", + "P" => "\u{0050}", + "Pacute" => "\u{1E54}", + "Pcircle" => "\u{24C5}", + "Pdotaccent" => "\u{1E56}", + "Pecyrillic" => "\u{041F}", + "Peharmenian" => "\u{054A}", + "Pemiddlehookcyrillic" => "\u{04A6}", + "Phi" => "\u{03A6}", + "Phook" => "\u{01A4}", + "Pi" => "\u{03A0}", + "Piwrarmenian" => "\u{0553}", + "Pmonospace" => "\u{FF30}", + "Psi" => "\u{03A8}", + "Psicyrillic" => "\u{0470}", + "Psmall" => "\u{F770}", + "Q" => "\u{0051}", + "Qcircle" => "\u{24C6}", + "Qmonospace" => "\u{FF31}", + "Qsmall" => "\u{F771}", + "R" => "\u{0052}", + "Raarmenian" => "\u{054C}", + "Racute" => "\u{0154}", + "Rcaron" => "\u{0158}", + "Rcedilla" => "\u{0156}", + "Rcircle" => "\u{24C7}", + "Rcommaaccent" => "\u{0156}", + "Rdblgrave" => "\u{0210}", + "Rdotaccent" => "\u{1E58}", + "Rdotbelow" => "\u{1E5A}", + "Rdotbelowmacron" => "\u{1E5C}", + "Reharmenian" => "\u{0550}", + "Rfraktur" => "\u{211C}", + "Rho" => "\u{03A1}", + "Ringsmall" => "\u{F6FC}", + "Rinvertedbreve" => "\u{0212}", + "Rlinebelow" => "\u{1E5E}", + "Rmonospace" => "\u{FF32}", + "Rsmall" => "\u{F772}", + "Rsmallinverted" => "\u{0281}", + "Rsmallinvertedsuperior" => "\u{02B6}", + "S" => "\u{0053}", + "SF010000" => "\u{250C}", + "SF020000" => "\u{2514}", + "SF030000" => "\u{2510}", + "SF040000" => "\u{2518}", + "SF050000" => "\u{253C}", + "SF060000" => "\u{252C}", + "SF070000" => "\u{2534}", + "SF080000" => "\u{251C}", + "SF090000" => "\u{2524}", + "SF100000" => "\u{2500}", + "SF110000" => "\u{2502}", + "SF190000" => "\u{2561}", + "SF200000" => "\u{2562}", + "SF210000" => "\u{2556}", + "SF220000" => "\u{2555}", + "SF230000" => "\u{2563}", + "SF240000" => "\u{2551}", + "SF250000" => "\u{2557}", + "SF260000" => "\u{255D}", + "SF270000" => "\u{255C}", + "SF280000" => "\u{255B}", + "SF360000" => "\u{255E}", + "SF370000" => "\u{255F}", + "SF380000" => "\u{255A}", + "SF390000" => "\u{2554}", + "SF400000" => "\u{2569}", + "SF410000" => "\u{2566}", + "SF420000" => "\u{2560}", + "SF430000" => "\u{2550}", + "SF440000" => "\u{256C}", + "SF450000" => "\u{2567}", + "SF460000" => "\u{2568}", + "SF470000" => "\u{2564}", + "SF480000" => "\u{2565}", + "SF490000" => "\u{2559}", + "SF500000" => "\u{2558}", + "SF510000" => "\u{2552}", + "SF520000" => "\u{2553}", + "SF530000" => "\u{256B}", + "SF540000" => "\u{256A}", + "Sacute" => "\u{015A}", + "Sacutedotaccent" => "\u{1E64}", + "Sampigreek" => "\u{03E0}", + "Scaron" => "\u{0160}", + "Scarondotaccent" => "\u{1E66}", + "Scaronsmall" => "\u{F6FD}", + "Scedilla" => "\u{015E}", + "Schwa" => "\u{018F}", + "Schwacyrillic" => "\u{04D8}", + "Schwadieresiscyrillic" => "\u{04DA}", + "Scircle" => "\u{24C8}", + "Scircumflex" => "\u{015C}", + "Scommaaccent" => "\u{0218}", + "Sdotaccent" => "\u{1E60}", + "Sdotbelow" => "\u{1E62}", + "Sdotbelowdotaccent" => "\u{1E68}", + "Seharmenian" => "\u{054D}", + "Sevenroman" => "\u{2166}", + "Shaarmenian" => "\u{0547}", + "Shacyrillic" => "\u{0428}", + "Shchacyrillic" => "\u{0429}", + "Sheicoptic" => "\u{03E2}", + "Shhacyrillic" => "\u{04BA}", + "Shimacoptic" => "\u{03EC}", + "Sigma" => "\u{03A3}", + "Sixroman" => "\u{2165}", + "Smonospace" => "\u{FF33}", + "Softsigncyrillic" => "\u{042C}", + "Ssmall" => "\u{F773}", + "Stigmagreek" => "\u{03DA}", + "T" => "\u{0054}", + "Tau" => "\u{03A4}", + "Tbar" => "\u{0166}", + "Tcaron" => "\u{0164}", + "Tcedilla" => "\u{0162}", + "Tcircle" => "\u{24C9}", + "Tcircumflexbelow" => "\u{1E70}", + "Tcommaaccent" => "\u{0162}", + "Tdotaccent" => "\u{1E6A}", + "Tdotbelow" => "\u{1E6C}", + "Tecyrillic" => "\u{0422}", + "Tedescendercyrillic" => "\u{04AC}", + "Tenroman" => "\u{2169}", + "Tetsecyrillic" => "\u{04B4}", + "Theta" => "\u{0398}", + "Thook" => "\u{01AC}", + "Thorn" => "\u{00DE}", + "Thornsmall" => "\u{F7FE}", + "Threeroman" => "\u{2162}", + "Tildesmall" => "\u{F6FE}", + "Tiwnarmenian" => "\u{054F}", + "Tlinebelow" => "\u{1E6E}", + "Tmonospace" => "\u{FF34}", + "Toarmenian" => "\u{0539}", + "Tonefive" => "\u{01BC}", + "Tonesix" => "\u{0184}", + "Tonetwo" => "\u{01A7}", + "Tretroflexhook" => "\u{01AE}", + "Tsecyrillic" => "\u{0426}", + "Tshecyrillic" => "\u{040B}", + "Tsmall" => "\u{F774}", + "Twelveroman" => "\u{216B}", + "Tworoman" => "\u{2161}", + "U" => "\u{0055}", + "Uacute" => "\u{00DA}", + "Uacutesmall" => "\u{F7FA}", + "Ubreve" => "\u{016C}", + "Ucaron" => "\u{01D3}", + "Ucircle" => "\u{24CA}", + "Ucircumflex" => "\u{00DB}", + "Ucircumflexbelow" => "\u{1E76}", + "Ucircumflexsmall" => "\u{F7FB}", + "Ucyrillic" => "\u{0423}", + "Udblacute" => "\u{0170}", + "Udblgrave" => "\u{0214}", + "Udieresis" => "\u{00DC}", + "Udieresisacute" => "\u{01D7}", + "Udieresisbelow" => "\u{1E72}", + "Udieresiscaron" => "\u{01D9}", + "Udieresiscyrillic" => "\u{04F0}", + "Udieresisgrave" => "\u{01DB}", + "Udieresismacron" => "\u{01D5}", + "Udieresissmall" => "\u{F7FC}", + "Udotbelow" => "\u{1EE4}", + "Ugrave" => "\u{00D9}", + "Ugravesmall" => "\u{F7F9}", + "Uhookabove" => "\u{1EE6}", + "Uhorn" => "\u{01AF}", + "Uhornacute" => "\u{1EE8}", + "Uhorndotbelow" => "\u{1EF0}", + "Uhorngrave" => "\u{1EEA}", + "Uhornhookabove" => "\u{1EEC}", + "Uhorntilde" => "\u{1EEE}", + "Uhungarumlaut" => "\u{0170}", + "Uhungarumlautcyrillic" => "\u{04F2}", + "Uinvertedbreve" => "\u{0216}", + "Ukcyrillic" => "\u{0478}", + "Umacron" => "\u{016A}", + "Umacroncyrillic" => "\u{04EE}", + "Umacrondieresis" => "\u{1E7A}", + "Umonospace" => "\u{FF35}", + "Uogonek" => "\u{0172}", + "Upsilon" => "\u{03A5}", + "Upsilon1" => "\u{03D2}", + "Upsilonacutehooksymbolgreek" => "\u{03D3}", + "Upsilonafrican" => "\u{01B1}", + "Upsilondieresis" => "\u{03AB}", + "Upsilondieresishooksymbolgreek" => "\u{03D4}", + "Upsilonhooksymbol" => "\u{03D2}", + "Upsilontonos" => "\u{038E}", + "Uring" => "\u{016E}", + "Ushortcyrillic" => "\u{040E}", + "Usmall" => "\u{F775}", + "Ustraightcyrillic" => "\u{04AE}", + "Ustraightstrokecyrillic" => "\u{04B0}", + "Utilde" => "\u{0168}", + "Utildeacute" => "\u{1E78}", + "Utildebelow" => "\u{1E74}", + "V" => "\u{0056}", + "Vcircle" => "\u{24CB}", + "Vdotbelow" => "\u{1E7E}", + "Vecyrillic" => "\u{0412}", + "Vewarmenian" => "\u{054E}", + "Vhook" => "\u{01B2}", + "Vmonospace" => "\u{FF36}", + "Voarmenian" => "\u{0548}", + "Vsmall" => "\u{F776}", + "Vtilde" => "\u{1E7C}", + "W" => "\u{0057}", + "Wacute" => "\u{1E82}", + "Wcircle" => "\u{24CC}", + "Wcircumflex" => "\u{0174}", + "Wdieresis" => "\u{1E84}", + "Wdotaccent" => "\u{1E86}", + "Wdotbelow" => "\u{1E88}", + "Wgrave" => "\u{1E80}", + "Wmonospace" => "\u{FF37}", + "Wsmall" => "\u{F777}", + "X" => "\u{0058}", + "Xcircle" => "\u{24CD}", + "Xdieresis" => "\u{1E8C}", + "Xdotaccent" => "\u{1E8A}", + "Xeharmenian" => "\u{053D}", + "Xi" => "\u{039E}", + "Xmonospace" => "\u{FF38}", + "Xsmall" => "\u{F778}", + "Y" => "\u{0059}", + "Yacute" => "\u{00DD}", + "Yacutesmall" => "\u{F7FD}", + "Yatcyrillic" => "\u{0462}", + "Ycircle" => "\u{24CE}", + "Ycircumflex" => "\u{0176}", + "Ydieresis" => "\u{0178}", + "Ydieresissmall" => "\u{F7FF}", + "Ydotaccent" => "\u{1E8E}", + "Ydotbelow" => "\u{1EF4}", + "Yericyrillic" => "\u{042B}", + "Yerudieresiscyrillic" => "\u{04F8}", + "Ygrave" => "\u{1EF2}", + "Yhook" => "\u{01B3}", + "Yhookabove" => "\u{1EF6}", + "Yiarmenian" => "\u{0545}", + "Yicyrillic" => "\u{0407}", + "Yiwnarmenian" => "\u{0552}", + "Ymonospace" => "\u{FF39}", + "Ysmall" => "\u{F779}", + "Ytilde" => "\u{1EF8}", + "Yusbigcyrillic" => "\u{046A}", + "Yusbigiotifiedcyrillic" => "\u{046C}", + "Yuslittlecyrillic" => "\u{0466}", + "Yuslittleiotifiedcyrillic" => "\u{0468}", + "Z" => "\u{005A}", + "Zaarmenian" => "\u{0536}", + "Zacute" => "\u{0179}", + "Zcaron" => "\u{017D}", + "Zcaronsmall" => "\u{F6FF}", + "Zcircle" => "\u{24CF}", + "Zcircumflex" => "\u{1E90}", + "Zdot" => "\u{017B}", + "Zdotaccent" => "\u{017B}", + "Zdotbelow" => "\u{1E92}", + "Zecyrillic" => "\u{0417}", + "Zedescendercyrillic" => "\u{0498}", + "Zedieresiscyrillic" => "\u{04DE}", + "Zeta" => "\u{0396}", + "Zhearmenian" => "\u{053A}", + "Zhebrevecyrillic" => "\u{04C1}", + "Zhecyrillic" => "\u{0416}", + "Zhedescendercyrillic" => "\u{0496}", + "Zhedieresiscyrillic" => "\u{04DC}", + "Zlinebelow" => "\u{1E94}", + "Zmonospace" => "\u{FF3A}", + "Zsmall" => "\u{F77A}", + "Zstroke" => "\u{01B5}", + "a" => "\u{0061}", + "aabengali" => "\u{0986}", + "aacute" => "\u{00E1}", + "aadeva" => "\u{0906}", + "aagujarati" => "\u{0A86}", + "aagurmukhi" => "\u{0A06}", + "aamatragurmukhi" => "\u{0A3E}", + "aarusquare" => "\u{3303}", + "aavowelsignbengali" => "\u{09BE}", + "aavowelsigndeva" => "\u{093E}", + "aavowelsigngujarati" => "\u{0ABE}", + "abbreviationmarkarmenian" => "\u{055F}", + "abbreviationsigndeva" => "\u{0970}", + "abengali" => "\u{0985}", + "abopomofo" => "\u{311A}", + "abreve" => "\u{0103}", + "abreveacute" => "\u{1EAF}", + "abrevecyrillic" => "\u{04D1}", + "abrevedotbelow" => "\u{1EB7}", + "abrevegrave" => "\u{1EB1}", + "abrevehookabove" => "\u{1EB3}", + "abrevetilde" => "\u{1EB5}", + "acaron" => "\u{01CE}", + "acircle" => "\u{24D0}", + "acircumflex" => "\u{00E2}", + "acircumflexacute" => "\u{1EA5}", + "acircumflexdotbelow" => "\u{1EAD}", + "acircumflexgrave" => "\u{1EA7}", + "acircumflexhookabove" => "\u{1EA9}", + "acircumflextilde" => "\u{1EAB}", + "acute" => "\u{00B4}", + "acutebelowcmb" => "\u{0317}", + "acutecmb" => "\u{0301}", + "acutecomb" => "\u{0301}", + "acutedeva" => "\u{0954}", + "acutelowmod" => "\u{02CF}", + "acutetonecmb" => "\u{0341}", + "acyrillic" => "\u{0430}", + "adblgrave" => "\u{0201}", + "addakgurmukhi" => "\u{0A71}", + "adeva" => "\u{0905}", + "adieresis" => "\u{00E4}", + "adieresiscyrillic" => "\u{04D3}", + "adieresismacron" => "\u{01DF}", + "adotbelow" => "\u{1EA1}", + "adotmacron" => "\u{01E1}", + "ae" => "\u{00E6}", + "aeacute" => "\u{01FD}", + "aekorean" => "\u{3150}", + "aemacron" => "\u{01E3}", + "afii00208" => "\u{2015}", + "afii08941" => "\u{20A4}", + "afii10017" => "\u{0410}", + "afii10018" => "\u{0411}", + "afii10019" => "\u{0412}", + "afii10020" => "\u{0413}", + "afii10021" => "\u{0414}", + "afii10022" => "\u{0415}", + "afii10023" => "\u{0401}", + "afii10024" => "\u{0416}", + "afii10025" => "\u{0417}", + "afii10026" => "\u{0418}", + "afii10027" => "\u{0419}", + "afii10028" => "\u{041A}", + "afii10029" => "\u{041B}", + "afii10030" => "\u{041C}", + "afii10031" => "\u{041D}", + "afii10032" => "\u{041E}", + "afii10033" => "\u{041F}", + "afii10034" => "\u{0420}", + "afii10035" => "\u{0421}", + "afii10036" => "\u{0422}", + "afii10037" => "\u{0423}", + "afii10038" => "\u{0424}", + "afii10039" => "\u{0425}", + "afii10040" => "\u{0426}", + "afii10041" => "\u{0427}", + "afii10042" => "\u{0428}", + "afii10043" => "\u{0429}", + "afii10044" => "\u{042A}", + "afii10045" => "\u{042B}", + "afii10046" => "\u{042C}", + "afii10047" => "\u{042D}", + "afii10048" => "\u{042E}", + "afii10049" => "\u{042F}", + "afii10050" => "\u{0490}", + "afii10051" => "\u{0402}", + "afii10052" => "\u{0403}", + "afii10053" => "\u{0404}", + "afii10054" => "\u{0405}", + "afii10055" => "\u{0406}", + "afii10056" => "\u{0407}", + "afii10057" => "\u{0408}", + "afii10058" => "\u{0409}", + "afii10059" => "\u{040A}", + "afii10060" => "\u{040B}", + "afii10061" => "\u{040C}", + "afii10062" => "\u{040E}", + "afii10063" => "\u{F6C4}", + "afii10064" => "\u{F6C5}", + "afii10065" => "\u{0430}", + "afii10066" => "\u{0431}", + "afii10067" => "\u{0432}", + "afii10068" => "\u{0433}", + "afii10069" => "\u{0434}", + "afii10070" => "\u{0435}", + "afii10071" => "\u{0451}", + "afii10072" => "\u{0436}", + "afii10073" => "\u{0437}", + "afii10074" => "\u{0438}", + "afii10075" => "\u{0439}", + "afii10076" => "\u{043A}", + "afii10077" => "\u{043B}", + "afii10078" => "\u{043C}", + "afii10079" => "\u{043D}", + "afii10080" => "\u{043E}", + "afii10081" => "\u{043F}", + "afii10082" => "\u{0440}", + "afii10083" => "\u{0441}", + "afii10084" => "\u{0442}", + "afii10085" => "\u{0443}", + "afii10086" => "\u{0444}", + "afii10087" => "\u{0445}", + "afii10088" => "\u{0446}", + "afii10089" => "\u{0447}", + "afii10090" => "\u{0448}", + "afii10091" => "\u{0449}", + "afii10092" => "\u{044A}", + "afii10093" => "\u{044B}", + "afii10094" => "\u{044C}", + "afii10095" => "\u{044D}", + "afii10096" => "\u{044E}", + "afii10097" => "\u{044F}", + "afii10098" => "\u{0491}", + "afii10099" => "\u{0452}", + "afii10100" => "\u{0453}", + "afii10101" => "\u{0454}", + "afii10102" => "\u{0455}", + "afii10103" => "\u{0456}", + "afii10104" => "\u{0457}", + "afii10105" => "\u{0458}", + "afii10106" => "\u{0459}", + "afii10107" => "\u{045A}", + "afii10108" => "\u{045B}", + "afii10109" => "\u{045C}", + "afii10110" => "\u{045E}", + "afii10145" => "\u{040F}", + "afii10146" => "\u{0462}", + "afii10147" => "\u{0472}", + "afii10148" => "\u{0474}", + "afii10192" => "\u{F6C6}", + "afii10193" => "\u{045F}", + "afii10194" => "\u{0463}", + "afii10195" => "\u{0473}", + "afii10196" => "\u{0475}", + "afii10831" => "\u{F6C7}", + "afii10832" => "\u{F6C8}", + "afii10846" => "\u{04D9}", + "afii299" => "\u{200E}", + "afii300" => "\u{200F}", + "afii301" => "\u{200D}", + "afii57381" => "\u{066A}", + "afii57388" => "\u{060C}", + "afii57392" => "\u{0660}", + "afii57393" => "\u{0661}", + "afii57394" => "\u{0662}", + "afii57395" => "\u{0663}", + "afii57396" => "\u{0664}", + "afii57397" => "\u{0665}", + "afii57398" => "\u{0666}", + "afii57399" => "\u{0667}", + "afii57400" => "\u{0668}", + "afii57401" => "\u{0669}", + "afii57403" => "\u{061B}", + "afii57407" => "\u{061F}", + "afii57409" => "\u{0621}", + "afii57410" => "\u{0622}", + "afii57411" => "\u{0623}", + "afii57412" => "\u{0624}", + "afii57413" => "\u{0625}", + "afii57414" => "\u{0626}", + "afii57415" => "\u{0627}", + "afii57416" => "\u{0628}", + "afii57417" => "\u{0629}", + "afii57418" => "\u{062A}", + "afii57419" => "\u{062B}", + "afii57420" => "\u{062C}", + "afii57421" => "\u{062D}", + "afii57422" => "\u{062E}", + "afii57423" => "\u{062F}", + "afii57424" => "\u{0630}", + "afii57425" => "\u{0631}", + "afii57426" => "\u{0632}", + "afii57427" => "\u{0633}", + "afii57428" => "\u{0634}", + "afii57429" => "\u{0635}", + "afii57430" => "\u{0636}", + "afii57431" => "\u{0637}", + "afii57432" => "\u{0638}", + "afii57433" => "\u{0639}", + "afii57434" => "\u{063A}", + "afii57440" => "\u{0640}", + "afii57441" => "\u{0641}", + "afii57442" => "\u{0642}", + "afii57443" => "\u{0643}", + "afii57444" => "\u{0644}", + "afii57445" => "\u{0645}", + "afii57446" => "\u{0646}", + "afii57448" => "\u{0648}", + "afii57449" => "\u{0649}", + "afii57450" => "\u{064A}", + "afii57451" => "\u{064B}", + "afii57452" => "\u{064C}", + "afii57453" => "\u{064D}", + "afii57454" => "\u{064E}", + "afii57455" => "\u{064F}", + "afii57456" => "\u{0650}", + "afii57457" => "\u{0651}", + "afii57458" => "\u{0652}", + "afii57470" => "\u{0647}", + "afii57505" => "\u{06A4}", + "afii57506" => "\u{067E}", + "afii57507" => "\u{0686}", + "afii57508" => "\u{0698}", + "afii57509" => "\u{06AF}", + "afii57511" => "\u{0679}", + "afii57512" => "\u{0688}", + "afii57513" => "\u{0691}", + "afii57514" => "\u{06BA}", + "afii57519" => "\u{06D2}", + "afii57534" => "\u{06D5}", + "afii57636" => "\u{20AA}", + "afii57645" => "\u{05BE}", + "afii57658" => "\u{05C3}", + "afii57664" => "\u{05D0}", + "afii57665" => "\u{05D1}", + "afii57666" => "\u{05D2}", + "afii57667" => "\u{05D3}", + "afii57668" => "\u{05D4}", + "afii57669" => "\u{05D5}", + "afii57670" => "\u{05D6}", + "afii57671" => "\u{05D7}", + "afii57672" => "\u{05D8}", + "afii57673" => "\u{05D9}", + "afii57674" => "\u{05DA}", + "afii57675" => "\u{05DB}", + "afii57676" => "\u{05DC}", + "afii57677" => "\u{05DD}", + "afii57678" => "\u{05DE}", + "afii57679" => "\u{05DF}", + "afii57680" => "\u{05E0}", + "afii57681" => "\u{05E1}", + "afii57682" => "\u{05E2}", + "afii57683" => "\u{05E3}", + "afii57684" => "\u{05E4}", + "afii57685" => "\u{05E5}", + "afii57686" => "\u{05E6}", + "afii57687" => "\u{05E7}", + "afii57688" => "\u{05E8}", + "afii57689" => "\u{05E9}", + "afii57690" => "\u{05EA}", + "afii57694" => "\u{FB2A}", + "afii57695" => "\u{FB2B}", + "afii57700" => "\u{FB4B}", + "afii57705" => "\u{FB1F}", + "afii57716" => "\u{05F0}", + "afii57717" => "\u{05F1}", + "afii57718" => "\u{05F2}", + "afii57723" => "\u{FB35}", + "afii57793" => "\u{05B4}", + "afii57794" => "\u{05B5}", + "afii57795" => "\u{05B6}", + "afii57796" => "\u{05BB}", + "afii57797" => "\u{05B8}", + "afii57798" => "\u{05B7}", + "afii57799" => "\u{05B0}", + "afii57800" => "\u{05B2}", + "afii57801" => "\u{05B1}", + "afii57802" => "\u{05B3}", + "afii57803" => "\u{05C2}", + "afii57804" => "\u{05C1}", + "afii57806" => "\u{05B9}", + "afii57807" => "\u{05BC}", + "afii57839" => "\u{05BD}", + "afii57841" => "\u{05BF}", + "afii57842" => "\u{05C0}", + "afii57929" => "\u{02BC}", + "afii61248" => "\u{2105}", + "afii61289" => "\u{2113}", + "afii61352" => "\u{2116}", + "afii61573" => "\u{202C}", + "afii61574" => "\u{202D}", + "afii61575" => "\u{202E}", + "afii61664" => "\u{200C}", + "afii63167" => "\u{066D}", + "afii64937" => "\u{02BD}", + "agrave" => "\u{00E0}", + "agujarati" => "\u{0A85}", + "agurmukhi" => "\u{0A05}", + "ahiragana" => "\u{3042}", + "ahookabove" => "\u{1EA3}", + "aibengali" => "\u{0990}", + "aibopomofo" => "\u{311E}", + "aideva" => "\u{0910}", + "aiecyrillic" => "\u{04D5}", + "aigujarati" => "\u{0A90}", + "aigurmukhi" => "\u{0A10}", + "aimatragurmukhi" => "\u{0A48}", + "ainarabic" => "\u{0639}", + "ainfinalarabic" => "\u{FECA}", + "aininitialarabic" => "\u{FECB}", + "ainmedialarabic" => "\u{FECC}", + "ainvertedbreve" => "\u{0203}", + "aivowelsignbengali" => "\u{09C8}", + "aivowelsigndeva" => "\u{0948}", + "aivowelsigngujarati" => "\u{0AC8}", + "akatakana" => "\u{30A2}", + "akatakanahalfwidth" => "\u{FF71}", + "akorean" => "\u{314F}", + "alef" => "\u{05D0}", + "alefarabic" => "\u{0627}", + "alefdageshhebrew" => "\u{FB30}", + "aleffinalarabic" => "\u{FE8E}", + "alefhamzaabovearabic" => "\u{0623}", + "alefhamzaabovefinalarabic" => "\u{FE84}", + "alefhamzabelowarabic" => "\u{0625}", + "alefhamzabelowfinalarabic" => "\u{FE88}", + "alefhebrew" => "\u{05D0}", + "aleflamedhebrew" => "\u{FB4F}", + "alefmaddaabovearabic" => "\u{0622}", + "alefmaddaabovefinalarabic" => "\u{FE82}", + "alefmaksuraarabic" => "\u{0649}", + "alefmaksurafinalarabic" => "\u{FEF0}", + "alefmaksurainitialarabic" => "\u{FEF3}", + "alefmaksuramedialarabic" => "\u{FEF4}", + "alefpatahhebrew" => "\u{FB2E}", + "alefqamatshebrew" => "\u{FB2F}", + "aleph" => "\u{2135}", + "allequal" => "\u{224C}", + "alpha" => "\u{03B1}", + "alphatonos" => "\u{03AC}", + "amacron" => "\u{0101}", + "amonospace" => "\u{FF41}", + "ampersand" => "\u{0026}", + "ampersandmonospace" => "\u{FF06}", + "ampersandsmall" => "\u{F726}", + "amsquare" => "\u{33C2}", + "anbopomofo" => "\u{3122}", + "angbopomofo" => "\u{3124}", + "angkhankhuthai" => "\u{0E5A}", + "angle" => "\u{2220}", + "anglebracketleft" => "\u{3008}", + "anglebracketleftvertical" => "\u{FE3F}", + "anglebracketright" => "\u{3009}", + "anglebracketrightvertical" => "\u{FE40}", + "angleleft" => "\u{2329}", + "angleright" => "\u{232A}", + "angstrom" => "\u{212B}", + "anoteleia" => "\u{0387}", + "anudattadeva" => "\u{0952}", + "anusvarabengali" => "\u{0982}", + "anusvaradeva" => "\u{0902}", + "anusvaragujarati" => "\u{0A82}", + "aogonek" => "\u{0105}", + "apaatosquare" => "\u{3300}", + "aparen" => "\u{249C}", + "apostrophearmenian" => "\u{055A}", + "apostrophemod" => "\u{02BC}", + "apple" => "\u{F8FF}", + "approaches" => "\u{2250}", + "approxequal" => "\u{2248}", + "approxequalorimage" => "\u{2252}", + "approximatelyequal" => "\u{2245}", + "araeaekorean" => "\u{318E}", + "araeakorean" => "\u{318D}", + "arc" => "\u{2312}", + "arighthalfring" => "\u{1E9A}", + "aring" => "\u{00E5}", + "aringacute" => "\u{01FB}", + "aringbelow" => "\u{1E01}", + "arrowboth" => "\u{2194}", + "arrowdashdown" => "\u{21E3}", + "arrowdashleft" => "\u{21E0}", + "arrowdashright" => "\u{21E2}", + "arrowdashup" => "\u{21E1}", + "arrowdblboth" => "\u{21D4}", + "arrowdbldown" => "\u{21D3}", + "arrowdblleft" => "\u{21D0}", + "arrowdblright" => "\u{21D2}", + "arrowdblup" => "\u{21D1}", + "arrowdown" => "\u{2193}", + "arrowdownleft" => "\u{2199}", + "arrowdownright" => "\u{2198}", + "arrowdownwhite" => "\u{21E9}", + "arrowheaddownmod" => "\u{02C5}", + "arrowheadleftmod" => "\u{02C2}", + "arrowheadrightmod" => "\u{02C3}", + "arrowheadupmod" => "\u{02C4}", + "arrowhorizex" => "\u{F8E7}", + "arrowleft" => "\u{2190}", + "arrowleftdbl" => "\u{21D0}", + "arrowleftdblstroke" => "\u{21CD}", + "arrowleftoverright" => "\u{21C6}", + "arrowleftwhite" => "\u{21E6}", + "arrowright" => "\u{2192}", + "arrowrightdblstroke" => "\u{21CF}", + "arrowrightheavy" => "\u{279E}", + "arrowrightoverleft" => "\u{21C4}", + "arrowrightwhite" => "\u{21E8}", + "arrowtableft" => "\u{21E4}", + "arrowtabright" => "\u{21E5}", + "arrowup" => "\u{2191}", + "arrowupdn" => "\u{2195}", + "arrowupdnbse" => "\u{21A8}", + "arrowupdownbase" => "\u{21A8}", + "arrowupleft" => "\u{2196}", + "arrowupleftofdown" => "\u{21C5}", + "arrowupright" => "\u{2197}", + "arrowupwhite" => "\u{21E7}", + "arrowvertex" => "\u{F8E6}", + "asciicircum" => "\u{005E}", + "asciicircummonospace" => "\u{FF3E}", + "asciitilde" => "\u{007E}", + "asciitildemonospace" => "\u{FF5E}", + "ascript" => "\u{0251}", + "ascriptturned" => "\u{0252}", + "asmallhiragana" => "\u{3041}", + "asmallkatakana" => "\u{30A1}", + "asmallkatakanahalfwidth" => "\u{FF67}", + "asterisk" => "\u{002A}", + "asteriskaltonearabic" => "\u{066D}", + "asteriskarabic" => "\u{066D}", + "asteriskmath" => "\u{2217}", + "asteriskmonospace" => "\u{FF0A}", + "asterisksmall" => "\u{FE61}", + "asterism" => "\u{2042}", + "asuperior" => "\u{F6E9}", + "asymptoticallyequal" => "\u{2243}", + "at" => "\u{0040}", + "atilde" => "\u{00E3}", + "atmonospace" => "\u{FF20}", + "atsmall" => "\u{FE6B}", + "aturned" => "\u{0250}", + "aubengali" => "\u{0994}", + "aubopomofo" => "\u{3120}", + "audeva" => "\u{0914}", + "augujarati" => "\u{0A94}", + "augurmukhi" => "\u{0A14}", + "aulengthmarkbengali" => "\u{09D7}", + "aumatragurmukhi" => "\u{0A4C}", + "auvowelsignbengali" => "\u{09CC}", + "auvowelsigndeva" => "\u{094C}", + "auvowelsigngujarati" => "\u{0ACC}", + "avagrahadeva" => "\u{093D}", + "aybarmenian" => "\u{0561}", + "ayin" => "\u{05E2}", + "ayinaltonehebrew" => "\u{FB20}", + "ayinhebrew" => "\u{05E2}", + "b" => "\u{0062}", + "babengali" => "\u{09AC}", + "backslash" => "\u{005C}", + "backslashmonospace" => "\u{FF3C}", + "badeva" => "\u{092C}", + "bagujarati" => "\u{0AAC}", + "bagurmukhi" => "\u{0A2C}", + "bahiragana" => "\u{3070}", + "bahtthai" => "\u{0E3F}", + "bakatakana" => "\u{30D0}", + "bar" => "\u{007C}", + "barmonospace" => "\u{FF5C}", + "bbopomofo" => "\u{3105}", + "bcircle" => "\u{24D1}", + "bdotaccent" => "\u{1E03}", + "bdotbelow" => "\u{1E05}", + "beamedsixteenthnotes" => "\u{266C}", + "because" => "\u{2235}", + "becyrillic" => "\u{0431}", + "beharabic" => "\u{0628}", + "behfinalarabic" => "\u{FE90}", + "behinitialarabic" => "\u{FE91}", + "behiragana" => "\u{3079}", + "behmedialarabic" => "\u{FE92}", + "behmeeminitialarabic" => "\u{FC9F}", + "behmeemisolatedarabic" => "\u{FC08}", + "behnoonfinalarabic" => "\u{FC6D}", + "bekatakana" => "\u{30D9}", + "benarmenian" => "\u{0562}", + "bet" => "\u{05D1}", + "beta" => "\u{03B2}", + "betasymbolgreek" => "\u{03D0}", + "betdagesh" => "\u{FB31}", + "betdageshhebrew" => "\u{FB31}", + "bethebrew" => "\u{05D1}", + "betrafehebrew" => "\u{FB4C}", + "bhabengali" => "\u{09AD}", + "bhadeva" => "\u{092D}", + "bhagujarati" => "\u{0AAD}", + "bhagurmukhi" => "\u{0A2D}", + "bhook" => "\u{0253}", + "bihiragana" => "\u{3073}", + "bikatakana" => "\u{30D3}", + "bilabialclick" => "\u{0298}", + "bindigurmukhi" => "\u{0A02}", + "birusquare" => "\u{3331}", + "blackcircle" => "\u{25CF}", + "blackdiamond" => "\u{25C6}", + "blackdownpointingtriangle" => "\u{25BC}", + "blackleftpointingpointer" => "\u{25C4}", + "blackleftpointingtriangle" => "\u{25C0}", + "blacklenticularbracketleft" => "\u{3010}", + "blacklenticularbracketleftvertical" => "\u{FE3B}", + "blacklenticularbracketright" => "\u{3011}", + "blacklenticularbracketrightvertical" => "\u{FE3C}", + "blacklowerlefttriangle" => "\u{25E3}", + "blacklowerrighttriangle" => "\u{25E2}", + "blackrectangle" => "\u{25AC}", + "blackrightpointingpointer" => "\u{25BA}", + "blackrightpointingtriangle" => "\u{25B6}", + "blacksmallsquare" => "\u{25AA}", + "blacksmilingface" => "\u{263B}", + "blacksquare" => "\u{25A0}", + "blackstar" => "\u{2605}", + "blackupperlefttriangle" => "\u{25E4}", + "blackupperrighttriangle" => "\u{25E5}", + "blackuppointingsmalltriangle" => "\u{25B4}", + "blackuppointingtriangle" => "\u{25B2}", + "blank" => "\u{2423}", + "blinebelow" => "\u{1E07}", + "block" => "\u{2588}", + "bmonospace" => "\u{FF42}", + "bobaimaithai" => "\u{0E1A}", + "bohiragana" => "\u{307C}", + "bokatakana" => "\u{30DC}", + "bparen" => "\u{249D}", + "bqsquare" => "\u{33C3}", + "braceex" => "\u{F8F4}", + "braceleft" => "\u{007B}", + "braceleftbt" => "\u{F8F3}", + "braceleftmid" => "\u{F8F2}", + "braceleftmonospace" => "\u{FF5B}", + "braceleftsmall" => "\u{FE5B}", + "bracelefttp" => "\u{F8F1}", + "braceleftvertical" => "\u{FE37}", + "braceright" => "\u{007D}", + "bracerightbt" => "\u{F8FE}", + "bracerightmid" => "\u{F8FD}", + "bracerightmonospace" => "\u{FF5D}", + "bracerightsmall" => "\u{FE5C}", + "bracerighttp" => "\u{F8FC}", + "bracerightvertical" => "\u{FE38}", + "bracketleft" => "\u{005B}", + "bracketleftbt" => "\u{F8F0}", + "bracketleftex" => "\u{F8EF}", + "bracketleftmonospace" => "\u{FF3B}", + "bracketlefttp" => "\u{F8EE}", + "bracketright" => "\u{005D}", + "bracketrightbt" => "\u{F8FB}", + "bracketrightex" => "\u{F8FA}", + "bracketrightmonospace" => "\u{FF3D}", + "bracketrighttp" => "\u{F8F9}", + "breve" => "\u{02D8}", + "brevebelowcmb" => "\u{032E}", + "brevecmb" => "\u{0306}", + "breveinvertedbelowcmb" => "\u{032F}", + "breveinvertedcmb" => "\u{0311}", + "breveinverteddoublecmb" => "\u{0361}", + "bridgebelowcmb" => "\u{032A}", + "bridgeinvertedbelowcmb" => "\u{033A}", + "brokenbar" => "\u{00A6}", + "bstroke" => "\u{0180}", + "bsuperior" => "\u{F6EA}", + "btopbar" => "\u{0183}", + "buhiragana" => "\u{3076}", + "bukatakana" => "\u{30D6}", + "bullet" => "\u{2022}", + "bulletinverse" => "\u{25D8}", + "bulletoperator" => "\u{2219}", + "bullseye" => "\u{25CE}", + "c" => "\u{0063}", + "caarmenian" => "\u{056E}", + "cabengali" => "\u{099A}", + "cacute" => "\u{0107}", + "cadeva" => "\u{091A}", + "cagujarati" => "\u{0A9A}", + "cagurmukhi" => "\u{0A1A}", + "calsquare" => "\u{3388}", + "candrabindubengali" => "\u{0981}", + "candrabinducmb" => "\u{0310}", + "candrabindudeva" => "\u{0901}", + "candrabindugujarati" => "\u{0A81}", + "capslock" => "\u{21EA}", + "careof" => "\u{2105}", + "caron" => "\u{02C7}", + "caronbelowcmb" => "\u{032C}", + "caroncmb" => "\u{030C}", + "carriagereturn" => "\u{21B5}", + "cbopomofo" => "\u{3118}", + "ccaron" => "\u{010D}", + "ccedilla" => "\u{00E7}", + "ccedillaacute" => "\u{1E09}", + "ccircle" => "\u{24D2}", + "ccircumflex" => "\u{0109}", + "ccurl" => "\u{0255}", + "cdot" => "\u{010B}", + "cdotaccent" => "\u{010B}", + "cdsquare" => "\u{33C5}", + "cedilla" => "\u{00B8}", + "cedillacmb" => "\u{0327}", + "cent" => "\u{00A2}", + "centigrade" => "\u{2103}", + "centinferior" => "\u{F6DF}", + "centmonospace" => "\u{FFE0}", + "centoldstyle" => "\u{F7A2}", + "centsuperior" => "\u{F6E0}", + "chaarmenian" => "\u{0579}", + "chabengali" => "\u{099B}", + "chadeva" => "\u{091B}", + "chagujarati" => "\u{0A9B}", + "chagurmukhi" => "\u{0A1B}", + "chbopomofo" => "\u{3114}", + "cheabkhasiancyrillic" => "\u{04BD}", + "checkmark" => "\u{2713}", + "checyrillic" => "\u{0447}", + "chedescenderabkhasiancyrillic" => "\u{04BF}", + "chedescendercyrillic" => "\u{04B7}", + "chedieresiscyrillic" => "\u{04F5}", + "cheharmenian" => "\u{0573}", + "chekhakassiancyrillic" => "\u{04CC}", + "cheverticalstrokecyrillic" => "\u{04B9}", + "chi" => "\u{03C7}", + "chieuchacirclekorean" => "\u{3277}", + "chieuchaparenkorean" => "\u{3217}", + "chieuchcirclekorean" => "\u{3269}", + "chieuchkorean" => "\u{314A}", + "chieuchparenkorean" => "\u{3209}", + "chochangthai" => "\u{0E0A}", + "chochanthai" => "\u{0E08}", + "chochingthai" => "\u{0E09}", + "chochoethai" => "\u{0E0C}", + "chook" => "\u{0188}", + "cieucacirclekorean" => "\u{3276}", + "cieucaparenkorean" => "\u{3216}", + "cieuccirclekorean" => "\u{3268}", + "cieuckorean" => "\u{3148}", + "cieucparenkorean" => "\u{3208}", + "cieucuparenkorean" => "\u{321C}", + "circle" => "\u{25CB}", + "circlemultiply" => "\u{2297}", + "circleot" => "\u{2299}", + "circleplus" => "\u{2295}", + "circlepostalmark" => "\u{3036}", + "circlewithlefthalfblack" => "\u{25D0}", + "circlewithrighthalfblack" => "\u{25D1}", + "circumflex" => "\u{02C6}", + "circumflexbelowcmb" => "\u{032D}", + "circumflexcmb" => "\u{0302}", + "clear" => "\u{2327}", + "clickalveolar" => "\u{01C2}", + "clickdental" => "\u{01C0}", + "clicklateral" => "\u{01C1}", + "clickretroflex" => "\u{01C3}", + "club" => "\u{2663}", + "clubsuitblack" => "\u{2663}", + "clubsuitwhite" => "\u{2667}", + "cmcubedsquare" => "\u{33A4}", + "cmonospace" => "\u{FF43}", + "cmsquaredsquare" => "\u{33A0}", + "coarmenian" => "\u{0581}", + "colon" => "\u{003A}", + "colonmonetary" => "\u{20A1}", + "colonmonospace" => "\u{FF1A}", + "colonsign" => "\u{20A1}", + "colonsmall" => "\u{FE55}", + "colontriangularhalfmod" => "\u{02D1}", + "colontriangularmod" => "\u{02D0}", + "comma" => "\u{002C}", + "commaabovecmb" => "\u{0313}", + "commaaboverightcmb" => "\u{0315}", + "commaaccent" => "\u{F6C3}", + "commaarabic" => "\u{060C}", + "commaarmenian" => "\u{055D}", + "commainferior" => "\u{F6E1}", + "commamonospace" => "\u{FF0C}", + "commareversedabovecmb" => "\u{0314}", + "commareversedmod" => "\u{02BD}", + "commasmall" => "\u{FE50}", + "commasuperior" => "\u{F6E2}", + "commaturnedabovecmb" => "\u{0312}", + "commaturnedmod" => "\u{02BB}", + "compass" => "\u{263C}", + "congruent" => "\u{2245}", + "contourintegral" => "\u{222E}", + "control" => "\u{2303}", + "controlACK" => "\u{0006}", + "controlBEL" => "\u{0007}", + "controlBS" => "\u{0008}", + "controlCAN" => "\u{0018}", + "controlCR" => "\u{000D}", + "controlDC1" => "\u{0011}", + "controlDC2" => "\u{0012}", + "controlDC3" => "\u{0013}", + "controlDC4" => "\u{0014}", + "controlDEL" => "\u{007F}", + "controlDLE" => "\u{0010}", + "controlEM" => "\u{0019}", + "controlENQ" => "\u{0005}", + "controlEOT" => "\u{0004}", + "controlESC" => "\u{001B}", + "controlETB" => "\u{0017}", + "controlETX" => "\u{0003}", + "controlFF" => "\u{000C}", + "controlFS" => "\u{001C}", + "controlGS" => "\u{001D}", + "controlHT" => "\u{0009}", + "controlLF" => "\u{000A}", + "controlNAK" => "\u{0015}", + "controlRS" => "\u{001E}", + "controlSI" => "\u{000F}", + "controlSO" => "\u{000E}", + "controlSOT" => "\u{0002}", + "controlSTX" => "\u{0001}", + "controlSUB" => "\u{001A}", + "controlSYN" => "\u{0016}", + "controlUS" => "\u{001F}", + "controlVT" => "\u{000B}", + "copyright" => "\u{00A9}", + "copyrightsans" => "\u{F8E9}", + "copyrightserif" => "\u{F6D9}", + "cornerbracketleft" => "\u{300C}", + "cornerbracketlefthalfwidth" => "\u{FF62}", + "cornerbracketleftvertical" => "\u{FE41}", + "cornerbracketright" => "\u{300D}", + "cornerbracketrighthalfwidth" => "\u{FF63}", + "cornerbracketrightvertical" => "\u{FE42}", + "corporationsquare" => "\u{337F}", + "cosquare" => "\u{33C7}", + "coverkgsquare" => "\u{33C6}", + "cparen" => "\u{249E}", + "cruzeiro" => "\u{20A2}", + "cstretched" => "\u{0297}", + "curlyand" => "\u{22CF}", + "curlyor" => "\u{22CE}", + "currency" => "\u{00A4}", + "cyrBreve" => "\u{F6D1}", + "cyrFlex" => "\u{F6D2}", + "cyrbreve" => "\u{F6D4}", + "cyrflex" => "\u{F6D5}", + "d" => "\u{0064}", + "daarmenian" => "\u{0564}", + "dabengali" => "\u{09A6}", + "dadarabic" => "\u{0636}", + "dadeva" => "\u{0926}", + "dadfinalarabic" => "\u{FEBE}", + "dadinitialarabic" => "\u{FEBF}", + "dadmedialarabic" => "\u{FEC0}", + "dagesh" => "\u{05BC}", + "dageshhebrew" => "\u{05BC}", + "dagger" => "\u{2020}", + "daggerdbl" => "\u{2021}", + "dagujarati" => "\u{0AA6}", + "dagurmukhi" => "\u{0A26}", + "dahiragana" => "\u{3060}", + "dakatakana" => "\u{30C0}", + "dalarabic" => "\u{062F}", + "dalet" => "\u{05D3}", + "daletdagesh" => "\u{FB33}", + "daletdageshhebrew" => "\u{FB33}", + "dalethatafpatah" => "\u{05D3}\u{05B2}", + "dalethatafpatahhebrew" => "\u{05D3}\u{05B2}", + "dalethatafsegol" => "\u{05D3}\u{05B1}", + "dalethatafsegolhebrew" => "\u{05D3}\u{05B1}", + "dalethebrew" => "\u{05D3}", + "dalethiriq" => "\u{05D3}\u{05B4}", + "dalethiriqhebrew" => "\u{05D3}\u{05B4}", + "daletholam" => "\u{05D3}\u{05B9}", + "daletholamhebrew" => "\u{05D3}\u{05B9}", + "daletpatah" => "\u{05D3}\u{05B7}", + "daletpatahhebrew" => "\u{05D3}\u{05B7}", + "daletqamats" => "\u{05D3}\u{05B8}", + "daletqamatshebrew" => "\u{05D3}\u{05B8}", + "daletqubuts" => "\u{05D3}\u{05BB}", + "daletqubutshebrew" => "\u{05D3}\u{05BB}", + "daletsegol" => "\u{05D3}\u{05B6}", + "daletsegolhebrew" => "\u{05D3}\u{05B6}", + "daletsheva" => "\u{05D3}\u{05B0}", + "daletshevahebrew" => "\u{05D3}\u{05B0}", + "dalettsere" => "\u{05D3}\u{05B5}", + "dalettserehebrew" => "\u{05D3}\u{05B5}", + "dalfinalarabic" => "\u{FEAA}", + "dammaarabic" => "\u{064F}", + "dammalowarabic" => "\u{064F}", + "dammatanaltonearabic" => "\u{064C}", + "dammatanarabic" => "\u{064C}", + "danda" => "\u{0964}", + "dargahebrew" => "\u{05A7}", + "dargalefthebrew" => "\u{05A7}", + "dasiapneumatacyrilliccmb" => "\u{0485}", + "dblGrave" => "\u{F6D3}", + "dblanglebracketleft" => "\u{300A}", + "dblanglebracketleftvertical" => "\u{FE3D}", + "dblanglebracketright" => "\u{300B}", + "dblanglebracketrightvertical" => "\u{FE3E}", + "dblarchinvertedbelowcmb" => "\u{032B}", + "dblarrowleft" => "\u{21D4}", + "dblarrowright" => "\u{21D2}", + "dbldanda" => "\u{0965}", + "dblgrave" => "\u{F6D6}", + "dblgravecmb" => "\u{030F}", + "dblintegral" => "\u{222C}", + "dbllowline" => "\u{2017}", + "dbllowlinecmb" => "\u{0333}", + "dbloverlinecmb" => "\u{033F}", + "dblprimemod" => "\u{02BA}", + "dblverticalbar" => "\u{2016}", + "dblverticallineabovecmb" => "\u{030E}", + "dbopomofo" => "\u{3109}", + "dbsquare" => "\u{33C8}", + "dcaron" => "\u{010F}", + "dcedilla" => "\u{1E11}", + "dcircle" => "\u{24D3}", + "dcircumflexbelow" => "\u{1E13}", + "dcroat" => "\u{0111}", + "ddabengali" => "\u{09A1}", + "ddadeva" => "\u{0921}", + "ddagujarati" => "\u{0AA1}", + "ddagurmukhi" => "\u{0A21}", + "ddalarabic" => "\u{0688}", + "ddalfinalarabic" => "\u{FB89}", + "dddhadeva" => "\u{095C}", + "ddhabengali" => "\u{09A2}", + "ddhadeva" => "\u{0922}", + "ddhagujarati" => "\u{0AA2}", + "ddhagurmukhi" => "\u{0A22}", + "ddotaccent" => "\u{1E0B}", + "ddotbelow" => "\u{1E0D}", + "decimalseparatorarabic" => "\u{066B}", + "decimalseparatorpersian" => "\u{066B}", + "decyrillic" => "\u{0434}", + "degree" => "\u{00B0}", + "dehihebrew" => "\u{05AD}", + "dehiragana" => "\u{3067}", + "deicoptic" => "\u{03EF}", + "dekatakana" => "\u{30C7}", + "deleteleft" => "\u{232B}", + "deleteright" => "\u{2326}", + "delta" => "\u{03B4}", + "deltaturned" => "\u{018D}", + "denominatorminusonenumeratorbengali" => "\u{09F8}", + "dezh" => "\u{02A4}", + "dhabengali" => "\u{09A7}", + "dhadeva" => "\u{0927}", + "dhagujarati" => "\u{0AA7}", + "dhagurmukhi" => "\u{0A27}", + "dhook" => "\u{0257}", + "dialytikatonos" => "\u{0385}", + "dialytikatonoscmb" => "\u{0344}", + "diamond" => "\u{2666}", + "diamondsuitwhite" => "\u{2662}", + "dieresis" => "\u{00A8}", + "dieresisacute" => "\u{F6D7}", + "dieresisbelowcmb" => "\u{0324}", + "dieresiscmb" => "\u{0308}", + "dieresisgrave" => "\u{F6D8}", + "dieresistonos" => "\u{0385}", + "dihiragana" => "\u{3062}", + "dikatakana" => "\u{30C2}", + "dittomark" => "\u{3003}", + "divide" => "\u{00F7}", + "divides" => "\u{2223}", + "divisionslash" => "\u{2215}", + "djecyrillic" => "\u{0452}", + "dkshade" => "\u{2593}", + "dlinebelow" => "\u{1E0F}", + "dlsquare" => "\u{3397}", + "dmacron" => "\u{0111}", + "dmonospace" => "\u{FF44}", + "dnblock" => "\u{2584}", + "dochadathai" => "\u{0E0E}", + "dodekthai" => "\u{0E14}", + "dohiragana" => "\u{3069}", + "dokatakana" => "\u{30C9}", + "dollar" => "\u{0024}", + "dollarinferior" => "\u{F6E3}", + "dollarmonospace" => "\u{FF04}", + "dollaroldstyle" => "\u{F724}", + "dollarsmall" => "\u{FE69}", + "dollarsuperior" => "\u{F6E4}", + "dong" => "\u{20AB}", + "dorusquare" => "\u{3326}", + "dotaccent" => "\u{02D9}", + "dotaccentcmb" => "\u{0307}", + "dotbelowcmb" => "\u{0323}", + "dotbelowcomb" => "\u{0323}", + "dotkatakana" => "\u{30FB}", + "dotlessi" => "\u{0131}", + "dotlessj" => "\u{F6BE}", + "dotlessjstrokehook" => "\u{0284}", + "dotmath" => "\u{22C5}", + "dottedcircle" => "\u{25CC}", + "doubleyodpatah" => "\u{FB1F}", + "doubleyodpatahhebrew" => "\u{FB1F}", + "downtackbelowcmb" => "\u{031E}", + "downtackmod" => "\u{02D5}", + "dparen" => "\u{249F}", + "dsuperior" => "\u{F6EB}", + "dtail" => "\u{0256}", + "dtopbar" => "\u{018C}", + "duhiragana" => "\u{3065}", + "dukatakana" => "\u{30C5}", + "dz" => "\u{01F3}", + "dzaltone" => "\u{02A3}", + "dzcaron" => "\u{01C6}", + "dzcurl" => "\u{02A5}", + "dzeabkhasiancyrillic" => "\u{04E1}", + "dzecyrillic" => "\u{0455}", + "dzhecyrillic" => "\u{045F}", + "e" => "\u{0065}", + "eacute" => "\u{00E9}", + "earth" => "\u{2641}", + "ebengali" => "\u{098F}", + "ebopomofo" => "\u{311C}", + "ebreve" => "\u{0115}", + "ecandradeva" => "\u{090D}", + "ecandragujarati" => "\u{0A8D}", + "ecandravowelsigndeva" => "\u{0945}", + "ecandravowelsigngujarati" => "\u{0AC5}", + "ecaron" => "\u{011B}", + "ecedillabreve" => "\u{1E1D}", + "echarmenian" => "\u{0565}", + "echyiwnarmenian" => "\u{0587}", + "ecircle" => "\u{24D4}", + "ecircumflex" => "\u{00EA}", + "ecircumflexacute" => "\u{1EBF}", + "ecircumflexbelow" => "\u{1E19}", + "ecircumflexdotbelow" => "\u{1EC7}", + "ecircumflexgrave" => "\u{1EC1}", + "ecircumflexhookabove" => "\u{1EC3}", + "ecircumflextilde" => "\u{1EC5}", + "ecyrillic" => "\u{0454}", + "edblgrave" => "\u{0205}", + "edeva" => "\u{090F}", + "edieresis" => "\u{00EB}", + "edot" => "\u{0117}", + "edotaccent" => "\u{0117}", + "edotbelow" => "\u{1EB9}", + "eegurmukhi" => "\u{0A0F}", + "eematragurmukhi" => "\u{0A47}", + "efcyrillic" => "\u{0444}", + "egrave" => "\u{00E8}", + "egujarati" => "\u{0A8F}", + "eharmenian" => "\u{0567}", + "ehbopomofo" => "\u{311D}", + "ehiragana" => "\u{3048}", + "ehookabove" => "\u{1EBB}", + "eibopomofo" => "\u{311F}", + "eight" => "\u{0038}", + "eightarabic" => "\u{0668}", + "eightbengali" => "\u{09EE}", + "eightcircle" => "\u{2467}", + "eightcircleinversesansserif" => "\u{2791}", + "eightdeva" => "\u{096E}", + "eighteencircle" => "\u{2471}", + "eighteenparen" => "\u{2485}", + "eighteenperiod" => "\u{2499}", + "eightgujarati" => "\u{0AEE}", + "eightgurmukhi" => "\u{0A6E}", + "eighthackarabic" => "\u{0668}", + "eighthangzhou" => "\u{3028}", + "eighthnotebeamed" => "\u{266B}", + "eightideographicparen" => "\u{3227}", + "eightinferior" => "\u{2088}", + "eightmonospace" => "\u{FF18}", + "eightoldstyle" => "\u{F738}", + "eightparen" => "\u{247B}", + "eightperiod" => "\u{248F}", + "eightpersian" => "\u{06F8}", + "eightroman" => "\u{2177}", + "eightsuperior" => "\u{2078}", + "eightthai" => "\u{0E58}", + "einvertedbreve" => "\u{0207}", + "eiotifiedcyrillic" => "\u{0465}", + "ekatakana" => "\u{30A8}", + "ekatakanahalfwidth" => "\u{FF74}", + "ekonkargurmukhi" => "\u{0A74}", + "ekorean" => "\u{3154}", + "elcyrillic" => "\u{043B}", + "element" => "\u{2208}", + "elevencircle" => "\u{246A}", + "elevenparen" => "\u{247E}", + "elevenperiod" => "\u{2492}", + "elevenroman" => "\u{217A}", + "ellipsis" => "\u{2026}", + "ellipsisvertical" => "\u{22EE}", + "emacron" => "\u{0113}", + "emacronacute" => "\u{1E17}", + "emacrongrave" => "\u{1E15}", + "emcyrillic" => "\u{043C}", + "emdash" => "\u{2014}", + "emdashvertical" => "\u{FE31}", + "emonospace" => "\u{FF45}", + "emphasismarkarmenian" => "\u{055B}", + "emptyset" => "\u{2205}", + "enbopomofo" => "\u{3123}", + "encyrillic" => "\u{043D}", + "endash" => "\u{2013}", + "endashvertical" => "\u{FE32}", + "endescendercyrillic" => "\u{04A3}", + "eng" => "\u{014B}", + "engbopomofo" => "\u{3125}", + "enghecyrillic" => "\u{04A5}", + "enhookcyrillic" => "\u{04C8}", + "enspace" => "\u{2002}", + "eogonek" => "\u{0119}", + "eokorean" => "\u{3153}", + "eopen" => "\u{025B}", + "eopenclosed" => "\u{029A}", + "eopenreversed" => "\u{025C}", + "eopenreversedclosed" => "\u{025E}", + "eopenreversedhook" => "\u{025D}", + "eparen" => "\u{24A0}", + "epsilon" => "\u{03B5}", + "epsilontonos" => "\u{03AD}", + "equal" => "\u{003D}", + "equalmonospace" => "\u{FF1D}", + "equalsmall" => "\u{FE66}", + "equalsuperior" => "\u{207C}", + "equivalence" => "\u{2261}", + "erbopomofo" => "\u{3126}", + "ercyrillic" => "\u{0440}", + "ereversed" => "\u{0258}", + "ereversedcyrillic" => "\u{044D}", + "escyrillic" => "\u{0441}", + "esdescendercyrillic" => "\u{04AB}", + "esh" => "\u{0283}", + "eshcurl" => "\u{0286}", + "eshortdeva" => "\u{090E}", + "eshortvowelsigndeva" => "\u{0946}", + "eshreversedloop" => "\u{01AA}", + "eshsquatreversed" => "\u{0285}", + "esmallhiragana" => "\u{3047}", + "esmallkatakana" => "\u{30A7}", + "esmallkatakanahalfwidth" => "\u{FF6A}", + "estimated" => "\u{212E}", + "esuperior" => "\u{F6EC}", + "eta" => "\u{03B7}", + "etarmenian" => "\u{0568}", + "etatonos" => "\u{03AE}", + "eth" => "\u{00F0}", + "etilde" => "\u{1EBD}", + "etildebelow" => "\u{1E1B}", + "etnahtafoukhhebrew" => "\u{0591}", + "etnahtafoukhlefthebrew" => "\u{0591}", + "etnahtahebrew" => "\u{0591}", + "etnahtalefthebrew" => "\u{0591}", + "eturned" => "\u{01DD}", + "eukorean" => "\u{3161}", + "euro" => "\u{20AC}", + "evowelsignbengali" => "\u{09C7}", + "evowelsigndeva" => "\u{0947}", + "evowelsigngujarati" => "\u{0AC7}", + "exclam" => "\u{0021}", + "exclamarmenian" => "\u{055C}", + "exclamdbl" => "\u{203C}", + "exclamdown" => "\u{00A1}", + "exclamdownsmall" => "\u{F7A1}", + "exclammonospace" => "\u{FF01}", + "exclamsmall" => "\u{F721}", + "existential" => "\u{2203}", + "ezh" => "\u{0292}", + "ezhcaron" => "\u{01EF}", + "ezhcurl" => "\u{0293}", + "ezhreversed" => "\u{01B9}", + "ezhtail" => "\u{01BA}", + "f" => "\u{0066}", + "fadeva" => "\u{095E}", + "fagurmukhi" => "\u{0A5E}", + "fahrenheit" => "\u{2109}", + "fathaarabic" => "\u{064E}", + "fathalowarabic" => "\u{064E}", + "fathatanarabic" => "\u{064B}", + "fbopomofo" => "\u{3108}", + "fcircle" => "\u{24D5}", + "fdotaccent" => "\u{1E1F}", + "feharabic" => "\u{0641}", + "feharmenian" => "\u{0586}", + "fehfinalarabic" => "\u{FED2}", + "fehinitialarabic" => "\u{FED3}", + "fehmedialarabic" => "\u{FED4}", + "feicoptic" => "\u{03E5}", + "female" => "\u{2640}", + "ff" => "\u{FB00}", + "ffi" => "\u{FB03}", + "ffl" => "\u{FB04}", + "fi" => "\u{FB01}", + "fifteencircle" => "\u{246E}", + "fifteenparen" => "\u{2482}", + "fifteenperiod" => "\u{2496}", + "figuredash" => "\u{2012}", + "filledbox" => "\u{25A0}", + "filledrect" => "\u{25AC}", + "finalkaf" => "\u{05DA}", + "finalkafdagesh" => "\u{FB3A}", + "finalkafdageshhebrew" => "\u{FB3A}", + "finalkafhebrew" => "\u{05DA}", + "finalkafqamats" => "\u{05DA}\u{05B8}", + "finalkafqamatshebrew" => "\u{05DA}\u{05B8}", + "finalkafsheva" => "\u{05DA}\u{05B0}", + "finalkafshevahebrew" => "\u{05DA}\u{05B0}", + "finalmem" => "\u{05DD}", + "finalmemhebrew" => "\u{05DD}", + "finalnun" => "\u{05DF}", + "finalnunhebrew" => "\u{05DF}", + "finalpe" => "\u{05E3}", + "finalpehebrew" => "\u{05E3}", + "finaltsadi" => "\u{05E5}", + "finaltsadihebrew" => "\u{05E5}", + "firsttonechinese" => "\u{02C9}", + "fisheye" => "\u{25C9}", + "fitacyrillic" => "\u{0473}", + "five" => "\u{0035}", + "fivearabic" => "\u{0665}", + "fivebengali" => "\u{09EB}", + "fivecircle" => "\u{2464}", + "fivecircleinversesansserif" => "\u{278E}", + "fivedeva" => "\u{096B}", + "fiveeighths" => "\u{215D}", + "fivegujarati" => "\u{0AEB}", + "fivegurmukhi" => "\u{0A6B}", + "fivehackarabic" => "\u{0665}", + "fivehangzhou" => "\u{3025}", + "fiveideographicparen" => "\u{3224}", + "fiveinferior" => "\u{2085}", + "fivemonospace" => "\u{FF15}", + "fiveoldstyle" => "\u{F735}", + "fiveparen" => "\u{2478}", + "fiveperiod" => "\u{248C}", + "fivepersian" => "\u{06F5}", + "fiveroman" => "\u{2174}", + "fivesuperior" => "\u{2075}", + "fivethai" => "\u{0E55}", + "fl" => "\u{FB02}", + "florin" => "\u{0192}", + "fmonospace" => "\u{FF46}", + "fmsquare" => "\u{3399}", + "fofanthai" => "\u{0E1F}", + "fofathai" => "\u{0E1D}", + "fongmanthai" => "\u{0E4F}", + "forall" => "\u{2200}", + "four" => "\u{0034}", + "fourarabic" => "\u{0664}", + "fourbengali" => "\u{09EA}", + "fourcircle" => "\u{2463}", + "fourcircleinversesansserif" => "\u{278D}", + "fourdeva" => "\u{096A}", + "fourgujarati" => "\u{0AEA}", + "fourgurmukhi" => "\u{0A6A}", + "fourhackarabic" => "\u{0664}", + "fourhangzhou" => "\u{3024}", + "fourideographicparen" => "\u{3223}", + "fourinferior" => "\u{2084}", + "fourmonospace" => "\u{FF14}", + "fournumeratorbengali" => "\u{09F7}", + "fouroldstyle" => "\u{F734}", + "fourparen" => "\u{2477}", + "fourperiod" => "\u{248B}", + "fourpersian" => "\u{06F4}", + "fourroman" => "\u{2173}", + "foursuperior" => "\u{2074}", + "fourteencircle" => "\u{246D}", + "fourteenparen" => "\u{2481}", + "fourteenperiod" => "\u{2495}", + "fourthai" => "\u{0E54}", + "fourthtonechinese" => "\u{02CB}", + "fparen" => "\u{24A1}", + "fraction" => "\u{2044}", + "franc" => "\u{20A3}", + "g" => "\u{0067}", + "gabengali" => "\u{0997}", + "gacute" => "\u{01F5}", + "gadeva" => "\u{0917}", + "gafarabic" => "\u{06AF}", + "gaffinalarabic" => "\u{FB93}", + "gafinitialarabic" => "\u{FB94}", + "gafmedialarabic" => "\u{FB95}", + "gagujarati" => "\u{0A97}", + "gagurmukhi" => "\u{0A17}", + "gahiragana" => "\u{304C}", + "gakatakana" => "\u{30AC}", + "gamma" => "\u{03B3}", + "gammalatinsmall" => "\u{0263}", + "gammasuperior" => "\u{02E0}", + "gangiacoptic" => "\u{03EB}", + "gbopomofo" => "\u{310D}", + "gbreve" => "\u{011F}", + "gcaron" => "\u{01E7}", + "gcedilla" => "\u{0123}", + "gcircle" => "\u{24D6}", + "gcircumflex" => "\u{011D}", + "gcommaaccent" => "\u{0123}", + "gdot" => "\u{0121}", + "gdotaccent" => "\u{0121}", + "gecyrillic" => "\u{0433}", + "gehiragana" => "\u{3052}", + "gekatakana" => "\u{30B2}", + "geometricallyequal" => "\u{2251}", + "gereshaccenthebrew" => "\u{059C}", + "gereshhebrew" => "\u{05F3}", + "gereshmuqdamhebrew" => "\u{059D}", + "germandbls" => "\u{00DF}", + "gershayimaccenthebrew" => "\u{059E}", + "gershayimhebrew" => "\u{05F4}", + "getamark" => "\u{3013}", + "ghabengali" => "\u{0998}", + "ghadarmenian" => "\u{0572}", + "ghadeva" => "\u{0918}", + "ghagujarati" => "\u{0A98}", + "ghagurmukhi" => "\u{0A18}", + "ghainarabic" => "\u{063A}", + "ghainfinalarabic" => "\u{FECE}", + "ghaininitialarabic" => "\u{FECF}", + "ghainmedialarabic" => "\u{FED0}", + "ghemiddlehookcyrillic" => "\u{0495}", + "ghestrokecyrillic" => "\u{0493}", + "gheupturncyrillic" => "\u{0491}", + "ghhadeva" => "\u{095A}", + "ghhagurmukhi" => "\u{0A5A}", + "ghook" => "\u{0260}", + "ghzsquare" => "\u{3393}", + "gihiragana" => "\u{304E}", + "gikatakana" => "\u{30AE}", + "gimarmenian" => "\u{0563}", + "gimel" => "\u{05D2}", + "gimeldagesh" => "\u{FB32}", + "gimeldageshhebrew" => "\u{FB32}", + "gimelhebrew" => "\u{05D2}", + "gjecyrillic" => "\u{0453}", + "glottalinvertedstroke" => "\u{01BE}", + "glottalstop" => "\u{0294}", + "glottalstopinverted" => "\u{0296}", + "glottalstopmod" => "\u{02C0}", + "glottalstopreversed" => "\u{0295}", + "glottalstopreversedmod" => "\u{02C1}", + "glottalstopreversedsuperior" => "\u{02E4}", + "glottalstopstroke" => "\u{02A1}", + "glottalstopstrokereversed" => "\u{02A2}", + "gmacron" => "\u{1E21}", + "gmonospace" => "\u{FF47}", + "gohiragana" => "\u{3054}", + "gokatakana" => "\u{30B4}", + "gparen" => "\u{24A2}", + "gpasquare" => "\u{33AC}", + "gradient" => "\u{2207}", + "grave" => "\u{0060}", + "gravebelowcmb" => "\u{0316}", + "gravecmb" => "\u{0300}", + "gravecomb" => "\u{0300}", + "gravedeva" => "\u{0953}", + "gravelowmod" => "\u{02CE}", + "gravemonospace" => "\u{FF40}", + "gravetonecmb" => "\u{0340}", + "greater" => "\u{003E}", + "greaterequal" => "\u{2265}", + "greaterequalorless" => "\u{22DB}", + "greatermonospace" => "\u{FF1E}", + "greaterorequivalent" => "\u{2273}", + "greaterorless" => "\u{2277}", + "greateroverequal" => "\u{2267}", + "greatersmall" => "\u{FE65}", + "gscript" => "\u{0261}", + "gstroke" => "\u{01E5}", + "guhiragana" => "\u{3050}", + "guillemotleft" => "\u{00AB}", + "guillemotright" => "\u{00BB}", + "guilsinglleft" => "\u{2039}", + "guilsinglright" => "\u{203A}", + "gukatakana" => "\u{30B0}", + "guramusquare" => "\u{3318}", + "gysquare" => "\u{33C9}", + "h" => "\u{0068}", + "haabkhasiancyrillic" => "\u{04A9}", + "haaltonearabic" => "\u{06C1}", + "habengali" => "\u{09B9}", + "hadescendercyrillic" => "\u{04B3}", + "hadeva" => "\u{0939}", + "hagujarati" => "\u{0AB9}", + "hagurmukhi" => "\u{0A39}", + "haharabic" => "\u{062D}", + "hahfinalarabic" => "\u{FEA2}", + "hahinitialarabic" => "\u{FEA3}", + "hahiragana" => "\u{306F}", + "hahmedialarabic" => "\u{FEA4}", + "haitusquare" => "\u{332A}", + "hakatakana" => "\u{30CF}", + "hakatakanahalfwidth" => "\u{FF8A}", + "halantgurmukhi" => "\u{0A4D}", + "hamzaarabic" => "\u{0621}", + "hamzadammaarabic" => "\u{0621}\u{064F}", + "hamzadammatanarabic" => "\u{0621}\u{064C}", + "hamzafathaarabic" => "\u{0621}\u{064E}", + "hamzafathatanarabic" => "\u{0621}\u{064B}", + "hamzalowarabic" => "\u{0621}", + "hamzalowkasraarabic" => "\u{0621}\u{0650}", + "hamzalowkasratanarabic" => "\u{0621}\u{064D}", + "hamzasukunarabic" => "\u{0621}\u{0652}", + "hangulfiller" => "\u{3164}", + "hardsigncyrillic" => "\u{044A}", + "harpoonleftbarbup" => "\u{21BC}", + "harpoonrightbarbup" => "\u{21C0}", + "hasquare" => "\u{33CA}", + "hatafpatah" => "\u{05B2}", + "hatafpatah16" => "\u{05B2}", + "hatafpatah23" => "\u{05B2}", + "hatafpatah2f" => "\u{05B2}", + "hatafpatahhebrew" => "\u{05B2}", + "hatafpatahnarrowhebrew" => "\u{05B2}", + "hatafpatahquarterhebrew" => "\u{05B2}", + "hatafpatahwidehebrew" => "\u{05B2}", + "hatafqamats" => "\u{05B3}", + "hatafqamats1b" => "\u{05B3}", + "hatafqamats28" => "\u{05B3}", + "hatafqamats34" => "\u{05B3}", + "hatafqamatshebrew" => "\u{05B3}", + "hatafqamatsnarrowhebrew" => "\u{05B3}", + "hatafqamatsquarterhebrew" => "\u{05B3}", + "hatafqamatswidehebrew" => "\u{05B3}", + "hatafsegol" => "\u{05B1}", + "hatafsegol17" => "\u{05B1}", + "hatafsegol24" => "\u{05B1}", + "hatafsegol30" => "\u{05B1}", + "hatafsegolhebrew" => "\u{05B1}", + "hatafsegolnarrowhebrew" => "\u{05B1}", + "hatafsegolquarterhebrew" => "\u{05B1}", + "hatafsegolwidehebrew" => "\u{05B1}", + "hbar" => "\u{0127}", + "hbopomofo" => "\u{310F}", + "hbrevebelow" => "\u{1E2B}", + "hcedilla" => "\u{1E29}", + "hcircle" => "\u{24D7}", + "hcircumflex" => "\u{0125}", + "hdieresis" => "\u{1E27}", + "hdotaccent" => "\u{1E23}", + "hdotbelow" => "\u{1E25}", + "he" => "\u{05D4}", + "heart" => "\u{2665}", + "heartsuitblack" => "\u{2665}", + "heartsuitwhite" => "\u{2661}", + "hedagesh" => "\u{FB34}", + "hedageshhebrew" => "\u{FB34}", + "hehaltonearabic" => "\u{06C1}", + "heharabic" => "\u{0647}", + "hehebrew" => "\u{05D4}", + "hehfinalaltonearabic" => "\u{FBA7}", + "hehfinalalttwoarabic" => "\u{FEEA}", + "hehfinalarabic" => "\u{FEEA}", + "hehhamzaabovefinalarabic" => "\u{FBA5}", + "hehhamzaaboveisolatedarabic" => "\u{FBA4}", + "hehinitialaltonearabic" => "\u{FBA8}", + "hehinitialarabic" => "\u{FEEB}", + "hehiragana" => "\u{3078}", + "hehmedialaltonearabic" => "\u{FBA9}", + "hehmedialarabic" => "\u{FEEC}", + "heiseierasquare" => "\u{337B}", + "hekatakana" => "\u{30D8}", + "hekatakanahalfwidth" => "\u{FF8D}", + "hekutaarusquare" => "\u{3336}", + "henghook" => "\u{0267}", + "herutusquare" => "\u{3339}", + "het" => "\u{05D7}", + "hethebrew" => "\u{05D7}", + "hhook" => "\u{0266}", + "hhooksuperior" => "\u{02B1}", + "hieuhacirclekorean" => "\u{327B}", + "hieuhaparenkorean" => "\u{321B}", + "hieuhcirclekorean" => "\u{326D}", + "hieuhkorean" => "\u{314E}", + "hieuhparenkorean" => "\u{320D}", + "hihiragana" => "\u{3072}", + "hikatakana" => "\u{30D2}", + "hikatakanahalfwidth" => "\u{FF8B}", + "hiriq" => "\u{05B4}", + "hiriq14" => "\u{05B4}", + "hiriq21" => "\u{05B4}", + "hiriq2d" => "\u{05B4}", + "hiriqhebrew" => "\u{05B4}", + "hiriqnarrowhebrew" => "\u{05B4}", + "hiriqquarterhebrew" => "\u{05B4}", + "hiriqwidehebrew" => "\u{05B4}", + "hlinebelow" => "\u{1E96}", + "hmonospace" => "\u{FF48}", + "hoarmenian" => "\u{0570}", + "hohipthai" => "\u{0E2B}", + "hohiragana" => "\u{307B}", + "hokatakana" => "\u{30DB}", + "hokatakanahalfwidth" => "\u{FF8E}", + "holam" => "\u{05B9}", + "holam19" => "\u{05B9}", + "holam26" => "\u{05B9}", + "holam32" => "\u{05B9}", + "holamhebrew" => "\u{05B9}", + "holamnarrowhebrew" => "\u{05B9}", + "holamquarterhebrew" => "\u{05B9}", + "holamwidehebrew" => "\u{05B9}", + "honokhukthai" => "\u{0E2E}", + "hookabovecomb" => "\u{0309}", + "hookcmb" => "\u{0309}", + "hookpalatalizedbelowcmb" => "\u{0321}", + "hookretroflexbelowcmb" => "\u{0322}", + "hoonsquare" => "\u{3342}", + "horicoptic" => "\u{03E9}", + "horizontalbar" => "\u{2015}", + "horncmb" => "\u{031B}", + "hotsprings" => "\u{2668}", + "house" => "\u{2302}", + "hparen" => "\u{24A3}", + "hsuperior" => "\u{02B0}", + "hturned" => "\u{0265}", + "huhiragana" => "\u{3075}", + "huiitosquare" => "\u{3333}", + "hukatakana" => "\u{30D5}", + "hukatakanahalfwidth" => "\u{FF8C}", + "hungarumlaut" => "\u{02DD}", + "hungarumlautcmb" => "\u{030B}", + "hv" => "\u{0195}", + "hyphen" => "\u{002D}", + "hypheninferior" => "\u{F6E5}", + "hyphenmonospace" => "\u{FF0D}", + "hyphensmall" => "\u{FE63}", + "hyphensuperior" => "\u{F6E6}", + "hyphentwo" => "\u{2010}", + "i" => "\u{0069}", + "iacute" => "\u{00ED}", + "iacyrillic" => "\u{044F}", + "ibengali" => "\u{0987}", + "ibopomofo" => "\u{3127}", + "ibreve" => "\u{012D}", + "icaron" => "\u{01D0}", + "icircle" => "\u{24D8}", + "icircumflex" => "\u{00EE}", + "icyrillic" => "\u{0456}", + "idblgrave" => "\u{0209}", + "ideographearthcircle" => "\u{328F}", + "ideographfirecircle" => "\u{328B}", + "ideographicallianceparen" => "\u{323F}", + "ideographiccallparen" => "\u{323A}", + "ideographiccentrecircle" => "\u{32A5}", + "ideographicclose" => "\u{3006}", + "ideographiccomma" => "\u{3001}", + "ideographiccommaleft" => "\u{FF64}", + "ideographiccongratulationparen" => "\u{3237}", + "ideographiccorrectcircle" => "\u{32A3}", + "ideographicearthparen" => "\u{322F}", + "ideographicenterpriseparen" => "\u{323D}", + "ideographicexcellentcircle" => "\u{329D}", + "ideographicfestivalparen" => "\u{3240}", + "ideographicfinancialcircle" => "\u{3296}", + "ideographicfinancialparen" => "\u{3236}", + "ideographicfireparen" => "\u{322B}", + "ideographichaveparen" => "\u{3232}", + "ideographichighcircle" => "\u{32A4}", + "ideographiciterationmark" => "\u{3005}", + "ideographiclaborcircle" => "\u{3298}", + "ideographiclaborparen" => "\u{3238}", + "ideographicleftcircle" => "\u{32A7}", + "ideographiclowcircle" => "\u{32A6}", + "ideographicmedicinecircle" => "\u{32A9}", + "ideographicmetalparen" => "\u{322E}", + "ideographicmoonparen" => "\u{322A}", + "ideographicnameparen" => "\u{3234}", + "ideographicperiod" => "\u{3002}", + "ideographicprintcircle" => "\u{329E}", + "ideographicreachparen" => "\u{3243}", + "ideographicrepresentparen" => "\u{3239}", + "ideographicresourceparen" => "\u{323E}", + "ideographicrightcircle" => "\u{32A8}", + "ideographicsecretcircle" => "\u{3299}", + "ideographicselfparen" => "\u{3242}", + "ideographicsocietyparen" => "\u{3233}", + "ideographicspace" => "\u{3000}", + "ideographicspecialparen" => "\u{3235}", + "ideographicstockparen" => "\u{3231}", + "ideographicstudyparen" => "\u{323B}", + "ideographicsunparen" => "\u{3230}", + "ideographicsuperviseparen" => "\u{323C}", + "ideographicwaterparen" => "\u{322C}", + "ideographicwoodparen" => "\u{322D}", + "ideographiczero" => "\u{3007}", + "ideographmetalcircle" => "\u{328E}", + "ideographmooncircle" => "\u{328A}", + "ideographnamecircle" => "\u{3294}", + "ideographsuncircle" => "\u{3290}", + "ideographwatercircle" => "\u{328C}", + "ideographwoodcircle" => "\u{328D}", + "ideva" => "\u{0907}", + "idieresis" => "\u{00EF}", + "idieresisacute" => "\u{1E2F}", + "idieresiscyrillic" => "\u{04E5}", + "idotbelow" => "\u{1ECB}", + "iebrevecyrillic" => "\u{04D7}", + "iecyrillic" => "\u{0435}", + "ieungacirclekorean" => "\u{3275}", + "ieungaparenkorean" => "\u{3215}", + "ieungcirclekorean" => "\u{3267}", + "ieungkorean" => "\u{3147}", + "ieungparenkorean" => "\u{3207}", + "igrave" => "\u{00EC}", + "igujarati" => "\u{0A87}", + "igurmukhi" => "\u{0A07}", + "ihiragana" => "\u{3044}", + "ihookabove" => "\u{1EC9}", + "iibengali" => "\u{0988}", + "iicyrillic" => "\u{0438}", + "iideva" => "\u{0908}", + "iigujarati" => "\u{0A88}", + "iigurmukhi" => "\u{0A08}", + "iimatragurmukhi" => "\u{0A40}", + "iinvertedbreve" => "\u{020B}", + "iishortcyrillic" => "\u{0439}", + "iivowelsignbengali" => "\u{09C0}", + "iivowelsigndeva" => "\u{0940}", + "iivowelsigngujarati" => "\u{0AC0}", + "ij" => "\u{0133}", + "ikatakana" => "\u{30A4}", + "ikatakanahalfwidth" => "\u{FF72}", + "ikorean" => "\u{3163}", + "ilde" => "\u{02DC}", + "iluyhebrew" => "\u{05AC}", + "imacron" => "\u{012B}", + "imacroncyrillic" => "\u{04E3}", + "imageorapproximatelyequal" => "\u{2253}", + "imatragurmukhi" => "\u{0A3F}", + "imonospace" => "\u{FF49}", + "increment" => "\u{2206}", + "infinity" => "\u{221E}", + "iniarmenian" => "\u{056B}", + "integral" => "\u{222B}", + "integralbottom" => "\u{2321}", + "integralbt" => "\u{2321}", + "integralex" => "\u{F8F5}", + "integraltop" => "\u{2320}", + "integraltp" => "\u{2320}", + "intersection" => "\u{2229}", + "intisquare" => "\u{3305}", + "invbullet" => "\u{25D8}", + "invcircle" => "\u{25D9}", + "invsmileface" => "\u{263B}", + "iocyrillic" => "\u{0451}", + "iogonek" => "\u{012F}", + "iota" => "\u{03B9}", + "iotadieresis" => "\u{03CA}", + "iotadieresistonos" => "\u{0390}", + "iotalatin" => "\u{0269}", + "iotatonos" => "\u{03AF}", + "iparen" => "\u{24A4}", + "irigurmukhi" => "\u{0A72}", + "ismallhiragana" => "\u{3043}", + "ismallkatakana" => "\u{30A3}", + "ismallkatakanahalfwidth" => "\u{FF68}", + "issharbengali" => "\u{09FA}", + "istroke" => "\u{0268}", + "isuperior" => "\u{F6ED}", + "iterationhiragana" => "\u{309D}", + "iterationkatakana" => "\u{30FD}", + "itilde" => "\u{0129}", + "itildebelow" => "\u{1E2D}", + "iubopomofo" => "\u{3129}", + "iucyrillic" => "\u{044E}", + "ivowelsignbengali" => "\u{09BF}", + "ivowelsigndeva" => "\u{093F}", + "ivowelsigngujarati" => "\u{0ABF}", + "izhitsacyrillic" => "\u{0475}", + "izhitsadblgravecyrillic" => "\u{0477}", + "j" => "\u{006A}", + "jaarmenian" => "\u{0571}", + "jabengali" => "\u{099C}", + "jadeva" => "\u{091C}", + "jagujarati" => "\u{0A9C}", + "jagurmukhi" => "\u{0A1C}", + "jbopomofo" => "\u{3110}", + "jcaron" => "\u{01F0}", + "jcircle" => "\u{24D9}", + "jcircumflex" => "\u{0135}", + "jcrossedtail" => "\u{029D}", + "jdotlessstroke" => "\u{025F}", + "jecyrillic" => "\u{0458}", + "jeemarabic" => "\u{062C}", + "jeemfinalarabic" => "\u{FE9E}", + "jeeminitialarabic" => "\u{FE9F}", + "jeemmedialarabic" => "\u{FEA0}", + "jeharabic" => "\u{0698}", + "jehfinalarabic" => "\u{FB8B}", + "jhabengali" => "\u{099D}", + "jhadeva" => "\u{091D}", + "jhagujarati" => "\u{0A9D}", + "jhagurmukhi" => "\u{0A1D}", + "jheharmenian" => "\u{057B}", + "jis" => "\u{3004}", + "jmonospace" => "\u{FF4A}", + "jparen" => "\u{24A5}", + "jsuperior" => "\u{02B2}", + "k" => "\u{006B}", + "kabashkircyrillic" => "\u{04A1}", + "kabengali" => "\u{0995}", + "kacute" => "\u{1E31}", + "kacyrillic" => "\u{043A}", + "kadescendercyrillic" => "\u{049B}", + "kadeva" => "\u{0915}", + "kaf" => "\u{05DB}", + "kafarabic" => "\u{0643}", + "kafdagesh" => "\u{FB3B}", + "kafdageshhebrew" => "\u{FB3B}", + "kaffinalarabic" => "\u{FEDA}", + "kafhebrew" => "\u{05DB}", + "kafinitialarabic" => "\u{FEDB}", + "kafmedialarabic" => "\u{FEDC}", + "kafrafehebrew" => "\u{FB4D}", + "kagujarati" => "\u{0A95}", + "kagurmukhi" => "\u{0A15}", + "kahiragana" => "\u{304B}", + "kahookcyrillic" => "\u{04C4}", + "kakatakana" => "\u{30AB}", + "kakatakanahalfwidth" => "\u{FF76}", + "kappa" => "\u{03BA}", + "kappasymbolgreek" => "\u{03F0}", + "kapyeounmieumkorean" => "\u{3171}", + "kapyeounphieuphkorean" => "\u{3184}", + "kapyeounpieupkorean" => "\u{3178}", + "kapyeounssangpieupkorean" => "\u{3179}", + "karoriisquare" => "\u{330D}", + "kashidaautoarabic" => "\u{0640}", + "kashidaautonosidebearingarabic" => "\u{0640}", + "kasmallkatakana" => "\u{30F5}", + "kasquare" => "\u{3384}", + "kasraarabic" => "\u{0650}", + "kasratanarabic" => "\u{064D}", + "kastrokecyrillic" => "\u{049F}", + "katahiraprolongmarkhalfwidth" => "\u{FF70}", + "kaverticalstrokecyrillic" => "\u{049D}", + "kbopomofo" => "\u{310E}", + "kcalsquare" => "\u{3389}", + "kcaron" => "\u{01E9}", + "kcedilla" => "\u{0137}", + "kcircle" => "\u{24DA}", + "kcommaaccent" => "\u{0137}", + "kdotbelow" => "\u{1E33}", + "keharmenian" => "\u{0584}", + "kehiragana" => "\u{3051}", + "kekatakana" => "\u{30B1}", + "kekatakanahalfwidth" => "\u{FF79}", + "kenarmenian" => "\u{056F}", + "kesmallkatakana" => "\u{30F6}", + "kgreenlandic" => "\u{0138}", + "khabengali" => "\u{0996}", + "khacyrillic" => "\u{0445}", + "khadeva" => "\u{0916}", + "khagujarati" => "\u{0A96}", + "khagurmukhi" => "\u{0A16}", + "khaharabic" => "\u{062E}", + "khahfinalarabic" => "\u{FEA6}", + "khahinitialarabic" => "\u{FEA7}", + "khahmedialarabic" => "\u{FEA8}", + "kheicoptic" => "\u{03E7}", + "khhadeva" => "\u{0959}", + "khhagurmukhi" => "\u{0A59}", + "khieukhacirclekorean" => "\u{3278}", + "khieukhaparenkorean" => "\u{3218}", + "khieukhcirclekorean" => "\u{326A}", + "khieukhkorean" => "\u{314B}", + "khieukhparenkorean" => "\u{320A}", + "khokhaithai" => "\u{0E02}", + "khokhonthai" => "\u{0E05}", + "khokhuatthai" => "\u{0E03}", + "khokhwaithai" => "\u{0E04}", + "khomutthai" => "\u{0E5B}", + "khook" => "\u{0199}", + "khorakhangthai" => "\u{0E06}", + "khzsquare" => "\u{3391}", + "kihiragana" => "\u{304D}", + "kikatakana" => "\u{30AD}", + "kikatakanahalfwidth" => "\u{FF77}", + "kiroguramusquare" => "\u{3315}", + "kiromeetorusquare" => "\u{3316}", + "kirosquare" => "\u{3314}", + "kiyeokacirclekorean" => "\u{326E}", + "kiyeokaparenkorean" => "\u{320E}", + "kiyeokcirclekorean" => "\u{3260}", + "kiyeokkorean" => "\u{3131}", + "kiyeokparenkorean" => "\u{3200}", + "kiyeoksioskorean" => "\u{3133}", + "kjecyrillic" => "\u{045C}", + "klinebelow" => "\u{1E35}", + "klsquare" => "\u{3398}", + "kmcubedsquare" => "\u{33A6}", + "kmonospace" => "\u{FF4B}", + "kmsquaredsquare" => "\u{33A2}", + "kohiragana" => "\u{3053}", + "kohmsquare" => "\u{33C0}", + "kokaithai" => "\u{0E01}", + "kokatakana" => "\u{30B3}", + "kokatakanahalfwidth" => "\u{FF7A}", + "kooposquare" => "\u{331E}", + "koppacyrillic" => "\u{0481}", + "koreanstandardsymbol" => "\u{327F}", + "koroniscmb" => "\u{0343}", + "kparen" => "\u{24A6}", + "kpasquare" => "\u{33AA}", + "ksicyrillic" => "\u{046F}", + "ktsquare" => "\u{33CF}", + "kturned" => "\u{029E}", + "kuhiragana" => "\u{304F}", + "kukatakana" => "\u{30AF}", + "kukatakanahalfwidth" => "\u{FF78}", + "kvsquare" => "\u{33B8}", + "kwsquare" => "\u{33BE}", + "l" => "\u{006C}", + "labengali" => "\u{09B2}", + "lacute" => "\u{013A}", + "ladeva" => "\u{0932}", + "lagujarati" => "\u{0AB2}", + "lagurmukhi" => "\u{0A32}", + "lakkhangyaothai" => "\u{0E45}", + "lamaleffinalarabic" => "\u{FEFC}", + "lamalefhamzaabovefinalarabic" => "\u{FEF8}", + "lamalefhamzaaboveisolatedarabic" => "\u{FEF7}", + "lamalefhamzabelowfinalarabic" => "\u{FEFA}", + "lamalefhamzabelowisolatedarabic" => "\u{FEF9}", + "lamalefisolatedarabic" => "\u{FEFB}", + "lamalefmaddaabovefinalarabic" => "\u{FEF6}", + "lamalefmaddaaboveisolatedarabic" => "\u{FEF5}", + "lamarabic" => "\u{0644}", + "lambda" => "\u{03BB}", + "lambdastroke" => "\u{019B}", + "lamed" => "\u{05DC}", + "lameddagesh" => "\u{FB3C}", + "lameddageshhebrew" => "\u{FB3C}", + "lamedhebrew" => "\u{05DC}", + "lamedholam" => "\u{05DC}\u{05B9}", + "lamedholamdagesh" => "\u{05DC}\u{05B9}\u{05BC}", + "lamedholamdageshhebrew" => "\u{05DC}\u{05B9}\u{05BC}", + "lamedholamhebrew" => "\u{05DC}\u{05B9}", + "lamfinalarabic" => "\u{FEDE}", + "lamhahinitialarabic" => "\u{FCCA}", + "laminitialarabic" => "\u{FEDF}", + "lamjeeminitialarabic" => "\u{FCC9}", + "lamkhahinitialarabic" => "\u{FCCB}", + "lamlamhehisolatedarabic" => "\u{FDF2}", + "lammedialarabic" => "\u{FEE0}", + "lammeemhahinitialarabic" => "\u{FD88}", + "lammeeminitialarabic" => "\u{FCCC}", + "lammeemjeeminitialarabic" => "\u{FEDF}\u{FEE4}\u{FEA0}", + "lammeemkhahinitialarabic" => "\u{FEDF}\u{FEE4}\u{FEA8}", + "largecircle" => "\u{25EF}", + "lbar" => "\u{019A}", + "lbelt" => "\u{026C}", + "lbopomofo" => "\u{310C}", + "lcaron" => "\u{013E}", + "lcedilla" => "\u{013C}", + "lcircle" => "\u{24DB}", + "lcircumflexbelow" => "\u{1E3D}", + "lcommaaccent" => "\u{013C}", + "ldot" => "\u{0140}", + "ldotaccent" => "\u{0140}", + "ldotbelow" => "\u{1E37}", + "ldotbelowmacron" => "\u{1E39}", + "leftangleabovecmb" => "\u{031A}", + "lefttackbelowcmb" => "\u{0318}", + "less" => "\u{003C}", + "lessequal" => "\u{2264}", + "lessequalorgreater" => "\u{22DA}", + "lessmonospace" => "\u{FF1C}", + "lessorequivalent" => "\u{2272}", + "lessorgreater" => "\u{2276}", + "lessoverequal" => "\u{2266}", + "lesssmall" => "\u{FE64}", + "lezh" => "\u{026E}", + "lfblock" => "\u{258C}", + "lhookretroflex" => "\u{026D}", + "lira" => "\u{20A4}", + "liwnarmenian" => "\u{056C}", + "lj" => "\u{01C9}", + "ljecyrillic" => "\u{0459}", + "ll" => "\u{F6C0}", + "lladeva" => "\u{0933}", + "llagujarati" => "\u{0AB3}", + "llinebelow" => "\u{1E3B}", + "llladeva" => "\u{0934}", + "llvocalicbengali" => "\u{09E1}", + "llvocalicdeva" => "\u{0961}", + "llvocalicvowelsignbengali" => "\u{09E3}", + "llvocalicvowelsigndeva" => "\u{0963}", + "lmiddletilde" => "\u{026B}", + "lmonospace" => "\u{FF4C}", + "lmsquare" => "\u{33D0}", + "lochulathai" => "\u{0E2C}", + "logicaland" => "\u{2227}", + "logicalnot" => "\u{00AC}", + "logicalnotreversed" => "\u{2310}", + "logicalor" => "\u{2228}", + "lolingthai" => "\u{0E25}", + "longs" => "\u{017F}", + "lowlinecenterline" => "\u{FE4E}", + "lowlinecmb" => "\u{0332}", + "lowlinedashed" => "\u{FE4D}", + "lozenge" => "\u{25CA}", + "lparen" => "\u{24A7}", + "lslash" => "\u{0142}", + "lsquare" => "\u{2113}", + "lsuperior" => "\u{F6EE}", + "ltshade" => "\u{2591}", + "luthai" => "\u{0E26}", + "lvocalicbengali" => "\u{098C}", + "lvocalicdeva" => "\u{090C}", + "lvocalicvowelsignbengali" => "\u{09E2}", + "lvocalicvowelsigndeva" => "\u{0962}", + "lxsquare" => "\u{33D3}", + "m" => "\u{006D}", + "mabengali" => "\u{09AE}", + "macron" => "\u{00AF}", + "macronbelowcmb" => "\u{0331}", + "macroncmb" => "\u{0304}", + "macronlowmod" => "\u{02CD}", + "macronmonospace" => "\u{FFE3}", + "macute" => "\u{1E3F}", + "madeva" => "\u{092E}", + "magujarati" => "\u{0AAE}", + "magurmukhi" => "\u{0A2E}", + "mahapakhhebrew" => "\u{05A4}", + "mahapakhlefthebrew" => "\u{05A4}", + "mahiragana" => "\u{307E}", + "maichattawalowleftthai" => "\u{F895}", + "maichattawalowrightthai" => "\u{F894}", + "maichattawathai" => "\u{0E4B}", + "maichattawaupperleftthai" => "\u{F893}", + "maieklowleftthai" => "\u{F88C}", + "maieklowrightthai" => "\u{F88B}", + "maiekthai" => "\u{0E48}", + "maiekupperleftthai" => "\u{F88A}", + "maihanakatleftthai" => "\u{F884}", + "maihanakatthai" => "\u{0E31}", + "maitaikhuleftthai" => "\u{F889}", + "maitaikhuthai" => "\u{0E47}", + "maitholowleftthai" => "\u{F88F}", + "maitholowrightthai" => "\u{F88E}", + "maithothai" => "\u{0E49}", + "maithoupperleftthai" => "\u{F88D}", + "maitrilowleftthai" => "\u{F892}", + "maitrilowrightthai" => "\u{F891}", + "maitrithai" => "\u{0E4A}", + "maitriupperleftthai" => "\u{F890}", + "maiyamokthai" => "\u{0E46}", + "makatakana" => "\u{30DE}", + "makatakanahalfwidth" => "\u{FF8F}", + "male" => "\u{2642}", + "mansyonsquare" => "\u{3347}", + "maqafhebrew" => "\u{05BE}", + "mars" => "\u{2642}", + "masoracirclehebrew" => "\u{05AF}", + "masquare" => "\u{3383}", + "mbopomofo" => "\u{3107}", + "mbsquare" => "\u{33D4}", + "mcircle" => "\u{24DC}", + "mcubedsquare" => "\u{33A5}", + "mdotaccent" => "\u{1E41}", + "mdotbelow" => "\u{1E43}", + "meemarabic" => "\u{0645}", + "meemfinalarabic" => "\u{FEE2}", + "meeminitialarabic" => "\u{FEE3}", + "meemmedialarabic" => "\u{FEE4}", + "meemmeeminitialarabic" => "\u{FCD1}", + "meemmeemisolatedarabic" => "\u{FC48}", + "meetorusquare" => "\u{334D}", + "mehiragana" => "\u{3081}", + "meizierasquare" => "\u{337E}", + "mekatakana" => "\u{30E1}", + "mekatakanahalfwidth" => "\u{FF92}", + "mem" => "\u{05DE}", + "memdagesh" => "\u{FB3E}", + "memdageshhebrew" => "\u{FB3E}", + "memhebrew" => "\u{05DE}", + "menarmenian" => "\u{0574}", + "merkhahebrew" => "\u{05A5}", + "merkhakefulahebrew" => "\u{05A6}", + "merkhakefulalefthebrew" => "\u{05A6}", + "merkhalefthebrew" => "\u{05A5}", + "mhook" => "\u{0271}", + "mhzsquare" => "\u{3392}", + "middledotkatakanahalfwidth" => "\u{FF65}", + "middot" => "\u{00B7}", + "mieumacirclekorean" => "\u{3272}", + "mieumaparenkorean" => "\u{3212}", + "mieumcirclekorean" => "\u{3264}", + "mieumkorean" => "\u{3141}", + "mieumpansioskorean" => "\u{3170}", + "mieumparenkorean" => "\u{3204}", + "mieumpieupkorean" => "\u{316E}", + "mieumsioskorean" => "\u{316F}", + "mihiragana" => "\u{307F}", + "mikatakana" => "\u{30DF}", + "mikatakanahalfwidth" => "\u{FF90}", + "minus" => "\u{2212}", + "minusbelowcmb" => "\u{0320}", + "minuscircle" => "\u{2296}", + "minusmod" => "\u{02D7}", + "minusplus" => "\u{2213}", + "minute" => "\u{2032}", + "miribaarusquare" => "\u{334A}", + "mirisquare" => "\u{3349}", + "mlonglegturned" => "\u{0270}", + "mlsquare" => "\u{3396}", + "mmcubedsquare" => "\u{33A3}", + "mmonospace" => "\u{FF4D}", + "mmsquaredsquare" => "\u{339F}", + "mohiragana" => "\u{3082}", + "mohmsquare" => "\u{33C1}", + "mokatakana" => "\u{30E2}", + "mokatakanahalfwidth" => "\u{FF93}", + "molsquare" => "\u{33D6}", + "momathai" => "\u{0E21}", + "moverssquare" => "\u{33A7}", + "moverssquaredsquare" => "\u{33A8}", + "mparen" => "\u{24A8}", + "mpasquare" => "\u{33AB}", + "mssquare" => "\u{33B3}", + "msuperior" => "\u{F6EF}", + "mturned" => "\u{026F}", + "mu" => "\u{00B5}", + "mu1" => "\u{00B5}", + "muasquare" => "\u{3382}", + "muchgreater" => "\u{226B}", + "muchless" => "\u{226A}", + "mufsquare" => "\u{338C}", + "mugreek" => "\u{03BC}", + "mugsquare" => "\u{338D}", + "muhiragana" => "\u{3080}", + "mukatakana" => "\u{30E0}", + "mukatakanahalfwidth" => "\u{FF91}", + "mulsquare" => "\u{3395}", + "multiply" => "\u{00D7}", + "mumsquare" => "\u{339B}", + "munahhebrew" => "\u{05A3}", + "munahlefthebrew" => "\u{05A3}", + "musicalnote" => "\u{266A}", + "musicalnotedbl" => "\u{266B}", + "musicflatsign" => "\u{266D}", + "musicsharpsign" => "\u{266F}", + "mussquare" => "\u{33B2}", + "muvsquare" => "\u{33B6}", + "muwsquare" => "\u{33BC}", + "mvmegasquare" => "\u{33B9}", + "mvsquare" => "\u{33B7}", + "mwmegasquare" => "\u{33BF}", + "mwsquare" => "\u{33BD}", + "n" => "\u{006E}", + "nabengali" => "\u{09A8}", + "nabla" => "\u{2207}", + "nacute" => "\u{0144}", + "nadeva" => "\u{0928}", + "nagujarati" => "\u{0AA8}", + "nagurmukhi" => "\u{0A28}", + "nahiragana" => "\u{306A}", + "nakatakana" => "\u{30CA}", + "nakatakanahalfwidth" => "\u{FF85}", + "napostrophe" => "\u{0149}", + "nasquare" => "\u{3381}", + "nbopomofo" => "\u{310B}", + "nbspace" => "\u{00A0}", + "ncaron" => "\u{0148}", + "ncedilla" => "\u{0146}", + "ncircle" => "\u{24DD}", + "ncircumflexbelow" => "\u{1E4B}", + "ncommaaccent" => "\u{0146}", + "ndotaccent" => "\u{1E45}", + "ndotbelow" => "\u{1E47}", + "nehiragana" => "\u{306D}", + "nekatakana" => "\u{30CD}", + "nekatakanahalfwidth" => "\u{FF88}", + "newsheqelsign" => "\u{20AA}", + "nfsquare" => "\u{338B}", + "ngabengali" => "\u{0999}", + "ngadeva" => "\u{0919}", + "ngagujarati" => "\u{0A99}", + "ngagurmukhi" => "\u{0A19}", + "ngonguthai" => "\u{0E07}", + "nhiragana" => "\u{3093}", + "nhookleft" => "\u{0272}", + "nhookretroflex" => "\u{0273}", + "nieunacirclekorean" => "\u{326F}", + "nieunaparenkorean" => "\u{320F}", + "nieuncieuckorean" => "\u{3135}", + "nieuncirclekorean" => "\u{3261}", + "nieunhieuhkorean" => "\u{3136}", + "nieunkorean" => "\u{3134}", + "nieunpansioskorean" => "\u{3168}", + "nieunparenkorean" => "\u{3201}", + "nieunsioskorean" => "\u{3167}", + "nieuntikeutkorean" => "\u{3166}", + "nihiragana" => "\u{306B}", + "nikatakana" => "\u{30CB}", + "nikatakanahalfwidth" => "\u{FF86}", + "nikhahitleftthai" => "\u{F899}", + "nikhahitthai" => "\u{0E4D}", + "nine" => "\u{0039}", + "ninearabic" => "\u{0669}", + "ninebengali" => "\u{09EF}", + "ninecircle" => "\u{2468}", + "ninecircleinversesansserif" => "\u{2792}", + "ninedeva" => "\u{096F}", + "ninegujarati" => "\u{0AEF}", + "ninegurmukhi" => "\u{0A6F}", + "ninehackarabic" => "\u{0669}", + "ninehangzhou" => "\u{3029}", + "nineideographicparen" => "\u{3228}", + "nineinferior" => "\u{2089}", + "ninemonospace" => "\u{FF19}", + "nineoldstyle" => "\u{F739}", + "nineparen" => "\u{247C}", + "nineperiod" => "\u{2490}", + "ninepersian" => "\u{06F9}", + "nineroman" => "\u{2178}", + "ninesuperior" => "\u{2079}", + "nineteencircle" => "\u{2472}", + "nineteenparen" => "\u{2486}", + "nineteenperiod" => "\u{249A}", + "ninethai" => "\u{0E59}", + "nj" => "\u{01CC}", + "njecyrillic" => "\u{045A}", + "nkatakana" => "\u{30F3}", + "nkatakanahalfwidth" => "\u{FF9D}", + "nlegrightlong" => "\u{019E}", + "nlinebelow" => "\u{1E49}", + "nmonospace" => "\u{FF4E}", + "nmsquare" => "\u{339A}", + "nnabengali" => "\u{09A3}", + "nnadeva" => "\u{0923}", + "nnagujarati" => "\u{0AA3}", + "nnagurmukhi" => "\u{0A23}", + "nnnadeva" => "\u{0929}", + "nohiragana" => "\u{306E}", + "nokatakana" => "\u{30CE}", + "nokatakanahalfwidth" => "\u{FF89}", + "nonbreakingspace" => "\u{00A0}", + "nonenthai" => "\u{0E13}", + "nonuthai" => "\u{0E19}", + "noonarabic" => "\u{0646}", + "noonfinalarabic" => "\u{FEE6}", + "noonghunnaarabic" => "\u{06BA}", + "noonghunnafinalarabic" => "\u{FB9F}", + "noonhehinitialarabic" => "\u{FEE7}\u{FEEC}", + "nooninitialarabic" => "\u{FEE7}", + "noonjeeminitialarabic" => "\u{FCD2}", + "noonjeemisolatedarabic" => "\u{FC4B}", + "noonmedialarabic" => "\u{FEE8}", + "noonmeeminitialarabic" => "\u{FCD5}", + "noonmeemisolatedarabic" => "\u{FC4E}", + "noonnoonfinalarabic" => "\u{FC8D}", + "notcontains" => "\u{220C}", + "notelement" => "\u{2209}", + "notelementof" => "\u{2209}", + "notequal" => "\u{2260}", + "notgreater" => "\u{226F}", + "notgreaternorequal" => "\u{2271}", + "notgreaternorless" => "\u{2279}", + "notidentical" => "\u{2262}", + "notless" => "\u{226E}", + "notlessnorequal" => "\u{2270}", + "notparallel" => "\u{2226}", + "notprecedes" => "\u{2280}", + "notsubset" => "\u{2284}", + "notsucceeds" => "\u{2281}", + "notsuperset" => "\u{2285}", + "nowarmenian" => "\u{0576}", + "nparen" => "\u{24A9}", + "nssquare" => "\u{33B1}", + "nsuperior" => "\u{207F}", + "ntilde" => "\u{00F1}", + "nu" => "\u{03BD}", + "nuhiragana" => "\u{306C}", + "nukatakana" => "\u{30CC}", + "nukatakanahalfwidth" => "\u{FF87}", + "nuktabengali" => "\u{09BC}", + "nuktadeva" => "\u{093C}", + "nuktagujarati" => "\u{0ABC}", + "nuktagurmukhi" => "\u{0A3C}", + "numbersign" => "\u{0023}", + "numbersignmonospace" => "\u{FF03}", + "numbersignsmall" => "\u{FE5F}", + "numeralsigngreek" => "\u{0374}", + "numeralsignlowergreek" => "\u{0375}", + "numero" => "\u{2116}", + "nun" => "\u{05E0}", + "nundagesh" => "\u{FB40}", + "nundageshhebrew" => "\u{FB40}", + "nunhebrew" => "\u{05E0}", + "nvsquare" => "\u{33B5}", + "nwsquare" => "\u{33BB}", + "nyabengali" => "\u{099E}", + "nyadeva" => "\u{091E}", + "nyagujarati" => "\u{0A9E}", + "nyagurmukhi" => "\u{0A1E}", + "o" => "\u{006F}", + "oacute" => "\u{00F3}", + "oangthai" => "\u{0E2D}", + "obarred" => "\u{0275}", + "obarredcyrillic" => "\u{04E9}", + "obarreddieresiscyrillic" => "\u{04EB}", + "obengali" => "\u{0993}", + "obopomofo" => "\u{311B}", + "obreve" => "\u{014F}", + "ocandradeva" => "\u{0911}", + "ocandragujarati" => "\u{0A91}", + "ocandravowelsigndeva" => "\u{0949}", + "ocandravowelsigngujarati" => "\u{0AC9}", + "ocaron" => "\u{01D2}", + "ocircle" => "\u{24DE}", + "ocircumflex" => "\u{00F4}", + "ocircumflexacute" => "\u{1ED1}", + "ocircumflexdotbelow" => "\u{1ED9}", + "ocircumflexgrave" => "\u{1ED3}", + "ocircumflexhookabove" => "\u{1ED5}", + "ocircumflextilde" => "\u{1ED7}", + "ocyrillic" => "\u{043E}", + "odblacute" => "\u{0151}", + "odblgrave" => "\u{020D}", + "odeva" => "\u{0913}", + "odieresis" => "\u{00F6}", + "odieresiscyrillic" => "\u{04E7}", + "odotbelow" => "\u{1ECD}", + "oe" => "\u{0153}", + "oekorean" => "\u{315A}", + "ogonek" => "\u{02DB}", + "ogonekcmb" => "\u{0328}", + "ograve" => "\u{00F2}", + "ogujarati" => "\u{0A93}", + "oharmenian" => "\u{0585}", + "ohiragana" => "\u{304A}", + "ohookabove" => "\u{1ECF}", + "ohorn" => "\u{01A1}", + "ohornacute" => "\u{1EDB}", + "ohorndotbelow" => "\u{1EE3}", + "ohorngrave" => "\u{1EDD}", + "ohornhookabove" => "\u{1EDF}", + "ohorntilde" => "\u{1EE1}", + "ohungarumlaut" => "\u{0151}", + "oi" => "\u{01A3}", + "oinvertedbreve" => "\u{020F}", + "okatakana" => "\u{30AA}", + "okatakanahalfwidth" => "\u{FF75}", + "okorean" => "\u{3157}", + "olehebrew" => "\u{05AB}", + "omacron" => "\u{014D}", + "omacronacute" => "\u{1E53}", + "omacrongrave" => "\u{1E51}", + "omdeva" => "\u{0950}", + "omega" => "\u{03C9}", + "omega1" => "\u{03D6}", + "omegacyrillic" => "\u{0461}", + "omegalatinclosed" => "\u{0277}", + "omegaroundcyrillic" => "\u{047B}", + "omegatitlocyrillic" => "\u{047D}", + "omegatonos" => "\u{03CE}", + "omgujarati" => "\u{0AD0}", + "omicron" => "\u{03BF}", + "omicrontonos" => "\u{03CC}", + "omonospace" => "\u{FF4F}", + "one" => "\u{0031}", + "onearabic" => "\u{0661}", + "onebengali" => "\u{09E7}", + "onecircle" => "\u{2460}", + "onecircleinversesansserif" => "\u{278A}", + "onedeva" => "\u{0967}", + "onedotenleader" => "\u{2024}", + "oneeighth" => "\u{215B}", + "onefitted" => "\u{F6DC}", + "onegujarati" => "\u{0AE7}", + "onegurmukhi" => "\u{0A67}", + "onehackarabic" => "\u{0661}", + "onehalf" => "\u{00BD}", + "onehangzhou" => "\u{3021}", + "oneideographicparen" => "\u{3220}", + "oneinferior" => "\u{2081}", + "onemonospace" => "\u{FF11}", + "onenumeratorbengali" => "\u{09F4}", + "oneoldstyle" => "\u{F731}", + "oneparen" => "\u{2474}", + "oneperiod" => "\u{2488}", + "onepersian" => "\u{06F1}", + "onequarter" => "\u{00BC}", + "oneroman" => "\u{2170}", + "onesuperior" => "\u{00B9}", + "onethai" => "\u{0E51}", + "onethird" => "\u{2153}", + "oogonek" => "\u{01EB}", + "oogonekmacron" => "\u{01ED}", + "oogurmukhi" => "\u{0A13}", + "oomatragurmukhi" => "\u{0A4B}", + "oopen" => "\u{0254}", + "oparen" => "\u{24AA}", + "openbullet" => "\u{25E6}", + "option" => "\u{2325}", + "ordfeminine" => "\u{00AA}", + "ordmasculine" => "\u{00BA}", + "orthogonal" => "\u{221F}", + "oshortdeva" => "\u{0912}", + "oshortvowelsigndeva" => "\u{094A}", + "oslash" => "\u{00F8}", + "oslashacute" => "\u{01FF}", + "osmallhiragana" => "\u{3049}", + "osmallkatakana" => "\u{30A9}", + "osmallkatakanahalfwidth" => "\u{FF6B}", + "ostrokeacute" => "\u{01FF}", + "osuperior" => "\u{F6F0}", + "otcyrillic" => "\u{047F}", + "otilde" => "\u{00F5}", + "otildeacute" => "\u{1E4D}", + "otildedieresis" => "\u{1E4F}", + "oubopomofo" => "\u{3121}", + "overline" => "\u{203E}", + "overlinecenterline" => "\u{FE4A}", + "overlinecmb" => "\u{0305}", + "overlinedashed" => "\u{FE49}", + "overlinedblwavy" => "\u{FE4C}", + "overlinewavy" => "\u{FE4B}", + "overscore" => "\u{00AF}", + "ovowelsignbengali" => "\u{09CB}", + "ovowelsigndeva" => "\u{094B}", + "ovowelsigngujarati" => "\u{0ACB}", + "p" => "\u{0070}", + "paampssquare" => "\u{3380}", + "paasentosquare" => "\u{332B}", + "pabengali" => "\u{09AA}", + "pacute" => "\u{1E55}", + "padeva" => "\u{092A}", + "pagedown" => "\u{21DF}", + "pageup" => "\u{21DE}", + "pagujarati" => "\u{0AAA}", + "pagurmukhi" => "\u{0A2A}", + "pahiragana" => "\u{3071}", + "paiyannoithai" => "\u{0E2F}", + "pakatakana" => "\u{30D1}", + "palatalizationcyrilliccmb" => "\u{0484}", + "palochkacyrillic" => "\u{04C0}", + "pansioskorean" => "\u{317F}", + "paragraph" => "\u{00B6}", + "parallel" => "\u{2225}", + "parenleft" => "\u{0028}", + "parenleftaltonearabic" => "\u{FD3E}", + "parenleftbt" => "\u{F8ED}", + "parenleftex" => "\u{F8EC}", + "parenleftinferior" => "\u{208D}", + "parenleftmonospace" => "\u{FF08}", + "parenleftsmall" => "\u{FE59}", + "parenleftsuperior" => "\u{207D}", + "parenlefttp" => "\u{F8EB}", + "parenleftvertical" => "\u{FE35}", + "parenright" => "\u{0029}", + "parenrightaltonearabic" => "\u{FD3F}", + "parenrightbt" => "\u{F8F8}", + "parenrightex" => "\u{F8F7}", + "parenrightinferior" => "\u{208E}", + "parenrightmonospace" => "\u{FF09}", + "parenrightsmall" => "\u{FE5A}", + "parenrightsuperior" => "\u{207E}", + "parenrighttp" => "\u{F8F6}", + "parenrightvertical" => "\u{FE36}", + "partialdiff" => "\u{2202}", + "paseqhebrew" => "\u{05C0}", + "pashtahebrew" => "\u{0599}", + "pasquare" => "\u{33A9}", + "patah" => "\u{05B7}", + "patah11" => "\u{05B7}", + "patah1d" => "\u{05B7}", + "patah2a" => "\u{05B7}", + "patahhebrew" => "\u{05B7}", + "patahnarrowhebrew" => "\u{05B7}", + "patahquarterhebrew" => "\u{05B7}", + "patahwidehebrew" => "\u{05B7}", + "pazerhebrew" => "\u{05A1}", + "pbopomofo" => "\u{3106}", + "pcircle" => "\u{24DF}", + "pdotaccent" => "\u{1E57}", + "pe" => "\u{05E4}", + "pecyrillic" => "\u{043F}", + "pedagesh" => "\u{FB44}", + "pedageshhebrew" => "\u{FB44}", + "peezisquare" => "\u{333B}", + "pefinaldageshhebrew" => "\u{FB43}", + "peharabic" => "\u{067E}", + "peharmenian" => "\u{057A}", + "pehebrew" => "\u{05E4}", + "pehfinalarabic" => "\u{FB57}", + "pehinitialarabic" => "\u{FB58}", + "pehiragana" => "\u{307A}", + "pehmedialarabic" => "\u{FB59}", + "pekatakana" => "\u{30DA}", + "pemiddlehookcyrillic" => "\u{04A7}", + "perafehebrew" => "\u{FB4E}", + "percent" => "\u{0025}", + "percentarabic" => "\u{066A}", + "percentmonospace" => "\u{FF05}", + "percentsmall" => "\u{FE6A}", + "period" => "\u{002E}", + "periodarmenian" => "\u{0589}", + "periodcentered" => "\u{00B7}", + "periodhalfwidth" => "\u{FF61}", + "periodinferior" => "\u{F6E7}", + "periodmonospace" => "\u{FF0E}", + "periodsmall" => "\u{FE52}", + "periodsuperior" => "\u{F6E8}", + "perispomenigreekcmb" => "\u{0342}", + "perpendicular" => "\u{22A5}", + "perthousand" => "\u{2030}", + "peseta" => "\u{20A7}", + "pfsquare" => "\u{338A}", + "phabengali" => "\u{09AB}", + "phadeva" => "\u{092B}", + "phagujarati" => "\u{0AAB}", + "phagurmukhi" => "\u{0A2B}", + "phi" => "\u{03C6}", + "phi1" => "\u{03D5}", + "phieuphacirclekorean" => "\u{327A}", + "phieuphaparenkorean" => "\u{321A}", + "phieuphcirclekorean" => "\u{326C}", + "phieuphkorean" => "\u{314D}", + "phieuphparenkorean" => "\u{320C}", + "philatin" => "\u{0278}", + "phinthuthai" => "\u{0E3A}", + "phisymbolgreek" => "\u{03D5}", + "phook" => "\u{01A5}", + "phophanthai" => "\u{0E1E}", + "phophungthai" => "\u{0E1C}", + "phosamphaothai" => "\u{0E20}", + "pi" => "\u{03C0}", + "pieupacirclekorean" => "\u{3273}", + "pieupaparenkorean" => "\u{3213}", + "pieupcieuckorean" => "\u{3176}", + "pieupcirclekorean" => "\u{3265}", + "pieupkiyeokkorean" => "\u{3172}", + "pieupkorean" => "\u{3142}", + "pieupparenkorean" => "\u{3205}", + "pieupsioskiyeokkorean" => "\u{3174}", + "pieupsioskorean" => "\u{3144}", + "pieupsiostikeutkorean" => "\u{3175}", + "pieupthieuthkorean" => "\u{3177}", + "pieuptikeutkorean" => "\u{3173}", + "pihiragana" => "\u{3074}", + "pikatakana" => "\u{30D4}", + "pisymbolgreek" => "\u{03D6}", + "piwrarmenian" => "\u{0583}", + "plus" => "\u{002B}", + "plusbelowcmb" => "\u{031F}", + "pluscircle" => "\u{2295}", + "plusminus" => "\u{00B1}", + "plusmod" => "\u{02D6}", + "plusmonospace" => "\u{FF0B}", + "plussmall" => "\u{FE62}", + "plussuperior" => "\u{207A}", + "pmonospace" => "\u{FF50}", + "pmsquare" => "\u{33D8}", + "pohiragana" => "\u{307D}", + "pointingindexdownwhite" => "\u{261F}", + "pointingindexleftwhite" => "\u{261C}", + "pointingindexrightwhite" => "\u{261E}", + "pointingindexupwhite" => "\u{261D}", + "pokatakana" => "\u{30DD}", + "poplathai" => "\u{0E1B}", + "postalmark" => "\u{3012}", + "postalmarkface" => "\u{3020}", + "pparen" => "\u{24AB}", + "precedes" => "\u{227A}", + "prescription" => "\u{211E}", + "primemod" => "\u{02B9}", + "primereversed" => "\u{2035}", + "product" => "\u{220F}", + "projective" => "\u{2305}", + "prolongedkana" => "\u{30FC}", + "propellor" => "\u{2318}", + "propersubset" => "\u{2282}", + "propersuperset" => "\u{2283}", + "proportion" => "\u{2237}", + "proportional" => "\u{221D}", + "psi" => "\u{03C8}", + "psicyrillic" => "\u{0471}", + "psilipneumatacyrilliccmb" => "\u{0486}", + "pssquare" => "\u{33B0}", + "puhiragana" => "\u{3077}", + "pukatakana" => "\u{30D7}", + "pvsquare" => "\u{33B4}", + "pwsquare" => "\u{33BA}", + "q" => "\u{0071}", + "qadeva" => "\u{0958}", + "qadmahebrew" => "\u{05A8}", + "qafarabic" => "\u{0642}", + "qaffinalarabic" => "\u{FED6}", + "qafinitialarabic" => "\u{FED7}", + "qafmedialarabic" => "\u{FED8}", + "qamats" => "\u{05B8}", + "qamats10" => "\u{05B8}", + "qamats1a" => "\u{05B8}", + "qamats1c" => "\u{05B8}", + "qamats27" => "\u{05B8}", + "qamats29" => "\u{05B8}", + "qamats33" => "\u{05B8}", + "qamatsde" => "\u{05B8}", + "qamatshebrew" => "\u{05B8}", + "qamatsnarrowhebrew" => "\u{05B8}", + "qamatsqatanhebrew" => "\u{05B8}", + "qamatsqatannarrowhebrew" => "\u{05B8}", + "qamatsqatanquarterhebrew" => "\u{05B8}", + "qamatsqatanwidehebrew" => "\u{05B8}", + "qamatsquarterhebrew" => "\u{05B8}", + "qamatswidehebrew" => "\u{05B8}", + "qarneyparahebrew" => "\u{059F}", + "qbopomofo" => "\u{3111}", + "qcircle" => "\u{24E0}", + "qhook" => "\u{02A0}", + "qmonospace" => "\u{FF51}", + "qof" => "\u{05E7}", + "qofdagesh" => "\u{FB47}", + "qofdageshhebrew" => "\u{FB47}", + "qofhatafpatah" => "\u{05E7}\u{05B2}", + "qofhatafpatahhebrew" => "\u{05E7}\u{05B2}", + "qofhatafsegol" => "\u{05E7}\u{05B1}", + "qofhatafsegolhebrew" => "\u{05E7}\u{05B1}", + "qofhebrew" => "\u{05E7}", + "qofhiriq" => "\u{05E7}\u{05B4}", + "qofhiriqhebrew" => "\u{05E7}\u{05B4}", + "qofholam" => "\u{05E7}\u{05B9}", + "qofholamhebrew" => "\u{05E7}\u{05B9}", + "qofpatah" => "\u{05E7}\u{05B7}", + "qofpatahhebrew" => "\u{05E7}\u{05B7}", + "qofqamats" => "\u{05E7}\u{05B8}", + "qofqamatshebrew" => "\u{05E7}\u{05B8}", + "qofqubuts" => "\u{05E7}\u{05BB}", + "qofqubutshebrew" => "\u{05E7}\u{05BB}", + "qofsegol" => "\u{05E7}\u{05B6}", + "qofsegolhebrew" => "\u{05E7}\u{05B6}", + "qofsheva" => "\u{05E7}\u{05B0}", + "qofshevahebrew" => "\u{05E7}\u{05B0}", + "qoftsere" => "\u{05E7}\u{05B5}", + "qoftserehebrew" => "\u{05E7}\u{05B5}", + "qparen" => "\u{24AC}", + "quarternote" => "\u{2669}", + "qubuts" => "\u{05BB}", + "qubuts18" => "\u{05BB}", + "qubuts25" => "\u{05BB}", + "qubuts31" => "\u{05BB}", + "qubutshebrew" => "\u{05BB}", + "qubutsnarrowhebrew" => "\u{05BB}", + "qubutsquarterhebrew" => "\u{05BB}", + "qubutswidehebrew" => "\u{05BB}", + "question" => "\u{003F}", + "questionarabic" => "\u{061F}", + "questionarmenian" => "\u{055E}", + "questiondown" => "\u{00BF}", + "questiondownsmall" => "\u{F7BF}", + "questiongreek" => "\u{037E}", + "questionmonospace" => "\u{FF1F}", + "questionsmall" => "\u{F73F}", + "quotedbl" => "\u{0022}", + "quotedblbase" => "\u{201E}", + "quotedblleft" => "\u{201C}", + "quotedblmonospace" => "\u{FF02}", + "quotedblprime" => "\u{301E}", + "quotedblprimereversed" => "\u{301D}", + "quotedblright" => "\u{201D}", + "quoteleft" => "\u{2018}", + "quoteleftreversed" => "\u{201B}", + "quotereversed" => "\u{201B}", + "quoteright" => "\u{2019}", + "quoterightn" => "\u{0149}", + "quotesinglbase" => "\u{201A}", + "quotesingle" => "\u{0027}", + "quotesinglemonospace" => "\u{FF07}", + "r" => "\u{0072}", + "raarmenian" => "\u{057C}", + "rabengali" => "\u{09B0}", + "racute" => "\u{0155}", + "radeva" => "\u{0930}", + "radical" => "\u{221A}", + "radicalex" => "\u{F8E5}", + "radoverssquare" => "\u{33AE}", + "radoverssquaredsquare" => "\u{33AF}", + "radsquare" => "\u{33AD}", + "rafe" => "\u{05BF}", + "rafehebrew" => "\u{05BF}", + "ragujarati" => "\u{0AB0}", + "ragurmukhi" => "\u{0A30}", + "rahiragana" => "\u{3089}", + "rakatakana" => "\u{30E9}", + "rakatakanahalfwidth" => "\u{FF97}", + "ralowerdiagonalbengali" => "\u{09F1}", + "ramiddlediagonalbengali" => "\u{09F0}", + "ramshorn" => "\u{0264}", + "ratio" => "\u{2236}", + "rbopomofo" => "\u{3116}", + "rcaron" => "\u{0159}", + "rcedilla" => "\u{0157}", + "rcircle" => "\u{24E1}", + "rcommaaccent" => "\u{0157}", + "rdblgrave" => "\u{0211}", + "rdotaccent" => "\u{1E59}", + "rdotbelow" => "\u{1E5B}", + "rdotbelowmacron" => "\u{1E5D}", + "referencemark" => "\u{203B}", + "reflexsubset" => "\u{2286}", + "reflexsuperset" => "\u{2287}", + "registered" => "\u{00AE}", + "registersans" => "\u{F8E8}", + "registerserif" => "\u{F6DA}", + "reharabic" => "\u{0631}", + "reharmenian" => "\u{0580}", + "rehfinalarabic" => "\u{FEAE}", + "rehiragana" => "\u{308C}", + "rehyehaleflamarabic" => "\u{0631}\u{FEF3}\u{FE8E}\u{0644}", + "rekatakana" => "\u{30EC}", + "rekatakanahalfwidth" => "\u{FF9A}", + "resh" => "\u{05E8}", + "reshdageshhebrew" => "\u{FB48}", + "reshhatafpatah" => "\u{05E8}\u{05B2}", + "reshhatafpatahhebrew" => "\u{05E8}\u{05B2}", + "reshhatafsegol" => "\u{05E8}\u{05B1}", + "reshhatafsegolhebrew" => "\u{05E8}\u{05B1}", + "reshhebrew" => "\u{05E8}", + "reshhiriq" => "\u{05E8}\u{05B4}", + "reshhiriqhebrew" => "\u{05E8}\u{05B4}", + "reshholam" => "\u{05E8}\u{05B9}", + "reshholamhebrew" => "\u{05E8}\u{05B9}", + "reshpatah" => "\u{05E8}\u{05B7}", + "reshpatahhebrew" => "\u{05E8}\u{05B7}", + "reshqamats" => "\u{05E8}\u{05B8}", + "reshqamatshebrew" => "\u{05E8}\u{05B8}", + "reshqubuts" => "\u{05E8}\u{05BB}", + "reshqubutshebrew" => "\u{05E8}\u{05BB}", + "reshsegol" => "\u{05E8}\u{05B6}", + "reshsegolhebrew" => "\u{05E8}\u{05B6}", + "reshsheva" => "\u{05E8}\u{05B0}", + "reshshevahebrew" => "\u{05E8}\u{05B0}", + "reshtsere" => "\u{05E8}\u{05B5}", + "reshtserehebrew" => "\u{05E8}\u{05B5}", + "reversedtilde" => "\u{223D}", + "reviahebrew" => "\u{0597}", + "reviamugrashhebrew" => "\u{0597}", + "revlogicalnot" => "\u{2310}", + "rfishhook" => "\u{027E}", + "rfishhookreversed" => "\u{027F}", + "rhabengali" => "\u{09DD}", + "rhadeva" => "\u{095D}", + "rho" => "\u{03C1}", + "rhook" => "\u{027D}", + "rhookturned" => "\u{027B}", + "rhookturnedsuperior" => "\u{02B5}", + "rhosymbolgreek" => "\u{03F1}", + "rhotichookmod" => "\u{02DE}", + "rieulacirclekorean" => "\u{3271}", + "rieulaparenkorean" => "\u{3211}", + "rieulcirclekorean" => "\u{3263}", + "rieulhieuhkorean" => "\u{3140}", + "rieulkiyeokkorean" => "\u{313A}", + "rieulkiyeoksioskorean" => "\u{3169}", + "rieulkorean" => "\u{3139}", + "rieulmieumkorean" => "\u{313B}", + "rieulpansioskorean" => "\u{316C}", + "rieulparenkorean" => "\u{3203}", + "rieulphieuphkorean" => "\u{313F}", + "rieulpieupkorean" => "\u{313C}", + "rieulpieupsioskorean" => "\u{316B}", + "rieulsioskorean" => "\u{313D}", + "rieulthieuthkorean" => "\u{313E}", + "rieultikeutkorean" => "\u{316A}", + "rieulyeorinhieuhkorean" => "\u{316D}", + "rightangle" => "\u{221F}", + "righttackbelowcmb" => "\u{0319}", + "righttriangle" => "\u{22BF}", + "rihiragana" => "\u{308A}", + "rikatakana" => "\u{30EA}", + "rikatakanahalfwidth" => "\u{FF98}", + "ring" => "\u{02DA}", + "ringbelowcmb" => "\u{0325}", + "ringcmb" => "\u{030A}", + "ringhalfleft" => "\u{02BF}", + "ringhalfleftarmenian" => "\u{0559}", + "ringhalfleftbelowcmb" => "\u{031C}", + "ringhalfleftcentered" => "\u{02D3}", + "ringhalfright" => "\u{02BE}", + "ringhalfrightbelowcmb" => "\u{0339}", + "ringhalfrightcentered" => "\u{02D2}", + "rinvertedbreve" => "\u{0213}", + "rittorusquare" => "\u{3351}", + "rlinebelow" => "\u{1E5F}", + "rlongleg" => "\u{027C}", + "rlonglegturned" => "\u{027A}", + "rmonospace" => "\u{FF52}", + "rohiragana" => "\u{308D}", + "rokatakana" => "\u{30ED}", + "rokatakanahalfwidth" => "\u{FF9B}", + "roruathai" => "\u{0E23}", + "rparen" => "\u{24AD}", + "rrabengali" => "\u{09DC}", + "rradeva" => "\u{0931}", + "rragurmukhi" => "\u{0A5C}", + "rreharabic" => "\u{0691}", + "rrehfinalarabic" => "\u{FB8D}", + "rrvocalicbengali" => "\u{09E0}", + "rrvocalicdeva" => "\u{0960}", + "rrvocalicgujarati" => "\u{0AE0}", + "rrvocalicvowelsignbengali" => "\u{09C4}", + "rrvocalicvowelsigndeva" => "\u{0944}", + "rrvocalicvowelsigngujarati" => "\u{0AC4}", + "rsuperior" => "\u{F6F1}", + "rtblock" => "\u{2590}", + "rturned" => "\u{0279}", + "rturnedsuperior" => "\u{02B4}", + "ruhiragana" => "\u{308B}", + "rukatakana" => "\u{30EB}", + "rukatakanahalfwidth" => "\u{FF99}", + "rupeemarkbengali" => "\u{09F2}", + "rupeesignbengali" => "\u{09F3}", + "rupiah" => "\u{F6DD}", + "ruthai" => "\u{0E24}", + "rvocalicbengali" => "\u{098B}", + "rvocalicdeva" => "\u{090B}", + "rvocalicgujarati" => "\u{0A8B}", + "rvocalicvowelsignbengali" => "\u{09C3}", + "rvocalicvowelsigndeva" => "\u{0943}", + "rvocalicvowelsigngujarati" => "\u{0AC3}", + "s" => "\u{0073}", + "sabengali" => "\u{09B8}", + "sacute" => "\u{015B}", + "sacutedotaccent" => "\u{1E65}", + "sadarabic" => "\u{0635}", + "sadeva" => "\u{0938}", + "sadfinalarabic" => "\u{FEBA}", + "sadinitialarabic" => "\u{FEBB}", + "sadmedialarabic" => "\u{FEBC}", + "sagujarati" => "\u{0AB8}", + "sagurmukhi" => "\u{0A38}", + "sahiragana" => "\u{3055}", + "sakatakana" => "\u{30B5}", + "sakatakanahalfwidth" => "\u{FF7B}", + "sallallahoualayhewasallamarabic" => "\u{FDFA}", + "samekh" => "\u{05E1}", + "samekhdagesh" => "\u{FB41}", + "samekhdageshhebrew" => "\u{FB41}", + "samekhhebrew" => "\u{05E1}", + "saraaathai" => "\u{0E32}", + "saraaethai" => "\u{0E41}", + "saraaimaimalaithai" => "\u{0E44}", + "saraaimaimuanthai" => "\u{0E43}", + "saraamthai" => "\u{0E33}", + "saraathai" => "\u{0E30}", + "saraethai" => "\u{0E40}", + "saraiileftthai" => "\u{F886}", + "saraiithai" => "\u{0E35}", + "saraileftthai" => "\u{F885}", + "saraithai" => "\u{0E34}", + "saraothai" => "\u{0E42}", + "saraueeleftthai" => "\u{F888}", + "saraueethai" => "\u{0E37}", + "saraueleftthai" => "\u{F887}", + "sarauethai" => "\u{0E36}", + "sarauthai" => "\u{0E38}", + "sarauuthai" => "\u{0E39}", + "sbopomofo" => "\u{3119}", + "scaron" => "\u{0161}", + "scarondotaccent" => "\u{1E67}", + "scedilla" => "\u{015F}", + "schwa" => "\u{0259}", + "schwacyrillic" => "\u{04D9}", + "schwadieresiscyrillic" => "\u{04DB}", + "schwahook" => "\u{025A}", + "scircle" => "\u{24E2}", + "scircumflex" => "\u{015D}", + "scommaaccent" => "\u{0219}", + "sdotaccent" => "\u{1E61}", + "sdotbelow" => "\u{1E63}", + "sdotbelowdotaccent" => "\u{1E69}", + "seagullbelowcmb" => "\u{033C}", + "second" => "\u{2033}", + "secondtonechinese" => "\u{02CA}", + "section" => "\u{00A7}", + "seenarabic" => "\u{0633}", + "seenfinalarabic" => "\u{FEB2}", + "seeninitialarabic" => "\u{FEB3}", + "seenmedialarabic" => "\u{FEB4}", + "segol" => "\u{05B6}", + "segol13" => "\u{05B6}", + "segol1f" => "\u{05B6}", + "segol2c" => "\u{05B6}", + "segolhebrew" => "\u{05B6}", + "segolnarrowhebrew" => "\u{05B6}", + "segolquarterhebrew" => "\u{05B6}", + "segoltahebrew" => "\u{0592}", + "segolwidehebrew" => "\u{05B6}", + "seharmenian" => "\u{057D}", + "sehiragana" => "\u{305B}", + "sekatakana" => "\u{30BB}", + "sekatakanahalfwidth" => "\u{FF7E}", + "semicolon" => "\u{003B}", + "semicolonarabic" => "\u{061B}", + "semicolonmonospace" => "\u{FF1B}", + "semicolonsmall" => "\u{FE54}", + "semivoicedmarkkana" => "\u{309C}", + "semivoicedmarkkanahalfwidth" => "\u{FF9F}", + "sentisquare" => "\u{3322}", + "sentosquare" => "\u{3323}", + "seven" => "\u{0037}", + "sevenarabic" => "\u{0667}", + "sevenbengali" => "\u{09ED}", + "sevencircle" => "\u{2466}", + "sevencircleinversesansserif" => "\u{2790}", + "sevendeva" => "\u{096D}", + "seveneighths" => "\u{215E}", + "sevengujarati" => "\u{0AED}", + "sevengurmukhi" => "\u{0A6D}", + "sevenhackarabic" => "\u{0667}", + "sevenhangzhou" => "\u{3027}", + "sevenideographicparen" => "\u{3226}", + "seveninferior" => "\u{2087}", + "sevenmonospace" => "\u{FF17}", + "sevenoldstyle" => "\u{F737}", + "sevenparen" => "\u{247A}", + "sevenperiod" => "\u{248E}", + "sevenpersian" => "\u{06F7}", + "sevenroman" => "\u{2176}", + "sevensuperior" => "\u{2077}", + "seventeencircle" => "\u{2470}", + "seventeenparen" => "\u{2484}", + "seventeenperiod" => "\u{2498}", + "seventhai" => "\u{0E57}", + "sfthyphen" => "\u{00AD}", + "shaarmenian" => "\u{0577}", + "shabengali" => "\u{09B6}", + "shacyrillic" => "\u{0448}", + "shaddaarabic" => "\u{0651}", + "shaddadammaarabic" => "\u{FC61}", + "shaddadammatanarabic" => "\u{FC5E}", + "shaddafathaarabic" => "\u{FC60}", + "shaddafathatanarabic" => "\u{0651}\u{064B}", + "shaddakasraarabic" => "\u{FC62}", + "shaddakasratanarabic" => "\u{FC5F}", + "shade" => "\u{2592}", + "shadedark" => "\u{2593}", + "shadelight" => "\u{2591}", + "shademedium" => "\u{2592}", + "shadeva" => "\u{0936}", + "shagujarati" => "\u{0AB6}", + "shagurmukhi" => "\u{0A36}", + "shalshelethebrew" => "\u{0593}", + "shbopomofo" => "\u{3115}", + "shchacyrillic" => "\u{0449}", + "sheenarabic" => "\u{0634}", + "sheenfinalarabic" => "\u{FEB6}", + "sheeninitialarabic" => "\u{FEB7}", + "sheenmedialarabic" => "\u{FEB8}", + "sheicoptic" => "\u{03E3}", + "sheqel" => "\u{20AA}", + "sheqelhebrew" => "\u{20AA}", + "sheva" => "\u{05B0}", + "sheva115" => "\u{05B0}", + "sheva15" => "\u{05B0}", + "sheva22" => "\u{05B0}", + "sheva2e" => "\u{05B0}", + "shevahebrew" => "\u{05B0}", + "shevanarrowhebrew" => "\u{05B0}", + "shevaquarterhebrew" => "\u{05B0}", + "shevawidehebrew" => "\u{05B0}", + "shhacyrillic" => "\u{04BB}", + "shimacoptic" => "\u{03ED}", + "shin" => "\u{05E9}", + "shindagesh" => "\u{FB49}", + "shindageshhebrew" => "\u{FB49}", + "shindageshshindot" => "\u{FB2C}", + "shindageshshindothebrew" => "\u{FB2C}", + "shindageshsindot" => "\u{FB2D}", + "shindageshsindothebrew" => "\u{FB2D}", + "shindothebrew" => "\u{05C1}", + "shinhebrew" => "\u{05E9}", + "shinshindot" => "\u{FB2A}", + "shinshindothebrew" => "\u{FB2A}", + "shinsindot" => "\u{FB2B}", + "shinsindothebrew" => "\u{FB2B}", + "shook" => "\u{0282}", + "sigma" => "\u{03C3}", + "sigma1" => "\u{03C2}", + "sigmafinal" => "\u{03C2}", + "sigmalunatesymbolgreek" => "\u{03F2}", + "sihiragana" => "\u{3057}", + "sikatakana" => "\u{30B7}", + "sikatakanahalfwidth" => "\u{FF7C}", + "siluqhebrew" => "\u{05BD}", + "siluqlefthebrew" => "\u{05BD}", + "similar" => "\u{223C}", + "sindothebrew" => "\u{05C2}", + "siosacirclekorean" => "\u{3274}", + "siosaparenkorean" => "\u{3214}", + "sioscieuckorean" => "\u{317E}", + "sioscirclekorean" => "\u{3266}", + "sioskiyeokkorean" => "\u{317A}", + "sioskorean" => "\u{3145}", + "siosnieunkorean" => "\u{317B}", + "siosparenkorean" => "\u{3206}", + "siospieupkorean" => "\u{317D}", + "siostikeutkorean" => "\u{317C}", + "six" => "\u{0036}", + "sixarabic" => "\u{0666}", + "sixbengali" => "\u{09EC}", + "sixcircle" => "\u{2465}", + "sixcircleinversesansserif" => "\u{278F}", + "sixdeva" => "\u{096C}", + "sixgujarati" => "\u{0AEC}", + "sixgurmukhi" => "\u{0A6C}", + "sixhackarabic" => "\u{0666}", + "sixhangzhou" => "\u{3026}", + "sixideographicparen" => "\u{3225}", + "sixinferior" => "\u{2086}", + "sixmonospace" => "\u{FF16}", + "sixoldstyle" => "\u{F736}", + "sixparen" => "\u{2479}", + "sixperiod" => "\u{248D}", + "sixpersian" => "\u{06F6}", + "sixroman" => "\u{2175}", + "sixsuperior" => "\u{2076}", + "sixteencircle" => "\u{246F}", + "sixteencurrencydenominatorbengali" => "\u{09F9}", + "sixteenparen" => "\u{2483}", + "sixteenperiod" => "\u{2497}", + "sixthai" => "\u{0E56}", + "slash" => "\u{002F}", + "slashmonospace" => "\u{FF0F}", + "slong" => "\u{017F}", + "slongdotaccent" => "\u{1E9B}", + "smileface" => "\u{263A}", + "smonospace" => "\u{FF53}", + "sofpasuqhebrew" => "\u{05C3}", + "softhyphen" => "\u{00AD}", + "softsigncyrillic" => "\u{044C}", + "sohiragana" => "\u{305D}", + "sokatakana" => "\u{30BD}", + "sokatakanahalfwidth" => "\u{FF7F}", + "soliduslongoverlaycmb" => "\u{0338}", + "solidusshortoverlaycmb" => "\u{0337}", + "sorusithai" => "\u{0E29}", + "sosalathai" => "\u{0E28}", + "sosothai" => "\u{0E0B}", + "sosuathai" => "\u{0E2A}", + "space" => "\u{0020}", + "spacehackarabic" => "\u{0020}", + "spade" => "\u{2660}", + "spadesuitblack" => "\u{2660}", + "spadesuitwhite" => "\u{2664}", + "sparen" => "\u{24AE}", + "squarebelowcmb" => "\u{033B}", + "squarecc" => "\u{33C4}", + "squarecm" => "\u{339D}", + "squarediagonalcrosshatchfill" => "\u{25A9}", + "squarehorizontalfill" => "\u{25A4}", + "squarekg" => "\u{338F}", + "squarekm" => "\u{339E}", + "squarekmcapital" => "\u{33CE}", + "squareln" => "\u{33D1}", + "squarelog" => "\u{33D2}", + "squaremg" => "\u{338E}", + "squaremil" => "\u{33D5}", + "squaremm" => "\u{339C}", + "squaremsquared" => "\u{33A1}", + "squareorthogonalcrosshatchfill" => "\u{25A6}", + "squareupperlefttolowerrightfill" => "\u{25A7}", + "squareupperrighttolowerleftfill" => "\u{25A8}", + "squareverticalfill" => "\u{25A5}", + "squarewhitewithsmallblack" => "\u{25A3}", + "srsquare" => "\u{33DB}", + "ssabengali" => "\u{09B7}", + "ssadeva" => "\u{0937}", + "ssagujarati" => "\u{0AB7}", + "ssangcieuckorean" => "\u{3149}", + "ssanghieuhkorean" => "\u{3185}", + "ssangieungkorean" => "\u{3180}", + "ssangkiyeokkorean" => "\u{3132}", + "ssangnieunkorean" => "\u{3165}", + "ssangpieupkorean" => "\u{3143}", + "ssangsioskorean" => "\u{3146}", + "ssangtikeutkorean" => "\u{3138}", + "ssuperior" => "\u{F6F2}", + "sterling" => "\u{00A3}", + "sterlingmonospace" => "\u{FFE1}", + "strokelongoverlaycmb" => "\u{0336}", + "strokeshortoverlaycmb" => "\u{0335}", + "subset" => "\u{2282}", + "subsetnotequal" => "\u{228A}", + "subsetorequal" => "\u{2286}", + "succeeds" => "\u{227B}", + "suchthat" => "\u{220B}", + "suhiragana" => "\u{3059}", + "sukatakana" => "\u{30B9}", + "sukatakanahalfwidth" => "\u{FF7D}", + "sukunarabic" => "\u{0652}", + "summation" => "\u{2211}", + "sun" => "\u{263C}", + "superset" => "\u{2283}", + "supersetnotequal" => "\u{228B}", + "supersetorequal" => "\u{2287}", + "svsquare" => "\u{33DC}", + "syouwaerasquare" => "\u{337C}", + "t" => "\u{0074}", + "tabengali" => "\u{09A4}", + "tackdown" => "\u{22A4}", + "tackleft" => "\u{22A3}", + "tadeva" => "\u{0924}", + "tagujarati" => "\u{0AA4}", + "tagurmukhi" => "\u{0A24}", + "taharabic" => "\u{0637}", + "tahfinalarabic" => "\u{FEC2}", + "tahinitialarabic" => "\u{FEC3}", + "tahiragana" => "\u{305F}", + "tahmedialarabic" => "\u{FEC4}", + "taisyouerasquare" => "\u{337D}", + "takatakana" => "\u{30BF}", + "takatakanahalfwidth" => "\u{FF80}", + "tatweelarabic" => "\u{0640}", + "tau" => "\u{03C4}", + "tav" => "\u{05EA}", + "tavdages" => "\u{FB4A}", + "tavdagesh" => "\u{FB4A}", + "tavdageshhebrew" => "\u{FB4A}", + "tavhebrew" => "\u{05EA}", + "tbar" => "\u{0167}", + "tbopomofo" => "\u{310A}", + "tcaron" => "\u{0165}", + "tccurl" => "\u{02A8}", + "tcedilla" => "\u{0163}", + "tcheharabic" => "\u{0686}", + "tchehfinalarabic" => "\u{FB7B}", + "tchehinitialarabic" => "\u{FB7C}", + "tchehmedialarabic" => "\u{FB7D}", + "tchehmeeminitialarabic" => "\u{FB7C}\u{FEE4}", + "tcircle" => "\u{24E3}", + "tcircumflexbelow" => "\u{1E71}", + "tcommaaccent" => "\u{0163}", + "tdieresis" => "\u{1E97}", + "tdotaccent" => "\u{1E6B}", + "tdotbelow" => "\u{1E6D}", + "tecyrillic" => "\u{0442}", + "tedescendercyrillic" => "\u{04AD}", + "teharabic" => "\u{062A}", + "tehfinalarabic" => "\u{FE96}", + "tehhahinitialarabic" => "\u{FCA2}", + "tehhahisolatedarabic" => "\u{FC0C}", + "tehinitialarabic" => "\u{FE97}", + "tehiragana" => "\u{3066}", + "tehjeeminitialarabic" => "\u{FCA1}", + "tehjeemisolatedarabic" => "\u{FC0B}", + "tehmarbutaarabic" => "\u{0629}", + "tehmarbutafinalarabic" => "\u{FE94}", + "tehmedialarabic" => "\u{FE98}", + "tehmeeminitialarabic" => "\u{FCA4}", + "tehmeemisolatedarabic" => "\u{FC0E}", + "tehnoonfinalarabic" => "\u{FC73}", + "tekatakana" => "\u{30C6}", + "tekatakanahalfwidth" => "\u{FF83}", + "telephone" => "\u{2121}", + "telephoneblack" => "\u{260E}", + "telishagedolahebrew" => "\u{05A0}", + "telishaqetanahebrew" => "\u{05A9}", + "tencircle" => "\u{2469}", + "tenideographicparen" => "\u{3229}", + "tenparen" => "\u{247D}", + "tenperiod" => "\u{2491}", + "tenroman" => "\u{2179}", + "tesh" => "\u{02A7}", + "tet" => "\u{05D8}", + "tetdagesh" => "\u{FB38}", + "tetdageshhebrew" => "\u{FB38}", + "tethebrew" => "\u{05D8}", + "tetsecyrillic" => "\u{04B5}", + "tevirhebrew" => "\u{059B}", + "tevirlefthebrew" => "\u{059B}", + "thabengali" => "\u{09A5}", + "thadeva" => "\u{0925}", + "thagujarati" => "\u{0AA5}", + "thagurmukhi" => "\u{0A25}", + "thalarabic" => "\u{0630}", + "thalfinalarabic" => "\u{FEAC}", + "thanthakhatlowleftthai" => "\u{F898}", + "thanthakhatlowrightthai" => "\u{F897}", + "thanthakhatthai" => "\u{0E4C}", + "thanthakhatupperleftthai" => "\u{F896}", + "theharabic" => "\u{062B}", + "thehfinalarabic" => "\u{FE9A}", + "thehinitialarabic" => "\u{FE9B}", + "thehmedialarabic" => "\u{FE9C}", + "thereexists" => "\u{2203}", + "therefore" => "\u{2234}", + "theta" => "\u{03B8}", + "theta1" => "\u{03D1}", + "thetasymbolgreek" => "\u{03D1}", + "thieuthacirclekorean" => "\u{3279}", + "thieuthaparenkorean" => "\u{3219}", + "thieuthcirclekorean" => "\u{326B}", + "thieuthkorean" => "\u{314C}", + "thieuthparenkorean" => "\u{320B}", + "thirteencircle" => "\u{246C}", + "thirteenparen" => "\u{2480}", + "thirteenperiod" => "\u{2494}", + "thonangmonthothai" => "\u{0E11}", + "thook" => "\u{01AD}", + "thophuthaothai" => "\u{0E12}", + "thorn" => "\u{00FE}", + "thothahanthai" => "\u{0E17}", + "thothanthai" => "\u{0E10}", + "thothongthai" => "\u{0E18}", + "thothungthai" => "\u{0E16}", + "thousandcyrillic" => "\u{0482}", + "thousandsseparatorarabic" => "\u{066C}", + "thousandsseparatorpersian" => "\u{066C}", + "three" => "\u{0033}", + "threearabic" => "\u{0663}", + "threebengali" => "\u{09E9}", + "threecircle" => "\u{2462}", + "threecircleinversesansserif" => "\u{278C}", + "threedeva" => "\u{0969}", + "threeeighths" => "\u{215C}", + "threegujarati" => "\u{0AE9}", + "threegurmukhi" => "\u{0A69}", + "threehackarabic" => "\u{0663}", + "threehangzhou" => "\u{3023}", + "threeideographicparen" => "\u{3222}", + "threeinferior" => "\u{2083}", + "threemonospace" => "\u{FF13}", + "threenumeratorbengali" => "\u{09F6}", + "threeoldstyle" => "\u{F733}", + "threeparen" => "\u{2476}", + "threeperiod" => "\u{248A}", + "threepersian" => "\u{06F3}", + "threequarters" => "\u{00BE}", + "threequartersemdash" => "\u{F6DE}", + "threeroman" => "\u{2172}", + "threesuperior" => "\u{00B3}", + "threethai" => "\u{0E53}", + "thzsquare" => "\u{3394}", + "tihiragana" => "\u{3061}", + "tikatakana" => "\u{30C1}", + "tikatakanahalfwidth" => "\u{FF81}", + "tikeutacirclekorean" => "\u{3270}", + "tikeutaparenkorean" => "\u{3210}", + "tikeutcirclekorean" => "\u{3262}", + "tikeutkorean" => "\u{3137}", + "tikeutparenkorean" => "\u{3202}", + "tilde" => "\u{02DC}", + "tildebelowcmb" => "\u{0330}", + "tildecmb" => "\u{0303}", + "tildecomb" => "\u{0303}", + "tildedoublecmb" => "\u{0360}", + "tildeoperator" => "\u{223C}", + "tildeoverlaycmb" => "\u{0334}", + "tildeverticalcmb" => "\u{033E}", + "timescircle" => "\u{2297}", + "tipehahebrew" => "\u{0596}", + "tipehalefthebrew" => "\u{0596}", + "tippigurmukhi" => "\u{0A70}", + "titlocyrilliccmb" => "\u{0483}", + "tiwnarmenian" => "\u{057F}", + "tlinebelow" => "\u{1E6F}", + "tmonospace" => "\u{FF54}", + "toarmenian" => "\u{0569}", + "tohiragana" => "\u{3068}", + "tokatakana" => "\u{30C8}", + "tokatakanahalfwidth" => "\u{FF84}", + "tonebarextrahighmod" => "\u{02E5}", + "tonebarextralowmod" => "\u{02E9}", + "tonebarhighmod" => "\u{02E6}", + "tonebarlowmod" => "\u{02E8}", + "tonebarmidmod" => "\u{02E7}", + "tonefive" => "\u{01BD}", + "tonesix" => "\u{0185}", + "tonetwo" => "\u{01A8}", + "tonos" => "\u{0384}", + "tonsquare" => "\u{3327}", + "topatakthai" => "\u{0E0F}", + "tortoiseshellbracketleft" => "\u{3014}", + "tortoiseshellbracketleftsmall" => "\u{FE5D}", + "tortoiseshellbracketleftvertical" => "\u{FE39}", + "tortoiseshellbracketright" => "\u{3015}", + "tortoiseshellbracketrightsmall" => "\u{FE5E}", + "tortoiseshellbracketrightvertical" => "\u{FE3A}", + "totaothai" => "\u{0E15}", + "tpalatalhook" => "\u{01AB}", + "tparen" => "\u{24AF}", + "trademark" => "\u{2122}", + "trademarksans" => "\u{F8EA}", + "trademarkserif" => "\u{F6DB}", + "tretroflexhook" => "\u{0288}", + "triagdn" => "\u{25BC}", + "triaglf" => "\u{25C4}", + "triagrt" => "\u{25BA}", + "triagup" => "\u{25B2}", + "ts" => "\u{02A6}", + "tsadi" => "\u{05E6}", + "tsadidagesh" => "\u{FB46}", + "tsadidageshhebrew" => "\u{FB46}", + "tsadihebrew" => "\u{05E6}", + "tsecyrillic" => "\u{0446}", + "tsere" => "\u{05B5}", + "tsere12" => "\u{05B5}", + "tsere1e" => "\u{05B5}", + "tsere2b" => "\u{05B5}", + "tserehebrew" => "\u{05B5}", + "tserenarrowhebrew" => "\u{05B5}", + "tserequarterhebrew" => "\u{05B5}", + "tserewidehebrew" => "\u{05B5}", + "tshecyrillic" => "\u{045B}", + "tsuperior" => "\u{F6F3}", + "ttabengali" => "\u{099F}", + "ttadeva" => "\u{091F}", + "ttagujarati" => "\u{0A9F}", + "ttagurmukhi" => "\u{0A1F}", + "tteharabic" => "\u{0679}", + "ttehfinalarabic" => "\u{FB67}", + "ttehinitialarabic" => "\u{FB68}", + "ttehmedialarabic" => "\u{FB69}", + "tthabengali" => "\u{09A0}", + "tthadeva" => "\u{0920}", + "tthagujarati" => "\u{0AA0}", + "tthagurmukhi" => "\u{0A20}", + "tturned" => "\u{0287}", + "tuhiragana" => "\u{3064}", + "tukatakana" => "\u{30C4}", + "tukatakanahalfwidth" => "\u{FF82}", + "tusmallhiragana" => "\u{3063}", + "tusmallkatakana" => "\u{30C3}", + "tusmallkatakanahalfwidth" => "\u{FF6F}", + "twelvecircle" => "\u{246B}", + "twelveparen" => "\u{247F}", + "twelveperiod" => "\u{2493}", + "twelveroman" => "\u{217B}", + "twentycircle" => "\u{2473}", + "twentyhangzhou" => "\u{5344}", + "twentyparen" => "\u{2487}", + "twentyperiod" => "\u{249B}", + "two" => "\u{0032}", + "twoarabic" => "\u{0662}", + "twobengali" => "\u{09E8}", + "twocircle" => "\u{2461}", + "twocircleinversesansserif" => "\u{278B}", + "twodeva" => "\u{0968}", + "twodotenleader" => "\u{2025}", + "twodotleader" => "\u{2025}", + "twodotleadervertical" => "\u{FE30}", + "twogujarati" => "\u{0AE8}", + "twogurmukhi" => "\u{0A68}", + "twohackarabic" => "\u{0662}", + "twohangzhou" => "\u{3022}", + "twoideographicparen" => "\u{3221}", + "twoinferior" => "\u{2082}", + "twomonospace" => "\u{FF12}", + "twonumeratorbengali" => "\u{09F5}", + "twooldstyle" => "\u{F732}", + "twoparen" => "\u{2475}", + "twoperiod" => "\u{2489}", + "twopersian" => "\u{06F2}", + "tworoman" => "\u{2171}", + "twostroke" => "\u{01BB}", + "twosuperior" => "\u{00B2}", + "twothai" => "\u{0E52}", + "twothirds" => "\u{2154}", + "u" => "\u{0075}", + "uacute" => "\u{00FA}", + "ubar" => "\u{0289}", + "ubengali" => "\u{0989}", + "ubopomofo" => "\u{3128}", + "ubreve" => "\u{016D}", + "ucaron" => "\u{01D4}", + "ucircle" => "\u{24E4}", + "ucircumflex" => "\u{00FB}", + "ucircumflexbelow" => "\u{1E77}", + "ucyrillic" => "\u{0443}", + "udattadeva" => "\u{0951}", + "udblacute" => "\u{0171}", + "udblgrave" => "\u{0215}", + "udeva" => "\u{0909}", + "udieresis" => "\u{00FC}", + "udieresisacute" => "\u{01D8}", + "udieresisbelow" => "\u{1E73}", + "udieresiscaron" => "\u{01DA}", + "udieresiscyrillic" => "\u{04F1}", + "udieresisgrave" => "\u{01DC}", + "udieresismacron" => "\u{01D6}", + "udotbelow" => "\u{1EE5}", + "ugrave" => "\u{00F9}", + "ugujarati" => "\u{0A89}", + "ugurmukhi" => "\u{0A09}", + "uhiragana" => "\u{3046}", + "uhookabove" => "\u{1EE7}", + "uhorn" => "\u{01B0}", + "uhornacute" => "\u{1EE9}", + "uhorndotbelow" => "\u{1EF1}", + "uhorngrave" => "\u{1EEB}", + "uhornhookabove" => "\u{1EED}", + "uhorntilde" => "\u{1EEF}", + "uhungarumlaut" => "\u{0171}", + "uhungarumlautcyrillic" => "\u{04F3}", + "uinvertedbreve" => "\u{0217}", + "ukatakana" => "\u{30A6}", + "ukatakanahalfwidth" => "\u{FF73}", + "ukcyrillic" => "\u{0479}", + "ukorean" => "\u{315C}", + "umacron" => "\u{016B}", + "umacroncyrillic" => "\u{04EF}", + "umacrondieresis" => "\u{1E7B}", + "umatragurmukhi" => "\u{0A41}", + "umonospace" => "\u{FF55}", + "underscore" => "\u{005F}", + "underscoredbl" => "\u{2017}", + "underscoremonospace" => "\u{FF3F}", + "underscorevertical" => "\u{FE33}", + "underscorewavy" => "\u{FE4F}", + "union" => "\u{222A}", + "universal" => "\u{2200}", + "uogonek" => "\u{0173}", + "uparen" => "\u{24B0}", + "upblock" => "\u{2580}", + "upperdothebrew" => "\u{05C4}", + "upsilon" => "\u{03C5}", + "upsilondieresis" => "\u{03CB}", + "upsilondieresistonos" => "\u{03B0}", + "upsilonlatin" => "\u{028A}", + "upsilontonos" => "\u{03CD}", + "uptackbelowcmb" => "\u{031D}", + "uptackmod" => "\u{02D4}", + "uragurmukhi" => "\u{0A73}", + "uring" => "\u{016F}", + "ushortcyrillic" => "\u{045E}", + "usmallhiragana" => "\u{3045}", + "usmallkatakana" => "\u{30A5}", + "usmallkatakanahalfwidth" => "\u{FF69}", + "ustraightcyrillic" => "\u{04AF}", + "ustraightstrokecyrillic" => "\u{04B1}", + "utilde" => "\u{0169}", + "utildeacute" => "\u{1E79}", + "utildebelow" => "\u{1E75}", + "uubengali" => "\u{098A}", + "uudeva" => "\u{090A}", + "uugujarati" => "\u{0A8A}", + "uugurmukhi" => "\u{0A0A}", + "uumatragurmukhi" => "\u{0A42}", + "uuvowelsignbengali" => "\u{09C2}", + "uuvowelsigndeva" => "\u{0942}", + "uuvowelsigngujarati" => "\u{0AC2}", + "uvowelsignbengali" => "\u{09C1}", + "uvowelsigndeva" => "\u{0941}", + "uvowelsigngujarati" => "\u{0AC1}", + "v" => "\u{0076}", + "vadeva" => "\u{0935}", + "vagujarati" => "\u{0AB5}", + "vagurmukhi" => "\u{0A35}", + "vakatakana" => "\u{30F7}", + "vav" => "\u{05D5}", + "vavdagesh" => "\u{FB35}", + "vavdagesh65" => "\u{FB35}", + "vavdageshhebrew" => "\u{FB35}", + "vavhebrew" => "\u{05D5}", + "vavholam" => "\u{FB4B}", + "vavholamhebrew" => "\u{FB4B}", + "vavvavhebrew" => "\u{05F0}", + "vavyodhebrew" => "\u{05F1}", + "vcircle" => "\u{24E5}", + "vdotbelow" => "\u{1E7F}", + "vecyrillic" => "\u{0432}", + "veharabic" => "\u{06A4}", + "vehfinalarabic" => "\u{FB6B}", + "vehinitialarabic" => "\u{FB6C}", + "vehmedialarabic" => "\u{FB6D}", + "vekatakana" => "\u{30F9}", + "venus" => "\u{2640}", + "verticalbar" => "\u{007C}", + "verticallineabovecmb" => "\u{030D}", + "verticallinebelowcmb" => "\u{0329}", + "verticallinelowmod" => "\u{02CC}", + "verticallinemod" => "\u{02C8}", + "vewarmenian" => "\u{057E}", + "vhook" => "\u{028B}", + "vikatakana" => "\u{30F8}", + "viramabengali" => "\u{09CD}", + "viramadeva" => "\u{094D}", + "viramagujarati" => "\u{0ACD}", + "visargabengali" => "\u{0983}", + "visargadeva" => "\u{0903}", + "visargagujarati" => "\u{0A83}", + "vmonospace" => "\u{FF56}", + "voarmenian" => "\u{0578}", + "voicediterationhiragana" => "\u{309E}", + "voicediterationkatakana" => "\u{30FE}", + "voicedmarkkana" => "\u{309B}", + "voicedmarkkanahalfwidth" => "\u{FF9E}", + "vokatakana" => "\u{30FA}", + "vparen" => "\u{24B1}", + "vtilde" => "\u{1E7D}", + "vturned" => "\u{028C}", + "vuhiragana" => "\u{3094}", + "vukatakana" => "\u{30F4}", + "w" => "\u{0077}", + "wacute" => "\u{1E83}", + "waekorean" => "\u{3159}", + "wahiragana" => "\u{308F}", + "wakatakana" => "\u{30EF}", + "wakatakanahalfwidth" => "\u{FF9C}", + "wakorean" => "\u{3158}", + "wasmallhiragana" => "\u{308E}", + "wasmallkatakana" => "\u{30EE}", + "wattosquare" => "\u{3357}", + "wavedash" => "\u{301C}", + "wavyunderscorevertical" => "\u{FE34}", + "wawarabic" => "\u{0648}", + "wawfinalarabic" => "\u{FEEE}", + "wawhamzaabovearabic" => "\u{0624}", + "wawhamzaabovefinalarabic" => "\u{FE86}", + "wbsquare" => "\u{33DD}", + "wcircle" => "\u{24E6}", + "wcircumflex" => "\u{0175}", + "wdieresis" => "\u{1E85}", + "wdotaccent" => "\u{1E87}", + "wdotbelow" => "\u{1E89}", + "wehiragana" => "\u{3091}", + "weierstrass" => "\u{2118}", + "wekatakana" => "\u{30F1}", + "wekorean" => "\u{315E}", + "weokorean" => "\u{315D}", + "wgrave" => "\u{1E81}", + "whitebullet" => "\u{25E6}", + "whitecircle" => "\u{25CB}", + "whitecircleinverse" => "\u{25D9}", + "whitecornerbracketleft" => "\u{300E}", + "whitecornerbracketleftvertical" => "\u{FE43}", + "whitecornerbracketright" => "\u{300F}", + "whitecornerbracketrightvertical" => "\u{FE44}", + "whitediamond" => "\u{25C7}", + "whitediamondcontainingblacksmalldiamond" => "\u{25C8}", + "whitedownpointingsmalltriangle" => "\u{25BF}", + "whitedownpointingtriangle" => "\u{25BD}", + "whiteleftpointingsmalltriangle" => "\u{25C3}", + "whiteleftpointingtriangle" => "\u{25C1}", + "whitelenticularbracketleft" => "\u{3016}", + "whitelenticularbracketright" => "\u{3017}", + "whiterightpointingsmalltriangle" => "\u{25B9}", + "whiterightpointingtriangle" => "\u{25B7}", + "whitesmallsquare" => "\u{25AB}", + "whitesmilingface" => "\u{263A}", + "whitesquare" => "\u{25A1}", + "whitestar" => "\u{2606}", + "whitetelephone" => "\u{260F}", + "whitetortoiseshellbracketleft" => "\u{3018}", + "whitetortoiseshellbracketright" => "\u{3019}", + "whiteuppointingsmalltriangle" => "\u{25B5}", + "whiteuppointingtriangle" => "\u{25B3}", + "wihiragana" => "\u{3090}", + "wikatakana" => "\u{30F0}", + "wikorean" => "\u{315F}", + "wmonospace" => "\u{FF57}", + "wohiragana" => "\u{3092}", + "wokatakana" => "\u{30F2}", + "wokatakanahalfwidth" => "\u{FF66}", + "won" => "\u{20A9}", + "wonmonospace" => "\u{FFE6}", + "wowaenthai" => "\u{0E27}", + "wparen" => "\u{24B2}", + "wring" => "\u{1E98}", + "wsuperior" => "\u{02B7}", + "wturned" => "\u{028D}", + "wynn" => "\u{01BF}", + "x" => "\u{0078}", + "xabovecmb" => "\u{033D}", + "xbopomofo" => "\u{3112}", + "xcircle" => "\u{24E7}", + "xdieresis" => "\u{1E8D}", + "xdotaccent" => "\u{1E8B}", + "xeharmenian" => "\u{056D}", + "xi" => "\u{03BE}", + "xmonospace" => "\u{FF58}", + "xparen" => "\u{24B3}", + "xsuperior" => "\u{02E3}", + "y" => "\u{0079}", + "yaadosquare" => "\u{334E}", + "yabengali" => "\u{09AF}", + "yacute" => "\u{00FD}", + "yadeva" => "\u{092F}", + "yaekorean" => "\u{3152}", + "yagujarati" => "\u{0AAF}", + "yagurmukhi" => "\u{0A2F}", + "yahiragana" => "\u{3084}", + "yakatakana" => "\u{30E4}", + "yakatakanahalfwidth" => "\u{FF94}", + "yakorean" => "\u{3151}", + "yamakkanthai" => "\u{0E4E}", + "yasmallhiragana" => "\u{3083}", + "yasmallkatakana" => "\u{30E3}", + "yasmallkatakanahalfwidth" => "\u{FF6C}", + "yatcyrillic" => "\u{0463}", + "ycircle" => "\u{24E8}", + "ycircumflex" => "\u{0177}", + "ydieresis" => "\u{00FF}", + "ydotaccent" => "\u{1E8F}", + "ydotbelow" => "\u{1EF5}", + "yeharabic" => "\u{064A}", + "yehbarreearabic" => "\u{06D2}", + "yehbarreefinalarabic" => "\u{FBAF}", + "yehfinalarabic" => "\u{FEF2}", + "yehhamzaabovearabic" => "\u{0626}", + "yehhamzaabovefinalarabic" => "\u{FE8A}", + "yehhamzaaboveinitialarabic" => "\u{FE8B}", + "yehhamzaabovemedialarabic" => "\u{FE8C}", + "yehinitialarabic" => "\u{FEF3}", + "yehmedialarabic" => "\u{FEF4}", + "yehmeeminitialarabic" => "\u{FCDD}", + "yehmeemisolatedarabic" => "\u{FC58}", + "yehnoonfinalarabic" => "\u{FC94}", + "yehthreedotsbelowarabic" => "\u{06D1}", + "yekorean" => "\u{3156}", + "yen" => "\u{00A5}", + "yenmonospace" => "\u{FFE5}", + "yeokorean" => "\u{3155}", + "yeorinhieuhkorean" => "\u{3186}", + "yerahbenyomohebrew" => "\u{05AA}", + "yerahbenyomolefthebrew" => "\u{05AA}", + "yericyrillic" => "\u{044B}", + "yerudieresiscyrillic" => "\u{04F9}", + "yesieungkorean" => "\u{3181}", + "yesieungpansioskorean" => "\u{3183}", + "yesieungsioskorean" => "\u{3182}", + "yetivhebrew" => "\u{059A}", + "ygrave" => "\u{1EF3}", + "yhook" => "\u{01B4}", + "yhookabove" => "\u{1EF7}", + "yiarmenian" => "\u{0575}", + "yicyrillic" => "\u{0457}", + "yikorean" => "\u{3162}", + "yinyang" => "\u{262F}", + "yiwnarmenian" => "\u{0582}", + "ymonospace" => "\u{FF59}", + "yod" => "\u{05D9}", + "yoddagesh" => "\u{FB39}", + "yoddageshhebrew" => "\u{FB39}", + "yodhebrew" => "\u{05D9}", + "yodyodhebrew" => "\u{05F2}", + "yodyodpatahhebrew" => "\u{FB1F}", + "yohiragana" => "\u{3088}", + "yoikorean" => "\u{3189}", + "yokatakana" => "\u{30E8}", + "yokatakanahalfwidth" => "\u{FF96}", + "yokorean" => "\u{315B}", + "yosmallhiragana" => "\u{3087}", + "yosmallkatakana" => "\u{30E7}", + "yosmallkatakanahalfwidth" => "\u{FF6E}", + "yotgreek" => "\u{03F3}", + "yoyaekorean" => "\u{3188}", + "yoyakorean" => "\u{3187}", + "yoyakthai" => "\u{0E22}", + "yoyingthai" => "\u{0E0D}", + "yparen" => "\u{24B4}", + "ypogegrammeni" => "\u{037A}", + "ypogegrammenigreekcmb" => "\u{0345}", + "yr" => "\u{01A6}", + "yring" => "\u{1E99}", + "ysuperior" => "\u{02B8}", + "ytilde" => "\u{1EF9}", + "yturned" => "\u{028E}", + "yuhiragana" => "\u{3086}", + "yuikorean" => "\u{318C}", + "yukatakana" => "\u{30E6}", + "yukatakanahalfwidth" => "\u{FF95}", + "yukorean" => "\u{3160}", + "yusbigcyrillic" => "\u{046B}", + "yusbigiotifiedcyrillic" => "\u{046D}", + "yuslittlecyrillic" => "\u{0467}", + "yuslittleiotifiedcyrillic" => "\u{0469}", + "yusmallhiragana" => "\u{3085}", + "yusmallkatakana" => "\u{30E5}", + "yusmallkatakanahalfwidth" => "\u{FF6D}", + "yuyekorean" => "\u{318B}", + "yuyeokorean" => "\u{318A}", + "yyabengali" => "\u{09DF}", + "yyadeva" => "\u{095F}", + "z" => "\u{007A}", + "zaarmenian" => "\u{0566}", + "zacute" => "\u{017A}", + "zadeva" => "\u{095B}", + "zagurmukhi" => "\u{0A5B}", + "zaharabic" => "\u{0638}", + "zahfinalarabic" => "\u{FEC6}", + "zahinitialarabic" => "\u{FEC7}", + "zahiragana" => "\u{3056}", + "zahmedialarabic" => "\u{FEC8}", + "zainarabic" => "\u{0632}", + "zainfinalarabic" => "\u{FEB0}", + "zakatakana" => "\u{30B6}", + "zaqefgadolhebrew" => "\u{0595}", + "zaqefqatanhebrew" => "\u{0594}", + "zarqahebrew" => "\u{0598}", + "zayin" => "\u{05D6}", + "zayindagesh" => "\u{FB36}", + "zayindageshhebrew" => "\u{FB36}", + "zayinhebrew" => "\u{05D6}", + "zbopomofo" => "\u{3117}", + "zcaron" => "\u{017E}", + "zcircle" => "\u{24E9}", + "zcircumflex" => "\u{1E91}", + "zcurl" => "\u{0291}", + "zdot" => "\u{017C}", + "zdotaccent" => "\u{017C}", + "zdotbelow" => "\u{1E93}", + "zecyrillic" => "\u{0437}", + "zedescendercyrillic" => "\u{0499}", + "zedieresiscyrillic" => "\u{04DF}", + "zehiragana" => "\u{305C}", + "zekatakana" => "\u{30BC}", + "zero" => "\u{0030}", + "zeroarabic" => "\u{0660}", + "zerobengali" => "\u{09E6}", + "zerodeva" => "\u{0966}", + "zerogujarati" => "\u{0AE6}", + "zerogurmukhi" => "\u{0A66}", + "zerohackarabic" => "\u{0660}", + "zeroinferior" => "\u{2080}", + "zeromonospace" => "\u{FF10}", + "zerooldstyle" => "\u{F730}", + "zeropersian" => "\u{06F0}", + "zerosuperior" => "\u{2070}", + "zerothai" => "\u{0E50}", + "zerowidthjoiner" => "\u{FEFF}", + "zerowidthnonjoiner" => "\u{200C}", + "zerowidthspace" => "\u{200B}", + "zeta" => "\u{03B6}", + "zhbopomofo" => "\u{3113}", + "zhearmenian" => "\u{056A}", + "zhebrevecyrillic" => "\u{04C2}", + "zhecyrillic" => "\u{0436}", + "zhedescendercyrillic" => "\u{0497}", + "zhedieresiscyrillic" => "\u{04DD}", + "zihiragana" => "\u{3058}", + "zikatakana" => "\u{30B8}", + "zinorhebrew" => "\u{05AE}", + "zlinebelow" => "\u{1E95}", + "zmonospace" => "\u{FF5A}", + "zohiragana" => "\u{305E}", + "zokatakana" => "\u{30BE}", + "zparen" => "\u{24B5}", + "zretroflexhook" => "\u{0290}", + "zstroke" => "\u{01B6}", + "zuhiragana" => "\u{305A}", + "zukatakana" => "\u{30BA}", + "angbracketleft" => "\u{3008}", + "angbracketright" => "\u{3009}", + "circlecopyrt" => "\u{00A9}", + "controlNULL" => "\u{0000}", + "angbracketleftbig" => "\u{2329}", + "angbracketleftBig" => "\u{2329}", + "angbracketleftbigg" => "\u{2329}", + "angbracketleftBigg" => "\u{2329}", + "angbracketrightBig" => "\u{232A}", + "angbracketrightbig" => "\u{232A}", + "angbracketrightBigg" => "\u{232A}", + "angbracketrightbigg" => "\u{232A}", + "arrowhookleft" => "\u{21AA}", + "arrowhookright" => "\u{21A9}", + "arrowlefttophalf" => "\u{21BC}", + "arrowleftbothalf" => "\u{21BD}", + "arrownortheast" => "\u{2197}", + "arrownorthwest" => "\u{2196}", + "arrowrighttophalf" => "\u{21C0}", + "arrowrightbothalf" => "\u{21C1}", + "arrowsoutheast" => "\u{2198}", + "arrowsouthwest" => "\u{2199}", + "backslashbig" => "\u{2216}", + "backslashBig" => "\u{2216}", + "backslashBigg" => "\u{2216}", + "backslashbigg" => "\u{2216}", + "bardbl" => "\u{2016}", + "bracehtipdownleft" => "\u{FE37}", + "bracehtipdownright" => "\u{FE37}", + "bracehtipupleft" => "\u{FE38}", + "bracehtipupright" => "\u{FE38}", + "braceleftBig" => "\u{007B}", + "braceleftbig" => "\u{007B}", + "braceleftbigg" => "\u{007B}", + "braceleftBigg" => "\u{007B}", + "bracerightBig" => "\u{007D}", + "bracerightbig" => "\u{007D}", + "bracerightbigg" => "\u{007D}", + "bracerightBigg" => "\u{007D}", + "bracketleftbig" => "\u{005B}", + "bracketleftBig" => "\u{005B}", + "bracketleftbigg" => "\u{005B}", + "bracketleftBigg" => "\u{005B}", + "bracketleftmath" => "\u{005B}", + "bracketrightBig" => "\u{005D}", + "bracketrightbig" => "\u{005D}", + "bracketrightbigg" => "\u{005D}", + "bracketrightBigg" => "\u{005D}", + "bracketrightmath" => "\u{005D}", + "ceilingleftbig" => "\u{2308}", + "ceilingleftBig" => "\u{2308}", + "ceilingleftBigg" => "\u{2308}", + "ceilingleftbigg" => "\u{2308}", + "ceilingrightbig" => "\u{2309}", + "ceilingrightBig" => "\u{2309}", + "ceilingrightbigg" => "\u{2309}", + "ceilingrightBigg" => "\u{2309}", + "circledotdisplay" => "\u{2299}", + "circledottext" => "\u{2299}", + "circlemultiplydisplay" => "\u{2297}", + "circlemultiplytext" => "\u{2297}", + "circleplusdisplay" => "\u{2295}", + "circleplustext" => "\u{2295}", + "contintegraldisplay" => "\u{222E}", + "contintegraltext" => "\u{222E}", + "coproductdisplay" => "\u{2210}", + "coproducttext" => "\u{2210}", + "epsilon1" => "\u{03B5}", + "equalmath" => "\u{003D}", + "floorleftBig" => "\u{230A}", + "floorleftbig" => "\u{230A}", + "floorleftbigg" => "\u{230A}", + "floorleftBigg" => "\u{230A}", + "floorrightbig" => "\u{230B}", + "floorrightBig" => "\u{230B}", + "floorrightBigg" => "\u{230B}", + "floorrightbigg" => "\u{230B}", + "hatwide" => "\u{0302}", + "hatwider" => "\u{0302}", + "hatwidest" => "\u{0302}", + "intercal" => "\u{1D40}", + "integraldisplay" => "\u{222B}", + "integraltext" => "\u{222B}", + "intersectiondisplay" => "\u{22C2}", + "intersectiontext" => "\u{22C2}", + "logicalanddisplay" => "\u{2227}", + "logicalandtext" => "\u{2227}", + "logicalordisplay" => "\u{2228}", + "logicalortext" => "\u{2228}", + "parenleftBig" => "\u{0028}", + "parenleftbig" => "\u{0028}", + "parenleftBigg" => "\u{0028}", + "parenleftbigg" => "\u{0028}", + "parenleftmath" => "\u{0028}", + "parenrightBig" => "\u{0029}", + "parenrightbig" => "\u{0029}", + "parenrightBigg" => "\u{0029}", + "parenrightbigg" => "\u{0029}", + "parenrightmath" => "\u{0029}", + "plusmath" => "\u{002B}", + "prime" => "\u{2032}", + "productdisplay" => "\u{220F}", + "producttext" => "\u{220F}", + "radicalbig" => "\u{221A}", + "radicalBig" => "\u{221A}", + "radicalBigg" => "\u{221A}", + "radicalbigg" => "\u{221A}", + "radicalbt" => "\u{221A}", + "radicaltp" => "\u{221A}", + "radicalvertex" => "\u{221A}", + "slashbig" => "\u{002F}", + "slashBig" => "\u{002F}", + "slashBigg" => "\u{002F}", + "slashbigg" => "\u{002F}", + "summationdisplay" => "\u{2211}", + "summationtext" => "\u{2211}", + "tildewide" => "\u{02DC}", + "tildewider" => "\u{02DC}", + "tildewidest" => "\u{02DC}", + "uniondisplay" => "\u{22C3}", + "unionmultidisplay" => "\u{228E}", + "unionmultitext" => "\u{228E}", + "unionsqdisplay" => "\u{2294}", + "unionsqtext" => "\u{2294}", + "uniontext" => "\u{22C3}", + "vextenddouble" => "\u{2225}", + "vextendsingle" => "\u{2223}", +}; + +static REVERSE_GLYPH_NAMES: phf::Map = phf_map! { + '\u{0000}' => "controlNULL", + '\u{0001}' => "controlSTX", + '\u{0002}' => "controlSOT", + '\u{0003}' => "controlETX", + '\u{0004}' => "controlEOT", + '\u{0005}' => "controlENQ", + '\u{0006}' => "controlACK", + '\u{0007}' => "controlBEL", + '\u{0008}' => "controlBS", + '\u{0009}' => "controlHT", + '\u{000A}' => "controlLF", + '\u{000B}' => "controlVT", + '\u{000C}' => "controlFF", + '\u{000D}' => "controlCR", + '\u{000E}' => "controlSO", + '\u{000F}' => "controlSI", + '\u{0010}' => "controlDLE", + '\u{0011}' => "controlDC1", + '\u{0012}' => "controlDC2", + '\u{0013}' => "controlDC3", + '\u{0014}' => "controlDC4", + '\u{0015}' => "controlNAK", + '\u{0016}' => "controlSYN", + '\u{0017}' => "controlETB", + '\u{0018}' => "controlCAN", + '\u{0019}' => "controlEM", + '\u{001A}' => "controlSUB", + '\u{001B}' => "controlESC", + '\u{001C}' => "controlFS", + '\u{001D}' => "controlGS", + '\u{001E}' => "controlRS", + '\u{001F}' => "controlUS", + '\u{0020}' => "space", + '\u{0021}' => "exclam", + '\u{0022}' => "quotedbl", + '\u{0023}' => "numbersign", + '\u{0024}' => "dollar", + '\u{0025}' => "percent", + '\u{0026}' => "ampersand", + '\u{0027}' => "quotesingle", + '\u{0028}' => "parenleft", + '\u{0029}' => "parenright", + '\u{002A}' => "asterisk", + '\u{002B}' => "plus", + '\u{002C}' => "comma", + '\u{002D}' => "hyphen", + '\u{002E}' => "period", + '\u{002F}' => "slash", + '\u{0030}' => "zero", + '\u{0031}' => "one", + '\u{0032}' => "two", + '\u{0033}' => "three", + '\u{0034}' => "four", + '\u{0035}' => "five", + '\u{0036}' => "six", + '\u{0037}' => "seven", + '\u{0038}' => "eight", + '\u{0039}' => "nine", + '\u{003A}' => "colon", + '\u{003B}' => "semicolon", + '\u{003C}' => "less", + '\u{003D}' => "equal", + '\u{003E}' => "greater", + '\u{003F}' => "question", + '\u{0040}' => "at", + '\u{0041}' => "A", + '\u{0042}' => "B", + '\u{0043}' => "C", + '\u{0044}' => "D", + '\u{0045}' => "E", + '\u{0046}' => "F", + '\u{0047}' => "G", + '\u{0048}' => "H", + '\u{0049}' => "I", + '\u{004A}' => "J", + '\u{004B}' => "K", + '\u{004C}' => "L", + '\u{004D}' => "M", + '\u{004E}' => "N", + '\u{004F}' => "O", + '\u{0050}' => "P", + '\u{0051}' => "Q", + '\u{0052}' => "R", + '\u{0053}' => "S", + '\u{0054}' => "T", + '\u{0055}' => "U", + '\u{0056}' => "V", + '\u{0057}' => "W", + '\u{0058}' => "X", + '\u{0059}' => "Y", + '\u{005A}' => "Z", + '\u{005B}' => "bracketleft", + '\u{005C}' => "backslash", + '\u{005D}' => "bracketright", + '\u{005E}' => "asciicircum", + '\u{005F}' => "underscore", + '\u{0060}' => "grave", + '\u{0061}' => "a", + '\u{0062}' => "b", + '\u{0063}' => "c", + '\u{0064}' => "d", + '\u{0065}' => "e", + '\u{0066}' => "f", + '\u{0067}' => "g", + '\u{0068}' => "h", + '\u{0069}' => "i", + '\u{006A}' => "j", + '\u{006B}' => "k", + '\u{006C}' => "l", + '\u{006D}' => "m", + '\u{006E}' => "n", + '\u{006F}' => "o", + '\u{0070}' => "p", + '\u{0071}' => "q", + '\u{0072}' => "r", + '\u{0073}' => "s", + '\u{0074}' => "t", + '\u{0075}' => "u", + '\u{0076}' => "v", + '\u{0077}' => "w", + '\u{0078}' => "x", + '\u{0079}' => "y", + '\u{007A}' => "z", + '\u{007B}' => "braceleft", + '\u{007C}' => "bar", + '\u{007D}' => "braceright", + '\u{007E}' => "asciitilde", + '\u{007F}' => "controlDEL", + '\u{00A0}' => "nbspace", + '\u{00A1}' => "exclamdown", + '\u{00A2}' => "cent", + '\u{00A3}' => "sterling", + '\u{00A4}' => "currency", + '\u{00A5}' => "yen", + '\u{00A6}' => "brokenbar", + '\u{00A7}' => "section", + '\u{00A8}' => "dieresis", + '\u{00A9}' => "copyright", + '\u{00AA}' => "ordfeminine", + '\u{00AB}' => "guillemotleft", + '\u{00AC}' => "logicalnot", + '\u{00AD}' => "sfthyphen", + '\u{00AE}' => "registered", + '\u{00AF}' => "macron", + '\u{00B0}' => "degree", + '\u{00B1}' => "plusminus", + '\u{00B2}' => "twosuperior", + '\u{00B3}' => "threesuperior", + '\u{00B4}' => "acute", + '\u{00B5}' => "mu", + '\u{00B6}' => "paragraph", + '\u{00B7}' => "middot", + '\u{00B8}' => "cedilla", + '\u{00B9}' => "onesuperior", + '\u{00BA}' => "ordmasculine", + '\u{00BB}' => "guillemotright", + '\u{00BC}' => "onequarter", + '\u{00BD}' => "onehalf", + '\u{00BE}' => "threequarters", + '\u{00BF}' => "questiondown", + '\u{00C0}' => "Agrave", + '\u{00C1}' => "Aacute", + '\u{00C2}' => "Acircumflex", + '\u{00C3}' => "Atilde", + '\u{00C4}' => "Adieresis", + '\u{00C5}' => "Aring", + '\u{00C6}' => "AE", + '\u{00C7}' => "Ccedilla", + '\u{00C8}' => "Egrave", + '\u{00C9}' => "Eacute", + '\u{00CA}' => "Ecircumflex", + '\u{00CB}' => "Edieresis", + '\u{00CC}' => "Igrave", + '\u{00CD}' => "Iacute", + '\u{00CE}' => "Icircumflex", + '\u{00CF}' => "Idieresis", + '\u{00D0}' => "Eth", + '\u{00D1}' => "Ntilde", + '\u{00D2}' => "Ograve", + '\u{00D3}' => "Oacute", + '\u{00D4}' => "Ocircumflex", + '\u{00D5}' => "Otilde", + '\u{00D6}' => "Odieresis", + '\u{00D7}' => "multiply", + '\u{00D8}' => "Oslash", + '\u{00D9}' => "Ugrave", + '\u{00DA}' => "Uacute", + '\u{00DB}' => "Ucircumflex", + '\u{00DC}' => "Udieresis", + '\u{00DD}' => "Yacute", + '\u{00DE}' => "Thorn", + '\u{00DF}' => "germandbls", + '\u{00E0}' => "agrave", + '\u{00E1}' => "aacute", + '\u{00E2}' => "acircumflex", + '\u{00E3}' => "atilde", + '\u{00E4}' => "adieresis", + '\u{00E5}' => "aring", + '\u{00E6}' => "ae", + '\u{00E7}' => "ccedilla", + '\u{00E8}' => "egrave", + '\u{00E9}' => "eacute", + '\u{00EA}' => "ecircumflex", + '\u{00EB}' => "edieresis", + '\u{00EC}' => "igrave", + '\u{00ED}' => "iacute", + '\u{00EE}' => "icircumflex", + '\u{00EF}' => "idieresis", + '\u{00F0}' => "eth", + '\u{00F1}' => "ntilde", + '\u{00F2}' => "ograve", + '\u{00F3}' => "oacute", + '\u{00F4}' => "ocircumflex", + '\u{00F5}' => "otilde", + '\u{00F6}' => "odieresis", + '\u{00F7}' => "divide", + '\u{00F8}' => "oslash", + '\u{00F9}' => "ugrave", + '\u{00FA}' => "uacute", + '\u{00FB}' => "ucircumflex", + '\u{00FC}' => "udieresis", + '\u{00FD}' => "yacute", + '\u{00FE}' => "thorn", + '\u{00FF}' => "ydieresis", + '\u{0100}' => "Amacron", + '\u{0101}' => "amacron", + '\u{0102}' => "Abreve", + '\u{0103}' => "abreve", + '\u{0104}' => "Aogonek", + '\u{0105}' => "aogonek", + '\u{0106}' => "Cacute", + '\u{0107}' => "cacute", + '\u{0108}' => "Ccircumflex", + '\u{0109}' => "ccircumflex", + '\u{010A}' => "Cdot", + '\u{010B}' => "cdot", + '\u{010C}' => "Ccaron", + '\u{010D}' => "ccaron", + '\u{010E}' => "Dcaron", + '\u{010F}' => "dcaron", + '\u{0110}' => "Dcroat", + '\u{0111}' => "dcroat", + '\u{0112}' => "Emacron", + '\u{0113}' => "emacron", + '\u{0114}' => "Ebreve", + '\u{0115}' => "ebreve", + '\u{0116}' => "Edot", + '\u{0117}' => "edot", + '\u{0118}' => "Eogonek", + '\u{0119}' => "eogonek", + '\u{011A}' => "Ecaron", + '\u{011B}' => "ecaron", + '\u{011C}' => "Gcircumflex", + '\u{011D}' => "gcircumflex", + '\u{011E}' => "Gbreve", + '\u{011F}' => "gbreve", + '\u{0120}' => "Gdot", + '\u{0121}' => "gdot", + '\u{0122}' => "Gcedilla", + '\u{0123}' => "gcedilla", + '\u{0124}' => "Hcircumflex", + '\u{0125}' => "hcircumflex", + '\u{0126}' => "Hbar", + '\u{0127}' => "hbar", + '\u{0128}' => "Itilde", + '\u{0129}' => "itilde", + '\u{012A}' => "Imacron", + '\u{012B}' => "imacron", + '\u{012C}' => "Ibreve", + '\u{012D}' => "ibreve", + '\u{012E}' => "Iogonek", + '\u{012F}' => "iogonek", + '\u{0130}' => "Idot", + '\u{0131}' => "dotlessi", + '\u{0132}' => "IJ", + '\u{0133}' => "ij", + '\u{0134}' => "Jcircumflex", + '\u{0135}' => "jcircumflex", + '\u{0136}' => "Kcedilla", + '\u{0137}' => "kcedilla", + '\u{0138}' => "kgreenlandic", + '\u{0139}' => "Lacute", + '\u{013A}' => "lacute", + '\u{013B}' => "Lcedilla", + '\u{013C}' => "lcedilla", + '\u{013D}' => "Lcaron", + '\u{013E}' => "lcaron", + '\u{013F}' => "Ldot", + '\u{0140}' => "ldot", + '\u{0141}' => "Lslash", + '\u{0142}' => "lslash", + '\u{0143}' => "Nacute", + '\u{0144}' => "nacute", + '\u{0145}' => "Ncedilla", + '\u{0146}' => "ncedilla", + '\u{0147}' => "Ncaron", + '\u{0148}' => "ncaron", + '\u{0149}' => "napostrophe", + '\u{014A}' => "Eng", + '\u{014B}' => "eng", + '\u{014C}' => "Omacron", + '\u{014D}' => "omacron", + '\u{014E}' => "Obreve", + '\u{014F}' => "obreve", + '\u{0150}' => "Odblacute", + '\u{0151}' => "odblacute", + '\u{0152}' => "OE", + '\u{0153}' => "oe", + '\u{0154}' => "Racute", + '\u{0155}' => "racute", + '\u{0156}' => "Rcedilla", + '\u{0157}' => "rcedilla", + '\u{0158}' => "Rcaron", + '\u{0159}' => "rcaron", + '\u{015A}' => "Sacute", + '\u{015B}' => "sacute", + '\u{015C}' => "Scircumflex", + '\u{015D}' => "scircumflex", + '\u{015E}' => "Scedilla", + '\u{015F}' => "scedilla", + '\u{0160}' => "Scaron", + '\u{0161}' => "scaron", + '\u{0162}' => "Tcedilla", + '\u{0163}' => "tcedilla", + '\u{0164}' => "Tcaron", + '\u{0165}' => "tcaron", + '\u{0166}' => "Tbar", + '\u{0167}' => "tbar", + '\u{0168}' => "Utilde", + '\u{0169}' => "utilde", + '\u{016A}' => "Umacron", + '\u{016B}' => "umacron", + '\u{016C}' => "Ubreve", + '\u{016D}' => "ubreve", + '\u{016E}' => "Uring", + '\u{016F}' => "uring", + '\u{0170}' => "Udblacute", + '\u{0171}' => "udblacute", + '\u{0172}' => "Uogonek", + '\u{0173}' => "uogonek", + '\u{0174}' => "Wcircumflex", + '\u{0175}' => "wcircumflex", + '\u{0176}' => "Ycircumflex", + '\u{0177}' => "ycircumflex", + '\u{0178}' => "Ydieresis", + '\u{0179}' => "Zacute", + '\u{017A}' => "zacute", + '\u{017B}' => "Zdot", + '\u{017C}' => "zdot", + '\u{017D}' => "Zcaron", + '\u{017E}' => "zcaron", + '\u{017F}' => "longs", + '\u{0180}' => "bstroke", + '\u{0181}' => "Bhook", + '\u{0182}' => "Btopbar", + '\u{0183}' => "btopbar", + '\u{0184}' => "Tonesix", + '\u{0185}' => "tonesix", + '\u{0186}' => "Oopen", + '\u{0187}' => "Chook", + '\u{0188}' => "chook", + '\u{0189}' => "Dafrican", + '\u{018A}' => "Dhook", + '\u{018B}' => "Dtopbar", + '\u{018C}' => "dtopbar", + '\u{018D}' => "deltaturned", + '\u{018E}' => "Ereversed", + '\u{018F}' => "Schwa", + '\u{0190}' => "Eopen", + '\u{0191}' => "Fhook", + '\u{0192}' => "florin", + '\u{0193}' => "Ghook", + '\u{0194}' => "Gammaafrican", + '\u{0195}' => "hv", + '\u{0196}' => "Iotaafrican", + '\u{0197}' => "Istroke", + '\u{0198}' => "Khook", + '\u{0199}' => "khook", + '\u{019A}' => "lbar", + '\u{019B}' => "lambdastroke", + '\u{019C}' => "Mturned", + '\u{019D}' => "Nhookleft", + '\u{019E}' => "nlegrightlong", + '\u{019F}' => "Ocenteredtilde", + '\u{01A0}' => "Ohorn", + '\u{01A1}' => "ohorn", + '\u{01A2}' => "Oi", + '\u{01A3}' => "oi", + '\u{01A4}' => "Phook", + '\u{01A5}' => "phook", + '\u{01A6}' => "yr", + '\u{01A7}' => "Tonetwo", + '\u{01A8}' => "tonetwo", + '\u{01A9}' => "Esh", + '\u{01AA}' => "eshreversedloop", + '\u{01AB}' => "tpalatalhook", + '\u{01AC}' => "Thook", + '\u{01AD}' => "thook", + '\u{01AE}' => "Tretroflexhook", + '\u{01AF}' => "Uhorn", + '\u{01B0}' => "uhorn", + '\u{01B1}' => "Upsilonafrican", + '\u{01B2}' => "Vhook", + '\u{01B3}' => "Yhook", + '\u{01B4}' => "yhook", + '\u{01B5}' => "Zstroke", + '\u{01B6}' => "zstroke", + '\u{01B7}' => "Ezh", + '\u{01B8}' => "Ezhreversed", + '\u{01B9}' => "ezhreversed", + '\u{01BA}' => "ezhtail", + '\u{01BB}' => "twostroke", + '\u{01BC}' => "Tonefive", + '\u{01BD}' => "tonefive", + '\u{01BE}' => "glottalinvertedstroke", + '\u{01BF}' => "wynn", + '\u{01C0}' => "clickdental", + '\u{01C1}' => "clicklateral", + '\u{01C2}' => "clickalveolar", + '\u{01C3}' => "clickretroflex", + '\u{01C4}' => "DZcaron", + '\u{01C5}' => "Dzcaron", + '\u{01C6}' => "dzcaron", + '\u{01C7}' => "LJ", + '\u{01C8}' => "Lj", + '\u{01C9}' => "lj", + '\u{01CA}' => "NJ", + '\u{01CB}' => "Nj", + '\u{01CC}' => "nj", + '\u{01CD}' => "Acaron", + '\u{01CE}' => "acaron", + '\u{01CF}' => "Icaron", + '\u{01D0}' => "icaron", + '\u{01D1}' => "Ocaron", + '\u{01D2}' => "ocaron", + '\u{01D3}' => "Ucaron", + '\u{01D4}' => "ucaron", + '\u{01D5}' => "Udieresismacron", + '\u{01D6}' => "udieresismacron", + '\u{01D7}' => "Udieresisacute", + '\u{01D8}' => "udieresisacute", + '\u{01D9}' => "Udieresiscaron", + '\u{01DA}' => "udieresiscaron", + '\u{01DB}' => "Udieresisgrave", + '\u{01DC}' => "udieresisgrave", + '\u{01DD}' => "eturned", + '\u{01DE}' => "Adieresismacron", + '\u{01DF}' => "adieresismacron", + '\u{01E0}' => "Adotmacron", + '\u{01E1}' => "adotmacron", + '\u{01E2}' => "AEmacron", + '\u{01E3}' => "aemacron", + '\u{01E4}' => "Gstroke", + '\u{01E5}' => "gstroke", + '\u{01E6}' => "Gcaron", + '\u{01E7}' => "gcaron", + '\u{01E8}' => "Kcaron", + '\u{01E9}' => "kcaron", + '\u{01EA}' => "Oogonek", + '\u{01EB}' => "oogonek", + '\u{01EC}' => "Oogonekmacron", + '\u{01ED}' => "oogonekmacron", + '\u{01EE}' => "Ezhcaron", + '\u{01EF}' => "ezhcaron", + '\u{01F0}' => "jcaron", + '\u{01F1}' => "DZ", + '\u{01F2}' => "Dz", + '\u{01F3}' => "dz", + '\u{01F4}' => "Gacute", + '\u{01F5}' => "gacute", + '\u{01FA}' => "Aringacute", + '\u{01FB}' => "aringacute", + '\u{01FC}' => "AEacute", + '\u{01FD}' => "aeacute", + '\u{01FE}' => "Oslashacute", + '\u{01FF}' => "oslashacute", + '\u{0200}' => "Adblgrave", + '\u{0201}' => "adblgrave", + '\u{0202}' => "Ainvertedbreve", + '\u{0203}' => "ainvertedbreve", + '\u{0204}' => "Edblgrave", + '\u{0205}' => "edblgrave", + '\u{0206}' => "Einvertedbreve", + '\u{0207}' => "einvertedbreve", + '\u{0208}' => "Idblgrave", + '\u{0209}' => "idblgrave", + '\u{020A}' => "Iinvertedbreve", + '\u{020B}' => "iinvertedbreve", + '\u{020C}' => "Odblgrave", + '\u{020D}' => "odblgrave", + '\u{020E}' => "Oinvertedbreve", + '\u{020F}' => "oinvertedbreve", + '\u{0210}' => "Rdblgrave", + '\u{0211}' => "rdblgrave", + '\u{0212}' => "Rinvertedbreve", + '\u{0213}' => "rinvertedbreve", + '\u{0214}' => "Udblgrave", + '\u{0215}' => "udblgrave", + '\u{0216}' => "Uinvertedbreve", + '\u{0217}' => "uinvertedbreve", + '\u{0218}' => "Scommaaccent", + '\u{0219}' => "scommaaccent", + '\u{0250}' => "aturned", + '\u{0251}' => "ascript", + '\u{0252}' => "ascriptturned", + '\u{0253}' => "bhook", + '\u{0254}' => "oopen", + '\u{0255}' => "ccurl", + '\u{0256}' => "dtail", + '\u{0257}' => "dhook", + '\u{0258}' => "ereversed", + '\u{0259}' => "schwa", + '\u{025A}' => "schwahook", + '\u{025B}' => "eopen", + '\u{025C}' => "eopenreversed", + '\u{025D}' => "eopenreversedhook", + '\u{025E}' => "eopenreversedclosed", + '\u{025F}' => "jdotlessstroke", + '\u{0260}' => "ghook", + '\u{0261}' => "gscript", + '\u{0263}' => "gammalatinsmall", + '\u{0264}' => "ramshorn", + '\u{0265}' => "hturned", + '\u{0266}' => "hhook", + '\u{0267}' => "henghook", + '\u{0268}' => "istroke", + '\u{0269}' => "iotalatin", + '\u{026B}' => "lmiddletilde", + '\u{026C}' => "lbelt", + '\u{026D}' => "lhookretroflex", + '\u{026E}' => "lezh", + '\u{026F}' => "mturned", + '\u{0270}' => "mlonglegturned", + '\u{0271}' => "mhook", + '\u{0272}' => "nhookleft", + '\u{0273}' => "nhookretroflex", + '\u{0275}' => "obarred", + '\u{0277}' => "omegalatinclosed", + '\u{0278}' => "philatin", + '\u{0279}' => "rturned", + '\u{027A}' => "rlonglegturned", + '\u{027B}' => "rhookturned", + '\u{027C}' => "rlongleg", + '\u{027D}' => "rhook", + '\u{027E}' => "rfishhook", + '\u{027F}' => "rfishhookreversed", + '\u{0281}' => "Rsmallinverted", + '\u{0282}' => "shook", + '\u{0283}' => "esh", + '\u{0284}' => "dotlessjstrokehook", + '\u{0285}' => "eshsquatreversed", + '\u{0286}' => "eshcurl", + '\u{0287}' => "tturned", + '\u{0288}' => "tretroflexhook", + '\u{0289}' => "ubar", + '\u{028A}' => "upsilonlatin", + '\u{028B}' => "vhook", + '\u{028C}' => "vturned", + '\u{028D}' => "wturned", + '\u{028E}' => "yturned", + '\u{0290}' => "zretroflexhook", + '\u{0291}' => "zcurl", + '\u{0292}' => "ezh", + '\u{0293}' => "ezhcurl", + '\u{0294}' => "glottalstop", + '\u{0295}' => "glottalstopreversed", + '\u{0296}' => "glottalstopinverted", + '\u{0297}' => "cstretched", + '\u{0298}' => "bilabialclick", + '\u{029A}' => "eopenclosed", + '\u{029B}' => "Gsmallhook", + '\u{029D}' => "jcrossedtail", + '\u{029E}' => "kturned", + '\u{02A0}' => "qhook", + '\u{02A1}' => "glottalstopstroke", + '\u{02A2}' => "glottalstopstrokereversed", + '\u{02A3}' => "dzaltone", + '\u{02A4}' => "dezh", + '\u{02A5}' => "dzcurl", + '\u{02A6}' => "ts", + '\u{02A7}' => "tesh", + '\u{02A8}' => "tccurl", + '\u{02B0}' => "hsuperior", + '\u{02B1}' => "hhooksuperior", + '\u{02B2}' => "jsuperior", + '\u{02B4}' => "rturnedsuperior", + '\u{02B5}' => "rhookturnedsuperior", + '\u{02B6}' => "Rsmallinvertedsuperior", + '\u{02B7}' => "wsuperior", + '\u{02B8}' => "ysuperior", + '\u{02B9}' => "primemod", + '\u{02BA}' => "dblprimemod", + '\u{02BB}' => "commaturnedmod", + '\u{02BC}' => "afii57929", + '\u{02BD}' => "afii64937", + '\u{02BE}' => "ringhalfright", + '\u{02BF}' => "ringhalfleft", + '\u{02C0}' => "glottalstopmod", + '\u{02C1}' => "glottalstopreversedmod", + '\u{02C2}' => "arrowheadleftmod", + '\u{02C3}' => "arrowheadrightmod", + '\u{02C4}' => "arrowheadupmod", + '\u{02C5}' => "arrowheaddownmod", + '\u{02C6}' => "circumflex", + '\u{02C7}' => "caron", + '\u{02C8}' => "verticallinemod", + '\u{02C9}' => "firsttonechinese", + '\u{02CA}' => "secondtonechinese", + '\u{02CB}' => "fourthtonechinese", + '\u{02CC}' => "verticallinelowmod", + '\u{02CD}' => "macronlowmod", + '\u{02CE}' => "gravelowmod", + '\u{02CF}' => "acutelowmod", + '\u{02D0}' => "colontriangularmod", + '\u{02D1}' => "colontriangularhalfmod", + '\u{02D2}' => "ringhalfrightcentered", + '\u{02D3}' => "ringhalfleftcentered", + '\u{02D4}' => "uptackmod", + '\u{02D5}' => "downtackmod", + '\u{02D6}' => "plusmod", + '\u{02D7}' => "minusmod", + '\u{02D8}' => "breve", + '\u{02D9}' => "dotaccent", + '\u{02DA}' => "ring", + '\u{02DB}' => "ogonek", + '\u{02DC}' => "ilde", + '\u{02DD}' => "hungarumlaut", + '\u{02DE}' => "rhotichookmod", + '\u{02E0}' => "gammasuperior", + '\u{02E3}' => "xsuperior", + '\u{02E4}' => "glottalstopreversedsuperior", + '\u{02E5}' => "tonebarextrahighmod", + '\u{02E6}' => "tonebarhighmod", + '\u{02E7}' => "tonebarmidmod", + '\u{02E8}' => "tonebarlowmod", + '\u{02E9}' => "tonebarextralowmod", + '\u{0300}' => "gravecmb", + '\u{0301}' => "acutecmb", + '\u{0302}' => "circumflexcmb", + '\u{0303}' => "tildecmb", + '\u{0304}' => "macroncmb", + '\u{0305}' => "overlinecmb", + '\u{0306}' => "brevecmb", + '\u{0307}' => "dotaccentcmb", + '\u{0308}' => "dieresiscmb", + '\u{0309}' => "hookabovecomb", + '\u{030A}' => "ringcmb", + '\u{030B}' => "hungarumlautcmb", + '\u{030C}' => "caroncmb", + '\u{030D}' => "verticallineabovecmb", + '\u{030E}' => "dblverticallineabovecmb", + '\u{030F}' => "dblgravecmb", + '\u{0310}' => "candrabinducmb", + '\u{0311}' => "breveinvertedcmb", + '\u{0312}' => "commaturnedabovecmb", + '\u{0313}' => "commaabovecmb", + '\u{0314}' => "commareversedabovecmb", + '\u{0315}' => "commaaboverightcmb", + '\u{0316}' => "gravebelowcmb", + '\u{0317}' => "acutebelowcmb", + '\u{0318}' => "lefttackbelowcmb", + '\u{0319}' => "righttackbelowcmb", + '\u{031A}' => "leftangleabovecmb", + '\u{031B}' => "horncmb", + '\u{031C}' => "ringhalfleftbelowcmb", + '\u{031D}' => "uptackbelowcmb", + '\u{031E}' => "downtackbelowcmb", + '\u{031F}' => "plusbelowcmb", + '\u{0320}' => "minusbelowcmb", + '\u{0321}' => "hookpalatalizedbelowcmb", + '\u{0322}' => "hookretroflexbelowcmb", + '\u{0323}' => "dotbelowcmb", + '\u{0324}' => "dieresisbelowcmb", + '\u{0325}' => "ringbelowcmb", + '\u{0327}' => "cedillacmb", + '\u{0328}' => "ogonekcmb", + '\u{0329}' => "verticallinebelowcmb", + '\u{032A}' => "bridgebelowcmb", + '\u{032B}' => "dblarchinvertedbelowcmb", + '\u{032C}' => "caronbelowcmb", + '\u{032D}' => "circumflexbelowcmb", + '\u{032E}' => "brevebelowcmb", + '\u{032F}' => "breveinvertedbelowcmb", + '\u{0330}' => "tildebelowcmb", + '\u{0331}' => "macronbelowcmb", + '\u{0332}' => "lowlinecmb", + '\u{0333}' => "dbllowlinecmb", + '\u{0334}' => "tildeoverlaycmb", + '\u{0335}' => "strokeshortoverlaycmb", + '\u{0336}' => "strokelongoverlaycmb", + '\u{0337}' => "solidusshortoverlaycmb", + '\u{0338}' => "soliduslongoverlaycmb", + '\u{0339}' => "ringhalfrightbelowcmb", + '\u{033A}' => "bridgeinvertedbelowcmb", + '\u{033B}' => "squarebelowcmb", + '\u{033C}' => "seagullbelowcmb", + '\u{033D}' => "xabovecmb", + '\u{033E}' => "tildeverticalcmb", + '\u{033F}' => "dbloverlinecmb", + '\u{0340}' => "gravetonecmb", + '\u{0341}' => "acutetonecmb", + '\u{0342}' => "perispomenigreekcmb", + '\u{0343}' => "koroniscmb", + '\u{0344}' => "dialytikatonoscmb", + '\u{0345}' => "ypogegrammenigreekcmb", + '\u{0360}' => "tildedoublecmb", + '\u{0361}' => "breveinverteddoublecmb", + '\u{0374}' => "numeralsigngreek", + '\u{0375}' => "numeralsignlowergreek", + '\u{037A}' => "ypogegrammeni", + '\u{037E}' => "questiongreek", + '\u{0384}' => "tonos", + '\u{0385}' => "dialytikatonos", + '\u{0386}' => "Alphatonos", + '\u{0387}' => "anoteleia", + '\u{0388}' => "Epsilontonos", + '\u{0389}' => "Etatonos", + '\u{038A}' => "Iotatonos", + '\u{038C}' => "Omicrontonos", + '\u{038E}' => "Upsilontonos", + '\u{038F}' => "Omegatonos", + '\u{0390}' => "iotadieresistonos", + '\u{0391}' => "Alpha", + '\u{0392}' => "Beta", + '\u{0393}' => "Gamma", + '\u{0394}' => "Deltagreek", + '\u{0395}' => "Epsilon", + '\u{0396}' => "Zeta", + '\u{0397}' => "Eta", + '\u{0398}' => "Theta", + '\u{0399}' => "Iota", + '\u{039A}' => "Kappa", + '\u{039B}' => "Lambda", + '\u{039C}' => "Mu", + '\u{039D}' => "Nu", + '\u{039E}' => "Xi", + '\u{039F}' => "Omicron", + '\u{03A0}' => "Pi", + '\u{03A1}' => "Rho", + '\u{03A3}' => "Sigma", + '\u{03A4}' => "Tau", + '\u{03A5}' => "Upsilon", + '\u{03A6}' => "Phi", + '\u{03A7}' => "Chi", + '\u{03A8}' => "Psi", + '\u{03A9}' => "Omegagreek", + '\u{03AA}' => "Iotadieresis", + '\u{03AB}' => "Upsilondieresis", + '\u{03AC}' => "alphatonos", + '\u{03AD}' => "epsilontonos", + '\u{03AE}' => "etatonos", + '\u{03AF}' => "iotatonos", + '\u{03B0}' => "upsilondieresistonos", + '\u{03B1}' => "alpha", + '\u{03B2}' => "beta", + '\u{03B3}' => "gamma", + '\u{03B4}' => "delta", + '\u{03B5}' => "epsilon", + '\u{03B6}' => "zeta", + '\u{03B7}' => "eta", + '\u{03B8}' => "theta", + '\u{03B9}' => "iota", + '\u{03BA}' => "kappa", + '\u{03BB}' => "lambda", + '\u{03BC}' => "mugreek", + '\u{03BD}' => "nu", + '\u{03BE}' => "xi", + '\u{03BF}' => "omicron", + '\u{03C0}' => "pi", + '\u{03C1}' => "rho", + '\u{03C2}' => "sigma1", + '\u{03C3}' => "sigma", + '\u{03C4}' => "tau", + '\u{03C5}' => "upsilon", + '\u{03C6}' => "phi", + '\u{03C7}' => "chi", + '\u{03C8}' => "psi", + '\u{03C9}' => "omega", + '\u{03CA}' => "iotadieresis", + '\u{03CB}' => "upsilondieresis", + '\u{03CC}' => "omicrontonos", + '\u{03CD}' => "upsilontonos", + '\u{03CE}' => "omegatonos", + '\u{03D0}' => "betasymbolgreek", + '\u{03D1}' => "theta1", + '\u{03D2}' => "Upsilon1", + '\u{03D3}' => "Upsilonacutehooksymbolgreek", + '\u{03D4}' => "Upsilondieresishooksymbolgreek", + '\u{03D5}' => "phi1", + '\u{03D6}' => "omega1", + '\u{03DA}' => "Stigmagreek", + '\u{03DC}' => "Digammagreek", + '\u{03DE}' => "Koppagreek", + '\u{03E0}' => "Sampigreek", + '\u{03E2}' => "Sheicoptic", + '\u{03E3}' => "sheicoptic", + '\u{03E4}' => "Feicoptic", + '\u{03E5}' => "feicoptic", + '\u{03E6}' => "Kheicoptic", + '\u{03E7}' => "kheicoptic", + '\u{03E8}' => "Horicoptic", + '\u{03E9}' => "horicoptic", + '\u{03EA}' => "Gangiacoptic", + '\u{03EB}' => "gangiacoptic", + '\u{03EC}' => "Shimacoptic", + '\u{03ED}' => "shimacoptic", + '\u{03EE}' => "Deicoptic", + '\u{03EF}' => "deicoptic", + '\u{03F0}' => "kappasymbolgreek", + '\u{03F1}' => "rhosymbolgreek", + '\u{03F2}' => "sigmalunatesymbolgreek", + '\u{03F3}' => "yotgreek", + '\u{0401}' => "Iocyrillic", + '\u{0402}' => "Djecyrillic", + '\u{0403}' => "Gjecyrillic", + '\u{0404}' => "Ecyrillic", + '\u{0405}' => "Dzecyrillic", + '\u{0406}' => "Icyrillic", + '\u{0407}' => "Yicyrillic", + '\u{0408}' => "Jecyrillic", + '\u{0409}' => "Ljecyrillic", + '\u{040A}' => "Njecyrillic", + '\u{040B}' => "Tshecyrillic", + '\u{040C}' => "Kjecyrillic", + '\u{040E}' => "Ushortcyrillic", + '\u{040F}' => "Dzhecyrillic", + '\u{0410}' => "Acyrillic", + '\u{0411}' => "Becyrillic", + '\u{0412}' => "Vecyrillic", + '\u{0413}' => "Gecyrillic", + '\u{0414}' => "Decyrillic", + '\u{0415}' => "Iecyrillic", + '\u{0416}' => "Zhecyrillic", + '\u{0417}' => "Zecyrillic", + '\u{0418}' => "Iicyrillic", + '\u{0419}' => "Iishortcyrillic", + '\u{041A}' => "Kacyrillic", + '\u{041B}' => "Elcyrillic", + '\u{041C}' => "Emcyrillic", + '\u{041D}' => "Encyrillic", + '\u{041E}' => "Ocyrillic", + '\u{041F}' => "Pecyrillic", + '\u{0420}' => "Ercyrillic", + '\u{0421}' => "Escyrillic", + '\u{0422}' => "Tecyrillic", + '\u{0423}' => "Ucyrillic", + '\u{0424}' => "Efcyrillic", + '\u{0425}' => "Khacyrillic", + '\u{0426}' => "Tsecyrillic", + '\u{0427}' => "Checyrillic", + '\u{0428}' => "Shacyrillic", + '\u{0429}' => "Shchacyrillic", + '\u{042A}' => "Hardsigncyrillic", + '\u{042B}' => "Yericyrillic", + '\u{042C}' => "Softsigncyrillic", + '\u{042D}' => "Ereversedcyrillic", + '\u{042E}' => "IUcyrillic", + '\u{042F}' => "IAcyrillic", + '\u{0430}' => "acyrillic", + '\u{0431}' => "afii10066", + '\u{0432}' => "afii10067", + '\u{0433}' => "afii10068", + '\u{0434}' => "afii10069", + '\u{0435}' => "afii10070", + '\u{0436}' => "afii10072", + '\u{0437}' => "afii10073", + '\u{0438}' => "afii10074", + '\u{0439}' => "afii10075", + '\u{043A}' => "afii10076", + '\u{043B}' => "afii10077", + '\u{043C}' => "afii10078", + '\u{043D}' => "afii10079", + '\u{043E}' => "afii10080", + '\u{043F}' => "afii10081", + '\u{0440}' => "afii10082", + '\u{0441}' => "afii10083", + '\u{0442}' => "afii10084", + '\u{0443}' => "afii10085", + '\u{0444}' => "afii10086", + '\u{0445}' => "afii10087", + '\u{0446}' => "afii10088", + '\u{0447}' => "afii10089", + '\u{0448}' => "afii10090", + '\u{0449}' => "afii10091", + '\u{044A}' => "afii10092", + '\u{044B}' => "afii10093", + '\u{044C}' => "afii10094", + '\u{044D}' => "afii10095", + '\u{044E}' => "afii10096", + '\u{044F}' => "afii10097", + '\u{0451}' => "afii10071", + '\u{0452}' => "afii10099", + '\u{0453}' => "afii10100", + '\u{0454}' => "afii10101", + '\u{0455}' => "afii10102", + '\u{0456}' => "afii10103", + '\u{0457}' => "afii10104", + '\u{0458}' => "afii10105", + '\u{0459}' => "afii10106", + '\u{045A}' => "afii10107", + '\u{045B}' => "afii10108", + '\u{045C}' => "afii10109", + '\u{045E}' => "afii10110", + '\u{045F}' => "afii10193", + '\u{0460}' => "Omegacyrillic", + '\u{0461}' => "omegacyrillic", + '\u{0462}' => "Yatcyrillic", + '\u{0463}' => "afii10194", + '\u{0464}' => "Eiotifiedcyrillic", + '\u{0465}' => "eiotifiedcyrillic", + '\u{0466}' => "Yuslittlecyrillic", + '\u{0467}' => "yuslittlecyrillic", + '\u{0468}' => "Yuslittleiotifiedcyrillic", + '\u{0469}' => "yuslittleiotifiedcyrillic", + '\u{046A}' => "Yusbigcyrillic", + '\u{046B}' => "yusbigcyrillic", + '\u{046C}' => "Yusbigiotifiedcyrillic", + '\u{046D}' => "yusbigiotifiedcyrillic", + '\u{046E}' => "Ksicyrillic", + '\u{046F}' => "ksicyrillic", + '\u{0470}' => "Psicyrillic", + '\u{0471}' => "psicyrillic", + '\u{0472}' => "Fitacyrillic", + '\u{0473}' => "afii10195", + '\u{0474}' => "Izhitsacyrillic", + '\u{0475}' => "afii10196", + '\u{0476}' => "Izhitsadblgravecyrillic", + '\u{0477}' => "izhitsadblgravecyrillic", + '\u{0478}' => "Ukcyrillic", + '\u{0479}' => "ukcyrillic", + '\u{047A}' => "Omegaroundcyrillic", + '\u{047B}' => "omegaroundcyrillic", + '\u{047C}' => "Omegatitlocyrillic", + '\u{047D}' => "omegatitlocyrillic", + '\u{047E}' => "Otcyrillic", + '\u{047F}' => "otcyrillic", + '\u{0480}' => "Koppacyrillic", + '\u{0481}' => "koppacyrillic", + '\u{0482}' => "thousandcyrillic", + '\u{0483}' => "titlocyrilliccmb", + '\u{0484}' => "palatalizationcyrilliccmb", + '\u{0485}' => "dasiapneumatacyrilliccmb", + '\u{0486}' => "psilipneumatacyrilliccmb", + '\u{0490}' => "Gheupturncyrillic", + '\u{0491}' => "afii10098", + '\u{0492}' => "Ghestrokecyrillic", + '\u{0493}' => "ghestrokecyrillic", + '\u{0494}' => "Ghemiddlehookcyrillic", + '\u{0495}' => "ghemiddlehookcyrillic", + '\u{0496}' => "Zhedescendercyrillic", + '\u{0497}' => "zhedescendercyrillic", + '\u{0498}' => "Zedescendercyrillic", + '\u{0499}' => "zedescendercyrillic", + '\u{049A}' => "Kadescendercyrillic", + '\u{049B}' => "kadescendercyrillic", + '\u{049C}' => "Kaverticalstrokecyrillic", + '\u{049D}' => "kaverticalstrokecyrillic", + '\u{049E}' => "Kastrokecyrillic", + '\u{049F}' => "kastrokecyrillic", + '\u{04A0}' => "Kabashkircyrillic", + '\u{04A1}' => "kabashkircyrillic", + '\u{04A2}' => "Endescendercyrillic", + '\u{04A3}' => "endescendercyrillic", + '\u{04A4}' => "Enghecyrillic", + '\u{04A5}' => "enghecyrillic", + '\u{04A6}' => "Pemiddlehookcyrillic", + '\u{04A7}' => "pemiddlehookcyrillic", + '\u{04A8}' => "Haabkhasiancyrillic", + '\u{04A9}' => "haabkhasiancyrillic", + '\u{04AA}' => "Esdescendercyrillic", + '\u{04AB}' => "esdescendercyrillic", + '\u{04AC}' => "Tedescendercyrillic", + '\u{04AD}' => "tedescendercyrillic", + '\u{04AE}' => "Ustraightcyrillic", + '\u{04AF}' => "ustraightcyrillic", + '\u{04B0}' => "Ustraightstrokecyrillic", + '\u{04B1}' => "ustraightstrokecyrillic", + '\u{04B2}' => "Hadescendercyrillic", + '\u{04B3}' => "hadescendercyrillic", + '\u{04B4}' => "Tetsecyrillic", + '\u{04B5}' => "tetsecyrillic", + '\u{04B6}' => "Chedescendercyrillic", + '\u{04B7}' => "chedescendercyrillic", + '\u{04B8}' => "Cheverticalstrokecyrillic", + '\u{04B9}' => "cheverticalstrokecyrillic", + '\u{04BA}' => "Shhacyrillic", + '\u{04BB}' => "shhacyrillic", + '\u{04BC}' => "Cheabkhasiancyrillic", + '\u{04BD}' => "cheabkhasiancyrillic", + '\u{04BE}' => "Chedescenderabkhasiancyrillic", + '\u{04BF}' => "chedescenderabkhasiancyrillic", + '\u{04C0}' => "palochkacyrillic", + '\u{04C1}' => "Zhebrevecyrillic", + '\u{04C2}' => "zhebrevecyrillic", + '\u{04C3}' => "Kahookcyrillic", + '\u{04C4}' => "kahookcyrillic", + '\u{04C7}' => "Enhookcyrillic", + '\u{04C8}' => "enhookcyrillic", + '\u{04CB}' => "Chekhakassiancyrillic", + '\u{04CC}' => "chekhakassiancyrillic", + '\u{04D0}' => "Abrevecyrillic", + '\u{04D1}' => "abrevecyrillic", + '\u{04D2}' => "Adieresiscyrillic", + '\u{04D3}' => "adieresiscyrillic", + '\u{04D4}' => "Aiecyrillic", + '\u{04D5}' => "aiecyrillic", + '\u{04D6}' => "Iebrevecyrillic", + '\u{04D7}' => "iebrevecyrillic", + '\u{04D8}' => "Schwacyrillic", + '\u{04D9}' => "afii10846", + '\u{04DA}' => "Schwadieresiscyrillic", + '\u{04DB}' => "schwadieresiscyrillic", + '\u{04DC}' => "Zhedieresiscyrillic", + '\u{04DD}' => "zhedieresiscyrillic", + '\u{04DE}' => "Zedieresiscyrillic", + '\u{04DF}' => "zedieresiscyrillic", + '\u{04E0}' => "Dzeabkhasiancyrillic", + '\u{04E1}' => "dzeabkhasiancyrillic", + '\u{04E2}' => "Imacroncyrillic", + '\u{04E3}' => "imacroncyrillic", + '\u{04E4}' => "Idieresiscyrillic", + '\u{04E5}' => "idieresiscyrillic", + '\u{04E6}' => "Odieresiscyrillic", + '\u{04E7}' => "odieresiscyrillic", + '\u{04E8}' => "Obarredcyrillic", + '\u{04E9}' => "obarredcyrillic", + '\u{04EA}' => "Obarreddieresiscyrillic", + '\u{04EB}' => "obarreddieresiscyrillic", + '\u{04EE}' => "Umacroncyrillic", + '\u{04EF}' => "umacroncyrillic", + '\u{04F0}' => "Udieresiscyrillic", + '\u{04F1}' => "udieresiscyrillic", + '\u{04F2}' => "Uhungarumlautcyrillic", + '\u{04F3}' => "uhungarumlautcyrillic", + '\u{04F4}' => "Chedieresiscyrillic", + '\u{04F5}' => "chedieresiscyrillic", + '\u{04F8}' => "Yerudieresiscyrillic", + '\u{04F9}' => "yerudieresiscyrillic", + '\u{0531}' => "Aybarmenian", + '\u{0532}' => "Benarmenian", + '\u{0533}' => "Gimarmenian", + '\u{0534}' => "Daarmenian", + '\u{0535}' => "Echarmenian", + '\u{0536}' => "Zaarmenian", + '\u{0537}' => "Eharmenian", + '\u{0538}' => "Etarmenian", + '\u{0539}' => "Toarmenian", + '\u{053A}' => "Zhearmenian", + '\u{053B}' => "Iniarmenian", + '\u{053C}' => "Liwnarmenian", + '\u{053D}' => "Xeharmenian", + '\u{053E}' => "Caarmenian", + '\u{053F}' => "Kenarmenian", + '\u{0540}' => "Hoarmenian", + '\u{0541}' => "Jaarmenian", + '\u{0542}' => "Ghadarmenian", + '\u{0543}' => "Cheharmenian", + '\u{0544}' => "Menarmenian", + '\u{0545}' => "Yiarmenian", + '\u{0546}' => "Nowarmenian", + '\u{0547}' => "Shaarmenian", + '\u{0548}' => "Voarmenian", + '\u{0549}' => "Chaarmenian", + '\u{054A}' => "Peharmenian", + '\u{054B}' => "Jheharmenian", + '\u{054C}' => "Raarmenian", + '\u{054D}' => "Seharmenian", + '\u{054E}' => "Vewarmenian", + '\u{054F}' => "Tiwnarmenian", + '\u{0550}' => "Reharmenian", + '\u{0551}' => "Coarmenian", + '\u{0552}' => "Yiwnarmenian", + '\u{0553}' => "Piwrarmenian", + '\u{0554}' => "Keharmenian", + '\u{0555}' => "Oharmenian", + '\u{0556}' => "Feharmenian", + '\u{0559}' => "ringhalfleftarmenian", + '\u{055A}' => "apostrophearmenian", + '\u{055B}' => "emphasismarkarmenian", + '\u{055C}' => "exclamarmenian", + '\u{055D}' => "commaarmenian", + '\u{055E}' => "questionarmenian", + '\u{055F}' => "abbreviationmarkarmenian", + '\u{0561}' => "aybarmenian", + '\u{0562}' => "benarmenian", + '\u{0563}' => "gimarmenian", + '\u{0564}' => "daarmenian", + '\u{0565}' => "echarmenian", + '\u{0566}' => "zaarmenian", + '\u{0567}' => "eharmenian", + '\u{0568}' => "etarmenian", + '\u{0569}' => "toarmenian", + '\u{056A}' => "zhearmenian", + '\u{056B}' => "iniarmenian", + '\u{056C}' => "liwnarmenian", + '\u{056D}' => "xeharmenian", + '\u{056E}' => "caarmenian", + '\u{056F}' => "kenarmenian", + '\u{0570}' => "hoarmenian", + '\u{0571}' => "jaarmenian", + '\u{0572}' => "ghadarmenian", + '\u{0573}' => "cheharmenian", + '\u{0574}' => "menarmenian", + '\u{0575}' => "yiarmenian", + '\u{0576}' => "nowarmenian", + '\u{0577}' => "shaarmenian", + '\u{0578}' => "voarmenian", + '\u{0579}' => "chaarmenian", + '\u{057A}' => "peharmenian", + '\u{057B}' => "jheharmenian", + '\u{057C}' => "raarmenian", + '\u{057D}' => "seharmenian", + '\u{057E}' => "vewarmenian", + '\u{057F}' => "tiwnarmenian", + '\u{0580}' => "reharmenian", + '\u{0581}' => "coarmenian", + '\u{0582}' => "yiwnarmenian", + '\u{0583}' => "piwrarmenian", + '\u{0584}' => "keharmenian", + '\u{0585}' => "oharmenian", + '\u{0586}' => "feharmenian", + '\u{0587}' => "echyiwnarmenian", + '\u{0589}' => "periodarmenian", + '\u{0591}' => "etnahtafoukhhebrew", + '\u{0592}' => "segoltahebrew", + '\u{0593}' => "shalshelethebrew", + '\u{0594}' => "zaqefqatanhebrew", + '\u{0595}' => "zaqefgadolhebrew", + '\u{0596}' => "tipehahebrew", + '\u{0597}' => "reviahebrew", + '\u{0598}' => "zarqahebrew", + '\u{0599}' => "pashtahebrew", + '\u{059A}' => "yetivhebrew", + '\u{059B}' => "tevirhebrew", + '\u{059C}' => "gereshaccenthebrew", + '\u{059D}' => "gereshmuqdamhebrew", + '\u{059E}' => "gershayimaccenthebrew", + '\u{059F}' => "qarneyparahebrew", + '\u{05A0}' => "telishagedolahebrew", + '\u{05A1}' => "pazerhebrew", + '\u{05A3}' => "munahhebrew", + '\u{05A4}' => "mahapakhhebrew", + '\u{05A5}' => "merkhahebrew", + '\u{05A6}' => "merkhakefulahebrew", + '\u{05A7}' => "dargahebrew", + '\u{05A8}' => "qadmahebrew", + '\u{05A9}' => "telishaqetanahebrew", + '\u{05AA}' => "yerahbenyomohebrew", + '\u{05AB}' => "olehebrew", + '\u{05AC}' => "iluyhebrew", + '\u{05AD}' => "dehihebrew", + '\u{05AE}' => "zinorhebrew", + '\u{05AF}' => "masoracirclehebrew", + '\u{05B0}' => "afii57799", + '\u{05B1}' => "afii57801", + '\u{05B2}' => "afii57800", + '\u{05B3}' => "afii57802", + '\u{05B4}' => "afii57793", + '\u{05B5}' => "afii57794", + '\u{05B6}' => "afii57795", + '\u{05B7}' => "afii57798", + '\u{05B8}' => "afii57797", + '\u{05B9}' => "afii57806", + '\u{05BB}' => "afii57796", + '\u{05BC}' => "afii57807", + '\u{05BD}' => "afii57839", + '\u{05BE}' => "afii57645", + '\u{05BF}' => "afii57841", + '\u{05C0}' => "afii57842", + '\u{05C1}' => "afii57804", + '\u{05C2}' => "afii57803", + '\u{05C3}' => "afii57658", + '\u{05C4}' => "upperdothebrew", + '\u{05D0}' => "afii57664", + '\u{05D1}' => "afii57665", + '\u{05D2}' => "afii57666", + '\u{05D3}' => "afii57667", + '\u{05D4}' => "afii57668", + '\u{05D5}' => "afii57669", + '\u{05D6}' => "afii57670", + '\u{05D7}' => "afii57671", + '\u{05D8}' => "afii57672", + '\u{05D9}' => "afii57673", + '\u{05DA}' => "afii57674", + '\u{05DB}' => "afii57675", + '\u{05DC}' => "afii57676", + '\u{05DD}' => "afii57677", + '\u{05DE}' => "afii57678", + '\u{05DF}' => "afii57679", + '\u{05E0}' => "afii57680", + '\u{05E1}' => "afii57681", + '\u{05E2}' => "afii57682", + '\u{05E3}' => "afii57683", + '\u{05E4}' => "afii57684", + '\u{05E5}' => "afii57685", + '\u{05E6}' => "afii57686", + '\u{05E7}' => "afii57687", + '\u{05E8}' => "afii57688", + '\u{05E9}' => "afii57689", + '\u{05EA}' => "afii57690", + '\u{05F0}' => "afii57716", + '\u{05F1}' => "afii57717", + '\u{05F2}' => "afii57718", + '\u{05F3}' => "gereshhebrew", + '\u{05F4}' => "gershayimhebrew", + '\u{060C}' => "afii57388", + '\u{061B}' => "afii57403", + '\u{061F}' => "afii57407", + '\u{0621}' => "afii57409", + '\u{0622}' => "afii57410", + '\u{0623}' => "afii57411", + '\u{0624}' => "afii57412", + '\u{0625}' => "afii57413", + '\u{0626}' => "afii57414", + '\u{0627}' => "afii57415", + '\u{0628}' => "afii57416", + '\u{0629}' => "afii57417", + '\u{062A}' => "afii57418", + '\u{062B}' => "afii57419", + '\u{062C}' => "afii57420", + '\u{062D}' => "afii57421", + '\u{062E}' => "afii57422", + '\u{062F}' => "afii57423", + '\u{0630}' => "afii57424", + '\u{0631}' => "afii57425", + '\u{0632}' => "afii57426", + '\u{0633}' => "afii57427", + '\u{0634}' => "afii57428", + '\u{0635}' => "afii57429", + '\u{0636}' => "afii57430", + '\u{0637}' => "afii57431", + '\u{0638}' => "afii57432", + '\u{0639}' => "afii57433", + '\u{063A}' => "afii57434", + '\u{0640}' => "afii57440", + '\u{0641}' => "afii57441", + '\u{0642}' => "afii57442", + '\u{0643}' => "afii57443", + '\u{0644}' => "afii57444", + '\u{0645}' => "afii57445", + '\u{0646}' => "afii57446", + '\u{0647}' => "afii57470", + '\u{0648}' => "afii57448", + '\u{0649}' => "afii57449", + '\u{064A}' => "afii57450", + '\u{064B}' => "afii57451", + '\u{064C}' => "afii57452", + '\u{064D}' => "afii57453", + '\u{064E}' => "afii57454", + '\u{064F}' => "afii57455", + '\u{0650}' => "afii57456", + '\u{0651}' => "afii57457", + '\u{0652}' => "afii57458", + '\u{0660}' => "afii57392", + '\u{0661}' => "afii57393", + '\u{0662}' => "afii57394", + '\u{0663}' => "afii57395", + '\u{0664}' => "afii57396", + '\u{0665}' => "afii57397", + '\u{0666}' => "afii57398", + '\u{0667}' => "afii57399", + '\u{0668}' => "afii57400", + '\u{0669}' => "afii57401", + '\u{066A}' => "afii57381", + '\u{066B}' => "decimalseparatorarabic", + '\u{066C}' => "thousandsseparatorarabic", + '\u{066D}' => "afii63167", + '\u{0679}' => "afii57511", + '\u{067E}' => "afii57506", + '\u{0686}' => "afii57507", + '\u{0688}' => "afii57512", + '\u{0691}' => "afii57513", + '\u{0698}' => "afii57508", + '\u{06A4}' => "afii57505", + '\u{06AF}' => "afii57509", + '\u{06BA}' => "afii57514", + '\u{06C1}' => "haaltonearabic", + '\u{06D1}' => "yehthreedotsbelowarabic", + '\u{06D2}' => "afii57519", + '\u{06D5}' => "afii57534", + '\u{06F0}' => "zeropersian", + '\u{06F1}' => "onepersian", + '\u{06F2}' => "twopersian", + '\u{06F3}' => "threepersian", + '\u{06F4}' => "fourpersian", + '\u{06F5}' => "fivepersian", + '\u{06F6}' => "sixpersian", + '\u{06F7}' => "sevenpersian", + '\u{06F8}' => "eightpersian", + '\u{06F9}' => "ninepersian", + '\u{0901}' => "candrabindudeva", + '\u{0902}' => "anusvaradeva", + '\u{0903}' => "visargadeva", + '\u{0905}' => "adeva", + '\u{0906}' => "aadeva", + '\u{0907}' => "ideva", + '\u{0908}' => "iideva", + '\u{0909}' => "udeva", + '\u{090A}' => "uudeva", + '\u{090B}' => "rvocalicdeva", + '\u{090C}' => "lvocalicdeva", + '\u{090D}' => "ecandradeva", + '\u{090E}' => "eshortdeva", + '\u{090F}' => "edeva", + '\u{0910}' => "aideva", + '\u{0911}' => "ocandradeva", + '\u{0912}' => "oshortdeva", + '\u{0913}' => "odeva", + '\u{0914}' => "audeva", + '\u{0915}' => "kadeva", + '\u{0916}' => "khadeva", + '\u{0917}' => "gadeva", + '\u{0918}' => "ghadeva", + '\u{0919}' => "ngadeva", + '\u{091A}' => "cadeva", + '\u{091B}' => "chadeva", + '\u{091C}' => "jadeva", + '\u{091D}' => "jhadeva", + '\u{091E}' => "nyadeva", + '\u{091F}' => "ttadeva", + '\u{0920}' => "tthadeva", + '\u{0921}' => "ddadeva", + '\u{0922}' => "ddhadeva", + '\u{0923}' => "nnadeva", + '\u{0924}' => "tadeva", + '\u{0925}' => "thadeva", + '\u{0926}' => "dadeva", + '\u{0927}' => "dhadeva", + '\u{0928}' => "nadeva", + '\u{0929}' => "nnnadeva", + '\u{092A}' => "padeva", + '\u{092B}' => "phadeva", + '\u{092C}' => "badeva", + '\u{092D}' => "bhadeva", + '\u{092E}' => "madeva", + '\u{092F}' => "yadeva", + '\u{0930}' => "radeva", + '\u{0931}' => "rradeva", + '\u{0932}' => "ladeva", + '\u{0933}' => "lladeva", + '\u{0934}' => "llladeva", + '\u{0935}' => "vadeva", + '\u{0936}' => "shadeva", + '\u{0937}' => "ssadeva", + '\u{0938}' => "sadeva", + '\u{0939}' => "hadeva", + '\u{093C}' => "nuktadeva", + '\u{093D}' => "avagrahadeva", + '\u{093E}' => "aavowelsigndeva", + '\u{093F}' => "ivowelsigndeva", + '\u{0940}' => "iivowelsigndeva", + '\u{0941}' => "uvowelsigndeva", + '\u{0942}' => "uuvowelsigndeva", + '\u{0943}' => "rvocalicvowelsigndeva", + '\u{0944}' => "rrvocalicvowelsigndeva", + '\u{0945}' => "ecandravowelsigndeva", + '\u{0946}' => "eshortvowelsigndeva", + '\u{0947}' => "evowelsigndeva", + '\u{0948}' => "aivowelsigndeva", + '\u{0949}' => "ocandravowelsigndeva", + '\u{094A}' => "oshortvowelsigndeva", + '\u{094B}' => "ovowelsigndeva", + '\u{094C}' => "auvowelsigndeva", + '\u{094D}' => "viramadeva", + '\u{0950}' => "omdeva", + '\u{0951}' => "udattadeva", + '\u{0952}' => "anudattadeva", + '\u{0953}' => "gravedeva", + '\u{0954}' => "acutedeva", + '\u{0958}' => "qadeva", + '\u{0959}' => "khhadeva", + '\u{095A}' => "ghhadeva", + '\u{095B}' => "zadeva", + '\u{095C}' => "dddhadeva", + '\u{095D}' => "rhadeva", + '\u{095E}' => "fadeva", + '\u{095F}' => "yyadeva", + '\u{0960}' => "rrvocalicdeva", + '\u{0961}' => "llvocalicdeva", + '\u{0962}' => "lvocalicvowelsigndeva", + '\u{0963}' => "llvocalicvowelsigndeva", + '\u{0964}' => "danda", + '\u{0965}' => "dbldanda", + '\u{0966}' => "zerodeva", + '\u{0967}' => "onedeva", + '\u{0968}' => "twodeva", + '\u{0969}' => "threedeva", + '\u{096A}' => "fourdeva", + '\u{096B}' => "fivedeva", + '\u{096C}' => "sixdeva", + '\u{096D}' => "sevendeva", + '\u{096E}' => "eightdeva", + '\u{096F}' => "ninedeva", + '\u{0970}' => "abbreviationsigndeva", + '\u{0981}' => "candrabindubengali", + '\u{0982}' => "anusvarabengali", + '\u{0983}' => "visargabengali", + '\u{0985}' => "abengali", + '\u{0986}' => "aabengali", + '\u{0987}' => "ibengali", + '\u{0988}' => "iibengali", + '\u{0989}' => "ubengali", + '\u{098A}' => "uubengali", + '\u{098B}' => "rvocalicbengali", + '\u{098C}' => "lvocalicbengali", + '\u{098F}' => "ebengali", + '\u{0990}' => "aibengali", + '\u{0993}' => "obengali", + '\u{0994}' => "aubengali", + '\u{0995}' => "kabengali", + '\u{0996}' => "khabengali", + '\u{0997}' => "gabengali", + '\u{0998}' => "ghabengali", + '\u{0999}' => "ngabengali", + '\u{099A}' => "cabengali", + '\u{099B}' => "chabengali", + '\u{099C}' => "jabengali", + '\u{099D}' => "jhabengali", + '\u{099E}' => "nyabengali", + '\u{099F}' => "ttabengali", + '\u{09A0}' => "tthabengali", + '\u{09A1}' => "ddabengali", + '\u{09A2}' => "ddhabengali", + '\u{09A3}' => "nnabengali", + '\u{09A4}' => "tabengali", + '\u{09A5}' => "thabengali", + '\u{09A6}' => "dabengali", + '\u{09A7}' => "dhabengali", + '\u{09A8}' => "nabengali", + '\u{09AA}' => "pabengali", + '\u{09AB}' => "phabengali", + '\u{09AC}' => "babengali", + '\u{09AD}' => "bhabengali", + '\u{09AE}' => "mabengali", + '\u{09AF}' => "yabengali", + '\u{09B0}' => "rabengali", + '\u{09B2}' => "labengali", + '\u{09B6}' => "shabengali", + '\u{09B7}' => "ssabengali", + '\u{09B8}' => "sabengali", + '\u{09B9}' => "habengali", + '\u{09BC}' => "nuktabengali", + '\u{09BE}' => "aavowelsignbengali", + '\u{09BF}' => "ivowelsignbengali", + '\u{09C0}' => "iivowelsignbengali", + '\u{09C1}' => "uvowelsignbengali", + '\u{09C2}' => "uuvowelsignbengali", + '\u{09C3}' => "rvocalicvowelsignbengali", + '\u{09C4}' => "rrvocalicvowelsignbengali", + '\u{09C7}' => "evowelsignbengali", + '\u{09C8}' => "aivowelsignbengali", + '\u{09CB}' => "ovowelsignbengali", + '\u{09CC}' => "auvowelsignbengali", + '\u{09CD}' => "viramabengali", + '\u{09D7}' => "aulengthmarkbengali", + '\u{09DC}' => "rrabengali", + '\u{09DD}' => "rhabengali", + '\u{09DF}' => "yyabengali", + '\u{09E0}' => "rrvocalicbengali", + '\u{09E1}' => "llvocalicbengali", + '\u{09E2}' => "lvocalicvowelsignbengali", + '\u{09E3}' => "llvocalicvowelsignbengali", + '\u{09E6}' => "zerobengali", + '\u{09E7}' => "onebengali", + '\u{09E8}' => "twobengali", + '\u{09E9}' => "threebengali", + '\u{09EA}' => "fourbengali", + '\u{09EB}' => "fivebengali", + '\u{09EC}' => "sixbengali", + '\u{09ED}' => "sevenbengali", + '\u{09EE}' => "eightbengali", + '\u{09EF}' => "ninebengali", + '\u{09F0}' => "ramiddlediagonalbengali", + '\u{09F1}' => "ralowerdiagonalbengali", + '\u{09F2}' => "rupeemarkbengali", + '\u{09F3}' => "rupeesignbengali", + '\u{09F4}' => "onenumeratorbengali", + '\u{09F5}' => "twonumeratorbengali", + '\u{09F6}' => "threenumeratorbengali", + '\u{09F7}' => "fournumeratorbengali", + '\u{09F8}' => "denominatorminusonenumeratorbengali", + '\u{09F9}' => "sixteencurrencydenominatorbengali", + '\u{09FA}' => "issharbengali", + '\u{0A02}' => "bindigurmukhi", + '\u{0A05}' => "agurmukhi", + '\u{0A06}' => "aagurmukhi", + '\u{0A07}' => "igurmukhi", + '\u{0A08}' => "iigurmukhi", + '\u{0A09}' => "ugurmukhi", + '\u{0A0A}' => "uugurmukhi", + '\u{0A0F}' => "eegurmukhi", + '\u{0A10}' => "aigurmukhi", + '\u{0A13}' => "oogurmukhi", + '\u{0A14}' => "augurmukhi", + '\u{0A15}' => "kagurmukhi", + '\u{0A16}' => "khagurmukhi", + '\u{0A17}' => "gagurmukhi", + '\u{0A18}' => "ghagurmukhi", + '\u{0A19}' => "ngagurmukhi", + '\u{0A1A}' => "cagurmukhi", + '\u{0A1B}' => "chagurmukhi", + '\u{0A1C}' => "jagurmukhi", + '\u{0A1D}' => "jhagurmukhi", + '\u{0A1E}' => "nyagurmukhi", + '\u{0A1F}' => "ttagurmukhi", + '\u{0A20}' => "tthagurmukhi", + '\u{0A21}' => "ddagurmukhi", + '\u{0A22}' => "ddhagurmukhi", + '\u{0A23}' => "nnagurmukhi", + '\u{0A24}' => "tagurmukhi", + '\u{0A25}' => "thagurmukhi", + '\u{0A26}' => "dagurmukhi", + '\u{0A27}' => "dhagurmukhi", + '\u{0A28}' => "nagurmukhi", + '\u{0A2A}' => "pagurmukhi", + '\u{0A2B}' => "phagurmukhi", + '\u{0A2C}' => "bagurmukhi", + '\u{0A2D}' => "bhagurmukhi", + '\u{0A2E}' => "magurmukhi", + '\u{0A2F}' => "yagurmukhi", + '\u{0A30}' => "ragurmukhi", + '\u{0A32}' => "lagurmukhi", + '\u{0A35}' => "vagurmukhi", + '\u{0A36}' => "shagurmukhi", + '\u{0A38}' => "sagurmukhi", + '\u{0A39}' => "hagurmukhi", + '\u{0A3C}' => "nuktagurmukhi", + '\u{0A3E}' => "aamatragurmukhi", + '\u{0A3F}' => "imatragurmukhi", + '\u{0A40}' => "iimatragurmukhi", + '\u{0A41}' => "umatragurmukhi", + '\u{0A42}' => "uumatragurmukhi", + '\u{0A47}' => "eematragurmukhi", + '\u{0A48}' => "aimatragurmukhi", + '\u{0A4B}' => "oomatragurmukhi", + '\u{0A4C}' => "aumatragurmukhi", + '\u{0A4D}' => "halantgurmukhi", + '\u{0A59}' => "khhagurmukhi", + '\u{0A5A}' => "ghhagurmukhi", + '\u{0A5B}' => "zagurmukhi", + '\u{0A5C}' => "rragurmukhi", + '\u{0A5E}' => "fagurmukhi", + '\u{0A66}' => "zerogurmukhi", + '\u{0A67}' => "onegurmukhi", + '\u{0A68}' => "twogurmukhi", + '\u{0A69}' => "threegurmukhi", + '\u{0A6A}' => "fourgurmukhi", + '\u{0A6B}' => "fivegurmukhi", + '\u{0A6C}' => "sixgurmukhi", + '\u{0A6D}' => "sevengurmukhi", + '\u{0A6E}' => "eightgurmukhi", + '\u{0A6F}' => "ninegurmukhi", + '\u{0A70}' => "tippigurmukhi", + '\u{0A71}' => "addakgurmukhi", + '\u{0A72}' => "irigurmukhi", + '\u{0A73}' => "uragurmukhi", + '\u{0A74}' => "ekonkargurmukhi", + '\u{0A81}' => "candrabindugujarati", + '\u{0A82}' => "anusvaragujarati", + '\u{0A83}' => "visargagujarati", + '\u{0A85}' => "agujarati", + '\u{0A86}' => "aagujarati", + '\u{0A87}' => "igujarati", + '\u{0A88}' => "iigujarati", + '\u{0A89}' => "ugujarati", + '\u{0A8A}' => "uugujarati", + '\u{0A8B}' => "rvocalicgujarati", + '\u{0A8D}' => "ecandragujarati", + '\u{0A8F}' => "egujarati", + '\u{0A90}' => "aigujarati", + '\u{0A91}' => "ocandragujarati", + '\u{0A93}' => "ogujarati", + '\u{0A94}' => "augujarati", + '\u{0A95}' => "kagujarati", + '\u{0A96}' => "khagujarati", + '\u{0A97}' => "gagujarati", + '\u{0A98}' => "ghagujarati", + '\u{0A99}' => "ngagujarati", + '\u{0A9A}' => "cagujarati", + '\u{0A9B}' => "chagujarati", + '\u{0A9C}' => "jagujarati", + '\u{0A9D}' => "jhagujarati", + '\u{0A9E}' => "nyagujarati", + '\u{0A9F}' => "ttagujarati", + '\u{0AA0}' => "tthagujarati", + '\u{0AA1}' => "ddagujarati", + '\u{0AA2}' => "ddhagujarati", + '\u{0AA3}' => "nnagujarati", + '\u{0AA4}' => "tagujarati", + '\u{0AA5}' => "thagujarati", + '\u{0AA6}' => "dagujarati", + '\u{0AA7}' => "dhagujarati", + '\u{0AA8}' => "nagujarati", + '\u{0AAA}' => "pagujarati", + '\u{0AAB}' => "phagujarati", + '\u{0AAC}' => "bagujarati", + '\u{0AAD}' => "bhagujarati", + '\u{0AAE}' => "magujarati", + '\u{0AAF}' => "yagujarati", + '\u{0AB0}' => "ragujarati", + '\u{0AB2}' => "lagujarati", + '\u{0AB3}' => "llagujarati", + '\u{0AB5}' => "vagujarati", + '\u{0AB6}' => "shagujarati", + '\u{0AB7}' => "ssagujarati", + '\u{0AB8}' => "sagujarati", + '\u{0AB9}' => "hagujarati", + '\u{0ABC}' => "nuktagujarati", + '\u{0ABE}' => "aavowelsigngujarati", + '\u{0ABF}' => "ivowelsigngujarati", + '\u{0AC0}' => "iivowelsigngujarati", + '\u{0AC1}' => "uvowelsigngujarati", + '\u{0AC2}' => "uuvowelsigngujarati", + '\u{0AC3}' => "rvocalicvowelsigngujarati", + '\u{0AC4}' => "rrvocalicvowelsigngujarati", + '\u{0AC5}' => "ecandravowelsigngujarati", + '\u{0AC7}' => "evowelsigngujarati", + '\u{0AC8}' => "aivowelsigngujarati", + '\u{0AC9}' => "ocandravowelsigngujarati", + '\u{0ACB}' => "ovowelsigngujarati", + '\u{0ACC}' => "auvowelsigngujarati", + '\u{0ACD}' => "viramagujarati", + '\u{0AD0}' => "omgujarati", + '\u{0AE0}' => "rrvocalicgujarati", + '\u{0AE6}' => "zerogujarati", + '\u{0AE7}' => "onegujarati", + '\u{0AE8}' => "twogujarati", + '\u{0AE9}' => "threegujarati", + '\u{0AEA}' => "fourgujarati", + '\u{0AEB}' => "fivegujarati", + '\u{0AEC}' => "sixgujarati", + '\u{0AED}' => "sevengujarati", + '\u{0AEE}' => "eightgujarati", + '\u{0AEF}' => "ninegujarati", + '\u{0E01}' => "kokaithai", + '\u{0E02}' => "khokhaithai", + '\u{0E03}' => "khokhuatthai", + '\u{0E04}' => "khokhwaithai", + '\u{0E05}' => "khokhonthai", + '\u{0E06}' => "khorakhangthai", + '\u{0E07}' => "ngonguthai", + '\u{0E08}' => "chochanthai", + '\u{0E09}' => "chochingthai", + '\u{0E0A}' => "chochangthai", + '\u{0E0B}' => "sosothai", + '\u{0E0C}' => "chochoethai", + '\u{0E0D}' => "yoyingthai", + '\u{0E0E}' => "dochadathai", + '\u{0E0F}' => "topatakthai", + '\u{0E10}' => "thothanthai", + '\u{0E11}' => "thonangmonthothai", + '\u{0E12}' => "thophuthaothai", + '\u{0E13}' => "nonenthai", + '\u{0E14}' => "dodekthai", + '\u{0E15}' => "totaothai", + '\u{0E16}' => "thothungthai", + '\u{0E17}' => "thothahanthai", + '\u{0E18}' => "thothongthai", + '\u{0E19}' => "nonuthai", + '\u{0E1A}' => "bobaimaithai", + '\u{0E1B}' => "poplathai", + '\u{0E1C}' => "phophungthai", + '\u{0E1D}' => "fofathai", + '\u{0E1E}' => "phophanthai", + '\u{0E1F}' => "fofanthai", + '\u{0E20}' => "phosamphaothai", + '\u{0E21}' => "momathai", + '\u{0E22}' => "yoyakthai", + '\u{0E23}' => "roruathai", + '\u{0E24}' => "ruthai", + '\u{0E25}' => "lolingthai", + '\u{0E26}' => "luthai", + '\u{0E27}' => "wowaenthai", + '\u{0E28}' => "sosalathai", + '\u{0E29}' => "sorusithai", + '\u{0E2A}' => "sosuathai", + '\u{0E2B}' => "hohipthai", + '\u{0E2C}' => "lochulathai", + '\u{0E2D}' => "oangthai", + '\u{0E2E}' => "honokhukthai", + '\u{0E2F}' => "paiyannoithai", + '\u{0E30}' => "saraathai", + '\u{0E31}' => "maihanakatthai", + '\u{0E32}' => "saraaathai", + '\u{0E33}' => "saraamthai", + '\u{0E34}' => "saraithai", + '\u{0E35}' => "saraiithai", + '\u{0E36}' => "sarauethai", + '\u{0E37}' => "saraueethai", + '\u{0E38}' => "sarauthai", + '\u{0E39}' => "sarauuthai", + '\u{0E3A}' => "phinthuthai", + '\u{0E3F}' => "bahtthai", + '\u{0E40}' => "saraethai", + '\u{0E41}' => "saraaethai", + '\u{0E42}' => "saraothai", + '\u{0E43}' => "saraaimaimuanthai", + '\u{0E44}' => "saraaimaimalaithai", + '\u{0E45}' => "lakkhangyaothai", + '\u{0E46}' => "maiyamokthai", + '\u{0E47}' => "maitaikhuthai", + '\u{0E48}' => "maiekthai", + '\u{0E49}' => "maithothai", + '\u{0E4A}' => "maitrithai", + '\u{0E4B}' => "maichattawathai", + '\u{0E4C}' => "thanthakhatthai", + '\u{0E4D}' => "nikhahitthai", + '\u{0E4E}' => "yamakkanthai", + '\u{0E4F}' => "fongmanthai", + '\u{0E50}' => "zerothai", + '\u{0E51}' => "onethai", + '\u{0E52}' => "twothai", + '\u{0E53}' => "threethai", + '\u{0E54}' => "fourthai", + '\u{0E55}' => "fivethai", + '\u{0E56}' => "sixthai", + '\u{0E57}' => "seventhai", + '\u{0E58}' => "eightthai", + '\u{0E59}' => "ninethai", + '\u{0E5A}' => "angkhankhuthai", + '\u{0E5B}' => "khomutthai", + '\u{1D40}' => "intercal", + '\u{1E00}' => "Aringbelow", + '\u{1E01}' => "aringbelow", + '\u{1E02}' => "Bdotaccent", + '\u{1E03}' => "bdotaccent", + '\u{1E04}' => "Bdotbelow", + '\u{1E05}' => "bdotbelow", + '\u{1E06}' => "Blinebelow", + '\u{1E07}' => "blinebelow", + '\u{1E08}' => "Ccedillaacute", + '\u{1E09}' => "ccedillaacute", + '\u{1E0A}' => "Ddotaccent", + '\u{1E0B}' => "ddotaccent", + '\u{1E0C}' => "Ddotbelow", + '\u{1E0D}' => "ddotbelow", + '\u{1E0E}' => "Dlinebelow", + '\u{1E0F}' => "dlinebelow", + '\u{1E10}' => "Dcedilla", + '\u{1E11}' => "dcedilla", + '\u{1E12}' => "Dcircumflexbelow", + '\u{1E13}' => "dcircumflexbelow", + '\u{1E14}' => "Emacrongrave", + '\u{1E15}' => "emacrongrave", + '\u{1E16}' => "Emacronacute", + '\u{1E17}' => "emacronacute", + '\u{1E18}' => "Ecircumflexbelow", + '\u{1E19}' => "ecircumflexbelow", + '\u{1E1A}' => "Etildebelow", + '\u{1E1B}' => "etildebelow", + '\u{1E1C}' => "Ecedillabreve", + '\u{1E1D}' => "ecedillabreve", + '\u{1E1E}' => "Fdotaccent", + '\u{1E1F}' => "fdotaccent", + '\u{1E20}' => "Gmacron", + '\u{1E21}' => "gmacron", + '\u{1E22}' => "Hdotaccent", + '\u{1E23}' => "hdotaccent", + '\u{1E24}' => "Hdotbelow", + '\u{1E25}' => "hdotbelow", + '\u{1E26}' => "Hdieresis", + '\u{1E27}' => "hdieresis", + '\u{1E28}' => "Hcedilla", + '\u{1E29}' => "hcedilla", + '\u{1E2A}' => "Hbrevebelow", + '\u{1E2B}' => "hbrevebelow", + '\u{1E2C}' => "Itildebelow", + '\u{1E2D}' => "itildebelow", + '\u{1E2E}' => "Idieresisacute", + '\u{1E2F}' => "idieresisacute", + '\u{1E30}' => "Kacute", + '\u{1E31}' => "kacute", + '\u{1E32}' => "Kdotbelow", + '\u{1E33}' => "kdotbelow", + '\u{1E34}' => "Klinebelow", + '\u{1E35}' => "klinebelow", + '\u{1E36}' => "Ldotbelow", + '\u{1E37}' => "ldotbelow", + '\u{1E38}' => "Ldotbelowmacron", + '\u{1E39}' => "ldotbelowmacron", + '\u{1E3A}' => "Llinebelow", + '\u{1E3B}' => "llinebelow", + '\u{1E3C}' => "Lcircumflexbelow", + '\u{1E3D}' => "lcircumflexbelow", + '\u{1E3E}' => "Macute", + '\u{1E3F}' => "macute", + '\u{1E40}' => "Mdotaccent", + '\u{1E41}' => "mdotaccent", + '\u{1E42}' => "Mdotbelow", + '\u{1E43}' => "mdotbelow", + '\u{1E44}' => "Ndotaccent", + '\u{1E45}' => "ndotaccent", + '\u{1E46}' => "Ndotbelow", + '\u{1E47}' => "ndotbelow", + '\u{1E48}' => "Nlinebelow", + '\u{1E49}' => "nlinebelow", + '\u{1E4A}' => "Ncircumflexbelow", + '\u{1E4B}' => "ncircumflexbelow", + '\u{1E4C}' => "Otildeacute", + '\u{1E4D}' => "otildeacute", + '\u{1E4E}' => "Otildedieresis", + '\u{1E4F}' => "otildedieresis", + '\u{1E50}' => "Omacrongrave", + '\u{1E51}' => "omacrongrave", + '\u{1E52}' => "Omacronacute", + '\u{1E53}' => "omacronacute", + '\u{1E54}' => "Pacute", + '\u{1E55}' => "pacute", + '\u{1E56}' => "Pdotaccent", + '\u{1E57}' => "pdotaccent", + '\u{1E58}' => "Rdotaccent", + '\u{1E59}' => "rdotaccent", + '\u{1E5A}' => "Rdotbelow", + '\u{1E5B}' => "rdotbelow", + '\u{1E5C}' => "Rdotbelowmacron", + '\u{1E5D}' => "rdotbelowmacron", + '\u{1E5E}' => "Rlinebelow", + '\u{1E5F}' => "rlinebelow", + '\u{1E60}' => "Sdotaccent", + '\u{1E61}' => "sdotaccent", + '\u{1E62}' => "Sdotbelow", + '\u{1E63}' => "sdotbelow", + '\u{1E64}' => "Sacutedotaccent", + '\u{1E65}' => "sacutedotaccent", + '\u{1E66}' => "Scarondotaccent", + '\u{1E67}' => "scarondotaccent", + '\u{1E68}' => "Sdotbelowdotaccent", + '\u{1E69}' => "sdotbelowdotaccent", + '\u{1E6A}' => "Tdotaccent", + '\u{1E6B}' => "tdotaccent", + '\u{1E6C}' => "Tdotbelow", + '\u{1E6D}' => "tdotbelow", + '\u{1E6E}' => "Tlinebelow", + '\u{1E6F}' => "tlinebelow", + '\u{1E70}' => "Tcircumflexbelow", + '\u{1E71}' => "tcircumflexbelow", + '\u{1E72}' => "Udieresisbelow", + '\u{1E73}' => "udieresisbelow", + '\u{1E74}' => "Utildebelow", + '\u{1E75}' => "utildebelow", + '\u{1E76}' => "Ucircumflexbelow", + '\u{1E77}' => "ucircumflexbelow", + '\u{1E78}' => "Utildeacute", + '\u{1E79}' => "utildeacute", + '\u{1E7A}' => "Umacrondieresis", + '\u{1E7B}' => "umacrondieresis", + '\u{1E7C}' => "Vtilde", + '\u{1E7D}' => "vtilde", + '\u{1E7E}' => "Vdotbelow", + '\u{1E7F}' => "vdotbelow", + '\u{1E80}' => "Wgrave", + '\u{1E81}' => "wgrave", + '\u{1E82}' => "Wacute", + '\u{1E83}' => "wacute", + '\u{1E84}' => "Wdieresis", + '\u{1E85}' => "wdieresis", + '\u{1E86}' => "Wdotaccent", + '\u{1E87}' => "wdotaccent", + '\u{1E88}' => "Wdotbelow", + '\u{1E89}' => "wdotbelow", + '\u{1E8A}' => "Xdotaccent", + '\u{1E8B}' => "xdotaccent", + '\u{1E8C}' => "Xdieresis", + '\u{1E8D}' => "xdieresis", + '\u{1E8E}' => "Ydotaccent", + '\u{1E8F}' => "ydotaccent", + '\u{1E90}' => "Zcircumflex", + '\u{1E91}' => "zcircumflex", + '\u{1E92}' => "Zdotbelow", + '\u{1E93}' => "zdotbelow", + '\u{1E94}' => "Zlinebelow", + '\u{1E95}' => "zlinebelow", + '\u{1E96}' => "hlinebelow", + '\u{1E97}' => "tdieresis", + '\u{1E98}' => "wring", + '\u{1E99}' => "yring", + '\u{1E9A}' => "arighthalfring", + '\u{1E9B}' => "slongdotaccent", + '\u{1EA0}' => "Adotbelow", + '\u{1EA1}' => "adotbelow", + '\u{1EA2}' => "Ahookabove", + '\u{1EA3}' => "ahookabove", + '\u{1EA4}' => "Acircumflexacute", + '\u{1EA5}' => "acircumflexacute", + '\u{1EA6}' => "Acircumflexgrave", + '\u{1EA7}' => "acircumflexgrave", + '\u{1EA8}' => "Acircumflexhookabove", + '\u{1EA9}' => "acircumflexhookabove", + '\u{1EAA}' => "Acircumflextilde", + '\u{1EAB}' => "acircumflextilde", + '\u{1EAC}' => "Acircumflexdotbelow", + '\u{1EAD}' => "acircumflexdotbelow", + '\u{1EAE}' => "Abreveacute", + '\u{1EAF}' => "abreveacute", + '\u{1EB0}' => "Abrevegrave", + '\u{1EB1}' => "abrevegrave", + '\u{1EB2}' => "Abrevehookabove", + '\u{1EB3}' => "abrevehookabove", + '\u{1EB4}' => "Abrevetilde", + '\u{1EB5}' => "abrevetilde", + '\u{1EB6}' => "Abrevedotbelow", + '\u{1EB7}' => "abrevedotbelow", + '\u{1EB8}' => "Edotbelow", + '\u{1EB9}' => "edotbelow", + '\u{1EBA}' => "Ehookabove", + '\u{1EBB}' => "ehookabove", + '\u{1EBC}' => "Etilde", + '\u{1EBD}' => "etilde", + '\u{1EBE}' => "Ecircumflexacute", + '\u{1EBF}' => "ecircumflexacute", + '\u{1EC0}' => "Ecircumflexgrave", + '\u{1EC1}' => "ecircumflexgrave", + '\u{1EC2}' => "Ecircumflexhookabove", + '\u{1EC3}' => "ecircumflexhookabove", + '\u{1EC4}' => "Ecircumflextilde", + '\u{1EC5}' => "ecircumflextilde", + '\u{1EC6}' => "Ecircumflexdotbelow", + '\u{1EC7}' => "ecircumflexdotbelow", + '\u{1EC8}' => "Ihookabove", + '\u{1EC9}' => "ihookabove", + '\u{1ECA}' => "Idotbelow", + '\u{1ECB}' => "idotbelow", + '\u{1ECC}' => "Odotbelow", + '\u{1ECD}' => "odotbelow", + '\u{1ECE}' => "Ohookabove", + '\u{1ECF}' => "ohookabove", + '\u{1ED0}' => "Ocircumflexacute", + '\u{1ED1}' => "ocircumflexacute", + '\u{1ED2}' => "Ocircumflexgrave", + '\u{1ED3}' => "ocircumflexgrave", + '\u{1ED4}' => "Ocircumflexhookabove", + '\u{1ED5}' => "ocircumflexhookabove", + '\u{1ED6}' => "Ocircumflextilde", + '\u{1ED7}' => "ocircumflextilde", + '\u{1ED8}' => "Ocircumflexdotbelow", + '\u{1ED9}' => "ocircumflexdotbelow", + '\u{1EDA}' => "Ohornacute", + '\u{1EDB}' => "ohornacute", + '\u{1EDC}' => "Ohorngrave", + '\u{1EDD}' => "ohorngrave", + '\u{1EDE}' => "Ohornhookabove", + '\u{1EDF}' => "ohornhookabove", + '\u{1EE0}' => "Ohorntilde", + '\u{1EE1}' => "ohorntilde", + '\u{1EE2}' => "Ohorndotbelow", + '\u{1EE3}' => "ohorndotbelow", + '\u{1EE4}' => "Udotbelow", + '\u{1EE5}' => "udotbelow", + '\u{1EE6}' => "Uhookabove", + '\u{1EE7}' => "uhookabove", + '\u{1EE8}' => "Uhornacute", + '\u{1EE9}' => "uhornacute", + '\u{1EEA}' => "Uhorngrave", + '\u{1EEB}' => "uhorngrave", + '\u{1EEC}' => "Uhornhookabove", + '\u{1EED}' => "uhornhookabove", + '\u{1EEE}' => "Uhorntilde", + '\u{1EEF}' => "uhorntilde", + '\u{1EF0}' => "Uhorndotbelow", + '\u{1EF1}' => "uhorndotbelow", + '\u{1EF2}' => "Ygrave", + '\u{1EF3}' => "ygrave", + '\u{1EF4}' => "Ydotbelow", + '\u{1EF5}' => "ydotbelow", + '\u{1EF6}' => "Yhookabove", + '\u{1EF7}' => "yhookabove", + '\u{1EF8}' => "Ytilde", + '\u{1EF9}' => "ytilde", + '\u{2002}' => "enspace", + '\u{200B}' => "zerowidthspace", + '\u{200C}' => "afii61664", + '\u{200D}' => "afii301", + '\u{200E}' => "afii299", + '\u{200F}' => "afii300", + '\u{2010}' => "hyphentwo", + '\u{2012}' => "figuredash", + '\u{2013}' => "endash", + '\u{2014}' => "emdash", + '\u{2015}' => "afii00208", + '\u{2016}' => "dblverticalbar", + '\u{2017}' => "dbllowline", + '\u{2018}' => "quoteleft", + '\u{2019}' => "quoteright", + '\u{201A}' => "quotesinglbase", + '\u{201B}' => "quoteleftreversed", + '\u{201C}' => "quotedblleft", + '\u{201D}' => "quotedblright", + '\u{201E}' => "quotedblbase", + '\u{2020}' => "dagger", + '\u{2021}' => "daggerdbl", + '\u{2022}' => "bullet", + '\u{2024}' => "onedotenleader", + '\u{2025}' => "twodotenleader", + '\u{2026}' => "ellipsis", + '\u{202C}' => "afii61573", + '\u{202D}' => "afii61574", + '\u{202E}' => "afii61575", + '\u{2030}' => "perthousand", + '\u{2032}' => "minute", + '\u{2033}' => "second", + '\u{2035}' => "primereversed", + '\u{2039}' => "guilsinglleft", + '\u{203A}' => "guilsinglright", + '\u{203B}' => "referencemark", + '\u{203C}' => "exclamdbl", + '\u{203E}' => "overline", + '\u{2042}' => "asterism", + '\u{2044}' => "fraction", + '\u{2070}' => "zerosuperior", + '\u{2074}' => "foursuperior", + '\u{2075}' => "fivesuperior", + '\u{2076}' => "sixsuperior", + '\u{2077}' => "sevensuperior", + '\u{2078}' => "eightsuperior", + '\u{2079}' => "ninesuperior", + '\u{207A}' => "plussuperior", + '\u{207C}' => "equalsuperior", + '\u{207D}' => "parenleftsuperior", + '\u{207E}' => "parenrightsuperior", + '\u{207F}' => "nsuperior", + '\u{2080}' => "zeroinferior", + '\u{2081}' => "oneinferior", + '\u{2082}' => "twoinferior", + '\u{2083}' => "threeinferior", + '\u{2084}' => "fourinferior", + '\u{2085}' => "fiveinferior", + '\u{2086}' => "sixinferior", + '\u{2087}' => "seveninferior", + '\u{2088}' => "eightinferior", + '\u{2089}' => "nineinferior", + '\u{208D}' => "parenleftinferior", + '\u{208E}' => "parenrightinferior", + '\u{20A1}' => "colonmonetary", + '\u{20A2}' => "cruzeiro", + '\u{20A3}' => "franc", + '\u{20A4}' => "afii08941", + '\u{20A7}' => "peseta", + '\u{20A9}' => "won", + '\u{20AA}' => "afii57636", + '\u{20AB}' => "dong", + '\u{20AC}' => "Euro", + '\u{2103}' => "centigrade", + '\u{2105}' => "afii61248", + '\u{2109}' => "fahrenheit", + '\u{2111}' => "Ifraktur", + '\u{2113}' => "afii61289", + '\u{2116}' => "afii61352", + '\u{2118}' => "weierstrass", + '\u{211C}' => "Rfraktur", + '\u{211E}' => "prescription", + '\u{2121}' => "telephone", + '\u{2122}' => "trademark", + '\u{2126}' => "Ohm", + '\u{212B}' => "angstrom", + '\u{212E}' => "estimated", + '\u{2135}' => "aleph", + '\u{2153}' => "onethird", + '\u{2154}' => "twothirds", + '\u{215B}' => "oneeighth", + '\u{215C}' => "threeeighths", + '\u{215D}' => "fiveeighths", + '\u{215E}' => "seveneighths", + '\u{2160}' => "Oneroman", + '\u{2161}' => "Tworoman", + '\u{2162}' => "Threeroman", + '\u{2163}' => "Fourroman", + '\u{2164}' => "Fiveroman", + '\u{2165}' => "Sixroman", + '\u{2166}' => "Sevenroman", + '\u{2167}' => "Eightroman", + '\u{2168}' => "Nineroman", + '\u{2169}' => "Tenroman", + '\u{216A}' => "Elevenroman", + '\u{216B}' => "Twelveroman", + '\u{2170}' => "oneroman", + '\u{2171}' => "tworoman", + '\u{2172}' => "threeroman", + '\u{2173}' => "fourroman", + '\u{2174}' => "fiveroman", + '\u{2175}' => "sixroman", + '\u{2176}' => "sevenroman", + '\u{2177}' => "eightroman", + '\u{2178}' => "nineroman", + '\u{2179}' => "tenroman", + '\u{217A}' => "elevenroman", + '\u{217B}' => "twelveroman", + '\u{2190}' => "arrowleft", + '\u{2191}' => "arrowup", + '\u{2192}' => "arrowright", + '\u{2193}' => "arrowdown", + '\u{2194}' => "arrowboth", + '\u{2195}' => "arrowupdn", + '\u{2196}' => "arrowupleft", + '\u{2197}' => "arrowupright", + '\u{2198}' => "arrowdownright", + '\u{2199}' => "arrowdownleft", + '\u{21A8}' => "arrowupdnbse", + '\u{21A9}' => "arrowhookright", + '\u{21AA}' => "arrowhookleft", + '\u{21B5}' => "carriagereturn", + '\u{21BC}' => "harpoonleftbarbup", + '\u{21BD}' => "arrowleftbothalf", + '\u{21C0}' => "harpoonrightbarbup", + '\u{21C1}' => "arrowrightbothalf", + '\u{21C4}' => "arrowrightoverleft", + '\u{21C5}' => "arrowupleftofdown", + '\u{21C6}' => "arrowleftoverright", + '\u{21CD}' => "arrowleftdblstroke", + '\u{21CF}' => "arrowrightdblstroke", + '\u{21D0}' => "arrowdblleft", + '\u{21D1}' => "arrowdblup", + '\u{21D2}' => "arrowdblright", + '\u{21D3}' => "arrowdbldown", + '\u{21D4}' => "arrowdblboth", + '\u{21DE}' => "pageup", + '\u{21DF}' => "pagedown", + '\u{21E0}' => "arrowdashleft", + '\u{21E1}' => "arrowdashup", + '\u{21E2}' => "arrowdashright", + '\u{21E3}' => "arrowdashdown", + '\u{21E4}' => "arrowtableft", + '\u{21E5}' => "arrowtabright", + '\u{21E6}' => "arrowleftwhite", + '\u{21E7}' => "arrowupwhite", + '\u{21E8}' => "arrowrightwhite", + '\u{21E9}' => "arrowdownwhite", + '\u{21EA}' => "capslock", + '\u{2200}' => "forall", + '\u{2202}' => "partialdiff", + '\u{2203}' => "existential", + '\u{2205}' => "emptyset", + '\u{2206}' => "Delta", + '\u{2207}' => "gradient", + '\u{2208}' => "element", + '\u{2209}' => "notelement", + '\u{220B}' => "suchthat", + '\u{220C}' => "notcontains", + '\u{220F}' => "product", + '\u{2210}' => "coproductdisplay", + '\u{2211}' => "summation", + '\u{2212}' => "minus", + '\u{2213}' => "minusplus", + '\u{2215}' => "divisionslash", + '\u{2216}' => "backslashbig", + '\u{2217}' => "asteriskmath", + '\u{2219}' => "bulletoperator", + '\u{221A}' => "radical", + '\u{221D}' => "proportional", + '\u{221E}' => "infinity", + '\u{221F}' => "orthogonal", + '\u{2220}' => "angle", + '\u{2223}' => "divides", + '\u{2225}' => "parallel", + '\u{2226}' => "notparallel", + '\u{2227}' => "logicaland", + '\u{2228}' => "logicalor", + '\u{2229}' => "intersection", + '\u{222A}' => "union", + '\u{222B}' => "integral", + '\u{222C}' => "dblintegral", + '\u{222E}' => "contourintegral", + '\u{2234}' => "therefore", + '\u{2235}' => "because", + '\u{2236}' => "ratio", + '\u{2237}' => "proportion", + '\u{223C}' => "similar", + '\u{223D}' => "reversedtilde", + '\u{2243}' => "asymptoticallyequal", + '\u{2245}' => "approximatelyequal", + '\u{2248}' => "approxequal", + '\u{224C}' => "allequal", + '\u{2250}' => "approaches", + '\u{2251}' => "geometricallyequal", + '\u{2252}' => "approxequalorimage", + '\u{2253}' => "imageorapproximatelyequal", + '\u{2260}' => "notequal", + '\u{2261}' => "equivalence", + '\u{2262}' => "notidentical", + '\u{2264}' => "lessequal", + '\u{2265}' => "greaterequal", + '\u{2266}' => "lessoverequal", + '\u{2267}' => "greateroverequal", + '\u{226A}' => "muchless", + '\u{226B}' => "muchgreater", + '\u{226E}' => "notless", + '\u{226F}' => "notgreater", + '\u{2270}' => "notlessnorequal", + '\u{2271}' => "notgreaternorequal", + '\u{2272}' => "lessorequivalent", + '\u{2273}' => "greaterorequivalent", + '\u{2276}' => "lessorgreater", + '\u{2277}' => "greaterorless", + '\u{2279}' => "notgreaternorless", + '\u{227A}' => "precedes", + '\u{227B}' => "succeeds", + '\u{2280}' => "notprecedes", + '\u{2281}' => "notsucceeds", + '\u{2282}' => "propersubset", + '\u{2283}' => "propersuperset", + '\u{2284}' => "notsubset", + '\u{2285}' => "notsuperset", + '\u{2286}' => "reflexsubset", + '\u{2287}' => "reflexsuperset", + '\u{228A}' => "subsetnotequal", + '\u{228B}' => "supersetnotequal", + '\u{228E}' => "unionmultidisplay", + '\u{2294}' => "unionsqdisplay", + '\u{2295}' => "circleplus", + '\u{2296}' => "minuscircle", + '\u{2297}' => "circlemultiply", + '\u{2299}' => "circleot", + '\u{22A3}' => "tackleft", + '\u{22A4}' => "tackdown", + '\u{22A5}' => "perpendicular", + '\u{22BF}' => "righttriangle", + '\u{22C2}' => "intersectiondisplay", + '\u{22C3}' => "uniondisplay", + '\u{22C5}' => "dotmath", + '\u{22CE}' => "curlyor", + '\u{22CF}' => "curlyand", + '\u{22DA}' => "lessequalorgreater", + '\u{22DB}' => "greaterequalorless", + '\u{22EE}' => "ellipsisvertical", + '\u{2302}' => "house", + '\u{2303}' => "control", + '\u{2305}' => "projective", + '\u{2308}' => "ceilingleftbig", + '\u{2309}' => "ceilingrightbig", + '\u{230A}' => "floorleftBig", + '\u{230B}' => "floorrightbig", + '\u{2310}' => "logicalnotreversed", + '\u{2312}' => "arc", + '\u{2318}' => "propellor", + '\u{2320}' => "integraltop", + '\u{2321}' => "integralbottom", + '\u{2325}' => "option", + '\u{2326}' => "deleteright", + '\u{2327}' => "clear", + '\u{2329}' => "angleleft", + '\u{232A}' => "angleright", + '\u{232B}' => "deleteleft", + '\u{2423}' => "blank", + '\u{2460}' => "onecircle", + '\u{2461}' => "twocircle", + '\u{2462}' => "threecircle", + '\u{2463}' => "fourcircle", + '\u{2464}' => "fivecircle", + '\u{2465}' => "sixcircle", + '\u{2466}' => "sevencircle", + '\u{2467}' => "eightcircle", + '\u{2468}' => "ninecircle", + '\u{2469}' => "tencircle", + '\u{246A}' => "elevencircle", + '\u{246B}' => "twelvecircle", + '\u{246C}' => "thirteencircle", + '\u{246D}' => "fourteencircle", + '\u{246E}' => "fifteencircle", + '\u{246F}' => "sixteencircle", + '\u{2470}' => "seventeencircle", + '\u{2471}' => "eighteencircle", + '\u{2472}' => "nineteencircle", + '\u{2473}' => "twentycircle", + '\u{2474}' => "oneparen", + '\u{2475}' => "twoparen", + '\u{2476}' => "threeparen", + '\u{2477}' => "fourparen", + '\u{2478}' => "fiveparen", + '\u{2479}' => "sixparen", + '\u{247A}' => "sevenparen", + '\u{247B}' => "eightparen", + '\u{247C}' => "nineparen", + '\u{247D}' => "tenparen", + '\u{247E}' => "elevenparen", + '\u{247F}' => "twelveparen", + '\u{2480}' => "thirteenparen", + '\u{2481}' => "fourteenparen", + '\u{2482}' => "fifteenparen", + '\u{2483}' => "sixteenparen", + '\u{2484}' => "seventeenparen", + '\u{2485}' => "eighteenparen", + '\u{2486}' => "nineteenparen", + '\u{2487}' => "twentyparen", + '\u{2488}' => "oneperiod", + '\u{2489}' => "twoperiod", + '\u{248A}' => "threeperiod", + '\u{248B}' => "fourperiod", + '\u{248C}' => "fiveperiod", + '\u{248D}' => "sixperiod", + '\u{248E}' => "sevenperiod", + '\u{248F}' => "eightperiod", + '\u{2490}' => "nineperiod", + '\u{2491}' => "tenperiod", + '\u{2492}' => "elevenperiod", + '\u{2493}' => "twelveperiod", + '\u{2494}' => "thirteenperiod", + '\u{2495}' => "fourteenperiod", + '\u{2496}' => "fifteenperiod", + '\u{2497}' => "sixteenperiod", + '\u{2498}' => "seventeenperiod", + '\u{2499}' => "eighteenperiod", + '\u{249A}' => "nineteenperiod", + '\u{249B}' => "twentyperiod", + '\u{249C}' => "aparen", + '\u{249D}' => "bparen", + '\u{249E}' => "cparen", + '\u{249F}' => "dparen", + '\u{24A0}' => "eparen", + '\u{24A1}' => "fparen", + '\u{24A2}' => "gparen", + '\u{24A3}' => "hparen", + '\u{24A4}' => "iparen", + '\u{24A5}' => "jparen", + '\u{24A6}' => "kparen", + '\u{24A7}' => "lparen", + '\u{24A8}' => "mparen", + '\u{24A9}' => "nparen", + '\u{24AA}' => "oparen", + '\u{24AB}' => "pparen", + '\u{24AC}' => "qparen", + '\u{24AD}' => "rparen", + '\u{24AE}' => "sparen", + '\u{24AF}' => "tparen", + '\u{24B0}' => "uparen", + '\u{24B1}' => "vparen", + '\u{24B2}' => "wparen", + '\u{24B3}' => "xparen", + '\u{24B4}' => "yparen", + '\u{24B5}' => "zparen", + '\u{24B6}' => "Acircle", + '\u{24B7}' => "Bcircle", + '\u{24B8}' => "Ccircle", + '\u{24B9}' => "Dcircle", + '\u{24BA}' => "Ecircle", + '\u{24BB}' => "Fcircle", + '\u{24BC}' => "Gcircle", + '\u{24BD}' => "Hcircle", + '\u{24BE}' => "Icircle", + '\u{24BF}' => "Jcircle", + '\u{24C0}' => "Kcircle", + '\u{24C1}' => "Lcircle", + '\u{24C2}' => "Mcircle", + '\u{24C3}' => "Ncircle", + '\u{24C4}' => "Ocircle", + '\u{24C5}' => "Pcircle", + '\u{24C6}' => "Qcircle", + '\u{24C7}' => "Rcircle", + '\u{24C8}' => "Scircle", + '\u{24C9}' => "Tcircle", + '\u{24CA}' => "Ucircle", + '\u{24CB}' => "Vcircle", + '\u{24CC}' => "Wcircle", + '\u{24CD}' => "Xcircle", + '\u{24CE}' => "Ycircle", + '\u{24CF}' => "Zcircle", + '\u{24D0}' => "acircle", + '\u{24D1}' => "bcircle", + '\u{24D2}' => "ccircle", + '\u{24D3}' => "dcircle", + '\u{24D4}' => "ecircle", + '\u{24D5}' => "fcircle", + '\u{24D6}' => "gcircle", + '\u{24D7}' => "hcircle", + '\u{24D8}' => "icircle", + '\u{24D9}' => "jcircle", + '\u{24DA}' => "kcircle", + '\u{24DB}' => "lcircle", + '\u{24DC}' => "mcircle", + '\u{24DD}' => "ncircle", + '\u{24DE}' => "ocircle", + '\u{24DF}' => "pcircle", + '\u{24E0}' => "qcircle", + '\u{24E1}' => "rcircle", + '\u{24E2}' => "scircle", + '\u{24E3}' => "tcircle", + '\u{24E4}' => "ucircle", + '\u{24E5}' => "vcircle", + '\u{24E6}' => "wcircle", + '\u{24E7}' => "xcircle", + '\u{24E8}' => "ycircle", + '\u{24E9}' => "zcircle", + '\u{2500}' => "SF100000", + '\u{2502}' => "SF110000", + '\u{250C}' => "SF010000", + '\u{2510}' => "SF030000", + '\u{2514}' => "SF020000", + '\u{2518}' => "SF040000", + '\u{251C}' => "SF080000", + '\u{2524}' => "SF090000", + '\u{252C}' => "SF060000", + '\u{2534}' => "SF070000", + '\u{253C}' => "SF050000", + '\u{2550}' => "SF430000", + '\u{2551}' => "SF240000", + '\u{2552}' => "SF510000", + '\u{2553}' => "SF520000", + '\u{2554}' => "SF390000", + '\u{2555}' => "SF220000", + '\u{2556}' => "SF210000", + '\u{2557}' => "SF250000", + '\u{2558}' => "SF500000", + '\u{2559}' => "SF490000", + '\u{255A}' => "SF380000", + '\u{255B}' => "SF280000", + '\u{255C}' => "SF270000", + '\u{255D}' => "SF260000", + '\u{255E}' => "SF360000", + '\u{255F}' => "SF370000", + '\u{2560}' => "SF420000", + '\u{2561}' => "SF190000", + '\u{2562}' => "SF200000", + '\u{2563}' => "SF230000", + '\u{2564}' => "SF470000", + '\u{2565}' => "SF480000", + '\u{2566}' => "SF410000", + '\u{2567}' => "SF450000", + '\u{2568}' => "SF460000", + '\u{2569}' => "SF400000", + '\u{256A}' => "SF540000", + '\u{256B}' => "SF530000", + '\u{256C}' => "SF440000", + '\u{2580}' => "upblock", + '\u{2584}' => "dnblock", + '\u{2588}' => "block", + '\u{258C}' => "lfblock", + '\u{2590}' => "rtblock", + '\u{2591}' => "ltshade", + '\u{2592}' => "shade", + '\u{2593}' => "dkshade", + '\u{25A0}' => "blacksquare", + '\u{25A1}' => "H22073", + '\u{25A3}' => "squarewhitewithsmallblack", + '\u{25A4}' => "squarehorizontalfill", + '\u{25A5}' => "squareverticalfill", + '\u{25A6}' => "squareorthogonalcrosshatchfill", + '\u{25A7}' => "squareupperlefttolowerrightfill", + '\u{25A8}' => "squareupperrighttolowerleftfill", + '\u{25A9}' => "squarediagonalcrosshatchfill", + '\u{25AA}' => "H18543", + '\u{25AB}' => "H18551", + '\u{25AC}' => "blackrectangle", + '\u{25B2}' => "blackuppointingtriangle", + '\u{25B3}' => "whiteuppointingtriangle", + '\u{25B4}' => "blackuppointingsmalltriangle", + '\u{25B5}' => "whiteuppointingsmalltriangle", + '\u{25B6}' => "blackrightpointingtriangle", + '\u{25B7}' => "whiterightpointingtriangle", + '\u{25B9}' => "whiterightpointingsmalltriangle", + '\u{25BA}' => "blackrightpointingpointer", + '\u{25BC}' => "blackdownpointingtriangle", + '\u{25BD}' => "whitedownpointingtriangle", + '\u{25BF}' => "whitedownpointingsmalltriangle", + '\u{25C0}' => "blackleftpointingtriangle", + '\u{25C1}' => "whiteleftpointingtriangle", + '\u{25C3}' => "whiteleftpointingsmalltriangle", + '\u{25C4}' => "blackleftpointingpointer", + '\u{25C6}' => "blackdiamond", + '\u{25C7}' => "whitediamond", + '\u{25C8}' => "whitediamondcontainingblacksmalldiamond", + '\u{25C9}' => "fisheye", + '\u{25CA}' => "lozenge", + '\u{25CB}' => "circle", + '\u{25CC}' => "dottedcircle", + '\u{25CE}' => "bullseye", + '\u{25CF}' => "H18533", + '\u{25D0}' => "circlewithlefthalfblack", + '\u{25D1}' => "circlewithrighthalfblack", + '\u{25D8}' => "bulletinverse", + '\u{25D9}' => "invcircle", + '\u{25E2}' => "blacklowerrighttriangle", + '\u{25E3}' => "blacklowerlefttriangle", + '\u{25E4}' => "blackupperlefttriangle", + '\u{25E5}' => "blackupperrighttriangle", + '\u{25E6}' => "openbullet", + '\u{25EF}' => "largecircle", + '\u{2605}' => "blackstar", + '\u{2606}' => "whitestar", + '\u{260E}' => "telephoneblack", + '\u{260F}' => "whitetelephone", + '\u{261C}' => "pointingindexleftwhite", + '\u{261D}' => "pointingindexupwhite", + '\u{261E}' => "pointingindexrightwhite", + '\u{261F}' => "pointingindexdownwhite", + '\u{262F}' => "yinyang", + '\u{263A}' => "smileface", + '\u{263B}' => "blacksmilingface", + '\u{263C}' => "compass", + '\u{2640}' => "female", + '\u{2641}' => "earth", + '\u{2642}' => "male", + '\u{2660}' => "spade", + '\u{2661}' => "heartsuitwhite", + '\u{2662}' => "diamondsuitwhite", + '\u{2663}' => "club", + '\u{2664}' => "spadesuitwhite", + '\u{2665}' => "heart", + '\u{2666}' => "diamond", + '\u{2667}' => "clubsuitwhite", + '\u{2668}' => "hotsprings", + '\u{2669}' => "quarternote", + '\u{266A}' => "musicalnote", + '\u{266B}' => "eighthnotebeamed", + '\u{266C}' => "beamedsixteenthnotes", + '\u{266D}' => "musicflatsign", + '\u{266F}' => "musicsharpsign", + '\u{2713}' => "checkmark", + '\u{278A}' => "onecircleinversesansserif", + '\u{278B}' => "twocircleinversesansserif", + '\u{278C}' => "threecircleinversesansserif", + '\u{278D}' => "fourcircleinversesansserif", + '\u{278E}' => "fivecircleinversesansserif", + '\u{278F}' => "sixcircleinversesansserif", + '\u{2790}' => "sevencircleinversesansserif", + '\u{2791}' => "eightcircleinversesansserif", + '\u{2792}' => "ninecircleinversesansserif", + '\u{279E}' => "arrowrightheavy", + '\u{3000}' => "ideographicspace", + '\u{3001}' => "ideographiccomma", + '\u{3002}' => "ideographicperiod", + '\u{3003}' => "dittomark", + '\u{3004}' => "jis", + '\u{3005}' => "ideographiciterationmark", + '\u{3006}' => "ideographicclose", + '\u{3007}' => "ideographiczero", + '\u{3008}' => "anglebracketleft", + '\u{3009}' => "anglebracketright", + '\u{300A}' => "dblanglebracketleft", + '\u{300B}' => "dblanglebracketright", + '\u{300C}' => "cornerbracketleft", + '\u{300D}' => "cornerbracketright", + '\u{300E}' => "whitecornerbracketleft", + '\u{300F}' => "whitecornerbracketright", + '\u{3010}' => "blacklenticularbracketleft", + '\u{3011}' => "blacklenticularbracketright", + '\u{3012}' => "postalmark", + '\u{3013}' => "getamark", + '\u{3014}' => "tortoiseshellbracketleft", + '\u{3015}' => "tortoiseshellbracketright", + '\u{3016}' => "whitelenticularbracketleft", + '\u{3017}' => "whitelenticularbracketright", + '\u{3018}' => "whitetortoiseshellbracketleft", + '\u{3019}' => "whitetortoiseshellbracketright", + '\u{301C}' => "wavedash", + '\u{301D}' => "quotedblprimereversed", + '\u{301E}' => "quotedblprime", + '\u{3020}' => "postalmarkface", + '\u{3021}' => "onehangzhou", + '\u{3022}' => "twohangzhou", + '\u{3023}' => "threehangzhou", + '\u{3024}' => "fourhangzhou", + '\u{3025}' => "fivehangzhou", + '\u{3026}' => "sixhangzhou", + '\u{3027}' => "sevenhangzhou", + '\u{3028}' => "eighthangzhou", + '\u{3029}' => "ninehangzhou", + '\u{3036}' => "circlepostalmark", + '\u{3041}' => "asmallhiragana", + '\u{3042}' => "ahiragana", + '\u{3043}' => "ismallhiragana", + '\u{3044}' => "ihiragana", + '\u{3045}' => "usmallhiragana", + '\u{3046}' => "uhiragana", + '\u{3047}' => "esmallhiragana", + '\u{3048}' => "ehiragana", + '\u{3049}' => "osmallhiragana", + '\u{304A}' => "ohiragana", + '\u{304B}' => "kahiragana", + '\u{304C}' => "gahiragana", + '\u{304D}' => "kihiragana", + '\u{304E}' => "gihiragana", + '\u{304F}' => "kuhiragana", + '\u{3050}' => "guhiragana", + '\u{3051}' => "kehiragana", + '\u{3052}' => "gehiragana", + '\u{3053}' => "kohiragana", + '\u{3054}' => "gohiragana", + '\u{3055}' => "sahiragana", + '\u{3056}' => "zahiragana", + '\u{3057}' => "sihiragana", + '\u{3058}' => "zihiragana", + '\u{3059}' => "suhiragana", + '\u{305A}' => "zuhiragana", + '\u{305B}' => "sehiragana", + '\u{305C}' => "zehiragana", + '\u{305D}' => "sohiragana", + '\u{305E}' => "zohiragana", + '\u{305F}' => "tahiragana", + '\u{3060}' => "dahiragana", + '\u{3061}' => "tihiragana", + '\u{3062}' => "dihiragana", + '\u{3063}' => "tusmallhiragana", + '\u{3064}' => "tuhiragana", + '\u{3065}' => "duhiragana", + '\u{3066}' => "tehiragana", + '\u{3067}' => "dehiragana", + '\u{3068}' => "tohiragana", + '\u{3069}' => "dohiragana", + '\u{306A}' => "nahiragana", + '\u{306B}' => "nihiragana", + '\u{306C}' => "nuhiragana", + '\u{306D}' => "nehiragana", + '\u{306E}' => "nohiragana", + '\u{306F}' => "hahiragana", + '\u{3070}' => "bahiragana", + '\u{3071}' => "pahiragana", + '\u{3072}' => "hihiragana", + '\u{3073}' => "bihiragana", + '\u{3074}' => "pihiragana", + '\u{3075}' => "huhiragana", + '\u{3076}' => "buhiragana", + '\u{3077}' => "puhiragana", + '\u{3078}' => "hehiragana", + '\u{3079}' => "behiragana", + '\u{307A}' => "pehiragana", + '\u{307B}' => "hohiragana", + '\u{307C}' => "bohiragana", + '\u{307D}' => "pohiragana", + '\u{307E}' => "mahiragana", + '\u{307F}' => "mihiragana", + '\u{3080}' => "muhiragana", + '\u{3081}' => "mehiragana", + '\u{3082}' => "mohiragana", + '\u{3083}' => "yasmallhiragana", + '\u{3084}' => "yahiragana", + '\u{3085}' => "yusmallhiragana", + '\u{3086}' => "yuhiragana", + '\u{3087}' => "yosmallhiragana", + '\u{3088}' => "yohiragana", + '\u{3089}' => "rahiragana", + '\u{308A}' => "rihiragana", + '\u{308B}' => "ruhiragana", + '\u{308C}' => "rehiragana", + '\u{308D}' => "rohiragana", + '\u{308E}' => "wasmallhiragana", + '\u{308F}' => "wahiragana", + '\u{3090}' => "wihiragana", + '\u{3091}' => "wehiragana", + '\u{3092}' => "wohiragana", + '\u{3093}' => "nhiragana", + '\u{3094}' => "vuhiragana", + '\u{309B}' => "voicedmarkkana", + '\u{309C}' => "semivoicedmarkkana", + '\u{309D}' => "iterationhiragana", + '\u{309E}' => "voicediterationhiragana", + '\u{30A1}' => "asmallkatakana", + '\u{30A2}' => "akatakana", + '\u{30A3}' => "ismallkatakana", + '\u{30A4}' => "ikatakana", + '\u{30A5}' => "usmallkatakana", + '\u{30A6}' => "ukatakana", + '\u{30A7}' => "esmallkatakana", + '\u{30A8}' => "ekatakana", + '\u{30A9}' => "osmallkatakana", + '\u{30AA}' => "okatakana", + '\u{30AB}' => "kakatakana", + '\u{30AC}' => "gakatakana", + '\u{30AD}' => "kikatakana", + '\u{30AE}' => "gikatakana", + '\u{30AF}' => "kukatakana", + '\u{30B0}' => "gukatakana", + '\u{30B1}' => "kekatakana", + '\u{30B2}' => "gekatakana", + '\u{30B3}' => "kokatakana", + '\u{30B4}' => "gokatakana", + '\u{30B5}' => "sakatakana", + '\u{30B6}' => "zakatakana", + '\u{30B7}' => "sikatakana", + '\u{30B8}' => "zikatakana", + '\u{30B9}' => "sukatakana", + '\u{30BA}' => "zukatakana", + '\u{30BB}' => "sekatakana", + '\u{30BC}' => "zekatakana", + '\u{30BD}' => "sokatakana", + '\u{30BE}' => "zokatakana", + '\u{30BF}' => "takatakana", + '\u{30C0}' => "dakatakana", + '\u{30C1}' => "tikatakana", + '\u{30C2}' => "dikatakana", + '\u{30C3}' => "tusmallkatakana", + '\u{30C4}' => "tukatakana", + '\u{30C5}' => "dukatakana", + '\u{30C6}' => "tekatakana", + '\u{30C7}' => "dekatakana", + '\u{30C8}' => "tokatakana", + '\u{30C9}' => "dokatakana", + '\u{30CA}' => "nakatakana", + '\u{30CB}' => "nikatakana", + '\u{30CC}' => "nukatakana", + '\u{30CD}' => "nekatakana", + '\u{30CE}' => "nokatakana", + '\u{30CF}' => "hakatakana", + '\u{30D0}' => "bakatakana", + '\u{30D1}' => "pakatakana", + '\u{30D2}' => "hikatakana", + '\u{30D3}' => "bikatakana", + '\u{30D4}' => "pikatakana", + '\u{30D5}' => "hukatakana", + '\u{30D6}' => "bukatakana", + '\u{30D7}' => "pukatakana", + '\u{30D8}' => "hekatakana", + '\u{30D9}' => "bekatakana", + '\u{30DA}' => "pekatakana", + '\u{30DB}' => "hokatakana", + '\u{30DC}' => "bokatakana", + '\u{30DD}' => "pokatakana", + '\u{30DE}' => "makatakana", + '\u{30DF}' => "mikatakana", + '\u{30E0}' => "mukatakana", + '\u{30E1}' => "mekatakana", + '\u{30E2}' => "mokatakana", + '\u{30E3}' => "yasmallkatakana", + '\u{30E4}' => "yakatakana", + '\u{30E5}' => "yusmallkatakana", + '\u{30E6}' => "yukatakana", + '\u{30E7}' => "yosmallkatakana", + '\u{30E8}' => "yokatakana", + '\u{30E9}' => "rakatakana", + '\u{30EA}' => "rikatakana", + '\u{30EB}' => "rukatakana", + '\u{30EC}' => "rekatakana", + '\u{30ED}' => "rokatakana", + '\u{30EE}' => "wasmallkatakana", + '\u{30EF}' => "wakatakana", + '\u{30F0}' => "wikatakana", + '\u{30F1}' => "wekatakana", + '\u{30F2}' => "wokatakana", + '\u{30F3}' => "nkatakana", + '\u{30F4}' => "vukatakana", + '\u{30F5}' => "kasmallkatakana", + '\u{30F6}' => "kesmallkatakana", + '\u{30F7}' => "vakatakana", + '\u{30F8}' => "vikatakana", + '\u{30F9}' => "vekatakana", + '\u{30FA}' => "vokatakana", + '\u{30FB}' => "dotkatakana", + '\u{30FC}' => "prolongedkana", + '\u{30FD}' => "iterationkatakana", + '\u{30FE}' => "voicediterationkatakana", + '\u{3105}' => "bbopomofo", + '\u{3106}' => "pbopomofo", + '\u{3107}' => "mbopomofo", + '\u{3108}' => "fbopomofo", + '\u{3109}' => "dbopomofo", + '\u{310A}' => "tbopomofo", + '\u{310B}' => "nbopomofo", + '\u{310C}' => "lbopomofo", + '\u{310D}' => "gbopomofo", + '\u{310E}' => "kbopomofo", + '\u{310F}' => "hbopomofo", + '\u{3110}' => "jbopomofo", + '\u{3111}' => "qbopomofo", + '\u{3112}' => "xbopomofo", + '\u{3113}' => "zhbopomofo", + '\u{3114}' => "chbopomofo", + '\u{3115}' => "shbopomofo", + '\u{3116}' => "rbopomofo", + '\u{3117}' => "zbopomofo", + '\u{3118}' => "cbopomofo", + '\u{3119}' => "sbopomofo", + '\u{311A}' => "abopomofo", + '\u{311B}' => "obopomofo", + '\u{311C}' => "ebopomofo", + '\u{311D}' => "ehbopomofo", + '\u{311E}' => "aibopomofo", + '\u{311F}' => "eibopomofo", + '\u{3120}' => "aubopomofo", + '\u{3121}' => "oubopomofo", + '\u{3122}' => "anbopomofo", + '\u{3123}' => "enbopomofo", + '\u{3124}' => "angbopomofo", + '\u{3125}' => "engbopomofo", + '\u{3126}' => "erbopomofo", + '\u{3127}' => "ibopomofo", + '\u{3128}' => "ubopomofo", + '\u{3129}' => "iubopomofo", + '\u{3131}' => "kiyeokkorean", + '\u{3132}' => "ssangkiyeokkorean", + '\u{3133}' => "kiyeoksioskorean", + '\u{3134}' => "nieunkorean", + '\u{3135}' => "nieuncieuckorean", + '\u{3136}' => "nieunhieuhkorean", + '\u{3137}' => "tikeutkorean", + '\u{3138}' => "ssangtikeutkorean", + '\u{3139}' => "rieulkorean", + '\u{313A}' => "rieulkiyeokkorean", + '\u{313B}' => "rieulmieumkorean", + '\u{313C}' => "rieulpieupkorean", + '\u{313D}' => "rieulsioskorean", + '\u{313E}' => "rieulthieuthkorean", + '\u{313F}' => "rieulphieuphkorean", + '\u{3140}' => "rieulhieuhkorean", + '\u{3141}' => "mieumkorean", + '\u{3142}' => "pieupkorean", + '\u{3143}' => "ssangpieupkorean", + '\u{3144}' => "pieupsioskorean", + '\u{3145}' => "sioskorean", + '\u{3146}' => "ssangsioskorean", + '\u{3147}' => "ieungkorean", + '\u{3148}' => "cieuckorean", + '\u{3149}' => "ssangcieuckorean", + '\u{314A}' => "chieuchkorean", + '\u{314B}' => "khieukhkorean", + '\u{314C}' => "thieuthkorean", + '\u{314D}' => "phieuphkorean", + '\u{314E}' => "hieuhkorean", + '\u{314F}' => "akorean", + '\u{3150}' => "aekorean", + '\u{3151}' => "yakorean", + '\u{3152}' => "yaekorean", + '\u{3153}' => "eokorean", + '\u{3154}' => "ekorean", + '\u{3155}' => "yeokorean", + '\u{3156}' => "yekorean", + '\u{3157}' => "okorean", + '\u{3158}' => "wakorean", + '\u{3159}' => "waekorean", + '\u{315A}' => "oekorean", + '\u{315B}' => "yokorean", + '\u{315C}' => "ukorean", + '\u{315D}' => "weokorean", + '\u{315E}' => "wekorean", + '\u{315F}' => "wikorean", + '\u{3160}' => "yukorean", + '\u{3161}' => "eukorean", + '\u{3162}' => "yikorean", + '\u{3163}' => "ikorean", + '\u{3164}' => "hangulfiller", + '\u{3165}' => "ssangnieunkorean", + '\u{3166}' => "nieuntikeutkorean", + '\u{3167}' => "nieunsioskorean", + '\u{3168}' => "nieunpansioskorean", + '\u{3169}' => "rieulkiyeoksioskorean", + '\u{316A}' => "rieultikeutkorean", + '\u{316B}' => "rieulpieupsioskorean", + '\u{316C}' => "rieulpansioskorean", + '\u{316D}' => "rieulyeorinhieuhkorean", + '\u{316E}' => "mieumpieupkorean", + '\u{316F}' => "mieumsioskorean", + '\u{3170}' => "mieumpansioskorean", + '\u{3171}' => "kapyeounmieumkorean", + '\u{3172}' => "pieupkiyeokkorean", + '\u{3173}' => "pieuptikeutkorean", + '\u{3174}' => "pieupsioskiyeokkorean", + '\u{3175}' => "pieupsiostikeutkorean", + '\u{3176}' => "pieupcieuckorean", + '\u{3177}' => "pieupthieuthkorean", + '\u{3178}' => "kapyeounpieupkorean", + '\u{3179}' => "kapyeounssangpieupkorean", + '\u{317A}' => "sioskiyeokkorean", + '\u{317B}' => "siosnieunkorean", + '\u{317C}' => "siostikeutkorean", + '\u{317D}' => "siospieupkorean", + '\u{317E}' => "sioscieuckorean", + '\u{317F}' => "pansioskorean", + '\u{3180}' => "ssangieungkorean", + '\u{3181}' => "yesieungkorean", + '\u{3182}' => "yesieungsioskorean", + '\u{3183}' => "yesieungpansioskorean", + '\u{3184}' => "kapyeounphieuphkorean", + '\u{3185}' => "ssanghieuhkorean", + '\u{3186}' => "yeorinhieuhkorean", + '\u{3187}' => "yoyakorean", + '\u{3188}' => "yoyaekorean", + '\u{3189}' => "yoikorean", + '\u{318A}' => "yuyeokorean", + '\u{318B}' => "yuyekorean", + '\u{318C}' => "yuikorean", + '\u{318D}' => "araeakorean", + '\u{318E}' => "araeaekorean", + '\u{3200}' => "kiyeokparenkorean", + '\u{3201}' => "nieunparenkorean", + '\u{3202}' => "tikeutparenkorean", + '\u{3203}' => "rieulparenkorean", + '\u{3204}' => "mieumparenkorean", + '\u{3205}' => "pieupparenkorean", + '\u{3206}' => "siosparenkorean", + '\u{3207}' => "ieungparenkorean", + '\u{3208}' => "cieucparenkorean", + '\u{3209}' => "chieuchparenkorean", + '\u{320A}' => "khieukhparenkorean", + '\u{320B}' => "thieuthparenkorean", + '\u{320C}' => "phieuphparenkorean", + '\u{320D}' => "hieuhparenkorean", + '\u{320E}' => "kiyeokaparenkorean", + '\u{320F}' => "nieunaparenkorean", + '\u{3210}' => "tikeutaparenkorean", + '\u{3211}' => "rieulaparenkorean", + '\u{3212}' => "mieumaparenkorean", + '\u{3213}' => "pieupaparenkorean", + '\u{3214}' => "siosaparenkorean", + '\u{3215}' => "ieungaparenkorean", + '\u{3216}' => "cieucaparenkorean", + '\u{3217}' => "chieuchaparenkorean", + '\u{3218}' => "khieukhaparenkorean", + '\u{3219}' => "thieuthaparenkorean", + '\u{321A}' => "phieuphaparenkorean", + '\u{321B}' => "hieuhaparenkorean", + '\u{321C}' => "cieucuparenkorean", + '\u{3220}' => "oneideographicparen", + '\u{3221}' => "twoideographicparen", + '\u{3222}' => "threeideographicparen", + '\u{3223}' => "fourideographicparen", + '\u{3224}' => "fiveideographicparen", + '\u{3225}' => "sixideographicparen", + '\u{3226}' => "sevenideographicparen", + '\u{3227}' => "eightideographicparen", + '\u{3228}' => "nineideographicparen", + '\u{3229}' => "tenideographicparen", + '\u{322A}' => "ideographicmoonparen", + '\u{322B}' => "ideographicfireparen", + '\u{322C}' => "ideographicwaterparen", + '\u{322D}' => "ideographicwoodparen", + '\u{322E}' => "ideographicmetalparen", + '\u{322F}' => "ideographicearthparen", + '\u{3230}' => "ideographicsunparen", + '\u{3231}' => "ideographicstockparen", + '\u{3232}' => "ideographichaveparen", + '\u{3233}' => "ideographicsocietyparen", + '\u{3234}' => "ideographicnameparen", + '\u{3235}' => "ideographicspecialparen", + '\u{3236}' => "ideographicfinancialparen", + '\u{3237}' => "ideographiccongratulationparen", + '\u{3238}' => "ideographiclaborparen", + '\u{3239}' => "ideographicrepresentparen", + '\u{323A}' => "ideographiccallparen", + '\u{323B}' => "ideographicstudyparen", + '\u{323C}' => "ideographicsuperviseparen", + '\u{323D}' => "ideographicenterpriseparen", + '\u{323E}' => "ideographicresourceparen", + '\u{323F}' => "ideographicallianceparen", + '\u{3240}' => "ideographicfestivalparen", + '\u{3242}' => "ideographicselfparen", + '\u{3243}' => "ideographicreachparen", + '\u{3260}' => "kiyeokcirclekorean", + '\u{3261}' => "nieuncirclekorean", + '\u{3262}' => "tikeutcirclekorean", + '\u{3263}' => "rieulcirclekorean", + '\u{3264}' => "mieumcirclekorean", + '\u{3265}' => "pieupcirclekorean", + '\u{3266}' => "sioscirclekorean", + '\u{3267}' => "ieungcirclekorean", + '\u{3268}' => "cieuccirclekorean", + '\u{3269}' => "chieuchcirclekorean", + '\u{326A}' => "khieukhcirclekorean", + '\u{326B}' => "thieuthcirclekorean", + '\u{326C}' => "phieuphcirclekorean", + '\u{326D}' => "hieuhcirclekorean", + '\u{326E}' => "kiyeokacirclekorean", + '\u{326F}' => "nieunacirclekorean", + '\u{3270}' => "tikeutacirclekorean", + '\u{3271}' => "rieulacirclekorean", + '\u{3272}' => "mieumacirclekorean", + '\u{3273}' => "pieupacirclekorean", + '\u{3274}' => "siosacirclekorean", + '\u{3275}' => "ieungacirclekorean", + '\u{3276}' => "cieucacirclekorean", + '\u{3277}' => "chieuchacirclekorean", + '\u{3278}' => "khieukhacirclekorean", + '\u{3279}' => "thieuthacirclekorean", + '\u{327A}' => "phieuphacirclekorean", + '\u{327B}' => "hieuhacirclekorean", + '\u{327F}' => "koreanstandardsymbol", + '\u{328A}' => "ideographmooncircle", + '\u{328B}' => "ideographfirecircle", + '\u{328C}' => "ideographwatercircle", + '\u{328D}' => "ideographwoodcircle", + '\u{328E}' => "ideographmetalcircle", + '\u{328F}' => "ideographearthcircle", + '\u{3290}' => "ideographsuncircle", + '\u{3294}' => "ideographnamecircle", + '\u{3296}' => "ideographicfinancialcircle", + '\u{3298}' => "ideographiclaborcircle", + '\u{3299}' => "ideographicsecretcircle", + '\u{329D}' => "ideographicexcellentcircle", + '\u{329E}' => "ideographicprintcircle", + '\u{32A3}' => "ideographiccorrectcircle", + '\u{32A4}' => "ideographichighcircle", + '\u{32A5}' => "ideographiccentrecircle", + '\u{32A6}' => "ideographiclowcircle", + '\u{32A7}' => "ideographicleftcircle", + '\u{32A8}' => "ideographicrightcircle", + '\u{32A9}' => "ideographicmedicinecircle", + '\u{3300}' => "apaatosquare", + '\u{3303}' => "aarusquare", + '\u{3305}' => "intisquare", + '\u{330D}' => "karoriisquare", + '\u{3314}' => "kirosquare", + '\u{3315}' => "kiroguramusquare", + '\u{3316}' => "kiromeetorusquare", + '\u{3318}' => "guramusquare", + '\u{331E}' => "kooposquare", + '\u{3322}' => "sentisquare", + '\u{3323}' => "sentosquare", + '\u{3326}' => "dorusquare", + '\u{3327}' => "tonsquare", + '\u{332A}' => "haitusquare", + '\u{332B}' => "paasentosquare", + '\u{3331}' => "birusquare", + '\u{3333}' => "huiitosquare", + '\u{3336}' => "hekutaarusquare", + '\u{3339}' => "herutusquare", + '\u{333B}' => "peezisquare", + '\u{3342}' => "hoonsquare", + '\u{3347}' => "mansyonsquare", + '\u{3349}' => "mirisquare", + '\u{334A}' => "miribaarusquare", + '\u{334D}' => "meetorusquare", + '\u{334E}' => "yaadosquare", + '\u{3351}' => "rittorusquare", + '\u{3357}' => "wattosquare", + '\u{337B}' => "heiseierasquare", + '\u{337C}' => "syouwaerasquare", + '\u{337D}' => "taisyouerasquare", + '\u{337E}' => "meizierasquare", + '\u{337F}' => "corporationsquare", + '\u{3380}' => "paampssquare", + '\u{3381}' => "nasquare", + '\u{3382}' => "muasquare", + '\u{3383}' => "masquare", + '\u{3384}' => "kasquare", + '\u{3385}' => "KBsquare", + '\u{3386}' => "MBsquare", + '\u{3387}' => "GBsquare", + '\u{3388}' => "calsquare", + '\u{3389}' => "kcalsquare", + '\u{338A}' => "pfsquare", + '\u{338B}' => "nfsquare", + '\u{338C}' => "mufsquare", + '\u{338D}' => "mugsquare", + '\u{338E}' => "squaremg", + '\u{338F}' => "squarekg", + '\u{3390}' => "Hzsquare", + '\u{3391}' => "khzsquare", + '\u{3392}' => "mhzsquare", + '\u{3393}' => "ghzsquare", + '\u{3394}' => "thzsquare", + '\u{3395}' => "mulsquare", + '\u{3396}' => "mlsquare", + '\u{3397}' => "dlsquare", + '\u{3398}' => "klsquare", + '\u{3399}' => "fmsquare", + '\u{339A}' => "nmsquare", + '\u{339B}' => "mumsquare", + '\u{339C}' => "squaremm", + '\u{339D}' => "squarecm", + '\u{339E}' => "squarekm", + '\u{339F}' => "mmsquaredsquare", + '\u{33A0}' => "cmsquaredsquare", + '\u{33A1}' => "squaremsquared", + '\u{33A2}' => "kmsquaredsquare", + '\u{33A3}' => "mmcubedsquare", + '\u{33A4}' => "cmcubedsquare", + '\u{33A5}' => "mcubedsquare", + '\u{33A6}' => "kmcubedsquare", + '\u{33A7}' => "moverssquare", + '\u{33A8}' => "moverssquaredsquare", + '\u{33A9}' => "pasquare", + '\u{33AA}' => "kpasquare", + '\u{33AB}' => "mpasquare", + '\u{33AC}' => "gpasquare", + '\u{33AD}' => "radsquare", + '\u{33AE}' => "radoverssquare", + '\u{33AF}' => "radoverssquaredsquare", + '\u{33B0}' => "pssquare", + '\u{33B1}' => "nssquare", + '\u{33B2}' => "mussquare", + '\u{33B3}' => "mssquare", + '\u{33B4}' => "pvsquare", + '\u{33B5}' => "nvsquare", + '\u{33B6}' => "muvsquare", + '\u{33B7}' => "mvsquare", + '\u{33B8}' => "kvsquare", + '\u{33B9}' => "mvmegasquare", + '\u{33BA}' => "pwsquare", + '\u{33BB}' => "nwsquare", + '\u{33BC}' => "muwsquare", + '\u{33BD}' => "mwsquare", + '\u{33BE}' => "kwsquare", + '\u{33BF}' => "mwmegasquare", + '\u{33C0}' => "kohmsquare", + '\u{33C1}' => "mohmsquare", + '\u{33C2}' => "amsquare", + '\u{33C3}' => "bqsquare", + '\u{33C4}' => "squarecc", + '\u{33C5}' => "cdsquare", + '\u{33C6}' => "coverkgsquare", + '\u{33C7}' => "cosquare", + '\u{33C8}' => "dbsquare", + '\u{33C9}' => "gysquare", + '\u{33CA}' => "hasquare", + '\u{33CB}' => "HPsquare", + '\u{33CD}' => "KKsquare", + '\u{33CE}' => "squarekmcapital", + '\u{33CF}' => "ktsquare", + '\u{33D0}' => "lmsquare", + '\u{33D1}' => "squareln", + '\u{33D2}' => "squarelog", + '\u{33D3}' => "lxsquare", + '\u{33D4}' => "mbsquare", + '\u{33D5}' => "squaremil", + '\u{33D6}' => "molsquare", + '\u{33D8}' => "pmsquare", + '\u{33DB}' => "srsquare", + '\u{33DC}' => "svsquare", + '\u{33DD}' => "wbsquare", + '\u{5344}' => "twentyhangzhou", + '\u{F6BE}' => "dotlessj", + '\u{F6BF}' => "LL", + '\u{F6C0}' => "ll", + '\u{F6C3}' => "commaaccent", + '\u{F6C4}' => "afii10063", + '\u{F6C5}' => "afii10064", + '\u{F6C6}' => "afii10192", + '\u{F6C7}' => "afii10831", + '\u{F6C8}' => "afii10832", + '\u{F6C9}' => "Acute", + '\u{F6CA}' => "Caron", + '\u{F6CB}' => "Dieresis", + '\u{F6CC}' => "DieresisAcute", + '\u{F6CD}' => "DieresisGrave", + '\u{F6CE}' => "Grave", + '\u{F6CF}' => "Hungarumlaut", + '\u{F6D0}' => "Macron", + '\u{F6D1}' => "cyrBreve", + '\u{F6D2}' => "cyrFlex", + '\u{F6D3}' => "dblGrave", + '\u{F6D4}' => "cyrbreve", + '\u{F6D5}' => "cyrflex", + '\u{F6D6}' => "dblgrave", + '\u{F6D7}' => "dieresisacute", + '\u{F6D8}' => "dieresisgrave", + '\u{F6D9}' => "copyrightserif", + '\u{F6DA}' => "registerserif", + '\u{F6DB}' => "trademarkserif", + '\u{F6DC}' => "onefitted", + '\u{F6DD}' => "rupiah", + '\u{F6DE}' => "threequartersemdash", + '\u{F6DF}' => "centinferior", + '\u{F6E0}' => "centsuperior", + '\u{F6E1}' => "commainferior", + '\u{F6E2}' => "commasuperior", + '\u{F6E3}' => "dollarinferior", + '\u{F6E4}' => "dollarsuperior", + '\u{F6E5}' => "hypheninferior", + '\u{F6E6}' => "hyphensuperior", + '\u{F6E7}' => "periodinferior", + '\u{F6E8}' => "periodsuperior", + '\u{F6E9}' => "asuperior", + '\u{F6EA}' => "bsuperior", + '\u{F6EB}' => "dsuperior", + '\u{F6EC}' => "esuperior", + '\u{F6ED}' => "isuperior", + '\u{F6EE}' => "lsuperior", + '\u{F6EF}' => "msuperior", + '\u{F6F0}' => "osuperior", + '\u{F6F1}' => "rsuperior", + '\u{F6F2}' => "ssuperior", + '\u{F6F3}' => "tsuperior", + '\u{F6F4}' => "Brevesmall", + '\u{F6F5}' => "Caronsmall", + '\u{F6F6}' => "Circumflexsmall", + '\u{F6F7}' => "Dotaccentsmall", + '\u{F6F8}' => "Hungarumlautsmall", + '\u{F6F9}' => "Lslashsmall", + '\u{F6FA}' => "OEsmall", + '\u{F6FB}' => "Ogoneksmall", + '\u{F6FC}' => "Ringsmall", + '\u{F6FD}' => "Scaronsmall", + '\u{F6FE}' => "Tildesmall", + '\u{F6FF}' => "Zcaronsmall", + '\u{F721}' => "exclamsmall", + '\u{F724}' => "dollaroldstyle", + '\u{F726}' => "ampersandsmall", + '\u{F730}' => "zerooldstyle", + '\u{F731}' => "oneoldstyle", + '\u{F732}' => "twooldstyle", + '\u{F733}' => "threeoldstyle", + '\u{F734}' => "fouroldstyle", + '\u{F735}' => "fiveoldstyle", + '\u{F736}' => "sixoldstyle", + '\u{F737}' => "sevenoldstyle", + '\u{F738}' => "eightoldstyle", + '\u{F739}' => "nineoldstyle", + '\u{F73F}' => "questionsmall", + '\u{F760}' => "Gravesmall", + '\u{F761}' => "Asmall", + '\u{F762}' => "Bsmall", + '\u{F763}' => "Csmall", + '\u{F764}' => "Dsmall", + '\u{F765}' => "Esmall", + '\u{F766}' => "Fsmall", + '\u{F767}' => "Gsmall", + '\u{F768}' => "Hsmall", + '\u{F769}' => "Ismall", + '\u{F76A}' => "Jsmall", + '\u{F76B}' => "Ksmall", + '\u{F76C}' => "Lsmall", + '\u{F76D}' => "Msmall", + '\u{F76E}' => "Nsmall", + '\u{F76F}' => "Osmall", + '\u{F770}' => "Psmall", + '\u{F771}' => "Qsmall", + '\u{F772}' => "Rsmall", + '\u{F773}' => "Ssmall", + '\u{F774}' => "Tsmall", + '\u{F775}' => "Usmall", + '\u{F776}' => "Vsmall", + '\u{F777}' => "Wsmall", + '\u{F778}' => "Xsmall", + '\u{F779}' => "Ysmall", + '\u{F77A}' => "Zsmall", + '\u{F7A1}' => "exclamdownsmall", + '\u{F7A2}' => "centoldstyle", + '\u{F7A8}' => "Dieresissmall", + '\u{F7AF}' => "Macronsmall", + '\u{F7B4}' => "Acutesmall", + '\u{F7B8}' => "Cedillasmall", + '\u{F7BF}' => "questiondownsmall", + '\u{F7E0}' => "Agravesmall", + '\u{F7E1}' => "Aacutesmall", + '\u{F7E2}' => "Acircumflexsmall", + '\u{F7E3}' => "Atildesmall", + '\u{F7E4}' => "Adieresissmall", + '\u{F7E5}' => "Aringsmall", + '\u{F7E6}' => "AEsmall", + '\u{F7E7}' => "Ccedillasmall", + '\u{F7E8}' => "Egravesmall", + '\u{F7E9}' => "Eacutesmall", + '\u{F7EA}' => "Ecircumflexsmall", + '\u{F7EB}' => "Edieresissmall", + '\u{F7EC}' => "Igravesmall", + '\u{F7ED}' => "Iacutesmall", + '\u{F7EE}' => "Icircumflexsmall", + '\u{F7EF}' => "Idieresissmall", + '\u{F7F0}' => "Ethsmall", + '\u{F7F1}' => "Ntildesmall", + '\u{F7F2}' => "Ogravesmall", + '\u{F7F3}' => "Oacutesmall", + '\u{F7F4}' => "Ocircumflexsmall", + '\u{F7F5}' => "Otildesmall", + '\u{F7F6}' => "Odieresissmall", + '\u{F7F8}' => "Oslashsmall", + '\u{F7F9}' => "Ugravesmall", + '\u{F7FA}' => "Uacutesmall", + '\u{F7FB}' => "Ucircumflexsmall", + '\u{F7FC}' => "Udieresissmall", + '\u{F7FD}' => "Yacutesmall", + '\u{F7FE}' => "Thornsmall", + '\u{F7FF}' => "Ydieresissmall", + '\u{F884}' => "maihanakatleftthai", + '\u{F885}' => "saraileftthai", + '\u{F886}' => "saraiileftthai", + '\u{F887}' => "saraueleftthai", + '\u{F888}' => "saraueeleftthai", + '\u{F889}' => "maitaikhuleftthai", + '\u{F88A}' => "maiekupperleftthai", + '\u{F88B}' => "maieklowrightthai", + '\u{F88C}' => "maieklowleftthai", + '\u{F88D}' => "maithoupperleftthai", + '\u{F88E}' => "maitholowrightthai", + '\u{F88F}' => "maitholowleftthai", + '\u{F890}' => "maitriupperleftthai", + '\u{F891}' => "maitrilowrightthai", + '\u{F892}' => "maitrilowleftthai", + '\u{F893}' => "maichattawaupperleftthai", + '\u{F894}' => "maichattawalowrightthai", + '\u{F895}' => "maichattawalowleftthai", + '\u{F896}' => "thanthakhatupperleftthai", + '\u{F897}' => "thanthakhatlowrightthai", + '\u{F898}' => "thanthakhatlowleftthai", + '\u{F899}' => "nikhahitleftthai", + '\u{F8E5}' => "radicalex", + '\u{F8E6}' => "arrowvertex", + '\u{F8E7}' => "arrowhorizex", + '\u{F8E8}' => "registersans", + '\u{F8E9}' => "copyrightsans", + '\u{F8EA}' => "trademarksans", + '\u{F8EB}' => "parenlefttp", + '\u{F8EC}' => "parenleftex", + '\u{F8ED}' => "parenleftbt", + '\u{F8EE}' => "bracketlefttp", + '\u{F8EF}' => "bracketleftex", + '\u{F8F0}' => "bracketleftbt", + '\u{F8F1}' => "bracelefttp", + '\u{F8F2}' => "braceleftmid", + '\u{F8F3}' => "braceleftbt", + '\u{F8F4}' => "braceex", + '\u{F8F5}' => "integralex", + '\u{F8F6}' => "parenrighttp", + '\u{F8F7}' => "parenrightex", + '\u{F8F8}' => "parenrightbt", + '\u{F8F9}' => "bracketrighttp", + '\u{F8FA}' => "bracketrightex", + '\u{F8FB}' => "bracketrightbt", + '\u{F8FC}' => "bracerighttp", + '\u{F8FD}' => "bracerightmid", + '\u{F8FE}' => "bracerightbt", + '\u{F8FF}' => "apple", + '\u{FB00}' => "ff", + '\u{FB01}' => "fi", + '\u{FB02}' => "fl", + '\u{FB03}' => "ffi", + '\u{FB04}' => "ffl", + '\u{FB1F}' => "afii57705", + '\u{FB20}' => "ayinaltonehebrew", + '\u{FB2A}' => "afii57694", + '\u{FB2B}' => "afii57695", + '\u{FB2C}' => "shindageshshindot", + '\u{FB2D}' => "shindageshsindot", + '\u{FB2E}' => "alefpatahhebrew", + '\u{FB2F}' => "alefqamatshebrew", + '\u{FB30}' => "alefdageshhebrew", + '\u{FB31}' => "betdagesh", + '\u{FB32}' => "gimeldagesh", + '\u{FB33}' => "daletdagesh", + '\u{FB34}' => "hedagesh", + '\u{FB35}' => "afii57723", + '\u{FB36}' => "zayindagesh", + '\u{FB38}' => "tetdagesh", + '\u{FB39}' => "yoddagesh", + '\u{FB3A}' => "finalkafdagesh", + '\u{FB3B}' => "kafdagesh", + '\u{FB3C}' => "lameddagesh", + '\u{FB3E}' => "memdagesh", + '\u{FB40}' => "nundagesh", + '\u{FB41}' => "samekhdagesh", + '\u{FB43}' => "pefinaldageshhebrew", + '\u{FB44}' => "pedagesh", + '\u{FB46}' => "tsadidagesh", + '\u{FB47}' => "qofdagesh", + '\u{FB48}' => "reshdageshhebrew", + '\u{FB49}' => "shindagesh", + '\u{FB4A}' => "tavdages", + '\u{FB4B}' => "afii57700", + '\u{FB4C}' => "betrafehebrew", + '\u{FB4D}' => "kafrafehebrew", + '\u{FB4E}' => "perafehebrew", + '\u{FB4F}' => "aleflamedhebrew", + '\u{FB57}' => "pehfinalarabic", + '\u{FB58}' => "pehinitialarabic", + '\u{FB59}' => "pehmedialarabic", + '\u{FB67}' => "ttehfinalarabic", + '\u{FB68}' => "ttehinitialarabic", + '\u{FB69}' => "ttehmedialarabic", + '\u{FB6B}' => "vehfinalarabic", + '\u{FB6C}' => "vehinitialarabic", + '\u{FB6D}' => "vehmedialarabic", + '\u{FB7B}' => "tchehfinalarabic", + '\u{FB7C}' => "tchehinitialarabic", + '\u{FB7D}' => "tchehmedialarabic", + '\u{FB89}' => "ddalfinalarabic", + '\u{FB8B}' => "jehfinalarabic", + '\u{FB8D}' => "rrehfinalarabic", + '\u{FB93}' => "gaffinalarabic", + '\u{FB94}' => "gafinitialarabic", + '\u{FB95}' => "gafmedialarabic", + '\u{FB9F}' => "noonghunnafinalarabic", + '\u{FBA4}' => "hehhamzaaboveisolatedarabic", + '\u{FBA5}' => "hehhamzaabovefinalarabic", + '\u{FBA7}' => "hehfinalaltonearabic", + '\u{FBA8}' => "hehinitialaltonearabic", + '\u{FBA9}' => "hehmedialaltonearabic", + '\u{FBAF}' => "yehbarreefinalarabic", + '\u{FC08}' => "behmeemisolatedarabic", + '\u{FC0B}' => "tehjeemisolatedarabic", + '\u{FC0C}' => "tehhahisolatedarabic", + '\u{FC0E}' => "tehmeemisolatedarabic", + '\u{FC48}' => "meemmeemisolatedarabic", + '\u{FC4B}' => "noonjeemisolatedarabic", + '\u{FC4E}' => "noonmeemisolatedarabic", + '\u{FC58}' => "yehmeemisolatedarabic", + '\u{FC5E}' => "shaddadammatanarabic", + '\u{FC5F}' => "shaddakasratanarabic", + '\u{FC60}' => "shaddafathaarabic", + '\u{FC61}' => "shaddadammaarabic", + '\u{FC62}' => "shaddakasraarabic", + '\u{FC6D}' => "behnoonfinalarabic", + '\u{FC73}' => "tehnoonfinalarabic", + '\u{FC8D}' => "noonnoonfinalarabic", + '\u{FC94}' => "yehnoonfinalarabic", + '\u{FC9F}' => "behmeeminitialarabic", + '\u{FCA1}' => "tehjeeminitialarabic", + '\u{FCA2}' => "tehhahinitialarabic", + '\u{FCA4}' => "tehmeeminitialarabic", + '\u{FCC9}' => "lamjeeminitialarabic", + '\u{FCCA}' => "lamhahinitialarabic", + '\u{FCCB}' => "lamkhahinitialarabic", + '\u{FCCC}' => "lammeeminitialarabic", + '\u{FCD1}' => "meemmeeminitialarabic", + '\u{FCD2}' => "noonjeeminitialarabic", + '\u{FCD5}' => "noonmeeminitialarabic", + '\u{FCDD}' => "yehmeeminitialarabic", + '\u{FD3E}' => "parenleftaltonearabic", + '\u{FD3F}' => "parenrightaltonearabic", + '\u{FD88}' => "lammeemhahinitialarabic", + '\u{FDF2}' => "lamlamhehisolatedarabic", + '\u{FDFA}' => "sallallahoualayhewasallamarabic", + '\u{FE30}' => "twodotleadervertical", + '\u{FE31}' => "emdashvertical", + '\u{FE32}' => "endashvertical", + '\u{FE33}' => "underscorevertical", + '\u{FE34}' => "wavyunderscorevertical", + '\u{FE35}' => "parenleftvertical", + '\u{FE36}' => "parenrightvertical", + '\u{FE37}' => "braceleftvertical", + '\u{FE38}' => "bracerightvertical", + '\u{FE39}' => "tortoiseshellbracketleftvertical", + '\u{FE3A}' => "tortoiseshellbracketrightvertical", + '\u{FE3B}' => "blacklenticularbracketleftvertical", + '\u{FE3C}' => "blacklenticularbracketrightvertical", + '\u{FE3D}' => "dblanglebracketleftvertical", + '\u{FE3E}' => "dblanglebracketrightvertical", + '\u{FE3F}' => "anglebracketleftvertical", + '\u{FE40}' => "anglebracketrightvertical", + '\u{FE41}' => "cornerbracketleftvertical", + '\u{FE42}' => "cornerbracketrightvertical", + '\u{FE43}' => "whitecornerbracketleftvertical", + '\u{FE44}' => "whitecornerbracketrightvertical", + '\u{FE49}' => "overlinedashed", + '\u{FE4A}' => "overlinecenterline", + '\u{FE4B}' => "overlinewavy", + '\u{FE4C}' => "overlinedblwavy", + '\u{FE4D}' => "lowlinedashed", + '\u{FE4E}' => "lowlinecenterline", + '\u{FE4F}' => "underscorewavy", + '\u{FE50}' => "commasmall", + '\u{FE52}' => "periodsmall", + '\u{FE54}' => "semicolonsmall", + '\u{FE55}' => "colonsmall", + '\u{FE59}' => "parenleftsmall", + '\u{FE5A}' => "parenrightsmall", + '\u{FE5B}' => "braceleftsmall", + '\u{FE5C}' => "bracerightsmall", + '\u{FE5D}' => "tortoiseshellbracketleftsmall", + '\u{FE5E}' => "tortoiseshellbracketrightsmall", + '\u{FE5F}' => "numbersignsmall", + '\u{FE61}' => "asterisksmall", + '\u{FE62}' => "plussmall", + '\u{FE63}' => "hyphensmall", + '\u{FE64}' => "lesssmall", + '\u{FE65}' => "greatersmall", + '\u{FE66}' => "equalsmall", + '\u{FE69}' => "dollarsmall", + '\u{FE6A}' => "percentsmall", + '\u{FE6B}' => "atsmall", + '\u{FE82}' => "alefmaddaabovefinalarabic", + '\u{FE84}' => "alefhamzaabovefinalarabic", + '\u{FE86}' => "wawhamzaabovefinalarabic", + '\u{FE88}' => "alefhamzabelowfinalarabic", + '\u{FE8A}' => "yehhamzaabovefinalarabic", + '\u{FE8B}' => "yehhamzaaboveinitialarabic", + '\u{FE8C}' => "yehhamzaabovemedialarabic", + '\u{FE8E}' => "aleffinalarabic", + '\u{FE90}' => "behfinalarabic", + '\u{FE91}' => "behinitialarabic", + '\u{FE92}' => "behmedialarabic", + '\u{FE94}' => "tehmarbutafinalarabic", + '\u{FE96}' => "tehfinalarabic", + '\u{FE97}' => "tehinitialarabic", + '\u{FE98}' => "tehmedialarabic", + '\u{FE9A}' => "thehfinalarabic", + '\u{FE9B}' => "thehinitialarabic", + '\u{FE9C}' => "thehmedialarabic", + '\u{FE9E}' => "jeemfinalarabic", + '\u{FE9F}' => "jeeminitialarabic", + '\u{FEA0}' => "jeemmedialarabic", + '\u{FEA2}' => "hahfinalarabic", + '\u{FEA3}' => "hahinitialarabic", + '\u{FEA4}' => "hahmedialarabic", + '\u{FEA6}' => "khahfinalarabic", + '\u{FEA7}' => "khahinitialarabic", + '\u{FEA8}' => "khahmedialarabic", + '\u{FEAA}' => "dalfinalarabic", + '\u{FEAC}' => "thalfinalarabic", + '\u{FEAE}' => "rehfinalarabic", + '\u{FEB0}' => "zainfinalarabic", + '\u{FEB2}' => "seenfinalarabic", + '\u{FEB3}' => "seeninitialarabic", + '\u{FEB4}' => "seenmedialarabic", + '\u{FEB6}' => "sheenfinalarabic", + '\u{FEB7}' => "sheeninitialarabic", + '\u{FEB8}' => "sheenmedialarabic", + '\u{FEBA}' => "sadfinalarabic", + '\u{FEBB}' => "sadinitialarabic", + '\u{FEBC}' => "sadmedialarabic", + '\u{FEBE}' => "dadfinalarabic", + '\u{FEBF}' => "dadinitialarabic", + '\u{FEC0}' => "dadmedialarabic", + '\u{FEC2}' => "tahfinalarabic", + '\u{FEC3}' => "tahinitialarabic", + '\u{FEC4}' => "tahmedialarabic", + '\u{FEC6}' => "zahfinalarabic", + '\u{FEC7}' => "zahinitialarabic", + '\u{FEC8}' => "zahmedialarabic", + '\u{FECA}' => "ainfinalarabic", + '\u{FECB}' => "aininitialarabic", + '\u{FECC}' => "ainmedialarabic", + '\u{FECE}' => "ghainfinalarabic", + '\u{FECF}' => "ghaininitialarabic", + '\u{FED0}' => "ghainmedialarabic", + '\u{FED2}' => "fehfinalarabic", + '\u{FED3}' => "fehinitialarabic", + '\u{FED4}' => "fehmedialarabic", + '\u{FED6}' => "qaffinalarabic", + '\u{FED7}' => "qafinitialarabic", + '\u{FED8}' => "qafmedialarabic", + '\u{FEDA}' => "kaffinalarabic", + '\u{FEDB}' => "kafinitialarabic", + '\u{FEDC}' => "kafmedialarabic", + '\u{FEDE}' => "lamfinalarabic", + '\u{FEDF}' => "laminitialarabic", + '\u{FEE0}' => "lammedialarabic", + '\u{FEE2}' => "meemfinalarabic", + '\u{FEE3}' => "meeminitialarabic", + '\u{FEE4}' => "meemmedialarabic", + '\u{FEE6}' => "noonfinalarabic", + '\u{FEE7}' => "nooninitialarabic", + '\u{FEE8}' => "noonmedialarabic", + '\u{FEEA}' => "hehfinalalttwoarabic", + '\u{FEEB}' => "hehinitialarabic", + '\u{FEEC}' => "hehmedialarabic", + '\u{FEEE}' => "wawfinalarabic", + '\u{FEF0}' => "alefmaksurafinalarabic", + '\u{FEF2}' => "yehfinalarabic", + '\u{FEF3}' => "alefmaksurainitialarabic", + '\u{FEF4}' => "alefmaksuramedialarabic", + '\u{FEF5}' => "lamalefmaddaaboveisolatedarabic", + '\u{FEF6}' => "lamalefmaddaabovefinalarabic", + '\u{FEF7}' => "lamalefhamzaaboveisolatedarabic", + '\u{FEF8}' => "lamalefhamzaabovefinalarabic", + '\u{FEF9}' => "lamalefhamzabelowisolatedarabic", + '\u{FEFA}' => "lamalefhamzabelowfinalarabic", + '\u{FEFB}' => "lamalefisolatedarabic", + '\u{FEFC}' => "lamaleffinalarabic", + '\u{FEFF}' => "zerowidthjoiner", + '\u{FF01}' => "exclammonospace", + '\u{FF02}' => "quotedblmonospace", + '\u{FF03}' => "numbersignmonospace", + '\u{FF04}' => "dollarmonospace", + '\u{FF05}' => "percentmonospace", + '\u{FF06}' => "ampersandmonospace", + '\u{FF07}' => "quotesinglemonospace", + '\u{FF08}' => "parenleftmonospace", + '\u{FF09}' => "parenrightmonospace", + '\u{FF0A}' => "asteriskmonospace", + '\u{FF0B}' => "plusmonospace", + '\u{FF0C}' => "commamonospace", + '\u{FF0D}' => "hyphenmonospace", + '\u{FF0E}' => "periodmonospace", + '\u{FF0F}' => "slashmonospace", + '\u{FF10}' => "zeromonospace", + '\u{FF11}' => "onemonospace", + '\u{FF12}' => "twomonospace", + '\u{FF13}' => "threemonospace", + '\u{FF14}' => "fourmonospace", + '\u{FF15}' => "fivemonospace", + '\u{FF16}' => "sixmonospace", + '\u{FF17}' => "sevenmonospace", + '\u{FF18}' => "eightmonospace", + '\u{FF19}' => "ninemonospace", + '\u{FF1A}' => "colonmonospace", + '\u{FF1B}' => "semicolonmonospace", + '\u{FF1C}' => "lessmonospace", + '\u{FF1D}' => "equalmonospace", + '\u{FF1E}' => "greatermonospace", + '\u{FF1F}' => "questionmonospace", + '\u{FF20}' => "atmonospace", + '\u{FF21}' => "Amonospace", + '\u{FF22}' => "Bmonospace", + '\u{FF23}' => "Cmonospace", + '\u{FF24}' => "Dmonospace", + '\u{FF25}' => "Emonospace", + '\u{FF26}' => "Fmonospace", + '\u{FF27}' => "Gmonospace", + '\u{FF28}' => "Hmonospace", + '\u{FF29}' => "Imonospace", + '\u{FF2A}' => "Jmonospace", + '\u{FF2B}' => "Kmonospace", + '\u{FF2C}' => "Lmonospace", + '\u{FF2D}' => "Mmonospace", + '\u{FF2E}' => "Nmonospace", + '\u{FF2F}' => "Omonospace", + '\u{FF30}' => "Pmonospace", + '\u{FF31}' => "Qmonospace", + '\u{FF32}' => "Rmonospace", + '\u{FF33}' => "Smonospace", + '\u{FF34}' => "Tmonospace", + '\u{FF35}' => "Umonospace", + '\u{FF36}' => "Vmonospace", + '\u{FF37}' => "Wmonospace", + '\u{FF38}' => "Xmonospace", + '\u{FF39}' => "Ymonospace", + '\u{FF3A}' => "Zmonospace", + '\u{FF3B}' => "bracketleftmonospace", + '\u{FF3C}' => "backslashmonospace", + '\u{FF3D}' => "bracketrightmonospace", + '\u{FF3E}' => "asciicircummonospace", + '\u{FF3F}' => "underscoremonospace", + '\u{FF40}' => "gravemonospace", + '\u{FF41}' => "amonospace", + '\u{FF42}' => "bmonospace", + '\u{FF43}' => "cmonospace", + '\u{FF44}' => "dmonospace", + '\u{FF45}' => "emonospace", + '\u{FF46}' => "fmonospace", + '\u{FF47}' => "gmonospace", + '\u{FF48}' => "hmonospace", + '\u{FF49}' => "imonospace", + '\u{FF4A}' => "jmonospace", + '\u{FF4B}' => "kmonospace", + '\u{FF4C}' => "lmonospace", + '\u{FF4D}' => "mmonospace", + '\u{FF4E}' => "nmonospace", + '\u{FF4F}' => "omonospace", + '\u{FF50}' => "pmonospace", + '\u{FF51}' => "qmonospace", + '\u{FF52}' => "rmonospace", + '\u{FF53}' => "smonospace", + '\u{FF54}' => "tmonospace", + '\u{FF55}' => "umonospace", + '\u{FF56}' => "vmonospace", + '\u{FF57}' => "wmonospace", + '\u{FF58}' => "xmonospace", + '\u{FF59}' => "ymonospace", + '\u{FF5A}' => "zmonospace", + '\u{FF5B}' => "braceleftmonospace", + '\u{FF5C}' => "barmonospace", + '\u{FF5D}' => "bracerightmonospace", + '\u{FF5E}' => "asciitildemonospace", + '\u{FF61}' => "periodhalfwidth", + '\u{FF62}' => "cornerbracketlefthalfwidth", + '\u{FF63}' => "cornerbracketrighthalfwidth", + '\u{FF64}' => "ideographiccommaleft", + '\u{FF65}' => "middledotkatakanahalfwidth", + '\u{FF66}' => "wokatakanahalfwidth", + '\u{FF67}' => "asmallkatakanahalfwidth", + '\u{FF68}' => "ismallkatakanahalfwidth", + '\u{FF69}' => "usmallkatakanahalfwidth", + '\u{FF6A}' => "esmallkatakanahalfwidth", + '\u{FF6B}' => "osmallkatakanahalfwidth", + '\u{FF6C}' => "yasmallkatakanahalfwidth", + '\u{FF6D}' => "yusmallkatakanahalfwidth", + '\u{FF6E}' => "yosmallkatakanahalfwidth", + '\u{FF6F}' => "tusmallkatakanahalfwidth", + '\u{FF70}' => "katahiraprolongmarkhalfwidth", + '\u{FF71}' => "akatakanahalfwidth", + '\u{FF72}' => "ikatakanahalfwidth", + '\u{FF73}' => "ukatakanahalfwidth", + '\u{FF74}' => "ekatakanahalfwidth", + '\u{FF75}' => "okatakanahalfwidth", + '\u{FF76}' => "kakatakanahalfwidth", + '\u{FF77}' => "kikatakanahalfwidth", + '\u{FF78}' => "kukatakanahalfwidth", + '\u{FF79}' => "kekatakanahalfwidth", + '\u{FF7A}' => "kokatakanahalfwidth", + '\u{FF7B}' => "sakatakanahalfwidth", + '\u{FF7C}' => "sikatakanahalfwidth", + '\u{FF7D}' => "sukatakanahalfwidth", + '\u{FF7E}' => "sekatakanahalfwidth", + '\u{FF7F}' => "sokatakanahalfwidth", + '\u{FF80}' => "takatakanahalfwidth", + '\u{FF81}' => "tikatakanahalfwidth", + '\u{FF82}' => "tukatakanahalfwidth", + '\u{FF83}' => "tekatakanahalfwidth", + '\u{FF84}' => "tokatakanahalfwidth", + '\u{FF85}' => "nakatakanahalfwidth", + '\u{FF86}' => "nikatakanahalfwidth", + '\u{FF87}' => "nukatakanahalfwidth", + '\u{FF88}' => "nekatakanahalfwidth", + '\u{FF89}' => "nokatakanahalfwidth", + '\u{FF8A}' => "hakatakanahalfwidth", + '\u{FF8B}' => "hikatakanahalfwidth", + '\u{FF8C}' => "hukatakanahalfwidth", + '\u{FF8D}' => "hekatakanahalfwidth", + '\u{FF8E}' => "hokatakanahalfwidth", + '\u{FF8F}' => "makatakanahalfwidth", + '\u{FF90}' => "mikatakanahalfwidth", + '\u{FF91}' => "mukatakanahalfwidth", + '\u{FF92}' => "mekatakanahalfwidth", + '\u{FF93}' => "mokatakanahalfwidth", + '\u{FF94}' => "yakatakanahalfwidth", + '\u{FF95}' => "yukatakanahalfwidth", + '\u{FF96}' => "yokatakanahalfwidth", + '\u{FF97}' => "rakatakanahalfwidth", + '\u{FF98}' => "rikatakanahalfwidth", + '\u{FF99}' => "rukatakanahalfwidth", + '\u{FF9A}' => "rekatakanahalfwidth", + '\u{FF9B}' => "rokatakanahalfwidth", + '\u{FF9C}' => "wakatakanahalfwidth", + '\u{FF9D}' => "nkatakanahalfwidth", + '\u{FF9E}' => "voicedmarkkanahalfwidth", + '\u{FF9F}' => "semivoicedmarkkanahalfwidth", + '\u{FFE0}' => "centmonospace", + '\u{FFE1}' => "sterlingmonospace", + '\u{FFE3}' => "macronmonospace", + '\u{FFE5}' => "yenmonospace", + '\u{FFE6}' => "wonmonospace", +}; diff --git a/vendor/hayro-interpret/src/font/generated/mac_expert.rs b/vendor/hayro-interpret/src/font/generated/mac_expert.rs new file mode 100644 index 00000000..3c8971b0 --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/mac_expert.rs @@ -0,0 +1,174 @@ +// See +use phf::phf_map; + +pub(crate) fn get(code: u8) -> Option<&'static str> { + MAC_EXPERT.get(&code).copied() +} + +static MAC_EXPERT: phf::Map = phf_map! { + 190_u8 => "AEsmall", + 135_u8 => "Aacutesmall", + 137_u8 => "Acircumflexsmall", + 39_u8 => "Acutesmall", + 138_u8 => "Adieresissmall", + 136_u8 => "Agravesmall", + 140_u8 => "Aringsmall", + 97_u8 => "Asmall", + 139_u8 => "Atildesmall", + 243_u8 => "Brevesmall", + 98_u8 => "Bsmall", + 174_u8 => "Caronsmall", + 141_u8 => "Ccedillasmall", + 201_u8 => "Cedillasmall", + 94_u8 => "Circumflexsmall", + 99_u8 => "Csmall", + 172_u8 => "Dieresissmall", + 250_u8 => "Dotaccentsmall", + 100_u8 => "Dsmall", + 142_u8 => "Eacutesmall", + 144_u8 => "Ecircumflexsmall", + 145_u8 => "Edieresissmall", + 143_u8 => "Egravesmall", + 101_u8 => "Esmall", + 68_u8 => "Ethsmall", + 102_u8 => "Fsmall", + 96_u8 => "Gravesmall", + 103_u8 => "Gsmall", + 104_u8 => "Hsmall", + 34_u8 => "Hungarumlautsmall", + 146_u8 => "Iacutesmall", + 148_u8 => "Icircumflexsmall", + 149_u8 => "Idieresissmall", + 147_u8 => "Igravesmall", + 105_u8 => "Ismall", + 106_u8 => "Jsmall", + 107_u8 => "Ksmall", + 194_u8 => "Lslashsmall", + 108_u8 => "Lsmall", + 244_u8 => "Macronsmall", + 109_u8 => "Msmall", + 110_u8 => "Nsmall", + 150_u8 => "Ntildesmall", + 207_u8 => "OEsmall", + 151_u8 => "Oacutesmall", + 153_u8 => "Ocircumflexsmall", + 154_u8 => "Odieresissmall", + 242_u8 => "Ogoneksmall", + 152_u8 => "Ogravesmall", + 191_u8 => "Oslashsmall", + 111_u8 => "Osmall", + 155_u8 => "Otildesmall", + 112_u8 => "Psmall", + 113_u8 => "Qsmall", + 251_u8 => "Ringsmall", + 114_u8 => "Rsmall", + 167_u8 => "Scaronsmall", + 115_u8 => "Ssmall", + 185_u8 => "Thornsmall", + 126_u8 => "Tildesmall", + 116_u8 => "Tsmall", + 156_u8 => "Uacutesmall", + 158_u8 => "Ucircumflexsmall", + 159_u8 => "Udieresissmall", + 157_u8 => "Ugravesmall", + 117_u8 => "Usmall", + 118_u8 => "Vsmall", + 119_u8 => "Wsmall", + 120_u8 => "Xsmall", + 180_u8 => "Yacutesmall", + 216_u8 => "Ydieresissmall", + 121_u8 => "Ysmall", + 189_u8 => "Zcaronsmall", + 122_u8 => "Zsmall", + 38_u8 => "ampersandsmall", + 129_u8 => "asuperior", + 245_u8 => "bsuperior", + 169_u8 => "centinferior", + 35_u8 => "centoldstyle", + 130_u8 => "centsuperior", + 58_u8 => "colon", + 123_u8 => "colonmonetary", + 44_u8 => "comma", + 178_u8 => "commainferior", + 248_u8 => "commasuperior", + 182_u8 => "dollarinferior", + 36_u8 => "dollaroldstyle", + 37_u8 => "dollarsuperior", + 235_u8 => "dsuperior", + 165_u8 => "eightinferior", + 56_u8 => "eightoldstyle", + 161_u8 => "eightsuperior", + 228_u8 => "esuperior", + 214_u8 => "exclamdownsmall", + 33_u8 => "exclamsmall", + 86_u8 => "ff", + 89_u8 => "ffi", + 90_u8 => "ffl", + 87_u8 => "fi", + 208_u8 => "figuredash", + 76_u8 => "fiveeighths", + 176_u8 => "fiveinferior", + 53_u8 => "fiveoldstyle", + 222_u8 => "fivesuperior", + 88_u8 => "fl", + 162_u8 => "fourinferior", + 52_u8 => "fouroldstyle", + 221_u8 => "foursuperior", + 47_u8 => "fraction", + 45_u8 => "hyphen", + 95_u8 => "hypheninferior", + 209_u8 => "hyphensuperior", + 233_u8 => "isuperior", + 241_u8 => "lsuperior", + 247_u8 => "msuperior", + 187_u8 => "nineinferior", + 57_u8 => "nineoldstyle", + 225_u8 => "ninesuperior", + 246_u8 => "nsuperior", + 43_u8 => "onedotenleader", + 74_u8 => "oneeighth", + 124_u8 => "onefitted", + 72_u8 => "onehalf", + 193_u8 => "oneinferior", + 49_u8 => "oneoldstyle", + 71_u8 => "onequarter", + 218_u8 => "onesuperior", + 78_u8 => "onethird", + 175_u8 => "osuperior", + 91_u8 => "parenleftinferior", + 40_u8 => "parenleftsuperior", + 93_u8 => "parenrightinferior", + 41_u8 => "parenrightsuperior", + 46_u8 => "period", + 179_u8 => "periodinferior", + 249_u8 => "periodsuperior", + 192_u8 => "questiondownsmall", + 63_u8 => "questionsmall", + 229_u8 => "rsuperior", + 125_u8 => "rupiah", + 59_u8 => "semicolon", + 77_u8 => "seveneighths", + 166_u8 => "seveninferior", + 55_u8 => "sevenoldstyle", + 224_u8 => "sevensuperior", + 164_u8 => "sixinferior", + 54_u8 => "sixoldstyle", + 223_u8 => "sixsuperior", + 32_u8 => "space", + 234_u8 => "ssuperior", + 75_u8 => "threeeighths", + 163_u8 => "threeinferior", + 51_u8 => "threeoldstyle", + 73_u8 => "threequarters", + 61_u8 => "threequartersemdash", + 220_u8 => "threesuperior", + 230_u8 => "tsuperior", + 42_u8 => "twodotenleader", + 170_u8 => "twoinferior", + 50_u8 => "twooldstyle", + 219_u8 => "twosuperior", + 79_u8 => "twothirds", + 188_u8 => "zeroinferior", + 48_u8 => "zerooldstyle", + 226_u8 => "zerosuperior", +}; diff --git a/vendor/hayro-interpret/src/font/generated/mac_os_roman.rs b/vendor/hayro-interpret/src/font/generated/mac_os_roman.rs new file mode 100644 index 00000000..1d71ca4c --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/mac_os_roman.rs @@ -0,0 +1,48 @@ +// See +use phf::phf_map; + +pub(crate) fn get(code: u8) -> Option<&'static str> { + MAC_OS_ROMAN.get(&code).copied() +} + +pub(crate) fn get_inverse(name: &str) -> Option { + MAC_OS_ROMAN_INVERSE.get(name).copied() +} + +static MAC_OS_ROMAN: phf::Map = phf_map! { + 173_u8 => "notequal", + 176_u8 => "infinity", + 178_u8 => "lessequal", + 179_u8 => "greaterequal", + 182_u8 => "partialdiff", + 183_u8 => "summation", + 184_u8 => "product", + 185_u8 => "pi", + 186_u8 => "integral", + 189_u8 => "Omega", + 195_u8 => "radical", + 197_u8 => "approxequal", + 198_u8 => "Delta", + 215_u8 => "lozenge", + 219_u8 => "Euro", + 240_u8 => "apple" +}; + +static MAC_OS_ROMAN_INVERSE: phf::Map<&'static str, u8> = phf_map! { + "notequal" => 0o0255_u8, + "infinity" => 0o0260_u8, + "lessequal" => 0o0262_u8, + "greaterequal" => 0o0263_u8, + "partialdiff" => 0o0266_u8, + "summation" => 0o0267_u8, + "product" => 0o0270_u8, + "pi" => 0o0271_u8, + "integral" => 0o0272_u8, + "Omega" => 0o0275_u8, + "radical" => 0o0303_u8, + "approxequal" => 0o0305_u8, + "Delta" => 0o0306_u8, + "lozenge" => 0o0327_u8, + "Euro" => 0o0333_u8, + "apple" => 0o0360_u8, +}; diff --git a/vendor/hayro-interpret/src/font/generated/mac_roman.rs b/vendor/hayro-interpret/src/font/generated/mac_roman.rs new file mode 100644 index 00000000..82f66da5 --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/mac_roman.rs @@ -0,0 +1,432 @@ +// See +use phf::phf_map; + +pub(crate) fn get(code: u8) -> Option<&'static str> { + MAC_ROMAN.get(&code).copied() +} + +pub(crate) fn get_inverse(name: &str) -> Option { + MAC_ROMAN_INVERSE.get(name).copied() +} + +static MAC_ROMAN: phf::Map = phf_map! { + 65_u8 => "A", + 174_u8 => "AE", + 231_u8 => "Aacute", + 229_u8 => "Acircumflex", + 128_u8 => "Adieresis", + 203_u8 => "Agrave", + 129_u8 => "Aring", + 204_u8 => "Atilde", + 66_u8 => "B", + 67_u8 => "C", + 130_u8 => "Ccedilla", + 68_u8 => "D", + 69_u8 => "E", + 131_u8 => "Eacute", + 230_u8 => "Ecircumflex", + 232_u8 => "Edieresis", + 233_u8 => "Egrave", + 70_u8 => "F", + 71_u8 => "G", + 72_u8 => "H", + 73_u8 => "I", + 234_u8 => "Iacute", + 235_u8 => "Icircumflex", + 236_u8 => "Idieresis", + 237_u8 => "Igrave", + 74_u8 => "J", + 75_u8 => "K", + 76_u8 => "L", + 77_u8 => "M", + 78_u8 => "N", + 132_u8 => "Ntilde", + 79_u8 => "O", + 206_u8 => "OE", + 238_u8 => "Oacute", + 239_u8 => "Ocircumflex", + 133_u8 => "Odieresis", + 241_u8 => "Ograve", + 175_u8 => "Oslash", + 205_u8 => "Otilde", + 80_u8 => "P", + 81_u8 => "Q", + 82_u8 => "R", + 83_u8 => "S", + 84_u8 => "T", + 85_u8 => "U", + 242_u8 => "Uacute", + 243_u8 => "Ucircumflex", + 134_u8 => "Udieresis", + 244_u8 => "Ugrave", + 86_u8 => "V", + 87_u8 => "W", + 88_u8 => "X", + 89_u8 => "Y", + 217_u8 => "Ydieresis", + 90_u8 => "Z", + 97_u8 => "a", + 135_u8 => "aacute", + 137_u8 => "acircumflex", + 171_u8 => "acute", + 138_u8 => "adieresis", + 190_u8 => "ae", + 136_u8 => "agrave", + 38_u8 => "ampersand", + 140_u8 => "aring", + 94_u8 => "asciicircum", + 126_u8 => "asciitilde", + 42_u8 => "asterisk", + 64_u8 => "at", + 139_u8 => "atilde", + 98_u8 => "b", + 92_u8 => "backslash", + 124_u8 => "bar", + 123_u8 => "braceleft", + 125_u8 => "braceright", + 91_u8 => "bracketleft", + 93_u8 => "bracketright", + 249_u8 => "breve", + 165_u8 => "bullet", + 99_u8 => "c", + 255_u8 => "caron", + 141_u8 => "ccedilla", + 252_u8 => "cedilla", + 162_u8 => "cent", + 246_u8 => "circumflex", + 58_u8 => "colon", + 44_u8 => "comma", + 169_u8 => "copyright", + 219_u8 => "currency", + 100_u8 => "d", + 160_u8 => "dagger", + 224_u8 => "daggerdbl", + 161_u8 => "degree", + 172_u8 => "dieresis", + 214_u8 => "divide", + 36_u8 => "dollar", + 250_u8 => "dotaccent", + 245_u8 => "dotlessi", + 101_u8 => "e", + 142_u8 => "eacute", + 144_u8 => "ecircumflex", + 145_u8 => "edieresis", + 143_u8 => "egrave", + 56_u8 => "eight", + 201_u8 => "ellipsis", + 209_u8 => "emdash", + 208_u8 => "endash", + 61_u8 => "equal", + 33_u8 => "exclam", + 193_u8 => "exclamdown", + 102_u8 => "f", + 222_u8 => "fi", + 53_u8 => "five", + 223_u8 => "fl", + 196_u8 => "florin", + 52_u8 => "four", + 218_u8 => "fraction", + 103_u8 => "g", + 167_u8 => "germandbls", + 96_u8 => "grave", + 62_u8 => "greater", + 199_u8 => "guillemotleft", + 200_u8 => "guillemotright", + 220_u8 => "guilsinglleft", + 221_u8 => "guilsinglright", + 104_u8 => "h", + 253_u8 => "hungarumlaut", + 45_u8 => "hyphen", + 105_u8 => "i", + 146_u8 => "iacute", + 148_u8 => "icircumflex", + 149_u8 => "idieresis", + 147_u8 => "igrave", + 106_u8 => "j", + 107_u8 => "k", + 108_u8 => "l", + 60_u8 => "less", + 194_u8 => "logicalnot", + 109_u8 => "m", + 248_u8 => "macron", + 181_u8 => "mu", + 110_u8 => "n", + 57_u8 => "nine", + 150_u8 => "ntilde", + 35_u8 => "numbersign", + 111_u8 => "o", + 151_u8 => "oacute", + 153_u8 => "ocircumflex", + 154_u8 => "odieresis", + 207_u8 => "oe", + 254_u8 => "ogonek", + 152_u8 => "ograve", + 49_u8 => "one", + 187_u8 => "ordfeminine", + 188_u8 => "ordmasculine", + 191_u8 => "oslash", + 155_u8 => "otilde", + 112_u8 => "p", + 166_u8 => "paragraph", + 40_u8 => "parenleft", + 41_u8 => "parenright", + 37_u8 => "percent", + 46_u8 => "period", + 225_u8 => "periodcentered", + 228_u8 => "perthousand", + 43_u8 => "plus", + 177_u8 => "plusminus", + 113_u8 => "q", + 63_u8 => "question", + 192_u8 => "questiondown", + 34_u8 => "quotedbl", + 227_u8 => "quotedblbase", + 210_u8 => "quotedblleft", + 211_u8 => "quotedblright", + 212_u8 => "quoteleft", + 213_u8 => "quoteright", + 226_u8 => "quotesinglbase", + 39_u8 => "quotesingle", + 114_u8 => "r", + 168_u8 => "registered", + 251_u8 => "ring", + 115_u8 => "s", + 164_u8 => "section", + 59_u8 => "semicolon", + 55_u8 => "seven", + 54_u8 => "six", + 47_u8 => "slash", + 32_u8 => "space", + 163_u8 => "sterling", + 116_u8 => "t", + 51_u8 => "three", + 247_u8 => "tilde", + 170_u8 => "trademark", + 50_u8 => "two", + 117_u8 => "u", + 156_u8 => "uacute", + 158_u8 => "ucircumflex", + 159_u8 => "udieresis", + 157_u8 => "ugrave", + 95_u8 => "underscore", + 118_u8 => "v", + 119_u8 => "w", + 120_u8 => "x", + 121_u8 => "y", + 216_u8 => "ydieresis", + 180_u8 => "yen", + 122_u8 => "z", + 48_u8 => "zero", + 202_u8 => "nbspace", +}; + +static MAC_ROMAN_INVERSE: phf::Map<&'static str, u8> = phf_map! { + "A" => 65_u8, + "AE" => 174_u8, + "Aacute" => 231_u8, + "Acircumflex" => 229_u8, + "Adieresis" => 128_u8, + "Agrave" => 203_u8, + "Aring" => 129_u8, + "Atilde" => 204_u8, + "B" => 66_u8, + "C" => 67_u8, + "Ccedilla" => 130_u8, + "D" => 68_u8, + "E" => 69_u8, + "Eacute" => 131_u8, + "Ecircumflex" => 230_u8, + "Edieresis" => 232_u8, + "Egrave" => 233_u8, + "F" => 70_u8, + "G" => 71_u8, + "H" => 72_u8, + "I" => 73_u8, + "Iacute" => 234_u8, + "Icircumflex" => 235_u8, + "Idieresis" => 236_u8, + "Igrave" => 237_u8, + "J" => 74_u8, + "K" => 75_u8, + "L" => 76_u8, + "M" => 77_u8, + "N" => 78_u8, + "Ntilde" => 132_u8, + "O" => 79_u8, + "OE" => 206_u8, + "Oacute" => 238_u8, + "Ocircumflex" => 239_u8, + "Odieresis" => 133_u8, + "Ograve" => 241_u8, + "Oslash" => 175_u8, + "Otilde" => 205_u8, + "P" => 80_u8, + "Q" => 81_u8, + "R" => 82_u8, + "S" => 83_u8, + "T" => 84_u8, + "U" => 85_u8, + "Uacute" => 242_u8, + "Ucircumflex" => 243_u8, + "Udieresis" => 134_u8, + "Ugrave" => 244_u8, + "V" => 86_u8, + "W" => 87_u8, + "X" => 88_u8, + "Y" => 89_u8, + "Ydieresis" => 217_u8, + "Z" => 90_u8, + "a" => 97_u8, + "aacute" => 135_u8, + "acircumflex" => 137_u8, + "acute" => 171_u8, + "adieresis" => 138_u8, + "ae" => 190_u8, + "agrave" => 136_u8, + "ampersand" => 38_u8, + "aring" => 140_u8, + "asciicircum" => 94_u8, + "asciitilde" => 126_u8, + "asterisk" => 42_u8, + "at" => 64_u8, + "atilde" => 139_u8, + "b" => 98_u8, + "backslash" => 92_u8, + "bar" => 124_u8, + "braceleft" => 123_u8, + "braceright" => 125_u8, + "bracketleft" => 91_u8, + "bracketright" => 93_u8, + "breve" => 249_u8, + "bullet" => 165_u8, + "c" => 99_u8, + "caron" => 255_u8, + "ccedilla" => 141_u8, + "cedilla" => 252_u8, + "cent" => 162_u8, + "circumflex" => 246_u8, + "colon" => 58_u8, + "comma" => 44_u8, + "copyright" => 169_u8, + "currency" => 219_u8, + "d" => 100_u8, + "dagger" => 160_u8, + "daggerdbl" => 224_u8, + "degree" => 161_u8, + "dieresis" => 172_u8, + "divide" => 214_u8, + "dollar" => 36_u8, + "dotaccent" => 250_u8, + "dotlessi" => 245_u8, + "e" => 101_u8, + "eacute" => 142_u8, + "ecircumflex" => 144_u8, + "edieresis" => 145_u8, + "egrave" => 143_u8, + "eight" => 56_u8, + "ellipsis" => 201_u8, + "emdash" => 209_u8, + "endash" => 208_u8, + "equal" => 61_u8, + "exclam" => 33_u8, + "exclamdown" => 193_u8, + "f" => 102_u8, + "fi" => 222_u8, + "five" => 53_u8, + "fl" => 223_u8, + "florin" => 196_u8, + "four" => 52_u8, + "fraction" => 218_u8, + "g" => 103_u8, + "germandbls" => 167_u8, + "grave" => 96_u8, + "greater" => 62_u8, + "guillemotleft" => 199_u8, + "guillemotright" => 200_u8, + "guilsinglleft" => 220_u8, + "guilsinglright" => 221_u8, + "h" => 104_u8, + "hungarumlaut" => 253_u8, + "hyphen" => 45_u8, + "i" => 105_u8, + "iacute" => 146_u8, + "icircumflex" => 148_u8, + "idieresis" => 149_u8, + "igrave" => 147_u8, + "j" => 106_u8, + "k" => 107_u8, + "l" => 108_u8, + "less" => 60_u8, + "logicalnot" => 194_u8, + "m" => 109_u8, + "macron" => 248_u8, + "mu" => 181_u8, + "n" => 110_u8, + "nine" => 57_u8, + "ntilde" => 150_u8, + "numbersign" => 35_u8, + "o" => 111_u8, + "oacute" => 151_u8, + "ocircumflex" => 153_u8, + "odieresis" => 154_u8, + "oe" => 207_u8, + "ogonek" => 254_u8, + "ograve" => 152_u8, + "one" => 49_u8, + "ordfeminine" => 187_u8, + "ordmasculine" => 188_u8, + "oslash" => 191_u8, + "otilde" => 155_u8, + "p" => 112_u8, + "paragraph" => 166_u8, + "parenleft" => 40_u8, + "parenright" => 41_u8, + "percent" => 37_u8, + "period" => 46_u8, + "periodcentered" => 225_u8, + "perthousand" => 228_u8, + "plus" => 43_u8, + "plusminus" => 177_u8, + "q" => 113_u8, + "question" => 63_u8, + "questiondown" => 192_u8, + "quotedbl" => 34_u8, + "quotedblbase" => 227_u8, + "quotedblleft" => 210_u8, + "quotedblright" => 211_u8, + "quoteleft" => 212_u8, + "quoteright" => 213_u8, + "quotesinglbase" => 226_u8, + "quotesingle" => 39_u8, + "r" => 114_u8, + "registered" => 168_u8, + "ring" => 251_u8, + "s" => 115_u8, + "section" => 164_u8, + "semicolon" => 59_u8, + "seven" => 55_u8, + "six" => 54_u8, + "slash" => 47_u8, + "space" => 32_u8, + "sterling" => 163_u8, + "t" => 116_u8, + "three" => 51_u8, + "tilde" => 247_u8, + "trademark" => 170_u8, + "two" => 50_u8, + "u" => 117_u8, + "uacute" => 156_u8, + "ucircumflex" => 158_u8, + "udieresis" => 159_u8, + "ugrave" => 157_u8, + "underscore" => 95_u8, + "v" => 118_u8, + "w" => 119_u8, + "x" => 120_u8, + "y" => 121_u8, + "ydieresis" => 216_u8, + "yen" => 180_u8, + "z" => 122_u8, + "zero" => 48_u8, + "nbspace" => 202_u8, +}; diff --git a/vendor/hayro-interpret/src/font/generated/metrics.rs b/vendor/hayro-interpret/src/font/generated/metrics.rs new file mode 100644 index 00000000..c9ac725e --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/metrics.rs @@ -0,0 +1,4215 @@ +use phf::phf_map; + +pub(crate) static COURIER: phf::Map<&'static str, f32> = phf_map! { + "space" => 600_f32, + "exclam" => 600_f32, + "quotedbl" => 600_f32, + "numbersign" => 600_f32, + "dollar" => 600_f32, + "percent" => 600_f32, + "ampersand" => 600_f32, + "quoteright" => 600_f32, + "parenleft" => 600_f32, + "parenright" => 600_f32, + "asterisk" => 600_f32, + "plus" => 600_f32, + "comma" => 600_f32, + "hyphen" => 600_f32, + "period" => 600_f32, + "slash" => 600_f32, + "zero" => 600_f32, + "one" => 600_f32, + "two" => 600_f32, + "three" => 600_f32, + "four" => 600_f32, + "five" => 600_f32, + "six" => 600_f32, + "seven" => 600_f32, + "eight" => 600_f32, + "nine" => 600_f32, + "colon" => 600_f32, + "semicolon" => 600_f32, + "less" => 600_f32, + "equal" => 600_f32, + "greater" => 600_f32, + "question" => 600_f32, + "at" => 600_f32, + "A" => 600_f32, + "B" => 600_f32, + "C" => 600_f32, + "D" => 600_f32, + "E" => 600_f32, + "F" => 600_f32, + "G" => 600_f32, + "H" => 600_f32, + "I" => 600_f32, + "J" => 600_f32, + "K" => 600_f32, + "L" => 600_f32, + "M" => 600_f32, + "N" => 600_f32, + "O" => 600_f32, + "P" => 600_f32, + "Q" => 600_f32, + "R" => 600_f32, + "S" => 600_f32, + "T" => 600_f32, + "U" => 600_f32, + "V" => 600_f32, + "W" => 600_f32, + "X" => 600_f32, + "Y" => 600_f32, + "Z" => 600_f32, + "bracketleft" => 600_f32, + "backslash" => 600_f32, + "bracketright" => 600_f32, + "asciicircum" => 600_f32, + "underscore" => 600_f32, + "quoteleft" => 600_f32, + "a" => 600_f32, + "b" => 600_f32, + "c" => 600_f32, + "d" => 600_f32, + "e" => 600_f32, + "f" => 600_f32, + "g" => 600_f32, + "h" => 600_f32, + "i" => 600_f32, + "j" => 600_f32, + "k" => 600_f32, + "l" => 600_f32, + "m" => 600_f32, + "n" => 600_f32, + "o" => 600_f32, + "p" => 600_f32, + "q" => 600_f32, + "r" => 600_f32, + "s" => 600_f32, + "t" => 600_f32, + "u" => 600_f32, + "v" => 600_f32, + "w" => 600_f32, + "x" => 600_f32, + "y" => 600_f32, + "z" => 600_f32, + "braceleft" => 600_f32, + "bar" => 600_f32, + "braceright" => 600_f32, + "asciitilde" => 600_f32, + "exclamdown" => 600_f32, + "cent" => 600_f32, + "sterling" => 600_f32, + "fraction" => 600_f32, + "yen" => 600_f32, + "florin" => 600_f32, + "section" => 600_f32, + "currency" => 600_f32, + "quotesingle" => 600_f32, + "quotedblleft" => 600_f32, + "guillemotleft" => 600_f32, + "guilsinglleft" => 600_f32, + "guilsinglright" => 600_f32, + "fi" => 600_f32, + "fl" => 600_f32, + "endash" => 600_f32, + "dagger" => 600_f32, + "daggerdbl" => 600_f32, + "periodcentered" => 600_f32, + "paragraph" => 600_f32, + "bullet" => 600_f32, + "quotesinglbase" => 600_f32, + "quotedblbase" => 600_f32, + "quotedblright" => 600_f32, + "guillemotright" => 600_f32, + "ellipsis" => 600_f32, + "perthousand" => 600_f32, + "questiondown" => 600_f32, + "grave" => 600_f32, + "acute" => 600_f32, + "circumflex" => 600_f32, + "tilde" => 600_f32, + "macron" => 600_f32, + "breve" => 600_f32, + "dotaccent" => 600_f32, + "dieresis" => 600_f32, + "ring" => 600_f32, + "cedilla" => 600_f32, + "hungarumlaut" => 600_f32, + "ogonek" => 600_f32, + "caron" => 600_f32, + "emdash" => 600_f32, + "AE" => 600_f32, + "ordfeminine" => 600_f32, + "Lslash" => 600_f32, + "Oslash" => 600_f32, + "OE" => 600_f32, + "ordmasculine" => 600_f32, + "ae" => 600_f32, + "dotlessi" => 600_f32, + "lslash" => 600_f32, + "oslash" => 600_f32, + "oe" => 600_f32, + "germandbls" => 600_f32, + "Idieresis" => 600_f32, + "eacute" => 600_f32, + "abreve" => 600_f32, + "uhungarumlaut" => 600_f32, + "ecaron" => 600_f32, + "Ydieresis" => 600_f32, + "divide" => 600_f32, + "Yacute" => 600_f32, + "Acircumflex" => 600_f32, + "aacute" => 600_f32, + "Ucircumflex" => 600_f32, + "yacute" => 600_f32, + "scommaaccent" => 600_f32, + "ecircumflex" => 600_f32, + "Uring" => 600_f32, + "Udieresis" => 600_f32, + "aogonek" => 600_f32, + "Uacute" => 600_f32, + "uogonek" => 600_f32, + "Edieresis" => 600_f32, + "Dcroat" => 600_f32, + "commaaccent" => 600_f32, + "copyright" => 600_f32, + "Emacron" => 600_f32, + "ccaron" => 600_f32, + "aring" => 600_f32, + "Ncommaaccent" => 600_f32, + "lacute" => 600_f32, + "agrave" => 600_f32, + "Tcommaaccent" => 600_f32, + "Cacute" => 600_f32, + "atilde" => 600_f32, + "Edotaccent" => 600_f32, + "scaron" => 600_f32, + "scedilla" => 600_f32, + "iacute" => 600_f32, + "lozenge" => 600_f32, + "Rcaron" => 600_f32, + "Gcommaaccent" => 600_f32, + "ucircumflex" => 600_f32, + "acircumflex" => 600_f32, + "Amacron" => 600_f32, + "rcaron" => 600_f32, + "ccedilla" => 600_f32, + "Zdotaccent" => 600_f32, + "Thorn" => 600_f32, + "Omacron" => 600_f32, + "Racute" => 600_f32, + "Sacute" => 600_f32, + "dcaron" => 600_f32, + "Umacron" => 600_f32, + "uring" => 600_f32, + "threesuperior" => 600_f32, + "Ograve" => 600_f32, + "Agrave" => 600_f32, + "Abreve" => 600_f32, + "multiply" => 600_f32, + "uacute" => 600_f32, + "Tcaron" => 600_f32, + "partialdiff" => 600_f32, + "ydieresis" => 600_f32, + "Nacute" => 600_f32, + "icircumflex" => 600_f32, + "Ecircumflex" => 600_f32, + "adieresis" => 600_f32, + "edieresis" => 600_f32, + "cacute" => 600_f32, + "nacute" => 600_f32, + "umacron" => 600_f32, + "Ncaron" => 600_f32, + "Iacute" => 600_f32, + "plusminus" => 600_f32, + "brokenbar" => 600_f32, + "registered" => 600_f32, + "Gbreve" => 600_f32, + "Idotaccent" => 600_f32, + "summation" => 600_f32, + "Egrave" => 600_f32, + "racute" => 600_f32, + "omacron" => 600_f32, + "Zacute" => 600_f32, + "Zcaron" => 600_f32, + "greaterequal" => 600_f32, + "Eth" => 600_f32, + "Ccedilla" => 600_f32, + "lcommaaccent" => 600_f32, + "tcaron" => 600_f32, + "eogonek" => 600_f32, + "Uogonek" => 600_f32, + "Aacute" => 600_f32, + "Adieresis" => 600_f32, + "egrave" => 600_f32, + "zacute" => 600_f32, + "iogonek" => 600_f32, + "Oacute" => 600_f32, + "oacute" => 600_f32, + "amacron" => 600_f32, + "sacute" => 600_f32, + "idieresis" => 600_f32, + "Ocircumflex" => 600_f32, + "Ugrave" => 600_f32, + "Delta" => 600_f32, + "thorn" => 600_f32, + "twosuperior" => 600_f32, + "Odieresis" => 600_f32, + "mu" => 600_f32, + "igrave" => 600_f32, + "ohungarumlaut" => 600_f32, + "Eogonek" => 600_f32, + "dcroat" => 600_f32, + "threequarters" => 600_f32, + "Scedilla" => 600_f32, + "lcaron" => 600_f32, + "Kcommaaccent" => 600_f32, + "Lacute" => 600_f32, + "trademark" => 600_f32, + "edotaccent" => 600_f32, + "Igrave" => 600_f32, + "Imacron" => 600_f32, + "Lcaron" => 600_f32, + "onehalf" => 600_f32, + "lessequal" => 600_f32, + "ocircumflex" => 600_f32, + "ntilde" => 600_f32, + "Uhungarumlaut" => 600_f32, + "Eacute" => 600_f32, + "emacron" => 600_f32, + "gbreve" => 600_f32, + "onequarter" => 600_f32, + "Scaron" => 600_f32, + "Scommaaccent" => 600_f32, + "Ohungarumlaut" => 600_f32, + "degree" => 600_f32, + "ograve" => 600_f32, + "Ccaron" => 600_f32, + "ugrave" => 600_f32, + "radical" => 600_f32, + "Dcaron" => 600_f32, + "rcommaaccent" => 600_f32, + "Ntilde" => 600_f32, + "otilde" => 600_f32, + "Rcommaaccent" => 600_f32, + "Lcommaaccent" => 600_f32, + "Atilde" => 600_f32, + "Aogonek" => 600_f32, + "Aring" => 600_f32, + "Otilde" => 600_f32, + "zdotaccent" => 600_f32, + "Ecaron" => 600_f32, + "Iogonek" => 600_f32, + "kcommaaccent" => 600_f32, + "minus" => 600_f32, + "Icircumflex" => 600_f32, + "ncaron" => 600_f32, + "tcommaaccent" => 600_f32, + "logicalnot" => 600_f32, + "odieresis" => 600_f32, + "udieresis" => 600_f32, + "notequal" => 600_f32, + "gcommaaccent" => 600_f32, + "eth" => 600_f32, + "zcaron" => 600_f32, + "ncommaaccent" => 600_f32, + "onesuperior" => 600_f32, + "imacron" => 600_f32, + "Euro" => 600_f32, +}; + +pub(crate) static COURIER_BOLD: phf::Map<&'static str, f32> = phf_map! { + "space" => 600_f32, + "exclam" => 600_f32, + "quotedbl" => 600_f32, + "numbersign" => 600_f32, + "dollar" => 600_f32, + "percent" => 600_f32, + "ampersand" => 600_f32, + "quoteright" => 600_f32, + "parenleft" => 600_f32, + "parenright" => 600_f32, + "asterisk" => 600_f32, + "plus" => 600_f32, + "comma" => 600_f32, + "hyphen" => 600_f32, + "period" => 600_f32, + "slash" => 600_f32, + "zero" => 600_f32, + "one" => 600_f32, + "two" => 600_f32, + "three" => 600_f32, + "four" => 600_f32, + "five" => 600_f32, + "six" => 600_f32, + "seven" => 600_f32, + "eight" => 600_f32, + "nine" => 600_f32, + "colon" => 600_f32, + "semicolon" => 600_f32, + "less" => 600_f32, + "equal" => 600_f32, + "greater" => 600_f32, + "question" => 600_f32, + "at" => 600_f32, + "A" => 600_f32, + "B" => 600_f32, + "C" => 600_f32, + "D" => 600_f32, + "E" => 600_f32, + "F" => 600_f32, + "G" => 600_f32, + "H" => 600_f32, + "I" => 600_f32, + "J" => 600_f32, + "K" => 600_f32, + "L" => 600_f32, + "M" => 600_f32, + "N" => 600_f32, + "O" => 600_f32, + "P" => 600_f32, + "Q" => 600_f32, + "R" => 600_f32, + "S" => 600_f32, + "T" => 600_f32, + "U" => 600_f32, + "V" => 600_f32, + "W" => 600_f32, + "X" => 600_f32, + "Y" => 600_f32, + "Z" => 600_f32, + "bracketleft" => 600_f32, + "backslash" => 600_f32, + "bracketright" => 600_f32, + "asciicircum" => 600_f32, + "underscore" => 600_f32, + "quoteleft" => 600_f32, + "a" => 600_f32, + "b" => 600_f32, + "c" => 600_f32, + "d" => 600_f32, + "e" => 600_f32, + "f" => 600_f32, + "g" => 600_f32, + "h" => 600_f32, + "i" => 600_f32, + "j" => 600_f32, + "k" => 600_f32, + "l" => 600_f32, + "m" => 600_f32, + "n" => 600_f32, + "o" => 600_f32, + "p" => 600_f32, + "q" => 600_f32, + "r" => 600_f32, + "s" => 600_f32, + "t" => 600_f32, + "u" => 600_f32, + "v" => 600_f32, + "w" => 600_f32, + "x" => 600_f32, + "y" => 600_f32, + "z" => 600_f32, + "braceleft" => 600_f32, + "bar" => 600_f32, + "braceright" => 600_f32, + "asciitilde" => 600_f32, + "exclamdown" => 600_f32, + "cent" => 600_f32, + "sterling" => 600_f32, + "fraction" => 600_f32, + "yen" => 600_f32, + "florin" => 600_f32, + "section" => 600_f32, + "currency" => 600_f32, + "quotesingle" => 600_f32, + "quotedblleft" => 600_f32, + "guillemotleft" => 600_f32, + "guilsinglleft" => 600_f32, + "guilsinglright" => 600_f32, + "fi" => 600_f32, + "fl" => 600_f32, + "endash" => 600_f32, + "dagger" => 600_f32, + "daggerdbl" => 600_f32, + "periodcentered" => 600_f32, + "paragraph" => 600_f32, + "bullet" => 600_f32, + "quotesinglbase" => 600_f32, + "quotedblbase" => 600_f32, + "quotedblright" => 600_f32, + "guillemotright" => 600_f32, + "ellipsis" => 600_f32, + "perthousand" => 600_f32, + "questiondown" => 600_f32, + "grave" => 600_f32, + "acute" => 600_f32, + "circumflex" => 600_f32, + "tilde" => 600_f32, + "macron" => 600_f32, + "breve" => 600_f32, + "dotaccent" => 600_f32, + "dieresis" => 600_f32, + "ring" => 600_f32, + "cedilla" => 600_f32, + "hungarumlaut" => 600_f32, + "ogonek" => 600_f32, + "caron" => 600_f32, + "emdash" => 600_f32, + "AE" => 600_f32, + "ordfeminine" => 600_f32, + "Lslash" => 600_f32, + "Oslash" => 600_f32, + "OE" => 600_f32, + "ordmasculine" => 600_f32, + "ae" => 600_f32, + "dotlessi" => 600_f32, + "lslash" => 600_f32, + "oslash" => 600_f32, + "oe" => 600_f32, + "germandbls" => 600_f32, + "Idieresis" => 600_f32, + "eacute" => 600_f32, + "abreve" => 600_f32, + "uhungarumlaut" => 600_f32, + "ecaron" => 600_f32, + "Ydieresis" => 600_f32, + "divide" => 600_f32, + "Yacute" => 600_f32, + "Acircumflex" => 600_f32, + "aacute" => 600_f32, + "Ucircumflex" => 600_f32, + "yacute" => 600_f32, + "scommaaccent" => 600_f32, + "ecircumflex" => 600_f32, + "Uring" => 600_f32, + "Udieresis" => 600_f32, + "aogonek" => 600_f32, + "Uacute" => 600_f32, + "uogonek" => 600_f32, + "Edieresis" => 600_f32, + "Dcroat" => 600_f32, + "commaaccent" => 600_f32, + "copyright" => 600_f32, + "Emacron" => 600_f32, + "ccaron" => 600_f32, + "aring" => 600_f32, + "Ncommaaccent" => 600_f32, + "lacute" => 600_f32, + "agrave" => 600_f32, + "Tcommaaccent" => 600_f32, + "Cacute" => 600_f32, + "atilde" => 600_f32, + "Edotaccent" => 600_f32, + "scaron" => 600_f32, + "scedilla" => 600_f32, + "iacute" => 600_f32, + "lozenge" => 600_f32, + "Rcaron" => 600_f32, + "Gcommaaccent" => 600_f32, + "ucircumflex" => 600_f32, + "acircumflex" => 600_f32, + "Amacron" => 600_f32, + "rcaron" => 600_f32, + "ccedilla" => 600_f32, + "Zdotaccent" => 600_f32, + "Thorn" => 600_f32, + "Omacron" => 600_f32, + "Racute" => 600_f32, + "Sacute" => 600_f32, + "dcaron" => 600_f32, + "Umacron" => 600_f32, + "uring" => 600_f32, + "threesuperior" => 600_f32, + "Ograve" => 600_f32, + "Agrave" => 600_f32, + "Abreve" => 600_f32, + "multiply" => 600_f32, + "uacute" => 600_f32, + "Tcaron" => 600_f32, + "partialdiff" => 600_f32, + "ydieresis" => 600_f32, + "Nacute" => 600_f32, + "icircumflex" => 600_f32, + "Ecircumflex" => 600_f32, + "adieresis" => 600_f32, + "edieresis" => 600_f32, + "cacute" => 600_f32, + "nacute" => 600_f32, + "umacron" => 600_f32, + "Ncaron" => 600_f32, + "Iacute" => 600_f32, + "plusminus" => 600_f32, + "brokenbar" => 600_f32, + "registered" => 600_f32, + "Gbreve" => 600_f32, + "Idotaccent" => 600_f32, + "summation" => 600_f32, + "Egrave" => 600_f32, + "racute" => 600_f32, + "omacron" => 600_f32, + "Zacute" => 600_f32, + "Zcaron" => 600_f32, + "greaterequal" => 600_f32, + "Eth" => 600_f32, + "Ccedilla" => 600_f32, + "lcommaaccent" => 600_f32, + "tcaron" => 600_f32, + "eogonek" => 600_f32, + "Uogonek" => 600_f32, + "Aacute" => 600_f32, + "Adieresis" => 600_f32, + "egrave" => 600_f32, + "zacute" => 600_f32, + "iogonek" => 600_f32, + "Oacute" => 600_f32, + "oacute" => 600_f32, + "amacron" => 600_f32, + "sacute" => 600_f32, + "idieresis" => 600_f32, + "Ocircumflex" => 600_f32, + "Ugrave" => 600_f32, + "Delta" => 600_f32, + "thorn" => 600_f32, + "twosuperior" => 600_f32, + "Odieresis" => 600_f32, + "mu" => 600_f32, + "igrave" => 600_f32, + "ohungarumlaut" => 600_f32, + "Eogonek" => 600_f32, + "dcroat" => 600_f32, + "threequarters" => 600_f32, + "Scedilla" => 600_f32, + "lcaron" => 600_f32, + "Kcommaaccent" => 600_f32, + "Lacute" => 600_f32, + "trademark" => 600_f32, + "edotaccent" => 600_f32, + "Igrave" => 600_f32, + "Imacron" => 600_f32, + "Lcaron" => 600_f32, + "onehalf" => 600_f32, + "lessequal" => 600_f32, + "ocircumflex" => 600_f32, + "ntilde" => 600_f32, + "Uhungarumlaut" => 600_f32, + "Eacute" => 600_f32, + "emacron" => 600_f32, + "gbreve" => 600_f32, + "onequarter" => 600_f32, + "Scaron" => 600_f32, + "Scommaaccent" => 600_f32, + "Ohungarumlaut" => 600_f32, + "degree" => 600_f32, + "ograve" => 600_f32, + "Ccaron" => 600_f32, + "ugrave" => 600_f32, + "radical" => 600_f32, + "Dcaron" => 600_f32, + "rcommaaccent" => 600_f32, + "Ntilde" => 600_f32, + "otilde" => 600_f32, + "Rcommaaccent" => 600_f32, + "Lcommaaccent" => 600_f32, + "Atilde" => 600_f32, + "Aogonek" => 600_f32, + "Aring" => 600_f32, + "Otilde" => 600_f32, + "zdotaccent" => 600_f32, + "Ecaron" => 600_f32, + "Iogonek" => 600_f32, + "kcommaaccent" => 600_f32, + "minus" => 600_f32, + "Icircumflex" => 600_f32, + "ncaron" => 600_f32, + "tcommaaccent" => 600_f32, + "logicalnot" => 600_f32, + "odieresis" => 600_f32, + "udieresis" => 600_f32, + "notequal" => 600_f32, + "gcommaaccent" => 600_f32, + "eth" => 600_f32, + "zcaron" => 600_f32, + "ncommaaccent" => 600_f32, + "onesuperior" => 600_f32, + "imacron" => 600_f32, + "Euro" => 600_f32, +}; + +pub(crate) static COURIER_BOLD_OBLIQUE: phf::Map<&'static str, f32> = phf_map! { + "space" => 600_f32, + "exclam" => 600_f32, + "quotedbl" => 600_f32, + "numbersign" => 600_f32, + "dollar" => 600_f32, + "percent" => 600_f32, + "ampersand" => 600_f32, + "quoteright" => 600_f32, + "parenleft" => 600_f32, + "parenright" => 600_f32, + "asterisk" => 600_f32, + "plus" => 600_f32, + "comma" => 600_f32, + "hyphen" => 600_f32, + "period" => 600_f32, + "slash" => 600_f32, + "zero" => 600_f32, + "one" => 600_f32, + "two" => 600_f32, + "three" => 600_f32, + "four" => 600_f32, + "five" => 600_f32, + "six" => 600_f32, + "seven" => 600_f32, + "eight" => 600_f32, + "nine" => 600_f32, + "colon" => 600_f32, + "semicolon" => 600_f32, + "less" => 600_f32, + "equal" => 600_f32, + "greater" => 600_f32, + "question" => 600_f32, + "at" => 600_f32, + "A" => 600_f32, + "B" => 600_f32, + "C" => 600_f32, + "D" => 600_f32, + "E" => 600_f32, + "F" => 600_f32, + "G" => 600_f32, + "H" => 600_f32, + "I" => 600_f32, + "J" => 600_f32, + "K" => 600_f32, + "L" => 600_f32, + "M" => 600_f32, + "N" => 600_f32, + "O" => 600_f32, + "P" => 600_f32, + "Q" => 600_f32, + "R" => 600_f32, + "S" => 600_f32, + "T" => 600_f32, + "U" => 600_f32, + "V" => 600_f32, + "W" => 600_f32, + "X" => 600_f32, + "Y" => 600_f32, + "Z" => 600_f32, + "bracketleft" => 600_f32, + "backslash" => 600_f32, + "bracketright" => 600_f32, + "asciicircum" => 600_f32, + "underscore" => 600_f32, + "quoteleft" => 600_f32, + "a" => 600_f32, + "b" => 600_f32, + "c" => 600_f32, + "d" => 600_f32, + "e" => 600_f32, + "f" => 600_f32, + "g" => 600_f32, + "h" => 600_f32, + "i" => 600_f32, + "j" => 600_f32, + "k" => 600_f32, + "l" => 600_f32, + "m" => 600_f32, + "n" => 600_f32, + "o" => 600_f32, + "p" => 600_f32, + "q" => 600_f32, + "r" => 600_f32, + "s" => 600_f32, + "t" => 600_f32, + "u" => 600_f32, + "v" => 600_f32, + "w" => 600_f32, + "x" => 600_f32, + "y" => 600_f32, + "z" => 600_f32, + "braceleft" => 600_f32, + "bar" => 600_f32, + "braceright" => 600_f32, + "asciitilde" => 600_f32, + "exclamdown" => 600_f32, + "cent" => 600_f32, + "sterling" => 600_f32, + "fraction" => 600_f32, + "yen" => 600_f32, + "florin" => 600_f32, + "section" => 600_f32, + "currency" => 600_f32, + "quotesingle" => 600_f32, + "quotedblleft" => 600_f32, + "guillemotleft" => 600_f32, + "guilsinglleft" => 600_f32, + "guilsinglright" => 600_f32, + "fi" => 600_f32, + "fl" => 600_f32, + "endash" => 600_f32, + "dagger" => 600_f32, + "daggerdbl" => 600_f32, + "periodcentered" => 600_f32, + "paragraph" => 600_f32, + "bullet" => 600_f32, + "quotesinglbase" => 600_f32, + "quotedblbase" => 600_f32, + "quotedblright" => 600_f32, + "guillemotright" => 600_f32, + "ellipsis" => 600_f32, + "perthousand" => 600_f32, + "questiondown" => 600_f32, + "grave" => 600_f32, + "acute" => 600_f32, + "circumflex" => 600_f32, + "tilde" => 600_f32, + "macron" => 600_f32, + "breve" => 600_f32, + "dotaccent" => 600_f32, + "dieresis" => 600_f32, + "ring" => 600_f32, + "cedilla" => 600_f32, + "hungarumlaut" => 600_f32, + "ogonek" => 600_f32, + "caron" => 600_f32, + "emdash" => 600_f32, + "AE" => 600_f32, + "ordfeminine" => 600_f32, + "Lslash" => 600_f32, + "Oslash" => 600_f32, + "OE" => 600_f32, + "ordmasculine" => 600_f32, + "ae" => 600_f32, + "dotlessi" => 600_f32, + "lslash" => 600_f32, + "oslash" => 600_f32, + "oe" => 600_f32, + "germandbls" => 600_f32, + "Idieresis" => 600_f32, + "eacute" => 600_f32, + "abreve" => 600_f32, + "uhungarumlaut" => 600_f32, + "ecaron" => 600_f32, + "Ydieresis" => 600_f32, + "divide" => 600_f32, + "Yacute" => 600_f32, + "Acircumflex" => 600_f32, + "aacute" => 600_f32, + "Ucircumflex" => 600_f32, + "yacute" => 600_f32, + "scommaaccent" => 600_f32, + "ecircumflex" => 600_f32, + "Uring" => 600_f32, + "Udieresis" => 600_f32, + "aogonek" => 600_f32, + "Uacute" => 600_f32, + "uogonek" => 600_f32, + "Edieresis" => 600_f32, + "Dcroat" => 600_f32, + "commaaccent" => 600_f32, + "copyright" => 600_f32, + "Emacron" => 600_f32, + "ccaron" => 600_f32, + "aring" => 600_f32, + "Ncommaaccent" => 600_f32, + "lacute" => 600_f32, + "agrave" => 600_f32, + "Tcommaaccent" => 600_f32, + "Cacute" => 600_f32, + "atilde" => 600_f32, + "Edotaccent" => 600_f32, + "scaron" => 600_f32, + "scedilla" => 600_f32, + "iacute" => 600_f32, + "lozenge" => 600_f32, + "Rcaron" => 600_f32, + "Gcommaaccent" => 600_f32, + "ucircumflex" => 600_f32, + "acircumflex" => 600_f32, + "Amacron" => 600_f32, + "rcaron" => 600_f32, + "ccedilla" => 600_f32, + "Zdotaccent" => 600_f32, + "Thorn" => 600_f32, + "Omacron" => 600_f32, + "Racute" => 600_f32, + "Sacute" => 600_f32, + "dcaron" => 600_f32, + "Umacron" => 600_f32, + "uring" => 600_f32, + "threesuperior" => 600_f32, + "Ograve" => 600_f32, + "Agrave" => 600_f32, + "Abreve" => 600_f32, + "multiply" => 600_f32, + "uacute" => 600_f32, + "Tcaron" => 600_f32, + "partialdiff" => 600_f32, + "ydieresis" => 600_f32, + "Nacute" => 600_f32, + "icircumflex" => 600_f32, + "Ecircumflex" => 600_f32, + "adieresis" => 600_f32, + "edieresis" => 600_f32, + "cacute" => 600_f32, + "nacute" => 600_f32, + "umacron" => 600_f32, + "Ncaron" => 600_f32, + "Iacute" => 600_f32, + "plusminus" => 600_f32, + "brokenbar" => 600_f32, + "registered" => 600_f32, + "Gbreve" => 600_f32, + "Idotaccent" => 600_f32, + "summation" => 600_f32, + "Egrave" => 600_f32, + "racute" => 600_f32, + "omacron" => 600_f32, + "Zacute" => 600_f32, + "Zcaron" => 600_f32, + "greaterequal" => 600_f32, + "Eth" => 600_f32, + "Ccedilla" => 600_f32, + "lcommaaccent" => 600_f32, + "tcaron" => 600_f32, + "eogonek" => 600_f32, + "Uogonek" => 600_f32, + "Aacute" => 600_f32, + "Adieresis" => 600_f32, + "egrave" => 600_f32, + "zacute" => 600_f32, + "iogonek" => 600_f32, + "Oacute" => 600_f32, + "oacute" => 600_f32, + "amacron" => 600_f32, + "sacute" => 600_f32, + "idieresis" => 600_f32, + "Ocircumflex" => 600_f32, + "Ugrave" => 600_f32, + "Delta" => 600_f32, + "thorn" => 600_f32, + "twosuperior" => 600_f32, + "Odieresis" => 600_f32, + "mu" => 600_f32, + "igrave" => 600_f32, + "ohungarumlaut" => 600_f32, + "Eogonek" => 600_f32, + "dcroat" => 600_f32, + "threequarters" => 600_f32, + "Scedilla" => 600_f32, + "lcaron" => 600_f32, + "Kcommaaccent" => 600_f32, + "Lacute" => 600_f32, + "trademark" => 600_f32, + "edotaccent" => 600_f32, + "Igrave" => 600_f32, + "Imacron" => 600_f32, + "Lcaron" => 600_f32, + "onehalf" => 600_f32, + "lessequal" => 600_f32, + "ocircumflex" => 600_f32, + "ntilde" => 600_f32, + "Uhungarumlaut" => 600_f32, + "Eacute" => 600_f32, + "emacron" => 600_f32, + "gbreve" => 600_f32, + "onequarter" => 600_f32, + "Scaron" => 600_f32, + "Scommaaccent" => 600_f32, + "Ohungarumlaut" => 600_f32, + "degree" => 600_f32, + "ograve" => 600_f32, + "Ccaron" => 600_f32, + "ugrave" => 600_f32, + "radical" => 600_f32, + "Dcaron" => 600_f32, + "rcommaaccent" => 600_f32, + "Ntilde" => 600_f32, + "otilde" => 600_f32, + "Rcommaaccent" => 600_f32, + "Lcommaaccent" => 600_f32, + "Atilde" => 600_f32, + "Aogonek" => 600_f32, + "Aring" => 600_f32, + "Otilde" => 600_f32, + "zdotaccent" => 600_f32, + "Ecaron" => 600_f32, + "Iogonek" => 600_f32, + "kcommaaccent" => 600_f32, + "minus" => 600_f32, + "Icircumflex" => 600_f32, + "ncaron" => 600_f32, + "tcommaaccent" => 600_f32, + "logicalnot" => 600_f32, + "odieresis" => 600_f32, + "udieresis" => 600_f32, + "notequal" => 600_f32, + "gcommaaccent" => 600_f32, + "eth" => 600_f32, + "zcaron" => 600_f32, + "ncommaaccent" => 600_f32, + "onesuperior" => 600_f32, + "imacron" => 600_f32, + "Euro" => 600_f32, +}; + +pub(crate) static COURIER_OBLIQUE: phf::Map<&'static str, f32> = phf_map! { + "space" => 600_f32, + "exclam" => 600_f32, + "quotedbl" => 600_f32, + "numbersign" => 600_f32, + "dollar" => 600_f32, + "percent" => 600_f32, + "ampersand" => 600_f32, + "quoteright" => 600_f32, + "parenleft" => 600_f32, + "parenright" => 600_f32, + "asterisk" => 600_f32, + "plus" => 600_f32, + "comma" => 600_f32, + "hyphen" => 600_f32, + "period" => 600_f32, + "slash" => 600_f32, + "zero" => 600_f32, + "one" => 600_f32, + "two" => 600_f32, + "three" => 600_f32, + "four" => 600_f32, + "five" => 600_f32, + "six" => 600_f32, + "seven" => 600_f32, + "eight" => 600_f32, + "nine" => 600_f32, + "colon" => 600_f32, + "semicolon" => 600_f32, + "less" => 600_f32, + "equal" => 600_f32, + "greater" => 600_f32, + "question" => 600_f32, + "at" => 600_f32, + "A" => 600_f32, + "B" => 600_f32, + "C" => 600_f32, + "D" => 600_f32, + "E" => 600_f32, + "F" => 600_f32, + "G" => 600_f32, + "H" => 600_f32, + "I" => 600_f32, + "J" => 600_f32, + "K" => 600_f32, + "L" => 600_f32, + "M" => 600_f32, + "N" => 600_f32, + "O" => 600_f32, + "P" => 600_f32, + "Q" => 600_f32, + "R" => 600_f32, + "S" => 600_f32, + "T" => 600_f32, + "U" => 600_f32, + "V" => 600_f32, + "W" => 600_f32, + "X" => 600_f32, + "Y" => 600_f32, + "Z" => 600_f32, + "bracketleft" => 600_f32, + "backslash" => 600_f32, + "bracketright" => 600_f32, + "asciicircum" => 600_f32, + "underscore" => 600_f32, + "quoteleft" => 600_f32, + "a" => 600_f32, + "b" => 600_f32, + "c" => 600_f32, + "d" => 600_f32, + "e" => 600_f32, + "f" => 600_f32, + "g" => 600_f32, + "h" => 600_f32, + "i" => 600_f32, + "j" => 600_f32, + "k" => 600_f32, + "l" => 600_f32, + "m" => 600_f32, + "n" => 600_f32, + "o" => 600_f32, + "p" => 600_f32, + "q" => 600_f32, + "r" => 600_f32, + "s" => 600_f32, + "t" => 600_f32, + "u" => 600_f32, + "v" => 600_f32, + "w" => 600_f32, + "x" => 600_f32, + "y" => 600_f32, + "z" => 600_f32, + "braceleft" => 600_f32, + "bar" => 600_f32, + "braceright" => 600_f32, + "asciitilde" => 600_f32, + "exclamdown" => 600_f32, + "cent" => 600_f32, + "sterling" => 600_f32, + "fraction" => 600_f32, + "yen" => 600_f32, + "florin" => 600_f32, + "section" => 600_f32, + "currency" => 600_f32, + "quotesingle" => 600_f32, + "quotedblleft" => 600_f32, + "guillemotleft" => 600_f32, + "guilsinglleft" => 600_f32, + "guilsinglright" => 600_f32, + "fi" => 600_f32, + "fl" => 600_f32, + "endash" => 600_f32, + "dagger" => 600_f32, + "daggerdbl" => 600_f32, + "periodcentered" => 600_f32, + "paragraph" => 600_f32, + "bullet" => 600_f32, + "quotesinglbase" => 600_f32, + "quotedblbase" => 600_f32, + "quotedblright" => 600_f32, + "guillemotright" => 600_f32, + "ellipsis" => 600_f32, + "perthousand" => 600_f32, + "questiondown" => 600_f32, + "grave" => 600_f32, + "acute" => 600_f32, + "circumflex" => 600_f32, + "tilde" => 600_f32, + "macron" => 600_f32, + "breve" => 600_f32, + "dotaccent" => 600_f32, + "dieresis" => 600_f32, + "ring" => 600_f32, + "cedilla" => 600_f32, + "hungarumlaut" => 600_f32, + "ogonek" => 600_f32, + "caron" => 600_f32, + "emdash" => 600_f32, + "AE" => 600_f32, + "ordfeminine" => 600_f32, + "Lslash" => 600_f32, + "Oslash" => 600_f32, + "OE" => 600_f32, + "ordmasculine" => 600_f32, + "ae" => 600_f32, + "dotlessi" => 600_f32, + "lslash" => 600_f32, + "oslash" => 600_f32, + "oe" => 600_f32, + "germandbls" => 600_f32, + "Idieresis" => 600_f32, + "eacute" => 600_f32, + "abreve" => 600_f32, + "uhungarumlaut" => 600_f32, + "ecaron" => 600_f32, + "Ydieresis" => 600_f32, + "divide" => 600_f32, + "Yacute" => 600_f32, + "Acircumflex" => 600_f32, + "aacute" => 600_f32, + "Ucircumflex" => 600_f32, + "yacute" => 600_f32, + "scommaaccent" => 600_f32, + "ecircumflex" => 600_f32, + "Uring" => 600_f32, + "Udieresis" => 600_f32, + "aogonek" => 600_f32, + "Uacute" => 600_f32, + "uogonek" => 600_f32, + "Edieresis" => 600_f32, + "Dcroat" => 600_f32, + "commaaccent" => 600_f32, + "copyright" => 600_f32, + "Emacron" => 600_f32, + "ccaron" => 600_f32, + "aring" => 600_f32, + "Ncommaaccent" => 600_f32, + "lacute" => 600_f32, + "agrave" => 600_f32, + "Tcommaaccent" => 600_f32, + "Cacute" => 600_f32, + "atilde" => 600_f32, + "Edotaccent" => 600_f32, + "scaron" => 600_f32, + "scedilla" => 600_f32, + "iacute" => 600_f32, + "lozenge" => 600_f32, + "Rcaron" => 600_f32, + "Gcommaaccent" => 600_f32, + "ucircumflex" => 600_f32, + "acircumflex" => 600_f32, + "Amacron" => 600_f32, + "rcaron" => 600_f32, + "ccedilla" => 600_f32, + "Zdotaccent" => 600_f32, + "Thorn" => 600_f32, + "Omacron" => 600_f32, + "Racute" => 600_f32, + "Sacute" => 600_f32, + "dcaron" => 600_f32, + "Umacron" => 600_f32, + "uring" => 600_f32, + "threesuperior" => 600_f32, + "Ograve" => 600_f32, + "Agrave" => 600_f32, + "Abreve" => 600_f32, + "multiply" => 600_f32, + "uacute" => 600_f32, + "Tcaron" => 600_f32, + "partialdiff" => 600_f32, + "ydieresis" => 600_f32, + "Nacute" => 600_f32, + "icircumflex" => 600_f32, + "Ecircumflex" => 600_f32, + "adieresis" => 600_f32, + "edieresis" => 600_f32, + "cacute" => 600_f32, + "nacute" => 600_f32, + "umacron" => 600_f32, + "Ncaron" => 600_f32, + "Iacute" => 600_f32, + "plusminus" => 600_f32, + "brokenbar" => 600_f32, + "registered" => 600_f32, + "Gbreve" => 600_f32, + "Idotaccent" => 600_f32, + "summation" => 600_f32, + "Egrave" => 600_f32, + "racute" => 600_f32, + "omacron" => 600_f32, + "Zacute" => 600_f32, + "Zcaron" => 600_f32, + "greaterequal" => 600_f32, + "Eth" => 600_f32, + "Ccedilla" => 600_f32, + "lcommaaccent" => 600_f32, + "tcaron" => 600_f32, + "eogonek" => 600_f32, + "Uogonek" => 600_f32, + "Aacute" => 600_f32, + "Adieresis" => 600_f32, + "egrave" => 600_f32, + "zacute" => 600_f32, + "iogonek" => 600_f32, + "Oacute" => 600_f32, + "oacute" => 600_f32, + "amacron" => 600_f32, + "sacute" => 600_f32, + "idieresis" => 600_f32, + "Ocircumflex" => 600_f32, + "Ugrave" => 600_f32, + "Delta" => 600_f32, + "thorn" => 600_f32, + "twosuperior" => 600_f32, + "Odieresis" => 600_f32, + "mu" => 600_f32, + "igrave" => 600_f32, + "ohungarumlaut" => 600_f32, + "Eogonek" => 600_f32, + "dcroat" => 600_f32, + "threequarters" => 600_f32, + "Scedilla" => 600_f32, + "lcaron" => 600_f32, + "Kcommaaccent" => 600_f32, + "Lacute" => 600_f32, + "trademark" => 600_f32, + "edotaccent" => 600_f32, + "Igrave" => 600_f32, + "Imacron" => 600_f32, + "Lcaron" => 600_f32, + "onehalf" => 600_f32, + "lessequal" => 600_f32, + "ocircumflex" => 600_f32, + "ntilde" => 600_f32, + "Uhungarumlaut" => 600_f32, + "Eacute" => 600_f32, + "emacron" => 600_f32, + "gbreve" => 600_f32, + "onequarter" => 600_f32, + "Scaron" => 600_f32, + "Scommaaccent" => 600_f32, + "Ohungarumlaut" => 600_f32, + "degree" => 600_f32, + "ograve" => 600_f32, + "Ccaron" => 600_f32, + "ugrave" => 600_f32, + "radical" => 600_f32, + "Dcaron" => 600_f32, + "rcommaaccent" => 600_f32, + "Ntilde" => 600_f32, + "otilde" => 600_f32, + "Rcommaaccent" => 600_f32, + "Lcommaaccent" => 600_f32, + "Atilde" => 600_f32, + "Aogonek" => 600_f32, + "Aring" => 600_f32, + "Otilde" => 600_f32, + "zdotaccent" => 600_f32, + "Ecaron" => 600_f32, + "Iogonek" => 600_f32, + "kcommaaccent" => 600_f32, + "minus" => 600_f32, + "Icircumflex" => 600_f32, + "ncaron" => 600_f32, + "tcommaaccent" => 600_f32, + "logicalnot" => 600_f32, + "odieresis" => 600_f32, + "udieresis" => 600_f32, + "notequal" => 600_f32, + "gcommaaccent" => 600_f32, + "eth" => 600_f32, + "zcaron" => 600_f32, + "ncommaaccent" => 600_f32, + "onesuperior" => 600_f32, + "imacron" => 600_f32, + "Euro" => 600_f32, +}; + +pub(crate) static HELVETICA: phf::Map<&'static str, f32> = phf_map! { + "space" => 278_f32, + "exclam" => 278_f32, + "quotedbl" => 355_f32, + "numbersign" => 556_f32, + "dollar" => 556_f32, + "percent" => 889_f32, + "ampersand" => 667_f32, + "quoteright" => 222_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asterisk" => 389_f32, + "plus" => 584_f32, + "comma" => 278_f32, + "hyphen" => 333_f32, + "period" => 278_f32, + "slash" => 278_f32, + "zero" => 556_f32, + "one" => 556_f32, + "two" => 556_f32, + "three" => 556_f32, + "four" => 556_f32, + "five" => 556_f32, + "six" => 556_f32, + "seven" => 556_f32, + "eight" => 556_f32, + "nine" => 556_f32, + "colon" => 278_f32, + "semicolon" => 278_f32, + "less" => 584_f32, + "equal" => 584_f32, + "greater" => 584_f32, + "question" => 556_f32, + "at" => 1015_f32, + "A" => 667_f32, + "B" => 667_f32, + "C" => 722_f32, + "D" => 722_f32, + "E" => 667_f32, + "F" => 611_f32, + "G" => 778_f32, + "H" => 722_f32, + "I" => 278_f32, + "J" => 500_f32, + "K" => 667_f32, + "L" => 556_f32, + "M" => 833_f32, + "N" => 722_f32, + "O" => 778_f32, + "P" => 667_f32, + "Q" => 778_f32, + "R" => 722_f32, + "S" => 667_f32, + "T" => 611_f32, + "U" => 722_f32, + "V" => 667_f32, + "W" => 944_f32, + "X" => 667_f32, + "Y" => 667_f32, + "Z" => 611_f32, + "bracketleft" => 278_f32, + "backslash" => 278_f32, + "bracketright" => 278_f32, + "asciicircum" => 469_f32, + "underscore" => 556_f32, + "quoteleft" => 222_f32, + "a" => 556_f32, + "b" => 556_f32, + "c" => 500_f32, + "d" => 556_f32, + "e" => 556_f32, + "f" => 278_f32, + "g" => 556_f32, + "h" => 556_f32, + "i" => 222_f32, + "j" => 222_f32, + "k" => 500_f32, + "l" => 222_f32, + "m" => 833_f32, + "n" => 556_f32, + "o" => 556_f32, + "p" => 556_f32, + "q" => 556_f32, + "r" => 333_f32, + "s" => 500_f32, + "t" => 278_f32, + "u" => 556_f32, + "v" => 500_f32, + "w" => 722_f32, + "x" => 500_f32, + "y" => 500_f32, + "z" => 500_f32, + "braceleft" => 334_f32, + "bar" => 260_f32, + "braceright" => 334_f32, + "asciitilde" => 584_f32, + "exclamdown" => 333_f32, + "cent" => 556_f32, + "sterling" => 556_f32, + "fraction" => 167_f32, + "yen" => 556_f32, + "florin" => 556_f32, + "section" => 556_f32, + "currency" => 556_f32, + "quotesingle" => 191_f32, + "quotedblleft" => 333_f32, + "guillemotleft" => 556_f32, + "guilsinglleft" => 333_f32, + "guilsinglright" => 333_f32, + "fi" => 500_f32, + "fl" => 500_f32, + "endash" => 556_f32, + "dagger" => 556_f32, + "daggerdbl" => 556_f32, + "periodcentered" => 278_f32, + "paragraph" => 537_f32, + "bullet" => 350_f32, + "quotesinglbase" => 222_f32, + "quotedblbase" => 333_f32, + "quotedblright" => 333_f32, + "guillemotright" => 556_f32, + "ellipsis" => 1000_f32, + "perthousand" => 1000_f32, + "questiondown" => 611_f32, + "grave" => 333_f32, + "acute" => 333_f32, + "circumflex" => 333_f32, + "tilde" => 333_f32, + "macron" => 333_f32, + "breve" => 333_f32, + "dotaccent" => 333_f32, + "dieresis" => 333_f32, + "ring" => 333_f32, + "cedilla" => 333_f32, + "hungarumlaut" => 333_f32, + "ogonek" => 333_f32, + "caron" => 333_f32, + "emdash" => 1000_f32, + "AE" => 1000_f32, + "ordfeminine" => 370_f32, + "Lslash" => 556_f32, + "Oslash" => 778_f32, + "OE" => 1000_f32, + "ordmasculine" => 365_f32, + "ae" => 889_f32, + "dotlessi" => 278_f32, + "lslash" => 222_f32, + "oslash" => 611_f32, + "oe" => 944_f32, + "germandbls" => 611_f32, + "Idieresis" => 278_f32, + "eacute" => 556_f32, + "abreve" => 556_f32, + "uhungarumlaut" => 556_f32, + "ecaron" => 556_f32, + "Ydieresis" => 667_f32, + "divide" => 584_f32, + "Yacute" => 667_f32, + "Acircumflex" => 667_f32, + "aacute" => 556_f32, + "Ucircumflex" => 722_f32, + "yacute" => 500_f32, + "scommaaccent" => 500_f32, + "ecircumflex" => 556_f32, + "Uring" => 722_f32, + "Udieresis" => 722_f32, + "aogonek" => 556_f32, + "Uacute" => 722_f32, + "uogonek" => 556_f32, + "Edieresis" => 667_f32, + "Dcroat" => 722_f32, + "commaaccent" => 250_f32, + "copyright" => 737_f32, + "Emacron" => 667_f32, + "ccaron" => 500_f32, + "aring" => 556_f32, + "Ncommaaccent" => 722_f32, + "lacute" => 222_f32, + "agrave" => 556_f32, + "Tcommaaccent" => 611_f32, + "Cacute" => 722_f32, + "atilde" => 556_f32, + "Edotaccent" => 667_f32, + "scaron" => 500_f32, + "scedilla" => 500_f32, + "iacute" => 278_f32, + "lozenge" => 471_f32, + "Rcaron" => 722_f32, + "Gcommaaccent" => 778_f32, + "ucircumflex" => 556_f32, + "acircumflex" => 556_f32, + "Amacron" => 667_f32, + "rcaron" => 333_f32, + "ccedilla" => 500_f32, + "Zdotaccent" => 611_f32, + "Thorn" => 667_f32, + "Omacron" => 778_f32, + "Racute" => 722_f32, + "Sacute" => 667_f32, + "dcaron" => 643_f32, + "Umacron" => 722_f32, + "uring" => 556_f32, + "threesuperior" => 333_f32, + "Ograve" => 778_f32, + "Agrave" => 667_f32, + "Abreve" => 667_f32, + "multiply" => 584_f32, + "uacute" => 556_f32, + "Tcaron" => 611_f32, + "partialdiff" => 476_f32, + "ydieresis" => 500_f32, + "Nacute" => 722_f32, + "icircumflex" => 278_f32, + "Ecircumflex" => 667_f32, + "adieresis" => 556_f32, + "edieresis" => 556_f32, + "cacute" => 500_f32, + "nacute" => 556_f32, + "umacron" => 556_f32, + "Ncaron" => 722_f32, + "Iacute" => 278_f32, + "plusminus" => 584_f32, + "brokenbar" => 260_f32, + "registered" => 737_f32, + "Gbreve" => 778_f32, + "Idotaccent" => 278_f32, + "summation" => 600_f32, + "Egrave" => 667_f32, + "racute" => 333_f32, + "omacron" => 556_f32, + "Zacute" => 611_f32, + "Zcaron" => 611_f32, + "greaterequal" => 549_f32, + "Eth" => 722_f32, + "Ccedilla" => 722_f32, + "lcommaaccent" => 222_f32, + "tcaron" => 317_f32, + "eogonek" => 556_f32, + "Uogonek" => 722_f32, + "Aacute" => 667_f32, + "Adieresis" => 667_f32, + "egrave" => 556_f32, + "zacute" => 500_f32, + "iogonek" => 222_f32, + "Oacute" => 778_f32, + "oacute" => 556_f32, + "amacron" => 556_f32, + "sacute" => 500_f32, + "idieresis" => 278_f32, + "Ocircumflex" => 778_f32, + "Ugrave" => 722_f32, + "Delta" => 612_f32, + "thorn" => 556_f32, + "twosuperior" => 333_f32, + "Odieresis" => 778_f32, + "mu" => 556_f32, + "igrave" => 278_f32, + "ohungarumlaut" => 556_f32, + "Eogonek" => 667_f32, + "dcroat" => 556_f32, + "threequarters" => 834_f32, + "Scedilla" => 667_f32, + "lcaron" => 299_f32, + "Kcommaaccent" => 667_f32, + "Lacute" => 556_f32, + "trademark" => 1000_f32, + "edotaccent" => 556_f32, + "Igrave" => 278_f32, + "Imacron" => 278_f32, + "Lcaron" => 556_f32, + "onehalf" => 834_f32, + "lessequal" => 549_f32, + "ocircumflex" => 556_f32, + "ntilde" => 556_f32, + "Uhungarumlaut" => 722_f32, + "Eacute" => 667_f32, + "emacron" => 556_f32, + "gbreve" => 556_f32, + "onequarter" => 834_f32, + "Scaron" => 667_f32, + "Scommaaccent" => 667_f32, + "Ohungarumlaut" => 778_f32, + "degree" => 400_f32, + "ograve" => 556_f32, + "Ccaron" => 722_f32, + "ugrave" => 556_f32, + "radical" => 453_f32, + "Dcaron" => 722_f32, + "rcommaaccent" => 333_f32, + "Ntilde" => 722_f32, + "otilde" => 556_f32, + "Rcommaaccent" => 722_f32, + "Lcommaaccent" => 556_f32, + "Atilde" => 667_f32, + "Aogonek" => 667_f32, + "Aring" => 667_f32, + "Otilde" => 778_f32, + "zdotaccent" => 500_f32, + "Ecaron" => 667_f32, + "Iogonek" => 278_f32, + "kcommaaccent" => 500_f32, + "minus" => 584_f32, + "Icircumflex" => 278_f32, + "ncaron" => 556_f32, + "tcommaaccent" => 278_f32, + "logicalnot" => 584_f32, + "odieresis" => 556_f32, + "udieresis" => 556_f32, + "notequal" => 549_f32, + "gcommaaccent" => 556_f32, + "eth" => 556_f32, + "zcaron" => 500_f32, + "ncommaaccent" => 556_f32, + "onesuperior" => 333_f32, + "imacron" => 278_f32, + "Euro" => 556_f32, +}; + +pub(crate) static HELVETICA_BOLD: phf::Map<&'static str, f32> = phf_map! { + "space" => 278_f32, + "exclam" => 333_f32, + "quotedbl" => 474_f32, + "numbersign" => 556_f32, + "dollar" => 556_f32, + "percent" => 889_f32, + "ampersand" => 722_f32, + "quoteright" => 278_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asterisk" => 389_f32, + "plus" => 584_f32, + "comma" => 278_f32, + "hyphen" => 333_f32, + "period" => 278_f32, + "slash" => 278_f32, + "zero" => 556_f32, + "one" => 556_f32, + "two" => 556_f32, + "three" => 556_f32, + "four" => 556_f32, + "five" => 556_f32, + "six" => 556_f32, + "seven" => 556_f32, + "eight" => 556_f32, + "nine" => 556_f32, + "colon" => 333_f32, + "semicolon" => 333_f32, + "less" => 584_f32, + "equal" => 584_f32, + "greater" => 584_f32, + "question" => 611_f32, + "at" => 975_f32, + "A" => 722_f32, + "B" => 722_f32, + "C" => 722_f32, + "D" => 722_f32, + "E" => 667_f32, + "F" => 611_f32, + "G" => 778_f32, + "H" => 722_f32, + "I" => 278_f32, + "J" => 556_f32, + "K" => 722_f32, + "L" => 611_f32, + "M" => 833_f32, + "N" => 722_f32, + "O" => 778_f32, + "P" => 667_f32, + "Q" => 778_f32, + "R" => 722_f32, + "S" => 667_f32, + "T" => 611_f32, + "U" => 722_f32, + "V" => 667_f32, + "W" => 944_f32, + "X" => 667_f32, + "Y" => 667_f32, + "Z" => 611_f32, + "bracketleft" => 333_f32, + "backslash" => 278_f32, + "bracketright" => 333_f32, + "asciicircum" => 584_f32, + "underscore" => 556_f32, + "quoteleft" => 278_f32, + "a" => 556_f32, + "b" => 611_f32, + "c" => 556_f32, + "d" => 611_f32, + "e" => 556_f32, + "f" => 333_f32, + "g" => 611_f32, + "h" => 611_f32, + "i" => 278_f32, + "j" => 278_f32, + "k" => 556_f32, + "l" => 278_f32, + "m" => 889_f32, + "n" => 611_f32, + "o" => 611_f32, + "p" => 611_f32, + "q" => 611_f32, + "r" => 389_f32, + "s" => 556_f32, + "t" => 333_f32, + "u" => 611_f32, + "v" => 556_f32, + "w" => 778_f32, + "x" => 556_f32, + "y" => 556_f32, + "z" => 500_f32, + "braceleft" => 389_f32, + "bar" => 280_f32, + "braceright" => 389_f32, + "asciitilde" => 584_f32, + "exclamdown" => 333_f32, + "cent" => 556_f32, + "sterling" => 556_f32, + "fraction" => 167_f32, + "yen" => 556_f32, + "florin" => 556_f32, + "section" => 556_f32, + "currency" => 556_f32, + "quotesingle" => 238_f32, + "quotedblleft" => 500_f32, + "guillemotleft" => 556_f32, + "guilsinglleft" => 333_f32, + "guilsinglright" => 333_f32, + "fi" => 611_f32, + "fl" => 611_f32, + "endash" => 556_f32, + "dagger" => 556_f32, + "daggerdbl" => 556_f32, + "periodcentered" => 278_f32, + "paragraph" => 556_f32, + "bullet" => 350_f32, + "quotesinglbase" => 278_f32, + "quotedblbase" => 500_f32, + "quotedblright" => 500_f32, + "guillemotright" => 556_f32, + "ellipsis" => 1000_f32, + "perthousand" => 1000_f32, + "questiondown" => 611_f32, + "grave" => 333_f32, + "acute" => 333_f32, + "circumflex" => 333_f32, + "tilde" => 333_f32, + "macron" => 333_f32, + "breve" => 333_f32, + "dotaccent" => 333_f32, + "dieresis" => 333_f32, + "ring" => 333_f32, + "cedilla" => 333_f32, + "hungarumlaut" => 333_f32, + "ogonek" => 333_f32, + "caron" => 333_f32, + "emdash" => 1000_f32, + "AE" => 1000_f32, + "ordfeminine" => 370_f32, + "Lslash" => 611_f32, + "Oslash" => 778_f32, + "OE" => 1000_f32, + "ordmasculine" => 365_f32, + "ae" => 889_f32, + "dotlessi" => 278_f32, + "lslash" => 278_f32, + "oslash" => 611_f32, + "oe" => 944_f32, + "germandbls" => 611_f32, + "Idieresis" => 278_f32, + "eacute" => 556_f32, + "abreve" => 556_f32, + "uhungarumlaut" => 611_f32, + "ecaron" => 556_f32, + "Ydieresis" => 667_f32, + "divide" => 584_f32, + "Yacute" => 667_f32, + "Acircumflex" => 722_f32, + "aacute" => 556_f32, + "Ucircumflex" => 722_f32, + "yacute" => 556_f32, + "scommaaccent" => 556_f32, + "ecircumflex" => 556_f32, + "Uring" => 722_f32, + "Udieresis" => 722_f32, + "aogonek" => 556_f32, + "Uacute" => 722_f32, + "uogonek" => 611_f32, + "Edieresis" => 667_f32, + "Dcroat" => 722_f32, + "commaaccent" => 250_f32, + "copyright" => 737_f32, + "Emacron" => 667_f32, + "ccaron" => 556_f32, + "aring" => 556_f32, + "Ncommaaccent" => 722_f32, + "lacute" => 278_f32, + "agrave" => 556_f32, + "Tcommaaccent" => 611_f32, + "Cacute" => 722_f32, + "atilde" => 556_f32, + "Edotaccent" => 667_f32, + "scaron" => 556_f32, + "scedilla" => 556_f32, + "iacute" => 278_f32, + "lozenge" => 494_f32, + "Rcaron" => 722_f32, + "Gcommaaccent" => 778_f32, + "ucircumflex" => 611_f32, + "acircumflex" => 556_f32, + "Amacron" => 722_f32, + "rcaron" => 389_f32, + "ccedilla" => 556_f32, + "Zdotaccent" => 611_f32, + "Thorn" => 667_f32, + "Omacron" => 778_f32, + "Racute" => 722_f32, + "Sacute" => 667_f32, + "dcaron" => 743_f32, + "Umacron" => 722_f32, + "uring" => 611_f32, + "threesuperior" => 333_f32, + "Ograve" => 778_f32, + "Agrave" => 722_f32, + "Abreve" => 722_f32, + "multiply" => 584_f32, + "uacute" => 611_f32, + "Tcaron" => 611_f32, + "partialdiff" => 494_f32, + "ydieresis" => 556_f32, + "Nacute" => 722_f32, + "icircumflex" => 278_f32, + "Ecircumflex" => 667_f32, + "adieresis" => 556_f32, + "edieresis" => 556_f32, + "cacute" => 556_f32, + "nacute" => 611_f32, + "umacron" => 611_f32, + "Ncaron" => 722_f32, + "Iacute" => 278_f32, + "plusminus" => 584_f32, + "brokenbar" => 280_f32, + "registered" => 737_f32, + "Gbreve" => 778_f32, + "Idotaccent" => 278_f32, + "summation" => 600_f32, + "Egrave" => 667_f32, + "racute" => 389_f32, + "omacron" => 611_f32, + "Zacute" => 611_f32, + "Zcaron" => 611_f32, + "greaterequal" => 549_f32, + "Eth" => 722_f32, + "Ccedilla" => 722_f32, + "lcommaaccent" => 278_f32, + "tcaron" => 389_f32, + "eogonek" => 556_f32, + "Uogonek" => 722_f32, + "Aacute" => 722_f32, + "Adieresis" => 722_f32, + "egrave" => 556_f32, + "zacute" => 500_f32, + "iogonek" => 278_f32, + "Oacute" => 778_f32, + "oacute" => 611_f32, + "amacron" => 556_f32, + "sacute" => 556_f32, + "idieresis" => 278_f32, + "Ocircumflex" => 778_f32, + "Ugrave" => 722_f32, + "Delta" => 612_f32, + "thorn" => 611_f32, + "twosuperior" => 333_f32, + "Odieresis" => 778_f32, + "mu" => 611_f32, + "igrave" => 278_f32, + "ohungarumlaut" => 611_f32, + "Eogonek" => 667_f32, + "dcroat" => 611_f32, + "threequarters" => 834_f32, + "Scedilla" => 667_f32, + "lcaron" => 400_f32, + "Kcommaaccent" => 722_f32, + "Lacute" => 611_f32, + "trademark" => 1000_f32, + "edotaccent" => 556_f32, + "Igrave" => 278_f32, + "Imacron" => 278_f32, + "Lcaron" => 611_f32, + "onehalf" => 834_f32, + "lessequal" => 549_f32, + "ocircumflex" => 611_f32, + "ntilde" => 611_f32, + "Uhungarumlaut" => 722_f32, + "Eacute" => 667_f32, + "emacron" => 556_f32, + "gbreve" => 611_f32, + "onequarter" => 834_f32, + "Scaron" => 667_f32, + "Scommaaccent" => 667_f32, + "Ohungarumlaut" => 778_f32, + "degree" => 400_f32, + "ograve" => 611_f32, + "Ccaron" => 722_f32, + "ugrave" => 611_f32, + "radical" => 549_f32, + "Dcaron" => 722_f32, + "rcommaaccent" => 389_f32, + "Ntilde" => 722_f32, + "otilde" => 611_f32, + "Rcommaaccent" => 722_f32, + "Lcommaaccent" => 611_f32, + "Atilde" => 722_f32, + "Aogonek" => 722_f32, + "Aring" => 722_f32, + "Otilde" => 778_f32, + "zdotaccent" => 500_f32, + "Ecaron" => 667_f32, + "Iogonek" => 278_f32, + "kcommaaccent" => 556_f32, + "minus" => 584_f32, + "Icircumflex" => 278_f32, + "ncaron" => 611_f32, + "tcommaaccent" => 333_f32, + "logicalnot" => 584_f32, + "odieresis" => 611_f32, + "udieresis" => 611_f32, + "notequal" => 549_f32, + "gcommaaccent" => 611_f32, + "eth" => 611_f32, + "zcaron" => 500_f32, + "ncommaaccent" => 611_f32, + "onesuperior" => 333_f32, + "imacron" => 278_f32, + "Euro" => 556_f32, +}; + +pub(crate) static HELVETICA_BOLD_OBLIQUE: phf::Map<&'static str, f32> = phf_map! { + "space" => 278_f32, + "exclam" => 333_f32, + "quotedbl" => 474_f32, + "numbersign" => 556_f32, + "dollar" => 556_f32, + "percent" => 889_f32, + "ampersand" => 722_f32, + "quoteright" => 278_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asterisk" => 389_f32, + "plus" => 584_f32, + "comma" => 278_f32, + "hyphen" => 333_f32, + "period" => 278_f32, + "slash" => 278_f32, + "zero" => 556_f32, + "one" => 556_f32, + "two" => 556_f32, + "three" => 556_f32, + "four" => 556_f32, + "five" => 556_f32, + "six" => 556_f32, + "seven" => 556_f32, + "eight" => 556_f32, + "nine" => 556_f32, + "colon" => 333_f32, + "semicolon" => 333_f32, + "less" => 584_f32, + "equal" => 584_f32, + "greater" => 584_f32, + "question" => 611_f32, + "at" => 975_f32, + "A" => 722_f32, + "B" => 722_f32, + "C" => 722_f32, + "D" => 722_f32, + "E" => 667_f32, + "F" => 611_f32, + "G" => 778_f32, + "H" => 722_f32, + "I" => 278_f32, + "J" => 556_f32, + "K" => 722_f32, + "L" => 611_f32, + "M" => 833_f32, + "N" => 722_f32, + "O" => 778_f32, + "P" => 667_f32, + "Q" => 778_f32, + "R" => 722_f32, + "S" => 667_f32, + "T" => 611_f32, + "U" => 722_f32, + "V" => 667_f32, + "W" => 944_f32, + "X" => 667_f32, + "Y" => 667_f32, + "Z" => 611_f32, + "bracketleft" => 333_f32, + "backslash" => 278_f32, + "bracketright" => 333_f32, + "asciicircum" => 584_f32, + "underscore" => 556_f32, + "quoteleft" => 278_f32, + "a" => 556_f32, + "b" => 611_f32, + "c" => 556_f32, + "d" => 611_f32, + "e" => 556_f32, + "f" => 333_f32, + "g" => 611_f32, + "h" => 611_f32, + "i" => 278_f32, + "j" => 278_f32, + "k" => 556_f32, + "l" => 278_f32, + "m" => 889_f32, + "n" => 611_f32, + "o" => 611_f32, + "p" => 611_f32, + "q" => 611_f32, + "r" => 389_f32, + "s" => 556_f32, + "t" => 333_f32, + "u" => 611_f32, + "v" => 556_f32, + "w" => 778_f32, + "x" => 556_f32, + "y" => 556_f32, + "z" => 500_f32, + "braceleft" => 389_f32, + "bar" => 280_f32, + "braceright" => 389_f32, + "asciitilde" => 584_f32, + "exclamdown" => 333_f32, + "cent" => 556_f32, + "sterling" => 556_f32, + "fraction" => 167_f32, + "yen" => 556_f32, + "florin" => 556_f32, + "section" => 556_f32, + "currency" => 556_f32, + "quotesingle" => 238_f32, + "quotedblleft" => 500_f32, + "guillemotleft" => 556_f32, + "guilsinglleft" => 333_f32, + "guilsinglright" => 333_f32, + "fi" => 611_f32, + "fl" => 611_f32, + "endash" => 556_f32, + "dagger" => 556_f32, + "daggerdbl" => 556_f32, + "periodcentered" => 278_f32, + "paragraph" => 556_f32, + "bullet" => 350_f32, + "quotesinglbase" => 278_f32, + "quotedblbase" => 500_f32, + "quotedblright" => 500_f32, + "guillemotright" => 556_f32, + "ellipsis" => 1000_f32, + "perthousand" => 1000_f32, + "questiondown" => 611_f32, + "grave" => 333_f32, + "acute" => 333_f32, + "circumflex" => 333_f32, + "tilde" => 333_f32, + "macron" => 333_f32, + "breve" => 333_f32, + "dotaccent" => 333_f32, + "dieresis" => 333_f32, + "ring" => 333_f32, + "cedilla" => 333_f32, + "hungarumlaut" => 333_f32, + "ogonek" => 333_f32, + "caron" => 333_f32, + "emdash" => 1000_f32, + "AE" => 1000_f32, + "ordfeminine" => 370_f32, + "Lslash" => 611_f32, + "Oslash" => 778_f32, + "OE" => 1000_f32, + "ordmasculine" => 365_f32, + "ae" => 889_f32, + "dotlessi" => 278_f32, + "lslash" => 278_f32, + "oslash" => 611_f32, + "oe" => 944_f32, + "germandbls" => 611_f32, + "Idieresis" => 278_f32, + "eacute" => 556_f32, + "abreve" => 556_f32, + "uhungarumlaut" => 611_f32, + "ecaron" => 556_f32, + "Ydieresis" => 667_f32, + "divide" => 584_f32, + "Yacute" => 667_f32, + "Acircumflex" => 722_f32, + "aacute" => 556_f32, + "Ucircumflex" => 722_f32, + "yacute" => 556_f32, + "scommaaccent" => 556_f32, + "ecircumflex" => 556_f32, + "Uring" => 722_f32, + "Udieresis" => 722_f32, + "aogonek" => 556_f32, + "Uacute" => 722_f32, + "uogonek" => 611_f32, + "Edieresis" => 667_f32, + "Dcroat" => 722_f32, + "commaaccent" => 250_f32, + "copyright" => 737_f32, + "Emacron" => 667_f32, + "ccaron" => 556_f32, + "aring" => 556_f32, + "Ncommaaccent" => 722_f32, + "lacute" => 278_f32, + "agrave" => 556_f32, + "Tcommaaccent" => 611_f32, + "Cacute" => 722_f32, + "atilde" => 556_f32, + "Edotaccent" => 667_f32, + "scaron" => 556_f32, + "scedilla" => 556_f32, + "iacute" => 278_f32, + "lozenge" => 494_f32, + "Rcaron" => 722_f32, + "Gcommaaccent" => 778_f32, + "ucircumflex" => 611_f32, + "acircumflex" => 556_f32, + "Amacron" => 722_f32, + "rcaron" => 389_f32, + "ccedilla" => 556_f32, + "Zdotaccent" => 611_f32, + "Thorn" => 667_f32, + "Omacron" => 778_f32, + "Racute" => 722_f32, + "Sacute" => 667_f32, + "dcaron" => 743_f32, + "Umacron" => 722_f32, + "uring" => 611_f32, + "threesuperior" => 333_f32, + "Ograve" => 778_f32, + "Agrave" => 722_f32, + "Abreve" => 722_f32, + "multiply" => 584_f32, + "uacute" => 611_f32, + "Tcaron" => 611_f32, + "partialdiff" => 494_f32, + "ydieresis" => 556_f32, + "Nacute" => 722_f32, + "icircumflex" => 278_f32, + "Ecircumflex" => 667_f32, + "adieresis" => 556_f32, + "edieresis" => 556_f32, + "cacute" => 556_f32, + "nacute" => 611_f32, + "umacron" => 611_f32, + "Ncaron" => 722_f32, + "Iacute" => 278_f32, + "plusminus" => 584_f32, + "brokenbar" => 280_f32, + "registered" => 737_f32, + "Gbreve" => 778_f32, + "Idotaccent" => 278_f32, + "summation" => 600_f32, + "Egrave" => 667_f32, + "racute" => 389_f32, + "omacron" => 611_f32, + "Zacute" => 611_f32, + "Zcaron" => 611_f32, + "greaterequal" => 549_f32, + "Eth" => 722_f32, + "Ccedilla" => 722_f32, + "lcommaaccent" => 278_f32, + "tcaron" => 389_f32, + "eogonek" => 556_f32, + "Uogonek" => 722_f32, + "Aacute" => 722_f32, + "Adieresis" => 722_f32, + "egrave" => 556_f32, + "zacute" => 500_f32, + "iogonek" => 278_f32, + "Oacute" => 778_f32, + "oacute" => 611_f32, + "amacron" => 556_f32, + "sacute" => 556_f32, + "idieresis" => 278_f32, + "Ocircumflex" => 778_f32, + "Ugrave" => 722_f32, + "Delta" => 612_f32, + "thorn" => 611_f32, + "twosuperior" => 333_f32, + "Odieresis" => 778_f32, + "mu" => 611_f32, + "igrave" => 278_f32, + "ohungarumlaut" => 611_f32, + "Eogonek" => 667_f32, + "dcroat" => 611_f32, + "threequarters" => 834_f32, + "Scedilla" => 667_f32, + "lcaron" => 400_f32, + "Kcommaaccent" => 722_f32, + "Lacute" => 611_f32, + "trademark" => 1000_f32, + "edotaccent" => 556_f32, + "Igrave" => 278_f32, + "Imacron" => 278_f32, + "Lcaron" => 611_f32, + "onehalf" => 834_f32, + "lessequal" => 549_f32, + "ocircumflex" => 611_f32, + "ntilde" => 611_f32, + "Uhungarumlaut" => 722_f32, + "Eacute" => 667_f32, + "emacron" => 556_f32, + "gbreve" => 611_f32, + "onequarter" => 834_f32, + "Scaron" => 667_f32, + "Scommaaccent" => 667_f32, + "Ohungarumlaut" => 778_f32, + "degree" => 400_f32, + "ograve" => 611_f32, + "Ccaron" => 722_f32, + "ugrave" => 611_f32, + "radical" => 549_f32, + "Dcaron" => 722_f32, + "rcommaaccent" => 389_f32, + "Ntilde" => 722_f32, + "otilde" => 611_f32, + "Rcommaaccent" => 722_f32, + "Lcommaaccent" => 611_f32, + "Atilde" => 722_f32, + "Aogonek" => 722_f32, + "Aring" => 722_f32, + "Otilde" => 778_f32, + "zdotaccent" => 500_f32, + "Ecaron" => 667_f32, + "Iogonek" => 278_f32, + "kcommaaccent" => 556_f32, + "minus" => 584_f32, + "Icircumflex" => 278_f32, + "ncaron" => 611_f32, + "tcommaaccent" => 333_f32, + "logicalnot" => 584_f32, + "odieresis" => 611_f32, + "udieresis" => 611_f32, + "notequal" => 549_f32, + "gcommaaccent" => 611_f32, + "eth" => 611_f32, + "zcaron" => 500_f32, + "ncommaaccent" => 611_f32, + "onesuperior" => 333_f32, + "imacron" => 278_f32, + "Euro" => 556_f32, +}; + +pub(crate) static HELVETICA_OBLIQUE: phf::Map<&'static str, f32> = phf_map! { + "space" => 278_f32, + "exclam" => 278_f32, + "quotedbl" => 355_f32, + "numbersign" => 556_f32, + "dollar" => 556_f32, + "percent" => 889_f32, + "ampersand" => 667_f32, + "quoteright" => 222_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asterisk" => 389_f32, + "plus" => 584_f32, + "comma" => 278_f32, + "hyphen" => 333_f32, + "period" => 278_f32, + "slash" => 278_f32, + "zero" => 556_f32, + "one" => 556_f32, + "two" => 556_f32, + "three" => 556_f32, + "four" => 556_f32, + "five" => 556_f32, + "six" => 556_f32, + "seven" => 556_f32, + "eight" => 556_f32, + "nine" => 556_f32, + "colon" => 278_f32, + "semicolon" => 278_f32, + "less" => 584_f32, + "equal" => 584_f32, + "greater" => 584_f32, + "question" => 556_f32, + "at" => 1015_f32, + "A" => 667_f32, + "B" => 667_f32, + "C" => 722_f32, + "D" => 722_f32, + "E" => 667_f32, + "F" => 611_f32, + "G" => 778_f32, + "H" => 722_f32, + "I" => 278_f32, + "J" => 500_f32, + "K" => 667_f32, + "L" => 556_f32, + "M" => 833_f32, + "N" => 722_f32, + "O" => 778_f32, + "P" => 667_f32, + "Q" => 778_f32, + "R" => 722_f32, + "S" => 667_f32, + "T" => 611_f32, + "U" => 722_f32, + "V" => 667_f32, + "W" => 944_f32, + "X" => 667_f32, + "Y" => 667_f32, + "Z" => 611_f32, + "bracketleft" => 278_f32, + "backslash" => 278_f32, + "bracketright" => 278_f32, + "asciicircum" => 469_f32, + "underscore" => 556_f32, + "quoteleft" => 222_f32, + "a" => 556_f32, + "b" => 556_f32, + "c" => 500_f32, + "d" => 556_f32, + "e" => 556_f32, + "f" => 278_f32, + "g" => 556_f32, + "h" => 556_f32, + "i" => 222_f32, + "j" => 222_f32, + "k" => 500_f32, + "l" => 222_f32, + "m" => 833_f32, + "n" => 556_f32, + "o" => 556_f32, + "p" => 556_f32, + "q" => 556_f32, + "r" => 333_f32, + "s" => 500_f32, + "t" => 278_f32, + "u" => 556_f32, + "v" => 500_f32, + "w" => 722_f32, + "x" => 500_f32, + "y" => 500_f32, + "z" => 500_f32, + "braceleft" => 334_f32, + "bar" => 260_f32, + "braceright" => 334_f32, + "asciitilde" => 584_f32, + "exclamdown" => 333_f32, + "cent" => 556_f32, + "sterling" => 556_f32, + "fraction" => 167_f32, + "yen" => 556_f32, + "florin" => 556_f32, + "section" => 556_f32, + "currency" => 556_f32, + "quotesingle" => 191_f32, + "quotedblleft" => 333_f32, + "guillemotleft" => 556_f32, + "guilsinglleft" => 333_f32, + "guilsinglright" => 333_f32, + "fi" => 500_f32, + "fl" => 500_f32, + "endash" => 556_f32, + "dagger" => 556_f32, + "daggerdbl" => 556_f32, + "periodcentered" => 278_f32, + "paragraph" => 537_f32, + "bullet" => 350_f32, + "quotesinglbase" => 222_f32, + "quotedblbase" => 333_f32, + "quotedblright" => 333_f32, + "guillemotright" => 556_f32, + "ellipsis" => 1000_f32, + "perthousand" => 1000_f32, + "questiondown" => 611_f32, + "grave" => 333_f32, + "acute" => 333_f32, + "circumflex" => 333_f32, + "tilde" => 333_f32, + "macron" => 333_f32, + "breve" => 333_f32, + "dotaccent" => 333_f32, + "dieresis" => 333_f32, + "ring" => 333_f32, + "cedilla" => 333_f32, + "hungarumlaut" => 333_f32, + "ogonek" => 333_f32, + "caron" => 333_f32, + "emdash" => 1000_f32, + "AE" => 1000_f32, + "ordfeminine" => 370_f32, + "Lslash" => 556_f32, + "Oslash" => 778_f32, + "OE" => 1000_f32, + "ordmasculine" => 365_f32, + "ae" => 889_f32, + "dotlessi" => 278_f32, + "lslash" => 222_f32, + "oslash" => 611_f32, + "oe" => 944_f32, + "germandbls" => 611_f32, + "Idieresis" => 278_f32, + "eacute" => 556_f32, + "abreve" => 556_f32, + "uhungarumlaut" => 556_f32, + "ecaron" => 556_f32, + "Ydieresis" => 667_f32, + "divide" => 584_f32, + "Yacute" => 667_f32, + "Acircumflex" => 667_f32, + "aacute" => 556_f32, + "Ucircumflex" => 722_f32, + "yacute" => 500_f32, + "scommaaccent" => 500_f32, + "ecircumflex" => 556_f32, + "Uring" => 722_f32, + "Udieresis" => 722_f32, + "aogonek" => 556_f32, + "Uacute" => 722_f32, + "uogonek" => 556_f32, + "Edieresis" => 667_f32, + "Dcroat" => 722_f32, + "commaaccent" => 250_f32, + "copyright" => 737_f32, + "Emacron" => 667_f32, + "ccaron" => 500_f32, + "aring" => 556_f32, + "Ncommaaccent" => 722_f32, + "lacute" => 222_f32, + "agrave" => 556_f32, + "Tcommaaccent" => 611_f32, + "Cacute" => 722_f32, + "atilde" => 556_f32, + "Edotaccent" => 667_f32, + "scaron" => 500_f32, + "scedilla" => 500_f32, + "iacute" => 278_f32, + "lozenge" => 471_f32, + "Rcaron" => 722_f32, + "Gcommaaccent" => 778_f32, + "ucircumflex" => 556_f32, + "acircumflex" => 556_f32, + "Amacron" => 667_f32, + "rcaron" => 333_f32, + "ccedilla" => 500_f32, + "Zdotaccent" => 611_f32, + "Thorn" => 667_f32, + "Omacron" => 778_f32, + "Racute" => 722_f32, + "Sacute" => 667_f32, + "dcaron" => 643_f32, + "Umacron" => 722_f32, + "uring" => 556_f32, + "threesuperior" => 333_f32, + "Ograve" => 778_f32, + "Agrave" => 667_f32, + "Abreve" => 667_f32, + "multiply" => 584_f32, + "uacute" => 556_f32, + "Tcaron" => 611_f32, + "partialdiff" => 476_f32, + "ydieresis" => 500_f32, + "Nacute" => 722_f32, + "icircumflex" => 278_f32, + "Ecircumflex" => 667_f32, + "adieresis" => 556_f32, + "edieresis" => 556_f32, + "cacute" => 500_f32, + "nacute" => 556_f32, + "umacron" => 556_f32, + "Ncaron" => 722_f32, + "Iacute" => 278_f32, + "plusminus" => 584_f32, + "brokenbar" => 260_f32, + "registered" => 737_f32, + "Gbreve" => 778_f32, + "Idotaccent" => 278_f32, + "summation" => 600_f32, + "Egrave" => 667_f32, + "racute" => 333_f32, + "omacron" => 556_f32, + "Zacute" => 611_f32, + "Zcaron" => 611_f32, + "greaterequal" => 549_f32, + "Eth" => 722_f32, + "Ccedilla" => 722_f32, + "lcommaaccent" => 222_f32, + "tcaron" => 317_f32, + "eogonek" => 556_f32, + "Uogonek" => 722_f32, + "Aacute" => 667_f32, + "Adieresis" => 667_f32, + "egrave" => 556_f32, + "zacute" => 500_f32, + "iogonek" => 222_f32, + "Oacute" => 778_f32, + "oacute" => 556_f32, + "amacron" => 556_f32, + "sacute" => 500_f32, + "idieresis" => 278_f32, + "Ocircumflex" => 778_f32, + "Ugrave" => 722_f32, + "Delta" => 612_f32, + "thorn" => 556_f32, + "twosuperior" => 333_f32, + "Odieresis" => 778_f32, + "mu" => 556_f32, + "igrave" => 278_f32, + "ohungarumlaut" => 556_f32, + "Eogonek" => 667_f32, + "dcroat" => 556_f32, + "threequarters" => 834_f32, + "Scedilla" => 667_f32, + "lcaron" => 299_f32, + "Kcommaaccent" => 667_f32, + "Lacute" => 556_f32, + "trademark" => 1000_f32, + "edotaccent" => 556_f32, + "Igrave" => 278_f32, + "Imacron" => 278_f32, + "Lcaron" => 556_f32, + "onehalf" => 834_f32, + "lessequal" => 549_f32, + "ocircumflex" => 556_f32, + "ntilde" => 556_f32, + "Uhungarumlaut" => 722_f32, + "Eacute" => 667_f32, + "emacron" => 556_f32, + "gbreve" => 556_f32, + "onequarter" => 834_f32, + "Scaron" => 667_f32, + "Scommaaccent" => 667_f32, + "Ohungarumlaut" => 778_f32, + "degree" => 400_f32, + "ograve" => 556_f32, + "Ccaron" => 722_f32, + "ugrave" => 556_f32, + "radical" => 453_f32, + "Dcaron" => 722_f32, + "rcommaaccent" => 333_f32, + "Ntilde" => 722_f32, + "otilde" => 556_f32, + "Rcommaaccent" => 722_f32, + "Lcommaaccent" => 556_f32, + "Atilde" => 667_f32, + "Aogonek" => 667_f32, + "Aring" => 667_f32, + "Otilde" => 778_f32, + "zdotaccent" => 500_f32, + "Ecaron" => 667_f32, + "Iogonek" => 278_f32, + "kcommaaccent" => 500_f32, + "minus" => 584_f32, + "Icircumflex" => 278_f32, + "ncaron" => 556_f32, + "tcommaaccent" => 278_f32, + "logicalnot" => 584_f32, + "odieresis" => 556_f32, + "udieresis" => 556_f32, + "notequal" => 549_f32, + "gcommaaccent" => 556_f32, + "eth" => 556_f32, + "zcaron" => 500_f32, + "ncommaaccent" => 556_f32, + "onesuperior" => 333_f32, + "imacron" => 278_f32, + "Euro" => 556_f32, +}; + +pub(crate) static SYMBOL: phf::Map<&'static str, f32> = phf_map! { + "space" => 250_f32, + "exclam" => 333_f32, + "universal" => 713_f32, + "numbersign" => 500_f32, + "existential" => 549_f32, + "percent" => 833_f32, + "ampersand" => 778_f32, + "suchthat" => 439_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asteriskmath" => 500_f32, + "plus" => 549_f32, + "comma" => 250_f32, + "minus" => 549_f32, + "period" => 250_f32, + "slash" => 278_f32, + "zero" => 500_f32, + "one" => 500_f32, + "two" => 500_f32, + "three" => 500_f32, + "four" => 500_f32, + "five" => 500_f32, + "six" => 500_f32, + "seven" => 500_f32, + "eight" => 500_f32, + "nine" => 500_f32, + "colon" => 278_f32, + "semicolon" => 278_f32, + "less" => 549_f32, + "equal" => 549_f32, + "greater" => 549_f32, + "question" => 444_f32, + "congruent" => 549_f32, + "Alpha" => 722_f32, + "Beta" => 667_f32, + "Chi" => 722_f32, + "Delta" => 612_f32, + "Epsilon" => 611_f32, + "Phi" => 763_f32, + "Gamma" => 603_f32, + "Eta" => 722_f32, + "Iota" => 333_f32, + "theta1" => 631_f32, + "Kappa" => 722_f32, + "Lambda" => 686_f32, + "Mu" => 889_f32, + "Nu" => 722_f32, + "Omicron" => 722_f32, + "Pi" => 768_f32, + "Theta" => 741_f32, + "Rho" => 556_f32, + "Sigma" => 592_f32, + "Tau" => 611_f32, + "Upsilon" => 690_f32, + "sigma1" => 439_f32, + "Omega" => 768_f32, + "Xi" => 645_f32, + "Psi" => 795_f32, + "Zeta" => 611_f32, + "bracketleft" => 333_f32, + "therefore" => 863_f32, + "bracketright" => 333_f32, + "perpendicular" => 658_f32, + "underscore" => 500_f32, + "radicalex" => 500_f32, + "alpha" => 631_f32, + "beta" => 549_f32, + "chi" => 549_f32, + "delta" => 494_f32, + "epsilon" => 439_f32, + "phi" => 521_f32, + "gamma" => 411_f32, + "eta" => 603_f32, + "iota" => 329_f32, + "phi1" => 603_f32, + "kappa" => 549_f32, + "lambda" => 549_f32, + "mu" => 576_f32, + "nu" => 521_f32, + "omicron" => 549_f32, + "pi" => 549_f32, + "theta" => 521_f32, + "rho" => 549_f32, + "sigma" => 603_f32, + "tau" => 439_f32, + "upsilon" => 576_f32, + "omega1" => 713_f32, + "omega" => 686_f32, + "xi" => 493_f32, + "psi" => 686_f32, + "zeta" => 494_f32, + "braceleft" => 480_f32, + "bar" => 200_f32, + "braceright" => 480_f32, + "similar" => 549_f32, + "Euro" => 750_f32, + "Upsilon1" => 620_f32, + "minute" => 247_f32, + "lessequal" => 549_f32, + "fraction" => 167_f32, + "infinity" => 713_f32, + "florin" => 500_f32, + "club" => 753_f32, + "diamond" => 753_f32, + "heart" => 753_f32, + "spade" => 753_f32, + "arrowboth" => 1042_f32, + "arrowleft" => 987_f32, + "arrowup" => 603_f32, + "arrowright" => 987_f32, + "arrowdown" => 603_f32, + "degree" => 400_f32, + "plusminus" => 549_f32, + "second" => 411_f32, + "greaterequal" => 549_f32, + "multiply" => 549_f32, + "proportional" => 713_f32, + "partialdiff" => 494_f32, + "bullet" => 460_f32, + "divide" => 549_f32, + "notequal" => 549_f32, + "equivalence" => 549_f32, + "approxequal" => 549_f32, + "ellipsis" => 1000_f32, + "arrowvertex" => 603_f32, + "arrowhorizex" => 1000_f32, + "carriagereturn" => 658_f32, + "aleph" => 823_f32, + "Ifraktur" => 686_f32, + "Rfraktur" => 795_f32, + "weierstrass" => 987_f32, + "circlemultiply" => 768_f32, + "circleplus" => 768_f32, + "emptyset" => 823_f32, + "intersection" => 768_f32, + "union" => 768_f32, + "propersuperset" => 713_f32, + "reflexsuperset" => 713_f32, + "notsubset" => 713_f32, + "propersubset" => 713_f32, + "reflexsubset" => 713_f32, + "element" => 713_f32, + "notelement" => 713_f32, + "angle" => 768_f32, + "gradient" => 713_f32, + "registerserif" => 790_f32, + "copyrightserif" => 790_f32, + "trademarkserif" => 890_f32, + "product" => 823_f32, + "radical" => 549_f32, + "dotmath" => 250_f32, + "logicalnot" => 713_f32, + "logicaland" => 603_f32, + "logicalor" => 603_f32, + "arrowdblboth" => 1042_f32, + "arrowdblleft" => 987_f32, + "arrowdblup" => 603_f32, + "arrowdblright" => 987_f32, + "arrowdbldown" => 603_f32, + "lozenge" => 494_f32, + "angleleft" => 329_f32, + "registersans" => 790_f32, + "copyrightsans" => 790_f32, + "trademarksans" => 786_f32, + "summation" => 713_f32, + "parenlefttp" => 384_f32, + "parenleftex" => 384_f32, + "parenleftbt" => 384_f32, + "bracketlefttp" => 384_f32, + "bracketleftex" => 384_f32, + "bracketleftbt" => 384_f32, + "bracelefttp" => 494_f32, + "braceleftmid" => 494_f32, + "braceleftbt" => 494_f32, + "braceex" => 494_f32, + "angleright" => 329_f32, + "integral" => 274_f32, + "integraltp" => 686_f32, + "integralex" => 686_f32, + "integralbt" => 686_f32, + "parenrighttp" => 384_f32, + "parenrightex" => 384_f32, + "parenrightbt" => 384_f32, + "bracketrighttp" => 384_f32, + "bracketrightex" => 384_f32, + "bracketrightbt" => 384_f32, + "bracerighttp" => 494_f32, + "bracerightmid" => 494_f32, + "bracerightbt" => 494_f32, + "apple" => 790_f32, +}; + +pub(crate) static TIMES_BOLD: phf::Map<&'static str, f32> = phf_map! { + "space" => 250_f32, + "exclam" => 333_f32, + "quotedbl" => 555_f32, + "numbersign" => 500_f32, + "dollar" => 500_f32, + "percent" => 1000_f32, + "ampersand" => 833_f32, + "quoteright" => 333_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asterisk" => 500_f32, + "plus" => 570_f32, + "comma" => 250_f32, + "hyphen" => 333_f32, + "period" => 250_f32, + "slash" => 278_f32, + "zero" => 500_f32, + "one" => 500_f32, + "two" => 500_f32, + "three" => 500_f32, + "four" => 500_f32, + "five" => 500_f32, + "six" => 500_f32, + "seven" => 500_f32, + "eight" => 500_f32, + "nine" => 500_f32, + "colon" => 333_f32, + "semicolon" => 333_f32, + "less" => 570_f32, + "equal" => 570_f32, + "greater" => 570_f32, + "question" => 500_f32, + "at" => 930_f32, + "A" => 722_f32, + "B" => 667_f32, + "C" => 722_f32, + "D" => 722_f32, + "E" => 667_f32, + "F" => 611_f32, + "G" => 778_f32, + "H" => 778_f32, + "I" => 389_f32, + "J" => 500_f32, + "K" => 778_f32, + "L" => 667_f32, + "M" => 944_f32, + "N" => 722_f32, + "O" => 778_f32, + "P" => 611_f32, + "Q" => 778_f32, + "R" => 722_f32, + "S" => 556_f32, + "T" => 667_f32, + "U" => 722_f32, + "V" => 722_f32, + "W" => 1000_f32, + "X" => 722_f32, + "Y" => 722_f32, + "Z" => 667_f32, + "bracketleft" => 333_f32, + "backslash" => 278_f32, + "bracketright" => 333_f32, + "asciicircum" => 581_f32, + "underscore" => 500_f32, + "quoteleft" => 333_f32, + "a" => 500_f32, + "b" => 556_f32, + "c" => 444_f32, + "d" => 556_f32, + "e" => 444_f32, + "f" => 333_f32, + "g" => 500_f32, + "h" => 556_f32, + "i" => 278_f32, + "j" => 333_f32, + "k" => 556_f32, + "l" => 278_f32, + "m" => 833_f32, + "n" => 556_f32, + "o" => 500_f32, + "p" => 556_f32, + "q" => 556_f32, + "r" => 444_f32, + "s" => 389_f32, + "t" => 333_f32, + "u" => 556_f32, + "v" => 500_f32, + "w" => 722_f32, + "x" => 500_f32, + "y" => 500_f32, + "z" => 444_f32, + "braceleft" => 394_f32, + "bar" => 220_f32, + "braceright" => 394_f32, + "asciitilde" => 520_f32, + "exclamdown" => 333_f32, + "cent" => 500_f32, + "sterling" => 500_f32, + "fraction" => 167_f32, + "yen" => 500_f32, + "florin" => 500_f32, + "section" => 500_f32, + "currency" => 500_f32, + "quotesingle" => 278_f32, + "quotedblleft" => 500_f32, + "guillemotleft" => 500_f32, + "guilsinglleft" => 333_f32, + "guilsinglright" => 333_f32, + "fi" => 556_f32, + "fl" => 556_f32, + "endash" => 500_f32, + "dagger" => 500_f32, + "daggerdbl" => 500_f32, + "periodcentered" => 250_f32, + "paragraph" => 540_f32, + "bullet" => 350_f32, + "quotesinglbase" => 333_f32, + "quotedblbase" => 500_f32, + "quotedblright" => 500_f32, + "guillemotright" => 500_f32, + "ellipsis" => 1000_f32, + "perthousand" => 1000_f32, + "questiondown" => 500_f32, + "grave" => 333_f32, + "acute" => 333_f32, + "circumflex" => 333_f32, + "tilde" => 333_f32, + "macron" => 333_f32, + "breve" => 333_f32, + "dotaccent" => 333_f32, + "dieresis" => 333_f32, + "ring" => 333_f32, + "cedilla" => 333_f32, + "hungarumlaut" => 333_f32, + "ogonek" => 333_f32, + "caron" => 333_f32, + "emdash" => 1000_f32, + "AE" => 1000_f32, + "ordfeminine" => 300_f32, + "Lslash" => 667_f32, + "Oslash" => 778_f32, + "OE" => 1000_f32, + "ordmasculine" => 330_f32, + "ae" => 722_f32, + "dotlessi" => 278_f32, + "lslash" => 278_f32, + "oslash" => 500_f32, + "oe" => 722_f32, + "germandbls" => 556_f32, + "Idieresis" => 389_f32, + "eacute" => 444_f32, + "abreve" => 500_f32, + "uhungarumlaut" => 556_f32, + "ecaron" => 444_f32, + "Ydieresis" => 722_f32, + "divide" => 570_f32, + "Yacute" => 722_f32, + "Acircumflex" => 722_f32, + "aacute" => 500_f32, + "Ucircumflex" => 722_f32, + "yacute" => 500_f32, + "scommaaccent" => 389_f32, + "ecircumflex" => 444_f32, + "Uring" => 722_f32, + "Udieresis" => 722_f32, + "aogonek" => 500_f32, + "Uacute" => 722_f32, + "uogonek" => 556_f32, + "Edieresis" => 667_f32, + "Dcroat" => 722_f32, + "commaaccent" => 250_f32, + "copyright" => 747_f32, + "Emacron" => 667_f32, + "ccaron" => 444_f32, + "aring" => 500_f32, + "Ncommaaccent" => 722_f32, + "lacute" => 278_f32, + "agrave" => 500_f32, + "Tcommaaccent" => 667_f32, + "Cacute" => 722_f32, + "atilde" => 500_f32, + "Edotaccent" => 667_f32, + "scaron" => 389_f32, + "scedilla" => 389_f32, + "iacute" => 278_f32, + "lozenge" => 494_f32, + "Rcaron" => 722_f32, + "Gcommaaccent" => 778_f32, + "ucircumflex" => 556_f32, + "acircumflex" => 500_f32, + "Amacron" => 722_f32, + "rcaron" => 444_f32, + "ccedilla" => 444_f32, + "Zdotaccent" => 667_f32, + "Thorn" => 611_f32, + "Omacron" => 778_f32, + "Racute" => 722_f32, + "Sacute" => 556_f32, + "dcaron" => 672_f32, + "Umacron" => 722_f32, + "uring" => 556_f32, + "threesuperior" => 300_f32, + "Ograve" => 778_f32, + "Agrave" => 722_f32, + "Abreve" => 722_f32, + "multiply" => 570_f32, + "uacute" => 556_f32, + "Tcaron" => 667_f32, + "partialdiff" => 494_f32, + "ydieresis" => 500_f32, + "Nacute" => 722_f32, + "icircumflex" => 278_f32, + "Ecircumflex" => 667_f32, + "adieresis" => 500_f32, + "edieresis" => 444_f32, + "cacute" => 444_f32, + "nacute" => 556_f32, + "umacron" => 556_f32, + "Ncaron" => 722_f32, + "Iacute" => 389_f32, + "plusminus" => 570_f32, + "brokenbar" => 220_f32, + "registered" => 747_f32, + "Gbreve" => 778_f32, + "Idotaccent" => 389_f32, + "summation" => 600_f32, + "Egrave" => 667_f32, + "racute" => 444_f32, + "omacron" => 500_f32, + "Zacute" => 667_f32, + "Zcaron" => 667_f32, + "greaterequal" => 549_f32, + "Eth" => 722_f32, + "Ccedilla" => 722_f32, + "lcommaaccent" => 278_f32, + "tcaron" => 416_f32, + "eogonek" => 444_f32, + "Uogonek" => 722_f32, + "Aacute" => 722_f32, + "Adieresis" => 722_f32, + "egrave" => 444_f32, + "zacute" => 444_f32, + "iogonek" => 278_f32, + "Oacute" => 778_f32, + "oacute" => 500_f32, + "amacron" => 500_f32, + "sacute" => 389_f32, + "idieresis" => 278_f32, + "Ocircumflex" => 778_f32, + "Ugrave" => 722_f32, + "Delta" => 612_f32, + "thorn" => 556_f32, + "twosuperior" => 300_f32, + "Odieresis" => 778_f32, + "mu" => 556_f32, + "igrave" => 278_f32, + "ohungarumlaut" => 500_f32, + "Eogonek" => 667_f32, + "dcroat" => 556_f32, + "threequarters" => 750_f32, + "Scedilla" => 556_f32, + "lcaron" => 394_f32, + "Kcommaaccent" => 778_f32, + "Lacute" => 667_f32, + "trademark" => 1000_f32, + "edotaccent" => 444_f32, + "Igrave" => 389_f32, + "Imacron" => 389_f32, + "Lcaron" => 667_f32, + "onehalf" => 750_f32, + "lessequal" => 549_f32, + "ocircumflex" => 500_f32, + "ntilde" => 556_f32, + "Uhungarumlaut" => 722_f32, + "Eacute" => 667_f32, + "emacron" => 444_f32, + "gbreve" => 500_f32, + "onequarter" => 750_f32, + "Scaron" => 556_f32, + "Scommaaccent" => 556_f32, + "Ohungarumlaut" => 778_f32, + "degree" => 400_f32, + "ograve" => 500_f32, + "Ccaron" => 722_f32, + "ugrave" => 556_f32, + "radical" => 549_f32, + "Dcaron" => 722_f32, + "rcommaaccent" => 444_f32, + "Ntilde" => 722_f32, + "otilde" => 500_f32, + "Rcommaaccent" => 722_f32, + "Lcommaaccent" => 667_f32, + "Atilde" => 722_f32, + "Aogonek" => 722_f32, + "Aring" => 722_f32, + "Otilde" => 778_f32, + "zdotaccent" => 444_f32, + "Ecaron" => 667_f32, + "Iogonek" => 389_f32, + "kcommaaccent" => 556_f32, + "minus" => 570_f32, + "Icircumflex" => 389_f32, + "ncaron" => 556_f32, + "tcommaaccent" => 333_f32, + "logicalnot" => 570_f32, + "odieresis" => 500_f32, + "udieresis" => 556_f32, + "notequal" => 549_f32, + "gcommaaccent" => 500_f32, + "eth" => 500_f32, + "zcaron" => 444_f32, + "ncommaaccent" => 556_f32, + "onesuperior" => 300_f32, + "imacron" => 278_f32, + "Euro" => 500_f32, +}; + +pub(crate) static TIMES_BOLD_ITALIC: phf::Map<&'static str, f32> = phf_map! { + "space" => 250_f32, + "exclam" => 389_f32, + "quotedbl" => 555_f32, + "numbersign" => 500_f32, + "dollar" => 500_f32, + "percent" => 833_f32, + "ampersand" => 778_f32, + "quoteright" => 333_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asterisk" => 500_f32, + "plus" => 570_f32, + "comma" => 250_f32, + "hyphen" => 333_f32, + "period" => 250_f32, + "slash" => 278_f32, + "zero" => 500_f32, + "one" => 500_f32, + "two" => 500_f32, + "three" => 500_f32, + "four" => 500_f32, + "five" => 500_f32, + "six" => 500_f32, + "seven" => 500_f32, + "eight" => 500_f32, + "nine" => 500_f32, + "colon" => 333_f32, + "semicolon" => 333_f32, + "less" => 570_f32, + "equal" => 570_f32, + "greater" => 570_f32, + "question" => 500_f32, + "at" => 832_f32, + "A" => 667_f32, + "B" => 667_f32, + "C" => 667_f32, + "D" => 722_f32, + "E" => 667_f32, + "F" => 667_f32, + "G" => 722_f32, + "H" => 778_f32, + "I" => 389_f32, + "J" => 500_f32, + "K" => 667_f32, + "L" => 611_f32, + "M" => 889_f32, + "N" => 722_f32, + "O" => 722_f32, + "P" => 611_f32, + "Q" => 722_f32, + "R" => 667_f32, + "S" => 556_f32, + "T" => 611_f32, + "U" => 722_f32, + "V" => 667_f32, + "W" => 889_f32, + "X" => 667_f32, + "Y" => 611_f32, + "Z" => 611_f32, + "bracketleft" => 333_f32, + "backslash" => 278_f32, + "bracketright" => 333_f32, + "asciicircum" => 570_f32, + "underscore" => 500_f32, + "quoteleft" => 333_f32, + "a" => 500_f32, + "b" => 500_f32, + "c" => 444_f32, + "d" => 500_f32, + "e" => 444_f32, + "f" => 333_f32, + "g" => 500_f32, + "h" => 556_f32, + "i" => 278_f32, + "j" => 278_f32, + "k" => 500_f32, + "l" => 278_f32, + "m" => 778_f32, + "n" => 556_f32, + "o" => 500_f32, + "p" => 500_f32, + "q" => 500_f32, + "r" => 389_f32, + "s" => 389_f32, + "t" => 278_f32, + "u" => 556_f32, + "v" => 444_f32, + "w" => 667_f32, + "x" => 500_f32, + "y" => 444_f32, + "z" => 389_f32, + "braceleft" => 348_f32, + "bar" => 220_f32, + "braceright" => 348_f32, + "asciitilde" => 570_f32, + "exclamdown" => 389_f32, + "cent" => 500_f32, + "sterling" => 500_f32, + "fraction" => 167_f32, + "yen" => 500_f32, + "florin" => 500_f32, + "section" => 500_f32, + "currency" => 500_f32, + "quotesingle" => 278_f32, + "quotedblleft" => 500_f32, + "guillemotleft" => 500_f32, + "guilsinglleft" => 333_f32, + "guilsinglright" => 333_f32, + "fi" => 556_f32, + "fl" => 556_f32, + "endash" => 500_f32, + "dagger" => 500_f32, + "daggerdbl" => 500_f32, + "periodcentered" => 250_f32, + "paragraph" => 500_f32, + "bullet" => 350_f32, + "quotesinglbase" => 333_f32, + "quotedblbase" => 500_f32, + "quotedblright" => 500_f32, + "guillemotright" => 500_f32, + "ellipsis" => 1000_f32, + "perthousand" => 1000_f32, + "questiondown" => 500_f32, + "grave" => 333_f32, + "acute" => 333_f32, + "circumflex" => 333_f32, + "tilde" => 333_f32, + "macron" => 333_f32, + "breve" => 333_f32, + "dotaccent" => 333_f32, + "dieresis" => 333_f32, + "ring" => 333_f32, + "cedilla" => 333_f32, + "hungarumlaut" => 333_f32, + "ogonek" => 333_f32, + "caron" => 333_f32, + "emdash" => 1000_f32, + "AE" => 944_f32, + "ordfeminine" => 266_f32, + "Lslash" => 611_f32, + "Oslash" => 722_f32, + "OE" => 944_f32, + "ordmasculine" => 300_f32, + "ae" => 722_f32, + "dotlessi" => 278_f32, + "lslash" => 278_f32, + "oslash" => 500_f32, + "oe" => 722_f32, + "germandbls" => 500_f32, + "Idieresis" => 389_f32, + "eacute" => 444_f32, + "abreve" => 500_f32, + "uhungarumlaut" => 556_f32, + "ecaron" => 444_f32, + "Ydieresis" => 611_f32, + "divide" => 570_f32, + "Yacute" => 611_f32, + "Acircumflex" => 667_f32, + "aacute" => 500_f32, + "Ucircumflex" => 722_f32, + "yacute" => 444_f32, + "scommaaccent" => 389_f32, + "ecircumflex" => 444_f32, + "Uring" => 722_f32, + "Udieresis" => 722_f32, + "aogonek" => 500_f32, + "Uacute" => 722_f32, + "uogonek" => 556_f32, + "Edieresis" => 667_f32, + "Dcroat" => 722_f32, + "commaaccent" => 250_f32, + "copyright" => 747_f32, + "Emacron" => 667_f32, + "ccaron" => 444_f32, + "aring" => 500_f32, + "Ncommaaccent" => 722_f32, + "lacute" => 278_f32, + "agrave" => 500_f32, + "Tcommaaccent" => 611_f32, + "Cacute" => 667_f32, + "atilde" => 500_f32, + "Edotaccent" => 667_f32, + "scaron" => 389_f32, + "scedilla" => 389_f32, + "iacute" => 278_f32, + "lozenge" => 494_f32, + "Rcaron" => 667_f32, + "Gcommaaccent" => 722_f32, + "ucircumflex" => 556_f32, + "acircumflex" => 500_f32, + "Amacron" => 667_f32, + "rcaron" => 389_f32, + "ccedilla" => 444_f32, + "Zdotaccent" => 611_f32, + "Thorn" => 611_f32, + "Omacron" => 722_f32, + "Racute" => 667_f32, + "Sacute" => 556_f32, + "dcaron" => 608_f32, + "Umacron" => 722_f32, + "uring" => 556_f32, + "threesuperior" => 300_f32, + "Ograve" => 722_f32, + "Agrave" => 667_f32, + "Abreve" => 667_f32, + "multiply" => 570_f32, + "uacute" => 556_f32, + "Tcaron" => 611_f32, + "partialdiff" => 494_f32, + "ydieresis" => 444_f32, + "Nacute" => 722_f32, + "icircumflex" => 278_f32, + "Ecircumflex" => 667_f32, + "adieresis" => 500_f32, + "edieresis" => 444_f32, + "cacute" => 444_f32, + "nacute" => 556_f32, + "umacron" => 556_f32, + "Ncaron" => 722_f32, + "Iacute" => 389_f32, + "plusminus" => 570_f32, + "brokenbar" => 220_f32, + "registered" => 747_f32, + "Gbreve" => 722_f32, + "Idotaccent" => 389_f32, + "summation" => 600_f32, + "Egrave" => 667_f32, + "racute" => 389_f32, + "omacron" => 500_f32, + "Zacute" => 611_f32, + "Zcaron" => 611_f32, + "greaterequal" => 549_f32, + "Eth" => 722_f32, + "Ccedilla" => 667_f32, + "lcommaaccent" => 278_f32, + "tcaron" => 366_f32, + "eogonek" => 444_f32, + "Uogonek" => 722_f32, + "Aacute" => 667_f32, + "Adieresis" => 667_f32, + "egrave" => 444_f32, + "zacute" => 389_f32, + "iogonek" => 278_f32, + "Oacute" => 722_f32, + "oacute" => 500_f32, + "amacron" => 500_f32, + "sacute" => 389_f32, + "idieresis" => 278_f32, + "Ocircumflex" => 722_f32, + "Ugrave" => 722_f32, + "Delta" => 612_f32, + "thorn" => 500_f32, + "twosuperior" => 300_f32, + "Odieresis" => 722_f32, + "mu" => 576_f32, + "igrave" => 278_f32, + "ohungarumlaut" => 500_f32, + "Eogonek" => 667_f32, + "dcroat" => 500_f32, + "threequarters" => 750_f32, + "Scedilla" => 556_f32, + "lcaron" => 382_f32, + "Kcommaaccent" => 667_f32, + "Lacute" => 611_f32, + "trademark" => 1000_f32, + "edotaccent" => 444_f32, + "Igrave" => 389_f32, + "Imacron" => 389_f32, + "Lcaron" => 611_f32, + "onehalf" => 750_f32, + "lessequal" => 549_f32, + "ocircumflex" => 500_f32, + "ntilde" => 556_f32, + "Uhungarumlaut" => 722_f32, + "Eacute" => 667_f32, + "emacron" => 444_f32, + "gbreve" => 500_f32, + "onequarter" => 750_f32, + "Scaron" => 556_f32, + "Scommaaccent" => 556_f32, + "Ohungarumlaut" => 722_f32, + "degree" => 400_f32, + "ograve" => 500_f32, + "Ccaron" => 667_f32, + "ugrave" => 556_f32, + "radical" => 549_f32, + "Dcaron" => 722_f32, + "rcommaaccent" => 389_f32, + "Ntilde" => 722_f32, + "otilde" => 500_f32, + "Rcommaaccent" => 667_f32, + "Lcommaaccent" => 611_f32, + "Atilde" => 667_f32, + "Aogonek" => 667_f32, + "Aring" => 667_f32, + "Otilde" => 722_f32, + "zdotaccent" => 389_f32, + "Ecaron" => 667_f32, + "Iogonek" => 389_f32, + "kcommaaccent" => 500_f32, + "minus" => 606_f32, + "Icircumflex" => 389_f32, + "ncaron" => 556_f32, + "tcommaaccent" => 278_f32, + "logicalnot" => 606_f32, + "odieresis" => 500_f32, + "udieresis" => 556_f32, + "notequal" => 549_f32, + "gcommaaccent" => 500_f32, + "eth" => 500_f32, + "zcaron" => 389_f32, + "ncommaaccent" => 556_f32, + "onesuperior" => 300_f32, + "imacron" => 278_f32, + "Euro" => 500_f32, +}; + +pub(crate) static TIMES_ITALIC: phf::Map<&'static str, f32> = phf_map! { + "space" => 250_f32, + "exclam" => 333_f32, + "quotedbl" => 420_f32, + "numbersign" => 500_f32, + "dollar" => 500_f32, + "percent" => 833_f32, + "ampersand" => 778_f32, + "quoteright" => 333_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asterisk" => 500_f32, + "plus" => 675_f32, + "comma" => 250_f32, + "hyphen" => 333_f32, + "period" => 250_f32, + "slash" => 278_f32, + "zero" => 500_f32, + "one" => 500_f32, + "two" => 500_f32, + "three" => 500_f32, + "four" => 500_f32, + "five" => 500_f32, + "six" => 500_f32, + "seven" => 500_f32, + "eight" => 500_f32, + "nine" => 500_f32, + "colon" => 333_f32, + "semicolon" => 333_f32, + "less" => 675_f32, + "equal" => 675_f32, + "greater" => 675_f32, + "question" => 500_f32, + "at" => 920_f32, + "A" => 611_f32, + "B" => 611_f32, + "C" => 667_f32, + "D" => 722_f32, + "E" => 611_f32, + "F" => 611_f32, + "G" => 722_f32, + "H" => 722_f32, + "I" => 333_f32, + "J" => 444_f32, + "K" => 667_f32, + "L" => 556_f32, + "M" => 833_f32, + "N" => 667_f32, + "O" => 722_f32, + "P" => 611_f32, + "Q" => 722_f32, + "R" => 611_f32, + "S" => 500_f32, + "T" => 556_f32, + "U" => 722_f32, + "V" => 611_f32, + "W" => 833_f32, + "X" => 611_f32, + "Y" => 556_f32, + "Z" => 556_f32, + "bracketleft" => 389_f32, + "backslash" => 278_f32, + "bracketright" => 389_f32, + "asciicircum" => 422_f32, + "underscore" => 500_f32, + "quoteleft" => 333_f32, + "a" => 500_f32, + "b" => 500_f32, + "c" => 444_f32, + "d" => 500_f32, + "e" => 444_f32, + "f" => 278_f32, + "g" => 500_f32, + "h" => 500_f32, + "i" => 278_f32, + "j" => 278_f32, + "k" => 444_f32, + "l" => 278_f32, + "m" => 722_f32, + "n" => 500_f32, + "o" => 500_f32, + "p" => 500_f32, + "q" => 500_f32, + "r" => 389_f32, + "s" => 389_f32, + "t" => 278_f32, + "u" => 500_f32, + "v" => 444_f32, + "w" => 667_f32, + "x" => 444_f32, + "y" => 444_f32, + "z" => 389_f32, + "braceleft" => 400_f32, + "bar" => 275_f32, + "braceright" => 400_f32, + "asciitilde" => 541_f32, + "exclamdown" => 389_f32, + "cent" => 500_f32, + "sterling" => 500_f32, + "fraction" => 167_f32, + "yen" => 500_f32, + "florin" => 500_f32, + "section" => 500_f32, + "currency" => 500_f32, + "quotesingle" => 214_f32, + "quotedblleft" => 556_f32, + "guillemotleft" => 500_f32, + "guilsinglleft" => 333_f32, + "guilsinglright" => 333_f32, + "fi" => 500_f32, + "fl" => 500_f32, + "endash" => 500_f32, + "dagger" => 500_f32, + "daggerdbl" => 500_f32, + "periodcentered" => 250_f32, + "paragraph" => 523_f32, + "bullet" => 350_f32, + "quotesinglbase" => 333_f32, + "quotedblbase" => 556_f32, + "quotedblright" => 556_f32, + "guillemotright" => 500_f32, + "ellipsis" => 889_f32, + "perthousand" => 1000_f32, + "questiondown" => 500_f32, + "grave" => 333_f32, + "acute" => 333_f32, + "circumflex" => 333_f32, + "tilde" => 333_f32, + "macron" => 333_f32, + "breve" => 333_f32, + "dotaccent" => 333_f32, + "dieresis" => 333_f32, + "ring" => 333_f32, + "cedilla" => 333_f32, + "hungarumlaut" => 333_f32, + "ogonek" => 333_f32, + "caron" => 333_f32, + "emdash" => 889_f32, + "AE" => 889_f32, + "ordfeminine" => 276_f32, + "Lslash" => 556_f32, + "Oslash" => 722_f32, + "OE" => 944_f32, + "ordmasculine" => 310_f32, + "ae" => 667_f32, + "dotlessi" => 278_f32, + "lslash" => 278_f32, + "oslash" => 500_f32, + "oe" => 667_f32, + "germandbls" => 500_f32, + "Idieresis" => 333_f32, + "eacute" => 444_f32, + "abreve" => 500_f32, + "uhungarumlaut" => 500_f32, + "ecaron" => 444_f32, + "Ydieresis" => 556_f32, + "divide" => 675_f32, + "Yacute" => 556_f32, + "Acircumflex" => 611_f32, + "aacute" => 500_f32, + "Ucircumflex" => 722_f32, + "yacute" => 444_f32, + "scommaaccent" => 389_f32, + "ecircumflex" => 444_f32, + "Uring" => 722_f32, + "Udieresis" => 722_f32, + "aogonek" => 500_f32, + "Uacute" => 722_f32, + "uogonek" => 500_f32, + "Edieresis" => 611_f32, + "Dcroat" => 722_f32, + "commaaccent" => 250_f32, + "copyright" => 760_f32, + "Emacron" => 611_f32, + "ccaron" => 444_f32, + "aring" => 500_f32, + "Ncommaaccent" => 667_f32, + "lacute" => 278_f32, + "agrave" => 500_f32, + "Tcommaaccent" => 556_f32, + "Cacute" => 667_f32, + "atilde" => 500_f32, + "Edotaccent" => 611_f32, + "scaron" => 389_f32, + "scedilla" => 389_f32, + "iacute" => 278_f32, + "lozenge" => 471_f32, + "Rcaron" => 611_f32, + "Gcommaaccent" => 722_f32, + "ucircumflex" => 500_f32, + "acircumflex" => 500_f32, + "Amacron" => 611_f32, + "rcaron" => 389_f32, + "ccedilla" => 444_f32, + "Zdotaccent" => 556_f32, + "Thorn" => 611_f32, + "Omacron" => 722_f32, + "Racute" => 611_f32, + "Sacute" => 500_f32, + "dcaron" => 544_f32, + "Umacron" => 722_f32, + "uring" => 500_f32, + "threesuperior" => 300_f32, + "Ograve" => 722_f32, + "Agrave" => 611_f32, + "Abreve" => 611_f32, + "multiply" => 675_f32, + "uacute" => 500_f32, + "Tcaron" => 556_f32, + "partialdiff" => 476_f32, + "ydieresis" => 444_f32, + "Nacute" => 667_f32, + "icircumflex" => 278_f32, + "Ecircumflex" => 611_f32, + "adieresis" => 500_f32, + "edieresis" => 444_f32, + "cacute" => 444_f32, + "nacute" => 500_f32, + "umacron" => 500_f32, + "Ncaron" => 667_f32, + "Iacute" => 333_f32, + "plusminus" => 675_f32, + "brokenbar" => 275_f32, + "registered" => 760_f32, + "Gbreve" => 722_f32, + "Idotaccent" => 333_f32, + "summation" => 600_f32, + "Egrave" => 611_f32, + "racute" => 389_f32, + "omacron" => 500_f32, + "Zacute" => 556_f32, + "Zcaron" => 556_f32, + "greaterequal" => 549_f32, + "Eth" => 722_f32, + "Ccedilla" => 667_f32, + "lcommaaccent" => 278_f32, + "tcaron" => 300_f32, + "eogonek" => 444_f32, + "Uogonek" => 722_f32, + "Aacute" => 611_f32, + "Adieresis" => 611_f32, + "egrave" => 444_f32, + "zacute" => 389_f32, + "iogonek" => 278_f32, + "Oacute" => 722_f32, + "oacute" => 500_f32, + "amacron" => 500_f32, + "sacute" => 389_f32, + "idieresis" => 278_f32, + "Ocircumflex" => 722_f32, + "Ugrave" => 722_f32, + "Delta" => 612_f32, + "thorn" => 500_f32, + "twosuperior" => 300_f32, + "Odieresis" => 722_f32, + "mu" => 500_f32, + "igrave" => 278_f32, + "ohungarumlaut" => 500_f32, + "Eogonek" => 611_f32, + "dcroat" => 500_f32, + "threequarters" => 750_f32, + "Scedilla" => 500_f32, + "lcaron" => 300_f32, + "Kcommaaccent" => 667_f32, + "Lacute" => 556_f32, + "trademark" => 980_f32, + "edotaccent" => 444_f32, + "Igrave" => 333_f32, + "Imacron" => 333_f32, + "Lcaron" => 611_f32, + "onehalf" => 750_f32, + "lessequal" => 549_f32, + "ocircumflex" => 500_f32, + "ntilde" => 500_f32, + "Uhungarumlaut" => 722_f32, + "Eacute" => 611_f32, + "emacron" => 444_f32, + "gbreve" => 500_f32, + "onequarter" => 750_f32, + "Scaron" => 500_f32, + "Scommaaccent" => 500_f32, + "Ohungarumlaut" => 722_f32, + "degree" => 400_f32, + "ograve" => 500_f32, + "Ccaron" => 667_f32, + "ugrave" => 500_f32, + "radical" => 453_f32, + "Dcaron" => 722_f32, + "rcommaaccent" => 389_f32, + "Ntilde" => 667_f32, + "otilde" => 500_f32, + "Rcommaaccent" => 611_f32, + "Lcommaaccent" => 556_f32, + "Atilde" => 611_f32, + "Aogonek" => 611_f32, + "Aring" => 611_f32, + "Otilde" => 722_f32, + "zdotaccent" => 389_f32, + "Ecaron" => 611_f32, + "Iogonek" => 333_f32, + "kcommaaccent" => 444_f32, + "minus" => 675_f32, + "Icircumflex" => 333_f32, + "ncaron" => 500_f32, + "tcommaaccent" => 278_f32, + "logicalnot" => 675_f32, + "odieresis" => 500_f32, + "udieresis" => 500_f32, + "notequal" => 549_f32, + "gcommaaccent" => 500_f32, + "eth" => 500_f32, + "zcaron" => 389_f32, + "ncommaaccent" => 500_f32, + "onesuperior" => 300_f32, + "imacron" => 278_f32, + "Euro" => 500_f32, +}; + +pub(crate) static TIMES_ROMAN: phf::Map<&'static str, f32> = phf_map! { + "space" => 250_f32, + "exclam" => 333_f32, + "quotedbl" => 408_f32, + "numbersign" => 500_f32, + "dollar" => 500_f32, + "percent" => 833_f32, + "ampersand" => 778_f32, + "quoteright" => 333_f32, + "parenleft" => 333_f32, + "parenright" => 333_f32, + "asterisk" => 500_f32, + "plus" => 564_f32, + "comma" => 250_f32, + "hyphen" => 333_f32, + "period" => 250_f32, + "slash" => 278_f32, + "zero" => 500_f32, + "one" => 500_f32, + "two" => 500_f32, + "three" => 500_f32, + "four" => 500_f32, + "five" => 500_f32, + "six" => 500_f32, + "seven" => 500_f32, + "eight" => 500_f32, + "nine" => 500_f32, + "colon" => 278_f32, + "semicolon" => 278_f32, + "less" => 564_f32, + "equal" => 564_f32, + "greater" => 564_f32, + "question" => 444_f32, + "at" => 921_f32, + "A" => 722_f32, + "B" => 667_f32, + "C" => 667_f32, + "D" => 722_f32, + "E" => 611_f32, + "F" => 556_f32, + "G" => 722_f32, + "H" => 722_f32, + "I" => 333_f32, + "J" => 389_f32, + "K" => 722_f32, + "L" => 611_f32, + "M" => 889_f32, + "N" => 722_f32, + "O" => 722_f32, + "P" => 556_f32, + "Q" => 722_f32, + "R" => 667_f32, + "S" => 556_f32, + "T" => 611_f32, + "U" => 722_f32, + "V" => 722_f32, + "W" => 944_f32, + "X" => 722_f32, + "Y" => 722_f32, + "Z" => 611_f32, + "bracketleft" => 333_f32, + "backslash" => 278_f32, + "bracketright" => 333_f32, + "asciicircum" => 469_f32, + "underscore" => 500_f32, + "quoteleft" => 333_f32, + "a" => 444_f32, + "b" => 500_f32, + "c" => 444_f32, + "d" => 500_f32, + "e" => 444_f32, + "f" => 333_f32, + "g" => 500_f32, + "h" => 500_f32, + "i" => 278_f32, + "j" => 278_f32, + "k" => 500_f32, + "l" => 278_f32, + "m" => 778_f32, + "n" => 500_f32, + "o" => 500_f32, + "p" => 500_f32, + "q" => 500_f32, + "r" => 333_f32, + "s" => 389_f32, + "t" => 278_f32, + "u" => 500_f32, + "v" => 500_f32, + "w" => 722_f32, + "x" => 500_f32, + "y" => 500_f32, + "z" => 444_f32, + "braceleft" => 480_f32, + "bar" => 200_f32, + "braceright" => 480_f32, + "asciitilde" => 541_f32, + "exclamdown" => 333_f32, + "cent" => 500_f32, + "sterling" => 500_f32, + "fraction" => 167_f32, + "yen" => 500_f32, + "florin" => 500_f32, + "section" => 500_f32, + "currency" => 500_f32, + "quotesingle" => 180_f32, + "quotedblleft" => 444_f32, + "guillemotleft" => 500_f32, + "guilsinglleft" => 333_f32, + "guilsinglright" => 333_f32, + "fi" => 556_f32, + "fl" => 556_f32, + "endash" => 500_f32, + "dagger" => 500_f32, + "daggerdbl" => 500_f32, + "periodcentered" => 250_f32, + "paragraph" => 453_f32, + "bullet" => 350_f32, + "quotesinglbase" => 333_f32, + "quotedblbase" => 444_f32, + "quotedblright" => 444_f32, + "guillemotright" => 500_f32, + "ellipsis" => 1000_f32, + "perthousand" => 1000_f32, + "questiondown" => 444_f32, + "grave" => 333_f32, + "acute" => 333_f32, + "circumflex" => 333_f32, + "tilde" => 333_f32, + "macron" => 333_f32, + "breve" => 333_f32, + "dotaccent" => 333_f32, + "dieresis" => 333_f32, + "ring" => 333_f32, + "cedilla" => 333_f32, + "hungarumlaut" => 333_f32, + "ogonek" => 333_f32, + "caron" => 333_f32, + "emdash" => 1000_f32, + "AE" => 889_f32, + "ordfeminine" => 276_f32, + "Lslash" => 611_f32, + "Oslash" => 722_f32, + "OE" => 889_f32, + "ordmasculine" => 310_f32, + "ae" => 667_f32, + "dotlessi" => 278_f32, + "lslash" => 278_f32, + "oslash" => 500_f32, + "oe" => 722_f32, + "germandbls" => 500_f32, + "Idieresis" => 333_f32, + "eacute" => 444_f32, + "abreve" => 444_f32, + "uhungarumlaut" => 500_f32, + "ecaron" => 444_f32, + "Ydieresis" => 722_f32, + "divide" => 564_f32, + "Yacute" => 722_f32, + "Acircumflex" => 722_f32, + "aacute" => 444_f32, + "Ucircumflex" => 722_f32, + "yacute" => 500_f32, + "scommaaccent" => 389_f32, + "ecircumflex" => 444_f32, + "Uring" => 722_f32, + "Udieresis" => 722_f32, + "aogonek" => 444_f32, + "Uacute" => 722_f32, + "uogonek" => 500_f32, + "Edieresis" => 611_f32, + "Dcroat" => 722_f32, + "commaaccent" => 250_f32, + "copyright" => 760_f32, + "Emacron" => 611_f32, + "ccaron" => 444_f32, + "aring" => 444_f32, + "Ncommaaccent" => 722_f32, + "lacute" => 278_f32, + "agrave" => 444_f32, + "Tcommaaccent" => 611_f32, + "Cacute" => 667_f32, + "atilde" => 444_f32, + "Edotaccent" => 611_f32, + "scaron" => 389_f32, + "scedilla" => 389_f32, + "iacute" => 278_f32, + "lozenge" => 471_f32, + "Rcaron" => 667_f32, + "Gcommaaccent" => 722_f32, + "ucircumflex" => 500_f32, + "acircumflex" => 444_f32, + "Amacron" => 722_f32, + "rcaron" => 333_f32, + "ccedilla" => 444_f32, + "Zdotaccent" => 611_f32, + "Thorn" => 556_f32, + "Omacron" => 722_f32, + "Racute" => 667_f32, + "Sacute" => 556_f32, + "dcaron" => 588_f32, + "Umacron" => 722_f32, + "uring" => 500_f32, + "threesuperior" => 300_f32, + "Ograve" => 722_f32, + "Agrave" => 722_f32, + "Abreve" => 722_f32, + "multiply" => 564_f32, + "uacute" => 500_f32, + "Tcaron" => 611_f32, + "partialdiff" => 476_f32, + "ydieresis" => 500_f32, + "Nacute" => 722_f32, + "icircumflex" => 278_f32, + "Ecircumflex" => 611_f32, + "adieresis" => 444_f32, + "edieresis" => 444_f32, + "cacute" => 444_f32, + "nacute" => 500_f32, + "umacron" => 500_f32, + "Ncaron" => 722_f32, + "Iacute" => 333_f32, + "plusminus" => 564_f32, + "brokenbar" => 200_f32, + "registered" => 760_f32, + "Gbreve" => 722_f32, + "Idotaccent" => 333_f32, + "summation" => 600_f32, + "Egrave" => 611_f32, + "racute" => 333_f32, + "omacron" => 500_f32, + "Zacute" => 611_f32, + "Zcaron" => 611_f32, + "greaterequal" => 549_f32, + "Eth" => 722_f32, + "Ccedilla" => 667_f32, + "lcommaaccent" => 278_f32, + "tcaron" => 326_f32, + "eogonek" => 444_f32, + "Uogonek" => 722_f32, + "Aacute" => 722_f32, + "Adieresis" => 722_f32, + "egrave" => 444_f32, + "zacute" => 444_f32, + "iogonek" => 278_f32, + "Oacute" => 722_f32, + "oacute" => 500_f32, + "amacron" => 444_f32, + "sacute" => 389_f32, + "idieresis" => 278_f32, + "Ocircumflex" => 722_f32, + "Ugrave" => 722_f32, + "Delta" => 612_f32, + "thorn" => 500_f32, + "twosuperior" => 300_f32, + "Odieresis" => 722_f32, + "mu" => 500_f32, + "igrave" => 278_f32, + "ohungarumlaut" => 500_f32, + "Eogonek" => 611_f32, + "dcroat" => 500_f32, + "threequarters" => 750_f32, + "Scedilla" => 556_f32, + "lcaron" => 344_f32, + "Kcommaaccent" => 722_f32, + "Lacute" => 611_f32, + "trademark" => 980_f32, + "edotaccent" => 444_f32, + "Igrave" => 333_f32, + "Imacron" => 333_f32, + "Lcaron" => 611_f32, + "onehalf" => 750_f32, + "lessequal" => 549_f32, + "ocircumflex" => 500_f32, + "ntilde" => 500_f32, + "Uhungarumlaut" => 722_f32, + "Eacute" => 611_f32, + "emacron" => 444_f32, + "gbreve" => 500_f32, + "onequarter" => 750_f32, + "Scaron" => 556_f32, + "Scommaaccent" => 556_f32, + "Ohungarumlaut" => 722_f32, + "degree" => 400_f32, + "ograve" => 500_f32, + "Ccaron" => 667_f32, + "ugrave" => 500_f32, + "radical" => 453_f32, + "Dcaron" => 722_f32, + "rcommaaccent" => 333_f32, + "Ntilde" => 722_f32, + "otilde" => 500_f32, + "Rcommaaccent" => 667_f32, + "Lcommaaccent" => 611_f32, + "Atilde" => 722_f32, + "Aogonek" => 722_f32, + "Aring" => 722_f32, + "Otilde" => 722_f32, + "zdotaccent" => 444_f32, + "Ecaron" => 611_f32, + "Iogonek" => 333_f32, + "kcommaaccent" => 500_f32, + "minus" => 564_f32, + "Icircumflex" => 333_f32, + "ncaron" => 500_f32, + "tcommaaccent" => 278_f32, + "logicalnot" => 564_f32, + "odieresis" => 500_f32, + "udieresis" => 500_f32, + "notequal" => 549_f32, + "gcommaaccent" => 500_f32, + "eth" => 500_f32, + "zcaron" => 444_f32, + "ncommaaccent" => 500_f32, + "onesuperior" => 300_f32, + "imacron" => 278_f32, + "Euro" => 500_f32, +}; + +pub(crate) static ZAPF_DING_BATS: phf::Map<&'static str, f32> = phf_map! { + "space" => 278_f32, + "a1" => 974_f32, + "a2" => 961_f32, + "a202" => 974_f32, + "a3" => 980_f32, + "a4" => 719_f32, + "a5" => 789_f32, + "a119" => 790_f32, + "a118" => 791_f32, + "a117" => 690_f32, + "a11" => 960_f32, + "a12" => 939_f32, + "a13" => 549_f32, + "a14" => 855_f32, + "a15" => 911_f32, + "a16" => 933_f32, + "a105" => 911_f32, + "a17" => 945_f32, + "a18" => 974_f32, + "a19" => 755_f32, + "a20" => 846_f32, + "a21" => 762_f32, + "a22" => 761_f32, + "a23" => 571_f32, + "a24" => 677_f32, + "a25" => 763_f32, + "a26" => 760_f32, + "a27" => 759_f32, + "a28" => 754_f32, + "a6" => 494_f32, + "a7" => 552_f32, + "a8" => 537_f32, + "a9" => 577_f32, + "a10" => 692_f32, + "a29" => 786_f32, + "a30" => 788_f32, + "a31" => 788_f32, + "a32" => 790_f32, + "a33" => 793_f32, + "a34" => 794_f32, + "a35" => 816_f32, + "a36" => 823_f32, + "a37" => 789_f32, + "a38" => 841_f32, + "a39" => 823_f32, + "a40" => 833_f32, + "a41" => 816_f32, + "a42" => 831_f32, + "a43" => 923_f32, + "a44" => 744_f32, + "a45" => 723_f32, + "a46" => 749_f32, + "a47" => 790_f32, + "a48" => 792_f32, + "a49" => 695_f32, + "a50" => 776_f32, + "a51" => 768_f32, + "a52" => 792_f32, + "a53" => 759_f32, + "a54" => 707_f32, + "a55" => 708_f32, + "a56" => 682_f32, + "a57" => 701_f32, + "a58" => 826_f32, + "a59" => 815_f32, + "a60" => 789_f32, + "a61" => 789_f32, + "a62" => 707_f32, + "a63" => 687_f32, + "a64" => 696_f32, + "a65" => 689_f32, + "a66" => 786_f32, + "a67" => 787_f32, + "a68" => 713_f32, + "a69" => 791_f32, + "a70" => 785_f32, + "a71" => 791_f32, + "a72" => 873_f32, + "a73" => 761_f32, + "a74" => 762_f32, + "a203" => 762_f32, + "a75" => 759_f32, + "a204" => 759_f32, + "a76" => 892_f32, + "a77" => 892_f32, + "a78" => 788_f32, + "a79" => 784_f32, + "a81" => 438_f32, + "a82" => 138_f32, + "a83" => 277_f32, + "a84" => 415_f32, + "a97" => 392_f32, + "a98" => 392_f32, + "a99" => 668_f32, + "a100" => 668_f32, + "a89" => 390_f32, + "a90" => 390_f32, + "a93" => 317_f32, + "a94" => 317_f32, + "a91" => 276_f32, + "a92" => 276_f32, + "a205" => 509_f32, + "a85" => 509_f32, + "a206" => 410_f32, + "a86" => 410_f32, + "a87" => 234_f32, + "a88" => 234_f32, + "a95" => 334_f32, + "a96" => 334_f32, + "a101" => 732_f32, + "a102" => 544_f32, + "a103" => 544_f32, + "a104" => 910_f32, + "a106" => 667_f32, + "a107" => 760_f32, + "a108" => 760_f32, + "a112" => 776_f32, + "a111" => 595_f32, + "a110" => 694_f32, + "a109" => 626_f32, + "a120" => 788_f32, + "a121" => 788_f32, + "a122" => 788_f32, + "a123" => 788_f32, + "a124" => 788_f32, + "a125" => 788_f32, + "a126" => 788_f32, + "a127" => 788_f32, + "a128" => 788_f32, + "a129" => 788_f32, + "a130" => 788_f32, + "a131" => 788_f32, + "a132" => 788_f32, + "a133" => 788_f32, + "a134" => 788_f32, + "a135" => 788_f32, + "a136" => 788_f32, + "a137" => 788_f32, + "a138" => 788_f32, + "a139" => 788_f32, + "a140" => 788_f32, + "a141" => 788_f32, + "a142" => 788_f32, + "a143" => 788_f32, + "a144" => 788_f32, + "a145" => 788_f32, + "a146" => 788_f32, + "a147" => 788_f32, + "a148" => 788_f32, + "a149" => 788_f32, + "a150" => 788_f32, + "a151" => 788_f32, + "a152" => 788_f32, + "a153" => 788_f32, + "a154" => 788_f32, + "a155" => 788_f32, + "a156" => 788_f32, + "a157" => 788_f32, + "a158" => 788_f32, + "a159" => 788_f32, + "a160" => 894_f32, + "a161" => 838_f32, + "a163" => 1016_f32, + "a164" => 458_f32, + "a196" => 748_f32, + "a165" => 924_f32, + "a192" => 748_f32, + "a166" => 918_f32, + "a167" => 927_f32, + "a168" => 928_f32, + "a169" => 928_f32, + "a170" => 834_f32, + "a171" => 873_f32, + "a172" => 828_f32, + "a173" => 924_f32, + "a162" => 924_f32, + "a174" => 917_f32, + "a175" => 930_f32, + "a176" => 931_f32, + "a177" => 463_f32, + "a178" => 883_f32, + "a179" => 836_f32, + "a193" => 836_f32, + "a180" => 867_f32, + "a199" => 867_f32, + "a181" => 696_f32, + "a200" => 696_f32, + "a182" => 874_f32, + "a201" => 874_f32, + "a183" => 760_f32, + "a184" => 946_f32, + "a197" => 771_f32, + "a185" => 865_f32, + "a194" => 771_f32, + "a198" => 888_f32, + "a186" => 967_f32, + "a195" => 888_f32, + "a187" => 831_f32, + "a188" => 873_f32, + "a189" => 927_f32, + "a190" => 970_f32, + "a191" => 918_f32, +}; diff --git a/vendor/hayro-interpret/src/font/generated/mod.rs b/vendor/hayro-interpret/src/font/generated/mod.rs new file mode 100644 index 00000000..4edf16a7 --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/mod.rs @@ -0,0 +1,9 @@ +pub(crate) mod glyph_names; +pub(crate) mod mac_expert; +pub(crate) mod mac_os_roman; +pub(crate) mod mac_roman; +pub(crate) mod metrics; +pub(crate) mod standard; +pub(crate) mod symbol; +pub(crate) mod win_ansi; +pub(crate) mod zapf_dings; diff --git a/vendor/hayro-interpret/src/font/generated/standard.rs b/vendor/hayro-interpret/src/font/generated/standard.rs new file mode 100644 index 00000000..a3972c8e --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/standard.rs @@ -0,0 +1,157 @@ +use phf::phf_map; + +pub(crate) fn get(code: u8) -> Option<&'static str> { + STANDARD.get(&code).copied() +} + +static STANDARD: phf::Map = phf_map! { + 32_u8 => "space", + 33_u8 => "exclam", + 34_u8 => "quotedbl", + 35_u8 => "numbersign", + 36_u8 => "dollar", + 37_u8 => "percent", + 38_u8 => "ampersand", + 39_u8 => "quoteright", + 40_u8 => "parenleft", + 41_u8 => "parenright", + 42_u8 => "asterisk", + 43_u8 => "plus", + 44_u8 => "comma", + 45_u8 => "hyphen", + 46_u8 => "period", + 47_u8 => "slash", + 48_u8 => "zero", + 49_u8 => "one", + 50_u8 => "two", + 51_u8 => "three", + 52_u8 => "four", + 53_u8 => "five", + 54_u8 => "six", + 55_u8 => "seven", + 56_u8 => "eight", + 57_u8 => "nine", + 58_u8 => "colon", + 59_u8 => "semicolon", + 60_u8 => "less", + 61_u8 => "equal", + 62_u8 => "greater", + 63_u8 => "question", + 64_u8 => "at", + 65_u8 => "A", + 66_u8 => "B", + 67_u8 => "C", + 68_u8 => "D", + 69_u8 => "E", + 70_u8 => "F", + 71_u8 => "G", + 72_u8 => "H", + 73_u8 => "I", + 74_u8 => "J", + 75_u8 => "K", + 76_u8 => "L", + 77_u8 => "M", + 78_u8 => "N", + 79_u8 => "O", + 80_u8 => "P", + 81_u8 => "Q", + 82_u8 => "R", + 83_u8 => "S", + 84_u8 => "T", + 85_u8 => "U", + 86_u8 => "V", + 87_u8 => "W", + 88_u8 => "X", + 89_u8 => "Y", + 90_u8 => "Z", + 91_u8 => "bracketleft", + 92_u8 => "backslash", + 93_u8 => "bracketright", + 94_u8 => "asciicircum", + 95_u8 => "underscore", + 96_u8 => "quoteleft", + 97_u8 => "a", + 98_u8 => "b", + 99_u8 => "c", + 100_u8 => "d", + 101_u8 => "e", + 102_u8 => "f", + 103_u8 => "g", + 104_u8 => "h", + 105_u8 => "i", + 106_u8 => "j", + 107_u8 => "k", + 108_u8 => "l", + 109_u8 => "m", + 110_u8 => "n", + 111_u8 => "o", + 112_u8 => "p", + 113_u8 => "q", + 114_u8 => "r", + 115_u8 => "s", + 116_u8 => "t", + 117_u8 => "u", + 118_u8 => "v", + 119_u8 => "w", + 120_u8 => "x", + 121_u8 => "y", + 122_u8 => "z", + 123_u8 => "braceleft", + 124_u8 => "bar", + 125_u8 => "braceright", + 126_u8 => "asciitilde", + 161_u8 => "exclamdown", + 162_u8 => "cent", + 163_u8 => "sterling", + 164_u8 => "fraction", + 165_u8 => "yen", + 166_u8 => "florin", + 167_u8 => "section", + 168_u8 => "currency", + 169_u8 => "quotesingle", + 170_u8 => "quotedblleft", + 171_u8 => "guillemotleft", + 172_u8 => "guilsinglleft", + 173_u8 => "guilsinglright", + 174_u8 => "fi", + 175_u8 => "fl", + 177_u8 => "endash", + 178_u8 => "dagger", + 179_u8 => "daggerdbl", + 180_u8 => "periodcentered", + 182_u8 => "paragraph", + 183_u8 => "bullet", + 184_u8 => "quotesinglbase", + 185_u8 => "quotedblbase", + 186_u8 => "quotedblright", + 187_u8 => "guillemotright", + 188_u8 => "ellipsis", + 189_u8 => "perthousand", + 191_u8 => "questiondown", + 193_u8 => "grave", + 194_u8 => "acute", + 195_u8 => "circumflex", + 196_u8 => "tilde", + 197_u8 => "macron", + 198_u8 => "breve", + 199_u8 => "dotaccent", + 200_u8 => "dieresis", + 202_u8 => "ring", + 203_u8 => "cedilla", + 205_u8 => "hungarumlaut", + 206_u8 => "ogonek", + 207_u8 => "caron", + 208_u8 => "emdash", + 225_u8 => "AE", + 227_u8 => "ordfeminine", + 232_u8 => "Lslash", + 233_u8 => "Oslash", + 234_u8 => "OE", + 235_u8 => "ordmasculine", + 241_u8 => "ae", + 245_u8 => "dotlessi", + 248_u8 => "lslash", + 249_u8 => "oslash", + 250_u8 => "oe", + 251_u8 => "germandbls", +}; diff --git a/vendor/hayro-interpret/src/font/generated/symbol.rs b/vendor/hayro-interpret/src/font/generated/symbol.rs new file mode 100644 index 00000000..ba68a278 --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/symbol.rs @@ -0,0 +1,197 @@ +use phf::phf_map; + +pub(crate) fn get(code: u8) -> Option<&'static str> { + SYMBOL.get(&code).copied() +} + +static SYMBOL: phf::Map = phf_map! { + 32_u8 => "space", + 33_u8 => "exclam", + 34_u8 => "universal", + 35_u8 => "numbersign", + 36_u8 => "existential", + 37_u8 => "percent", + 38_u8 => "ampersand", + 39_u8 => "suchthat", + 40_u8 => "parenleft", + 41_u8 => "parenright", + 42_u8 => "asteriskmath", + 43_u8 => "plus", + 44_u8 => "comma", + 45_u8 => "minus", + 46_u8 => "period", + 47_u8 => "slash", + 48_u8 => "zero", + 49_u8 => "one", + 50_u8 => "two", + 51_u8 => "three", + 52_u8 => "four", + 53_u8 => "five", + 54_u8 => "six", + 55_u8 => "seven", + 56_u8 => "eight", + 57_u8 => "nine", + 58_u8 => "colon", + 59_u8 => "semicolon", + 60_u8 => "less", + 61_u8 => "equal", + 62_u8 => "greater", + 63_u8 => "question", + 64_u8 => "congruent", + 65_u8 => "Alpha", + 66_u8 => "Beta", + 67_u8 => "Chi", + 68_u8 => "Delta", + 69_u8 => "Epsilon", + 70_u8 => "Phi", + 71_u8 => "Gamma", + 72_u8 => "Eta", + 73_u8 => "Iota", + 74_u8 => "theta1", + 75_u8 => "Kappa", + 76_u8 => "Lambda", + 77_u8 => "Mu", + 78_u8 => "Nu", + 79_u8 => "Omicron", + 80_u8 => "Pi", + 81_u8 => "Theta", + 82_u8 => "Rho", + 83_u8 => "Sigma", + 84_u8 => "Tau", + 85_u8 => "Upsilon", + 86_u8 => "sigma1", + 87_u8 => "Omega", + 88_u8 => "Xi", + 89_u8 => "Psi", + 90_u8 => "Zeta", + 91_u8 => "bracketleft", + 92_u8 => "therefore", + 93_u8 => "bracketright", + 94_u8 => "perpendicular", + 95_u8 => "underscore", + 96_u8 => "radicalex", + 97_u8 => "alpha", + 98_u8 => "beta", + 99_u8 => "chi", + 100_u8 => "delta", + 101_u8 => "epsilon", + 102_u8 => "phi", + 103_u8 => "gamma", + 104_u8 => "eta", + 105_u8 => "iota", + 106_u8 => "phi1", + 107_u8 => "kappa", + 108_u8 => "lambda", + 109_u8 => "mu", + 110_u8 => "nu", + 111_u8 => "omicron", + 112_u8 => "pi", + 113_u8 => "theta", + 114_u8 => "rho", + 115_u8 => "sigma", + 116_u8 => "tau", + 117_u8 => "upsilon", + 118_u8 => "omega1", + 119_u8 => "omega", + 120_u8 => "xi", + 121_u8 => "psi", + 122_u8 => "zeta", + 123_u8 => "braceleft", + 124_u8 => "bar", + 125_u8 => "braceright", + 126_u8 => "similar", + 160_u8 => "Euro", + 161_u8 => "Upsilon1", + 162_u8 => "minute", + 163_u8 => "lessequal", + 164_u8 => "fraction", + 165_u8 => "infinity", + 166_u8 => "florin", + 167_u8 => "club", + 168_u8 => "diamond", + 169_u8 => "heart", + 170_u8 => "spade", + 171_u8 => "arrowboth", + 172_u8 => "arrowleft", + 173_u8 => "arrowup", + 174_u8 => "arrowright", + 175_u8 => "arrowdown", + 176_u8 => "degree", + 177_u8 => "plusminus", + 178_u8 => "second", + 179_u8 => "greaterequal", + 180_u8 => "multiply", + 181_u8 => "proportional", + 182_u8 => "partialdiff", + 183_u8 => "bullet", + 184_u8 => "divide", + 185_u8 => "notequal", + 186_u8 => "equivalence", + 187_u8 => "approxequal", + 188_u8 => "ellipsis", + 189_u8 => "arrowvertex", + 190_u8 => "arrowhorizex", + 191_u8 => "carriagereturn", + 192_u8 => "aleph", + 193_u8 => "Ifraktur", + 194_u8 => "Rfraktur", + 195_u8 => "weierstrass", + 196_u8 => "circlemultiply", + 197_u8 => "circleplus", + 198_u8 => "emptyset", + 199_u8 => "intersection", + 200_u8 => "union", + 201_u8 => "propersuperset", + 202_u8 => "reflexsuperset", + 203_u8 => "notsubset", + 204_u8 => "propersubset", + 205_u8 => "reflexsubset", + 206_u8 => "element", + 207_u8 => "notelement", + 208_u8 => "angle", + 209_u8 => "gradient", + 210_u8 => "registerserif", + 211_u8 => "copyrightserif", + 212_u8 => "trademarkserif", + 213_u8 => "product", + 214_u8 => "radical", + 215_u8 => "dotmath", + 216_u8 => "logicalnot", + 217_u8 => "logicaland", + 218_u8 => "logicalor", + 219_u8 => "arrowdblboth", + 220_u8 => "arrowdblleft", + 221_u8 => "arrowdblup", + 222_u8 => "arrowdblright", + 223_u8 => "arrowdbldown", + 224_u8 => "lozenge", + 225_u8 => "angleleft", + 226_u8 => "registersans", + 227_u8 => "copyrightsans", + 228_u8 => "trademarksans", + 229_u8 => "summation", + 230_u8 => "parenlefttp", + 231_u8 => "parenleftex", + 232_u8 => "parenleftbt", + 233_u8 => "bracketlefttp", + 234_u8 => "bracketleftex", + 235_u8 => "bracketleftbt", + 236_u8 => "bracelefttp", + 237_u8 => "braceleftmid", + 238_u8 => "braceleftbt", + 239_u8 => "braceex", + 241_u8 => "angleright", + 242_u8 => "integral", + 243_u8 => "integraltp", + 244_u8 => "integralex", + 245_u8 => "integralbt", + 246_u8 => "parenrighttp", + 247_u8 => "parenrightex", + 248_u8 => "parenrightbt", + 249_u8 => "bracketrighttp", + 250_u8 => "bracketrightex", + 251_u8 => "bracketrightbt", + 252_u8 => "bracerighttp", + 253_u8 => "bracerightmid", + 254_u8 => "bracerightbt", +}; diff --git a/vendor/hayro-interpret/src/font/generated/win_ansi.rs b/vendor/hayro-interpret/src/font/generated/win_ansi.rs new file mode 100644 index 00000000..6db56ba9 --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/win_ansi.rs @@ -0,0 +1,230 @@ +// See +use phf::phf_map; + +pub(crate) fn get(code: u8) -> Option<&'static str> { + WIN_ANSI + .get(&code) + .or(if code > 40 { Some(&"bullet") } else { None }) + .copied() +} + +static WIN_ANSI: phf::Map = phf_map! { + 65_u8 => "A", + 198_u8 => "AE", + 193_u8 => "Aacute", + 194_u8 => "Acircumflex", + 196_u8 => "Adieresis", + 192_u8 => "Agrave", + 197_u8 => "Aring", + 195_u8 => "Atilde", + 66_u8 => "B", + 67_u8 => "C", + 199_u8 => "Ccedilla", + 68_u8 => "D", + 69_u8 => "E", + 201_u8 => "Eacute", + 202_u8 => "Ecircumflex", + 203_u8 => "Edieresis", + 200_u8 => "Egrave", + 208_u8 => "Eth", + 128_u8 => "Euro", + 70_u8 => "F", + 71_u8 => "G", + 72_u8 => "H", + 73_u8 => "I", + 205_u8 => "Iacute", + 206_u8 => "Icircumflex", + 207_u8 => "Idieresis", + 204_u8 => "Igrave", + 74_u8 => "J", + 75_u8 => "K", + 76_u8 => "L", + 77_u8 => "M", + 78_u8 => "N", + 209_u8 => "Ntilde", + 79_u8 => "O", + 140_u8 => "OE", + 211_u8 => "Oacute", + 212_u8 => "Ocircumflex", + 214_u8 => "Odieresis", + 210_u8 => "Ograve", + 216_u8 => "Oslash", + 213_u8 => "Otilde", + 80_u8 => "P", + 81_u8 => "Q", + 82_u8 => "R", + 83_u8 => "S", + 138_u8 => "Scaron", + 84_u8 => "T", + 222_u8 => "Thorn", + 85_u8 => "U", + 218_u8 => "Uacute", + 219_u8 => "Ucircumflex", + 220_u8 => "Udieresis", + 217_u8 => "Ugrave", + 86_u8 => "V", + 87_u8 => "W", + 88_u8 => "X", + 89_u8 => "Y", + 221_u8 => "Yacute", + 159_u8 => "Ydieresis", + 90_u8 => "Z", + 142_u8 => "Zcaron", + 97_u8 => "a", + 225_u8 => "aacute", + 226_u8 => "acircumflex", + 180_u8 => "acute", + 228_u8 => "adieresis", + 230_u8 => "ae", + 224_u8 => "agrave", + 38_u8 => "ampersand", + 229_u8 => "aring", + 94_u8 => "asciicircum", + 126_u8 => "asciitilde", + 42_u8 => "asterisk", + 64_u8 => "at", + 227_u8 => "atilde", + 98_u8 => "b", + 92_u8 => "backslash", + 124_u8 => "bar", + 123_u8 => "braceleft", + 125_u8 => "braceright", + 91_u8 => "bracketleft", + 93_u8 => "bracketright", + 166_u8 => "brokenbar", + 149_u8 => "bullet", + 99_u8 => "c", + 231_u8 => "ccedilla", + 184_u8 => "cedilla", + 162_u8 => "cent", + 136_u8 => "circumflex", + 58_u8 => "colon", + 44_u8 => "comma", + 169_u8 => "copyright", + 164_u8 => "currency", + 100_u8 => "d", + 134_u8 => "dagger", + 135_u8 => "daggerdbl", + 176_u8 => "degree", + 168_u8 => "dieresis", + 247_u8 => "divide", + 36_u8 => "dollar", + 101_u8 => "e", + 233_u8 => "eacute", + 234_u8 => "ecircumflex", + 235_u8 => "edieresis", + 232_u8 => "egrave", + 56_u8 => "eight", + 133_u8 => "ellipsis", + 151_u8 => "emdash", + 150_u8 => "endash", + 61_u8 => "equal", + 240_u8 => "eth", + 33_u8 => "exclam", + 161_u8 => "exclamdown", + 102_u8 => "f", + 53_u8 => "five", + 131_u8 => "florin", + 52_u8 => "four", + 103_u8 => "g", + 223_u8 => "germandbls", + 96_u8 => "grave", + 62_u8 => "greater", + 171_u8 => "guillemotleft", + 187_u8 => "guillemotright", + 139_u8 => "guilsinglleft", + 155_u8 => "guilsinglright", + 104_u8 => "h", + 45_u8 => "hyphen", + 105_u8 => "i", + 237_u8 => "iacute", + 238_u8 => "icircumflex", + 239_u8 => "idieresis", + 236_u8 => "igrave", + 106_u8 => "j", + 107_u8 => "k", + 108_u8 => "l", + 60_u8 => "less", + 172_u8 => "logicalnot", + 109_u8 => "m", + 175_u8 => "macron", + 181_u8 => "mu", + 215_u8 => "multiply", + 110_u8 => "n", + 57_u8 => "nine", + 241_u8 => "ntilde", + 35_u8 => "numbersign", + 111_u8 => "o", + 243_u8 => "oacute", + 244_u8 => "ocircumflex", + 246_u8 => "odieresis", + 156_u8 => "oe", + 242_u8 => "ograve", + 49_u8 => "one", + 189_u8 => "onehalf", + 188_u8 => "onequarter", + 185_u8 => "onesuperior", + 170_u8 => "ordfeminine", + 186_u8 => "ordmasculine", + 248_u8 => "oslash", + 245_u8 => "otilde", + 112_u8 => "p", + 182_u8 => "paragraph", + 40_u8 => "parenleft", + 41_u8 => "parenright", + 37_u8 => "percent", + 46_u8 => "period", + 183_u8 => "periodcentered", + 137_u8 => "perthousand", + 43_u8 => "plus", + 177_u8 => "plusminus", + 113_u8 => "q", + 63_u8 => "question", + 191_u8 => "questiondown", + 34_u8 => "quotedbl", + 132_u8 => "quotedblbase", + 147_u8 => "quotedblleft", + 148_u8 => "quotedblright", + 145_u8 => "quoteleft", + 146_u8 => "quoteright", + 130_u8 => "quotesinglbase", + 39_u8 => "quotesingle", + 114_u8 => "r", + 174_u8 => "registered", + 115_u8 => "s", + 154_u8 => "scaron", + 167_u8 => "section", + 59_u8 => "semicolon", + 55_u8 => "seven", + 54_u8 => "six", + 47_u8 => "slash", + 32_u8 => "space", + 163_u8 => "sterling", + 116_u8 => "t", + 254_u8 => "thorn", + 51_u8 => "three", + 190_u8 => "threequarters", + 179_u8 => "threesuperior", + 152_u8 => "tilde", + 153_u8 => "trademark", + 50_u8 => "two", + 178_u8 => "twosuperior", + 117_u8 => "u", + 250_u8 => "uacute", + 251_u8 => "ucircumflex", + 252_u8 => "udieresis", + 249_u8 => "ugrave", + 95_u8 => "underscore", + 118_u8 => "v", + 119_u8 => "w", + 120_u8 => "x", + 121_u8 => "y", + 253_u8 => "yacute", + 255_u8 => "ydieresis", + 165_u8 => "yen", + 122_u8 => "z", + 158_u8 => "zcaron", + 48_u8 => "zero", + 160_u8 => "nbspace", + 173_u8 => "sfthyphen", +}; diff --git a/vendor/hayro-interpret/src/font/generated/zapf_dings.rs b/vendor/hayro-interpret/src/font/generated/zapf_dings.rs new file mode 100644 index 00000000..19834cfd --- /dev/null +++ b/vendor/hayro-interpret/src/font/generated/zapf_dings.rs @@ -0,0 +1,210 @@ +use phf::phf_map; + +pub(crate) fn get(code: u8) -> Option<&'static str> { + ZAPF_DING_BATS.get(&code).copied() +} + +static ZAPF_DING_BATS: phf::Map = phf_map! { + 32_u8 => "space", + 33_u8 => "a1", + 34_u8 => "a2", + 35_u8 => "a202", + 36_u8 => "a3", + 37_u8 => "a4", + 38_u8 => "a5", + 39_u8 => "a119", + 40_u8 => "a118", + 41_u8 => "a117", + 42_u8 => "a11", + 43_u8 => "a12", + 44_u8 => "a13", + 45_u8 => "a14", + 46_u8 => "a15", + 47_u8 => "a16", + 48_u8 => "a105", + 49_u8 => "a17", + 50_u8 => "a18", + 51_u8 => "a19", + 52_u8 => "a20", + 53_u8 => "a21", + 54_u8 => "a22", + 55_u8 => "a23", + 56_u8 => "a24", + 57_u8 => "a25", + 58_u8 => "a26", + 59_u8 => "a27", + 60_u8 => "a28", + 61_u8 => "a6", + 62_u8 => "a7", + 63_u8 => "a8", + 64_u8 => "a9", + 65_u8 => "a10", + 66_u8 => "a29", + 67_u8 => "a30", + 68_u8 => "a31", + 69_u8 => "a32", + 70_u8 => "a33", + 71_u8 => "a34", + 72_u8 => "a35", + 73_u8 => "a36", + 74_u8 => "a37", + 75_u8 => "a38", + 76_u8 => "a39", + 77_u8 => "a40", + 78_u8 => "a41", + 79_u8 => "a42", + 80_u8 => "a43", + 81_u8 => "a44", + 82_u8 => "a45", + 83_u8 => "a46", + 84_u8 => "a47", + 85_u8 => "a48", + 86_u8 => "a49", + 87_u8 => "a50", + 88_u8 => "a51", + 89_u8 => "a52", + 90_u8 => "a53", + 91_u8 => "a54", + 92_u8 => "a55", + 93_u8 => "a56", + 94_u8 => "a57", + 95_u8 => "a58", + 96_u8 => "a59", + 97_u8 => "a60", + 98_u8 => "a61", + 99_u8 => "a62", + 100_u8 => "a63", + 101_u8 => "a64", + 102_u8 => "a65", + 103_u8 => "a66", + 104_u8 => "a67", + 105_u8 => "a68", + 106_u8 => "a69", + 107_u8 => "a70", + 108_u8 => "a71", + 109_u8 => "a72", + 110_u8 => "a73", + 111_u8 => "a74", + 112_u8 => "a203", + 113_u8 => "a75", + 114_u8 => "a204", + 115_u8 => "a76", + 116_u8 => "a77", + 117_u8 => "a78", + 118_u8 => "a79", + 119_u8 => "a81", + 120_u8 => "a82", + 121_u8 => "a83", + 122_u8 => "a84", + 123_u8 => "a97", + 124_u8 => "a98", + 125_u8 => "a99", + 126_u8 => "a100", + 128_u8 => "a89", + 129_u8 => "a90", + 130_u8 => "a93", + 131_u8 => "a94", + 132_u8 => "a91", + 133_u8 => "a92", + 134_u8 => "a205", + 135_u8 => "a85", + 136_u8 => "a206", + 137_u8 => "a86", + 138_u8 => "a87", + 139_u8 => "a88", + 140_u8 => "a95", + 141_u8 => "a96", + 161_u8 => "a101", + 162_u8 => "a102", + 163_u8 => "a103", + 164_u8 => "a104", + 165_u8 => "a106", + 166_u8 => "a107", + 167_u8 => "a108", + 168_u8 => "a112", + 169_u8 => "a111", + 170_u8 => "a110", + 171_u8 => "a109", + 172_u8 => "a120", + 173_u8 => "a121", + 174_u8 => "a122", + 175_u8 => "a123", + 176_u8 => "a124", + 177_u8 => "a125", + 178_u8 => "a126", + 179_u8 => "a127", + 180_u8 => "a128", + 181_u8 => "a129", + 182_u8 => "a130", + 183_u8 => "a131", + 184_u8 => "a132", + 185_u8 => "a133", + 186_u8 => "a134", + 187_u8 => "a135", + 188_u8 => "a136", + 189_u8 => "a137", + 190_u8 => "a138", + 191_u8 => "a139", + 192_u8 => "a140", + 193_u8 => "a141", + 194_u8 => "a142", + 195_u8 => "a143", + 196_u8 => "a144", + 197_u8 => "a145", + 198_u8 => "a146", + 199_u8 => "a147", + 200_u8 => "a148", + 201_u8 => "a149", + 202_u8 => "a150", + 203_u8 => "a151", + 204_u8 => "a152", + 205_u8 => "a153", + 206_u8 => "a154", + 207_u8 => "a155", + 208_u8 => "a156", + 209_u8 => "a157", + 210_u8 => "a158", + 211_u8 => "a159", + 212_u8 => "a160", + 213_u8 => "a161", + 214_u8 => "a163", + 215_u8 => "a164", + 216_u8 => "a196", + 217_u8 => "a165", + 218_u8 => "a192", + 219_u8 => "a166", + 220_u8 => "a167", + 221_u8 => "a168", + 222_u8 => "a169", + 223_u8 => "a170", + 224_u8 => "a171", + 225_u8 => "a172", + 226_u8 => "a173", + 227_u8 => "a162", + 228_u8 => "a174", + 229_u8 => "a175", + 230_u8 => "a176", + 231_u8 => "a177", + 232_u8 => "a178", + 233_u8 => "a179", + 234_u8 => "a193", + 235_u8 => "a180", + 236_u8 => "a199", + 237_u8 => "a181", + 238_u8 => "a200", + 239_u8 => "a182", + 241_u8 => "a201", + 242_u8 => "a183", + 243_u8 => "a184", + 244_u8 => "a197", + 245_u8 => "a185", + 246_u8 => "a194", + 247_u8 => "a198", + 248_u8 => "a186", + 249_u8 => "a195", + 250_u8 => "a187", + 251_u8 => "a188", + 252_u8 => "a189", + 253_u8 => "a190", + 254_u8 => "a191", +}; diff --git a/vendor/hayro-interpret/src/font/glyph_simulator.rs b/vendor/hayro-interpret/src/font/glyph_simulator.rs new file mode 100644 index 00000000..903a6632 --- /dev/null +++ b/vendor/hayro-interpret/src/font/glyph_simulator.rs @@ -0,0 +1,53 @@ +use skrifa::GlyphId; +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; + +#[derive(Debug)] +pub(crate) struct GlyphSimulator { + // We simulate that Type1 glyphs have glyph IDs so we can handle them transparently + // to OpenType fonts. + glyph_to_string: RefCell>, + string_to_glyph: RefCell>, + glyph_counter: Cell, +} + +impl GlyphSimulator { + pub(crate) fn new() -> Self { + let mut glyph_to_string = HashMap::new(); + glyph_to_string.insert(GlyphId::NOTDEF, "notdef".to_string()); + + let mut string_to_glyph = HashMap::new(); + string_to_glyph.insert("notdef".to_string(), GlyphId::NOTDEF); + + Self { + glyph_to_string: RefCell::new(glyph_to_string), + string_to_glyph: RefCell::new(string_to_glyph), + glyph_counter: Cell::new(1), + } + } + + pub(crate) fn string_to_glyph(&self, string: &str) -> GlyphId { + if let Some(g) = self.string_to_glyph.borrow().get(string) { + *g + } else { + let gid = GlyphId::new(self.glyph_counter.get()); + self.string_to_glyph + .borrow_mut() + .insert(string.to_string(), gid); + self.glyph_to_string + .borrow_mut() + .insert(gid, string.to_string()); + + self.glyph_counter.set(self.glyph_counter.get() + 1); + + gid + } + } + + pub(crate) fn glyph_to_string(&self, glyph: GlyphId) -> Option { + self.glyph_to_string + .borrow() + .get(&glyph) + .map(|s| s.to_string()) + } +} diff --git a/vendor/hayro-interpret/src/font/mod.rs b/vendor/hayro-interpret/src/font/mod.rs new file mode 100644 index 00000000..5ccf5c48 --- /dev/null +++ b/vendor/hayro-interpret/src/font/mod.rs @@ -0,0 +1,689 @@ +//! Interacting with the different kinds of PDF fonts. + +use crate::context::Context; +use crate::context::InterpreterCache; +use crate::device::Device; +use crate::font::cid::Type0Font; +use crate::font::generated::{ + glyph_names, mac_expert, mac_os_roman, mac_roman, standard, win_ansi, +}; +use crate::font::true_type::TrueTypeFont; +use crate::font::type1::Type1Font; +use crate::font::type3::Type3; +use crate::interpret::state::State; +use crate::{CMapResolverFn, CacheKey, FontResolverFn, InterpreterSettings, Paint}; +use bitflags::bitflags; +use hayro_syntax::object::Name; +use hayro_syntax::object::dict::keys::SUBTYPE; +use hayro_syntax::object::dict::keys::*; +use hayro_syntax::object::{Dict, Stream}; +use hayro_syntax::page::Resources; +use hayro_syntax::xref::XRef; +use kurbo::{Affine, BezPath, Vec2}; +use outline::OutlineFont; +use skrifa::GlyphId; +use std::borrow::Cow; +use std::fmt::Debug; +use std::ops::Deref; +use std::rc::Rc; +use std::sync::Arc; + +mod blob; +mod cid; +mod generated; +mod glyph_simulator; +pub(crate) mod outline; +mod standard_font; +mod true_type; +mod type1; +pub(crate) mod type3; + +pub(crate) const UNITS_PER_EM: f32 = 1000.0; + +pub(crate) fn stretch_glyph(path: BezPath, expected_width: f32, actual_width: f32) -> BezPath { + if actual_width != 0.0 && actual_width != expected_width { + let stretch_factor = expected_width / actual_width; + Affine::scale_non_uniform(stretch_factor as f64, 1.0) * path + } else { + path + } +} + +/// A container for the bytes of a PDF file. +pub type FontData = Arc + Send + Sync>; + +/// Strip the 6-character subset prefix from a PostScript font name. +/// +/// PDF subset fonts use names like "ABCDEF+TimesNewRoman". This function +/// returns `TimesNewRoman` from such a name, or the original name if no +/// valid prefix is found. +pub(crate) fn strip_subset_prefix(name: &str) -> &str { + match name.split_once('+') { + Some((prefix, rest)) if prefix.len() == 6 => rest, + _ => name, + } +} + +use crate::util::hash128; +use hayro_cmap::{BfString, CMap, CMapName, CharacterCollection}; +pub use outline::OutlineFontData; +pub use standard_font::StandardFont; + +/// A glyph that can be drawn. +pub enum Glyph<'a> { + /// A glyph defined by an outline. + Outline(OutlineGlyph), + /// A type3 glyph, defined by PDF drawing instructions. + Type3(Box>), +} + +impl Glyph<'_> { + /// Returns the Unicode code point for this glyph, if available. + /// + /// This method attempts to determine the Unicode character that this glyph + /// represents. The exact fallback chain depends on the font type: + /// + /// **For Outline Fonts (Type1, TrueType, CFF):** + /// 1. `ToUnicode` cmap + /// 2. Glyph name โ†’ Unicode (via Adobe Glyph List) + /// 3. Unicode naming conventions (e.g., "uni0041", "u0041") + /// + /// **For CID Fonts (Type0):** + /// 1. `ToUnicode` cmap + /// + /// + /// **For Type3 Fonts:** + /// 1. `ToUnicode` cmap + /// + /// Returns `None` if the Unicode value could not be determined. + /// + /// Please note that this method is still somewhat experimental and might + /// not work reliably in all cases. + pub fn as_unicode(&self) -> Option { + match self { + Glyph::Outline(g) => g.as_unicode(), + Glyph::Type3(g) => g.as_unicode(), + } + } +} + +/// An identifier that uniquely identifies a glyph, for caching purposes. +#[derive(Clone, Debug)] +pub struct GlyphIdentifier { + id: GlyphId, + font: OutlineFont, +} + +impl CacheKey for GlyphIdentifier { + fn cache_key(&self) -> u128 { + hash128(&(self.id, self.font.cache_key())) + } +} + +/// A glyph defined by an outline. +#[derive(Clone, Debug)] +pub struct OutlineGlyph { + pub(crate) id: GlyphId, + pub(crate) font: OutlineFont, + pub(crate) char_code: u32, +} + +impl OutlineGlyph { + /// Return the outline of the glyph, assuming an upem value of 1000. + pub fn outline(&self) -> BezPath { + self.font.outline_glyph(self.id, self.char_code) + } + + /// Return the identifier of the glyph. You can use this to calculate the cache key + /// for the glyph. + /// + /// Note that the `glyph_transform` attribute is not considered in the cache key of + /// the identifier, only the glyph ID and the font. + pub fn identifier(&self) -> GlyphIdentifier { + GlyphIdentifier { + id: self.id, + font: self.font.clone(), + } + } + + /// Returns the Unicode code point for this glyph, if available. + /// + /// See [`Glyph::as_unicode`] for details on the fallback chain used. + pub fn as_unicode(&self) -> Option { + self.font.char_code_to_unicode(self.char_code) + } + + /// Get raw font bytes and metadata for downstream use. + /// + /// Returns `None` for Type1 fonts. + pub fn font_data(&self) -> Option { + self.font.font_data() + } + + /// Get the glyph ID within the font. + pub fn glyph_id(&self) -> GlyphId { + self.id + } + + /// PdfCraft patch: whether this glyph belongs to a vertical-writing Type0 font. + /// The glyph outline stays upright; text extraction must follow the font's advance axis. + pub fn is_vertical(&self) -> bool { + matches!(&self.font, OutlineFont::Type0(font) if !font.is_horizontal()) + } + + /// Get the advance width for this glyph. + /// + /// The advance width is how far to move horizontally after drawing + /// this glyph before drawing the next one. + pub fn advance_width(&self) -> Option { + self.font.glyph_advance_width(self.char_code) + } + + /// Get the cache key for this glyph's font. + /// + /// This identifies the font uniquely, even when `font_data()` returns `None` + /// (e.g., for Type1 fonts). Useful for grouping glyphs by font. + pub fn font_cache_key(&self) -> u128 { + self.font.cache_key() + } +} + +/// A type3 glyph. +#[derive(Clone)] +pub struct Type3Glyph<'a> { + pub(crate) font: Rc>, + pub(crate) glyph_id: GlyphId, + pub(crate) state: State<'a>, + pub(crate) parent_resources: Resources<'a>, + pub(crate) cache: InterpreterCache<'a>, + pub(crate) xref: &'a XRef, + pub(crate) settings: InterpreterSettings, + pub(crate) nesting_depth: u32, + pub(crate) char_code: u32, +} + +/// A glyph defined by PDF drawing instructions. +impl<'a> Type3Glyph<'a> { + /// Draw the type3 glyph to the given device. + pub fn interpret( + &self, + device: &mut impl Device<'a>, + transform: Affine, + glyph_transform: Affine, + paint: &Paint<'a>, + ) { + self.font + .render_glyph(self, transform, glyph_transform, paint, device); + } + + /// Returns the Unicode code point for this glyph, if available. + /// + /// Note: Type3 fonts can only provide Unicode via `ToUnicode` cmap. + pub fn as_unicode(&self) -> Option { + self.font.char_code_to_unicode(self.char_code) + } + + /// PdfCraft patch: the advance width in glyph space (1000 units per em), from `/Widths`. + pub fn advance_width(&self) -> Option { + u8::try_from(self.char_code).ok().map(|c| self.font.glyph_width(c)) + } +} + +impl CacheKey for Type3Glyph<'_> { + fn cache_key(&self) -> u128 { + hash128(&(self.font.cache_key(), self.glyph_id)) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct Font<'a>(u128, FontType<'a>); + +impl<'a> Font<'a> { + pub(crate) fn new( + dict: &Dict<'a>, + font_resolver: &FontResolverFn, + cmap_resolver: &CMapResolverFn, + ) -> Option { + let f_type = match dict.get::>(SUBTYPE)?.deref() { + TYPE1 | MM_TYPE1 => { + FontType::Type1(Rc::new(Type1Font::new(dict, font_resolver, cmap_resolver)?)) + } + // PDFBOX-5463: PDF viewers seem to accept OpenType as well. + TRUE_TYPE | OPEN_TYPE => FontType::TrueType(Rc::new(TrueTypeFont::new( + dict, + font_resolver, + cmap_resolver, + )?)), + TYPE0 => FontType::Type0(Rc::new(Type0Font::new(dict, font_resolver, cmap_resolver)?)), + TYPE3 => FontType::Type3(Rc::new(Type3::new(dict, cmap_resolver)?)), + f => { + warn!( + "unimplemented font type {:?}", + std::str::from_utf8(f).unwrap_or("unknown type") + ); + + return None; + } + }; + + let cache_key = dict.cache_key(); + + Some(Self(cache_key, f_type)) + } + + pub(crate) fn new_standard( + standard_font: StandardFont, + font_resolver: &FontResolverFn, + ) -> Option { + let font = Type1Font::new_standard(standard_font, font_resolver)?; + + Some(Self(0, FontType::Type1(Rc::new(font)))) + } + + pub(crate) fn map_code(&self, code: u32) -> GlyphId { + match &self.1 { + FontType::Type1(f) => { + debug_assert!(code <= u8::MAX as u32); + + f.map_code(code as u8) + } + FontType::TrueType(t) => { + debug_assert!(code <= u8::MAX as u32); + + t.map_code(code as u8) + } + FontType::Type0(t) => t.map_code(code), + FontType::Type3(t) => { + debug_assert!(code <= u8::MAX as u32); + + t.map_code(code as u8) + } + } + } + + pub(crate) fn get_glyph( + &self, + glyph: GlyphId, + char_code: u32, + ctx: &mut Context<'a>, + resources: &Resources<'a>, + origin_displacement: Vec2, + ) -> (Glyph<'a>, Affine) { + let glyph_transform = ctx.get().text_state.full_transform() + * Affine::scale(1.0 / UNITS_PER_EM as f64) + * Affine::translate(origin_displacement); + + let glyph = match &self.1 { + FontType::Type1(t) => { + let font = OutlineFont::Type1(t.clone()); + Glyph::Outline(OutlineGlyph { + id: glyph, + font, + char_code, + }) + } + FontType::TrueType(t) => { + let font = OutlineFont::TrueType(t.clone()); + Glyph::Outline(OutlineGlyph { + id: glyph, + font, + char_code, + }) + } + FontType::Type0(t) => { + let font = OutlineFont::Type0(t.clone()); + Glyph::Outline(OutlineGlyph { + id: glyph, + font, + char_code, + }) + } + FontType::Type3(t) => { + let nesting_depth = ctx.nesting_depth() + 1; + let shape_glyph = Type3Glyph { + font: t.clone(), + glyph_id: glyph, + state: ctx.get().clone(), + parent_resources: resources.clone(), + cache: ctx.interpreter_cache.clone(), + xref: ctx.xref, + settings: ctx.settings.clone(), + nesting_depth, + char_code, + }; + + Glyph::Type3(Box::new(shape_glyph)) + } + }; + + (glyph, glyph_transform) + } + + pub(crate) fn code_advance(&self, code: u32) -> Vec2 { + match &self.1 { + FontType::Type1(t) => { + debug_assert!(code <= u8::MAX as u32); + + Vec2::new(t.glyph_width(code as u8).unwrap_or(0.0) as f64, 0.0) + } + FontType::TrueType(t) => { + debug_assert!(code <= u8::MAX as u32); + + Vec2::new(t.glyph_width(code as u8) as f64, 0.0) + } + FontType::Type0(t) => t.code_advance(code), + FontType::Type3(t) => { + debug_assert!(code <= u8::MAX as u32); + + Vec2::new(t.glyph_width(code as u8) as f64, 0.0) + } + } + } + + pub(crate) fn origin_displacement(&self, code: u32) -> Vec2 { + match &self.1 { + FontType::Type1(_) => Vec2::default(), + FontType::TrueType(_) => Vec2::default(), + FontType::Type0(t) => t.origin_displacement(code), + FontType::Type3(_) => Vec2::default(), + } + } + + pub(crate) fn read_code(&self, bytes: &[u8], offset: usize) -> (u32, usize) { + match &self.1 { + FontType::Type1(_) => (bytes[offset] as u32, 1), + FontType::TrueType(_) => (bytes[offset] as u32, 1), + FontType::Type0(t) => t.read_code(bytes, offset), + FontType::Type3(_) => (bytes[offset] as u32, 1), + } + } + + pub(crate) fn is_horizontal(&self) -> bool { + match &self.1 { + FontType::Type1(_) => true, + FontType::TrueType(_) => true, + FontType::Type0(t) => t.is_horizontal(), + FontType::Type3(_) => true, + } + } +} + +impl CacheKey for Font<'_> { + fn cache_key(&self) -> u128 { + self.0 + } +} + +#[derive(Clone, Debug)] +enum FontType<'a> { + Type1(Rc), + TrueType(Rc), + Type0(Rc), + Type3(Rc>), +} + +#[derive(Debug)] +enum Encoding { + Standard, + MacRoman, + WinAnsi, + MacExpert, + BuiltIn, +} + +impl Encoding { + fn map_code(&self, code: u8) -> Option<&'static str> { + if code == 0 { + return Some(".notdef"); + } + match self { + Self::Standard => standard::get(code), + Self::MacRoman => mac_roman::get(code).or_else(|| mac_os_roman::get(code)), + Self::WinAnsi => win_ansi::get(code), + Self::MacExpert => mac_expert::get(code), + Self::BuiltIn => None, + } + } +} + +/// The font stretch. +#[derive(Debug, Copy, Clone)] +pub enum FontStretch { + /// Normal. + Normal, + /// Ultra condensed. + UltraCondensed, + /// Extra condensed. + ExtraCondensed, + /// Condensed. + Condensed, + /// Semi condensed. + SemiCondensed, + /// Semi expanded. + SemiExpanded, + /// Expanded. + Expanded, + /// Extra expanded. + ExtraExpanded, + /// Ultra expanded. + UltraExpanded, +} + +impl FontStretch { + fn from_string(s: &str) -> Self { + match s { + "UltraCondensed" => Self::UltraCondensed, + "ExtraCondensed" => Self::ExtraCondensed, + "Condensed" => Self::Condensed, + "SemiCondensed" => Self::SemiCondensed, + "SemiExpanded" => Self::SemiExpanded, + "Expanded" => Self::Expanded, + "ExtraExpanded" => Self::ExtraExpanded, + "UltraExpanded" => Self::UltraExpanded, + _ => Self::Normal, + } + } +} + +bitflags! { + /// Bitflags describing various characteristics of fonts. + #[derive(Debug)] + pub(crate) struct FontFlags: u32 { + const FIXED_PITCH = 1 << 0; + const SERIF = 1 << 1; + const SYMBOLIC = 1 << 2; + const SCRIPT = 1 << 3; + const NON_SYMBOLIC = 1 << 5; + const ITALIC = 1 << 6; + const ALL_CAP = 1 << 16; + const SMALL_CAP = 1 << 17; + const FORCE_BOLD = 1 << 18; + } +} + +/// A query for a font. +pub enum FontQuery { + /// A query for one of the 14 PDF standard fonts. + Standard(StandardFont), + /// A query for a font that is not embedded in the PDF file. + /// + /// Note that this type of query is currently not supported, + /// but will be implemented in the future. + Fallback(FallbackFontQuery), +} + +/// A query for a font with specific properties. +#[derive(Debug, Clone)] +pub struct FallbackFontQuery { + /// The postscript name of the font. + pub post_script_name: Option, + /// The name of the font. + pub font_name: Option, + /// The family of the font. + pub font_family: Option, + /// The stretch of the font. + pub font_stretch: FontStretch, + /// The weight of the font. + pub font_weight: u32, + /// Whether the font is monospaced. + pub is_fixed_pitch: bool, + /// Whether the font is serif. + pub is_serif: bool, + /// Whether the font is italic. + pub is_italic: bool, + /// Whether the font is bold. + pub is_bold: bool, + /// Whether the font is small cap. + pub is_small_cap: bool, + /// The character collection (registry/ordering) if this is a CID font. + pub character_collection: Option, +} + +impl FallbackFontQuery { + pub(crate) fn new(dict: &Dict<'_>) -> Self { + let post_script_name = dict + .get::>(BASE_FONT) + .map(|n| strip_subset_prefix(n.as_str()).to_string()); + + let mut data = Self { + post_script_name, + ..Default::default() + }; + + if let Some(descriptor) = dict.get::>(FONT_DESC) { + data.font_name = dict + .get::>(FONT_NAME) + .map(|n| strip_subset_prefix(n.as_str()).to_string()); + data.font_family = descriptor + .get::>(FONT_FAMILY) + .map(|n| n.as_str().to_string()); + data.font_stretch = descriptor + .get::>(FONT_STRETCH) + .map(|n| FontStretch::from_string(n.as_str())) + .unwrap_or(FontStretch::Normal); + data.font_weight = descriptor.get::(FONT_WEIGHT).unwrap_or(400); + + if let Some(flags) = descriptor + .get::(FLAGS) + .map(FontFlags::from_bits_truncate) + { + data.is_serif = flags.contains(FontFlags::SERIF); + data.is_italic = flags.contains(FontFlags::ITALIC); + data.is_small_cap = flags.contains(FontFlags::SMALL_CAP); + } + } + + data.is_italic |= data + .post_script_name + .as_ref() + .is_some_and(|s| s.contains("Italic")); + + data.is_bold |= data + .post_script_name + .as_ref() + .is_some_and(|s| s.contains("Bold")); + + data + } + + /// Do a best-effort fallback to the 14 standard fonts based on the query. + pub fn pick_standard_font(&self) -> StandardFont { + if self.is_fixed_pitch { + match (self.is_bold, self.is_italic) { + (true, true) => StandardFont::CourierBoldOblique, + (true, false) => StandardFont::CourierBold, + (false, true) => StandardFont::CourierOblique, + (false, false) => StandardFont::Courier, + } + } else if !self.is_serif { + match (self.is_bold, self.is_italic) { + (true, true) => StandardFont::HelveticaBoldOblique, + (true, false) => StandardFont::HelveticaBold, + (false, true) => StandardFont::HelveticaOblique, + (false, false) => StandardFont::Helvetica, + } + } else { + match (self.is_bold, self.is_italic) { + (true, true) => StandardFont::TimesBoldItalic, + (true, false) => StandardFont::TimesBold, + (false, true) => StandardFont::TimesItalic, + (false, false) => StandardFont::TimesRoman, + } + } + } +} + +impl Default for FallbackFontQuery { + fn default() -> Self { + Self { + post_script_name: None, + font_name: None, + font_family: None, + font_stretch: FontStretch::Normal, + font_weight: 400, + is_fixed_pitch: false, + is_serif: false, + is_italic: false, + is_bold: false, + is_small_cap: false, + character_collection: None, + } + } +} + +/// Convert a glyph name to a Unicode character, if possible. +/// An incomplete implementation of the Adobe Glyph List Specification +/// +pub(crate) fn glyph_name_to_unicode(name: &str) -> Option { + if let Some(unicode_str) = glyph_names::get(name) { + return unicode_str.chars().next(); + } + + unicode_from_name(name).or_else(|| { + warn!("failed to map glyph name {} to unicode", name); + + None + }) +} + +pub(crate) fn unicode_from_name(name: &str) -> Option { + let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32); + + name.starts_with("uni") + .then(|| name.get(3..).and_then(convert)) + .or_else(|| { + name.starts_with("u") + .then(|| name.get(1..).and_then(convert)) + }) + .flatten() +} + +pub(crate) fn read_to_unicode(dict: &Dict<'_>, cmap_resolver: &CMapResolverFn) -> Option { + dict.get::>(TO_UNICODE) + .and_then(|s| s.decoded().ok()) + // See PDFJS-11915, where `Identity-H` is used for `ToUnicode`. I don't + // believe it's valid, but at least mupdf seems to be able to deal with it. + .or_else(|| { + dict.get::>(TO_UNICODE) + .and_then(|name| (cmap_resolver)(CMapName::from_bytes(name.as_ref()))) + .map(|d| Cow::Owned(d.to_vec())) + }) + .and_then(|data| { + let cmap_resolver = cmap_resolver.clone(); + CMap::parse(&data, move |name| (cmap_resolver)(name)) + }) +} + +// When mapping to glyphs, some fonts might only have a glyph for the "normalized" +// name. +pub(crate) fn normalized_glyph_name(mut name: &str) -> &str { + if name == "nbspace" { + name = "space"; + } + + if name == "sfthyphen" { + name = "hyphen"; + } + + name +} diff --git a/vendor/hayro-interpret/src/font/outline.rs b/vendor/hayro-interpret/src/font/outline.rs new file mode 100644 index 00000000..558d886c --- /dev/null +++ b/vendor/hayro-interpret/src/font/outline.rs @@ -0,0 +1,146 @@ +use crate::CacheKey; +use crate::font::cid::Type0Font; +use crate::font::true_type::TrueTypeFont; +use crate::font::type1::Type1Font; +use hayro_cmap::BfString; +use kurbo::BezPath; +use skrifa::GlyphId; +use skrifa::outline::OutlinePen; +use std::rc::Rc; + +/// Font data and metadata for downstream use. +#[derive(Clone)] +pub struct OutlineFontData { + /// Raw font bytes (TrueType/OpenType/CFF data). + pub data: crate::font::FontData, + /// Cache key for font deduplication. + pub cache_key: u128, + /// PostScript name (e.g., "TimesNewRomanPS-BoldMT"). + pub postscript_name: Option, + /// Font weight (100-900, 400=normal, 700=bold). + pub weight: Option, + /// Whether the font is italic/oblique. + pub is_italic: bool, + /// Whether the font is serif (vs sans-serif). + pub is_serif: bool, + /// Whether the font is monospace. + pub is_monospace: bool, +} + +pub(crate) struct OutlinePath(BezPath); + +impl OutlinePath { + pub(crate) fn new() -> Self { + Self(BezPath::new()) + } + + pub(crate) fn take(self) -> BezPath { + self.0 + } +} + +impl OutlinePen for OutlinePath { + #[inline] + fn move_to(&mut self, x: f32, y: f32) { + self.0.move_to((x, y)); + } + + #[inline] + fn line_to(&mut self, x: f32, y: f32) { + if !self.0.elements().is_empty() { + self.0.line_to((x, y)); + } + } + + #[inline] + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + if !self.0.elements().is_empty() { + self.0.quad_to((cx, cy), (x, y)); + } + } + + #[inline] + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + if !self.0.elements().is_empty() { + self.0.curve_to((cx0, cy0), (cx1, cy1), (x, y)); + } + } + + #[inline] + fn close(&mut self) { + if !self.0.elements().is_empty() { + self.0.close_path(); + } + } +} + +#[derive(Debug, Clone)] +pub(crate) enum OutlineFont { + Type1(Rc), + TrueType(Rc), + Type0(Rc), +} + +impl CacheKey for OutlineFont { + fn cache_key(&self) -> u128 { + match self { + Self::Type1(f) => f.cache_key(), + Self::TrueType(t) => t.cache_key(), + Self::Type0(t0) => t0.cache_key(), + } + } +} + +impl OutlineFont { + pub(crate) fn outline_glyph(&self, glyph: GlyphId, code: u32) -> BezPath { + match self { + Self::Type1(t) => t.outline_glyph(glyph), + Self::TrueType(t) => t.outline_glyph(glyph), + Self::Type0(t) => t.outline_glyph(glyph, code), + } + } + + pub(crate) fn char_code_to_unicode(&self, char_code: u32) -> Option { + match self { + Self::Type1(t) => t.char_code_to_unicode(char_code), + Self::TrueType(t) => t.char_code_to_unicode(char_code), + Self::Type0(t) => t.char_code_to_unicode(char_code), + } + } + + /// Get the advance width for a glyph by character code. + pub(crate) fn glyph_advance_width(&self, char_code: u32) -> Option { + match self { + Self::Type1(t) => t.glyph_width(char_code as u8), + Self::TrueType(t) => Some(t.glyph_width(char_code as u8)), + Self::Type0(t) => Some(t.code_advance(char_code).x as f32), + } + } + + /// Get raw font bytes and metadata. + /// + /// Returns None for Type1 fonts and non-embedded TrueType fonts. + pub(crate) fn font_data(&self) -> Option { + match self { + Self::Type1(_) => None, + Self::TrueType(t) => Some(OutlineFontData { + data: t.font_data()?, + cache_key: t.cache_key(), + postscript_name: t.postscript_name().map(|s| s.to_string()), + weight: t.weight(), + is_italic: t.is_italic(), + is_serif: t.is_serif(), + is_monospace: t.is_monospace(), + }), + Self::Type0(t) => Some(OutlineFontData { + data: t.font_data()?, + cache_key: t.cache_key(), + postscript_name: t.postscript_name().map(|s| s.to_string()), + weight: t.weight(), + is_italic: t.is_italic(), + is_serif: t.is_serif(), + is_monospace: t.is_monospace(), + }), + } + } +} diff --git a/vendor/hayro-interpret/src/font/standard_font.rs b/vendor/hayro-interpret/src/font/standard_font.rs new file mode 100644 index 00000000..c73c75dd --- /dev/null +++ b/vendor/hayro-interpret/src/font/standard_font.rs @@ -0,0 +1,694 @@ +use crate::FontResolverFn; +use crate::font::blob::{CffFontBlob, OpenTypeFontBlob}; +use crate::font::generated::{glyph_names, metrics, standard, symbol, zapf_dings}; +use crate::font::true_type::{Width, read_encoding, read_widths}; +use crate::font::{ + Encoding, FontData, FontQuery, glyph_name_to_unicode, normalized_glyph_name, stretch_glyph, + strip_subset_prefix, +}; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Name; +use hayro_syntax::object::dict::keys::{BASE_FONT, FONT_DESC, FONT_WEIGHT, ITALIC_ANGLE}; +use kurbo::{Affine, BezPath, Shape}; +use skrifa::GlyphId; +use skrifa::raw::TableProvider; +use std::cell::RefCell; +use std::collections::HashMap; + +/// The 14 standard fonts of PDF. +#[derive(Copy, Clone, Debug)] +pub enum StandardFont { + /// Helvetica. + Helvetica, + /// Helvetica Bold. + HelveticaBold, + /// Helvetica Oblique. + HelveticaOblique, + /// Helvetica Bold Oblique. + HelveticaBoldOblique, + /// Courier. + Courier, + /// Courier Bold. + CourierBold, + /// Courier Oblique. + CourierOblique, + /// Courier Bold Oblique. + CourierBoldOblique, + /// Times Roman. + TimesRoman, + /// Times Bold. + TimesBold, + /// Times Italic. + TimesItalic, + /// Times Bold Italic. + TimesBoldItalic, + /// Zapf Dingbats - a decorative symbol font. + ZapfDingBats, + /// Symbol - a mathematical symbol font. + Symbol, +} + +impl StandardFont { + pub(crate) fn code_to_name(&self, code: u8) -> Option<&'static str> { + match self { + Self::Symbol => symbol::get(code), + // Note that this font does not return postscript character names, + // but instead has a custom encoding. + Self::ZapfDingBats => zapf_dings::get(code), + _ => standard::get(code), + } + } + + pub(crate) fn get_width(&self, mut name: &str) -> Option { + // + if name == ".notdef" { + return Some(250.0); + } + + name = normalized_glyph_name(name); + + match self { + Self::Helvetica => metrics::HELVETICA.get(name).copied(), + Self::HelveticaBold => metrics::HELVETICA_BOLD.get(name).copied(), + Self::HelveticaOblique => metrics::HELVETICA_OBLIQUE.get(name).copied(), + Self::HelveticaBoldOblique => metrics::HELVETICA_BOLD_OBLIQUE.get(name).copied(), + Self::Courier => metrics::COURIER.get(name).copied(), + Self::CourierBold => metrics::COURIER_BOLD.get(name).copied(), + Self::CourierOblique => metrics::COURIER_OBLIQUE.get(name).copied(), + Self::CourierBoldOblique => metrics::COURIER_BOLD_OBLIQUE.get(name).copied(), + Self::TimesRoman => metrics::TIMES_ROMAN.get(name).copied(), + Self::TimesBold => metrics::TIMES_BOLD.get(name).copied(), + Self::TimesItalic => metrics::TIMES_ITALIC.get(name).copied(), + Self::TimesBoldItalic => metrics::TIMES_BOLD_ITALIC.get(name).copied(), + Self::ZapfDingBats => metrics::ZAPF_DING_BATS.get(name).copied(), + Self::Symbol => metrics::SYMBOL.get(name).copied(), + } + } + + pub(crate) fn as_str(&self) -> &'static str { + match self { + Self::Helvetica => "Helvetica", + Self::HelveticaBold => "Helvetica Bold", + Self::HelveticaOblique => "Helvetica Oblique", + Self::HelveticaBoldOblique => "Helvetica Bold Oblique", + Self::Courier => "Courier", + Self::CourierBold => "Courier Bold", + Self::CourierOblique => "Courier Oblique", + Self::CourierBoldOblique => "Courier Bold Oblique", + Self::TimesRoman => "Times Roman", + Self::TimesBold => "Times Bold", + Self::TimesItalic => "Times Italic", + Self::TimesBoldItalic => "Times Bold Italic", + Self::ZapfDingBats => "Zapf Dingbats", + Self::Symbol => "Symbol", + } + } + + /// Return the postscrit name of the font. + pub fn postscript_name(&self) -> &'static str { + match self { + Self::Helvetica => "Helvetica", + Self::HelveticaBold => "Helvetica-Bold", + Self::HelveticaOblique => "Helvetica-Oblique", + Self::HelveticaBoldOblique => "Helvetica-BoldOblique", + Self::Courier => "Courier", + Self::CourierBold => "Courier-Bold", + Self::CourierOblique => "Courier-Oblique", + Self::CourierBoldOblique => "Courier-BoldOblique", + Self::TimesRoman => "Times-Roman", + Self::TimesBold => "Times-Bold", + Self::TimesItalic => "Times-Italic", + Self::TimesBoldItalic => "Times-BoldItalic", + Self::ZapfDingBats => "ZapfDingbats", + Self::Symbol => "Symbol", + } + } + + pub(crate) fn is_bold(&self) -> bool { + matches!( + self, + Self::HelveticaBold + | Self::HelveticaBoldOblique + | Self::CourierBold + | Self::CourierBoldOblique + | Self::TimesBold + | Self::TimesBoldItalic + ) + } + + pub(crate) fn is_italic(&self) -> bool { + matches!( + self, + Self::HelveticaOblique + | Self::HelveticaBoldOblique + | Self::CourierOblique + | Self::CourierBoldOblique + | Self::TimesItalic + | Self::TimesBoldItalic + ) + } + + pub(crate) fn is_serif(&self) -> bool { + matches!( + self, + Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic + ) + } + + pub(crate) fn is_monospace(&self) -> bool { + matches!( + self, + Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique + ) + } + + /// Return suitable font data for the given standard font. + /// + /// Currently, this will return the corresponding Foxit font, which is a set of permissibly + /// licensed fonts that is also very light-weight. + /// + /// You can use the result of this method in your implementation of [`FontResolverFn`]. + /// + /// [`FontResolverFn`]: crate::FontResolverFn + #[cfg(feature = "embed-fonts")] + pub fn get_font_data(&self) -> (FontData, u32) { + use std::sync::Arc; + + let data = match self { + Self::Helvetica => &include_bytes!("../../assets/FoxitSans.pfb")[..], + Self::HelveticaBold => &include_bytes!("../../assets/FoxitSansBold.pfb")[..], + Self::HelveticaOblique => &include_bytes!("../../assets/FoxitSansItalic.pfb")[..], + Self::HelveticaBoldOblique => { + &include_bytes!("../../assets/FoxitSansBoldItalic.pfb")[..] + } + Self::Courier => &include_bytes!("../../assets/FoxitFixed.pfb")[..], + Self::CourierBold => &include_bytes!("../../assets/FoxitFixedBold.pfb")[..], + Self::CourierOblique => &include_bytes!("../../assets/FoxitFixedItalic.pfb")[..], + Self::CourierBoldOblique => { + &include_bytes!("../../assets/FoxitFixedBoldItalic.pfb")[..] + } + Self::TimesRoman => &include_bytes!("../../assets/FoxitSerif.pfb")[..], + Self::TimesBold => &include_bytes!("../../assets/FoxitSerifBold.pfb")[..], + Self::TimesItalic => &include_bytes!("../../assets/FoxitSerifItalic.pfb")[..], + Self::TimesBoldItalic => &include_bytes!("../../assets/FoxitSerifBoldItalic.pfb")[..], + Self::ZapfDingBats => &include_bytes!("../../assets/FoxitDingbats.pfb")[..], + Self::Symbol => { + include_bytes!("../../assets/FoxitSymbol.pfb") + } + }; + + (Arc::new(data), 0) + } +} + +enum StandardFontFamily { + Helvetica, + Courier, + Times, +} + +pub(crate) fn select_standard_font( + dict: &Dict<'_>, + descriptor: &Dict<'_>, +) -> Option<(StandardFont, bool)> { + let base_font = dict.get::>(BASE_FONT)?; + let name = strip_subset_prefix(base_font.as_str()); + + // First try whether it matches literally. + match name { + "Helvetica" => return Some((StandardFont::Helvetica, true)), + "Helvetica-Bold" => return Some((StandardFont::HelveticaBold, true)), + "Helvetica-Oblique" => return Some((StandardFont::HelveticaOblique, true)), + "Helvetica-BoldOblique" => return Some((StandardFont::HelveticaBoldOblique, true)), + "Courier" => return Some((StandardFont::Courier, true)), + "Courier-Bold" => return Some((StandardFont::CourierBold, true)), + "Courier-Oblique" => return Some((StandardFont::CourierOblique, true)), + "Courier-BoldOblique" => return Some((StandardFont::CourierBoldOblique, true)), + "Times-Roman" => return Some((StandardFont::TimesRoman, true)), + "Times-Bold" => return Some((StandardFont::TimesBold, true)), + "Times-Italic" => return Some((StandardFont::TimesItalic, true)), + "Times-BoldItalic" => return Some((StandardFont::TimesBoldItalic, true)), + "Symbol" => return Some((StandardFont::Symbol, true)), + "ZapfDingbats" => return Some((StandardFont::ZapfDingBats, true)), + _ => {} + } + + // Now, we bruteforce, trying to determine a suitable font based on the + // keywords that appear in the name. + let lower = name.to_ascii_lowercase(); + + let is_bold = descriptor.get::(FONT_WEIGHT).is_some_and(|w| w >= 700) + || lower.contains("bold") + || lower.contains("demi"); + let is_italic = descriptor + .get::(ITALIC_ANGLE) + .is_some_and(|a| a != 0.0) + || lower.contains("italic") + || lower.contains("oblique"); + + let (family, exact) = if lower.contains("helvetica") { + (Some(StandardFontFamily::Helvetica), true) + } else if lower.contains("arial") || lower.contains("sans") { + (Some(StandardFontFamily::Helvetica), false) + } else if lower.contains("courier") { + (Some(StandardFontFamily::Courier), true) + } else if lower.contains("mono") { + (Some(StandardFontFamily::Courier), false) + } else if lower.contains("times") { + (Some(StandardFontFamily::Times), true) + } else if lower.contains("serif") { + (Some(StandardFontFamily::Times), false) + } else if lower.contains("zapfdingbats") || lower.contains("dingbats") { + return Some((StandardFont::ZapfDingBats, false)); + } else { + (None, false) + }; + + let font = match (family?, is_bold, is_italic) { + (StandardFontFamily::Helvetica, false, false) => StandardFont::Helvetica, + (StandardFontFamily::Helvetica, true, false) => StandardFont::HelveticaBold, + (StandardFontFamily::Helvetica, false, true) => StandardFont::HelveticaOblique, + (StandardFontFamily::Helvetica, true, true) => StandardFont::HelveticaBoldOblique, + (StandardFontFamily::Courier, false, false) => StandardFont::Courier, + (StandardFontFamily::Courier, true, false) => StandardFont::CourierBold, + (StandardFontFamily::Courier, false, true) => StandardFont::CourierOblique, + (StandardFontFamily::Courier, true, true) => StandardFont::CourierBoldOblique, + (StandardFontFamily::Times, false, false) => StandardFont::TimesRoman, + (StandardFontFamily::Times, true, false) => StandardFont::TimesBold, + (StandardFontFamily::Times, false, true) => StandardFont::TimesItalic, + (StandardFontFamily::Times, true, true) => StandardFont::TimesBoldItalic, + }; + + Some((font, exact)) +} + +#[derive(Debug)] +pub(crate) enum StandardFontBlob { + Cff(CffFontBlob), + Otf(OpenTypeFontBlob, HashMap), +} + +impl StandardFontBlob { + pub(crate) fn from_data(data: FontData, index: u32) -> Option { + if let Some(blob) = CffFontBlob::new(data.clone()) { + Some(Self::new_cff(blob)) + } else { + OpenTypeFontBlob::new(data, index).map(Self::new_otf) + } + } + + pub(crate) fn new_cff(blob: CffFontBlob) -> Self { + Self::Cff(blob) + } + + pub(crate) fn new_otf(blob: OpenTypeFontBlob) -> Self { + let glyph_names = blob.glyph_names(); + Self::Otf(blob, glyph_names) + } +} + +impl StandardFontBlob { + pub(crate) fn name_to_glyph(&self, name: &str) -> Option { + match self { + Self::Cff(blob) => blob.glyph_index_by_name(name), + Self::Otf(_, glyph_names) => glyph_names.get(name).copied(), + } + } + + pub(crate) fn unicode_to_glyph(&self, code: u32) -> Option { + match self { + Self::Cff(_) => None, + Self::Otf(blob, _) => blob + .font_ref() + .cmap() + .ok() + .and_then(|c| c.map_codepoint(code)), + } + } + + pub(crate) fn advance_width(&self, glyph: GlyphId) -> Option { + match self { + Self::Cff(_) => None, + Self::Otf(blob, _) => blob.glyph_metrics().advance_width(glyph), + } + } + + pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + // Standard fonts have empty outlines for these, but in Liberation Sans + // they are a .notdef rectangle. + if glyph == GlyphId::NOTDEF { + return BezPath::new(); + } + + match self { + Self::Cff(blob) => blob.outline_glyph(glyph), + Self::Otf(blob, _) => blob.outline_glyph(glyph), + } + } +} + +#[derive(Debug)] +pub(crate) struct StandardKind { + base_font: StandardFont, + base_font_blob: StandardFontBlob, + encoding: Encoding, + widths: Vec, + missing_width: f32, + fallback: bool, + glyph_to_code: RefCell>, + encodings: HashMap, +} + +impl StandardKind { + pub(crate) fn new(dict: &Dict<'_>, resolver: &FontResolverFn) -> Option { + let descriptor = dict.get::>(FONT_DESC).unwrap_or_default(); + let (font, exact) = select_standard_font(dict, &descriptor)?; + Self::new_with_standard(dict, font, !exact, resolver) + } + + pub(crate) fn new_with_standard( + dict: &Dict<'_>, + base_font: StandardFont, + fallback: bool, + resolver: &FontResolverFn, + ) -> Option { + let descriptor = dict.get::>(FONT_DESC).unwrap_or_default(); + let (widths, missing_width) = read_widths(dict, &descriptor)?; + + let (mut encoding, encoding_map) = read_encoding(dict); + + // See PDFJS-16464: Ignore encodings for non-embedded Type1 symbol fonts. + if matches!(base_font, StandardFont::Symbol | StandardFont::ZapfDingBats) { + encoding = Encoding::BuiltIn; + } + + let (blob, index) = resolver(&FontQuery::Standard(base_font))?; + let base_font_blob = StandardFontBlob::from_data(blob, index)?; + + Some(Self { + base_font, + base_font_blob, + widths, + missing_width, + encodings: encoding_map, + glyph_to_code: RefCell::new(HashMap::new()), + fallback, + encoding, + }) + } + + fn code_to_ps_name(&self, code: u8) -> Option<&str> { + let bf = self.base_font; + + self.encodings + .get(&code) + .map(String::as_str) + .or_else(|| match self.encoding { + Encoding::BuiltIn => bf.code_to_name(code), + _ => self.encoding.map_code(code), + }) + } + + pub(crate) fn map_code(&self, code: u8) -> GlyphId { + let result = self + .code_to_ps_name(code) + .and_then(|c| { + self.base_font_blob + .name_to_glyph(c) + .or_else(|| { + // If the font doesn't have a POST table, try to map via unicode instead. + glyph_names::get(c).and_then(|c| { + self.base_font_blob + .unicode_to_glyph(c.chars().nth(0).unwrap() as u32) + }) + }) + // PdfCraft patch: an accented letter the stand-in lacks is built from its + // parts (see `ACCENTED_LETTERS`). + .or_else(|| self.composite_glyph(code, c)) + }) + .unwrap_or(GlyphId::NOTDEF); + self.glyph_to_code.borrow_mut().insert(result, code); + + result + } + + /// PdfCraft patch: the glyph id standing for the letter `name` built from its base letter and + /// accent, when the stand-in has both. Ids from [`COMPOSITE_GLYPH_BASE`] up are one per + /// character code, so the outline is found again from the code alone. + fn composite_glyph(&self, code: u8, name: &str) -> Option { + let (base, accent, _) = accented_parts(name)?; + self.base_font_blob.name_to_glyph(base)?; + self.base_font_blob.name_to_glyph(accent)?; + Some(GlyphId::new(COMPOSITE_GLYPH_BASE + u32::from(code))) + } + + /// PdfCraft patch: the outline of a glyph from [`Self::composite_glyph`] โ€” the base letter, + /// then the accent centred on it. An accent above a letter taller than `x` is raised by the + /// difference (the stand-in's accents sit on lowercase letters) and, in an italic font, + /// moved along the slant; an ogonek hangs from the letter's right edge. + fn composite_outline(&self, glyph: GlyphId) -> Option { + let code = u8::try_from(glyph.to_u32().checked_sub(COMPOSITE_GLYPH_BASE)?).ok()?; + let (base, accent, place) = accented_parts(self.code_to_ps_name(code)?)?; + let outline = |name: &str| { + self.base_font_blob + .name_to_glyph(name) + .map(|g| self.base_font_blob.outline_glyph(g)) + }; + let mut path = outline(base)?; + let accent = outline(accent)?; + if path.elements().is_empty() || accent.elements().is_empty() { + return Some(path); + } + let (b, a) = (path.bounding_box(), accent.bounding_box()); + let (dx, dy) = match place { + Place::Below => (b.center().x - a.center().x, 0.0), + Place::Ogonek => (b.x1 - a.x1, 0.0), + Place::Above => { + let x_top = outline("x") + .filter(|x| !x.elements().is_empty()) + .map_or(b.y1, |x| x.bounding_box().y1); + // Overshoot of round lowercase letters (o, g, s) over x is not a taller letter. + let raise = if b.y1 > x_top * 1.1 { + b.y1 - x_top + } else { + 0.0 + }; + let slant = if self.base_font.is_italic() { + raise * ITALIC_ACCENT_SLANT + } else { + 0.0 + }; + (b.center().x - a.center().x + slant, raise) + } + }; + if !(dx.is_finite() && dy.is_finite()) { + return Some(path); + } + path.extend(Affine::translate((dx, dy)) * accent); + Some(path) + } + + pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + let composite = self.composite_outline(glyph); + let is_composite = composite.is_some(); + let path = composite.unwrap_or_else(|| self.base_font_blob.outline_glyph(glyph)); + + // If the font is not embedded, we might need to stretch it so that + // it matches the metrics of the actual underlying font blob. + + // PdfCraft patch: a composite id isn't a glyph of the blob, so its width comes from the + // standard metrics of its name. + let blob_width = if is_composite { + None + } else { + self.base_font_blob.advance_width(glyph) + }; + if let Some(code) = self.glyph_to_code.borrow().get(&glyph).copied() + && let Some(actual_width) = blob_width.or_else(|| { + self.code_to_ps_name(code) + .and_then(|name| self.base_font.get_width(name)) + }) + { + // From my experiments: Most PDF viewers, if they detect a font is a + // standard font, they completely ignore the widths array, even if + // different widths are indicated there. So only if it's an unknown + // font do we check the widths array. Otherwise, we always use the + // base font metrics. + let should_width = if self.fallback { + if let Some(Width::Value(w)) = self.widths.get(code as usize).copied() { + w + } else { + return path; + } + } else if let Some(w) = self + .code_to_ps_name(code) + .and_then(|name| self.base_font.get_width(name)) + { + w + } else { + return path; + }; + + return stretch_glyph(path, should_width, actual_width); + } + + path + } + + pub(crate) fn glyph_width(&self, code: u8) -> Option { + match self.widths.get(code as usize).copied() { + Some(Width::Value(w)) => Some(w), + Some(Width::Missing) => Some(self.missing_width), + None => self + .code_to_ps_name(code) + .and_then(|c| self.base_font.get_width(c)), + } + } + + pub(crate) fn char_code_to_unicode(&self, code: u8) -> Option { + self.code_to_ps_name(code).and_then(glyph_name_to_unicode) + } + + pub(crate) fn is_italic(&self) -> bool { + self.base_font.is_italic() + } + + pub(crate) fn is_bold(&self) -> bool { + self.base_font.is_bold() + } + + pub(crate) fn is_serif(&self) -> bool { + self.base_font.is_serif() + } + + pub(crate) fn is_monospace(&self) -> bool { + self.base_font.is_monospace() + } +} + +/// PdfCraft patch: the first glyph id of the letters [`StandardKind`] builds from parts, far +/// above the glyph count of any font (one id per character code from here). +const COMPOSITE_GLYPH_BASE: u32 = 0x0100_0000; + +/// PdfCraft patch: how far an accent raised over a capital moves right per unit of height in +/// an italic stand-in (tan 15ยฐ; the standard italics lean 12ยฐ to 16ยฐ). +const ITALIC_ACCENT_SLANT: f64 = 0.27; + +/// Where an accent goes on its base letter. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Place { + Above, + Below, + Ogonek, +} + +/// PdfCraft patch: the stand-ins for the standard fonts (PDFium's Foxit fonts) carry only the +/// glyphs of the Standard, WinAnsi and Mac Roman encodings, so a font that isn't embedded and +/// names other Latin letters in its /Differences (`/Gbreve`, `/Idotaccent`, `/Scedilla`โ€ฆ, as +/// iText 2 writes Turkish text with Times New Roman left unembedded) drew nothing for them. Each of +/// these Latin Extended-A letters is a base letter the stand-ins have plus one of their accents, +/// as Type 1 `seac` composites and the composites of the standard-14 AFM files are built. +/// Letters whose accent isn't a plain one (ฤ ฤพ ลฅ and the comma accents) aren't here. +const ACCENTED_LETTERS: &[(&str, &str, &str, Place)] = &[ + ("Amacron", "A", "macron", Place::Above), + ("amacron", "a", "macron", Place::Above), + ("Abreve", "A", "breve", Place::Above), + ("abreve", "a", "breve", Place::Above), + ("Aogonek", "A", "ogonek", Place::Ogonek), + ("aogonek", "a", "ogonek", Place::Ogonek), + ("Cacute", "C", "acute", Place::Above), + ("cacute", "c", "acute", Place::Above), + ("Ccircumflex", "C", "circumflex", Place::Above), + ("ccircumflex", "c", "circumflex", Place::Above), + ("Cdotaccent", "C", "dotaccent", Place::Above), + ("cdotaccent", "c", "dotaccent", Place::Above), + ("Ccaron", "C", "caron", Place::Above), + ("ccaron", "c", "caron", Place::Above), + ("Dcaron", "D", "caron", Place::Above), + ("Emacron", "E", "macron", Place::Above), + ("emacron", "e", "macron", Place::Above), + ("Ebreve", "E", "breve", Place::Above), + ("ebreve", "e", "breve", Place::Above), + ("Edotaccent", "E", "dotaccent", Place::Above), + ("edotaccent", "e", "dotaccent", Place::Above), + ("Eogonek", "E", "ogonek", Place::Ogonek), + ("eogonek", "e", "ogonek", Place::Ogonek), + ("Ecaron", "E", "caron", Place::Above), + ("ecaron", "e", "caron", Place::Above), + ("Gcircumflex", "G", "circumflex", Place::Above), + ("gcircumflex", "g", "circumflex", Place::Above), + ("Gbreve", "G", "breve", Place::Above), + ("gbreve", "g", "breve", Place::Above), + ("Gdotaccent", "G", "dotaccent", Place::Above), + ("gdotaccent", "g", "dotaccent", Place::Above), + ("Hcircumflex", "H", "circumflex", Place::Above), + ("hcircumflex", "h", "circumflex", Place::Above), + ("Itilde", "I", "tilde", Place::Above), + ("itilde", "dotlessi", "tilde", Place::Above), + ("Imacron", "I", "macron", Place::Above), + ("imacron", "dotlessi", "macron", Place::Above), + ("Ibreve", "I", "breve", Place::Above), + ("ibreve", "dotlessi", "breve", Place::Above), + ("Iogonek", "I", "ogonek", Place::Ogonek), + ("iogonek", "i", "ogonek", Place::Ogonek), + ("Idotaccent", "I", "dotaccent", Place::Above), + ("Jcircumflex", "J", "circumflex", Place::Above), + ("Lacute", "L", "acute", Place::Above), + ("lacute", "l", "acute", Place::Above), + ("Nacute", "N", "acute", Place::Above), + ("nacute", "n", "acute", Place::Above), + ("Ncaron", "N", "caron", Place::Above), + ("ncaron", "n", "caron", Place::Above), + ("Omacron", "O", "macron", Place::Above), + ("omacron", "o", "macron", Place::Above), + ("Obreve", "O", "breve", Place::Above), + ("obreve", "o", "breve", Place::Above), + ("Ohungarumlaut", "O", "hungarumlaut", Place::Above), + ("ohungarumlaut", "o", "hungarumlaut", Place::Above), + ("Racute", "R", "acute", Place::Above), + ("racute", "r", "acute", Place::Above), + ("Rcaron", "R", "caron", Place::Above), + ("rcaron", "r", "caron", Place::Above), + ("Sacute", "S", "acute", Place::Above), + ("sacute", "s", "acute", Place::Above), + ("Scircumflex", "S", "circumflex", Place::Above), + ("scircumflex", "s", "circumflex", Place::Above), + ("Scedilla", "S", "cedilla", Place::Below), + ("scedilla", "s", "cedilla", Place::Below), + ("Tcedilla", "T", "cedilla", Place::Below), + ("tcedilla", "t", "cedilla", Place::Below), + ("Tcaron", "T", "caron", Place::Above), + ("Utilde", "U", "tilde", Place::Above), + ("utilde", "u", "tilde", Place::Above), + ("Umacron", "U", "macron", Place::Above), + ("umacron", "u", "macron", Place::Above), + ("Ubreve", "U", "breve", Place::Above), + ("ubreve", "u", "breve", Place::Above), + ("Uring", "U", "ring", Place::Above), + ("uring", "u", "ring", Place::Above), + ("Uhungarumlaut", "U", "hungarumlaut", Place::Above), + ("uhungarumlaut", "u", "hungarumlaut", Place::Above), + ("Uogonek", "U", "ogonek", Place::Ogonek), + ("uogonek", "u", "ogonek", Place::Ogonek), + ("Wcircumflex", "W", "circumflex", Place::Above), + ("wcircumflex", "w", "circumflex", Place::Above), + ("Ycircumflex", "Y", "circumflex", Place::Above), + ("ycircumflex", "y", "circumflex", Place::Above), + ("Zacute", "Z", "acute", Place::Above), + ("zacute", "z", "acute", Place::Above), + ("Zdotaccent", "Z", "dotaccent", Place::Above), + ("zdotaccent", "z", "dotaccent", Place::Above), +]; + +/// PdfCraft patch: the base letter, accent and placement of an accented letter in +/// [`ACCENTED_LETTERS`], by glyph name (`uni011E` and `u011E` forms too). +fn accented_parts(name: &str) -> Option<(&'static str, &'static str, Place)> { + let name = normalized_glyph_name(name); + let by_name = |n: &str| { + ACCENTED_LETTERS + .iter() + .find(|(letter, ..)| *letter == n) + .map(|&(_, base, accent, place)| (base, accent, place)) + }; + by_name(name).or_else(|| by_name(glyph_names::get_reverse(glyph_name_to_unicode(name)?)?)) +} diff --git a/vendor/hayro-interpret/src/font/true_type.rs b/vendor/hayro-interpret/src/font/true_type.rs new file mode 100644 index 00000000..40f33590 --- /dev/null +++ b/vendor/hayro-interpret/src/font/true_type.rs @@ -0,0 +1,496 @@ +use crate::font::blob::{CffFontBlob, OpenTypeFontBlob}; +use crate::font::generated::{glyph_names, mac_os_roman, mac_roman, standard, win_ansi}; +use crate::font::standard_font::StandardKind; +use crate::font::{ + Encoding, FallbackFontQuery, FontFlags, glyph_name_to_unicode, read_to_unicode, + strip_subset_prefix, unicode_from_name, +}; +use crate::util::OptionLog; +use crate::{CMapResolverFn, CacheKey, FontResolverFn}; +use hayro_cmap::{BfString, CMap}; +use hayro_syntax::object::Array; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Name; +use hayro_syntax::object::Object; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::*; +use kurbo::BezPath; +use skrifa::attribute::Style; +use skrifa::raw::TableProvider; +use skrifa::raw::tables::cmap::PlatformId; +use skrifa::{GlyphId, MetadataProvider}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::Arc; + +#[derive(Debug)] +pub(crate) struct TrueTypeFont { + cache_key: u128, + kind: Kind, + to_unicode: Option, +} + +#[derive(Debug)] +enum Kind { + Embedded(EmbeddedKind), + Standard(StandardKind), +} + +impl TrueTypeFont { + pub(crate) fn new( + dict: &Dict<'_>, + font_resolver: &FontResolverFn, + cmap_resolver: &CMapResolverFn, + ) -> Option { + let cache_key = dict.cache_key(); + let to_unicode = read_to_unicode(dict, cmap_resolver); + + if let Some(embedded) = EmbeddedKind::new(dict) { + return Some(Self { + cache_key, + kind: Kind::Embedded(embedded), + to_unicode, + }); + } + + let fallback = || { + let fallback_query = FallbackFontQuery::new(dict); + let standard_font = fallback_query.pick_standard_font(); + + warn!( + "unable to load TrueType font {}, falling back to {}", + fallback_query + .post_script_name + .unwrap_or("(no name)".to_string()), + standard_font.as_str() + ); + + Some(Self { + cache_key, + kind: Kind::Standard(StandardKind::new_with_standard( + dict, + standard_font, + true, + font_resolver, + )?), + to_unicode: to_unicode.clone(), + }) + }; + + if let Some(standard) = StandardKind::new(dict, font_resolver) { + Some(Self { + cache_key, + kind: Kind::Standard(standard), + to_unicode, + }) + } else { + fallback() + } + } + + pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + match &self.kind { + Kind::Embedded(e) => e.outline_glyph(glyph), + Kind::Standard(s) => s.outline_glyph(glyph), + } + } + + pub(crate) fn font_data(&self) -> Option { + match &self.kind { + Kind::Embedded(e) => Some(e.base_font.font_data()), + Kind::Standard(_) => None, + } + } + + pub(crate) fn postscript_name(&self) -> Option<&str> { + match &self.kind { + Kind::Embedded(e) => e.postscript_name.as_deref(), + Kind::Standard(_) => None, + } + } + + pub(crate) fn weight(&self) -> Option { + match &self.kind { + Kind::Embedded(e) => { + let weight = e.base_font.font_ref().attributes().weight.value().round() as u32; + if weight > 0 { Some(weight) } else { None } + } + Kind::Standard(s) => Some(if s.is_bold() { 700 } else { 400 }), + } + } + + pub(crate) fn is_italic(&self) -> bool { + match &self.kind { + Kind::Embedded(e) => { + // Check PDF font flags first + if let Some(flags) = &e.font_flags + && flags.contains(FontFlags::ITALIC) + { + return true; + } + // Check skrifa font attributes + e.base_font.font_ref().attributes().style != Style::Normal + } + Kind::Standard(s) => s.is_italic(), + } + } + + pub(crate) fn is_serif(&self) -> bool { + match &self.kind { + Kind::Embedded(e) => e + .font_flags + .as_ref() + .is_some_and(|f| f.contains(FontFlags::SERIF)), + Kind::Standard(s) => s.is_serif(), + } + } + + pub(crate) fn is_monospace(&self) -> bool { + match &self.kind { + Kind::Embedded(e) => { + // Check PDF font flags first + if let Some(flags) = &e.font_flags + && flags.contains(FontFlags::FIXED_PITCH) + { + return true; + } + // Check skrifa font metrics + e.base_font + .font_ref() + .metrics( + skrifa::instance::Size::unscaled(), + skrifa::instance::LocationRef::default(), + ) + .is_monospace + } + Kind::Standard(s) => s.is_monospace(), + } + } + + pub(crate) fn map_code(&self, code: u8) -> GlyphId { + match &self.kind { + Kind::Embedded(e) => e.map_code(code), + Kind::Standard(s) => s.map_code(code), + } + } + + pub(crate) fn glyph_width(&self, code: u8) -> f32 { + match &self.kind { + Kind::Embedded(e) => e.glyph_width(code), + Kind::Standard(s) => s.glyph_width(code).unwrap_or(0.0), + } + } + + pub(crate) fn char_code_to_unicode(&self, code: u32) -> Option { + if let Some(to_unicode) = &self.to_unicode + && let Some(c) = to_unicode.lookup_bf_string(code) + { + return Some(c); + } + + match &self.kind { + Kind::Embedded(e) => e + .code_to_unicode_name(code as u8) + .and_then(glyph_name_to_unicode) + .map(BfString::Char), + Kind::Standard(s) => s.char_code_to_unicode(code as u8).map(BfString::Char), + } + + // TODO: The test PDFs below fail (but mutool can render them correctly). + // There is likely some other strategy that requires processing the font tables + // hayro-tests/pdfs/custom/font_truetype_7.pdf + // hayro-tests/pdfs/custom/font_truetype_6.pdf + } +} + +#[derive(Debug)] +struct EmbeddedKind { + base_font: OpenTypeFontBlob, + widths: Vec, + missing_width: f32, + font_flags: Option, + glyph_names: HashMap, + encoding: Encoding, + // Only used for PDFs that mistakenly embed a + // CFF font. + cff_blob: Option, + differences: HashMap, + cached_mappings: RefCell>, + /// PostScript name from the PDF. + postscript_name: Option, +} + +impl EmbeddedKind { + fn new(dict: &Dict<'_>) -> Option { + let descriptor = dict.get::>(FONT_DESC).unwrap_or_default(); + + let font_flags = descriptor.get::(FLAGS).and_then(FontFlags::from_bits); + + let (widths, missing_width) = read_widths(dict, &descriptor)?; + let (encoding, differences) = read_encoding(dict); + let base_font = descriptor + .get::>(FONT_FILE2) + .and_then(|s| s.decoded().ok()) + .and_then(|d| OpenTypeFontBlob::new(Arc::new(d.to_vec()), 0))?; + + let glyph_names = base_font.glyph_names(); + + let cff_font_blob = base_font + .font_ref() + .cff() + .ok() + .and_then(|cff| CffFontBlob::new(Arc::new(cff.offset_data().as_ref().to_vec()))); + + let postscript_name = dict + .get::>(BASE_FONT) + .map(|n| strip_subset_prefix(n.as_str()).to_string()); + + Some(Self { + base_font, + differences, + cff_blob: cff_font_blob, + widths, + missing_width, + glyph_names, + font_flags, + encoding, + cached_mappings: RefCell::new(HashMap::new()), + postscript_name, + }) + } + + fn is_non_symbolic(&self) -> bool { + self.font_flags + .as_ref() + .map(|f| f.contains(FontFlags::NON_SYMBOLIC)) + .unwrap_or(false) + } + + fn code_to_name(&self, code: u8) -> Option<&str> { + self.differences + .get(&code) + .map(|s| s.as_str()) + .or_else(|| self.encoding.map_code(code)) + // See PDFJS-6410 - PDF has no base encoding, so let's fallback to + // standard here. + .or_else(|| standard::get(code)) + } + + /// PdfCraft patch: the glyph name text extraction reads a code as. Without a base encoding + /// a TrueType font's codes are read as WinAnsi, matching pdftotext's output, not as + /// Standard: OCR engines write the text layer of a scan with WinAnsi codes and no + /// `/Encoding`, and Standard has no name for รผ, รถ or รŸ and gives รฉ (0xE9) as ร˜. Glyph + /// selection (`code_to_name`) is unchanged. + fn code_to_unicode_name(&self, code: u8) -> Option<&str> { + self.differences + .get(&code) + .map(|s| s.as_str()) + .or_else(|| self.encoding.map_code(code)) + .or_else(|| win_ansi::get(code)) + } + + fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + self.base_font.outline_glyph(glyph) + } + + fn map_code(&self, code: u8) -> GlyphId { + if let Some(glyph) = self.cached_mappings.borrow().get(&code) { + return *glyph; + } + + if let Some(blob) = self.cff_blob.as_ref() { + return self + .code_to_name(code) + .and_then(|name| blob.glyph_index_by_name(name)) + .unwrap_or(GlyphId::NOTDEF); + } + + let mut glyph = None; + + if self.is_non_symbolic() { + let Some(lookup) = self.code_to_name(code) else { + return GlyphId::NOTDEF; + }; + + if let Ok(cmap) = self.base_font.font_ref().cmap() { + for record in cmap.encoding_records() { + if record.platform_id() == PlatformId::Windows + && record.encoding_id() == 1 + && let Ok(subtable) = record.subtable(cmap.offset_data()) + { + glyph = glyph.or_else(|| { + glyph_names::get(lookup) + .map(|n| n.to_string()) + .or_else(|| unicode_from_name(lookup).map(|n| n.to_string())) + .and_then(|n| n.chars().next()) + .and_then(|c| subtable.map_codepoint(c)) + .filter(|g| *g != GlyphId::NOTDEF) + }); + } + } + + for record in cmap.encoding_records() { + if record.platform_id() == PlatformId::Macintosh + && record.encoding_id() == 0 + && let Ok(subtable) = record.subtable(cmap.offset_data()) + { + glyph = glyph.or_else(|| { + mac_os_roman::get_inverse(lookup) + .or_else(|| mac_roman::get_inverse(lookup)) + .and_then(|c| subtable.map_codepoint(c)) + .filter(|g| *g != GlyphId::NOTDEF) + }); + } + } + } + + if glyph.is_none() { + if let Some(gid) = self.glyph_names.get(lookup) { + glyph = Some(*gid); + } else if let Some(gid) = glyph_num_string(lookup) { + glyph = Some(GlyphId::new(gid)); + } + } + } else if let Ok(cmap) = self.base_font.font_ref().cmap() { + for record in cmap.encoding_records() { + if record.platform_id() == PlatformId::Windows + && matches!(record.encoding_id(), 0 | 1) + { + if let Ok(subtable) = record.subtable(cmap.offset_data()) { + for offset in [0x0000_u32, 0xF000, 0xF100, 0xF200] { + glyph = glyph + .or_else(|| subtable.map_codepoint(code as u32 + offset)) + .filter(|g| *g != GlyphId::NOTDEF); + } + } + } else if matches!( + record.platform_id(), + PlatformId::Macintosh | PlatformId::Unicode + ) && record.encoding_id() == 0 + && let Ok(subtable) = record.subtable(cmap.offset_data()) + { + glyph = glyph + .or_else(|| subtable.map_codepoint(code)) + .filter(|g| *g != GlyphId::NOTDEF); + } + } + } + + let glyph = glyph.unwrap_or(GlyphId::NOTDEF); + self.cached_mappings.borrow_mut().insert(code, glyph); + + glyph + } + + fn glyph_width(&self, code: u8) -> f32 { + match self.widths.get(code as usize).copied() { + Some(Width::Value(w)) => w, + Some(Width::Missing) => self.missing_width, + _ => self + .base_font + .glyph_metrics() + .advance_width(self.map_code(code)) + .warn_none(&format!("failed to find advance width for code {code}")) + .unwrap_or(0.0), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum Width { + Value(f32), + Missing, +} + +pub(crate) fn read_widths(dict: &Dict<'_>, descriptor: &Dict<'_>) -> Option<(Vec, f32)> { + let mut widths = Vec::new(); + + let first_char = dict.get::(FIRST_CHAR); + let last_char = dict.get::(LAST_CHAR); + let widths_arr = dict.get::>(WIDTHS); + let missing_width = descriptor.get::(MISSING_WIDTH).unwrap_or(0.0); + + if let (Some(fc), Some(lc), Some(w)) = (first_char, last_char, widths_arr) { + // PdfCraft patch: simple-font codes are single bytes, so only codes up to 255 get an + // entry (one per code below /FirstChar was pushed: 4294967295 asked for 34 GB). An + // entry that isn't a number leaves its code missing instead of ending the array, and a + // /LastChar below /FirstChar means no widths rather than no font. + widths = vec![Width::Missing; u8::MAX as usize + 1]; + for (code, entry) in (fc..=lc.min(u8::MAX as usize)).zip(w.iter::>()) { + if let (Some(slot), Object::Number(n)) = (widths.get_mut(code), entry) { + *slot = Width::Value(n.as_f32()); + } + } + } + + Some((widths, missing_width)) +} + +fn glyph_num_string(s: &str) -> Option { + if !s.starts_with('g') || s.len() < 2 { + return None; + } + + s[1..].parse::().ok() +} + +impl CacheKey for TrueTypeFont { + fn cache_key(&self) -> u128 { + self.cache_key + } +} + +pub(crate) fn read_encoding(dict: &Dict<'_>) -> (Encoding, HashMap) { + fn get_encoding_base(dict: &Dict<'_>, name: Name<'_>) -> Encoding { + match dict.get::>(name.as_ref()) { + Some(n) => match n.deref() { + WIN_ANSI_ENCODING => Encoding::WinAnsi, + MAC_ROMAN_ENCODING => Encoding::MacRoman, + MAC_EXPERT_ENCODING => Encoding::MacExpert, + STANDARD_ENCODING => Encoding::Standard, + _ => { + warn!("Unknown font encoding {}", name.as_str()); + + Encoding::BuiltIn + } + }, + None => Encoding::BuiltIn, + } + } + + let mut map = HashMap::new(); + + if let Some(encoding_dict) = dict.get::>(ENCODING) { + if let Some(differences) = encoding_dict.get::>(DIFFERENCES) { + let entries = differences.iter::>(); + + let mut code = 0_i64; + + for obj in entries { + match obj { + Object::Number(num) => { + code = num.as_i64(); + } + Object::Name(name) => { + if let Ok(code) = code.try_into() { + map.insert(code, name.as_str().to_string()); + } + code += 1; + } + _ => {} + } + } + } + + ( + get_encoding_base(&encoding_dict, Name::new_unescaped(BASE_ENCODING)), + map, + ) + } else { + ( + get_encoding_base(dict, Name::new_unescaped(ENCODING)), + HashMap::new(), + ) + } +} diff --git a/vendor/hayro-interpret/src/font/type1.rs b/vendor/hayro-interpret/src/font/type1.rs new file mode 100644 index 00000000..32b5e3dd --- /dev/null +++ b/vendor/hayro-interpret/src/font/type1.rs @@ -0,0 +1,341 @@ +use crate::font::blob::{CffFontBlob, Type1FontBlob}; +use crate::font::standard_font::{StandardFont, StandardKind, select_standard_font}; +use crate::font::true_type::{Width, read_encoding, read_widths}; +use crate::font::{ + Encoding, FallbackFontQuery, glyph_name_to_unicode, normalized_glyph_name, read_to_unicode, +}; +use crate::{CMapResolverFn, CacheKey, FontResolverFn}; +use hayro_cmap::{BfString, CMap}; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::{FONT_DESC, FONT_FILE, FONT_FILE3}; +use kurbo::BezPath; +use skrifa::GlyphId; +use std::collections::HashMap; +use std::sync::Arc; + +#[derive(Debug)] +pub(crate) struct Type1Font(u128, Kind, Option); + +impl Type1Font { + pub(crate) fn new( + dict: &Dict<'_>, + resolver: &FontResolverFn, + cmap_resolver: &CMapResolverFn, + ) -> Option { + let cache_key = dict.cache_key(); + + let to_unicode = read_to_unicode(dict, cmap_resolver); + + let fallback = || { + // TODO: Actually use fallback fonts + let fallback_query = FallbackFontQuery::new(dict); + let standard_font = fallback_query.pick_standard_font(); + + warn!( + "unable to load font {}, falling back to {}", + fallback_query + .post_script_name + .unwrap_or("(no name)".to_string()), + standard_font.as_str() + ); + + Some(Self( + cache_key, + Kind::Standard(StandardKind::new_with_standard( + dict, + standard_font, + true, + resolver, + )?), + to_unicode.clone(), + )) + }; + + let inner = if is_cff(dict) { + if let Some(cff) = CffKind::new(dict) { + Self(cache_key, Kind::Cff(cff), to_unicode) + } else { + return fallback(); + } + } else if is_type1(dict) { + if let Some(f) = Type1Kind::new(dict) { + Self(cache_key, Kind::Type1(f), to_unicode) + } else { + return fallback(); + } + } else if let Some(standard) = StandardKind::new(dict, resolver) { + Self(cache_key, Kind::Standard(standard), to_unicode) + } else { + return fallback(); + }; + + Some(inner) + } + + pub(crate) fn new_standard(font: StandardFont, resolver: &FontResolverFn) -> Option { + let dict = Dict::default(); + let standard = StandardKind::new_with_standard(&dict, font, true, resolver)?; + + Some(Self(0, Kind::Standard(standard), None)) + } + + pub(crate) fn map_code(&self, code: u8) -> GlyphId { + match &self.1 { + Kind::Standard(s) => s.map_code(code), + Kind::Type1(s) => s.map_code(code), + Kind::Cff(c) => c.map_code(code), + } + } + + pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + match &self.1 { + Kind::Standard(s) => s.outline_glyph(glyph), + Kind::Cff(c) => c.outline_glyph(glyph), + Kind::Type1(t) => t.outline_glyph(glyph), + } + } + + pub(crate) fn glyph_width(&self, code: u8) -> Option { + match &self.1 { + Kind::Standard(s) => s.glyph_width(code), + Kind::Cff(c) => c.glyph_width(code), + Kind::Type1(t) => t.glyph_width(code), + } + } + + pub(crate) fn char_code_to_unicode(&self, char_code: u32) -> Option { + if let Some(to_unicode) = &self.2 + && let Some(c) = to_unicode.lookup_bf_string(char_code) + { + // Skip null character mappings and fall back to glyph name + // lookup. Some PDFs have incorrect ToUnicode mappings that map + // to U+0000. + if c != BfString::Char('\0') { + return Some(c); + } + } + + let code = char_code as u8; + match &self.1 { + Kind::Standard(s) => s.char_code_to_unicode(code).map(BfString::Char), + Kind::Cff(c) => c.char_code_to_unicode(code).map(BfString::Char), + Kind::Type1(t) => t.char_code_to_unicode(code).map(BfString::Char), + } + } +} + +impl CacheKey for Type1Font { + fn cache_key(&self) -> u128 { + self.0 + } +} + +#[derive(Debug)] +enum Kind { + Standard(StandardKind), + Cff(CffKind), + Type1(Type1Kind), +} + +fn is_cff(dict: &Dict<'_>) -> bool { + dict.get::>(FONT_DESC) + .map(|dict| dict.contains_key(FONT_FILE3)) + .unwrap_or(false) +} + +fn is_type1(dict: &Dict<'_>) -> bool { + dict.get::>(FONT_DESC) + .map(|dict| dict.contains_key(FONT_FILE)) + .unwrap_or(false) +} + +#[derive(Debug)] +struct Type1Kind { + font: Type1FontBlob, + encoding: Encoding, + widths: Vec, + missing_width: f32, + encodings: HashMap, + name_to_gid: HashMap, + standard_font: Option, +} + +impl Type1Kind { + fn new(dict: &Dict<'_>) -> Option { + let descriptor = dict.get::>(FONT_DESC)?; + let data = descriptor.get::>(FONT_FILE)?; + let font = Type1FontBlob::new(Arc::new(data.decoded().ok()?.to_vec()))?; + + let (encoding, encodings) = read_encoding(dict); + let (widths, missing_width) = read_widths(dict, &descriptor)?; + let standard_font = select_standard_font(dict, &descriptor).map(|(f, _)| f); + + let name_to_gid: HashMap = font + .table() + .glyph_names() + .map(|(gid, name)| (name.to_string(), gid)) + .collect(); + + Some(Self { + font, + encoding, + widths, + missing_width, + encodings, + name_to_gid, + standard_font, + }) + } + + fn name_to_glyph(&self, name: &str) -> Option { + self.name_to_gid.get(name).copied() + } + + fn map_code(&self, code: u8) -> GlyphId { + if let Some(entry) = self.encodings.get(&code) { + self.name_to_glyph(entry) + } else { + match self.encoding { + Encoding::BuiltIn => self.font.table().encoding().and_then(|e| e.map(code)), + _ => self + .encoding + .map_code(code) + .and_then(|name| self.name_to_glyph(name)), + } + } + .unwrap_or(GlyphId::NOTDEF) + } + + fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + self.font.outline_glyph(glyph) + } + + fn code_to_ps_name(&self, code: u8) -> Option<&str> { + self.encodings + .get(&code) + .map(String::as_str) + .or_else(|| match self.encoding { + Encoding::BuiltIn => self + .font + .table() + .encoding() + .and_then(|e| e.glyph_name(code)), + _ => self.encoding.map_code(code), + }) + } + + fn glyph_width(&self, code: u8) -> Option { + match self.widths.get(code as usize).copied() { + Some(Width::Value(w)) => Some(w), + Some(Width::Missing) => Some(self.missing_width), + _ => { + // If font looks like a standard font, get the width from there. + let sf = self.standard_font?; + self.code_to_ps_name(code) + .and_then(|name| sf.get_width(name)) + } + } + } + + fn char_code_to_unicode(&self, code: u8) -> Option { + self.code_to_ps_name(code).and_then(glyph_name_to_unicode) + } +} + +#[derive(Debug)] +struct CffKind { + font: CffFontBlob, + encoding: Encoding, + widths: Vec, + missing_width: f32, + encodings: HashMap, + name_to_gid: HashMap, + gid_to_name: Vec>, + standard_font: Option, +} + +impl CffKind { + fn new(dict: &Dict<'_>) -> Option { + let descriptor = dict.get::>(FONT_DESC)?; + let data = descriptor.get::>(FONT_FILE3)?; + let font = CffFontBlob::new(Arc::new(data.decoded().ok()?.to_vec()))?; + + let (encoding, encodings) = read_encoding(dict); + let (widths, missing_width) = read_widths(dict, &descriptor)?; + let standard_font = select_standard_font(dict, &descriptor).map(|(f, _)| f); + let mut gid_to_name = vec![None; font.num_glyphs() as usize]; + let name_to_gid: HashMap = font + .glyph_names() + .into_iter() + .inspect(|(gid, name)| gid_to_name[gid.to_u32() as usize] = Some(name.clone())) + .map(|(gid, name)| (name, gid)) + .collect(); + + Some(Self { + font, + encoding, + widths, + missing_width, + encodings, + name_to_gid, + gid_to_name, + standard_font, + }) + } + + fn map_code(&self, code: u8) -> GlyphId { + let get_glyph = |entry: &str| { + self.name_to_gid + .get(entry) + .copied() + .or_else(|| self.name_to_gid.get(normalized_glyph_name(entry)).copied()) + }; + + if let Some(entry) = self.encodings.get(&code) { + get_glyph(entry) + } else { + match self.encoding { + Encoding::BuiltIn => self.font.glyph_index(code), + _ => self.encoding.map_code(code).and_then(get_glyph), + } + } + .unwrap_or(GlyphId::NOTDEF) + } + + fn outline_glyph(&self, glyph: GlyphId) -> BezPath { + self.font.outline_glyph(glyph) + } + + fn code_to_ps_name(&self, code: u8) -> Option<&str> { + if let Some(entry) = self.encodings.get(&code) { + Some(entry.as_str()) + } else { + match self.encoding { + Encoding::BuiltIn => self + .font + .glyph_index(code) + .and_then(|gid| self.gid_to_name.get(gid.to_u32() as usize)) + .and_then(|name| name.as_deref()), + _ => self.encoding.map_code(code), + } + } + } + + fn glyph_width(&self, code: u8) -> Option { + match self.widths.get(code as usize).copied() { + Some(Width::Value(w)) => Some(w), + Some(Width::Missing) => Some(self.missing_width), + _ => { + // If font looks like a standard font, get the width from there. + let sf = self.standard_font?; + self.code_to_ps_name(code) + .and_then(|name| sf.get_width(name)) + } + } + } + + fn char_code_to_unicode(&self, code: u8) -> Option { + self.code_to_ps_name(code).and_then(glyph_name_to_unicode) + } +} diff --git a/vendor/hayro-interpret/src/font/type3.rs b/vendor/hayro-interpret/src/font/type3.rs new file mode 100644 index 00000000..6824d4ff --- /dev/null +++ b/vendor/hayro-interpret/src/font/type3.rs @@ -0,0 +1,265 @@ +use crate::CMapResolverFn; +use crate::context::Context; +use crate::device::Device; +use crate::font::glyph_simulator::GlyphSimulator; +use crate::font::true_type::{Width, read_encoding, read_widths}; +use crate::font::{Encoding, Glyph, Type3Glyph, UNITS_PER_EM, read_to_unicode}; +use crate::interpret::state::TextState; +use crate::soft_mask::SoftMask; +use crate::util::RectExt; +use crate::{BlendMode, interpret}; +use crate::{CacheKey, ClipPath, GlyphDrawMode, PathDrawMode}; +use crate::{Image, Paint}; +use hayro_cmap::{BfString, CMap}; +use hayro_syntax::content::TypedIter; +use hayro_syntax::content::ops::TypedInstruction; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::{CHAR_PROCS, FONT_BBOX, FONT_MATRIX, RESOURCES}; +use hayro_syntax::page::Resources; +use kurbo::{Affine, BezPath, Rect}; +use skrifa::GlyphId; +use std::collections::HashMap; + +#[derive(Debug)] +pub(crate) struct Type3<'a> { + widths: Vec, + missing_width: f32, + encoding: Encoding, + encodings: HashMap, + dict: Dict<'a>, + char_procs: HashMap>, + glyph_simulator: GlyphSimulator, + font_bbox: Rect, + matrix: Affine, + to_unicode: Option, +} + +impl<'a> Type3<'a> { + pub(crate) fn new(dict: &Dict<'a>, cmap_resolver: &CMapResolverFn) -> Option { + let (encoding, encodings) = read_encoding(dict); + let (widths, missing_width) = read_widths(dict, dict)?; + let font_bbox = dict + .get::(FONT_BBOX) + .unwrap_or(hayro_syntax::object::Rect::ZERO) + .to_kurbo(); + + let matrix = Affine::new( + dict.get::<[f64; 6]>(FONT_MATRIX) + .unwrap_or([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]), + ); + + let char_procs = { + let mut procs = HashMap::new(); + let dict = dict.get::>(CHAR_PROCS).unwrap_or_default(); + + for name in dict.keys() { + if let Some(prog) = dict.get::>(name.as_ref()) { + procs.insert(name.as_str().to_string(), prog); + } + } + + procs + }; + + let to_unicode = read_to_unicode(dict, cmap_resolver); + + Some(Self { + glyph_simulator: GlyphSimulator::new(), + encoding, + font_bbox, + char_procs, + widths, + missing_width, + encodings, + matrix, + dict: dict.clone(), + to_unicode, + }) + } + + pub(crate) fn map_code(&self, code: u8) -> GlyphId { + self.encodings + .get(&code) + .map(|s| s.as_str()) + .or_else(|| self.encoding.map_code(code)) + .map(|g| self.glyph_simulator.string_to_glyph(g)) + .unwrap_or(GlyphId::NOTDEF) + } + + pub(crate) fn glyph_width(&self, code: u8) -> f32 { + let w = match self.widths.get(code as usize).copied() { + Some(Width::Value(w)) => w, + _ => self.missing_width, + }; + (w * self.matrix.as_coeffs()[0] as f32) * UNITS_PER_EM + } + + pub(crate) fn char_code_to_unicode(&self, char_code: u32) -> Option { + // PdfCraft patch: upstream only consulted ToUnicode. Many Type3 fonts (e.g. TeX/dvips + // bitmap fonts) have none; fall back to the /Encoding glyph names, then to printable + // ASCII codes, as other viewers do for text extraction. + self.to_unicode + .as_ref() + .and_then(|t| t.lookup_bf_string(char_code)) + .or_else(|| { + let code = u8::try_from(char_code).ok()?; + let name = self + .encodings + .get(&code) + .map(|s| s.as_str()) + .or_else(|| self.encoding.map_code(code)); + name.and_then(crate::font::glyph_name_to_unicode) + .or_else(|| (0x20..0x7f).contains(&code).then_some(code as char)) + .map(BfString::Char) + }) + } + + pub(crate) fn render_glyph( + &self, + glyph: &Type3Glyph<'a>, + transform: Affine, + glyph_transform: Affine, + paint: &Paint<'a>, + device: &mut impl Device<'a>, + ) -> Option<()> { + // PdfCraft patch: a glyph procedure may show text in this same font (through + // inherited resources), recursing until the stack overflowed. + if glyph.nesting_depth > crate::context::MAX_PAINT_NESTING + || !crate::context::take_nested_paint(glyph.nesting_depth) + { + return None; + } + let mut state = glyph.state.clone(); + let root_transform = + transform * glyph_transform * self.matrix * Affine::scale(UNITS_PER_EM as f64); + state.ctm = root_transform; + + // Not sure if this is mentioned anywhere, but I do think we need to reset the text state + // (though the graphics state itself should be preserved). + state.text_state = TextState::default(); + + let name = self.glyph_simulator.glyph_to_string(glyph.glyph_id)?; + let program = self.char_procs.get(&name)?; + // PdfCraft patch: see `context::MAX_PAGE_CONTENT`. + let decoded = crate::context::decode_content(program)?; + let iter = TypedIter::new(decoded.as_ref()); + + let is_shape_glyph = { + let mut iter = iter.clone(); + let mut is_shape_glyph = true; + + while let Some(op) = iter.next() { + match op { + TypedInstruction::ShapeGlyph(_) => { + break; + } + TypedInstruction::ColorGlyph(_) => { + is_shape_glyph = false; + break; + } + _ => {} + } + } + + is_shape_glyph + }; + + let mut context = Context::new_with( + state.ctm, + self.font_bbox, + &glyph.cache, + glyph.xref, + glyph.settings.clone(), + state, + glyph.nesting_depth, + ); + + // A procedure's own dictionary replaces the font's, and the font's replaces the + // enclosing scope. Only absence inherits; an explicit empty dictionary is isolated. + let resources = Resources::from_scope( + program.dict().get(RESOURCES).or_else(|| self.dict.get(RESOURCES)), + glyph.parent_resources.clone(), + ); + + if is_shape_glyph { + let mut device = Type3ShapeGlyphDevice::new(device, paint.clone()); + interpret(iter, &resources, &mut context, &mut device); + } else { + interpret(iter, &resources, &mut context, device); + } + + Some(()) + } +} + +impl CacheKey for Type3<'_> { + fn cache_key(&self) -> u128 { + self.dict.cache_key() + } +} + +struct Type3ShapeGlyphDevice<'a, 'b, T: Device<'a>> { + inner: &'b mut T, + paint: Paint<'a>, +} + +impl<'a, 'b, T: Device<'a>> Type3ShapeGlyphDevice<'a, 'b, T> { + pub(crate) fn new(device: &'b mut T, paint: Paint<'a>) -> Self { + Self { + inner: device, + paint, + } + } +} + +// Only filling, stroking of paths and stencil masks are allowed. +impl<'a, T: Device<'a>> Device<'a> for Type3ShapeGlyphDevice<'a, '_, T> { + fn set_soft_mask(&mut self, m: Option>) { + self.inner.set_soft_mask(m); + } + + fn draw_path( + &mut self, + path: &BezPath, + transform: Affine, + _: &Paint<'_>, + draw_mode: &PathDrawMode, + ) { + self.inner + .draw_path(path, transform, &self.paint, draw_mode); + } + + fn push_clip_path(&mut self, clip_path: &ClipPath) { + self.inner.push_clip_path(clip_path); + } + + fn push_transparency_group(&mut self, _: f32, _: Option>, _: BlendMode) {} + + fn draw_glyph( + &mut self, + g: &Glyph<'a>, + transform: Affine, + glyph_transform: Affine, + p: &Paint<'a>, + draw_mode: &GlyphDrawMode, + ) { + self.inner + .draw_glyph(g, transform, glyph_transform, p, draw_mode); + } + + fn pop_clip_path(&mut self) { + self.inner.pop_clip_path(); + } + + fn pop_transparency_group(&mut self) {} + + fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) { + if let Image::Stencil(mut s) = image { + s.paint = self.paint.clone(); + self.inner.draw_image(Image::Stencil(s), transform); + } + } + + fn set_blend_mode(&mut self, _: BlendMode) {} +} diff --git a/vendor/hayro-interpret/src/function/mod.rs b/vendor/hayro-interpret/src/function/mod.rs new file mode 100644 index 00000000..005ab0bf --- /dev/null +++ b/vendor/hayro-interpret/src/function/mod.rs @@ -0,0 +1,157 @@ +//! PDF functions. +//! +//! PDF has the concept of functions, representing objects that take a certain number of values +//! as input, do some processing on them and then return some output. + +mod type0; +mod type2; +mod type3; +mod type4; + +use crate::function::type0::Type0; +use crate::function::type2::Type2; +use crate::function::type3::Type3; +use crate::function::type4::Type4; +use hayro_syntax::object::Dict; +use hayro_syntax::object::dict::keys::{DOMAIN, FUNCTION_TYPE, RANGE}; +use hayro_syntax::object::{Object, dict_or_stream}; +use smallvec::SmallVec; +use std::sync::Arc; + +/// The input/output type of functions. +pub(crate) type Values = SmallVec<[f32; 4]>; +/// PdfCraft patch (backport of hayro 0.8): interior breakpoints of stitching functions. +pub(crate) type StitchingBounds = SmallVec<[f32; 3]>; +type TupleVec = SmallVec<[(f32, f32); 4]>; + +// PdfCraft patch: PDF functions are untrusted object graphs. A normal color/transfer function +// has only a few nodes; cap both nesting and total construction work, including repeated +// references, before creating their children. These are resource limits, not PDF format limits. +const MAX_FUNCTION_DEPTH: usize = 64; +const MAX_FUNCTION_NODES: usize = 10_000; + +struct ConstructionBudget { + remaining: usize, + // Stable backing-byte identity catches cycles without rejecting shared siblings. Looking + // at pointers only compares identities; no pointer is ever dereferenced. + path: Vec<(*const u8, usize)>, +} + +#[derive(Debug)] +enum FunctionType { + Type0(Type0), + Type2(Type2), + Type3(Type3), + Type4(Type4), +} + +/// A PDF function. +#[derive(Debug, Clone)] +pub struct Function(Arc); + +impl Function { + /// Create a new function. + pub fn new(obj: &Object<'_>) -> Option { + Self::new_inner( + obj, + &mut ConstructionBudget { + remaining: MAX_FUNCTION_NODES, + path: Vec::new(), + }, + 0, + ) + } + + fn new_inner(obj: &Object<'_>, budget: &mut ConstructionBudget, depth: usize) -> Option { + if depth >= MAX_FUNCTION_DEPTH { + warn!("PDF function nesting exceeds the limit of {MAX_FUNCTION_DEPTH} levels"); + return None; + } + if budget.remaining == 0 { + warn!("PDF function construction exceeds the limit of {MAX_FUNCTION_NODES} nodes"); + return None; + } + budget.remaining -= 1; + let (dict, stream) = dict_or_stream(obj)?; + let identity = (dict.data().as_ptr(), dict.data().len()); + if budget.path.contains(&identity) { + warn!("PDF function contains a cyclic stitching-function reference"); + return None; + } + budget.path.push(identity); + let result = (|| { + let function_type = match dict.get::(FUNCTION_TYPE)? { + 0 => FunctionType::Type0(Type0::new(stream?)?), + 2 => FunctionType::Type2(Type2::new(dict)?), + 3 => FunctionType::Type3(Type3::new(dict, budget, depth + 1)?), + 4 => FunctionType::Type4(Type4::new(stream?)?), + _ => return None, + }; + Some(Self(Arc::new(function_type))) + })(); + budget.path.pop(); + result + } + + /// Evaluate the function with the given input. + pub fn eval(&self, input: Values) -> Option { + match self.0.as_ref() { + FunctionType::Type0(t0) => t0.eval(input), + FunctionType::Type2(t2) => Some(t2.eval(*input.first()?)), + FunctionType::Type3(t3) => t3.eval(*input.first()?), + FunctionType::Type4(t4) => Some(t4.eval(input)?), + } + } + + /// PdfCraft patch (backport of hayro 0.8): inputs where a stitching function switches + /// sub-function, so a gradient approximation can place a stop exactly there. + pub(crate) fn stitching_bounds(&self) -> StitchingBounds { + match self.0.as_ref() { + FunctionType::Type3(t3) => t3.stitching_bounds(), + _ => SmallVec::new(), + } + } +} + +#[derive(Debug, Clone)] +struct Clamper { + domain: TupleVec, + range: Option, +} + +impl Clamper { + fn new(dict: &Dict<'_>) -> Option { + let domain = dict.get::(DOMAIN)?; + let range = dict.get::(RANGE); + + Some(Self { domain, range }) + } + + fn clamp_input(&self, input: &mut [f32]) { + if input.len() != self.domain.len() { + warn!("the domain of the function didn't match the input arguments"); + } + + for ((min, max), val) in self.domain.iter().zip(input.iter_mut()) { + *val = val.min(*max).max(*min); + } + } + + fn clamp_output(&self, output: &mut [f32]) { + if let Some(range) = &self.range { + if range.len() != output.len() { + warn!("the range of the function didn't match the output arguments"); + } + + for ((min, max), val) in range.iter().zip(output.iter_mut()) { + *val = val.min(*max).max(*min); + } + } + } +} + +/// Linearly interpolate the value `x`, assuming that it lies within the range `x_min` and `x_max`, +/// to the range `y_min` and `y_max`. +pub(crate) fn interpolate(x: f32, x_min: f32, x_max: f32, y_min: f32, y_max: f32) -> f32 { + y_min + (x - x_min) * (y_max - y_min) / (x_max - x_min) +} diff --git a/vendor/hayro-interpret/src/function/type0.rs b/vendor/hayro-interpret/src/function/type0.rs new file mode 100644 index 00000000..02bbc41b --- /dev/null +++ b/vendor/hayro-interpret/src/function/type0.rs @@ -0,0 +1,327 @@ +use crate::function::{Clamper, TupleVec, Values, interpolate}; +use hayro_syntax::bit_reader::BitReader; +use hayro_syntax::object::Array; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::{BITS_PER_SAMPLE, DECODE, ENCODE, SIZE}; +use smallvec::{SmallVec, ToSmallVec, smallvec}; +use std::collections::HashMap; + +/// A type 0 function (sampled function). +#[derive(Debug)] +pub(crate) struct Type0 { + sizes: IntVec, + table: HashMap, + clamper: Clamper, + range: TupleVec, + bits_per_sample: u8, + encode: TupleVec, + decode: TupleVec, +} + +impl Type0 { + /// Create a new type 0 function. + pub(crate) fn new(stream: &Stream<'_>) -> Option { + let dict = stream.dict(); + let bits_per_sample = dict.get::(BITS_PER_SAMPLE)?; + + if !matches!(bits_per_sample, 1 | 2 | 4 | 8 | 16 | 24 | 32) { + error!("invalid bits per sample: {bits_per_sample}"); + + return None; + } + + let clamper = Clamper::new(dict)?; + let range = clamper.range.clone()?; + + if range.is_empty() { + warn!("encountered Type0 function with invalid range length 0."); + + return None; + } + + let sizes = dict + .get::>(SIZE)? + .iter::() + .collect::(); + + let encode = dict.get::(ENCODE).unwrap_or( + sizes + .iter() + .map(|s| (0.0, s.saturating_sub(1) as f32)) + .collect(), + ); + + // PdfCraft patch: see `MAX_SAMPLE_VALUES`. + let Some(num_expected_entries) = sizes + .iter() + .try_fold(1usize, |n, s| n.checked_mul(*s as usize)) + .filter(|entries| *entries <= MAX_SAMPLE_ENTRIES) + .and_then(|entries| entries.checked_mul(range.len())) + .filter(|n| *n <= MAX_SAMPLE_VALUES) + else { + warn!("Type0 function has too many samples"); + + return None; + }; + + let decode = dict.get::(DECODE).unwrap_or(range.clone()); + + let mut data = { + let decoded = stream.decoded().ok()?; + let mut buf = vec![]; + let mut reader = BitReader::new(&decoded); + + while buf.len() < num_expected_entries + && let Some(data) = reader.read(bits_per_sample) + { + buf.push(data); + } + + buf + }; + + if data.len() != num_expected_entries { + warn!("Type0 function didn't have the expected number of sample entries."); + data.truncate(num_expected_entries); + } + + let table = build_table(&data, &sizes, range.len())?; + + Some(Self { + sizes, + clamper, + range, + bits_per_sample, + table, + encode, + decode, + }) + } + + /// Evaluate a type 0 function with the given input. + pub(crate) fn eval(&self, mut input: Values) -> Option { + if input.len() != self.sizes.len() { + warn!("wrong number of arguments for sampled function"); + + return None; + } + + self.clamper.clamp_input(&mut input); + + let mut key = input; + + for (((x, domain), encode), size) in key + .iter_mut() + .zip(self.clamper.domain.iter()) + .zip(self.encode.iter()) + .zip(self.sizes.iter()) + { + *x = interpolate(*x, domain.0, domain.1, encode.0, encode.1); + *x = x.max(0.0).min(*size as f32 - 1.0); + } + + let in_prev = key.iter().map(|v| v.floor() as u32).collect::(); + let in_next = key.iter().map(|v| v.ceil() as u32).collect::(); + + let interpolator = Interpolator::new( + key.clone().to_smallvec(), + in_prev, + in_next, + self.sizes.clone(), + self.range.len(), + ); + + let interpolated = interpolator.interpolate(&self.table)?; + + let mut out = interpolated + .iter() + .zip(self.decode.iter()) + .map(|(x, decode)| { + interpolate( + *x, + 0.0, + (2_u32.pow(self.bits_per_sample as u32) - 1) as f32, + decode.0, + decode.1, + ) + }) + .collect::>(); + + self.clamper.clamp_output(&mut out); + + Some(out) + } +} + +type FloatVec = SmallVec<[f32; 4]>; +type IntVec = SmallVec<[u32; 4]>; + +// See +struct Interpolator { + input: FloatVec, + sizes: IntVec, + in_prev: IntVec, + in_next: IntVec, + out_len: usize, +} + +impl Interpolator { + fn new( + input: FloatVec, + in_prev: IntVec, + in_next: IntVec, + sizes: IntVec, + out_len: usize, + ) -> Self { + Self { + input, + in_prev, + in_next, + sizes, + out_len, + } + } + + fn interpolate(&self, table: &HashMap) -> Option { + self.interpolate_inner(smallvec![0; self.input.len()], 0, table) + } + + fn interpolate_inner( + &self, + mut coord: IntVec, + step: usize, + table: &HashMap, + ) -> Option { + if step == self.input.len() - 1 { + if self.in_prev[step] == self.in_next[step] { + coord[step] = self.in_prev[step]; + + Some( + table + .get(&Key::from_raw(&self.sizes, &coord))? + .clone() + .iter() + .map(|n| *n as f32) + .collect(), + ) + } else { + coord[step] = self.in_prev[step]; + let val1 = table.get(&Key::from_raw(&self.sizes, &coord))?; + coord[step] = self.in_next[step]; + let val2 = table.get(&Key::from_raw(&self.sizes, &coord))?; + let mut out = smallvec![0.0; self.out_len]; + + for i in 0..self.out_len { + out[i] = interpolate( + self.input[step], + self.in_prev[step] as f32, + self.in_next[step] as f32, + val1[i] as f32, + val2[i] as f32, + ); + } + + Some(out) + } + } else if self.in_prev[step] == self.in_next[step] { + coord[step] = self.in_prev[step]; + self.interpolate_inner(coord, step + 1, table) + } else { + coord[step] = self.in_prev[step]; + let val1 = self.interpolate_inner(coord.clone(), step + 1, table)?; + coord[step] = self.in_next[step]; + let val2 = self.interpolate_inner(coord, step + 1, table)?; + + let mut out = smallvec![0.0; self.out_len]; + + for i in 0..self.out_len { + out[i] = interpolate( + self.input[step], + self.in_prev[step] as f32, + self.in_next[step] as f32, + val1[i], + val2[i], + ); + } + + Some(out) + } + } +} + +/// PdfCraft patch: the most sample values a sampled function may have, and the most table +/// entries (one per point of its `/Size` grid). The samples were read into 4-byte values and +/// then into a table entry each, so a stream inflating to 256 MiB of 1-bit samples asked for +/// 8 GB; `/Size` sets how many the table expects. The largest real tables are DeviceN tint +/// transforms: 33 ร— 33 ร— 33 ร— 33 points with 3 outputs is 1.2 million entries and 3.6 million +/// values, 17^5 points with 4 outputs 5.7 million values. +const MAX_SAMPLE_VALUES: usize = 1 << 23; +const MAX_SAMPLE_ENTRIES: usize = 1 << 21; + +fn build_table(data: &[u32], sizes: &[u32], n: usize) -> Option> { + let mut key = Key::new(sizes); + let mut table = HashMap::new(); + + let mut first = true; + + for b in data.chunks_exact(n) { + if !first { + key.increment(); + } + + table.insert(key.clone(), b.to_smallvec()); + + first = false; + } + + Some(table) +} + +/// A sampled function consists of a (possibly) multi-dimensional table that we can index +/// into. We do this by representing the entries as a flat list of vectors, where each +/// element in the vector represents the value of the key in that specific dimension. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct Key { + sizes: SmallVec<[u32; 4]>, + parts: SmallVec<[u32; 4]>, +} + +impl Key { + fn new(sizes: &[u32]) -> Self { + let parts = smallvec![0; sizes.len()]; + + Self { + sizes: sizes.to_smallvec(), + parts, + } + } + + fn from_raw(sizes: &[u32], parts: &[u32]) -> Self { + Self { + sizes: sizes.to_smallvec(), + parts: parts.to_smallvec(), + } + } + + fn increment(&mut self) -> Option<()> { + self.increment_index(0) + } + + fn increment_index(&mut self, index: usize) -> Option<()> { + let size = *self.sizes.get(index).or_else(|| { + error!("overflowed key in sampled function"); + + None + })?; + let val = self.parts.get_mut(index)?; + + if *val >= (size - 1) { + *val = 0; + self.increment_index(index + 1)?; + } else { + *val += 1; + } + + Some(()) + } +} diff --git a/vendor/hayro-interpret/src/function/type2.rs b/vendor/hayro-interpret/src/function/type2.rs new file mode 100644 index 00000000..3e12fdaf --- /dev/null +++ b/vendor/hayro-interpret/src/function/type2.rs @@ -0,0 +1,148 @@ +use crate::function::{Clamper, Values}; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Number; +use hayro_syntax::object::dict::keys::{C0, C1, N}; +use smallvec::{SmallVec, smallvec}; + +/// A type 2 function (exponential function). +#[derive(Debug)] +pub(crate) struct Type2 { + c0: Values, + c1: Values, + clamper: Clamper, + n: f32, +} + +impl Type2 { + /// Create a new type 2 function. + pub(crate) fn new(dict: &Dict<'_>) -> Option { + let c0 = dict.get::(C0).unwrap_or(smallvec![0.0]); + let c1 = dict.get::(C1).unwrap_or(smallvec![1.0]); + let clamper = Clamper::new(dict)?; + let n = dict.get::(N)?.as_f64() as f32; + + Some(Self { c0, c1, clamper, n }) + } + + /// Evaluate the function with the given input. + pub(crate) fn eval(&self, input: f32) -> Values { + let mut input = [input]; + self.clamper.clamp_input(&mut input); + + let mut out = self + .c0 + .iter() + .zip(self.c1.iter()) + .map(|(c0, c1)| *c0 + input[0].powf(self.n) * (*c1 - *c0)) + .collect::>(); + + self.clamper.clamp_output(&mut out); + + out + } +} + +#[cfg(test)] +mod tests { + use crate::function::Function; + + use hayro_syntax::object::Object; + use hayro_syntax::object::{Dict, FromBytes}; + use smallvec::smallvec; + + #[test] + fn simple() { + let func = Function::new(&Object::Dict( + Dict::from_bytes( + b"<< + /FunctionType 2 + /Domain [ 0 1 ] + /C0 [ 0 20 ] + /C1 [ 30 -50 ] + /N 1 + >>", + ) + .unwrap(), + )) + .unwrap(); + + assert_eq!(func.eval(smallvec![0.0]).unwrap().as_ref(), &[0.0, 20.0]); + assert_eq!(func.eval(smallvec![0.5]).unwrap().as_ref(), &[15.0, -15.0]); + assert_eq!(func.eval(smallvec![1.0]).unwrap().as_ref(), &[30.0, -50.0]); + } + + #[test] + fn with_exponent() { + let func = Function::new(&Object::Dict( + Dict::from_bytes( + b"<< + /FunctionType 2 + /Domain [ 0 1 ] + /C0 [ 0 ] + /C1 [ 30 ] + /N 2 + >>", + ) + .unwrap(), + )) + .unwrap(); + + assert_eq!(func.eval(smallvec![0.5]), Some(smallvec![7.5])); + } + + #[test] + fn clamp_domain() { + let func = Function::new(&Object::Dict( + Dict::from_bytes( + b"<< + /FunctionType 2 + /Domain [ 0.2 0.8 ] + /C0 [ 0 ] + /C1 [ 30 ] + /N 2 + >>", + ) + .unwrap(), + )) + .unwrap(); + + assert_eq!( + func.eval(smallvec![0.0]).as_ref(), + func.eval(smallvec![0.2]).as_ref(), + ); + assert_eq!( + func.eval(smallvec![-10.]).as_ref(), + func.eval(smallvec![0.2]).as_ref(), + ); + assert_eq!( + func.eval(smallvec![0.8]).as_ref(), + func.eval(smallvec![0.8]).as_ref(), + ); + assert_eq!( + func.eval(smallvec![1.2]).as_ref(), + func.eval(smallvec![1.0]).as_ref(), + ); + } + + #[test] + fn clamp_range() { + let func = Function::new(&Object::Dict( + Dict::from_bytes( + b"<< + /FunctionType 2 + /Domain [ 0.0 1.0 ] + /Range [10.0 20.0] + /C0 [ 0 ] + /C1 [ 30 ] + /N 1 + >>", + ) + .unwrap(), + )) + .unwrap(); + + assert_eq!(func.eval(smallvec![0.0]), Some(smallvec![10.0])); + assert_eq!(func.eval(smallvec![0.5]), Some(smallvec![15.0])); + assert_eq!(func.eval(smallvec![1.0]), Some(smallvec![20.0])); + } +} diff --git a/vendor/hayro-interpret/src/function/type3.rs b/vendor/hayro-interpret/src/function/type3.rs new file mode 100644 index 00000000..b0b15b86 --- /dev/null +++ b/vendor/hayro-interpret/src/function/type3.rs @@ -0,0 +1,302 @@ +use crate::function::{Clamper, Function, StitchingBounds, TupleVec, Values, interpolate}; +use hayro_syntax::object::Array; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Object; +use hayro_syntax::object::dict::keys::{BOUNDS, ENCODE, FUNCTIONS}; +use smallvec::smallvec; + +/// A type 3 function (stitching function). +#[derive(Debug)] +pub(crate) struct Type3 { + functions: Vec, + bounds: Vec, + encode: TupleVec, + clamper: Clamper, +} + +impl Type3 { + /// Create a new type 3 function. + pub(super) fn new( + dict: &Dict<'_>, + budget: &mut super::ConstructionBudget, + depth: usize, + ) -> Option { + let clamper = Clamper::new(dict)?; + + // PdfCraft patch: all descendants share the caller's depth, work and cycle guards. + // The resolved iterator also ends on an unresolved/cyclic reference. Require one + // resolved child per raw slot so that failure cannot silently shorten the function list. + let array = dict.get::>(FUNCTIONS)?; + let mut objects = array.iter::>(); + let functions: Vec = array + .raw_iter() + .map(|_| { + let Some(object) = objects.next() else { + warn!( + "PDF stitching function contains an unresolved or cyclic child reference" + ); + return None; + }; + Function::new_inner(&object, budget, depth) + }) + .collect::>()?; + if functions.is_empty() { + warn!("PDF stitching function has no child functions"); + return None; + } + let domain = *clamper.domain.first()?; + let mut bounds = vec![domain.0 - 0.0001]; + if let Some(a) = dict.get::>(BOUNDS) { + bounds.extend(a.iter::()); + } + // Add a small delta so that the interval is considered to be closed on the right. + bounds.push(domain.1 + 0.0001); + + let encode = dict.get::(ENCODE)?; + + Some(Self { + functions, + clamper, + bounds, + encode, + }) + } + + /// Evaluate the function with the given input. + pub(crate) fn eval(&self, input: f32) -> Option { + let mut input = [input]; + self.clamper.clamp_input(&mut input); + + let index = find_interval(&self.bounds, input[0])?; + + let bounds_i = *self.bounds.get(index + 1)?; + let bounds_i_minus_1 = *self.bounds.get(index)?; + + // - 1 because we inserted a dummy bound in the constructor. + let encoding = self.encode.get(index)?; + let function = self.functions.get(index)?; + let encoded = interpolate(input[0], bounds_i_minus_1, bounds_i, encoding.0, encoding.1); + + let mut evaluated = function.eval(smallvec![encoded])?; + + self.clamper.clamp_output(&mut evaluated); + + Some(evaluated) + } + + // PdfCraft patch (backport of hayro 0.8). + pub(crate) fn stitching_bounds(&self) -> StitchingBounds { + let mut stitching_bounds = StitchingBounds::new(); + if self.bounds.len() > 2 { + stitching_bounds.extend_from_slice(&self.bounds[1..self.bounds.len() - 1]); + } + + for (index, function) in self.functions.iter().enumerate() { + let Some(bounds_i_minus_1) = self.bounds.get(index).copied() else { + continue; + }; + let Some(bounds_i) = self.bounds.get(index + 1).copied() else { + continue; + }; + + let Some((encode_min, encode_max)) = self.encode.get(index).copied() else { + continue; + }; + + if (encode_max - encode_min).abs() <= f32::EPSILON { + continue; + } + + for child_bound in function.stitching_bounds() { + let bound = interpolate( + child_bound, + encode_min, + encode_max, + bounds_i_minus_1, + bounds_i, + ); + let min_bound = bounds_i_minus_1.min(bounds_i); + let max_bound = bounds_i_minus_1.max(bounds_i); + if bound > min_bound && bound < max_bound { + stitching_bounds.push(bound); + } + } + } + + stitching_bounds.sort_by(f32::total_cmp); + stitching_bounds.dedup_by(|a, b| (*a - *b).abs() <= f32::EPSILON); + + stitching_bounds + } +} + +fn find_interval(bounds: &[f32], x: f32) -> Option { + if x < *bounds.first()? || x >= *bounds.last()? { + return None; + } + + match bounds.binary_search_by(|val| { + if *val <= x { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + } + }) { + Ok(i) => Some(i - 1), + Err(i) => Some(i - 1), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hayro_syntax::object::FromBytes; + + #[test] + fn simple() { + let data = b"<< + /FunctionType 3 + /Domain [-7 7] + /Functions [ + << /FunctionType 2 + /Domain [0 1] + /C0 [0.5 0.5 0.5] + /C1 [0.5 0.5 0.5] + /N 1 + >> + << /FunctionType 2 + /Domain [0 1] + /C0 [0.7 0.7 0.7] + /C1 [0.7 0.7 0.7] + /N 1 + >> + ] + /Bounds [0] + /Encode [0 1 0 1] +>>"; + + let dict = Object::from_bytes(data).unwrap(); + let function = Function::new(&dict).unwrap(); + + assert_eq!( + function.eval(smallvec![-7.0]).unwrap().as_slice(), + &[0.5, 0.5, 0.5] + ); + assert_eq!( + function.eval(smallvec![-3.0]).unwrap().as_slice(), + &[0.5, 0.5, 0.5] + ); + assert_eq!( + function.eval(smallvec![-0.5]).unwrap().as_slice(), + &[0.5, 0.5, 0.5] + ); + assert_eq!( + function.eval(smallvec![0.0]).unwrap().as_slice(), + &[0.7, 0.7, 0.7] + ); + assert_eq!( + function.eval(smallvec![7.0]).unwrap().as_slice(), + &[0.7, 0.7, 0.7] + ); + } + + fn function_limits_stitching(levels: usize) -> String { + let mut function = "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>".to_owned(); + for _ in 1..levels { + function = format!( + "<< /FunctionType 3 /Domain [0 1] /Functions [{function}] /Bounds [] /Encode [0 1] >>" + ); + } + function + } + + #[test] + fn function_limits_stitching_depth_is_bounded() { + // Each enclosing function contributes a dictionary and /Functions array; + // the leaf contributes a dictionary and its /Domain (or /C0, /C1) array. + // Thus 32 function levels reach exactly 64 direct containers, while 33 + // reach 66 and are rejected by syntax before function construction. + let data = function_limits_stitching(32); + let object = Object::from_bytes(data.as_bytes()).unwrap(); + assert_eq!( + Function::new(&object).unwrap().eval(smallvec![0.5]), + Some(smallvec![0.5]) + ); + let data = function_limits_stitching(33); + assert!(Object::from_bytes(data.as_bytes()).is_none()); + } + + fn function_limits_indirect_stitching(levels: usize) -> hayro_syntax::Pdf { + use std::fmt::Write; + + let mut objects = vec![ + "<< /Type /Catalog /Pages 2 0 R >>".to_owned(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_owned(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] >>".to_owned(), + ]; + for level in 0..levels { + objects.push(if level + 1 == levels { + "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>".to_owned() + } else { + format!( + "<< /FunctionType 3 /Domain [0 1] /Functions [{} 0 R] /Bounds [] /Encode [0 1] >>", + level + 5 + ) + }); + } + let mut data = String::from("%PDF-1.7\n"); + let mut offsets = vec![0]; + for (index, object) in objects.iter().enumerate() { + offsets.push(data.len()); + writeln!(data, "{} 0 obj\n{object}\nendobj", index + 1).unwrap(); + } + let xref = data.len(); + writeln!(data, "xref\n0 {}\n0000000000 65535 f ", offsets.len()).unwrap(); + for offset in offsets.iter().skip(1) { + writeln!(data, "{offset:010} 00000 n ").unwrap(); + } + writeln!(data, "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF", offsets.len()).unwrap(); + hayro_syntax::Pdf::new(data.into_bytes()).unwrap() + } + + #[test] + fn function_limits_indirect_stitching_depth_is_bounded() { + use hayro_syntax::object::ObjectIdentifier; + + // Indirect functions stay at two direct containers each, so the parser's + // limit cannot mask a regression in the separate 64-function depth guard. + for levels in [64, 65] { + let pdf = function_limits_indirect_stitching(levels); + for index in 0..levels { + assert!(pdf.xref().get::>(ObjectIdentifier::new((index + 4) as i32, 0)).is_some()); + } + let object = pdf.xref().get::>(ObjectIdentifier::new(4, 0)).unwrap(); + let function = Function::new(&object); + if levels == 64 { + assert_eq!(function.unwrap().eval(smallvec![0.5]), Some(smallvec![0.5])); + } else { + assert!(function.is_none()); + } + } + } + + fn function_limits_fanout(children: usize) -> String { + let leaf = "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >> "; + format!( + "<< /FunctionType 3 /Domain [0 1] /Functions [{}] /Bounds [{}] /Encode [{}] >>", + leaf.repeat(children), + "0.5 ".repeat(children.saturating_sub(1)), + "0 1 ".repeat(children) + ) + } + + #[test] + fn function_limits_stitching_fanout_shares_a_node_budget() { + let data = function_limits_fanout(9999); + let object = Object::from_bytes(data.as_bytes()).unwrap(); + assert!(Function::new(&object).is_some()); + let data = function_limits_fanout(10000); + let object = Object::from_bytes(data.as_bytes()).unwrap(); + assert!(Function::new(&object).is_none()); + } +} diff --git a/vendor/hayro-interpret/src/function/type4.rs b/vendor/hayro-interpret/src/function/type4.rs new file mode 100644 index 00000000..e4c0d1e0 --- /dev/null +++ b/vendor/hayro-interpret/src/function/type4.rs @@ -0,0 +1,1103 @@ +use crate::function::{Clamper, Values}; +use hayro_syntax::content; +use hayro_syntax::object::Number; +use hayro_syntax::object::Stream; +use hayro_syntax::reader::Reader; +use hayro_syntax::reader::{ReaderContext, ReaderExt}; +use smallvec::SmallVec; +use std::array; +use std::ops::Rem; + +// PdfCraft patch: calculator color/transfer functions normally contain a handful of operators. +// Bound every parsed branch and every executed operator as well as recursive procedure depth. +// A shared budget is never reset when entering an `if` or `ifelse` branch. +const MAX_CALCULATOR_DEPTH: usize = 64; +const MAX_CALCULATOR_OPS: usize = 10_000; + +struct CalculatorBudget { + remaining: usize, +} + +impl CalculatorBudget { + fn new() -> Self { + Self { + remaining: MAX_CALCULATOR_OPS, + } + } + + fn take(&mut self) -> Option<()> { + if self.remaining == 0 { + warn!("PDF calculator function exceeds the limit of {MAX_CALCULATOR_OPS} operators"); + return None; + } + self.remaining -= 1; + Some(()) + } +} + +/// A type 4 function (postscript function). +#[derive(Debug)] +pub(crate) struct Type4 { + program: Vec, + clamper: Clamper, +} + +impl Type4 { + /// Create a new type 4 function. + pub(crate) fn new(stream: &Stream<'_>) -> Option { + let dict = stream.dict().clone(); + let clamper = Clamper::new(&dict)?; + + Some(Self { + clamper, + program: parse_procedure(&stream.decoded().ok()?)?, + }) + } + + /// Evaluate the function with the given input. + pub(crate) fn eval(&self, mut input: Values) -> Option { + self.clamper.clamp_input(&mut input); + + let mut arg_stack = InterpreterStack::new(); + + for input in input { + arg_stack.push(Argument::Float(input))?; + } + + eval_inner( + &self.program, + &mut arg_stack, + &mut CalculatorBudget::new(), + 0, + )?; + + let mut out: SmallVec<_> = arg_stack.items().iter().map(|i| i.as_f32()).collect(); + + self.clamper.clamp_output(&mut out); + + Some(out) + } +} + +#[derive(Clone, Copy)] +enum Argument { + Float(f32), + Bool(bool), +} + +impl Default for Argument { + fn default() -> Self { + Self::Float(0.0) + } +} + +impl Argument { + fn as_bool(&self) -> bool { + match self { + Self::Float(f) => *f != 0.0, + Self::Bool(b) => *b, + } + } + + fn as_f32(&self) -> f32 { + match self { + Self::Float(f) => *f, + Self::Bool(b) => { + if *b { + 1.0 + } else { + 0.0 + } + } + } + } +} + +struct ArgumentsStack { + stack: [T; C], + len: usize, +} + +impl ArgumentsStack { + fn new() -> Self { + Self { + stack: array::from_fn(|_| T::default()), + len: 0, + } + } + + #[inline] + fn len(&self) -> usize { + self.len + } + + #[inline] + fn is_empty(&self) -> bool { + self.len == 0 + } + + #[inline] + fn push(&mut self, n: T) -> Option<()> { + if self.len == C { + error!("overflowed post script argument stack"); + + None + } else { + self.stack[self.len] = n; + self.len += 1; + + Some(()) + } + } + + #[inline] + fn at(&self, index: usize) -> Option<&T> { + if index >= self.len { + None + } else { + Some(&self.stack[index]) + } + } + + #[inline] + fn last(&self) -> Option<&T> { + if self.is_empty() { + None + } else { + Some(&self.stack[self.len - 1]) + } + } + + #[inline] + fn pop(&mut self) -> Option { + if self.is_empty() { + error!("underflowed post script argument stack"); + + None + } else { + self.len -= 1; + Some(std::mem::take(&mut self.stack[self.len])) + } + } + + #[inline] + fn items(&self) -> &[T] { + &self.stack[..self.len] + } + + #[inline] + fn items_mut(&mut self) -> &mut [T] { + &mut self.stack[..self.len] + } +} + +type InterpreterStack = ArgumentsStack; +type ParseStack = ArgumentsStack, 2>; + +fn eval_inner( + procedure: &[PostScriptOp], + arg_stack: &mut InterpreterStack, + budget: &mut CalculatorBudget, + depth: usize, +) -> Option<()> { + if depth >= MAX_CALCULATOR_DEPTH { + warn!("PDF calculator nesting exceeds the limit of {MAX_CALCULATOR_DEPTH} levels"); + return None; + } + macro_rules! zero { + ($eval:expr) => { + arg_stack.push($eval)?; + }; + } + + macro_rules! one_f { + ($eval:expr) => { + let n1 = arg_stack.pop()?; + arg_stack.push(Argument::Float($eval(n1.as_f32())))?; + }; + } + + macro_rules! two_f { + ($eval:expr) => { + let n2 = arg_stack.pop()?; + let n1 = arg_stack.pop()?; + arg_stack.push(Argument::Float($eval(n1.as_f32(), n2.as_f32())))?; + }; + } + + macro_rules! four { + ($eval_f:expr, $eval_b:expr) => { + let n2 = arg_stack.pop()?; + let n1 = arg_stack.pop()?; + + let res = match (n1, n2) { + (Argument::Float(f1), Argument::Float(f2)) => Argument::Float($eval_f(f1, f2)), + (Argument::Float(_), Argument::Bool(f2)) => { + Argument::Bool($eval_b(n1.as_bool(), f2)) + } + (Argument::Bool(f1), Argument::Float(_)) => { + Argument::Bool($eval_b(f1, n2.as_bool())) + } + (Argument::Bool(f1), Argument::Bool(f2)) => Argument::Bool($eval_b(f1, f2)), + }; + + arg_stack.push(res)?; + }; + } + + fn bf(cond: bool) -> f32 { + (cond as i32) as f32 + } + + for op in procedure { + budget.take()?; + match op { + PostScriptOp::Number(n) => arg_stack.push(Argument::Float(n.as_f64() as f32))?, + PostScriptOp::Abs => { + one_f!(|n: f32| n.abs()); + } + PostScriptOp::Add => { + two_f!(|n1: f32, n2: f32| n1 + n2); + } + PostScriptOp::Atan => { + two_f!(|n1: f32, n2: f32| { + let mut res = n1.atan2(n2).to_degrees() % 360.0; + if res < 0.0 { + res += 360.0; + } + + res + }); + } + PostScriptOp::Ceiling => { + one_f!(|n: f32| n.ceil()); + } + PostScriptOp::Cos => { + one_f!(|n: f32| n.to_radians().cos()); + } + PostScriptOp::Cvi => { + one_f!(|n: f32| n.trunc()); + } + PostScriptOp::Cvr => { + one_f!(|n: f32| n); + } + PostScriptOp::Div => { + two_f!(|n1: f32, n2: f32| n1 / n2); + } + PostScriptOp::Exp => { + two_f!(|n1: f32, n2: f32| n1.powf(n2)); + } + PostScriptOp::Floor => { + one_f!(|n: f32| n.floor()); + } + PostScriptOp::Idiv => { + // PdfCraft patch: zero divisors and MIN / -1 have no integer result. + let n2 = arg_stack.pop()?.as_f32() as i32; + let n1 = arg_stack.pop()?.as_f32() as i32; + let quotient = n1.checked_div(n2).or_else(|| { + warn!("PDF calculator idiv has a zero divisor or an out-of-range result"); + None + })?; + arg_stack.push(Argument::Float(quotient as f32))?; + } + PostScriptOp::Ln => { + one_f!(|n: f32| n.ln()); + } + PostScriptOp::Log => { + one_f!(|n: f32| n.log10()); + } + PostScriptOp::Mod => { + two_f!(|n1: f32, n2: f32| n1.rem(n2)); + } + PostScriptOp::Mul => { + two_f!(|n1: f32, n2: f32| n1 * n2); + } + PostScriptOp::Neg => { + one_f!(|n: f32| -n); + } + PostScriptOp::Round => { + one_f!(|n: f32| n.round()); + } + PostScriptOp::Sin => { + one_f!(|n: f32| n.to_radians().sin()); + } + PostScriptOp::Sqrt => { + one_f!(|n: f32| n.sqrt()); + } + PostScriptOp::Sub => { + two_f!(|n1: f32, n2: f32| n1 - n2); + } + PostScriptOp::Truncate => { + one_f!(|n: f32| n.trunc()); + } + PostScriptOp::And => { + two_f!(|n1: f32, n2: f32| (n1 as i32 & n2 as i32) as f32); + } + PostScriptOp::Bitshift => { + two_f!(|n1: f32, n2: f32| { + // PdfCraft patch: PLRM, bitshift, discards shifted-out bits and fills + // with zeroes. All bits are gone at >= 32; use the signed word's bit + // pattern for negative operands, and never negate MIN for right shifts. + let num = n1 as i32 as u32; + let shift = n2 as i32; + let bits = if shift >= 0 { + num.checked_shl(shift as u32).unwrap_or(0) + } else { + num.checked_shr(shift.unsigned_abs()).unwrap_or(0) + }; + bits as i32 as f32 + }); + } + PostScriptOp::Eq => { + two_f!(|n1: f32, n2: f32| bf(n1 == n2)); + } + PostScriptOp::False => { + zero!(Argument::Bool(false)); + } + PostScriptOp::Ge => { + two_f!(|n1: f32, n2: f32| bf(n1 >= n2)); + } + PostScriptOp::Gt => { + two_f!(|n1: f32, n2: f32| bf(n1 > n2)); + } + PostScriptOp::Le => { + two_f!(|n1: f32, n2: f32| bf(n1 <= n2)); + } + PostScriptOp::Lt => { + two_f!(|n1: f32, n2: f32| bf(n1 < n2)); + } + PostScriptOp::Ne => { + two_f!(|n1: f32, n2: f32| bf(n1 != n2)); + } + PostScriptOp::Not => { + let arg = arg_stack.pop()?; + + let res = match arg { + Argument::Float(f) => Argument::Float(!(f as i32) as f32), + Argument::Bool(b) => Argument::Bool(!b), + }; + + arg_stack.push(res)?; + } + PostScriptOp::Or => { + four!( + |n1: f32, n2: f32| ((n1 as i32) | (n2 as i32)) as f32, + |b1: bool, b2: bool| b1 || b2 + ); + } + PostScriptOp::True => { + zero!(Argument::Bool(true)); + } + PostScriptOp::Xor => { + four!( + |n1: f32, n2: f32| ((n1 as i32) ^ (n2 as i32)) as f32, + |b1: bool, b2: bool| b1 ^ b2 + ); + } + PostScriptOp::If(p) => { + let cond = arg_stack.pop()?.as_bool(); + + if cond { + eval_inner(p, arg_stack, budget, depth + 1)?; + } + } + PostScriptOp::IfElse(p1, p2) => { + let cond = arg_stack.pop()?.as_bool(); + + if cond { + eval_inner(p1, arg_stack, budget, depth + 1)?; + } else { + eval_inner(p2, arg_stack, budget, depth + 1)?; + } + } + PostScriptOp::Copy => { + let n = arg_stack.pop()?.as_f32() as u32 as usize; + let start = arg_stack.len().checked_sub(n)?; + for i in start..arg_stack.len() { + arg_stack.push(*arg_stack.at(i)?)?; + } + } + PostScriptOp::Dup => { + arg_stack.push(*arg_stack.last()?)?; + } + PostScriptOp::Exch => { + let n2 = arg_stack.pop()?; + let n1 = arg_stack.pop()?; + + arg_stack.push(n2)?; + arg_stack.push(n1)?; + } + PostScriptOp::Index => { + let n = arg_stack.pop()?.as_f32() as u32 as usize; + let n = arg_stack.len().checked_sub(n.checked_add(1)?)?; + + arg_stack.push(*arg_stack.at(n)?)?; + } + PostScriptOp::Pop => { + arg_stack.pop()?; + } + PostScriptOp::Roll => { + let j = arg_stack.pop()?.as_f32() as i32; + let n = arg_stack.pop()?.as_f32() as u32 as usize; + let trimmed_n = arg_stack.len().checked_sub(n)?; + + let target = &mut arg_stack.items_mut()[trimmed_n..]; + + if target.is_empty() { + continue; + } + + if j >= 0 { + let shift = j as usize % target.len(); + target.rotate_right(shift); + } else { + let shift = j.unsigned_abs() as usize % target.len(); + target.rotate_left(shift); + } + } + } + } + + Some(()) +} + +fn parse_procedure(data: &[u8]) -> Option> { + let mut r = Reader::new(data); + parse_procedure_inner(&mut r, &mut CalculatorBudget::new(), 0) +} + +fn parse_procedure_inner( + r: &mut Reader<'_>, + budget: &mut CalculatorBudget, + depth: usize, +) -> Option> { + if depth >= MAX_CALCULATOR_DEPTH { + warn!("PDF calculator nesting exceeds the limit of {MAX_CALCULATOR_DEPTH} levels"); + return None; + } + let mut stack = ParseStack::new(); + + let mut ops = vec![]; + r.skip_white_spaces_and_comments(); + r.forward_tag(b"{")?; + + loop { + r.skip_white_spaces_and_comments(); + + if r.peek_byte()? == b'}' { + r.forward_tag(b"}")?; + + break; + } else if r.peek_byte()? == b'{' { + budget.take()?; + stack.push(parse_procedure_inner(r, budget, depth + 1)?)?; + } else { + budget.take()?; + let op = PostScriptOp::from_reader(r, &mut stack)?; + ops.push(op); + } + } + + Some(ops) +} + +#[derive(Debug, Clone, PartialEq)] +pub(super) enum PostScriptOp { + Number(Number), + Abs, + Add, + Atan, + Ceiling, + Cos, + Cvi, + Cvr, + Div, + Exp, + Floor, + Idiv, + Ln, + Log, + Mod, + Mul, + Neg, + Round, + Sin, + Sqrt, + Sub, + Truncate, + And, + Bitshift, + Eq, + False, + Ge, + Gt, + Le, + Lt, + Ne, + Not, + Or, + True, + Xor, + If(Vec), + IfElse(Vec, Vec), + Copy, + Dup, + Exch, + Index, + Pop, + Roll, +} + +impl PostScriptOp { + fn from_reader(r: &mut Reader<'_>, stack: &mut ParseStack) -> Option { + let op = if let Some(n) = r.read::(&ReaderContext::dummy()) { + // TODO: Support radix numbers + Self::Number(n) + } else { + let op = r.read::>(&ReaderContext::dummy())?; + match op.as_ref() { + b"abs" => Self::Abs, + b"add" => Self::Add, + b"atan" => Self::Atan, + b"ceiling" => Self::Ceiling, + b"cos" => Self::Cos, + b"cvi" => Self::Cvi, + b"cvr" => Self::Cvr, + b"div" => Self::Div, + b"exp" => Self::Exp, + b"floor" => Self::Floor, + b"idiv" => Self::Idiv, + b"ln" => Self::Ln, + b"log" => Self::Log, + b"mod" => Self::Mod, + b"mul" => Self::Mul, + b"neg" => Self::Neg, + b"round" => Self::Round, + b"sin" => Self::Sin, + b"sqrt" => Self::Sqrt, + b"sub" => Self::Sub, + b"truncate" => Self::Truncate, + b"and" => Self::And, + b"bitshift" => Self::Bitshift, + b"eq" => Self::Eq, + b"false" => Self::False, + b"ge" => Self::Ge, + b"gt" => Self::Gt, + b"le" => Self::Le, + b"lt" => Self::Lt, + b"ne" => Self::Ne, + b"not" => Self::Not, + b"or" => Self::Or, + b"true" => Self::True, + b"xor" => Self::Xor, + b"if" => Self::If(stack.pop()?), + b"ifelse" => { + let s = stack.pop()?; + let f = stack.pop()?; + Self::IfElse(f, s) + } + b"copy" => Self::Copy, + b"dup" => Self::Dup, + b"exch" => Self::Exch, + b"index" => Self::Index, + b"pop" => Self::Pop, + b"roll" => Self::Roll, + _ => { + error!("encountered unknown postscript operator {op:?}"); + + return None; + } + } + }; + + Some(op) + } +} + +#[cfg(test)] +mod tests { + use crate::function::type4::{PostScriptOp, Type4, parse_procedure}; + use crate::function::{Clamper, Function, FunctionType, TupleVec, Values}; + use std::f32::consts::LN_10; + use std::sync::Arc; + + use hayro_syntax::object::Number; + use smallvec::smallvec; + + #[test] + fn lex_1() { + let program = b"{ copy dup 2.0 exch roll }"; + let parsed = parse_procedure(program).unwrap(); + + assert_eq!( + parsed, + vec![ + PostScriptOp::Copy, + PostScriptOp::Dup, + PostScriptOp::Number(Number::from_f32(2.0)), + PostScriptOp::Exch, + PostScriptOp::Roll, + ] + ); + } + + #[test] + fn lex_3() { + let program = b" { {dup exch} if {0} {1} ifelse }"; + let parsed = parse_procedure(program).unwrap(); + + assert_eq!( + parsed, + vec![ + PostScriptOp::If(vec![PostScriptOp::Dup, PostScriptOp::Exch]), + PostScriptOp::IfElse( + vec![PostScriptOp::Number(Number::from_i32(0))], + vec![PostScriptOp::Number(Number::from_i32(1))] + ) + ] + ); + } + + fn op_impl(prog: &str, out: &[f32]) { + let procedure = format!("{{{prog}}}"); + let procedure = parse_procedure(procedure.as_bytes()).unwrap(); + + let type4 = Type4 { + program: procedure, + clamper: Clamper { + domain: TupleVec::default(), + range: None, + }, + }; + + let res = type4.eval(Values::new()).unwrap(); + + assert_eq!(res.as_slice(), out); + } + + #[test] + fn op_abs() { + op_impl("4.5 abs", &[4.5]); + op_impl("-3 abs", &[3.0]); + op_impl("0 abs", &[0.0]); + } + + #[test] + fn op_atan() { + op_impl("0 1 atan", &[0.0]); + op_impl("1 0 atan", &[90.0]); + op_impl("-100 0 atan", &[270.0]); + op_impl("4 4 atan", &[45.0]); + } + + #[test] + fn op_ceiling() { + op_impl("3.2 ceiling", &[4.0]); + op_impl("-4.8 ceiling", &[-4.0]); + op_impl("99 ceiling", &[99.0]); + } + + #[test] + fn op_cos() { + op_impl("0 cos", &[1.0]); + // Should be zero, but floating point impreciseness. + op_impl("90 cos", &[-4.371139e-8]); + } + + #[test] + fn op_cvi() { + op_impl("-47.8 cvi", &[-47.0]); + op_impl("520.9 cvi", &[520.0]); + } + + #[test] + fn op_cvr() { + op_impl("-47.8 cvr", &[-47.8]); + op_impl("520 cvr", &[520.0]); + } + + #[test] + fn op_div() { + op_impl("3 2 div", &[1.5]); + op_impl("4 2 div", &[2.0]); + } + + #[test] + fn op_exp() { + op_impl("9 0.5 exp", &[3.0]); + op_impl("-9 -1 exp", &[-0.11111111]); + } + + #[test] + fn op_floor() { + op_impl("3.2 floor", &[3.0]); + op_impl("-4.8 floor", &[-5.0]); + op_impl("99 floor", &[99.0]); + } + + #[test] + fn op_idiv() { + op_impl("3 2 idiv", &[1.0]); + op_impl("4 2 idiv", &[2.0]); + op_impl("-5 2 idiv", &[-2.0]); + } + + #[test] + fn op_ln() { + op_impl("10 ln", &[LN_10]); + op_impl("100 ln", &[4.6051702]); + } + + #[test] + fn op_log() { + op_impl("10 log", &[1.0]); + op_impl("100 log", &[2.0]); + } + + #[test] + fn op_mod() { + op_impl("5 3 mod", &[2.0]); + op_impl("5 2 mod", &[1.0]); + op_impl("-5 3 mod", &[-2.0]); + } + + #[test] + fn op_mul() { + op_impl("5 3 mul", &[15.0]); + op_impl("-2 6 mul", &[-12.0]); + } + + #[test] + fn op_neg() { + op_impl("4.5 neg", &[-4.5]); + op_impl("-3 neg", &[3.0]); + } + + #[test] + fn op_round() { + op_impl("3.2 round", &[3.0]); + op_impl("6.5 round", &[7.0]); + op_impl("-4.8 round", &[-5.0]); + // TODO: This rounding doesn't match the PS spec. + // op_impl("-6.5 round", &[-6.0]); + op_impl("99 round", &[99.0]); + } + + #[test] + fn op_sin() { + op_impl("0.0 sin", &[0.0]); + op_impl("90.0 sin", &[1.0]); + } + + #[test] + fn op_sqrt() { + op_impl("100 sqrt", &[10.0]); + } + + #[test] + fn op_sub() { + op_impl("3 4 sub", &[-1.0]); + op_impl("6 0 sub", &[6.0]); + } + + #[test] + fn op_truncate() { + op_impl("3.2 truncate", &[3.0]); + op_impl("-4.8 truncate", &[-4.0]); + op_impl("99 truncate", &[99.0]); + } + + #[test] + fn op_and() { + op_impl("true true and", &[1.0]); + op_impl("true false and", &[0.0]); + op_impl("false true and", &[0.0]); + op_impl("false false and", &[0.0]); + op_impl("99 1 and", &[1.0]); + op_impl("52 7 and", &[4.0]); + } + + #[test] + fn op_bitshift() { + op_impl("7 3 bitshift", &[56.0]); + op_impl("142 -3 bitshift", &[17.0]); + } + + #[test] + fn op_eq() { + op_impl("4.0 4 eq", &[1.0]); + op_impl("-2.0 -3 eq", &[0.0]); + } + + #[test] + fn op_false() { + op_impl("false", &[0.0]); + } + + #[test] + fn op_ge() { + op_impl("4.2 4 ge", &[1.0]); + op_impl("4.2 4.2 ge", &[1.0]); + op_impl("4.2 6 ge", &[0.0]); + } + + #[test] + fn op_gt() { + op_impl("4.2 4 gt", &[1.0]); + op_impl("4.2 4.2 gt", &[0.0]); + op_impl("4.2 6 gt", &[0.0]); + } + + #[test] + fn op_le() { + op_impl("4.2 4 le", &[0.0]); + op_impl("4.2 4.2 le", &[1.0]); + op_impl("4.2 6 le", &[1.0]); + } + + #[test] + fn op_lt() { + op_impl("4.2 4 lt", &[0.0]); + op_impl("4.2 4.2 lt", &[0.0]); + op_impl("4.2 6 lt", &[1.0]); + } + + #[test] + fn op_ne() { + op_impl("3.0 3 ne", &[0.0]); + op_impl("3.0 3.0 ne", &[0.0]); + op_impl("3.0 3.1 ne", &[1.0]); + } + + #[test] + fn op_not() { + op_impl("true not", &[0.0]); + op_impl("false not", &[1.0]); + op_impl("52 not", &[-53.0]); + } + + #[test] + fn op_or() { + op_impl("true true or", &[1.0]); + op_impl("true false or", &[1.0]); + op_impl("false true or", &[1.0]); + op_impl("false false or", &[0.0]); + op_impl("17 5 or", &[21.0]); + } + + #[test] + fn op_xor() { + op_impl("true true xor", &[0.0]); + op_impl("true false xor", &[1.0]); + op_impl("false true xor", &[1.0]); + op_impl("false false xor", &[0.0]); + op_impl("7 3 xor", &[4.0]); + op_impl("12 3 xor", &[15.0]); + } + + #[test] + fn op_if() { + op_impl("true { 1.0 } if", &[1.0]); + op_impl("false { 1.0 } if", &[]); + } + + #[test] + fn op_ifelse() { + op_impl("true { 1.0 } { 2.0 } ifelse", &[1.0]); + op_impl("false { 1.0 } { 2.0 } ifelse", &[2.0]); + } + + #[test] + fn op_copy() { + op_impl("1 2 3 2 copy", &[1.0, 2.0, 3.0, 2.0, 3.0]); + op_impl("1 2 3 0 copy", &[1.0, 2.0, 3.0]); + } + + #[test] + fn op_dup() { + op_impl("1 2 3 dup", &[1.0, 2.0, 3.0, 3.0]); + } + + #[test] + fn op_exch() { + op_impl("1 2 3 exch", &[1.0, 3.0, 2.0]); + } + + #[test] + fn op_index() { + op_impl("1 2 3 4 0 index", &[1.0, 2.0, 3.0, 4.0, 4.0]); + op_impl("1 2 3 4 3 index", &[1.0, 2.0, 3.0, 4.0, 1.0]); + } + + #[test] + fn op_pop() { + op_impl("1 2 3 pop", &[1.0, 2.0]); + } + + #[test] + fn op_roll() { + op_impl("1 2 3 3 -1 roll", &[2.0, 3.0, 1.0]); + op_impl("1 2 3 3 1 roll", &[3.0, 1.0, 2.0]); + op_impl("1 2 3 3 0 roll", &[1.0, 2.0, 3.0]); + op_impl("1 2 3 3 5 roll", &[2.0, 3.0, 1.0]); + op_impl("0 2 roll", &[]); + op_impl( + "1 2 3 4 5 6 7 5 2 roll", + &[1.0, 2.0, 6.0, 7.0, 3.0, 4.0, 5.0], + ); + } + + #[test] + fn domain() { + let procedure = parse_procedure(b"{ }").unwrap(); + + let type4 = Function(Arc::new(FunctionType::Type4(Type4 { + program: procedure, + clamper: Clamper { + domain: smallvec![(-5.0, 5.0), (-5.0, 5.0), (-5.0, 5.0)], + range: None, + }, + }))); + + let input = smallvec![-10.0, -2.0, 6.0]; + let res = type4.eval(input).unwrap(); + + assert_eq!(res.as_slice(), &[-5.0, -2.0, 5.0]); + } + + fn function_limits_result(program: &str) -> Option { + Type4 { + program: parse_procedure(format!("{{ {program} }}").as_bytes())?, + clamper: Clamper { + domain: TupleVec::default(), + range: None, + }, + } + .eval(Values::new()) + } + + #[test] + fn function_limits_idiv_zero_is_refused() { + assert!(function_limits_result("1 0 idiv").is_none()); + } + + #[test] + fn function_limits_idiv_overflow_is_refused() { + assert!(function_limits_result("-2147483648 -1 idiv").is_none()); + } + + #[test] + fn function_limits_large_bitshifts_discard_all_bits() { + for shift in [32, -32, 2147483647, -2147483648] { + assert_eq!( + function_limits_result(&format!("7 {shift} bitshift")), + Some(smallvec![0.0]) + ); + } + assert_eq!( + function_limits_result("1073741824 1 bitshift"), + Some(smallvec![-2147483648.0]) + ); + assert_eq!( + function_limits_result("-2147483648 -1 bitshift"), + Some(smallvec![1073741824.0]) + ); + } + + #[test] + fn function_limits_roll_handles_the_most_negative_integer() { + assert_eq!( + function_limits_result("1 2 3 3 -2147483648 roll"), + Some(smallvec![3.0, 1.0, 2.0]) + ); + } + + #[test] + fn function_limits_large_indices_are_refused() { + assert!(function_limits_result("1 4294967295 index").is_none()); + } + + #[test] + fn function_limits_operand_stack_overflow_is_refused() { + assert_eq!( + function_limits_result(&format!("1 {}", "dup ".repeat(63))) + .unwrap() + .len(), + 64 + ); + assert!(function_limits_result(&format!("1 {}", "dup ".repeat(64))).is_none()); + } + + fn function_limits_nested_program(levels: usize) -> String { + let mut program = "{ 0 }".to_owned(); + for _ in 1..levels { + program = format!("{{ true {program} if }}"); + } + program + } + + #[test] + fn function_limits_calculator_parse_depth_is_bounded() { + assert!(parse_procedure(function_limits_nested_program(64).as_bytes()).is_some()); + assert!(parse_procedure(function_limits_nested_program(65).as_bytes()).is_none()); + } + + #[test] + fn function_limits_calculator_parse_work_is_shared() { + assert!(parse_procedure(format!("{{ {} }}", "0 pop ".repeat(5000)).as_bytes()).is_some()); + assert!(parse_procedure(format!("{{ {} }}", "0 pop ".repeat(5001)).as_bytes()).is_none()); + } + + #[test] + fn function_limits_parse_budget_counts_unselected_branches() { + let branch = "0 pop ".repeat(2500); + let program = format!("{{ true {{ {branch} }} {{ {branch} }} ifelse }}"); + assert!(parse_procedure(program.as_bytes()).is_none()); + assert!(parse_procedure(b"{ {} {} }").is_some()); + assert!(parse_procedure(b"{ {} {} {} }").is_none()); + } + fn function_limits_eval(program: Vec) -> Option { + Type4 { + program, + clamper: Clamper { + domain: TupleVec::default(), + range: None, + }, + } + .eval(Values::new()) + } + + #[test] + fn function_limits_calculator_eval_work_is_bounded() { + let pair = [PostScriptOp::Number(Number::from_i32(0)), PostScriptOp::Pop]; + assert_eq!( + function_limits_eval((0..5000).flat_map(|_| pair.iter().cloned()).collect()), + Some(Values::new()) + ); + assert!( + function_limits_eval((0..5001).flat_map(|_| pair.iter().cloned()).collect()).is_none() + ); + } + + #[test] + fn function_limits_eval_budget_is_shared_by_branches() { + let pair = [PostScriptOp::Number(Number::from_i32(0)), PostScriptOp::Pop]; + let mut program: Vec<_> = (0..3000).flat_map(|_| pair.iter().cloned()).collect(); + program.push(PostScriptOp::True); + program.push(PostScriptOp::If( + (0..2000).flat_map(|_| pair.iter().cloned()).collect(), + )); + assert!(function_limits_eval(program).is_none()); + } + #[test] + fn function_limits_calculator_eval_depth_is_bounded() { + let mut program = vec![PostScriptOp::Number(Number::from_i32(0))]; + for _ in 1..64 { + program = vec![PostScriptOp::True, PostScriptOp::If(program)]; + } + assert_eq!(function_limits_eval(program.clone()), Some(smallvec![0.0])); + assert!( + function_limits_eval(vec![PostScriptOp::True, PostScriptOp::If(program)]).is_none() + ); + } +} diff --git a/vendor/hayro-interpret/src/gradient.rs b/vendor/hayro-interpret/src/gradient.rs new file mode 100644 index 00000000..1cfa30af --- /dev/null +++ b/vendor/hayro-interpret/src/gradient.rs @@ -0,0 +1,361 @@ +//! SVG-like gradient conversion for encoded shadings. +//! +//! PDF shadings are very expensive to evaluate. Because of this, at least for +//! radial-axial shadings it's useful if we can instead approximate them by +//! SVG-like sRGB gradients. +//! +//! PdfCraft patch: backported from hayro-interpret 0.8.0 (Apache-2.0 OR MIT), adapted to this +//! version's `EncodedShadingType::RadialAxial` variant. Remove when the vendored copy moves to +//! 0.8. A full-page axial soft mask was sampled pixel by pixel (InDesign fades: 15 s per page). + +use crate::color::AlphaColor; +use crate::encode::{EncodedShadingPattern, EncodedShadingType, RadialAxialParams}; +use crate::function::StitchingBounds; +use crate::shading::ShadingFunction; +use kurbo::{Affine, Circle, Point, Rect, Shape}; +use smallvec::smallvec; + +const MAX_GRADIENT_SUBDIVISION_DEPTH: u8 = 10; +const RADIAL_EPSILON: f64 = 1.0e-6; + +/// An SVG-like gradient. +pub struct SvgGradient { + /// The kind of the gradient. + pub kind: SvgGradientKind, + /// The transform from encoded shading coordinates into the renderer's coordinate space. + pub transform: Affine, + /// The stops of the gradient in the sRGB color space. + pub stops: Vec, +} + +/// The kind of the SVG gradient. +pub enum SvgGradientKind { + /// Linear gradient. + Linear { + /// The normalized start point of the gradient line. + start: Point, + /// The normalized end point of the gradient line. + end: Point, + }, + /// Radial gradient. + Radial { + /// The center point of the start circle. + start_center: Point, + /// The radius of the start circle. + start_radius: f32, + /// The center point of the end circle. + end_center: Point, + /// The radius of the end circle. + end_radius: f32, + }, +} + +/// A color stop for an SVG-like gradient. +pub struct SvgGradientStop { + /// Normalized offset. + pub offset: f32, + /// RGBA color components in sRGB. + pub color: [f32; 4], +} + +impl EncodedShadingPattern { + /// Convert an axial/radial PDF shading into an SVG-like gradient approximation, or `None` + /// for other shading types and for cases a native gradient cannot represent exactly. + pub fn as_svg_gradient(&self, path_bbox: Rect, tolerance: f32) -> Option { + let EncodedShadingType::RadialAxial { + function, + params, + domain, + extend, + } = &self.shading_type + else { + return None; + }; + + EncodedRadialAxialShading { + function, + params, + domain: *domain, + extend: *extend, + } + .as_svg_gradient(self, path_bbox, tolerance) + } +} + +struct EncodedRadialAxialShading<'a> { + function: &'a ShadingFunction, + params: &'a RadialAxialParams, + domain: [f32; 2], + extend: [bool; 2], +} + +impl EncodedRadialAxialShading<'_> { + fn as_svg_gradient( + &self, + pattern: &EncodedShadingPattern, + path_bbox: Rect, + tolerance: f32, + ) -> Option { + // A couple of cases cannot be losslessly represented by an SVG gradient. + match self.params { + RadialAxialParams::Axial => { + if !self.axial_extend_covers_bbox(pattern, path_bbox) { + return None; + } + } + RadialAxialParams::Radial { .. } => { + if !self.radial_extend_covers_bbox(pattern, path_bbox) + || pattern.background_color.components()[3] > 0.0 + { + return None; + } + } + } + + Some(SvgGradient { + kind: self.native_gradient_kind(), + transform: pattern.base_transform.inverse(), + stops: approximate_gradient_stops( + |t| self.sample_t(pattern, t), + &self.normalized_stitching_bounds(), + tolerance, + ), + }) + } + + fn native_gradient_kind(&self) -> SvgGradientKind { + match self.params { + RadialAxialParams::Axial => SvgGradientKind::Linear { + start: Point::ZERO, + end: Point::new(1.0, 0.0), + }, + RadialAxialParams::Radial { p1, r } => SvgGradientKind::Radial { + start_center: Point::ZERO, + start_radius: r.x as f32, + end_center: *p1, + end_radius: r.y as f32, + }, + } + } + + fn sample_t(&self, pattern: &EncodedShadingPattern, t: f32) -> [f32; 4] { + let (t0, t1) = (self.domain[0], self.domain[1]); + let t = t0 + (t1 - t0) * t; + let Some(out) = self.function.eval(&smallvec![t]) else { + return [0.0, 0.0, 0.0, 0.0]; + }; + let mut components = pattern.color_space.to_rgba(&out, 1.0, false).components(); + components[3] *= pattern.opacity; + + if let Some(tf) = &pattern.transfer_function { + return tf.apply(&AlphaColor::new(components)).components(); + } + + components + } + + fn axial_extend_covers_bbox(&self, pattern: &EncodedShadingPattern, path_bbox: Rect) -> bool { + let path_bbox = pattern.base_transform.transform_rect_bbox(path_bbox); + + let min_x = path_bbox.min_x(); + let max_x = path_bbox.max_x(); + + (self.extend[0] || min_x >= 0.0) && (self.extend[1] || max_x <= 1.0) + } + + fn radial_extend_covers_bbox(&self, pattern: &EncodedShadingPattern, path_bbox: Rect) -> bool { + let RadialAxialParams::Radial { p1, r } = *self.params else { + return false; + }; + + let path_bbox = pattern.base_transform.transform_rect_bbox(path_bbox); + + if p1.x.abs() <= RADIAL_EPSILON && p1.y.abs() <= RADIAL_EPSILON { + return self.same_center_radial_extend_covers_bbox(path_bbox, r.x, r.y); + } + + let start_extend_ok = self.extend[0] || r.x.abs() <= RADIAL_EPSILON; + let end_extend_ok = self.extend[1] || circle_covers_bbox(Circle::new(p1, r.y), path_bbox); + + start_extend_ok && end_extend_ok + } + + fn same_center_radial_extend_covers_bbox(&self, path_bbox: Rect, r0: f64, r1: f64) -> bool { + let mut inner_circle = Circle::new(Point::ZERO, r0); + let mut outer_circle = Circle::new(Point::ZERO, r1); + let mut inner_extend = self.extend[0]; + let mut outer_extend = self.extend[1]; + + if inner_circle.radius > outer_circle.radius { + std::mem::swap(&mut inner_circle, &mut outer_circle); + std::mem::swap(&mut inner_extend, &mut outer_extend); + } + + let inner_extend_ok = inner_extend + || inner_circle.radius <= RADIAL_EPSILON + || circle_outside_bbox(inner_circle, path_bbox); + let outer_extend_ok = outer_extend || circle_covers_bbox(outer_circle, path_bbox); + + inner_extend_ok && outer_extend_ok + } + + fn normalized_stitching_bounds(&self) -> StitchingBounds { + let [t0, t1] = self.domain; + let dt = t1 - t0; + if dt.abs() <= f32::EPSILON { + return StitchingBounds::new(); + } + + let mut bounds = self + .function + .stitching_bounds() + .into_iter() + .map(|bound| (bound - t0) / dt) + .filter(|bound| *bound > 0.0 && *bound < 1.0) + .collect::(); + + if dt < 0.0 { + bounds.reverse(); + } + + bounds + } +} + +fn circle_covers_bbox(circle: Circle, bbox: Rect) -> bool { + if circle.radius < 0.0 { + return false; + } + + let radius_squared = circle.radius * circle.radius; + [ + Point::new(bbox.x0, bbox.y0), + Point::new(bbox.x1, bbox.y0), + Point::new(bbox.x0, bbox.y1), + Point::new(bbox.x1, bbox.y1), + ] + .into_iter() + .all(|point| { + let delta = point - circle.center; + delta.hypot2() <= radius_squared + }) +} + +fn circle_outside_bbox(circle: Circle, bbox: Rect) -> bool { + if circle.radius <= 0.0 { + return true; + } + + let closest = Point::new( + circle.center.x.clamp(bbox.x0, bbox.x1), + circle.center.y.clamp(bbox.y0, bbox.y1), + ); + + !circle.contains(closest) +} + +fn approximate_gradient_stops( + mut sample: impl FnMut(f32) -> [f32; 4], + breakpoints: &[f32], + tolerance: f32, +) -> Vec { + let mut interval_bounds = Vec::with_capacity(breakpoints.len() + 2); + interval_bounds.push(0.0); + interval_bounds.extend(breakpoints.iter().copied()); + interval_bounds.push(1.0); + + let mut stops = Vec::new(); + for interval in interval_bounds.windows(2) { + let start_offset = interval[0]; + let end_offset = interval[1]; + + let start_color = sample(start_offset); + stops.push(SvgGradientStop { + offset: start_offset, + color: start_color, + }); + + let end_color = sample(end_offset); + + approximate_gradient_interval( + &mut sample, + &mut stops, + GradientSample { + offset: start_offset, + color: start_color, + }, + GradientSample { + offset: end_offset, + color: end_color, + }, + tolerance.max(0.0), + 0, + ); + } + + stops +} + +#[derive(Clone, Copy)] +struct GradientSample { + offset: f32, + color: [f32; 4], +} + +fn approximate_gradient_interval( + sample: &mut impl FnMut(f32) -> [f32; 4], + stops: &mut Vec, + start: GradientSample, + end: GradientSample, + tolerance: f32, + depth: u8, +) { + let offset_delta = end.offset - start.offset; + let quarter_offset = start.offset + offset_delta * 0.25; + let mid_offset = start.offset + offset_delta * 0.5; + let three_quarter_offset = start.offset + offset_delta * 0.75; + + let quarter_color = sample(quarter_offset); + let mid_color = sample(mid_offset); + let three_quarter_color = sample(three_quarter_offset); + + if color_error(quarter_color, lerp_color(start.color, end.color, 0.25)) <= tolerance + && color_error(mid_color, lerp_color(start.color, end.color, 0.5)) <= tolerance + && color_error( + three_quarter_color, + lerp_color(start.color, end.color, 0.75), + ) <= tolerance + || depth == MAX_GRADIENT_SUBDIVISION_DEPTH + { + stops.push(SvgGradientStop { + offset: end.offset, + color: end.color, + }); + + return; + } + + let mid = GradientSample { + offset: mid_offset, + color: mid_color, + }; + + approximate_gradient_interval(sample, stops, start, mid, tolerance, depth + 1); + approximate_gradient_interval(sample, stops, mid, end, tolerance, depth + 1); +} + +fn lerp_color(c0: [f32; 4], c1: [f32; 4], t: f32) -> [f32; 4] { + [ + c0[0] + (c1[0] - c0[0]) * t, + c0[1] + (c1[1] - c0[1]) * t, + c0[2] + (c1[2] - c0[2]) * t, + c0[3] + (c1[3] - c0[3]) * t, + ] +} + +fn color_error(c0: [f32; 4], c1: [f32; 4]) -> f32 { + c0.iter() + .zip(c1) + .map(|(a, b)| (a - b).abs()) + .fold(0.0, f32::max) +} diff --git a/vendor/hayro-interpret/src/interpret/mod.rs b/vendor/hayro-interpret/src/interpret/mod.rs new file mode 100644 index 00000000..a0c6308e --- /dev/null +++ b/vendor/hayro-interpret/src/interpret/mod.rs @@ -0,0 +1,943 @@ +use crate::{BlendMode, FillRule}; +use crate::color::ColorSpace; +use crate::context::Context; +use crate::convert::{convert_line_cap, convert_line_join}; +use crate::device::Device; +use crate::font::{Font, FontData, FontQuery, StandardFont}; +use crate::interpret::path::{ + apply_pending_clip, close_path, fill_path, fill_path_impl, fill_stroke_path, stroke_path, +}; +use crate::interpret::state::{TextStateFont, handle_gs}; +use crate::interpret::text::TextRenderingMode; +use crate::pattern::{Pattern, ShadingPattern}; +use crate::shading::Shading; +use crate::util::{OptionLog, RectExt}; +use crate::x_object::{ + FormXObject, ImageXObject, XObject, draw_form_xobject, draw_image_xobject, draw_xobject, +}; +use hayro_syntax::content::TypedIter; +use hayro_syntax::content::ops::TypedInstruction; +use hayro_syntax::object::dict::keys::{ + ACTUAL_TEXT, ANNOTS, AP, AS, C, CA, F, MCID, N, OC, QUADPOINTS, RECT, SUBTYPE, +}; +use hayro_syntax::object::{Array, Dict, Object, Rect, Stream, dict_or_stream}; +use hayro_syntax::page::{ContentReader, Page, Resources}; +use hayro_syntax::MAX_DECODED_STREAM; +use kurbo::{Affine, BezPath, Point, Shape}; +use smallvec::smallvec; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub(crate) mod path; +pub(crate) mod state; +pub(crate) mod text; + +pub use state::ActiveTransferFunction; + +/// A callback function for resolving font queries. +/// +/// The first argument is the raw data, the second argument is the index in case the font +/// is a TTC, otherwise it should be 0. +pub type FontResolverFn = Arc Option<(FontData, u32)> + Send + Sync>; +/// A callback function for resolving cmap names to their files. +pub type CMapResolverFn = + Arc) -> Option<&'static [u8]> + Send + Sync>; +/// A callback function for resolving warnings during interpretation. +pub type WarningSinkFn = Arc; + +#[derive(Clone)] +/// Settings that should be applied during the interpretation process. +pub struct InterpreterSettings { + /// Nearly every PDF contains text. In most cases, PDF files embed the fonts they use, and + /// hayro can therefore read the font files and do all the processing needed. However, there + /// are two problems: + /// - Fonts don't _have_ to be embedded, it's possible that the PDF file only defines the basic + /// metadata of the font, like its name, but relies on the PDF processor to find that font + /// in its environment. + /// - The PDF specification requires a list of 14 fonts that should always be available to a + /// PDF processor. These include: + /// - Times New Roman (Normal, Bold, Italic, `BoldItalic`) + /// - Courier (Normal, Bold, Italic, `BoldItalic`) + /// - Helvetica (Normal, Bold, Italic, `BoldItalic`) + /// - `ZapfDingBats` + /// - Symbol + /// + /// Because of this, if any of the above situations occurs, this callback will be called, which + /// expects the data of an appropriate font to be returned, if available. If no such font is + /// provided, the text will most likely fail to render. + /// + /// For the font data, there are two different formats that are accepted: + /// - Any valid TTF/OTF font. + /// - A valid CFF font program. + /// + /// The following recommendations are given for the implementation of this callback function. + /// + /// For the standard fonts, in case the original fonts are available on the system, you should + /// just return those. Otherwise, for Helvetica, Courier and Times New Roman, the best alternative + /// are the corresponding fonts of the [Liberation font family](https://github.com/liberationfonts/liberation-fonts). + /// If you prefer smaller fonts, you can use the [Foxit CFF fonts](https://github.com/LaurenzV/hayro/tree/master/assets/standard_fonts), + /// which are much smaller but are missing glyphs for certain scripts. + /// + /// For the `Symbol` and `ZapfDingBats` fonts, you should also prefer the system fonts, and if + /// not available to you, you can, similarly to above, use the corresponding fonts from Foxit. + /// + /// If you don't want having to deal with this, you can just enable the `embed-fonts` feature + /// and use the default implementation of the callback. + pub font_resolver: FontResolverFn, + /// A callback for resolving cmaps that aren't embedded. + /// + /// When the PDF requires using a cmap that is not directly embedded in the PDF, + /// this callback will be called to attempt fetching the data of the file. + /// + /// When the `embed-cmaps` feature is enabled, this uses `load_embedded` + /// method from `hayro-cmap` by default, which embeds the cmap files for + /// all 61 predefined cmaps + /// that the PDF specification requires to be readily available on a system. + /// Otherwise, you can implement your custom logic for lazily fetching the + /// data. If you are fine not supporting such PDFs, you can simply pass a closure + /// that always returns `None`. + pub cmap_resolver: CMapResolverFn, + /// In certain cases, `hayro` will emit a warning in case an issue was encountered while interpreting + /// the PDF file. Providing a callback allows you to catch those warnings and handle them, if desired. + pub warning_sink: WarningSinkFn, + /// Whether annotations should be rendered as well. + /// + /// Note that this feature is currently not fully implemented yet, so some + /// annotations might be missing. + pub render_annotations: bool, + /// PdfCraft patch: hide comments (markup annotations) but keep form fields (`/Widget`) + /// and links, for the viewer's "Hide all comments". + pub hide_comments: bool, + /// PdfCraft patch: viewer overrides for optional content groups (object number, generation, + /// visible), applied on top of the document's default configuration (Layers panel toggles). + pub ocg_overrides: Arc>, + /// PdfCraft patch: once this is `true`, interpretation stops before the next content + /// operator. What was drawn is then incomplete, so only set it when the output will be + /// thrown away (a viewer's render whose result nobody can receive any more). + pub cancelled: Option>, + /// PdfCraft patch: a low-detail render for previews (page backdrops, thumbnails). Images + /// that would cover only a few device pixels are skipped: redrawing them at full detail + /// costs a decode and a resample each for pixels nobody can see. Never set it for a final + /// render (tiles, whole pages, print, export). + pub preview: bool, +} + +impl Default for InterpreterSettings { + fn default() -> Self { + Self { + #[cfg(not(feature = "embed-fonts"))] + font_resolver: Arc::new(|_| None), + #[cfg(feature = "embed-fonts")] + font_resolver: Arc::new(|query| match query { + FontQuery::Standard(s) => Some(s.get_font_data()), + FontQuery::Fallback(f) => Some(f.pick_standard_font().get_font_data()), + }), + #[cfg(feature = "embed-cmaps")] + cmap_resolver: Arc::new(hayro_cmap::load_embedded), + #[cfg(not(feature = "embed-cmaps"))] + cmap_resolver: Arc::new(|_| None), + warning_sink: Arc::new(|_| {}), + render_annotations: true, + hide_comments: false, + ocg_overrides: Arc::new(Vec::new()), + cancelled: None, + preview: false, + } + } +} + +#[derive(Copy, Clone, Debug)] +/// Warnings that can occur while interpreting a PDF file. +pub enum InterpreterWarning { + /// An unsupported font kind was encountered. + /// + /// Currently, only CID fonts with non-identity encoding are unsupported. + UnsupportedFont, + /// An image failed to decode. + ImageDecodeFailure, + /// PdfCraft patch (18): a page content stream was skipped after exhausting its safety budget. + ContentTruncated, +} + +/// interpret the contents of the page and render them into the device. +pub fn interpret_page<'a>( + page: &Page<'a>, + context: &mut Context<'a>, + device: &mut impl Device<'a>, +) { + let resources = page.resources(); + let mut content = page.content_reader(); + // PdfCraft patch: the page's own contents count against its content budget too, where they + // are decoded whole. Streamed content is only ever held a window at a time. + crate::context::charge_content(content.materialized_len()); + interpret_content(&mut content, resources, context, device); + + if context.settings.render_annotations + && let Some(annot_arr) = page.raw().get::>(ANNOTS) + { + for annot in annot_arr.iter::>() { + let flags = annot.get::(F).unwrap_or(0); + + // Annotation should be hidden (Hidden = bit 2, NoView = bit 6). + // PdfCraft patch: NoView was not honoured upstream. + if flags & (2 | 32) != 0 { + continue; + } + // PdfCraft patch: "Hide all comments" keeps widgets and links. + if context.settings.hide_comments + && !matches!(annot.get::>(SUBTYPE).as_deref(), Some(b"Widget" | b"Link")) + { + continue; + } + + // PdfCraft patch: `/N` may be a stream or a dictionary of appearance states selected + // by `/AS` (checkboxes, radio buttons, โ€ฆ). Upstream only handled the stream form. + let normal = annot.get::>(AP).and_then(|ap| { + ap.get::>(N).or_else(|| { + let states = ap.get::>(N)?; + let state = annot.get::>(AS)?; + states.get::>(state.as_ref()) + }) + }); + if let Some(apx) = normal.and_then(|o| FormXObject::new(&o)) { + let Some(rect) = annot.get::(RECT) else { + continue; + }; + + let annot_rect = rect.to_kurbo(); + // 12.5.5. Appearance streams + // "The algorithm outlined in this subclause shall be used + // to map from the coordinate system of the appearance XObject." + + // 1) The appearanceโ€™s bounding box (specified by its BBox entry) + // shall be transformed, using Matrix, to produce a + // quadrilateral with arbitrary orientation. The transformed + // appearance box is the smallest upright rectangle that + // encompasses this quadrilateral. + let transformed_rect = (apx.matrix + * kurbo::Rect::new( + apx.bbox[0] as f64, + apx.bbox[1] as f64, + apx.bbox[2] as f64, + apx.bbox[3] as f64, + ) + .to_path(0.1)) + .bounding_box(); + + // 2) A matrix A shall be computed that scales and translates + // the transformed appearance box to align with the edges + // of the annotationโ€™s rectangle (specified by the Rect entry). + // A maps the lower-left corner (the corner with the smallest + // x and y coordinates) and the upper-right corner (the + // corner with the greatest x and y coordinates) of the + // transformed appearance box to the corresponding corners + // of the annotationโ€™s rectangle. + // PdfCraft patch: the translation is taken after scaling (it was + // `/Rect.x0 - box.x0`, right only for a box at the origin or a scale of 1). + let (sx, sy) = ( + annot_rect.width() / transformed_rect.width(), + annot_rect.height() / transformed_rect.height(), + ); + let affine = Affine::new([ + sx, + 0.0, + 0.0, + sy, + annot_rect.x0 - sx * transformed_rect.x0, + annot_rect.y0 - sy * transformed_rect.y0, + ]); + + // PdfCraft patch: a /BBox or /Matrix that collapses the appearance box to a line + // or a point leaves nothing to map onto /Rect: the scales above divide by zero, + // and the infinite or NaN matrix reached the device with everything the + // appearance draws. Clipped to such a box the appearance shows nothing, so skip + // it, as when the mapping overflows. + if !(transformed_rect.is_finite() + && transformed_rect.width() > 0.0 + && transformed_rect.height() > 0.0 + && affine.is_finite()) + { + continue; + } + + // 3) Matrix shall be concatenated with A to form a matrix + // AA that maps from the appearanceโ€™s coordinate system to + // the annotationโ€™s rectangle in default user space. + context.save_state(); + context.pre_concat_affine(affine); + context.push_root_transform(); + + draw_form_xobject(resources, &apx, context, device); + context.pop_root_transform(); + context.restore_state(device); + } else if annot.get::>(SUBTYPE).as_deref() == Some(b"Highlight") { + // PdfCraft patch: a Highlight without a usable normal appearance was not drawn. + draw_highlight_without_appearance(&annot, context, device); + } + } + } +} + +/// PdfCraft patch: the normal appearance of a Highlight annotation that has none (ISO 32000-2 +/// ยง12.5.6.10), drawn as PdfCraft draws its own highlights (pdfcraft-annot): each /QuadPoints +/// quadrilateral filled with /C at /CA opacity, blended with Multiply so the marked-up content +/// stays legible. /QuadPoints that aren't groups of eight numbers, or a /C that isn't a gray, RGB +/// or CMYK colour (absent or empty means transparent), draw nothing. +fn draw_highlight_without_appearance<'a>( + annot: &Dict<'a>, + context: &mut Context<'a>, + device: &mut impl Device<'a>, +) { + let numbers = |key| -> Option> { + annot + .get::>(key)? + .iter::>() + .map(|o| match o { + Object::Number(n) => Some(n.as_f32()), + _ => None, + }) + .collect() + }; + let Some(quad_points) = numbers(QUADPOINTS).filter(|q| !q.is_empty() && q.len() % 8 == 0) + else { + return; + }; + let (color_space, color) = match numbers(C).as_deref() { + Some(&[g]) => (ColorSpace::device_gray(), smallvec![g]), + Some(&[r, g, b]) => (ColorSpace::device_rgb(), smallvec![r, g, b]), + Some(&[c, m, y, k]) => (ColorSpace::device_cmyk(), smallvec![c, m, y, k]), + _ => return, + }; + + let mut path = BezPath::new(); + // Corners in /QuadPoints order: upper left, upper right, lower left, lower right. + for &[x1, y1, x2, y2, x3, y3, x4, y4] in quad_points.as_chunks::<8>().0 { + path.move_to((x1 as f64, y1 as f64)); + path.line_to((x2 as f64, y2 as f64)); + path.line_to((x4 as f64, y4 as f64)); + path.line_to((x3 as f64, y3 as f64)); + path.close_path(); + } + + context.save_state(); + let state = &mut context.get_mut().graphics_state; + state.none_stroke_cs = color_space; + state.non_stroke_color = color; + state.non_stroke_pattern = None; + state.non_stroke_alpha = annot.get::(CA).unwrap_or(1.0).clamp(0.0, 1.0); + state.soft_mask = None; + state.blend_mode = BlendMode::Multiply; + *context.path_mut() = path; + fill_path(context, device, FillRule::NonZero); + context.restore_state(device); +} + +/// Interprets the instructions that `ops` yields, until they run out or the caller cancels. It runs +/// once per window of a content stream, between one `save_state` and its restore (see +/// `interpret_content`). +fn interpret_window<'a>( + ops: &mut TypedIter<'_>, + resources: &Resources<'a>, + context: &mut Context<'a>, + device: &mut impl Device<'a>, +) { + while let Some(op) = ops.next() { + // PdfCraft patch: stop when the caller cancelled (see `InterpreterSettings::cancelled`). + if context.settings.cancelled.as_ref().is_some_and(|c| c.load(Ordering::Relaxed)) { + break; + } + match op { + TypedInstruction::SaveState(_) => context.save_state(), + TypedInstruction::StrokeColorDeviceRgb(s) => { + context.get_mut().graphics_state.stroke_cs = ColorSpace::device_rgb(); + context.get_mut().graphics_state.stroke_color = + smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()]; + context.get_mut().graphics_state.stroke_pattern = None; + } + TypedInstruction::StrokeColorDeviceGray(s) => { + context.get_mut().graphics_state.stroke_cs = ColorSpace::device_gray(); + context.get_mut().graphics_state.stroke_color = smallvec![s.0.as_f32()]; + context.get_mut().graphics_state.stroke_pattern = None; + } + TypedInstruction::StrokeColorCmyk(s) => { + context.get_mut().graphics_state.stroke_cs = ColorSpace::device_cmyk(); + context.get_mut().graphics_state.stroke_color = + smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()]; + context.get_mut().graphics_state.stroke_pattern = None; + } + TypedInstruction::LineWidth(w) => { + context.get_mut().graphics_state.stroke_props.line_width = w.0.as_f32(); + } + TypedInstruction::LineCap(c) => { + context.get_mut().graphics_state.stroke_props.line_cap = convert_line_cap(c); + } + TypedInstruction::LineJoin(j) => { + context.get_mut().graphics_state.stroke_props.line_join = convert_line_join(j); + } + TypedInstruction::MiterLimit(l) => { + context.get_mut().graphics_state.stroke_props.miter_limit = l.0.as_f32(); + } + TypedInstruction::Transform(t) => { + context.pre_concat_transform(t); + } + TypedInstruction::RectPath(r) => { + let rect = kurbo::Rect::new( + r.0.as_f64(), + r.1.as_f64(), + r.0.as_f64() + r.2.as_f64(), + r.1.as_f64() + r.3.as_f64(), + ) + .to_path(0.1); + context.path_mut().extend(rect); + } + TypedInstruction::MoveTo(m) => { + let p = Point::new(m.0.as_f64(), m.1.as_f64()); + *(context.last_point_mut()) = p; + *(context.sub_path_start_mut()) = p; + context.path_mut().move_to(p); + } + TypedInstruction::FillPathEvenOdd(_) => { + fill_path(context, device, FillRule::EvenOdd); + } + TypedInstruction::FillPathNonZero(_) => { + fill_path(context, device, FillRule::NonZero); + } + TypedInstruction::FillPathNonZeroCompatibility(_) => { + fill_path(context, device, FillRule::NonZero); + } + TypedInstruction::FillAndStrokeEvenOdd(_) => { + fill_stroke_path(context, device, FillRule::EvenOdd); + } + TypedInstruction::FillAndStrokeNonZero(_) => { + fill_stroke_path(context, device, FillRule::NonZero); + } + TypedInstruction::CloseAndStrokePath(_) => { + close_path(context); + stroke_path(context, device); + } + TypedInstruction::CloseFillAndStrokeEvenOdd(_) => { + close_path(context); + fill_stroke_path(context, device, FillRule::EvenOdd); + } + TypedInstruction::CloseFillAndStrokeNonZero(_) => { + close_path(context); + fill_stroke_path(context, device, FillRule::NonZero); + } + TypedInstruction::NonStrokeColorDeviceGray(s) => { + context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_gray(); + context.get_mut().graphics_state.non_stroke_color = smallvec![s.0.as_f32()]; + context.get_mut().graphics_state.non_stroke_pattern = None; + } + TypedInstruction::NonStrokeColorDeviceRgb(s) => { + context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_rgb(); + context.get_mut().graphics_state.non_stroke_color = + smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32()]; + context.get_mut().graphics_state.non_stroke_pattern = None; + } + TypedInstruction::NonStrokeColorCmyk(s) => { + context.get_mut().graphics_state.none_stroke_cs = ColorSpace::device_cmyk(); + context.get_mut().graphics_state.non_stroke_color = + smallvec![s.0.as_f32(), s.1.as_f32(), s.2.as_f32(), s.3.as_f32()]; + context.get_mut().graphics_state.non_stroke_pattern = None; + } + TypedInstruction::LineTo(m) => { + if !context.path().elements().is_empty() { + let last_point = *context.last_point(); + let mut p = Point::new(m.0.as_f64(), m.1.as_f64()); + *(context.last_point_mut()) = p; + if last_point == p { + // Add a small delta so that zero width lines can still have a round stroke. + p.x += 0.0001; + } + + context.path_mut().line_to(p); + } + } + TypedInstruction::CubicTo(c) => { + if !context.path().elements().is_empty() { + let p1 = Point::new(c.0.as_f64(), c.1.as_f64()); + let p2 = Point::new(c.2.as_f64(), c.3.as_f64()); + let p3 = Point::new(c.4.as_f64(), c.5.as_f64()); + + *(context.last_point_mut()) = p3; + + context.path_mut().curve_to(p1, p2, p3); + } + } + TypedInstruction::CubicStartTo(c) => { + if !context.path().elements().is_empty() { + let p1 = *context.last_point(); + let p2 = Point::new(c.0.as_f64(), c.1.as_f64()); + let p3 = Point::new(c.2.as_f64(), c.3.as_f64()); + + *(context.last_point_mut()) = p3; + + context.path_mut().curve_to(p1, p2, p3); + } + } + TypedInstruction::CubicEndTo(c) => { + if !context.path().elements().is_empty() { + let p2 = Point::new(c.0.as_f64(), c.1.as_f64()); + let p3 = Point::new(c.2.as_f64(), c.3.as_f64()); + + *(context.last_point_mut()) = p3; + + context.path_mut().curve_to(p2, p3, p3); + } + } + TypedInstruction::ClosePath(_) => { + close_path(context); + } + TypedInstruction::SetGraphicsState(gs) => { + if let Some(gs) = resources + .get_ext_g_state(gs.0) + .warn_none(&format!("failed to get extgstate {}", gs.0.as_str())) + { + handle_gs(&gs, context, resources); + } + } + TypedInstruction::StrokePath(_) => { + stroke_path(context, device); + } + TypedInstruction::EndPath(_) => { + apply_pending_clip(context, device); + context.path_mut().truncate(0); + } + TypedInstruction::NonStrokeColor(c) => { + let gs = &mut context.get_mut().graphics_state; + gs.non_stroke_color = c.0.into_iter().map(|n| n.as_f32()).collect(); + gs.non_stroke_pattern = None; + } + TypedInstruction::StrokeColor(c) => { + let gs = &mut context.get_mut().graphics_state; + gs.stroke_color = c.0.into_iter().map(|n| n.as_f32()).collect(); + gs.stroke_pattern = None; + } + TypedInstruction::ClipNonZero(_) => { + *(context.clip_mut()) = Some(FillRule::NonZero); + } + TypedInstruction::ClipEvenOdd(_) => { + *(context.clip_mut()) = Some(FillRule::EvenOdd); + } + TypedInstruction::RestoreState(_) => context.restore_state(device), + TypedInstruction::FlatnessTolerance(_) => { + // Ignore for now. + } + TypedInstruction::ColorSpaceStroke(c) => { + let cs = if let Some(named) = ColorSpace::new_from_name(c.0) { + named + } else { + context + .get_color_space(resources, c.0) + .unwrap_or(ColorSpace::device_gray()) + }; + + if !cs.is_pattern() { + context.get_mut().graphics_state.stroke_pattern = None; + } + context.get_mut().graphics_state.stroke_color = cs.initial_color(); + context.get_mut().graphics_state.stroke_cs = cs; + } + TypedInstruction::ColorSpaceNonStroke(c) => { + let cs = if let Some(named) = ColorSpace::new_from_name(c.0) { + named + } else { + context + .get_color_space(resources, c.0) + .unwrap_or(ColorSpace::device_gray()) + }; + + if !cs.is_pattern() { + context.get_mut().graphics_state.non_stroke_pattern = None; + } + context.get_mut().graphics_state.non_stroke_color = cs.initial_color(); + context.get_mut().graphics_state.none_stroke_cs = cs; + } + TypedInstruction::DashPattern(p) => { + context.get_mut().graphics_state.stroke_props.dash_offset = p.1.as_f32(); + // kurbo apparently cannot properly deal with offsets that are exactly 0. + context.get_mut().graphics_state.stroke_props.dash_array = + p.0.iter::() + .map(|n| if n == 0.0 { 0.01 } else { n }) + .collect(); + } + TypedInstruction::RenderingIntent(_) => { + // Ignore for now. + } + TypedInstruction::NonStrokeColorNamed(n) => { + context.get_mut().graphics_state.non_stroke_color = + n.0.into_iter().map(|n| n.as_f32()).collect(); + context.get_mut().graphics_state.non_stroke_pattern = n.1.and_then(|name| { + resources + .get_pattern(name) + .and_then(|d| Pattern::new(d, context, resources)) + }); + } + TypedInstruction::StrokeColorNamed(n) => { + context.get_mut().graphics_state.stroke_color = + n.0.into_iter().map(|n| n.as_f32()).collect(); + context.get_mut().graphics_state.stroke_pattern = n.1.and_then(|name| { + resources + .get_pattern(name) + .and_then(|d| Pattern::new(d, context, resources)) + }); + } + TypedInstruction::BeginMarkedContentWithProperties(bdc) => { + // Properties can be either: + // 1. A Name that references an entry in the Resources/Properties dictionary + // 2. An inline dictionary with an OC key + + let mcid = dict_or_stream(bdc.1).and_then(|(props, _)| props.get::(MCID)); + + let oc = bdc + .1 + .clone() + .into_name() + .and_then(|name| { + let r = resources.properties.get_ref(name.as_ref())?; + let d = resources + .properties + .get::>(name) + .unwrap_or_default(); + Some((d, r)) + }) + .or_else(|| { + let (props, _) = dict_or_stream(bdc.1)?; + let r = props.get_ref(OC)?; + let d = props.get::>(OC).unwrap_or_default(); + Some((d, r)) + }); + + if let Some((dict, oc_ref)) = oc { + context.ocg_state.begin_ocg(&dict, oc_ref.into()); + } else { + context.ocg_state.begin_marked_content(); + } + + device.begin_marked_content(bdc.0, mcid); + + // PdfCraft patch (25): hand the device the sequence's `/ActualText`, from an inline + // property list or one named in the resources' `/Properties`. + let actual = dict_or_stream(bdc.1) + .map(|(props, _)| props.clone()) + .or_else(|| { + let name = bdc.1.clone().into_name()?; + resources.properties.get::>(name) + }) + .and_then(|props| props.get::>(ACTUAL_TEXT)); + if let Some(text) = actual { + device.actual_text(text.as_bytes()); + } + } + TypedInstruction::MarkedContentPointWithProperties(_) => {} + TypedInstruction::EndMarkedContent(_) => { + context.ocg_state.end_marked_content(); + device.end_marked_content(); + } + TypedInstruction::MarkedContentPoint(_) => {} + TypedInstruction::BeginMarkedContent(bmc) => { + context.ocg_state.begin_marked_content(); + device.begin_marked_content(bmc.0, None); + } + TypedInstruction::BeginText(_) => { + context.get_mut().text_state.text_matrix = Affine::IDENTITY; + context.get_mut().text_state.text_line_matrix = Affine::IDENTITY; + } + TypedInstruction::SetTextMatrix(m) => { + let m = Affine::new([ + m.0.as_f64(), + m.1.as_f64(), + m.2.as_f64(), + m.3.as_f64(), + m.4.as_f64(), + m.5.as_f64(), + ]); + context.get_mut().text_state.text_line_matrix = m; + context.get_mut().text_state.text_matrix = m; + } + TypedInstruction::EndText(_) => { + let has_outline = context + .get() + .text_state + .clip_paths + .segments() + .next() + .is_some(); + + if has_outline { + let clip_path = context.get().ctm * context.get().text_state.clip_paths.clone(); + + context.push_clip_path(clip_path, FillRule::NonZero, device); + } + + context.get_mut().text_state.clip_paths.truncate(0); + } + TypedInstruction::TextFont(t) => { + let name = t.0; + + // In case we are unable to resolve the font, two scenarios: + // 1) If the font doesn't exist in the first place in the resource dictionary, + // assume Helvetica (this seems to be what other PDF viewers do). + // 2) In case it's `None` because we were unable to resolve the font + // (for whatever reason), leave it as `None`. Better showing no + // text at all than garbage text. + let font = if let Some(font_dict) = resources.get_font(name) { + context.resolve_font(&font_dict) + } else { + Font::new_standard(StandardFont::Helvetica, &context.settings.font_resolver) + .map(TextStateFont::Fallback) + }; + + context.get_mut().text_state.font_size = t.1.as_f32(); + context.get_mut().text_state.font = font; + } + TypedInstruction::ShowText(s) => { + if context.get().text_state.font.is_none() { + // Even if no explicit font was set, we try to assume Helvetica. Acrobat + // seems to do the same. + context.get_mut().text_state.font = Font::new_standard( + StandardFont::Helvetica, + &context.settings.font_resolver, + ) + .map(TextStateFont::Fallback); + } + + text::show_text_string(context, device, resources, s.0); + } + TypedInstruction::ShowTexts(s) => { + if context.get().text_state.font.is_none() { + // Even if no explicit font was set, we try to assume Helvetica. Acrobat + // seems to do the same. + context.get_mut().text_state.font = Font::new_standard( + StandardFont::Helvetica, + &context.settings.font_resolver, + ) + .map(TextStateFont::Fallback); + } + + text::show_text_array(context, device, resources, s.0); + } + TypedInstruction::HorizontalScaling(h) => { + context.get_mut().text_state.horizontal_scaling = h.0.as_f32(); + } + TypedInstruction::TextLeading(tl) => { + context.get_mut().text_state.leading = tl.0.as_f32(); + } + TypedInstruction::CharacterSpacing(c) => { + context.get_mut().text_state.char_space = c.0.as_f32(); + } + TypedInstruction::WordSpacing(w) => { + context.get_mut().text_state.word_space = w.0.as_f32(); + } + TypedInstruction::NextLine(n) => { + let (tx, ty) = (n.0.as_f64(), n.1.as_f64()); + text::next_line(context, tx, ty); + } + TypedInstruction::NextLineUsingLeading(_) => { + text::next_line(context, 0.0, -context.get().text_state.leading as f64); + } + TypedInstruction::NextLineAndShowText(n) => { + text::next_line(context, 0.0, -context.get().text_state.leading as f64); + text::show_text_string(context, device, resources, n.0); + } + TypedInstruction::TextRenderingMode(r) => { + let mode = match r.0.as_i64() { + 0 => TextRenderingMode::Fill, + 1 => TextRenderingMode::Stroke, + 2 => TextRenderingMode::FillStroke, + 3 => TextRenderingMode::Invisible, + 4 => TextRenderingMode::FillAndClip, + 5 => TextRenderingMode::StrokeAndClip, + 6 => TextRenderingMode::FillAndStrokeAndClip, + 7 => TextRenderingMode::Clip, + _ => { + warn!("unknown text rendering mode {}", r.0.as_i64()); + + TextRenderingMode::Fill + } + }; + + context.get_mut().text_state.render_mode = mode; + } + TypedInstruction::NextLineAndSetLeading(n) => { + let (tx, ty) = (n.0.as_f64(), n.1.as_f64()); + context.get_mut().text_state.leading = -ty as f32; + text::next_line(context, tx, ty); + } + TypedInstruction::ShapeGlyph(_) => {} + TypedInstruction::XObject(x) => { + let cache = context.interpreter_cache.object_cache.clone(); + // PdfCraft patch: decoded images are cached per quality (preview or final). + let decoded_cache = context + .interpreter_cache + .decoded_cache + .for_render(context.settings.preview); + let transfer_function = context.get().graphics_state.transfer_function.clone(); + if let Some(x_object) = resources.get_x_object(x.0).and_then(|s| { + XObject::new( + &s, + &context.settings.warning_sink, + &cache, + Some(&decoded_cache), + transfer_function.clone(), + ) + }) { + draw_xobject(&x_object, resources, context, device); + } + } + TypedInstruction::InlineImage(i) => { + let warning_sink = context.settings.warning_sink.clone(); + let transfer_function = context.get().graphics_state.transfer_function.clone(); + let cache = context.interpreter_cache.object_cache.clone(); + // PdfCraft patch: inline images are not cached (see `DecodedImageKey`). + if let Some(x_object) = ImageXObject::new( + i.0, + |name| context.get_color_space(resources, name), + &warning_sink, + &cache, + None, + false, + transfer_function, + ) { + draw_image_xobject(&x_object, context, device); + } + } + TypedInstruction::TextRise(t) => { + context.get_mut().text_state.rise = t.0.as_f32(); + } + TypedInstruction::Shading(s) => { + if !context.ocg_state.is_visible() { + continue; + } + + let transfer_function = context.get().graphics_state.transfer_function.clone(); + + if let Some(sp) = resources + .get_shading(s.0) + .and_then(|o| { + let (dict, stream) = dict_or_stream(&o)?; + Shading::new(dict, stream, &context.interpreter_cache.object_cache) + }) + .map(|s| { + Pattern::Shading(ShadingPattern { + shading: Arc::new(s), + matrix: Affine::IDENTITY, + opacity: context.get().graphics_state.non_stroke_alpha, + transfer_function: transfer_function.clone(), + }) + }) + { + context.save_state(); + context.push_root_transform(); + let st = context.get_mut(); + st.graphics_state.non_stroke_pattern = Some(sp); + st.graphics_state.none_stroke_cs = ColorSpace::pattern(); + + device.set_soft_mask(st.graphics_state.soft_mask.clone()); + device.set_blend_mode(st.graphics_state.blend_mode); + + let bbox = context.bbox().to_path(0.1); + let inverted_bbox = context.get().ctm.inverse() * bbox; + fill_path_impl(context, device, FillRule::NonZero, Some(&inverted_bbox)); + + context.pop_root_transform(); + context.restore_state(device); + } else { + warn!("failed to process shading"); + } + } + TypedInstruction::BeginCompatibility(_) => {} + TypedInstruction::EndCompatibility(_) => {} + TypedInstruction::ColorGlyph(_) => {} + TypedInstruction::ShowTextWithParameters(t) => { + context.get_mut().text_state.word_space = t.0.as_f32(); + context.get_mut().text_state.char_space = t.1.as_f32(); + text::next_line(context, 0.0, -context.get().text_state.leading as f64); + text::show_text_string(context, device, resources, t.2); + } + _ => { + warn!("failed to read an operator"); + } + } + } +} + +/// Interpret the instructions from `ops` and render them into the device. +pub fn interpret<'a>( + mut ops: TypedIter<'_>, + resources: &Resources<'a>, + context: &mut Context<'a>, + device: &mut impl Device<'a>, +) { + let num_states = context.num_states(); + + context.save_state(); + + interpret_window(&mut ops, resources, context, device); + + while context.num_states() > num_states { + context.restore_state(device); + } + // PdfCraft patch (18): say that content was skipped, so a truncated page isn't a clean render. + if crate::context::content_was_truncated() { + (context.settings.warning_sink)(InterpreterWarning::ContentTruncated); + } +} + +/// PdfCraft patch: the bytes of a window of page content. A window grows only while an instruction +/// at its end is incomplete. +const CONTENT_WINDOW: usize = 64 << 10; + +/// PdfCraft patch: interprets the content of a page a window at a time, so the decoded content is +/// never all in memory (see `ContentReader`). A window ends at an instruction that is not complete +/// yet: its bytes are kept, and the next window starts with them. +fn interpret_content<'a>( + content: &mut ContentReader<'_>, + resources: &Resources<'a>, + context: &mut Context<'a>, + device: &mut impl Device<'a>, +) { + let num_states = context.num_states(); + + context.save_state(); + + let mut window = Vec::with_capacity(CONTENT_WINDOW); + let mut exhausted = false; + let mut cut = false; + loop { + let (committed, needs_more) = { + let mut ops = TypedIter::new_window(&window, exhausted); + interpret_window(&mut ops, resources, context, device); + (ops.committed(), ops.needs_more()) + }; + window.drain(..committed); + + let cancelled = context + .settings + .cancelled + .as_ref() + .is_some_and(|c| c.load(Ordering::Relaxed)); + if !needs_more || cancelled || exhausted { + break; + } + // A window that is still incomplete at its end waits for more of the stream. It doubles + // while it waits, so one long instruction costs time in proportion to its length, and it + // stops growing at `MAX_DECODED_STREAM`, the most any one stream may decode to. + if window.len() >= MAX_DECODED_STREAM { + cut = true; + break; + } + let want = window.len().max(CONTENT_WINDOW); + if content.read_into(&mut window, want) == 0 { + exhausted = true; + } + } + + while context.num_states() > num_states { + context.restore_state(device); + } + // PdfCraft patch (18): say that content was skipped, so a truncated page isn't a clean render. + if cut || content.truncated() || crate::context::content_was_truncated() { + (context.settings.warning_sink)(InterpreterWarning::ContentTruncated); + } +} diff --git a/vendor/hayro-interpret/src/interpret/path.rs b/vendor/hayro-interpret/src/interpret/path.rs new file mode 100644 index 00000000..838ea099 --- /dev/null +++ b/vendor/hayro-interpret/src/interpret/path.rs @@ -0,0 +1,178 @@ +use crate::color::{Color, ColorSpace}; +use crate::context::{Context, path_as_rect}; +use crate::device::Device; +use crate::util::{BezPathExt, Float32Ext}; +use crate::{FillRule, Paint, PathDrawMode, StrokeProps}; +use kurbo::{BezPath, Cap, Join, PathEl}; +use smallvec::smallvec; + +pub(crate) fn fill_path<'a>( + context: &mut Context<'a>, + device: &mut impl Device<'a>, + fill_rule: FillRule, +) { + fill_path_impl(context, device, fill_rule, None); + apply_pending_clip(context, device); + + context.path_mut().truncate(0); +} + +pub(crate) fn stroke_path<'a>(context: &mut Context<'a>, device: &mut impl Device<'a>) { + stroke_path_impl(context, device, None); + apply_pending_clip(context, device); + + context.path_mut().truncate(0); +} + +pub(crate) fn fill_stroke_path<'a>( + context: &mut Context<'a>, + device: &mut impl Device<'a>, + fill_rule: FillRule, +) { + fill_path_impl(context, device, fill_rule, None); + stroke_path_impl(context, device, None); + apply_pending_clip(context, device); + + context.path_mut().truncate(0); +} + +/// PdfCraft patch: a clip set by `W` / `W*` takes effect once the path-painting operator that +/// ends the path has painted it, whichever operator that is (ISO 32000-2 ยง8.5.4). Upstream +/// applied it only on `n`, and otherwise left it pending for some later, unrelated `n`. +pub(crate) fn apply_pending_clip<'a>(context: &mut Context<'a>, device: &mut impl Device<'a>) { + if let Some(rule) = context.clip_mut().take() + && !context.path().elements().is_empty() + { + let clip_path = context.get().ctm * context.path().clone(); + context.push_clip_path(clip_path, rule, device); + } +} + +pub(crate) fn close_path(context: &mut Context<'_>) { + // This is necessary to prevent artifacts (see for example issue 157), + // but it does cause some weird lines (maybe conflation artifacts) in + // pdftc_900k_0907. + if context.path().elements().last() != Some(&PathEl::ClosePath) && !context.path().is_empty() { + context.path_mut().close_path(); + + *(context.last_point_mut()) = *context.sub_path_start(); + } +} + +pub(crate) fn fill_path_impl<'a>( + context: &mut Context<'a>, + device: &mut impl Device<'a>, + fill_rule: FillRule, + path: Option<&BezPath>, +) { + if !context.ocg_state.is_visible() { + return; + } + + let base_transform = context.get().ctm; + + let paint = get_paint(context, false); + device.set_soft_mask(context.get().graphics_state.soft_mask.clone()); + device.set_blend_mode(context.get().graphics_state.blend_mode); + + let mut draw = |path: &BezPath| { + // pdf.js issue 4260: Replace zero-sized paths with a small stroke instead. + let bbox = path.fast_bounding_box(); + + match ( + (bbox.width() as f32).is_nearly_zero(), + (bbox.height() as f32).is_nearly_zero(), + ) { + (false, false) => { + let draw_mode = PathDrawMode::Fill(fill_rule); + if let Some(rect) = path_as_rect(path) { + device.draw_rect(&rect, base_transform, &paint, &draw_mode); + } else { + device.draw_path(path, base_transform, &paint, &draw_mode); + } + } + _ => { + let mut path = BezPath::new(); + path.move_to((bbox.x0, bbox.y0)); + path.line_to((bbox.x1, bbox.y1)); + + let stroke_props = StrokeProps { + // TODO: Make dependent on transform? + line_width: 0.001, + line_join: Join::Bevel, + line_cap: Cap::Butt, + ..Default::default() + }; + + device.draw_path( + &path, + base_transform, + &paint, + &PathDrawMode::Stroke(stroke_props), + ); + } + }; + }; + + match path { + None => draw(context.path()), + Some(path) => draw(path), + }; +} + +pub(crate) fn stroke_path_impl<'a>( + context: &mut Context<'a>, + device: &mut impl Device<'a>, + path: Option<&BezPath>, +) { + if !context.ocg_state.is_visible() { + return; + } + + let base_transform = context.get().ctm; + + let stroke_props = context.stroke_props(); + device.set_soft_mask(context.get().graphics_state.soft_mask.clone()); + device.set_blend_mode(context.get().graphics_state.blend_mode); + let paint = get_paint(context, true); + + let path = path.unwrap_or(context.path()); + let draw_mode = PathDrawMode::Stroke(stroke_props); + + if let Some(rect) = path_as_rect(path) { + device.draw_rect(&rect, base_transform, &paint, &draw_mode); + } else { + device.draw_path(path, base_transform, &paint, &draw_mode); + } +} + +pub(crate) fn get_paint<'a>(context: &Context<'a>, is_stroke: bool) -> Paint<'a> { + let data = if is_stroke { + context.get().stroke_data() + } else { + context.get().non_stroke_data() + }; + + if data.color_space.is_pattern() || data.pattern.is_some() { + if let Some(mut pattern) = data.pattern { + if let Some(tf) = &data.transfer_function { + pattern.set_transfer_function(tf.clone()); + } + + pattern.pre_concat_transform(context.root_transform()); + + Paint::Pattern(Box::new(pattern)) + } else { + // Pattern was likely invalid, use transparent paint. + Paint::Color(Color::new(ColorSpace::device_gray(), smallvec![0.0], 0.0)) + } + } else { + let color = Color::new(data.color_space, data.color, data.alpha); + + if let Some(tf) = &data.transfer_function { + Paint::Color(Color::from_rgba(tf.apply(&color.to_rgba()))) + } else { + Paint::Color(color) + } + } +} diff --git a/vendor/hayro-interpret/src/interpret/state.rs b/vendor/hayro-interpret/src/interpret/state.rs new file mode 100644 index 00000000..95b773fb --- /dev/null +++ b/vendor/hayro-interpret/src/interpret/state.rs @@ -0,0 +1,440 @@ +use crate::StrokeProps; +use crate::color::{AlphaColor, ColorComponents, ColorSpace}; +use crate::context::Context; +use crate::convert::{convert_line_cap, convert_line_join}; +use crate::font::{Font, UNITS_PER_EM}; +use crate::function::Function; +use crate::interpret::text::TextRenderingMode; +use crate::pattern::Pattern; +use crate::soft_mask::SoftMask; +use crate::types::BlendMode; +use crate::util::OptionLog; +use hayro_syntax::content::ops::{LineCap, LineJoin}; +use hayro_syntax::object::dict::keys::{FONT, SMASK, TR, TR2}; +use hayro_syntax::object::{Array, Dict, Name, Number, Object}; +use hayro_syntax::page::Resources; +use kurbo::{Affine, BezPath, Vec2}; +use smallvec::smallvec; +use std::ops::Deref; + +/// A transfer function. +#[derive(Clone, Debug)] +pub enum ActiveTransferFunction { + /// A single transfer function applied to all components. + Single(Function), + /// Four transfer functions, one for each component. + Four([Function; 4]), +} + +impl ActiveTransferFunction { + /// Apply the transfer function to the RGB channels of an RGBA color. + /// The alpha channel is left unchanged. + pub fn apply(&self, color: &AlphaColor) -> AlphaColor { + let mut rgba = color.components(); + + match self { + Self::Single(f) => { + for c in &mut rgba[..3] { + if let Some(out) = f.eval(smallvec![*c]) { + *c = out[0]; + } + } + } + Self::Four(functions) => { + for (i, f) in functions[..3].iter().enumerate() { + if let Some(out) = f.eval(smallvec![rgba[i]]) { + rgba[i] = out[0]; + } + } + } + } + + AlphaColor::new(rgba) + } +} + +#[derive(Clone, Debug)] +pub(crate) enum ClipType { + Dummy, + Real, +} + +#[derive(Clone, Debug)] +pub(crate) struct State<'a> { + // Note that the text state and ctm are theoretically part of the graphics state, + // but we keep them separate for simplicity. + pub(crate) graphics_state: GraphicsState<'a>, + pub(crate) text_state: TextState<'a>, + pub(crate) ctm: Affine, + // Strictly speaking not part of the graphics state, but we keep it there for + // consistency. + pub(crate) clips: Vec, +} + +impl Default for State<'_> { + fn default() -> Self { + State { + ctm: Affine::IDENTITY, + clips: vec![], + text_state: TextState::default(), + graphics_state: GraphicsState::default(), + } + } +} + +impl<'a> State<'a> { + pub(crate) fn new(initial_transform: Affine) -> Self { + Self { + ctm: initial_transform, + ..Self::default() + } + } + + pub(crate) fn stroke_data(&self) -> PaintData<'a> { + PaintData { + alpha: self.graphics_state.stroke_alpha, + color: self.graphics_state.stroke_color.clone(), + color_space: self.graphics_state.stroke_cs.clone(), + pattern: self.graphics_state.stroke_pattern.clone(), + transfer_function: self.graphics_state.transfer_function.clone(), + } + } + + pub(crate) fn non_stroke_data(&self) -> PaintData<'a> { + PaintData { + alpha: self.graphics_state.non_stroke_alpha, + color: self.graphics_state.non_stroke_color.clone(), + color_space: self.graphics_state.none_stroke_cs.clone(), + pattern: self.graphics_state.non_stroke_pattern.clone(), + transfer_function: self.graphics_state.transfer_function.clone(), + } + } +} + +#[derive(Clone, Debug)] +pub(crate) enum TextStateFont<'a> { + /// The font in the text state was explicitly set and resolves to a valid + /// font. + Font(Font<'a>), + /// The font was not set or an invalid font was set. + Fallback(Font<'a>), +} + +impl<'a> Deref for TextStateFont<'a> { + type Target = Font<'a>; + + fn deref(&self) -> &Self::Target { + match self { + TextStateFont::Font(f) => f, + TextStateFont::Fallback(f) => f, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct TextState<'a> { + pub(crate) char_space: f32, + pub(crate) word_space: f32, + // Note that this stores 1/100 of the actual scaling. + pub(crate) horizontal_scaling: f32, + pub(crate) leading: f32, + pub(crate) font: Option>, + pub(crate) font_size: f32, + pub(crate) rise: f32, + pub(crate) render_mode: TextRenderingMode, + + pub(crate) text_matrix: Affine, + pub(crate) text_line_matrix: Affine, + + // When setting the text rendering mode to `clip`, the glyphs should instead be collected + // as paths and then applied as 1 single clip path. This field stores those clip paths. + pub(crate) clip_paths: BezPath, +} + +impl<'a> TextState<'a> { + fn temp_transform(&self) -> Affine { + Affine::new([ + self.font_size as f64 * self.horizontal_scaling() as f64, + 0.0, + 0.0, + self.font_size as f64, + 0.0, + self.rise as f64, + ]) + } + + fn horizontal_scaling(&self) -> f32 { + self.horizontal_scaling / 100.0 + } + + fn font_horizontal(&self) -> bool { + self.font + .as_ref() + .map(|f| f.is_horizontal()) + .unwrap_or(false) + } + + pub(crate) fn apply_adjustment(&mut self, adjustment: f32) { + let horizontal = self.font_horizontal(); + + let horizontal_scaling = if horizontal { + self.horizontal_scaling() + } else { + 1.0 + }; + + let scaled_adjustment = -adjustment / UNITS_PER_EM * self.font_size * horizontal_scaling; + let (tx, ty) = if horizontal { + (scaled_adjustment, 0.0) + } else { + (0.0, scaled_adjustment) + }; + + self.text_matrix *= Affine::new([1.0, 0.0, 0.0, 1.0, tx as f64, ty as f64]); + } + + pub(crate) fn apply_code_advance(&mut self, char_code: u32, code_len: usize) { + let glyph_advance = self + .font + .as_ref() + .map(|f| f.code_advance(char_code)) + .unwrap_or(Vec2::ZERO); + let horizontal = self.font_horizontal(); + + let word_space = if char_code == 32 && code_len == 1 { + self.word_space + } else { + 0.0 + }; + + let base_advance = + |advance: f32| advance / UNITS_PER_EM * self.font_size + self.char_space + word_space; + + let tx = if horizontal { + base_advance(glyph_advance.x as f32) * self.horizontal_scaling() + } else { + 0.0 + }; + + let ty = if !horizontal { + base_advance(glyph_advance.y as f32) + } else { + 0.0 + }; + + self.text_matrix *= Affine::new([1.0, 0.0, 0.0, 1.0, tx as f64, ty as f64]); + } + + pub(crate) fn full_transform(&self) -> Affine { + self.text_matrix * self.temp_transform() + } +} + +impl Default for TextState<'_> { + fn default() -> Self { + Self { + char_space: 0.0, + word_space: 0.0, + horizontal_scaling: 100.0, + leading: 0.0, + font: None, + // Not in the specification, but we just define it so we don't need to use an option. + font_size: 1.0, + render_mode: TextRenderingMode::default(), + text_matrix: Affine::IDENTITY, + text_line_matrix: Affine::IDENTITY, + rise: 0.0, + clip_paths: BezPath::default(), + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct GraphicsState<'a> { + // Stroke parameters. + pub(crate) stroke_props: StrokeProps, + + // Stroke paint parameters. + pub(crate) stroke_color: ColorComponents, + pub(crate) stroke_pattern: Option>, + pub(crate) stroke_cs: ColorSpace, + pub(crate) stroke_alpha: f32, + + // Non-stroke paint parameters. + pub(crate) non_stroke_color: ColorComponents, + pub(crate) non_stroke_pattern: Option>, + pub(crate) none_stroke_cs: ColorSpace, + pub(crate) non_stroke_alpha: f32, + + pub(crate) soft_mask: Option>, + pub(crate) transfer_function: Option, + pub(crate) blend_mode: BlendMode, +} + +impl Default for GraphicsState<'_> { + fn default() -> Self { + GraphicsState { + stroke_props: StrokeProps::default(), + non_stroke_alpha: 1.0, + stroke_cs: ColorSpace::device_gray(), + stroke_color: smallvec![0.0,], + none_stroke_cs: ColorSpace::device_gray(), + non_stroke_color: smallvec![0.0], + stroke_alpha: 1.0, + stroke_pattern: None, + non_stroke_pattern: None, + soft_mask: None, + transfer_function: None, + blend_mode: BlendMode::default(), + } + } +} + +pub(crate) struct PaintData<'a> { + pub(crate) alpha: f32, + pub(crate) color: ColorComponents, + pub(crate) color_space: ColorSpace, + pub(crate) pattern: Option>, + pub(crate) transfer_function: Option, +} + +pub(crate) fn handle_gs<'a>( + dict: &Dict<'a>, + context: &mut Context<'a>, + parent_resources: &Resources<'a>, +) { + for key in dict.keys() { + handle_gs_single(dict, key.clone(), context, parent_resources).warn_none(&format!( + "invalid value in graphics state for {}", + key.as_str() + )); + } +} + +pub(crate) fn handle_gs_single<'a>( + dict: &Dict<'a>, + key: Name<'_>, + context: &mut Context<'a>, + parent_resources: &Resources<'a>, +) -> Option<()> { + // TODO Can we use constants here somehow? + match key.as_str() { + "LW" => context.get_mut().graphics_state.stroke_props.line_width = dict.get::(key)?, + "LC" => { + context.get_mut().graphics_state.stroke_props.line_cap = + convert_line_cap(LineCap(dict.get::(key)?)); + } + "LJ" => { + context.get_mut().graphics_state.stroke_props.line_join = + convert_line_join(LineJoin(dict.get::(key)?)); + } + "ML" => context.get_mut().graphics_state.stroke_props.miter_limit = dict.get::(key)?, + "CA" => context.get_mut().graphics_state.stroke_alpha = dict.get::(key)?, + "ca" => context.get_mut().graphics_state.non_stroke_alpha = dict.get::(key)?, + "TR" | "TR2" => { + let function = match dict + .get::>(TR2) + .or_else(|| dict.get::>(TR))? + { + Object::Array(array) => { + let mut iter = array.iter::>(); + let functions = [ + Function::new(&iter.next()?)?, + Function::new(&iter.next()?)?, + Function::new(&iter.next()?)?, + Function::new(&iter.next()?)?, + ]; + + Some(ActiveTransferFunction::Four(functions)) + } + // Only `Identity` and `Default` are valid, which both just reset it. + Object::Name(_) => None, + o => Some(ActiveTransferFunction::Single(Function::new(&o)?)), + }; + + context.get_mut().graphics_state.transfer_function = function; + } + "SMask" => { + if let Some(name) = dict.get::>(SMASK) { + if name.deref() == b"None" { + context.get_mut().graphics_state.soft_mask = None; + } + } else { + context.get_mut().graphics_state.soft_mask = dict + .get::>(SMASK) + .and_then(|d| SoftMask::new(&d, context, parent_resources.clone())); + } + } + "BM" => { + if let Some(name) = dict.get::>(key.as_ref()) { + if let Some(bm) = convert_blend_mode(name.as_str()) { + context.get_mut().graphics_state.blend_mode = bm; + + return Some(()); + } + } else if let Some(arr) = dict.get::>(key) { + for name in arr.iter::>() { + if let Some(bm) = convert_blend_mode(name.as_str()) { + context.get_mut().graphics_state.blend_mode = bm; + + return Some(()); + } + } + } + + warn!("unknown blend mode, defaulting to Normal"); + context.get_mut().graphics_state.blend_mode = BlendMode::Normal; + } + "Font" => { + let arr = dict.get::>(FONT)?; + let mut iter = arr.iter::>(); + let font_dict = iter.next()?.into_dict()?; + let size = iter.next()?.into_number()?.as_f32(); + + let font = context.resolve_font(&font_dict); + context.get_mut().text_state.font_size = size; + context.get_mut().text_state.font = font; + } + "D" => { + let arr = dict.get::>(key)?; + let mut iter = arr.iter::>(); + let dash_array = iter.next()?.into_array()?; + let dash_phase = iter.next()?.into_number()?.as_f32(); + + context.get_mut().graphics_state.stroke_props.dash_offset = dash_phase; + context.get_mut().graphics_state.stroke_props.dash_array = dash_array + .iter::() + // kurbo apparently cannot properly deal with offsets that are exactly 0. + .map(|n| if n == 0.0 { 0.01 } else { n }) + .collect(); + } + "Type" => {} + _ => {} + } + + Some(()) +} + +fn convert_blend_mode(name: &str) -> Option { + let bm = match name { + "Normal" => BlendMode::Normal, + "Multiply" => BlendMode::Multiply, + "Screen" => BlendMode::Screen, + "Overlay" => BlendMode::Overlay, + "Darken" => BlendMode::Darken, + "Lighten" => BlendMode::Lighten, + "ColorDodge" => BlendMode::ColorDodge, + "ColorBurn" => BlendMode::ColorBurn, + "HardLight" => BlendMode::HardLight, + "SoftLight" => BlendMode::SoftLight, + "Difference" => BlendMode::Difference, + "Exclusion" => BlendMode::Exclusion, + "Hue" => BlendMode::Hue, + "Saturation" => BlendMode::Saturation, + "Color" => BlendMode::Color, + "Luminosity" => BlendMode::Luminosity, + _ => return None, + }; + + Some(bm) +} diff --git a/vendor/hayro-interpret/src/interpret/text.rs b/vendor/hayro-interpret/src/interpret/text.rs new file mode 100644 index 00000000..65a9a279 --- /dev/null +++ b/vendor/hayro-interpret/src/interpret/text.rs @@ -0,0 +1,260 @@ +use crate::GlyphDrawMode; +use crate::context::Context; +use crate::device::Device; +use crate::font::Glyph; +use crate::interpret::path::get_paint; +use crate::interpret::state::TextStateFont; +use hayro_syntax::object; +use hayro_syntax::page::Resources; +use kurbo::Affine; + +pub(crate) fn show_text_string<'a>( + ctx: &mut Context<'a>, + device: &mut impl Device<'a>, + resources: &Resources<'a>, + text: &object::String<'_>, +) { + show_text_run(ctx, |ctx, pass| show_string_pass(ctx, device, resources, text, pass)); +} + +pub(crate) fn show_text_array<'a>( + ctx: &mut Context<'a>, + device: &mut impl Device<'a>, + resources: &Resources<'a>, + text: &object::Array<'_>, +) { + show_text_run(ctx, |ctx, pass| { + for item in text.iter::>() { + match item { + object::Object::Number(number) => ctx.get_mut().text_state.apply_adjustment(number.as_f32()), + object::Object::String(text) => show_string_pass(ctx, device, resources, &text, pass), + _ => {} + } + } + }); +} + +#[derive(Clone, Copy)] +enum TextPass { + All, + Fill, + Stroke, +} + +/// Fill the entire text-showing operation before stroking it, including all strings in TJ. +/// Replaying the source avoids an input-sized vector of positioned glyphs. Only the text +/// matrix advances while reading these operands; clipping paths from the first pass remain. +fn show_text_run<'a>(ctx: &mut Context<'a>, mut draw: impl FnMut(&mut Context<'a>, TextPass)) { + if matches!( + ctx.get().text_state.render_mode, + TextRenderingMode::FillStroke | TextRenderingMode::FillAndStrokeAndClip + ) { + let start = ctx.get().text_state.text_matrix; + draw(ctx, TextPass::Fill); + let end = ctx.get().text_state.text_matrix; + ctx.get_mut().text_state.text_matrix = start; + draw(ctx, TextPass::Stroke); + ctx.get_mut().text_state.text_matrix = end; + } else { + draw(ctx, TextPass::All); + } +} + +fn show_string_pass<'a>( + ctx: &mut Context<'a>, + device: &mut impl Device<'a>, + resources: &Resources<'a>, + text: &object::String<'_>, + pass: TextPass, +) { + let Some(font) = ctx.get().text_state.font.clone() else { + warn!("tried to show text without active font"); + + return; + }; + + let bytes = text.as_bytes(); + + // In case we have a fallback font (which occurs if either no font was set at all + // in the content stream, or an invalid one), we only want to show the glyphs + // using Helvetica if the bytes are actually valid ASCII. + let show_glyphs = matches!(font, TextStateFont::Font(_)) + || (matches!(font, TextStateFont::Fallback(_)) && bytes.is_ascii()); + + let mut cur_idx = 0; + + while cur_idx < bytes.len() { + let (code, adv) = font.read_code(bytes, cur_idx); + cur_idx += adv; + + if show_glyphs { + let (glyph, glyph_transform) = font.get_glyph( + font.map_code(code), + code, + ctx, + resources, + font.origin_displacement(code), + ); + show_glyph(ctx, device, &glyph, glyph_transform, pass); + } + + ctx.get_mut().text_state.apply_code_advance(code, adv); + } +} + +pub(crate) fn next_line(ctx: &mut Context<'_>, tx: f64, ty: f64) { + let new_matrix = ctx.get_mut().text_state.text_line_matrix * Affine::translate((tx, ty)); + ctx.get_mut().text_state.text_line_matrix = new_matrix; + ctx.get_mut().text_state.text_matrix = new_matrix; +} + +fn show_glyph<'a>( + ctx: &mut Context<'a>, + device: &mut impl Device<'a>, + glyph: &Glyph<'a>, + glyph_transform: Affine, + pass: TextPass, +) { + if !ctx.ocg_state.is_visible() { + return; + } + + // Type 3 procedures paint themselves and ignore fill/stroke modes. Keep the existing + // single-paint behavior when an outline run requires a second, stroke-only pass. + if matches!((glyph, pass), (Glyph::Type3(_), TextPass::Stroke)) { + return; + } + + device.set_soft_mask(ctx.get().graphics_state.soft_mask.clone()); + device.set_blend_mode(ctx.get().graphics_state.blend_mode); + let stroke_props = ctx.stroke_props(); + + // PdfCraft patch: only mode 3 has any effect on text in a Type 3 font (ISO 32000-1 + // 9.3.6). Stroking ran the glyph procedure a second time, so `2 Tr` made bitmap fonts + // look bold, and the clip modes have no outline to clip with. + let render_mode = match (glyph, ctx.get().text_state.render_mode, pass) { + (Glyph::Type3(_), TextRenderingMode::Invisible, _) => TextRenderingMode::Invisible, + (Glyph::Type3(_), _, _) => TextRenderingMode::Fill, + (_, TextRenderingMode::FillAndStrokeAndClip, TextPass::Fill) => TextRenderingMode::FillAndClip, + (_, _, TextPass::Fill) => TextRenderingMode::Fill, + (_, _, TextPass::Stroke) => TextRenderingMode::Stroke, + (_, mode, TextPass::All) => mode, + }; + + match render_mode { + TextRenderingMode::Fill => { + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, false), + &GlyphDrawMode::Fill, + ); + } + TextRenderingMode::Stroke => { + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, true), + &GlyphDrawMode::Stroke(stroke_props), + ); + } + TextRenderingMode::FillStroke => { + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, false), + &GlyphDrawMode::Fill, + ); + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, true), + &GlyphDrawMode::Stroke(stroke_props), + ); + } + TextRenderingMode::Invisible => { + // Still call draw_glyph for invisible text, so that it can + // for example be used for text extraction. + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, false), + &GlyphDrawMode::Invisible, + ); + } + TextRenderingMode::Clip => { + clip_glyph(ctx, glyph, glyph_transform); + } + TextRenderingMode::FillAndClip => { + clip_glyph(ctx, glyph, glyph_transform); + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, false), + &GlyphDrawMode::Fill, + ); + } + TextRenderingMode::StrokeAndClip => { + clip_glyph(ctx, glyph, glyph_transform); + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, true), + &GlyphDrawMode::Stroke(stroke_props), + ); + } + TextRenderingMode::FillAndStrokeAndClip => { + clip_glyph(ctx, glyph, glyph_transform); + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, false), + &GlyphDrawMode::Fill, + ); + device.draw_glyph( + glyph, + ctx.get().ctm, + glyph_transform, + &get_paint(ctx, true), + &GlyphDrawMode::Stroke(stroke_props), + ); + } + } +} + +pub(crate) fn clip_glyph(context: &mut Context<'_>, glyph: &Glyph<'_>, transform: Affine) { + match glyph { + Glyph::Outline(o) => { + let outline = transform * o.outline(); + let has_outline = outline.segments().next().is_some(); + + if has_outline { + context.get_mut().text_state.clip_paths.extend(outline); + } + } + Glyph::Type3(_) => { + warn!("text rendering mode clip not implemented for shape glyphs"); + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub(crate) enum TextRenderingMode { + #[default] + Fill, + Stroke, + FillStroke, + Invisible, + FillAndClip, + StrokeAndClip, + FillAndStrokeAndClip, + Clip, +} diff --git a/vendor/hayro-interpret/src/lib.rs b/vendor/hayro-interpret/src/lib.rs new file mode 100644 index 00000000..24c3be29 --- /dev/null +++ b/vendor/hayro-interpret/src/lib.rs @@ -0,0 +1,64 @@ +/*! +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. +*/ + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +#[macro_use] +mod log; + +mod cache; +mod context; +mod convert; +mod device; +mod function; +mod interpret; +mod ocg; +mod soft_mask; +mod types; +mod x_object; +/// PdfCraft patch: exported for its regression test. +pub use x_object::image_size_ok; + +pub mod color; +pub mod encode; +pub mod font; +pub mod gradient; +pub mod pattern; +pub mod shading; +pub mod util; + +pub use cache::CacheKey; +pub use context::*; +pub use device::*; +pub use function::Function; +pub use hayro_cmap; +pub use hayro_syntax; +pub use interpret::*; +pub use soft_mask::*; +pub use types::*; +pub use util::{RectExt, TransformExt}; diff --git a/vendor/hayro-interpret/src/log.rs b/vendor/hayro-interpret/src/log.rs new file mode 100644 index 00000000..fee9828c --- /dev/null +++ b/vendor/hayro-interpret/src/log.rs @@ -0,0 +1,27 @@ +//! Logging macros that optionally forward to the `log` crate. + +macro_rules! error { + ($fmt:literal $(, $($arg:expr),* $(,)?)?) => {{ + #[cfg(feature = "logging")] + { + ::log::error!($fmt $(, $($arg),*)?); + } + #[cfg(not(feature = "logging"))] + { + $($(let _ = &$arg;)*)? + } + }}; +} + +macro_rules! warn { + ($fmt:literal $(, $($arg:expr),* $(,)?)?) => {{ + #[cfg(feature = "logging")] + { + ::log::warn!($fmt $(, $($arg),*)?); + } + #[cfg(not(feature = "logging"))] + { + $($(let _ = &$arg;)*)? + } + }}; +} diff --git a/vendor/hayro-interpret/src/ocg.rs b/vendor/hayro-interpret/src/ocg.rs new file mode 100644 index 00000000..c59b70df --- /dev/null +++ b/vendor/hayro-interpret/src/ocg.rs @@ -0,0 +1,179 @@ +use hayro_syntax::object::dict::keys::{BASE_STATE, D, OCGS, OCMD, OCPROPERTIES, OFF, ON, P, TYPE}; +use hayro_syntax::object::{Array, Dict, Name, ObjectIdentifier}; +use std::collections::HashSet; + +pub(crate) struct OcgState { + inactive_ocgs: HashSet, + visibility_stack: Vec, +} + +impl OcgState { + fn dummy() -> Self { + Self { + inactive_ocgs: HashSet::default(), + visibility_stack: vec![], + } + } + + pub(crate) fn from_catalog(catalog: &Dict<'_>) -> Self { + let Some(oc_properties) = catalog.get::>(OCPROPERTIES) else { + return Self::dummy(); + }; + + let Some(config) = oc_properties.get::>(D) else { + return Self::dummy(); + }; + + let mut inactive = HashSet::new(); + + let base_state = config + .get::>(BASE_STATE) + .and_then(|b| BaseState::from_name(b.as_ref())); + + if base_state.unwrap_or(BaseState::On) == BaseState::Off + && let Some(ocgs) = oc_properties.get::>(OCGS) + { + for item in ocgs.raw_iter() { + if let Some(ref_) = item.as_obj_ref() { + let id: ObjectIdentifier = ref_.into(); + inactive.insert(id); + } + } + } + + let mut read_ocg_array = |key, insert_active: bool| { + if let Some(arr) = config.get::>(key) { + for item in arr.raw_iter() { + if let Some(ref_) = item.as_obj_ref() { + let id: ObjectIdentifier = ref_.into(); + if insert_active { + inactive.remove(&id); + } else { + inactive.insert(id); + } + } + } + } + }; + + read_ocg_array(ON, true); + read_ocg_array(OFF, false); + + Self { + inactive_ocgs: inactive, + visibility_stack: Vec::new(), + } + } + + pub(crate) fn begin_single_oc(&mut self, ocg_id: ObjectIdentifier) { + let is_active = !self.inactive_ocgs.contains(&ocg_id); + let visible = self.is_visible() && is_active; + self.visibility_stack.push(visible); + } + + pub(crate) fn begin_ocmd(&mut self, ocmd: &Dict<'_>) { + let policy = ocmd + .get::>(P) + .and_then(|n| OcmdPolicy::from_name(n.as_ref())) + .unwrap_or(OcmdPolicy::AnyOn); + + let mut ocg_ids: Vec = Vec::new(); + + if let Some(arr) = ocmd.get::>(OCGS) { + for item in arr.raw_iter() { + if let Some(ref_) = item.as_obj_ref() { + ocg_ids.push(ref_.into()); + } + } + } else if let Some(ref_) = ocmd.get_ref(OCGS) { + ocg_ids.push(ref_.into()); + } + + let is_active = if ocg_ids.is_empty() { + true + } else { + match policy { + OcmdPolicy::AllOn => ocg_ids.iter().all(|id| !self.inactive_ocgs.contains(id)), + OcmdPolicy::AnyOn => ocg_ids.iter().any(|id| !self.inactive_ocgs.contains(id)), + OcmdPolicy::AnyOff => ocg_ids.iter().any(|id| self.inactive_ocgs.contains(id)), + OcmdPolicy::AllOff => ocg_ids.iter().all(|id| self.inactive_ocgs.contains(id)), + } + }; + + let visible = self.is_visible() && is_active; + self.visibility_stack.push(visible); + } + + pub(crate) fn begin_ocg(&mut self, props: &Dict<'_>, ref_id: ObjectIdentifier) { + match props.get::>(TYPE).as_deref() { + Some(OCMD) => self.begin_ocmd(props), + _ => self.begin_single_oc(ref_id), + } + } + + pub(crate) fn begin_marked_content(&mut self) { + let visible = self.is_visible(); + self.visibility_stack.push(visible); + } + + pub(crate) fn end_marked_content(&mut self) { + self.visibility_stack.pop(); + } + + /// PdfCraft patch: override one group's state (viewer layer toggles). + pub(crate) fn set_active(&mut self, id: ObjectIdentifier, active: bool) { + if active { + self.inactive_ocgs.remove(&id); + } else { + self.inactive_ocgs.insert(id); + } + } + + pub(crate) fn is_visible(&self) -> bool { + self.visibility_stack.last().copied().unwrap_or(true) + } +} + +impl Default for OcgState { + fn default() -> Self { + Self::dummy() + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum BaseState { + On, + Off, + Unchanged, +} + +impl BaseState { + fn from_name(name: &[u8]) -> Option { + match name { + b"ON" => Some(Self::On), + b"OFF" => Some(Self::Off), + b"Unchanged" => Some(Self::Unchanged), + _ => None, + } + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum OcmdPolicy { + AllOn, + AnyOn, + AnyOff, + AllOff, +} + +impl OcmdPolicy { + fn from_name(name: &[u8]) -> Option { + match name { + b"AllOn" => Some(Self::AllOn), + b"AnyOn" => Some(Self::AnyOn), + b"AnyOff" => Some(Self::AnyOff), + b"AllOff" => Some(Self::AllOff), + _ => None, + } + } +} diff --git a/vendor/hayro-interpret/src/pattern.rs b/vendor/hayro-interpret/src/pattern.rs new file mode 100644 index 00000000..3d6d2229 --- /dev/null +++ b/vendor/hayro-interpret/src/pattern.rs @@ -0,0 +1,353 @@ +//! PDF patterns. + +use crate::cache::Cache; +use crate::color::{Color, ColorSpace}; +use crate::context::{Context, InterpreterCache}; +use crate::device::Device; +use crate::font::Glyph; +use crate::interpret::state::{ActiveTransferFunction, State}; +use crate::shading::Shading; +use crate::soft_mask::SoftMask; +use crate::util::{Float32Ext, RectExt, hash128}; +use crate::{BlendMode, CacheKey, ClipPath, GlyphDrawMode, Image, PathDrawMode}; +use crate::{FillRule, InterpreterSettings, Paint, interpret}; +use hayro_syntax::content::TypedIter; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::{ + BBOX, EXT_G_STATE, MATRIX, PAINT_TYPE, RESOURCES, SHADING, X_STEP, Y_STEP, +}; +use hayro_syntax::object::{Object, dict_or_stream}; +use hayro_syntax::page::Resources; +use hayro_syntax::xref::XRef; +use kurbo::{Affine, BezPath, Rect, Shape}; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +/// A PDF pattern. +#[derive(Debug, Clone)] +pub enum Pattern<'a> { + /// A shading pattern. + Shading(ShadingPattern), + /// A tiling pattern. + Tiling(Box>), +} + +impl<'a> Pattern<'a> { + pub(crate) fn new( + object: Object<'a>, + ctx: &Context<'a>, + resources: &Resources<'a>, + ) -> Option { + match object { + Object::Dict(dict) => Some(Self::Shading(ShadingPattern::new( + &dict, + &ctx.interpreter_cache.object_cache, + ctx.get().graphics_state.non_stroke_alpha, + )?)), + Object::Stream(stream) => Some(Self::Tiling(Box::new(TilingPattern::new( + stream, ctx, resources, + )?))), + _ => None, + } + } + + pub(crate) fn pre_concat_transform(&mut self, transform: Affine) { + match self { + Self::Shading(p) => { + p.matrix = transform * p.matrix; + let transformed_clip_path = p.shading.clip_path.clone().map(|r| p.matrix * r); + Arc::make_mut(&mut p.shading).clip_path = transformed_clip_path; + } + Self::Tiling(p) => p.matrix = transform * p.matrix, + } + } + + pub(crate) fn set_transfer_function(&mut self, tf: ActiveTransferFunction) { + if let Self::Shading(p) = self { + p.transfer_function = Some(tf); + } + } +} + +impl CacheKey for Pattern<'_> { + fn cache_key(&self) -> u128 { + match self { + Self::Shading(p) => p.cache_key(), + Self::Tiling(p) => p.cache_key(), + } + } +} + +/// A shading pattern. +#[derive(Clone, Debug)] +pub struct ShadingPattern { + /// The underlying shading of the pattern. + pub shading: Arc, + /// A transformation matrix to apply prior to rendering. + pub matrix: Affine, + /// An additional opacity to apply to the shading pattern. + pub opacity: f32, + /// An optional transfer function to apply to the shading's output colors. + pub transfer_function: Option, +} + +impl ShadingPattern { + pub(crate) fn new(dict: &Dict<'_>, cache: &Cache, opacity: f32) -> Option { + let shading = dict.get::>(SHADING).and_then(|o| { + let (dict, stream) = dict_or_stream(&o)?; + + Shading::new(dict, stream, cache) + })?; + let matrix = dict + .get::<[f64; 6]>(MATRIX) + .map(Affine::new) + .unwrap_or_default(); + + if dict.contains_key(EXT_G_STATE) { + warn!("shading patterns with ext_g_state are not supported yet"); + } + + Some(Self { + shading: Arc::new(shading), + opacity, + matrix, + transfer_function: None, + }) + } +} + +impl CacheKey for ShadingPattern { + fn cache_key(&self) -> u128 { + hash128(&(self.shading.cache_key(), self.matrix.cache_key())) + } +} + +/// A tiling pattern. +#[derive(Clone)] +pub struct TilingPattern<'a> { + cache_key: u128, + ctx_bbox: Rect, + /// The bbox of the tiling pattern. + pub bbox: Rect, + /// The step in the x direction. + pub x_step: f32, + /// The step in the y direction. + pub y_step: f32, + /// A transformation to apply prior to rendering. + pub matrix: Affine, + stream: Stream<'a>, + is_color: bool, + pub(crate) stroke_paint: Color, + pub(crate) non_stroking_paint: Color, + pub(crate) parent_resources: Resources<'a>, + pub(crate) cache: InterpreterCache<'a>, + pub(crate) settings: InterpreterSettings, + pub(crate) xref: &'a XRef, + nesting_depth: u32, +} + +impl Debug for TilingPattern<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("TilingPattern") + } +} + +impl<'a> TilingPattern<'a> { + pub(crate) fn new( + stream: Stream<'a>, + ctx: &Context<'a>, + resources: &Resources<'a>, + ) -> Option { + let cache_key = stream.cache_key(); + let dict = stream.dict(); + + let bbox = dict.get::(BBOX)?.to_kurbo(); + let x_step = dict.get::(X_STEP)?; + let y_step = dict.get::(Y_STEP)?; + + if x_step.is_nearly_zero() || y_step.is_nearly_zero() || bbox.is_zero_area() { + return None; + } + + let is_color = dict.get::(PAINT_TYPE)? == 1; + let matrix = dict + .get::<[f64; 6]>(MATRIX) + .map(Affine::new) + .unwrap_or_default(); + + let state = ctx.get().clone(); + let ctx_bbox = ctx.bbox(); + + let fill_cs = state + .graphics_state + .none_stroke_cs + .pattern_cs() + .unwrap_or(ColorSpace::device_gray()); + let stroke_cs = state + .graphics_state + .stroke_cs + .pattern_cs() + .unwrap_or(ColorSpace::device_gray()); + + let non_stroking_paint = Color::new( + fill_cs, + state.graphics_state.non_stroke_color.clone(), + state.graphics_state.non_stroke_alpha, + ); + let stroke_paint = Color::new( + stroke_cs, + state.graphics_state.stroke_color.clone(), + state.graphics_state.stroke_alpha, + ); + let nesting_depth = ctx.nesting_depth() + 1; + + Some(Self { + cache_key, + bbox, + x_step, + y_step, + matrix, + ctx_bbox, + is_color, + stream, + stroke_paint, + non_stroking_paint, + settings: ctx.settings.clone(), + parent_resources: resources.clone(), + cache: ctx.interpreter_cache.clone(), + xref: ctx.xref, + nesting_depth, + }) + } + + /// Interpret the contents of the pattern into the given device. + pub fn interpret( + &self, + device: &mut impl Device<'a>, + initial_transform: Affine, + is_stroke: bool, + ) -> Option<()> { + // PdfCraft patch: a tiling pattern can paint itself (directly, or through resources it + // inherits when its own are missing), which recursed until the stack overflowed. Bound + // the nesting like XObjects do, tighter because every level rasterizes a tile. + if self.nesting_depth > crate::context::MAX_PAINT_NESTING || !crate::context::take_nested_paint(self.nesting_depth) { + warn!("tiling pattern nesting depth or paint budget exceeded"); + return None; + } + let state = State::new(initial_transform); + + let mut context = Context::new_with( + state.ctm, + // TODO: bbox? + (initial_transform * self.ctx_bbox.to_path(0.1)).bounding_box(), + &self.cache, + self.xref, + self.settings.clone(), + state, + self.nesting_depth, + ); + + // PdfCraft patch: see `context::MAX_PAGE_CONTENT`. + let decoded = crate::context::decode_content(&self.stream)?; + // Tiling patterns have their own required resource scope, even when malformed + // input omits the dictionary. They never borrow names from their caller. + let resources = Resources::from_scope( + Some(self.stream.dict().get(RESOURCES).unwrap_or_default()), + self.parent_resources.clone(), + ); + let iter = TypedIter::new(decoded.as_ref()); + + let clip_path = ClipPath { + path: initial_transform * self.bbox.to_path(0.1), + fill: FillRule::NonZero, + }; + device.push_clip_path(&clip_path); + + if self.is_color { + interpret(iter, &resources, &mut context, device); + } else { + let paint = if !is_stroke { + Paint::Color(self.non_stroking_paint.clone()) + } else { + Paint::Color(self.stroke_paint.clone()) + }; + + let mut device = StencilPatternDevice::new(device, paint.clone()); + interpret(iter, &resources, &mut context, &mut device); + } + + device.pop_clip_path(); + + Some(()) + } +} + +impl CacheKey for TilingPattern<'_> { + fn cache_key(&self) -> u128 { + self.cache_key + } +} + +struct StencilPatternDevice<'a, 'b, T: Device<'a>> { + inner: &'b mut T, + paint: Paint<'a>, +} + +impl<'a, 'b, T: Device<'a>> StencilPatternDevice<'a, 'b, T> { + pub(crate) fn new(device: &'b mut T, paint: Paint<'a>) -> Self { + Self { + inner: device, + paint, + } + } +} + +// Only filling, stroking of paths and stencil masks are allowed. +impl<'a, T: Device<'a>> Device<'a> for StencilPatternDevice<'a, '_, T> { + fn draw_path( + &mut self, + path: &BezPath, + transform: Affine, + _: &Paint<'_>, + draw_mode: &PathDrawMode, + ) { + self.inner + .draw_path(path, transform, &self.paint, draw_mode); + } + + fn set_soft_mask(&mut self, _: Option>) {} + + fn push_clip_path(&mut self, clip_path: &ClipPath) { + self.inner.push_clip_path(clip_path); + } + + fn push_transparency_group(&mut self, _: f32, _: Option>, _: BlendMode) {} + + fn draw_glyph( + &mut self, + g: &Glyph<'a>, + transform: Affine, + glyph_transform: Affine, + p: &Paint<'a>, + draw_mode: &GlyphDrawMode, + ) { + self.inner + .draw_glyph(g, transform, glyph_transform, p, draw_mode); + } + + fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) { + if let Image::Stencil(mut s) = image { + s.paint = self.paint.clone(); + self.inner.draw_image(Image::Stencil(s), transform); + } + } + + fn pop_clip_path(&mut self) { + self.inner.pop_clip_path(); + } + + fn pop_transparency_group(&mut self) {} + + fn set_blend_mode(&mut self, _: BlendMode) {} +} diff --git a/vendor/hayro-interpret/src/shading.rs b/vendor/hayro-interpret/src/shading.rs new file mode 100644 index 00000000..98fbb583 --- /dev/null +++ b/vendor/hayro-interpret/src/shading.rs @@ -0,0 +1,1081 @@ +//! PDF shadings. + +#![allow(clippy::needless_range_loop)] + +use crate::CacheKey; +use crate::cache::Cache; +use crate::color::{ColorComponents, ColorSpace}; +use crate::function::{Function, StitchingBounds, Values, interpolate}; +use crate::util::{Float32Ext, PointExt, RectExt}; +use hayro_syntax::bit_reader::BitReader; +use hayro_syntax::object::Array; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Object; +use hayro_syntax::object::Rect; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::{ + BACKGROUND, BBOX, BITS_PER_COMPONENT, BITS_PER_COORDINATE, BITS_PER_FLAG, COLORSPACE, COORDS, + DECODE, DOMAIN, EXTEND, FUNCTION, MATRIX, SHADING_TYPE, VERTICES_PER_ROW, +}; +use kurbo::{Affine, BezPath, CubicBez, ParamCurve, Point, Shape}; +use smallvec::{SmallVec, smallvec}; +use std::sync::Arc; + +/// The function supplied to a shading. +#[derive(Debug, Clone)] +pub enum ShadingFunction { + /// A single function, which should be used to evaluate all components of the shading. + Single(Function), + /// Multiple functions, one for each color component. + Multiple(SmallVec<[Function; 4]>), +} + +impl ShadingFunction { + /// Evaluate the shading function. + pub fn eval(&self, input: &Values) -> Option { + match self { + Self::Single(s) => s.eval(input.clone()), + Self::Multiple(m) => { + // 1-in, 1-out function for each color component. + + let mut out = smallvec![]; + + for func in m { + out.push(*func.eval(input.clone())?.first()?); + } + + Some(out) + } + } + } + + /// PdfCraft patch (backport of hayro 0.8): stitching breakpoints of all component functions. + pub(crate) fn stitching_bounds(&self) -> StitchingBounds { + let mut bounds = StitchingBounds::new(); + + match self { + Self::Single(function) => bounds.extend(function.stitching_bounds()), + Self::Multiple(functions) => { + for function in functions { + bounds.extend(function.stitching_bounds()); + } + } + } + + bounds.sort_by(f32::total_cmp); + bounds.dedup_by(|a, b| (*a - *b).abs() <= f32::EPSILON); + bounds + } +} + +/// A type of shading. +#[derive(Debug)] +pub enum ShadingType { + /// A function-based shading. + FunctionBased { + /// The domain of the function. + domain: [f32; 4], + /// A transform to apply to the shading. + matrix: Affine, + /// The function that should be used to evaluate the shading. + function: ShadingFunction, + }, + /// A radial-axial shading. + RadialAxial { + /// The coordinates of the shading. + /// + /// For axial shadings, only the first 4 entries are relevant, representing the x/y coordinates + /// of the first point and the coordinates for the second point. + /// + /// For radial shadings, the coordinates contain the x/y coordinates as well as the radius + /// for both circles. + coords: [f32; 6], + /// The domain of the shading. + domain: [f32; 2], + /// The function forming the basis of the shading. + function: ShadingFunction, + /// The extends in the left/right direction of the shading. + extend: [bool; 2], + /// Whether the shading is axial or radial. + axial: bool, + }, + /// A triangle-mesh shading. + TriangleMesh { + /// The triangles making up the shading. + triangles: Vec, + /// An optional function used for calculating the sampled color values. + function: Option, + }, + /// A coons-patch-mesh shading. + CoonsPatchMesh { + /// The patches that make up the shading. + patches: Vec, + /// An optional function used for calculating the sampled color values. + function: Option, + }, + /// A tensor-product-patch-mesh shading. + TensorProductPatchMesh { + /// The patches that make up the shading. + patches: Vec, + /// An optional function used for calculating the sampled color values. + function: Option, + }, + /// A dummy shading that should just be drawn transparent. + Dummy, +} + +/// A PDF shading. +#[derive(Clone, Debug)] +pub struct Shading { + cache_key: u128, + /// The type of shading. + pub shading_type: Arc, + /// The color space of the shading. + pub color_space: ColorSpace, + /// A clip path that should be applied to the shading. + pub clip_path: Option, + /// The background color of the shading. + pub background: Option>, +} + +impl Shading { + pub(crate) fn new(dict: &Dict<'_>, stream: Option<&Stream<'_>>, cache: &Cache) -> Option { + let cache_key = dict.cache_key(); + + let shading_num = dict.get::(SHADING_TYPE)?; + + let color_space = ColorSpace::new(dict.get(COLORSPACE)?, cache)?; + + let shading_type = match shading_num { + 1 => { + let domain = dict.get::<[f32; 4]>(DOMAIN).unwrap_or([0.0, 1.0, 0.0, 1.0]); + let matrix = dict + .get::<[f64; 6]>(MATRIX) + .map(Affine::new) + .unwrap_or_default(); + let function = read_function(dict, &color_space)?; + + ShadingType::FunctionBased { + domain, + matrix, + function, + } + } + 2 | 3 => { + let domain = dict.get::<[f32; 2]>(DOMAIN).unwrap_or([0.0, 1.0]); + let function = read_function(dict, &color_space)?; + let extend = dict.get::<[bool; 2]>(EXTEND).unwrap_or([false, false]); + let (coords, invalid) = if shading_num == 2 { + let read = dict.get::<[f32; 4]>(COORDS)?; + let invalid = (read[0] - read[2]).is_nearly_zero() + && (read[1] - read[3]).is_nearly_zero(); + ([read[0], read[1], read[2], read[3], 0.0, 0.0], invalid) + } else { + let read = dict.get::<[f32; 6]>(COORDS)?; + let invalid = (read[0] - read[3]).is_nearly_zero() + && (read[1] - read[4]).is_nearly_zero() + && (read[2] - read[5]).is_nearly_zero(); + (read, invalid) + }; + + let axial = shading_num == 2; + + if invalid { + ShadingType::Dummy + } else { + ShadingType::RadialAxial { + domain, + function, + extend, + coords, + axial, + } + } + } + 4 => { + let stream = stream?; + let stream_data = stream.decoded().ok()?; + let bp_coord = dict.get::(BITS_PER_COORDINATE)?; + let bp_comp = dict.get::(BITS_PER_COMPONENT)?; + let bpf = dict.get::(BITS_PER_FLAG)?; + let function = read_function(dict, &color_space); + let decode = dict + .get::>(DECODE)? + .iter::() + .collect::>(); + + let triangles = read_free_form_triangles( + stream_data.as_ref(), + bpf, + bp_coord, + bp_comp, + function.is_some(), + &decode, + )?; + + ShadingType::TriangleMesh { + triangles, + function, + } + } + 5 => { + let stream = stream?; + let stream_data = stream.decoded().ok()?; + let bp_coord = dict.get::(BITS_PER_COORDINATE)?; + let bp_comp = dict.get::(BITS_PER_COMPONENT)?; + let function = read_function(dict, &color_space); + let decode = dict + .get::>(DECODE)? + .iter::() + .collect::>(); + let vertices_per_row = dict.get::(VERTICES_PER_ROW)?; + + let triangles = read_lattice_triangles( + stream_data.as_ref(), + bp_coord, + bp_comp, + function.is_some(), + vertices_per_row, + &decode, + )?; + + ShadingType::TriangleMesh { + triangles, + function, + } + } + 6 => { + let stream = stream?; + let stream_data = stream.decoded().ok()?; + let bp_coord = dict.get::(BITS_PER_COORDINATE)?; + let bp_comp = dict.get::(BITS_PER_COMPONENT)?; + let bpf = dict.get::(BITS_PER_FLAG)?; + let function = read_function(dict, &color_space); + let decode = dict + .get::>(DECODE)? + .iter::() + .collect::>(); + + let patches = read_coons_patch_mesh( + stream_data.as_ref(), + bpf, + bp_coord, + bp_comp, + function.is_some(), + &decode, + )?; + + ShadingType::CoonsPatchMesh { patches, function } + } + 7 => { + let stream = stream?; + let stream_data = stream.decoded().ok()?; + let bp_coord = dict.get::(BITS_PER_COORDINATE)?; + let bp_comp = dict.get::(BITS_PER_COMPONENT)?; + let bpf = dict.get::(BITS_PER_FLAG)?; + let function = read_function(dict, &color_space); + let decode = dict + .get::>(DECODE)? + .iter::() + .collect::>(); + + let patches = read_tensor_product_patch_mesh( + stream_data.as_ref(), + bpf, + bp_coord, + bp_comp, + function.is_some(), + &decode, + )?; + + ShadingType::TensorProductPatchMesh { patches, function } + } + _ => return None, + }; + + let bbox = dict.get::(BBOX).map(|r| r.to_kurbo()); + let background = dict + .get::>(BACKGROUND) + .map(|a| a.iter::().collect::>()); + + Some(Self { + cache_key, + shading_type: Arc::new(shading_type), + color_space, + clip_path: bbox.map(|r| r.to_path(0.1)), + background, + }) + } +} + +impl CacheKey for Shading { + fn cache_key(&self) -> u128 { + self.cache_key + } +} + +/// A triangle made up of three vertices. +#[derive(Clone, Debug)] +pub struct Triangle { + /// The first vertex. + pub p0: TriangleVertex, + /// The second vertex. + pub p1: TriangleVertex, + /// The third vertex. + pub p2: TriangleVertex, + kurbo_tri: kurbo::Triangle, + d00: f64, + d01: f64, + d11: f64, +} + +impl Triangle { + /// Create a new triangle. + pub fn new(p0: TriangleVertex, p1: TriangleVertex, p2: TriangleVertex) -> Self { + let v0 = p1.point - p0.point; + let v1 = p2.point - p0.point; + + let d00 = v0.dot(v0); + let d01 = v0.dot(v1); + let d11 = v1.dot(v1); + + let kurbo_tri = kurbo::Triangle::new(p0.point, p1.point, p2.point); + + Self { + p0, + p1, + kurbo_tri, + p2, + d00, + d01, + d11, + } + } + + /// Get the interpolated colors of the point from the triangle. + /// + /// Returns `None` if the point is not inside of the triangle. + pub fn interpolate(&self, pos: Point) -> ColorComponents { + let (u, v, w) = self.barycentric_coords(pos); + + let mut result = smallvec![]; + + for i in 0..self.p0.colors.len() { + let c0 = self.p0.colors[i]; + let c1 = self.p1.colors[i]; + let c2 = self.p2.colors[i]; + result.push(u * c0 + v * c1 + w * c2); + } + + result + } + + /// Return whether the point is contained within the triangle. + pub fn contains_point(&self, pos: Point) -> bool { + self.kurbo_tri.winding(pos) != 0 + } + + /// Return the bounding box of the triangle. + pub fn bounding_box(&self) -> kurbo::Rect { + self.kurbo_tri.bounding_box() + } + + fn barycentric_coords(&self, p: Point) -> (f32, f32, f32) { + let (a, b, c) = (self.p0.point, self.p1.point, self.p2.point); + let v0 = b - a; + let v1 = c - a; + let v2 = p - a; + + let d00 = self.d00; + let d01 = self.d01; + let d11 = self.d11; + let d20 = v2.dot(v0); + let d21 = v2.dot(v1); + + let denom = d00 * d11 - d01 * d01; + let v = (d11 * d20 - d01 * d21) / denom; + let w = (d00 * d21 - d01 * d20) / denom; + let u = (1.0 - v - w) as f32; + + (u, v as f32, w as f32) + } +} + +/// A triangle vertex. +#[derive(Clone, Debug)] +pub struct TriangleVertex { + flag: u32, + /// The position of the vertex. + pub point: Point, + /// The color component of the vertex. + pub colors: ColorComponents, +} + +/// A coons patch. +#[derive(Clone, Debug)] +pub struct CoonsPatch { + /// The control points of the coons patch. + pub control_points: [Point; 12], + /// The colors at each corner of the coons patch. + pub colors: [ColorComponents; 4], +} + +/// A tensor-product patch. +#[derive(Clone, Debug)] +pub struct TensorProductPatch { + /// The control points of the tensor-product patch (4x4 grid = 16 points). + pub control_points: [Point; 16], + /// The colors at each corner of the tensor-product patch. + pub colors: [ColorComponents; 4], +} + +impl CoonsPatch { + /// Map the point to the coordinates of the coons patch. + pub fn map_coordinate(&self, p: Point) -> Point { + let (u, v) = (p.x, p.y); + + let cp = &self.control_points; + + let c1 = CubicBez::new(cp[0], cp[11], cp[10], cp[9]); + let c2 = CubicBez::new(cp[3], cp[4], cp[5], cp[6]); + let d1 = CubicBez::new(cp[0], cp[1], cp[2], cp[3]); + let d2 = CubicBez::new(cp[9], cp[8], cp[7], cp[6]); + + let sc = (1.0 - v) * c1.eval(u).to_vec2() + v * c2.eval(u).to_vec2(); + let sd = (1.0 - u) * d1.eval(v).to_vec2() + u * d2.eval(v).to_vec2(); + let sb = (1.0 - v) * ((1.0 - u) * c1.eval(0.0).to_vec2() + u * c1.eval(1.0).to_vec2()) + + v * ((1.0 - u) * c2.eval(0.0).to_vec2() + u * c2.eval(1.0).to_vec2()); + + (sc + sd - sb).to_point() + } + + /// Approximate the patch by triangles. + pub fn to_triangles(&self, buffer: &mut Vec) { + generate_patch_triangles(|p| self.map_coordinate(p), |p| self.interpolate(p), buffer); + } + + /// Get the interpolated colors of the point from the patch. + pub fn interpolate(&self, pos: Point) -> ColorComponents { + let (u, v) = (pos.x, pos.y); + let (c0, c1, c2, c3) = { + ( + &self.colors[0], + &self.colors[1], + &self.colors[2], + &self.colors[3], + ) + }; + + let mut result = SmallVec::new(); + for i in 0..c0.len() { + let val = (1.0 - u) * (1.0 - v) * c0[i] as f64 + + u * (1.0 - v) * c3[i] as f64 + + u * v * c2[i] as f64 + + (1.0 - u) * v * c1[i] as f64; + result.push(val as f32); + } + + result + } +} + +impl TensorProductPatch { + /// Evaluate Bernstein polynomial `B_i(t)` for tensor-product patches. + fn bernstein(i: usize, t: f64) -> f64 { + match i { + 0 => (1.0 - t).powi(3), + 1 => 3.0 * t * (1.0 - t).powi(2), + 2 => 3.0 * t.powi(2) * (1.0 - t), + 3 => t.powi(3), + _ => 0.0, + } + } + + /// Map the point to the coordinates of the tensor product patch. + pub fn map_coordinate(&self, p: Point) -> Point { + let (u, v) = (p.x, p.y); + + let mut x = 0.0; + let mut y = 0.0; + + fn idx(i: usize, j: usize) -> usize { + match (i, j) { + (0, 0) => 0, + (0, 1) => 1, + (0, 2) => 2, + (0, 3) => 3, + (1, 0) => 11, + (1, 1) => 12, + (1, 2) => 13, + (1, 3) => 4, + (2, 0) => 10, + (2, 1) => 15, + (2, 2) => 14, + (2, 3) => 5, + (3, 0) => 9, + (3, 1) => 8, + (3, 2) => 7, + (3, 3) => 6, + _ => panic!("Invalid index"), + } + } + + for i in 0..4 { + for j in 0..4 { + let control_point_idx = idx(i, j); + let basis = Self::bernstein(i, u) * Self::bernstein(j, v); + + x += self.control_points[control_point_idx].x * basis; + y += self.control_points[control_point_idx].y * basis; + } + } + + Point::new(x, y) + } + + /// Approximate the tensor product patch mesh by triangles. + pub fn to_triangles(&self, buffer: &mut Vec) { + generate_patch_triangles(|p| self.map_coordinate(p), |p| self.interpolate(p), buffer); + } + + /// Get the interpolated colors of the point from the patch. + pub fn interpolate(&self, pos: Point) -> ColorComponents { + let (u, v) = (pos.x, pos.y); + let (c0, c1, c2, c3) = { + ( + &self.colors[0], + &self.colors[1], + &self.colors[2], + &self.colors[3], + ) + }; + + let mut result = SmallVec::new(); + for i in 0..c0.len() { + let val = (1.0 - u) * (1.0 - v) * c0[i] as f64 + + u * (1.0 - v) * c3[i] as f64 + + u * v * c2[i] as f64 + + (1.0 - u) * v * c1[i] as f64; + result.push(val as f32); + } + + result + } +} + +/// PdfCraft patch: the most triangles a free-form or lattice mesh, and the most patches a +/// patch mesh, may hold. Each was read without limit: a patch can be 19 bits, a triangle 216 +/// bytes in memory, so a small stream made gigabytes. Real meshes are far smaller (a 200 ร— 200 +/// gradient mesh is 40 thousand patches). +const MAX_MESH_TRIANGLES: usize = 1 << 20; +const MAX_MESH_PATCHES: usize = 1 << 16; + +fn read_free_form_triangles( + data: &[u8], + bpf: u8, + bp_cord: u8, + bp_comp: u8, + has_function: bool, + decode: &[f32], +) -> Option> { + let mut triangles = vec![]; + + let ([x_min, x_max, y_min, y_max], decode) = split_decode(decode)?; + let mut reader = BitReader::new(data); + let helpers = InterpolationHelpers::new(bp_cord, bp_comp, x_min, x_max, y_min, y_max); + + let read_single = |reader: &mut BitReader<'_>| -> Option { + helpers.read_triangle_vertex(reader, bpf, has_function, decode) + }; + + let mut a = None; + let mut b = None; + let mut c = None; + + loop { + let Some(first) = read_single(&mut reader) else { + break; + }; + + if first.flag == 0 { + let second = read_single(&mut reader)?; + let third = read_single(&mut reader)?; + + a = Some(first.clone()); + b = Some(second.clone()); + c = Some(third.clone()); + } else if first.flag == 1 { + a = Some(b.clone()?); + b = Some(c.clone()?); + c = Some(first); + } else if first.flag == 2 { + b = Some(c.clone()?); + c = Some(first); + } + + let (p0, p1, p2) = (a.clone()?, b.clone()?, c.clone()?); + + if p0.point.nearly_same(p1.point) || p1.point.nearly_same(p2.point) { + continue; + } + + // PdfCraft patch: see `MAX_MESH_TRIANGLES`. + if triangles.len() >= MAX_MESH_TRIANGLES { + warn!("free-form mesh has over {MAX_MESH_TRIANGLES} triangles; the rest is not drawn"); + break; + } + triangles.push(Triangle::new(a.clone()?, b.clone()?, c.clone()?)); + } + + Some(triangles) +} + +/// Common interpolation functions used across different shading types. +struct InterpolationHelpers { + bp_coord: u8, + bp_comp: u8, + coord_max: f32, + comp_max: f32, + x_min: f32, + x_max: f32, + y_min: f32, + y_max: f32, +} + +impl InterpolationHelpers { + fn new(bp_coord: u8, bp_comp: u8, x_min: f32, x_max: f32, y_min: f32, y_max: f32) -> Self { + let coord_max = 2.0_f32.powi(bp_coord as i32) - 1.0; + let comp_max = 2.0_f32.powi(bp_comp as i32) - 1.0; + Self { + bp_coord, + bp_comp, + coord_max, + comp_max, + x_min, + x_max, + y_min, + y_max, + } + } + + fn interpolate_coord(&self, n: u32, d_min: f32, d_max: f32) -> f32 { + interpolate(n as f32, 0.0, self.coord_max, d_min, d_max) + } + + fn interpolate_comp(&self, n: u32, d_min: f32, d_max: f32) -> f32 { + interpolate(n as f32, 0.0, self.comp_max, d_min, d_max) + } + + fn read_point(&self, reader: &mut BitReader<'_>) -> Option { + let x = self.interpolate_coord(reader.read(self.bp_coord)?, self.x_min, self.x_max); + let y = self.interpolate_coord(reader.read(self.bp_coord)?, self.y_min, self.y_max); + Some(Point::new(x as f64, y as f64)) + } + + fn read_colors( + &self, + reader: &mut BitReader<'_>, + has_function: bool, + decode: &[f32], + ) -> Option { + let mut colors = smallvec![]; + if has_function { + colors.push(self.interpolate_comp( + reader.read(self.bp_comp)?, + *decode.first()?, + *decode.get(1)?, + )); + } else { + let num_components = decode.len() / 2; + for (_, decode) in (0..num_components).zip(decode.chunks_exact(2)) { + colors.push(self.interpolate_comp( + reader.read(self.bp_comp)?, + decode[0], + decode[1], + )); + } + } + Some(colors) + } + + fn read_triangle_vertex( + &self, + reader: &mut BitReader<'_>, + bpf: u8, + has_function: bool, + decode: &[f32], + ) -> Option { + let flag = reader.read(bpf)?; + let point = self.read_point(reader)?; + let colors = self.read_colors(reader, has_function, decode)?; + reader.align(); + + Some(TriangleVertex { + flag, + point, + colors, + }) + } +} + +/// Split decode array into coordinate bounds and component decode values. +fn split_decode(decode: &[f32]) -> Option<([f32; 4], &[f32])> { + decode.split_first_chunk::<4>().map(|(a, b)| (*a, b)) +} + +/// Generate triangles from a grid of points using a mapping function. +fn generate_patch_triangles(map_coordinate: F, interpolate: I, buffer: &mut Vec) +where + F: Fn(Point) -> Point, + I: Fn(Point) -> ColorComponents, +{ + const GRID_SIZE: usize = 20; + let mut grid = vec![vec![Point::ZERO; GRID_SIZE]; GRID_SIZE]; + + // Create grid by mapping unit square coordinates. + for i in 0..GRID_SIZE { + for j in 0..GRID_SIZE { + let u = i as f64 / (GRID_SIZE - 1) as f64; // 0.0 to 1.0 (left to right). + let v = j as f64 / (GRID_SIZE - 1) as f64; // 0.0 to 1.0 (top to bottom). + + // Map unit square coordinate to patch coordinate. + let unit_point = Point::new(u, v); + grid[i][j] = map_coordinate(unit_point); + } + } + + for i in 0..(GRID_SIZE - 1) { + for j in 0..(GRID_SIZE - 1) { + let p00 = grid[i][j]; + let p10 = grid[i + 1][j]; + let p01 = grid[i][j + 1]; + let p11 = grid[i + 1][j + 1]; + + // Calculate unit square coordinates for color interpolation. + let u0 = i as f64 / (GRID_SIZE - 1) as f64; + let u1 = (i + 1) as f64 / (GRID_SIZE - 1) as f64; + let v0 = j as f64 / (GRID_SIZE - 1) as f64; + let v1 = (j + 1) as f64 / (GRID_SIZE - 1) as f64; + + // Create triangle vertices with interpolated colors. + let v00 = TriangleVertex { + flag: 0, + point: p00, + colors: interpolate(Point::new(u0, v0)), + }; + let v10 = TriangleVertex { + flag: 0, + point: p10, + colors: interpolate(Point::new(u1, v0)), + }; + let v01 = TriangleVertex { + flag: 0, + point: p01, + colors: interpolate(Point::new(u0, v1)), + }; + let v11 = TriangleVertex { + flag: 0, + point: p11, + colors: interpolate(Point::new(u1, v1)), + }; + + let inflate_point = |p: Point, mid: Point| -> Point { + const INFLATION_FACTOR: f64 = 1.025; + mid + (p - mid) * INFLATION_FACTOR + }; + + // Converting to triangles is lossy and can lead to gaps. To make this a bit better, + // we slightly inflate the triangles. + let inflate = |mut triangle: Triangle| { + let mid = triangle.kurbo_tri.centroid(); + triangle.p0.point = inflate_point(triangle.p0.point, mid); + triangle.p1.point = inflate_point(triangle.p1.point, mid); + triangle.p2.point = inflate_point(triangle.p2.point, mid); + + triangle + }; + + buffer.push(inflate(Triangle::new( + v00.clone(), + v10.clone(), + v01.clone(), + ))); + buffer.push(inflate(Triangle::new( + v10.clone(), + v11.clone(), + v01.clone(), + ))); + } + } +} + +fn read_lattice_triangles( + data: &[u8], + bp_cord: u8, + bp_comp: u8, + has_function: bool, + vertices_per_row: u32, + decode: &[f32], +) -> Option> { + // PdfCraft patch: ISO 32000-2 8.7.4.5.6 requires at least two vertices per row; with none, + // the loop below pushed empty rows for ever, and one row of 4294967295 read the whole stream + // into memory before any cap (see `MAX_MESH_TRIANGLES`). + if !(2..=(MAX_MESH_TRIANGLES / 2) as u32).contains(&vertices_per_row) { + warn!("lattice-form mesh with {vertices_per_row} vertices per row"); + + return None; + } + + let mut lattices = vec![]; + + let ([x_min, x_max, y_min, y_max], decode) = split_decode(decode)?; + let mut reader = BitReader::new(data); + let helpers = InterpolationHelpers::new(bp_cord, bp_comp, x_min, x_max, y_min, y_max); + + let read_single = |reader: &mut BitReader<'_>| -> Option { + let point = helpers.read_point(reader)?; + let colors = helpers.read_colors(reader, has_function, decode)?; + reader.align(); + + Some(TriangleVertex { + flag: 0, + point, + colors, + }) + }; + + 'outer: loop { + // PdfCraft patch: see `MAX_MESH_TRIANGLES` (two triangles per vertex at most). + if lattices.len().saturating_mul(vertices_per_row as usize) >= MAX_MESH_TRIANGLES / 2 { + warn!("lattice-form mesh has too many vertices; the rest is not drawn"); + break; + } + let mut single_row = vec![]; + + for _ in 0..vertices_per_row { + let Some(next) = read_single(&mut reader) else { + break 'outer; + }; + + single_row.push(next); + } + + lattices.push(single_row); + } + + let mut triangles = vec![]; + + for i in 0..lattices.len().saturating_sub(1) { + for j in 0..(vertices_per_row as usize).saturating_sub(1) { + triangles.push(Triangle::new( + lattices[i][j].clone(), + lattices[i + 1][j].clone(), + lattices[i][j + 1].clone(), + )); + + triangles.push(Triangle::new( + lattices[i + 1][j + 1].clone(), + lattices[i + 1][j].clone(), + lattices[i][j + 1].clone(), + )); + } + } + + Some(triangles) +} + +fn read_coons_patch_mesh( + data: &[u8], + bpf: u8, + bp_coord: u8, + bp_comp: u8, + has_function: bool, + decode: &[f32], +) -> Option> { + read_patch_mesh( + data, + bpf, + bp_coord, + bp_comp, + has_function, + decode, + 12, + |control_points, colors| { + let mut coons_points = [Point::ZERO; 12]; + coons_points.copy_from_slice(&control_points[0..12]); + CoonsPatch { + control_points: coons_points, + colors, + } + }, + ) +} + +/// Generic patch mesh reading function that works for both Coons and Tensor Product patches. +#[allow(clippy::too_many_arguments)] +fn read_patch_mesh( + data: &[u8], + bpf: u8, + bp_coord: u8, + bp_comp: u8, + has_function: bool, + decode: &[f32], + control_points_count: usize, + create_patch: F, +) -> Option> +where + F: Fn([Point; 16], [ColorComponents; 4]) -> P, +{ + let ([x_min, x_max, y_min, y_max], decode) = split_decode(decode)?; + let mut reader = BitReader::new(data); + let helpers = InterpolationHelpers::new(bp_coord, bp_comp, x_min, x_max, y_min, y_max); + + let read_colors = |reader: &mut BitReader<'_>| -> Option { + helpers.read_colors(reader, has_function, decode) + }; + + let mut prev_patch_points: Option> = None; + let mut prev_patch_colors: Option<[ColorComponents; 4]> = None; + let mut patches = vec![]; + + while let Some(flag) = reader.read(bpf) { + // PdfCraft patch: see `MAX_MESH_PATCHES`. + if patches.len() >= MAX_MESH_PATCHES { + warn!("patch mesh has over {MAX_MESH_PATCHES} patches; the rest is not drawn"); + break; + } + let mut control_points = vec![Point::ZERO; 16]; // Always allocate 16, use subset as needed. + let mut colors = [smallvec![], smallvec![], smallvec![], smallvec![]]; + + match flag { + 0 => { + for i in 0..control_points_count { + control_points[i] = helpers.read_point(&mut reader)?; + } + + for i in 0..4 { + colors[i] = read_colors(&mut reader)?; + } + + prev_patch_points = Some(control_points.clone()); + prev_patch_colors = Some(colors.clone()); + } + 1..=3 => { + let prev_points = prev_patch_points.as_ref()?; + let prev_colors = prev_patch_colors.as_ref()?; + + copy_patch_control_points(flag, prev_points, &mut control_points); + + match flag { + 1 => { + colors[0] = prev_colors[1].clone(); + colors[1] = prev_colors[2].clone(); + } + 2 => { + colors[0] = prev_colors[2].clone(); + colors[1] = prev_colors[3].clone(); + } + 3 => { + colors[0] = prev_colors[3].clone(); + colors[1] = prev_colors[0].clone(); + } + _ => unreachable!(), + } + + for i in 4..control_points_count { + control_points[i] = helpers.read_point(&mut reader)?; + } + + colors[2] = read_colors(&mut reader)?; + colors[3] = read_colors(&mut reader)?; + + prev_patch_points = Some(control_points.clone()); + prev_patch_colors = Some(colors.clone()); + } + _ => break, + } + + let mut fixed_points = [Point::ZERO; 16]; + for i in 0..16 { + if i < control_points.len() { + fixed_points[i] = control_points[i]; + } + } + + patches.push(create_patch(fixed_points, colors)); + } + Some(patches) +} + +fn copy_patch_control_points( + flag: u32, + prev_control_points: &[Point], + control_points: &mut [Point], +) { + match flag { + 1 => { + control_points[0] = prev_control_points[3]; + control_points[1] = prev_control_points[4]; + control_points[2] = prev_control_points[5]; + control_points[3] = prev_control_points[6]; + } + 2 => { + control_points[0] = prev_control_points[6]; + control_points[1] = prev_control_points[7]; + control_points[2] = prev_control_points[8]; + control_points[3] = prev_control_points[9]; + } + 3 => { + control_points[0] = prev_control_points[9]; + control_points[1] = prev_control_points[10]; + control_points[2] = prev_control_points[11]; + control_points[3] = prev_control_points[0]; + } + _ => {} + } +} + +fn read_tensor_product_patch_mesh( + data: &[u8], + bpf: u8, + bp_coord: u8, + bp_comp: u8, + has_function: bool, + decode: &[f32], +) -> Option> { + read_patch_mesh( + data, + bpf, + bp_coord, + bp_comp, + has_function, + decode, + 16, + |control_points, colors| TensorProductPatch { + control_points, + colors, + }, + ) +} + +fn read_function(dict: &Dict<'_>, color_space: &ColorSpace) -> Option { + if let Some(arr) = dict.get::>(FUNCTION) { + let arr: Option> = arr + .iter::>() + .map(|o| Function::new(&o)) + .collect(); + let arr = arr?; + + if arr.len() != color_space.num_components() as usize { + warn!("function array of shading has wrong size"); + + return None; + } + + Some(ShadingFunction::Multiple(arr)) + } else if let Some(obj) = dict.get::>(FUNCTION) { + Some(ShadingFunction::Single(Function::new(&obj)?)) + } else { + None + } +} diff --git a/vendor/hayro-interpret/src/soft_mask.rs b/vendor/hayro-interpret/src/soft_mask.rs new file mode 100644 index 00000000..92cab551 --- /dev/null +++ b/vendor/hayro-interpret/src/soft_mask.rs @@ -0,0 +1,283 @@ +use crate::color::{Color, ColorComponents, ColorSpace}; +use crate::context::{Context, InterpreterCache}; +use crate::device::Device; +use crate::function::Function; +use crate::interpret::state::State; +use crate::util::hash128; +use crate::x_object::{FormXObject, draw_form_xobject}; +use crate::{CacheKey, InterpreterSettings}; +use hayro_syntax::object::Name; +use hayro_syntax::object::ObjectIdentifier; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::*; +use hayro_syntax::object::{Dict, Object}; +use hayro_syntax::page::Resources; +use hayro_syntax::xref::XRef; +use kurbo::Affine; +use smallvec::smallvec; +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; +use std::ops::Deref; +use std::rc::Rc; + +/// Type type of mask. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MaskType { + /// A luminosity mask. + Luminosity, + /// An alpha mask. + Alpha, +} + +/// A transfer function to apply to the opacity values of a mask. +pub struct TransferFunction(Function); + +impl TransferFunction { + /// Apply the transfer function to byte opacity values. + /// + /// Inputs and outputs use the same rounded 8-bit samples as the renderer. A function + /// that cannot evaluate a sample leaves it unchanged. + pub fn apply_to(&self, values: &mut [u8]) { + for value in values { + if let Some(output) = self.0.eval(smallvec![f32::from(*value) / 255.0]) + .and_then(|v| v.first().copied()) + .filter(|output| output.is_finite()) + { + *value = (output.clamp(0.0, 1.0) * 255.0 + 0.5) as u8; + } + } + } + + /// Apply the transfer function to the given value. + /// + /// The input value needs to be between 0 and 1 and the return value is + /// guaranteed to be between 0 and 1. + #[inline] + pub fn apply(&self, val: f32) -> f32 { + self.0 + .eval(smallvec![val]) + .and_then(|v| v.first().copied()) + .unwrap_or(0.0) + .clamp(0.0, 1.0) + } +} + +struct Repr<'a> { + cache_key: u128, + obj_id: ObjectIdentifier, + group: FormXObject<'a>, + mask_type: MaskType, + parent_resources: Resources<'a>, + root_transform: Affine, + bbox: kurbo::Rect, + interpreter_cache: InterpreterCache<'a>, + transfer_function: Option, + settings: InterpreterSettings, + background: Color, + xref: &'a XRef, + nesting_depth: u32, +} + +impl Hash for Repr<'_> { + fn hash(&self, state: &mut H) { + self.obj_id.hash(state); + } +} + +/// A soft mask. +#[derive(Clone, Hash)] +pub struct SoftMask<'a>(Rc>); + +impl Debug for SoftMask<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SoftMask({:?})", self.0.obj_id) + } +} + +impl PartialEq for SoftMask<'_> { + fn eq(&self, other: &Self) -> bool { + self.0.obj_id == other.0.obj_id + } +} + +impl Eq for SoftMask<'_> {} + +impl CacheKey for SoftMask<'_> { + fn cache_key(&self) -> u128 { + self.0.cache_key + } +} + +impl<'a> SoftMask<'a> { + pub(crate) fn new( + dict: &Dict<'a>, + context: &Context<'a>, + parent_resources: Resources<'a>, + ) -> Option { + // TODO: With this setup, if there is a luminosity mask and alpha mask pointing to the + // same xobject, the ID will be the same. + let obj_id = dict.get_ref(G)?.into(); + let group_stream = dict.get::>(G)?; + let group = FormXObject::new(&group_stream)?; + // PdfCraft patch: the group's /CS is optional (ISO 32000-2 ยง11.6.5.2; Chrome/Skia omit + // it). Upstream returned None here, silently dropping the whole soft mask. Fall back to + // DeviceRGB, the blending space our renderer composites in. + let cs = group + .dict + .get::>(GROUP) + .and_then(|g| g.get::>(CS)) + .and_then(|o| ColorSpace::new(o, &context.interpreter_cache.object_cache)) + .unwrap_or_else(ColorSpace::device_rgb); + let transfer_function = dict + .get::>(TR) + .and_then(|o| Function::new(&o)) + .map(TransferFunction); + let (mask_type, background) = match dict.get::>(S)?.deref() { + LUMINOSITY => { + let color = dict + .get::(BC) + .map(|c| Color::new(cs, c, 1.0)) + .unwrap_or(Color::new(ColorSpace::device_gray(), smallvec![0.0], 1.0)); + + (MaskType::Luminosity, color) + } + ALPHA => ( + MaskType::Alpha, + // Background color attribute should only be used with luminosity masks. + Color::new(ColorSpace::device_gray(), smallvec![0.0], 1.0), + ), + _ => return None, + }; + let nesting_depth = context.nesting_depth() + 1; + let root_transform = context.get().ctm; + let bbox = context.bbox(); + let resources_key = resources_cache_key(&parent_resources); + let cache_key = hash128(&( + dict.cache_key(), + root_transform.cache_key(), + [bbox.x0.to_bits(), bbox.y0.to_bits(), bbox.x1.to_bits(), bbox.y1.to_bits()], + resources_key, + nesting_depth, + )); + + Some(Self(Rc::new(Repr { + cache_key, + obj_id, + group, + mask_type, + root_transform, + transfer_function, + bbox: context.bbox(), + interpreter_cache: context.interpreter_cache.clone(), + settings: context.settings.clone(), + xref: context.xref, + background, + parent_resources, + nesting_depth, + }))) + } + + /// Interpret the contents of the mask into the given device. + pub fn interpret(&self, device: &mut impl Device<'a>) { + let state = State::new(self.0.root_transform); + let mut ctx = Context::new_with( + self.0.root_transform, + self.0.bbox, + &self.0.interpreter_cache, + self.0.xref, + self.0.settings.clone(), + state, + self.0.nesting_depth, + ); + draw_form_xobject(&self.0.parent_resources, &self.0.group, &mut ctx, device); + } + + /// Return the object identifier of the mask. + /// + /// This can be used as a unique identifier for caching purposes. + pub fn id(&self) -> ObjectIdentifier { + self.0.obj_id + } + + /// Return the underlying mask type. + pub fn mask_type(&self) -> MaskType { + self.0.mask_type + } + + /// The background color against which the mask should be composited. + pub fn background_color(&self) -> Color { + self.0.background.clone() + } + + /// Return the transfer function that should be used for the mask. + pub fn transfer_function(&self) -> Option<&TransferFunction> { + self.0.transfer_function.as_ref() + } +} + +fn resources_cache_key(resources: &Resources<'_>) -> u128 { + let local = ( + resources.ext_g_states.obj_id(), + resources.fonts.obj_id(), + resources.properties.obj_id(), + resources.color_spaces.obj_id(), + resources.x_objects.obj_id(), + resources.patterns.obj_id(), + resources.shadings.obj_id(), + ); + hash128(&(local, resources.parent().map(resources_cache_key))) +} + +#[cfg(test)] +mod transfer_tests { + use super::*; + use hayro_syntax::object::FromBytes; + + fn transfer(bytes: &[u8]) -> TransferFunction { + let dict = Dict::from_bytes(bytes).expect("synthetic function dictionary"); + TransferFunction(Function::new(&Object::Dict(dict)).expect("synthetic function")) + } + + #[test] + fn byte_transfer_rounds_near_equal_endpoints() { + let function = transfer(b"<< /FunctionType 2 /Domain [0 1] /C0 [0.5001] /C1 [0.5] /N 1 >>"); + assert!(function.apply(0.0) > function.apply(1.0)); + let mut ends = [0, 255]; + function.apply_to(&mut ends); + assert_eq!(ends, [128, 128], "sub-byte differences do not invert a byte mask"); + } + + #[test] + fn byte_transfer_preserves_identity_and_maps_inversion() { + for (function, expected) in [ + (b"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>".as_slice(), [0, 1, 127, 128, 254, 255]), + (b"<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>".as_slice(), [255, 254, 128, 127, 1, 0]), + ] { + let mut values = [0, 1, 127, 128, 254, 255]; + transfer(function).apply_to(&mut values); + assert_eq!(values, expected); + } + } + + #[test] + fn byte_transfer_keeps_a_sample_when_the_function_has_no_output() { + let function = transfer(b"<< /FunctionType 2 /Domain [0 1] /C0 [] /C1 [] /N 1 >>"); + let mut values = [0, 17, 127, 255]; + function.apply_to(&mut values); + assert_eq!(values, [0, 17, 127, 255]); + } + + #[test] + fn byte_transfer_keeps_samples_when_malformed_functions_are_not_finite() { + // Fractional powers of a negative domain produce NaN; finite coefficients can + // also overflow during interpolation. Neither is a byte opacity value. + for bytes in [ + b"<< /FunctionType 2 /Domain [-1 -0.5] /C0 [0] /C1 [1] /N 0.5 >>".as_slice(), + b"<< /FunctionType 2 /Domain [0 1] /C0 [-300000000000000000000000000000000000000] /C1 [300000000000000000000000000000000000000] /N 1 >>", + ] { + let mut values = [0, 17, 127, 255]; + transfer(bytes).apply_to(&mut values); + assert_eq!(values, [0, 17, 127, 255]); + } + } +} diff --git a/vendor/hayro-interpret/src/types.rs b/vendor/hayro-interpret/src/types.rs new file mode 100644 index 00000000..64d2b849 --- /dev/null +++ b/vendor/hayro-interpret/src/types.rs @@ -0,0 +1,366 @@ +use crate::CacheKey; +use crate::color::Color; +use crate::pattern::Pattern; +use crate::util::hash128; +use crate::x_object::ImageXObject; +use hayro_syntax::object::Stream; +use kurbo::{BezPath, Cap, Join}; +use smallvec::{SmallVec, smallvec}; + +/// A clip path. +#[derive(Debug, Clone)] +pub struct ClipPath { + /// The clipping path. + pub path: BezPath, + /// The fill rule. + pub fill: FillRule, +} + +impl CacheKey for ClipPath { + fn cache_key(&self) -> u128 { + hash128(&(&self.path.to_svg(), &self.fill)) + } +} + +/// A stencil image. +pub struct StencilImage<'a, 'b> { + pub(crate) paint: Paint<'a>, + pub(crate) image_xobject: ImageXObject<'b>, +} + +impl<'a, 'b> StencilImage<'a, 'b> { + /// Perform some operation with the stencil data of the image. + /// + /// The second argument allows you to give the image decoder a hint for + /// what resolution of the image you want to have. Note that this does not + /// mean that the resulting image will have that dimension. Instead, it allows + /// the image decoder to extract a lower-resolution version of the image in + /// certain cases. + pub fn with_stencil( + &self, + func: impl FnOnce(LumaData, &Paint<'a>), + target_dimension: Option<(u32, u32)>, + ) { + if let Some(decoded) = self.image_xobject.decoded_mask(target_dimension) { + func(decoded.luma, &self.paint); + } + } + + // These are hidden since clients are supposed to call get the + // width/height from `LumaData` instead. + #[doc(hidden)] + pub fn width(&self) -> u32 { + self.image_xobject.width() + } + + #[doc(hidden)] + pub fn height(&self) -> u32 { + self.image_xobject.height() + } +} + +impl CacheKey for StencilImage<'_, '_> { + fn cache_key(&self) -> u128 { + self.image_xobject.cache_key() + } +} + +/// A raster image. +pub struct RasterImage<'a>(pub(crate) ImageXObject<'a>); + +impl RasterImage<'_> { + /// Perform some operation with the RGB and alpha channel of the image. + /// + /// The second argument allows you to give the image decoder a hint for + /// what resolution of the image you want to have. Note that this does not + /// mean that the resulting image will have that dimension. Instead, it allows + /// the image decoder to extract a lower-resolution version of the image in + /// certain cases. + pub fn with_rgba( + &self, + func: impl FnOnce(ImageData, Option), + target_dimension: Option<(u32, u32)>, + ) { + if let Some(decoded) = self.0.decoded_raster(target_dimension) { + func(decoded.image, decoded.alpha); + } + } + + /// Return the underlying stream object. + /// + /// This allows you to get access to the raw encoded image data, without doing any decoding. + pub fn stream(&self) -> &Stream<'_> { + self.0.stream() + } + + // These are hidden since clients are supposed to call get the + // width/height from `LumaData` instead. + #[doc(hidden)] + pub fn width(&self) -> u32 { + self.0.width() + } + + #[doc(hidden)] + pub fn height(&self) -> u32 { + self.0.height() + } +} + +impl CacheKey for RasterImage<'_> { + fn cache_key(&self) -> u128 { + self.0.cache_key() + } +} + +/// A type of image. +pub enum Image<'a, 'b> { + /// A stencil image. + Stencil(StencilImage<'a, 'b>), + /// A normal raster image. + Raster(RasterImage<'b>), +} + +impl Image<'_, '_> { + // These are hidden since clients are supposed to call get the + // width/height from `LumaData/RgbData` instead. + #[doc(hidden)] + pub fn width(&self) -> u32 { + match self { + Image::Stencil(s) => s.width(), + Image::Raster(r) => r.width(), + } + } + + // These are hidden since clients are supposed to call get the + // width/height from `LumaData/RgbData` instead. + #[doc(hidden)] + pub fn height(&self) -> u32 { + match self { + Image::Stencil(s) => s.height(), + Image::Raster(r) => r.height(), + } + } +} + +impl CacheKey for Image<'_, '_> { + fn cache_key(&self) -> u128 { + match self { + Image::Stencil(i) => i.cache_key(), + Image::Raster(i) => i.cache_key(), + } + } +} + +/// A structure holding 3-channel RGB data. +#[derive(Clone)] +pub struct RgbData { + /// The actual data. It is guaranteed to have the length width * height * 3. + pub data: Vec, + /// The width. + pub width: u32, + /// The height. + pub height: u32, + /// Whether the image should be interpolated. + pub interpolate: bool, + /// Additional scaling factors to apply to the image. + /// + /// In most cases, those factors will just be 1.0, and you can + /// ignore them. There are two situations in which they will not be equal + /// to 1: + /// 1) The PDF provided wrong metadata about the width/height of the image, + /// which needs to be corrected + /// 2) A lower resolution of the image was requested, in which case it needs + /// to be scaled up so that it still covers the same area. + /// + /// The first number indicates the x scaling factor, the second number the + /// y scaling factor. + pub scale_factors: (f32, f32), +} + +/// A structure holding 1-channel luma data. +#[derive(Clone)] +pub struct LumaData { + /// The actual data. It is guaranteed to have the length width * height. + pub data: Vec, + /// The width. + pub width: u32, + /// The height. + pub height: u32, + /// Whether the image should be interpolated. + pub interpolate: bool, + /// Additional scaling factors to apply to the image. + /// + /// In most cases, those factors will just be 1.0, and you can + /// ignore them. There are two situations in which they will not be equal + /// to 1: + /// 1) The PDF provided wrong metadata about the width/height of the image, + /// which needs to be corrected + /// 2) A lower resolution of the image was requested, in which case it needs + /// to be scaled up so that it still covers the same area. + /// + /// The first number indicates the x scaling factor, the second number the + /// y scaling factor. + pub scale_factors: (f32, f32), +} + +/// The color data of a raster image, either 3-channel RGB or 1-channel luma. +#[derive(Clone)] +pub enum ImageData { + /// 3-channel RGB data. + Rgb(RgbData), + /// 1-channel grayscale data. + Luma(LumaData), +} + +impl ImageData { + /// The width of the image. + pub fn width(&self) -> u32 { + match self { + Self::Rgb(d) => d.width, + Self::Luma(d) => d.width, + } + } + + /// The height of the image. + pub fn height(&self) -> u32 { + match self { + Self::Rgb(d) => d.height, + Self::Luma(d) => d.height, + } + } + + /// Whether the image should be interpolated. + pub fn interpolate(&self) -> bool { + match self { + Self::Rgb(d) => d.interpolate, + Self::Luma(d) => d.interpolate, + } + } + + /// The scaling factors of the image. + pub fn scale_factors(&self) -> (f32, f32) { + match self { + Self::Rgb(d) => d.scale_factors, + Self::Luma(d) => d.scale_factors, + } + } +} + +/// A type of paint. +#[derive(Clone, Debug)] +pub enum Paint<'a> { + /// A solid RGBA color. + Color(Color), + /// A PDF pattern. + Pattern(Box>), +} + +impl CacheKey for Paint<'_> { + fn cache_key(&self) -> u128 { + match self { + Paint::Color(c) => { + // TODO: We should actually cache the color with color space etc., not just the + // RGBA8 version. + hash128(&c.to_rgba().to_rgba8()) + } + Paint::Pattern(p) => p.cache_key(), + } + } +} + +/// The draw mode that should be used for a path. +#[derive(Clone, Debug)] +pub enum PathDrawMode { + /// Draw using a fill. + Fill(FillRule), + /// Draw using a stroke. + Stroke(StrokeProps), +} + +/// The draw mode that should be used for a glyph. +#[derive(Clone, Debug)] +pub enum GlyphDrawMode { + /// Draw using a fill. + Fill, + /// Draw using a stroke. + Stroke(StrokeProps), + /// Invisible text (for text extraction but not visual rendering). + Invisible, +} + +/// Stroke properties. +#[derive(Clone, Debug)] +pub struct StrokeProps { + /// The line width. + pub line_width: f32, + /// The line cap. + pub line_cap: Cap, + /// The line join. + pub line_join: Join, + /// The miter limit. + pub miter_limit: f32, + /// The dash array. + pub dash_array: SmallVec<[f32; 4]>, + /// The dash offset. + pub dash_offset: f32, +} + +impl Default for StrokeProps { + fn default() -> Self { + Self { + line_width: 1.0, + line_cap: Cap::Butt, + line_join: Join::Miter, + miter_limit: 10.0, + dash_array: smallvec![], + dash_offset: 0.0, + } + } +} + +/// A fill rule. +#[derive(Clone, Debug, Copy, Hash, PartialEq, Eq)] +pub enum FillRule { + /// Non-zero filling. + NonZero, + /// Even-odd filling. + EvenOdd, +} + +/// A blend mode. +#[derive(Clone, Debug, Copy, Hash, PartialEq, Eq, Default)] +pub enum BlendMode { + /// Normal blend mode (default). + #[default] + Normal, + /// Multiply blend mode. + Multiply, + /// Screen blend mode. + Screen, + /// Overlay blend mode. + Overlay, + /// Darken blend mode. + Darken, + /// Lighten blend mode. + Lighten, + /// `ColorDodge` blend mode. + ColorDodge, + /// `ColorBurn` blend mode. + ColorBurn, + /// `HardLight` blend mode. + HardLight, + /// `SoftLight` blend mode. + SoftLight, + /// Difference blend mode. + Difference, + /// Exclusion blend mode. + Exclusion, + /// Hue blend mode. + Hue, + /// Saturation blend mode. + Saturation, + /// Color blend mode. + Color, + /// Luminosity blend mode. + Luminosity, +} diff --git a/vendor/hayro-interpret/src/util.rs b/vendor/hayro-interpret/src/util.rs new file mode 100644 index 00000000..d39b450b --- /dev/null +++ b/vendor/hayro-interpret/src/util.rs @@ -0,0 +1,183 @@ +//! A number of utility methods. + +use kurbo::{BezPath, PathEl, Rect}; +use siphasher::sip128::{Hasher128, SipHasher13}; +use std::hash::Hash; +use std::ops::Sub; + +pub(crate) trait OptionLog { + fn warn_none(self, f: &str) -> Self; +} + +impl OptionLog for Option { + #[inline] + fn warn_none(self, _f: &str) -> Self { + self.or_else(|| { + warn!("{_f}"); + + None + }) + } +} + +const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 8) as f32; + +/// A number of useful methods for f32 numbers. +pub trait Float32Ext: Sized + Sub + Copy + PartialOrd { + /// Whether the number is approximately 0. + fn is_nearly_zero(&self) -> bool { + self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO) + } + + /// Whether the number is nearly equal to another number. + fn is_nearly_equal(&self, other: f32) -> bool { + (*self - other).is_nearly_zero() + } + + /// Whether the number is nearly equal to another number. + fn is_nearly_less_or_equal(&self, other: f32) -> bool { + (*self - other).is_nearly_zero() || *self < other + } + + /// Whether the number is nearly equal to another number. + fn is_nearly_greater_or_equal(&self, other: f32) -> bool { + (*self - other).is_nearly_zero() || *self > other + } + + /// Whether the number is approximately 0, with a given tolerance. + fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool; +} + +impl Float32Ext for f32 { + fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool { + debug_assert!(tolerance >= 0.0, "tolerance must be non-negative"); + + self.abs() <= tolerance + } +} + +/// A number of useful methods for f64 numbers. +pub trait Float64Ext: Sized + Sub + Copy + PartialOrd { + /// Whether the number is approximately 0. + fn is_nearly_zero(&self) -> bool { + self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO as f64) + } + + /// Whether the number is nearly equal to another number. + fn is_nearly_equal(&self, other: f64) -> bool { + (*self - other).is_nearly_zero() + } + + /// Whether the number is nearly equal to another number. + fn is_nearly_less_or_equal(&self, other: f64) -> bool { + (*self - other).is_nearly_zero() || *self < other + } + + /// Whether the number is nearly equal to another number. + fn is_nearly_greater_or_equal(&self, other: f64) -> bool { + (*self - other).is_nearly_zero() || *self > other + } + + /// Whether the number is approximately 0, with a given tolerance. + fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool; +} + +impl Float64Ext for f64 { + fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool { + debug_assert!(tolerance >= 0.0, "tolerance must be non-negative"); + + self.abs() <= tolerance + } +} + +pub(crate) trait PointExt: Sized { + fn x(&self) -> f32; + fn y(&self) -> f32; + + fn nearly_same(&self, other: Self) -> bool { + self.x().is_nearly_equal(other.x()) && self.y().is_nearly_equal(other.y()) + } +} + +impl PointExt for kurbo::Point { + fn x(&self) -> f32 { + self.x as f32 + } + + fn y(&self) -> f32 { + self.y as f32 + } +} + +/// Calculate a 128-bit siphash of a value. +pub(crate) fn hash128(value: &T) -> u128 { + let mut state = SipHasher13::new(); + value.hash(&mut state); + state.finish128().as_u128() +} + +pub(crate) trait BezPathExt { + fn fast_bounding_box(&self) -> Rect; +} + +impl BezPathExt for BezPath { + fn fast_bounding_box(&self) -> Rect { + let mut min_x = f64::INFINITY; + let mut min_y = f64::INFINITY; + let mut max_x = f64::NEG_INFINITY; + let mut max_y = f64::NEG_INFINITY; + + let mut include = |x: f64, y: f64| { + min_x = min_x.min(x); + min_y = min_y.min(y); + max_x = max_x.max(x); + max_y = max_y.max(y); + }; + + for el in self.elements() { + match *el { + PathEl::MoveTo(p) | PathEl::LineTo(p) => include(p.x, p.y), + PathEl::QuadTo(p1, p2) => { + include(p1.x, p1.y); + include(p2.x, p2.y); + } + PathEl::CurveTo(p1, p2, p3) => { + include(p1.x, p1.y); + include(p2.x, p2.y); + include(p3.x, p3.y); + } + PathEl::ClosePath => {} + } + } + + if min_x > max_x { + Rect::ZERO + } else { + Rect::new(min_x, min_y, max_x, max_y) + } + } +} + +/// Extension methods for converting a [`hayro_syntax::transform::Transform`] to a [`kurbo::Affine`]. +pub trait TransformExt { + /// Convert to a `kurbo::Affine`. + fn to_kurbo(&self) -> kurbo::Affine; +} + +impl TransformExt for hayro_syntax::transform::Transform { + fn to_kurbo(&self) -> kurbo::Affine { + kurbo::Affine::new(self.as_coeffs()) + } +} + +/// Extension methods for rectangles. +pub trait RectExt { + /// Convert the rectangle to a `kurbo` rectangle. + fn to_kurbo(&self) -> Rect; +} + +impl RectExt for hayro_syntax::object::Rect { + fn to_kurbo(&self) -> Rect { + Rect::new(self.x0, self.y0, self.x1, self.y1) + } +} diff --git a/vendor/hayro-interpret/src/x_object.rs b/vendor/hayro-interpret/src/x_object.rs new file mode 100644 index 00000000..767a54f3 --- /dev/null +++ b/vendor/hayro-interpret/src/x_object.rs @@ -0,0 +1,1062 @@ +use crate::cache::{Cache, DecodedImageCache, DecodedImageKey}; +use crate::color::{ColorComponents, ColorSpace, ToRgb}; +use crate::context::Context; +use crate::device::Device; +use crate::function::{Function, interpolate}; +use crate::interpret::path::get_paint; +use crate::interpret::state::ActiveTransferFunction; +use crate::{BlendMode, CacheKey, ClipPath, Image, RasterImage, StencilImage}; +use crate::{FillRule, InterpreterWarning, WarningSinkFn, interpret}; +use crate::{ImageData, LumaData, RgbData}; +use hayro_syntax::bit_reader::BitReader; +use hayro_syntax::content::TypedIter; +use hayro_syntax::object::Array; +use hayro_syntax::object::Dict; +use hayro_syntax::object::Name; +use hayro_syntax::object::Object; +use hayro_syntax::object::Stream; +use hayro_syntax::object::dict::keys::*; +use hayro_syntax::object::stream::{FilterResult, ImageColorSpace, ImageDecodeParams}; +use hayro_syntax::page::Resources; +use kurbo::{Affine, Rect, Shape}; +use smallvec::{SmallVec, smallvec}; +use std::borrow::Cow; +use std::iter; +use std::ops::Deref; + +pub(crate) enum XObject<'a> { + FormXObject(FormXObject<'a>), + ImageXObject(ImageXObject<'a>), +} + +impl<'a> XObject<'a> { + pub(crate) fn new( + stream: &Stream<'a>, + warning_sink: &WarningSinkFn, + cache: &Cache, + decoded_cache: Option<&DecodedImageCache>, + transfer_function: Option, + ) -> Option { + let dict = stream.dict(); + match dict.get::>(SUBTYPE)?.deref() { + IMAGE => Some(Self::ImageXObject(ImageXObject::new( + stream, + |_| None, + warning_sink, + cache, + decoded_cache, + false, + transfer_function, + )?)), + FORM => Some(Self::FormXObject(FormXObject::new(stream)?)), + _ => None, + } + } +} + +pub(crate) struct FormXObject<'a> { + // PdfCraft patch: counted against the page's content budget while the form is alive. + pub(crate) decoded: crate::context::DecodedContent<'a>, + pub(crate) matrix: Affine, + pub(crate) bbox: [f32; 4], + is_transparency_group: bool, + pub(crate) dict: Dict<'a>, + resources: Option>, +} + +impl<'a> FormXObject<'a> { + pub(crate) fn new(stream: &Stream<'a>) -> Option { + let dict = stream.dict(); + + // PdfCraft patch: see `context::MAX_PAGE_CONTENT`. + let decoded = crate::context::decode_content(stream)?; + let resources = dict.get::>(RESOURCES); + + let matrix = Affine::new( + dict.get::<[f64; 6]>(MATRIX) + .unwrap_or([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]), + ); + let bbox = dict.get::<[f32; 4]>(BBOX)?; + let is_transparency_group = dict.get::>(GROUP).is_some(); + + Some(Self { + decoded, + matrix, + is_transparency_group, + bbox, + dict: dict.clone(), + resources, + }) + } +} + +pub(crate) fn draw_xobject<'a>( + x_object: &XObject<'a>, + resources: &Resources<'a>, + context: &mut Context<'a>, + device: &mut impl Device<'a>, +) { + match x_object { + XObject::FormXObject(f) => draw_form_xobject(resources, f, context, device), + XObject::ImageXObject(i) => { + draw_image_xobject(i, context, device); + } + } +} + +pub(crate) fn draw_form_xobject<'a, 'b>( + resources: &Resources<'a>, + x_object: &'b FormXObject<'a>, + context: &mut Context<'a>, + device: &mut impl Device<'a>, +) { + if !context.ocg_state.is_visible() { + return; + } + + if !context.begin_nested_interpretation() { + return; + } + + let has_oc = xobject_oc(&x_object.dict, context); + if !context.ocg_state.is_visible() { + if has_oc { + context.ocg_state.end_marked_content(); + } + context.end_nested_interpretation(); + return; + } + + let iter = TypedIter::new(x_object.decoded.as_ref()); + + context.path_mut().truncate(0); + context.save_state(); + context.pre_concat_affine(x_object.matrix); + context.push_root_transform(); + + if x_object.is_transparency_group { + device.push_transparency_group( + context.get().graphics_state.non_stroke_alpha, + std::mem::take(&mut context.get_mut().graphics_state.soft_mask), + std::mem::take(&mut context.get_mut().graphics_state.blend_mode), + ); + + context.get_mut().graphics_state.non_stroke_alpha = 1.0; + context.get_mut().graphics_state.stroke_alpha = 1.0; + } + + device.set_soft_mask(context.get().graphics_state.soft_mask.clone()); + device.set_blend_mode(context.get().graphics_state.blend_mode); + + device.push_clip_path(&ClipPath { + path: context.get().ctm + * Rect::new( + x_object.bbox[0] as f64, + x_object.bbox[1] as f64, + x_object.bbox[2] as f64, + x_object.bbox[3] as f64, + ) + .to_path(0.1), + fill: FillRule::NonZero, + }); + + interpret( + iter, + &Resources::from_scope(x_object.resources.clone(), resources.clone()), + context, + device, + ); + + device.pop_clip_path(); + + if x_object.is_transparency_group { + device.pop_transparency_group(); + } + + context.pop_root_transform(); + context.restore_state(device); + + if has_oc { + context.ocg_state.end_marked_content(); + } + + context.end_nested_interpretation(); +} + +pub(crate) fn draw_image_xobject<'a, 'b>( + x_object: &ImageXObject<'b>, + context: &mut Context<'a>, + device: &mut impl Device<'a>, +) { + if !context.ocg_state.is_visible() { + return; + } + + let has_oc = xobject_oc(x_object.stream.dict(), context); + if !context.ocg_state.is_visible() { + if has_oc { + context.ocg_state.end_marked_content(); + } + return; + } + + let width = x_object.width as f64; + let height = x_object.height as f64; + + context.save_state(); + context.pre_concat_affine(Affine::new([ + 1.0 / width, + 0.0, + 0.0, + -1.0 / height, + 0.0, + 1.0, + ])); + let transform = context.get().ctm; + + let has_alpha = x_object.has_mask(); + + let mut soft_mask = std::mem::take(&mut context.get_mut().graphics_state.soft_mask); + let blend_mode = std::mem::take(&mut context.get_mut().graphics_state.blend_mode); + + // If image has smask, the soft mask from the graphics state should be discarde. + if has_alpha { + soft_mask = None; + } + + device.push_transparency_group( + context.get().graphics_state.non_stroke_alpha, + std::mem::take(&mut soft_mask), + blend_mode, + ); + + device.set_soft_mask(None); + device.set_blend_mode(BlendMode::default()); + + let image = if x_object.is_mask { + Image::Stencil(StencilImage { + paint: get_paint(context, false), + image_xobject: x_object.clone(), + }) + } else { + Image::Raster(RasterImage(x_object.clone())) + }; + + device.draw_image(image, transform); + device.pop_transparency_group(); + + context.restore_state(device); + + if has_oc { + context.ocg_state.end_marked_content(); + } +} + +fn xobject_oc(dict: &Dict<'_>, context: &mut Context<'_>) -> bool { + let Some(oc_dict) = dict.get::>(OC) else { + return false; + }; + + if let Some(oc_ref) = dict.get_ref(OC) { + context.ocg_state.begin_ocg(&oc_dict, oc_ref.into()); + } else { + context.ocg_state.begin_ocmd(&oc_dict); + } + + true +} + +#[derive(Clone)] +pub(crate) struct ImageXObject<'a> { + width: u32, + height: u32, + color_space: Option, + cache: Cache, + /// PdfCraft patch: decoded pixels shared across renders (see `decoded_raster`); `None` + /// for an image that is never cached (an inline image). + decoded_cache: Option, + interpolate: bool, + is_mask: bool, + is_stencil_mask: bool, + stream: Stream<'a>, + transfer_function: Option, + warning_sink: WarningSinkFn, +} + +impl<'a> ImageXObject<'a> { + pub(crate) fn new( + stream: &Stream<'a>, + resolve_cs: impl FnOnce(&Name<'_>) -> Option, + warning_sink: &WarningSinkFn, + cache: &Cache, + decoded_cache: Option<&DecodedImageCache>, + mut is_mask: bool, + transfer_function: Option, + ) -> Option { + let dict = stream.dict(); + + let is_stencil_mask = dict + .get::(IM) + .or_else(|| dict.get::(IMAGE_MASK)) + .unwrap_or(false); + is_mask |= is_stencil_mask; + + let image_cs = if is_mask { + // Masks are always single-channel. + Some(ColorSpace::device_gray()) + } else { + let cs_obj = dict + .get::>(CS) + .or_else(|| dict.get::>(COLORSPACE)); + + cs_obj + .clone() + .and_then(|c| ColorSpace::new(c, cache)) + // Inline images can also refer to color spaces by name. + .or_else(|| { + cs_obj + .and_then(|c| c.into_name()) + .and_then(|n| resolve_cs(&n)) + }) + }; + + let interpolate = dict + .get::(I) + .or_else(|| dict.get::(INTERPOLATE)) + .unwrap_or(false); + + let width = dict.get::(W).or_else(|| dict.get::(WIDTH))?; + let height = dict.get::(H).or_else(|| dict.get::(HEIGHT))?; + + if width == 0 || height == 0 { + return None; + } + + Some(Self { + width, + cache: cache.clone(), + decoded_cache: decoded_cache.cloned(), + height, + color_space: image_cs, + warning_sink: warning_sink.clone(), + transfer_function, + interpolate, + stream: stream.clone(), + is_mask, + is_stencil_mask, + }) + } + + pub(crate) fn decoded_mask(&self, target_dimension: Option<(u32, u32)>) -> Option { + if !self.is_mask { + return None; + } + + // PdfCraft patch: decode once per `DecodedImageKey`; tiles and scales share it. + let Some(cache) = &self.decoded_cache else { + return decode_mask(self, target_dimension); + }; + cache.get_or_insert_with( + self.decode_key(cache, true, target_dimension), + MAX_DECODED_IMAGE_BYTES, + decoded_mask_size, + || decode_mask(self, target_dimension), + ) + } + + pub(crate) fn decoded_raster( + &self, + target_dimension: Option<(u32, u32)>, + ) -> Option { + if self.is_mask { + return None; + } + + // PdfCraft patch: decode once per `DecodedImageKey`; tiles and scales share it. + let Some(cache) = &self.decoded_cache else { + return decode_raster(self, target_dimension); + }; + cache.get_or_insert_with( + self.decode_key(cache, false, target_dimension), + MAX_DECODED_IMAGE_BYTES, + decoded_raster_size, + || decode_raster(self, target_dimension), + ) + } + + /// PdfCraft patch: the cache key of decoding this image (as a mask when `mask_output`) at + /// `target_dimension`: every input of the decode (see [`DecodedImageKey`]). `None` when one + /// of them can't be keyed cheaply (a transfer function): such a decode isn't cached. + pub(crate) fn decode_key( + &self, + cache: &DecodedImageCache, + mask_output: bool, + target_dimension: Option<(u32, u32)>, + ) -> Option { + if self.transfer_function.is_some() { + return None; + } + let dict = self.stream.dict(); + let bytes = dict.data(); + Some(DecodedImageKey { + mask_output, + dict_at: bytes.as_ptr() as usize, + dict_len: bytes.len(), + dict_hash: self.stream.cache_key(), + obj: dict.obj_id().map(|id| (id.obj_number, id.gen_number)), + is_mask: self.is_mask, + is_stencil_mask: self.is_stencil_mask, + interpolate: self.interpolate, + target: target_dimension, + preview: cache.preview, + }) + } + + pub(crate) fn width(&self) -> u32 { + self.width + } + + pub(crate) fn height(&self) -> u32 { + self.height + } + + pub(crate) fn stream(&self) -> &Stream<'a> { + &self.stream + } + + fn has_mask(&self) -> bool { + let dict = self.stream.dict(); + + dict.contains_key(SMASK_IN_DATA) || dict.contains_key(SMASK) || dict.contains_key(MASK) + } +} + +/// PdfCraft patch: budget for [`DecodedImageCache`] entries per interpreter cache: decoded +/// image pixels shared across tiles and scales. Past it the cache starts a fresh epoch. +const MAX_DECODED_IMAGE_BYTES: usize = 64 << 20; + +/// PdfCraft patch: pixel bytes held by a decoded mask. +fn decoded_mask_size(mask: &DecodedMask) -> usize { + mask.luma.width as usize * mask.luma.height as usize +} + +/// PdfCraft patch: pixel bytes held by a decoded raster image and its mask. +fn decoded_raster_size(raster: &DecodedRaster) -> usize { + let image = match &raster.image { + ImageData::Rgb(d) => d.width as usize * d.height as usize * 3, + ImageData::Luma(d) => d.width as usize * d.height as usize, + }; + let alpha = raster + .alpha + .as_ref() + .map(|a| a.width as usize * a.height as usize) + .unwrap_or(0); + image.saturating_add(alpha) +} + +#[derive(Clone)] +pub(crate) struct DecodedMask { + pub(crate) luma: LumaData, +} + +#[derive(Clone)] +pub(crate) struct DecodedRaster { + pub(crate) image: ImageData, + pub(crate) alpha: Option, +} + +struct DecodeContext<'a> { + decoded: FilterResult<'a>, + width: u32, + height: u32, + scale_factors: (f32, f32), + color_space: ColorSpace, + bits_per_component: u8, + decode_arr: SmallVec<[(f32, f32); 4]>, +} + +fn decode_context<'a>( + obj: &ImageXObject<'a>, + target_dimension: Option<(u32, u32)>, +) -> Option> { + // PdfCraft patch: images and masks over `MAX_IMAGE_PIXELS` are not decoded at all (the + // renderer skips them anyway): a fuzzed `/W 4294967295` stencil mask allocated gigabytes here. + if !image_size_ok(obj.width, obj.height) { + (obj.warning_sink)(InterpreterWarning::ImageDecodeFailure); + return None; + } + let dict = obj.stream.dict(); + let dict_bpc = dict + .get::(BPC) + .or_else(|| dict.get::(BITS_PER_COMPONENT)); + let color_space = obj.color_space.clone(); + let is_indexed = obj.color_space.as_ref().is_some_and(|cs| cs.is_indexed()); + + let decode_params = ImageDecodeParams { + is_indexed, + bpc: dict_bpc, + num_components: color_space.as_ref().map(|c| c.num_components()), + target_dimension, + width: obj.width, + height: obj.height, + }; + + let decoded = obj + .stream + .decoded_image(&decode_params) + .map_err(|_| (obj.warning_sink)(InterpreterWarning::ImageDecodeFailure)) + .ok()?; + + let (mut scale_x, mut scale_y) = (1.0, 1.0); + + let (width, height) = decoded + .image_data + .as_ref() + .map(|d| { + scale_x = obj.width as f32 / d.width as f32; + scale_y = obj.height as f32 / d.height as f32; + + (d.width, d.height) + }) + .unwrap_or((obj.width, obj.height)); + + let color_space = color_space + .or_else(|| { + decoded + .image_data + .as_ref() + .map(|i| i.color_space) + .and_then(|c| { + c.and_then(|c| match c { + ImageColorSpace::Gray => Some(ColorSpace::device_gray()), + ImageColorSpace::Rgb => Some(ColorSpace::device_rgb()), + ImageColorSpace::Cmyk => Some(ColorSpace::device_cmyk()), + ImageColorSpace::Unknown(_) => None, + }) + }) + }) + .unwrap_or(ColorSpace::device_gray()); + + let fallback_bpc = if obj.is_stencil_mask { 1 } else { 8 }; + + let bits_per_component = decoded + .image_data + .as_ref() + .map(|i| i.bits_per_component) + .or(dict_bpc) + .unwrap_or(fallback_bpc); + + let decode_arr = dict + .get::>(D) + .or_else(|| dict.get::>(DECODE)) + .map(|a| a.iter::<(f32, f32)>().collect::>()) + .unwrap_or(color_space.default_decode_arr(bits_per_component as f32)); + + Some(DecodeContext { + decoded, + width, + height, + scale_factors: (scale_x, scale_y), + color_space, + bits_per_component, + decode_arr, + }) +} + +fn decode_mask( + obj: &ImageXObject<'_>, + target_dimension: Option<(u32, u32)>, +) -> Option { + let ctx = decode_context(obj, target_dimension)?; + let mut height = ctx.height; + + let data = decode_mask_bytes( + ctx.decoded.data, + ctx.width, + &mut height, + &ctx.color_space, + ctx.bits_per_component, + &ctx.decode_arr, + // Note: The semantics between "normal" soft masks (i.e. masks defined in + // the graphics state or via `Mask`/`SMask` are inverted compared to + // stencil masks (defined via `ImageMask`). The former match the semantics + // of normal alpha images, where 0 stands for invisible and MAX stands for + // fully opaque. For stencil masks, it's the other way around: 1 means the + // paint is visible, while 0 means it's invisible. + obj.is_stencil_mask, + )?; + + Some(DecodedMask { + luma: LumaData { + data, + width: ctx.width, + height, + interpolate: obj.interpolate, + scale_factors: ctx.scale_factors, + }, + }) +} + +fn decode_raster( + obj: &ImageXObject<'_>, + target_dimension: Option<(u32, u32)>, +) -> Option { + let mut ctx = decode_context(obj, target_dimension)?; + let mut height = ctx.height; + + let is_default_decode = ctx.decode_arr + == ctx + .color_space + .default_decode_arr(ctx.bits_per_component as f32); + let is_inverted_default_decode = ctx.decode_arr + == ctx + .color_space + .inverted_default_decode_arr(ctx.bits_per_component as f32); + + let image_data = if ctx.bits_per_component == 8 + && ctx.color_space.supports_u8() + && obj.transfer_function.is_none() + && (is_default_decode || is_inverted_default_decode) + { + // This is actually the most common case, where the PDF is embedded + // in such a way where we don't need to decode. In this case, + // we can prevent the round-trip from f32 back to u8 and just return + // the raw decoded data, which will already be in + // RGB8/gray-scale with values between 0 and 255. + fix_image_length( + ctx.decoded.data.to_mut(), + ctx.width, + &mut height, + 0, + &ctx.color_space, + )?; + + if is_inverted_default_decode { + for b in ctx.decoded.data.to_mut() { + *b = 255 - *b; + } + } + + if ctx.color_space.is_device_gray() { + Some(ImageData::Luma(LumaData { + data: core::mem::take(&mut ctx.decoded.data).into_owned(), + width: ctx.width, + height, + interpolate: obj.interpolate, + scale_factors: ctx.scale_factors, + })) + } else { + let mut output_buf = vec![0; ctx.width as usize * height as usize * 3]; + ctx.color_space + .convert_u8(&ctx.decoded.data, &mut output_buf)?; + + Some(ImageData::Rgb(RgbData { + data: output_buf, + width: ctx.width, + height, + interpolate: obj.interpolate, + scale_factors: ctx.scale_factors, + })) + } + } else { + let components = get_components( + &ctx.decoded.data, + ctx.width, + height, + &ctx.color_space, + ctx.bits_per_component, + )?; + + let mut f32_data = apply_decode_array( + &components, + &ctx.color_space, + ctx.bits_per_component, + &ctx.decode_arr, + )?; + + fix_image_length(&mut f32_data, ctx.width, &mut height, 0.0, &ctx.color_space)?; + + let mut rgb_data = get_rgb_data( + &f32_data, + ctx.width, + height, + ctx.scale_factors, + &ctx.color_space, + obj.interpolate, + ); + + if let Some(transfer_function) = &obj.transfer_function + && let Some(rgb_data) = &mut rgb_data + { + let apply_single = |data: u8, function: &Function| { + function + .eval(smallvec![data as f32 / 255.0]) + .and_then(|v| v.first().copied()) + .map(|v| (v * 255.0 + 0.5) as u8) + .unwrap_or(data) + }; + + match transfer_function { + ActiveTransferFunction::Single(s) => { + for data in &mut rgb_data.data { + *data = apply_single(*data, s); + } + } + ActiveTransferFunction::Four(f) => { + for data in rgb_data.data.chunks_exact_mut(3) { + data[0] = apply_single(data[0], &f[0]); + data[1] = apply_single(data[1], &f[1]); + data[2] = apply_single(data[2], &f[2]); + } + } + } + } + + rgb_data.map(ImageData::Rgb) + }; + + let mut image = image_data?; + + let alpha = if let Some((alpha, matte_rgb)) = + resolve_matte(obj, &ctx.color_space, target_dimension) + && alpha.width == ctx.width + && alpha.height == height + { + unpremultiply(&mut image, &alpha.data, &matte_rgb); + + Some(alpha) + } else { + // Use flatten here, so in case the alpha channel is invalid we can still + // return the main image (see PDFJS-19611). + resolve_alpha( + obj, + &mut ctx.decoded, + Some(&image), + &ctx.color_space, + ctx.bits_per_component, + ctx.width, + &mut height, + ctx.scale_factors, + target_dimension, + ) + .flatten() + }; + + Some(DecodedRaster { image, alpha }) +} + +fn decode_mask_bytes( + mut decoded_data: Cow<'_, [u8]>, + width: u32, + height: &mut u32, + color_space: &ColorSpace, + bits_per_component: u8, + decode_arr: &[(f32, f32)], + invert: bool, +) -> Option> { + let default_decode = color_space.default_decode_arr(bits_per_component as f32); + let inverted_default = color_space.inverted_default_decode_arr(bits_per_component as f32); + let fast_path = bits_per_component == 8 + && (decode_arr == default_decode.as_slice() || decode_arr == inverted_default.as_slice()); + + let mut data = if fast_path { + let should_invert = invert ^ (decode_arr == inverted_default.as_slice()); + if should_invert { + for b in decoded_data.to_mut() { + *b = 255 - *b; + } + } + + decoded_data.into_owned() + } else { + let components = get_components( + &decoded_data, + width, + *height, + color_space, + bits_per_component, + )?; + + let f32_data = + apply_decode_array(&components, color_space, bits_per_component, decode_arr)?; + + if invert { + f32_data + .iter() + .map(|alpha| ((1.0 - *alpha) * 255.0 + 0.5) as u8) + .collect() + } else { + f32_data + .iter() + .map(|alpha| (*alpha * 255.0 + 0.5) as u8) + .collect() + } + }; + + fix_image_length(&mut data, width, height, 0, color_space)?; + + Some(data) +} + +fn resolve_alpha( + obj: &ImageXObject<'_>, + decoded: &mut FilterResult<'_>, + image_data: Option<&ImageData>, + color_space: &ColorSpace, + bits_per_component: u8, + width: u32, + height: &mut u32, + scale_factors: (f32, f32), + target_dimension: Option<(u32, u32)>, +) -> Option> { + let dict = obj.stream.dict(); + + let alpha = if let Some(1) = dict.get::(SMASK_IN_DATA) { + let smask_data = decoded.image_data.as_mut().and_then(|i| i.alpha.take()); + + if let Some(mut data) = smask_data { + fix_image_length(&mut data, width, height, 0, &ColorSpace::device_gray())?; + + Some(LumaData { + data, + width, + height: *height, + interpolate: obj.interpolate, + scale_factors, + }) + } else { + None + } + // Note: `SMASK` field takes precedence over `MASK`, so order matters here. + } else if let Some(s_mask) = dict + .get::>(SMASK) + .or_else(|| dict.get::>(MASK)) + { + let obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None)?; + + decode_mask(&obj, target_dimension).map(|decoded| decoded.luma) + } else if let Some(color_key_mask) = dict.get::>(MASK) { + let mut mask_data = vec![]; + + // TODO: Make this less ugly. + let raw_data = match image_data { + Some(ImageData::Luma(d)) => &d.data, + _ => decoded.data.as_ref(), + }; + + let components = get_components(raw_data, width, *height, color_space, bits_per_component)?; + + for pixel in components.chunks_exact(color_space.num_components() as usize) { + let mut mask_val = 0; + + for (component, min_max) in pixel.iter().zip(color_key_mask.chunks_exact(2)) { + if *component > min_max[1] || *component < min_max[0] { + mask_val = 255; + } + } + + mask_data.push(mask_val); + } + + fix_image_length(&mut mask_data, width, height, 0, &ColorSpace::device_gray())?; + + Some(LumaData { + data: mask_data, + width, + height: *height, + interpolate: obj.interpolate, + scale_factors, + }) + } else { + None + }; + + Some(alpha) +} + +fn resolve_matte( + obj: &ImageXObject<'_>, + color_space: &ColorSpace, + target_dimension: Option<(u32, u32)>, +) -> Option<(LumaData, [u8; 3])> { + let dict = obj.stream.dict(); + let s_mask = dict.get::>(SMASK)?; + let matte = s_mask.dict().get::(MATTE)?; + + if matte.len() != color_space.num_components() as usize { + return None; + } + + // In theory, matte needs to be applied in the image's original color space, + // but we always do it in RGB for now. + let mut matte_rgb = [0_u8; 3]; + color_space.convert_f32(&matte, &mut matte_rgb, false); + + let mask_obj = ImageXObject::new(&s_mask, |_| None, &obj.warning_sink, &obj.cache, obj.decoded_cache.as_ref(), true, None)?; + let alpha = decode_mask(&mask_obj, target_dimension)?.luma; + + Some((alpha, matte_rgb)) +} + +fn unpremultiply(image: &mut ImageData, alpha: &[u8], matte_rgb: &[u8]) { + match image { + ImageData::Rgb(rgb) => { + for (pixel, &a) in rgb.data.chunks_exact_mut(3).zip(alpha.iter()) { + if a == 0 { + continue; + } + let inv_alpha = 255.0 / a as f32; + for (c, &m) in pixel.iter_mut().zip(matte_rgb.iter()) { + let m = m as f32; + *c = (m + (*c as f32 - m) * inv_alpha) as u8; + } + } + } + ImageData::Luma(luma) => { + let m = matte_rgb[0] as f32; + for (c, &a) in luma.data.iter_mut().zip(alpha.iter()) { + if a == 0 { + continue; + } + let inv_alpha = 255.0 / a as f32; + *c = (m + (*c as f32 - m) * inv_alpha) as u8; + } + } + } +} + +fn get_rgb_data( + decoded: &[f32], + width: u32, + height: u32, + scale_factors: (f32, f32), + cs: &ColorSpace, + interpolate: bool, +) -> Option { + // To prevent a panic when calling the `chunks` method. + if cs.num_components() == 0 { + return None; + } + + let mut output = vec![0; width as usize * height as usize * 3]; + cs.convert_f32(decoded, &mut output, false); + + Some(RgbData { + data: output, + width, + height, + interpolate, + scale_factors, + }) +} + +impl CacheKey for ImageXObject<'_> { + fn cache_key(&self) -> u128 { + self.stream.cache_key() + } +} + +#[must_use] +fn fix_image_length( + image: &mut Vec, + width: u32, + height: &mut u32, + filler: T, + cs: &ColorSpace, +) -> Option<()> { + let row_len = width as usize * cs.num_components() as usize; + + if (row_len * *height as usize) <= image.len() { + // Too much data (or just the right amount), truncate it. + image.truncate(row_len * *height as usize); + } else { + // Too little data, adapt the height and pad. + *height = image.len().div_ceil(row_len) as u32; + + if !image.len().is_multiple_of(row_len) { + image.extend(iter::repeat_n(filler, row_len - (image.len() % row_len))); + } + } + + if width == 0 || *height == 0 { + None + } else { + Some(()) + } +} + +fn get_components( + data: &[u8], + width: u32, + height: u32, + color_space: &ColorSpace, + bits_per_component: u8, +) -> Option> { + let result = match bits_per_component { + 1..8 | 9..16 => { + let mut buf = vec![]; + let bpc = bits_per_component; + let mut reader = BitReader::new(data); + + for _ in 0..height { + for _ in 0..width { + for _ in 0..color_space.num_components() { + // See `stream_ccit_not_enough_data`, some images seemingly don't have + // enough data, so we just pad with zeroes in this case. + let next = reader.read(bpc).unwrap_or(0) as u16; + + buf.push(next); + } + } + + reader.align(); + } + + buf + } + 8 => data.iter().map(|v| *v as u16).collect(), + 16 => data + .chunks(2) + .map(|v| u16::from_be_bytes([v[0], v[1]])) + .collect(), + _ => { + warn!("unsupported bits per component: {bits_per_component}"); + return None; + } + }; + + Some(result) +} + +fn apply_decode_array( + components: &[u16], + color_space: &ColorSpace, + bits_per_component: u8, + decode: &[(f32, f32)], +) -> Option> { + let interpolate = |n: f32, d_min: f32, d_max: f32| { + interpolate( + n, + 0.0, + 2.0_f32.powi(bits_per_component as i32) - 1.0, + d_min, + d_max, + ) + }; + + let mut decoded_arr = vec![]; + + for pixel in components.chunks(color_space.num_components() as usize) { + for (component, (d_min, d_max)) in pixel.iter().zip(decode) { + decoded_arr.push(interpolate(*component as f32, *d_min, *d_max)); + } + } + + Some(decoded_arr) +} + +/// PdfCraft patch: whether an image or mask of `width` ร— `height` may be decoded (at most +/// 2^28 pixels, the renderer's `MAX_IMAGE_PIXELS`). +pub fn image_size_ok(width: u32, height: u32) -> bool { + width > 0 && height > 0 && u64::from(width) * u64::from(height) <= 1 << 28 +} diff --git a/vendor/hayro-jbig2/.gitignore b/vendor/hayro-jbig2/.gitignore new file mode 100644 index 00000000..c9c20c27 --- /dev/null +++ b/vendor/hayro-jbig2/.gitignore @@ -0,0 +1,2 @@ +test-inputs/ +snapshots/ \ No newline at end of file diff --git a/vendor/hayro-jbig2/Cargo.lock b/vendor/hayro-jbig2/Cargo.lock new file mode 100644 index 00000000..058a7289 --- /dev/null +++ b/vendor/hayro-jbig2/Cargo.lock @@ -0,0 +1,824 @@ +# 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 = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[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 = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[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", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hayro-ccitt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" + +[[package]] +name = "hayro-jbig2" +version = "0.3.0" +dependencies = [ + "criterion", + "fearless_simd", + "hayro-ccitt", + "image", + "indicatif", + "rayon", + "serde", + "serde_json", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[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 = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "rayon", + "unicode-width", + "web-time", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[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 = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +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 = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[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 = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[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 = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[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 = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/vendor/hayro-jbig2/Cargo.toml b/vendor/hayro-jbig2/Cargo.toml new file mode 100644 index 00000000..1efdc40b --- /dev/null +++ b/vendor/hayro-jbig2/Cargo.toml @@ -0,0 +1,129 @@ +# 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" +name = "hayro-jbig2" +version = "0.3.0" +authors = ["Laurenz Stampfl "] +build = false +exclude = [ + "/tests/", + "/manifest_*.json", + "/sync.py", + "/generate_huffman_tables.py", + "/benches/", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A memory-safe, pure-Rust JBIG2 decoder." +readme = "README.md" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/LaurenzV/hayro" +resolver = "2" + +[features] +default = [ + "std", + "image", + "simd", +] +image = [ + "dep:image", + "std", +] +simd = [ + "fearless_simd", + "std", +] +std = [] + +[lib] +name = "hayro_jbig2" +path = "src/lib.rs" + +[[example]] +name = "jbig2png" +path = "examples/jbig2png.rs" + +[dependencies.fearless_simd] +version = "0.4.0" +optional = true + +[dependencies.hayro-ccitt] +version = "0.3.0" +default-features = false + +[dependencies.image] +version = "0.25" +optional = true +default-features = false + +[dev-dependencies.criterion] +version = "0.5" +features = ["html_reports"] + +[dev-dependencies.image] +version = "0.25" +features = ["png"] +default-features = false + +[dev-dependencies.indicatif] +version = "0.17" +features = ["rayon"] + +[dev-dependencies.rayon] +version = "1.11" + +[dev-dependencies.serde] +version = "1" +features = ["derive"] + +[dev-dependencies.serde_json] +version = "1" + +[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-jbig2/Cargo.toml.orig b/vendor/hayro-jbig2/Cargo.toml.orig new file mode 100644 index 00000000..a264dbd0 --- /dev/null +++ b/vendor/hayro-jbig2/Cargo.toml.orig @@ -0,0 +1,41 @@ +[package] +name = "hayro-jbig2" +version = "0.3.0" +description = "A memory-safe, pure-Rust JBIG2 decoder." +authors = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +readme = "README.md" +exclude = ["/tests/", "/manifest_*.json", "/sync.py", "/generate_huffman_tables.py", "/benches/"] + +[features] +default = ["std", "image", "simd"] +std = [] +simd = ["fearless_simd", "std"] +image = ["dep:image", "std"] + +[dependencies] +fearless_simd = { workspace = true, optional = true } +hayro-ccitt = { workspace = true } +image = { workspace = true, default-features = false, optional = true } + +[lints] +workspace = true + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } +image = { workspace = true, default-features = false, features = ["png"] } +indicatif = { version = "0.17", features = ["rayon"] } +rayon = "1.11" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[[test]] +name = "asset_suite" +path = "tests/mod.rs" +harness = false + +[[bench]] +name = "decode" +harness = false diff --git a/vendor/hayro-jbig2/LICENSE-APACHE b/vendor/hayro-jbig2/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/vendor/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/vendor/hayro-jbig2/LICENSE-MIT b/vendor/hayro-jbig2/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/vendor/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/vendor/hayro-jbig2/README.md b/vendor/hayro-jbig2/README.md new file mode 100644 index 00000000..09634014 --- /dev/null +++ b/vendor/hayro-jbig2/README.md @@ -0,0 +1,30 @@ +# hayro-jbig2 + +[![Crates.io](https://img.shields.io/crates/v/hayro-jbig2.svg)](https://crates.io/crates/hayro-jbig2) +[![Documentation](https://docs.rs/hayro-jbig2/badge.svg)](https://docs.rs/hayro-jbig2) + +A memory-safe, pure-Rust JBIG2 decoder. + +`hayro-jbig2` decodes JBIG2 images as specified in ITU-T T.88 (also known as +ISO/IEC 14492). JBIG2 is a bi-level image compression standard commonly used +in PDF documents for compressing scanned text documents. + +## Status + +This crate is currently under development. + +## Safety +By default, the crate has the `simd` feature enabled, which uses the +[`fearless_simd`](https://github.com/linebender/fearless_simd) crate to accelerate decoding of JBIG2 images with halftoning. +If you want to eliminate any usage of unsafe in this crate as well as its dependencies, you can simply disable this +feature and still get very good performance in the vast majority of cases. Unsafe code is forbidden +via a crate-level attribute. + +## 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. diff --git a/vendor/hayro-jbig2/SOURCE.json b/vendor/hayro-jbig2/SOURCE.json new file mode 100644 index 00000000..50ec8937 --- /dev/null +++ b/vendor/hayro-jbig2/SOURCE.json @@ -0,0 +1,194 @@ +{ + "crate": "hayro-jbig2", + "source_repository": "https://github.com/storytold/pdfcraft", + "source_revision": "7437bde378f43856eada40af25b983812e615e8a", + "source_directory": "vendor/hayro-jbig2", + "files": [ + { + "path": ".gitignore", + "source_sha256": "e26ccb13f796bd902ec2b34df79e1442e5d50abcb53ca9b7ea66eb2507ef2157", + "source_git_blob": "c9c20c270e3478f364e71bcbff5183cd0649b551", + "sha256": "e26ccb13f796bd902ec2b34df79e1442e5d50abcb53ca9b7ea66eb2507ef2157" + }, + { + "path": "Cargo.lock", + "source_sha256": "45ae502a202a6267a2ebd39eb4d8c12870b2d11701871ac2350e81e886ce2873", + "source_git_blob": "058a72899ddc4daa55ecd87debda16be40bf8d01", + "sha256": "45ae502a202a6267a2ebd39eb4d8c12870b2d11701871ac2350e81e886ce2873" + }, + { + "path": "Cargo.toml", + "source_sha256": "1b77fcad9f4b16874635660a2bba07996f1dec8cc1c3e2a35926b2b0a463bc2e", + "source_git_blob": "1efdc40b014f340fc12872724b2a1c3d4f26a0c8", + "sha256": "1b77fcad9f4b16874635660a2bba07996f1dec8cc1c3e2a35926b2b0a463bc2e" + }, + { + "path": "Cargo.toml.orig", + "source_sha256": "e38a00d34fec56c1b7737f0fcaf974e0a98c36d64629d27370f3c89c0f285430", + "source_git_blob": "a264dbd013c1a3ca226c7a8ea9288f35218942a0", + "sha256": "e38a00d34fec56c1b7737f0fcaf974e0a98c36d64629d27370f3c89c0f285430" + }, + { + "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": "9402ef9028cfb46c86df4fe4ae6d63d6a3e8f212cbf02ed831f7850b19900fa6", + "source_git_blob": "0963401425a546adc0858e35cd37c7c17fbad19a", + "sha256": "9402ef9028cfb46c86df4fe4ae6d63d6a3e8f212cbf02ed831f7850b19900fa6" + }, + { + "path": "examples/jbig2png.rs", + "source_sha256": "e7bc8f6b820af6874beeba23d042ecf8028d39de2f2b9779bd5a510d5cbe282f", + "source_git_blob": "5ea6526266387b972ce6cc68a1c11af4a9d94f78", + "sha256": "e7bc8f6b820af6874beeba23d042ecf8028d39de2f2b9779bd5a510d5cbe282f" + }, + { + "path": "src/arithmetic_decoder.rs", + "source_sha256": "8d2c365a9499900e4538f2f79d74e9a56f2c24d92229abb1317ed8988d1b4624", + "source_git_blob": "8e6adfb5a19cc813be257723eeb83441425141ad", + "sha256": "8d2c365a9499900e4538f2f79d74e9a56f2c24d92229abb1317ed8988d1b4624" + }, + { + "path": "src/bitmap.rs", + "source_sha256": "59ed83d8bac6c39e40270a9856ca3faee628de7d9efae1b01f714e140e2b6b7e", + "source_git_blob": "35ed97561597d30a70fe819cf8cfbd3780538f75", + "sha256": "59ed83d8bac6c39e40270a9856ca3faee628de7d9efae1b01f714e140e2b6b7e" + }, + { + "path": "src/decode/generic.rs", + "source_sha256": "6646417cbea6b4563f47f6fa01d521f4654d609aa86713d34b4474963d222f81", + "source_git_blob": "a788163f237a37016948e390083aefae702c4898", + "sha256": "6646417cbea6b4563f47f6fa01d521f4654d609aa86713d34b4474963d222f81" + }, + { + "path": "src/decode/generic_refinement.rs", + "source_sha256": "625075671e510e8ff1d7c3cade857792413c091d57a35561ad0591d7f04c2ea5", + "source_git_blob": "a566b8de58c078dec874d5d1dea36c0f557f1a16", + "sha256": "625075671e510e8ff1d7c3cade857792413c091d57a35561ad0591d7f04c2ea5" + }, + { + "path": "src/decode/halftone.rs", + "source_sha256": "0e4a5ef92e194b4eba2367196e904786dd7d346cdf0ab291c72a77b5aee5873a", + "source_git_blob": "eb5c1c21e8a7d1a99c497e83ea59547b7dd19a96", + "sha256": "0e4a5ef92e194b4eba2367196e904786dd7d346cdf0ab291c72a77b5aee5873a" + }, + { + "path": "src/decode/mod.rs", + "source_sha256": "09f1e329bedf8d0defa22450a7d8c9f4e2cef7fede533763a2354ad79fa18fe1", + "source_git_blob": "da9e0eccf5ddde79bd4884dd0e4fc235e66757c3", + "sha256": "09f1e329bedf8d0defa22450a7d8c9f4e2cef7fede533763a2354ad79fa18fe1" + }, + { + "path": "src/decode/pattern.rs", + "source_sha256": "abdd326c08dee0488678d74915e6bb4138c9b0c07d44f24c3a5794ff640799ba", + "source_git_blob": "2c15d70b8f9925638e41cec0eb1d73b91c1533f2", + "sha256": "abdd326c08dee0488678d74915e6bb4138c9b0c07d44f24c3a5794ff640799ba" + }, + { + "path": "src/decode/symbol.rs", + "source_sha256": "aa57e88cdc6fd1647d7996875c253a643fedfc4438d5137e326e00d17d0d29ee", + "source_git_blob": "f1aa8a6639e53dd350057d8d078198d363daa918", + "sha256": "aa57e88cdc6fd1647d7996875c253a643fedfc4438d5137e326e00d17d0d29ee" + }, + { + "path": "src/decode/text.rs", + "source_sha256": "01863cdcd48ab68b5230948d12212a815d293c7c9d5382a140772088bfb1c405", + "source_git_blob": "bbc2ac55c7d7d5b8aebffbecfe93fda8b88526e8", + "sha256": "01863cdcd48ab68b5230948d12212a815d293c7c9d5382a140772088bfb1c405" + }, + { + "path": "src/error.rs", + "source_sha256": "39b6ec8fa058bc5c1722892290882e123d193d3844ed5846b30b742b94ba72b0", + "source_git_blob": "c579368b44b444566178e68a2705cf9561fad71f", + "sha256": "39b6ec8fa058bc5c1722892290882e123d193d3844ed5846b30b742b94ba72b0" + }, + { + "path": "src/file.rs", + "source_sha256": "10467a415ab2e8b92a8951a86f4c02c5c74f5a005629a11865a20bc23b26ca55", + "source_git_blob": "1440a22efa89f0deec0029968c3a20bad8526d63", + "sha256": "10467a415ab2e8b92a8951a86f4c02c5c74f5a005629a11865a20bc23b26ca55" + }, + { + "path": "src/gray_scale.rs", + "source_sha256": "46f12893f4573610789015ba7036f6afb1d30b1454c0c674fc7ccdd9337d7487", + "source_git_blob": "2836ec77a7c8d3970b13dd46f576778c64bfe2d9", + "sha256": "46f12893f4573610789015ba7036f6afb1d30b1454c0c674fc7ccdd9337d7487" + }, + { + "path": "src/huffman_table.rs", + "source_sha256": "8c0dc0681351b9573fedd00afed26173be8bd4583210063bff75c4e95e9d8054", + "source_git_blob": "2183c8eea065ff4f6bd708fa92fa4ba66844e739", + "sha256": "8c0dc0681351b9573fedd00afed26173be8bd4583210063bff75c4e95e9d8054" + }, + { + "path": "src/huffman_tables_generated.rs", + "source_sha256": "030a09fc699f15034b5cc6a10cb138284d2c7423fb86c0ef5a69b1c02d690ec9", + "source_git_blob": "d3bfe2b62ac7d79992c4e1b0a1cb84402538b3b4", + "sha256": "030a09fc699f15034b5cc6a10cb138284d2c7423fb86c0ef5a69b1c02d690ec9" + }, + { + "path": "src/integer_decoder.rs", + "source_sha256": "6a5a89d8df1ab6c011f7a92223fbd384bbf274ccb1f15705c994d7bd55d1a680", + "source_git_blob": "8b7656d2964058618687ce2409142dddcdeb068a", + "sha256": "6a5a89d8df1ab6c011f7a92223fbd384bbf274ccb1f15705c994d7bd55d1a680" + }, + { + "path": "src/integration.rs", + "source_sha256": "196ae7e63e863f92a431679a032835c45992fca686c494bab2b98566ae044187", + "source_git_blob": "70df0f42d67acdafc5e67810886353eb4b25373c", + "sha256": "196ae7e63e863f92a431679a032835c45992fca686c494bab2b98566ae044187" + }, + { + "path": "src/lazy.rs", + "source_sha256": "6996be0661d8bb4fc6ec2c9623fee8a7a1221ff612d0e9f37b01ac711f0b921e", + "source_git_blob": "a85501ef68ae044c17e8f124616d6ae42ac731c7", + "sha256": "6996be0661d8bb4fc6ec2c9623fee8a7a1221ff612d0e9f37b01ac711f0b921e" + }, + { + "path": "src/lib.rs", + "source_sha256": "22463230cc82543ee93791b642566a308f0f4eb14d3089b24dc2e7f06d0f784c", + "source_git_blob": "ad77a6dd17920b2db66f6bb5a0f2cb8efaafd41a", + "sha256": "22463230cc82543ee93791b642566a308f0f4eb14d3089b24dc2e7f06d0f784c" + }, + { + "path": "src/page_info.rs", + "source_sha256": "2406719e3bcccaea574b26cb87c9ee235ade96c7aaaa67b1301e2327a308284e", + "source_git_blob": "cc0c89c0619890734adad4c14fecabcd4a73f585", + "sha256": "2406719e3bcccaea574b26cb87c9ee235ade96c7aaaa67b1301e2327a308284e" + }, + { + "path": "src/reader.rs", + "source_sha256": "990f01b0dbec60df52bc779da14c5af765172eb243b11d2c74fb0d5eb4d07175", + "source_git_blob": "418915ca2338a3ce26525a985aff182e85a7f08b", + "sha256": "990f01b0dbec60df52bc779da14c5af765172eb243b11d2c74fb0d5eb4d07175" + }, + { + "path": "src/segment.rs", + "source_sha256": "58d9fd05a15358b4ca945ffa3b8c1872f4ea57f8bbe50e8db3c6fce5bcac2914", + "source_git_blob": "35c17c38af62b1fece4e280cda923cdcb23a8661", + "sha256": "58d9fd05a15358b4ca945ffa3b8c1872f4ea57f8bbe50e8db3c6fce5bcac2914" + }, + { + "path": "src/simd.rs", + "source_sha256": "0bf2996ae8e49e0fe9a396997312017d6b6bd3d1385c30a65755da9b51ebea3d", + "source_git_blob": "2bd1adb7e2b77d3ead05940dcc7514fe12b6913a", + "sha256": "0bf2996ae8e49e0fe9a396997312017d6b6bd3d1385c30a65755da9b51ebea3d" + }, + { + "path": "src/symbol_id_decoder.rs", + "source_sha256": "8db13cb84a359ecea5bc56a1d15b8ef28e35329733e4a691f44877172f5b671f", + "source_git_blob": "33ca1c6df4a04595dac0d1d6c43b835a0544b897", + "sha256": "8db13cb84a359ecea5bc56a1d15b8ef28e35329733e4a691f44877172f5b671f" + } + ] +} diff --git a/vendor/hayro-jbig2/examples/jbig2png.rs b/vendor/hayro-jbig2/examples/jbig2png.rs new file mode 100644 index 00000000..5ea65262 --- /dev/null +++ b/vendor/hayro-jbig2/examples/jbig2png.rs @@ -0,0 +1,64 @@ +//! This example shows you how to convert a JBIG2 image into a PNG file. + +#![allow(missing_docs)] + +use std::process::ExitCode; + +use image::{GrayImage, ImageDecoder}; + +fn main() -> ExitCode { + let args: Vec = std::env::args().collect(); + + if args.len() != 3 { + eprintln!("Usage: {} ", args[0]); + + return ExitCode::FAILURE; + } + + let input_path = &args[1]; + let output_path = &args[2]; + + let data = match std::fs::read(input_path) { + Ok(data) => data, + Err(err) => { + eprintln!("Failed to read input file: {err}"); + + return ExitCode::FAILURE; + } + }; + + let image = match hayro_jbig2::Image::new(&data) { + Ok(image) => image, + Err(err) => { + eprintln!("Failed to parse JBIG2: {err}"); + + return ExitCode::FAILURE; + } + }; + + let (width, height) = image.dimensions(); + println!("Decoded: {width}x{height} image"); + + let mut buf = vec![0_u8; image.total_bytes() as usize]; + if let Err(err) = image.read_image(&mut buf) { + eprintln!("Failed to decode JBIG2: {err}"); + + return ExitCode::FAILURE; + } + + let Some(gray) = GrayImage::from_raw(width, height, buf) else { + eprintln!("Internal error: Buffer size mismatch"); + + return ExitCode::FAILURE; + }; + + if let Err(err) = gray.save(output_path) { + eprintln!("Failed to save PNG: {err}"); + + return ExitCode::FAILURE; + } + + eprintln!("Saved: {output_path}"); + + ExitCode::SUCCESS +} diff --git a/vendor/hayro-jbig2/src/arithmetic_decoder.rs b/vendor/hayro-jbig2/src/arithmetic_decoder.rs new file mode 100644 index 00000000..8e6adfb5 --- /dev/null +++ b/vendor/hayro-jbig2/src/arithmetic_decoder.rs @@ -0,0 +1,398 @@ +// Note: This is a copy of the arithmetic decoder from `hayro-jpeg2000`, +// keep the two in sync. + +//! The arithmetic decoder, described in Annex C. +//! +//! The arithmetic decoder keeps track of some state and continuously receives +//! context labels as input, each time yielding a new bit from the original data +//! as output. + +pub(crate) struct ArithmeticDecoder<'a> { + /// The underlying encoded data. + data: &'a [u8], + /// The C-register (see Table C.1). + c: u32, + /// The A-register (see Table C.1). + a: u32, + /// The pointer to the current byte. + base_pointer: u32, + /// The bit shift counter. + shift_count: u32, +} + +impl<'a> ArithmeticDecoder<'a> { + pub(crate) fn new(data: &'a [u8]) -> Self { + let mut decoder = ArithmeticDecoder { + data, + c: 0, + a: 0, + base_pointer: 0, + shift_count: 0, + }; + + decoder.initialize(); + + decoder + } + + /// Read the next bit using the given context label. + #[inline(always)] + pub(crate) fn read_bit(&mut self, context: &mut ArithmeticDecoderContext) -> u32 { + self.decode(context) + } + + /// The INITDEC procedure from C.3.5. + /// + /// We use the version from Annex G in . + fn initialize(&mut self) { + self.c = ((self.current_byte() as u32) ^ 0xff) << 16; + self.read_byte(); + + self.c <<= 7; + self.shift_count -= 7; + self.a = 0x8000; + } + + /// The BYTEIN procedure from C.3.4. + /// + /// We use the version from Annex G from . + #[inline(always)] + fn read_byte(&mut self) { + if self.current_byte() == 0xff { + let b1 = self.next_byte(); + + if b1 > 0x8f { + self.shift_count = 8; + } else { + self.base_pointer += 1; + self.c = self + .c + .wrapping_add(0xfe00) + .wrapping_sub((self.current_byte() as u32) << 9); + self.shift_count = 7; + } + } else { + self.base_pointer += 1; + self.c = self + .c + .wrapping_add(0xff00) + .wrapping_sub((self.current_byte() as u32) << 8); + self.shift_count = 8; + } + } + + /// The RENORMD procedure from C.3.3. + #[inline(always)] + fn renormalize(&mut self) { + // Original code: + // loop { + // if self.shift_count == 0 { + // self.read_byte(); + // } + // + // self.a <<= 1; + // self.c <<= 1; + // self.shift_count -= 1; + // + // if self.a & 0x8000 != 0 { + // break; + // } + // } + + // Optimization: Batch shifts. + while self.a & 0x8000 == 0 { + if self.shift_count == 0 { + self.read_byte(); + } + + let shifts_needed = self.a.leading_zeros() - 16; + let batch = shifts_needed.min(self.shift_count); + self.a <<= batch; + self.c <<= batch; + self.shift_count -= batch; + } + } + + /// The `LPS_EXCHANGE` procedure from C.3.2. + #[inline(always)] + #[allow(unused, reason = "for reference")] + fn exchange_lps(&mut self, context: &mut ArithmeticDecoderContext, qe_entry: &QeData) -> u32 { + // Original code: + // let d; + // + // if self.a < qe_entry.qe { + // self.a = qe_entry.qe; + // d = context.mps; + // context.index = qe_entry.nmps; + // } else { + // self.a = qe_entry.qe; + // d = 1 - context.mps; + // + // if qe_entry.switch { + // context.mps = 1 - context.mps; + // } + // + // context.index = qe_entry.nlps; + // } + + // Branchless version, shows better performance. + + let cond = (self.a < qe_entry.qe) as u32; + let inv_cond = 1 - cond; + + self.a = qe_entry.qe; + // d = if cond { mps } else { 1 - mps } + let d = context.mps() ^ inv_cond; + // flip mps only when !cond && switch + context.xor_mps(inv_cond & (qe_entry.switch as u32)); + // index = if cond { nmps } else { nlps } + let cond_u8 = cond as u8; + let inv_cond_u8 = inv_cond as u8; + context.set_index(cond_u8 * qe_entry.nmps + inv_cond_u8 * qe_entry.nlps); + + d + } + + /// The `MPS_EXCHANGE` procedure from C.3.2. + #[inline(always)] + #[allow(unused, reason = "for reference")] + fn exchange_mps(&mut self, context: &mut ArithmeticDecoderContext, qe_entry: &QeData) -> u32 { + // Original code: + // let d; + // + // if self.a < qe_entry.qe { + // d = 1 - context.mps; + // + // if qe_entry.switch { + // context.mps = 1 - context.mps; + // } + // + // context.index = qe_entry.nlps; + // } else { + // d = context.mps; + // context.index = qe_entry.nmps; + // } + + // Branchless version, shows better performance. + let cond = (self.a < qe_entry.qe) as u32; + let inv_cond = 1 - cond; + // d = if cond { 1 - mps } else { mps } + let d = context.mps() ^ cond; + // flip mps only when cond && switch + context.xor_mps(cond & (qe_entry.switch as u32)); + // index = if cond { nlps } else { nmps } + let cond_u8 = cond as u8; + let inv_cond_u8 = inv_cond as u8; + context.set_index(cond_u8 * qe_entry.nlps + inv_cond_u8 * qe_entry.nmps); + d + } + + /// The DECODE procedure from C.3.2. + /// + /// We use the version from Annex G from . + #[inline(always)] + pub(crate) fn decode(&mut self, context: &mut ArithmeticDecoderContext) -> u32 { + let qe_entry = &QE_TABLE[context.index() as usize]; + + self.a -= qe_entry.qe; + + // Old code: + // let d; + // + // if (self.c >> 16) < self.a { + // if self.a & 0x8000 == 0 { + // d = self.exchange_mps(context, qe_entry); + // self.renormalize(); + // } else { + // d = context.mps(); + // } + // } else { + // self.c -= self.a << 16; + // + // d = self.exchange_lps(context, qe_entry); + // self.renormalize(); + // } + // + // d + + // This is a faster version that reduces branching, which has shown + // itself to be the main limiting factor for better performance. + // We short-circuit the case where just the most probably symbol is + // returned, and otherwise use a code path that works for both, + // MPS_EXCHANGE and LPS_EXCHANGE. + + if (self.c >> 16) < self.a && self.a & 0x8000 != 0 { + return context.mps(); + } + + // Unified branchless MPS_EXCHANGE / LPS_EXCHANGE. + // + // Compare with exchange_mps and exchange_lps above โ€” the only + // difference between them is that LPS flips the role of cond: + // exchange_mps: d = mps ^ cond, flip when cond, index = cond*nlps + inv*nmps + // exchange_lps: d = mps ^ inv_cond, flip when inv_cond, index = cond*nmps + inv*nlps + // + // This is equivalent to XOR-ing cond with is_lps, so we can handle + // both paths with a single branchless computation. + // + // As can be seen above, renormalization is always performed. + let is_lps = ((self.c >> 16) >= self.a) as u32; + + // LPS: C -= A << 16 (no-op when MPS). + let lps_mask = is_lps.wrapping_neg(); // 0xFFFF_FFFF if LPS, 0 if MPS + self.c -= (self.a << 16) & lps_mask; + + // Same condition as in exchange_mps / exchange_lps. + let cond = (self.a < qe_entry.qe) as u32; + + // LPS: a = qe (no-op when MPS, a stays as a - qe). + self.a = (self.a & !lps_mask) | (qe_entry.qe & lps_mask); + + // exchange_mps: d = mps ^ cond โ†’ cond ^ 0 + // exchange_lps: d = mps ^ inv_cond โ†’ cond ^ 1 + // unified: d = mps ^ (cond ^ is_lps) + let d = context.mps() ^ cond ^ is_lps; + + // exchange_mps: flip mps when cond & switch โ†’ (cond ^ 0) & switch + // exchange_lps: flip mps when inv_cond & switch โ†’ (cond ^ 1) & switch + // unified: flip mps when (cond ^ is_lps) & switch + context.xor_mps((cond ^ is_lps) & (qe_entry.switch as u32)); + + // exchange_mps: index = cond * nlps + inv_cond * nmps + // exchange_lps: index = cond * nmps + inv_cond * nlps (swapped) + // unified: the result is always exactly nmps or nlps โ€” + // pick nlps when (cond ^ is_lps) == 1, nmps otherwise. + let pick_nlps = ((cond ^ is_lps) as u8).wrapping_neg(); // 0xFF or 0x00 + context.set_index(qe_entry.nmps ^ ((qe_entry.nmps ^ qe_entry.nlps) & pick_nlps)); + + self.renormalize(); + + d + } + + #[inline(always)] + fn current_byte(&self) -> u8 { + self.data + .get(self.base_pointer as usize) + .copied() + // "The number of bytes corresponding to the coding passes is + // specified in the packet header. Often at that point there are + // more symbols to be decoded. Therefore, the decoder shall extend + // the input bit stream to the arithmetic coder with 0xFF bytes, + // as necessary, until all symbols have been decoded." + .unwrap_or(0xFF) + } + + #[inline(always)] + fn next_byte(&self) -> u8 { + self.data + .get((self.base_pointer + 1) as usize) + .copied() + .unwrap_or(0xFF) + } +} + +// Previously, we stored the context as 2 u32's, but doing it with a bit-packed +// u8 seems to be slightly better (though it doesn't make that huge of a +// difference). +/// Bits 0-6 = index (0-46). +/// Bit 7 = mps (0 or 1). +#[derive(Copy, Clone, Debug, Default)] +pub(crate) struct ArithmeticDecoderContext(u8); + +impl ArithmeticDecoderContext { + #[inline(always)] + pub(crate) fn index(self) -> u32 { + (self.0 & 0x7F) as u32 + } + + #[inline(always)] + pub(crate) fn mps(self) -> u32 { + (self.0 >> 7) as u32 + } + + #[inline(always)] + fn set_index(&mut self, index: u8) { + self.0 = (self.0 & 0x80) | index; + } + + #[inline(always)] + fn xor_mps(&mut self, val: u32) { + self.0 ^= ((val & 1) << 7) as u8; + } +} + +#[derive(Debug, Clone, Copy)] +struct QeData { + qe: u32, + nmps: u8, + nlps: u8, + switch: bool, +} + +macro_rules! qe { + ($($qe:expr, $nmps:expr, $nlps:expr, $switch:expr),+ $(,)?) => { + [ + $( + QeData { + qe: $qe, + nmps: $nmps, + nlps: $nlps, + switch: $switch, + } + ),+ + ] + }; +} + +/// QE values and associated data from Table C.2. +#[rustfmt::skip] +static QE_TABLE: [QeData; 47] = qe!( + 0x5601, 1, 1, true, + 0x3401, 2, 6, false, + 0x1801, 3, 9, false, + 0x0AC1, 4, 12, false, + 0x0521, 5, 29, false, + 0x0221, 38, 33, false, + 0x5601, 7, 6, true, + 0x5401, 8, 14, false, + 0x4801, 9, 14, false, + 0x3801, 10, 14, false, + 0x3001, 11, 17, false, + 0x2401, 12, 18, false, + 0x1C01, 13, 20, false, + 0x1601, 29, 21, false, + 0x5601, 15, 14, true, + 0x5401, 16, 14, false, + 0x5101, 17, 15, false, + 0x4801, 18, 16, false, + 0x3801, 19, 17, false, + 0x3401, 20, 18, false, + 0x3001, 21, 19, false, + 0x2801, 22, 19, false, + 0x2401, 23, 20, false, + 0x2201, 24, 21, false, + 0x1C01, 25, 22, false, + 0x1801, 26, 23, false, + 0x1601, 27, 24, false, + 0x1401, 28, 25, false, + 0x1201, 29, 26, false, + 0x1101, 30, 27, false, + 0x0AC1, 31, 28, false, + 0x09C1, 32, 29, false, + 0x08A1, 33, 30, false, + 0x0521, 34, 31, false, + 0x0441, 35, 32, false, + 0x02A1, 36, 33, false, + 0x0221, 37, 34, false, + 0x0141, 38, 35, false, + 0x0111, 39, 36, false, + 0x0085, 40, 37, false, + 0x0049, 41, 38, false, + 0x0025, 42, 39, false, + 0x0015, 43, 40, false, + 0x0009, 44, 41, false, + 0x0005, 45, 42, false, + 0x0001, 45, 43, false, + 0x5601, 46, 46, false, +); diff --git a/vendor/hayro-jbig2/src/bitmap.rs b/vendor/hayro-jbig2/src/bitmap.rs new file mode 100644 index 00000000..35ed9756 --- /dev/null +++ b/vendor/hayro-jbig2/src/bitmap.rs @@ -0,0 +1,227 @@ +//! Bitmap representation for JBIG2 decoding. +//! +//! "The variable whose value is the result of this decoding procedure is shown +//! in Table 3." (6.2.3) +//! +//! "GBREG - The decoded region bitmap." (Table 3) + +use crate::decode::CombinationOperator; +use crate::error::{OverflowError, Result, bail}; +use alloc::vec; +use alloc::vec::Vec; + +// To guard better against malicious files, we limit bitmap dimensions +// to `u16::MAX`, which should be more than enough. +pub(crate) const MAX_DIMENSION: u32 = u16::MAX as u32; + +/// PdfCraft patch: the most pixels one bitmap may have (2^28, the renderer's +/// `MAX_IMAGE_PIXELS`). Each side alone may be 65535, so a single region could be 4.3 billion +/// pixels, decoded one by one. +pub(crate) const MAX_PIXELS: u64 = 1 << 28; + +/// The underlying word type for packed pixel storage. +/// Change this to `u32` to switch back to 32-bit words. +pub(crate) type Word = u32; + +pub(crate) const WORD_BITS: u32 = Word::BITS; +pub(crate) const WORD_SHIFT: u32 = WORD_BITS - 1; + +/// A decoded bitmap with position information. +/// +/// "Pixels decoded by the MMR decoder having the value 'black' shall be treated +/// as having the value 1. Pixels decoded by the MMR decoder having the value +/// 'white' shall be treated as having the value 0." (6.2.6) +#[derive(Debug, Clone, Default)] +pub(crate) struct Bitmap { + /// Width in pixels. + pub(crate) width: u32, + /// Height in pixels. + pub(crate) height: u32, + /// Number of words per row. + pub(crate) stride: u32, + /// Packed pixel data, one bit per pixel, row-major order. + /// Each row is padded to a word boundary. + pub(crate) data: Vec, + /// "This four-byte field gives the horizontal offset in pixels of the bitmap + /// encoded in this segment relative to the page bitmap." (7.4.1.3) + pub(crate) x_location: u32, + /// "This four-byte field gives the vertical offset in pixels of the bitmap + /// encoded in this segment relative to the page bitmap." (7.4.1.4) + pub(crate) y_location: u32, +} + +impl Bitmap { + /// Create a new bitmap filled with white pixels (false). + /// + /// The bitmap is positioned at (0, 0). + pub(crate) fn new(width: u32, height: u32) -> Result { + Self::new_with(width, height, 0, 0, false) + } + + /// Create a new bitmap with full configuration. + pub(crate) fn new_with( + width: u32, + height: u32, + x_location: u32, + y_location: u32, + default_pixel: bool, + ) -> Result { + if width > MAX_DIMENSION || height > MAX_DIMENSION { + bail!(OverflowError::BitmapDimension); + } + // PdfCraft patch: see `MAX_PIXELS`. + if u64::from(width) * u64::from(height) > MAX_PIXELS { + bail!(OverflowError::BitmapDimension); + } + + let stride = width.div_ceil(WORD_BITS); + let default_word: Word = if default_pixel { !0 } else { 0 }; + let data = vec![default_word; stride as usize * height as usize]; + Ok(Self { + width, + height, + stride, + data, + x_location, + y_location, + }) + } + + pub(crate) fn reinitialize( + &mut self, + width: u32, + height: u32, + default_pixel: bool, + ) -> Result<()> { + if width > MAX_DIMENSION || height > MAX_DIMENSION { + return Err(OverflowError::BitmapDimension.into()); + } + // PdfCraft patch: see `MAX_PIXELS` (the page bitmap is sized here). + if u64::from(width) * u64::from(height) > MAX_PIXELS { + return Err(OverflowError::BitmapDimension.into()); + } + + let stride = width.div_ceil(WORD_BITS); + let default_word: Word = if default_pixel { !0 } else { 0 }; + self.width = width; + self.height = height; + self.stride = stride; + self.x_location = 0; + self.y_location = 0; + self.data.clear(); + self.data + .resize(stride as usize * height as usize, default_word); + + Ok(()) + } + + /// Get a pixel value at (x, y). + #[inline] + pub(crate) fn get_pixel(&self, x: u32, y: u32) -> u8 { + if x >= self.width || y >= self.height { + return 0; + } + + let word_idx = (y * self.stride + x / WORD_BITS) as usize; + let bit_pos = WORD_SHIFT - (x % WORD_BITS); + ((self.data[word_idx] >> bit_pos) & 1) as u8 + } + + /// Set a pixel value at (x, y). + #[inline] + pub(crate) fn set_pixel(&mut self, x: u32, y: u32, value: u8) { + if x >= self.width || y >= self.height { + return; + } + + let word_idx = (y * self.stride + x / WORD_BITS) as usize; + let bit_pos = WORD_SHIFT - (x % WORD_BITS); + self.data[word_idx] |= (value as Word) << bit_pos; + } + + /// Combine another bitmap into this one at a specific location. + /// + /// "These operators describe how the segment's bitmap is to be combined with + /// the page bitmap." (7.4.1.5) + /// + /// Pixels outside the destination bitmap are ignored. + pub(crate) fn combine(&mut self, other: &Self, x: i32, y: i32, operator: CombinationOperator) { + // TODO: Figure out how we can optimize this, especially since this is a + // bottleneck for symbols/patterns. + let dest_x_start = x.max(0); + let dest_x_end = (x + other.width as i32).min(self.width as i32); + + if dest_x_start >= dest_x_end { + return; + } + + let src_x_start = (dest_x_start - x) as u32; + let dest_x_start = dest_x_start as u32; + let dest_x_end = dest_x_end as u32; + + for src_y in 0..other.height { + let dest_y = y + src_y as i32; + if dest_y < 0 || dest_y >= self.height as i32 { + continue; + } + + let dest_y = dest_y as u32; + + let first_word = dest_x_start / WORD_BITS; + let last_word = (dest_x_end - 1) / WORD_BITS; + + for word_idx in first_word..=last_word { + let word_start_x = word_idx * WORD_BITS; + let word_end_x = word_start_x + WORD_BITS; + + let px_start = dest_x_start.max(word_start_x); + let px_end = dest_x_end.min(word_end_x); + + let bit_start = px_start - word_start_x; + let bit_end = px_end - word_start_x; + + let mask: Word = if bit_end == WORD_BITS { + !0 >> bit_start + } else { + (!0 >> bit_start) & !(!0 >> bit_end) + }; + + let src_x_for_range = src_x_start + (px_start - dest_x_start); + let src_word_idx = src_x_for_range / WORD_BITS; + let src_bit_offset = src_x_for_range % WORD_BITS; + + let src_word1 = other.get_word(src_y, src_word_idx); + let src_word2 = other.get_word(src_y, src_word_idx + 1); + + let src_raw = if src_bit_offset == 0 { + src_word1 + } else { + (src_word1 << src_bit_offset) | (src_word2 >> (WORD_BITS - src_bit_offset)) + }; + let src_aligned = src_raw >> bit_start; + + let dest_idx = (dest_y * self.stride + word_idx) as usize; + let dest_word = self.data[dest_idx]; + + let result = match operator { + CombinationOperator::Or => dest_word | src_aligned, + CombinationOperator::And => dest_word & src_aligned, + CombinationOperator::Xor => dest_word ^ src_aligned, + CombinationOperator::Xnor => !(dest_word ^ src_aligned), + CombinationOperator::Replace => src_aligned, + }; + + self.data[dest_idx] = (dest_word & !mask) | (result & mask); + } + } + } + + #[inline] + pub(crate) fn get_word(&self, row: u32, word_idx: u32) -> Word { + if row >= self.height || word_idx >= self.stride { + return 0; + } + + self.data[(row * self.stride + word_idx) as usize] + } +} diff --git a/vendor/hayro-jbig2/src/decode/generic.rs b/vendor/hayro-jbig2/src/decode/generic.rs new file mode 100644 index 00000000..a788163f --- /dev/null +++ b/vendor/hayro-jbig2/src/decode/generic.rs @@ -0,0 +1,792 @@ +//! Generic region segment parsing and decoding (7.4.6, 6.2). + +use super::{ + AdaptiveTemplatePixel, RegionBitmap, RegionSegmentInfo, Template, parse_region_segment_info, +}; +use crate::ScratchBuffers; +use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext}; +use crate::bitmap::{Bitmap, WORD_BITS, WORD_SHIFT, Word}; +use crate::error::{ParseError, RegionError, Result, TemplateError, bail}; +use crate::reader::Reader; + +/// Generic region decoding procedure (6.2). +pub(crate) fn decode( + header: &GenericRegionHeader<'_>, + ctx: &mut ScratchBuffers, +) -> Result { + let mut bitmap = Bitmap::new_with( + header.region_info.width, + header.region_info.height, + header.region_info.x_location, + header.region_info.y_location, + false, + )?; + + decode_into(header, &mut bitmap, ctx)?; + + Ok(RegionBitmap { + bitmap, + combination_operator: header.region_info.combination_operator, + }) +} + +pub(crate) fn decode_into( + header: &GenericRegionHeader<'_>, + bitmap: &mut Bitmap, + ctx: &mut ScratchBuffers, +) -> Result<()> { + let data = header.data; + + if header.mmr { + // "6.2.6 Decoding using MMR coding" + let _ = decode_bitmap_mmr(bitmap, data)?; + } else { + let mut decoder = ArithmeticDecoder::new(data); + ctx.contexts.clear(); + ctx.contexts.resize( + 1 << header.template.context_bits(), + ArithmeticDecoderContext::default(), + ); + + // "6.2.5 Decoding using a template and arithmetic coding" + decode_bitmap_arithmetic_coding( + bitmap, + &mut decoder, + &mut ctx.contexts, + header.template, + header.tpgdon, + &header.adaptive_template_pixels, + )?; + } + + Ok(()) +} + +/// Parsed generic region segment header (7.4.6.1). +#[derive(Debug, Clone)] +pub(crate) struct GenericRegionHeader<'a> { + pub(crate) region_info: RegionSegmentInfo, + pub(crate) mmr: bool, + pub(crate) template: Template, + pub(crate) tpgdon: bool, + pub(crate) adaptive_template_pixels: [AdaptiveTemplatePixel; 4], + pub(crate) data: &'a [u8], +} + +/// Parse a generic region segment header (7.4.6.1). +pub(crate) fn parse<'a>( + reader: &mut Reader<'a>, + had_unknown_length: bool, +) -> Result> { + let mut region_info = parse_region_segment_info(reader)?; + let flags = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + let mmr = flags & 0x01 != 0; + let template = Template::from_byte(flags >> 1); + let tpgdon = flags & 0x08 != 0; + let ext_template = flags & 0x10 != 0; + let adaptive_template_pixels = if mmr { + [AdaptiveTemplatePixel::default(); 4] + } else { + parse_adaptive_template_pixels(reader, template, ext_template)? + }; + let mut data = reader.tail().ok_or(ParseError::UnexpectedEof)?; + + // "As a special case, as noted in 7.2.7, an immediate generic region segment + // may have an unknown length. In this case, it also indicates the height of + // the generic region (i.e. the number of rows that have been decoded in this + // segment; it must be no greater than the region segment bitmap height value + // in the segment's region segment information field." (7.4.6.4) + if had_unknown_length { + // Length has already been validated during segment parsing. + let (head, tail) = data.split_at(data.len() - 4); + let row_count = u32::from_be_bytes(tail.try_into().unwrap()); + + if row_count > region_info.height { + bail!(RegionError::InvalidDimension); + } + + region_info.height = row_count; + data = head; + } + + Ok(GenericRegionHeader { + region_info, + mmr, + template, + tpgdon, + adaptive_template_pixels, + data, + }) +} + +/// Parse adaptive template pixel positions (7.4.6.3). +pub(crate) fn parse_adaptive_template_pixels( + reader: &mut Reader<'_>, + template: Template, + // TODO: Find a test with this flag. + _ext_template: bool, +) -> Result<[AdaptiveTemplatePixel; 4]> { + let num_pixels = template.adaptive_template_pixels() as usize; + + let mut pixels = [AdaptiveTemplatePixel::default(); 4]; + + for pixel in pixels.iter_mut().take(num_pixels) { + let x = reader.read_byte().ok_or(ParseError::UnexpectedEof)? as i8; + let y = reader.read_byte().ok_or(ParseError::UnexpectedEof)? as i8; + + // Validate AT pixel location (6.2.5.4, Figure 7). + // AT pixels must reference already-decoded pixels: + // - y must be <= 0 (current row or above) + // - if y == 0, x must be < 0 (strictly to the left of current pixel) + if y > 0 || (y == 0 && x >= 0) { + bail!(TemplateError::InvalidAtPixel); + } + + *pixel = AdaptiveTemplatePixel { x, y }; + } + + Ok(pixels) +} + +/// Whether the adaptive template pixels correspond to the default ones. +/// See Table 5. +fn has_default_at_pixels(template: Template, at_pixels: &[AdaptiveTemplatePixel; 4]) -> bool { + match template { + Template::Template0 => { + at_pixels[0].x == 3 + && at_pixels[0].y == -1 + && at_pixels[1].x == -3 + && at_pixels[1].y == -1 + && at_pixels[2].x == 2 + && at_pixels[2].y == -2 + && at_pixels[3].x == -2 + && at_pixels[3].y == -2 + } + Template::Template1 => at_pixels[0].x == 3 && at_pixels[0].y == -1, + Template::Template2 => at_pixels[0].x == 2 && at_pixels[0].y == -1, + Template::Template3 => at_pixels[0].x == 2 && at_pixels[0].y == -1, + } +} + +/// Decode a bitmap using MMR coding (6.2.6). +pub(crate) fn decode_bitmap_mmr(bitmap: &mut Bitmap, data: &[u8]) -> Result { + /// A decoder sink that writes decoded pixels into a `Bitmap`. + struct BitmapDecoder<'a> { + bitmap: &'a mut Bitmap, + x: u32, + y: u32, + /// Precomputed start index into `bitmap.data` for the current row. + row_start: usize, + /// Accumulator for pixels emitted by `push_pixel`. + buf: u8, + /// Number of bits accumulated in `buf`. + buf_len: u8, + } + + impl<'a> BitmapDecoder<'a> { + fn new(bitmap: &'a mut Bitmap) -> Self { + Self { + bitmap, + x: 0, + y: 0, + row_start: 0, + buf: 0, + buf_len: 0, + } + } + + #[inline] + fn flush_buf(&mut self) { + if self.buf_len == 0 { + return; + } + + let start_x = self.x - self.buf_len as u32; + if start_x < self.bitmap.width { + let word_idx = (start_x / WORD_BITS) as usize; + let bit_in_word = start_x % WORD_BITS; + let shift = WORD_SHIFT - bit_in_word - (self.buf_len as u32 - 1); + self.bitmap.data[self.row_start + word_idx] |= (self.buf as Word) << shift; + } + self.buf = 0; + self.buf_len = 0; + } + } + + impl hayro_ccitt::Decoder for BitmapDecoder<'_> { + #[inline] + fn push_pixel(&mut self, white: bool) { + self.buf = (self.buf << 1) | white as u8; + self.buf_len += 1; + self.x += 1; + + if self.buf_len == 8 { + self.flush_buf(); + } + } + + #[inline] + fn push_pixel_chunk(&mut self, white: bool, chunk_count: u32) { + const WORD_BYTES: usize = (WORD_BITS / 8) as usize; + const BYTE_MASKS: [Word; WORD_BYTES] = { + #[allow(trivial_numeric_casts)] + let mut masks = [0 as Word; WORD_BYTES]; + let mut i = 0; + while i < WORD_BYTES { + #[allow(trivial_numeric_casts)] + { + masks[i] = (0xFF as Word) << ((WORD_BYTES - 1 - i) * 8); + } + i += 1; + } + masks + }; + + let row_start = self.row_start; + let end_x = (self.x + chunk_count * 8).min(self.bitmap.width); + let white_mask = (white as Word).wrapping_neg(); + + let start = (self.x / 8) as usize; + let end = (end_x / 8) as usize; + let first_full = start.div_ceil(WORD_BYTES); + let last_full = end / WORD_BYTES; + + for b in start..(first_full * WORD_BYTES).min(end) { + self.bitmap.data[row_start + b / WORD_BYTES] |= + BYTE_MASKS[b % WORD_BYTES] & white_mask; + } + + if last_full > first_full { + self.bitmap.data[row_start + first_full..row_start + last_full].fill(white_mask); + } + + for b in (first_full.max(last_full) * WORD_BYTES)..end { + self.bitmap.data[row_start + b / WORD_BYTES] |= + BYTE_MASKS[b % WORD_BYTES] & white_mask; + } + + self.x = end_x; + } + + #[inline] + fn next_line(&mut self) { + self.flush_buf(); + + self.x = 0; + self.y += 1; + self.row_start = (self.y * self.bitmap.stride) as usize; + } + } + + let width = bitmap.width; + let height = bitmap.height; + let mut decoder = BitmapDecoder::new(bitmap); + + let settings = hayro_ccitt::DecodeSettings { + columns: width, + rows: height, + // "If the number of bytes contained in the encoded bitmap is known in + // advance, then it is permissible for the data stream not to contain + // an EOFB" (6.2.6). But it _can_ contain it, which is what this + // flag indicates. + end_of_block: true, + end_of_line: false, + rows_are_byte_aligned: false, + encoding: hayro_ccitt::EncodingMode::Group4, + // "Pixels decoded by the MMR decoder having the value 'black' shall be + // treated as having the value 1. Pixels decoded by the MMR decoder + // having the value 'white' shall be treated as having the value 0." + // (6.2.6) + // + // hayro-ccitt uses 1 for white, 0 for black by default, so we need to + // invert to match JBIG2 convention. + invert_black: true, + }; + + // "An invocation of the generic region decoding procedure with MMR equal to + // 1 shall consume an integral number of bytes, beginning and ending on a + // byte boundary. This may involve skipping over some bits in the last byte + // read." (6.2.6) + // + // hayro-ccitt already aligns to the byte boundary before returning, so + // nothing else to do here. + let mut context = hayro_ccitt::DecoderContext::new(settings); + Ok(hayro_ccitt::decode(data, &mut decoder, &mut context) + .map_err(|_| RegionError::InvalidMmrData)?) +} + +// I'm not sure why, but I was getting very weird codegen (with bad performance) +// when attempting to do this via generics. Hence why we use a macro for that. +macro_rules! decode_loop { + ($bitmap:expr, $decoder:expr, $contexts:expr, $ctx_gatherer:expr, + $tpgdon:expr, $sltp_context:expr, $gather:expr) => {{ + let bitmap: &mut Bitmap = $bitmap; + let decoder: &mut ArithmeticDecoder<'_> = $decoder; + let contexts: &mut [ArithmeticDecoderContext] = $contexts; + let ctx_gatherer: &mut ContextGatherer<'_> = $ctx_gatherer; + let width = bitmap.width; + let height = bitmap.height; + + // "1) Set: LTP = 0" (6.2.5.7) + let mut ltp = false; + + // "3) Decode each row as follows:" (6.2.5.7) + for y in 0..height { + // "b) If TPGDON is 1, then decode a bit using the arithmetic entropy + // coder" (6.2.5.7) + if $tpgdon { + let sltp = decoder.read_bit(&mut contexts[$sltp_context as usize]); + // "Let SLTP be the value of this bit. Set: LTP = LTP XOR SLTP" (6.2.5.7) + ltp = ltp != (sltp != 0); + } + + // "c) If LTP = 1 then set every pixel of the current row of GBREG equal + // to the corresponding pixel of the row immediately above." (6.2.5.7) + if ltp { + // If y == 0, pixels remain the same. + if y > 0 { + let stride = bitmap.stride as usize; + let src = (y as usize - 1) * stride; + bitmap + .data + .copy_within(src..src + stride, y as usize * stride); + } + } else { + // "d) If LTP = 0 then, from left to right, decode each pixel of the + // current row of GBREG." (6.2.5.7) + ctx_gatherer.start_row(bitmap, y); + + for x in 0..width { + ctx_gatherer.maybe_reload_buffers(bitmap, x); + let context_bits = ($gather)(ctx_gatherer, bitmap, x) as usize; + let pixel = decoder.read_bit(&mut contexts[context_bits]); + let value = pixel as u8; + bitmap.set_pixel(x, y, value); + ctx_gatherer.update_current_row(x, value); + } + } + } + }}; +} + +/// Decode a bitmap using arithmetic coding (6.2.5). +pub(crate) fn decode_bitmap_arithmetic_coding( + bitmap: &mut Bitmap, + decoder: &mut ArithmeticDecoder<'_>, + contexts: &mut [ArithmeticDecoderContext], + template: Template, + tpgdon: bool, + adaptive_template_pixels: &[AdaptiveTemplatePixel; 4], +) -> Result<()> { + let mut ctx_gatherer = ContextGatherer::new(template, adaptive_template_pixels); + + // See Figure 8 - 11. + let sltp_context: u16 = match template { + Template::Template0 => 0b1001101100100101, + Template::Template1 => 0b0011110010101, + Template::Template2 => 0b0011100101, + Template::Template3 => 0b0110010101, + }; + + if ctx_gatherer.use_default_at { + match template { + Template::Template0 => decode_loop!( + bitmap, + decoder, + contexts, + &mut ctx_gatherer, + tpgdon, + sltp_context, + ContextGatherer::gather_template0_default + ), + Template::Template1 => decode_loop!( + bitmap, + decoder, + contexts, + &mut ctx_gatherer, + tpgdon, + sltp_context, + ContextGatherer::gather_template1_default + ), + Template::Template2 => decode_loop!( + bitmap, + decoder, + contexts, + &mut ctx_gatherer, + tpgdon, + sltp_context, + ContextGatherer::gather_template2_default + ), + Template::Template3 => decode_loop!( + bitmap, + decoder, + contexts, + &mut ctx_gatherer, + tpgdon, + sltp_context, + ContextGatherer::gather_template3_default + ), + } + } else { + match template { + Template::Template0 => decode_loop!( + bitmap, + decoder, + contexts, + &mut ctx_gatherer, + tpgdon, + sltp_context, + ContextGatherer::gather_template0_custom + ), + Template::Template1 => decode_loop!( + bitmap, + decoder, + contexts, + &mut ctx_gatherer, + tpgdon, + sltp_context, + ContextGatherer::gather_template1_custom + ), + Template::Template2 => decode_loop!( + bitmap, + decoder, + contexts, + &mut ctx_gatherer, + tpgdon, + sltp_context, + ContextGatherer::gather_template2_custom + ), + Template::Template3 => decode_loop!( + bitmap, + decoder, + contexts, + &mut ctx_gatherer, + tpgdon, + sltp_context, + ContextGatherer::gather_template3_custom + ), + } + } + + Ok(()) +} + +pub(crate) struct ContextGatherer<'a> { + template: Template, + at_pixels: &'a [AdaptiveTemplatePixel; 4], + use_default_at: bool, + /// Used in `maybe_reload_buffers` to determine how far to the right + /// we might access pixels. + max_right: u32, + /// Current position. + cur_y: u32, + cur_x: u32, + /// Pre-fetched pixel buffers for rows y-2, y-1, y. + buf_m2: Word, + buf_m1: Word, + buf_cur: Word, + /// The current context for all 3 rows. + ctx: u16, +} + +impl<'a> ContextGatherer<'a> { + pub(crate) fn new(template: Template, at_pixels: &'a [AdaptiveTemplatePixel; 4]) -> Self { + let use_default_at = has_default_at_pixels(template, at_pixels); + let max_right = match template { + Template::Template0 | Template::Template1 => { + if use_default_at { + 3 + } else { + 2 + } + } + Template::Template2 | Template::Template3 => { + if use_default_at { + 2 + } else { + 1 + } + } + }; + Self { + template, + at_pixels, + use_default_at, + max_right, + cur_y: 0, + buf_m2: 0, + buf_m1: 0, + buf_cur: 0, + cur_x: 0, + ctx: 0, + } + } + + const SHIFT_MASK_T0: u16 = 0b0110_0011_1100_1110; + const SHIFT_MASK_T1: u16 = 0b0001_1101_1110_0110; + const SHIFT_MASK_T2: u16 = 0b0000_0011_0111_0010; + const SHIFT_MASK_T3: u16 = 0b0000_0011_1100_1110; + + const SHIFT_MASK_T0_DEFAULT: u16 = 0b1111_0111_1110_1110; + const SHIFT_MASK_T1_DEFAULT: u16 = 0b0001_1101_1111_0110; + const SHIFT_MASK_T2_DEFAULT: u16 = 0b0000_0011_0111_1010; + const SHIFT_MASK_T3_DEFAULT: u16 = 0b0000_0011_1110_1110; + + pub(crate) fn start_row(&mut self, bitmap: &Bitmap, y: u32) { + self.cur_y = y; + self.cur_x = 0; + + self.buf_m2 = if y >= 2 { + Self::load_word(bitmap, y - 2, 0) + } else { + 0 + }; + self.buf_m1 = if y >= 1 { + Self::load_word(bitmap, y - 1, 0) + } else { + 0 + }; + self.buf_cur = 0; + + // Start initializing the contexts. Note that this won't load all initial + // pixels yet, those will only be loaded after our first call to `gather`. + // See 6.2.5.3 for the pixel positions. + self.ctx = if self.use_default_at { + self.init_context_default() + } else { + self.init_context_custom() + }; + } + + #[inline] + fn init_context_custom(&self) -> u16 { + match self.template { + Template::Template0 => { + let m2 = Self::get_buf_pixel(self.buf_m2, 0); + let m1 = (Self::get_buf_pixel(self.buf_m1, 0) << 1) + | Self::get_buf_pixel(self.buf_m1, 1); + (m2 << 12) | (m1 << 5) + } + Template::Template1 => { + let m2 = (Self::get_buf_pixel(self.buf_m2, 0) << 1) + | Self::get_buf_pixel(self.buf_m2, 1); + let m1 = (Self::get_buf_pixel(self.buf_m1, 0) << 1) + | Self::get_buf_pixel(self.buf_m1, 1); + (m2 << 9) | (m1 << 4) + } + Template::Template2 => { + let m2 = Self::get_buf_pixel(self.buf_m2, 0); + let m1 = Self::get_buf_pixel(self.buf_m1, 0); + (m2 << 7) | (m1 << 3) + } + Template::Template3 => { + let m1 = Self::get_buf_pixel(self.buf_m1, 0); + m1 << 5 + } + } + } + + #[inline] + fn init_context_default(&self) -> u16 { + match self.template { + Template::Template0 => { + (Self::get_buf_pixel(self.buf_m2, 0) << 12) + | (Self::get_buf_pixel(self.buf_m2, 1) << 11) + | (Self::get_buf_pixel(self.buf_m1, 0) << 6) + | (Self::get_buf_pixel(self.buf_m1, 1) << 5) + | (Self::get_buf_pixel(self.buf_m1, 2) << 4) + } + Template::Template1 => { + (Self::get_buf_pixel(self.buf_m2, 0) << 10) + | (Self::get_buf_pixel(self.buf_m2, 1) << 9) + | (Self::get_buf_pixel(self.buf_m1, 0) << 5) + | (Self::get_buf_pixel(self.buf_m1, 1) << 4) + | (Self::get_buf_pixel(self.buf_m1, 2) << 3) + } + Template::Template2 => { + (Self::get_buf_pixel(self.buf_m2, 0) << 7) + | (Self::get_buf_pixel(self.buf_m1, 0) << 3) + | (Self::get_buf_pixel(self.buf_m1, 1) << 2) + } + Template::Template3 => { + (Self::get_buf_pixel(self.buf_m1, 0) << 5) + | (Self::get_buf_pixel(self.buf_m1, 1) << 4) + } + } + } + + #[inline(always)] + pub(crate) fn load_word(bitmap: &Bitmap, row_y: u32, start_x: u32) -> Word { + let word_idx = start_x / WORD_BITS; + + if start_x.is_multiple_of(WORD_BITS) { + bitmap.get_word(row_y, word_idx) + } else { + let bit_offset = start_x % WORD_BITS; + let word1 = bitmap.get_word(row_y, word_idx); + let word2 = bitmap.get_word(row_y, word_idx + 1); + (word1 << bit_offset) | (word2 >> (WORD_BITS - bit_offset)) + } + } + + #[inline] + pub(crate) fn get_buf_pixel(buf: Word, pos: u32) -> u16 { + if pos < WORD_BITS { + ((buf >> (WORD_SHIFT - pos)) & 1) as u16 + } else { + 0 + } + } + + #[inline(always)] + pub(crate) fn maybe_reload_buffers(&mut self, bitmap: &Bitmap, x: u32) { + if x + self.max_right >= self.cur_x + WORD_BITS { + let new_start = x.saturating_sub(4); + self.cur_x = new_start; + self.buf_m2 = if self.cur_y >= 2 { + Self::load_word(bitmap, self.cur_y - 2, new_start) + } else { + 0 + }; + self.buf_m1 = if self.cur_y >= 1 { + Self::load_word(bitmap, self.cur_y - 1, new_start) + } else { + 0 + }; + self.buf_cur = Self::load_word(bitmap, self.cur_y, new_start); + } + } + + #[inline(always)] + fn gather_template0_custom(&mut self, bitmap: &Bitmap, x: u32) -> u16 { + let bx = x - self.cur_x; + let xi = x as i32; + let yi = self.cur_y as i32; + + let new_pixels = (Self::get_buf_pixel(self.buf_m2, bx + 1) << 12) + | (Self::get_buf_pixel(self.buf_m1, bx + 2) << 5) + | Self::get_buf_pixel(self.buf_cur, bx.wrapping_sub(1)) + // Note that the cast from i32 to u32 is deliberate here: Negative positions + // should be resolved to 0, by casting negative i32 to u32 we end up with + // a very large positive number which is guaranteed to be OOB. The same + // applies to all other occurrences of this pattern (also in `generic_refinement.rs`). + | ((bitmap.get_pixel((xi + self.at_pixels[3].x as i32) as u32, (yi + self.at_pixels[3].y as i32) as u32) as u16) << 15) + | ((bitmap.get_pixel((xi + self.at_pixels[2].x as i32) as u32, (yi + self.at_pixels[2].y as i32) as u32) as u16) << 11) + | ((bitmap.get_pixel((xi + self.at_pixels[1].x as i32) as u32, (yi + self.at_pixels[1].y as i32) as u32) as u16) << 10) + | ((bitmap.get_pixel((xi + self.at_pixels[0].x as i32) as u32, (yi + self.at_pixels[0].y as i32) as u32) as u16) << 4); + + self.ctx = ((self.ctx << 1) & Self::SHIFT_MASK_T0) | new_pixels; + self.ctx + } + + #[inline(always)] + fn gather_template1_custom(&mut self, bitmap: &Bitmap, x: u32) -> u16 { + let bx = x - self.cur_x; + let xi = x as i32; + let yi = self.cur_y as i32; + + let new_pixels = (Self::get_buf_pixel(self.buf_m2, bx + 2) << 9) + | (Self::get_buf_pixel(self.buf_m1, bx + 2) << 4) + | Self::get_buf_pixel(self.buf_cur, bx.wrapping_sub(1)) + | ((bitmap.get_pixel( + (xi + self.at_pixels[0].x as i32) as u32, + (yi + self.at_pixels[0].y as i32) as u32, + ) as u16) + << 3); + + self.ctx = ((self.ctx << 1) & Self::SHIFT_MASK_T1) | new_pixels; + self.ctx + } + + #[inline(always)] + fn gather_template2_custom(&mut self, bitmap: &Bitmap, x: u32) -> u16 { + let bx = x - self.cur_x; + let xi = x as i32; + let yi = self.cur_y as i32; + + let new_pixels = (Self::get_buf_pixel(self.buf_m2, bx + 1) << 7) + | (Self::get_buf_pixel(self.buf_m1, bx + 1) << 3) + | Self::get_buf_pixel(self.buf_cur, bx.wrapping_sub(1)) + | ((bitmap.get_pixel( + (xi + self.at_pixels[0].x as i32) as u32, + (yi + self.at_pixels[0].y as i32) as u32, + ) as u16) + << 2); + + self.ctx = ((self.ctx << 1) & Self::SHIFT_MASK_T2) | new_pixels; + self.ctx + } + + #[inline(always)] + fn gather_template3_custom(&mut self, bitmap: &Bitmap, x: u32) -> u16 { + let bx = x - self.cur_x; + let xi = x as i32; + let yi = self.cur_y as i32; + + let new_pixels = (Self::get_buf_pixel(self.buf_m1, bx + 1) << 5) + | Self::get_buf_pixel(self.buf_cur, bx.wrapping_sub(1)) + | ((bitmap.get_pixel( + (xi + self.at_pixels[0].x as i32) as u32, + (yi + self.at_pixels[0].y as i32) as u32, + ) as u16) + << 4); + + self.ctx = ((self.ctx << 1) & Self::SHIFT_MASK_T3) | new_pixels; + self.ctx + } + + #[inline(always)] + pub(crate) fn gather_template0_default(&mut self, _bitmap: &Bitmap, x: u32) -> u16 { + let bx = x - self.cur_x; + let new_pixels = (Self::get_buf_pixel(self.buf_m2, bx + 2) << 11) + | (Self::get_buf_pixel(self.buf_m1, bx + 3) << 4) + | Self::get_buf_pixel(self.buf_cur, bx.wrapping_sub(1)); + + self.ctx = ((self.ctx << 1) & Self::SHIFT_MASK_T0_DEFAULT) | new_pixels; + self.ctx + } + + #[inline(always)] + pub(crate) fn gather_template1_default(&mut self, _bitmap: &Bitmap, x: u32) -> u16 { + let bx = x - self.cur_x; + let new_pixels = (Self::get_buf_pixel(self.buf_m2, bx + 2) << 9) + | (Self::get_buf_pixel(self.buf_m1, bx + 3) << 3) + | Self::get_buf_pixel(self.buf_cur, bx.wrapping_sub(1)); + + self.ctx = ((self.ctx << 1) & Self::SHIFT_MASK_T1_DEFAULT) | new_pixels; + self.ctx + } + + #[inline(always)] + pub(crate) fn gather_template2_default(&mut self, _bitmap: &Bitmap, x: u32) -> u16 { + let bx = x - self.cur_x; + let new_pixels = (Self::get_buf_pixel(self.buf_m2, bx + 1) << 7) + | (Self::get_buf_pixel(self.buf_m1, bx + 2) << 2) + | Self::get_buf_pixel(self.buf_cur, bx.wrapping_sub(1)); + + self.ctx = ((self.ctx << 1) & Self::SHIFT_MASK_T2_DEFAULT) | new_pixels; + self.ctx + } + + #[inline(always)] + pub(crate) fn gather_template3_default(&mut self, _bitmap: &Bitmap, x: u32) -> u16 { + let bx = x - self.cur_x; + let new_pixels = (Self::get_buf_pixel(self.buf_m1, bx + 2) << 4) + | Self::get_buf_pixel(self.buf_cur, bx.wrapping_sub(1)); + + self.ctx = ((self.ctx << 1) & Self::SHIFT_MASK_T3_DEFAULT) | new_pixels; + self.ctx + } + + /// Note: The caller must ensure that `gather` has been called for this `x` + /// first. + #[inline(always)] + pub(crate) fn update_current_row(&mut self, x: u32, value: u8) { + debug_assert!(x >= self.cur_x && x < self.cur_x + WORD_BITS); + + let bit_pos = WORD_SHIFT - (x - self.cur_x); + #[allow(trivial_numeric_casts)] + let mask = (1 as Word) << bit_pos; + self.buf_cur = (self.buf_cur & !mask) | ((value as Word) << bit_pos); + } +} diff --git a/vendor/hayro-jbig2/src/decode/generic_refinement.rs b/vendor/hayro-jbig2/src/decode/generic_refinement.rs new file mode 100644 index 00000000..a566b8de --- /dev/null +++ b/vendor/hayro-jbig2/src/decode/generic_refinement.rs @@ -0,0 +1,507 @@ +//! Generic refinement region segment parsing and decoding (7.4.7, 6.3). + +use alloc::vec::Vec; + +use super::{ + AdaptiveTemplatePixel, RefinementTemplate, RegionBitmap, RegionSegmentInfo, + parse_refinement_at_pixels, parse_region_segment_info, +}; +use crate::ScratchBuffers; +use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext}; +use crate::bitmap::{Bitmap, WORD_BITS, WORD_SHIFT, Word}; +use crate::decode::generic::ContextGatherer; +use crate::error::{OverflowError, ParseError, RegionError, Result, bail}; +use crate::reader::Reader; + +/// Generic refinement region decoding procedure (6.3). +pub(crate) fn decode( + header: &GenericRefinementRegionHeader<'_>, + reference: &Bitmap, + ctx: &mut ScratchBuffers, +) -> Result { + let mut region = Bitmap::new_with( + header.region_info.width, + header.region_info.height, + header.region_info.x_location, + header.region_info.y_location, + false, + )?; + + decode_into(header, reference, &mut region, ctx)?; + + Ok(RegionBitmap { + bitmap: region, + combination_operator: header.region_info.combination_operator, + }) +} + +/// Decode a generic refinement region directly into an existing bitmap. +pub(crate) fn decode_into( + header: &GenericRefinementRegionHeader<'_>, + reference: &Bitmap, + region: &mut Bitmap, + ctx: &mut ScratchBuffers, +) -> Result<()> { + let data = header.data; + + // Validate that the region fits within the reference bitmap. + // When referring to another segment, dimensions must match exactly (7.4.7.5). + // When using the page bitmap as reference, the region must fit within the page. + if header.region_info.width > reference.width || header.region_info.height > reference.height { + bail!(RegionError::InvalidDimension); + } + + let reference_dx = i32::try_from(reference.x_location) + .ok() + .and_then(|r: i32| { + i32::try_from(header.region_info.x_location) + .ok() + .and_then(|h| r.checked_sub(h)) + }) + .ok_or(OverflowError::ReferenceOffset)?; + let reference_dy = i32::try_from(reference.y_location) + .ok() + .and_then(|r: i32| { + i32::try_from(header.region_info.y_location) + .ok() + .and_then(|h| r.checked_sub(h)) + }) + .ok_or(OverflowError::ReferenceOffset)?; + + let mut decoder = ArithmeticDecoder::new(data); + let num_context_bits = header.template.context_bits(); + ctx.contexts.clear(); + ctx.contexts + .resize(1 << num_context_bits, ArithmeticDecoderContext::default()); + + decode_bitmap( + &mut decoder, + &mut ctx.contexts, + region, + reference, + reference_dx, + reference_dy, + header.template, + &header.adaptive_template_pixels, + header.tpgron, + )?; + + Ok(()) +} + +/// Parsed generic refinement region segment header (7.4.7.1). +#[derive(Debug, Clone)] +pub(crate) struct GenericRefinementRegionHeader<'a> { + pub(crate) region_info: RegionSegmentInfo, + pub(crate) template: RefinementTemplate, + pub(crate) tpgron: bool, + pub(crate) adaptive_template_pixels: Vec, + pub(crate) data: &'a [u8], +} + +/// Parse a generic refinement region segment header (7.4.7.1). +pub(crate) fn parse<'a>(reader: &mut Reader<'a>) -> Result> { + let region_info = parse_region_segment_info(reader)?; + let flags = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + let template = RefinementTemplate::from_byte(flags); + let tpgron = flags & 0x02 != 0; + let adaptive_template_pixels = if template == RefinementTemplate::Template0 { + parse_refinement_at_pixels(reader)? + } else { + Vec::new() + }; + let data = reader.tail().ok_or(ParseError::UnexpectedEof)?; + + Ok(GenericRefinementRegionHeader { + region_info, + template, + tpgron, + adaptive_template_pixels, + data, + }) +} + +fn has_default_at_pixels(at_pixels: &[AdaptiveTemplatePixel]) -> bool { + at_pixels.len() >= 2 + && at_pixels[0].x == -1 + && at_pixels[0].y == -1 + && at_pixels[1].x == -1 + && at_pixels[1].y == -1 +} + +pub(crate) struct RefinementContextGatherer<'a> { + template: RefinementTemplate, + at_pixels: &'a [AdaptiveTemplatePixel], + use_default_at: bool, + reference_dx: i32, + reference_dy: i32, + reg_y: u32, + reg_cur_x: u32, + reg_m1: Word, + reg_cur: Word, + ref_cur_x: u32, + ref_m1: Word, + ref_cur: Word, + ref_p1: Word, + ctx: u16, +} + +const SHIFT_MASK_T0_DEFAULT: u16 = 0b1_1001_1011_0110; +const SHIFT_MASK_T0_CUSTOM: u16 = 0b0_1000_1011_0110; +const SHIFT_MASK_T1: u16 = 0b11_0001_1010; + +impl<'a> RefinementContextGatherer<'a> { + pub(crate) fn new( + template: RefinementTemplate, + at_pixels: &'a [AdaptiveTemplatePixel], + reference_dx: i32, + reference_dy: i32, + ) -> Self { + let use_default_at = + template == RefinementTemplate::Template0 && has_default_at_pixels(at_pixels); + Self { + template, + at_pixels, + use_default_at, + reference_dx, + reference_dy, + reg_y: 0, + reg_cur_x: 0, + reg_m1: 0, + reg_cur: 0, + ref_cur_x: 0, + ref_m1: 0, + ref_cur: 0, + ref_p1: 0, + ctx: 0, + } + } + + pub(crate) fn start_row(&mut self, region: &Bitmap, reference: &Bitmap, y: u32) { + self.reg_y = y; + let ref_y = y as i32 - self.reference_dy; + // This will on purpose wrap to a very large `u32` for negative values, such that + // they resolve to 0 in `ContextGatherer::load_word`. + let ref_x0 = (-self.reference_dx) as u32; + + self.reg_cur_x = 0; + self.reg_m1 = if y >= 1 { + ContextGatherer::load_word(region, y - 1, 0) + } else { + 0 + }; + self.reg_cur = 0; + + self.ref_cur_x = 0; + self.ref_m1 = ContextGatherer::load_word(reference, (ref_y - 1) as u32, 0); + self.ref_cur = ContextGatherer::load_word(reference, ref_y as u32, 0); + self.ref_p1 = ContextGatherer::load_word(reference, (ref_y + 1) as u32, 0); + + self.ctx = match self.template { + RefinementTemplate::Template1 => { + let rbx = ref_x0.wrapping_sub(self.ref_cur_x); + + let reg_m1_0 = ContextGatherer::get_buf_pixel(self.reg_m1, 0); + let ref_cur_m1 = ContextGatherer::get_buf_pixel(self.ref_cur, rbx.wrapping_sub(1)); + let ref_cur_0 = ContextGatherer::get_buf_pixel(self.ref_cur, rbx); + let ref_p1_0 = ContextGatherer::get_buf_pixel(self.ref_p1, rbx); + + (reg_m1_0 << 7) | (ref_cur_m1 << 3) | (ref_cur_0 << 2) | (ref_p1_0) + } + RefinementTemplate::Template0 if self.use_default_at => { + let rbx = ref_x0.wrapping_sub(self.ref_cur_x); + let reg_m1_0 = ContextGatherer::get_buf_pixel(self.reg_m1, 0); + let ref_m1_m1 = ContextGatherer::get_buf_pixel(self.ref_m1, rbx.wrapping_sub(1)); + let ref_m1_0 = ContextGatherer::get_buf_pixel(self.ref_m1, rbx); + let ref_cur_m1 = ContextGatherer::get_buf_pixel(self.ref_cur, rbx.wrapping_sub(1)); + let ref_cur_0 = ContextGatherer::get_buf_pixel(self.ref_cur, rbx); + let ref_p1_m1 = ContextGatherer::get_buf_pixel(self.ref_p1, rbx.wrapping_sub(1)); + let ref_p1_0 = ContextGatherer::get_buf_pixel(self.ref_p1, rbx); + + (reg_m1_0 << 10) + | (ref_m1_m1 << 7) + | (ref_m1_0 << 6) + | (ref_cur_m1 << 4) + | (ref_cur_0 << 3) + | (ref_p1_m1 << 1) + | ref_p1_0 + } + RefinementTemplate::Template0 => { + let rbx = ref_x0.wrapping_sub(self.ref_cur_x); + let reg_m1_0 = ContextGatherer::get_buf_pixel(self.reg_m1, 0); + let ref_m1_0 = ContextGatherer::get_buf_pixel(self.ref_m1, rbx); + let ref_cur_m1 = ContextGatherer::get_buf_pixel(self.ref_cur, rbx.wrapping_sub(1)); + let ref_cur_0 = ContextGatherer::get_buf_pixel(self.ref_cur, rbx); + let ref_p1_m1 = ContextGatherer::get_buf_pixel(self.ref_p1, rbx.wrapping_sub(1)); + let ref_p1_0 = ContextGatherer::get_buf_pixel(self.ref_p1, rbx); + (reg_m1_0 << 10) + | (ref_m1_0 << 6) + | (ref_cur_m1 << 4) + | (ref_cur_0 << 3) + | (ref_p1_m1 << 1) + | ref_p1_0 + } + }; + } + + #[inline(always)] + pub(crate) fn maybe_reload_buffers(&mut self, region: &Bitmap, reference: &Bitmap, x: u32) { + if x + 1 >= self.reg_cur_x + WORD_BITS { + let new_start = x.saturating_sub(1); + self.reg_cur_x = new_start; + self.reg_m1 = if self.reg_y >= 1 { + ContextGatherer::load_word(region, self.reg_y - 1, new_start) + } else { + 0 + }; + self.reg_cur = ContextGatherer::load_word(region, self.reg_y, new_start); + } + + let ref_x_signed = x as i32 - self.reference_dx; + if ref_x_signed < 0 { + self.ref_cur_x = 0; + self.ref_m1 = 0; + self.ref_cur = 0; + self.ref_p1 = 0; + } else { + let ref_x = ref_x_signed as u32; + + if ref_x + 1 >= self.ref_cur_x + WORD_BITS || ref_x < self.ref_cur_x { + let new_start = ref_x.saturating_sub(1); + let ref_y = self.reg_y as i32 - self.reference_dy; + self.ref_cur_x = new_start; + self.ref_m1 = ContextGatherer::load_word(reference, (ref_y - 1) as u32, new_start); + self.ref_cur = ContextGatherer::load_word(reference, ref_y as u32, new_start); + self.ref_p1 = ContextGatherer::load_word(reference, (ref_y + 1) as u32, new_start); + } + } + } + + #[inline(always)] + pub(crate) fn tpgr_all_same(&self, x: u32) -> bool { + let ref_x = (x as i32 - self.reference_dx) as u32; + let rbx = ref_x.wrapping_sub(self.ref_cur_x); + + #[inline(always)] + fn extract_3bits(buf: Word, rbx: u32) -> u32 { + if rbx == 0 || rbx >= WORD_BITS - 1 { + // Near word boundary, fall back to individual extraction. + let b0 = ContextGatherer::get_buf_pixel(buf, rbx.wrapping_sub(1)) as u32; + let b1 = ContextGatherer::get_buf_pixel(buf, rbx) as u32; + let b2 = ContextGatherer::get_buf_pixel(buf, rbx.wrapping_add(1)) as u32; + (b0 << 2) | (b1 << 1) | b2 + } else { + // Fast path: shift and mask 3 bits at once. + #[allow(trivial_numeric_casts)] + #[allow(clippy::unnecessary_cast)] + { + ((buf >> (WORD_SHIFT - rbx - 1)) & 0b111) as u32 + } + } + } + + let m1 = extract_3bits(self.ref_m1, rbx); + let cur = extract_3bits(self.ref_cur, rbx); + let p1 = extract_3bits(self.ref_p1, rbx); + + // Check that all 9 pixels are the same. + m1 == cur && cur == p1 && (cur == 0 || cur == 0b111) + } + + #[inline(always)] + pub(crate) fn ref_center_pixel(&self, x: u32) -> u8 { + let ref_x = (x as i32 - self.reference_dx) as u32; + let rbx = ref_x.wrapping_sub(self.ref_cur_x); + + ContextGatherer::get_buf_pixel(self.ref_cur, rbx) as u8 + } + + #[inline(always)] + pub(crate) fn update_current_row(&mut self, x: u32, value: u8) { + let bx = x - self.reg_cur_x; + debug_assert!(bx < WORD_BITS); + let bit_pos = WORD_SHIFT - bx; + #[allow(trivial_numeric_casts)] + let mask = (1 as Word) << bit_pos; + self.reg_cur = (self.reg_cur & !mask) | ((value as Word) << bit_pos); + } + + #[inline(always)] + fn gather_template0_default(&mut self, _region: &Bitmap, _reference: &Bitmap, x: u32) -> u16 { + let bx = x - self.reg_cur_x; + let ref_x = (x as i32 - self.reference_dx) as u32; + let rbx = ref_x.wrapping_sub(self.ref_cur_x); + + let new_pixels = (ContextGatherer::get_buf_pixel(self.reg_m1, bx + 1) << 10) + | (ContextGatherer::get_buf_pixel(self.reg_cur, bx.wrapping_sub(1)) << 9) + | (ContextGatherer::get_buf_pixel(self.ref_m1, rbx.wrapping_add(1)) << 6) + | (ContextGatherer::get_buf_pixel(self.ref_cur, rbx.wrapping_add(1)) << 3) + | ContextGatherer::get_buf_pixel(self.ref_p1, rbx.wrapping_add(1)); + + self.ctx = ((self.ctx << 1) & SHIFT_MASK_T0_DEFAULT) | new_pixels; + self.ctx + } + + #[inline(always)] + fn gather_template0_custom(&mut self, region: &Bitmap, reference: &Bitmap, x: u32) -> u16 { + let bx = x - self.reg_cur_x; + let ref_x_i = x as i32 - self.reference_dx; + let rbx = ref_x_i as u32 - self.ref_cur_x; + + let xi = x as i32; + let yi = self.reg_y as i32; + let ref_y = yi - self.reference_dy; + let at1 = self.at_pixels[0]; + let at2 = self.at_pixels[1]; + + let new_pixels = ((region.get_pixel((xi + at1.x as i32) as u32, (yi + at1.y as i32) as u32) + as u16) + << 12) + | (ContextGatherer::get_buf_pixel(self.reg_m1, bx + 1) << 10) + | (ContextGatherer::get_buf_pixel(self.reg_cur, bx.wrapping_sub(1)) << 9) + | ((reference.get_pixel( + (ref_x_i + at2.x as i32) as u32, + (ref_y + at2.y as i32) as u32, + ) as u16) + << 8) + | (ContextGatherer::get_buf_pixel(self.ref_m1, rbx.wrapping_add(1)) << 6) + | (ContextGatherer::get_buf_pixel(self.ref_cur, rbx.wrapping_add(1)) << 3) + | ContextGatherer::get_buf_pixel(self.ref_p1, rbx.wrapping_add(1)); + + self.ctx = ((self.ctx << 1) & SHIFT_MASK_T0_CUSTOM) | new_pixels; + self.ctx + } + + #[inline(always)] + fn gather_template1(&mut self, _region: &Bitmap, _reference: &Bitmap, x: u32) -> u16 { + let bx = x - self.reg_cur_x; + let ref_x = (x as i32 - self.reference_dx) as u32; + let rbx = ref_x.wrapping_sub(self.ref_cur_x); + + let new_pixels = (ContextGatherer::get_buf_pixel(self.reg_m1, bx + 1) << 7) + | (ContextGatherer::get_buf_pixel(self.reg_cur, bx.wrapping_sub(1)) << 6) + | (ContextGatherer::get_buf_pixel(self.ref_m1, rbx) << 5) + | (ContextGatherer::get_buf_pixel(self.ref_cur, rbx.wrapping_add(1)) << 2) + | ContextGatherer::get_buf_pixel(self.ref_p1, rbx.wrapping_add(1)); + + self.ctx = ((self.ctx << 1) & SHIFT_MASK_T1) | new_pixels; + self.ctx + } +} + +/// Decode a refinement bitmap (6.3.5.6). +pub(crate) fn decode_bitmap( + decoder: &mut ArithmeticDecoder<'_>, + contexts: &mut [ArithmeticDecoderContext], + region: &mut Bitmap, + reference: &Bitmap, + reference_dx: i32, + reference_dy: i32, + gr_template: RefinementTemplate, + adaptive_template_pixels: &[AdaptiveTemplatePixel], + tpgron: bool, +) -> Result<()> { + macro_rules! refinement_decode_loop { + ($gatherer:expr, $tpgron:expr, $sltp_context:expr, $gather:expr) => {{ + let gatherer: &mut RefinementContextGatherer<'_> = $gatherer; + let width = region.width; + let height = region.height; + + // "1) Set LTP = 0." (6.3.5.6) + let mut ltp = false; + + // "3) Decode each row as follows:" (6.3.5.6) + for y in 0..height { + // "b) If TPGRON is 1, then decode a bit using the arithmetic entropy + // coder" (6.3.5.6) + if $tpgron { + let sltp = decoder.read_bit(&mut contexts[$sltp_context as usize]); + // "Let SLTP be the value of this bit. Set: LTP = LTP XOR SLTP" + ltp = ltp != (sltp != 0); + } + + gatherer.start_row(region, reference, y); + + // "c) If LTP = 0 then, from left to right, explicitly decode all pixels + // of the current row of GRREG." (6.3.5.6) + if !ltp { + for x in 0..width { + gatherer.maybe_reload_buffers(region, reference, x); + let context = ($gather)(gatherer, region, reference, x) as usize; + let pixel = decoder.read_bit(&mut contexts[context]); + region.set_pixel(x, y, pixel as u8); + gatherer.update_current_row(x, pixel as u8); + } + } else { + // "d) If LTP = 1 then, from left to right, implicitly decode certain + // pixels of the current row of GRREG, and explicitly decode the rest." + // (6.3.5.6) + for x in 0..width { + gatherer.maybe_reload_buffers(region, reference, x); + + let context = ($gather)(gatherer, region, reference, x) as usize; + + // "i) Set TPGRPIX equal to 1 if: + // - TPGRON is 1 AND; + // - a 3 ร— 3 pixel array in the reference bitmap (Figure 16), + // centred at the location corresponding to the current pixel, + // contains pixels all of the same value." (6.3.5.6) + if gatherer.tpgr_all_same(x) { + // "ii) If TPGRPIX is 1 then implicitly decode the current pixel + // by setting it equal to its predicted value (TPGRVAL)." (6.3.5.6) + // + // "When TPGRPIX is set to 1, set TPGRVAL equal to the current pixel + // predicted value, which is the common value of the nine adjacent + // pixels in the 3 ร— 3 array." (6.3.5.6) + let val = gatherer.ref_center_pixel(x); + region.set_pixel(x, y, val); + gatherer.update_current_row(x, val); + } else { + // "iii) Otherwise, explicitly decode the current pixel using the + // methodology of steps 3 c) i) through 3 c) iii) above." (6.3.5.6) + let pixel = decoder.read_bit(&mut contexts[context]); + region.set_pixel(x, y, pixel as u8); + gatherer.update_current_row(x, pixel as u8); + } + } + } + } + }}; + } + + let mut gatherer = RefinementContextGatherer::new( + gr_template, + adaptive_template_pixels, + reference_dx, + reference_dy, + ); + + // Context for SLTP depends on template (Figures 14, 15). + let sltp_context: u16 = match gr_template { + RefinementTemplate::Template0 => 0b0000000010000, + RefinementTemplate::Template1 => 0b0000001000, + }; + + match gr_template { + RefinementTemplate::Template0 if gatherer.use_default_at => refinement_decode_loop!( + &mut gatherer, + tpgron, + sltp_context, + RefinementContextGatherer::gather_template0_default + ), + RefinementTemplate::Template0 => refinement_decode_loop!( + &mut gatherer, + tpgron, + sltp_context, + RefinementContextGatherer::gather_template0_custom + ), + RefinementTemplate::Template1 => refinement_decode_loop!( + &mut gatherer, + tpgron, + sltp_context, + RefinementContextGatherer::gather_template1 + ), + } + + Ok(()) +} diff --git a/vendor/hayro-jbig2/src/decode/halftone.rs b/vendor/hayro-jbig2/src/decode/halftone.rs new file mode 100644 index 00000000..eb5c1c21 --- /dev/null +++ b/vendor/hayro-jbig2/src/decode/halftone.rs @@ -0,0 +1,425 @@ +//! Halftone region segment parsing and decoding (7.4.5, 6.6). + +use alloc::vec; +use alloc::vec::Vec; + +use super::RegionBitmap; +use super::pattern::PatternDictionary; +use super::{CombinationOperator, RegionSegmentInfo, Template, parse_region_segment_info}; +use crate::ScratchBuffers; +use crate::bitmap::{Bitmap, MAX_DIMENSION, WORD_BITS}; +use crate::error::{OverflowError, ParseError, RegionError, Result, bail}; +use crate::gray_scale::{GrayScaleParams, decode_gray_scale_image}; +use crate::reader::Reader; + +/// Decode a halftone region segment (7.4.5.2, 6.6). +pub(crate) fn decode( + header: &HalftoneRegionHeader<'_>, + pattern_dict: &PatternDictionary, + ctx: &mut ScratchBuffers, +) -> Result { + let region = &header.region_info; + + let mut htreg = Bitmap::new_with( + region.width, + region.height, + region.x_location, + region.y_location, + header.flags.initial_pixel_color, + )?; + + decode_into(header, pattern_dict, &mut htreg, ctx)?; + + Ok(RegionBitmap { + bitmap: htreg, + combination_operator: region.combination_operator, + }) +} + +pub(crate) fn decode_into( + header: &HalftoneRegionHeader<'_>, + pattern_dict: &PatternDictionary, + htreg: &mut Bitmap, + ctx: &mut ScratchBuffers, +) -> Result<()> { + let skip_bitmap = if header.flags.enable_skip { + Some(compute_skip_bitmap(header, pattern_dict, htreg)?) + } else { + None + }; + + // "3) Set HBPP to โŒˆlogโ‚‚(HNUMPATS)โŒ‰." (6.6.5) + let bits_per_pixel = (pattern_dict.patterns.len() as u32) + .saturating_sub(1) + .checked_ilog2() + .map_or(1, |n| n + 1); + + // "4) Decode an image GI of size HGW by HGH with HBPP bits per pixel using + // the gray-scale image decoding procedure as described in Annex C." (6.6.5) + let gs_params = GrayScaleParams { + use_mmr: header.flags.mmr, + bits_per_pixel, + width: header.grid_position_and_size.width, + height: header.grid_position_and_size.height, + template: header.flags.template, + skip_mask: skip_bitmap.as_deref(), + }; + let gi = decode_gray_scale_image(header.data, &gs_params, ctx)?; + + // "5) Place sequentially the patterns corresponding to the values in GI into + // HTREG by the procedure described in 6.6.5.2." (6.6.5) + render_patterns(htreg, &gi, header, pattern_dict)?; + + Ok(()) +} + +/// Parse a halftone region segment header (7.4.5.1). +pub(crate) fn parse<'a>(reader: &mut Reader<'a>) -> Result> { + let region_info = parse_region_segment_info(reader)?; + let flags_byte = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + let mmr = flags_byte & 0x01 != 0; + let template = Template::from_byte(flags_byte >> 1); + let enable_skip = flags_byte & 0x08 != 0; + let combination_operator = CombinationOperator::from_value(flags_byte >> 4)?; + let initial_pixel_color = flags_byte & 0x80 != 0; + + let flags = HalftoneRegionFlags { + mmr, + template, + enable_skip, + combination_operator, + initial_pixel_color, + }; + + let grid_width = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + let grid_height = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + + if grid_width > MAX_DIMENSION || grid_height > MAX_DIMENSION { + bail!(OverflowError::GridDimension); + } + // PdfCraft patch: the product is bounded too (see `bitmap::MAX_PIXELS`). + if u64::from(grid_width) * u64::from(grid_height) > crate::bitmap::MAX_PIXELS { + bail!(OverflowError::GridDimension); + } + + // TODO: Check grid offsets as well. + + let grid_horizontal_offset = reader.read_i32().ok_or(ParseError::UnexpectedEof)?; + let grid_vertical_offset = reader.read_i32().ok_or(ParseError::UnexpectedEof)?; + + let grid_position_and_size = HalftoneGridPositionAndSize { + width: grid_width, + height: grid_height, + horizontal_offset: grid_horizontal_offset, + vertical_offset: grid_vertical_offset, + }; + + let grid_x_vector = reader.read_u16().ok_or(ParseError::UnexpectedEof)?; + let grid_y_vector = reader.read_u16().ok_or(ParseError::UnexpectedEof)?; + + let grid_vector = HalftoneGridVector { + x_vector: grid_x_vector, + y_vector: grid_y_vector, + }; + + let data = reader.tail().ok_or(ParseError::UnexpectedEof)?; + + Ok(HalftoneRegionHeader { + region_info, + flags, + grid_position_and_size, + grid_vector, + data, + }) +} + +/// Parsed halftone region segment flags (7.4.5.1.1). +#[derive(Debug, Clone)] +pub(crate) struct HalftoneRegionFlags { + pub(crate) mmr: bool, + pub(crate) template: Template, + pub(crate) enable_skip: bool, + pub(crate) combination_operator: CombinationOperator, + pub(crate) initial_pixel_color: bool, +} + +/// Halftone grid position and size (7.4.5.1.2). +#[derive(Debug, Clone)] +pub(crate) struct HalftoneGridPositionAndSize { + pub(crate) width: u32, + pub(crate) height: u32, + pub(crate) horizontal_offset: i32, + pub(crate) vertical_offset: i32, +} + +/// Halftone grid vector (7.4.5.1.3). +#[derive(Debug, Clone)] +pub(crate) struct HalftoneGridVector { + /// `HRX` - 256 times the horizontal coordinate of the halftone grid vector. + pub(crate) x_vector: u16, + /// `HRY` - 256 times the vertical coordinate of the halftone grid vector. + pub(crate) y_vector: u16, +} + +/// Parsed halftone region segment header (7.4.5.1). +#[derive(Debug, Clone)] +pub(crate) struct HalftoneRegionHeader<'a> { + pub(crate) region_info: RegionSegmentInfo, + pub(crate) flags: HalftoneRegionFlags, + pub(crate) grid_position_and_size: HalftoneGridPositionAndSize, + pub(crate) grid_vector: HalftoneGridVector, + pub(crate) data: &'a [u8], +} + +struct GridCoords { + x: i64, + y: i64, + row_x: i64, + row_y: i64, + hrx: i64, + hry: i64, +} + +impl GridCoords { + fn new(grid: &HalftoneGridPositionAndSize, vector: &HalftoneGridVector) -> Self { + Self { + x: grid.horizontal_offset as i64, + y: grid.vertical_offset as i64, + row_x: grid.horizontal_offset as i64, + row_y: grid.vertical_offset as i64, + hrx: vector.x_vector as i64, + hry: vector.y_vector as i64, + } + } + + #[inline] + fn get(&self) -> (i32, i32) { + ((self.x >> 8) as i32, (self.y >> 8) as i32) + } + + #[inline] + fn advance_col(&mut self) { + self.x += self.hrx; + self.y -= self.hry; + } + + #[inline] + fn advance_row(&mut self) { + self.row_x += self.hry; + self.row_y += self.hrx; + self.x = self.row_x; + self.y = self.row_y; + } +} + +/// Compute the HSKIP bitmap (6.6.5.1). +fn compute_skip_bitmap( + header: &HalftoneRegionHeader<'_>, + pattern_dict: &PatternDictionary, + htreg: &Bitmap, +) -> Result> { + let grid = &header.grid_position_and_size; + let pattern_width = pattern_dict.pattern_width as i32; + let pattern_height = pattern_dict.pattern_height as i32; + let region_width = htreg.width as i32; + let region_height = htreg.height as i32; + + let stride = grid.width.div_ceil(32); + let len = stride + .checked_mul(grid.height) + .ok_or(OverflowError::BitmapDimension)? as usize; + let mut hskip = vec![0_u32; len]; + let mut coords = GridCoords::new(grid, &header.grid_vector); + + for m_g in 0..grid.height { + for n_g in 0..grid.width { + let (x, y) = coords.get(); + + let skip = (x + pattern_width <= 0) + || (x >= region_width) + || (y + pattern_height <= 0) + || (y >= region_height); + + if skip { + let word_idx = (m_g * stride + n_g / 32) as usize; + let bit_pos = 31 - (n_g % 32); + hskip[word_idx] |= 1 << bit_pos; + } + + coords.advance_col(); + } + coords.advance_row(); + } + + Ok(hskip) +} + +/// Render patterns into the target region (6.6.5.2). +fn render_patterns( + region: &mut Bitmap, + gi: &[u32], + header: &HalftoneRegionHeader<'_>, + pattern_dict: &PatternDictionary, +) -> Result<()> { + let grid = &header.grid_position_and_size; + let op = header.flags.combination_operator; + let pw = pattern_dict.pattern_width; + let ph = pattern_dict.pattern_height; + let region_w = region.width as i32; + let region_h = region.height as i32; + let stride = region.stride; + let num_patterns = pattern_dict.patterns.len(); + + // The original pattern mask has 1s in the top `pw` bits. + let original_mask = if pw >= WORD_BITS { + !0_u32 + } else { + !0_u32 << (WORD_BITS - pw) + }; + + macro_rules! blit_rows { + ($data:expr, $shifted:expr, $yu:expr, $first_word:expr, $two:expr, + |$d0:ident, $s0:ident| $op0:expr, |$d1:ident, $s1:ident| $op1:expr) => { + if $two { + for row in 0..ph { + let base = (($yu + row) * stride + $first_word) as usize; + let si = row as usize * 2; + let $d0 = $data[base]; + let $s0 = $shifted[si]; + $data[base] = $op0; + let $d1 = $data[base + 1]; + let $s1 = $shifted[si + 1]; + $data[base + 1] = $op1; + } + } else { + for row in 0..ph { + let base = (($yu + row) * stride + $first_word) as usize; + let $d0 = $data[base]; + let $s0 = $shifted[row as usize * 2]; + $data[base] = $op0; + } + } + }; + } + + let mut coords = GridCoords::new(grid, &header.grid_vector); + let mut gi_idx = 0; + + for _ in 0..grid.height { + for _ in 0..grid.width { + let (x, y) = coords.get(); + + let pattern_index = gi[gi_idx] as usize; + gi_idx += 1; + + if pattern_index >= num_patterns { + bail!(RegionError::GrayScaleOutOfRange); + } + + // Check whether we can use the blit fast path. + let shifted = + if x >= 0 && y >= 0 && x + pw as i32 <= region_w && y + ph as i32 <= region_h { + pattern_dict.shifted_pattern(pattern_index, x as u32 % WORD_BITS) + } else { + None + }; + + if let Some(shifted) = shifted { + let xu = x as u32; + let yu = y as u32; + let first_word = xu / WORD_BITS; + let offset = xu % WORD_BITS; + let two = offset != 0 && first_word + 1 < stride; + + // TODO: It seems our current test suite only triggers for + // `Or`. + + match op { + CombinationOperator::Or => { + blit_rows!( + region.data, + shifted, + yu, + first_word, + two, + |d, s| d | s, + |d, s| d | s + ); + } + CombinationOperator::Xor => { + blit_rows!( + region.data, + shifted, + yu, + first_word, + two, + |d, s| d ^ s, + |d, s| d ^ s + ); + } + CombinationOperator::And => { + let inv_mask0 = !(original_mask >> offset); + let inv_mask1 = if offset == 0 { + !0 + } else { + !(original_mask << (WORD_BITS - offset)) + }; + blit_rows!( + region.data, + shifted, + yu, + first_word, + two, + |d, s| d & (s | inv_mask0), + |d, s| d & (s | inv_mask1) + ); + } + CombinationOperator::Replace => { + let inv_mask0 = !(original_mask >> offset); + let inv_mask1 = if offset == 0 { + !0 + } else { + !(original_mask << (WORD_BITS - offset)) + }; + blit_rows!( + region.data, + shifted, + yu, + first_word, + two, + |d, s| (d & inv_mask0) | s, + |d, s| (d & inv_mask1) | s + ); + } + CombinationOperator::Xnor => { + let mask0 = original_mask >> offset; + let mask1 = if offset == 0 { + 0 + } else { + original_mask << (WORD_BITS - offset) + }; + let inv_mask0 = !mask0; + let inv_mask1 = !mask1; + blit_rows!( + region.data, + shifted, + yu, + first_word, + two, + |d, s| (d & inv_mask0) | (!(d ^ s) & mask0), + |d, s| (d & inv_mask1) | (!(d ^ s) & mask1) + ); + } + } + } else { + let pattern = &pattern_dict.patterns[pattern_index]; + region.combine(pattern, x, y, op); + } + + coords.advance_col(); + } + coords.advance_row(); + } + + Ok(()) +} diff --git a/vendor/hayro-jbig2/src/decode/mod.rs b/vendor/hayro-jbig2/src/decode/mod.rs new file mode 100644 index 00000000..da9e0ecc --- /dev/null +++ b/vendor/hayro-jbig2/src/decode/mod.rs @@ -0,0 +1,236 @@ +//! Region and dictionary segment parsing and decoding. + +pub(crate) mod generic; +pub(crate) mod generic_refinement; +pub(crate) mod halftone; +pub(crate) mod pattern; +pub(crate) mod symbol; +pub(crate) mod text; + +use crate::bitmap::Bitmap; +use crate::decode::RefinementTemplate::{Template0, Template1}; +use crate::error::{ParseError, RegionError, Result, bail, err}; +use crate::reader::Reader; +use alloc::vec; +use alloc::vec::Vec; + +/// A decoded region bitmap with its combination operator. +#[derive(Debug, Clone)] +pub(crate) struct RegionBitmap { + /// The decoded bitmap. + pub(crate) bitmap: Bitmap, + /// How to combine this region with the page bitmap. + pub(crate) combination_operator: CombinationOperator, +} + +/// "These operators describe how the segment's bitmap is to be combined with +/// the page bitmap." (7.4.1.5) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) enum CombinationOperator { + /// 0 OR + #[default] + Or, + /// 1 AND + And, + /// 2 XOR + Xor, + /// 3 XNOR + Xnor, + /// 4 REPLACE + Replace, +} + +// Not really a default, just used as a dummy placeholder. + +impl CombinationOperator { + pub(crate) fn from_value(value: u8) -> Result { + match value & 0x07 { + 0 => Ok(Self::Or), + 1 => Ok(Self::And), + 2 => Ok(Self::Xor), + 3 => Ok(Self::Xnor), + 4 => Ok(Self::Replace), + _ => err!(RegionError::InvalidCombinationOperator), + } + } +} + +/// Template used for arithmetic of generic regions. +/// +/// - Generic regions: `GBTEMPLATE` (7.4.6.2) +/// - Symbol dictionaries: `SDTEMPLATE` (7.4.2.1.1) +/// - Pattern dictionaries: `HDTEMPLATE` (7.4.4.1.1) +/// - Halftone regions: `HTEMPLATE` (7.4.5.1.1) +/// - Gray-scale images: `GSTEMPLATE` (Annex C) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Template { + /// Template 0 + /// + /// Context bits: 16 (Figure 3). + Template0 = 0, + /// Template 1 + /// + /// Context bits: 13 (Figure 4). + Template1 = 1, + /// Template 2 + /// + /// Context bits: 10 (Figure 5). + Template2 = 2, + /// Template 3 + /// + /// Context bits: 10 (Figure 6). + Template3 = 3, +} + +impl Template { + pub(crate) fn from_byte(value: u8) -> Self { + match value & 0x03 { + 0 => Self::Template0, + 1 => Self::Template1, + 2 => Self::Template2, + 3 => Self::Template3, + _ => unreachable!(), + } + } + + /// Number of context bits for this template (6.2.5.3). + pub(crate) fn context_bits(self) -> usize { + match self { + Self::Template0 => 16, + Self::Template1 => 13, + Self::Template2 | Self::Template3 => 10, + } + } + + /// Number of adaptive template pixels (6.2.5.3). + pub(crate) fn adaptive_template_pixels(&self) -> u8 { + match self { + Self::Template0 => 4, + Self::Template1 | Self::Template2 | Self::Template3 => 1, + } + } +} + +/// Parsed region segment information field (7.4.1). +/// +/// "A region segment information field contains the following subfields, as +/// shown in Figure 30: +/// - Region segment bitmap width โ€“ see 7.4.1.1. +/// - Region segment bitmap height โ€“ see 7.4.1.2. +/// - Region segment bitmap X location โ€“ see 7.4.1.3. +/// - Region segment bitmap Y location โ€“ see 7.4.1.4. +/// - Region segment flags โ€“ see 7.4.1.5." (7.4.1) +#[derive(Debug, Clone)] +pub(crate) struct RegionSegmentInfo { + /// "This four-byte field gives the width in pixels of the bitmap encoded + /// in this segment." (7.4.1.1) + pub(crate) width: u32, + /// "This four-byte field gives the height in pixels of the bitmap encoded + /// in this segment." (7.4.1.2) + pub(crate) height: u32, + /// "This four-byte field gives the horizontal offset in pixels of the bitmap + /// encoded in this segment relative to the page bitmap." (7.4.1.3) + pub(crate) x_location: u32, + /// "This four-byte field gives the vertical offset in pixels of the bitmap + /// encoded in this segment relative to the page bitmap." (7.4.1.4) + pub(crate) y_location: u32, + /// "Bits 0-2: External combination operator." (7.4.1.5) + pub(crate) combination_operator: CombinationOperator, + /// "Bit 3: Colour extension flag (COLEXTFLAG). This field specifies whether + /// the region segment is extended to represent coloured bitmap." (7.4.1.5) + pub(crate) _colour_extension: bool, +} + +/// Parse the region segment information field (7.4.1). +pub(crate) fn parse_region_segment_info(reader: &mut Reader<'_>) -> Result { + // 7.4.1.1: Region segment bitmap width + let width = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + // 7.4.1.2: Region segment bitmap height + let height = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + // 7.4.1.3: Region segment bitmap X location + let x_location = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + // 7.4.1.: Region segment bitmap Y location + let y_location = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + + // 7.4.1.5: Region segment flags + let flags = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + + // "Bits 0-2: External combination operator." + let combination_operator = CombinationOperator::from_value(flags)?; + + // "Bit 3: Colour extension flag (COLEXTFLAG)." + let colour_extension = flags & 0x08 != 0; + + // "Bits 4-7: Reserved; must be 0." + if flags & 0xF0 != 0 { + bail!(RegionError::InvalidCombinationOperator); + } + + Ok(RegionSegmentInfo { + width, + height, + x_location, + y_location, + combination_operator, + _colour_extension: colour_extension, + }) +} + +/// Adaptive template pixel position for generic and refinement regions (6.2.5.4, Figure 7). +#[derive(Debug, Clone, Copy, Default)] +pub(crate) struct AdaptiveTemplatePixel { + pub(crate) x: i8, + pub(crate) y: i8, +} + +/// Parse refinement adaptive template pixels (used by symbol dictionary and text region). +/// +/// Used for: +/// - Symbol dictionary refinement AT flags (7.4.2.1.3): SDRATX1/SDRATY1, SDRATX2/SDRATY2 +/// - Text region refinement AT flags (7.4.3.1.3): SBRATX1/SBRATY1, SBRATX2/SBRATY2 +/// - Generic refinement region AT flags (7.4.7.3): GRATX1/GRATY1, GRATX2/GRATY2 +pub(crate) fn parse_refinement_at_pixels( + reader: &mut Reader<'_>, +) -> Result> { + let x1 = reader.read_byte().ok_or(ParseError::UnexpectedEof)? as i8; + let y1 = reader.read_byte().ok_or(ParseError::UnexpectedEof)? as i8; + + let x2 = reader.read_byte().ok_or(ParseError::UnexpectedEof)? as i8; + let y2 = reader.read_byte().ok_or(ParseError::UnexpectedEof)? as i8; + + Ok(vec![ + AdaptiveTemplatePixel { x: x1, y: y1 }, + AdaptiveTemplatePixel { x: x2, y: y2 }, + ]) +} + +/// Template used for refinement arithmetic coding (7.4.7.2). +/// +/// - Generic refinement regions: `GRTEMPLATE` (7.4.7.2) +/// - Symbol dictionaries: `SDRTEMPLATE` (7.4.2.1.1) +/// - Text regions: `SBRTEMPLATE` (7.4.3.1.1) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RefinementTemplate { + /// Template 0: 13 pixels (6.3.5.3, Figure 12) + Template0 = 0, + /// Template 1: 10 pixels (6.3.5.3, Figure 13) + Template1 = 1, +} + +impl RefinementTemplate { + pub(crate) fn from_byte(value: u8) -> Self { + if value & 0x01 == 0 { + Template0 + } else { + Template1 + } + } + + /// Number of context bits for this template (6.3.5.3). + pub(crate) fn context_bits(&self) -> usize { + match self { + Self::Template0 => 13, + Self::Template1 => 10, + } + } +} diff --git a/vendor/hayro-jbig2/src/decode/pattern.rs b/vendor/hayro-jbig2/src/decode/pattern.rs new file mode 100644 index 00000000..2c15d70b --- /dev/null +++ b/vendor/hayro-jbig2/src/decode/pattern.rs @@ -0,0 +1,221 @@ +//! Pattern dictionary segment parsing and decoding (7.4.4, 6.7). + +use super::{AdaptiveTemplatePixel, Template, generic}; +use crate::ScratchBuffers; +use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext}; +use crate::bitmap::{Bitmap, WORD_BITS, Word}; +use crate::error::{OverflowError, ParseError, Result}; +use crate::reader::Reader; +use alloc::vec::Vec; + +/// Decode a pattern dictionary segment (7.4.4.2, 6.7). +pub(crate) fn decode( + header: &PatternDictionaryHeader<'_>, + scratch: &mut ScratchBuffers, +) -> Result { + let pattern_width = header.pattern_width as u32; + let pattern_height = header.pattern_height as u32; + let num_patterns = header + .num_patterns + .checked_add(1) + .ok_or(OverflowError::BitmapDimension)?; + + // "1) Create a bitmap B_HDC. The height of this bitmap is HDPH. The width + // of the bitmap is (GRAYMAX + 1) ร— HDPW. This bitmap contains all the + // patterns concatenated left to right." (6.7.5) + let collective_width = num_patterns + .checked_mul(pattern_width) + .ok_or(OverflowError::BitmapDimension)?; + + let mut collective_bitmap = Bitmap::new(collective_width, pattern_height)?; + + // "2) Decode the collective bitmap using a generic region decoding procedure + // as described in 6.2." (6.7.5) + if header.mmr { + let _ = generic::decode_bitmap_mmr(&mut collective_bitmap, header.data)?; + } else { + let at_pixels = match header.template { + Template::Template0 => [ + AdaptiveTemplatePixel { + x: -(pattern_width as i8), + y: 0, + }, + AdaptiveTemplatePixel { x: -3, y: -1 }, + AdaptiveTemplatePixel { x: 2, y: -2 }, + AdaptiveTemplatePixel { x: -2, y: -2 }, + ], + Template::Template1 | Template::Template2 | Template::Template3 => [ + AdaptiveTemplatePixel { + x: -(pattern_width as i8), + y: 0, + }, + // Unused. + AdaptiveTemplatePixel::default(), + AdaptiveTemplatePixel::default(), + AdaptiveTemplatePixel::default(), + ], + }; + + let mut decoder = ArithmeticDecoder::new(header.data); + scratch.contexts.clear(); + scratch.contexts.resize( + 1 << header.template.context_bits(), + ArithmeticDecoderContext::default(), + ); + + generic::decode_bitmap_arithmetic_coding( + &mut collective_bitmap, + &mut decoder, + &mut scratch.contexts, + header.template, + false, + &at_pixels, + )?; + } + + // "3) Set: GRAY = 0" (6.7.5) + // "4) While GRAY โ‰ค GRAYMAX:" (6.7.5) + let mut patterns = Vec::with_capacity(num_patterns as usize); + + let src_stride = collective_bitmap.stride; + + for gray in 0..num_patterns { + // "a) Let the subimage of B_HDC consisting of HPH rows and columns + // HDPW ร— GRAY through HDPW ร— (GRAY + 1) โˆ’ 1 be denoted B_P. Set: + // HDPATS[GRAY] = B_P" (6.7.5)" + let start_x = gray * pattern_width; + let mut pattern = Bitmap::new(pattern_width, pattern_height)?; + + let src_word_idx = start_x / WORD_BITS; + let src_bit_offset = start_x % WORD_BITS; + let dst_stride = pattern.stride; + + for y in 0..pattern_height { + let src_row = (y * src_stride + src_word_idx) as usize; + let dst_row = (y * dst_stride) as usize; + + for w in 0..dst_stride { + let s1 = collective_bitmap.data[src_row + w as usize]; + let s2 = collective_bitmap + .data + .get(src_row + w as usize + 1) + .copied() + .unwrap_or(0); + + let word: Word = if src_bit_offset == 0 { + s1 + } else { + (s1 << src_bit_offset) | (s2 >> (WORD_BITS - src_bit_offset)) + }; + + pattern.data[dst_row + w as usize] = word; + } + + // Clear trailing bits in the last word beyond `pattern_width`. + let trailing = pattern_width % WORD_BITS; + if trailing != 0 { + let last = dst_row + (dst_stride - 1) as usize; + pattern.data[last] &= !0 << (WORD_BITS - trailing); + } + } + + patterns.push(pattern); + } + + // It turns out that when rendering patterns, the `Bitmap::combine` operation + // can often be a huge bottleneck. This is because we need to do a lot of + // fiddling to compose the patterns in the right position. And since + // if a page uses such patterns, there's going to be a lot of them, + // this takes up a lot of time. + // + // The key insight for our optimization is that patterns are usually + // very small. Therefore, we precompute a new version of each pattern + // for all possible 32-bit alignments, which later on allows us to + // very easily blit it into the backdrop, significantly speeding up + // the whole process. + let shifted_patterns = if pattern_width <= WORD_BITS && pattern_height <= WORD_BITS { + let h = pattern_height as usize; + let mut data = Vec::with_capacity(patterns.len() * 32 * h * 2); + for pattern in &patterns { + for k in 0..32_u32 { + for y in 0..h { + let word = pattern.data[y]; + if k == 0 { + data.push(word); + data.push(0); + } else { + data.push(word >> k); + data.push(word << (WORD_BITS - k)); + } + } + } + } + data + } else { + Vec::new() + }; + + Ok(PatternDictionary { + patterns, + pattern_width, + pattern_height, + shifted_patterns, + }) +} + +/// A decoded pattern dictionary. +#[derive(Debug, Clone)] +pub(crate) struct PatternDictionary { + pub(crate) patterns: Vec, + pub(crate) pattern_width: u32, + pub(crate) pattern_height: u32, + shifted_patterns: Vec, +} + +impl PatternDictionary { + #[inline(always)] + pub(crate) fn shifted_pattern(&self, pattern_idx: usize, bit_offset: u32) -> Option<&[Word]> { + let h = self.pattern_height as usize; + let len = h * 2; + let start = (pattern_idx * 32 + bit_offset as usize) * len; + self.shifted_patterns.get(start..start + len) + } +} + +/// Parsed pattern dictionary segment header (7.4.4.1). +#[derive(Debug, Clone)] +pub(crate) struct PatternDictionaryHeader<'a> { + pub(crate) mmr: bool, + pub(crate) template: Template, + /// `HDPW` + pub(crate) pattern_width: u8, + /// `HDPH` + pub(crate) pattern_height: u8, + /// `GRAYMAX` + pub(crate) num_patterns: u32, + pub(crate) data: &'a [u8], +} + +/// Parse a pattern dictionary segment header (7.4.4.1). +pub(crate) fn parse<'a>(reader: &mut Reader<'a>) -> Result> { + let flags_byte = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + let mmr = flags_byte & 0x01 != 0; + let template = Template::from_byte(flags_byte >> 1); + let pattern_width = reader + .read_nonzero_byte() + .ok_or(ParseError::UnexpectedEof)?; + let pattern_height = reader + .read_nonzero_byte() + .ok_or(ParseError::UnexpectedEof)?; + let num_patterns = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + let data = reader.tail().ok_or(ParseError::UnexpectedEof)?; + + Ok(PatternDictionaryHeader { + mmr, + template, + pattern_width, + pattern_height, + num_patterns, + data, + }) +} diff --git a/vendor/hayro-jbig2/src/decode/symbol.rs b/vendor/hayro-jbig2/src/decode/symbol.rs new file mode 100644 index 00000000..f1aa8a66 --- /dev/null +++ b/vendor/hayro-jbig2/src/decode/symbol.rs @@ -0,0 +1,866 @@ +//! Symbol dictionary segment parsing and decoding (7.4.2, 6.5). + +use alloc::vec; +use alloc::vec::Vec; + +use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext}; +use crate::bitmap::{Bitmap, WORD_BITS}; +use crate::decode::generic::{decode_bitmap_mmr, parse_adaptive_template_pixels}; +use crate::decode::text::{ + DecodeContext, ReferenceCorner, TextRegionContexts, TextRegionFlags, TextRegionHeader, + TextRegionHuffmanFlags, decode_with, +}; +use crate::decode::{ + AdaptiveTemplatePixel, CombinationOperator, RefinementTemplate, RegionSegmentInfo, Template, + parse_refinement_at_pixels, +}; +use crate::decode::{generic, generic_refinement}; +use crate::error::{ + DecodeError, HuffmanError, OverflowError, ParseError, Result, SymbolError, bail, +}; +use crate::huffman_table::{HuffmanTable, StandardHuffmanTables}; +use crate::integer_decoder::IntegerDecoder; +use crate::reader::Reader; + +const MAX_SYMBOLS: u32 = u16::MAX as u32; + +/// PdfCraft patch: the most symbol pixels one dictionary may decode (2^26). Symbols are +/// decoded pixel by pixel, and up to 65535 of them of up to 65535 ร— 65535 each kept a fuzzed +/// file decoding for minutes; real dictionaries hold about a million pixels. +const MAX_DICTIONARY_PIXELS: u64 = 1 << 26; + +/// PdfCraft patch: decoding steps that decode nothing, allowed beyond what a valid dictionary can +/// need. A height class without a symbol and an export run of length 0 don't move their loops +/// on, and past the end of its data the arithmetic decoder can decode them for ever: one changed +/// byte in a symbol dictionary hung the renderer. A valid dictionary needs at most one height +/// class per new symbol, and `2 ร— symbols + 1` export runs (they alternate between exported and +/// not, so at most one zero-length run before each symbol's). Encoders don't write empty steps +/// back to back; this tolerates a thousand. +const EXTRA_EMPTY_STEPS: u64 = 1024; + +/// Decode a symbol dictionary segment (7.4.2, 6.5). +pub(crate) fn decode( + header: &SymbolDictionaryHeader<'_>, + input_symbols: &[&Bitmap], + referred_tables: &[HuffmanTable], + standard_tables: &StandardHuffmanTables, + input_contexts: Option<&RetainedContexts>, +) -> Result { + let num_new_symbols = header.num_new_symbols; + + let mut ctx = SymbolDecodeContext { + a_ctx: ArithmeticContext::new(header.data, header, input_contexts)?, + h_ctx: HuffmanContext::new(header.data, header, referred_tables, standard_tables)?, + symbols: Symbols::new(input_symbols, num_new_symbols as usize), + symbol_widths: Vec::with_capacity(num_new_symbols as usize), + height_class_first_symbol: 0, + symbols_decoded_count: 0, + total_width: 0, + height_class_height: 0, + pixels: 0, + header, + standard_tables, + }; + + let read_height_class_delta = |ctx: &mut SymbolDecodeContext<'_>| { + if ctx.header.flags.use_huffman { + ctx.h_ctx + .height_class_delta_table + .decode(&mut ctx.h_ctx.reader) + } else { + Ok(ctx + .a_ctx + .height_class_delta_decoder + .decode(&mut ctx.a_ctx.decoder)) + } + }; + + let read_symbol_width_delta = |ctx: &mut SymbolDecodeContext<'_>| { + if ctx.header.flags.use_huffman { + ctx.h_ctx + .symbol_width_delta_table + .decode(&mut ctx.h_ctx.reader) + } else { + Ok(ctx + .a_ctx + .symbol_width_delta_decoder + .decode(&mut ctx.a_ctx.decoder)) + } + }; + + // PdfCraft patch: see `EXTRA_EMPTY_STEPS`. + let mut height_classes_left = u64::from(num_new_symbols) + EXTRA_EMPTY_STEPS; + + while ctx.symbols_decoded_count < num_new_symbols { + if height_classes_left == 0 { + bail!(SymbolError::Invalid); + } + height_classes_left -= 1; + let height_class_delta = + read_height_class_delta(&mut ctx)?.ok_or(SymbolError::UnexpectedOob)?; + + ctx.height_class_height = ctx + .height_class_height + .checked_add_signed(height_class_delta) + .ok_or(OverflowError::BitmapDimension)?; + + let mut symbol_width: u32 = 0; + ctx.total_width = 0; + ctx.height_class_first_symbol = ctx.symbols_decoded_count; + + // "If the result of this decoding is OOB then all the symbols + // in this height class have been decoded." + while let Some(width_delta) = read_symbol_width_delta(&mut ctx)? { + // Prevent infinite loop for invalid files. + if ctx.symbols_decoded_count >= num_new_symbols { + bail!(SymbolError::TooManySymbols); + } + + symbol_width = symbol_width + .checked_add_signed(width_delta) + .ok_or(OverflowError::BitmapDimension)?; + ctx.total_width = ctx + .total_width + .checked_add(symbol_width) + .ok_or(OverflowError::BitmapDimension)?; + // PdfCraft patch: see `MAX_DICTIONARY_PIXELS`. + ctx.pixels = ctx + .pixels + .saturating_add(u64::from(symbol_width) * u64::from(ctx.height_class_height)); + if ctx.pixels > MAX_DICTIONARY_PIXELS { + bail!(OverflowError::BitmapDimension); + } + + match (ctx.header.flags.use_huffman, ctx.header.flags.use_refagg) { + (false, false) => { + // Decode a single symbol using a simple generic decoding procedure, + // described in 6.5.8.1. + let mut region = Bitmap::new(symbol_width, ctx.height_class_height)?; + generic::decode_bitmap_arithmetic_coding( + &mut region, + &mut ctx.a_ctx.decoder, + &mut ctx.a_ctx.generic_region_contexts, + ctx.header.flags.template, + false, + &ctx.header.adaptive_template_pixels, + )?; + + ctx.symbols.new.push(region); + } + (true, false) => { + // Decode a single symbol width. We don't actually decode the symbols + // yet, those will be decoded later on from the collective bitmap. + ctx.symbol_widths.push(symbol_width); + } + (_, true) => { + // Also decode a single symbol, but using refinement-aggregation. + // In this case, we can have both, huffman and arithmetic coding. + let symbol = decode_refinement_aggregation_bitmap(&mut ctx, symbol_width)?; + + ctx.symbols.new.push(symbol); + } + } + + ctx.symbols_decoded_count += 1; + } + + if ctx.header.flags.use_huffman && !ctx.header.flags.use_refagg { + // In case we have huffman coding and no refinement-aggregation, we use + // the previously decoded symbol widths to decode the collective bitmap + // and extract the individual symbols from that bitmap. + decode_height_class_collective_bitmap(&mut ctx)?; + } + } + + let exported = export_symbols(&mut ctx)?; + + let retained_contexts = if ctx.header.flags.bitmap_context_retained { + Some(RetainedContexts { + generic_region: ctx.a_ctx.generic_region_contexts, + refinement_region: ctx.a_ctx.refinement_region_contexts, + }) + } else { + None + }; + + Ok(SymbolDictionary { + exported_symbols: exported, + retained_contexts, + }) +} + +#[derive(Debug, Clone)] +pub(crate) struct RetainedContexts { + pub(crate) generic_region: Vec, + pub(crate) refinement_region: Vec, +} + +/// A decoded symbol dictionary segment. +#[derive(Debug, Clone)] +pub(crate) struct SymbolDictionary { + pub(crate) exported_symbols: Vec, + pub(crate) retained_contexts: Option, +} + +/// Decode a symbol bitmap using refinement/aggregate coding (6.5.8.2). +fn decode_refinement_aggregation_bitmap( + ctx: &mut SymbolDecodeContext<'_>, + symbol_width: u32, +) -> Result { + // 6.5.8.2.1 Number of symbol instances in the aggregation. + let aggregation_instance_count = if ctx.header.flags.use_huffman { + ctx.h_ctx + .aggregation_instance_count_table + .decode(&mut ctx.h_ctx.reader)? + } else { + ctx.a_ctx + .aggregation_instance_count_decoder + .decode(&mut ctx.a_ctx.decoder) + } + .ok_or(DecodeError::Symbol(SymbolError::UnexpectedOob))?; + + if aggregation_instance_count == 1 { + decode_refinement_bitmap(ctx, symbol_width) + } else if aggregation_instance_count > 1 { + decode_aggregation_bitmap(ctx, symbol_width, aggregation_instance_count as u32) + } else { + Err(DecodeError::Symbol(SymbolError::Invalid)) + } +} + +/// Decode a refinement bitmap symbol with a single aggregate (6.5.8.2). +fn decode_refinement_bitmap( + ctx: &mut SymbolDecodeContext<'_>, + symbol_width: u32, +) -> Result { + let use_huffman = ctx.header.flags.use_huffman; + let mut symbol_code_length = 32 - (ctx.total_symbols() - 1).leading_zeros(); + + let (symbol_id, refinement_x_offset, refinement_y_offset) = if use_huffman { + // See 6.5.8.2.3, the value should be at least 1 if we use huffman coding. + symbol_code_length = symbol_code_length.max(1); + + let symbol_id = ctx + .h_ctx + .reader + .read_bits(symbol_code_length as u8) + .ok_or(ParseError::UnexpectedEof)? as usize; + + let refinement_x_offset = ctx + .standard_tables + .table_o() + .decode(&mut ctx.h_ctx.reader)? + .ok_or(HuffmanError::UnexpectedOob)?; + + let refinement_y_offset = ctx + .standard_tables + .table_o() + .decode(&mut ctx.h_ctx.reader)? + .ok_or(HuffmanError::UnexpectedOob)?; + + (symbol_id, refinement_x_offset, refinement_y_offset) + } else { + // Note that the contexts should be reused across multiple + // bitmaps in the same symbol dictionary. + let contexts = ctx + .a_ctx + .text_region_contexts + .get_or_insert_with(|| TextRegionContexts::new(symbol_code_length)); + + let symbol_id = contexts.iaid.decode(&mut ctx.a_ctx.decoder) as usize; + + let refinement_x_offset = contexts + .iardx + .decode(&mut ctx.a_ctx.decoder) + .ok_or(SymbolError::UnexpectedOob)?; + + let refinement_y_offset = contexts + .iardy + .decode(&mut ctx.a_ctx.decoder) + .ok_or(SymbolError::UnexpectedOob)?; + + (symbol_id, refinement_x_offset, refinement_y_offset) + }; + + let reference_region = ctx.symbols.get(symbol_id).ok_or(SymbolError::OutOfRange)?; + let mut region = Bitmap::new(symbol_width, ctx.height_class_height)?; + + if use_huffman { + let bitmap_size = ctx + .standard_tables + .table_a() + .decode(&mut ctx.h_ctx.reader)? + .ok_or(HuffmanError::UnexpectedOob)? as usize; + ctx.h_ctx.reader.align(); + + let bitmap_data = ctx + .h_ctx + .reader + .read_bytes(bitmap_size) + .ok_or(ParseError::UnexpectedEof)?; + + let mut bitmap_decoder = ArithmeticDecoder::new(bitmap_data); + + generic_refinement::decode_bitmap( + &mut bitmap_decoder, + &mut ctx.a_ctx.refinement_region_contexts, + &mut region, + reference_region, + refinement_x_offset, + refinement_y_offset, + ctx.header.flags.refinement_template, + &ctx.header.refinement_at_pixels, + false, + )?; + } else { + generic_refinement::decode_bitmap( + &mut ctx.a_ctx.decoder, + &mut ctx.a_ctx.refinement_region_contexts, + &mut region, + reference_region, + refinement_x_offset, + refinement_y_offset, + ctx.header.flags.refinement_template, + &ctx.header.refinement_at_pixels, + false, + )?; + } + + Ok(region) +} + +/// Decode an aggregation bitmap with more than one aggregate (6.5.8.2). +fn decode_aggregation_bitmap( + ctx: &mut SymbolDecodeContext<'_>, + symbol_width: u32, + aggregation_instance_count: u32, +) -> Result { + let use_huffman = ctx.header.flags.use_huffman; + + // Concatenate input and new symbols. + let mut all_symbols: Vec<&Bitmap> = + Vec::with_capacity(ctx.symbols.input.len() + ctx.symbols.new.len()); + all_symbols.extend(ctx.symbols.input.iter().copied()); + for sym in &ctx.symbols.new { + all_symbols.push(sym); + } + + // Set all parameters according to Table 17. + + let symbol_code_length = 32 - (ctx.total_symbols() - 1).leading_zeros(); + + let symbol_id_table = if use_huffman { + Some(HuffmanTable::build_uniform( + ctx.total_symbols(), + symbol_code_length, + )?) + } else { + None + }; + + let huffman_flags = if use_huffman { + Some(TextRegionHuffmanFlags { + first_s_table: 0, + delta_s_table: 0, + delta_t_table: 0, + refinement_width_table: 1, + refinement_height_table: 1, + refinement_y_table: 1, + refinement_x_table: 1, + refinement_size_table: 0, + }) + } else { + None + }; + + let header = TextRegionHeader { + region_info: RegionSegmentInfo { + width: symbol_width, + height: ctx.height_class_height, + x_location: 0, + y_location: 0, + combination_operator: CombinationOperator::Or, + _colour_extension: false, + }, + flags: TextRegionFlags { + use_huffman, + use_refinement: true, + log_strip_size: 0, + reference_corner: ReferenceCorner::TopLeft, + transposed: false, + combination_operator: CombinationOperator::Or, + default_pixel: false, + delta_s_offset: 0, + refinement_template: ctx.header.flags.refinement_template, + }, + huffman_flags, + refinement_at_pixels: ctx.header.refinement_at_pixels.clone(), + num_instances: aggregation_instance_count, + symbol_id_table, + data: &[], // Data comes from parent decoder context, not the header. + }; + + let decode_ctx = if use_huffman { + DecodeContext::new_huffman(&mut ctx.h_ctx.reader, &header, &[], ctx.standard_tables)? + } else { + let contexts = ctx + .a_ctx + .text_region_contexts + .get_or_insert_with(|| TextRegionContexts::new(symbol_code_length)); + + DecodeContext::new_arithmetic( + &mut ctx.a_ctx.decoder, + contexts, + &mut ctx.a_ctx.refinement_region_contexts, + ) + }; + + let mut bitmap = Bitmap::new_with( + header.region_info.width, + header.region_info.height, + header.region_info.x_location, + header.region_info.y_location, + header.flags.default_pixel, + )?; + decode_with(decode_ctx, &all_symbols, &header, &mut bitmap)?; + Ok(bitmap) +} + +struct Symbols<'a> { + input: &'a [&'a Bitmap], + new: Vec, +} + +impl<'a> Symbols<'a> { + fn new(input: &'a [&'a Bitmap], capacity: usize) -> Self { + Self { + input, + new: Vec::with_capacity(capacity), + } + } + + fn input_count(&self) -> u32 { + self.input.len() as u32 + } + + fn get(&self, index: usize) -> Option<&Bitmap> { + if index < self.input.len() { + Some(self.input[index]) + } else { + self.new.get(index - self.input.len()) + } + } +} + +struct SymbolDecodeContext<'a> { + header: &'a SymbolDictionaryHeader<'a>, + a_ctx: ArithmeticContext<'a>, + h_ctx: HuffmanContext<'a>, + symbols: Symbols<'a>, + standard_tables: &'a StandardHuffmanTables, + symbol_widths: Vec, + height_class_first_symbol: u32, + symbols_decoded_count: u32, + total_width: u32, + height_class_height: u32, + /// PdfCraft patch: symbol pixels decoded so far (see `MAX_DICTIONARY_PIXELS`). + pixels: u64, +} + +impl SymbolDecodeContext<'_> { + fn total_symbols(&self) -> u32 { + self.symbols.input_count() + self.header.num_new_symbols + } +} + +struct ArithmeticContext<'a> { + decoder: ArithmeticDecoder<'a>, + /// `IADH` + height_class_delta_decoder: IntegerDecoder, + /// `IADW` + symbol_width_delta_decoder: IntegerDecoder, + /// `IAEX` + export_run_length_decoder: IntegerDecoder, + /// `IAAI` + aggregation_instance_count_decoder: IntegerDecoder, + generic_region_contexts: Vec, + refinement_region_contexts: Vec, + text_region_contexts: Option, +} + +impl<'a> ArithmeticContext<'a> { + fn new( + data: &'a [u8], + header: &SymbolDictionaryHeader<'_>, + input_contexts: Option<&RetainedContexts>, + ) -> Result { + let decoder = ArithmeticDecoder::new(data); + + let height_class_delta_decoder = IntegerDecoder::new(); + let symbol_width_delta_decoder = IntegerDecoder::new(); + let export_run_length_decoder = IntegerDecoder::new(); + let aggregation_instance_count_decoder = IntegerDecoder::new(); + + let (generic_region_contexts, refinement_region_contexts) = + if header.flags.bitmap_context_used { + let ctx = input_contexts.ok_or(SymbolError::Invalid)?; + (ctx.generic_region.clone(), ctx.refinement_region.clone()) + } else { + let template = header.flags.template; + let num_contexts = 1 << template.context_bits(); + let refinement_template = header.flags.refinement_template; + let num_refinement_contexts = 1 << refinement_template.context_bits(); + ( + vec![ArithmeticDecoderContext::default(); num_contexts], + vec![ArithmeticDecoderContext::default(); num_refinement_contexts], + ) + }; + + Ok(Self { + decoder, + height_class_delta_decoder, + symbol_width_delta_decoder, + export_run_length_decoder, + aggregation_instance_count_decoder, + generic_region_contexts, + refinement_region_contexts, + text_region_contexts: None, + }) + } +} + +struct HuffmanContext<'a> { + /// `SDHUFFDH` + height_class_delta_table: &'a HuffmanTable, + /// `SDHUFFDW` + symbol_width_delta_table: &'a HuffmanTable, + /// `SDHUFFBMSIZE` + collective_bitmap_size_table: &'a HuffmanTable, + /// `SDHUFFAGGINST` + aggregation_instance_count_table: &'a HuffmanTable, + export_run_length_table: &'a HuffmanTable, + reader: Reader<'a>, +} + +impl<'a> HuffmanContext<'a> { + fn new( + data: &'a [u8], + header: &SymbolDictionaryHeader<'_>, + referred_tables: &'a [HuffmanTable], + standard_tables: &'a StandardHuffmanTables, + ) -> Result { + let reader = Reader::new(data); + + let custom_count = [ + header.flags.delta_height_table == HuffmanTableSelection::UserSupplied, + header.flags.delta_width_table == HuffmanTableSelection::UserSupplied, + header.flags.collective_bitmap_size_table == HuffmanTableSelection::UserSupplied, + header.flags.aggregate_instance_table == HuffmanTableSelection::UserSupplied, + ] + .into_iter() + .filter(|x| *x) + .count(); + + if referred_tables.len() < custom_count { + bail!(HuffmanError::MissingTables); + } + + let mut custom_table_idx = 0; + let mut get_custom = || -> &HuffmanTable { + let table = &referred_tables[custom_table_idx]; + custom_table_idx += 1; + table + }; + + // Select Huffman tables based on flags (7.4.2.1.6). + let mut get_table = |selection: HuffmanTableSelection| -> &HuffmanTable { + match selection { + HuffmanTableSelection::TableB1 => standard_tables.table_a(), + HuffmanTableSelection::TableB2 => standard_tables.table_b(), + HuffmanTableSelection::TableB3 => standard_tables.table_c(), + HuffmanTableSelection::TableB4 => standard_tables.table_d(), + HuffmanTableSelection::TableB5 => standard_tables.table_e(), + HuffmanTableSelection::UserSupplied => get_custom(), + } + }; + + let height_class_delta_table = get_table(header.flags.delta_height_table); + let symbol_width_delta_table = get_table(header.flags.delta_width_table); + let collective_bitmap_size_table = get_table(header.flags.collective_bitmap_size_table); + let aggregation_instance_count_table = get_table(header.flags.aggregate_instance_table); + let export_run_length_table = get_table(HuffmanTableSelection::TableB1); + + Ok(Self { + reader, + height_class_delta_table, + symbol_width_delta_table, + collective_bitmap_size_table, + aggregation_instance_count_table, + export_run_length_table, + }) + } +} + +/// Decode a height class collective bitmap (6.5.9). +fn decode_height_class_collective_bitmap(ctx: &mut SymbolDecodeContext<'_>) -> Result<()> { + let bitmap_size = ctx + .h_ctx + .collective_bitmap_size_table + .decode(&mut ctx.h_ctx.reader)? + .ok_or(HuffmanError::UnexpectedOob)?; + let bitmap_size = u32::try_from(bitmap_size).map_err(|_| OverflowError::BitmapDimension)?; + + ctx.h_ctx.reader.align(); + + let collective_bitmap = if bitmap_size == 0 { + // Bitmap is stored uncompressed, so we basically just copy-paste + // each byte. Rows are padded to the byte boundary. + let row_bytes = ctx.total_width.div_ceil(8); + + let mut bitmap = Bitmap::new(ctx.total_width, ctx.height_class_height)?; + for y in 0..ctx.height_class_height { + for byte_x in 0..row_bytes { + let byte = ctx + .h_ctx + .reader + .read_byte() + .ok_or(ParseError::UnexpectedEof)?; + for bit in 0..8 { + let x = byte_x * 8 + bit; + if x < ctx.total_width { + let pixel = (byte >> (7 - bit)) & 1; + bitmap.set_pixel(x, y, pixel); + } + } + } + } + bitmap + } else { + // Otherwise, we need to use MMR decoding. + let bitmap_data = ctx + .h_ctx + .reader + .read_bytes(bitmap_size as usize) + .ok_or(ParseError::UnexpectedEof)?; + + let mut bitmap = Bitmap::new(ctx.total_width, ctx.height_class_height)?; + decode_bitmap_mmr(&mut bitmap, bitmap_data)?; + bitmap + }; + + // Finally, we simply chop up the collective bitmap into its constituent + // symbols. + let mut x_offset: u32 = 0; + for symbol_idx in ctx.height_class_first_symbol..ctx.symbols_decoded_count { + let symbol_width = ctx.symbol_widths[symbol_idx as usize]; + let mut symbol = Bitmap::new(symbol_width, ctx.height_class_height)?; + + for y in 0..ctx.height_class_height { + let src_bit_offset = x_offset % WORD_BITS; + let src_word_start = x_offset / WORD_BITS; + let dest_row = (y * symbol.stride) as usize; + + for w in 0..symbol.stride { + let sw = src_word_start + w; + let src1 = collective_bitmap.get_word(y, sw); + let word = if src_bit_offset == 0 { + src1 + } else { + let src2 = collective_bitmap.get_word(y, sw + 1); + (src1 << src_bit_offset) | (src2 >> (WORD_BITS - src_bit_offset)) + }; + symbol.data[dest_row + w as usize] = word; + } + + // Clear trailing bits in the last word beyond `symbol_width`. + let trailing = symbol_width % WORD_BITS; + if trailing != 0 && symbol.stride > 0 { + let last = dest_row + (symbol.stride - 1) as usize; + symbol.data[last] &= !0 << (WORD_BITS - trailing); + } + } + + ctx.symbols.new.push(symbol); + x_offset = x_offset + .checked_add(symbol_width) + .ok_or(OverflowError::Index)?; + } + + Ok(()) +} + +/// Exported symbols (6.5.10). +fn export_symbols(ctx: &mut SymbolDecodeContext<'_>) -> Result> { + let total_symbols = ctx.total_symbols(); + + let mut read_run_length = || -> Result { + let value = if ctx.header.flags.use_huffman { + ctx.h_ctx + .export_run_length_table + .decode(&mut ctx.h_ctx.reader)? + } else { + ctx.a_ctx + .export_run_length_decoder + .decode(&mut ctx.a_ctx.decoder) + } + .ok_or(HuffmanError::UnexpectedOob)?; + + u32::try_from(value).map_err(|_| HuffmanError::InvalidCode.into()) + }; + let mut exported = Vec::with_capacity(ctx.header.num_exported_symbols as usize); + let mut index: u32 = 0; + let mut should_export = false; + // PdfCraft patch: see `EXTRA_EMPTY_STEPS`. + let mut runs_left = u64::from(total_symbols) * 2 + 1 + EXTRA_EMPTY_STEPS; + + while index < total_symbols { + if runs_left == 0 { + bail!(SymbolError::Invalid); + } + runs_left -= 1; + let run_length = read_run_length()?; + + let end_index = index.checked_add(run_length).ok_or(OverflowError::Index)?; + if end_index > total_symbols { + bail!(SymbolError::OutOfRange); + } + + if should_export { + for symbol_idx in index..end_index { + let symbol = ctx + .symbols + .get(symbol_idx as usize) + .ok_or(SymbolError::OutOfRange)? + .clone(); + exported.push(symbol); + } + } + + index = end_index; + should_export = !should_export; + } + + if exported.len() != ctx.header.num_exported_symbols as usize { + bail!(SymbolError::NoSymbols); + } + + Ok(exported) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum HuffmanTableSelection { + TableB1, + TableB2, + TableB3, + TableB4, + TableB5, + UserSupplied, +} + +/// Parsed symbol dictionary segment flags (7.4.2.1.1). +#[derive(Debug, Clone)] +pub(crate) struct SymbolDictionaryFlags { + pub(crate) use_huffman: bool, + pub(crate) use_refagg: bool, + pub(crate) delta_height_table: HuffmanTableSelection, + pub(crate) delta_width_table: HuffmanTableSelection, + pub(crate) collective_bitmap_size_table: HuffmanTableSelection, + pub(crate) aggregate_instance_table: HuffmanTableSelection, + pub(crate) bitmap_context_used: bool, + pub(crate) bitmap_context_retained: bool, + pub(crate) template: Template, + pub(crate) refinement_template: RefinementTemplate, +} + +/// Parsed symbol dictionary segment header (7.4.2.1). +#[derive(Debug, Clone)] +pub(crate) struct SymbolDictionaryHeader<'a> { + pub(crate) flags: SymbolDictionaryFlags, + pub(crate) adaptive_template_pixels: [AdaptiveTemplatePixel; 4], + pub(crate) refinement_at_pixels: Vec, + pub(crate) num_exported_symbols: u32, + pub(crate) num_new_symbols: u32, + pub(crate) data: &'a [u8], +} + +/// Parse a symbol dictionary segment header (7.4.2.1). +pub(crate) fn parse<'a>(reader: &mut Reader<'a>) -> Result> { + let flags_word = reader.read_u16().ok_or(ParseError::UnexpectedEof)?; + let use_huffman = flags_word & 0x0001 != 0; + let use_refagg = flags_word & 0x0002 != 0; + + let delta_height_table = match (flags_word >> 2) & 0x03 { + 0 => HuffmanTableSelection::TableB4, + 1 => HuffmanTableSelection::TableB5, + 3 => HuffmanTableSelection::UserSupplied, + _ => bail!(HuffmanError::InvalidSelection), + }; + + let delta_width_table = match (flags_word >> 4) & 0x03 { + 0 => HuffmanTableSelection::TableB2, + 1 => HuffmanTableSelection::TableB3, + 3 => HuffmanTableSelection::UserSupplied, + _ => bail!(HuffmanError::InvalidSelection), + }; + + let collective_bitmap_size_table = if flags_word & 0x0040 != 0 { + HuffmanTableSelection::UserSupplied + } else { + HuffmanTableSelection::TableB1 + }; + + let aggregate_instance_table = if flags_word & 0x0080 != 0 { + HuffmanTableSelection::UserSupplied + } else { + HuffmanTableSelection::TableB1 + }; + + let bitmap_context_used = flags_word & 0x0100 != 0; + let bitmap_context_retained = flags_word & 0x0200 != 0; + let template = Template::from_byte((flags_word >> 10) as u8); + let refinement_template = RefinementTemplate::from_byte((flags_word >> 12) as u8); + + let flags = SymbolDictionaryFlags { + use_huffman, + use_refagg, + delta_height_table, + delta_width_table, + collective_bitmap_size_table, + aggregate_instance_table, + bitmap_context_used, + bitmap_context_retained, + template, + refinement_template, + }; + + let at_pixels = if !use_huffman { + parse_adaptive_template_pixels(reader, template, false)? + } else { + // Unused. + [AdaptiveTemplatePixel::default(); 4] + }; + + let refinement_at_pixels = if use_refagg && refinement_template == RefinementTemplate::Template0 + { + parse_refinement_at_pixels(reader)? + } else { + Vec::new() + }; + let num_exported_symbols = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + let num_new_symbols = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + + if num_exported_symbols.saturating_add(num_new_symbols) > MAX_SYMBOLS { + bail!(OverflowError::SymbolCount) + } + + let data = reader.tail().ok_or(ParseError::UnexpectedEof)?; + + Ok(SymbolDictionaryHeader { + flags, + adaptive_template_pixels: at_pixels, + refinement_at_pixels, + num_exported_symbols, + num_new_symbols, + data, + }) +} diff --git a/vendor/hayro-jbig2/src/decode/text.rs b/vendor/hayro-jbig2/src/decode/text.rs new file mode 100644 index 00000000..bbc2ac55 --- /dev/null +++ b/vendor/hayro-jbig2/src/decode/text.rs @@ -0,0 +1,903 @@ +//! Text region segment parsing and decoding (7.4.3, 6.4). + +use alloc::vec; +use alloc::vec::Vec; +use core::iter; + +use super::{ + AdaptiveTemplatePixel, CombinationOperator, RefinementTemplate, RegionSegmentInfo, + parse_refinement_at_pixels, parse_region_segment_info, +}; +use super::{RegionBitmap, generic_refinement}; +use crate::ScratchBuffers; +use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext}; +use crate::bitmap::Bitmap; +use crate::error::{HuffmanError, OverflowError, ParseError, Result, SymbolError, bail}; +use crate::huffman_table::{HuffmanTable, StandardHuffmanTables, TableLine}; +use crate::integer_decoder::IntegerDecoder; +use crate::reader::Reader; +use crate::symbol_id_decoder::SymbolIdDecoder; + +/// Decode a text region segment (6.4). +pub(crate) fn decode( + header: &TextRegionHeader<'_>, + symbols: &[&Bitmap], + referred_tables: &[HuffmanTable], + standard_tables: &StandardHuffmanTables, + scratch: &mut ScratchBuffers, +) -> Result { + let mut bitmap = Bitmap::new_with( + header.region_info.width, + header.region_info.height, + header.region_info.x_location, + header.region_info.y_location, + header.flags.default_pixel, + )?; + + decode_into( + header, + symbols, + referred_tables, + standard_tables, + &mut bitmap, + scratch, + )?; + + Ok(RegionBitmap { + bitmap, + combination_operator: header.region_info.combination_operator, + }) +} + +pub(crate) fn decode_into( + header: &TextRegionHeader<'_>, + symbols: &[&Bitmap], + referred_tables: &[HuffmanTable], + standard_tables: &StandardHuffmanTables, + bitmap: &mut Bitmap, + scratch: &mut ScratchBuffers, +) -> Result<()> { + if header.flags.use_huffman { + let mut reader = Reader::new(header.data); + let ctx = + DecodeContext::new_huffman(&mut reader, header, referred_tables, standard_tables)?; + decode_with(ctx, symbols, header, bitmap)?; + } else { + let mut decoder = ArithmeticDecoder::new(header.data); + + let num_symbols = symbols.len() as u32; + let symbol_code_length = 32 - num_symbols.saturating_sub(1).leading_zeros(); + let mut contexts = TextRegionContexts::new(symbol_code_length); + + let num_gr_contexts = 1 << header.flags.refinement_template.context_bits(); + scratch.contexts.clear(); + scratch + .contexts + .resize(num_gr_contexts, ArithmeticDecoderContext::default()); + + let ctx = DecodeContext::new_arithmetic(&mut decoder, &mut contexts, &mut scratch.contexts); + decode_with(ctx, symbols, header, bitmap)?; + } + + Ok(()) +} + +/// Decode a text region segment with a decode context (6.4). +pub(crate) fn decode_with( + mut ctx: DecodeContext<'_, '_>, + symbols: &[&Bitmap], + header: &TextRegionHeader<'_>, + region: &mut Bitmap, +) -> Result<()> { + let strip_size = header.strip_size(); + + // Arbitrarily chosen, but we need some limit to prevent timeouts. + const MAX_INSTANCES: u32 = 10_000; + + if header.num_instances > MAX_INSTANCES { + bail!(SymbolError::TooManyInstances); + } + + let mut strip_t = ctx + .read_strip_delta_t(strip_size)? + .checked_neg() + .ok_or(OverflowError::PlacementCoordinate)?; + let mut first_s: i32 = 0; + let mut instance_count = 0; + + while instance_count < header.num_instances { + let delta_t = ctx.read_strip_delta_t(strip_size)?; + strip_t = strip_t + .checked_add(delta_t) + .ok_or(OverflowError::PlacementCoordinate)?; + + let mut first_symbol_in_strip = true; + let mut current_s = 0; + + loop { + // Prevent infinite loop for invalid files. + if instance_count > header.num_instances { + bail!(SymbolError::TooManySymbols); + } + + if first_symbol_in_strip { + let delta_first_s = ctx.read_first_s()?; + first_s = first_s + .checked_add(delta_first_s) + .ok_or(OverflowError::PlacementCoordinate)?; + current_s = first_s; + first_symbol_in_strip = false; + } else { + let Some(delta_s) = ctx.read_delta_s()? else { + // OOB - end of strip. + break; + }; + + current_s = current_s + .checked_add(delta_s) + .and_then(|v| v.checked_add(header.flags.delta_s_offset as i32)) + .ok_or(OverflowError::PlacementCoordinate)?; + } + + let current_t = ctx.read_symbol_t(strip_size, header.flags.log_strip_size)?; + let symbol_t = strip_t + .checked_add(current_t) + .ok_or(OverflowError::PlacementCoordinate)?; + + let symbol_id = ctx.read_symbol_id()?; + + let symbol_bitmap = + decode_symbol_instance_bitmap(&mut ctx, symbols, header, symbol_id)?; + + let symbol_bitmap_ref: &Bitmap = match &symbol_bitmap { + SymbolBitmap::Reference(idx) => symbols.get(*idx).ok_or(SymbolError::OutOfRange)?, + SymbolBitmap::Owned(bitmap) => bitmap, + }; + let symbol_width = symbol_bitmap_ref.width as i32; + let symbol_height = symbol_bitmap_ref.height as i32; + + if !header.flags.transposed + && (header.flags.reference_corner == ReferenceCorner::TopRight + || header.flags.reference_corner == ReferenceCorner::BottomRight) + { + current_s = current_s + .checked_add(symbol_width - 1) + .ok_or(OverflowError::PlacementCoordinate)?; + } else if header.flags.transposed + && (header.flags.reference_corner == ReferenceCorner::BottomLeft + || header.flags.reference_corner == ReferenceCorner::BottomRight) + { + current_s = current_s + .checked_add(symbol_height - 1) + .ok_or(OverflowError::PlacementCoordinate)?; + } + + let symbol_s = current_s; + + let (x, y) = if !header.flags.transposed { + match header.flags.reference_corner { + ReferenceCorner::TopLeft => (symbol_s, symbol_t), + ReferenceCorner::TopRight => (symbol_s - symbol_width + 1, symbol_t), + ReferenceCorner::BottomLeft => (symbol_s, symbol_t - symbol_height + 1), + ReferenceCorner::BottomRight => { + (symbol_s - symbol_width + 1, symbol_t - symbol_height + 1) + } + } + } else { + match header.flags.reference_corner { + ReferenceCorner::TopLeft => (symbol_t, symbol_s), + ReferenceCorner::TopRight => (symbol_t - symbol_width + 1, symbol_s), + ReferenceCorner::BottomLeft => (symbol_t, symbol_s - symbol_height + 1), + ReferenceCorner::BottomRight => { + (symbol_t - symbol_width + 1, symbol_s - symbol_height + 1) + } + } + }; + + region.combine(symbol_bitmap_ref, x, y, header.flags.combination_operator); + + if !header.flags.transposed + && (header.flags.reference_corner == ReferenceCorner::TopLeft + || header.flags.reference_corner == ReferenceCorner::BottomLeft) + { + current_s = current_s + .checked_add(symbol_width - 1) + .ok_or(OverflowError::PlacementCoordinate)?; + } else if header.flags.transposed + && (header.flags.reference_corner == ReferenceCorner::TopLeft + || header.flags.reference_corner == ReferenceCorner::TopRight) + { + current_s = current_s + .checked_add(symbol_height - 1) + .ok_or(OverflowError::PlacementCoordinate)?; + } + + instance_count += 1; + } + } + + Ok(()) +} + +/// Shared integer decoder contexts for text region decoding. +pub(crate) struct TextRegionContexts { + /// IADT: Strip delta T decoder (6.4.6) + pub(crate) iadt: IntegerDecoder, + /// IAFS: First symbol S coordinate decoder (6.4.7) + pub(crate) iafs: IntegerDecoder, + /// IADS: Subsequent symbol S coordinate decoder (6.4.8) + pub(crate) iads: IntegerDecoder, + /// IAIT: Symbol instance T coordinate decoder (6.4.9) + pub(crate) iait: IntegerDecoder, + /// IAID: Symbol ID decoder (6.4.10) + pub(crate) iaid: SymbolIdDecoder, + /// IARI: Refinement image indicator decoder (6.4.11) + pub(crate) iari: IntegerDecoder, + /// IARDW: Refinement delta width decoder (6.4.11.1) + pub(crate) iardw: IntegerDecoder, + /// IARDH: Refinement delta height decoder (6.4.11.2) + pub(crate) iardh: IntegerDecoder, + /// IARDX: Refinement X offset decoder (6.4.11.3) + pub(crate) iardx: IntegerDecoder, + /// IARDY: Refinement Y offset decoder (6.4.11.4) + pub(crate) iardy: IntegerDecoder, +} + +impl TextRegionContexts { + /// Create new text region contexts with the given symbol code length. + pub(crate) fn new(symbol_code_length: u32) -> Self { + Self { + iadt: IntegerDecoder::new(), + iafs: IntegerDecoder::new(), + iads: IntegerDecoder::new(), + iait: IntegerDecoder::new(), + iaid: SymbolIdDecoder::new(symbol_code_length), + iari: IntegerDecoder::new(), + iardw: IntegerDecoder::new(), + iardh: IntegerDecoder::new(), + iardx: IntegerDecoder::new(), + iardy: IntegerDecoder::new(), + } + } +} + +pub(crate) enum DecodeContext<'a, 'b> { + Huffman { + reader: &'a mut Reader<'b>, + tables: TextRegionHuffmanTables<'a>, + symbol_codes: &'a HuffmanTable, + }, + Arithmetic { + decoder: &'a mut ArithmeticDecoder<'b>, + contexts: &'a mut TextRegionContexts, + gr_contexts: &'a mut [ArithmeticDecoderContext], + }, +} + +impl<'a, 'b> DecodeContext<'a, 'b> { + pub(crate) fn new_huffman( + reader: &'a mut Reader<'b>, + header: &'a TextRegionHeader<'_>, + referred_tables: &'a [HuffmanTable], + standard_tables: &'a StandardHuffmanTables, + ) -> Result { + let huffman_flags = header + .huffman_flags + .as_ref() + .ok_or(HuffmanError::InvalidSelection)?; + let tables = select_huffman_tables(huffman_flags, referred_tables, standard_tables)?; + let symbol_codes = header + .symbol_id_table + .as_ref() + .ok_or(HuffmanError::MissingTables)?; + + Ok(DecodeContext::Huffman { + reader, + tables, + symbol_codes, + }) + } + + pub(crate) fn new_arithmetic( + decoder: &'a mut ArithmeticDecoder<'b>, + contexts: &'a mut TextRegionContexts, + gr_contexts: &'a mut [ArithmeticDecoderContext], + ) -> Self { + DecodeContext::Arithmetic { + decoder, + contexts, + gr_contexts, + } + } + + fn read_strip_delta_t(&mut self, strip_size: u32) -> Result { + match self { + DecodeContext::Huffman { reader, tables, .. } => { + let value = tables.delta_t.decode_no_oob(reader)?; + value + .checked_mul(strip_size as i32) + .ok_or(SymbolError::InvalidStripDelta.into()) + } + DecodeContext::Arithmetic { + decoder, contexts, .. + } => { + let value = contexts + .iadt + .decode(decoder) + .ok_or(SymbolError::UnexpectedOob)?; + value + .checked_mul(strip_size as i32) + .ok_or(SymbolError::InvalidStripDelta.into()) + } + } + } + + /// Decode first symbol instance S coordinate (6.4.7). + fn read_first_s(&mut self) -> Result { + match self { + DecodeContext::Huffman { reader, tables, .. } => tables.first_s.decode_no_oob(reader), + DecodeContext::Arithmetic { + decoder, contexts, .. + } => contexts + .iafs + .decode(decoder) + .ok_or(SymbolError::UnexpectedOob.into()), + } + } + + /// Decode subsequent symbol instance S coordinate (6.4.8). + fn read_delta_s(&mut self) -> Result> { + match self { + DecodeContext::Huffman { reader, tables, .. } => tables.delta_s.decode(reader), + DecodeContext::Arithmetic { + decoder, contexts, .. + } => Ok(contexts.iads.decode(decoder)), + } + } + + /// Decode symbol instance T coordinate (6.4.9). + fn read_symbol_t(&mut self, strip_size: u32, log_strip_size: u8) -> Result { + if strip_size == 1 { + return Ok(0); + } + + match self { + DecodeContext::Huffman { reader, .. } => reader + .read_bits(log_strip_size) + .ok_or(HuffmanError::InvalidCode.into()) + .map(|v| v as i32), + DecodeContext::Arithmetic { + decoder, contexts, .. + } => contexts + .iait + .decode(decoder) + .ok_or(SymbolError::UnexpectedOob.into()), + } + } + + /// Decode symbol instance symbol ID (6.4.10). + fn read_symbol_id(&mut self) -> Result { + match self { + DecodeContext::Huffman { + reader, + symbol_codes, + .. + } => symbol_codes.decode_no_oob(reader).map(|v| v as usize), + DecodeContext::Arithmetic { + decoder, contexts, .. + } => Ok(contexts.iaid.decode(decoder) as usize), + } + } + + fn read_refinement_flag(&mut self) -> Result { + match self { + DecodeContext::Huffman { reader, .. } => { + reader.read_bit().ok_or(ParseError::UnexpectedEof.into()) + } + DecodeContext::Arithmetic { + decoder, contexts, .. + } => Ok(contexts + .iari + .decode(decoder) + .ok_or(SymbolError::UnexpectedOob)? as u8), + } + } + + /// Decode symbol instance refinement delta width (6.4.11.1). + fn read_refinement_delta_width(&mut self) -> Result { + match self { + DecodeContext::Huffman { reader, tables, .. } => { + tables.refinement_width.decode_no_oob(reader) + } + DecodeContext::Arithmetic { + decoder, contexts, .. + } => contexts + .iardw + .decode(decoder) + .ok_or(SymbolError::UnexpectedOob.into()), + } + } + + /// Decode symbol instance refinement delta height (6.4.11.2). + fn read_refinement_delta_height(&mut self) -> Result { + match self { + DecodeContext::Huffman { reader, tables, .. } => { + tables.refinement_height.decode_no_oob(reader) + } + DecodeContext::Arithmetic { + decoder, contexts, .. + } => contexts + .iardh + .decode(decoder) + .ok_or(SymbolError::UnexpectedOob.into()), + } + } + + /// Decode symbol instance refinement x offset (6.4.11.3). + fn read_refinement_x_offset(&mut self) -> Result { + match self { + DecodeContext::Huffman { reader, tables, .. } => { + tables.refinement_x.decode_no_oob(reader) + } + DecodeContext::Arithmetic { + decoder, contexts, .. + } => contexts + .iardx + .decode(decoder) + .ok_or(SymbolError::UnexpectedOob.into()), + } + } + + /// Decode symbol instance refinement y offset (6.4.11.4). + fn read_refinement_y_offset(&mut self) -> Result { + match self { + DecodeContext::Huffman { reader, tables, .. } => { + tables.refinement_y.decode_no_oob(reader) + } + DecodeContext::Arithmetic { + decoder, contexts, .. + } => contexts + .iardy + .decode(decoder) + .ok_or(SymbolError::UnexpectedOob.into()), + } + } + + /// Decode the refinement bitmap, steps 5) to 7) of 6.4.11. + fn decode_refinement_bitmap( + &mut self, + refined: &mut Bitmap, + reference_bitmap: &Bitmap, + reference_x_offset: i32, + reference_y_offset: i32, + refinement_template: RefinementTemplate, + refinement_at_pixels: &[AdaptiveTemplatePixel], + ) -> Result<()> { + match self { + DecodeContext::Huffman { reader, tables, .. } => { + let refinement_data_size = tables.refinement_size.decode_no_oob(reader)? as u32; + reader.align(); + + let refinement_data = reader + .read_bytes(refinement_data_size as usize) + .ok_or(ParseError::UnexpectedEof)?; + + let mut decoder = ArithmeticDecoder::new(refinement_data); + let num_context_bits = refinement_template.context_bits(); + let mut contexts = vec![ArithmeticDecoderContext::default(); 1 << num_context_bits]; + + generic_refinement::decode_bitmap( + &mut decoder, + &mut contexts, + refined, + reference_bitmap, + reference_x_offset, + reference_y_offset, + refinement_template, + refinement_at_pixels, + false, + ) + } + DecodeContext::Arithmetic { + decoder, + gr_contexts, + .. + } => generic_refinement::decode_bitmap( + decoder, + gr_contexts, + refined, + reference_bitmap, + reference_x_offset, + reference_y_offset, + refinement_template, + refinement_at_pixels, + false, + ), + } + } +} + +/// Result of determining a symbol instance bitmap. +enum SymbolBitmap { + /// Use the symbol at this index directly (`R_I` = 0). + Reference(usize), + /// Use this refined bitmap (`R_I` = 1). + Owned(Bitmap), +} + +/// Decode the symbol instance bitmap (6.4.11). +fn decode_symbol_instance_bitmap( + ctx: &mut DecodeContext<'_, '_>, + symbols: &[&Bitmap], + header: &TextRegionHeader<'_>, + symbol_id: usize, +) -> Result { + if !header.flags.use_refinement || ctx.read_refinement_flag()? == 0 { + return Ok(SymbolBitmap::Reference(symbol_id)); + } + + // Otherwise, the refinement flag was 1. + + let reference_bitmap = symbols.get(symbol_id).ok_or(SymbolError::OutOfRange)?; + + let rdw = ctx.read_refinement_delta_width()?; + let rdh = ctx.read_refinement_delta_height()?; + let rdx = ctx.read_refinement_x_offset()?; + let rdy = ctx.read_refinement_y_offset()?; + + let refined_width = (reference_bitmap.width as i32) + .checked_add(rdw) + .and_then(|v| u32::try_from(v).ok()) + .ok_or(OverflowError::BitmapDimension)?; + let refined_height = (reference_bitmap.height as i32) + .checked_add(rdh) + .and_then(|v| u32::try_from(v).ok()) + .ok_or(OverflowError::BitmapDimension)?; + let reference_x_offset = rdw + .div_euclid(2) + .checked_add(rdx) + .ok_or(OverflowError::ReferenceOffset)?; + let reference_y_offset = rdh + .div_euclid(2) + .checked_add(rdy) + .ok_or(OverflowError::ReferenceOffset)?; + + let mut refined_bitmap = Bitmap::new(refined_width, refined_height)?; + + ctx.decode_refinement_bitmap( + &mut refined_bitmap, + reference_bitmap, + reference_x_offset, + reference_y_offset, + header.flags.refinement_template, + &header.refinement_at_pixels, + )?; + + Ok(SymbolBitmap::Owned(refined_bitmap)) +} + +/// Select Huffman tables based on flags (7.4.3.1.6). +fn select_huffman_tables<'a>( + flags: &TextRegionHuffmanFlags, + custom_tables: &'a [HuffmanTable], + standard_tables: &'a StandardHuffmanTables, +) -> Result> { + let mut custom_table_idx = 0; + + let mut get_custom = || -> Result<&'a HuffmanTable> { + let table = custom_tables + .get(custom_table_idx) + .ok_or(HuffmanError::MissingTables)?; + custom_table_idx += 1; + Ok(table) + }; + + // "1) SBHUFFFS" + let first_s = match flags.first_s_table { + 0 => standard_tables.table_f(), + 1 => standard_tables.table_g(), + 3 => get_custom()?, + _ => bail!(HuffmanError::InvalidSelection), + }; + + // "2) SBHUFFDS" + let delta_s = match flags.delta_s_table { + 0 => standard_tables.table_h(), + 1 => standard_tables.table_i(), + 2 => standard_tables.table_j(), + 3 => get_custom()?, + _ => bail!(HuffmanError::InvalidSelection), + }; + + // "3) SBHUFFDT" + let delta_t = match flags.delta_t_table { + 0 => standard_tables.table_k(), + 1 => standard_tables.table_l(), + 2 => standard_tables.table_m(), + 3 => get_custom()?, + _ => bail!(HuffmanError::InvalidSelection), + }; + + // "4) SBHUFFRDW" + let refinement_width = match flags.refinement_width_table { + 0 => standard_tables.table_n(), + 1 => standard_tables.table_o(), + 3 => get_custom()?, + _ => bail!(HuffmanError::InvalidSelection), + }; + + // "5) SBHUFFRDH" + let refinement_height = match flags.refinement_height_table { + 0 => standard_tables.table_n(), + 1 => standard_tables.table_o(), + 3 => get_custom()?, + _ => bail!(HuffmanError::InvalidSelection), + }; + + // "6) SBHUFFRDY" + let refinement_y = match flags.refinement_y_table { + 0 => standard_tables.table_n(), + 1 => standard_tables.table_o(), + 3 => get_custom()?, + _ => bail!(HuffmanError::InvalidSelection), + }; + + // "7) SBHUFFRDX" + let refinement_x = match flags.refinement_x_table { + 0 => standard_tables.table_n(), + 1 => standard_tables.table_o(), + 3 => get_custom()?, + _ => bail!(HuffmanError::InvalidSelection), + }; + + // "8) SBHUFFRSIZE" + let refinement_size = match flags.refinement_size_table { + 0 => standard_tables.table_a(), + 1 => get_custom()?, + _ => bail!(HuffmanError::InvalidSelection), + }; + + Ok(TextRegionHuffmanTables { + first_s, + delta_s, + delta_t, + refinement_width, + refinement_height, + refinement_y, + refinement_x, + refinement_size, + }) +} + +/// Decode the symbol ID Huffman table (7.4.3.1.7). +fn decode_symbol_id_huffman_table( + reader: &mut Reader<'_>, + num_symbols: u32, +) -> Result { + let mut runcode_lines: Vec = Vec::with_capacity(35); + for runcode_idx in 0..35 { + let prefix_length = reader.read_bits(4).ok_or(HuffmanError::InvalidCode)? as u8; + runcode_lines.push(TableLine::new(runcode_idx, prefix_length, 0)); + } + + let runcode_table = HuffmanTable::build(&runcode_lines)?; + let mut symbol_code_lengths = Vec::with_capacity(num_symbols as usize); + + while symbol_code_lengths.len() < num_symbols as usize { + let runcode = runcode_table.decode_no_oob(reader)? as u32; + + match runcode { + 0..=31 => { + symbol_code_lengths.push(runcode as u8); + } + 32 => { + // Copy previous 3-6 times. + let extra_bits = reader.read_bits(2).ok_or(HuffmanError::InvalidCode)? as usize; + let repeat = extra_bits + 3; + let previous_length = *symbol_code_lengths + .last() + .ok_or(HuffmanError::InvalidCode)?; + symbol_code_lengths.extend(iter::repeat_n(previous_length, repeat)); + } + 33 => { + // Repeat 0 length 3-10 times. + let extra_bits = reader.read_bits(3).ok_or(HuffmanError::InvalidCode)? as usize; + let repeat = extra_bits + 3; + symbol_code_lengths.extend(iter::repeat_n(0, repeat)); + } + 34 => { + // Repeat 0 length 11-138 times. + let extra_bits = reader.read_bits(7).ok_or(HuffmanError::InvalidCode)? as usize; + let repeat = extra_bits + 11; + symbol_code_lengths.extend(iter::repeat_n(0, repeat)); + } + _ => bail!(HuffmanError::InvalidCode), + } + } + + if symbol_code_lengths.len() != num_symbols as usize { + bail!(HuffmanError::InvalidCode); + } + + reader.align(); + + let symbol_lines: Vec = symbol_code_lengths + .iter() + .enumerate() + .map(|(symbol_idx, &prefix_length)| TableLine::new(symbol_idx as i32, prefix_length, 0)) + .collect(); + HuffmanTable::build(&symbol_lines) +} + +pub(crate) struct TextRegionHuffmanTables<'a> { + first_s: &'a HuffmanTable, + delta_s: &'a HuffmanTable, + delta_t: &'a HuffmanTable, + refinement_width: &'a HuffmanTable, + refinement_height: &'a HuffmanTable, + refinement_y: &'a HuffmanTable, + refinement_x: &'a HuffmanTable, + refinement_size: &'a HuffmanTable, +} + +/// Reference corner for symbol placement. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ReferenceCorner { + BottomLeft, + TopLeft, + BottomRight, + TopRight, +} + +impl ReferenceCorner { + fn from_byte(value: u8) -> Self { + match value { + 0 => Self::BottomLeft, + 1 => Self::TopLeft, + 2 => Self::BottomRight, + 3 => Self::TopRight, + _ => unreachable!(), + } + } +} + +/// Parsed text region segment flags (7.4.3.1.1). +#[derive(Debug, Clone)] +pub(crate) struct TextRegionFlags { + pub(crate) use_huffman: bool, + pub(crate) use_refinement: bool, + pub(crate) log_strip_size: u8, + pub(crate) reference_corner: ReferenceCorner, + pub(crate) transposed: bool, + pub(crate) combination_operator: CombinationOperator, + pub(crate) default_pixel: bool, + pub(crate) delta_s_offset: i8, + pub(crate) refinement_template: RefinementTemplate, +} + +/// Text region segment Huffman flags (7.4.3.1.2). +#[derive(Debug, Clone)] +pub(crate) struct TextRegionHuffmanFlags { + pub(crate) first_s_table: u8, + pub(crate) delta_s_table: u8, + pub(crate) delta_t_table: u8, + pub(crate) refinement_width_table: u8, + pub(crate) refinement_height_table: u8, + pub(crate) refinement_y_table: u8, + pub(crate) refinement_x_table: u8, + pub(crate) refinement_size_table: u8, +} + +/// Parsed text region segment header (7.4.3.1). +#[derive(Debug, Clone)] +pub(crate) struct TextRegionHeader<'a> { + pub(crate) region_info: RegionSegmentInfo, + pub(crate) flags: TextRegionFlags, + pub(crate) huffman_flags: Option, + pub(crate) refinement_at_pixels: Vec, + pub(crate) num_instances: u32, + pub(crate) symbol_id_table: Option, + pub(crate) data: &'a [u8], +} + +impl TextRegionHeader<'_> { + pub(crate) fn strip_size(&self) -> u32 { + 1_u32 << self.flags.log_strip_size + } +} + +/// Parse text region segment flags (7.4.3.1.1). +fn parse_text_region_flags(reader: &mut Reader<'_>) -> Result { + let flags_word = reader.read_u16().ok_or(ParseError::UnexpectedEof)?; + let use_huffman = flags_word & 0x0001 != 0; + let use_refinement = flags_word & 0x0002 != 0; + let log_strip_size = ((flags_word >> 2) & 0x03) as u8; + let reference_corner = ReferenceCorner::from_byte(((flags_word >> 4) & 0x03) as u8); + let transposed = flags_word & 0x0040 != 0; + let combination_operator = CombinationOperator::from_value(((flags_word >> 7) & 0x03) as u8)?; + + let default_pixel = flags_word & 0x0200 != 0; + + let delta_s_offset_raw = ((flags_word >> 10) & 0x1F) as u8; + let delta_s_offset = if delta_s_offset_raw & 0x10 != 0 { + (delta_s_offset_raw | 0xE0) as i8 + } else { + delta_s_offset_raw as i8 + }; + + let refinement_template = RefinementTemplate::from_byte((flags_word >> 15) as u8); + + Ok(TextRegionFlags { + use_huffman, + use_refinement, + log_strip_size, + reference_corner, + transposed, + combination_operator, + default_pixel, + delta_s_offset, + refinement_template, + }) +} + +/// Parse text region Huffman flags (7.4.3.1.2). +fn parse_text_region_huffman_flags(reader: &mut Reader<'_>) -> Result { + let flags_word = reader.read_u16().ok_or(ParseError::UnexpectedEof)?; + let first_s_table = (flags_word & 0x03) as u8; + let delta_s_table = ((flags_word >> 2) & 0x03) as u8; + let delta_t_table = ((flags_word >> 4) & 0x03) as u8; + let refinement_width_table = ((flags_word >> 6) & 0x03) as u8; + let refinement_height_table = ((flags_word >> 8) & 0x03) as u8; + let refinement_y_table = ((flags_word >> 10) & 0x03) as u8; + let refinement_x_table = ((flags_word >> 12) & 0x03) as u8; + let refinement_size_table = ((flags_word >> 14) & 0x01) as u8; + + Ok(TextRegionHuffmanFlags { + first_s_table, + delta_s_table, + delta_t_table, + refinement_width_table, + refinement_height_table, + refinement_y_table, + refinement_x_table, + refinement_size_table, + }) +} + +/// Parse a text region segment header (7.4.3.1). +pub(crate) fn parse<'a>(reader: &mut Reader<'a>, num_symbols: u32) -> Result> { + let region_info = parse_region_segment_info(reader)?; + let flags = parse_text_region_flags(reader)?; + let huffman_flags = if flags.use_huffman { + Some(parse_text_region_huffman_flags(reader)?) + } else { + None + }; + + let refinement_at_pixels = + if flags.use_refinement && flags.refinement_template == RefinementTemplate::Template0 { + parse_refinement_at_pixels(reader)? + } else { + Vec::new() + }; + + let num_instances = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + + let symbol_id_table = if flags.use_huffman { + Some(decode_symbol_id_huffman_table(reader, num_symbols)?) + } else { + None + }; + + let data = reader.tail().ok_or(ParseError::UnexpectedEof)?; + + Ok(TextRegionHeader { + region_info, + flags, + huffman_flags, + refinement_at_pixels, + num_instances, + symbol_id_table, + data, + }) +} diff --git a/vendor/hayro-jbig2/src/error.rs b/vendor/hayro-jbig2/src/error.rs new file mode 100644 index 00000000..c579368b --- /dev/null +++ b/vendor/hayro-jbig2/src/error.rs @@ -0,0 +1,322 @@ +//! Error types for JBIG2 decoding. + +use core::fmt; + +/// The main error type for JBIG2 decoding operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + /// Errors related to reading/parsing data. + Parse(ParseError), + /// Errors related to file structure. + Format(FormatError), + /// Errors related to segment processing. + Segment(SegmentError), + /// Errors related to Huffman decoding. + Huffman(HuffmanError), + /// Errors related to region parameters. + Region(RegionError), + /// Errors related to template configuration. + Template(TemplateError), + /// Errors related to symbol handling. + Symbol(SymbolError), + /// Arithmetic overflow in calculations. + Overflow(OverflowError), + /// Feature not yet implemented. + Unsupported, +} + +/// Errors related to reading/parsing data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParseError { + /// Unexpected end of input. + UnexpectedEof, +} + +/// Errors related to file structure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatError { + /// Invalid file header signature. + InvalidHeader, + /// Reserved bits are not zero. + ReservedBits, + /// Missing required page information segment. + MissingPageInfo, + /// Page height unknown with no stripe segments. + UnknownPageHeight, + /// Page has zero width or height. + EmptyPage, +} + +/// Errors related to segment processing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SegmentError { + /// Unknown or reserved segment type. + UnknownType, + /// Invalid referred-to segment count. + InvalidReferredCount, + /// Segment refers to a larger segment number. + InvalidReference, + /// Missing end marker for unknown-length region. + MissingEndMarker, + /// Missing required pattern dictionary. + MissingPatternDictionary, +} + +/// Errors related to Huffman decoding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HuffmanError { + /// Invalid Huffman code sequence. + InvalidCode, + /// Invalid Huffman table selection. + InvalidSelection, + /// Not enough referred Huffman tables. + MissingTables, + /// Unexpected out-of-band value. + UnexpectedOob, + /// Huffman tree has conflicting codes (one code is a prefix of another). + ConflictingCodes, + /// Prefix length exceeds maximum supported value. + PrefixLengthTooLarge, +} + +/// Errors related to region parameters. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RegionError { + /// Invalid combination operator value. + InvalidCombinationOperator, + /// Region with invalid dimension. + InvalidDimension, + /// MMR (Modified Modified Read) decoding failed. + InvalidMmrData, + /// Gray-scale value exceeds pattern count. + GrayScaleOutOfRange, +} + +/// Errors related to template configuration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TemplateError { + /// An invalid template value was used. + Invalid, + /// Invalid adaptive template pixel location. + InvalidAtPixel, +} + +/// Errors related to symbol handling. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SymbolError { + /// No symbols available for text region. + NoSymbols, + /// The symbol dictionary contains more symbols than expected. + TooManySymbols, + /// Symbol ID out of valid range. + OutOfRange, + /// Unexpected out-of-band value. + UnexpectedOob, + /// An invalid symbol was encountered. + Invalid, + /// Strip delta multiplication overflowed. + InvalidStripDelta, + /// Too many symbol instances in a text region. + TooManyInstances, +} + +/// Arithmetic overflow errors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OverflowError { + /// Bitmap dimension computation overflowed. + BitmapDimension, + /// Halftone grid dimension computation overflowed. + GridDimension, + /// Placement coordinate computation overflowed. + PlacementCoordinate, + /// Reference offset computation overflowed. + ReferenceOffset, + /// Index computation overflowed. + Index, + /// A symbol dictionary has too many symbols. + SymbolCount, +} + +impl fmt::Display for DecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Parse(e) => write!(f, "{e}"), + Self::Format(e) => write!(f, "{e}"), + Self::Segment(e) => write!(f, "{e}"), + Self::Huffman(e) => write!(f, "{e}"), + Self::Region(e) => write!(f, "{e}"), + Self::Template(e) => write!(f, "{e}"), + Self::Symbol(e) => write!(f, "{e}"), + Self::Overflow(e) => write!(f, "{e}"), + Self::Unsupported => write!(f, "unsupported feature"), + } + } +} + +impl fmt::Display for OverflowError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BitmapDimension => write!(f, "overflow in bitmap dimension computation"), + Self::PlacementCoordinate => write!(f, "overflow in placement coordinate computation"), + Self::ReferenceOffset => write!(f, "overflow in reference offset computation"), + Self::Index => write!(f, "overflow in index computation"), + Self::GridDimension => write!(f, "overflow in grid dimension computation"), + Self::SymbolCount => write!(f, "a symbol dictionary has too many symbols"), + } + } +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnexpectedEof => write!(f, "unexpected end of input"), + } + } +} + +impl fmt::Display for FormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidHeader => write!(f, "invalid JBIG2 file header"), + Self::ReservedBits => write!(f, "reserved bits must be zero"), + Self::MissingPageInfo => write!(f, "missing page information segment"), + Self::UnknownPageHeight => write!(f, "page height unknown with no stripe segments"), + Self::EmptyPage => write!(f, "page has zero width or height"), + } + } +} + +impl fmt::Display for SegmentError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownType => write!(f, "unknown or reserved segment type"), + Self::InvalidReferredCount => write!(f, "invalid referred-to segment count"), + Self::InvalidReference => write!(f, "segment refers to larger segment number"), + Self::MissingEndMarker => write!(f, "missing end marker for unknown-length region"), + Self::MissingPatternDictionary => write!(f, "missing required pattern dictionary"), + } + } +} + +impl fmt::Display for HuffmanError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidCode => write!(f, "invalid Huffman code"), + Self::InvalidSelection => write!(f, "invalid Huffman table selection"), + Self::MissingTables => write!(f, "not enough referred Huffman tables"), + Self::UnexpectedOob => write!(f, "unexpected out-of-band value"), + Self::ConflictingCodes => write!(f, "conflicting Huffman codes"), + Self::PrefixLengthTooLarge => write!(f, "Huffman prefix length too large"), + } + } +} + +impl fmt::Display for RegionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidCombinationOperator => write!(f, "invalid combination operator"), + Self::InvalidDimension => write!(f, "invalid dimension value"), + Self::InvalidMmrData => write!(f, "invalid MMR-encoded data"), + Self::GrayScaleOutOfRange => write!(f, "gray-scale value exceeds pattern count"), + } + } +} + +impl fmt::Display for TemplateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Invalid => write!(f, "invalid template value"), + Self::InvalidAtPixel => write!(f, "invalid adaptive template pixel location"), + } + } +} + +impl fmt::Display for SymbolError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoSymbols => write!(f, "no symbols available"), + Self::OutOfRange => write!(f, "symbol ID out of range"), + Self::UnexpectedOob => write!(f, "unexpected out-of-band value"), + Self::TooManySymbols => write!(f, "symbol dictionary contains too many symbols"), + Self::Invalid => write!(f, "invalid symbol encountered"), + Self::InvalidStripDelta => write!(f, "strip delta overflow"), + Self::TooManyInstances => write!(f, "too many symbol instances"), + } + } +} + +impl core::error::Error for DecodeError {} +impl core::error::Error for ParseError {} +impl core::error::Error for FormatError {} +impl core::error::Error for SegmentError {} +impl core::error::Error for HuffmanError {} +impl core::error::Error for RegionError {} +impl core::error::Error for TemplateError {} +impl core::error::Error for SymbolError {} +impl core::error::Error for OverflowError {} + +impl From for DecodeError { + fn from(e: ParseError) -> Self { + Self::Parse(e) + } +} + +impl From for DecodeError { + fn from(e: FormatError) -> Self { + Self::Format(e) + } +} + +impl From for DecodeError { + fn from(e: SegmentError) -> Self { + Self::Segment(e) + } +} + +impl From for DecodeError { + fn from(e: HuffmanError) -> Self { + Self::Huffman(e) + } +} + +impl From for DecodeError { + fn from(e: RegionError) -> Self { + Self::Region(e) + } +} + +impl From for DecodeError { + fn from(e: TemplateError) -> Self { + Self::Template(e) + } +} + +impl From for DecodeError { + fn from(e: SymbolError) -> Self { + Self::Symbol(e) + } +} + +impl From for DecodeError { + fn from(e: OverflowError) -> Self { + Self::Overflow(e) + } +} + +/// Result type for JBIG2 decoding operations. +pub type Result = core::result::Result; + +macro_rules! bail { + ($err:expr) => { + return Err($err.into()) + }; +} + +macro_rules! err { + ($err:expr) => { + Err($err.into()) + }; +} + +pub(crate) use bail; +pub(crate) use err; diff --git a/vendor/hayro-jbig2/src/file.rs b/vendor/hayro-jbig2/src/file.rs new file mode 100644 index 00000000..1440a22e --- /dev/null +++ b/vendor/hayro-jbig2/src/file.rs @@ -0,0 +1,214 @@ +//! File format parsing for JBIG2 bitstreams (Annex D of ITU-T T.88). +//! +//! This module handles parsing of standalone JBIG2 files in both sequential +//! and random-access organization formats. + +use alloc::vec::Vec; + +use crate::error::{FormatError, ParseError, Result, bail}; +use crate::reader::Reader; +use crate::segment::{ + Segment, SegmentType, parse_segment, parse_segment_data, parse_segment_header, +}; + +/// "There are two standalone file organizations possible for a JBIG2 bitstream." +/// (Annex D) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FileOrganization { + /// "This is a standalone file organization. This organization is intended for + /// streaming applications, where the decoder is guaranteed to begin at the start + /// of the bitstream and decode everything up to the end of the bitstream." + /// (D.1) + Sequential, + /// "This is a standalone file organization. This organization is intended for + /// random-access applications, where the decoder might want to process parts of + /// the file in an arbitrary order." (D.2) + RandomAccess, +} + +/// Parsed file header. +/// +/// "A file header contains the following fields, in order: +/// ID string โ€“ see D.4.1. +/// File header flags โ€“ see D.4.2. +/// Number of pages โ€“ see D.4.3." (D.4) +#[derive(Debug, Clone)] +pub(crate) struct FileHeader { + /// The file organization type. + pub(crate) organization: FileOrganization, + /// The number of pages in the file, if known. + pub(crate) _number_of_pages: Option, + /// "If this bit is 0, no generic region segments uses the templates with + /// 12 AT pixels. If the file contains one or more generic region segments + /// using such templates, this bit must be 1." (D.4.2, Bit 2) + pub(crate) _uses_extended_templates: bool, + /// "If this bit is 0, no region segment is extended to be coloured. If the + /// file contains one or more coloured region segments, this bit must be 1." + /// (D.4.2, Bit 3) + pub(crate) _contains_coloured_regions: bool, +} + +/// A parsed JBIG2 file. +#[derive(Debug)] +pub(crate) struct File<'a> { + /// The file header. + pub(crate) _header: FileHeader, + /// The segments in the file. + pub(crate) segments: Vec>, +} + +/// "This is an 8-byte sequence containing 0x97 0x4A 0x42 0x32 0x0D 0x0A 0x1A 0x0A." +/// (D.4.1) +const FILE_HEADER_ID: [u8; 8] = [0x97, 0x4A, 0x42, 0x32, 0x0D, 0x0A, 0x1A, 0x0A]; + +/// Parse a standalone JBIG2 file. +pub(crate) fn parse_file(data: &[u8]) -> Result> { + let mut reader = Reader::new(data); + + let header = parse_file_header(&mut reader)?; + let mut segments = parse_segments(&mut reader, header.organization)?; + + // Technically shouldn't be necessary because the spec mandates that segments + // are in sorted order, but just to be safe. + segments.sort_by_key(|seg| seg.header.segment_number); + + Ok(File { + _header: header, + segments, + }) +} + +fn parse_file_header(reader: &mut Reader<'_>) -> Result { + // D.4.1: ID string + let id = reader.read_bytes(8).ok_or(ParseError::UnexpectedEof)?; + if id != FILE_HEADER_ID { + bail!(FormatError::InvalidHeader); + } + + // D.4.2: File header flags + let flags = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + + // "Bit 0: File organization type. If this bit is 0, the file uses the + // random-access organization. If this bit is 1, the file uses the + // sequential organization." (D.4.2) + let organization = if flags & 0x01 != 0 { + FileOrganization::Sequential + } else { + FileOrganization::RandomAccess + }; + + // "Bit 1: Unknown number of pages. If this bit is 0, then the number of + // pages contained in the file is known. If this bit is 1, then the number + // of pages contained in the file was not known at the time that the file + // header was coded." (D.4.2) + let unknown_page_count = flags & 0x02 != 0; + + // "Bit 2: If this bit is 0, no generic region segments uses the templates + // with 12 AT pixels." (D.4.2) + let uses_extended_templates = flags & 0x04 != 0; + + // "Bit 3: If this bit is 0, no region segment is extended to be coloured." + // (D.4.2) + let contains_coloured_regions = flags & 0x08 != 0; + + // "Bits 4-7: Reserved; must be 0." (D.4.2) + if flags & 0xF0 != 0 { + bail!(FormatError::ReservedBits); + } + + // D.4.3: Number of pages + // "This is a 4-byte field, and is not present if the 'unknown number of + // pages' bit was 1." (D.4.3) + let number_of_pages = if unknown_page_count { + None + } else { + Some(reader.read_u32().ok_or(ParseError::UnexpectedEof)?) + }; + + Ok(FileHeader { + organization, + _number_of_pages: number_of_pages, + _uses_extended_templates: uses_extended_templates, + _contains_coloured_regions: contains_coloured_regions, + }) +} + +fn parse_segments<'a>( + reader: &mut Reader<'a>, + organization: FileOrganization, +) -> Result>> { + let mut segments = Vec::new(); + + match organization { + FileOrganization::Sequential => parse_segments_sequential(reader, &mut segments)?, + FileOrganization::RandomAccess => parse_segments_random_access(reader, &mut segments)?, + } + + Ok(segments) +} + +/// Parse segments in sequential organization. +/// +/// "In this organization, the file structure looks like Figure D.1. A file header +/// is followed by a sequence of segments. The two parts of each segment are stored +/// together: first the segment header then the segment data." (D.1) +pub(crate) fn parse_segments_sequential<'a>( + reader: &mut Reader<'a>, + segments: &mut Vec>, +) -> Result<()> { + loop { + if reader.at_end() { + break; + } + + let segment = parse_segment(reader)?; + + // "If a file contains an end of file segment, it must be the last segment." + // (7.4.11) + let is_eof = matches!(segment.header.segment_type, SegmentType::EndOfFile); + segments.push(segment); + + if is_eof { + break; + } + } + + Ok(()) +} + +/// Parse segments in random-access organization. +/// +/// "In this organization, the file structure looks like Figure D.2. A file header +/// is followed by a sequence of segments headers; the last segment header is +/// followed by the data for the first segment, then the data for the second +/// segment, and so on." (D.2) +fn parse_segments_random_access<'a>( + reader: &mut Reader<'a>, + segments: &mut Vec>, +) -> Result<()> { + let mut headers = Vec::new(); + + loop { + if reader.at_end() { + break; + } + + let header = parse_segment_header(reader)?; + + // "If a file contains an end of file segment, it must be the last segment." + // (7.4.11) + let is_eof = matches!(header.segment_type, SegmentType::EndOfFile); + headers.push(header); + + if is_eof { + break; + } + } + + // Then, read all segment data. + for header in headers { + segments.push(parse_segment_data(reader, header)?); + } + + Ok(()) +} diff --git a/vendor/hayro-jbig2/src/gray_scale.rs b/vendor/hayro-jbig2/src/gray_scale.rs new file mode 100644 index 00000000..2836ec77 --- /dev/null +++ b/vendor/hayro-jbig2/src/gray_scale.rs @@ -0,0 +1,291 @@ +//! Gray-scale image decoding procedure (Annex C). + +use alloc::vec; +use alloc::vec::Vec; + +use crate::ScratchBuffers; +use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext}; +use crate::bitmap::{Bitmap, MAX_DIMENSION, WORD_BITS}; +use crate::decode::generic::{ContextGatherer, decode_bitmap_mmr}; +use crate::decode::{AdaptiveTemplatePixel, Template}; +use crate::error::{OverflowError, Result, bail}; +use crate::simd::{self, Level, Simd, u32x8}; + +/// Input parameters to the gray-scale image decoding procedure (Table C.1). +#[derive(Debug, Clone)] +pub(crate) struct GrayScaleParams<'a> { + /// `GSMMR` - Specifies whether MMR is used. + pub(crate) use_mmr: bool, + /// `GSBPP` - The number of bits per gray-scale value. + pub(crate) bits_per_pixel: u32, + /// `GSW` - The width of the gray-scale image. + pub(crate) width: u32, + /// `GSH` - The height of the gray-scale image. + pub(crate) height: u32, + /// `GSTEMPLATE` - The template used to code the gray-scale bitplanes. + pub(crate) template: Template, + /// `GSKIP` - A mask indicating which values should be skipped. + /// None if `GSUSESKIP` = 0. + pub(crate) skip_mask: Option<&'a [u32]>, +} + +/// The gray-scale image decoding procedure (Annex C, C.5). +/// +/// Returns `GSVALS`: the decoded gray-scale image array, GSW ร— GSH pixels. +#[inline(always)] +pub(crate) fn decode_gray_scale_image( + data: &[u8], + params: &GrayScaleParams<'_>, + ctx: &mut ScratchBuffers, +) -> Result> { + // Table C.1: "GSMMR specifies whether MMR is used." + if params.use_mmr { + decode_mmr(data, params) + } else { + decode_arithmetic(data, params, ctx) + } +} + +/// The gray-scale image decoding procedure using MMR (Annex C, C.5). +/// +/// Table C.4: "MMR = GSMMR" +fn decode_mmr(data: &[u8], params: &GrayScaleParams<'_>) -> Result> { + // `GSW` - The width of the gray-scale image. + let width = params.width; + // `GSH` - The height of the gray-scale image. + let height = params.height; + // `GSBPP` - The number of bits per gray-scale value. + let bits_per_pixel = params.bits_per_pixel; + let stride = width.div_ceil(WORD_BITS); + + let mut offset = 0; + decode_bitplanes(width, height, stride, bits_per_pixel, |_, bitplane| { + // Table C.4: "GBW = GSW, GBH = GSH" + offset += decode_bitmap_mmr(bitplane, &data[offset..])?; + + Ok(()) + }) +} + +/// The gray-scale image decoding procedure using arithmetic coding (Annex C, C.5). +/// +/// Table C.4: "GBTEMPLATE = GSTEMPLATE, TPGDON = 0, USESKIP = GSUSESKIP, SKIP = GSKIP" +fn decode_arithmetic( + data: &[u8], + params: &GrayScaleParams<'_>, + ctx: &mut ScratchBuffers, +) -> Result> { + // `GSW` - The width of the gray-scale image. + let width = params.width; + // `GSH` - The height of the gray-scale image. + let height = params.height; + + if width > MAX_DIMENSION || height > MAX_DIMENSION { + bail!(OverflowError::BitmapDimension); + } + // PdfCraft patch: the product is bounded too (see `bitmap::MAX_PIXELS`). + if u64::from(width) * u64::from(height) > crate::bitmap::MAX_PIXELS { + bail!(OverflowError::BitmapDimension); + } + + // `GSBPP` - The number of bits per gray-scale value. + let bits_per_pixel = params.bits_per_pixel; + let stride = width.div_ceil(WORD_BITS); + // `GSKIP` - The skip mask (if GSUSESKIP = 1). + let skip_mask = params.skip_mask; + let skip_stride = width.div_ceil(32); + // `GSTEMPLATE` - The template used for bitplane decoding. + let template = params.template; + + // Table C.4: Adaptive template pixel positions. + let at_pixels: [AdaptiveTemplatePixel; 4] = match template { + Template::Template0 => [ + AdaptiveTemplatePixel { x: 3, y: -1 }, + AdaptiveTemplatePixel { x: -3, y: -1 }, + AdaptiveTemplatePixel { x: 2, y: -2 }, + AdaptiveTemplatePixel { x: -2, y: -2 }, + ], + Template::Template1 => [ + AdaptiveTemplatePixel { x: 3, y: -1 }, + // Unused. + AdaptiveTemplatePixel::default(), + AdaptiveTemplatePixel::default(), + AdaptiveTemplatePixel::default(), + ], + Template::Template2 | Template::Template3 => [ + AdaptiveTemplatePixel { x: 2, y: -1 }, + // Unused. + AdaptiveTemplatePixel::default(), + AdaptiveTemplatePixel::default(), + AdaptiveTemplatePixel::default(), + ], + }; + + let mut decoder = ArithmeticDecoder::new(data); + ctx.contexts.clear(); + ctx.contexts.resize( + 1 << template.context_bits(), + ArithmeticDecoderContext::default(), + ); + + macro_rules! gs_decode_loop { + ($gather:expr) => { + decode_bitplanes(width, height, stride, bits_per_pixel, |_, bitplane| { + // Table C.4: "GBW = GSW, GBH = GSH, TPGDON = 0" + let mut gatherer = ContextGatherer::new(template, &at_pixels); + + for y in 0..height { + gatherer.start_row(bitplane, y); + for x in 0..width { + gatherer.maybe_reload_buffers(bitplane, x); + + // Table C.4: "USESKIP = GSUSESKIP, SKIP = GSKIP" + if let Some(mask) = skip_mask { + let word_idx = (y * skip_stride + x / 32) as usize; + let bit_pos = 31 - (x % 32); + if (mask[word_idx] >> bit_pos) & 1 != 0 { + // Still need to update the context. + let _ = ($gather)(&mut gatherer, bitplane, x); + gatherer.update_current_row(x, 0); + continue; + } + } + + let context = ($gather)(&mut gatherer, bitplane, x); + let pixel = decoder.read_bit(&mut ctx.contexts[context as usize]); + let value = pixel as u8; + + bitplane.set_pixel(x, y, value); + gatherer.update_current_row(x, value); + } + } + + Ok(()) + }) + }; + } + + match template { + Template::Template0 => gs_decode_loop!(ContextGatherer::gather_template0_default), + Template::Template1 => gs_decode_loop!(ContextGatherer::gather_template1_default), + Template::Template2 => gs_decode_loop!(ContextGatherer::gather_template2_default), + Template::Template3 => gs_decode_loop!(ContextGatherer::gather_template3_default), + } +} + +/// The bitplane decoding and gray value computation procedure (C.5). +/// +/// The closure `decode_next` is called for each bitplane, receiving the bitplane +/// index (`GSBPP`-1 down to 0) and a zeroed bitmap to decode into. +fn decode_bitplanes( + width: u32, + height: u32, + stride: u32, + bits_per_pixel: u32, + mut decode_next: F, +) -> Result> +where + F: FnMut(u32, &mut Bitmap) -> Result<()>, +{ + let size = width as usize * height as usize; + // `GSVALS` - The decoded gray-scale image array. + let mut values = vec![0_u32; size]; + + let mut bitplane = Bitmap::new(width, height)?; + let mut prev_plane = vec![0; bitplane.data.len()]; + + simd::dispatch!(Level::new(), simd => { + for j in (0..bits_per_pixel).rev() { + bitplane.data.fill(0); + decode_next(j, &mut bitplane)?; + + // Done every time except the first time. + if j < bits_per_pixel - 1 { + // C.5 step 3b: "GSPLANES[J][x, y] = GSPLANES[J + 1][x, y] XOR GSPLANES[J][x, y]" + xor_planes(simd, &mut bitplane.data, &prev_plane); + } + + // C.5 step 4: "GSVALS[x, y] = sum(J=0 to GSBPP-1) GSPLANES[J][x, y] * 2^J" + extract_bits(simd, &bitplane.data, &mut values, width, height, stride, j); + + // C.5 step 3c: "Set J = J - 1." + prev_plane.copy_from_slice(&bitplane.data); + } + + Ok(values) + }) +} + +#[inline(always)] +fn xor_planes(simd: S, plane: &mut [u32], prev: &[u32]) { + let simd_end = plane.len() & !7; + + for i in (0..simd_end).step_by(simd::SIMD_WIDTH) { + let chunk = &mut plane[i..i + simd::SIMD_WIDTH]; + let mut a = u32x8::from_slice(simd, chunk); + let b = u32x8::from_slice(simd, &prev[i..i + simd::SIMD_WIDTH]); + a ^= b; + a.store(chunk); + } + + for i in simd_end..plane.len() { + plane[i] ^= prev[i]; + } +} + +#[inline(always)] +fn extract_bits( + simd: S, + data: &[u32], + values: &mut [u32], + width: u32, + height: u32, + stride: u32, + j: u32, +) { + /// Bit masks for extracting individual bits from a packed u32 word (MSB-first). + /// `BIT_MASKS[i] = 1 << (31 - i)`. + const BIT_MASKS: [u32; 32] = { + let mut masks = [0_u32; 32]; + let mut i = 0; + while i < 32 { + masks[i] = 1 << (31 - i); + i += 1; + } + masks + }; + + let bit_value = u32x8::splat(simd, 1_u32 << j); + let zero = u32x8::splat(simd, 0); + let simd_end = (width as usize) & !7; + + for y in 0..height as usize { + let row_data = y * stride as usize; + let row_vals = y * width as usize; + + // Handle in chunks of 8. + for x in (0..simd_end).step_by(simd::SIMD_WIDTH) { + let word = data[row_data + x / 32]; + let bit_offset = x % 32; + + let splatted = u32x8::splat(simd, word); + let masks = + u32x8::from_slice(simd, &BIT_MASKS[bit_offset..bit_offset + simd::SIMD_WIDTH]); + let extracted = splatted & masks; + let nonzero = extracted.simd_gt(zero); + let contribution = nonzero.select(bit_value, zero); + let vals = &mut values[row_vals + x..row_vals + x + simd::SIMD_WIDTH]; + let current = u32x8::from_slice(simd, vals); + (current | contribution).store(vals); + } + + // Handle the tail. + for x in simd_end..width as usize { + let word_idx = row_data + x / 32; + let bit_pos = 31 - (x % 32); + if (data[word_idx] >> bit_pos) & 1 != 0 { + values[row_vals + x] |= 1 << j; + } + } + } +} diff --git a/vendor/hayro-jbig2/src/huffman_table.rs b/vendor/hayro-jbig2/src/huffman_table.rs new file mode 100644 index 00000000..2183c8ee --- /dev/null +++ b/vendor/hayro-jbig2/src/huffman_table.rs @@ -0,0 +1,621 @@ +//! Huffman table decoding, described in Annex B. + +use alloc::rc::Rc; +use alloc::vec; +use alloc::vec::Vec; +use core::num::NonZeroU32; + +use crate::error::{HuffmanError, ParseError, Result, bail}; +use crate::lazy::Lazy; +use crate::reader::Reader; + +include!("huffman_tables_generated.rs"); + +/// Maximum number of nodes in an inline Huffman table. +const INLINE_TABLE_SIZE: usize = 43; + +/// A queryable Huffman table. +#[derive(Debug, Clone)] +pub(crate) struct HuffmanTable(Rc); + +impl HuffmanTable { + /// Create a new inline Huffman table from a fixed-size node array. + fn from_inline(nodes: [HuffmanNode; INLINE_TABLE_SIZE]) -> Self { + Self(Rc::new(InnerHuffmanTable::Inline { nodes })) + } + + /// Create a new dynamic Huffman table from a vector of nodes. + fn from_dynamic(nodes: Vec) -> Self { + Self(Rc::new(InnerHuffmanTable::Dynamic { nodes })) + } + + /// Decode a value from the bit reader using this Huffman table + /// (B.4 "Using a Huffman table"). + /// + /// Returns `Ok(None)` for out-of-band (OOB) values, `Ok(Some(value))` for decoded values. + pub(crate) fn decode(&self, reader: &mut Reader<'_>) -> Result> { + let nodes: &[HuffmanNode] = match self.0.as_ref() { + InnerHuffmanTable::Inline { nodes } => nodes, + InnerHuffmanTable::Dynamic { nodes } => nodes, + }; + + HuffmanNode::decode_from(nodes, 0, reader) + } + + /// Decode a value using the huffman table, erroring out in case an OOB + /// was encountered. + pub(crate) fn decode_no_oob(&self, reader: &mut Reader<'_>) -> Result { + Ok(self.decode(reader)?.ok_or(HuffmanError::UnexpectedOob)?) + } + + /// Build a Huffman table from table line definitions (B.3 "Assigning + /// the prefix codes"). + pub(crate) fn build(lines: &[TableLine]) -> Result { + // `NTEMP` - Number of table lines. + let line_count = lines.len(); + + // Step 1: "Build a histogram in the array LENCOUNT counting the number of times + // each prefix length value occurs in PREFLEN: LENCOUNT[I] is the number of times + // that the value I occurs in the array PREFLEN." + // `LENMAX` - Maximum prefix length. + let max_prefix_length = lines.iter().map(|l| l.prefix_length).max().unwrap_or(0) as usize; + + if max_prefix_length > 32 { + bail!(HuffmanError::PrefixLengthTooLarge); + } + + // `LENCOUNT` - Histogram of prefix lengths. + let mut length_counts = vec![0_u32; max_prefix_length + 1]; + for line in lines { + length_counts[line.prefix_length as usize] += 1; + } + + // Step 2: "Let LENMAX be the largest value for which LENCOUNT[LENMAX] > 0. Set: + // CURLEN = 1, FIRSTCODE[0] = 0, LENCOUNT[0] = 0" + // `FIRSTCODE` - First code value for each length. + let mut first_code_per_length = vec![0_u32; max_prefix_length + 1]; + // `CODES` - Assigned prefix codes for each line. + let mut assigned_codes = vec![0_u32; line_count]; + length_counts[0] = 0; + + // Step 3: "While CURLEN โ‰ค LENMAX, perform the following operations:" + // `CURLEN` - Current length being processed. + for current_length in 1..=max_prefix_length { + // a) "Set: FIRSTCODE[CURLEN] = (FIRSTCODE[CURLEN โˆ’ 1] + LENCOUNT[CURLEN โˆ’ 1]) ร— 2 + // CURCODE = FIRSTCODE[CURLEN] + // CURTEMP = 0" + first_code_per_length[current_length] = (first_code_per_length[current_length - 1] + + length_counts[current_length - 1]) + .checked_mul(2) + .ok_or(HuffmanError::InvalidCode)?; + // `CURCODE` - Current code value being assigned. + let mut current_code = first_code_per_length[current_length]; + + // b) "While CURTEMP < NTEMP, perform the following operations:" + // `CURTEMP` - Current line index. + for line_index in 0..line_count { + // i) "If PREFLEN[CURTEMP] = CURLEN, then set: + // CODES[CURTEMP] = CURCODE + // CURCODE = CURCODE + 1" + if lines[line_index].prefix_length as usize == current_length { + assigned_codes[line_index] = current_code; + current_code = current_code + .checked_add(1) + .ok_or(HuffmanError::InvalidCode)?; + } + // ii) "Set CURTEMP = CURTEMP + 1" (implicit in for loop) + } + // c) "Set CURLEN = CURLEN + 1" (implicit in for loop) + } + + // Build tree from assigned codes. + let mut nodes = vec![HuffmanNode::new_intermediate()]; + + for (i, line) in lines.iter().enumerate() { + // "Note that the PREFLEN value 0 indicates that the table line is never used." + if line.prefix_length == 0 { + continue; + } + + Self::insert_code( + &mut nodes, + 0, // root index + assigned_codes[i], + line.prefix_length, + line.range_low, + line.range_length, + line.is_lower, + line.is_out_of_band, + )?; + } + + Ok(Self::from_dynamic(nodes)) + } + + /// Build a uniform Huffman table where all symbols have the same code length. + /// + /// This is used for symbol ID encoding (6.5.8.2.3): "For each value of i from + /// 0 to SDNUMINSYMS + NSYMSDECODED โ€“ 1, set SBSYMCODES[i] to the binary + /// representation of i using a SBSYMCODELEN-bit string." + pub(crate) fn build_uniform(num_symbols: u32, code_length: u32) -> Result { + let lines: Vec = (0..num_symbols) + .map(|i| TableLine::new(i as i32, code_length as u8, 0)) + .collect(); + Self::build(&lines) + } + + /// Insert a code into the Huffman tree. + fn insert_code( + nodes: &mut Vec, + node_index: u32, + code: u32, + prefix_length: u8, + range_low: i32, + range_length: u8, + is_lower: bool, + is_out_of_band: bool, + ) -> Result<()> { + if prefix_length == 0 { + // We've consumed all bits, this should be a leaf. + nodes[node_index as usize] = + HuffmanNode::new_leaf(range_low, range_length, is_lower, is_out_of_band); + return Ok(()); + } + + // Get the next bit (MSB first). + let bit = (code >> (prefix_length - 1)) & 1; + let remaining_code = code & ((1 << (prefix_length - 1)) - 1); + + let child_index = match nodes[node_index as usize].get_child(bit == 0) { + Some(idx) => idx, + None => { + let new_idx = NonZeroU32::new(nodes.len() as u32).unwrap(); + nodes.push(HuffmanNode::new_intermediate()); + nodes[node_index as usize].set_child(bit == 0, new_idx)?; + new_idx + } + }; + + Self::insert_code( + nodes, + child_index.get(), + remaining_code, + prefix_length - 1, + range_low, + range_length, + is_lower, + is_out_of_band, + ) + } + + /// Read a custom Huffman table from the bitstream (B.2 "Decoding a code table"). + pub(crate) fn read_custom(reader: &mut Reader<'_>) -> Result { + // 1) "Decode the code table flags field as described in B.2.1. This sets the values + // HTOOB, HTPS and HTRS." + let flags = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + + // `HTOOB` + let has_out_of_band = (flags & 1) != 0; + // `HTPS` + let prefix_length_bits = ((flags >> 1) & 7) + 1; + // `HTRS` + let range_length_bits = ((flags >> 4) & 7) + 1; + + // 2) "Decode the code table lowest value field as described in B.2.2. Let HTLOW be + // the value decoded." + // `HTLOW` + let minimum_value = reader.read_i32().ok_or(ParseError::UnexpectedEof)?; + + // 3) "Decode the code table highest value field as described in B.2.3. Let HTHIGH be + // the value decoded." + // `HTHIGH` + let maximum_value = reader.read_i32().ok_or(ParseError::UnexpectedEof)?; + + // 4) "Set: CURRANGELOW = HTLOW, NTEMP = 0" + let mut lines = Vec::new(); + // `CURRANGELOW` + let mut current_range_low = minimum_value; + + // 5) "Decode each table line as follows:" + // d) "If CURRANGELOW โ‰ฅ HTHIGH then proceed to step 6." + while current_range_low < maximum_value { + // a) "Read HTPS bits. Set PREFLEN[NTEMP] to the value decoded." + let prefix_length = reader + .read_bits(prefix_length_bits) + .ok_or(HuffmanError::InvalidCode)? as u8; + // b) "Read HTRS bits. Let RANGELEN[NTEMP] be the value decoded." + let range_length = reader + .read_bits(range_length_bits) + .ok_or(HuffmanError::InvalidCode)? as u8; + + // c) "Set: RANGELOW[NTEMP] = CURRANGELOW + // CURRANGELOW = CURRANGELOW + 2^RANGELEN[NTEMP] + // NTEMP = NTEMP + 1" + lines.push(TableLine::new( + current_range_low, + prefix_length, + range_length, + )); + + let range_size = 1_i64 + .checked_shl(range_length as u32) + .ok_or(HuffmanError::InvalidCode)?; + let next_range_low = (current_range_low as i64) + .checked_add(range_size) + .ok_or(HuffmanError::InvalidCode)?; + current_range_low = + i32::try_from(next_range_low).map_err(|_| HuffmanError::InvalidCode)?; + } + + // 6) "Read HTPS bits. Let LOWPREFLEN be the value read." + // 7) "Set: PREFLEN[NTEMP] = LOWPREFLEN, RANGELEN[NTEMP] = 32, + // RANGELOW[NTEMP] = HTLOW โˆ’ 1, NTEMP = NTEMP + 1 + // This is the lower range table line for this table." + lines.push(TableLine::lower( + minimum_value + .checked_sub(1) + .ok_or(HuffmanError::InvalidCode)?, + reader + .read_bits(prefix_length_bits) + .ok_or(HuffmanError::InvalidCode)? as u8, + 32, + )); + + // 8) "Read HTPS bits. Let HIGHPREFLEN be the value read." + // 9) "Set: PREFLEN[NTEMP] = HIGHPREFLEN, RANGELEN[NTEMP] = 32, + // RANGELOW[NTEMP] = HTHIGH, NTEMP = NTEMP + 1 + // This is the upper range table line for this table." + lines.push(TableLine::upper( + current_range_low, + reader + .read_bits(prefix_length_bits) + .ok_or(HuffmanError::InvalidCode)? as u8, + 32, + )); + + // 10) "If HTOOB is 1, then: + // a) Read HTPS bits. Let OOBPREFLEN be the value read. + // b) Set: PREFLEN[NTEMP] = OOBPREFLEN, NTEMP = NTEMP + 1 + // This is the out-of-band table line for this table." + if has_out_of_band { + lines.push(TableLine::oob( + reader + .read_bits(prefix_length_bits) + .ok_or(HuffmanError::InvalidCode)? as u8, + )); + } + + // 11) "Create the prefix codes using the algorithm described in B.3." + Self::build(&lines) + } +} + +/// A table line definition used to build the Huffman tree. +pub(crate) struct TableLine { + /// `RANGELOW` - The base value for computing the decoded value. + /// For normal/upper lines: value = `range_low` + offset + /// For lower lines: value = `range_low` - offset + pub(crate) range_low: i32, + /// `PREFLEN` - Prefix code length. + pub(crate) prefix_length: u8, + /// `RANGELEN` - Number of additional bits. + pub(crate) range_length: u8, + /// True if this is a lower range line (uses subtraction). + pub(crate) is_lower: bool, + /// `OOB` - True if this is the out-of-band marker. + pub(crate) is_out_of_band: bool, +} + +impl TableLine { + /// Create a normal table line. + pub(crate) const fn new(range_low: i32, prefix_length: u8, range_length: u8) -> Self { + Self { + range_low, + prefix_length, + range_length, + is_lower: false, + is_out_of_band: false, + } + } + + /// Create a lower range line (-โˆž...`range_high`). + const fn lower(range_high: i32, prefix_length: u8, range_length: u8) -> Self { + Self { + range_low: range_high, + prefix_length, + range_length, + is_lower: true, + is_out_of_band: false, + } + } + + /// Create an upper range line (`range_low`...+โˆž). + const fn upper(range_low: i32, prefix_length: u8, range_length: u8) -> Self { + Self { + range_low, + prefix_length, + range_length, + is_lower: false, + is_out_of_band: false, + } + } + + /// Create an out-of-band marker line. + const fn oob(prefix_length: u8) -> Self { + Self { + range_low: 0, + prefix_length, + range_length: 0, + is_lower: false, + is_out_of_band: true, + } + } +} + +/// A node in the Huffman tree. +#[derive(Debug, Clone, Copy)] +enum HuffmanNode { + /// Intermediate node. + Intermediate { + zero: Option, + one: Option, + }, + /// Leaf node. + Leaf(LeafData), + /// Empty node (padding to fill fixed-size arrays in inline tables). + Empty, +} + +impl HuffmanNode { + fn new_intermediate() -> Self { + Self::Intermediate { + zero: None, + one: None, + } + } + + fn new_leaf(range_low: i32, range_length: u8, is_lower: bool, is_out_of_band: bool) -> Self { + Self::Leaf(LeafData { + range_low, + range_length, + is_lower, + is_out_of_band, + }) + } + + /// Get the child index for a given bit (0 or 1). + fn get_child(&self, child_zero: bool) -> Option { + match self { + Self::Intermediate { zero, one } => { + if child_zero { + *zero + } else { + *one + } + } + _ => None, + } + } + + /// Set the child index for a given bit (0 or 1). + fn set_child( + &mut self, + child_zero: bool, + index: NonZeroU32, + ) -> core::result::Result<(), HuffmanError> { + match self { + Self::Intermediate { zero, one } => { + if child_zero { + *zero = Some(index); + } else { + *one = Some(index); + } + Ok(()) + } + _ => Err(HuffmanError::ConflictingCodes), + } + } + + /// Implements B.4 "Using a Huffman table". + fn decode_from( + nodes: &[Self], + mut node_index: u32, + reader: &mut Reader<'_>, + ) -> Result> { + // 1) "Read one bit at a time until the bit string read matches the code assigned to + // one of the table lines." + loop { + match nodes[node_index as usize] { + Self::Intermediate { zero, one } => { + let bit = reader.read_bit().ok_or(ParseError::UnexpectedEof)?; + let child_index = if bit == 0 { zero } else { one }; + node_index = child_index.ok_or(HuffmanError::InvalidCode)?.get(); + } + Self::Leaf(leaf) => { + // 3) "If HTOOB is 1 for this table, and table line I is the out-of-band + // table line for this table, then set: HTVAL = OOB" + if leaf.is_out_of_band { + return Ok(None); + } + + // 2) "Read RANGELEN[I] bits. Let HTOFFSET be the value read." + // `HTOFFSET` + let range_offset = reader + .read_bits(leaf.range_length) + .ok_or(HuffmanError::InvalidCode)? + as i32; + + // 4) "Otherwise, if table line I is the lower range table line for this + // table, then set: HTVAL = RANGELOW[I] โˆ’ HTOFFSET" + // 5) "Otherwise, set: HTVAL = RANGELOW[I] + HTOFFSET" + // `HTVAL` + let value = if leaf.is_lower { + leaf.range_low - range_offset + } else { + leaf.range_low + range_offset + }; + + return Ok(Some(value)); + } + Self::Empty => { + bail!(HuffmanError::InvalidCode); + } + } + } + } +} + +/// Information stored at a leaf node of the Huffman tree. +#[derive(Debug, Clone, Copy)] +struct LeafData { + /// `RANGELOW` - The base value for computing the decoded value. + range_low: i32, + /// `RANGELEN` - Number of additional bits to read. + range_length: u8, + /// True if this is a lower range line (uses subtraction). + is_lower: bool, + /// `OOB` - True if this is the out-of-band marker. + is_out_of_band: bool, +} + +/// The inner representation of a Huffman table. +/// +/// This can be either an inline table (fixed-size array for standard tables) +/// or a dynamic table (Vec for runtime-built custom tables). +#[derive(Debug, Clone)] +#[allow( + clippy::large_enum_variant, + reason = "Inline variant is expected to be large." +)] +enum InnerHuffmanTable { + Inline { + nodes: [HuffmanNode; INLINE_TABLE_SIZE], + }, + Dynamic { + nodes: Vec, + }, +} + +/// Standard Huffman tables (`TABLE_A` through `TABLE_O`). +#[derive(Debug)] +pub(crate) struct StandardHuffmanTables { + table_a: Lazy, + table_b: Lazy, + table_c: Lazy, + table_d: Lazy, + table_e: Lazy, + table_f: Lazy, + table_g: Lazy, + table_h: Lazy, + table_i: Lazy, + table_j: Lazy, + table_k: Lazy, + table_l: Lazy, + table_m: Lazy, + table_n: Lazy, + table_o: Lazy, +} + +impl Default for StandardHuffmanTables { + fn default() -> Self { + Self::new() + } +} + +impl StandardHuffmanTables { + /// Create a new instance. + pub(crate) fn new() -> Self { + Self { + table_a: Lazy::new(|| HuffmanTable::from_inline(TABLE_A)), + table_b: Lazy::new(|| HuffmanTable::from_inline(TABLE_B)), + table_c: Lazy::new(|| HuffmanTable::from_inline(TABLE_C)), + table_d: Lazy::new(|| HuffmanTable::from_inline(TABLE_D)), + table_e: Lazy::new(|| HuffmanTable::from_inline(TABLE_E)), + table_f: Lazy::new(|| HuffmanTable::from_inline(TABLE_F)), + table_g: Lazy::new(|| HuffmanTable::from_inline(TABLE_G)), + table_h: Lazy::new(|| HuffmanTable::from_inline(TABLE_H)), + table_i: Lazy::new(|| HuffmanTable::from_inline(TABLE_I)), + table_j: Lazy::new(|| HuffmanTable::from_inline(TABLE_J)), + table_k: Lazy::new(|| HuffmanTable::from_inline(TABLE_K)), + table_l: Lazy::new(|| HuffmanTable::from_inline(TABLE_L)), + table_m: Lazy::new(|| HuffmanTable::from_inline(TABLE_M)), + table_n: Lazy::new(|| HuffmanTable::from_inline(TABLE_N)), + table_o: Lazy::new(|| HuffmanTable::from_inline(TABLE_O)), + } + } + + /// Get Table B.1 (`TABLE_A`). + pub(crate) fn table_a(&self) -> &HuffmanTable { + self.table_a.get(|| HuffmanTable::from_inline(TABLE_A)) + } + + /// Get Table B.2 (`TABLE_B`). + pub(crate) fn table_b(&self) -> &HuffmanTable { + self.table_b.get(|| HuffmanTable::from_inline(TABLE_B)) + } + + /// Get Table B.3 (`TABLE_C`). + pub(crate) fn table_c(&self) -> &HuffmanTable { + self.table_c.get(|| HuffmanTable::from_inline(TABLE_C)) + } + + /// Get Table B.4 (`TABLE_D`). + pub(crate) fn table_d(&self) -> &HuffmanTable { + self.table_d.get(|| HuffmanTable::from_inline(TABLE_D)) + } + + /// Get Table B.5 (`TABLE_E`). + pub(crate) fn table_e(&self) -> &HuffmanTable { + self.table_e.get(|| HuffmanTable::from_inline(TABLE_E)) + } + + /// Get Table B.6 (`TABLE_F`). + pub(crate) fn table_f(&self) -> &HuffmanTable { + self.table_f.get(|| HuffmanTable::from_inline(TABLE_F)) + } + + /// Get Table B.7 (`TABLE_G`). + pub(crate) fn table_g(&self) -> &HuffmanTable { + self.table_g.get(|| HuffmanTable::from_inline(TABLE_G)) + } + + /// Get Table B.8 (`TABLE_H`). + pub(crate) fn table_h(&self) -> &HuffmanTable { + self.table_h.get(|| HuffmanTable::from_inline(TABLE_H)) + } + + /// Get Table B.9 (`TABLE_I`). + pub(crate) fn table_i(&self) -> &HuffmanTable { + self.table_i.get(|| HuffmanTable::from_inline(TABLE_I)) + } + + /// Get Table B.10 (`TABLE_J`). + pub(crate) fn table_j(&self) -> &HuffmanTable { + self.table_j.get(|| HuffmanTable::from_inline(TABLE_J)) + } + + /// Get Table B.11 (`TABLE_K`). + pub(crate) fn table_k(&self) -> &HuffmanTable { + self.table_k.get(|| HuffmanTable::from_inline(TABLE_K)) + } + + /// Get Table B.12 (`TABLE_L`). + pub(crate) fn table_l(&self) -> &HuffmanTable { + self.table_l.get(|| HuffmanTable::from_inline(TABLE_L)) + } + + /// Get Table B.13 (`TABLE_M`). + pub(crate) fn table_m(&self) -> &HuffmanTable { + self.table_m.get(|| HuffmanTable::from_inline(TABLE_M)) + } + + /// Get Table B.14 (`TABLE_N`). + pub(crate) fn table_n(&self) -> &HuffmanTable { + self.table_n.get(|| HuffmanTable::from_inline(TABLE_N)) + } + + /// Get Table B.15 (`TABLE_O`). + pub(crate) fn table_o(&self) -> &HuffmanTable { + self.table_o.get(|| HuffmanTable::from_inline(TABLE_O)) + } +} diff --git a/vendor/hayro-jbig2/src/huffman_tables_generated.rs b/vendor/hayro-jbig2/src/huffman_tables_generated.rs new file mode 100644 index 00000000..d3bfe2b6 --- /dev/null +++ b/vendor/hayro-jbig2/src/huffman_tables_generated.rs @@ -0,0 +1,727 @@ +// Auto-generated Huffman tables. Do not edit manually. +// Generated by generate_huffman_tables.py + +type N = HuffmanNode; +type L = LeafData; + +const fn nz(n: u32) -> NonZeroU32 { NonZeroU32::new(n).unwrap() } + +/// Standard Huffman table A (Table B.1). +#[rustfmt::skip] +const TABLE_A: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(1)), one: Some(nz(2)) }, + N::Leaf(L { range_low: 0, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(3)), one: Some(nz(4)) }, + N::Leaf(L { range_low: 16, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(5)), one: Some(nz(6)) }, + N::Leaf(L { range_low: 272, range_length: 16, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 65808, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table B (Table B.2). +#[rustfmt::skip] +const TABLE_B: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(1)), one: Some(nz(2)) }, + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(3)), one: Some(nz(4)) }, + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(5)), one: Some(nz(6)) }, + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(7)), one: Some(nz(8)) }, + N::Leaf(L { range_low: 3, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(9)), one: Some(nz(10)) }, + N::Leaf(L { range_low: 11, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(11)), one: Some(nz(12)) }, + N::Leaf(L { range_low: 75, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: true }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table C (Table B.3). +#[rustfmt::skip] +const TABLE_C: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(9)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(10)), one: Some(nz(2)) }, + N::Intermediate { zero: Some(nz(11)), one: Some(nz(3)) }, + N::Intermediate { zero: Some(nz(12)), one: Some(nz(4)) }, + N::Intermediate { zero: Some(nz(13)), one: Some(nz(5)) }, + N::Intermediate { zero: Some(nz(16)), one: Some(nz(6)) }, + N::Intermediate { zero: Some(nz(15)), one: Some(nz(7)) }, + N::Intermediate { zero: Some(nz(8)), one: Some(nz(14)) }, + N::Leaf(L { range_low: -256, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 3, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 11, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -257, range_length: 32, is_lower: true, is_out_of_band: false }), + N::Leaf(L { range_low: 75, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: true }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table D (Table B.4). +#[rustfmt::skip] +const TABLE_D: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(1)), one: Some(nz(2)) }, + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(3)), one: Some(nz(4)) }, + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(5)), one: Some(nz(6)) }, + N::Leaf(L { range_low: 3, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(7)), one: Some(nz(8)) }, + N::Leaf(L { range_low: 4, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(9)), one: Some(nz(10)) }, + N::Leaf(L { range_low: 12, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 76, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table E (Table B.5). +#[rustfmt::skip] +const TABLE_E: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(8)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(9)), one: Some(nz(2)) }, + N::Intermediate { zero: Some(nz(10)), one: Some(nz(3)) }, + N::Intermediate { zero: Some(nz(11)), one: Some(nz(4)) }, + N::Intermediate { zero: Some(nz(12)), one: Some(nz(5)) }, + N::Intermediate { zero: Some(nz(14)), one: Some(nz(6)) }, + N::Intermediate { zero: Some(nz(7)), one: Some(nz(13)) }, + N::Leaf(L { range_low: -255, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 3, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 4, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 12, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -256, range_length: 32, is_lower: true, is_out_of_band: false }), + N::Leaf(L { range_low: 76, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table F (Table B.6). +#[rustfmt::skip] +const TABLE_F: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(16)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(6)), one: Some(nz(2)) }, + N::Intermediate { zero: Some(nz(21)), one: Some(nz(3)) }, + N::Intermediate { zero: Some(nz(4)), one: Some(nz(13)) }, + N::Intermediate { zero: Some(nz(5)), one: Some(nz(12)) }, + N::Leaf(L { range_low: -2048, range_length: 10, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(7)), one: Some(nz(10)) }, + N::Intermediate { zero: Some(nz(8)), one: Some(nz(9)) }, + N::Leaf(L { range_low: -1024, range_length: 9, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -512, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(11)), one: Some(nz(15)) }, + N::Leaf(L { range_low: -256, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -128, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(14)), one: Some(nz(24)) }, + N::Leaf(L { range_low: -64, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -32, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(17)), one: Some(nz(18)) }, + N::Leaf(L { range_low: 0, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(19)), one: Some(nz(20)) }, + N::Leaf(L { range_low: 128, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 256, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(22)), one: Some(nz(23)) }, + N::Leaf(L { range_low: 512, range_length: 9, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 1024, range_length: 10, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(25)), one: Some(nz(26)) }, + N::Leaf(L { range_low: -2049, range_length: 32, is_lower: true, is_out_of_band: false }), + N::Leaf(L { range_low: 2048, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table G (Table B.7). +#[rustfmt::skip] +const TABLE_G: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(5)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(2)), one: Some(nz(9)) }, + N::Intermediate { zero: Some(nz(3)), one: Some(nz(14)) }, + N::Intermediate { zero: Some(nz(4)), one: Some(nz(8)) }, + N::Leaf(L { range_low: -1024, range_length: 9, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(6)), one: Some(nz(23)) }, + N::Intermediate { zero: Some(nz(7)), one: Some(nz(22)) }, + N::Leaf(L { range_low: -512, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -256, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(10)), one: Some(nz(17)) }, + N::Intermediate { zero: Some(nz(21)), one: Some(nz(11)) }, + N::Intermediate { zero: Some(nz(12)), one: Some(nz(13)) }, + N::Leaf(L { range_low: -128, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -64, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(15)), one: Some(nz(16)) }, + N::Leaf(L { range_low: -32, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(18)), one: Some(nz(26)) }, + N::Intermediate { zero: Some(nz(19)), one: Some(nz(20)) }, + N::Leaf(L { range_low: 32, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 64, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 128, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 256, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(24)), one: Some(nz(25)) }, + N::Leaf(L { range_low: 512, range_length: 9, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 1024, range_length: 10, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(27)), one: Some(nz(28)) }, + N::Leaf(L { range_low: -1025, range_length: 32, is_lower: true, is_out_of_band: false }), + N::Leaf(L { range_low: 2048, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table H (Table B.8). +#[rustfmt::skip] +const TABLE_H: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(19)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(16)), one: Some(nz(2)) }, + N::Intermediate { zero: Some(nz(21)), one: Some(nz(3)) }, + N::Intermediate { zero: Some(nz(24)), one: Some(nz(4)) }, + N::Intermediate { zero: Some(nz(33)), one: Some(nz(5)) }, + N::Intermediate { zero: Some(nz(14)), one: Some(nz(6)) }, + N::Intermediate { zero: Some(nz(7)), one: Some(nz(9)) }, + N::Intermediate { zero: Some(nz(8)), one: Some(nz(12)) }, + N::Leaf(L { range_low: -15, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(10)), one: Some(nz(37)) }, + N::Intermediate { zero: Some(nz(11)), one: Some(nz(13)) }, + N::Leaf(L { range_low: -7, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -5, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -3, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(15)), one: Some(nz(35)) }, + N::Leaf(L { range_low: -2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(27)), one: Some(nz(17)) }, + N::Intermediate { zero: Some(nz(18)), one: Some(nz(29)) }, + N::Leaf(L { range_low: -1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(20)), one: Some(nz(40)) }, + N::Leaf(L { range_low: 0, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(30)), one: Some(nz(22)) }, + N::Intermediate { zero: Some(nz(23)), one: Some(nz(31)) }, + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(32)), one: Some(nz(25)) }, + N::Intermediate { zero: Some(nz(26)), one: Some(nz(28)) }, + N::Leaf(L { range_low: 3, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 4, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 20, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 22, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 38, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 70, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 134, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(34)), one: Some(nz(36)) }, + N::Leaf(L { range_low: 262, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 390, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 646, range_length: 10, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(38)), one: Some(nz(39)) }, + N::Leaf(L { range_low: -16, range_length: 32, is_lower: true, is_out_of_band: false }), + N::Leaf(L { range_low: 1670, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: true }), + N::Empty, + N::Empty, +]; + +/// Standard Huffman table I (Table B.9). +#[rustfmt::skip] +const TABLE_I: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(19)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(16)), one: Some(nz(2)) }, + N::Intermediate { zero: Some(nz(23)), one: Some(nz(3)) }, + N::Intermediate { zero: Some(nz(26)), one: Some(nz(4)) }, + N::Intermediate { zero: Some(nz(35)), one: Some(nz(5)) }, + N::Intermediate { zero: Some(nz(14)), one: Some(nz(6)) }, + N::Intermediate { zero: Some(nz(7)), one: Some(nz(9)) }, + N::Intermediate { zero: Some(nz(8)), one: Some(nz(12)) }, + N::Leaf(L { range_low: -31, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(10)), one: Some(nz(39)) }, + N::Intermediate { zero: Some(nz(11)), one: Some(nz(13)) }, + N::Leaf(L { range_low: -15, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -11, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -7, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(15)), one: Some(nz(37)) }, + N::Leaf(L { range_low: -5, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(29)), one: Some(nz(17)) }, + N::Intermediate { zero: Some(nz(18)), one: Some(nz(31)) }, + N::Leaf(L { range_low: -3, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(42)), one: Some(nz(20)) }, + N::Intermediate { zero: Some(nz(21)), one: Some(nz(22)) }, + N::Leaf(L { range_low: -1, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 1, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(32)), one: Some(nz(24)) }, + N::Intermediate { zero: Some(nz(25)), one: Some(nz(33)) }, + N::Leaf(L { range_low: 3, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(34)), one: Some(nz(27)) }, + N::Intermediate { zero: Some(nz(28)), one: Some(nz(30)) }, + N::Leaf(L { range_low: 5, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 7, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 39, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 43, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 75, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 139, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 267, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(36)), one: Some(nz(38)) }, + N::Leaf(L { range_low: 523, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 779, range_length: 9, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 1291, range_length: 11, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(40)), one: Some(nz(41)) }, + N::Leaf(L { range_low: -32, range_length: 32, is_lower: true, is_out_of_band: false }), + N::Leaf(L { range_low: 3339, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: true }), +]; + +/// Standard Huffman table J (Table B.10). +#[rustfmt::skip] +const TABLE_J: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(16)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(40)), one: Some(nz(2)) }, + N::Intermediate { zero: Some(nz(13)), one: Some(nz(3)) }, + N::Intermediate { zero: Some(nz(28)), one: Some(nz(4)) }, + N::Intermediate { zero: Some(nz(5)), one: Some(nz(8)) }, + N::Intermediate { zero: Some(nz(35)), one: Some(nz(6)) }, + N::Intermediate { zero: Some(nz(7)), one: Some(nz(12)) }, + N::Leaf(L { range_low: -21, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(22)), one: Some(nz(9)) }, + N::Intermediate { zero: Some(nz(10)), one: Some(nz(37)) }, + N::Intermediate { zero: Some(nz(11)), one: Some(nz(24)) }, + N::Leaf(L { range_low: -5, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -4, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(14)), one: Some(nz(19)) }, + N::Intermediate { zero: Some(nz(15)), one: Some(nz(18)) }, + N::Leaf(L { range_low: -3, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(17)), one: Some(nz(25)) }, + N::Leaf(L { range_low: -2, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(26)), one: Some(nz(20)) }, + N::Intermediate { zero: Some(nz(21)), one: Some(nz(27)) }, + N::Leaf(L { range_low: 3, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(23)), one: Some(nz(36)) }, + N::Leaf(L { range_low: 4, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 5, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 6, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 70, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 102, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(29)), one: Some(nz(32)) }, + N::Intermediate { zero: Some(nz(30)), one: Some(nz(31)) }, + N::Leaf(L { range_low: 134, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 198, range_length: 7, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(33)), one: Some(nz(34)) }, + N::Leaf(L { range_low: 326, range_length: 8, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 582, range_length: 9, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 1094, range_length: 10, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 2118, range_length: 11, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(38)), one: Some(nz(39)) }, + N::Leaf(L { range_low: -22, range_length: 32, is_lower: true, is_out_of_band: false }), + N::Leaf(L { range_low: 4166, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: true }), + N::Empty, + N::Empty, +]; + +/// Standard Huffman table K (Table B.11). +#[rustfmt::skip] +const TABLE_K: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(1)), one: Some(nz(2)) }, + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(3)), one: Some(nz(4)) }, + N::Leaf(L { range_low: 2, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(5)), one: Some(nz(8)) }, + N::Intermediate { zero: Some(nz(6)), one: Some(nz(7)) }, + N::Leaf(L { range_low: 4, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 5, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(9)), one: Some(nz(12)) }, + N::Intermediate { zero: Some(nz(10)), one: Some(nz(11)) }, + N::Leaf(L { range_low: 7, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 9, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(13)), one: Some(nz(18)) }, + N::Intermediate { zero: Some(nz(14)), one: Some(nz(15)) }, + N::Leaf(L { range_low: 13, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(16)), one: Some(nz(17)) }, + N::Leaf(L { range_low: 17, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 21, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(19)), one: Some(nz(22)) }, + N::Intermediate { zero: Some(nz(20)), one: Some(nz(21)) }, + N::Leaf(L { range_low: 29, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 45, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(23)), one: Some(nz(24)) }, + N::Leaf(L { range_low: 77, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 141, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table L (Table B.12). +#[rustfmt::skip] +const TABLE_L: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(1)), one: Some(nz(2)) }, + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(3)), one: Some(nz(4)) }, + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(5)), one: Some(nz(6)) }, + N::Leaf(L { range_low: 3, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(7)), one: Some(nz(10)) }, + N::Intermediate { zero: Some(nz(8)), one: Some(nz(9)) }, + N::Leaf(L { range_low: 5, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 6, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(11)), one: Some(nz(16)) }, + N::Intermediate { zero: Some(nz(12)), one: Some(nz(13)) }, + N::Leaf(L { range_low: 8, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(14)), one: Some(nz(15)) }, + N::Leaf(L { range_low: 10, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 11, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(17)), one: Some(nz(20)) }, + N::Intermediate { zero: Some(nz(18)), one: Some(nz(19)) }, + N::Leaf(L { range_low: 13, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 17, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(21)), one: Some(nz(22)) }, + N::Leaf(L { range_low: 25, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(23)), one: Some(nz(24)) }, + N::Leaf(L { range_low: 41, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 73, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table M (Table B.13). +#[rustfmt::skip] +const TABLE_M: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(1)), one: Some(nz(2)) }, + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(3)), one: Some(nz(5)) }, + N::Intermediate { zero: Some(nz(4)), one: Some(nz(12)) }, + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(6)), one: Some(nz(8)) }, + N::Intermediate { zero: Some(nz(7)), one: Some(nz(11)) }, + N::Leaf(L { range_low: 3, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(9)), one: Some(nz(16)) }, + N::Intermediate { zero: Some(nz(10)), one: Some(nz(13)) }, + N::Leaf(L { range_low: 4, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 5, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 7, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(14)), one: Some(nz(15)) }, + N::Leaf(L { range_low: 15, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 17, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(17)), one: Some(nz(20)) }, + N::Intermediate { zero: Some(nz(18)), one: Some(nz(19)) }, + N::Leaf(L { range_low: 21, range_length: 3, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 29, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(21)), one: Some(nz(22)) }, + N::Leaf(L { range_low: 45, range_length: 5, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(23)), one: Some(nz(24)) }, + N::Leaf(L { range_low: 77, range_length: 6, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 141, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table N (Table B.14). +#[rustfmt::skip] +const TABLE_N: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(5)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(2)), one: Some(nz(6)) }, + N::Intermediate { zero: Some(nz(3)), one: Some(nz(4)) }, + N::Leaf(L { range_low: -2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: -1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(7)), one: Some(nz(8)) }, + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; + +/// Standard Huffman table O (Table B.15). +#[rustfmt::skip] +const TABLE_O: [N; INLINE_TABLE_SIZE] = [ + N::Intermediate { zero: Some(nz(16)), one: Some(nz(1)) }, + N::Intermediate { zero: Some(nz(14)), one: Some(nz(2)) }, + N::Intermediate { zero: Some(nz(12)), one: Some(nz(3)) }, + N::Intermediate { zero: Some(nz(10)), one: Some(nz(4)) }, + N::Intermediate { zero: Some(nz(8)), one: Some(nz(5)) }, + N::Intermediate { zero: Some(nz(6)), one: Some(nz(22)) }, + N::Intermediate { zero: Some(nz(7)), one: Some(nz(21)) }, + N::Leaf(L { range_low: -24, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(9)), one: Some(nz(20)) }, + N::Leaf(L { range_low: -8, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(11)), one: Some(nz(19)) }, + N::Leaf(L { range_low: -4, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(13)), one: Some(nz(18)) }, + N::Leaf(L { range_low: -2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(15)), one: Some(nz(17)) }, + N::Leaf(L { range_low: -1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 0, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 1, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 2, range_length: 0, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 3, range_length: 1, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 5, range_length: 2, is_lower: false, is_out_of_band: false }), + N::Leaf(L { range_low: 9, range_length: 4, is_lower: false, is_out_of_band: false }), + N::Intermediate { zero: Some(nz(23)), one: Some(nz(24)) }, + N::Leaf(L { range_low: -25, range_length: 32, is_lower: true, is_out_of_band: false }), + N::Leaf(L { range_low: 25, range_length: 32, is_lower: false, is_out_of_band: false }), + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, + N::Empty, +]; diff --git a/vendor/hayro-jbig2/src/integer_decoder.rs b/vendor/hayro-jbig2/src/integer_decoder.rs new file mode 100644 index 00000000..8b7656d2 --- /dev/null +++ b/vendor/hayro-jbig2/src/integer_decoder.rs @@ -0,0 +1,118 @@ +use alloc::vec; +use alloc::vec::Vec; + +use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext}; + +/// The integer arithmetic decoder (A.2). +pub(crate) struct IntegerDecoder { + /// `CX` - Context memory for the integer decoder. + contexts: Vec, +} + +impl IntegerDecoder { + /// Create a new integer decoder with fresh contexts. + #[inline(always)] + pub(crate) fn new() -> Self { + Self { + // A.2: "Each arithmetic integer decoding procedure requires 512 bytes of + // storage for its context memory." + contexts: vec![ArithmeticDecoderContext::default(); 512], + } + } + + /// The integer arithmetic decoding procedure (A.2, Figure A.1). + /// + /// Returns `Some(value)` on success, or `None` if OOB (out-of-band) is decoded. + #[inline(always)] + pub(crate) fn decode(&mut self, decoder: &mut ArithmeticDecoder<'_>) -> Option { + // A.2 step 1: "Set: PREV = 1" + // `PREV` - Context prefix, contains bits decoded so far plus a leading 1. + let mut prev: u32 = 1; + + // A.2 step 2: "Follow the flowchart in Figure A.1. Decode each bit with + // CX equal to 'IAx + PREV' where 'IAx' represents the identifier of the + // current arithmetic integer decoding procedure, '+' represents + // concatenation, and the rightmost 9 bits of PREV are used." + + // `S` - Sign bit. + let s = self.decode_bit(decoder, &mut prev); + + // `V` - Magnitude value, decoded according to Figure A.1 flowchart. + #[expect( + clippy::same_functions_in_if_condition, + reason = "each call mutates `prev`" + )] + let v = if self.decode_bit(decoder, &mut prev) == 0 { + // Figure A.1: "V = next 2 bits" + self.decode_n_bits(decoder, &mut prev, 2) + } else if self.decode_bit(decoder, &mut prev) == 0 { + // Figure A.1: "V = (next 4 bits) + 4" + self.decode_n_bits(decoder, &mut prev, 4) + 4 + } else if self.decode_bit(decoder, &mut prev) == 0 { + // Figure A.1: "V = (next 6 bits) + 20" + self.decode_n_bits(decoder, &mut prev, 6) + 20 + } else if self.decode_bit(decoder, &mut prev) == 0 { + // Figure A.1: "V = (next 8 bits) + 84" + self.decode_n_bits(decoder, &mut prev, 8) + 84 + } else if self.decode_bit(decoder, &mut prev) == 0 { + // Figure A.1: "V = (next 12 bits) + 340" + self.decode_n_bits(decoder, &mut prev, 12) + 340 + } else { + // Figure A.1: "V = (next 32 bits) + 4436" + self.decode_n_bits(decoder, &mut prev, 32) + .wrapping_add(4436) + }; + + // A.2: "The result of the integer arithmetic decoding procedure is equal to: + // - V if S = 0 + // - -V if S = 1 and V > 0 + // - OOB if S = 1 and V = 0" + if s == 0 { + Some(v as i32) + } else if v > 0 { + Some(-(v as i32)) + } else { + None + } + } + + /// Decode a single bit and update `PREV` (A.2 step 3). + /// + /// A.2 step 3: "After each bit is decoded: If PREV < 256 set: + /// PREV = (PREV << 1) OR D. Otherwise set: + /// PREV = (((PREV << 1) OR D) AND 511) OR 256" + #[inline(always)] + fn decode_bit(&mut self, decoder: &mut ArithmeticDecoder<'_>, prev: &mut u32) -> u32 { + let ctx_idx = (*prev & 0x1FF) as usize; + // `D` - The just-decoded bit. + let d = decoder.read_bit(&mut self.contexts[ctx_idx]); + + // A.2 step 3: Update PREV. + if *prev < 256 { + *prev = (*prev << 1) | d; + } else { + // A.2: "PREV always contains the values of the eight most-recently- + // decoded bits, plus a leading 1 bit, which is used to indicate the + // number of bits decoded so far." + *prev = (((*prev << 1) | d) & 511) | 256; + } + + d + } + + /// Decode `n` bits and update `PREV` for each. + #[inline(always)] + fn decode_n_bits( + &mut self, + decoder: &mut ArithmeticDecoder<'_>, + prev: &mut u32, + n: usize, + ) -> u32 { + let mut value = 0_u32; + for _ in 0..n { + let bit = self.decode_bit(decoder, prev); + value = (value << 1) | bit; + } + value + } +} diff --git a/vendor/hayro-jbig2/src/integration.rs b/vendor/hayro-jbig2/src/integration.rs new file mode 100644 index 00000000..70df0f42 --- /dev/null +++ b/vendor/hayro-jbig2/src/integration.rs @@ -0,0 +1,151 @@ +//! Integration with the [image] crate. + +use std::{ + ffi::OsStr, + io::{BufRead, Seek}, +}; + +use crate::Image; +use ::image::error::{DecodingError, ImageFormatHint}; +use ::image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult}; +use image::hooks::{decoding_hook_registered, register_format_detection_hook}; + +impl ImageDecoder for Image<'_> { + fn dimensions(&self) -> (u32, u32) { + (self.width(), self.height()) + } + + fn color_type(&self) -> ColorType { + ColorType::L8 + } + + fn original_color_type(&self) -> ExtendedColorType { + ExtendedColorType::L1 + } + + fn read_image(self, buf: &mut [u8]) -> ImageResult<()> + where + Self: Sized, + { + decode_into_buf(&self, buf) + } + + fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { + decode_into_buf(&self, buf) + } +} + +fn decode_into_buf(image: &Image<'_>, buf: &mut [u8]) -> ImageResult<()> { + struct LumaDecoder<'a> { + buf: &'a mut [u8], + pos: usize, + } + + impl crate::Decoder for LumaDecoder<'_> { + fn push_pixel(&mut self, black: bool) { + self.buf[self.pos] = if black { 0 } else { 255 }; + self.pos += 1; + } + + fn push_pixel_chunk(&mut self, black: bool, chunk_count: u32) { + let luma = if black { 0 } else { 255 }; + let count = chunk_count as usize * 8; + self.buf[self.pos..self.pos + count].fill(luma); + self.pos += count; + } + + fn next_line(&mut self) {} + } + + let mut decoder = LumaDecoder { buf, pos: 0 }; + image.decode(&mut decoder)?; + + Ok(()) +} + +/// JBIG2 decoder compatible with `image` decoding hook APIs that pass an `impl BufRead + Seek`. +#[doc(hidden)] +pub struct Jbig2Decoder { + input: Vec, + width: u32, + height: u32, +} + +impl Jbig2Decoder { + /// Create a new decoder that decodes from the stream `r`. + pub fn new(r: R) -> ImageResult { + let mut input = Vec::new(); + let mut r = r; + r.read_to_end(&mut input)?; + + let image = Image::new(&input)?; + + Ok(Self { + width: image.width(), + height: image.height(), + input, + }) + } +} + +impl ImageDecoder for Jbig2Decoder { + fn dimensions(&self) -> (u32, u32) { + (self.width, self.height) + } + + fn color_type(&self) -> ColorType { + ColorType::L8 + } + + fn original_color_type(&self) -> ExtendedColorType { + ExtendedColorType::L1 + } + + fn read_image(self, buf: &mut [u8]) -> ImageResult<()> + where + Self: Sized, + { + let image = Image::new(&self.input)?; + decode_into_buf(&image, buf) + } + + fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { + let image = Image::new(&self.input)?; + decode_into_buf(&image, buf) + } +} + +impl From for DecodingError { + fn from(value: crate::DecodeError) -> Self { + let format = ImageFormatHint::Name("JBIG2".to_owned()); + Self::new(format, value) + } +} + +impl From for ImageError { + fn from(value: crate::DecodeError) -> Self { + Self::Decoding(value.into()) + } +} + +const JBIG2_MAGIC: &[u8] = &[0x97, 0x4A, 0x42, 0x32, 0x0D, 0x0A, 0x1A, 0x0A]; + +/// Registers the decoder with the `image` crate so that non-format-specific calls such as +/// `ImageReader::open("image.jbig2")?.decode()?;` work with JBIG2 files. +/// +/// Returns `true` on success, or `false` if the hook for JBIG2 is already registered. +pub fn register_decoding_hook() -> bool { + if decoding_hook_registered(OsStr::new("jbig2")) { + return false; + } + + for extension in ["jbig2", "jb2"] { + image::hooks::register_decoding_hook( + extension.into(), + Box::new(|r| Ok(Box::new(Jbig2Decoder::new(r)?))), + ); + register_format_detection_hook(extension.into(), JBIG2_MAGIC, None); + } + + true +} diff --git a/vendor/hayro-jbig2/src/lazy.rs b/vendor/hayro-jbig2/src/lazy.rs new file mode 100644 index 00000000..a85501ef --- /dev/null +++ b/vendor/hayro-jbig2/src/lazy.rs @@ -0,0 +1,29 @@ +#[cfg(feature = "std")] +#[derive(Debug)] +pub(crate) struct Lazy(std::cell::OnceCell); + +#[cfg(feature = "std")] +impl Lazy { + pub(crate) fn new(_value: impl FnOnce() -> T) -> Self { + Self(std::cell::OnceCell::new()) + } + + pub(crate) fn get(&self, init: impl FnOnce() -> T) -> &T { + self.0.get_or_init(init) + } +} + +#[cfg(not(feature = "std"))] +#[derive(Debug)] +pub(crate) struct Lazy(T); + +#[cfg(not(feature = "std"))] +impl Lazy { + pub(crate) fn new(value: impl FnOnce() -> T) -> Self { + Self(value()) + } + + pub(crate) fn get(&self, _init: impl FnOnce() -> T) -> &T { + &self.0 + } +} diff --git a/vendor/hayro-jbig2/src/lib.rs b/vendor/hayro-jbig2/src/lib.rs new file mode 100644 index 00000000..ad77a6dd --- /dev/null +++ b/vendor/hayro-jbig2/src/lib.rs @@ -0,0 +1,692 @@ +/*! +A memory-safe, pure-Rust JBIG2 decoder. + +`hayro-jbig2` decodes JBIG2 images as specified in ITU-T T.88 (also known as +ISO/IEC 14492). JBIG2 is a bi-level image compression standard commonly used +in PDF documents for compressing scanned text documents. + +The crate is `no_std` compatible but requires an allocator to be available. + +# Safety +This crate forbids unsafe code via a crate-level attribute. +*/ + +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] +#![allow(missing_docs)] + +extern crate alloc; + +use alloc::vec::Vec; + +use crate::arithmetic_decoder::ArithmeticDecoderContext; + +/// A reusable context for decoding JBIG2 images. +#[derive(Default)] +pub struct DecoderContext { + pub(crate) page_state: PageState, + pub(crate) scratch_buffers: ScratchBuffers, + pub(crate) page_bitmap: Bitmap, +} + +#[derive(Default)] +pub(crate) struct ScratchBuffers { + pub(crate) contexts: Vec, +} + +/// A decoder for JBIG2 images. +pub trait Decoder { + /// Push a single pixel to the output. + fn push_pixel(&mut self, black: bool); + /// Push multiple chunks of 8 pixels of the same color. + /// + /// The `chunk_count` parameter indicates how many 8-pixel chunks to push. + /// For example, if this method is called with `white = true` and + /// `chunk_count = 10`, 80 white pixels are pushed (10 ร— 8 = 80). + /// + /// You can assume that this method is only called if the number of already + /// pushed pixels is a multiple of 8 (i.e. byte-aligned). + fn push_pixel_chunk(&mut self, black: bool, chunk_count: u32); + /// Called when a row has been completed. + fn next_line(&mut self); +} + +#[cfg(feature = "image")] +pub mod integration; + +mod arithmetic_decoder; +mod bitmap; +mod decode; +mod error; +mod file; +mod gray_scale; +mod huffman_table; +mod integer_decoder; +mod lazy; +mod page_info; +mod reader; +mod segment; +mod simd; +mod symbol_id_decoder; + +use error::bail; +pub use error::{ + DecodeError, FormatError, HuffmanError, OverflowError, ParseError, RegionError, Result, + SegmentError, SymbolError, TemplateError, +}; + +use crate::file::parse_segments_sequential; +use bitmap::Bitmap; +use decode::CombinationOperator; +use decode::generic; +use decode::generic_refinement; +use decode::halftone; +use decode::pattern; +use decode::pattern::PatternDictionary; +use decode::symbol; +use decode::symbol::SymbolDictionary; +use decode::text; +use file::parse_file; +use huffman_table::{HuffmanTable, StandardHuffmanTables}; +use page_info::{PageInformation, parse_page_information}; +use reader::Reader; +use segment::SegmentType; + +/// A JBIG2 image. +pub struct Image<'a> { + /// The parsed segments. + segments: Vec>, + /// The width of the image in pixels. + width: u32, + /// The height of the image in pixels. + height: u32, + /// The height determined from `EndOfStripe` segments, if applicable. + height_from_stripes: Option, +} + +impl<'a> Image<'a> { + /// Parse a JBIG2 file from the given data. + /// + /// The file is expected to use the sequential or random-access organization, + /// as defined in Annex D.1 and D.2. + pub fn new(data: &'a [u8]) -> Result { + let file = parse_file(data)?; + Self::from_segments(file.segments) + } + + /// Parse an embedded JBIG2 image with optional global segments. + /// + /// The file is expected to use the embedded organization defined in + /// Annex D.3. + pub fn new_embedded(data: &'a [u8], globals: Option<&'a [u8]>) -> Result { + let mut segments = Vec::new(); + if let Some(globals_data) = globals { + let mut reader = Reader::new(globals_data); + parse_segments_sequential(&mut reader, &mut segments)?; + }; + + let mut reader = Reader::new(data); + parse_segments_sequential(&mut reader, &mut segments)?; + + segments.sort_by_key(|seg| seg.header.segment_number); + + Self::from_segments(segments) + } + + fn from_segments(segments: Vec>) -> Result { + // Pre-scan for stripe height from EndOfStripe segments. + let height_from_stripes = segments + .iter() + .filter(|seg| seg.header.segment_type == SegmentType::EndOfStripe) + .filter_map(|seg| u32::from_be_bytes(seg.data.try_into().ok()?).checked_add(1)) + .max(); + + // Find and parse page information to extract dimensions. + let page_info_seg = segments + .iter() + .find(|s| s.header.segment_type == SegmentType::PageInformation) + .ok_or(FormatError::MissingPageInfo)?; + + let mut reader = Reader::new(page_info_seg.data); + let page_info = parse_page_information(&mut reader)?; + + // "A page's bitmap height may be declared in its page information segment + // to be unknown (by specifying a height of 0xFFFFFFFF). In this case, the + // page must be striped." (7.4.8.2) + let height = if page_info.height == 0xFFFF_FFFF { + height_from_stripes.ok_or(FormatError::UnknownPageHeight)? + } else { + page_info.height + }; + + if page_info.width == 0 || height == 0 { + bail!(FormatError::EmptyPage); + } + + Ok(Self { + segments, + width: page_info.width, + height, + height_from_stripes, + }) + } + + /// The width of the image in pixels. + pub fn width(&self) -> u32 { + self.width + } + + /// The height of the image in pixels. + pub fn height(&self) -> u32 { + self.height + } + + /// Decode the image data through the given [`Decoder`]. + pub fn decode(&self, decoder: &mut D) -> Result<()> { + let mut ctx = DecoderContext::default(); + + self.decode_with(decoder, &mut ctx) + } + + /// Decode the image data through the given [`Decoder`] and [`DecoderContext`]. + /// + /// This is useful in case you want to convert multiple JBIG2 images, + /// as it allows `hayro-jbig2` to reuse allocations during decoding. + pub fn decode_with(&self, decoder: &mut D, ctx: &mut DecoderContext) -> Result<()> { + decode_segments(&self.segments, self.height_from_stripes, ctx)?; + emit_bitmap(&ctx.page_bitmap, decoder); + + Ok(()) + } +} + +fn emit_bitmap(bitmap: &Bitmap, decoder: &mut D) { + let width = bitmap.width; + let bytes_per_row = width.div_ceil(8) as usize; + + for row in bitmap.data.chunks_exact(bitmap.stride as usize) { + let mut x = 0_u32; + let mut chunk_byte: Option = None; + let mut chunk_count = 0_u32; + + let bytes = row.iter().flat_map(|w| w.to_be_bytes()).take(bytes_per_row); + + for byte in bytes { + let remaining = width - x; + + if remaining >= 8 && (byte == 0x00 || byte == 0xFF) { + // Continue the previous chunk. + if chunk_byte == Some(byte) { + chunk_count += 1; + x += 8; + continue; + } + + // Flush previous chunk if any, then start new one. + if let Some(b) = chunk_byte { + decoder.push_pixel_chunk(b == 0xFF, chunk_count); + } + + chunk_byte = Some(byte); + chunk_count = 1; + x += 8; + + continue; + } + + // Can't continue/start chunk, flush any existing chunk first. + if let Some(b) = chunk_byte.take() { + decoder.push_pixel_chunk(b == 0xFF, chunk_count); + chunk_count = 0; + } + + // Emit individual pixels. + let count = remaining.min(8); + for i in 0..count { + decoder.push_pixel((byte >> (7 - i)) & 1 != 0); + } + x += count; + } + + // Flush any remaining chunk at end of row. + if let Some(b) = chunk_byte { + decoder.push_pixel_chunk(b == 0xFF, chunk_count); + } + + decoder.next_line(); + } +} + +fn decode_segments( + segments: &[segment::Segment<'_>], + height_from_stripes: Option, + decoder_ctx: &mut DecoderContext, +) -> Result<()> { + // Find and parse page information segment first. + if let Some(page_info) = segments + .iter() + .find(|s| s.header.segment_type == SegmentType::PageInformation) + { + let mut reader = Reader::new(page_info.data); + init_page( + &mut reader, + height_from_stripes, + &mut decoder_ctx.page_state, + &mut decoder_ctx.page_bitmap, + )?; + } else { + bail!(FormatError::MissingPageInfo); + } + + let page_bitmap = &mut decoder_ctx.page_bitmap; + let page_state = &mut decoder_ctx.page_state; + let scratch_buffers = &mut decoder_ctx.scratch_buffers; + + // Process all segments. + for seg in segments { + let mut reader = Reader::new(seg.data); + + match seg.header.segment_type { + SegmentType::PageInformation => { + // Already processed above, skip. + } + SegmentType::ImmediateGenericRegion | SegmentType::ImmediateLosslessGenericRegion => { + let had_unknown_length = seg.header.data_length.is_none(); + let header = generic::parse(&mut reader, had_unknown_length)?; + + if page_state.can_decode_directly(page_bitmap, &header.region_info, false) { + generic::decode_into(&header, page_bitmap, scratch_buffers)?; + } else { + let region = generic::decode(&header, scratch_buffers)?; + page_bitmap.combine( + ®ion.bitmap, + region.bitmap.x_location as i32, + region.bitmap.y_location as i32, + region.combination_operator, + ); + } + page_state.page_pristine = false; + } + SegmentType::IntermediateGenericRegion => { + // Intermediate segments cannot have unknown length. + let header = generic::parse(&mut reader, false)?; + let region = generic::decode(&header, scratch_buffers)?; + page_state.store_region(seg.header.segment_number, region.bitmap); + } + SegmentType::PatternDictionary => { + let header = pattern::parse(&mut reader)?; + let dictionary = pattern::decode(&header, scratch_buffers)?; + page_state.store_pattern_dictionary(seg.header.segment_number, dictionary); + } + SegmentType::SymbolDictionary => { + // "1) Concatenate all the input symbol dictionaries to form SDINSYMS." + // (6.5.5, step 1) + // Collect references to avoid cloning; symbols are only cloned if re-exported. + let input_symbols: Vec<&Bitmap> = seg + .header + .referred_to_segments + .iter() + .filter_map(|&num| page_state.get_symbol_dictionary(num)) + .flat_map(|dict| dict.exported_symbols.iter()) + .collect(); + + // Collect Huffman tables from referred table segments. + let referred_tables: Vec = seg + .header + .referred_to_segments + .iter() + .filter_map(|&num| page_state.get_huffman_table(num)) + .cloned() + .collect(); + + // Get retained contexts from the last referred symbol dictionary (7.4.2.2 step 3). + let retained_contexts = seg + .header + .referred_to_segments + .last() + .and_then(|&num| page_state.get_symbol_dictionary(num)) + .and_then(|dict| dict.retained_contexts.as_ref()); + + let header = symbol::parse(&mut reader)?; + let dictionary = symbol::decode( + &header, + &input_symbols, + &referred_tables, + &page_state.standard_tables, + retained_contexts, + )?; + page_state.store_symbol_dictionary(seg.header.segment_number, dictionary); + } + SegmentType::ImmediateTextRegion | SegmentType::ImmediateLosslessTextRegion => { + // Collect symbols from referred symbol dictionaries (SBSYMS). + let symbols: Vec<&Bitmap> = seg + .header + .referred_to_segments + .iter() + .filter_map(|&num| page_state.get_symbol_dictionary(num)) + .flat_map(|dict| dict.exported_symbols.iter()) + .collect(); + + // Collect Huffman tables from referred table segments. + // "These user-supplied Huffman decoding tables may be supplied either + // as a Tables segment..." (7.4.3.1.6) + let referred_tables: Vec = seg + .header + .referred_to_segments + .iter() + .filter_map(|&num| page_state.get_huffman_table(num)) + .cloned() + .collect(); + + let header = text::parse(&mut reader, symbols.len() as u32)?; + + if page_state.can_decode_directly( + page_bitmap, + &header.region_info, + header.flags.default_pixel, + ) { + text::decode_into( + &header, + &symbols, + &referred_tables, + &page_state.standard_tables, + page_bitmap, + scratch_buffers, + )?; + } else { + let region = text::decode( + &header, + &symbols, + &referred_tables, + &page_state.standard_tables, + scratch_buffers, + )?; + page_bitmap.combine( + ®ion.bitmap, + region.bitmap.x_location as i32, + region.bitmap.y_location as i32, + region.combination_operator, + ); + } + page_state.page_pristine = false; + } + SegmentType::IntermediateTextRegion => { + // Collect symbols from referred symbol dictionaries (SBSYMS). + let symbols: Vec<&Bitmap> = seg + .header + .referred_to_segments + .iter() + .filter_map(|&num| page_state.get_symbol_dictionary(num)) + .flat_map(|dict| dict.exported_symbols.iter()) + .collect(); + + // Collect Huffman tables from referred table segments. + let referred_tables: Vec = seg + .header + .referred_to_segments + .iter() + .filter_map(|&num| page_state.get_huffman_table(num)) + .cloned() + .collect(); + + let header = text::parse(&mut reader, symbols.len() as u32)?; + let region = text::decode( + &header, + &symbols, + &referred_tables, + &page_state.standard_tables, + scratch_buffers, + )?; + page_state.store_region(seg.header.segment_number, region.bitmap); + } + SegmentType::ImmediateHalftoneRegion | SegmentType::ImmediateLosslessHalftoneRegion => { + let pattern_dict = seg + .header + .referred_to_segments + .first() + .and_then(|&num| page_state.get_pattern_dictionary(num)) + .ok_or(SegmentError::MissingPatternDictionary)?; + + let header = halftone::parse(&mut reader)?; + + if page_state.can_decode_directly( + page_bitmap, + &header.region_info, + header.flags.initial_pixel_color, + ) { + halftone::decode_into(&header, pattern_dict, page_bitmap, scratch_buffers)?; + } else { + let region = halftone::decode(&header, pattern_dict, scratch_buffers)?; + page_bitmap.combine( + ®ion.bitmap, + region.bitmap.x_location as i32, + region.bitmap.y_location as i32, + region.combination_operator, + ); + } + page_state.page_pristine = false; + } + SegmentType::IntermediateHalftoneRegion => { + let pattern_dict = seg + .header + .referred_to_segments + .first() + .and_then(|&num| page_state.get_pattern_dictionary(num)) + .ok_or(SegmentError::MissingPatternDictionary)?; + + let header = halftone::parse(&mut reader)?; + let region = halftone::decode(&header, pattern_dict, scratch_buffers)?; + page_state.store_region(seg.header.segment_number, region.bitmap); + } + SegmentType::IntermediateGenericRefinementRegion => { + // Same logic as immediate refinement, but store result instead of combining. + let reference = seg + .header + .referred_to_segments + .first() + .and_then(|&num| page_state.get_referred_segment(num)) + .unwrap_or(page_bitmap); + + let header = generic_refinement::parse(&mut reader)?; + let region = generic_refinement::decode(&header, reference, scratch_buffers)?; + page_state.store_region(seg.header.segment_number, region.bitmap); + } + SegmentType::ImmediateGenericRefinementRegion + | SegmentType::ImmediateLosslessGenericRefinementRegion => { + // "3) Determine the buffer associated with the region segment that + // this segment refers to." (7.4.7.5) + // + // "2) If there are no referred-to segments, then use the page + // bitmap as the reference buffer." (7.4.7.5) + let referred_segment = seg + .header + .referred_to_segments + .first() + .and_then(|&num| page_state.get_referred_segment(num)); + + let header = generic_refinement::parse(&mut reader)?; + + if let Some(referred_segment) = referred_segment + && page_state.can_decode_directly(page_bitmap, &header.region_info, false) + { + generic_refinement::decode_into( + &header, + referred_segment, + page_bitmap, + scratch_buffers, + )?; + } else { + let reference = referred_segment.unwrap_or(page_bitmap); + let region = generic_refinement::decode(&header, reference, scratch_buffers)?; + page_bitmap.combine( + ®ion.bitmap, + region.bitmap.x_location as i32, + region.bitmap.y_location as i32, + region.combination_operator, + ); + } + page_state.page_pristine = false; + } + SegmentType::Tables => { + // "Tables โ€“ see 7.4.13." (type 53) + // "This segment contains data which defines one or more user-supplied + // Huffman coding tables." (7.4.13) + let table = HuffmanTable::read_custom(&mut reader)?; + page_state.store_huffman_table(seg.header.segment_number, table); + } + SegmentType::EndOfPage | SegmentType::EndOfFile => { + break; + } + // Other segment types not yet implemented. + _ => {} + } + } + + Ok(()) +} + +/// Page-level decoding state for a JBIG2 page. +#[derive(Default)] +pub(crate) struct PageState { + /// The parsed page information. + pub(crate) page_info: PageInformation, + /// Whether the page bitmap is still in its initial state (not yet painted to). + pub(crate) page_pristine: bool, + /// Decoded intermediate regions, stored as (`segment_number`, region) pairs. + pub(crate) referred_segments: Vec<(u32, Bitmap)>, + /// Decoded pattern dictionaries, stored as (`segment_number`, dictionary) pairs. + pub(crate) pattern_dictionaries: Vec<(u32, PatternDictionary)>, + /// Decoded symbol dictionaries, stored as (`segment_number`, dictionary) pairs. + pub(crate) symbol_dictionaries: Vec<(u32, SymbolDictionary)>, + /// Decoded Huffman tables from table segments, stored as (`segment_number`, table) pairs. + /// "Tables โ€“ see 7.4.13." (type 53) + pub(crate) huffman_tables: Vec<(u32, HuffmanTable)>, + /// Standard Huffman tables (`TABLE_A` through `TABLE_O`). + pub(crate) standard_tables: StandardHuffmanTables, +} + +impl PageState { + fn reset(&mut self, page_info: PageInformation) { + self.page_info = page_info; + self.page_pristine = true; + self.referred_segments.clear(); + self.pattern_dictionaries.clear(); + self.symbol_dictionaries.clear(); + self.huffman_tables.clear(); + // Standard tables are lazily built and reused across images. + } + + /// Check if an immediate region can be decoded directly into the page bitmap. + fn can_decode_directly( + &self, + page_bitmap: &Bitmap, + region_info: &decode::RegionSegmentInfo, + region_default_pixel: bool, + ) -> bool { + if !self.page_pristine { + return false; + } + + let covers_page = region_info.x_location == 0 + && region_info.y_location == 0 + && region_info.width == page_bitmap.width + && region_info.height == page_bitmap.height; + + if !covers_page { + return false; + } + + let page_default_is_zero = self.page_info.flags.default_pixel == 0; + + if region_default_pixel == page_default_is_zero { + return false; + } + + let op = region_info.combination_operator; + match op { + CombinationOperator::Replace => true, + CombinationOperator::Or | CombinationOperator::Xor => page_default_is_zero, + CombinationOperator::And | CombinationOperator::Xnor => !page_default_is_zero, + } + } + + /// Store a decoded region for later reference. + fn store_region(&mut self, segment_number: u32, region: Bitmap) { + self.referred_segments.push((segment_number, region)); + } + + /// Look up a referred segment by number. + fn get_referred_segment(&self, segment_number: u32) -> Option<&Bitmap> { + self.referred_segments + .binary_search_by_key(&segment_number, |(num, _)| *num) + .ok() + .map(|idx| &self.referred_segments[idx].1) + } + + /// Store a decoded pattern dictionary for later reference. + fn store_pattern_dictionary(&mut self, segment_number: u32, dictionary: PatternDictionary) { + self.pattern_dictionaries.push((segment_number, dictionary)); + } + + /// Look up a pattern dictionary by segment number. + fn get_pattern_dictionary(&self, segment_number: u32) -> Option<&PatternDictionary> { + self.pattern_dictionaries + .binary_search_by_key(&segment_number, |(num, _)| *num) + .ok() + .map(|idx| &self.pattern_dictionaries[idx].1) + } + + /// Store a decoded symbol dictionary for later reference. + fn store_symbol_dictionary(&mut self, segment_number: u32, dictionary: SymbolDictionary) { + self.symbol_dictionaries.push((segment_number, dictionary)); + } + + /// Look up a symbol dictionary by segment number. + fn get_symbol_dictionary(&self, segment_number: u32) -> Option<&SymbolDictionary> { + self.symbol_dictionaries + .binary_search_by_key(&segment_number, |(num, _)| *num) + .ok() + .map(|idx| &self.symbol_dictionaries[idx].1) + } + + /// Store a decoded Huffman table for later reference. + fn store_huffman_table(&mut self, segment_number: u32, table: HuffmanTable) { + self.huffman_tables.push((segment_number, table)); + } + + /// Look up a Huffman table by segment number. + fn get_huffman_table(&self, segment_number: u32) -> Option<&HuffmanTable> { + self.huffman_tables + .binary_search_by_key(&segment_number, |(num, _)| *num) + .ok() + .map(|idx| &self.huffman_tables[idx].1) + } +} + +/// Parse page information and initialize the page bitmap. +fn init_page( + reader: &mut Reader<'_>, + height_from_stripes: Option, + page: &mut PageState, + bitmap: &mut Bitmap, +) -> Result<()> { + let page_info = parse_page_information(reader)?; + + // "A page's bitmap height may be declared in its page information segment + // to be unknown (by specifying a height of 0xFFFFFFFF). In this case, the + // page must be striped." (7.4.8.2) + let height = if page_info.height == 0xFFFF_FFFF { + height_from_stripes.ok_or(FormatError::UnknownPageHeight)? + } else { + page_info.height + }; + + // "Bit 2: Page default pixel value. This bit contains the initial value + // for every pixel in the page, before any region segments are decoded + // or drawn." (7.4.8.5) + bitmap.reinitialize(page_info.width, height, page_info.flags.default_pixel != 0)?; + + page.reset(page_info); + + Ok(()) +} diff --git a/vendor/hayro-jbig2/src/page_info.rs b/vendor/hayro-jbig2/src/page_info.rs new file mode 100644 index 00000000..cc0c89c0 --- /dev/null +++ b/vendor/hayro-jbig2/src/page_info.rs @@ -0,0 +1,150 @@ +//! Page information segment parsing (7.4.8). + +use crate::decode::CombinationOperator; +use crate::error::{ParseError, Result}; +use crate::reader::Reader; + +/// Parsed page information segment (7.4.8). +#[derive(Debug, Clone, Default)] +pub(crate) struct PageInformation { + /// "This is a four-byte value containing the width in pixels of the page's + /// bitmap." (7.4.8.1) + pub(crate) width: u32, + /// "This is a four-byte value containing the height in pixels of the page's + /// bitmap." (7.4.8.2) + pub(crate) height: u32, + /// "This is a four-byte value containing the resolution of the original page + /// medium, measured in pixels/metre in the horizontal direction. If this + /// value is unknown, then this field must contain 0x00000000." (7.4.8.3) + /// + /// `None` means unknown resolution. + pub(crate) _x_resolution: Option, + /// "This is a four-byte value containing the resolution of the original page + /// medium, measured in pixels/metre in the vertical direction. If this value + /// is unknown, then this field must contain 0x00000000." (7.4.8.4) + /// + /// `None` means unknown resolution. + pub(crate) _y_resolution: Option, + /// Page segment flags (7.4.8.5). + pub(crate) flags: PageFlags, + /// Page striping information (7.4.8.6). + pub(crate) _striping: PageStriping, +} + +/// Page segment flags (7.4.8.5, Figure 56). +#[derive(Debug, Clone)] +#[allow(unused)] +#[derive(Default)] +pub(crate) struct PageFlags { + /// "Bit 0: Page is eventually lossless. If this bit is 0, then the file does + /// not contain a lossless representation of the original (pre-coding) page. + /// If this bit is 1, then the file contains enough information to reconstruct + /// the original page." (7.4.8.5) + pub(crate) is_lossless: bool, + /// "Bit 1: Page might contain refinements. If this bit is 0, then no + /// refinement region segment may be associated with the page. If this bit + /// is 1, then such segments may be associated with the page." (7.4.8.5) + pub(crate) might_contain_refinements: bool, + /// "Bit 2: Page default pixel value. This bit contains the initial value for + /// every pixel in the page, before any region segments are decoded or drawn." + /// (7.4.8.5) + pub(crate) default_pixel: u8, + /// "Bits 3-4: Page default combination operator." (7.4.8.5) + pub(crate) default_combination_operator: CombinationOperator, + /// "Bit 5: Page requires auxiliary buffers. If this bit is 0, then no region + /// segment requiring an auxiliary buffer may be associated with the page." + /// (7.4.8.5) + pub(crate) requires_auxiliary_buffers: bool, + /// "Bit 6: Page combination operator overridden. If this bit is 0, then every + /// direct region segment associated with this page must use the page's default + /// combination operator. If this bit is 1, then direct region segments + /// associated with this page may use any combination operators." (7.4.8.5) + pub(crate) combination_operator_overridden: bool, + /// "Bit 7: Page might contain coloured segment. If this bit is 0, then no + /// segment with colour extension may be associated with the page." (7.4.8.5) + pub(crate) might_contain_coloured: bool, +} + +/// Page striping information (7.4.8.6, Figure 57). +#[derive(Debug, Clone, Default)] +pub(crate) struct PageStriping { + /// "Bit 15: Page is striped. If the 'page is striped' bit is 1, then the page + /// may have end of stripe segments associated with it." (7.4.8.6) + /// + /// "If the page's bitmap height is unknown (indicated by a page bitmap height + /// of 0xFFFFFFFF) then the 'page is striped' bit must be 1." (7.4.8.6) + pub(crate) _is_striped: bool, + /// "Bits 0-14: Maximum stripe size." (7.4.8.6) + /// + /// "The maximum size of each stripe (the distance between an end of stripe + /// segment's end row and the end row of the previous end of stripe segment, + /// or 0 in the case of the first end of stripe segment) must be no more than + /// the page's maximum stripe size." (7.4.8.6) + pub(crate) _max_stripe_size: u16, +} + +/// Parse a page information segment (7.4.8). +pub(crate) fn parse_page_information(reader: &mut Reader<'_>) -> Result { + // 7.4.8.1: Page bitmap width + let width = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + + // 7.4.8.2: Page bitmap height + let height = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + + // 7.4.8.3: Page X resolution + let x_resolution_raw = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + let x_resolution = if x_resolution_raw == 0 { + None + } else { + Some(x_resolution_raw) + }; + + // 7.4.8.4: Page Y resolution + let y_resolution_raw = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + let y_resolution = if y_resolution_raw == 0 { + None + } else { + Some(y_resolution_raw) + }; + + // 7.4.8.5: Page segment flags + let flags_byte = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + let flags = parse_page_flags(flags_byte); + + // 7.4.8.6: Page striping information + let striping_raw = reader.read_u16().ok_or(ParseError::UnexpectedEof)?; + let striping = PageStriping { + _is_striped: striping_raw & 0x8000 != 0, + _max_stripe_size: striping_raw & 0x7FFF, + }; + + Ok(PageInformation { + width, + height, + _x_resolution: x_resolution, + _y_resolution: y_resolution, + flags, + _striping: striping, + }) +} + +fn parse_page_flags(flags: u8) -> PageFlags { + let combo_bits = (flags >> 3) & 0x03; + let default_combination_operator = match combo_bits { + 0 => CombinationOperator::Or, + 1 => CombinationOperator::And, + 2 => CombinationOperator::Xor, + 3 => CombinationOperator::Xnor, + _ => unreachable!(), + }; + + PageFlags { + is_lossless: flags & 0x01 != 0, + might_contain_refinements: flags & 0x02 != 0, + default_pixel: (flags >> 2) & 0x01, + default_combination_operator, + requires_auxiliary_buffers: flags & 0x20 != 0, + combination_operator_overridden: flags & 0x40 != 0, + might_contain_coloured: flags & 0x80 != 0, + } +} diff --git a/vendor/hayro-jbig2/src/reader.rs b/vendor/hayro-jbig2/src/reader.rs new file mode 100644 index 00000000..418915ca --- /dev/null +++ b/vendor/hayro-jbig2/src/reader.rs @@ -0,0 +1,173 @@ +/// A reader for reading bits and bytes from a byte stream. +#[derive(Debug, Clone)] +pub(crate) struct Reader<'a> { + /// The underlying data. + data: &'a [u8], + /// The position in bits. + cur_pos: usize, +} + +impl<'a> Reader<'a> { + #[inline(always)] + pub(crate) fn new(data: &'a [u8]) -> Self { + Self { data, cur_pos: 0 } + } + + #[inline(always)] + pub(crate) fn align(&mut self) { + let bit_pos = self.bit_pos(); + + if !bit_pos.is_multiple_of(8) { + self.cur_pos += 8 - bit_pos; + } + } + + #[inline(always)] + pub(crate) fn at_end(&self) -> bool { + self.byte_pos() >= self.data.len() + } + + #[inline(always)] + pub(crate) fn tail(&self) -> Option<&'a [u8]> { + self.data.get(self.byte_pos()..) + } + + /// Read the given number of bytes. + /// + /// Assumes that the reader is currently byte-aligned. + #[inline(always)] + pub(crate) fn read_bytes(&mut self, len: usize) -> Option<&'a [u8]> { + debug_assert_eq!(self.bit_pos(), 0); + + let bytes = self.peek_bytes(len)?; + self.cur_pos += len * 8; + + Some(bytes) + } + + /// Read a single byte. + /// + /// Assumes that the reader is currently byte-aligned. + #[inline(always)] + pub(crate) fn read_byte(&mut self) -> Option { + debug_assert_eq!(self.bit_pos(), 0); + + let byte = self.peek_byte()?; + self.cur_pos += 8; + + Some(byte) + } + + /// Read a single byte, returning `None` if it's zero. + #[inline(always)] + pub(crate) fn read_nonzero_byte(&mut self) -> Option { + let byte = self.read_byte()?; + + if byte == 0 { None } else { Some(byte) } + } + + /// Skip the given number of bytes. + /// + /// Assumes that the reader is currently byte-aligned. + #[inline(always)] + pub(crate) fn skip_bytes(&mut self, len: usize) -> Option<()> { + debug_assert_eq!(self.bit_pos(), 0); + + self.read_bytes(len).map(|_| ()) + } + + /// Peek the given number of bytes. + /// + /// Assumes that the reader is currently byte-aligned. + #[inline(always)] + pub(crate) fn peek_bytes(&self, len: usize) -> Option<&'a [u8]> { + debug_assert_eq!(self.bit_pos(), 0); + + let start = self.byte_pos(); + let end = start.checked_add(len)?; + self.data.get(start..end) + } + + /// Peek the next byte. + /// + /// Assumes that the reader is currently byte-aligned. + #[inline(always)] + pub(crate) fn peek_byte(&self) -> Option { + debug_assert_eq!(self.bit_pos(), 0); + + self.data.get(self.byte_pos()).copied() + } + + /// Read an u16 number. + /// + /// Assumes that the reader is currently byte-aligned. + #[inline(always)] + pub(crate) fn read_u16(&mut self) -> Option { + Some(u16::from_be_bytes(self.read_bytes(2)?.try_into().ok()?)) + } + + /// Read an u32 number. + /// + /// Assumes that the reader is currently byte-aligned. + #[inline(always)] + pub(crate) fn read_u32(&mut self) -> Option { + Some(u32::from_be_bytes(self.read_bytes(4)?.try_into().ok()?)) + } + + /// Read an i32 number. + /// + /// Assumes that the reader is currently byte-aligned. + #[inline(always)] + pub(crate) fn read_i32(&mut self) -> Option { + Some(i32::from_be_bytes(self.read_bytes(4)?.try_into().ok()?)) + } + + #[inline(always)] + pub(crate) fn read_bit(&mut self) -> Option { + let byte = self.cur_byte()?; + let shift = 7 - (self.bit_pos()); + self.cur_pos += 1; + Some((byte >> shift) & 1) + } + + #[inline(always)] + pub(crate) fn read_bits(&mut self, count: u8) -> Option { + debug_assert!(count <= 32); + + let mut value = 0_u32; + let mut remaining = count; + + while remaining > 0 { + let bit_offset = self.bit_pos(); + let byte = self.cur_byte()? as u32; + + let available = (8 - bit_offset) as u8; + let take = remaining.min(available); + + let shift = available - take; + let mask = (1 << take) - 1; + let bits = (byte >> shift) & mask; + + value = (value << take) | bits; + self.cur_pos += take as usize; + remaining -= take; + } + + Some(value) + } + + #[inline(always)] + pub(crate) fn byte_pos(&self) -> usize { + self.cur_pos >> 3 + } + + #[inline(always)] + pub(crate) fn cur_byte(&self) -> Option { + self.data.get(self.byte_pos()).copied() + } + + #[inline(always)] + pub(crate) fn bit_pos(&self) -> usize { + self.cur_pos & 7 + } +} diff --git a/vendor/hayro-jbig2/src/segment.rs b/vendor/hayro-jbig2/src/segment.rs new file mode 100644 index 00000000..35c17c38 --- /dev/null +++ b/vendor/hayro-jbig2/src/segment.rs @@ -0,0 +1,419 @@ +//! Segment parsing for JBIG2 bitstreams (Section 7.2). +//! +//! This module handles parsing of individual segment headers and defines +//! the segment types used in JBIG2. + +use alloc::vec::Vec; + +use crate::error::{ParseError, Result, SegmentError, bail, err}; +use crate::reader::Reader; + +/// "The segment type is a number between 0 and 63, inclusive. Not all values +/// are allowed." (7.3) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SegmentType { + /// Symbol dictionary โ€“ see 7.4.2. (type 0) + SymbolDictionary, + /// Intermediate text region โ€“ see 7.4.3. (type 4) + IntermediateTextRegion, + /// Immediate text region โ€“ see 7.4.3. (type 6) + ImmediateTextRegion, + /// Immediate lossless text region โ€“ see 7.4.3. (type 7) + ImmediateLosslessTextRegion, + /// Pattern dictionary โ€“ see 7.4.4. (type 16) + PatternDictionary, + /// Intermediate halftone region โ€“ see 7.4.5. (type 20) + IntermediateHalftoneRegion, + /// Immediate halftone region โ€“ see 7.4.5. (type 22) + ImmediateHalftoneRegion, + /// Immediate lossless halftone region โ€“ see 7.4.5. (type 23) + ImmediateLosslessHalftoneRegion, + /// Intermediate generic region โ€“ see 7.4.6. (type 36) + IntermediateGenericRegion, + /// Immediate generic region โ€“ see 7.4.6. (type 38) + ImmediateGenericRegion, + /// Immediate lossless generic region โ€“ see 7.4.6. (type 39) + ImmediateLosslessGenericRegion, + /// Intermediate generic refinement region โ€“ see 7.4.7. (type 40) + IntermediateGenericRefinementRegion, + /// Immediate generic refinement region โ€“ see 7.4.7. (type 42) + ImmediateGenericRefinementRegion, + /// Immediate lossless generic refinement region โ€“ see 7.4.7. (type 43) + ImmediateLosslessGenericRefinementRegion, + /// Page information โ€“ see 7.4.8. (type 48) + PageInformation, + /// End of page โ€“ see 7.4.9. (type 49) + EndOfPage, + /// End of stripe โ€“ see 7.4.10. (type 50) + EndOfStripe, + /// End of file โ€“ see 7.4.11. (type 51) + EndOfFile, + /// Profiles โ€“ see 7.4.12. (type 52) + Profiles, + /// Tables โ€“ see 7.4.13. (type 53) + Tables, + /// Colour palette โ€“ see 7.4.16. (type 54) + ColourPalette, + /// Extension - see 7.4.14. (type 62) + Extension, +} + +impl SegmentType { + /// "All other segment types are reserved and must not be used." (7.3) + fn from_type_value(value: u8) -> Result { + match value { + 0 => Ok(Self::SymbolDictionary), + 4 => Ok(Self::IntermediateTextRegion), + 6 => Ok(Self::ImmediateTextRegion), + 7 => Ok(Self::ImmediateLosslessTextRegion), + 16 => Ok(Self::PatternDictionary), + 20 => Ok(Self::IntermediateHalftoneRegion), + 22 => Ok(Self::ImmediateHalftoneRegion), + 23 => Ok(Self::ImmediateLosslessHalftoneRegion), + 36 => Ok(Self::IntermediateGenericRegion), + 38 => Ok(Self::ImmediateGenericRegion), + 39 => Ok(Self::ImmediateLosslessGenericRegion), + 40 => Ok(Self::IntermediateGenericRefinementRegion), + 42 => Ok(Self::ImmediateGenericRefinementRegion), + 43 => Ok(Self::ImmediateLosslessGenericRefinementRegion), + 48 => Ok(Self::PageInformation), + 49 => Ok(Self::EndOfPage), + 50 => Ok(Self::EndOfStripe), + 51 => Ok(Self::EndOfFile), + 52 => Ok(Self::Profiles), + 53 => Ok(Self::Tables), + 54 => Ok(Self::ColourPalette), + 62 => Ok(Self::Extension), + _ => err!(SegmentError::UnknownType), + } + } +} + +/// A parsed segment header (7.2.1). +#[derive(Debug, Clone)] +pub(crate) struct SegmentHeader { + /// "This four-byte field contains the segment's segment number. The valid + /// range of segment numbers is 0 through 4294967295 (0xFFFFFFFF) inclusive." + /// (7.2.2) + pub(crate) segment_number: u32, + /// "Bits 0-5: Segment type. See 7.3." (7.2.3) + pub(crate) segment_type: SegmentType, + /// "Bit 7: Deferred non-retain. If this bit is 1, this segment is flagged + /// as retained only by itself and its attached extension segments." (7.2.3) + pub(crate) _retain_flag: bool, + /// "This field encodes the number of the page to which this segment belongs. + /// The first page must be numbered '1'. This field may contain a value of + /// zero; this value indicates that this segment is not associated with any + /// page." (7.2.6) + pub(crate) _page_association: u32, + /// "This field contains the segment numbers of the segments that this segment + /// refers to, if any." (7.2.5) + pub(crate) referred_to_segments: Vec, + /// "This 4-byte field contains the length of the segment's segment data part, + /// in bytes." (7.2.7) + /// + /// `None` means the length was unknown (0xFFFFFFFF), which is only valid for + /// immediate generic region segments in sequential organization. + pub(crate) data_length: Option, +} + +/// A parsed segment with its header and data. +#[derive(Debug)] +pub(crate) struct Segment<'a> { + /// The segment header. + pub(crate) header: SegmentHeader, + /// The segment data (borrowed slice). + pub(crate) data: &'a [u8], +} + +/// Parse a segment header (7.2). +pub(crate) fn parse_segment_header(reader: &mut Reader<'_>) -> Result { + // 7.2.2: Segment number + // "This four-byte field contains the segment's segment number. The valid + // range of segment numbers is 0 through 4294967295 (0xFFFFFFFF) inclusive. + // As mentioned before, it is possible for there to be gaps in the segment + // numbering." + let segment_number = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + + // 7.2.3: Segment header flags + // "This is a 1-byte field." + let flags = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + + // "Bits 0-5: Segment type. See 7.3." + let segment_type = SegmentType::from_type_value(flags & 0x3F)?; + + // "Bit 6: Page association field size. See 7.2.6." + let page_association_long = flags & 0x40 != 0; + + // "Bit 7: Deferred non-retain. If this bit is 1, this segment is flagged as + // retained only by itself and its attached extension segments." + let retain_flag = flags & 0x80 == 0; + + // 7.2.4: Referred-to segment count and retention flags + // "This field contains one or more bytes indicating how many other segments + // are referred to by this segment, and which segments contain data that is + // needed after this segment." + // + // "The three most significant bits of the first byte in this field determine + // the length of the field. If the value of this three-bit subfield is between + // 0 and 4, then the field is one byte long. If the value of this three-bit + // subfield is 7, then the field is at least five bytes long. This three-bit + // subfield must not contain values of 5 and 6." + let count_and_retention = reader.read_byte().ok_or(ParseError::UnexpectedEof)?; + let short_count = (count_and_retention >> 5) & 0x07; + + if short_count == 5 || short_count == 6 { + bail!(SegmentError::InvalidReferredCount); + } + + let referred_to_count = if short_count < 7 { + // Short form: "Bits 5-7: Count of referred-to segments. This field may + // take on values between zero and four." + short_count as u32 + } else { + // Long form: "In the case where the field is in the long format (at least + // five bytes long), it is composed of an initial four-byte field, followed + // by a succession of one-byte fields." + // + // "Bits 0-28: Count of referred-to segments. This specifies the number of + // segments that this segment refers to." + // "Bits 29-31: Indication of long-form format. This field must contain the + // value 7." + let rest = reader.read_bytes(3).ok_or(ParseError::UnexpectedEof)?; + u32::from_be_bytes([count_and_retention & 0x1F, rest[0], rest[1], rest[2]]) + }; + + // Skip retention flag bytes in long form. + // "The first one-byte field following the initial four-byte field is formatted + // as follows: Bit 0: Retain bit for this segment. Bit 1-7: Retain bits for + // referred-to segments." + if short_count == 7 { + // Number of retention bytes: ceil((referred_to_count + 1) / 8) + let retention_bytes = (referred_to_count as usize + 1).div_ceil(8); + reader + .skip_bytes(retention_bytes) + .ok_or(ParseError::UnexpectedEof)?; + } + + // 7.2.5: Referred-to segment numbers + // "When the current segment's number is 256 or less, then each referred-to + // segment number is one byte long. Otherwise, when the current segment's + // number is 65536 or less, each referred-to segment number is two bytes long. + // Otherwise, each referred-to segment number is four bytes long." + let mut referred_to_segments = Vec::with_capacity(referred_to_count as usize); + for _ in 0..referred_to_count { + let referred = if segment_number <= 256 { + reader.read_byte().ok_or(ParseError::UnexpectedEof)? as u32 + } else if segment_number <= 65536 { + reader.read_u16().ok_or(ParseError::UnexpectedEof)? as u32 + } else { + reader.read_u32().ok_or(ParseError::UnexpectedEof)? + }; + + // If a segment refers to other segments, it must refer to only segments + // with lower segment numbers. + if referred >= segment_number { + bail!(SegmentError::InvalidReference); + } + + referred_to_segments.push(referred); + } + + // 7.2.6: Segment page association + // "This field is one byte long if this segment's page association field size + // flag bit is 0, and is four bytes long if this segment's page association + // field size flag bit is 1." + let page_association = if page_association_long { + reader.read_u32().ok_or(ParseError::UnexpectedEof)? + } else { + reader.read_byte().ok_or(ParseError::UnexpectedEof)? as u32 + }; + + // 7.2.7: Segment data length + // "This 4-byte field contains the length of the segment's segment data part, + // in bytes." + // + // "If the segment's type is 'Immediate generic region', then the length field + // may contain the value 0xFFFFFFFF. This value is intended to mean that the + // length of the segment's data part is unknown at the time that the segment + // header is written." + let data_length_raw = reader.read_u32().ok_or(ParseError::UnexpectedEof)?; + let data_length = if data_length_raw == 0xFFFFFFFF { + None + } else { + Some(data_length_raw) + }; + + Ok(SegmentHeader { + segment_number, + segment_type, + _retain_flag: retain_flag, + _page_association: page_association, + referred_to_segments, + data_length, + }) +} + +/// Parse a complete segment (header + data). +pub(crate) fn parse_segment<'a>(reader: &mut Reader<'a>) -> Result> { + let header = parse_segment_header(reader)?; + parse_segment_data(reader, header) +} + +/// Parse segment data for a previously parsed header. +/// +/// "If the segment's type is 'Immediate generic region', then the length field +/// may contain the value 0xFFFFFFFF. This value is intended to mean that the +/// length of the segment's data part is unknown at the time that the segment +/// header is written." (7.2.7) +pub(crate) fn parse_segment_data<'a>( + reader: &mut Reader<'a>, + header: SegmentHeader, +) -> Result> { + let data = if let Some(len) = header.data_length { + reader + .read_bytes(len as usize) + .ok_or(ParseError::UnexpectedEof)? + } else { + // "In order for the decoder to correctly decode the segment, it needs to + // read the four-byte row count field, which is stored in the last four + // bytes of the segment's data part. These four bytes can be detected + // without knowing the length of the data part in advance: if MMR is 1, + // they are preceded by the two-byte sequence 0x00 0x00; if MMR is 0, they + // are preceded by the two-byte sequence 0xFF 0xAC." (7.4.6.4) + let len = scan_for_immediate_generic_region_size(reader)?; + reader.read_bytes(len).ok_or(ParseError::UnexpectedEof)? + }; + + Ok(Segment { header, data }) +} + +/// Scan for the end of an immediate generic region segment with unknown length. +/// +/// "The form of encoding used by the segment may be determined by examining +/// the eighteenth byte of its segment data part, and the end sequences can +/// occur anywhere after that eighteenth byte." (7.2.7) +fn scan_for_immediate_generic_region_size(reader: &Reader<'_>) -> Result { + let mut scan = reader.clone(); + let start_offset = scan.byte_pos(); + + scan.skip_bytes(17).ok_or(ParseError::UnexpectedEof)?; + let flags = scan.read_byte().ok_or(ParseError::UnexpectedEof)?; + let uses_mmr = (flags & 1) != 0; + + // "if MMR is 1, they are preceded by the two-byte sequence 0x00 0x00; + // if MMR is 0, they are preceded by the two-byte sequence 0xFF 0xAC." + let end_marker: [u8; 2] = if uses_mmr { [0x00, 0x00] } else { [0xFF, 0xAC] }; + + // Search for the end marker. The marker is followed by a 4-byte row count. + while let Some(bytes) = scan.peek_bytes(6) { + if bytes[..2] == end_marker { + // Found the marker. Total size is current offset + 2 (marker) + 4 (row count) - start. + return Ok(scan.byte_pos() - start_offset + 2 + 4); + } + scan.skip_bytes(1).ok_or(ParseError::UnexpectedEof)?; + } + + err!(SegmentError::MissingEndMarker) +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use super::*; + + #[test] + fn test_segment_header_example_1() { + // 7.2.8 Segment header example, EXAMPLE 1: + // "A segment header consisting of the sequence of bytes: + // 0x00 0x00 0x00 0x20 0x86 0x6B 0x02 0x1E 0x05 0x04" + // + // Plus 4 bytes for data length (not shown in example). + let data = [ + 0x00, 0x00, 0x00, 0x20, // Segment number = 32 + 0x86, // Flags: type 6, page assoc 1 byte, deferred non-retain + 0x6B, // Refers to 3 segments, retention flags + 0x02, 0x1E, 0x05, // Referred segments: 2, 30, 5 + 0x04, // Page association = 4 + 0x00, 0x00, 0x00, 0x10, // Data length = 16 (added for complete header) + ]; + + let mut reader = Reader::new(&data); + let header = parse_segment_header(&mut reader).unwrap(); + + // "0x00 0x00 0x00 0x20: This segment's number is 0x00000020, or 32 decimal." + assert_eq!(header.segment_number, 32); + + // "0x86: This segment's type is 6. Its page association field is one byte + // long. It is retained by only its attached extension segments." + assert_eq!(header.segment_type, SegmentType::ImmediateTextRegion); + assert!(!header._retain_flag); + + // "0x6B: This segment refers to three other segments. It is referred to by + // some other segment. This is the last reference to the second of the three + // segments that it refers to." + // "0x02 0x1E 0x05: The three segments that it refers to are numbers 2, 30, and 5." + assert_eq!(header.referred_to_segments, vec![2, 30, 5]); + + // "0x04: This segment is associated with page number 4." + assert_eq!(header._page_association, 4); + + assert_eq!(header.data_length, Some(16)); + } + + #[test] + fn test_segment_header_example_2() { + // 7.2.8 Segment header example, EXAMPLE 2: + // "A segment header consisting of the sequence of bytes, in hexadecimal: + // 00 00 02 34 40 E0 00 00 09 02 FD 01 00 00 02 00 + // 1E 00 05 02 00 02 01 02 02 02 03 02 04 00 00 04 + // 01" + // + // Plus 4 bytes for data length (not shown in example). + #[rustfmt::skip] + let data = [ + 0x00, 0x00, 0x02, 0x34, // Segment number = 564 + 0x40, // Flags: type 0, page assoc 4 bytes + 0xE0, 0x00, 0x00, 0x09, // Long form: refers to 9 segments + 0x02, 0xFD, // Retention flags (2 bytes) + 0x01, 0x00, // Referred segment 256 + 0x00, 0x02, // Referred segment 2 + 0x00, 0x1E, // Referred segment 30 + 0x00, 0x05, // Referred segment 5 + 0x02, 0x00, // Referred segment 512 + 0x02, 0x01, // Referred segment 513 + 0x02, 0x02, // Referred segment 514 + 0x02, 0x03, // Referred segment 515 + 0x02, 0x04, // Referred segment 516 + 0x00, 0x00, 0x04, 0x01, // Page association = 1025 + 0x00, 0x00, 0x00, 0x20, // Data length = 32 (added for complete header) + ]; + + let mut reader = Reader::new(&data); + let header = parse_segment_header(&mut reader).unwrap(); + + // "00 00 02 34: This segment's number is 0x00000234, or 564 decimal." + assert_eq!(header.segment_number, 564); + + // "40: This segment's type is 0. Its page association field is four bytes long." + assert_eq!(header.segment_type, SegmentType::SymbolDictionary); + assert!(header._retain_flag); + + // "E0 00 00 09: This segment's referred-to segment count field is in the long + // format. This segment refers to nine other segments." + // "01 00 ... 02 04: The nine segments that it refers to are each identified by + // two bytes, since this segment's number is between 256 and 65535. The segments + // that it refers to are, in decimal, numbers 256, 2, 30, 5, 512, 513, 514, 515, + // and 516." + assert_eq!( + header.referred_to_segments, + vec![256, 2, 30, 5, 512, 513, 514, 515, 516] + ); + + // "00 00 04 01: This segment is associated with page number 1025." + assert_eq!(header._page_association, 1025); + + assert_eq!(header.data_length, Some(32)); + } +} diff --git a/vendor/hayro-jbig2/src/simd.rs b/vendor/hayro-jbig2/src/simd.rs new file mode 100644 index 00000000..2bd1adb7 --- /dev/null +++ b/vendor/hayro-jbig2/src/simd.rs @@ -0,0 +1,256 @@ +pub(crate) const SIMD_WIDTH: usize = 8; + +#[cfg(feature = "simd")] +mod inner { + use super::SIMD_WIDTH; + use core::ops::{BitAnd, BitOr, BitXor, BitXorAssign}; + use fearless_simd::{Select, SimdBase, SimdInt}; + + pub(crate) use fearless_simd::{Level, Simd, dispatch}; + + #[derive(Copy, Clone)] + #[allow(non_camel_case_types)] + #[repr(C, align(32))] + pub(crate) struct mask32x8 { + inner: fearless_simd::mask32x8, + } + + impl mask32x8 { + #[inline(always)] + pub(crate) fn select(self, if_true: u32x8, if_false: u32x8) -> u32x8 { + u32x8 { + inner: self.inner.select(if_true.inner, if_false.inner), + } + } + } + + #[derive(Copy, Clone)] + #[allow(non_camel_case_types)] + #[repr(C, align(32))] + pub(crate) struct u32x8 { + inner: fearless_simd::u32x8, + } + + impl u32x8 { + #[inline(always)] + pub(crate) fn from_slice(simd: S, slice: &[u32]) -> Self { + Self { + inner: fearless_simd::u32x8::from_slice(simd, &slice[..SIMD_WIDTH]), + } + } + + #[inline(always)] + pub(crate) fn splat(simd: S, val: u32) -> Self { + Self { + inner: fearless_simd::u32x8::splat(simd, val), + } + } + + #[inline(always)] + pub(crate) fn store(self, slice: &mut [u32]) { + self.inner.store_slice(&mut slice[..SIMD_WIDTH]); + } + + #[inline(always)] + pub(crate) fn simd_gt(self, other: Self) -> mask32x8 { + mask32x8 { + inner: self.inner.simd_gt(other.inner), + } + } + } + + impl BitAnd for u32x8 { + type Output = Self; + #[inline(always)] + fn bitand(self, rhs: Self) -> Self { + Self { + inner: self.inner & rhs.inner, + } + } + } + + impl BitOr for u32x8 { + type Output = Self; + #[inline(always)] + fn bitor(self, rhs: Self) -> Self { + Self { + inner: self.inner | rhs.inner, + } + } + } + + impl BitXor for u32x8 { + type Output = Self; + #[inline(always)] + fn bitxor(self, rhs: Self) -> Self { + Self { + inner: self.inner ^ rhs.inner, + } + } + } + + impl BitXorAssign for u32x8 { + #[inline(always)] + fn bitxor_assign(&mut self, rhs: Self) { + self.inner = self.inner ^ rhs.inner; + } + } +} + +#[cfg(not(feature = "simd"))] +mod inner { + use super::SIMD_WIDTH; + use core::marker::PhantomData; + use core::ops::{BitAnd, BitOr, BitXor, BitXorAssign}; + + pub(crate) trait Simd: Copy + Clone {} + + #[derive(Copy, Clone)] + pub(crate) struct ScalarSimd; + impl Simd for ScalarSimd {} + + pub(crate) struct Level; + impl Level { + #[inline(always)] + pub(crate) fn new() -> Self { + Level + } + } + + macro_rules! simd_dispatch { + ($level:expr, $simd:ident => $body:expr) => {{ + let _ = $level; + let $simd = $crate::simd::ScalarSimd; + $body + }}; + } + + pub(crate) use simd_dispatch as dispatch; + + #[derive(Copy, Clone)] + #[allow(non_camel_case_types)] + #[repr(C, align(32))] + pub(crate) struct mask32x8 { + val: [bool; SIMD_WIDTH], + _marker: PhantomData, + } + + impl mask32x8 { + #[inline(always)] + pub(crate) fn select(self, if_true: u32x8, if_false: u32x8) -> u32x8 { + let mut result = [0u32; SIMD_WIDTH]; + for i in 0..SIMD_WIDTH { + result[i] = if self.val[i] { + if_true.val[i] + } else { + if_false.val[i] + }; + } + u32x8 { + val: result, + _marker: PhantomData, + } + } + } + + #[derive(Copy, Clone)] + #[allow(non_camel_case_types)] + #[repr(C, align(32))] + pub(crate) struct u32x8 { + pub(super) val: [u32; SIMD_WIDTH], + _marker: PhantomData, + } + + impl u32x8 { + #[inline(always)] + pub(crate) fn from_slice(_simd: S, slice: &[u32]) -> Self { + let mut val = [0u32; SIMD_WIDTH]; + val.copy_from_slice(&slice[..SIMD_WIDTH]); + Self { + val, + _marker: PhantomData, + } + } + + #[inline(always)] + pub(crate) fn splat(_simd: S, val: u32) -> Self { + Self { + val: [val; SIMD_WIDTH], + _marker: PhantomData, + } + } + + #[inline(always)] + pub(crate) fn store(self, slice: &mut [u32]) { + slice[..SIMD_WIDTH].copy_from_slice(&self.val); + } + + #[inline(always)] + pub(crate) fn simd_gt(self, other: Self) -> mask32x8 { + let mut result = [false; SIMD_WIDTH]; + for i in 0..SIMD_WIDTH { + result[i] = self.val[i] > other.val[i]; + } + mask32x8 { + val: result, + _marker: PhantomData, + } + } + } + + impl BitAnd for u32x8 { + type Output = Self; + #[inline(always)] + fn bitand(self, rhs: Self) -> Self { + let mut result = [0u32; SIMD_WIDTH]; + for i in 0..SIMD_WIDTH { + result[i] = self.val[i] & rhs.val[i]; + } + Self { + val: result, + _marker: PhantomData, + } + } + } + + impl BitOr for u32x8 { + type Output = Self; + #[inline(always)] + fn bitor(self, rhs: Self) -> Self { + let mut result = [0u32; SIMD_WIDTH]; + for i in 0..SIMD_WIDTH { + result[i] = self.val[i] | rhs.val[i]; + } + Self { + val: result, + _marker: PhantomData, + } + } + } + + impl BitXor for u32x8 { + type Output = Self; + #[inline(always)] + fn bitxor(self, rhs: Self) -> Self { + let mut result = [0u32; SIMD_WIDTH]; + for i in 0..SIMD_WIDTH { + result[i] = self.val[i] ^ rhs.val[i]; + } + Self { + val: result, + _marker: PhantomData, + } + } + } + + impl BitXorAssign for u32x8 { + #[inline(always)] + fn bitxor_assign(&mut self, rhs: Self) { + for i in 0..SIMD_WIDTH { + self.val[i] ^= rhs.val[i]; + } + } + } +} + +pub(crate) use inner::*; diff --git a/vendor/hayro-jbig2/src/symbol_id_decoder.rs b/vendor/hayro-jbig2/src/symbol_id_decoder.rs new file mode 100644 index 00000000..33ca1c6d --- /dev/null +++ b/vendor/hayro-jbig2/src/symbol_id_decoder.rs @@ -0,0 +1,40 @@ +//! Symbol ID (IAID) decoder (A.3). + +use alloc::vec; +use alloc::vec::Vec; + +use crate::arithmetic_decoder::{ArithmeticDecoder, ArithmeticDecoderContext}; + +pub(crate) struct SymbolIdDecoder { + contexts: Vec, + code_len: u32, +} + +impl SymbolIdDecoder { + pub(crate) fn new(code_len: u32) -> Self { + // A.3: "The number of contexts required is 2^SBSYMCODELEN, which is less + // than twice the maximum symbol ID." + let num_contexts = 1_usize << code_len; + + Self { + contexts: vec![ArithmeticDecoderContext::default(); num_contexts], + code_len, + } + } + + #[inline(always)] + pub(crate) fn decode(&mut self, decoder: &mut ArithmeticDecoder<'_>) -> u32 { + let mut prev = 1_u32; + + for _ in 0..self.code_len { + let ctx_mask = (1_u32 << (self.code_len + 1)) - 1; + let ctx_idx = (prev & ctx_mask) as usize; + let d = decoder.read_bit(&mut self.contexts[ctx_idx]); + + prev = (prev << 1) | d; + } + + prev -= 1 << self.code_len; + prev + } +} diff --git a/vendor/hayro-syntax/Cargo.lock b/vendor/hayro-syntax/Cargo.lock new file mode 100644 index 00000000..ebc07dff --- /dev/null +++ b/vendor/hayro-syntax/Cargo.lock @@ -0,0 +1,272 @@ +# 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 = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[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", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hayro-ccitt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" + +[[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-syntax" +version = "0.7.2" +dependencies = [ + "aes", + "cbc", + "flate2", + "hayro-ccitt", + "hayro-jbig2", + "hayro-jpeg2000", + "log", + "md5", + "memchr", + "rustc-hash", + "sha2", + "smallvec", + "zune-jpeg", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[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 = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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-syntax/Cargo.toml b/vendor/hayro-syntax/Cargo.toml new file mode 100644 index 00000000..4c3460a3 --- /dev/null +++ b/vendor/hayro-syntax/Cargo.toml @@ -0,0 +1,160 @@ +# 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-syntax" +version = "0.7.2" +authors = ["Laurenz Stampfl "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A low-level crate for reading PDF files." +readme = "README.md" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/LaurenzV/hayro" +resolver = "2" + +[features] +default = [ + "std", + "images", +] +images = [ + "dep:zune-jpeg", + "dep:hayro-ccitt", + "dep:hayro-jbig2", + "dep:hayro-jpeg2000", +] +logging = [ + "dep:log", + "hayro-jpeg2000?/logging", +] +std = [ + "hayro-jbig2?/std", + "hayro-jpeg2000?/std", + "dep:rustc-hash", + "dep:stringprep", + "zune-jpeg?/std", +] +unsafe = [ + "std", + "dep:flate2", + "dep:memchr", + "hayro-jbig2?/simd", + "hayro-jpeg2000?/simd", + "zune-jpeg?/x86", + "zune-jpeg?/neon", +] + +[lib] +name = "hayro_syntax" +path = "src/lib.rs" + +[[example]] +name = "iterate" +path = "examples/iterate.rs" + +[dependencies.flate2] +version = "1" +optional = true + +[dependencies.hayro-ccitt] +version = "0.3.0" +optional = true +default-features = false + +[dependencies.hayro-jbig2] +version = "=0.3.0" +optional = true +default-features = false + +[dependencies.hayro-jpeg2000] +version = "0.3.5" +optional = true +default-features = false + +[dependencies.log] +version = "0.4" +optional = true + +[dependencies.memchr] +version = "2.8.0" +features = ["alloc"] +optional = true +default-features = false + +[dependencies.rustc-hash] +version = "2" +optional = true + +[dependencies.smallvec] +version = "1" + +[dependencies.stringprep] +version = "0.1" +optional = true + +[dependencies.zune-jpeg] +version = "0.5" +optional = true +default-features = false + +[dev-dependencies.aes] +version = "0.8.4" + +[dev-dependencies.cbc] +version = "0.1.2" + +[dev-dependencies.md5] +version = "0.8" + +[dev-dependencies.sha2] +version = "0.10" + +[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-syntax/Cargo.toml.orig b/vendor/hayro-syntax/Cargo.toml.orig new file mode 100644 index 00000000..bbc9dcf3 --- /dev/null +++ b/vendor/hayro-syntax/Cargo.toml.orig @@ -0,0 +1,46 @@ +[package] +name = "hayro-syntax" +version = "0.7.2" +description = "A low-level crate for reading PDF files." +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +readme = "README.md" + +[features] +default = ["std", "images"] +std = ["hayro-jbig2?/std", "hayro-jpeg2000?/std", "dep:rustc-hash", "zune-jpeg?/std"] +images = ["dep:zune-jpeg", "dep:hayro-ccitt", "dep:hayro-jbig2", "dep:hayro-jpeg2000"] +unsafe = [ + "std", + "dep:flate2", + "dep:memchr", + "hayro-jbig2?/simd", + "hayro-jpeg2000?/simd", + "zune-jpeg?/x86", + "zune-jpeg?/neon", +] +logging = ["dep:log", "hayro-jpeg2000?/logging"] + +[dependencies] +log = { workspace = true, optional = true } +rustc-hash = { workspace = true, optional = true } +smallvec = { workspace = true } +flate2 = { workspace = true, optional = true } +memchr = { workspace = true, optional = true } + +zune-jpeg = { workspace = true, optional = true } +hayro-ccitt = { workspace = true, optional = true } +hayro-jbig2 = { workspace = true, optional = true } +hayro-jpeg2000 = { workspace = true, optional = true } + +[lints] +workspace = true + +[dev-dependencies] +sha2 = "0.10" +md5 = "0.8" +aes = "0.8.4" +cbc = "0.1.2" diff --git a/vendor/hayro-syntax/LICENSE-APACHE b/vendor/hayro-syntax/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/vendor/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/vendor/hayro-syntax/LICENSE-MIT b/vendor/hayro-syntax/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/vendor/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/vendor/hayro-syntax/README.md b/vendor/hayro-syntax/README.md new file mode 100644 index 00000000..941a40ea --- /dev/null +++ b/vendor/hayro-syntax/README.md @@ -0,0 +1,90 @@ +# hayro-syntax + +[![Crates.io](https://img.shields.io/crates/v/hayro-syntax.svg)](https://crates.io/crates/hayro-syntax) +[![Documentation](https://docs.rs/hayro-syntax/badge.svg)](https://docs.rs/hayro-syntax) + + + +A low-level library for reading PDF files. + +This crate implements the `Syntax` chapter of the PDF reference, and therefore +serves as a very good basis for building various abstractions on top of it, without having to reimplement +the PDF parsing logic. + +This crate does not provide more high-level functionality, such as parsing fonts or color spaces. +Such functionality is out-of-scope for `hayro-syntax`, since this crate is supposed to be +as *light-weight* and *application-agnostic* as possible. + +Functionality-wise, this crate is therefore close to feature-complete. The main missing feature +is support for password-protected documents. In addition to that, more low-level APIs might be +added in the future. + +The crate is `no_std` compatible but requires an allocator to be available. + +## Example +This short example shows you how to load a PDF file and iterate over the content streams of all +pages. +```rust +use hayro_syntax::Pdf; +use std::path::PathBuf; + +// First load the data that constitutes the PDF file. +let data = std::fs::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../hayro-tests/pdfs/custom/text_with_rise.pdf"), +) +.unwrap(); + +// Then create a new PDF file from it. +// +// Here we are just unwrapping in case reading the file failed, but you +// might instead want to apply proper error handling. +let pdf = Pdf::new(data).unwrap(); + +// First access all pages, and then iterate over the operators of each page's +// content stream and print them. +let pages = pdf.pages(); +for page in pages.iter() { + let mut ops = page.typed_operations(); + + while let Some(op) = ops.next() { + println!("{op:?}"); + } +} +``` + +## Safety +When the `unsafe` feature is disabled, there is only use of `unsafe` in `hayro-syntax`. +We also unconditionally use the `smallvec` crate which uses unsafe internally, but that's +it. + +For better performance, it is strongly recommended to enable the `unsafe` feature, +which does result in slightly more unsafe code being, but does give better performance: +- We will use the `flate2` crate for decoding flate streams. +- SIMD will be enabled to accelerate decoding of images. +- `memchr` will be used to accelerate some of the fallback parsing code. + +## Features +The supported features include: +- Parsing xref tables in all its possible formats, including xref streams. +- Best-effort attempt at repairing PDF files with broken xref tables. +- Parsing of all objects types (also in object streams). +- Parsing and decoding PDF streams. +- Iterating over pages as well as their content streams in a typed or untyped fashion. +- The crate is very lightweight, especially in comparison to other PDF crates. + +## Limitations +- There are still a few features missing, for example, support for + password-protected PDFs. In addition to that, many properties (like page annotations) are + currently not exposed. +- This crate is for read-only processing, you cannot directly use it to manipulate PDF files. + If you need to do that, there are other crates in the Rust ecosystem that are suitable for this. + + + +## 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. diff --git a/vendor/hayro-syntax/SOURCE.json b/vendor/hayro-syntax/SOURCE.json new file mode 100644 index 00000000..721bac61 --- /dev/null +++ b/vendor/hayro-syntax/SOURCE.json @@ -0,0 +1,375 @@ +{ + "crate": "hayro-syntax", + "source_repository": "https://github.com/storytold/pdfcraft", + "source_revision": "7437bde378f43856eada40af25b983812e615e8a", + "source_directory": "vendor/hayro-syntax", + "files": [ + { + "path": "Cargo.lock", + "source_sha256": "d73da432072780b3d1fc725da68ad174c75f1720d0ed903521f664bfc16c70a8", + "source_git_blob": "ebc07dfff7b53f62a5ff422c19a779ab61c106cc", + "sha256": "d73da432072780b3d1fc725da68ad174c75f1720d0ed903521f664bfc16c70a8" + }, + { + "path": "Cargo.toml", + "source_sha256": "2520802133a82f9bfca9be7fe4a384af1bce05ada198fd9d637d99d877625333", + "source_git_blob": "cc34e45f8745780a781c2bb2328fa79669998abf", + "sha256": "86349f42b6632812f9ea23eddf56075698dfabbee6c31a04bf1d96431a5cb810" + }, + { + "path": "Cargo.toml.orig", + "source_sha256": "4d011edd25ba0cd22252643cc8f9c967c64e3480735cabcba3d10b1798f72e83", + "source_git_blob": "bbc9dcf3ebf210012f740790840d119b3e501d37", + "sha256": "4d011edd25ba0cd22252643cc8f9c967c64e3480735cabcba3d10b1798f72e83" + }, + { + "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": "fc541ca0e9c3a74ea719cd0ded4a0bcc5886ed3d94376248cc98340b2448a3c8", + "source_git_blob": "941a40ea87accd63096c52ae700dca519add7b6e", + "sha256": "fc541ca0e9c3a74ea719cd0ded4a0bcc5886ed3d94376248cc98340b2448a3c8" + }, + { + "path": "examples/iterate.rs", + "source_sha256": "f311d2d624032b7ca72300f23bd4961885386c15473026852fae6a8242448be6", + "source_git_blob": "a30a63c9999c0d9c628df28f38ac8eaa8dfee4d5", + "sha256": "f311d2d624032b7ca72300f23bd4961885386c15473026852fae6a8242448be6" + }, + { + "path": "src/bit_reader.rs", + "source_sha256": "ed776feb5048a26e45661e1d35c92b9d20faa8b7bcb0e92b36b0872d532cc6b1", + "source_git_blob": "361f00b11db3d6fd4c70db50e6648648d5b394da", + "sha256": "ed776feb5048a26e45661e1d35c92b9d20faa8b7bcb0e92b36b0872d532cc6b1" + }, + { + "path": "src/byte_reader.rs", + "source_sha256": "aa8495ca920d5d1f27db8ec98557fd1dc6376a3b6823c1cf8bbd6ed9b9b0c8cf", + "source_git_blob": "78b282c8f3df43ee43faee63c944b922539b2ec8", + "sha256": "296cee7a0095b399685128f4ee5d854156597c745847e04db12e398f2afb24cc" + }, + { + "path": "src/content/gen_ops.py", + "source_sha256": "ac3949287de7c7b187a62611f763662e230210ab39630d6a86511a70cdd6c523", + "source_git_blob": "2d286a506dc13c0b616afae07b7d48f752346120", + "sha256": "ac3949287de7c7b187a62611f763662e230210ab39630d6a86511a70cdd6c523" + }, + { + "path": "src/content/mod.rs", + "source_sha256": "b0f36badb2688c90691c27a4e5dcdfec7639858081553242744dd7bc39ffc0d0", + "source_git_blob": "8e47324d0b0bd90ed15efe5ff1e9de0cb46e75d9", + "sha256": "b0f36badb2688c90691c27a4e5dcdfec7639858081553242744dd7bc39ffc0d0" + }, + { + "path": "src/content/ops.rs", + "source_sha256": "b29f00f4570a2dc061b2189cf2a96a499ffe424abfb846dd4cec99cc1ef8901f", + "source_git_blob": "b066165841182b9b1e5ba350000744e705e95f86", + "sha256": "b29f00f4570a2dc061b2189cf2a96a499ffe424abfb846dd4cec99cc1ef8901f" + }, + { + "path": "src/content/ops_generated.rs", + "source_sha256": "7229ddbe9f4e76b8cc792f428c5d3c9070b393d592916a0f007dabd417eda7ae", + "source_git_blob": "1028193c98d4c8b75240f853ec91760e3332bd71", + "sha256": "7229ddbe9f4e76b8cc792f428c5d3c9070b393d592916a0f007dabd417eda7ae" + }, + { + "path": "src/crypto/aes.rs", + "source_sha256": "2eece3570a553613c3203d444cf7c466036c87a9ba8f905dc153046ca3da6486", + "source_git_blob": "9d4abad3d19e0db47e68d89da7d8c6ed992781c3", + "sha256": "2eece3570a553613c3203d444cf7c466036c87a9ba8f905dc153046ca3da6486" + }, + { + "path": "src/crypto/md5.rs", + "source_sha256": "619d740c54d70adb98ceb45a163df0197b54e74d5ed7a4d4ef4081f9992030f7", + "source_git_blob": "30e8453bd23b6ce256616122cbc2c1b11b773d69", + "sha256": "619d740c54d70adb98ceb45a163df0197b54e74d5ed7a4d4ef4081f9992030f7" + }, + { + "path": "src/crypto/mod.rs", + "source_sha256": "6fca01cee7e67f3aaf435101b7ea98fa4fe23f807eadeaec63ee1c2ece759189", + "source_git_blob": "15371514c10dc993b335edfec19a3f989b497954", + "sha256": "6fca01cee7e67f3aaf435101b7ea98fa4fe23f807eadeaec63ee1c2ece759189" + }, + { + "path": "src/crypto/rc4.rs", + "source_sha256": "4a37d4a2fc75663a280d4d17197d326696f29dd300b5c066d5d9c73431ed220b", + "source_git_blob": "46dd1b6e4a8bc5e52986e55cbadb420524e68d82", + "sha256": "4a37d4a2fc75663a280d4d17197d326696f29dd300b5c066d5d9c73431ed220b" + }, + { + "path": "src/crypto/sha256.rs", + "source_sha256": "fb87c10a1c658916e7eef01934a26666529f08d70be4de4531e33c9d4308f3ae", + "source_git_blob": "968a024baf0056ebc2d4b1261d51893d6828a0e8", + "sha256": "fb87c10a1c658916e7eef01934a26666529f08d70be4de4531e33c9d4308f3ae" + }, + { + "path": "src/crypto/sha384.rs", + "source_sha256": "a0653063b53dd65cc9e487c5f91a800ca6282b1077873f5e4a68523ed6e21e2d", + "source_git_blob": "d046f46099b002c9ce41ff63aa4e6fd4df0e9bd8", + "sha256": "a0653063b53dd65cc9e487c5f91a800ca6282b1077873f5e4a68523ed6e21e2d" + }, + { + "path": "src/crypto/sha512.rs", + "source_sha256": "e6cdb0d530a2d424199437a75ce4a97471090b3ef3d5a2d572778caa04fb61c8", + "source_git_blob": "b1d3e9ed358760f8786073051be7750cf1e5db72", + "sha256": "e6cdb0d530a2d424199437a75ce4a97471090b3ef3d5a2d572778caa04fb61c8" + }, + { + "path": "src/data.rs", + "source_sha256": "c8dc5a157358e024d73e405333ecac01e94ba6880488d5a10b952bd3fc47fd41", + "source_git_blob": "9d5e2030f8d6dab7167cd2c5d8da36a7e5688c34", + "sha256": "c8dc5a157358e024d73e405333ecac01e94ba6880488d5a10b952bd3fc47fd41" + }, + { + "path": "src/filter/ascii_85.rs", + "source_sha256": "df32c6783b1c1969f9b6b03de3bf204384dfdba767f7beefdd8baa8590387d24", + "source_git_blob": "5a18cf8352d7d5a1770b4ea3068254187ff4b4f9", + "sha256": "df32c6783b1c1969f9b6b03de3bf204384dfdba767f7beefdd8baa8590387d24" + }, + { + "path": "src/filter/ascii_hex.rs", + "source_sha256": "3943384166df1f81f76c8f64a28766131dc947311d672d01ffa5fef05b0a30c8", + "source_git_blob": "edcd82d5bf335b6a266ff7d6871fb8eb4f446bbe", + "sha256": "3943384166df1f81f76c8f64a28766131dc947311d672d01ffa5fef05b0a30c8" + }, + { + "path": "src/filter/ccitt.rs", + "source_sha256": "101e2e7e69ece280a258edcadd29f5ea0442ce6822cf1dd1906dca41a69436bf", + "source_git_blob": "98924f2dbdd942bc6ee64f66749b8d2496d8b45e", + "sha256": "101e2e7e69ece280a258edcadd29f5ea0442ce6822cf1dd1906dca41a69436bf" + }, + { + "path": "src/filter/dct.rs", + "source_sha256": "48452c1817b0a95b079aff068c8a625078c90354978af1de4ff97142e6bb0dd2", + "source_git_blob": "a4fbd1761ee2b1e16ac2848f0209f563ec529065", + "sha256": "48452c1817b0a95b079aff068c8a625078c90354978af1de4ff97142e6bb0dd2" + }, + { + "path": "src/filter/jbig2.rs", + "source_sha256": "0ac3c16e02cb13f2d5fe401edd35949320662906c3906cb39fe743acd5e32b09", + "source_git_blob": "d87253f536e6b8ac18323f1f06164b516ad775df", + "sha256": "0ac3c16e02cb13f2d5fe401edd35949320662906c3906cb39fe743acd5e32b09" + }, + { + "path": "src/filter/jpx.rs", + "source_sha256": "a9cab59f286bf3d3c99aa008f8fdede5028117c0f36018f3c02e52ed097c299a", + "source_git_blob": "6a788591e3338c55eccd616e21aa6e683f8d3351", + "sha256": "a9cab59f286bf3d3c99aa008f8fdede5028117c0f36018f3c02e52ed097c299a" + }, + { + "path": "src/filter/lzw_flate.rs", + "source_sha256": "e2d2196bc47c00474dc8f61d281ba327b88aa0d5f9e5475d29bf63a0572522e3", + "source_git_blob": "f9da1590b27b3e582ddac2fdfe9c1ef80ea72aff", + "sha256": "e1a6125b27423c91d79127ce6f8bfec507bd4d6ab0f959b2740a8a6e392d3e49" + }, + { + "path": "src/filter/mod.rs", + "source_sha256": "5e6d62da5b1fd023f85f79bc8ade928b508214e13668f9fc99f3a08bc4d48a15", + "source_git_blob": "22bebf6d3287f1dc893c67b76a139108b28a8188", + "sha256": "8cc6c2572519046bd27adfc597630045e453829a954eaf1747fee7a447e5c5cb" + }, + { + "path": "src/filter/run_length.rs", + "source_sha256": "a60985c746312cd50a030b910db99cee25895baffb1737efe6a9dd9fff8fb4c6", + "source_git_blob": "2bef8e706afa99fd74c1c2ebbcbe2006a24467f5", + "sha256": "a60985c746312cd50a030b910db99cee25895baffb1737efe6a9dd9fff8fb4c6" + }, + { + "path": "src/lib.rs", + "source_sha256": "0439293354fe274dd0a956f7d8840f512b18b5a84cabcea63452dfddb6629c7e", + "source_git_blob": "b676523158fc0a0cb6ed947308c249859bc802dc", + "sha256": "bc8374d895a0d21bf1e13a2123c470557624151573531518933b6368efa7ade6" + }, + { + "path": "src/log.rs", + "source_sha256": "f32705bc40d499540f2d473087b75db707e4ce4c0f80d6f997432dfa64e4dba2", + "source_git_blob": "f359cbe2411dfb37a621dc9d5a880873329f4154", + "sha256": "f32705bc40d499540f2d473087b75db707e4ce4c0f80d6f997432dfa64e4dba2" + }, + { + "path": "src/math.rs", + "source_sha256": "2ae8acf7443fd25024965b0136958da0a40adb24dff5cd593b092431834d90e5", + "source_git_blob": "12d4724f6d3778a72e22d78190ca4b966e184af8", + "sha256": "2ae8acf7443fd25024965b0136958da0a40adb24dff5cd593b092431834d90e5" + }, + { + "path": "src/metadata.rs", + "source_sha256": "e7324559c77181ffd297f877f5eb00222ee0739ff8f2a83d1ebd4a948383af6d", + "source_git_blob": "5b86e7d4249a612ad16d4924e4a449d8ca835ceb", + "sha256": "e7324559c77181ffd297f877f5eb00222ee0739ff8f2a83d1ebd4a948383af6d" + }, + { + "path": "src/object/array.rs", + "source_sha256": "6fbddf05724848db2389068a0504cff6b0783e35baa58e64bf2a31d48a1042ea", + "source_git_blob": "d39230f7dbf114dd202a16b21f7a6bb70be5ead3", + "sha256": "36e937b44ca020e1b4846a47e8126bcf141d8fe1ba084690f17a4878861f7f7d" + }, + { + "path": "src/object/bool.rs", + "source_sha256": "67089f2d24e281a2106a8e2ab1bb452edaabf8f406ba31605fd9b62c88b3094a", + "source_git_blob": "8cfb3afb93f42af5e558e75c1529e12aaf0385f3", + "sha256": "67089f2d24e281a2106a8e2ab1bb452edaabf8f406ba31605fd9b62c88b3094a" + }, + { + "path": "src/object/date.rs", + "source_sha256": "3a6361f527f94c68d77c8164ab5286f78e669310adf9f034dc0eed88375ef213", + "source_git_blob": "364d34d5a9fa0f8a95fafdac7ffbe85e56f3a672", + "sha256": "3a6361f527f94c68d77c8164ab5286f78e669310adf9f034dc0eed88375ef213" + }, + { + "path": "src/object/dict.rs", + "source_sha256": "bcfefabe931614b79ea54940d02fce534eb2363f1b75a03d6428fd7a04c57981", + "source_git_blob": "1e016b567e8ec17801fa8282c47f653092839cb5", + "sha256": "c53c686b05163fcb714d1ddacc8c55d340f745c29fead38f38d3c6b691673730" + }, + { + "path": "src/object/indirect.rs", + "source_sha256": "4058b7f23880aab1cb0149fcfae225904afec180af521885608e0543c2dffc50", + "source_git_blob": "cd0e6ff7aa58f1f9190c46e9e46db0d1b64c4ef3", + "sha256": "4058b7f23880aab1cb0149fcfae225904afec180af521885608e0543c2dffc50" + }, + { + "path": "src/object/mod.rs", + "source_sha256": "6188f89366cdb1bcfc0742c778dd5997447ad79015b9994fcaa9437cb006e615", + "source_git_blob": "d692db8b385600474db3dbda9ee7c0da01713c04", + "sha256": "6188f89366cdb1bcfc0742c778dd5997447ad79015b9994fcaa9437cb006e615" + }, + { + "path": "src/object/name.rs", + "source_sha256": "c3fb0ec7aa3a1652ba2557b25fd059f3e5253b4534625888f342169967848a6c", + "source_git_blob": "892d4d4f6d4c9806defe2c03ab4b1ae5565155b9", + "sha256": "c3fb0ec7aa3a1652ba2557b25fd059f3e5253b4534625888f342169967848a6c" + }, + { + "path": "src/object/null.rs", + "source_sha256": "55ea533359e2c35decfd157977141adc87afcbdb5ef000d366439238cb6b5d38", + "source_git_blob": "6f025b193e91d34cfd187f654be0a57aa3bf1407", + "sha256": "55ea533359e2c35decfd157977141adc87afcbdb5ef000d366439238cb6b5d38" + }, + { + "path": "src/object/number.rs", + "source_sha256": "4bc2df257241ccb66450b7176bcab20d780bc9e22469cec588ebee5fac272905", + "source_git_blob": "bd37b19c3053317dabbfa38d3473618631e6b669", + "sha256": "85598a27cf730aa9d2fe6a7094a2b375e67c4fed588de86b47cb8ebf38a702fa" + }, + { + "path": "src/object/rect.rs", + "source_sha256": "0deae0ab94968507764d1943d5380289e84881e2994152f933bcdcf1d1495992", + "source_git_blob": "13f46315b185724cd63f8bc6eb70f47ac4eb05e5", + "sha256": "0deae0ab94968507764d1943d5380289e84881e2994152f933bcdcf1d1495992" + }, + { + "path": "src/object/ref.rs", + "source_sha256": "b5ff0720a3946ef6e77922c8f56e4a0cacd7bbdb13280b9583740b5fd8a7c8fd", + "source_git_blob": "71951f4d82b1e281c67bde79d1179766f4fd515f", + "sha256": "b5ff0720a3946ef6e77922c8f56e4a0cacd7bbdb13280b9583740b5fd8a7c8fd" + }, + { + "path": "src/object/stream.rs", + "source_sha256": "151721c7bfe558a6942aba323d3185cf6db5bf726bd905326881c70ef480240f", + "source_git_blob": "833610817b4738304a7376cd43ea079cb352bd8c", + "sha256": "e83d7ecb01a038d5a0ae589d77e877f9521097010e5f88db8f8a333fd161aff1" + }, + { + "path": "src/object/string.rs", + "source_sha256": "cc0b8c7de9b665fe00b59608c97e3c10c8ec4e9ca8d5497789d2edcda7d6d5e9", + "source_git_blob": "c168d59351810dcd6f9a0c6c64da164403114023", + "sha256": "cc0b8c7de9b665fe00b59608c97e3c10c8ec4e9ca8d5497789d2edcda7d6d5e9" + }, + { + "path": "src/object/tuple.rs", + "source_sha256": "eeaf3f14a962fc98a109d1a74d7e70e069816d909e5e55766361658ef7b26aea", + "source_git_blob": "4a7190e91653c9fb47539dfec0edd631a99a3618", + "sha256": "eeaf3f14a962fc98a109d1a74d7e70e069816d909e5e55766361658ef7b26aea" + }, + { + "path": "src/page.rs", + "source_sha256": "19d0e7604a3a692956e523128022d07944253b308793598795e09595cb04e79e", + "source_git_blob": "7d63931a324f5faf90bb37e0a90670643567dace", + "sha256": "681681016eda0d1cdbec10d2cb0305c5f439724ec5d0b3be218a18e99b5e72b8" + }, + { + "path": "src/pdf.rs", + "source_sha256": "b6938cd27757a20dca83b49db46d73ab32a0ca2a474a0c4517d1d82b71a1be1c", + "source_git_blob": "c196cb14acc1bfd4dd572d7fedd8345b40e44873", + "sha256": "c2efc38c9afa0eab8ef90ad0aaa393a4910555ca463b616ba86db767a385d7d7" + }, + { + "path": "src/reader.rs", + "source_sha256": "f2f10a5cddecd70dae36304f432d052f4d7fdf5113dc4473a06de502375c4f2e", + "source_git_blob": "818f767f7b982bbe759f9c81dc91f0ddce335d25", + "sha256": "126aa382d6c3e0f53cc9f4d8de34a984134612f7c93c5d14bb622c9d8b460f26" + }, + { + "path": "src/sync.rs", + "source_sha256": "71eee1f5b66f390d626318c9281afb366ef55a4fcefa47989e690010c604f334", + "source_git_blob": "7991b0b5894551aa6620793a86efd3628c11ae46", + "sha256": "71eee1f5b66f390d626318c9281afb366ef55a4fcefa47989e690010c604f334" + }, + { + "path": "src/transform.rs", + "source_sha256": "40cf670b296220e7a7737dacc80eff888d42275d026fe17269a2458866a60fc8", + "source_git_blob": "f9b4a5e7572bcf9ea69956834e6a214a07ef177f", + "sha256": "40cf670b296220e7a7737dacc80eff888d42275d026fe17269a2458866a60fc8" + }, + { + "path": "src/trivia.rs", + "source_sha256": "0e90f8a7c82627cfeb2cbe0922bff15a9a2a1929da5f59549cfdd09792df357e", + "source_git_blob": "0097dc9967e6f480a62ea292ea393b2c8812f997", + "sha256": "0e90f8a7c82627cfeb2cbe0922bff15a9a2a1929da5f59549cfdd09792df357e" + }, + { + "path": "src/util.rs", + "source_sha256": "ffb769bfcec4573caacb964a31bef95c0509495ff94c0269e4fde5d49a3184b9", + "source_git_blob": "4b7b0d0cfa3c4cb8e32ae5d86fc7493ba7fe4c39", + "sha256": "ffb769bfcec4573caacb964a31bef95c0509495ff94c0269e4fde5d49a3184b9" + }, + { + "path": "src/xref.rs", + "source_sha256": "51ffeeb7d4ac035bae2cd5ba9288c180088ae06cdcefe2b29dfeda560a793aa0", + "source_git_blob": "20e33a410489d42e438a3f2b87d70c7bb8d66eae", + "sha256": "2bd19a3df5f81c4c0babc80a473877714fe13d2542eafb5c8b5843f81af36d80" + }, + { + "path": "src/decode_limits_tests.rs", + "source_sha256": null, + "source_git_blob": null, + "sha256": "758bee2a3fb1c0ac59a4f9747f153d8d2ae7d44f5cdcea13d8a084b04b368283" + } + ], + "compatibility_reference": { + "repository": "https://github.com/LaurenzV/hayro", + "revision": "ea9c81dc70cde40c64205986fdcfcd481fe934d8", + "version": "0.8.0", + "license": "Apache-2.0 OR MIT", + "archive_sha256": "6cdc818d8d265dcb42081d488f7599df4f73fb79e9d347ecedcd3c23809d2026", + "reference_files": [ + "src/object/mod.rs", + "src/object/array.rs", + "src/object/dict.rs", + "src/page.rs" + ], + "changes": [ + "Bound direct arrays and dictionaries to 64 levels using the existing guarded reader, including decoded streams.", + "Add Resources::from_scope and use whole-dictionary resource replacement in page-tree and page construction.", + "Preserve iterative indirect page-tree traversal, sibling order and reference deduplication under a 100,000-entry work cap; budget and fallible frame/page reservation failures reject loading without brute-force fallback." + ], + "preserved": [ + "Existing bounded decoding, streaming, parser repair, cache and allocation safeguards." + ] + }, + "local_safety_repairs": [ + "Match existing standalone-plus-as-zero tolerance in number parsing while retaining regular-character terminator rejection and numeric overflow guards.", + "End raw array iteration on object parse failure instead of panicking when validation and parsing differ." + ] +} diff --git a/vendor/hayro-syntax/examples/iterate.rs b/vendor/hayro-syntax/examples/iterate.rs new file mode 100644 index 00000000..a30a63c9 --- /dev/null +++ b/vendor/hayro-syntax/examples/iterate.rs @@ -0,0 +1,26 @@ +//! This example shows how you can iterate over the content stream of all pages in a PDF. + +use hayro_syntax::Pdf; +use std::path::PathBuf; + +fn main() { + // First load the data that constitutes the PDF file. + let data = std::fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../in.pdf")).unwrap(); + + // Then create a new PDF file from it. + // + // Here we are just unwrapping in case reading the file failed, but you + // might instead want to apply proper error handling. + let pdf = Pdf::new(data).unwrap(); + + // First access all pages, and then iterate over the operators of each page's + // content stream and print them. + let pages = pdf.pages(); + for page in pages.iter() { + let mut ops = page.typed_operations(); + + while let Some(op) = ops.next() { + println!("{op:?}"); + } + } +} diff --git a/vendor/hayro-syntax/src/bit_reader.rs b/vendor/hayro-syntax/src/bit_reader.rs new file mode 100644 index 00000000..361f00b1 --- /dev/null +++ b/vendor/hayro-syntax/src/bit_reader.rs @@ -0,0 +1,623 @@ +//! A simple bit reader and writer. + +use core::fmt::Debug; +use smallvec::{SmallVec, smallvec}; + +/// A bit reader. +#[derive(Debug, Clone)] +pub struct BitReader<'a> { + /// The underlying bytes of the bit reader. + pub data: &'a [u8], + cur_pos: usize, +} + +impl<'a> BitReader<'a> { + /// Create a new bit reader. + #[inline] + pub fn new(data: &'a [u8]) -> Self { + Self::new_with(data, 0) + } + + /// Create a new bit reader, and start at a specific bit offset. + #[inline] + pub fn new_with(data: &'a [u8], cur_pos: usize) -> Self { + Self { data, cur_pos } + } + + /// Align the reader to the next byte boundary. + #[inline] + pub fn align(&mut self) { + let bit_pos = self.bit_pos(); + + if !bit_pos.is_multiple_of(8) { + self.cur_pos += 8 - bit_pos; + } + } + + /// Read the given number of bits from the byte stream. + /// + /// Returns `None` if `bit_size` > 32. + #[inline(always)] + pub fn read(&mut self, bit_size: u8) -> Option { + if !(1..=32).contains(&bit_size) { + return None; + } + + let byte_pos = self.byte_pos(); + + if byte_pos >= self.data.len() { + return None; + } + + let item = match bit_size { + 8 => { + let item = self.data[byte_pos] as u32; + self.cur_pos += 8; + + Some(item) + } + 1..=32 => { + let bit_pos = self.bit_pos(); + let end_byte_pos = (bit_pos + bit_size as usize - 1) / 8; + let mut read = [0_u8; 8]; + + for (i, r) in read.iter_mut().enumerate().take(end_byte_pos + 1) { + *r = *self.data.get(byte_pos + i)?; + } + + let item = (u64::from_be_bytes(read) >> (64 - bit_pos - bit_size as usize)) as u32 + & bit_mask(bit_size); + self.cur_pos += bit_size as usize; + + Some(item) + } + _ => None, + }?; + + Some(item) + } + + /// Peak the given number of bits. + pub fn peak(&mut self, bit_size: u8) -> Option { + self.clone().read(bit_size) + } + + /// Whether the bit reader has read all bytes. + pub fn at_end(&self) -> bool { + self.byte_pos() >= self.data.len() + } + + /// Get the current byte position. + #[inline] + pub fn byte_pos(&self) -> usize { + self.cur_pos / 8 + } + + /// Get the current position within the byte. + #[inline] + pub fn bit_pos(&self) -> usize { + self.cur_pos % 8 + } + + /// Get the current position in bits. + #[inline] + pub fn cur_pos(&self) -> usize { + self.cur_pos + } + + /// Get the tail (aligned to the current byte position) of the data. + #[inline] + pub fn tail(&self) -> &'a [u8] { + &self.data[self.byte_pos()..] + } + + /// Get the full byte (aligned to the byte boundary) of the current position. + #[inline] + pub fn cur_byte(&self) -> Option { + self.data.get(self.byte_pos()).copied() + } +} + +/// Get the mask for the given bit size. +pub fn bit_mask(bit_size: u8) -> u32 { + match bit_size { + 0 => 0, + 1..=31 => (1_u32 << bit_size) - 1, + _ => u32::MAX, + } +} +/// A bit writer. +#[derive(Debug)] +pub struct BitWriter<'a> { + data: &'a mut [u8], + cur_pos: usize, + bit_size: u8, +} + +impl<'a> BitWriter<'a> { + /// Create a new bit writer for a fixed bit size between 1 and 32 (inclusive). + #[inline] + pub fn new(data: &'a mut [u8], bit_size: u8) -> Option { + if !(1..=32).contains(&bit_size) { + return None; + } + + Some(Self { + data, + bit_size, + cur_pos: 0, + }) + } + + /// Split off the already-written parts and return a new bit writer for the tail. + #[inline] + pub fn split_off(self) -> (&'a [u8], Self) { + // Assumes that we are currently aligned to a byte boundary! + let (left, right) = self.data.split_at_mut(self.cur_pos / 8); + ( + left, + BitWriter { + data: right, + cur_pos: 0, + bit_size: self.bit_size, + }, + ) + } + + /// Align the writer to the next byte boundary. + #[inline] + pub fn align(&mut self) { + let bit_pos = self.bit_pos(); + + if !bit_pos.is_multiple_of(8) { + self.cur_pos += 8 - bit_pos; + } + } + + /// Return the number of written bits. + #[inline] + pub fn cur_pos(&self) -> usize { + self.cur_pos + } + + /// Return the whole underlying buffer. + #[inline] + pub fn get_data(&self) -> &[u8] { + self.data + } + + fn bit_pos(&self) -> usize { + self.cur_pos % 8 + } + + /// Write the given number into the buffer. + #[inline] + pub fn write(&mut self, val: u32) -> Option<()> { + let bit_size = self.bit_size as usize; + let mut bits_left = bit_size; + let value = val & bit_mask(self.bit_size); + + while bits_left > 0 { + let already_advanced = bit_size - bits_left; + let absolute_pos = self.cur_pos + already_advanced; + let byte_pos = absolute_pos / 8; + let bit_pos = absolute_pos % 8; + let bits_in_byte = (8 - bit_pos).min(bits_left); + let shift = bits_left - bits_in_byte; + let chunk_mask = bit_mask(bits_in_byte as u8); + let chunk = ((value >> shift) & chunk_mask) as u8; + + let shift_in_byte = 8 - bits_in_byte - bit_pos; + let byte = self.data.get_mut(byte_pos)?; + let byte_mask = (chunk_mask as u8) << shift_in_byte; + + *byte = (*byte & !byte_mask) | ((chunk << shift_in_byte) & byte_mask); + + bits_left -= bits_in_byte; + } + + self.cur_pos += bit_size; + + Some(()) + } + + /// Fill bytes with a repeated value when at a byte boundary. + #[inline] + pub fn fill_bytes(&mut self, byte: u8, count: usize) -> Option<()> { + debug_assert_eq!(self.cur_pos % 8, 0); + + let start = self.cur_pos / 8; + let end = start + count; + self.data.get_mut(start..end)?.fill(byte); + self.cur_pos += count * 8; + Some(()) + } + + /// Write multiple numbers at once. + #[inline] + pub fn write_bits(&mut self, bits: impl IntoIterator) -> Option<()> { + for bit in bits { + let value: u32 = bit; + self.write(value)?; + } + + Some(()) + } +} + +/// An iterator over bit chunks. +pub struct BitChunks<'a> { + reader: BitReader<'a>, + bit_size: u8, + chunk_len: usize, +} + +impl<'a> BitChunks<'a> { + /// Create a new iterator over bit chunks. + pub fn new(data: &'a [u8], bit_size: u8, chunk_len: usize) -> Option { + if !(1..=16).contains(&bit_size) || chunk_len == 0 { + return None; + } + + let reader = BitReader::new(data); + + Some(Self { + reader, + bit_size, + chunk_len, + }) + } +} + +impl Iterator for BitChunks<'_> { + type Item = BitChunk; + + fn next(&mut self) -> Option { + let mut bits = SmallVec::new(); + + for _ in 0..self.chunk_len { + bits.push(self.reader.read(self.bit_size)? as u16); + } + + Some(BitChunk { bits }) + } +} + +/// A chunk of bits. +#[derive(Debug, Clone)] +pub struct BitChunk { + bits: SmallVec<[u16; 4]>, +} + +impl BitChunk { + /// Return an iterator over the numbers in the chunk. + pub fn iter(&self) -> impl Iterator + '_ { + self.bits.iter().copied() + } + + /// Create a new bit chunk with the given value being repeated `count` times. + pub fn new(val: u8, count: usize) -> Self { + Self { + bits: smallvec![val as u16; count], + } + } + + /// Create a new bit chunk from the given reader. + pub fn from_reader( + bit_reader: &mut BitReader<'_>, + bit_size: u8, + chunk_len: usize, + ) -> Option { + if !(1..=16).contains(&bit_size) || chunk_len == 0 { + return None; + } + + let mut bits = SmallVec::new(); + + for _ in 0..chunk_len { + bits.push(bit_reader.read(bit_size)? as u16); + } + + Some(Self { bits }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bit_reader_16() { + let data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]; + let mut reader = BitReader::new(&data); + assert_eq!( + reader.read(16).unwrap() as u16, + u16::from_be_bytes([0x01, 0x02]) + ); + assert_eq!( + reader.read(16).unwrap() as u16, + u16::from_be_bytes([0x03, 0x04]) + ); + assert_eq!( + reader.read(16).unwrap() as u16, + u16::from_be_bytes([0x05, 0x06]) + ); + } + + #[test] + fn bit_writer_16() { + let mut buf = vec![0_u8; 6]; + let mut writer = BitWriter::new(&mut buf, 16).unwrap(); + writer + .write(u16::from_be_bytes([0x01, 0x02]) as u32) + .unwrap(); + writer + .write(u16::from_be_bytes([0x03, 0x04]) as u32) + .unwrap(); + writer + .write(u16::from_be_bytes([0x05, 0x06]) as u32) + .unwrap(); + + assert_eq!(buf, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); + } + + #[test] + fn bit_reader_12() { + let data = [0b10011000, 0b00011111, 0b10101001, 0b11101001, 0b00011010]; + let mut reader = BitReader::new(&data); + assert_eq!(reader.read(12).unwrap(), 0b100110000001); + assert_eq!(reader.read(12).unwrap(), 0b111110101001); + assert_eq!(reader.read(12).unwrap(), 0b111010010001); + } + + #[test] + fn bit_reader_9() { + let data = [0b10011000, 0b00011111, 0b10101001, 0b11101001, 0b00011010]; + let mut reader = BitReader::new(&data); + assert_eq!(reader.read(9).unwrap(), 0b100110000); + assert_eq!(reader.read(9).unwrap(), 0b001111110); + assert_eq!(reader.read(9).unwrap(), 0b101001111); + assert_eq!(reader.read(9).unwrap(), 0b010010001); + } + + #[test] + fn bit_writer_8() { + let mut buf = vec![0_u8; 3]; + let mut writer = BitWriter::new(&mut buf, 8).unwrap(); + writer.write(0x01).unwrap(); + writer.write(0x02).unwrap(); + writer.write(0x03).unwrap(); + + assert_eq!(buf, [0x01, 0x02, 0x03]); + } + + #[test] + fn bit_reader_8() { + let data = [0x01, 0x02, 0x03]; + let mut reader = BitReader::new(&data); + assert_eq!(reader.read(8).unwrap(), 0x01); + assert_eq!(reader.read(8).unwrap(), 0x02); + assert_eq!(reader.read(8).unwrap(), 0x03); + } + + #[test] + fn bit_writer_4() { + let mut buf = vec![0_u8; 3]; + let mut writer = BitWriter::new(&mut buf, 4).unwrap(); + writer.write(0b1001).unwrap(); + writer.write(0b1000).unwrap(); + writer.write(0b0001).unwrap(); + writer.write(0b1111).unwrap(); + writer.write(0b1010).unwrap(); + writer.write(0b1001).unwrap(); + + assert_eq!(buf, [0b10011000, 0b00011111, 0b10101001]); + } + + #[test] + fn bit_reader_4() { + let data = [0b10011000, 0b00011111, 0b10101001]; + let mut reader = BitReader::new(&data); + assert_eq!(reader.read(4).unwrap(), 0b1001); + assert_eq!(reader.read(4).unwrap(), 0b1000); + assert_eq!(reader.read(4).unwrap(), 0b0001); + assert_eq!(reader.read(4).unwrap(), 0b1111); + assert_eq!(reader.read(4).unwrap(), 0b1010); + assert_eq!(reader.read(4).unwrap(), 0b1001); + } + + #[test] + fn bit_writer_2() { + let mut buf = vec![0_u8; 2]; + let mut writer = BitWriter::new(&mut buf, 2).unwrap(); + writer.write(0b10).unwrap(); + writer.write(0b01).unwrap(); + writer.write(0b10).unwrap(); + writer.write(0b00).unwrap(); + writer.write(0b00).unwrap(); + writer.write(0b01).unwrap(); + writer.write(0b00).unwrap(); + writer.write(0b00).unwrap(); + + assert_eq!(buf, [0b10011000, 0b00010000]); + } + + #[test] + fn bit_reader_2() { + let data = [0b10011000, 0b00010000]; + let mut reader = BitReader::new(&data); + assert_eq!(reader.read(2).unwrap(), 0b10); + assert_eq!(reader.read(2).unwrap(), 0b01); + assert_eq!(reader.read(2).unwrap(), 0b10); + assert_eq!(reader.read(2).unwrap(), 0b00); + assert_eq!(reader.read(2).unwrap(), 0b00); + assert_eq!(reader.read(2).unwrap(), 0b01); + assert_eq!(reader.read(2).unwrap(), 0b00); + assert_eq!(reader.read(2).unwrap(), 0b00); + } + + #[test] + fn bit_writer_1() { + let mut buf = vec![0_u8; 2]; + let mut writer = BitWriter::new(&mut buf, 1).unwrap(); + writer.write(0b1).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b1).unwrap(); + writer.write(0b1).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b0).unwrap(); + + writer.write(0b0).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b1).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b0).unwrap(); + writer.write(0b0).unwrap(); + + assert_eq!(buf, [0b10011000, 0b00010000]); + } + + #[test] + fn bit_reader_1() { + let data = [0b10011000, 0b00010000]; + let mut reader = BitReader::new(&data); + assert_eq!(reader.read(1).unwrap(), 0b1); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b1); + assert_eq!(reader.read(1).unwrap(), 0b1); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b1); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + } + + #[test] + fn bit_reader_align() { + let data = [0b10011000, 0b00010000]; + let mut reader = BitReader::new(&data); + assert_eq!(reader.read(1).unwrap(), 0b1); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b1); + reader.align(); + + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b1); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + assert_eq!(reader.read(1).unwrap(), 0b0); + } + + #[test] + fn bit_reader_chunks() { + let data = [0b10011000, 0b00010000]; + let mut reader = BitChunks::new(&data, 1, 3).unwrap(); + assert_eq!(reader.next().unwrap().bits.as_ref(), &[0b1, 0b0, 0b0]); + assert_eq!(reader.next().unwrap().bits.as_ref(), &[0b1, 0b1, 0b0]); + assert_eq!(reader.next().unwrap().bits.as_ref(), &[0b0, 0b0, 0b0]); + assert_eq!(reader.next().unwrap().bits.as_ref(), &[0b0, 0b0, 0b1]); + assert_eq!(reader.next().unwrap().bits.as_ref(), &[0b0, 0b0, 0b0]); + } + + #[test] + fn bit_reader_varying_bit_sizes() { + let data = [0b10011000, 0b00011111, 0b10101001]; + let mut reader = BitReader::new(&data); + assert_eq!(reader.read(4).unwrap(), 0b1001); + assert_eq!(reader.read(1).unwrap(), 0b1); + assert_eq!(reader.read(4).unwrap(), 0b0000); + assert_eq!(reader.read(5).unwrap(), 0b00111); + assert_eq!(reader.read(1).unwrap(), 0b1); + assert_eq!(reader.read(2).unwrap(), 0b11); + assert_eq!(reader.read(7).unwrap(), 0b0101001); + } + + #[test] + fn bit_writer_rejects_invalid_sizes() { + let mut buf = [0_u8; 4]; + assert!(BitWriter::new(&mut buf, 0).is_none()); + assert!(BitWriter::new(&mut buf, 33).is_none()); + } + + #[test] + fn bit_reader_rejects_invalid_sizes() { + let data = [0xff]; + let mut reader = BitReader::new(&data); + assert!(reader.read(0).is_none()); + assert!(reader.read(33).is_none()); + } + + #[test] + fn bit_writer_round_trip_all_bit_sizes() { + for bit_size in 1_u8..=32 { + let mask = bit_mask(bit_size); + let sample_count = if bit_size > 16 { 4 } else { 6 }; + let values: Vec = (0..sample_count) + .map(|i| { + let seed = 0x9E37_u32.wrapping_mul(i as u32 + 1) ^ (bit_size as u32 * 0x45); + seed & mask + }) + .collect(); + + let total_bits = bit_size as usize * values.len(); + let mut buf = vec![0_u8; total_bits.div_ceil(8)]; + let mut writer = BitWriter::new(&mut buf, bit_size).unwrap(); + + for value in &values { + writer.write(*value).unwrap(); + } + + let mut reader = BitReader::new(&buf); + for expected in &values { + assert_eq!( + reader.read(bit_size).unwrap(), + *expected, + "round-trip failed for bit size {bit_size}" + ); + } + } + } + + #[test] + fn bit_writer_round_trip_12_and_25_bits() { + for &bit_size in &[12_u8, 25_u8] { + let mask = bit_mask(bit_size); + let values: Vec = (0..5) + .map(|i| ((0xABCDE_u32.wrapping_mul(i + 1)) ^ (bit_size as u32)) & mask) + .collect(); + + let total_bits = bit_size as usize * values.len(); + let mut buf = vec![0_u8; total_bits.div_ceil(8)]; + let mut writer = BitWriter::new(&mut buf, bit_size).unwrap(); + + for value in &values { + writer.write(*value).unwrap(); + } + + let mut reader = BitReader::new(&buf); + for expected in &values { + assert_eq!( + reader.read(bit_size).unwrap(), + *expected, + "round-trip failed for bit size {bit_size}" + ); + } + } + } +} diff --git a/vendor/hayro-syntax/src/byte_reader.rs b/vendor/hayro-syntax/src/byte_reader.rs new file mode 100644 index 00000000..e85d7c6e --- /dev/null +++ b/vendor/hayro-syntax/src/byte_reader.rs @@ -0,0 +1,319 @@ +//! A byte reader. + +use core::cell::Cell; +use core::ops::Range; + +/// Direct arrays and dictionaries may nest at most 64 levels, including in decoded streams. +const MAX_CONTAINER_DEPTH: usize = 64; + +/// A reader for reading bytes and PDF objects. +#[derive(Clone, Debug)] +pub struct Reader<'a> { + /// The underlying data of the reader. + pub data: &'a [u8], + /// The current byte-offset. + pub offset: usize, + /// PdfCraft patch: set when a read needed a byte past the end of `data`. A decision made from + /// such a read could change if more data followed (see `content::UntypedIter`). + past_end: Cell, + // PdfCraft patch: bound direct-object nesting while skipping arrays and dictionaries + // and while parsing dictionaries (`Dict::skip`, `Array::skip`, `parse_dict_with`). + skip_depth: usize, +} + +impl<'a> Reader<'a> { + /// Create a new reader. + #[inline] + pub fn new(data: &'a [u8]) -> Self { + Self::new_with(data, 0) + } + + /// Create a new reader at the given offset. + #[inline] + pub fn new_with(data: &'a [u8], offset: usize) -> Self { + Self { data, offset, past_end: Cell::new(false), skip_depth: 0 } + } + + /// PdfCraft patch: whether a read needed bytes past the end of the data since the flag was + /// last cleared. + #[inline] + pub(crate) fn looked_past_end(&self) -> bool { + self.past_end.get() + } + + /// PdfCraft patch: clears the flag set by [`Reader::looked_past_end`]. + #[inline] + pub(crate) fn clear_looked_past_end(&self) { + self.past_end.set(false); + } + + pub(crate) fn enter_container(&mut self) -> Option<()> { + if self.skip_depth >= MAX_CONTAINER_DEPTH { + return None; + } + self.skip_depth += 1; + Some(()) + } + + pub(crate) fn leave_container(&mut self) { + self.skip_depth -= 1; + } + + /// Returns `true` if the reader has reached the end of the data. + #[inline] + pub fn at_end(&self) -> bool { + let at_end = self.offset >= self.data.len(); + if at_end { + // PdfCraft patch: a parser that stops at the end decides from it (see `past_end`). + self.past_end.set(true); + } + + at_end + } + + /// PdfCraft patch: carries over whether `other`, a clone of this reader, looked past the end. + #[inline] + pub(crate) fn absorb_past_end(&self, other: &Reader<'_>) { + if other.past_end.get() { + self.past_end.set(true); + } + } + + /// Moves the reader offset to the end of the data. + #[inline] + pub fn jump_to_end(&mut self) { + self.offset = self.data.len(); + } + + /// Moves the reader to the specified offset. + #[inline] + pub fn jump(&mut self, offset: usize) { + self.offset = offset; + } + + /// Returns the remaining data from the current offset to the end. + #[inline] + pub fn tail(&mut self) -> Option<&'a [u8]> { + self.data.get(self.offset..) + } + + /// Returns the total length of the underlying data. + #[inline] + pub fn len(&self) -> usize { + self.data.len() + } + + /// Returns `true` if the underlying data is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// Returns a slice of the data for the specified range. + #[inline] + pub fn range(&self, range: Range) -> Option<&'a [u8]> { + self.data.get(range) + } + + /// Returns the current offset of the reader. + #[inline] + pub fn offset(&self) -> usize { + self.offset + } + + /// Reads the specified number of bytes and advances the offset. + #[inline] + pub fn read_bytes(&mut self, len: usize) -> Option<&'a [u8]> { + let v = self.peek_bytes(len)?; + self.offset += len; + + Some(v) + } + + /// Reads a single byte and advances the offset. + #[inline] + pub fn read_byte(&mut self) -> Option { + let v = self.peek_byte()?; + self.offset += 1; + + Some(v) + } + + /// Skips the specified number of bytes by advancing the offset. + #[inline] + pub fn skip_bytes(&mut self, len: usize) -> Option<()> { + self.read_bytes(len).map(|_| {}) + } + + /// Peeks the specified number of bytes. + #[inline] + pub fn peek_bytes(&self, len: usize) -> Option<&'a [u8]> { + let bytes = self + .offset + .checked_add(len) + .and_then(|end| self.data.get(self.offset..end)); + if bytes.is_none() { + self.past_end.set(true); + } + + bytes + } + + /// Peeks a single byte. + #[inline] + pub fn peek_byte(&self) -> Option { + let byte = self.data.get(self.offset).copied(); + if byte.is_none() { + self.past_end.set(true); + } + + byte + } + + /// Eat the next byte if it satisfies the condition. + #[inline] + pub fn eat(&mut self, f: impl Fn(u8) -> bool) -> Option { + let val = self.peek_byte()?; + if f(val) { + self.forward(); + Some(val) + } else { + None + } + } + + /// Advances the offset by one byte. + #[inline] + pub fn forward(&mut self) { + self.offset += 1; + } + + /// Advances the offset by one byte if the current byte satisfies the predicate. + #[inline] + pub fn forward_if(&mut self, f: impl Fn(u8) -> bool) -> Option<()> { + if f(self.peek_byte()?) { + self.forward(); + + Some(()) + } else { + None + } + } + + /// Advances the offset while bytes satisfy the predicate, at least one time. + #[inline] + pub fn forward_while_1(&mut self, f: impl Fn(u8) -> bool) -> Option<()> { + self.eat(&f)?; + self.forward_while(f); + Some(()) + } + + /// Advances the offset if the next bytes match the specified tag. + #[inline] + pub fn forward_tag(&mut self, tag: &[u8]) -> Option<()> { + self.peek_tag(tag)?; + self.offset += tag.len(); + + Some(()) + } + + /// Advances the offset while the given byte satisfies the predicate. + #[inline] + pub fn forward_while(&mut self, f: impl Fn(u8) -> bool) { + while let Some(b) = self.peek_byte() { + if f(b) { + self.forward(); + } else { + break; + } + } + } + + /// Checks if the next bytes match the specified tag. + #[inline] + pub fn peek_tag(&self, tag: &[u8]) -> Option<()> { + let mut cloned = self.clone(); + + for b in tag.iter().copied() { + if cloned.peek_byte() == Some(b) { + cloned.forward(); + } else { + self.past_end.set(cloned.past_end.get()); + return None; + } + } + + Some(()) + } + + /// Read a u16 integer (in big endian order). + #[inline] + pub fn read_u16(&mut self) -> Option { + let bytes = self.read_bytes(2)?; + + Some(u16::from_be_bytes([bytes[0], bytes[1]])) + } + + /// Read a u32 integer (in big endian order). + #[inline] + pub fn read_u32(&mut self) -> Option { + let bytes = self.read_bytes(4)?; + + Some(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) + } + + /// Read a u64 integer (in big endian order). + #[inline] + pub fn read_u64(&mut self) -> Option { + let bytes = self.read_bytes(8)?; + + Some(u64::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ])) + } +} + +#[cfg(test)] +mod tests { + use super::Reader; + use crate::object::{Array, Dict}; + use crate::reader::ReaderExt; + use alloc::format; + use alloc::string::String; + + #[test] + fn peek_bytes_rejects_overflowing_len() { + let reader = Reader::new(b"abc"); + assert!(reader.peek_bytes(usize::MAX).is_none()); + } + + #[test] + fn direct_container_limit_is_sixty_four_and_unwinds_after_failure() { + for depth in [1, 63, 64, 65, 128, 129] { + let array = format!("{}0{}", "[".repeat(depth), "]".repeat(depth)); + let dictionary = format!("{}0{}", "<< /A ".repeat(depth), " >>".repeat(depth)); + for in_content in [false, true] { + let mut reader = Reader::new(array.as_bytes()); + assert_eq!(reader.skip::>(in_content).is_some(), depth <= 64, "array {depth}"); + assert_eq!(reader.skip_depth, 0, "a rejected array must unwind its depth"); + + let mut reader = Reader::new(dictionary.as_bytes()); + assert_eq!(reader.skip::>(in_content).is_some(), depth <= 64, "dictionary {depth}"); + assert_eq!(reader.skip_depth, 0, "a rejected dictionary must unwind its depth"); + } + } + } + + #[test] + fn mixed_arrays_and_dictionaries_share_the_same_limit() { + for depth in [64, 65] { + let mut data = String::from("0"); + for level in 0..depth { + data = if level % 2 == 0 { format!("<< /A {data} >>") } else { format!("[{data}]") }; + } + let mut reader = Reader::new(data.as_bytes()); + assert_eq!(reader.skip::>(true).is_some(), depth <= 64); + assert_eq!(reader.skip_depth, 0); + } + } +} diff --git a/vendor/hayro-syntax/src/content/gen_ops.py b/vendor/hayro-syntax/src/content/gen_ops.py new file mode 100644 index 00000000..2d286a50 --- /dev/null +++ b/vendor/hayro-syntax/src/content/gen_ops.py @@ -0,0 +1,222 @@ +from enum import Enum + + +class Type(Enum): + Number = "Number" + String = "String" + Array = "Array" + Dict = "Dict" + Object = "Object" + Stream = "Stream" + VecNum = "SmallVec<[Number; OPERANDS_THRESHOLD]>" + Name = "Name" + + +ops = { + "Compatibility operators": [ + ("BX", "BeginCompatibility", []), + ("EX", "EndCompatibility", []), + ], + "Graphic state operators": [ + ("q", "SaveState", []), + ("Q", "RestoreState", []), + ("cm", "Transform", [Type.Number] * 6), + ("w", "LineWidth", [Type.Number]), + ("J", "LineCap", [Type.Number]), + ("j", "LineJoin", [Type.Number]), + ("M", "MiterLimit", [Type.Number]), + ("d", "DashPattern", [Type.Array, Type.Number]), + ("ri", "RenderingIntent", [Type.Name]), + ("i", "FlatnessTolerance", [Type.Number]), + ("gs", "SetGraphicsState", [Type.Name]), + ], + "Path-construction operators": [ + ("m", "MoveTo", [Type.Number, Type.Number]), + ("l", "LineTo", [Type.Number, Type.Number]), + ("c", "CubicTo", [Type.Number] * 6), + ("v", "CubicStartTo", [Type.Number] * 4), + ("y", "CubicEndTo", [Type.Number] * 4), + ("h", "ClosePath", []), + ("re", "RectPath", [Type.Number] * 4), + ], + "Path-painting operators": [ + ("S", "StrokePath", []), + ("s", "CloseAndStrokePath", []), + ("f", "FillPathNonZero", []), + ("F", "FillPathNonZeroCompatibility", []), + ("f*", "FillPathEvenOdd", []), + ("B", "FillAndStrokeNonZero", []), + ("B*", "FillAndStrokeEvenOdd", []), + ("b", "CloseFillAndStrokeNonZero", []), + ("b*", "CloseFillAndStrokeEvenOdd", []), + ("n", "EndPath", []), + ], + "Clipping path operators": [ + ("W", "ClipNonZero", []), + ("W*", "ClipEvenOdd", []), + ], + "Colour operators": [ + ("CS", "ColorSpaceStroke", [Type.Name]), + ("cs", "ColorSpaceNonStroke", [Type.Name]), + ("SC", "StrokeColor", [Type.VecNum]), + ("SCN", "StrokeColorNamed", True), + ("sc", "NonStrokeColor", [Type.VecNum]), + ("scn", "NonStrokeColorNamed", True), + ("G", "StrokeColorDeviceGray", [Type.Number]), + ("g", "NonStrokeColorDeviceGray", [Type.Number]), + ("RG", "StrokeColorDeviceRgb", [Type.Number] * 3), + ("rg", "NonStrokeColorDeviceRgb", [Type.Number] * 3), + ("K", "StrokeColorCmyk", [Type.Number] * 4), + ("k", "NonStrokeColorCmyk", [Type.Number] * 4), + ], + "Shading operator": [("sh", "Shading", [Type.Name])], + "XObject operator": [("Do", "XObject", [Type.Name])], + "Inline-image operators": [ + ("BI", "InlineImage", [Type.Stream]), + # We do not emit ID and EI in the parser. + # ("ID", "BeginInlineImageData", []), + # ("EI", "EndInlineImage", []), + ], + "Text-state operators": [ + ("Tc", "CharacterSpacing", [Type.Number]), + ("Tw", "WordSpacing", [Type.Number]), + ("Tz", "HorizontalScaling", [Type.Number]), + ("TL", "TextLeading", [Type.Number]), + ("Tf", "TextFont", [Type.Name, Type.Number]), + ("Tr", "TextRenderingMode", [Type.Number]), + ("Ts", "TextRise", [Type.Number]), + ], + "Text-object operators": [ + ("BT", "BeginText", []), + ("ET", "EndText", []), + ], + "Text-positioning operators": [ + ("Td", "NextLine", [Type.Number] * 2), + ("TD", "NextLineAndSetLeading", [Type.Number] * 2), + ("Tm", "SetTextMatrix", [Type.Number] * 6), + ("T*", "NextLineUsingLeading", []), + ], + "Text-showing operators": [ + ("Tj", "ShowText", [Type.String]), + ("'", "NextLineAndShowText", [Type.String]), + ('"', "ShowTextWithParameters", [Type.Number, Type.Number, Type.String]), + ("TJ", "ShowTexts", [Type.Array]), + ], + "Type 3 font operators": [ + ("d0", "ColorGlyph", [Type.Number] * 2), + ("d1", "ShapeGlyph", [Type.Number] * 6), + ], + "Marked content operators": [ + ("MP", "MarkedContentPoint", [Type.Name]), + # Second argument can be name or dict + ("DP", "MarkedContentPointWithProperties", [Type.Name, Type.Object]), + ("BMC", "BeginMarkedContent", [Type.Name]), + # Second argument can be name or dict + ("BDC", "BeginMarkedContentWithProperties", [Type.Name, Type.Object]), + ("EMC", "EndMarkedContent", []), + ], +} + + +def rust_type(t: Type) -> str: + return { + Type.Number: "Number", + Type.String: "&'b object::String<'a>", + Type.Array: "&'b Array<'a>", + Type.Object: "&'b Object<'a>", + Type.Stream: "&'b Stream<'a>", + Type.Name: "&'b Name<'a>", + Type.Dict: "&'b Dict<'a>", + Type.VecNum: "SmallVec<[Number; OPERANDS_THRESHOLD]>", + }[t] + + +def lifetime_if_needed(types): + return ( + "<'b, 'a>" + if any( + t in [Type.Array, Type.Object, Type.Stream, Type.Dict, Type.Name, Type.String] + for t in types + ) + else "" + ) + + +def gen_struct(name, code, types): + lifetime = lifetime_if_needed(types) + count = len(types) + macro_suffix = count + struct = [f"#[derive(Debug, PartialEq, Clone)]"] + if count == 0: + struct.append(f"pub struct {name};") + elif count == 1: + struct.append(f"pub struct {name}{lifetime}(pub {rust_type(types[0])});") + if types[0] == Type.VecNum: + macro_suffix = "_all" + else: + struct.append(f"pub struct {name}{lifetime}(") + struct += [f" pub {rust_type(t)}," for t in types] + struct.append(");") + # Escape the Rust string literal properly + escaped_code = code.replace('"', '\\"') + struct.append(f'op{macro_suffix}!({name}{lifetime}, "{escaped_code}");') + return "\n".join(struct) + + +def gen_enum_variant(name, types): + has_lifetime = name in ["StrokeColorNamed", "NonStrokeColorNamed"] or ( + (type(types) is list) + and any(t in [Type.Array, Type.Object, Type.Stream, Type.Name, Type.String] for t in types) + ) + inner_type = f"{name}<'b, 'a>" if has_lifetime else name + return f"{name}({inner_type})" + + +def gen_dispatch_match(code, name, types): + escaped_code = code.replace('"', '\\"') + return f'b"{escaped_code}" => {name}::from_stack(instruction.operands)?.into(),' + + +# Generate all code pieces +structs = [] +enum_variants = [] +dispatch_arms = [] + +for category in ops.values(): + for code, name, types in category: + if type(types) is list: + structs.append(gen_struct(name, code, types)) + enum_variants.append(gen_enum_variant(name, types)) + dispatch_arms.append(gen_dispatch_match(code, name, types)) + +# Build the final Rust code blocks +struct_block = "\n\n".join(structs) + +enum_block = ( + "#[derive(Debug, PartialEq, Clone)]\n" + "pub enum TypedInstruction<'b, 'a> {\n" + " " + ",\n ".join(enum_variants) + ",\n" + " Fallback(&'b Operator<'a>),\n}" +) + +dispatch_block = ( + "impl<'b, 'a> TypedInstruction<'b, 'a> {\n" + " #[inline(always)]\n" + " pub(crate) fn dispatch(instruction: &Instruction<'b, 'a>) -> Option {\n" + " let op_name = instruction.operator.as_ref();\n" + " Some(match op_name {\n" + + " " + + "\n ".join(dispatch_arms) + + "\n" + " _ => return Some(Self::Fallback(instruction.operator)),\n" + " })\n" + " }\n" + "}" +) + +gen_notice = "// THIS FILE IS AUTO-GENERATED, DO NOT EDIT MANUALLY" +imports = "use crate::content::Operator;" + +joined = "\n\n".join([gen_notice, imports, struct_block, enum_block, dispatch_block]) + +with open("ops_generated.rs", "w") as f: + f.write(joined) diff --git a/vendor/hayro-syntax/src/content/mod.rs b/vendor/hayro-syntax/src/content/mod.rs new file mode 100644 index 00000000..8e47324d --- /dev/null +++ b/vendor/hayro-syntax/src/content/mod.rs @@ -0,0 +1,1017 @@ +/*! +PDF content operators. + +This module provides facilities to read and interpret PDF content streams using +high-level types. + +``` +use hayro_syntax::object::Number; +use hayro_syntax::content::*; +use hayro_syntax::content::ops::*; + +let content_stream = b"1 0 0 -1 0 200 cm +0 1.0 0 rg +0 0 m +200 0 l +200 200 l +0 200 l +h +f"; + +let mut iter = TypedIter::new(content_stream); +assert!(matches!(iter.next(), Some(TypedInstruction::Transform(_)))); +assert!(matches!(iter.next(), Some(TypedInstruction::NonStrokeColorDeviceRgb(_)))); +assert!(matches!(iter.next(), Some(TypedInstruction::MoveTo(_)))); +assert!(matches!(iter.next(), Some(TypedInstruction::LineTo(_)))); +assert!(matches!(iter.next(), Some(TypedInstruction::LineTo(_)))); +assert!(matches!(iter.next(), Some(TypedInstruction::LineTo(_)))); +assert!(matches!(iter.next(), Some(TypedInstruction::ClosePath(_)))); +assert!(matches!(iter.next(), Some(TypedInstruction::FillPathNonZero(_)))); +``` +*/ + +#[allow(missing_docs)] +pub mod ops; + +use crate::content::ops::TypedInstruction; +use crate::object; +use crate::object::dict::InlineImageDict; +use crate::object::name::{Name, skip_name_like}; +use crate::object::{Array, Null, Number, Object, Stream}; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; +use crate::trivia::{Comment, is_white_space_character}; +use crate::util::find_needle; +use core::array; +use core::fmt::{Debug, Formatter}; +use core::ops::Deref; +use core::marker::PhantomData; +use smallvec::SmallVec; + +// 6 operands are used for example for ctm or cubic curves, +// but anything above should be pretty rare (only for example for +// DeviceN color spaces, or invalid PDF files). So we settle on 10. +const OPERANDS_THRESHOLD: usize = 10; + +impl Debug for Operator<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0.as_str()) + } +} + +/// A content stream operator. +#[derive(Clone, PartialEq)] +pub struct Operator<'a>(Name<'a>); + +impl Deref for Operator<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + +impl Skippable for Operator<'_> { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + skip_name_like(r, false).map(|_| ()) + } +} + +impl<'a> Readable<'a> for Operator<'a> { + fn read(r: &mut Reader<'a>, _: &ReaderContext<'a>) -> Option { + let start = r.offset(); + skip_name_like(r, false)?; + let end = r.offset(); + let data = r.range(start..end)?; + + if data.is_empty() { + return None; + } + + Some(Self(Name::new(data)?)) + } +} + +/// PdfCraft patch: how many `EI` candidates an inline image examines, and how far past each +/// one the end-of-data heuristic looks, before taking the current candidate. Unbounded, the +/// heuristic re-parsed the rest of the stream for every candidate, and each re-parse ran the +/// heuristic again on the inline images it met: a fuzzed file hung for minutes. +const MAX_EI_CANDIDATES: u32 = 256; +const MAX_EI_LOOKAHEAD_STEPS: u32 = 256; + +/// A source of PDF content instructions. +/// +/// The tokenizer owns any state needed to assemble one instruction and may choose how bytes are +/// backed. The current production implementation is [`UntypedIter`], which reads one complete +/// byte slice; a streaming implementation can use the same interface without changing typed +/// operator dispatch. +pub trait ContentTokenizer<'a> { + /// Return the next instruction, or `None` after the content stream ends or becomes invalid. + fn next_instruction<'b>(&'b mut self) -> Option>; +} + +/// An iterator over operators in the PDF content streams, providing raw access to the +/// instructions. This is the slice-backed [`ContentTokenizer`] implementation. +#[derive(Clone)] +pub struct UntypedIter<'a> { + reader: Reader<'a>, + stack: Stack<'a>, + operator: Option>, + /// PdfCraft patch: this iterator only checks whether data reads as content (the + /// end-of-data heuristic below); inline images it meets end at their first `EI`. + lookahead: bool, + /// PdfCraft patch: whether `reader` holds the whole content stream. The end of a window of a + /// longer stream can fall inside an instruction, so nothing that reads up to it is decided + /// (see `needs_more`). + complete: bool, + /// PdfCraft patch: set when `next` stopped because the window ran out before the next + /// instruction was decided. The caller reads more data and resumes at `committed`. + needs_more: bool, + /// PdfCraft patch: the offset just after the instruction `next` returned last. + committed: usize, +} + +impl<'a> UntypedIter<'a> { + /// Create a new untyped iterator. + pub fn new(data: &'a [u8]) -> Self { + Self::new_window(data, true) + } + + /// PdfCraft patch: an iterator over `data`, which is the whole stream when `complete`, and + /// otherwise the start of a longer one. + pub(crate) fn new_window(data: &'a [u8], complete: bool) -> Self { + Self { + reader: Reader::new(data), + stack: Stack::new(), + operator: None, + lookahead: false, + complete, + needs_more: false, + committed: 0, + } + } + + /// Create a new empty untyped iterator. + pub fn empty() -> Self { + Self::new_window(&[], true) + } + + /// Return the next instruction. + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Option> { + self.stack.clear(); + self.operator = None; + self.needs_more = false; + self.reader.clear_looked_past_end(); + + if self.complete { + self.reader.skip_white_spaces_and_comments(); + } else { + // A window may drop what comes before the next instruction, blanks included, but not a + // comment that the window ends in: that comment may go on in the next window. + self.committed = self.skip_blanks_and_comments(); + } + + while !self.reader.at_end() { + // I believe booleans/null never appear as an operator? + if matches!( + self.reader.peek_byte()?, + b'/' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'[' | b'<' | b'(' + ) { + // See issue 994. In all sane scenarios, if the next byte is a number + // it has to be an operand (a number). However, it's possible that + // the number is followed by a regular character, in which case it + // should behave more like an operator (even though there exists + // no operator that starts with a number). In order to preserve + // similar behavior to Acrobat and Chromium, we try to consume + // such an operator and then simply skip it. + if let Some(object) = self.reader.read_without_context::>() { + if self.undecided() { + return None; + } + self.stack.push(object)?; + } else if self.reader.read_without_context::>().is_some() { + if self.undecided() { + return None; + } + self.stack.clear(); + } else { + self.undecided(); + return None; + } + } else { + let operator = match self.reader.read_without_context::>() { + Some(o) => o, + None => { + if self.undecided() { + return None; + } + + warn!("failed to read operator in content stream"); + + self.reader.jump_to_end(); + return None; + } + }; + if self.undecided() { + return None; + } + + // Inline images need special casing... + if operator.as_ref() == b"BI" { + // The ID operator will already be consumed by this. + let Some(inline_dict) = self + .reader + .read_without_context::>() + else { + self.undecided(); + return None; + }; + let dict = inline_dict.get_dict().clone(); + + // One whitespace after "ID". + if self.reader.read_white_space().is_none() { + self.undecided(); + return None; + } + + let stream_data = self.reader.tail()?; + let start_offset = self.reader.offset(); + let mut candidates = 0; + // PdfCraft patch: set when a look-ahead below ran into the end of the window. + let mut lookahead_touched = false; + let mut accepted = false; + + 'outer: while let Some(pos) = find_needle(self.reader.tail()?, b"EI") { + self.reader.read_bytes(pos)?; + + if self.reader.peek_bytes(2) == Some(b"EI") { + // If the following character is not a whitespace character, then we are in a ASCII-85 stream. + if self + .reader + .peek_bytes(3) + .is_some_and(|b| !is_white_space_character(b[2])) + { + self.reader.read_bytes(3)?; + + continue; + } + + let end_offset = self.reader.offset() - start_offset; + let image_data = &stream_data[..end_offset]; + + let stream = Stream::new(image_data, dict.clone()); + + // Note that there is a possibility that the encoded stream data + // contains the "EI" operator as part of the data, in which case we + // cannot confidently know whether we have hit the actual end of the + // stream. See also + // PDF 2.0 does have a `/Length` attribute we can read, but since it's relatively + // new we don't bother trying to read it. + let tail = &self.reader.tail()?[2..]; + let mut find_reader = Reader::new(tail); + // PdfCraft patch: bounded (see `MAX_EI_CANDIDATES`). + candidates += 1; + let mut steps = 0; + let heuristic = !self.lookahead && candidates <= MAX_EI_CANDIDATES; + + while heuristic && steps < MAX_EI_LOOKAHEAD_STEPS { + if find_reader.at_end() { + lookahead_touched = true; + break; + } + steps += 1; + let remaining = find_reader.tail()?; + let next_ei = find_needle(remaining, b"EI"); + let next_bi = find_needle(remaining, b"BI"); + + let (next_pos, is_ei) = match (next_ei, next_bi) { + (Some(ei), Some(bi)) if ei <= bi => (ei, true), + (Some(_), Some(bi)) => (bi, false), + (Some(ei), None) => (ei, true), + (None, Some(bi)) => (bi, false), + (None, None) => { + lookahead_touched = true; + break; + } + }; + + find_reader.read_bytes(next_pos)?; + + if is_ei { + let analyze_data = &tail[..find_reader.offset()]; + + // If there is any binary data in-between, we for sure + // have not reached the end. + if analyze_data.iter().any(|c| !c.is_ascii()) { + self.reader.read_bytes(2)?; + continue 'outer; + } + + // Otherwise, the only possibility that we reached an + // "EI", even though the previous one was valid, is + // that it's part of a string in the content + // stream that follows the inline image. Therefore, + // it should be valid to interpret `tail` as a content + // stream and there should be at least one text-related + // operator that can be parsed correctly. + + let mut iter = TypedIter::from_untyped(UntypedIter { + lookahead: true, + ..UntypedIter::new_window(tail, self.complete) + }); + let mut found = false; + let mut counter = 0; + + while let Some(op) = iter.next() { + // If we have read more than 20 valid operators, it should be + // safe to assume that we are in a content stream, so abort + // early. The only situation where this could reasonably + // be violated is if we have 20 subsequent instances of + // q/Q in the image data, which seems very unlikely. + if counter >= 20 { + found = true; + break; + } + + if matches!( + op, + TypedInstruction::NextLineAndShowText(_) + | TypedInstruction::ShowText(_) + | TypedInstruction::ShowTexts(_) + | TypedInstruction::ShowTextWithParameters(_) + ) { + // Now it should be safe to assume that the + // previous `EI` was the correct one. + found = true; + break; + } + + counter += 1; + } + lookahead_touched |= + iter.needs_more() || iter.untyped.reader.looked_past_end(); + + if !found { + // Seems like the data in-between is not a valid content + // stream, so we are likely still within the image data. + self.reader.read_bytes(2)?; + continue 'outer; + } + } else { + // Possibly another inline image, if so, the previously found "EI" + // is indeed the end of data. + let mut cloned = find_reader.clone(); + cloned.read_bytes(2)?; + let is_inline_image_dict = cloned + .read_without_context::>() + .is_some(); + lookahead_touched |= cloned.looked_past_end(); + if is_inline_image_dict { + break; + } + } + + find_reader.read_byte()?; + } + + // PdfCraft patch: the end is only certain if no look-ahead needed bytes + // past the window. Otherwise read more and decide again. + if !self.complete + && (lookahead_touched || self.reader.looked_past_end()) + { + self.needs_more = true; + return None; + } + + self.stack.push(Object::Stream(stream))?; + + self.reader.read_bytes(2)?; + self.reader.skip_white_spaces(); + + accepted = true; + + break; + } + } + + // PdfCraft patch: without an accepted "EI" the image may still end in later data. + if !accepted && !self.complete { + self.needs_more = true; + return None; + } + } + + self.operator = Some(operator); + self.committed = self.reader.offset(); + return Some(Instruction { + operands: &self.stack, + operator: self.operator.as_ref().unwrap(), + }); + } + + self.reader.skip_white_spaces_and_comments(); + } + + // PdfCraft patch: the window ended between instructions, and another may follow. + if !self.complete { + self.needs_more = true; + } + + None + } + + /// PdfCraft patch: skips the blanks and comments before an instruction, as + /// `skip_white_spaces_and_comments` does, and returns where a window may be cut: the start of a + /// comment that runs into the end of the window, or else where the skipping stopped. + fn skip_blanks_and_comments(&mut self) -> usize { + let mut open_comment = None; + while let Some(b) = self.reader.peek_byte() { + if is_white_space_character(b) { + self.reader.skip_white_spaces(); + } else if b == b'%' { + let start = self.reader.offset(); + Comment::skip(&mut self.reader, true); + open_comment = self.reader.at_end().then_some(start); + } else { + break; + } + } + + open_comment.unwrap_or(self.reader.offset()) + } + + /// PdfCraft patch: whether the instruction being read ran into the end of a window, so it + /// cannot be decided yet. Records that more data is needed. + fn undecided(&mut self) -> bool { + if !self.complete && self.reader.looked_past_end() { + self.needs_more = true; + } + + self.needs_more + } +} + +impl<'a> ContentTokenizer<'a> for UntypedIter<'a> { + fn next_instruction<'b>(&'b mut self) -> Option> { + self.next() + } +} + + +/// An iterator over PDF content streams that provide access to the instructions +/// in a typed fashion. +#[derive(Clone)] +pub struct TypedIter<'a, T = UntypedIter<'a>> { + untyped: T, + marker: PhantomData<&'a ()>, +} + +impl<'a> TypedIter<'a, UntypedIter<'a>> { + /// Create a new typed iterator. + pub fn new(data: &'a [u8]) -> Self { + Self { + untyped: UntypedIter::new(data), + marker: PhantomData, + } + } + + /// PdfCraft patch: an iterator over a window of a content stream: the whole stream when + /// `complete`, and otherwise the start of a longer one (see [`TypedIter::needs_more`]). + pub fn new_window(data: &'a [u8], complete: bool) -> Self { + Self { + untyped: UntypedIter::new_window(data, complete), + marker: PhantomData, + } + } + + /// PdfCraft patch: whether the last `next` returned `None` only because the window ran out + /// before the next instruction was decided. Read more of the stream, then resume at + /// [`TypedIter::committed`] in a window that starts there. + pub fn needs_more(&self) -> bool { + self.untyped.needs_more + } + + /// PdfCraft patch: the offset in the window just after the last instruction `next` returned. + pub fn committed(&self) -> usize { + self.untyped.committed + } + + pub(crate) fn from_untyped(untyped: UntypedIter<'a>) -> Self { + Self { untyped, marker: PhantomData } + } +} + +impl<'a, T> TypedIter<'a, T> +where + T: ContentTokenizer<'a>, +{ + /// Adapt a content tokenizer to the typed operator interface. + pub fn from_tokenizer(tokenizer: T) -> Self { + Self { untyped: tokenizer, marker: PhantomData } + } + + /// Return the next typed instruction. + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Option> { + let op = self.untyped.next_instruction()?; + // TODO: Explore whether dispatching can be made more efficient. + match TypedInstruction::dispatch(&op) { + Some(op) => Some(op), + // In case this returns `None`, the content stream is invalid. In case a path-drawing + // operator was used, let's abort completely, otherwise we might end up drawing random stuff. + // However, for other operators it could be worth it to just skip it but keep attempting + // to read other content operators. + None => { + if [ + &b"m"[..], + &b"l"[..], + &b"c"[..], + &b"v"[..], + &b"y"[..], + &b"h"[..], + &b"re"[..], + ] + .contains(&op.operator.0.deref()) + { + None + } else { + Some(TypedInstruction::Fallback(op.operator)) + } + } + } + } +} + +/// An instruction (= operator and its operands) in a content stream. +pub struct Instruction<'b, 'a> { + /// The stack containing the operands. + pub operands: &'b Stack<'a>, + /// The actual operator. + pub operator: &'b Operator<'a>, +} + +impl<'b, 'a> Instruction<'b, 'a> { + /// An iterator over the operands of the instruction. + pub fn operands(&self) -> OperandIterator<'b, 'a> { + OperandIterator::new(self.operands) + } +} + +/// A stack holding the arguments of an operator. +pub struct Stack<'a> { + // TODO: Explore using an object pool to avoid repeatedly + // allocating/deallocating objects. + data: [Object<'a>; OPERANDS_THRESHOLD], + len: usize, +} + +impl<'a> Default for Stack<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> Stack<'a> { + /// Create a new, empty stack. + pub fn new() -> Self { + Self { + data: array::from_fn(|_| Object::Null(Null)), + len: 0, + } + } + + fn push(&mut self, operand: Object<'a>) -> Option<()> { + if self.len >= OPERANDS_THRESHOLD { + return None; + } + + self.data[self.len] = operand; + self.len += 1; + Some(()) + } + + fn clear(&mut self) { + self.len = 0; + } + + fn len(&self) -> usize { + self.len + } + + fn as_slice(&self) -> &[Object<'a>] { + &self.data[..self.len] + } + + fn get<'b, T>(&'b self, index: usize) -> Option + where + T: Operand<'b, 'a>, + { + self.as_slice().get(index).and_then(T::from_object) + } + + fn get_all<'b, T>(&'b self) -> Option> + where + T: Operand<'b, 'a>, + { + let mut operands = SmallVec::new(); + + for op in self.as_slice() { + let converted = T::from_object(op)?; + operands.push(converted); + } + + Some(operands) + } +} + +impl Debug for Stack<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_list().entries(self.as_slice()).finish() + } +} + +impl Clone for Stack<'_> { + fn clone(&self) -> Self { + let mut stack = Self::new(); + for item in self.as_slice() { + stack.push(item.clone()).unwrap(); + } + stack + } +} + +impl PartialEq for Stack<'_> { + fn eq(&self, other: &Self) -> bool { + self.as_slice() == other.as_slice() + } +} + +trait Operand<'b, 'a>: Sized { + fn from_object(object: &'b Object<'a>) -> Option; +} + +impl<'b, 'a> Operand<'b, 'a> for Number { + fn from_object(object: &'b Object<'a>) -> Option { + match object { + Object::Number(n) => Some(*n), + _ => None, + } + } +} + +impl<'b, 'a> Operand<'b, 'a> for &'b object::String<'a> { + fn from_object(object: &'b Object<'a>) -> Option { + match object { + Object::String(s) => Some(s), + _ => None, + } + } +} + +impl<'b, 'a> Operand<'b, 'a> for &'b Name<'a> { + fn from_object(object: &'b Object<'a>) -> Option { + match object { + Object::Name(n) => Some(n), + _ => None, + } + } +} + +impl<'b, 'a> Operand<'b, 'a> for &'b Array<'a> { + fn from_object(object: &'b Object<'a>) -> Option { + match object { + Object::Array(a) => Some(a), + _ => None, + } + } +} + +impl<'b, 'a> Operand<'b, 'a> for &'b Stream<'a> { + fn from_object(object: &'b Object<'a>) -> Option { + match object { + Object::Stream(s) => Some(s), + _ => None, + } + } +} + +impl<'b, 'a> Operand<'b, 'a> for &'b Object<'a> { + fn from_object(object: &'b Object<'a>) -> Option { + Some(object) + } +} + +/// An iterator over the operands of an operator. +pub struct OperandIterator<'b, 'a> { + stack: &'b Stack<'a>, + cur_index: usize, +} + +impl<'b, 'a> OperandIterator<'b, 'a> { + fn new(stack: &'b Stack<'a>) -> Self { + Self { + stack, + cur_index: 0, + } + } +} + +impl<'b, 'a> Iterator for OperandIterator<'b, 'a> { + type Item = &'b Object<'a>; + + fn next(&mut self) -> Option { + if let Some(item) = self.stack.as_slice().get(self.cur_index) { + self.cur_index += 1; + + Some(item) + } else { + None + } + } +} + +pub(crate) trait OperatorTrait<'b, 'a>: Sized { + const OPERATOR: &'static str; + + fn from_stack(stack: &'b Stack<'a>) -> Option; +} + +mod macros { + macro_rules! op_impl { + ($t:ident $(<$($l:lifetime),+>)?, $e:expr, $n:expr, |$stack:ident : $stack_ty:ty| $body:block) => { + impl<'b, 'a> OperatorTrait<'b, 'a> for $t$(<$($l),+>)? { + const OPERATOR: &'static str = $e; + + #[inline(always)] + fn from_stack($stack: $stack_ty) -> Option { + $body.or_else(|| { + warn!("failed to convert operands for operator {}", Self::OPERATOR); + + None + }) + } + } + + impl<'b, 'a> From<$t$(<$($l),+>)?> for TypedInstruction<'b, 'a> { + fn from(value: $t$(<$($l),+>)?) -> Self { + TypedInstruction::$t(value) + } + } + + impl<'b, 'a> TryFrom> for $t$(<$($l),+>)? { + type Error = (); + + fn try_from(value: TypedInstruction<'b, 'a>) -> core::result::Result { + match value { + TypedInstruction::$t(e) => Ok(e), + _ => Err(()) + } + } + } + }; + } + + // The `shift` parameter will always be 0 in valid PDFs. The purpose of the parameter is + // so that in case there are garbage operands in the content stream, we prefer to use + // the operands that are closer to the operator instead of the values at the bottom + // of the stack. + + macro_rules! op0 { + ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => { + crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 0, |_stack: &'b Stack<'a>| { + Some(Self) + }); + } + } + + macro_rules! op1 { + ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => { + crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 1, |stack: &'b Stack<'a>| { + let shift = stack.len().saturating_sub(1); + Some(Self(stack.get(0 + shift)?)) + }); + } + } + + macro_rules! op_all { + ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => { + crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, u8::MAX as usize, |stack: &'b Stack<'a>| { + Some(Self(stack.get_all()?)) + }); + } + } + + macro_rules! op2 { + ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => { + crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 2, |stack: &'b Stack<'a>| { + let shift = stack.len().saturating_sub(2); + Some(Self(stack.get(0 + shift)?, stack.get(1 + shift)?)) + }); + } + } + + macro_rules! op3 { + ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => { + crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 3, |stack: &'b Stack<'a>| { + let shift = stack.len().saturating_sub(3); + Some(Self(stack.get(0 + shift)?, stack.get(1 + shift)?, + stack.get(2 + shift)?)) + }); + } + } + + macro_rules! op4 { + ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => { + crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 4, |stack: &'b Stack<'a>| { + let shift = stack.len().saturating_sub(4); + Some(Self(stack.get(0 + shift)?, stack.get(1 + shift)?, + stack.get(2 + shift)?, stack.get(3 + shift)?)) + }); + } + } + + macro_rules! op6 { + ($t:ident $(<$($l:lifetime),+>)?, $e:expr) => { + crate::content::macros::op_impl!($t$(<$($l),+>)?, $e, 6, |stack: &'b Stack<'a>| { + let shift = stack.len().saturating_sub(6); + Some(Self(stack.get(0 + shift)?, stack.get(1 + shift)?, + stack.get(2 + shift)?, stack.get(3 + shift)?, + stack.get(4 + shift)?, stack.get(5 + shift)?)) + }); + } + } + + pub(crate) use op_all; + pub(crate) use op_impl; + pub(crate) use op0; + pub(crate) use op1; + pub(crate) use op2; + pub(crate) use op3; + pub(crate) use op4; + pub(crate) use op6; +} + +#[cfg(test)] +mod tests { + use super::{ContentTokenizer, UntypedIter}; + + fn operators(data: &[u8]) -> Vec> { + let mut tokenizer = UntypedIter::new(data); + let mut result = Vec::new(); + while let Some(instruction) = tokenizer.next_instruction() { + result.push(instruction.operator.as_ref().to_vec()); + } + result + } + + #[test] + fn slice_tokenizer_keeps_the_existing_operator_sequence() { + let data = b"% comment\n1 -2.5 /Name /A#20B (a\\(b\\) \\101) <4869> [1 2] << /Key (value) >> [(a) 1] TJ BI /W 1 /H 1 /BPC 8 /CS /DeviceGray ID \x00 EI q Q"; + + assert_eq!(operators(data), [b"TJ".to_vec(), b"BI".to_vec(), b"q".to_vec(), b"Q".to_vec()]); + } + + #[test] + fn tokenizer_interface_preserves_every_byte_boundary() { + let data = b"% comment\n1 -2.5 /Name /A#20B (a\\(b\\) \\101) <4869> [1 2] << /Key (value) >> [(a) 1] TJ BI /W 1 /H 1 /BPC 8 /CS /DeviceGray ID \x00 EI q Q"; + let expected = operators(data); + + for split in 1..data.len() { + let mut joined = Vec::with_capacity(data.len()); + joined.extend_from_slice(&data[..split]); + joined.extend_from_slice(&data[split..]); + assert_eq!(operators(&joined), expected, "split at byte {split}"); + } + } + + /// The instructions of `data`, read from the whole of it. + fn slice_instructions(data: &[u8]) -> Vec { + let mut tokenizer = UntypedIter::new(data); + let mut result = Vec::new(); + while let Some(instruction) = tokenizer.next_instruction() { + result.push(format!("{:?} {:?}", instruction.operator, instruction.operands)); + } + result + } + + /// The instructions of `data` as a window driver reads them: the stream arrives in pieces of + /// the lengths `next_piece` gives. Each window holds the bytes from the first instruction + /// that is not yet decided, and another piece is appended when a window runs out. + fn windowed_instructions(data: &[u8], mut next_piece: impl FnMut() -> usize) -> Vec { + let mut result = Vec::new(); + let mut window: Vec = Vec::new(); + let mut fed = 0; + let mut exhausted = false; + loop { + let (committed, needs_more) = { + let mut tokenizer = UntypedIter::new_window(&window, exhausted); + while let Some(instruction) = tokenizer.next_instruction() { + result.push(format!("{:?} {:?}", instruction.operator, instruction.operands)); + } + (tokenizer.committed, tokenizer.needs_more) + }; + window.drain(..committed); + if !needs_more || exhausted { + break; + } + if fed == data.len() { + exhausted = true; + continue; + } + let end = fed.saturating_add(next_piece().max(1)).min(data.len()); + window.extend_from_slice(&data[fed..end]); + fed = end; + } + + result + } + + /// Content whose instructions are hard to read in pieces: inline images with `EI` in their + /// data or right after it, unterminated tokens, and operands that are too many or invalid. + const WINDOWED_CORPUS: [&[u8]; 14] = [ + b"% comment\n1 -2.5 /Name /A#20B (a\\(b\\) \\101) <4869> [1 2] << /Key (value) >> [(a) 1] TJ BI /W 1 /H 1 /BPC 8 /CS /DeviceGray ID \x00 EI q Q", + b"BT /F1 12 Tf 10 20 Td [(Hel) -250 (lo)] TJ (x) Tj ET", + b"q 1 0 0 1 0 0 cm BI /W 2 /H 2 /BPC 8 /CS /G ID \x00\x01EI\x00 EI Q 0 0 m 1 1 l S", + b"BI /W 1 /H 1 /BPC 8 /CS /G ID EI x EI EI BT (a) Tj ET", + b"0.5 .5 -.5 +5 12.3.4 f", + b"/A B C /D 1 2 3 4 5 6 7 8 9 10 11 12 re", + b"BI /W 1 /H 1 /BPC 8 /CS /G ID\n\x00\nEI BI /W 1 /H 1 /BPC 8 /CS /G ID \x01 EI Q", + b"(unterminated string 1 0 0 1 0 0 cm", + b"1 0 0 1 0 0 cm\n%comment at the end", + b"BI /W 1 /H 1 /BPC 8 /CS /G ID \x00 EIxy q", + b"BI /W 2 /H 1 /BPC 8 /CS /G ID \x00\x01 EI \x00 EI \x01 q", + b"[1 [2 [3]]] 0 d q Q", + b"/Lbl BDC EMC 1 w", + b"<> BDC BI /W 1 /H 1 /BPC 8 /CS /G ID \x07 EI (end) Tj", + ]; + + #[test] + fn windows_keep_every_instruction_at_every_split() { + for data in WINDOWED_CORPUS { + let expected = slice_instructions(data); + for split in 1..=data.len() { + let mut lengths = [split, data.len()].into_iter(); + let windowed = windowed_instructions(data, || lengths.next().unwrap_or(data.len())); + assert_eq!(windowed, expected, "{:?} split at byte {split}", String::from_utf8_lossy(data)); + } + } + } + + #[test] + fn windows_keep_every_instruction_in_pieces_of_any_size() { + for data in WINDOWED_CORPUS { + let expected = slice_instructions(data); + for piece in 1..=data.len() { + let windowed = windowed_instructions(data, || piece); + assert_eq!(windowed, expected, "{:?} in pieces of {piece}", String::from_utf8_lossy(data)); + } + } + } + + #[test] + fn windows_drop_blanks_and_keep_a_comment_that_may_go_on() { + // Blanks alone decide nothing, so a window of them can be dropped whole. + let blanks = vec![b' '; 4096]; + let mut tokenizer = UntypedIter::new_window(&blanks, false); + assert!(tokenizer.next().is_none()); + assert!(tokenizer.needs_more); + assert_eq!(tokenizer.committed, blanks.len()); + + // The window ends in a comment: it is kept, from its `%`, for the next window. + let data = b"1 0 0 1 0 0 cm % a comment that goes on"; + let mut tokenizer = UntypedIter::new_window(data, false); + assert!(tokenizer.next().is_some()); + assert!(tokenizer.next().is_none()); + assert!(tokenizer.needs_more); + assert_eq!(tokenizer.committed, data.iter().position(|&b| b == b'%').expect("a comment")); + } + + #[test] + fn windows_match_the_slice_on_random_content() { + const PARTS: [&[u8]; 24] = [ + b"BI", b" ID ", b"EI", b" EI ", b"/W", b" 1 ", b"-2.5", b".5", b"(", b")", b"\\", b"<", + b">", b"[", b"]", b"TJ", b"Tj", b"q", b"Q", b"re", b"f", b"% c\n", b"\n", b"\x00\xff", + ]; + // A fixed xorshift sequence, so that a failure can be reproduced. + let mut state = 0x9e37_79b9_7f4a_7c15_u64; + let mut next = move || { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state + }; + + for _ in 0..2000 { + let mut data = Vec::new(); + for _ in 0..next() % 40 { + let part = PARTS[(next() % PARTS.len() as u64) as usize]; + data.extend_from_slice(part); + } + + let expected = slice_instructions(&data); + let windowed = windowed_instructions(&data, || 1 + (next() % 12) as usize); + assert_eq!(windowed, expected, "{:?}", String::from_utf8_lossy(&data)); + } + } +} diff --git a/vendor/hayro-syntax/src/content/ops.rs b/vendor/hayro-syntax/src/content/ops.rs new file mode 100644 index 00000000..b0661658 --- /dev/null +++ b/vendor/hayro-syntax/src/content/ops.rs @@ -0,0 +1,259 @@ +//! Content stream operators. + +use crate::content::{Instruction, OPERANDS_THRESHOLD, OperatorTrait, Stack}; +use crate::object; +use crate::object::Array; +use crate::object::Name; +use crate::object::Number; +use crate::object::Object; +use crate::object::Stream; +use smallvec::{SmallVec, smallvec}; + +use crate::content::macros::{op_all, op_impl, op0, op1, op2, op3, op4, op6}; + +include!("ops_generated.rs"); + +// Need to special-case those because they have variable arguments. + +fn parse_named_color<'b, 'a>( + objects: &'b [Object<'a>], +) -> Option<(SmallVec<[Number; OPERANDS_THRESHOLD]>, Option<&'b Name<'a>>)> { + let mut nums = smallvec![]; + let mut name = None; + + for o in objects { + match o { + Object::Number(n) => nums.push(*n), + Object::Name(n) => name = Some(n), + _ => { + warn!("encountered unknown object {o:?} when parsing scn/SCN"); + + return None; + } + } + } + + Some((nums, name)) +} + +#[derive(Debug, Clone, PartialEq)] +pub struct StrokeColorNamed<'b, 'a>( + pub SmallVec<[Number; OPERANDS_THRESHOLD]>, + pub Option<&'b Name<'a>>, +); + +op_impl!( + StrokeColorNamed<'b, 'a>, + "SCN", + u8::MAX as usize, + |stack: &'b Stack<'a>| { + let (nums, name) = parse_named_color(stack.as_slice())?; + Some(StrokeColorNamed(nums, name)) + } +); + +#[derive(Debug, PartialEq, Clone)] +pub struct NonStrokeColorNamed<'b, 'a>( + pub SmallVec<[Number; OPERANDS_THRESHOLD]>, + pub Option<&'b Name<'a>>, +); + +op_impl!( + NonStrokeColorNamed<'b, 'a>, + "scn", + u8::MAX as usize, + |stack: &'b Stack<'a>| { + let (nums, name) = parse_named_color(stack.as_slice())?; + Some(NonStrokeColorNamed(nums, name)) + } +); + +#[cfg(test)] +mod tests { + use crate::content::TypedIter; + use crate::content::ops::{ + BeginMarkedContentWithProperties, ClosePath, EndMarkedContent, FillPathNonZero, LineTo, + MarkedContentPointWithProperties, MoveTo, NonStrokeColorDeviceRgb, NonStrokeColorNamed, + SetGraphicsState, StrokeColorNamed, Transform, TypedInstruction, + }; + use crate::object::Name; + use crate::object::Number; + use crate::object::Object; + use crate::object::{Dict, FromBytes}; + fn n(num: i32) -> Number { + Number::from_i32(num) + } + + #[test] + fn basic_ops_1() { + let input = b" +1 0 0 -1 0 200 cm +/g0 gs +1 0 0 rg +"; + + let mut iter = TypedIter::new(input); + + assert!(matches!( + iter.next(), + Some(TypedInstruction::Transform(Transform(a, b, c, d, e, f))) + if [a, b, c, d, e, f] == [n(1), n(0), n(0), n(-1), n(0), n(200)] + )); + assert!(matches!( + iter.next(), + Some(TypedInstruction::SetGraphicsState(SetGraphicsState(name))) + if name.as_ref() == b"g0" + )); + assert!(matches!( + iter.next(), + Some(TypedInstruction::NonStrokeColorDeviceRgb(NonStrokeColorDeviceRgb(r, g, b))) + if [r, g, b] == [n(1), n(0), n(0)] + )); + assert!(iter.next().is_none()); + } + + #[test] + fn basic_ops_2() { + let input = b" +20 20 m +180 20 l +180 180 l +20 180 l +h +f +"; + + let mut iter = TypedIter::new(input); + + assert!(matches!( + iter.next(), + Some(TypedInstruction::MoveTo(MoveTo(x, y))) if [x, y] == [n(20), n(20)] + )); + assert!(matches!( + iter.next(), + Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(180), n(20)] + )); + assert!(matches!( + iter.next(), + Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(180), n(180)] + )); + assert!(matches!( + iter.next(), + Some(TypedInstruction::LineTo(LineTo(x, y))) if [x, y] == [n(20), n(180)] + )); + assert!(matches!( + iter.next(), + Some(TypedInstruction::ClosePath(ClosePath)) + )); + assert!(matches!( + iter.next(), + Some(TypedInstruction::FillPathNonZero(FillPathNonZero)) + )); + assert!(iter.next().is_none()); + } + + #[test] + fn scn() { + let input = b" +0.0 scn +1.0 1.0 1.0 /DeviceRgb SCN +"; + + let mut iter = TypedIter::new(input); + + match iter.next() { + Some(TypedInstruction::NonStrokeColorNamed(NonStrokeColorNamed(nums, None))) => { + assert_eq!(nums.as_slice(), &[Number::from_f32(0.0)]); + } + other => panic!("unexpected instruction: {other:?}"), + } + + match iter.next() { + Some(TypedInstruction::StrokeColorNamed(StrokeColorNamed(nums, Some(name)))) => { + assert_eq!( + nums.as_slice(), + &[ + Number::from_f32(1.0), + Number::from_f32(1.0), + Number::from_f32(1.0) + ] + ); + assert_eq!(name.as_ref(), b"DeviceRgb"); + } + other => panic!("unexpected instruction: {other:?}"), + } + + assert!(iter.next().is_none()); + } + + #[test] + fn dp() { + let input = b"/Attribute<> DP"; + + let mut iter = TypedIter::new(input); + + match iter.next() { + Some(TypedInstruction::MarkedContentPointWithProperties( + MarkedContentPointWithProperties(name, object), + )) => { + assert_eq!(name.as_ref(), b"Attribute"); + assert_eq!( + object, + &Object::Dict(Dict::from_bytes(b"<>").unwrap()) + ); + } + other => panic!("unexpected instruction: {other:?}"), + } + + assert!(iter.next().is_none()); + } + + #[test] + fn bdc_with_dict() { + let input = b"/Span << /MCID 0 /Alt (Alt)>> BDC EMC"; + + let mut iter = TypedIter::new(input); + + match iter.next() { + Some(TypedInstruction::BeginMarkedContentWithProperties( + BeginMarkedContentWithProperties(name, object), + )) => { + assert_eq!(name.as_ref(), b"Span"); + assert_eq!( + object, + &Object::Dict(Dict::from_bytes(b"<< /MCID 0 /Alt (Alt)>>").unwrap()) + ); + } + other => panic!("unexpected instruction: {other:?}"), + } + + assert!(matches!( + iter.next(), + Some(TypedInstruction::EndMarkedContent(EndMarkedContent)) + )); + assert!(iter.next().is_none()); + } + + #[test] + fn bdc_with_name() { + let input = b"/Span /Name BDC EMC"; + + let mut iter = TypedIter::new(input); + + match iter.next() { + Some(TypedInstruction::BeginMarkedContentWithProperties( + BeginMarkedContentWithProperties(name, object), + )) => { + assert_eq!(name.as_ref(), b"Span"); + assert_eq!(object, &Object::Name(Name::new_unescaped(b"Name"))); + } + other => panic!("unexpected instruction: {other:?}"), + } + + assert!(matches!( + iter.next(), + Some(TypedInstruction::EndMarkedContent(EndMarkedContent)) + )); + assert!(iter.next().is_none()); + } +} diff --git a/vendor/hayro-syntax/src/content/ops_generated.rs b/vendor/hayro-syntax/src/content/ops_generated.rs new file mode 100644 index 00000000..1028193c --- /dev/null +++ b/vendor/hayro-syntax/src/content/ops_generated.rs @@ -0,0 +1,528 @@ +// THIS FILE IS AUTO-GENERATED, DO NOT EDIT MANUALLY + +use crate::content::Operator; + +#[derive(Debug, PartialEq, Clone)] +pub struct BeginCompatibility; +op0!(BeginCompatibility, "BX"); + +#[derive(Debug, PartialEq, Clone)] +pub struct EndCompatibility; +op0!(EndCompatibility, "EX"); + +#[derive(Debug, PartialEq, Clone)] +pub struct SaveState; +op0!(SaveState, "q"); + +#[derive(Debug, PartialEq, Clone)] +pub struct RestoreState; +op0!(RestoreState, "Q"); + +#[derive(Debug, PartialEq, Clone)] +pub struct Transform( + pub Number, + pub Number, + pub Number, + pub Number, + pub Number, + pub Number, +); +op6!(Transform, "cm"); + +#[derive(Debug, PartialEq, Clone)] +pub struct LineWidth(pub Number); +op1!(LineWidth, "w"); + +#[derive(Debug, PartialEq, Clone)] +pub struct LineCap(pub Number); +op1!(LineCap, "J"); + +#[derive(Debug, PartialEq, Clone)] +pub struct LineJoin(pub Number); +op1!(LineJoin, "j"); + +#[derive(Debug, PartialEq, Clone)] +pub struct MiterLimit(pub Number); +op1!(MiterLimit, "M"); + +#[derive(Debug, PartialEq, Clone)] +pub struct DashPattern<'b, 'a>( + pub &'b Array<'a>, + pub Number, +); +op2!(DashPattern<'b, 'a>, "d"); + +#[derive(Debug, PartialEq, Clone)] +pub struct RenderingIntent<'b, 'a>(pub &'b Name<'a>); +op1!(RenderingIntent<'b, 'a>, "ri"); + +#[derive(Debug, PartialEq, Clone)] +pub struct FlatnessTolerance(pub Number); +op1!(FlatnessTolerance, "i"); + +#[derive(Debug, PartialEq, Clone)] +pub struct SetGraphicsState<'b, 'a>(pub &'b Name<'a>); +op1!(SetGraphicsState<'b, 'a>, "gs"); + +#[derive(Debug, PartialEq, Clone)] +pub struct MoveTo( + pub Number, + pub Number, +); +op2!(MoveTo, "m"); + +#[derive(Debug, PartialEq, Clone)] +pub struct LineTo( + pub Number, + pub Number, +); +op2!(LineTo, "l"); + +#[derive(Debug, PartialEq, Clone)] +pub struct CubicTo( + pub Number, + pub Number, + pub Number, + pub Number, + pub Number, + pub Number, +); +op6!(CubicTo, "c"); + +#[derive(Debug, PartialEq, Clone)] +pub struct CubicStartTo( + pub Number, + pub Number, + pub Number, + pub Number, +); +op4!(CubicStartTo, "v"); + +#[derive(Debug, PartialEq, Clone)] +pub struct CubicEndTo( + pub Number, + pub Number, + pub Number, + pub Number, +); +op4!(CubicEndTo, "y"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ClosePath; +op0!(ClosePath, "h"); + +#[derive(Debug, PartialEq, Clone)] +pub struct RectPath( + pub Number, + pub Number, + pub Number, + pub Number, +); +op4!(RectPath, "re"); + +#[derive(Debug, PartialEq, Clone)] +pub struct StrokePath; +op0!(StrokePath, "S"); + +#[derive(Debug, PartialEq, Clone)] +pub struct CloseAndStrokePath; +op0!(CloseAndStrokePath, "s"); + +#[derive(Debug, PartialEq, Clone)] +pub struct FillPathNonZero; +op0!(FillPathNonZero, "f"); + +#[derive(Debug, PartialEq, Clone)] +pub struct FillPathNonZeroCompatibility; +op0!(FillPathNonZeroCompatibility, "F"); + +#[derive(Debug, PartialEq, Clone)] +pub struct FillPathEvenOdd; +op0!(FillPathEvenOdd, "f*"); + +#[derive(Debug, PartialEq, Clone)] +pub struct FillAndStrokeNonZero; +op0!(FillAndStrokeNonZero, "B"); + +#[derive(Debug, PartialEq, Clone)] +pub struct FillAndStrokeEvenOdd; +op0!(FillAndStrokeEvenOdd, "B*"); + +#[derive(Debug, PartialEq, Clone)] +pub struct CloseFillAndStrokeNonZero; +op0!(CloseFillAndStrokeNonZero, "b"); + +#[derive(Debug, PartialEq, Clone)] +pub struct CloseFillAndStrokeEvenOdd; +op0!(CloseFillAndStrokeEvenOdd, "b*"); + +#[derive(Debug, PartialEq, Clone)] +pub struct EndPath; +op0!(EndPath, "n"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ClipNonZero; +op0!(ClipNonZero, "W"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ClipEvenOdd; +op0!(ClipEvenOdd, "W*"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ColorSpaceStroke<'b, 'a>(pub &'b Name<'a>); +op1!(ColorSpaceStroke<'b, 'a>, "CS"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ColorSpaceNonStroke<'b, 'a>(pub &'b Name<'a>); +op1!(ColorSpaceNonStroke<'b, 'a>, "cs"); + +#[derive(Debug, PartialEq, Clone)] +pub struct StrokeColor(pub SmallVec<[Number; OPERANDS_THRESHOLD]>); +op_all!(StrokeColor, "SC"); + +#[derive(Debug, PartialEq, Clone)] +pub struct NonStrokeColor(pub SmallVec<[Number; OPERANDS_THRESHOLD]>); +op_all!(NonStrokeColor, "sc"); + +#[derive(Debug, PartialEq, Clone)] +pub struct StrokeColorDeviceGray(pub Number); +op1!(StrokeColorDeviceGray, "G"); + +#[derive(Debug, PartialEq, Clone)] +pub struct NonStrokeColorDeviceGray(pub Number); +op1!(NonStrokeColorDeviceGray, "g"); + +#[derive(Debug, PartialEq, Clone)] +pub struct StrokeColorDeviceRgb( + pub Number, + pub Number, + pub Number, +); +op3!(StrokeColorDeviceRgb, "RG"); + +#[derive(Debug, PartialEq, Clone)] +pub struct NonStrokeColorDeviceRgb( + pub Number, + pub Number, + pub Number, +); +op3!(NonStrokeColorDeviceRgb, "rg"); + +#[derive(Debug, PartialEq, Clone)] +pub struct StrokeColorCmyk( + pub Number, + pub Number, + pub Number, + pub Number, +); +op4!(StrokeColorCmyk, "K"); + +#[derive(Debug, PartialEq, Clone)] +pub struct NonStrokeColorCmyk( + pub Number, + pub Number, + pub Number, + pub Number, +); +op4!(NonStrokeColorCmyk, "k"); + +#[derive(Debug, PartialEq, Clone)] +pub struct Shading<'b, 'a>(pub &'b Name<'a>); +op1!(Shading<'b, 'a>, "sh"); + +#[derive(Debug, PartialEq, Clone)] +pub struct XObject<'b, 'a>(pub &'b Name<'a>); +op1!(XObject<'b, 'a>, "Do"); + +#[derive(Debug, PartialEq, Clone)] +pub struct InlineImage<'b, 'a>(pub &'b Stream<'a>); +op1!(InlineImage<'b, 'a>, "BI"); + +#[derive(Debug, PartialEq, Clone)] +pub struct CharacterSpacing(pub Number); +op1!(CharacterSpacing, "Tc"); + +#[derive(Debug, PartialEq, Clone)] +pub struct WordSpacing(pub Number); +op1!(WordSpacing, "Tw"); + +#[derive(Debug, PartialEq, Clone)] +pub struct HorizontalScaling(pub Number); +op1!(HorizontalScaling, "Tz"); + +#[derive(Debug, PartialEq, Clone)] +pub struct TextLeading(pub Number); +op1!(TextLeading, "TL"); + +#[derive(Debug, PartialEq, Clone)] +pub struct TextFont<'b, 'a>( + pub &'b Name<'a>, + pub Number, +); +op2!(TextFont<'b, 'a>, "Tf"); + +#[derive(Debug, PartialEq, Clone)] +pub struct TextRenderingMode(pub Number); +op1!(TextRenderingMode, "Tr"); + +#[derive(Debug, PartialEq, Clone)] +pub struct TextRise(pub Number); +op1!(TextRise, "Ts"); + +#[derive(Debug, PartialEq, Clone)] +pub struct BeginText; +op0!(BeginText, "BT"); + +#[derive(Debug, PartialEq, Clone)] +pub struct EndText; +op0!(EndText, "ET"); + +#[derive(Debug, PartialEq, Clone)] +pub struct NextLine( + pub Number, + pub Number, +); +op2!(NextLine, "Td"); + +#[derive(Debug, PartialEq, Clone)] +pub struct NextLineAndSetLeading( + pub Number, + pub Number, +); +op2!(NextLineAndSetLeading, "TD"); + +#[derive(Debug, PartialEq, Clone)] +pub struct SetTextMatrix( + pub Number, + pub Number, + pub Number, + pub Number, + pub Number, + pub Number, +); +op6!(SetTextMatrix, "Tm"); + +#[derive(Debug, PartialEq, Clone)] +pub struct NextLineUsingLeading; +op0!(NextLineUsingLeading, "T*"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ShowText<'b, 'a>(pub &'b object::String<'a>); +op1!(ShowText<'b, 'a>, "Tj"); + +#[derive(Debug, PartialEq, Clone)] +pub struct NextLineAndShowText<'b, 'a>(pub &'b object::String<'a>); +op1!(NextLineAndShowText<'b, 'a>, "'"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ShowTextWithParameters<'b, 'a>( + pub Number, + pub Number, + pub &'b object::String<'a>, +); +op3!(ShowTextWithParameters<'b, 'a>, "\""); + +#[derive(Debug, PartialEq, Clone)] +pub struct ShowTexts<'b, 'a>(pub &'b Array<'a>); +op1!(ShowTexts<'b, 'a>, "TJ"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ColorGlyph( + pub Number, + pub Number, +); +op2!(ColorGlyph, "d0"); + +#[derive(Debug, PartialEq, Clone)] +pub struct ShapeGlyph( + pub Number, + pub Number, + pub Number, + pub Number, + pub Number, + pub Number, +); +op6!(ShapeGlyph, "d1"); + +#[derive(Debug, PartialEq, Clone)] +pub struct MarkedContentPoint<'b, 'a>(pub &'b Name<'a>); +op1!(MarkedContentPoint<'b, 'a>, "MP"); + +#[derive(Debug, PartialEq, Clone)] +pub struct MarkedContentPointWithProperties<'b, 'a>( + pub &'b Name<'a>, + pub &'b Object<'a>, +); +op2!(MarkedContentPointWithProperties<'b, 'a>, "DP"); + +#[derive(Debug, PartialEq, Clone)] +pub struct BeginMarkedContent<'b, 'a>(pub &'b Name<'a>); +op1!(BeginMarkedContent<'b, 'a>, "BMC"); + +#[derive(Debug, PartialEq, Clone)] +pub struct BeginMarkedContentWithProperties<'b, 'a>( + pub &'b Name<'a>, + pub &'b Object<'a>, +); +op2!(BeginMarkedContentWithProperties<'b, 'a>, "BDC"); + +#[derive(Debug, PartialEq, Clone)] +pub struct EndMarkedContent; +op0!(EndMarkedContent, "EMC"); + +#[derive(Debug, PartialEq, Clone)] +pub enum TypedInstruction<'b, 'a> { + BeginCompatibility(BeginCompatibility), + EndCompatibility(EndCompatibility), + SaveState(SaveState), + RestoreState(RestoreState), + Transform(Transform), + LineWidth(LineWidth), + LineCap(LineCap), + LineJoin(LineJoin), + MiterLimit(MiterLimit), + DashPattern(DashPattern<'b, 'a>), + RenderingIntent(RenderingIntent<'b, 'a>), + FlatnessTolerance(FlatnessTolerance), + SetGraphicsState(SetGraphicsState<'b, 'a>), + MoveTo(MoveTo), + LineTo(LineTo), + CubicTo(CubicTo), + CubicStartTo(CubicStartTo), + CubicEndTo(CubicEndTo), + ClosePath(ClosePath), + RectPath(RectPath), + StrokePath(StrokePath), + CloseAndStrokePath(CloseAndStrokePath), + FillPathNonZero(FillPathNonZero), + FillPathNonZeroCompatibility(FillPathNonZeroCompatibility), + FillPathEvenOdd(FillPathEvenOdd), + FillAndStrokeNonZero(FillAndStrokeNonZero), + FillAndStrokeEvenOdd(FillAndStrokeEvenOdd), + CloseFillAndStrokeNonZero(CloseFillAndStrokeNonZero), + CloseFillAndStrokeEvenOdd(CloseFillAndStrokeEvenOdd), + EndPath(EndPath), + ClipNonZero(ClipNonZero), + ClipEvenOdd(ClipEvenOdd), + ColorSpaceStroke(ColorSpaceStroke<'b, 'a>), + ColorSpaceNonStroke(ColorSpaceNonStroke<'b, 'a>), + StrokeColor(StrokeColor), + StrokeColorNamed(StrokeColorNamed<'b, 'a>), + NonStrokeColor(NonStrokeColor), + NonStrokeColorNamed(NonStrokeColorNamed<'b, 'a>), + StrokeColorDeviceGray(StrokeColorDeviceGray), + NonStrokeColorDeviceGray(NonStrokeColorDeviceGray), + StrokeColorDeviceRgb(StrokeColorDeviceRgb), + NonStrokeColorDeviceRgb(NonStrokeColorDeviceRgb), + StrokeColorCmyk(StrokeColorCmyk), + NonStrokeColorCmyk(NonStrokeColorCmyk), + Shading(Shading<'b, 'a>), + XObject(XObject<'b, 'a>), + InlineImage(InlineImage<'b, 'a>), + CharacterSpacing(CharacterSpacing), + WordSpacing(WordSpacing), + HorizontalScaling(HorizontalScaling), + TextLeading(TextLeading), + TextFont(TextFont<'b, 'a>), + TextRenderingMode(TextRenderingMode), + TextRise(TextRise), + BeginText(BeginText), + EndText(EndText), + NextLine(NextLine), + NextLineAndSetLeading(NextLineAndSetLeading), + SetTextMatrix(SetTextMatrix), + NextLineUsingLeading(NextLineUsingLeading), + ShowText(ShowText<'b, 'a>), + NextLineAndShowText(NextLineAndShowText<'b, 'a>), + ShowTextWithParameters(ShowTextWithParameters<'b, 'a>), + ShowTexts(ShowTexts<'b, 'a>), + ColorGlyph(ColorGlyph), + ShapeGlyph(ShapeGlyph), + MarkedContentPoint(MarkedContentPoint<'b, 'a>), + MarkedContentPointWithProperties(MarkedContentPointWithProperties<'b, 'a>), + BeginMarkedContent(BeginMarkedContent<'b, 'a>), + BeginMarkedContentWithProperties(BeginMarkedContentWithProperties<'b, 'a>), + EndMarkedContent(EndMarkedContent), + Fallback(&'b Operator<'a>), +} + +impl<'b, 'a> TypedInstruction<'b, 'a> { + #[inline(always)] + pub(crate) fn dispatch(instruction: &Instruction<'b, 'a>) -> Option { + let op_name = instruction.operator.as_ref(); + Some(match op_name { + b"BX" => BeginCompatibility::from_stack(instruction.operands)?.into(), + b"EX" => EndCompatibility::from_stack(instruction.operands)?.into(), + b"q" => SaveState::from_stack(instruction.operands)?.into(), + b"Q" => RestoreState::from_stack(instruction.operands)?.into(), + b"cm" => Transform::from_stack(instruction.operands)?.into(), + b"w" => LineWidth::from_stack(instruction.operands)?.into(), + b"J" => LineCap::from_stack(instruction.operands)?.into(), + b"j" => LineJoin::from_stack(instruction.operands)?.into(), + b"M" => MiterLimit::from_stack(instruction.operands)?.into(), + b"d" => DashPattern::from_stack(instruction.operands)?.into(), + b"ri" => RenderingIntent::from_stack(instruction.operands)?.into(), + b"i" => FlatnessTolerance::from_stack(instruction.operands)?.into(), + b"gs" => SetGraphicsState::from_stack(instruction.operands)?.into(), + b"m" => MoveTo::from_stack(instruction.operands)?.into(), + b"l" => LineTo::from_stack(instruction.operands)?.into(), + b"c" => CubicTo::from_stack(instruction.operands)?.into(), + b"v" => CubicStartTo::from_stack(instruction.operands)?.into(), + b"y" => CubicEndTo::from_stack(instruction.operands)?.into(), + b"h" => ClosePath::from_stack(instruction.operands)?.into(), + b"re" => RectPath::from_stack(instruction.operands)?.into(), + b"S" => StrokePath::from_stack(instruction.operands)?.into(), + b"s" => CloseAndStrokePath::from_stack(instruction.operands)?.into(), + b"f" => FillPathNonZero::from_stack(instruction.operands)?.into(), + b"F" => FillPathNonZeroCompatibility::from_stack(instruction.operands)?.into(), + b"f*" => FillPathEvenOdd::from_stack(instruction.operands)?.into(), + b"B" => FillAndStrokeNonZero::from_stack(instruction.operands)?.into(), + b"B*" => FillAndStrokeEvenOdd::from_stack(instruction.operands)?.into(), + b"b" => CloseFillAndStrokeNonZero::from_stack(instruction.operands)?.into(), + b"b*" => CloseFillAndStrokeEvenOdd::from_stack(instruction.operands)?.into(), + b"n" => EndPath::from_stack(instruction.operands)?.into(), + b"W" => ClipNonZero::from_stack(instruction.operands)?.into(), + b"W*" => ClipEvenOdd::from_stack(instruction.operands)?.into(), + b"CS" => ColorSpaceStroke::from_stack(instruction.operands)?.into(), + b"cs" => ColorSpaceNonStroke::from_stack(instruction.operands)?.into(), + b"SC" => StrokeColor::from_stack(instruction.operands)?.into(), + b"SCN" => StrokeColorNamed::from_stack(instruction.operands)?.into(), + b"sc" => NonStrokeColor::from_stack(instruction.operands)?.into(), + b"scn" => NonStrokeColorNamed::from_stack(instruction.operands)?.into(), + b"G" => StrokeColorDeviceGray::from_stack(instruction.operands)?.into(), + b"g" => NonStrokeColorDeviceGray::from_stack(instruction.operands)?.into(), + b"RG" => StrokeColorDeviceRgb::from_stack(instruction.operands)?.into(), + b"rg" => NonStrokeColorDeviceRgb::from_stack(instruction.operands)?.into(), + b"K" => StrokeColorCmyk::from_stack(instruction.operands)?.into(), + b"k" => NonStrokeColorCmyk::from_stack(instruction.operands)?.into(), + b"sh" => Shading::from_stack(instruction.operands)?.into(), + b"Do" => XObject::from_stack(instruction.operands)?.into(), + b"BI" => InlineImage::from_stack(instruction.operands)?.into(), + b"Tc" => CharacterSpacing::from_stack(instruction.operands)?.into(), + b"Tw" => WordSpacing::from_stack(instruction.operands)?.into(), + b"Tz" => HorizontalScaling::from_stack(instruction.operands)?.into(), + b"TL" => TextLeading::from_stack(instruction.operands)?.into(), + b"Tf" => TextFont::from_stack(instruction.operands)?.into(), + b"Tr" => TextRenderingMode::from_stack(instruction.operands)?.into(), + b"Ts" => TextRise::from_stack(instruction.operands)?.into(), + b"BT" => BeginText::from_stack(instruction.operands)?.into(), + b"ET" => EndText::from_stack(instruction.operands)?.into(), + b"Td" => NextLine::from_stack(instruction.operands)?.into(), + b"TD" => NextLineAndSetLeading::from_stack(instruction.operands)?.into(), + b"Tm" => SetTextMatrix::from_stack(instruction.operands)?.into(), + b"T*" => NextLineUsingLeading::from_stack(instruction.operands)?.into(), + b"Tj" => ShowText::from_stack(instruction.operands)?.into(), + b"'" => NextLineAndShowText::from_stack(instruction.operands)?.into(), + b"\"" => ShowTextWithParameters::from_stack(instruction.operands)?.into(), + b"TJ" => ShowTexts::from_stack(instruction.operands)?.into(), + b"d0" => ColorGlyph::from_stack(instruction.operands)?.into(), + b"d1" => ShapeGlyph::from_stack(instruction.operands)?.into(), + b"MP" => MarkedContentPoint::from_stack(instruction.operands)?.into(), + b"DP" => MarkedContentPointWithProperties::from_stack(instruction.operands)?.into(), + b"BMC" => BeginMarkedContent::from_stack(instruction.operands)?.into(), + b"BDC" => BeginMarkedContentWithProperties::from_stack(instruction.operands)?.into(), + b"EMC" => EndMarkedContent::from_stack(instruction.operands)?.into(), + _ => return Some(Self::Fallback(instruction.operator)), + }) + } +} \ No newline at end of file diff --git a/vendor/hayro-syntax/src/crypto/aes.rs b/vendor/hayro-syntax/src/crypto/aes.rs new file mode 100644 index 00000000..9d4abad3 --- /dev/null +++ b/vendor/hayro-syntax/src/crypto/aes.rs @@ -0,0 +1,819 @@ +//! Ported from . + +// TODO: Explore performance improvements. + +use alloc::vec; +use alloc::vec::Vec; + +const S_BOX: [u8; 256] = [ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, +]; + +const INV_S_BOX: [u8; 256] = [ + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d, +]; + +const RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + +const MUL2: [u8; 256] = [ + 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1a, 0x1c, 0x1e, + 0x20, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2c, 0x2e, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3c, 0x3e, + 0x40, 0x42, 0x44, 0x46, 0x48, 0x4a, 0x4c, 0x4e, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5a, 0x5c, 0x5e, + 0x60, 0x62, 0x64, 0x66, 0x68, 0x6a, 0x6c, 0x6e, 0x70, 0x72, 0x74, 0x76, 0x78, 0x7a, 0x7c, 0x7e, + 0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c, 0x8e, 0x90, 0x92, 0x94, 0x96, 0x98, 0x9a, 0x9c, 0x9e, + 0xa0, 0xa2, 0xa4, 0xa6, 0xa8, 0xaa, 0xac, 0xae, 0xb0, 0xb2, 0xb4, 0xb6, 0xb8, 0xba, 0xbc, 0xbe, + 0xc0, 0xc2, 0xc4, 0xc6, 0xc8, 0xca, 0xcc, 0xce, 0xd0, 0xd2, 0xd4, 0xd6, 0xd8, 0xda, 0xdc, 0xde, + 0xe0, 0xe2, 0xe4, 0xe6, 0xe8, 0xea, 0xec, 0xee, 0xf0, 0xf2, 0xf4, 0xf6, 0xf8, 0xfa, 0xfc, 0xfe, + 0x1b, 0x19, 0x1f, 0x1d, 0x13, 0x11, 0x17, 0x15, 0x0b, 0x09, 0x0f, 0x0d, 0x03, 0x01, 0x07, 0x05, + 0x3b, 0x39, 0x3f, 0x3d, 0x33, 0x31, 0x37, 0x35, 0x2b, 0x29, 0x2f, 0x2d, 0x23, 0x21, 0x27, 0x25, + 0x5b, 0x59, 0x5f, 0x5d, 0x53, 0x51, 0x57, 0x55, 0x4b, 0x49, 0x4f, 0x4d, 0x43, 0x41, 0x47, 0x45, + 0x7b, 0x79, 0x7f, 0x7d, 0x73, 0x71, 0x77, 0x75, 0x6b, 0x69, 0x6f, 0x6d, 0x63, 0x61, 0x67, 0x65, + 0x9b, 0x99, 0x9f, 0x9d, 0x93, 0x91, 0x97, 0x95, 0x8b, 0x89, 0x8f, 0x8d, 0x83, 0x81, 0x87, 0x85, + 0xbb, 0xb9, 0xbf, 0xbd, 0xb3, 0xb1, 0xb7, 0xb5, 0xab, 0xa9, 0xaf, 0xad, 0xa3, 0xa1, 0xa7, 0xa5, + 0xdb, 0xd9, 0xdf, 0xdd, 0xd3, 0xd1, 0xd7, 0xd5, 0xcb, 0xc9, 0xcf, 0xcd, 0xc3, 0xc1, 0xc7, 0xc5, + 0xfb, 0xf9, 0xff, 0xfd, 0xf3, 0xf1, 0xf7, 0xf5, 0xeb, 0xe9, 0xef, 0xed, 0xe3, 0xe1, 0xe7, 0xe5, +]; + +const MUL3: [u8; 256] = [ + 0x00, 0x03, 0x06, 0x05, 0x0c, 0x0f, 0x0a, 0x09, 0x18, 0x1b, 0x1e, 0x1d, 0x14, 0x17, 0x12, 0x11, + 0x30, 0x33, 0x36, 0x35, 0x3c, 0x3f, 0x3a, 0x39, 0x28, 0x2b, 0x2e, 0x2d, 0x24, 0x27, 0x22, 0x21, + 0x60, 0x63, 0x66, 0x65, 0x6c, 0x6f, 0x6a, 0x69, 0x78, 0x7b, 0x7e, 0x7d, 0x74, 0x77, 0x72, 0x71, + 0x50, 0x53, 0x56, 0x55, 0x5c, 0x5f, 0x5a, 0x59, 0x48, 0x4b, 0x4e, 0x4d, 0x44, 0x47, 0x42, 0x41, + 0xc0, 0xc3, 0xc6, 0xc5, 0xcc, 0xcf, 0xca, 0xc9, 0xd8, 0xdb, 0xde, 0xdd, 0xd4, 0xd7, 0xd2, 0xd1, + 0xf0, 0xf3, 0xf6, 0xf5, 0xfc, 0xff, 0xfa, 0xf9, 0xe8, 0xeb, 0xee, 0xed, 0xe4, 0xe7, 0xe2, 0xe1, + 0xa0, 0xa3, 0xa6, 0xa5, 0xac, 0xaf, 0xaa, 0xa9, 0xb8, 0xbb, 0xbe, 0xbd, 0xb4, 0xb7, 0xb2, 0xb1, + 0x90, 0x93, 0x96, 0x95, 0x9c, 0x9f, 0x9a, 0x99, 0x88, 0x8b, 0x8e, 0x8d, 0x84, 0x87, 0x82, 0x81, + 0x9b, 0x98, 0x9d, 0x9e, 0x97, 0x94, 0x91, 0x92, 0x83, 0x80, 0x85, 0x86, 0x8f, 0x8c, 0x89, 0x8a, + 0xab, 0xa8, 0xad, 0xae, 0xa7, 0xa4, 0xa1, 0xa2, 0xb3, 0xb0, 0xb5, 0xb6, 0xbf, 0xbc, 0xb9, 0xba, + 0xfb, 0xf8, 0xfd, 0xfe, 0xf7, 0xf4, 0xf1, 0xf2, 0xe3, 0xe0, 0xe5, 0xe6, 0xef, 0xec, 0xe9, 0xea, + 0xcb, 0xc8, 0xcd, 0xce, 0xc7, 0xc4, 0xc1, 0xc2, 0xd3, 0xd0, 0xd5, 0xd6, 0xdf, 0xdc, 0xd9, 0xda, + 0x5b, 0x58, 0x5d, 0x5e, 0x57, 0x54, 0x51, 0x52, 0x43, 0x40, 0x45, 0x46, 0x4f, 0x4c, 0x49, 0x4a, + 0x6b, 0x68, 0x6d, 0x6e, 0x67, 0x64, 0x61, 0x62, 0x73, 0x70, 0x75, 0x76, 0x7f, 0x7c, 0x79, 0x7a, + 0x3b, 0x38, 0x3d, 0x3e, 0x37, 0x34, 0x31, 0x32, 0x23, 0x20, 0x25, 0x26, 0x2f, 0x2c, 0x29, 0x2a, + 0x0b, 0x08, 0x0d, 0x0e, 0x07, 0x04, 0x01, 0x02, 0x13, 0x10, 0x15, 0x16, 0x1f, 0x1c, 0x19, 0x1a, +]; + +const MUL9: [u8; 256] = [ + 0x00, 0x09, 0x12, 0x1b, 0x24, 0x2d, 0x36, 0x3f, 0x48, 0x41, 0x5a, 0x53, 0x6c, 0x65, 0x7e, 0x77, + 0x90, 0x99, 0x82, 0x8b, 0xb4, 0xbd, 0xa6, 0xaf, 0xd8, 0xd1, 0xca, 0xc3, 0xfc, 0xf5, 0xee, 0xe7, + 0x3b, 0x32, 0x29, 0x20, 0x1f, 0x16, 0x0d, 0x04, 0x73, 0x7a, 0x61, 0x68, 0x57, 0x5e, 0x45, 0x4c, + 0xab, 0xa2, 0xb9, 0xb0, 0x8f, 0x86, 0x9d, 0x94, 0xe3, 0xea, 0xf1, 0xf8, 0xc7, 0xce, 0xd5, 0xdc, + 0x76, 0x7f, 0x64, 0x6d, 0x52, 0x5b, 0x40, 0x49, 0x3e, 0x37, 0x2c, 0x25, 0x1a, 0x13, 0x08, 0x01, + 0xe6, 0xef, 0xf4, 0xfd, 0xc2, 0xcb, 0xd0, 0xd9, 0xae, 0xa7, 0xbc, 0xb5, 0x8a, 0x83, 0x98, 0x91, + 0x4d, 0x44, 0x5f, 0x56, 0x69, 0x60, 0x7b, 0x72, 0x05, 0x0c, 0x17, 0x1e, 0x21, 0x28, 0x33, 0x3a, + 0xdd, 0xd4, 0xcf, 0xc6, 0xf9, 0xf0, 0xeb, 0xe2, 0x95, 0x9c, 0x87, 0x8e, 0xb1, 0xb8, 0xa3, 0xaa, + 0xec, 0xe5, 0xfe, 0xf7, 0xc8, 0xc1, 0xda, 0xd3, 0xa4, 0xad, 0xb6, 0xbf, 0x80, 0x89, 0x92, 0x9b, + 0x7c, 0x75, 0x6e, 0x67, 0x58, 0x51, 0x4a, 0x43, 0x34, 0x3d, 0x26, 0x2f, 0x10, 0x19, 0x02, 0x0b, + 0xd7, 0xde, 0xc5, 0xcc, 0xf3, 0xfa, 0xe1, 0xe8, 0x9f, 0x96, 0x8d, 0x84, 0xbb, 0xb2, 0xa9, 0xa0, + 0x47, 0x4e, 0x55, 0x5c, 0x63, 0x6a, 0x71, 0x78, 0x0f, 0x06, 0x1d, 0x14, 0x2b, 0x22, 0x39, 0x30, + 0x9a, 0x93, 0x88, 0x81, 0xbe, 0xb7, 0xac, 0xa5, 0xd2, 0xdb, 0xc0, 0xc9, 0xf6, 0xff, 0xe4, 0xed, + 0x0a, 0x03, 0x18, 0x11, 0x2e, 0x27, 0x3c, 0x35, 0x42, 0x4b, 0x50, 0x59, 0x66, 0x6f, 0x74, 0x7d, + 0xa1, 0xa8, 0xb3, 0xba, 0x85, 0x8c, 0x97, 0x9e, 0xe9, 0xe0, 0xfb, 0xf2, 0xcd, 0xc4, 0xdf, 0xd6, + 0x31, 0x38, 0x23, 0x2a, 0x15, 0x1c, 0x07, 0x0e, 0x79, 0x70, 0x6b, 0x62, 0x5d, 0x54, 0x4f, 0x46, +]; + +const MUL11: [u8; 256] = [ + 0x00, 0x0b, 0x16, 0x1d, 0x2c, 0x27, 0x3a, 0x31, 0x58, 0x53, 0x4e, 0x45, 0x74, 0x7f, 0x62, 0x69, + 0xb0, 0xbb, 0xa6, 0xad, 0x9c, 0x97, 0x8a, 0x81, 0xe8, 0xe3, 0xfe, 0xf5, 0xc4, 0xcf, 0xd2, 0xd9, + 0x7b, 0x70, 0x6d, 0x66, 0x57, 0x5c, 0x41, 0x4a, 0x23, 0x28, 0x35, 0x3e, 0x0f, 0x04, 0x19, 0x12, + 0xcb, 0xc0, 0xdd, 0xd6, 0xe7, 0xec, 0xf1, 0xfa, 0x93, 0x98, 0x85, 0x8e, 0xbf, 0xb4, 0xa9, 0xa2, + 0xf6, 0xfd, 0xe0, 0xeb, 0xda, 0xd1, 0xcc, 0xc7, 0xae, 0xa5, 0xb8, 0xb3, 0x82, 0x89, 0x94, 0x9f, + 0x46, 0x4d, 0x50, 0x5b, 0x6a, 0x61, 0x7c, 0x77, 0x1e, 0x15, 0x08, 0x03, 0x32, 0x39, 0x24, 0x2f, + 0x8d, 0x86, 0x9b, 0x90, 0xa1, 0xaa, 0xb7, 0xbc, 0xd5, 0xde, 0xc3, 0xc8, 0xf9, 0xf2, 0xef, 0xe4, + 0x3d, 0x36, 0x2b, 0x20, 0x11, 0x1a, 0x07, 0x0c, 0x65, 0x6e, 0x73, 0x78, 0x49, 0x42, 0x5f, 0x54, + 0xf7, 0xfc, 0xe1, 0xea, 0xdb, 0xd0, 0xcd, 0xc6, 0xaf, 0xa4, 0xb9, 0xb2, 0x83, 0x88, 0x95, 0x9e, + 0x47, 0x4c, 0x51, 0x5a, 0x6b, 0x60, 0x7d, 0x76, 0x1f, 0x14, 0x09, 0x02, 0x33, 0x38, 0x25, 0x2e, + 0x8c, 0x87, 0x9a, 0x91, 0xa0, 0xab, 0xb6, 0xbd, 0xd4, 0xdf, 0xc2, 0xc9, 0xf8, 0xf3, 0xee, 0xe5, + 0x3c, 0x37, 0x2a, 0x21, 0x10, 0x1b, 0x06, 0x0d, 0x64, 0x6f, 0x72, 0x79, 0x48, 0x43, 0x5e, 0x55, + 0x01, 0x0a, 0x17, 0x1c, 0x2d, 0x26, 0x3b, 0x30, 0x59, 0x52, 0x4f, 0x44, 0x75, 0x7e, 0x63, 0x68, + 0xb1, 0xba, 0xa7, 0xac, 0x9d, 0x96, 0x8b, 0x80, 0xe9, 0xe2, 0xff, 0xf4, 0xc5, 0xce, 0xd3, 0xd8, + 0x7a, 0x71, 0x6c, 0x67, 0x56, 0x5d, 0x40, 0x4b, 0x22, 0x29, 0x34, 0x3f, 0x0e, 0x05, 0x18, 0x13, + 0xca, 0xc1, 0xdc, 0xd7, 0xe6, 0xed, 0xf0, 0xfb, 0x92, 0x99, 0x84, 0x8f, 0xbe, 0xb5, 0xa8, 0xa3, +]; + +const MUL13: [u8; 256] = [ + 0x00, 0x0d, 0x1a, 0x17, 0x34, 0x39, 0x2e, 0x23, 0x68, 0x65, 0x72, 0x7f, 0x5c, 0x51, 0x46, 0x4b, + 0xd0, 0xdd, 0xca, 0xc7, 0xe4, 0xe9, 0xfe, 0xf3, 0xb8, 0xb5, 0xa2, 0xaf, 0x8c, 0x81, 0x96, 0x9b, + 0xbb, 0xb6, 0xa1, 0xac, 0x8f, 0x82, 0x95, 0x98, 0xd3, 0xde, 0xc9, 0xc4, 0xe7, 0xea, 0xfd, 0xf0, + 0x6b, 0x66, 0x71, 0x7c, 0x5f, 0x52, 0x45, 0x48, 0x03, 0x0e, 0x19, 0x14, 0x37, 0x3a, 0x2d, 0x20, + 0x6d, 0x60, 0x77, 0x7a, 0x59, 0x54, 0x43, 0x4e, 0x05, 0x08, 0x1f, 0x12, 0x31, 0x3c, 0x2b, 0x26, + 0xbd, 0xb0, 0xa7, 0xaa, 0x89, 0x84, 0x93, 0x9e, 0xd5, 0xd8, 0xcf, 0xc2, 0xe1, 0xec, 0xfb, 0xf6, + 0xd6, 0xdb, 0xcc, 0xc1, 0xe2, 0xef, 0xf8, 0xf5, 0xbe, 0xb3, 0xa4, 0xa9, 0x8a, 0x87, 0x90, 0x9d, + 0x06, 0x0b, 0x1c, 0x11, 0x32, 0x3f, 0x28, 0x25, 0x6e, 0x63, 0x74, 0x79, 0x5a, 0x57, 0x40, 0x4d, + 0xda, 0xd7, 0xc0, 0xcd, 0xee, 0xe3, 0xf4, 0xf9, 0xb2, 0xbf, 0xa8, 0xa5, 0x86, 0x8b, 0x9c, 0x91, + 0x0a, 0x07, 0x10, 0x1d, 0x3e, 0x33, 0x24, 0x29, 0x62, 0x6f, 0x78, 0x75, 0x56, 0x5b, 0x4c, 0x41, + 0x61, 0x6c, 0x7b, 0x76, 0x55, 0x58, 0x4f, 0x42, 0x09, 0x04, 0x13, 0x1e, 0x3d, 0x30, 0x27, 0x2a, + 0xb1, 0xbc, 0xab, 0xa6, 0x85, 0x88, 0x9f, 0x92, 0xd9, 0xd4, 0xc3, 0xce, 0xed, 0xe0, 0xf7, 0xfa, + 0xb7, 0xba, 0xad, 0xa0, 0x83, 0x8e, 0x99, 0x94, 0xdf, 0xd2, 0xc5, 0xc8, 0xeb, 0xe6, 0xf1, 0xfc, + 0x67, 0x6a, 0x7d, 0x70, 0x53, 0x5e, 0x49, 0x44, 0x0f, 0x02, 0x15, 0x18, 0x3b, 0x36, 0x21, 0x2c, + 0x0c, 0x01, 0x16, 0x1b, 0x38, 0x35, 0x22, 0x2f, 0x64, 0x69, 0x7e, 0x73, 0x50, 0x5d, 0x4a, 0x47, + 0xdc, 0xd1, 0xc6, 0xcb, 0xe8, 0xe5, 0xf2, 0xff, 0xb4, 0xb9, 0xae, 0xa3, 0x80, 0x8d, 0x9a, 0x97, +]; + +const MUL14: [u8; 256] = [ + 0x00, 0x0e, 0x1c, 0x12, 0x38, 0x36, 0x24, 0x2a, 0x70, 0x7e, 0x6c, 0x62, 0x48, 0x46, 0x54, 0x5a, + 0xe0, 0xee, 0xfc, 0xf2, 0xd8, 0xd6, 0xc4, 0xca, 0x90, 0x9e, 0x8c, 0x82, 0xa8, 0xa6, 0xb4, 0xba, + 0xdb, 0xd5, 0xc7, 0xc9, 0xe3, 0xed, 0xff, 0xf1, 0xab, 0xa5, 0xb7, 0xb9, 0x93, 0x9d, 0x8f, 0x81, + 0x3b, 0x35, 0x27, 0x29, 0x03, 0x0d, 0x1f, 0x11, 0x4b, 0x45, 0x57, 0x59, 0x73, 0x7d, 0x6f, 0x61, + 0xad, 0xa3, 0xb1, 0xbf, 0x95, 0x9b, 0x89, 0x87, 0xdd, 0xd3, 0xc1, 0xcf, 0xe5, 0xeb, 0xf9, 0xf7, + 0x4d, 0x43, 0x51, 0x5f, 0x75, 0x7b, 0x69, 0x67, 0x3d, 0x33, 0x21, 0x2f, 0x05, 0x0b, 0x19, 0x17, + 0x76, 0x78, 0x6a, 0x64, 0x4e, 0x40, 0x52, 0x5c, 0x06, 0x08, 0x1a, 0x14, 0x3e, 0x30, 0x22, 0x2c, + 0x96, 0x98, 0x8a, 0x84, 0xae, 0xa0, 0xb2, 0xbc, 0xe6, 0xe8, 0xfa, 0xf4, 0xde, 0xd0, 0xc2, 0xcc, + 0x41, 0x4f, 0x5d, 0x53, 0x79, 0x77, 0x65, 0x6b, 0x31, 0x3f, 0x2d, 0x23, 0x09, 0x07, 0x15, 0x1b, + 0xa1, 0xaf, 0xbd, 0xb3, 0x99, 0x97, 0x85, 0x8b, 0xd1, 0xdf, 0xcd, 0xc3, 0xe9, 0xe7, 0xf5, 0xfb, + 0x9a, 0x94, 0x86, 0x88, 0xa2, 0xac, 0xbe, 0xb0, 0xea, 0xe4, 0xf6, 0xf8, 0xd2, 0xdc, 0xce, 0xc0, + 0x7a, 0x74, 0x66, 0x68, 0x42, 0x4c, 0x5e, 0x50, 0x0a, 0x04, 0x16, 0x18, 0x32, 0x3c, 0x2e, 0x20, + 0xec, 0xe2, 0xf0, 0xfe, 0xd4, 0xda, 0xc8, 0xc6, 0x9c, 0x92, 0x80, 0x8e, 0xa4, 0xaa, 0xb8, 0xb6, + 0x0c, 0x02, 0x10, 0x1e, 0x34, 0x3a, 0x28, 0x26, 0x7c, 0x72, 0x60, 0x6e, 0x44, 0x4a, 0x58, 0x56, + 0x37, 0x39, 0x2b, 0x25, 0x0f, 0x01, 0x13, 0x1d, 0x47, 0x49, 0x5b, 0x55, 0x7f, 0x71, 0x63, 0x6d, + 0xd7, 0xd9, 0xcb, 0xc5, 0xef, 0xe1, 0xf3, 0xfd, 0xa7, 0xa9, 0xbb, 0xb5, 0x9f, 0x91, 0x83, 0x8d, +]; + +struct AESCore; + +impl AESCore { + fn sub_bytes(state: &mut [u8; 16]) { + for byte in state.iter_mut() { + *byte = S_BOX[*byte as usize]; + } + } + + fn inv_sub_bytes(state: &mut [u8; 16]) { + for byte in state.iter_mut() { + *byte = INV_S_BOX[*byte as usize]; + } + } + + fn shift_rows(state: &mut [u8; 16]) { + let temp = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = temp; + + let temp1 = state[2]; + let temp2 = state[6]; + state[2] = state[10]; + state[6] = state[14]; + state[10] = temp1; + state[14] = temp2; + + let temp = state[15]; + state[15] = state[11]; + state[11] = state[7]; + state[7] = state[3]; + state[3] = temp; + } + + fn inv_shift_rows(state: &mut [u8; 16]) { + let temp = state[13]; + state[13] = state[9]; + state[9] = state[5]; + state[5] = state[1]; + state[1] = temp; + + let temp1 = state[2]; + let temp2 = state[6]; + state[2] = state[10]; + state[6] = state[14]; + state[10] = temp1; + state[14] = temp2; + + let temp = state[3]; + state[3] = state[7]; + state[7] = state[11]; + state[11] = state[15]; + state[15] = temp; + } + + #[inline] + fn mix_columns(state: &mut [u8; 16]) { + for i in 0..4 { + let col = i * 4; + let [s0, s1, s2, s3] = [state[col], state[col + 1], state[col + 2], state[col + 3]]; + + state[col] = MUL2[s0 as usize] ^ MUL3[s1 as usize] ^ s2 ^ s3; + state[col + 1] = s0 ^ MUL2[s1 as usize] ^ MUL3[s2 as usize] ^ s3; + state[col + 2] = s0 ^ s1 ^ MUL2[s2 as usize] ^ MUL3[s3 as usize]; + state[col + 3] = MUL3[s0 as usize] ^ s1 ^ s2 ^ MUL2[s3 as usize]; + } + } + + #[inline] + fn inv_mix_columns(state: &mut [u8; 16]) { + for i in 0..4 { + let col = i * 4; + let [s0, s1, s2, s3] = [state[col], state[col + 1], state[col + 2], state[col + 3]]; + + state[col] = + MUL14[s0 as usize] ^ MUL11[s1 as usize] ^ MUL13[s2 as usize] ^ MUL9[s3 as usize]; + state[col + 1] = + MUL9[s0 as usize] ^ MUL14[s1 as usize] ^ MUL11[s2 as usize] ^ MUL13[s3 as usize]; + state[col + 2] = + MUL13[s0 as usize] ^ MUL9[s1 as usize] ^ MUL14[s2 as usize] ^ MUL11[s3 as usize]; + state[col + 3] = + MUL11[s0 as usize] ^ MUL13[s1 as usize] ^ MUL9[s2 as usize] ^ MUL14[s3 as usize]; + } + } + + fn add_round_key(state: &mut [u8; 16], round_key: &[u8; 16]) { + for i in 0..16 { + state[i] ^= round_key[i]; + } + } +} + +#[derive(Clone)] +pub(crate) struct AESCipher { + round_keys: [[u8; 16]; ROUNDS], +} + +pub(crate) type AES128Cipher = AESCipher<16, 11>; +pub(crate) type AES256Cipher = AESCipher<32, 15>; + +impl AESCipher { + pub(crate) fn new(key: &[u8]) -> Option { + if key.len() != KEY_SIZE { + return None; + } + + let mut round_keys = [[0_u8; 16]; ROUNDS]; + + match KEY_SIZE { + 16 => Self::expand_key_128(&mut round_keys, key), + 32 => Self::expand_key_256(&mut round_keys, key), + _ => return None, + } + + Some(Self { round_keys }) + } + + fn expand_key_128(round_keys: &mut [[u8; 16]; ROUNDS], key: &[u8]) { + round_keys[0].copy_from_slice(&key[..16]); + + for i in 1..ROUNDS { + let mut temp = [0_u8; 4]; + temp.copy_from_slice(&round_keys[i - 1][12..16]); + + temp.rotate_left(1); + for b in &mut temp { + *b = S_BOX[*b as usize]; + } + + temp[0] ^= RCON[i - 1]; + + for j in 0..4 { + for k in 0..4 { + round_keys[i][j * 4 + k] = round_keys[i - 1][j * 4 + k] ^ temp[k]; + } + if j < 3 { + temp.copy_from_slice(&round_keys[i][j * 4..(j + 1) * 4]); + } + } + } + } + + fn expand_key_256(round_keys: &mut [[u8; 16]; ROUNDS], key: &[u8]) { + round_keys[0].copy_from_slice(&key[0..16]); + round_keys[1].copy_from_slice(&key[16..32]); + + for i in 2..ROUNDS { + let mut temp = [0_u8; 4]; + + if i % 2 == 0 { + temp.copy_from_slice(&round_keys[i - 1][12..16]); + temp.rotate_left(1); + for b in &mut temp { + *b = S_BOX[*b as usize]; + } + temp[0] ^= RCON[(i / 2) - 1]; + } else { + temp.copy_from_slice(&round_keys[i - 1][12..16]); + for b in &mut temp { + *b = S_BOX[*b as usize]; + } + } + + for j in 0..4 { + for k in 0..4 { + round_keys[i][j * 4 + k] = round_keys[i - 2][j * 4 + k] ^ temp[k]; + } + if j < 3 { + temp.copy_from_slice(&round_keys[i][j * 4..(j + 1) * 4]); + } + } + } + } + + pub(crate) fn encrypt_block(&self, input: &[u8; 16]) -> [u8; 16] { + let mut state = *input; + + AESCore::add_round_key(&mut state, &self.round_keys[0]); + + let main_rounds = ROUNDS - 1; + for round in 1..main_rounds { + AESCore::sub_bytes(&mut state); + AESCore::shift_rows(&mut state); + AESCore::mix_columns(&mut state); + AESCore::add_round_key(&mut state, &self.round_keys[round]); + } + + AESCore::sub_bytes(&mut state); + AESCore::shift_rows(&mut state); + AESCore::add_round_key(&mut state, &self.round_keys[main_rounds]); + + state + } + + pub(crate) fn decrypt_block(&self, input: &[u8; 16]) -> [u8; 16] { + let mut state = *input; + + let main_rounds = ROUNDS - 1; + + AESCore::add_round_key(&mut state, &self.round_keys[main_rounds]); + AESCore::inv_shift_rows(&mut state); + AESCore::inv_sub_bytes(&mut state); + + for round in (1..main_rounds).rev() { + AESCore::add_round_key(&mut state, &self.round_keys[round]); + AESCore::inv_mix_columns(&mut state); + AESCore::inv_shift_rows(&mut state); + AESCore::inv_sub_bytes(&mut state); + } + + AESCore::add_round_key(&mut state, &self.round_keys[0]); + + state + } + + pub(crate) fn encrypt_cbc(&self, data: &[u8], iv: &[u8; 16]) -> Vec { + let mut result = Vec::new(); + let mut current_iv = *iv; + + let mut padded_data = data.to_vec(); + let pad_len = 16 - (data.len() % 16); + if pad_len == 0 { + padded_data.extend(vec![16_u8; 16]); + } else { + padded_data.extend(vec![pad_len as u8; pad_len]); + } + + for chunk in padded_data.chunks(16) { + let mut block = [0_u8; 16]; + block.copy_from_slice(chunk); + + for i in 0..16 { + block[i] ^= current_iv[i]; + } + + let encrypted = self.encrypt_block(&block); + result.extend_from_slice(&encrypted); + current_iv = encrypted; + } + + result + } + + pub(crate) fn decrypt_cbc(&self, data: &[u8], iv: &[u8; 16], unpad: bool) -> Vec { + let mut result = Vec::new(); + let mut prev_block = *iv; + + for chunk in data.chunks_exact(16) { + let mut block = [0_u8; 16]; + block.copy_from_slice(chunk); + + let decrypted = self.decrypt_block(&block); + + let mut plain_block = [0_u8; 16]; + for i in 0..16 { + plain_block[i] = decrypted[i] ^ prev_block[i]; + } + + result.extend_from_slice(&plain_block); + prev_block = block; + } + + if unpad + && let Some(&pad_len) = result.last() + && pad_len > 0 + && pad_len <= 16 + && result.len() >= pad_len as usize + { + let start = result.len() - pad_len as usize; + if result[start..].iter().all(|&b| b == pad_len) { + result.truncate(start); + } + } + + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aes::cipher::{ + BlockDecryptMut, BlockEncrypt, BlockEncryptMut, KeyInit, generic_array::GenericArray, + }; + use aes::{Aes128, Aes256}; + use cbc::{ + Decryptor, Encryptor, + cipher::{KeyIvInit, block_padding::Pkcs7}, + }; + + type Aes128Cbc = Encryptor; + type Aes128CbcDec = Decryptor; + type Aes256Cbc = Encryptor; + type Aes256CbcDec = Decryptor; + + fn aes_128_block_test(key: &[u8; 16], plaintext: &[u8; 16]) { + let our_cipher = AES128Cipher::new(key).unwrap(); + + let external_cipher = Aes128::new_from_slice(key).unwrap(); + let mut external_block = GenericArray::clone_from_slice(plaintext); + external_cipher.encrypt_block(&mut external_block); + + let our_ciphertext = our_cipher.encrypt_block(plaintext); + assert_eq!( + our_ciphertext, + external_block.as_slice(), + "encryption should match" + ); + + let our_decrypted = our_cipher.decrypt_block(&our_ciphertext); + assert_eq!( + our_decrypted, *plaintext, + "roundtrip should recover original plaintext" + ); + } + + fn aes_256_block_test(key: &[u8; 32], plaintext: &[u8; 16]) { + let our_cipher = AES256Cipher::new(key).unwrap(); + + let external_cipher = Aes256::new_from_slice(key).unwrap(); + let mut external_block = GenericArray::clone_from_slice(plaintext); + external_cipher.encrypt_block(&mut external_block); + + let our_ciphertext = our_cipher.encrypt_block(plaintext); + assert_eq!( + our_ciphertext, + external_block.as_slice(), + "encryption should match" + ); + + let our_decrypted = our_cipher.decrypt_block(&our_ciphertext); + assert_eq!( + our_decrypted, *plaintext, + "roundtrip should recover original plaintext" + ); + } + + fn aes_128_cbc_test(key: &[u8; 16], iv: &[u8; 16], plaintext: &[u8]) { + let our_cipher = AES128Cipher::new(key).unwrap(); + + let our_ciphertext = our_cipher.encrypt_cbc(plaintext, iv); + + let external_decryptor = Aes128CbcDec::new_from_slices(key, iv).unwrap(); + let mut external_buffer = our_ciphertext.clone(); + let external_decrypted = external_decryptor + .decrypt_padded_mut::(&mut external_buffer) + .unwrap(); + + assert_eq!( + external_decrypted, plaintext, + "AES-128 CBC: our encryption should be decryptable by external crate" + ); + + let external_encryptor = Aes128Cbc::new_from_slices(key, iv).unwrap(); + let mut buffer = plaintext.to_vec(); + buffer.resize(plaintext.len() + 16, 0); + let external_ciphertext = external_encryptor + .encrypt_padded_mut::(&mut buffer, plaintext.len()) + .unwrap(); + + let our_decrypted = our_cipher.decrypt_cbc(external_ciphertext, iv, true); + assert_eq!( + our_decrypted, plaintext, + "AES-128 CBC: external encryption should be decryptable by our implementation" + ); + + let our_roundtrip = our_cipher.decrypt_cbc(&our_ciphertext, iv, true); + assert_eq!( + our_roundtrip, plaintext, + "AES-128 CBC: our roundtrip should work" + ); + } + + fn aes_256_cbc_test(key: &[u8; 32], iv: &[u8; 16], plaintext: &[u8]) { + let our_cipher = AES256Cipher::new(key).unwrap(); + + let our_ciphertext = our_cipher.encrypt_cbc(plaintext, iv); + + let external_decryptor = Aes256CbcDec::new_from_slices(key, iv).unwrap(); + let mut external_buffer = our_ciphertext.clone(); + let external_decrypted = external_decryptor + .decrypt_padded_mut::(&mut external_buffer) + .unwrap(); + + assert_eq!( + external_decrypted, plaintext, + "AES-256 CBC: our encryption should be decryptable by external crate" + ); + + let external_encryptor = Aes256Cbc::new_from_slices(key, iv).unwrap(); + let mut buffer = plaintext.to_vec(); + buffer.resize(plaintext.len() + 16, 0); + let external_ciphertext = external_encryptor + .encrypt_padded_mut::(&mut buffer, plaintext.len()) + .unwrap(); + + let our_decrypted = our_cipher.decrypt_cbc(external_ciphertext, iv, true); + assert_eq!( + our_decrypted, plaintext, + "AES-256 CBC: external encryption should be decryptable by our implementation" + ); + + let our_roundtrip = our_cipher.decrypt_cbc(&our_ciphertext, iv, true); + assert_eq!( + our_roundtrip, plaintext, + "AES-256 CBC: our roundtrip should work" + ); + } + + #[test] + fn test_aes128_block_operations() { + let test_cases = [ + ( + [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, + 0xcf, 0x4f, 0x3c, + ], + [ + 0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, + 0x37, 0x07, 0x34, + ], + ), + ( + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, + ], + [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, + ], + ), + ([0xff; 16], [0x00; 16]), + ([0x00; 16], [0xff; 16]), + ]; + + for (key, plaintext) in test_cases { + aes_128_block_test(&key, &plaintext); + } + } + + #[test] + fn test_aes256_block_operations() { + let test_cases = [ + ( + [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, + 0x7d, 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, + 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, + ], + [ + 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, + 0x93, 0x17, 0x2a, + ], + ), + ( + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, + 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + ], + [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, + ], + ), + ([0xff; 32], [0x00; 16]), + ([0x00; 32], [0xff; 16]), + ]; + + for (key, plaintext) in test_cases { + aes_256_block_test(&key, &plaintext); + } + } + + #[test] + fn test_aes128_encryption_decryption() { + let test_cases = [ + ( + [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, + 0xcf, 0x4f, 0x3c, + ], + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, + ], + b"Hello, World!" as &[u8], + ), + ( + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, + ], + [0xff; 16], + b"The quick brown fox jumps over the lazy dog", + ), + ( + [0xff; 16], + [0x00; 16], + b"AES-128 encryption test vector for CBC mode with PKCS7 padding", + ), + ([0xaa; 16], [0x55; 16], &[0x42_u8; 100]), + ]; + + for (key, iv, plaintext) in test_cases { + aes_128_cbc_test(&key, &iv, plaintext); + } + } + + #[test] + fn test_aes256_encryption_decryption() { + let test_cases = [ + ( + [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, + 0x7d, 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, + 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, + ], + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, + ], + b"Hello, World!" as &[u8], + ), + ( + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, + 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + ], + [0xff; 16], + b"The quick brown fox jumps over the lazy dog", + ), + ( + [0xff; 32], + [0x00; 16], + b"AES-256 encryption test vector for CBC mode with PKCS7 padding", + ), + ([0xaa; 32], [0x55; 16], &[0x42_u8; 100]), + ]; + + for (key, iv, plaintext) in test_cases { + aes_256_cbc_test(&key, &iv, plaintext); + } + } + + #[test] + fn test_aes128_cbc_decryption() { + let test_cases = [ + ( + [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, + 0xcf, 0x4f, 0x3c, + ], + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, + ], + b"Hello, World!" as &[u8], + ), + ( + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, + ], + [0xff; 16], + b"The quick brown fox jumps over the lazy dog", + ), + ( + [0xff; 16], + [0x00; 16], + b"AES-128 test vector for CBC mode with PKCS7 padding", + ), + ([0xaa; 16], [0x55; 16], &[0x42_u8; 100]), + ]; + + for (key, iv, plaintext) in test_cases { + aes_128_cbc_test(&key, &iv, plaintext); + } + } + + #[test] + fn test_aes256_cbc_decryption() { + let test_cases = [ + ( + [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, + 0x7d, 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, + 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, + ], + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, + ], + b"Hello, World!" as &[u8], + ), + ( + [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, + 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + ], + [0xff; 16], + b"The quick brown fox jumps over the lazy dog", + ), + ( + [0xff; 32], + [0x00; 16], + b"AES-256 test vector for CBC mode with PKCS7 padding", + ), + ([0xaa; 32], [0x55; 16], &[0x42_u8; 100]), + ]; + + for (key, iv, plaintext) in test_cases { + aes_256_cbc_test(&key, &iv, plaintext); + } + } + + #[test] + fn test_edge_cases() { + let key128 = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + let key256 = [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, + 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, + 0x09, 0x14, 0xdf, 0xf4, + ]; + let iv = [0x00; 16]; + + aes_128_cbc_test(&key128, &iv, b""); + aes_256_cbc_test(&key256, &iv, b""); + + aes_128_cbc_test(&key128, &iv, b"a"); + aes_256_cbc_test(&key256, &iv, b"a"); + + aes_128_cbc_test(&key128, &iv, &[0xff_u8; 15]); + aes_256_cbc_test(&key256, &iv, &[0xff_u8; 15]); + + aes_128_cbc_test(&key128, &iv, &[0xff_u8; 16]); + aes_256_cbc_test(&key256, &iv, &[0xff_u8; 16]); + + aes_128_cbc_test(&key128, &iv, &[0xff_u8; 17]); + aes_256_cbc_test(&key256, &iv, &[0xff_u8; 17]); + } + + #[test] + fn test_invalid_key_lengths() { + assert!(AES128Cipher::new(&[0_u8; 15]).is_none()); + assert!(AES128Cipher::new(&[0_u8; 17]).is_none()); + assert!(AES256Cipher::new(&[0_u8; 31]).is_none()); + assert!(AES256Cipher::new(&[0_u8; 33]).is_none()); + } +} diff --git a/vendor/hayro-syntax/src/crypto/md5.rs b/vendor/hayro-syntax/src/crypto/md5.rs new file mode 100644 index 00000000..30e8453b --- /dev/null +++ b/vendor/hayro-syntax/src/crypto/md5.rs @@ -0,0 +1,116 @@ +//! Ported from . + +use alloc::vec::Vec; + +const SHIFT_AMOUNTS: [u32; 64] = [ + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, + 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, + 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, +]; + +const CONSTANTS: [u32; 64] = [ + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, +]; + +pub(crate) fn calculate(data: &[u8]) -> [u8; 16] { + let mut state = [ + 0x67452301_u32, + 0xefcdab89_u32, + 0x98badcfe_u32, + 0x10325476_u32, + ]; + + let original_len = data.len(); + let mut message = data.to_vec(); + message.push(0x80); + + while (message.len() % 64) != 56 { + message.push(0); + } + + message.extend_from_slice(&(original_len as u64 * 8).to_le_bytes()); + + for chunk in message.chunks_exact(64) { + let words: Vec = chunk + .chunks_exact(4) + .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap())) + .collect(); + + let mut working_vars = state; + + for i in 0..64 { + let (f, g) = match i { + 0..=15 => ( + (working_vars[1] & working_vars[2]) | (!working_vars[1] & working_vars[3]), + i, + ), + 16..=31 => ( + (working_vars[3] & working_vars[1]) | (!working_vars[3] & working_vars[2]), + (5 * i + 1) % 16, + ), + 32..=47 => ( + working_vars[1] ^ working_vars[2] ^ working_vars[3], + (3 * i + 5) % 16, + ), + 48..=63 => ( + working_vars[2] ^ (working_vars[1] | !working_vars[3]), + (7 * i) % 16, + ), + _ => unreachable!(), + }; + + let temp = working_vars[3]; + working_vars[3] = working_vars[2]; + working_vars[2] = working_vars[1]; + working_vars[1] = working_vars[1].wrapping_add( + (working_vars[0] + .wrapping_add(f) + .wrapping_add(CONSTANTS[i]) + .wrapping_add(words[g])) + .rotate_left(SHIFT_AMOUNTS[i]), + ); + working_vars[0] = temp; + } + + for (state_val, working_val) in state.iter_mut().zip(working_vars.iter()) { + *state_val = state_val.wrapping_add(*working_val); + } + } + + let mut result = [0_u8; 16]; + for (i, &word) in state.iter().enumerate() { + result[i * 4..(i + 1) * 4].copy_from_slice(&word.to_le_bytes()); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + + fn md5_test(data: &[u8]) { + let our_result = calculate(data); + + let external_result = md5::compute(data); + + assert_eq!( + our_result, + external_result.as_slice(), + "MD5 calculation should match external md5 crate for input: {data:?}" + ); + } + + #[test] + fn correctness() { + md5_test(b"Hello, World!"); + md5_test(b"The quick brown fox jumps over the lazy dog"); + md5_test(b"abcdefghijklmnopqrstuvwxyz"); + } +} diff --git a/vendor/hayro-syntax/src/crypto/mod.rs b/vendor/hayro-syntax/src/crypto/mod.rs new file mode 100644 index 00000000..15371514 --- /dev/null +++ b/vendor/hayro-syntax/src/crypto/mod.rs @@ -0,0 +1,905 @@ +//! Cryptographic implementations for hayro, ported from pdf.js. +//! +//! **Important note**: Please keep in mind that these haven't been +//! audited and should not be used for security-critical purposes, like creating new +//! encrypted PDFs. They solely serve the purpose of being able to decrypt and read +//! _already_ encrypted documents, where security isn't really relevant. + +use crate::crypto::DecryptionError::InvalidEncryption; +use crate::crypto::aes::{AES128Cipher, AES256Cipher}; +use crate::crypto::rc4::Rc4; +use crate::object; +use crate::object::dict::keys::{ + CF, CFM, ENCRYPT_META_DATA, FILTER, LENGTH, O, OE, P, R, STM_F, STR_F, U, UE, V, +}; +use crate::object::{Dict, Name, ObjectIdentifier}; +use crate::sync::HashMap; +use alloc::string::ToString; +use alloc::vec; +use alloc::vec::Vec; +use core::cmp; +use core::ops::Deref; + +mod aes; +mod md5; +mod rc4; +mod sha256; +mod sha384; +mod sha512; + +const PASSWORD_PADDING: [u8; 32] = [ + 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A, +]; + +/// An error that occurred during decryption. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum DecryptionError { + /// The ID entry is missing in the PDF. + MissingIDEntry, + /// The PDF is password-protected and no password (or a wrong one) was + /// provided. + PasswordProtected, + /// The PDF has invalid encryption. + InvalidEncryption, + /// The PDF uses an unsupported encryption algorithm. + UnsupportedAlgorithm, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum DecryptorTag { + None, + Rc4, + Aes128, + Aes256, +} + +impl DecryptorTag { + fn from_name(name: &Name<'_>) -> Option { + match name.as_str() { + "None" | "Identity" => Some(Self::None), + "V2" => Some(Self::Rc4), + "AESV2" => Some(Self::Aes128), + "AESV3" => Some(Self::Aes256), + _ => None, + } + } +} +#[derive(Debug, Clone)] +pub(crate) enum Decryptor { + None, + Rc4 { key: Vec }, + Aes128 { key: Vec, dict: DecryptorData }, + Aes256 { key: Vec, dict: DecryptorData }, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum DecryptionTarget { + String, + Stream, +} + +impl Decryptor { + pub(crate) fn decrypt( + &self, + id: ObjectIdentifier, + data: &[u8], + target: DecryptionTarget, + ) -> Option> { + match self { + Self::None => Some(data.to_vec()), + Self::Rc4 { key } => decrypt_rc4(key, data, id), + Self::Aes128 { key, dict } | Self::Aes256 { key, dict } => { + let crypt_dict = match target { + DecryptionTarget::String => dict.string_filter, + DecryptionTarget::Stream => dict.stream_filter, + }; + + match crypt_dict.cfm { + DecryptorTag::None => Some(data.to_vec()), + DecryptorTag::Rc4 => decrypt_rc4(key, data, id), + DecryptorTag::Aes128 => decrypt_aes128(key, data, id), + DecryptorTag::Aes256 => decrypt_aes256(key, data), + } + } + } + } +} + +pub(crate) fn get( + dict: &Dict<'_>, + id: &[u8], + password: &[u8], +) -> Result { + let filter = dict.get::>(FILTER).ok_or(InvalidEncryption)?; + + if filter.deref() != b"Standard" { + return Err(DecryptionError::UnsupportedAlgorithm); + } + + let encryption_v = dict.get::(V).ok_or(InvalidEncryption)?; + let encrypt_metadata = dict.get::(ENCRYPT_META_DATA).unwrap_or(true); + let revision = dict.get::(R).ok_or(InvalidEncryption)?; + let length = match encryption_v { + 1 => 40, + 2 => dict.get::(LENGTH).unwrap_or(40), + 4 => dict.get::(LENGTH).unwrap_or(128), + 5 => 256, + _ => return Err(DecryptionError::UnsupportedAlgorithm), + }; + + let (algorithm, data) = match encryption_v { + 1 => (DecryptorTag::Rc4, None), + 2 => (DecryptorTag::Rc4, None), + 4 => ( + DecryptorTag::Aes128, + Some(DecryptorData::from_dict(dict, length).ok_or(InvalidEncryption)?), + ), + 5 | 6 => ( + DecryptorTag::Aes256, + Some(DecryptorData::from_dict(dict, length).ok_or(InvalidEncryption)?), + ), + _ => { + return Err(DecryptionError::UnsupportedAlgorithm); + } + }; + + let byte_length = length / 8; + if byte_length == 0 { + return Err(InvalidEncryption); + } + + let owner_string = dict.get::>(O).ok_or(InvalidEncryption)?; + let user_string = dict.get::>(U).ok_or(InvalidEncryption)?; + let permissions = { + let raw = dict.get::(P).ok_or(InvalidEncryption)?; + + if raw < 0 { + u32::from_be_bytes((raw as i32).to_be_bytes()) + } else { + raw as u32 + } + }; + + let mut decryption_key = if revision <= 4 { + // PdfCraft patch: (see vendor/README.md hayro-syntax (14)) the password is tried as + // given and then in PDFDocEncoding, which revisions 2โ€“4 specify. + let mut found = Err(DecryptionError::PasswordProtected); + for candidate in legacy_passwords(password) { + found = decryption_key_rev1234( + &candidate, + encrypt_metadata, + revision, + byte_length, + &owner_string, + permissions, + id, + ) + .and_then(|key| { + authenticate_user_password_rev234(revision, &key, id, &user_string).map(|()| key) + }); + if found.is_ok() { + break; + } + } + found? + } else { + // PdfCraft patch: (see vendor/README.md hayro-syntax (16)) the password is tried in its + // SASLprep form, which revisions 5โ€“6 specify, and then as given. + let mut found = Err(DecryptionError::PasswordProtected); + for candidate in aes256_passwords(password) { + found = decryption_key_rev56(dict, revision, &candidate, &owner_string, &user_string); + if found.is_ok() { + break; + } + } + found? + }; + + // See pdf.js issue 19484. + if encryption_v == 4 && decryption_key.len() < 16 { + decryption_key.resize(16, 0); + } + + match algorithm { + DecryptorTag::None => Ok(Decryptor::None), + DecryptorTag::Rc4 => Ok(Decryptor::Rc4 { + key: decryption_key, + }), + DecryptorTag::Aes128 => Ok(Decryptor::Aes128 { + key: decryption_key, + dict: data.unwrap(), + }), + DecryptorTag::Aes256 => Ok(Decryptor::Aes256 { + key: decryption_key, + dict: data.unwrap(), + }), + } +} + +/// Algorithm 1.A: Encryption of data using the AES algorithms +fn decrypt_aes256(key: &[u8], data: &[u8]) -> Option> { + // a) Use the 32-byte file encryption key for the AES-256 symmetric key algorithm, + // along with the string or stream data to be encrypted. + // Use the AES algorithm in Cipher Block Chaining (CBC) mode, which requires an initialization + // vector. The block size parameter is set to 16 bytes, and the initialization + // vector is a 16-byte random number that is stored as the first 16 bytes of the + // encrypted stream or string. + let (iv, data) = data.split_at_checked(16)?; + let iv: [u8; 16] = iv.try_into().ok()?; + let cipher = AES256Cipher::new(key)?; + Some(cipher.decrypt_cbc(data, &iv, true)) +} + +fn decrypt_aes128(key: &[u8], data: &[u8], id: ObjectIdentifier) -> Option> { + decrypt_rc_aes(key, id, true, |key| { + // If using the AES algorithm, the Cipher Block Chaining (CBC) mode, which requires an initialization + // vector, is used. The block size parameter is set to 16 bytes, and the initialization vector is a 16-byte + // random number that is stored as the first 16 bytes of the encrypted stream or string. + let cipher = AES128Cipher::new(key)?; + let (iv, data) = data.split_at_checked(16)?; + let iv: [u8; 16] = iv.try_into().ok()?; + + Some(cipher.decrypt_cbc(data, &iv, true)) + }) +} + +fn decrypt_rc4(key: &[u8], data: &[u8], id: ObjectIdentifier) -> Option> { + decrypt_rc_aes(key, id, false, |key| { + let mut rc = Rc4::new(key); + Some(rc.decrypt(data)) + }) +} + +/// Algorithm 1: Encryption of data using the RC4 or AES algorithms +fn decrypt_rc_aes( + key: &[u8], + id: ObjectIdentifier, + aes: bool, + with_key: impl FnOnce(&[u8]) -> Option>, +) -> Option> { + let n = key.len(); + // a) Obtain the object number and generation number from the object identifier of + // the string or stream to be encrypted (see 7.3.10, "Indirect objects"). If the + // string is a direct object, use the identifier of the indirect object containing + // it. + let mut key = key.to_vec(); + + // b) For all strings and streams without crypt filter specifier; treating the + // object number and generation number as binary integers, extend the original + // n-byte file encryption key to n + 5 bytes by appending the low-order 3 bytes of + // the object number and the low-order 2 bytes of the generation number in that + // order, low-order byte first. + key.extend(&id.obj_number.to_le_bytes()[..3]); + key.extend(&id.gen_number.to_le_bytes()[..2]); + + // If using the AES algorithm, extend the file encryption key an additional 4 bytes by adding the value + // "sAlT", which corresponds to the hexadecimal values 0x73, 0x41, 0x6C, 0x54. (This addition is done + // for backward compatibility and is not intended to provide additional security.) + if aes { + key.extend(b"sAlT"); + } + + // c) Initialise the MD5 hash function and pass the result of step (b) as input + // to this function. + let hash = md5::calculate(&key); + + // d) Use the first (n + 5) bytes, up to a maximum of 16, of the output + // from the MD5 hash as the key for the RC4 or AES symmetric key algorithms, + // along with the string or stream data to be encrypted. + let final_key = &hash[..cmp::min(16, n + 5)]; + + with_key(final_key) +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct DecryptorData { + stream_filter: CryptDictionary, + string_filter: CryptDictionary, +} + +impl DecryptorData { + fn from_dict(dict: &Dict<'_>, default_length: u16) -> Option { + let mut mappings = HashMap::new(); + + if let Some(dict) = dict.get::>(CF) { + for key in dict.keys() { + if let Some(dict) = dict.get::>(key.as_ref()) + && let Some(crypt_dict) = CryptDictionary::from_dict(&dict, default_length) + { + mappings.insert(key.as_str().to_string(), crypt_dict); + } + } + } + + let stm_f = *mappings + .get(dict.get::>(STM_F)?.as_str()) + .unwrap_or(&CryptDictionary::identity(default_length)); + let str_f = *mappings + .get(dict.get::>(STR_F)?.as_str()) + .unwrap_or(&CryptDictionary::identity(default_length)); + + Some(Self { + stream_filter: stm_f, + string_filter: str_f, + }) + } +} + +#[derive(Debug, Copy, Clone)] +struct CryptDictionary { + cfm: DecryptorTag, + _length: u16, +} + +impl CryptDictionary { + fn from_dict(dict: &Dict<'_>, default_length: u16) -> Option { + let cfm = DecryptorTag::from_name(&dict.get::>(CFM)?)?; + // The standard security handler expresses the Length entry in bytes (e.g., 32 means a + // length of 256 bits) and public-key security handlers express it as is (e.g., 256 means a + // length of 256 bits). + // Note: We only support the standard security handler. + let mut length = dict.get::(LENGTH).unwrap_or(default_length / 8); + + // When CFM is AESV2, the Length key shall have the value of 128. When + // CFM is AESV3, the Length key shall have a value of 256. + if cfm == DecryptorTag::Aes128 { + length = 16; + } else if cfm == DecryptorTag::Aes256 { + length = 32; + } + + Some(Self { + cfm, + _length: length, + }) + } + + fn identity(default_length: u16) -> Self { + Self { + cfm: DecryptorTag::None, + _length: default_length, + } + } +} + +/// Algorithm 2.B: Computing a hash (revision 6 and later) +fn compute_hash_rev56( + password: &[u8], + validation_salt: &[u8], + user_string: Option<&[u8]>, + revision: u8, +) -> Result<[u8; 32], DecryptionError> { + // Take the SHA-256 hash of the original input to the algorithm and name the resulting + // 32 bytes, K. + let mut k = { + let mut input = Vec::new(); + input.extend_from_slice(password); + input.extend_from_slice(validation_salt); + + if let Some(user_string) = user_string { + input.extend_from_slice(user_string); + } + + let hash = sha256::calculate(&input); + + // Apparently revision 5 only uses this hash. + if revision == 5 { + return Ok(hash); + } + + hash.to_vec() + }; + + let mut round: u16 = 0; + + // Perform the following steps (a)-(d) 64 times: + loop { + // a) Make a new string, K1, consisting of 64 repetitions of the sequence: + // input password, K, the 48-byte user key. The 48 byte user key is only used when + // checking the owner password or creating the owner key. If checking the user + // password or creating the user key, K1 is the concatenation of the input + // password and K. + let k1 = { + let mut single: Vec = vec![]; + single.extend(password); + single.extend(&k); + + if let Some(user_string) = user_string { + single.extend(user_string); + } + + single.repeat(64) + }; + + // b) Encrypt K1 with the AES-128 (CBC, no padding) algorithm, + // using the first 16 bytes of K as the key and the second 16 bytes of K as the + // initialization vector. The result of this encryption is E. + let e = { + let aes = AES128Cipher::new(&k[..16]).ok_or(InvalidEncryption)?; + let mut res = aes.encrypt_cbc(&k1, &k[16..32].try_into().unwrap()); + + // Remove padding that was added by `encrypt_cbc`. + res.truncate(k1.len()); + + res + }; + + // c) Taking the first 16 bytes of E as an unsigned big-endian integer, + // compute the remainder, modulo 3. If the result is 0, the next hash used is + // SHA-256, if the result is 1, the next hash used is SHA-384, if the result is + // 2, the next hash used is SHA-512. + let num = u128::from_be_bytes(e[..16].try_into().unwrap()) % 3; + + // d) Using the hash algorithm determined in step c, take the hash of E. + // The result is a new value of K, which will be 32, 48, or 64 bytes in length. + k = match num { + 0 => sha256::calculate(&e).to_vec(), + 1 => sha384::calculate(&e).to_vec(), + 2 => sha512::calculate(&e).to_vec(), + _ => unreachable!(), + }; + + round += 1; + + // Repeat the process (a-d) with this new value for K. Following 64 rounds + // (round number 0 to round number 63), do the following, starting with round + // number 64: + if round > 63 { + // e) Look at the very last byte of E. If the value of that byte + // (taken as an unsigned integer) is greater than the round number - 32, + // repeat steps (a-d) again. + let last_byte = *e.last().unwrap(); + + // f) Repeat from steps (a-e) until the value of the last byte + // is < (round number) - 32. + // For some reason we need to use <= here? + if (last_byte as u16) <= round - 32 { + break; + } + } + } + + // The first 32 bytes of the final K are the output of the algorithm. + let mut result = [0_u8; 32]; + result.copy_from_slice(&k[..32]); + Ok(result) +} + +/// `PdfCraft` patch: the byte strings to try as a revision 2โ€“4 password. Revisions 2โ€“4 take the +/// password in `PDFDocEncoding` (ISO 32000-2 ยง7.6.4.3.2, Algorithm 2 a), but the password arrives +/// as UTF-8 text: first the bytes as given (what earlier versions used, and what some writers +/// store), then, for text with characters outside ASCII that `PDFDocEncoding` all has ("ฤฑlฤฑkโ‚ฌ", +/// "ร‡ok gรผzel"), its `PDFDocEncoding` bytes. +fn legacy_passwords(password: &[u8]) -> Vec> { + let mut out = vec![password.to_vec()]; + if let Ok(text) = core::str::from_utf8(password) + && !text.is_ascii() + && let Some(doc) = text.chars().map(pdf_doc_byte).collect::>>() + { + out.push(doc); + } + out +} + +/// `PdfCraft` patch: `PDFDocEncoding` (ISO 32000-2 Annex D) of `c`, `None` when it has none. +/// It differs from Latin-1 in 0x18โ€“0x1F and 0x80โ€“0xA0 (0x9F is undefined). +fn pdf_doc_byte(c: char) -> Option { + const LOW: [char; 8] = ['ห˜', 'ห‡', 'ห†', 'ห™', 'ห', 'ห›', 'หš', 'หœ']; + const HIGH: [char; 33] = [ + 'โ€ข', 'โ€ ', 'โ€ก', 'โ€ฆ', 'โ€”', 'โ€“', 'ฦ’', 'โ„', 'โ€น', 'โ€บ', 'โˆ’', 'โ€ฐ', 'โ€ž', 'โ€œ', 'โ€', 'โ€˜', 'โ€™', 'โ€š', + 'โ„ข', '๏ฌ', '๏ฌ‚', 'ล', 'ล’', 'ล ', 'ลธ', 'ลฝ', 'ฤฑ', 'ล‚', 'ล“', 'ลก', 'ลพ', '\u{FFFD}', 'โ‚ฌ', + ]; + let at = |table: &[char], base: u8| { + table + .iter() + .position(|&t| t == c && t != '\u{FFFD}') + .and_then(|i| u8::try_from(i).ok()) + .map(|i| base + i) + }; + at(&LOW, 0x18) + .or_else(|| at(&HIGH, 0x80)) + .or_else(|| match u32::from(c) { + 0x18..=0x1F | 0x80..=0xA0 => None, + n => u8::try_from(n).ok(), + }) +} + +/// `PdfCraft` patch: the byte strings to try as a revision 5โ€“6 password. Revisions 5โ€“6 take the +/// password processed with `SASLprep` (RFC 4013) and in UTF-8, at most 127 bytes (ISO 32000-2 +/// ยง7.6.4.3.3, Algorithm 2.A aโ€“b), but the password arrives as typed: first its `SASLprep` +/// form, as pdfcraft-crypt writes it (a decomposed "cafรฉ" composed, a fullwidth "๏ฝ๏ฝ‚" narrowed, +/// a no-break space made a space), then, when that differs, the bytes as given (what earlier +/// versions used, and what writers that skip `SASLprep` store), each at most 127 bytes. +fn aes256_passwords(password: &[u8]) -> Vec> { + let given = password.get(..127).unwrap_or(password).to_vec(); + match sasl_prepared(password) { + Some(prepared) if prepared != given => vec![prepared, given], + _ => vec![given], + } +} + +/// `PdfCraft` patch: `password` processed with `SASLprep`, in UTF-8 and truncated to 127 bytes, +/// as pdfcraft-crypt does it: a password `SASLprep` refuses (one with prohibited or unassigned +/// characters) is taken as given. `None` when `password` isn't UTF-8. +#[cfg(feature = "std")] +fn sasl_prepared(password: &[u8]) -> Option> { + let text = core::str::from_utf8(password).ok()?; + let prepared = stringprep::saslprep(text).map_or_else(|_| text.to_string(), |p| p.into_owned()); + let mut bytes = prepared.into_bytes(); + bytes.truncate(127); + Some(bytes) +} + +/// `PdfCraft` patch: without `std`, which stringprep needs, passwords are taken as given. +#[cfg(not(feature = "std"))] +fn sasl_prepared(_: &[u8]) -> Option> { + None +} + +/// Algorithm 2: Computing a file encryption key in order to encrypt a document (revision 4 and earlier) +fn decryption_key_rev1234( + password: &[u8], + encrypt_metadata: bool, + revision: u8, + byte_length: u16, + owner_string: &object::String<'_>, + permissions: u32, + id: &[u8], +) -> Result, DecryptionError> { + let mut md5_input = vec![]; + + // (PDFDocEncoding: see `legacy_passwords`, PdfCraft patch.) + // a) Pad or truncate password to 32 bytes using PASSWORD_PADDING. + let mut padded_password = [0_u8; 32]; + let copy_len = password.len().min(32); + padded_password[..copy_len].copy_from_slice(&password[..copy_len]); + if copy_len < 32 { + padded_password[copy_len..].copy_from_slice(&PASSWORD_PADDING[..(32 - copy_len)]); + } + + // b) Initialise the MD5 hash function and pass the + // result of step a) as input to this function. + md5_input.extend(&padded_password); + + // c) Pass the value of the encryption dictionary's O entry + // to the MD5 hash function. + md5_input.extend(owner_string.as_ref()); + + // d) Convert the integer value of the P entry to a 32-bit unsigned + // binary number and pass these bytes to the MD5 hash function, low-order byte first. + md5_input.extend(permissions.to_le_bytes()); + + // e) Pass the first element of the file's file identifier array to the MD5 hash function. + md5_input.extend(id); + + // f) (Security handlers of revision 4 or greater) If document metadata + // is not being encrypted, pass 4 bytes with the value 0xFFFFFFFF to the MD5 hash function. + if !encrypt_metadata && revision >= 4 { + md5_input.extend(&[0xff, 0xff, 0xff, 0xff]); + } + + // g) Finish the hash. + let mut hash = md5::calculate(&md5_input); + + // h) For revisions >= 3, do the following 50 times: Take the output from the previous + // MD5 hash and pass the first n bytes of the output as input into a new MD5 hash, + // where n is the number of bytes of the file encryption key as defined by the value + // of the encryption dictionary's `Length` entry. + if revision >= 3 { + for _ in 0..50 { + hash = md5::calculate(&hash[..byte_length as usize]); + } + } + + let decryption_key = hash[..byte_length as usize].to_vec(); + Ok(decryption_key) +} + +/// Algorithm 6: Authenticating the user password +fn authenticate_user_password_rev234( + revision: u8, + decryption_key: &[u8], + id: &[u8], + user_string: &object::String<'_>, +) -> Result<(), DecryptionError> { + // a) Perform all but the last step of Algorithm 4 (revision 2) or Algorithm 5 (revision 3 + 4). + let result = match revision { + 2 => user_password_rev2(decryption_key), + 3 | 4 => user_password_rev34(decryption_key, id), + _ => return Err(InvalidEncryption), + }; + + // b) If the result of step (a) is equal to the value of the encryption dictionary's + // U entry (comparing on the first 16 bytes in the case of security handlers of + // revision 3 or greater), the password supplied is the correct user password. + match revision { + 2 => { + if result.as_slice() != user_string.as_ref() { + return Err(DecryptionError::PasswordProtected); + } + } + 3 | 4 => { + if Some(&result[..16]) != user_string.as_ref().get(0..16) { + return Err(DecryptionError::PasswordProtected); + } + } + _ => unreachable!(), + } + + Ok(()) +} + +/// Algorithm 4: Computing the encryption dictionaryโ€™s U-entry value +/// (Security handlers of revision 2). +fn user_password_rev2(decryption_key: &[u8]) -> Vec { + // a) Create a file encryption key based on the user password string. + // b) Encrypt the 32-byte padding string using an RC4 encryption + // function with the file encryption key from the preceding step. + let mut rc = Rc4::new(decryption_key); + rc.decrypt(&PASSWORD_PADDING) +} + +/// Algorithm 5: Computing the encryption dictionaryโ€™s U (user password) +/// value (Security handlers of revision 3 or 4). +fn user_password_rev34(decryption_key: &[u8], id: &[u8]) -> Vec { + // a) Create a file encryption key based on the user password string. + let mut rc = Rc4::new(decryption_key); + + let mut input = vec![]; + // b) Initialise the MD5 hash function and pass the 32-byte padding string. + input.extend(PASSWORD_PADDING); + + // c) Pass the first element of the file's file identifier array to the hash function + // and finish the hash. + input.extend(id); + let hash = md5::calculate(&input); + + // d) Encrypt the 16-byte result of the hash, using an RC4 encryption function with + // the encryption key from step (a). + let mut encrypted = rc.encrypt(&hash); + + // e) Do the following 19 times: Take the output from the previous invocation of the + // RC4 function and pass it as input to a new invocation of the function; use a file + // encryption key generated by taking each byte of the original file encryption key + // obtained in step (a) and performing an XOR (exclusive or) operation between that + // byte and the single-byte value of the iteration counter (from 1 to 19). + for i in 1..=19 { + let mut key = decryption_key.to_vec(); + for byte in &mut key { + *byte ^= i; + } + + let mut rc = Rc4::new(&key); + encrypted = rc.encrypt(&encrypted); + } + + encrypted.resize(32, 0); + encrypted +} + +/// Algorithm 2.A: Retrieving the file encryption key from an encrypted document in order to decrypt it (revision 6 and later) +fn decryption_key_rev56( + dict: &Dict<'_>, + revision: u8, + password: &[u8], + owner_string: &object::String<'_>, + user_string: &object::String<'_>, +) -> Result, DecryptionError> { + // a) The UTF-8 password string shall be generated from Unicode input by processing the input string with + // the SASLprep (Internet RFC 4013) profile of stringprep (Internet RFC 3454) using the Normalize and BiDi + // options, and then converting to a UTF-8 representation. + // (SASLprep: see `aes256_passwords`, PdfCraft patch.) + // b) Truncate the UTF-8 representation to 127 bytes if it is longer than 127 bytes. + let password = &password[..password.len().min(127)]; + + let string_len = if revision <= 4 { 32 } else { 48 }; + + let trimmed_os = owner_string.get(..string_len).ok_or(InvalidEncryption)?; + + let (owner_hash, owner_tail) = trimmed_os.split_at_checked(32).ok_or(InvalidEncryption)?; + let (owner_validation_salt, owner_key_salt) = + owner_tail.split_at_checked(8).ok_or(InvalidEncryption)?; + + let trimmed_us = user_string.get(..string_len).ok_or(InvalidEncryption)?; + let (user_hash, user_tail) = trimmed_us.split_at_checked(32).ok_or(InvalidEncryption)?; + let (user_validation_salt, user_key_salt) = + user_tail.split_at_checked(8).ok_or(InvalidEncryption)?; + + // c) Test the password against the owner key by computing a hash using algorithm 2.B + // with an input string consisting of the UTF-8 password concatenated with the 8 bytes of + // owner Validation Salt, concatenated with the 48-byte U string. If the 32-byte result + // matches the first 32 bytes of the O string, this is the owner password. + if compute_hash_rev56(password, owner_validation_salt, Some(trimmed_us), revision)? + == owner_hash + { + // d) Compute an intermediate owner key by computing a hash using algorithm 2.B with an input string + // consisting of the UTF-8 owner password concatenated with the 8 bytes of owner Key Salt, + // concatenated with the 48-byte U string. The 32-byte result is the key used to decrypt the 32-byte OE string + // using AES-256 in CBC mode with no padding and an initialization vector of zero. The 32-byte result is the file encryption key. + let intermediate_owner_key = + compute_hash_rev56(password, owner_key_salt, Some(trimmed_us), revision)?; + + let oe_string = dict + .get::>(OE) + .ok_or(InvalidEncryption)?; + + if oe_string.len() != 32 { + return Err(InvalidEncryption); + } + + let cipher = AES256Cipher::new(&intermediate_owner_key).ok_or(InvalidEncryption)?; + let zero_iv = [0_u8; 16]; + + Ok(cipher.decrypt_cbc(&oe_string, &zero_iv, false)) + } else if compute_hash_rev56(password, user_validation_salt, None, revision)? == user_hash { + // e) Compute an intermediate user key by computing a hash using algorithm 2.B with an input string + // consisting of the UTF-8 user password concatenated with the 8 bytes of user Key Salt. The 32-byte result + // is the key used to decrypt the 32-byte UE string using AES-256 in CBC mode with no padding and an + // initialization vector of zero. The 32-byte result is the file encryption key. + let intermediate_key = compute_hash_rev56(password, user_key_salt, None, revision)?; + + let ue_string = dict + .get::>(UE) + .ok_or(InvalidEncryption)?; + + if ue_string.len() != 32 { + return Err(InvalidEncryption); + } + + let cipher = AES256Cipher::new(&intermediate_key).ok_or(InvalidEncryption)?; + let zero_iv = [0_u8; 16]; + + Ok(cipher.decrypt_cbc(&ue_string, &zero_iv, false)) + } else { + Err(DecryptionError::PasswordProtected) + } + + // TODO: + // f) Decrypt the 16-byte Perms string using AES-256 in ECB mode with an initialization vector of zero and + // the file encryption key as the key. Verify that bytes 9-11 of the result are the characters "a", "d", + // "b". Bytes 0-3 of the decrypted Perms entry, treated as a little-endian integer, are the user + // permissions. They shall match the value in the P key. +} + +#[cfg(test)] +mod pdfcraft_password_tests { + use super::*; + + #[test] + fn legacy_passwords_are_tried_as_given_then_in_pdf_doc_encoding() { + assert_eq!(legacy_passwords(b"pw"), [b"pw".to_vec()]); + assert_eq!( + legacy_passwords("ฤฑlฤฑkโ‚ฌ".as_bytes()), + ["ฤฑlฤฑkโ‚ฌ".as_bytes().to_vec(), vec![0x9A, b'l', 0x9A, b'k', 0xA0]] + ); + assert_eq!( + legacy_passwords("ร‡ok".as_bytes()), + ["ร‡ok".as_bytes().to_vec(), vec![0xC7, b'o', b'k']] + ); + // PDFDocEncoding has no ลŸ: only the bytes as given. + assert_eq!(legacy_passwords("ลŸifre".as_bytes()), ["ลŸifre".as_bytes().to_vec()]); + assert_eq!(legacy_passwords(&[0xFF, 0x00]), [vec![0xFF, 0x00]]); + } + + #[test] + #[cfg(feature = "std")] + fn aes256_passwords_are_tried_in_their_saslprep_form_then_as_given() { + let both = |prepared: &str, given: &str| [prepared.into(), given.as_bytes().to_vec()]; + assert_eq!(aes256_passwords(b"pw"), [b"pw".to_vec()]); + assert_eq!( + aes256_passwords("caf\u{E9}".as_bytes()), + ["caf\u{E9}".as_bytes().to_vec()] + ); + assert_eq!( + aes256_passwords("cafe\u{301}".as_bytes()), + both("caf\u{E9}", "cafe\u{301}") + ); + assert_eq!( + aes256_passwords("a\u{A0}b".as_bytes()), + both("a b", "a\u{A0}b") + ); + assert_eq!( + aes256_passwords("\u{FF41}\u{FF42}".as_bytes()), + both("ab", "\u{FF41}\u{FF42}") + ); + // A soft hyphen is mapped to nothing. + assert_eq!( + aes256_passwords("pass\u{AD}word".as_bytes()), + both("password", "pass\u{AD}word") + ); + // SASLprep refuses a control character: as given. + assert_eq!(aes256_passwords(b"a\x07b"), [b"a\x07b".to_vec()]); + // Not UTF-8: as given. + assert_eq!(aes256_passwords(&[0xFF, 0x00]), [vec![0xFF, 0x00]]); + // At most 127 bytes, cut after SASLprep: 64 decomposed รฉ (192 bytes) become 128 bytes of + // precomposed รฉ, cut inside the last one. + let long = "e\u{301}".repeat(64); + let prepared = "\u{E9}".repeat(64); + assert_eq!( + aes256_passwords(long.as_bytes()), + [ + prepared.as_bytes()[..127].to_vec(), + long.as_bytes()[..127].to_vec() + ] + ); + } + + /// A revision 5 or 6 `/Encrypt` dictionary whose user and owner passwords are the bytes + /// `user` and `owner` and whose file key is `key` (ISO 32000-2 Algorithms 8 and 9). + #[cfg(feature = "std")] + fn rev56_dict(revision: u8, user: &[u8], owner: &[u8], key: &[u8; 32]) -> Vec { + let hash = |password: &[u8], salt: &[u8], u: Option<&[u8]>| { + compute_hash_rev56(password, salt, u, revision).unwrap() + }; + // AES-256 in CBC mode without padding: `encrypt_cbc` pads, which only adds a block. + let wrap = + |k: [u8; 32]| AES256Cipher::new(&k).unwrap().encrypt_cbc(key, &[0; 16])[..32].to_vec(); + let (user_validation, user_key, owner_validation, owner_key) = + ([1; 8], [2; 8], [3; 8], [4; 8]); + let u = [ + &hash(user, &user_validation, None)[..], + &user_validation, + &user_key, + ] + .concat(); + let ue = wrap(hash(user, &user_key, None)); + let o_hash = hash(owner, &owner_validation, Some(&u)); + let o = [&o_hash[..], &owner_validation, &owner_key].concat(); + let oe = wrap(hash(owner, &owner_key, Some(&u))); + let hex = |b: &[u8]| b.iter().map(|b| format!("{b:02X}")).collect::(); + let (o, u, oe, ue) = (hex(&o), hex(&u), hex(&oe), hex(&ue)); + format!( + "<< /Filter /Standard /V 5 /R {revision} /Length 256 /P -4 \ + /CF << /StdCF << /CFM /AESV3 /Length 32 >> >> /StmF /StdCF /StrF /StdCF \ + /O <{o}> /U <{u}> /OE <{oe}> /UE <{ue}> >>" + ) + .into_bytes() + } + + /// The file key that `password` unlocks in the `/Encrypt` dictionary `dict`. + #[cfg(feature = "std")] + fn unlock(dict: &[u8], password: &str) -> Result, DecryptionError> { + use crate::reader::{Reader, ReaderContext, ReaderExt}; + let dict = Reader::new(dict).read_with_context::>(&ReaderContext::dummy()); + match get(&dict.unwrap(), b"", password.as_bytes())? { + Decryptor::Aes256 { key, .. } => Ok(key), + other => panic!("not AES-256: {other:?}"), + } + } + + #[test] + #[cfg(feature = "std")] + fn aes256_passwords_open_as_typed_and_in_their_saslprep_form() { + let key = [9; 32]; + // As typed, and the SASLprep form that ISO 32000-2 keys the file on (and pdfcraft-crypt + // writes): a decomposed รฉ (NFKC composes it), a no-break space (mapped to a space) and + // fullwidth letters (what a Japanese IME types in full-width mode; NFKC narrows them). + // Precomposed "cafรฉ" is the control: SASLprep leaves it as it is. + let cases = [ + ("cafe\u{301}", "caf\u{E9}"), + ("a\u{A0}b", "a b"), + ("\u{FF41}\u{FF42}", "ab"), + ("caf\u{E9}", "caf\u{E9}"), + ]; + let (ok, wrong) = (Ok(key.to_vec()), Err(DecryptionError::PasswordProtected)); + // Revision 5 hashes once, so it takes every case; revision 6, whose hash is far slower + // in a debug build, takes the first. + for (typed, prepared) in cases { + let user = rev56_dict(5, prepared.as_bytes(), b"owner", &key); + let owner = rev56_dict(5, b"user", prepared.as_bytes(), &key); + for pw in [typed, prepared] { + assert_eq!(unlock(&user, pw), ok, "{typed:?}: user {pw:?}"); + assert_eq!(unlock(&owner, pw), ok, "{typed:?}: owner {pw:?}"); + } + assert_eq!(unlock(&user, "cafe"), wrong, "{typed:?}"); + // A writer that skipped SASLprep keyed the file on the password as typed: it opens + // with that. + let unprepared = rev56_dict(5, typed.as_bytes(), b"owner", &key); + assert_eq!(unlock(&unprepared, typed), ok, "{typed:?}: unprepared"); + } + let user = rev56_dict(6, "caf\u{E9}".as_bytes(), b"owner", &key); + assert_eq!(unlock(&user, "cafe\u{301}"), ok, "R6"); + assert_eq!(unlock(&user, "cafe"), wrong, "R6"); + } +} diff --git a/vendor/hayro-syntax/src/crypto/rc4.rs b/vendor/hayro-syntax/src/crypto/rc4.rs new file mode 100644 index 00000000..46dd1b6e --- /dev/null +++ b/vendor/hayro-syntax/src/crypto/rc4.rs @@ -0,0 +1,125 @@ +//! Ported from . + +use alloc::vec; +use alloc::vec::Vec; + +#[derive(Clone)] +pub(crate) struct Rc4 { + a: u8, + b: u8, + s: [u8; 256], +} + +impl Rc4 { + pub(crate) fn new(key: &[u8]) -> Self { + let mut s = [0_u8; 256]; + let key_length = key.len(); + + for (i, s) in s.iter_mut().enumerate() { + *s = i as u8; + } + + let mut j = 0_u8; + for i in 0..256 { + let tmp = s[i]; + j = j.wrapping_add(tmp).wrapping_add(key[i % key_length]); + s[i] = s[j as usize]; + s[j as usize] = tmp; + } + + Self { a: 0, b: 0, s } + } + + pub(crate) fn decrypt(&mut self, data: &[u8]) -> Vec { + let n = data.len(); + let mut output = vec![0_u8; n]; + + for i in 0..n { + self.a = self.a.wrapping_add(1); + let tmp = self.s[self.a as usize]; + self.b = self.b.wrapping_add(tmp); + let tmp2 = self.s[self.b as usize]; + self.s[self.a as usize] = tmp2; + self.s[self.b as usize] = tmp; + output[i] = data[i] ^ self.s[tmp.wrapping_add(tmp2) as usize]; + } + + output + } + + pub(crate) fn encrypt(&mut self, data: &[u8]) -> Vec { + self.decrypt(data) + } +} + +#[cfg(test)] +mod rc4_tests { + use crate::crypto::rc4::Rc4; + + fn rc4_decrypt(key: &[u8], input: &[u8]) -> Vec { + let mut cipher = Rc4::new(key); + cipher.decrypt(input) + } + + #[test] + fn correctness() { + assert_eq!(rc4_decrypt(b"a", &[0x68]), b"x"); + assert_eq!(rc4_decrypt(b"key", &[0x7F, 0x09, 0x47, 0x99]), b"test"); + assert_eq!( + rc4_decrypt(b"hello", &[0x78, 0x3E, 0xCD, 0x96, 0xCF]), + b"world" + ); + assert_eq!(rc4_decrypt(b"\x01\x02", &[0x0C, 0x74, 0xB9]), b"Hi!"); + assert_eq!(rc4_decrypt(b"secret", &[0x80, 0x45, 0xB5]), b"msg"); + + assert_eq!( + rc4_decrypt( + b"encryption", + &[ + 0x8A, 0x36, 0x3F, 0x85, 0xDB, 0x9A, 0x62, 0x7C, 0x6C, 0x56, 0x81, 0x89 + ] + ), + b"Hello World!" + ); + assert_eq!( + rc4_decrypt( + b"my_secret_key", + &[ + 0x1D, 0xE2, 0xCE, 0x64, 0x4C, 0x88, 0x1C, 0x42, 0x7D, 0x94, 0x7B, 0x1C, 0x49, + 0xCD, 0x62, 0x3F, 0xCA, 0x90, 0x99 + ] + ), + b"The quick brown fox" + ); + assert_eq!( + rc4_decrypt( + b"abcdefghijklmnop", + &[ + 0xF8, 0xA8, 0x2A, 0xAE, 0x92, 0x2F, 0x8C, 0x3B, 0x13, 0xC7, 0x4B, 0x99, 0xDB, + 0x41, 0x2C, 0x9F, 0x92, 0x59, 0xC2, 0xE5, 0x62, 0x2C + ] + ), + b"This is a test message" + ); + assert_eq!( + rc4_decrypt( + b"password123", + &[ + 0xDB, 0xC3, 0xA3, 0x21, 0x7E, 0x55, 0x0B, 0x65, 0xA2, 0x3F, 0x0E, 0xB7, 0x58, + 0x7C, 0x28, 0xEE, 0xD6, 0x92, 0x05, 0xBC, 0x6C + ] + ), + b"Decryption successful" + ); + assert_eq!( + rc4_decrypt( + b"very_long_key", + &[ + 0xC6, 0xD2, 0x00, 0x3B, 0x8C, 0x18, 0x49, 0x2C, 0xD0, 0x5C, 0x8A, 0x34, 0xEC, + 0xFE, 0xED, 0x62, 0xC0, 0x3C, 0x80, 0x81, 0x7E, 0x4E, 0xE1, 0x7D, 0xBD + ] + ), + b"PDF uses RC4 for security" + ); + } +} diff --git a/vendor/hayro-syntax/src/crypto/sha256.rs b/vendor/hayro-syntax/src/crypto/sha256.rs new file mode 100644 index 00000000..968a024b --- /dev/null +++ b/vendor/hayro-syntax/src/crypto/sha256.rs @@ -0,0 +1,151 @@ +//! Ported from . + +use alloc::vec; + +const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]; + +#[inline] +const fn rotr(x: u32, n: u32) -> u32 { + x.rotate_right(n) +} + +#[inline] +const fn ch(x: u32, y: u32, z: u32) -> u32 { + (x & y) ^ (!x & z) +} + +#[inline] +const fn maj(x: u32, y: u32, z: u32) -> u32 { + (x & y) ^ (x & z) ^ (y & z) +} + +#[inline] +const fn sigma_0(x: u32) -> u32 { + rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22) +} + +#[inline] +const fn sigma_1(x: u32) -> u32 { + rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25) +} + +#[inline] +const fn little_sigma_0(x: u32) -> u32 { + rotr(x, 7) ^ rotr(x, 18) ^ (x >> 3) +} + +#[inline] +const fn little_sigma_1(x: u32) -> u32 { + rotr(x, 17) ^ rotr(x, 19) ^ (x >> 10) +} + +pub(crate) fn calculate(data: &[u8]) -> [u8; 32] { + let mut h = [ + 0x6a09e667_u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + + let bit_len = data.len() as u64 * 8; + let padded_len = (data.len() + 9).div_ceil(64) * 64; + let mut padded = vec![0_u8; padded_len]; + + padded[..data.len()].copy_from_slice(data); + padded[data.len()] = 0x80; + + let len_bytes = bit_len.to_be_bytes(); + padded[padded_len - 8..].copy_from_slice(&len_bytes); + + for chunk in padded.chunks_exact(64) { + let mut w = [0_u32; 64]; + + for (i, word_bytes) in chunk.chunks_exact(4).enumerate() { + w[i] = u32::from_be_bytes([word_bytes[0], word_bytes[1], word_bytes[2], word_bytes[3]]); + } + + for i in 16..64 { + w[i] = little_sigma_1(w[i - 2]) + .wrapping_add(w[i - 7]) + .wrapping_add(little_sigma_0(w[i - 15])) + .wrapping_add(w[i - 16]); + } + + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h_var] = h; + + for i in 0..64 { + let temp1 = h_var + .wrapping_add(sigma_1(e)) + .wrapping_add(ch(e, f, g)) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let temp2 = sigma_0(a).wrapping_add(maj(a, b, c)); + + h_var = g; + g = f; + f = e; + e = d.wrapping_add(temp1); + d = c; + c = b; + b = a; + a = temp1.wrapping_add(temp2); + } + + h[0] = h[0].wrapping_add(a); + h[1] = h[1].wrapping_add(b); + h[2] = h[2].wrapping_add(c); + h[3] = h[3].wrapping_add(d); + h[4] = h[4].wrapping_add(e); + h[5] = h[5].wrapping_add(f); + h[6] = h[6].wrapping_add(g); + h[7] = h[7].wrapping_add(h_var); + } + + let mut result = [0_u8; 32]; + for (i, &hash_word) in h.iter().enumerate() { + let bytes = hash_word.to_be_bytes(); + result[i * 4..(i + 1) * 4].copy_from_slice(&bytes); + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use sha2::{Digest, Sha256}; + + #[test] + fn correctness() { + let test_cases = [ + b"" as &[u8], + b"a", + b"abc", + b"Hello, World!", + &[0xde, 0xad, 0xbe, 0xef], + ]; + + for test_case in test_cases { + let our_result = calculate(test_case); + let expected = Sha256::digest(test_case); + assert_eq!( + our_result, + expected.as_slice(), + "Failed for input: {test_case:?}" + ); + } + } +} diff --git a/vendor/hayro-syntax/src/crypto/sha384.rs b/vendor/hayro-syntax/src/crypto/sha384.rs new file mode 100644 index 00000000..d046f460 --- /dev/null +++ b/vendor/hayro-syntax/src/crypto/sha384.rs @@ -0,0 +1,51 @@ +//! Ported from . + +pub(crate) fn calculate(data: &[u8]) -> [u8; 48] { + let mut h = [ + 0xcbbb9d5dc1059ed8_u64, + 0x629a292a367cd507, + 0x9159015a3070dd17, + 0x152fecd8f70e5939, + 0x67332667ffc00b31, + 0x8eb44a8768581511, + 0xdb0c2e0d64f98fa7, + 0x47b5481dbefa4fa4, + ]; + + super::sha512::calculate_with_initial_values(data, &mut h); + + let mut result = [0_u8; 48]; + for (i, &hash_word) in h.iter().take(6).enumerate() { + let bytes = hash_word.to_be_bytes(); + result[i * 8..(i + 1) * 8].copy_from_slice(&bytes); + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use sha2::{Digest, Sha384}; + + #[test] + fn correctness() { + let test_cases = [ + b"" as &[u8], + b"a", + b"abc", + b"Hello, World!", + &[0xde, 0xad, 0xbe, 0xef], + ]; + + for test_case in test_cases { + let our_result = calculate(test_case); + let expected = Sha384::digest(test_case); + assert_eq!( + our_result, + expected.as_slice(), + "Failed for input: {test_case:?}" + ); + } + } +} diff --git a/vendor/hayro-syntax/src/crypto/sha512.rs b/vendor/hayro-syntax/src/crypto/sha512.rs new file mode 100644 index 00000000..b1d3e9ed --- /dev/null +++ b/vendor/hayro-syntax/src/crypto/sha512.rs @@ -0,0 +1,237 @@ +//! Ported from . + +use alloc::vec; + +pub(super) const K: [u64; 80] = [ + 0x428a2f98d728ae22, + 0x7137449123ef65cd, + 0xb5c0fbcfec4d3b2f, + 0xe9b5dba58189dbbc, + 0x3956c25bf348b538, + 0x59f111f1b605d019, + 0x923f82a4af194f9b, + 0xab1c5ed5da6d8118, + 0xd807aa98a3030242, + 0x12835b0145706fbe, + 0x243185be4ee4b28c, + 0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f, + 0x80deb1fe3b1696b1, + 0x9bdc06a725c71235, + 0xc19bf174cf692694, + 0xe49b69c19ef14ad2, + 0xefbe4786384f25e3, + 0x0fc19dc68b8cd5b5, + 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, + 0x4a7484aa6ea6e483, + 0x5cb0a9dcbd41fbd4, + 0x76f988da831153b5, + 0x983e5152ee66dfab, + 0xa831c66d2db43210, + 0xb00327c898fb213f, + 0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2, + 0xd5a79147930aa725, + 0x06ca6351e003826f, + 0x142929670a0e6e70, + 0x27b70a8546d22ffc, + 0x2e1b21385c26c926, + 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, + 0x650a73548baf63de, + 0x766a0abb3c77b2a8, + 0x81c2c92e47edaee6, + 0x92722c851482353b, + 0xa2bfe8a14cf10364, + 0xa81a664bbc423001, + 0xc24b8b70d0f89791, + 0xc76c51a30654be30, + 0xd192e819d6ef5218, + 0xd69906245565a910, + 0xf40e35855771202a, + 0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8, + 0x1e376c085141ab53, + 0x2748774cdf8eeb99, + 0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63, + 0x4ed8aa4ae3418acb, + 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc, + 0x78a5636f43172f60, + 0x84c87814a1f0ab72, + 0x8cc702081a6439ec, + 0x90befffa23631e28, + 0xa4506cebde82bde9, + 0xbef9a3f7b2c67915, + 0xc67178f2e372532b, + 0xca273eceea26619c, + 0xd186b8c721c0c207, + 0xeada7dd6cde0eb1e, + 0xf57d4f7fee6ed178, + 0x06f067aa72176fba, + 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, + 0x1b710b35131c471b, + 0x28db77f523047d84, + 0x32caab7b40c72493, + 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6, + 0x597f299cfc657e2a, + 0x5fcb6fab3ad6faec, + 0x6c44198c4a475817, +]; + +#[inline] +const fn rotr(x: u64, n: u32) -> u64 { + x.rotate_right(n) +} + +#[inline] +pub(super) const fn ch(x: u64, y: u64, z: u64) -> u64 { + (x & y) ^ (!x & z) +} + +#[inline] +pub(super) const fn maj(x: u64, y: u64, z: u64) -> u64 { + (x & y) ^ (x & z) ^ (y & z) +} + +#[inline] +pub(super) const fn sigma_0(x: u64) -> u64 { + rotr(x, 28) ^ rotr(x, 34) ^ rotr(x, 39) +} + +#[inline] +pub(super) const fn sigma_1(x: u64) -> u64 { + rotr(x, 14) ^ rotr(x, 18) ^ rotr(x, 41) +} + +#[inline] +pub(super) const fn little_sigma_0(x: u64) -> u64 { + rotr(x, 1) ^ rotr(x, 8) ^ (x >> 7) +} + +#[inline] +pub(super) const fn little_sigma_1(x: u64) -> u64 { + rotr(x, 19) ^ rotr(x, 61) ^ (x >> 6) +} + +pub(super) fn calculate_with_initial_values(data: &[u8], h: &mut [u64; 8]) { + let bit_len = data.len() as u128 * 8; + let padded_len = (data.len() + 17).div_ceil(128) * 128; // Round up to nearest multiple of 128 + let mut padded = vec![0_u8; padded_len]; + + padded[..data.len()].copy_from_slice(data); + + padded[data.len()] = 0x80; + + let len_bytes = bit_len.to_be_bytes(); + padded[padded_len - 16..].copy_from_slice(&len_bytes); + + for chunk in padded.chunks_exact(128) { + let mut w = [0_u64; 80]; + + for (i, word_bytes) in chunk.chunks_exact(8).enumerate() { + w[i] = u64::from_be_bytes([ + word_bytes[0], + word_bytes[1], + word_bytes[2], + word_bytes[3], + word_bytes[4], + word_bytes[5], + word_bytes[6], + word_bytes[7], + ]); + } + + for i in 16..80 { + w[i] = little_sigma_1(w[i - 2]) + .wrapping_add(w[i - 7]) + .wrapping_add(little_sigma_0(w[i - 15])) + .wrapping_add(w[i - 16]); + } + + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h_var] = *h; + + for i in 0..80 { + let temp1 = h_var + .wrapping_add(sigma_1(e)) + .wrapping_add(ch(e, f, g)) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let temp2 = sigma_0(a).wrapping_add(maj(a, b, c)); + + h_var = g; + g = f; + f = e; + e = d.wrapping_add(temp1); + d = c; + c = b; + b = a; + a = temp1.wrapping_add(temp2); + } + + h[0] = h[0].wrapping_add(a); + h[1] = h[1].wrapping_add(b); + h[2] = h[2].wrapping_add(c); + h[3] = h[3].wrapping_add(d); + h[4] = h[4].wrapping_add(e); + h[5] = h[5].wrapping_add(f); + h[6] = h[6].wrapping_add(g); + h[7] = h[7].wrapping_add(h_var); + } +} + +pub(crate) fn calculate(data: &[u8]) -> [u8; 64] { + let mut h = [ + 0x6a09e667f3bcc908_u64, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, + ]; + + calculate_with_initial_values(data, &mut h); + + let mut result = [0_u8; 64]; + for (i, &hash_word) in h.iter().enumerate() { + let bytes = hash_word.to_be_bytes(); + result[i * 8..(i + 1) * 8].copy_from_slice(&bytes); + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use sha2::{Digest, Sha512}; + + #[test] + fn correctness() { + let test_cases = [ + b"" as &[u8], + b"a", + b"abc", + b"Hello, World!", + &[0xde, 0xad, 0xbe, 0xef], + ]; + + for test_case in test_cases { + let our_result = calculate(test_case); + let expected = Sha512::digest(test_case); + assert_eq!( + our_result, + expected.as_slice(), + "Failed for input: {test_case:?}" + ); + } + } +} diff --git a/vendor/hayro-syntax/src/data.rs b/vendor/hayro-syntax/src/data.rs new file mode 100644 index 00000000..9d5e2030 --- /dev/null +++ b/vendor/hayro-syntax/src/data.rs @@ -0,0 +1,264 @@ +use crate::object::ObjectIdentifier; +use crate::object::Stream; +use crate::reader::ReaderContext; +use crate::sync::HashMap; +use crate::sync::{Arc, Mutex, MutexExt}; +use crate::util::SegmentList; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter}; + +/// A container for the bytes of a PDF file. +#[derive(Clone)] +pub struct PdfData { + #[cfg(feature = "std")] + inner: Arc + Send + Sync>, + #[cfg(not(feature = "std"))] + inner: Arc>, +} + +impl Debug for PdfData { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "PdfData {{ ... }}") + } +} + +impl AsRef<[u8]> for PdfData { + fn as_ref(&self) -> &[u8] { + (*self.inner).as_ref() + } +} + +#[cfg(feature = "std")] +impl + Send + Sync + 'static> From> for PdfData { + fn from(data: Arc) -> Self { + Self { inner: data } + } +} + +#[cfg(not(feature = "std"))] +impl + 'static> From> for PdfData { + fn from(data: Arc) -> Self { + Self { inner: data } + } +} + +impl From> for PdfData { + fn from(data: Vec) -> Self { + Self { + inner: Arc::new(data), + } + } +} + +/// A structure for storing the data of the PDF. +// To explain further: This crate uses a zero-parse approach, meaning that objects like +// dictionaries or arrays always store the underlying data and parse objects lazily as needed, +// instead of allocating the data and storing it in an owned way. However, the problem is that +// not all data is readily available in the original data of the PDF: Objects can also be +// stored in an object streams, in which case we first need to decode the stream before we can +// access the data. +// +// The purpose of `Data` is to allow us to access the original data as well as maybe decoded data +// by faking the same lifetime, so that we don't run into lifetime issues when dealing with +// PDF objects that actually stem from different data sources. +pub(crate) struct Data { + data: PdfData, + // 32 segments are more than enough as we can't have more objects than this. + decoded: SegmentList>, 32>, + map: Mutex>, + loading: LoadGate, +} + +impl Debug for Data { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "Data {{ ... }}") + } +} + +impl Data { + /// Create a new `Data` structure. + pub(crate) fn new(data: PdfData) -> Self { + Self { + data, + decoded: SegmentList::new(), + map: Mutex::new(HashMap::new()), + loading: LoadGate::default(), + } + } + + /// Get access to the original data of the PDF. + pub(crate) fn get(&self) -> &PdfData { + &self.data + } + + /// Get access to the data of a decoded object stream. + pub(crate) fn get_with(&self, id: ObjectIdentifier, ctx: &ReaderContext<'_>) -> Option<&[u8]> { + // PdfCraft patch: (#307, see vendor/README.md hayro-syntax (10)) register exactly once: + // racing misses must not allocate different slots + // for the same id (or reuse the resulting hole for another stream). + let idx = { + let mut map = self.map.get(); + let next = map.len(); + *map.entry(id).or_insert(next) + }; + if let Some(decoded) = self.decoded.get(idx) { + return decoded.as_deref(); + } + // Only cold initialization is serialized. Nested streams may depend on + // one another; the gate rejects cycles without blocking on our own cell. + let _loading = self.loading.enter(id)?; + self.decoded + .get_or_init(idx, || { + let stream = ctx.xref().get_with::>(id, ctx)?; + stream.decoded().ok().map(Cow::into_owned) + })? + .as_deref() + } +} + +/// PdfCraft patch: (#307) one cold initializer at a time prevents cross-thread dependency deadlocks. +/// The owner may recursively load different streams, up to a fixed depth. No +/// state mutex is held while parsing or decoding input. no_std is single-threaded +/// (its Arc/locks are Rc/RefCell), so it needs the same cycle/depth checks only. +#[derive(Default)] +struct LoadGate { + state: Mutex, + #[cfg(feature = "std")] + ready: std::sync::Condvar, +} + +#[derive(Default)] +struct LoadState { + active: Vec, + #[cfg(feature = "std")] + owner: Option, + failed: bool, +} + +impl LoadGate { + fn enter(&self, id: ObjectIdentifier) -> Option> { + let mut state = self.state.get(); + #[cfg(feature = "std")] + while !state.failed + && state + .owner + .is_some_and(|owner| owner != std::thread::current().id()) + { + state = self.ready.wait(state).unwrap_or_else(|p| p.into_inner()); + } + if state.failed || state.active.len() >= 64 || state.active.contains(&id) { + return None; + } + #[cfg(feature = "std")] + { + state.owner = Some(std::thread::current().id()); + } + state.active.push(id); + Some(LoadGuard(self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn recursive_loads_reject_cycles_and_limit_depth() { + let gate = LoadGate::default(); + let mut guards = Vec::new(); + for n in 0..64 { + guards.push( + gate.enter(ObjectIdentifier::new(n, 0)) + .expect("nested distinct stream"), + ); + } + assert!(gate.enter(ObjectIdentifier::new(64, 0)).is_none()); + assert!(gate.enter(ObjectIdentifier::new(0, 0)).is_none()); + while guards.pop().is_some() {} + assert!(gate.enter(ObjectIdentifier::new(0, 0)).is_some()); + } + + #[cfg(feature = "std")] + #[test] + fn competing_dependency_chains_cannot_wait_on_each_other() { + let gate = Arc::new(LoadGate::default()); + let barrier = Arc::new(std::sync::Barrier::new(16)); + let jobs: Vec<_> = (0..16) + .map(|n| { + let gate = gate.clone(); + let barrier = barrier.clone(); + std::thread::spawn(move || { + barrier.wait(); + let (a, b) = ( + ObjectIdentifier::new(n % 2, 0), + ObjectIdentifier::new(1 - n % 2, 0), + ); + let _a = gate.enter(a).unwrap(); + let _b = gate.enter(b).unwrap(); + assert!(gate.enter(a).is_none()); + }) + }) + .collect(); + for job in jobs { + job.join().unwrap(); + } + } + + #[cfg(feature = "std")] + #[test] + fn panicking_initializer_wakes_waiters_and_prevents_retries() { + use std::{ + panic::{AssertUnwindSafe, catch_unwind}, + sync::mpsc, + time::Duration, + }; + let gate = Arc::new(LoadGate::default()); + let (ready_tx, ready_rx) = mpsc::channel(); + let (done_tx, done_rx) = mpsc::channel(); + let mut jobs = Vec::new(); + let result = catch_unwind(AssertUnwindSafe(|| { + let _guard = gate.enter(ObjectIdentifier::new(1, 0)).unwrap(); + for _ in 0..8 { + let (gate, ready, done) = (gate.clone(), ready_tx.clone(), done_tx.clone()); + jobs.push(std::thread::spawn(move || { + ready.send(()).unwrap(); + done.send(gate.enter(ObjectIdentifier::new(2, 0)).is_none()) + .unwrap(); + })); + } + for _ in 0..8 { + ready_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + } + panic!("injected decode failure"); + })); + assert!(result.is_err()); + for _ in 0..8 { + assert!(done_rx.recv_timeout(Duration::from_secs(2)).unwrap()); + } + for job in jobs { + job.join().unwrap(); + } + assert!(gate.enter(ObjectIdentifier::new(1, 0)).is_none()); + assert!(gate.enter(ObjectIdentifier::new(3, 0)).is_none()); + } +} + +struct LoadGuard<'a>(&'a LoadGate); + +impl Drop for LoadGuard<'_> { + fn drop(&mut self) { + let mut state = self.0.state.get(); + state.active.pop(); + #[cfg(feature = "std")] + { + // OnceLock itself permits a retry after a panic. The parser does not: + // wake all waiters and fail closed until the caller replaces the Pdf. + state.failed |= std::thread::panicking(); + if state.active.is_empty() { + state.owner = None; + } + self.0.ready.notify_all(); + } + } +} diff --git a/vendor/hayro-syntax/src/decode_limits_tests.rs b/vendor/hayro-syntax/src/decode_limits_tests.rs new file mode 100644 index 00000000..4f3af09e --- /dev/null +++ b/vendor/hayro-syntax/src/decode_limits_tests.rs @@ -0,0 +1,195 @@ +//! Small synthetic cases for document-local ordinary stream expansion limits. +use super::*; +use crate::Pdf; +use crate::filter::MAX_DECODED_STREAM; +use crate::object::stream::ImageDecodeParams; +use alloc::collections::BTreeMap; +use alloc::format; + +fn run_length(data: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut at = 0; + while at < data.len() { + let mut run = 1; + while run < 128 && at + run < data.len() && data[at + run] == data[at] { + run += 1; + } + if run >= 3 { + out.extend([u8::try_from(257 - run).unwrap(), data[at]]); + at += run; + } else { + let start = at; + at += run; + while at < data.len() && at - start < 128 { + if at + 2 < data.len() && data[at] == data[at + 1] && data[at] == data[at + 2] { + break; + } + at += 1; + } + out.push(u8::try_from(at - start - 1).unwrap()); + out.extend_from_slice(&data[start..at]); + } + } + out.push(128); + out +} + +fn stream(data: &[u8], extra: &str) -> Vec { + let encoded = run_length(data); + let mut out = format!("<< /Length {} /Filter /RunLengthDecode {extra} >>\nstream\n", encoded.len()).into_bytes(); + out.extend_from_slice(&encoded); + out.extend_from_slice(b"\nendstream"); + out +} + +fn object(out: &mut Vec, offsets: &mut BTreeMap, n: u32, bytes: &[u8]) { + offsets.insert(n, out.len()); + out.extend_from_slice(format!("{n} 0 obj\n").as_bytes()); + out.extend_from_slice(bytes); + out.extend_from_slice(b"\nendobj\n"); +} + +fn base() -> (Vec, BTreeMap) { + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offsets = BTreeMap::new(); + object(&mut out, &mut offsets, 1, b"<< /Type /Catalog /Pages 2 0 R >>"); + object(&mut out, &mut offsets, 2, b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>"); + object(&mut out, &mut offsets, 3, b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>"); + (out, offsets) +} + +fn table(out: &mut Vec, offsets: &BTreeMap, extra: &str) -> usize { + let start = out.len(); + let size = offsets.keys().max().copied().unwrap_or(0) + 1; + out.extend_from_slice(format!("xref\n0 {size}\n").as_bytes()); + for n in 0..size { + match offsets.get(&n) { + Some(at) => out.extend_from_slice(format!("{at:010} 00000 n \n").as_bytes()), + None => out.extend_from_slice(b"0000000000 65535 f \n"), + } + } + out.extend_from_slice(format!("trailer\n<< /Root 1 0 R /Size {size} {extra} >>\nstartxref\n{start}\n%%EOF\n").as_bytes()); + start +} + +fn ordinary_stream_pdf() -> Vec { + let (mut out, mut offsets) = base(); + object(&mut out, &mut offsets, 4, &stream(&vec![b'A'; 512], "")); + table(&mut out, &offsets, ""); + out +} + +fn object_stream_pdf(repair: bool) -> (Vec, usize, BTreeMap) { + let (mut out, mut offsets) = base(); + let header = b"40 0 41 1024 "; + let mut payload = header.to_vec(); + payload.extend_from_slice(b"<< /Marker 123 >>"); + payload.resize(header.len() + 1024, b' '); + payload.extend_from_slice(b"<< /Marker 456 >>"); + object(&mut out, &mut offsets, 10, &stream(&payload, &format!("/Type /ObjStm /N 2 /First {}", header.len()))); + if repair { + // An invalid normal reference forces lazy repair to rediscover object 41 in its stream. + offsets.insert(41, 0); + let start = table(&mut out, &offsets, ""); + return (out, start, offsets); + } + let start = out.len(); + offsets.insert(11, start); + let mut rows = Vec::new(); + for n in 0..42 { + let (kind, a, b): (u8, u32, u16) = match n { + 40 => (2, 10, 0), + 41 => (2, 10, 1), + n => offsets.get(&n).map_or((0, 0, 65535), |at| (1, u32::try_from(*at).unwrap(), 0)), + }; + rows.push(kind); + rows.extend_from_slice(&a.to_be_bytes()); + rows.extend_from_slice(&b.to_be_bytes()); + } + object(&mut out, &mut offsets, 11, &stream(&rows, "/Type /XRef /Size 42 /Root 1 0 R /W [1 4 2]")); + out.extend_from_slice(format!("startxref\n{start}\n%%EOF\n").as_bytes()); + (out, start, offsets) +} + +fn marker(pdf: &Pdf, n: i32) -> Option { + pdf.xref().get::>(ObjectIdentifier::new(n, 0))?.get::(b"Marker") +} + +#[test] +fn decode_limits_default_and_lowering_are_bounded() { + assert_eq!(DecodeLimits::default().max_decoded_stream(), MAX_DECODED_STREAM); + let limits = DecodeLimits::default().with_max_decoded_stream(32); + assert_eq!(limits.with_max_decoded_stream(usize::MAX), limits); + assert_eq!(limits.with_max_decoded_stream(0).max_decoded_stream(), 0); +} + +#[test] +fn ordinary_stream_expansion_is_document_local_and_keeps_caller_budget() { + let bytes = ordinary_stream_pdf(); + let low = Pdf::new_with_limits(bytes.clone(), DecodeLimits::default().with_max_decoded_stream(32)).unwrap(); + let normal = Pdf::new(bytes).unwrap(); + let low_stream = low.xref().get::>(ObjectIdentifier::new(4, 0)).unwrap(); + let normal_stream = normal.xref().get::>(ObjectIdentifier::new(4, 0)).unwrap(); + assert!(low_stream.raw_data().len() < 32, "the compressed input itself fits"); + assert_eq!(low_stream.decoded().unwrap().as_ref(), &[b'A'; 32]); + assert_eq!(normal_stream.decoded().unwrap().as_ref(), &[b'A'; 512]); + assert_eq!(low_stream.decoded_within(7).unwrap().len(), 7); + assert_eq!(low_stream.decoded_raw_within(low_stream.raw_data(), 7).unwrap().len(), 7); + // A second document or a clone of its xref cannot mutate the first document's limit. + assert_eq!(low.decode_limits().max_decoded_stream(), 32); + assert_eq!(normal.decode_limits(), DecodeLimits::default()); + assert_eq!(low.xref().clone().decode_limits(), low.decode_limits()); +} + +#[test] +fn final_known_size_images_keep_their_distinct_pixel_budget() { + let limits = DecodeLimits::default().with_max_decoded_stream(32); + let params = ImageDecodeParams { width: 64, height: 1, bpc: Some(8), num_components: Some(1), ..Default::default() }; + assert_eq!(crate::filter::decode_limit(¶ms, false, limits), 32); + assert_eq!(crate::filter::decode_limit(¶ms, true, limits), 128); + assert_eq!(crate::filter::decode_limit(&ImageDecodeParams::default(), true, limits), 32); +} + +#[test] +fn object_stream_limit_applies_after_successful_root_xref() { + let (bytes, _, _) = object_stream_pdf(false); + // 512 permits the full 294-byte xref stream but cuts the 1052-byte object stream. + let limits = DecodeLimits::default().with_max_decoded_stream(512); + assert!(root_xref(bytes.clone().into(), b"", limits).is_ok()); + let low = Pdf::new_with_limits(bytes.clone(), limits).unwrap(); + let normal = Pdf::new(bytes).unwrap(); + assert_eq!(marker(&normal, 41), Some(456)); + assert_eq!(marker(&low, 40), Some(123)); + assert_eq!(marker(&low, 41), None); +} + +#[test] +fn root_prev_and_hybrid_xref_streams_use_the_limit_before_xref_exists() { + let (bytes, stream_at, offsets) = object_stream_pdf(false); + let limits = DecodeLimits::default().with_max_decoded_stream(32); + for via in ["root", "Prev", "XRefStm"] { + let mut input = bytes.clone(); + if via != "root" { + table(&mut input, &offsets, &format!("/{via} {stream_at}")); + } + assert!(root_xref(input.clone().into(), b"", DecodeLimits::default()).is_ok(), "{via}"); + assert!(root_xref(input.clone().into(), b"", limits).is_err(), "{via}: root decode escaped limit"); + // Best-effort open can rebuild ordinary offsets, but must retain the same limit. + let repaired = Pdf::new_with_limits(input, limits).unwrap(); + assert_eq!(repaired.decode_limits(), limits); + assert_eq!(marker(&repaired, 41), None, "{via}: fallback escaped limit"); + } +} + +#[test] +fn lazy_xref_repair_keeps_the_document_limit() { + let (bytes, _, _) = object_stream_pdf(true); + let limits = DecodeLimits::default().with_max_decoded_stream(512); + let low = Pdf::new_with_limits(bytes.clone(), limits).unwrap(); + let normal = Pdf::new(bytes).unwrap(); + assert_eq!(marker(&normal, 41), Some(456)); + assert_eq!(marker(&low, 41), None); + low.xref().repair(); + assert_eq!(marker(&low, 41), None); + assert_eq!(low.decode_limits(), limits); +} diff --git a/vendor/hayro-syntax/src/filter/ascii_85.rs b/vendor/hayro-syntax/src/filter/ascii_85.rs new file mode 100644 index 00000000..5a18cf83 --- /dev/null +++ b/vendor/hayro-syntax/src/filter/ascii_85.rs @@ -0,0 +1,91 @@ +// Keep in sync with `hayro-postscript/src/string/ascii_85.rs`. + +use crate::reader::Reader; +use crate::trivia::is_white_space_character; +use alloc::vec::Vec; + +pub(crate) fn decode(data: &[u8]) -> Option> { + const POW_85: [u32; 5] = [52200625, 614125, 7225, 85, 1]; + + let mut reader = Reader::new(data); + + let mut read_byte = || -> Option { + loop { + let b = reader.read_byte()?; + + // White space characters should be ignored. + if !is_white_space_character(b) { + return Some(b); + } + } + }; + + let flush_group = |group: &mut Vec, decoded: &mut Vec| -> Option<()> { + let (digits, output_len): ([u32; 5], usize) = match group.len() { + 0 => return Some(()), + 1 => return None, // A single character is not valid. + 2 => ( + [group[0], group[1], b'u', b'u', b'u'].map(|b| (b - b'!') as u32), + 1, + ), + 3 => ( + [group[0], group[1], group[2], b'u', b'u'].map(|b| (b - b'!') as u32), + 2, + ), + 4 => ( + [group[0], group[1], group[2], group[3], b'u'].map(|b| (b - b'!') as u32), + 3, + ), + 5 => ( + [group[0], group[1], group[2], group[3], group[4]].map(|b| (b - b'!') as u32), + 4, + ), + _ => unreachable!(), + }; + + let value = digits[0] + .checked_mul(POW_85[0])? + .checked_add(digits[1].checked_mul(POW_85[1])?)? + .checked_add(digits[2].checked_mul(POW_85[2])?)? + .checked_add(digits[3].checked_mul(POW_85[3])?)? + .checked_add(digits[4])?; + + decoded.extend_from_slice(&value.to_be_bytes()[..output_len]); + group.clear(); + Some(()) + }; + + let mut decoded = Vec::with_capacity(data.len() * 4 / 5); + let mut group = Vec::with_capacity(5); + + loop { + let Some(b) = read_byte() else { + // Be lenient and accept what we have (see PDFBOX-5910). + flush_group(&mut group, &mut decoded)?; + + return Some(decoded); + }; + + match b { + b'!'..=b'u' => { + group.push(b); + + if group.len() == 5 { + flush_group(&mut group, &mut decoded)?; + } + } + b'z' => { + flush_group(&mut group, &mut decoded)?; + decoded.extend_from_slice(&[0, 0, 0, 0]); + } + b'~' => { + // Technically requires a '>', but there is a PDF where it isn't + // appended and decodes fine in other viewers. + flush_group(&mut group, &mut decoded)?; + + return Some(decoded); + } + _ => return None, // Invalid character. + } + } +} diff --git a/vendor/hayro-syntax/src/filter/ascii_hex.rs b/vendor/hayro-syntax/src/filter/ascii_hex.rs new file mode 100644 index 00000000..edcd82d5 --- /dev/null +++ b/vendor/hayro-syntax/src/filter/ascii_hex.rs @@ -0,0 +1,108 @@ +// Keep in sync with `hayro-postscript/src/string/ascii_hex.rs`. + +use crate::trivia::is_white_space_character; +use crate::util::ByteBuf; +use alloc::vec::Vec; + +pub(crate) fn decode(data: &[u8]) -> Option> { + decode_into(data) +} + +pub(crate) fn decode_into(data: &[u8]) -> Option { + // Find end (at `>` or end of data) and check for whitespace. + let mut has_whitespace = false; + let mut end = data.len(); + + for (idx, byte) in data.iter().enumerate() { + has_whitespace |= is_white_space_character(*byte); + + if *byte == b'>' { + end = idx; + break; + } + } + + let data = &data[..end]; + let mut decoded = B::with_capacity(data.len().div_ceil(2)); + + if !has_whitespace { + // Fast path, don't need to worry about white spaces. + let (chunks, remainder) = data.as_chunks::<2>(); + + for &[hi, lo] in chunks { + decoded.push(decode_hex_digit(hi)? << 4 | decode_hex_digit(lo)?); + } + + if let [hi] = remainder { + decoded.push(decode_hex_digit(*hi)? << 4); + } + } else { + // Slow path, need to strip white spaces. + let mut iter = data.iter().copied(); + let mut read_byte = || -> Option { + loop { + let b = iter.next()?; + if !is_white_space_character(b) { + return Some(b); + } + } + }; + + loop { + match (read_byte(), read_byte()) { + (Some(hi), Some(lo)) => { + decoded.push(decode_hex_digit(hi)? << 4 | decode_hex_digit(lo)?); + } + (Some(hi), None) => { + decoded.push(decode_hex_digit(hi)? << 4); + + break; + } + (None, _) => break, + } + } + } + + Some(decoded) +} + +#[inline(always)] +pub(crate) fn decode_hex_digit(c: u8) -> Option { + match c { + b'0'..=b'9' => Some(c - b'0'), + b'A'..=b'F' => Some(c - b'A' + 10), + b'a'..=b'f' => Some(c - b'a' + 10), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use crate::filter::ascii_hex::decode; + + #[test] + fn decode_simple() { + let input = b"AF3E2901>"; + assert_eq!(decode(input).unwrap(), vec![0xaf, 0x3e, 0x29, 0x01]); + } + + #[test] + fn decode_whitespaces() { + let input = b"AF3 E2 901>"; + assert_eq!(decode(input).unwrap(), vec![0xaf, 0x3e, 0x29, 0x01]); + } + + #[test] + // Not valid for ASCII hex streams since they require a trailing >, + // but used by PDF hex strings as well. + fn decode_without_gt() { + let input = b"AF3E2901"; + assert_eq!(decode(input).unwrap(), vec![0xaf, 0x3e, 0x29, 0x01]); + } + + #[test] + fn decode_with_padding() { + let input = b"AF3E291>"; + assert_eq!(decode(input).unwrap(), vec![0xaf, 0x3e, 0x29, 0x10]); + } +} diff --git a/vendor/hayro-syntax/src/filter/ccitt.rs b/vendor/hayro-syntax/src/filter/ccitt.rs new file mode 100644 index 00000000..98924f2d --- /dev/null +++ b/vendor/hayro-syntax/src/filter/ccitt.rs @@ -0,0 +1,156 @@ +use crate::object::Dict; +use crate::object::dict::keys::{ + BLACK_IS_1, COLUMNS, ENCODED_BYTE_ALIGN, END_OF_BLOCK, END_OF_LINE, K, ROWS, +}; +use crate::object::stream::{FilterResult, ImageColorSpace, ImageData, ImageDecodeParams}; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use core::iter; +use hayro_ccitt::{DecodeSettings, Decoder, DecoderContext, EncodingMode}; + +pub(crate) fn decode( + data: &[u8], + params: &Dict<'_>, + image_params: &ImageDecodeParams, +) -> Option> { + let k = params.get::(K).unwrap_or(0); + + let rows = params.get::(ROWS).unwrap_or(image_params.height); + let end_of_block = params.get::(END_OF_BLOCK).unwrap_or(true); + + let settings = DecodeSettings { + columns: params.get::(COLUMNS).unwrap_or(1728) as u32, + rows, + end_of_block, + end_of_line: params.get::(END_OF_LINE).unwrap_or(false), + rows_are_byte_aligned: params.get::(ENCODED_BYTE_ALIGN).unwrap_or(false), + encoding: if k < 0 { + EncodingMode::Group4 + } else if k == 0 { + EncodingMode::Group3_1D + } else { + EncodingMode::Group3_2D { k: k as u32 } + }, + invert_black: params.get::(BLACK_IS_1).unwrap_or(false), + }; + + // PdfCraft patch: refuse absurd sizes before the decoder allocates for them. + if !super::ccitt_size_ok(settings.columns, settings.rows) { + return None; + } + + // Whenever possible (if we don't have an indexed color space), we convert + // the data as 8-bit instead of 1-bit, so that it can be easier converted + // into an RGBA8 image. + + let (decoded, bpc) = if image_params.is_indexed { + struct BitPackDecoder { + output: Vec, + decoded_rows: u32, + buffer: u8, + bit_count: u8, + } + + impl BitPackDecoder { + fn push_bit(&mut self, white: bool) { + let bit = if white { 1 } else { 0 }; + self.buffer = (self.buffer << 1) | bit; + self.bit_count += 1; + + if self.bit_count == 8 { + self.output.push(self.buffer); + self.buffer = 0; + self.bit_count = 0; + } + } + + fn flush(&mut self) { + if self.bit_count > 0 { + let padded = self.buffer << (8 - self.bit_count); + self.output.push(padded); + self.buffer = 0; + self.bit_count = 0; + } + } + } + + impl Decoder for BitPackDecoder { + fn push_pixel(&mut self, white: bool) { + self.push_bit(white); + } + + fn push_pixel_chunk(&mut self, white: bool, chunk_count: u32) { + let byte = if white { 0xFF } else { 0x00 }; + self.output + .extend(iter::repeat_n(byte, chunk_count as usize)); + } + + fn next_line(&mut self) { + self.decoded_rows += 1; + self.flush(); + } + } + + let mut decoder = BitPackDecoder { + output: Vec::new(), + decoded_rows: 0, + buffer: 0, + bit_count: 0, + }; + let mut context = DecoderContext::new(settings); + let result = hayro_ccitt::decode(data, &mut decoder, &mut context); + + // If we decoded at least one row, let's be lenient and return what we got. + // See also 0001763.pdf. + if result.is_err() && decoder.decoded_rows == 0 { + return None; + } + + (decoder.output, 1) + } else { + struct Luma8Decoder { + output: Vec, + decoded_rows: u32, + } + + impl Decoder for Luma8Decoder { + fn push_pixel(&mut self, white: bool) { + self.output.push(if white { 0xFF } else { 0x00 }); + } + + fn push_pixel_chunk(&mut self, white: bool, chunk_count: u32) { + let byte = if white { 0xFF } else { 0x00 }; + self.output + .extend(iter::repeat_n(byte, chunk_count as usize * 8)); + } + + fn next_line(&mut self) { + self.decoded_rows += 1; + } + } + + let mut decoder = Luma8Decoder { + output: Vec::new(), + decoded_rows: 0, + }; + let mut context = DecoderContext::new(settings); + let result = hayro_ccitt::decode(data, &mut decoder, &mut context); + + if result.is_err() && decoder.decoded_rows == 0 { + return None; + } + + (decoder.output, 8) + }; + + Some(FilterResult { + data: Cow::Owned(decoded), + image_data: Some(ImageData { + alpha: None, + color_space: Some(ImageColorSpace::Gray), + bits_per_component: bpc, + width: settings.columns, + height: image_params.height, + }), + }) +} diff --git a/vendor/hayro-syntax/src/filter/dct.rs b/vendor/hayro-syntax/src/filter/dct.rs new file mode 100644 index 00000000..a4fbd176 --- /dev/null +++ b/vendor/hayro-syntax/src/filter/dct.rs @@ -0,0 +1,181 @@ +use crate::object::Dict; +use crate::object::dict::keys::COLOR_TRANSFORM; +use crate::object::stream::{FilterResult, ImageColorSpace, ImageData, ImageDecodeParams}; +use alloc::borrow::Cow; +use core::num::NonZeroU32; +use zune_jpeg::zune_core::bytestream::ZCursor; +use zune_jpeg::zune_core::colorspace::ColorSpace; +use zune_jpeg::zune_core::colorspace::ColorSpace::CMYK; +use zune_jpeg::zune_core::options::DecoderOptions; + +pub(crate) fn decode( + data: &[u8], + params: &Dict<'_>, + image_params: &ImageDecodeParams, +) -> Option> { + if image_params.width > u16::MAX as u32 || image_params.height > u16::MAX as u32 { + return None; + } + + // Some PDFs have weird JPEGs where the JPEG metadata is completely wrong + // (for example indicating that one of the dimensions is u16::MAX), but the + // metadata in the PDF image dictionary is correct. Therefore, we first + // validate the JPEG metadata and patch the data if any of the dimensions + // are too large (if they are too small, they will just be padded later on). + let data = maybe_patch_jpeg_dimensions(data, image_params)?; + + let options = DecoderOptions::default() + .set_max_width(u16::MAX as usize) + .set_max_height(u16::MAX as usize); + let mut decoder = zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(&*data), options); + decoder.decode_headers().ok()?; + + let color_transform = params.get::(COLOR_TRANSFORM); + let input_color_space = decoder.input_colorspace().unwrap(); + + let mut out_colorspace = if let Some(num_components) = image_params.num_components + && !matches!(num_components, 1 | 3 | 4) + { + ColorSpace::MultiBand(NonZeroU32::new(num_components as u32)?) + } else { + match input_color_space { + ColorSpace::YCbCr => { + if color_transform.is_none_or(|c| c == 1) { + ColorSpace::RGB + } else { + ColorSpace::YCbCr + } + } + ColorSpace::RGB | ColorSpace::RGBA => ColorSpace::RGB, + ColorSpace::Luma | ColorSpace::LumaA => ColorSpace::Luma, + // TODO: Find test case with color transform on cmyk + CMYK => CMYK, + ColorSpace::YCCK => ColorSpace::YCCK, + _ => ColorSpace::RGB, + } + }; + + // In case image had APP14 marker, we might have to override the colorspace. + if input_color_space == CMYK && decoder.info().unwrap().components == 3 { + out_colorspace = ColorSpace::RGB; + } + + decoder.set_options(DecoderOptions::default().jpeg_set_out_colorspace(out_colorspace)); + let mut decoded = decoder.decode().ok()?; + + if out_colorspace == ColorSpace::YCCK { + // See + for c in decoded.chunks_mut(4) { + let y = c[0] as f32; + let cb = c[1] as f32; + let cr = c[2] as f32; + c[0] = (434.456 - y - 1.402 * cr) as u8; + c[1] = (119.541 - y + 0.344 * cb + 0.714 * cr) as u8; + c[2] = (481.816 - y - 1.772 * cb) as u8; + } + } + + let width = decoder.dimensions().unwrap().0 as u32; + let height = decoder.dimensions().unwrap().1 as u32; + + let image_data = ImageData { + alpha: None, + color_space: match out_colorspace { + ColorSpace::RGB | ColorSpace::YCbCr => Some(ImageColorSpace::Rgb), + ColorSpace::Luma => Some(ImageColorSpace::Gray), + ColorSpace::YCCK | CMYK => Some(ImageColorSpace::Cmyk), + ColorSpace::MultiBand(_) => None, + _ => None, + }, + bits_per_component: 8, + width, + height, + }; + + Some(FilterResult { + data: Cow::Owned(decoded), + image_data: Some(image_data), + }) +} + +fn maybe_patch_jpeg_dimensions<'a>( + data: &'a [u8], + image_params: &ImageDecodeParams, +) -> Option> { + let sof_offset = find_sof_marker(data)?; + + let height_offset = sof_offset.checked_add(5)?; + let width_offset = sof_offset.checked_add(7)?; + + let jpeg_height = u16::from_be_bytes([ + *data.get(height_offset)?, + *data.get(height_offset.checked_add(1)?)?, + ]); + let jpeg_width = u16::from_be_bytes([ + *data.get(width_offset)?, + *data.get(width_offset.checked_add(1)?)?, + ]); + + let jpeg_area = (jpeg_width as usize).checked_mul(jpeg_height as usize)?; + let image_area = (image_params.width as usize).checked_mul(image_params.height as usize)?; + let need_patch = jpeg_area > image_area; + + if !need_patch { + return Some(Cow::Borrowed(data)); + } + + let target_w = (image_params.width as u16).to_be_bytes(); + let target_h = (image_params.height as u16).to_be_bytes(); + + let mut patched = data.to_vec(); + patched[height_offset..height_offset.checked_add(2)?].copy_from_slice(&target_h); + patched[width_offset..width_offset.checked_add(2)?].copy_from_slice(&target_w); + + Some(Cow::Owned(patched)) +} + +fn find_sof_marker(data: &[u8]) -> Option { + let mut i = 0_usize; + + while i.checked_add(1).is_some_and(|next| next < data.len()) { + if data[i] != 0xFF { + i += 1; + continue; + } + + let marker = data[i + 1]; + + // Note: Not sure if 100% correct/robust, is AI-generated. + match marker { + // All SOF markers carry dimensions: SOF0โ€“SOF15, excluding + // 0xC4 (DHT), 0xC8 (JPG), 0xCC (DAC) which are not frame markers. + 0xC0..=0xCF if marker != 0xC4 && marker != 0xC8 && marker != 0xCC => { + return Some(i); + } + // Skip padding bytes (0xFF followed by 0xFF). + 0xFF => { + i += 1; + + continue; + } + // SOI (0xD8), EOI (0xD9), TEM (0x01) and stuffed byte (0x00) + // are standalone markers with no payload. + 0xD8 | 0xD9 | 0x01 | 0x00 => { + i += 2; + + continue; + } + // All other markers have a 2-byte length field โ€” skip over them. + _ => { + let len_start = i.checked_add(2)?; + let len_end = i.checked_add(3)?; + let seg_len = + u16::from_be_bytes([*data.get(len_start)?, *data.get(len_end)?]) as usize; + + i = i.checked_add(2)?.checked_add(seg_len)?; + } + } + } + + None +} diff --git a/vendor/hayro-syntax/src/filter/jbig2.rs b/vendor/hayro-syntax/src/filter/jbig2.rs new file mode 100644 index 00000000..d87253f5 --- /dev/null +++ b/vendor/hayro-syntax/src/filter/jbig2.rs @@ -0,0 +1,96 @@ +use crate::bit_reader::BitWriter; +use crate::object::Dict; +use crate::object::Stream; +use crate::object::dict::keys::JBIG2_GLOBALS; +use crate::object::stream::{FilterResult, ImageColorSpace, ImageData, ImageDecodeParams}; +use alloc::borrow::Cow; +use alloc::vec; +use alloc::vec::Vec; +use core::iter; + +/// Decode JBIG2 data from a PDF stream. +/// +/// The `params` dictionary may contain a `JBIG2Globals` entry pointing to +/// a stream with shared symbol dictionaries. +pub(crate) fn decode( + data: &[u8], + params: &Dict<'_>, + image_params: &ImageDecodeParams, +) -> Option> { + let globals = params + .get::>(JBIG2_GLOBALS) + .and_then(|g| g.decoded().ok()); + + let image = hayro_jbig2::Image::new_embedded(data, globals.as_deref()).ok()?; + + // Whenever possible (if we don't have an indexed color space), we convert + // the data as 8-bit instead of 1-bit, so that it can be easier converted + // into an RGBA8 image. + + // We need to invert the color because JBIG2 uses black = 1 and + // white = 0, but PDF uses the opposite. + let (decoded, bpc) = if image_params.is_indexed { + let row_bytes = (image.width() as usize).div_ceil(8); + let mut packed = vec![0_u8; row_bytes * image.height() as usize]; + + struct BitWriterDecoder<'a> { + writer: BitWriter<'a>, + } + + impl hayro_jbig2::Decoder for BitWriterDecoder<'_> { + fn push_pixel(&mut self, black: bool) { + let _ = self.writer.write(u32::from(!black)); + } + + fn push_pixel_chunk(&mut self, black: bool, chunk_count: u32) { + let byte_value = if black { 0x00 } else { 0xFF }; + let _ = self.writer.fill_bytes(byte_value, chunk_count as usize); + } + + fn next_line(&mut self) { + // Images need to be padded to the byte boundary after each row. + self.writer.align(); + } + } + + let writer = BitWriter::new(&mut packed, 1)?; + let mut decoder = BitWriterDecoder { writer }; + image.decode(&mut decoder).ok()?; + + (packed, 1) + } else { + struct Luma8Decoder { + output: Vec, + } + + impl hayro_jbig2::Decoder for Luma8Decoder { + fn push_pixel(&mut self, black: bool) { + self.output.push(if black { 0x00 } else { 0xFF }); + } + + fn push_pixel_chunk(&mut self, black: bool, chunk_count: u32) { + let byte = if black { 0x00 } else { 0xFF }; + self.output + .extend(iter::repeat_n(byte, chunk_count as usize * 8)); + } + + fn next_line(&mut self) {} + } + + let mut decoder = Luma8Decoder { output: Vec::new() }; + image.decode(&mut decoder).ok()?; + + (decoder.output, 8) + }; + + Some(FilterResult { + data: Cow::Owned(decoded), + image_data: Some(ImageData { + alpha: None, + color_space: Some(ImageColorSpace::Gray), + bits_per_component: bpc, + width: image.width(), + height: image.height(), + }), + }) +} diff --git a/vendor/hayro-syntax/src/filter/jpx.rs b/vendor/hayro-syntax/src/filter/jpx.rs new file mode 100644 index 00000000..6a788591 --- /dev/null +++ b/vendor/hayro-syntax/src/filter/jpx.rs @@ -0,0 +1,191 @@ +use crate::bit_reader::{BitWriter, bit_mask}; +use crate::filter::FilterResult; +use crate::object::stream::{ImageColorSpace, ImageData, ImageDecodeParams}; +use alloc::borrow::Cow; +use alloc::vec; +use alloc::vec::Vec; +use hayro_jpeg2000::{ColorSpace, DecodeSettings}; + +impl ImageColorSpace { + fn num_components(&self) -> u8 { + match self { + Self::Gray => 1, + Self::Rgb => 3, + Self::Cmyk => 4, + Self::Unknown(num) => *num, + } + } +} + +pub(crate) fn decode(data: &[u8], params: &ImageDecodeParams) -> Option> { + use crate::object::stream::ImageColorSpace; + + let settings = DecodeSettings { + resolve_palette_indices: false, + strict: false, + target_resolution: params.target_dimension, + }; + + let image = hayro_jpeg2000::Image::new(data, &settings).ok()?; + + let width = image.width(); + let height = image.height(); + let bpc = params.bpc.unwrap_or(image.original_bit_depth()); + let cs = match image.color_space() { + ColorSpace::Gray => ImageColorSpace::Gray, + ColorSpace::RGB => ImageColorSpace::Rgb, + ColorSpace::CMYK => ImageColorSpace::Cmyk, + ColorSpace::Unknown { num_channels } => ImageColorSpace::Unknown(*num_channels), + ColorSpace::Icc { + num_channels: num_components, + .. + } => match num_components { + 1 => ImageColorSpace::Gray, + 3 => ImageColorSpace::Rgb, + 4 => ImageColorSpace::Cmyk, + _ => return None, + }, + }; + let has_alpha = image.has_alpha(); + let bitmap = image.decode().ok()?; + + let (mut data, mut alpha) = if !has_alpha { + (bitmap, None) + } else { + // Extract the alpha channel. + let total_channels = cs.num_components() + 1; + let mut color_channels = Vec::with_capacity( + (bitmap.len() / total_channels as usize) * cs.num_components() as usize, + ); + let mut alpha_channel = Vec::with_capacity(bitmap.len() / total_channels as usize); + + for sample in bitmap.chunks_exact(total_channels as usize) { + let (alpha, color) = sample.split_last()?; + alpha_channel.push(*alpha); + color_channels.extend_from_slice(color); + } + + (color_channels, Some(alpha_channel)) + }; + + // The decoded image is always 8-bit, so if necessary we have to rescale + // ourselves. + if bpc != 8 { + data = scale(&data, bpc, cs.num_components(), width, height)?; + alpha = alpha.and_then(|alpha| scale(&alpha, bpc, cs.num_components(), width, height)); + } + + Some(FilterResult { + data: Cow::Owned(data), + image_data: Some(ImageData { + alpha, + color_space: Some(cs), + bits_per_component: bpc, + width, + height, + }), + }) +} + +fn scale( + data: &[u8], + bit_per_component: u8, + num_components: u8, + width: u32, + height: u32, +) -> Option> { + if bit_per_component == 0 + || bit_per_component > 32 + || num_components == 0 + || width == 0 + || height == 0 + { + return None; + } + + // PdfCraft patch: normalize all supported bit widths without an overflowing + // shift or floating-point endpoint rounding, including no_std builds. + let max_sample = u64::from(bit_mask(bit_per_component)); + + let bits_per_row = (width as usize) + .checked_mul(num_components as usize)? + .checked_mul(bit_per_component as usize)?; + let input_len = bits_per_row.div_ceil(8).checked_mul(height as usize)?; + let mut input = vec![0; input_len]; + let mut writer = BitWriter::new(&mut input, bit_per_component)?; + let components_per_row = (num_components as usize).checked_mul(width as usize)?; + + for bytes in data.chunks_exact(components_per_row) { + for byte in bytes { + // The numerator is at most 255 * u32::MAX + 127, which fits in + // u64. The rounded quotient is <= max_sample, so the cast is exact. + let scaled = ((u64::from(*byte) * max_sample + 127) / 255) as u32; + writer.write(scaled)?; + } + + writer.align(); + } + + let final_pos = writer.cur_pos(); + input.truncate(final_pos); + + Some(input) +} + +#[cfg(test)] +mod tests { + use super::scale; + use crate::bit_reader::{BitReader, bit_mask}; + use alloc::vec::Vec; + + #[test] + fn jpx_scale_accepts_32_bit_samples() { + assert_eq!( + scale(&[0, 255], 32, 1, 2, 1).unwrap(), + [0, 0, 0, 0, 255, 255, 255, 255] + ); + } + + #[test] + fn jpx_scale_keeps_supported_bit_depths() { + for bpc in 1_u8..=32 { + let bytes = scale(&[0, 255], bpc, 1, 2, 1).unwrap(); + assert_eq!(bytes.len(), (2 * usize::from(bpc)).div_ceil(8)); + let mut reader = BitReader::new(&bytes); + assert_eq!(reader.read(bpc), Some(0)); + assert_eq!(reader.read(bpc), Some(bit_mask(bpc))); + } + } + + #[test] + fn jpx_scale_rejects_out_of_range_bit_depths() { + for bpc in [0, 33, 255] { + assert!(scale(&[255], bpc, 1, 1, 1).is_none()); + } + } + + #[test] + fn jpx_scale_rejects_zero_dimensions_and_components() { + for (components, width, height) in [(0, 1, 1), (1, 0, 1), (1, 1, 0)] { + assert!(scale(&[255], 32, components, width, height).is_none()); + } + } + + #[test] + fn jpx_scale_preserves_one_eight_and_sixteen_bit_samples() { + let samples: Vec = (0..=255).collect(); + for bpc in [1, 8, 16] { + let bytes = scale(&samples, bpc, 1, 256, 1).unwrap(); + let mut reader = BitReader::new(&bytes); + for byte in &samples { + let expected = match bpc { + 1 => u32::from(*byte >= 128), + 8 => u32::from(*byte), + 16 => u32::from(*byte) * 257, + _ => unreachable!(), + }; + assert_eq!(reader.read(bpc), Some(expected), "{bpc}-bit sample {byte}"); + } + } + } +} diff --git a/vendor/hayro-syntax/src/filter/lzw_flate.rs b/vendor/hayro-syntax/src/filter/lzw_flate.rs new file mode 100644 index 00000000..e65e83bd --- /dev/null +++ b/vendor/hayro-syntax/src/filter/lzw_flate.rs @@ -0,0 +1,1335 @@ +use crate::bit_reader::{BitChunk, BitChunks, BitReader, BitWriter, bit_mask}; +use crate::object::Dict; +use crate::object::dict::keys::{BITS_PER_COMPONENT, COLORS, COLUMNS, EARLY_CHANGE, PREDICTOR}; +use alloc::vec; +use alloc::vec::Vec; + +pub(crate) mod flate { + use super::*; + use crate::filter::lzw_flate::{PredictorParams, apply_predictor}; + + pub(crate) use fallback::FlateState; + + /// PdfCraft patch: whether a Flate stream with `params` has no predictor, so that + /// [`FlateState`] can produce it a piece at a time. + pub(crate) fn is_plain(params: &Dict<'_>) -> bool { + PredictorParams::from_params(params).predictor == 1 + } + use crate::object::Dict; + + /// PdfCraft patch: `limit` is the most bytes inflated (see `filter::decode_limit`). + #[cfg(feature = "unsafe")] + pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option> { + use flate2::read::{DeflateDecoder, ZlibDecoder}; + use std::io::Read; + + // Read in chunks and never past `limit`, so the buffer doesn't grow past it either. + fn read_up_to(mut decoder: impl Read, limit: usize) -> Option> { + let mut result = Vec::new(); + let mut chunk = vec![0; 64 * 1024]; + while result.len() < limit { + let n = decoder.read(&mut chunk).ok()?; + if n == 0 { + break; + } + let take = n.min(limit - result.len()); + result.extend_from_slice(chunk.get(..take)?); + } + Some(result) + } + let zlib_stream = |data: &[u8]| read_up_to(ZlibDecoder::new(data), limit); + let deflate_stream = |data: &[u8]| read_up_to(DeflateDecoder::new(data), limit); + + let decoded = zlib_stream(data) + .or_else(|| deflate_stream(data)) + .or_else(|| { + warn!("flate stream is broken, decoding with fallback"); + + fallback::decode(data, limit) + })?; + // PdfCraft patch: output cut at the limit is reported, never dropped silently. + if decoded.len() >= limit { + warn!("flate stream stopped at its decode limit of {limit} bytes"); + } + let params = PredictorParams::from_params(params); + apply_predictor(decoded, ¶ms) + } + + #[cfg(not(feature = "unsafe"))] + pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option> { + let decoded = fallback::decode(data, limit)?; + // PdfCraft patch: output cut at the limit is reported, never dropped silently. + if decoded.len() >= limit { + warn!("flate stream stopped at its decode limit of {limit} bytes"); + } + let params = PredictorParams::from_params(params); + apply_predictor(decoded, ¶ms) + } + + /// Ported from + /// TODO: Rewrite this in idiomatic Rust. + mod fallback { + use alloc::vec; + use alloc::vec::Vec; + + pub(crate) fn decode(data: &[u8], limit: usize) -> Option> { + flate_decode(data, limit) + } + + fn flate_decode(data: &[u8], limit: usize) -> Option> { + let mut state = FlateState::new(data); + let mut output = Vec::new(); + state.decode_into(data, &mut output, limit); + + Some(output) + } + + /// PdfCraft patch: where a Flate stream is between two calls of [`FlateState::decode_into`]. + /// Decoding pauses at any output size and resumes where it stopped, so a stream can be + /// produced a piece at a time without holding all of it. + #[derive(Default)] + pub(crate) struct FlateState { + pos: usize, + code_buf: u32, + code_size: u8, + eof: bool, + complete: bool, + failed: bool, + block: Block, + /// The last `WINDOW` bytes decoded: back-references copy from them, whatever the caller + /// has done with the bytes it was given. + history: Vec, + /// How many bytes have been decoded in all. + total: usize, + } + + /// The furthest a Flate back-reference reaches: the largest distance in `DIST_DECODE`. + const WINDOW: usize = 32 << 10; + + impl FlateState { + /// The state at the start of `data`, past a zlib header if it has one. + pub(crate) fn new(data: &[u8]) -> Self { + let pos = match data { + [cmf, flg, ..] if (cmf & 0x0f) == 0x08 && (u16::from(*cmf) << 8 | u16::from(*flg)).is_multiple_of(31) && (flg & 0x20) == 0 => 2, + _ => 0, + }; + + Self { + pos, + ..Self::default() + } + } + + /// Whether nothing more can be decoded from `data`. + pub(crate) fn finished(&self, data: &[u8]) -> bool { + self.eof || (matches!(self.block, Block::Header) && self.pos >= data.len()) + } + + /// Whether decoding reached a valid final block without dropping malformed data. + pub(crate) fn complete(&self) -> bool { + self.complete && !self.failed + } + + /// Appends decoded bytes to `out` until it holds `until` bytes or the stream ends. + /// `data` is the whole encoded stream, the same on every call. + pub(crate) fn decode_into(&mut self, data: &[u8], out: &mut Vec, until: usize) { + // The working buffer is the history followed by the bytes decoded now. Only the + // latter go to `out`, which the caller may have drained since. + let base = self.history.len(); + let mut stream = FlateStream { + data, + pos: self.pos, + code_buf: self.code_buf, + code_size: self.code_size, + output: core::mem::take(&mut self.history), + base, + total: self.total, + eof: self.eof, + complete: self.complete, + failed: self.failed, + limit: base.saturating_add(until.saturating_sub(out.len())), + block: core::mem::take(&mut self.block), + }; + stream.run(); + + out.extend_from_slice(stream.output.get(base..).unwrap_or(&[])); + self.total = self.total.saturating_add(stream.output.len() - base); + let keep = stream.output.len().min(WINDOW); + stream.output.drain(..stream.output.len() - keep); + self.history = stream.output; + + self.pos = stream.pos; + self.code_buf = stream.code_buf; + self.code_size = stream.code_size; + self.eof = stream.eof; + self.complete = stream.complete; + self.failed = stream.failed; + self.block = stream.block; + } + } + + /// PdfCraft patch: the working state of one [`FlateState::decode_into`] call. + struct FlateStream<'a> { + data: &'a [u8], + pos: usize, + code_buf: u32, + code_size: u8, + /// The history, then the bytes decoded in this call. + output: Vec, + /// How many bytes of `output` are history. + base: usize, + /// How many bytes were decoded before this call. + total: usize, + eof: bool, + complete: bool, + failed: bool, + /// PdfCraft patch: the most bytes in `output` (see `filter::decode_limit`). + limit: usize, + block: Block, + } + + /// PdfCraft patch: the block a Flate stream is in. + #[derive(Default)] + enum Block { + /// Between blocks, at a block header (or at the end of the data). + #[default] + Header, + /// Inside a stored block, with `left` bytes still to copy. + Stored { left: usize, last: bool }, + /// Inside a block coded with Huffman tables. `copy` is a back-reference that the limit + /// cut off, as (whether its source exists, distance, bytes left to copy). + Coded { + lit: HuffmanTable, + dist: HuffmanTable, + last: bool, + copy: Option<(bool, usize, usize)>, + }, + } + + impl FlateStream<'_> { + /// Decodes until `output` holds `limit` bytes or the stream ends. + fn run(&mut self) { + loop { + if self.eof || self.output.len() >= self.limit { + return; + } + + match self.block { + Block::Header => { + if self.pos >= self.data.len() { + return; + } + + self.read_block(); + } + Block::Stored { .. } => self.copy_stored(), + Block::Coded { .. } => self.read_codes(), + } + } + } + + fn get_byte(&mut self) -> Option { + if self.pos >= self.data.len() { + None + } else { + let byte = self.data[self.pos]; + self.pos += 1; + Some(byte) + } + } + + fn peek_byte(&self) -> Option { + if self.pos >= self.data.len() { + None + } else { + Some(self.data[self.pos]) + } + } + + fn get_bits(&mut self, bits: u8) -> Option { + while self.code_size < bits { + let b = self.get_byte()?; + self.code_buf |= (b as u32) << self.code_size; + self.code_size += 8; + } + + let result = self.code_buf & ((1 << bits) - 1); + self.code_buf >>= bits; + self.code_size -= bits; + + Some(result) + } + + fn get_code(&mut self, table: &HuffmanTable) -> Option { + let codes = &table.codes; + let max_len = table.max_len; + + while self.code_size < max_len { + if let Some(b) = self.get_byte() { + self.code_buf |= (b as u32) << self.code_size; + self.code_size += 8; + } else { + // Premature end of stream + break; + } + } + + let code = codes.get((self.code_buf & ((1 << max_len) - 1)) as usize)?; + let code_len = (code >> 16) as u8; + let code_val = code & 0xffff; + + if code_len < 1 || self.code_size < code_len { + return None; + } + + self.code_buf >>= code_len; + self.code_size -= code_len; + + Some(code_val as u16) + } + + fn read_block(&mut self) { + // Read block header + let hdr = match self.get_bits(3) { + Some(h) => h, + None => { + warn!("bad block header in flate stream"); + self.eof = true; + self.failed = true; + return; + } + }; + + // PdfCraft patch: the final block ends the stream once it is decoded, not when its + // header is read: a stored block still has its bytes to copy. + let last = (hdr & 1) != 0; + + let hdr = hdr >> 1; + + match hdr { + 0 => self.start_uncompressed_block(last), + 1 => self.start_compressed_block(true, last), + 2 => self.start_compressed_block(false, last), + _ => { + warn!("unknown block type in flate stream"); + self.eof = true; + self.failed = true; + } + } + } + + fn start_uncompressed_block(&mut self, last: bool) { + // Skip any remaining bits in current byte + self.code_buf = 0; + self.code_size = 0; + + let len_low = match self.get_byte() { + Some(b) => b as u16, + None => { + warn!("bad block header in flate stream"); + self.eof = true; + self.failed = true; + return; + } + }; + + let len_high = match self.get_byte() { + Some(b) => b as u16, + None => { + warn!("bad block header in flate stream"); + self.eof = true; + self.failed = true; + return; + } + }; + + let block_len = len_low | (len_high << 8); + + let nlen_low = match self.get_byte() { + Some(b) => b as u16, + None => { + warn!("bad block header in flate stream"); + self.eof = true; + self.failed = true; + return; + } + }; + + let nlen_high = match self.get_byte() { + Some(b) => b as u16, + None => { + warn!("bad block header in flate stream"); + self.eof = true; + self.failed = true; + return; + } + }; + + let check = nlen_low | (nlen_high << 8); + + if check != !block_len && (block_len != 0 || check != 0) { + // Ignoring error for bad "empty" block + warn!("bad uncompressed block length in flate stream"); + self.failed = true; + } + + if block_len == 0 { + if last || self.peek_byte().is_none() { + self.eof = true; + self.complete = last; + } + } else { + self.block = Block::Stored { + left: usize::from(block_len), + last, + }; + } + } + + /// Copies the stored block's bytes, up to the limit or the end of the data. + fn copy_stored(&mut self) { + let Block::Stored { mut left, last } = self.block else { + return; + }; + + while left > 0 && self.output.len() < self.limit { + let rest = self.data.get(self.pos..).unwrap_or(&[]); + if rest.is_empty() { + // Premature end of stream: the rest of the block is missing. + self.eof = true; + self.failed = true; + return; + } + + let take = left.min(rest.len()).min(self.limit - self.output.len()); + self.output + .extend_from_slice(rest.get(..take).unwrap_or(&[])); + self.pos += take; + left -= take; + } + + if left > 0 { + self.block = Block::Stored { left, last }; + } else { + self.block = Block::Header; + if last { + self.eof = true; + self.complete = true; + } + } + } + + fn start_compressed_block(&mut self, fixed: bool, last: bool) { + let (lit_code_table, dist_code_table) = if fixed { + (get_fixed_lit_table(), get_fixed_dist_table()) + } else { + match self.read_dynamic_tables() { + Some(tables) => tables, + None => { + self.eof = true; + self.failed = true; + return; + } + } + }; + + self.block = Block::Coded { + lit: lit_code_table, + dist: dist_code_table, + last, + copy: None, + }; + } + + /// Decodes the codes of a Huffman-coded block until its end, the limit or the end of + /// the data. + fn read_codes(&mut self) { + let Block::Coded { + lit: lit_code_table, + dist: dist_code_table, + last, + mut copy, + } = core::mem::take(&mut self.block) + else { + return; + }; + + loop { + if let Some((valid, distance, left)) = copy { + let left = self.copy_back(valid, distance, left); + if left > 0 { + // The limit cut the copy short: it resumes on the next call. + self.block = Block::Coded { + lit: lit_code_table, + dist: dist_code_table, + last, + copy: Some((valid, distance, left)), + }; + return; + } + copy = None; + } + + // PdfCraft patch: one code adds at most 258 bytes; stop at the limit. + if self.output.len() >= self.limit { + self.block = Block::Coded { + lit: lit_code_table, + dist: dist_code_table, + last, + copy, + }; + return; + } + + let code1 = match self.get_code(&lit_code_table) { + Some(c) => c, + None => { + self.eof = true; + self.failed = true; + return; + } + }; + + if code1 < 256 { + self.output.push(code1 as u8); + } else if code1 == 256 { + if last { + self.eof = true; + self.complete = true; + } + return; + } else { + let code1 = code1 - 257; + let length_info = LENGTH_DECODE.get(code1 as usize).copied().unwrap_or(0); + let extra_bits = (length_info >> 16) as u8; + let mut length = (length_info & 0xffff) as usize; + + if extra_bits > 0 { + if let Some(extra) = self.get_bits(extra_bits) { + length += extra as usize; + } else { + self.eof = true; + self.failed = true; + return; + } + } + + let dist_code = match self.get_code(&dist_code_table) { + Some(c) => c, + None => { + self.eof = true; + self.failed = true; + return; + } + }; + + let dist_info = match DIST_DECODE.get(dist_code as usize) { + Some(&info) => info, + None => { + warn!("invalid distance code {} in flate stream", dist_code); + + self.eof = true; + + self.failed = true; + return; + } + }; + + let extra_bits = (dist_info >> 16) as u8; + let mut distance = (dist_info & 0xffff) as usize; + + if extra_bits > 0 { + if let Some(extra) = self.get_bits(extra_bits) { + distance += extra as usize; + } else { + self.eof = true; + self.failed = true; + return; + } + } + + // Copy from previous output. As before, a distance reaching before the first + // byte decoded copies nothing. + let decoded = self.total.saturating_add(self.output.len().saturating_sub(self.base)); + let valid = distance > 0 && distance <= decoded; + self.failed |= !valid; + copy = Some((valid, distance, length)); + } + } + } + + /// Copies `left` bytes from `distance` back in the output, up to the limit. Returns how + /// many are still to copy. A copy whose source does not exist writes nothing. + fn copy_back(&mut self, valid: bool, distance: usize, mut left: usize) -> usize { + while left > 0 && self.output.len() < self.limit { + if valid { + let byte = self + .output + .len() + .checked_sub(distance) + .and_then(|index| self.output.get(index)) + .copied(); + if let Some(byte) = byte { + self.output.push(byte); + } + } + left -= 1; + } + + left + } + + fn read_dynamic_tables(&mut self) -> Option<(HuffmanTable, HuffmanTable)> { + let num_lit_codes = self.get_bits(5)? as usize + 257; + let num_dist_codes = self.get_bits(5)? as usize + 1; + let num_code_len_codes = self.get_bits(4)? as usize + 4; + + // Build code length code table + let mut code_len_code_lengths = vec![0_u8; 19]; + for i in 0..num_code_len_codes { + code_len_code_lengths[CODE_LEN_CODE_MAP[i] as usize] = self.get_bits(3)? as u8; + } + + let code_len_table = generate_huffman_table(&code_len_code_lengths); + + // Read code lengths + let total_codes = num_lit_codes + num_dist_codes; + let mut code_lengths = vec![0_u8; total_codes]; + let mut i = 0; + + while i < total_codes { + let code = self.get_code(&code_len_table)?; + + match code { + 0..=15 => { + code_lengths[i] = code as u8; + i += 1; + } + 16 => { + // Repeat previous + let repeat_count = self.get_bits(2)? as usize + 3; + let prev = if i > 0 { code_lengths[i - 1] } else { 0 }; + for _ in 0..repeat_count { + if i < total_codes { + code_lengths[i] = prev; + i += 1; + } + } + } + 17 => { + // Repeat zero 3-10 times + let repeat_count = self.get_bits(3)? as usize + 3; + for _ in 0..repeat_count { + if i < total_codes { + code_lengths[i] = 0; + i += 1; + } + } + } + 18 => { + // Repeat zero 11-138 times + let repeat_count = self.get_bits(7)? as usize + 11; + for _ in 0..repeat_count { + if i < total_codes { + code_lengths[i] = 0; + i += 1; + } + } + } + _ => return None, + } + } + + let lit_table = generate_huffman_table(&code_lengths[..num_lit_codes]); + let dist_table = generate_huffman_table(&code_lengths[num_lit_codes..]); + + Some((lit_table, dist_table)) + } + } + + struct HuffmanTable { + codes: Vec, + max_len: u8, + } + + fn generate_huffman_table(lengths: &[u8]) -> HuffmanTable { + let _n = lengths.len(); + + // Find max code length + let max_len = lengths.iter().cloned().max().unwrap_or(0); + + if max_len == 0 { + return HuffmanTable { + codes: vec![0; 1], + max_len: 1, + }; + } + + // Build the table + let size = 1 << max_len; + let mut codes = vec![0_u32; size]; + + let mut code = 0_u32; + for len in 1..=max_len { + for (val, &length) in lengths.iter().enumerate() { + if length == len { + // Bit-reverse the code + let mut code2 = 0_u32; + let mut t = code; + for _ in 0..len { + code2 = (code2 << 1) | (t & 1); + t >>= 1; + } + + // Fill the table entries + let skip = 1 << len; + let mut i = code2 as usize; + while i < size { + codes[i] = ((len as u32) << 16) | (val as u32); + i += skip; + } + code += 1; + } + } + code <<= 1; + } + + HuffmanTable { codes, max_len } + } + + fn get_fixed_lit_table() -> HuffmanTable { + HuffmanTable { + codes: FIXED_LIT_CODE_TAB.to_vec(), + max_len: 9, + } + } + + fn get_fixed_dist_table() -> HuffmanTable { + HuffmanTable { + codes: FIXED_DIST_CODE_TAB.to_vec(), + max_len: 5, + } + } + + const CODE_LEN_CODE_MAP: [u8; 19] = [ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15, + ]; + + const LENGTH_DECODE: [u32; 29] = [ + 0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a, 0x1000b, + 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f, 0x30023, 0x3002b, + 0x30033, 0x3003b, 0x40043, 0x40053, 0x40063, 0x40073, 0x50083, 0x500a3, 0x500c3, + 0x500e3, 0x00102, + ]; + + const DIST_DECODE: [u32; 30] = [ + 0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d, 0x30011, + 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1, 0x70101, 0x70181, + 0x80201, 0x80301, 0x90401, 0x90601, 0xa0801, 0xa0c01, 0xb1001, 0xb1801, 0xc2001, + 0xc3001, 0xd4001, 0xd6001, + ]; + + const FIXED_LIT_CODE_TAB: [u32; 512] = [ + 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0, 0x70108, + 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0, 0x70104, 0x80058, + 0x80018, 0x90090, 0x70114, 0x80078, 0x80038, 0x900d0, 0x7010c, 0x80068, 0x80028, + 0x900b0, 0x80008, 0x80088, 0x80048, 0x900f0, 0x70102, 0x80054, 0x80014, 0x8011c, + 0x70112, 0x80074, 0x80034, 0x900c8, 0x7010a, 0x80064, 0x80024, 0x900a8, 0x80004, + 0x80084, 0x80044, 0x900e8, 0x70106, 0x8005c, 0x8001c, 0x90098, 0x70116, 0x8007c, + 0x8003c, 0x900d8, 0x7010e, 0x8006c, 0x8002c, 0x900b8, 0x8000c, 0x8008c, 0x8004c, + 0x900f8, 0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c4, + 0x70109, 0x80062, 0x80022, 0x900a4, 0x80002, 0x80082, 0x80042, 0x900e4, 0x70105, + 0x8005a, 0x8001a, 0x90094, 0x70115, 0x8007a, 0x8003a, 0x900d4, 0x7010d, 0x8006a, + 0x8002a, 0x900b4, 0x8000a, 0x8008a, 0x8004a, 0x900f4, 0x70103, 0x80056, 0x80016, + 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cc, 0x7010b, 0x80066, 0x80026, 0x900ac, + 0x80006, 0x80086, 0x80046, 0x900ec, 0x70107, 0x8005e, 0x8001e, 0x9009c, 0x70117, + 0x8007e, 0x8003e, 0x900dc, 0x7010f, 0x8006e, 0x8002e, 0x900bc, 0x8000e, 0x8008e, + 0x8004e, 0x900fc, 0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, + 0x900c2, 0x70108, 0x80061, 0x80021, 0x900a2, 0x80001, 0x80081, 0x80041, 0x900e2, + 0x70104, 0x80059, 0x80019, 0x90092, 0x70114, 0x80079, 0x80039, 0x900d2, 0x7010c, + 0x80069, 0x80029, 0x900b2, 0x80009, 0x80089, 0x80049, 0x900f2, 0x70102, 0x80055, + 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900ca, 0x7010a, 0x80065, 0x80025, + 0x900aa, 0x80005, 0x80085, 0x80045, 0x900ea, 0x70106, 0x8005d, 0x8001d, 0x9009a, + 0x70116, 0x8007d, 0x8003d, 0x900da, 0x7010e, 0x8006d, 0x8002d, 0x900ba, 0x8000d, + 0x8008d, 0x8004d, 0x900fa, 0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, + 0x80033, 0x900c6, 0x70109, 0x80063, 0x80023, 0x900a6, 0x80003, 0x80083, 0x80043, + 0x900e6, 0x70105, 0x8005b, 0x8001b, 0x90096, 0x70115, 0x8007b, 0x8003b, 0x900d6, + 0x7010d, 0x8006b, 0x8002b, 0x900b6, 0x8000b, 0x8008b, 0x8004b, 0x900f6, 0x70103, + 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900ce, 0x7010b, 0x80067, + 0x80027, 0x900ae, 0x80007, 0x80087, 0x80047, 0x900ee, 0x70107, 0x8005f, 0x8001f, + 0x9009e, 0x70117, 0x8007f, 0x8003f, 0x900de, 0x7010f, 0x8006f, 0x8002f, 0x900be, + 0x8000f, 0x8008f, 0x8004f, 0x900fe, 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, + 0x80070, 0x80030, 0x900c1, 0x70108, 0x80060, 0x80020, 0x900a1, 0x80000, 0x80080, + 0x80040, 0x900e1, 0x70104, 0x80058, 0x80018, 0x90091, 0x70114, 0x80078, 0x80038, + 0x900d1, 0x7010c, 0x80068, 0x80028, 0x900b1, 0x80008, 0x80088, 0x80048, 0x900f1, + 0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c9, 0x7010a, + 0x80064, 0x80024, 0x900a9, 0x80004, 0x80084, 0x80044, 0x900e9, 0x70106, 0x8005c, + 0x8001c, 0x90099, 0x70116, 0x8007c, 0x8003c, 0x900d9, 0x7010e, 0x8006c, 0x8002c, + 0x900b9, 0x8000c, 0x8008c, 0x8004c, 0x900f9, 0x70101, 0x80052, 0x80012, 0x8011a, + 0x70111, 0x80072, 0x80032, 0x900c5, 0x70109, 0x80062, 0x80022, 0x900a5, 0x80002, + 0x80082, 0x80042, 0x900e5, 0x70105, 0x8005a, 0x8001a, 0x90095, 0x70115, 0x8007a, + 0x8003a, 0x900d5, 0x7010d, 0x8006a, 0x8002a, 0x900b5, 0x8000a, 0x8008a, 0x8004a, + 0x900f5, 0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cd, + 0x7010b, 0x80066, 0x80026, 0x900ad, 0x80006, 0x80086, 0x80046, 0x900ed, 0x70107, + 0x8005e, 0x8001e, 0x9009d, 0x70117, 0x8007e, 0x8003e, 0x900dd, 0x7010f, 0x8006e, + 0x8002e, 0x900bd, 0x8000e, 0x8008e, 0x8004e, 0x900fd, 0x70100, 0x80051, 0x80011, + 0x80119, 0x70110, 0x80071, 0x80031, 0x900c3, 0x70108, 0x80061, 0x80021, 0x900a3, + 0x80001, 0x80081, 0x80041, 0x900e3, 0x70104, 0x80059, 0x80019, 0x90093, 0x70114, + 0x80079, 0x80039, 0x900d3, 0x7010c, 0x80069, 0x80029, 0x900b3, 0x80009, 0x80089, + 0x80049, 0x900f3, 0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, + 0x900cb, 0x7010a, 0x80065, 0x80025, 0x900ab, 0x80005, 0x80085, 0x80045, 0x900eb, + 0x70106, 0x8005d, 0x8001d, 0x9009b, 0x70116, 0x8007d, 0x8003d, 0x900db, 0x7010e, + 0x8006d, 0x8002d, 0x900bb, 0x8000d, 0x8008d, 0x8004d, 0x900fb, 0x70101, 0x80053, + 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c7, 0x70109, 0x80063, 0x80023, + 0x900a7, 0x80003, 0x80083, 0x80043, 0x900e7, 0x70105, 0x8005b, 0x8001b, 0x90097, + 0x70115, 0x8007b, 0x8003b, 0x900d7, 0x7010d, 0x8006b, 0x8002b, 0x900b7, 0x8000b, + 0x8008b, 0x8004b, 0x900f7, 0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, + 0x80037, 0x900cf, 0x7010b, 0x80067, 0x80027, 0x900af, 0x80007, 0x80087, 0x80047, + 0x900ef, 0x70107, 0x8005f, 0x8001f, 0x9009f, 0x70117, 0x8007f, 0x8003f, 0x900df, + 0x7010f, 0x8006f, 0x8002f, 0x900bf, 0x8000f, 0x8008f, 0x8004f, 0x900ff, + ]; + + const FIXED_DIST_CODE_TAB: [u32; 32] = [ + 0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c, 0x50002, + 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000, 0x50001, 0x50011, + 0x50009, 0x50019, 0x50005, 0x50015, 0x5000d, 0x5001d, 0x50003, 0x50013, 0x5000b, + 0x5001b, 0x50007, 0x50017, 0x5000f, 0x00000, + ]; + } +} + +pub(crate) mod lzw { + use crate::bit_reader::BitReader; + use crate::filter::lzw_flate::{PredictorParams, apply_predictor}; + use crate::object::Dict; + use alloc::vec; + use alloc::vec::Vec; + + /// Decode a LZW-encoded stream. PdfCraft patch: `limit` is the most bytes produced (see + /// `filter::decode_limit`); with a full table every 12-bit code can repeat an entry of + /// thousands of bytes. + pub(crate) fn decode(data: &[u8], params: &Dict<'_>, limit: usize) -> Option> { + let params = PredictorParams::from_params(params); + + let decoded = decode_impl(data, params.early_change, limit)?; + // PdfCraft patch: output cut at the limit is reported, never dropped silently. + if decoded.len() >= limit { + warn!("LZW stream stopped at its decode limit of {limit} bytes"); + } + + apply_predictor(decoded, ¶ms) + } + + const CLEAR_TABLE: usize = 256; + const EOD: usize = 257; + const MAX_ENTRIES: usize = 4096; + const INITIAL_SIZE: u16 = 258; + + fn decode_impl(data: &[u8], early_change: bool, limit: usize) -> Option> { + let mut table = Table::new(early_change); + let mut bit_size = table.code_length(); + let mut reader = BitReader::new(data); + let mut decoded = vec![]; + let mut prev = None; + + loop { + if decoded.len() >= limit { + return Some(decoded); + } + let next = match reader.read(bit_size) { + Some(code) => code as usize, + None => { + warn!("premature EOF in LZW stream, EOD code missing"); + return Some(decoded); + } + }; + + match next { + CLEAR_TABLE => { + table.clear(); + prev = None; + bit_size = table.code_length(); + } + EOD => return Some(decoded), + new => { + if new > table.size() { + warn!("invalid LZW code: {} (table size: {})", new, table.size()); + return None; + } + + // PdfCraft patch: never past `limit`. + let left = limit.saturating_sub(decoded.len()); + if new < table.size() { + let entry = table.get(new)?; + let first_byte = entry[0]; + decoded.extend_from_slice(entry.get(..entry.len().min(left))?); + + if let Some(prev_code) = prev { + table.register(prev_code, first_byte); + } + } else if new == table.size() && prev.is_some() { + let prev_code = prev.unwrap(); + let prev_entry = table.get(prev_code)?; + let first_byte = prev_entry[0]; + + let new_entry = table.register(prev_code, first_byte)?; + decoded.extend_from_slice(new_entry.get(..new_entry.len().min(left))?); + } else { + warn!("LZW decode error: code {new} not found and prev is None"); + return None; + } + + bit_size = table.code_length(); + prev = Some(new); + } + } + } + } + + struct Table { + early_change: bool, + entries: Vec>>, + } + + impl Table { + fn new(early_change: bool) -> Self { + let mut entries: Vec<_> = (0..=255).map(|b| Some(vec![b])).collect(); + + // Clear table and EOD don't have any data. + entries.push(None); // 256 = CLEAR_TABLE + entries.push(None); // 257 = EOD + + Self { + early_change, + entries, + } + } + + fn push(&mut self, entry: Vec) -> Option<&[u8]> { + if self.entries.len() >= MAX_ENTRIES { + None + } else { + self.entries.push(Some(entry)); + self.entries.last()?.as_ref().map(|v| &**v) + } + } + + fn register(&mut self, prev: usize, new_byte: u8) -> Option<&[u8]> { + let prev_entry = self.get(prev)?; + + let mut new_entry = Vec::with_capacity(prev_entry.len() + 1); + new_entry.extend(prev_entry); + new_entry.push(new_byte); + self.push(new_entry) + } + + fn get(&self, index: usize) -> Option<&[u8]> { + self.entries.get(index)?.as_ref().map(|v| &**v) + } + + fn clear(&mut self) { + self.entries.truncate(INITIAL_SIZE as usize); + } + + fn size(&self) -> usize { + self.entries.len() + } + + fn code_length(&self) -> u8 { + const TEN: usize = 512; + const ELEVEN: usize = 1024; + const TWELVE: usize = 2048; + + let adjusted = self.entries.len() + (if self.early_change { 1 } else { 0 }); + + if adjusted >= TWELVE { + 12 + } else if adjusted >= ELEVEN { + 11 + } else if adjusted >= TEN { + 10 + } else { + 9 + } + } + } +} + +struct PredictorParams { + predictor: u8, + colors: u8, + bits_per_component: u8, + columns: usize, + early_change: bool, +} + +impl PredictorParams { + fn row_length_in_bytes(&self) -> usize { + // PdfCraft patch: saturating, and in usize (colors ร— bits overflowed u8): a fuzzed + // `/Columns 9223372036854775807` wrapped to a 2^61-byte row. + self.columns + .saturating_mul(usize::from(self.bits_per_component) * usize::from(self.colors)) + .div_ceil(8) + } +} + +impl Default for PredictorParams { + fn default() -> Self { + Self { + predictor: 1, + colors: 1, + bits_per_component: 8, + columns: 1, + early_change: true, + } + } +} + +impl PredictorParams { + fn from_params(dict: &Dict<'_>) -> Self { + Self { + predictor: dict.get(PREDICTOR).unwrap_or(1), + colors: dict.get(COLORS).unwrap_or(1), + bits_per_component: dict.get(BITS_PER_COMPONENT).unwrap_or(8), + columns: dict.get(COLUMNS).unwrap_or(1), + early_change: dict.get::(EARLY_CHANGE).map(|e| e != 0).unwrap_or(true), + } + } +} + +fn apply_predictor(data: Vec, params: &PredictorParams) -> Option> { + match params.predictor { + 1 => Some(data), + i => { + let is_png_predictor = i >= 10; + + let row_len = params.row_length_in_bytes(); + + let total_row_len = if is_png_predictor { + // + 1 Because each row must start with the predictor that is used for PNG predictors. + row_len.saturating_add(1) + } else { + row_len + }; + + // PdfCraft patch: a zero-width row divided by zero below, and a row longer than the + // data was allocated before finding there is nothing to read (no rows: empty output, + // as before). + if row_len == 0 { + return None; + } + if total_row_len > data.len() { + return Some(Vec::new()); + } + + let num_rows = data.len() / total_row_len; + + if !matches!(params.bits_per_component, 1 | 2 | 4 | 8 | 16) { + warn!("invalid bits per component {}", params.bits_per_component); + + return None; + } + + let (bit_size, chunk_len) = if is_png_predictor { + ( + 8, + // PdfCraft patch: in usize (the u8 product overflowed). + (usize::from(params.colors) * usize::from(params.bits_per_component)).div_ceil(8), + ) + } else { + (params.bits_per_component, params.colors as usize) + }; + let zero_row = vec![0; row_len]; + let mut prev_row = BitChunks::new(&zero_row, bit_size, chunk_len)?; + let zero_col = BitChunk::new(0, chunk_len); + let mut out = vec![0; num_rows * row_len]; + let mut writer = BitWriter::new(&mut out, bit_size)?; + + for in_row in data.chunks_exact(total_row_len) { + if is_png_predictor { + let predictor = in_row[0]; + let in_data = &in_row[1..]; + let in_data_chunks = BitChunks::new(in_data, bit_size, chunk_len)?; + + match predictor { + 1 => apply::( + prev_row, + zero_col.clone(), + zero_col.clone(), + in_data_chunks, + &mut writer, + chunk_len, + bit_size, + )?, + 2 => apply::( + prev_row, + zero_col.clone(), + zero_col.clone(), + in_data_chunks, + &mut writer, + chunk_len, + bit_size, + )?, + 3 => apply::( + prev_row, + zero_col.clone(), + zero_col.clone(), + in_data_chunks, + &mut writer, + chunk_len, + bit_size, + )?, + 4 => apply::( + prev_row, + zero_col.clone(), + zero_col.clone(), + in_data_chunks, + &mut writer, + chunk_len, + bit_size, + )?, + _ => { + // Just copy the data. + let mut reader = BitReader::new(in_data); + + while let Some(data) = reader.read(bit_size) { + writer.write(data); + } + } + } + } else if i == 2 { + apply::( + prev_row, + zero_col.clone(), + zero_col.clone(), + BitChunks::new(in_row, bit_size, chunk_len)?, + &mut writer, + chunk_len, + bit_size, + ); + } else { + warn!("unknown predictor {i}"); + + return None; + } + + // PdfCraft patch: packed TIFF samples can leave padding bits at the end + // of a row. split_off requires byte alignment or the next row overwrites it. + writer.align(); + let (data, new_writer) = writer.split_off(); + writer = new_writer; + prev_row = BitChunks::new(data, bit_size, chunk_len)?; + } + + Some(out) + } + } +} + +fn apply<'a, T: Predictor>( + prev_row: BitChunks<'a>, + mut prev_col: BitChunk, + mut top_left: BitChunk, + cur_row: BitChunks<'a>, + writer: &mut BitWriter<'a>, + chunk_len: usize, + bit_size: u8, +) -> Option<()> { + for (cur_row, prev_row) in cur_row.zip(prev_row) { + let old_pos = writer.cur_pos(); + + for (((cur_row, prev_row), prev_col), top_left) in cur_row + .iter() + .zip(prev_row.iter()) + .zip(prev_col.iter()) + .zip(top_left.iter()) + { + // Note that the wrapping behavior when adding inside the predictors is dependent on the + // bit size, so it wouldn't be triggered for bits per component < 16. So we mask out + // the bytes manually, which is equivalent to a wrapping add. + writer.write( + T::predict(cur_row, prev_row, prev_col, top_left) as u32 & bit_mask(bit_size), + ); + } + + prev_col = { + let out_data = writer.get_data(); + let mut reader = BitReader::new_with(out_data, old_pos); + BitChunk::from_reader(&mut reader, bit_size, chunk_len).unwrap() + }; + + top_left = prev_row; + } + + Some(()) +} + +trait Predictor { + fn predict(cur_row: u16, prev_row: u16, prev_col: u16, top_left: u16) -> u16; +} + +struct Sub; +impl Predictor for Sub { + fn predict(cur_row: u16, _: u16, prev_col: u16, _: u16) -> u16 { + cur_row.wrapping_add(prev_col) + } +} + +struct Up; +impl Predictor for Up { + fn predict(cur_row: u16, prev_row: u16, _: u16, _: u16) -> u16 { + cur_row.wrapping_add(prev_row) + } +} + +struct Avg; +impl Predictor for Avg { + fn predict(cur_row: u16, prev_row: u16, prev_col: u16, _: u16) -> u16 { + cur_row.wrapping_add(((prev_col as u32 + prev_row as u32) / 2) as u16) + } +} + +struct Paeth; +impl Predictor for Paeth { + fn predict(cur_row: u16, prev_row: u16, prev_col: u16, top_left: u16) -> u16 { + fn paeth(a: u16, b: u16, c: u16) -> u16 { + let a = a as i32; + let b = b as i32; + let c = c as i32; + + let p = a + b - c; + let pa = (p - a).abs(); + let pb = (p - b).abs(); + let pc = (p - c).abs(); + + if pa <= pb && pa <= pc { + a as u16 + } else if pb <= pc { + b as u16 + } else { + c as u16 + } + } + + cur_row.wrapping_add(paeth(prev_col, prev_row, top_left)) + } +} + +#[cfg(test)] +#[rustfmt::skip] +mod tests { + use crate::filter::lzw_flate::{PredictorParams, apply_predictor, flate, lzw}; + use crate::object::Dict; + + #[test] + fn decode_lzw() { + let input = [0x80, 0x0B, 0x60, 0x50, 0x22, 0x0C, 0x0C, 0x85, 0x01]; + let decoded = lzw::decode(&input, &Dict::default(), usize::MAX).unwrap(); + + assert_eq!(decoded, vec![45, 45, 45, 45, 45, 65, 45, 45, 45, 66]); + } + + #[test] + fn decode_flate_zlib() { + let input = [ + 0x78, 0x9c, 0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0x7, 0x0, 0x5, 0x8c, 0x1, 0xf5, + ]; + + let decoded = flate::decode(&input, &Dict::default(), usize::MAX).unwrap(); + assert_eq!(decoded, b"Hello"); + } + + #[test] + fn decode_flate() { + let input = [0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0x7, 0x0]; + + let decoded = flate::decode(&input, &Dict::default(), usize::MAX).unwrap(); + assert_eq!(decoded, b"Hello"); + } + + fn predictor_expected() -> Vec { + vec![ + // Row 1 + 127, 127, 127, 125, 129, 127, 123, 130, 128, + // Row 2 + 128, 129, 126, 126, 132, 124, 121, 127, 126, + // Row 3 + 131, 130, 122, 133, 129, 128, 127, 100, 126, + ] + } + + fn predictor_test(predictor: u8, input: &[u8]) { + let params = PredictorParams { + predictor, + colors: 3, + bits_per_component: 8, + columns: 3, + early_change: false, + }; + + let expected = predictor_expected(); + let out = apply_predictor(input.to_vec(), ¶ms).unwrap(); + + assert_eq!(expected, out); + } + + #[test] + fn predictor_sub() { + predictor_test( + 11, + &[ + // Row 1 + 1, 127, 127, 127, 254, 2, 0, 254, 1, 1, + // Row 2 + 1, 128, 129, 126, 254, 3, 254, 251, 251, 2, + // Row 3 + 1, 131, 130, 122, 2, 255, 6, 250, 227, 254, + ], + ); + } + + #[test] + fn predictor_up() { + predictor_test( + 12, + &[ + // Row 1 + 2, 127, 127, 127, 125, 129, 127, 123, 130, 128, + // Row 2 + 2, 1, 2, 255, 1, 3, 253, 254, 253, 254, + // Row 3 + 2, 3, 1, 252, 7, 253, 4, 6, 229, 0, + ], + ); + } + + #[test] + fn predictor_avg() { + predictor_test( + 13, + &[ + // Row 1 + 3, 127, 127, 127, 62, 66, 64, 61, 66, 65, + // Row 2 + 3, 65, 66, 63, 0, 3, 254, 253, 252, 0, + // Row 3 + 3, 67, 66, 59, 5, 254, 5, 0, 228, 255, + ], + ); + } + + #[test] + fn predictor_paeth() { + predictor_test( + 14, + &[ + // Row 1 + 4, 127, 127, 127, 254, 2, 0, 254, 1, 1, + // Row 2 + 4, 1, 2, 255, 1, 3, 254, 254, 251, 2, + // Row 3 + 4, 3, 1, 252, 5, 253, 6, 1, 229, 254, + ], + ); + } +} diff --git a/vendor/hayro-syntax/src/filter/mod.rs b/vendor/hayro-syntax/src/filter/mod.rs new file mode 100644 index 00000000..90ffd76e --- /dev/null +++ b/vendor/hayro-syntax/src/filter/mod.rs @@ -0,0 +1,173 @@ +//! Results from decoding filtered data streams. + +mod ascii_85; +pub(crate) mod ascii_hex; +#[cfg(feature = "images")] +mod ccitt; +#[cfg(feature = "images")] +mod dct; +#[cfg(feature = "images")] +mod jbig2; +#[cfg(feature = "images")] +mod jpx; +mod lzw_flate; +mod run_length; + +// PdfCraft patch: the incremental Flate decoder that `page::ContentReader` drives. +pub(crate) use lzw_flate::flate::{FlateState, is_plain as flate_is_plain}; + +use crate::object::Dict; +use crate::object::Name; +use crate::object::dict::keys::*; +use crate::object::stream::{DecodeFailure, FilterResult, ImageDecodeParams}; +use core::ops::Deref; + +/// A data filter. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Filter { + /// ASCII hexadecimal encoding. + AsciiHexDecode, + /// ASCII base-85 encoding. + Ascii85Decode, + /// Lempel-Ziv-Welch (LZW) compression. + LzwDecode, + /// DEFLATE compression (zlib/gzip). + FlateDecode, + /// Run-length encoding compression. + RunLengthDecode, + /// CCITT Group 3 or Group 4 fax compression. + CcittFaxDecode, + /// JBIG2 compression for bi-level images. + Jbig2Decode, + /// JPEG (DCT) compression. + DctDecode, + /// JPEG 2000 compression. + JpxDecode, + /// Encryption filter. + Crypt, +} + +impl Filter { + fn debug_name(&self) -> &'static str { + match self { + Self::AsciiHexDecode => "ascii_hex", + Self::Ascii85Decode => "ascii_85", + Self::LzwDecode => "lzw", + Self::FlateDecode => "flate", + Self::RunLengthDecode => "run-length", + Self::CcittFaxDecode => "ccit_fax", + Self::Jbig2Decode => "jbig2", + Self::DctDecode => "dct", + Self::JpxDecode => "jpx", + Self::Crypt => "crypt", + } + } + + pub(crate) fn from_name(name: Name<'_>) -> Option { + match name.deref() { + ASCII_HEX_DECODE | ASCII_HEX_DECODE_ABBREVIATION => Some(Self::AsciiHexDecode), + ASCII85_DECODE | ASCII85_DECODE_ABBREVIATION => Some(Self::Ascii85Decode), + LZW_DECODE | LZW_DECODE_ABBREVIATION => Some(Self::LzwDecode), + FLATE_DECODE | FLATE_DECODE_ABBREVIATION => Some(Self::FlateDecode), + RUN_LENGTH_DECODE | RUN_LENGTH_DECODE_ABBREVIATION => Some(Self::RunLengthDecode), + CCITTFAX_DECODE | CCITTFAX_DECODE_ABBREVIATION => Some(Self::CcittFaxDecode), + JBIG2_DECODE => Some(Self::Jbig2Decode), + DCT_DECODE | DCT_DECODE_ABBREVIATION => Some(Self::DctDecode), + JPX_DECODE => Some(Self::JpxDecode), + CRYPT => Some(Self::Crypt), + _ => { + warn!("unknown filter: {}", name.as_str()); + + None + } + } + } + + /// PdfCraft patch: `limit` is the most bytes the filter may produce (see `decode_limit`). + pub(crate) fn apply( + &self, + data: &[u8], + params: &Dict<'_>, + #[cfg_attr(not(feature = "images"), allow(unused))] image_params: &ImageDecodeParams, + limit: usize, + ) -> Result, DecodeFailure> { + let res = match self { + Self::AsciiHexDecode => ascii_hex::decode(data) + .map(FilterResult::from_data) + .ok_or(DecodeFailure::StreamDecode), + Self::Ascii85Decode => ascii_85::decode(data) + .map(FilterResult::from_data) + .ok_or(DecodeFailure::StreamDecode), + Self::RunLengthDecode => run_length::decode(data, limit) + .map(FilterResult::from_data) + .ok_or(DecodeFailure::StreamDecode), + Self::LzwDecode => lzw_flate::lzw::decode(data, params, limit) + .map(FilterResult::from_data) + .ok_or(DecodeFailure::StreamDecode), + Self::FlateDecode => lzw_flate::flate::decode(data, params, limit) + .map(FilterResult::from_data) + .ok_or(DecodeFailure::StreamDecode), + #[cfg(feature = "images")] + Self::DctDecode => dct::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode), + #[cfg(feature = "images")] + Self::CcittFaxDecode => ccitt::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode), + #[cfg(feature = "images")] + Self::Jbig2Decode => jbig2::decode(data, params, image_params).ok_or(DecodeFailure::ImageDecode), + #[cfg(feature = "images")] + Self::JpxDecode => jpx::decode(data, image_params).ok_or(DecodeFailure::ImageDecode), + #[cfg(not(feature = "images"))] + Self::DctDecode | Self::CcittFaxDecode | Self::Jbig2Decode | Self::JpxDecode => { + warn!("image decoding is not supported (enable the `images` feature)"); + Err(DecodeFailure::ImageDecode) + } + _ => Err(DecodeFailure::StreamDecode), + }; + + if res.is_err() { + warn!("failed to apply filter {}", self.debug_name()); + } + + res + } +} + +/// PdfCraft patch: the most bytes Flate, LZW or RunLength may produce for a stream that isn't an +/// image of known size: content streams, fonts, forms, ICC profiles. These filters expand without +/// limit (Flate about 1000:1), so a 2 MB page that inflated to 2 GB took the renderer past 3 GB in +/// under a second. They never write past the limit (the buffer can still grow to under twice +/// it). Real streams are far smaller; the same figure bounds lopdf's load-time decoding. +pub const MAX_DECODED_STREAM: usize = 256 << 20; + +/// PdfCraft patch: the most bytes they may produce for an image of known size, however large it +/// says it is. +const MAX_DECODED_IMAGE: u64 = 1 << 30; + +/// PdfCraft patch: the output limit for one filter of a stream decoded with `params`; `last` +/// says whether it is the last filter. The last filter of an image whose size, bits per +/// component and components are known produces its pixels, plus at most one PNG predictor tag +/// byte per pixel byte (a predictor row can be as short as one byte): bytes past that are never +/// drawn. Earlier filters produce compressed data, which can be larger than the pixels, and +/// everything else gets the document's ordinary stream limit (at most [`MAX_DECODED_STREAM`]). +/// Decoding stops at the limit and keeps what it +/// has. +pub(crate) fn decode_limit(params: &ImageDecodeParams, last: bool, limits: crate::DecodeLimits) -> usize { + let (true, Some(bpc), Some(components)) = (last, params.bpc, params.num_components) else { + return limits.max_decoded_stream(); + }; + if params.width == 0 || params.height == 0 { + return limits.max_decoded_stream(); + } + let bits = u64::from(params.width) * u64::from(components) * u64::from(bpc); + let pixels = bits.div_ceil(8).saturating_mul(u64::from(params.height)); + let image = pixels.saturating_mul(2).min(MAX_DECODED_IMAGE); + usize::try_from(image).unwrap_or(MAX_DECODED_STREAM) +} + +/// PdfCraft patch: whether a CCITT image of `columns` ร— `rows` may be decoded. The decoder sizes +/// its line buffers from `/Columns`, which a fuzzed file set to 4294967295 (a 4 GiB allocation); +/// real fax lines are a few thousand pixels wide. +pub fn ccitt_size_ok(columns: u32, rows: u32) -> bool { + const MAX_COLUMNS: u32 = 1 << 20; + const MAX_PIXELS: u64 = 1 << 28; + columns > 0 && columns <= MAX_COLUMNS && u64::from(columns) * u64::from(rows.max(1)) <= MAX_PIXELS +} diff --git a/vendor/hayro-syntax/src/filter/run_length.rs b/vendor/hayro-syntax/src/filter/run_length.rs new file mode 100644 index 00000000..2bef8e70 --- /dev/null +++ b/vendor/hayro-syntax/src/filter/run_length.rs @@ -0,0 +1,55 @@ +use crate::reader::Reader; +use alloc::vec; +use alloc::vec::Vec; + +/// PdfCraft patch: `limit` is the most bytes produced (see `filter::decode_limit`); a run of 128 +/// copies costs two input bytes, so a few megabytes could ask for gigabytes. +pub(crate) fn decode(data: &[u8], limit: usize) -> Option> { + let mut reader = Reader::new(data); + let mut decoded = vec![]; + + loop { + let left = limit.saturating_sub(decoded.len()); + if left == 0 { + break; + } + let length = reader.read_byte()?; + + match length { + 128 => break, + 0..=127 => { + // PDFBOX-3990, just abort early if stream is invalid. + let Some(bytes) = reader.read_bytes(length as usize + 1) else { + break; + }; + + decoded.extend(bytes.iter().take(left)); + } + _ => { + let length = (257 - length as usize).min(left); + let byte = reader.read_byte()?; + decoded.extend(core::iter::repeat_n(byte, length)); + } + } + } + + // PdfCraft patch: output cut at the limit is reported, never dropped silently. + if decoded.len() >= limit { + warn!("run-length stream stopped at its decode limit of {limit} bytes"); + } + Some(decoded) +} + +#[cfg(test)] +mod tests { + use crate::filter::run_length::decode; + + #[test] + fn run_length() { + let input = vec![4, 10, 11, 12, 13, 14, 253, 3, 128]; + assert_eq!( + decode(&input, usize::MAX).unwrap(), + vec![10, 11, 12, 13, 14, 3, 3, 3, 3] + ); + } +} diff --git a/vendor/hayro-syntax/src/lib.rs b/vendor/hayro-syntax/src/lib.rs new file mode 100644 index 00000000..99c203c4 --- /dev/null +++ b/vendor/hayro-syntax/src/lib.rs @@ -0,0 +1,103 @@ +/*! +A low-level library for reading PDF files. + +This crate implements the `Syntax` chapter of the PDF reference, and therefore +serves as a very good basis for building various abstractions on top of it, without having to reimplement +the PDF parsing logic. + +This crate does not provide more high-level functionality, such as parsing fonts or color spaces. +Such functionality is out-of-scope for `hayro-syntax`, since this crate is supposed to be +as *light-weight* and *application-agnostic* as possible. + +Functionality-wise, this crate is therefore close to feature-complete. The main missing feature +is support for password-protected documents. In addition to that, more low-level APIs might be +added in the future. + +The crate is `no_std` compatible but requires an allocator to be available. + +# Example +This short example shows you how to load a PDF file and iterate over the content streams of all +pages. +```rust +use hayro_syntax::Pdf; +// A small synthetic document; applications ordinarily read these bytes from a file. +let data = 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 = Pdf::new(data.to_vec()).unwrap(); + +// First access all pages, and then iterate over the operators of each page's +// content stream and print them. +let pages = pdf.pages(); +for page in pages.iter() { + let mut ops = page.typed_operations(); + + while let Some(op) = ops.next() { + println!("{op:?}"); + } +} +``` + +# Safety +When the `unsafe` feature is disabled, there is only use of `unsafe` in `hayro-syntax`. +We also unconditionally use the `smallvec` crate which uses unsafe internally, but that's +it. + +For better performance, it is strongly recommended to enable the `unsafe` feature, +which does result in slightly more unsafe code being, but does give better performance: +- We will use the `flate2` crate for decoding flate streams. +- SIMD will be enabled to accelerate decoding of images. +- `memchr` will be used to accelerate some of the fallback parsing code. + +# Features +The supported features include: +- Parsing xref tables in all its possible formats, including xref streams. +- Best-effort attempt at repairing PDF files with broken xref tables. +- Parsing of all objects types (also in object streams). +- Parsing and decoding PDF streams. +- Iterating over pages as well as their content streams in a typed or untyped fashion. +- The crate is very lightweight, especially in comparison to other PDF crates. + +# Limitations +- There are still a few features missing, for example, support for + password-protected PDFs. In addition to that, many properties (like page annotations) are + currently not exposed. +- This crate is for read-only processing, you cannot directly use it to manipulate PDF files. + If you need to do that, there are other crates in the Rust ecosystem that are suitable for this. +*/ + +#![cfg_attr(not(feature = "std"), no_std)] +#![deny(missing_docs)] + +extern crate alloc; + +#[macro_use] +mod log; + +pub(crate) mod math; +pub(crate) mod sync; + +mod data; +pub(crate) mod filter; +pub(crate) mod pdf; +pub(crate) mod trivia; +pub(crate) mod util; + +pub mod content; +mod crypto; +pub mod metadata; +pub mod object; +pub mod page; +pub mod transform; +pub mod xref; + +// We only expose them so hayro-interpret can use them, but they are not intended +// to be used by others +#[doc(hidden)] +pub mod bit_reader; +#[doc(hidden)] +pub mod byte_reader; +#[doc(hidden)] +pub mod reader; + +pub use data::PdfData; +pub use filter::*; +pub use pdf::*; diff --git a/vendor/hayro-syntax/src/log.rs b/vendor/hayro-syntax/src/log.rs new file mode 100644 index 00000000..f359cbe2 --- /dev/null +++ b/vendor/hayro-syntax/src/log.rs @@ -0,0 +1,40 @@ +//! Logging macros that optionally forward to the `log` crate. + +macro_rules! debug { + ($fmt:literal $(, $($arg:expr),* $(,)?)?) => {{ + #[cfg(feature = "logging")] + { + ::log::debug!($fmt $(, $($arg),*)?); + } + #[cfg(not(feature = "logging"))] + { + $($(let _ = &$arg;)*)? + } + }}; +} + +macro_rules! error { + ($fmt:literal $(, $($arg:expr),* $(,)?)?) => {{ + #[cfg(feature = "logging")] + { + ::log::error!($fmt $(, $($arg),*)?); + } + #[cfg(not(feature = "logging"))] + { + $($(let _ = &$arg;)*)? + } + }}; +} + +macro_rules! warn { + ($fmt:literal $(, $($arg:expr),* $(,)?)?) => {{ + #[cfg(feature = "logging")] + { + ::log::warn!($fmt $(, $($arg),*)?); + } + #[cfg(not(feature = "logging"))] + { + $($(let _ = &$arg;)*)? + } + }}; +} diff --git a/vendor/hayro-syntax/src/math.rs b/vendor/hayro-syntax/src/math.rs new file mode 100644 index 00000000..12d4724f --- /dev/null +++ b/vendor/hayro-syntax/src/math.rs @@ -0,0 +1,36 @@ +// Note that these polyfills can be very imprecise, but hopefully good enough +// for the vast majority of cases. + +#[inline(always)] +pub(crate) fn trunc_f64(x: f64) -> f64 { + #[cfg(feature = "std")] + { + x.trunc() + } + #[cfg(not(feature = "std"))] + { + x as i64 as f64 + } +} + +#[inline(always)] +pub(crate) fn powi_f64(x: f64, n: u32) -> f64 { + #[cfg(feature = "std")] + { + x.powi(n as i32) + } + #[cfg(not(feature = "std"))] + { + let mut result = 1.0; + let mut base = x; + let mut exp = n; + while exp > 0 { + if exp & 1 == 1 { + result *= base; + } + base *= base; + exp >>= 1; + } + result + } +} diff --git a/vendor/hayro-syntax/src/metadata.rs b/vendor/hayro-syntax/src/metadata.rs new file mode 100644 index 00000000..5b86e7d4 --- /dev/null +++ b/vendor/hayro-syntax/src/metadata.rs @@ -0,0 +1,43 @@ +//! Reading document metadata. + +use crate::object::DateTime; +use alloc::vec::Vec; + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +/// The metadata of a PDF document. +pub struct Metadata { + /// The creation date of the document. + pub creation_date: Option, + /// The modification date of the document. + pub modification_date: Option, + /// The title of the document. + /// + /// In the vast majority of cases, this is going to be an ASCII string, but it doesn't have to + /// be. + pub title: Option>, + /// The author of the document. + /// + /// In the vast majority of cases, this is going to be an ASCII string, but it doesn't have to + /// be. + pub author: Option>, + /// The subject of the document. + /// + /// In the vast majority of cases, this is going to be an ASCII string, but it doesn't have to + /// be. + pub subject: Option>, + /// The keywords of the document. + /// + /// In the vast majority of cases, this is going to be an ASCII string, but it doesn't have to + /// be. + pub keywords: Option>, + /// The creator of the document. + /// + /// In the vast majority of cases, this is going to be an ASCII string, but it doesn't have to + /// be. + pub creator: Option>, + /// The producer of the document. + /// + /// In the vast majority of cases, this is going to be an ASCII string, but it doesn't have to + /// be. + pub producer: Option>, +} diff --git a/vendor/hayro-syntax/src/object/array.rs b/vendor/hayro-syntax/src/object/array.rs new file mode 100644 index 00000000..c62b460e --- /dev/null +++ b/vendor/hayro-syntax/src/object/array.rs @@ -0,0 +1,443 @@ +//! Arrays. + +use crate::object::macros::object; +use crate::object::r#ref::MaybeRef; +use crate::object::{FromBytes, Object, ObjectLike}; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter}; +use core::marker::PhantomData; +use smallvec::SmallVec; + +/// An array of PDF objects. +#[derive(Clone)] +pub struct Array<'a> { + data: &'a [u8], + ctx: ReaderContext<'a>, +} + +// Note that this is not structural equality, i.e. two arrays with the same +// items are still considered different if they have different whitespaces. +impl PartialEq for Array<'_> { + fn eq(&self, other: &Self) -> bool { + self.data == other.data + } +} + +impl<'a> Array<'a> { + /// Returns an iterator over the objects of the array. + pub fn raw_iter(&self) -> ArrayIter<'a> { + ArrayIter::new(self.data, &self.ctx) + } + + /// Returns an iterator over the resolved objects of the array. + #[allow( + private_bounds, + reason = "users shouldn't be able to implement `ObjectLike` for custom objects." + )] + pub fn iter(&self) -> ResolvedArrayIter<'a, T> + where + T: ObjectLike<'a>, + { + ResolvedArrayIter::new(self.data, &self.ctx) + } + + /// Return a flex iterator over the items in the array. + pub fn flex_iter(&self) -> FlexArrayIter<'a> { + FlexArrayIter::new(self.data, &self.ctx) + } + + /// Return the raw data of the array. + pub fn data(&self) -> &'a [u8] { + self.data + } +} + +impl Debug for Array<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let mut debug_list = f.debug_list(); + + self.raw_iter().for_each(|i| { + debug_list.entry(&i); + }); + + Ok(()) + } +} + +object!(Array<'a>, Array); + +impl Skippable for Array<'_> { + fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()> { + r.forward_tag(b"[")?; + r.enter_container()?; + + // PdfCraft patch: leave even when an element's skip fails through `?`. + let result = (|| loop { + r.skip_white_spaces_and_comments(); + + if let Some(()) = r.forward_tag(b"]") { + return Some(()); + } else if is_content_stream { + r.skip::>(true)?; + } else { + r.skip::>>(false)?; + } + })(); + r.leave_container(); + result + } +} + +impl Default for Array<'_> { + fn default() -> Self { + Self::from_bytes(b"[]").unwrap() + } +} + +impl<'a> Readable<'a> for Array<'a> { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let bytes = r.skip::>(ctx.in_content_stream())?; + + Some(Self { + data: &bytes[1..bytes.len() - 1], + ctx: ctx.clone(), + }) + } +} + +/// An iterator over the items of an array. +pub struct ArrayIter<'a> { + reader: Reader<'a>, + ctx: ReaderContext<'a>, +} + +impl<'a> ArrayIter<'a> { + fn new(data: &'a [u8], ctx: &ReaderContext<'a>) -> Self { + Self { + reader: Reader::new(data), + ctx: ctx.clone(), + } + } +} + +impl<'a> Iterator for ArrayIter<'a> { + type Item = MaybeRef>; + + fn next(&mut self) -> Option { + self.reader.skip_white_spaces_and_comments(); + + if !self.reader.at_end() { + // Validation and object parsing can differ on malformed input. End + // iteration on failure rather than relying on that parser invariant. + let item = self.reader.read_with_context::>>(&self.ctx); + if item.is_none() { + self.reader.jump_to_end(); + } + return item; + } + + None + } +} + +/// An iterator over the array that resolves objects of a specific type. +pub struct ResolvedArrayIter<'a, T> { + flex_iter: FlexArrayIter<'a>, + phantom_data: PhantomData, +} + +impl<'a, T> ResolvedArrayIter<'a, T> { + fn new(data: &'a [u8], ctx: &ReaderContext<'a>) -> Self { + Self { + flex_iter: FlexArrayIter::new(data, ctx), + phantom_data: PhantomData, + } + } +} + +impl<'a, T> Iterator for ResolvedArrayIter<'a, T> +where + T: ObjectLike<'a>, +{ + type Item = T; + + fn next(&mut self) -> Option { + self.flex_iter.next::() + } +} + +/// An iterator over the array that allows reading a different object each time. +pub struct FlexArrayIter<'a> { + reader: Reader<'a>, + ctx: ReaderContext<'a>, +} + +impl<'a> FlexArrayIter<'a> { + fn new(data: &'a [u8], ctx: &ReaderContext<'a>) -> Self { + Self { + reader: Reader::new(data), + ctx: ctx.clone(), + } + } + + #[allow( + private_bounds, + reason = "users shouldn't be able to implement `ObjectLike` for custom objects." + )] + #[allow(clippy::should_implement_trait)] + /// Try reading the next item as a specific object from the array. + pub fn next>(&mut self) -> Option { + self.reader.skip_white_spaces_and_comments(); + + if !self.reader.at_end() { + return match self.reader.read_with_context::>(&self.ctx)? { + MaybeRef::Ref(r) => self.ctx.xref().get_with::(r.into(), &self.ctx), + MaybeRef::NotRef(i) => Some(i), + }; + } + + None + } +} + +impl<'a, T: ObjectLike<'a> + Copy + Default, const C: usize> TryFrom> for [T; C] { + type Error = (); + + fn try_from(value: Array<'a>) -> Result { + let mut iter = value.iter::(); + + let mut val = [T::default(); C]; + + for i in 0..C { + val[i] = iter.next().ok_or(())?; + } + + if iter.next().is_some() { + warn!("found excess elements in array"); + + return Err(()); + } + + Ok(val) + } +} + +impl<'a, T: ObjectLike<'a> + Copy + Default, const C: usize> TryFrom> for [T; C] +where + [T; C]: TryFrom, Error = ()>, +{ + type Error = (); + + fn try_from(value: Object<'a>) -> Result { + match value { + Object::Array(a) => a.try_into(), + _ => Err(()), + } + } +} + +impl<'a, T: ObjectLike<'a> + Copy + Default, const C: usize> Readable<'a> for [T; C] { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let array = Array::read(r, ctx)?; + array.try_into().ok() + } +} + +impl<'a, T: ObjectLike<'a> + Copy + Default, const C: usize> ObjectLike<'a> for [T; C] {} + +impl<'a, T: ObjectLike<'a>> TryFrom> for Vec { + type Error = (); + + fn try_from(value: Array<'a>) -> Result { + Ok(value.iter::().collect()) + } +} + +impl<'a, T: ObjectLike<'a>> TryFrom> for Vec { + type Error = (); + + fn try_from(value: Object<'a>) -> Result { + match value { + Object::Array(a) => a.try_into(), + _ => Err(()), + } + } +} + +impl<'a, T: ObjectLike<'a>> Readable<'a> for Vec { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let array = Array::read(r, ctx)?; + array.try_into().ok() + } +} + +impl<'a, T: ObjectLike<'a>> ObjectLike<'a> for Vec {} + +impl<'a, U: ObjectLike<'a>, T: ObjectLike<'a> + smallvec::Array> TryFrom> + for SmallVec +{ + type Error = (); + + fn try_from(value: Array<'a>) -> Result { + Ok(value.iter::().collect()) + } +} + +impl<'a, U: ObjectLike<'a>, T: ObjectLike<'a> + smallvec::Array> TryFrom> + for SmallVec +{ + type Error = (); + + fn try_from(value: Object<'a>) -> Result { + match value { + Object::Array(a) => a.try_into(), + _ => Err(()), + } + } +} + +impl<'a, U: ObjectLike<'a>, T: ObjectLike<'a> + smallvec::Array> Readable<'a> + for SmallVec +{ + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let array = Array::read(r, ctx)?; + array.try_into().ok() + } +} + +impl<'a, U: ObjectLike<'a>, T: ObjectLike<'a> + smallvec::Array> ObjectLike<'a> + for SmallVec +where + U: Clone, + U: Debug, +{ +} + +#[cfg(test)] +mod tests { + use crate::object::Array; + use crate::object::Object; + use crate::object::r#ref::{MaybeRef, ObjRef}; + use crate::reader::Reader; + use crate::reader::{ReaderContext, ReaderExt}; + use crate::xref::XRef; + + fn array_impl(data: &[u8]) -> Option>> { + Reader::new(data) + .read_with_context::>(&ReaderContext::new(XRef::dummy(), false)) + .map(|a| a.iter::>().collect::>()) + } + + fn array_ref_impl(data: &[u8]) -> Option>>> { + Reader::new(data) + .read_with_context::>(&ReaderContext::new(XRef::dummy(), false)) + .map(|a| a.raw_iter().collect::>()) + } + + #[test] + fn raw_iteration_accepts_standalone_plus_and_preserves_later_items() { + // The array validator accepts '+', but the old Number reader rejected it, + // making raw iteration panic before reaching the following number. + for bytes in [b"[+ 3]".as_slice(), b"[- 3]", b"[. 3]", b"[+. 3]", b"[-. 3]"] { + let values = array_ref_impl(bytes).unwrap(); + assert_eq!(values.len(), 2); + assert!(matches!(values[0], MaybeRef::NotRef(Object::Number(n)) if n.as_i64() == 0)); + assert!(matches!(values[1], MaybeRef::NotRef(Object::Number(n)) if n.as_i64() == 3)); + } + } + + #[test] + fn raw_iteration_ends_on_object_parse_failure_without_panicking() { + // Exercise the defensive branch directly, independently of validation. + let mut iter = super::ArrayIter::new(b"+abc 3", ReaderContext::dummy_ref()); + assert!(iter.next().is_none()); + assert!(iter.reader.at_end()); + assert!(iter.next().is_none()); + } + + #[test] + fn empty_array_1() { + let res = array_impl(b"[]").unwrap(); + assert!(res.is_empty()); + } + + #[test] + fn empty_array_2() { + let res = array_impl(b"[ \n]").unwrap(); + assert!(res.is_empty()); + } + + #[test] + fn array_1() { + let res = array_impl(b"[34]").unwrap(); + assert!(matches!(res[0], Object::Number(_))); + } + + #[test] + fn array_2() { + let res = array_impl(b"[true ]").unwrap(); + assert!(matches!(res[0], Object::Boolean(_))); + } + + #[test] + fn array_3() { + let res = array_impl(b"[true \n false 34.564]").unwrap(); + assert!(matches!(res[0], Object::Boolean(_))); + assert!(matches!(res[1], Object::Boolean(_))); + assert!(matches!(res[2], Object::Number(_))); + } + + #[test] + fn array_4() { + let res = array_impl(b"[(A string.) << /Hi 34.35 >>]").unwrap(); + assert!(matches!(res[0], Object::String(_))); + assert!(matches!(res[1], Object::Dict(_))); + } + + #[test] + fn array_5() { + let res = array_impl(b"[[32] 345.6]").unwrap(); + assert!(matches!(res[0], Object::Array(_))); + assert!(matches!(res[1], Object::Number(_))); + } + + #[test] + fn array_with_ref() { + let res = array_ref_impl(b"[345 34 5 R 34.0]").unwrap(); + assert!(matches!(res[0], MaybeRef::NotRef(Object::Number(_)))); + assert!(matches!( + res[1], + MaybeRef::Ref(ObjRef { + obj_number: 34, + gen_number: 5 + }) + )); + assert!(matches!(res[2], MaybeRef::NotRef(Object::Number(_)))); + } + + #[test] + fn malformed_ref_is_not_valid_array() { + assert!(array_ref_impl(b"[8 0R]").is_none()); + } + + #[test] + fn array_with_single_number_is_valid() { + assert!(array_ref_impl(b"[345345345]").is_some()); + } + + #[test] + fn array_with_comment() { + let res = array_impl(b"[true % A comment \n false]").unwrap(); + assert!(matches!(res[0], Object::Boolean(_))); + assert!(matches!(res[1], Object::Boolean(_))); + } + + #[test] + fn array_with_trailing() { + let res = array_impl(b"[(Hi) /Test]trialing data").unwrap(); + assert!(matches!(res[0], Object::String(_))); + assert!(matches!(res[1], Object::Name(_))); + } +} diff --git a/vendor/hayro-syntax/src/object/bool.rs b/vendor/hayro-syntax/src/object/bool.rs new file mode 100644 index 00000000..8cfb3afb --- /dev/null +++ b/vendor/hayro-syntax/src/object/bool.rs @@ -0,0 +1,61 @@ +//! Booleans. + +use crate::object::Object; +use crate::object::macros::object; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; + +impl Skippable for bool { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + match r.peek_byte()? { + b't' => r.forward_tag(b"true"), + b'f' => r.forward_tag(b"false"), + _ => None, + } + } +} + +impl Readable<'_> for bool { + fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option { + match r.skip::(true)? { + b"true" => Some(true), + b"false" => Some(false), + _ => None, + } + } +} + +object!(bool, Boolean); + +#[cfg(test)] +mod tests { + use crate::reader::Reader; + use crate::reader::ReaderExt; + + #[test] + fn bool_true() { + assert!( + Reader::new("true".as_bytes()) + .read_without_context::() + .unwrap() + ); + } + + #[test] + fn bool_false() { + assert!( + !Reader::new("false".as_bytes()) + .read_without_context::() + .unwrap() + ); + } + + #[test] + fn bool_trailing() { + assert!( + Reader::new("trueabdf".as_bytes()) + .read_without_context::() + .unwrap() + ); + } +} diff --git a/vendor/hayro-syntax/src/object/date.rs b/vendor/hayro-syntax/src/object/date.rs new file mode 100644 index 00000000..364d34d5 --- /dev/null +++ b/vendor/hayro-syntax/src/object/date.rs @@ -0,0 +1,230 @@ +use crate::byte_reader::Reader; +use core::str::FromStr; + +/// A date time. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct DateTime { + /// The year. + pub year: u16, + /// The year. + pub month: u8, + /// The day. + pub day: u8, + /// The hour. + pub hour: u8, + /// The minute. + pub minute: u8, + /// The second. + pub second: u8, + /// The offset in hours from UTC. + pub utc_offset_hour: i8, + /// The offset in minutes from UTC. + pub utc_offset_minute: u8, +} + +impl DateTime { + pub(crate) fn from_bytes(bytes: &[u8]) -> Option { + let mut reader = Reader::new(bytes); + + reader.forward_tag(b"D:")?; + + let read_num = |reader: &mut Reader<'_>, bytes: u8, min: u16, max: u16| -> Option { + if matches!(reader.peek_byte()?, b'-' | b'+' | b'Z') { + return None; + } + + let num = u16::from_str(core::str::from_utf8(reader.read_bytes(bytes as usize)?).ok()?) + .ok()?; + + if num < min || num > max { + return None; + } + + Some(num) + }; + + let year = read_num(&mut reader, 4, 0, 9999)?; + let month = read_num(&mut reader, 2, 1, 12) + .map(|n| n as u8) + .unwrap_or(1); + let day = read_num(&mut reader, 2, 1, 31) + .map(|n| n as u8) + .unwrap_or(1); + let hour = read_num(&mut reader, 2, 0, 23) + .map(|n| n as u8) + .unwrap_or(0); + let minute = read_num(&mut reader, 2, 0, 59) + .map(|n| n as u8) + .unwrap_or(0); + let second = read_num(&mut reader, 2, 0, 59) + .map(|n| n as u8) + .unwrap_or(0); + + let (utc_offset_hour, utc_offset_minute) = if !reader.at_end() { + let multiplier = match reader.read_byte()? { + b'-' => -1, + _ => 1, + }; + + let hour = multiplier + * read_num(&mut reader, 2, 0, 23) + .map(|n| n as i8) + .unwrap_or(0); + reader.forward_tag(b"\'"); + let minute = read_num(&mut reader, 2, 0, 59) + .map(|n| n as u8) + .unwrap_or(0); + + (hour, minute) + } else { + (0, 0) + }; + + Some(Self { + year, + month, + day, + hour, + minute, + second, + utc_offset_hour, + utc_offset_minute, + }) + } +} + +#[cfg(test)] +mod tests { + use super::DateTime; + + #[allow(clippy::too_many_arguments)] + fn dt( + year: u16, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + utc_hour: i8, + utc_minute: u8, + ) -> DateTime { + DateTime { + year, + month, + day, + hour, + minute, + second, + utc_offset_hour: utc_hour, + utc_offset_minute: utc_minute, + } + } + + fn parse(str: &str) -> DateTime { + DateTime::from_bytes(str.as_bytes()).unwrap() + } + + #[test] + fn year_only_defaults() { + assert_eq!(parse("D:2023"), dt(2023, 1, 1, 0, 0, 0, 0, 0)); + } + + #[test] + fn year_month_defaults() { + assert_eq!(parse("D:202312"), dt(2023, 12, 1, 0, 0, 0, 0, 0)); + } + + #[test] + fn year_month_day_defaults() { + assert_eq!(parse("D:20231225"), dt(2023, 12, 25, 0, 0, 0, 0, 0)); + } + + #[test] + fn ymdh() { + assert_eq!(parse("D:2023122514"), dt(2023, 12, 25, 14, 0, 0, 0, 0)); + } + + #[test] + fn ymdhm() { + assert_eq!(parse("D:202312251430"), dt(2023, 12, 25, 14, 30, 0, 0, 0)); + } + + #[test] + fn full_local_time() { + assert_eq!( + parse("D:20231225143015"), + dt(2023, 12, 25, 14, 30, 15, 0, 0) + ); + } + + #[test] + fn example_from_spec() { + assert_eq!( + parse("D:199812231952-08'00"), + dt(1998, 12, 23, 19, 52, 0, -8, 0) + ); + } + + #[test] + fn positive_offset_with_minutes() { + assert_eq!( + parse("D:20230701120000+05'30"), + dt(2023, 7, 1, 12, 0, 0, 5, 30) + ); + } + + #[test] + fn utc_z() { + assert_eq!(parse("D:20230701120000Z"), dt(2023, 7, 1, 12, 0, 0, 0, 0)); + } + + #[test] + fn utc_z_with_zero_offsets() { + assert_eq!( + parse("D:20230701120000Z00'00"), + dt(2023, 7, 1, 12, 0, 0, 0, 0) + ); + } + + #[test] + fn negative_offset_with_minutes() { + assert_eq!( + parse("D:20230701120000-03'15"), + dt(2023, 7, 1, 12, 0, 0, -3, 15) + ); + } + + #[test] + fn leap_year() { + assert_eq!( + parse("D:20000229010203+01'00"), + dt(2000, 2, 29, 1, 2, 3, 1, 0) + ); + } + + #[test] + fn max_values() { + assert_eq!( + parse("D:99991231235959+14'00"), + dt(9999, 12, 31, 23, 59, 59, 14, 0) + ); + } + + #[test] + fn min_values() { + assert_eq!(parse("D:00000101000000+00'00"), dt(0, 1, 1, 0, 0, 0, 0, 0)); + } + + #[test] + fn offset_hour_only() { + assert_eq!(parse("D:202307011200+02"), dt(2023, 7, 1, 12, 0, 0, 2, 0)); + } + + #[test] + fn offset_negative_zero_hour() { + assert_eq!( + parse("D:202307011200-00'45"), + dt(2023, 7, 1, 12, 0, 0, 0, 45) + ); + } +} diff --git a/vendor/hayro-syntax/src/object/dict.rs b/vendor/hayro-syntax/src/object/dict.rs new file mode 100644 index 00000000..0d945d7a --- /dev/null +++ b/vendor/hayro-syntax/src/object/dict.rs @@ -0,0 +1,1414 @@ +//! Dictionaries. + +use crate::object::macros::object; +use crate::object::r#ref::{MaybeRef, ObjRef}; +use crate::object::{Name, ObjectIdentifier}; +use crate::object::{Object, ObjectLike}; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; +use crate::sync::FxHashMap; +use crate::sync::{Arc, OnceLock}; +use alloc::boxed::Box; +use alloc::format; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter}; +use core::ops::Deref; + +/// A dictionary, which is a key-value map, keys being names, and values being any PDF object or +/// objetc reference. +#[derive(Clone)] +pub struct Dict<'a>(Inner<'a>); + +#[derive(Clone)] +enum Inner<'a> { + Empty, + Some(Arc>), +} + +impl Default for Dict<'_> { + fn default() -> Self { + Self::empty() + } +} + +// Note that this is not structural equality, i.e. two dictionaries with the same +// items are still considered different if they have different whitespaces. +impl PartialEq for Dict<'_> { + fn eq(&self, other: &Self) -> bool { + self.data() == other.data() + } +} + +impl<'a> Dict<'a> { + /// Create a new empty dictionary. + pub fn empty() -> Self { + Self(Inner::Empty) + } + + /// Get the raw bytes underlying to the dictionary. + pub fn data(&self) -> &'a [u8] { + match &self.0 { + Inner::Empty => &[], + Inner::Some(repr) => repr.data, + } + } + + /// Returns the number of entries in the dictionary. + pub fn len(&self) -> usize { + self.offsets().map_or(0, FxHashMap::len) + } + + /// Return whether the dictionary is empty. + pub fn is_empty(&self) -> bool { + self.offsets().is_none_or(FxHashMap::is_empty) + } + + /// Checks whether the dictionary contains an entry with a specific key. + pub fn contains_key(&self, key: impl AsRef<[u8]>) -> bool { + self.offsets() + .is_some_and(|offsets| offsets.contains_key(key.as_ref())) + } + + /// Returns the entry of a key as a specific object, or try to resolve it in case it's + /// an object reference. + #[allow( + private_bounds, + reason = "users shouldn't be able to implement `ObjectLike` for custom objects." + )] + pub fn get(&self, key: impl AsRef<[u8]>) -> Option + where + T: ObjectLike<'a>, + { + self.get_raw::(key.as_ref())?.resolve(self.ctx()) + } + + /// Get the object reference linked to a key. + pub fn get_ref(&self, key: impl AsRef<[u8]>) -> Option { + let offset = *self.offsets()?.get(key.as_ref())?; + + Reader::new(&self.data()[offset..]).read_with_context::(self.ctx()) + } + + /// Returns an iterator over all keys in the dictionary. + pub fn keys(&self) -> impl Iterator> + '_ { + self.offsets() + .into_iter() + .flat_map(|offsets| offsets.keys().cloned()) + } + + /// An iterator over all entries in the dictionary, sorted by key. + pub fn entries(&self) -> impl Iterator, MaybeRef>)> + '_ { + let mut sorted_keys = self.keys().collect::>(); + sorted_keys.sort_by(|n1, n2| n1.as_ref().cmp(n2.as_ref())); + sorted_keys.into_iter().map(|k| { + let obj = self.get_raw(k.deref()).unwrap(); + (k, obj) + }) + } + + /// Return the object identifier of the dict, if it's an indirect object. + pub fn obj_id(&self) -> Option { + self.ctx().obj_number() + } + + /// Return the raw entry for a specific key. + #[allow(private_bounds)] + pub fn get_raw(&self, key: impl AsRef<[u8]>) -> Option> + where + T: Readable<'a>, + { + let offset = *self.offsets()?.get(key.as_ref())?; + + Reader::new(&self.data()[offset..]).read_with_context::>(self.ctx()) + } + + pub(crate) fn ctx(&self) -> &ReaderContext<'a> { + match &self.0 { + Inner::Empty => ReaderContext::dummy_ref(), + Inner::Some(repr) => &repr.ctx, + } + } + + fn offsets(&self) -> Option<&FxHashMap, usize>> { + match &self.0 { + Inner::Empty => None, + Inner::Some(repr) => Some(repr.offsets.get_or_init(|| { + // PdfCraft patch: (#307) large resource dictionaries are read for every + // page at open, but most are never looked up. Index them only on demand. + // The first pass already validated the same immutable bytes with this + // context. Keep the same parser, including duplicate-key and repair rules. + let mut offsets = new_offsets(); + let mut reader = Reader::new(repr.data); + let (start, end): (Option<&[u8]>, &[u8]) = if repr.inline { + (None, b"ID") + } else { + (Some(b"<<"), b">>") + }; + let _ = parse_dict_with(&mut reader, &repr.ctx, start, end, |name, offset, _| { + offsets.insert(name.into_owned(), offset); + Some(()) + }); + offsets + })), + } + } +} + +impl Debug for Dict<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let mut debug_struct = f.debug_struct("Dict"); + + if let Some(offsets) = self.offsets() { + let mut r = Reader::new(self.data()); + + for (key, val) in offsets { + r.jump(*val); + debug_struct.field( + &format!("{:?}", key.as_str()), + &r.read_with_context::>>(&ReaderContext::dummy()) + .unwrap(), + ); + } + } + Ok(()) + } +} + +impl Skippable for Dict<'_> { + fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()> { + r.forward_tag(b"<<")?; + r.enter_container()?; + + // PdfCraft patch: leave even when a value or garbage fails through `?`. + let result = (|| loop { + r.skip_white_spaces_and_comments(); + + if let Some(()) = r.forward_tag(b">>") { + break Some(()); + } else { + let Some(_) = r.skip::>(is_content_stream) else { + // In case there is garbage in-between, be lenient and just try to skip it. + r.skip::>(is_content_stream)?; + continue; + }; + + r.skip_white_spaces_and_comments(); + + if is_content_stream { + r.skip::>(is_content_stream)?; + } else { + r.skip::>>(is_content_stream)?; + } + } + })(); + r.leave_container(); + result + } +} + +impl<'a> Readable<'a> for Dict<'a> { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + read_inner(r, ctx, Some(b"<<"), b">>") + } +} + +// PdfCraft patch: (#307) retain the fast single-pass index for ordinary small +// dictionaries. Above this entry count, validate without retaining a hash table. +const EAGER_DICT_ENTRIES: usize = 32; + +fn new_offsets() -> FxHashMap, usize> { + #[cfg(feature = "std")] + return FxHashMap::with_capacity_and_hasher(8, rustc_hash::FxBuildHasher); + #[cfg(not(feature = "std"))] + FxHashMap::new() +} + +pub(crate) type SharedIndex = Arc, usize>>>; + +#[derive(Debug)] +pub(crate) struct CachedDict { + data: Box<[u8]>, + offsets: SharedIndex, +} + +// PdfCraft patch: share a validated large dictionary's index across pages. The cache +// belongs to the xref, owns its keys, and is bounded even on documents with many unique +// dictionaries. Comparing the bytes also makes address reuse harmless. No reader context +// is cached: decryption, object identity and reference-cycle tracking stay with each read. +#[derive(Debug, Default)] +pub(crate) struct DictCache { + entries: FxHashMap>, + bytes: usize, +} + +impl DictCache { + fn get(&self, data: &[u8]) -> Option> { + self.entries + .get(&(data.as_ptr() as usize)) + .filter(|cached| data.starts_with(&cached.data)) + .cloned() + } + + fn insert(&mut self, data: &[u8], offsets: SharedIndex) { + const MAX_BYTES: usize = 8 << 20; + if self.entries.len() < 256 && data.len() <= MAX_BYTES.saturating_sub(self.bytes) { + self.bytes += data.len(); + self.entries.insert( + data.as_ptr() as usize, + Arc::new(CachedDict { + data: data.into(), + offsets, + }), + ); + } + } +} + +fn read_inner<'a>( + r: &mut Reader<'a>, + ctx: &ReaderContext<'a>, + start_tag: Option<&[u8]>, + end_tag: &[u8], +) -> Option> { + let cacheable = start_tag.is_some() && !ctx.in_content_stream(); + if cacheable { + let tail = r.tail()?; + if let Some(cached) = ctx + .xref() + .with_dict_cache(|cache| cache.get(tail)) + .flatten() + { + let data = r.read_bytes(cached.data.len())?; + return Some(Dict(Inner::Some(Arc::new(Repr { + data, + offsets: cached.offsets.clone(), + inline: false, + ctx: ctx.clone(), + })))); + } + } + let mut offsets = Some(new_offsets()); + + let (data, repaired) = parse_dict_with( + r, + ctx, + start_tag, + end_tag, + #[inline] + |name, offset, _| { + if let Some(index) = &mut offsets { + index.insert(name.into_owned(), offset); + if index.len() > EAGER_DICT_ENTRIES { + offsets = None; + } + } + Some(()) + }, + )?; + + let index = Arc::new(OnceLock::new()); + if let Some(offsets) = offsets { + let _ = index.set(offsets); + } + // Repair may read objects through the current reference context; only cache the + // context-independent validating path. Inline/content dictionaries stay uncached. + if cacheable && !repaired && data.len() >= 1024 { + ctx.xref() + .with_dict_cache(|cache| cache.insert(data, index.clone())); + } + Some(Dict(Inner::Some(Arc::new(Repr { + data, + offsets: index, + inline: start_tag.is_none(), + ctx: ctx.clone(), + })))) +} + +pub(crate) struct DictProbe<'a> { + pub(crate) data: &'a [u8], + pub(crate) has_root: bool, + pub(crate) has_type: bool, +} + +// We use that method during fallback xref parsing, so that we can search for +// the dictionaries we need without allocating the whole hash map. +// If the above TODOs are ever resolved, we can probably remove this method. +pub(crate) fn probe_dict<'a>( + r: &mut Reader<'a>, + ctx: &ReaderContext<'a>, + start_tag: Option<&[u8]>, + end_tag: &[u8], +) -> Option> { + let mut has_root = false; + let mut has_type = false; + + let (data, _) = parse_dict_with( + r, + ctx, + start_tag, + end_tag, + #[inline] + |name, _, _value_reader| { + if name.as_ref() == keys::ROOT { + has_root = true; + } else if name.as_ref() == keys::TYPE { + has_type = true; + } + + Some(()) + }, + )?; + + Some(DictProbe { + data, + has_root, + has_type, + }) +} + +// PdfCraft patch: an object where a key belongs is read, not skipped, so a dictionary +// holding another one there (`<< << โ€ฆ`) recursed without a bound. Count each parsed +// dictionary like a skipped one, and leave on every exit of the parse below. +fn parse_dict_with<'a, F>( + r: &mut Reader<'a>, + ctx: &ReaderContext<'a>, + start_tag: Option<&[u8]>, + end_tag: &[u8], + on_entry: F, +) -> Option<(&'a [u8], bool)> +where + F: FnMut(Name<'a>, usize, &Reader<'a>) -> Option<()>, +{ + // An inline image dictionary has no enclosing << >> pair; count only actual + // containers. Its array/dictionary operands still enter the same depth guard. + let container = start_tag.is_some(); + if container { + r.enter_container()?; + } + let result = parse_dict_with_inner(r, ctx, start_tag, end_tag, on_entry); + if container { + r.leave_container(); + } + result +} + +fn parse_dict_with_inner<'a, F>( + r: &mut Reader<'a>, + ctx: &ReaderContext<'a>, + start_tag: Option<&[u8]>, + end_tag: &[u8], + mut on_entry: F, +) -> Option<(&'a [u8], bool)> +where + F: FnMut(Name<'a>, usize, &Reader<'a>) -> Option<()>, +{ + let dict_data = r.tail()?; + let start_offset = r.offset(); + let mut repaired = false; + + // Inline image dictionaries don't start with '<<'. + if let Some(start_tag) = start_tag { + r.forward_tag(start_tag)?; + } + + loop { + r.skip_white_spaces_and_comments(); + + // Normal dictionaries end with '>>', inline image dictionaries end with BD. + if let Some(()) = r.peek_tag(end_tag) { + r.forward_tag(end_tag)?; + let end_offset = r.offset() - start_offset; + + break Some((&dict_data[..end_offset], repaired)); + } else { + let Some(name) = r.read_without_context::>() else { + if start_tag.is_some() { + // In case there is garbage in-between, be lenient and just try to skip it. + // But only do this if we are parsing a proper dictionary as opposed to an + // inline dictionary. + repaired = true; + r.read::>(ctx)?; + continue; + } else { + return None; + } + }; + r.skip_white_spaces_and_comments(); + + let offset = r.offset() - start_offset; + // Do note that we are including objects in our dictionary even if they + // are the null object, meaning that a call to `contains_key` will return `true` + // even if the object is the null object. The PDF reference in theory requires + // us to treat them as non-existing. Previously, we included a check to determine + // whether the object is `null` before inserting it, but that caused problems + // in some test cases where encryption + object streams are involved (as we would + // attempt to read an object stream before having resolved the encryption dictionary). + on_entry(name, offset, r)?; + + if ctx.in_content_stream() { + r.skip::>(ctx.in_content_stream())?; + } else { + r.skip::>>(ctx.in_content_stream())?; + } + } + } +} + +object!(Dict<'a>, Dict); + +struct Repr<'a> { + data: &'a [u8], + offsets: SharedIndex, + inline: bool, + ctx: ReaderContext<'a>, +} + +pub(crate) struct InlineImageDict<'a>(Dict<'a>); + +impl<'a> InlineImageDict<'a> { + pub(crate) fn get_dict(&self) -> &Dict<'a> { + &self.0 + } +} + +impl<'a> Readable<'a> for InlineImageDict<'a> { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + Some(Self(read_inner(r, ctx, None, b"ID")?)) + } +} + +#[cfg(test)] +mod lazy_index_tests { + use super::*; + use crate::object::{Array, FromBytes}; + use alloc::string::String; + + #[test] + fn inline_image_operands_count_only_their_actual_containers() { + for depth in [63, 64, 65] { + let data = format!("/Value {}0{} ID", "[".repeat(depth), "]".repeat(depth)); + let mut reader = Reader::new(data.as_bytes()); + assert_eq!(InlineImageDict::read(&mut reader, &ReaderContext::dummy()).is_some(), depth <= 64); + } + } + + fn entries() -> String { + let mut data = String::new(); + for i in 0..100 { + data.push_str(&format!("/Key{i} {i} ")); + } + // Escaped and long names, duplicate keys, null and nested values keep + // exactly the same semantics after the second (index-building) pass. + data.push_str("/Key99 123 /Esc#61ped 7 /ThisNameIsLongerThanTheInlineNameBuffer 9 /Nil null /Nested << /N [1 2 3] >> "); + data + } + + #[test] + fn large_indexes_are_deferred_and_clones_share_the_result() { + let data = format!("<< {} >>", entries()); + let dict = Dict::from_bytes(data.as_bytes()).unwrap(); + let Inner::Some(repr) = &dict.0 else { + panic!("nonempty") + }; + assert!(repr.offsets.get().is_none()); + let cloned = dict.clone(); + assert_eq!(cloned.get::(b"Key99"), Some(123)); + assert!(repr.offsets.get().is_some()); + assert_eq!(dict.len(), 104); + assert_eq!(dict.get::(b"Escaped"), Some(7)); + assert_eq!( + dict.get::(b"ThisNameIsLongerThanTheInlineNameBuffer"), + Some(9) + ); + assert!(dict.contains_key(b"Nil")); + assert!(!dict.contains_key(b"Missing")); + assert_eq!( + dict.get::>(b"Nested") + .unwrap() + .get::>(b"N") + .unwrap() + .iter::() + .collect::>(), + [1, 2, 3] + ); + assert_eq!(dict.keys().count(), dict.len()); + assert_eq!(dict.entries().count(), dict.len()); + assert!(core::ptr::eq( + dict.offsets().unwrap(), + cloned.offsets().unwrap() + )); + } + + #[test] + fn small_indexes_stay_eager_and_large_inline_dicts_work() { + let dict = Dict::from_bytes(b"<< /Key 42 >>").unwrap(); + let Inner::Some(repr) = &dict.0 else { + panic!("nonempty") + }; + assert!(repr.offsets.get().is_some()); + assert_eq!(dict.get::(b"Key"), Some(42)); + let data = format!("{} ID", entries()); + let mut reader = Reader::new(data.as_bytes()); + let inline = InlineImageDict::read(&mut reader, &ReaderContext::dummy()).unwrap(); + assert_eq!(inline.0.get::(b"Key99"), Some(123)); + assert_eq!(inline.0.len(), 104); + } + + #[test] + fn large_dicts_are_still_validated_before_any_lookup() { + let data = format!("<< {} /Broken [", entries()); + assert!(Dict::from_bytes(data.as_bytes()).is_none()); + } + + #[test] + fn repeated_resource_reads_share_indexes_but_keep_object_context() { + let data = format!( + "%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources 5 0 R >> endobj\n4 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources 5 0 R >> endobj\n5 0 obj << /XObject << {} >> >> endobj\ntrailer << /Root 1 0 R >>\n%%EOF", + entries() + ); + let pdf = crate::Pdf::new(data.into_bytes()).unwrap(); + let a = pdf + .xref() + .get::>(ObjectIdentifier::new(5, 0)) + .unwrap() + .get::>(b"XObject") + .unwrap(); + let b = pdf + .xref() + .get::>(ObjectIdentifier::new(5, 0)) + .unwrap() + .get::>(b"XObject") + .unwrap(); + let (Inner::Some(a_repr), Inner::Some(b_repr)) = (&a.0, &b.0) else { + panic!("nonempty") + }; + assert!(Arc::ptr_eq(&a_repr.offsets, &b_repr.offsets)); + assert_eq!(a.obj_id(), Some(ObjectIdentifier::new(5, 0))); + assert_eq!(a.get::(b"Key99"), Some(123)); + assert_eq!(b.get::(b"Escaped"), Some(7)); + assert!(core::ptr::eq(a.offsets().unwrap(), b.offsets().unwrap())); + } + + #[test] + fn index_cache_is_bounded_and_rejects_changed_or_truncated_bytes() { + let mut cache = DictCache::default(); + let mut data = format!("<< {} >>", entries()).into_bytes(); + cache.insert(&data, Arc::new(OnceLock::new())); + assert!(cache.get(&data).is_some()); + assert!(cache.get(&data[..data.len() - 1]).is_none()); + data[3] = b'Z'; + assert!(cache.get(&data).is_none()); + let buffers: Vec<_> = (0..300) + .map(|i| format!("<< /Key{i} 1 >>").into_bytes()) + .collect(); + for data in &buffers { + cache.insert(data, Arc::new(OnceLock::new())); + } + assert_eq!(cache.entries.len(), 256); + assert!(cache.bytes <= 8 << 20); + } +} + +/// A collection of possible keys in a PDF dictionary. Copied and adapted from `PDFBox`. +#[allow(missing_docs)] +pub mod keys { + macro_rules! key { + ($i:ident, $e:expr) => { + pub const $i: &'static [u8] = $e; + }; + } + + // A + key!(A, b"A"); + key!(AA, b"AA"); + key!(ABSOLUTE_COLORIMETRIC, b"AbsoluteColorimetric"); + key!(AC, b"AC"); + key!(ACRO_FORM, b"AcroForm"); + key!(ACTUAL_TEXT, b"ActualText"); + key!(ADBE, b"ADBE"); + key!(ADBE_PKCS7_DETACHED, b"adbe.pkcs7.detached"); + key!(ADBE_PKCS7_SHA1, b"adbe.pkcs7.sha1"); + key!(ADBE_X509_RSA_SHA1, b"adbe.x509.rsa_sha1"); + key!(ADOBE_PPKLITE, b"Adobe.PPKLite"); + key!(AESV2, b"AESV2"); + key!(AESV3, b"AESV3"); + key!(AF, b"AF"); + key!(AF_RELATIONSHIP, b"AFRelationship"); + key!(AFTER, b"After"); + key!(AI_META_DATA, b"AIMetaData"); + key!(AIS, b"AIS"); + key!(ALL_OFF, b"AllOff"); + key!(ALL_ON, b"AllOn"); + key!(ALT, b"Alt"); + key!(ALPHA, b"Alpha"); + key!(ALTERNATE, b"Alternate"); + key!(ANNOT, b"Annot"); + key!(ANNOTS, b"Annots"); + key!(ANTI_ALIAS, b"AntiAlias"); + key!(ANY_OFF, b"AnyOff"); + key!(ANY_ON, b"AnyOn"); + key!(AP, b"AP"); + key!(AP_REF, b"APRef"); + key!(APP, b"App"); + key!(ART_BOX, b"ArtBox"); + key!(ARTIFACT, b"Artifact"); + key!(AS, b"AS"); + key!(ASCENT, b"Ascent"); + key!(ASCII_HEX_DECODE, b"ASCIIHexDecode"); + key!(ASCII_HEX_DECODE_ABBREVIATION, b"AHx"); + key!(ASCII85_DECODE, b"ASCII85Decode"); + key!(ASCII85_DECODE_ABBREVIATION, b"A85"); + key!(ATTACHED, b"Attached"); + key!(AUTHOR, b"Author"); + key!(AVG_WIDTH, b"AvgWidth"); + + // B + key!(B, b"B"); + key!(BACKGROUND, b"Background"); + key!(BASE_ENCODING, b"BaseEncoding"); + key!(BASE_FONT, b"BaseFont"); + key!(BASE_STATE, b"BaseState"); + key!(BASE_VERSION, b"BaseVersion"); + key!(BBOX, b"BBox"); + key!(BC, b"BC"); + key!(BE, b"BE"); + key!(BEAD, b"BEAD"); + key!(BEFORE, b"Before"); + key!(BG, b"BG"); + key!(BITS_PER_COMPONENT, b"BitsPerComponent"); + key!(BITS_PER_COORDINATE, b"BitsPerCoordinate"); + key!(BITS_PER_FLAG, b"BitsPerFlag"); + key!(BITS_PER_SAMPLE, b"BitsPerSample"); + key!(BL, b"Bl"); + key!(BLACK_IS_1, b"BlackIs1"); + key!(BLACK_POINT, b"BlackPoint"); + key!(BLEED_BOX, b"BleedBox"); + key!(BM, b"BM"); + key!(BORDER, b"Border"); + key!(BOUNDS, b"Bounds"); + key!(BPC, b"BPC"); + key!(BS, b"BS"); + key!(BTN, b"Btn"); + key!(BYTERANGE, b"ByteRange"); + + // C + key!(C, b"C"); + key!(C0, b"C0"); + key!(C1, b"C1"); + key!(CA, b"CA"); + key!(CA_NS, b"ca"); + key!(CALGRAY, b"CalGray"); + key!(CALRGB, b"CalRGB"); + key!(CALCMYK, b"CalCMYK"); + key!(CAP, b"Cap"); + key!(CAP_HEIGHT, b"CapHeight"); + key!(CATALOG, b"Catalog"); + key!(CCITTFAX_DECODE, b"CCITTFaxDecode"); + key!(CCITTFAX_DECODE_ABBREVIATION, b"CCF"); + key!(CENTER_WINDOW, b"CenterWindow"); + key!(CERT, b"Cert"); + key!(CERTS, b"Certs"); + key!(CF, b"CF"); + key!(CFM, b"CFM"); + key!(CH, b"Ch"); + key!(CHAR_PROCS, b"CharProcs"); + key!(CHAR_SET, b"CharSet"); + key!(CHECK_SUM, b"CheckSum"); + key!(CI, b"CI"); + key!(CICI_SIGNIT, b"CICI.SignIt"); + key!(CID_FONT_TYPE0, b"CIDFontType0"); + key!(CID_FONT_TYPE0C, b"CIDFontType0C"); + key!(CID_FONT_TYPE2, b"CIDFontType2"); + key!(CID_TO_GID_MAP, b"CIDToGIDMap"); + key!(CID_SET, b"CIDSet"); + key!(CIDSYSTEMINFO, b"CIDSystemInfo"); + key!(CL, b"CL"); + key!(CLASS_MAP, b"ClassMap"); + key!(CLR_F, b"ClrF"); + key!(CLR_FF, b"ClrFf"); + key!(CMAP, b"CMap"); + key!(CMAPNAME, b"CMapName"); + key!(CMYK, b"CMYK"); + key!(CO, b"CO"); + key!(COLOR, b"Color"); + key!(COLLECTION, b"Collection"); + key!(COLLECTION_ITEM, b"CollectionItem"); + key!(COLLECTION_FIELD, b"CollectionField"); + key!(COLLECTION_SCHEMA, b"CollectionSchema"); + key!(COLLECTION_SORT, b"CollectionSort"); + key!(COLLECTION_SUBITEM, b"CollectionSubitem"); + key!(COLOR_BURN, b"ColorBurn"); + key!(COLOR_DODGE, b"ColorDodge"); + key!(COLORANTS, b"Colorants"); + key!(COLORS, b"Colors"); + key!(COLORSPACE, b"ColorSpace"); + key!(COLOR_TRANSFORM, b"ColorTransform"); + key!(COLUMNS, b"Columns"); + key!(COMPATIBLE, b"Compatible"); + key!(COMPONENTS, b"Components"); + key!(CONTACT_INFO, b"ContactInfo"); + key!(CONTENTS, b"Contents"); + key!(COORDS, b"Coords"); + key!(COUNT, b"Count"); + key!(CP, b"CP"); + key!(CREATION_DATE, b"CreationDate"); + key!(CREATOR, b"Creator"); + key!(CRL, b"CRL"); + key!(CRLS, b"CRLS"); + key!(CROP_BOX, b"CropBox"); + key!(CRYPT, b"Crypt"); + key!(CS, b"CS"); + key!(CYX, b"CYX"); + + // D + key!(D, b"D"); + key!(DA, b"DA"); + key!(DARKEN, b"Darken"); + key!(DATE, b"Date"); + key!(DCT_DECODE, b"DCTDecode"); + key!(DCT_DECODE_ABBREVIATION, b"DCT"); + key!(DECODE, b"Decode"); + key!(DECODE_PARMS, b"DecodeParms"); + key!(DEFAULT, b"default"); + key!(DEFAULT_CMYK, b"DefaultCMYK"); + key!(DEFAULT_CRYPT_FILTER, b"DefaultCryptFilter"); + key!(DEFAULT_GRAY, b"DefaultGray"); + key!(DEFAULT_RGB, b"DefaultRGB"); + key!(DESC, b"Desc"); + key!(DESCENDANT_FONTS, b"DescendantFonts"); + key!(DESCENT, b"Descent"); + key!(DEST, b"Dest"); + key!(DEST_OUTPUT_PROFILE, b"DestOutputProfile"); + key!(DESTS, b"Dests"); + key!(DEVICE_CMYK, b"DeviceCMYK"); + key!(DEVICE_GRAY, b"DeviceGray"); + key!(DEVICE_N, b"DeviceN"); + key!(DEVICE_RGB, b"DeviceRGB"); + key!(DI, b"Di"); + key!(DIFFERENCE, b"Difference"); + key!(DIFFERENCES, b"Differences"); + key!(DIGEST_METHOD, b"DigestMethod"); + key!(DIGEST_RIPEMD160, b"RIPEMD160"); + key!(DIGEST_SHA1, b"SHA1"); + key!(DIGEST_SHA256, b"SHA256"); + key!(DIGEST_SHA384, b"SHA384"); + key!(DIGEST_SHA512, b"SHA512"); + key!(DIRECTION, b"Direction"); + key!(DISPLAY_DOC_TITLE, b"DisplayDocTitle"); + key!(DL, b"DL"); + key!(DM, b"Dm"); + key!(DOC, b"Doc"); + key!(DOC_CHECKSUM, b"DocChecksum"); + key!(DOC_TIME_STAMP, b"DocTimeStamp"); + key!(DOCMDP, b"DocMDP"); + key!(DOCUMENT, b"Document"); + key!(DOMAIN, b"Domain"); + key!(DOS, b"DOS"); + key!(DP, b"DP"); + key!(DR, b"DR"); + key!(DS, b"DS"); + key!(DSS, b"DSS"); + key!(DUPLEX, b"Duplex"); + key!(DUR, b"Dur"); + key!(DV, b"DV"); + key!(DW, b"DW"); + key!(DW2, b"DW2"); + + // E + key!(E, b"E"); + key!(EARLY_CHANGE, b"EarlyChange"); + key!(EF, b"EF"); + key!(EMBEDDED_FDFS, b"EmbeddedFDFs"); + key!(EMBEDDED_FILE, b"EmbeddedFile"); + key!(EMBEDDED_FILES, b"EmbeddedFiles"); + key!(EMPTY, b""); + key!(ENCODE, b"Encode"); + key!(ENCODED_BYTE_ALIGN, b"EncodedByteAlign"); + key!(ENCODING, b"Encoding"); + key!(ENCODING_90MS_RKSJ_H, b"90ms-RKSJ-H"); + key!(ENCODING_90MS_RKSJ_V, b"90ms-RKSJ-V"); + key!(ENCODING_ETEN_B5_H, b"ETen-B5-H"); + key!(ENCODING_ETEN_B5_V, b"ETen-B5-V"); + key!(ENCRYPT, b"Encrypt"); + key!(ENCRYPT_META_DATA, b"EncryptMetadata"); + key!(ENCRYPTED_PAYLOAD, b"EncryptedPayload"); + key!(END_OF_BLOCK, b"EndOfBlock"); + key!(END_OF_LINE, b"EndOfLine"); + key!(ENTRUST_PPKEF, b"Entrust.PPKEF"); + key!(EXCLUSION, b"Exclusion"); + key!(EXTENSIONS, b"Extensions"); + key!(EXTENSION_LEVEL, b"ExtensionLevel"); + key!(EX_DATA, b"ExData"); + key!(EXPORT, b"Export"); + key!(EXPORT_STATE, b"ExportState"); + key!(EXT_G_STATE, b"ExtGState"); + key!(EXTEND, b"Extend"); + key!(EXTENDS, b"Extends"); + + // F + key!(F, b"F"); + key!(F_DECODE_PARMS, b"FDecodeParms"); + key!(F_FILTER, b"FFilter"); + key!(FB, b"FB"); + key!(FDF, b"FDF"); + key!(FF, b"Ff"); + key!(FIELDS, b"Fields"); + key!(FILESPEC, b"Filespec"); + key!(FILTER, b"Filter"); + key!(FIRST, b"First"); + key!(FIRST_CHAR, b"FirstChar"); + key!(FIT_WINDOW, b"FitWindow"); + key!(FL, b"FL"); + key!(FLAGS, b"Flags"); + key!(FLATE_DECODE, b"FlateDecode"); + key!(FLATE_DECODE_ABBREVIATION, b"Fl"); + key!(FO, b"Fo"); + key!(FOLDERS, b"Folders"); + key!(FONT, b"Font"); + key!(FONT_BBOX, b"FontBBox"); + key!(FONT_DESC, b"FontDescriptor"); + key!(FONT_FAMILY, b"FontFamily"); + key!(FONT_FILE, b"FontFile"); + key!(FONT_FILE2, b"FontFile2"); + key!(FONT_FILE3, b"FontFile3"); + key!(FONT_MATRIX, b"FontMatrix"); + key!(FONT_NAME, b"FontName"); + key!(FONT_STRETCH, b"FontStretch"); + key!(FONT_WEIGHT, b"FontWeight"); + key!(FORM, b"Form"); + key!(FORMTYPE, b"FormType"); + key!(FRM, b"FRM"); + key!(FS, b"FS"); + key!(FT, b"FT"); + key!(FUNCTION, b"Function"); + key!(FUNCTION_TYPE, b"FunctionType"); + key!(FUNCTIONS, b"Functions"); + + // G + key!(G, b"G"); + key!(GAMMA, b"Gamma"); + key!(GROUP, b"Group"); + key!(GTS_PDFA1, b"GTS_PDFA1"); + + // H + key!(H, b"H"); + key!(HARD_LIGHT, b"HardLight"); + key!(HEIGHT, b"Height"); + key!(HELV, b"Helv"); + key!(HIDE_MENUBAR, b"HideMenubar"); + key!(HIDE_TOOLBAR, b"HideToolbar"); + key!(HIDE_WINDOWUI, b"HideWindowUI"); + key!(HUE, b"Hue"); + + // I + key!(I, b"I"); + key!(IC, b"IC"); + key!(ICC_BASED, b"ICCBased"); + key!(ID, b"ID"); + key!(ID_TREE, b"IDTree"); + key!(IDENTITY, b"Identity"); + key!(IDENTITY_H, b"Identity-H"); + key!(IDENTITY_V, b"Identity-V"); + key!(IF, b"IF"); + key!(ILLUSTRATOR, b"Illustrator"); + key!(IM, b"IM"); + key!(IMAGE, b"Image"); + key!(IMAGE_MASK, b"ImageMask"); + key!(INDEX, b"Index"); + key!(INDEXED, b"Indexed"); + key!(INFO, b"Info"); + key!(INKLIST, b"InkList"); + key!(INTENT, b"Intent"); + key!(INTERPOLATE, b"Interpolate"); + key!(IRT, b"IRT"); + key!(IT, b"IT"); + key!(ITALIC_ANGLE, b"ItalicAngle"); + key!(ISSUER, b"Issuer"); + key!(IX, b"IX"); + + // J + key!(JAVA_SCRIPT, b"JavaScript"); + key!(JBIG2_DECODE, b"JBIG2Decode"); + key!(JBIG2_GLOBALS, b"JBIG2Globals"); + key!(JPX_DECODE, b"JPXDecode"); + key!(JS, b"JS"); + + // K + key!(K, b"K"); + key!(KEYWORDS, b"Keywords"); + key!(KEY_USAGE, b"KeyUsage"); + key!(KIDS, b"Kids"); + + // L + key!(L, b"L"); + key!(LAB, b"Lab"); + key!(LANG, b"Lang"); + key!(LAST, b"Last"); + key!(LAST_CHAR, b"LastChar"); + key!(LAST_MODIFIED, b"LastModified"); + key!(LC, b"LC"); + key!(LE, b"LE"); + key!(LEADING, b"Leading"); + key!(LEGAL_ATTESTATION, b"LegalAttestation"); + key!(LENGTH, b"Length"); + key!(LENGTH1, b"Length1"); + key!(LENGTH2, b"Length2"); + key!(LENGTH3, b"Length3"); + key!(LIGHTEN, b"Lighten"); + key!(LIMITS, b"Limits"); + key!(LINEARIZED, b"Linearized"); + key!(LJ, b"LJ"); + key!(LL, b"LL"); + key!(LLE, b"LLE"); + key!(LLO, b"LLO"); + key!(LOCATION, b"Location"); + key!(LUMINOSITY, b"Luminosity"); + key!(LW, b"LW"); + key!(LZW_DECODE, b"LZWDecode"); + key!(LZW_DECODE_ABBREVIATION, b"LZW"); + + // M + key!(M, b"M"); + key!(MAC, b"Mac"); + key!(MAC_EXPERT_ENCODING, b"MacExpertEncoding"); + key!(MAC_ROMAN_ENCODING, b"MacRomanEncoding"); + key!(MARK_INFO, b"MarkInfo"); + key!(MASK, b"Mask"); + key!(MATRIX, b"Matrix"); + key!(MATTE, b"Matte"); + key!(MAX_LEN, b"MaxLen"); + key!(MAX_WIDTH, b"MaxWidth"); + key!(MCID, b"MCID"); + key!(MDP, b"MDP"); + key!(MEDIA_BOX, b"MediaBox"); + key!(MEASURE, b"Measure"); + key!(METADATA, b"Metadata"); + key!(MISSING_WIDTH, b"MissingWidth"); + key!(MIX, b"Mix"); + key!(MK, b"MK"); + key!(ML, b"ML"); + key!(MM_TYPE1, b"MMType1"); + key!(MOD_DATE, b"ModDate"); + key!(MULTIPLY, b"Multiply"); + + // N + key!(N, b"N"); + key!(NAME, b"Name"); + key!(NAMES, b"Names"); + key!(NAVIGATOR, b"Navigator"); + key!(NEED_APPEARANCES, b"NeedAppearances"); + key!(NEW_WINDOW, b"NewWindow"); + key!(NEXT, b"Next"); + key!(NM, b"NM"); + key!(NON_EFONT_NO_WARN, b"NonEFontNoWarn"); + key!(NON_FULL_SCREEN_PAGE_MODE, b"NonFullScreenPageMode"); + key!(NONE, b"None"); + key!(NORMAL, b"Normal"); + key!(NUMS, b"Nums"); + + // O + key!(O, b"O"); + key!(OBJ, b"Obj"); + key!(OBJR, b"OBJR"); + key!(OBJ_STM, b"ObjStm"); + key!(OC, b"OC"); + key!(OCG, b"OCG"); + key!(OCGS, b"OCGs"); + key!(OCMD, b"OCMD"); + key!(OCPROPERTIES, b"OCProperties"); + key!(OCSP, b"OCSP"); + key!(OCSPS, b"OCSPs"); + key!(OE, b"OE"); + key!(OID, b"OID"); + key!(OFF, b"OFF"); + key!(ON, b"ON"); + key!(OP, b"OP"); + key!(OP_NS, b"op"); + key!(OPEN_ACTION, b"OpenAction"); + key!(OPEN_TYPE, b"OpenType"); + key!(OPI, b"OPI"); + key!(OPM, b"OPM"); + key!(OPT, b"Opt"); + key!(ORDER, b"Order"); + key!(ORDERING, b"Ordering"); + key!(OS, b"OS"); + key!(OUTLINES, b"Outlines"); + key!(OUTPUT_CONDITION, b"OutputCondition"); + key!(OUTPUT_CONDITION_IDENTIFIER, b"OutputConditionIdentifier"); + key!(OUTPUT_INTENT, b"OutputIntent"); + key!(OUTPUT_INTENTS, b"OutputIntents"); + key!(OVERLAY, b"Overlay"); + + // P + key!(P, b"P"); + key!(PA, b"PA"); + key!(PAGE, b"Page"); + key!(PAGE_LABELS, b"PageLabels"); + key!(PAGE_LAYOUT, b"PageLayout"); + key!(PAGE_MODE, b"PageMode"); + key!(PAGES, b"Pages"); + key!(PAINT_TYPE, b"PaintType"); + key!(PANOSE, b"Panose"); + key!(PARAMS, b"Params"); + key!(PARENT, b"Parent"); + key!(PARENT_TREE, b"ParentTree"); + key!(PARENT_TREE_NEXT_KEY, b"ParentTreeNextKey"); + key!(PART, b"Part"); + key!(PATH, b"Path"); + key!(PATTERN, b"Pattern"); + key!(PATTERN_TYPE, b"PatternType"); + key!(PC, b"PC"); + key!(PDF_DOC_ENCODING, b"PDFDocEncoding"); + key!(PERMS, b"Perms"); + key!(PERCEPTUAL, b"Perceptual"); + key!(PIECE_INFO, b"PieceInfo"); + key!(PG, b"Pg"); + key!(PI, b"PI"); + key!(PO, b"PO"); + key!(POPUP, b"Popup"); + key!(PRE_RELEASE, b"PreRelease"); + key!(PREDICTOR, b"Predictor"); + key!(PREV, b"Prev"); + key!(PRINT, b"Print"); + key!(PRINT_AREA, b"PrintArea"); + key!(PRINT_CLIP, b"PrintClip"); + key!(PRINT_SCALING, b"PrintScaling"); + key!(PRINT_STATE, b"PrintState"); + key!(PRIVATE, b"Private"); + key!(PROC_SET, b"ProcSet"); + key!(PROCESS, b"Process"); + key!(PRODUCER, b"Producer"); + key!(PROP_BUILD, b"Prop_Build"); + key!(PROPERTIES, b"Properties"); + key!(PS, b"PS"); + key!(PT_DATA, b"PtData"); + key!(PUB_SEC, b"PubSec"); + key!(PV, b"PV"); + + // Q + key!(Q, b"Q"); + key!(QUADPOINTS, b"QuadPoints"); + + // R + key!(R, b"R"); + key!(RANGE, b"Range"); + key!(RC, b"RC"); + key!(RD, b"RD"); + key!(REASON, b"Reason"); + key!(REASONS, b"Reasons"); + key!(RECIPIENTS, b"Recipients"); + key!(RECT, b"Rect"); + key!(REF, b"Ref"); + key!(REFERENCE, b"Reference"); + key!(REGISTRY, b"Registry"); + key!(REGISTRY_NAME, b"RegistryName"); + key!(RELATIVE_COLORIMETRIC, b"RelativeColorimetric"); + key!(RENAME, b"Rename"); + key!(REPEAT, b"Repeat"); + key!(RES_FORK, b"ResFork"); + key!(RESOURCES, b"Resources"); + key!(RGB, b"RGB"); + key!(RI, b"RI"); + key!(ROLE_MAP, b"RoleMap"); + key!(ROOT, b"Root"); + key!(ROTATE, b"Rotate"); + key!(ROWS, b"Rows"); + key!(RT, b"RT"); + key!(RUN_LENGTH_DECODE, b"RunLengthDecode"); + key!(RUN_LENGTH_DECODE_ABBREVIATION, b"RL"); + key!(RV, b"RV"); + + // S + key!(S, b"S"); + key!(SA, b"SA"); + key!(SATURATION, b"Saturation"); + key!(SCHEMA, b"Schema"); + key!(SCREEN, b"Screen"); + key!(SE, b"SE"); + key!(SEPARATION, b"Separation"); + key!(SET_F, b"SetF"); + key!(SET_FF, b"SetFf"); + key!(SHADING, b"Shading"); + key!(SHADING_TYPE, b"ShadingType"); + key!(SIG, b"Sig"); + key!(SIG_FLAGS, b"SigFlags"); + key!(SIG_REF, b"SigRef"); + key!(SIZE, b"Size"); + key!(SM, b"SM"); + key!(SMASK, b"SMask"); + key!(SMASK_IN_DATA, b"SMaskInData"); + key!(SOFT_LIGHT, b"SoftLight"); + key!(SORT, b"Sort"); + key!(SOUND, b"Sound"); + key!(SPLIT, b"Split"); + key!(SS, b"SS"); + key!(ST, b"St"); + key!(STANDARD_ENCODING, b"StandardEncoding"); + key!(STATE, b"State"); + key!(STATE_MODEL, b"StateModel"); + key!(STATUS, b"Status"); + key!(STD_CF, b"StdCF"); + key!(STEM_H, b"StemH"); + key!(STEM_V, b"StemV"); + key!(STM_F, b"StmF"); + key!(STR_F, b"StrF"); + key!(STRUCT_ELEM, b"StructElem"); + key!(STRUCT_PARENT, b"StructParent"); + key!(STRUCT_PARENTS, b"StructParents"); + key!(STRUCT_TREE_ROOT, b"StructTreeRoot"); + key!(STYLE, b"Style"); + key!(SUB_FILTER, b"SubFilter"); + key!(SUBJ, b"Subj"); + key!(SUBJECT, b"Subject"); + key!(SUBJECT_DN, b"SubjectDN"); + key!(SUBTYPE, b"Subtype"); + key!(SUPPLEMENT, b"Supplement"); + key!(SV, b"SV"); + key!(SV_CERT, b"SVCert"); + key!(SW, b"SW"); + key!(SY, b"Sy"); + key!(SYNCHRONOUS, b"Synchronous"); + key!(T, b"T"); + key!(TARGET, b"Target"); + key!(TEMPLATES, b"Templates"); + key!(THREAD, b"Thread"); + key!(THREADS, b"Threads"); + key!(THREE_DD, b"3DD"); + key!(THUMB, b"Thumb"); + key!(TI, b"TI"); + key!(TILING_TYPE, b"TilingType"); + key!(TIME_STAMP, b"TimeStamp"); + key!(TITLE, b"Title"); + key!(TK, b"TK"); + key!(TM, b"TM"); + key!(TO_UNICODE, b"ToUnicode"); + key!(TR, b"TR"); + key!(TR2, b"TR2"); + key!(TRAPPED, b"Trapped"); + key!(TRANS, b"Trans"); + key!(TRANSFORM_METHOD, b"TransformMethod"); + key!(TRANSFORM_PARAMS, b"TransformParams"); + key!(TRANSPARENCY, b"Transparency"); + key!(TREF, b"TRef"); + key!(TRIM_BOX, b"TrimBox"); + key!(TRUE_TYPE, b"TrueType"); + key!(TRUSTED_MODE, b"TrustedMode"); + key!(TU, b"TU"); + key!(TX, b"Tx"); + key!(TYPE, b"Type"); + key!(TYPE0, b"Type0"); + key!(TYPE1, b"Type1"); + key!(TYPE1C, b"Type1C"); + key!(TYPE3, b"Type3"); + + // U + key!(U, b"U"); + key!(UE, b"UE"); + key!(UF, b"UF"); + key!(UNCHANGED, b"Unchanged"); + key!(UNIX, b"Unix"); + key!(URI, b"URI"); + key!(URL, b"URL"); + key!(URL_TYPE, b"URLType"); + key!(USAGE, b"Usage"); + key!(USE_CMAP, b"UseCMap"); + key!(USER_UNIT, b"UserUnit"); + + // V + key!(V, b"V"); + key!(VE, b"VE"); + key!(VERISIGN_PPKVS, b"VeriSign.PPKVS"); + key!(VERSION, b"Version"); + key!(VERTICES, b"Vertices"); + key!(VERTICES_PER_ROW, b"VerticesPerRow"); + key!(VIEW, b"View"); + key!(VIEW_AREA, b"ViewArea"); + key!(VIEW_CLIP, b"ViewClip"); + key!(VIEW_STATE, b"ViewState"); + key!(VIEWER_PREFERENCES, b"ViewerPreferences"); + key!(VOLUME, b"Volume"); + key!(VP, b"VP"); + key!(VRI, b"VRI"); + + // W + key!(W, b"W"); + key!(W2, b"W2"); + key!(WC, b"WC"); + key!(WHITE_POINT, b"WhitePoint"); + key!(WIDGET, b"Widget"); + key!(WIDTH, b"Width"); + key!(WIDTHS, b"Widths"); + key!(WIN, b"Win"); + key!(WIN_ANSI_ENCODING, b"WinAnsiEncoding"); + key!(WMODE, b"WMode"); + key!(WP, b"WP"); + key!(WS, b"WS"); + + // X + key!(X, b"X"); + key!(XFA, b"XFA"); + key!(X_STEP, b"XStep"); + key!(XHEIGHT, b"XHeight"); + key!(XOBJECT, b"XObject"); + key!(XREF, b"XRef"); + key!(XREF_STM, b"XRefStm"); + + // Y + key!(Y, b"Y"); + key!(Y_STEP, b"YStep"); + key!(YES, b"Yes"); + + // Z + key!(ZA_DB, b"ZaDb"); +} + +#[cfg(test)] +mod tests { + use crate::object::Number; + use crate::object::dict::keys::{COLORSPACE, EXT_G_STATE, FONT, PROC_SET}; + use crate::object::dict::{Dict, InlineImageDict}; + use crate::object::string; + use crate::object::{Name, ObjRef}; + use crate::reader::Reader; + use crate::reader::{ReaderContext, ReaderExt}; + + fn dict_impl(data: &[u8]) -> Option> { + Reader::new(data).read_with_context::>(&ReaderContext::dummy()) + } + + #[test] + fn empty_dict_1() { + let dict_data = b"<<>>"; + let dict = dict_impl(dict_data).unwrap(); + + assert_eq!(dict.len(), 0); + } + + #[test] + fn empty_dict_2() { + let dict_data = b"<< \n >>"; + let dict = dict_impl(dict_data).unwrap(); + + assert_eq!(dict.len(), 0); + } + + #[test] + fn dict_1() { + let dict_data = b"<< /Hi 34.0 >>"; + let dict = dict_impl(dict_data).unwrap(); + + assert_eq!(dict.len(), 1); + assert!(dict.get::(Name::new_unescaped(b"Hi")).is_some()); + } + + #[test] + fn dict_2() { + let dict_data = b"<< /Hi \n 34.0 /Second true >>"; + let dict = dict_impl(dict_data).unwrap(); + + assert_eq!(dict.len(), 2); + assert!(dict.get::(Name::new_unescaped(b"Hi")).is_some()); + assert!(dict.get::(Name::new_unescaped(b"Second")).is_some()); + } + + #[test] + fn dict_complex() { + let data = "<< /Type /Example +/Subtype /DictionaryExample +/Version 0.01 +/IntegerItem 12 +/StringItem ( a string ) +/Subdictionary << /Item1 0.4 + /Item2 true + /LastItem ( not ! ) + /VeryLastItem ( OK ) + >> +>>"; + + let dict = Reader::new(data.as_bytes()) + .read_with_context::>(&ReaderContext::dummy()) + .unwrap(); + assert_eq!(dict.len(), 6); + assert!(dict.get::>(Name::new_unescaped(b"Type")).is_some()); + assert!( + dict.get::>(Name::new_unescaped(b"Subtype")) + .is_some() + ); + assert!( + dict.get::(Name::new_unescaped(b"Version")) + .is_some() + ); + assert!( + dict.get::(Name::new_unescaped(b"IntegerItem")) + .is_some() + ); + assert!( + dict.get::>(Name::new_unescaped(b"StringItem")) + .is_some() + ); + assert!( + dict.get::>(Name::new_unescaped(b"Subdictionary")) + .is_some() + ); + } + + #[test] + fn dict_with_trailing() { + let dict_data = b"<< /Hi 67.0 >>trailing data"; + let dict = dict_impl(dict_data).unwrap(); + + assert_eq!(dict.len(), 1); + } + + #[test] + fn dict_with_comment() { + let dict_data = b"<< /Hi % A comment \n 67.0 % Another comment \n >>"; + let dict = dict_impl(dict_data).unwrap(); + + assert_eq!(dict.len(), 1); + } + + #[test] + fn inline_dict() { + let dict_data = b"/W 17 /H 17 /CS /RGB /BPC 8 /F [ /A85 /LZW ] ID "; + + let dict = Reader::new(&dict_data[..]) + .read_with_context::>(&ReaderContext::dummy()) + .unwrap(); + + assert_eq!(dict.get_dict().len(), 5); + } + + #[test] + fn dict_with_escaped_name() { + let dict_data = b"<< /PANTONE#20104#20C 234 >>"; + let dict = dict_impl(dict_data).unwrap(); + + assert!(dict.contains_key("PANTONE 104 C")); + } + + #[test] + fn garbage_in_between() { + let dict_data = b"<< +/ProcSet [ /PDF /Text ] +/Font << /F4 31 0 R /F6 23 0 R >> +/ExtGState << /GS2 14 0 R +2000 + /GS3 15 0 R >> +/ColorSpace << /Cs5 13 0 R >> +>> "; + let dict = dict_impl(dict_data).unwrap(); + + assert!(dict.contains_key(PROC_SET)); + assert!(dict.contains_key(FONT)); + assert!(dict.contains_key(EXT_G_STATE)); + assert!(dict.contains_key(COLORSPACE)); + + let Some(dict) = dict.get::>(EXT_G_STATE) else { + panic!("failed to parse ext g state"); + }; + + assert_eq!(dict.get_ref("GS2"), Some(ObjRef::new(14, 0))); + assert_eq!(dict.get_ref("GS3"), Some(ObjRef::new(15, 0))); + } +} diff --git a/vendor/hayro-syntax/src/object/indirect.rs b/vendor/hayro-syntax/src/object/indirect.rs new file mode 100644 index 00000000..cd0e6ff7 --- /dev/null +++ b/vendor/hayro-syntax/src/object/indirect.rs @@ -0,0 +1,61 @@ +use crate::object::{ObjectIdentifier, ObjectLike}; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; + +#[derive(Debug, Clone)] +pub(crate) struct IndirectObject { + id: ObjectIdentifier, + inner: T, +} + +impl IndirectObject { + pub(crate) fn get(self) -> T { + self.inner + } + + pub(crate) fn id(&self) -> &ObjectIdentifier { + &self.id + } +} + +impl<'a, T> Readable<'a> for IndirectObject +where + T: ObjectLike<'a>, +{ + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let mut ctx = ctx.clone(); + let id = r.read_without_context::()?; + + if ctx.parent_chain_contains(&id) { + warn!("cycle detected in indirect object: {id:?}"); + + return None; + } + + ctx.set_obj_number(id); + ctx.parent_chain_push(id); + r.skip_white_spaces_and_comments(); + let inner = r.read_with_context::(&ctx)?; + r.skip_white_spaces_and_comments(); + // We are lenient and don't require it. + r.forward_tag(b"endobj"); + + Some(Self { id, inner }) + } +} + +impl Skippable for IndirectObject +where + T: Skippable, +{ + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + r.skip::(false)?; + r.skip_white_spaces_and_comments(); + r.skip::(false)?; + r.skip_white_spaces_and_comments(); + // We are lenient and don't require it. + r.forward_tag(b"endobj"); + + Some(()) + } +} diff --git a/vendor/hayro-syntax/src/object/mod.rs b/vendor/hayro-syntax/src/object/mod.rs new file mode 100644 index 00000000..d692db8b --- /dev/null +++ b/vendor/hayro-syntax/src/object/mod.rs @@ -0,0 +1,399 @@ +//! Parsing and reading from PDF objects. + +pub use crate::object::array::Array; +pub use crate::object::date::DateTime; +pub use crate::object::dict::Dict; +pub use crate::object::name::Name; +use crate::object::name::skip_name_like; +pub use crate::object::null::Null; +pub use crate::object::number::Number; +pub use crate::object::rect::Rect; +pub use crate::object::r#ref::{MaybeRef, ObjRef}; +pub use crate::object::stream::Stream; +pub use crate::object::string::String; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; +use core::fmt::Debug; + +mod bool; +mod date; +mod null; +mod number; +mod rect; +mod r#ref; +mod string; +mod tuple; + +pub(crate) mod indirect; +pub(crate) mod name; + +pub mod array; +pub mod dict; +pub mod stream; + +/// A trait for PDF objects. +pub(crate) trait ObjectLike<'a>: TryFrom> + Readable<'a> + Debug + Clone {} + +/// A trait for PDF objects that can be borrowed directly from an [`Object`]. +pub(crate) trait ObjectRefLike<'a>: Sized { + fn cast_ref<'b>(obj: &'b Object<'a>) -> Option<&'b Self>; +} + +/// A primitive PDF object. +#[derive(Debug, Clone, PartialEq)] +pub enum Object<'a> { + /// A null object. + Null(Null), + /// A boolean object. + Boolean(bool), + /// A number object. + Number(Number), + /// A string object. + String(String<'a>), + /// A name object. + Name(Name<'a>), + /// A dict object. + Dict(Dict<'a>), + /// An array object. + Array(Array<'a>), + /// A stream object. + // Can only be an indirect object in theory and thus comes with some caveats, + // but we just treat it the same. + Stream(Stream<'a>), +} + +impl<'a> Object<'a> { + /// Try casting the object to a specific subtype. + pub(crate) fn cast(self) -> Option + where + T: ObjectLike<'a>, + { + self.try_into().ok() + } + + /// Try casting the object to a reference of the specific type. + pub(crate) fn cast_ref(&self) -> Option<&T> + where + T: ObjectRefLike<'a>, + { + T::cast_ref(self) + } + + /// Try casting the object to a dict. + #[inline(always)] + pub fn into_dict(self) -> Option> { + self.cast() + } + + /// Try casting the object to a name. + #[inline(always)] + pub fn into_name(self) -> Option> { + self.cast() + } + + /// Try casting the object to the null object. + #[inline(always)] + pub fn into_null(self) -> Option { + self.cast() + } + + /// Try casting the object to a bool. + #[inline(always)] + pub fn into_bool(self) -> Option { + self.cast() + } + + /// Try casting the object to a string. + #[inline(always)] + pub fn into_string(self) -> Option> { + self.cast() + } + + /// Try casting the object to a stream. + #[inline(always)] + pub fn into_stream(self) -> Option> { + self.cast() + } + + /// Try casting the object to an array. + #[inline(always)] + pub fn into_array(self) -> Option> { + self.cast() + } + + /// Try casting the object to a u8. + #[inline(always)] + pub fn into_u8(self) -> Option { + self.cast() + } + + /// Try casting the object to a u16. + #[inline(always)] + pub fn into_u16(self) -> Option { + self.cast() + } + + /// Try casting the object to a f32. + #[inline(always)] + pub fn into_f32(self) -> Option { + self.cast() + } + + /// Try casting the object to a i32. + #[inline(always)] + pub fn into_i32(self) -> Option { + self.cast() + } + + /// Try casting the object to a number. + #[inline(always)] + pub fn into_number(self) -> Option { + self.cast() + } +} + +impl<'a> ObjectLike<'a> for Object<'a> {} +impl<'a> ObjectRefLike<'a> for Object<'a> { + fn cast_ref(obj: &Self) -> Option<&Self> { + Some(obj) + } +} + +impl Skippable for Object<'_> { + fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()> { + match r.peek_byte()? { + b'n' => Null::skip(r, is_content_stream), + b't' | b'f' => bool::skip(r, is_content_stream), + b'/' => Name::skip(r, is_content_stream), + b'<' => match r.peek_bytes(2)? { + // A stream can never appear in a dict/array, so it should never be skipped. + b"<<" => Dict::skip(r, is_content_stream), + _ => String::skip(r, is_content_stream), + }, + b'(' => String::skip(r, is_content_stream), + b'.' | b'+' | b'-' | b'0'..=b'9' => Number::skip(r, is_content_stream), + b'[' => Array::skip(r, is_content_stream), + // See test case operator-in-TJ-array-0: Be lenient and skip content operators in + // array + _ => skip_name_like(r, false), + } + } +} + +impl<'a> Readable<'a> for Object<'a> { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let object = match r.peek_byte()? { + b'n' => Self::Null(Null::read(r, ctx)?), + b't' | b'f' => Self::Boolean(bool::read(r, ctx)?), + b'/' => Self::Name(Name::read(r, ctx)?), + b'<' => match r.peek_bytes(2)? { + b"<<" => { + let mut cloned = r.clone(); + let dict = Dict::read(&mut cloned, ctx); + cloned.skip_white_spaces_and_comments(); + let is_stream = cloned.forward_tag(b"stream").is_some(); + // PdfCraft patch: what the clone looked at past the end counts for `r` too. + r.absorb_past_end(&cloned); + let dict = dict?; + + if is_stream { + Object::Stream(Stream::read(r, ctx)?) + } else { + r.jump(cloned.offset()); + + Object::Dict(dict) + } + } + _ => Self::String(String::read(r, ctx)?), + }, + b'(' => Self::String(String::read(r, ctx)?), + b'.' | b'+' | b'-' | b'0'..=b'9' => Self::Number(Number::read(r, ctx)?), + b'[' => Self::Array(Array::read(r, ctx)?), + // See the comment in `skip`. + _ => { + skip_name_like(r, false)?; + Self::Null(Null) + } + }; + + Some(object) + } +} + +/// A trait for objects that can be parsed from a simple byte stream. +pub trait FromBytes<'a>: Sized { + /// Try to read the object from the given bytes. + fn from_bytes(b: &'a [u8]) -> Option; +} + +impl<'a, T: Readable<'a>> FromBytes<'a> for T { + fn from_bytes(b: &'a [u8]) -> Option { + Self::from_bytes_impl(b) + } +} + +/// An identifier for a PDF object. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct ObjectIdentifier { + /// The object number. + pub obj_number: i32, + /// The generation number. + pub gen_number: i32, +} + +impl ObjectIdentifier { + /// Create a new `ObjectIdentifier`. + pub fn new(obj_number: i32, gen_number: i32) -> Self { + Self { + obj_number, + gen_number, + } + } +} + +impl Readable<'_> for ObjectIdentifier { + fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option { + let obj_number = r.read_without_context::()?; + r.skip_white_spaces_and_comments(); + let gen_number = r.read_without_context::()?; + r.skip_white_spaces_and_comments(); + r.forward_tag(b"obj")?; + + Some(Self { + obj_number, + gen_number, + }) + } +} + +impl Skippable for ObjectIdentifier { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + r.skip::(false)?; + r.skip_white_spaces_and_comments(); + r.skip::(false)?; + r.skip_white_spaces_and_comments(); + r.forward_tag(b"obj")?; + + Some(()) + } +} + +/// A convenience function that extracts a dict and a stream from an object. +/// If the object is just a dictionary, it will return `None` for the stream. +/// If the object is a stream, it will return its dictionary as well as the stream +/// itself. +pub fn dict_or_stream<'a, 'b>( + obj: &'b Object<'a>, +) -> Option<(&'b Dict<'a>, Option<&'b Stream<'a>>)> { + if let Some(stream) = obj.cast_ref::>() { + Some((stream.dict(), Some(stream))) + } else { + obj.cast_ref::>().map(|dict| (dict, None)) + } +} + +mod macros { + macro_rules! object { + ($t:ident $(<$l:lifetime>),*, $s:ident) => { + impl<'a> TryFrom> for $t$(<$l>),* { + type Error = (); + + fn try_from(value: Object<'a>) -> core::result::Result { + match value { + Object::$s(b) => Ok(b), + _ => Err(()), + } + } + } + + impl<'a> crate::object::ObjectLike<'a> for $t$(<$l>),* {} + + impl<'a> crate::object::ObjectRefLike<'a> for $t$(<$l>),* { + fn cast_ref<'b>(obj: &'b crate::object::Object<'a>) -> Option<&'b Self> { + match obj { + crate::object::Object::$s(b) => Some(b), + _ => None, + } + } + } + }; + } + + pub(crate) use object; +} + +#[cfg(test)] +mod tests { + use crate::object::{Array, Dict, Name, Null, Number, Object, Stream, String}; + use crate::reader::Reader; + use crate::reader::{ReaderContext, ReaderExt}; + + fn object_impl(data: &[u8]) -> Option> { + let mut r = Reader::new(data); + r.read_with_context::>(&ReaderContext::dummy()) + } + + #[test] + fn null() { + assert!(matches!(object_impl(b"null").unwrap(), Object::Null(_))); + } + + #[test] + fn bool() { + assert!(matches!(object_impl(b"true").unwrap(), Object::Boolean(_))); + } + + #[test] + fn number() { + assert!(matches!(object_impl(b"34.5").unwrap(), Object::Number(_))); + } + + #[test] + fn string_1() { + assert!(matches!(object_impl(b"(Hi)").unwrap(), Object::String(_))); + } + + #[test] + fn string_2() { + assert!(matches!(object_impl(b"<34>").unwrap(), Object::String(_))); + } + + #[test] + fn name() { + assert!(matches!(object_impl(b"/Name").unwrap(), Object::Name(_))); + } + + #[test] + fn dict() { + assert!(matches!( + object_impl(b"<>").unwrap(), + Object::Dict(_) + )); + } + + #[test] + fn array() { + assert!(matches!(object_impl(b"[45]").unwrap(), Object::Array(_))); + } + + #[test] + fn stream() { + assert!(matches!( + object_impl(b"<< /Length 3 >> stream\nabc\nendstream").unwrap(), + Object::Stream(_) + )); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn object_sizes() { + assert_eq!(size_of::>(), 40); + assert_eq!(size_of::>(), 32); + assert_eq!(size_of::>(), 8); + assert_eq!(size_of::>(), 32); + assert_eq!(size_of::(), 0); + assert_eq!(size_of::(), 16); + assert_eq!(size_of::>(), 24); + assert_eq!(size_of::>(), 32); + } +} diff --git a/vendor/hayro-syntax/src/object/name.rs b/vendor/hayro-syntax/src/object/name.rs new file mode 100644 index 00000000..892d4d4f --- /dev/null +++ b/vendor/hayro-syntax/src/object/name.rs @@ -0,0 +1,362 @@ +//! Names. + +use crate::filter::ascii_hex::decode_hex_digit; +use crate::object::Object; +use crate::object::macros::object; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, Skippable}; +use crate::trivia::is_regular_character; +use core::borrow::Borrow; +use core::fmt::{self, Debug, Formatter}; +use core::hash::{Hash, Hasher}; +use core::ops::Deref; +use smallvec::SmallVec; + +#[derive(Clone)] +enum NameInner<'a> { + Borrowed(&'a [u8]), + Owned(SmallVec<[u8; 23]>), +} + +/// A PDF name object. +#[derive(Clone)] +pub struct Name<'a>(NameInner<'a>); + +impl<'a> Deref for Name<'a> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl AsRef<[u8]> for Name<'_> { + fn as_ref(&self) -> &[u8] { + match &self.0 { + NameInner::Borrowed(data) => data, + NameInner::Owned(data) => data, + } + } +} + +impl Borrow<[u8]> for Name<'_> { + fn borrow(&self) -> &[u8] { + self.as_ref() + } +} + +impl PartialEq for Name<'_> { + fn eq(&self, other: &Self) -> bool { + self.as_ref() == other.as_ref() + } +} + +impl Eq for Name<'_> {} + +impl Hash for Name<'_> { + fn hash(&self, state: &mut H) { + self.as_ref().hash(state); + } +} + +impl PartialOrd for Name<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Name<'_> { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.as_ref().cmp(other.as_ref()) + } +} + +impl<'a> Name<'a> { + // PdfCraft patch: (#307) lazy dictionary indexes cannot borrow through a + // OnceLock without making the enclosing PDF pages invariant in their lifetime. + // Ordinary PDF keys fit inline, so owning these bytes needs no allocation. + pub(crate) fn into_owned(self) -> Name<'static> { + Name(NameInner::Owned(match self.0 { + NameInner::Borrowed(data) => SmallVec::from_slice(data), + NameInner::Owned(data) => data, + })) + } + + /// Create a new name from a sequence of bytes. + #[inline] + pub fn new(data: &'a [u8]) -> Option { + if !data.contains(&b'#') { + Some(Self::new_unescaped(data)) + } else { + Self::new_escaped(data) + } + } + + /// Create a new name from an unescaped byte sequence. + #[inline] + pub fn new_unescaped(data: &'a [u8]) -> Self { + Self(NameInner::Borrowed(data)) + } + + /// Create a new name from bytes that may contain escape sequences. + #[inline] + pub fn new_escaped(data: &'a [u8]) -> Option { + let mut result = SmallVec::new(); + let mut r = Reader::new(data); + + while let Some(b) = r.read_byte() { + if b == b'#' { + let hex = r.read_bytes(2)?; + result.push(decode_hex_digit(hex[0])? << 4 | decode_hex_digit(hex[1])?); + } else { + result.push(b); + } + } + + Some(Self(NameInner::Owned(result))) + } + + /// Return a string representation of the name. + /// + /// Returns a placeholder in case the name is not UTF-8 encoded. + pub fn as_str(&self) -> &str { + core::str::from_utf8(self.as_ref()).unwrap_or("{non-ascii key}") + } +} + +impl Debug for Name<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match core::str::from_utf8(self.as_ref()) { + Ok(s) => ::fmt(s, f), + Err(_) => <[u8] as Debug>::fmt(self.as_ref(), f), + } + } +} + +object!(Name<'a>, Name); + +impl Skippable for Name<'_> { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + skip_name_like(r, true).map(|_| ()) + } +} + +impl<'a> Readable<'a> for Name<'a> { + fn read(r: &mut Reader<'a>, _: &ReaderContext<'a>) -> Option { + let start = r.offset(); + skip_name_like(r, true)?; + let end = r.offset(); + + // Exclude leading solidus. + let data = r.range(start + 1..end)?; + Self::new(data) + } +} + +// This method is shared by `Name` and the parser for content stream operators (which behave like +// names, except that they aren't preceded by a solidus. +pub(crate) fn skip_name_like(r: &mut Reader<'_>, solidus: bool) -> Option<()> { + // Note that we are not validating hex escape sequences here + // (since this method can lie on the hot path), so it's possible + // this method will yield invalid names. Validation needs to happen during actual + // actual reading. + if solidus { + r.forward_tag(b"/")?; + r.forward_while(is_regular_character); + } else { + r.forward_while_1(is_regular_character)?; + } + + Some(()) +} + +#[cfg(test)] +mod tests { + use crate::object::Name; + use crate::reader::Reader; + use crate::reader::ReaderExt; + use std::ops::Deref; + + #[test] + fn name_1() { + assert_eq!( + Reader::new("/".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"" + ); + } + + #[test] + fn name_2() { + assert!( + Reader::new("dfg".as_bytes()) + .read_without_context::>() + .is_none() + ); + } + + #[test] + fn name_3() { + assert!( + Reader::new("/AB#FG".as_bytes()) + .read_without_context::>() + .is_none() + ); + } + + #[test] + fn name_4() { + assert_eq!( + Reader::new("/Name1".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"Name1" + ); + } + + #[test] + fn name_5() { + assert_eq!( + Reader::new("/ASomewhatLongerName".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"ASomewhatLongerName" + ); + } + + #[test] + fn name_6() { + assert_eq!( + Reader::new("/A;Name_With-Various***Characters?".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"A;Name_With-Various***Characters?" + ); + } + + #[test] + fn name_7() { + assert_eq!( + Reader::new("/1.2".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"1.2" + ); + } + + #[test] + fn name_8() { + assert_eq!( + Reader::new("/$$".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"$$" + ); + } + + #[test] + fn name_9() { + assert_eq!( + Reader::new("/@pattern".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"@pattern" + ); + } + + #[test] + fn name_10() { + assert_eq!( + Reader::new("/.notdef".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b".notdef" + ); + } + + #[test] + fn name_11() { + assert_eq!( + Reader::new("/lime#20Green".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"lime Green" + ); + } + + #[test] + fn name_12() { + assert_eq!( + Reader::new("/paired#28#29parentheses".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"paired()parentheses" + ); + } + + #[test] + fn name_13() { + assert_eq!( + Reader::new("/The_Key_of_F#23_Minor".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"The_Key_of_F#_Minor" + ); + } + + #[test] + fn name_14() { + assert_eq!( + Reader::new("/A#42".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"AB" + ); + } + + #[test] + fn name_15() { + assert_eq!( + Reader::new("/A#3b".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"A;" + ); + } + + #[test] + fn name_16() { + assert_eq!( + Reader::new("/A#3B".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"A;" + ); + } + + #[test] + fn name_17() { + assert_eq!( + Reader::new("/k1 ".as_bytes()) + .read_without_context::>() + .unwrap() + .deref(), + b"k1" + ); + } +} diff --git a/vendor/hayro-syntax/src/object/null.rs b/vendor/hayro-syntax/src/object/null.rs new file mode 100644 index 00000000..6f025b19 --- /dev/null +++ b/vendor/hayro-syntax/src/object/null.rs @@ -0,0 +1,53 @@ +//! The null object. + +use crate::object::Object; +use crate::object::macros::object; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, Skippable}; + +/// The null object. +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] +pub struct Null; + +object!(Null, Null); + +impl Skippable for Null { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + r.forward_tag(b"null") + } +} + +impl Readable<'_> for Null { + fn read(r: &mut Reader<'_>, ctx: &ReaderContext<'_>) -> Option { + Self::skip(r, ctx.in_content_stream())?; + + Some(Self) + } +} + +#[cfg(test)] +mod tests { + use crate::object::Null; + use crate::reader::Reader; + use crate::reader::ReaderExt; + + #[test] + fn null() { + assert_eq!( + Reader::new("null".as_bytes()) + .read_without_context::() + .unwrap(), + Null + ); + } + + #[test] + fn null_trailing() { + assert_eq!( + Reader::new("nullabs".as_bytes()) + .read_without_context::() + .unwrap(), + Null + ); + } +} diff --git a/vendor/hayro-syntax/src/object/number.rs b/vendor/hayro-syntax/src/object/number.rs new file mode 100644 index 00000000..b9985612 --- /dev/null +++ b/vendor/hayro-syntax/src/object/number.rs @@ -0,0 +1,621 @@ +//! Numbers. + +use crate::math::{powi_f64, trunc_f64}; +use crate::object::macros::object; +use crate::object::{Object, ObjectLike}; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; +use crate::trivia::{is_regular_character, is_white_space_character}; +use core::fmt::Debug; + +#[rustfmt::skip] +static POWERS_OF_10: [f64; 20] = [ + 1.0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, + 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, +]; + +/// A number. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Number(pub(crate) InternalNumber); + +impl Number { + /// The number zero. + pub const ZERO: Self = Self::from_i32(0); + /// The number one. + pub const ONE: Self = Self::from_i32(1); + + /// Returns the number as a f64. + pub fn as_f64(&self) -> f64 { + match self.0 { + InternalNumber::Real(r) => r, + InternalNumber::Integer(i) => i as f64, + } + } + + /// Returns the number as a f32. + pub fn as_f32(&self) -> f32 { + match self.0 { + InternalNumber::Real(r) => r as f32, + InternalNumber::Integer(i) => i as f32, + } + } + + /// Returns the number as an i64. + pub fn as_i64(&self) -> i64 { + match self.0 { + InternalNumber::Real(r) => { + let res = r as i64; + + if !(trunc_f64(r) == r) { + debug!("float {r} was truncated to {res}"); + } + + res + } + InternalNumber::Integer(i) => i, + } + } + + /// Create a new `Number` from an f32 number. + pub const fn from_f32(num: f32) -> Self { + Self(InternalNumber::Real(num as f64)) + } + + /// Create a new `Number` from an i32 number. + pub const fn from_i32(num: i32) -> Self { + Self(InternalNumber::Integer(num as i64)) + } +} + +impl Skippable for Number { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + let has_sign = r.forward_if(|b| b == b'+' || b == b'-').is_some(); + + // Some PDFs have weird trailing minuses, so try to accept those as well. + match r.peek_byte()? { + b'.' => { + r.read_byte()?; + // See PDFJS-9252 - treat a single . as 0. + r.forward_while(is_digit_or_minus); + } + b'0'..=b'9' | b'-' => { + r.forward_while_1(is_digit_or_minus)?; + if let Some(()) = r.forward_tag(b".") { + r.forward_while(is_digit_or_minus); + } + } + // See PDFJS-bug1753983 - accept just + or - as a zero. + // ALso see PDFJS-bug1953099, where the sign is followed by a show + // text string operand, requiring us to allow '<' and '(' as well. + b if has_sign && (is_white_space_character(b) || matches!(b, b'(' | b'<')) => {} + _ => return None, + } + + // See issue 994. Don't accept numbers that are followed by a regular character. + if r.peek_byte().is_some_and(is_regular_character) { + return None; + } + + Some(()) + } +} + +impl Readable<'_> for Number { + #[inline] + fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option { + let old_offset = r.offset(); + read_inner(r).or_else(|| { + r.jump(old_offset); + None + }) + } +} + +#[inline(always)] +fn read_inner(r: &mut Reader<'_>) -> Option { + let first = r.peek_byte()?; + let has_sign = matches!(first, b'+' | b'-'); + let negative = match first { + b'-' => { + r.forward(); + true + } + b'+' => { + r.forward(); + false + } + _ => false, + }; + + // PdfCraft patch: high-precision float mantissa overflow (jsPDF double-precision coordinates). + // Digits that don't fit in `mantissa` are dropped: past the dot they are only precision; + // before it, each one scales the value by ten (`extra_int_digits`). + let mut mantissa: u64 = 0; + let mut has_dot = false; + let mut decimal_shift: u32 = 0; + let mut has_digits = false; + let mut extra_int_digits: u32 = 0; + + loop { + match r.peek_byte() { + Some(b'0'..=b'9') => { + let d = (r.read_byte().unwrap() - b'0') as u64; + has_digits = true; + if let Some(next) = mantissa.checked_mul(10).and_then(|m| m.checked_add(d)) { + mantissa = next; + if has_dot { + decimal_shift += 1; + } + } else if !has_dot { + extra_int_digits = extra_int_digits.saturating_add(1); + } + // If has_dot and adding the digit would overflow u64: + // Safely ignore excess trailing fractional digits (do not add, do not increment decimal_shift). + } + Some(b'.') if !has_dot => { + r.forward(); + has_dot = true; + } + // Some weird PDFs have trailing minus in the fraction of number. + Some(b'-') if has_digits => { + r.forward(); + r.forward_while(is_digit_or_minus); + break; + } + _ => break, + } + } + + // See issue 994. Don't accept numbers that are followed by a regular character + // without any white space in-between. Check tolerant zero tokens as well. + if r.peek_byte().is_some_and(is_regular_character) { + return None; + } + + if !has_digits { + if has_sign || has_dot { + // Treat numbers like just `-`, `+` or `-.` as zero, consistently with skip. + return Some(Number(InternalNumber::Integer(0))); + } + return None; + } + + // A value whose integer part overflowed `u64` is a real, scaled by the digits dropped and + // kept finite. + let scaled = |value: f64| -> f64 { + let value = if extra_int_digits > 0 { value * powi_f64(10.0, extra_int_digits) } else { value }; + if value.is_finite() { value } else { f64::MAX } + }; + + if !has_dot && extra_int_digits > 0 { + let value = scaled(mantissa as f64); + return Some(Number(InternalNumber::Real(if negative { -value } else { value }))); + } + + if !has_dot { + let value = if mantissa <= i64::MAX as u64 { + let i = mantissa as i64; + if negative { -i } else { i } + } else if negative && mantissa == (i64::MIN as u64) { + i64::MIN + } else { + let mut val = mantissa as f64; + if negative { + val = -val; + } + return Some(Number(InternalNumber::Real(val))); + }; + Some(Number(InternalNumber::Integer(value))) + } else { + let mut value = scaled(mantissa as f64); + + if decimal_shift > 0 { + if decimal_shift < POWERS_OF_10.len() as u32 { + value /= POWERS_OF_10[decimal_shift as usize]; + } else { + value /= powi_f64(10.0, decimal_shift); + } + } + + if negative { + value = -value; + } + + Some(Number(InternalNumber::Real(value))) + } +} + +object!(Number, Number); + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum InternalNumber { + Real(f64), + Integer(i64), +} + +macro_rules! int_num { + ($i:ident) => { + impl Skippable for $i { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + r.forward_if(|b| b == b'+' || b == b'-'); + r.forward_while_1(is_digit)?; + + // We have a float instead of an integer. + if r.peek_byte() == Some(b'.') { + return None; + } + + // See issue 994. Don't accept numbers that are followed by a regular character + // without any white space in-between. + if r.peek_byte().is_some_and(is_regular_character) { + return None; + } + + Some(()) + } + } + + impl<'a> Readable<'a> for $i { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option<$i> { + r.read::(ctx) + .map(|n| n.as_i64()) + .and_then(|n| n.try_into().ok()) + } + } + + impl TryFrom> for $i { + type Error = (); + + fn try_from(value: Object<'_>) -> core::result::Result { + match value { + Object::Number(n) => n.as_i64().try_into().ok().ok_or(()), + _ => Err(()), + } + } + } + + impl<'a> ObjectLike<'a> for $i {} + }; +} + +int_num!(i32); +int_num!(i64); +int_num!(u32); +int_num!(u16); +int_num!(usize); +int_num!(u8); + +impl Skippable for f32 { + fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()> { + r.skip::(is_content_stream).map(|_| {}) + } +} + +impl Readable<'_> for f32 { + fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option { + r.read_without_context::() + .map(|n| n.as_f64() as Self) + } +} + +impl TryFrom> for f32 { + type Error = (); + + fn try_from(value: Object<'_>) -> Result { + match value { + Object::Number(n) => Ok(n.as_f64() as Self), + _ => Err(()), + } + } +} + +impl ObjectLike<'_> for f32 {} + +impl Skippable for f64 { + fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()> { + r.skip::(is_content_stream).map(|_| {}) + } +} + +impl Readable<'_> for f64 { + fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option { + r.read_without_context::().map(|n| n.as_f64()) + } +} + +impl TryFrom> for f64 { + type Error = (); + + fn try_from(value: Object<'_>) -> Result { + match value { + Object::Number(n) => Ok(n.as_f64()), + _ => Err(()), + } + } +} + +impl ObjectLike<'_> for f64 {} + +pub(crate) fn is_digit(byte: u8) -> bool { + byte.is_ascii_digit() +} + +pub(crate) fn is_digit_or_minus(byte: u8) -> bool { + is_digit(byte) || byte == b'-' +} + +#[cfg(test)] +mod tests { + use crate::object::Number; + use crate::reader::Reader; + use crate::reader::ReaderExt; + + #[test] + fn tolerant_signed_and_dot_zero_tokens_match_skip() { + for token in ["+", "-", ".", "+.", "-.", "0", "+0", "-0", "0.", "+0.", "-0."] { + let bytes = format!("{token} 3"); + let mut skipped = Reader::new(bytes.as_bytes()); + assert!(skipped.skip::(false).is_some(), "{token}"); + let mut read = Reader::new(bytes.as_bytes()); + assert_eq!(read.read_without_context::().unwrap().as_f64(), 0.0, "{token}"); + assert_eq!(read.offset(), token.len(), "{token}"); + assert_eq!(read.offset(), skipped.offset(), "{token}"); + } + } + + #[test] + fn tolerant_zero_tokens_still_require_number_terminators() { + for token in ["+abc", "-abc", ".abc", "+.abc", "-.abc", "+1abc", "-1abc", "1.2abc"] { + let mut read = Reader::new(token.as_bytes()); + assert!(read.read_without_context::().is_none(), "{token}"); + assert_eq!(read.offset(), 0, "failed reads restore their offset"); + assert!(Reader::new(token.as_bytes()).skip::(false).is_none(), "{token}"); + } + } + + #[test] + fn int_1() { + assert_eq!( + Reader::new("0".as_bytes()) + .read_without_context::() + .unwrap(), + 0 + ); + } + + #[test] + fn int_3() { + assert_eq!( + Reader::new("+32".as_bytes()) + .read_without_context::() + .unwrap(), + 32 + ); + } + + #[test] + fn int_4() { + assert_eq!( + Reader::new("-32".as_bytes()) + .read_without_context::() + .unwrap(), + -32 + ); + } + + #[test] + fn int_6() { + assert_eq!( + Reader::new("98349".as_bytes()) + .read_without_context::() + .unwrap(), + 98349 + ); + } + + #[test] + fn int_7() { + assert_eq!( + Reader::new("003245".as_bytes()) + .read_without_context::() + .unwrap(), + 3245 + ); + } + + #[test] + fn int_min_does_not_panic() { + assert_eq!( + Reader::new("-9223372036854775808".as_bytes()) + .read_without_context::() + .unwrap(), + i64::MIN + ); + } + + #[test] + fn real_1() { + assert_eq!( + Reader::new("3".as_bytes()) + .read_without_context::() + .unwrap(), + 3.0 + ); + } + + #[test] + fn real_3() { + assert_eq!( + Reader::new("+32".as_bytes()) + .read_without_context::() + .unwrap(), + 32.0 + ); + } + + #[test] + fn real_4() { + assert_eq!( + Reader::new("-32".as_bytes()) + .read_without_context::() + .unwrap(), + -32.0 + ); + } + + #[test] + fn real_5() { + assert_eq!( + Reader::new("-32.01".as_bytes()) + .read_without_context::() + .unwrap(), + -32.01 + ); + } + + #[test] + fn real_6() { + assert_eq!( + Reader::new("-.345".as_bytes()) + .read_without_context::() + .unwrap(), + -0.345 + ); + } + + #[test] + fn real_7() { + assert_eq!( + Reader::new("-.00143".as_bytes()) + .read_without_context::() + .unwrap(), + -0.00143 + ); + } + + #[test] + fn real_8() { + assert_eq!( + Reader::new("-12.0013".as_bytes()) + .read_without_context::() + .unwrap(), + -12.0013 + ); + } + + #[test] + fn real_9() { + assert_eq!( + Reader::new("98349.432534".as_bytes()) + .read_without_context::() + .unwrap(), + 98_349.43 + ); + } + + #[test] + fn real_10() { + assert_eq!( + Reader::new("-34534656.34".as_bytes()) + .read_without_context::() + .unwrap(), + -34534656.34 + ); + } + + #[test] + fn real_failing() { + assert!( + Reader::new("+abc".as_bytes()) + .read_without_context::() + .is_none() + ); + } + + #[test] + fn number_1() { + assert_eq!( + Reader::new("+32".as_bytes()) + .read_without_context::() + .unwrap() + .as_f64() as f32, + 32.0 + ); + } + + #[test] + fn number_2() { + assert_eq!( + Reader::new("-32.01".as_bytes()) + .read_without_context::() + .unwrap() + .as_f64() as f32, + -32.01 + ); + } + + #[test] + fn number_3() { + assert_eq!( + Reader::new("-.345".as_bytes()) + .read_without_context::() + .unwrap() + .as_f64() as f32, + -0.345 + ); + } + + #[test] + fn large_number() { + assert_eq!( + Reader::new("38359922".as_bytes()) + .read_without_context::() + .unwrap() + .as_i64(), + 38359922 + ); + } + + #[test] + fn large_number_2() { + assert_eq!( + Reader::new("4294966260".as_bytes()) + .read_without_context::() + .unwrap(), + 4294966260 + ); + } + + #[test] + fn high_precision_float_does_not_overflow_mantissa() { + let num1 = Reader::new("3874.9606299212600788".as_bytes()) + .read_without_context::() + .unwrap(); + assert!((num1 - 3874.96062992126).abs() < 1e-6, "got {num1}"); + + let num2 = Reader::new("5493.5433070866147318".as_bytes()) + .read_without_context::() + .unwrap(); + assert!((num2 - 5493.543307086615).abs() < 1e-6, "got {num2}"); + + // Verify extreme fractional precision (50 decimal places) truncates safely + let num3 = Reader::new("0.12345678901234567890123456789012345678901234567890".as_bytes()) + .read_without_context::() + .unwrap(); + assert!((num3 - 0.12345678901234568).abs() < 1e-15, "got {num3}"); + + // Integer digits past `u64` scale the value instead of being dropped. + let num4 = Reader::new("123456789012345678901.5".as_bytes()) + .read_without_context::() + .unwrap(); + assert!((num4 / 1.2345678901234568e20 - 1.0).abs() < 1e-12, "got {num4}"); + let num5 = Reader::new("-123456789012345678901".as_bytes()) + .read_without_context::() + .unwrap(); + assert!((num5 / -1.2345678901234568e20 - 1.0).abs() < 1e-12, "got {num5}"); + // Hundreds of digits stay finite. + let huge = "9".repeat(400); + let num6 = Reader::new(huge.as_bytes()).read_without_context::().unwrap(); + assert!(num6.is_finite() && num6 > 1e300, "got {num6}"); + } +} diff --git a/vendor/hayro-syntax/src/object/rect.rs b/vendor/hayro-syntax/src/object/rect.rs new file mode 100644 index 00000000..13f46315 --- /dev/null +++ b/vendor/hayro-syntax/src/object/rect.rs @@ -0,0 +1,83 @@ +//! Rectangles. + +use crate::object::Array; +use crate::object::{Object, ObjectLike}; +use crate::reader::{Readable, ReaderContext, ReaderExt}; + +use crate::reader::Reader; + +/// A rectangle. +#[derive(Copy, Clone, Debug)] +pub struct Rect { + /// The minimum x coordinate. + pub x0: f64, + /// The minimum y coordinate. + pub y0: f64, + /// The maximum x coordinate. + pub x1: f64, + /// The maximum y coordinate. + pub y1: f64, +} + +impl Rect { + /// The empty rectangle at the origin. + pub const ZERO: Self = Self::new(0., 0., 0., 0.); + + /// A new rectangle from minimum and maximum coordinates. + #[inline(always)] + pub const fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Self { + Self { x0, y0, x1, y1 } + } + + /// The intersection of two rectangles. + #[inline] + pub fn intersect(&self, other: Self) -> Self { + let x0 = self.x0.max(other.x0); + let y0 = self.y0.max(other.y0); + let x1 = self.x1.min(other.x1); + let y1 = self.y1.min(other.y1); + Self::new(x0, y0, x1.max(x0), y1.max(y0)) + } + + /// The width of the rectangle. + #[inline] + pub const fn width(&self) -> f64 { + self.x1 - self.x0 + } + + /// The height of the rectangle. + #[inline] + pub const fn height(&self) -> f64 { + self.y1 - self.y0 + } +} + +impl<'a> Readable<'a> for Rect { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let arr = r.read::>(ctx)?; + from_arr(&arr) + } +} + +fn from_arr(array: &Array<'_>) -> Option { + let mut iter = array.iter::(); + let x0 = iter.next()? as f64; + let y0 = iter.next()? as f64; + let x1 = iter.next()? as f64; + let y1 = iter.next()? as f64; + + Some(Rect::new(x0.min(x1), y0.min(y1), x1.max(x0), y1.max(y0))) +} + +impl TryFrom> for Rect { + type Error = (); + + fn try_from(value: Object<'_>) -> Result { + match value { + Object::Array(arr) => from_arr(&arr).ok_or(()), + _ => Err(()), + } + } +} + +impl ObjectLike<'_> for Rect {} diff --git a/vendor/hayro-syntax/src/object/ref.rs b/vendor/hayro-syntax/src/object/ref.rs new file mode 100644 index 00000000..71951f4d --- /dev/null +++ b/vendor/hayro-syntax/src/object/ref.rs @@ -0,0 +1,187 @@ +//! Object references. + +use crate::object::ObjectIdentifier; +use crate::object::ObjectLike; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; +use core::fmt::{Debug, Formatter}; + +/// A reference to an object. +#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)] +pub struct ObjRef { + /// The object number. + pub obj_number: i32, + /// The generation number. + pub gen_number: i32, +} + +impl ObjRef { + /// Create a new object reference. + pub fn new(obj_number: i32, gen_number: i32) -> Self { + Self { + obj_number, + gen_number, + } + } +} + +impl From for ObjectIdentifier { + fn from(value: ObjRef) -> Self { + Self::new(value.obj_number, value.gen_number) + } +} + +impl From for ObjRef { + fn from(value: ObjectIdentifier) -> Self { + Self::new(value.obj_number, value.gen_number) + } +} + +impl Skippable for ObjRef { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + r.skip::(false)?; + r.skip_white_spaces(); + r.skip::(false)?; + r.skip_white_spaces(); + r.forward_tag(b"R")?; + + Some(()) + } +} + +impl Readable<'_> for ObjRef { + fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option { + let obj_ref = r.read_without_context::()?; + r.skip_white_spaces(); + let gen_num = r.read_without_context::()?; + r.skip_white_spaces(); + r.forward_tag(b"R")?; + + Some(Self::new(obj_ref, gen_num)) + } +} + +/// A struct that is either an object or a reference to an object. +#[derive(PartialEq, Eq)] +pub enum MaybeRef { + /// A reference to an object. + Ref(ObjRef), + /// An object. + NotRef(T), +} + +impl MaybeRef { + /// If the object is an object reference, return it. + pub fn as_obj_ref(&self) -> Option { + match self { + Self::Ref(r) => Some(*r), + Self::NotRef(_) => None, + } + } +} + +#[allow(private_bounds)] +impl<'a, T> MaybeRef +where + T: ObjectLike<'a>, +{ + /// Resolve the `MaybeRef` object with the given xref table. + pub(crate) fn resolve(self, ctx: &ReaderContext<'a>) -> Option { + match self { + Self::Ref(r) => ctx.xref().get_with::(r.into(), ctx), + Self::NotRef(t) => Some(t), + } + } +} + +impl TryFrom> for ObjRef { + type Error = (); + + fn try_from(value: MaybeRef) -> Result { + match value { + MaybeRef::Ref(r) => Ok(r), + MaybeRef::NotRef(_) => Err(()), + } + } +} + +impl Debug for MaybeRef +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Self::Ref(r) => write!(f, "{r:?}"), + Self::NotRef(nr) => write!(f, "{nr:?}"), + } + } +} + +impl Skippable for MaybeRef +where + T: Skippable, +{ + fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()> { + r.skip::(is_content_stream) + .or_else(|| r.skip::(is_content_stream)) + .map(|_| {}) + } +} + +impl<'a, T> Readable<'a> for MaybeRef +where + T: Readable<'a>, +{ + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + if let Some(obj) = r.read::(ctx) { + Some(Self::Ref(obj)) + } else { + Some(Self::NotRef(r.read::(ctx)?)) + } + } +} + +#[cfg(test)] +mod tests { + use crate::object::ObjRef; + use crate::reader::Reader; + use crate::reader::ReaderExt; + + #[test] + fn ref_1() { + assert_eq!( + Reader::new("34 1 R".as_bytes()) + .read_without_context::() + .unwrap(), + ObjRef::new(34, 1) + ); + } + + #[test] + fn ref_trailing() { + assert_eq!( + Reader::new("256 0 R (hi)".as_bytes()) + .read_without_context::() + .unwrap(), + ObjRef::new(256, 0) + ); + } + + #[test] + fn ref_invalid_1() { + assert!( + Reader::new("256 R".as_bytes()) + .read_without_context::() + .is_none() + ); + } + + #[test] + fn ref_invalid_2() { + assert!( + Reader::new("256 257".as_bytes()) + .read_without_context::() + .is_none() + ); + } +} diff --git a/vendor/hayro-syntax/src/object/stream.rs b/vendor/hayro-syntax/src/object/stream.rs new file mode 100644 index 00000000..c975ed9f --- /dev/null +++ b/vendor/hayro-syntax/src/object/stream.rs @@ -0,0 +1,466 @@ +//! Streams. + +use crate::crypto::DecryptionTarget; +use crate::filter::Filter; +use crate::object; +use crate::object::Dict; +use crate::object::Name; +use crate::object::dict::keys::{DECODE_PARMS, DP, F, FILTER, LENGTH, TYPE}; +use crate::object::{Array, ObjectIdentifier}; +use crate::object::{Object, ObjectLike, ObjectRefLike}; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; +use crate::trivia::is_white_space_character; +use crate::util::{OptionLog, find_needle}; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use core::fmt::{Debug, Formatter}; +use smallvec::SmallVec; + +struct FiltersAndParams<'a> { + filters: SmallVec<[Filter; 2]>, + params: SmallVec<[Dict<'a>; 2]>, +} + +/// PdfCraft patch: how the decoded data of a stream can be produced (see `Stream::incremental`). +pub(crate) enum Incremental { + /// The stream has no filter, so its data is its own bytes. + Plain, + /// The stream is Flate-compressed without a predictor, so it decodes a piece at a time. + Flate, + /// Anything else decodes whole. + Whole, +} + +/// A stream of arbitrary data. +#[derive(Clone)] +pub struct Stream<'a> { + dict: Dict<'a>, + data: &'a [u8], +} + +impl PartialEq for Stream<'_> { + fn eq(&self, other: &Self) -> bool { + self.dict == other.dict && self.data == other.data + } +} + +/// Additional parameters for decoding images. +#[derive(Clone, PartialEq, Default)] +pub struct ImageDecodeParams { + /// Whether the color space of the image is an indexed color space. + pub is_indexed: bool, + /// The bits per component of the image, if that information is available. + pub bpc: Option, + /// The components per channel of the image, if that information is available. + pub num_components: Option, + /// A target resolution for the image. Note that this is only a hint so that + /// in case it's possible, a version of the image will be extracted that + /// is as close as possible to the hinted dimension. + pub target_dimension: Option<(u32, u32)>, + /// The width of the image as indicated by the image dictionary. + pub width: u32, + /// The height of the image as indicated by the image dictionary. + pub height: u32, +} + +impl<'a> Stream<'a> { + pub(crate) fn new(data: &'a [u8], dict: Dict<'a>) -> Self { + Self { dict, data } + } + + fn filters_and_params(&self) -> FiltersAndParams<'a> { + let mut collected_filters = SmallVec::new(); + let mut collected_params = SmallVec::new(); + + if let Some(filter) = self + .dict + .get::>(F) + .or_else(|| self.dict.get::>(FILTER)) + .and_then(Filter::from_name) + { + let params = self + .dict + .get::>(DP) + .or_else(|| self.dict.get::>(DECODE_PARMS)) + .unwrap_or_default(); + + collected_filters.push(filter); + collected_params.push(params); + } else if let Some(filters) = self + .dict + .get::>(F) + .or_else(|| self.dict.get::>(FILTER)) + { + let filters = filters.iter::>().map(Filter::from_name); + let mut params = self + .dict + .get::>(DP) + .or_else(|| self.dict.get::>(DECODE_PARMS)) + .map(|a| a.iter::>()); + + for filter in filters { + let params = params + .as_mut() + .and_then(|p| p.next()) + .and_then(|p| p.into_dict()) + .unwrap_or_default(); + + if let Some(filter) = filter { + collected_filters.push(filter); + collected_params.push(params); + } + } + } + + FiltersAndParams { + filters: collected_filters, + params: collected_params, + } + } + + /// Return the raw, decrypted data of the stream. + /// + /// Stream filters will not be applied. + pub fn raw_data(&self) -> Cow<'a, [u8]> { + let ctx = self.dict.ctx(); + + if ctx.xref().needs_decryption(ctx) + && self + .dict + .get::>(TYPE) + .map(|t| t.as_ref() != b"XRef") + .unwrap_or(true) + { + Cow::Owned( + ctx.xref() + .decrypt(self.obj_id(), self.data, DecryptionTarget::Stream) + // TODO: MAybe an error would be better? + .unwrap_or_default(), + ) + } else { + Cow::Borrowed(self.data) + } + } + + /// Return the raw, underlying dictionary of the stream. + pub fn dict(&self) -> &Dict<'a> { + &self.dict + } + + /// The immutable ordinary-stream decode policy of this stream's document. + pub fn decode_limits(&self) -> crate::DecodeLimits { + self.dict.ctx().decode_limits() + } + + /// Return the object identifier of the stream. + pub fn obj_id(&self) -> ObjectIdentifier { + self.dict.obj_id().unwrap_or_else(|| { + // In theory shouldn't ever happen, but could theoretically be + // triggered in a crafted PDF. + ObjectIdentifier::new(0, 0) + }) + } + + /// Return the filters that are applied to the stream. + pub fn filters(&self) -> SmallVec<[Filter; 2]> { + self.filters_and_params().filters + } + + /// PdfCraft patch: how the decoded data of the stream can be produced a piece at a time. + pub(crate) fn incremental(&self) -> Incremental { + let filters_and_params = self.filters_and_params(); + + match ( + filters_and_params.filters.as_slice(), + filters_and_params.params.first(), + ) { + ([], _) => Incremental::Plain, + ([Filter::FlateDecode], Some(params)) if crate::filter::flate_is_plain(params) => Incremental::Flate, + _ => Incremental::Whole, + } + } + + /// Return the decoded data of the stream. + /// + /// Note that the result of this method will not be cached, so calling it multiple + /// times is expensive. + pub fn decoded(&self) -> Result, DecodeFailure> { + self.decoded_image(&ImageDecodeParams::default()) + .map(|r| r.data) + } + + /// Return the decoded data of the stream, and return image metadata + /// if available. + pub fn decoded_image( + &self, + image_params: &ImageDecodeParams, + ) -> Result, DecodeFailure> { + self.decode_within(image_params, usize::MAX) + } + + /// PdfCraft patch: the decoded data of the stream, for callers that budget what a page + /// holds. Flate, LZW and RunLength stop at `max` bytes (and at + /// this PDF's ordinary stream limit in any case). A stream without filters is returned + /// as it is; ASCIIHex and ASCII85 barely expand, and the image codecs stop at their own pixel + /// limits. + pub fn decoded_within(&self, max: usize) -> Result, DecodeFailure> { + self.decode_within(&ImageDecodeParams::default(), max) + .map(|r| r.data) + } + + fn decode_within( + &self, + image_params: &ImageDecodeParams, + max: usize, + ) -> Result, DecodeFailure> { + self.decode_raw_within(self.raw_data(), image_params, max) + } + + /// PdfCraft patch: decode caller-supplied raw/decrypted bytes with the same filters and limits. + pub(crate) fn decoded_raw_within( + &self, + data: Cow<'a, [u8]>, + max: usize, + ) -> Result, DecodeFailure> { + self.decode_raw_within(data, &ImageDecodeParams::default(), max) + .map(|r| r.data) + } + + fn decode_raw_within( + &self, + data: Cow<'a, [u8]>, + image_params: &ImageDecodeParams, + max: usize, + ) -> Result, DecodeFailure> { + let filters_and_params = self.filters_and_params(); + let count = filters_and_params.filters.len(); + + let mut current: Option> = None; + + for (i, (filter, params)) in filters_and_params + .filters + .iter() + .zip(filters_and_params.params.iter()) + .enumerate() + { + // PdfCraft patch: see `filter::decode_limit`. + let limit = crate::filter::decode_limit(image_params, i + 1 == count, self.dict.ctx().decode_limits()).min(max); + let new = filter.apply(current.as_ref().map(|c| c.data.as_ref()).unwrap_or(&data), params, image_params, limit)?; + current = Some(new); + } + + Ok(current.unwrap_or(FilterResult { + data, + image_data: None, + })) + } +} + +impl Debug for Stream<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "Stream (len: {:?})", self.data.len()) + } +} + +impl Skippable for Stream<'_> { + fn skip(_: &mut Reader<'_>, _: bool) -> Option<()> { + // A stream can never appear in a dict/array, so it should never be skipped. + warn!("attempted to skip a stream object"); + + None + } +} + +impl<'a> Readable<'a> for Stream<'a> { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let dict = r.read_with_context::>(ctx)?; + + if dict.contains_key(F) { + warn!("encountered stream referencing external file, which is unsupported"); + + return None; + } + + let offset = r.offset(); + parse_proper(r, &dict) + .or_else(|| { + warn!("failed to parse stream, trying to parse it manually"); + + r.jump(offset); + parse_fallback(r, &dict) + }) + .error_none("was unable to manually parse the stream") + } +} + +#[derive(Debug, Copy, Clone)] +/// A failure that can occur during decoding a data stream. +pub enum DecodeFailure { + /// An image stream failed to decode. + ImageDecode, + /// A data stream failed to decode. + StreamDecode, + /// A failure occurred while decrypting a file. + Decryption, + /// An unknown failure occurred. + Unknown, +} + +/// An image color space. +#[derive(Debug, Copy, Clone)] +pub enum ImageColorSpace { + /// Grayscale color space. + Gray, + /// RGB color space. + Rgb, + /// CMYK color space. + Cmyk, + /// An unknown color space. + Unknown(u8), +} + +/// Additional data that is extracted from some image streams. +pub struct ImageData { + /// An optional alpha channel of the image. + pub alpha: Option>, + /// The color space of the image. + pub color_space: Option, + /// The bits per component of the image. + pub bits_per_component: u8, + /// The width of the image. + pub width: u32, + /// The height of the image. + pub height: u32, +} + +/// The result of applying a filter. +pub struct FilterResult<'a> { + /// The decoded data. + pub data: Cow<'a, [u8]>, + /// Additional data that is extracted from JPX image streams. + pub image_data: Option, +} + +impl FilterResult<'_> { + pub(crate) fn from_data(data: Vec) -> Self { + Self { + data: Cow::Owned(data), + image_data: None, + } + } +} + +fn parse_proper<'a>(r: &mut Reader<'a>, dict: &Dict<'a>) -> Option> { + let length = dict.get::(LENGTH)?; + + r.skip_white_spaces_and_comments(); + r.forward_tag(b"stream")?; + r.forward_tag(b"\n") + .or_else(|| r.forward_tag(b"\r\n")) + .or_else(|| r.forward_tag(b"\r"))?; + let data = r.read_bytes(length as usize)?; + r.skip_white_spaces(); + r.forward_tag(b"endstream")?; + + Some(Stream::new(data, dict.clone())) +} + +fn parse_fallback<'a>(r: &mut Reader<'a>, dict: &Dict<'a>) -> Option> { + // PdfCraft patch: `stream` must be the next token. Scanning ahead to a later + // `stream` made a dictionary look like a stream โ€” in particular an indirect + // `/AP /N` appearance-state dictionary (`<< /Yes 5 0 R /Off 6 0 R >>`, as Quartz + // writes checkboxes). The widget then had no form to draw, so the box disappeared. + // A wrong `/Length` still recovers, because that keyword sits directly after the dict. + r.skip_white_spaces_and_comments(); + r.forward_tag(b"stream")?; + + r.forward_tag(b"\n") + .or_else(|| r.forward_tag(b"\r\n")) + // Technically not allowed, but no reason to not try it. + .or_else(|| r.forward_tag(b"\r"))?; + + let tail = r.tail()?; + let endstream_offset = find_needle(tail, b"endstream")?; + let data_end = trim_trailing_ascii_whitespace(&tail[..endstream_offset]); + let data = tail.get(..data_end)?; + + r.read_bytes(endstream_offset)?; + r.skip_white_spaces(); + r.forward_tag(b"endstream")?; + + Some(Stream::new(data, dict.clone())) +} + +fn trim_trailing_ascii_whitespace(data: &[u8]) -> usize { + let mut end = data.len(); + + while data + .get(end.wrapping_sub(1)) + .copied() + .is_some_and(is_white_space_character) + { + end -= 1; + } + + end +} + +impl<'a> TryFrom> for Stream<'a> { + type Error = (); + + fn try_from(value: Object<'a>) -> Result { + match value { + Object::Stream(s) => Ok(s), + _ => Err(()), + } + } +} + +impl<'a> ObjectLike<'a> for Stream<'a> {} +impl<'a> ObjectRefLike<'a> for Stream<'a> { + fn cast_ref<'b>(obj: &'b Object<'a>) -> Option<&'b Self> { + match obj { + Object::Stream(stream) => Some(stream), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::object::Stream; + use crate::reader::Reader; + use crate::reader::{ReaderContext, ReaderExt}; + + #[test] + fn stream() { + let data = b"<< /Length 10 >> stream\nabcdefghij\nendstream"; + let mut r = Reader::new(data); + let stream = r + .read_with_context::>(&ReaderContext::dummy()) + .unwrap(); + + assert_eq!(stream.data, b"abcdefghij"); + } + + #[test] + fn stream_fallback() { + let data = b"<< /Length 999 >> stream\nabcdefghij\nendstream"; + let mut r = Reader::new(data); + let stream = r + .read_with_context::>(&ReaderContext::dummy()) + .unwrap(); + + assert_eq!(stream.data, b"abcdefghij"); + } + + /// A dictionary followed by some other object's stream is not itself a stream. + /// `parse_fallback` used to latch onto that later `stream` keyword. + #[test] + fn dictionary_is_not_a_later_objects_stream() { + let data = b"<< /Yes 5 0 R /Off 6 0 R >>\nendobj\n11 0 obj << /Length 5 >> stream\nhello\nendstream"; + let mut r = Reader::new(data); + assert!(r.read_with_context::>(&ReaderContext::dummy()).is_none()); + } +} diff --git a/vendor/hayro-syntax/src/object/string.rs b/vendor/hayro-syntax/src/object/string.rs new file mode 100644 index 00000000..c168d593 --- /dev/null +++ b/vendor/hayro-syntax/src/object/string.rs @@ -0,0 +1,548 @@ +//! Strings. + +use crate::crypto::DecryptionTarget; +use crate::filter::ascii_hex; +use crate::object::Object; +use crate::object::macros::object; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; +use crate::trivia::is_white_space_character; +use alloc::vec::Vec; +use core::borrow::Borrow; +use core::hash::{Hash, Hasher}; +use core::ops::Deref; +use smallvec::SmallVec; + +#[derive(Clone)] +enum StringInner<'a> { + Borrowed(&'a [u8]), + Owned(SmallVec<[u8; 23]>), +} + +impl AsRef<[u8]> for StringInner<'_> { + fn as_ref(&self) -> &[u8] { + match self { + Self::Borrowed(data) => data, + Self::Owned(data) => data, + } + } +} + +/// A PDF string object. +#[derive(Clone)] +pub struct String<'a>(StringInner<'a>); + +impl<'a> String<'a> { + /// Returns the string data as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + self.as_ref() + } +} + +impl Deref for String<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl AsRef<[u8]> for String<'_> { + fn as_ref(&self) -> &[u8] { + match &self.0 { + StringInner::Borrowed(data) => data, + StringInner::Owned(data) => data, + } + } +} + +impl Borrow<[u8]> for String<'_> { + fn borrow(&self) -> &[u8] { + self.as_ref() + } +} + +impl PartialEq for String<'_> { + fn eq(&self, other: &Self) -> bool { + self.as_ref() == other.as_ref() + } +} + +impl Eq for String<'_> {} + +impl Hash for String<'_> { + fn hash(&self, state: &mut H) { + self.as_ref().hash(state); + } +} + +impl core::fmt::Debug for String<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + <[u8] as core::fmt::Debug>::fmt(self.as_ref(), f) + } +} + +object!(String<'a>, String); + +impl Skippable for String<'_> { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + match r.peek_byte()? { + b'<' => skip_hex(r), + b'(' => skip_literal(r), + _ => None, + } + } +} + +impl<'a> Readable<'a> for String<'a> { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + let decoded = match r.peek_byte()? { + b'<' => StringInner::Owned(read_hex(r)?), + b'(' => read_literal(r)?, + _ => return None, + }; + + // Apply decryption if needed. + let final_data = if ctx.xref().needs_decryption(ctx) { + if let Some(obj_number) = ctx.obj_number() { + ctx.xref() + .decrypt(obj_number, decoded.as_ref(), DecryptionTarget::String) + .map(StringInner::from) + .unwrap_or(decoded) + } else { + decoded + } + } else { + decoded + }; + + Some(Self(final_data)) + } +} + +impl From> for StringInner<'_> { + fn from(value: Vec) -> Self { + Self::Owned(SmallVec::from_vec(value)) + } +} + +fn skip_hex(r: &mut Reader<'_>) -> Option<()> { + r.forward_tag(b"<")?; + while let Some(b) = r.peek_byte() { + let is_hex = b.is_ascii_hexdigit(); + let is_whitespace = is_white_space_character(b); + + if !is_hex && !is_whitespace { + break; + } + + r.read_byte()?; + } + r.forward_tag(b">")?; + + Some(()) +} + +fn read_hex(r: &mut Reader<'_>) -> Option> { + let start = r.offset(); + skip_hex(r)?; + let end = r.offset(); + + // Exclude outer brackets. + let raw = r.range(start + 1..end - 1)?; + let decoded = ascii_hex::decode_into(raw)?; + + Some(decoded) +} + +fn skip_literal(r: &mut Reader<'_>) -> Option<()> { + r.forward_tag(b"(")?; + let mut bracket_counter = 1; + + while bracket_counter > 0 { + let byte = r.read_byte()?; + + match byte { + b'\\' => { + let _ = r.read_byte()?; + } + b'(' => bracket_counter += 1, + b')' => bracket_counter -= 1, + _ => {} + }; + } + + Some(()) +} + +fn read_literal<'a>(r: &mut Reader<'a>) -> Option> { + let start = r.offset(); + skip_literal(r)?; + let end = r.offset(); + + // Exclude outer parentheses. + let data = r.range(start + 1..end - 1)?; + + if !data.iter().any(|b| matches!(b, b'\\' | b'\n' | b'\r')) { + return Some(StringInner::Borrowed(data)); + } + + let mut r = Reader::new(data); + let mut result = SmallVec::new(); + + while let Some(byte) = r.read_byte() { + match byte { + b'\\' => { + let next = r.read_byte()?; + + if is_octal_digit(next) { + let second = r.read_byte(); + let third = r.read_byte(); + + let bytes = match (second, third) { + (Some(n1), Some(n2)) => match (is_octal_digit(n1), is_octal_digit(n2)) { + (true, true) => [next, n1, n2], + (true, _) => { + r.jump(r.offset() - 1); + [b'0', next, n1] + } + _ => { + r.jump(r.offset() - 2); + [b'0', b'0', next] + } + }, + (Some(n1), None) => { + if is_octal_digit(n1) { + [b'0', next, n1] + } else { + r.jump(r.offset() - 1); + [b'0', b'0', next] + } + } + _ => [b'0', b'0', next], + }; + + let str = core::str::from_utf8(&bytes).unwrap(); + + if let Ok(num) = u8::from_str_radix(str, 8) { + result.push(num); + } else { + warn!("overflow occurred while parsing octal literal string"); + } + } else { + match next { + b'n' => result.push(0xA), + b'r' => result.push(0xD), + b't' => result.push(0x9), + b'b' => result.push(0x8), + b'f' => result.push(0xC), + b'(' => result.push(b'('), + b')' => result.push(b')'), + b'\\' => result.push(b'\\'), + b'\n' | b'\r' => { + // A conforming reader shall disregard the REVERSE SOLIDUS + // and the end-of-line marker following it when reading + // the string; the resulting string value shall be + // identical to that which would be read if the string + // were not split. + r.skip_eol_characters(); + } + _ => result.push(next), + } + } + } + b'(' | b')' => result.push(byte), + // An end-of-line marker appearing within a literal string + // without a preceding REVERSE SOLIDUS shall be treated as + // a byte value of (0Ah), irrespective of whether the end-of-line + // marker was a CARRIAGE RETURN (0Dh), a LINE FEED (0Ah), or both. + b'\n' | b'\r' => { + result.push(b'\n'); + r.skip_eol_characters(); + } + other => result.push(other), + } + } + + Some(StringInner::Owned(result)) +} + +fn is_octal_digit(byte: u8) -> bool { + matches!(byte, b'0'..=b'7') +} + +#[cfg(test)] +mod tests { + use crate::object::String; + use crate::reader::Reader; + use crate::reader::ReaderExt; + + #[test] + fn hex_string_empty() { + assert_eq!( + Reader::new(b"<>") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"" + ); + } + + #[test] + fn hex_string_1() { + assert_eq!( + Reader::new(b"<00010203>") + .read_without_context::>() + .unwrap() + .as_bytes(), + &[0x00, 0x01, 0x02, 0x03] + ); + } + + #[test] + fn hex_string_2() { + assert_eq!( + Reader::new(b"<000102034>") + .read_without_context::>() + .unwrap() + .as_bytes(), + &[0x00, 0x01, 0x02, 0x03, 0x40] + ); + } + + #[test] + fn hex_string_trailing_1() { + assert_eq!( + Reader::new(b"<000102034>dfgfg4") + .read_without_context::>() + .unwrap() + .as_bytes(), + &[0x00, 0x01, 0x02, 0x03, 0x40] + ); + } + + #[test] + fn hex_string_trailing_2() { + assert_eq!( + Reader::new(b"<1 3 4>dfgfg4") + .read_without_context::>() + .unwrap() + .as_bytes(), + &[0x13, 0x40] + ); + } + + #[test] + fn hex_string_trailing_3() { + assert_eq!( + Reader::new(b"<1>dfgfg4") + .read_without_context::>() + .unwrap() + .as_bytes(), + &[0x10] + ); + } + + #[test] + fn hex_string_invalid_1() { + assert!( + Reader::new(b"<") + .read_without_context::>() + .is_none() + ); + } + + #[test] + fn hex_string_invalid_2() { + assert!( + Reader::new(b"34AD") + .read_without_context::>() + .is_none() + ); + } + + #[test] + fn literal_string_empty() { + assert_eq!( + Reader::new(b"()") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"" + ); + } + + #[test] + fn literal_string_1() { + assert_eq!( + Reader::new(b"(Hi there.)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi there." + ); + } + + #[test] + fn literal_string_2() { + assert!( + Reader::new(b"(Hi \\777)") + .read_without_context::>() + .is_some() + ); + } + + #[test] + fn literal_string_3() { + assert_eq!( + Reader::new(b"(Hi ) there.)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi " + ); + } + + #[test] + fn literal_string_4() { + assert_eq!( + Reader::new(b"(Hi (()) there)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi (()) there" + ); + } + + #[test] + fn literal_string_5() { + assert_eq!( + Reader::new(b"(Hi \\()") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi (" + ); + } + + #[test] + fn literal_string_6() { + assert_eq!( + Reader::new(b"(Hi \\\nthere)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi there" + ); + } + + #[test] + fn literal_string_7() { + assert_eq!( + Reader::new(b"(Hi \\05354)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi +54" + ); + } + + #[test] + fn literal_string_8() { + assert_eq!( + Reader::new(b"(\\3)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"\x03" + ); + } + + #[test] + fn literal_string_9() { + assert_eq!( + Reader::new(b"(\\36)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"\x1e" + ); + } + + #[test] + fn literal_string_10() { + assert_eq!( + Reader::new(b"(\\36ab)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"\x1eab" + ); + } + + #[test] + fn literal_string_11() { + assert_eq!( + Reader::new(b"(\\00Y)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"\0Y" + ); + } + + #[test] + fn literal_string_12() { + assert_eq!( + Reader::new(b"(\\0Y)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"\0Y" + ); + } + + #[test] + fn literal_string_trailing() { + assert_eq!( + Reader::new(b"(Hi there.)abcde") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi there." + ); + } + + #[test] + fn literal_string_invalid() { + assert_eq!( + Reader::new(b"(Hi \\778)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi \x3F8" + ); + } + + #[test] + fn string_1() { + assert_eq!( + Reader::new(b"(Hi there.)") + .read_without_context::>() + .unwrap() + .as_bytes(), + b"Hi there." + ); + } + + #[test] + fn string_2() { + assert_eq!( + Reader::new(b"<00010203>") + .read_without_context::>() + .unwrap() + .as_bytes(), + &[0x00, 0x01, 0x02, 0x03] + ); + } +} diff --git a/vendor/hayro-syntax/src/object/tuple.rs b/vendor/hayro-syntax/src/object/tuple.rs new file mode 100644 index 00000000..4a7190e9 --- /dev/null +++ b/vendor/hayro-syntax/src/object/tuple.rs @@ -0,0 +1,174 @@ +use crate::object::r#ref::MaybeRef; +use crate::object::{Object, ObjectLike}; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt}; +use core::fmt::Debug; +// Note that tuples don't correspond to any specific PDF object. Instead, they simply +// represent a number of PDF objects that are only separated by whitespaces, i.e. +// in an array. We only have those implementations so that it is easier to iterate +// over tuples of items in a PDF array, which happens quite often. + +impl<'a, T, U> Readable<'a> for (T, U) +where + T: ObjectLike<'a>, + U: ObjectLike<'a>, +{ + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + r.skip_white_spaces_and_comments(); + let t = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let u = r.read::>(ctx)?.resolve(ctx)?; + Some((t, u)) + } +} + +impl<'a, T, U> TryFrom> for (T, U) +where + T: ObjectLike<'a>, + U: ObjectLike<'a>, +{ + type Error = (); + + fn try_from(_: Object<'a>) -> Result { + Err(()) + } +} + +impl<'a, T, U> ObjectLike<'a> for (T, U) +where + T: ObjectLike<'a> + Debug + Clone, + U: ObjectLike<'a> + Debug + Clone, +{ +} + +impl<'a, T, U, V> Readable<'a> for (T, U, V) +where + T: ObjectLike<'a>, + U: ObjectLike<'a>, + V: ObjectLike<'a>, +{ + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + r.skip_white_spaces_and_comments(); + let t = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let u = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let v = r.read::>(ctx)?.resolve(ctx)?; + + Some((t, u, v)) + } +} + +impl<'a, T, U, V> TryFrom> for (T, U, V) +where + T: ObjectLike<'a>, + U: ObjectLike<'a>, + V: ObjectLike<'a>, +{ + type Error = (); + + fn try_from(_: Object<'a>) -> Result { + Err(()) + } +} + +impl<'a, T, U, V> ObjectLike<'a> for (T, U, V) +where + T: ObjectLike<'a> + Debug + Clone, + U: ObjectLike<'a> + Debug + Clone, + V: ObjectLike<'a> + Debug + Clone, +{ +} + +impl<'a, T, U, V, W> Readable<'a> for (T, U, V, W) +where + T: ObjectLike<'a>, + U: ObjectLike<'a>, + V: ObjectLike<'a>, + W: ObjectLike<'a>, +{ + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + r.skip_white_spaces_and_comments(); + let t = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let u = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let v = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let w = r.read::>(ctx)?.resolve(ctx)?; + + Some((t, u, v, w)) + } +} + +impl<'a, T, U, V, W> TryFrom> for (T, U, V, W) +where + T: ObjectLike<'a>, + U: ObjectLike<'a>, + V: ObjectLike<'a>, + W: ObjectLike<'a>, +{ + type Error = (); + + fn try_from(_: Object<'a>) -> Result { + Err(()) + } +} + +impl<'a, T, U, V, W> ObjectLike<'a> for (T, U, V, W) +where + T: ObjectLike<'a> + Debug + Clone, + U: ObjectLike<'a> + Debug + Clone, + V: ObjectLike<'a> + Debug + Clone, + W: ObjectLike<'a> + Debug + Clone, +{ +} + +impl<'a, T, U, V, W, X> Readable<'a> for (T, U, V, W, X) +where + T: ObjectLike<'a>, + U: ObjectLike<'a>, + V: ObjectLike<'a>, + W: ObjectLike<'a>, + X: ObjectLike<'a>, +{ + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option { + r.skip_white_spaces_and_comments(); + let t = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let u = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let v = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let w = r.read::>(ctx)?.resolve(ctx)?; + r.skip_white_spaces_and_comments(); + let x = r.read::>(ctx)?.resolve(ctx)?; + + Some((t, u, v, w, x)) + } +} + +impl<'a, T, U, V, W, X> TryFrom> for (T, U, V, W, X) +where + T: ObjectLike<'a>, + U: ObjectLike<'a>, + V: ObjectLike<'a>, + W: ObjectLike<'a>, + X: ObjectLike<'a>, +{ + type Error = (); + + fn try_from(_: Object<'a>) -> Result { + Err(()) + } +} + +impl<'a, T, U, V, W, X> ObjectLike<'a> for (T, U, V, W, X) +where + T: ObjectLike<'a> + Debug + Clone, + U: ObjectLike<'a> + Debug + Clone, + V: ObjectLike<'a> + Debug + Clone, + W: ObjectLike<'a> + Debug + Clone, + X: ObjectLike<'a> + Debug + Clone, +{ +} diff --git a/vendor/hayro-syntax/src/page.rs b/vendor/hayro-syntax/src/page.rs new file mode 100644 index 00000000..20d6adef --- /dev/null +++ b/vendor/hayro-syntax/src/page.rs @@ -0,0 +1,1308 @@ +//! Reading the pages of a PDF document. + +use crate::content::{TypedIter, UntypedIter}; +use crate::object::Array; +use crate::object::Dict; +use crate::object::Name; +use crate::object::Rect; +use crate::object::Stream; +use crate::object::dict::keys::*; +use crate::object::{MaybeRef, Object, ObjectLike}; +use crate::object::array::ArrayIter; +use crate::reader::ReaderContext; +use crate::sync::OnceLock; +use crate::transform::Transform; +use crate::util::FloatExt; +use crate::xref::XRef; +use crate::filter::{FlateState, MAX_DECODED_STREAM}; +use crate::object::stream::Incremental; +use alloc::borrow::Cow; +use alloc::boxed::Box; +use alloc::collections::{BTreeSet, VecDeque}; +use alloc::vec; +use alloc::vec::Vec; +use core::ops::Deref; + +/// Attributes that can be inherited. +#[derive(Debug, Clone)] +struct PagesContext { + media_box: Option, + crop_box: Option, + rotate: Option, +} + +impl PagesContext { + fn new() -> Self { + Self { + media_box: None, + crop_box: None, + rotate: None, + } + } +} + +/// A structure holding the pages of a PDF document. +pub struct Pages<'a> { + pages: Vec>, + xref: &'a XRef, +} + +impl<'a> Pages<'a> { + /// Create a new `Pages` object within a bounded amount of page-tree work. + fn new( + pages_dict: &Dict<'a>, + ctx: &ReaderContext<'a>, + xref: &'a XRef, + work_limit: usize, + ) -> Result { + let mut pages = vec![]; + resolve_pages(pages_dict, &mut pages, ctx, work_limit)?; + + Ok(Self { pages, xref }) + } + + /// Create a new `Pages` object by bruteforce-searching. + /// + /// Of course this could result in the order of pages being messed up, but + /// this is still better than nothing. + pub(crate) fn new_brute_force(ctx: &ReaderContext<'a>, xref: &'a XRef) -> Option { + let mut pages = vec![]; + + for object in xref.objects() { + if let Some(dict) = object.into_dict() + && let Some(page) = Page::new( + &dict, + &PagesContext::new(), + Resources::new(Dict::empty(), None, ctx), + true, + ) + { + pages.push(page); + } + } + + if pages.is_empty() { + return None; + } + + Some(Self { pages, xref }) + } + + /// Return the xref table (of the document the pages belong to). + pub fn xref(&self) -> &'a XRef { + self.xref + } +} + +impl<'a> Deref for Pages<'a> { + type Target = [Page<'a>]; + + fn deref(&self) -> &Self::Target { + &self.pages + } +} + +/// Craft patch: bound traversal work, not valid indirect page-tree depth. The root +/// and every raw `/Kids` entry consume one unit, including malformed, direct and +/// repeated entries. This also bounds the stack, visited set and returned pages. +/// Budget exhaustion or failed frame/page reservation rejects the whole traversal; +/// it must not trigger the unbounded brute-force page-repair fallback. +const MAX_PAGE_TREE_WORK: usize = 100_000; + +#[derive(Debug, PartialEq, Eq)] +enum PageTreeError { + Malformed, + Limit, +} + +struct PageTreeFrame<'a> { + kids: ArrayIter<'a>, + ctx: PagesContext, + resources: Resources<'a>, +} + +impl<'a> PageTreeFrame<'a> { + fn new(dict: &Dict<'a>, mut ctx: PagesContext, resources: Resources<'a>) -> Option { + ctx.media_box = dict.get::(MEDIA_BOX).or(ctx.media_box); + ctx.crop_box = dict.get::(CROP_BOX).or(ctx.crop_box); + ctx.rotate = dict.get::(ROTATE).or(ctx.rotate); + let resources = Resources::from_scope(dict.get::>(RESOURCES), resources); + + Some(Self { kids: dict.get::>(KIDS)?.raw_iter(), ctx, resources }) + } +} + +/// PdfCraft patch: the largest `/UserUnit` honoured, as in Acrobat (a larger value counts as +/// this one). +const MAX_USER_UNIT: f64 = 75_000.0; + +fn resolve_pages<'a>( + pages_dict: &Dict<'a>, + entries: &mut Vec>, + reader_ctx: &ReaderContext<'a>, + work_limit: usize, +) -> Result<(), PageTreeError> { + let mut remaining = work_limit.checked_sub(1).ok_or(PageTreeError::Limit)?; + let root = + PageTreeFrame::new(pages_dict, PagesContext::new(), Resources::new(Dict::empty(), None, reader_ctx)).ok_or(PageTreeError::Malformed)?; + let mut visited = BTreeSet::new(); + if let Some(id) = pages_dict.obj_id() { + visited.insert(id); + } + let mut stack = Vec::new(); + stack.try_reserve(1).map_err(|_| PageTreeError::Limit)?; + stack.push(root); + + while let Some(frame) = stack.last_mut() { + let Some(kid) = frame.kids.next() else { + stack.pop(); + continue; + }; + remaining = remaining.checked_sub(1).ok_or(PageTreeError::Limit)?; + + let dict = match kid { + MaybeRef::Ref(id) => { + // Check raw references before resolution: resolving a cyclic or + // invalid reference through a typed iterator can end that iterator + // and hide all later siblings. Leaves are deduplicated as well. + if !visited.insert(id.into()) { + continue; + } + // Resolve each indirect child with the original document context, + // preserving decode limits without accumulating an ancestor chain. + reader_ctx.xref().get_with::>(id.into(), reader_ctx) + } + MaybeRef::NotRef(object) => object.into_dict(), + }; + let Some(dict) = dict else { + continue; + }; + + match dict.get::>(TYPE).as_deref() { + Some(PAGES) => { + if let Some(child) = PageTreeFrame::new(&dict, frame.ctx.clone(), frame.resources.clone()) { + stack.try_reserve(1).map_err(|_| PageTreeError::Limit)?; + stack.push(child); + } + } + // Let's be lenient and assume it's a `Page` in case it's `None` or something else + // (see corpus test case 0083781). + _ => { + if let Some(page) = Page::new(&dict, &frame.ctx, frame.resources.clone(), false) { + entries.try_reserve(1).map_err(|_| PageTreeError::Limit)?; + entries.push(page); + } + } + } + } + + Ok(()) +} + +/// The rotation of the page. +#[derive(Debug, Copy, Clone)] +pub enum Rotation { + /// No rotation. + None, + /// A rotation of 90 degrees. + Horizontal, + /// A rotation of 180 degrees. + Flipped, + /// A rotation of 270 degrees. + FlippedHorizontal, +} + +/// PdfCraft patch: the cut-off for content that expands from its compressed streams. The decoded +/// content of a page may be `MAX_CONTENT_EXPANSION` times the size of those streams, and never less +/// than `MAX_DECODED_STREAM`. Real content expands a few times, and a stream that inflates a +/// thousandfold from a few kilobytes is a decompression bomb. +const MAX_CONTENT_EXPANSION: usize = 64; + +/// PdfCraft patch: the decoded content of a page, produced a piece at a time, so that the whole of +/// it need not be in memory at once (see `interpret_page`). Plain streams and Flate streams without +/// a predictor are decoded as they are read. Any other stream is decoded whole when the reader is +/// made, as `page_stream` does, and the decoded streams share one `MAX_DECODED_STREAM`. +/// +/// The decoded bytes of the page's streams are also cut off at a budget: `MAX_CONTENT_EXPANSION` +/// times their compressed size, counting each distinct stream once however many times `/Contents` +/// names it. Content that reaches the budget is cut short, and [`ContentReader::truncated`] says so. +pub struct ContentReader<'a> { + pieces: VecDeque>, + budget: usize, + produced: usize, + materialized: usize, + truncated: bool, +} + +/// One part of the content of a page, in order. +enum Piece<'a> { + /// A stream's own bytes, which are not decoded. + Plain { data: Cow<'a, [u8]>, pos: usize }, + /// A Flate stream, decoded as it is read. + Flate { data: Cow<'a, [u8]>, state: FlateState }, + /// A stream that was decoded whole. + Whole { data: Vec, pos: usize }, + /// The space that separates the streams of a `/Contents` array. + Separator { written: bool }, +} + +impl Piece<'_> { + fn exhausted(&self) -> bool { + match self { + Piece::Plain { data, pos } => *pos >= data.len(), + Piece::Flate { data, state } => state.finished(data), + Piece::Whole { data, pos } => *pos >= data.len(), + Piece::Separator { written } => *written, + } + } + + fn incomplete(&self) -> bool { + matches!(self, Piece::Flate { state, .. } if !state.complete()) + } + + /// Appends the next bytes of the piece to `out`, which may end at `limit` bytes. + fn append_to(&mut self, out: &mut Vec, limit: usize) { + match self { + Piece::Plain { data, pos } => copy_from(data, pos, out, limit), + Piece::Flate { data, state } => state.decode_into(data, out, limit), + Piece::Whole { data, pos } => copy_from(data, pos, out, limit), + Piece::Separator { written } => { + out.push(b' '); + *written = true; + } + } + } +} + +/// Copies the bytes of `data` from `pos` to `out`, up to `limit` bytes in all. +fn copy_from(data: &[u8], pos: &mut usize, out: &mut Vec, limit: usize) { + let rest = data.get(*pos..).unwrap_or(&[]); + let take = rest.len().min(limit.saturating_sub(out.len())); + out.extend_from_slice(rest.get(..take).unwrap_or(&[])); + *pos += take; +} + +impl<'a> ContentReader<'a> { + /// A reader of no content. + fn empty() -> Self { + Self { + pieces: VecDeque::new(), + budget: 0, + produced: 0, + materialized: 0, + truncated: false, + } + } + + /// A reader of `streams`, the content of a page in order. `separated` says whether they come + /// from an array, whose streams are separated by spaces. `cap` is the most bytes that streams + /// decoded whole may make in all, and at least the budget's floor. + fn new(streams: Vec>, separated: bool, cap: usize) -> Self { + let mut counted = BTreeSet::new(); + let mut compressed = 0_usize; + let mut pieces = VecDeque::new(); + let mut materialized = 0_usize; + // The bytes that the streams decoded whole take up, separators included, as `page_stream` + // counted them against `MAX_DECODED_STREAM`. + let mut whole_used = 0_usize; + let mut accepting_pieces = true; + let mut truncated = false; + for stream in streams { + let newly_counted = counted.insert(stream.obj_id()); + if !accepting_pieces { + truncated = true; + if newly_counted { + compressed = compressed.saturating_add(stream.raw_data().len()); + } + continue; + } + + let incremental = stream.incremental(); + let left = cap.saturating_sub(whole_used); + if matches!(&incremental, Incremental::Whole) && left == 0 { + accepting_pieces = false; + truncated = true; + if newly_counted { + compressed = compressed.saturating_add(stream.raw_data().len()); + } + continue; + } + + let raw = stream.raw_data(); + if newly_counted { + compressed = compressed.saturating_add(raw.len()); + } + + match incremental { + Incremental::Plain => pieces.push_back(Piece::Plain { + data: raw, + pos: 0, + }), + Incremental::Flate => { + let state = FlateState::new(&raw); + pieces.push_back(Piece::Flate { data: raw, state }); + } + Incremental::Whole => { + let limit = left.min(stream.dict().ctx().decode_limits().max_decoded_stream()); + let Ok(data) = stream.decoded_raw_within(raw, limit) else { + truncated = true; + continue; + }; + let mut data = data.into_owned(); + // Whole-stream decoders may stop at the requested limit without exposing + // completion. Treat reaching it as incomplete rather than certifying a prefix. + if data.len() >= limit { + truncated = true; + data.truncate(limit); + } + whole_used = whole_used.saturating_add(data.len()); + materialized = materialized.saturating_add(data.len()); + pieces.push_back(Piece::Whole { data, pos: 0 }); + } + } + + if separated { + whole_used = whole_used.saturating_add(1); + pieces.push_back(Piece::Separator { written: false }); + } + } + let budget = cap.max(MAX_CONTENT_EXPANSION.saturating_mul(compressed)); + + Self { + pieces, + budget, + produced: 0, + materialized, + truncated, + } + } + + /// Appends up to `max` bytes of the decoded content to `out`, and returns how many it appended. + /// That is fewer than `max` only at the end of the content, or where it is cut short. + pub fn read_into(&mut self, out: &mut Vec, max: usize) -> usize { + let start = out.len(); + let end = start.saturating_add(max); + + while out.len() < end { + let Some(piece) = self.pieces.front_mut() else { + break; + }; + if piece.exhausted() { + self.truncated |= piece.incomplete(); + self.pieces.pop_front(); + continue; + } + + let allowed = self.budget.saturating_sub(self.produced); + if allowed == 0 { + self.truncated = true; + self.pieces.clear(); + break; + } + + let before = out.len(); + piece.append_to(out, end.min(before.saturating_add(allowed))); + self.produced += out.len() - before; + } + + out.len() - start + } + + /// The bytes of the content that were decoded whole when the reader was made. The caller + /// charges these against its own budget, as it did for the whole content. + pub fn materialized_len(&self) -> usize { + self.materialized + } + + /// Whether a stream was omitted, failed to decode, or reached a content budget. + /// This reports known incomplete input; it does not certify rendering fidelity. + pub fn truncated(&self) -> bool { + self.truncated + } +} + +/// A PDF page. +pub struct Page<'a> { + inner: Dict<'a>, + media_box: Rect, + crop_box: Rect, + rotation: Rotation, + user_unit: f32, + page_streams: OnceLock>>, + resources: Resources<'a>, + ctx: ReaderContext<'a>, +} + +impl<'a> Page<'a> { + fn new( + dict: &Dict<'a>, + ctx: &PagesContext, + resources: Resources<'a>, + brute_force: bool, + ) -> Option { + // In general, pages without content are allowed, but in case we are brute-forcing + // we ignore them. + if brute_force && !dict.contains_key(CONTENTS) { + return None; + } + + let media_box = dict.get::(MEDIA_BOX).or(ctx.media_box).unwrap_or(A4); + + let crop_box = dict + .get::(CROP_BOX) + .or(ctx.crop_box) + .unwrap_or(media_box); + + let rotation = match dict + .get::(ROTATE) + .or(ctx.rotate) + .unwrap_or(0) + .rem_euclid(360) + { + 0 => Rotation::None, + 90 => Rotation::Horizontal, + 180 => Rotation::Flipped, + 270 => Rotation::FlippedHorizontal, + _ => Rotation::None, + }; + + // PdfCraft patch: the page's own `/UserUnit` (it isn't inherited), read as Acrobat reads + // it: a number from 1 up, at most `MAX_USER_UNIT`; anything else counts as 1. + let user_unit = dict + .get::(USER_UNIT) + .filter(|u| *u >= 1.0) + .map_or(1.0, |u| u.min(MAX_USER_UNIT) as f32); + + let ctx = resources.ctx.clone(); + let resources = Resources::from_scope( + dict.get::>(RESOURCES), + resources, + ); + + Some(Self { + inner: dict.clone(), + media_box, + crop_box, + rotation, + user_unit, + page_streams: OnceLock::new(), + resources, + ctx, + }) + } + + fn operations_impl(&self) -> Option> { + let stream = self.page_stream()?; + let iter = UntypedIter::new(stream); + + Some(iter) + } + + /// PdfCraft patch: the content of the page, to be read a piece at a time (see + /// [`ContentReader`]). Bare Flate streams use the separate incremental content budget, + /// even when the document's ordinary materialization limit is lower. Consequently this + /// can yield more bytes than [`Page::page_stream`] for the same stream. Whole-stream + /// fallback decoders retain the document's ordinary limit. + pub fn content_reader(&self) -> ContentReader<'a> { + self.content_reader_within(MAX_DECODED_STREAM) + } + + /// PdfCraft patch: [`Page::content_reader`], with `cap` in place of `MAX_DECODED_STREAM`. + fn content_reader_within(&self, cap: usize) -> ContentReader<'a> { + if let Some(stream) = self.inner.get::>(CONTENTS) { + ContentReader::new(vec![stream], false, cap) + } else if let Some(array) = self.inner.get::>(CONTENTS) { + ContentReader::new(array.iter::>().collect(), true, cap) + } else { + warn!("contents entry of page was neither stream nor array of streams"); + + ContentReader::empty() + } + } + + /// Return the decoded content stream of the page. + pub fn page_stream(&self) -> Option<&[u8]> { + let convert_single = |s: Stream<'_>| { + let data = s.decoded().ok()?; + // PdfCraft patch: take the decoded buffer instead of copying it (up to + // `MAX_DECODED_STREAM` twice over). + Some(data.into_owned()) + }; + + self.page_streams + .get_or_init(|| { + if let Some(stream) = self.inner.get::>(CONTENTS) { + convert_single(stream) + } else if let Some(array) = self.inner.get::>(CONTENTS) { + let mut collected = vec![]; + + for stream in array.iter::>() { + // PdfCraft patch: the streams share one `MAX_DECODED_STREAM` (an array + // naming one inflating stream twenty times decoded 5 GB). + let left = MAX_DECODED_STREAM.saturating_sub(collected.len()); + if left == 0 { + break; + } + let Ok(data) = stream.decoded_within(left) else { + continue; + }; + // Exactly what this stream and its separator need: growing by doubling + // here held 1 GB for a 256 MiB stream. + collected.reserve_exact(data.len() + 1); + collected.extend_from_slice(&data); + // Streams must have at least one whitespace in-between. + collected.push(b' '); + } + + Some(collected) + } else { + warn!("contents entry of page was neither stream nor array of streams"); + + None + } + }) + .as_ref() + .map(|d| d.as_slice()) + } + + /// Get the resources of the page. + pub fn resources(&self) -> &Resources<'a> { + &self.resources + } + + /// Get the media box of the page. + pub fn media_box(&self) -> Rect { + self.media_box + } + + /// Get the rotation of the page. + pub fn rotation(&self) -> Rotation { + self.rotation + } + + /// Get the crop box of the page. + pub fn crop_box(&self) -> Rect { + self.crop_box + } + + /// PdfCraft patch: the size of the page's user-space unit in points (`/UserUnit`). + pub fn user_unit(&self) -> f32 { + self.user_unit + } + + /// Return the intersection of crop box and media box. + pub fn intersected_crop_box(&self) -> Rect { + self.crop_box().intersect(self.media_box()) + } + + /// Return the base dimensions of the page (same as `intersected_crop_box`, but with special + /// handling applied for zero-area pages). + pub fn base_dimensions(&self) -> (f32, f32) { + let crop_box = self.intersected_crop_box(); + + if (crop_box.width() as f32).is_nearly_zero() || (crop_box.height() as f32).is_nearly_zero() + { + (A4.width() as f32, A4.height() as f32) + } else { + ( + crop_box.width().max(1.0) as f32, + crop_box.height().max(1.0) as f32, + ) + } + } + + /// Return the with and height of the page that should be assumed when rendering the page. + /// + /// Depending on the document, it is either based on the media box or the crop box + /// of the page. In addition to that, it also takes the rotation of the page into account. + pub fn render_dimensions(&self) -> (f32, f32) { + let (base_width, base_height) = self.base_dimensions(); + // PdfCraft patch: in points, after `/UserUnit`. + let (mut base_width, mut base_height) = + (base_width * self.user_unit, base_height * self.user_unit); + + if matches!( + self.rotation(), + Rotation::Horizontal | Rotation::FlippedHorizontal + ) { + core::mem::swap(&mut base_width, &mut base_height); + } + + (base_width, base_height) + } + + /// Return an untyped iterator over the operators of the page's content stream. + pub fn operations(&self) -> UntypedIter<'_> { + self.operations_impl().unwrap_or(UntypedIter::empty()) + } + + /// Get the raw dictionary of the page. + pub fn raw(&self) -> &Dict<'a> { + &self.inner + } + + /// Get the xref table (of the document the page belongs to). + pub fn xref(&self) -> &'a XRef { + self.ctx.xref() + } + + /// Return a typed iterator over the operators of the page's content stream. + pub fn typed_operations(&self) -> TypedIter<'_> { + TypedIter::from_untyped(self.operations()) + } + + /// Return the initial transform that should be applied when rendering. + /// + /// This accounts for the mismatch between PDF's y-up and most renderers' + /// y-down coordinate system, the rotation of the page and the offset of + /// the crop box. + pub fn initial_transform(&self, invert_y: bool) -> Transform { + let crop_box = self.intersected_crop_box(); + let (_, base_height) = self.base_dimensions(); + // PdfCraft patch: the page in points, after `/UserUnit`. + let base_height = base_height * self.user_unit; + let (width, height) = self.render_dimensions(); + + let horizontal_t = Transform::ROTATE_CW_90 * Transform::translate((0.0, -width as f64)); + let flipped_horizontal_t = + Transform::translate((0.0, height as f64)) * Transform::ROTATE_CCW_90; + + let rotation_transform = match self.rotation() { + Rotation::None => Transform::IDENTITY, + Rotation::Horizontal => { + if invert_y { + horizontal_t + } else { + flipped_horizontal_t + } + } + Rotation::Flipped => { + Transform::scale(-1.0) * Transform::translate((-width as f64, -height as f64)) + } + Rotation::FlippedHorizontal => { + if invert_y { + flipped_horizontal_t + } else { + horizontal_t + } + } + }; + + let inversion_transform = if invert_y { + Transform::new([1.0, 0.0, 0.0, -1.0, 0.0, base_height as f64]) + } else { + Transform::IDENTITY + }; + + let user_space = Transform::translate((-crop_box.x0, -crop_box.y0)); + // PdfCraft patch: user space in units of `/UserUnit` points. A page without it keeps + // exactly the transform it had. + let user_space = if self.user_unit == 1.0 { + user_space + } else { + Transform::scale(f64::from(self.user_unit)) * user_space + }; + + rotation_transform * inversion_transform * user_space + } +} + +/// A structure keeping track of the resources of a page. +#[derive(Clone, Debug)] +pub struct Resources<'a> { + parent: Option>, + ctx: ReaderContext<'a>, + /// The raw dictionary of external graphics states. + pub ext_g_states: Dict<'a>, + /// The raw dictionary of fonts. + pub fonts: Dict<'a>, + /// The raw dictionary of properties. + pub properties: Dict<'a>, + /// The raw dictionary of color spaces. + pub color_spaces: Dict<'a>, + /// The raw dictionary of x objects. + pub x_objects: Dict<'a>, + /// The raw dictionary of patterns. + pub patterns: Dict<'a>, + /// The raw dictionary of shadings. + pub shadings: Dict<'a>, +} + +impl<'a> Resources<'a> { + /// Use an object's entire resource dictionary, or inherit the enclosing scope when absent. + /// + /// An explicit dictionary, including an empty one, replaces the enclosing resources; + /// missing names do not fall back individually. The enclosing reader context is retained + /// so indirect resources keep the document's cross-references and decode limits. + pub fn from_scope(resources: Option>, parent: Self) -> Self { + match resources { + Some(resources) => Self::new(resources, None, &parent.ctx), + None => parent, + } + } + + /// Create a new `Resources` object from a dictionary with a parent. + pub fn from_parent(resources: Dict<'a>, parent: Self) -> Self { + let ctx = parent.ctx.clone(); + + Self::new(resources, Some(parent), &ctx) + } + + /// Create a new `Resources` object. + pub(crate) fn new(resources: Dict<'a>, parent: Option, ctx: &ReaderContext<'a>) -> Self { + let ext_g_states = resources.get::>(EXT_G_STATE).unwrap_or_default(); + let fonts = resources.get::>(FONT).unwrap_or_default(); + let color_spaces = resources.get::>(COLORSPACE).unwrap_or_default(); + let x_objects = resources.get::>(XOBJECT).unwrap_or_default(); + let patterns = resources.get::>(PATTERN).unwrap_or_default(); + let shadings = resources.get::>(SHADING).unwrap_or_default(); + let properties = resources.get::>(PROPERTIES).unwrap_or_default(); + + let parent = parent.map(Box::new); + + Self { + parent, + ext_g_states, + fonts, + color_spaces, + properties, + x_objects, + patterns, + shadings, + ctx: ctx.clone(), + } + } + + fn get_resource>(&self, name: &Name<'_>, dict: &Dict<'a>) -> Option { + dict.get::(name.deref()) + } + + /// Get the parent in the resource, chain, if available. + pub fn parent(&self) -> Option<&Self> { + self.parent.as_deref() + } + + /// Get an external graphics state by name. + pub fn get_ext_g_state(&self, name: &Name<'_>) -> Option> { + self.get_resource::>(name, &self.ext_g_states) + .or_else(|| self.parent.as_ref().and_then(|p| p.get_ext_g_state(name))) + } + + /// Get a color space by name. + pub fn get_color_space(&self, name: &Name<'_>) -> Option> { + self.get_resource::>(name, &self.color_spaces) + .or_else(|| self.parent.as_ref().and_then(|p| p.get_color_space(name))) + } + + /// Get a font by name. + pub fn get_font(&self, name: &Name<'_>) -> Option> { + self.get_resource::>(name, &self.fonts) + .or_else(|| self.parent.as_ref().and_then(|p| p.get_font(name))) + } + + /// Get a pattern by name. + pub fn get_pattern(&self, name: &Name<'_>) -> Option> { + self.get_resource::>(name, &self.patterns) + .or_else(|| self.parent.as_ref().and_then(|p| p.get_pattern(name))) + } + + /// Get an x object by name. + pub fn get_x_object(&self, name: &Name<'_>) -> Option> { + self.get_resource::>(name, &self.x_objects) + .or_else(|| self.parent.as_ref().and_then(|p| p.get_x_object(name))) + } + + /// Get a shading by name. + pub fn get_shading(&self, name: &Name<'_>) -> Option> { + self.get_resource::>(name, &self.shadings) + .or_else(|| self.parent.as_ref().and_then(|p| p.get_shading(name))) + } +} + +// +const POINTS_PER_INCH: f64 = 72.0; +const POINTS_PER_MM: f64 = 1.0 / (10.0 * 2.54) * POINTS_PER_INCH; + +/// The dimension of an A4 page. +pub const A4: Rect = Rect { + x0: 0.0, + y0: 0.0, + x1: 210.0 * POINTS_PER_MM, + y1: 297.0 * POINTS_PER_MM, +}; + +pub(crate) mod cached { + use crate::page::{MAX_PAGE_TREE_WORK, PageTreeError, Pages}; + use crate::reader::ReaderContext; + use crate::xref::XRef; + use core::ops::Deref; + + // Keep in sync with the implementation in `sync`. We duplicate it here + // to make it more visible since we have unsafe code here. + #[cfg(feature = "std")] + pub(crate) use std::sync::Arc; + + #[cfg(not(feature = "std"))] + pub(crate) use alloc::rc::Rc as Arc; + + pub(crate) struct CachedPages { + pages: Pages<'static>, + // NOTE: `pages` references the data in `xref`, so it's important that `xref` + // appears after `pages` in the struct definition to ensure correct drop order. + _xref: Arc, + } + + impl CachedPages { + pub(crate) fn new(xref: Arc) -> Option { + Self::new_with_limit(xref, MAX_PAGE_TREE_WORK) + } + + pub(super) fn new_with_limit(xref: Arc, work_limit: usize) -> Option { + // SAFETY: + // - The XRef's location is stable in memory: + // - We wrapped it in a `Arc` (or `Rc` in `no_std`), which implements `StableDeref`. + // - The struct owns the `Arc`, ensuring that the inner value is not dropped during the whole + // duration. + // - The internal 'static lifetime is not leaked because its rewritten + // to the self-lifetime in `pages()`. + let xref_reference: &'static XRef = unsafe { core::mem::transmute(xref.deref()) }; + + let ctx = ReaderContext::new(xref_reference, false); + let pages = match xref_reference.get_with(xref.trailer_data().pages_ref, &ctx) { + Some(p) => match Pages::new(&p, &ctx, xref_reference, work_limit) { + Ok(pages) => pages, + Err(PageTreeError::Malformed) => Pages::new_brute_force(&ctx, xref_reference)?, + // A limit is a failed load, never permission to bypass the bound + // or return a partially walked page tree through repair. + Err(PageTreeError::Limit) => return None, + }, + None => Pages::new_brute_force(&ctx, xref_reference)?, + }; + + Some(Self { pages, _xref: xref }) + } + + pub(crate) fn get(&self) -> &Pages<'_> { + &self.pages + } + } +} + +#[cfg(test)] +mod page_tree_tests { + use super::{MAX_PAGE_TREE_WORK, Rotation, cached}; + use crate::object::{FromBytes, Name}; + use crate::{DecodeLimits, Pdf}; + use core::fmt::Write; + + // Real xref offsets keep repair from masking page-tree traversal failures. + fn pdf_bytes(objects: &[String]) -> Vec { + let mut data = String::from("%PDF-1.7\n"); + let mut offsets = vec![0]; + for (index, object) in objects.iter().enumerate() { + offsets.push(data.len()); + writeln!(data, "{} 0 obj\n{object}\nendobj", index + 1).unwrap(); + } + let xref = data.len(); + writeln!(data, "xref\n0 {}\n0000000000 65535 f ", offsets.len()).unwrap(); + for offset in offsets.iter().skip(1) { + writeln!(data, "{offset:010} 00000 n ").unwrap(); + } + writeln!(data, "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF", offsets.len()).unwrap(); + data.into_bytes() + } + + fn cached_with_limit(objects: &[String], limit: usize) -> Option { + let xref = crate::xref::root_xref(pdf_bytes(objects).into(), b"", DecodeLimits::default()).unwrap(); + cached::CachedPages::new_with_limit(cached::Arc::new(xref), limit) + } + + fn assert_deep_tree() { + let depth = 1024; + let font_id = depth + 3; + let mut objects = vec!["<< /Type /Catalog /Pages 2 0 R >>".to_string()]; + for level in 0..depth { + let attributes = if level == 0 { + "/MediaBox [0 0 100 200] /CropBox [2 3 90 180] /Rotate 90 /Resources << /Font << /Old << /BaseFont /Helvetica >> >> /ExtGState << /G << /CA 0.5 >> >> >>".to_string() + } else if level == depth / 2 { + format!("/MediaBox [0 0 300 400] /Rotate 180 /Resources << /Font << /New {font_id} 0 R >> >>") + } else { + String::new() + }; + objects.push(format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 {attributes} >>", level + 3)); + } + objects.push(format!("<< /Type /Page /Parent {} 0 R >>", depth + 1)); + objects.push("<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>".to_string()); + let limits = DecodeLimits::default().with_max_decoded_stream(31); + let pdf = Pdf::new_with_limits(pdf_bytes(&objects), limits).unwrap(); + assert_eq!(pdf.pages().len(), 1); + let page = &pdf.pages()[0]; + assert_eq!(page.raw().obj_id().unwrap().obj_number, (depth + 2) as i32); + let media = page.media_box(); + assert_eq!((media.x0, media.y0, media.x1, media.y1), (0.0, 0.0, 300.0, 400.0)); + let crop = page.crop_box(); + assert_eq!((crop.x0, crop.y0, crop.x1, crop.y1), (2.0, 3.0, 90.0, 180.0)); + assert!(matches!(page.rotation(), Rotation::Flipped)); + let old = Name::from_bytes(b"/Old").unwrap(); + let new = Name::from_bytes(b"/New").unwrap(); + let state = Name::from_bytes(b"/G").unwrap(); + assert!(page.resources().get_font(&old).is_none()); + assert!(page.resources().get_ext_g_state(&state).is_none()); + assert_eq!(page.resources().get_font(&new).unwrap().get::>(b"BaseFont").unwrap().as_ref(), b"Courier"); + assert_eq!(page.ctx.decode_limits().max_decoded_stream(), 31); + } + + #[test] + fn deeply_nested_indirect_page_tree_preserves_inheritance() { + assert_deep_tree(); + #[cfg(not(target_arch = "wasm32"))] + std::thread::Builder::new().stack_size(1 << 20).spawn(assert_deep_tree).unwrap().join().unwrap(); + } + + #[test] + fn cycles_invalid_children_and_duplicates_keep_later_siblings_in_order() { + let objects = [ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R 4 0 R 3 0 R 7 0 R 5 0 R] /Count 4 /MediaBox [0 0 200 300] /Rotate 90 >>", + "<< /Type /Pages /Kids [2 0 R 999 0 R null 5 0 R 6 0 R] /Rotate 180 >>", + "<< /Type /Page /MediaBox [0 0 40 40] >>", + "<< /Type /Page /MediaBox [0 0 50 50] >>", + "<< /Type /Pages /Kids [3 0 R 8 0 R] /Rotate 270 >>", + "<< /Type /Page /MediaBox [0 0 70 70] >>", + "<< /Type /Page /MediaBox [0 0 80 80] >>", + ] + .map(str::to_string); + let pages = cached_with_limit(&objects, MAX_PAGE_TREE_WORK).unwrap(); + let ids: Vec<_> = pages.get().iter().map(|page| page.raw().obj_id().unwrap().obj_number).collect(); + assert_eq!(ids, [5, 8, 4, 7]); + assert!(matches!(pages.get()[0].rotation(), Rotation::Flipped)); + assert!(matches!(pages.get()[1].rotation(), Rotation::FlippedHorizontal)); + assert!(matches!(pages.get()[2].rotation(), Rotation::Horizontal)); + assert!(matches!(pages.get()[3].rotation(), Rotation::Horizontal)); + } + + #[test] + fn work_limit_counts_root_and_every_entry_and_never_repairs_partial_pages() { + for kids in ["3 0 R 3 0 R", "999 0 R 3 0 R", "null 3 0 R", "7 << /Type /Page /MediaBox [0 0 40 40] /Contents [] >>"] { + let objects = [ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + format!("<< /Type /Pages /Kids [{kids}] /Count 1 >>"), + // The fallback would find this page even after an exhausted walk. + "<< /Type /Page /MediaBox [0 0 40 40] /Contents [] >>".to_string(), + ]; + for limit in 0..=2 { + assert!(cached_with_limit(&objects, limit).is_none(), "{kids}: limit {limit} must fail closed"); + } + assert_eq!(cached_with_limit(&objects, 3).unwrap().get().len(), 1, "{kids}"); + } + } + + #[test] + fn malformed_root_keeps_existing_brute_force_repair() { + let objects = ["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Count 1 >>", "<< /Type /Page /MediaBox [0 0 40 40] /Contents [] >>"] + .map(str::to_string); + let repaired = cached_with_limit(&objects, 1).unwrap(); + assert_eq!(repaired.get().len(), 1); + assert_eq!(repaired.get()[0].raw().obj_id().unwrap().obj_number, 3); + } +} + +#[cfg(test)] +mod resources_scope_tests { + use super::Resources; + use crate::object::{Dict, FromBytes, Name}; + use crate::reader::ReaderContext; + + #[test] + fn explicit_scope_replaces_all_categories_and_absence_inherits() { + let enclosing = Dict::from_bytes(b"<< /Font << /F << /BaseFont /Helvetica >> >> /ExtGState << /G << /CA 0.5 >> >> >>").unwrap(); + let parent = Resources::new(enclosing, None, ReaderContext::dummy_ref()); + let font = Name::from_bytes(b"/F").unwrap(); + let state = Name::from_bytes(b"/G").unwrap(); + let inherited = Resources::from_scope(None, parent.clone()); + assert!(inherited.get_font(&font).is_some()); + assert!(inherited.get_ext_g_state(&state).is_some()); + + for bytes in [b"<< >>".as_slice(), b"<< /ProcSet [/PDF] >>", b"<< /Font << /Other << /BaseFont /Courier >> >> >>"] { + let own = Resources::from_scope(Some(Dict::from_bytes(bytes).unwrap()), parent.clone()); + assert!(own.get_font(&font).is_none(), "an explicit scope must not borrow a font"); + assert!(own.get_ext_g_state(&state).is_none(), "nor another missing resource category"); + assert!(own.parent().is_none()); + } + } +} + +#[cfg(test)] +mod content_reader_tests { + use crate::Pdf; + + /// A zlib stream of `data` in stored blocks, which expands by nothing. + fn stored_zlib(data: &[u8]) -> Vec { + let mut out = vec![0x78, 0x01]; + let mut chunks = data.chunks(0xffff).peekable(); + if chunks.peek().is_none() { + out.extend_from_slice(&[1, 0, 0, 0xff, 0xff]); + } + while let Some(chunk) = chunks.next() { + out.push(u8::from(chunks.peek().is_none())); + let len = u16::try_from(chunk.len()).expect("a stored block holds at most 65535 bytes"); + out.extend_from_slice(&len.to_le_bytes()); + out.extend_from_slice(&(!len).to_le_bytes()); + out.extend_from_slice(chunk); + } + out.extend_from_slice(&[0; 4]); + out + } + + /// A zlib stream of one literal byte and then `count` copies of a 258-byte match from it, in + /// fixed Huffman codes: about 160 bytes of output for each byte of input. + fn fixed_run_bomb(count: usize) -> Vec { + struct Bits { + out: Vec, + buf: u8, + len: u32, + } + impl Bits { + /// Bits are sent least significant first. + fn put(&mut self, value: u32, bits: u32) { + for i in 0..bits { + self.buf |= (((value >> i) & 1) as u8) << self.len; + self.len += 1; + if self.len == 8 { + self.out.push(self.buf); + self.buf = 0; + self.len = 0; + } + } + } + + /// Huffman codes are sent most significant bit first. + fn code(&mut self, code: u32, bits: u32) { + for i in (0..bits).rev() { + self.put((code >> i) & 1, 1); + } + } + } + + let mut bits = Bits { + out: vec![0x78, 0x01], + buf: 0, + len: 0, + }; + bits.put(1, 1); // The final block. + bits.put(1, 2); // Fixed Huffman codes. + bits.code(0x30 + u32::from(b'a'), 8); // The literal 'a'. + for _ in 0..count { + bits.code(0xc5, 8); // Length 258 (code 285), no extra bits. + bits.code(0, 5); // Distance 1 (code 0), no extra bits. + } + bits.code(0, 7); // End of block (code 256). + if bits.len > 0 { + bits.out.push(bits.buf); + } + bits.out.extend_from_slice(&[0; 4]); + bits.out + } + + /// A one-page PDF whose content is `stream`, with the stream dictionary entries `dict`. + fn page_pdf(stream: &[u8], dict: &str) -> Vec { + let mut pdf = 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 40] /Contents 4 0 R >> endobj\n4 0 obj << {dict} /Length {} >> stream\n", + stream.len() + ) + .into_bytes(); + pdf.extend_from_slice(stream); + pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n"); + pdf + } + + /// A one-page PDF with an array of content references to `streams`. + fn page_pdf_array(streams: &[(&[u8], &str)], refs: &[usize]) -> Vec { + let contents = refs + .iter() + .map(|index| format!("{} 0 R", 4 + index)) + .collect::>() + .join(" "); + let mut pdf = 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 40] /Contents [{contents}] >> endobj\n" + ) + .into_bytes(); + for (index, (stream, dict)) in streams.iter().enumerate() { + let object = 4 + index; + pdf.extend_from_slice( + format!("{object} 0 obj << {dict} /Length {} >> stream\n", stream.len()) + .as_bytes(), + ); + pdf.extend_from_slice(stream); + pdf.extend_from_slice(b"\nendstream endobj\n"); + } + pdf.extend_from_slice(b"trailer << /Root 1 0 R >>\n%%EOF\n"); + pdf + } + + /// The content of the first page, read `piece` bytes at a time, with a budget of `cap`. + fn read_content(pdf: Vec, cap: usize, piece: usize) -> (Vec, bool) { + let doc = Pdf::new(pdf).expect("parses"); + let page = doc.pages().first().expect("one page"); + let mut content = page.content_reader_within(cap); + let mut out = Vec::new(); + while content.read_into(&mut out, piece) > 0 {} + + (out, content.truncated()) + } + + #[test] + fn plain_and_whole_pieces_keep_their_bytes() { + let plain = b"q 1 0 0 1 0 0 cm Q\n"; + let whole = b"q 0 0 m Q\n"; + let encoded: String = whole.iter().map(|byte| format!("{byte:02x}")).collect::() + ">"; + + for piece in [1, 7, 4096, 65_536] { + let (plain_out, plain_truncated) = read_content(page_pdf(plain, ""), 1 << 20, piece); + assert_eq!(plain_out, plain, "plain piece {piece}"); + assert!(!plain_truncated, "plain piece {piece}"); + + let (whole_out, whole_truncated) = read_content( + page_pdf(encoded.as_bytes(), "/Filter /ASCIIHexDecode"), + 1 << 20, + piece, + ); + assert_eq!(whole_out, whole, "whole piece {piece}"); + assert!(!whole_truncated, "whole piece {piece}"); + } + } + + #[test] + fn repeated_content_reference_counts_compressed_bytes_once() { + let data = vec![b'q'; 1 << 16]; + let pdf = page_pdf_array(&[(data.as_slice(), "")], &[0, 0]); + let doc = Pdf::new(pdf).expect("parses"); + let page = doc.pages().first().expect("one page"); + let mut content = page.content_reader_within(1 << 20); + let expected_budget = 64 * data.len(); + assert_eq!(content.budget, expected_budget); + + let mut out = Vec::new(); + while content.read_into(&mut out, 4096) > 0 {} + let mut expected = data.clone(); + expected.push(b' '); + expected.extend_from_slice(&data); + expected.push(b' '); + assert!( + out == expected, + "expected {} bytes, got {}", + expected.len(), + out.len() + ); + assert!(!content.truncated()); + } + + #[test] + fn whole_cap_still_counts_later_unique_streams() { + let first = vec![b'q'; 1 << 20]; + let later = vec![b'Q'; 12_000]; + let hex = |data: &[u8]| { + data.iter() + .map(|byte| format!("{byte:02x}")) + .collect::() + + ">" + }; + let first_raw = hex(&first); + let later_raw = hex(&later); + let pdf = page_pdf_array( + &[ + (first_raw.as_bytes(), "/Filter /ASCIIHexDecode"), + (later_raw.as_bytes(), "/Filter /ASCIIHexDecode"), + ], + &[0, 1], + ); + let cap = 1 << 20; + let doc = Pdf::new(pdf).expect("parses"); + let page = doc.pages().first().expect("one page"); + let mut content = page.content_reader_within(cap); + let expected_budget = 64 * (first_raw.len() + later_raw.len()); + assert!(expected_budget > cap); + assert_eq!(content.budget, expected_budget); + assert_eq!(content.materialized_len(), cap); + assert_eq!(content.pieces.len(), 2, "only the first whole stream and separator are kept"); + + let mut out = Vec::new(); + while content.read_into(&mut out, 65_536) > 0 {} + let mut expected = first; + expected.push(b' '); + assert_eq!(out, expected); + assert!(content.truncated(), "omitted streams must not look complete"); + } + + #[test] + fn malformed_flate_and_failed_whole_streams_report_incomplete_content() { + for (data, filter) in [ + (vec![0x78, 0x9c, 0x07], "/Filter /FlateDecode"), + (vec![0x78, 0x9c, 0x01, 10, 0, 245, 255, b'q'], "/Filter /FlateDecode"), + (b"not an image".to_vec(), "/Filter /DCTDecode"), + ] { + let (_, incomplete) = read_content(page_pdf(&data, filter), 4096, 7); + assert!(incomplete, "failed {filter} must not look like complete content"); + } + } + + #[test] + fn whole_stream_reports_a_smaller_document_decode_limit() { + // RunLength repeat packet: 128 decoded bytes, followed by EOD. + let data = page_pdf(&[129, b'A', 128], "/Filter /RunLengthDecode"); + let limits = crate::DecodeLimits::default().with_max_decoded_stream(32); + let doc = Pdf::new_with_limits(data, limits).expect("parses"); + let page = doc.pages().first().expect("one page"); + let mut content = page.content_reader(); + let mut out = Vec::new(); + while content.read_into(&mut out, 7) > 0 {} + assert_eq!(out, vec![b'A'; 32]); + assert!(content.truncated(), "the smaller document limit cut this stream"); + } + + #[test] + fn a_nonfinal_flate_block_is_not_complete_at_input_eof() { + let mut data = stored_zlib(b"q Q"); + // Stored zlib fixture starts with its two-byte header then the final-block flag. + data[2] &= !1; + // Drop the checksum so the next block is absent rather than malformed checksum bytes. + data.truncate(data.len() - 4); + let (out, incomplete) = read_content(page_pdf(&data, "/Filter /FlateDecode"), 4096, 2); + assert_eq!(out, b"q Q"); + assert!(incomplete); + } + + #[test] + fn content_past_the_cap_is_read_to_the_end() { + let content: Vec = b"q 1 0 0 1 0 0 cm Q\n".iter().copied().cycle().take(3 << 20).collect(); + let pdf = page_pdf(&stored_zlib(&content), "/Filter /FlateDecode"); + // The cap is far below the content, but stored data expands by nothing, so nothing is cut. + for piece in [7, 4096, 1 << 16] { + let (out, truncated) = read_content(pdf.clone(), 1 << 20, piece); + assert_eq!(out.len(), content.len(), "piece {piece}"); + assert!(out == content, "piece {piece}: the bytes are the content's"); + assert!(!truncated, "piece {piece}"); + } + } + + #[test] + fn a_stream_that_expands_past_the_budget_is_cut_there() { + let cap = 1 << 20; + let bomb = fixed_run_bomb(400_000); + let (out, truncated) = read_content(page_pdf(&bomb, "/Filter /FlateDecode"), cap, 1 << 16); + + assert!(truncated, "the expansion is past the budget"); + // The budget is 64 times the compressed stream, which is more than the cap here. + assert_eq!(out.len(), 64 * bomb.len()); + assert!(out.iter().all(|&b| b == b'a')); + } + + #[test] + fn a_small_expansion_under_the_cap_is_not_cut() { + let bomb = fixed_run_bomb(1000); + let (out, truncated) = read_content(page_pdf(&bomb, "/Filter /FlateDecode"), 1 << 20, 4096); + + assert!(!truncated); + assert_eq!(out.len(), 1 + 258 * 1000); + } +} diff --git a/vendor/hayro-syntax/src/pdf.rs b/vendor/hayro-syntax/src/pdf.rs new file mode 100644 index 00000000..46600e52 --- /dev/null +++ b/vendor/hayro-syntax/src/pdf.rs @@ -0,0 +1,234 @@ +//! The starting point for reading PDF files. + +use crate::PdfData; +use crate::object::Object; +use crate::page::Pages; +use crate::page::cached::CachedPages; +use crate::reader::Reader; +use crate::sync::Arc; +use crate::xref::{XRef, XRefError, fallback, root_xref}; + +pub use crate::crypto::DecryptionError; +use crate::metadata::Metadata; + +/// Immutable limits for decoding one PDF's streams. +/// +/// These limits apply to ordinary stream expansion (Flate, LZW and RunLength), including +/// object and xref streams. Known-size image pixels and incremental page content keep their +/// separate budgets. In particular, a bare Flate page read through `Page::content_reader` +/// (also used by interpretation and rasterization) is decoded incrementally under its content +/// budget, not this ordinary materialization limit. `Page::page_stream` materializes that +/// same stream and does use this limit, so the two paths can produce different lengths. +/// These limits do not impose a total document memory or execution-time limit. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DecodeLimits { + max_decoded_stream: usize, +} + +impl DecodeLimits { + /// The existing ordinary stream limit, 256 MiB. + pub const DEFAULT: Self = Self { max_decoded_stream: crate::filter::MAX_DECODED_STREAM }; + + /// Lower the maximum expanded bytes per ordinary stream. Requests above the existing + /// 256 MiB ceiling are clamped; zero permits no ordinary materialized expansion. + /// This does not lower the separate incremental page-content budget. + pub const fn with_max_decoded_stream(self, max: usize) -> Self { + Self { max_decoded_stream: if max < self.max_decoded_stream { max } else { self.max_decoded_stream } } + } + + /// The maximum expanded bytes per ordinary stream. + pub const fn max_decoded_stream(self) -> usize { + self.max_decoded_stream + } +} + +impl Default for DecodeLimits { + fn default() -> Self { + Self::DEFAULT + } +} + +/// A PDF file. +pub struct Pdf { + xref: Arc, + header_version: PdfVersion, + pages: CachedPages, + data: PdfData, +} + +/// An error that occurred while loading a PDF file. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LoadPdfError { + /// An error occurred while processing an encrypted document. + Decryption(DecryptionError), + /// The PDF was invalid or could not be parsed due to some other unknown reason. + Invalid, +} + +#[allow(clippy::len_without_is_empty)] +impl Pdf { + /// Try to read the given PDF file. + /// + /// Returns `Err` if it was unable to read it. + pub fn new(data: impl Into) -> Result { + Self::new_with_password(data, "") + } + + /// Try to read the given PDF file with a password. + /// + /// Returns `Err` if it was unable to read it or if the password is incorrect. + pub fn new_with_password(data: impl Into, password: &str) -> Result { + Self::new_with_password_and_limits(data, password, DecodeLimits::default()) + } + + /// Try to read a PDF using document-local stream expansion limits. + pub fn new_with_limits(data: impl Into, limits: DecodeLimits) -> Result { + Self::new_with_password_and_limits(data, "", limits) + } + + /// Try to read a PDF with a password and document-local stream expansion limits. + /// The same immutable limits apply during initial xref parsing, repair and later reads. + pub fn new_with_password_and_limits(data: impl Into, password: &str, limits: DecodeLimits) -> Result { + let data = data.into(); + let password = password.as_bytes(); + let version = find_version(data.as_ref()).unwrap_or(PdfVersion::Pdf10); + let xref = match root_xref(data.clone(), password, limits) { + Ok(x) => x, + Err(e) => match e { + XRefError::Unknown => fallback(data.clone(), password, limits).ok_or(LoadPdfError::Invalid)?, + XRefError::Encryption(e) => return Err(LoadPdfError::Decryption(e)), + }, + }; + let xref = Arc::new(xref); + + let pages = CachedPages::new(xref.clone()).ok_or(LoadPdfError::Invalid)?; + + Ok(Self { xref, header_version: version, pages, data }) + } + + /// Return this document's immutable stream expansion limits. + pub fn decode_limits(&self) -> DecodeLimits { + self.xref.decode_limits() + } + + /// Return the number of objects present in the PDF file. + pub fn len(&self) -> usize { + self.xref.len() + } + + /// Return an iterator over all objects defined in the PDF file. + pub fn objects(&self) -> impl IntoIterator> { + self.xref.objects() + } + + /// Return the version of the PDF file. + pub fn version(&self) -> PdfVersion { + self.xref + .trailer_data() + .version + .unwrap_or(self.header_version) + } + + /// Return the underlying data of the PDF file. + pub fn data(&self) -> &PdfData { + &self.data + } + + /// Return the pages of the PDF file. + pub fn pages(&self) -> &Pages<'_> { + self.pages.get() + } + + /// Return the xref of the PDF file. + pub fn xref(&self) -> &XRef { + &self.xref + } + + /// Return the metadata in the document information dictionary of the document. + pub fn metadata(&self) -> &Metadata { + self.xref.metadata() + } +} + +fn find_version(data: &[u8]) -> Option { + let data = &data[..data.len().min(2000)]; + let mut r = Reader::new(data); + + while r.forward_tag(b"%PDF-").is_none() { + r.read_byte()?; + } + + PdfVersion::from_bytes(r.tail()?) +} + +/// The version of a PDF document. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum PdfVersion { + /// PDF 1.0. + Pdf10, + /// PDF 1.1. + Pdf11, + /// PDF 1.2. + Pdf12, + /// PDF 1.3. + Pdf13, + /// PDF 1.4. + Pdf14, + /// PDF 1.5. + Pdf15, + /// PDF 1.6. + Pdf16, + /// PDF 1.7. + Pdf17, + /// PDF 2.0. + Pdf20, +} + +impl PdfVersion { + pub(crate) fn from_bytes(bytes: &[u8]) -> Option { + match bytes.get(..3)? { + b"1.0" => Some(Self::Pdf10), + b"1.1" => Some(Self::Pdf11), + b"1.2" => Some(Self::Pdf12), + b"1.3" => Some(Self::Pdf13), + b"1.4" => Some(Self::Pdf14), + b"1.5" => Some(Self::Pdf15), + b"1.6" => Some(Self::Pdf16), + b"1.7" => Some(Self::Pdf17), + b"2.0" => Some(Self::Pdf20), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::pdf::{Pdf, PdfVersion}; + + #[test] + fn issue_49() { + let _ = Pdf::new(Vec::new()); + } + + fn version_pdf(header: &str, catalog_version: &str) -> alloc::vec::Vec { + alloc::format!( + "%PDF-{header}\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R {catalog_version} >>\nendobj\n\ + 2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\ + 3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>\nendobj\n\ + trailer\n<< /Root 1 0 R >>\n%%EOF\n" + ) + .into_bytes() + } + + #[test] + fn pdf_version_header() { + let pdf = Pdf::new(version_pdf("1.7", "")).unwrap(); + assert_eq!(pdf.version(), PdfVersion::Pdf17); + } + + #[test] + fn pdf_version_catalog() { + let pdf = Pdf::new(version_pdf("1.3", "/Version /1.4")).unwrap(); + assert_eq!(pdf.version(), PdfVersion::Pdf14); + } +} diff --git a/vendor/hayro-syntax/src/reader.rs b/vendor/hayro-syntax/src/reader.rs new file mode 100644 index 00000000..1b9babfc --- /dev/null +++ b/vendor/hayro-syntax/src/reader.rs @@ -0,0 +1,248 @@ +//! Reading bytes and PDF objects from data. + +use crate::object::ObjectIdentifier; +use crate::sync::Arc; +use crate::trivia::{Comment, is_eol_character, is_white_space_character}; +use crate::xref::XRef; +use smallvec::{SmallVec, smallvec}; + +pub use crate::byte_reader::Reader; + +/// Extension trait for the `Reader` struct. +pub trait ReaderExt<'a> { + fn read>(&mut self, ctx: &ReaderContext<'a>) -> Option; + fn read_with_context>(&mut self, ctx: &ReaderContext<'a>) -> Option; + fn read_without_context>(&mut self) -> Option; + fn skip(&mut self, is_content_stream: bool) -> Option<&'a [u8]>; + fn skip_white_spaces(&mut self); + fn read_white_space(&mut self) -> Option<()>; + fn skip_eol_characters(&mut self); + fn skip_white_spaces_and_comments(&mut self); +} + +impl<'a> ReaderExt<'a> for Reader<'a> { + // Note: If `PLAIN` is true, it means that the data we are about to read _might_ contain + // an object reference instead of an actual object. if `PLAIN` is false, then an object + // reference cannot occur. The main reason we make this distinction is that when parsing + // a number, we cannot unambiguously distinguish whether it's a real number or the + // start of an object reference. In content streams, object references cannot appear, + // so in order to speed this up we set `PLAIN` to false, meaning that as soon as we + // encounter a number we know it's a number, and don't need to do a look-ahead to ensure + // that it's not an object reference. + #[inline] + fn read>(&mut self, ctx: &ReaderContext<'a>) -> Option { + let old_offset = self.offset; + + T::read(self, ctx).or_else(|| { + self.offset = old_offset; + + None + }) + } + + #[inline] + fn read_with_context>(&mut self, ctx: &ReaderContext<'a>) -> Option { + self.read::(ctx) + } + + #[inline] + fn read_without_context>(&mut self) -> Option { + self.read::(ReaderContext::dummy_ref()) + } + + #[inline] + fn skip(&mut self, is_content_stream: bool) -> Option<&'a [u8]> { + let old_offset = self.offset; + + T::skip(self, is_content_stream).or_else(|| { + self.offset = old_offset; + None + })?; + + self.data.get(old_offset..self.offset) + } + + #[inline] + fn skip_white_spaces(&mut self) { + while let Some(b) = self.peek_byte() { + if is_white_space_character(b) { + self.forward(); + } else { + return; + } + } + } + + #[inline] + fn read_white_space(&mut self) -> Option<()> { + if is_white_space_character(self.peek_byte()?) { + let w = self.read_byte()?; + + if w == b'\r' && self.peek_byte().is_some_and(|b| b == b'\n') { + self.read_byte()?; + } + + return Some(()); + } + + None + } + + #[inline] + fn skip_eol_characters(&mut self) { + while let Some(b) = self.peek_byte() { + if is_eol_character(b) { + self.forward(); + } else { + return; + } + } + } + + #[inline] + fn skip_white_spaces_and_comments(&mut self) { + while let Some(b) = self.peek_byte() { + if is_white_space_character(b) { + self.skip_white_spaces(); + } else if b == b'%' { + Comment::skip(self, true); + } else { + return; + } + } + } +} + +#[derive(Clone, Debug)] +struct ReaderContextData<'a> { + xref: &'a XRef, + in_content_stream: bool, + in_object_stream: bool, + obj_number: Option, + parent_chain: SmallVec<[ObjectIdentifier; 8]>, +} + +#[derive(Clone, Debug)] +enum ReaderContextInner<'a> { + Shared(Arc>), + Dummy { in_content_stream: bool, limits: crate::DecodeLimits }, +} + +/// Context for reading PDF objects. +#[derive(Clone, Debug)] +pub struct ReaderContext<'a>(ReaderContextInner<'a>); + +impl<'a> ReaderContext<'a> { + pub(crate) fn new(xref: &'a XRef, in_content_stream: bool) -> Self { + Self(ReaderContextInner::Shared(Arc::new(ReaderContextData { + xref, + in_content_stream, + obj_number: None, + in_object_stream: false, + parent_chain: smallvec![], + }))) + } + + pub fn dummy() -> Self { + Self::dummy_with_limits(crate::DecodeLimits::default()) + } + + pub(crate) fn dummy_with_limits(limits: crate::DecodeLimits) -> Self { + Self(ReaderContextInner::Dummy { in_content_stream: false, limits }) + } + + pub(crate) fn decode_limits(&self) -> crate::DecodeLimits { + match &self.0 { + ReaderContextInner::Shared(inner) => inner.xref.decode_limits(), + ReaderContextInner::Dummy { limits, .. } => *limits, + } + } + + pub(crate) fn dummy_ref() -> &'static Self { + &Self(ReaderContextInner::Dummy { in_content_stream: true, limits: crate::DecodeLimits::DEFAULT }) + } + + #[inline] + pub(crate) fn xref(&self) -> &'a XRef { + match &self.0 { + ReaderContextInner::Shared(inner) => inner.xref, + ReaderContextInner::Dummy { .. } => XRef::dummy(), + } + } + + #[inline] + pub(crate) fn in_content_stream(&self) -> bool { + match &self.0 { + ReaderContextInner::Shared(inner) => inner.in_content_stream, + ReaderContextInner::Dummy { in_content_stream, .. } => *in_content_stream, + } + } + + #[inline] + pub(crate) fn in_object_stream(&self) -> bool { + match &self.0 { + ReaderContextInner::Shared(inner) => inner.in_object_stream, + ReaderContextInner::Dummy { .. } => false, + } + } + + #[inline] + pub(crate) fn obj_number(&self) -> Option { + match &self.0 { + ReaderContextInner::Shared(inner) => inner.obj_number, + ReaderContextInner::Dummy { .. } => None, + } + } + + #[inline] + pub(crate) fn set_obj_number(&mut self, id: ObjectIdentifier) { + match &mut self.0 { + ReaderContextInner::Shared(inner) => Arc::make_mut(inner).obj_number = Some(id), + ReaderContextInner::Dummy { .. } => {} + } + } + + #[inline] + pub(crate) fn set_in_content_stream(&mut self, val: bool) { + match &mut self.0 { + ReaderContextInner::Shared(inner) => Arc::make_mut(inner).in_content_stream = val, + ReaderContextInner::Dummy { in_content_stream, .. } => *in_content_stream = val, + } + } + + #[inline] + pub(crate) fn set_in_object_stream(&mut self, val: bool) { + match &mut self.0 { + ReaderContextInner::Shared(inner) => Arc::make_mut(inner).in_object_stream = val, + ReaderContextInner::Dummy { .. } => {} + } + } + + #[inline] + pub(crate) fn parent_chain_contains(&self, id: &ObjectIdentifier) -> bool { + match &self.0 { + ReaderContextInner::Shared(inner) => inner.parent_chain.contains(id), + ReaderContextInner::Dummy { .. } => false, + } + } + + #[inline] + pub(crate) fn parent_chain_push(&mut self, id: ObjectIdentifier) { + match &mut self.0 { + ReaderContextInner::Shared(inner) => Arc::make_mut(inner).parent_chain.push(id), + ReaderContextInner::Dummy { .. } => {} + } + } +} + +pub trait Readable<'a>: Sized { + fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option; + fn from_bytes_impl(b: &'a [u8]) -> Option { + let mut r = Reader::new(b); + Self::read(&mut r, &ReaderContext::dummy()) + } +} + +pub trait Skippable { + fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()>; +} diff --git a/vendor/hayro-syntax/src/sync.rs b/vendor/hayro-syntax/src/sync.rs new file mode 100644 index 00000000..7991b0b5 --- /dev/null +++ b/vendor/hayro-syntax/src/sync.rs @@ -0,0 +1,104 @@ +#[cfg(feature = "std")] +pub(crate) use std::collections::HashMap; + +#[cfg(not(feature = "std"))] +pub(crate) use alloc::collections::BTreeMap as HashMap; + +#[cfg(feature = "std")] +pub(crate) use rustc_hash::FxHashMap; + +#[cfg(not(feature = "std"))] +pub(crate) use alloc::collections::BTreeMap as FxHashMap; + +// Keep in sync with the implementation in `page`. +#[cfg(feature = "std")] +pub(crate) use std::sync::Arc; + +#[cfg(not(feature = "std"))] +pub(crate) use alloc::rc::Rc as Arc; + +#[cfg(feature = "std")] +pub(crate) use std::sync::OnceLock; + +#[cfg(not(feature = "std"))] +pub(crate) use core::cell::OnceCell as OnceLock; + +#[cfg(feature = "std")] +pub(crate) use std::sync::Mutex; + +#[cfg(not(feature = "std"))] +pub(crate) use core::cell::RefCell as Mutex; + +#[cfg(feature = "std")] +pub(crate) use std::sync::RwLock; + +#[cfg(not(feature = "std"))] +pub(crate) use core::cell::RefCell as RwLock; + +#[cfg(feature = "std")] +pub(crate) type MutexGuard<'a, T> = std::sync::MutexGuard<'a, T>; + +#[cfg(not(feature = "std"))] +pub(crate) type MutexGuard<'a, T> = core::cell::RefMut<'a, T>; + +#[cfg(feature = "std")] +pub(crate) type RwLockReadGuard<'a, T> = std::sync::RwLockReadGuard<'a, T>; + +#[cfg(not(feature = "std"))] +pub(crate) type RwLockReadGuard<'a, T> = core::cell::Ref<'a, T>; + +#[cfg(feature = "std")] +pub(crate) type RwLockWriteGuard<'a, T> = std::sync::RwLockWriteGuard<'a, T>; + +#[cfg(not(feature = "std"))] +pub(crate) type RwLockWriteGuard<'a, T> = core::cell::RefMut<'a, T>; + +pub(crate) trait MutexExt { + fn get(&self) -> MutexGuard<'_, T>; +} + +#[cfg(feature = "std")] +impl MutexExt for Mutex { + fn get(&self) -> MutexGuard<'_, T> { + // PdfCraft patch: (#307) cached values remain owned after an unwinding caller; do not cascade a + // worker panic into unrelated readers. Initialization has its own failure state. + self.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) + } +} + +#[cfg(not(feature = "std"))] +impl MutexExt for Mutex { + fn get(&self) -> MutexGuard<'_, T> { + self.borrow_mut() + } +} + +pub(crate) trait RwLockExt { + fn get(&self) -> RwLockReadGuard<'_, T>; + fn put(&self) -> RwLockWriteGuard<'_, T>; +} + +#[cfg(feature = "std")] +impl RwLockExt for RwLock { + // PdfCraft patch: (#307) poisoned locks are recovered rather than unwrapped, and the + // non-blocking try_get/try_put (which repair asserted could not fail) become blocking. + fn get(&self) -> RwLockReadGuard<'_, T> { + self.read().unwrap_or_else(|poisoned| poisoned.into_inner()) + } + + fn put(&self) -> RwLockWriteGuard<'_, T> { + self.write() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + } +} + +#[cfg(not(feature = "std"))] +impl RwLockExt for RwLock { + fn get(&self) -> RwLockReadGuard<'_, T> { + self.borrow() + } + + fn put(&self) -> RwLockWriteGuard<'_, T> { + self.borrow_mut() + } +} diff --git a/vendor/hayro-syntax/src/transform.rs b/vendor/hayro-syntax/src/transform.rs new file mode 100644 index 00000000..f9b4a5e7 --- /dev/null +++ b/vendor/hayro-syntax/src/transform.rs @@ -0,0 +1,56 @@ +// Copyright 2018 the Kurbo Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! 2D affine transformations. + +use core::ops::Mul; + +/// A 2D affine transform. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Transform([f64; 6]); + +impl Transform { + /// The identity transform. + pub const IDENTITY: Self = Self::scale(1.0); + + #[inline(always)] + pub(crate) const fn new(c: [f64; 6]) -> Self { + Self(c) + } + + #[inline(always)] + pub(crate) const fn scale(s: f64) -> Self { + Self([s, 0.0, 0.0, s, 0.0, 0.0]) + } + + pub(crate) const ROTATE_CW_90: Self = Self::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]); + + pub(crate) const ROTATE_CCW_90: Self = Self::new([0.0, -1.0, 1.0, 0.0, 0.0, 0.0]); + + #[inline(always)] + pub(crate) const fn translate(p: (f64, f64)) -> Self { + Self([1.0, 0.0, 0.0, 1.0, p.0, p.1]) + } + + /// Get the coefficients of the transform. + #[inline(always)] + pub const fn as_coeffs(self) -> [f64; 6] { + self.0 + } +} + +impl Mul for Transform { + type Output = Self; + + #[inline] + fn mul(self, other: Self) -> Self { + Self([ + self.0[0] * other.0[0] + self.0[2] * other.0[1], + self.0[1] * other.0[0] + self.0[3] * other.0[1], + self.0[0] * other.0[2] + self.0[2] * other.0[3], + self.0[1] * other.0[2] + self.0[3] * other.0[3], + self.0[0] * other.0[4] + self.0[2] * other.0[5] + self.0[4], + self.0[1] * other.0[4] + self.0[3] * other.0[5] + self.0[5], + ]) + } +} diff --git a/vendor/hayro-syntax/src/trivia.rs b/vendor/hayro-syntax/src/trivia.rs new file mode 100644 index 00000000..0097dc99 --- /dev/null +++ b/vendor/hayro-syntax/src/trivia.rs @@ -0,0 +1,83 @@ +//! Comments and white spaces. + +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable}; + +const fn build_regular_character_table() -> [bool; 256] { + let mut table = [true; 256]; + + // Whitespace characters. + table[0x00] = false; + table[0x09] = false; + table[0x0a] = false; + table[0x0c] = false; + table[0x0d] = false; + table[0x20] = false; + + // Delimiter characters. + table[b'(' as usize] = false; + table[b')' as usize] = false; + table[b'<' as usize] = false; + table[b'>' as usize] = false; + table[b'[' as usize] = false; + table[b']' as usize] = false; + table[b'{' as usize] = false; + table[b'}' as usize] = false; + table[b'/' as usize] = false; + table[b'%' as usize] = false; + + table +} + +const REGULAR_CHARACTER_TABLE: [bool; 256] = build_regular_character_table(); + +const fn build_white_space_table() -> [bool; 256] { + let mut table = [false; 256]; + + table[0x00] = true; + table[0x09] = true; + table[0x0a] = true; + table[0x0c] = true; + table[0x0d] = true; + table[0x20] = true; + + table +} + +const WHITE_SPACE_CHARACTER_TABLE: [bool; 256] = build_white_space_table(); + +#[inline(always)] +pub(crate) fn is_white_space_character(char: u8) -> bool { + WHITE_SPACE_CHARACTER_TABLE[char as usize] +} + +#[inline(always)] +pub(crate) fn is_regular_character(char: u8) -> bool { + REGULAR_CHARACTER_TABLE[char as usize] +} + +#[inline(always)] +pub(crate) fn is_eol_character(char: u8) -> bool { + matches!(char, 0x0a | 0x0d) +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub(crate) struct Comment<'a>(pub(crate) &'a [u8]); + +impl Skippable for Comment<'_> { + fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> { + r.forward_tag(b"%")?; + r.forward_while(|b| !is_eol_character(b)); + + Some(()) + } +} + +impl<'a> Readable<'a> for Comment<'a> { + fn read(r: &mut Reader<'a>, _: &ReaderContext<'_>) -> Option { + let bytes = r.skip::>(false)?; + let bytes = bytes.get(1..bytes.len()).unwrap(); + + Some(Comment(bytes)) + } +} diff --git a/vendor/hayro-syntax/src/util.rs b/vendor/hayro-syntax/src/util.rs new file mode 100644 index 00000000..4b7b0d0c --- /dev/null +++ b/vendor/hayro-syntax/src/util.rs @@ -0,0 +1,217 @@ +use crate::sync::OnceLock; +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::ops::Sub; +use smallvec::{Array, SmallVec}; + +pub(crate) trait OptionLog { + fn error_none(self, f: &str) -> Self; +} + +impl OptionLog for Option { + fn error_none(self, _f: &str) -> Self { + self.or_else(|| { + error!("{_f}"); + + None + }) + } +} + +const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 12) as f32; + +/// A number of useful methods for f32 numbers. +pub(crate) trait FloatExt: Sized + Sub + Copy { + /// Whether the number is approximately 0. + fn is_nearly_zero(&self) -> bool { + self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO) + } + + /// Whether the number is approximately 0, with a given tolerance. + fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool; +} + +impl FloatExt for f32 { + fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool { + debug_assert!(tolerance >= 0.0, "tolerance must be non-negative"); + + self.abs() <= tolerance + } +} + +pub(crate) trait ByteBuf: Default { + fn with_capacity(capacity: usize) -> Self; + fn push(&mut self, byte: u8); +} + +impl ByteBuf for Vec { + fn with_capacity(capacity: usize) -> Self { + Self::with_capacity(capacity) + } + + fn push(&mut self, byte: u8) { + Self::push(self, byte); + } +} + +impl ByteBuf for SmallVec +where + A: Array, +{ + fn with_capacity(capacity: usize) -> Self { + Self::with_capacity(capacity) + } + + fn push(&mut self, byte: u8) { + Self::push(self, byte); + } +} + +#[cfg(feature = "unsafe")] +pub(crate) fn find_needle(haystack: &[u8], needle: &[u8]) -> Option { + memchr::memmem::find(haystack, needle) +} + +#[cfg(feature = "unsafe")] +pub(crate) fn findr_needle(haystack: &[u8], needle: &[u8]) -> Option { + memchr::memmem::rfind(haystack, needle) +} + +#[cfg(not(feature = "unsafe"))] +pub(crate) fn find_needle(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() { + return Some(0); + } + + let first = needle[0]; + let mut i = 0; + + while i + needle.len() <= haystack.len() { + while haystack.get(i).copied()? != first { + i += 1; + if i + needle.len() > haystack.len() { + return None; + } + } + + if haystack.get(i..)?.starts_with(needle) { + return Some(i); + } + + i += 1; + } + + None +} + +#[cfg(not(feature = "unsafe"))] +pub(crate) fn findr_needle(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() { + return Some(haystack.len()); + } + + if needle.len() > haystack.len() { + return None; + } + + let first = needle[0]; + let mut i = haystack.len() - needle.len(); + + loop { + while haystack.get(i).copied()? != first { + i = i.checked_sub(1)?; + } + + if haystack.get(i..)?.starts_with(needle) { + return Some(i); + } + + i = i.checked_sub(1)?; + } +} + +/// Allows to store elements at an index with an `&self` reference. +/// +/// This is similar to a thread-safe arena data structure, but with less moving +/// parts and no unsafe code. It's based on the segment list presented in +/// +/// +/// Indices should be used in order. Usage of higher indices implies more memory +/// usage (even if lower indices are not in use). +/// +/// The capacity is limited at 2^C - 1. Out-of-range indices return `None` (PdfCraft patch: +/// upstream panics; #307 reaches this with object-stream slot numbers from the file). +pub(crate) struct SegmentList([OnceLock]>>; C]); + +impl SegmentList { + pub(crate) fn new() -> Self { + Self(core::array::from_fn(|_| OnceLock::new())) + } + + pub(crate) fn get(&self, i: usize) -> Option<&T> { + let (s, k) = self.locate(i)?; + let segment = self.0.get(s as usize)?.get()?; + segment.get(k)?.get() + } + + #[track_caller] + pub(crate) fn get_or_init(&self, i: usize, f: impl FnOnce() -> T) -> Option<&T> { + let (s, k) = self.locate(i)?; + let segment = self.0.get(s as usize)?.get_or_init(|| { + (0..2_usize.pow(s)) + .map(|_| OnceLock::new()) + .collect::>() + .into_boxed_slice() + }); + Some(segment.get(k)?.get_or_init(f)) + } + + fn locate(&self, i: usize) -> Option<(u32, usize)> { + let power = i.checked_add(2)?.checked_next_power_of_two()? / 2; + let s = power.trailing_zeros(); + let k = i - ((1 << s) - 1); + Some((s, k)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_segment_list() { + let e = SegmentList::::new(); + + // Ensure that it works. + for i in 0..500 { + e.get_or_init(i, || format!("{i}")); + } + for i in 0..500 { + assert_eq!(e.get(i), Some(&format!("{i}"))); + } + + // Ensure that all slots in the first 8 segments are actually in use, + // i.e. we didn't overallocate. + for s in 0..8 { + assert!(e.0[s].get().unwrap().iter().all(|s| s.get().is_some())); + } + } + + #[test] + fn segment_list_rejects_out_of_range_indices() { + let list = SegmentList::::new(); + assert_eq!(list.get_or_init(2, || 7), Some(&7)); + for index in [3, 20, usize::MAX - 1, usize::MAX] { + assert_eq!(list.get(index), None); + assert_eq!(list.get_or_init(index, || 9), None); + } + } + + #[test] + fn test_findr_needle() { + assert_eq!(findr_needle(b"abc foo def foo xyz", b"foo"), Some(12)); + assert_eq!(findr_needle(b"abc", b""), Some(3)); + assert_eq!(findr_needle(b"abc", b"abcd"), None); + assert_eq!(findr_needle(b"abc", b"z"), None); + } +} diff --git a/vendor/hayro-syntax/src/xref.rs b/vendor/hayro-syntax/src/xref.rs new file mode 100644 index 00000000..d6f6b33d --- /dev/null +++ b/vendor/hayro-syntax/src/xref.rs @@ -0,0 +1,1370 @@ +//! Reading and querying the xref table of a PDF file. + +use crate::crypto::{DecryptionError, DecryptionTarget, Decryptor, get}; +use crate::data::Data; +use crate::metadata::Metadata; +use crate::object::Name; +use crate::object::ObjectIdentifier; +use crate::object::Stream; +use crate::object::dict::keys::{ + AUTHOR, CREATION_DATE, CREATOR, ENCRYPT, FIRST, ID, INDEX, INFO, KEYWORDS, MOD_DATE, N, + OCPROPERTIES, PAGES, PREV, PRODUCER, ROOT, SIZE, SUBJECT, TITLE, TYPE, VERSION, W, XREF_STM, +}; +use crate::object::dict::{DictCache, probe_dict}; +use crate::object::indirect::IndirectObject; +use crate::object::{Array, MaybeRef}; +use crate::object::{DateTime, Dict}; +use crate::object::{Object, ObjectLike}; +use crate::pdf::PdfVersion; +use crate::reader::Reader; +use crate::reader::{Readable, ReaderContext, ReaderExt}; +use crate::sync::{Arc, FxHashMap, Mutex, RwLock, RwLockExt}; +use crate::trivia::is_white_space_character; +use crate::util::findr_needle; +use crate::{DecodeLimits, PdfData, object}; +use alloc::collections::BTreeSet; +use alloc::vec; +use alloc::vec::Vec; +use core::cmp::max; +use core::iter; +use core::ops::Deref; + +pub(crate) const XREF_ENTRY_LEN: usize = 20; + +#[derive(Debug, Copy, Clone)] +pub(crate) enum XRefError { + Unknown, + Encryption(DecryptionError), +} + +/// Parse the "root" xref from the PDF. +pub(crate) fn root_xref(data: PdfData, password: &[u8], limits: DecodeLimits) -> Result { + let mut xref_map = FxHashMap::default(); + let xref_pos = find_last_xref_pos(data.as_ref()).ok_or(XRefError::Unknown)?; + let trailer = populate_xref_impl(data.as_ref(), xref_pos, &mut xref_map, limits).ok_or(XRefError::Unknown)?; + + XRef::new(data.clone(), xref_map, XRefInput::TrailerDictData(trailer), false, password, limits) +} + +/// Try to manually parse the PDF to build an xref table and trailer dictionary. +pub(crate) fn fallback(data: PdfData, password: &[u8], limits: DecodeLimits) -> Option { + warn!("xref table was invalid, trying to manually build xref table"); + let (xref_map, xref_input) = fallback_xref_map(&data, password, limits); + + if let Some(xref_input) = xref_input { + warn!("rebuild xref table with {} entries", xref_map.len()); + + XRef::new(data.clone(), xref_map, xref_input, true, password, limits).ok() + } else { + warn!("couldn't find trailer dictionary, failed to rebuild xref table"); + + None + } +} + +fn fallback_xref_map<'a>(data: &'a PdfData, password: &[u8], limits: DecodeLimits) -> (XrefMap, Option>) { + fallback_xref_map_inner(data, ReaderContext::dummy_with_limits(limits), true, password) +} + +fn fallback_xref_map_inner<'a>( + data: &'a PdfData, + mut dummy_ctx: ReaderContext<'a>, + recurse: bool, + password: &[u8], +) -> (XrefMap, Option>) { + let mut xref_map = FxHashMap::default(); + let mut trailer_dicts = vec![]; + let mut root_ref = None; + + let mut r = Reader::new(data.as_ref()); + + let mut last_obj_num = None; + + loop { + let cur_pos = r.offset(); + + let mut old_r = r.clone(); + + // First try to check if we have an object identifier. + if r.peek_byte().is_some_and(|b: u8| b.is_ascii_digit()) { + if let Some(obj_id) = r.read::(&dummy_ctx) { + let mut cloned = r.clone(); + // Check that the object following it is actually valid before inserting it. + cloned.skip_white_spaces_and_comments(); + if cloned.skip::>(false).is_some() { + xref_map.insert(obj_id, EntryType::Normal(cur_pos)); + last_obj_num = Some(obj_id); + dummy_ctx.set_obj_number(obj_id); + } + } else { + // There must be a white space before the next object number. + r.forward_while(|b| !is_white_space_character(b)); + } + } else { + // Then, try to check whether we have a dictionary, in particular a trailer + // dictionary. + let mut probe_reader = r.clone(); + if r.peek_bytes(2).is_some_and(|b| b == b"<<") + && let Some(probe) = + { probe_dict(&mut probe_reader, &dummy_ctx, Some(b"<<"), b">>") } + { + r = probe_reader; + if probe.has_root || probe.has_type { + let mut dict_reader = Reader::new(probe.data); + if let Some(dict) = dict_reader.read_with_context::>(&dummy_ctx) { + if probe.has_root && dict.contains_key(ROOT) { + trailer_dicts.push(dict.clone()); + } + + if dict + .get::>(TYPE) + .is_some_and(|n| n.as_str() == "Catalog") + { + root_ref = last_obj_num; + } + + if let Some(stream) = old_r.read::>(&dummy_ctx) + && dict.get::>(TYPE).as_deref() == Some(b"ObjStm") + && let Some(data) = stream.decoded().ok() + && let Some(last_obj_num) = last_obj_num + && let Some(obj_stream) = ObjectStream::new(stream, &data, &dummy_ctx) + { + for (idx, (obj_num, _)) in obj_stream.offsets.iter().enumerate() { + let id = ObjectIdentifier::new(*obj_num as i32, 0); + // If we already found an entry for that object number that was not + // inside an object stream. Somewhat arbitrary and maybe + // we can do better, but that seems to work for the current + // set of tests. + if xref_map + .get(&id) + .is_none_or(|e| !matches!(e, &EntryType::Normal(_))) + { + xref_map.insert( + id, + EntryType::ObjStream( + last_obj_num.obj_number as u32, + idx as u32, + ), + ); + } + } + } + } + } + } else { + // We can skip everything until the next white space character, + // as there cannot possibly be any new dictionary/object identifier + // until then. + let old_pos = r.offset; + r.forward_while(|b| !is_white_space_character(b)); + if r.offset == old_pos { + r.read_byte(); + } + } + } + + if r.at_end() { + break; + } + } + + // Try to choose the right trailer dict by doing basic validation. + let mut trailer_dict = None; + + for dict in trailer_dicts { + if let Some(root_id) = dict.get_raw::>(ROOT) { + let check = |dict: &Dict<'_>| -> bool { dict.contains_key(PAGES) }; + + match root_id { + MaybeRef::Ref(r) => match xref_map.get(&r.into()) { + Some(EntryType::Normal(offset)) => { + let mut reader = Reader::new(&data.as_ref()[*offset..]); + + if let Some(obj) = + reader.read_with_context::>>(&dummy_ctx) + && { + let obj = obj.get(); + check(&obj) + } + { + trailer_dict = Some(dict); + } + } + Some(EntryType::ObjStream(obj_num, idx)) => { + if let Some(EntryType::Normal(offset)) = + xref_map.get(&ObjectIdentifier::new(*obj_num as i32, 0)) + { + let mut reader = Reader::new(&data.as_ref()[*offset..]); + + if let Some(stream) = + reader.read_with_context::>>(&dummy_ctx) + && { + let stream = stream.get(); + if let Some(data) = stream.decoded().ok() + && let Some(object_stream) = + ObjectStream::new(stream, &data, &dummy_ctx) + && let Some(obj) = object_stream.get::>(*idx) + { + check(&obj) + } else { + false + } + } + { + trailer_dict = Some(dict); + } + } + } + _ => {} + }, + MaybeRef::NotRef(d) => { + if check(&d) { + trailer_dict = Some(dict); + } + } + } + } + } + + let has_encryption = trailer_dict + .as_ref() + .is_some_and(|t| t.contains_key(ENCRYPT)); + + if has_encryption && recurse { + // The problem is that in this case, we have used a dummy reader context which does not have + // a decryptor. Therefore, we were unable to decrypt any of the object streams and missed + // all objects that are inside of such a stream. Therefore, we need to redo the process + // using a `ReaderContext` that does have the ability to decrypt. + if let Ok(xref) = XRef::new( + data.clone(), + xref_map.clone(), + XRefInput::TrailerDictData(trailer_dict.as_ref().map(|d| d.data()).unwrap()), + true, + password, + dummy_ctx.decode_limits(), + ) { + let ctx = ReaderContext::new(&xref, false); + let (patched_map, _) = fallback_xref_map_inner(data, ctx, false, password); + xref_map = patched_map; + } + } + + if let Some(trailer_dict_data) = trailer_dict.map(|d| d.data()) { + ( + xref_map, + Some(XRefInput::TrailerDictData(trailer_dict_data)), + ) + } else if let Some(root_ref) = root_ref { + (xref_map, Some(XRefInput::RootRef(root_ref))) + } else { + (xref_map, None) + } +} + +const DUMMY_XREF: XRef = XRef(Inner::Dummy); + +/// An xref table. +#[derive(Debug, Clone)] +pub struct XRef(Inner); + +impl XRef { + // PdfCraft patch: keep the bounded dictionary-index cache local to this PDF. + pub(crate) fn with_dict_cache(&self, f: impl FnOnce(&mut DictCache) -> T) -> Option { + let Inner::Some(repr) = &self.0 else { + return None; + }; + #[cfg(feature = "std")] + let mut cache = repr + .dict_cache + .lock() + .unwrap_or_else(|poison| poison.into_inner()); + #[cfg(not(feature = "std"))] + let mut cache = repr.dict_cache.try_borrow_mut().ok()?; + Some(f(&mut cache)) + } + + fn new(data: PdfData, xref_map: XrefMap, input: XRefInput<'_>, repaired: bool, password: &[u8], limits: DecodeLimits) -> Result { + // This is a bit hacky, but the problem is we can't read the resolved trailer dictionary + // before we actually created the xref struct. So we first create it using dummy data + // and then populate the data. + let trailer_data = TrailerData::dummy(); + + let mut xref = Self(Inner::Some(Arc::new(SomeRepr { + data: Arc::new(Data::new(data)), + map: Arc::new(RwLock::new(MapRepr { xref_map, repaired })), + decryptor: Arc::new(Decryptor::None), + has_ocgs: false, + metadata: Arc::new(Metadata::default()), + trailer_data, + password: password.to_vec(), + limits, + dict_cache: Arc::new(Mutex::new(DictCache::default())), + }))); + + // We read the trailer twice, once to determine the encryption used and then a second + // time to resolve the catalog dictionary, etc. This allows us to support catalog dictionaries + // that are stored in an encrypted object stream. + + let decryptor = { + match input { + XRefInput::TrailerDictData(trailer_dict_data) => { + let mut r = Reader::new(trailer_dict_data); + + let trailer_dict = r + .read_with_context::>(&ReaderContext::new(&xref, false)) + .ok_or(XRefError::Unknown)?; + + get_decryptor(&trailer_dict, password)? + } + XRefInput::RootRef(_) => Decryptor::None, + } + }; + + match &mut xref.0 { + Inner::Dummy => unreachable!(), + Inner::Some(r) => { + let mutable = Arc::make_mut(r); + mutable.decryptor = Arc::new(decryptor.clone()); + } + } + + let (trailer_data, has_ocgs, metadata) = match input { + XRefInput::TrailerDictData(trailer_dict_data) => { + let mut r = Reader::new(trailer_dict_data); + + let trailer_dict = r + .read_with_context::>(&ReaderContext::new(&xref, false)) + .ok_or(XRefError::Unknown)?; + + let root_ref = trailer_dict.get_ref(ROOT).ok_or(XRefError::Unknown)?; + let root = trailer_dict + .get::>(ROOT) + .ok_or(XRefError::Unknown)?; + let metadata = trailer_dict + .get::>(INFO) + .map(|d| parse_metadata(&d)) + .unwrap_or_default(); + let pages_ref = root.get_ref(PAGES).ok_or(XRefError::Unknown)?; + let has_ocgs = root.get::>(OCPROPERTIES).is_some(); + let version = root + .get::>(VERSION) + .and_then(|v| PdfVersion::from_bytes(v.deref())); + + let td = TrailerData { + pages_ref: pages_ref.into(), + root_ref: root_ref.into(), + version, + }; + + (td, has_ocgs, metadata) + } + XRefInput::RootRef(root_ref) => { + let root = xref.get::>(root_ref).ok_or(XRefError::Unknown)?; + let pages_ref = root.get_ref(PAGES).ok_or(XRefError::Unknown)?; + + let td = TrailerData { + pages_ref: pages_ref.into(), + root_ref, + version: None, + }; + + (td, false, Metadata::default()) + } + }; + + match &mut xref.0 { + Inner::Dummy => unreachable!(), + Inner::Some(r) => { + let mutable = Arc::make_mut(r); + mutable.trailer_data = trailer_data; + mutable.decryptor = Arc::new(decryptor); + mutable.has_ocgs = has_ocgs; + mutable.metadata = Arc::new(metadata); + } + } + + Ok(xref) + } + + pub(crate) fn dummy() -> &'static Self { + &DUMMY_XREF + } + + pub(crate) fn decode_limits(&self) -> DecodeLimits { + match &self.0 { + Inner::Dummy => DecodeLimits::default(), + Inner::Some(repr) => repr.limits, + } + } + + pub(crate) fn len(&self) -> usize { + match &self.0 { + Inner::Dummy => 0, + Inner::Some(r) => r.map.get().xref_map.len(), + } + } + + pub(crate) fn trailer_data(&self) -> &TrailerData { + match &self.0 { + Inner::Dummy => unreachable!(), + Inner::Some(r) => &r.trailer_data, + } + } + + pub(crate) fn metadata(&self) -> &Metadata { + match &self.0 { + Inner::Dummy => unreachable!(), + Inner::Some(r) => &r.metadata, + } + } + + /// Return the object ID of the root dictionary. + pub fn root_id(&self) -> ObjectIdentifier { + self.trailer_data().root_ref + } + + /// Whether the PDF has optional content groups. + pub fn has_optional_content_groups(&self) -> bool { + match &self.0 { + Inner::Dummy => false, + Inner::Some(r) => r.has_ocgs, + } + } + + pub(crate) fn objects(&self) -> impl IntoIterator> + '_ { + match &self.0 { + Inner::Dummy => unimplemented!(), + Inner::Some(r) => { + let locked = r.map.get(); + let mut elements = locked + .xref_map + .iter() + .map(|(id, e)| { + let offset = match e { + EntryType::Normal(o) => (*o, 0), + EntryType::ObjStream(id, index) => { + if let Some(EntryType::Normal(offset)) = + locked.xref_map.get(&ObjectIdentifier::new(*id as i32, 0)) + { + (*offset, *index) + } else { + (usize::MAX, 0) + } + } + }; + + (*id, offset) + }) + .collect::>(); + + // Try to yield in the order the objects appeared in the + // PDF. + elements.sort_by(|e1, e2| e1.1.cmp(&e2.1)); + + let mut iter = elements.into_iter(); + + iter::from_fn(move || { + for next in iter.by_ref() { + if let Some(obj) = self.get_with(next.0, &ReaderContext::new(self, false)) { + return Some(obj); + } else { + // Skip invalid objects. + continue; + } + } + + None + }) + } + } + } + + pub(crate) fn repair(&self) { + let Inner::Some(r) = &self.0 else { + return; + }; + + // All callers release read guards before following references/repairing. + // Fallback reconstruction uses its own dummy/temporary xref, never this + // locked map. Serialize the one repair and let concurrent readers wait. + let mut locked = r.map.put(); + if locked.repaired { + return; + } + // Set first so an unwinding fallback cannot trigger a reparse storm. + // Assignment below publishes a complete replacement atomically. + locked.repaired = true; + let (xref_map, _) = fallback_xref_map(r.data.get(), &r.password, r.limits); + locked.xref_map = xref_map; + } + + #[inline] + pub(crate) fn needs_decryption(&self, ctx: &ReaderContext<'_>) -> bool { + match &self.0 { + Inner::Dummy => false, + Inner::Some(r) => { + if matches!(r.decryptor.as_ref(), Decryptor::None) { + false + } else { + !ctx.in_content_stream() && !ctx.in_object_stream() + } + } + } + } + + #[inline] + pub(crate) fn decrypt( + &self, + id: ObjectIdentifier, + data: &[u8], + target: DecryptionTarget, + ) -> Option> { + match &self.0 { + Inner::Dummy => Some(data.to_vec()), + Inner::Some(r) => r.decryptor.decrypt(id, data, target), + } + } + + /// Return the object with the given identifier. + #[allow(private_bounds)] + pub fn get<'a, T>(&'a self, id: ObjectIdentifier) -> Option + where + T: ObjectLike<'a>, + { + let ctx = ReaderContext::new(self, false); + self.get_with(id, &ctx) + } + + /// Return the object with the given identifier. + #[allow(private_bounds)] + pub(crate) fn get_with<'a, T>( + &'a self, + id: ObjectIdentifier, + ctx: &ReaderContext<'a>, + ) -> Option + where + T: ObjectLike<'a>, + { + let Inner::Some(repr) = &self.0 else { + return None; + }; + + let locked = repr.map.get(); + + let mut r = Reader::new(repr.data.get().as_ref()); + + let entry = *locked.xref_map.get(&id).or({ + // An indirect reference to an undefined object shall not be considered an error by a PDF processor; it + // shall be treated as a reference to the null object. + None + })?; + let repaired = locked.repaired; + drop(locked); + + let mut ctx = ctx.clone(); + ctx.set_obj_number(id); + ctx.set_in_content_stream(false); + + match entry { + EntryType::Normal(offset) => { + ctx.set_in_object_stream(false); + r.jump(offset); + + if let Some(object) = r.read_with_context::>(&ctx) { + if object.id() == &id { + return Some(object.get()); + } + } else { + // There is a valid object at the offset, it's just not of the type the caller + // expected, which is fine. + if r.skip::>>(false).is_some() { + return None; + } + }; + + // The xref table is broken, try to repair if not already repaired. + if repaired { + error!( + "attempt was made at repairing xref, but object {id:?} still couldn't be read" + ); + + None + } else { + warn!("broken xref, attempting to repair"); + + self.repair(); + + // Now try reading again. + self.get_with::(id, &ctx) + } + } + EntryType::ObjStream(obj_stram_gen_num, index) => { + // Generation number is implicitly 0. + let obj_stream_id = ObjectIdentifier::new(obj_stram_gen_num as i32, 0); + + if obj_stream_id == id { + warn!("cycle detected in object stream"); + + return None; + } + + // Stream objects cannot themselves be stored in object streams + // (PDF 7.5.7). Reject malformed chains before recursively loading + // them, including A-in-B/B-in-A and arbitrarily deep chains. + if !matches!( + repr.map.get().xref_map.get(&obj_stream_id), + Some(EntryType::Normal(_)) + ) { + return None; + } + + let stream = self.get_with::>(obj_stream_id, &ctx)?; + let data = repr.data.get_with(obj_stream_id, &ctx)?; + let object_stream = ObjectStream::new(stream, data, &ctx)?; + object_stream.get(index) + } + } + } +} + +/// An input that is passed to the xref constructor so that we can fully resolve +/// the PDF. +#[derive(Debug, Copy, Clone)] +pub(crate) enum XRefInput<'a> { + /// This option is going to be uesd in 99.999% of the case. It contains the + /// raw data of the trailer dictionary which is then going to be processed. + TrailerDictData(&'a [u8]), + /// In case the trailer dictionary could not be read (for example because + /// it is cut-off), we just pass the object ID of the root dictionary + /// in case we have found one, and try our best to build the PDF just + /// with the information we have there. + /// + /// Note that this won't work if the document is encrypted, as we + /// can't access the crypto dictionary. + RootRef(ObjectIdentifier), +} + +pub(crate) fn find_last_xref_pos(data: &[u8]) -> Option { + let needle = b"startxref"; + let pos = findr_needle(data, needle)?; + let mut finder = Reader::new(data); + finder.jump(pos); + finder.forward_tag(needle)?; + finder.skip_white_spaces_and_comments(); + finder.read_without_context::()?.try_into().ok() +} + +/// A type of xref entry. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum EntryType { + /// An indirect object that is at a specific offset in the original data. + Normal(usize), + /// An indirect object that is part of an object stream. First number indicates the object + /// number of the _object stream_ (the generation number is always 0), the second number indicates + /// the index in the object stream. + ObjStream(u32, u32), +} + +type XrefMap = FxHashMap; + +/// Representation of a proper xref table. +#[derive(Debug)] +struct MapRepr { + xref_map: XrefMap, + repaired: bool, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct TrailerData { + pub(crate) pages_ref: ObjectIdentifier, + pub(crate) root_ref: ObjectIdentifier, + pub(crate) version: Option, +} + +impl TrailerData { + pub(crate) fn dummy() -> Self { + Self { + pages_ref: ObjectIdentifier::new(0, 0), + root_ref: ObjectIdentifier::new(0, 0), + version: None, + } + } +} + +#[derive(Debug, Clone)] +struct SomeRepr { + dict_cache: Arc>, + data: Arc, + map: Arc>, + metadata: Arc, + decryptor: Arc, + has_ocgs: bool, + password: Vec, + limits: DecodeLimits, + trailer_data: TrailerData, +} + +#[derive(Debug, Clone)] +enum Inner { + /// A dummy xref table that doesn't have any entries. + Dummy, + /// A proper xref table. + Some(Arc), +} + +#[derive(Debug)] +struct XRefEntry { + offset: usize, + gen_number: i32, + used: bool, +} + +impl XRefEntry { + pub(crate) fn read(data: &[u8]) -> Option { + #[inline(always)] + fn parse_u32(data: &[u8]) -> Option { + let mut accum = 0_u32; + + for byte in data { + accum = accum.checked_mul(10)?; + + match *byte { + b'0'..=b'9' => accum = accum.checked_add((*byte - b'0') as u32)?, + _ => return None, + } + } + + Some(accum) + } + + let offset = parse_u32(&data[0..10])? as usize; + let gen_number = i32::try_from(parse_u32(&data[11..16])?).ok()?; + + let used = data[17] == b'n'; + + Some(Self { + offset, + gen_number, + used, + }) + } +} + +fn populate_xref_impl<'a>(data: &'a [u8], pos: usize, xref_map: &mut XrefMap, limits: DecodeLimits) -> Option<&'a [u8]> { + let mut visited = BTreeSet::new(); + populate_xref_impl_inner(data, pos, xref_map, &mut visited, limits) +} + +/// Maximum number of allowed xref `Prev` pointers before we abort. +const MAX_XREF_CHAIN_DEPTH: usize = 256; + +fn populate_xref_impl_inner<'a>( + data: &'a [u8], + pos: usize, + xref_map: &mut XrefMap, + visited: &mut BTreeSet, + limits: DecodeLimits, +) -> Option<&'a [u8]> { + if !visited.insert(pos) { + warn!("circular xref PREV chain detected at offset {}", pos); + + return None; + } + + if visited.len() > MAX_XREF_CHAIN_DEPTH { + warn!( + "xref PREV chain exceeds maximum depth of {}", + MAX_XREF_CHAIN_DEPTH + ); + + return None; + } + + let mut reader = Reader::new(data); + reader.jump(pos); + // In case the position points to before the object number of a xref stream. + reader.skip_white_spaces_and_comments(); + + let mut r2 = reader.clone(); + if reader.clone().read_without_context::().is_some() { + populate_from_xref_stream(data, &mut r2, xref_map, visited, limits) + } else { + populate_from_xref_table(data, &mut r2, xref_map, visited, limits) + } +} + +pub(super) struct SubsectionHeader { + pub(super) start: u32, + pub(super) num_entries: u32, +} + +impl Readable<'_> for SubsectionHeader { + fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option { + r.skip_white_spaces(); + let start = r.read_without_context::()?; + r.skip_white_spaces(); + let num_entries = r.read_without_context::()?; + r.skip_white_spaces(); + + Some(Self { start, num_entries }) + } +} + +/// Populate the xref table, and return the trailer dict. +fn populate_from_xref_table<'a>( + data: &'a [u8], + reader: &mut Reader<'a>, + insert_map: &mut XrefMap, + visited: &mut BTreeSet, + limits: DecodeLimits, +) -> Option<&'a [u8]> { + let trailer = { + let mut reader = reader.clone(); + read_xref_table_trailer(&mut reader, &ReaderContext::dummy_with_limits(limits))? + }; + + reader.skip_white_spaces(); + reader.forward_tag(b"xref")?; + reader.skip_white_spaces(); + + let mut max_obj = 0; + + if let Some(prev) = trailer.get::(PREV) { + // First insert the entries from any previous xref tables. + populate_xref_impl_inner(data, prev as usize, insert_map, visited, limits)?; + } + + // In hybrid files, entries in `XRefStm` should have higher priority, therefore we insert them + // after looking at `PREV`. + if let Some(xref_stm) = trailer.get::(XREF_STM) { + populate_xref_impl_inner(data, xref_stm as usize, insert_map, visited, limits)?; + } + + while let Some(header) = reader.read_without_context::() { + reader.skip_white_spaces(); + + let start = header.start; + let end = start.checked_add(header.num_entries)?; + + for obj_number in start..end { + max_obj = max(max_obj, obj_number); + let bytes = reader.read_bytes(XREF_ENTRY_LEN)?; + let entry = XRefEntry::read(bytes)?; + + // Specification says we should ignore any object number > SIZE, but probably + // not important? + if entry.used { + insert_map.insert( + ObjectIdentifier::new(obj_number as i32, entry.gen_number), + EntryType::Normal(entry.offset), + ); + } + } + } + + Some(trailer.data()) +} + +fn populate_from_xref_stream<'a>( + data: &'a [u8], + reader: &mut Reader<'a>, + insert_map: &mut XrefMap, + visited: &mut BTreeSet, + limits: DecodeLimits, +) -> Option<&'a [u8]> { + let stream = reader.read_with_context::>>(&ReaderContext::dummy_with_limits(limits))?.get(); + + if let Some(prev) = stream.dict().get::(PREV) { + // First insert the entries from any previous xref tables. + let _ = populate_xref_impl_inner(data, prev as usize, insert_map, visited, limits)?; + } + + let size = stream.dict().get::(SIZE)?; + + let [f1_len, f2_len, f3_len] = stream.dict().get::<[u8; 3]>(W)?; + + if f2_len > size_of::() as u8 { + error!("xref offset length is larger than the allowed limit"); + + return None; + } + + // Do such files exist? + if f1_len != 1 { + warn!("first field in xref stream was longer than 1"); + } + + let xref_data = stream.decoded().ok()?; + let mut xref_reader = Reader::new(xref_data.as_ref()); + + if let Some(arr) = stream.dict().get::>(INDEX) { + let iter = arr.iter::<(u32, u32)>(); + + for (start, num_elements) in iter { + xref_stream_subsection( + &mut xref_reader, + start, + num_elements, + f1_len, + f2_len, + f3_len, + insert_map, + )?; + } + } else { + xref_stream_subsection( + &mut xref_reader, + 0, + size, + f1_len, + f2_len, + f3_len, + insert_map, + )?; + } + + Some(stream.dict().data()) +} + +fn xref_stream_num(data: &[u8]) -> Option { + Some(match data.len() { + 0 => return None, + 1 => u8::from_be(data[0]) as u32, + 2 => u16::from_be_bytes(data[0..2].try_into().ok()?) as u32, + 3 => u32::from_be_bytes([0, data[0], data[1], data[2]]), + 4 => u32::from_be_bytes(data[0..4].try_into().ok()?), + 8 => { + if let Ok(num) = u32::try_from(u64::from_be_bytes(data[0..8].try_into().ok()?)) { + return Some(num); + } else { + warn!("xref stream number is too large"); + + return None; + } + } + _n => { + warn!("invalid xref stream number {_n}"); + + return None; + } + }) +} + +fn xref_stream_subsection<'a>( + xref_reader: &mut Reader<'a>, + start: u32, + num_elements: u32, + f1_len: u8, + f2_len: u8, + f3_len: u8, + insert_map: &mut XrefMap, +) -> Option<()> { + for i in 0..num_elements { + let f_type = if f1_len == 0 { + 1 + } else { + // We assume a length of 1. + xref_reader.read_bytes(1)?[0] + }; + + let obj_number = start + i; + + match f_type { + // We don't care about free objects. + 0 => { + xref_reader.skip_bytes(f2_len as usize + f3_len as usize)?; + } + 1 => { + let offset = if f2_len > 0 { + let data = xref_reader.read_bytes(f2_len as usize)?; + xref_stream_num(data)? + } else { + 0 + }; + + let gen_number = if f3_len > 0 { + let data = xref_reader.read_bytes(f3_len as usize)?; + xref_stream_num(data)? + } else { + 0 + }; + + insert_map.insert( + ObjectIdentifier::new(obj_number as i32, gen_number as i32), + EntryType::Normal(offset as usize), + ); + } + 2 => { + let obj_stream_number = { + let data = xref_reader.read_bytes(f2_len as usize)?; + xref_stream_num(data)? + }; + let gen_number = 0; + let index = if f3_len > 0 { + let data = xref_reader.read_bytes(f3_len as usize)?; + xref_stream_num(data)? + } else { + 0 + }; + + insert_map.insert( + ObjectIdentifier::new(obj_number as i32, gen_number), + EntryType::ObjStream(obj_stream_number, index), + ); + } + _ => { + warn!("xref has unknown field type {f_type}"); + + return None; + } + } + } + + Some(()) +} + +fn read_xref_table_trailer<'a>( + reader: &mut Reader<'a>, + ctx: &ReaderContext<'a>, +) -> Option> { + reader.skip_white_spaces(); + reader.forward_tag(b"xref")?; + reader.skip_white_spaces(); + + while let Some(header) = reader.read_without_context::() { + let len = XREF_ENTRY_LEN.checked_mul(header.num_entries as usize)?; + reader.jump(reader.offset().checked_add(len)?); + } + + reader.skip_white_spaces(); + reader.forward_tag(b"trailer")?; + reader.skip_white_spaces(); + + reader.read_with_context::>(ctx) +} + +fn get_decryptor(trailer_dict: &Dict<'_>, password: &[u8]) -> Result { + if let Some(encryption_dict) = trailer_dict.get::>(ENCRYPT) { + let id = if let Some(id) = trailer_dict + .get::>(ID) + .and_then(|a| a.flex_iter().next::>()) + { + id.to_vec() + } else { + // Assume an empty ID entry. + vec![] + }; + + get(&encryption_dict, &id, password).map_err(XRefError::Encryption) + } else { + Ok(Decryptor::None) + } +} + +struct ObjectStream<'a> { + data: &'a [u8], + ctx: ReaderContext<'a>, + offsets: Vec<(u32, usize)>, +} + +impl<'a> ObjectStream<'a> { + fn new(inner: Stream<'_>, data: &'a [u8], ctx: &ReaderContext<'a>) -> Option { + let num_objects = inner.dict().get::(N)?; + let first_offset = inner.dict().get::(FIRST)?; + + let mut r = Reader::new(data); + + let mut offsets = vec![]; + + for _ in 0..num_objects { + r.skip_white_spaces_and_comments(); + // Skip object number + let obj_num = r.read_without_context::()?; + r.skip_white_spaces_and_comments(); + let relative_offset = r.read_without_context::()?; + offsets.push((obj_num, first_offset + relative_offset)); + } + + let mut ctx = ctx.clone(); + ctx.set_in_object_stream(true); + + Some(Self { data, ctx, offsets }) + } + + fn get(&self, index: u32) -> Option + where + T: ObjectLike<'a>, + { + let offset = self.offsets.get(index as usize)?.1; + let mut r = Reader::new(self.data); + r.jump(offset); + r.skip_white_spaces_and_comments(); + + r.read_with_context::(&self.ctx) + } +} + +fn parse_metadata(info_dict: &Dict<'_>) -> Metadata { + Metadata { + creation_date: info_dict + .get::>(CREATION_DATE) + .and_then(|c| DateTime::from_bytes(&c)), + modification_date: info_dict + .get::>(MOD_DATE) + .and_then(|c| DateTime::from_bytes(&c)), + title: info_dict + .get::>(TITLE) + .map(|t| t.to_vec()), + author: info_dict + .get::>(AUTHOR) + .map(|t| t.to_vec()), + subject: info_dict + .get::>(SUBJECT) + .map(|t| t.to_vec()), + keywords: info_dict + .get::>(KEYWORDS) + .map(|t| t.to_vec()), + creator: info_dict + .get::>(CREATOR) + .map(|t| t.to_vec()), + producer: info_dict + .get::>(PRODUCER) + .map(|t| t.to_vec()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Contributor-original, MIT OR Apache-2.0. Matches the bounded, uncompressed + // synthetic regression used to reproduce the cold-publication race. A second + // stream optionally checks that racing registration never aliases two ids. + fn object_stream_fixture(second: bool) -> Vec { + let mut out = b"%PDF-1.5\n".to_vec(); + let mut offsets = alloc::collections::BTreeMap::new(); + fn object( + out: &mut Vec, + offsets: &mut alloc::collections::BTreeMap, + n: u32, + body: &[u8], + ) { + offsets.insert(n, out.len()); + out.extend_from_slice(format!("{n} 0 obj\n").as_bytes()); + out.extend_from_slice(body); + out.extend_from_slice(b"\nendobj\n"); + } + object( + &mut out, + &mut offsets, + 1, + b"<< /Type /Catalog /Pages 2 0 R >>", + ); + object( + &mut out, + &mut offsets, + 2, + b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>", + ); + object( + &mut out, + &mut offsets, + 3, + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>", + ); + let mut payload = b"20 0 << /Marker 123 >>".to_vec(); + payload.resize(payload.len() + 4 * 1024 * 1024, b' '); + let mut stream = format!( + "<< /Type /ObjStm /N 1 /First 5 /Length {} >>\nstream\n", + payload.len() + ) + .into_bytes(); + stream.extend(payload); + stream.extend_from_slice(b"\nendstream"); + object(&mut out, &mut offsets, 10, &stream); + if second { + let payload = b"21 0 << /Marker 456 >>"; + let mut stream = format!( + "<< /Type /ObjStm /N 1 /First 5 /Length {} >>\nstream\n", + payload.len() + ) + .into_bytes(); + stream.extend_from_slice(payload); + stream.extend_from_slice(b"\nendstream"); + object(&mut out, &mut offsets, 12, &stream); + } + let xref = out.len(); + offsets.insert(11, xref); + let size = if second { 22 } else { 21 }; + let mut rows = Vec::new(); + for number in 0..size { + let (kind, a, b): (u8, u32, u16) = match number { + 20 => (2, 10, 0), + 21 => (2, 12, 0), + n => offsets + .get(&n) + .map_or((0, 0, 65535), |offset| (1, *offset as u32, 0)), + }; + rows.push(kind); + rows.extend_from_slice(&a.to_be_bytes()); + rows.extend_from_slice(&b.to_be_bytes()); + } + let mut stream = format!( + "<< /Type /XRef /Size {size} /Root 1 0 R /W [1 4 2] /Length {} >>\nstream\n", + rows.len() + ) + .into_bytes(); + stream.extend(rows); + stream.extend_from_slice(b"\nendstream"); + object(&mut out, &mut offsets, 11, &stream); + out.extend_from_slice(format!("startxref\n{xref}\n%%EOF\n").as_bytes()); + out + } + + #[cfg(feature = "std")] + #[test] + fn concurrent_cold_object_stream_reads_all_succeed() { + use std::sync::Barrier; + let bytes = Arc::new(object_stream_fixture(true)); + for _ in 0..10 { + let pdf = Arc::new(crate::Pdf::new(bytes.clone()).unwrap()); + let barrier = Arc::new(Barrier::new(16)); + let jobs: Vec<_> = (0..16) + .map(|_| { + let (pdf, barrier) = (pdf.clone(), barrier.clone()); + std::thread::spawn(move || { + barrier.wait(); + for (id, marker) in [(20, 123), (21, 456), (20, 123)] { + let dict = pdf + .xref() + .get::>(ObjectIdentifier::new(id, 0)) + .unwrap(); + assert_eq!(dict.get::(b"Marker"), Some(marker)); + } + }) + }) + .collect(); + for job in jobs { + job.join().unwrap(); + } + } + } + + #[test] + fn recursive_object_stream_locations_fail_without_recursing() { + let pdf = crate::Pdf::new(object_stream_fixture(true)).unwrap(); + let Inner::Some(repr) = &pdf.xref().0 else { + panic!("real xref") + }; + { + let mut map = repr.map.put(); + map.xref_map + .insert(ObjectIdentifier::new(10, 0), EntryType::ObjStream(12, 0)); + map.xref_map + .insert(ObjectIdentifier::new(12, 0), EntryType::ObjStream(10, 0)); + } + for id in [10, 12, 20, 21] { + assert!( + pdf.xref() + .get::>(ObjectIdentifier::new(id, 0)) + .is_none() + ); + } + } + + #[cfg(feature = "std")] + #[test] + fn concurrent_repair_and_read_retries_the_repaired_map() { + let pdf = Arc::new(crate::Pdf::new(object_stream_fixture(false)).unwrap()); + let Inner::Some(repr) = &pdf.xref().0 else { + panic!("real xref") + }; + repr.map + .put() + .xref_map + .insert(ObjectIdentifier::new(1, 0), EntryType::Normal(0)); + let barrier = Arc::new(std::sync::Barrier::new(16)); + let jobs: Vec<_> = (0..16) + .map(|_| { + let (pdf, barrier) = (pdf.clone(), barrier.clone()); + std::thread::spawn(move || { + barrier.wait(); + for _ in 0..8 { + let root = pdf + .xref() + .get::>(ObjectIdentifier::new(1, 0)) + .unwrap(); + assert!(root.get_ref(PAGES).is_some()); + pdf.xref().repair(); // already repaired is an idempotent no-op + } + }) + }) + .collect(); + for job in jobs { + job.join().unwrap(); + } + assert!(repr.map.get().repaired); + } + + #[cfg(feature = "std")] + #[test] + fn poisoned_repair_lock_does_not_cascade_or_retry_repair() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + let pdf = crate::Pdf::new(object_stream_fixture(false)).unwrap(); + let Inner::Some(repr) = &pdf.xref().0 else { + panic!("real xref") + }; + assert!( + catch_unwind(AssertUnwindSafe(|| { + let mut map = repr.map.put(); + map.repaired = true; // same state as an unwinding fallback before publication + panic!("injected repair failure"); + })) + .is_err() + ); + pdf.xref().repair(); + assert!( + pdf.xref() + .get::>(ObjectIdentifier::new(1, 0)) + .is_some() + ); + assert!(repr.map.get().repaired); + } + + #[test] + fn circular_prev_chain() { + let mut pdf = b"%PDF-1.0\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".to_vec(); + let expected_xref_pos = pdf.len(); + pdf.extend_from_slice( + format!( + "xref\n\ + 0 1\n\ + 0000000000 65535 f \r\n\ + trailer\n<< /Size 1 /Root 1 0 R /Prev {expected_xref_pos} >>\n\ + startxref\n{expected_xref_pos}\n%%EOF" + ) + .as_bytes(), + ); + + let mut xref_map = FxHashMap::default(); + let xref_pos = find_last_xref_pos(pdf.as_ref()).unwrap(); + let _result = populate_xref_impl(pdf.as_ref(), xref_pos, &mut xref_map, DecodeLimits::default()); + } + + #[test] + fn find_last_xref_uses_last_startxref() { + let pdf = b"%PDF-1.0\nstartxref\n5\n%%EOF\nstartxref\n42\n%%EOF"; + assert_eq!(find_last_xref_pos(pdf), Some(42)); + } + + #[test] + fn xref_table_trailer_rejects_overflowing_entry_skip() { + let data = b"xref\n0 999999999999999999999\ntrailer\n<<>>"; + let mut reader = Reader::new(data); + assert!(read_xref_table_trailer(&mut reader, &ReaderContext::dummy()).is_none()); + } +} + +#[cfg(test)] +#[path = "decode_limits_tests.rs"] +mod decode_limits_tests; diff --git a/vendor/hayro/.cargo-ok b/vendor/hayro/.cargo-ok new file mode 100644 index 00000000..5f8b7958 --- /dev/null +++ b/vendor/hayro/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/hayro/.cargo_vcs_info.json b/vendor/hayro/.cargo_vcs_info.json new file mode 100644 index 00000000..4ea4f2df --- /dev/null +++ b/vendor/hayro/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "1e5b8eb7bfe03afe431ed0bc1d704c9432323ef6" + }, + "path_in_vcs": "hayro" +} \ No newline at end of file diff --git a/vendor/hayro/Cargo.lock b/vendor/hayro/Cargo.lock new file mode 100644 index 00000000..de7668d7 --- /dev/null +++ b/vendor/hayro/Cargo.lock @@ -0,0 +1,665 @@ +# 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 = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + +[[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", +] + +[[package]] +name = "foldhash" +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 = "glifo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9d48c6d81526ad2f9d5d6e5fddf5f6949ffbc46fcac0db0d061a6e65097019" +dependencies = [ + "bytemuck", + "foldhash", + "hashbrown", + "log", + "peniko", + "png", + "skrifa", + "smallvec", + "vello_common", +] + +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hayro" +version = "0.7.1" +dependencies = [ + "bytemuck", + "hayro-interpret", + "image", + "kurbo", + "log", + "pic-scale", + "vello_cpu", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2613d0406898995042d0794c4245b2f2fba1246490c8ac593769bba6551129d" +dependencies = [ + "bitflags", + "hayro-cmap", + "hayro-syntax", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edeafd70aa2db743de8ede8637d07ec87db05efe69cde371d03f1b185fcef27" +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 = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[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 = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo", + "linebender_resource_handle", + "smallvec", +] + +[[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 = "pic-scale" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b05a62c3762cb26cb62665b915fea652def8a9f609b0b289b7f782a7394307b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[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 = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +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 = "vello_common" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3361bff7f7d82c0c496b92048db83846691f0e844cc28dee92b1c824291b55ee" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown", + "log", + "peniko", + "png", + "smallvec", + "thiserror", +] + +[[package]] +name = "vello_cpu" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8ded630e8316bb94a55881256506d1f3b9947b5f66db8a7d32ca7ba02decd0" +dependencies = [ + "bytemuck", + "glifo", + "hashbrown", + "png", + "vello_common", +] + +[[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 = "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/Cargo.toml b/vendor/hayro/Cargo.toml new file mode 100644 index 00000000..87ab46a2 --- /dev/null +++ b/vendor/hayro/Cargo.toml @@ -0,0 +1,122 @@ +# 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" +version = "0.7.1" +authors = ["Laurenz Stampfl "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A rasterizer for 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-interpret/embed-cmaps"] +embed-fonts = ["hayro-interpret/embed-fonts"] +logging = ["hayro-interpret/logging"] + +[lib] +name = "hayro" +path = "src/lib.rs" + +[[example]] +name = "render" +path = "examples/render.rs" + +[dependencies.bytemuck] +version = "1" +features = ["bytemuck_derive"] + +[dependencies.hayro-interpret] +version = "0.7.0" +default-features = false + +[dependencies.image] +version = "0.25" +features = ["png"] +default-features = false + +[dependencies.kurbo] +version = "0.13.0" + +# PdfCraft patch: report image resampling fallbacks through the application's logger. +[dependencies.log] +version = "0.4" + +[dependencies.pic-scale] +version = "0.7.9" +features = [ + "neon", + "rdm", + "sse", + "avx", +] +default-features = false + +[dependencies.vello_cpu] +version = "0.0.8" +features = [ + "std", + "png", + "u8_pipeline", +] +default-features = false + +# PdfCraft patch: the upstream-only `[dev-dependencies.log]` entry is removed (log is a normal +# dependency since the logging patch), so `cargo test -p hayro` can run this crate's unit tests +# even though a patched non-workspace crate may not require dev-dependencies. +[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/Cargo.toml.orig b/vendor/hayro/Cargo.toml.orig new file mode 100644 index 00000000..36bdd8bb --- /dev/null +++ b/vendor/hayro/Cargo.toml.orig @@ -0,0 +1,33 @@ +[package] +name = "hayro" +version = "0.7.1" +description = "A rasterizer for PDF files." +edition = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +readme = "README.md" + +[dependencies] +hayro-interpret = { workspace = true } +kurbo = { workspace = true } +image = { workspace = true, features = ["png"] } +bytemuck = { workspace = true, features = ["bytemuck_derive"] } +vello_cpu = { workspace = true } +pic-scale = { workspace = true } +# PdfCraft patch: report image resampling fallbacks through the application's logger. +log = { workspace = true } + +[dev-dependencies] +log = { workspace = true } + +[features] +default = ["embed-fonts", "embed-cmaps"] +embed-fonts = ["hayro-interpret/embed-fonts"] +# Embed the 61 predefined cmaps into the binary. Adds about ~250KB of brotli-encoded data. +embed-cmaps = ["hayro-interpret/embed-cmaps"] +logging = ["hayro-interpret/logging"] + +[lints] +workspace = true diff --git a/vendor/hayro/LICENSE-APACHE b/vendor/hayro/LICENSE-APACHE new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/vendor/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/vendor/hayro/LICENSE-MIT b/vendor/hayro/LICENSE-MIT new file mode 100644 index 00000000..ae3742de --- /dev/null +++ b/vendor/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/vendor/hayro/README.md b/vendor/hayro/README.md new file mode 100644 index 00000000..e09af359 --- /dev/null +++ b/vendor/hayro/README.md @@ -0,0 +1,46 @@ +# hayro + +[![Crates.io](https://img.shields.io/crates/v/hayro.svg)](https://crates.io/crates/hayro) +[![Documentation](https://docs.rs/hayro/badge.svg)](https://docs.rs/hayro) + + + +A crate for rendering PDF files. + +This crate allows you to render pages of a PDF file into bitmaps. It is supposed to be relatively +lightweight, since we do not have any dependencies on the GPU. All the rendering happens on the CPU. + +The ultimate goal of this crate is to be a *feature-complete* and *performant* PDF rasterizer. +With that said, we are currently still very far away from reaching that goal: So far, no effort +has been put into performance optimizations, as we are still working on implementing missing features. +However, this crate is currently the most comprehensive and feature-complete +implementation of a PDF rasterizer in pure Rust. This claim is supported by the fact that we currently +include over 1000 PDF files in our regression test suite. The majority of those have been scraped +from the `pdf.js` and `PDFBOX` test suites and therefore represent a very large and diverse sample +of PDF files. + +As mentioned, there are still some serious limitations, including lack of support for +encrypted/password-protected PDF files, blending and isolation, knockout groups as well as a range +of smaller features such as color key masking. But you should be able to render the vast majority +of PDF files without too many issues. + +### Safety +This crate forbids unsafe code via a crate-level attribute. + +### Examples +For usage examples, see the [example](https://github.com/LaurenzV/hayro/tree/master/hayro/examples) in +the GitHub repository. + +### Cargo features +This crate has one optional feature: +- `embed-fonts`: See the description of [`hayro-interpret`](https://docs.rs/hayro-interpret/latest/hayro_interpret/#cargo-features) for more information. + + + +## 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. diff --git a/vendor/hayro/SOURCE.json b/vendor/hayro/SOURCE.json new file mode 100644 index 00000000..50e0340a --- /dev/null +++ b/vendor/hayro/SOURCE.json @@ -0,0 +1,74 @@ +{ + "crate": "hayro", + "source_repository": "https://github.com/storytold/pdfcraft", + "source_revision": "7437bde378f43856eada40af25b983812e615e8a", + "source_directory": "vendor/hayro", + "files": [ + { + "path": ".cargo-ok", + "source_sha256": "afbf9d0f3560b0fd7795e81c42a0a79ee6b6fc67e064f77826aee642cad28d91", + "source_git_blob": "5f8b795830acbab5961c1a28c76a7916c9631493", + "sha256": "afbf9d0f3560b0fd7795e81c42a0a79ee6b6fc67e064f77826aee642cad28d91" + }, + { + "path": ".cargo_vcs_info.json", + "source_sha256": "92782bf02a2f42784dd2bf7cc6037780fff2974d6d66a32539a405b5ff527b4f", + "source_git_blob": "4ea4f2df77e6ab7bdde54de34409d32cd3cd2073", + "sha256": "92782bf02a2f42784dd2bf7cc6037780fff2974d6d66a32539a405b5ff527b4f" + }, + { + "path": "Cargo.lock", + "source_sha256": "e74d97562bd37a010ecc49af9aa5fde2a22c8c9ae551f83a446c68def200bf6c", + "source_git_blob": "de7668d7ebf73711e739828309e67be62fddee19", + "sha256": "e74d97562bd37a010ecc49af9aa5fde2a22c8c9ae551f83a446c68def200bf6c" + }, + { + "path": "Cargo.toml", + "source_sha256": "efe75cedc81cae62b7a74af2a718b40b0605807874a0f9be37c7b508109decc2", + "source_git_blob": "87ab46a239665a340310707f03bf0e16f0e80e31", + "sha256": "efe75cedc81cae62b7a74af2a718b40b0605807874a0f9be37c7b508109decc2" + }, + { + "path": "Cargo.toml.orig", + "source_sha256": "74943bfca81911b4abe23333370ff1031dd73da4b85739941939c022a7937595", + "source_git_blob": "36bdd8bb07d694cd96bb0575a5caea09aa2594a8", + "sha256": "74943bfca81911b4abe23333370ff1031dd73da4b85739941939c022a7937595" + }, + { + "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": "3c9a96f92225ed01e5135ccddfeeb9084eb45678932105f03e8145c8487c12d4", + "source_git_blob": "e09af3594decc3fd60df78fdc434ec7d340cc1aa", + "sha256": "3c9a96f92225ed01e5135ccddfeeb9084eb45678932105f03e8145c8487c12d4" + }, + { + "path": "examples/render.rs", + "source_sha256": "0b512c65652e56267bb1928548ab49f8ae997107a55fb4db50debe5f94ace910", + "source_git_blob": "1e6ae24f072b34f125d35d22a7612ce546b1fddf", + "sha256": "0b512c65652e56267bb1928548ab49f8ae997107a55fb4db50debe5f94ace910" + }, + { + "path": "src/lib.rs", + "source_sha256": "1ce3e02072acb639c33476618446af80fca29b960a3ca764168c1c2d348d5e5d", + "source_git_blob": "def102b4291b1fe99f2c8975477e5d294c4f955f", + "sha256": "1ce3e02072acb639c33476618446af80fca29b960a3ca764168c1c2d348d5e5d" + }, + { + "path": "src/renderer.rs", + "source_sha256": "b74b630b73e86b69b26ef433ee6aabbb684def133e4ae46f0cbf8f9f1fbd4484", + "source_git_blob": "23fc2e36cae7368e1a508bb246f3a1a15bb60a67", + "sha256": "b74b630b73e86b69b26ef433ee6aabbb684def133e4ae46f0cbf8f9f1fbd4484" + } + ] +} diff --git a/vendor/hayro/examples/render.rs b/vendor/hayro/examples/render.rs new file mode 100644 index 00000000..1e6ae24f --- /dev/null +++ b/vendor/hayro/examples/render.rs @@ -0,0 +1,149 @@ +//! This example shows you how you can render a PDF file to PNG. + +use hayro::hayro_interpret::InterpreterSettings; +use hayro::hayro_interpret::font::{FontData, FontQuery, StandardFont}; +use hayro::hayro_interpret::hayro_cmap::CidFamily; +use hayro::hayro_syntax::Pdf; +use hayro::{RenderCache, RenderSettings, render}; +use std::path::Path; +use std::sync::Arc; +use vello_cpu::color::palette::css::WHITE; + +fn load_asset(name: &str) -> Option<(FontData, u32)> { + let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("../hayro-tests/assets"); + let path = base.join(name); + let data = std::fs::read(&path).ok()?; + Some((Arc::new(data), 0)) +} + +fn main() { + if let Ok(()) = log::set_logger(&LOGGER) { + log::set_max_level(log::LevelFilter::Trace); + } + + let file = std::fs::read(std::env::args().nth(1).unwrap()).unwrap(); + let output_dir = std::env::args().nth(2).unwrap_or_else(|| ".".to_string()); + let scale = std::env::args() + .nth(3) + .and_then(|s| s.parse::().ok()) + .unwrap_or(1.0); + + // Create output directory if it doesn't exist + std::fs::create_dir_all(&output_dir).unwrap(); + + let pdf = Pdf::new(file).unwrap(); + + let interpreter_settings = InterpreterSettings { + font_resolver: Arc::new(move |query| match query { + FontQuery::Standard(s) => { + let name = pick_standard_font(s); + load_asset(name).or_else(|| Some(s.get_font_data())) + } + FontQuery::Fallback(f) => { + if let Some(cc) = &f.character_collection { + let name = match cc.family { + CidFamily::AdobeGB1 | CidFamily::AdobeCNS1 => { + if f.is_bold { + "NotoSansCJKsc-Bold.otf" + } else { + "NotoSansCJKsc-Regular.otf" + } + } + CidFamily::AdobeJapan1 => { + if f.is_bold { + "NotoSansCJKjp-Bold.otf" + } else { + "NotoSansCJKjp-Regular.otf" + } + } + CidFamily::AdobeKorea1 => { + if f.is_bold { + "NotoSansCJKkr-Bold.otf" + } else { + "NotoSansCJKkr-Regular.otf" + } + } + _ => { + let name = pick_standard_font(&f.pick_standard_font()); + return load_asset(name) + .or_else(|| Some(f.pick_standard_font().get_font_data())); + } + }; + + if let Some(data) = load_asset(name) { + return Some(data); + } + } + + let name = pick_standard_font(&f.pick_standard_font()); + load_asset(name).or_else(|| Some(f.pick_standard_font().get_font_data())) + } + }), + ..Default::default() + }; + + let render_settings = RenderSettings { + x_scale: scale, + y_scale: scale, + bg_color: WHITE, + ..Default::default() + }; + let cache = RenderCache::new(); + + for (idx, page) in pdf.pages().iter().enumerate() { + let pixmap = render(page, &cache, &interpreter_settings, &render_settings); + let output_path = format!("{}/rendered_{idx}.png", output_dir); + std::fs::write(output_path, pixmap.into_png().unwrap()).unwrap(); + } +} + +fn pick_standard_font(font: &StandardFont) -> &'static str { + match font { + StandardFont::Helvetica => "LiberationSans-Regular.ttf", + StandardFont::HelveticaBold => "LiberationSans-Bold.ttf", + StandardFont::HelveticaOblique => "LiberationSans-Italic.ttf", + StandardFont::HelveticaBoldOblique => "LiberationSans-BoldItalic.ttf", + StandardFont::Courier => "LiberationMono-Regular.ttf", + StandardFont::CourierBold => "LiberationMono-Bold.ttf", + StandardFont::CourierOblique => "LiberationMono-Italic.ttf", + StandardFont::CourierBoldOblique => "LiberationMono-BoldItalic.ttf", + StandardFont::TimesRoman => "LiberationSerif-Regular.ttf", + StandardFont::TimesBold => "LiberationSerif-Bold.ttf", + StandardFont::TimesItalic => "LiberationSerif-Italic.ttf", + StandardFont::TimesBoldItalic => "LiberationSerif-BoldItalic.ttf", + StandardFont::ZapfDingBats => "FoxitDingbats.pfb", + StandardFont::Symbol => "FoxitSymbol.pfb", + } +} + +/// A simple stderr logger. +static LOGGER: SimpleLogger = SimpleLogger; +struct SimpleLogger; +impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + metadata.level() <= log::LevelFilter::Warn + } + + fn log(&self, record: &log::Record<'_>) { + if self.enabled(record.metadata()) { + let target = if !record.target().is_empty() { + record.target() + } else { + record.module_path().unwrap_or_default() + }; + + let line = record.line().unwrap_or(0); + let args = record.args(); + + match record.level() { + log::Level::Error => eprintln!("Error (in {target}:{line}): {args}"), + log::Level::Warn => eprintln!("Warning (in {target}:{line}): {args}"), + log::Level::Info => eprintln!("Info (in {target}:{line}): {args}"), + log::Level::Debug => eprintln!("Debug (in {target}:{line}): {args}"), + log::Level::Trace => eprintln!("Trace (in {target}:{line}): {args}"), + } + } + } + + fn flush(&self) {} +} diff --git a/vendor/hayro/src/lib.rs b/vendor/hayro/src/lib.rs new file mode 100644 index 00000000..def102b4 --- /dev/null +++ b/vendor/hayro/src/lib.rs @@ -0,0 +1,669 @@ +/*! +A crate for rendering PDF files. + +This crate allows you to render pages of a PDF file into bitmaps. It is supposed to be relatively +lightweight, since we do not have any dependencies on the GPU. All the rendering happens on the CPU. + +The ultimate goal of this crate is to be a *feature-complete* and *performant* PDF rasterizer. +With that said, we are currently still very far away from reaching that goal: So far, no effort +has been put into performance optimizations, as we are still working on implementing missing features. +However, this crate is currently the most comprehensive and feature-complete +implementation of a PDF rasterizer in pure Rust. This claim is supported by the fact that we currently +include over 1000 PDF files in our regression test suite. The majority of those have been scraped +from the `pdf.js` and `PDFBOX` test suites and therefore represent a very large and diverse sample +of PDF files. + +As mentioned, there are still some serious limitations, including lack of support for +encrypted/password-protected PDF files, blending and isolation, knockout groups as well as a range +of smaller features such as color key masking. But you should be able to render the vast majority +of PDF files without too many issues. + +## Safety +This crate forbids unsafe code via a crate-level attribute. + +## Examples +For usage examples, see the [example](https://github.com/LaurenzV/hayro/tree/master/hayro/examples) in +the GitHub repository. + +## Cargo features +This crate has one optional feature: +- `embed-fonts`: See the description of [`hayro-interpret`](https://docs.rs/hayro-interpret/latest/hayro_interpret/#cargo-features) for more information. +*/ + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +use crate::renderer::Renderer; +use hayro_interpret::Device; +use hayro_interpret::FillRule; +use hayro_interpret::InterpreterCache; +use hayro_interpret::InterpreterSettings; +use hayro_interpret::hayro_syntax::Pdf; +use hayro_interpret::hayro_syntax::page::Page; +use hayro_interpret::util::{RectExt, TransformExt}; +use hayro_interpret::{BlendMode, Context}; +use hayro_interpret::{ClipPath, interpret_page}; +use kurbo::{Affine, Rect, Shape}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::ops::RangeInclusive; +use std::rc::Rc; + +pub use hayro_interpret; +pub use hayro_interpret::hayro_syntax; +pub use vello_cpu; + +use vello_cpu::color::AlphaColor; +use vello_cpu::color::Srgb; +use vello_cpu::color::palette::css::TRANSPARENT; +use vello_cpu::color::palette::css::WHITE; +use vello_cpu::{Level, Pixmap, RenderMode}; + +mod renderer; +/// PdfCraft patch: exported for its regression test. +pub use renderer::{image_resampling_size, tiling_cell_scale}; +/// PdfCraft patch: exported for its regression test. +#[doc(hidden)] +pub use renderer::set_skip_offscreen_paths; + +/// A cache used by the renderer. +/// +/// Ideally, such a cache should be constructed once per PDF and then reused across +/// multiple render invocations on the same document. +#[derive(Clone, Default)] +pub struct RenderCache<'a> { + pub(crate) interpreter_cache: InterpreterCache<'a>, + pub(crate) outline_cache: Rc>>>, +} + +impl<'a> RenderCache<'a> { + /// Create a new render cache. + pub fn new() -> Self { + Self { + interpreter_cache: InterpreterCache::new(), + outline_cache: Rc::new(RefCell::new(HashMap::new())), + } + } +} + +/// PdfCraft patch: replacement colours for the "reading colours" view setting (Acrobat's +/// "Replace Document Colors"). Solid paints are mapped by luminance: dark paints move towards +/// `text`, light paints towards `background`. Images, shadings and patterns never pass through +/// the mapping. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ColorReplacement { + /// The sRGB colour that dark paints (low luminance) are mapped towards. + pub text: [u8; 3], + /// The sRGB colour that light paints (high luminance) are mapped towards. + pub background: [u8; 3], + /// When `true`, paints with a luminance above 10% are left unchanged, so only near-black + /// text and line art is recoloured. + pub only_black: bool, + /// When `true`, line-art paints are mapped in addition to text paints. + pub line_art: bool, +} + +impl ColorReplacement { + /// Maps a solid paint colour. `is_text` distinguishes glyph fills/strokes from + /// plain path art. Alpha is copied verbatim. Never panics. + pub fn map(&self, rgba8: [u8; 4], is_text: bool) -> [u8; 4] { + if !is_text && !self.line_art { + return rgba8; + } + let [r, g, b, a] = rgba8; + // Luminance of the gamma-encoded sRGB bytes, normalized to `0.0..=1.0`. + let t = (0.2126 * f32::from(r) + 0.7152 * f32::from(g) + 0.0722 * f32::from(b)) / 255.0; + if self.only_black && t > 0.10 { + return rgba8; + } + let [text_r, text_g, text_b] = self.text; + let [background_r, background_g, background_b] = self.background; + let channel = |text_ch: u8, background_ch: u8| { + let mixed = f32::from(text_ch) * (1.0 - t) + f32::from(background_ch) * t; + // The mixture of two channel values with weights summing to one stays within + // `0.0..=255.0` up to float noise; the clamp keeps the cast total anyway. + mixed.round().clamp(0.0, 255.0) as u8 + }; + [ + channel(text_r, background_r), + channel(text_g, background_g), + channel(text_b, background_b), + a, + ] + } +} + +/// Settings to apply during rendering. +#[derive(Clone, Copy)] +pub struct RenderSettings { + /// How much the contents should be scaled into the x direction. + pub x_scale: f32, + /// How much the contents should be scaled into the y direction. + pub y_scale: f32, + /// The width of the viewport. If this is set to `None`, the width will be chosen + /// automatically based on the scale factor and the dimensions of the PDF. + pub width: Option, + /// The height of the viewport. If this is set to `None`, the height will be chosen + /// automatically based on the scale factor and the dimensions of the PDF. + pub height: Option, + /// The background color. Determines the color of the base + /// rectangle during rendering to a pixmap. + pub bg_color: AlphaColor, + /// PdfCraft patch: top-left corner of the rendered region, in device pixels of the scaled + /// page. Together with `width`/`height` this renders one tile of a large page. + pub x_offset: f32, + /// See `x_offset`. + pub y_offset: f32, + /// PdfCraft patch: when set, solid text and line-art paint colours are remapped while + /// rendering; `None` renders exactly as without the patch. + pub color_replacement: Option, +} + +impl Default for RenderSettings { + fn default() -> Self { + Self { + x_scale: 1.0, + y_scale: 1.0, + width: None, + height: None, + bg_color: TRANSPARENT, + x_offset: 0.0, + y_offset: 0.0, + color_replacement: None, + } + } +} + +/// Render the page with the given settings to a pixmap. +pub fn render<'a>( + page: &'a Page<'a>, + cache: &RenderCache<'a>, + interpreter_settings: &InterpreterSettings, + render_settings: &RenderSettings, +) -> Pixmap { + let (pix_width, pix_height) = render_size(page, render_settings); + let mut pixmap = Pixmap::new(pix_width, pix_height); + render_into( + page, + cache, + interpreter_settings, + render_settings, + pixmap.data_as_u8_slice_mut(), + ); + pixmap +} + +/// PdfCraft patch: the size in pixels of what [`render`] produces with these settings. +pub fn render_size(page: &Page<'_>, render_settings: &RenderSettings) -> (u16, u16) { + let (width, height) = page.render_dimensions(); + let (scaled_width, scaled_height) = ( + (width * render_settings.x_scale) as f64, + (height * render_settings.y_scale) as f64, + ); + ( + render_settings.width.unwrap_or(scaled_width.floor() as u16), + render_settings + .height + .unwrap_or(scaled_height.floor() as u16), + ) +} + +/// PdfCraft patch: [`render`] into `buffer`, premultiplied RGBA8 rows of [`render_size`] +/// pixels, so the caller chooses the allocation (one aligned to whole pixels can then become +/// texture data without a copy). `buffer.len()` must be width ร— height ร— 4; any other length +/// leaves `buffer` untouched (vello_cpu asserts on the length, and a panic must not be reachable). +pub fn render_into<'a>( + page: &'a Page<'a>, + cache: &RenderCache<'a>, + interpreter_settings: &InterpreterSettings, + render_settings: &RenderSettings, + buffer: &mut [u8], +) { + let (x_scale, y_scale) = (render_settings.x_scale, render_settings.y_scale); + let initial_transform = Affine::translate((-render_settings.x_offset as f64, -render_settings.y_offset as f64)) + * Affine::scale_non_uniform(x_scale as f64, y_scale as f64) + * page.initial_transform(true).to_kurbo(); + + let (pix_width, pix_height) = render_size(page, render_settings); + let expected = (pix_width as usize) + .checked_mul(pix_height as usize) + .and_then(|n| n.checked_mul(4)); + if expected != Some(buffer.len()) { + return; + } + let mut state = Context::new( + initial_transform, + Rect::new(0.0, 0.0, pix_width as f64, pix_height as f64), + &cache.interpreter_cache, + page.xref(), + interpreter_settings.clone(), + ); + + let vc_settings = vello_cpu::RenderSettings { + level: Level::new(), + num_threads: 0, + render_mode: RenderMode::OptimizeSpeed, + }; + + let mut device = Renderer::new( + pix_width, + pix_height, + vc_settings, + cache, + render_settings.color_replacement, + interpreter_settings.preview, + ); + + device.ctx.set_paint(render_settings.bg_color); + device + .ctx + .fill_rect(&Rect::new(0.0, 0.0, pix_width as f64, pix_height as f64)); + let mut clip_path = page.intersected_crop_box().to_kurbo().to_path(0.1); + clip_path.apply_affine(initial_transform); + device.push_clip_path(&ClipPath { + path: clip_path, + fill: FillRule::NonZero, + }); + + device.push_transparency_group(1.0, None, BlendMode::Normal); + interpret_page(page, &mut state, &mut device); + + device.pop_transparency_group(); + + device.pop_clip_path(); + + let mut resources = vello_cpu::Resources::default(); + device.ctx.render_to_buffer( + &mut resources, + buffer, + pix_width, + pix_height, + vc_settings.render_mode, + ); +} + +// Just a convenience method for testing. +#[doc(hidden)] +pub fn render_pdf( + pdf: &Pdf, + scale: f32, + settings: InterpreterSettings, + color_replacement: Option, + range: Option>, +) -> Option> { + let cache = RenderCache::new(); + let rendered = pdf + .pages() + .iter() + .enumerate() + .flat_map(|(idx, page)| { + if range.clone().is_some_and(|range| !range.contains(&idx)) { + return None; + } + + let pixmap = render( + page, + &cache, + &settings, + &RenderSettings { + x_scale: scale, + y_scale: scale, + bg_color: WHITE, + color_replacement, + ..Default::default() + }, + ); + + Some(pixmap) + }) + .collect(); + + Some(rendered) +} + +pub(crate) fn derive_settings(settings: &vello_cpu::RenderSettings) -> vello_cpu::RenderSettings { + vello_cpu::RenderSettings { + num_threads: 0, + ..*settings + } +} + +// PdfCraft patch: unit tests for the reading-colours mapping. +#[cfg(test)] +mod color_replacement_tests { + use super::ColorReplacement; + + fn config( + text: [u8; 3], + background: [u8; 3], + only_black: bool, + line_art: bool, + ) -> ColorReplacement { + ColorReplacement { + text, + background, + only_black, + line_art, + } + } + + #[test] + fn black_maps_to_text_colour() { + let r = config([0x12, 0x34, 0x56], [0xff, 0xff, 0xff], false, true); + assert_eq!(r.map([0x00, 0x00, 0x00, 0xff], true), [0x12, 0x34, 0x56, 0xff]); + } + + #[test] + fn white_maps_to_background() { + let r = config([0x00, 0x00, 0x00], [0xab, 0xcd, 0xef], false, true); + assert_eq!(r.map([0xff, 0xff, 0xff, 0xff], true), [0xab, 0xcd, 0xef, 0xff]); + } + + #[test] + fn midgrey_interpolates() { + // 0x80 grey between black text and a white background lands near 0x80. + let r = config([0x00, 0x00, 0x00], [0xff, 0xff, 0xff], false, true); + let [mapped_r, mapped_g, mapped_b, mapped_a] = r.map([0x80, 0x80, 0x80, 0xff], true); + assert!(mapped_r.abs_diff(0x80) <= 2); + assert!(mapped_g.abs_diff(0x80) <= 2); + assert!(mapped_b.abs_diff(0x80) <= 2); + assert_eq!(mapped_a, 0xff); + } + + #[test] + fn alpha_copied_verbatim() { + let r = config([0xff, 0x00, 0x00], [0x00, 0x00, 0xff], false, true); + assert_eq!(r.map([0x00, 0x00, 0x00, 137], true), [0xff, 0x00, 0x00, 137]); + assert_eq!(r.map([0xff, 0xff, 0xff, 0], true), [0x00, 0x00, 0xff, 0]); + } + + #[test] + fn only_black_threshold() { + // White text on black: a mapped grey v becomes 255 - v, so mapped and skipped + // values are distinguishable. + let r = config([0xff, 0xff, 0xff], [0x00, 0x00, 0x00], true, true); + // Luminance t = 25/255 โ‰ˆ 0.098 is just under the 0.10 threshold: mapped. + assert_eq!(r.map([25, 25, 25, 255], true), [230, 230, 230, 255]); + // Luminance t = 27/255 โ‰ˆ 0.106 is just over it: left unchanged. + assert_eq!(r.map([27, 27, 27, 255], true), [27, 27, 27, 255]); + } + + #[test] + fn line_art_false_skips_paths_but_not_text() { + let r = config([0xff, 0xff, 0xff], [0x00, 0x00, 0x00], false, false); + // A plain path fill is left unchanged when line art is off. + assert_eq!(r.map([0x00, 0x00, 0x00, 255], false), [0x00, 0x00, 0x00, 255]); + // Text fills/strokes still map. + assert_eq!(r.map([0x00, 0x00, 0x00, 255], true), [0xff, 0xff, 0xff, 255]); + } +} + +// PdfCraft patch: end-to-end test for the reading-colours renderer hook. +#[cfg(test)] +mod color_replacement_render_tests { + use super::{ColorReplacement, InterpreterSettings, Pdf, Pixmap, render_pdf}; + use vello_cpu::color::{AlphaColor, Srgb}; + + /// A one-page 200ร—100 pt PDF with a white background rect, black Helvetica text, + /// a solid red `rg` rect, a CMYK-filled rect and a 4ร—4 uncompressed `/DeviceRGB` + /// image. Byte-level object layout follows `fixture()` in + /// `crates/automation/tests/automation.rs`. + fn fixture() -> Vec { + let body = "1 1 1 rg 0 0 200 100 re f\n\ + 0 0 0 rg BT /F1 24 Tf 20 70 Td (HI) Tj ET\n\ + 1 0 0 rg 20 20 50 40 re f\n\ + 1 0 0 0 k 100 20 50 40 re f\n\ + q 20 0 0 20 160 20 cm /Im0 Do Q"; + // 16 opaque pure-blue pixels; blue's low luminance makes an (incorrect) mapping + // immediately visible as a light grey. + let image: Vec = (0..16).flat_map(|_| [0x00_u8, 0x00, 0xff]).collect(); + + let mut objs: Vec> = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [4 0 R] /Count 1 /MediaBox [0 0 200 100] >>".to_vec(), + b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /Contents 5 0 R /Resources << /Font << /F1 3 0 R >> \ + /XObject << /Im0 6 0 R >> >> >>" + .to_vec(), + format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(), + ]; + let mut image_obj = format!( + "<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB \ + /BitsPerComponent 8 /Length {} >>\nstream\n", + image.len() + ) + .into_bytes(); + image_obj.extend_from_slice(&image); + image_obj.extend_from_slice(b"\nendstream"); + objs.push(image_obj); + + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes()); + out.extend_from_slice(o); + out.extend_from_slice(b"\nendobj\n"); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice( + format!( + "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", + objs.len() + 1 + ) + .as_bytes(), + ); + out + } + + fn replacement(line_art: bool) -> ColorReplacement { + ColorReplacement { + text: [0xff, 0xff, 0xff], + background: [0x00, 0x00, 0x00], + only_black: false, + line_art, + } + } + + /// A one-page 100ร—50 pt PDF whose only content is a 4ร—4 `/ImageMask` stencil (CCITT-style + /// line art) painted with the current fill colour, black. Each row's four samples are + /// `0011`: under `/Decode [0 1]` a sample value of 0 marks the painted area, so the left + /// half of the 40ร—40 pt image paints and the right half does not. + fn stencil_fixture() -> Vec { + let body = "0 0 0 rg q 40 0 0 40 30 5 cm /Im0 Do Q"; + // Four rows, one byte each (a 4-wide row uses the top nibble): samples 0,0,1,1 โ€” + // under Decode [0 1] the zeros paint, so the left half of every row is ink and the + // right half is unpainted. + let mask: [u8; 4] = [0x30, 0x30, 0x30, 0x30]; + + let objs: Vec> = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 100 50] >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >>" + .to_vec(), + format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()).into_bytes(), + { + let mut o = format!( + "<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ImageMask true \ + /BitsPerComponent 1 /Decode [0 1] /Length {} >>\nstream\n", + mask.len() + ) + .into_bytes(); + o.extend_from_slice(&mask); + o.extend_from_slice(b"\nendstream"); + o + }, + ]; + + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes()); + out.extend_from_slice(o); + out.extend_from_slice(b"\nendobj\n"); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice( + format!( + "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", + objs.len() + 1 + ) + .as_bytes(), + ); + out + } + + /// Renders the stencil fixture the way the app's render crate drives hayro for a + /// white-on-black user: the page clears to `bg` (`RenderSettings.bg_color`) while the + /// solid paints go through `color_replacement`. `render_pdf` cannot express the mapped + /// clear colour (it always clears white). + fn render_stencil_on( + bg: AlphaColor, + color_replacement: Option, + ) -> Pixmap { + use super::{RenderCache, RenderSettings, render}; + + let pdf = Pdf::new(stencil_fixture()).unwrap(); + let cache = RenderCache::new(); + let settings = RenderSettings { + x_scale: 1.0, + y_scale: 1.0, + bg_color: bg, + color_replacement, + ..RenderSettings::default() + }; + render( + pdf.pages().get(0).expect("the fixture has one page"), + &cache, + &InterpreterSettings::default(), + &settings, + ) + } + + fn render(color_replacement: Option) -> Pixmap { + let pdf = Pdf::new(fixture()).unwrap(); + render_pdf( + &pdf, + 1.0, + InterpreterSettings::default(), + color_replacement, + None, + ) + .unwrap() + .remove(0) + } + + /// Samples the pixel at (x, y); all fixture content is opaque, so the premultiplied + /// value is the straight colour. + fn sample(pixmap: &Pixmap, x: u16, y: u16) -> [u8; 4] { + let p = pixmap.sample(x, y); + [p.r, p.g, p.b, p.a] + } + + fn count_pixels( + pixmap: &Pixmap, + region: (u16, u16, u16, u16), + matches: impl Fn([u8; 4]) -> bool, + ) -> u32 { + let (x0, y0, x1, y1) = region; + let mut count = 0; + for y in y0..y1 { + for x in x0..x1 { + if matches(sample(pixmap, x, y)) { + count += 1; + } + } + } + count + } + + #[test] + fn color_replacement_maps_text_and_line_art() { + let is_white = |p: [u8; 4]| p == [255, 255, 255, 255]; + let is_black = |p: [u8; 4]| p == [0, 0, 0, 255]; + + let control = render(None); + let replaced = render(Some(replacement(true))); + + // Background: the full-page white rect is line art and maps to the + // `background` colour exactly (t = 1.0). + assert_eq!(sample(&control, 5, 5), [255, 255, 255, 255]); + assert_eq!(sample(&replaced, 5, 5), [0, 0, 0, 255]); + + // Text: solid black glyph cores in the control, fully white after replacement. + // Region covers the "HI" glyphs (PDF baseline y=70, 24 pt โ†’ device y 10..34). + let text_region = (16, 10, 56, 34); + assert!(count_pixels(&control, text_region, is_black) >= 10); + assert!(count_pixels(&replaced, text_region, is_white) >= 10); + + // Red rect: luminance t โ‰ˆ 0.2126 maps every channel to 255ยท(1โˆ’t) โ‰ˆ 201 (โ‰ˆ0xC9), + // a near-white neutral grey. + assert_eq!(sample(&control, 45, 60), [255, 0, 0, 255]); + let [r, g, b, a] = sample(&replaced, 45, 60); + assert!(r.abs_diff(201) <= 4, "red channel {r} not โ‰ˆ 201"); + assert!(g.abs_diff(201) <= 4 && b.abs_diff(201) <= 4, "g/b not โ‰ˆ 201: {g}/{b}"); + assert_eq!(a, 255); + + // CMYK rect (cyan): mapped to a neutral grey that differs from the control + // colour, whatever exact value the CMYKโ†’RGB transform produces. + let cmyk_control = sample(&control, 125, 60); + let cmyk_replaced = sample(&replaced, 125, 60); + assert_ne!(cmyk_control[..3], cmyk_replaced[..3]); + assert!( + (i32::from(cmyk_replaced[0]) - i32::from(cmyk_replaced[1])).abs() <= 2 + && (i32::from(cmyk_replaced[1]) - i32::from(cmyk_replaced[2])).abs() <= 2, + "mapped CMYK not neutral: {cmyk_replaced:?}" + ); + assert_eq!(cmyk_replaced[3], 255); + + // Image pixels pass through untouched, identical to the control render. + assert_eq!(sample(&control, 170, 70), [0, 0, 255, 255]); + assert_eq!(sample(&replaced, 170, 70), [0, 0, 255, 255]); + + // With line-art replacement off, the red rect and the background rect keep + // their original colours while text is still replaced. + let no_line_art = render(Some(replacement(false))); + assert_eq!(sample(&no_line_art, 45, 60), [255, 0, 0, 255]); + assert_eq!(sample(&no_line_art, 5, 5), [255, 255, 255, 255]); + assert!(count_pixels(&no_line_art, text_region, is_white) >= 10); + } + + /// A stencil mask's ink paints with the mapped solid colour on the mapped background: + /// it is line art in disguise, so white-on-black must leave it visible (black ink on + /// black would disappear). The image occupies device x 30..70, y 5..45 (PDF 30..70 ร— + /// 5..45 pt); its left half is painted ink, its right half shows the page background. + #[test] + fn color_replacement_maps_stencil_mask_ink() { + use vello_cpu::color::palette::css::{BLACK, WHITE}; + + let control = render_stencil_on(WHITE, None); + let replaced = render_stencil_on(BLACK, Some(replacement(true))); + + // Control: the page clears white and the stencil's ink keeps its black fill colour. + assert_eq!(sample(&control, 5, 5), [255, 255, 255, 255]); + assert_eq!(sample(&control, 40, 25), [0, 0, 0, 255]); + // With white-on-black, the background clears black and the ink maps to white: ink + // and background stay distinguishable, and each matches its mapped colour exactly. + assert_eq!(sample(&replaced, 5, 5), [0, 0, 0, 255]); + let ink = sample(&replaced, 40, 25); + assert_eq!(ink, [255, 255, 255, 255]); + assert_ne!(ink[..3], sample(&replaced, 5, 5)[..3], "ink must differ from the background"); + // The unpainted right half of the image area shows the mapped background. + assert_eq!(sample(&replaced, 60, 25), [0, 0, 0, 255]); + + // The line-art gate holds: with line-art replacement off, the ink keeps its + // original colour while the page still clears to the background. + let no_line_art = render_stencil_on(BLACK, Some(replacement(false))); + assert_eq!(sample(&no_line_art, 5, 5), [0, 0, 0, 255]); + assert_eq!(sample(&no_line_art, 40, 25), [0, 0, 0, 255]); + } +} diff --git a/vendor/hayro/src/renderer.rs b/vendor/hayro/src/renderer.rs new file mode 100644 index 00000000..23fc2e36 --- /dev/null +++ b/vendor/hayro/src/renderer.rs @@ -0,0 +1,1879 @@ +use crate::{ColorReplacement, RenderCache, derive_settings}; +use hayro_interpret::color::Color; +use hayro_interpret::encode::EncodedShadingPattern; +use hayro_interpret::font::Glyph; +use hayro_interpret::pattern::Pattern; +use hayro_interpret::{ + BlendMode, CacheKey, ClipPath, Device, FillRule, GlyphDrawMode, ImageData, LumaData, MaskType, + Paint, PathDrawMode, RgbData, SoftMask, StrokeProps, +}; +use kurbo::{Affine, BezPath, Point, Rect, Shape, Vec2}; +use pic_scale::{ + ImageSize, ImageStore, ImageStoreMut, PicScaleError, Resampling, ResamplingFunction, Scaler, +}; +use std::collections::HashMap; +use std::mem::size_of; +use std::rc::Rc; +use std::sync::Arc; +use vello_cpu::color::DynamicColor; +use vello_cpu::color::palette::css::BLACK; +use vello_cpu::color::{AlphaColor, PremulRgba8, Srgb}; +use vello_cpu::peniko::{ColorStop, Compose, Fill, Gradient, ImageQuality, ImageSampler, Mix}; +use vello_cpu::{ + Image, ImageSource, Mask, PaintType, Pixmap, RenderContext, RenderSettings, peniko, +}; + +/// PdfCraft patch: largest image drawn, in pixels (2^28 โ‰ˆ 268 MP, well above real page images). +const MAX_IMAGE_PIXELS: u64 = 1 << 28; + +/// PdfCraft patch: source axes allowed into CatmullRom planning. Unlike the final pixmap, +/// sources need not fit u16: a 65536-pixel strip can safely shrink to a 16-pixel image. +/// This resource cap follows the existing CCITT column limit; the filter-work estimate below +/// additionally bounds tables/scratch before entering the opaque PicScale planner. +const MAX_RESAMPLING_SOURCE_SIDE: u32 = 1 << 20; +const MAX_RESAMPLING_PLAN_BYTES: usize = 128 << 20; + +/// PdfCraft patch: source, intermediate and RGBA byte lengths, independent of backend sides. +fn source_byte_len(width: u32, height: u32, channels: usize, max_pixels: u64) -> Option { + if width == 0 || height == 0 || !matches!(channels, 1 | 3 | 4) { + return None; + } + let pixels = u64::from(width).checked_mul(u64::from(height))?; + if pixels > max_pixels { + return None; + } + let pixels = usize::try_from(pixels).ok()?; + let rgba_len = pixels.checked_mul(4)?; + if rgba_len > isize::MAX as usize { + return None; + } + pixels.checked_mul(channels) +} + +/// PdfCraft patch: only final pixmaps/padded glyphs need to fit the backend's u16 sides. +fn image_byte_len(width: u32, height: u32, channels: usize, max_pixels: u64) -> Option { + if width > u32::from(u16::MAX) || height > u32::from(u16::MAX) { + return None; + } + source_byte_len(width, height, channels, max_pixels) +} + +/// PdfCraft patch: conservative CatmullRom planning work for one axis in PicScale 0.7.12. +/// Its four-tap kernel grows on downscaling. Include float/32-aligned i16 weights, temporary +/// float/f64/usize tap arrays and two copies of the bounds. This is a size estimate, not a +/// fallible reserve inside PicScale or a bound on total renderer memory. +fn filter_plan_bytes(source: u32, target: u32) -> Option { + if source == target { + return Some(0); + } + let (source, target) = (usize::try_from(source).ok()?, usize::try_from(target).ok()?); + if target == 0 { + return None; + } + let taps = source.checked_mul(4)?.div_ceil(target).max(4); + let aligned_taps = taps.div_ceil(32).checked_mul(32)?; + let float_weights = taps.checked_mul(target)?.checked_mul(size_of::())?; + let fixed_weights = aligned_taps + .checked_mul(target)? + .checked_mul(size_of::())?; + let tap_scratch = taps.checked_mul(size_of::() + size_of::() + size_of::())?; + let bounds = target.checked_mul(4)?.checked_mul(size_of::())?; + float_weights + .checked_add(fixed_weights)? + .checked_add(tap_scratch)? + .checked_add(bounds) +} + +/// PdfCraft patch: alpha-aware RGBA plans also need source-sized premultiplication scratch. +/// Bound the combined scratch before planning, including a conservative crossed intermediate +/// (or four rows for a short target) when both convolution axes change. +fn resampling_scratch_byte_len( + source: (u32, u32), + target: (u32, u32), + channels: usize, + max_pixels: u64, +) -> Option { + if source == target { + return Some(0); + } + let alpha = if channels == 4 { + source_byte_len(source.0, source.1, channels, max_pixels)? + } else { + 0 + }; + let filter = if source.0 != target.0 && source.1 != target.1 { + source_byte_len(source.0, target.1.max(4), channels, max_pixels)? + } else { + 0 + }; + let len = alpha.checked_add(filter)?; + let limit = max_pixels.saturating_mul(4).min(isize::MAX as u64); + if u64::try_from(len).ok()? > limit { + return None; + } + Some(len) +} + +/// PdfCraft patch: validate output, crossed intermediate and filter-work bounds before planning. +/// Source width ร— target height also bounds PicScale's smaller single-threaded four-row scratch. +/// Nearest-neighbour (`!interpolate`) has none of those: it copies source pixels into the target, +/// so only the source and target buffers are bounded. +fn resampling_byte_len( + source: (u32, u32), + target: (u32, u32), + channels: usize, + data_len: usize, + max_pixels: u64, + interpolate: bool, +) -> Option { + if source_byte_len(source.0, source.1, channels, max_pixels)? != data_len { + return None; + } + let target_len = image_byte_len(target.0, target.1, channels, max_pixels)?; + if !interpolate { + return Some(target_len); + } + resampling_scratch_byte_len(source, target, channels, max_pixels)?; + source_byte_len(source.0, target.1, channels, max_pixels)?; + if source.0 > MAX_RESAMPLING_SOURCE_SIDE || source.1 > MAX_RESAMPLING_SOURCE_SIDE { + return None; + } + let plan_bytes = filter_plan_bytes(source.0, target.0)? + .checked_add(filter_plan_bytes(source.1, target.1)?)?; + if plan_bytes > MAX_RESAMPLING_PLAN_BYTES { + return None; + } + Some(target_len) +} + +/// PdfCraft patch: image dimensions selected before allocating resampling buffers. +/// Exported for PdfCraft's small, metadata-only regression tests. `None` rejects invalid +/// sources/scales or an over-budget target/intermediate; the caller may retain a valid source. +/// `interpolate` selects the Catmull-Rom limits; nearest-neighbour (`false`) skips them. +#[doc(hidden)] +pub fn image_resampling_size( + width: u32, + height: u32, + channels: usize, + data_len: usize, + x_scale: f32, + y_scale: f32, + max_pixels: u64, + interpolate: bool, +) -> Option<(u32, u32)> { + if !x_scale.is_finite() || !y_scale.is_finite() || x_scale <= 0.0 || y_scale <= 0.0 { + return None; + } + let target = if x_scale < 1.0 || y_scale < 1.0 { + let side = |size: u32, scale: f32| { + (size as f32 * scale) + .ceil() + .max(1.0) + .min((u16::MAX / 2) as f32) as u32 + }; + (side(width, x_scale), side(height, y_scale)) + } else { + (width, height) + }; + resampling_byte_len( + (width, height), + target, + channels, + data_len, + max_pixels, + interpolate, + )?; + Some(target) +} + +/// PdfCraft patch: allocation failures in image conversion/resampling keep the renderer alive. +fn image_buffer(len: usize) -> Option> { + let mut out = Vec::new(); + out.try_reserve_exact(len).ok()?; + Some(out) +} + +/// PdfCraft patch (#624): the source index that PicScale's nearest-neighbour plan copies for +/// destination `index` of `dst` samples taken from `src`. It is the same fixed-point centre +/// sampling as `ResampleNearestPlan`, so these are the samples a nearest resize would copy. +fn nearest_source_index(src: u32, dst: u32, index: u32) -> usize { + let step = (u64::from(src) << 32) / u64::from(dst); + ((u64::from(index) * step + (step >> 1)) >> 32) as usize +} + +/// PdfCraft patch (#624): nearest-resizes a colour image and its same-sized soft mask straight +/// into premultiplied RGBA of `target` size. Colour and mask come from the same source pixel, so +/// this equals a nearest resize of the expanded RGBA followed by premultiplying each pixel. +fn nearest_premultiplied_rgba( + color: &ImageData, + alpha: &[u8], + source: (u32, u32), + target: (u32, u32), +) -> Option> { + let (samples, channels) = match color { + ImageData::Luma(luma) => (luma.data.as_slice(), 1), + ImageData::Rgb(rgb) => (rgb.data.as_slice(), 3), + }; + let mut out = image_buffer(image_byte_len(target.0, target.1, 4, MAX_IMAGE_PIXELS)?)?; + for ty in 0..target.1 { + let sy = nearest_source_index(source.1, target.1, ty); + for tx in 0..target.0 { + let si = sy * source.0 as usize + nearest_source_index(source.0, target.0, tx); + let [r, g, b] = match samples.get(si * channels..si * channels + channels)? { + [v] => [*v; 3], + [r, g, b] => [*r, *g, *b], + _ => return None, + }; + let a = *alpha.get(si)?; + out.extend_from_slice( + &AlphaColor::from_rgba8(r, g, b, a) + .premultiply() + .to_rgba8() + .to_u8_array(), + ); + } + } + Some(out) +} + +/// PdfCraft patch: most pieces one dashed stroke may be cut into. Stroke expansion keeps every +/// dash, so a pattern that is tiny next to its path asks for billions: a fuzzed `/D [[11] 0]` on +/// a line from x = 92234775807 allocated over 5 GB, and `[0.000001] 0 d` on a 20 pt line over +/// 10 GB. Measured in release builds: a stroke cut into 250,000 pieces renders in about 16 ms and +/// 75 MB at its peak (a million took 80 ms and 290 MB). Dotted and dashed lines in real documents +/// stay far below this. +const MAX_DASHES_PER_STROKE: f64 = 250_000.0; + +/// PdfCraft patch: the dash pattern to stroke `path` with. Empty (a solid line) when the pattern +/// is invalid, or when it could cut the path into more than [`MAX_DASHES_PER_STROKE`] pieces. +/// ISO 32000-2 ยง8.4.3.6 requires nonnegative entries that are not all zero; a pattern whose +/// period isn't positive (`[-1 -1] 0 d`) made kurbo's search for the starting dash loop for ever. +fn usable_dash_pattern(path: &BezPath, dashes: &[f32]) -> Vec { + if dashes.iter().any(|d| !d.is_finite() || *d < 0.0) { + return Vec::new(); + } + let sum: f64 = dashes.iter().map(|d| f64::from(*d)).sum(); + // kurbo repeats an odd-length pattern once, as SVG does, so a period covers it twice. + let (period, per_period) = if dashes.len() % 2 == 1 { + (2.0 * sum, dashes.len().saturating_mul(2)) + } else { + (sum, dashes.len()) + }; + if !period.is_finite() || period <= 0.0 { + return Vec::new(); + } + let (length, restarts) = control_polygon_length(path); + // kurbo restarts the pattern at every subpath and after every `ClosePath`, so each restart + // can begin up to a whole period of pieces however short its segment is. + let pieces = (length / period + restarts as f64) * per_period as f64; + if !pieces.is_finite() || pieces > MAX_DASHES_PER_STROKE { + return Vec::new(); + } + dashes.iter().map(|d| f64::from(*d)).collect() +} + +/// PdfCraft patch: the length of `path`'s control polygon, which a curve never exceeds, and how +/// many times kurbo restarts a dash pattern along it (each `MoveTo` and `ClosePath`). +fn control_polygon_length(path: &BezPath) -> (f64, usize) { + let (mut length, mut restarts) = (0.0, 0usize); + let (mut start, mut last) = (Point::ORIGIN, Point::ORIGIN); + for el in path.elements() { + match *el { + kurbo::PathEl::MoveTo(p) => { + (start, last) = (p, p); + restarts = restarts.saturating_add(1); + } + kurbo::PathEl::LineTo(p) => { + length += last.distance(p); + last = p; + } + kurbo::PathEl::QuadTo(c, p) => { + length += last.distance(c) + c.distance(p); + last = p; + } + kurbo::PathEl::CurveTo(c1, c2, p) => { + length += last.distance(c1) + c1.distance(c2) + c2.distance(p); + last = p; + } + kurbo::PathEl::ClosePath => { + length += last.distance(start); + last = start; + restarts = restarts.saturating_add(1); + } + } + } + (length, restarts) +} + +/// PdfCraft patch: how far, in user space, drawing may place geometry outside a path's exact +/// outline. vello expands strokes to a tolerance of at most 0.25 user units (`TOL / max(|a|, |d|, +/// 1)` in vello_common's `flatten::stroke`), and kurbo approximates round joins and caps to 1e-3. +/// Fills are flattened into chords, which stay inside the outline's bounds. +const DRAW_SLACK: f64 = 0.5; + +/// PdfCraft patch: previews skip images below this target area in device pixels: redrawing +/// them at full detail costs a decode and a resample each for pixels nobody can see. +const PREVIEW_MIN_IMAGE_AREA: u32 = 16; + +thread_local! { + /// PdfCraft patch: whether [`may_paint_canvas`] may skip paths on this thread. Turned off + /// only by PdfCraft's regression test, which renders each tile with and without skipping. + static SKIP_OFFSCREEN_PATHS: std::cell::Cell = const { std::cell::Cell::new(true) }; +} + +/// PdfCraft patch: exported for its regression test (see [`SKIP_OFFSCREEN_PATHS`]). +#[doc(hidden)] +pub fn set_skip_offscreen_paths(skip: bool) { + SKIP_OFFSCREEN_PATHS.with(|s| s.set(skip)); +} + +/// PdfCraft patch: whether a device-space box can paint a pixel of a `width` ร— `height` +/// canvas. A box whose bounds miss the canvas is skipped without changing a pixel. +/// Bounds that aren't finite are drawn as before. +fn paints_canvas(device: Rect, width: u16, height: u16) -> bool { + if !SKIP_OFFSCREEN_PATHS.with(std::cell::Cell::get) { + return true; + } + let finite = [device.x0, device.y0, device.x1, device.y1] + .iter() + .all(|v| v.is_finite()); + !finite + || (device.x0 < f64::from(width) + && device.x1 > 0.0 + && device.y0 < f64::from(height) + && device.y1 > 0.0) +} + +/// PdfCraft patch: whether a path drawn with `transform` can paint a pixel of a `width` ร— +/// `height` canvas. `reach` is how far its paint extends beyond the path, in user space: 0 for a +/// fill; for a stroke, half its width times the larger of its miter limit (kurbo draws a miter +/// only when its tip is within `limit ร— width / 2` of the vertex) and โˆš2 (square caps). The box +/// of the path's points and control points (a curve stays inside their hull) grown by that and +/// [`DRAW_SLACK`], mapped to the device and grown by 2 pixels for antialiasing, contains +/// everything it paints: a path whose box misses the canvas is skipped without changing a pixel, +/// so a tile of a large page no longer strokes and fills every path on the page. Bounds that +/// aren't finite (an empty path) are drawn as before. +fn may_paint_canvas( + path: &BezPath, + transform: Affine, + reach: f64, + width: u16, + height: u16, +) -> bool { + let mut points = Rect::new( + f64::INFINITY, + f64::INFINITY, + f64::NEG_INFINITY, + f64::NEG_INFINITY, + ); + let mut add = |p: Point| points = points.union_pt(p); + for el in path.elements() { + match *el { + kurbo::PathEl::MoveTo(p) | kurbo::PathEl::LineTo(p) => add(p), + kurbo::PathEl::QuadTo(c, p) => { + add(c); + add(p); + } + kurbo::PathEl::CurveTo(c1, c2, p) => { + add(c1); + add(c2); + add(p); + } + kurbo::PathEl::ClosePath => {} + } + } + let grow = reach + DRAW_SLACK; + let device = transform + .transform_rect_bbox(points.inflate(grow, grow)) + .inflate(2.0, 2.0); + paints_canvas(device, width, height) +} + +/// PdfCraft patch: whether a `rect` drawn with `transform` can paint a pixel of a `width` ร— +/// `height` canvas. Used for images: decoded data can only shrink against the stream-size box +/// (subsampled `scale_factors`), so the box over-approximates what is drawn and skipping it +/// never changes a pixel, while a tile of an image-heavy page no longer decodes, resamples and +/// rasterizes every image on the page. +fn may_paint_rect(rect: Rect, transform: Affine, width: u16, height: u16) -> bool { + let device = transform + .transform_rect_bbox(rect.inflate(DRAW_SLACK, DRAW_SLACK)) + .inflate(2.0, 2.0); + paints_canvas(device, width, height) +} + +/// PdfCraft patch: the whole-pixel device rectangle of a shrunk, axis-aligned image, as +/// `(width, height, x0, y0)`. `transform` maps source pixels to device pixels and `planned` is the +/// size [`image_resampling_size`] chose; `None` (draw as before) for rotated, skewed or flipped +/// images, non-finite geometry, or a snapped size that strays more than a pixel from `planned` +/// (so the memory budget that size passed still holds). +fn snap_minified_image( + transform: &Affine, + source: (u32, u32), + planned: (u32, u32), +) -> Option<(u32, u32, f64, f64)> { + let [a, b, c, d, e, f] = transform.as_coeffs(); + let tiny = 1e-4 * a.abs().max(d.abs()); + if !(a > 0.0 && d > 0.0 && b.abs() <= tiny && c.abs() <= tiny) { + return None; + } + let edge = |origin: f64, scale: f64, side: u32| { + let (lo, hi) = (origin.round(), (origin + scale * f64::from(side)).round()); + (hi - lo >= 1.0 && lo.abs() < 1e6 && hi.abs() < 1e6).then_some((lo, hi)) + }; + let (x0, x1) = edge(e, a, source.0)?; + let (y0, y1) = edge(f, d, source.1)?; + // Bounded by the 1e6 check above, so the casts cannot saturate. + let (w, h) = ((x1 - x0) as u32, (y1 - y0) as u32); + (w.abs_diff(planned.0) <= 1 && h.abs_diff(planned.1) <= 1).then_some((w, h, x0, y0)) +} + +pub(crate) struct Renderer { + pub(crate) ctx: RenderContext, + /// PdfCraft patch: when set, solid text and line-art paint colours are remapped + /// while rendering (the "reading colours" view setting). + color_replacement: Option, + pub(crate) inside_pattern: bool, + /// Soft masks drawn during this render. PdfCraft note: it belongs to one `Renderer` (one + /// render, every nested renderer starting empty) and so to one `preview` setting: a mask + /// drawn for a preview, which may leave out tiny images, never reaches a final render. + pub(crate) soft_mask_cache: HashMap, + pub(crate) outline_cache: Rc>>>, + pub(crate) cur_mask: Option, + pub(crate) in_type3_glyph: bool, + pub(crate) scaler: Scaler, + /// PdfCraft patch: low-detail preview render (see `InterpreterSettings::preview`). + pub(crate) preview: bool, + // TODO: Remove this once vello_cpu bug with non-transparent images is fixed + image_transparency_stack: Vec, +} + +#[derive(Clone, Copy)] +enum ImagePixelFormat { + Luma, + Rgb, + Rgba, +} + +impl Renderer { + pub(crate) fn new( + width: u16, + height: u16, + settings: RenderSettings, + cache: &RenderCache<'_>, + color_replacement: Option, + preview: bool, + ) -> Self { + Self { + ctx: RenderContext::new_with(width, height, settings), + color_replacement, + inside_pattern: false, + soft_mask_cache: HashMap::default(), + outline_cache: cache.outline_cache.clone(), + cur_mask: None, + in_type3_glyph: false, + scaler: Scaler::new(ResamplingFunction::CatmullRom), + preview, + image_transparency_stack: Vec::new(), + } + } + + /// PdfCraft patch: returns the stroke's reach for [`may_paint_canvas`]. + fn set_stroke_properties( + &mut self, + stroke_props: &StrokeProps, + is_text: bool, + path: &BezPath, + ) -> f64 { + let threshold = if is_text { 0.25 } else { 1.0 }; + + // Best-effort attempt to ensure a line width of at least 1.0, as required by the PDF + // specification. If we are stroking text, we reduce the threshold as it will otherwise + // lead to very bold-looking text at low resolutions. + let min_factor = max_factor(self.ctx.transform()); + let mut line_width = stroke_props.line_width.max(0.01); + let transformed_width = line_width * min_factor; + + // Only enforce line width if not inside of pattern or type 3 glyph. + if transformed_width < threshold && !self.inside_pattern && !self.in_type3_glyph { + line_width /= transformed_width; + line_width *= threshold; + } + + // PdfCraft patch: a stroke far wider than the canvas looks the same as one a few canvases + // wide, but its geometry grows with the width: a fuzzed `/LW 9223372036854775807` + // allocated 10 GB in stroke expansion. + let widest = 4.0 * (f32::from(self.ctx.width()) + f32::from(self.ctx.height())); + if min_factor.is_finite() && min_factor > 0.0 && line_width * min_factor > widest { + line_width = widest / min_factor; + } + + let stroke = kurbo::Stroke { + width: line_width as f64, + join: stroke_props.line_join, + miter_limit: stroke_props.miter_limit as f64, + start_cap: stroke_props.line_cap, + end_cap: stroke_props.line_cap, + // PdfCraft patch: see `usable_dash_pattern`. + dash_pattern: usable_dash_pattern(path, &stroke_props.dash_array).into(), + dash_offset: if stroke_props.dash_offset.is_finite() { + stroke_props.dash_offset as f64 + } else { + 0.0 + }, + }; + + // A miter limit that isn't finite leaves miters unbounded: never skipped. + let miter = stroke.miter_limit.abs(); + let reach = stroke.width / 2.0 + * if miter.is_finite() { + miter.max(std::f64::consts::SQRT_2) + } else { + f64::INFINITY + }; + self.ctx.set_stroke(stroke); + reach + } + + fn draw_image_with_alpha_mask(&mut self, image_data: ImageData, mut alpha_data: LumaData) { + // PdfCraft patch: mismatched masks need a dummy RGB staging image. Validate and + // reserve it before creating/pushing any mask, so failure skips the whole image. + let Some(len) = source_byte_len(alpha_data.width, alpha_data.height, 3, MAX_IMAGE_PIXELS) + else { + log::warn!( + "Image alpha-mask staging dimensions exceeded the buffer limit; image skipped" + ); + return; + }; + let Some(mut dummy_rgb) = image_buffer(len) else { + log::warn!("Image alpha-mask staging buffer could not be allocated; image skipped"); + return; + }; + dummy_rgb.resize(len, 0); + let mask = { + let transform = *self.ctx.transform() + * Affine::scale_non_uniform( + image_data.width() as f64 / alpha_data.width as f64, + image_data.height() as f64 / alpha_data.height as f64, + ); + // PdfCraft patch (#1649): a mask that differs in size from its image carries the + // shape on its own (Word saves text as a tiny black image with a glyph-bitmap mask). + // Shrunk by nearest-neighbour it keeps one sample in N and drops every stroke thinner + // than N mask pixels, so a shrunk mask is filtered whatever its `/Interpolate`, which + // only asks for smoothing when the mask is enlarged. + let (x, y) = x_y_advances(&transform); + if x.length() < 1.0 || y.length() < 1.0 { + alpha_data.interpolate = true; + } + let mut renderer = Self { + ctx: RenderContext::new_with( + self.ctx.width(), + self.ctx.height(), + derive_settings(self.ctx.render_settings()), + ), + // PdfCraft patch: this renders an image's alpha mask, which must keep its + // original values, so it is never remapped by the reading-colours setting. + color_replacement: None, + inside_pattern: false, + soft_mask_cache: HashMap::default(), + outline_cache: self.outline_cache.clone(), + cur_mask: None, + in_type3_glyph: false, + scaler: self.scaler, + preview: self.preview, + image_transparency_stack: Vec::new(), + }; + let mut mask_pix = Pixmap::new(self.ctx.width(), self.ctx.height()); + let rgb_data = ImageData::Rgb(RgbData { + data: dummy_rgb, + width: alpha_data.width, + height: alpha_data.height, + interpolate: alpha_data.interpolate, + scale_factors: alpha_data.scale_factors, + }); + renderer.ctx.set_transform(transform); + // Note that there is a circle between `draw_image` and `draw_image_with_alpha_mask`, + // but `draw_image_with_alpha_mask` is only called if the dimensions or interpolate + // values between alpha_data and rgb_data don't match, which they do here. + renderer.draw_image(rgb_data, Some(alpha_data)); + renderer.ctx.flush(); + let mut resources = vello_cpu::Resources::default(); + renderer.ctx.render_to_pixmap(&mut resources, &mut mask_pix); + Mask::new_alpha(&mask_pix) + }; + + self.ctx.push_mask_layer(mask); + self.image_transparency_stack.push(true); + self.draw_image(image_data, None); + self.image_transparency_stack.pop(); + self.ctx.pop_layer(); + } + + fn resize_image_data( + &self, + data: Vec, + src_width: u32, + src_height: u32, + new_width: u32, + new_height: u32, + pixel_format: ImagePixelFormat, + interpolate: bool, + ) -> (Vec, u32, u32) { + // PdfCraft patch (#624): `/Interpolate` governs the pre-minification pass too. Using + // Catmull-Rom here for an image that did not request interpolation blends hard edges + // before ImageQuality::Low gets a chance to sample it. Keep the pre-resize (and its + // bounded memory/performance behaviour), but use nearest-neighbour when interpolation + // is disabled. PDF's default for `/Interpolate` is false. + let nearest = Scaler::new(ResamplingFunction::Nearest); + let scaler = if interpolate { &self.scaler } else { &nearest }; + let resized = match pixel_format { + ImagePixelFormat::Luma => self.resize_image_data_impl::<1>( + scaler, + interpolate, + data, + src_width, + src_height, + new_width, + new_height, + |scaler, source_size, target_size| { + scaler.plan_planar_resampling(source_size, target_size) + }, + ), + ImagePixelFormat::Rgb => self.resize_image_data_impl::<3>( + scaler, + interpolate, + data, + src_width, + src_height, + new_width, + new_height, + |scaler, source_size, target_size| { + scaler.plan_rgb_resampling(source_size, target_size) + }, + ), + ImagePixelFormat::Rgba => self.resize_image_data_impl::<4>( + scaler, + interpolate, + data, + src_width, + src_height, + new_width, + new_height, + |scaler, source_size, target_size| { + scaler.plan_rgba_resampling(source_size, target_size, true) + }, + ), + }; + match resized { + Ok(out) => (out, new_width, new_height), + Err(original) => { + log::warn!( + "Image resampling failed or exceeded its buffer limit; falling back to source data" + ); + (original, src_width, src_height) + } + } + } + + fn resize_image_data_impl( + &self, + scaler: &Scaler, + interpolate: bool, + data: Vec, + src_width: u32, + src_height: u32, + new_width: u32, + new_height: u32, + plan: impl FnOnce( + &Scaler, + ImageSize, + ImageSize, + ) -> Result>, PicScaleError>, + ) -> Result, Vec> { + // PdfCraft patch: validate before constructing a plan or allocating its output/scratch. + // Returning the owned input preserves both its pixels and dimensions on any failure. + let resized = (|| { + let len = resampling_byte_len( + (src_width, src_height), + (new_width, new_height), + N, + data.len(), + MAX_IMAGE_PIXELS, + interpolate, + )?; + let source_size = ImageSize::new(src_width as usize, src_height as usize); + let target_size = ImageSize::new(new_width as usize, new_height as usize); + let src = ImageStore::::from_slice(&data, source_size.width, source_size.height) + .ok()?; + let plan = plan(scaler, source_size, target_size).ok()?; + let scratch_len = plan.scratch_size(); + // Nearest-neighbour needs no scratch, so any scratch it reports is refused. + let scratch_bound = if interpolate { + resampling_scratch_byte_len( + (src_width, src_height), + (new_width, new_height), + N, + MAX_IMAGE_PIXELS, + )? + } else { + 0 + }; + if scratch_len > scratch_bound { + return None; + } + let mut out = image_buffer(len)?; + out.resize(len, 0); + let mut dst = + ImageStoreMut::::from_slice(&mut out, target_size.width, target_size.height) + .ok()?; + let mut scratch = image_buffer(scratch_len)?; + scratch.resize(scratch_len, 0); + plan.resample_with_scratch(&src, &mut dst, &mut scratch) + .ok()?; + Some(out) + })(); + match resized { + Some(out) => Ok(out), + None => Err(data), + } + } + + fn draw_image(&mut self, image_data: ImageData, alpha_data: Option) { + let cur_transform = *self.ctx.transform(); + let (x_scale, y_scale) = { + let (x, y) = x_y_advances(&cur_transform); + (x.length() as f32, y.length() as f32) + }; + // PdfCraft patch: do not send non-finite or collapsed geometry to the rasterizer. + if !cur_transform.as_coeffs().iter().all(|v| v.is_finite()) + || !x_scale.is_finite() + || !y_scale.is_finite() + || x_scale <= 0.0 + || y_scale <= 0.0 + { + log::warn!("Image has an invalid transform and was skipped"); + return; + } + let (source_width, source_height) = (image_data.width(), image_data.height()); + let channels = match &image_data { + ImageData::Luma(_) => 1, + ImageData::Rgb(_) => 3, + }; + let data_len = match &image_data { + ImageData::Luma(a) => a.data.len(), + ImageData::Rgb(a) => a.data.len(), + }; + if source_byte_len(source_width, source_height, channels, MAX_IMAGE_PIXELS) + != Some(data_len) + { + log::warn!("Image has invalid or over-limit source dimensions/data and was skipped"); + return; + } + let interpolate = image_data.interpolate(); + if let Some(a) = &alpha_data { + if source_byte_len(a.width, a.height, 1, MAX_IMAGE_PIXELS) != Some(a.data.len()) { + log::warn!("Image has invalid or over-limit alpha dimensions/data and was skipped"); + return; + } + if a.width != source_width || a.height != source_height || a.interpolate != interpolate + { + if let Some(alpha) = alpha_data { + self.draw_image_with_alpha_mask(image_data, alpha); + } + return; + } + } + let mut quality = if interpolate { + ImageQuality::Medium + } else { + ImageQuality::Low + }; + let has_alpha = alpha_data.is_some(); + let mut may_have_transparency = has_alpha || self.force_images_may_have_transparency(); + let requested_resize = x_scale < 1.0 || y_scale < 1.0; + let (new_width, new_height) = match image_resampling_size( + source_width, + source_height, + channels, + data_len, + x_scale, + y_scale, + MAX_IMAGE_PIXELS, + interpolate, + ) { + Some(target) => target, + None => { + if image_byte_len(source_width, source_height, 4, MAX_IMAGE_PIXELS).is_none() { + log::warn!( + "Image resampling exceeded its buffer limit and the original resolution cannot fit the backend; image skipped" + ); + return; + } + log::warn!( + "Image resampling exceeded its buffer limit; retaining the original resolution" + ); + (source_width, source_height) + } + }; + // PdfCraft patch (#624): High is bicubic and would blend the nearest pre-resize of an image + // that did not request interpolation, so only interpolated glyph images are promoted. + if requested_resize && self.in_type3_glyph && interpolate { + quality = ImageQuality::High; + } + // PdfCraft patch: an axis-aligned, interpolated image that is shrunk lands on whole device + // pixels. It is shrunk once to exactly its snapped device rectangle and drawn 1:1 at the + // snapped corner. Otherwise the shrunk raster (rounded up to a whole size) is sampled a + // second time at a fractional scale and offset, which blurs scans and photos. + let snapped = if interpolate + && requested_resize + && !self.in_type3_glyph + && (new_width, new_height) != (source_width, source_height) + { + snap_minified_image(&cur_transform, (source_width, source_height), (new_width, new_height)) + } else { + None + }; + let (new_width, new_height) = snapped.map_or((new_width, new_height), |s| (s.0, s.1)); + let needs_resize = (new_width, new_height) != (source_width, source_height); + + // PdfCraft patch (#624): a minified image with a soft mask that is not interpolated is + // nearest-sampled straight into premultiplied target RGBA. That skips the full-resolution + // RGBA copy below and the premultiply pass after it. + let direct = match &alpha_data { + Some(alpha) if needs_resize && !interpolate => nearest_premultiplied_rgba( + &image_data, + &alpha.data, + (source_width, source_height), + (new_width, new_height), + ), + _ => None, + }; + let premultiplied = direct.is_some(); + // Preserve the single-channel/RGB fast paths. With alpha, expand directly to RGBA; + // every source and alpha length was validated before zipping or allocating. + let (data, format) = match (direct, image_data, alpha_data) { + (Some(rgba), _, _) => (rgba, ImagePixelFormat::Rgba), + (None, ImageData::Luma(luma), None) => (luma.data, ImagePixelFormat::Luma), + (None, ImageData::Rgb(rgb), None) => (rgb.data, ImagePixelFormat::Rgb), + (None, image, Some(alpha)) => { + let Some(len) = source_byte_len(source_width, source_height, 4, MAX_IMAGE_PIXELS) + else { + return; + }; + let Some(mut rgba) = image_buffer(len) else { + log::warn!( + "Image RGBA buffer could not be allocated and the image was skipped" + ); + return; + }; + match image { + ImageData::Luma(luma) => { + rgba.extend( + luma.data + .iter() + .zip(alpha.data) + .flat_map(|(g, a)| [*g, *g, *g, a]), + ); + } + ImageData::Rgb(rgb) => { + rgba.extend( + rgb.data + .as_chunks::<3>() + .0 + .iter() + .zip(alpha.data) + .flat_map(|([r, g, b], a)| [*r, *g, *b, a]), + ); + } + } + (rgba, ImagePixelFormat::Rgba) + } + }; + let (data, mut img_width, mut img_height) = if premultiplied { + (data, new_width, new_height) + } else if needs_resize { + self.resize_image_data( + data, + source_width, + source_height, + new_width, + new_height, + format, + interpolate, + ) + } else { + (data, source_width, source_height) + }; + if image_byte_len(img_width, img_height, 4, MAX_IMAGE_PIXELS).is_none() { + log::warn!( + "Image resampling failed and the original resolution cannot fit the backend; image skipped" + ); + return; + } + let mut additional_transform = Affine::scale_non_uniform( + source_width as f64 / img_width as f64, + source_height as f64 / img_height as f64, + ); + let mut rgba_data = if matches!(format, ImagePixelFormat::Rgba) { + data + } else { + let Some(len) = image_byte_len(img_width, img_height, 4, MAX_IMAGE_PIXELS) else { + return; + }; + let Some(mut rgba) = image_buffer(len) else { + log::warn!("Image RGBA buffer could not be allocated and the image was skipped"); + return; + }; + match format { + ImagePixelFormat::Luma => rgba.extend(data.iter().flat_map(|g| [*g, *g, *g, 255])), + ImagePixelFormat::Rgb => rgba.extend( + data.as_chunks::<3>() + .0 + .iter() + .flat_map(|[r, g, b]| [*r, *g, *b, 255]), + ), + ImagePixelFormat::Rgba => {} // Handled above. + } + rgba + }; + if has_alpha && !premultiplied { + let (chunks, _) = rgba_data.as_chunks_mut::<4>(); + for chunk in chunks { + let [r, g, b, a] = *chunk; + *chunk = AlphaColor::from_rgba8(r, g, b, a) + .premultiply() + .to_rgba8() + .to_u8_array(); + } + } + // Type 3 glyphs get a transparent two-pixel frame for interpolation. Check the padded + // RGBA allocation and u16 dimensions too, before narrowing or growing either side. + if self.in_type3_glyph { + let (Some(width), Some(height)) = (img_width.checked_add(4), img_height.checked_add(4)) + else { + return; + }; + let Some(len) = image_byte_len(width, height, 4, MAX_IMAGE_PIXELS) else { + log::warn!( + "Image glyph padding exceeded its buffer limit and the image was skipped" + ); + return; + }; + let Some(mut padded) = image_buffer(len) else { + log::warn!("Image glyph padding could not be allocated and the image was skipped"); + return; + }; + let row_len = width as usize * 4; // Checked RGBA length above bounds every row. + padded.resize(row_len * 2, 0); + for row in rgba_data.chunks_exact(img_width as usize * 4) { + padded.extend([0; 8]); + padded.extend_from_slice(row); + padded.extend([0; 8]); + } + padded.resize(len, 0); + (img_width, img_height) = (width, height); + additional_transform *= Affine::translate((-2.0, -2.0)); + may_have_transparency = true; + rgba_data = padded; + } + let pixmap = Pixmap::from_parts_with_opacity( + bytemuck::cast_vec(rgba_data), + img_width as u16, + img_height as u16, + may_have_transparency, + ); + let draw_transform = match snapped { + Some((_, _, x0, y0)) => Affine::translate((x0, y0)), + None => cur_transform * additional_transform, + }; + self.draw_pixmap(Arc::new(pixmap), quality, draw_transform); + } + + fn push_clip_path_inner(&mut self, clip_path: &BezPath, fill: FillRule) { + let old_transform = *self.ctx.transform(); + + self.ctx.set_fill_rule(convert_fill_rule(fill)); + self.ctx.set_transform(Affine::IDENTITY); + self.ctx.push_clip_path(clip_path); + + self.ctx.set_transform(old_transform); + } + + fn draw_pixmap(&mut self, pixmap: Arc, quality: ImageQuality, transform: Affine) { + let (width, height) = (pixmap.width(), pixmap.height()); + let image = Image { + image: ImageSource::Pixmap(pixmap), + sampler: ImageSampler { + x_extend: peniko::Extend::Pad, + y_extend: peniko::Extend::Pad, + quality, + alpha: 1.0, + }, + }; + + self.ctx.set_transform(transform); + self.ctx.set_paint(image); + self.ctx + .fill_rect(&Rect::new(0.0, 0.0, width as f64, height as f64)); + } + + /// PdfCraft patch: remaps a solid paint colour through `color_replacement`, if one is + /// set. `Paint::Pattern` (images, shadings, tiling patterns) always passes through + /// untouched. + fn replaced<'a>(&self, paint: &Paint<'a>, is_text: bool) -> Paint<'a> { + let Some(replacement) = self.color_replacement else { + return paint.clone(); + }; + match paint { + Paint::Color(c) => { + let mapped = replacement.map(c.to_rgba().to_rgba8(), is_text); + let rgba = hayro_interpret::color::AlphaColor::from_rgba8( + mapped[0], mapped[1], mapped[2], mapped[3], + ); + Paint::Color(Color::from_rgba(rgba)) + } + other => other.clone(), + } + } + + #[must_use] + fn set_paint(&mut self, paint: &Paint<'_>, path: &BezPath, is_stroke: bool) -> Option { + let mut paint_transform = Affine::IDENTITY; + let mut clip_path = None; + + let paint: PaintType = match paint.clone() { + Paint::Color(c) => { + let c = c.to_rgba().to_rgba8(); + AlphaColor::from_rgba8(c[0], c[1], c[2], c[3]).into() + } + Paint::Pattern(p) => { + let path_transform = self.ctx.transform(); + + match *p { + Pattern::Shading(s) => { + clip_path = s.shading.clip_path.clone(); + let mut bbox = (*path_transform * path.clone()).bounding_box(); + + if is_stroke { + // Try to account for stroke in bbox. + let (a1, a2) = x_y_advances(path_transform); + let factor = a1.length().max(a2.length()) * self.ctx.stroke().width; + bbox = bbox.inflate(factor, factor); + } + + bbox = bbox.intersect(Rect::new( + 0.0, + 0.0, + self.ctx.width() as f64, + self.ctx.height() as f64, + )); + + // PdfCraft patch: mesh shadings are sampled only inside `bbox` (and a + // pixel around it: texels at a fractional edge look one pixel further). + let encoded = s.encode_within(Some(bbox.inflate(1.0, 1.0))); + + // PdfCraft patch (backport of hayro 0.8): axial and radial shadings + // become native gradients instead of a texture sampled pixel by pixel. + // Upstream also shifts by half a pixel for vello_cpu 0.3; 0.0.8 already + // evaluates gradients at pixel centres, like the texture below. + if let Some(native) = + encoded.as_svg_gradient(bbox, NATIVE_GRADIENT_TOLERANCE) + { + self.ctx + .set_paint_transform(path_transform.inverse() * native.transform); + self.ctx.set_paint(native_gradient(native)); + return clip_path; + } + + let (image, width, height, transform, may_have_transparency) = + render_shading_texture(bbox, &encoded); + let may_have_transparency = + may_have_transparency || self.force_images_may_have_transparency(); + paint_transform = path_transform.inverse() * transform; + + let pixmap = Pixmap::from_parts_with_opacity( + image, + width as u16, + height as u16, + may_have_transparency, + ); + + let image = Image { + image: ImageSource::Pixmap(Arc::new(pixmap)), + sampler: ImageSampler { + x_extend: peniko::Extend::Repeat, + y_extend: peniko::Extend::Repeat, + quality: ImageQuality::Medium, + alpha: 1.0, + }, + }; + + PaintType::Image(image) + } + Pattern::Tiling(t) => { + const MAX_PIXMAP_SIZE: f32 = 3000.0; + // TODO: Raise this limit and perform downsampling if reached + // (see pdftc_100k_0138.pdf). + const MIN_PIXMAP_SIZE: f32 = 1.0; + + let bbox = t.bbox; + let max_x_scale = MAX_PIXMAP_SIZE / bbox.width() as f32; + let min_x_scale = MIN_PIXMAP_SIZE / bbox.width() as f32; + let max_y_scale = MAX_PIXMAP_SIZE / bbox.height() as f32; + let min_y_scale = MIN_PIXMAP_SIZE / bbox.height() as f32; + + let (mut xs, mut ys) = { + let (x, y) = x_y_advances(&(t.matrix)); + (x.length() as f32, y.length() as f32) + }; + xs = xs.max(min_x_scale).min(max_x_scale); + ys = ys.max(min_y_scale).min(max_y_scale); + // PdfCraft patch: the cell pixmap is `step ร— scale` pixels, which only the + // u16 cast below bounded (a fuzzed /XStep far beyond /BBox gave 65535ยฒ cells). + xs = tiling_cell_scale(xs, t.x_step, MAX_PIXMAP_SIZE); + ys = tiling_cell_scale(ys, t.y_step, MAX_PIXMAP_SIZE); + + let x_step = xs * t.x_step; + let y_step = ys * t.y_step; + + let scaled_width = bbox.width() as f32 * xs; + let scaled_height = bbox.height() as f32 * ys; + let pix_width = x_step.abs().round() as u16; + let pix_height = y_step.abs().round() as u16; + + let mut renderer = Self { + ctx: RenderContext::new_with( + pix_width, + pix_height, + derive_settings(self.ctx.render_settings()), + ), + // PdfCraft patch: a pattern cell is painted as one artwork, so its + // content is never remapped by the reading-colours setting. + color_replacement: None, + cur_mask: None, + inside_pattern: true, + soft_mask_cache: HashMap::default(), + outline_cache: self.outline_cache.clone(), + in_type3_glyph: false, + scaler: self.scaler, + preview: self.preview, + image_transparency_stack: Vec::new(), + }; + let mut initial_transform = Affine::scale_non_uniform(xs as f64, ys as f64) + * Affine::translate((-bbox.x0, -bbox.y0)); + t.interpret(&mut renderer, initial_transform, is_stroke); + let mut pix = Pixmap::new(pix_width, pix_height); + renderer.ctx.flush(); + let mut resources = vello_cpu::Resources::default(); + renderer.ctx.render_to_pixmap(&mut resources, &mut pix); + + // TODO: Fix these + if x_step < 0.0 { + initial_transform *= + Affine::new([-1.0, 0.0, 0.0, 1.0, scaled_width as f64, 0.0]); + } + + if y_step < 0.0 { + initial_transform *= + Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, scaled_height as f64]); + } + + paint_transform = + path_transform.inverse() * t.matrix * initial_transform.inverse(); + + let image = Image { + image: ImageSource::Pixmap(Arc::new(pix)), + sampler: ImageSampler { + x_extend: peniko::Extend::Repeat, + y_extend: peniko::Extend::Repeat, + quality: ImageQuality::Medium, + alpha: 1.0, + }, + }; + + PaintType::Image(image) + } + } + } + }; + + self.ctx.set_paint_transform(paint_transform); + self.ctx.set_paint(paint); + + clip_path + } + + fn stroke_path( + &mut self, + path: &BezPath, + transform: Affine, + paint: &Paint<'_>, + stroke_props: &StrokeProps, + is_text: bool, + ) { + self.ctx.set_transform(transform); + let reach = self.set_stroke_properties(stroke_props, is_text, path); + // PdfCraft patch: see `may_paint_canvas`. + if !may_paint_canvas(path, transform, reach, self.ctx.width(), self.ctx.height()) { + return; + } + + // PdfCraft patch: apply the reading-colours mapping before painting. + let paint = self.replaced(paint, is_text); + let clip_path = self.set_paint(&paint, path, true); + if let Some(clip_path) = clip_path.as_ref() { + self.push_clip_path_inner(clip_path, FillRule::NonZero); + } + self.ctx.stroke_path(path); + if clip_path.is_some() { + self.ctx.pop_clip_path(); + } + } + + fn fill_path( + &mut self, + path: &BezPath, + transform: Affine, + paint: &Paint<'_>, + fill_rule: FillRule, + // PdfCraft patch: distinguishes glyph fills from plain line art for the + // reading-colours mapping. + is_text: bool, + ) { + self.ctx.set_fill_rule(convert_fill_rule(fill_rule)); + self.ctx.set_transform(transform); + // PdfCraft patch: see `may_paint_canvas`. + if !may_paint_canvas(path, transform, 0.0, self.ctx.width(), self.ctx.height()) { + return; + } + + // PdfCraft patch: apply the reading-colours mapping before painting. + let paint = self.replaced(paint, is_text); + let clip_path = self.set_paint(&paint, path, false); + if let Some(clip_path) = clip_path.as_ref() { + self.push_clip_path_inner(clip_path, fill_rule); + } + + self.ctx.fill_path(path); + + if clip_path.is_some() { + self.ctx.pop_clip_path(); + } + } + + fn fill_glyph<'a>( + &mut self, + glyph: &Glyph<'a>, + transform: Affine, + glyph_transform: Affine, + paint: &Paint<'a>, + ) { + match glyph { + Glyph::Outline(o) => { + let base_outline = self.cached_outline(o); + + self.fill_path( + base_outline.as_ref(), + transform * glyph_transform, + paint, + FillRule::NonZero, + // PdfCraft patch: glyph outlines are text for the reading-colours mapping. + true, + ); + } + Glyph::Type3(s) => { + // PdfCraft patch: a type 3 glyph's content stream keeps painting as line art + // (its internal ops reach `fill_path`/`stroke_path` with `is_text: false`), so + // reading colours remap it only when line-art replacement is on. + self.in_type3_glyph = true; + s.interpret(self, transform, glyph_transform, paint); + self.in_type3_glyph = false; + } + } + } + + fn stroke_glyph<'a>( + &mut self, + glyph: &Glyph<'a>, + transform: Affine, + glyph_transform: Affine, + paint: &Paint<'a>, + stroke_props: &StrokeProps, + ) { + match glyph { + Glyph::Outline(o) => { + let base_outline = self.cached_outline(o); + + self.stroke_path( + &(glyph_transform * base_outline.as_ref().clone()), + transform, + paint, + stroke_props, + true, + ); + } + Glyph::Type3(s) => { + s.interpret(self, transform, glyph_transform, paint); + } + } + } + + fn cached_outline(&self, glyph: &hayro_interpret::font::OutlineGlyph) -> Rc { + let id = glyph.identifier().cache_key(); + + if let Some(path) = self.outline_cache.borrow().get(&id) { + return path.clone(); + } + + let path = Rc::new(glyph.outline()); + self.outline_cache.borrow_mut().insert(id, path.clone()); + path + } + + fn force_images_may_have_transparency(&self) -> bool { + self.cur_mask.is_some() + || self + .image_transparency_stack + .last() + .copied() + .unwrap_or(false) + || self.ctx.blend_mode() != peniko::BlendMode::default() + } +} + +impl<'a> Device<'a> for Renderer { + fn draw_image(&mut self, image: hayro_interpret::Image<'a, '_>, mut transform: Affine) { + // PdfCraft patch: see `may_paint_rect`. Checked before decoding: off-canvas images + // cost nothing, so a tile no longer decodes every image on its page. + if !may_paint_rect( + Rect::new(0.0, 0.0, f64::from(image.width()), f64::from(image.height())), + transform, + self.ctx.width(), + self.ctx.height(), + ) { + return; + } + self.ctx.set_paint_transform(Affine::IDENTITY); + self.ctx.set_aliasing_threshold(Some(1)); + + let target_width = (transform * Point::new(image.width() as f64, 0.0)) + .to_vec2() + .length() + .ceil() as u32; + let target_height = (transform * Point::new(0.0, image.height() as f64)) + .to_vec2() + .length() + .ceil() as u32; + + // PdfCraft patch: previews skip images that would cover only a few device pixels. + // Final renders always draw them. (Measured as axis lengths: the `target_*` above + // map a point, so they include the translation and only suit the decode hint.) + if self.preview { + let c = transform.as_coeffs(); + let tiny_w = (f64::from(image.width()) * (c[0] * c[0] + c[1] * c[1]).sqrt()).ceil() as u32; + let tiny_h = (f64::from(image.height()) * (c[2] * c[2] + c[3] * c[3]).sqrt()).ceil() as u32; + if tiny_w.saturating_mul(tiny_h) < PREVIEW_MIN_IMAGE_AREA { + return; + } + } + + match image { + hayro_interpret::Image::Stencil(s) => { + s.with_stencil( + |stencil, paint| { + transform *= Affine::scale_non_uniform( + stencil.scale_factors.0 as f64, + stencil.scale_factors.1 as f64, + ); + + // PdfCraft patch: a stencil mask is line art in disguise, so its solid + // paint goes through the reading-colours mapping (line-art gate) like + // every other solid fill; baked in raw, its ink would stay at its + // original colour and vanish into a mapped background. + let paint = &self.replaced(paint, false); + match paint { + Paint::Color(c) => { + let color = c.to_rgba().to_rgba8(); + let (rgb_bytes, alpha) = ( + stencil + .data + .iter() + .flat_map(|_| [color[0], color[1], color[2]]) + .collect::>(), + color[3], + ); + + let blend_mode = self.ctx.blend_mode(); + let push_layer = + alpha != 255 || blend_mode != peniko::BlendMode::default(); + self.ctx.set_transform(transform); + if push_layer { + self.ctx.push_layer( + None, + Some(blend_mode), + Some(alpha as f32 / 255.0), + None, + None, + ); + } + let old_rule = *self.ctx.fill_rule(); + self.ctx.set_fill_rule(Fill::NonZero); + + let rgb_data = ImageData::Rgb(RgbData { + data: rgb_bytes, + width: stencil.width, + height: stencil.height, + interpolate: stencil.interpolate, + scale_factors: stencil.scale_factors, + }); + self.draw_image(rgb_data, Some(stencil)); + + if push_layer { + self.ctx.pop_layer(); + } + + self.ctx.set_fill_rule(old_rule); + } + Paint::Pattern(_) => { + let (width, height) = (self.ctx.width(), self.ctx.height()); + let stencil_rect = Rect::new( + 0.0, + 0.0, + stencil.width as f64, + stencil.height as f64, + ); + let mask_pix = { + let rgb_bytes = ImageData::Rgb(RgbData { + data: vec![ + 255; + stencil.width as usize + * stencil.height as usize + * 3 + ], + width: stencil.width, + height: stencil.height, + interpolate: stencil.interpolate, + scale_factors: stencil.scale_factors, + }); + let mut sub_renderer = Self { + ctx: RenderContext::new_with( + width, + height, + derive_settings(self.ctx.render_settings()), + ), + // PdfCraft patch: a stencil mask must keep its original + // coverage values, so its content is never remapped by + // the reading-colours setting. + color_replacement: None, + inside_pattern: false, + soft_mask_cache: HashMap::default(), + outline_cache: self.outline_cache.clone(), + cur_mask: None, + in_type3_glyph: false, + scaler: self.scaler, + preview: self.preview, + image_transparency_stack: Vec::new(), + }; + let mut sub_pix = Pixmap::new(width, height); + sub_renderer.ctx.set_transform(transform); + sub_renderer.draw_image(rgb_bytes, Some(stencil)); + sub_renderer.ctx.flush(); + let mut resources = vello_cpu::Resources::default(); + sub_renderer + .ctx + .render_to_pixmap(&mut resources, &mut sub_pix); + sub_pix + }; + + self.ctx.push_layer( + None, + Some(self.ctx.blend_mode()), + None, + Some(Mask::new_luminance(&mask_pix)), + None, + ); + self.ctx.set_transform(transform); + + let clip_path = + self.set_paint(paint, &stencil_rect.to_path(0.1), true); + if let Some(clip_path) = clip_path.as_ref() { + self.push_clip_path_inner(clip_path, FillRule::NonZero); + } + self.ctx.fill_rect(&stencil_rect); + if clip_path.is_some() { + self.ctx.pop_clip_path(); + } + + self.ctx.pop_layer(); + } + }; + }, + Some((target_width, target_height)), + ); + } + hayro_interpret::Image::Raster(r) => { + r.with_rgba( + |image, alpha| { + let (sx, sy) = image.scale_factors(); + transform *= Affine::scale_non_uniform(sx as f64, sy as f64); + self.ctx.set_transform(transform); + self.draw_image(image, alpha); + }, + Some((target_width, target_height)), + ); + } + } + + self.ctx.set_aliasing_threshold(None); + } + + fn push_clip_path(&mut self, clip_path: &ClipPath) { + self.push_clip_path_inner(&clip_path.path, clip_path.fill); + } + + fn push_transparency_group( + &mut self, + opacity: f32, + mask: Option>, + blend_mode: BlendMode, + ) { + let settings = *self.ctx.render_settings(); + let force_images_may_have_transparency = mask.is_some() + || blend_mode != BlendMode::Normal + || self + .image_transparency_stack + .last() + .copied() + .unwrap_or(false); + self.ctx.push_layer( + None, + Some(convert_blend_mode(blend_mode)), + Some(opacity), + // TODO: Deduplicate + mask.map(|m| { + let width = self.ctx.width(); + let height = self.ctx.height(); + + self.soft_mask_cache + .entry(m.cache_key()) + .or_insert_with(|| draw_soft_mask(&m, settings, width, height, self.preview)) + .clone() + }), + None, + ); + self.image_transparency_stack + .push(force_images_may_have_transparency); + } + + fn pop_clip_path(&mut self) { + self.ctx.pop_clip_path(); + } + + fn pop_transparency_group(&mut self) { + self.image_transparency_stack.pop(); + self.ctx.pop_layer(); + } + + fn set_soft_mask(&mut self, mask: Option>) { + let settings = *self.ctx.render_settings(); + self.cur_mask = mask.map(|m| { + let width = self.ctx.width(); + let height = self.ctx.height(); + + self.soft_mask_cache + .entry(m.cache_key()) + .or_insert_with(|| draw_soft_mask(&m, settings, width, height, self.preview)) + .clone() + }); + if let Some(mask) = self.cur_mask.clone() { + self.ctx.set_mask(mask); + } else { + self.ctx.reset_mask(); + } + } + + fn draw_path( + &mut self, + path: &BezPath, + transform: Affine, + paint: &Paint<'_>, + draw_mode: &PathDrawMode, + ) { + match draw_mode { + PathDrawMode::Fill(f) => { + Self::fill_path(self, path, transform, paint, *f, false); + } + PathDrawMode::Stroke(s) => { + Self::stroke_path(self, path, transform, paint, s, false); + } + } + } + + fn draw_rect( + &mut self, + rect: &Rect, + transform: Affine, + paint: &Paint<'_>, + draw_mode: &PathDrawMode, + ) { + let path = rect.to_path(0.1); + match draw_mode { + PathDrawMode::Fill(fill_rule) => { + self.ctx.set_fill_rule(convert_fill_rule(*fill_rule)); + self.ctx.set_transform(transform); + // PdfCraft patch: see `may_paint_canvas`. + if !may_paint_canvas(&path, transform, 0.0, self.ctx.width(), self.ctx.height()) { + return; + } + + // PdfCraft patch: apply the reading-colours mapping before painting. + let paint = self.replaced(paint, false); + let clip_path = self.set_paint(&paint, &path, false); + if let Some(clip_path) = clip_path.as_ref() { + self.push_clip_path_inner(clip_path, *fill_rule); + } + + self.ctx.fill_rect(rect); + + if clip_path.is_some() { + self.ctx.pop_clip_path(); + } + } + PathDrawMode::Stroke(s) => { + Self::stroke_path(self, &path, transform, paint, s, false); + } + } + } + + fn draw_glyph( + &mut self, + glyph: &Glyph<'a>, + transform: Affine, + glyph_transform: Affine, + paint: &Paint<'a>, + draw_mode: &GlyphDrawMode, + ) { + match draw_mode { + GlyphDrawMode::Fill => { + Self::fill_glyph(self, glyph, transform, glyph_transform, paint); + } + GlyphDrawMode::Stroke(s) => { + Self::stroke_glyph(self, glyph, transform, glyph_transform, paint, s); + } + GlyphDrawMode::Invisible => { + // Don't render invisible text for visual output + } + } + } + + fn set_blend_mode(&mut self, blend_mode: BlendMode) { + self.ctx.set_blend_mode(convert_blend_mode(blend_mode)); + } +} + +/// PdfCraft patch (backport of hayro 0.8): largest per-channel error allowed when approximating +/// a shading function by gradient stops. +const NATIVE_GRADIENT_TOLERANCE: f32 = 0.01; + +/// PdfCraft patch (backport of hayro 0.8): a `vello_cpu` gradient for an approximated shading. +fn native_gradient(native: hayro_interpret::gradient::SvgGradient) -> PaintType { + use hayro_interpret::gradient::SvgGradientKind; + + let stops = native + .stops + .iter() + .map(|stop| ColorStop { + offset: stop.offset, + color: DynamicColor::from_alpha_color(AlphaColor::::new(stop.color)), + }) + .collect::>(); + + match native.kind { + SvgGradientKind::Linear { start, end } => Gradient::new_linear(start, end), + SvgGradientKind::Radial { + start_center, + start_radius, + end_center, + end_radius, + } => Gradient::new_two_point_radial(start_center, start_radius, end_center, end_radius), + } + .with_extend(peniko::Extend::Pad) + .with_stops(stops.as_slice()) + .into() +} + +// TODO: Deduplicate with hayro-svg? +fn render_shading_texture( + path_bbox: Rect, + shading_pattern: &EncodedShadingPattern, +) -> (Vec, u32, u32, Affine, bool) { + let base_width = (path_bbox.width() as f32).max(1.0); + let base_height = (path_bbox.height() as f32).max(1.0); + + let width = (base_width).ceil() as u32; + let height = (base_height).ceil() as u32; + + let (x_advance, y_advance) = x_y_advances(&shading_pattern.base_transform); + + let mut buf = vec![PremulRgba8::from_u32(0); width as usize * height as usize]; + let mut start_point = shading_pattern.base_transform + * Affine::translate((0.5, 0.5)) + * Point::new(path_bbox.x0, path_bbox.y0); + let mut may_have_transparency = false; + + for row in buf.chunks_exact_mut(width as usize) { + let mut point = start_point; + + for pixel in row { + let sample = shading_pattern.sample(point); + *pixel = AlphaColor::::new(sample).premultiply().to_rgba8(); + may_have_transparency |= pixel.a != 255; + + point += x_advance; + } + + start_point += y_advance; + } + + ( + buf, + width, + height, + Affine::translate((path_bbox.x0, path_bbox.y0)), + may_have_transparency, + ) +} + +fn draw_soft_mask(mask: &SoftMask<'_>, settings: RenderSettings, width: u16, height: u16, preview: bool) -> Mask { + let mut renderer = Renderer { + ctx: RenderContext::new_with(width, height, derive_settings(&settings)), + // PdfCraft patch: a soft mask must keep its original luminance values, so its + // content is never remapped by the reading-colours setting. + color_replacement: None, + inside_pattern: false, + cur_mask: None, + soft_mask_cache: HashMap::default(), + outline_cache: Rc::new(std::cell::RefCell::new(HashMap::new())), + in_type3_glyph: false, + scaler: Scaler::new(ResamplingFunction::CatmullRom), + preview, + image_transparency_stack: Vec::new(), + }; + + let bg_color = mask.background_color().to_rgba(); + let apply_bg = bg_color.to_rgba8() != BLACK.to_rgba8().to_u8_array(); + + if apply_bg { + renderer + .ctx + .set_paint(AlphaColor::::new(bg_color.components())); + renderer + .ctx + .fill_rect(&Rect::new(0.0, 0.0, width as f64, height as f64)); + renderer.ctx.push_layer(None, None, None, None, None); + } + + mask.interpret(&mut renderer); + + if apply_bg { + renderer.ctx.pop_layer(); + } + + let mut pix = Pixmap::new(width, height); + renderer.ctx.flush(); + let mut resources = vello_cpu::Resources::default(); + renderer.ctx.render_to_pixmap(&mut resources, &mut pix); + + let mut rendered_mask = match mask.mask_type() { + MaskType::Luminosity => Mask::new_luminance(&pix), + MaskType::Alpha => Mask::new_alpha(&pix), + }; + + if let Some(transfer_function) = mask.transfer_function() { + let mut map = Vec::new(); + + for y in 0..rendered_mask.height() { + for x in 0..rendered_mask.width() { + map.push( + (transfer_function.apply(rendered_mask.sample(x, y) as f32 / 255.0) * 255.0 + + 0.5) as u8, + ); + } + } + + rendered_mask = Mask::from_parts(map, rendered_mask.width(), rendered_mask.height()); + } + + rendered_mask +} + +pub(crate) fn max_factor(transform: &Affine) -> f32 { + let scale_skew_transform = { + let c = transform.as_coeffs(); + Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0]) + }; + + let x_advance = scale_skew_transform * Point::new(1.0, 0.0); + let y_advance = scale_skew_transform * Point::new(0.0, 1.0); + + x_advance + .to_vec2() + .length() + .max(y_advance.to_vec2().length()) as f32 +} + +pub(crate) fn x_y_advances(transform: &Affine) -> (Vec2, Vec2) { + let scale_skew_transform = { + let c = transform.as_coeffs(); + Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0]) + }; + + let x_advance = scale_skew_transform * Point::new(1.0, 0.0); + let y_advance = scale_skew_transform * Point::new(0.0, 1.0); + + ( + Vec2::new(x_advance.x, x_advance.y), + Vec2::new(y_advance.x, y_advance.y), + ) +} + +fn convert_fill_rule(fill_rule: FillRule) -> Fill { + match fill_rule { + FillRule::NonZero => Fill::NonZero, + FillRule::EvenOdd => Fill::EvenOdd, + } +} + +fn convert_blend_mode(blend_mode: BlendMode) -> peniko::BlendMode { + let mix = match blend_mode { + BlendMode::Normal => Mix::Normal, + BlendMode::Multiply => Mix::Multiply, + BlendMode::Screen => Mix::Screen, + BlendMode::Overlay => Mix::Overlay, + BlendMode::Darken => Mix::Darken, + BlendMode::Lighten => Mix::Lighten, + BlendMode::ColorDodge => Mix::ColorDodge, + BlendMode::ColorBurn => Mix::ColorBurn, + BlendMode::HardLight => Mix::HardLight, + BlendMode::SoftLight => Mix::SoftLight, + BlendMode::Difference => Mix::Difference, + BlendMode::Exclusion => Mix::Exclusion, + BlendMode::Hue => Mix::Hue, + BlendMode::Saturation => Mix::Saturation, + BlendMode::Color => Mix::Color, + BlendMode::Luminosity => Mix::Luminosity, + }; + + peniko::BlendMode::new(mix, Compose::SrcOver) +} + +/// PdfCraft patch: the scale for a tiling cell, lowered so that the cell pixmap (`|step| ร— +/// scale` pixels a side) stays within `max_pixels`. Geometry is unchanged (the pattern transform +/// is derived from the scale); an over-large step only renders at lower resolution. +pub fn tiling_cell_scale(scale: f32, step: f32, max_pixels: f32) -> f32 { + let step = step.abs(); + if step.is_finite() && step > 0.0 && scale * step > max_pixels { + max_pixels / step + } else { + scale + } +} 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อจอ—ฬฬ€ฬฬ”อ€gฬ„ฬงอ”ฬซฬซ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 00000000..1ea54310 Binary files /dev/null and b/vendor/krilla/icc/sGrey-v2-magic.icc differ diff --git a/vendor/krilla/icc/sGrey-v4.icc b/vendor/krilla/icc/sGrey-v4.icc new file mode 100644 index 00000000..2187b678 Binary files /dev/null and b/vendor/krilla/icc/sGrey-v4.icc differ diff --git a/vendor/krilla/icc/sRGB-v2-magic.icc b/vendor/krilla/icc/sRGB-v2-magic.icc new file mode 100644 index 00000000..b29f729d Binary files /dev/null and b/vendor/krilla/icc/sRGB-v2-magic.icc differ diff --git a/vendor/krilla/icc/sRGB-v4.icc b/vendor/krilla/icc/sRGB-v4.icc new file mode 100644 index 00000000..d9f3c055 Binary files /dev/null and b/vendor/krilla/icc/sRGB-v4.icc differ 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") + ); +}