diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae2f93fb..7f2730a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: - name: CLI without the MCP server still builds if: runner.os == 'Linux' - run: cargo check -p printcraft-cli --no-default-features + run: cargo check -p pdfcraft-cli --no-default-features wasm: name: wasm32 check diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 4c9849b0..a86fc140 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -49,4 +49,4 @@ jobs: CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked # The release packaging, end to end (a quicker release profile than the real release). CARGO_PROFILE_RELEASE_LTO=false CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 packaging/freebsd/package.sh - tar -tzf dist/release/printcraft-*-freebsd-*.tar.gz | grep -E '/bin/printcraft$|/bin/printcraft-cli$|applications/' + tar -tzf dist/release/pdfcraft-*-freebsd-*.tar.gz | grep -E '/bin/pdfcraft$|/bin/pdfcraft-cli$|applications/' diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index 29817c7e..39adb8d5 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -46,12 +46,12 @@ jobs: } - name: XML, desktop entry, AppStream, Flatpak manifest run: | - xmllint --noout packaging/windows/printcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* + xmllint --noout packaging/windows/pdfcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* # Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon (#57). - if grep -oE '"$RUNNER_TEMP/ai.storyteller.printcraft.metainfo.xml" - appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.printcraft.metainfo.xml" - python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.printcraft.yml')); assert m['id'] == 'ai.storyteller.printcraft' and m['command'] == 'printcraft'" + desktop-file-validate packaging/linux/ai.storyteller.pdfcraft.desktop + sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.pdfcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.pdfcraft.metainfo.xml" + appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.pdfcraft.metainfo.xml" + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.pdfcraft.yml')); assert m['id'] == 'ai.storyteller.pdfcraft' and m['command'] == 'pdfcraft'" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7533b7c6..7e709422 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,5 @@ # Release pipeline: every push to `release` builds signed installers for every platform and -# creates or updates the draft GitHub Release "PrintCraft v". See docs/releasing.md. +# creates or updates the draft GitHub Release "PdfCraft v". See docs/releasing.md. # # All jobs run in the `release` environment (restricted to the `release` branch), which holds # the signing secrets. Every secret is optional: missing ones produce unsigned artifacts and a @@ -28,7 +28,7 @@ env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 - PRINTCRAFT_BUILD_SHA: ${{ github.sha }} + PDFCRAFT_BUILD_SHA: ${{ github.sha }} INPUT_VERSION: ${{ inputs.version }} jobs: @@ -58,7 +58,7 @@ jobs: echo "date=$(date -u +%Y-%m-%d)" echo "prerelease=$pre" } >>"$GITHUB_OUTPUT" - echo "### PrintCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" + echo "### PdfCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" macos: name: macOS universal @@ -66,8 +66,8 @@ jobs: runs-on: macos-15 environment: release env: - PRINTCRAFT_VERSION: ${{ needs.version.outputs.version }} - PRINTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + PDFCRAFT_VERSION: ${{ needs.version.outputs.version }} + PDFCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 # The craft-fonts build input (Japanese fonts): every release embeds it. Bump deliberately. @@ -85,7 +85,7 @@ jobs: key: release-macos-universal - name: Apply version override if: inputs.version != '' - run: cargo xtask version set "$PRINTCRAFT_VERSION" + run: cargo xtask version set "$PDFCRAFT_VERSION" - name: Import signing certificate env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} @@ -125,8 +125,8 @@ jobs: # .github/workflows/windows-arm64.yml installs and runs it on ARM64 hardware. - { arch: arm64, target: aarch64-pc-windows-msvc } env: - PRINTCRAFT_VERSION: ${{ needs.version.outputs.version }} - PRINTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + PDFCRAFT_VERSION: ${{ needs.version.outputs.version }} + PDFCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} steps: - uses: actions/checkout@v4 # The craft-fonts build input (Japanese fonts): every release embeds it. Bump deliberately. @@ -145,7 +145,7 @@ jobs: - name: Apply version override if: inputs.version != '' shell: bash - run: cargo xtask version set "$PRINTCRAFT_VERSION" + run: cargo xtask version set "$PDFCRAFT_VERSION" - name: Install WiX shell: pwsh run: dotnet tool install --global wix --version 5.0.2 @@ -183,8 +183,8 @@ jobs: - { arch: x86_64, runner: ubuntu-22.04, deb: amd64 } - { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 } env: - PRINTCRAFT_VERSION: ${{ needs.version.outputs.version }} - PRINTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + PDFCRAFT_VERSION: ${{ needs.version.outputs.version }} + PDFCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} NFPM_VERSION: 2.47.0 steps: - uses: actions/checkout@v4 @@ -210,7 +210,7 @@ jobs: key: release-linux-${{ matrix.arch }} - name: Apply version override if: inputs.version != '' - run: cargo xtask version set "$PRINTCRAFT_VERSION" + run: cargo xtask version set "$PDFCRAFT_VERSION" - name: Build and package (AppImage, deb, rpm, tar.gz) env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts @@ -219,9 +219,9 @@ jobs: - name: Check the packages run: | dpkg-deb --info dist/release/*.deb - dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/printcraft$|metainfo|applications' - ldd target/release/printcraft - python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.printcraft.yml')); assert m['id'] == 'ai.storyteller.printcraft'" + dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/pdfcraft$|metainfo|applications' + ldd target/release/pdfcraft + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.pdfcraft.yml')); assert m['id'] == 'ai.storyteller.pdfcraft'" for appimage in dist/release/*.AppImage; do chmod +x "$appimage" APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version @@ -240,8 +240,8 @@ jobs: environment: release timeout-minutes: 180 env: - PRINTCRAFT_VERSION: ${{ needs.version.outputs.version }} - PRINTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + PDFCRAFT_VERSION: ${{ needs.version.outputs.version }} + PDFCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} VERSION_OVERRIDE: ${{ inputs.version }} steps: - uses: actions/checkout@v4 @@ -260,7 +260,7 @@ jobs: usesh: true cpu: 4 mem: 12288 - envs: "PRINTCRAFT_VERSION PRINTCRAFT_BUILD_DATE VERSION_OVERRIDE" + envs: "PDFCRAFT_VERSION PDFCRAFT_BUILD_DATE VERSION_OVERRIDE" # The same packages as the FreeBSD CI workflow, plus bash for the packaging script. prepare: | pkg install -y bash git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib @@ -270,7 +270,7 @@ jobs: . "$HOME/.cargo/env" git config --global --add safe.directory "$PWD" export CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1 - if [ -n "$VERSION_OVERRIDE" ]; then cargo xtask version set "$PRINTCRAFT_VERSION"; fi + if [ -n "$VERSION_OVERRIDE" ]; then cargo xtask version set "$PDFCRAFT_VERSION"; fi packaging/freebsd/package.sh # Only dist/ comes back out of the VM: copying the target dir back fails on large builds. rm -rf target @@ -286,8 +286,8 @@ jobs: runs-on: ubuntu-latest environment: release env: - PRINTCRAFT_VERSION: ${{ needs.version.outputs.version }} - PRINTCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + PDFCRAFT_VERSION: ${{ needs.version.outputs.version }} + PDFCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} TRUNK_VERSION: 0.21.14 steps: - uses: actions/checkout@v4 @@ -311,7 +311,7 @@ jobs: trunk --version - name: Apply version override if: inputs.version != '' - run: cargo xtask version set "$PRINTCRAFT_VERSION" + run: cargo xtask version set "$PDFCRAFT_VERSION" - name: Build and zip env: CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts @@ -349,7 +349,7 @@ jobs: working-directory: artifacts run: | tag="v$VERSION" - title="PrintCraft v$VERSION" + title="PdfCraft v$VERSION" pre=() if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml index bb05a3eb..d831ffc5 100644 --- a/.github/workflows/windows-arm64.yml +++ b/.github/workflows/windows-arm64.yml @@ -53,7 +53,7 @@ jobs: run: ./packaging/windows/package.ps1 -Arch arm64 - uses: actions/upload-artifact@v4 with: - name: printcraft-windows-arm64 + name: pdfcraft-windows-arm64 path: dist/arm64/* if-no-files-found: error retention-days: 7 @@ -66,7 +66,7 @@ jobs: steps: - uses: actions/download-artifact@v4 with: - name: printcraft-windows-arm64 + name: pdfcraft-windows-arm64 path: dist - name: Install the MSI, check it, run it, uninstall it shell: pwsh @@ -75,8 +75,8 @@ jobs: $msi = (Get-ChildItem dist/*.msi | Select-Object -First 1).FullName $p = Start-Process msiexec.exe -ArgumentList '/i', "`"$msi`"", '/qn', '/l*v', 'install.log' -Wait -PassThru if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 60; throw "msiexec /i exited $($p.ExitCode)" } - $dir = Join-Path $env:ProgramFiles 'PrintCraft' - foreach ($exe in 'printcraft.exe', 'printcraft-cli.exe') { + $dir = Join-Path $env:ProgramFiles 'PdfCraft' + foreach ($exe in 'pdfcraft.exe', 'pdfcraft-cli.exe') { $path = Join-Path $dir $exe if (-not (Test-Path $path)) { Get-ChildItem -Recurse $env:ProgramFiles -Filter $exe | Select-Object -First 3; throw "$exe not installed in $dir" } $bytes = [System.IO.File]::ReadAllBytes($path) @@ -85,9 +85,9 @@ jobs: Write-Output "ok $path (ARM64)" } # The installed CLI runs natively: open, render and edit a small PDF through the engine. - $cli = Join-Path $dir 'printcraft-cli.exe' + $cli = Join-Path $dir 'pdfcraft-cli.exe' & $cli tools | Out-Null - if ($LASTEXITCODE -ne 0) { throw "printcraft-cli tools exited $LASTEXITCODE" } + if ($LASTEXITCODE -ne 0) { throw "pdfcraft-cli tools exited $LASTEXITCODE" } $body = 'BT /F1 24 Tf 20 150 Td (Hello ARM64) Tj ET' $objs = @('<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>', @@ -103,13 +103,13 @@ jobs: $steps = '[{"tool":"doc_open","args":{"path":"hello.pdf"}},{"tool":"text_paragraphs","args":{"doc":1,"page":1}},{"tool":"text_edit","args":{"doc":1,"page":1,"paragraph":1,"text":"Edited on ARM64"}},{"tool":"page_render","args":{"doc":1,"page":1,"dpi":72},"out":"smoke/page.png"},{"tool":"doc_save","args":{"doc":1,"path":"out.pdf"}}]' [System.IO.File]::WriteAllText("$PWD\smoke\steps.json", $steps) $out = & $cli run --script smoke\steps.json --root smoke 2>&1 | Out-String - if ($LASTEXITCODE -ne 0) { Write-Output $out; throw "printcraft-cli run exited $LASTEXITCODE" } + if ($LASTEXITCODE -ne 0) { Write-Output $out; throw "pdfcraft-cli run exited $LASTEXITCODE" } if ($out -notmatch 'Hello ARM64') { Write-Output $out; throw 'text_paragraphs did not read the page' } foreach ($f in 'smoke\page.png', 'smoke\out.pdf') { if (-not (Test-Path $f) -or (Get-Item $f).Length -eq 0) { Write-Output $out; throw "$f was not written" } } - Write-Output 'ok printcraft-cli: opened, read, edited, rendered and saved a PDF on ARM64' + Write-Output 'ok pdfcraft-cli: opened, read, edited, rendered and saved a PDF on ARM64' $p = Start-Process msiexec.exe -ArgumentList '/x', "`"$msi`"", '/qn' -Wait -PassThru if ($p.ExitCode -ne 0) { throw "msiexec /x exited $($p.ExitCode)" } - if (Test-Path (Join-Path $dir 'printcraft.exe')) { throw 'uninstall left printcraft.exe behind' } + if (Test-Path (Join-Path $dir 'pdfcraft.exe')) { throw 'uninstall left pdfcraft.exe behind' } Write-Output 'ok uninstalled' test: diff --git a/AGENTS.md b/AGENTS.md index 0c6d8407..0720da26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ Every asset must be one of: - **Public domain.** - **Contributor-original:** created, photographed or generated by a contributor who owns it and licenses it under the project licence (`MIT OR Apache-2.0`). -**One narrow exception: the ArtCraft brand.** The ArtCraft name, wordmark and logos in `docs/brand/` are trademarks of the ArtCraft Team and are **not** open source (manifest identifier `LicenseRef-Storyteller-Trademark`, terms in `docs/brand/LICENSE-brand.txt`). They may be used only unmodified, only within PrintCraft, and only as `kind = "logo"` entries from that directory, to identify the project. Forks and modified versions must remove them. Never add other files under this licence, and never modify the logos. +**One narrow exception: the ArtCraft brand.** The ArtCraft name, wordmark and logos in `docs/brand/` are trademarks of the ArtCraft Team and are **not** open source (manifest identifier `LicenseRef-Storyteller-Trademark`, terms in `docs/brand/LICENSE-brand.txt`). They may be used only unmodified, only within PdfCraft, and only as `kind = "logo"` entries from that directory, to identify the project. Forks and modified versions must remove them. Never add other files under this licence, and never modify the logos. Anything else is forbidden, including assets whose licence you cannot prove from the source. Examples: - system fonts (Apple, Microsoft, Google Android system UI fonts that are not openly licensed); @@ -46,7 +46,7 @@ Anything else is forbidden, including assets whose licence you cannot prove from - "free for personal use" material; - screenshots of other people's software. -**Screenshots of PrintCraft** are contributor-original. Everything visible in them must itself be allowed: our UI, Lucide icons, OFL fonts, and demo documents built only from allowed assets. Never screenshot a document that uses proprietary fonts or images. +**Screenshots of PdfCraft** are contributor-original. Everything visible in them must itself be allowed: our UI, Lucide icons, OFL fonts, and demo documents built only from allowed assets. Never screenshot a document that uses proprietary fonts or images. ### 1.3 Every asset is attributed @@ -69,7 +69,7 @@ Anything else is forbidden, including assets whose licence you cannot prove from ### 1.4 Fonts live in craft-fonts - Font assets shared by the Crafting Apps, including every Japanese font, live in [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), not here. **Never commit a font file to this repository**; adding a font means adding it to craft-fonts. (The Latin UI fonts already in `assets/fonts/` — Inter, JetBrains Mono, Dancing Script — stay.) Team members: the shared rules are in [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md) (internal). -- craft-fonts is an **optional build input**, never a Cargo dependency: `CRAFT_FONTS_DIR= cargo build` makes `crates/fonts/build.rs` embed its fonts as `printcraft_fonts::CRAFT_FONTS`. Without it PrintCraft builds, tests and runs, but has no Japanese face (Japanese UI text shows replacement boxes, and writing Japanese into a PDF returns a clear error). Release builds always set it (`.github/workflows/release.yml`), and `ATTRIBUTION.toml` lists it as a `[[build_input]]`. +- craft-fonts is an **optional build input**, never a Cargo dependency: `CRAFT_FONTS_DIR= cargo build` makes `crates/fonts/build.rs` embed its fonts as `pdfcraft_fonts::CRAFT_FONTS`. Without it PdfCraft builds, tests and runs, but has no Japanese face (Japanese UI text shows replacement boxes, and writing Japanese into a PDF returns a clear error). Release builds always set it (`.github/workflows/release.yml`), and `ATTRIBUTION.toml` lists it as a `[[build_input]]`. ### 1.5 Procedure for adding or changing an asset @@ -94,16 +94,16 @@ If you find an asset that breaks these rules, stop and remove it from the reposi ## 3. Agent control (automation and MCP) -- **Everything is reachable headlessly.** Every user-facing feature must be callable without the GUI: an engine API, then a tool in `crates/automation` (`printcraft-cli run` and MCP use the same table). A feature is not done until its tool exists and has an end-to-end test (`crates/automation/README.md`, "Adding a tool"). +- **Everything is reachable headlessly.** Every user-facing feature must be callable without the GUI: an engine API, then a tool in `crates/automation` (`pdfcraft-cli run` and MCP use the same table). A feature is not done until its tool exists and has an end-to-end test (`crates/automation/README.md`, "Adding a tool"). - **The UI control channel is opt-in too.** It runs only when the app is launched with `--control `. It binds to loopback, uses a fresh random token per launch, and writes it to an owner-only file. Never enable it by default, widen the bind address, or log the token. -- **The MCP server is opt-in.** Never make the app, the CLI or an installer start it, register it with an agent, or listen on a port by default. It runs only when a user (or an agent the user configured) launches `printcraft-cli mcp`. Any future network transport must be off by default, bound to loopback and authenticated with a token. +- **The MCP server is opt-in.** Never make the app, the CLI or an installer start it, register it with an agent, or listen on a port by default. It runs only when a user (or an agent the user configured) launches `pdfcraft-cli mcp`. Any future network transport must be off by default, bound to loopback and authenticated with a token. - **Safe by default.** Tools must never discard unsaved work silently, must write atomically, must honour `--root`, and must return errors an agent can act on (say what was wrong and what is allowed). - **Track parity honestly.** Every user-facing feature has an entry in `parity/acrobat-features.toml`. Mark it `shipped` only when it works end to end and the entry cites a passing test (`evidence`); `cargo xtask parity` (part of `xtask ci`) rejects claims whose commands, tools or tests don't exist. -- **Use it to test.** Agents should drive the engine through `printcraft-cli run --script` or the MCP tools, and look at `page_render` output, to check their own changes. For UI changes, launch the app with `--control` and use `printcraft-cli ui` (`inspect`, `click`, `screenshot`) to check the real interface. +- **Use it to test.** Agents should drive the engine through `pdfcraft-cli run --script` or the MCP tools, and look at `page_render` output, to check their own changes. For UI changes, launch the app with `--control` and use `pdfcraft-cli ui` (`inspect`, `click`, `screenshot`) to check the real interface. ## 4. Never crash -PrintCraft opens files from strangers, and people trust it with their work. A malformed PDF, a bad tool argument, a corrupt settings file or a full disk must produce an error the user (or agent) can act on, never a crash and never lost work. **This outranks feature work:** don't ship a feature by adding a panic path, and fix a crash before building on top of it. The rules below are the ones that bind this repository; the shared standard for every Crafting App, [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md), is internal to the storytold team. +PdfCraft opens files from strangers, and people trust it with their work. A malformed PDF, a bad tool argument, a corrupt settings file or a full disk must produce an error the user (or agent) can act on, never a crash and never lost work. **This outranks feature work:** don't ship a feature by adding a panic path, and fix a crash before building on top of it. The rules below are the ones that bind this repository; the shared standard for every Crafting App, [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md), is internal to the storytold team. - **Fail with `Result`.** Non-test code returns errors through the crate's error type and `?`. Where readers are lenient by design, fall back gracefully and record the repair (see Fidelity in `CLAUDE.md`). - **No panicking shortcuts.** No `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!` or `unimplemented!` outside tests, unless the call is provably infallible (e.g. writing to a `Vec`), with a comment saying why. `assert!` only for internal invariants that input cannot reach. diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 2f0ded9a..dc602514 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -2,7 +2,7 @@ -Every asset PrintCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). +Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). ## In this repository (242) @@ -202,41 +202,41 @@ Every asset PrintCraft includes, bundles or uses to build its published material | `vendor/hayro-interpret/assets/FoxitSymbol.pfb` | FoxitSymbol (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) | | `vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc` | CGATS001Compat v2 micro ICC profile | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | CC0-1.0 | https://github.com/saucecontrol/Compact-ICC-Profiles | Default CMYK colour conversion (vendored hayro-interpret) | | `vendor/hayro-interpret/assets/LAB.icc` | Lab abstract ICC profile | The Hayro Authors (generated with LittleCMS 2) | MIT OR Apache-2.0 | https://github.com/LaurenzV/hayro (hayro-interpret 0.7.0 assets/) | Lab colour space conversion (vendored hayro-interpret) | -| `docs/brand/artcraft-logo-white.png` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it. | -| `docs/brand/artcraft-logo-white.svg` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it. | -| `docs/brand/artcraft-logo.png` | ArtCraft brand: artcraft-logo | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it. | -| `docs/brand/artcraft-logo.svg` | ArtCraft brand: artcraft-logo | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it. | -| `docs/brand/artcraft-mark-black.png` | ArtCraft brand: artcraft-mark-black | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it. | -| `docs/brand/artcraft-mark-black.svg` | ArtCraft brand: artcraft-mark-black | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it. | -| `docs/brand/artcraft-mark.png` | ArtCraft brand: artcraft-mark | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it. | -| `docs/brand/artcraft-mark.svg` | ArtCraft brand: artcraft-mark | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it. | -| `assets/app-icon/printcraft.svg` | PrintCraft app icon (master vector) | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Master artwork; every other app-icon file is rendered from it (packaging/icons.sh) | -| `assets/app-icon/printcraft-small.svg` | PrintCraft app icon (light vector) | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | README header and assets/app-icon/README.md | -| `assets/app-icon/printcraft-1024.png` | PrintCraft app icon, 1024 px on Apple's icon grid | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Runtime window/Dock icon on macOS (apps/printcraft/src/main.rs) | -| `assets/app-icon/printcraft.icns` | PrintCraft app icon (macOS .icns) | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | macOS app bundle icon | -| `assets/app-icon/printcraft.ico` | PrintCraft app icon (Windows .ico, 16-256 px) | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Embedded in printcraft.exe by apps/printcraft/build.rs (Explorer, taskbar, Start menu, Alt-Tab) | -| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.printcraft.png` | PrintCraft app icon, 16 px | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop) | -| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.printcraft.png` | PrintCraft app icon, 24 px | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop) | -| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.printcraft.png` | PrintCraft app icon, 32 px | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop) | -| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.printcraft.png` | PrintCraft app icon, 48 px | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop) | -| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.printcraft.png` | PrintCraft app icon, 64 px | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop) | -| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.printcraft.png` | PrintCraft app icon, 128 px | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop) | -| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.printcraft.png` | PrintCraft app icon, 256 px | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop); runtime window icon on Windows and Linux (apps/printcraft/src/main.rs) | -| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.printcraft.png` | PrintCraft app icon, 512 px | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop) | -| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.printcraft.svg` | PrintCraft app icon (Linux scalable) | PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop) | -| `docs/images/printcraft-bookmarks.png` | PrintCraft screenshot: Bookmarks | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: navigation. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-dark.png` | PrintCraft screenshot: Dark theme | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: themes. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-find.png` | PrintCraft screenshot: Find text | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: search. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-forms.png` | PrintCraft screenshot: Interactive form | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: forms. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-layers.png` | PrintCraft screenshot: Layers | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: layers. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-organize.png` | PrintCraft screenshot: Organize pages | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: organize. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-palette.png` | PrintCraft screenshot: Command palette | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: tools. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-properties.png` | PrintCraft screenshot: Document properties | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: metadata. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-scripts.png` | PrintCraft screenshot: Scripts of the world | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: rendering. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-split.png` | PrintCraft screenshot: Split dialog | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: split. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-tools.png` | PrintCraft screenshot: All tools | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: tool catalogue. Shows PrintCraft UI, Lucide icons, the showcase PDF built from the [[fetched]] OFL fonts, and the ArtCraft mark (Storyteller trademark from docs/brand/, not covered by this licence) | -| `docs/images/printcraft-twoup.png` | PrintCraft screenshot: Two-up reading | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: layouts. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | -| `docs/images/printcraft-viewer.png` | PrintCraft screenshot: Viewer with comments | PrintCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README hero. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/brand/artcraft-logo-white.png` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | +| `docs/brand/artcraft-logo-white.svg` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | +| `docs/brand/artcraft-logo.png` | ArtCraft brand: artcraft-logo | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | +| `docs/brand/artcraft-logo.svg` | ArtCraft brand: artcraft-logo | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | +| `docs/brand/artcraft-mark-black.png` | ArtCraft brand: artcraft-mark-black | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | +| `docs/brand/artcraft-mark-black.svg` | ArtCraft brand: artcraft-mark-black | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | +| `docs/brand/artcraft-mark.png` | ArtCraft brand: artcraft-mark | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | +| `docs/brand/artcraft-mark.svg` | ArtCraft brand: artcraft-mark | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. | +| `assets/app-icon/pdfcraft.svg` | PdfCraft app icon (master vector) | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Master artwork; every other app-icon file is rendered from it (packaging/icons.sh) | +| `assets/app-icon/pdfcraft-small.svg` | PdfCraft app icon (light vector) | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | README header and assets/app-icon/README.md | +| `assets/app-icon/pdfcraft-1024.png` | PdfCraft app icon, 1024 px on Apple's icon grid | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Runtime window/Dock icon on macOS (apps/pdfcraft/src/main.rs) | +| `assets/app-icon/pdfcraft.icns` | PdfCraft app icon (macOS .icns) | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | macOS app bundle icon | +| `assets/app-icon/pdfcraft.ico` | PdfCraft app icon (Windows .ico, 16-256 px) | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Embedded in pdfcraft.exe by apps/pdfcraft/build.rs (Explorer, taskbar, Start menu, Alt-Tab) | +| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 16 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | +| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 24 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | +| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 32 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | +| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 48 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | +| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 64 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | +| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 128 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | +| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 256 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop); runtime window icon on Windows and Linux (apps/pdfcraft/src/main.rs) | +| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.pdfcraft.png` | PdfCraft app icon, 512 px | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | +| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.pdfcraft.svg` | PdfCraft app icon (Linux scalable) | PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py) | MIT OR Apache-2.0 | Contributor-original (see assets/app-icon/README.md) | Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop) | +| `docs/images/pdfcraft-bookmarks.png` | PdfCraft screenshot: Bookmarks | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: navigation. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-dark.png` | PdfCraft screenshot: Dark theme | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: themes. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-find.png` | PdfCraft screenshot: Find text | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: search. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-forms.png` | PdfCraft screenshot: Interactive form | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: forms. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-layers.png` | PdfCraft screenshot: Layers | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: layers. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-organize.png` | PdfCraft screenshot: Organize pages | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: organize. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-palette.png` | PdfCraft screenshot: Command palette | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: tools. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-properties.png` | PdfCraft screenshot: Document properties | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: metadata. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-scripts.png` | PdfCraft screenshot: Scripts of the world | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: rendering. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-split.png` | PdfCraft screenshot: Split dialog | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: split. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-tools.png` | PdfCraft screenshot: All tools | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: tool catalogue. Shows PdfCraft UI, Lucide icons, the showcase PDF built from the [[fetched]] OFL fonts, and the ArtCraft mark (Storyteller trademark from docs/brand/, not covered by this licence) | +| `docs/images/pdfcraft-twoup.png` | PdfCraft screenshot: Two-up reading | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README: layouts. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | +| `docs/images/pdfcraft-viewer.png` | PdfCraft screenshot: Viewer with comments | PdfCraft contributors (generated by `cargo xtask screenshots`) | MIT OR Apache-2.0 | this repository | README hero. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts | | `assets/icons/pin.svg` | Lucide icon "pin" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pin.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/move-right.svg` | Lucide icon "move-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/reply.svg` | Lucide icon "reply" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/reply.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | @@ -244,10 +244,10 @@ Every asset PrintCraft includes, bundles or uses to build its published material | `assets/icons/thumbs-down.svg` | Lucide icon "thumbs-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/thumbs-down.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/message-square-plus.svg` | Lucide icon "message-square-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-square-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/circle-x.svg` | Lucide icon "circle-x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -| `crates/ui-egui/tests/data/form.pdf` | One-page AcroForm test fixture (text, check box, radio group, combo box) | PrintCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects, no third-party content | Form-filling UI tests (crates/ui-egui/tests/forms.rs, commands.rs) | +| `crates/ui-egui/tests/data/form.pdf` | One-page AcroForm test fixture (text, check box, radio group, combo box) | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects, no third-party content | Form-filling UI tests (crates/ui-egui/tests/forms.rs, commands.rs) | | `assets/icons/pentagon.svg` | Lucide icon "pentagon" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pentagon.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/message-square-quote.svg` | Lucide icon "message-square-quote" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-square-quote.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -| `crates/sign/tests/data/openssl-signed.pdf` | One-page PDF signed with OpenSSL CMS (adbe.pkcs7.detached) by the RSA test key | PrintCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl cms -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): a signature PrintCraft did not make | +| `crates/sign/tests/data/openssl-signed.pdf` | One-page PDF signed with OpenSSL CMS (adbe.pkcs7.detached) by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl cms -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): a signature PdfCraft did not make | | `assets/icons/clipboard-paste.svg` | Lucide icon "clipboard-paste" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/clipboard-paste.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/fonts/DancingScript.ttf` | Dancing Script (variable) | The Dancing Script Project Authors (Pablo Impallari) | OFL-1.1 | https://github.com/google/fonts/blob/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/dancingscript/DancingScript%5Bwght%5D.ttf | Fill & Sign typed signatures and initials: the typed name's glyph outlines, drawn as filled paths (the font file itself is not embedded in PDFs) | diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 796b5b6c..b2008de4 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -2,7 +2,7 @@ # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). # # [[asset]] a file in this repository (committed or vendored); `sha256` must match the file. -# [[bundled]] a file compiled into PrintCraft by a Cargo dependency (its crate version must match Cargo.lock). +# [[bundled]] a file compiled into PdfCraft by a Cargo dependency (its crate version must match Cargo.lock). # [[fetched]] downloaded at build time by xtask (never committed), pinned by `sha256`. # [[build_input]] an optional build input outside this repository (e.g. craft-fonts via CRAFT_FONTS_DIR); # never committed or downloaded here, its own repository attributes each file. @@ -2486,7 +2486,7 @@ source = "ArtCraft Team brand assets (provided by the project owner, 2026-10-01; licence = "LicenseRef-Storyteller-Trademark" licence_file = "docs/brand/LICENSE-brand.txt" kind = "logo" -usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it." +usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it." sha256 = "8b86aeb06a12a90ba77a60e0d90329272310e244ee5c1f9b161b391280ca79d8" [[asset]] @@ -2497,7 +2497,7 @@ source = "ArtCraft Team brand assets (provided by the project owner, 2026-10-01; licence = "LicenseRef-Storyteller-Trademark" licence_file = "docs/brand/LICENSE-brand.txt" kind = "logo" -usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it." +usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it." sha256 = "a3e03dc49f9ad4225b8517d5424309c8d1c864d3996d862190d9cc797d5af427" [[asset]] @@ -2508,7 +2508,7 @@ source = "ArtCraft Team brand assets (provided by the project owner, 2026-10-01; licence = "LicenseRef-Storyteller-Trademark" licence_file = "docs/brand/LICENSE-brand.txt" kind = "logo" -usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it." +usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it." sha256 = "08fc6d9d3717e8f13417bac6f9025b50e11d1e0b32e9e000883d2fe0c4a0c67b" [[asset]] @@ -2519,7 +2519,7 @@ source = "ArtCraft Team brand assets (provided by the project owner, 2026-10-01; licence = "LicenseRef-Storyteller-Trademark" licence_file = "docs/brand/LICENSE-brand.txt" kind = "logo" -usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it." +usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it." sha256 = "3188971e1affdf4f6be4fa65f715376a7a305e6aa741ec78b0296c8f934eff08" [[asset]] @@ -2530,7 +2530,7 @@ source = "ArtCraft Team brand assets (provided by the project owner, 2026-10-01; licence = "LicenseRef-Storyteller-Trademark" licence_file = "docs/brand/LICENSE-brand.txt" kind = "logo" -usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it." +usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it." sha256 = "5954cc0306e1b9d7beb47097e2e842157113a39d9aba078b57304902687936a9" [[asset]] @@ -2541,7 +2541,7 @@ source = "ArtCraft Team brand assets (provided by the project owner, 2026-10-01; licence = "LicenseRef-Storyteller-Trademark" licence_file = "docs/brand/LICENSE-brand.txt" kind = "logo" -usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it." +usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it." sha256 = "570adcb4f7ba8ca0647f97074476bab81d3272988f1cdee52efa21bf026036a4" [[asset]] @@ -2552,7 +2552,7 @@ source = "ArtCraft Team brand assets (provided by the project owner, 2026-10-01; licence = "LicenseRef-Storyteller-Trademark" licence_file = "docs/brand/LICENSE-brand.txt" kind = "logo" -usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it." +usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it." sha256 = "9d372157a949c147318561e629b11561c28b7cbc14c84b516a3fe6e84d8fbbad" [[asset]] @@ -2563,13 +2563,13 @@ source = "ArtCraft Team brand assets (provided by the project owner, 2026-10-01; licence = "LicenseRef-Storyteller-Trademark" licence_file = "docs/brand/LICENSE-brand.txt" kind = "logo" -usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PrintCraft; forks and modified versions must remove it." +usage = "ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it." sha256 = "f87af81b6fa411da8820eed2bfdcc54c0ee9cd453b4e868c64f56c69db142d0e" [[asset]] -path = "assets/app-icon/printcraft.svg" -title = "PrintCraft app icon (master vector)" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/pdfcraft.svg" +title = "PdfCraft app icon (master vector)" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" @@ -2578,9 +2578,9 @@ usage = "Master artwork; every other app-icon file is rendered from it (packagin sha256 = "52b384a10a2a7fdd3145d9822972cfd568ef0cb7312549bd4399addd89d45a6d" [[asset]] -path = "assets/app-icon/printcraft-small.svg" -title = "PrintCraft app icon (light vector)" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/pdfcraft-small.svg" +title = "PdfCraft app icon (light vector)" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" @@ -2589,20 +2589,20 @@ usage = "README header and assets/app-icon/README.md" sha256 = "b036304c11ef5dcad36636d06ca0bf331b51e47650d25d3c916a0e1eabf544c7" [[asset]] -path = "assets/app-icon/printcraft-1024.png" -title = "PrintCraft app icon, 1024 px on Apple's icon grid" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/pdfcraft-1024.png" +title = "PdfCraft app icon, 1024 px on Apple's icon grid" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Runtime window/Dock icon on macOS (apps/printcraft/src/main.rs)" +usage = "Runtime window/Dock icon on macOS (apps/pdfcraft/src/main.rs)" sha256 = "da577d3f21fbcf5c739067787a9e683be2b064d76813b5d5fec9074a7a106538" [[asset]] -path = "assets/app-icon/printcraft.icns" -title = "PrintCraft app icon (macOS .icns)" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/pdfcraft.icns" +title = "PdfCraft app icon (macOS .icns)" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" @@ -2611,256 +2611,256 @@ usage = "macOS app bundle icon" sha256 = "2425148222873de241d267aba667a8b234ecdd0149f2ca8bb6e79443b743b1e8" [[asset]] -path = "assets/app-icon/printcraft.ico" -title = "PrintCraft app icon (Windows .ico, 16-256 px)" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/pdfcraft.ico" +title = "PdfCraft app icon (Windows .ico, 16-256 px)" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Embedded in printcraft.exe by apps/printcraft/build.rs (Explorer, taskbar, Start menu, Alt-Tab)" +usage = "Embedded in pdfcraft.exe by apps/pdfcraft/build.rs (Explorer, taskbar, Start menu, Alt-Tab)" sha256 = "56daaaae9ae5389794909570885809adf5e1d9183bef2d9669ed2f9bba3ed6f5" [[asset]] -path = "assets/app-icon/hicolor/16x16/apps/ai.storyteller.printcraft.png" -title = "PrintCraft app icon, 16 px" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/16x16/apps/ai.storyteller.pdfcraft.png" +title = "PdfCraft app icon, 16 px" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop)" sha256 = "9b77095cbe75dcba6b74a1fdcd4064812850efaf7a12edebefc9ded5eabb49da" [[asset]] -path = "assets/app-icon/hicolor/24x24/apps/ai.storyteller.printcraft.png" -title = "PrintCraft app icon, 24 px" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/24x24/apps/ai.storyteller.pdfcraft.png" +title = "PdfCraft app icon, 24 px" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop)" sha256 = "a4cd466687f6c5cb59eef3fc564bdba1c8f0d9e324754eeebab82f18823efae7" [[asset]] -path = "assets/app-icon/hicolor/32x32/apps/ai.storyteller.printcraft.png" -title = "PrintCraft app icon, 32 px" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/32x32/apps/ai.storyteller.pdfcraft.png" +title = "PdfCraft app icon, 32 px" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop)" sha256 = "e22279013a5ed5c02571e1e4e9bc4cef9aa0847fcb733a285b2916fabd16bf78" [[asset]] -path = "assets/app-icon/hicolor/48x48/apps/ai.storyteller.printcraft.png" -title = "PrintCraft app icon, 48 px" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/48x48/apps/ai.storyteller.pdfcraft.png" +title = "PdfCraft app icon, 48 px" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop)" sha256 = "55daf2d5575f6c4e4c4975c9f1c74231178f1801963a8b19f68dfa9bc50cb22f" [[asset]] -path = "assets/app-icon/hicolor/64x64/apps/ai.storyteller.printcraft.png" -title = "PrintCraft app icon, 64 px" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" +title = "PdfCraft app icon, 64 px" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop)" sha256 = "2975c200f341e247b5fa18f568fb27d390e3fa01e9b2ad37acb48d0e1cc8a124" [[asset]] -path = "assets/app-icon/hicolor/128x128/apps/ai.storyteller.printcraft.png" -title = "PrintCraft app icon, 128 px" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/128x128/apps/ai.storyteller.pdfcraft.png" +title = "PdfCraft app icon, 128 px" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop)" sha256 = "6ef927ca252b40bc9b5d8843d84137334362f8aa1acbb1bf8c15a02affd25b2b" [[asset]] -path = "assets/app-icon/hicolor/256x256/apps/ai.storyteller.printcraft.png" -title = "PrintCraft app icon, 256 px" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/256x256/apps/ai.storyteller.pdfcraft.png" +title = "PdfCraft app icon, 256 px" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop); runtime window icon on Windows and Linux (apps/printcraft/src/main.rs)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop); runtime window icon on Windows and Linux (apps/pdfcraft/src/main.rs)" sha256 = "cde7ee8c5ee38f9fc40a429fd9e6d99a035bcd321701fce331bb13bac126614d" [[asset]] -path = "assets/app-icon/hicolor/512x512/apps/ai.storyteller.printcraft.png" -title = "PrintCraft app icon, 512 px" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/512x512/apps/ai.storyteller.pdfcraft.png" +title = "PdfCraft app icon, 512 px" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop)" sha256 = "5ede733e93b96f4eb8d94299232cf2f9605fa19b0d913f95fe9828c316a2d066" [[asset]] -path = "assets/app-icon/hicolor/scalable/apps/ai.storyteller.printcraft.svg" -title = "PrintCraft app icon (Linux scalable)" -author = "PrintCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" +path = "assets/app-icon/hicolor/scalable/apps/ai.storyteller.pdfcraft.svg" +title = "PdfCraft app icon (Linux scalable)" +author = "PdfCraft contributors (drawn by the project owner in ArtCraft; vectorised with craftrules vectorize_tile.py)" source = "Contributor-original (see assets/app-icon/README.md)" licence = "MIT OR Apache-2.0" licence_file = "assets/app-icon/LICENSE.txt" kind = "icon" -usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.printcraft.desktop)" +usage = "Linux hicolor theme icon (packaging/linux/ai.storyteller.pdfcraft.desktop)" sha256 = "52b384a10a2a7fdd3145d9822972cfd568ef0cb7312549bd4399addd89d45a6d" [[asset]] -path = "docs/images/printcraft-bookmarks.png" -title = "PrintCraft screenshot: Bookmarks" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-bookmarks.png" +title = "PdfCraft screenshot: Bookmarks" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: navigation. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: navigation. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "6daf2dceda8737da0c5f9e7895e5c4cc24cc845938ffca478713c2230573240d" [[asset]] -path = "docs/images/printcraft-dark.png" -title = "PrintCraft screenshot: Dark theme" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-dark.png" +title = "PdfCraft screenshot: Dark theme" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: themes. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: themes. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "18fbd4931b2a8d01a88aef0d5d0837e3cdc12aa35f8529bedbb4a930565d47e5" [[asset]] -path = "docs/images/printcraft-find.png" -title = "PrintCraft screenshot: Find text" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-find.png" +title = "PdfCraft screenshot: Find text" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: search. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: search. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "b52771502be4c03e2dabf107c77d5183fad7f39655146d59761dda855c2d4b52" [[asset]] -path = "docs/images/printcraft-forms.png" -title = "PrintCraft screenshot: Interactive form" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-forms.png" +title = "PdfCraft screenshot: Interactive form" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: forms. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: forms. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "f8eb7705056d32e9c8e4d3b8b197c3e500faae08672e4622d6327d2799b536e7" [[asset]] -path = "docs/images/printcraft-layers.png" -title = "PrintCraft screenshot: Layers" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-layers.png" +title = "PdfCraft screenshot: Layers" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: layers. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: layers. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "20ff9e1c6ecc74d000a7514b82fe9e39f82c8ef5e7c1a52e9060c5ca349020d6" [[asset]] -path = "docs/images/printcraft-organize.png" -title = "PrintCraft screenshot: Organize pages" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-organize.png" +title = "PdfCraft screenshot: Organize pages" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: organize. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: organize. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "0a309ba73bcfccbe0106824b55fc347cc0b32293d1db68149d5eccec9c087ba1" [[asset]] -path = "docs/images/printcraft-palette.png" -title = "PrintCraft screenshot: Command palette" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-palette.png" +title = "PdfCraft screenshot: Command palette" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: tools. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: tools. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "74867793e506c03255459e838eef0b08e485c13b7ea8a6b8de3ec16870067eda" [[asset]] -path = "docs/images/printcraft-properties.png" -title = "PrintCraft screenshot: Document properties" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-properties.png" +title = "PdfCraft screenshot: Document properties" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: metadata. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: metadata. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "86f293543b299fa1665db999a68978138f8d61a4082d7c5b5025498e188da459" [[asset]] -path = "docs/images/printcraft-scripts.png" -title = "PrintCraft screenshot: Scripts of the world" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-scripts.png" +title = "PdfCraft screenshot: Scripts of the world" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: rendering. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: rendering. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "0dde8ed5a14dee7396379963adff517bd249c7dd0fe494c389b612e643584b15" [[asset]] -path = "docs/images/printcraft-split.png" -title = "PrintCraft screenshot: Split dialog" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-split.png" +title = "PdfCraft screenshot: Split dialog" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: split. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: split. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "bf31e13b563ead564af968c5fae8d1c07b91ee38867d0cf1aae9e0582efc2fe5" [[asset]] -path = "docs/images/printcraft-tools.png" -title = "PrintCraft screenshot: All tools" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-tools.png" +title = "PdfCraft screenshot: All tools" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: tool catalogue. Shows PrintCraft UI, Lucide icons, the showcase PDF built from the [[fetched]] OFL fonts, and the ArtCraft mark (Storyteller trademark from docs/brand/, not covered by this licence)" +usage = "README: tool catalogue. Shows PdfCraft UI, Lucide icons, the showcase PDF built from the [[fetched]] OFL fonts, and the ArtCraft mark (Storyteller trademark from docs/brand/, not covered by this licence)" sha256 = "61731a921902e285d40826d401f1ef25624dabb8c8ac1f8f0c09c1292a17ee1a" [[asset]] -path = "docs/images/printcraft-twoup.png" -title = "PrintCraft screenshot: Two-up reading" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-twoup.png" +title = "PdfCraft screenshot: Two-up reading" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README: layouts. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README: layouts. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "a72ed1b84c827e3aa1dd3282d857b4742f02d8ff23fc39b62115c840ebad14b4" [[asset]] -path = "docs/images/printcraft-viewer.png" -title = "PrintCraft screenshot: Viewer with comments" -author = "PrintCraft contributors (generated by `cargo xtask screenshots`)" +path = "docs/images/pdfcraft-viewer.png" +title = "PdfCraft screenshot: Viewer with comments" +author = "PdfCraft contributors (generated by `cargo xtask screenshots`)" source = "this repository" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "image" -usage = "README hero. Shows only PrintCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" +usage = "README hero. Shows only PdfCraft UI, Lucide icons and the showcase PDF built from the [[fetched]] OFL fonts" sha256 = "ecd261028d8837f7f7c013dd796d81ca7e89643bcfcbad3ab9fbfbe87ded04e3" [[asset]] @@ -2943,7 +2943,7 @@ sha256 = "ef1fec17d88794ba6c34c5cf63bdadbd234b3d4dffbcc3d55468b47e3f398472" [[asset]] path = "crates/ui-egui/tests/data/form.pdf" title = "One-page AcroForm test fixture (text, check box, radio group, combo box)" -author = "PrintCraft contributors" +author = "PdfCraft contributors" source = "Contributor-original: hand-written PDF objects, no third-party content" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" @@ -2976,12 +2976,12 @@ sha256 = "69a76b4cd352dbdea61c63ed1e8d1e02083ee1773b3ced2a1c8cdf8c90fb2055" [[asset]] path = "crates/sign/tests/data/openssl-signed.pdf" title = "One-page PDF signed with OpenSSL CMS (adbe.pkcs7.detached) by the RSA test key" -author = "PrintCraft contributors" +author = "PdfCraft contributors" source = "Contributor-original: hand-written PDF objects; signature made with `openssl cms -sign` over its byte ranges (see crates/sign/tests/data/README.md)" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "document" -usage = "Signature validation tests (crates/sign/tests/pdf.rs): a signature PrintCraft did not make" +usage = "Signature validation tests (crates/sign/tests/pdf.rs): a signature PdfCraft did not make" sha256 = "8b91c3b4e39c4a31717a0dd0685bd7d0468671df32aee5b9818d44dc72c2bc3c" [[asset]] diff --git a/CLAUDE.md b/CLAUDE.md index 60f00c7f..ccad9e7b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ -# PrintCraft — instructions for agents +# PdfCraft — instructions for agents -PrintCraft is a clean-room, open-source, Rust-native PDF application targeting Adobe Acrobat Pro parity. It runs natively on macOS, Windows, Linux and FreeBSD, and on the web via WASM. It is the sibling of `../photocraft` (a Photoshop-class editor) and follows the same conventions. +PdfCraft is a clean-room, open-source, Rust-native PDF application targeting Adobe Acrobat Pro parity. It runs natively on macOS, Windows, Linux and FreeBSD, and on the web via WASM. It is the sibling of `../photocraft` (a Photoshop-class editor) and follows the same conventions. ## Start every session here 1. Read `plan/STATUS.md`: the current milestone, the next unchecked task and any blockers. `ROADMAP.md` holds the milestone estimates and progress; update its table and log at the end of every session. @@ -13,7 +13,7 @@ PrintCraft is a clean-room, open-source, Rust-native PDF application targeting A ## Non-negotiables - **Assets: read `AGENTS.md` §1 before adding or showing any icon, image, font or document.** No assets from Adobe products, ever. Only openly licensed or contributor-original assets are allowed, each with an entry in `ATTRIBUTION.toml`. `cargo xtask assets` enforces this. `AGENTS.md` overrides this file. -- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts).** Never commit font files here (`AGENTS.md` §1.4; team members: [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md), internal). It is the optional build input `CRAFT_FONTS_DIR`: `git clone https://github.com/storytold/craft-fonts ../craft-fonts && CRAFT_FONTS_DIR=../craft-fonts cargo test --workspace` embeds the Japanese fonts (UI fallback, Japanese text in edited PDFs) and runs their tests, which otherwise skip. Code using `printcraft_fonts::CRAFT_FONTS` must work when it is empty. +- **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts).** Never commit font files here (`AGENTS.md` §1.4; team members: [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md), internal). It is the optional build input `CRAFT_FONTS_DIR`: `git clone https://github.com/storytold/craft-fonts ../craft-fonts && CRAFT_FONTS_DIR=../craft-fonts cargo test --workspace` embeds the Japanese fonts (UI fallback, Japanese text in edited PDFs) and runs their tests, which otherwise skip. Code using `pdfcraft_fonts::CRAFT_FONTS` must work when it is empty. - **Clean-room.** - Never read, disassemble or copy anything inside the Acrobat bundle (names and listings only). **Never open `Contents/Resources/JavaScripts/`.** - Behaviour comes from public docs, specs (ISO 32000-2, the Arlington model) and black-box observation (`plan/acrobat/`). @@ -35,13 +35,13 @@ PrintCraft is a clean-room, open-source, Rust-native PDF application targeting A - **Commits:** one task id per commit (e.g. `M1.4: xref stream reader`). Commit only green states. End messages with the attribution line required by the environment. ## Running and looking at the app -- `cargo run -p printcraft -- ` opens the desktop app. -- `cargo run -p printcraft-cli -- run --script steps.json --root DIR` drives the engine headlessly through the automation tools (`printcraft-cli tools` lists them). Use it, together with `page_render`, to check engine changes. `printcraft-cli mcp` is the opt-in MCP server (AGENTS.md §3). +- `cargo run -p pdfcraft -- ` opens the desktop app. +- `cargo run -p pdfcraft-cli -- run --script steps.json --root DIR` drives the engine headlessly through the automation tools (`pdfcraft-cli tools` lists them). Use it, together with `page_render`, to check engine changes. `pdfcraft-cli mcp` is the opt-in MCP server (AGENTS.md §3). - `cargo xtask fuzz --time 300` mutation-fuzzes open/render/edit/save in child processes. Findings land in `fuzz-out/findings/` (git-ignored; never commit corpus-derived files). Turn every real finding into a small synthetic regression test before fixing it. - `cargo xtask parity [--partial]` reports Acrobat-parity progress from `parity/acrobat-features.toml`. Update the entry when a feature ships. -- `cargo xtask demo-pdf` builds `dist/demo/printcraft-showcase.pdf` (needs Chrome) for visual checks. -- For UI work, **look at the result**. Either launch `printcraft --control FILE doc.pdf` and use `printcraft-cli ui --control FILE screenshot --out x.png` (plus `inspect`, `click`, `key`, `type`, `command`, `set`), or take a headless shot with `cargo run -p printcraft-ui-egui --example shot`. Compare against `plan/acrobat/02-ui-ux.md`. Control-channel tests use kittest (`crates/ui-egui/tests/control.rs`). +- `cargo xtask demo-pdf` builds `dist/demo/pdfcraft-showcase.pdf` (needs Chrome) for visual checks. +- For UI work, **look at the result**. Either launch `pdfcraft --control FILE doc.pdf` and use `pdfcraft-cli ui --control FILE screenshot --out x.png` (plus `inspect`, `click`, `key`, `type`, `command`, `set`), or take a headless shot with `cargo run -p pdfcraft-ui-egui --example shot`. Compare against `plan/acrobat/02-ui-ux.md`. Control-channel tests use kittest (`crates/ui-egui/tests/control.rs`). - Parallel agents: use a separate `CARGO_TARGET_DIR` per agent and separate git worktrees. ## Current bootstrap debt (tracked in STATUS.md) -- `printcraft-render` renders through the `hayro` crate directly and inspects documents through `lopdf`. Both get replaced by `cos` / `model` / the DisplayList device (M1–M2, ADR-0004). +- `pdfcraft-render` renders through the `hayro` crate directly and inspects documents through `lopdf`. Both get replaced by `cos` / `model` / the DisplayList device (M1–M2, ADR-0004). diff --git a/Cargo.lock b/Cargo.lock index a6efac42..0b4ab6f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3445,6 +3445,365 @@ dependencies = [ "hmac", ] +[[package]] +name = "pdfcraft" +version = "0.2.1" +dependencies = [ + "eframe", + "egui", + "fmv-macos-events", + "pdfcraft-ui-egui", + "rustls-native-certs", + "serde_json", + "ureq", + "winresource", +] + +[[package]] +name = "pdfcraft-a11y" +version = "0.2.1" +dependencies = [ + "pdfcraft-content", + "pdfcraft-cos", + "pdfcraft-model", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-annot" +version = "0.2.1" +dependencies = [ + "hayro-syntax", + "pdfcraft-cos", + "pdfcraft-fonts", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-automation" +version = "0.2.1" +dependencies = [ + "base64 0.22.1", + "image", + "pdfcraft-engine", + "pdfcraft-organize", + "pdfcraft-render", + "png 0.18.1", + "serde_json", +] + +[[package]] +name = "pdfcraft-cli" +version = "0.2.1" +dependencies = [ + "base64 0.22.1", + "pdfcraft-automation", + "pdfcraft-cos", + "pdfcraft-engine", + "pdfcraft-render", + "serde_json", +] + +[[package]] +name = "pdfcraft-compare" +version = "0.2.1" + +[[package]] +name = "pdfcraft-content" +version = "0.2.1" +dependencies = [ + "pdfcraft-cos", +] + +[[package]] +name = "pdfcraft-cos" +version = "0.2.1" +dependencies = [ + "hayro-syntax", + "pdfcraft-crypt", + "pdfcraft-filters", + "proptest", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-create" +version = "0.2.1" +dependencies = [ + "hayro-syntax", + "image", + "pdfcraft-cos", + "pdfcraft-fonts", + "pdfcraft-model", + "png 0.18.1", + "thiserror 2.0.21", + "tiff", +] + +[[package]] +name = "pdfcraft-crypt" +version = "0.2.1" +dependencies = [ + "aes", + "md-5", + "proptest", + "sha2", + "stringprep", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-edit" +version = "0.2.1" +dependencies = [ + "hayro-syntax", + "pdfcraft-annot", + "pdfcraft-content", + "pdfcraft-cos", + "pdfcraft-fonts", + "pdfcraft-model", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-engine" +version = "0.2.1" +dependencies = [ + "chrono", + "image", + "pdfcraft-a11y", + "pdfcraft-annot", + "pdfcraft-compare", + "pdfcraft-cos", + "pdfcraft-create", + "pdfcraft-edit", + "pdfcraft-export", + "pdfcraft-fonts", + "pdfcraft-forms", + "pdfcraft-js", + "pdfcraft-model", + "pdfcraft-ocr", + "pdfcraft-optimize", + "pdfcraft-organize", + "pdfcraft-preflight", + "pdfcraft-print", + "pdfcraft-redact", + "pdfcraft-render", + "pdfcraft-sign", + "pdfcraft-xfdf", + "png 0.18.1", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-export" +version = "0.2.1" +dependencies = [ + "flate2", +] + +[[package]] +name = "pdfcraft-filters" +version = "0.2.1" +dependencies = [ + "flate2", + "proptest", + "thiserror 2.0.21", + "weezl 0.2.1", +] + +[[package]] +name = "pdfcraft-fonts" +version = "0.2.1" +dependencies = [ + "pdfcraft-cos", + "skrifa 0.44.0", +] + +[[package]] +name = "pdfcraft-forms" +version = "0.2.1" +dependencies = [ + "hayro-syntax", + "pdfcraft-content", + "pdfcraft-cos", + "pdfcraft-fonts", + "pdfcraft-model", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-geom" +version = "0.2.1" + +[[package]] +name = "pdfcraft-js" +version = "0.2.1" +dependencies = [ + "boa_engine", +] + +[[package]] +name = "pdfcraft-model" +version = "0.2.1" +dependencies = [ + "pdfcraft-cos", +] + +[[package]] +name = "pdfcraft-ocr" +version = "0.2.1" +dependencies = [ + "ocrs", + "pdfcraft-fonts", + "rten", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-optimize" +version = "0.2.1" +dependencies = [ + "image", + "pdfcraft-annot", + "pdfcraft-content", + "pdfcraft-cos", + "pdfcraft-model", + "pdfcraft-render", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-organize" +version = "0.2.1" +dependencies = [ + "hayro-syntax", + "pdfcraft-cos", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-preflight" +version = "0.2.1" +dependencies = [ + "pdfcraft-cos", + "pdfcraft-model", +] + +[[package]] +name = "pdfcraft-print" +version = "0.2.1" +dependencies = [ + "pdfcraft-content", + "pdfcraft-cos", + "pdfcraft-model", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-redact" +version = "0.2.1" +dependencies = [ + "pdfcraft-annot", + "pdfcraft-content", + "pdfcraft-cos", + "pdfcraft-edit", + "pdfcraft-fonts", + "pdfcraft-forms", + "pdfcraft-model", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-render" +version = "0.2.1" +dependencies = [ + "getrandom 0.4.3", + "hayro", + "hayro-syntax", + "kurbo 0.13.1", + "log", + "lopdf", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-sign" +version = "0.2.1" +dependencies = [ + "aes", + "aws-lc-rs", + "cbc", + "des", + "getrandom 0.4.3", + "hmac", + "p256", + "p384", + "pbkdf2", + "pdfcraft-annot", + "pdfcraft-cos", + "pdfcraft-fonts", + "rc2", + "rsa", + "security-framework", + "sha1", + "sha2", + "thiserror 2.0.21", +] + +[[package]] +name = "pdfcraft-ui-egui" +version = "0.2.1" +dependencies = [ + "arboard", + "base64 0.22.1", + "eframe", + "egui", + "egui_extras", + "egui_kittest", + "getrandom 0.4.3", + "image", + "js-sys", + "log", + "pdfcraft-cos", + "pdfcraft-engine", + "pdfcraft-fonts", + "pdfcraft-model", + "pdfcraft-render", + "png 0.18.1", + "rfd", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "pdfcraft-web" +version = "0.2.1" +dependencies = [ + "eframe", + "egui", + "js-sys", + "pdfcraft-ui-egui", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "pdfcraft-xfdf" +version = "0.2.1" +dependencies = [ + "pdfcraft-annot", + "pdfcraft-cos", + "pdfcraft-forms", + "pdfcraft-model", + "roxmltree", + "thiserror 2.0.21", +] + [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -3776,365 +4135,6 @@ dependencies = [ "wnaf", ] -[[package]] -name = "printcraft" -version = "0.2.1" -dependencies = [ - "eframe", - "egui", - "fmv-macos-events", - "printcraft-ui-egui", - "rustls-native-certs", - "serde_json", - "ureq", - "winresource", -] - -[[package]] -name = "printcraft-a11y" -version = "0.2.1" -dependencies = [ - "printcraft-content", - "printcraft-cos", - "printcraft-model", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-annot" -version = "0.2.1" -dependencies = [ - "hayro-syntax", - "printcraft-cos", - "printcraft-fonts", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-automation" -version = "0.2.1" -dependencies = [ - "base64 0.22.1", - "image", - "png 0.18.1", - "printcraft-engine", - "printcraft-organize", - "printcraft-render", - "serde_json", -] - -[[package]] -name = "printcraft-cli" -version = "0.2.1" -dependencies = [ - "base64 0.22.1", - "printcraft-automation", - "printcraft-cos", - "printcraft-engine", - "printcraft-render", - "serde_json", -] - -[[package]] -name = "printcraft-compare" -version = "0.2.1" - -[[package]] -name = "printcraft-content" -version = "0.2.1" -dependencies = [ - "printcraft-cos", -] - -[[package]] -name = "printcraft-cos" -version = "0.2.1" -dependencies = [ - "hayro-syntax", - "printcraft-crypt", - "printcraft-filters", - "proptest", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-create" -version = "0.2.1" -dependencies = [ - "hayro-syntax", - "image", - "png 0.18.1", - "printcraft-cos", - "printcraft-fonts", - "printcraft-model", - "thiserror 2.0.21", - "tiff", -] - -[[package]] -name = "printcraft-crypt" -version = "0.2.1" -dependencies = [ - "aes", - "md-5", - "proptest", - "sha2", - "stringprep", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-edit" -version = "0.2.1" -dependencies = [ - "hayro-syntax", - "printcraft-annot", - "printcraft-content", - "printcraft-cos", - "printcraft-fonts", - "printcraft-model", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-engine" -version = "0.2.1" -dependencies = [ - "chrono", - "image", - "png 0.18.1", - "printcraft-a11y", - "printcraft-annot", - "printcraft-compare", - "printcraft-cos", - "printcraft-create", - "printcraft-edit", - "printcraft-export", - "printcraft-fonts", - "printcraft-forms", - "printcraft-js", - "printcraft-model", - "printcraft-ocr", - "printcraft-optimize", - "printcraft-organize", - "printcraft-preflight", - "printcraft-print", - "printcraft-redact", - "printcraft-render", - "printcraft-sign", - "printcraft-xfdf", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-export" -version = "0.2.1" -dependencies = [ - "flate2", -] - -[[package]] -name = "printcraft-filters" -version = "0.2.1" -dependencies = [ - "flate2", - "proptest", - "thiserror 2.0.21", - "weezl 0.2.1", -] - -[[package]] -name = "printcraft-fonts" -version = "0.2.1" -dependencies = [ - "printcraft-cos", - "skrifa 0.44.0", -] - -[[package]] -name = "printcraft-forms" -version = "0.2.1" -dependencies = [ - "hayro-syntax", - "printcraft-content", - "printcraft-cos", - "printcraft-fonts", - "printcraft-model", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-geom" -version = "0.2.1" - -[[package]] -name = "printcraft-js" -version = "0.2.1" -dependencies = [ - "boa_engine", -] - -[[package]] -name = "printcraft-model" -version = "0.2.1" -dependencies = [ - "printcraft-cos", -] - -[[package]] -name = "printcraft-ocr" -version = "0.2.1" -dependencies = [ - "ocrs", - "printcraft-fonts", - "rten", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-optimize" -version = "0.2.1" -dependencies = [ - "image", - "printcraft-annot", - "printcraft-content", - "printcraft-cos", - "printcraft-model", - "printcraft-render", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-organize" -version = "0.2.1" -dependencies = [ - "hayro-syntax", - "printcraft-cos", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-preflight" -version = "0.2.1" -dependencies = [ - "printcraft-cos", - "printcraft-model", -] - -[[package]] -name = "printcraft-print" -version = "0.2.1" -dependencies = [ - "printcraft-content", - "printcraft-cos", - "printcraft-model", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-redact" -version = "0.2.1" -dependencies = [ - "printcraft-annot", - "printcraft-content", - "printcraft-cos", - "printcraft-edit", - "printcraft-fonts", - "printcraft-forms", - "printcraft-model", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-render" -version = "0.2.1" -dependencies = [ - "getrandom 0.4.3", - "hayro", - "hayro-syntax", - "kurbo 0.13.1", - "log", - "lopdf", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-sign" -version = "0.2.1" -dependencies = [ - "aes", - "aws-lc-rs", - "cbc", - "des", - "getrandom 0.4.3", - "hmac", - "p256", - "p384", - "pbkdf2", - "printcraft-annot", - "printcraft-cos", - "printcraft-fonts", - "rc2", - "rsa", - "security-framework", - "sha1", - "sha2", - "thiserror 2.0.21", -] - -[[package]] -name = "printcraft-ui-egui" -version = "0.2.1" -dependencies = [ - "arboard", - "base64 0.22.1", - "eframe", - "egui", - "egui_extras", - "egui_kittest", - "getrandom 0.4.3", - "image", - "js-sys", - "log", - "png 0.18.1", - "printcraft-cos", - "printcraft-engine", - "printcraft-fonts", - "printcraft-model", - "printcraft-render", - "rfd", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "printcraft-web" -version = "0.2.1" -dependencies = [ - "eframe", - "egui", - "js-sys", - "printcraft-ui-egui", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "printcraft-xfdf" -version = "0.2.1" -dependencies = [ - "printcraft-annot", - "printcraft-cos", - "printcraft-forms", - "printcraft-model", - "roxmltree", - "thiserror 2.0.21", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" diff --git a/Cargo.toml b/Cargo.toml index c94309e2..76be6e26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,44 +2,44 @@ resolver = "3" members = ["crates/*", "apps/*", "xtask"] exclude = ["vendor"] -default-members = ["crates/*", "apps/printcraft", "apps/printcraft-cli", "xtask"] +default-members = ["crates/*", "apps/pdfcraft", "apps/pdfcraft-cli", "xtask"] [workspace.package] version = "0.2.1" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.90" -repository = "https://github.com/storytold/printcraft" +repository = "https://github.com/storytold/pdfcraft" [workspace.dependencies] # internal -printcraft-geom = { path = "crates/geom", version = "0.2.0" } -printcraft-filters = { path = "crates/filters", version = "0.2.0" } -printcraft-cos = { path = "crates/cos", version = "0.2.0" } -printcraft-crypt = { path = "crates/crypt", version = "0.2.0" } -printcraft-organize = { path = "crates/organize", version = "0.2.0" } -printcraft-annot = { path = "crates/annot", version = "0.2.0" } -printcraft-fonts = { path = "crates/fonts", version = "0.2.0" } -printcraft-model = { path = "crates/model", version = "0.2.0" } -printcraft-content = { path = "crates/content", version = "0.2.0" } -printcraft-redact = { path = "crates/redact", version = "0.2.0" } -printcraft-print = { path = "crates/print", version = "0.2.0" } -printcraft-xfdf = { path = "crates/xfdf", version = "0.2.0" } -printcraft-sign = { path = "crates/sign", version = "0.2.0" } -printcraft-optimize = { path = "crates/optimize", version = "0.2.0" } -printcraft-a11y = { path = "crates/a11y", version = "0.2.0" } -printcraft-ocr = { path = "crates/ocr", version = "0.2.0" } -printcraft-js = { path = "crates/js", version = "0.2.0" } -printcraft-compare = { path = "crates/compare", version = "0.2.0" } -printcraft-preflight = { path = "crates/preflight", version = "0.2.0" } -printcraft-export = { path = "crates/export", version = "0.2.0" } -printcraft-edit = { path = "crates/edit", version = "0.2.0" } -printcraft-create = { path = "crates/create", version = "0.2.0" } -printcraft-forms = { path = "crates/forms", version = "0.2.0" } -printcraft-render = { path = "crates/render", version = "0.2.0" } -printcraft-engine = { path = "crates/engine", version = "0.2.0" } -printcraft-ui-egui = { path = "crates/ui-egui", version = "0.2.0" } -printcraft-automation = { path = "crates/automation", version = "0.2.0", default-features = false } +pdfcraft-geom = { path = "crates/geom", version = "0.2.0" } +pdfcraft-filters = { path = "crates/filters", version = "0.2.0" } +pdfcraft-cos = { path = "crates/cos", version = "0.2.0" } +pdfcraft-crypt = { path = "crates/crypt", version = "0.2.0" } +pdfcraft-organize = { path = "crates/organize", version = "0.2.0" } +pdfcraft-annot = { path = "crates/annot", version = "0.2.0" } +pdfcraft-fonts = { path = "crates/fonts", version = "0.2.0" } +pdfcraft-model = { path = "crates/model", version = "0.2.0" } +pdfcraft-content = { path = "crates/content", version = "0.2.0" } +pdfcraft-redact = { path = "crates/redact", version = "0.2.0" } +pdfcraft-print = { path = "crates/print", version = "0.2.0" } +pdfcraft-xfdf = { path = "crates/xfdf", version = "0.2.0" } +pdfcraft-sign = { path = "crates/sign", version = "0.2.0" } +pdfcraft-optimize = { path = "crates/optimize", version = "0.2.0" } +pdfcraft-a11y = { path = "crates/a11y", version = "0.2.0" } +pdfcraft-ocr = { path = "crates/ocr", version = "0.2.0" } +pdfcraft-js = { path = "crates/js", version = "0.2.0" } +pdfcraft-compare = { path = "crates/compare", version = "0.2.0" } +pdfcraft-preflight = { path = "crates/preflight", version = "0.2.0" } +pdfcraft-export = { path = "crates/export", version = "0.2.0" } +pdfcraft-edit = { path = "crates/edit", version = "0.2.0" } +pdfcraft-create = { path = "crates/create", version = "0.2.0" } +pdfcraft-forms = { path = "crates/forms", version = "0.2.0" } +pdfcraft-render = { path = "crates/render", version = "0.2.0" } +pdfcraft-engine = { path = "crates/engine", version = "0.2.0" } +pdfcraft-ui-egui = { path = "crates/ui-egui", version = "0.2.0" } +pdfcraft-automation = { path = "crates/automation", version = "0.2.0", default-features = false } # pdf hayro = "0.7.1" diff --git a/LICENSE-MIT b/LICENSE-MIT index 8ba1dfe2..d2c0cd3f 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 ArtCraft Team and the PrintCraft contributors +Copyright (c) 2026 ArtCraft Team and the PdfCraft contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NOTICE b/NOTICE index efe5186a..62e047ec 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,5 @@ -PrintCraft -Copyright (c) 2026 ArtCraft Team and the PrintCraft contributors. +PdfCraft +Copyright (c) 2026 ArtCraft Team and the PdfCraft contributors. Licensed under the MIT License or the Apache License, Version 2.0, at your option (LICENSE-MIT, LICENSE-APACHE). diff --git a/README.md b/README.md index 55fdeb4f..8b04e28c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

-

PrintCraft

+

PdfCraft

The PDF workbench; an open-source, clean-room reimplementation of Adobe Acrobat, rebuilt in pure Rust.
@@ -28,7 +28,7 @@

- PrintCraft on getartcraft.com · + PdfCraft on getartcraft.com · ArtCraft · All Crafting Apps

@@ -36,9 +36,9 @@

- PrintCraft with the PrintCraft Showcase cover page open, the All tools panel on the left and 20 threaded comments on the right + PdfCraft with the PdfCraft Showcase cover page open, the All tools panel on the left and 20 threaded comments on the right
- The PrintCraft Showcase, a 13-page specimen PDF, open with the All tools panel and threaded comments. + The PdfCraft Showcase, a 13-page specimen PDF, open with the All tools panel and threaded comments.

> [!NOTE] @@ -65,13 +65,13 @@ ## Community -PrintCraft is part of [ArtCraft](https://getartcraft.com). Come say hello, get help and follow development: +PdfCraft is part of [ArtCraft](https://getartcraft.com). Come say hello, get help and follow development: - **Discord: [discord.gg/artcraft](https://discord.gg/artcraft)**. This is the fastest way to get help and share feedback. The app has a Discord button in its title bar. -- **Web page:** [getartcraft.com/apps/printcraft](https://getartcraft.com/apps/printcraft) -- **Source:** [github.com/storytold/printcraft](https://github.com/storytold/printcraft) +- **Web page:** [getartcraft.com/apps/pdfcraft](https://getartcraft.com/apps/pdfcraft) +- **Source:** [github.com/storytold/pdfcraft](https://github.com/storytold/pdfcraft) -The ArtCraft name and logos in `docs/brand/` are trademarks of the ArtCraft Team and are not open source. They may be used only unmodified, and only as part of PrintCraft (see `docs/brand/LICENSE-brand.txt`). Forks and modified versions must remove them. +The ArtCraft name and logos in `docs/brand/` are trademarks of the ArtCraft Team and are not open source. They may be used only unmodified, and only as part of PdfCraft (see `docs/brand/LICENSE-brand.txt`). Forks and modified versions must remove them. ## Highlights @@ -102,10 +102,10 @@ No account, no telemetry, no cloud. It works offline and opens instantly. The en ## Read anything, beautifully -PrintCraft renders PDFs with care for the details that make a page feel right: kerning and ligatures, right-to-left and complex scripts, vertical CJK, colour emoji, shadings, blend modes, soft masks and optional content. +PdfCraft renders PDFs with care for the details that make a page feel right: kerning and ligatures, right-to-left and complex scripts, vertical CJK, colour emoji, shadings, blend modes, soft masks and optional content.

- The Scripts of the World page: Arabic, Hebrew, Devanagari, Thai, Greek, Cyrillic, Chinese, Korean, IPA, Armenian, Georgian and Tamil samples, with vertical Japanese in the right margin + The Scripts of the World page: Arabic, Hebrew, Devanagari, Thai, Greek, Cyrillic, Chinese, Korean, IPA, Armenian, Georgian and Tamil samples, with vertical Japanese in the right margin
Twelve writing systems on one page, plus vertical Japanese, at 125%.

@@ -117,8 +117,8 @@ PrintCraft renders PDFs with care for the details that make a page feel right: k - - + + @@ -136,8 +136,8 @@ Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) k
Two facing pages in Read mode with the dark theme: the Foreword and the Setting Text chapter with its drop cap and pull quoteThe dark theme showing the Code and Images chapter, a syntax-coloured listing and a fractal image, with the comments panel openTwo facing pages in Read mode with the dark theme: the Foreword and the Setting Text chapter with its drop cap and pull quoteThe dark theme showing the Code and Images chapter, a syntax-coloured listing and a fractal image, with the comments panel open
Two-up Read mode, ready for long reading
- - + + @@ -155,7 +155,7 @@ Open **Organize pages** to see every page at once: - **Undo anything:** ⌘Z, then save.

- The Organize pages grid with the showcase's pages as thumbnails, three of them selected, and the page toolbar above + The Organize pages grid with the showcase's pages as thumbnails, three of them selected, and the page toolbar above
Organize pages with three pages selected and the page tools in the toolbar above.

@@ -171,10 +171,10 @@ Open **Organize pages** to see every page at once: - *Atomic:* the file is written to a temporary copy, then swapped in. - *Verified:* independently checked with qpdf. -Unsaved documents carry a dot on their tab, and closing or quitting asks before anything is lost. Changes are autosaved every minute. If PrintCraft ever quits unexpectedly, it offers to recover your work the next time it opens. Encrypted documents stay encrypted on disk. +Unsaved documents carry a dot on their tab, and closing or quitting asks before anything is lost. Changes are autosaved every minute. If PdfCraft ever quits unexpectedly, it offers to recover your work the next time it opens. Encrypted documents stay encrypted on disk. - +
The find bar showing match 10 of 16 for the word 'type', highlighted in the Expressive Type chapter headingThe Bookmarks panel showing the nested outline of the showcase, with page labels such as Cover, i and ii, next to the Scripts of the World pageThe find bar showing match 10 of 16 for the word 'type', highlighted in the Expressive Type chapter headingThe Bookmarks panel showing the nested outline of the showcase, with page labels such as Cover, i and ii, next to the Scripts of the World page
Find as you type: match 10 of 16The Split document dialog over the organize view, set to one page per file and reporting that it creates 13 files from 13 pages
Split document: one page per file makes 13 files.
The Split document dialog over the organize view, set to one page per file and reporting that it creates 13 files from 13 pages
Split document: one page per file makes 13 files.
@@ -193,25 +193,25 @@ Nothing quietly disappears along the way: Every page of a combined document renders pixel-identical to its source. ```sh -printcraft-cli combine report.pdf appendix.pdf --out combined.pdf -printcraft-cli extract report.pdf --pages 1,3,5 --out highlights.pdf -printcraft-cli split report.pdf --every 10 --out-dir parts/ +pdfcraft-cli combine report.pdf appendix.pdf --out combined.pdf +pdfcraft-cli extract report.pdf --pages 1,3,5 --out highlights.pdf +pdfcraft-cli split report.pdf --every 10 --out-dir parts/ ``` --- ## Open protected documents and respect their rules -PrintCraft implements the PDF standard security handler completely: +PdfCraft implements the PDF standard security handler completely: - every revision, from 40-bit RC4 to AES-256; - user and owner passwords, including Unicode passwords normalised with SASLprep; - crypt filters and attachment-only encryption. -Documents restricted by their author show a clear notice, and PrintCraft honours their permissions. Enter the owner password and the restrictions lift. Edits to encrypted documents are saved encrypted, under the same keys. +Documents restricted by their author show a clear notice, and PdfCraft honours their permissions. Enter the owner password and the restrictions lift. Edits to encrypted documents are saved encrypted, under the same keys. - +
The Document Properties dialog on its Description tab, with editable title, author, subject and keywords, and tabs for Security, Fonts and Advanced
Document Properties, Description tab
The Document Properties dialog on its Description tab, with editable title, author, subject and keywords, and tabs for Security, Fonts and Advanced
Document Properties, Description tab
**Document Properties** shows: @@ -228,8 +228,8 @@ Documents restricted by their author show a clear notice, and PrintCraft honours - - + + @@ -243,8 +243,8 @@ Press ⌘K to search every tool and command, or browse the **All tool
The Interactive Form page with highlighted text fields, checkboxes, radio buttons, a list and a signature field, and the Fields panel listing all 13 fields and their valuesThe Review and Markup page with highlights, shapes, ink and an APPROVED stamp under a DRAFT watermark, and the Layers panel with Draft watermark and Print-only notesThe Interactive Form page with highlighted text fields, checkboxes, radio buttons, a list and a signature field, and the Fields panel listing all 13 fields and their valuesThe Review and Markup page with highlights, shapes, ink and an APPROVED stamp under a DRAFT watermark, and the Layers panel with Draft watermark and Print-only notes
Forms: every field with its current value, field highlighting, and checkboxes, radio buttons, lists and signatures drawn the way their author designed them.
- - + + @@ -259,12 +259,12 @@ Press ⌘K to search every tool and command, or browse the **All tool - **Native on macOS, Windows, Linux and FreeBSD**, and **in the browser** through WebAssembly, from the same Rust codebase. Windows builds come for x64, x86 and ARM64 (Windows on ARM, no emulation); every ARM64 change is tested on ARM64 hardware in CI. - **Private by design.** Documents never leave your machine. There's no account, no telemetry and no cloud processing. - **Engine first.** Parsing, rendering and editing live in reusable library crates. The interface is one swappable layer on top. -- **Scriptable.** The `printcraft-cli` tool (see [Built for agents, too](#built-for-agents-too)) covers inspecting, rendering, extracting text, editing, combining, extracting pages and splitting. Robustness sweeps run on the same engine as the app. +- **Scriptable.** The `pdfcraft-cli` tool (see [Built for agents, too](#built-for-agents-too)) covers inspecting, rendering, extracting text, editing, combining, extracting pages and splitting. Robustness sweeps run on the same engine as the app. ```sh -printcraft-cli info form.pdf # structure as JSON -printcraft-cli text paper.pdf --page 3 # reading-order text -printcraft-cli edit in.pdf --rotate 1,2:90 --delete 5 --title "Q3" --out out.pdf +pdfcraft-cli info form.pdf # structure as JSON +pdfcraft-cli text paper.pdf --page 3 # reading-order text +pdfcraft-cli edit in.pdf --rotate 1,2:90 --delete 5 --title "Q3" --out out.pdf ``` --- @@ -273,59 +273,59 @@ printcraft-cli edit in.pdf --rotate 1,2:90 --delete 5 --title "Q3" --out out.pd Every engine feature is reachable without the GUI, through one table of JSON-Schema-described tools: open, inspect, render pages to PNG, extract and find text, rotate, delete, move and insert pages, edit bookmarks and page labels, add, reply to, restyle and delete comments (highlight a phrase just by naming it), list and fill in form fields, protect with passwords, set metadata, undo and redo, save, combine, extract and split. Three front doors share it: -- **`printcraft-cli run`**, for one-off calls and JSON scripts: +- **`pdfcraft-cli run`**, for one-off calls and JSON scripts: ```sh - printcraft-cli tools # every tool and its JSON Schema - printcraft-cli run text_find doc=1 query=invoice # key=value; values parse as JSON - printcraft-cli run --script review.json # e.g. comment_add {"type": "highlight", "find": "total due"} - printcraft-cli run --script steps.json --root ./work # several steps in one session + pdfcraft-cli tools # every tool and its JSON Schema + pdfcraft-cli run text_find doc=1 query=invoice # key=value; values parse as JSON + pdfcraft-cli run --script review.json # e.g. comment_add {"type": "highlight", "find": "total due"} + pdfcraft-cli run --script steps.json --root ./work # several steps in one session ``` -- **An MCP server**, for AI agents such as Claude. **It is opt-in:** PrintCraft never starts it on its own, and it opens no network port. It runs only while an agent launches `printcraft-cli mcp`, talks over stdin/stdout, and stops when the agent disconnects. To enable it, add it to your agent's MCP configuration: +- **An MCP server**, for AI agents such as Claude. **It is opt-in:** PdfCraft never starts it on its own, and it opens no network port. It runs only while an agent launches `pdfcraft-cli mcp`, talks over stdin/stdout, and stops when the agent disconnects. To enable it, add it to your agent's MCP configuration: ```json - { "mcpServers": { "printcraft": { "command": "printcraft-cli", "args": ["mcp", "--root", "/path/to/your/pdfs"] } } } + { "mcpServers": { "pdfcraft": { "command": "pdfcraft-cli", "args": ["mcp", "--root", "/path/to/your/pdfs"] } } } ``` - `--root` confines every file the agent can read or write to one directory. Builds that should not include the server at all can use `cargo build -p printcraft-cli --no-default-features`. + `--root` confines every file the agent can read or write to one directory. Builds that should not include the server at all can use `cargo build -p pdfcraft-cli --no-default-features`. -- **The Rust API** (`printcraft_automation::Automation::call`), for embedding. +- **The Rust API** (`pdfcraft_automation::Automation::call`), for embedding. Edits stay in memory, undoable, until `doc_save`. Saving to the same file appends an incremental update, so the original bytes are preserved, and the write is atomic. Unsaved changes are never discarded silently. ### Driving the app itself -Start the desktop app with `printcraft --control /tmp/pc.json` and an agent can see and operate the real interface: the widget tree with labels and positions (from the accessibility tree), clicks, typing, keys, commands, view options and screenshots. This is also off by default. It listens only on loopback, and every connection must present the random token written to that file, which only you can read. +Start the desktop app with `pdfcraft --control /tmp/pc.json` and an agent can see and operate the real interface: the widget tree with labels and positions (from the accessibility tree), clicks, typing, keys, commands, view options and screenshots. This is also off by default. It listens only on loopback, and every connection must present the random token written to that file, which only you can read. ```sh -printcraft-cli ui --control /tmp/pc.json inspect query=rotate # find widgets -printcraft-cli ui --control /tmp/pc.json click label="Organize pages" -printcraft-cli ui --control /tmp/pc.json key key=K modifiers='["command"]' -printcraft-cli ui --control /tmp/pc.json command id=comment.square # pick a tool, then draw: -printcraft-cli ui --control /tmp/pc.json drag from='[400,300]' to='[600,420]' -printcraft-cli ui --control /tmp/pc.json screenshot --out window.png +pdfcraft-cli ui --control /tmp/pc.json inspect query=rotate # find widgets +pdfcraft-cli ui --control /tmp/pc.json click label="Organize pages" +pdfcraft-cli ui --control /tmp/pc.json key key=K modifiers='["command"]' +pdfcraft-cli ui --control /tmp/pc.json command id=comment.square # pick a tool, then draw: +pdfcraft-cli ui --control /tmp/pc.json drag from='[400,300]' to='[600,420]' +pdfcraft-cli ui --control /tmp/pc.json screenshot --out window.png ``` --- ## How it's built -PrintCraft is a Cargo workspace of focused crates, layered so the core never depends on the UI: +PdfCraft is a Cargo workspace of focused crates, layered so the core never depends on the UI: | Crate | What it does | |---|---| -| `printcraft-filters` | Every PDF stream filter (Flate, LZW, ASCII85, RunLength, predictors), encode and decode, property-tested | -| `printcraft-crypt` | The standard security handler: RC4, AES-128/256, revisions 2–6, permissions | -| `printcraft-cos` | The PDF object layer: tolerant parsing, repair, copy-on-write edits, incremental and full writing | -| `printcraft-organize` | Page operations, combine / extract / split, bookmarks, page labels, document information | -| `printcraft-fonts` | Font metrics and encodings for generated appearances | -| `printcraft-annot` | Comments: builders and appearance streams for notes, text markup, shapes, ink and text boxes; replies, status, edits | -| `printcraft-forms` | Interactive forms: the field model, filling with regenerated appearances, Clear form | -| `printcraft-render` | Rendering, inspection and text extraction with reading order | -| `printcraft-engine` | The façade every frontend uses: sessions, edits, undo, saving, the tool catalogue | -| `printcraft-automation` | Agent control: the headless tool table, `printcraft-cli run`, and the opt-in MCP server | -| `printcraft-ui-egui` | The desktop and web interface | +| `pdfcraft-filters` | Every PDF stream filter (Flate, LZW, ASCII85, RunLength, predictors), encode and decode, property-tested | +| `pdfcraft-crypt` | The standard security handler: RC4, AES-128/256, revisions 2–6, permissions | +| `pdfcraft-cos` | The PDF object layer: tolerant parsing, repair, copy-on-write edits, incremental and full writing | +| `pdfcraft-organize` | Page operations, combine / extract / split, bookmarks, page labels, document information | +| `pdfcraft-fonts` | Font metrics and encodings for generated appearances | +| `pdfcraft-annot` | Comments: builders and appearance streams for notes, text markup, shapes, ink and text boxes; replies, status, edits | +| `pdfcraft-forms` | Interactive forms: the field model, filling with regenerated appearances, Clear form | +| `pdfcraft-render` | Rendering, inspection and text extraction with reading order | +| `pdfcraft-engine` | The façade every frontend uses: sessions, edits, undo, saving, the tool catalogue | +| `pdfcraft-automation` | Agent control: the headless tool table, `pdfcraft-cli run`, and the opt-in MCP server | +| `pdfcraft-ui-egui` | The desktop and web interface | **Quality gates.** Every change passes the same automated checks: - formatting, and Clippy with warnings as errors; @@ -339,14 +339,14 @@ On top of those, two corpus sweeps run over real-world files: The output is verified with independent tools: hayro, qpdf and poppler. -PrintCraft is a clean-room implementation. Its behaviour comes from the ISO 32000 specification and black-box observation, never from anyone else's code. Every icon, font and image is openly licensed and listed in [ATTRIBUTION.md](ATTRIBUTION.md). +PdfCraft is a clean-room implementation. Its behaviour comes from the ISO 32000 specification and black-box observation, never from anyone else's code. Every icon, font and image is openly licensed and listed in [ATTRIBUTION.md](ATTRIBUTION.md). ## Get started ```sh -git clone https://github.com/storytold/printcraft -cd printcraft -cargo run --release -p printcraft -- some.pdf # desktop app +git clone https://github.com/storytold/pdfcraft +cd pdfcraft +cargo run --release -p pdfcraft -- some.pdf # desktop app cargo xtask demo-pdf # build the showcase PDF used in these screenshots cargo xtask screenshots # regenerate every screenshot in this README ``` @@ -357,12 +357,12 @@ edited PDFs): ```sh git clone https://github.com/storytold/craft-fonts ../craft-fonts -CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p printcraft -- some.pdf +CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p pdfcraft -- some.pdf ``` ## What's next -PrintCraft is young and moving fast. The aim is a workbench where you can view, organize, annotate, fill, sign and edit PDFs, at parity with Acrobat Pro. +PdfCraft is young and moving fast. The aim is a workbench where you can view, organize, annotate, fill, sign and edit PDFs, at parity with Acrobat Pro. **Where it stands (October 2026), honestly:** about half of Acrobat Pro's offline features are in (88% of the must-haves), but that is roughly a third of the work, because the hardest parts are still ahead. @@ -379,7 +379,7 @@ The honest assessment by area, what's lacking and where we're going are in **[RO ## The Crafting Apps -PrintCraft is one of the **Crafting Apps**: free, open-source creative tools from the +PdfCraft is one of the **Crafting Apps**: free, open-source creative tools from the [ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to stand on its own. @@ -389,7 +389,7 @@ stand on its own. | | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) | | | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) | | | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) | -| | **PrintCraft** | **Reading, organizing and protecting PDFs · you are here** | [GitHub](https://github.com/storytold/printcraft) | [Website](https://getartcraft.com/apps/printcraft) | +| | **PdfCraft** | **Reading, organizing and protecting PDFs · you are here** | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) | | | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) | | | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) | @@ -414,15 +414,15 @@ And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video stud discord.gg/artcraft · getartcraft.com · The Crafting Apps · - PrintCraft + PdfCraft

--- ## License and credits -PrintCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option. -Copyright (c) 2026 ArtCraft Team and the PrintCraft contributors. Required notices are in [NOTICE](NOTICE). +PdfCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option. +Copyright (c) 2026 ArtCraft Team and the PdfCraft contributors. Required notices are in [NOTICE](NOTICE). Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md). Release builds also embed @@ -431,10 +431,10 @@ the Japanese fonts of [craft-fonts](https://github.com/storytold/craft-fonts/blo The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as -part of this repository and PrintCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). +part of this repository and PdfCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). Forks and modified versions must remove them. -Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. PrintCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Adobe Inc.; these names are used only to describe the workflows it is compatible with. +Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. PdfCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Adobe Inc.; these names are used only to describe the workflows it is compatible with.

ArtCraft
diff --git a/ROADMAP.md b/ROADMAP.md index daa7c7b1..52fc9925 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,4 +1,4 @@ -# PrintCraft roadmap +# PdfCraft roadmap The milestones, current progress and time estimates to Acrobat Pro feature parity. Keep this up to date: - **Every session:** update the progress column and add a line to the log. @@ -6,7 +6,7 @@ The milestones, current progress and time estimates to Acrobat Pro feature parit Detailed task lists and acceptance tests are in `plan/execution-plan.md` (local-only). This file is the public summary. -**What "parity" means here:** the offline feature set of Acrobat Pro, milestones M0–M14. It excludes Adobe's cloud services (Document Cloud storage, Adobe Sign, the Adobe AI Assistant). Those have no clean-room equivalent; PrintCraft's alternatives are local-first, plus opt-in providers (M13). +**What "parity" means here:** the offline feature set of Acrobat Pro, milestones M0–M14. It excludes Adobe's cloud services (Document Cloud storage, Adobe Sign, the Adobe AI Assistant). Those have no clean-room equivalent; PdfCraft's alternatives are local-first, plus opt-in providers (M13). ## Estimate summary @@ -127,6 +127,7 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run Newest first. One line per session: the date, what moved, and the new overall percentage. +- **2026-10-07 (session 16):** PrintCraft is renamed PdfCraft: repo `storytold/pdfcraft`, crates and binaries `pdfcraft*`, bundle id `ai.storyteller.pdfcraft`. On first launch the app moves the old PrintCraft settings and crash-recovery folders to the new name, so upgrades keep recent files, preferences and unsaved work. No feature change. ≈ 30–35%. - **2026-10-07 (session 15):** Five community issues, each with a regression test: in two-page view, Next page moves a whole spread (#77, #70); Export to Word opens in Word again, with control characters dropped and receipt-length pages kept within Word's 22 in, checked in Microsoft Word (#78, #72); macOS opens PDFs from Finder, Open With and the Dock (Apple events through the audited `fmv-macos-events`, PDF declared in the Info.plist), and the packaging no longer describes the image editor it was copied from (#82, #73); Add text keeps the box being typed when you click elsewhere, with Done and Discard beside it, reproduced and checked with real OS input (#86, #74); sharper, higher-contrast text, most visible on Windows, with every text colour at WCAG AA in both themes (#84, #76; the console window was already gone in 0.2.1). Still open: #79 (layered "not verified" signature appearances), #80 (native macOS menu bar), #81 (Homebrew and Scoop). P0 88%, P1 53%; 805 features 50.2% shipped (53.7% weighted). ≈ 30–35%. - **2026-10-06 (session 14):** Issues: move and resize existing text boxes in Edit text (#59, closes #40); Windows release builds open no console window and the Start Menu shortcut has its icon (#58, #57; rolled out to every Craft app); native Windows ARM64 installers, installed and run on an ARM64 runner in CI (#66, #71; the intermittent ARM64 test crash was a WARP shader-JIT race between parallel GPU-rendering tests, not our code); XFA forms are detected and explained instead of failing silently (#65, #60 stays open for real XFA). Also: squiggly underline tool, comment opacity, Preferences ▸ Identity, drag-and-drop/hover tests; community table export to Word/HTML/RTF (#62, with linear-time detection and a column cap). #63 (timestamps/LTV with outbound network) held for review fixes and an owner decision on network access. ≈ 30–35% (unchanged). - **2026-10-05 (session 13, later):** FreeBSD: CI builds and tests the workspace in a FreeBSD 14.3 VM (#45) and the release ships a FreeBSD x86_64 tarball, checked in CI (#42); README lists FreeBSD. Fuzzing: the nightly job's 11 hangs fixed (#41: our parser's exponential retries, inline images, Type 3 fan-out), plus a JBIG2 hang (#43, vendored hayro-jbig2) and a Type 1 font stack overflow (#46, skrifa 0.47); a full run now finds 0 crashes. Landed community PRs #36 (text editing, Japanese replacements), #47 (English/Japanese interface) and #48 (XFDF colour crash). Windows launch crash with Intel's Vulkan driver fixed by using Direct3D 12 (#49, issue #37); one copy of the Japanese font instead of two (#51, −8.3 MB); full screen and About get real tests (#50). P0 88%, P1 52%. ≈ 30–35%. @@ -174,7 +175,7 @@ Newest first. One line per session: the date, what moved, and the new overall pe - Overall ≈ 9.5%. - **2026-09-30 (session 5, after a machine crash; no work lost):** - - Agent control: new `printcraft-automation` crate with 20 JSON-Schema tools, an opt-in MCP server (`printcraft-cli mcp`, stdio only, can be compiled out), and `printcraft-cli run`/`tools`. + - Agent control: new `pdfcraft-automation` crate with 20 JSON-Schema tools, an opt-in MCP server (`pdfcraft-cli mcp`, stdio only, can be compiled out), and `pdfcraft-cli run`/`tools`. - Text on rotated pages now reads along its lines; the pdf.js oracle is unchanged at median 0.980. - Finding text in 520 pages: 17.9 s → 3.7 s (parallel), about 20 ms when repeated (cached). - CI: dependency licence audit (`deny.toml`, `xtask deny`) and a GitHub workflow for macOS, Windows, Linux and wasm. diff --git a/apps/pdfcraft-cli/Cargo.toml b/apps/pdfcraft-cli/Cargo.toml new file mode 100644 index 00000000..d2add5d7 --- /dev/null +++ b/apps/pdfcraft-cli/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "pdfcraft-cli" +description = "PdfCraft command-line tool: inspect, render and robustness-check PDFs" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "pdfcraft-cli" +path = "src/main.rs" + +[lints] +workspace = true + +[dependencies] +pdfcraft-render = { workspace = true } +serde_json = { workspace = true } +pdfcraft-engine = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-automation = { workspace = true } +base64 = "0.22" + +[features] +default = ["mcp"] +# The opt-in MCP server (`pdfcraft-cli mcp`). Build with --no-default-features to leave it out. +mcp = ["pdfcraft-automation/mcp"] diff --git a/apps/printcraft-cli/src/main.rs b/apps/pdfcraft-cli/src/main.rs similarity index 86% rename from apps/printcraft-cli/src/main.rs rename to apps/pdfcraft-cli/src/main.rs index f703f676..6e79ffe3 100644 --- a/apps/printcraft-cli/src/main.rs +++ b/apps/pdfcraft-cli/src/main.rs @@ -1,24 +1,24 @@ -//! printcraft-cli — headless PrintCraft. +//! pdfcraft-cli — headless PdfCraft. //! //! ```text -//! printcraft-cli info [--password PW] document summary as JSON -//! printcraft-cli render --page N [--dpi 96] --out x.pam -//! printcraft-cli text [--page N] extracted text (pages separated by form feeds) -//! printcraft-cli edit --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1] +//! pdfcraft-cli info [--password PW] document summary as JSON +//! pdfcraft-cli render --page N [--dpi 96] --out x.pam +//! pdfcraft-cli text [--page N] extracted text (pages separated by form feeds) +//! pdfcraft-cli edit --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1] //! [--insert-blank 1] [--title T] [--author A] [--full] -//! printcraft-cli combine … --out combined.pdf -//! printcraft-cli extract --pages 1,3,5 --out out.pdf -//! printcraft-cli split (--every N | --before 3,7) [--out-dir DIR] -//! printcraft-cli check [--timeout 20] [--dpi 36] [--json out.json] -//! printcraft-cli tools automation tools and their JSON Schemas -//! printcraft-cli run [key=value …] [--root DIR] [--out image.png] -//! printcraft-cli run --script steps.json [--root DIR] [{"tool": "doc_open", "args": {…}}, …] -//! printcraft-cli mcp [--root DIR] MCP server on stdin/stdout (opt-in) -//! printcraft-cli ui --control FILE [key=value …] [--out shot.png] +//! pdfcraft-cli combine … --out combined.pdf +//! pdfcraft-cli extract --pages 1,3,5 --out out.pdf +//! pdfcraft-cli split (--every N | --before 3,7) [--out-dir DIR] +//! pdfcraft-cli check [--timeout 20] [--dpi 36] [--json out.json] +//! pdfcraft-cli tools automation tools and their JSON Schemas +//! pdfcraft-cli run [key=value …] [--root DIR] [--out image.png] +//! pdfcraft-cli run --script steps.json [--root DIR] [{"tool": "doc_open", "args": {…}}, …] +//! pdfcraft-cli mcp [--root DIR] MCP server on stdin/stdout (opt-in) +//! pdfcraft-cli ui --control FILE [key=value …] [--out shot.png] //! drive a running app started with --control FILE //! ``` //! -//! `run` and `mcp` drive the same tool table (`printcraft-automation`). In `run`, values parse as +//! `run` and `mcp` drive the same tool table (`pdfcraft-automation`). In `run`, values parse as //! JSON when they can (`pages=[1,3]`, `degrees=90`) and are strings otherwise. A script runs its //! steps in one session, so `doc_open` returns id 1, the next document id 2, and so on. //! @@ -37,7 +37,7 @@ use std::process::{Command, ExitCode, Stdio}; use std::sync::Arc; use std::time::{Duration, Instant}; -use printcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind, inspect}; +use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind, inspect}; fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); @@ -58,19 +58,19 @@ fn main() -> ExitCode { #[cfg(feature = "mcp")] Some("mcp") => mcp(&args[1..]), Some("--version") => { - println!("printcraft-cli {}", env!("CARGO_PKG_VERSION")); - println!("Discord: {} (help and feedback)", printcraft_engine::links::DISCORD); - println!("Web: {}", printcraft_engine::links::APP_PAGE); - println!("Source: {}", printcraft_engine::links::GITHUB); + println!("pdfcraft-cli {}", env!("CARGO_PKG_VERSION")); + println!("Discord: {} (help and feedback)", pdfcraft_engine::links::DISCORD); + println!("Web: {}", pdfcraft_engine::links::APP_PAGE); + println!("Source: {}", pdfcraft_engine::links::GITHUB); Ok(()) } - _ => Err("usage: printcraft-cli … (see source header for options)\nhelp and feedback: https://discord.gg/artcraft" + _ => Err("usage: pdfcraft-cli … (see source header for options)\nhelp and feedback: https://discord.gg/artcraft" .into()), }; match result { Ok(()) => ExitCode::SUCCESS, Err(e) => { - eprintln!("printcraft-cli: {e}"); + eprintln!("pdfcraft-cli: {e}"); ExitCode::FAILURE } } @@ -112,7 +112,7 @@ fn info(args: &[String]) -> Result<(), String> { "first_page_pt": info.pages.first().map(|p| [p.width, p.height]), "title": info.title, "author": info.author, "producer": info.producer, "creator": info.creator, "encrypted": info.encrypted, "tagged": info.tagged, "javascript": info.has_javascript, - "xfa": info.xfa.map(|x| match x { printcraft_render::Xfa::Static => "static", printcraft_render::Xfa::Dynamic => "dynamic" }), + "xfa": info.xfa.map(|x| match x { pdfcraft_render::Xfa::Static => "static", pdfcraft_render::Xfa::Dynamic => "dynamic" }), "bookmarks": info.outline.len(), "annotations": info.annotations.len(), "fields": info.fields.len(), "links": info.links.len(), "layers": info.layers.len(), "attachments": info.attachments.len(), "page_labels": info.pages.iter().take(8).map(|p| p.label.clone()).collect::>(), @@ -150,7 +150,7 @@ fn page_list(s: &str) -> Result, String> { /// Apply page and metadata edits through the engine and save (incrementally unless `--full`). fn edit(args: &[String]) -> Result<(), String> { - use printcraft_engine::{Edit, Session}; + use pdfcraft_engine::{Edit, Session}; let path = *positional(args).first().ok_or("edit: missing file")?; let out = flag(args, "--out").ok_or("edit: missing --out")?; let mut session = Session::new(); @@ -198,7 +198,7 @@ fn combine(args: &[String]) -> Result<(), String> { return Err("combine: give at least two input files".into()); } let sources = inputs.iter().map(|p| Ok((file_stem(p), read(p)?))).collect::, String>>()?; - let bytes = printcraft_engine::Session::new().combine(&sources).map_err(|e| e.to_string())?; + let bytes = pdfcraft_engine::Session::new().combine(&sources).map_err(|e| e.to_string())?; std::fs::write(out, bytes.as_slice()).map_err(|e| format!("{out}: {e}")) } @@ -206,14 +206,14 @@ fn extract(args: &[String]) -> Result<(), String> { let path = *positional(args).first().ok_or("extract: missing file")?; let out = flag(args, "--out").ok_or("extract: missing --out")?; let pages = page_list(flag(args, "--pages").ok_or("extract: missing --pages")?)?; - let mut session = printcraft_engine::Session::new(); + let mut session = pdfcraft_engine::Session::new(); let id = session.open(path, None, read(path)?, flag(args, "--password")).map_err(|e| e.to_string())?; let bytes = session.extract(id, &pages).map_err(|e| e.to_string())?; std::fs::write(out, bytes.as_slice()).map_err(|e| format!("{out}: {e}")) } fn split(args: &[String]) -> Result<(), String> { - use printcraft_engine::SplitBy; + use pdfcraft_engine::SplitBy; let path = *positional(args).first().ok_or("split: missing file")?; let by = match (flag(args, "--every"), flag(args, "--before")) { (Some(n), None) => SplitBy::PageCount(n.parse().map_err(|_| "bad --every")?), @@ -221,7 +221,7 @@ fn split(args: &[String]) -> Result<(), String> { _ => return Err("split: give either --every N or --before PAGES".into()), }; let dir = PathBuf::from(flag(args, "--out-dir").unwrap_or(".")); - let mut session = printcraft_engine::Session::new(); + let mut session = pdfcraft_engine::Session::new(); let id = session.open(path, None, read(path)?, flag(args, "--password")).map_err(|e| e.to_string())?; let stem = file_stem(path); for (a, b, bytes) in session.split(id, &by).map_err(|e| e.to_string())? { @@ -288,7 +288,7 @@ fn check_one(args: &[String]) -> Result<(), String> { /// Open with cos, touch every object, edit the catalog, save incrementally and in full (both /// output styles), and reopen each result. Errors are fine; panics, hangs and aborts are bugs. fn edit_round_trip(bytes: &[u8]) { - use printcraft_cos::{Document, Object, SaveOptions, write_full, write_incremental}; + use pdfcraft_cos::{Document, Object, SaveOptions, write_full, write_incremental}; let Ok(mut doc) = Document::open(Arc::new(bytes.to_vec())) else { return }; for num in doc.object_numbers().into_iter().take(20_000) { if let Ok(o) = doc.try_get(num) @@ -298,7 +298,7 @@ fn edit_round_trip(bytes: &[u8]) { } } if let Some(root) = doc.root() { - let _ = doc.update_dict(root, |d| d.set(b"PrintCraftFuzz".to_vec(), Object::Bool(true))); + let _ = doc.update_dict(root, |d| d.set(b"PdfCraftFuzz".to_vec(), Object::Bool(true))); } let classic = SaveOptions { object_streams: false, ..SaveOptions::default() }; for out in [write_incremental(&doc, &SaveOptions::default()), write_full(&doc, &SaveOptions::default()), write_full(&doc, &classic)] @@ -397,8 +397,8 @@ fn run_child(exe: &Path, file: &Path, dpi: &str, timeout: Duration) -> serde_jso // ---- automation -------------------------------------------------------------------------------- -fn automation(args: &[String]) -> Result { - let a = printcraft_automation::Automation::new(); +fn automation(args: &[String]) -> Result { + let a = pdfcraft_automation::Automation::new(); match flag(args, "--root") { Some(root) => a.with_root(root).map_err(|e| format!("--root {root}: {e}")), None => Ok(a), @@ -406,7 +406,7 @@ fn automation(args: &[String]) -> Result Result<(), String> { - let list: Vec = printcraft_automation::tools() + let list: Vec = pdfcraft_automation::tools() .iter() .map(|t| serde_json::json!({ "name": t.name, "description": t.description, "read_only": t.read_only, "command": t.command, "input_schema": t.input_schema })) .collect(); @@ -415,11 +415,11 @@ fn tools() -> Result<(), String> { } /// Print a tool's result: JSON as JSON; images go to `--out` (or are summarised). -fn print_output(content: Vec, out: Option<&str>) -> Result<(), String> { +fn print_output(content: Vec, out: Option<&str>) -> Result<(), String> { for c in content { match c { - printcraft_automation::Content::Json(v) => println!("{}", serde_json::to_string_pretty(&v).unwrap_or_default()), - printcraft_automation::Content::Png { data, width, height } => match out { + pdfcraft_automation::Content::Json(v) => println!("{}", serde_json::to_string_pretty(&v).unwrap_or_default()), + pdfcraft_automation::Content::Png { data, width, height } => match out { Some(path) => { std::fs::write(path, &data).map_err(|e| format!("{path}: {e}"))?; println!("{}", serde_json::json!({ "image": path, "width": width, "height": height })); @@ -446,7 +446,7 @@ fn run(args: &[String]) -> Result<(), String> { } return Ok(()); } - let tool = *positional(args).first().ok_or("run: missing tool name (see `printcraft-cli tools`)")?; + let tool = *positional(args).first().ok_or("run: missing tool name (see `pdfcraft-cli tools`)")?; let mut obj = serde_json::Map::new(); for kv in positional(args).iter().skip(1) { let (k, v) = kv.split_once('=').ok_or(format!("run: expected key=value, got {kv:?}"))?; @@ -459,17 +459,17 @@ fn run(args: &[String]) -> Result<(), String> { #[cfg(feature = "mcp")] fn mcp(args: &[String]) -> Result<(), String> { - let mut server = printcraft_automation::mcp::McpServer::new(automation(args)?); - eprintln!("printcraft-cli: MCP server on stdio (protocol {}); close stdin to stop", printcraft_automation::mcp::PROTOCOL_VERSIONS[0]); + let mut server = pdfcraft_automation::mcp::McpServer::new(automation(args)?); + eprintln!("pdfcraft-cli: MCP server on stdio (protocol {}); close stdin to stop", pdfcraft_automation::mcp::PROTOCOL_VERSIONS[0]); server.serve(std::io::stdin().lock(), std::io::stdout().lock()).map_err(|e| e.to_string()) } // ---- UI control channel client ----------------------------------------------------------------- -/// One request to a running app's control channel (`printcraft --control FILE`). +/// One request to a running app's control channel (`pdfcraft --control FILE`). fn ui(args: &[String]) -> Result<(), String> { use std::io::{BufRead, BufReader, Write as _}; - let file = flag(args, "--control").ok_or("ui: missing --control FILE (start the app with `printcraft --control FILE`)")?; + let file = flag(args, "--control").ok_or("ui: missing --control FILE (start the app with `pdfcraft --control FILE`)")?; let info: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(file).map_err(|e| format!("{file}: {e}"))?).map_err(|e| format!("{file}: {e}"))?; let port = info["port"].as_u64().ok_or(format!("{file}: no port"))?; diff --git a/apps/printcraft-web/Cargo.toml b/apps/pdfcraft-web/Cargo.toml similarity index 75% rename from apps/printcraft-web/Cargo.toml rename to apps/pdfcraft-web/Cargo.toml index dc7ad99c..a8d472d4 100644 --- a/apps/printcraft-web/Cargo.toml +++ b/apps/pdfcraft-web/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "printcraft-web" -description = "PrintCraft in the browser (wasm32; build with `trunk build --release` in this directory)" +name = "pdfcraft-web" +description = "PdfCraft in the browser (wasm32; build with `trunk build --release` in this directory)" version.workspace = true edition.workspace = true license.workspace = true @@ -10,7 +10,7 @@ publish = false workspace = true [dependencies] -printcraft-ui-egui = { workspace = true } +pdfcraft-ui-egui = { workspace = true } eframe = { workspace = true, features = ["default_fonts", "glow", "persistence", "accesskit"] } egui = { workspace = true } diff --git a/apps/printcraft-web/Trunk.toml b/apps/pdfcraft-web/Trunk.toml similarity index 77% rename from apps/printcraft-web/Trunk.toml rename to apps/pdfcraft-web/Trunk.toml index 7a307477..563c2267 100644 --- a/apps/printcraft-web/Trunk.toml +++ b/apps/pdfcraft-web/Trunk.toml @@ -1,4 +1,4 @@ -# Build with: trunk build --release (from apps/printcraft-web) +# Build with: trunk build --release (from apps/pdfcraft-web) # The release zip (packaging/web/package.sh) expects the site in dist/web with relative URLs. [build] target = "index.html" diff --git a/apps/printcraft-web/index.html b/apps/pdfcraft-web/index.html similarity index 88% rename from apps/printcraft-web/index.html rename to apps/pdfcraft-web/index.html index 5980e367..46b3a25e 100644 --- a/apps/printcraft-web/index.html +++ b/apps/pdfcraft-web/index.html @@ -3,7 +3,7 @@ - PrintCraft + PdfCraft - + diff --git a/apps/printcraft-web/src/main.rs b/apps/pdfcraft-web/src/main.rs similarity index 84% rename from apps/printcraft-web/src/main.rs rename to apps/pdfcraft-web/src/main.rs index b96a91b7..cf37bda9 100644 --- a/apps/printcraft-web/src/main.rs +++ b/apps/pdfcraft-web/src/main.rs @@ -1,4 +1,4 @@ -//! PrintCraft in the browser. Build: `cd apps/printcraft-web && trunk build --release` +//! PdfCraft in the browser. Build: `cd apps/pdfcraft-web && trunk build --release` //! (trunk generates the JS loader; no handwritten JS — plan/execution-plan.md §1). #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -6,13 +6,13 @@ #[cfg(target_arch = "wasm32")] fn main() { use eframe::wasm_bindgen::JsCast; - use printcraft_ui_egui::PrintCraftApp; + use pdfcraft_ui_egui::PdfCraftApp; let options = eframe::WebOptions { renderer: eframe::Renderer::Glow, ..Default::default() }; wasm_bindgen_futures::spawn_local(async move { let Some(canvas) = web_sys::window() .and_then(|w| w.document()) - .and_then(|d| d.get_element_by_id("printcraft")) + .and_then(|d| d.get_element_by_id("pdfcraft")) .and_then(|e| e.dyn_into::().ok()) else { return; @@ -22,8 +22,9 @@ fn main() { canvas, options, Box::new(|cc| { - let mut app = PrintCraftApp::new(); - if let Some(json) = cc.storage.and_then(|s| s.get_string("printcraft")) { + let mut app = PdfCraftApp::new(); + // "printcraft": settings saved under the app's former name, PrintCraft. + if let Some(json) = cc.storage.and_then(|s| s.get_string("pdfcraft").or_else(|| s.get_string("printcraft"))) { app.restore(&json); } // `?file=` opens a PDF from a URL (same-origin or CORS-enabled). @@ -40,7 +41,7 @@ fn main() { } ctx.request_repaint(); } - Err(e) => eframe::web_sys::console::error_1(&format!("PrintCraft: could not fetch {url}: {e}").into()), + Err(e) => eframe::web_sys::console::error_1(&format!("PdfCraft: could not fetch {url}: {e}").into()), } }); } @@ -75,5 +76,5 @@ async fn fetch_bytes(url: &str) -> Result, String> { #[cfg(not(target_arch = "wasm32"))] fn main() { - eprintln!("printcraft-web targets wasm32: run `trunk serve` or `trunk build --release` in apps/printcraft-web"); + eprintln!("pdfcraft-web targets wasm32: run `trunk serve` or `trunk build --release` in apps/pdfcraft-web"); } diff --git a/apps/printcraft/Cargo.toml b/apps/pdfcraft/Cargo.toml similarity index 84% rename from apps/printcraft/Cargo.toml rename to apps/pdfcraft/Cargo.toml index e0215885..df5cc75f 100644 --- a/apps/printcraft/Cargo.toml +++ b/apps/pdfcraft/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "printcraft" -description = "PrintCraft desktop app" +name = "pdfcraft" +description = "PdfCraft desktop app" version.workspace = true edition.workspace = true license.workspace = true @@ -13,7 +13,7 @@ workspace = true # trusted roots. No bundled root list (webpki-roots and webpki-root-certs are CDLA-licensed). ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "_ring"] } rustls-native-certs = "0.8" -printcraft-ui-egui = { workspace = true } +pdfcraft-ui-egui = { workspace = true } eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "persistence", "links"] } egui = { workspace = true } serde_json = { workspace = true } @@ -25,5 +25,5 @@ serde_json = { workspace = true } fmv-macos-events = "=0.1.0" [build-dependencies] -# Icon + VERSIONINFO for printcraft.exe; build.rs only uses it when targeting Windows. +# Icon + VERSIONINFO for pdfcraft.exe; build.rs only uses it when targeting Windows. winresource = "0.1" diff --git a/apps/pdfcraft/build.rs b/apps/pdfcraft/build.rs new file mode 100644 index 00000000..df73faa2 --- /dev/null +++ b/apps/pdfcraft/build.rs @@ -0,0 +1,29 @@ +//! Windows only: embed the app icon and version info (VERSIONINFO) into `pdfcraft.exe`, so it +//! shows in Explorer, the taskbar, the Start menu and Alt-Tab. +//! +//! On every other target this does nothing. A missing resource compiler is a warning, so a +//! cross-compile from macOS or Linux still links, unless `PDFCRAFT_REQUIRE_WINRES=1` turns it +//! into an error (for release builds). + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../../assets/app-icon/pdfcraft.ico"); + println!("cargo:rerun-if-env-changed=PDFCRAFT_REQUIRE_WINRES"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { + return; + } + let mut res = winresource::WindowsResource::new(); + res.set_icon("../../assets/app-icon/pdfcraft.ico") + .set("ProductName", "PdfCraft") + .set("FileDescription", "PdfCraft PDF workbench") + .set("LegalCopyright", "Copyright (c) the PdfCraft contributors. MIT OR Apache-2.0.") + .set("OriginalFilename", "pdfcraft.exe") + .set("InternalName", "pdfcraft"); + if let Err(e) = res.compile() { + if std::env::var_os("PDFCRAFT_REQUIRE_WINRES").is_some() { + println!("cargo::error=embedding Windows resources failed: {e}"); + return; + } + println!("cargo:warning=pdfcraft.exe built without icon/version resources: {e}"); + } +} diff --git a/apps/printcraft/src/apple_events.rs b/apps/pdfcraft/src/apple_events.rs similarity index 87% rename from apps/printcraft/src/apple_events.rs rename to apps/pdfcraft/src/apple_events.rs index 37f8aaf6..36cab0e0 100644 --- a/apps/printcraft/src/apple_events.rs +++ b/apps/pdfcraft/src/apple_events.rs @@ -1,9 +1,9 @@ //! macOS open-documents and quit Apple events (#73). //! -//! Finder double-clicks, Open With, drops on the Dock icon and `open -a PrintCraft file.pdf` don't +//! Finder double-clicks, Open With, drops on the Dock icon and `open -a PdfCraft file.pdf` don't //! pass paths on the command line: LaunchServices sends the running (or just-launched) app a //! `kAEOpenDocuments` ('odoc') Apple event. winit 0.30 doesn't handle it and owns the -//! `NSApplicationDelegate`, so AppKit answered "PrintCraft cannot open files in the PDF document +//! `NSApplicationDelegate`, so AppKit answered "PdfCraft cannot open files in the PDF document //! format". Handling it ourselves needs Objective-C class declarations, i.e. `unsafe`, which this //! workspace forbids. The audited `fmv-macos-events` crate (also used by PhotoCraft) wraps exactly //! that, an `NSAppleEventManager` handler registered before Finder's launch event that leaves @@ -11,7 +11,7 @@ //! [`OsEvent`]s that the UI polls every frame. use fmv_macos_events::{Event, Inbox, Registration}; -use printcraft_ui_egui::{OsEvent, OsEventsFn}; +use pdfcraft_ui_egui::{OsEvent, OsEventsFn}; /// Keeps the Apple-event handlers registered; hold it until the event loop returns. pub struct AppleEvents { @@ -27,7 +27,7 @@ impl AppleEvents { Self { _registration: registration, inbox } } - /// The queue the UI drains (`PrintCraftApp::os_events`); events arriving later wake `ctx`. + /// The queue the UI drains (`PdfCraftApp::os_events`); events arriving later wake `ctx`. pub fn connect(&self, ctx: &egui::Context) -> OsEventsFn { let ctx = ctx.clone(); self.inbox.set_wake(move || ctx.request_repaint()); diff --git a/apps/printcraft/src/main.rs b/apps/pdfcraft/src/main.rs similarity index 74% rename from apps/printcraft/src/main.rs rename to apps/pdfcraft/src/main.rs index cf796360..c36b09f7 100644 --- a/apps/printcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -1,6 +1,6 @@ -//! PrintCraft desktop app. +//! PdfCraft desktop app. //! -//! Usage: `printcraft [options] [files…]` +//! Usage: `pdfcraft [options] [files…]` //! //! View options (applied after the files open; also the seed of the UI control channel): //! `--page N --zoom 150 --layout continuous|two-up|single --panel comments|bookmarks|pages|fields|layers|attachments|none @@ -9,7 +9,7 @@ //! //! `--control ` enables the UI control channel (off by default): the app listens on a random //! loopback port and writes `{"port", "token", "pid"}` to `` (owner-only permissions). -//! Agents then drive it with `printcraft-cli ui --control …`. +//! Agents then drive it with `pdfcraft-cli ui --control …`. // Release builds on Windows are GUI-subsystem programs, so launching the app doesn't open a console // window next to it (#57). `--version` and diagnostics then go nowhere when started from a terminal @@ -17,27 +17,54 @@ #![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_ui_egui::PrintCraftApp; +use pdfcraft_ui_egui::PdfCraftApp; #[cfg(target_os = "macos")] mod apple_events; mod updates; /// Freedesktop app id: the `.desktop` file name and the hicolor icon name. -const APP_ID: &str = "ai.storyteller.printcraft"; +const APP_ID: &str = "ai.storyteller.pdfcraft"; /// The app icon (assets/app-icon/README.md). macOS gets the version on Apple's icon grid, with a /// transparent margin; Windows and Linux get the full-bleed tile. #[cfg(target_os = "macos")] -const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/printcraft-1024.png"); +const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/pdfcraft-1024.png"); #[cfg(not(target_os = "macos"))] -const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.printcraft.png"); +const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.pdfcraft.png"); + +/// The app was called PrintCraft before; settings saved then are under this key. +const LEGACY_STORAGE_KEY: &str = "printcraft"; + +/// Move the settings and crash-recovery folders of the app's former name, PrintCraft, to the new +/// name once, so an upgrade keeps recent files, preferences and unsaved work. Best effort: a +/// folder is left alone when the new one already exists or the move fails. +fn migrate_legacy_folders() { + let mut moves = vec![(eframe::storage_dir("PrintCraft"), eframe::storage_dir("PdfCraft"))]; + // Recovery lives in the settings folder except on Windows, where it is under %LOCALAPPDATA%. + if cfg!(windows) { + let local = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty()).map(std::path::PathBuf::from); + moves.push((local.as_ref().map(|d| d.join("PrintCraft")), local.map(|d| d.join("PdfCraft")))); + } + for (old, new) in moves { + let (Some(old), Some(new)) = (old, new) else { continue }; + if !old.is_dir() || new.exists() { + continue; + } + if let Some(parent) = new.parent() { + let _ = std::fs::create_dir_all(parent); + } + if let Err(e) = std::fs::rename(&old, &new) { + eprintln!("pdfcraft: moving {} to {}: {e}", old.display(), new.display()); + } + } +} fn main() -> eframe::Result { // Last-resort guard (AGENTS.md §4): commands, edits, opens and saves catch panics and report // them; this hook logs every panic, caught or not, with a backtrace when RUST_BACKTRACE is set. std::panic::set_hook(Box::new(|info| { - eprintln!("printcraft: internal error: {info}"); + eprintln!("pdfcraft: internal error: {info}"); let trace = std::backtrace::Backtrace::capture(); if trace.status() == std::backtrace::BacktraceStatus::Captured { eprintln!("{trace}"); @@ -50,7 +77,7 @@ fn main() -> eframe::Result { while let Some(a) = args.next() { match a.as_str() { "--version" => { - println!("printcraft {}", env!("CARGO_PKG_VERSION")); + println!("pdfcraft {}", env!("CARGO_PKG_VERSION")); return Ok(()); } "--control" => control_file = args.next(), @@ -63,22 +90,23 @@ fn main() -> eframe::Result { } let integrated = cfg!(target_os = "macos"); let mut viewport = egui::ViewportBuilder::default() - .with_title("PrintCraft") + .with_title("PdfCraft") .with_inner_size([1440.0, 920.0]) .with_min_inner_size([820.0, 520.0]) .with_drag_and_drop(true) - // Wayland app id: matches packaging/linux/ai.storyteller.printcraft.desktop. + // Wayland app id: matches packaging/linux/ai.storyteller.pdfcraft.desktop. .with_app_id(APP_ID); // Dock, taskbar, Alt-Tab and launcher icon when running unbundled. match eframe::icon_data::from_png_bytes(APP_ICON_PNG) { Ok(icon) => viewport = viewport.with_icon(icon), - Err(e) => eprintln!("printcraft: app icon: {e}"), + Err(e) => eprintln!("pdfcraft: app icon: {e}"), } if integrated { viewport = viewport.with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false); } - // eframe would otherwise derive the settings folder from the app id: keep it under "PrintCraft". - let persistence_path = eframe::storage_dir("PrintCraft").map(|d| d.join("app.ron")); + migrate_legacy_folders(); + // eframe would otherwise derive the settings folder from the app id: keep it under "PdfCraft". + let persistence_path = eframe::storage_dir("PdfCraft").map(|d| d.join("app.ron")); let mut native = eframe::NativeOptions { viewport, persistence_path, ..Default::default() }; configure_gpu(&mut native); // Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one @@ -88,11 +116,11 @@ fn main() -> eframe::Result { #[cfg(target_os = "macos")] let apple_events = &apple_events; eframe::run_native( - "PrintCraft", + "PdfCraft", native, Box::new(move |cc| { - let mut app = PrintCraftApp::new(); - if let Some(json) = cc.storage.and_then(|s| s.get_string("printcraft")) { + let mut app = PdfCraftApp::new(); + if let Some(json) = cc.storage.and_then(|s| s.get_string("pdfcraft").or_else(|| s.get_string(LEGACY_STORAGE_KEY))) { app.restore(&json); } app.integrated_titlebar = integrated; @@ -104,21 +132,21 @@ fn main() -> eframe::Result { } if let Some(file) = &control_file { let client = app.attach_control(&cc.egui_ctx); - match printcraft_ui_egui::control::serve(client).and_then(|ep| write_control_file(file, ep.port, &ep.token).map(|()| ep.port)) { - Ok(port) => eprintln!("printcraft: UI control channel on 127.0.0.1:{port} (connection details in {file})"), - Err(e) => eprintln!("printcraft: --control {file}: {e}"), + match pdfcraft_ui_egui::control::serve(client).and_then(|ep| write_control_file(file, ep.port, &ep.token).map(|()| ep.port)) { + Ok(port) => eprintln!("pdfcraft: UI control channel on 127.0.0.1:{port} (connection details in {file})"), + Err(e) => eprintln!("pdfcraft: --control {file}: {e}"), } } // Autosave unsaved changes; offer to recover documents a crashed session left behind. - if let Some(dir) = printcraft_ui_egui::RecoveryStore::default_dir() { - app.enable_recovery(printcraft_ui_egui::RecoveryStore::new(dir)); + if let Some(dir) = pdfcraft_ui_egui::RecoveryStore::default_dir() { + app.enable_recovery(pdfcraft_ui_egui::RecoveryStore::new(dir)); } for f in files { app.open_path(&f); } for (k, v) in options { if let Err(e) = app.set_option(&k, &v) { - eprintln!("printcraft: --{k} {v}: {e}"); + eprintln!("pdfcraft: --{k} {v}: {e}"); } } Ok(Box::new(app)) @@ -154,7 +182,7 @@ fn write_control_file(path: &str, port: u16, token: &str) -> std::io::Result<()> /// It also saves battery. Machines with one GPU are unaffected. /// - On Windows, use Direct3D 12, falling back to OpenGL, and never load Vulkan drivers unless /// `WGPU_BACKEND` asks for them. Creating a Vulkan instance loads every installed Vulkan driver -/// into the process, and a faulty one (an Intel driver in issue #37) crashed PrintCraft before +/// into the process, and a faulty one (an Intel driver in issue #37) crashed PdfCraft before /// its window appeared. D3D12 is the native, best-supported backend there. fn configure_gpu(native: &mut eframe::NativeOptions) { let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut native.wgpu_options.wgpu_setup else { return }; diff --git a/apps/printcraft/src/updates.rs b/apps/pdfcraft/src/updates.rs similarity index 78% rename from apps/printcraft/src/updates.rs rename to apps/pdfcraft/src/updates.rs index 708065f4..e88c2f97 100644 --- a/apps/printcraft/src/updates.rs +++ b/apps/pdfcraft/src/updates.rs @@ -1,10 +1,10 @@ -//! Asks GitHub for the latest PrintCraft release (Help ▸ Check for updates, issue #28). +//! Asks GitHub for the latest PdfCraft release (Help ▸ Check for updates, issue #28). use std::time::Duration; -use printcraft_ui_egui::updates::{RELEASES_PAGE, Release}; +use pdfcraft_ui_egui::updates::{RELEASES_PAGE, Release}; -const LATEST: &str = "https://api.github.com/repos/storytold/printcraft/releases/latest"; +const LATEST: &str = "https://api.github.com/repos/storytold/pdfcraft/releases/latest"; /// The latest release. The answer is untrusted: its size is capped, and only a page under /// [`RELEASES_PAGE`] is ever offered for download (anything else falls back to that list). @@ -17,7 +17,7 @@ pub fn latest_release() -> Result { let mut response = agent .get(LATEST) .header("Accept", "application/vnd.github+json") - .header("User-Agent", concat!("PrintCraft/", env!("CARGO_PKG_VERSION"))) + .header("User-Agent", concat!("PdfCraft/", env!("CARGO_PKG_VERSION"))) .call() .map_err(|e| format!("couldn't reach GitHub ({e})"))?; let body = response.body_mut().with_config().limit(1 << 20).read_to_string().map_err(|e| format!("unreadable answer ({e})"))?; @@ -51,9 +51,9 @@ mod tests { #[test] fn answers_are_read_and_only_our_release_pages_are_offered() { - let r = parse(r#"{"tag_name":"v0.2.0","html_url":"https://github.com/storytold/printcraft/releases/tag/v0.2.0"}"#).unwrap(); - assert_eq!(r, Release { version: "v0.2.0".into(), url: "https://github.com/storytold/printcraft/releases/tag/v0.2.0".into() }); - for elsewhere in ["https://example.com/printcraft.exe", "https://github.com/storytold/printcraft/releases.evil/x", "javascript:alert(1)"] { + let r = parse(r#"{"tag_name":"v0.2.0","html_url":"https://github.com/storytold/pdfcraft/releases/tag/v0.2.0"}"#).unwrap(); + assert_eq!(r, Release { version: "v0.2.0".into(), url: "https://github.com/storytold/pdfcraft/releases/tag/v0.2.0".into() }); + for elsewhere in ["https://example.com/pdfcraft.exe", "https://github.com/storytold/pdfcraft/releases.evil/x", "javascript:alert(1)"] { let r = parse(&format!(r#"{{"tag_name":"v9.9.9","html_url":"{elsewhere}"}}"#)).unwrap(); assert_eq!(r.url, RELEASES_PAGE, "{elsewhere}"); } @@ -61,12 +61,12 @@ mod tests { assert!(parse("").is_err()); } - /// Live: asks GitHub over TLS with the OS's roots (`cargo test -p printcraft -- --ignored`). + /// Live: asks GitHub over TLS with the OS's roots (`cargo test -p pdfcraft -- --ignored`). #[test] #[ignore = "needs network access"] fn github_answers_with_the_latest_release() { let r = latest_release().unwrap(); - assert!(printcraft_ui_egui::updates::is_newer(&r.version, "0.0.0"), "{r:?}"); + assert!(pdfcraft_ui_egui::updates::is_newer(&r.version, "0.0.0"), "{r:?}"); assert!(r.url.starts_with(RELEASES_PAGE), "{r:?}"); } } diff --git a/apps/printcraft-cli/Cargo.toml b/apps/printcraft-cli/Cargo.toml deleted file mode 100644 index 371c1328..00000000 --- a/apps/printcraft-cli/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "printcraft-cli" -description = "PrintCraft command-line tool: inspect, render and robustness-check PDFs" -version.workspace = true -edition.workspace = true -license.workspace = true - -[[bin]] -name = "printcraft-cli" -path = "src/main.rs" - -[lints] -workspace = true - -[dependencies] -printcraft-render = { workspace = true } -serde_json = { workspace = true } -printcraft-engine = { workspace = true } -printcraft-cos = { workspace = true } -printcraft-automation = { workspace = true } -base64 = "0.22" - -[features] -default = ["mcp"] -# The opt-in MCP server (`printcraft-cli mcp`). Build with --no-default-features to leave it out. -mcp = ["printcraft-automation/mcp"] diff --git a/apps/printcraft/build.rs b/apps/printcraft/build.rs deleted file mode 100644 index 778cffc1..00000000 --- a/apps/printcraft/build.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Windows only: embed the app icon and version info (VERSIONINFO) into `printcraft.exe`, so it -//! shows in Explorer, the taskbar, the Start menu and Alt-Tab. -//! -//! On every other target this does nothing. A missing resource compiler is a warning, so a -//! cross-compile from macOS or Linux still links, unless `PRINTCRAFT_REQUIRE_WINRES=1` turns it -//! into an error (for release builds). - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=../../assets/app-icon/printcraft.ico"); - println!("cargo:rerun-if-env-changed=PRINTCRAFT_REQUIRE_WINRES"); - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { - return; - } - let mut res = winresource::WindowsResource::new(); - res.set_icon("../../assets/app-icon/printcraft.ico") - .set("ProductName", "PrintCraft") - .set("FileDescription", "PrintCraft PDF workbench") - .set("LegalCopyright", "Copyright (c) the PrintCraft contributors. MIT OR Apache-2.0.") - .set("OriginalFilename", "printcraft.exe") - .set("InternalName", "printcraft"); - if let Err(e) = res.compile() { - if std::env::var_os("PRINTCRAFT_REQUIRE_WINRES").is_some() { - println!("cargo::error=embedding Windows resources failed: {e}"); - return; - } - println!("cargo:warning=printcraft.exe built without icon/version resources: {e}"); - } -} diff --git a/assets/app-icon/LICENSE.txt b/assets/app-icon/LICENSE.txt index d472892a..1480a31c 100644 --- a/assets/app-icon/LICENSE.txt +++ b/assets/app-icon/LICENSE.txt @@ -1,8 +1,8 @@ -PrintCraft app icon (printcraft.svg, printcraft-small.svg and every file derived from them in this -directory: printcraft-1024.png, printcraft.icns, printcraft.ico, hicolor/). +PdfCraft app icon (pdfcraft.svg, pdfcraft-small.svg and every file derived from them in this +directory: pdfcraft-1024.png, pdfcraft.icns, pdfcraft.ico, hicolor/). Original artwork made for this project by the project owner, drawn in ArtCraft and vectorised for -PrintCraft (no third-party material). Licensed like the rest of PrintCraft, under either of +PdfCraft (no third-party material). Licensed like the rest of PdfCraft, under either of * Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) * MIT license (https://opensource.org/licenses/MIT) diff --git a/assets/app-icon/README.md b/assets/app-icon/README.md index 208effe0..64eb9a30 100644 --- a/assets/app-icon/README.md +++ b/assets/app-icon/README.md @@ -1,6 +1,6 @@ -# PrintCraft app icon +# PdfCraft app icon -PrintCraft app icon: an engraved lion's head on green +PdfCraft app icon: an engraved lion's head on green **Creature:** a lion, in a frontal head-and-shoulders portrait, mane running off the bottom of the tile. @@ -13,7 +13,7 @@ a full-bleed colour field, no frame or roundel, the animal looking at the viewer |---|---|---| | Ink | `#0b0b0c` | line work and the figure's contour | | Paper | `#efe9dc` | the figure (the lion's silhouette) | -| PrintCraft green (app colour) | `#12a58a` | the full-bleed field | +| PdfCraft green (app colour) | `#12a58a` | the full-bleed field | **Tile:** `viewBox="0 0 512 512"`, a rounded square with `rx=112` that clips everything. Windows and Linux icons use the full-bleed tile. macOS icons put it on Apple's grid (an 824 px body centred on a transparent @@ -21,23 +21,23 @@ icons use the full-bleed tile. macOS icons put it on Apple's grid (an 824 px bod **Provenance:** the project owner's original drawing, made in ArtCraft (2880 px, keyed to the palette), then vectorised with craftrules `assets/logo-options/_tools/vectorize_tile.py` (potrace; no filtering or -warping). The source drawing is kept in craftrules at `assets/app-icons/printcraft/source.png`, not here. +warping). The source drawing is kept in craftrules at `assets/app-icons/pdfcraft/source.png`, not here. Licence: [LICENSE.txt](LICENSE.txt) (`MIT OR Apache-2.0`, like the repo). ## Files | File | What it is | |---|---| -| `printcraft.svg` | the master vector (traced at 2048 px); every PNG, `.ico` and `.icns` is rendered from it | -| `printcraft-small.svg` | a lighter vector (traced at 1024 px) for places where size matters, such as this README | -| `printcraft-1024.png` | 1024 px on Apple's grid; also the runtime Dock icon on macOS | -| `printcraft.icns` | macOS icon (16–1024 px) | -| `printcraft.ico` | Windows icon (16–256 px), embedded in `printcraft.exe` by `apps/printcraft/build.rs` | -| `hicolor/x/apps/ai.storyteller.printcraft.png` | Linux hicolor theme, 16–512 px; the 256 px one is the runtime icon on Windows and Linux | -| `hicolor/scalable/apps/ai.storyteller.printcraft.svg` | Linux scalable icon (copy of the master) | +| `pdfcraft.svg` | the master vector (traced at 2048 px); every PNG, `.ico` and `.icns` is rendered from it | +| `pdfcraft-small.svg` | a lighter vector (traced at 1024 px) for places where size matters, such as this README | +| `pdfcraft-1024.png` | 1024 px on Apple's grid; also the runtime Dock icon on macOS | +| `pdfcraft.icns` | macOS icon (16–1024 px) | +| `pdfcraft.ico` | Windows icon (16–256 px), embedded in `pdfcraft.exe` by `apps/pdfcraft/build.rs` | +| `hicolor/x/apps/ai.storyteller.pdfcraft.png` | Linux hicolor theme, 16–512 px; the 256 px one is the runtime icon on Windows and Linux | +| `hicolor/scalable/apps/ai.storyteller.pdfcraft.svg` | Linux scalable icon (copy of the master) | -Where it shows: `apps/printcraft/src/main.rs` sets the window icon (Dock, taskbar, Alt-Tab, launcher) and the -Wayland app id `ai.storyteller.printcraft`; `packaging/linux/ai.storyteller.printcraft.desktop` names the +Where it shows: `apps/pdfcraft/src/main.rs` sets the window icon (Dock, taskbar, Alt-Tab, launcher) and the +Wayland app id `ai.storyteller.pdfcraft`; `packaging/linux/ai.storyteller.pdfcraft.desktop` names the hicolor icon. ## Regenerate diff --git a/assets/app-icon/hicolor/128x128/apps/ai.storyteller.printcraft.png b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.pdfcraft.png similarity index 100% rename from assets/app-icon/hicolor/128x128/apps/ai.storyteller.printcraft.png rename to assets/app-icon/hicolor/128x128/apps/ai.storyteller.pdfcraft.png diff --git a/assets/app-icon/hicolor/16x16/apps/ai.storyteller.printcraft.png b/assets/app-icon/hicolor/16x16/apps/ai.storyteller.pdfcraft.png similarity index 100% rename from assets/app-icon/hicolor/16x16/apps/ai.storyteller.printcraft.png rename to assets/app-icon/hicolor/16x16/apps/ai.storyteller.pdfcraft.png diff --git a/assets/app-icon/hicolor/24x24/apps/ai.storyteller.printcraft.png b/assets/app-icon/hicolor/24x24/apps/ai.storyteller.pdfcraft.png similarity index 100% rename from assets/app-icon/hicolor/24x24/apps/ai.storyteller.printcraft.png rename to assets/app-icon/hicolor/24x24/apps/ai.storyteller.pdfcraft.png diff --git a/assets/app-icon/hicolor/256x256/apps/ai.storyteller.printcraft.png b/assets/app-icon/hicolor/256x256/apps/ai.storyteller.pdfcraft.png similarity index 100% rename from assets/app-icon/hicolor/256x256/apps/ai.storyteller.printcraft.png rename to assets/app-icon/hicolor/256x256/apps/ai.storyteller.pdfcraft.png diff --git a/assets/app-icon/hicolor/32x32/apps/ai.storyteller.printcraft.png b/assets/app-icon/hicolor/32x32/apps/ai.storyteller.pdfcraft.png similarity index 100% rename from assets/app-icon/hicolor/32x32/apps/ai.storyteller.printcraft.png rename to assets/app-icon/hicolor/32x32/apps/ai.storyteller.pdfcraft.png diff --git a/assets/app-icon/hicolor/48x48/apps/ai.storyteller.printcraft.png b/assets/app-icon/hicolor/48x48/apps/ai.storyteller.pdfcraft.png similarity index 100% rename from assets/app-icon/hicolor/48x48/apps/ai.storyteller.printcraft.png rename to assets/app-icon/hicolor/48x48/apps/ai.storyteller.pdfcraft.png diff --git a/assets/app-icon/hicolor/512x512/apps/ai.storyteller.printcraft.png b/assets/app-icon/hicolor/512x512/apps/ai.storyteller.pdfcraft.png similarity index 100% rename from assets/app-icon/hicolor/512x512/apps/ai.storyteller.printcraft.png rename to assets/app-icon/hicolor/512x512/apps/ai.storyteller.pdfcraft.png diff --git a/assets/app-icon/hicolor/64x64/apps/ai.storyteller.printcraft.png b/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png similarity index 100% rename from assets/app-icon/hicolor/64x64/apps/ai.storyteller.printcraft.png rename to assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png diff --git a/assets/app-icon/hicolor/scalable/apps/ai.storyteller.printcraft.svg b/assets/app-icon/hicolor/scalable/apps/ai.storyteller.pdfcraft.svg similarity index 100% rename from assets/app-icon/hicolor/scalable/apps/ai.storyteller.printcraft.svg rename to assets/app-icon/hicolor/scalable/apps/ai.storyteller.pdfcraft.svg diff --git a/assets/app-icon/printcraft-1024.png b/assets/app-icon/pdfcraft-1024.png similarity index 100% rename from assets/app-icon/printcraft-1024.png rename to assets/app-icon/pdfcraft-1024.png diff --git a/assets/app-icon/printcraft-small.svg b/assets/app-icon/pdfcraft-small.svg similarity index 100% rename from assets/app-icon/printcraft-small.svg rename to assets/app-icon/pdfcraft-small.svg diff --git a/assets/app-icon/printcraft.icns b/assets/app-icon/pdfcraft.icns similarity index 100% rename from assets/app-icon/printcraft.icns rename to assets/app-icon/pdfcraft.icns diff --git a/assets/app-icon/printcraft.ico b/assets/app-icon/pdfcraft.ico similarity index 100% rename from assets/app-icon/printcraft.ico rename to assets/app-icon/pdfcraft.ico diff --git a/assets/app-icon/printcraft.svg b/assets/app-icon/pdfcraft.svg similarity index 100% rename from assets/app-icon/printcraft.svg rename to assets/app-icon/pdfcraft.svg diff --git a/assets/demo/README.md b/assets/demo/README.md index f68a9480..30fda8ab 100644 --- a/assets/demo/README.md +++ b/assets/demo/README.md @@ -1,15 +1,15 @@ -# PrintCraft showcase PDF +# PdfCraft showcase PDF `showcase.html` is the source of a 13-page, US Letter test document for viewing and rendering. The PDF is built on your machine and is not committed: ```sh -cargo xtask demo-pdf # writes dist/demo/printcraft-showcase.pdf +cargo xtask demo-pdf # writes dist/demo/pdfcraft-showcase.pdf cargo xtask demo-pdf --chrome /path/to/chrome --out /tmp/showcase.pdf ``` You need Google Chrome, Chromium or Edge. The tool looks for it in the usual -install locations and on `PATH`, or uses `--chrome` or the `PRINTCRAFT_CHROME` +install locations and on `PATH`, or uses `--chrome` or the `PDFCRAFT_CHROME` environment variable if you set one. ## How it is built @@ -27,7 +27,7 @@ environment variable if you set one. 3. **Post-processing with `lopdf`** (`xtask/src/demo_pdf/`) adds what a browser cannot emit: - **Annotations.** The HTML contains marker links to - `https://mark.printcraft.invalid//`. Chrome turns each one into + `https://mark.pdfcraft.invalid//`. Chrome turns each one into a Link annotation whose `/Rect` follows the layout. The xtask replaces each marker with a real annotation: Highlight, Underline, StrikeOut, Squiggly, Caret, Text with Popup and a threaded reply and review state, @@ -75,4 +75,4 @@ count is not 12, because the post-processing uses fixed page indices. To check the result, look at the output of `pdfinfo`, `pdffonts` and `qpdf --check`, and render the pages with -`pdftoppm -r 60 -png dist/demo/printcraft-showcase.pdf /tmp/showcase`. +`pdftoppm -r 60 -png dist/demo/pdfcraft-showcase.pdf /tmp/showcase`. diff --git a/assets/demo/showcase-data.csv b/assets/demo/showcase-data.csv index 2ae4f4ad..59d52864 100644 --- a/assets/demo/showcase-data.csv +++ b/assets/demo/showcase-data.csv @@ -1,4 +1,4 @@ -# Illustrative sample data behind the charts on page 7 of the PrintCraft showcase. +# Illustrative sample data behind the charts on page 7 of the PdfCraft showcase. # These are made-up numbers for demonstration, not measurements. chart,series,label,value pages_per_second,throughput,Text,189 diff --git a/assets/demo/showcase.html b/assets/demo/showcase.html index 924557ec..267e56a5 100644 --- a/assets/demo/showcase.html +++ b/assets/demo/showcase.html @@ -2,14 +2,14 @@ -PrintCraft Showcase +PdfCraft Showcase @@ -351,7 +351,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;

A tour of the page, glyph by glyph.

Typography, world scripts, vector graphics, mathematics, forms and annotations — gathered in one document to put a PDF engine through its paces.

-
PrintCraft
Open-source PDF toolkit in Rust
+
PdfCraft
Open-source PDF toolkit in Rust
Generated by cargo xtask demo-pdf
US Letter · 8.5 × 11 in
@@ -359,7 +359,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft ShowcaseContents
+
PdfCraft ShowcaseContents
Inside this specimen

Contents

    @@ -378,19 +378,19 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;

    Colophon

    Body text is set in EB Garamond with Playfair Display for display, Inter for labels, and JetBrains Mono for code. Every font is openly licensed (OFL) and listed in ATTRIBUTION.md. Layout is authored in HTML and CSS, printed by headless Chrome, then post-processed with lopdf.

    What to look for

    Open the bookmarks, layers, attachments and comments panels. Page labels run Cover, i–ii, then 1–10. One layer adds a draft watermark; another holds print-only notes and starts hidden.
    -
    PrintCraft Showcasei
    +
    PdfCraft Showcasei
-
PrintCraft ShowcaseForeword
+
PdfCraft ShowcaseForeword
Foreword

Why a specimen?

A PDF reader is judged in the first second. The page appears, and either the letters sit on it the way the author intended or they do not. Every kerning pair, every ligature and every soft gradient is a small promise the file makes and the renderer must keep.

-

This document collects those promises in one place. It was written for the people who build PrintCraft, as a single file to open after every change: if all of it still looks right, the change is probably safe. It is also, we hope, pleasant to read on its own.

+

This document collects those promises in one place. It was written for the people who build PdfCraft, as a single file to open after every change: if all of it still looks right, the change is probably safe. It is also, we hope, pleasant to read on its own.

Nothing here is exotic. Each feature is something ordinary documents use every day — an invoice with a form field, a thesis full of equations, a contract with comments in the margin, a brochure with a translucent photograph laid over a tinted panel. What is unusual is only that they are all together, and that each page says plainly what it is testing.

The layout is authored in HTML and printed by a browser; the interactive parts are then written into the file by hand, object by object. That split mirrors the real world, where most PDFs are produced by one program and amended by several others.

-
The PrintCraft contributors
+
The PdfCraft contributors

Tracking, from tight to airy

@@ -413,12 +413,12 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase1 · Setting Text
+
PdfCraft Showcase1 · Setting Text
Chapter one

Setting Text

Two justified columns with automatic hyphenation, old-style figures, a drop cap and a pull quote — the everyday machinery of a well-made page.

-

Long before there were screens, a page of type was a negotiation between the width of a column and the length of the words that had to fill it. The compositor adjusted spaces, divided a word at a sensible syllable, and occasionally rewrote a line so the paragraph would lie flat. Justified text asks for exactly that care, and a renderer must honour the spacing it was given.1

+

Long before there were screens, a page of type was a negotiation between the width of a column and the length of the words that had to fill it. The compositor adjusted spaces, divided a word at a sensible syllable, and occasionally rewrote a line so the paragraph would lie flat. Justified text asks for exactly that care, and a renderer must honour the spacing it was given.1

A PDF does not reflow. Every glyph has already been placed by the program that wrote the file, and the reader’s job is to reproduce those positions faithfully at any zoom. That sounds simple until one remembers that a single line may mix three fonts, a ligature standing in for two letters, and a hyphen that exists only because the line happened to break there in 1994 or 2026.

Old-style figures, like the ones in this paragraph’s 1,280 characters and 36 lines, rise and fall with the lowercase so that numbers do not shout. Ligatures such as fi, fl and ffi in words like officially, affluent and afflicted join letters that would otherwise collide.2

“Good typography is noticed only when it is missing.”A working maxim of the trade
@@ -437,7 +437,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase2 · OpenType Features
+
PdfCraft Showcase2 · OpenType Features
Chapter two

OpenType Features

Each row shows the same text with a feature off, then on. The glyphs differ; the underlying characters do not.

@@ -476,7 +476,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase3 · Expressive Type
+
PdfCraft Showcase3 · Expressive Type
Chapter three

Expressive Type

Effects that become transparency groups, soft masks, clipping paths and transformed text objects once they reach PDF.

@@ -513,7 +513,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase4 · Scripts of the World
+
PdfCraft Showcase4 · Scripts of the World
Chapter four

Scripts of the World

A short greeting — roughly, “every letter deserves a good home” — in writing systems that need bidirectional layout, contextual shaping, reordering and vertical setting.

@@ -551,7 +551,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase5 · Mathematics
+
PdfCraft Showcase5 · Mathematics
Chapter five

Mathematics

Equations written in MathML Core and laid out by the browser: stretchy delimiters, radicals, large operators, scripts and matrices.

@@ -592,7 +592,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase6 · Vector Graphics
+
PdfCraft Showcase6 · Vector Graphics
Chapter six

Vector Graphics

Nine small plates, each exercising one piece of the PDF imaging model.

@@ -652,7 +652,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase7 · Data & Tables
+
PdfCraft Showcase7 · Data & Tables
Chapter seven

Data & Tables

Charts drawn as SVG and a striped table. All figures are illustrative sample data, not measurements.

@@ -731,7 +731,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase8 · Code & Images
+
PdfCraft Showcase8 · Code & Images
Chapter eight

Code & Images

A monospaced listing with syntax colouring, and the raster that it describes.

@@ -761,7 +761,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;

Links

@@ -771,15 +771,15 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
-
PrintCraft Showcase9 · Review & Markup
+
PdfCraft Showcase9 · Review & Markup
Chapter nine

Review & Markup

Every annotation on this page was added after printing, each with its own appearance stream, author, date and unique name.

Text markup

-

Reviewers rarely rewrite a document; they highlight the phrase that matters, underline a claim that needs a source, and strike out a redundant word before moving on.

-

A wavy line says check the spelling here, while a caret marks where text should  be inserted.

+

Reviewers rarely rewrite a document; they highlight the phrase that matters, underline a claim that needs a source, and strike out a redundant word before moving on.

+

A wavy line says check the spelling here, while a caret marks where text should  be inserted.

Comments

Sticky notes carry threaded replies, and a free-text box writes directly on the page. The paperclip holds an attached file that can be saved from the comment itself.

Legend

@@ -789,16 +789,16 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
Review canvas - . - . - . - . - . - . - . - . - . - . + . + . + . + . + . + . + . + . + . + .
Review & Markup9
diff --git a/crates/a11y/Cargo.toml b/crates/a11y/Cargo.toml index 47a958bd..a5900e54 100644 --- a/crates/a11y/Cargo.toml +++ b/crates/a11y/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-a11y" +name = "pdfcraft-a11y" description = "Accessibility: the Accessibility Checker (32 rules), its report and fixes" version.workspace = true edition.workspace = true @@ -11,7 +11,7 @@ repository.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-model = { workspace = true } -printcraft-content = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-model = { workspace = true } +pdfcraft-content = { workspace = true } thiserror.workspace = true diff --git a/crates/a11y/README.md b/crates/a11y/README.md index be85efd6..f00f429b 100644 --- a/crates/a11y/README.md +++ b/crates/a11y/README.md @@ -1,4 +1,4 @@ -# printcraft-a11y +# pdfcraft-a11y Layer L4: accessibility (execution plan M12.3). Today: the Accessibility Checker's full check. diff --git a/crates/a11y/src/alt.rs b/crates/a11y/src/alt.rs index 653a3a5a..95e272d3 100644 --- a/crates/a11y/src/alt.rs +++ b/crates/a11y/src/alt.rs @@ -3,8 +3,8 @@ use std::collections::{HashMap, HashSet}; -use printcraft_content::Matrix; -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; +use pdfcraft_content::Matrix; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; /// A Figure element (after the role map). #[derive(Clone, Debug, PartialEq)] @@ -24,7 +24,7 @@ pub enum AltError { #[error("object {0} is not a figure")] NotAFigure(u32), #[error("{0}")] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } fn tree_root(doc: &Document) -> Option { @@ -139,7 +139,7 @@ fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet) -> Option<[f64; let mut marks: Vec = Vec::new(); let mut bbox = None; let mut path: Vec<(f64, f64)> = Vec::new(); - for op in printcraft_content::parse(&page_content(doc, page)).ops { + for op in pdfcraft_content::parse(&page_content(doc, page)).ops { let inside = marks.iter().any(|m| *m); let top = ctm.last().copied().unwrap_or(Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])); match op.op.as_slice() { @@ -216,7 +216,7 @@ fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet) -> Option<[f64; pub fn figures(doc: &Document) -> Vec
{ let Some(root) = tree_root(doc) else { return Vec::new() }; let role_map = root.get(b"RoleMap").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).unwrap_or_default(); - let pages = printcraft_model::pages(doc); + let pages = pdfcraft_model::pages(doc); let index: HashMap = pages.iter().enumerate().map(|(i, p)| (p.obj, i)).collect(); elements(doc) .into_iter() @@ -259,21 +259,21 @@ pub fn set_alt(doc: &mut Document, obj: ObjRef, alt: Option<&str>) -> Result<(), pub fn mark_decorative(doc: &mut Document, obj: ObjRef) -> Result<(), AltError> { let d = check_figure(doc, obj)?; let ids: HashSet = mcids(&d).into_iter().collect(); - let pages = printcraft_model::pages(doc); + let pages = pdfcraft_model::pages(doc); let pg = d.get(b"Pg").and_then(Object::as_ref).or_else(|| elements(doc).into_iter().find(|(r, _, _)| *r == obj).and_then(|(_, _, p)| p)); if let Some(page) = pg.and_then(|p| pages.iter().find(|x| x.obj == p)).filter(|_| !ids.is_empty()) { - let mut ops = printcraft_content::parse(&page_content(doc, &page.dict)).ops; + let mut ops = pdfcraft_content::parse(&page_content(doc, &page.dict)).ops; let mut changed = false; for op in ops.iter_mut() { let ours = op.op == b"BDC" && matches!(op.operands.get(1), Some(Object::Dict(m)) if m.get(b"MCID").and_then(Object::as_int).is_some_and(|n| ids.contains(&n))); if ours { - *op = printcraft_content::Op::new("BMC", vec![Object::name("Artifact")]); + *op = pdfcraft_content::Op::new("BMC", vec![Object::name("Artifact")]); changed = true; } } if changed { - let s = doc.add(Object::Stream(Stream::flate(Dict::new(), &printcraft_content::serialize_ops(&ops)))); + let s = doc.add(Object::Stream(Stream::flate(Dict::new(), &pdfcraft_content::serialize_ops(&ops)))); doc.update_dict(page.obj, |p| p.set(b"Contents".to_vec(), Object::Ref(s)))?; // The parent tree no longer points those ids at the figure. if let Some(key) = page.dict.get(b"StructParents").and_then(Object::as_int) { diff --git a/crates/a11y/src/content.rs b/crates/a11y/src/content.rs index 22373e88..2c6a100e 100644 --- a/crates/a11y/src/content.rs +++ b/crates/a11y/src/content.rs @@ -3,8 +3,8 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; -use printcraft_cos::{Dict, Document, ObjRef, Object}; -use printcraft_model::Page; +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; +use pdfcraft_model::Page; use crate::structure::Tree; use crate::{Finding, Rule}; @@ -73,7 +73,7 @@ impl Walk<'_> { // Marked-content stack: whether each level is tagged or an artifact. let mut stack: Vec = Vec::new(); let mut untagged = 0; - for op in printcraft_content::parse(data).ops { + for op in pdfcraft_content::parse(data).ops { let inside = covered || stack.last().copied().unwrap_or(false); match op.op.as_slice() { b"BMC" => stack.push(inside || op.name(0) == Some(b"Artifact")), diff --git a/crates/a11y/src/lib.rs b/crates/a11y/src/lib.rs index 281fa1c5..550b4e6a 100644 --- a/crates/a11y/src/lib.rs +++ b/crates/a11y/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-a11y — accessibility (L4): the Accessibility Checker's full check. +//! pdfcraft-a11y — accessibility (L4): the Accessibility Checker's full check. //! //! [`check`] runs the 32 rules in Acrobat's seven categories (Document, Page Content, Forms, //! Alternate Text, Tables, Lists, Headings) and returns, per rule, Passed, Failed, Needs manual @@ -10,7 +10,7 @@ use std::collections::BTreeSet; -use printcraft_cos::Document; +use pdfcraft_cos::Document; mod alt; mod content; @@ -458,7 +458,7 @@ const MAX_FINDINGS: usize = 200; /// Run the full check. pub fn check(doc: &Document, options: &Options) -> Report { - let pages = printcraft_model::pages(doc); + let pages = pdfcraft_model::pages(doc); let page_list: Vec = match &options.pages { Some(p) => p.iter().copied().filter(|p| *p < pages.len()).collect(), None => (0..pages.len()).collect(), diff --git a/crates/a11y/src/structure.rs b/crates/a11y/src/structure.rs index d46e5fc9..9f978361 100644 --- a/crates/a11y/src/structure.rs +++ b/crates/a11y/src/structure.rs @@ -3,8 +3,8 @@ use std::collections::{HashMap, HashSet}; -use printcraft_cos::{Dict, Document, ObjRef, Object}; -use printcraft_model::Page; +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; +use pdfcraft_model::Page; use crate::{Finding, Rule}; diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index 0131d924..3534eeb1 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use printcraft_cos::Document; +use pdfcraft_cos::Document; use super::*; @@ -265,7 +265,7 @@ fn setting_alt_text_and_marking_decorative() { assert_eq!(r.result(Rule::FiguresAltText).unwrap().status, Status::Passed); set_alt(&mut doc, f[0].obj, None).unwrap(); assert_eq!(figures(&doc)[0].alt, None); - assert_eq!(set_alt(&mut doc, printcraft_cos::ObjRef::new(6, 0), Some("x")), Err(AltError::NotAFigure(6))); + assert_eq!(set_alt(&mut doc, pdfcraft_cos::ObjRef::new(6, 0), Some("x")), Err(AltError::NotAFigure(6))); // Decorative: the figure leaves the tags and its content becomes an artifact. mark_decorative(&mut doc, f[0].obj).unwrap(); let left = figures(&doc); @@ -273,9 +273,9 @@ fn setting_alt_text_and_marking_decorative() { assert_eq!(left[0].alt.as_deref(), Some("A bar")); let r = check(&doc, &Options { rules: [Rule::FiguresAltText, Rule::TaggedContent].into(), pages: None }); assert_eq!(failed(&r), Vec::::new(), "{:?}", r.results); - let pages = printcraft_model::pages(&doc); + let pages = pdfcraft_model::pages(&doc); let c = doc.resolve(pages[0].dict.get(b"Contents").unwrap()); - let printcraft_cos::Object::Stream(c) = &*c else { panic!("one content stream") }; + let pdfcraft_cos::Object::Stream(c) = &*c else { panic!("one content stream") }; let text = String::from_utf8(c.decoded().unwrap()).unwrap(); assert!(text.contains("/Artifact BMC") && !text.contains("/MCID 0"), "{text}"); } diff --git a/crates/annot/Cargo.toml b/crates/annot/Cargo.toml index 36b75bbd..9603e572 100644 --- a/crates/annot/Cargo.toml +++ b/crates/annot/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-annot" +name = "pdfcraft-annot" description = "Annotations (comments): builders, appearance streams and edits, ISO 32000-2 §12.5" version.workspace = true edition.workspace = true @@ -9,8 +9,8 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-fonts = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-fonts = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/annot/README.md b/crates/annot/README.md index 85d83065..b3224b7b 100644 --- a/crates/annot/README.md +++ b/crates/annot/README.md @@ -1,6 +1,6 @@ -# printcraft-annot +# pdfcraft-annot -Comments (annotations), ISO 32000-2 §12.5. Layer L3, depends only on `printcraft-cos`. +Comments (annotations), ISO 32000-2 §12.5. Layer L3, depends only on `pdfcraft-cos`. ## What it does @@ -9,7 +9,7 @@ Comments (annotations), ISO 32000-2 §12.5. Layer L3, depends only on `printcraf line and arrow (`Line` with `/LE`), freehand drawing (`Ink`) and text box (`FreeText`). - **Appearance streams** (`appearance::build`) generated from the annotation dictionary, so new and restyled comments look the same in every viewer. Highlights multiply (`/BM /Multiply`). - Note icons are PrintCraft's own drawings. Text boxes use standard Helvetica (WinAnsi), with + Note icons are PdfCraft's own drawings. Text boxes use standard Helvetica (WinAnsi), with line breaking by approximate Helvetica proportions (no font program is bundled). - **Edits:** reply (`/IRT`), set review status (a `/State` reply, as Acrobat's "Set status" does), change text, move, resize, restyle, delete (with pop-up and replies). @@ -26,7 +26,7 @@ delete_annotation(&mut doc, page, index)?; ``` A comment is addressed by `(page, index in /Annots)`, the same pair -`printcraft_render::Annotation` reports. `Meta` carries the date and the `/NM` id so edits stay +`pdfcraft_render::Annotation` reports. `Meta` carries the date and the `/NM` id so edits stay deterministic; the engine supplies them. ## Fidelity rules diff --git a/crates/annot/src/appearance.rs b/crates/annot/src/appearance.rs index 3397f429..ee51446b 100644 --- a/crates/annot/src/appearance.rs +++ b/crates/annot/src/appearance.rs @@ -1,4 +1,4 @@ -//! Appearance streams (§12.5.5) for the comment types PrintCraft creates, execution plan M5.2. +//! Appearance streams (§12.5.5) for the comment types PdfCraft creates, execution plan M5.2. //! //! [`build`] draws a normal appearance (`/AP /N`) from the annotation dictionary alone, so the //! same code serves new comments and restyled ones. Drawings are in page space with @@ -6,13 +6,13 @@ //! It returns `None` for anything it cannot draw faithfully (cloudy borders, unknown line //! endings, indirect geometry), so callers never replace an appearance with a worse one. //! -//! The note icons are PrintCraft's own drawings (AGENTS.md §1). Text boxes use the standard +//! The note icons are PdfCraft's own drawings (AGENTS.md §1). Text boxes use the standard //! Helvetica font with WinAnsi encoding; line breaking uses [`text_width`], an approximation of //! Helvetica's proportions by character class (no font program or metrics file is bundled). -use printcraft_cos::{Dict, Object, PdfString, Stream}; -pub use printcraft_fonts::{helvetica_width as text_width, wrap}; -use printcraft_fonts::{literal, win_ansi}; +use pdfcraft_cos::{Dict, Object, PdfString, Stream}; +pub use pdfcraft_fonts::{helvetica_width as text_width, wrap}; +use pdfcraft_fonts::{literal, win_ansi}; use crate::{NOTE_SIZE, Rgb, n}; @@ -435,7 +435,7 @@ pub fn build(d: &Dict) -> Option { res.set(b"XObject".to_vec(), Object::Dict(xo)); return Some(form(rect, c.as_bytes(), res)); } - // Only PrintCraft's own Fill & Sign marks are drawn here. + // Only PdfCraft's own Fill & Sign marks are drawn here. let name = d.name(b"Name")?; let col = stroke.unwrap_or([0.0; 3]); let [x0, y0, x1, y1] = rect; @@ -482,7 +482,7 @@ pub fn build(d: &Dict) -> Option { n(y0 + h / 2.0) )), other => { - // Only stamps PrintCraft made: others keep their own artwork. + // Only stamps PdfCraft made: others keep their own artwork. if !matches!(d.get(b"PCStamp"), Some(Object::Bool(true))) { return None; } @@ -727,9 +727,9 @@ fn ellipse(x0: f64, y0: f64, x1: f64, y1: f64) -> String { s } -/// PrintCraft's note icons, drawn in a 20 × 20 box: a speech bubble for `/Comment`, a page +/// PdfCraft's note icons, drawn in a 20 × 20 box: a speech bubble for `/Comment`, a page /// with a folded corner for everything else, both filled with the note colour. -/// File attachment icons in a 20 × 20 box: PrintCraft's own drawings. +/// File attachment icons in a 20 × 20 box: PdfCraft's own drawings. fn attach_icon(name: &str, col: Rgb) -> String { let mut s = format!("{}{}1.2 w 1 j 1 J\n", rg(col), rg_stroke(col)); match name { diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index 0c5350a3..a0f76306 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-annot — comments (annotations), ISO 32000-2 §12.5, execution plan M5.1–M5.3. +//! pdfcraft-annot — comments (annotations), ISO 32000-2 §12.5, execution plan M5.1–M5.3. //! //! Builders for the comment types Acrobat's commenting tools create (sticky note, highlight, //! underline, strikethrough, squiggly, rectangle, oval, line/arrow, freehand ink, text box), @@ -6,15 +6,15 @@ //! change its text, recolour, move, resize and delete. //! //! Addressing: a comment is `(page, index)`, its position in the page's `/Annots` array, which -//! is what `printcraft_render::Annotation::index` reports. Inline annotation dictionaries are +//! is what `pdfcraft_render::Annotation::index` reports. Inline annotation dictionaries are //! promoted to indirect objects when they are edited (replies need a reference to point at). //! -//! Every edit mutates a `printcraft_cos::Document` (copy-on-write); callers snapshot it first +//! Every edit mutates a `pdfcraft_cos::Document` (copy-on-write); callers snapshot it first //! for undo. Keys we do not understand are left alone. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; pub mod appearance; pub mod links; @@ -35,7 +35,7 @@ pub enum AnnotError { #[error("{0} comments can't be restyled yet (their appearance can't be regenerated)")] Unsupported(String), #[error("{0}")] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } pub type Rgb = [f64; 3]; @@ -101,7 +101,7 @@ pub enum FillMark { } impl FillMark { - /// The `/Name` of the stamp PrintCraft draws for it. + /// The `/Name` of the stamp PdfCraft draws for it. pub fn name(self) -> &'static str { match self { FillMark::Check => "PCCheck", @@ -121,7 +121,7 @@ impl FillMark { } } -/// The stamps of Acrobat's stamp palette (drawn in PrintCraft's own style). +/// The stamps of Acrobat's stamp palette (drawn in PdfCraft's own style). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum StampKind { // Standard business. @@ -921,7 +921,7 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R Shape::Stamp { stamp, by, .. } => { d.set(b"C".to_vec(), rgb(style.color)); d.set(b"Name".to_vec(), Object::name(stamp.name())); - // Marks the stamp as drawn by PrintCraft: other stamps with standard names keep + // Marks the stamp as drawn by PdfCraft: other stamps with standard names keep // their own artwork. d.set(b"PCStamp".to_vec(), Object::Bool(true)); if let Some(b) = by { @@ -987,7 +987,7 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R let mut ef = Dict::new(); ef.set(b"Type".to_vec(), Object::name("EmbeddedFile")); ef.set(b"Params".to_vec(), Object::Dict(params)); - let ef = doc.add(Object::Stream(printcraft_cos::Stream::flate(ef, data))); + let ef = doc.add(Object::Stream(pdfcraft_cos::Stream::flate(ef, data))); let mut efd = Dict::new(); efd.set(b"F".to_vec(), Object::Ref(ef)); efd.set(b"UF".to_vec(), Object::Ref(ef)); @@ -1255,7 +1255,7 @@ fn reply( fd.set(b"Type".to_vec(), Object::name("XObject")); fd.set(b"Subtype".to_vec(), Object::name("Form")); fd.set(b"BBox".to_vec(), num_array(&[0.0, 0.0, 0.0, 0.0])); - let ap = doc.add(Object::Stream(printcraft_cos::Stream::from_raw(fd, Vec::new()))); + let ap = doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(fd, Vec::new()))); let mut apd = Dict::new(); apd.set(b"N".to_vec(), Object::Ref(ap)); d.set(b"AP".to_vec(), Object::Dict(apd)); @@ -1460,7 +1460,7 @@ pub fn set_style( // ── reading ───────────────────────────────────────────────────────────────────────────────── /// One comment as the viewer lists it (the same fields and rules as -/// `printcraft_render::Annotation`), read straight from the object graph. The engine uses it to +/// `pdfcraft_render::Annotation`), read straight from the object graph. The engine uses it to /// refresh the comment list after a comment edit without re-inspecting the whole document. #[derive(Clone, Debug, PartialEq)] pub struct Summary { diff --git a/crates/annot/src/links.rs b/crates/annot/src/links.rs index 70ab267f..3d8d6d4d 100644 --- a/crates/annot/src/links.rs +++ b/crates/annot/src/links.rs @@ -1,7 +1,7 @@ //! Links (ISO 32000-2 §12.5.6.5): create, edit and remove link annotations with Acrobat's Link //! Properties (appearance and a go-to-page or open-a-web-page action). -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; use crate::{AnnotError, annots, page_ref, page_refs, set_annots}; @@ -11,7 +11,7 @@ pub enum LinkAction { /// A page of this document (0-based), shown fitting the window (`/Fit`). Page(usize), Uri(String), - /// Something PrintCraft doesn't edit yet (named destination, JavaScript…): kept as is. + /// Something PdfCraft doesn't edit yet (named destination, JavaScript…): kept as is. Other(String), } diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index c1b7929f..afcee4bb 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use printcraft_cos::{Document, ObjRef, Object, SaveOptions, write_incremental}; +use pdfcraft_cos::{Document, ObjRef, Object, SaveOptions, write_incremental}; use super::*; diff --git a/crates/automation/Cargo.toml b/crates/automation/Cargo.toml index 7d59e152..1033ab28 100644 --- a/crates/automation/Cargo.toml +++ b/crates/automation/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "printcraft-automation" -description = "Agent control for PrintCraft: a headless tool set over the engine and an MCP server" +name = "pdfcraft-automation" +description = "Agent control for PdfCraft: a headless tool set over the engine and an MCP server" version.workspace = true edition.workspace = true license.workspace = true @@ -9,9 +9,9 @@ license.workspace = true workspace = true [dependencies] -printcraft-engine = { workspace = true } -printcraft-render = { workspace = true } -printcraft-organize = { workspace = true } +pdfcraft-engine = { workspace = true } +pdfcraft-render = { workspace = true } +pdfcraft-organize = { workspace = true } serde_json = { workspace = true } png = "0.18" base64 = { version = "0.22", optional = true } diff --git a/crates/automation/README.md b/crates/automation/README.md index a536a7f1..f680ae80 100644 --- a/crates/automation/README.md +++ b/crates/automation/README.md @@ -1,9 +1,9 @@ -# printcraft-automation +# pdfcraft-automation -Agent control for PrintCraft: a headless tool table over the engine, and an opt-in MCP server. +Agent control for PdfCraft: a headless tool table over the engine, and an opt-in MCP server. - **Layer:** L7 (architecture §13), but headless. It depends on `engine`, `render` and `organize`, never on a UI toolkit. -- **Status:** over a hundred tools; `printcraft-cli tools` lists the current set with their schemas (the count isn't repeated here, so it can't go stale). They cover inspection, rendering, text, page edits and organizing, page labels, bookmarks, comments and their review, form filling and authoring (fields, properties, scripts, data exchange, detection), redaction and sanitizing, password protection and signatures, adding text, images, links and stamps, optimizing, OCR, printing, export, metadata, undo/redo, save, combine, extract and split. Comment tools take geometry in the same top-left-origin points as everything else, and `comment_add` can mark text by searching for it (`find`). The MCP server also serves the open documents as resources, page images included. The running app's UI is driven separately, through the `ui.*` control channel in `printcraft-ui-egui`. +- **Status:** over a hundred tools; `pdfcraft-cli tools` lists the current set with their schemas (the count isn't repeated here, so it can't go stale). They cover inspection, rendering, text, page edits and organizing, page labels, bookmarks, comments and their review, form filling and authoring (fields, properties, scripts, data exchange, detection), redaction and sanitizing, password protection and signatures, adding text, images, links and stamps, optimizing, OCR, printing, export, metadata, undo/redo, save, combine, extract and split. Comment tools take geometry in the same top-left-origin points as everything else, and `comment_add` can mark text by searching for it (`find`). The MCP server also serves the open documents as resources, page images included. The running app's UI is driven separately, through the `ui.*` control channel in `pdfcraft-ui-egui`. ## API @@ -27,9 +27,9 @@ mcp::McpServer::new(a).serve(stdin, stdout)? // newline-delimited JS ## MCP server -**Opt-in only.** Nothing starts it automatically, and it opens no port. It runs while `printcraft-cli mcp [--root DIR]` runs, normally launched by an agent from its MCP configuration. It stops when stdin closes. The CLI's `mcp` Cargo feature (on by default) compiles it out entirely when disabled. +**Opt-in only.** Nothing starts it automatically, and it opens no port. It runs while `pdfcraft-cli mcp [--root DIR]` runs, normally launched by an agent from its MCP configuration. It stops when stdin closes. The CLI's `mcp` Cargo feature (on by default) compiles it out entirely when disabled. -Implemented: `initialize` (protocol 2025-06-18, 2025-03-26, 2024-11-05), `ping`, `tools/list` (with `readOnlyHint`/`destructiveHint` annotations), `tools/call` (JSON results also returned as `structuredContent`; images as `image/png`), and `resources/list`, `resources/templates/list` and `resources/read`. The resources expose the open documents read-only: `printcraft://doc/{doc}/info` (JSON), `…/text`, `…/page/{page}/text` and `…/page/{page}/image{?dpi}` (PNG, 1–600 dpi). Tool failures come back as `isError: true` results, so the agent can read and recover from them. +Implemented: `initialize` (protocol 2025-06-18, 2025-03-26, 2024-11-05), `ping`, `tools/list` (with `readOnlyHint`/`destructiveHint` annotations), `tools/call` (JSON results also returned as `structuredContent`; images as `image/png`), and `resources/list`, `resources/templates/list` and `resources/read`. The resources expose the open documents read-only: `pdfcraft://doc/{doc}/info` (JSON), `…/text`, `…/page/{page}/text` and `…/page/{page}/image{?dpi}` (PNG, 1–600 dpi). Tool failures come back as `isError: true` results, so the agent can read and recover from them. ## Adding a tool diff --git a/crates/automation/src/a11y.rs b/crates/automation/src/a11y.rs index 383dbf10..2c015623 100644 --- a/crates/automation/src/a11y.rs +++ b/crates/automation/src/a11y.rs @@ -1,6 +1,6 @@ //! Accessibility tools: the full check, the accessibility report, and the automatic fixes. -use printcraft_engine::a11y::{Category, Options, Report, Rule, Status}; +use pdfcraft_engine::a11y::{Category, Options, Report, Rule, Status}; use serde_json::{Value, json}; use crate::{Args, Automation, Result, ToolError, failed, write_atomic}; @@ -84,7 +84,7 @@ impl Automation { let r = doc.accessibility_check(&o).ok_or_else(|| failed("the document can't be read for checking"))?; let path = self.resolve(a.str("path")?, true)?; let (y, m, d) = self.session.today(); - let html = printcraft_engine::a11y::report_html(&r, &doc.name, &format!("{y}-{m:02}-{d:02}")); + let html = pdfcraft_engine::a11y::report_html(&r, &doc.name, &format!("{y}-{m:02}-{d:02}")); write_atomic(&path, html.as_bytes())?; let mut out = report_json(&r); out["path"] = json!(path.to_string_lossy()); @@ -125,9 +125,9 @@ impl Automation { let figure = u32::try_from(a.int("figure")?) .map_err(|_| ToolError::InvalidArgs("figure must be a figure number from accessibility_figures".into()))?; let edit = if a.opt_bool("decorative")?.unwrap_or(false) { - printcraft_engine::Edit::MarkDecorative { figure } + pdfcraft_engine::Edit::MarkDecorative { figure } } else { - printcraft_engine::Edit::SetAltText { figure, alt: a.opt_str("alt")?.map(str::to_owned) } + pdfcraft_engine::Edit::SetAltText { figure, alt: a.opt_str("alt")?.map(str::to_owned) } }; let id = self.doc(a)?.id; self.session.apply(id, edit).map_err(failed)?; @@ -136,13 +136,13 @@ impl Automation { } impl Automation { - fn ocr_settings(&self, a: &Args) -> Result { - let mut settings = printcraft_engine::ocr::OcrSettings::default(); + fn ocr_settings(&self, a: &Args) -> Result { + let mut settings = pdfcraft_engine::ocr::OcrSettings::default(); if let Some(d) = a.opt_num("dpi")? { settings.dpi = d.clamp(72.0, 600.0) as f32; } if let Some(l) = a.opt_str("language")? { - if !printcraft_engine::ocr::LANGUAGES.iter().any(|x| x.0 == l) { + if !pdfcraft_engine::ocr::LANGUAGES.iter().any(|x| x.0 == l) { return Err(ToolError::InvalidArgs(format!("unsupported language {l:?}"))); } settings.language = l.into(); @@ -157,13 +157,13 @@ impl Automation { let settings = self.ocr_settings(a)?; let folder = self.resolve(a.str("folder")?, true)?; std::fs::create_dir_all(&folder).map_err(|e| failed(e.to_string()))?; - let ocr = printcraft_engine::ocr::engine().map_err(failed)?; + let ocr = pdfcraft_engine::ocr::engine().map_err(failed)?; let mut out = Vec::new(); for p in a.strs("paths")? { let name = std::path::Path::new(p).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "document.pdf".into()); let result = self.resolve(p, false).map_err(|e| e.to_string()).and_then(|src| std::fs::read(&src).map_err(|e| e.to_string())).and_then(|bytes| { - printcraft_engine::ocr::recognize_file(&name, std::sync::Arc::new(bytes), None, settings.clone(), &ocr, |_, _| true) + pdfcraft_engine::ocr::recognize_file(&name, std::sync::Arc::new(bytes), None, settings.clone(), &ocr, |_, _| true) }); out.push(match result { Ok(r) => { @@ -194,15 +194,15 @@ impl Automation { } pub(crate) fn ocr_status(&self) -> Result { - use printcraft_engine::ocr; + use pdfcraft_engine::ocr; let dirs: Vec = ocr::Models::search_dirs().iter().map(|d| d.to_string_lossy().into_owned()).collect(); let langs: Vec = ocr::LANGUAGES.iter().map(|(c, n)| json!({ "code": c, "name": n })).collect(); Ok(json!({ "available": ocr::available(), "search_dirs": dirs, "languages": langs })) } } -fn request_json(r: &printcraft_engine::js::Request) -> Value { - use printcraft_engine::js::Request as R; +fn request_json(r: &pdfcraft_engine::js::Request) -> Value { + use pdfcraft_engine::js::Request as R; match r { R::Reset(n) => json!({ "reset": n }), R::Print => json!({ "print": true }), @@ -228,14 +228,14 @@ impl Automation { } pub(crate) fn js_set_document_script(&mut self, a: &Args) -> Result { - let edit = printcraft_engine::Edit::SetDocumentScript { name: a.str("name")?.into(), script: a.opt_str("script")?.map(str::to_string) }; + let edit = pdfcraft_engine::Edit::SetDocumentScript { name: a.str("name")?.into(), script: a.opt_str("script")?.map(str::to_string) }; let id = self.doc(a)?.id; self.session.apply(id, edit).map_err(failed)?; self.js_document_scripts(a) } pub(crate) fn form_set_script(&mut self, a: &Args) -> Result { - let edit = printcraft_engine::Edit::SetFieldScript { + let edit = pdfcraft_engine::Edit::SetFieldScript { name: a.str("field")?.into(), event: a.str("event")?.into(), script: a.opt_str("script")?.map(str::to_string), @@ -252,7 +252,7 @@ impl Automation { let path = self.resolve(p, false)?; files.push((p.to_string(), std::fs::read(&path).map_err(|e| failed(format!("{p}: {e}")))?)); } - let csv = printcraft_engine::merge_data_files(&files).map_err(failed)?; + let csv = pdfcraft_engine::merge_data_files(&files).map_err(failed)?; let target = self.resolve(a.str("path")?, true)?; write_atomic(&target, csv.as_bytes())?; let columns = csv.lines().next().map_or(0, |h| h.split(',').count()); @@ -262,16 +262,16 @@ impl Automation { pub(crate) fn doc_export_office(&self, a: &Args) -> Result { let path = self.resolve(a.str("path")?, true)?; let ext = path.extension().map(|e| e.to_string_lossy().into_owned()).unwrap_or_default(); - let format = printcraft_engine::compare::OfficeFormat::from_extension(&ext) + let format = pdfcraft_engine::compare::OfficeFormat::from_extension(&ext) .ok_or_else(|| ToolError::InvalidArgs(format!("unsupported extension {ext:?} (docx, html or rtf)")))?; let bytes = self.doc(a)?.export_office(format); write_atomic(&path, &bytes)?; Ok(json!({ "path": path.to_string_lossy(), "bytes": bytes.len(), "format": format.extension() })) } - fn pdfa_level(&self, a: &Args) -> Result { + fn pdfa_level(&self, a: &Args) -> Result { let l = a.opt_str("level")?.unwrap_or("2b"); - printcraft_engine::pdfa::Level::from_id(l).ok_or_else(|| ToolError::InvalidArgs(format!("unknown PDF/A level {l:?} (2b or 3b)"))) + pdfcraft_engine::pdfa::Level::from_id(l).ok_or_else(|| ToolError::InvalidArgs(format!("unknown PDF/A level {l:?} (2b or 3b)"))) } pub(crate) fn pdfa_verify(&self, a: &Args) -> Result { @@ -294,12 +294,12 @@ impl Automation { pub(crate) fn pdfa_convert(&mut self, a: &Args) -> Result { let level = self.pdfa_level(a)?; let id = self.doc(a)?.id; - self.session.apply(id, printcraft_engine::Edit::ConvertPdfA { level }).map_err(failed)?; + self.session.apply(id, pdfcraft_engine::Edit::ConvertPdfA { level }).map_err(failed)?; self.pdfa_verify(a) } pub(crate) fn action_list(&self) -> Result { - use printcraft_engine::actions::{Step, builtin}; + use pdfcraft_engine::actions::{Step, builtin}; let step_json = |s: &Step| json!({ "step": s.id(), "arg": s.arg() }); let actions: Vec = builtin() .iter() @@ -310,7 +310,7 @@ impl Automation { } pub(crate) fn action_run(&mut self, a: &Args) -> Result { - use printcraft_engine::actions::{Action, Step, builtin, run_on}; + use pdfcraft_engine::actions::{Action, Step, builtin, run_on}; let action = if let Some(name) = a.opt_str("action")? { builtin() .into_iter() @@ -349,25 +349,25 @@ impl Automation { Ok(json!({ "action": action.name, "files": out })) } - fn compare_ids(&self, a: &Args) -> Result<(printcraft_engine::DocId, printcraft_engine::DocId)> { + fn compare_ids(&self, a: &Args) -> Result<(pdfcraft_engine::DocId, pdfcraft_engine::DocId)> { let new = self.doc(a)?.id; let other = a.int("other")?; let old = u64::try_from(other) .ok() - .and_then(|o| self.session.get(printcraft_engine::DocId(o))) + .and_then(|o| self.session.get(pdfcraft_engine::DocId(o))) .map(|d| d.id) .ok_or_else(|| ToolError::InvalidArgs(format!("no open document {other}")))?; Ok((old, new)) } pub(crate) fn doc_compare(&self, a: &Args) -> Result { - use printcraft_engine::compare::Kind; + use pdfcraft_engine::compare::Kind; let (old, new) = self.compare_ids(a)?; let c = self.session.compare(old, new).map_err(failed)?; let limit = a.opt_int("limit")?.unwrap_or(500).max(1) as usize; let r2 = |r: &[f64; 4]| r.map(|v| (v * 100.0).round() / 100.0); let side = - |s: &printcraft_engine::compare::Side| json!({ "text": s.text, "page": s.page + 1, "rects": s.rects.iter().map(r2).collect::>() }); + |s: &pdfcraft_engine::compare::Side| json!({ "text": s.text, "page": s.page + 1, "rects": s.rects.iter().map(r2).collect::>() }); let list: Vec = c .changes .iter() @@ -413,8 +413,8 @@ impl Automation { .iter() .map(|(p, c)| { let kind = match c.kind { - printcraft_engine::detect::Kind::Text => "text", - printcraft_engine::detect::Kind::CheckBox => "checkbox", + pdfcraft_engine::detect::Kind::Text => "text", + pdfcraft_engine::detect::Kind::CheckBox => "checkbox", }; json!({ "page": p + 1, "kind": kind, "name": c.name, "rect": c.rect.map(|v| (v * 100.0).round() / 100.0) }) }) @@ -426,7 +426,7 @@ impl Automation { } pub(crate) fn form_actions(&self, a: &Args) -> Result { - use printcraft_engine::FieldAction as A; + use pdfcraft_engine::FieldAction as A; let list: Vec = self .doc(a)? .field_actions(a.str("field")?) @@ -451,7 +451,7 @@ impl Automation { } pub(crate) fn form_set_actions(&mut self, a: &Args) -> Result { - use printcraft_engine::{FieldAction as A, FieldTrigger as T}; + use pdfcraft_engine::{FieldAction as A, FieldTrigger as T}; let items = a.get("actions").and_then(Value::as_array).ok_or_else(|| ToolError::InvalidArgs("actions must be an array".into()))?; let bad = |m: String| ToolError::InvalidArgs(m); let names = |v: &Value| -> Vec { @@ -483,10 +483,8 @@ impl Automation { acts.push((t, act)); } let name = a.str("field")?.to_string(); - let edit = printcraft_engine::Edit::SetFieldProps { - name, - props: Box::new(printcraft_engine::FieldProps { actions: Some(acts), ..Default::default() }), - }; + let edit = + pdfcraft_engine::Edit::SetFieldProps { name, props: Box::new(pdfcraft_engine::FieldProps { actions: Some(acts), ..Default::default() }) }; let id = self.doc(a)?.id; self.session.apply(id, edit).map_err(failed)?; self.form_actions(a) diff --git a/crates/automation/src/comments.rs b/crates/automation/src/comments.rs index 80961e76..0d9f5870 100644 --- a/crates/automation/src/comments.rs +++ b/crates/automation/src/comments.rs @@ -3,14 +3,14 @@ //! Geometry follows the automation convention: points from the top-left of the displayed page, //! y down. It is converted to PDF user space (crop box, `/Rotate`) here. -use printcraft_engine::{Edit, Markup, NewAnnotation, NoteIcon, ReviewState, Rgb, Shape, StampGroup, StampKind, Style, SummarySort}; -use printcraft_render::{Annotation, PageInfo}; +use pdfcraft_engine::{Edit, Markup, NewAnnotation, NoteIcon, ReviewState, Rgb, Shape, StampGroup, StampKind, Style, SummarySort}; +use pdfcraft_render::{Annotation, PageInfo}; use serde_json::{Value, json}; use crate::{Args, Automation, Result, ToolError, failed}; /// Author used when a tool call names none. -pub(crate) const DEFAULT_AUTHOR: &str = "PrintCraft"; +pub(crate) const DEFAULT_AUTHOR: &str = "PdfCraft"; pub(crate) fn parse_color(s: &str) -> Result { let named = match s.to_ascii_lowercase().as_str() { @@ -277,9 +277,9 @@ impl Automation { let data = std::fs::read(&path).map_err(|e| failed(format!("{}: {e}", path.display())))?; let file = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); let icon = match a.opt_str("icon")? { - Some(n) => printcraft_engine::AttachIcon::from_name(n) + Some(n) => pdfcraft_engine::AttachIcon::from_name(n) .ok_or_else(|| ToolError::InvalidArgs(format!("unknown icon {n:?} (PushPin, Paperclip, Graph, Tag)")))?, - None => printcraft_engine::AttachIcon::PushPin, + None => pdfcraft_engine::AttachIcon::PushPin, }; Shape::Attachment { at: to_user(&info, x, y), icon, file, data } } @@ -332,7 +332,7 @@ impl Automation { } pub(crate) fn fill_sign_add(&mut self, a: &Args) -> Result { - use printcraft_engine::FillMark; + use pdfcraft_engine::FillMark; let page = self.page(a)?; let info = self.doc(a)?.info.pages[page].clone(); let [x, y] = a.need::<2>("at", "Fill & Sign")?; @@ -340,7 +340,7 @@ impl Automation { let author = a.opt_str("author")?.unwrap_or(DEFAULT_AUTHOR).to_string(); let size = 10.0; let text_at = |t: &str| { - let w = (printcraft_engine::annot_text::text_width(t, size) + 8.0).clamp(20.0, 600.0); + let w = (pdfcraft_engine::annot_text::text_width(t, size) + 8.0).clamp(20.0, 600.0); let h = size * 1.2 + 6.0; Shape::Typewriter { rect: [at[0], at[1] - h, at[0] + w, at[1]], font_size: size } }; @@ -359,7 +359,7 @@ impl Automation { let t = a.str("text")?; let h = if kind == "initials" { 24.0 } else { 32.0 }; let shape = - printcraft_engine::typed_signature_shape(at, t, h).ok_or_else(|| ToolError::InvalidArgs("text has nothing to draw".into()))?; + pdfcraft_engine::typed_signature_shape(at, t, h).ok_or_else(|| ToolError::InvalidArgs("text has nothing to draw".into()))?; (shape, String::new()) } kind => { @@ -470,7 +470,7 @@ impl Automation { page, rect: [ux, uy, ux, uy], name, - file: printcraft_engine::MarkFile { + file: pdfcraft_engine::MarkFile { name: file, bytes: std::sync::Arc::new(bytes), page: a.opt_int("file_page")?.unwrap_or(1).max(1) as usize - 1, diff --git a/crates/automation/src/content.rs b/crates/automation/src/content.rs index 79f29a2f..9a044885 100644 --- a/crates/automation/src/content.rs +++ b/crates/automation/src/content.rs @@ -2,7 +2,7 @@ //! move/resize/retype/restyle it, delete it. Geometry is in points from the top-left of the //! displayed page (y down), like every other tool. -use printcraft_engine::{AddedContent, AddedText, Edit, FontFamily, TextAlign}; +use pdfcraft_engine::{AddedContent, AddedText, Edit, FontFamily, TextAlign}; use serde_json::{Value, json}; use crate::comments::parse_color; diff --git a/crates/automation/src/forms.rs b/crates/automation/src/forms.rs index 6ed1a328..0203934b 100644 --- a/crates/automation/src/forms.rs +++ b/crates/automation/src/forms.rs @@ -1,8 +1,8 @@ //! Form tools: list fields, fill them (several at once, one undo step), clear the form, and //! prepare a form (add, change and delete fields). -use printcraft_engine::form_scripts::{CalcOp, Calculate, Format, Validate}; -use printcraft_engine::{Edit, FieldProps, FieldValue, FormField, FormFieldKind, NewField, field_flags}; +use pdfcraft_engine::form_scripts::{CalcOp, Calculate, Format, Validate}; +use pdfcraft_engine::{Edit, FieldProps, FieldValue, FormField, FormFieldKind, NewField, field_flags}; use serde_json::{Value, json}; use crate::{Args, Automation, Result, ToolError, failed}; @@ -43,8 +43,8 @@ fn format_arg(v: &Value) -> Result { /// `{"border": "#FF0000" | "none", "fill": …, "width": 1-3, "style": "solid"|…, "text_color": …, /// "font": "helvetica"|"times"|"courier"}`, changing only what is given. -fn look_arg(v: &Value, mut l: printcraft_engine::FieldLook) -> Result { - use printcraft_engine::{BorderStyle, FieldFont}; +fn look_arg(v: &Value, mut l: pdfcraft_engine::FieldLook) -> Result { + use pdfcraft_engine::{BorderStyle, FieldFont}; let o = v.as_object().ok_or_else(|| bad("appearance must be an object"))?; let colour = |k: &str| -> Result>> { match o.get(k) { @@ -206,7 +206,7 @@ impl Automation { } if f.actions.format != Format::None { o.insert("format".into(), format_json(&f.actions.format)); - o.insert("display".into(), json!(f.value.first().map(|v| printcraft_engine::form_scripts::format_value(&f.actions.format, v)))); + o.insert("display".into(), json!(f.value.first().map(|v| pdfcraft_engine::form_scripts::format_value(&f.actions.format, v)))); } if let Validate::Range { min, max } = &f.actions.validate { o.insert("validate".into(), json!({ "min": min, "max": max })); @@ -367,7 +367,7 @@ impl Automation { flags: match a.get("flags") { None => Vec::new(), Some(v) => { - use printcraft_engine::field_flags as ff; + use pdfcraft_engine::field_flags as ff; let obj = v.as_object().ok_or_else(|| bad("flags must be an object of booleans"))?; let mut out = Vec::new(); for (k, on) in obj { @@ -397,7 +397,7 @@ impl Automation { check_style: match a.opt_str("check_style")? { None => None, Some(s) => Some( - printcraft_engine::CheckStyle::ALL + pdfcraft_engine::CheckStyle::ALL .into_iter() .find(|c| c.label().eq_ignore_ascii_case(s)) .ok_or_else(|| bad(format!("unknown check style {s:?} (check, circle, cross, diamond, square, star)")))?, @@ -431,10 +431,10 @@ impl Automation { return Ok(out); } let order = match a.str("order")? { - "row" | "rows" => printcraft_engine::TabOrder::Row, - "column" | "columns" => printcraft_engine::TabOrder::Column, - "structure" => printcraft_engine::TabOrder::Structure, - "annotations" | "unspecified" => printcraft_engine::TabOrder::Annotations, + "row" | "rows" => pdfcraft_engine::TabOrder::Row, + "column" | "columns" => pdfcraft_engine::TabOrder::Column, + "structure" => pdfcraft_engine::TabOrder::Structure, + "annotations" | "unspecified" => pdfcraft_engine::TabOrder::Annotations, o => return Err(bad(format!("unknown order {o:?} (row, column, structure, annotations)"))), }; let mut out = self.apply(a, Edit::SetTabOrder { pages, order })?; @@ -463,15 +463,14 @@ impl Automation { let id = self.doc(a)?.id; let path = self.resolve(a.str("path")?, true)?; let ext = path.extension().map(|e| e.to_string_lossy().into_owned()).unwrap_or_default(); - let format = - printcraft_engine::DataFormat::from_extension(&ext).ok_or_else(|| bad("the path must end in .xfdf, .fdf, .xml, .csv or .txt"))?; + let format = pdfcraft_engine::DataFormat::from_extension(&ext).ok_or_else(|| bad("the path must end in .xfdf, .fdf, .xml, .csv or .txt"))?; let (comments, fields) = match a.opt_str("what")?.unwrap_or("all") { "all" => (true, true), "comments" => (true, false), "fields" => (false, true), w => return Err(bad(format!("unknown what {w:?} (all, comments, fields)"))), }; - if comments && !matches!(format, printcraft_engine::DataFormat::Xfdf | printcraft_engine::DataFormat::Fdf) { + if comments && !matches!(format, pdfcraft_engine::DataFormat::Xfdf | pdfcraft_engine::DataFormat::Fdf) { return Err(bad("comments travel as .xfdf or .fdf; .xml, .csv and .txt hold form data (use what: fields)")); } let bytes = self.session.export_data(id, format, comments, fields).map_err(failed)?; diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index bf16225d..9adde658 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -1,10 +1,10 @@ -//! printcraft-automation — agent control for PrintCraft (architecture §13). +//! pdfcraft-automation — agent control for PdfCraft (architecture §13). //! //! - **Layer:** L7. Headless: depends on the engine, never on a UI toolkit. //! - [`Automation`] is a tool set over an engine [`Session`]: open, inspect, render, extract and //! find text, edit pages and metadata, undo/redo, save, combine, extract and split. Every tool //! has a JSON Schema ([`tools`]) and takes and returns JSON, so the same table drives the MCP -//! server ([`mcp`]), `printcraft-cli run` and (later) the UI control channel. +//! server ([`mcp`]), `pdfcraft-cli run` and (later) the UI control channel. //! - Pages are **1-based** in every tool, as people number them. Rectangles are in PDF points //! with the origin at the top-left of the displayed page. //! - An optional root directory confines every path a tool reads or writes. @@ -27,8 +27,8 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; -use printcraft_engine::{DocId, Document, Edit, Session, commands}; -use printcraft_render::{PageRenderer, PageText, RenderConfig, RenderRequest, RequestKind}; +use pdfcraft_engine::{DocId, Document, Edit, Session, commands}; +use pdfcraft_render::{PageRenderer, PageText, RenderConfig, RenderRequest, RequestKind}; use serde_json::{Value, json}; pub use tools::{ToolDef, tools}; @@ -77,7 +77,7 @@ const MAX_DPI: f64 = 600.0; /// Page texts of one document version: (the working bytes, one slot per page). type TextCache = (Arc>, Vec>>); -/// A headless PrintCraft session driven by tool calls. +/// A headless PdfCraft session driven by tool calls. pub struct Automation { session: Session, root: Option, @@ -148,18 +148,18 @@ impl Automation { None => (0..doc.info.pages.len()).collect(), }; let parity = match a.opt_str("subset")?.unwrap_or("all") { - "all" => printcraft_engine::PageParity::Both, - "even" => printcraft_engine::PageParity::Even, - "odd" => printcraft_engine::PageParity::Odd, + "all" => pdfcraft_engine::PageParity::Both, + "even" => pdfcraft_engine::PageParity::Even, + "odd" => pdfcraft_engine::PageParity::Odd, s => return Err(ToolError::InvalidArgs(format!("unknown subset {s:?} (all, even, odd)"))), }; let orientation = match a.opt_str("orientation")?.unwrap_or("all") { - "all" => printcraft_engine::PageOrientation::Both, - "landscape" => printcraft_engine::PageOrientation::Landscape, - "portrait" => printcraft_engine::PageOrientation::Portrait, + "all" => pdfcraft_engine::PageOrientation::Both, + "landscape" => pdfcraft_engine::PageOrientation::Landscape, + "portrait" => pdfcraft_engine::PageOrientation::Portrait, o => return Err(ToolError::InvalidArgs(format!("unknown orientation {o:?} (all, landscape, portrait)"))), }; - let pages = printcraft_engine::filter_pages(&self.doc(&a)?.info, &base, parity, orientation); + let pages = pdfcraft_engine::filter_pages(&self.doc(&a)?.info, &base, parity, orientation); if pages.is_empty() { return Err(failed("no pages match the filters")); } @@ -194,7 +194,7 @@ impl Automation { } "command_list" => self.command_list(&a)?, "page_number" => { - use printcraft_organize::LabelStyle as L; + use pdfcraft_organize::LabelStyle as L; let n = self.doc(&a)?.info.pages.len(); let (from, to) = (a.int("from")?, a.int("to")?); if from < 1 || to < from || to as usize > n { @@ -332,7 +332,7 @@ impl Automation { write_atomic(&path, &bytes)?; json!({ "path": path.to_string_lossy(), "format": ext, "bytes": bytes.len() }) } else { - use printcraft_engine::ImageEdit; + use pdfcraft_engine::ImageEdit; let change = match a.str("action")? { "move" => { let r: Vec = @@ -392,7 +392,7 @@ impl Automation { } let block = self.doc(&a)?.text_blocks(page)[k as usize - 1].clone(); let text = a.opt_str("text")?.map(str::to_owned).unwrap_or(block.text); - let mut style = printcraft_engine::BlockStyle { + let mut style = pdfcraft_engine::BlockStyle { size: a.opt_num("size")?, underline: a.opt_bool("underline")?, line_spacing: a.opt_num("line_spacing")?, @@ -407,9 +407,9 @@ impl Automation { } if let Some(f) = a.opt_str("font")? { let family = match f { - "helvetica" => printcraft_engine::FontFamily::Helvetica, - "times" => printcraft_engine::FontFamily::Times, - "courier" => printcraft_engine::FontFamily::Courier, + "helvetica" => pdfcraft_engine::FontFamily::Helvetica, + "times" => pdfcraft_engine::FontFamily::Times, + "courier" => pdfcraft_engine::FontFamily::Courier, other => return Err(ToolError::InvalidArgs(format!("unknown font {other:?} (helvetica, times, courier)"))), }; style.family = Some((family, a.opt_bool("bold")?.unwrap_or(false), a.opt_bool("italic")?.unwrap_or(false))); @@ -419,10 +419,10 @@ impl Automation { } if let Some(al) = a.opt_str("align")? { style.align = Some(match al { - "left" => printcraft_engine::TextAlign::Left, - "center" => printcraft_engine::TextAlign::Center, - "right" => printcraft_engine::TextAlign::Right, - "justify" => printcraft_engine::TextAlign::Justify, + "left" => pdfcraft_engine::TextAlign::Left, + "center" => pdfcraft_engine::TextAlign::Center, + "right" => pdfcraft_engine::TextAlign::Right, + "justify" => pdfcraft_engine::TextAlign::Justify, other => return Err(ToolError::InvalidArgs(format!("unknown align {other:?}"))), }); } @@ -537,10 +537,10 @@ impl Automation { "sign_document" => self.sign_document(&a)?, "sign_keychain_ids" => { #[cfg(target_os = "macos")] - let ids: Vec = printcraft_engine::sign::keychain::identities(None) + let ids: Vec = pdfcraft_engine::sign::keychain::identities(None) .map_err(failed)? .iter() - .map(|id| json!({ "id": printcraft_engine::sign::keychain::reference(&id.certificate), "certificate": signing::cert_json(&id.certificate) })) + .map(|id| json!({ "id": pdfcraft_engine::sign::keychain::reference(&id.certificate), "certificate": signing::cert_json(&id.certificate) })) .collect(); #[cfg(not(target_os = "macos"))] let ids: Vec = Vec::new(); @@ -623,7 +623,7 @@ impl Automation { } fn doc_initial_view(&mut self, a: &Args) -> Result { - use printcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N}; + use pdfcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N}; let bad = |m: String| ToolError::InvalidArgs(m); let mut v = self.doc(a)?.initial_view(); let before = v.clone(); @@ -711,7 +711,7 @@ impl Automation { } fn doc_optimize(&mut self, a: &Args) -> Result { - use printcraft_engine::optimize::{Compression, ImageSettings, Settings}; + use pdfcraft_engine::optimize::{Compression, ImageSettings, Settings}; let id = self.doc(a)?.id; let before = self.doc(a)?.bytes.len(); let path = self.resolve(a.str("path")?, true)?; @@ -755,7 +755,7 @@ impl Automation { *flag = b; } } - let discard: Vec = match a.get("discard") { + let discard: Vec = match a.get("discard") { None => Vec::new(), Some(v) => v .as_array() @@ -763,7 +763,7 @@ impl Automation { .iter() .map(|x| { x.as_str() - .and_then(printcraft_engine::Hidden::from_id) + .and_then(pdfcraft_engine::Hidden::from_id) .ok_or_else(|| ToolError::InvalidArgs(format!("unknown discard category {x}"))) }) .collect::>()?, @@ -793,12 +793,12 @@ impl Automation { } /// A watermark or background picture: `file` (an image or a PDF) and `file_page` (1-based). - fn mark_file(&self, a: &Args) -> Result> { + fn mark_file(&self, a: &Args) -> Result> { let Some(p) = a.opt_str("file")? else { return Ok(None) }; let path = self.resolve(p, false)?; let bytes = std::fs::read(&path).map_err(|e| failed(format!("{}: {e}", path.display())))?; let page = a.opt_int("file_page")?.unwrap_or(1).max(1) as usize - 1; - Ok(Some(printcraft_engine::MarkFile { + Ok(Some(pdfcraft_engine::MarkFile { name: path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(), bytes: Arc::new(bytes), page, @@ -812,7 +812,7 @@ impl Automation { } fn marks(&mut self, tool: &str, a: &Args) -> Result { - use printcraft_engine::{Background, HeaderFooter, MarkKind, Watermark}; + use pdfcraft_engine::{Background, HeaderFooter, MarkKind, Watermark}; let n = self.doc(a)?.info.pages.len(); let pages = match a.opt_ints("pages")? { Some(_) => self.pages(a, "pages")?, @@ -891,7 +891,7 @@ impl Automation { None => (0..doc.info.pages.len()).collect(), }; let stem = doc.name.trim_end_matches(".pdf").trim_end_matches(".PDF").to_string(); - let mut ex = printcraft_engine::export::Exporter::new(doc); + let mut ex = pdfcraft_engine::export::Exporter::new(doc); if tool == "doc_export_text" { let path = self.resolve(a.str("path")?, true)?; let text = ex.text_of(&pages).map_err(failed)?; @@ -902,10 +902,10 @@ impl Automation { std::fs::create_dir_all(&folder).map_err(|e| failed(format!("{}: {e}", folder.display())))?; if tool == "doc_export_all_images" { let min = u32::try_from(a.opt_int("min_size")?.unwrap_or(0).max(0)).unwrap_or(u32::MAX); - let out = printcraft_engine::export::extract_images(&doc.export_source(), &pages, min).map_err(failed)?; + let out = pdfcraft_engine::export::extract_images(&doc.export_source(), &pages, min).map_err(failed)?; let mut files = Vec::new(); for (k, img) in out.images.iter().enumerate() { - let path = folder.join(printcraft_engine::export::image_file_name(&stem, img, k + 1)); + let path = folder.join(pdfcraft_engine::export::image_file_name(&stem, img, k + 1)); write_atomic(&path, &img.data)?; files.push(json!({ "path": path.to_string_lossy(), "page": img.page + 1, "width": img.width, "height": img.height })); } @@ -915,9 +915,9 @@ impl Automation { let dpi = a.opt_num("dpi")?.unwrap_or(150.0); let quality = a.opt_int("quality")?.unwrap_or(85).clamp(1, 100) as u8; let format = match a.opt_str("format")?.unwrap_or("png") { - "png" => printcraft_engine::export::ImageFormat::Png, - "jpeg" | "jpg" => printcraft_engine::export::ImageFormat::Jpeg { quality }, - "tiff" | "tif" => printcraft_engine::export::ImageFormat::Tiff, + "png" => pdfcraft_engine::export::ImageFormat::Png, + "jpeg" | "jpg" => pdfcraft_engine::export::ImageFormat::Jpeg { quality }, + "tiff" | "tif" => pdfcraft_engine::export::ImageFormat::Tiff, f => return Err(ToolError::InvalidArgs(format!("unknown format {f:?} (png, jpeg, tiff)"))), }; let mut files = Vec::new(); @@ -971,7 +971,7 @@ impl Automation { } fn page_set_box(&mut self, a: &Args) -> Result { - use printcraft_engine::{BoxSpec, PageBox}; + use pdfcraft_engine::{BoxSpec, PageBox}; let doc = self.doc(a)?; let n = doc.info.pages.len(); let pages = match a.opt_ints("pages")? { @@ -1010,7 +1010,7 @@ impl Automation { } fn doc_protect(&mut self, a: &Args) -> Result { - use printcraft_engine::{Algorithm, Changes, Printing, Protection}; + use pdfcraft_engine::{Algorithm, Changes, Printing, Protection}; let d = Protection::default(); let p = Protection { open_password: a.opt_str("open_password")?.map(str::to_owned), @@ -1155,14 +1155,14 @@ impl Automation { self.session.split_by_size(id, (mb * 1_048_576.0) as usize).map_err(failed)? } else { let by = match (a.opt_int("every")?, a.opt_ints("before")?) { - (Some(n), None) if n > 0 => printcraft_organize::SplitBy::PageCount(n as usize), - (None, Some(b)) => printcraft_organize::SplitBy::Before(one_based(&b)?), + (Some(n), None) if n > 0 => pdfcraft_organize::SplitBy::PageCount(n as usize), + (None, Some(b)) => pdfcraft_organize::SplitBy::Before(one_based(&b)?), _ if bookmarks => { titles = self.session.bookmark_splits(id); if titles.is_empty() { return Err(failed("the document has no top-level bookmarks")); } - printcraft_organize::SplitBy::Before(titles.iter().map(|t| t.0).collect()) + pdfcraft_organize::SplitBy::Before(titles.iter().map(|t| t.0).collect()) } _ => return Err(ToolError::InvalidArgs("every must be a positive page count".into())), }; @@ -1414,7 +1414,7 @@ impl Args<'_> { } /// The bookmark tree as JSON, with 1-based paths and pages. -fn bookmark_tree(items: &[printcraft_render::OutlineItem], parent: &[usize]) -> Vec { +fn bookmark_tree(items: &[pdfcraft_render::OutlineItem], parent: &[usize]) -> Vec { items .iter() .enumerate() @@ -1483,7 +1483,7 @@ fn info(d: &Document) -> Value { "tagged": i.tagged, "has_javascript": i.has_javascript, // XFA forms aren't read yet: "static" (fields work, XFA data ignored) or "dynamic" (placeholder pages). - "xfa": i.xfa.map(|x| match x { printcraft_render::Xfa::Static => "static", printcraft_render::Xfa::Dynamic => "dynamic" }), + "xfa": i.xfa.map(|x| match x { pdfcraft_render::Xfa::Static => "static", pdfcraft_render::Xfa::Dynamic => "dynamic" }), "security": security, "pages": i.pages.iter().enumerate().map(|(n, p)| json!({ "page": n + 1, "label": p.label, "width": p.width, "height": p.height, "rotation": p.rotation, @@ -1500,9 +1500,9 @@ fn info(d: &Document) -> Value { "links": i.links.iter().map(|l| json!({ "page": page1(l.page), "rect": l.rect, "target": match &l.target { - printcraft_render::LinkTarget::Page(p) => json!({ "page": page1(*p) }), - printcraft_render::LinkTarget::Uri(u) => json!({ "uri": u }), - printcraft_render::LinkTarget::Other(o) => json!({ "other": o }), + pdfcraft_render::LinkTarget::Page(p) => json!({ "page": page1(*p) }), + pdfcraft_render::LinkTarget::Uri(u) => json!({ "uri": u }), + pdfcraft_render::LinkTarget::Other(o) => json!({ "other": o }), }, })).collect::>(), "layers": i.layers.iter().map(|l| json!({ "name": l.name, "visible": l.visible })).collect::>(), @@ -1515,7 +1515,7 @@ fn info(d: &Document) -> Value { }) } -fn outline(items: &[printcraft_render::OutlineItem]) -> Value { +fn outline(items: &[pdfcraft_render::OutlineItem]) -> Value { Value::Array(items.iter().map(|o| json!({ "title": o.title, "page": o.page.map(|p| p + 1), "children": outline(&o.children) })).collect()) } @@ -1582,7 +1582,7 @@ fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> { let dir = path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new(".")); let name = path.file_name().ok_or_else(|| failed(format!("{}: not a file path", path.display())))?; std::fs::create_dir_all(dir).map_err(|e| failed(format!("{}: {e}", dir.display())))?; - let tmp = dir.join(format!(".{}.printcraft-tmp", name.to_string_lossy())); + let tmp = dir.join(format!(".{}.pdfcraft-tmp", name.to_string_lossy())); std::fs::write(&tmp, bytes).map_err(|e| failed(format!("{}: {e}", tmp.display())))?; std::fs::rename(&tmp, path).map_err(|e| { let _ = std::fs::remove_file(&tmp); diff --git a/crates/automation/src/links.rs b/crates/automation/src/links.rs index 17fdee94..381f7a04 100644 --- a/crates/automation/src/links.rs +++ b/crates/automation/src/links.rs @@ -1,7 +1,7 @@ //! Link tools: list, add, edit and delete links; create links from URLs in the text; remove //! all links. Rectangles are points from the top-left of the displayed page. -use printcraft_engine::{Edit, LinkAction, LinkHighlight, LinkStyle}; +use pdfcraft_engine::{Edit, LinkAction, LinkHighlight, LinkStyle}; use serde_json::{Value, json}; use crate::comments::parse_color; diff --git a/crates/automation/src/mcp.rs b/crates/automation/src/mcp.rs index bfa33544..e746cf53 100644 --- a/crates/automation/src/mcp.rs +++ b/crates/automation/src/mcp.rs @@ -1,13 +1,13 @@ //! A Model Context Protocol server over the automation tools. //! -//! **Opt-in only.** Nothing in PrintCraft starts this server on its own: it runs when a user -//! launches `printcraft-cli mcp` (usually by adding that command to their agent's MCP +//! **Opt-in only.** Nothing in PdfCraft starts this server on its own: it runs when a user +//! launches `pdfcraft-cli mcp` (usually by adding that command to their agent's MCP //! configuration), and stops when its input closes. It opens no network port; the transport is //! newline-delimited JSON-RPC 2.0 over stdin/stdout. //! //! Implemented: `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`, //! `resources/templates/list`, `resources/read`, and the `notifications/*` the client sends. -//! Resources expose the open documents read-only: `printcraft://doc/{doc}/info` (JSON), +//! Resources expose the open documents read-only: `pdfcraft://doc/{doc}/info` (JSON), //! `…/text` (plain text), `…/page/{page}/text` and `…/page/{page}/image` (PNG; `?dpi=` 1–600). Tool failures are reported in the result (`isError: true`) so the agent can read //! them; protocol errors use JSON-RPC error codes. @@ -26,10 +26,10 @@ const INVALID_REQUEST: i64 = -32600; const METHOD_NOT_FOUND: i64 = -32601; const INVALID_PARAMS: i64 = -32602; -const INSTRUCTIONS: &str = "PrintCraft edits PDFs. Open a file with doc_open to get a document id, then inspect \ +const INSTRUCTIONS: &str = "PdfCraft edits PDFs. Open a file with doc_open to get a document id, then inspect \ (doc_info, text_extract, text_find, page_render) or edit it (page_*, doc_set_info). Edits are undoable \ (edit_undo) and stay in memory until doc_save. Page numbers are 1-based. Open documents are also \ -resources: printcraft://doc/{doc}/info, /text, /page/{page}/text and /page/{page}/image."; +resources: pdfcraft://doc/{doc}/info, /text, /page/{page}/text and /page/{page}/image."; pub struct McpServer { automation: Automation, @@ -92,7 +92,7 @@ impl McpServer { Ok(json!({ "protocolVersion": version, "capabilities": { "tools": { "listChanged": false }, "resources": { "listChanged": false, "subscribe": false } }, - "serverInfo": { "name": "printcraft", "title": "PrintCraft", "version": env!("CARGO_PKG_VERSION"), "websiteUrl": printcraft_engine::links::APP_PAGE }, + "serverInfo": { "name": "pdfcraft", "title": "PdfCraft", "version": env!("CARGO_PKG_VERSION"), "websiteUrl": pdfcraft_engine::links::APP_PAGE }, "instructions": INSTRUCTIONS, })) } @@ -118,7 +118,7 @@ impl McpServer { } } -/// The resource behind a `printcraft://` URI. +/// The resource behind a `pdfcraft://` URI. #[derive(Debug, PartialEq)] enum Resource { Info(u64), @@ -128,7 +128,7 @@ enum Resource { } fn parse_uri(uri: &str) -> Option { - let rest = uri.strip_prefix("printcraft://doc/")?; + let rest = uri.strip_prefix("pdfcraft://doc/")?; let (path, query) = rest.split_once('?').unwrap_or((rest, "")); let dpi = query.split('&').find_map(|kv| kv.strip_prefix("dpi=")).and_then(|v| v.parse::().ok()); let parts: Vec<&str> = path.split('/').collect(); @@ -144,13 +144,13 @@ fn parse_uri(uri: &str) -> Option { fn resource_templates() -> Value { json!([ - { "uriTemplate": "printcraft://doc/{doc}/info", "name": "Document information", "mimeType": "application/json", + { "uriTemplate": "pdfcraft://doc/{doc}/info", "name": "Document information", "mimeType": "application/json", "description": "Metadata, pages, bookmarks, annotations, fields, links, layers, attachments, fonts and security of an open document (as doc_info)." }, - { "uriTemplate": "printcraft://doc/{doc}/text", "name": "Document text", "mimeType": "text/plain", + { "uriTemplate": "pdfcraft://doc/{doc}/text", "name": "Document text", "mimeType": "text/plain", "description": "The text of every page in reading order, each page under a \"Page n\" heading." }, - { "uriTemplate": "printcraft://doc/{doc}/page/{page}/text", "name": "Page text", "mimeType": "text/plain", + { "uriTemplate": "pdfcraft://doc/{doc}/page/{page}/text", "name": "Page text", "mimeType": "text/plain", "description": "The text of one page (1-based) in reading order." }, - { "uriTemplate": "printcraft://doc/{doc}/page/{page}/image{?dpi}", "name": "Page image", "mimeType": "image/png", + { "uriTemplate": "pdfcraft://doc/{doc}/page/{page}/image{?dpi}", "name": "Page image", "mimeType": "image/png", "description": "One page (1-based) rendered to PNG; dpi 1–600, default 96." }, ]) } @@ -164,15 +164,15 @@ impl McpServer { let mut out = Vec::new(); for d in docs.unwrap_or_default() { let (id, name) = (d["doc"].as_u64().unwrap_or(0), d["name"].as_str().unwrap_or("document")); - out.push( - json!({ "uri": format!("printcraft://doc/{id}/info"), "name": format!("{name} (information)"), "mimeType": "application/json" }), - ); - out.push(json!({ "uri": format!("printcraft://doc/{id}/text"), "name": format!("{name} (text)"), "mimeType": "text/plain" })); + out.push(json!({ "uri": format!("pdfcraft://doc/{id}/info"), "name": format!("{name} (information)"), "mimeType": "application/json" })); + out.push(json!({ "uri": format!("pdfcraft://doc/{id}/text"), "name": format!("{name} (text)"), "mimeType": "text/plain" })); // Pages are listed for short documents; longer ones use the templates. let pages = d["pages"].as_u64().unwrap_or(0); if pages <= 50 { for p in 1..=pages { - out.push(json!({ "uri": format!("printcraft://doc/{id}/page/{p}/image"), "name": format!("{name}, page {p}"), "mimeType": "image/png" })); + out.push( + json!({ "uri": format!("pdfcraft://doc/{id}/page/{p}/image"), "name": format!("{name}, page {p}"), "mimeType": "image/png" }), + ); } } } @@ -266,10 +266,10 @@ mod tests { #[test] fn resource_uris_parse() { - assert_eq!(parse_uri("printcraft://doc/3/info"), Some(Resource::Info(3))); - assert_eq!(parse_uri("printcraft://doc/3/page/2/image?dpi=36"), Some(Resource::PageImage(3, 2, Some(36.0)))); - assert_eq!(parse_uri("printcraft://doc/3/page/2/text"), Some(Resource::PageText(3, 2))); - assert_eq!(parse_uri("printcraft://doc/x/info"), None); + assert_eq!(parse_uri("pdfcraft://doc/3/info"), Some(Resource::Info(3))); + assert_eq!(parse_uri("pdfcraft://doc/3/page/2/image?dpi=36"), Some(Resource::PageImage(3, 2, Some(36.0)))); + assert_eq!(parse_uri("pdfcraft://doc/3/page/2/text"), Some(Resource::PageText(3, 2))); + assert_eq!(parse_uri("pdfcraft://doc/x/info"), None); assert_eq!(parse_uri("file:///etc/passwd"), None); } } diff --git a/crates/automation/src/printing.rs b/crates/automation/src/printing.rs index a56e8ca6..34fd799b 100644 --- a/crates/automation/src/printing.rs +++ b/crates/automation/src/printing.rs @@ -1,7 +1,7 @@ //! Printing tools: list printers, and print (or save the print-ready PDF) with Acrobat's Print //! dialog options. -use printcraft_engine::print::{self, Binding, BookletSubset, Content, Layout, Orientation, PAPERS, PageOrder, SizeMode, Subset, spool}; +use pdfcraft_engine::print::{self, Binding, BookletSubset, Content, Layout, Orientation, PAPERS, PageOrder, SizeMode, Subset, spool}; use serde_json::{Value, json}; use crate::{Args, Automation, Result, ToolError, failed, write_atomic}; diff --git a/crates/automation/src/redact.rs b/crates/automation/src/redact.rs index 6c68ade6..6f322cd9 100644 --- a/crates/automation/src/redact.rs +++ b/crates/automation/src/redact.rs @@ -1,7 +1,7 @@ //! Redaction tools: mark areas, text, patterns or whole pages for redaction, apply the marks //! (removing what they cover for good) or clear them. -use printcraft_engine::{Edit, Hidden, NewAnnotation, RedactPattern, Shape, Style, find_pattern, rect_quad}; +use pdfcraft_engine::{Edit, Hidden, NewAnnotation, RedactPattern, Shape, Style, find_pattern, rect_quad}; use serde_json::{Value, json}; use crate::comments::{DEFAULT_AUTHOR, parse_color}; diff --git a/crates/automation/src/signing.rs b/crates/automation/src/signing.rs index c9bfebe9..ce60b33c 100644 --- a/crates/automation/src/signing.rs +++ b/crates/automation/src/signing.rs @@ -2,8 +2,8 @@ //! document, and manage trusted certificates. Rectangles are points from the top-left of the //! displayed page. -use printcraft_engine::sign::{self, Certificate, DigitalId, Modification, Name, PrivateKey}; -use printcraft_engine::{SignOptions, SignatureInfo, SignatureStatus}; +use pdfcraft_engine::sign::{self, Certificate, DigitalId, Modification, Name, PrivateKey}; +use pdfcraft_engine::{SignOptions, SignatureInfo, SignatureStatus}; use serde_json::{Value, json}; use crate::{Args, Automation, Result, ToolError, failed, write_atomic}; diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index f27e993f..21828c84 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -690,7 +690,7 @@ pub fn tools() -> Vec { json!({ "doc": doc(), "pages": pages("to fill (default: all)"), "color": { "type": "string" }, "file": { "type": "string" }, "file_page": { "type": "integer", "minimum": 1 }, "scale": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 }, "opacity": { "type": "number", "minimum": 0, "maximum": 1 }, "replace": { "type": "boolean" } }), &["doc"], )), - t("doc_remove_marks", "Remove header & footer, watermark or background", "Remove every header and footer, watermark or background PrintCraft (or a compatible tool) added. Undoable.") + t("doc_remove_marks", "Remove header & footer, watermark or background", "Remove every header and footer, watermark or background PdfCraft (or a compatible tool) added. Undoable.") .destructive() .with(schema(json!({ "doc": doc(), "kind": { "type": "string", "enum": ["header_footer", "watermark", "background"] } }), &["doc", "kind"])), t("doc_export_images", "Export pages as images", "Write pages as PNG, JPEG or TIFF files (`_page_.png|jpg|tif`) into a folder, at a resolution (default 150 dpi). JPEG and TIFF are flattened onto white paper. Includes unsaved edits.") @@ -841,7 +841,7 @@ pub fn tools() -> Vec { }), &["paths", "folder"], )), - t("ocr_status", "OCR status", "Whether text recognition is available (its models are installed: run `cargo xtask models` or set PRINTCRAFT_MODELS), where it looks for them, and the languages it reads.") + t("ocr_status", "OCR status", "Whether text recognition is available (its models are installed: run `cargo xtask models` or set PDFCRAFT_MODELS), where it looks for them, and the languages it reads.") .ro() .cmd("ocr.recognize") .with(schema(json!({}), &[])), @@ -999,7 +999,7 @@ pub fn tools() -> Vec { .with(schema(json!({ "doc": doc(), "comments": { "type": "boolean", "description": "Default true." }, "fields": { "type": "boolean", "description": "Default true." } }), &["doc"])), t("edit_undo", "Undo", "Undo the last edit of a document.").cmd("edit.undo").with(schema(json!({ "doc": doc() }), &["doc"])), t("edit_redo", "Redo", "Redo the last undone edit of a document.").cmd("edit.redo").with(schema(json!({ "doc": doc() }), &["doc"])), - t("command_list", "List commands", "Every registered PrintCraft command with its menu, shortcut, whether it is enabled now, and the tool that automates it.") + t("command_list", "List commands", "Every registered PdfCraft command with its menu, shortcut, whether it is enabled now, and the tool that automates it.") .ro() .with(schema(json!({ "doc": doc() }), &[])), ] diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 578a1b23..13e35ea1 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -3,8 +3,8 @@ use std::path::{Path, PathBuf}; #[cfg(feature = "mcp")] -use printcraft_automation::mcp::McpServer; -use printcraft_automation::{Automation, Content, ToolError, tools}; +use pdfcraft_automation::mcp::McpServer; +use pdfcraft_automation::{Automation, Content, ToolError, tools}; use serde_json::{Value, json}; /// A PDF with `n` 200×300 pt pages reading "Page 1", "Page 2", … @@ -37,7 +37,7 @@ fn fixture(n: usize) -> Vec { /// A fresh directory with `a.pdf` (3 pages) and `b.pdf` (2 pages). fn workdir(test: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!("printcraft-automation-{test}-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!("pdfcraft-automation-{test}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("a.pdf"), fixture(3)).unwrap(); @@ -77,7 +77,7 @@ fn tool_table_is_well_formed() { } assert!(!(t.read_only && t.destructive), "{} is both read-only and destructive", t.name); if let Some(c) = t.command { - assert!(printcraft_engine::commands::command(c).is_some(), "{} names unregistered command {c}", t.name); + assert!(pdfcraft_engine::commands::command(c).is_some(), "{} names unregistered command {c}", t.name); } } } @@ -206,11 +206,11 @@ fn errors_are_specific_and_safe() { assert_eq!(ok(&mut a, "doc_list", json!({}))["documents"], json!([])); // The root confines reads and writes. - let outside = std::env::temp_dir().join("printcraft-automation-outside.pdf"); + let outside = std::env::temp_dir().join("pdfcraft-automation-outside.pdf"); std::fs::write(&outside, fixture(1)).unwrap(); assert!(matches!(err(&mut a, "doc_open", json!({ "path": outside.to_str().unwrap() })), ToolError::Failed(m) if m.contains("outside"))); assert!( - matches!(err(&mut a, "doc_open", json!({ "path": "../printcraft-automation-outside.pdf" })), ToolError::Failed(m) if m.contains("outside")) + matches!(err(&mut a, "doc_open", json!({ "path": "../pdfcraft-automation-outside.pdf" })), ToolError::Failed(m) if m.contains("outside")) ); let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); assert!(a.call("doc_save", &json!({ "doc": doc, "path": "new/../../escape.pdf" })).is_err()); @@ -265,7 +265,7 @@ fn mcp_session_over_stdio() { assert_eq!(replies.len(), 4, "the notification gets no reply"); assert_eq!(replies[0]["result"]["protocolVersion"], "2025-03-26"); - assert_eq!(replies[0]["result"]["serverInfo"]["name"], "printcraft"); + assert_eq!(replies[0]["result"]["serverInfo"]["name"], "pdfcraft"); assert_eq!(replies[1]["result"]["tools"].as_array().unwrap().len(), tools().len()); assert_eq!(replies[2]["result"]["structuredContent"]["pages"], 3); assert_eq!(replies[2]["result"]["isError"], false); @@ -283,7 +283,7 @@ fn mcp_errors() { assert_eq!(rpc(&mut s, 1, "initialize", json!({ "protocolVersion": "1999-01-01" }))["result"]["protocolVersion"], "2025-06-18"); assert_eq!(rpc(&mut s, 2, "ping", json!({}))["result"], json!({})); assert_eq!(rpc(&mut s, 3, "prompts/list", json!({}))["error"]["code"], -32601); - assert_eq!(rpc(&mut s, 6, "resources/read", json!({ "uri": "printcraft://doc/9/info" }))["error"]["code"], -32602); + assert_eq!(rpc(&mut s, 6, "resources/read", json!({ "uri": "pdfcraft://doc/9/info" }))["error"]["code"], -32602); assert_eq!(rpc(&mut s, 4, "tools/call", json!({ "name": "nope" }))["error"]["code"], -32602); let failed = rpc(&mut s, 5, "tools/call", json!({ "name": "doc_open", "arguments": { "path": "/definitely/not/here.pdf" } })); assert_eq!(failed["result"]["isError"], true); @@ -303,19 +303,19 @@ fn mcp_resources_expose_open_documents() { rpc(&mut s, 4, "tools/call", json!({ "name": "doc_open", "arguments": { "path": "a.pdf" } })); let list = rpc(&mut s, 5, "resources/list", json!({}))["result"]["resources"].as_array().cloned().unwrap(); assert_eq!(list.len(), 2 + 3, "info, text and three page images"); - assert_eq!(list[0]["uri"], "printcraft://doc/1/info"); + assert_eq!(list[0]["uri"], "pdfcraft://doc/1/info"); let read = |s: &mut McpServer, uri: &str| rpc(s, 6, "resources/read", json!({ "uri": uri }))["result"]["contents"][0].clone(); - let text = read(&mut s, "printcraft://doc/1/text"); + let text = read(&mut s, "pdfcraft://doc/1/text"); assert_eq!(text["mimeType"], "text/plain"); assert!(text["text"].as_str().unwrap().contains("Page 2\nPage 2"), "{text}"); - assert_eq!(read(&mut s, "printcraft://doc/1/page/3/text")["text"], "Page 3"); - let info: Value = serde_json::from_str(read(&mut s, "printcraft://doc/1/info")["text"].as_str().unwrap()).unwrap(); + assert_eq!(read(&mut s, "pdfcraft://doc/1/page/3/text")["text"], "Page 3"); + let info: Value = serde_json::from_str(read(&mut s, "pdfcraft://doc/1/info")["text"].as_str().unwrap()).unwrap(); assert_eq!(info["pages"].as_array().unwrap().len(), 3); - let img = read(&mut s, "printcraft://doc/1/page/1/image?dpi=36"); + let img = read(&mut s, "pdfcraft://doc/1/page/1/image?dpi=36"); use base64::Engine as _; let png = base64::engine::general_purpose::STANDARD.decode(img["blob"].as_str().unwrap()).unwrap(); assert_eq!(&png[1..4], b"PNG"); - assert_eq!(rpc(&mut s, 7, "resources/read", json!({ "uri": "printcraft://doc/1/page/9/image" }))["error"]["code"], -32602); + assert_eq!(rpc(&mut s, 7, "resources/read", json!({ "uri": "pdfcraft://doc/1/page/9/image" }))["error"]["code"], -32602); } #[test] @@ -487,7 +487,7 @@ fn protecting_through_tools() { #[test] fn forms_through_tools() { let dir = workdir("forms"); - let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist/demo/printcraft-showcase.pdf"); + let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../dist/demo/pdfcraft-showcase.pdf"); if !src.exists() { eprintln!("skipped: run `cargo xtask demo-pdf` for the showcase form"); return; @@ -1121,9 +1121,9 @@ fn links_through_tools() { assert_eq!(list["count"], 3); let added = list["links"].as_array().unwrap().iter().find(|l| l["to_page"] == 1).unwrap().clone(); assert_eq!(added["rect"], json!([72.0, 300.0, 200.0, 320.0])); - ok(&mut a, "link_edit", json!({ "doc": doc, "page": 1, "index": added["index"], "url": "https://printcraft.dev" })); + ok(&mut a, "link_edit", json!({ "doc": doc, "page": 1, "index": added["index"], "url": "https://pdfcraft.dev" })); let list = ok(&mut a, "link_list", json!({ "doc": doc })); - assert!(list["links"].as_array().unwrap().iter().any(|l| l["url"] == "https://printcraft.dev")); + assert!(list["links"].as_array().unwrap().iter().any(|l| l["url"] == "https://pdfcraft.dev")); ok(&mut a, "link_delete", json!({ "doc": doc, "page": 1, "index": added["index"] })); let r = ok(&mut a, "links_remove", json!({ "doc": doc })); assert_eq!(r["removed"], 2); diff --git a/crates/compare/Cargo.toml b/crates/compare/Cargo.toml index 2182822a..42c92ee9 100644 --- a/crates/compare/Cargo.toml +++ b/crates/compare/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-compare" +name = "pdfcraft-compare" description = "Compare files: word-level text differences between two documents, grouped into changes with their places on the pages" version.workspace = true edition.workspace = true diff --git a/crates/compare/src/lib.rs b/crates/compare/src/lib.rs index 31e661c5..91245cbc 100644 --- a/crates/compare/src/lib.rs +++ b/crates/compare/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-compare — Compare files: text differences (L4). +//! pdfcraft-compare — Compare files: text differences (L4). //! //! Both documents' words (in reading order, with page and box) are diffed with Myers' //! O((N+M)·D) algorithm after the common prefix and suffix are set aside; the edit script is diff --git a/crates/content/Cargo.toml b/crates/content/Cargo.toml index d16a7fee..c553df0b 100644 --- a/crates/content/Cargo.toml +++ b/crates/content/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-content" +name = "pdfcraft-content" description = "Content streams (ISO 32000-2 §7.8, §8–9): operators with source spans, inline images, serializer, matrices" version.workspace = true edition.workspace = true @@ -9,4 +9,4 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } +pdfcraft-cos = { workspace = true } diff --git a/crates/content/README.md b/crates/content/README.md index 9286f04a..d9d8af10 100644 --- a/crates/content/README.md +++ b/crates/content/README.md @@ -1,4 +1,4 @@ -# printcraft-content +# pdfcraft-content Layer L2: content streams (ISO 32000-2 §7.8.2, §8, §9). diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index c541d701..70e173d3 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -7,7 +7,7 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_cos::{Dict, Lexer, Object, PdfString, serialize}; +use pdfcraft_cos::{Dict, Lexer, Object, PdfString, serialize}; #[cfg(test)] mod tests; diff --git a/crates/cos/Cargo.toml b/crates/cos/Cargo.toml index 7a82e40f..f02f63c5 100644 --- a/crates/cos/Cargo.toml +++ b/crates/cos/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-cos" +name = "pdfcraft-cos" description = "PDF object layer: lazy tolerant parsing, copy-on-write edits, incremental and full saving" version.workspace = true edition.workspace = true @@ -9,8 +9,8 @@ license.workspace = true workspace = true [dependencies] -printcraft-crypt = { workspace = true } -printcraft-filters = { workspace = true } +pdfcraft-crypt = { workspace = true } +pdfcraft-filters = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/cos/examples/cosinfo.rs b/crates/cos/examples/cosinfo.rs index 7cabf2d2..011bb52b 100644 --- a/crates/cos/examples/cosinfo.rs +++ b/crates/cos/examples/cosinfo.rs @@ -1,8 +1,8 @@ //! Print how a file was opened: revisions, repair notes, trailer, security and the catalog. -//! `cargo run -p printcraft-cos --example cosinfo f.pdf [password] [object-number…]` -fn show(o: &printcraft_cos::Object) -> String { +//! `cargo run -p pdfcraft-cos --example cosinfo f.pdf [password] [object-number…]` +fn show(o: &pdfcraft_cos::Object) -> String { let mut t = Vec::new(); - printcraft_cos::serialize(o, &mut t); + pdfcraft_cos::serialize(o, &mut t); let s = String::from_utf8_lossy(&t).into_owned(); s.chars().take(400).collect() } @@ -13,13 +13,13 @@ fn main() { let password = args.next().filter(|p| !p.is_empty()); let objs: Vec = args.filter_map(|a| a.parse().ok()).collect(); let bytes = std::sync::Arc::new(std::fs::read(&path).expect("readable")); - match printcraft_cos::Document::open_with_password(bytes, password.as_deref()) { + match pdfcraft_cos::Document::open_with_password(bytes, password.as_deref()) { Ok(doc) => { println!("version {} revisions {:?}", doc.version(), doc.revisions()); for r in doc.repair_log() { println!("repair: {r}"); } - println!("trailer {}", show(&printcraft_cos::Object::Dict(doc.trailer().clone()))); + println!("trailer {}", show(&pdfcraft_cos::Object::Dict(doc.trailer().clone()))); if let Some(s) = doc.security() { println!("security: auth {:?}, key {} bytes, {:?}", s.auth(), s.file_key().len(), s.permissions()); } diff --git a/crates/cos/examples/protect.rs b/crates/cos/examples/protect.rs index f2bb8dd9..6f27a609 100644 --- a/crates/cos/examples/protect.rs +++ b/crates/cos/examples/protect.rs @@ -1,15 +1,15 @@ -//! Encrypt a PDF: `cargo run -p printcraft-cos --example protect in.pdf out.pdf rc4-40|rc4-128|aes-128|aes-256 USER OWNER` +//! Encrypt a PDF: `cargo run -p pdfcraft-cos --example protect in.pdf out.pdf rc4-40|rc4-128|aes-128|aes-256 USER OWNER` fn main() { let a: Vec = std::env::args().skip(1).collect(); let [input, output, alg, user, owner] = &a[..] else { panic!("usage: protect in out alg user owner") }; let algorithm = match alg.as_str() { - "rc4-40" => printcraft_cos::Algorithm::Rc4_40, - "rc4-128" => printcraft_cos::Algorithm::Rc4_128, - "aes-128" => printcraft_cos::Algorithm::Aes128, - _ => printcraft_cos::Algorithm::Aes256, + "rc4-40" => pdfcraft_cos::Algorithm::Rc4_40, + "rc4-128" => pdfcraft_cos::Algorithm::Rc4_128, + "aes-128" => pdfcraft_cos::Algorithm::Aes128, + _ => pdfcraft_cos::Algorithm::Aes256, }; - let mut doc = printcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(input).unwrap())).unwrap(); - doc.set_encryption(&printcraft_cos::NewEncryption { + let mut doc = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(input).unwrap())).unwrap(); + doc.set_encryption(&pdfcraft_cos::NewEncryption { algorithm, user_password: user, owner_password: owner, @@ -18,5 +18,5 @@ fn main() { seed: [1; 32], }) .unwrap(); - std::fs::write(output, printcraft_cos::write_full(&doc, &Default::default()).unwrap()).unwrap(); + std::fs::write(output, pdfcraft_cos::write_full(&doc, &Default::default()).unwrap()).unwrap(); } diff --git a/crates/cos/src/document.rs b/crates/cos/src/document.rs index 0629ba60..910be06b 100644 --- a/crates/cos/src/document.rs +++ b/crates/cos/src/document.rs @@ -99,7 +99,7 @@ pub struct Document { header_offset: usize, version: String, /// The authenticated security handler of an encrypted document. - security: Option>, + security: Option>, /// Object number of the `/Encrypt` dictionary (never encrypted itself). encrypt_num: Option, /// Encryption was added, changed or removed since opening: only a full save can apply it. @@ -107,7 +107,7 @@ pub struct Document { /// Set by edits that must not leave earlier revisions in the file (redaction). full_save: bool, /// The handler and `/Encrypt` object number that saves use, when encryption changed. - out_security: Option>, + out_security: Option>, out_encrypt_num: Option, } @@ -255,9 +255,9 @@ impl Document { Some(Object::Array(a)) => a.first().and_then(|s| s.as_string()).map(|s| s.bytes.clone()).unwrap_or_default(), _ => Vec::new(), }; - let handler = printcraft_crypt::SecurityHandler::open(params, &id0, password).map_err(|e| match e { - printcraft_crypt::CryptError::WrongPassword if password.is_none() => CosError::NeedsPassword, - printcraft_crypt::CryptError::WrongPassword => CosError::WrongPassword, + let handler = pdfcraft_crypt::SecurityHandler::open(params, &id0, password).map_err(|e| match e { + pdfcraft_crypt::CryptError::WrongPassword if password.is_none() => CosError::NeedsPassword, + pdfcraft_crypt::CryptError::WrongPassword => CosError::WrongPassword, other => CosError::Security(other.to_string()), })?; self.security = Some(Arc::new(handler)); @@ -269,12 +269,12 @@ impl Document { } /// The security handler, when the document is encrypted. - pub fn security(&self) -> Option<&printcraft_crypt::SecurityHandler> { + pub fn security(&self) -> Option<&pdfcraft_crypt::SecurityHandler> { self.security.as_deref() } /// What the opening password allows (`None` for unencrypted documents: everything). - pub fn permissions(&self) -> Option { + pub fn permissions(&self) -> Option { self.security().map(|s| s.permissions()) } @@ -296,19 +296,19 @@ impl Document { /// The handler used to write: the new one after `set_encryption` / `remove_encryption`, /// otherwise the one the document was opened with. - pub(crate) fn output_security(&self) -> (Option<&printcraft_crypt::SecurityHandler>, Option) { + pub(crate) fn output_security(&self) -> (Option<&pdfcraft_crypt::SecurityHandler>, Option) { if self.encryption_changed { (self.out_security.as_deref(), self.out_encrypt_num) } else { (self.security.as_deref(), self.encrypt_num) } } /// The security the next save writes: protection applied with `set_encryption`, none after /// `remove_encryption`, otherwise the security the document was opened with. - pub fn output_handler(&self) -> Option<&printcraft_crypt::SecurityHandler> { + pub fn output_handler(&self) -> Option<&pdfcraft_crypt::SecurityHandler> { self.output_security().0 } /// Protect the document with a password (§7.6.4). Takes effect on the next save, which is /// always a full rewrite. Returns the handler (authenticated as owner). - pub fn set_encryption(&mut self, params: &printcraft_crypt::NewEncryption) -> Result<(), CosError> { + pub fn set_encryption(&mut self, params: &pdfcraft_crypt::NewEncryption) -> Result<(), CosError> { // The file identifier is part of the key; make sure it exists and keep it. let id0 = match self.trailer.get(b"ID") { Some(Object::Array(a)) if a.len() == 2 => a[0].as_string().map(|s| s.bytes.clone()).unwrap_or_default(), @@ -320,7 +320,7 @@ impl Document { id } }; - let h = printcraft_crypt::create(params, &id0).map_err(|e| CosError::Security(e.to_string()))?; + let h = pdfcraft_crypt::create(params, &id0).map_err(|e| CosError::Security(e.to_string()))?; let d = h.dict(); let s = |b: &[u8]| Object::String(crate::PdfString { bytes: b.to_vec(), hex: true }); let mut e = Dict::new(); @@ -928,7 +928,7 @@ fn generated_id(seed: &[u8; 32]) -> Vec { let mut out = Vec::new(); for i in 0..2u8 { let mut h = std::collections::hash_map::DefaultHasher::new(); - (seed, i, b"printcraft id").hash(&mut h); + (seed, i, b"pdfcraft id").hash(&mut h); out.extend_from_slice(&h.finish().to_be_bytes()); } out @@ -1087,7 +1087,7 @@ mod tests { // /W [1 1 1]: at 3 bytes a row that is more than 8,388,607 objects' worth, so it is not // decoded (in full: the limit applies while inflating) and the objects are found by // reconstruction instead. - let bomb = printcraft_filters::encode_flate(&printcraft_filters::encode_flate(&vec![0u8; 32 << 20])); + let bomb = pdfcraft_filters::encode_flate(&pdfcraft_filters::encode_flate(&vec![0u8; 32 << 20])); let mut bytes = b"%PDF-1.7\n".to_vec(); let o1 = bytes.len(); bytes.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"); diff --git a/crates/cos/src/lib.rs b/crates/cos/src/lib.rs index ad85c4f8..4cab44d7 100644 --- a/crates/cos/src/lib.rs +++ b/crates/cos/src/lib.rs @@ -1,8 +1,8 @@ -//! printcraft-cos — the PDF object layer (L1, standalone with `printcraft-filters`). +//! pdfcraft-cos — the PDF object layer (L1, standalone with `pdfcraft-filters`). //! //! Parses the COS object graph lazily and tolerantly, keeps edits in a copy-on-write overlay //! (cheap snapshots for undo), and writes documents back either incrementally (original bytes -//! untouched) or as a full, garbage-collected rewrite. The PDF object graph *is* PrintCraft's +//! untouched) or as a full, garbage-collected rewrite. The PDF object graph *is* PdfCraft's //! document model (plan/architecture.md §5, ADR-0010). //! //! Status (M1 in progress): parsing of all xref forms, object streams, repair by scanning, @@ -20,7 +20,7 @@ mod writer; pub use document::{Document, Revision, XrefEntry}; pub use object::{Dict, MAX_DECODED, Name, ObjRef, Object, PdfString, Stream}; pub use parser::{Lexer, parse_indirect}; -pub use printcraft_crypt::{Algorithm, Auth, Method as CryptMethod, NewEncryption, Permissions, SecurityHandler}; +pub use pdfcraft_crypt::{Algorithm, Auth, Method as CryptMethod, NewEncryption, Permissions, SecurityHandler}; pub use writer::{SaveOptions, pdf_date, serialize, write_full, write_incremental}; #[derive(Debug, thiserror::Error, Clone, PartialEq)] diff --git a/crates/cos/src/object.rs b/crates/cos/src/object.rs index 96eac347..983b5373 100644 --- a/crates/cos/src/object.rs +++ b/crates/cos/src/object.rs @@ -5,7 +5,7 @@ use std::sync::Arc; -use printcraft_filters::{Filter, Params}; +use pdfcraft_filters::{Filter, Params}; use crate::CosError; @@ -176,7 +176,7 @@ impl Stream { pub fn flate(mut dict: Dict, data: &[u8]) -> Self { dict.set(b"Filter".to_vec(), Object::Name(b"FlateDecode".to_vec())); dict.remove(b"DecodeParms"); - Self { dict, raw: Arc::new(printcraft_filters::encode_flate(data)) } + Self { dict, raw: Arc::new(pdfcraft_filters::encode_flate(data)) } } /// The filter chain declared in the dictionary. @@ -225,7 +225,7 @@ impl Stream { if chain.is_empty() { return Ok(self.raw.as_ref().clone()); } - printcraft_filters::decode_tolerant(&chain, &self.raw, max.min(MAX_DECODED)).map(|(v, _)| v).map_err(|e| CosError::Filter(e.to_string())) + pdfcraft_filters::decode_tolerant(&chain, &self.raw, max.min(MAX_DECODED)).map(|(v, _)| v).map_err(|e| CosError::Filter(e.to_string())) } /// Decoded data; any corruption is an error. @@ -234,7 +234,7 @@ impl Stream { if chain.is_empty() { return Ok(self.raw.as_ref().clone()); } - printcraft_filters::decode(&chain, &self.raw, MAX_DECODED).map_err(|e| CosError::Filter(e.to_string())) + pdfcraft_filters::decode(&chain, &self.raw, MAX_DECODED).map_err(|e| CosError::Filter(e.to_string())) } } diff --git a/crates/cos/src/security.rs b/crates/cos/src/security.rs index 4db862f6..709176b3 100644 --- a/crates/cos/src/security.rs +++ b/crates/cos/src/security.rs @@ -1,4 +1,4 @@ -//! Bridging the COS object model and `printcraft-crypt`: reading `/Encrypt` dictionaries and +//! Bridging the COS object model and `pdfcraft-crypt`: reading `/Encrypt` dictionaries and //! decrypting / encrypting the strings and streams of an object (§7.6.2). //! //! Not encrypted, per the spec: the `/Encrypt` dictionary itself, cross-reference streams, the @@ -6,7 +6,7 @@ //! metadata streams when `/EncryptMetadata` is false. Streams naming a crypt filter (`/Crypt` in //! `/Filter`) use that filter; embedded files use `/EFF`. -use printcraft_crypt::{EncryptDict, Method, SecurityHandler, StreamKind}; +use pdfcraft_crypt::{EncryptDict, Method, SecurityHandler, StreamKind}; use crate::{Dict, Document, Object, PdfString, Stream}; diff --git a/crates/cos/tests/corpus.rs b/crates/cos/tests/corpus.rs index a160e21a..71a72b37 100644 --- a/crates/cos/tests/corpus.rs +++ b/crates/cos/tests/corpus.rs @@ -7,7 +7,7 @@ use std::sync::Arc; -use printcraft_cos::{Document, Object, SaveOptions, write_full, write_incremental}; +use pdfcraft_cos::{Document, Object, SaveOptions, write_full, write_incremental}; /// Leaf pages reachable from the catalog (cycle-safe), like viewers count them. fn page_count(doc: &Document) -> Option { @@ -56,7 +56,7 @@ fn corpus_parity() { let result = std::panic::catch_unwind(|| -> Result<(), String> { let mut doc = match Document::open(bytes.clone()) { Ok(d) => d, - Err(printcraft_cos::CosError::NeedsPassword) => return Err("encrypted".into()), + Err(pdfcraft_cos::CosError::NeedsPassword) => return Err("encrypted".into()), Err(e) => return Err(format!("open: {e}")), }; // hayro may repair a broken /Count; compare against the leaf walk count loosely. @@ -64,14 +64,14 @@ fn corpus_parity() { return Err(format!("page count {:?} vs hayro {expected}", page_count(&doc))); } let root = doc.root().ok_or("no root")?; - doc.update_dict(root, |d| d.set(b"PrintCraftTest".to_vec(), Object::Bool(true))).map_err(|e| e.to_string())?; + doc.update_dict(root, |d| d.set(b"PdfCraftTest".to_vec(), Object::Bool(true))).map_err(|e| e.to_string())?; let saved = write_incremental(&doc, &SaveOptions::default()).map_err(|e| format!("save: {e}"))?; // Reconstructed files are rewritten in full; everything else must keep its bytes. if !doc.revisions().is_empty() && saved.get(..bytes.len()) != Some(&bytes[..]) { return Err("prefix modified".into()); } let again = Document::open(Arc::new(saved.clone())).map_err(|e| format!("reopen: {e}"))?; - let flag = again.get(again.root().ok_or("no root after save")?).as_dict().and_then(|d| d.get(b"PrintCraftTest").cloned()); + let flag = again.get(again.root().ok_or("no root after save")?).as_dict().and_then(|d| d.get(b"PdfCraftTest").cloned()); if flag != Some(Object::Bool(true)) { return Err("edit lost after reopen".into()); } @@ -118,10 +118,10 @@ fn corpus_passwords() { for (file, pw) in &cases { let Ok(bytes) = std::fs::read(dir.join(file)) else { continue }; let bytes = Arc::new(bytes); - if !matches!(Document::open(bytes.clone()), Err(printcraft_cos::CosError::NeedsPassword)) { + if !matches!(Document::open(bytes.clone()), Err(pdfcraft_cos::CosError::NeedsPassword)) { failures.push(format!("{file}: opened without a password")); } - if !matches!(Document::open_with_password(bytes.clone(), Some("definitely wrong")), Err(printcraft_cos::CosError::WrongPassword)) { + if !matches!(Document::open_with_password(bytes.clone(), Some("definitely wrong")), Err(pdfcraft_cos::CosError::WrongPassword)) { failures.push(format!("{file}: wrong password not rejected")); } match Document::open_with_password(bytes.clone(), Some(pw)) { @@ -132,7 +132,7 @@ fn corpus_passwords() { continue; } let root = doc.root().unwrap(); - doc.update_dict(root, |d| d.set(b"PrintCraftTest".to_vec(), Object::Bool(true))).unwrap(); + doc.update_dict(root, |d| d.set(b"PdfCraftTest".to_vec(), Object::Bool(true))).unwrap(); let saved = write_incremental(&doc, &SaveOptions::default()).unwrap(); match Document::open_with_password(Arc::new(saved.clone()), Some(pw)) { Ok(again) if page_count(&again) == pages => {} diff --git a/crates/cos/tests/encryption.rs b/crates/cos/tests/encryption.rs index 071712d6..83bbaca0 100644 --- a/crates/cos/tests/encryption.rs +++ b/crates/cos/tests/encryption.rs @@ -4,7 +4,7 @@ use std::sync::Arc; -use printcraft_cos::{Algorithm, Auth, CosError, Document, NewEncryption, ObjRef, Object, PdfString, SaveOptions, write_full, write_incremental}; +use pdfcraft_cos::{Algorithm, Auth, CosError, Document, NewEncryption, ObjRef, Object, PdfString, SaveOptions, write_full, write_incremental}; const SECRET: &str = "BT /F1 18 Tf 20 100 Td (Secret page) Tj ET"; @@ -180,7 +180,7 @@ fn qpdf_agrees() { eprintln!("qpdf not installed; skipping"); return; } - let dir = std::env::temp_dir().join(format!("printcraft-enc-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!("pdfcraft-enc-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); for alg in ALL { let path = dir.join(format!("{alg:?}.pdf")); diff --git a/crates/cos/tests/writer.rs b/crates/cos/tests/writer.rs index 3c05e5af..bfe85882 100644 --- a/crates/cos/tests/writer.rs +++ b/crates/cos/tests/writer.rs @@ -3,7 +3,7 @@ use std::sync::Arc; -use printcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, write_full, write_incremental}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, write_full, write_incremental}; /// Four pages, a shared font, document info with a non-ASCII title. fn fixture() -> Vec { @@ -71,10 +71,8 @@ fn incremental_saves_stack_on_an_xref_stream_file() { let mut doc = Document::open(packed.clone()).unwrap(); // Edit an object that lives in an object stream. let info = doc.trailer().reference(b"Info").unwrap(); - doc.update_dict(info, |d: &mut Dict| { - d.set(b"Title".to_vec(), Object::String(printcraft_cos::PdfString { bytes: b"Second".to_vec(), hex: false })) - }) - .unwrap(); + doc.update_dict(info, |d: &mut Dict| d.set(b"Title".to_vec(), Object::String(pdfcraft_cos::PdfString { bytes: b"Second".to_vec(), hex: false }))) + .unwrap(); let added = doc.add(Object::Int(42)); let updated = write_incremental(&doc, &SaveOptions::default()).unwrap(); assert!(updated.starts_with(&packed), "an incremental save only appends"); @@ -98,7 +96,7 @@ fn qpdf_accepts_object_stream_output() { assert!(out.status.success()); let doc = Document::open(Arc::new(fixture())).unwrap(); let packed = write_full(&doc, &SaveOptions::default()).unwrap(); - let path = std::env::temp_dir().join(format!("printcraft-objstm-{}.pdf", std::process::id())); + let path = std::env::temp_dir().join(format!("pdfcraft-objstm-{}.pdf", std::process::id())); std::fs::write(&path, &packed).unwrap(); let check = std::process::Command::new("qpdf").arg("--check").arg(&path).output().unwrap(); let _ = std::fs::remove_file(&path); @@ -112,8 +110,8 @@ fn objects_referenced_by_the_encrypt_dictionary_stay_outside_object_streams() { // do (pdf.js issue7665). A full save must keep them readable before decryption. let doc = Document::open(Arc::new(fixture())).unwrap(); let mut enc = doc.clone(); - enc.set_encryption(&printcraft_cos::NewEncryption { - algorithm: printcraft_cos::Algorithm::Aes256, + enc.set_encryption(&pdfcraft_cos::NewEncryption { + algorithm: pdfcraft_cos::Algorithm::Aes256, user_password: "", owner_password: "owner", permissions: -4, diff --git a/crates/create/Cargo.toml b/crates/create/Cargo.toml index 2f5e8bf3..9ee7d849 100644 --- a/crates/create/Cargo.toml +++ b/crates/create/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-create" +name = "pdfcraft-create" description = "Create PDFs: blank documents, from images (PNG, JPEG) and from plain text" version.workspace = true edition.workspace = true @@ -9,9 +9,9 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-model = { workspace = true } -printcraft-fonts = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-model = { workspace = true } +pdfcraft-fonts = { workspace = true } png = "0.18" tiff = "0.11" image = { version = "0.25", default-features = false, features = ["bmp", "gif"] } diff --git a/crates/create/README.md b/crates/create/README.md index eba6b357..01f0be61 100644 --- a/crates/create/README.md +++ b/crates/create/README.md @@ -1,4 +1,4 @@ -# printcraft-create +# pdfcraft-create Layer L4. Create a PDF (Acrobat's Create a PDF tool), execution plan M10.2: @@ -9,4 +9,4 @@ Layer L4. Create a PDF (Acrobat's Create a PDF tool), execution plan M10.2: with transparency as a soft mask; - `from_text(text, …)`: plain text set in Helvetica, wrapped and paginated. -Every function returns a `printcraft_cos::Document`; the caller writes it. +Every function returns a `pdfcraft_cos::Document`; the caller writes it. diff --git a/crates/create/src/extract.rs b/crates/create/src/extract.rs index dc4b8486..6503216f 100644 --- a/crates/create/src/extract.rs +++ b/crates/create/src/extract.rs @@ -1,11 +1,11 @@ //! Export a PDF ▸ Image ▸ Export all images, and Edit ▸ Save image as: the images that pages //! use, as files. JPEG images are written unchanged; other images are decoded and written as -//! PNG (with their soft mask as alpha). Images PrintCraft can't decode yet (JPEG 2000, JBIG2, +//! PNG (with their soft mask as alpha). Images PdfCraft can't decode yet (JPEG 2000, JBIG2, //! CCITT, separations) are reported, never silently left out. use std::collections::HashSet; -use printcraft_cos::{Dict, Document, ObjRef, Object, Stream}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream}; /// One image, ready to write. #[derive(Clone, Debug, PartialEq)] @@ -33,7 +33,7 @@ const MAX_PIXELS: u64 = 100_000_000; /// The images of `pages` (0-based; each image once, on the first page using it), skipping /// those with fewer than `min_side` pixels on their shorter side. pub fn extract_images(doc: &Document, pages: &[usize], min_side: u32) -> ImageExport { - let all = printcraft_model::pages(doc); + let all = pdfcraft_model::pages(doc); let mut out = ImageExport::default(); let mut seen = HashSet::new(); for &p in pages { diff --git a/crates/create/src/lib.rs b/crates/create/src/lib.rs index cbe21a8a..3c499ea5 100644 --- a/crates/create/src/lib.rs +++ b/crates/create/src/lib.rs @@ -1,12 +1,12 @@ -//! printcraft-create — create PDFs from nothing, images or text (L4). See the README. +//! pdfcraft-create — create PDFs from nothing, images or text (L4). See the README. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; mod extract; pub use extract::{ExtractedImage, ImageExport, extract_images, image_file}; -use printcraft_fonts::{literal, win_ansi, wrap}; +use pdfcraft_fonts::{literal, win_ansi, wrap}; #[derive(Debug, thiserror::Error, PartialEq)] pub enum CreateError { @@ -50,7 +50,7 @@ fn add_page(doc: &mut Document, w: f64, h: f64, resources: Dict, content: Option fn set_title(doc: &mut Document, title: &str) { let mut info = Dict::new(); info.set(b"Title".to_vec(), PdfString::text(title)); - info.set(b"Producer".to_vec(), PdfString::text("PrintCraft")); + info.set(b"Producer".to_vec(), PdfString::text("PdfCraft")); let r = doc.add(info); doc.trailer_mut().set(b"Info".to_vec(), Object::Ref(r)); } diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index e6407266..8b215168 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use printcraft_cos::{SaveOptions, write_full}; +use pdfcraft_cos::{SaveOptions, write_full}; use super::*; @@ -168,7 +168,7 @@ fn images_export_as_jpeg_unchanged_and_others_as_png() { fn indexed_and_one_bit_images_decode() { let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap(); // Replace the page's image with a 4×1 indexed image (two colours) and add a 1-bit mask. - let page = printcraft_model::pages(&doc)[0].clone(); + let page = pdfcraft_model::pages(&doc)[0].clone(); let res = doc.resolve(page.dict.get(b"Resources").unwrap()).as_dict().cloned().unwrap(); let xo = doc.resolve(res.get(b"XObject").unwrap()).as_dict().cloned().unwrap(); let (_, r) = xo.iter().next().map(|(k, v)| (k.clone(), v.as_ref().unwrap())).unwrap(); diff --git a/crates/crypt/Cargo.toml b/crates/crypt/Cargo.toml index eb07bf14..9edb008f 100644 --- a/crates/crypt/Cargo.toml +++ b/crates/crypt/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-crypt" +name = "pdfcraft-crypt" description = "PDF standard security handler (ISO 32000-2 §7.6): RC4 and AES, revisions 2–6" version.workspace = true edition.workspace = true diff --git a/crates/crypt/src/lib.rs b/crates/crypt/src/lib.rs index 5b03f1d6..267c8f1c 100644 --- a/crates/crypt/src/lib.rs +++ b/crates/crypt/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-crypt — the PDF standard security handler (ISO 32000-2 §7.6), standalone (L0). +//! pdfcraft-crypt — the PDF standard security handler (ISO 32000-2 §7.6), standalone (L0). //! //! Supports every revision of the standard handler: //! - R2 (40-bit RC4), R3 (RC4 up to 128-bit), R4 (crypt filters: RC4 or AES-128); diff --git a/crates/edit/Cargo.toml b/crates/edit/Cargo.toml index ab676c87..1aadd633 100644 --- a/crates/edit/Cargo.toml +++ b/crates/edit/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-edit" +name = "pdfcraft-edit" description = "Page content editing: headers and footers, watermarks, backgrounds, Bates numbers (ISO 32000-2 §14.8)" version.workspace = true edition.workspace = true @@ -9,12 +9,12 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-content = { workspace = true } -printcraft-fonts = { workspace = true } -printcraft-model = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-content = { workspace = true } +pdfcraft-fonts = { workspace = true } +pdfcraft-model = { workspace = true } thiserror = { workspace = true } [dev-dependencies] hayro-syntax = { workspace = true } -printcraft-annot = { workspace = true } +pdfcraft-annot = { workspace = true } diff --git a/crates/edit/src/added.rs b/crates/edit/src/added.rs index 111c4940..d189dedf 100644 --- a/crates/edit/src/added.rs +++ b/crates/edit/src/added.rs @@ -7,8 +7,8 @@ //! content. Boxes are in display space (origin at the bottom-left of the page as shown, after //! `/Rotate`), so items stay upright on rotated pages. -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; -use printcraft_fonts::{helvetica_width, literal, win_ansi}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; +use pdfcraft_fonts::{helvetica_width, literal, win_ansi}; use crate::{EditError, check, contents, n, page_list, place_tagged}; @@ -133,8 +133,8 @@ impl AddedImage { /// The matrix from image space (the unit square) to display space: crop, flip, rotate, /// then fill the box. - fn matrix(&self) -> printcraft_content::Matrix { - use printcraft_content::Matrix as M; + fn matrix(&self) -> pdfcraft_content::Matrix { + use pdfcraft_content::Matrix as M; let [l, b, r, t] = self.crop.map(|v| v.clamp(0.0, 0.45)); let (cw, ch) = ((1.0 - l - r).max(0.05), (1.0 - b - t).max(0.05)); let mut m = M([1.0 / cw, 0.0, 0.0, 1.0 / ch, -l / cw, -b / ch]); diff --git a/crates/edit/src/flatten.rs b/crates/edit/src/flatten.rs index d579d676..704f12b5 100644 --- a/crates/edit/src/flatten.rs +++ b/crates/edit/src/flatten.rs @@ -10,7 +10,7 @@ use std::collections::HashSet; -use printcraft_cos::{Dict, Document, ObjRef, Object}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; use crate::{EditError, check, n, page_list, place_tagged}; diff --git a/crates/edit/src/images.rs b/crates/edit/src/images.rs index b112c840..ff6f5494 100644 --- a/crates/edit/src/images.rs +++ b/crates/edit/src/images.rs @@ -3,8 +3,8 @@ //! `q … cm … Q` with the extra transform), replace (it draws another XObject in the same place), //! or delete. Only the stream that draws it is rewritten, as a new object. -use printcraft_content::{Matrix, Op, parse, serialize_ops}; -use printcraft_cos::{Dict, Document, ObjRef, Object, Stream}; +use pdfcraft_content::{Matrix, Op, parse, serialize_ops}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream}; use crate::EditError; @@ -44,8 +44,8 @@ fn streams(doc: &Document, page: &Dict) -> Vec<(Object, Vec)> { .collect() } -fn page_of(doc: &Document, page: usize) -> Result { - printcraft_model::pages(doc).into_iter().nth(page).ok_or(EditError::NoSuchPage(page)) +fn page_of(doc: &Document, page: usize) -> Result { + pdfcraft_model::pages(doc).into_iter().nth(page).ok_or(EditError::NoSuchPage(page)) } fn xobjects(doc: &Document, page: &Dict) -> Dict { @@ -149,7 +149,7 @@ pub fn change_image(doc: &mut Document, page: usize, index: usize, change: &Imag let m = pm.then(&Matrix(*t)).then(&inv); vec![ Op::new("q", vec![]), - Op::new("cm", m.0.iter().map(|v| printcraft_content::num(*v)).collect()), + Op::new("cm", m.0.iter().map(|v| pdfcraft_content::num(*v)).collect()), ops[img.op].clone(), Op::new("Q", vec![]), ] diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index d18bd692..7950184e 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-edit — page content editing (L4). Today: page marks, Acrobat's Header & footer, +//! pdfcraft-edit — page content editing (L4). Today: page marks, Acrobat's Header & footer, //! Watermark, Background and Bates numbering (execution plan M7.6). //! //! **How marks are stored.** Each mark is one content stream appended to (or, for content behind @@ -14,8 +14,8 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_cos::{Dict, Document, Object, Stream}; -use printcraft_fonts::{helvetica_width, literal, win_ansi}; +use pdfcraft_cos::{Dict, Document, Object, Stream}; +use pdfcraft_fonts::{helvetica_width, literal, win_ansi}; #[derive(Debug, thiserror::Error, PartialEq)] pub enum EditError { @@ -24,7 +24,7 @@ pub enum EditError { #[error("{0}")] Invalid(String), #[error("{0}")] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } /// The kinds of page marks. @@ -76,7 +76,7 @@ impl Default for HeaderFooter { /// the document, an image (drawn into a unit square) or a form (a page of a PDF). #[derive(Clone, Copy, Debug, PartialEq)] pub struct MarkSource { - pub xobject: printcraft_cos::ObjRef, + pub xobject: pdfcraft_cos::ObjRef, /// Its natural size in points. pub size: (f64, f64), /// An image XObject (unit square); otherwise a form whose `/Matrix` maps it to `size`. @@ -154,7 +154,7 @@ fn picture(src: &MarkSource, page: (f64, f64), scale: f64, rotation: f64, offset } /// Register a mark picture in the page's own resources (`/PCPic`). -fn add_picture_resource(doc: &mut Document, page: &printcraft_model::Page, src: &MarkSource) -> Result<(), EditError> { +fn add_picture_resource(doc: &mut Document, page: &pdfcraft_model::Page, src: &MarkSource) -> Result<(), EditError> { let p = doc.get(page.obj).as_dict().cloned().unwrap_or_default(); let mut res = p.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default(); let mut xo = res.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default(); @@ -238,8 +238,8 @@ fn bates_start(template: &str) -> Option { t.split('#').nth(2).and_then(|s| s.parse().ok()).or(Some(1)) } -fn page_list(doc: &Document) -> Vec { - printcraft_model::pages(doc) +fn page_list(doc: &Document) -> Vec { + pdfcraft_model::pages(doc) } fn check(pages: &[usize], count: usize) -> Result<(), EditError> { @@ -250,7 +250,7 @@ fn check(pages: &[usize], count: usize) -> Result<(), EditError> { } /// The page's `/Contents` as a list of stream references (inline content is promoted). -fn contents(doc: &mut Document, page: &printcraft_model::Page) -> Result, EditError> { +fn contents(doc: &mut Document, page: &pdfcraft_model::Page) -> Result, EditError> { let obj = doc.get(page.obj); let d = obj.as_dict().cloned().unwrap_or_default(); Ok(match d.get(b"Contents").cloned() { @@ -265,7 +265,7 @@ fn contents(doc: &mut Document, page: &printcraft_model::Page) -> Result, content: Option<&[u8]>) -> Result<(), EditError> { +fn add_resources(doc: &mut Document, page: &pdfcraft_model::Page, opacity: Option, content: Option<&[u8]>) -> Result<(), EditError> { let mut res = page.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default(); let mut fonts = res.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default(); for (name, base) in [(&b"PCHelv"[..], "Helvetica"), (b"PCTimes", "Times-Roman"), (b"PCCour", "Courier")] { @@ -296,8 +296,8 @@ fn add_resources(doc: &mut Document, page: &printcraft_model::Page, opacity: Opt Ok(()) } -const WRAP_OPEN: &[u8] = b"q %PrintCraft\n"; -const WRAP_CLOSE: &[u8] = b"Q %PrintCraft\n"; +const WRAP_OPEN: &[u8] = b"q %PdfCraft\n"; +const WRAP_CLOSE: &[u8] = b"Q %PdfCraft\n"; #[cfg(test)] fn stream_bytes(doc: &Document, o: &Object) -> Option> { @@ -322,12 +322,12 @@ fn tagged(tag: &str) -> Dict { /// Put a mark's content on a page: behind (prepended) or on top (appended, after wrapping the /// original content in q/Q). -fn place(doc: &mut Document, page: &printcraft_model::Page, kind: MarkKind, content: Vec, behind: bool) -> Result<(), EditError> { +fn place(doc: &mut Document, page: &pdfcraft_model::Page, kind: MarkKind, content: Vec, behind: bool) -> Result<(), EditError> { place_tagged(doc, page, kind.tag(), content, behind) } /// Put content on a page, tagged `tag` (see [`place`]). -fn place_tagged(doc: &mut Document, page: &printcraft_model::Page, tag: &str, content: Vec, behind: bool) -> Result<(), EditError> { +fn place_tagged(doc: &mut Document, page: &pdfcraft_model::Page, tag: &str, content: Vec, behind: bool) -> Result<(), EditError> { let mut list = contents(doc, page)?; let mark = Object::Ref(doc.add(Object::Stream(Stream::flate(tagged(tag), &content)))); if behind { diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index fb072014..f8a5d67e 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use printcraft_cos::{Document, SaveOptions, write_incremental}; +use pdfcraft_cos::{Document, SaveOptions, write_incremental}; use super::*; @@ -40,7 +40,7 @@ fn reopen(doc: &Document) -> Document { /// The decoded content streams of a page, in order. fn streams(doc: &Document, page: usize) -> Vec { - let p = &printcraft_model::pages(doc)[page]; + let p = &pdfcraft_model::pages(doc)[page]; let c = p.dict.get(b"Contents").cloned(); let list = match c.map(|c| doc.resolve(&c)).as_deref() { Some(Object::Array(a)) => a.clone(), @@ -77,7 +77,7 @@ fn header_and_footer_are_drawn_in_display_space_and_wrap_the_original_content() let s0 = streams(&doc, 0); // q-wrapper, original, Q-wrapper, header/footer. assert_eq!(s0.len(), 4, "{s0:?}"); - assert_eq!((s0[0].as_str(), s0[2].as_str()), ("q %PrintCraft\n", "Q %PrintCraft\n")); + assert_eq!((s0[0].as_str(), s0[2].as_str()), ("q %PdfCraft\n", "Q %PdfCraft\n")); let mark = &s0[3]; assert!(mark.contains("/PCMark /HeaderFooter") && mark.contains("(Page 1 of 3) Tj") && mark.contains("(Confidential \\(draft\\)) Tj"), "{mark}"); assert!(mark.contains("1 0 0 1 0 0 cm"), "upright page: identity"); @@ -86,11 +86,11 @@ fn header_and_footer_are_drawn_in_display_space_and_wrap_the_original_content() assert!(s1.last().unwrap().contains("0 1 -1 0 600 0 cm") && s1.last().unwrap().contains("(Page 2 of 3)"), "{s1:?}"); // A page without content gets just the mark; its inherited resources are copied, not changed. assert_eq!(streams(&doc, 2).len(), 1); - let p2 = &printcraft_model::pages(&doc)[2]; + let p2 = &pdfcraft_model::pages(&doc)[2]; let res = doc.resolve(p2.dict.get(b"Resources").unwrap()); let fonts = doc.resolve(res.as_dict().unwrap().get(b"Font").unwrap()); assert!(fonts.as_dict().unwrap().contains(b"PCHelv") && fonts.as_dict().unwrap().contains(b"F1")); - let shared = doc.get(printcraft_cos::ObjRef::new(6, 0)); + let shared = doc.get(pdfcraft_cos::ObjRef::new(6, 0)); let shared_fonts = doc.resolve(shared.as_dict().unwrap().get(b"Font").unwrap()); assert!(!shared_fonts.as_dict().unwrap().contains(b"PCHelv"), "the shared dictionary is untouched"); assert_eq!(marks_present(&doc), [MarkKind::HeaderFooter]); @@ -145,7 +145,7 @@ fn invalid_requests_change_nothing() { #[test] fn flattening_draws_appearances_into_the_page_and_removes_the_comments() { - use printcraft_annot::{Meta, NewAnnotation, NoteIcon, Shape, Style, add_annotation, add_reply}; + use pdfcraft_annot::{Meta, NewAnnotation, NoteIcon, Shape, Style, add_annotation, add_reply}; let mut doc = fixture(); let meta = Meta { date: None, id: "x".into() }; let add = |doc: &mut Document, shape: Shape| { @@ -159,7 +159,7 @@ fn flattening_draws_appearances_into_the_page_and_removes_the_comments() { let n = flatten(&mut doc, &[0], true, false).unwrap(); assert_eq!(n, 2, "the rectangle and the note icon are drawn; the reply has nothing to draw"); let doc = reopen(&doc); - let p = &printcraft_model::pages(&doc)[0]; + let p = &pdfcraft_model::pages(&doc)[0]; assert!(!p.dict.contains(b"Annots"), "comments, pop-up and reply are gone"); let s = streams(&doc, 0); assert_eq!(s.len(), before.len() + 3, "wrapped original + flattened content: {s:?}"); @@ -281,7 +281,7 @@ fn descriptor_text_page() -> Document { /// The Japanese fallback face comes from craft-fonts, an optional build input. fn without_craft_fonts(test: &str) -> bool { - if printcraft_fonts::document_japanese_font().is_some() { + if pdfcraft_fonts::document_japanese_font().is_some() { return false; } eprintln!("skipping {test}: built without craft-fonts (set CRAFT_FONTS_DIR to run it)"); @@ -324,7 +324,7 @@ fn japanese_edit_without_craft_fonts_is_a_clear_error() { let before = page_content_bytes(&doc, 0); let line = text::replace_line(&mut doc, 0, 0, "日本語の文字"); let block = text::replace_block(&mut doc, 0, 0, "日本語の文字"); - if printcraft_fonts::document_japanese_font().is_some() { + if pdfcraft_fonts::document_japanese_font().is_some() { eprintln!("built with craft-fonts: the Japanese edits succeed (checked by the tests above)"); assert!(line.is_ok() && block.is_ok()); return; @@ -411,7 +411,7 @@ fn paragraphs_are_found_and_rewrapped() { assert_eq!(blocks[0].lines, [0, 1, 2]); let width = blocks[0].rect[2] - blocks[0].rect[0]; let next = blocks[1].rect; - let long = "PrintCraft rewraps a paragraph to its own width when its text changes, keeping the font, size, colour and line spacing."; + let long = "PdfCraft rewraps a paragraph to its own width when its text changes, keeping the font, size, colour and line spacing."; assert_eq!(text::replace_block(&mut doc, 0, 0, long).unwrap().substituted, None); let doc = reopen(&doc); let lines = text::text_lines(&doc, 0).unwrap(); @@ -438,14 +438,14 @@ fn paragraphs_are_found_and_rewrapped() { } fn page_content_bytes(doc: &Document, page: usize) -> Vec { - let p = printcraft_model::pages(doc).swap_remove(page); + let p = pdfcraft_model::pages(doc).swap_remove(page); let c = p.dict.get(b"Contents").unwrap(); match &*doc.resolve(c) { - printcraft_cos::Object::Stream(s) => s.decoded().unwrap(), - printcraft_cos::Object::Array(a) => a + pdfcraft_cos::Object::Stream(s) => s.decoded().unwrap(), + pdfcraft_cos::Object::Array(a) => a .iter() .flat_map(|x| match &*doc.resolve(x) { - printcraft_cos::Object::Stream(s) => s.decoded().unwrap(), + pdfcraft_cos::Object::Stream(s) => s.decoded().unwrap(), _ => Vec::new(), }) .collect(), @@ -505,16 +505,16 @@ fn page_images_move_turn_replace_and_delete() { // The text after it is untouched. assert_eq!(text::text_lines(&doc, 0).unwrap()[0].text, "Caption"); // Replace with another image object, in the same place. - let mut d = printcraft_cos::Dict::new(); + let mut d = pdfcraft_cos::Dict::new(); for (k, v) in [ - (&b"Type"[..], printcraft_cos::Object::name("XObject")), - (b"Subtype", printcraft_cos::Object::name("Image")), - (b"Width", printcraft_cos::Object::Int(1)), - (b"Height", printcraft_cos::Object::Int(1)), + (&b"Type"[..], pdfcraft_cos::Object::name("XObject")), + (b"Subtype", pdfcraft_cos::Object::name("Image")), + (b"Width", pdfcraft_cos::Object::Int(1)), + (b"Height", pdfcraft_cos::Object::Int(1)), ] { d.set(k.to_vec(), v); } - let other = doc.add(printcraft_cos::Object::Stream(printcraft_cos::Stream::from_raw(d, vec![0]))); + let other = doc.add(pdfcraft_cos::Object::Stream(pdfcraft_cos::Stream::from_raw(d, vec![0]))); images::change_image(&mut doc, 0, 0, &images::ImageChange::Replace(other)).unwrap(); let imgs = images::page_images(&doc, 0).unwrap(); assert_eq!((imgs[0].object, imgs[0].width), (Some(other), 1)); @@ -566,13 +566,13 @@ fn a_new_paragraph_colour_does_not_spill_into_the_text_after_it() { text::rewrite_block(&mut doc, 0, 0, None, &style).unwrap(); let doc = reopen(&doc); // The fill colour in force where the second paragraph is shown (q/Q nest it). - let ops = printcraft_content::parse(&page_content_bytes(&doc, 0)).ops; + let ops = pdfcraft_content::parse(&page_content_bytes(&doc, 0)).ops; let (mut fill, mut stack) = (String::from("0 g"), Vec::new()); for op in &ops { match op.op.as_slice() { b"q" => stack.push(fill.clone()), b"Q" => fill = stack.pop().unwrap_or_default(), - b"g" | b"rg" | b"k" => fill = String::from_utf8_lossy(&printcraft_content::serialize_ops(std::slice::from_ref(op))).trim().to_string(), + b"g" | b"rg" | b"k" => fill = String::from_utf8_lossy(&pdfcraft_content::serialize_ops(std::slice::from_ref(op))).trim().to_string(), b"Tj" if op.operands.first().and_then(|o| o.as_string()).is_some_and(|s| s.to_text() == "Second paragraph") => break, _ => {} } @@ -609,7 +609,7 @@ fn recolouring_a_paragraph_in_a_shared_text_object_keeps_order_and_nesting() { let doc = reopen(&doc); let texts: Vec = text::text_blocks(&doc, 0).unwrap().into_iter().map(|b| b.text).collect(); assert_eq!(texts, ["First paragraph", "Second paragraph", "Third paragraph"]); - let ops = printcraft_content::parse(&page_content_bytes(&doc, 0)).ops; + let ops = pdfcraft_content::parse(&page_content_bytes(&doc, 0)).ops; let (mut in_text, mut depth, mut red) = (false, 0usize, Vec::new()); for op in &ops { match op.op.as_slice() { diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 987e59a2..2454be81 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -13,10 +13,10 @@ use std::collections::HashMap; use std::rc::Rc; -use printcraft_content::{Matrix, Op, parse, serialize_ops}; -use printcraft_cos::{Dict, Document, Object, PdfString, Stream}; -use printcraft_fonts::pdf::Metrics; -use printcraft_fonts::{GlyphError, japanese_glyph}; +use pdfcraft_content::{Matrix, Op, parse, serialize_ops}; +use pdfcraft_cos::{Dict, Document, Object, PdfString, Stream}; +use pdfcraft_fonts::pdf::Metrics; +use pdfcraft_fonts::{GlyphError, japanese_glyph}; use crate::EditError; @@ -71,7 +71,7 @@ impl TextState { /// Operators that set this state (inside a text object). fn ops(&self) -> Vec { - let n = printcraft_content::num; + let n = pdfcraft_content::num; let mut v = Vec::new(); if let Some((f, size)) = &self.font { v.push(Op::new("Tf", vec![Object::name(f), n(*size)])); @@ -167,8 +167,8 @@ fn content_streams(doc: &Document, page: &Dict) -> Vec<(Object, Vec)> { .collect() } -fn page_dict(doc: &Document, page: usize) -> Result { - printcraft_model::pages(doc).into_iter().nth(page).ok_or(EditError::NoSuchPage(page)) +fn page_dict(doc: &Document, page: usize) -> Result { + pdfcraft_model::pages(doc).into_iter().nth(page).ok_or(EditError::NoSuchPage(page)) } fn fill_color(fill: &[Op]) -> [f64; 3] { @@ -516,14 +516,14 @@ fn unicode_hex(ch: char) -> String { /// This build has no Japanese face to draw replacement text with. fn no_japanese_font() -> EditError { EditError::Invalid( - "this text needs PrintCraft's Japanese fallback font, which this build doesn't include \ + "this text needs PdfCraft's Japanese fallback font, which this build doesn't include \ (official releases do; to build it in, set CRAFT_FONTS_DIR to a craft-fonts checkout)" .into(), ) } fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result { - let family = printcraft_fonts::document_japanese_font().ok_or_else(no_japanese_font)?.family; + let family = pdfcraft_fonts::document_japanese_font().ok_or_else(no_japanese_font)?.family; let mut chars = Vec::new(); for ch in text.chars() { if !chars.contains(&ch) { @@ -628,12 +628,12 @@ pub fn replace_line(doc: &mut Document, page: usize, line: usize, text: &str) -> let bytes = type3_encode(&fallback, &text) .ok_or_else(|| EditError::Invalid(format!("\"{text}\" can't be shown by the Japanese fallback")))?; let size = font_size_before(&ops, first).unwrap_or(target.size); - replacement.push(Op::new("Tf", vec![Object::name(&fallback.name), printcraft_content::num(size)])); + replacement.push(Op::new("Tf", vec![Object::name(&fallback.name), pdfcraft_content::num(size)])); replacement.push(Op::new("Tj", vec![Object::String(PdfString::literal(bytes))])); - replacement.push(Op::new("Tf", vec![Object::name(&target.font), printcraft_content::num(size)])); + replacement.push(Op::new("Tf", vec![Object::name(&target.font), pdfcraft_content::num(size)])); substituted = Some(format!("{} Type3", fallback.family)); } else { - let win = printcraft_fonts::win_ansi(&text); + let win = pdfcraft_fonts::win_ansi(&text); // WinAnsi turns what it can't show into '?'; refuse rather than print the wrong thing. let back: String = win.iter().map(|b| char::from_u32(u32::from(*b)).unwrap_or('?')).collect(); if text.chars().zip(back.chars()).any(|(a, b)| b == '?' && a != '?') { @@ -644,9 +644,9 @@ pub fn replace_line(doc: &mut Document, page: usize, line: usize, text: &str) -> let family = source_family(&target.base_font); let base = family.base_font(target.bold, target.italic); let substitute_name = format!("PCEd{}", base.replace('-', "")); - replacement.push(Op::new("Tf", vec![Object::name(&substitute_name), printcraft_content::num(size)])); + replacement.push(Op::new("Tf", vec![Object::name(&substitute_name), pdfcraft_content::num(size)])); replacement.push(Op::new("Tj", vec![Object::String(PdfString::literal(win))])); - replacement.push(Op::new("Tf", vec![Object::name(&target.font), printcraft_content::num(size)])); + replacement.push(Op::new("Tf", vec![Object::name(&target.font), pdfcraft_content::num(size)])); substituted = Some(base.to_string()); let mut f = Dict::new(); f.set(b"Type".to_vec(), Object::name("Font")); @@ -864,8 +864,8 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option let std_width = move |s: &str, size: f64| -> f64 { match family { crate::added::Family::Courier => s.chars().count() as f64 * 0.6 * size, - crate::added::Family::Times => printcraft_fonts::helvetica_width(s, size) * 0.92, - crate::added::Family::Helvetica => printcraft_fonts::helvetica_width(s, size) * if bold { 1.05 } else { 1.0 }, + crate::added::Family::Times => pdfcraft_fonts::helvetica_width(s, size) * 0.92, + crate::added::Family::Helvetica => pdfcraft_fonts::helvetica_width(s, size) * if bold { 1.05 } else { 1.0 }, } }; let [dx, dy] = style.offset.unwrap_or([0.0, 0.0]); @@ -911,7 +911,7 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option substituted = Some(format!("{} Type3", fallback.family)); (name, Box::new(move |s: &str| type3_encode(&encoder, s))) } else { - let win = printcraft_fonts::win_ansi(&text); + let win = pdfcraft_fonts::win_ansi(&text); let back: String = win.iter().map(|c| char::from_u32(u32::from(*c)).unwrap_or('?')).collect(); let base = family.base_font(bold, italic); if text.chars().zip(back.chars()).any(|(a, c)| c == '?' && a != '?') { @@ -927,7 +927,7 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option if style.family.is_none() { substituted = Some(base.to_string()); } - (name, Box::new(|s: &str| Some(printcraft_fonts::win_ansi(s)))) + (name, Box::new(|s: &str| Some(pdfcraft_fonts::win_ansi(s)))) }; // Line spacing in text space: the paragraph's own (scaled with the size), or 1.2 × the size. let lead = match style.line_spacing { @@ -936,7 +936,7 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option None if o_state.leading > 0.0 => o_state.leading, None => size * 1.2, }; - let n = printcraft_content::num; + let n = pdfcraft_content::num; // In its own graphics state, so a new colour (or anything else) stops at the paragraph. let mut block_ops = vec![Op::new("q", vec![])]; // Moved: a translation inside that state. The move is in page space, so it is taken back diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index e104aa77..7a60170f 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "printcraft-engine" -description = "PrintCraft engine façade: session, documents, command/tool catalogue" +name = "pdfcraft-engine" +description = "PdfCraft engine façade: session, documents, command/tool catalogue" version.workspace = true edition.workspace = true license.workspace = true @@ -10,26 +10,26 @@ workspace = true [dependencies] image = { version = "0.25", default-features = false, features = ["jpeg", "tiff"] } -printcraft-render = { workspace = true } -printcraft-cos = { workspace = true } -printcraft-organize = { workspace = true } -printcraft-annot = { workspace = true } -printcraft-forms = { workspace = true } -printcraft-edit = { workspace = true } -printcraft-redact = { workspace = true } -printcraft-print = { workspace = true } -printcraft-xfdf = { workspace = true } -printcraft-sign = { workspace = true } -printcraft-optimize = { workspace = true } -printcraft-a11y = { workspace = true } -printcraft-ocr = { workspace = true } -printcraft-js = { workspace = true } -printcraft-compare = { workspace = true } -printcraft-preflight = { workspace = true } -printcraft-export = { workspace = true } -printcraft-fonts = { workspace = true } -printcraft-create = { workspace = true } -printcraft-model = { workspace = true } +pdfcraft-render = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-organize = { workspace = true } +pdfcraft-annot = { workspace = true } +pdfcraft-forms = { workspace = true } +pdfcraft-edit = { workspace = true } +pdfcraft-redact = { workspace = true } +pdfcraft-print = { workspace = true } +pdfcraft-xfdf = { workspace = true } +pdfcraft-sign = { workspace = true } +pdfcraft-optimize = { workspace = true } +pdfcraft-a11y = { workspace = true } +pdfcraft-ocr = { workspace = true } +pdfcraft-js = { workspace = true } +pdfcraft-compare = { workspace = true } +pdfcraft-preflight = { workspace = true } +pdfcraft-export = { workspace = true } +pdfcraft-fonts = { workspace = true } +pdfcraft-create = { workspace = true } +pdfcraft-model = { workspace = true } thiserror = { workspace = true } png = "0.18" diff --git a/crates/engine/src/actions.rs b/crates/engine/src/actions.rs index 938a8d4b..f679cf87 100644 --- a/crates/engine/src/actions.rs +++ b/crates/engine/src/actions.rs @@ -117,7 +117,7 @@ pub struct Action { pub name: String, pub description: String, pub steps: Vec, - /// One of PrintCraft's own actions (can't be edited or deleted). + /// One of PdfCraft's own actions (can't be edited or deleted). pub builtin: bool, } diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index dc3d6b2c..a941f85e 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -14,7 +14,7 @@ pub enum Availability { Ready, /// Planned; ships in the named milestone. Planned(&'static str), - /// Cloud-only in Acrobat; PrintCraft offers an optional pluggable provider instead. + /// Cloud-only in Acrobat; PdfCraft offers an optional pluggable provider instead. Provider, } diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index e22f2be5..1812a91b 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -298,11 +298,11 @@ pub const COMMANDS: &[CommandSpec] = &[ c("page.number", "Number pages…", PAGES, None, Assembly, "hash"), c("help.shortcuts", "Keyboard shortcuts", HELP, None, Nothing, "circle-help"), c("help.discord", "Join the ArtCraft Discord", HELP, None, Nothing, "messages-square"), - c("help.app_page", "PrintCraft web page", HELP, None, Nothing, "globe"), - c("help.github", "PrintCraft on GitHub", HELP, None, Nothing, "code-xml"), + c("help.app_page", "PdfCraft web page", HELP, None, Nothing, "globe"), + c("help.github", "PdfCraft on GitHub", HELP, None, Nothing, "code-xml"), c("help.website", "ArtCraft website", HELP, None, Nothing, "external-link"), c("help.check_updates", "Check for updates…", HELP, None, Nothing, "cloud"), - c("help.about", "About PrintCraft", HELP, None, Nothing, "info"), + c("help.about", "About PdfCraft", HELP, None, Nothing, "info"), ]; pub fn command(id: &str) -> Option<&'static CommandSpec> { diff --git a/crates/engine/src/compare.rs b/crates/engine/src/compare.rs index 88e565ee..bfcf578b 100644 --- a/crates/engine/src/compare.rs +++ b/crates/engine/src/compare.rs @@ -3,7 +3,7 @@ use std::sync::Arc; -pub use printcraft_compare::{Change, Comparison, Kind, Side}; +pub use pdfcraft_compare::{Change, Comparison, Kind, Side}; use crate::{DocId, Edit, EditError, Markup, NewAnnotation, NoteIcon, Session, Shape}; @@ -18,25 +18,24 @@ pub fn colour(kind: Kind) -> crate::Rgb { impl crate::Document { /// Standards ▸ Verify PDF/A: the rules the document breaks for `level`. - pub fn pdfa_verify(&self, level: printcraft_preflight::Level) -> Vec { - self.editor.as_ref().map(|e| printcraft_preflight::verify(&e.cos, level)).unwrap_or_default() + pub fn pdfa_verify(&self, level: pdfcraft_preflight::Level) -> Vec { + self.editor.as_ref().map(|e| pdfcraft_preflight::verify(&e.cos, level)).unwrap_or_default() } /// The standards the document declares (Standards panel). - pub fn standards(&self) -> printcraft_preflight::Declared { - self.editor.as_ref().map(|e| printcraft_preflight::declared(&e.cos)).unwrap_or_default() + pub fn standards(&self) -> pdfcraft_preflight::Declared { + self.editor.as_ref().map(|e| pdfcraft_preflight::declared(&e.cos)).unwrap_or_default() } /// Every word of the document in reading order, with page and box. - pub fn words(&self) -> Vec { - let config = printcraft_render::RenderConfig { password: self.password.as_deref().map(Arc::from), ..Default::default() }; - let mut r = printcraft_render::PageRenderer::new(self.bytes.clone(), config); + pub fn words(&self) -> Vec { + let config = pdfcraft_render::RenderConfig { password: self.password.as_deref().map(Arc::from), ..Default::default() }; + let mut r = pdfcraft_render::PageRenderer::new(self.bytes.clone(), config); let mut out = Vec::new(); for (page, info) in self.info.pages.iter().enumerate() { - let res = - r.render(printcraft_render::RenderRequest { page, kind: printcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() }); + let res = r.render(pdfcraft_render::RenderRequest { page, kind: pdfcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() }); if let Some(t) = res.text { - out.extend(crate::js::page_words(&t, info).into_iter().map(|(text, rect)| printcraft_compare::Word { text, page, rect })); + out.extend(crate::js::page_words(&t, info).into_iter().map(|(text, rect)| pdfcraft_compare::Word { text, page, rect })); } } out @@ -48,7 +47,7 @@ impl Session { pub fn compare(&self, old: DocId, new: DocId) -> Result { let a = self.get(old).ok_or(EditError::NoDocument)?; let b = self.get(new).ok_or(EditError::NoDocument)?; - Ok(printcraft_compare::compare(&a.words(), &b.words())) + Ok(pdfcraft_compare::compare(&a.words(), &b.words())) } /// Visual compare: regions where page n of `new` looks different from page n of `old` @@ -57,23 +56,23 @@ impl Session { let a = self.get(old).ok_or(EditError::NoDocument)?; let b = self.get(new).ok_or(EditError::NoDocument)?; let renderer = |d: &crate::Document| { - printcraft_render::PageRenderer::new( + pdfcraft_render::PageRenderer::new( d.bytes.clone(), - printcraft_render::RenderConfig { password: d.password.as_deref().map(Arc::from), ..Default::default() }, + pdfcraft_render::RenderConfig { password: d.password.as_deref().map(Arc::from), ..Default::default() }, ) }; let (mut ra, mut rb) = (renderer(a), renderer(b)); let scale = dpi.clamp(18.0, 150.0) / 72.0; let mut out = Vec::new(); for page in 0..a.info.pages.len().min(b.info.pages.len()) { - let req = printcraft_render::RenderRequest { page, scale, ..Default::default() }; + let req = pdfcraft_render::RenderRequest { page, scale, ..Default::default() }; let (x, y) = (ra.render(req), rb.render(req)); if x.error.is_some() || y.error.is_some() { continue; } let info = &b.info.pages[page]; let s = y.width as f32 / info.width.max(1e-3); - for r in printcraft_compare::visual_regions((&y.rgba, y.width, y.height), (&x.rgba, x.width, x.height), 24) { + for r in pdfcraft_compare::visual_regions((&y.rgba, y.width, y.height), (&x.rgba, x.width, x.height), 24) { let p = info.view_to_user(r[0] as f32 / s, r[1] as f32 / s); let q = info.view_to_user(r[2] as f32 / s, r[3] as f32 / s); out.push((page, [p[0].min(q[0]) as f64, p[1].min(q[1]) as f64, p[0].max(q[0]) as f64, p[1].max(q[1]) as f64])); @@ -86,7 +85,7 @@ impl Session { pub fn compare_report(&self, old: DocId, new: DocId) -> Result>, EditError> { let c = self.compare(old, new)?; let name = |id| self.get(id).map(|d| d.name.clone()).unwrap_or_default(); - self.create_from_text("Compare Report", &printcraft_compare::report(&c, &name(old), &name(new))) + self.create_from_text("Compare Report", &pdfcraft_compare::report(&c, &name(old), &name(new))) } /// Mark the differences in `new` as comments: highlights over replaced and inserted text @@ -152,20 +151,20 @@ impl OfficeFormat { impl crate::Document { /// The pages as paragraphs and images (for Word, HTML and RTF export). - pub fn export_pages(&self) -> Vec { + pub fn export_pages(&self) -> Vec { let Some(cos) = self.editor.as_ref().map(|e| &e.cos) else { return Vec::new() }; self.info .pages .iter() .enumerate() .map(|(i, info)| { - let blocks = printcraft_edit::text_blocks(cos, i) + let blocks = pdfcraft_edit::text_blocks(cos, i) .unwrap_or_default() .into_iter() .filter(|b| !b.text.trim().is_empty()) .map(|b| { let f = b.base_font.to_ascii_lowercase(); - printcraft_export::Block { + pdfcraft_export::Block { text: b.text, rect: b.rect, size: b.size, @@ -180,10 +179,10 @@ impl crate::Document { .enumerate() .filter_map(|(k, im)| { let (ext, bytes) = self.page_image_file(i, k).ok()?; - Some(printcraft_export::Image { ext: if ext == "jpg" { "jpg" } else { "png" }, bytes, rect: im.rect }) + Some(pdfcraft_export::Image { ext: if ext == "jpg" { "jpg" } else { "png" }, bytes, rect: im.rect }) }) .collect(); - printcraft_export::Page { width: info.width as f64, height: info.height as f64, blocks, images } + pdfcraft_export::Page { width: info.width as f64, height: info.height as f64, blocks, images } }) .collect() } @@ -193,9 +192,9 @@ impl crate::Document { let pages = self.export_pages(); let title = self.info.title.clone().unwrap_or_else(|| self.name.trim_end_matches(".pdf").to_string()); match format { - OfficeFormat::Docx => printcraft_export::docx(&pages, &title), - OfficeFormat::Html => printcraft_export::html(&pages, &title).into_bytes(), - OfficeFormat::Rtf => printcraft_export::rtf(&pages).into_bytes(), + OfficeFormat::Docx => pdfcraft_export::docx(&pages, &title), + OfficeFormat::Html => pdfcraft_export::html(&pages, &title).into_bytes(), + OfficeFormat::Rtf => pdfcraft_export::rtf(&pages).into_bytes(), } } } diff --git a/crates/engine/src/export.rs b/crates/engine/src/export.rs index e36f0865..aa4e062d 100644 --- a/crates/engine/src/export.rs +++ b/crates/engine/src/export.rs @@ -4,7 +4,7 @@ //! text, from a //! document's working file (so unsaved edits and hidden layers are respected, as on screen). -use printcraft_render::{PageRenderer, RenderRequest, RequestKind}; +use pdfcraft_render::{PageRenderer, RenderRequest, RequestKind}; use crate::Document; @@ -18,7 +18,7 @@ pub struct Exporter { #[derive(Clone)] pub struct ExportSource { pub bytes: std::sync::Arc>, - pub config: printcraft_render::RenderConfig, + pub config: pdfcraft_render::RenderConfig, pub pages: usize, } @@ -31,13 +31,13 @@ impl Document { /// Export all images: the images `pages` (0-based) use, each once, skipping those under /// `min_side` pixels on their shorter side. JPEGs come out unchanged, other images as PNG. -pub fn extract_images(src: &ExportSource, pages: &[usize], min_side: u32) -> Result { - let doc = printcraft_cos::Document::open_with_password(src.bytes.clone(), src.config.password.as_deref()).map_err(|e| e.to_string())?; - Ok(printcraft_create::extract_images(&doc, pages, min_side)) +pub fn extract_images(src: &ExportSource, pages: &[usize], min_side: u32) -> Result { + let doc = pdfcraft_cos::Document::open_with_password(src.bytes.clone(), src.config.password.as_deref()).map_err(|e| e.to_string())?; + Ok(pdfcraft_create::extract_images(&doc, pages, min_side)) } /// The file name for the `index`-th (1-based) exported image: `_Page__Image_.`. -pub fn image_file_name(stem: &str, image: &printcraft_create::ExtractedImage, index: usize) -> String { +pub fn image_file_name(stem: &str, image: &pdfcraft_create::ExtractedImage, index: usize) -> String { format!("{stem}_Page_{}_Image_{index:04}.{}", image.page + 1, image.extension) } diff --git a/crates/engine/src/js.rs b/crates/engine/src/js.rs index 7618d029..809c44cc 100644 --- a/crates/engine/src/js.rs +++ b/crates/engine/src/js.rs @@ -1,13 +1,13 @@ //! Acrobat JavaScript in documents: field scripts run during form edits ([`JsRunner`], wired -//! into `forms` through its [`printcraft_forms::Scripts`] hook), and button actions and console +//! into `forms` through its [`pdfcraft_forms::Scripts`] hook), and button actions and console //! input run on request ([`Session::run_javascript`]). What scripts ask of the viewer (alerts, //! printing, navigation, links) is kept for the front end in [`JsOutput`]. //! //! Preferences ▸ JavaScript ▸ Enable Acrobat JavaScript is [`Session::set_javascript`]; with it //! off, scripts don't run and AF calls keep working natively. -use printcraft_forms::{Field, FieldChange, FieldEvent, FieldKind, ScriptResult, Scripts}; -pub use printcraft_js::{Limits, Outcome, Request}; +use pdfcraft_forms::{Field, FieldChange, FieldEvent, FieldKind, ScriptResult, Scripts}; +pub use pdfcraft_js::{Limits, Outcome, Request}; use crate::{DocId, Edit, EditError, Session}; @@ -41,8 +41,8 @@ impl JsOutput { } } -fn kind(k: FieldKind) -> printcraft_js::FieldType { - use printcraft_js::FieldType as T; +fn kind(k: FieldKind) -> pdfcraft_js::FieldType { + use pdfcraft_js::FieldType as T; match k { FieldKind::Text => T::Text, FieldKind::CheckBox => T::CheckBox, @@ -55,25 +55,25 @@ fn kind(k: FieldKind) -> printcraft_js::FieldType { } /// `field.display` from the first widget's annotation flags. -fn display(doc: &printcraft_cos::Document, f: &Field) -> i32 { +fn display(doc: &pdfcraft_cos::Document, f: &Field) -> i32 { let flags = f.widgets.first().and_then(|w| doc.get(w.obj).as_dict().and_then(|d| d.int(b"F"))).unwrap_or(4); if flags & 2 != 0 { - printcraft_js::DISPLAY_HIDDEN + pdfcraft_js::DISPLAY_HIDDEN } else if flags & 32 != 0 { - printcraft_js::DISPLAY_NO_VIEW + pdfcraft_js::DISPLAY_NO_VIEW } else if flags & 4 == 0 { - printcraft_js::DISPLAY_NO_PRINT + pdfcraft_js::DISPLAY_NO_PRINT } else { - printcraft_js::DISPLAY_VISIBLE + pdfcraft_js::DISPLAY_VISIBLE } } /// A form field as scripts see it. -pub fn field_state(doc: &printcraft_cos::Document, f: &Field) -> printcraft_js::FieldState { - let mut s = printcraft_js::FieldState::new(f.name.clone(), kind(f.kind), f.value.clone()); +pub fn field_state(doc: &pdfcraft_cos::Document, f: &Field) -> pdfcraft_js::FieldState { + let mut s = pdfcraft_js::FieldState::new(f.name.clone(), kind(f.kind), f.value.clone()); s.default = f.default.clone(); s.readonly = f.read_only(); - s.required = f.has(printcraft_forms::flags::REQUIRED); + s.required = f.has(pdfcraft_forms::flags::REQUIRED); s.display = display(doc, f); s.char_limit = f.max_len; s.options = match f.kind { @@ -84,7 +84,7 @@ pub fn field_state(doc: &printcraft_cos::Document, f: &Field) -> printcraft_js:: } /// The changes a script made, against the fields as they were. -fn changes(before: &[printcraft_js::FieldState], after: &[printcraft_js::FieldState]) -> Vec { +fn changes(before: &[pdfcraft_js::FieldState], after: &[pdfcraft_js::FieldState]) -> Vec { after .iter() .filter_map(|a| { @@ -103,27 +103,26 @@ fn changes(before: &[printcraft_js::FieldState], after: &[printcraft_js::FieldSt /// Runs field scripts with the document's object model and keeps what they print or ask for. pub struct JsRunner { - pub doc: printcraft_js::DocInfo, + pub doc: pdfcraft_js::DocInfo, pub doc_scripts: Vec, pub output: JsOutput, /// The fields in the document as the forms code last read them (for `display`). - cos: printcraft_cos::Document, + cos: pdfcraft_cos::Document, } impl JsRunner { - pub fn new(cos: &printcraft_cos::Document, file_name: &str) -> JsRunner { - let doc = - printcraft_js::DocInfo { file_name: file_name.to_string(), num_pages: printcraft_model::pages(cos).len(), page: 0, info: info(cos) }; - JsRunner { doc, doc_scripts: printcraft_forms::document_scripts(cos), output: JsOutput::default(), cos: cos.clone() } + pub fn new(cos: &pdfcraft_cos::Document, file_name: &str) -> JsRunner { + let doc = pdfcraft_js::DocInfo { file_name: file_name.to_string(), num_pages: pdfcraft_model::pages(cos).len(), page: 0, info: info(cos) }; + JsRunner { doc, doc_scripts: pdfcraft_forms::document_scripts(cos), output: JsOutput::default(), cos: cos.clone() } } } /// The Info dictionary as `this.info` keys (lower-case). -fn info(cos: &printcraft_cos::Document) -> Vec<(String, String)> { +fn info(cos: &pdfcraft_cos::Document) -> Vec<(String, String)> { let Some(d) = cos.trailer().get(b"Info").map(|i| cos.resolve(i)).and_then(|i| i.as_dict().cloned()) else { return Vec::new() }; d.iter() .filter_map(|(k, v)| match &*cos.resolve(v) { - printcraft_cos::Object::String(s) => Some((String::from_utf8_lossy(k).to_lowercase(), s.to_text())), + pdfcraft_cos::Object::String(s) => Some((String::from_utf8_lossy(k).to_lowercase(), s.to_text())), _ => None, }) .collect() @@ -132,8 +131,8 @@ fn info(cos: &printcraft_cos::Document) -> Vec<(String, String)> { impl Scripts for JsRunner { fn run(&mut self, event: FieldEvent, script: &str, target: &Field, value: &str, fields: &[Field]) -> ScriptResult { let states: Vec<_> = fields.iter().map(|f| field_state(&self.cos, f)).collect(); - let ev = printcraft_js::Event::field(event.name(), &target.name, value); - let o = printcraft_js::run(script, &ev, &self.doc, &states, &self.doc_scripts, Limits::default()); + let ev = pdfcraft_js::Event::field(event.name(), &target.name, value); + let o = pdfcraft_js::run(script, &ev, &self.doc, &states, &self.doc_scripts, Limits::default()); self.output.absorb(&o); if o.error.is_some() { // As in Acrobat, a failing script leaves the value alone (the error goes to the console). @@ -150,14 +149,14 @@ impl Scripts for JsRunner { } /// A document-level JavaScript: its name and source. -pub fn document_scripts(cos: &printcraft_cos::Document) -> Vec<(String, String)> { - printcraft_forms::document_scripts_named(cos) +pub fn document_scripts(cos: &pdfcraft_cos::Document) -> Vec<(String, String)> { + pdfcraft_forms::document_scripts_named(cos) } impl crate::Document { /// Field Properties ▸ Actions: field `name`'s action per trigger. - pub fn field_actions(&self, name: &str) -> Vec<(printcraft_forms::Trigger, printcraft_forms::FieldAction)> { - self.editor.as_ref().and_then(|e| printcraft_forms::field_actions(&e.cos, name).ok()).unwrap_or_default() + pub fn field_actions(&self, name: &str) -> Vec<(pdfcraft_forms::Trigger, pdfcraft_forms::FieldAction)> { + self.editor.as_ref().and_then(|e| pdfcraft_forms::field_actions(&e.cos, name).ok()).unwrap_or_default() } /// Document JavaScripts (name, source), in name order. @@ -191,13 +190,13 @@ impl Session { let doc = self.get(id).ok_or(EditError::NoDocument)?; let cos = doc.editor.as_ref().map(|e| e.cos.clone()).ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default()))?; let runner = JsRunner::new(&cos, &doc.name); - let fields = printcraft_forms::fields(&cos); + let fields = pdfcraft_forms::fields(&cos); let states: Vec<_> = fields.iter().map(|f| field_state(&cos, f)).collect(); let event = match target { - Some(t) => printcraft_js::Event { will_commit: false, ..printcraft_js::Event::field("Mouse Up", t, "") }, - None => printcraft_js::Event::doc("Console"), + Some(t) => pdfcraft_js::Event { will_commit: false, ..pdfcraft_js::Event::field("Mouse Up", t, "") }, + None => pdfcraft_js::Event::doc("Console"), }; - let mut o = printcraft_js::run(script, &event, &runner.doc, &states, &runner.doc_scripts, Limits::default()); + let mut o = pdfcraft_js::run(script, &event, &runner.doc, &states, &runner.doc_scripts, Limits::default()); let after: Vec<_> = states.iter().map(|s| o.changed.iter().find(|c| c.name == s.name).unwrap_or(s).clone()).collect(); let changes = changes(&states, &after); let resets: Vec> = o @@ -228,13 +227,13 @@ impl Session { /// A page's words for form-field detection, in user space (underscore runs split from the /// text around them). -fn detection_words(text: &printcraft_render::PageText, info: &printcraft_render::PageInfo) -> Vec { - page_words(text, info).into_iter().map(|(text, rect)| printcraft_forms::detect::Word { text, rect }).collect() +fn detection_words(text: &pdfcraft_render::PageText, info: &pdfcraft_render::PageInfo) -> Vec { + page_words(text, info).into_iter().map(|(text, rect)| pdfcraft_forms::detect::Word { text, rect }).collect() } /// A page's words in reading order, in user space ([x0, y0, x1, y1]); runs of underscores are /// words of their own. -pub(crate) fn page_words(text: &printcraft_render::PageText, info: &printcraft_render::PageInfo) -> Vec<(String, [f64; 4])> { +pub(crate) fn page_words(text: &pdfcraft_render::PageText, info: &pdfcraft_render::PageInfo) -> Vec<(String, [f64; 4])> { let mut words: Vec<(String, [f32; 4])> = Vec::new(); let mut last_line = u32::MAX; // A space glyph ends the word before it. @@ -277,22 +276,21 @@ pub(crate) fn page_words(text: &printcraft_render::PageText, info: &printcraft_r impl Session { /// Prepare a form ▸ automatic field detection: the fields `pages` (0-based; empty = all) /// seem to ask for, named from their labels, as (page, candidate). - pub fn detect_fields(&self, id: DocId, pages: &[usize]) -> Vec<(usize, printcraft_forms::detect::Candidate)> { + pub fn detect_fields(&self, id: DocId, pages: &[usize]) -> Vec<(usize, pdfcraft_forms::detect::Candidate)> { let Some(doc) = self.get(id) else { return Vec::new() }; let Some(cos) = doc.editor.as_ref().map(|e| &e.cos) else { return Vec::new() }; - let config = printcraft_render::RenderConfig { password: doc.password.as_deref().map(std::sync::Arc::from), ..Default::default() }; - let mut r = printcraft_render::PageRenderer::new(doc.bytes.clone(), config); + let config = pdfcraft_render::RenderConfig { password: doc.password.as_deref().map(std::sync::Arc::from), ..Default::default() }; + let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config); let mut taken: Vec = doc.form.iter().map(|f| f.name.clone()).collect(); let all = doc.info.pages.len(); let list: Vec = if pages.is_empty() { (0..all).collect() } else { pages.iter().copied().filter(|p| *p < all).collect() }; let mut out = Vec::new(); for page in list { - let res = - r.render(printcraft_render::RenderRequest { page, kind: printcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() }); + let res = r.render(pdfcraft_render::RenderRequest { page, kind: pdfcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() }); let words = res.text.map(|t| detection_words(&t, &doc.info.pages[page])).unwrap_or_default(); - let shapes = printcraft_forms::detect::page_shapes(cos, page); + let shapes = pdfcraft_forms::detect::page_shapes(cos, page); let existing: Vec<[f64; 4]> = doc.form.iter().flat_map(|f| f.widgets.iter()).filter(|w| w.page == Some(page)).map(|w| w.rect).collect(); - for c in printcraft_forms::detect::detect(&words, &shapes, &existing, &taken) { + for c in pdfcraft_forms::detect::detect(&words, &shapes, &existing, &taken) { taken.push(c.name.clone()); out.push((page, c)); } @@ -313,8 +311,8 @@ impl Session { page, rect: c.rect, kind: match c.kind { - printcraft_forms::detect::Kind::Text => printcraft_forms::NewField::Text { multiline: c.rect[3] - c.rect[1] > 30.0 }, - printcraft_forms::detect::Kind::CheckBox => printcraft_forms::NewField::CheckBox, + pdfcraft_forms::detect::Kind::Text => pdfcraft_forms::NewField::Text { multiline: c.rect[3] - c.rect[1] > 30.0 }, + pdfcraft_forms::detect::Kind::CheckBox => pdfcraft_forms::NewField::CheckBox, }, name: Some(c.name), }) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 5767330b..108388ed 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1,9 +1,9 @@ -//! printcraft-engine — the façade every frontend talks to (architecture §4). +//! pdfcraft-engine — the façade every frontend talks to (architecture §4). //! //! Holds open documents, their edit history and the tool catalogue. Frontends never touch the //! parsing, rendering or editing crates directly. //! -//! **Editing model.** Each document keeps a `printcraft_cos::Document` (the object graph with a +//! **Editing model.** Each document keeps a `pdfcraft_cos::Document` (the object graph with a //! copy-on-write overlay of edits). An edit runs on a clone, and on success the previous state is //! pushed onto the undo stack (clones share all unchanged data, so this is cheap). After every //! edit the *working file* is produced by an incremental write — original bytes plus one @@ -21,23 +21,23 @@ pub mod js; pub mod links; pub mod ocr; -pub use printcraft_organize::{BoxSpec, PageBox, SplitBy, split_ranges}; +pub use pdfcraft_organize::{BoxSpec, PageBox, SplitBy, split_ranges}; /// One file produced by a split: (1-based first page, last page, PDF bytes). -pub use printcraft_organize::LabelStyle; -pub use printcraft_organize::view::{InitialView, Layout as InitialLayout, Magnification, Navigation}; +pub use pdfcraft_organize::LabelStyle; +pub use pdfcraft_organize::view::{InitialView, Layout as InitialLayout, Magnification, Navigation}; -pub use printcraft_cos::Algorithm; -pub use printcraft_edit::{ +pub use pdfcraft_cos::Algorithm; +pub use pdfcraft_edit::{ Added, AddedImage, AddedText, Align as TextAlign, Background, Content as AddedContent, Family as FontFamily, HeaderFooter, MarkKind, Watermark, }; -pub use printcraft_forms::{ +pub use pdfcraft_forms::{ BorderStyle, CheckStyle, Field as FormField, FieldAction, FieldFont, FieldKind as FormFieldKind, FieldProps, FieldValue, Look as FieldLook, NewField, TabOrder, Trigger as FieldTrigger, Widget as FormWidget, af as form_scripts, flags as field_flags, }; -pub use printcraft_a11y as a11y; -pub use printcraft_edit::{BlockStyle, PageImage, TextBlock, TextLine}; +pub use pdfcraft_a11y as a11y; +pub use pdfcraft_edit::{BlockStyle, PageImage, TextBlock, TextLine}; /// A change to an existing page image. #[derive(Clone, Debug, PartialEq)] @@ -56,7 +56,7 @@ pub enum ImageEdit { }, Delete, } -pub use printcraft_fonts::{ScriptOutline, script_outline}; +pub use pdfcraft_fonts::{ScriptOutline, script_outline}; /// Fill & Sign: `text` in the script font as a typed signature, its left edge at `at` (user /// space, vertically centred) and `height` points tall. `None` for text with no outlines. @@ -72,36 +72,36 @@ pub fn typed_signature_shape(at: [f64; 2], text: &str, height: f64) -> Option)]) -> Result { let rows = - files.iter().map(|(name, bytes)| printcraft_xfdf::data_values(bytes).map_err(|e| format!("{name}: {e}"))).collect::, _>>()?; - Ok(printcraft_xfdf::merge_csv(&rows)) + files.iter().map(|(name, bytes)| pdfcraft_xfdf::data_values(bytes).map_err(|e| format!("{name}: {e}"))).collect::, _>>()?; + Ok(pdfcraft_xfdf::merge_csv(&rows)) } pub type SplitPart = (usize, usize, Arc>); use std::sync::Arc; -use printcraft_cos::{SaveOptions, write_full, write_incremental}; -use printcraft_render::{DocInfo, OpenError, RenderConfig, RenderPool, inspect}; +use pdfcraft_cos::{SaveOptions, write_full, write_incremental}; +use pdfcraft_render::{DocInfo, OpenError, RenderConfig, RenderPool, inspect}; /// Stable identifier of an open document within a session. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -168,12 +168,12 @@ fn scope_of(edit: &Edit) -> Scope { } /// One undo/redo step: its label, the document state, its passwords, and what it changed. -type Snapshot = (String, printcraft_cos::Document, Keys, Scope); +type Snapshot = (String, pdfcraft_cos::Document, Keys, Scope); /// Editing state of a document (absent when the document cannot be edited yet, e.g. encrypted). #[derive(Clone)] struct Editor { - cos: printcraft_cos::Document, + cos: pdfcraft_cos::Document, undo: Vec, redo: Vec, keys: Keys, @@ -198,17 +198,17 @@ pub struct Document { /// Why the document cannot be edited (e.g. encryption), if so. pub read_only_reason: Option, /// Interactive form fields of the current state (empty without a form). - pub form: Arc>, + pub form: Arc>, /// Page marks present (headers and footers, watermarks, backgrounds), for Update/Remove. pub marks: Vec, /// Text and images added with Edit a PDF ▸ Add content (still editable). - pub added: Vec, + pub added: Vec, /// Link annotations, for Edit a PDF ▸ Link. - pub links: Vec, + pub links: Vec, /// Signature fields, validated against the session's trust store (Signatures panel). pub signatures: Arc>, trust: Arc, - sig_cache: Arc, + sig_cache: Arc, editor: Option, config: RenderConfig, /// What field scripts printed or asked for (see [`Session::take_js_output`]). @@ -229,20 +229,20 @@ impl Document { } /// Edit a PDF: the images `page` draws (0-based). - pub fn page_images(&self, page: usize) -> Vec { - self.editor.as_ref().and_then(|e| printcraft_edit::page_images(&e.cos, page).ok()).unwrap_or_default() + pub fn page_images(&self, page: usize) -> Vec { + self.editor.as_ref().and_then(|e| pdfcraft_edit::page_images(&e.cos, page).ok()).unwrap_or_default() } /// Save image as: image `index` on `page` as a file (extension, bytes). pub fn page_image_file(&self, page: usize, index: usize) -> Result<(&'static str, Vec), String> { let editor = self.editor.as_ref().ok_or("the document can't be read")?; let img = self.page_images(page).into_iter().nth(index).ok_or_else(|| format!("page {} has no image {}", page + 1, index + 1))?; - printcraft_create::image_file(&editor.cos, img.object.ok_or("the image has no object")?) + pdfcraft_create::image_file(&editor.cos, img.object.ok_or("the image has no object")?) } /// Edit a PDF ▸ Edit text: the paragraphs on `page` (0-based). - pub fn text_blocks(&self, page: usize) -> Vec { - self.editor.as_ref().and_then(|e| printcraft_edit::text_blocks(&e.cos, page).ok()).unwrap_or_default() + pub fn text_blocks(&self, page: usize) -> Vec { + self.editor.as_ref().and_then(|e| pdfcraft_edit::text_blocks(&e.cos, page).ok()).unwrap_or_default() } /// A counter that changes with every edit (for caches of derived data). @@ -251,8 +251,8 @@ impl Document { } /// Edit a PDF ▸ Edit text: the lines of existing text on `page` (0-based). - pub fn text_lines(&self, page: usize) -> Vec { - self.editor.as_ref().and_then(|e| printcraft_edit::text_lines(&e.cos, page).ok()).unwrap_or_default() + pub fn text_lines(&self, page: usize) -> Vec { + self.editor.as_ref().and_then(|e| pdfcraft_edit::text_lines(&e.cos, page).ok()).unwrap_or_default() } /// Add alternate text: the figures, in document order. @@ -289,7 +289,7 @@ impl Document { } pub fn initial_view(&self) -> InitialView { - self.editor.as_ref().map(|e| printcraft_organize::initial_view(&e.cos)).unwrap_or_default() + self.editor.as_ref().map(|e| pdfcraft_organize::initial_view(&e.cos)).unwrap_or_default() } /// Signed: at least one signature field holds a signature. @@ -315,7 +315,7 @@ impl Document { } /// What the opening password allows; `None` when the document is not encrypted. - pub fn permissions(&self) -> Option { + pub fn permissions(&self) -> Option { self.editor.as_ref().and_then(|e| e.cos.permissions()) } @@ -364,48 +364,48 @@ impl Document { let stream = d.crypt_filters.iter().find(|(name, _)| *name == d.stm_f).map(|(_, m)| *m); let method = match (d.v, stream) { (1..=3, _) if d.length_bits <= 40 => "RC4, 40-bit", - (1..=3, _) | (_, Some(printcraft_cos::CryptMethod::Rc4)) => "RC4, 128-bit", - (_, Some(printcraft_cos::CryptMethod::Aes128)) => "AES, 128-bit", - (_, Some(printcraft_cos::CryptMethod::Aes256)) => "AES, 256-bit", + (1..=3, _) | (_, Some(pdfcraft_cos::CryptMethod::Rc4)) => "RC4, 128-bit", + (_, Some(pdfcraft_cos::CryptMethod::Aes128)) => "AES, 128-bit", + (_, Some(pdfcraft_cos::CryptMethod::Aes256)) => "AES, 256-bit", _ => "Attachments only", }; let pending = editor.cos.encryption_changed(); - let permissions = if pending { printcraft_cos::Permissions { bits: h.permissions().bits, owner: false } } else { h.permissions() }; - Some(SecuritySummary { method: method.into(), owner: h.auth() == printcraft_cos::Auth::Owner && !pending, permissions, pending }) + let permissions = if pending { pdfcraft_cos::Permissions { bits: h.permissions().bits, owner: false } } else { h.permissions() }; + Some(SecuritySummary { method: method.into(), owner: h.auth() == pdfcraft_cos::Auth::Owner && !pending, permissions, pending }) } /// Comment properties of the comment at `(page, index)` (Comment Properties dialog). - pub fn comment_props(&self, page: usize, index: usize) -> Option { - self.editor.as_ref().and_then(|e| printcraft_annot::props(&e.cos, page, index)) + pub fn comment_props(&self, page: usize, index: usize) -> Option { + self.editor.as_ref().and_then(|e| pdfcraft_annot::props(&e.cos, page, index)) } /// A form field's Appearance-tab look (borders, colours, font). - pub fn field_look(&self, name: &str) -> Option { + pub fn field_look(&self, name: &str) -> Option { let e = self.editor.as_ref()?; let f = self.form.iter().find(|f| f.name == name)?; - Some(printcraft_forms::look(&e.cos, f)) + Some(pdfcraft_forms::look(&e.cos, f)) } /// A check box's or radio button's mark (Field Properties ▸ Options), `None` for other fields. pub fn field_check_style(&self, name: &str) -> Option { let e = self.editor.as_ref()?; let f = self.form.iter().find(|f| f.name == name)?; - matches!(f.kind, FormFieldKind::CheckBox | FormFieldKind::Radio).then(|| printcraft_forms::check_style(&e.cos, f)) + matches!(f.kind, FormFieldKind::CheckBox | FormFieldKind::Radio).then(|| pdfcraft_forms::check_style(&e.cos, f)) } /// Remove Hidden Information: what each category would remove. pub fn hidden_info(&self) -> Vec<(Hidden, usize)> { - self.editor.as_ref().map(|e| printcraft_redact::sanitize::scan(&e.cos)).unwrap_or_default() + self.editor.as_ref().map(|e| pdfcraft_redact::sanitize::scan(&e.cos)).unwrap_or_default() } /// Every page's media, crop, bleed, trim and art boxes (user space), for Set Page Boxes. pub fn page_boxes(&self) -> Vec<[[f64; 4]; 5]> { - self.editor.as_ref().and_then(|e| printcraft_organize::page_boxes(&e.cos).ok()).unwrap_or_default() + self.editor.as_ref().and_then(|e| pdfcraft_organize::page_boxes(&e.cos).ok()).unwrap_or_default() } /// Current value of a document-information entry (Title, Author, …). pub fn info_value(&self, key: &str) -> Option { - self.editor.as_ref().and_then(|e| printcraft_organize::info(&e.cos, key)) + self.editor.as_ref().and_then(|e| pdfcraft_organize::info(&e.cos, key)) } /// The name shown on the tab and window: the document title when the document asks for it @@ -413,8 +413,8 @@ impl Document { pub fn display_name(&self) -> String { self.editor .as_ref() - .filter(|e| printcraft_organize::displays_doc_title(&e.cos)) - .and_then(|e| printcraft_organize::info(&e.cos, "Title")) + .filter(|e| pdfcraft_organize::displays_doc_title(&e.cos)) + .and_then(|e| pdfcraft_organize::info(&e.cos, "Title")) .map(|t| t.trim().to_owned()) .filter(|t| !t.is_empty()) .unwrap_or_else(|| self.name.clone()) @@ -458,7 +458,7 @@ pub struct SecuritySummary { /// Opened with the owner password (no restrictions apply). pub owner: bool, /// For pending protection: the restrictions as they will apply to others. - pub permissions: printcraft_cos::Permissions, + pub permissions: pdfcraft_cos::Permissions, /// Set in this session; written by the next save. pub pending: bool, } @@ -500,7 +500,7 @@ pub struct Protection { /// "Enable text access for screen reader devices". pub accessibility: bool, /// Compatibility level; AES-256 (Acrobat X and later) by default. - pub algorithm: printcraft_cos::Algorithm, + pub algorithm: pdfcraft_cos::Algorithm, /// `false`: "Encrypt all document contents except metadata". pub encrypt_metadata: bool, } @@ -531,7 +531,7 @@ impl Default for Protection { changes: Changes::None, copy: false, accessibility: true, - algorithm: printcraft_cos::Algorithm::Aes256, + algorithm: pdfcraft_cos::Algorithm::Aes256, encrypt_metadata: true, } } @@ -576,7 +576,7 @@ impl Protection { if [&self.open_password, &self.permissions_password].into_iter().flatten().any(|p| p.is_empty()) { return bad("passwords can't be empty"); } - if self.algorithm != printcraft_cos::Algorithm::Aes256 + if self.algorithm != pdfcraft_cos::Algorithm::Aes256 && [&self.open_password, &self.permissions_password].into_iter().flatten().any(|p| p.chars().any(|c| !(' '..='~').contains(&c))) { return bad("this compatibility level supports only plain ASCII passwords; use AES-256 (Acrobat X and later)"); @@ -663,7 +663,7 @@ pub enum Edit { NumberPages { from: usize, to: usize, - style: printcraft_organize::LabelStyle, + style: pdfcraft_organize::LabelStyle, prefix: String, first: u32, }, @@ -825,18 +825,18 @@ pub enum Edit { /// Standards ▸ Save as PDF/A: fix what can be fixed for `level` (metadata, output intent, /// forbidden actions, annotation flags, …). ConvertPdfA { - level: printcraft_preflight::Level, + level: pdfcraft_preflight::Level, }, /// What a script (button or console) changed in form fields: values, read-only, required /// and visibility. ApplyScriptChanges { - changes: Vec, + changes: Vec, }, /// Scan & OCR ▸ Recognize text: put recognised words on `page` as invisible text over the /// image (from [`ocr::OcrJob::run`]). AddOcrText { page: usize, - words: Vec, + words: Vec, }, /// Edit a PDF ▸ Edit text: replace the text of line `line` (from `Document::text_lines`) on /// `page`, in its own font when it can show it, else in Helvetica. @@ -858,7 +858,7 @@ pub enum Edit { block: usize, text: String, /// Formatting changes (font, size, colour, alignment); default keeps the paragraph's. - style: printcraft_edit::BlockStyle, + style: pdfcraft_edit::BlockStyle, }, /// Order tabs manually: move a field one place earlier or later on its page. MoveInTabOrder { @@ -1107,13 +1107,13 @@ fn annotation_noun(s: &Shape) -> &'static str { /// Opened as owner, or nothing is restricted (no permissions password was set): security may /// be changed, as in Acrobat. -fn unrestricted(p: &printcraft_cos::Permissions) -> bool { +fn unrestricted(p: &pdfcraft_cos::Permissions) -> bool { const ALL: i32 = 0b1111_0011_1100; // bits 3–6 and 9–12 p.owner || p.bits & ALL == ALL } /// Whether the opening password allows an edit (§7.6.4.2, Table 22). -fn check_permission(edit: &Edit, p: &printcraft_cos::Permissions) -> Result<(), EditError> { +fn check_permission(edit: &Edit, p: &pdfcraft_cos::Permissions) -> Result<(), EditError> { match edit { Edit::RotatePages { .. } | Edit::DeletePages { .. } @@ -1239,7 +1239,7 @@ impl EditCtx { // Real clock: mix in sub-second time so ids from two sessions don't collide. seed ^= std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.subsec_nanos() as u64).unwrap_or(0) << 32; } - Self { js: None, date: now.map(printcraft_cos::pdf_date), today: (1970, 1, 1), seed, count: 0 } + Self { js: None, date: now.map(pdfcraft_cos::pdf_date), today: (1970, 1, 1), seed, count: 0 } } /// 32 bytes of entropy for new encryption keys and salts. `RandomState` is seeded by the @@ -1258,7 +1258,7 @@ impl EditCtx { } /// A fresh `/NM`: a random-looking UUID (version 4 layout) from a splitmix64 stream. - fn meta(&mut self) -> printcraft_annot::Meta { + fn meta(&mut self) -> pdfcraft_annot::Meta { let mut next = || { self.count += 1; let mut z = self.seed.wrapping_add(self.count.wrapping_mul(0x9E37_79B9_7F4A_7C15)); @@ -1275,46 +1275,46 @@ impl EditCtx { 0x8000 | (b >> 48) & 0x3fff, b & 0xffff_ffff_ffff ); - printcraft_annot::Meta { date: self.date.clone(), id } + pdfcraft_annot::Meta { date: self.date.clone(), id } } } /// Perform an edit on a working copy (the caller discards it on error). -fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> Result<(), EditError> { +fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> Result<(), EditError> { match edit { - Edit::RotatePages { pages, degrees } => printcraft_organize::rotate_pages(doc, pages, *degrees)?, - Edit::DeletePages { pages } => printcraft_organize::delete_pages(doc, pages)?, - Edit::MovePages { pages, to } => printcraft_organize::move_pages(doc, pages, *to)?, + Edit::RotatePages { pages, degrees } => pdfcraft_organize::rotate_pages(doc, pages, *degrees)?, + Edit::DeletePages { pages } => pdfcraft_organize::delete_pages(doc, pages)?, + Edit::MovePages { pages, to } => pdfcraft_organize::move_pages(doc, pages, *to)?, Edit::InsertBlankPage { at, width, height } => { - printcraft_organize::insert_blank_page(doc, *at, *width, *height)?; + pdfcraft_organize::insert_blank_page(doc, *at, *width, *height)?; } - Edit::SetInfo { key, value } => printcraft_organize::set_info(doc, key, value)?, - Edit::DuplicatePages { pages } => printcraft_organize::duplicate_pages(doc, pages)?, + Edit::SetInfo { key, value } => pdfcraft_organize::set_info(doc, key, value)?, + Edit::DuplicatePages { pages } => pdfcraft_organize::duplicate_pages(doc, pages)?, Edit::ReplacePages { pages, name, bytes, src_pages } => { let src = open_source(name, bytes)?; - printcraft_organize::replace_pages(doc, pages, &src, src_pages)?; + pdfcraft_organize::replace_pages(doc, pages, &src, src_pages)?; } - Edit::SetPageBox { pages, which, spec } => printcraft_organize::set_page_box(doc, pages, *which, *spec)?, + Edit::SetPageBox { pages, which, spec } => pdfcraft_organize::set_page_box(doc, pages, *which, *spec)?, Edit::InsertPagesFrom { name, bytes, pages, at } => { let src = open_source(name, bytes)?; let pages = match pages { Some(p) => p.clone(), - None => (0..printcraft_organize::page_count(&src)?).collect(), + None => (0..pdfcraft_organize::page_count(&src)?).collect(), }; - printcraft_organize::import_pages(doc, &src, &pages, *at)?; + pdfcraft_organize::import_pages(doc, &src, &pages, *at)?; } Edit::AddBookmark { parent, index, title, page } => { - printcraft_organize::add_bookmark(doc, parent, *index, title, *page)?; + pdfcraft_organize::add_bookmark(doc, parent, *index, title, *page)?; } - Edit::RenameBookmark { path, title } => printcraft_organize::rename_bookmark(doc, path, title)?, - Edit::DeleteBookmark { path } => printcraft_organize::delete_bookmark(doc, path)?, + Edit::RenameBookmark { path, title } => pdfcraft_organize::rename_bookmark(doc, path, title)?, + Edit::DeleteBookmark { path } => pdfcraft_organize::delete_bookmark(doc, path)?, Edit::MoveBookmark { from, to_parent, index } => { - printcraft_organize::move_bookmark(doc, from, to_parent, *index)?; + pdfcraft_organize::move_bookmark(doc, from, to_parent, *index)?; } - Edit::SetBookmarkPage { path, page } => printcraft_organize::set_bookmark_page(doc, path, *page)?, - Edit::NumberPages { from, to, style, prefix, first } => printcraft_organize::number_pages(doc, *from, *to, *style, prefix, *first)?, + Edit::SetBookmarkPage { path, page } => pdfcraft_organize::set_bookmark_page(doc, path, *page)?, + Edit::NumberPages { from, to, style, prefix, first } => pdfcraft_organize::number_pages(doc, *from, *to, *style, prefix, *first)?, Edit::AddAnnotation(a) => { - printcraft_annot::add_annotation(doc, a, &cx.meta())?; + pdfcraft_annot::add_annotation(doc, a, &cx.meta())?; } Edit::AddCustomStamp { page, rect, name, file, author } => { let src = mark_source(doc, file)?; @@ -1327,200 +1327,198 @@ fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) - *rect }; let shape = Shape::CustomStamp { rect, name: name.clone(), picture: src.xobject, image: src.image, size: (sw, sh) }; - let style = printcraft_annot::Style::default_for(&shape); + let style = pdfcraft_annot::Style::default_for(&shape); let new = NewAnnotation { page: *page, shape, style, contents: name.clone(), author: author.clone() }; - printcraft_annot::add_annotation(doc, &new, &cx.meta())?; + pdfcraft_annot::add_annotation(doc, &new, &cx.meta())?; } - Edit::DeleteAnnotation { page, index } => printcraft_annot::delete_annotation(doc, *page, *index)?, - Edit::SetAnnotationContents { page, index, text } => printcraft_annot::set_contents(doc, *page, *index, text, &cx.meta())?, + Edit::DeleteAnnotation { page, index } => pdfcraft_annot::delete_annotation(doc, *page, *index)?, + Edit::SetAnnotationContents { page, index, text } => pdfcraft_annot::set_contents(doc, *page, *index, text, &cx.meta())?, Edit::ReplyToAnnotation { page, index, text, author } => { - printcraft_annot::add_reply(doc, *page, *index, text, author, &cx.meta())?; + pdfcraft_annot::add_reply(doc, *page, *index, text, author, &cx.meta())?; } Edit::SetAnnotationStatus { page, index, state, author } => { - printcraft_annot::set_review_state(doc, *page, *index, *state, author, &cx.meta())?; + pdfcraft_annot::set_review_state(doc, *page, *index, *state, author, &cx.meta())?; } Edit::EraseInk { page, index, path, radius } => { - printcraft_annot::erase_ink(doc, *page, *index, path, *radius, &cx.meta())?; + pdfcraft_annot::erase_ink(doc, *page, *index, path, *radius, &cx.meta())?; } Edit::ReplaceText { page, quads, text, author, strike, caret } => { - printcraft_annot::add_text_replacement(doc, *page, quads, text, author, strike, caret, &cx.meta())?; + pdfcraft_annot::add_text_replacement(doc, *page, quads, text, author, strike, caret, &cx.meta())?; } Edit::MarkAnnotation { page, index, marked, author } => { - printcraft_annot::set_marked(doc, *page, *index, *marked, author, &cx.meta())?; + pdfcraft_annot::set_marked(doc, *page, *index, *marked, author, &cx.meta())?; } - Edit::LockAnnotation { page, index, locked } => printcraft_annot::set_locked(doc, *page, *index, *locked)?, - Edit::MoveAnnotation { page, index, dx, dy } => printcraft_annot::move_annotation(doc, *page, *index, *dx, *dy, &cx.meta())?, - Edit::ResizeAnnotation { page, index, rect } => printcraft_annot::set_rect(doc, *page, *index, *rect, &cx.meta())?, + Edit::LockAnnotation { page, index, locked } => pdfcraft_annot::set_locked(doc, *page, *index, *locked)?, + Edit::MoveAnnotation { page, index, dx, dy } => pdfcraft_annot::move_annotation(doc, *page, *index, *dx, *dy, &cx.meta())?, + Edit::ResizeAnnotation { page, index, rect } => pdfcraft_annot::set_rect(doc, *page, *index, *rect, &cx.meta())?, Edit::StyleAnnotation { page, index, color, opacity, width } => { - printcraft_annot::set_style(doc, *page, *index, *color, *opacity, *width, &cx.meta())?; + pdfcraft_annot::set_style(doc, *page, *index, *color, *opacity, *width, &cx.meta())?; } Edit::SetAnnotationInfo { page, index, author, subject, icon } => { - printcraft_annot::set_info(doc, *page, *index, author.as_deref(), subject.as_deref(), *icon, &cx.meta())?; + pdfcraft_annot::set_info(doc, *page, *index, author.as_deref(), subject.as_deref(), *icon, &cx.meta())?; } Edit::SetFieldValue { name, value } => match cx.js.as_mut() { - Some(js) => printcraft_forms::set_value_with(doc, name, value, js)?, - None => printcraft_forms::set_value(doc, name, value)?, + Some(js) => pdfcraft_forms::set_value_with(doc, name, value, js)?, + None => pdfcraft_forms::set_value(doc, name, value)?, }, Edit::ApplyScriptChanges { changes } => match cx.js.as_mut() { - Some(js) => printcraft_forms::apply_script_changes(doc, changes, js)?, - None => printcraft_forms::apply_script_changes(doc, changes, &mut printcraft_forms::NoScripts)?, + Some(js) => pdfcraft_forms::apply_script_changes(doc, changes, js)?, + None => pdfcraft_forms::apply_script_changes(doc, changes, &mut pdfcraft_forms::NoScripts)?, }, Edit::SetFieldImage { name, image } => { - let (img, _) = printcraft_create::image_xobject(doc, name, image)?; + let (img, _) = pdfcraft_create::image_xobject(doc, name, image)?; let px = match &*doc.get(img) { - printcraft_cos::Object::Stream(s) => (s.dict.int(b"Width").unwrap_or(1) as u32, s.dict.int(b"Height").unwrap_or(1) as u32), + pdfcraft_cos::Object::Stream(s) => (s.dict.int(b"Width").unwrap_or(1) as u32, s.dict.int(b"Height").unwrap_or(1) as u32), _ => (1, 1), }; - printcraft_forms::set_button_icon(doc, name, img, px)?; + pdfcraft_forms::set_button_icon(doc, name, img, px)?; } Edit::ResetForm { names } => { - printcraft_forms::reset(doc, names.as_deref())?; + pdfcraft_forms::reset(doc, names.as_deref())?; } Edit::AddField { page, rect, kind, name } => { - printcraft_forms::add_field(doc, *page, *rect, kind, name.as_deref())?; + pdfcraft_forms::add_field(doc, *page, *rect, kind, name.as_deref())?; } Edit::SetFieldProps { name, props } => { - printcraft_forms::set_props(doc, name, props)?; + pdfcraft_forms::set_props(doc, name, props)?; } Edit::DuplicateField { name, pages } => { - if printcraft_forms::duplicate_field(doc, name, pages)? == 0 { - return Err(EditError::Form(printcraft_forms::FormError::Invalid(format!("{name} is already on those pages")))); + if pdfcraft_forms::duplicate_field(doc, name, pages)? == 0 { + return Err(EditError::Form(pdfcraft_forms::FormError::Invalid(format!("{name} is already on those pages")))); } } - Edit::DeleteField { name } => printcraft_forms::delete_field(doc, name)?, - Edit::SetTabOrder { pages, order } => printcraft_forms::set_tab_order(doc, pages, *order)?, - Edit::MoveInTabOrder { name, earlier } => printcraft_forms::move_in_tab_order(doc, name, *earlier)?, - Edit::SetInitialView(v) => printcraft_organize::set_initial_view(doc, v)?, + Edit::DeleteField { name } => pdfcraft_forms::delete_field(doc, name)?, + Edit::SetTabOrder { pages, order } => pdfcraft_forms::set_tab_order(doc, pages, *order)?, + Edit::MoveInTabOrder { name, earlier } => pdfcraft_forms::move_in_tab_order(doc, name, *earlier)?, + Edit::SetInitialView(v) => pdfcraft_organize::set_initial_view(doc, v)?, Edit::SetAltText { figure, alt } => { - let r = printcraft_cos::ObjRef::new(*figure, doc.generation(*figure)); + let r = pdfcraft_cos::ObjRef::new(*figure, doc.generation(*figure)); a11y::set_alt(doc, r, alt.as_deref()).map_err(|e| EditError::Accessibility(e.to_string()))?; } Edit::EditTextLine { page, line, text } => { - printcraft_edit::replace_line(doc, *page, *line, text)?; + pdfcraft_edit::replace_line(doc, *page, *line, text)?; } Edit::SetFieldScript { name, event, script } => { - printcraft_forms::set_field_script(doc, name, event, script.as_deref())?; + pdfcraft_forms::set_field_script(doc, name, event, script.as_deref())?; match cx.js.as_mut() { - Some(js) => printcraft_forms::recalculate_with(doc, js)?, - None => printcraft_forms::recalculate(doc)?, + Some(js) => pdfcraft_forms::recalculate_with(doc, js)?, + None => pdfcraft_forms::recalculate(doc)?, }; } Edit::ConvertPdfA { level } => { - printcraft_preflight::convert(doc, *level).map_err(|e| EditError::Invalid(e.to_string()))?; + pdfcraft_preflight::convert(doc, *level).map_err(|e| EditError::Invalid(e.to_string()))?; } - Edit::SetDocumentScript { name, script } => printcraft_forms::set_document_script(doc, name, script.as_deref())?, + Edit::SetDocumentScript { name, script } => pdfcraft_forms::set_document_script(doc, name, script.as_deref())?, Edit::AddOcrText { page, words } => { - printcraft_edit::stamp(doc, *page, "OCR", printcraft_ocr::text_layer(words))?; + pdfcraft_edit::stamp(doc, *page, "OCR", pdfcraft_ocr::text_layer(words))?; } Edit::EditPageImage { page, index, change } => { - let img = printcraft_edit::page_images(doc, *page)? + let img = pdfcraft_edit::page_images(doc, *page)? .into_iter() .nth(*index) - .ok_or_else(|| EditError::Edit(printcraft_edit::EditError::Invalid(format!("page {} has no image {}", page + 1, index + 1))))?; + .ok_or_else(|| EditError::Edit(pdfcraft_edit::EditError::Invalid(format!("page {} has no image {}", page + 1, index + 1))))?; let c = match change { - ImageEdit::Move(to) => printcraft_edit::ImageChange::Transform(printcraft_edit::rect_to_rect(img.rect, *to)), - ImageEdit::Rotate(q) => printcraft_edit::ImageChange::Transform(printcraft_edit::turn_about_centre(img.rect, *q, false, false)), + ImageEdit::Move(to) => pdfcraft_edit::ImageChange::Transform(pdfcraft_edit::rect_to_rect(img.rect, *to)), + ImageEdit::Rotate(q) => pdfcraft_edit::ImageChange::Transform(pdfcraft_edit::turn_about_centre(img.rect, *q, false, false)), ImageEdit::Flip { horizontal } => { - printcraft_edit::ImageChange::Transform(printcraft_edit::turn_about_centre(img.rect, 0, *horizontal, !*horizontal)) + pdfcraft_edit::ImageChange::Transform(pdfcraft_edit::turn_about_centre(img.rect, 0, *horizontal, !*horizontal)) } - ImageEdit::Replace { name, bytes } => printcraft_edit::ImageChange::Replace(printcraft_create::image_xobject(doc, name, bytes)?.0), - ImageEdit::Delete => printcraft_edit::ImageChange::Delete, + ImageEdit::Replace { name, bytes } => pdfcraft_edit::ImageChange::Replace(pdfcraft_create::image_xobject(doc, name, bytes)?.0), + ImageEdit::Delete => pdfcraft_edit::ImageChange::Delete, }; - printcraft_edit::change_image(doc, *page, *index, &c)?; + pdfcraft_edit::change_image(doc, *page, *index, &c)?; } Edit::EditTextBlock { page, block, text, style } => { - printcraft_edit::rewrite_block(doc, *page, *block, Some(text), style)?; + pdfcraft_edit::rewrite_block(doc, *page, *block, Some(text), style)?; } Edit::MarkDecorative { figure } => { - let r = printcraft_cos::ObjRef::new(*figure, doc.generation(*figure)); + let r = pdfcraft_cos::ObjRef::new(*figure, doc.generation(*figure)); a11y::mark_decorative(doc, r).map_err(|e| EditError::Accessibility(e.to_string()))?; } Edit::AddHeaderFooter { pages, settings, replace } => { let date = cx.today; - printcraft_edit::add_header_footer(doc, pages, settings, *replace, &printcraft_edit::Context { date })?; + pdfcraft_edit::add_header_footer(doc, pages, settings, *replace, &pdfcraft_edit::Context { date })?; } Edit::AddWatermark { pages, settings, replace, file } => { let mut s = settings.clone(); if let Some(f) = file { s.source = Some(mark_source(doc, f)?); } - printcraft_edit::add_watermark(doc, pages, &s, *replace)? + pdfcraft_edit::add_watermark(doc, pages, &s, *replace)? } Edit::AddBackground { pages, settings, replace, file } => { let mut s = settings.clone(); if let Some(f) = file { s.source = Some(mark_source(doc, f)?); } - printcraft_edit::add_background(doc, pages, &s, *replace)? + pdfcraft_edit::add_background(doc, pages, &s, *replace)? } Edit::RemoveMarks { kind } => { - let n = printcraft_model::pages(doc).len(); - if printcraft_edit::remove_marks(doc, &(0..n).collect::>(), *kind)? == 0 { - return Err(EditError::Edit(printcraft_edit::EditError::Invalid("there is nothing to remove".into()))); + let n = pdfcraft_model::pages(doc).len(); + if pdfcraft_edit::remove_marks(doc, &(0..n).collect::>(), *kind)? == 0 { + return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("there is nothing to remove".into()))); } } Edit::AddText { page, text } => { - printcraft_edit::add_content(doc, *page, &AddedContent::Text(text.clone()))?; + pdfcraft_edit::add_content(doc, *page, &AddedContent::Text(text.clone()))?; } Edit::AddImage { page, rect, name, bytes } => { - let (image, natural) = printcraft_create::image_xobject(doc, name, bytes)?; + let (image, natural) = pdfcraft_create::image_xobject(doc, name, bytes)?; let rect = match rect { Some(r) => *r, None => { - let p = printcraft_model::pages(doc).swap_remove(*page); + let p = pdfcraft_model::pages(doc).swap_remove(*page); let (pw, ph) = p.display_size(doc); let k = ((pw * 0.8) / natural.0).min((ph * 0.8) / natural.1).min(1.0); let (w, h) = (natural.0 * k, natural.1 * k); [(pw - w) / 2.0, (ph - h) / 2.0, (pw + w) / 2.0, (ph + h) / 2.0] } }; - printcraft_edit::add_content(doc, *page, &AddedContent::Image(printcraft_edit::AddedImage::new(rect, image)))?; + pdfcraft_edit::add_content(doc, *page, &AddedContent::Image(pdfcraft_edit::AddedImage::new(rect, image)))?; } - Edit::UpdateContent { page, index, content } => printcraft_edit::update_content(doc, *page, *index, content)?, - Edit::DeleteContent { page, index } => printcraft_edit::delete_content(doc, *page, *index)?, + Edit::UpdateContent { page, index, content } => pdfcraft_edit::update_content(doc, *page, *index, content)?, + Edit::DeleteContent { page, index } => pdfcraft_edit::delete_content(doc, *page, *index)?, Edit::ReplaceImage { page, index, name, bytes } => { - let item = printcraft_edit::list_added(doc).into_iter().filter(|a| a.page == *page).nth(*index); + let item = pdfcraft_edit::list_added(doc).into_iter().filter(|a| a.page == *page).nth(*index); let Some(AddedContent::Image(old)) = item.map(|a| a.content) else { - return Err(EditError::Edit(printcraft_edit::EditError::Invalid("that item is not an image".into()))); + return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("that item is not an image".into()))); }; - let (image, _) = printcraft_create::image_xobject(doc, name, bytes)?; - printcraft_edit::update_content(doc, *page, *index, &AddedContent::Image(printcraft_edit::AddedImage { image, ..old }))?; + let (image, _) = pdfcraft_create::image_xobject(doc, name, bytes)?; + pdfcraft_edit::update_content(doc, *page, *index, &AddedContent::Image(pdfcraft_edit::AddedImage { image, ..old }))?; } Edit::ApplyRedactions { pages } => { - printcraft_redact::apply(doc, pages.as_deref())?; + pdfcraft_redact::apply(doc, pages.as_deref())?; } Edit::ClearRedactions => { - printcraft_redact::clear_marks(doc, None)?; + pdfcraft_redact::clear_marks(doc, None)?; } Edit::AddLink { page, rect, action, style } => { - printcraft_annot::links::add(doc, *page, *rect, action, style)?; + pdfcraft_annot::links::add(doc, *page, *rect, action, style)?; } - Edit::SetLink { page, index, rect, action, style } => { - printcraft_annot::links::set(doc, *page, *index, *rect, action.as_ref(), style.as_ref())? - } - Edit::DeleteLink { page, index } => printcraft_annot::links::delete(doc, *page, *index)?, + Edit::SetLink { page, index, rect, action, style } => pdfcraft_annot::links::set(doc, *page, *index, *rect, action.as_ref(), style.as_ref())?, + Edit::DeleteLink { page, index } => pdfcraft_annot::links::delete(doc, *page, *index)?, Edit::RemoveLinks { pages } => { - if printcraft_annot::links::remove_all(doc, pages.as_deref())? == 0 { - return Err(EditError::Edit(printcraft_edit::EditError::Invalid("there are no links to remove".into()))); + if pdfcraft_annot::links::remove_all(doc, pages.as_deref())? == 0 { + return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("there are no links to remove".into()))); } } Edit::AddLinks { links, style } => { - if printcraft_annot::links::add_many(doc, links, style)? == 0 { - return Err(EditError::Edit(printcraft_edit::EditError::Invalid("no web addresses were found".into()))); + if pdfcraft_annot::links::add_many(doc, links, style)? == 0 { + return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("no web addresses were found".into()))); } } Edit::ImportData { bytes, .. } => { - printcraft_xfdf::import(doc, bytes)?; + pdfcraft_xfdf::import(doc, bytes)?; } Edit::RemoveHidden { which } => { - printcraft_redact::sanitize::remove_hidden(doc, which)?; + pdfcraft_redact::sanitize::remove_hidden(doc, which)?; } Edit::Sanitize => { - printcraft_redact::sanitize::sanitize(doc)?; + pdfcraft_redact::sanitize::sanitize(doc)?; } Edit::Flatten { comments, fields } => { - let n = printcraft_model::pages(doc).len(); - printcraft_edit::flatten(doc, &(0..n).collect::>(), *comments, *fields)?; + let n = pdfcraft_model::pages(doc).len(); + pdfcraft_edit::flatten(doc, &(0..n).collect::>(), *comments, *fields)?; } Edit::Protect(p) => { p.validate()?; @@ -1528,7 +1526,7 @@ fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) - // Without a permissions password nothing is restricted, so the owner password is a // random one nobody needs. let random_owner: String = cx.entropy().iter().map(|b| format!("{b:02x}")).collect(); - let params = printcraft_cos::NewEncryption { + let params = pdfcraft_cos::NewEncryption { algorithm: p.algorithm, user_password: p.open_password.as_deref().unwrap_or(""), owner_password: p.permissions_password.as_deref().unwrap_or(&random_owner), @@ -1554,15 +1552,15 @@ fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) - } /// The comment list, read from the object graph (as `inspect` lists comments). -fn comment_list(doc: &printcraft_cos::Document) -> Vec { - printcraft_annot::summaries(doc) +fn comment_list(doc: &pdfcraft_cos::Document) -> Vec { + pdfcraft_annot::summaries(doc) .into_iter() - .map(|s| printcraft_render::Annotation { + .map(|s| pdfcraft_render::Annotation { page: s.page, subtype: s.subtype, author: s.author, contents: s.contents, - modified: s.modified.map(|m| printcraft_render::pretty_date(&m)), + modified: s.modified.map(|m| pdfcraft_render::pretty_date(&m)), name: s.name, in_reply_to: s.in_reply_to, rect: s.rect, @@ -1612,13 +1610,13 @@ pub fn guard(f: impl FnOnce() -> T) -> Result { } /// Parse another PDF to copy pages from. -fn open_source(name: &str, bytes: &Arc>) -> Result { - match std::panic::catch_unwind(|| printcraft_cos::Document::open(bytes.clone())) { +fn open_source(name: &str, bytes: &Arc>) -> Result { + match std::panic::catch_unwind(|| pdfcraft_cos::Document::open(bytes.clone())) { Ok(Ok(d)) if d.permissions().is_some_and(|p| !p.assemble()) => { Err(EditError::Source(format!("{name}: its security settings don't allow copying pages"))) } Ok(Ok(d)) => Ok(d), - Ok(Err(printcraft_cos::CosError::NeedsPassword)) => Err(EditError::Source(format!("{name}: it is password-protected"))), + Ok(Err(pdfcraft_cos::CosError::NeedsPassword)) => Err(EditError::Source(format!("{name}: it is password-protected"))), Ok(Err(e)) => Err(EditError::Source(format!("{name}: {e}"))), Err(_) => Err(EditError::Source(format!("{name}: the file could not be read"))), } @@ -1637,27 +1635,27 @@ pub enum EditError { #[error("the document's security settings don't allow {0}; open it with the owner password to make this change")] NotPermitted(&'static str), #[error("{0}")] - Organize(#[from] printcraft_organize::OrganizeError), + Organize(#[from] pdfcraft_organize::OrganizeError), #[error("{0}")] - Bookmark(#[from] printcraft_organize::OutlineError), + Bookmark(#[from] pdfcraft_organize::OutlineError), #[error("{0}")] - Comment(#[from] printcraft_annot::AnnotError), + Comment(#[from] pdfcraft_annot::AnnotError), #[error("{0}")] Protection(String), #[error("{0}")] - Form(#[from] printcraft_forms::FormError), + Form(#[from] pdfcraft_forms::FormError), #[error(transparent)] - Redact(#[from] printcraft_redact::RedactError), + Redact(#[from] pdfcraft_redact::RedactError), #[error("{0}")] Print(String), #[error("{0}")] Accessibility(String), #[error(transparent)] - Data(#[from] printcraft_xfdf::DataError), + Data(#[from] pdfcraft_xfdf::DataError), #[error("{0}")] - Edit(#[from] printcraft_edit::EditError), + Edit(#[from] pdfcraft_edit::EditError), #[error("{0}")] - Create(#[from] printcraft_create::CreateError), + Create(#[from] pdfcraft_create::CreateError), #[error("the edited document could not be written: {0}")] Write(String), #[error("the edited document could not be reopened: {0}")] @@ -1680,8 +1678,8 @@ pub enum EditError { Invalid(String), } -impl From for EditError { - fn from(e: printcraft_sign::SignError) -> Self { +impl From for EditError { + fn from(e: pdfcraft_sign::SignError) -> Self { EditError::Sign(e.to_string()) } } @@ -1699,8 +1697,8 @@ pub struct Session { } /// Validate the signature fields of `cos` (written as `bytes`). -fn signatures_of(cos: &printcraft_cos::Document, bytes: &[u8], trust: &TrustStore, cache: &printcraft_sign::DigestCache) -> Arc> { - Arc::new(printcraft_sign::pdf::list_cached(cos, bytes, trust, cache)) +fn signatures_of(cos: &pdfcraft_cos::Document, bytes: &[u8], trust: &TrustStore, cache: &pdfcraft_sign::DigestCache) -> Arc> { + Arc::new(pdfcraft_sign::pdf::list_cached(cos, bytes, trust, cache)) } impl Session { @@ -1734,7 +1732,7 @@ impl Session { /// A dynamic stamp's second line: "By Ada at 2:14 pm, Oct 02, 2026" (local time). pub fn stamp_by_line(&self, author: &str) -> String { let offset = if self.clock.is_some() { 0 } else { local_utc_offset() }; - let d = self.now().map(|t| printcraft_cos::pdf_date(t + offset)).unwrap_or_default(); + let d = self.now().map(|t| pdfcraft_cos::pdf_date(t + offset)).unwrap_or_default(); let num = |a: usize, b: usize| d.get(a..b).and_then(|s| s.parse::().ok()).unwrap_or(0); let (y, mo, day, hh, mm) = (num(2, 6), num(6, 8), num(8, 10), num(10, 12), num(12, 14)); const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; @@ -1745,7 +1743,7 @@ impl Session { pub fn today(&self) -> (i64, u32, u32) { let offset = if self.clock.is_some() { 0 } else { local_utc_offset() }; - self.now().map(|t| printcraft_cos::pdf_date(t + offset)).as_deref().and_then(parse_ymd).unwrap_or((1970, 1, 1)) + self.now().map(|t| pdfcraft_cos::pdf_date(t + offset)).as_deref().and_then(parse_ymd).unwrap_or((1970, 1, 1)) } /// Open a document from bytes. Rendering starts lazily when pages are requested. @@ -1769,7 +1767,7 @@ impl Session { } fn open_unguarded(&mut self, name: String, path: Option, bytes: Arc>, password: Option<&str>) -> Result { - let cos = std::panic::catch_unwind(|| printcraft_cos::Document::open_with_password(bytes.clone(), password)); + let cos = std::panic::catch_unwind(|| pdfcraft_cos::Document::open_with_password(bytes.clone(), password)); // The renderer authenticates on its own. It cannot use the owner password of R2–R4 // files, so give it the user password that owner authentication recovers. let (info, render_password) = match inspect(bytes.clone(), password) { @@ -1794,11 +1792,11 @@ impl Session { Ok(Err(e)) => (None, Some(e.to_string())), Err(_) => (None, Some("the document structure could not be read for editing".into())), }; - let form = editor.as_ref().map(|e| printcraft_forms::fields(&e.cos)).unwrap_or_default(); - let marks = editor.as_ref().map(|e| printcraft_edit::marks_present(&e.cos)).unwrap_or_default(); - let added = editor.as_ref().map(|e| printcraft_edit::list_added(&e.cos)).unwrap_or_default(); - let links = editor.as_ref().map(|e| printcraft_annot::links::list(&e.cos)).unwrap_or_default(); - let sig_cache = Arc::new(printcraft_sign::DigestCache::default()); + let form = editor.as_ref().map(|e| pdfcraft_forms::fields(&e.cos)).unwrap_or_default(); + let marks = editor.as_ref().map(|e| pdfcraft_edit::marks_present(&e.cos)).unwrap_or_default(); + let added = editor.as_ref().map(|e| pdfcraft_edit::list_added(&e.cos)).unwrap_or_default(); + let links = editor.as_ref().map(|e| pdfcraft_annot::links::list(&e.cos)).unwrap_or_default(); + let sig_cache = Arc::new(pdfcraft_sign::DigestCache::default()); let signatures = editor.as_ref().map(|e| signatures_of(&e.cos, &bytes, &self.trust, &sig_cache)).unwrap_or_default(); self.next_id += 1; let id = DocId(self.next_id); @@ -1856,7 +1854,7 @@ impl Session { .unwrap_or_else(|m| Err(EditError::Invalid(format!("{} failed unexpectedly ({m}); the document was not changed", edit.label()))))?; // Signed documents are only ever saved incrementally: an edit that needs a full rewrite // (applying redactions, changing security, sanitizing) would invalidate the signatures. - let rewrites = |c: &printcraft_cos::Document| c.full_save_required() || c.encryption_changed(); + let rewrites = |c: &pdfcraft_cos::Document| c.full_save_required() || c.encryption_changed(); if signed && rewrites(&next) && !rewrites(&editor.cos) { return Err(EditError::SignedRewrite(edit.label())); } @@ -1940,14 +1938,14 @@ impl Session { } else { editor.cos.bytes().clone() }; - let form = Arc::new(printcraft_forms::fields(&editor.cos)); + let form = Arc::new(pdfcraft_forms::fields(&editor.cos)); match scope { Scope::Comments => doc.info.annotations = comment_list(&editor.cos), Scope::Form => { for f in &mut doc.info.fields { if let Some(ff) = form.iter().find(|x| x.name == f.name) { f.value = match ff.kind { - printcraft_forms::FieldKind::CheckBox | printcraft_forms::FieldKind::Radio => { + pdfcraft_forms::FieldKind::CheckBox | pdfcraft_forms::FieldKind::Radio => { Some(ff.value.first().cloned().unwrap_or_else(|| "Off".into())) } _ if ff.value.is_empty() => None, @@ -1986,10 +1984,10 @@ impl Session { } } doc.info = info; - doc.form = Arc::new(printcraft_forms::fields(&editor.cos)); - doc.marks = printcraft_edit::marks_present(&editor.cos); - doc.added = printcraft_edit::list_added(&editor.cos); - doc.links = printcraft_annot::links::list(&editor.cos); + doc.form = Arc::new(pdfcraft_forms::fields(&editor.cos)); + doc.marks = pdfcraft_edit::marks_present(&editor.cos); + doc.added = pdfcraft_edit::list_added(&editor.cos); + doc.links = pdfcraft_annot::links::list(&editor.cos); doc.signatures = signatures_of(&editor.cos, &bytes, &doc.trust, &doc.sig_cache); doc.bytes = bytes.clone(); doc.renderer = RenderPool::new(bytes, render_threads(), doc.config.clone()); @@ -2004,7 +2002,7 @@ impl Session { if !editor.cos.is_modified() { return Ok(editor.cos.bytes().clone()); } - let opts = SaveOptions { mod_date: self.now().map(printcraft_cos::pdf_date), ..SaveOptions::default() }; + let opts = SaveOptions { mod_date: self.now().map(pdfcraft_cos::pdf_date), ..SaveOptions::default() }; guard(|| write_incremental(&editor.cos, &opts)).map_err(EditError::Write)?.map(Arc::new).map_err(|e| EditError::Write(e.to_string())) } @@ -2015,7 +2013,7 @@ impl Session { return Err(EditError::Signed); } let editor = doc.editor.as_ref().ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default()))?; - let opts = SaveOptions { mod_date: self.now().map(printcraft_cos::pdf_date), ..SaveOptions::default() }; + let opts = SaveOptions { mod_date: self.now().map(pdfcraft_cos::pdf_date), ..SaveOptions::default() }; guard(|| write_full(&editor.cos, &opts)).map_err(EditError::Write)?.map(Arc::new).map_err(|e| EditError::Write(e.to_string())) } @@ -2026,7 +2024,7 @@ impl Session { if let Some(editor) = doc.editor.as_mut() { // An encrypted file needs the password it is protected with now (the owner // password when known, so saving never downgrades this session's rights). - editor.cos = printcraft_cos::Document::open_with_password(bytes.clone(), editor.keys.reopen.as_deref()) + editor.cos = pdfcraft_cos::Document::open_with_password(bytes.clone(), editor.keys.reopen.as_deref()) .map_err(|e| EditError::Reopen(e.to_string()))?; } if let Some(p) = path { @@ -2051,7 +2049,7 @@ impl Session { candidates.push(Keys::default()); let (cos, keys) = candidates .into_iter() - .find_map(|k| printcraft_cos::Document::open_with_password(base.clone(), k.reopen.as_deref()).ok().map(|c| (c, k))) + .find_map(|k| pdfcraft_cos::Document::open_with_password(base.clone(), k.reopen.as_deref()).ok().map(|c| (c, k))) .ok_or_else(|| EditError::Reopen("the saved file can't be opened".into()))?; editor.cos = cos; editor.keys = keys; @@ -2065,22 +2063,22 @@ impl Session { /// The page count of another PDF (Replace Pages, Insert Pages dialogs). pub fn page_count_of(&self, name: &str, bytes: &Arc>) -> Result { - Ok(printcraft_organize::page_count(&open_source(name, bytes)?)?) + Ok(pdfcraft_organize::page_count(&open_source(name, bytes)?)?) } /// A new blank document (Create ▸ Blank page). pub fn create_blank(&self, width: f64, height: f64, pages: usize) -> Result>, EditError> { - self.write_new(&printcraft_create::blank(width, height, pages)?) + self.write_new(&pdfcraft_create::blank(width, height, pages)?) } /// A new document with one page per image (PNG, JPEG). pub fn create_from_images(&self, images: &[(String, Vec)]) -> Result>, EditError> { - self.write_new(&printcraft_create::from_images(images)?) + self.write_new(&pdfcraft_create::from_images(images)?) } /// A new document from plain text (US Letter, 11 pt Helvetica). pub fn create_from_text(&self, title: &str, text: &str) -> Result>, EditError> { - self.write_new(&printcraft_create::from_text(title, text, printcraft_create::LETTER, 11.0)?) + self.write_new(&pdfcraft_create::from_text(title, text, pdfcraft_create::LETTER, 11.0)?) } /// Reduce File Size: Acrobat's defaults (images above 225 ppi to 150 ppi, JPEG medium @@ -2102,11 +2100,11 @@ impl Session { } let editor = doc.editor.as_ref().ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default()))?; let mut cos = editor.cos.clone(); - let discarded = if discard.is_empty() { Vec::new() } else { printcraft_redact::sanitize::remove_hidden(&mut cos, discard)? }; + let discarded = if discard.is_empty() { Vec::new() } else { pdfcraft_redact::sanitize::remove_hidden(&mut cos, discard)? }; let report = optimize::optimize(&mut cos, settings).map_err(|e| EditError::Optimize(e.to_string()))?; - let all: Vec = cos.object_numbers().into_iter().map(|n| printcraft_cos::ObjRef::new(n, cos.generation(n))).collect(); - let merged = printcraft_organize::dedupe_resources(&mut cos, &all, false); - let opts = SaveOptions { mod_date: self.now().map(printcraft_cos::pdf_date), ..SaveOptions::default() }; + let all: Vec = cos.object_numbers().into_iter().map(|n| pdfcraft_cos::ObjRef::new(n, cos.generation(n))).collect(); + let merged = pdfcraft_organize::dedupe_resources(&mut cos, &all, false); + let opts = SaveOptions { mod_date: self.now().map(pdfcraft_cos::pdf_date), ..SaveOptions::default() }; let bytes = write_full(&cos, &opts).map_err(|e| EditError::Write(e.to_string()))?; Ok((Arc::new(bytes), OptimizeReport { optimize: report, merged, discarded })) } @@ -2117,18 +2115,19 @@ impl Session { let doc = self.get(id).ok_or(EditError::NoDocument)?; let cos = match doc.editor.as_ref() { Some(e) => e.cos.clone(), - None => printcraft_cos::Document::open_with_password(doc.bytes.clone(), doc.password.as_deref()) - .map_err(|e| EditError::Write(e.to_string()))?, + None => { + pdfcraft_cos::Document::open_with_password(doc.bytes.clone(), doc.password.as_deref()).map_err(|e| EditError::Write(e.to_string()))? + } }; guard(|| match format { - DataFormat::Xfdf => printcraft_xfdf::export_xfdf(&cos, comments, fields, &doc.name).into_bytes(), - DataFormat::Fdf => printcraft_xfdf::export_fdf(&cos, comments, fields, &doc.name), - other => printcraft_xfdf::export_data(&cos, other).into_bytes(), + DataFormat::Xfdf => pdfcraft_xfdf::export_xfdf(&cos, comments, fields, &doc.name).into_bytes(), + DataFormat::Fdf => pdfcraft_xfdf::export_fdf(&cos, comments, fields, &doc.name), + other => pdfcraft_xfdf::export_data(&cos, other).into_bytes(), }) .map_err(EditError::Write) } - /// The print-ready PDF for `settings` (sheets laid out for the paper; see `printcraft-print`). + /// The print-ready PDF for `settings` (sheets laid out for the paper; see `pdfcraft-print`). pub fn print_pdf(&self, id: DocId, settings: &print::Settings) -> Result, EditError> { let doc = self.get(id).ok_or(EditError::NoDocument)?; if !doc.allows_printing() { @@ -2136,17 +2135,18 @@ impl Session { } let cos = match doc.editor.as_ref() { Some(e) => e.cos.clone(), - None => printcraft_cos::Document::open_with_password(doc.bytes.clone(), doc.password.as_deref()) - .map_err(|e| EditError::Write(e.to_string()))?, + None => { + pdfcraft_cos::Document::open_with_password(doc.bytes.clone(), doc.password.as_deref()).map_err(|e| EditError::Write(e.to_string()))? + } }; - printcraft_print::impose(&cos, settings).map_err(|e| EditError::Print(e.to_string())) + pdfcraft_print::impose(&cos, settings).map_err(|e| EditError::Print(e.to_string())) } /// Combine whole files, in order, into new PDF bytes (one bookmark per file). pub fn combine(&self, sources: &[(String, Arc>)]) -> Result>, EditError> { let docs = sources.iter().map(|(n, b)| open_source(n, b)).collect::, _>>()?; - let named: Vec<(&str, &printcraft_cos::Document)> = sources.iter().map(|(n, _)| n.as_str()).zip(docs.iter()).collect(); - let out = printcraft_organize::combine(&named)?; + let named: Vec<(&str, &pdfcraft_cos::Document)> = sources.iter().map(|(n, _)| n.as_str()).zip(docs.iter()).collect(); + let out = pdfcraft_organize::combine(&named)?; self.write_new(&out) } @@ -2158,17 +2158,17 @@ impl Session { let range = range.as_deref().map(str::trim).filter(|r| !r.is_empty()); pages.push(match range { Some(r) => { - let n = printcraft_organize::page_count(d)?; - let p = printcraft_print::select_pages(n, Some(r), &[], printcraft_print::Subset::All, false) + let n = pdfcraft_organize::page_count(d)?; + let p = pdfcraft_print::select_pages(n, Some(r), &[], pdfcraft_print::Subset::All, false) .map_err(|e| EditError::Print(format!("{name}: {e}")))?; Some(p) } None => None, }); } - let named: Vec<(&str, &printcraft_cos::Document, Option<&[usize]>)> = + let named: Vec<(&str, &pdfcraft_cos::Document, Option<&[usize]>)> = sources.iter().zip(docs.iter()).zip(&pages).map(|(((n, _, _), d), p)| (n.as_str(), d, p.as_deref())).collect(); - let out = printcraft_organize::combine_selected(&named)?; + let out = pdfcraft_organize::combine_selected(&named)?; self.write_new(&out) } @@ -2178,21 +2178,21 @@ impl Session { if src.permissions().is_some_and(|p| !p.assemble()) { return Err(EditError::NotPermitted("extracting pages")); } - let out = printcraft_organize::extract_pages(src, pages)?; + let out = pdfcraft_organize::extract_pages(src, pages)?; self.write_new(&out) } /// Split the document into several new PDFs. - pub fn split(&self, id: DocId, by: &printcraft_organize::SplitBy) -> Result, EditError> { + pub fn split(&self, id: DocId, by: &pdfcraft_organize::SplitBy) -> Result, EditError> { let src = self.cos(id)?; if src.permissions().is_some_and(|p| !p.assemble()) { return Err(EditError::NotPermitted("splitting the document")); } - let n = printcraft_organize::page_count(src)?; - printcraft_organize::split_ranges(n, by) + let n = pdfcraft_organize::page_count(src)?; + pdfcraft_organize::split_ranges(n, by) .into_iter() .map(|r| { - let doc = printcraft_organize::extract_pages(src, &r.clone().collect::>())?; + let doc = pdfcraft_organize::extract_pages(src, &r.clone().collect::>())?; Ok((r.start + 1, r.end, self.write_new(&doc)?)) }) .collect() @@ -2206,18 +2206,18 @@ impl Session { if src.permissions().is_some_and(|p| !p.assemble()) { return Err(EditError::NotPermitted("splitting the document")); } - let n = printcraft_organize::page_count(src)?; + let n = pdfcraft_organize::page_count(src)?; let mut cuts = Vec::new(); let mut used = 0usize; for p in 0..n { - let size = self.write_new(&printcraft_organize::extract_pages(src, &[p])?)?.len(); + let size = self.write_new(&pdfcraft_organize::extract_pages(src, &[p])?)?.len(); if used > 0 && used + size > max_bytes { cuts.push(p); used = 0; } used += size; } - self.split(id, &printcraft_organize::SplitBy::Before(cuts)) + self.split(id, &pdfcraft_organize::SplitBy::Before(cuts)) } /// Web addresses in the text of every page (Create links from URLs): (page, one rectangle @@ -2225,15 +2225,14 @@ impl Session { pub fn find_urls(&self, id: DocId) -> Vec<(usize, Vec<[f64; 4]>, String)> { let Some(doc) = self.get(id) else { return Vec::new() }; let config = RenderConfig { password: doc.password.as_deref().map(Arc::from), ..Default::default() }; - let mut r = printcraft_render::PageRenderer::new(doc.bytes.clone(), config); + let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config); let mut out = Vec::new(); for page in 0..doc.info.pages.len() { - let res = - r.render(printcraft_render::RenderRequest { page, kind: printcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() }); + let res = r.render(pdfcraft_render::RenderRequest { page, kind: pdfcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() }); let Some(text) = res.text else { continue }; let found: std::cell::RefCell, String)>> = Default::default(); let hits = text.find_with(|chars| { - let urls = printcraft_annot::links::find_urls(chars); + let urls = pdfcraft_annot::links::find_urls(chars); let ranges = urls.iter().map(|u| u.0.clone()).collect(); *found.borrow_mut() = urls; ranges @@ -2283,13 +2282,13 @@ impl Session { Ok(id) } - fn cos(&self, id: DocId) -> Result<&printcraft_cos::Document, EditError> { + fn cos(&self, id: DocId) -> Result<&pdfcraft_cos::Document, EditError> { let doc = self.get(id).ok_or(EditError::NoDocument)?; doc.editor.as_ref().map(|e| &e.cos).ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default())) } - fn write_new(&self, doc: &printcraft_cos::Document) -> Result>, EditError> { - let opts = SaveOptions { mod_date: self.now().map(printcraft_cos::pdf_date), ..SaveOptions::default() }; + fn write_new(&self, doc: &pdfcraft_cos::Document) -> Result>, EditError> { + let opts = SaveOptions { mod_date: self.now().map(pdfcraft_cos::pdf_date), ..SaveOptions::default() }; write_full(doc, &opts).map(Arc::new).map_err(|e| EditError::Write(e.to_string())) } @@ -2360,12 +2359,12 @@ impl Session { } /// The certificates trusted for signing. - pub fn trusted_certificates(&self) -> &[printcraft_sign::Certificate] { + pub fn trusted_certificates(&self) -> &[pdfcraft_sign::Certificate] { &self.trust.certs } /// Replace the trusted certificates and revalidate every open document's signatures. - pub fn set_trusted_certificates(&mut self, certs: Vec) { + pub fn set_trusted_certificates(&mut self, certs: Vec) { self.trust = Arc::new(TrustStore { certs }); for doc in &mut self.docs { doc.trust = self.trust.clone(); @@ -2378,7 +2377,7 @@ impl Session { /// The signing time as a PDF date in local time with its offset (`D:…+02'00'`). pub fn signing_date(&self) -> String { let offset = if self.clock.is_some() { 0 } else { local_utc_offset() }; - let local = printcraft_cos::pdf_date(self.now().unwrap_or(0) + offset); + let local = pdfcraft_cos::pdf_date(self.now().unwrap_or(0) + offset); let stamp = local.trim_end_matches('Z'); if offset == 0 { return format!("{stamp}Z"); @@ -2390,14 +2389,14 @@ impl Session { /// Sign the document's current state with `id` (Use a certificate ▸ Digitally sign). Returns /// the signed file; the caller saves it and then calls [`Session::mark_signed`]. An empty /// `opts.date` takes the session clock. - pub fn sign(&self, doc: DocId, id: &printcraft_sign::DigitalId, mut opts: SignOptions) -> Result>, EditError> { + pub fn sign(&self, doc: DocId, id: &pdfcraft_sign::DigitalId, mut opts: SignOptions) -> Result>, EditError> { let d = self.get(doc).ok_or(EditError::NoDocument)?; // (Encrypted documents are refused by the signer for now.) let editor = d.editor.as_ref().ok_or_else(|| EditError::ReadOnly(d.read_only_reason.clone().unwrap_or_default()))?; if opts.date.is_empty() { opts.date = self.signing_date(); } - Ok(Arc::new(printcraft_sign::sign(&editor.cos, id, &opts)?)) + Ok(Arc::new(pdfcraft_sign::sign(&editor.cos, id, &opts)?)) } /// Record that the signed file `bytes` was saved (to `path`): like [`Session::mark_saved`], @@ -2435,15 +2434,15 @@ pub struct MarkFile { } /// Bring a mark's picture into `doc`: a PDF page as a form XObject, or an image. -fn mark_source(doc: &mut printcraft_cos::Document, f: &MarkFile) -> Result { +fn mark_source(doc: &mut pdfcraft_cos::Document, f: &MarkFile) -> Result { let head = &f.bytes[..f.bytes.len().min(1024)]; if head.windows(5).any(|w| w == b"%PDF-") { - let src = printcraft_cos::Document::open(f.bytes.clone()).map_err(|e| EditError::Source(format!("{}: {e}", f.name)))?; - let (xobject, size) = printcraft_organize::page_as_form(doc, &src, f.page)?; - Ok(printcraft_edit::MarkSource { xobject, size, image: false }) + let src = pdfcraft_cos::Document::open(f.bytes.clone()).map_err(|e| EditError::Source(format!("{}: {e}", f.name)))?; + let (xobject, size) = pdfcraft_organize::page_as_form(doc, &src, f.page)?; + Ok(pdfcraft_edit::MarkSource { xobject, size, image: false }) } else { - let (xobject, size) = printcraft_create::image_xobject(doc, &f.name, &f.bytes)?; - Ok(printcraft_edit::MarkSource { xobject, size, image: true }) + let (xobject, size) = pdfcraft_create::image_xobject(doc, &f.name, &f.bytes)?; + Ok(pdfcraft_edit::MarkSource { xobject, size, image: true }) } } @@ -2507,7 +2506,7 @@ pub fn comment_type_name(subtype: &str) -> &str { } /// [`comment_type_name`], telling callouts, clouds and typewriter text apart by `/IT`. -pub fn comment_kind(a: &printcraft_render::Annotation) -> &str { +pub fn comment_kind(a: &pdfcraft_render::Annotation) -> &str { match a.intent.as_deref() { Some("FreeTextCallout") => "Callout", Some("PolygonCloud") => "Cloud", @@ -2519,13 +2518,13 @@ pub fn comment_kind(a: &printcraft_render::Annotation) -> &str { /// The text of a comment summary: one block per comment (replies indented under it), with a /// "Page N" heading when sorted by page. Checkmarks and status replies are left out, as are /// pop-ups; the number is the comment's position on its page. -pub fn comment_summary(name: &str, all: &[printcraft_render::Annotation], sort: SummarySort) -> String { +pub fn comment_summary(name: &str, all: &[pdfcraft_render::Annotation], sort: SummarySort) -> String { use std::fmt::Write; - let top: Vec<&printcraft_render::Annotation> = all.iter().filter(|a| a.in_reply_to.is_none() && a.state.is_none()).collect(); - let replies_of = |a: &printcraft_render::Annotation| -> Vec<&printcraft_render::Annotation> { + let top: Vec<&pdfcraft_render::Annotation> = all.iter().filter(|a| a.in_reply_to.is_none() && a.state.is_none()).collect(); + let replies_of = |a: &pdfcraft_render::Annotation| -> Vec<&pdfcraft_render::Annotation> { all.iter().filter(|r| r.state.is_none() && r.in_reply_to.is_some() && r.in_reply_to == a.name && a.name.is_some()).collect() }; - let mut numbered: Vec<(usize, &printcraft_render::Annotation)> = Vec::new(); + let mut numbered: Vec<(usize, &pdfcraft_render::Annotation)> = Vec::new(); let mut last = usize::MAX; let mut n = 0; for a in &top { @@ -2594,7 +2593,7 @@ pub enum PageOrientation { } /// The pages of `pages` (0-based) that pass the filters. -pub fn filter_pages(info: &printcraft_render::DocInfo, pages: &[usize], parity: PageParity, orientation: PageOrientation) -> Vec { +pub fn filter_pages(info: &pdfcraft_render::DocInfo, pages: &[usize], parity: PageParity, orientation: PageOrientation) -> Vec { pages .iter() .copied() diff --git a/crates/engine/src/links.rs b/crates/engine/src/links.rs index fd474d05..3df31cd1 100644 --- a/crates/engine/src/links.rs +++ b/crates/engine/src/links.rs @@ -1,13 +1,13 @@ -//! Where PrintCraft and the ArtCraft community live on the web. One table, so the Help menu, the +//! Where PdfCraft and the ArtCraft community live on the web. One table, so the Help menu, the //! About dialog, the home screen, the CLI and the README agree. /// The app's name in ArtCraft URLs (`getartcraft.com/apps/{APP}`, `github.com/storytold/{APP}`). -pub const APP: &str = "printcraft"; +pub const APP: &str = "pdfcraft"; pub const DISCORD: &str = "https://discord.gg/artcraft"; pub const WEBSITE: &str = "https://getartcraft.com"; -pub const APP_PAGE: &str = "https://getartcraft.com/apps/printcraft"; -pub const GITHUB: &str = "https://github.com/storytold/printcraft"; +pub const APP_PAGE: &str = "https://getartcraft.com/apps/pdfcraft"; +pub const GITHUB: &str = "https://github.com/storytold/pdfcraft"; /// A link and the registry command that opens it. #[derive(Clone, Copy, Debug)] @@ -22,8 +22,8 @@ pub struct Link { /// In the order they are shown. Discord comes first: it is where people get help fastest. pub const LINKS: &[Link] = &[ Link { command: "help.discord", label: "Join the ArtCraft Discord", url: DISCORD, icon: "messages-square" }, - Link { command: "help.app_page", label: "PrintCraft web page", url: APP_PAGE, icon: "globe" }, - Link { command: "help.github", label: "PrintCraft on GitHub", url: GITHUB, icon: "code-xml" }, + Link { command: "help.app_page", label: "PdfCraft web page", url: APP_PAGE, icon: "globe" }, + Link { command: "help.github", label: "PdfCraft on GitHub", url: GITHUB, icon: "code-xml" }, Link { command: "help.website", label: "ArtCraft website", url: WEBSITE, icon: "external-link" }, ]; diff --git a/crates/engine/src/ocr.rs b/crates/engine/src/ocr.rs index 4c803372..cb245f14 100644 --- a/crates/engine/src/ocr.rs +++ b/crates/engine/src/ocr.rs @@ -7,9 +7,9 @@ use std::sync::{Arc, Mutex}; -use printcraft_render::{PageInfo, PageRenderer, RenderConfig, RenderRequest, RequestKind}; +use pdfcraft_render::{PageInfo, PageRenderer, RenderConfig, RenderRequest, RequestKind}; -pub use printcraft_ocr::{LANGUAGES, Models, Ocr, OcrError, PlacedWord}; +pub use pdfcraft_ocr::{LANGUAGES, Models, Ocr, OcrError, PlacedWord}; use crate::{DocId, Edit, EditError, Session}; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 545dcb0f..a0f8a32d 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -38,8 +38,8 @@ fn session_with(n: usize) -> (Session, DocId) { fn page_texts(s: &Session, id: DocId) -> Vec { let doc = s.get(id).unwrap(); - let config = printcraft_render::RenderConfig { password: doc.password.as_deref().map(Arc::from), ..Default::default() }; - let mut r = printcraft_render::PageRenderer::new(doc.bytes.clone(), config); + let config = pdfcraft_render::RenderConfig { password: doc.password.as_deref().map(Arc::from), ..Default::default() }; + let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config); (0..doc.info.pages.len()) .map(|p| { let r = r.render(RenderRequestFor::text(p)); @@ -51,8 +51,8 @@ fn page_texts(s: &Session, id: DocId) -> Vec { /// Shorthand for building text-extraction requests. struct RenderRequestFor; impl RenderRequestFor { - fn text(page: usize) -> printcraft_render::RenderRequest { - printcraft_render::RenderRequest { page, kind: printcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() } + fn text(page: usize) -> pdfcraft_render::RenderRequest { + pdfcraft_render::RenderRequest { page, kind: pdfcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() } } } @@ -94,7 +94,7 @@ fn failed_edit_changes_nothing() { let (mut s, id) = session_with(2); let before = s.get(id).unwrap().bytes.clone(); let err = s.apply(id, Edit::DeletePages { pages: vec![0, 1] }).unwrap_err(); - assert_eq!(err, EditError::Organize(printcraft_organize::OrganizeError::WouldRemoveAllPages)); + assert_eq!(err, EditError::Organize(pdfcraft_organize::OrganizeError::WouldRemoveAllPages)); let doc = s.get(id).unwrap(); assert!(!doc.dirty); assert_eq!(doc.can_undo(), None); @@ -121,7 +121,7 @@ fn save_is_incremental_stamps_mod_date_and_rebases() { s.apply(id, Edit::RotatePages { pages: vec![1], degrees: -90 }).unwrap(); let second = s.save_bytes(id).unwrap(); assert_eq!(&second[..saved.len()], &saved[..]); - let reopened = printcraft_cos::Document::open(second).unwrap(); + let reopened = pdfcraft_cos::Document::open(second).unwrap(); assert_eq!(reopened.revisions().len(), 3); } @@ -206,7 +206,7 @@ fn combine_extract_split_and_insert_from_file() { let eid = s.open_new("Extract.pdf", ex).unwrap(); assert_eq!(page_texts(&s, eid), ["Page 3", "Page 1"]); // Split every 2 pages. - let parts = s.split(id, &printcraft_organize::SplitBy::PageCount(2)).unwrap(); + let parts = s.split(id, &pdfcraft_organize::SplitBy::PageCount(2)).unwrap(); assert_eq!(parts.iter().map(|(a, b, _)| (*a, *b)).collect::>(), [(1, 2), (3, 3)]); // Insert pages from a file, undoably, into the open document. s.apply(id, Edit::InsertPagesFrom { name: "b.pdf".into(), bytes: other, pages: Some(vec![1]), at: 1 }).unwrap(); @@ -219,9 +219,9 @@ fn combine_extract_split_and_insert_from_file() { } fn protected(user: &str, owner: &str, permissions: i32) -> Arc> { - let mut doc = printcraft_cos::Document::open(Arc::new(fixture(2))).unwrap(); - doc.set_encryption(&printcraft_cos::NewEncryption { - algorithm: printcraft_cos::Algorithm::Aes256, + let mut doc = pdfcraft_cos::Document::open(Arc::new(fixture(2))).unwrap(); + doc.set_encryption(&pdfcraft_cos::NewEncryption { + algorithm: pdfcraft_cos::Algorithm::Aes256, user_password: user, owner_password: owner, permissions, @@ -229,7 +229,7 @@ fn protected(user: &str, owner: &str, permissions: i32) -> Arc> { seed: [7; 32], }) .unwrap(); - Arc::new(printcraft_cos::write_full(&doc, &Default::default()).unwrap()) + Arc::new(pdfcraft_cos::write_full(&doc, &Default::default()).unwrap()) } #[test] @@ -244,7 +244,7 @@ fn encrypted_documents_open_edit_and_save_encrypted() { s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 }).unwrap(); let saved = s.save_bytes(id).unwrap(); assert_eq!(&saved[..bytes.len()], &bytes[..], "incremental"); - assert!(printcraft_cos::Document::open(saved.clone()).is_err(), "still protected after saving"); + assert!(pdfcraft_cos::Document::open(saved.clone()).is_err(), "still protected after saving"); let mut s2 = Session::new(); let id2 = s2.open("x.pdf", None, saved, Some("pw")).unwrap(); assert_eq!(s2.get(id2).unwrap().info.pages[0].rotation, 90); @@ -280,9 +280,9 @@ fn combining_protected_files_is_refused_clearly() { #[test] fn owner_password_of_older_revisions_opens_the_viewer_too() { - for alg in [printcraft_cos::Algorithm::Rc4_128, printcraft_cos::Algorithm::Aes128] { - let mut doc = printcraft_cos::Document::open(Arc::new(fixture(1))).unwrap(); - doc.set_encryption(&printcraft_cos::NewEncryption { + for alg in [pdfcraft_cos::Algorithm::Rc4_128, pdfcraft_cos::Algorithm::Aes128] { + let mut doc = pdfcraft_cos::Document::open(Arc::new(fixture(1))).unwrap(); + doc.set_encryption(&pdfcraft_cos::NewEncryption { algorithm: alg, user_password: "u", owner_password: "o", @@ -291,7 +291,7 @@ fn owner_password_of_older_revisions_opens_the_viewer_too() { seed: [1; 32], }) .unwrap(); - let bytes = Arc::new(printcraft_cos::write_full(&doc, &Default::default()).unwrap()); + let bytes = Arc::new(pdfcraft_cos::write_full(&doc, &Default::default()).unwrap()); let mut s = Session::new(); let id = s.open("x.pdf", None, bytes, Some("o")).unwrap_or_else(|e| panic!("{alg:?}: {e}")); let d = s.get(id).unwrap(); @@ -332,7 +332,7 @@ fn recovered_documents_reopen_unsaved_at_their_original_path() { assert!(s2.autosave_snapshots().is_empty(), "already in the recovery store"); } -fn outline_titles(items: &[printcraft_render::OutlineItem]) -> Vec { +fn outline_titles(items: &[pdfcraft_render::OutlineItem]) -> Vec { items .iter() .map(|o| { @@ -374,7 +374,7 @@ fn bookmark_edits_show_in_the_viewer_undo_and_save() { fn number_pages_shows_in_the_viewer_and_undoes() { let (mut s, id) = session_with(5); let labels = |s: &Session| s.get(id).unwrap().info.pages.iter().map(|p| p.label.clone()).collect::>(); - use printcraft_organize::LabelStyle; + use pdfcraft_organize::LabelStyle; s.apply(id, Edit::NumberPages { from: 0, to: 1, style: LabelStyle::LowerRoman, prefix: String::new(), first: 1 }).unwrap(); s.apply(id, Edit::NumberPages { from: 2, to: 4, style: LabelStyle::Decimal, prefix: "§".into(), first: 10 }).unwrap(); // The inspector (lopdf-based, independent) formats them the same way. @@ -387,8 +387,8 @@ fn number_pages_shows_in_the_viewer_and_undoes() { /// RGBA of the pixel at PDF point (x, y) on `page`, rendered at 1 px/pt (page height 300). fn pixel(s: &Session, id: DocId, page: usize, x: u32, y: u32) -> [u8; 4] { let doc = s.get(id).unwrap(); - let mut r = printcraft_render::PageRenderer::new(doc.bytes.clone(), doc.config.clone()); - let out = r.render(printcraft_render::RenderRequest { page, scale: 1.0, ..Default::default() }); + let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), doc.config.clone()); + let out = r.render(pdfcraft_render::RenderRequest { page, scale: 1.0, ..Default::default() }); assert!(out.error.is_none(), "{:?}", out.error); let i = (((300 - y) * out.width + x) * 4) as usize; out.rgba[i..i + 4].try_into().unwrap() @@ -468,10 +468,10 @@ fn highlight_multiplies_over_text() { #[test] fn comment_permission_is_enforced() { - let mut cos = printcraft_cos::Document::open(Arc::new(fixture(1))).unwrap(); + let mut cos = pdfcraft_cos::Document::open(Arc::new(fixture(1))).unwrap(); // Owner "own", empty user password, everything allowed except commenting (bit 6). - let params = printcraft_cos::NewEncryption { - algorithm: printcraft_cos::Algorithm::Aes256, + let params = pdfcraft_cos::NewEncryption { + algorithm: pdfcraft_cos::Algorithm::Aes256, user_password: "", owner_password: "own", permissions: !(1 << 5), @@ -479,7 +479,7 @@ fn comment_permission_is_enforced() { seed: [7; 32], }; cos.set_encryption(¶ms).unwrap(); - let bytes = printcraft_cos::write_full(&cos, &printcraft_cos::SaveOptions::default()).unwrap(); + let bytes = pdfcraft_cos::write_full(&cos, &pdfcraft_cos::SaveOptions::default()).unwrap(); let mut s = Session::new(); let id = s.open("locked.pdf", None, Arc::new(bytes), None).unwrap(); let err = s.apply(id, rect_comment(0, [10.0, 10.0, 50.0, 50.0])).unwrap_err(); @@ -513,7 +513,7 @@ fn protect_with_an_open_password_then_save_reopen_and_undo() { assert_eq!(d.security_summary().unwrap().method, "AES, 256-bit"); assert_eq!(page_texts(&s, id), ["Page 1", "Page 2"], "still viewable in this session"); let saved = s.save_bytes(id).unwrap(); - assert!(printcraft_cos::Document::open(saved.clone()).is_err(), "needs the password"); + assert!(pdfcraft_cos::Document::open(saved.clone()).is_err(), "needs the password"); s.mark_saved(id, saved.clone(), None).unwrap(); // Further edits keep working and saving stays encrypted. s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 }).unwrap(); @@ -526,7 +526,7 @@ fn protect_with_an_open_password_then_save_reopen_and_undo() { assert!(s2.get(id2).unwrap().allows_security_change()); s2.apply(id2, Edit::RemoveProtection).unwrap(); let plain = s2.save_bytes(id2).unwrap(); - assert!(printcraft_cos::Document::open(plain).is_ok()); + assert!(pdfcraft_cos::Document::open(plain).is_ok()); } #[test] @@ -557,7 +557,7 @@ fn protection_is_validated_undoable_and_never_logged() { let (mut s, id) = session_with(1); assert!(matches!(s.apply(id, Edit::Protect(protection(None, None))), Err(EditError::Protection(_)))); assert!(matches!(s.apply(id, Edit::Protect(protection(Some("same"), Some("same")))), Err(EditError::Protection(_)))); - let rc4 = Protection { algorithm: printcraft_cos::Algorithm::Rc4_128, ..protection(Some("pässword"), None) }; + let rc4 = Protection { algorithm: pdfcraft_cos::Algorithm::Rc4_128, ..protection(Some("pässword"), None) }; assert!(matches!(s.apply(id, Edit::Protect(rc4)), Err(EditError::Protection(_)))); assert!(!format!("{:?}", Edit::Protect(protection(Some("hunter2"), Some("x")))).contains("hunter2")); s.apply(id, Edit::Protect(protection(Some("pw"), Some("owner")))).unwrap(); @@ -645,7 +645,7 @@ fn comment_edits_refresh_the_list_exactly_as_a_full_inspection_would() { s.apply(id, Edit::DeleteAnnotation { page: 1, index: 0 }).unwrap(); s.undo(id).unwrap(); let d = s.get(id).unwrap(); - let full = printcraft_render::inspect(d.bytes.clone(), None).unwrap(); + let full = pdfcraft_render::inspect(d.bytes.clone(), None).unwrap(); assert_eq!(format!("{:?}", d.info.annotations), format!("{:?}", full.annotations)); assert_eq!(d.info.file_size, full.file_size); assert_eq!(d.info.annotations.len(), 7); @@ -659,8 +659,8 @@ fn form_edits_refresh_field_values_without_a_full_inspection() { s.apply(id, Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text("Ada".into()) }).unwrap(); s.apply(id, Edit::SetFieldValue { name: "ok".into(), value: FieldValue::Check(true) }).unwrap(); let d = s.get(id).unwrap(); - let full = printcraft_render::inspect(d.bytes.clone(), None).unwrap(); - let values = |fs: &[printcraft_render::Field]| fs.iter().map(|f| (f.name.clone(), f.value.clone())).collect::>(); + let full = pdfcraft_render::inspect(d.bytes.clone(), None).unwrap(); + let values = |fs: &[pdfcraft_render::Field]| fs.iter().map(|f| (f.name.clone(), f.value.clone())).collect::>(); assert_eq!(values(&d.info.fields), values(&full.fields)); assert_eq!(values(&d.info.fields), [("name".to_string(), Some("Ada".to_string())), ("ok".to_string(), Some("Yes".to_string()))]); } @@ -698,7 +698,7 @@ fn redaction_marks_apply_for_good_and_undo() { let (mut s, id) = session_with(2); // "Page 1" at 24 pt from x 20: the "1" starts near x 82.7 (Helvetica widths). let shape = - Shape::Redact { quads: vec![printcraft_annot::rect_quad([80.0, 140.0, 100.0, 180.0])], overlay: String::new(), look: Default::default() }; + Shape::Redact { quads: vec![pdfcraft_annot::rect_quad([80.0, 140.0, 100.0, 180.0])], overlay: String::new(), look: Default::default() }; let mark = Edit::AddAnnotation(NewAnnotation { page: 0, style: Style::default_for(&shape), shape, contents: String::new(), author: "Ada".into() }); s.apply(id, mark).unwrap(); @@ -917,8 +917,8 @@ fn added_images_rotate_flip_and_crop_as_drawn() { s.apply(id, Edit::AddImage { page: 0, rect: Some([50.0, 100.0, 150.0, 200.0]), name: "rb.png".into(), bytes: Arc::new(png) }).unwrap(); let colour_at = |s: &Session, x: u32, y_from_top: u32| -> [u8; 3] { let doc = s.get(id).unwrap(); - let mut r = printcraft_render::PageRenderer::new(doc.bytes.clone(), Default::default()); - let out = r.render(printcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() }); + let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), Default::default()); + let out = r.render(pdfcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() }); let i = ((y_from_top * out.width + x) * 4) as usize; [out.rgba[i], out.rgba[i + 1], out.rgba[i + 2]] }; @@ -961,7 +961,7 @@ fn revert_goes_back_to_the_saved_version() { fn split_by_size_and_bookmarks_and_page_filters() { let (mut s, id) = session_with(6); // Every page alone is a few hundred bytes: a limit of about two pages gives three parts. - let one = s.split(id, &printcraft_organize::SplitBy::PageCount(1)).unwrap()[0].2.len(); + let one = s.split(id, &pdfcraft_organize::SplitBy::PageCount(1)).unwrap()[0].2.len(); let parts = s.split_by_size(id, one * 2 + one / 2).unwrap(); assert!(parts.len() >= 2 && parts.len() <= 6, "{}", parts.len()); assert_eq!(parts.last().unwrap().1, 6, "every page is in a part"); @@ -1397,7 +1397,7 @@ fn exporting_office_files_keeps_images() { assert_eq!(d.export_pages()[0].images.len(), 1); let docx = d.export_office(compare::OfficeFormat::Docx); assert!(docx.windows(16).any(|w| w == b"word/media/image")); - if let Ok(dir) = std::env::var("PRINTCRAFT_EXPORT_DIR") { + if let Ok(dir) = std::env::var("PDFCRAFT_EXPORT_DIR") { std::fs::write(format!("{dir}/pic.docx"), &docx).unwrap(); } assert!(String::from_utf8(d.export_office(compare::OfficeFormat::Html)).unwrap().contains("data:image/png;base64,")); diff --git a/crates/export/Cargo.toml b/crates/export/Cargo.toml index 0d5d8bea..f318d953 100644 --- a/crates/export/Cargo.toml +++ b/crates/export/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-export" +name = "pdfcraft-export" description = "Export a PDF to HTML, Word (.docx) and RTF from its paragraphs and images" version.workspace = true edition.workspace = true diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs index 5dd50766..e92d09ec 100644 --- a/crates/export/src/lib.rs +++ b/crates/export/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-export — Export a PDF ▸ Word, HTML, RTF (L4). +//! pdfcraft-export — Export a PDF ▸ Word, HTML, RTF (L4). //! //! The engine reduces each page to [`Page`]: paragraphs (text, box, size, bold/italic) and //! images (encoded bytes and box), in reading order. The writers turn that into a flowing diff --git a/crates/filters/Cargo.toml b/crates/filters/Cargo.toml index f8ea7c19..9d2ae34b 100644 --- a/crates/filters/Cargo.toml +++ b/crates/filters/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-filters" +name = "pdfcraft-filters" description = "PDF stream filters (ISO 32000-2 §7.4): Flate, LZW, predictors, ASCII85, ASCIIHex, RunLength" version.workspace = true edition.workspace = true diff --git a/crates/filters/README.md b/crates/filters/README.md index a35749d4..de7f8d22 100644 --- a/crates/filters/README.md +++ b/crates/filters/README.md @@ -1,8 +1,8 @@ -# printcraft-filters +# pdfcraft-filters PDF stream filters: the decoders and encoders behind `/Filter` and `/DecodeParms`. -- **Layer:** L0, standalone. No dependency on any other PrintCraft crate. `cos` depends on it (architecture §3 rule 3). +- **Layer:** L0, standalone. No dependency on any other PdfCraft crate. `cos` depends on it (architecture §3 rule 3). - **Licence:** MIT OR Apache-2.0. Clean-room: written from ISO 32000-2 §7.4, RFC 1950/1951 (through `flate2` with the pure-Rust `miniz_oxide` backend), TIFF 6.0 §14 (predictor 2) and RFC 2083 §6 (PNG filters). - `#![forbid(unsafe_code)]`. Builds for `wasm32-unknown-unknown`. diff --git a/crates/filters/fuzz/Cargo.toml b/crates/filters/fuzz/Cargo.toml index efa0774d..27be5f28 100644 --- a/crates/filters/fuzz/Cargo.toml +++ b/crates/filters/fuzz/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-filters-fuzz" +name = "pdfcraft-filters-fuzz" version = "0.0.0" publish = false edition = "2024" @@ -8,13 +8,13 @@ license = "MIT OR Apache-2.0" [package.metadata] cargo-fuzz = true -# Detached from the PrintCraft workspace: run with `cargo +nightly fuzz run decode` +# Detached from the PdfCraft workspace: run with `cargo +nightly fuzz run decode` # from crates/filters/. [workspace] [dependencies] libfuzzer-sys = "0.4" -printcraft-filters = { path = ".." } +pdfcraft-filters = { path = ".." } [[bin]] name = "decode" diff --git a/crates/filters/fuzz/fuzz_targets/decode.rs b/crates/filters/fuzz/fuzz_targets/decode.rs index c9923ed7..22415b95 100644 --- a/crates/filters/fuzz/fuzz_targets/decode.rs +++ b/crates/filters/fuzz/fuzz_targets/decode.rs @@ -2,7 +2,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use printcraft_filters::{Filter, Params, decode, decode_tolerant}; +use pdfcraft_filters::{Filter, Params, decode, decode_tolerant}; const FILTERS: [Filter; 5] = [Filter::Flate, Filter::Lzw, Filter::AsciiHex, Filter::Ascii85, Filter::RunLength]; const PREDICTORS: [i64; 8] = [1, 2, 10, 11, 12, 13, 14, 15]; diff --git a/crates/filters/src/lib.rs b/crates/filters/src/lib.rs index 6ec4a5de..ffaf08ca 100644 --- a/crates/filters/src/lib.rs +++ b/crates/filters/src/lib.rs @@ -1,6 +1,6 @@ //! PDF stream filters (ISO 32000-2 §7.4). //! -//! Layer L0, standalone: this crate depends on no other PrintCraft crate. +//! Layer L0, standalone: this crate depends on no other PdfCraft crate. //! //! Decoding covers the general-purpose filters (`FlateDecode`, `LZWDecode` with //! TIFF/PNG predictors, `ASCIIHexDecode`, `ASCII85Decode`, `RunLengthDecode`). diff --git a/crates/filters/tests/proptests.rs b/crates/filters/tests/proptests.rs index a4300e68..cbf175b0 100644 --- a/crates/filters/tests/proptests.rs +++ b/crates/filters/tests/proptests.rs @@ -1,7 +1,7 @@ //! Round-trip property tests (encode → decode) for every encodable filter and predictor, //! and fuzz-style tests feeding arbitrary bytes and parameters to every decoder. -use printcraft_filters::{Filter, FilterError, Params, decode, decode_tolerant, encode}; +use pdfcraft_filters::{Filter, FilterError, Params, decode, decode_tolerant, encode}; use proptest::prelude::*; const MAX: usize = 1 << 24; @@ -142,7 +142,7 @@ proptest! { #[test] fn flate_bomb_in_chain() { - let bomb = printcraft_filters::encode_flate(&vec![0u8; 32 << 20]); + let bomb = pdfcraft_filters::encode_flate(&vec![0u8; 32 << 20]); let a85 = encode(&Filter::Ascii85, &Params::default(), &bomb).unwrap(); let chain = [(Filter::Ascii85, Params::default()), (Filter::Flate, Params::default())]; assert!(matches!(decode(&chain, &a85, 1 << 20), Err(FilterError::LimitExceeded(_)))); diff --git a/crates/fonts/Cargo.toml b/crates/fonts/Cargo.toml index 8f3970d9..e2b38801 100644 --- a/crates/fonts/Cargo.toml +++ b/crates/fonts/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-fonts" +name = "pdfcraft-fonts" description = "Font metrics and text encoding helpers for generated appearances (standard fonts, WinAnsi)" version.workspace = true edition.workspace = true @@ -9,6 +9,6 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } +pdfcraft-cos = { workspace = true } # Glyph outlines of the bundled script font (typed signatures); already in the tree via egui. skrifa = "0.44" diff --git a/crates/fonts/README.md b/crates/fonts/README.md index 0e02c0e3..a3be1fe1 100644 --- a/crates/fonts/README.md +++ b/crates/fonts/README.md @@ -1,11 +1,11 @@ -# printcraft-fonts +# pdfcraft-fonts -Layer L2. Font helpers for the appearance streams PrintCraft generates (comments, form fields). +Layer L2. Font helpers for the appearance streams PdfCraft generates (comments, form fields). Today it holds what generated appearances need without a font program: - `helvetica_width`: an approximation of Helvetica's proportions by character class. No metrics - file or font program from any vendor is bundled; widths are PrintCraft's own estimates, good + file or font program from any vendor is bundled; widths are PdfCraft's own estimates, good enough for line breaking and alignment, not for typesetting. - `wrap`: greedy line breaking with that measure (paragraphs on newlines, long words split). - `win_ansi`: Unicode → WinAnsiEncoding bytes (`?` for characters it can't represent), and diff --git a/crates/fonts/src/craft.rs b/crates/fonts/src/craft.rs index 0898b167..7bb82fa6 100644 --- a/crates/fonts/src/craft.rs +++ b/crates/fonts/src/craft.rs @@ -1,6 +1,6 @@ //! Fonts from the optional craft-fonts build input (). //! -//! `build.rs` embeds every font in craft-fonts' manifest when PrintCraft is built with +//! `build.rs` embeds every font in craft-fonts' manifest when PdfCraft is built with //! `CRAFT_FONTS_DIR=`; otherwise [`CRAFT_FONTS`] is empty and everything here returns //! nothing. Callers must work either way. @@ -75,7 +75,7 @@ const fn find(family: &str, style: &str) -> Option<&'static [u8]> { } /// Shippori Mincho Regular from craft-fonts, the preferred face for Japanese document text. -/// `None` when PrintCraft was built without craft-fonts. +/// `None` when PdfCraft was built without craft-fonts. pub static SHIPPORI_MINCHO: Option<&[u8]> = find("Shippori Mincho", "Regular"); #[cfg(test)] diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index a0d80d96..2d39c1a7 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-fonts — font metrics and encodings for generated appearances (L2). +//! pdfcraft-fonts — font metrics and encodings for generated appearances (L2). //! //! See the README: the metrics are approximations by character class (no vendor metrics files //! are bundled). The full font subsystem lands in M2.2/M7. diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 91c326c7..42c87d18 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; -use printcraft_cos::{Dict, Document, Object}; +use pdfcraft_cos::{Dict, Document, Object}; /// How the bytes of a string map to codes. #[derive(Clone, Debug, PartialEq)] @@ -491,7 +491,7 @@ fn unicode_map(doc: &Document, font: &Dict, composite: bool) -> HashMap Vec { } } -fn read(doc: &Document, o: &Object, pages: &[printcraft_cos::ObjRef]) -> Option { +fn read(doc: &Document, o: &Object, pages: &[pdfcraft_cos::ObjRef]) -> Option { let a = doc.resolve(o); let d = a.as_dict()?; Some(match d.name(b"S")? { @@ -158,7 +158,7 @@ pub fn field_actions(doc: &Document, name: &str) -> Result Result, FormError> { +fn write(doc: &mut Document, a: &FieldAction, pages: &[pdfcraft_cos::ObjRef]) -> Result, FormError> { let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("Action")); let list = |v: &[String]| Object::Array(v.iter().map(|s| Object::String(PdfString::text(s))).collect()); diff --git a/crates/forms/src/af.rs b/crates/forms/src/af.rs index e82783b2..7eddf769 100644 --- a/crates/forms/src/af.rs +++ b/crates/forms/src/af.rs @@ -1074,7 +1074,7 @@ mod tests { // ── push buttons ──────────────────────────────────────────────────────────────────────────── -/// What clicking a push button does (its mouse-up action), as far as PrintCraft can run it +/// What clicking a push button does (its mouse-up action), as far as PdfCraft can run it /// without a JavaScript engine. #[derive(Clone, Debug, PartialEq)] pub enum ButtonAction { @@ -1090,18 +1090,18 @@ pub enum ButtonAction { GoTo(usize), /// `app.alert("…")`. Alert(String), - /// Submit the form to a URL (not sent: PrintCraft never posts form data on its own). + /// Submit the form to a URL (not sent: PdfCraft never posts form data on its own). Submit(String), /// `event.target.buttonImportIcon()`: choose an image for the button (an image field). ImportIcon, - /// A script PrintCraft can't run yet. + /// A script PdfCraft can't run yet. Script(String), } /// Recognise the common one-line button scripts. pub fn button_script(js: &str) -> ButtonAction { let t = js.trim(); - // Print takes an options object whose contents don't change what PrintCraft does. + // Print takes an options object whose contents don't change what PdfCraft does. if t.starts_with("this.print(") || t.starts_with("print(") || t.contains(";this.print(") || t.contains("; this.print(") { return ButtonAction::Named("Print".into()); } diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index 97b895d8..f13f53f8 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -7,10 +7,10 @@ //! //! The `/DA` font is used when the form's `/DR` defines it as a simple font; text is encoded in //! WinAnsi. Otherwise (composite fonts, missing resources) Helvetica is used, so text is always -//! visible. Widths use the approximate Helvetica metrics of `printcraft-fonts`. +//! visible. Widths use the approximate Helvetica metrics of `pdfcraft-fonts`. -use printcraft_cos::{Dict, Document, Object, Stream}; -use printcraft_fonts::{helvetica_width, literal, win_ansi, wrap}; +use pdfcraft_cos::{Dict, Document, Object, Stream}; +use pdfcraft_fonts::{helvetica_width, literal, win_ansi, wrap}; use crate::{Field, FieldKind, Widget, acroform, flags}; diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index 7fd7ab7e..e7f4e8a0 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -5,8 +5,8 @@ //! check boxes), a thin grey border and a white background, and appearance streams generated //! immediately so every viewer shows the empty field. -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; -use printcraft_fonts::{helvetica_width, literal, win_ansi}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; +use pdfcraft_fonts::{helvetica_width, literal, win_ansi}; use crate::{Field, FieldKind, FormError, Widget, appearance, fields, flags, page_refs}; @@ -218,7 +218,7 @@ impl FieldFont { } /// The mark a check box or radio button shows when on (Field Properties ▸ Options ▸ style). -/// Stored as `/MK /CA`, the ZapfDingbats character Acrobat uses for it; PrintCraft draws the +/// Stored as `/MK /CA`, the ZapfDingbats character Acrobat uses for it; PdfCraft draws the /// mark as paths, so no symbol font is needed. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CheckStyle { diff --git a/crates/forms/src/detect.rs b/crates/forms/src/detect.rs index a116b054..d7a57482 100644 --- a/crates/forms/src/detect.rs +++ b/crates/forms/src/detect.rs @@ -5,8 +5,8 @@ //! [`detect`] is a pure function over the page's words and drawn shapes; [`page_shapes`] reads //! the shapes from a page's content. -use printcraft_content::{Matrix, parse}; -use printcraft_cos::{Document, Object}; +use pdfcraft_content::{Matrix, parse}; +use pdfcraft_cos::{Document, Object}; /// A piece of text on the page, in user space. #[derive(Clone, Debug, PartialEq)] @@ -42,7 +42,7 @@ pub struct Candidate { /// The rectangles and horizontal segments a page draws (stroked or filled), in user space. pub fn page_shapes(doc: &Document, page: usize) -> Shapes { let mut out = Shapes::default(); - let Some(p) = printcraft_model::pages(doc).into_iter().nth(page) else { return out }; + let Some(p) = pdfcraft_model::pages(doc).into_iter().nth(page) else { return out }; let list: Vec = match p.dict.get(b"Contents").map(|c| doc.resolve(c)) { Some(c) => match &*c { Object::Array(a) => a.clone(), diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 070ab8e6..5c3d3df3 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-forms — interactive forms (AcroForm, ISO 32000-2 §12.7), execution plan M6.1–M6.2. +//! pdfcraft-forms — interactive forms (AcroForm, ISO 32000-2 §12.7), execution plan M6.1–M6.2. //! //! - [`fields`]: the field tree flattened to terminal fields, each with its widgets (page, //! rectangle, on-state), inherited attributes (`/FT`, `/Ff`, `/V`, `/DV`, `/DA`, `/Q`, @@ -13,7 +13,7 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; mod actions; pub mod af; @@ -45,7 +45,7 @@ pub enum FormError { #[error("{0}")] Invalid(String), #[error("{0}")] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -801,7 +801,7 @@ fn set_states(doc: &mut Document, f: &Field, on: Option<&str>) -> Result<(), For (Some(c), Some(s)) if c == s => s, _ => "Off", }; - // A widget without appearances for its states gets PrintCraft's own. + // A widget without appearances for its states gets PdfCraft's own. let has_ap = doc.get(w.obj).as_dict().and_then(|d| d.get(b"AP").cloned()).is_some(); if !has_ap { let on_name = w.on_state.clone().unwrap_or_else(|| on.unwrap_or("Yes").to_string()); diff --git a/crates/forms/src/scripting.rs b/crates/forms/src/scripting.rs index e22540e9..9e9a36f2 100644 --- a/crates/forms/src/scripting.rs +++ b/crates/forms/src/scripting.rs @@ -6,7 +6,7 @@ //! calculation order, Format when the appearance is drawn — and applies what the script asks //! for: the event's value, `rc`, and changes to other fields. -use printcraft_cos::{Document, Object, PdfString}; +use pdfcraft_cos::{Document, Object, PdfString}; use crate::{Field, FieldKind, FieldValue, FormError, fields, flags}; @@ -218,7 +218,7 @@ pub fn set_document_script(doc: &mut Document, name: &str, js: Option<&str>) -> } } if let Some(js) = js { - let mut action = printcraft_cos::Dict::new(); + let mut action = pdfcraft_cos::Dict::new(); action.set(b"S".to_vec(), Object::name("JavaScript")); action.set(b"JS".to_vec(), PdfString::text(js)); let a = doc.add(Object::Dict(action)); @@ -229,7 +229,7 @@ pub fn set_document_script(doc: &mut Document, name: &str, js: Option<&str>) -> _ => String::new(), }); pairs = kept.into_iter().flat_map(|(k, v)| [k, v]).collect(); - let mut t = printcraft_cos::Dict::new(); + let mut t = pdfcraft_cos::Dict::new(); t.set(b"Names".to_vec(), Object::Array(pairs)); names.set(b"JavaScript".to_vec(), Object::Dict(t)); doc.update_dict(root, |c| c.set(b"Names".to_vec(), Object::Dict(names)))?; @@ -244,7 +244,7 @@ pub fn set_field_script(doc: &mut Document, name: &str, event: &str, js: Option< let all = fields(doc); let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?.clone(); let action = |doc: &mut Document, js: &str| { - let mut a = printcraft_cos::Dict::new(); + let mut a = pdfcraft_cos::Dict::new(); a.set(b"S".to_vec(), Object::name("JavaScript")); a.set(b"JS".to_vec(), PdfString::text(js)); Object::Ref(doc.add(Object::Dict(a))) diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 7273cb08..9a88905b 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use printcraft_cos::{Document, Object, SaveOptions, write_incremental}; +use pdfcraft_cos::{Document, Object, SaveOptions, write_incremental}; use super::*; @@ -133,7 +133,7 @@ fn check_boxes_and_radios_switch_states() { let size = field(&all, "size"); assert_eq!(size.value, ["L"]); assert_eq!(size.widgets.iter().map(|w| w.state.clone().unwrap()).collect::>(), ["Off", "L"]); - // The bare check box got PrintCraft's own appearances and kept its other keys. + // The bare check box got PdfCraft's own appearances and kept its other keys. let bare = field(&all, "bare"); assert_eq!(bare.value, ["Yes"]); assert_eq!(bare.widgets[0].on_state.as_deref(), Some("Yes")); @@ -208,7 +208,7 @@ fn every_field_type_can_be_added_named_and_drawn() { { // One page. let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap(); - let mut p = printcraft_cos::Dict::new(); + let mut p = pdfcraft_cos::Dict::new(); p.set(b"Type".to_vec(), Object::name("Page")); p.set(b"Parent".to_vec(), Object::Ref(pages)); p.set(b"MediaBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 600.into(), 800.into()])); @@ -312,7 +312,7 @@ fn properties_rename_and_delete() { fn deleting_works_with_a_form_dictionary_inside_the_catalog() { let mut doc = fixture(); let root = doc.root().unwrap(); - let af = doc.get(printcraft_cos::ObjRef::new(4, 0)).as_dict().cloned().unwrap(); + let af = doc.get(pdfcraft_cos::ObjRef::new(4, 0)).as_dict().cloned().unwrap(); doc.update_dict(root, |d| d.set(b"AcroForm".to_vec(), Object::Dict(af))).unwrap(); let before = fields(&doc).len(); delete_field(&mut doc, "name").unwrap(); @@ -324,7 +324,7 @@ fn deleting_works_with_a_form_dictionary_inside_the_catalog() { fn one_page() -> Document { let mut doc = Document::new_empty(); let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap(); - let mut p = printcraft_cos::Dict::new(); + let mut p = pdfcraft_cos::Dict::new(); p.set(b"Type".to_vec(), Object::name("Page")); p.set(b"Parent".to_vec(), Object::Ref(pages)); p.set(b"MediaBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 600.into(), 800.into()])); @@ -584,7 +584,7 @@ fn ordering_tabs_manually() { } // A comment between the widgets keeps its place. let page = page_refs(&doc)[0]; - let mut note = printcraft_cos::Dict::new(); + let mut note = pdfcraft_cos::Dict::new(); note.set(b"Type".to_vec(), Object::name("Annot")); note.set(b"Subtype".to_vec(), Object::name("Text")); note.set(b"Rect".to_vec(), Object::Array(vec![0.into(), 0.into(), 10.into(), 10.into()])); @@ -621,7 +621,7 @@ fn duplicating_a_field_across_pages_shares_its_value() { // A second and third page. let pages = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Pages").unwrap(); for _ in 0..2 { - let mut p = printcraft_cos::Dict::new(); + let mut p = pdfcraft_cos::Dict::new(); p.set(b"Type".to_vec(), Object::name("Page")); p.set(b"Parent".to_vec(), Object::Ref(pages)); p.set(b"MediaBox".to_vec(), Object::Array(vec![0.into(), 0.into(), 600.into(), 800.into()])); @@ -675,7 +675,7 @@ fn image_fields_ask_for_a_picture_and_show_it() { { d.set(k.to_vec(), v); } - let img = doc.add(Object::Stream(printcraft_cos::Stream::from_raw(d, vec![0; 24]))); + let img = doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(d, vec![0; 24]))); set_button_icon(&mut doc, &name, img, (4, 2)).unwrap(); let w = doc.get(f.widgets[0].obj).as_dict().cloned().unwrap(); let mk = w.get(b"MK").unwrap().as_dict().unwrap().clone(); @@ -742,12 +742,12 @@ fn detection_finds_blanks_rules_boxes_and_names_them() { #[test] fn page_shapes_reads_boxes_and_rules() { let mut doc = fixture(); - let page = printcraft_model::pages(&doc)[0].obj; - let s = doc.add(printcraft_cos::Object::Stream(printcraft_cos::Stream::flate( + let page = pdfcraft_model::pages(&doc)[0].obj; + let s = doc.add(pdfcraft_cos::Object::Stream(pdfcraft_cos::Stream::flate( Default::default(), b"q 2 0 0 2 0 0 cm 10 10 6 6 re S 20 50 m 120 50 l S 0 0 m 5 5 l S 30 300 100 0.5 re f Q", ))); - doc.update_dict(page, |d| d.set(b"Contents".to_vec(), printcraft_cos::Object::Ref(s))).unwrap(); + doc.update_dict(page, |d| d.set(b"Contents".to_vec(), pdfcraft_cos::Object::Ref(s))).unwrap(); let sh = crate::detect::page_shapes(&doc, 0); assert_eq!(sh.boxes, [[20.0, 20.0, 32.0, 32.0]]); assert_eq!(sh.rules, [[40.0, 100.0, 240.0, 100.0], [60.0, 600.5, 260.0, 600.5]]); diff --git a/crates/geom/Cargo.toml b/crates/geom/Cargo.toml index 25ca71dd..cf79a46e 100644 --- a/crates/geom/Cargo.toml +++ b/crates/geom/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "printcraft-geom" -description = "Geometry primitives for PrintCraft (points, rects, page transforms)" +name = "pdfcraft-geom" +description = "Geometry primitives for PdfCraft (points, rects, page transforms)" version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs index eaa109c6..b04e47cb 100644 --- a/crates/geom/src/lib.rs +++ b/crates/geom/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-geom — L0 geometry primitives shared by every PrintCraft crate. +//! pdfcraft-geom — L0 geometry primitives shared by every PdfCraft crate. //! //! PDF user space is y-up with the origin at the bottom-left of the page; view space is y-down. //! `PageRect` is always in PDF user space (points, 1/72 inch). diff --git a/crates/js/Cargo.toml b/crates/js/Cargo.toml index b62d6647..ee09b8a8 100644 --- a/crates/js/Cargo.toml +++ b/crates/js/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-js" +name = "pdfcraft-js" description = "Acrobat JavaScript: a sandboxed interpreter (boa) with the form object model subset (event, getField, app, util, console)" version.workspace = true edition.workspace = true diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index 647f80f1..574b7888 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-js — Acrobat JavaScript for forms (L3). +//! pdfcraft-js — Acrobat JavaScript for forms (L3). //! //! Field scripts (format, keystroke, validate, calculate), button actions and document-level //! scripts run in [boa](https://boajs.dev), a JavaScript engine written in Rust, with the subset @@ -152,7 +152,7 @@ pub enum Request { /// `this.pageNum = n` (0-based). GoToPage(usize), LaunchUrl(String), - /// `this.submitForm(url)`: never sent on PrintCraft's own. + /// `this.submitForm(url)`: never sent on PdfCraft's own. Submit(String), /// `field.setFocus()`. Focus(String), @@ -1080,7 +1080,7 @@ pub fn run(script: &str, event: &Event, doc: &DocInfo, fields: &[FieldState], do { std::thread::scope(|scope| { let spawned = std::thread::Builder::new() - .name("printcraft-js".into()) + .name("pdfcraft-js".into()) .stack_size(SCRIPT_STACK) .spawn_scoped(scope, || run_here(script, event, doc, fields, doc_scripts, limits)); match spawned { diff --git a/crates/model/Cargo.toml b/crates/model/Cargo.toml index 271ddeb8..32942cac 100644 --- a/crates/model/Cargo.toml +++ b/crates/model/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-model" +name = "pdfcraft-model" description = "Typed views over the PDF object graph (page tree with inherited attributes, page geometry)" version.workspace = true edition.workspace = true @@ -9,4 +9,4 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } +pdfcraft-cos = { workspace = true } diff --git a/crates/model/README.md b/crates/model/README.md index c354a818..90d6ceba 100644 --- a/crates/model/README.md +++ b/crates/model/README.md @@ -1,6 +1,6 @@ -# printcraft-model +# pdfcraft-model -Layer L2: typed views over the `printcraft-cos` object graph. It starts small, with what several +Layer L2: typed views over the `pdfcraft-cos` object graph. It starts small, with what several L3/L4 crates need: - `pages(doc)`: leaf pages in order, each with its inherited attributes (`Resources`, diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 9d2c9feb..6e558dea 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -1,8 +1,8 @@ -//! printcraft-model — typed views over the PDF object graph (L2). See the README. +//! pdfcraft-model — typed views over the PDF object graph (L2). See the README. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_cos::{Dict, Document, ObjRef, Object}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; /// Attributes a page inherits from its ancestors (ISO 32000-2 §7.7.3.4). pub const INHERITABLE: [&[u8]; 4] = [b"Resources", b"MediaBox", b"CropBox", b"Rotate"]; diff --git a/crates/ocr/Cargo.toml b/crates/ocr/Cargo.toml index 436d3100..46bb3a1e 100644 --- a/crates/ocr/Cargo.toml +++ b/crates/ocr/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-ocr" +name = "pdfcraft-ocr" description = "Scan & OCR: recognise text in page images (ocrs) and write it as an invisible, searchable text layer" version.workspace = true edition.workspace = true @@ -11,7 +11,7 @@ repository.workspace = true workspace = true [dependencies] -printcraft-fonts = { workspace = true } +pdfcraft-fonts = { workspace = true } ocrs = { version = "0.13.1", default-features = false, features = ["rten"] } rten = { version = "0.26.0", default-features = false, features = ["rten_format"] } thiserror.workspace = true diff --git a/crates/ocr/src/lib.rs b/crates/ocr/src/lib.rs index 026dda97..f026f5bd 100644 --- a/crates/ocr/src/lib.rs +++ b/crates/ocr/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-ocr — Scan & OCR ▸ Recognize text (L4). +//! pdfcraft-ocr — Scan & OCR ▸ Recognize text (L4). //! //! Recognition runs the ocrs engine (MIT/Apache-2.0) with its pre-trained models (CC-BY-SA-4.0, //! fetched by `cargo xtask models`; see ATTRIBUTION.toml). The caller renders a page to pixels; @@ -12,7 +12,7 @@ use std::path::{Path, PathBuf}; -pub use printcraft_fonts::helvetica_width; +pub use pdfcraft_fonts::helvetica_width; /// The model files, as named in ATTRIBUTION.toml. pub const DETECTION_MODEL: &str = "text-detection.rten"; @@ -23,7 +23,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[("en", "English")]; #[derive(Debug, thiserror::Error)] pub enum OcrError { - #[error("the text recognition models are not installed (run `cargo xtask models`, or set PRINTCRAFT_MODELS)")] + #[error("the text recognition models are not installed (run `cargo xtask models`, or set PDFCRAFT_MODELS)")] NoModels, #[error("loading {0}: {1}")] Load(String, String), @@ -47,7 +47,7 @@ impl Models { (m.detection.is_file() && m.recognition.is_file()).then_some(m) } - /// Look for the models: `$PRINTCRAFT_MODELS`, then `models/` beside the executable (and + /// Look for the models: `$PDFCRAFT_MODELS`, then `models/` beside the executable (and /// `Resources/models` in a macOS bundle), then the source tree's `assets/models/`. pub fn find() -> Option { Self::search_dirs().iter().find_map(|d| Self::in_dir(d)) @@ -56,7 +56,7 @@ impl Models { /// The directories [`Models::find`] looks in, in order. pub fn search_dirs() -> Vec { let mut dirs = Vec::new(); - if let Some(d) = std::env::var_os("PRINTCRAFT_MODELS") { + if let Some(d) = std::env::var_os("PDFCRAFT_MODELS") { dirs.push(PathBuf::from(d)); } if let Some(exe) = std::env::current_exe().ok().and_then(|e| e.parent().map(Path::to_path_buf)) { @@ -196,7 +196,7 @@ pub fn text_layer(words: &[PlacedWord]) -> Vec { let nums: Vec = m.iter().map(|v| num(*v)).collect(); out.extend_from_slice(nums.join(" ").as_bytes()); out.extend_from_slice(b" Tm "); - out.extend(printcraft_fonts::literal(&printcraft_fonts::win_ansi(&w.text))); + out.extend(pdfcraft_fonts::literal(&pdfcraft_fonts::win_ansi(&w.text))); out.extend_from_slice(b" Tj\n"); } out.extend_from_slice(b"ET\nEMC\n"); diff --git a/crates/optimize/Cargo.toml b/crates/optimize/Cargo.toml index 82a12690..184ee969 100644 --- a/crates/optimize/Cargo.toml +++ b/crates/optimize/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-optimize" +name = "pdfcraft-optimize" description = "The PDF Optimizer: image downsampling and recompression, discarding objects, clean-up" version.workspace = true edition.workspace = true @@ -11,13 +11,13 @@ repository.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-content = { workspace = true } -printcraft-model = { workspace = true } -printcraft-annot = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-content = { workspace = true } +pdfcraft-model = { workspace = true } +pdfcraft-annot = { workspace = true } thiserror.workspace = true # JPEG decoding (zune-jpeg) and encoding, and bicubic resampling; already in the tree. image = { version = "0.25", default-features = false, features = ["jpeg"] } [dev-dependencies] -printcraft-render = { workspace = true } +pdfcraft-render = { workspace = true } diff --git a/crates/optimize/README.md b/crates/optimize/README.md index 5d984d90..6b1adb38 100644 --- a/crates/optimize/README.md +++ b/crates/optimize/README.md @@ -1,4 +1,4 @@ -# printcraft-optimize +# pdfcraft-optimize Layer L4: the PDF Optimizer (execution plan M11.1; Acrobat's Reduce File Size and Optimize PDF ▸ Advanced optimization). @@ -16,6 +16,6 @@ let report = optimize(&mut doc, &Settings::default())?; // Reduce File Size's - **Discard objects:** thumbnails, alternate images, document tags, print settings. - **Clean up:** Flate for streams with no filter. -The engine runs Remove Hidden Information (`printcraft-redact`) for the user-data categories, +The engine runs Remove Hidden Information (`pdfcraft-redact`) for the user-data categories, then this, then merges identical objects and writes a full, compressed save. JPEG decoding and encoding and resampling come from the `image` crate. diff --git a/crates/optimize/src/audit.rs b/crates/optimize/src/audit.rs index 07709773..351d883b 100644 --- a/crates/optimize/src/audit.rs +++ b/crates/optimize/src/audit.rs @@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet}; -use printcraft_cos::{Dict, Document, ObjRef, Object}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; /// The categories, in Acrobat's order. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -153,7 +153,7 @@ impl Audit<'_> { fn size(doc: &Document, num: u32) -> u64 { let obj = doc.get(ObjRef::new(num, doc.generation(num))); let mut out = Vec::new(); - printcraft_cos::serialize(&obj, &mut out); + pdfcraft_cos::serialize(&obj, &mut out); // "n g obj\n … \nendobj\n" and its cross-reference entry. out.len() as u64 + 20 + 20 } @@ -163,7 +163,7 @@ fn size(doc: &Document, num: u32) -> u64 { pub fn audit_space(doc: &Document, file_len: u64) -> Vec { let mut a = Audit { doc, owner: HashMap::new() }; let cat = doc.root().and_then(|r| doc.get(r).as_dict().cloned()).unwrap_or_default(); - let pages = printcraft_model::pages(doc); + let pages = pdfcraft_model::pages(doc); // Order matters: the most specific owners first. if let Some(s) = cat.get(b"StructTreeRoot") { // Structure elements point at pages and annotations; those stay out (claim skips pages, @@ -189,7 +189,7 @@ pub fn audit_space(doc: &Document, file_len: u64) -> Vec { } } // Resources before the content: a content stream's dictionary may point at what it - // draws (PrintCraft's /PCAdded records an added image), which stays an image or font. + // draws (PdfCraft's /PCAdded records an added image), which stays an image or font. let res = a.resolve_dict(p.dict.get(b"Resources")); a.resources(&res); if let Some(c) = p.dict.get(b"Contents") { diff --git a/crates/optimize/src/images.rs b/crates/optimize/src/images.rs index 1b224124..f023a854 100644 --- a/crates/optimize/src/images.rs +++ b/crates/optimize/src/images.rs @@ -2,8 +2,8 @@ use std::collections::{HashMap, HashSet}; -use printcraft_content::Matrix; -use printcraft_cos::{Dict, Document, ObjRef, Object, Stream}; +use pdfcraft_content::Matrix; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream}; use crate::{Compression, ImageSettings, OptimizeError, Report, Settings}; @@ -49,7 +49,7 @@ fn walk(doc: &Document, data: &[u8], resources: &Dict, ctm: Matrix, depth: usize } let xobjects = resources.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default(); let mut stack = vec![ctm]; - for op in printcraft_content::parse(data).ops { + for op in pdfcraft_content::parse(data).ops { let top = stack.last().copied().unwrap_or(ctm); match op.op.as_slice() { b"q" => stack.push(top), @@ -112,7 +112,7 @@ pub fn effective_resolutions(doc: &Document, pages: &[ObjRef]) -> HashMap Option { let cs = doc.resolve(cs); match &*cs { diff --git a/crates/optimize/src/lib.rs b/crates/optimize/src/lib.rs index 3b0607ca..89b90a0a 100644 --- a/crates/optimize/src/lib.rs +++ b/crates/optimize/src/lib.rs @@ -4,7 +4,7 @@ //! - **Images:** each image's effective resolution is measured where pages draw it (the //! smallest over its uses, through form XObjects); colour and grayscale images above a //! threshold are resampled (bicubic) to a target resolution and recompressed as JPEG or Flate. -//! A new image replaces the old one only if it is smaller. Images PrintCraft can't decode +//! A new image replaces the old one only if it is smaller. Images PdfCraft can't decode //! faithfully (CMYK and other colour spaces, masks, decode arrays, JPEG 2000, JBIG2, CCITT, //! more than 8 bits) are left alone. //! - **Discard objects:** page thumbnails, alternate images, document tags (structure tree), @@ -12,7 +12,7 @@ //! - **Clean up:** Flate-compress streams that have no filter. //! //! Metadata, attachments, comments, scripts, private data, hidden layers, bookmarks and form -//! fields are discarded by `printcraft-redact`'s Remove Hidden Information, which the engine +//! fields are discarded by `pdfcraft-redact`'s Remove Hidden Information, which the engine //! runs alongside. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -23,7 +23,7 @@ mod links; pub use audit::{SpaceCategory, SpaceUse, audit_space}; -use printcraft_cos::{Document, ObjRef, Object, Stream}; +use pdfcraft_cos::{Document, ObjRef, Object, Stream}; pub use images::effective_resolutions; @@ -32,7 +32,7 @@ pub enum OptimizeError { #[error("the document has no page tree")] NoPages, #[error(transparent)] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } /// How resampled (or recompressed) images are stored. @@ -119,7 +119,7 @@ pub struct Report { /// Optimize `doc` in place. pub fn optimize(doc: &mut Document, settings: &Settings) -> Result { let mut report = Report::default(); - let pages = printcraft_annot::page_refs(doc).map_err(|_| OptimizeError::NoPages)?; + let pages = pdfcraft_annot::page_refs(doc).map_err(|_| OptimizeError::NoPages)?; images::run(doc, &pages, settings, &mut report)?; if settings.discard_thumbnails { for p in &pages { @@ -168,7 +168,7 @@ pub fn optimize(doc: &mut Document, settings: &Settings) -> Result { let mut n = 0; diff --git a/crates/optimize/src/links.rs b/crates/optimize/src/links.rs index b294cfe2..744881df 100644 --- a/crates/optimize/src/links.rs +++ b/crates/optimize/src/links.rs @@ -3,7 +3,7 @@ use std::collections::{HashSet, VecDeque}; -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; /// Named destinations: the `/Names /Dests` tree (entries) and the old `/Dests` dictionary. fn named_dests(doc: &Document) -> HashSet> { @@ -87,7 +87,7 @@ fn valid(o: &Object, pages: &HashSet, count: usize, names: &HashSet Result<(usize, usize), printcraft_cos::CosError> { +pub(crate) fn remove_invalid(doc: &mut Document, pages: &[ObjRef]) -> Result<(usize, usize), pdfcraft_cos::CosError> { let set: HashSet = pages.iter().copied().collect(); let names = named_dests(doc); let mut links = 0; @@ -176,7 +176,7 @@ fn referenced_names(doc: &Document) -> HashSet> { } /// Remove named destinations nothing refers to. Returns how many went. -pub(crate) fn remove_unreferenced_dests(doc: &mut Document) -> Result { +pub(crate) fn remove_unreferenced_dests(doc: &mut Document) -> Result { let Some(root) = doc.root() else { return Ok(0) }; let cat = doc.get(root).as_dict().cloned().unwrap_or_default(); let used = referenced_names(doc); diff --git a/crates/optimize/tests/optimize.rs b/crates/optimize/tests/optimize.rs index 3fcb593e..1b9b969a 100644 --- a/crates/optimize/tests/optimize.rs +++ b/crates/optimize/tests/optimize.rs @@ -2,8 +2,8 @@ use std::sync::Arc; -use printcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full}; -use printcraft_optimize::{Compression, ImageSettings, Settings, SpaceCategory, audit_space, effective_resolutions, optimize}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full}; +use pdfcraft_optimize::{Compression, ImageSettings, Settings, SpaceCategory, audit_space, effective_resolutions, optimize}; fn image(doc: &mut Document, w: u32, h: u32, n: usize, jpeg: bool, smask: Option) -> ObjRef { // A smooth gradient with a little texture (photo-like). @@ -94,7 +94,7 @@ fn images_are_measured_where_drawn_and_downsampled() { &[("A", masked), ("B", photo), ("C", twice)], "q 72 0 0 72 36 36 cm /A Do Q q 1 0 0 1 100 100 cm 600 0 0 400 0 0 cm /B Do Q q 72 0 0 72 0 0 cm /C Do Q q 288 0 0 288 300 300 cm /C Do Q", ); - let pages = printcraft_annot::page_refs(&doc).unwrap(); + let pages = pdfcraft_annot::page_refs(&doc).unwrap(); let ppi = effective_resolutions(&doc, &pages); assert_eq!(ppi[&big].round(), 600.0); assert_eq!(ppi[&small].round(), 50.0); @@ -122,9 +122,9 @@ fn images_are_measured_where_drawn_and_downsampled() { // The optimized file opens, and the page still shows the picture where it was. let reopened = Document::open(Arc::new(after_bytes.clone())).unwrap(); - assert_eq!(printcraft_annot::page_refs(&reopened).unwrap().len(), 2); - let mut r = printcraft_render::PageRenderer::new(Arc::new(after_bytes), printcraft_render::RenderConfig::default()); - let out = r.render(printcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() }); + assert_eq!(pdfcraft_annot::page_refs(&reopened).unwrap().len(), 2); + let mut r = pdfcraft_render::PageRenderer::new(Arc::new(after_bytes), pdfcraft_render::RenderConfig::default()); + let out = r.render(pdfcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() }); assert!(out.error.is_none(), "{:?}", out.error); // Top-left image area (36..180 x 600..744 user → y from the top 48..192): drawn, not white. let i = ((100 * out.width + 100) * 4) as usize; @@ -224,7 +224,7 @@ fn invalid_links_and_unreferenced_destinations_go() { d.set(b"Dest".to_vec(), dest); Object::Ref(doc.add(Object::Dict(d))) }; - let s = |t: &str| Object::String(printcraft_cos::PdfString::literal(t.as_bytes().to_vec())); + let s = |t: &str| Object::String(pdfcraft_cos::PdfString::literal(t.as_bytes().to_vec())); let annots = vec![ link(&mut doc, Object::Array(vec![Object::Ref(p), Object::name("Fit")])), link(&mut doc, Object::Array(vec![Object::Ref(ObjRef::new(999, 0)), Object::name("Fit")])), diff --git a/crates/organize/Cargo.toml b/crates/organize/Cargo.toml index 46fb417e..c047afc3 100644 --- a/crates/organize/Cargo.toml +++ b/crates/organize/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-organize" +name = "pdfcraft-organize" description = "Page and document-structure edits: rotate, delete, move, insert pages; document info" version.workspace = true edition.workspace = true @@ -9,7 +9,7 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } +pdfcraft-cos = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/organize/src/boxes.rs b/crates/organize/src/boxes.rs index 3e199605..c9ccb0f1 100644 --- a/crates/organize/src/boxes.rs +++ b/crates/organize/src/boxes.rs @@ -4,7 +4,7 @@ //! → crop) and are clipped to the media box. Margins are measured inward from the media box in //! default user space (unrotated), as Acrobat's dialog does. -use printcraft_cos::{Document, Object}; +use pdfcraft_cos::{Document, Object}; use crate::{OrganizeError, check, walk}; diff --git a/crates/organize/src/dedupe.rs b/crates/organize/src/dedupe.rs index 96a32716..ba1f39eb 100644 --- a/crates/organize/src/dedupe.rs +++ b/crates/organize/src/dedupe.rs @@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; -use printcraft_cos::{Document, ObjRef, Object}; +use pdfcraft_cos::{Document, ObjRef, Object}; const RESOURCE_TYPES: &[&[u8]] = &[b"Font", b"FontDescriptor", b"ExtGState", b"Pattern", b"Encoding", b"CMap", b"Halftone"]; const COLOR_SPACE_FAMILIES: &[&[u8]] = &[b"ICCBased", b"Indexed", b"Separation", b"DeviceN", b"CalRGB", b"CalGray", b"Lab", b"Pattern"]; @@ -240,7 +240,7 @@ pub fn dedupe_resources(doc: &mut Document, candidates: &[ObjRef], index_existin #[cfg(test)] mod tests { use super::*; - use printcraft_cos::{Dict, Stream}; + use pdfcraft_cos::{Dict, Stream}; use std::sync::Arc; fn stream(data: &[u8]) -> Object { diff --git a/crates/organize/src/import.rs b/crates/organize/src/import.rs index b5fe841d..ddd4fa23 100644 --- a/crates/organize/src/import.rs +++ b/crates/organize/src/import.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; -use printcraft_cos::{Dict, Document, ObjRef, Object}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; use crate::{INHERITABLE, OrganizeError, pages_root, rebuild, walk}; @@ -534,7 +534,7 @@ fn set_attachments(dst: &mut Document, mut entries: Vec<(Vec, Object)>) -> R entries.sort_by(|a, b| a.0.cmp(&b.0)); let mut names = Vec::new(); for (k, v) in entries { - names.push(Object::String(printcraft_cos::PdfString::literal(k))); + names.push(Object::String(pdfcraft_cos::PdfString::literal(k))); names.push(v); } let mut tree = Dict::new(); @@ -565,7 +565,7 @@ fn add_outline(doc: &mut Document, marks: &[Mark<'_>]) -> Result<(), OrganizeErr let (Some(&first_item), Some(&last_item)) = (items.first(), items.last()) else { return Ok(()) }; for (i, ((title, page, src, page_map), r)) in marks.iter().zip(&items).enumerate() { let mut d = Dict::new(); - d.set(b"Title".to_vec(), Object::String(printcraft_cos::PdfString::text(title))); + d.set(b"Title".to_vec(), Object::String(pdfcraft_cos::PdfString::text(title))); d.set(b"Parent".to_vec(), Object::Ref(outlines)); d.set(b"Dest".to_vec(), Object::Array(vec![Object::Ref(*page), Object::name("Fit")])); if i > 0 { @@ -668,7 +668,7 @@ fn copy_outline_level( /// and its `/Matrix` undoes the page rotation, so it draws upright as displayed. Returns the /// form and its displayed size in points. pub fn page_as_form(dst: &mut Document, src: &Document, page: usize) -> Result<(ObjRef, (f64, f64)), OrganizeError> { - use printcraft_cos::Stream; + use pdfcraft_cos::Stream; let all = walk(src)?; // `walk` gives the inheritable attributes (resources, boxes, rotation); the content is // the page's own. diff --git a/crates/organize/src/labels.rs b/crates/organize/src/labels.rs index 8f406c0b..e25a408c 100644 --- a/crates/organize/src/labels.rs +++ b/crates/organize/src/labels.rs @@ -5,7 +5,7 @@ //! `from..=to`, and keep every later page showing the label it had before. The tree is written //! back as one flat `/Nums` array, with ranges that merely continue the previous one merged. -use printcraft_cos::{Dict, Document, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, Object, PdfString}; use crate::{OrganizeError, page_count, pages_root}; diff --git a/crates/organize/src/lib.rs b/crates/organize/src/lib.rs index 6d34d797..f6786509 100644 --- a/crates/organize/src/lib.rs +++ b/crates/organize/src/lib.rs @@ -1,6 +1,6 @@ -//! printcraft-organize — page and document-structure edits (L4). +//! pdfcraft-organize — page and document-structure edits (L4). //! -//! Every operation mutates a `printcraft_cos::Document` (copy-on-write), so callers snapshot the +//! Every operation mutates a `pdfcraft_cos::Document` (copy-on-write), so callers snapshot the //! document before an edit for undo. Operations never drop data they do not understand. //! //! Page-tree strategy: before restructuring, inheritable page attributes (`Resources`, @@ -11,7 +11,7 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; mod boxes; mod dedupe; @@ -42,7 +42,7 @@ pub enum OrganizeError { #[error("{0}")] Invalid(String), #[error("{0}")] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } const INHERITABLE: [&[u8]; 4] = [b"Resources", b"MediaBox", b"CropBox", b"Rotate"]; diff --git a/crates/organize/src/outline.rs b/crates/organize/src/outline.rs index 50a4d2d4..ac1ffff3 100644 --- a/crates/organize/src/outline.rs +++ b/crates/organize/src/outline.rs @@ -11,7 +11,7 @@ use std::collections::HashSet; -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; use crate::{OrganizeError, walk}; @@ -39,7 +39,7 @@ pub enum OutlineError { #[error("{0}")] Organize(#[from] OrganizeError), #[error("{0}")] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } type Result = std::result::Result; diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index 5d105a77..f8c30ca4 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use printcraft_cos::{Document, Object, SaveOptions, write_full, write_incremental}; +use pdfcraft_cos::{Document, Object, SaveOptions, write_full, write_incremental}; use super::*; @@ -181,11 +181,11 @@ fn inserts_blank_page() { fn document_info_edits_round_trip_with_unicode() { let mut doc = open(fixture()); set_info(&mut doc, "Title", "Résumé — 履歴書").unwrap(); - set_info(&mut doc, "Author", "PrintCraft").unwrap(); + set_info(&mut doc, "Author", "PdfCraft").unwrap(); set_info(&mut doc, "Producer", "").unwrap(); // clearing removes the key let doc = save_and_reopen(&doc); assert_eq!(info(&doc, "Title").as_deref(), Some("Résumé — 履歴書")); - assert_eq!(info(&doc, "Author").as_deref(), Some("PrintCraft")); + assert_eq!(info(&doc, "Author").as_deref(), Some("PdfCraft")); assert_eq!(info(&doc, "Producer"), None); } @@ -705,9 +705,9 @@ fn initial_view_round_trips_and_keeps_scripts() { // A script opening action survives a change that needs no destination. let mut doc = doc; let root = doc.root().unwrap(); - let mut js = printcraft_cos::Dict::new(); + let mut js = pdfcraft_cos::Dict::new(); js.set(b"S".to_vec(), Object::name("JavaScript")); - js.set(b"JS".to_vec(), printcraft_cos::PdfString::text("app.alert('hi')")); + js.set(b"JS".to_vec(), pdfcraft_cos::PdfString::text("app.alert('hi')")); doc.update_dict(root, |c| c.set(b"OpenAction".to_vec(), Object::Dict(js))).unwrap(); crate::set_initial_view(&mut doc, &crate::InitialView { layout: Layout::SinglePage, ..Default::default() }).unwrap(); assert!(doc.get(root).as_dict().unwrap().get(b"OpenAction").and_then(|o| o.as_dict()).is_some_and(|d| d.name(b"S") == Some(b"JavaScript"))); diff --git a/crates/organize/src/view.rs b/crates/organize/src/view.rs index 0f951698..efca2ce4 100644 --- a/crates/organize/src/view.rs +++ b/crates/organize/src/view.rs @@ -2,7 +2,7 @@ //! panel, page layout, magnification, page; window and interface options) and its language and //! binding (ISO 32000-2 §7.7.2 Table 29, §12.2 viewer preferences). -use printcraft_cos::{Dict, Document, Object, PdfString}; +use pdfcraft_cos::{Dict, Document, Object, PdfString}; use crate::OrganizeError; @@ -65,7 +65,7 @@ pub struct InitialView { pub right_to_left: bool, } -fn catalog(doc: &Document) -> Result<(printcraft_cos::ObjRef, Dict), OrganizeError> { +fn catalog(doc: &Document) -> Result<(pdfcraft_cos::ObjRef, Dict), OrganizeError> { let r = doc.root().ok_or(OrganizeError::NoPageTree)?; Ok((r, doc.get(r).as_dict().cloned().ok_or(OrganizeError::NoPageTree)?)) } diff --git a/crates/preflight/Cargo.toml b/crates/preflight/Cargo.toml index 26adc2dd..b0eef005 100644 --- a/crates/preflight/Cargo.toml +++ b/crates/preflight/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-preflight" +name = "pdfcraft-preflight" description = "Standards: verify PDF/A (ISO 19005) conformance and convert documents to PDF/A-2b or 3b" version.workspace = true edition.workspace = true @@ -11,5 +11,5 @@ repository.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-model = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-model = { workspace = true } diff --git a/crates/preflight/src/icc.rs b/crates/preflight/src/icc.rs index dcd3bf68..935dd105 100644 --- a/crates/preflight/src/icc.rs +++ b/crates/preflight/src/icc.rs @@ -48,7 +48,7 @@ fn curve() -> Vec { pub fn srgb() -> Vec { // sRGB primaries, Bradford-adapted to the D50 profile connection space. let tags: Vec<(&[u8; 4], Vec)> = vec![ - (b"desc", desc("sRGB (PrintCraft)")), + (b"desc", desc("sRGB (PdfCraft)")), (b"cprt", text("No copyright, use freely")), (b"wtpt", xyz(0.9642, 1.0, 0.8249)), (b"rXYZ", xyz(0.4361, 0.2225, 0.0139)), diff --git a/crates/preflight/src/lib.rs b/crates/preflight/src/lib.rs index beb78dc3..eb6bedd5 100644 --- a/crates/preflight/src/lib.rs +++ b/crates/preflight/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-preflight — Standards: PDF/A (L4). +//! pdfcraft-preflight — Standards: PDF/A (L4). //! //! [`verify`] checks a document against the parts of ISO 19005-2/-3 (PDF/A-2b, PDF/A-3b) that can //! be decided from the object graph: @@ -24,7 +24,7 @@ pub mod icc; mod xmp; -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; /// The PDF/A part and conformance level. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -226,7 +226,7 @@ pub fn verify(doc: &Document, level: Level) -> Vec { issue("6.4.1", "The form asks viewers to make appearances (NeedAppearances)".into(), None, true); } // Pages: annotations and page actions. - let pages = printcraft_model::pages(doc); + let pages = pdfcraft_model::pages(doc); let page_of = |r: ObjRef| { pages.iter().position(|p| { p.dict @@ -398,13 +398,13 @@ fn strip_actions(doc: &Document, d: &mut Dict) -> bool { } /// Make `doc` conform to `level` as far as possible, then re-check it. -pub fn convert(doc: &mut Document, level: Level) -> Result { +pub fn convert(doc: &mut Document, level: Level) -> Result { let mut fixed = Vec::new(); if doc.trailer().get(b"Encrypt").is_some() { doc.remove_encryption(); fixed.push("Removed encryption".into()); } - let root = doc.root().ok_or(printcraft_cos::CosError::Syntax { offset: 0, detail: "no catalog".into() })?; + let root = doc.root().ok_or(pdfcraft_cos::CosError::Syntax { offset: 0, detail: "no catalog".into() })?; // Catalog: actions, JavaScript, NeedAppearances, output intent, metadata. let mut cat = catalog(doc); if cat.remove(b"AA").is_some() { @@ -462,7 +462,7 @@ pub fn convert(doc: &mut Document, level: Level) -> Result Result = annots.into_iter().filter(|a| a.as_ref() != Some(r)).collect(); diff --git a/crates/preflight/src/tests.rs b/crates/preflight/src/tests.rs index aee23f43..6613e6fc 100644 --- a/crates/preflight/src/tests.rs +++ b/crates/preflight/src/tests.rs @@ -77,7 +77,7 @@ fn convert_fixes_all_but_fonts() { assert_eq!(xmp::value(&x, "dc:title").as_deref(), Some("Report")); assert_eq!(xmp::value(&x, "xmp:CreateDate").as_deref(), Some("2024-01-05T09:30:00Z")); // It survives a save. - let bytes = printcraft_cos::write_full(&doc, &Default::default()).unwrap(); + let bytes = pdfcraft_cos::write_full(&doc, &Default::default()).unwrap(); let back = Document::open(std::sync::Arc::new(bytes)).unwrap(); assert_eq!(verify(&back, Level::A2b).len(), 1); } diff --git a/crates/preflight/src/xmp.rs b/crates/preflight/src/xmp.rs index f4dc189b..3057213d 100644 --- a/crates/preflight/src/xmp.rs +++ b/crates/preflight/src/xmp.rs @@ -1,7 +1,7 @@ //! XMP metadata for PDF/A: the identification schema and the document information, kept //! consistent (ISO 19005-2 §6.6). -use printcraft_cos::Document; +use pdfcraft_cos::Document; /// The value of a simple XMP property, written either as an attribute (`pdfaid:part="2"`) or /// as an element (`2`, or inside an `rdf:Alt`/`rdf:Seq`). diff --git a/crates/print/Cargo.toml b/crates/print/Cargo.toml index aca8388e..83b98de7 100644 --- a/crates/print/Cargo.toml +++ b/crates/print/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-print" +name = "pdfcraft-print" description = "Printing: page selection, sizing (fit, actual, shrink, custom), multiple pages per sheet, booklets, posters, comments & forms; print-ready PDF and the system spooler" version.workspace = true edition.workspace = true @@ -9,7 +9,7 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-content = { workspace = true } -printcraft-model = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-content = { workspace = true } +pdfcraft-model = { workspace = true } thiserror = { workspace = true } diff --git a/crates/print/README.md b/crates/print/README.md index abadfbc0..074e05a5 100644 --- a/crates/print/README.md +++ b/crates/print/README.md @@ -1,4 +1,4 @@ -# printcraft-print +# pdfcraft-print Layer L4: printing (execution plan M10.5). diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs index a0ef7989..31edd874 100644 --- a/crates/print/src/lib.rs +++ b/crates/print/src/lib.rs @@ -17,8 +17,8 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_content::Matrix; -use printcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full}; +use pdfcraft_content::Matrix; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full}; pub mod range; pub mod spool; @@ -36,7 +36,7 @@ pub enum PrintError { #[error("{0}")] Spool(String), #[error(transparent)] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] @@ -449,7 +449,7 @@ fn appearance_matrix(doc: &Document, form: &Dict, rect: [f64; 4]) -> Option Result { - let page = printcraft_model::pages(doc).swap_remove(index); + let page = pdfcraft_model::pages(doc).swap_remove(index); let crop = page.crop(doc); let mut res = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default(); let mut body = if content == Content::FormFieldsOnly { Vec::new() } else { decoded_contents(doc, &page.dict) }; @@ -500,7 +500,7 @@ fn page_form(doc: &mut Document, index: usize, content: Content) -> Result Result, PrintError> { - let pages = printcraft_model::pages(src); + let pages = pdfcraft_model::pages(src); let sizes: Vec<(f64, f64)> = pages.iter().map(|p| p.display_size(src)).collect(); let sheets = layout(&sizes, settings)?; let mut doc = src.clone(); diff --git a/crates/print/src/spool.rs b/crates/print/src/spool.rs index 8ca21758..c744579d 100644 --- a/crates/print/src/spool.rs +++ b/crates/print/src/spool.rs @@ -31,7 +31,7 @@ pub struct Job { impl Default for Job { fn default() -> Self { - Job { printer: None, copies: 1, collate: true, duplex: Duplex::Off, grayscale: false, title: "PrintCraft".into() } + Job { printer: None, copies: 1, collate: true, duplex: Duplex::Off, grayscale: false, title: "PdfCraft".into() } } } @@ -89,7 +89,7 @@ pub fn printers() -> Vec { pub fn submit(pdf: &[u8], job: &Job) -> Result { #[cfg(all(unix, not(target_arch = "wasm32")))] { - let dir = std::env::temp_dir().join(format!("printcraft-print-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!("pdfcraft-print-{}", std::process::id())); std::fs::create_dir_all(&dir).map_err(|e| PrintError::Spool(e.to_string()))?; let file = dir.join(format!("job-{}.pdf", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos()))); std::fs::write(&file, pdf).map_err(|e| PrintError::Spool(e.to_string()))?; diff --git a/crates/print/src/tests.rs b/crates/print/src/tests.rs index f2a09d6d..9ef4ab7d 100644 --- a/crates/print/src/tests.rs +++ b/crates/print/src/tests.rs @@ -143,7 +143,7 @@ fn imposed_pdf_has_the_sheets_and_honours_comments_and_forms() { let doc = fixture(3); let out = impose(&doc, &settings(vec![0, 1, 2], Layout::multiple(2))).unwrap(); let printed = Document::open(Arc::new(out.clone())).unwrap(); - let pages = printcraft_model::pages(&printed); + let pages = pdfcraft_model::pages(&printed); assert_eq!(pages.len(), 2); assert_eq!(pages[0].crop(&printed), [0.0, 0.0, 792.0, 612.0]); // The source pages are form XObjects holding their content. diff --git a/crates/redact/Cargo.toml b/crates/redact/Cargo.toml index 64b0d2ba..4aee20bf 100644 --- a/crates/redact/Cargo.toml +++ b/crates/redact/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-redact" +name = "pdfcraft-redact" description = "Redaction (architecture §11.2): marks, apply (glyphs, images, vectors, XObjects, annotations, fields), verification" version.workspace = true edition.workspace = true @@ -9,13 +9,13 @@ license.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-content = { workspace = true } -printcraft-fonts = { workspace = true } -printcraft-model = { workspace = true } -printcraft-forms = { workspace = true } -printcraft-edit = { workspace = true } -printcraft-annot = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-content = { workspace = true } +pdfcraft-fonts = { workspace = true } +pdfcraft-model = { workspace = true } +pdfcraft-forms = { workspace = true } +pdfcraft-edit = { workspace = true } +pdfcraft-annot = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/redact/README.md b/crates/redact/README.md index e32cbb24..61bf80b8 100644 --- a/crates/redact/README.md +++ b/crates/redact/README.md @@ -1,9 +1,9 @@ -# printcraft-redact +# pdfcraft-redact Layer L4 (may use `edit`): redaction, architecture §11.2. ```rust -// Marks are Redact annotations (printcraft-annot: Shape::Redact { quads, overlay }). +// Marks are Redact annotations (pdfcraft-annot: Shape::Redact { quads, overlay }). let marks: Vec = marks(&doc); // page, areas (user space), fill, overlay text let report = apply(&mut doc, None)?; // everything, or Some(&[pages]) clear_marks(&mut doc, None)?; // remove marks without applying @@ -18,7 +18,7 @@ clear_marks(&mut doc, None)?; // remove marks without app more than a fifth of its size (or a point) both ways; zero-size text goes when its origin is inside; - **images**: fully covered → removed; partly covered → a copy with the covered pixels cleared - (8/16-bit and 1/2/4-bit images, image masks; Flate/LZW/RL/A85/AHx). Codecs PrintCraft can't + (8/16-bit and 1/2/4-bit images, image masks; Flate/LZW/RL/A85/AHx). Codecs PdfCraft can't re-encode (DCT, JPX, JBIG2, CCITT) are removed whole (fail-closed); inline images under an area are removed; - **vectors**: covered paths are removed; partly covered paths and shadings are clipped so diff --git a/crates/redact/src/image.rs b/crates/redact/src/image.rs index f76dce93..4d61fd6c 100644 --- a/crates/redact/src/image.rs +++ b/crates/redact/src/image.rs @@ -1,11 +1,11 @@ //! Clearing the pixels of an image under redaction regions. The image is decoded, every pixel //! whose centre maps into a region is set to zero (no ink for image masks), and the result is //! written as a new Flate image; the original object is untouched (other pages may use it). -//! Images whose codec PrintCraft can't decode (DCT, JPX, JBIG2, CCITT) return `None`, and the +//! Images whose codec PdfCraft can't decode (DCT, JPX, JBIG2, CCITT) return `None`, and the //! caller removes the whole image instead; that is fail-closed. -use printcraft_content::{Matrix, overlaps}; -use printcraft_cos::{Document, Object, Stream}; +use pdfcraft_content::{Matrix, overlaps}; +use pdfcraft_cos::{Document, Object, Stream}; /// Most pixels an image may have to be cleared in place (larger ones are removed). const MAX_PIXELS: u64 = 64 << 20; diff --git a/crates/redact/src/interp.rs b/crates/redact/src/interp.rs index 2feb7679..e0c64a3d 100644 --- a/crates/redact/src/interp.rs +++ b/crates/redact/src/interp.rs @@ -20,11 +20,11 @@ use std::collections::HashMap; use std::rc::Rc; -use printcraft_content::{Matrix, Op, contains, num, overlaps, parse, serialize_ops, string}; -use printcraft_cos::{Dict, Document, ObjRef, Object, Stream}; +use pdfcraft_content::{Matrix, Op, contains, num, overlaps, parse, serialize_ops, string}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream}; use crate::{Report, image}; -use printcraft_fonts::pdf::Metrics; +use pdfcraft_fonts::pdf::Metrics; const MAX_DEPTH: usize = 12; diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index 57189ade..fa29fbc1 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -1,6 +1,6 @@ //! Redaction (architecture §11.2, execution plan M8.5–M8.7). Layer L4. //! -//! Content is marked for redaction with Redact annotations (§12.5.6.23); `printcraft-annot` +//! Content is marked for redaction with Redact annotations (§12.5.6.23); `pdfcraft-annot` //! creates them. [`apply`] then removes everything under the marks, for good: //! - text glyphs (the rest of each line keeps its position), inline images, and paths that the //! marks cover; images and vectors partly under a mark lose the covered part; @@ -14,7 +14,7 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -use printcraft_cos::{Dict, Document, ObjRef, Object, Stream}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream}; mod image; mod interp; @@ -37,11 +37,11 @@ pub enum RedactError { #[error("redaction could not be verified: {0} item(s) were still found under the marks, so nothing was changed")] Residue(usize), #[error(transparent)] - Edit(#[from] printcraft_edit::EditError), + Edit(#[from] pdfcraft_edit::EditError), #[error(transparent)] - Form(#[from] printcraft_forms::FormError), + Form(#[from] pdfcraft_forms::FormError), #[error(transparent)] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } /// A redaction mark on a page. @@ -56,13 +56,13 @@ pub struct Mark { pub fill: Option, pub overlay: String, /// How the overlay text is drawn (`/DA`, `/Q`, `/Repeat`). - pub look: printcraft_annot::OverlayLook, + pub look: pdfcraft_annot::OverlayLook, } /// The overlay look of a Redact annotation: font, size and colour from `/DA`, alignment from /// `/Q`, repetition from `/Repeat`. -fn overlay_look(doc: &Document, d: &Dict) -> printcraft_annot::OverlayLook { - let mut look = printcraft_annot::OverlayLook::default(); +fn overlay_look(doc: &Document, d: &Dict) -> pdfcraft_annot::OverlayLook { + let mut look = pdfcraft_annot::OverlayLook::default(); if let Some(da) = d.get(b"DA").and_then(|o| doc.resolve(o).as_string().map(|s| String::from_utf8_lossy(&s.bytes).into_owned())) { let t: Vec<&str> = da.split_whitespace().collect(); for (i, w) in t.iter().enumerate() { @@ -78,7 +78,7 @@ fn overlay_look(doc: &Document, d: &Dict) -> printcraft_annot::OverlayLook { } } "Tf" if i >= 2 => { - look.font = printcraft_annot::OverlayFont::from_resource(t[i - 2].trim_start_matches('/')); + look.font = pdfcraft_annot::OverlayFont::from_resource(t[i - 2].trim_start_matches('/')); look.size = t[i - 1].parse::().unwrap_or(0.0).max(0.0); } _ => {} @@ -130,7 +130,7 @@ pub(crate) fn annots_of(doc: &Document, page: &Dict) -> Vec { /// Every redaction mark in the document, page by page. pub fn marks(doc: &Document) -> Vec { let mut out = Vec::new(); - for (pi, p) in printcraft_model::pages(doc).iter().enumerate() { + for (pi, p) in pdfcraft_model::pages(doc).iter().enumerate() { for a in annots_of(doc, &p.dict) { let Some(r) = a.as_ref() else { continue }; let obj = doc.get(r); @@ -214,10 +214,10 @@ fn overlay_content(marks: &[&Mark]) -> Vec { // set; repeated to fill the area when asked. let look = &m.look; let (res, width): (&str, fn(&str, f64) -> f64) = match look.font { - printcraft_annot::OverlayFont::Helvetica => ("PCHelv", printcraft_fonts::helvetica_width), + pdfcraft_annot::OverlayFont::Helvetica => ("PCHelv", pdfcraft_fonts::helvetica_width), // Approximations of the standard metrics (no font program is bundled). - printcraft_annot::OverlayFont::Times => ("PCTimes", |s, size| printcraft_fonts::helvetica_width(s, size) * 0.9), - printcraft_annot::OverlayFont::Courier => ("PCCour", |s, size| s.chars().count() as f64 * size * 0.6), + pdfcraft_annot::OverlayFont::Times => ("PCTimes", |s, size| pdfcraft_fonts::helvetica_width(s, size) * 0.9), + pdfcraft_annot::OverlayFont::Courier => ("PCCour", |s, size| s.chars().count() as f64 * size * 0.6), }; let [cr, cg, cb] = look.color.map(|v| v.clamp(0.0, 1.0)); for r in &m.rects { @@ -252,7 +252,7 @@ fn overlay_content(marks: &[&Mark]) -> Vec { _ => r[0] + (w - lw) / 2.0, }; c.extend(format!("1 0 0 1 {} {} Tm ", n(x), n(y)).bytes()); - c.extend_from_slice(&printcraft_fonts::literal(&printcraft_fonts::win_ansi(line))); + c.extend_from_slice(&pdfcraft_fonts::literal(&pdfcraft_fonts::win_ansi(line))); c.extend_from_slice(b" Tj "); y -= size * 1.2; } @@ -277,10 +277,10 @@ pub fn apply(doc: &mut Document, pages: Option<&[usize]>) -> Result = Vec::new(); let widget_owner: Vec<(ObjRef, String)> = - printcraft_forms::fields(doc).into_iter().flat_map(|f| f.widgets.iter().map(|w| (w.obj, f.name.clone())).collect::>()).collect(); + pdfcraft_forms::fields(doc).into_iter().flat_map(|f| f.widgets.iter().map(|w| (w.obj, f.name.clone())).collect::>()).collect(); for &pi in &by_page { - let page = printcraft_model::pages(doc).swap_remove(pi); + let page = pdfcraft_model::pages(doc).swap_remove(pi); let page_marks: Vec<&Mark> = chosen.iter().copied().filter(|m| m.page == pi).collect(); let rects: Vec<[f64; 4]> = page_marks.iter().flat_map(|m| m.rects.iter().copied()).collect(); @@ -289,7 +289,7 @@ pub fn apply(doc: &mut Document, pages: Option<&[usize]>) -> Result) -> Result) -> Result) -> Result = chosen.iter().copied().filter(|m| m.page == pi).collect(); let content = overlay_content(&page_marks); if !content.is_empty() { - printcraft_edit::stamp(doc, pi, "Redaction", content)?; + pdfcraft_edit::stamp(doc, pi, "Redaction", content)?; } } // 5. Verify: nothing readable may remain under a region. let mut residue = 0; for &pi in &by_page { - let page = printcraft_model::pages(doc).swap_remove(pi); + let page = pdfcraft_model::pages(doc).swap_remove(pi); let rects: Vec<[f64; 4]> = chosen.iter().filter(|m| m.page == pi).flat_map(|m| m.rects.iter().copied()).collect(); let (list, data) = page_streams(doc, &page.dict, pi)?; // The overlay stream (last) draws text of its own: the overlay label is allowed. @@ -412,7 +412,7 @@ pub fn apply(doc: &mut Document, pages: Option<&[usize]>) -> Result 0 { return Err(RedactError::Residue(residue)); @@ -429,7 +429,7 @@ pub fn clear_marks(doc: &mut Document, pages: Option<&[usize]>) -> Result = annots .iter() diff --git a/crates/redact/src/sanitize.rs b/crates/redact/src/sanitize.rs index 0b61df40..11756bba 100644 --- a/crates/redact/src/sanitize.rs +++ b/crates/redact/src/sanitize.rs @@ -4,13 +4,13 @@ //! categories; [`sanitize`] removes all of them. Every removal makes the next save a full //! rewrite, so the earlier revision (which still holds the data) leaves the file. -use printcraft_content::Matrix; -use printcraft_cos::{Dict, Document, ObjRef, Object}; +use pdfcraft_content::Matrix; +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; use crate::interp::{Mode, Scope, process}; use crate::{RedactError, Report, annots_of, page_streams}; -/// The categories of Acrobat's Remove Hidden Information panel that PrintCraft handles. +/// The categories of Acrobat's Remove Hidden Information panel that PdfCraft handles. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Hidden { /// Document information (`/Info`) and XMP metadata streams. @@ -142,9 +142,9 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re return Ok(0); } let mut total = 0; - let n = printcraft_model::pages(doc).len(); + let n = pdfcraft_model::pages(doc).len(); for pi in 0..n { - let page = printcraft_model::pages(doc).swap_remove(pi); + let page = pdfcraft_model::pages(doc).swap_remove(pi); let Ok((list, data)) = page_streams(doc, &page.dict, pi) else { if write { return Err(RedactError::Unreadable(pi + 1)); @@ -175,7 +175,7 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re _ => Dict::new(), }; dict.remove(b"Length"); - new_list[i] = Object::Ref(doc.add(Object::Stream(printcraft_cos::Stream::flate(dict, &bytes)))); + new_list[i] = Object::Ref(doc.add(Object::Stream(pdfcraft_cos::Stream::flate(dict, &bytes)))); changed = true; } if changed || !out.xobjects.is_empty() { @@ -199,7 +199,7 @@ fn content_pass(doc: &mut Document, text: bool, layers: bool, write: bool) -> Re /// How many items each category would remove (categories with nothing are included as 0). pub fn scan(doc: &Document) -> Vec<(Hidden, usize)> { let cat = catalog(doc).map(|c| c.1).unwrap_or_default(); - let pages = printcraft_model::pages(doc); + let pages = pdfcraft_model::pages(doc); let annots: Vec = pages.iter().flat_map(|p| annots_of(doc, &p.dict)).filter_map(|a| doc.resolve(&a).as_dict().cloned()).collect(); let count_sub = |s: &[u8]| annots.iter().filter(|a| a.name(b"Subtype") == Some(s)).count(); let names = sub(doc, &cat, b"Names").unwrap_or_default(); @@ -215,7 +215,7 @@ pub fn scan(doc: &Document) -> Vec<(Hidden, usize)> { } Hidden::Attachments => sub(doc, &names, b"EmbeddedFiles").map_or(0, |t| name_tree_len(doc, &t, 0)) + count_sub(b"FileAttachment"), Hidden::Comments => annots.iter().filter(|a| is_comment(a.name(b"Subtype").unwrap_or(b""))).count(), - Hidden::FormFields => printcraft_forms::fields(doc).len(), + Hidden::FormFields => pdfcraft_forms::fields(doc).len(), Hidden::HiddenText => content_pass(&mut doc2, true, false, false).unwrap_or(0), Hidden::HiddenLayers => { let off = hidden_layers(doc).len(); @@ -250,13 +250,13 @@ pub fn remove_hidden(doc: &mut Document, which: &[Hidden]) -> Result 0 { // Fields without appearances get one first, so their values stay visible. - for f in printcraft_forms::fields(doc) { + for f in pdfcraft_forms::fields(doc) { if f.widgets.iter().any(|w| !doc.get(w.obj).as_dict().is_some_and(|d| d.contains(b"AP"))) { - printcraft_forms::redraw_field(doc, &f.name)?; + pdfcraft_forms::redraw_field(doc, &f.name)?; } } - let n = printcraft_model::pages(doc).len(); - printcraft_edit::flatten(doc, &(0..n).collect::>(), false, true)?; + let n = pdfcraft_model::pages(doc).len(); + pdfcraft_edit::flatten(doc, &(0..n).collect::>(), false, true)?; doc.update_dict(root, |d| { d.remove(b"AcroForm"); })?; @@ -268,7 +268,7 @@ pub fn remove_hidden(doc: &mut Document, which: &[Hidden]) -> Result = Vec::new(); for a in &list { diff --git a/crates/redact/src/tags.rs b/crates/redact/src/tags.rs index 5df296a4..d233b266 100644 --- a/crates/redact/src/tags.rs +++ b/crates/redact/src/tags.rs @@ -7,13 +7,13 @@ use std::collections::{HashMap, HashSet}; -use printcraft_cos::{Dict, Document, ObjRef, Object}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; /// Each MCID's operators (serialized), and whether anything is still drawn inside it. fn by_mcid(data: &[u8]) -> HashMap, bool)> { let mut out: HashMap, bool)> = HashMap::new(); let mut stack: Vec> = Vec::new(); - for op in printcraft_content::parse(data).ops { + for op in pdfcraft_content::parse(data).ops { match op.op.as_slice() { b"BDC" => { let id = match op.operands.get(1) { @@ -45,7 +45,7 @@ fn by_mcid(data: &[u8]) -> HashMap, bool)> { _ => false, }; let mut bytes = Vec::new(); - printcraft_content::write_op(&op, &mut bytes); + pdfcraft_content::write_op(&op, &mut bytes); for id in stack.iter().flatten() { let e = out.entry(*id).or_default(); e.0.extend_from_slice(&bytes); @@ -91,7 +91,7 @@ fn mcid_of(o: &Object, page: Option, target: ObjRef) -> Option { } /// Clean the elements owning `changed` marked content on `page`; returns how many changed. -pub(crate) fn clean(doc: &mut Document, page: ObjRef, changed: &HashSet, empty: &HashSet) -> Result { +pub(crate) fn clean(doc: &mut Document, page: ObjRef, changed: &HashSet, empty: &HashSet) -> Result { if changed.is_empty() { return Ok(0); } diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 0ca0b794..1f4175fa 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use printcraft_annot::{Meta, NewAnnotation, Shape, Style, add_annotation, rect_quad}; -use printcraft_cos::{Document, SaveOptions, write_full, write_incremental}; +use pdfcraft_annot::{Meta, NewAnnotation, Shape, Style, add_annotation, rect_quad}; +use pdfcraft_cos::{Document, SaveOptions, write_full, write_incremental}; use super::*; @@ -60,19 +60,19 @@ fn mark(doc: &mut Document, page: usize, rects: &[[f64; 4]], overlay: &str) { /// The decoded content streams of a page, joined. fn content(doc: &Document, page: usize) -> String { - let p = &printcraft_model::pages(doc)[page]; + let p = &pdfcraft_model::pages(doc)[page]; let (_, data) = page_streams(doc, &p.dict, page).unwrap(); data.iter().map(|d| String::from_utf8_lossy(d).into_owned()).collect::>().join("\n") } /// Glyphs (and inline images) of page `page` under `rects` (the verifier's count). fn under(doc: &mut Document, page: usize, rects: &[[f64; 4]]) -> usize { - let p = printcraft_model::pages(doc).swap_remove(page); + let p = pdfcraft_model::pages(doc).swap_remove(page); let (_, data) = page_streams(doc, &p.dict, page).unwrap(); let res = p.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default(); let mut rep = Report::default(); let mut scope = Scope::new(rects, Mode::Verify, &mut rep); - process(doc, &mut scope, &data, &res, printcraft_content::Matrix::IDENTITY).residue + process(doc, &mut scope, &data, &res, pdfcraft_content::Matrix::IDENTITY).residue } fn reopen(doc: &Document) -> Document { @@ -146,11 +146,11 @@ fn rotated_text_and_composite_fonts() { let mut doc = one_page(src, "", vec![]); let font_ref = doc.add(Object::Dict(Dict::new())); let parsed = { - let mut lx = printcraft_cos::Lexer::new(&t0, 0); + let mut lx = pdfcraft_cos::Lexer::new(&t0, 0); lx.object().unwrap() }; doc.set(font_ref, parsed); - let page = printcraft_model::pages(&doc)[0].obj; + let page = pdfcraft_model::pages(&doc)[0].obj; doc.update_dict(page, |d| { let mut res = d.get(b"Resources").and_then(Object::as_dict).cloned().unwrap(); let mut fonts = res.get(b"Font").and_then(Object::as_dict).cloned().unwrap(); @@ -178,7 +178,7 @@ fn images_are_removed_or_have_their_pixels_cleared() { assert_eq!((r.images_removed, r.images_cleared), (1, 1), "{r:?}"); let c = content(&doc, 0); assert!(!c.contains("/Im1 Do") && !c.contains("/Im2 Do"), "{c}"); - let p = printcraft_model::pages(&doc).swap_remove(0); + let p = pdfcraft_model::pages(&doc).swap_remove(0); let res = doc.resolve(p.dict.get(b"Resources").unwrap()).as_dict().cloned().unwrap(); let xo = doc.resolve(res.get(b"XObject").unwrap()).as_dict().cloned().unwrap(); let new = xo.iter().find(|(k, _)| k.starts_with(b"PCRedacted")).map(|(_, v)| v.clone()).expect("a cleared copy"); @@ -244,12 +244,12 @@ fn comments_links_and_fields_under_a_mark_go() { &Meta::default(), ) .unwrap(); - printcraft_forms::add_field(&mut doc, 0, [10.0, 50.0, 100.0, 70.0], &printcraft_forms::NewField::Text { multiline: false }, Some("ssn")).unwrap(); + pdfcraft_forms::add_field(&mut doc, 0, [10.0, 50.0, 100.0, 70.0], &pdfcraft_forms::NewField::Text { multiline: false }, Some("ssn")).unwrap(); mark(&mut doc, 0, &[[0.0, 0.0, 120.0, 80.0]], "REDACTED"); let r = apply(&mut doc, None).unwrap(); assert_eq!((r.annotations, r.fields), (1, 1), "{r:?}"); - assert!(printcraft_forms::fields(&doc).is_empty()); - let p = printcraft_model::pages(&doc).swap_remove(0); + assert!(pdfcraft_forms::fields(&doc).is_empty()); + let p = pdfcraft_model::pages(&doc).swap_remove(0); assert_eq!(annots_of(&doc, &p.dict).len(), 1, "only the far rectangle stays"); let c = content(&doc, 0); assert!(c.contains("(REDACTED) Tj"), "overlay text: {c}"); @@ -269,7 +269,7 @@ fn nothing_to_apply_and_clearing_marks() { #[test] fn unreadable_content_fails_closed() { let mut doc = one_page(b"", "", vec![]); - let page = printcraft_model::pages(&doc)[0].obj; + let page = pdfcraft_model::pages(&doc)[0].obj; let bad = doc.add(Object::Stream(Stream::from_raw( { let mut d = Dict::new(); @@ -314,8 +314,8 @@ fn hidden_fixture() -> Document { let mut doc = pdf(objs); let info = doc.add(Object::Dict({ let mut d = Dict::new(); - d.set(b"Title".to_vec(), printcraft_cos::PdfString::text("Secret plan")); - d.set(b"Author".to_vec(), printcraft_cos::PdfString::text("Ada")); + d.set(b"Title".to_vec(), pdfcraft_cos::PdfString::text("Secret plan")); + d.set(b"Author".to_vec(), pdfcraft_cos::PdfString::text("Ada")); d })); doc.trailer_mut().set(b"Info".to_vec(), Object::Ref(info)); @@ -357,8 +357,8 @@ fn hidden_information_is_counted_and_removed() { for k in [&b"Outlines"[..], b"OpenAction", b"PieceInfo", b"AcroForm", b"Metadata"] { assert!(!cat.contains(k), "{}", String::from_utf8_lossy(k)); } - assert!(printcraft_forms::fields(&doc).is_empty()); - let p = printcraft_model::pages(&doc).swap_remove(0); + assert!(pdfcraft_forms::fields(&doc).is_empty()); + let p = pdfcraft_model::pages(&doc).swap_remove(0); assert!(annots_of(&doc, &p.dict).is_empty()); let shown = doc.object_numbers().into_iter().any(|n| match &*doc.get(ObjRef::new(n, doc.generation(n))) { Object::Stream(s) => s.decoded().is_ok_and(|d| d.windows(5).any(|w| w == b"(Ada)")), @@ -370,8 +370,7 @@ fn hidden_information_is_counted_and_removed() { #[test] fn overlay_text_takes_its_font_size_colour_alignment_and_repeats() { let mut doc = one_page(b"BT /F1 12 Tf 20 250 Td (Secret salary figures) Tj ET", "", Vec::new()); - let look = - printcraft_annot::OverlayLook { font: printcraft_annot::OverlayFont::Courier, size: 8.0, color: [0.0, 0.0, 1.0], align: 0, repeat: true }; + let look = pdfcraft_annot::OverlayLook { font: pdfcraft_annot::OverlayFont::Courier, size: 8.0, color: [0.0, 0.0, 1.0], align: 0, repeat: true }; let shape = Shape::Redact { quads: vec![rect_quad([10.0, 200.0, 290.0, 270.0])], overlay: "REDACTED".into(), look }; let style = Style::default_for(&shape); add_annotation(&mut doc, &NewAnnotation { page: 0, shape, style, contents: String::new(), author: "T".into() }, &Meta::default()).unwrap(); @@ -385,7 +384,7 @@ fn overlay_text_takes_its_font_size_colour_alignment_and_repeats() { // 70 pt high at 8 pt × 1.2 leading: several lines, each the word repeated. assert!(c.matches(" Tm (REDACTED REDACTED").count() >= 5, "{c}"); assert!(c.contains("1 0 0 1 11 "), "left aligned at the area's edge: {c}"); - let p = &printcraft_model::pages(&doc)[0]; + let p = &pdfcraft_model::pages(&doc)[0]; let fonts = p .dict .get(b"Resources") @@ -413,10 +412,10 @@ fn tags_lose_what_redaction_removed() { mark(&mut doc, 0, &[[5.0, 195.0, 60.0, 212.0]], ""); let report = apply(&mut doc, None).unwrap(); assert_eq!(report.tags, 1); - let secret = doc.get(printcraft_cos::ObjRef::new(9, 0)).as_dict().cloned().unwrap(); + let secret = doc.get(pdfcraft_cos::ObjRef::new(9, 0)).as_dict().cloned().unwrap(); assert!(secret.get(b"ActualText").is_none() && secret.get(b"Alt").is_none()); assert!(secret.get(b"K").is_none(), "its marked content is empty now"); - let public = doc.get(printcraft_cos::ObjRef::new(10, 0)).as_dict().cloned().unwrap(); + let public = doc.get(pdfcraft_cos::ObjRef::new(10, 0)).as_dict().cloned().unwrap(); assert_eq!(public.get(b"K").and_then(Object::as_int), Some(1)); assert!(public.get(b"ActualText").is_some(), "untouched content keeps its tags"); } diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 5e646481..49d10378 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "printcraft-render" -description = "Page rendering (bootstrap: hayro) and document inspection for PrintCraft" +name = "pdfcraft-render" +description = "Page rendering (bootstrap: hayro) and document inspection for PdfCraft" version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/render/src/inspect.rs b/crates/render/src/inspect.rs index c1f5b16d..e5c4f429 100644 --- a/crates/render/src/inspect.rs +++ b/crates/render/src/inspect.rs @@ -1,7 +1,7 @@ //! Document inspection: everything panels need that is not pixels. //! //! Page geometry comes from hayro (which resolves inheritance and rotation); the rest comes from -//! lopdf (bootstrap, replaced by `printcraft-model` in M2). Inspection is *tolerant*: when lopdf +//! lopdf (bootstrap, replaced by `pdfcraft-model` in M2). Inspection is *tolerant*: when lopdf //! cannot load a file that hayro can render, panels are simply empty and `warnings` says why. use std::collections::{BTreeMap, HashMap, HashSet}; diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 4dfc82c7..506dd89e 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1,4 +1,4 @@ -//! printcraft-render — page rasterization and document inspection. +//! pdfcraft-render — page rasterization and document inspection. //! //! **Bootstrap status (see ADR-0004):** rasterization goes straight through the `hayro` crate //! (hayro-interpret + vello_cpu), and inspection (outline, annotations, fields, layers, diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index 3088e786..47261f68 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -298,7 +298,7 @@ impl RenderPool { }; let (wtx, wrx) = channel::<()>(); let (shared, out, bytes, config) = (self.shared.clone(), self.results_tx.clone(), self.bytes.clone(), self.config.clone()); - match std::thread::Builder::new().name(format!("printcraft-render-{id}")).spawn(move || worker(id, bytes, config, shared, wrx, out)) { + match std::thread::Builder::new().name(format!("pdfcraft-render-{id}")).spawn(move || worker(id, bytes, config, shared, wrx, out)) { Ok(h) => { lock(&self.wake).push(wtx); lock(&self._workers).push(h); diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index eebf675d..624a956c 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -1,4 +1,4 @@ -//! Text extraction (bootstrap for `printcraft-text`, architecture §8). +//! Text extraction (bootstrap for `pdfcraft-text`, architecture §8). //! //! A hayro `Device` that ignores paint and records every glyph: its Unicode value (ToUnicode → //! encoding → glyph name fallbacks, handled by hayro) and its box in *page view space*, i.e. @@ -180,7 +180,7 @@ impl<'a> Device<'a> for TextDevice { fn push_transparency_group(&mut self, _: f32, _: Option>, _: BlendMode) {} fn draw_glyph(&mut self, glyph: &Glyph<'a>, transform: Affine, glyph_transform: Affine, _: &Paint<'a>, _: &GlyphDrawMode) { let Some(u) = glyph.as_unicode() else { - if std::env::var_os("PRINTCRAFT_TEXT_DEBUG").is_some() { + if std::env::var_os("PDFCRAFT_TEXT_DEBUG").is_some() { eprintln!("NOUNICODE {} {:?}", if matches!(glyph, Glyph::Type3(_)) { "t3" } else { "ol" }, (transform * glyph_transform).as_coeffs()); } return; @@ -195,7 +195,7 @@ impl<'a> Device<'a> for TextDevice { // Glyph space → view space. Glyph space uses 1000 units per em for outline glyphs; the em // box spans descender (−200) to ascender (800), the advance gives the width. let t = transform * glyph_transform; - if std::env::var_os("PRINTCRAFT_TEXT_DEBUG").is_some() { + if std::env::var_os("PDFCRAFT_TEXT_DEBUG").is_some() { let kind = if matches!(glyph, Glyph::Type3(_)) { "t3" } else { "ol" }; eprintln!("{kind} {text:?} transform={:?} glyph_transform={:?}", transform.as_coeffs(), glyph_transform.as_coeffs()); } diff --git a/crates/sign/Cargo.toml b/crates/sign/Cargo.toml index 259ce0c8..ddb23cdd 100644 --- a/crates/sign/Cargo.toml +++ b/crates/sign/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "printcraft-sign" +name = "pdfcraft-sign" description = "Digital signatures (ISO 32000-2 §12.8, PAdES): X.509, CMS, PKCS #12, signing and validation" version.workspace = true edition.workspace = true @@ -11,9 +11,9 @@ repository.workspace = true workspace = true [dependencies] -printcraft-cos = { workspace = true } -printcraft-annot = { workspace = true } -printcraft-fonts = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-annot = { workspace = true } +pdfcraft-fonts = { workspace = true } thiserror.workspace = true sha1 = "0.11" sha2 = "0.11" diff --git a/crates/sign/README.md b/crates/sign/README.md index e2115777..bd6c40b4 100644 --- a/crates/sign/README.md +++ b/crates/sign/README.md @@ -1,4 +1,4 @@ -# printcraft-sign +# pdfcraft-sign Layer L4: digital signatures (execution plan M9; ISO 32000-2 §12.8; PAdES, ETSI EN 319 142). diff --git a/crates/sign/src/keychain.rs b/crates/sign/src/keychain.rs index 49392203..4d52031b 100644 --- a/crates/sign/src/keychain.rs +++ b/crates/sign/src/keychain.rs @@ -1,6 +1,6 @@ //! Digital IDs from the macOS Keychain: identities (a certificate with its private key) that //! sign through the Security framework. The private key never leaves the Keychain; macOS may -//! ask the user to allow PrintCraft to use it. +//! ask the user to allow PdfCraft to use it. use std::path::Path; use std::sync::Arc; @@ -31,7 +31,7 @@ impl ExternalKey for KeychainKey { } } -/// The `keychain:` reference PrintCraft keeps for an identity. +/// The `keychain:` reference PdfCraft keeps for an identity. pub fn reference(c: &Certificate) -> String { let d = DigestAlg::Sha256.digest(&[&c.raw]); format!("keychain:{}", d.iter().map(|b| format!("{b:02x}")).collect::()) @@ -49,7 +49,7 @@ pub fn find(reference_or_name: &str) -> Result { } /// The signing identities in the user's keychains (or only in the keychain file `keychain`). -/// Identities with keys PrintCraft can't use (other curves, Ed25519) are left out. +/// Identities with keys PdfCraft can't use (other curves, Ed25519) are left out. pub fn identities(keychain: Option<&Path>) -> Result, SignError> { let mut search = ItemSearchOptions::new(); search.class(ItemClass::identity()).load_refs(true).limit(Limit::All); diff --git a/crates/sign/src/keys.rs b/crates/sign/src/keys.rs index 9b125743..5799195e 100644 --- a/crates/sign/src/keys.rs +++ b/crates/sign/src/keys.rs @@ -227,7 +227,7 @@ enum Inner { External(std::sync::Arc), } -/// A private key PrintCraft can't read, only ask to sign (OS key stores, tokens). +/// A private key PdfCraft can't read, only ask to sign (OS key stores, tokens). pub trait ExternalKey: Send + Sync { /// Sign `msg`, hashing it with `alg` (PKCS #1 v1.5 for RSA, DER-encoded ECDSA). fn sign(&self, alg: DigestAlg, msg: &[u8]) -> Result, SignError>; @@ -248,7 +248,7 @@ impl std::fmt::Debug for PrivateKey { } impl PrivateKey { - /// A key held outside PrintCraft's memory (its public half is `public`). It can't be + /// A key held outside PdfCraft's memory (its public half is `public`). It can't be /// exported to a PKCS #12 file. pub fn external(public: PublicKey, key: std::sync::Arc) -> PrivateKey { PrivateKey { inner: Inner::External(key), public, pkcs8: Vec::new() } diff --git a/crates/sign/src/lib.rs b/crates/sign/src/lib.rs index 9af82ba1..e46b2da3 100644 --- a/crates/sign/src/lib.rs +++ b/crates/sign/src/lib.rs @@ -35,5 +35,5 @@ pub enum SignError { #[error("{0}")] Pdf(String), #[error(transparent)] - Cos(#[from] printcraft_cos::CosError), + Cos(#[from] pdfcraft_cos::CosError), } diff --git a/crates/sign/src/pdf.rs b/crates/sign/src/pdf.rs index be20a445..ddfa3ea7 100644 --- a/crates/sign/src/pdf.rs +++ b/crates/sign/src/pdf.rs @@ -14,7 +14,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString, SaveOptions, Stream}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, SaveOptions, Stream}; use crate::SignError; use crate::cms::SignedData; @@ -235,7 +235,7 @@ fn sig_fields(doc: &Document) -> Vec { /// Which page holds each annotation. fn annot_pages(doc: &Document) -> HashMap { let mut map = HashMap::new(); - for (i, p) in printcraft_annot::page_refs(doc).unwrap_or_default().into_iter().enumerate() { + for (i, p) in pdfcraft_annot::page_refs(doc).unwrap_or_default().into_iter().enumerate() { if let Some(a) = doc.get(p).as_dict().and_then(|d| d.get(b"Annots").map(|a| doc.resolve(a))).and_then(|a| a.as_array().cloned()) { for r in a.iter().filter_map(Object::as_ref) { map.insert(r, i); @@ -301,7 +301,7 @@ pub fn list_cached(doc: &Document, bytes: &[u8], trust: &TrustStore, cache: &Dig } fn page_index(doc: &Document, p: ObjRef) -> Option { - printcraft_annot::page_refs(doc).ok()?.iter().position(|r| *r == p) + pdfcraft_annot::page_refs(doc).ok()?.iter().position(|r| *r == p) } /// Validate one signature dictionary. @@ -355,7 +355,7 @@ fn validate_into(doc: &Document, bytes: &[u8], trust: &TrustStore, v: &Dict, inf // The signed revision's number: its cross-reference sections (1 for a reconstructed file). info.revision = cache.revision(bytes, covered).map_or(1, |d| d.revisions().len().max(1)); if info.sub_filter.as_deref() == Some("adbe.x509.rsa_sha1") { - info.details.push("This signature uses the legacy adbe.x509.rsa_sha1 format, which PrintCraft does not validate yet.".into()); + info.details.push("This signature uses the legacy adbe.x509.rsa_sha1 format, which PdfCraft does not validate yet.".into()); return; } let sd = match SignedData::parse(&contents) { @@ -452,7 +452,7 @@ fn classify_changes(doc: &Document, old: Option, p: Option) -> Mod let Some(old) = old else { return Modification::Disallowed(vec!["the signed version could not be read".into()]); }; - let old_content: HashSet = printcraft_annot::page_refs(&old) + let old_content: HashSet = pdfcraft_annot::page_refs(&old) .unwrap_or_default() .into_iter() .flat_map(|pg| { @@ -470,7 +470,7 @@ fn classify_changes(doc: &Document, old: Option, p: Option) -> Mod let (mut allowed, mut disallowed): (Vec<&str>, Vec<&str>) = (Vec::new(), Vec::new()); // Stored at the same place in both (and not edited since): the same bytes, unchanged. let same_place = |num: u32| -> bool { - use printcraft_cos::XrefEntry; + use pdfcraft_cos::XrefEntry; if doc.is_edited(num) { return false; } @@ -706,7 +706,7 @@ pub fn sign(doc: &Document, id: &DigitalId, opts: &SignOptions) -> Result Result Result String { /// The visible signature: the signer's name large on the left, the details on the right /// (Acrobat's standard layout), in Helvetica. fn appearance(rect: [f64; 4], name: &str, cert: &Certificate, opts: &SignOptions) -> Stream { - use printcraft_fonts::{helvetica_width, literal, win_ansi, wrap}; + use pdfcraft_fonts::{helvetica_width, literal, win_ansi, wrap}; let (w, h) = ((rect[2] - rect[0]).max(0.0), (rect[3] - rect[1]).max(0.0)); let a = &opts.appearance; let mut lines: Vec = Vec::new(); diff --git a/crates/sign/tests/crypto.rs b/crates/sign/tests/crypto.rs index 25eb80b7..df60eac1 100644 --- a/crates/sign/tests/crypto.rs +++ b/crates/sign/tests/crypto.rs @@ -1,8 +1,8 @@ //! The cryptographic core against files made by OpenSSL 3 (tests/data/README.md). -use printcraft_sign::der::Time; -use printcraft_sign::keys::DigestAlg; -use printcraft_sign::{Certificate, Name, PublicKey, SignError, cms, pkcs12}; +use pdfcraft_sign::der::Time; +use pdfcraft_sign::keys::DigestAlg; +use pdfcraft_sign::{Certificate, Name, PublicKey, SignError, cms, pkcs12}; fn data(name: &str) -> Vec { std::fs::read(format!("{}/tests/data/{name}", env!("CARGO_MANIFEST_DIR"))).unwrap() @@ -61,6 +61,7 @@ fn opens_every_openssl_flavour_of_pkcs12() { #[test] fn certificates_parse_as_openssl_made_them() { let c = Certificate::parse(&pem("rsa.crt.pem")).unwrap(); + // The fixtures were made when the app was called PrintCraft. assert_eq!(c.subject.display(), "CN=Test Signer RSA, O=PrintCraft Tests, C=US"); assert_eq!(c.serial_hex(), "03E9"); assert!(c.is_self_signed()); @@ -97,7 +98,7 @@ fn signatures_round_trip_for_every_key_type() { fn new_digital_ids_are_self_signed_and_survive_a_p12_round_trip() { let name = Name::build("Grace Hopper", "Compilers", "Navy", "grace@example.com", "us"); let now = Time { year: 2026, month: 10, day: 2, hour: 12, minute: 0, second: 0 }; - for key in [printcraft_sign::PrivateKey::generate_rsa(2048).unwrap(), printcraft_sign::PrivateKey::generate_p256().unwrap()] { + for key in [pdfcraft_sign::PrivateKey::generate_rsa(2048).unwrap(), pdfcraft_sign::PrivateKey::generate_p256().unwrap()] { let cert = Certificate::self_signed(&name, &key, now, 5, &[0x42, 0x01]).unwrap(); assert!(cert.is_self_signed()); assert_eq!(cert.subject.display(), "C=US, O=Navy, OU=Compilers, CN=Grace Hopper, E=grace@example.com"); @@ -119,11 +120,11 @@ fn new_digital_ids_are_self_signed_and_survive_a_p12_round_trip() { #[test] fn certificates_load_from_pem_and_der_and_export_as_pem() { let pem_text = data("rsa.crt.pem"); - let certs = printcraft_sign::x509::load_certificates(&pem_text).unwrap(); + let certs = pdfcraft_sign::x509::load_certificates(&pem_text).unwrap(); assert_eq!(certs.len(), 1); let der = certs[0].raw.clone(); - assert_eq!(printcraft_sign::x509::load_certificates(&der).unwrap()[0], certs[0]); - let back = printcraft_sign::x509::to_pem(&certs[0]); - assert_eq!(printcraft_sign::x509::load_certificates(back.as_bytes()).unwrap()[0], certs[0]); - assert!(printcraft_sign::x509::load_certificates(b"hello").is_err()); + assert_eq!(pdfcraft_sign::x509::load_certificates(&der).unwrap()[0], certs[0]); + let back = pdfcraft_sign::x509::to_pem(&certs[0]); + assert_eq!(pdfcraft_sign::x509::load_certificates(back.as_bytes()).unwrap()[0], certs[0]); + assert!(pdfcraft_sign::x509::load_certificates(b"hello").is_err()); } diff --git a/crates/sign/tests/data/README.md b/crates/sign/tests/data/README.md index a77b448d..4b9c98d0 100644 --- a/crates/sign/tests/data/README.md +++ b/crates/sign/tests/data/README.md @@ -34,4 +34,4 @@ openssl pkcs12 -export -inkey leaf.key -in leaf.crt -certfile ca.crt -out chain. `openssl-signed.pdf` is a hand-written one-page PDF with a `/Contents` placeholder, signed with `openssl cms -sign -binary -md sha256 -outform DER -signer rsa.crt -inkey rsa.key` over its byte ranges (`adbe.pkcs7.detached`, with OpenSSL's signing-time attribute). It checks the -validator against a signature PrintCraft did not make; poppler's `pdfsig` reports it valid. +validator against a signature PdfCraft did not make; poppler's `pdfsig` reports it valid. diff --git a/crates/sign/tests/pdf.rs b/crates/sign/tests/pdf.rs index 4b0eafc8..f3fbeb4b 100644 --- a/crates/sign/tests/pdf.rs +++ b/crates/sign/tests/pdf.rs @@ -2,8 +2,8 @@ use std::sync::Arc; -use printcraft_cos::{Document, Object, PdfString, SaveOptions, write_incremental}; -use printcraft_sign::{Modification, SignOptions, Status, TrustStore, pkcs12, signatures}; +use pdfcraft_cos::{Document, Object, PdfString, SaveOptions, write_incremental}; +use pdfcraft_sign::{Modification, SignOptions, Status, TrustStore, pkcs12, signatures}; fn data(name: &str) -> Vec { std::fs::read(format!("{}/tests/data/{name}", env!("CARGO_MANIFEST_DIR"))).unwrap() @@ -55,7 +55,7 @@ fn opts() -> SignOptions { fn signing_then_validating_with_and_without_trust() { for file in ["rsa-aes.p12", "ec-p256.p12", "ec-p384.p12", "chain.p12"] { let id = pkcs12::open(&data(file), "test").unwrap(); - let signed = printcraft_sign::sign(&open(&fixture()), &id, &opts()).unwrap(); + let signed = pdfcraft_sign::sign(&open(&fixture()), &id, &opts()).unwrap(); assert!(signed.starts_with(&fixture()), "{file}: an incremental update"); let doc = open(&signed); let sigs = signatures(&doc, &signed, &TrustStore::default()); @@ -88,9 +88,9 @@ fn signing_then_validating_with_and_without_trust() { #[test] fn signing_an_existing_field_and_counter_signing() { let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap(); - let first = printcraft_sign::sign(&open(&fixture()), &id, &SignOptions { field: Some("Approval".into()), ..opts() }).unwrap(); + let first = pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { field: Some("Approval".into()), ..opts() }).unwrap(); let id2 = pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(); - let second = printcraft_sign::sign(&open(&first), &id2, &opts()).unwrap(); + let second = pdfcraft_sign::sign(&open(&first), &id2, &opts()).unwrap(); let doc = open(&second); let sigs = signatures(&doc, &second, &TrustStore::default()); let a = sigs.iter().find(|s| s.field == "Approval").unwrap(); @@ -102,8 +102,8 @@ fn signing_an_existing_field_and_counter_signing() { let b = sigs.iter().find(|s| s.field == "Signature1").unwrap(); assert_eq!((b.revision, b.modification.clone()), (3, Modification::None)); assert!(matches!( - printcraft_sign::sign(&doc, &id, &SignOptions { field: Some("Approval".into()), ..opts() }), - Err(printcraft_sign::SignError::Pdf(_)) + pdfcraft_sign::sign(&doc, &id, &SignOptions { field: Some("Approval".into()), ..opts() }), + Err(pdfcraft_sign::SignError::Pdf(_)) )); } @@ -115,13 +115,13 @@ fn edit_after(signed: &[u8], f: impl FnOnce(&mut Document)) -> Vec { } fn add_comment(doc: &mut Document) { - let mut d = printcraft_cos::Dict::new(); + let mut d = pdfcraft_cos::Dict::new(); d.set(b"Type".to_vec(), Object::name("Annot")); d.set(b"Subtype".to_vec(), Object::name("Text")); d.set(b"Rect".to_vec(), Object::Array(vec![Object::Int(10), Object::Int(10), Object::Int(30), Object::Int(30)])); d.set(b"Contents".to_vec(), PdfString::text("A note")); let r = doc.add(Object::Dict(d)); - let page = printcraft_cos::ObjRef { num: 3, generation: 0 }; + let page = pdfcraft_cos::ObjRef { num: 3, generation: 0 }; doc.update_dict(page, |p| { if let Some(Object::Array(a)) = p.get_mut(b"Annots") { a.push(Object::Ref(r)); @@ -131,10 +131,10 @@ fn add_comment(doc: &mut Document) { } fn change_text(doc: &mut Document) { - let mut d = printcraft_cos::Dict::new(); + let mut d = pdfcraft_cos::Dict::new(); d.set(b"Length".to_vec(), Object::Int(40)); - let s = printcraft_cos::Stream::from_raw(d, b"BT /F1 14 Tf 20 250 Td (Other text) Tj ET".to_vec()); - doc.set(printcraft_cos::ObjRef { num: 4, generation: 0 }, Object::Stream(s)); + let s = pdfcraft_cos::Stream::from_raw(d, b"BT /F1 14 Tf 20 250 Td (Other text) Tj ET".to_vec()); + doc.set(pdfcraft_cos::ObjRef { num: 4, generation: 0 }, Object::Stream(s)); } #[test] @@ -143,7 +143,7 @@ fn later_changes_are_classified_under_the_signature_permissions() { let trust = TrustStore { certs: vec![id.certificate.clone()] }; let check = |bytes: &[u8]| signatures(&open(bytes), bytes, &trust).into_iter().find(|s| s.signed).unwrap(); // Approval signature: comments are permitted, rewriting page content is not. - let signed = printcraft_sign::sign(&open(&fixture()), &id, &opts()).unwrap(); + let signed = pdfcraft_sign::sign(&open(&fixture()), &id, &opts()).unwrap(); let s = check(&edit_after(&signed, add_comment)); assert_eq!(s.status, Status::Valid, "{:?}", s.details); assert_eq!(s.modification, Modification::Allowed(vec!["comments".into()])); @@ -151,23 +151,23 @@ fn later_changes_are_classified_under_the_signature_permissions() { assert_eq!(s.status, Status::Invalid); assert_eq!(s.modification, Modification::Disallowed(vec!["page content".into()])); // Certified with "no changes allowed": even a comment invalidates it. - let certified = printcraft_sign::sign(&open(&fixture()), &id, &SignOptions { certify: Some(1), ..opts() }).unwrap(); + let certified = pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { certify: Some(1), ..opts() }).unwrap(); let s = check(&certified); assert_eq!((s.certify, s.status), (Some(1), Status::Valid)); assert_eq!(check(&edit_after(&certified, add_comment)).status, Status::Invalid); // Certified allowing comments: fine. - let certified3 = printcraft_sign::sign(&open(&fixture()), &id, &SignOptions { certify: Some(3), ..opts() }).unwrap(); + let certified3 = pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { certify: Some(3), ..opts() }).unwrap(); assert_eq!(check(&edit_after(&certified3, add_comment)).status, Status::Valid); } #[test] fn invisible_signatures_and_refusals() { let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap(); - let signed = printcraft_sign::sign(&open(&fixture()), &id, &SignOptions { rect: None, ..opts() }).unwrap(); + let signed = pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { rect: None, ..opts() }).unwrap(); let s = signatures(&open(&signed), &signed, &TrustStore::default()).into_iter().find(|s| s.signed).unwrap(); assert!(!s.visible); - assert!(printcraft_sign::sign(&open(&fixture()), &id, &SignOptions { page: 5, ..opts() }).is_err()); - assert_eq!(printcraft_sign::pdf::display_date("D:20261002120000+01'00'"), "2026.10.02 12:00:00 +01'00'"); + assert!(pdfcraft_sign::sign(&open(&fixture()), &id, &SignOptions { page: 5, ..opts() }).is_err()); + assert_eq!(pdfcraft_sign::pdf::display_date("D:20261002120000+01'00'"), "2026.10.02 12:00:00 +01'00'"); } #[test] @@ -180,7 +180,7 @@ fn validates_a_signature_made_by_openssl() { assert_eq!(s.signer.as_deref(), Some("Test Signer RSA")); assert!(s.signing_time.is_some_and(|t| t.year == 2026), "from the CMS signing-time attribute"); assert!(!s.visible); - let rsa = printcraft_sign::Certificate::parse(&pkcs12::open(&data("rsa-aes.p12"), "test").unwrap().certificate.raw).unwrap(); + let rsa = pdfcraft_sign::Certificate::parse(&pkcs12::open(&data("rsa-aes.p12"), "test").unwrap().certificate.raw).unwrap(); let s = signatures(&doc, &bytes, &TrustStore { certs: vec![rsa] }).into_iter().next().unwrap(); assert_eq!(s.status, Status::Valid, "{:?}", s.details); // A later comment is allowed for this approval signature. @@ -201,7 +201,7 @@ trailer << /Root 1 0 R >> .to_vec(); let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap(); for certify in [None, Some(2)] { - let signed = printcraft_sign::sign(&open(&bytes), &id, &SignOptions { certify, rect: None, ..opts() }).unwrap(); + let signed = pdfcraft_sign::sign(&open(&bytes), &id, &SignOptions { certify, rect: None, ..opts() }).unwrap(); let s = signatures(&open(&signed), &signed, &TrustStore::default()).into_iter().find(|s| s.signed).unwrap(); assert_eq!((s.status, s.certify, s.modification.clone()), (Status::Unknown, certify, Modification::None), "{:?}", s.details); } @@ -214,7 +214,7 @@ trailer << /Root 1 0 R >> #[ignore = "creates a temporary macOS keychain; run with --ignored"] fn signing_with_keychain_identities() { use std::process::Command; - let dir = std::env::temp_dir().join(format!("printcraft-keychain-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!("pdfcraft-keychain-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let kc = dir.join("test.keychain-db"); let sec = |args: &[&str]| Command::new("security").args(args).output().unwrap(); @@ -237,11 +237,11 @@ fn signing_with_keychain_identities() { let out = sec(&["import", &p, "-k", k, "-P", "test", "-A"]); assert!(out.status.success(), "{f}: {}", String::from_utf8_lossy(&out.stderr)); } - let ids = printcraft_sign::keychain::identities(Some(&kc)).unwrap(); + let ids = pdfcraft_sign::keychain::identities(Some(&kc)).unwrap(); assert_eq!(ids.len(), 2, "{ids:?}"); for id in &ids { assert!(id.key.is_external()); - let signed = printcraft_sign::sign(&open(&fixture()), id, &opts()).unwrap(); + let signed = pdfcraft_sign::sign(&open(&fixture()), id, &opts()).unwrap(); let trusted = signatures(&open(&signed), &signed, &TrustStore { certs: vec![id.certificate.clone()] }); let s = trusted.iter().find(|s| s.signed).unwrap(); assert_eq!(s.status, Status::Valid, "{:?}", s.details); diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index ee199abf..38c23962 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "printcraft-ui-egui" -description = "The egui shell of PrintCraft" +name = "pdfcraft-ui-egui" +description = "The egui shell of PdfCraft" version.workspace = true edition.workspace = true license.workspace = true @@ -9,9 +9,9 @@ license.workspace = true workspace = true [dependencies] -printcraft-fonts = { workspace = true } -printcraft-engine = { workspace = true } -printcraft-render = { workspace = true } +pdfcraft-fonts = { workspace = true } +pdfcraft-engine = { workspace = true } +pdfcraft-render = { workspace = true } egui = { workspace = true } egui_extras = { workspace = true } eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "persistence", "links"] } @@ -36,7 +36,7 @@ js-sys = "0.3" web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url"] } [dev-dependencies] -printcraft-cos = { workspace = true } -printcraft-model = { workspace = true } +pdfcraft-cos = { workspace = true } +pdfcraft-model = { workspace = true } egui_kittest = { workspace = true, features = ["eframe", "wgpu"] } image = { version = "0.25", default-features = false, features = ["png"] } diff --git a/crates/ui-egui/examples/shot.rs b/crates/ui-egui/examples/shot.rs index 93642d2b..01e48e03 100644 --- a/crates/ui-egui/examples/shot.rs +++ b/crates/ui-egui/examples/shot.rs @@ -1,16 +1,16 @@ -//! Headless screenshot of the real PrintCraft shell (egui_kittest + wgpu, no window needed). +//! Headless screenshot of the real PdfCraft shell (egui_kittest + wgpu, no window needed). //! //! ```text -//! cargo run -p printcraft-ui-egui --example shot -- out.png [file.pdf] [--size 1440x900] [--scale 2] [--page 3 --panel pages …] +//! cargo run -p pdfcraft-ui-egui --example shot -- out.png [file.pdf] [--size 1440x900] [--scale 2] [--page 3 --panel pages …] //! ``` //! `--width N` downscales the image to N pixels wide (README screenshots). Any other -//! `--key value` pair is passed to `PrintCraftApp::set_option` +//! `--key value` pair is passed to `PdfCraftApp::set_option` //! (the same verbs as the desktop app's command-line flags and the future control channel). use std::time::{Duration, Instant}; use egui_kittest::Harness; -use printcraft_ui_egui::PrintCraftApp; +use pdfcraft_ui_egui::PdfCraftApp; fn main() -> Result<(), String> { let mut args = std::env::args().skip(1); @@ -32,7 +32,7 @@ fn main() -> Result<(), String> { } } let mut harness = Harness::builder().with_size(size).with_pixels_per_point(scale).build_eframe(move |_cc| { - let mut app = PrintCraftApp::new(); + let mut app = PdfCraftApp::new(); if let Some(f) = &file { let bytes = std::fs::read(f).unwrap_or_default(); let name = std::path::Path::new(f).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); diff --git a/crates/ui-egui/src/a11y_ui.rs b/crates/ui-egui/src/a11y_ui.rs index 4d518193..90317cea 100644 --- a/crates/ui-egui/src/a11y_ui.rs +++ b/crates/ui-egui/src/a11y_ui.rs @@ -5,11 +5,11 @@ use std::collections::BTreeSet; use egui::{Align, Layout}; -use printcraft_engine::DocId; -use printcraft_engine::a11y::{Category, Options, Report, Rule, Status}; +use pdfcraft_engine::DocId; +use pdfcraft_engine::a11y::{Category, Options, Report, Rule, Status}; use crate::theme::{self, Tokens}; -use crate::{Dialog, PrintCraftApp, PropsTab, RightPanel, icons, widgets}; +use crate::{Dialog, PdfCraftApp, PropsTab, RightPanel, icons, widgets}; /// The options dialog's settings (kept for the session, like Acrobat's). #[derive(Clone, Debug, PartialEq)] @@ -67,7 +67,7 @@ pub(crate) fn fixable(rule: Rule) -> bool { matches!(rule, Rule::PrimaryLanguage | Rule::Title | Rule::TabOrder) } -pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (bool, bool) { +pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) { let pages = app.active_ids().and_then(|(_, id)| app.session.get(id)).map_or(1, |d| d.info.pages.len().max(1)); let o = &mut app.a11y_options; o.to = o.to.clamp(1, pages); @@ -283,7 +283,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &mut A11yState, doc: D action } -impl PrintCraftApp { +impl PdfCraftApp { /// Check for accessibility: the options first, unless they are turned off. pub(crate) fn start_accessibility_check(&mut self) { if self.a11y_options.show_dialog { @@ -336,7 +336,7 @@ impl PrintCraftApp { let Some((_, report)) = self.a11y.report.as_ref().filter(|(d, _)| *d == id) else { return }; let Some(doc) = self.session.get(id) else { return }; let (y, m, d) = self.session.today(); - let html = printcraft_engine::a11y::report_html(report, &doc.name, &format!("{y}-{m:02}-{d:02}")); + let html = pdfcraft_engine::a11y::report_html(report, &doc.name, &format!("{y}-{m:02}-{d:02}")); let stem = doc.name.trim_end_matches(".pdf").trim_end_matches(".PDF").to_string(); self.write_files(&[(format!("{stem} Accessibility Report.html"), std::sync::Arc::new(html.into_bytes()))], "Save the accessibility report"); } @@ -387,7 +387,7 @@ impl PrintCraftApp { #[derive(Clone, Default)] pub struct AltDraft { pub doc: Option, - pub figures: Vec, + pub figures: Vec, pub index: usize, pub texts: Vec, pub decorative: Vec, @@ -402,22 +402,22 @@ impl std::fmt::Debug for AltDraft { } /// Render a figure's area for the dialog (at most 360 × 220 px). -fn figure_picture(ctx: &egui::Context, doc: &printcraft_engine::Document, f: &printcraft_engine::a11y::Figure) -> Option { +fn figure_picture(ctx: &egui::Context, doc: &pdfcraft_engine::Document, f: &pdfcraft_engine::a11y::Figure) -> Option { let (page, b) = (f.page?, f.bbox?); let info = doc.info.pages.get(page)?; let (u, v) = (info.user_to_view(b[0] as f32, b[1] as f32), info.user_to_view(b[2] as f32, b[3] as f32)); let (x0, y0, x1, y1) = (u[0].min(v[0]), u[1].min(v[1]), u[0].max(v[0]), u[1].max(v[1])); let (w, h) = ((x1 - x0).max(1.0), (y1 - y0).max(1.0)); let scale = (360.0 / w).min(220.0 / h).clamp(0.05, 8.0); - let tile = printcraft_render::Tile { + let tile = pdfcraft_render::Tile { x: (x0 * scale).floor().max(0.0) as u32, y: (y0 * scale).floor().max(0.0) as u32, w: (w * scale).ceil().max(1.0) as u32, h: (h * scale).ceil().max(1.0) as u32, }; - let config = printcraft_render::RenderConfig { password: doc.password.as_deref().map(std::sync::Arc::from), ..Default::default() }; - let mut r = printcraft_render::PageRenderer::new(doc.bytes.clone(), config); - let out = r.render(printcraft_render::RenderRequest { page, kind: printcraft_render::RequestKind::Pixels, tile: Some(tile), scale, tag: 0 }); + let config = pdfcraft_render::RenderConfig { password: doc.password.as_deref().map(std::sync::Arc::from), ..Default::default() }; + let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config); + let out = r.render(pdfcraft_render::RenderRequest { page, kind: pdfcraft_render::RequestKind::Pixels, tile: Some(tile), scale, tag: 0 }); if out.error.is_some() || out.width == 0 { return None; } @@ -425,7 +425,7 @@ fn figure_picture(ctx: &egui::Context, doc: &printcraft_engine::Document, f: &pr Some(ctx.load_texture("alt-figure", img, egui::TextureOptions::LINEAR)) } -pub(crate) fn alt_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (bool, bool) { +pub(crate) fn alt_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) { let ctx = ui.ctx().clone(); let doc = app.alt_draft.doc.and_then(|id| app.session.get(id)); let d = &mut app.alt_draft; @@ -489,7 +489,7 @@ pub(crate) fn alt_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) - (save, cancel) } -impl PrintCraftApp { +impl PdfCraftApp { /// Prepare for accessibility ▸ Add alternate text. pub(crate) fn start_alt_text(&mut self) { let Some((_, id)) = self.active_ids() else { return }; @@ -512,13 +512,13 @@ impl PrintCraftApp { let mut edits = Vec::new(); for (k, f) in d.figures.iter().enumerate() { if d.decorative[k] { - edits.push(printcraft_engine::Edit::MarkDecorative { figure: f.obj.num }); + edits.push(pdfcraft_engine::Edit::MarkDecorative { figure: f.obj.num }); } else if d.texts[k].trim() != f.alt.as_deref().unwrap_or("").trim() { - edits.push(printcraft_engine::Edit::SetAltText { figure: f.obj.num, alt: Some(d.texts[k].clone()) }); + edits.push(pdfcraft_engine::Edit::SetAltText { figure: f.obj.num, alt: Some(d.texts[k].clone()) }); } } if !edits.is_empty() { - self.apply_edit(printcraft_engine::Edit::Batch { label: "Set alternate text".into(), edits }); + self.apply_edit(pdfcraft_engine::Edit::Batch { label: "Set alternate text".into(), edits }); } } } diff --git a/crates/ui-egui/src/actions_ui.rs b/crates/ui-egui/src/actions_ui.rs index 04058551..7e78a240 100644 --- a/crates/ui-egui/src/actions_ui.rs +++ b/crates/ui-egui/src/actions_ui.rs @@ -1,13 +1,13 @@ -//! Action Wizard: the actions (PrintCraft's and your own), running one over files in the +//! Action Wizard: the actions (PdfCraft's and your own), running one over files in the //! background, and creating or editing an action's steps. Your actions are remembered. use std::sync::{Arc, Mutex}; use egui::{Align, Layout}; -use printcraft_engine::actions::{Action, Step, builtin}; +use pdfcraft_engine::actions::{Action, Step, builtin}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; /// The Action Wizard's state. #[derive(Clone, Debug, Default, PartialEq)] @@ -28,7 +28,7 @@ pub struct RunProgress { pub message: Option, } -impl PrintCraftApp { +impl PdfCraftApp { /// Built-in actions, then the user's. pub fn all_actions(&self) -> Vec { let mut v = builtin(); @@ -58,7 +58,7 @@ impl PrintCraftApp { if let Ok(mut s) = p.lock() { s.done = i; } - let r = printcraft_engine::actions::run_on(&action, &name, Arc::new(bytes), |_, _| {}).and_then(|r| { + let r = pdfcraft_engine::actions::run_on(&action, &name, Arc::new(bytes), |_, _| {}).and_then(|r| { #[cfg(not(target_arch = "wasm32"))] crate::editing::write_atomically(&dir.join(&name).to_string_lossy(), &r.bytes).map_err(|e| e.to_string())?; #[cfg(target_arch = "wasm32")] @@ -83,7 +83,7 @@ impl PrintCraftApp { if self.run_inline { work(); } else { - std::thread::Builder::new().name("printcraft-action".into()).spawn(work).ok(); + std::thread::Builder::new().name("pdfcraft-action".into()).spawn(work).ok(); } #[cfg(target_arch = "wasm32")] work(); @@ -177,7 +177,7 @@ pub fn decode(v: &serde_json::Value) -> Vec { } /// The Action Wizard dialog. Returns `true` to close. -pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool { +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { if app.wizard.editing.is_some() { return edit_body(ui, app, t); } @@ -245,7 +245,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bo close } -fn edit_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool { +fn edit_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { let Some((original, a)) = app.wizard.editing.as_mut() else { return false }; ui.label(egui::RichText::new(if original.is_some() { "Edit Action" } else { "New Action" }).font(theme::semibold(18.0))); ui.add_space(8.0); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index d75ced88..e939c2a5 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -9,11 +9,11 @@ use std::ops::Range; use std::sync::Arc; use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHandle, TextureOptions, Vec2, pos2, vec2}; -use printcraft_engine::{DocId, Edit}; -use printcraft_render::{DocInfo, LinkTarget, PageText, RenderPool, RenderRequest, RequestKind, Tile}; +use pdfcraft_engine::{DocId, Edit}; +use pdfcraft_render::{DocInfo, LinkTarget, PageText, RenderPool, RenderRequest, RequestKind, Tile}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, QuickTool, RightPanel, comments, icons, widgets}; +use crate::{PdfCraftApp, QuickTool, RightPanel, comments, icons, widgets}; /// Logical pixels per PDF point at 100% (96 dpi, like browsers). pub const PT: f32 = 96.0 / 72.0; @@ -141,10 +141,10 @@ pub struct DocView { pub pending_edit: Option, /// Edit text: the lines per page (with the document generation they were read at), and the /// line being edited. - pub(crate) edit_lines: HashMap)>, + pub(crate) edit_lines: HashMap)>, pub line_editor: Option, /// Edit text & images: the images per page (by document generation), and the selected one. - pub(crate) edit_images: HashMap)>, + pub(crate) edit_images: HashMap)>, pub image_selection: Option, /// A paragraph box being dragged (moved, or resized from its right edge) in Edit text. pub block_drag: Option, @@ -566,7 +566,7 @@ impl DocView { } /// Displayed page size in points for this view rotation. - fn display_size(&self, p: &printcraft_render::PageInfo) -> (f32, f32) { + fn display_size(&self, p: &pdfcraft_render::PageInfo) -> (f32, f32) { if self.rotation % 180 == 90 { (p.height, p.width) } else { (p.width, p.height) } } @@ -713,7 +713,7 @@ impl DocView { } PageLayout::TwoUp => { // With a cover page, the first page sits alone on the right. - let rows: Vec<&[printcraft_render::PageInfo]> = if self.cover && !info.pages.is_empty() { + let rows: Vec<&[pdfcraft_render::PageInfo]> = if self.cover && !info.pages.is_empty() { std::iter::once(&info.pages[..1]).chain(info.pages[1..].chunks(2)).collect() } else { info.pages.chunks(2).collect() @@ -898,7 +898,7 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context) { } } -pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { +pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); // The Search panel closed: its search moves to the find bar. let search_open = app.right == Some(crate::RightPanel::Search); @@ -1176,11 +1176,11 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { let corners = [(vx as f64 - w / 2.0, vy as f64 - h / 2.0), (vx as f64 + w / 2.0, vy as f64 + h / 2.0)]; let u: Vec<[f32; 2]> = corners.iter().map(|(x, y)| info.pages[i].view_to_user(*x as f32, *y as f32)).collect(); let rect = [u[0][0].min(u[1][0]) as f64, u[0][1].min(u[1][1]) as f64, u[0][0].max(u[1][0]) as f64, u[0][1].max(u[1][1]) as f64]; - let by = (kind.group() == printcraft_engine::StampGroup::Dynamic).then(|| by_line.clone()); - let shape = printcraft_engine::Shape::Stamp { rect, stamp: kind, by }; - view.pending_edit = Some(printcraft_engine::Edit::AddAnnotation(printcraft_engine::NewAnnotation { + let by = (kind.group() == pdfcraft_engine::StampGroup::Dynamic).then(|| by_line.clone()); + let shape = pdfcraft_engine::Shape::Stamp { rect, stamp: kind, by }; + view.pending_edit = Some(pdfcraft_engine::Edit::AddAnnotation(pdfcraft_engine::NewAnnotation { page: i, - style: printcraft_engine::Style::default_for(&shape), + style: pdfcraft_engine::Style::default_for(&shape), shape, contents: String::new(), author: author.clone(), @@ -1198,11 +1198,11 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { let (vx, vy) = xf.screen_to_view(p); let u = info.pages[i].view_to_user(vx, vy); let (x, y) = (u[0] as f64, u[1] as f64); - view.pending_edit = Some(printcraft_engine::Edit::AddCustomStamp { + view.pending_edit = Some(pdfcraft_engine::Edit::AddCustomStamp { page: i, rect: [x, y, x, y], name: cs.name.clone(), - file: printcraft_engine::MarkFile { name: cs.file.clone(), bytes: cs.data.clone(), page: cs.page }, + file: pdfcraft_engine::MarkFile { name: cs.file.clone(), bytes: cs.data.clone(), page: cs.page }, author: author.clone(), }); stamp_placed = true; @@ -1370,7 +1370,7 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { // red border. if view.highlight_fields { for f in form.iter() { - let required = f.has(printcraft_engine::field_flags::REQUIRED); + let required = f.has(pdfcraft_engine::field_flags::REQUIRED); for w in f.widgets.iter().filter(|w| w.page == Some(i)) { let r = w.rect; let sr = xf.user_rect(info, i, [r[0] as f32, r[1] as f32, r[2] as f32, r[3] as f32]); @@ -1563,7 +1563,7 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { } let form_notice = view.forms.notice.take(); // One crop, then back to selecting (as Acrobat does). - let cropped = view.pending_edit.as_ref().is_some_and(|e| matches!(e, printcraft_engine::Edit::SetPageBox { .. })); + let cropped = view.pending_edit.as_ref().is_some_and(|e| matches!(e, pdfcraft_engine::Edit::SetPageBox { .. })); let mut tool = app.quick_tool; comments::keys(ui.ctx(), view, &mut tool, allowed); if preparing { @@ -1658,9 +1658,9 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { app.views[index].pending_edit = Some(if app.quick_tool == QuickTool::Comment(comments::CommentTool::Highlight) { // An area highlight: a highlight over the box. let style = app.comment_prefs.style(comments::CommentTool::Highlight); - printcraft_engine::Edit::AddAnnotation(printcraft_engine::NewAnnotation { + pdfcraft_engine::Edit::AddAnnotation(pdfcraft_engine::NewAnnotation { page, - shape: printcraft_engine::Shape::TextMarkup { kind: printcraft_engine::Markup::Highlight, quads }, + shape: pdfcraft_engine::Shape::TextMarkup { kind: pdfcraft_engine::Markup::Highlight, quads }, style, contents: String::new(), author, @@ -1693,8 +1693,8 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) { } /// Run a push button's action (the ones that need no JavaScript engine). -fn run_button(app: &mut PrintCraftApp, index: usize, ctx: &egui::Context, name: &str, action: printcraft_engine::form_scripts::ButtonAction) { - use printcraft_engine::form_scripts::ButtonAction as B; +fn run_button(app: &mut PdfCraftApp, index: usize, ctx: &egui::Context, name: &str, action: pdfcraft_engine::form_scripts::ButtonAction) { + use pdfcraft_engine::form_scripts::ButtonAction as B; let pages = app.session.get(app.views[index].id).map_or(0, |d| d.info.pages.len()); match action { B::Reset { fields, exclude } => { @@ -1706,7 +1706,7 @@ fn run_button(app: &mut PrintCraftApp, index: usize, ctx: &egui::Context, name: (false, true) => Some(all.iter().filter(|n| !listed(n)).cloned().collect()), }; app.views[index].forms.focus = None; - app.views[index].pending_edit = Some(printcraft_engine::Edit::ResetForm { names }); + app.views[index].pending_edit = Some(pdfcraft_engine::Edit::ResetForm { names }); } B::Named(n) => match n.as_str() { "Print" => app.open_print(), @@ -1720,7 +1720,7 @@ fn run_button(app: &mut PrintCraftApp, index: usize, ctx: &egui::Context, name: B::GoTo(p) => app.views[index].go_to_page(p.min(pages.saturating_sub(1))), B::Alert(m) => app.notify(m), B::Submit(url) => { - app.notify(format!("{name} submits the form to {url}; PrintCraft doesn't send form data. Save the document to keep your entries.")) + app.notify(format!("{name} submits the form to {url}; PdfCraft doesn't send form data. Save the document to keep your entries.")) } B::ImportIcon => app.choose_field_image(name), B::Script(js) => { @@ -1859,16 +1859,16 @@ fn notices( let mut open_repairs = false; let msg = if secured { Some(("lock", "This document is secured. Some changes are restricted by its security settings.".to_string(), false)) - } else if info.xfa == Some(printcraft_render::Xfa::Dynamic) { + } else if info.xfa == Some(pdfcraft_render::Xfa::Dynamic) { Some(( "triangle-alert", - "This is a dynamic XFA form, which PrintCraft can't display yet. What you see is the file's placeholder page.".to_string(), + "This is a dynamic XFA form, which PdfCraft can't display yet. What you see is the file's placeholder page.".to_string(), false, )) - } else if info.xfa == Some(printcraft_render::Xfa::Static) { + } else if info.xfa == Some(pdfcraft_render::Xfa::Static) { Some(( "triangle-alert", - "This form also contains XFA data, which PrintCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead.".to_string(), + "This form also contains XFA data, which PdfCraft doesn't read yet. You can fill its fields, but Acrobat may show the XFA values instead.".to_string(), true, )) } else if !info.fields.is_empty() { @@ -1911,7 +1911,7 @@ fn notices( } /// The floating quick-action bar at the left edge of the document area. -fn quick_bar(app: &mut PrintCraftApp, area: Rect, ui: &mut egui::Ui) { +fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let pos = area.left_top() + vec2(14.0, 14.0); egui::Area::new(egui::Id::new("quick-bar")).order(egui::Order::Middle).fixed_pos(pos).show(ui.ctx(), |ui| { @@ -2094,7 +2094,7 @@ fn organize_toolbar(view: &mut DocView, info: &DocInfo, editable: bool, ui: &mut // selection in Organize Pages). let sel = icons::button(ui, "list", 30.0, false, "Select pages"); egui::Popup::menu(&sel).show(|ui| { - use printcraft_engine::{PageOrientation as O, PageParity as P, filter_pages}; + use pdfcraft_engine::{PageOrientation as O, PageParity as P, filter_pages}; let all: Vec = (0..n).collect(); for (label, parity, orient) in [ ("All pages", P::Both, O::Both), diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index cb737600..71b34760 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -4,9 +4,9 @@ use egui::{Align, Align2, Color32, CornerRadius, Layout, Rect, Sense, Stroke, ve use crate::canvas::{Fit, PageLayout}; use crate::theme::{self, ThemeKind, Tokens}; -use crate::{Dialog, Mode, PrintCraftApp, PropsTab, RightPanel, icons, widgets}; +use crate::{Dialog, Mode, PdfCraftApp, PropsTab, RightPanel, icons, widgets}; -pub fn tab_strip(app: &mut PrintCraftApp, ui: &mut egui::Ui) { +pub fn tab_strip(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let left = if cfg!(target_os = "macos") && app.integrated_titlebar { 80 } else { 8 }; egui::Panel::top("tab_strip") @@ -55,7 +55,7 @@ pub fn tab_strip(app: &mut PrintCraftApp, ui: &mut egui::Ui) { app.dialog = Some(Dialog::Shortcuts); } // One click to the community, from anywhere in the app. - if widgets::ghost_button(ui, "messages-square", "Discord").on_hover_text(printcraft_engine::links::DISCORD).clicked() { + if widgets::ghost_button(ui, "messages-square", "Discord").on_hover_text(pdfcraft_engine::links::DISCORD).clicked() { app.execute("help.discord"); } }); @@ -103,7 +103,7 @@ fn tab(ui: &mut egui::Ui, t: &Tokens, name: &str, dirty: bool, active: bool, clo resp.on_hover_text(if dirty { format!("{name} — unsaved changes") } else { name.to_string() }) } -pub fn mode_bar(app: &mut PrintCraftApp, ui: &mut egui::Ui) { +pub fn mode_bar(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); egui::Panel::top("mode_bar") .exact_size(48.0) @@ -152,7 +152,7 @@ pub fn mode_bar(app: &mut PrintCraftApp, ui: &mut egui::Ui) { }); } -fn main_menu(app: &mut PrintCraftApp, ui: &mut egui::Ui) { +fn main_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let language = app.language; let t = Tokens::get(ui.ctx()); let resp = widgets::ghost_button(ui, "panel-left", language.tr("Menu")); @@ -256,7 +256,7 @@ fn main_menu(app: &mut PrintCraftApp, ui: &mut egui::Ui) { }); } -pub fn right_rail(app: &mut PrintCraftApp, ui: &mut egui::Ui) { +pub fn right_rail(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let Some((index, id)) = app.active_ids() else { return }; let Some(doc) = app.session.get(id) else { return }; diff --git a/crates/ui-egui/src/combine_ui.rs b/crates/ui-egui/src/combine_ui.rs index 3f168c78..6466e8d2 100644 --- a/crates/ui-egui/src/combine_ui.rs +++ b/crates/ui-egui/src/combine_ui.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use egui::{Align, Layout}; use crate::theme::{self, Tokens}; -use crate::{Dialog, PrintCraftApp, icons, widgets}; +use crate::{Dialog, PdfCraftApp, icons, widgets}; #[derive(Clone, Debug)] pub struct CombineFile { @@ -23,7 +23,7 @@ enum RowAction { Remove(usize), } -pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (bool, bool) { +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) { ui.label(egui::RichText::new("Combine files").font(theme::semibold(18.0))); ui.add_space(4.0); ui.label(egui::RichText::new("Files are combined in this order. Leave Pages empty to take every page.").small().color(t.text_faint)); @@ -88,12 +88,12 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (b (go, cancel) } -impl PrintCraftApp { +impl PdfCraftApp { /// Add picked files to the Combine files list (and show it). pub(crate) fn stage_combine(&mut self, files: Vec<(String, Vec)>) { for (name, bytes) in files { let bytes = Arc::new(bytes); - match printcraft_render::inspect(bytes.clone(), None) { + match pdfcraft_render::inspect(bytes.clone(), None) { Ok(info) => self.combine_draft.push(CombineFile { name, bytes, pages: info.pages.len(), range: String::new() }), Err(e) => self.notify(format!("Couldn't add {name}: {e}")), } diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index c1a0e8b5..276f9c03 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -1,15 +1,15 @@ -//! What each registered command does in this frontend (`printcraft_engine::commands` says +//! What each registered command does in this frontend (`pdfcraft_engine::commands` says //! what it is called, where it appears, which key runs it and when it is enabled). //! //! Menus, keyboard shortcuts, the palette, the tool panels and automation (`set_option`, and -//! the control channel later) all call `PrintCraftApp::execute`. +//! the control channel later) all call `PdfCraftApp::execute`. -use printcraft_engine::Edit; -use printcraft_engine::commands::{self, COMMANDS, CommandSpec}; +use pdfcraft_engine::Edit; +use pdfcraft_engine::commands::{self, COMMANDS, CommandSpec}; -use crate::{Dialog, Mode, PrintCraftApp, PropsTab, RightPanel, SaveTarget, theme::ThemeKind, widgets}; +use crate::{Dialog, Mode, PdfCraftApp, PropsTab, RightPanel, SaveTarget, theme::ThemeKind, widgets}; -impl PrintCraftApp { +impl PdfCraftApp { pub(crate) fn command_enabled(&self, spec: &CommandSpec) -> bool { commands::is_enabled(spec, &self.session, self.active_ids().map(|(_, id)| id)) } @@ -20,7 +20,7 @@ impl PrintCraftApp { /// Last-resort guard (AGENTS.md §4): a command that panics is reported and the app, with its /// open documents, keeps running. Edits are applied to a copy, so the document is unchanged. pub fn execute(&mut self, id: &str) -> bool { - match printcraft_engine::guard(|| self.execute_unguarded(id)) { + match pdfcraft_engine::guard(|| self.execute_unguarded(id)) { Ok(done) => done, Err(m) => { self.notify(format!("That didn't work: an internal error stopped it ({m}). Your documents are unchanged.")); @@ -44,9 +44,9 @@ impl PrintCraftApp { commands::Needs::Marks(k) if self.active.is_some() => format!( "This document has no {} to change", match k { - printcraft_engine::MarkKind::HeaderFooter => "header or footer", - printcraft_engine::MarkKind::Watermark => "watermark", - printcraft_engine::MarkKind::Background => "background", + pdfcraft_engine::MarkKind::HeaderFooter => "header or footer", + pdfcraft_engine::MarkKind::Watermark => "watermark", + pdfcraft_engine::MarkKind::Background => "background", } ), commands::Needs::Security | commands::Needs::ProtectedSecurity if self.active.is_some() => { @@ -102,8 +102,8 @@ impl PrintCraftApp { } self.dialog = Some(Dialog::NumberPages); } - link if printcraft_engine::links::for_command(link).is_some() => { - if let Some(l) = printcraft_engine::links::for_command(link) { + link if pdfcraft_engine::links::for_command(link).is_some() => { + if let Some(l) = pdfcraft_engine::links::for_command(link) { self.open_url(l.url); } } @@ -149,14 +149,8 @@ impl PrintCraftApp { self.views[i].clear_selection(); let style = self.comment_prefs.style(tool); let author = self.comment_prefs.author.clone(); - let shape = printcraft_engine::Shape::TextMarkup { kind, quads }; - self.apply_edit(Edit::AddAnnotation(printcraft_engine::NewAnnotation { - page, - shape, - style, - contents: String::new(), - author, - })); + let shape = pdfcraft_engine::Shape::TextMarkup { kind, quads }; + self.apply_edit(Edit::AddAnnotation(pdfcraft_engine::NewAnnotation { page, shape, style, contents: String::new(), author })); } } } @@ -206,7 +200,7 @@ impl PrintCraftApp { | "edit.header_footer.update" | "edit.watermark.update" | "edit.background.update" => { - use printcraft_engine::MarkKind as K; + use pdfcraft_engine::MarkKind as K; let kind = if id.starts_with("edit.header_footer") { K::HeaderFooter } else if id.starts_with("edit.watermark") { @@ -225,10 +219,10 @@ impl PrintCraftApp { self.marks_draft.hf.text[5] = "<>".into(); } self.marks_draft.focused_box = 5; - self.dialog = Some(Dialog::Marks(printcraft_engine::MarkKind::HeaderFooter)); + self.dialog = Some(Dialog::Marks(pdfcraft_engine::MarkKind::HeaderFooter)); } "edit.header_footer.remove" | "edit.watermark.remove" | "edit.background.remove" => { - use printcraft_engine::MarkKind as K; + use pdfcraft_engine::MarkKind as K; let kind = match id { "edit.header_footer.remove" => K::HeaderFooter, "edit.watermark.remove" => K::Watermark, @@ -323,9 +317,9 @@ impl PrintCraftApp { } "form.tab_order.row" | "form.tab_order.column" | "form.tab_order.structure" => { let order = match id { - "form.tab_order.row" => printcraft_engine::TabOrder::Row, - "form.tab_order.column" => printcraft_engine::TabOrder::Column, - _ => printcraft_engine::TabOrder::Structure, + "form.tab_order.row" => pdfcraft_engine::TabOrder::Row, + "form.tab_order.column" => pdfcraft_engine::TabOrder::Column, + _ => pdfcraft_engine::TabOrder::Structure, }; let n = active.and_then(|i| self.session.get(self.views[i].id)).map_or(0, |d| d.info.pages.len()); self.apply_edit(Edit::SetTabOrder { pages: (0..n).collect(), order }); @@ -379,9 +373,9 @@ impl PrintCraftApp { } "form.export_data" => self.export_data_dialog(false, true), "form.merge_data" => self.merge_data_dialog(), - "export.docx" => self.export_office_dialog(printcraft_engine::compare::OfficeFormat::Docx), - "export.html" => self.export_office_dialog(printcraft_engine::compare::OfficeFormat::Html), - "export.rtf" => self.export_office_dialog(printcraft_engine::compare::OfficeFormat::Rtf), + "export.docx" => self.export_office_dialog(pdfcraft_engine::compare::OfficeFormat::Docx), + "export.html" => self.export_office_dialog(pdfcraft_engine::compare::OfficeFormat::Html), + "export.rtf" => self.export_office_dialog(pdfcraft_engine::compare::OfficeFormat::Rtf), "form.prepare" => { self.left = crate::LeftPanel::Tool("form"); self.left_open = true; @@ -496,7 +490,7 @@ impl PrintCraftApp { } /// Render a top-level menu's registered commands (with live labels, shortcuts and enablement). -pub(crate) fn registry_menu(app: &mut PrintCraftApp, ui: &mut egui::Ui, menu: &str) { +pub(crate) fn registry_menu(app: &mut PdfCraftApp, ui: &mut egui::Ui, menu: &str) { let mac = cfg!(target_os = "macos") || cfg!(target_arch = "wasm32"); for spec in commands::menu(menu) { let label = commands::current_label(spec, &app.session, app.active_ids().map(|(_, id)| id)); diff --git a/crates/ui-egui/src/comment_props.rs b/crates/ui-egui/src/comment_props.rs index 2bffc6cc..8468feff 100644 --- a/crates/ui-egui/src/comment_props.rs +++ b/crates/ui-egui/src/comment_props.rs @@ -3,11 +3,11 @@ //! subject, modified) and Review History (status changes), with Acrobat's Locked box. use egui::{Align, Layout}; -use printcraft_engine::{CommentProps, Edit, NoteIcon}; +use pdfcraft_engine::{CommentProps, Edit, NoteIcon}; use crate::comments::swatch_grid; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PropsTab { @@ -28,7 +28,7 @@ pub struct PropsDraft { const ICONS: [NoteIcon; 7] = [NoteIcon::Comment, NoteIcon::Note, NoteIcon::Help, NoteIcon::Insert, NoteIcon::Key, NoteIcon::NewParagraph, NoteIcon::Paragraph]; -impl PrintCraftApp { +impl PdfCraftApp { /// Attach file: ask for a file (or take `attach_override`) and attach it at `at`. pub fn attach_file_comment(&mut self, page: usize, at: [f64; 2]) { let picked = match self.attach_override.take() { @@ -43,8 +43,8 @@ impl PrintCraftApp { }; let Some((file, data)) = picked else { return }; let tool = crate::comments::CommentTool::Attach; - let shape = printcraft_engine::Shape::Attachment { at, icon: printcraft_engine::AttachIcon::PushPin, file, data }; - let edit = Edit::AddAnnotation(printcraft_engine::NewAnnotation { + let shape = pdfcraft_engine::Shape::Attachment { at, icon: pdfcraft_engine::AttachIcon::PushPin, file, data }; + let edit = Edit::AddAnnotation(pdfcraft_engine::NewAnnotation { page, shape, style: self.comment_prefs.style(tool), @@ -114,7 +114,7 @@ pub fn edits(d: &PropsDraft) -> Vec { } /// Draw the dialog; returns (apply, cancel). -pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (bool, bool) { +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) { let history: Vec<(String, String, String)> = app .active_ids() .and_then(|(_, id)| app.session.get(id)) @@ -188,7 +188,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (b ui.add(egui::TextEdit::singleline(&mut e.subject).desired_width(280.0)).labelled_by(l.id); ui.end_row(); ui.label("Modified"); - ui.label(e.modified.as_deref().map(printcraft_render::pretty_date).unwrap_or_else(|| "—".into())); + ui.label(e.modified.as_deref().map(pdfcraft_render::pretty_date).unwrap_or_else(|| "—".into())); ui.end_row(); }); } diff --git a/crates/ui-egui/src/comments.rs b/crates/ui-egui/src/comments.rs index fe75ba96..d1a093ab 100644 --- a/crates/ui-egui/src/comments.rs +++ b/crates/ui-egui/src/comments.rs @@ -9,12 +9,12 @@ //! Reply, Set status, Colour and Delete. //! - **Composer:** the floating "Add a comment" card used for new notes, text boxes and edits. //! -//! Everything is turned into `printcraft_engine::Edit`s, queued on the view as `pending_edit` and +//! Everything is turned into `pdfcraft_engine::Edit`s, queued on the view as `pending_edit` and //! applied by the app, so each change is one undo step. use egui::{Color32, CornerRadius, Pos2, Rect, Sense, Stroke, pos2, vec2}; -use printcraft_engine::{Edit, Markup, NewAnnotation, NoteIcon, ReviewState, Rgb, Shape, Style}; -use printcraft_render::{Annotation, DocInfo}; +use pdfcraft_engine::{Edit, Markup, NewAnnotation, NoteIcon, ReviewState, Rgb, Shape, Style}; +use pdfcraft_render::{Annotation, DocInfo}; use crate::canvas::{DocView, PageXform}; use crate::theme::{self, Tokens}; @@ -211,7 +211,7 @@ impl CommentTool { Self::Caret => Shape::Caret { rect: [0.0; 4] }, Self::ReplaceText => Shape::TextMarkup { kind: Markup::StrikeOut, quads: Vec::new() }, Self::Eraser => Shape::Ink { strokes: Vec::new() }, - Self::Attach => Shape::Attachment { at: [0.0; 2], icon: printcraft_engine::AttachIcon::PushPin, file: String::new(), data: Vec::new() }, + Self::Attach => Shape::Attachment { at: [0.0; 2], icon: pdfcraft_engine::AttachIcon::PushPin, file: String::new(), data: Vec::new() }, } } } @@ -1115,7 +1115,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, /// A text box sized to its text (at most 300 pt wide), hanging from its top-left corner. pub fn text_box_rect(at: [f64; 2], text: &str, size: f64) -> [f64; 4] { - use printcraft_engine::annot_text::{text_width, wrap}; + use pdfcraft_engine::annot_text::{text_width, wrap}; let pad = 2.0; let longest = text.lines().map(|l| text_width(l, size)).fold(0.0, f64::max); let w = (longest + 2.0 * pad + 4.0).clamp(40.0, 300.0); @@ -1186,8 +1186,7 @@ pub(crate) fn context_menu(ui: &mut egui::Ui, view: &mut DocView, info: &DocInfo } }); }); - let thread: Vec<&printcraft_render::Annotation> = - info.annotations.iter().filter(|r| a.name.is_some() && r.in_reply_to == a.name).collect(); + let thread: Vec<&pdfcraft_render::Annotation> = info.annotations.iter().filter(|r| a.name.is_some() && r.in_reply_to == a.name).collect(); let marked = crate::comments_panel::is_marked(&thread); if ui.add_enabled(allowed, egui::Button::new(if marked { "Remove checkmark" } else { "Mark with checkmark" })).clicked() { action = Some(CanvasAction::Edit(Box::new(Edit::MarkAnnotation { page, index, marked: !marked, author: prefs.author.clone() }))); @@ -1317,7 +1316,7 @@ mod tests { fn tools_round_trip_through_their_commands() { for t in ALL { assert_eq!(CommentTool::from_command(t.command()), Some(t)); - assert!(printcraft_engine::commands::command(t.command()).is_some(), "{} is registered", t.command()); + assert!(pdfcraft_engine::commands::command(t.command()).is_some(), "{} is registered", t.command()); assert!(icons::exists(t.icon()), "icon {}", t.icon()); assert!(GROUPS[t.group()].contains(&t)); } diff --git a/crates/ui-egui/src/comments_panel.rs b/crates/ui-egui/src/comments_panel.rs index e5a66905..b86b7aa4 100644 --- a/crates/ui-egui/src/comments_panel.rs +++ b/crates/ui-egui/src/comments_panel.rs @@ -5,8 +5,8 @@ //! opens its reply box and "…" menu (Edit, Set status, Delete). Status replies show as a badge. use egui::{Align, Color32, CornerRadius, Layout, Sense, Stroke, vec2}; -use printcraft_engine::{Edit, NoteIcon, ReviewState, Shape}; -use printcraft_render::{Annotation, DocInfo}; +use pdfcraft_engine::{Edit, NoteIcon, ReviewState, Shape}; +use pdfcraft_render::{Annotation, DocInfo}; use crate::canvas::DocView; use crate::comments::{CommentPrefs, CommentTool, SortBy, color32, status_badge}; @@ -128,7 +128,7 @@ pub(crate) fn show( if let Some(p) = info.pages.get(page) { let at = p.view_to_user(p.width - 44.0, 24.0); let text = std::mem::take(&mut view.comments.add_box); - edit = Some(Edit::AddAnnotation(printcraft_engine::NewAnnotation { + edit = Some(Edit::AddAnnotation(pdfcraft_engine::NewAnnotation { page, shape: Shape::Note { at: [at[0] as f64, at[1] as f64], icon: NoteIcon::Comment }, style: prefs.style(CommentTool::Note), diff --git a/crates/ui-egui/src/compare_ui.rs b/crates/ui-egui/src/compare_ui.rs index 83a395e6..0f8ff312 100644 --- a/crates/ui-egui/src/compare_ui.rs +++ b/crates/ui-egui/src/compare_ui.rs @@ -3,11 +3,11 @@ //! offers a report or the differences as comments. use egui::{Align, Color32, Layout}; -use printcraft_engine::DocId; -use printcraft_engine::compare::{Comparison, Kind}; +use pdfcraft_engine::DocId; +use pdfcraft_engine::compare::{Comparison, Kind}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, RightPanel, widgets}; +use crate::{PdfCraftApp, RightPanel, widgets}; /// The last comparison. pub struct CompareState { @@ -31,12 +31,12 @@ pub enum PanelAction { } pub fn colour(kind: Kind) -> Color32 { - let [r, g, b] = printcraft_engine::compare::colour(kind); + let [r, g, b] = pdfcraft_engine::compare::colour(kind); Color32::from_rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8) } /// The Compare Files dialog: pick the older document. Returns (compare, cancel). -pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (bool, bool) { +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) { ui.label(egui::RichText::new("Compare Files").font(theme::semibold(18.0))); ui.add_space(8.0); let Some((_, new)) = app.active_ids() else { return (false, true) }; @@ -136,7 +136,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &Option, action } -impl PrintCraftApp { +impl PdfCraftApp { /// Compare the chosen older document with the active one and show the differences. pub fn run_compare(&mut self) { let Some((i, new)) = self.active_ids() else { return }; diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs index 456e5d08..c852b3ca 100644 --- a/crates/ui-egui/src/content_ui.rs +++ b/crates/ui-egui/src/content_ui.rs @@ -4,8 +4,8 @@ //! Delete to remove it. Each change is one undoable engine edit. use egui::{Color32, CornerRadius, Pos2, Rect, Stroke}; -use printcraft_engine::{Added, AddedContent, AddedText, Edit, FontFamily, TextAlign}; -use printcraft_render::DocInfo; +use pdfcraft_engine::{Added, AddedContent, AddedText, Edit, FontFamily, TextAlign}; +use pdfcraft_render::DocInfo; use crate::canvas::{DocView, PageXform}; use crate::theme::Tokens; @@ -403,7 +403,7 @@ pub(crate) fn hint(ui: &mut egui::Ui, t: &Tokens) { ui.add_space(4.0); } -impl crate::PrintCraftApp { +impl crate::PdfCraftApp { /// Edit a PDF ▸ Add content ▸ Image: pick a file and place it in the middle of the current page. pub fn add_image_dialog(&mut self) { #[cfg(not(target_arch = "wasm32"))] @@ -449,7 +449,7 @@ impl crate::PrintCraftApp { self.apply_edit(Edit::EditPageImage { page, index, - change: printcraft_engine::ImageEdit::Replace { name, bytes: std::sync::Arc::new(bytes) }, + change: pdfcraft_engine::ImageEdit::Replace { name, bytes: std::sync::Arc::new(bytes) }, }); } Err(e) => self.notify(format!("Couldn't read {}: {e}", path.display())), @@ -539,18 +539,18 @@ impl crate::PrintCraftApp { /// What the image tools ask for. pub(crate) enum ImageAction { - Update(printcraft_engine::AddedContent), + Update(pdfcraft_engine::AddedContent), Replace, } /// Edit image: rotate, flip, crop, replace (shown while an added image is selected). -pub(crate) fn image_panel(ui: &mut egui::Ui, t: &Tokens, img: &printcraft_engine::AddedImage) -> Option { +pub(crate) fn image_panel(ui: &mut egui::Ui, t: &Tokens, img: &pdfcraft_engine::AddedImage) -> Option { let mut out = None; widgets::section_title(ui, "Edit image"); ui.horizontal(|ui| { let mut i = img.clone(); let r = i.rect; - let turn = |i: &mut printcraft_engine::AddedImage, k: u8| { + let turn = |i: &mut pdfcraft_engine::AddedImage, k: u8| { i.rotation = (i.rotation + k) % 4; // The box turns with the picture, around its centre. let (cx, cy, w, h) = ((r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0, r[2] - r[0], r[3] - r[1]); diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index ae3a255d..c644c360 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -8,7 +8,7 @@ //! - [`ControlPlugin`], an egui plugin, keeps a copy of the AccessKit tree from every frame's //! output (the widget tree with ids, roles, labels, rects and states) and injects queued input //! events (AccessKit click actions, pointer events, keys, text) at the start of later frames. -//! - Requests reach the app through a channel; `PrintCraftApp` answers them at the start of a +//! - Requests reach the app through a channel; `PdfCraftApp` answers them at the start of a //! frame, so they see and change exactly what the user would. //! //! Methods (JSON in, JSON out): @@ -105,7 +105,7 @@ pub struct ControlPlugin { impl egui::Plugin for ControlPlugin { fn debug_name(&self) -> &'static str { - "printcraft-control" + "pdfcraft-control" } fn input_hook(&mut self, ctx: &egui::Context, input: &mut egui::RawInput) { @@ -163,7 +163,7 @@ pub fn attach(ctx: &egui::Context) -> (Control, ControlClient) { (Control { rx, shared, pending: Vec::new(), next_tag: 1 }, ControlClient { tx, ctx: ctx.clone() }) } -/// What a request needs from the app (implemented by `PrintCraftApp`). +/// What a request needs from the app (implemented by `PdfCraftApp`). pub(crate) trait Host { fn state(&self) -> Value; fn command(&mut self, id: &str) -> Reply; @@ -510,7 +510,7 @@ fn screenshot_png(image: &egui::ColorImage, region: Option, ppp: f32 Ok(json!({ "png_base64": base64::engine::general_purpose::STANDARD.encode(out), "width": cw, "height": ch, "pixels_per_point": ppp })) } -impl Host for crate::PrintCraftApp { +impl Host for crate::PdfCraftApp { fn state(&self) -> Value { let active = self.active.and_then(|i| self.views.get(i)); let docs: Vec = self @@ -571,7 +571,7 @@ impl Host for crate::PrintCraftApp { } fn command(&mut self, id: &str) -> Reply { - let spec = printcraft_engine::commands::command(id).ok_or_else(|| format!("unknown command {id:?} (see ui.commands)"))?; + let spec = pdfcraft_engine::commands::command(id).ok_or_else(|| format!("unknown command {id:?} (see ui.commands)"))?; if !self.command_enabled(spec) { return Err(format!("{id} is disabled right now")); } @@ -580,7 +580,7 @@ impl Host for crate::PrintCraftApp { } fn commands(&self) -> Value { - let list: Vec = printcraft_engine::commands::COMMANDS + let list: Vec = pdfcraft_engine::commands::COMMANDS .iter() .map(|c| json!({ "id": c.id, "label": c.label, "menu": c.menu, "enabled": self.command_enabled(c), "shortcut": c.shortcut.map(|s| s.label(cfg!(target_os = "macos"))) })) .collect(); @@ -628,11 +628,11 @@ pub fn serve(client: ControlClient) -> std::io::Result { let port = listener.local_addr()?.port(); let token = random_token()?; let expected = token.clone(); - std::thread::Builder::new().name("printcraft-control".into()).spawn(move || { + std::thread::Builder::new().name("pdfcraft-control".into()).spawn(move || { for stream in listener.incoming().flatten() { let client = client.clone(); let expected = expected.clone(); - let _ = std::thread::Builder::new().name("printcraft-control-conn".into()).spawn(move || { + let _ = std::thread::Builder::new().name("pdfcraft-control-conn".into()).spawn(move || { let Ok(read) = stream.try_clone() else { return }; let mut write = stream; let mut authed = false; diff --git a/crates/ui-egui/src/create_ui.rs b/crates/ui-egui/src/create_ui.rs index e0cb428c..17f3e96a 100644 --- a/crates/ui-egui/src/create_ui.rs +++ b/crates/ui-egui/src/create_ui.rs @@ -3,7 +3,7 @@ use std::sync::Arc; -use crate::PrintCraftApp; +use crate::PdfCraftApp; /// File types Open accepts besides PDF (converted on open). pub const CONVERTIBLE: [&str; 12] = ["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx", "txt", "text"]; @@ -62,7 +62,7 @@ fn stem(name: &str) -> &str { name.rsplit_once('.').map_or(name, |(s, _)| s) } -impl PrintCraftApp { +impl PdfCraftApp { /// Convert a non-PDF file (image, text) into a new tab. Returns `None` when `bytes` is not /// something Create understands (the caller then tries to open it as a PDF). pub(crate) fn open_converted(&mut self, name: &str, bytes: &[u8]) -> Option> { @@ -178,9 +178,9 @@ impl PrintCraftApp { /// the saving. pub(crate) fn save_optimized( &mut self, - id: printcraft_engine::DocId, + id: pdfcraft_engine::DocId, suffix: &str, - result: Result<(Arc>, String), printcraft_engine::EditError>, + result: Result<(Arc>, String), pdfcraft_engine::EditError>, ) { let Some(doc) = self.session.get(id) else { return }; let (before, name) = (doc.bytes.len(), format!("{} ({suffix}).pdf", stem(&doc.name))); @@ -191,7 +191,7 @@ impl PrintCraftApp { return; } }; - let saved = |app: &mut PrintCraftApp, place: String| { + let saved = |app: &mut PdfCraftApp, place: String| { let pct = 100.0 * (1.0 - bytes.len() as f64 / before.max(1) as f64); app.notify(format!( "Saved {place}: {} → {} ({pct:.0}% smaller){detail}", diff --git a/crates/ui-egui/src/crop.rs b/crates/ui-egui/src/crop.rs index 096f7a94..1344a30f 100644 --- a/crates/ui-egui/src/crop.rs +++ b/crates/ui-egui/src/crop.rs @@ -2,8 +2,8 @@ //! rectangle; double-click a page to open Set Page Boxes for it. use egui::{Color32, CornerRadius, Pos2, Rect, Stroke}; -use printcraft_engine::{BoxSpec, Edit, PageBox}; -use printcraft_render::DocInfo; +use pdfcraft_engine::{BoxSpec, Edit, PageBox}; +use pdfcraft_render::DocInfo; use crate::canvas::{DocView, PageXform}; diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d69d88cd..e47853ec 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3,13 +3,13 @@ use egui::{Align, Layout}; use crate::theme::{self, Tokens}; -use printcraft_engine::Edit; +use pdfcraft_engine::Edit; -use crate::{CloseRequest, Dialog, PrintCraftApp, PropsTab, panels::human_size, widgets}; +use crate::{CloseRequest, Dialog, PdfCraftApp, PropsTab, panels::human_size, widgets}; const INFO_KEYS: [&str; 4] = ["Title", "Author", "Subject", "Keywords"]; -pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { +pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) { password(app, ctx); save_prompt(app, ctx); crate::updates::dialog(app, ctx); @@ -130,7 +130,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { row(ui, "PDF producer", i.producer.clone().unwrap_or_default()); } PropsTab::InitialView => { - use printcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N}; + use pdfcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N}; let editable = doc.allows_modification(); let pages = i.pages.len(); let Some((_, v)) = app.view_draft.as_mut() else { return }; @@ -410,15 +410,15 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { ui.radio_value(&mut draft.mode, M::Selection, "Before each selected page (select pages in Organize)") }); let plan = match draft.mode { - M::Pages => crate::SplitPlan::By(printcraft_engine::SplitBy::PageCount(draft.every)), - M::Selection => crate::SplitPlan::By(printcraft_engine::SplitBy::Before(selected)), + M::Pages => crate::SplitPlan::By(pdfcraft_engine::SplitBy::PageCount(draft.every)), + M::Selection => crate::SplitPlan::By(pdfcraft_engine::SplitBy::Before(selected)), M::Size => crate::SplitPlan::Size((draft.size_mb * 1_048_576.0) as usize), M::Bookmarks => crate::SplitPlan::Bookmarks, }; let files = match &plan { - crate::SplitPlan::By(by) => Some(printcraft_engine::split_ranges(n, by).len()), + crate::SplitPlan::By(by) => Some(pdfcraft_engine::split_ranges(n, by).len()), crate::SplitPlan::Bookmarks => { - Some(printcraft_engine::split_ranges(n, &printcraft_engine::SplitBy::Before(marks.iter().map(|m| m.0).collect())).len()) + Some(pdfcraft_engine::split_ranges(n, &pdfcraft_engine::SplitBy::Before(marks.iter().map(|m| m.0).collect())).len()) } crate::SplitPlan::Size(_) => None, }; @@ -547,7 +547,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { return; } Dialog::RotatePages => { - use printcraft_engine::{PageOrientation as O, PageParity as P}; + use pdfcraft_engine::{PageOrientation as O, PageParity as P}; let n = app.active_ids().and_then(|(_, id)| app.session.get(id)).map_or(1, |d| d.info.pages.len()); let d = &mut app.rotate_draft; ui.label(egui::RichText::new("Rotate Pages").font(theme::semibold(18.0))); @@ -701,7 +701,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { ui.horizontal(|ui| { ui.label("Sort comments by:"); egui::ComboBox::from_id_salt("summary-sort").selected_text(app.summary_sort.name()).show_ui(ui, |ui| { - for s in printcraft_engine::SummarySort::ALL { + for s in pdfcraft_engine::SummarySort::ALL { ui.selectable_value(&mut app.summary_sort, s, s.name()); } }); @@ -888,7 +888,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { return; } Dialog::NumberPages => { - use printcraft_engine::LabelStyle as L; + use pdfcraft_engine::LabelStyle as L; ui.label(egui::RichText::new("Number pages").font(theme::semibold(18.0))); ui.add_space(8.0); let Some((_, id)) = app.active_ids() else { return }; @@ -955,7 +955,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { ui.label(egui::RichText::new("Recover unsaved documents?").font(theme::semibold(18.0))); }); ui.add_space(6.0); - ui.label("PrintCraft didn't shut down normally. These documents had changes that were autosaved:"); + ui.label("PdfCraft didn't shut down normally. These documents had changes that were autosaved:"); ui.add_space(8.0); let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); egui::Grid::new("recoverable").num_columns(2).spacing([18.0, 6.0]).show(ui, |ui| { @@ -979,7 +979,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { let mac = cfg!(target_os = "macos") || cfg!(target_arch = "wasm32"); // Registered commands first (always in sync with the real bindings), then the // keys the document view handles itself. - let mut rows: Vec<(String, String)> = printcraft_engine::commands::COMMANDS + let mut rows: Vec<(String, String)> = pdfcraft_engine::commands::COMMANDS .iter() .filter_map(|c| c.shortcut.map(|k| (k.label(mac), c.label.trim_end_matches('…').to_string()))) .collect(); @@ -1015,7 +1015,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { ui.horizontal(|ui| { widgets::artcraft_mark(ui, 40.0); ui.vertical(|ui| { - ui.label(egui::RichText::new("PrintCraft").font(theme::semibold(20.0))); + ui.label(egui::RichText::new("PdfCraft").font(theme::semibold(20.0))); ui.label(format!("Version {}", env!("CARGO_PKG_VERSION"))); }); }); @@ -1130,7 +1130,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { app.redact_search.found = Some(n); } Some(Dialog::RemoveHidden) => { - let which: Vec = app.hidden_draft.found.iter().filter(|f| f.2 && f.1 > 0).map(|f| f.0).collect(); + let which: Vec = app.hidden_draft.found.iter().filter(|f| f.2 && f.1 > 0).map(|f| f.0).collect(); let n: usize = app.hidden_draft.found.iter().filter(|f| f.2).map(|f| f.1).sum(); if app.apply_edit(Edit::RemoveHidden { which }) { app.notify(format!("Removed {n} hidden item{}. Save to remove them from the file.", if n == 1 { "" } else { "s" })); @@ -1235,7 +1235,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { } /// Info edits needed to make the document match the Description draft. -fn draft_changes(app: &PrintCraftApp) -> Option> { +fn draft_changes(app: &PdfCraftApp) -> Option> { let (id, draft) = app.props_draft.as_ref()?; let doc = app.session.get(*id)?; let mut edits: Vec = INFO_KEYS @@ -1272,7 +1272,7 @@ pub(crate) fn save_prompt_key(ctx: &egui::Context) -> Option> { } /// "Save changes?" when closing a tab or quitting with unsaved edits. -fn save_prompt(app: &mut PrintCraftApp, ctx: &egui::Context) { +fn save_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) { let Some(req) = app.close_request else { return }; let index = match req { CloseRequest::Tab(i) => Some(i), @@ -1320,7 +1320,7 @@ fn yes(b: bool) -> String { } /// Password prompt for encrypted documents (Acrobat: "Password" dialog on open). -fn password(app: &mut PrintCraftApp, ctx: &egui::Context) { +fn password(app: &mut PdfCraftApp, ctx: &egui::Context) { let Some(prompt) = app.password_prompt.as_mut() else { return }; let t = Tokens::get(ctx); let mut submit = false; diff --git a/crates/ui-egui/src/edit_text_ui.rs b/crates/ui-egui/src/edit_text_ui.rs index 4805b43c..633cb545 100644 --- a/crates/ui-egui/src/edit_text_ui.rs +++ b/crates/ui-egui/src/edit_text_ui.rs @@ -5,8 +5,8 @@ //! its proportions), right-click for rotate, flip, replace, save and delete; Delete removes it. use egui::{Color32, CornerRadius, FontFamily, FontId, Pos2, Rect, Stroke}; -use printcraft_engine::Edit; -use printcraft_render::DocInfo; +use pdfcraft_engine::Edit; +use pdfcraft_render::DocInfo; use crate::canvas::{DocView, PageXform}; @@ -31,8 +31,8 @@ pub struct LineEditor { size: f32, focus: bool, /// The Format text panel's values, and what the paragraph had (to send only changes). - pub look: printcraft_engine::AddedText, - look0: printcraft_engine::AddedText, + pub look: pdfcraft_engine::AddedText, + look0: pdfcraft_engine::AddedText, /// Underline, line spacing (× size; 0 = the paragraph's own), character spacing (pt) and /// horizontal scale (%), and what they were. pub extras: Extras, @@ -87,9 +87,9 @@ impl LineEditor { } /// The formatting the panel changed. - pub fn style(&self) -> printcraft_engine::BlockStyle { + pub fn style(&self) -> pdfcraft_engine::BlockStyle { let (l, o) = (&self.look, &self.look0); - printcraft_engine::BlockStyle { + pdfcraft_engine::BlockStyle { family: (l.family != o.family || l.bold != o.bold || l.italic != o.italic).then_some((l.family, l.bold, l.italic)), size: (l.size != o.size).then_some(l.size), color: (l.color != o.color).then_some(l.color), @@ -112,15 +112,15 @@ impl LineEditor { } /// Adopt the rewritten paragraph's current geometry before the next overlay frame. - pub(crate) fn refresh_source(&mut self, block: &printcraft_engine::TextBlock) { + pub(crate) fn refresh_source(&mut self, block: &pdfcraft_engine::TextBlock) { self.source_rect = block.rect.map(|v| v as f32); self.multiline = block.lines.len() > 1; } } /// The look shown for a paragraph: the family and weight guessed from its PDF font name. -fn source_look(base_font: &str, size: f64, color: [f64; 3], detected_bold: bool, detected_italic: bool) -> printcraft_engine::AddedText { - use printcraft_engine::FontFamily as F; +fn source_look(base_font: &str, size: f64, color: [f64; 3], detected_bold: bool, detected_italic: bool) -> pdfcraft_engine::AddedText { + use pdfcraft_engine::FontFamily as F; let name = base_font.to_ascii_lowercase(); let family = if ["courier", "mono", "consolas", "menlo", "monaco", "lucida console"].iter().any(|s| name.contains(s)) { F::Courier @@ -129,7 +129,7 @@ fn source_look(base_font: &str, size: f64, color: [f64; 3], detected_bold: bool, } else { F::Helvetica }; - printcraft_engine::AddedText { + pdfcraft_engine::AddedText { family, bold: detected_bold || ["bold", "black", "heavy", "semibold", "demi"].iter().any(|s| name.contains(s)), italic: detected_italic || ["italic", "oblique", "slanted"].iter().any(|s| name.contains(s)), @@ -139,13 +139,13 @@ fn source_look(base_font: &str, size: f64, color: [f64; 3], detected_bold: bool, } } -fn look_of(b: &printcraft_engine::TextBlock) -> printcraft_engine::AddedText { +fn look_of(b: &pdfcraft_engine::TextBlock) -> pdfcraft_engine::AddedText { source_look(&b.base_font, b.size, b.color, b.bold, b.italic) } -fn editor_font(look: &printcraft_engine::AddedText, size: f32) -> FontId { +fn editor_font(look: &pdfcraft_engine::AddedText, size: f32) -> FontId { let family = match (look.family, look.bold) { - (printcraft_engine::FontFamily::Courier, _) => FontFamily::Monospace, + (pdfcraft_engine::FontFamily::Courier, _) => FontFamily::Monospace, (_, true) => FontFamily::Name("semibold".into()), (_, false) => FontFamily::Proportional, }; @@ -206,7 +206,7 @@ pub(crate) fn image_input( xf: &PageXform, page: usize, info: &DocInfo, - images: &[printcraft_engine::PageImage], + images: &[pdfcraft_engine::PageImage], view: &mut DocView, action: &mut Option, ) -> bool { @@ -280,14 +280,14 @@ pub(crate) fn image_input( } if preview != b { view.pending_edit = - Some(Edit::EditPageImage { page, index: i, change: printcraft_engine::ImageEdit::Move(user_box(xf, info, page, preview)) }); + Some(Edit::EditPageImage { page, index: i, change: pdfcraft_engine::ImageEdit::Move(user_box(xf, info, page, preview)) }); } } return true; } if ui.input(|inp| inp.key_pressed(egui::Key::Delete) || inp.key_pressed(egui::Key::Backspace)) && !ui.ctx().egui_wants_keyboard_input() { view.image_selection = None; - view.pending_edit = Some(Edit::EditPageImage { page, index: i, change: printcraft_engine::ImageEdit::Delete }); + view.pending_edit = Some(Edit::EditPageImage { page, index: i, change: pdfcraft_engine::ImageEdit::Delete }); return true; } } @@ -302,7 +302,7 @@ pub(crate) fn image_input( } if selected == Some(hit) { resp.context_menu(|ui| { - use printcraft_engine::ImageEdit as E; + use pdfcraft_engine::ImageEdit as E; let items: [(&str, Option); 4] = [ ("Rotate Clockwise", Some(E::Rotate(1))), ("Rotate Counterclockwise", Some(E::Rotate(3))), @@ -342,7 +342,7 @@ pub(crate) fn page_input( xf: &PageXform, page: usize, info: &DocInfo, - lines: &[printcraft_engine::TextBlock], + lines: &[pdfcraft_engine::TextBlock], view: &mut DocView, ) -> bool { let boxes: Vec = lines.iter().map(|l| xf.user_rect(info, page, l.rect.map(|v| v as f32)).expand(2.0)).collect(); @@ -377,9 +377,9 @@ pub(crate) fn page_input( let style = if d.resize { // The new width in user space: the box's change, added to the paragraph's own. let width = (l.rect[2] - l.rect[0]) + (to[2] - to[0]) - (from[2] - from[0]); - printcraft_engine::BlockStyle { width: Some(width), ..Default::default() } + pdfcraft_engine::BlockStyle { width: Some(width), ..Default::default() } } else { - printcraft_engine::BlockStyle { offset: Some([to[0] - from[0], to[1] - from[1]]), ..Default::default() } + pdfcraft_engine::BlockStyle { offset: Some([to[0] - from[0], to[1] - from[1]]), ..Default::default() } }; if preview != b { view.pending_edit = Some(Edit::EditTextBlock { page, block: d.block, text: l.text.clone(), style }); @@ -495,7 +495,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) - Some(apply) => { let ed = view.line_editor.take()?; let style = ed.style(); - (apply && (ed.text != ed.original || style != printcraft_engine::BlockStyle::default())).then_some(Edit::EditTextBlock { + (apply && (ed.text != ed.original || style != pdfcraft_engine::BlockStyle::default())).then_some(Edit::EditTextBlock { page: ed.page, block: ed.block, text: ed.text, diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index 12f0ccda..cfe4f8aa 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -1,9 +1,9 @@ //! Editing glue: apply engine edits from the UI, undo/redo, save/save-as, and the //! "save changes?" prompt when closing a tab or quitting with unsaved edits. -use printcraft_engine::Edit; +use pdfcraft_engine::Edit; -use crate::PrintCraftApp; +use crate::PdfCraftApp; /// What the user was doing when we asked whether to save. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -25,7 +25,7 @@ pub enum SaveTarget { As, } -impl PrintCraftApp { +impl PdfCraftApp { /// Apply an edit to the active document. Returns `true` on success; failures are shown. pub fn apply_edit(&mut self, edit: Edit) -> bool { let Some((i, id)) = self.active_ids() else { return false }; @@ -341,7 +341,7 @@ pub fn write_atomically(path: &str, bytes: &[u8]) -> std::io::Result<()> { use std::io::Write; let target = std::path::Path::new(path); let dir = target.parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(std::path::Path::new(".")); - let tmp = dir.join(format!(".{}.printcraft-{}.tmp", target.file_name().and_then(|n| n.to_str()).unwrap_or("save"), std::process::id())); + let tmp = dir.join(format!(".{}.pdfcraft-{}.tmp", target.file_name().and_then(|n| n.to_str()).unwrap_or("save"), std::process::id())); let result = (|| { let mut f = std::fs::File::create(&tmp)?; f.write_all(bytes)?; @@ -379,7 +379,7 @@ pub(crate) fn download(name: &str, bytes: &[u8]) -> Result<(), String> { Ok(()) } -impl PrintCraftApp { +impl PdfCraftApp { /// Carry out a Bookmarks-panel action as an undoable edit. pub fn bookmark_action(&mut self, action: crate::panels::BmAction) { use crate::panels::BmAction as A; @@ -427,7 +427,7 @@ impl PrintCraftApp { } } -fn bookmark_at<'a>(items: &'a [printcraft_render::OutlineItem], path: &[usize]) -> Option<&'a printcraft_render::OutlineItem> { +fn bookmark_at<'a>(items: &'a [pdfcraft_render::OutlineItem], path: &[usize]) -> Option<&'a pdfcraft_render::OutlineItem> { let (first, rest) = path.split_first()?; let item = items.get(*first)?; if rest.is_empty() { Some(item) } else { bookmark_at(&item.children, rest) } diff --git a/crates/ui-egui/src/export_ui.rs b/crates/ui-egui/src/export_ui.rs index bcee1e77..437f8e93 100644 --- a/crates/ui-egui/src/export_ui.rs +++ b/crates/ui-egui/src/export_ui.rs @@ -6,11 +6,11 @@ use std::sync::{Arc, Mutex}; use egui::{Align, Layout}; -use printcraft_engine::export::{ExportSource, Exporter, ImageFormat}; +use pdfcraft_engine::export::{ExportSource, Exporter, ImageFormat}; use crate::marks_ui::PageRange; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ExportKind { @@ -38,7 +38,7 @@ impl Default for ExportDraft { /// Progress of a background export: (done, total, final message once finished). pub type ExportStatus = Arc)>>>; -pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens, kind: ExportKind) -> (bool, bool) { +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: ExportKind) -> (bool, bool) { let count = app.active_ids().and_then(|(_, id)| app.session.get(id)).map(|d| d.info.pages.len()).unwrap_or(0); let d = &mut app.export_draft; ui.label( @@ -137,12 +137,12 @@ fn run( }; if kind == ExportKind::AllImages { set(0, None); - let out = match printcraft_engine::export::extract_images(&src, &pages, min_side) { + let out = match pdfcraft_engine::export::extract_images(&src, &pages, min_side) { Ok(o) => o, Err(e) => return format!("Export stopped: {e}"), }; for (k, img) in out.images.iter().enumerate() { - if let Err(e) = sink(&printcraft_engine::export::image_file_name(&stem, img, k + 1), img.data.clone()) { + if let Err(e) = sink(&pdfcraft_engine::export::image_file_name(&stem, img, k + 1), img.data.clone()) { return format!("Export stopped: {e}"); } } @@ -177,7 +177,7 @@ fn run( } } -impl PrintCraftApp { +impl PdfCraftApp { /// Start exporting the active document with the dialog's settings. pub(crate) fn start_export(&mut self, kind: ExportKind) { let Some((_, id)) = self.active_ids() else { return }; @@ -211,7 +211,7 @@ impl PrintCraftApp { if self.export_dir_override.is_some() { work(); // tests and automation: synchronous } else { - std::thread::Builder::new().name("printcraft-export".into()).spawn(work).ok(); + std::thread::Builder::new().name("pdfcraft-export".into()).spawn(work).ok(); } } #[cfg(target_arch = "wasm32")] diff --git a/crates/ui-egui/src/files.rs b/crates/ui-egui/src/files.rs index a5b8766d..c211019e 100644 --- a/crates/ui-egui/src/files.rs +++ b/crates/ui-egui/src/files.rs @@ -5,9 +5,9 @@ use std::sync::Arc; -use printcraft_engine::{Edit, SplitBy}; +use pdfcraft_engine::{Edit, SplitBy}; -use crate::PrintCraftApp; +use crate::PdfCraftApp; /// Why files were picked. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -49,7 +49,7 @@ impl Default for SplitDraft { } } -/// Acrobat's Split by: number of pages, file size, top-level bookmarks (and PrintCraft's +/// Acrobat's Split by: number of pages, file size, top-level bookmarks (and PdfCraft's /// before-selected-pages). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SplitMode { @@ -85,8 +85,8 @@ pub struct RotateDraft { pub which: u8, pub from: usize, pub to: usize, - pub parity: printcraft_engine::PageParity, - pub orientation: printcraft_engine::PageOrientation, + pub parity: pdfcraft_engine::PageParity, + pub orientation: pdfcraft_engine::PageOrientation, } impl Default for RotateDraft { @@ -95,7 +95,7 @@ impl Default for RotateDraft { } } -impl PrintCraftApp { +impl PdfCraftApp { /// Ask for files to combine (File ▸ Combine files…). pub fn combine_dialog(&mut self) { self.pick_files(FilePurpose::Combine, true); @@ -245,7 +245,7 @@ impl PrintCraftApp { if opts.delete { // Back on the original document. self.active = Some(i); - self.apply_edit(printcraft_engine::Edit::DeletePages { pages }); + self.apply_edit(pdfcraft_engine::Edit::DeletePages { pages }); } } @@ -290,12 +290,12 @@ impl PrintCraftApp { 2 => (d.from.max(1) - 1..d.to.min(n)).collect(), _ => (0..n).collect(), }; - let pages = printcraft_engine::filter_pages(&doc.info, &base, d.parity, d.orientation); + let pages = pdfcraft_engine::filter_pages(&doc.info, &base, d.parity, d.orientation); if pages.is_empty() { self.notify("No pages match those choices"); return; } - self.apply_edit(printcraft_engine::Edit::RotatePages { pages, degrees: d.degrees }); + self.apply_edit(pdfcraft_engine::Edit::RotatePages { pages, degrees: d.degrees }); } /// Split the active document and write the parts: into a chosen folder (desktop) or as @@ -364,7 +364,7 @@ pub(crate) fn strip_pdf(name: &str) -> &str { name.strip_suffix(".pdf").or_else(|| name.strip_suffix(".PDF")).unwrap_or(name) } -impl PrintCraftApp { +impl PdfCraftApp { /// Comments ▸ Import comments / Prepare a form ▸ Import data: XFDF, FDF, XML, CSV or text. pub fn import_data_dialog(&mut self) { #[cfg(not(target_arch = "wasm32"))] @@ -381,7 +381,7 @@ impl PrintCraftApp { match std::fs::read(&path) { Ok(bytes) => { let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); - self.apply_edit(printcraft_engine::Edit::ImportData { name, bytes: std::sync::Arc::new(bytes) }); + self.apply_edit(pdfcraft_engine::Edit::ImportData { name, bytes: std::sync::Arc::new(bytes) }); } Err(e) => self.notify(format!("Couldn't read {}: {e}", path.display())), } @@ -392,7 +392,7 @@ impl PrintCraftApp { /// Export all comments / form data: the format follows the file name's extension. /// Export a PDF ▸ Word, HTML or RTF: ask where (`save_override` in tests), then write. - pub fn export_office_dialog(&mut self, format: printcraft_engine::compare::OfficeFormat) { + pub fn export_office_dialog(&mut self, format: pdfcraft_engine::compare::OfficeFormat) { let Some((_, id)) = self.active_ids() else { return }; let Some(doc) = self.session.get(id) else { return }; let stem = doc.name.trim_end_matches(".pdf").trim_end_matches(".PDF").to_string(); @@ -448,7 +448,7 @@ impl PrintCraftApp { /// Merge the given data files and save the spreadsheet (asks where; `save_override` in tests). pub fn merge_data_files(&mut self, files: Vec<(String, Vec)>) { - let csv = match printcraft_engine::merge_data_files(&files) { + let csv = match pdfcraft_engine::merge_data_files(&files) { Ok(c) => c, Err(e) => return self.notify(e), }; @@ -494,7 +494,7 @@ impl PrintCraftApp { }; let Some(path) = path else { return }; let ext = path.extension().map(|e| e.to_string_lossy().into_owned()).unwrap_or_default(); - let format = printcraft_engine::DataFormat::from_extension(&ext).unwrap_or(printcraft_engine::DataFormat::Xfdf); + let format = pdfcraft_engine::DataFormat::from_extension(&ext).unwrap_or(pdfcraft_engine::DataFormat::Xfdf); match self.session.export_data(id, format, comments, fields) { Ok(bytes) => match crate::editing::write_atomically(&path.to_string_lossy(), &bytes) { Ok(()) => self.notify(format!("Exported to {}", path.display())), @@ -504,7 +504,7 @@ impl PrintCraftApp { } } #[cfg(target_arch = "wasm32")] - match self.session.export_data(id, printcraft_engine::DataFormat::Xfdf, comments, fields) { + match self.session.export_data(id, pdfcraft_engine::DataFormat::Xfdf, comments, fields) { Ok(bytes) => { let _ = crate::editing::download(&format!("{stem}.xfdf"), &bytes); } diff --git a/crates/ui-egui/src/fill_sign.rs b/crates/ui-egui/src/fill_sign.rs index 74c0a1a2..d569bc7f 100644 --- a/crates/ui-egui/src/fill_sign.rs +++ b/crates/ui-egui/src/fill_sign.rs @@ -1,11 +1,11 @@ //! Fill & Sign (Acrobat's Fill & Sign tool, execution plan M5.7): type text onto the page, place //! ✓ ✕ ● ─ marks and today's date, and sign with a drawn signature. Everything is an annotation -//! (typewriter text, PrintCraft-drawn stamps, ink), so it can be moved, deleted and undone like +//! (typewriter text, PdfCraft-drawn stamps, ink), so it can be moved, deleted and undone like //! any comment. use egui::{Color32, CornerRadius, Pos2, Sense, Stroke, pos2, vec2}; -use printcraft_engine::{Edit, FillMark, NewAnnotation, Shape, Style}; -use printcraft_render::DocInfo; +use pdfcraft_engine::{Edit, FillMark, NewAnnotation, Shape, Style}; +use pdfcraft_render::DocInfo; use crate::canvas::{DocView, PageXform}; use crate::theme::Tokens; @@ -161,7 +161,7 @@ pub fn signature_at(page: usize, at: [f64; 2], strokes: &[Vec<[f32; 2]>], author /// Place typed text in the script font with its left edge at `at`, `height` points tall. pub fn typed_signature_at(page: usize, at: [f64; 2], text: &str, height: f64, author: &str) -> Option { - printcraft_engine::typed_signature_shape(at, text, height).map(|shape| new(page, shape, String::new(), author)) + pdfcraft_engine::typed_signature_shape(at, text, height).map(|shape| new(page, shape, String::new(), author)) } /// Place a saved signature or initials. @@ -174,7 +174,7 @@ pub fn place(page: usize, at: [f64; 2], sig: &SavedSig, initials: bool, author: /// The text in the script font as a picture (`w`×`h` px, black on transparent), for previews. pub(crate) fn script_preview(text: &str, w: usize, h: usize) -> egui::ColorImage { - let o = printcraft_engine::script_outline(text); + let o = pdfcraft_engine::script_outline(text); let mut img = egui::ColorImage::filled([w, h], Color32::TRANSPARENT); let span = (o.ascent - o.descent).max(0.1); if o.contours.is_empty() { diff --git a/crates/ui-egui/src/forms_ui.rs b/crates/ui-egui/src/forms_ui.rs index 5ad40d1f..e33e6029 100644 --- a/crates/ui-egui/src/forms_ui.rs +++ b/crates/ui-egui/src/forms_ui.rs @@ -5,8 +5,8 @@ //! and list boxes open a list of their options. Every change is one undoable engine edit. use egui::{Color32, CornerRadius, Rect, Stroke, vec2}; -use printcraft_engine::{Edit, FieldValue, FormField, FormFieldKind, field_flags}; -use printcraft_render::DocInfo; +use pdfcraft_engine::{Edit, FieldValue, FormField, FormFieldKind, field_flags}; +use pdfcraft_render::DocInfo; use crate::canvas::{DocView, PageXform}; use crate::theme::Tokens; @@ -37,7 +37,7 @@ pub struct FormView { /// A message for the app to show (e.g. "buttons run JavaScript"). pub notice: Option, /// A push button was clicked: (its field name, what it does). - pub button: Option<(String, printcraft_engine::form_scripts::ButtonAction)>, + pub button: Option<(String, pdfcraft_engine::form_scripts::ButtonAction)>, } fn widget_rect(xf: &PageXform, info: &DocInfo, page: usize, r: [f64; 4]) -> Rect { @@ -140,7 +140,7 @@ const MONTHS: [&str; 12] = ["January", "February", "March", "April", "May", "Jun /// The date picker under a focused date field. Returns the picked date, formatted with the /// field's own format. fn calendar(ctx: &egui::Context, view: &mut DocView, field: egui::Rect, fmt: &str, today: (i64, u32, u32)) -> Option { - use printcraft_engine::form_scripts::{DateTime, format_date, parse_date}; + use pdfcraft_engine::form_scripts::{DateTime, format_date, parse_date}; let fx = view.forms.focus.as_mut()?; let (mut y, mut m) = fx.calendar.unwrap_or_else(|| match parse_date(&fx.text, fmt) { Some(d) => (d.y, d.m), @@ -290,7 +290,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f } }); // Date fields: a calendar under the field (Acrobat's date picker). - if let printcraft_engine::form_scripts::Format::Date(fmt) = &f.actions.format + if let pdfcraft_engine::form_scripts::Format::Date(fmt) = &f.actions.format && let Some(picked) = calendar(ctx, view, rect, fmt, today) { if let Some(fx) = view.forms.focus.as_mut() { diff --git a/crates/ui-egui/src/home.rs b/crates/ui-egui/src/home.rs index 1335e198..c04139e5 100644 --- a/crates/ui-egui/src/home.rs +++ b/crates/ui-egui/src/home.rs @@ -1,18 +1,18 @@ //! Home tab: recommended tools, open card, recent files (local only, never another app's list). use egui::{Align2, CornerRadius, Rect, Sense, Stroke, vec2}; -use printcraft_engine::catalog; +use pdfcraft_engine::catalog; use crate::theme::{self, Tokens}; -use crate::{LeftPanel, PrintCraftApp, icons, panels::human_size, widgets}; +use crate::{LeftPanel, PdfCraftApp, icons, panels::human_size, widgets}; const RECOMMENDED: [&str; 5] = ["organize", "comment", "form", "edit", "protect"]; -pub fn show(app: &mut PrintCraftApp, ui: &mut egui::Ui) { +pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { egui::Frame::NONE.inner_margin(egui::Margin { left: 36, right: 36, top: 28, bottom: 28 }).show(ui, |ui| { - ui.label(egui::RichText::new("Welcome to PrintCraft").font(theme::semibold(24.0))); + ui.label(egui::RichText::new("Welcome to PdfCraft").font(theme::semibold(24.0))); ui.label( egui::RichText::new("An open-source PDF workbench — local, private, and scriptable.").color(t.text_muted).font(theme::regular(14.0)), ); @@ -98,7 +98,7 @@ pub fn show(app: &mut PrintCraftApp, ui: &mut egui::Ui) { ui.label(egui::RichText::new("Recent").font(theme::semibold(17.0))); ui.add_space(8.0); if app.recent.is_empty() { - ui.label(egui::RichText::new("Files you open in PrintCraft appear here. Drop a PDF anywhere to open it.").color(t.text_muted)); + ui.label(egui::RichText::new("Files you open in PdfCraft appear here. Drop a PDF anywhere to open it.").color(t.text_muted)); } let mut open = None; for r in &app.recent { @@ -138,7 +138,7 @@ pub fn show(app: &mut PrintCraftApp, ui: &mut egui::Ui) { ui.add_space(20.0); widgets::section_title(ui, "Privacy"); ui.label( - egui::RichText::new("PrintCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider.") + egui::RichText::new("PdfCraft works offline. No telemetry, no account, and no cloud processing unless you add a provider.") .color(t.text_muted), ); }); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 56fc4bb1..0038e84b 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -107,17 +107,17 @@ mod tests { #[test] fn language_persists_and_invalid_input_keeps_current_language() { - let mut app = crate::PrintCraftApp::default(); + let mut app = crate::PdfCraftApp::default(); app.set_option("language", "ja").unwrap(); assert_eq!(app.language, Language::Ja); assert!(app.set_option("language", "xx").is_err()); assert_eq!(app.language, Language::Ja); - let mut restored = crate::PrintCraftApp::default(); + let mut restored = crate::PdfCraftApp::default(); restored.restore(&app.persist()); assert_eq!(restored.language, Language::Ja); restored.restore(r#"{"language":"xx"}"#); assert_eq!(restored.language, Language::Ja); - let mut legacy = crate::PrintCraftApp::default(); + let mut legacy = crate::PdfCraftApp::default(); legacy.restore("{}"); assert_eq!(legacy.language, Language::En); } diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 32c7bdb4..c3058b68 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -61,7 +61,7 @@ pub fn button(ui: &mut egui::Ui, name: &str, box_size: f32, selected: bool, tool mod tests { #[test] fn every_catalog_icon_exists() { - for g in printcraft_engine::catalog::TOOL_GROUPS { + for g in pdfcraft_engine::catalog::TOOL_GROUPS { assert!(super::exists(g.icon), "missing icon {}", g.icon); for s in g.sections { for i in s.items { diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index 719df3e4..1e0f1e27 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -2,11 +2,11 @@ //! the JavaScript console (⌘J), Document JavaScripts, and Preferences ▸ JavaScript. use egui::{Align, Layout}; -use printcraft_engine::js::{JsOutput, Request}; -use printcraft_engine::{DocId, Edit}; +use pdfcraft_engine::js::{JsOutput, Request}; +use pdfcraft_engine::{DocId, Edit}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; /// The JavaScript console: the input and the output so far. #[derive(Clone, Debug, Default, PartialEq)] @@ -22,7 +22,7 @@ pub struct DocJsDraft { pub script: String, } -impl PrintCraftApp { +impl PdfCraftApp { /// Act on what scripts produced in document `id`: alerts are shown, console output goes to /// the console, and print / page / link requests are carried out (form submissions are /// reported, never sent). @@ -49,7 +49,7 @@ impl PrintCraftApp { } } Request::Submit(u) => self.notify(format!( - "The form asks to be submitted to {u}; PrintCraft doesn't send form data. Save the document to keep your entries." + "The form asks to be submitted to {u}; PdfCraft doesn't send form data. Save the document to keep your entries." )), Request::Focus(_) | Request::Beep | Request::Reset(_) => {} } @@ -135,7 +135,7 @@ fn buttons(ui: &mut egui::Ui, primary: &str, others: &[&str]) -> Option } /// The JavaScript console. Returns `true` to close. -pub(crate) fn console_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool { +pub(crate) fn console_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { ui.label(egui::RichText::new("JavaScript Console").font(theme::semibold(18.0))); ui.add_space(6.0); if !app.session.javascript() { @@ -176,7 +176,7 @@ pub(crate) fn console_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Token } /// Document JavaScripts: list, edit, add and delete. Returns `true` to close. -pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool { +pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { ui.label(egui::RichText::new("Document JavaScripts").font(theme::semibold(18.0))); ui.add_space(6.0); let scripts = app.active_ids().and_then(|(_, id)| app.session.get(id)).map(|d| d.document_scripts()).unwrap_or_default(); @@ -224,7 +224,7 @@ pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &T } /// Preferences: interface language, identity and JavaScript. Returns `true` to close. -pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool { +pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { ui.label(egui::RichText::new(app.language.tr("Preferences")).font(theme::semibold(18.0))); ui.horizontal(|ui| { ui.label(app.language.tr("Interface language")); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index a1e3419a..3d8c14de 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1,9 +1,9 @@ -//! printcraft-ui-egui — the first PrintCraft shell (L7). +//! pdfcraft-ui-egui — the first PdfCraft shell (L7). //! //! Layout grammar follows plan/acrobat/02-ui-ux.md §1: tab strip, mode bar, left tool panel, //! floating quick-action bar, document area, right panel + right rail with page navigation. //! Everything here is presentation: documents, rendering and the tool catalogue live in -//! `printcraft-engine`. +//! `pdfcraft-engine`. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -67,7 +67,7 @@ pub mod theme; pub mod updates; mod widgets; -use printcraft_engine::{DocId, Session}; +use pdfcraft_engine::{DocId, Session}; pub use canvas::DocView; pub use editing::{CloseRequest, SaveTarget}; @@ -130,7 +130,7 @@ pub enum QuickTool { /// Edit a PDF ▸ Edit text: click a line of existing text to edit it. EditText, /// Add a stamp: click to place this stamp. - Stamp(printcraft_engine::StampKind), + Stamp(pdfcraft_engine::StampKind), /// A custom stamp from the library (its index). CustomStamp(usize), /// Edit a PDF ▸ Link: draw link areas, select and edit links. @@ -158,7 +158,7 @@ pub enum Dialog { /// Set Page Boxes (crop, trim, bleed, art, media). PageBoxes, /// Add / Update Header and Footer, Watermark, Background. - Marks(printcraft_engine::MarkKind), + Marks(pdfcraft_engine::MarkKind), /// Export a PDF ▸ Image / Text. Export(export_ui::ExportKind), /// Fill & Sign ▸ Create signature (the drawing pad). @@ -282,7 +282,7 @@ pub struct RecentFile { pub size: usize, } -pub struct PrintCraftApp { +pub struct PdfCraftApp { pub session: Session, pub views: Vec, /// `None` shows the Home tab. @@ -322,7 +322,7 @@ pub struct PrintCraftApp { /// Document Properties ▸ Description fields being edited: (document, Title/Author/Subject/Keywords). pub props_draft: Option<(DocId, [String; 4])>, /// Document Properties ▸ Initial View (and reading options) being edited. - pub view_draft: Option<(DocId, printcraft_engine::InitialView)>, + pub view_draft: Option<(DocId, pdfcraft_engine::InitialView)>, /// Files picked asynchronously for combine / insert (web). pub requests: files::Requests, /// Write exported files (split) here instead of asking (tests and automation). @@ -332,7 +332,7 @@ pub struct PrintCraftApp { pub extract_draft: ExtractDraft, pub rotate_draft: RotateDraft, /// Summarize Comments: sort order. - pub summary_sort: printcraft_engine::SummarySort, + pub summary_sort: pdfcraft_engine::SummarySort, /// The signing dialogs' state. pub sign_draft: Option, /// Digital ID files the user has created or added. @@ -349,7 +349,7 @@ pub struct PrintCraftApp { pub run_inline: bool, /// Action Wizard: the user's actions, the dialog state, the running action and (tests) the /// files to use instead of a picker. - pub custom_actions: Vec, + pub custom_actions: Vec, pub wizard: actions_ui::Wizard, pub action_run: Option>>, pub action_files_override: Option>, @@ -362,13 +362,13 @@ pub struct PrintCraftApp { pub js_console: js_ui::JsConsole, pub doc_js: js_ui::DocJsDraft, pub a11y: a11y_ui::A11yState, - pub a11y_skipped: std::collections::BTreeSet, + pub a11y_skipped: std::collections::BTreeSet, pub alt_draft: a11y_ui::AltDraft, /// List the macOS Keychain's signing identities among the digital IDs (the desktop app). pub keychain_ids: bool, pub cert_viewer: Option, /// The last space audit. - pub space_audit: Vec, + pub space_audit: Vec, /// Combine files: the files staged so far. pub combine_draft: Vec, /// The custom stamp library, and the stamp being created. @@ -435,7 +435,7 @@ pub struct PrintCraftApp { pub print_draft: PrintDraft, pub link_draft: Option, /// The style new text gets (Edit a PDF ▸ Format text). - pub text_style: printcraft_engine::AddedText, + pub text_style: pdfcraft_engine::AddedText, /// The Replace Pages dialog's state. pub replace_draft: Option, /// The last web link the app asked the system to open (tests and automation). @@ -447,18 +447,18 @@ pub struct PrintCraftApp { pub struct NumberDraft { pub from: usize, pub to: usize, - pub style: printcraft_engine::LabelStyle, + pub style: pdfcraft_engine::LabelStyle, pub prefix: String, pub start: u32, } -impl Default for PrintCraftApp { +impl Default for PdfCraftApp { fn default() -> Self { Self::new() } } -impl PrintCraftApp { +impl PdfCraftApp { pub fn new() -> Self { Self { session: Session::new(), @@ -562,7 +562,7 @@ impl PrintCraftApp { link_draft: None, text_style: content_ui::default_style(), replace_draft: None, - number_draft: NumberDraft { from: 1, to: 1, style: printcraft_engine::LabelStyle::Decimal, prefix: String::new(), start: 1 }, + number_draft: NumberDraft { from: 1, to: 1, style: pdfcraft_engine::LabelStyle::Decimal, prefix: String::new(), start: 1 }, } } @@ -574,8 +574,8 @@ impl PrintCraftApp { /// Open a document the way it asks to be opened: navigation panel, layout, magnification, /// page (Document Properties ▸ Initial View). - fn apply_initial_view(&mut self, index: usize, v: &printcraft_engine::InitialView) { - use printcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N}; + fn apply_initial_view(&mut self, index: usize, v: &pdfcraft_engine::InitialView) { + use pdfcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N}; let pages = self.session.get(self.views[index].id).map_or(0, |d| d.info.pages.len()); match v.navigation { N::PageOnly => {} @@ -618,7 +618,7 @@ impl PrintCraftApp { } fn try_open(&mut self, name: &str, path: Option, bytes: std::sync::Arc>, password: Option<&str>) -> Result<(), String> { - use printcraft_render::OpenError; + use pdfcraft_render::OpenError; let size = bytes.len(); let id = match self.session.open(name, path.clone(), bytes.clone(), password) { Ok(id) => id, @@ -692,7 +692,7 @@ impl PrintCraftApp { pub fn attachment_action(&mut self, doc: DocId, index: usize, open: bool) { let Some(d) = self.session.get(doc) else { return }; let Some(att) = d.info.attachments.get(index).cloned() else { return }; - let data = printcraft_render::attachment_data(&d.bytes, d.password.as_deref(), &att); + let data = pdfcraft_render::attachment_data(&d.bytes, d.password.as_deref(), &att); match (data, open) { (Err(e), _) => self.notify(format!("Couldn't read {}: {e}", att.name)), (Ok(bytes), true) => { @@ -818,19 +818,19 @@ impl PrintCraftApp { /// Run a catalogue command. Commands that aren't implemented yet say which milestone ships them. pub fn run_command(&mut self, command: &str) { - if printcraft_engine::commands::command(command).is_some() { + if pdfcraft_engine::commands::command(command).is_some() { self.execute(command); return; } // Not implemented yet: say which milestone ships it. - let when = printcraft_engine::catalog::TOOL_GROUPS + let when = pdfcraft_engine::catalog::TOOL_GROUPS .iter() .flat_map(|g| g.sections.iter().flat_map(|s| s.items.iter())) .find(|i| i.command == command) .map(|i| match i.availability { - printcraft_engine::catalog::Availability::Planned(m) => format!("ships in milestone {m}"), - printcraft_engine::catalog::Availability::Provider => "needs an AI provider (off by default)".to_string(), - printcraft_engine::catalog::Availability::Ready => "is available".to_string(), + pdfcraft_engine::catalog::Availability::Planned(m) => format!("ships in milestone {m}"), + pdfcraft_engine::catalog::Availability::Provider => "needs an AI provider (off by default)".to_string(), + pdfcraft_engine::catalog::Availability::Ready => "is available".to_string(), }) .unwrap_or_else(|| "is not available yet".into()); self.notify(format!("`{command}` {when}")); @@ -838,7 +838,7 @@ impl PrintCraftApp { /// Serialize the user's persistent state (recent files, theme). Local only. pub fn persist(&self) -> String { - let trusted: Vec = self.session.trusted_certificates().iter().map(printcraft_engine::sign::x509::to_pem).collect(); + let trusted: Vec = self.session.trusted_certificates().iter().map(pdfcraft_engine::sign::x509::to_pem).collect(); serde_json::json!({ "recent": self.recent, "theme": self.theme, @@ -898,7 +898,7 @@ impl PrintCraftApp { self.session.set_javascript(on); } if let Ok(pems) = serde_json::from_value::>(v["trusted"].clone()) { - let certs = pems.iter().filter_map(|p| printcraft_engine::sign::x509::load_certificates(p.as_bytes()).ok()).flatten().collect(); + let certs = pems.iter().filter_map(|p| pdfcraft_engine::sign::x509::load_certificates(p.as_bytes()).ok()).flatten().collect(); self.session.set_trusted_certificates(certs); } } @@ -947,7 +947,7 @@ impl PrintCraftApp { } } ("tool", _) => { - let g = printcraft_engine::catalog::group(value).ok_or_else(|| format!("unknown tool {value}"))?; + let g = pdfcraft_engine::catalog::group(value).ok_or_else(|| format!("unknown tool {value}"))?; self.left = LeftPanel::Tool(g.id); self.left_open = true; } @@ -963,9 +963,9 @@ impl PrintCraftApp { "split" => Some(Dialog::Split), "protect" => Some(Dialog::Protect), "page-boxes" => Some(Dialog::PageBoxes), - "header-footer" => Some(Dialog::Marks(printcraft_engine::MarkKind::HeaderFooter)), - "watermark" => Some(Dialog::Marks(printcraft_engine::MarkKind::Watermark)), - "background" => Some(Dialog::Marks(printcraft_engine::MarkKind::Background)), + "header-footer" => Some(Dialog::Marks(pdfcraft_engine::MarkKind::HeaderFooter)), + "watermark" => Some(Dialog::Marks(pdfcraft_engine::MarkKind::Watermark)), + "background" => Some(Dialog::Marks(pdfcraft_engine::MarkKind::Background)), "export-image" => Some(Dialog::Export(export_ui::ExportKind::Image)), "export-text" => Some(Dialog::Export(export_ui::ExportKind::Text)), "export-all-images" => Some(Dialog::Export(export_ui::ExportKind::AllImages)), @@ -1065,7 +1065,7 @@ impl PrintCraftApp { QuickTool::CustomStamp(i) } stamp if stamp.starts_with("stamp-") => QuickTool::Stamp( - printcraft_engine::StampKind::ALL + pdfcraft_engine::StampKind::ALL .into_iter() .find(|k| k.name().trim_start_matches("PC").eq_ignore_ascii_case(&stamp[6..])) .ok_or_else(|| format!("unknown stamp {stamp}"))?, @@ -1121,9 +1121,9 @@ impl PrintCraftApp { } } -impl eframe::App for PrintCraftApp { +impl eframe::App for PdfCraftApp { fn save(&mut self, storage: &mut dyn eframe::Storage) { - storage.set_string("printcraft", self.persist()); + storage.set_string("pdfcraft", self.persist()); } fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { @@ -1201,7 +1201,7 @@ impl eframe::App for PrintCraftApp { let title = self .active .and_then(|i| self.session.get(self.views[i].id)) - .map_or_else(|| "PrintCraft".to_owned(), |d| format!("{} — PrintCraft", d.display_name())); + .map_or_else(|| "PdfCraft".to_owned(), |d| format!("{} — PdfCraft", d.display_name())); if title != self.window_title { ctx.send_viewport_cmd(egui::ViewportCommand::Title(title.clone())); self.window_title = title; diff --git a/crates/ui-egui/src/link_ui.rs b/crates/ui-egui/src/link_ui.rs index d91d2884..07cd81e3 100644 --- a/crates/ui-egui/src/link_ui.rs +++ b/crates/ui-egui/src/link_ui.rs @@ -3,12 +3,12 @@ //! their outlines, a click selects one, a double-click edits it and Delete removes it. use egui::{Color32, CornerRadius, Pos2, Rect, Stroke}; -use printcraft_engine::{Edit, LinkAction, LinkHighlight, LinkItem, LinkStyle}; -use printcraft_render::DocInfo; +use pdfcraft_engine::{Edit, LinkAction, LinkHighlight, LinkItem, LinkStyle}; +use pdfcraft_render::DocInfo; use crate::canvas::{DocView, PageXform}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; const LINK_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6); @@ -132,7 +132,7 @@ pub(crate) fn keys(ctx: &egui::Context, view: &mut DocView) { } } -impl PrintCraftApp { +impl PdfCraftApp { /// Open Link Properties for a new link area or an existing link. pub(crate) fn open_link_props(&mut self, page: usize, rect: Option<[f64; 4]>, index: Option) { let Some((_, id)) = self.active_ids() else { return }; diff --git a/crates/ui-egui/src/marks_ui.rs b/crates/ui-egui/src/marks_ui.rs index 850ad26a..a4c602e6 100644 --- a/crates/ui-egui/src/marks_ui.rs +++ b/crates/ui-egui/src/marks_ui.rs @@ -3,10 +3,10 @@ //! preview of the current page. "Update" opens the same dialog in replace mode. use egui::{Align, Color32, CornerRadius, Layout, Rect, Stroke, pos2, vec2}; -use printcraft_engine::{Background, Edit, HeaderFooter, MarkKind, Watermark}; +use pdfcraft_engine::{Background, Edit, HeaderFooter, MarkKind, Watermark}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Subset { @@ -138,7 +138,7 @@ fn rgb32(c: [f64; 3]) -> Color32 { } /// The dialog for `kind`; returns (apply, cancel). -pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens, kind: MarkKind) -> (bool, bool) { +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: MarkKind) -> (bool, bool) { let Some((i, id)) = app.active_ids() else { return (false, true) }; let current = app.views[i].current; let Some(doc) = app.session.get(id) else { return (false, true) }; @@ -392,9 +392,9 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens, kind: (apply, cancel) } -fn mark_file(d: &MarksDraft) -> Option { +fn mark_file(d: &MarksDraft) -> Option { let (name, bytes) = d.file.clone().filter(|_| d.use_file)?; - Some(printcraft_engine::MarkFile { name, bytes, page: d.file_page.max(1) - 1 }) + Some(pdfcraft_engine::MarkFile { name, bytes, page: d.file_page.max(1) - 1 }) } /// Source ▸ File: Browse…, the page of a PDF, and the size relative to the page. diff --git a/crates/ui-egui/src/ocr_ui.rs b/crates/ui-egui/src/ocr_ui.rs index 0e0286a4..e85749f8 100644 --- a/crates/ui-egui/src/ocr_ui.rs +++ b/crates/ui-egui/src/ocr_ui.rs @@ -5,11 +5,11 @@ use std::sync::{Arc, Mutex}; use egui::{Align, Layout}; -use printcraft_engine::DocId; -use printcraft_engine::ocr::{LANGUAGES, OcrPage, OcrSettings}; +use pdfcraft_engine::DocId; +use pdfcraft_engine::ocr::{LANGUAGES, OcrPage, OcrSettings}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; /// Which pages to read. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -58,9 +58,9 @@ pub struct OcrRun { pub progress: Arc>, } -pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (bool, bool) { +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) { let pages = app.active_ids().and_then(|(_, id)| app.session.get(id)).map_or(1, |d| d.info.pages.len().max(1)); - let available = printcraft_engine::ocr::available(); + let available = pdfcraft_engine::ocr::available(); let d = &mut app.ocr_draft; d.to = d.to.clamp(1, pages); d.from = d.from.clamp(1, d.to); @@ -113,7 +113,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (b }); if !available { ui.label( - egui::RichText::new("Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PRINTCRAFT_MODELS).") + egui::RichText::new("Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS).") .small() .color(t.text_muted), ); @@ -134,7 +134,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (b (go, cancel) } -impl PrintCraftApp { +impl PdfCraftApp { /// Recognize text on the pages chosen in the dialog, in the background. pub fn start_ocr(&mut self) { let Some((vi, id)) = self.active_ids() else { return }; @@ -157,7 +157,7 @@ impl PrintCraftApp { let progress = Arc::new(Mutex::new(OcrProgress { total: job.pages.len(), ..Default::default() })); let p = progress.clone(); let work = move || { - let result = printcraft_engine::ocr::engine().map(|ocr| { + let result = pdfcraft_engine::ocr::engine().map(|ocr| { job.run(&ocr, |done, total| { let Ok(mut s) = p.lock() else { return false }; s.done = done; @@ -173,7 +173,7 @@ impl PrintCraftApp { if self.run_inline { work(); } else { - std::thread::Builder::new().name("printcraft-ocr".into()).spawn(work).ok(); + std::thread::Builder::new().name("pdfcraft-ocr".into()).spawn(work).ok(); } #[cfg(target_arch = "wasm32")] work(); @@ -200,14 +200,14 @@ impl PrintCraftApp { let p = progress.clone(); let work = move || { let (mut ok, mut words, mut failed) = (0, 0, Vec::new()); - match printcraft_engine::ocr::engine() { + match pdfcraft_engine::ocr::engine() { Err(e) => failed.push(e), Ok(ocr) => { for (i, (name, bytes)) in files.into_iter().enumerate() { if let Ok(mut s) = p.lock() { s.done = i; } - let r = printcraft_engine::ocr::recognize_file(&name, Arc::new(bytes), None, settings.clone(), &ocr, |_, _| true); + let r = pdfcraft_engine::ocr::recognize_file(&name, Arc::new(bytes), None, settings.clone(), &ocr, |_, _| true); let saved = r.and_then(|r| { #[cfg(not(target_arch = "wasm32"))] crate::editing::write_atomically(&dir.join(&name).to_string_lossy(), &r.bytes).map_err(|e| e.to_string())?; @@ -238,7 +238,7 @@ impl PrintCraftApp { if self.run_inline { work(); } else { - std::thread::Builder::new().name("printcraft-ocr-files".into()).spawn(work).ok(); + std::thread::Builder::new().name("pdfcraft-ocr-files".into()).spawn(work).ok(); } #[cfg(target_arch = "wasm32")] work(); diff --git a/crates/ui-egui/src/optimize_ui.rs b/crates/ui-egui/src/optimize_ui.rs index ffbf2537..339fbaae 100644 --- a/crates/ui-egui/src/optimize_ui.rs +++ b/crates/ui-egui/src/optimize_ui.rs @@ -2,11 +2,11 @@ //! Discard User Data and Clean Up panels. The result is saved as a copy, like Reduce File Size. use egui::{Align, Layout}; -use printcraft_engine::Hidden; -use printcraft_engine::optimize::{Compression, ImageSettings, QUALITIES, Settings}; +use pdfcraft_engine::Hidden; +use pdfcraft_engine::optimize::{Compression, ImageSettings, QUALITIES, Settings}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OptimizeTab { @@ -154,7 +154,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo } /// Audit Space Usage: bytes and share of the file per kind of content. -pub(crate) fn audit_body(ui: &mut egui::Ui, rows: &[printcraft_engine::optimize::SpaceUse], t: &Tokens) -> bool { +pub(crate) fn audit_body(ui: &mut egui::Ui, rows: &[pdfcraft_engine::optimize::SpaceUse], t: &Tokens) -> bool { ui.label(egui::RichText::new("Space Audit").font(crate::theme::semibold(18.0))); ui.add_space(8.0); egui::Grid::new("space-audit").num_columns(3).striped(true).spacing([24.0, 4.0]).show(ui, |ui| { @@ -180,7 +180,7 @@ pub(crate) fn audit_body(ui: &mut egui::Ui, rows: &[printcraft_engine::optimize: ok } -impl PrintCraftApp { +impl PdfCraftApp { /// Optimize PDF with the dialog's choices and save the copy. pub fn optimize_with_draft(&mut self) { let Some((_, id)) = self.active_ids() else { return }; diff --git a/crates/ui-egui/src/pageboxes.rs b/crates/ui-egui/src/pageboxes.rs index b2800282..05c7d9a2 100644 --- a/crates/ui-egui/src/pageboxes.rs +++ b/crates/ui-egui/src/pageboxes.rs @@ -4,10 +4,10 @@ //! millimetres, and the pages. A preview shows the current page's media box and the new box. use egui::{Align, Color32, CornerRadius, Layout, Rect, Stroke, pos2, vec2}; -use printcraft_engine::{BoxSpec, Edit, PageBox}; +use pdfcraft_engine::{BoxSpec, Edit, PageBox}; use crate::theme::{self, Tokens}; -use crate::{PrintCraftApp, widgets}; +use crate::{PdfCraftApp, widgets}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Unit { @@ -87,7 +87,7 @@ fn box_index(b: PageBox) -> usize { } /// Draw the dialog body; returns (apply, cancel). -pub(crate) fn body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> (bool, bool) { +pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (bool, bool) { let Some((i, id)) = app.active_ids() else { return (false, true) }; let current = app.views[i].current; let Some(doc) = app.session.get(id) else { return (false, true) }; diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index f307f54d..5f1a3877 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -2,10 +2,10 @@ //! catalogue, with a fuzzy-ish substring match. use egui::{Align2, CornerRadius, Rect, Sense, Stroke, vec2}; -use printcraft_engine::catalog::{Availability, TOOL_GROUPS}; +use pdfcraft_engine::catalog::{Availability, TOOL_GROUPS}; use crate::theme::{self, Tokens}; -use crate::{LeftPanel, PrintCraftApp, icons}; +use crate::{LeftPanel, PdfCraftApp, icons}; struct Hit { /// The tool panel to open (tools and catalogue items); `None` for plain commands. @@ -30,7 +30,7 @@ fn score(hay: &str, needle: &str) -> Option { needle.chars().all(|c| it.any(|x| x == c)).then_some(100) } -pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { +pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) { if !app.palette_open { return; } @@ -43,8 +43,8 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { let mut hits: Vec<(usize, Hit)> = Vec::new(); let mac = cfg!(target_os = "macos") || cfg!(target_arch = "wasm32"); let active = app.active_ids().map(|(_, id)| id); - for spec in printcraft_engine::commands::COMMANDS { - let label = printcraft_engine::commands::current_label(spec, &app.session, active); + for spec in pdfcraft_engine::commands::COMMANDS { + let label = pdfcraft_engine::commands::current_label(spec, &app.session, active); if let Some(s) = score(&label, &q).or_else(|| score(spec.id, &q).map(|s| s + 50)) { hits.push(( s, @@ -75,7 +75,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { } for sec in g.sections { for i in sec.items { - if printcraft_engine::commands::command(i.command).is_some() { + if pdfcraft_engine::commands::command(i.command).is_some() { continue; // listed above as a command } if let Some(s) = score(i.label, &q).or_else(|| score(i.command, &q).map(|s| s + 50)) { diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index ba6fab45..8f05b413 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -2,11 +2,11 @@ //! right-hand panels (Comments, Bookmarks, Pages, Fields, Layers, Attachments). use egui::{Align, Align2, Color32, CornerRadius, Layout, Rect, Sense, Stroke, pos2, vec2}; -use printcraft_engine::catalog::{self, Availability, TOOL_GROUPS, ToolGroup}; -use printcraft_render::{DocInfo, FieldKind, OutlineItem}; +use pdfcraft_engine::catalog::{self, Availability, TOOL_GROUPS, ToolGroup}; +use pdfcraft_render::{DocInfo, FieldKind, OutlineItem}; use crate::theme::{self, Tokens}; -use crate::{LeftPanel, PrintCraftApp, RightPanel, icons, widgets}; +use crate::{LeftPanel, PdfCraftApp, RightPanel, icons, widgets}; const COLLAPSED_TOOLS: usize = 14; @@ -14,7 +14,7 @@ fn hue(g: &ToolGroup) -> Color32 { Color32::from_rgb(g.hue[0], g.hue[1], g.hue[2]) } -pub fn left_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) { +pub fn left_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); egui::Panel::left("tool_panel") .resizable(false) @@ -52,7 +52,7 @@ fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, back: bool) -> (bool (go_back, close) } -fn all_tools(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) { +fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { let (_, close) = panel_header(ui, t, "All tools", false); if close { app.left_open = false; @@ -108,7 +108,7 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response { resp.on_hover_text(tip) } -fn tool_detail(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static ToolGroup) { +fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static ToolGroup) { let (back, close) = panel_header(ui, t, g.label, true); if back { app.left = LeftPanel::AllTools; @@ -210,8 +210,8 @@ fn tool_detail(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'stat /// Add a stamp: Dynamic, Sign Here and Standard Business stamps; click one, then click on the /// page to place it. -fn stamp_palette(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) { - use printcraft_engine::{StampGroup, StampKind}; +fn stamp_palette(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { + use pdfcraft_engine::{StampGroup, StampKind}; ui.label(egui::RichText::new("Choose a stamp, then click on the page to place it.").small().color(t.text_faint)); egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { for (group, title) in @@ -272,7 +272,7 @@ fn stamp_palette(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) { /// Edit a PDF ▸ Format text: for the selected added text (one undoable change), or the style /// new text gets. -fn format_section(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) { +fn format_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { // Editing a paragraph of existing text: its formatting, applied as it changes. if let Some((i, _)) = app.active_ids() && let Some(ed) = app.views[i].line_editor.clone() @@ -285,7 +285,7 @@ fn format_section(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) { } changed |= crate::edit_text_ui::extras_panel(ui, &mut ed.extras); if changed { - let edit = printcraft_engine::Edit::EditTextBlock { page: ed.page, block: ed.block, text: ed.text.clone(), style: ed.style() }; + let edit = pdfcraft_engine::Edit::EditTextBlock { page: ed.page, block: ed.block, text: ed.text.clone(), style: ed.style() }; if app.apply_edit(edit) { ed.applied(); if let Some(doc) = app.session.get(app.views[i].id) @@ -304,14 +304,14 @@ fn format_section(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) { let (page, index) = app.views[i].content.selected?; let doc = app.session.get(id)?; match &doc.added.iter().filter(|a| a.page == page).nth(index)?.content { - printcraft_engine::AddedContent::Image(img) => Some((page, index, img.clone())), + pdfcraft_engine::AddedContent::Image(img) => Some((page, index, img.clone())), _ => None, } }); if let Some((page, index, img)) = image { match crate::content_ui::image_panel(ui, t, &img) { Some(crate::content_ui::ImageAction::Update(content)) => { - app.apply_edit(printcraft_engine::Edit::UpdateContent { page, index, content }); + app.apply_edit(pdfcraft_engine::Edit::UpdateContent { page, index, content }); } Some(crate::content_ui::ImageAction::Replace) => app.replace_image_dialog(page, index), None => {} @@ -325,15 +325,15 @@ fn format_section(app: &mut PrintCraftApp, ui: &mut egui::Ui, t: &Tokens) { let doc = app.session.get(id)?; let a = doc.added.iter().filter(|a| a.page == page).nth(index)?; match &a.content { - printcraft_engine::AddedContent::Text(text) => Some((page, index, text.clone())), + pdfcraft_engine::AddedContent::Text(text) => Some((page, index, text.clone())), _ => None, } }); match selected { Some((page, index, text)) => { if let Some(style) = crate::content_ui::format_panel(ui, t, &text) { - app.text_style = printcraft_engine::AddedText { text: String::new(), rect: [0.0; 4], ..style.clone() }; - app.apply_edit(printcraft_engine::Edit::UpdateContent { page, index, content: printcraft_engine::AddedContent::Text(style) }); + app.text_style = pdfcraft_engine::AddedText { text: String::new(), rect: [0.0; 4], ..style.clone() }; + app.apply_edit(pdfcraft_engine::Edit::UpdateContent { page, index, content: pdfcraft_engine::AddedContent::Text(style) }); } } None if app.quick_tool == crate::QuickTool::AddText => { @@ -355,7 +355,7 @@ pub(crate) enum Nav { Flash(usize, [f32; 4]), } -pub fn right_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) { +pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let Some((index, id)) = app.active_ids() else { return }; let Some(panel) = app.right else { return }; @@ -365,7 +365,7 @@ pub fn right_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) { let mut attachment_action: Option<(usize, bool)> = None; // (index, open instead of save) let mut bm_action: Option = None; let mut bm_expand: Option = None; - let mut panel_edit: Option = None; + let mut panel_edit: Option = None; let mut panel_command: Option<&'static str> = None; let mut sig_action: Option = None; let mut a11y_action: Option = None; @@ -519,7 +519,7 @@ pub fn right_panel(app: &mut PrintCraftApp, ui: &mut egui::Ui) { ui.set_width(ui.available_width() - 70.0); ui.add(egui::Label::new(egui::RichText::new(&a.name).font(theme::medium(13.0))).truncate()); let mut meta = a.size.map(human_size).unwrap_or_default(); - if let printcraft_render::AttachmentSource::Annotation { page, .. } = a.source { + if let pdfcraft_render::AttachmentSource::Annotation { page, .. } = a.source { meta = format!("{meta} · on page {}", info.pages.get(page).map(|p| p.label.as_str()).unwrap_or("?")); } if let Some(d) = &a.description { @@ -783,10 +783,10 @@ fn fields( ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, - form: &[printcraft_engine::FormField], + form: &[pdfcraft_engine::FormField], preparing: bool, nav: &mut Option
- https://raw.githubusercontent.com/storytold/printcraft/main/docs/images/printcraft-viewer.png + https://raw.githubusercontent.com/storytold/pdfcraft/main/docs/images/pdfcraft-viewer.png @@ -52,7 +52,7 @@ tablet - printcraft + pdfcraft application/pdf diff --git a/packaging/linux/ai.storyteller.printcraft.mime.xml b/packaging/linux/ai.storyteller.pdfcraft.mime.xml similarity index 100% rename from packaging/linux/ai.storyteller.printcraft.mime.xml rename to packaging/linux/ai.storyteller.pdfcraft.mime.xml diff --git a/packaging/linux/flatpak/ai.storyteller.printcraft.yml b/packaging/linux/flatpak/ai.storyteller.pdfcraft.yml similarity index 67% rename from packaging/linux/flatpak/ai.storyteller.printcraft.yml rename to packaging/linux/flatpak/ai.storyteller.pdfcraft.yml index 49632f61..c2ba3b8c 100644 --- a/packaging/linux/flatpak/ai.storyteller.printcraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.pdfcraft.yml @@ -1,20 +1,20 @@ -# Flatpak manifest for PrintCraft, ready for a Flathub submission. +# Flatpak manifest for PdfCraft, ready for a Flathub submission. # # Flatpak builds run offline, so the crate sources must be vendored first. From the repo root: # # curl -fsSLO https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py # python3 flatpak-cargo-generator.py Cargo.lock -o packaging/linux/flatpak/cargo-sources.json -# flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.printcraft.yml -# flatpak run ai.storyteller.printcraft +# flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.pdfcraft.yml +# flatpak run ai.storyteller.pdfcraft # # For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit. -id: ai.storyteller.printcraft +id: ai.storyteller.pdfcraft runtime: org.freedesktop.Platform runtime-version: '25.08' sdk: org.freedesktop.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.rust-stable -command: printcraft +command: pdfcraft finish-args: - --share=ipc - --socket=wayland @@ -27,23 +27,23 @@ finish-args: build-options: append-path: /usr/lib/sdk/rust-stable/bin modules: - - name: printcraft + - name: pdfcraft buildsystem: simple build-options: env: - CARGO_HOME: /run/build/printcraft/cargo + CARGO_HOME: /run/build/pdfcraft/cargo CARGO_NET_OFFLINE: 'true' build-commands: - - cargo --offline build --release --locked -p printcraft - - install -Dm755 target/release/printcraft /app/bin/printcraft - - install -Dm644 packaging/linux/ai.storyteller.printcraft.desktop /app/share/applications/ai.storyteller.printcraft.desktop - - install -Dm644 packaging/linux/ai.storyteller.printcraft.mime.xml /app/share/mime/packages/ai.storyteller.printcraft.xml + - cargo --offline build --release --locked -p pdfcraft + - install -Dm755 target/release/pdfcraft /app/bin/pdfcraft + - install -Dm644 packaging/linux/ai.storyteller.pdfcraft.desktop /app/share/applications/ai.storyteller.pdfcraft.desktop + - install -Dm644 packaging/linux/ai.storyteller.pdfcraft.mime.xml /app/share/mime/packages/ai.storyteller.pdfcraft.xml - mkdir -p /app/share/icons && cp -R assets/app-icon/hicolor /app/share/icons/ - >- version=$(awk '/^\[/{p=($0=="[workspace.package]");next} p&&$1=="version"{gsub(/[" ]/,"",$3);print $3;exit}' Cargo.toml); install -d /app/share/metainfo; sed -e "s/@VERSION@/$version/" -e "s/@DATE@/$(date -u +%Y-%m-%d)/" - packaging/linux/ai.storyteller.printcraft.metainfo.xml.in > /app/share/metainfo/ai.storyteller.printcraft.metainfo.xml + packaging/linux/ai.storyteller.pdfcraft.metainfo.xml.in > /app/share/metainfo/ai.storyteller.pdfcraft.metainfo.xml sources: - type: dir path: ../../.. diff --git a/packaging/linux/nfpm.yaml b/packaging/linux/nfpm.yaml index 00a6457a..5bf20703 100644 --- a/packaging/linux/nfpm.yaml +++ b/packaging/linux/nfpm.yaml @@ -3,7 +3,7 @@ # exports VERSION and NFPM_ARCH. # # nfpm package -f packaging/linux/nfpm.yaml -p deb -t dist/release/ -name: printcraft +name: pdfcraft arch: ${NFPM_ARCH} platform: linux version: ${VERSION} @@ -12,14 +12,14 @@ version_schema: semver release: 1 section: graphics priority: optional -maintainer: ${PRINTCRAFT_MAINTAINER} +maintainer: ${PDFCRAFT_MAINTAINER} vendor: Learning Machines LLC -homepage: https://getartcraft.com/apps/printcraft +homepage: https://getartcraft.com/apps/pdfcraft license: MIT OR Apache-2.0 description: |- Open-source, native PDF workbench: read, comment, fill and sign forms, organize, combine, split, edit, redact, protect and export PDFs. - Includes printcraft-cli, a headless command runner and opt-in MCP server. + Includes pdfcraft-cli, a headless command runner and opt-in MCP server. # The binaries link only glibc and libgcc_s directly. Windowing (X11/Wayland, xkbcommon) and the # GPU (Vulkan, EGL/GL) are loaded at runtime, so they are dependencies/recommendations here @@ -54,11 +54,11 @@ overrides: - xdg-desktop-portal contents: - - src: ${STAGE}/usr/bin/printcraft - dst: /usr/bin/printcraft + - src: ${STAGE}/usr/bin/pdfcraft + dst: /usr/bin/pdfcraft file_info: { mode: 0755 } - - src: ${STAGE}/usr/bin/printcraft-cli - dst: /usr/bin/printcraft-cli + - src: ${STAGE}/usr/bin/pdfcraft-cli + dst: /usr/bin/pdfcraft-cli file_info: { mode: 0755 } - src: ${STAGE}/usr/share/ dst: /usr/share/ diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index 8f69f468..4a57b481 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# Build and package PrintCraft for Linux ( is x86_64 or aarch64): +# Build and package PdfCraft for Linux ( is x86_64 or aarch64): # -# $DIST/printcraft--linux-.AppImage any distro with glibc >= the build host's -# $DIST/printcraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... -# $DIST/printcraft--linux-.rpm Fedora, openSUSE, RHEL, ... -# $DIST/printcraft--linux-.tar.gz plain FHS-style tree (bin/, share/) +# $DIST/pdfcraft--linux-.AppImage any distro with glibc >= the build host's +# $DIST/pdfcraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... +# $DIST/pdfcraft--linux-.rpm Fedora, openSUSE, RHEL, ... +# $DIST/pdfcraft--linux-.tar.gz plain FHS-style tree (bin/, share/) # # Usage: packaging/linux/package.sh [--skip-build] [--formats "appimage deb rpm tar"] # @@ -15,7 +15,7 @@ set -euo pipefail # shellcheck source=../env.sh . "$(dirname "${BASH_SOURCE[0]}")/../env.sh" HERE="$ROOT/packaging/linux" -APP_ID=ai.storyteller.printcraft +APP_ID=ai.storyteller.pdfcraft SKIP_BUILD=0 FORMATS="appimage deb rpm tar" @@ -34,13 +34,13 @@ case "$ARCH" in aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;; *) echo "unsupported architecture $ARCH" >&2; exit 2 ;; esac -export PRINTCRAFT_MAINTAINER="${PRINTCRAFT_MAINTAINER:-PrintCraft maintainers }" -BASENAME="printcraft-$VERSION-linux-$ARCH" +export PDFCRAFT_MAINTAINER="${PDFCRAFT_MAINTAINER:-PdfCraft maintainers }" +BASENAME="pdfcraft-$VERSION-linux-$ARCH" -echo "==> PrintCraft $VERSION for Linux $ARCH ($FORMATS)" +echo "==> PdfCraft $VERSION for Linux $ARCH ($FORMATS)" if [ "$SKIP_BUILD" = 0 ]; then - (cd "$ROOT" && cargo build --release --locked -p printcraft -p printcraft-cli) + (cd "$ROOT" && cargo build --release --locked -p pdfcraft -p pdfcraft-cli) fi BIN="$CARGO_TARGET_DIR/release" WORK="$CARGO_TARGET_DIR/linux-package" @@ -48,18 +48,18 @@ STAGE="$WORK/root" rm -rf "$WORK" # ---- stage an FHS tree (shared by every format) ------------------------------------------------- -install -Dm755 "$BIN/printcraft" "$STAGE/usr/bin/printcraft" -install -Dm755 "$BIN/printcraft-cli" "$STAGE/usr/bin/printcraft-cli" -strip "$STAGE/usr/bin/printcraft" "$STAGE/usr/bin/printcraft-cli" 2>/dev/null || true +install -Dm755 "$BIN/pdfcraft" "$STAGE/usr/bin/pdfcraft" +install -Dm755 "$BIN/pdfcraft-cli" "$STAGE/usr/bin/pdfcraft-cli" +strip "$STAGE/usr/bin/pdfcraft" "$STAGE/usr/bin/pdfcraft-cli" 2>/dev/null || true install -Dm644 "$HERE/$APP_ID.desktop" "$STAGE/usr/share/applications/$APP_ID.desktop" install -Dm644 "$HERE/$APP_ID.mime.xml" "$STAGE/usr/share/mime/packages/$APP_ID.xml" mkdir -p "$STAGE/usr/share/metainfo" -sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$PRINTCRAFT_BUILD_DATE/g" \ +sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$PDFCRAFT_BUILD_DATE/g" \ "$HERE/$APP_ID.metainfo.xml.in" >"$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml" mkdir -p "$STAGE/usr/share/icons" cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/usr/share/icons/" -mkdir -p "$STAGE/usr/share/doc/printcraft" -copy_docs "$STAGE/usr/share/doc/printcraft" +mkdir -p "$STAGE/usr/share/doc/pdfcraft" +copy_docs "$STAGE/usr/share/doc/pdfcraft" if command -v desktop-file-validate >/dev/null; then desktop-file-validate "$STAGE/usr/share/applications/$APP_ID.desktop" @@ -92,10 +92,10 @@ fi # ---- AppImage ----------------------------------------------------------------------------------- if has appimage; then - APPDIR="$WORK/PrintCraft.AppDir" + APPDIR="$WORK/PdfCraft.AppDir" cp -R "$STAGE" "$APPDIR" mv "$APPDIR/usr/share/doc" "$WORK/doc-unused" - ln -s usr/bin/printcraft "$APPDIR/AppRun" + ln -s usr/bin/pdfcraft "$APPDIR/AppRun" cp "$HERE/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop" cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png" ln -s "$APP_ID.png" "$APPDIR/.DirIcon" @@ -115,6 +115,6 @@ if has appimage; then echo "wrote $OUT" fi -"$STAGE/usr/bin/printcraft-cli" --version +"$STAGE/usr/bin/pdfcraft-cli" --version echo "==> done" ls -lh "$DIST" diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in index c83f1bca..4934f733 100644 --- a/packaging/macos/Info.plist.in +++ b/packaging/macos/Info.plist.in @@ -7,13 +7,13 @@ CFBundleDevelopmentRegion en CFBundleDisplayName - PrintCraft + PdfCraft CFBundleName - PrintCraft + PdfCraft CFBundleExecutable - PrintCraft + PdfCraft CFBundleIdentifier - ai.storyteller.printcraft + ai.storyteller.pdfcraft CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType @@ -24,12 +24,12 @@ @SHORT_VERSION@ CFBundleVersion @SHORT_VERSION@ - PrintCraftVersion + PdfCraftVersion @VERSION@ - PrintCraftBuildCommit + PdfCraftBuildCommit @BUILD_SHA@ CFBundleIconFile - PrintCraft + PdfCraft LSApplicationCategoryType public.app-category.graphics-design + file over as an Apple event, which apps/pdfcraft/src/apple_events.rs receives. --> CFBundleDocumentTypes diff --git a/packaging/macos/entitlements.plist b/packaging/macos/entitlements.plist index de181717..02092120 100644 --- a/packaging/macos/entitlements.plist +++ b/packaging/macos/entitlements.plist @@ -1,6 +1,6 @@ - diff --git a/packaging/macos/import-cert.sh b/packaging/macos/import-cert.sh index 95b815a9..ae5ee44c 100755 --- a/packaging/macos/import-cert.sh +++ b/packaging/macos/import-cert.sh @@ -18,8 +18,8 @@ if [ -z "${APPLE_CERTIFICATE:-}" ]; then fi TMP="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" -KEYCHAIN="$TMP/printcraft-signing.keychain-db" -CERT="$TMP/printcraft-signing.p12" +KEYCHAIN="$TMP/pdfcraft-signing.keychain-db" +CERT="$TMP/pdfcraft-signing.p12" KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}" trap 'rm -f "$CERT"' EXIT diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index b7100fec..7f69443b 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Build, sign and (optionally) notarize the macOS release artifacts: # -# $DIST/printcraft--macos-.dmg PrintCraft.app on a drag-to-Applications DMG -# $DIST/printcraft-cli--macos-.zip the headless CLI +# $DIST/pdfcraft--macos-.dmg PdfCraft.app on a drag-to-Applications DMG +# $DIST/pdfcraft-cli--macos-.zip the headless CLI # # Usage: packaging/macos/package.sh [--arch universal|aarch64|x86_64] [--skip-build] # @@ -39,9 +39,9 @@ export MACOSX_DEPLOYMENT_TARGET=11.0 IDENTITY="${MACOS_SIGN_IDENTITY:--}" SHORT_VERSION="${VERSION%%-*}" WORK="$CARGO_TARGET_DIR/macos-package" -APP="$WORK/PrintCraft.app" -DMG="$DIST/printcraft-$VERSION-macos-$ARCH.dmg" -CLI_ZIP="$DIST/printcraft-cli-$VERSION-macos-$ARCH.zip" +APP="$WORK/PdfCraft.app" +DMG="$DIST/pdfcraft-$VERSION-macos-$ARCH.dmg" +CLI_ZIP="$DIST/pdfcraft-cli-$VERSION-macos-$ARCH.zip" NOTARIZE=0 if [ "$IDENTITY" = "-" ]; then @@ -52,18 +52,18 @@ else warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized" fi -echo "==> PrintCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE" +echo "==> PdfCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE" # ---- build ------------------------------------------------------------------------------------- if [ "$SKIP_BUILD" = 0 ]; then args=() for t in "${TARGETS[@]}"; do args+=(--target "$t"); done - (cd "$ROOT" && cargo build --release --locked -p printcraft -p printcraft-cli "${args[@]}") + (cd "$ROOT" && cargo build --release --locked -p pdfcraft -p pdfcraft-cli "${args[@]}") fi rm -rf "$WORK" mkdir -p "$WORK/bin" -for bin in printcraft printcraft-cli; do +for bin in pdfcraft pdfcraft-cli; do inputs=() for t in "${TARGETS[@]}"; do inputs+=("$CARGO_TARGET_DIR/$t/release/$bin"); done lipo -create -output "$WORK/bin/$bin" "${inputs[@]}" @@ -97,28 +97,28 @@ notarize() { fi } -# ---- PrintCraft.app ---------------------------------------------------------------------------- +# ---- PdfCraft.app ---------------------------------------------------------------------------- echo "==> assembling $APP" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" # Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile). -cp "$WORK/bin/printcraft" "$APP/Contents/MacOS/PrintCraft" -cp "$ROOT/assets/app-icon/printcraft.icns" "$APP/Contents/Resources/PrintCraft.icns" +cp "$WORK/bin/pdfcraft" "$APP/Contents/MacOS/PdfCraft" +cp "$ROOT/assets/app-icon/pdfcraft.icns" "$APP/Contents/Resources/PdfCraft.icns" copy_font_licences "$APP/Contents/Resources" sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \ - -e "s/@BUILD_SHA@/${PRINTCRAFT_BUILD_SHA:-unknown}/g" \ + -e "s/@BUILD_SHA@/${PDFCRAFT_BUILD_SHA:-unknown}/g" \ "$HERE/Info.plist.in" >"$APP/Contents/Info.plist" plutil -lint "$APP/Contents/Info.plist" printf 'APPL????' >"$APP/Contents/PkgInfo" # Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature). # Today the only nested code is the main executable; frameworks/helpers would be signed here too. -sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/PrintCraft" +sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/PdfCraft" sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP" codesign --verify --strict --deep --verbose=2 "$APP" if [ "$NOTARIZE" = 1 ]; then - ditto -c -k --keepParent "$APP" "$WORK/PrintCraft-notarize.zip" - notarize "$WORK/PrintCraft-notarize.zip" + ditto -c -k --keepParent "$APP" "$WORK/PdfCraft-notarize.zip" + notarize "$WORK/PdfCraft-notarize.zip" xcrun stapler staple "$APP" xcrun stapler validate "$APP" spctl --assess --type execute -vvv "$APP" @@ -128,12 +128,12 @@ fi echo "==> building $DMG" STAGE="$WORK/dmg" mkdir -p "$STAGE" -ditto "$APP" "$STAGE/PrintCraft.app" +ditto "$APP" "$STAGE/PdfCraft.app" ln -s /Applications "$STAGE/Applications" rm -f "$DMG" "$WORK/raw.dmg" # makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, # which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions. -hdiutil makehybrid -hfs -hfs-volume-name "PrintCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" +hdiutil makehybrid -hfs -hfs-volume-name "PdfCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" rm -f "$WORK/raw.dmg" sign "$DMG" @@ -147,17 +147,17 @@ fi # ---- CLI --------------------------------------------------------------------------------------- echo "==> building $CLI_ZIP" -CLI_DIR="$WORK/printcraft-cli-$VERSION-macos-$ARCH" +CLI_DIR="$WORK/pdfcraft-cli-$VERSION-macos-$ARCH" mkdir -p "$CLI_DIR" -cp "$WORK/bin/printcraft-cli" "$CLI_DIR/" +cp "$WORK/bin/pdfcraft-cli" "$CLI_DIR/" copy_docs "$CLI_DIR" -sign --options runtime "$CLI_DIR/printcraft-cli" -codesign --verify --strict --verbose=2 "$CLI_DIR/printcraft-cli" +sign --options runtime "$CLI_DIR/pdfcraft-cli" +codesign --verify --strict --verbose=2 "$CLI_DIR/pdfcraft-cli" rm -f "$CLI_ZIP" ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP" # A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online. if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi -"$WORK/bin/printcraft-cli" --version +"$WORK/bin/pdfcraft-cli" --version echo "==> done" ls -lh "$DMG" "$CLI_ZIP" diff --git a/packaging/web/README.md b/packaging/web/README.md index 29686140..20ed886a 100644 --- a/packaging/web/README.md +++ b/packaging/web/README.md @@ -1,22 +1,22 @@ -# Hosting PrintCraft for the web +# Hosting PdfCraft for the web -`printcraft-web-.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a -static site in `printcraft-web-/`: +`pdfcraft-web-.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a +static site in `pdfcraft-web-/`: | File | What it is | |---|---| | `index.html` | The page. It loads everything through relative URLs. | -| `printcraft-web-.js` | wasm-bindgen glue (generated, ES module) | -| `printcraft-web-_bg.wasm` | The app, about 13 MB, or 5 MB with compression | +| `pdfcraft-web-.js` | wasm-bindgen glue (generated, ES module) | +| `pdfcraft-web-_bg.wasm` | The app, about 13 MB, or 5 MB with compression | | `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache | There is no server-side code. Upload the folder's contents anywhere that serves static files. ## Any path works -All URLs in `index.html` are relative (`public_url = "./"` in `apps/printcraft-web/Trunk.toml`), +All URLs in `index.html` are relative (`public_url = "./"` in `apps/pdfcraft-web/Trunk.toml`), so the site works at a domain root (`https://example.com/`), under a prefix -(`https://example.com/tools/printcraft/`) and from a CDN bucket. The asset names carry a content +(`https://example.com/tools/pdfcraft/`) and from a CDN bucket. The asset names carry a content hash, so they can be cached forever. Only `index.html` needs revalidation. ## Required server settings @@ -31,7 +31,7 @@ hash, so they can be cached forever. Only `index.html` needs revalidation. `.js` files, and `no-cache` on `index.html`. - **HTTPS:** WebGPU (and the clipboard) only work in a secure context, which means `https://` or `http://localhost`. Over plain HTTP elsewhere, the app falls back to WebGL2. -- **No special isolation headers:** PrintCraft doesn't use `SharedArrayBuffer`, so it doesn't +- **No special isolation headers:** PdfCraft doesn't use `SharedArrayBuffer`, so it doesn't need `Cross-Origin-Opener-Policy` or `Cross-Origin-Embedder-Policy`. If your site already sends COEP `require-corp`, also send `Cross-Origin-Resource-Policy: same-origin` (or `cross-origin` when the files live on a CDN) on the app's files. @@ -39,7 +39,7 @@ hash, so they can be cached forever. Only `index.html` needs revalidation. nginx example: ```nginx -location /printcraft/ { +location /pdfcraft/ { types { application/wasm wasm; text/javascript js; text/html html; } gzip on; gzip_types application/wasm text/javascript text/html; @@ -54,8 +54,8 @@ Local test: `python3 -m http.server 8765` inside the folder, then open http://lo ```html
The command palette searching for 'page', listing Page grid, Page labels, Rotate pages, Insert pages, Delete pages, Extract pages and more, each with the tool it belongs toThe Welcome to PrintCraft home screen with recommended tools, a recent file, a privacy note, and the full tool catalogue in the side panelThe command palette searching for 'page', listing Page grid, Page labels, Rotate pages, Insert pages, Delete pages, Extract pages and more, each with the tool it belongs toThe Welcome to PdfCraft home screen with recommended tools, a recent file, a privacy note, and the full tool catalogue in the side panel
The ⌘K command palette
A PDF open with its threaded comments