Compare commits
2 Commits
main
...
fix/native
| Author | SHA1 | Date |
|---|---|---|
|
|
b1fb9f8cfe | |
|
|
7f9de7f8c3 |
|
|
@ -25,7 +25,7 @@ An **asset** is any non-code material that the project includes, ships, embeds,
|
|||
- Screenshots of Adobe software exist only as private, local observation notes under `plan/acrobat/`, which is gitignored. They must **never** be committed, published, linked, shown in docs, used as a design source to trace, or placed anywhere else in the repository or its outputs.
|
||||
- The only Adobe-authored material allowed is **non-visual technical data** that is openly licensed and required for PDF compatibility. Each item needs its own entry in `ATTRIBUTION.toml` with `adobe_data = true`, and the list below is closed:
|
||||
- The CMap resources (BSD-3-Clause), bundled inside the `hayro-cmap` dependency.
|
||||
- The standard-14 font metrics and encoding tables in the PDF specification. They are data, not typefaces. They are embedded as code in hayro, and as our own generated table in `crates/fonts/src/std14_metrics.rs` (approved by the owner, 2026-10-08; that file holds the data alone, so the code reading it can change without re-attesting the table), which is what generated appearances measure with and what survives hayro being retired.
|
||||
- The standard-14 font metrics and encoding tables in the PDF specification. They are data, not typefaces. They are embedded as code in hayro, and as our own generated table in `crates/fonts/src/std14.rs` (approved by the owner, 2026-10-08), which is what generated appearances measure with and what survives hayro being retired.
|
||||
|
||||
Adding anything to this list needs the user's explicit approval.
|
||||
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
|||
| `vendor/hayro-interpret/assets/FoxitSerifItalic.pfb` | FoxitSerifItalic (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
|
||||
| `vendor/hayro-interpret/assets/FoxitSymbol.pfb` | FoxitSymbol (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
|
||||
| `vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc` | CGATS001Compat v2 micro ICC profile | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | CC0-1.0 | https://github.com/saucecontrol/Compact-ICC-Profiles | Default CMYK colour conversion (vendored hayro-interpret) |
|
||||
| `crates/fonts/src/std14_metrics.rs` | Standard-14 font advance widths and encodings (as Rust tables) | Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors | MIT OR Apache-2.0 | ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. Data only: the code that reads these tables lives in std14.rs, so that ordinary edits do not churn this file's hash. | Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree. |
|
||||
| `crates/fonts/src/std14.rs` | Standard-14 font advance widths and encodings (as Rust tables) | Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors | MIT OR Apache-2.0 | ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. | Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree. |
|
||||
| `docs/brand/artcraft-logo-white.png` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
|
||||
| `docs/brand/artcraft-logo-white.svg` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
|
||||
| `docs/brand/artcraft-logo.png` | ArtCraft brand: artcraft-logo | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "389982dcbb7f1ac29fb150c9af43f9529db6e59a091b9971f0401684847d8b53"
|
||||
sha256 = "d0a69a35403e70b94c3adfef88b41ec719f4f6ab700d351c8f82a78c53b63ef6"
|
||||
|
||||
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
|
||||
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
|
||||
|
|
@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "d8ea23644d81476e3fd35c09780ab9b3ac8c9e28bed4e14e1cbc6493bb896d91"
|
||||
sha256 = "caa23201b7f25dba9f8dc579771e13435e5e8c72a7efb9770e917a2a33003a39"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
|
||||
|
|
@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "26dd0b0d697fd40db996fc102f3c7f2320bdeb6d6af38dbdde353bd5d940baad"
|
||||
sha256 = "5ead9fccdca17810a3a98de3fa499d8a2469a1ff015c3d0e11880ca629295ea1"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/cs.tsv"
|
||||
|
|
@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "322b8cda21a2b8d157716182846619bd92bfb31263d1f8d13da7eaad365fbfd7"
|
||||
sha256 = "d48f9d68711850cae48af53f7691facee4209f7846e3eb2c064d97dad2ae2426"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pt-br.tsv"
|
||||
|
|
@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "21bf9c73b482eb987ddaf839b4a4aa8f9938d5aa10428e6d39beea254a95a0c0"
|
||||
sha256 = "6bc2edec99534d0df9b5e4a1c2e18dc32ad84c32e404c74dd437d86119b1d6ea"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/es.tsv"
|
||||
|
|
@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "fef35ae1771edf2e044e72aebb958bb4827ded57234207f96b6cb7a250fedb22"
|
||||
sha256 = "cb6c04bd36ba6ce0ebc808da2e47b65797e14d59466e93802adec4d4f5056e3b"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
||||
|
|
@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "d723622298b10400e9a1ba697cbebd831f5e0f7e5bbe4545b97f61e64db7535f"
|
||||
sha256 = "0368944155991c2ea304788418fb326f30e45649539a0cf49539194b026ca104"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/de.tsv"
|
||||
|
|
@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "686ab9658b7ef9dd7f3af05ce73f99f1ab2a75f8357bad6f7e987d40a8730df9"
|
||||
sha256 = "f4969c353f1424c6b9b5b110820c69dc59907590d013221511eae3f86c29024e"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/te.tsv"
|
||||
|
|
@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "90b94038699aa74aa48ffbda3e90a2255b65d0dbf081e8ecf20c5804940fc129"
|
||||
sha256 = "4bb6fa4f3b6e236a4a353df79acd0c5528ea8354058eeaeb66c28bc0f7e02939"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/it.tsv"
|
||||
|
|
@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "5aa6475df91a3e82010aee198c2e29bb80328505bbd0240f3828e36a5d6dfdf3"
|
||||
sha256 = "adcbdfc683589b067e062a6200084bf9192935b297f5b616f8ed39db9c71db9b"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pl.tsv"
|
||||
|
|
@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "e6d306d9bf289fac2fe8fcfa6f9d0a51b650728e45669289d391be2031076692"
|
||||
sha256 = "aebe63cd91df2d2f9d2bdc1da43b017823dbc8a32135e999b9ac7041277be0c8"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ru.tsv"
|
||||
|
|
@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "277f4995a87297a2fcc473df1feebdbdf3378fa460a7f85eb1a0a151cee40a2b"
|
||||
sha256 = "3c8e3a28a5b751c6d6384809d4fd81c14c2ff5f561aaee5b58d24fbfa2ca4dc1"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/hu.tsv"
|
||||
|
|
@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "01b32370cedce935b74925858a79ee691dc2a5560ba5eb758de2de1c243c528e"
|
||||
sha256 = "637fcfdd0bf1898b413af7ccbc2eb4228bf656273de9f3ef9241bd128940aeab"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/bg.tsv"
|
||||
|
|
@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "f8ae97901c61dfdc0a844a23356173211bb052b3ff54128d7edc8097c535eee9"
|
||||
sha256 = "5285f8987bd8c265f73ff443906c1832a0886037592d334974cf539b9f972a3f"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/kk.tsv"
|
||||
|
|
@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "7107ba77577794395451dd85355a86ef3e033a7f240b72414b877b9b43adb19e"
|
||||
sha256 = "23779abc08be40ddfb305fd638fe280c854ac33473b2822006ba8bad399c0cad"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
||||
|
|
@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "114b3d50531eb1682535c7a646e62914b0c9afaa5d5d1b9f66b94fd472e4e7b3"
|
||||
sha256 = "c5129c40b202d8467d8d4cf0a189da8c2ba2af711998edb73f67e1b291289a04"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/uk.tsv"
|
||||
|
|
@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "f56ec5f4cb7d1992041df0783f1cfd76c27d5d180c3f73c60b104e3fd02f600d"
|
||||
sha256 = "11ed2aa56dc5f8f6656eba717500c74bb8c8f931c08f9f9b210b5b33dd9c2d5a"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ar.tsv"
|
||||
|
|
@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "113fdf57e28fcff90d212fb468011bedebfb98ac100218d29b584f4de4c87912"
|
||||
sha256 = "a3863e2fed5ef584a87f65083a6b9fedf26d18ecebd574efce22bd7a01e3281c"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/tr.tsv"
|
||||
|
|
@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "2b5edfe026578ed475f07f94f1d1ed4a3dac93a745c19209589936e2d67f6ec5"
|
||||
sha256 = "cade94846289c2822b408a2ea23e6c8f8f321708f4f27fd563695a3c4fade6a1"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/lt.tsv"
|
||||
|
|
@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "fe58835b0538f470b463f078e1b4cdf78a8e2632056bc61c6946071478f39a64"
|
||||
sha256 = "35b6b03dae01b9a2e98f6d1f682d0699d77772e6efec7e28e1f26039c57b151f"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
|
|
@ -2365,16 +2365,16 @@ usage = "Default CMYK colour conversion (vendored hayro-interpret)"
|
|||
sha256 = "73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/fonts/src/std14_metrics.rs"
|
||||
path = "crates/fonts/src/std14.rs"
|
||||
title = "Standard-14 font advance widths and encodings (as Rust tables)"
|
||||
author = "Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors"
|
||||
source = "ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. Data only: the code that reads these tables lives in std14.rs, so that ordinary edits do not churn this file's hash."
|
||||
source = "ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs."
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "data"
|
||||
adobe_data = true
|
||||
usage = "Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree."
|
||||
sha256 = "f225e0b044875b8fa6375b6b5ad935f0d99fcc1ad03ecf9f0baae2cedbfca4d2"
|
||||
sha256 = "4ce47db7e65467d4ea1d1642ff76dfcaeb9669c1811a93819d7e00e63fa9fcf8"
|
||||
|
||||
[[bundled]]
|
||||
crate = "epaint_default_fonts"
|
||||
|
|
@ -3351,4 +3351,4 @@ licence = "MIT OR Apache-2.0"
|
|||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "9bbbd0d516f284b571ad6e2ed811f6bbdee283cea8483686f4aa939db38ea288"
|
||||
sha256 = "2685fbb694cbdeb7632ecb5e7396e1209047c83319e28ceea032b3ec9fb2fc0e"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,7 +15,7 @@ repository = "https://github.com/storytold/pdfcraft"
|
|||
# Shared frontend components; engines remain independent of egui.
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] }
|
||||
# Shared system spooler; document composition remains in pdfcraft-print.
|
||||
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "f03ec27ab2c405969428512178f746183cd55ca0" }
|
||||
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" }
|
||||
# internal
|
||||
pdfcraft-geom = { path = "crates/geom", version = "0.7.0" }
|
||||
pdfcraft-filters = { path = "crates/filters", version = "0.7.0" }
|
||||
|
|
|
|||
|
|
@ -150,7 +150,6 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
|||
## Progress log
|
||||
|
||||
- **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged.
|
||||
- **2026-10-12 (M10, Windows printing without PowerShell, #1107):** craft-print now prints on Windows in-process: printers from EnumPrinters/GetDefaultPrinter, jobs drawn as vectors by Windows.Data.Pdf into the XPS print API with a driver-validated PrintTicket (exact paper, orientation, no scaling), so printing works where App Control for Business or AppLocker run PowerShell in Constrained Language Mode. Portrait and landscape sheets still print as one job (turned with /Rotate); a sheet size the driver lacks is refused rather than shrunk. Pins craft-libs f03ec27 (craft-libs#28); deny.toml allows the storytold/winprint.rs fork and adds a webpki-root-certs licence exception and a RUSTSEC-2024-0370 ignore for winprint's build-dependencies. An earlier craft-print revision was checked in Windows Sandbox under enforced WDAC: printers listed, CLI and desktop jobs spooled, and no powershell.exe in the 4688 audit. The final pinned revision passed craft-libs Windows CI, including a real paused-queue job; that Sandbox run has not been repeated. Estimates unchanged.
|
||||
- **2026-10-12 (M13, CLI robustness scans):** `pdfcraft-cli check` visits a directory once even when input roots overlap or directory symlinks lead back into the tree. PDFs reached through file symlinks remain accepted. Three real CLI regressions cover overlapping roots, a directory cycle and directory/file aliases. Estimates unchanged.
|
||||
- **2026-10-12 (M4, Type 3 extraction fidelity):** Extract and Split preserve shared image/resource dictionaries reached through Type 3 glyphs, including nested forms, patterns and soft masks. A bounded object-graph walk protects unknown glyph content; unreadable pages/forms retain their glyph resources, and shared explicit fonts are visited once. Separate page dictionaries still prune unused images. Synthetic save/reopen regressions and an opt-in Poppler pixel oracle cover resource ownership, nested forms, cycles, limits and repeated fonts. Estimates unchanged.
|
||||
- **2026-10-12 (M2, TIFF predictor rows):** Packed RGB images using TIFF horizontal differencing keep every row when samples do not fill the last byte. The renderer aligns each decoded row before moving to the next. Synthetic decoding tests cover 1/2/4-bit samples, horizontal differencing and an 8-bit control; a red/green page-render regression verifies visible output. Estimates unchanged.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ mcp::McpServer::new(a).serve(stdin, stdout)? // newline-delimited JS
|
|||
- Tools that change a document return its summary (`doc`, `pages`, `dirty`, `undo`, `redo`, …).
|
||||
- `comment_image_preview` returns JSON and a PNG: `layer: "background"` (the default) gives the page without the selected image signature/initials; `layer: "image"` gives its embedded image with alpha. The metadata supplies its displayed rectangle, document rotation, `image_rotation` (how far clockwise the image is shown turned: 0 for one placed by PdfCraft, which keeps it upright as displayed) and annotation opacity. Each result can be saved with the CLI's `--out`. Cache the background and transform the image for live movement/resizing; `comment_edit` commits the final geometry in one undo step. Previewing never changes the document.
|
||||
- For proportional image signature resizing, use the embedded image layer's width/height ratio rather than a previously stretched annotation rectangle. The GUI preserves this ratio at corners with the opposite corner anchored; edge midpoint handles change only their own axis. `comment_edit` accepts the resulting rectangle.
|
||||
- `doc_close` refuses to drop unsaved changes unless `discard_changes: true`. `doc_save` writes atomically, incrementally in place, and in full for a new path. Atomic output writes call `sync_all` on the staging file before replacing the destination. A write or sync error leaves the destination intact, removes staging and leaves the document dirty for retry. A filesystem that does not support syncing returns an error; there is no unsynced fallback. This flushes file contents, but does not guarantee persistence of the parent-directory rename across power loss.
|
||||
- `doc_close` refuses to drop unsaved changes unless `discard_changes: true`. `doc_save` writes atomically, incrementally in place, and in full for a new path.
|
||||
- With a root set, every read and write path must resolve inside it (symlinks and `..` included). Relative paths resolve inside the root, and `..` is resolved by name before the check. Every path outside the root gets the same refusal (`<path> is outside the allowed directory <root>`), whether or not it exists, so a confined agent can't probe the rest of the disk; on Windows another network share or device path (`\\host\share`, `\\?\UNC\…`, `\\.\…`) is refused without being contacted. "Not found" and other filesystem errors are reported only for paths inside the root. Tools that write several files into a folder name them after the document or input file, with separators, colons and control characters replaced by `_`, so a name can't lead them out of the folder. The root itself must be a folder.
|
||||
|
||||
## Text search and cache limits
|
||||
|
|
|
|||
|
|
@ -858,10 +858,6 @@ impl Automation {
|
|||
}
|
||||
|
||||
fn doc_save(&mut self, a: &Args) -> Result<Value> {
|
||||
self.doc_save_with(a, write_atomic)
|
||||
}
|
||||
|
||||
fn doc_save_with(&mut self, a: &Args, write: impl FnOnce(&Path, &[u8]) -> Result<()>) -> Result<Value> {
|
||||
let doc = self.doc(a)?;
|
||||
let id = doc.id;
|
||||
let target = match a.opt_str("path")? {
|
||||
|
|
@ -879,7 +875,7 @@ impl Automation {
|
|||
// A save that adds or removes encryption, or must not keep the old revision (Sanitize,
|
||||
// redaction), is rewritten in full even when an incremental save was asked for.
|
||||
let incremental = !full && bytes.starts_with(&self.doc(a)?.saved_bytes());
|
||||
write(&target, &bytes)?;
|
||||
write_atomic(&target, &bytes)?;
|
||||
let path = target.to_string_lossy().into_owned();
|
||||
self.session.mark_saved(id, bytes.clone(), Some(path.clone())).map_err(failed)?;
|
||||
Ok(json!({ "path": path, "bytes": bytes.len(), "incremental": incremental, "document": summary(self.doc(a)?) }))
|
||||
|
|
@ -2209,13 +2205,8 @@ fn info(d: &Document) -> Value {
|
|||
})).collect::<Vec<_>>(),
|
||||
"layers": i.layers.iter().map(|l| json!({ "name": l.name, "visible": l.visible })).collect::<Vec<_>>(),
|
||||
"attachments": i.attachments.iter().map(|a| json!({ "name": a.name, "description": a.description, "size": a.size })).collect::<Vec<_>>(),
|
||||
// `substitute` names the face standing in for a program the document does not embed, and
|
||||
// `substitution` says why. Both are null for an embedded font: nothing stands in. A null
|
||||
// `substitute` with a non-null `substitution` means no face was available at all, so
|
||||
// nothing is drawn for that font -- which is the answer to "why is my text missing?".
|
||||
"fonts": i.fonts.iter().map(|f| json!({
|
||||
"name": f.name, "kind": f.kind, "embedded": f.embedded, "subset": f.subset, "encoding": f.encoding,
|
||||
"substitute": f.substitute, "substitution": f.substitution,
|
||||
})).collect::<Vec<_>>(),
|
||||
"warnings": i.warnings,
|
||||
"repairs": d.repair_log(),
|
||||
|
|
@ -2310,24 +2301,13 @@ fn unused(dir: &Path, stem: &str) -> PathBuf {
|
|||
(2..10_000u32).map(|n| child(dir, &format!("{stem} ({n}).pdf"))).find(|p| !p.exists()).unwrap_or(first)
|
||||
}
|
||||
|
||||
/// Write and sync a temporary file in the same directory, then rename over the target.
|
||||
/// This flushes staged contents; it does not promise parent-directory durability after a
|
||||
/// power loss. Filesystems that refuse sync cause the save to fail before publication.
|
||||
/// Write via a temporary file in the same directory, then rename over the target.
|
||||
fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||
write_atomic_with(path, bytes, staging_suffixes())
|
||||
}
|
||||
|
||||
/// [`write_atomic`], trying the staging names that `suffixes` give.
|
||||
fn write_atomic_with(path: &Path, bytes: &[u8], suffixes: impl IntoIterator<Item = u64>) -> Result<()> {
|
||||
write_atomic_synced(path, bytes, suffixes, std::fs::File::sync_all)
|
||||
}
|
||||
|
||||
fn write_atomic_synced(
|
||||
path: &Path,
|
||||
bytes: &[u8],
|
||||
suffixes: impl IntoIterator<Item = u64>,
|
||||
sync: impl FnOnce(&std::fs::File) -> std::io::Result<()>,
|
||||
) -> Result<()> {
|
||||
use std::io::Write;
|
||||
// A folder can't be replaced, and staging beside it could land outside the root: "." names
|
||||
// the root itself, whose parent isn't ours to write in.
|
||||
|
|
@ -2342,9 +2322,7 @@ fn write_atomic_synced(
|
|||
// Closed at the end of the block, before the rename.
|
||||
let written = {
|
||||
let mut file = file;
|
||||
// Surface delayed writeback failures before replacing the destination or marking
|
||||
// a document clean. The handle is closed before rename (required on Windows).
|
||||
file.write_all(bytes).and_then(|()| sync(&file))
|
||||
file.write_all(bytes)
|
||||
};
|
||||
if let Err(e) = written {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
|
|
@ -2702,79 +2680,6 @@ mod tests {
|
|||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_staging_sync_keeps_the_destination_and_cleans_up() {
|
||||
for exists in [false, true] {
|
||||
let dir = staging_dir(if exists { "sync-existing" } else { "sync-new" });
|
||||
let target = dir.join("out.pdf");
|
||||
if exists {
|
||||
std::fs::write(&target, "OLD").unwrap();
|
||||
}
|
||||
let result = write_atomic_synced(&target, b"NEW", [1], |_| {
|
||||
assert_eq!(std::fs::read(staged(&dir, 1)).unwrap(), b"NEW", "sync follows writing");
|
||||
if exists {
|
||||
assert_eq!(read(&target), "OLD", "sync precedes publication");
|
||||
}
|
||||
Err(std::io::Error::other("injected storage sync failure"))
|
||||
});
|
||||
assert!(result.is_err(), "sync failure must refuse publication");
|
||||
assert!(result.unwrap_err().to_string().contains("injected storage sync failure"));
|
||||
if exists {
|
||||
assert_eq!(read(&target), "OLD");
|
||||
assert_eq!(listing(&dir), ["out.pdf"]);
|
||||
} else {
|
||||
assert!(!target.exists());
|
||||
assert!(listing(&dir).is_empty());
|
||||
}
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_save_sync_keeps_the_document_dirty_and_can_be_retried() {
|
||||
let dir = staging_dir("sync-dirty");
|
||||
let target = dir.join("out.pdf");
|
||||
let mut a = Automation::new().with_root(&dir).unwrap();
|
||||
let id = search_document(&mut a, 2);
|
||||
json(&mut a, "doc_save", json!({"doc": id.0, "path": "out.pdf"}));
|
||||
let old = std::fs::read(&target).unwrap();
|
||||
json(&mut a, "page_rotate", json!({"doc": id.0, "pages": [1], "degrees": 90}));
|
||||
let args = json!({"doc": id.0});
|
||||
let before = summary(a.doc(&Args(&args)).unwrap());
|
||||
let result = a.doc_save_with(&Args(&args), |path, bytes| {
|
||||
write_atomic_synced(path, bytes, [1], |_| Err(std::io::Error::other("injected storage sync failure")))
|
||||
});
|
||||
assert!(result.is_err());
|
||||
let doc = a.doc(&Args(&args)).unwrap();
|
||||
assert!(doc.dirty);
|
||||
assert_eq!(summary(doc), before, "failed save must not reset history or saved state");
|
||||
assert_eq!(std::fs::read(&target).unwrap(), old);
|
||||
assert_eq!(listing(&dir), ["out.pdf"]);
|
||||
json(&mut a, "doc_save", args.clone());
|
||||
assert!(!a.doc(&Args(&args)).unwrap().dirty);
|
||||
assert_ne!(std::fs::read(&target).unwrap(), old);
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_staging_sync_precedes_publication() {
|
||||
let dir = staging_dir("sync-success");
|
||||
let target = dir.join("out.pdf");
|
||||
std::fs::write(&target, "OLD").unwrap();
|
||||
let mut called = false;
|
||||
write_atomic_synced(&target, b"NEW", [1], |file| {
|
||||
called = true;
|
||||
assert_eq!(read(&target), "OLD");
|
||||
assert_eq!(std::fs::read(staged(&dir, 1)).unwrap(), b"NEW");
|
||||
file.sync_all()
|
||||
})
|
||||
.unwrap();
|
||||
assert!(called);
|
||||
assert_eq!(read(&target), "NEW");
|
||||
assert_eq!(listing(&dir), ["out.pdf"]);
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// The names in a folder: what a test can see was left behind.
|
||||
fn listing(dir: &Path) -> Vec<String> {
|
||||
let mut names: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect();
|
||||
|
|
|
|||
|
|
@ -409,13 +409,6 @@ fn open_inspect_render_and_find() {
|
|||
assert_eq!(info["pages"][0]["width"], 200.0);
|
||||
assert_eq!(info["pages"][1]["label"], "2");
|
||||
|
||||
// The fixture names /Helvetica without embedding it, so an agent can see both that a face is
|
||||
// standing in and why that face. `substitution` is null only when nothing stands in.
|
||||
let font = &info["fonts"][0];
|
||||
assert_eq!((&font["name"], &font["embedded"]), (&json!("Helvetica"), &json!(false)));
|
||||
assert_eq!(font["substitute"], json!("Helvetica"));
|
||||
assert!(font["substitution"].as_str().is_some_and(|w| w.contains("standard 14")), "{font}");
|
||||
|
||||
assert_eq!(page_text(&mut a, doc), ["Page 1", "Page 2", "Page 3"]);
|
||||
|
||||
let found = ok(&mut a, "text_find", json!({ "doc": doc, "query": "page 2" }));
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ fn result(auto: &mut Automation, args: Value) -> Value {
|
|||
}
|
||||
}
|
||||
|
||||
// Fresh fixtures include the current timestamp; retain their bytes for output comparisons.
|
||||
fn pdf() -> Vec<u8> {
|
||||
Session::new().create_from_text("Original", "Original text").unwrap().as_ref().clone()
|
||||
}
|
||||
|
|
@ -50,9 +49,8 @@ fn batch_is_nonrecursive_by_default_and_stop_reports_unprocessed_files() {
|
|||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join("input/sub")).unwrap();
|
||||
std::fs::write(dir.path().join("input/0.pdf"), b"broken").unwrap();
|
||||
let original = pdf();
|
||||
std::fs::write(dir.path().join("input/1.pdf"), &original).unwrap();
|
||||
std::fs::write(dir.path().join("input/sub/2.pdf"), &original).unwrap();
|
||||
std::fs::write(dir.path().join("input/1.pdf"), pdf()).unwrap();
|
||||
std::fs::write(dir.path().join("input/sub/2.pdf"), pdf()).unwrap();
|
||||
let mut auto = Automation::new().with_root(dir.path()).unwrap();
|
||||
let report = result(&mut auto, json!({"paths":["input"],"steps":["optimize"],"out_dir":"out","stop_on_error":true}));
|
||||
assert_eq!(report["failed"], 1);
|
||||
|
|
@ -63,7 +61,7 @@ fn batch_is_nonrecursive_by_default_and_stop_reports_unprocessed_files() {
|
|||
let report = result(&mut auto, json!({"paths":["input"],"steps":["optimize"],"out_dir":"out"}));
|
||||
assert_eq!(report["completed"], 1);
|
||||
assert!(!dir.path().join("out/sub").exists());
|
||||
assert!(std::fs::metadata(dir.path().join("out/1.pdf")).unwrap().len() <= original.len() as u64);
|
||||
assert!(std::fs::metadata(dir.path().join("out/1.pdf")).unwrap().len() <= pdf().len() as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -109,13 +107,12 @@ fn explicitly_selected_subfolder_is_processed_without_recursive() {
|
|||
#[test]
|
||||
fn explicit_file_can_write_a_numbered_copy_beside_its_source() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let original = pdf();
|
||||
std::fs::write(dir.path().join("page.pdf"), &original).unwrap();
|
||||
std::fs::write(dir.path().join("page.pdf"), pdf()).unwrap();
|
||||
let mut auto = Automation::new().with_root(dir.path()).unwrap();
|
||||
let report = result(&mut auto, json!({"paths":["page.pdf"],"steps":["convert"],"out_dir":"."}));
|
||||
assert_eq!(report["completed"], 1, "{report}");
|
||||
assert_eq!(std::fs::read(dir.path().join("page.pdf")).unwrap(), original);
|
||||
assert_eq!(std::fs::read(dir.path().join("page (2).pdf")).unwrap(), original);
|
||||
assert_eq!(std::fs::read(dir.path().join("page.pdf")).unwrap(), pdf());
|
||||
assert_eq!(std::fs::read(dir.path().join("page (2).pdf")).unwrap(), pdf());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
|
@ -135,14 +132,13 @@ fn non_utf8_discovered_names_are_reported_without_panicking() {
|
|||
assert_eq!(std::fs::read_dir(dir.path().join("input")).unwrap().count(), 0);
|
||||
assert!(!dir.path().join("out").exists());
|
||||
let valid = dir.path().join("input/valid-λ.pdf");
|
||||
let original = pdf();
|
||||
std::fs::write(&valid, &original).unwrap();
|
||||
assert_eq!(std::fs::read(&valid).unwrap(), original);
|
||||
std::fs::write(&valid, pdf()).unwrap();
|
||||
assert_eq!(std::fs::read(&valid).unwrap(), pdf());
|
||||
let mut auto = Automation::new().with_root(dir.path()).unwrap();
|
||||
let report = result(&mut auto, json!({"paths":["input"],"steps":["convert"],"out_dir":"out"}));
|
||||
assert_eq!(report["completed"], 1, "{report}");
|
||||
assert_eq!(report["failed"], 0, "{report}");
|
||||
assert_eq!(std::fs::read(dir.path().join("out/valid-λ.pdf")).unwrap(), original);
|
||||
assert_eq!(std::fs::read(dir.path().join("out/valid-λ.pdf")).unwrap(), pdf());
|
||||
eprintln!(
|
||||
"non-UTF-8 fixture capability unavailable: macOS filesystem rejected creation with EILSEQ92; valid-Unicode batch companion passed; invalid-name discovery still requires Linux/FreeBSD or a supporting filesystem"
|
||||
);
|
||||
|
|
@ -193,8 +189,7 @@ fn batch_ocr_uses_existing_models_or_reports_the_missing_models_per_file() {
|
|||
#[test]
|
||||
fn batch_output_collisions_are_safe_under_concurrent_writers() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let original = pdf();
|
||||
std::fs::write(dir.path().join("page.pdf"), &original).unwrap();
|
||||
std::fs::write(dir.path().join("page.pdf"), pdf()).unwrap();
|
||||
let barrier = std::sync::Barrier::new(2);
|
||||
std::thread::scope(|scope| {
|
||||
let handles: Vec<_> = (0..2)
|
||||
|
|
@ -211,7 +206,7 @@ fn batch_output_collisions_are_safe_under_concurrent_writers() {
|
|||
assert_ne!(reports[0]["files"][0]["output"], reports[1]["files"][0]["output"]);
|
||||
});
|
||||
for name in ["page.pdf", "page (2).pdf"] {
|
||||
assert_eq!(std::fs::read(dir.path().join("out").join(name)).unwrap(), original);
|
||||
assert_eq!(std::fs::read(dir.path().join("out").join(name)).unwrap(), pdf());
|
||||
}
|
||||
assert_eq!(std::fs::read_dir(dir.path().join("out")).unwrap().count(), 2, "no staging files remain");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,11 +216,6 @@ pub const COMMANDS: &[CommandSpec] = &[
|
|||
c("edit.snapshot", "Take a snapshot", EDIT, None, Copying, "camera"),
|
||||
c("view.full_screen", "Full screen mode", VIEW, Some(Shortcut::cmd("L")), Document, "maximize"),
|
||||
c("view.read_mode", "Read mode", VIEW, Some(Shortcut { command: true, shift: false, mac_ctrl: true, key: "H" }), Document, "book-open"),
|
||||
// Browsers' tab keys (#1542): Ctrl+Tab on every platform, ⌃Tab rather than ⌘Tab on macOS,
|
||||
// where the system takes ⌘Tab. They follow the tabs' order on screen and wrap around, and
|
||||
// also run from Combine files, which has no document open.
|
||||
c("view.next_tab", "Next tab", VIEW, Some(Shortcut { command: false, shift: false, mac_ctrl: true, key: "Tab" }), Nothing, "chevron-right"),
|
||||
c("view.previous_tab", "Previous tab", VIEW, Some(Shortcut { command: false, shift: true, mac_ctrl: true, key: "Tab" }), Nothing, "chevron-left"),
|
||||
c("view.focus_page_input", "Go to page…", VIEW, Some(Shortcut::cmd_shift("N")), Document, "text-cursor-input"),
|
||||
c("ui.dock.reset", "Reset panel layout", VIEW, None, Nothing, "settings"),
|
||||
c("view.theme", "Switch light / dark theme", None, None, Nothing, "moon"),
|
||||
|
|
|
|||
|
|
@ -1,370 +0,0 @@
|
|||
//! The one chain PdfCraft follows when a document does not embed a font program, and a record of
|
||||
//! what it chose and why.
|
||||
//!
|
||||
//! Substitution used to be decided per caller and never written down: `edit` picked a craft face
|
||||
//! by the script of the text it was drawing, `Std14::from_base_font` matched Latin names, and the
|
||||
//! renderer did its own thing. Nothing told the *user* that the glyphs they are looking at are not
|
||||
//! the ones the document asked for, which is the difference between "PdfCraft shows nothing for my
|
||||
//! Chinese PDF" being a mystery and being a sentence.
|
||||
//!
|
||||
//! This module answers the *dictionary* question — "the document does not carry this program; what
|
||||
//! will stand in for it, and why?" — which is what inspection and reporting need. It deliberately
|
||||
//! does not answer the *text* question ("which face has a glyph for こ?"), because that depends on
|
||||
//! the string being drawn and is already handled where the drawing happens. It delegates to the
|
||||
//! existing per-script selectors rather than restating their order, so there is one source of
|
||||
//! truth for each.
|
||||
//!
|
||||
//! The chain is tried in order, and the first step that yields a face wins:
|
||||
//!
|
||||
//! 1. The document embeds the program. Nothing is substituted.
|
||||
//! 2. `/BaseFont` names one of the standard 14 (or a well-known alias such as Arial). Exact
|
||||
//! published metrics apply, so this is as close to right as a substitution gets.
|
||||
//! 3. craft-fonts holds that exact family. Never another family — see [`crate::embedding`].
|
||||
//! 4. The descendant font's `/CIDSystemInfo /Ordering` names a script, and craft-fonts has a face
|
||||
//! for it. Today that means Adobe-Japan1.
|
||||
//! 5. The ordering names a script we have **no** licensed face for (GB1, CNS1, Korea1). There is
|
||||
//! no honest substitute, so the chain reports none: the caller draws nothing and can say why.
|
||||
//! 6. Otherwise, the nearest of the standard 14 by `/FontDescriptor /Flags` — serif, fixed pitch,
|
||||
//! bold and italic. This step always succeeds, so a simple font is never left without a face.
|
||||
|
||||
use crate::{CraftFont, Std14};
|
||||
|
||||
/// What a font dictionary asks for, as far as the dictionary alone can say.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Request<'a> {
|
||||
/// `/BaseFont` with any `ABCDEF+` subset tag already stripped.
|
||||
pub base_font: &'a str,
|
||||
/// Whether the descriptor carries `/FontFile`, `/FontFile2` or `/FontFile3` (or it is Type 3).
|
||||
pub embedded: bool,
|
||||
/// `/FontDescriptor /Flags`, or 0 when there is no descriptor.
|
||||
pub flags: u32,
|
||||
/// The descendant font's `/CIDSystemInfo /Ordering`, for a composite font.
|
||||
pub ordering: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// The face the chain picked.
|
||||
///
|
||||
/// `Debug`, `PartialEq` and `Eq` are written by hand rather than derived, because [`CraftFont`]
|
||||
/// carries a whole font program: deriving would print megabytes and compare font files.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Face {
|
||||
/// The document's own program; nothing was substituted.
|
||||
Embedded,
|
||||
/// One of the standard 14, whose metrics are exact.
|
||||
Standard14(Std14),
|
||||
/// A face from the optional craft-fonts build input.
|
||||
Craft(&'static CraftFont),
|
||||
/// No face at all. The caller must draw nothing rather than draw the wrong glyphs.
|
||||
None,
|
||||
}
|
||||
|
||||
impl PartialEq for Face {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Embedded, Self::Embedded) | (Self::None, Self::None) => true,
|
||||
(Self::Standard14(a), Self::Standard14(b)) => a == b,
|
||||
// craft-fonts entries are unique statics, so identity is the comparison that means
|
||||
// what we want: two styles of one family are different faces and must not be equal.
|
||||
(Self::Craft(a), Self::Craft(b)) => std::ptr::eq(*a, *b),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Face {}
|
||||
|
||||
impl std::fmt::Debug for Face {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Embedded => f.write_str("Embedded"),
|
||||
Self::Standard14(s) => write!(f, "Standard14({})", s.base_font()),
|
||||
// The name, never the bytes.
|
||||
Self::Craft(c) => write!(f, "Craft({})", c.name()),
|
||||
Self::None => f.write_str("None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Why the chain picked that face. The variants are in the order the chain tries them.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Why {
|
||||
/// The document carries the font program.
|
||||
Embedded,
|
||||
/// `/BaseFont` names a standard-14 face or a well-known alias of one.
|
||||
NamedStandard14,
|
||||
/// `/BaseFont` is otherwise unknown, but its name hints at a family ("Serif", "Roman",
|
||||
/// "Mono"). A guess from the name, and reported as one.
|
||||
NameHintedStandard14,
|
||||
/// craft-fonts holds this exact family.
|
||||
ExactCraftFamily,
|
||||
/// A craft-fonts face for the script named by `/CIDSystemInfo /Ordering`.
|
||||
ScriptFace,
|
||||
/// The ordering names a script with no licensed face available.
|
||||
NoFaceForScript,
|
||||
/// The nearest standard-14 face by descriptor flags.
|
||||
StyleMatched,
|
||||
}
|
||||
|
||||
impl Why {
|
||||
/// A stable sentence for a report, written for whoever has to act on it.
|
||||
pub fn note(self) -> &'static str {
|
||||
match self {
|
||||
Self::Embedded => "embedded in the document; not substituted",
|
||||
Self::NamedStandard14 => "names one of the standard 14 fonts, or a well-known alias of one, whose metrics are exact",
|
||||
Self::NameHintedStandard14 => {
|
||||
"not embedded and not a known family, but its name hints at one (Serif, Roman or Mono), so the nearest standard-14 face was chosen from that hint"
|
||||
}
|
||||
Self::ExactCraftFamily => "this exact family is available, so no other family was chosen",
|
||||
Self::ScriptFace => "substituted by a face for the script the font declares",
|
||||
Self::NoFaceForScript => {
|
||||
"no licensed face is available for the script this font declares, so nothing is drawn for it \
|
||||
rather than the wrong glyphs"
|
||||
}
|
||||
Self::StyleMatched => "not embedded and not a known family, so the nearest standard-14 face was chosen by its descriptor flags",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the document's own glyphs are being shown. Only [`Why::Embedded`] is faithful.
|
||||
pub fn is_faithful(self) -> bool {
|
||||
matches!(self, Self::Embedded)
|
||||
}
|
||||
}
|
||||
|
||||
/// What the chain chose, and why.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Substitution {
|
||||
pub face: Face,
|
||||
pub why: Why,
|
||||
}
|
||||
|
||||
impl Substitution {
|
||||
/// The chosen face's name, for a report. `None` when nothing was chosen.
|
||||
pub fn face_name(&self) -> Option<String> {
|
||||
match self.face {
|
||||
Face::Embedded => None,
|
||||
Face::Standard14(f) => Some(f.base_font().to_owned()),
|
||||
Face::Craft(f) => Some(f.name()),
|
||||
Face::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CID orderings we recognise, and whether a craft-fonts face may exist for them.
|
||||
///
|
||||
/// `Identity` is not a script: it says the codes are glyph ids, so it tells us nothing about
|
||||
/// which face would suit and the chain falls through to the flags.
|
||||
fn script_for(ordering: &str) -> Option<&'static str> {
|
||||
match ordering {
|
||||
"Japan1" => Some("Jpan"),
|
||||
// Registered orderings we have no openly licensed face for. AGENTS.md 1.1 rules out
|
||||
// Source Han and Noto CJK (Source Han rebranded), which is why these have none.
|
||||
"GB1" | "CNS1" | "Korea1" | "KR" => Some(""),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the chain to `req`. Total: every request gets a [`Substitution`].
|
||||
///
|
||||
/// See the module documentation for the order and the reasoning behind each step.
|
||||
pub fn substitute(req: &Request<'_>) -> Substitution {
|
||||
let at = |face, why| Substitution { face, why };
|
||||
if req.embedded {
|
||||
return at(Face::Embedded, Why::Embedded);
|
||||
}
|
||||
if let Some((f, how)) = Std14::recognise(req.base_font) {
|
||||
return match how {
|
||||
crate::std14::Match::Alias => at(Face::Standard14(f), Why::NamedStandard14),
|
||||
crate::std14::Match::FamilyHint => at(Face::Standard14(f), Why::NameHintedStandard14),
|
||||
};
|
||||
}
|
||||
if let Some(f) = crate::embedding::matching_faces(req.base_font).first() {
|
||||
return at(Face::Craft(f), Why::ExactCraftFamily);
|
||||
}
|
||||
if let Some(script) = req.ordering.and_then(script_for) {
|
||||
// A script we hold a face for; otherwise there is no honest substitute to offer.
|
||||
return match crate::CRAFT_FONTS.iter().find(|f| f.covers(script)) {
|
||||
Some(f) => at(Face::Craft(f), Why::ScriptFace),
|
||||
None => at(Face::None, Why::NoFaceForScript),
|
||||
};
|
||||
}
|
||||
at(Face::Standard14(style_matched(req.flags)), Why::StyleMatched)
|
||||
}
|
||||
|
||||
/// The nearest standard-14 face for `/FontDescriptor /Flags` (ISO 32000-2 table 121): fixed pitch
|
||||
/// is bit 1, serif bit 2, italic bit 7 and ForceBold bit 19.
|
||||
fn style_matched(flags: u32) -> Std14 {
|
||||
let (bold, italic) = (flags & 0x4_0000 != 0, flags & 0x40 != 0);
|
||||
match (flags & 1 != 0, flags & 2 != 0, bold, italic) {
|
||||
(true, _, false, false) => Std14::Courier,
|
||||
(true, _, true, false) => Std14::CourierBold,
|
||||
(true, _, false, true) => Std14::CourierOblique,
|
||||
(true, _, true, true) => Std14::CourierBoldOblique,
|
||||
(false, true, false, false) => Std14::TimesRoman,
|
||||
(false, true, true, false) => Std14::TimesBold,
|
||||
(false, true, false, true) => Std14::TimesItalic,
|
||||
(false, true, true, true) => Std14::TimesBoldItalic,
|
||||
(false, false, false, false) => Std14::Helvetica,
|
||||
(false, false, true, false) => Std14::HelveticaBold,
|
||||
(false, false, false, true) => Std14::HelveticaOblique,
|
||||
(false, false, true, true) => Std14::HelveticaBoldOblique,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn req<'a>(base: &'a str, embedded: bool, flags: u32, ordering: Option<&'a str>) -> Request<'a> {
|
||||
Request { base_font: base, embedded, flags, ordering }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_embedded_font_is_never_substituted() {
|
||||
// True even when the name and flags would otherwise pick something: embedding wins first.
|
||||
for (base, ordering) in [("Helvetica", None), ("Whatever", Some("GB1")), ("", None)] {
|
||||
let s = substitute(&req(base, true, 0, ordering));
|
||||
assert_eq!(s, Substitution { face: Face::Embedded, why: Why::Embedded });
|
||||
assert!(s.why.is_faithful());
|
||||
assert_eq!(s.face_name(), None, "nothing stands in, so there is no substitute to name");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_standard_14_name_wins_over_the_flags() {
|
||||
// Flags claiming a fixed-pitch serif must not override a name we have exact metrics for.
|
||||
let s = substitute(&req("Times-BoldItalic", false, 0b11, None));
|
||||
assert_eq!(s.face, Face::Standard14(Std14::TimesBoldItalic));
|
||||
assert_eq!(s.why, Why::NamedStandard14);
|
||||
assert_eq!(s.face_name().as_deref(), Some("Times-BoldItalic"));
|
||||
// Aliases and subset tags resolve the same way.
|
||||
for name in ["Arial", "ArialMT", "Arial-Bold", "Symbol", "ZapfDingbats", "CourierNew"] {
|
||||
assert_eq!(substitute(&req(name, false, 0, None)).why, Why::NamedStandard14, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_hint_is_reported_as_a_guess_not_as_a_standard_14_name() {
|
||||
// A house font whose name merely contains "Serif" gets Times, which is a sensible guess --
|
||||
// but calling that "names one of the standard 14" would tell the reader something untrue.
|
||||
let hinted = substitute(&req("SomeHouseSerif", false, 0, None));
|
||||
assert_eq!(hinted.face, Face::Standard14(Std14::TimesRoman));
|
||||
assert_eq!(hinted.why, Why::NameHintedStandard14);
|
||||
assert!(hinted.why.note().contains("hints at one"), "{}", hinted.why.note());
|
||||
|
||||
// Style words still apply to the guess.
|
||||
assert_eq!(substitute(&req("SomeHouseSerif-BoldItalic", false, 0, None)).face, Face::Standard14(Std14::TimesBoldItalic));
|
||||
// "Mono" hints at Courier the same way.
|
||||
assert_eq!(substitute(&req("HouseMono", false, 0, None)).why, Why::NameHintedStandard14);
|
||||
|
||||
// A real alias is still reported as the request it is.
|
||||
for alias in ["Arial", "CourierNew", "Times-Roman", "Symbol"] {
|
||||
assert_eq!(substitute(&req(alias, false, 0, None)).why, Why::NamedStandard14, "{alias}");
|
||||
}
|
||||
// A name with no hint at all falls through to the flags instead.
|
||||
assert_eq!(substitute(&req("HouseGrotesk", false, 0b10, None)).why, Why::StyleMatched);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_pick_the_nearest_of_the_fourteen() {
|
||||
// Bit 1 fixed pitch, bit 2 serif, bit 7 italic, bit 19 ForceBold.
|
||||
let cases = [
|
||||
(0, Std14::Helvetica),
|
||||
(0b10, Std14::TimesRoman),
|
||||
(0b1, Std14::Courier),
|
||||
(0x40, Std14::HelveticaOblique),
|
||||
(0x4_0000, Std14::HelveticaBold),
|
||||
(0x4_0040, Std14::HelveticaBoldOblique),
|
||||
(0b10 | 0x40, Std14::TimesItalic),
|
||||
(0b10 | 0x4_0000, Std14::TimesBold),
|
||||
(0b10 | 0x4_0040, Std14::TimesBoldItalic),
|
||||
(0b1 | 0x40, Std14::CourierOblique),
|
||||
(0b1 | 0x4_0000, Std14::CourierBold),
|
||||
(0b1 | 0x4_0040, Std14::CourierBoldOblique),
|
||||
// Fixed pitch and serif together: monospace is the stronger signal for layout.
|
||||
(0b11, Std14::Courier),
|
||||
];
|
||||
for (flags, want) in cases {
|
||||
let s = substitute(&req("SomethingUnknown", false, flags, None));
|
||||
assert_eq!(s.face, Face::Standard14(want), "flags {flags:#x}");
|
||||
assert_eq!(s.why, Why::StyleMatched);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_script_with_no_licensed_face_reports_none_rather_than_latin() {
|
||||
// The user's Chinese PDF draws nothing. The point of this is that we can now say so,
|
||||
// instead of quietly standing Helvetica in and showing the wrong glyphs.
|
||||
for ordering in ["GB1", "CNS1", "Korea1", "KR"] {
|
||||
let s = substitute(&req("SomeCJKFont", false, 0, Some(ordering)));
|
||||
assert_eq!(s.face, Face::None, "{ordering}");
|
||||
assert_eq!(s.why, Why::NoFaceForScript, "{ordering}");
|
||||
assert_eq!(s.face_name(), None);
|
||||
assert!(!s.why.is_faithful());
|
||||
assert!(s.why.note().contains("no licensed face"), "{ordering}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_ordering_is_not_a_script_and_falls_through_to_the_flags() {
|
||||
// /Ordering (Identity) says the codes are glyph ids. It implies nothing about the face,
|
||||
// so treating it as a script would invent a substitution the dictionary never asked for.
|
||||
let s = substitute(&req("SomeSubsetFont", false, 0b10, Some("Identity")));
|
||||
assert_eq!(s.face, Face::Standard14(Std14::TimesRoman));
|
||||
assert_eq!(s.why, Why::StyleMatched);
|
||||
// An unregistered ordering is treated the same way, not guessed at.
|
||||
assert_eq!(substitute(&req("X", false, 0, Some("MadeUp1"))).why, Why::StyleMatched);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_depends_on_craft_fonts_and_says_which_it_got() {
|
||||
// craft-fonts is an optional build input, so both outcomes are correct -- but they must be
|
||||
// distinguishable, and neither may be a silent Latin stand-in.
|
||||
let s = substitute(&req("SomeJapaneseFont", false, 0, Some("Japan1")));
|
||||
if crate::CRAFT_FONTS.iter().any(|f| f.covers("Jpan")) {
|
||||
assert_eq!(s.why, Why::ScriptFace);
|
||||
assert!(matches!(s.face, Face::Craft(_)));
|
||||
assert!(s.face_name().is_some_and(|n| !n.is_empty()));
|
||||
} else {
|
||||
assert_eq!(s.why, Why::NoFaceForScript);
|
||||
assert_eq!(s.face, Face::None);
|
||||
}
|
||||
assert!(!s.why.is_faithful(), "a substitution is never faithful");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_why_has_a_distinct_usable_note() {
|
||||
let all = [
|
||||
Why::Embedded,
|
||||
Why::NamedStandard14,
|
||||
Why::NameHintedStandard14,
|
||||
Why::ExactCraftFamily,
|
||||
Why::ScriptFace,
|
||||
Why::NoFaceForScript,
|
||||
Why::StyleMatched,
|
||||
];
|
||||
let mut notes: Vec<&str> = all.iter().map(|w| w.note()).collect();
|
||||
notes.sort_unstable();
|
||||
let before = notes.len();
|
||||
notes.dedup();
|
||||
assert_eq!(notes.len(), before, "each reason reads differently");
|
||||
assert!(all.iter().all(|w| !w.note().is_empty() && w.note().len() > 20), "notes are sentences, not labels");
|
||||
assert_eq!(all.iter().filter(|w| w.is_faithful()).count(), 1, "only an embedded program is faithful");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_input_is_total_and_never_panics() {
|
||||
// Every field is attacker-controlled: a name is any bytes the document chose.
|
||||
let long = "a".repeat(10_000);
|
||||
let names = ["", " ", "+", "ABCDEF+", "\u{0}\u{1}", long.as_str(), "../../etc/passwd", "Ĥéļvéťïçà"];
|
||||
for name in names {
|
||||
for flags in [0, u32::MAX, 0x4_0040, 1] {
|
||||
for ordering in [None, Some(""), Some("Japan1"), Some("\u{0}")] {
|
||||
let s = substitute(&req(name, false, flags, ordering));
|
||||
// Always a decision, and the reason always reads.
|
||||
assert!(!s.why.note().is_empty());
|
||||
assert_eq!(s.face == Face::None, s.why == Why::NoFaceForScript, "only one step declines: {name:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,13 +10,11 @@
|
|||
mod craft;
|
||||
pub mod embedding;
|
||||
mod encodings;
|
||||
pub mod fallback;
|
||||
pub mod pdf;
|
||||
mod script;
|
||||
mod shaping;
|
||||
pub mod source_shaping;
|
||||
mod std14;
|
||||
mod std14_metrics;
|
||||
pub use craft::{
|
||||
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style,
|
||||
document_japanese_fonts_for_style, document_thai_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts,
|
||||
|
|
|
|||
|
|
@ -32,60 +32,44 @@ pub enum Std14 {
|
|||
ZapfDingbats,
|
||||
}
|
||||
|
||||
/// How [`Std14::recognise`] matched a `/BaseFont` name.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Match {
|
||||
/// A standard-14 face or a well-known alias of one (Arial for Helvetica, CourierNew for
|
||||
/// Courier). The document is asking for this face.
|
||||
Alias,
|
||||
/// Only a family hint matched -- "Serif", "Roman" or "Mono" in an otherwise unknown name.
|
||||
/// The face is a guess from the name, not the one the document asked for.
|
||||
FamilyHint,
|
||||
}
|
||||
|
||||
impl Std14 {
|
||||
/// The font a `/BaseFont` name selects, by the spec's names and the common aliases viewers
|
||||
/// accept (`Arial` for `Helvetica`, `TimesNewRoman` for `Times`, `CourierNew` for `Courier`).
|
||||
/// A subset prefix such as `ABCDEF+Helvetica` is ignored.
|
||||
pub fn from_base_font(base: &str) -> Option<Self> {
|
||||
Self::recognise(base).map(|(face, _)| face)
|
||||
}
|
||||
|
||||
/// [`Std14::from_base_font`], also saying *how* the name matched.
|
||||
///
|
||||
/// Reporting needs the difference. "Arial" really is a request for Helvetica, but
|
||||
/// "SomeHouseSerif" is a house font whose name merely contains "Serif"; choosing Times for it
|
||||
/// is a reasonable guess from the name, not the face the document asked for, and a substitution
|
||||
/// report that called it a standard-14 name would be telling the reader something untrue.
|
||||
pub fn recognise(base: &str) -> Option<(Self, Match)> {
|
||||
let base = base.rsplit('+').next().unwrap_or(base);
|
||||
let flat: String = base.chars().filter(|c| c.is_ascii_alphanumeric()).map(|c| c.to_ascii_lowercase()).collect();
|
||||
let has = |w: &str| flat.contains(w);
|
||||
let (bold, italic) = (has("bold"), has("italic") || has("oblique"));
|
||||
// Two bools index a four-element array, so the subscript is provably 0..=3.
|
||||
let styled = |faces: [Self; 4]| faces[usize::from(bold) | usize::from(italic) << 1];
|
||||
let courier = || styled([Self::Courier, Self::CourierBold, Self::CourierOblique, Self::CourierBoldOblique]);
|
||||
let times = || styled([Self::TimesRoman, Self::TimesBold, Self::TimesItalic, Self::TimesBoldItalic]);
|
||||
if has("symbol") {
|
||||
return Some((Self::Symbol, Match::Alias));
|
||||
return Some(Self::Symbol);
|
||||
}
|
||||
if has("zapf") || has("dingbat") {
|
||||
return Some((Self::ZapfDingbats, Match::Alias));
|
||||
return Some(Self::ZapfDingbats);
|
||||
}
|
||||
if has("courier") {
|
||||
return Some((courier(), Match::Alias));
|
||||
if has("courier") || has("mono") {
|
||||
return Some(match (bold, italic) {
|
||||
(false, false) => Self::Courier,
|
||||
(true, false) => Self::CourierBold,
|
||||
(false, true) => Self::CourierOblique,
|
||||
(true, true) => Self::CourierBoldOblique,
|
||||
});
|
||||
}
|
||||
if has("mono") {
|
||||
return Some((courier(), Match::FamilyHint));
|
||||
}
|
||||
if has("times") {
|
||||
return Some((times(), Match::Alias));
|
||||
}
|
||||
if has("serif") || has("roman") {
|
||||
return Some((times(), Match::FamilyHint));
|
||||
if has("times") || has("serif") || has("roman") {
|
||||
return Some(match (bold, italic) {
|
||||
(false, false) => Self::TimesRoman,
|
||||
(true, false) => Self::TimesBold,
|
||||
(false, true) => Self::TimesItalic,
|
||||
(true, true) => Self::TimesBoldItalic,
|
||||
});
|
||||
}
|
||||
if has("helvetica") || has("arial") {
|
||||
return Some((styled([Self::Helvetica, Self::HelveticaBold, Self::HelveticaOblique, Self::HelveticaBoldOblique]), Match::Alias));
|
||||
return Some(match (bold, italic) {
|
||||
(false, false) => Self::Helvetica,
|
||||
(true, false) => Self::HelveticaBold,
|
||||
(false, true) => Self::HelveticaOblique,
|
||||
(true, true) => Self::HelveticaBoldOblique,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -93,7 +77,7 @@ impl Std14 {
|
|||
/// The advance of one character `code`, in thousandths of an em. Zero where the font's
|
||||
/// encoding leaves the code undefined.
|
||||
pub fn width(self, code: u8) -> f64 {
|
||||
f64::from(crate::std14_metrics::WIDTHS[self as usize][code as usize])
|
||||
f64::from(WIDTHS[self as usize][code as usize])
|
||||
}
|
||||
|
||||
/// The advance of `code` at `size` points.
|
||||
|
|
@ -117,10 +101,298 @@ impl Std14 {
|
|||
|
||||
/// The `/BaseFont` name for this font, as it is written into a PDF.
|
||||
pub fn base_font(self) -> &'static str {
|
||||
crate::std14_metrics::BASE_FONTS[self as usize]
|
||||
BASE_FONTS[self as usize]
|
||||
}
|
||||
}
|
||||
|
||||
static BASE_FONTS: [&str; 14] = [
|
||||
"Helvetica",
|
||||
"Helvetica-Bold",
|
||||
"Helvetica-Oblique",
|
||||
"Helvetica-BoldOblique",
|
||||
"Times-Roman",
|
||||
"Times-Bold",
|
||||
"Times-Italic",
|
||||
"Times-BoldItalic",
|
||||
"Courier",
|
||||
"Courier-Bold",
|
||||
"Courier-Oblique",
|
||||
"Courier-BoldOblique",
|
||||
"Symbol",
|
||||
"ZapfDingbats",
|
||||
];
|
||||
|
||||
/// Advance widths by code, one row per [`Std14`] variant in declaration order.
|
||||
#[rustfmt::skip]
|
||||
static WIDTHS: [[u16; 256]; 14] = [
|
||||
// Helvetica
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
|
||||
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
|
||||
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
|
||||
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
|
||||
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
|
||||
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
||||
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
|
||||
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
||||
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
||||
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
||||
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
||||
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
|
||||
],
|
||||
// HelveticaBold
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
|
||||
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
|
||||
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
|
||||
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
|
||||
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
|
||||
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
||||
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
|
||||
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
||||
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
||||
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
||||
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
||||
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
|
||||
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
|
||||
],
|
||||
// HelveticaOblique
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
|
||||
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
|
||||
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
|
||||
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
|
||||
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
|
||||
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
||||
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
|
||||
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
||||
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
||||
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
||||
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
||||
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
|
||||
],
|
||||
// HelveticaBoldOblique
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
|
||||
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
|
||||
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
|
||||
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
|
||||
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
|
||||
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
||||
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
|
||||
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
||||
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
||||
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
||||
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
||||
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
|
||||
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
|
||||
],
|
||||
// TimesRoman
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444,
|
||||
921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722,
|
||||
556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500,
|
||||
333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500,
|
||||
500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541, 0,
|
||||
500, 0, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 0, 611, 0,
|
||||
0, 333, 333, 444, 444, 350, 500, 1000, 333, 980, 389, 333, 722, 0, 444, 722,
|
||||
250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333,
|
||||
400, 564, 300, 300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444,
|
||||
722, 722, 722, 722, 722, 722, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
|
||||
722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722, 722, 556, 500,
|
||||
444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500,
|
||||
],
|
||||
// TimesBold
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
|
||||
930, 722, 667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778,
|
||||
611, 778, 722, 556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500,
|
||||
333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500,
|
||||
556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520, 0,
|
||||
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 0, 667, 0,
|
||||
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 444, 722,
|
||||
250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333,
|
||||
400, 570, 300, 300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500,
|
||||
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 389, 389, 389, 389,
|
||||
722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722, 722, 722, 611, 556,
|
||||
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
|
||||
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500,
|
||||
],
|
||||
// TimesItalic
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500,
|
||||
920, 611, 611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722,
|
||||
611, 722, 611, 500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500,
|
||||
333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500,
|
||||
500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541, 0,
|
||||
500, 0, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 0, 556, 0,
|
||||
0, 333, 333, 556, 556, 350, 500, 889, 333, 980, 389, 333, 667, 0, 389, 556,
|
||||
250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333,
|
||||
400, 675, 300, 300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500,
|
||||
611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
|
||||
722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556, 611, 500,
|
||||
500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444,
|
||||
],
|
||||
// TimesBoldItalic
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
|
||||
832, 667, 667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722,
|
||||
611, 722, 667, 556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500,
|
||||
333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500,
|
||||
500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570, 0,
|
||||
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 0, 611, 0,
|
||||
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 389, 611,
|
||||
250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333,
|
||||
400, 570, 300, 300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500,
|
||||
667, 667, 667, 667, 667, 667, 944, 667, 667, 667, 667, 667, 389, 389, 389, 389,
|
||||
722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722, 722, 611, 611, 500,
|
||||
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
|
||||
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444,
|
||||
],
|
||||
// Courier
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
|
||||
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
|
||||
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
],
|
||||
// CourierBold
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
|
||||
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
|
||||
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
],
|
||||
// CourierOblique
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
|
||||
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
|
||||
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
],
|
||||
// CourierBoldOblique
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
|
||||
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
|
||||
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
],
|
||||
// Symbol
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444,
|
||||
549, 722, 667, 722, 612, 611, 763, 603, 722, 333, 631, 722, 686, 889, 722, 722,
|
||||
768, 741, 556, 592, 611, 690, 439, 768, 645, 795, 611, 333, 863, 333, 658, 500,
|
||||
500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549, 549, 576, 521, 549,
|
||||
549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603,
|
||||
400, 549, 411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658,
|
||||
823, 686, 795, 987, 768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713,
|
||||
768, 713, 790, 790, 890, 823, 549, 250, 713, 603, 603, 1042, 987, 603, 987, 603,
|
||||
494, 329, 790, 790, 786, 713, 384, 384, 384, 384, 384, 384, 494, 494, 494, 494,
|
||||
0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494, 494, 494, 0,
|
||||
],
|
||||
// ZapfDingbats
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933,
|
||||
911, 945, 974, 755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537,
|
||||
577, 692, 786, 788, 788, 790, 793, 794, 816, 823, 789, 841, 823, 833, 816, 831,
|
||||
923, 744, 723, 749, 790, 792, 695, 776, 768, 792, 759, 707, 708, 682, 701, 826,
|
||||
815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785, 791, 873, 761, 762,
|
||||
762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668, 0,
|
||||
390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788, 788, 788, 788,
|
||||
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
|
||||
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
|
||||
788, 788, 788, 788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834,
|
||||
873, 828, 924, 924, 917, 930, 931, 463, 883, 836, 836, 867, 867, 696, 696, 874,
|
||||
0, 874, 760, 946, 771, 865, 771, 888, 967, 888, 831, 873, 927, 970, 918, 0,
|
||||
],
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1,300 +0,0 @@
|
|||
//! The generated standard-14 tables: the fourteen base font names and their per-code advance
|
||||
//! widths. **Data only, no logic** -- see `std14.rs` for everything that interprets it.
|
||||
//!
|
||||
//! This file is the closed-list item in AGENTS.md 1.1 and carries a SHA-256 in `ATTRIBUTION.toml`,
|
||||
//! so `cargo xtask assets` fails until a change to it is re-attested. It is kept separate from the
|
||||
//! code that reads it for exactly that reason: if ordinary edits churned the hash, re-attesting
|
||||
//! would become reflexive and the check would stop meaning anything.
|
||||
//!
|
||||
//! Widths are in thousandths of an em, indexed by character code in each face's own encoding --
|
||||
//! WinAnsi for the twelve text faces, and their built-in encodings for Symbol and ZapfDingbats.
|
||||
//! A zero means the encoding leaves that code undefined. Verified against the published
|
||||
//! specification by the tests in `std14.rs`, which type the expected values in by hand.
|
||||
|
||||
pub(crate) static BASE_FONTS: [&str; 14] = [
|
||||
"Helvetica",
|
||||
"Helvetica-Bold",
|
||||
"Helvetica-Oblique",
|
||||
"Helvetica-BoldOblique",
|
||||
"Times-Roman",
|
||||
"Times-Bold",
|
||||
"Times-Italic",
|
||||
"Times-BoldItalic",
|
||||
"Courier",
|
||||
"Courier-Bold",
|
||||
"Courier-Oblique",
|
||||
"Courier-BoldOblique",
|
||||
"Symbol",
|
||||
"ZapfDingbats",
|
||||
];
|
||||
|
||||
/// Advance widths by code, one row per [`Std14`] variant in declaration order.
|
||||
#[rustfmt::skip]
|
||||
pub(crate) static WIDTHS: [[u16; 256]; 14] = [
|
||||
// Helvetica
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
|
||||
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
|
||||
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
|
||||
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
|
||||
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
|
||||
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
||||
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
|
||||
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
||||
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
||||
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
||||
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
||||
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
|
||||
],
|
||||
// HelveticaBold
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
|
||||
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
|
||||
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
|
||||
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
|
||||
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
|
||||
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
||||
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
|
||||
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
||||
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
||||
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
||||
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
||||
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
|
||||
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
|
||||
],
|
||||
// HelveticaOblique
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
|
||||
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
|
||||
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
|
||||
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
|
||||
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
|
||||
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
||||
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
|
||||
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
||||
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
||||
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
||||
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
||||
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
|
||||
],
|
||||
// HelveticaBoldOblique
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
|
||||
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
|
||||
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
|
||||
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
|
||||
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
|
||||
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
|
||||
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
||||
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
|
||||
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
||||
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
||||
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
||||
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
||||
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
|
||||
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
|
||||
],
|
||||
// TimesRoman
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444,
|
||||
921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722,
|
||||
556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500,
|
||||
333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500,
|
||||
500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541, 0,
|
||||
500, 0, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 0, 611, 0,
|
||||
0, 333, 333, 444, 444, 350, 500, 1000, 333, 980, 389, 333, 722, 0, 444, 722,
|
||||
250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333,
|
||||
400, 564, 300, 300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444,
|
||||
722, 722, 722, 722, 722, 722, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
|
||||
722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722, 722, 556, 500,
|
||||
444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500,
|
||||
],
|
||||
// TimesBold
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
|
||||
930, 722, 667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778,
|
||||
611, 778, 722, 556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500,
|
||||
333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500,
|
||||
556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520, 0,
|
||||
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 0, 667, 0,
|
||||
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 444, 722,
|
||||
250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333,
|
||||
400, 570, 300, 300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500,
|
||||
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 389, 389, 389, 389,
|
||||
722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722, 722, 722, 611, 556,
|
||||
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
|
||||
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500,
|
||||
],
|
||||
// TimesItalic
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500,
|
||||
920, 611, 611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722,
|
||||
611, 722, 611, 500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500,
|
||||
333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500,
|
||||
500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541, 0,
|
||||
500, 0, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 0, 556, 0,
|
||||
0, 333, 333, 556, 556, 350, 500, 889, 333, 980, 389, 333, 667, 0, 389, 556,
|
||||
250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333,
|
||||
400, 675, 300, 300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500,
|
||||
611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
|
||||
722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556, 611, 500,
|
||||
500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444,
|
||||
],
|
||||
// TimesBoldItalic
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
|
||||
832, 667, 667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722,
|
||||
611, 722, 667, 556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500,
|
||||
333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500,
|
||||
500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570, 0,
|
||||
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 0, 611, 0,
|
||||
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 389, 611,
|
||||
250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333,
|
||||
400, 570, 300, 300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500,
|
||||
667, 667, 667, 667, 667, 667, 944, 667, 667, 667, 667, 667, 389, 389, 389, 389,
|
||||
722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722, 722, 611, 611, 500,
|
||||
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
|
||||
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444,
|
||||
],
|
||||
// Courier
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
|
||||
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
|
||||
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
],
|
||||
// CourierBold
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
|
||||
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
|
||||
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
],
|
||||
// CourierOblique
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
|
||||
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
|
||||
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
],
|
||||
// CourierBoldOblique
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
|
||||
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
|
||||
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
|
||||
],
|
||||
// Symbol
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278,
|
||||
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444,
|
||||
549, 722, 667, 722, 612, 611, 763, 603, 722, 333, 631, 722, 686, 889, 722, 722,
|
||||
768, 741, 556, 592, 611, 690, 439, 768, 645, 795, 611, 333, 863, 333, 658, 500,
|
||||
500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549, 549, 576, 521, 549,
|
||||
549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603,
|
||||
400, 549, 411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658,
|
||||
823, 686, 795, 987, 768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713,
|
||||
768, 713, 790, 790, 890, 823, 549, 250, 713, 603, 603, 1042, 987, 603, 987, 603,
|
||||
494, 329, 790, 790, 786, 713, 384, 384, 384, 384, 384, 384, 494, 494, 494, 494,
|
||||
0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494, 494, 494, 0,
|
||||
],
|
||||
// ZapfDingbats
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933,
|
||||
911, 945, 974, 755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537,
|
||||
577, 692, 786, 788, 788, 790, 793, 794, 816, 823, 789, 841, 823, 833, 816, 831,
|
||||
923, 744, 723, 749, 790, 792, 695, 776, 768, 792, 759, 707, 708, 682, 701, 826,
|
||||
815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785, 791, 873, 761, 762,
|
||||
762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668, 0,
|
||||
390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788, 788, 788, 788,
|
||||
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
|
||||
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
|
||||
788, 788, 788, 788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834,
|
||||
873, 828, 924, 924, 917, 930, 931, 463, 883, 836, 836, 867, 867, 696, 696, 874,
|
||||
0, 874, 760, 946, 771, 865, 771, 888, 967, 888, 831, 873, 927, 970, 918, 0,
|
||||
],
|
||||
];
|
||||
|
|
@ -71,7 +71,7 @@ this is not per-page paper-source selection.
|
|||
|
||||
**PDF scale** describes the source-to-sheet transform in the previewed print-ready
|
||||
PDF, not measured physical output. Fit reserves an 18 pt margin and can reduce even
|
||||
a matching page. On Windows craft-print sends the sheets as vectors with a PrintTicket
|
||||
for the sheet's exact paper and no scaling (#599's approach), validated by the driver:
|
||||
a job is refused, not shrunk, when the driver lacks the paper or would change it. Verify
|
||||
the driver's paper settings and physical calibration before relying on drawing scale.
|
||||
a matching page. The current Windows backend still rasterises and can substitute
|
||||
paper/shrink a job if its driver lacks the requested size. These controls do not
|
||||
add #599's strict PrintTicket validation or vector spooler. Verify the driver's
|
||||
paper/scaling settings and physical calibration before relying on drawing scale.
|
||||
|
|
|
|||
|
|
@ -365,13 +365,6 @@ pub struct FontInfo {
|
|||
pub embedded: bool,
|
||||
pub subset: bool,
|
||||
pub encoding: Option<String>,
|
||||
/// The face standing in for a program the document does not embed. `None` when the font is
|
||||
/// embedded (nothing stands in) or when no face was available at all -- `substitution` then
|
||||
/// says which of the two it is.
|
||||
pub substitute: Option<String>,
|
||||
/// Why a face was substituted, as a sentence for whoever has to act on it. `None` only when
|
||||
/// the document's own glyphs are being shown.
|
||||
pub substitution: Option<String>,
|
||||
}
|
||||
|
||||
/// Check that `bytes` still open as the same document: the parser reads them and the page
|
||||
|
|
@ -1297,38 +1290,20 @@ impl<'a> Inspector<'a> {
|
|||
_ => None,
|
||||
};
|
||||
// Embedded: FontFile/2/3 in the descriptor (for Type0, in the descendant font).
|
||||
let descriptor_of = |d: &'a Dictionary| d.get(b"FontDescriptor").ok().and_then(|o| self.dict(o));
|
||||
let descendant =
|
||||
f.get(b"DescendantFonts").ok().and_then(|o| self.resolve(o).as_array().ok()).and_then(|a| a.first()).and_then(|o| self.dict(o));
|
||||
let desc = descriptor_of(f).or_else(|| descendant.and_then(descriptor_of));
|
||||
let descriptor_of = |d: &Dictionary| d.get(b"FontDescriptor").ok().and_then(|o| self.dict(o)).cloned();
|
||||
let desc = descriptor_of(f).or_else(|| {
|
||||
f.get(b"DescendantFonts")
|
||||
.ok()
|
||||
.and_then(|o| self.resolve(o).as_array().ok())
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|o| self.dict(o))
|
||||
.and_then(descriptor_of)
|
||||
});
|
||||
let embedded =
|
||||
kind == "Type3" || desc.is_some_and(|d| d.get(b"FontFile").is_ok() || d.get(b"FontFile2").is_ok() || d.get(b"FontFile3").is_ok());
|
||||
// What the substitution chain needs beyond the name: the descriptor flags, and for
|
||||
// a composite font the script its descendant declares (ISO 32000-2 9.7.3).
|
||||
let flags = desc
|
||||
.and_then(|d| d.get(b"Flags").ok())
|
||||
.and_then(|o| self.resolve(o).as_i64().ok())
|
||||
.and_then(|n| u32::try_from(n).ok())
|
||||
.unwrap_or(0);
|
||||
let ordering =
|
||||
descendant.and_then(|d| d.get(b"CIDSystemInfo").ok()).and_then(|o| self.dict(o)).and_then(|c| self.text(c, b"Ordering"));
|
||||
let subset = base.len() > 7 && base.as_bytes()[6] == b'+' && base[..6].bytes().all(|b| b.is_ascii_uppercase());
|
||||
let name = if subset { base[7..].to_string() } else { base };
|
||||
let chosen = pdfcraft_fonts::fallback::substitute(&pdfcraft_fonts::fallback::Request {
|
||||
base_font: &name,
|
||||
embedded,
|
||||
flags,
|
||||
ordering: ordering.as_deref(),
|
||||
});
|
||||
let info = FontInfo {
|
||||
name,
|
||||
kind,
|
||||
embedded,
|
||||
subset,
|
||||
encoding,
|
||||
substitute: chosen.face_name(),
|
||||
substitution: (!chosen.why.is_faithful()).then(|| chosen.why.note().to_owned()),
|
||||
};
|
||||
let info = FontInfo { name, kind, embedded, subset, encoding };
|
||||
if seen.insert((info.name.clone(), info.kind.clone(), info.embedded)) {
|
||||
out.push(info);
|
||||
}
|
||||
|
|
@ -2006,20 +1981,7 @@ trailer << /Root 1 0 R >>
|
|||
let expected: &[u8] = if a.name == "notes.txt" { b"hello notes" } else { b"a,b,c" };
|
||||
assert_eq!(data, expected, "{}", a.name);
|
||||
}
|
||||
// A non-embedded /Helvetica is reported as standing in for itself: the standard-14
|
||||
// metrics are exact, so the substitution is named rather than left implicit.
|
||||
assert_eq!(
|
||||
info.fonts,
|
||||
vec![FontInfo {
|
||||
name: "Helvetica".into(),
|
||||
kind: "Type1".into(),
|
||||
embedded: false,
|
||||
subset: true,
|
||||
encoding: None,
|
||||
substitute: Some("Helvetica".into()),
|
||||
substitution: Some(pdfcraft_fonts::fallback::Why::NamedStandard14.note().into()),
|
||||
}]
|
||||
);
|
||||
assert_eq!(info.fonts, vec![FontInfo { name: "Helvetica".into(), kind: "Type1".into(), embedded: false, subset: true, encoding: None }]);
|
||||
}
|
||||
|
||||
const LAYERS: &[u8] = b"%PDF-1.7
|
||||
|
|
@ -2558,97 +2520,3 @@ mod version_tests {
|
|||
assert_eq!(super::version_label("Unknown"), "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod substitution_tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{FontInfo, inspect};
|
||||
|
||||
/// One page naming four fonts the document does not embed: a Chinese CID font (Adobe-GB1), a
|
||||
/// Japanese one (Adobe-Japan1), an unknown serif-flagged simple font, and an aliased
|
||||
/// standard-14 name.
|
||||
const NOT_EMBEDDED: &[u8] = b"%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 200 200] /Resources << /Font << /F1 4 0 R /F2 6 0 R /F3 8 0 R /F4 9 0 R /F5 11 0 R >> >> >> endobj
|
||||
4 0 obj << /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [5 0 R] >> endobj
|
||||
5 0 obj << /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> >> endobj
|
||||
6 0 obj << /Type /Font /Subtype /Type0 /BaseFont /KozMinPro-Regular /Encoding /UniJIS-UCS2-H /DescendantFonts [7 0 R] >> endobj
|
||||
7 0 obj << /Type /Font /Subtype /CIDFontType0 /BaseFont /KozMinPro-Regular /CIDSystemInfo << /Registry (Adobe) /Ordering (Japan1) /Supplement 4 >> >> endobj
|
||||
8 0 obj << /Type /Font /Subtype /TrueType /BaseFont /SomeHouseSerif /FontDescriptor 10 0 R >> endobj
|
||||
9 0 obj << /Type /Font /Subtype /TrueType /BaseFont /ArialMT >> endobj
|
||||
10 0 obj << /Type /FontDescriptor /FontName /SomeHouseSerif /Flags 34 >> endobj
|
||||
11 0 obj << /Type /Font /Subtype /TrueType /BaseFont /HouseGrotesk /FontDescriptor 12 0 R >> endobj
|
||||
12 0 obj << /Type /FontDescriptor /FontName /HouseGrotesk /Flags 33 >> endobj
|
||||
trailer << /Size 13 /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
fn font<'a>(fonts: &'a [FontInfo], name: &str) -> &'a FontInfo {
|
||||
fonts.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("{name} listed; got {:?}", fonts.iter().map(|f| &f.name).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_info_says_which_face_stands_in_and_why() {
|
||||
let info = inspect(Arc::new(NOT_EMBEDDED.to_vec()), None).expect("opens");
|
||||
|
||||
// The one that matters most to a user: a Chinese PDF that draws nothing. Before this,
|
||||
// doc_info said only `embedded: false` and left them to guess.
|
||||
let cjk = font(&info.fonts, "STSong-Light");
|
||||
assert!(!cjk.embedded);
|
||||
assert_eq!(cjk.substitute, None, "no licensed Chinese face, so nothing stands in");
|
||||
let why = cjk.substitution.as_deref().unwrap_or_default();
|
||||
assert!(why.contains("no licensed face") && why.contains("rather than the wrong glyphs"), "{why}");
|
||||
|
||||
// An aliased standard-14 name resolves to the real face, whose metrics are exact.
|
||||
let arial = font(&info.fonts, "ArialMT");
|
||||
assert_eq!(arial.substitute.as_deref(), Some("Helvetica"));
|
||||
assert!(arial.substitution.as_deref().unwrap_or_default().contains("standard 14"));
|
||||
|
||||
// A house font whose name merely contains "Serif" gets Times -- reported as the guess it
|
||||
// is, not as a standard-14 name, which would be untrue of a font nobody has heard of.
|
||||
let serif = font(&info.fonts, "SomeHouseSerif");
|
||||
assert_eq!(serif.substitute.as_deref(), Some("Times-Roman"));
|
||||
assert!(serif.substitution.as_deref().unwrap_or_default().contains("hints at one"), "{serif:?}");
|
||||
|
||||
// No hint in the name at all, so the descriptor flags decide: 33 = fixed pitch + nonsymbolic.
|
||||
let grotesk = font(&info.fonts, "HouseGrotesk");
|
||||
assert_eq!(grotesk.substitute.as_deref(), Some("Courier"));
|
||||
assert!(grotesk.substitution.as_deref().unwrap_or_default().contains("descriptor flags"), "{grotesk:?}");
|
||||
|
||||
// Japanese depends on the optional craft-fonts build input; either way it is reported,
|
||||
// and never as a Latin face.
|
||||
let jpan = font(&info.fonts, "KozMinPro-Regular");
|
||||
assert!(jpan.substitution.is_some(), "a substitution is always explained");
|
||||
match &jpan.substitute {
|
||||
Some(face) => {
|
||||
assert!(!face.starts_with("Helvetica") && !face.starts_with("Times"), "a Latin face is never a Japanese substitute: {face}")
|
||||
}
|
||||
None => assert!(jpan.substitution.as_deref().unwrap_or_default().contains("no licensed face")),
|
||||
}
|
||||
|
||||
// Nothing claims to be embedded, so every font here carries a reason.
|
||||
assert!(info.fonts.iter().all(|f| !f.embedded && f.substitution.is_some()), "{:?}", info.fonts);
|
||||
}
|
||||
|
||||
/// An embedded font is reported as faithful: nothing stands in, and there is nothing to explain.
|
||||
const EMBEDDED: &[u8] = b"%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 200 200] /Resources << /Font << /F1 4 0 R >> >> >> endobj
|
||||
4 0 obj << /Type /Font /Subtype /TrueType /BaseFont /ABCDEF+SomeHouseSans /FontDescriptor 5 0 R >> endobj
|
||||
5 0 obj << /Type /FontDescriptor /FontName /ABCDEF+SomeHouseSans /Flags 32 /FontFile2 6 0 R >> endobj
|
||||
6 0 obj << /Length 4 >> stream
|
||||
test
|
||||
endstream endobj
|
||||
trailer << /Size 7 /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
#[test]
|
||||
fn an_embedded_font_reports_no_substitution_at_all() {
|
||||
let info = inspect(Arc::new(EMBEDDED.to_vec()), None).expect("opens");
|
||||
let f = font(&info.fonts, "SomeHouseSans");
|
||||
assert!(f.embedded && f.subset, "{f:?}");
|
||||
assert_eq!((f.substitute.as_deref(), f.substitution.as_deref()), (None, None), "the document's own glyphs are shown");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,19 +98,15 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
if overflow && arrow(ui, "chevron-left", offset > 0.5, tl!("Scroll tabs left")) {
|
||||
scroll_to = Some((offset - (TAB_MIN_WIDTH + gap)).max(0.0));
|
||||
}
|
||||
// Keyed by document, not index: a tab dragged past the active one moves it without
|
||||
// the strip jumping to it.
|
||||
let state = (app.active_ids().map(|(_, id)| id), app.views.len(), app.combine_showing());
|
||||
let state = (app.active, app.views.len(), app.combine_showing());
|
||||
let changed = ui.data_mut(|data| {
|
||||
let id = ui.id().with("active_tab");
|
||||
let changed = data.get_temp::<(Option<pdfcraft_engine::DocId>, usize, bool)>(id) != Some(state);
|
||||
let changed = data.get_temp::<(Option<usize>, usize, bool)>(id) != Some(state);
|
||||
data.insert_temp(id, state);
|
||||
changed
|
||||
});
|
||||
let mut action = None;
|
||||
let mut close = None;
|
||||
// The pointer's x while a tab is dragged, for scrolling at the strip's edges.
|
||||
let mut drag_x = None;
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().always_scroll_the_only_direction = true;
|
||||
let mut area = egui::ScrollArea::horizontal().id_salt("document_tabs").max_width(tabs_width).auto_shrink([true, true]);
|
||||
|
|
@ -119,10 +115,6 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
let out = area.show(ui, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
// Each document tab's index and rect, and the one being dragged
|
||||
// (with whether it was just released).
|
||||
let mut slots = Vec::with_capacity(app.views.len());
|
||||
let mut dragged = None;
|
||||
for i in 0..app.views.len() {
|
||||
let Some(doc) = app.session.get(app.views[i].id) else { continue };
|
||||
let (name, dirty) = (doc.display_name(), doc.dirty);
|
||||
|
|
@ -141,12 +133,7 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
i,
|
||||
cap,
|
||||
tab_h,
|
||||
true,
|
||||
);
|
||||
slots.push((i, response.rect));
|
||||
if response.dragged() || response.drag_stopped() {
|
||||
dragged = Some((i, response.drag_stopped()));
|
||||
}
|
||||
if changed && app.active == Some(i) && !app.combine_showing() {
|
||||
response.scroll_to_me(Some(Align::Center));
|
||||
}
|
||||
|
|
@ -171,25 +158,6 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
app.active = Some(i);
|
||||
}
|
||||
}
|
||||
// Dragging a tab moves it (#1537): a line marks the gap it will land
|
||||
// in, and releasing the button drops it there.
|
||||
if let Some((from, released)) = dragged
|
||||
&& let Some(x) = ui.ctx().pointer_latest_pos().map(|p| p.x)
|
||||
{
|
||||
let to = drop_index(&slots, from, x);
|
||||
if released {
|
||||
if close.is_none() && action.is_none() {
|
||||
app.move_tab(from, to);
|
||||
}
|
||||
} else {
|
||||
drag_x = Some(x);
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
if let Some(marker) = drop_marker(&slots, from, to, ui.spacing().item_spacing.x) {
|
||||
let span = slots.first().map_or(ui.max_rect().y_range(), |(_, r)| r.y_range());
|
||||
ui.painter().vline(marker, span.shrink(4.0), Stroke::new(2.0, t.accent));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(i) = close
|
||||
&& let Some(view) = app.views.get(i)
|
||||
{
|
||||
|
|
@ -214,7 +182,6 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
usize::MAX,
|
||||
cap,
|
||||
tab_h,
|
||||
false,
|
||||
);
|
||||
if changed && app.combine_showing() {
|
||||
response.scroll_to_me(Some(Align::Center));
|
||||
|
|
@ -230,22 +197,6 @@ pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
let max = (out.content_size.x - out.inner_rect.width()).max(0.0);
|
||||
ui.data_mut(|d| d.insert_temp(scroll_id, (out.state.offset.x, max)));
|
||||
// A tab dragged to either edge of a strip that scrolls brings the hidden tabs in.
|
||||
if let Some(x) = drag_x
|
||||
&& max > 0.0
|
||||
{
|
||||
let step = if x < out.inner_rect.left() + TAB_DRAG_EDGE {
|
||||
-TAB_DRAG_STEP
|
||||
} else if x > out.inner_rect.right() - TAB_DRAG_EDGE {
|
||||
TAB_DRAG_STEP
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if step != 0.0 {
|
||||
ui.data_mut(|d| d.insert_temp(scroll_id.with("to"), (out.state.offset.x + step).clamp(0.0, max)));
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
});
|
||||
if overflow && arrow(ui, "chevron-right", offset < max_offset - 0.5, tl!("Scroll tabs right")) {
|
||||
scroll_to = Some((offset + TAB_MIN_WIDTH + gap).min(max_offset));
|
||||
|
|
@ -338,29 +289,6 @@ fn tabs_overflow(natural: &[f32], budget: f32, gap: f32, min: f32) -> bool {
|
|||
narrowest + gap * natural.len().saturating_sub(1) as f32 > budget
|
||||
}
|
||||
|
||||
/// How close to either end of a scrolling strip a dragged tab scrolls it, and how far per frame.
|
||||
const TAB_DRAG_EDGE: f32 = 24.0;
|
||||
const TAB_DRAG_STEP: f32 = 8.0;
|
||||
|
||||
/// Where the tab at `from` lands when dropped with the pointer at `x`: past every other tab
|
||||
/// whose centre is left of the pointer. `slots` are the tabs' indices and rects, in order.
|
||||
fn drop_index(slots: &[(usize, Rect)], from: usize, x: f32) -> usize {
|
||||
slots.iter().filter(|(i, r)| *i != from && r.center().x < x).count()
|
||||
}
|
||||
|
||||
/// The x of the line marking where the tab at `from` drops at `to`: in the gap before the other
|
||||
/// tab it lands in front of, or after the last. `None` when it would stay where it is.
|
||||
fn drop_marker(slots: &[(usize, Rect)], from: usize, to: usize, gap: f32) -> Option<f32> {
|
||||
if slots.iter().position(|(i, _)| *i == from) == Some(to) {
|
||||
return None;
|
||||
}
|
||||
let mut others = slots.iter().filter(|(i, _)| *i != from).map(|(_, r)| r);
|
||||
match to.checked_sub(1) {
|
||||
None => others.next().map(|r| r.left() - gap / 2.0),
|
||||
Some(before) => others.nth(before).map(|r| r.right() + gap / 2.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// A scroll arrow beside the tabs; dimmed (and inert) at its end. True when clicked.
|
||||
fn arrow(ui: &mut egui::Ui, icon: &str, enabled: bool, tip: &str) -> bool {
|
||||
ui.add_enabled_ui(enabled, |ui| icons::button(ui, icon, TAB_ARROW, false, tip)).inner.clicked()
|
||||
|
|
@ -415,7 +343,6 @@ fn tab(
|
|||
index: usize,
|
||||
cap: Option<f32>,
|
||||
height: f32,
|
||||
draggable: bool,
|
||||
) -> egui::Response {
|
||||
let font = theme::regular(13.0);
|
||||
let mut label = tab_name(name);
|
||||
|
|
@ -434,8 +361,7 @@ fn tab(
|
|||
let height = ui.available_height().max(height);
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, height), Sense::hover());
|
||||
let a11y = if dirty { format!("{name} (edited)") } else { name.to_string() };
|
||||
let sense = if draggable { Sense::click_and_drag() } else { Sense::click() };
|
||||
let resp = craft_ui::tabs::Tab::new(id, &a11y, active).sense(sense).focus_stroke(Stroke::new(1.0, t.accent)).show_at(ui, rect, |ui, resp| {
|
||||
let resp = craft_ui::tabs::Tab::new(id, &a11y, active).focus_stroke(Stroke::new(1.0, t.accent)).show_at(ui, rect, |ui, resp| {
|
||||
let press = theme::Press::track(ui, resp);
|
||||
let bg = if active {
|
||||
press.fill(t.chrome, t.chrome.lerp_to_gamma(t.hover, 0.65), t.hover)
|
||||
|
|
@ -824,41 +750,6 @@ mod tab_widths {
|
|||
assert_eq!(tab_cap(&[250.0; 3], 0.0, 4.0, 110.0), Some(110.0));
|
||||
}
|
||||
|
||||
/// Three 100-wide tabs, 4 apart, starting at x = 0.
|
||||
fn three_slots() -> Vec<(usize, egui::Rect)> {
|
||||
(0..3).map(|i| (i, egui::Rect::from_min_size(egui::pos2(i as f32 * 104.0, 0.0), egui::vec2(100.0, 30.0)))).collect()
|
||||
}
|
||||
|
||||
/// A dragged tab lands past every other tab whose centre the pointer has crossed.
|
||||
#[test]
|
||||
fn a_dragged_tab_drops_past_the_centres_it_crossed() {
|
||||
use super::drop_index;
|
||||
let slots = three_slots();
|
||||
// Centres at 50, 154 and 258.
|
||||
assert_eq!(drop_index(&slots, 0, 10.0), 0, "still over itself");
|
||||
assert_eq!(drop_index(&slots, 0, 160.0), 1, "past the middle tab's centre");
|
||||
assert_eq!(drop_index(&slots, 0, 900.0), 2, "past the end");
|
||||
assert_eq!(drop_index(&slots, 2, -50.0), 0, "before the start");
|
||||
assert_eq!(drop_index(&slots, 2, 100.0), 1, "between the first and middle centres");
|
||||
assert_eq!(drop_index(&[], 0, 100.0), 0);
|
||||
}
|
||||
|
||||
/// The drop line sits in the gap the tab will land in, and not at all where it already is.
|
||||
#[test]
|
||||
fn the_drop_line_marks_the_gap_and_hides_when_nothing_moves() {
|
||||
use super::drop_marker;
|
||||
let slots = three_slots();
|
||||
assert_eq!(drop_marker(&slots, 0, 0, 4.0), None);
|
||||
assert_eq!(drop_marker(&slots, 1, 1, 4.0), None);
|
||||
// The first tab dropped at the end: after the last tab.
|
||||
assert_eq!(drop_marker(&slots, 0, 2, 4.0), Some(310.0));
|
||||
// The first tab dropped between the middle and the last: in the gap between them.
|
||||
assert_eq!(drop_marker(&slots, 0, 1, 4.0), Some(206.0));
|
||||
// The last tab dropped at the start: before the first tab.
|
||||
assert_eq!(drop_marker(&slots, 2, 0, 4.0), Some(-2.0));
|
||||
assert_eq!(drop_marker(&[], 0, 1, 4.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mac_title_bar_centres_and_clears_the_traffic_lights() {
|
||||
// The lights are centred in the bar, and content keeps the same gap after them (#789).
|
||||
|
|
|
|||
|
|
@ -260,8 +260,6 @@ impl PdfCraftApp {
|
|||
}
|
||||
}
|
||||
"view.read_mode" => self.switch_mode(if self.mode == Mode::Read { Mode::AllTools } else { Mode::Read }, false),
|
||||
"view.next_tab" => self.step_tab(true),
|
||||
"view.previous_tab" => self.step_tab(false),
|
||||
"view.theme" => {
|
||||
let next = if self.theme == ThemeKind::Light { ThemePreference::Dark } else { ThemePreference::Light };
|
||||
self.set_theme_preference(next);
|
||||
|
|
@ -638,20 +636,6 @@ impl PdfCraftApp {
|
|||
true
|
||||
}
|
||||
|
||||
/// Shows the next tab (or the previous one) in the strip's order, wrapping around at either
|
||||
/// end (#1542). The Combine files tab, when open, comes after the documents. From Home the
|
||||
/// next tab is the first and the previous one the last.
|
||||
fn step_tab(&mut self, forward: bool) {
|
||||
let count = self.views.len() + usize::from(self.combine_tab.open);
|
||||
let current = if self.combine_showing() { Some(self.views.len()) } else { self.active };
|
||||
let Some(next) = tab_after(current, count, forward) else { return };
|
||||
if next < self.views.len() {
|
||||
self.active = Some(next);
|
||||
} else {
|
||||
self.open_combine_tab();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the registered keyboard shortcuts (more specific combinations first).
|
||||
pub(crate) fn registry_shortcuts(&mut self, ctx: &egui::Context) {
|
||||
use egui::{Key, KeyboardShortcut, Modifiers};
|
||||
|
|
@ -701,24 +685,6 @@ impl PdfCraftApp {
|
|||
}
|
||||
}
|
||||
|
||||
/// The tab after `current` of `count` (before it when `forward` is false), wrapping around. From
|
||||
/// no tab, forward is the first and back the last. `None` when there are no tabs.
|
||||
fn tab_after(current: Option<usize>, count: usize, forward: bool) -> Option<usize> {
|
||||
let last = count.checked_sub(1)?;
|
||||
Some(match (current.filter(|&i| i <= last), forward) {
|
||||
(None, true) => 0,
|
||||
(None, false) => last,
|
||||
(Some(i), true) => {
|
||||
if i == last {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
(Some(i), false) => i.checked_sub(1).unwrap_or(last),
|
||||
})
|
||||
}
|
||||
|
||||
/// Render a top-level menu's registered commands (with live labels, shortcuts and enablement).
|
||||
pub(crate) fn registry_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui, menu: &str) {
|
||||
for spec in commands::menu(menu) {
|
||||
|
|
@ -795,20 +761,4 @@ mod tests {
|
|||
assert_eq!(command_modifier_label(&ctx), "⌘");
|
||||
crate::i18n::set_current(previous);
|
||||
}
|
||||
|
||||
/// Next and previous tab step one place and wrap around at either end; from no tab they go
|
||||
/// to the first or the last (#1542).
|
||||
#[test]
|
||||
fn tab_steps_wrap_around_and_start_from_either_end() {
|
||||
assert_eq!(tab_after(Some(0), 3, true), Some(1));
|
||||
assert_eq!(tab_after(Some(2), 3, true), Some(0), "the last wraps to the first");
|
||||
assert_eq!(tab_after(Some(1), 3, false), Some(0));
|
||||
assert_eq!(tab_after(Some(0), 3, false), Some(2), "the first wraps to the last");
|
||||
assert_eq!(tab_after(None, 3, true), Some(0));
|
||||
assert_eq!(tab_after(None, 3, false), Some(2));
|
||||
assert_eq!(tab_after(Some(0), 1, true), Some(0), "a single tab stays");
|
||||
assert_eq!(tab_after(Some(7), 3, true), Some(0), "a stale index starts over");
|
||||
assert_eq!(tab_after(None, 0, true), None);
|
||||
assert_eq!(tab_after(Some(0), 0, false), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,8 +80,6 @@
|
|||
Column select تحديد عمود
|
||||
Full screen mode وضع ملء الشاشة
|
||||
Read mode وضع القراءة
|
||||
Next tab علامة التبويب التالية
|
||||
Previous tab علامة التبويب السابقة
|
||||
Switch light / dark theme التبديل بين السمة الفاتحة والداكنة
|
||||
Comments panel لوحة التعليقات
|
||||
Form fields panel لوحة حقول النموذج
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -116,8 +116,6 @@
|
|||
Column select Избор на колона
|
||||
Full screen mode Цял екран
|
||||
Read mode Режим на четене
|
||||
Next tab Следващ раздел
|
||||
Previous tab Предишен раздел
|
||||
Switch light / dark theme Превключи светла / тъмна тема
|
||||
Comments panel Панел с коментари
|
||||
Form fields panel Панел с полета на формуляра
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 8 and column 16.
|
|
|
@ -93,8 +93,6 @@
|
|||
Marquee zoom Zvětšit vybranou oblast
|
||||
Full screen mode Režim celé obrazovky
|
||||
Read mode Režim čtení
|
||||
Next tab Další karta
|
||||
Previous tab Předchozí karta
|
||||
Switch light / dark theme Přepnout světlý / tmavý motiv
|
||||
Comments panel Panel komentářů
|
||||
Form fields panel Panel formulářových polí
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -51,8 +51,6 @@
|
|||
Column select Spaltenauswahl
|
||||
Full screen mode Vollbildmodus
|
||||
Read mode Lesemodus
|
||||
Next tab Nächster Tab
|
||||
Previous tab Vorheriger Tab
|
||||
Switch light / dark theme Helles / dunkles Design umschalten
|
||||
Comments panel Kommentarbereich
|
||||
Form fields panel Formularfeldbereich
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
|
|
@ -65,8 +65,6 @@
|
|||
Column select Επιλογή στήλης
|
||||
Full screen mode Λειτουργία πλήρους οθόνης
|
||||
Read mode Λειτουργία ανάγνωσης
|
||||
Next tab Επόμενη καρτέλα
|
||||
Previous tab Προηγούμενη καρτέλα
|
||||
Switch light / dark theme Εναλλαγή φωτεινού / σκοτεινού θέματος
|
||||
Comments panel Πίνακας σχολίων
|
||||
Form fields panel Πίνακας πεδίων φόρμας
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 5.
|
|
|
@ -74,8 +74,6 @@
|
|||
Column select Selección de columna
|
||||
Full screen mode Modo de pantalla completa
|
||||
Read mode Modo de lectura
|
||||
Next tab Pestaña siguiente
|
||||
Previous tab Pestaña anterior
|
||||
Switch light / dark theme Cambiar entre tema claro y oscuro
|
||||
Comments panel Panel de comentarios
|
||||
Form fields panel Panel de campos de formulario
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
|
|
@ -51,8 +51,6 @@
|
|||
Column select Sélection de colonne
|
||||
Full screen mode Mode plein écran
|
||||
Read mode Mode lecture
|
||||
Next tab Onglet suivant
|
||||
Previous tab Onglet précédent
|
||||
Switch light / dark theme Basculer entre les thèmes clair et sombre
|
||||
Comments panel Panneau des commentaires
|
||||
Form fields panel Panneau des champs de formulaire
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
|
|
@ -51,8 +51,6 @@
|
|||
Column select Oszlopkijelölés
|
||||
Full screen mode Teljes képernyő
|
||||
Read mode Olvasási mód
|
||||
Next tab Következő lap
|
||||
Previous tab Előző lap
|
||||
Switch light / dark theme Világos / sötét téma váltása
|
||||
Comments panel Megjegyzések panel
|
||||
Form fields panel Űrlapmezők panel
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 7 and column 44.
|
|
|
@ -77,8 +77,6 @@
|
|||
Column select Selezione colonna
|
||||
Full screen mode Modalità schermo intero
|
||||
Read mode Modalità lettura
|
||||
Next tab Scheda successiva
|
||||
Previous tab Scheda precedente
|
||||
Switch light / dark theme Alterna tema chiaro/scuro
|
||||
Comments panel Pannello commenti
|
||||
Form fields panel Pannello campi modulo
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -80,8 +80,6 @@
|
|||
Column select 列の選択
|
||||
Full screen mode 全画面表示
|
||||
Read mode 閲覧モード
|
||||
Next tab 次のタブ
|
||||
Previous tab 前のタブ
|
||||
Switch light / dark theme 明るい/暗いテーマを切り替え
|
||||
Comments panel コメントパネル
|
||||
Form fields panel フォームフィールドパネル
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -54,8 +54,6 @@
|
|||
Take a snapshot Суретке түсіру
|
||||
Full screen mode Толық экран режимі
|
||||
Read mode Оқу режимі
|
||||
Next tab Келесі қойынды
|
||||
Previous tab Алдыңғы қойынды
|
||||
Switch light / dark theme Ашық / қараңғы теманы ауыстыру
|
||||
Comments panel Түсініктемелер панелі
|
||||
Form fields panel Форма өрістері панелі
|
||||
|
|
|
|||
|
|
|
@ -78,8 +78,6 @@
|
|||
Default zoom Numatytasis mastelis
|
||||
Full screen mode Viso ekrano veiksena
|
||||
Read mode Skaitymo veiksena
|
||||
Next tab Kita kortelė
|
||||
Previous tab Ankstesnė kortelė
|
||||
Comments Komentarai
|
||||
Comment Komentaras
|
||||
Add comment Pridėti komentarą
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -76,8 +76,6 @@
|
|||
Take a snapshot Zrób migawkę
|
||||
Full screen mode Tryb pełnoekranowy
|
||||
Read mode Tryb czytania
|
||||
Next tab Następna karta
|
||||
Previous tab Poprzednia karta
|
||||
Switch light / dark theme Przełącz jasny / ciemny motyw
|
||||
Comments panel Panel komentarzy
|
||||
Form fields panel Panel pól formularza
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -58,8 +58,6 @@
|
|||
Column select Seleção de coluna
|
||||
Full screen mode Modo de tela cheia
|
||||
Read mode Modo de leitura
|
||||
Next tab Próxima aba
|
||||
Previous tab Aba anterior
|
||||
Switch light / dark theme Alternar tema claro / escuro
|
||||
Comments panel Painel de comentários
|
||||
Form fields panel Painel de campos de formulário
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -71,8 +71,6 @@
|
|||
Column select Выделение столбца
|
||||
Full screen mode Полноэкранный режим
|
||||
Read mode Режим чтения
|
||||
Next tab Следующая вкладка
|
||||
Previous tab Предыдущая вкладка
|
||||
Switch light / dark theme Переключить светлую / тёмную тему
|
||||
Comments panel Панель комментариев
|
||||
Form fields panel Панель полей формы
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -1393,8 +1393,6 @@
|
|||
Read Branje
|
||||
Read Only Samo za branje
|
||||
Read mode Način branja
|
||||
Next tab Naslednji zavihek
|
||||
Previous tab Prejšnji zavihek
|
||||
Ready Omogočeno
|
||||
Real name Pravo ime
|
||||
Reason Razlog
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -57,8 +57,6 @@ comment menu Edit సవరించు
|
|||
Column select నిలువు వరుస ఎంపిక
|
||||
Full screen mode పూర్తి స్క్రీన్ మోడ్
|
||||
Read mode చదివే మోడ్
|
||||
Next tab తదుపరి ట్యాబ్
|
||||
Previous tab మునుపటి ట్యాబ్
|
||||
Switch light / dark theme లేత / ముదురు థీమ్ల మధ్య మారు
|
||||
Comments panel వ్యాఖ్యల ప్యానెల్
|
||||
Form fields panel ఫారం ఫీల్డ్ల ప్యానెల్
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 13 and column 16.
|
|
|
@ -42,8 +42,6 @@
|
|||
Take a snapshot Anlık görüntü al
|
||||
Full screen mode Tam ekran modu
|
||||
Read mode Okuma modu
|
||||
Next tab Sonraki sekme
|
||||
Previous tab Önceki sekme
|
||||
Switch light / dark theme Açık / koyu temaya geç
|
||||
Comments panel Yorumlar paneli
|
||||
Form fields panel Form alanları paneli
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 7.
|
|
|
@ -78,8 +78,6 @@
|
|||
Column select Виділення стовпця
|
||||
Full screen mode Повноекранний режим
|
||||
Read mode Режим читання
|
||||
Next tab Наступна вкладка
|
||||
Previous tab Попередня вкладка
|
||||
Switch light / dark theme Перемкнути світлу / темну тему
|
||||
Comments panel Панель коментарів
|
||||
Form fields panel Панель полів форми
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -122,8 +122,6 @@
|
|||
Column select 列选择
|
||||
Full screen mode 全屏模式
|
||||
Read mode 阅读模式
|
||||
Next tab 下一个标签页
|
||||
Previous tab 上一个标签页
|
||||
Switch light / dark theme 切换浅色/深色主题
|
||||
Comments panel 注释面板
|
||||
Form fields panel 表单域面板
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -81,8 +81,6 @@
|
|||
Column select 欄選取
|
||||
Full screen mode 全螢幕模式
|
||||
Read mode 閱讀模式
|
||||
Next tab 下一個分頁
|
||||
Previous tab 上一個分頁
|
||||
Switch light / dark theme 切換淺色/深色主題
|
||||
Comments panel 註解面板
|
||||
Form fields panel 表單欄位面板
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
|
|
@ -1800,20 +1800,6 @@ impl PdfCraftApp {
|
|||
};
|
||||
}
|
||||
|
||||
/// Moves the tab at `from` so it sits at `to` (#1537). The same document stays active
|
||||
/// wherever it lands; an index out of range changes nothing.
|
||||
pub fn move_tab(&mut self, from: usize, to: usize) {
|
||||
if from >= self.views.len() || to >= self.views.len() || from == to {
|
||||
return;
|
||||
}
|
||||
let active = self.active_ids().map(|(_, id)| id);
|
||||
let view = self.views.remove(from);
|
||||
self.views.insert(to, view);
|
||||
if let Some(id) = active {
|
||||
self.active = self.views.iter().position(|v| v.id == id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_ids(&self) -> Option<(usize, DocId)> {
|
||||
self.active.and_then(|i| self.views.get(i).map(|v| (i, v.id)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -346,63 +346,3 @@ fn startup_intake_waits_through_wrong_password_retry_and_cancel_preserves_the_ex
|
|||
assert_eq!(app.session.docs().iter().find(|doc| doc.name == "startup.pdf").expect("the startup document is available").info.pages.len(), 1);
|
||||
assert_eq!(app.session.get(existing).expect("the existing document remains open").bytes, original);
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
|
||||
fn assert_tool_runs_after_background_open(extension: &str, bytes: Vec<u8>, pages: usize) {
|
||||
let (mut harness, _client) = harness();
|
||||
harness.state_mut().close_tab(0);
|
||||
let (gate, started, release, finished) = open_worker::prepare_gate();
|
||||
harness.state_mut().open_prepare_gate = Some(gate);
|
||||
let path = std::env::temp_dir().join(format!("pdfcraft-tool-worker-{}.{extension}", std::process::id()));
|
||||
std::fs::write(&path, &bytes).expect("write picked document");
|
||||
harness.state_mut().pick_override = Some(vec![path.to_string_lossy().into_owned()]);
|
||||
harness.state_mut().run_command("export.text");
|
||||
harness.step();
|
||||
started.recv_timeout(Duration::from_secs(5)).expect("the picked document reached preparation");
|
||||
assert!(harness.state().active.is_none());
|
||||
assert!(harness.state().dialog.is_none(), "the tool waits for its document");
|
||||
release.send(()).expect("release picked document preparation");
|
||||
finished.recv_timeout(Duration::from_secs(5)).expect("picked document prepared");
|
||||
harness.step();
|
||||
let app = harness.state();
|
||||
assert_eq!(app.views.len(), 1, "the chosen document opens once");
|
||||
assert_eq!(app.session.get(app.views[0].id).expect("chosen document").info.pages.len(), pages);
|
||||
assert!(matches!(app.dialog, Some(crate::Dialog::Export(_))), "the chosen tool runs after preparation: {:?}", app.dialog);
|
||||
harness.run_steps(2);
|
||||
assert_eq!(harness.state().views.len(), 1);
|
||||
assert_eq!(std::fs::read(&path).expect("read unchanged input"), bytes);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_from_home_runs_after_pdf_preparation() {
|
||||
assert_tool_runs_after_background_open("pdf", fixture(2), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_from_home_runs_after_text_conversion() {
|
||||
assert_tool_runs_after_background_open("txt", b"Synthetic document for a chosen tool".to_vec(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
|
||||
fn a_startup_document_waits_for_an_active_document_to_prepare() {
|
||||
let (mut harness, _client) = harness();
|
||||
harness.state_mut().close_tab(0);
|
||||
let (gate, started, release, finished) = open_worker::prepare_gate();
|
||||
harness.state_mut().open_prepare_gate = Some(gate);
|
||||
let path = std::env::temp_dir().join(format!("pdfcraft-startup-worker-{}.pdf", std::process::id()));
|
||||
std::fs::write(&path, fixture(2)).expect("write chosen document");
|
||||
harness.state_mut().open_path(&path.to_string_lossy());
|
||||
started.recv_timeout(Duration::from_secs(5)).expect("chosen document reached preparation");
|
||||
harness.state().startup_inbox.lock().expect("startup inbox").push(("startup.pdf".into(), fixture(1)));
|
||||
harness.step();
|
||||
assert_eq!(harness.state().startup_inbox.lock().expect("startup inbox").len(), 1, "startup bytes wait for the pending active open");
|
||||
assert!(harness.state().views.is_empty());
|
||||
release.send(()).expect("release chosen document preparation");
|
||||
finished.recv_timeout(Duration::from_secs(5)).expect("chosen document prepared");
|
||||
harness.step();
|
||||
assert_eq!(harness.state().views.len(), 2);
|
||||
assert_eq!(harness.state().active, Some(0), "the chosen document keeps focus");
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ impl PrintDraft {
|
|||
}
|
||||
|
||||
/// Lists the printers off the UI thread (the desktop app sets [`PdfCraftApp::printer_source`]):
|
||||
/// on Windows each driver's capabilities are read for its two-sided support, and CUPS' driverless discovery can wait on
|
||||
/// on Windows that takes PowerShell a second or more, and CUPS' driverless discovery can wait on
|
||||
/// the network. Without a source the dialog reads them as it opens, so tests stay deterministic.
|
||||
pub type PrinterSource = std::sync::Arc<dyn Fn() -> Vec<spool::Printer> + Send + Sync>;
|
||||
|
||||
|
|
|
|||
|
|
@ -590,45 +590,3 @@ fn palette_ime_outside_click_closes_without_running_a_command() {
|
|||
assert!(h.state().palette_query.is_empty());
|
||||
assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().info.pages[0].rotation, 0);
|
||||
}
|
||||
|
||||
/// Ctrl+Tab and Ctrl+Shift+Tab step through the tabs in the strip's order, wrap around at either
|
||||
/// end and include the Combine files tab after the documents (#1542). A Windows or Linux Ctrl
|
||||
/// press is both `ctrl` and `command`.
|
||||
#[test]
|
||||
fn ctrl_tab_steps_through_the_tabs_in_order_and_wraps_around() {
|
||||
let mut h = harness();
|
||||
h.state_mut().open_bytes("second.pdf", None, fixture(1)).unwrap();
|
||||
h.state_mut().open_bytes("third.pdf", None, fixture(1)).unwrap();
|
||||
h.run_steps(3);
|
||||
let ctrl = Modifiers::CTRL | Modifiers::COMMAND;
|
||||
let press = |h: &mut Harness<'static, PdfCraftApp>, modifiers: Modifiers| {
|
||||
h.key_press_modifiers(modifiers, Key::Tab);
|
||||
h.run_steps(2);
|
||||
h.state().active
|
||||
};
|
||||
assert_eq!(h.state().active, Some(2));
|
||||
assert_eq!(press(&mut h, ctrl), Some(0), "the last tab wraps to the first");
|
||||
assert_eq!(press(&mut h, ctrl), Some(1));
|
||||
assert_eq!(press(&mut h, ctrl | Modifiers::SHIFT), Some(0));
|
||||
assert_eq!(press(&mut h, ctrl | Modifiers::SHIFT), Some(2), "the first tab wraps to the last");
|
||||
// The order is the one on screen, after the tabs are moved.
|
||||
let ids: Vec<_> = h.state().views.iter().map(|v| v.id).collect();
|
||||
h.state_mut().move_tab(2, 0);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(ids[2]));
|
||||
press(&mut h, ctrl);
|
||||
assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(ids[0]));
|
||||
// Combine files comes after the documents.
|
||||
h.state_mut().execute("page.combine");
|
||||
h.run_steps(2);
|
||||
assert!(h.state().combine_showing());
|
||||
assert_eq!(press(&mut h, ctrl), Some(0), "Combine files wraps to the first document");
|
||||
assert_eq!(press(&mut h, ctrl | Modifiers::SHIFT), None);
|
||||
assert!(h.state().combine_showing(), "back from the first document is Combine files");
|
||||
// Plain Tab still belongs to keyboard focus, not to the tabs.
|
||||
press(&mut h, Modifiers::NONE);
|
||||
assert!(h.state().combine_showing());
|
||||
for doc in h.state().session.docs() {
|
||||
assert!(!doc.dirty, "switching tabs changes no document");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1186,74 +1186,3 @@ fn combine_tab_uses_tab_interaction_without_changing_close_or_document_state() {
|
|||
assert!(h.query_by_role_and_label(Role::Tab, "Combine files").is_none());
|
||||
assert_eq!(h.state().views.iter().map(|v| v.id).collect::<Vec<_>>(), document_ids);
|
||||
}
|
||||
|
||||
/// Presses the named tab, drags it to `to` in a few steps and, unless `release` is false, lets go.
|
||||
fn drag_named_tab(h: &mut Harness<'static, PdfCraftApp>, name: &str, to: egui::Pos2, release: bool) {
|
||||
let from = h.get_by_role_and_label(egui::accesskit::Role::Tab, name).rect().center();
|
||||
h.event(egui::Event::PointerMoved(from));
|
||||
h.event(egui::Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
for k in 1..=6 {
|
||||
h.event(egui::Event::PointerMoved(from.lerp(to, k as f32 / 6.0)));
|
||||
h.run_steps(1);
|
||||
}
|
||||
if release {
|
||||
h.event(egui::Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_tab_moves_it_and_keeps_the_same_document_active() {
|
||||
use egui::accesskit::Role;
|
||||
let mut h = three_tab_harness();
|
||||
let ids: Vec<_> = h.state().views.iter().map(|view| view.id).collect();
|
||||
let tab = |h: &Harness<'static, PdfCraftApp>, name: &str| h.get_by_role_and_label(Role::Tab, name).rect();
|
||||
// first.pdf past the end; last.pdf, the active one, keeps the focus at its new place.
|
||||
let to = tab(&h, "last.pdf").right_center() + egui::vec2(20.0, 0.0);
|
||||
drag_named_tab(&mut h, "first.pdf", to, true);
|
||||
assert_eq!(h.state().views.iter().map(|view| view.id).collect::<Vec<_>>(), [ids[1], ids[2], ids[0]]);
|
||||
assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(ids[2]));
|
||||
assert!(tab(&h, "first.pdf").left() > tab(&h, "last.pdf").right(), "the strip draws the new order");
|
||||
// last.pdf, now in the middle, to the front.
|
||||
let to = tab(&h, "middle.pdf").left_center() - egui::vec2(10.0, 0.0);
|
||||
drag_named_tab(&mut h, "last.pdf", to, true);
|
||||
assert_eq!(h.state().views.iter().map(|view| view.id).collect::<Vec<_>>(), [ids[2], ids[1], ids[0]]);
|
||||
assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(ids[2]));
|
||||
assert_eq!(h.get_by_role_and_label(Role::Tab, "last.pdf").accesskit_node().is_selected(), Some(true));
|
||||
// Moving tabs touches no document.
|
||||
assert_eq!(h.state().session.docs().len(), 3);
|
||||
for doc in h.state().session.docs() {
|
||||
assert_eq!(doc.bytes.as_slice(), FIXTURE);
|
||||
assert!(!doc.dirty);
|
||||
}
|
||||
if let Ok(dir) = std::env::var("PDFCRAFT_TAB_SHOTS") {
|
||||
let to = tab(&h, "first.pdf").left_center() - egui::vec2(10.0, 0.0);
|
||||
drag_named_tab(&mut h, "last.pdf", to, false);
|
||||
h.render().unwrap().save(format!("{dir}/dragging-tab.png")).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tab_dropped_where_it_was_stays_and_a_click_still_selects_it() {
|
||||
let mut h = three_tab_harness();
|
||||
let ids: Vec<_> = h.state().views.iter().map(|view| view.id).collect();
|
||||
let middle = h.get_by_role_and_label(egui::accesskit::Role::Tab, "middle.pdf").rect();
|
||||
drag_named_tab(&mut h, "middle.pdf", middle.center() + egui::vec2(30.0, 0.0), true);
|
||||
assert_eq!(h.state().views.iter().map(|view| view.id).collect::<Vec<_>>(), ids);
|
||||
h.get_by_label("first.pdf").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(ids[0]));
|
||||
assert_eq!(h.state().views.iter().map(|view| view.id).collect::<Vec<_>>(), ids);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_combine_tab_does_not_move_and_documents_stay_in_order() {
|
||||
let mut h = three_tab_harness();
|
||||
let ids: Vec<_> = h.state().views.iter().map(|view| view.id).collect();
|
||||
h.state_mut().execute("page.combine");
|
||||
h.run_steps(3);
|
||||
let first = h.get_by_role_and_label(egui::accesskit::Role::Tab, "first.pdf").rect();
|
||||
drag_named_tab(&mut h, "Combine files", first.left_center() - egui::vec2(10.0, 0.0), true);
|
||||
assert_eq!(h.state().views.iter().map(|view| view.id).collect::<Vec<_>>(), ids);
|
||||
}
|
||||
|
|
|
|||
17
deny.toml
17
deny.toml
|
|
@ -32,9 +32,8 @@ allow-wildcard-paths = true
|
|||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
# Shared UI components and spooler; Cargo.toml pins the immutable revision. craft-print pins
|
||||
# storytold's winprint fork (Windows printing, #1107) to an immutable revision of its own.
|
||||
allow-git = ["https://github.com/storytold/craft-libs", "https://github.com/storytold/winprint.rs"]
|
||||
# Shared UI components; Cargo.toml pins the immutable revision.
|
||||
allow-git = ["https://github.com/storytold/craft-libs"]
|
||||
|
||||
[advisories]
|
||||
yanked = "warn"
|
||||
|
|
@ -44,10 +43,6 @@ ignore = [
|
|||
# private-key operations use aws-lc-rs on native targets and are refused in the browser
|
||||
# (ADR-0009). Revisit when a fixed `rsa` ships.
|
||||
{ id = "RUSTSEC-2023-0071", reason = "rsa is used for verification only (ADR-0009)" },
|
||||
# proc-macro-error is unmaintained (no vulnerability). It reaches the tree as a proc macro of
|
||||
# winprint's fmt-derive (craft-print's Windows spooler), so it runs at compile time on Windows
|
||||
# and is not in the binary.
|
||||
{ id = "RUSTSEC-2024-0370", reason = "compile-time proc macro of winprint's fmt-derive (craft-print Windows printing, #1107)" },
|
||||
]
|
||||
|
||||
# egui's default UI fonts (Ubuntu-Light, Hack, emoji-icon-font): font licences on the AGENTS.md
|
||||
|
|
@ -55,11 +50,3 @@ ignore = [
|
|||
[[licenses.exceptions]]
|
||||
crate = "epaint_default_fonts"
|
||||
allow = ["OFL-1.1", "Ubuntu-font-1.0"]
|
||||
|
||||
# winprint (craft-print's Windows spooler, #1107) lists reqwest as a build-dependency for its
|
||||
# PDFium download, which only runs with its `pdfium` feature; craft-print turns that off. The CA
|
||||
# bundle reqwest brings is CDLA-Permissive-2.0 data and is compiled into winprint's build script
|
||||
# only, never into PdfCraft. Remove when winprint makes those build-dependencies optional.
|
||||
[[licenses.exceptions]]
|
||||
crate = "webpki-root-certs"
|
||||
allow = ["CDLA-Permissive-2.0"]
|
||||
|
|
|
|||
|
|
@ -1188,10 +1188,10 @@ area = "B"
|
|||
tier = "P1"
|
||||
milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = ["view.next_tab", "view.previous_tab"]
|
||||
commands = []
|
||||
tools = ["doc_list"]
|
||||
evidence = ["crates/ui-egui/tests/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/viewer.rs::each_document_keeps_its_own_scroll_position", "crates/ui-egui/tests/viewer.rs::a_new_document_opens_at_the_top_after_a_relaunch", "crates/ui-egui/tests/ui.rs::dragging_a_tab_moves_it_and_keeps_the_same_document_active", "crates/ui-egui/tests/ui.rs::a_tab_dropped_where_it_was_stays_and_a_click_still_selects_it", "crates/ui-egui/tests/ui.rs::the_combine_tab_does_not_move_and_documents_stay_in_order", "crates/ui-egui/tests/commands.rs::ctrl_tab_steps_through_the_tabs_in_order_and_wraps_around"]
|
||||
notes = "each tab keeps its own scroll position (#189), and a newly opened one starts at the top, not where an earlier session left the scroll. Document tabs are dragged to reorder them (#1537): a line marks where the tab will land, the strip scrolls at its edges when it overflows, and the same document stays active; the Combine files tab stays after the documents. Dragging a tab out into its own window is view.multiple-windows. Ctrl+Tab and Ctrl+Shift+Tab (⌃Tab and ⌃⇧Tab on macOS; View ▸ Next tab / Previous tab) step through the tabs in their order on screen, Combine files included, and wrap around at either end (#1542)."
|
||||
evidence = ["crates/ui-egui/tests/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/viewer.rs::each_document_keeps_its_own_scroll_position", "crates/ui-egui/tests/viewer.rs::a_new_document_opens_at_the_top_after_a_relaunch"]
|
||||
notes = "each tab keeps its own scroll position (#189), and a newly opened one starts at the top, not where an earlier session left the scroll"
|
||||
|
||||
[[feature]]
|
||||
id = "view.multiple-windows"
|
||||
|
|
@ -3436,8 +3436,8 @@ milestone = "M3"
|
|||
status = "shipped"
|
||||
commands = ["file.properties"]
|
||||
tools = ["doc_info"]
|
||||
evidence = ["crates/ui-egui/tests/view.rs::fonts_tab_lists_fonts_and_embedding", "crates/render/src/inspect.rs::doc_info_says_which_face_stands_in_and_why", "crates/render/src/inspect.rs::an_embedded_font_reports_no_substitution_at_all"]
|
||||
notes = "name, type, embedding, subset and encoding; doc_info also reports the face standing in for a program the document does not embed and why it was chosen (Acrobat's Actual Font), including the case where no licensed face exists so nothing is drawn. Still missing: the Fonts tab in the interface does not show the substituted face yet, only the data layer does"
|
||||
evidence = ["crates/ui-egui/tests/view.rs::fonts_tab_lists_fonts_and_embedding"]
|
||||
notes = ""
|
||||
|
||||
[[feature]]
|
||||
id = "organize.initial-view"
|
||||
|
|
@ -7925,7 +7925,7 @@ status = "shipped"
|
|||
commands = ["print.dialog"]
|
||||
tools = ["printers", "printer_options", "doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::driverless_printers_join_the_list_without_a_queue", "crates/print/src/tests.rs::windows_printers_carry_one_default_and_keep_their_names", "crates/print/src/tests.rs::printer_options_come_from_the_ppd", "crates/print/src/tests.rs::printer_option_defaults_and_job_arguments", "crates/ui-egui/tests/print.rs::print_dialog_shows_the_printer_driver_options", "crates/ui-egui/tests/print.rs::printers_listed_in_the_background_preselect_the_default_unless_one_was_picked", "crates/ui-egui/src/web_print.rs::resolution_falls_with_the_sheet_count_and_long_jobs_are_refused", "crates/ui-egui/src/web_print.rs::large_sheets_are_drawn_within_the_pixel_cap_and_bad_sizes_are_refused", "crates/ui-egui/src/web_print.rs::a_sink_error_or_a_page_that_isnt_there_stops_the_job"]
|
||||
notes = "CUPS queues via lpstat -p -d and driverless destinations without a queue via lpstat -e; on Windows shared craft-print lists the queues (EnumPrinters), the default (GetDefaultPrinter) and each driver's two-sided support in this process, and draws jobs as vectors with Windows.Data.Pdf into the XPS print API with a driver-validated PrintTicket (exact paper, orientation, no scaling; portrait and landscape sheets in one job via /Rotate), starting no PowerShell, so printing works under App Control script enforcement (#1107); the printer list and submission run on workers; Save as PDF on every platform. Properties…: on CUPS the driver's own PPD job options (tray, paper type, finishing …, not installable options or what the dialog sets) for this job, sent as lp -o; on Windows the driver's Printing Preferences window (printui), saved as that printer's per-user defaults rather than per job. On the web: \"Browser's print dialog\" (the default there) reaches the system's printers through the browser; PdfCraft draws the sheets itself (300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000; at most 64 megapixels a sheet and 1 GiB a job) and prints them from a hidden iframe, one per printed page, a landscape sheet among portrait ones turned onto the paper; copies, two-sided and colour are chosen in the browser's dialog; a browser that refuses to print gets the print-ready PDF as a download. Checked in headless Chromium, not yet in Firefox or Safari; pictures, not vectors; sheets of another paper size print at their own size only where the browser honours named @page sizes (Chromium doesn't)"
|
||||
notes = "CUPS queues via lpstat -p -d and driverless destinations without a queue via lpstat -e; on Windows the queues and the default come from Win32_Printer (PowerShell CIM) and jobs are rendered with Windows.Data.Pdf and spooled through shared craft-print at each sheet's physical size and orientation, with bounded raster resolution up to 600 dpi (not vector passthrough); the printer list and submission run on workers without a console window. Native Windows regression execution is pending; Save as PDF on every platform. Properties…: on CUPS the driver's own PPD job options (tray, paper type, finishing …, not installable options or what the dialog sets) for this job, sent as lp -o; on Windows the driver's Printing Preferences window (printui), saved as that printer's per-user defaults rather than per job. On the web: \"Browser's print dialog\" (the default there) reaches the system's printers through the browser; PdfCraft draws the sheets itself (300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000; at most 64 megapixels a sheet and 1 GiB a job) and prints them from a hidden iframe, one per printed page, a landscape sheet among portrait ones turned onto the paper; copies, two-sided and colour are chosen in the browser's dialog; a browser that refuses to print gets the print-ready PDF as a download. Checked in headless Chromium, not yet in Firefox or Safari; pictures, not vectors; sheets of another paper size print at their own size only where the browser honours named @page sizes (Chromium doesn't)"
|
||||
|
||||
[[feature]]
|
||||
id = "print.copies-collate"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ pub const ADOBE_DATA_ALLOWED: &[(&str, &str)] = &[("hayro-cmap", "assets/cmaps.b
|
|||
/// technical data we hold ourselves rather than through a dependency. Each still needs an
|
||||
/// `[[asset]]` entry with `adobe_data = true`, so its SHA-256 is checked and any change to the
|
||||
/// table has to be re-attested deliberately.
|
||||
pub const ADOBE_DATA_FILES: &[&str] = &["crates/fonts/src/std14_metrics.rs"];
|
||||
pub const ADOBE_DATA_FILES: &[&str] = &["crates/fonts/src/std14.rs"];
|
||||
|
||||
/// File extensions that count as assets wherever they appear in the repository.
|
||||
const ASSET_EXTENSIONS: &[&str] = &[
|
||||
|
|
@ -580,7 +580,7 @@ mod tests {
|
|||
fn first_party_adobe_data_only_on_the_closed_list() {
|
||||
let l = lock(&[]);
|
||||
// The generated standard-14 table is on the list, as `kind = "data"`.
|
||||
let mut ok = asset("crates/fonts/src/std14_metrics.rs", "Adobe (Core14 AFM metrics); tables by the PdfCraft contributors", "data", "MIT");
|
||||
let mut ok = asset("crates/fonts/src/std14.rs", "Adobe (Core14 AFM metrics); tables by the PdfCraft contributors", "data", "MIT");
|
||||
ok.adobe_data = true;
|
||||
let m = Manifest { asset: vec![ok], ..Default::default() };
|
||||
assert!(check(&root(), &m, &[], &l).is_empty(), "{:?}", check(&root(), &m, &[], &l));
|
||||
|
|
@ -592,7 +592,7 @@ mod tests {
|
|||
assert!(check(&root(), &m, &[], &l).iter().any(|p| p.contains("closed list")));
|
||||
|
||||
// Even on the list, it may only be data — never a typeface.
|
||||
let mut wrong_kind = asset("crates/fonts/src/std14_metrics.rs", "Adobe", "font", "MIT");
|
||||
let mut wrong_kind = asset("crates/fonts/src/std14.rs", "Adobe", "font", "MIT");
|
||||
wrong_kind.adobe_data = true;
|
||||
let m = Manifest { asset: vec![wrong_kind], ..Default::default() };
|
||||
assert!(!check(&root(), &m, &[], &l).is_empty());
|
||||
|
|
|
|||
Loading…
Reference in New Issue