chore(installer): reconcile shared crate with current main (INSTALLER-UI)

Preserve the shared PDF additions and current dependency graph while adding
the installer workspace entry. Generated UI remains identical for all apps.
This commit is contained in:
skylartaylor
2026-10-16 04:10:02 +02:00
297 changed files with 94925 additions and 21 deletions
+88
View File
@@ -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
+2
View File
@@ -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:
+48
View File
@@ -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 |
Generated
+617 -21
View File
@@ -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"
+17
View File
@@ -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-<name> = { path = "crates/<name>" }`
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" }
+27
View File
@@ -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.
+2
View File
@@ -23,6 +23,8 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
## Libraries
- [`craft-pdf`](crates/pdf/README.md): independently optional PDF parsing, interpretation, rasterization and guarded authoring. Apps retain layout, fonts, editing and export policy; writer-only consumers do not enable the shared reader.
- [`craft-atomic`](crates/atomic/README.md): Crash-safe atomic file writing and replacement with same-directory staging, directory fsync, Apple `F_FULLFSYNC` fallback, transient lock backoff, and fault injection for the Craft suite.
- [`craft-cms`](crates/cms/README.md): pure-Rust ICC v2/v4 colour management: profile parsing
and writing, transforms, rendering intents, black point compensation, soft proofing, gamut
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "craft-pdf"
description = "Shared PDF reading, rendering and optional writing for Craft apps"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[features]
default = []
read = ["dep:hayro-syntax", "dep:thiserror"]
interpret = ["read", "dep:hayro-interpret"]
raster = ["interpret", "dep:hayro", "dep:bytemuck"]
write = ["dep:krilla", "dep:thiserror"]
write-pdf = ["write", "krilla/pdf"]
write-simple-text = ["write", "krilla/simple-text"]
[dependencies]
hayro-syntax = { workspace = true, optional = true }
hayro-interpret = { workspace = true, optional = true }
hayro = { workspace = true, optional = true }
bytemuck = { workspace = true, optional = true }
krilla = { workspace = true, optional = true }
thiserror = { workspace = true, optional = true }
[dev-dependencies]
hayro-syntax = "=0.7.2"
hayro-interpret = { version = "=0.7.0", default-features = false }
hayro-cmap = "=0.1.0"
kurbo = "0.13"
ttf-parser = "=0.25.1"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3"
+217
View File
@@ -0,0 +1,217 @@
# craft-pdf
Shared PDF infrastructure with independent optional features. Applications own document
models, editing, layout, font policy, worker scheduling, output presets and printing.
No feature is enabled by default.
| Feature | Provides |
| --- | --- |
| `read` | Bounded opening and the patched Hayro syntax types |
| `interpret` | `read` plus an app-owned Device interpreter, fonts and CMaps |
| `raster` | `interpret` plus premultiplied RGBA8 rendering |
| `write` | Guarded Krilla authoring; no shared reader dependency |
| `write-pdf` | `write` plus Krilla's embedded-PDF support |
| `write-simple-text` | `write` plus Krilla's optional text shaper and `draw_text` |
## Reading and rendering
`open` accepts shared bytes without copying. `open_slice` reserves storage fallibly before
copying. Both preserve typed password errors. Their `_with_options` variants accept
`OpenOptions`: a caller-owned input limit and immutable per-document `syntax::DecodeLimits`.
The default input limit is 512 MiB; callers can preserve their own accepted-file policy.
Ordinary Flate/LZW/RunLength expansion defaults to 256 MiB per stream and can be lowered,
including during initial xref reads, object streams, repair and fallback. Known-size image
pixels and streaming page content have separate backend budgets. These controls do not bound
total parser memory or execution time.
For a bare Flate page, `Page::page_stream` materializes content under the ordinary limit,
while `Page::content_reader` decodes incrementally under the separate streaming budget.
Interpretation and rasterization use that incremental path. Lowering the ordinary limit
(even to zero) therefore does not limit streamed Flate page content or cause a truncation
warning by itself. Whole-stream fallback decoders still use the ordinary limit.
`interpret_page` guards an app-owned Device. `raster::render` and `render_into` accept explicit
background, scale, viewport, offset and optional solid-colour replacement. Output is
premultiplied RGBA8. Geometry is cropped, rotated and scaled for UserUnit by the backend.
Dimensions without an explicit viewport floor to at least one pixel per side; callers can
request rounded-up sizes for partial edge pixels. Output is limited to 8192 pixels per side
and 64,000,000 pixels total. Invalid geometry and buffer lengths fail before drawing.
`InterpreterSettings` preserves caller font resolution, annotation policy, OCG object
identities, cancellation, preview quality and warning callbacks. Preview caches remain
separate from final-quality image entries. Reading-colour presets and print bypass policy
belong to the app; the shared layer only maps the requested colours.
Diagnostics report known image failures and incomplete page or nested painting content,
including omitted streams, failed decoding and exhausted content budgets. They are not a fidelity certificate: the
backend may approximate unsupported content, does not currently emit `UnsupportedFont`,
and cached objects need not repeat previous warnings. Structural xref repair and objects
omitted by a decode limit have no separate warning; a successful open does not certify
that all objects survived. Some tolerant whole-stream decoders can return an incomplete
prefix without a failure status. Use fresh caches for checks, while retaining these limitations.
Native unwinding panics become errors; discard partial devices, buffers, parsers and caches
after backend failure. WASM aborts and process-wide allocation failure cannot be caught.
Cancellation is polled between operators, so a decoder may finish before noticing it.
## Backend provenance and consumer pins
The shared reader vendors the current patched Hayro trees from PdfCraft revision
`7437bde378f43856eada40af25b983812e615e8a`. See
[`vendor/README.craft-reader.md`](../../vendor/README.craft-reader.md) for the transfer and
local patches. Lopdf, COS editing, inspection fallback and application caches remain app-owned.
The patched 0.7 reader also carries focused compatibility changes from Hayro 0.8:
whole-dictionary resource scopes, a 64-level direct-container limit, whole-run text
fill/stroke order and byte transfer-function sampling. It retains the existing Lab
Bradford and floating-color behavior, Type 3 handling, decoding limits and streaming
completion diagnostics. Iterative page-tree traversal preserves deep valid trees and
valid siblings around cycles, with a 100,000-entry work budget that fails closed rather
than exposing a partial page list. These changes do not imply full equivalence to the 0.8 backend;
the vendor notes record the boundaries. Existing consumers remain on their immutable
revision until they explicitly adopt a new one.
Consumers pin `craft-pdf` and their required root `[patch.crates-io]` entries to the same full
`https://github.com/storytold/craft-libs` revision. Cargo ignores dependency-local patches.
Reader consumers patch `hayro-syntax`, `hayro-interpret` and `hayro-jbig2`; raster consumers
also patch `hayro`. Writer consumers patch `krilla`. A plain `write` consumer does not need
reader patches. Keep unrelated craft-ui/craft-print versions unchanged when adopting PDF.
A pinned commit must be available remotely before a fresh public checkout can resolve it.
Synthetic native and WASM contracts cover opening, caller limits, alpha, geometry, settings,
preview/final caching, diagnostics, cancellation and native panic recovery. Vendored syntax
unit tests additionally cover decoder limits and incomplete streams. App-owned regression
suites retain higher-level fidelity, editing, text and worker contracts. Real-file corpora
stay outside this repository; dependency assets and licences are in the root attribution.
## Reader verification
The Shared PDF workflow runs native and WebAssembly facade contracts, an independent
`read-only-contract` consumer, and the transferred backend tests. The independent consumer
keeps interpreter-bearing dev-dependencies out of the shipping `read` feature graph.
Run `bash scripts/test-pdf-backends.sh` from the repository root to test all four vendored
backends, the syntax doctest and the syntax-only configuration. The script uses temporary
source copies and the complete local patch set, preserving the vendored lockfiles.
Run `cargo test --manifest-path crates/pdf/read-only-contract/Cargo.toml --locked` for the
read-only consumer, then repeat with `--target wasm32-unknown-unknown` and the test runner.
These backend checks supplement the root workspace gates; excluded vendor crates are not
covered by `cargo test --workspace` alone.
## Writing
Plain `write` retains Krilla raster images without its optional text shaper. Applications
that supply shaped glyphs do not need that shaper. Adapters that call `draw_text`, such as
DeckCraft, also need `write-simple-text` when adopting this revision. `write-pdf` adds
embedded-PDF support independently of text shaping.
The writer pins Krilla 0.8.2, with a local unwind-cleanup patch documented in
[`vendor/krilla/README.craft.md`](../../vendor/krilla/README.craft.md). Successful
serialization uses the same backend version as before extraction. It provides:
- `write::configuration`: version and archival validation configuration with typed failures.
- `write::Metadata` and `write::Writer`: common metadata setup and consuming serialization;
compression, colour profiles and other serialization settings pass through unchanged.
`Writer::new_with_language` also accepts an optional document language for the catalog
and XMP metadata; empty language is treated as absent, while callers retain
selection and validation policy for nonempty tags.
- `write::font`: fallible font copying/preparation for subset embedding, with a 512 MiB limit
per font, at most 256 variation coordinates, and rejection of non-finite coordinates.
- `write::guard`: a native unwind guard for the whole app export, including drawing and
postprocessing. App errors pass through; serialization errors retain backend diagnostics.
Caught panic messages retain up to 1024 Unicode scalar values; the installed panic hook
remains responsible for logging source locations and full diagnostics.
`Writer::document_mut` and `write::backend` deliberately expose Krilla's page, resource and
painting APIs. The apps keep their document walks, pagination, font selection/licensing,
outline fallbacks and warnings, colour policy, PDF/X postprocessing, tagging semantics,
links, export presets and native editing payloads. Shared writing does not introduce a
universal document model or claim additional format conformance. Dates and branding are
provided by callers; the shared writer does not read a clock.
`Writer::finish` consumes its document even on error. When `guard` returns an internal failure,
discard any writer or drawing state captured by the closure; construct it inside the closure
where possible. Failed page/surface cleanup poisons the backend document, so retaining it
after a narrow catch cannot silently serialize a missing page. This also rejects failure
during explicit surface finalization. Other captured app state must still be discarded.
Native unwind recovery cannot catch WASM aborts or process-wide out-of-memory
failures. Font preparation bounds do not impose a total memory, output-size or time budget on
PDF serialization. Individual unsupported fonts still follow each app's existing outline
fallback and warning behavior.
Run `cargo test -p craft-pdf --no-default-features --features write`, and repeat with
`--features write-pdf` and `--all-features`. The same writer contracts run with
`--target wasm32-unknown-unknown` and a matching `wasm-bindgen-test-runner`. They exercise
metadata, page sizes, versions, reproducibility, typed validation failures, malformed fonts,
application error preservation, embedded pages, and native-only panic recovery.
## Source pinning and local development
Consumers pin both the facade and patched writer to the same full commit:
```toml
[workspace.dependencies]
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "<full-commit-id>", version = "=0.1.0", default-features = false, features = ["write"] }
[patch.crates-io]
krilla = { git = "https://github.com/storytold/craft-libs", rev = "<full-commit-id>", version = "=0.8.2" }
```
Choose `write-pdf` only when embedding PDF pages. Cargo ignores dependency workspaces'
root patches; copy the Krilla patch into the consuming workspace. A compile-time marker
rejects unpatched Krilla. Check the normal dependency graph separately from the test graph
and ensure there is one Krilla source, including uses through `krilla-svg`. This writer
migration does not change an application's reader backend or its other craft-libs pins.
The selected revision must be remotely available before fresh consumers can resolve it.
Local path replacements are development-only and should not be committed to an app.
Run `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings` and
`cargo test --workspace`, then test `craft-pdf` with `--features write` and
`--features write-pdf` (the default feature set intentionally enables no PDF operations).
Run the same writer contracts on `wasm32-unknown-unknown` with a matching
`wasm-bindgen-test-runner`. Native recovery tests are subprocess or unwind checks;
WASM tests exercise successful paths and typed input errors, not recovery from aborts.
## Unicode and font contracts
The writer embeds the font and glyph run supplied by the application. Applications
must select a font that covers the requested characters, shape text and resolve
bidirectional layout before drawing. Passing correct Unicode only in document
metadata does not preserve page text. A selectable-text export must retain the
actual glyph outlines and a matching ToUnicode map, and applications must report
unsupported font preparation rather than silently dropping characters. Existing
app-specific outline fallback remains an explicit policy choice.
`tests/unicode_writing.rs` adds opt-in, externally supplied font fixtures. It exports,
reopens and interprets real page text without putting the source text in metadata,
checks nonzero glyph IDs, embedded fonts and visible non-space outlines, and asserts
exact extraction for Latin accents, Greek, Cyrillic, Vietnamese, Arabic and Thai.
A variable-font test checks default/empty coordinates and distinct light/bold
embedded outlines while preserving extraction. These are embedding/mapping
contracts; they do not qualify Arabic joining, RTL caret behavior, shaping quality,
or lossless text editing in an application's separate COS editor.
No PDF test fonts are stored in this repository. Obtain the licensed fixtures listed in
[`tests/fonts.json`](tests/fonts.json), verify their SHA-256 hashes, place them in
one directory using the listed names, then run:
```sh
CRAFT_PDF_TEST_FONT_DIR=/absolute/path/to/licensed-fonts \
RUSTFLAGS="--cfg craft_pdf_unicode_fixtures" \
cargo test -p craft-pdf --all-features --test unicode_writing
```
Repeat with `--target wasm32-unknown-unknown` and the matching test runner for
runtime WASM coverage. Ordinary builds and tests do not require these fonts or the
fixture environment variable. With write enabled, the complete test code is
compiled even without fixtures; its two runtime tests are explicitly ignored with a
provisioning reason. Running ignored tests without fixtures fails with setup instructions.
The Shared PDF CI workflow fetches and hash-verifies these fonts outside the checkout
and enables the contracts on native and WASM. Locally, run
`python3 scripts/fetch-pdf-test-fonts.py /absolute/path/to/licensed-fonts` to provision
them. `tests/upstream_reading.rs` uses the same Inter fixture to verify TrueType
WinAnsi fallback with missing or partial ToUnicode maps, explicit Encoding and
Differences precedence, and a ToUnicode override. The fallback changes extraction
only: existing nonsymbolic glyph selection can still choose StandardEncoding glyphs
or `.notdef`. Tests pin that distinction and the upstream WinAnsi unused-code bullet
mapping; fallback text is not proof of the original author's text. Without fixtures those reader
contracts are also compiled and explicitly ignored; provisioned CI executes them.
+537
View File
@@ -0,0 +1,537 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[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",
]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "craft-pdf"
version = "0.1.0"
dependencies = [
"hayro-syntax",
"thiserror",
]
[[package]]
name = "craft-pdf-read-only-contract"
version = "0.0.0"
dependencies = [
"craft-pdf",
"wasm-bindgen-test",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[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 = [
"hayro-ccitt",
]
[[package]]
name = "hayro-jpeg2000"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1"
[[package]]
name = "hayro-syntax"
version = "0.7.2"
dependencies = [
"hayro-jbig2",
"hayro-jpeg2000",
"rustc-hash",
"smallvec",
"stringprep",
"zune-jpeg",
]
[[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.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "minicov"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
dependencies = [
"cc",
"walkdir",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[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 = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc-hash"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[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.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
[[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 = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]
name = "tokio"
version = "1.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
dependencies = [
"pin-project-lite",
]
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[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 = "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.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
dependencies = [
"js-sys",
"tokio",
"wasm-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
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",
]
[[package]]
name = "wasm-bindgen-test-shared"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f692aa943ccd88363733b77063f32cfed5bc6cbea8e6e8b251b302f881606fe"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[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.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zune-core"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
[[package]]
name = "zune-jpeg"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "craft-pdf-read-only-contract"
version = "0.0.0"
publish = false
edition = "2024"
rust-version = "1.95"
license = "MIT OR Apache-2.0"
# Keep backend dev-dependencies in the parent workspace out of these tests.
[workspace]
resolver = "3"
[dependencies]
craft-pdf = { path = "..", default-features = false, features = ["read"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "=0.3.79"
[patch.crates-io]
hayro-syntax = { path = "../../../vendor/hayro-syntax" }
[lints.rust]
unsafe_code = "forbid"
+27
View File
@@ -0,0 +1,27 @@
# Independent read-only contract
This nested workspace tests the shipping `craft-pdf/read` graph separately from the parent
workspace, whose facade dev-dependencies include an interpreter. Its sole normal dependency is
`craft-pdf` with defaults disabled and `read` enabled. The local syntax patch is required to
exercise the vendored decode-limit and incremental-Flate behavior. No interpreter, rasterizer,
writer, or image codec dependency is declared here.
The same three assertions run natively and under `wasm-bindgen-test`: valid Flate page opening
and complete incremental consumption; independent per-document ordinary stream limits including
zero and a tighter per-call limit; and the caller input limit on borrowed/shared opening. The
17-byte synthetic compressed payload expands to 512 known bytes. The only target-specific dev
dependency is the WASM test harness.
From the repository root:
```sh
cargo test --manifest-path crates/pdf/read-only-contract/Cargo.toml --locked
cargo test --manifest-path crates/pdf/read-only-contract/Cargo.toml --locked --target wasm32-unknown-unknown
cargo tree --manifest-path crates/pdf/read-only-contract/Cargo.toml --locked --edges normal,features
```
Use a matching `wasm-bindgen-test-runner` through `CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER` for
actual WASM runtime execution. The native normal dependency graph must contain only the
`craft-pdf/read` path and its syntax/std dependencies; `hayro-interpret`, `hayro`, `krilla`,
`hayro-ccitt`, `hayro-jbig2`, `hayro-jpeg2000`, `zune-jpeg`, and the syntax `images`/`unsafe` features
must be absent. Keep the fixture's own lockfile for reproducibility.
+123
View File
@@ -0,0 +1,123 @@
//! Independent runtime contract for the shipping `craft-pdf/read` dependency graph.
//! The nested workspace excludes the facade's interpreter-bearing dev-dependencies.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use craft_pdf::syntax::{
DecodeLimits,
object::{ObjectIdentifier, Stream},
};
use craft_pdf::{OpenError, OpenOptions, open, open_slice, open_slice_with_options, open_with_options};
use std::sync::Arc;
// zlib-compressed b"q Q\n" repeated 128 times: 17 encoded bytes, 512 decoded bytes.
// This fixture exercises the pure-Rust Flate path without any compression dependency.
const FLATE_CONTENT: &[u8] = &[120, 156, 43, 84, 8, 228, 42, 28, 197, 35, 22, 3, 0, 132, 234, 118, 1];
fn flate_pdf() -> Vec<u8> {
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::<Stream<'_>>(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));
}
}
+19
View File
@@ -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;
+178
View File
@@ -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<ColorReplacement>,
}
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<Diagnostics, Error> {
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<u32>,
pub diagnostics: Diagnostics,
}
impl Raster {
pub fn bytes(&self) -> &[u8] {
bytemuck::cast_slice(&self.words)
}
pub fn into_words(self) -> Vec<u32> {
self.words
}
}
pub fn render<'a>(page: &'a Page<'a>, cache: &RenderCache<'a>, settings: &InterpreterSettings, options: &Options) -> Result<Raster, Error> {
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 })
}
+105
View File
@@ -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<Vec<u8>>, password: Option<&str>) -> Result<syntax::Pdf, OpenError> {
open_with_options(bytes, password, &OpenOptions::default())
}
/// Open shared input with explicit immutable caller limits.
pub fn open_with_options(bytes: Arc<Vec<u8>>, password: Option<&str>, options: &OpenOptions) -> Result<syntax::Pdf, OpenError> {
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<syntax::Pdf, OpenError> {
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<syntax::Pdf, OpenError> {
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,
}
+179
View File
@@ -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<backend::error::KrillaError>),
#[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<T, E: From<WriteError>>(operation: impl FnOnce() -> Result<T, E>) -> Result<T, E> {
catch_unwind(AssertUnwindSafe(operation)).map_err(|payload| {
let message = payload
.downcast_ref::<String>()
.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<Archival>) -> Result<Configuration, WriteError> {
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<String>,
pub authors: Vec<String>,
pub description: Option<String>,
pub keywords: Vec<String>,
pub created: Option<backend::metadata::DateTime>,
}
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<String>) -> 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<Vec<u8>, 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<Font, WriteError> {
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)));
}
}
+363
View File
@@ -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<Vec<u8>> {
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<Vec<u8>> {
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);
}
+40
View File
@@ -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"
}
]
+120
View File
@@ -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<SoftMask<'_>>) {}
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<SoftMask<'_>>, _: 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::<craft_pdf::syntax::object::Dict<'_>>(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());
}
+139
View File
@@ -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<BezPath>,
}
impl Device<'_> for TextDevice {
fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
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<SoftMask<'_>>, _: 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<u8> {
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")
}
}
+512
View File
@@ -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<u8> {
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<u8>, 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<u8>]) -> Vec<u8> {
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<u8> {
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::<Object<'_>>().next().unwrap(),
Object::Dict(dict) => dict.get::<Object<'_>>(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<u8> {
let mut payload = b"6 0 ".to_vec();
payload.extend_from_slice(value);
let encoded = payload.iter().map(|b| format!("{b:02X}")).collect::<String>() + ">";
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::<Object<'_>>(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::<Object<'_>>(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::<i32>(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::<Name<'_>>(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<GlyphEvent>,
clips: Vec<String>,
}
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<SoftMask<'_>>) {}
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<SoftMask<'_>>, _: 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<u8>]) -> Vec<u8> {
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<u8> {
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<u8> {
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<u8> {
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}");
}
}
}
}
+271
View File
@@ -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<u8>,
}
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<u8> = 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]);
}
+157
View File
@@ -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<u32>,
}
impl Device<'_> for TextDevice {
fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
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<SoftMask<'_>>, _: 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> <FF> 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 <E9> <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"), "•");
}
}
+319
View File
@@ -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<u8> {
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::<Dict<'_>>(pdf.xref().root_id()).unwrap();
let actual = catalog.get::<PdfString<'_>>(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("<dc:language>"));
assert!(xmp.contains(&format!("<rdf:li>{language}</rdf:li>")));
} else {
assert!(!xmp.contains("<dc:language>"));
}
}
}
#[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<WriteError> 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",
"<dc:title>",
"<dc:creator>",
"<pdf:Keywords>",
"<dc:description>",
"<xmp:CreatorTool>",
] {
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",
"<dc:title>",
"<dc:creator>",
"<pdf:Keywords>",
"<dc:description>",
"<xmp:CreatorTool>",
] {
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");
}
@@ -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
@@ -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.
@@ -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.
@@ -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
@@ -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.
@@ -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/
@@ -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.
@@ -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
@@ -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.
@@ -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
@@ -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.
+177
View File
@@ -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
+7
View File
@@ -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.
+116
View File
@@ -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/
+202
View File
@@ -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.
+21
View File
@@ -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.
+57
View File
@@ -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()
+77
View File
@@ -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()
+18
View File
@@ -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[@]}"
+58
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
{"v":1}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "0c98904d2ced8e37b63ed2bd1e60f95bc0c7c774"
},
"path_in_vcs": "hayro-interpret"
}
+529
View File
@@ -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",
]
+139
View File
@@ -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 <laurenz.stampfl@gmail.com>"]
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"
+38
View File
@@ -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
+177
View File
@@ -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
+7
View File
@@ -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.
+58
View File
@@ -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)
<!-- cargo-rdme start -->
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.
<!-- cargo-rdme end -->
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
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.
+468
View File
@@ -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."
]
}
Binary file not shown.
+116
View File
@@ -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/
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+27
View File
@@ -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.
+13
View File
@@ -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
```
+122
View File
@@ -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<String> = 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, "<meta charset='utf-8' /> ").unwrap();
writeln!(extractor.text, "<!-- page {} -->", 0).unwrap();
writeln!(
extractor.text,
"<div id='page{}' style='position: relative; width: {}px; height: {}px; border: 1px black solid'>",
0,
extractor.dimensions.0,
extractor.dimensions.1
).unwrap();
interpret_page(page, &mut context, &mut extractor);
writeln!(extractor.text, "</div>").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<SoftMask<'_>>) {}
fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {}
fn push_clip_path(&mut self, _: &ClipPath) {}
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: 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,
"<div style='position: absolute; color: black; left: {}px; top: {}px; font-size: {}pt'>{}</div>",
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) {}
}
+147
View File
@@ -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<DynamicImage>);
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<SoftMask<'_>>) {}
fn draw_path(&mut self, _: &BezPath, _: Affine, _: &Paint<'_>, _: &PathDrawMode) {}
fn push_clip_path(&mut self, _: &ClipPath) {}
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: 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::<Vec<_>>();
(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::<Vec<u8>>();
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) {}
}
+279
View File
@@ -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<u128, Option<Box<dyn Any + Send + Sync>>>;
#[derive(Clone)]
pub(crate) struct Cache(Arc<Mutex<CacheMap>>);
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<T: Clone + Send + Sync + 'static>(
&self,
id: u128,
f: impl FnOnce() -> Option<T>,
) -> Option<T> {
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::<T>().cloned()),
Entry::Vacant(_) => {
drop(locked);
let val = f();
self.0.lock().unwrap().insert(
id,
val.clone()
.map(|val| Box::new(val) as Box<dyn Any + Send + Sync>),
);
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<Mutex<DecodedImageCacheInner>>,
/// 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<DecodedImageKey, Option<Box<dyn Any + Send + Sync>>>,
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<T: Clone + Send + Sync + 'static>(
&self,
key: Option<DecodedImageKey>,
budget: usize,
size_of: impl FnOnce(&T) -> usize,
f: impl FnOnce() -> Option<T>,
) -> Option<T> {
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::<T>().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::<T>())
{
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<dyn Any + Send + Sync>),
);
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<T: CacheKey, U: CacheKey> 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<T: CacheKey> CacheKey for MaybeRef<T> {
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)
}
}
File diff suppressed because it is too large Load Diff
+486
View File
@@ -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<u32> = const { core::cell::Cell::new(0) };
static CONTENT_LEFT: core::cell::Cell<usize> = 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<bool> = 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<DecodedContent<'a>> {
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<RefCell<HashMap<u128, Option<Font<'a>>>>>,
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<State<'a>>,
path: BezPath,
sub_path_start: Point,
last_point: Point,
clip: Option<FillRule>,
root_transforms: Vec<Affine>,
bbox: Vec<Rect>,
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::<Dict<'_>>(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<FillRule> {
&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<ColorSpace> {
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<TextStateFont<'a>> {
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<Rect> {
// 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
}
}
+30
View File
@@ -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,
}
}
+92
View File
@@ -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<SoftMask<'a>>);
/// 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<SoftMask<'a>>,
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<i32>) {}
/// 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<SoftMask<'_>>) {}
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<SoftMask<'_>>, _: 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) {}
}
+520
View File
@@ -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<ActiveTransferFunction>,
}
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<kurbo::Rect>) -> 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<u64> =
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<kurbo::Rect>) -> 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<ShadingFunction>,
},
Dummy,
}
impl EncodedShadingType {
pub(crate) fn eval(
&self,
pos: Point,
bg_color: AlphaColor,
color_space: &ColorSpace,
) -> Option<AlphaColor> {
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<f32> {
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,
}
}
+335
View File
@@ -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<dyn AsRef<[u8]> + Send + Sync>;
type OpenTypeFontYoke = Yoke<OTFYoke<'static>, FontData>;
type CffFontYoke = Yoke<CFFYoke<'static>, FontData>;
/// A font blob for type 1 fonts.
#[derive(Clone)]
pub(crate) struct Type1FontBlob(Arc<Type1Font>);
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<Self> {
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<CffFontYoke>);
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<Self> {
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::<Option<Vec<_>>>()?;
let yoke = Yoke::<CFFYoke<'static>, 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<GlyphId> {
// 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<GlyphId> {
self.charset()?.glyph_id(Sid::new(cid))
}
pub(crate) fn glyph_index(&self, code: u8) -> Option<GlyphId> {
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<OpenTypeFontYoke>,
cff_blob: Option<CffFontBlob>,
}
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<Self> {
// 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::<OTFYoke<'static>, 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<String, GlyphId> {
// 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::<Vec<_>>();
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<HintingInstance>,
outline_glyphs: OutlineGlyphCollection<'a>,
}
#[derive(Yokeable, Clone)]
struct CFFYoke<'a> {
font: CffFontRef<'a>,
cff: Cff<'a>,
charset: Option<Charset<'a>>,
subfonts: Vec<Subfont>,
}
+645
View File
@@ -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<u32, f32>,
encoding: CMap,
to_unicode: Option<CMap>,
widths2: HashMap<u32, [f32; 3]>,
cid_to_gid_map: CidToGIdMap,
/// PostScript name from the PDF.
postscript_name: Option<String>,
/// Font flags from the font descriptor.
font_flags: Option<FontFlags>,
/// 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<Self> {
let cmap = read_encoding(&dict.get::<Object<'_>>(ENCODING)?, cmap_resolver)?;
let horizontal = cmap.metadata().writing_mode != Some(WritingMode::Vertical);
let descendant_font = dict
.get::<Array<'_>>(DESCENDANT_FONTS)?
.iter::<Dict<'_>>()
.next()?;
let font_descriptor = descendant_font
.get::<Dict<'_>>(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::<f32>(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::<Array<'_>>(W)
.and_then(|a| read_widths(&a))
.unwrap_or_default();
let widths2 = descendant_font
.get::<Array<'_>>(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::<Name<'_>>(BASE_FONT)
.map(|n| strip_subset_prefix(n.as_str()).to_string());
// Extract font flags from descriptor
let font_flags = font_descriptor
.get::<u32>(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<GlyphId> {
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<u32> {
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<crate::font::FontData> {
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<u32> {
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<BfString> {
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<Self> {
// 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::<Stream<'_>>(FONT_FILE2)
.or_else(|| descriptor.get::<Stream<'_>>(FONT_FILE3))
// See PDFBOX-2599. Apparently, Acrobat accepts this as well.
.or_else(|| descriptor.get::<Stream<'_>>(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<u16, GlyphId>,
inverse: HashMap<GlyphId, u16>,
},
}
impl CidToGIdMap {
fn new(dict: &Dict<'_>) -> Option<Self> {
if let Some(name) = dict.get::<Name<'_>>(CID_TO_GID_MAP) {
if name.deref() == IDENTITY {
Some(Self::Identity)
} else {
None
}
} else if let Some(stream) = dict.get::<Stream<'_>>(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<HashMap<u32, f32>> {
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::<f32>() {
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<HashMap<u32, [f32; 3]>> {
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::<f32>();
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<CharacterCollection> {
let info = descendant_font.get::<Dict<'_>>(CIDSYSTEMINFO)?;
let registry = info.get::<object::String<'_>>(REGISTRY)?;
let ordering = info.get::<object::String<'_>>(ORDERING)?;
let supplement = info.get::<i32>(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<CMap> {
// 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,
}
}
File diff suppressed because it is too large Load Diff
+174
View File
@@ -0,0 +1,174 @@
// See <https://github.com/apache/pdfbox/blob/4438b8fdc67a3a9ebfb194595d0e81f88b708a37/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/MacExpertEncoding.java>
use phf::phf_map;
pub(crate) fn get(code: u8) -> Option<&'static str> {
MAC_EXPERT.get(&code).copied()
}
static MAC_EXPERT: phf::Map<u8, &'static str> = 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",
};
@@ -0,0 +1,48 @@
// See <https://github.com/apache/pdfbox/blob/4438b8fdc67a3a9ebfb194595d0e81f88b708a37/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/MacOSRomanEncoding.java.java>
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<u8> {
MAC_OS_ROMAN_INVERSE.get(name).copied()
}
static MAC_OS_ROMAN: phf::Map<u8, &'static str> = 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,
};
+432
View File
@@ -0,0 +1,432 @@
// See <https://github.com/apache/pdfbox/blob/4438b8fdc67a3a9ebfb194595d0e81f88b708a37/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/MacRomanEncoding.java>
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<u8> {
MAC_ROMAN_INVERSE.get(name).copied()
}
static MAC_ROMAN: phf::Map<u8, &'static str> = 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,
};
File diff suppressed because it is too large Load Diff
+9
View File
@@ -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;
+157
View File
@@ -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<u8, &'static str> = 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",
};
+197
View File
@@ -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<u8, &'static str> = 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",
};
+230
View File
@@ -0,0 +1,230 @@
// See <https://github.com/apache/pdfbox/blob/4438b8fdc67a3a9ebfb194595d0e81f88b708a37/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/encoding/WinAnsiEncoding.java>
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<u8, &'static str> = 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",
};
+210
View File
@@ -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<u8, &'static str> = 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",
};
+53
View File
@@ -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<HashMap<GlyphId, String>>,
string_to_glyph: RefCell<HashMap<String, GlyphId>>,
glyph_counter: Cell<u32>,
}
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<String> {
self.glyph_to_string
.borrow()
.get(&glyph)
.map(|s| s.to_string())
}
}
+689
View File
@@ -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<dyn AsRef<[u8]> + 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<Type3Glyph<'a>>),
}
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<BfString> {
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<BfString> {
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<OutlineFontData> {
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<f32> {
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<Type3<'a>>,
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<BfString> {
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<f32> {
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<Self> {
let f_type = match dict.get::<Name<'_>>(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<Self> {
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<Type1Font>),
TrueType(Rc<TrueTypeFont>),
Type0(Rc<Type0Font>),
Type3(Rc<Type3<'a>>),
}
#[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<String>,
/// The name of the font.
pub font_name: Option<String>,
/// The family of the font.
pub font_family: Option<String>,
/// 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<CharacterCollection>,
}
impl FallbackFontQuery {
pub(crate) fn new(dict: &Dict<'_>) -> Self {
let post_script_name = dict
.get::<Name<'_>>(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::<Dict<'_>>(FONT_DESC) {
data.font_name = dict
.get::<Name<'_>>(FONT_NAME)
.map(|n| strip_subset_prefix(n.as_str()).to_string());
data.font_family = descriptor
.get::<Name<'_>>(FONT_FAMILY)
.map(|n| n.as_str().to_string());
data.font_stretch = descriptor
.get::<Name<'_>>(FONT_STRETCH)
.map(|n| FontStretch::from_string(n.as_str()))
.unwrap_or(FontStretch::Normal);
data.font_weight = descriptor.get::<u32>(FONT_WEIGHT).unwrap_or(400);
if let Some(flags) = descriptor
.get::<u32>(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
/// <https://github.com/adobe-type-tools/agl-specification>
pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
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<char> {
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<CMap> {
dict.get::<Stream<'_>>(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::<Name<'_>>(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
}
+146
View File
@@ -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<String>,
/// Font weight (100-900, 400=normal, 700=bold).
pub weight: Option<u32>,
/// 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<Type1Font>),
TrueType(Rc<TrueTypeFont>),
Type0(Rc<Type0Font>),
}
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<BfString> {
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<f32> {
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<OutlineFontData> {
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(),
}),
}
}
}
+694
View File
@@ -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<f32> {
// <https://github.com/apache/pdfbox/blob/129aafe26548c1ff935af9c55cb40a996186c35f/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDSimpleFont.java#L340>
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::<Name<'_>>(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::<u32>(FONT_WEIGHT).is_some_and(|w| w >= 700)
|| lower.contains("bold")
|| lower.contains("demi");
let is_italic = descriptor
.get::<f32>(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<String, GlyphId>),
}
impl StandardFontBlob {
pub(crate) fn from_data(data: FontData, index: u32) -> Option<Self> {
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<GlyphId> {
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<GlyphId> {
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<f32> {
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<Width>,
missing_width: f32,
fallback: bool,
glyph_to_code: RefCell<HashMap<GlyphId, u8>>,
encodings: HashMap<u8, String>,
}
impl StandardKind {
pub(crate) fn new(dict: &Dict<'_>, resolver: &FontResolverFn) -> Option<Self> {
let descriptor = dict.get::<Dict<'_>>(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<Self> {
let descriptor = dict.get::<Dict<'_>>(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<GlyphId> {
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<BezPath> {
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<f32> {
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<char> {
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)?)?))
}
+496
View File
@@ -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<CMap>,
}
#[derive(Debug)]
enum Kind {
Embedded(EmbeddedKind),
Standard(StandardKind),
}
impl TrueTypeFont {
pub(crate) fn new(
dict: &Dict<'_>,
font_resolver: &FontResolverFn,
cmap_resolver: &CMapResolverFn,
) -> Option<Self> {
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<crate::font::FontData> {
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<u32> {
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<BfString> {
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<Width>,
missing_width: f32,
font_flags: Option<FontFlags>,
glyph_names: HashMap<String, GlyphId>,
encoding: Encoding,
// Only used for PDFs that mistakenly embed a
// CFF font.
cff_blob: Option<CffFontBlob>,
differences: HashMap<u8, String>,
cached_mappings: RefCell<HashMap<u8, GlyphId>>,
/// PostScript name from the PDF.
postscript_name: Option<String>,
}
impl EmbeddedKind {
fn new(dict: &Dict<'_>) -> Option<Self> {
let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
let font_flags = descriptor.get::<u32>(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::<Stream<'_>>(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::<Name<'_>>(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<Width>, f32)> {
let mut widths = Vec::new();
let first_char = dict.get::<usize>(FIRST_CHAR);
let last_char = dict.get::<usize>(LAST_CHAR);
let widths_arr = dict.get::<Array<'_>>(WIDTHS);
let missing_width = descriptor.get::<f32>(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::<Object<'_>>()) {
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<u32> {
if !s.starts_with('g') || s.len() < 2 {
return None;
}
s[1..].parse::<u32>().ok()
}
impl CacheKey for TrueTypeFont {
fn cache_key(&self) -> u128 {
self.cache_key
}
}
pub(crate) fn read_encoding(dict: &Dict<'_>) -> (Encoding, HashMap<u8, String>) {
fn get_encoding_base(dict: &Dict<'_>, name: Name<'_>) -> Encoding {
match dict.get::<Name<'_>>(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::<Dict<'_>>(ENCODING) {
if let Some(differences) = encoding_dict.get::<Array<'_>>(DIFFERENCES) {
let entries = differences.iter::<Object<'_>>();
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(),
)
}
}
+341
View File
@@ -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<CMap>);
impl Type1Font {
pub(crate) fn new(
dict: &Dict<'_>,
resolver: &FontResolverFn,
cmap_resolver: &CMapResolverFn,
) -> Option<Self> {
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<Self> {
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<f32> {
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<BfString> {
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::<Dict<'_>>(FONT_DESC)
.map(|dict| dict.contains_key(FONT_FILE3))
.unwrap_or(false)
}
fn is_type1(dict: &Dict<'_>) -> bool {
dict.get::<Dict<'_>>(FONT_DESC)
.map(|dict| dict.contains_key(FONT_FILE))
.unwrap_or(false)
}
#[derive(Debug)]
struct Type1Kind {
font: Type1FontBlob,
encoding: Encoding,
widths: Vec<Width>,
missing_width: f32,
encodings: HashMap<u8, String>,
name_to_gid: HashMap<String, GlyphId>,
standard_font: Option<StandardFont>,
}
impl Type1Kind {
fn new(dict: &Dict<'_>) -> Option<Self> {
let descriptor = dict.get::<Dict<'_>>(FONT_DESC)?;
let data = descriptor.get::<Stream<'_>>(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<String, GlyphId> = 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<GlyphId> {
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<f32> {
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<char> {
self.code_to_ps_name(code).and_then(glyph_name_to_unicode)
}
}
#[derive(Debug)]
struct CffKind {
font: CffFontBlob,
encoding: Encoding,
widths: Vec<Width>,
missing_width: f32,
encodings: HashMap<u8, String>,
name_to_gid: HashMap<String, GlyphId>,
gid_to_name: Vec<Option<String>>,
standard_font: Option<StandardFont>,
}
impl CffKind {
fn new(dict: &Dict<'_>) -> Option<Self> {
let descriptor = dict.get::<Dict<'_>>(FONT_DESC)?;
let data = descriptor.get::<Stream<'_>>(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<String, GlyphId> = 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<f32> {
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<char> {
self.code_to_ps_name(code).and_then(glyph_name_to_unicode)
}
}
+265
View File
@@ -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<Width>,
missing_width: f32,
encoding: Encoding,
encodings: HashMap<u8, String>,
dict: Dict<'a>,
char_procs: HashMap<String, Stream<'a>>,
glyph_simulator: GlyphSimulator,
font_bbox: Rect,
matrix: Affine,
to_unicode: Option<CMap>,
}
impl<'a> Type3<'a> {
pub(crate) fn new(dict: &Dict<'a>, cmap_resolver: &CMapResolverFn) -> Option<Self> {
let (encoding, encodings) = read_encoding(dict);
let (widths, missing_width) = read_widths(dict, dict)?;
let font_bbox = dict
.get::<hayro_syntax::object::Rect>(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::<Dict<'_>>(CHAR_PROCS).unwrap_or_default();
for name in dict.keys() {
if let Some(prog) = dict.get::<Stream<'_>>(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<BfString> {
// 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<SoftMask<'a>>) {
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<SoftMask<'_>>, _: 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) {}
}
+157
View File
@@ -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<FunctionType>);
impl Function {
/// Create a new function.
pub fn new(obj: &Object<'_>) -> Option<Self> {
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<Self> {
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::<u8>(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<Values> {
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<TupleVec>,
}
impl Clamper {
fn new(dict: &Dict<'_>) -> Option<Self> {
let domain = dict.get::<TupleVec>(DOMAIN)?;
let range = dict.get::<TupleVec>(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)
}

Some files were not shown because too many files have changed in this diff Show More