diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..2408084cf --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +reviews: + auto_review: + # Dependabot bumps are checked by CI; skip CodeRabbit reviews on them. + ignore_usernames: + - "dependabot[bot]" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..72bac7ef7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +# The CLAUDE.md quality gate (`cargo xtask ci`: fmt, clippy -D warnings, tests, assets, layers, +# wasm) on every pull request and every push to main. Today only path-filtered workflows run on +# pull requests, so most PRs land with no test run at all. +# +# Ported from wordcraft#800. Linux only; standard GitHub-hosted runner, no secrets. +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + # Same craft-fonts commit as release.yml, so the font tests see what a release build embeds. + CRAFT_FONTS_REF: abb83316d96aa59c1cf64784289e378fe9fa5695 + +jobs: + gate: + name: cargo xtask ci + runs-on: ubuntu-24.04 + timeout-minutes: 90 + env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: "0" + RUST_BACKTRACE: "1" + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Check out craft-fonts + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: storytold/craft-fonts + ref: ${{ env.CRAFT_FONTS_REF }} + path: craft-fonts + persist-credentials: false + + - name: Install Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + pkg-config libxkbcommon-dev libwayland-dev libx11-dev libxcb1-dev libxcursor-dev \ + libxrandr-dev libxi-dev libfontconfig1-dev libfreetype-dev mesa-vulkan-drivers + + - name: Install Rust (stable + clippy, rustfmt, wasm32) + run: | + rustup toolchain install stable --profile minimal --component clippy,rustfmt \ + --target wasm32-unknown-unknown + rustup default stable + + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Quality gate + env: + CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts + run: cargo xtask ci diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 18392587a..01cb9cd66 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -18,16 +18,6 @@ concurrency: cancel-in-progress: true jobs: - eval: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 - - name: nix flake check --no-build - run: | - if [ -f flake.nix ]; then - nix flake check --no-build --all-systems - else - echo "No flake.nix present in this repository; skipping check." - fi + nix: + # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos. + uses: storytold/craft-common-actions/.github/workflows/nix.yml@main diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 000000000..ecee45a6d --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,20 @@ +name: SonarCloud + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: sonarcloud-${{ github.ref }} + cancel-in-progress: true + +jobs: + sonarcloud: + # Tracked at @main so org-wide CI improvements propagate without PR churn. + uses: storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main + secrets: inherit diff --git a/AGENTS.md b/AGENTS.md index 65693656c..0be7fa87b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ People trust DesignCraft with their layouts; a crash loses their work. **This ou - **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, menu path, shortcut, params doc, `enabled`, `run`) + tests. Tools emit commands (Begin/Preview/Commit). UI-only commands live in `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`). The control channel and MCP reach all of them. - **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. The UI crate is swappable. - **The UI is thin**: panels read engine state and act through `app.run(id, params)`. Colours come from `theme::Tokens`. +- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math). A crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic). - **Rust only** (no handwritten JS/TS). **Never break wasm** (`cargo xtask wasm`). - **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, assets, layers, wasm). One task id per commit (`M2.1: type tool caret navigation`). diff --git a/ASSETS.md b/ASSETS.md index 2731f47bf..40e8a97b4 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -69,3 +69,28 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra | `docs/brand/LICENSE-brand.txt` | (licence text) | ArtCraft Team | — | Terms for the ArtCraft Marks in `docs/brand/` | | `packaging/macos/dmg/background.svg` | @XusBadia | original work for this repository, derived from the DesignCraft app icon (`assets/app-icon/designcraft.svg`, referenced via ``, not copied); text is outlined Inter / JetBrains Mono glyph paths | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | DesignCraft macOS DMG window background for the Finder window; `packaging/macos/dmg/generate.py` renders it to `background.tiff` (2026-10-08) | | `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`) | 1x + 2x HiDPI TIFF for the macOS DMG window; includes the app icon (see its row) and LittleCMS's built-in sRGB profile ("No copyright, use freely") | + +## Assets bundled by the PDF backend + +These assets were already enabled by Hayro’s default font and CMap features. The pinned +backend keeps the same files; hashes and upstream licence notices are retained here. + +| Crate / file | Author | Source | Licence | SHA-256 | Use | +| --- | --- | --- | --- | --- | --- | +| `hayro-interpret 0.7.0 / assets/FoxitDingbats.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitDingbats.pfb | BSD-3-Clause | `845c752392b6c914fb989c75a08b7792b88f542d2499042ef2889f8c814a16ed` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixed.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitFixed.pfb | BSD-3-Clause | `b6c8fe53f134b8b6d4578cd2d544df4cee9624c4efa8d51a560fb40ea296101b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitFixedBold.pfb | BSD-3-Clause | `f1b7159702973f54fc86254ea38bcf3712b2a736eb3a0995751e9f4bb45ad603` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb | BSD-3-Clause | `8a000945843bd31add06aee63bf9fd41b7578b4f3242a4b7cd46349ad9d24d4d` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb | BSD-3-Clause | `5007faf8320fa1fcc08b8894b356ad976f60b992ba29ee5272578bbdebaf3876` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSans.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSans.pfb | BSD-3-Clause | `a02339e95d20db14cbdfc1c6baf359cd36a091d5434925c3a4040f60e1a1af9e` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSansBold.pfb | BSD-3-Clause | `7bdfa36de1d5757dcfaf8b8a4be95c6e41c40fa70dbed9d78bf979a7b9959054` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb | BSD-3-Clause | `555ce45924338e2e4b32821910221ce85da691fd59a9723c4c971a7c5627fc8b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSansItalic.pfb | BSD-3-Clause | `7751cbefeda351708dae8c4a88db043411bc39ab2ae2097685da845d4d16f307` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerif.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSerif.pfb | BSD-3-Clause | `4f57d2b9d884af8f907bf22df6019b52d86cbf6214fdbd02b1ae05472a543f35` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSerifBold.pfb | BSD-3-Clause | `0bdf4b04e964139818d51eda03d566ba999fc3ba2421b1c6c51f9dc969022e80` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb | BSD-3-Clause | `a406cac82583bf98175cb62c87ed5e95c45fbb34eece63a10b0a13e793cb2e10` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb | BSD-3-Clause | `610ae0687198045c4db5d1a6650fd5e92536631706382eb8b72e38df578d0ae9` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSymbol.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/FoxitSymbol.pfb | BSD-3-Clause | `47967d055530e7357088a08403115425643ec2cdfd6201ba8af0fbd7116c1539` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/CGATS001Compat-v2-micro.icc` | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc | CC0-1.0 | `73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0` | Default CMYK colour conversion (pinned hayro-interpret) | +| `hayro-cmap 0.1.0 / assets/cmaps.brotli` | Adobe | https://crates.io/crates/hayro-cmap/0.1.0 | BSD-3-Clause | `ddf37af521d904d5646c2fc37b2b7e912dc1b56f0d210190b61f598154758e46` | Character-code mapping tables required to read CJK PDFs. Non-visual data, allowed by the PDF compatibility exception | +| `hayro-interpret 0.7.0 / src/font/generated/metrics.rs` | Adobe (Core 14 AFM metrics, PDF specification encodings); tables by The Hayro Authors | https://github.com/storytold/pdfcraft/blob/7437bde378f43856eada40af25b983812e615e8a/vendor/hayro-interpret/src/font/generated/metrics.rs | MIT OR Apache-2.0 | `2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41` | Glyph widths and encodings for non-embedded standard fonts. Non-visual data, allowed by the PDF compatibility exception | diff --git a/Cargo.lock b/Cargo.lock index 3db056251..1180cf0b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -683,6 +683,15 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -832,8 +841,12 @@ source = "git+https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09 [[package]] name = "craft-pdf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c3487320b2d1de657b860b3c8abb44e1f2b32bd8#c3487320b2d1de657b860b3c8abb44e1f2b32bd8" +source = "git+https://github.com/storytold/craft-libs?rev=89dcbb3060377a26c99a6d80caa16084366704c4#89dcbb3060377a26c99a6d80caa16084366704c4" dependencies = [ + "bytemuck", + "hayro", + "hayro-interpret", + "hayro-syntax", "krilla", "thiserror 2.0.21", ] @@ -929,9 +942,10 @@ dependencies = [ [[package]] name = "designcraft" -version = "0.9.0" +version = "0.11.0" dependencies = [ "arboard", + "chrono", "designcraft-engine", "designcraft-ui-egui", "eframe", @@ -951,7 +965,7 @@ dependencies = [ [[package]] name = "designcraft-cli" -version = "0.9.0" +version = "0.11.0" dependencies = [ "designcraft-color", "designcraft-compose", @@ -967,7 +981,7 @@ dependencies = [ [[package]] name = "designcraft-color" -version = "0.9.0" +version = "0.11.0" dependencies = [ "craft-cms", "kurbo", @@ -979,7 +993,7 @@ dependencies = [ [[package]] name = "designcraft-compose" -version = "0.9.0" +version = "0.11.0" dependencies = [ "designcraft-color", "designcraft-doc", @@ -999,8 +1013,9 @@ dependencies = [ [[package]] name = "designcraft-doc" -version = "0.9.0" +version = "0.11.0" dependencies = [ + "chrono", "designcraft-color", "designcraft-geom", "proptest", @@ -1011,7 +1026,7 @@ dependencies = [ [[package]] name = "designcraft-engine" -version = "0.9.0" +version = "0.11.0" dependencies = [ "caseless", "craft-atomic", @@ -1050,8 +1065,9 @@ dependencies = [ [[package]] name = "designcraft-epub" -version = "0.9.0" +version = "0.11.0" dependencies = [ + "chrono", "designcraft-color", "designcraft-doc", "zip", @@ -1059,7 +1075,7 @@ dependencies = [ [[package]] name = "designcraft-fonts" -version = "0.9.0" +version = "0.11.0" dependencies = [ "core-text", "harfrust 0.13.3", @@ -1072,7 +1088,7 @@ dependencies = [ [[package]] name = "designcraft-format" -version = "0.9.0" +version = "0.11.0" dependencies = [ "designcraft-doc", "designcraft-images", @@ -1084,7 +1100,7 @@ dependencies = [ [[package]] name = "designcraft-geom" -version = "0.9.0" +version = "0.11.0" dependencies = [ "flo_curves", "kurbo", @@ -1097,7 +1113,7 @@ dependencies = [ [[package]] name = "designcraft-idml" -version = "0.9.0" +version = "0.11.0" dependencies = [ "designcraft-color", "designcraft-compose", @@ -1112,7 +1128,7 @@ dependencies = [ [[package]] name = "designcraft-images" -version = "0.9.0" +version = "0.11.0" dependencies = [ "craft-cms", "designcraft-color", @@ -1127,7 +1143,7 @@ dependencies = [ [[package]] name = "designcraft-mcp" -version = "0.9.0" +version = "0.11.0" dependencies = [ "designcraft-engine", "designcraft-geom", @@ -1140,8 +1156,9 @@ dependencies = [ [[package]] name = "designcraft-pdf" -version = "0.9.0" +version = "0.11.0" dependencies = [ + "chrono", "craft-pdf", "designcraft-color", "designcraft-compose", @@ -1162,8 +1179,9 @@ dependencies = [ [[package]] name = "designcraft-pptx" -version = "0.9.0" +version = "0.11.0" dependencies = [ + "chrono", "designcraft-color", "designcraft-compose", "designcraft-doc", @@ -1179,25 +1197,26 @@ dependencies = [ [[package]] name = "designcraft-render" -version = "0.9.0" +version = "0.11.0" dependencies = [ + "craft-pdf", "designcraft-color", "designcraft-compose", "designcraft-doc", "designcraft-fonts", "designcraft-geom", "designcraft-images", - "hayro", "image", "log", "serde", + "thiserror 2.0.21", "vello_common 0.2.0", "vello_cpu 0.2.0", ] [[package]] name = "designcraft-textimport" -version = "0.9.0" +version = "0.11.0" dependencies = [ "designcraft-doc", "quick-xml", @@ -1206,7 +1225,7 @@ dependencies = [ [[package]] name = "designcraft-tools" -version = "0.9.0" +version = "0.11.0" dependencies = [ "designcraft-color", "designcraft-compose", @@ -1218,7 +1237,7 @@ dependencies = [ [[package]] name = "designcraft-ui-egui" -version = "0.9.0" +version = "0.11.0" dependencies = [ "craft-ui", "designcraft-color", @@ -1245,7 +1264,7 @@ dependencies = [ [[package]] name = "designcraft-web" -version = "0.9.0" +version = "0.11.0" dependencies = [ "designcraft-engine", "designcraft-format", @@ -2105,7 +2124,7 @@ dependencies = [ [[package]] name = "hayro" version = "0.7.1" -source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" +source = "git+https://github.com/storytold/craft-libs?rev=89dcbb3060377a26c99a6d80caa16084366704c4#89dcbb3060377a26c99a6d80caa16084366704c4" dependencies = [ "bytemuck", "hayro-interpret", @@ -2135,7 +2154,7 @@ dependencies = [ [[package]] name = "hayro-interpret" version = "0.7.0" -source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" +source = "git+https://github.com/storytold/craft-libs?rev=89dcbb3060377a26c99a6d80caa16084366704c4#89dcbb3060377a26c99a6d80caa16084366704c4" dependencies = [ "bitflags 2.13.2", "hayro-cmap", @@ -2153,7 +2172,7 @@ dependencies = [ [[package]] name = "hayro-jbig2" version = "0.3.0" -source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" +source = "git+https://github.com/storytold/craft-libs?rev=89dcbb3060377a26c99a6d80caa16084366704c4#89dcbb3060377a26c99a6d80caa16084366704c4" dependencies = [ "fearless_simd", "hayro-ccitt", @@ -2177,7 +2196,7 @@ checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" [[package]] name = "hayro-syntax" version = "0.7.2" -source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" +source = "git+https://github.com/storytold/craft-libs?rev=89dcbb3060377a26c99a6d80caa16084366704c4#89dcbb3060377a26c99a6d80caa16084366704c4" dependencies = [ "flate2", "hayro-ccitt", @@ -2541,7 +2560,7 @@ dependencies = [ [[package]] name = "krilla" version = "0.8.2" -source = "git+https://github.com/storytold/craft-libs?rev=c3487320b2d1de657b860b3c8abb44e1f2b32bd8#c3487320b2d1de657b860b3c8abb44e1f2b32bd8" +source = "git+https://github.com/storytold/craft-libs?rev=89dcbb3060377a26c99a6d80caa16084366704c4#89dcbb3060377a26c99a6d80caa16084366704c4" dependencies = [ "base64", "bumpalo", @@ -5622,7 +5641,7 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" version = "0.30.13" -source = "git+https://github.com/skylartaylor/winit.git?rev=3d51077fd1a966f84df9ef287839dc84fd5b4ca7#3d51077fd1a966f84df9ef287839dc84fd5b4ca7" +source = "git+https://github.com/storytold/winit.git?rev=3d51077fd1a966f84df9ef287839dc84fd5b4ca7#3d51077fd1a966f84df9ef287839dc84fd5b4ca7" dependencies = [ "ahash", "android-activity", @@ -5796,7 +5815,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" [[package]] name = "xtask" -version = "0.9.0" +version = "0.11.0" dependencies = [ "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index ca5d0baaf..0e728e87f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,15 +4,15 @@ members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/designcraft", "apps/designcraft-cli", "xtask"] [workspace.package] -version = "0.9.0" +version = "0.11.0" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" repository = "https://github.com/storytold/designcraft" [workspace.dependencies] -# Shared writer; the immutable revision must be published before this consumer. -craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.1.0", default-features = false } +# Shared reader and writer; publish the immutable revision before this consumer. +craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "89dcbb3060377a26c99a6d80caa16084366704c4", version = "=0.1.0", default-features = false } # internal designcraft-geom = { path = "crates/geom" } designcraft-color = { path = "crates/color" } @@ -39,6 +39,8 @@ thiserror = "2" serde = { version = "1", features = ["derive", "rc"] } serde_json = { version = "1", features = ["float_roundtrip"] } log = "0.4" +# All date/time conversion goes through chrono (no hand-rolled calendar code beyond the 400-year range extension; AGENTS.md). +chrono = { version = "0.4.45", default-features = false, features = ["std"] } proptest = "1" rayon = "1" unicode-bidi = "0.3" @@ -66,7 +68,6 @@ krilla-svg = "=0.8.1" flo_curves = "0.8" # QR codes (Object › Generate QR Code); MIT. qrcodegen = "1.8" -hayro = "0.7" quick-xml = "0.41" zip = { version = "3", default-features = false, features = ["deflate"] } # INDD/INDT → IDML conversion for File › Open: storytold/indd-utils, tracking its main branch @@ -116,10 +117,9 @@ debug = 0 # Opt-in cancelable macOS termination on the pinned winit 0.30 line. [patch.crates-io] -winit = { git = "https://github.com/skylartaylor/winit.git", rev = "3d51077fd1a966f84df9ef287839dc84fd5b4ca7" } -krilla = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.8.2" } -# The hayro crates from pdfcraft's vendor/ directory, with the patches its README lists. -hayro = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" } -hayro-interpret = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" } -hayro-syntax = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" } -hayro-jbig2 = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" } +krilla = { git = "https://github.com/storytold/craft-libs", rev = "89dcbb3060377a26c99a6d80caa16084366704c4", version = "=0.8.2" } +winit = { git = "https://github.com/storytold/winit.git", rev = "3d51077fd1a966f84df9ef287839dc84fd5b4ca7" } +hayro = { git = "https://github.com/storytold/craft-libs", rev = "89dcbb3060377a26c99a6d80caa16084366704c4", version = "=0.7.1" } +hayro-interpret = { git = "https://github.com/storytold/craft-libs", rev = "89dcbb3060377a26c99a6d80caa16084366704c4", version = "=0.7.0" } +hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "89dcbb3060377a26c99a6d80caa16084366704c4", version = "=0.7.2" } +hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "89dcbb3060377a26c99a6d80caa16084366704c4", version = "=0.3.0" } diff --git a/NOTICE b/NOTICE index f4e3184f6..f1e997156 100644 --- a/NOTICE +++ b/NOTICE @@ -81,12 +81,35 @@ list (author, source, license) is in ASSETS.md. packages as OFL-.txt. Builds without CRAFT_FONTS_DIR do not include them. - All UI icons are drawn in code (crates/ui-egui/src/icons.rs) and are original work, MIT OR Apache-2.0. - hayro PDF crates (placed PDFs): hayro, hayro-interpret, hayro-syntax and hayro-jbig2 - (Apache-2.0 OR MIT) are pdfcraft's patched copies, fetched by git from its vendor/ directory at - the commit that Cargo.toml pins (https://github.com/storytold/pdfcraft, MIT OR Apache-2.0). + (Apache-2.0 OR MIT) are fetched from the pinned craft-libs source documented below. hayro-interpret embeds PDFium's Foxit standard-14 fonts under PDFium's BSD-style license (vendor/hayro-interpret/assets/LICENSE_FOXIT) and a CMYK profile under CC0-1.0. Rust dependencies (vello_cpu, kurbo, skrifa, harfrust, image, egui, eframe, …) are permissively -licensed (MIT/Apache-2.0/BSD/ISC/Zlib) and fetched through crates.io. +licensed (MIT/Apache-2.0/BSD/ISC/Zlib), fetched through crates.io or the pinned source below. No Adobe icons, images or artwork are included. + +PDF backend source + +The patched hayro 0.7.1, hayro-interpret 0.7.0, hayro-syntax 0.7.2 and hayro-jbig2 0.3.0 +are fetched from https://github.com/storytold/craft-libs at revision +89dcbb3060377a26c99a6d80caa16084366704c4. The four source snapshots were transferred +from https://github.com/storytold/pdfcraft at revision +7437bde378f43856eada40af25b983812e615e8a, then patched in craft-libs. Their upstream project is +https://github.com/LaurenzV/hayro, licensed MIT OR Apache-2.0. The snapshot retains each +crate's upstream licence files. The patch ledger is at +https://github.com/storytold/craft-libs/blob/89dcbb3060377a26c99a6d80caa16084366704c4/vendor/README.craft-reader.md. +Per-crate vendor/SOURCE.json files record transferred and local source hashes; asset +origins and unchanged per-file hashes are recorded separately in ASSETS.md. + +Assets already bundled through Hayro's default features retain their licences: +- Foxit standard-14 substitutes: Copyright 2014 PDFium Authors. All rights reserved. + Original code copyright 2014 Foxit Software Inc. BSD-3-Clause; see + licenses/pdf-backend/hayro-interpret/assets/LICENSE_FOXIT. +- CGATS001Compat-v2-micro.icc: CC0-1.0, Clinton Ingram; see + licenses/pdf-backend/hayro-interpret/assets/CGATS_LICENSE.txt. +- Predefined CMaps: Copyright 1990-2023 Adobe. All rights reserved. BSD-3-Clause, + non-visual character-mapping data; see licenses/pdf-backend/hayro-cmap/LICENSE.txt. + Adobe and contributor names do not endorse or promote this product. +Per-file hashes and sources are recorded in ASSETS.md. diff --git a/README.md b/README.md index 880433e33..4ca03d4dc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ Runs on macOS, Windows, Linux and the web License: MIT OR Apache-2.0 Agent-drivable over MCP +
+ Scanned by SonarCloud + Reviewed by CodeRabbit

diff --git a/ROADMAP.md b/ROADMAP.md index dbc4552b0..601fa7010 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,8 +348,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-22** · Paragraph rules: Rule Above's offset is measured from the first line's baseline to the rule's bottom edge, as in InDesign. Rules sat one ascent too high and crossed the previous paragraph's text (reported from an imported INDD document). No readiness percentage change. - **2026-10-18** · TOC separators preserve right-indent tabs through the dialog and IDML, including localized `$ID/^y` values. A separator picker exposes Tab Character, Right Indent Tab, Em Space and En Space. Existing paragraph tab leaders now work with the tutorial's TOC workflow. No readiness percentage change. - - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. @@ -640,3 +640,5 @@ Newest first. The shared craft-installer UI preserves this revision's install options and provides persistent installation, repair and Windows Settings removal completion screens. Silent modes stay unattended. Earlier Windows acceptance is recorded in packaging/windows/README.md; normal CI and production-profile upgrade qualification against pinned current releases are in progress. No release or PR has been published. - **2026-10-15** · Windows recovery folders validate the identity of the live locked file without reading it through a second handle. Missing or replaced locks disable writes and cleanup; regression tests cover retained exclusive locking and replacement-file protection. No readiness percentage change. + +- PDF reader infrastructure: pinned shared parsing/rasterization, transformed page-box geometry and explicit failures for known incomplete layered-PDF exports. Application ownership and parity targets are unchanged. diff --git a/apps/designcraft/Cargo.toml b/apps/designcraft/Cargo.toml index 7dc83bc55..81a267aa9 100644 --- a/apps/designcraft/Cargo.toml +++ b/apps/designcraft/Cargo.toml @@ -18,6 +18,7 @@ egui = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } log = { workspace = true } +chrono = { workspace = true } [build-dependencies] # Icon + VERSIONINFO for designcraft.exe; build.rs only uses it when the target is Windows. diff --git a/apps/designcraft/src/logging.rs b/apps/designcraft/src/logging.rs index d3e0dbea7..5fe70a749 100644 --- a/apps/designcraft/src/logging.rs +++ b/apps/designcraft/src/logging.rs @@ -90,27 +90,36 @@ fn matches(name: &str, target: &str) -> bool { } /// `2026-10-08T07:59:17.728Z` (UTC, milliseconds). A clock before 1970 reads as the epoch. +/// A clock far past chrono's year range still converts (the calendar repeats every 400 years), so +/// logging never panics and never silently resets to the epoch for a convertible instant. pub fn timestamp(t: SystemTime) -> String { let since = t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default(); - let secs = since.as_secs(); - let (days, rem) = (secs / 86_400, secs % 86_400); - let (y, m, d) = civil_from_days(days); - format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}.{:03}Z", rem / 3600, rem % 3600 / 60, rem % 60, since.subsec_millis()) + // Pre-1970 is clamped to the epoch by `unwrap_or_default` above. `as_secs` is `u64`; a value + // past `i64::MAX` seconds saturates rather than falling back to the epoch. + let secs = i64::try_from(since.as_secs()).unwrap_or(i64::MAX); + let (y, t) = utc_parts(secs, since.subsec_nanos()); + // `{:04}` rather than `%Y`, which writes a `+` before years past 9999. + format!("{y:04}-{}", t.format("%m-%dT%H:%M:%S%.3fZ")) } -/// Days since 1970-01-01 to a proleptic Gregorian (year, month, day) (Howard Hinnant's -/// `civil_from_days`, unsigned because `timestamp` clamps at the epoch). -fn civil_from_days(days: u64) -> (u64, u64, u64) { - let z = days + 719_468; - let era = z / 146_097; - let doe = z % 146_097; - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = doy - (153 * mp + 2) / 5 + 1; - let m = if mp < 10 { mp + 3 } else { mp - 9 }; - let y = yoe + era * 400 + u64::from(m <= 2); - (y, m, d) +/// Unix seconds (+ nanos) → (proleptic Gregorian year, the same wall-clock instant moved into +/// chrono's range by whole 400-year cycles). The calendar repeats every 400 years (146_097 days), +/// so month, day and time of day come from chrono and only the year is carried, which keeps every +/// `i64` exact. Replaces the old hand-rolled `civil_from_days`; chrono is the only calendar logic. +fn utc_parts(secs: i64, nanos: u32) -> (i64, chrono::NaiveDateTime) { + use chrono::Datelike; + let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400)); + let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097)); + // `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400. + let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt); + let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, nanos.min(999_999_999))); + match (date, time) { + (Some(d), Some(t)) => { + let y = i128::from(d.year()) + i128::from(cycles) * 400; + (i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.and_time(t)) + } + _ => (1970, chrono::NaiveDateTime::default()), // unreachable + } } fn numbered(dir: &Path, n: usize) -> PathBuf { @@ -317,6 +326,20 @@ mod tests { assert_eq!(timestamp(UNIX_EPOCH + Duration::from_millis(1_704_067_199_999)), "2023-12-31T23:59:59.999Z"); // Before the epoch (a clock set wrong) clamps instead of panicking. assert_eq!(timestamp(UNIX_EPOCH - Duration::from_secs(5)), "1970-01-01T00:00:00.000Z"); + // Past chrono's year range still converts via the 400-year cycle (the old code did too, + // through `civil_from_days`): never a silent reset to the epoch. Platform-safe: Windows' + // SystemTime can't hold this instant, so it is only checked where it exists. + // `u64::MAX / 2` seconds == `i64::MAX` seconds → year 292277026596 (old output). + if let Some(far) = UNIX_EPOCH.checked_add(Duration::from_secs(u64::MAX / 2)) { + assert_eq!(timestamp(far), "292277026596-12-04T15:30:07.000Z"); + } + // Years past 9999 keep the plain zero-padded year (no `+`). + assert_eq!(timestamp(UNIX_EPOCH + Duration::from_secs(253_402_300_800)), "10000-01-01T00:00:00.000Z"); + // 10_000_000_000_000 s is far beyond chrono's range; old output is year 318857. + // Windows' SystemTime holds only up to about year 30828, so this is checked where it fits. + if let Some(far) = UNIX_EPOCH.checked_add(Duration::from_secs(10_000_000_000_000)) { + assert_eq!(timestamp(far), "318857-05-20T17:46:40.000Z"); + } } #[test] diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 4a2c66397..89ea89bd3 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -1860,11 +1860,12 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C cur.band = Band { para: pi + 1, line0: ft.lines.len(), above: Some(above), tail: None }; } // Rules and shading for the paragraph. - if let Some((fi, bl, asc, _)) = first_line_rect { + if let Some((fi, bl, _, _)) = first_line_rect { let ft = &mut out.frames[fi]; if pp.rule_above.on { let r = &pp.rule_above; - let y = bl - asc - r.offset; + // The offset runs from the first line's baseline up to the rule's bottom edge. + let y = bl - r.offset; let col = ft .lines .iter() diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index ed4c7c476..15c412ada 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -7290,3 +7290,23 @@ fn greek_text_gets_greek_hyphenation() { let el = hyphen::Lang::for_language("Greek").unwrap(); assert_eq!(hyphen::hyphen_points_in("ανθρωπος", &hyphen::Limits::default(), el), vec![2, 5]); } + +/// Rule Above's offset runs from the first line's baseline up to the rule's bottom edge, as in +/// InDesign: a rule set clear of the cap height sits between the rows, not across the row above +/// (an INDD project list whose rules crossed the previous paragraph's text). +#[test] +fn rule_above_offset_is_measured_from_the_baseline() { + use designcraft_doc::Rule; + let rule = Rule { on: true, weight: 1.5, offset: 10.0, ..Default::default() }; + let para = ParaAttrs { rule_above: Some(rule), ..Default::default() }; + let (d, sid, fid) = doc_with("Row one\nRow two", Rect::new(0.0, 0.0, 300.0, 200.0), para); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let lines = all_lines(&cs); + let (first, second) = (lines[0], lines[1]); + let mut decos: Vec = cs.frame(fid).unwrap().decos.iter().map(|dc| dc.rect).collect(); + decos.sort_by(|a, b| a.y0.total_cmp(&b.y0)); + let rule = decos[1]; + assert!((rule.y1 - (second.baseline - 10.0)).abs() < 1e-6, "{rule:?}, baseline {}", second.baseline); + assert!((rule.height() - 1.5).abs() < 1e-6); + assert!(rule.y0 > first.baseline, "the rule stays below the row above: {rule:?}, its baseline {}", first.baseline); +} diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index ed7430db7..1d3da4062 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -14,6 +14,7 @@ designcraft-color = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } +chrono = { workspace = true } [dev-dependencies] proptest = { workspace = true } diff --git a/crates/doc/src/vars.rs b/crates/doc/src/vars.rs index 1ea4f8ced..0a3f5ea46 100644 --- a/crates/doc/src/vars.rs +++ b/crates/doc/src/vars.rs @@ -126,28 +126,46 @@ pub fn now() -> i64 { } } -/// (year, month 1–12, day 1–31) from days since 1970-01-01 (H. Hinnant's public-domain algorithm). +/// (year, month 1–12, day 1–31) from days since 1970-01-01. +/// +/// `NaiveDate` only covers about ±262143 years, but the system clock can run past it, so this +/// stays exact for every `days`. The Gregorian calendar repeats every 400 years (146_097 days): +/// chrono converts the day within its cycle and whole cycles go back into the year (an i128 so no +/// `days` overflows), which matches the proleptic Gregorian answer the old hand-rolled code gave. +/// 1970-01-01 is `num_days_from_ce` 719_163. fn civil(days: i64) -> (i64, u32, u32) { - let z = days + 719_468; - let era = z.div_euclid(146_097); - let doe = z.rem_euclid(146_097); - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = (doy - (153 * mp + 2) / 5 + 1) as u32; - let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; - (yoe + era * 400 + i64::from(m <= 2), m, d) + use chrono::Datelike; + let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097)); + match i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt) { + Some(d) => { + let y = i128::from(d.year()) + i128::from(cycles) * 400; + (i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.month(), d.day()) + } + // Unreachable: `rest + 719_163` is always a valid CE day. + None => (1970, 1, 1), + } } -/// Days since 1970-01-01 of a civil date (inverse of [`civil`]). +/// Days since 1970-01-01 of a civil date (inverse of [`civil`]), exact for any year. +/// +/// The Gregorian calendar repeats every 400 years (146_097 days): chrono converts the year within +/// its cycle and whole cycles are added back as days (an i128 so the extremes can't overflow), +/// which matches the proleptic Gregorian count the old hand-rolled code gave. 1970-01-01 is +/// `num_days_from_ce` 719_163. +/// +/// A day past the end of its month carries into the next one, as before chrono (`2019-02-30` +/// is 2019-03-02: `parse_iso_date` accepts any day 1..=31): chrono places the 1st of the month +/// and the rest of the days are added on. Only a month outside 1..=12 reads as 0. fn days_from_civil(y: i64, m: u32, d: u32) -> i64 { - let y = if m <= 2 { y - 1 } else { y }; - let era = y.div_euclid(400); - let yoe = y.rem_euclid(400); - let mp = i64::from(if m > 2 { m - 3 } else { m + 9 }); - let doy = (153 * mp + 2) / 5 + i64::from(d) - 1; - let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - era * 146_097 + doe - 719_468 + let (cycles, year) = (i128::from(y.div_euclid(400)), y.rem_euclid(400)); + i32::try_from(year) + .ok() + .and_then(|year| chrono::NaiveDate::from_ymd_opt(year, m, 1)) + .and_then(|first| { + let days = i128::from(chrono::Datelike::num_days_from_ce(&first)) - 719_163 + cycles * 146_097 + i128::from(d) - 1; + i64::try_from(days).ok() + }) + .unwrap_or(0) } /// Unix seconds of an ISO 8601 timestamp such as `2019-12-16T09:48:02+01:00` (seconds, the @@ -294,6 +312,30 @@ mod tests { assert_eq!(format_date(951_782_400, "yyyy-MM-dd"), "2000-02-29"); } + #[test] + fn dates_beyond_chrono_range() { + // Past `NaiveDate`'s ±262143 the 400-year cycles carry the year; the clock can reach this, + // so keep the proleptic Gregorian date the old formula gave rather than the epoch, on both + // sides of the epoch. Values from the pre-chrono formula, through the public `format_date`. + assert_eq!(format_date(10_000_000_000_000, "yyyy-MM-dd HH:mm"), "318857-05-20 17:46"); + assert_eq!(format_date(-8_334_659_289_600, "yyyy-MM-dd"), "-262145-03-01"); + // `days_from_civil` is exact and inverts `civil` past the range, both sides. + for (y, m, d, days) in + [(262_144i64, 1u32, 1u32, 95_026_602i64), (300_000, 6, 15, 108_853_388), (-262_145, 3, 1, -96_465_964), (-300_000, 2, 29, -110_292_219)] + { + assert_eq!(days_from_civil(y, m, d), days, "{y}-{m}-{d}"); + assert_eq!(civil(days), (y, m, d), "{days}"); + } + // The extremes must not overflow or panic (i128 intermediates). + let _ = (civil(i64::MAX), civil(i64::MIN), days_from_civil(i64::MAX, 12, 31), days_from_civil(i64::MIN, 1, 1)); + // A day past the end of its month carries over, as before chrono, instead of reading as 1970. + assert_eq!(days_from_civil(2019, 2, 30), days_from_civil(2019, 3, 2)); + assert_eq!(days_from_civil(2024, 2, 30), days_from_civil(2024, 3, 1)); + assert_eq!(days_from_civil(2019, 4, 31), days_from_civil(2019, 5, 1)); + assert_eq!(parse_iso_date("2019-02-30"), Some(1_551_484_800)); + assert_eq!(parse_iso_date("2019-02-30T09:48:02Z"), parse_iso_date("2019-03-02T09:48:02Z")); + } + #[test] fn iso_dates() { assert_eq!(parse_iso_date("2026-10-01T14:05:00Z"), Some(1_790_863_500)); diff --git a/crates/engine/src/cmd/export.rs b/crates/engine/src/cmd/export.rs index 8bbe41a8a..36fd3d52a 100644 --- a/crates/engine/src/cmd/export.rs +++ b/crates/engine/src/cmd/export.rs @@ -659,11 +659,11 @@ fn wrap_stand_in(it: &designcraft_doc::Item, depth: usize) -> designcraft_doc::I } /// A copy of `d` where placed PDFs with Object Layer Options hidden layers are 300 ppi images. -fn rasterize_layered(d: &designcraft_doc::Document) -> (Option, usize) { +fn rasterize_layered(d: &designcraft_doc::Document) -> Result<(Option, usize)> { let ids: Vec = d.all_items().into_iter().filter(|id| d.item(*id).is_some_and(|it| !it.pdf_hidden_layers.is_empty() && it.graphic().is_some())).collect(); if ids.is_empty() { - return (None, 0); + return Ok((None, 0)); } let mut out = d.clone(); let mut n = 0; @@ -678,7 +678,8 @@ fn rasterize_layered(d: &designcraft_doc::Document) -> (Option (Option Result { let flattened = ppi.map(|ppi| flatten(&st.doc, &s.cache, ppi)); // Placed PDFs with hidden layers go out as images showing just their visible layers. let base: &designcraft_doc::Document = flattened.as_ref().map_or(&st.doc, |f| &f.0); - let (layered, layer_count) = rasterize_layered(base); + let (layered, layer_count) = rasterize_layered(base)?; let base2: &designcraft_doc::Document = layered.as_ref().unwrap_or(base); let (fx_doc, fx_count) = rasterize_effects(base2, &s.cache); let doc: &designcraft_doc::Document = fx_doc.as_ref().unwrap_or(base2); diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 973262937..da38b8132 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -629,6 +629,9 @@ fn file_place(s: &mut Session, p: &Value) -> Result { }) .transpose()?; // Image Import Options: which page of a PDF (1-based). + if designcraft_render::is_pdf(&bytes) { + designcraft_render::check_pdf_input_size(bytes.len()).map_err(|e| bad("file.place", e.to_string()))?; + } let pdf_page = match p.get("pdfPage").and_then(Value::as_u64) { Some(n) if designcraft_render::is_pdf(&bytes) => { let count = designcraft_render::pdf_page_count(&bytes).unwrap_or(1); diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index c6e9e7566..6623b6cb8 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -398,6 +398,9 @@ fn reimport(d: &mut Document, sid: StoryId, path: &str, bytes: &[u8], before: Fi fn reload(path: impl AsRef) -> Result<(Vec, (u32, u32))> { let path = path.as_ref(); let b = crate::io::read_file(path).map_err(EngineError::Other)?; + if designcraft_render::is_pdf(&b) { + designcraft_render::check_pdf_input_size(b.len()).map_err(|e| bad("links", format!("{}: {e}", path.display())))?; + } let px = designcraft_render::image_size(&b).ok_or_else(|| bad("links", format!("{}: unsupported or corrupt image", path.display())))?; Ok((b, px)) } diff --git a/crates/engine/tests/pdf_backend.rs b/crates/engine/tests/pdf_backend.rs new file mode 100644 index 000000000..6b4f93081 --- /dev/null +++ b/crates/engine/tests/pdf_backend.rs @@ -0,0 +1,349 @@ +//! Synthetic placement/export regressions for PDF backend updates. + +use serde_json::json; + +use designcraft_engine::Session; +use designcraft_engine::cmd::{base64_decode, base64_encode}; + +fn pdf(objects: &[String]) -> Vec { + let mut bytes = b"%PDF-1.7\n".to_vec(); + let mut offsets = vec![0]; + for (i, object) in objects.iter().enumerate() { + offsets.push(bytes.len()); + bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes()); + } + let xref = bytes.len(); + bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f\r\n", offsets.len()).as_bytes()); + for offset in offsets.iter().skip(1) { + bytes.extend_from_slice(format!("{offset:010} 00000 n\r\n").as_bytes()); + } + bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", offsets.len()).as_bytes()); + bytes +} + +fn layered_pdf(second_name: &str) -> Vec { + layered_pdf_with_box(second_name, "") +} + +fn layered_pdf_with_box(second_name: &str, page_box: &str) -> Vec { + let content = "/OC /a BDC 0 0 1 rg 10 10 35 80 re f EMC /OC /b BDC 1 0 0 rg 55 10 35 80 re f EMC"; + pdf(&[ + "<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R 6 0 R] /D << /Order [5 0 R 6 0 R] >> >> >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + format!( + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] {page_box} /Resources << /Properties << /a 5 0 R /b 6 0 R >> >> /Contents 4 0 R >>" + ), + format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()), + "<< /Type /OCG /Name (Blue) >>".into(), + format!("<< /Type /OCG /Name ({second_name}) >>"), + ]) +} + +fn rendered(bytes: &[u8]) -> designcraft_render::Rendered { + let pm = designcraft_render::render_pdf_page(bytes, 0, 100).expect("render synthetic PDF"); + designcraft_render::Rendered { + width: pm.width().into(), + height: pm.height().into(), + pixels: pm.data().iter().flat_map(|p| [p.r, p.g, p.b, p.a]).collect(), + } +} + +// Optional local evidence output; no binary fixtures are checked in. +fn evidence(name: &str, bytes: &[u8]) { + if let Some(dir) = std::env::var_os("CRAFT_PDF_EVIDENCE_DIR") { + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(std::path::Path::new(&dir).join(name), bytes).unwrap(); + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn placed_pdf_preserves_transparent_background_and_layer_choices() { + let bytes = layered_pdf("Red"); + let img = rendered(&bytes); + assert_eq!(img.pixel(2, 2), [0, 0, 0, 0]); + assert_eq!(img.pixel(25, 50), [0, 0, 255, 255]); + assert_eq!(img.pixel(75, 50), [255, 0, 0, 255]); + let hidden = designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).unwrap(); + let img = rendered(&hidden); + assert_eq!(img.pixel(75, 50), [0, 0, 0, 0]); + assert_eq!(img.pixel(25, 50), [0, 0, 255, 255]); + evidence("layers-hidden.png", &img.to_png()); + evidence("layers-source.pdf", &bytes); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn duplicate_layer_names_keep_name_based_hiding() { + let bytes = layered_pdf("Blue"); + assert_eq!(designcraft_render::pdf_layers(&bytes).len(), 2); + let hidden = designcraft_render::pdf_hide_layers(&bytes, &["Blue".into()]).unwrap(); + let img = rendered(&hidden); + assert_eq!(img.pixel(25, 50), [0, 0, 0, 0]); + assert_eq!(img.pixel(75, 50), [0, 0, 0, 0]); + // A second render of the original must not inherit the hidden configuration. + assert_eq!(rendered(&bytes).pixel(25, 50), [0, 0, 255, 255]); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn crop_box_and_rotation_keep_placed_dimensions() { + let bytes = pdf(&[ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 100] /CropBox [10 20 110 80] /Rotate 90 >>".into(), + ]); + assert_eq!(designcraft_render::pdf_page_count(&bytes), Some(1)); + assert_eq!(designcraft_render::pdf_page_size(&bytes, 0), Some((60.0, 100.0))); + let img = rendered(&bytes); + assert_eq!((img.width, img.height), (60, 100)); + assert_eq!(img.pixel(20, 20), [0, 0, 0, 0]); +} + +fn exported(hidden: bool) -> Vec { + let mut session = Session::new(); + session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1})).unwrap(); + session.execute("edit.transparencyBlendSpace", &json!({"space": "rgb"})).unwrap(); + session.execute("frame.create", &json!({"rect": [0, 0, 100, 100]})).unwrap(); + session.execute("object.fill", &json!({"swatch": "[Black]"})).unwrap(); + let placed = session + .execute( + "file.place", + &json!({ + "base64": base64_encode(&layered_pdf("Red")), "name": "layers.pdf", "x": 0, "y": 0, "width": 100 + }), + ) + .unwrap(); + if hidden { + session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Red"]})).unwrap(); + } + let report = session.execute("file.exportPdf", &json!({"compress": false})).unwrap(); + let bytes = base64_decode(report["base64"].as_str().unwrap()); + if hidden { + assert!(report["warnings"].to_string().contains("hidden layers")); + } + bytes +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn hidden_layer_export_composites_over_underlying_artwork() { + let bytes = exported(true); + let img = rendered(&bytes); + let right = img.pixel(75, 50); + assert!(right[..3].iter().all(|c| *c < 45), "hidden area must show the black backdrop, not opaque white: {right:?}"); + let left = img.pixel(25, 50); + assert!(left[2] > 180 && left[0] < 80, "visible blue layer: {left:?}"); + evidence("hidden-export.pdf", &bytes); + evidence("hidden-export.png", &img.to_png()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn ordinary_placed_pages_still_embed_as_vectors() { + use hayro_syntax::object::Name; + let bytes = exported(false); + let parsed = hayro_syntax::Pdf::new(bytes.clone()).unwrap(); + let subtypes: Vec<_> = parsed + .objects() + .into_iter() + .filter_map(|o| o.into_stream()) + .filter_map(|s| s.dict().get::("Subtype").map(|n| n.as_str().to_owned())) + .collect(); + assert!(subtypes.iter().any(|s| s == "Form"), "placed PDF form: {subtypes:?}"); + assert!(!subtypes.iter().any(|s| s == "Image"), "no rasterization: {subtypes:?}"); + let img = rendered(&bytes); + let right = img.pixel(75, 50); + assert!(right[0] > 180 && right[1] < 80, "visible red vector layer: {right:?}"); + evidence("vector-export.pdf", &bytes); + evidence("vector-export.png", &img.to_png()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn oversized_image_does_not_silently_disappear_from_layered_export() { + // Just above the backend's 2^28-pixel cap. The stream is one byte; no large + // fixture or large image allocation is needed to exercise the refusal. + let content = "/OC /a BDC 0 0 1 rg 10 10 35 80 re f EMC q 100 0 0 100 0 0 cm /Im Do Q"; + let bytes = pdf(&[ + "<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [6 0 R] /D << /Order [6 0 R] >> >> >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /Properties << /a 6 0 R >> /XObject << /Im 5 0 R >> >> /Contents 4 0 R >>".into(), + format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()), + "<< /Type /XObject /Subtype /Image /Width 32769 /Height 8192 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >>\nstream\nX\nendstream".into(), + "<< /Type /OCG /Name (Blue) >>".into(), + ]); + let mut session = Session::new(); + session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1})).unwrap(); + let placed = session + .execute( + "file.place", + &json!({ + "base64": base64_encode(&bytes), "name": "oversized-scan.pdf", "x": 0, "y": 0, "width": 100 + }), + ) + .unwrap(); + session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Blue"]})).unwrap(); + let before = session.execute("file.serialize", &json!({})).unwrap(); + let result = session.execute("file.exportPdf", &json!({})); + assert!(result.is_err(), "export must report the image omitted by the backend's resource limit"); + assert_eq!(session.execute("file.serialize", &json!({})).unwrap(), before, "a failed export must leave the document unchanged"); + let error = result.unwrap_err().to_string(); + assert!(error.contains("oversized-scan.pdf") && error.contains("image"), "{error}"); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn failed_layer_rewrite_aborts_export_instead_of_revealing_hidden_content() { + // A stored layer choice can outlive relinking to a PDF with no layers. + let bytes = pdf(&[ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>".into(), + ]); + assert!(designcraft_render::render_pdf_page(&bytes, 0, 100).is_some()); + assert!(designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).is_none()); + let mut session = Session::new(); + session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1})).unwrap(); + let placed = + session.execute("file.place", &json!({"base64": base64_encode(&bytes), "name": "missing-layers.pdf", "x": 0, "y": 0, "width": 100})).unwrap(); + session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Red"]})).unwrap(); + let before = session.execute("file.serialize", &json!({})).unwrap(); + let result = session.execute("file.exportPdf", &json!({})); + assert!(result.is_err(), "failed layer hiding must not export the original visible layers"); + let error = result.unwrap_err().to_string(); + assert!(error.contains("missing-layers.pdf") && error.contains("layer"), "{error}"); + assert_eq!(session.execute("file.serialize", &json!({})).unwrap(), before); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn placed_standard_font_text_renders_ink_on_a_transparent_background() { + let content = "BT /F1 18 Tf 10 40 Td (PDF text) Tj ET"; + let bytes = pdf(&[ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>".into(), + format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".into(), + ]); + let img = rendered(&bytes); + let ink = (40..65).flat_map(|y| (5..95).map(move |x| (x, y))).filter(|(x, y)| img.pixel(*x, *y)[3] > 100).count(); + assert!(ink > 100, "standard-font glyphs must paint visible ink: {ink}"); + assert_eq!(img.pixel(2, 2), [0, 0, 0, 0]); + assert_eq!(img.pixel(50, 80), [0, 0, 0, 0]); + assert!(designcraft_render::pdf_page_png_checked(&bytes, 0, 100, &[], None).is_ok()); + evidence("standard-font-text.png", &img.to_png()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn unavailable_placed_page_reports_the_page_and_recovery_action() { + let error = designcraft_render::pdf_page_png_checked(&layered_pdf("Red"), 2, 100, &["Red".into()], None).unwrap_err(); + assert!(matches!(error, designcraft_render::PdfRasterError::MissingPage { page: 3 })); + assert!(error.to_string().contains("place the replacement PDF again")); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn hidden_layers_work_with_an_xref_stream_and_compressed_catalog() { + // Reuse the tiny synthetic objects, but put the catalog in an object stream + // and index everything with a binary cross-reference stream. + let classic = String::from_utf8(layered_pdf("Red")).unwrap(); + let objects: Vec<&str> = (1..=6).map(|i| classic.split_once(&format!("{i} 0 obj\n")).unwrap().1.split_once("\nendobj").unwrap().0).collect(); + let mut bytes = b"%PDF-1.7\n".to_vec(); + let mut offsets = [0u32; 9]; + for (i, object) in objects.iter().enumerate().skip(1) { + offsets[i + 1] = u32::try_from(bytes.len()).unwrap(); + bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes()); + } + offsets[7] = u32::try_from(bytes.len()).unwrap(); + let catalog = format!("1 0 {}", objects[0]); + bytes.extend_from_slice( + format!("7 0 obj\n<< /Type /ObjStm /N 1 /First 4 /Length {} >>\nstream\n{catalog}\nendstream\nendobj\n", catalog.len()).as_bytes(), + ); + offsets[8] = u32::try_from(bytes.len()).unwrap(); + let mut entries = Vec::new(); + for (i, offset) in offsets.iter().enumerate() { + let (kind, field, generation): (u8, u32, u16) = match i { + 0 => (0, 0, 65535), + 1 => (2, 7, 0), + _ => (1, *offset, 0), + }; + entries.push(kind); + entries.extend_from_slice(&field.to_be_bytes()); + entries.extend_from_slice(&generation.to_be_bytes()); + } + bytes.extend_from_slice(format!("8 0 obj\n<< /Type /XRef /Size 9 /Root 1 0 R /W [1 4 2] /Length {} >>\nstream\n", entries.len()).as_bytes()); + bytes.extend_from_slice(&entries); + bytes.extend_from_slice(format!("\nendstream\nendobj\nstartxref\n{}\n%%EOF\n", offsets[8]).as_bytes()); + assert_eq!(rendered(&bytes).pixel(75, 50), [255, 0, 0, 255]); + let hidden = designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).unwrap(); + let img = rendered(&hidden); + assert_eq!(img.pixel(75, 50), [0, 0, 0, 0]); + assert_eq!(img.pixel(25, 50), [0, 0, 255, 255]); + assert!(designcraft_render::pdf_page_png_checked(&bytes, 0, 100, &["Red".into()], None).is_ok()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn trailing_whitespace_does_not_prevent_layer_hiding() { + let mut bytes = layered_pdf("Red"); + bytes.extend_from_slice(&[b' '; 96]); + let hidden = designcraft_render::pdf_hide_layers(&bytes, &["Red".into()]).expect("trailing whitespace is accepted by the layer rewriter"); + let img = rendered(&hidden); + assert_eq!(img.pixel(75, 50), [0, 0, 0, 0]); + assert_eq!(img.pixel(25, 50), [0, 0, 255, 255]); + assert!(designcraft_render::pdf_page_png_checked(&bytes, 0, 100, &["Red".into()], None).is_ok()); +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn failed_content_decode_aborts_layered_export_without_changing_the_document() { + for filter in ["FlateDecode", "DCTDecode"] { + let bytes = pdf(&[ + "<< /Type /Catalog /Pages 2 0 R /OCProperties << /OCGs [5 0 R] /D << /Order [5 0 R] >> >> >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /Properties << /a 5 0 R >> >> /Contents 4 0 R >>".into(), + format!("<< /Filter /{filter} /Length 1 >>\nstream\nx\nendstream"), + "<< /Type /OCG /Name (Blue) >>".into(), + ]); + let mut session = Session::new(); + session.execute("file.new", &json!({"width": 100, "height": 100, "pages": 1})).unwrap(); + let placed = session + .execute("file.place", &json!({"base64": base64_encode(&bytes), "name": "broken-content.pdf", "x": 0, "y": 0, "width": 100})) + .unwrap(); + session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Blue"]})).unwrap(); + let before = session.execute("file.serialize", &json!({})).unwrap(); + let error = session.execute("file.exportPdf", &json!({})).unwrap_err().to_string(); + assert!(error.contains("broken-content.pdf") && error.contains("content"), "{filter}: {error}"); + assert_eq!(session.execute("file.serialize", &json!({})).unwrap(), before, "{filter}"); + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn hidden_layer_export_keeps_the_placed_trim_box() { + let bytes = layered_pdf_with_box("Red", "/TrimBox [0 0 50 100]"); + let mut session = Session::new(); + session.execute("file.new", &json!({"width": 50, "height": 100, "pages": 1})).unwrap(); + session.execute("edit.transparencyBlendSpace", &json!({"space": "rgb"})).unwrap(); + let placed = session + .execute( + "file.place", + &json!({"base64": base64_encode(&bytes), "name": "trimmed-layers.pdf", "pdfCrop": "trim", "x": 0, "y": 0, "width": 100}), + ) + .unwrap(); + session.execute("object.layerOptions", &json!({"ids": [placed["id"]], "hidden": ["Red"]})).unwrap(); + let before = session.execute("file.serialize", &json!({})).unwrap(); + let report = session.execute("file.exportPdf", &json!({"compress": false})).unwrap(); + let exported = base64_decode(report["base64"].as_str().unwrap()); + let img = rendered(&exported); + assert_eq!((img.width, img.height), (50, 100)); + // Omitting the shown box squeezes the whole page into the trimmed graphic, + // moving the blue rectangle's right edge from x=45 to x=22.5. + let blue = img.pixel(35, 50); + assert!(blue[2] > 180 && blue[0] < 80, "the trim box keeps the visible layer at its placed coordinates: {blue:?}"); + assert_eq!(session.execute("file.serialize", &json!({})).unwrap(), before); + evidence("trimmed-layer-export.pdf", &exported); +} diff --git a/crates/epub/Cargo.toml b/crates/epub/Cargo.toml index 0cbe1673c..2836b0a92 100644 --- a/crates/epub/Cargo.toml +++ b/crates/epub/Cargo.toml @@ -12,3 +12,4 @@ workspace = true designcraft-doc = { workspace = true } designcraft-color = { workspace = true } zip = { workspace = true } +chrono = { workspace = true } diff --git a/crates/epub/src/lib.rs b/crates/epub/src/lib.rs index 84bf56f1e..bd4571501 100644 --- a/crates/epub/src/lib.rs +++ b/crates/epub/src/lib.rs @@ -123,19 +123,32 @@ fn font_file(k: usize, f: &EmbeddedFont) -> (String, &'static str) { } /// An ISO 8601 UTC timestamp (`2026-10-13T09:30:00Z`) for Unix seconds `t`. +/// A time outside chrono's year range still converts: the calendar repeats every 400 years, so the +/// instant is moved into range by whole cycles and only the year is carried (matches the old +/// `civil_from_days`), rather than resetting to the epoch. fn iso8601(t: i64) -> String { - let (days, secs) = (t.div_euclid(86_400), t.rem_euclid(86_400)); - // Civil date from days since 1970-01-01 (Howard Hinnant's algorithm). - let z = days + 719_468; - let era = z.div_euclid(146_097); - let doe = z - era * 146_097; - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let day = doy - (153 * mp + 2) / 5 + 1; - let month = if mp < 10 { mp + 3 } else { mp - 9 }; - let year = yoe + era * 400 + i64::from(month <= 2); - format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}Z", secs / 3600, secs % 3600 / 60, secs % 60) + let (y, dt) = utc_parts(t, 0); + // `{:04}` rather than `%Y`, which writes a `+` before years past 9999. + format!("{y:04}-{}", dt.format("%m-%dT%H:%M:%SZ")) +} + +/// Unix seconds (+ nanos) → (proleptic Gregorian year, the instant moved into chrono's range by +/// whole 400-year cycles). Month, day and time of day come from chrono; only the year is carried, +/// which keeps every `i64` exact. chrono is the only calendar logic. +fn utc_parts(secs: i64, nanos: u32) -> (i64, chrono::NaiveDateTime) { + use chrono::Datelike; + let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400)); + let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097)); + // `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400. + let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt); + let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, nanos.min(999_999_999))); + match (date, time) { + (Some(d), Some(t)) => { + let y = i128::from(d.year()) + i128::from(cycles) * 400; + (i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.and_time(t)) + } + _ => (1970, chrono::NaiveDateTime::default()), // unreachable + } } /// The package's `dcterms:modified`: the given time, else the document's last save or creation. @@ -1582,6 +1595,14 @@ mod tests { (951_782_400, "2000-02-29T00:00:00Z"), (1_791_891_000, "2026-10-13T11:30:00Z"), (-86_400, "1969-12-31T00:00:00Z"), + // Years past 9999 keep the plain zero-padded year (no `+`). + (253_402_300_800, "10000-01-01T00:00:00Z"), + // Beyond chrono's range the old `civil_from_days` still converted; pin that output + // (never the epoch) in both directions and at the `i64` extremes. + (10_000_000_000_000, "318857-05-20T17:46:40Z"), + (-10_000_000_000_000, "-314918-08-13T06:13:20Z"), + (i64::MAX, "292277026596-12-04T15:30:07Z"), + (i64::MIN, "-292277022657-01-27T08:29:52Z"), ] { assert_eq!(iso8601(t), want); } diff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml index 6f40462b7..09da578ee 100644 --- a/crates/pdf/Cargo.toml +++ b/crates/pdf/Cargo.toml @@ -20,6 +20,7 @@ designcraft-images = { workspace = true } image = { workspace = true } thiserror = { workspace = true } log = { workspace = true } +chrono = { workspace = true } # The placed-PDF version check names hayro's PdfVersion (krilla::pdf::Pdf::version returns it). hayro-syntax = "0.7.2" diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 4e7627e9e..df44acbf2 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -436,20 +436,25 @@ fn date_time(t: i64) -> krilla::metadata::DateTime { .utc_offset_minute(0) } -/// Unix seconds (UTC) → (year, month, day, hour, minute, second), proleptic Gregorian. +/// Unix seconds (UTC) → (year, month, day, hour, minute, second), proleptic Gregorian. A time +/// outside chrono's year range still converts (the calendar repeats every 400 years, so the +/// instant is moved into range by whole cycles and only the year is carried, matching the old +/// `civil`), rather than resetting to the epoch. `date_time` applies the PDF `0..=9999` year clamp. pub(crate) fn civil(t: i64) -> (i64, u8, u8, u8, u8, u8) { - let days = t.div_euclid(86_400); - let secs = t.rem_euclid(86_400); - let z = days + 719_468; - let era = z.div_euclid(146_097); - let doe = z - era * 146_097; - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let day = doy - (153 * mp + 2) / 5 + 1; - let month = if mp < 10 { mp + 3 } else { mp - 9 }; - let year = yoe + era * 400 + i64::from(month <= 2); - (year, month as u8, day as u8, (secs / 3600) as u8, (secs / 60 % 60) as u8, (secs % 60) as u8) + use chrono::{Datelike, Timelike}; + let (days, sod) = (t.div_euclid(86_400), t.rem_euclid(86_400)); + let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097)); + // `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400. + let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt); + let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, 0)); + match (date, time) { + (Some(d), Some(t)) => { + let y = i128::from(d.year()) + i128::from(cycles) * 400; + let y = i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }); + (y, d.month() as u8, d.day() as u8, t.hour() as u8, t.minute() as u8, t.second() as u8) + } + _ => (1970, 1, 1, 0, 0, 0), // unreachable + } } /// A successful conversion and its name-independent diagnostic. Replayed once for each asset. @@ -2117,4 +2122,19 @@ mod eps_tests { assert_eq!(images, vec![("DeviceCMYK".to_string(), pixels.clone())], "{standard:?}"); } } + + #[test] + fn civil_dates_match_the_old_cycle_conversion_including_beyond_chrono() { + use super::civil; + assert_eq!(civil(0), (1970, 1, 1, 0, 0, 0)); + assert_eq!(civil(951_782_400), (2000, 2, 29, 0, 0, 0)); + assert_eq!(civil(1_791_504_000 + 3_723), (2026, 10, 9, 1, 2, 3)); + // Beyond chrono's year range the old `civil` still converted; pin that output, never the + // epoch. `date_time` is what clamps the year to the PDF `0..=9999` range afterwards. + assert_eq!(civil(253_402_300_800), (10000, 1, 1, 0, 0, 0)); + assert_eq!(civil(10_000_000_000_000), (318857, 5, 20, 17, 46, 40)); + assert_eq!(civil(-10_000_000_000_000), (-314918, 8, 13, 6, 13, 20)); + assert_eq!(civil(i64::MAX), (292277026596, 12, 4, 15, 30, 7)); + assert_eq!(civil(i64::MIN), (-292277022657, 1, 27, 8, 29, 52)); + } } diff --git a/crates/pptx/Cargo.toml b/crates/pptx/Cargo.toml index c6e1c876f..afc90c69c 100644 --- a/crates/pptx/Cargo.toml +++ b/crates/pptx/Cargo.toml @@ -19,6 +19,7 @@ skrifa = { workspace = true } image = { workspace = true } thiserror = { workspace = true } zip = { workspace = true } +chrono = { workspace = true } [dev-dependencies] designcraft-fonts = { workspace = true, features = ["testing"] } diff --git a/crates/pptx/src/package.rs b/crates/pptx/src/package.rs index ed5ad553b..13939aecc 100644 --- a/crates/pptx/src/package.rs +++ b/crates/pptx/src/package.rs @@ -290,20 +290,25 @@ fn core(p: &Package) -> String { s } -/// Unix seconds → UTC civil date and time (proleptic Gregorian). +/// Unix seconds → UTC civil date and time (proleptic Gregorian), year clamped to the W3CDTF +/// `0..=9999` range. A time outside chrono's year range still converts (the calendar repeats every +/// 400 years, so the instant is moved into range by whole cycles and only the year is carried, +/// matching the old `civil`), then the year is clamped — never a silent reset to the epoch. fn civil(t: i64) -> (i64, u32, u32, u32, u32, u32) { - let days = t.div_euclid(86_400); - let secs = t.rem_euclid(86_400); - let z = days + 719_468; - let era = z.div_euclid(146_097); - let doe = z.rem_euclid(146_097); - let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = (doy - (153 * mp + 2) / 5 + 1) as u32; - let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; - let y = yoe + era * 400 + i64::from(m <= 2); - (y.clamp(0, 9999), m, d, (secs / 3600) as u32, (secs / 60 % 60) as u32, (secs % 60) as u32) + use chrono::{Datelike, Timelike}; + let (days, sod) = (t.div_euclid(86_400), t.rem_euclid(86_400)); + let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097)); + // `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400. + let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt); + let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, 0)); + match (date, time) { + (Some(d), Some(t)) => { + let y = i128::from(d.year()) + i128::from(cycles) * 400; + let y = i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }); + (y.clamp(0, 9999), d.month(), d.day(), t.hour(), t.minute(), t.second()) + } + _ => (1970, 1, 1, 0, 0, 0), // unreachable + } } /// A neutral theme: text sets its own fonts and colours, so these only matter for new objects. @@ -331,5 +336,13 @@ mod tests { assert_eq!(civil(0), (1970, 1, 1, 0, 0, 0)); assert_eq!(civil(951_782_400), (2000, 2, 29, 0, 0, 0)); assert_eq!(civil(1_791_504_000 + 3_723), (2026, 10, 9, 1, 2, 3)); + // Beyond chrono's year range the old `civil` still converted and clamped only the year to + // the W3CDTF `0..=9999` range; pin that (month/day/time keep the real instant), never the + // epoch, in both directions and at the `i64` extremes. + assert_eq!(civil(253_402_300_800), (9999, 1, 1, 0, 0, 0)); + assert_eq!(civil(10_000_000_000_000), (9999, 5, 20, 17, 46, 40)); + assert_eq!(civil(-10_000_000_000_000), (0, 8, 13, 6, 13, 20)); + assert_eq!(civil(i64::MAX), (9999, 12, 4, 15, 30, 7)); + assert_eq!(civil(i64::MIN), (0, 1, 27, 8, 29, 52)); } } diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 5ababd51e..8cd446af1 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -17,9 +17,10 @@ designcraft-compose = { workspace = true } vello_cpu = { workspace = true } vello_common = { workspace = true } image = { workspace = true } -hayro = { workspace = true } +craft-pdf = { workspace = true, features = ["raster"] } designcraft-images = { workspace = true } log = { workspace = true } +thiserror = { workspace = true } serde = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index c42d8c897..6823fc006 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1235,29 +1235,93 @@ pub fn is_pdf(bytes: &[u8]) -> bool { /// Page count of a PDF. pub fn pdf_page_count(bytes: &[u8]) -> Option { - let pdf = hayro::hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.to_vec())).ok()?; + let pdf = craft_pdf::open_slice(bytes, None).ok()?; Some(pdf.pages().len()) } /// Size of PDF page `page` (0-based), in points, as it shows (crop box, rotation applied). pub fn pdf_page_size(bytes: &[u8], page: usize) -> Option<(f64, f64)> { - let pdf = hayro::hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.to_vec())).ok()?; + let pdf = craft_pdf::open_slice(bytes, None).ok()?; let p = pdf.pages().get(page)?; let (w, h) = p.render_dimensions(); Some((w as f64, h as f64)) } -/// PDF page `page` as PNG, its longer side about `max_side` pixels, with the `hidden` layers off; -/// only the `shown` part of it ([`designcraft_doc::Asset::shown_box`]) when given. +/// A placed page could not be rasterized completely for export. +#[derive(Debug, thiserror::Error)] +pub enum PdfRasterError { + #[error("{0}")] + Input(#[source] craft_pdf::OpenError), + #[error("{0}")] + Render(#[from] craft_pdf::raster::Error), + #[error("the PDF page could not be read or rendered")] + InvalidPage, + #[error("PDF page {page} is unavailable; choose an existing page in Image Import Options or place the replacement PDF again")] + MissingPage { page: usize }, + #[error("the selected PDF layers could not be hidden")] + LayerRewrite, + #[error("an image could not be decoded or exceeded the PDF renderer's resource limits; repair or replace this placed PDF before exporting again")] + ImageDecode, + #[error("the PDF renderer encountered an unsupported font")] + UnsupportedFont, + #[error( + "the PDF renderer could not read all page content or reached its resource limits; repair or replace this placed PDF before exporting again" + )] + ContentTruncated, +} + +/// Reject oversized PDFs before placement, layer rewriting or output allocation. +pub fn check_pdf_input_size(size: usize) -> Result<(), PdfRasterError> { + craft_pdf::check_input_size(size).map_err(PdfRasterError::Input) +} + +/// PDF page `page` as PNG, its longer side about `max_side` pixels, with the `hidden` layers off. +/// Only the `shown` part ([`designcraft_doc::Asset::shown_box`]) is kept when given. +/// Returns `None` on the same failures as [`pdf_page_png_checked`]. pub fn pdf_page_png(bytes: &[u8], page: usize, max_side: u32, hidden: &[String], shown: Option<[f64; 4]>) -> Option> { - let data = if hidden.is_empty() { bytes.to_vec() } else { pdf_hide_layers(bytes, hidden).unwrap_or_else(|| bytes.to_vec()) }; - let pm = render_pdf_page(&data, page, max_side)?; + pdf_page_png_checked(bytes, page, max_side, hidden, shown).ok() +} + +/// Rasterize a placed page for export, preserving available interpreter diagnostics. +/// This cannot detect backend approximations that do not emit an interpreter warning. +pub fn pdf_page_png_checked(bytes: &[u8], page: usize, max_side: u32, hidden: &[String], shown: Option<[f64; 4]>) -> Result, PdfRasterError> { + check_pdf_input_size(bytes.len())?; + let data = if hidden.is_empty() { + std::borrow::Cow::Borrowed(bytes) + } else { + std::borrow::Cow::Owned(pdf_hide_layers(bytes, hidden).ok_or(PdfRasterError::LayerRewrite)?) + }; + let raster = render_pdf_raster(&data, page, max_side)?; + if raster.diagnostics.image_decode_failed { + return Err(PdfRasterError::ImageDecode); + } + if raster.diagnostics.unsupported_font { + return Err(PdfRasterError::UnsupportedFont); + } + if raster.diagnostics.content_truncated { + return Err(PdfRasterError::ContentTruncated); + } + let pm = raster_pixmap(&raster); let pm = match shown { - Some(frac) => crop_pixmap(&pm, frac)?, + Some(frac) => crop_pixmap(&pm, frac).ok_or(PdfRasterError::InvalidPage)?, None => pm, }; - let pixels: Vec = pm.data().iter().flat_map(|p| [p.r, p.g, p.b, p.a]).collect(); - Some(Rendered { width: pm.width() as u32, height: pm.height() as u32, pixels }.to_png()) + let pixels = pm.data().iter().flat_map(|p| [p.r, p.g, p.b, p.a]).collect(); + Ok(Rendered { width: u32::from(pm.width()), height: u32::from(pm.height()), pixels }.to_png()) +} + +/// Rasterize PDF page `page` so its longer side is about `max_side` pixels (screen display). +/// Export callers that need failure diagnostics use [`pdf_page_png_checked`]. +pub fn render_pdf_page(bytes: &[u8], page: usize, max_side: u32) -> Option { + #[cfg(test)] + PDF_RENDERS.with(|n| n.set(n.get() + 1)); + let raster = render_pdf_raster(bytes, page, max_side).ok()?; + Some(raster_pixmap(&raster)) +} + +fn raster_pixmap(raster: &craft_pdf::raster::Raster) -> Pixmap { + let data = raster.bytes().as_chunks::<4>().0.iter().map(|c| vello_cpu::color::PremulRgba8 { r: c[0], g: c[1], b: c[2], a: c[3] }).collect(); + Pixmap::from_parts(data, raster.width as u16, raster.height as u16) } #[cfg(test)] @@ -1266,21 +1330,46 @@ thread_local! { static PDF_RENDERS: std::cell::Cell = const { std::cell::Cell::new(0) }; } -/// Rasterize PDF page `page` so its longer side is about `max_side` pixels (screen display; PDF -/// export embeds the page as vectors). -pub fn render_pdf_page(bytes: &[u8], page: usize, max_side: u32) -> Option { - #[cfg(test)] - PDF_RENDERS.with(|n| n.set(n.get() + 1)); - let pdf = hayro::hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.to_vec())).ok()?; - let p = pdf.pages().get(page)?; +fn render_pdf_raster(bytes: &[u8], page: usize, max_side: u32) -> Result { + let pdf = craft_pdf::open_slice(bytes, None).map_err(|e| match e { + craft_pdf::OpenError::Load(_) => PdfRasterError::InvalidPage, + other => PdfRasterError::Input(other), + })?; + let p = pdf.pages().get(page).ok_or(PdfRasterError::MissingPage { page: page.saturating_add(1) })?; let (w, h) = p.render_dimensions(); let scale = (max_side as f32 / w.max(h).max(1.0)).min(8.0); - let rs = hayro::RenderSettings { x_scale: scale, y_scale: scale, ..Default::default() }; - let pm = hayro::render(p, &hayro::RenderCache::new(), &hayro::hayro_interpret::InterpreterSettings::default(), &rs); - let (pw, ph) = (pm.width(), pm.height()); - let data: Vec = - pm.data_as_u8_slice().as_chunks::<4>().0.iter().map(|c| vello_cpu::color::PremulRgba8 { r: c[0], g: c[1], b: c[2], a: c[3] }).collect(); - Some(Pixmap::from_parts(data, pw, ph)) + craft_pdf::raster::render( + p, + &craft_pdf::raster::RenderCache::new(), + &craft_pdf::interpret::InterpreterSettings::default(), + &craft_pdf::raster::Options { scale: [scale; 2], ..Default::default() }, + ) + .map_err(PdfRasterError::Render) +} + +/// A page box of a PDF page (`crop`, `trim`, `bleed`, `art`, `media`) as (x, y, w, h) in points +/// from the top-left of the page as rendered (its visible crop box); missing boxes fall back to the +/// crop box, as PDF readers do. +pub fn pdf_page_box(bytes: &[u8], page: usize, kind: &str) -> Option<(f64, f64, f64, f64)> { + use craft_pdf::syntax::object::{Rect as PRect, dict::keys}; + let pdf = craft_pdf::open_slice(bytes, None).ok()?; + let p = pdf.pages().get(page)?; + let visible = p.intersected_crop_box(); + let page_box = match kind { + "media" => p.media_box(), + "trim" | "bleed" | "art" => { + let key: &[u8] = match kind { + "trim" => keys::TRIM_BOX, + "bleed" => keys::BLEED_BOX, + _ => keys::ART_BOX, + }; + p.raw().get::(key).map(|b| b.intersect(p.media_box())).unwrap_or(visible) + } + _ => visible, + }; + let transform = Affine::new(p.initial_transform(true).as_coeffs()); + let b = transform.transform_rect_bbox(Rect::new(page_box.x0, page_box.y0, page_box.x1, page_box.y1)); + b.is_finite().then_some((b.x0, b.y0, b.width(), b.height())) } /// The part of PDF page `page` that `crop` shows: its left, top, right and bottom edges as @@ -1289,10 +1378,10 @@ pub fn render_pdf_page(bytes: &[u8], page: usize, max_side: u32) -> Option Option<([f64; 4], (f64, f64))> { + use craft_pdf::syntax::object::{Rect as PRect, dict::keys}; use designcraft_doc::PdfCrop; - use hayro::hayro_syntax::object::{Rect as PRect, dict::keys}; const WHOLE: [f64; 4] = [0.0, 0.0, 1.0, 1.0]; - let pdf = hayro::hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.to_vec())).ok()?; + let pdf = craft_pdf::open_slice(bytes, None).ok()?; let p = pdf.pages().get(page)?; let (w, h) = p.render_dimensions(); let (w, h) = (f64::from(w), f64::from(h)); diff --git a/crates/render/src/pdf_bounds.rs b/crates/render/src/pdf_bounds.rs index 74665ae3c..17881df6e 100644 --- a/crates/render/src/pdf_bounds.rs +++ b/crates/render/src/pdf_bounds.rs @@ -2,8 +2,8 @@ //! of every painted path, glyph and image, each cut to the clip in force when it's painted. //! Nothing is rasterized, so measuring a page costs a fraction of drawing it. -use hayro::hayro_interpret::font::Glyph; -use hayro::hayro_interpret::{ +use craft_pdf::interpret::font::Glyph; +use craft_pdf::interpret::{ BlendMode, ClipPath, Context, Device, FillRule, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, RectExt, SoftMask, TransformExt, interpret_page, }; @@ -12,7 +12,7 @@ use vello_cpu::kurbo::{self, Affine, BezPath, Rect, Shape}; /// The bounds of what PDF page `page` draws, as fractions of the page as rendered (its visible /// crop box, rotated as it displays): left, top, right, bottom. `None` when it draws nothing. pub(crate) fn page_content_bounds(bytes: &[u8], page: usize) -> Option<[f64; 4]> { - let pdf = hayro::hayro_syntax::Pdf::new(std::sync::Arc::new(bytes.to_vec())).ok()?; + let pdf = craft_pdf::open_slice(bytes, None).ok()?; let p = pdf.pages().get(page)?; let (w, h) = p.render_dimensions(); let (w, h) = (f64::from(w), f64::from(h)); diff --git a/crates/render/src/pdf_layers.rs b/crates/render/src/pdf_layers.rs index 344585e3b..bc6d864a7 100644 --- a/crates/render/src/pdf_layers.rs +++ b/crates/render/src/pdf_layers.rs @@ -5,8 +5,8 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use hayro::hayro_syntax::Pdf; -use hayro::hayro_syntax::object::{Array, Dict, Name, ObjectIdentifier}; +use craft_pdf::syntax::Pdf; +use craft_pdf::syntax::object::{Array, Dict, Name, ObjectIdentifier}; /// (object number, name, on by default) of each layer. fn ocgs(pdf: &Pdf) -> Vec<(i32, String, bool)> { @@ -28,7 +28,7 @@ fn ocgs(pdf: &Pdf) -> Vec<(i32, String, bool)> { .filter_map(|r| { let d = xref.get::>(ObjectIdentifier::from(r))?; let name = d - .get::>(b"Name") + .get::>(b"Name") .map(|s| String::from_utf8_lossy(s.as_bytes()).to_string()) .unwrap_or_default(); let visible = if base_off { on.contains(&r.obj_number) } else { !off.contains(&r.obj_number) }; @@ -39,13 +39,13 @@ fn ocgs(pdf: &Pdf) -> Vec<(i32, String, bool)> { /// The layers of a placed PDF: (name, visible by default). pub fn pdf_layers(bytes: &[u8]) -> Vec<(String, bool)> { - Pdf::new(Arc::new(bytes.to_vec())).map(|p| ocgs(&p).into_iter().map(|(_, n, v)| (n, v)).collect()).unwrap_or_default() + craft_pdf::open_slice(bytes, None).map(|p| ocgs(&p).into_iter().map(|(_, n, v)| (n, v)).collect()).unwrap_or_default() } /// `bytes` with exactly the layers named in `hidden` off (the others on). `None` when the file /// has no such layers or can't be read. pub fn pdf_hide_layers(bytes: &[u8], hidden: &[String]) -> Option> { - let pdf = Pdf::new(Arc::new(bytes.to_vec())).ok()?; + let pdf = craft_pdf::open_slice(bytes, None).ok()?; let layers = ocgs(&pdf); if layers.is_empty() { return None; @@ -74,8 +74,8 @@ pub fn pdf_hide_layers(bytes: &[u8], hidden: &[String]) -> Option> { ); let xref_at = out.len(); let prev = { - let tail = String::from_utf8_lossy(&bytes[bytes.len().saturating_sub(64)..]).to_string(); - tail.rsplit("startxref").next().and_then(|t| t.split_whitespace().next()).and_then(|n| n.parse::().ok())? + let tail = String::from_utf8_lossy(&bytes[bytes.len().saturating_sub(1024)..]).to_string(); + tail.rsplit_once("startxref").and_then(|(_, t)| t.split_whitespace().next()).and_then(|n| n.parse::().ok())? }; out.extend_from_slice( format!("xref\n{root} 1\n{at:010} 00000 n\r\ntrailer\n<>\nstartxref\n{xref_at}\n%%EOF\n", root + 1) diff --git a/crates/render/tests/pdf_geometry.rs b/crates/render/tests/pdf_geometry.rs new file mode 100644 index 000000000..dcf9427e2 --- /dev/null +++ b/crates/render/tests/pdf_geometry.rs @@ -0,0 +1,61 @@ +fn page(width: f32, height: f32) -> Vec { + let content = format!("1 0 0 rg 0 0 {width} {height} re f"); + format!( + "%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 {width} {height}] /Contents 4 0 R >> endobj\n4 0 obj << /Length {} >> stream\n{content}\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF", + content.len() + ) + .into_bytes() +} + +#[test] +fn thin_pdf_page_has_visible_preview_and_export_pixels() { + let bytes = page(1000.0, 0.2); + let preview = designcraft_render::render_pdf_page(&bytes, 0, 700).unwrap(); + assert_eq!((preview.width(), preview.height()), (700, 1)); + assert!(preview.data().iter().any(|pixel| pixel.a > 0)); + let png = designcraft_render::pdf_page_png_checked(&bytes, 0, 700, &[], None).unwrap(); + let image = image::load_from_memory(&png).unwrap().to_rgba8(); + assert_eq!(image.dimensions(), (700, 1)); + assert!(image.pixels().any(|pixel| pixel[3] > 0)); +} + +#[test] +fn empty_media_box_preserves_the_parsers_default_page_repair() { + let bytes = page(0.0, 0.0); + let missing = String::from_utf8(bytes.clone()).unwrap().replace("/MediaBox [0 0 0 0]", ""); + let size = designcraft_render::pdf_page_size(&bytes, 0).unwrap(); + assert!(size.0 > 0.0 && size.1 > 0.0); + assert_eq!(Some(size), designcraft_render::pdf_page_size(missing.as_bytes(), 0)); + let preview = designcraft_render::render_pdf_page(&bytes, 0, 700).unwrap(); + let png = designcraft_render::pdf_page_png_checked(&bytes, 0, 700, &[], None).unwrap(); + let image = image::load_from_memory(&png).unwrap(); + assert_eq!((image.width(), image.height()), (u32::from(preview.width()), u32::from(preview.height()))); +} + +#[test] +fn pdf_input_limit_is_actionable_before_placement() { + let error = designcraft_render::check_pdf_input_size(craft_pdf::MAX_INPUT_BYTES + 1).unwrap_err(); + assert!(matches!(error, designcraft_render::PdfRasterError::Input(_))); + assert!(error.to_string().contains("input limit")); +} + +#[test] +fn pdf_boxes_match_scaled_and_rotated_page_coordinates() { + for (rotation, size, trim, media) in [ + (0, (200.0, 120.0), (20.0, 40.0, 140.0, 60.0), (-20.0, -40.0, 400.0, 200.0)), + (90, (120.0, 200.0), (20.0, 20.0, 60.0, 140.0), (-40.0, -20.0, 200.0, 400.0)), + (180, (200.0, 120.0), (40.0, 20.0, 140.0, 60.0), (-180.0, -40.0, 400.0, 200.0)), + (270, (120.0, 200.0), (40.0, 40.0, 60.0, 140.0), (-40.0, -180.0, 200.0, 400.0)), + ] { + let bytes = String::from_utf8(page(200.0, 100.0)).unwrap().replace( + "/MediaBox [0 0 200 100]", + &format!("/MediaBox [0 0 200 100] /CropBox [10 20 110 80] /TrimBox [20 30 90 60] /UserUnit 2 /Rotate {rotation}"), + ); + let bytes = bytes.as_bytes(); + assert_eq!(designcraft_render::pdf_page_size(bytes, 0), Some(size), "rotation {rotation}"); + assert_eq!(designcraft_render::pdf_page_box(bytes, 0, "crop"), Some((0.0, 0.0, size.0, size.1)), "rotation {rotation}"); + assert_eq!(designcraft_render::pdf_page_box(bytes, 0, "trim"), Some(trim), "rotation {rotation}"); + assert_eq!(designcraft_render::pdf_page_box(bytes, 0, "media"), Some(media), "rotation {rotation}"); + assert_eq!(designcraft_render::pdf_page_box(bytes, 0, "art"), Some((0.0, 0.0, size.0, size.1)), "missing box, rotation {rotation}"); + } +} diff --git a/crates/render/tests/placed_pdf.rs b/crates/render/tests/placed_pdf.rs index 234e43ba7..118453067 100644 --- a/crates/render/tests/placed_pdf.rs +++ b/crates/render/tests/placed_pdf.rs @@ -1,4 +1,4 @@ -//! Placed PDFs are drawn through pdfcraft's patched hayro crates. Calculator functions whose `idiv` +//! Placed PDFs are drawn through craft-pdf's patched Hayro crates. Calculator functions whose `idiv` //! divides by zero or overflows, stitching functions nested a hundred thousand deep, and mesh //! shadings with points far off the page, ten thousand overlapping triangles or forty thousand //! patches do not panic, overflow the stack or take more than a minute, and the rest of the page is @@ -9,14 +9,14 @@ use std::sync::Arc; use std::sync::mpsc::{self, RecvTimeoutError}; use std::time::Duration; +use craft_pdf::interpret::Function; +use craft_pdf::syntax::Pdf; +use craft_pdf::syntax::object::{Object, ObjectIdentifier}; use designcraft_compose::Cache; use designcraft_doc::build::NewDocument; use designcraft_doc::{Asset, AssetId, Content, Document, Graphic, Item, ItemId, PdfCrop, Shape, SpreadRef}; use designcraft_geom::{Affine, Rect, shapes}; use designcraft_render::{Rendered, Renderer, pdf_crop_box, pdf_page_size}; -use hayro::hayro_interpret::Function; -use hayro::hayro_syntax::Pdf; -use hayro::hayro_syntax::object::{Object, ObjectIdentifier}; const WHITE: [u8; 4] = [255, 255, 255, 255]; const RED: [u8; 4] = [255, 0, 0, 255]; diff --git a/docs/architecture.md b/docs/architecture.md index a90b75df3..6e0c4c66b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -113,3 +113,5 @@ builds (on manifest changes), packaging lint and the release workflow; `xtask ci | 2026-10-12 | minor | Documented cancelable Dock Quit through the native event-loop boundary | | 2026-10-12 | minor | Documented orderly native exit recovery and the remaining cancelable Dock Quit gap | | 2026-10-10 | major | Created from the code on main (92649ff) and `plan/architecture.md` | + +PDF parsing and placed-page rasterization use the pinned `craft-pdf` reader. DesignCraft retains document, placement, layer and export policy; dependency checks and bounds are described in [pdf-backend.md](pdf-backend.md). diff --git a/docs/pdf-backend.md b/docs/pdf-backend.md index fcc6afeaa..4cbc985d3 100644 --- a/docs/pdf-backend.md +++ b/docs/pdf-backend.md @@ -1,9 +1,12 @@ -# PDF writing boundary +# Shared PDF backend -DesignCraft pins `craft-pdf` and its patched Krilla 0.8.2 backend to craft-libs revision -`c3487320b2d1de657b860b3c8abb44e1f2b32bd8`. The `write-pdf` capability is required for -placed PDF pages and book merging. The ordinary reader dependencies, `craft-ui` and -`craft-print` revisions remain unchanged by this writer migration. +DesignCraft pins `craft-pdf`, patched Krilla 0.8.2 and the four Hayro reader crates to +craft-libs revision `89dcbb3060377a26c99a6d80caa16084366704c4`. Existing `craft-ui` and `craft-print` revisions +remain unchanged. The reader migration is reviewed on top of the separate writer change. + +## Writing + +The `write-pdf` capability is required for placed PDF pages and book merging. The shared library supplies fallible font copying/preparation, metadata setup, version and archival configuration, final serialization and a native unwind guard. DesignCraft retains @@ -32,3 +35,30 @@ Tests cover metadata key presence, hostile page selection, successful PDF export subprocess recovery for all three export entry points. Native/WASM export comparisons and independent PDF parsing/rendering supplement the repository gates. Physical printer output, Windows/Linux runtime and PDF/A/PDF/X certification remain separate acceptance work. + +## Reading + +Placed-PDF geometry, layers and raster output use `craft-pdf` with its explicit `raster` +feature. The facade and four patched Hayro crates are pinned to one immutable craft-libs +revision. The backend source was transferred from PdfCraft revision +`7437bde378f43856eada40af25b983812e615e8a`; the per-crate `vendor/SOURCE.json` manifests and `vendor/README.craft-reader.md` +record the transfer and subsequent craft-libs patches. Asset provenance and hashes +are recorded separately in ASSETS.md. +`cargo xtask pdf-deps` checks unique resolved packages, exact versions and sources. + +DesignCraft retains placement, layer-name policy, document state and the application render +cache. Render output is premultiplied RGBA8. Page boxes use the same crop, rotation and +UserUnit transform as raster output, including fractional placed-PDF dimensions. +Layered-PDF export stops on failed layer rewriting, missing pages or reported incomplete +content rather than saving an unchanged or incomplete placed page. Shared diagnostics only +report known backend warnings; they do not certify all PDF fidelity. + +DesignCraft uses a 512 MiB reader input bound, checked before parsing or copying. +Raster output has shared geometry/allocation bounds. Native unwind guards turn escaped +backend panics into errors; WASM aborts and process-wide allocation failures are not recoverable. +Application fonts, image assets, editing and export presets remain app-owned. Existing +craft-ui/craft-print versions are preserved. The reader migration builds on the independently qualified writer change. + +The shared commit must be published before a fresh public checkout can resolve these pins. +No sibling checkout is required. Licences and existing backend asset notices are recorded in +NOTICE, ASSETS.md and licenses/pdf-backend. The backend's removed LAB.icc is not restored. diff --git a/licenses/pdf-backend/hayro-cmap/LICENSE-APACHE b/licenses/pdf-backend/hayro-cmap/LICENSE-APACHE new file mode 100644 index 000000000..4947287f7 --- /dev/null +++ b/licenses/pdf-backend/hayro-cmap/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-cmap/LICENSE-MIT b/licenses/pdf-backend/hayro-cmap/LICENSE-MIT new file mode 100644 index 000000000..ae3742de4 --- /dev/null +++ b/licenses/pdf-backend/hayro-cmap/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-cmap/LICENSE.txt b/licenses/pdf-backend/hayro-cmap/LICENSE.txt new file mode 100644 index 000000000..2552cc990 --- /dev/null +++ b/licenses/pdf-backend/hayro-cmap/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 1990-2023 Adobe. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +Neither the name of Adobe nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/pdf-backend/hayro-interpret/LICENSE-APACHE b/licenses/pdf-backend/hayro-interpret/LICENSE-APACHE new file mode 100644 index 000000000..4947287f7 --- /dev/null +++ b/licenses/pdf-backend/hayro-interpret/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-interpret/LICENSE-MIT b/licenses/pdf-backend/hayro-interpret/LICENSE-MIT new file mode 100644 index 000000000..ae3742de4 --- /dev/null +++ b/licenses/pdf-backend/hayro-interpret/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-interpret/assets/CGATS_LICENSE.txt b/licenses/pdf-backend/hayro-interpret/assets/CGATS_LICENSE.txt new file mode 100644 index 000000000..550e190e5 --- /dev/null +++ b/licenses/pdf-backend/hayro-interpret/assets/CGATS_LICENSE.txt @@ -0,0 +1,116 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/ \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-interpret/assets/LICENSE_FOXIT b/licenses/pdf-backend/hayro-interpret/assets/LICENSE_FOXIT new file mode 100644 index 000000000..8b4ed6ddd --- /dev/null +++ b/licenses/pdf-backend/hayro-interpret/assets/LICENSE_FOXIT @@ -0,0 +1,27 @@ +// Copyright 2014 PDFium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/pdf-backend/hayro-jbig2/LICENSE-APACHE b/licenses/pdf-backend/hayro-jbig2/LICENSE-APACHE new file mode 100644 index 000000000..4947287f7 --- /dev/null +++ b/licenses/pdf-backend/hayro-jbig2/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-jbig2/LICENSE-MIT b/licenses/pdf-backend/hayro-jbig2/LICENSE-MIT new file mode 100644 index 000000000..ae3742de4 --- /dev/null +++ b/licenses/pdf-backend/hayro-jbig2/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-syntax/LICENSE-APACHE b/licenses/pdf-backend/hayro-syntax/LICENSE-APACHE new file mode 100644 index 000000000..4947287f7 --- /dev/null +++ b/licenses/pdf-backend/hayro-syntax/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro-syntax/LICENSE-MIT b/licenses/pdf-backend/hayro-syntax/LICENSE-MIT new file mode 100644 index 000000000..ae3742de4 --- /dev/null +++ b/licenses/pdf-backend/hayro-syntax/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/licenses/pdf-backend/hayro/LICENSE-APACHE b/licenses/pdf-backend/hayro/LICENSE-APACHE new file mode 100644 index 000000000..4947287f7 --- /dev/null +++ b/licenses/pdf-backend/hayro/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/licenses/pdf-backend/hayro/LICENSE-MIT b/licenses/pdf-backend/hayro/LICENSE-MIT new file mode 100644 index 000000000..ae3742de4 --- /dev/null +++ b/licenses/pdf-backend/hayro/LICENSE-MIT @@ -0,0 +1,7 @@ +Copyright (c) The Hayro Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index 2d7b80ee8..12ad8e0f2 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -148,7 +148,7 @@ if has appimage; then TOOL="$CARGO_TARGET_DIR/appimagetool-$APPIMAGETOOL_VERSION-$ARCH.AppImage" if [ ! -x "$TOOL" ]; then download="$TOOL.download" - curl -fsSL -o "$download" \ + curl --proto '=https' -fsSL -o "$download" \ "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c - chmod 755 "$download" diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..f1095122d --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,2 @@ +sonar.projectKey=storytold_designcraft +sonar.organization=storytold diff --git a/xtask/src/main.rs b/xtask/src/main.rs index c89b7e8d7..e730feb51 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -20,12 +20,12 @@ usage: cargo xtask commands: assets check that every icon/image/font/asset is attributed in ASSETS.md layers enforce the crate dependency layering (plan/architecture.md §3) + pdf-deps verify the pinned PDF backend, including Krilla dependencies wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates ci fmt --check, clippy -D warnings, test, assets, layers, wasm (stops at first failure) corpus [--download] show where test corpora live; --download fetches PngSuite into corpus/pngsuite stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`) - pdf-deps verify the pinned shared PDF writer and patched Krilla parity recompute the InDesign feature-parity summary in docs/parity-checklist.md ico ... pack square PNGs (<= 256 px) into a Windows .ico (see packaging/icons.sh) @@ -37,11 +37,11 @@ fn main() -> ExitCode { let result = match args.first().map(String::as_str) { Some("assets") => assets::run(&root()), Some("layers") => cmd_layers(), + Some("pdf-deps") => pdf_deps::run(&root()), Some("wasm") => cmd_wasm(), Some("ci") => cmd_ci(), Some("corpus") => cmd_corpus(rest.contains(&"--download")), Some("stats") => stats::run(&root(), rest.contains(&"--exact")), - Some("pdf-deps") => pdf_deps::run(&root()), Some("parity") => parity::run(&root()), Some("ico") => ico::run(&rest), Some("version") => version::run(&root(), &rest), @@ -162,6 +162,7 @@ fn cmd_ci() -> Result<(), String> { run(c, "cargo fmt --all -- --check") }), ), + ("pdf-deps", Box::new(|| pdf_deps::run(&root()))), ( "clippy", Box::new(|| { @@ -178,7 +179,6 @@ fn cmd_ci() -> Result<(), String> { run(c, "cargo test --workspace") }), ), - ("pdf-deps", Box::new(|| pdf_deps::run(&root()))), ("assets", Box::new(|| assets::run(&root()))), ("layers", Box::new(cmd_layers)), ("wasm", Box::new(cmd_wasm)), diff --git a/xtask/src/pdf_deps.rs b/xtask/src/pdf_deps.rs index d7dfb1cb7..bb2a4f01d 100644 --- a/xtask/src/pdf_deps.rs +++ b/xtask/src/pdf_deps.rs @@ -1,11 +1,18 @@ -//! Check the shared writer source, including transitive users such as krilla-svg. +//! Check the shared reader source, including the parser reached through Krilla. use std::path::Path; use std::process::Command; use serde_json::Value; -const REV: &str = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8"; -const PACKAGES: &[(&str, &str)] = &[("craft-pdf", "0.1.0"), ("krilla", "0.8.2")]; +const REV: &str = "89dcbb3060377a26c99a6d80caa16084366704c4"; +const PACKAGES: &[(&str, &str)] = &[ + ("craft-pdf", "0.1.0"), + ("krilla", "0.8.2"), + ("hayro", "0.7.1"), + ("hayro-interpret", "0.7.0"), + ("hayro-syntax", "0.7.2"), + ("hayro-jbig2", "0.3.0"), +]; pub fn run(root: &Path) -> Result<(), String> { let output = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) @@ -18,7 +25,7 @@ pub fn run(root: &Path) -> Result<(), String> { } let metadata: Value = serde_json::from_slice(&output.stdout).map_err(|e| format!("cargo metadata: {e}"))?; validate(&metadata)?; - println!("PDF writer: facade and patched Krilla resolve once to {REV}"); + println!("PDF backend: facade, Krilla and Hayro resolve once to {REV}"); Ok(()) } @@ -34,6 +41,15 @@ fn validate(metadata: &Value) -> Result<(), String> { return Err(format!("{name}: expected {version} from {source}; got {package}")); } } + let nodes = metadata.pointer("/resolve/nodes").and_then(Value::as_array).ok_or("cargo metadata has no resolved nodes")?; + let facade = packages.iter().find(|p| p["name"] == "craft-pdf").ok_or("craft-pdf: missing package")?; + let node = nodes.iter().find(|n| n["id"] == facade["id"]).ok_or("craft-pdf: missing resolved node")?; + let features = node["features"].as_array().ok_or("craft-pdf: missing features")?; + for required in ["read", "interpret", "raster", "write", "write-pdf"] { + if !features.iter().any(|f| f.as_str() == Some(required)) { + return Err(format!("craft-pdf: missing required {required} feature")); + } + } Ok(()) } @@ -43,17 +59,17 @@ mod tests { use serde_json::json; fn metadata() -> Value { - json!({"packages": PACKAGES.iter().map(|(name, version)| json!({"name":name,"version":version, - "source":format!("git+https://github.com/storytold/craft-libs?rev={REV}#{REV}")})).collect::>()}) + json!({"packages": PACKAGES.iter().map(|(name, version)| json!({"id":name,"name":name,"version":version, + "source":format!("git+https://github.com/storytold/craft-libs?rev={REV}#{REV}")})).collect::>(), "resolve": {"nodes": [{"id": "craft-pdf", "features": ["read", "interpret", "raster", "write", "write-pdf"]}]}}) } #[test] - fn accepts_one_pinned_facade_and_writer() { + fn accepts_one_pinned_facade_and_reader() { assert!(validate(&metadata()).is_ok()); } #[test] - fn rejects_unpatched_and_local_writer_sources() { + fn rejects_unpatched_and_local_reader_sources() { for source in [Value::Null, json!("registry+https://github.com/rust-lang/crates.io-index")] { let mut m = metadata(); m["packages"][1]["source"] = source; @@ -63,7 +79,7 @@ mod tests { #[test] fn rejects_missing_duplicate_and_wrong_version_packages() { - for index in [0, 1] { + for index in 0..PACKAGES.len() { let mut m = metadata(); m["packages"][index]["version"] = json!("99.0.0"); assert!(validate(&m).is_err()); @@ -77,6 +93,18 @@ mod tests { } } + #[test] + fn rejects_missing_reader_or_writer_features() { + for index in 0..5 { + let mut m = metadata(); + m["resolve"]["nodes"][0]["features"].as_array_mut().unwrap().remove(index); + assert!(validate(&m).is_err()); + } + let mut m = metadata(); + m["resolve"] = Value::Null; + assert!(validate(&m).is_err()); + } + #[test] fn rejects_facade_revision_drift_and_malformed_metadata() { let mut m = metadata();