Rename PrintCraft to PdfCraft (#174)

The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-07 16:03:57 -04:00 committed by GitHub
parent 1c78ff479c
commit d3db99818f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
351 changed files with 3218 additions and 3216 deletions

View File

@ -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

View File

@ -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/'

View File

@ -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 '<Icon Id="[^"]*"' packaging/windows/printcraft.wxs | grep -vE '\.(ico|exe)"$'; then
echo "::error file=packaging/windows/printcraft.wxs::Icon Id must end in .ico or .exe"; exit 1
if grep -oE '<Icon Id="[^"]*"' packaging/windows/pdfcraft.wxs | grep -vE '\.(ico|exe)"$'; then
echo "::error file=packaging/windows/pdfcraft.wxs::Icon Id must end in .ico or .exe"; exit 1
fi
desktop-file-validate packaging/linux/ai.storyteller.printcraft.desktop
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.printcraft.metainfo.xml.in >"$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'"

View File

@ -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<version>". See docs/releasing.md.
# creates or updates the draft GitHub Release "PdfCraft v<version>". 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

View File

@ -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:

View File

@ -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=<checkout> 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=<checkout> 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 <file>`. 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.

View File

@ -2,7 +2,7 @@
<!-- Generated from ATTRIBUTION.toml by `cargo xtask assets --write`. Do not edit by hand. -->
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) |

View File

@ -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]]

View File

@ -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 -- <file.pdf>` 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 -- <file.pdf>` 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).

718
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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

4
NOTICE
View File

@ -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).

144
README.md
View File

@ -8,7 +8,7 @@
</p>
<h1 align="center">PrintCraft</h1>
<h1 align="center">PdfCraft</h1>
<p align="center">
<b>The PDF workbench; an open-source, clean-room reimplementation of Adobe Acrobat, rebuilt in pure Rust.</b><br>
@ -28,7 +28,7 @@
</p>
<p align="center">
<a href="https://getartcraft.com/apps/printcraft"><b>PrintCraft on getartcraft.com</b></a> ·
<a href="https://getartcraft.com/apps/pdfcraft"><b>PdfCraft on getartcraft.com</b></a> ·
<a href="https://getartcraft.com/">ArtCraft</a> ·
<a href="https://getartcraft.com/apps">All Crafting Apps</a>
</p>
@ -36,9 +36,9 @@
<br>
<p align="center">
<img src="docs/images/printcraft-viewer.png" alt="PrintCraft with the PrintCraft Showcase cover page open, the All tools panel on the left and 20 threaded comments on the right" width="100%">
<img src="docs/images/pdfcraft-viewer.png" alt="PdfCraft with the PdfCraft Showcase cover page open, the All tools panel on the left and 20 threaded comments on the right" width="100%">
<br>
<sub>The PrintCraft Showcase, a 13-page specimen PDF, open with the All tools panel and threaded comments.</sub>
<sub>The PdfCraft Showcase, a 13-page specimen PDF, open with the All tools panel and threaded comments.</sub>
</p>
> [!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.
<p align="center">
<img src="docs/images/printcraft-scripts.png" alt="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" width="100%">
<img src="docs/images/pdfcraft-scripts.png" alt="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" width="100%">
<br>
<sub>Twelve writing systems on one page, plus vertical Japanese, at 125%.</sub>
</p>
@ -117,8 +117,8 @@ PrintCraft renders PDFs with care for the details that make a page feel right: k
<table>
<tr>
<td width="50%"><img src="docs/images/printcraft-twoup.png" alt="Two facing pages in Read mode with the dark theme: the Foreword and the Setting Text chapter with its drop cap and pull quote"></td>
<td width="50%"><img src="docs/images/printcraft-dark.png" alt="The dark theme showing the Code and Images chapter, a syntax-coloured listing and a fractal image, with the comments panel open"></td>
<td width="50%"><img src="docs/images/pdfcraft-twoup.png" alt="Two facing pages in Read mode with the dark theme: the Foreword and the Setting Text chapter with its drop cap and pull quote"></td>
<td width="50%"><img src="docs/images/pdfcraft-dark.png" alt="The dark theme showing the Code and Images chapter, a syntax-coloured listing and a fractal image, with the comments panel open"></td>
</tr>
<tr>
<td align="center"><sub>Two-up Read mode, ready for long reading</sub></td>
@ -136,8 +136,8 @@ Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) k
<table>
<tr>
<td width="50%"><img src="docs/images/printcraft-find.png" alt="The find bar showing match 10 of 16 for the word 'type', highlighted in the Expressive Type chapter heading"></td>
<td width="50%"><img src="docs/images/printcraft-bookmarks.png" alt="The 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"></td>
<td width="50%"><img src="docs/images/pdfcraft-find.png" alt="The find bar showing match 10 of 16 for the word 'type', highlighted in the Expressive Type chapter heading"></td>
<td width="50%"><img src="docs/images/pdfcraft-bookmarks.png" alt="The 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"></td>
</tr>
<tr>
<td align="center"><sub>Find as you type: match 10 of 16</sub></td>
@ -155,7 +155,7 @@ Open **Organize pages** to see every page at once:
- **Undo anything:** <kbd>⌘Z</kbd>, then save.
<p align="center">
<img src="docs/images/printcraft-organize.png" alt="The Organize pages grid with the showcase's pages as thumbnails, three of them selected, and the page toolbar above" width="100%">
<img src="docs/images/pdfcraft-organize.png" alt="The Organize pages grid with the showcase's pages as thumbnails, three of them selected, and the page toolbar above" width="100%">
<br>
<sub>Organize pages with three pages selected and the page tools in the toolbar above.</sub>
</p>
@ -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.
</td>
<td width="50%" valign="top"><img src="docs/images/printcraft-split.png" alt="The Split document dialog over the organize view, set to one page per file and reporting that it creates 13 files from 13 pages"><br><sub>Split document: one page per file makes 13 files.</sub></td>
<td width="50%" valign="top"><img src="docs/images/pdfcraft-split.png" alt="The Split document dialog over the organize view, set to one page per file and reporting that it creates 13 files from 13 pages"><br><sub>Split document: one page per file makes 13 files.</sub></td>
</tr>
</table>
@ -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.
<table>
<tr>
<td width="50%" valign="top"><img src="docs/images/printcraft-properties.png" alt="The Document Properties dialog on its Description tab, with editable title, author, subject and keywords, and tabs for Security, Fonts and Advanced"><br><sub>Document Properties, Description tab</sub></td>
<td width="50%" valign="top"><img src="docs/images/pdfcraft-properties.png" alt="The Document Properties dialog on its Description tab, with editable title, author, subject and keywords, and tabs for Security, Fonts and Advanced"><br><sub>Document Properties, Description tab</sub></td>
<td width="50%" valign="top">
**Document Properties** shows:
@ -228,8 +228,8 @@ Documents restricted by their author show a clear notice, and PrintCraft honours
<table>
<tr>
<td width="50%"><img src="docs/images/printcraft-forms.png" alt="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 values"></td>
<td width="50%"><img src="docs/images/printcraft-layers.png" alt="The 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"></td>
<td width="50%"><img src="docs/images/pdfcraft-forms.png" alt="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 values"></td>
<td width="50%"><img src="docs/images/pdfcraft-layers.png" alt="The 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"></td>
</tr>
<tr>
<td valign="top"><b>Forms</b>: every field with its current value, field highlighting, and checkboxes, radio buttons, lists and signatures drawn the way their author designed them.</td>
@ -243,8 +243,8 @@ Press <kbd>⌘K</kbd> to search every tool and command, or browse the **All tool
<table>
<tr>
<td width="50%"><img src="docs/images/printcraft-palette.png" alt="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 to"></td>
<td width="50%"><img src="docs/images/printcraft-tools.png" alt="The Welcome to PrintCraft home screen with recommended tools, a recent file, a privacy note, and the full tool catalogue in the side panel"></td>
<td width="50%"><img src="docs/images/pdfcraft-palette.png" alt="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 to"></td>
<td width="50%"><img src="docs/images/pdfcraft-tools.png" alt="The Welcome to PdfCraft home screen with recommended tools, a recent file, a privacy note, and the full tool catalogue in the side panel"></td>
</tr>
<tr>
<td align="center"><sub>The <kbd>⌘K</kbd> command palette</sub></td>
@ -259,12 +259,12 @@ Press <kbd>⌘K</kbd> 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.
| <img src="https://raw.githubusercontent.com/storytold/vectorcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.vectorcraft.png" alt="" width="32" height="32"> | **VectorCraft** | Vector illustration | [GitHub](https://github.com/storytold/vectorcraft) | [Website](https://getartcraft.com/apps/vectorcraft) |
| <img src="https://raw.githubusercontent.com/storytold/filmcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.filmcraft.png" alt="" width="32" height="32"> | **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
| <img src="https://raw.githubusercontent.com/storytold/lightcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png" alt="" width="32" height="32"> | **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
| <img src="https://raw.githubusercontent.com/storytold/printcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.printcraft.png" alt="" width="32" height="32"> | **PrintCraft** | **Reading, organizing and protecting PDFs · you are here** | [GitHub](https://github.com/storytold/printcraft) | [Website](https://getartcraft.com/apps/printcraft) |
| <img src="https://raw.githubusercontent.com/storytold/pdfcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.pdfcraft.png" alt="" width="32" height="32"> | **PdfCraft** | **Reading, organizing and protecting PDFs · you are here** | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **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
<a href="https://discord.gg/artcraft"><b>discord.gg/artcraft</b></a> ·
<a href="https://getartcraft.com/">getartcraft.com</a> ·
<a href="https://getartcraft.com/apps">The Crafting Apps</a> ·
<a href="https://getartcraft.com/apps/printcraft">PrintCraft</a>
<a href="https://getartcraft.com/apps/pdfcraft">PdfCraft</a>
</p>
---
## 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.
<sub>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.</sub>
<sub>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.</sub>
<p align="center">
<a href="https://getartcraft.com/"><img alt="ArtCraft" src="docs/brand/artcraft-mark.svg" width="28"></a><br>

View File

@ -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.

View File

@ -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"]

View File

@ -1,24 +1,24 @@
//! printcraft-cli — headless PrintCraft.
//! pdfcraft-cli — headless PdfCraft.
//!
//! ```text
//! printcraft-cli info <file.pdf> [--password PW] document summary as JSON
//! printcraft-cli render <file.pdf> --page N [--dpi 96] --out x.pam
//! printcraft-cli text <file.pdf> [--page N] extracted text (pages separated by form feeds)
//! printcraft-cli edit <in.pdf> --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1]
//! pdfcraft-cli info <file.pdf> [--password PW] document summary as JSON
//! pdfcraft-cli render <file.pdf> --page N [--dpi 96] --out x.pam
//! pdfcraft-cli text <file.pdf> [--page N] extracted text (pages separated by form feeds)
//! pdfcraft-cli edit <in.pdf> --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1]
//! [--insert-blank 1] [--title T] [--author A] [--full]
//! printcraft-cli combine <a.pdf> <b.pdf> … --out combined.pdf
//! printcraft-cli extract <in.pdf> --pages 1,3,5 --out out.pdf
//! printcraft-cli split <in.pdf> (--every N | --before 3,7) [--out-dir DIR]
//! printcraft-cli check <files or dirs…> [--timeout 20] [--dpi 36] [--json out.json]
//! printcraft-cli tools automation tools and their JSON Schemas
//! printcraft-cli run <tool> [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 <method> [key=value …] [--out shot.png]
//! pdfcraft-cli combine <a.pdf> <b.pdf> … --out combined.pdf
//! pdfcraft-cli extract <in.pdf> --pages 1,3,5 --out out.pdf
//! pdfcraft-cli split <in.pdf> (--every N | --before 3,7) [--out-dir DIR]
//! pdfcraft-cli check <files or dirs…> [--timeout 20] [--dpi 36] [--json out.json]
//! pdfcraft-cli tools automation tools and their JSON Schemas
//! pdfcraft-cli run <tool> [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 <method> [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<String> = 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 <info|render|text|edit|combine|extract|split|check|tools|run|mcp|ui> … (see source header for options)\nhelp and feedback: https://discord.gg/artcraft"
_ => Err("usage: pdfcraft-cli <info|render|text|edit|combine|extract|split|check|tools|run|mcp|ui> … (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::<Vec<_>>(),
@ -150,7 +150,7 @@ fn page_list(s: &str) -> Result<Vec<usize>, 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::<Result<Vec<_>, 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<printcraft_automation::Automation, String> {
let a = printcraft_automation::Automation::new();
fn automation(args: &[String]) -> Result<pdfcraft_automation::Automation, String> {
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<printcraft_automation::Automation, Stri
}
fn tools() -> Result<(), String> {
let list: Vec<serde_json::Value> = printcraft_automation::tools()
let list: Vec<serde_json::Value> = 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<printcraft_automation::Content>, out: Option<&str>) -> Result<(), String> {
fn print_output(content: Vec<pdfcraft_automation::Content>, 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"))?;

View File

@ -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 }

View File

@ -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"

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PrintCraft</title>
<title>PdfCraft</title>
<link data-trunk rel="rust" data-wasm-opt="z" />
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #f1f1f3; }
@ -12,6 +12,6 @@
</style>
</head>
<body>
<canvas id="printcraft"></canvas>
<canvas id="pdfcraft"></canvas>
</body>
</html>

View File

@ -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::<web_sys::HtmlCanvasElement>().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=<url>` 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<Vec<u8>, 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");
}

View File

@ -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"

29
apps/pdfcraft/build.rs Normal file
View File

@ -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}");
}
}

View File

@ -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());

View File

@ -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 <file>` enables the UI control channel (off by default): the app listens on a random
//! loopback port and writes `{"port", "token", "pid"}` to `<file>` (owner-only permissions).
//! Agents then drive it with `printcraft-cli ui --control <file> <method> …`.
//! Agents then drive it with `pdfcraft-cli ui --control <file> <method> …`.
// 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 };

View File

@ -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<Release, String> {
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("<html>").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:?}");
}
}

View File

@ -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"]

View File

@ -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}");
}
}

View File

@ -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)

View File

@ -1,6 +1,6 @@
# PrintCraft app icon
# PdfCraft app icon
<img src="printcraft-small.svg" alt="PrintCraft app icon: an engraved lion's head on green" width="128">
<img src="pdfcraft-small.svg" alt="PdfCraft app icon: an engraved lion's head on green" width="128">
**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/<n>x<n>/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/<n>x<n>/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

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 695 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 451 KiB

View File

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 265 KiB

View File

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 238 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 451 KiB

After

Width:  |  Height:  |  Size: 451 KiB

View File

@ -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/<kind>/<id>`. Chrome turns each one into
`https://mark.pdfcraft.invalid/<kind>/<id>`. 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`.

View File

@ -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

1 # 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.
2 # These are made-up numbers for demonstration, not measurements.
3 chart,series,label,value
4 pages_per_second,throughput,Text,189

View File

@ -2,14 +2,14 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>PrintCraft Showcase</title>
<title>PdfCraft Showcase</title>
<!--
PrintCraft Showcase: source for `cargo xtask demo-pdf`.
All prose in this file is original to the PrintCraft project.
PdfCraft Showcase: source for `cargo xtask demo-pdf`.
All prose in this file is original to the PdfCraft project.
Placeholders substituted by the xtask:
{{ASSETS}} file:// URL of the repository's assets/ directory
{{MANDELBROT_PNG}} data: URI of a procedurally generated PNG
Links to https://mark.printcraft.invalid/<kind>/<id> are *markers*: the xtask
Links to https://mark.pdfcraft.invalid/<kind>/<id> are *markers*: the xtask
converts the link rectangles Chrome emits for them into real PDF annotations
(highlight, sticky note, stamp, ...), so annotation geometry follows the layout.
-->
@ -351,7 +351,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<div class="grad">A tour of the page, glyph by glyph.</div>
<p class="sub">Typography, world scripts, vector graphics, mathematics, forms and annotations &mdash; gathered in one document to put a PDF engine through its paces.</p>
<div class="meta">
<div><strong>PrintCraft</strong><br>Open-source PDF toolkit in Rust</div>
<div><strong>PdfCraft</strong><br>Open-source PDF toolkit in Rust</div>
<div style="text-align:right">Generated by <span style="font-family:var(--mono)">cargo xtask demo-pdf</span><br>US Letter &middot; 8.5 &times; 11 in</div>
</div>
</div>
@ -359,7 +359,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 2. CONTENTS ============================ -->
<section class="page" id="contents">
<div class="runhead"><span>PrintCraft Showcase</span><span>Contents</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>Contents</span></div>
<div class="kicker">Inside this specimen</div>
<h2 class="sec">Contents</h2>
<ol class="toc">
@ -378,19 +378,19 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<div><h4>Colophon</h4>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 <span style="font-family:var(--mono)">lopdf</span>.</div>
<div><h4>What to look for</h4>Open the bookmarks, layers, attachments and comments panels. Page labels run Cover, i&ndash;ii, then 1&ndash;10. One layer adds a draft watermark; another holds print-only notes and starts hidden.</div>
</div>
<div class="folio"><span>PrintCraft Showcase</span><b>i</b></div>
<div class="folio"><span>PdfCraft Showcase</span><b>i</b></div>
</section>
<!-- ============================ 3. FOREWORD ============================ -->
<section class="page foreword" id="foreword">
<div class="runhead"><span>PrintCraft Showcase</span><span>Foreword</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>Foreword</span></div>
<div class="kicker">Foreword</div>
<h2 class="sec">Why a <em>specimen?</em></h2>
<p class="lead">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.</p>
<p>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.</p>
<p>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.</p>
<p>Nothing here is exotic. Each feature is something ordinary documents use every day &mdash; 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.</p>
<p>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.</p>
<div class="signoff">The PrintCraft contributors</div>
<div class="signoff">The PdfCraft contributors</div>
<h3>Tracking, from tight to airy</h3>
<div class="tracking">
@ -413,12 +413,12 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 4. SETTING TEXT ============================ -->
<section class="page" id="setting">
<div class="runhead"><span>PrintCraft Showcase</span><span>1 &middot; Setting Text</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>1 &middot; Setting Text</span></div>
<div class="kicker">Chapter one</div>
<h2 class="sec" id="h-setting">Setting <em>Text</em></h2>
<p class="dek">Two justified columns with automatic hyphenation, old-style figures, a drop cap and a pull quote &mdash; the everyday machinery of a well-made page.</p>
<div class="cols">
<p class="first">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 <a class="mk" href="https://mark.printcraft.invalid/highlight/body">honour the spacing</a> it was given.<sup class="fn">1</sup></p>
<p class="first">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 <a class="mk" href="https://mark.pdfcraft.invalid/highlight/body">honour the spacing</a> it was given.<sup class="fn">1</sup></p>
<p>A PDF does not reflow. Every glyph has already been placed by the program that wrote the file, and the reader&rsquo;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.</p>
<p>Old-style figures, like the ones in this paragraph&rsquo;s 1,280 characters and 36 lines, rise and fall with the lowercase so that numbers do not shout. Ligatures such as <i>fi</i>, <i>fl</i> and <i>ffi</i> in words like <i>officially</i>, <i>affluent</i> and <i>afflicted</i> join letters that would otherwise collide.<sup class="fn">2</sup></p>
<div class="pull">&ldquo;Good typography is noticed only when it is missing.&rdquo;<cite>A working maxim of the trade</cite></div>
@ -437,7 +437,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 5. OPENTYPE ============================ -->
<section class="page" id="opentype">
<div class="runhead"><span>PrintCraft Showcase</span><span>2 &middot; OpenType Features</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>2 &middot; OpenType Features</span></div>
<div class="kicker">Chapter two</div>
<h2 class="sec" id="h-opentype">OpenType <em>Features</em></h2>
<p class="dek">Each row shows the same text with a feature off, then on. The glyphs differ; the underlying characters do not.</p>
@ -476,7 +476,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 6. EXPRESSIVE ============================ -->
<section class="page" id="expressive">
<div class="runhead"><span>PrintCraft Showcase</span><span>3 &middot; Expressive Type</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>3 &middot; Expressive Type</span></div>
<div class="kicker">Chapter three</div>
<h2 class="sec" id="h-expressive">Expressive <em>Type</em></h2>
<p class="dek">Effects that become transparency groups, soft masks, clipping paths and transformed text objects once they reach PDF.</p>
@ -513,7 +513,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 7. SCRIPTS ============================ -->
<section class="page" id="scripts">
<div class="runhead"><span>PrintCraft Showcase</span><span>4 &middot; Scripts of the World</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>4 &middot; Scripts of the World</span></div>
<div class="kicker">Chapter four</div>
<h2 class="sec" id="h-scripts">Scripts of the <em>World</em></h2>
<p class="dek">A short greeting &mdash; roughly, &ldquo;every letter deserves a good home&rdquo; &mdash; in writing systems that need bidirectional layout, contextual shaping, reordering and vertical setting.</p>
@ -551,7 +551,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 8. MATH ============================ -->
<section class="page" id="math">
<div class="runhead"><span>PrintCraft Showcase</span><span>5 &middot; Mathematics</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>5 &middot; Mathematics</span></div>
<div class="kicker">Chapter five</div>
<h2 class="sec" id="h-math"><em>Mathematics</em></h2>
<p class="dek">Equations written in MathML Core and laid out by the browser: stretchy delimiters, radicals, large operators, scripts and matrices.</p>
@ -592,7 +592,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 9. VECTOR ============================ -->
<section class="page" id="vector">
<div class="runhead"><span>PrintCraft Showcase</span><span>6 &middot; Vector Graphics</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>6 &middot; Vector Graphics</span></div>
<div class="kicker">Chapter six</div>
<h2 class="sec" id="h-vector">Vector <em>Graphics</em></h2>
<p class="dek">Nine small plates, each exercising one piece of the PDF imaging model.</p>
@ -652,7 +652,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 10. DATA ============================ -->
<section class="page" id="data">
<div class="runhead"><span>PrintCraft Showcase</span><span>7 &middot; Data &amp; Tables</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>7 &middot; Data &amp; Tables</span></div>
<div class="kicker">Chapter seven</div>
<h2 class="sec" id="h-data">Data &amp; <em>Tables</em></h2>
<p class="dek">Charts drawn as SVG and a striped table. All figures are illustrative sample data, not measurements.</p>
@ -731,7 +731,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 11. CODE ============================ -->
<section class="page" id="code">
<div class="runhead"><span>PrintCraft Showcase</span><span>8 &middot; Code &amp; Images</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>8 &middot; Code &amp; Images</span></div>
<div class="kicker">Chapter eight</div>
<h2 class="sec" id="h-code">Code &amp; <em>Images</em></h2>
<p class="dek">A monospaced listing with syntax colouring, and the raster that it describes.</p>
@ -761,7 +761,7 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<h3>Links</h3>
<ul class="links" style="padding-left:14pt;margin:0;font-size:10pt">
<li>External: <a href="https://example.com/">https://example.com/</a></li>
<li>External: <a href="https://example.org/printcraft">example.org/printcraft</a></li>
<li>External: <a href="https://example.org/pdfcraft">example.org/pdfcraft</a></li>
<li>Internal: back to <a href="#contents">the contents</a> or ahead to <a href="#review">review &amp; markup</a></li>
</ul>
</div>
@ -771,15 +771,15 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
<!-- ============================ 12. REVIEW ============================ -->
<section class="page" id="review">
<div class="runhead"><span>PrintCraft Showcase</span><span>9 &middot; Review &amp; Markup</span></div>
<div class="runhead"><span>PdfCraft Showcase</span><span>9 &middot; Review &amp; Markup</span></div>
<div class="kicker">Chapter nine</div>
<h2 class="sec" id="h-review">Review &amp; <em>Markup</em></h2>
<p class="dek">Every annotation on this page was added after printing, each with its own appearance stream, author, date and unique name.</p>
<div class="review">
<div>
<h3 style="margin-top:0">Text markup</h3>
<p>Reviewers rarely rewrite a document; they <a class="mk" href="https://mark.printcraft.invalid/highlight/review">highlight the phrase that matters</a>, <a class="mk" href="https://mark.printcraft.invalid/underline/review">underline a claim that needs a source</a>, and <a class="mk" href="https://mark.printcraft.invalid/strikeout/review">strike out a redundant word</a> before moving on.</p>
<p>A wavy line says <a class="mk" href="https://mark.printcraft.invalid/squiggly/review">check the spelling here</a>, while a caret marks where text should<a class="mk" href="https://mark.printcraft.invalid/caret/review">&nbsp;&nbsp;</a>be inserted.</p>
<p>Reviewers rarely rewrite a document; they <a class="mk" href="https://mark.pdfcraft.invalid/highlight/review">highlight the phrase that matters</a>, <a class="mk" href="https://mark.pdfcraft.invalid/underline/review">underline a claim that needs a source</a>, and <a class="mk" href="https://mark.pdfcraft.invalid/strikeout/review">strike out a redundant word</a> before moving on.</p>
<p>A wavy line says <a class="mk" href="https://mark.pdfcraft.invalid/squiggly/review">check the spelling here</a>, while a caret marks where text should<a class="mk" href="https://mark.pdfcraft.invalid/caret/review">&nbsp;&nbsp;</a>be inserted.</p>
<h3>Comments</h3>
<p>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.</p>
<h3>Legend</h3>
@ -789,16 +789,16 @@ a.box { position: absolute; display: block; color: transparent; font-size: 1pt;
</div>
<div class="canvas">
<span class="lab" style="left:10pt;top:8pt">Review canvas</span>
<a class="box" style="left:12pt;top:26pt;width:152pt;height:40pt" href="https://mark.printcraft.invalid/freetext/1">.</a>
<a class="box" style="left:196pt;top:22pt;width:20pt;height:20pt" href="https://mark.printcraft.invalid/note/1">.</a>
<a class="box" style="left:196pt;top:60pt;width:18pt;height:22pt" href="https://mark.printcraft.invalid/attach/1">.</a>
<a class="box" style="left:16pt;top:88pt;width:90pt;height:60pt" href="https://mark.printcraft.invalid/square/1">.</a>
<a class="box" style="left:120pt;top:92pt;width:90pt;height:60pt" href="https://mark.printcraft.invalid/circle/1">.</a>
<a class="box" style="left:16pt;top:170pt;width:94pt;height:80pt" href="https://mark.printcraft.invalid/polygon/1">.</a>
<a class="box" style="left:120pt;top:170pt;width:96pt;height:80pt" href="https://mark.printcraft.invalid/polyline/1">.</a>
<a class="box" style="left:16pt;top:268pt;width:196pt;height:40pt" href="https://mark.printcraft.invalid/line/1">.</a>
<a class="box" style="left:16pt;top:322pt;width:196pt;height:50pt" href="https://mark.printcraft.invalid/ink/1">.</a>
<a class="box" style="left:30pt;top:388pt;width:170pt;height:56pt" href="https://mark.printcraft.invalid/stamp/1">.</a>
<a class="box" style="left:12pt;top:26pt;width:152pt;height:40pt" href="https://mark.pdfcraft.invalid/freetext/1">.</a>
<a class="box" style="left:196pt;top:22pt;width:20pt;height:20pt" href="https://mark.pdfcraft.invalid/note/1">.</a>
<a class="box" style="left:196pt;top:60pt;width:18pt;height:22pt" href="https://mark.pdfcraft.invalid/attach/1">.</a>
<a class="box" style="left:16pt;top:88pt;width:90pt;height:60pt" href="https://mark.pdfcraft.invalid/square/1">.</a>
<a class="box" style="left:120pt;top:92pt;width:90pt;height:60pt" href="https://mark.pdfcraft.invalid/circle/1">.</a>
<a class="box" style="left:16pt;top:170pt;width:94pt;height:80pt" href="https://mark.pdfcraft.invalid/polygon/1">.</a>
<a class="box" style="left:120pt;top:170pt;width:96pt;height:80pt" href="https://mark.pdfcraft.invalid/polyline/1">.</a>
<a class="box" style="left:16pt;top:268pt;width:196pt;height:40pt" href="https://mark.pdfcraft.invalid/line/1">.</a>
<a class="box" style="left:16pt;top:322pt;width:196pt;height:50pt" href="https://mark.pdfcraft.invalid/ink/1">.</a>
<a class="box" style="left:30pt;top:388pt;width:170pt;height:56pt" href="https://mark.pdfcraft.invalid/stamp/1">.</a>
</div>
</div>
<div class="folio"><span>Review &amp; Markup</span><b>9</b></div>

View File

@ -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

View File

@ -1,4 +1,4 @@
# printcraft-a11y
# pdfcraft-a11y
Layer L4: accessibility (execution plan M12.3). Today: the Accessibility Checker's full check.

View File

@ -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<Dict> {
@ -139,7 +139,7 @@ fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet<i64>) -> Option<[f64;
let mut marks: Vec<bool> = 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<i64>) -> Option<[f64;
pub fn figures(doc: &Document) -> Vec<Figure> {
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<ObjRef, usize> = 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<i64> = 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) {

View File

@ -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<bool> = 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")),

View File

@ -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<usize> = match &options.pages {
Some(p) => p.iter().copied().filter(|p| *p < pages.len()).collect(),
None => (0..pages.len()).collect(),

View File

@ -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};

View File

@ -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::<Rule>::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}");
}

View File

@ -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]

View File

@ -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

View File

@ -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<Stream> {
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<Stream> {
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 {

View File

@ -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 {

View File

@ -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),
}

View File

@ -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::*;

View File

@ -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 }

View File

@ -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

View File

@ -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<printcraft_engine::ocr::OcrSettings> {
let mut settings = printcraft_engine::ocr::OcrSettings::default();
fn ocr_settings(&self, a: &Args) -> Result<pdfcraft_engine::ocr::OcrSettings> {
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<Value> {
use printcraft_engine::ocr;
use pdfcraft_engine::ocr;
let dirs: Vec<String> = ocr::Models::search_dirs().iter().map(|d| d.to_string_lossy().into_owned()).collect();
let langs: Vec<Value> = 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<Value> {
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<Value> {
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<Value> {
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<printcraft_engine::pdfa::Level> {
fn pdfa_level(&self, a: &Args) -> Result<pdfcraft_engine::pdfa::Level> {
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<Value> {
@ -294,12 +294,12 @@ impl Automation {
pub(crate) fn pdfa_convert(&mut self, a: &Args) -> Result<Value> {
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<Value> {
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<Value> = builtin()
.iter()
@ -310,7 +310,7 @@ impl Automation {
}
pub(crate) fn action_run(&mut self, a: &Args) -> Result<Value> {
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<Value> {
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::<Vec<_>>() });
|s: &pdfcraft_engine::compare::Side| json!({ "text": s.text, "page": s.page + 1, "rects": s.rects.iter().map(r2).collect::<Vec<_>>() });
let list: Vec<Value> = 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<Value> {
use printcraft_engine::FieldAction as A;
use pdfcraft_engine::FieldAction as A;
let list: Vec<Value> = 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<Value> {
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<String> {
@ -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)

View File

@ -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<Rgb> {
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<Value> {
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,

View File

@ -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;

View File

@ -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<Format> {
/// `{"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<printcraft_engine::FieldLook> {
use printcraft_engine::{BorderStyle, FieldFont};
fn look_arg(v: &Value, mut l: pdfcraft_engine::FieldLook) -> Result<pdfcraft_engine::FieldLook> {
use pdfcraft_engine::{BorderStyle, FieldFont};
let o = v.as_object().ok_or_else(|| bad("appearance must be an object"))?;
let colour = |k: &str| -> Result<Option<Option<[f64; 3]>>> {
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)?;

View File

@ -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<u8>>, Vec<Option<Arc<PageText>>>);
/// A headless PrintCraft session driven by tool calls.
/// A headless PdfCraft session driven by tool calls.
pub struct Automation {
session: Session,
root: Option<PathBuf>,
@ -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<f64> =
@ -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<Value> = printcraft_engine::sign::keychain::identities(None)
let ids: Vec<Value> = 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<Value> = Vec::new();
@ -623,7 +623,7 @@ impl Automation {
}
fn doc_initial_view(&mut self, a: &Args) -> Result<Value> {
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<Value> {
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<printcraft_engine::Hidden> = match a.get("discard") {
let discard: Vec<pdfcraft_engine::Hidden> = 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::<Result<_>>()?,
@ -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<Option<printcraft_engine::MarkFile>> {
fn mark_file(&self, a: &Args) -> Result<Option<pdfcraft_engine::MarkFile>> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
fn bookmark_tree(items: &[pdfcraft_render::OutlineItem], parent: &[usize]) -> Vec<Value> {
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::<Vec<_>>(),
"layers": i.layers.iter().map(|l| json!({ "name": l.name, "visible": l.visible })).collect::<Vec<_>>(),
@ -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);

View File

@ -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;

View File

@ -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<Resource> {
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::<f64>().ok());
let parts: Vec<&str> = path.split('/').collect();
@ -144,13 +144,13 @@ fn parse_uri(uri: &str) -> Option<Resource> {
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);
}
}

View File

@ -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};

View File

@ -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};

View File

@ -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};

View File

@ -690,7 +690,7 @@ pub fn tools() -> Vec<ToolDef> {
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 (`<name>_page_<n>.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<ToolDef> {
}),
&["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<ToolDef> {
.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() }), &[])),
]

View File

@ -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<u8> {
/// 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);

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -1,4 +1,4 @@
# printcraft-content
# pdfcraft-content
Layer L2: content streams (ISO 32000-2 §7.8.2, §8, §9).

View File

@ -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;

View File

@ -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]

View File

@ -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<u32> = 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());
}

View File

@ -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<String> = 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();
}

View File

@ -99,7 +99,7 @@ pub struct Document {
header_offset: usize,
version: String,
/// The authenticated security handler of an encrypted document.
security: Option<Arc<printcraft_crypt::SecurityHandler>>,
security: Option<Arc<pdfcraft_crypt::SecurityHandler>>,
/// Object number of the `/Encrypt` dictionary (never encrypted itself).
encrypt_num: Option<u32>,
/// 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<Arc<printcraft_crypt::SecurityHandler>>,
out_security: Option<Arc<pdfcraft_crypt::SecurityHandler>>,
out_encrypt_num: Option<u32>,
}
@ -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<printcraft_crypt::Permissions> {
pub fn permissions(&self) -> Option<pdfcraft_crypt::Permissions> {
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<u32>) {
pub(crate) fn output_security(&self) -> (Option<&pdfcraft_crypt::SecurityHandler>, Option<u32>) {
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<u8> {
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");

View File

@ -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)]

View File

@ -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()))
}
}

View File

@ -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};

View File

@ -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<usize> {
@ -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 => {}

View File

@ -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"));

View File

@ -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<u8> {
@ -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,

View File

@ -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"] }

View File

@ -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.

View File

@ -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 {

View File

@ -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));
}

View File

@ -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();

View File

@ -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

View File

@ -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);

View File

@ -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 }

View File

@ -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]);

View File

@ -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};

View File

@ -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<u8>)> {
.collect()
}
fn page_of(doc: &Document, page: usize) -> Result<printcraft_model::Page, EditError> {
printcraft_model::pages(doc).into_iter().nth(page).ok_or(EditError::NoSuchPage(page))
fn page_of(doc: &Document, page: usize) -> Result<pdfcraft_model::Page, EditError> {
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![]),
]

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