Merge current main into #2155

Brings the branch up to date with main (47f42ba0), which moved 37 commits since
the merge base, including page-edge shadows (#2088), full rewrite after page
delete (#4279) and the licence-check stage (#2212). No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FredApps
2026-11-01 11:23:43 +01:00
co-authored by Claude Opus 5.5
63 changed files with 2555 additions and 426 deletions
+7
View File
@@ -1,3 +1,10 @@
# Minimum cargo-nextest for this repository: GHSA-vc25-qg58-w57c is fixed in 0.9.150 and CI
# installs exactly that version, so an older nextest refuses to run (exit 92 REQUIRED_VERSION_NOT_MET).
nextest-version = "0.9.150"
# These measurements require a quiet runner. Their in-process SERIAL mutex only
# serializes cargo test; nextest gives each test its own process. Reserve all slots
# so other tests cannot charge their CPU/render-worker time to a frame budget.
[[profile.default.overrides]]
filter = 'package(pdfcraft-ui-egui) & binary(perf)'
threads-required = "num-test-threads"
+1
View File
@@ -12,6 +12,7 @@ on:
permissions:
contents: write
packages: write
concurrency:
group: release-${{ github.ref }}
+190 -190
View File
@@ -36,181 +36,181 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `crates/ui-egui/src/i18n/ko.tsv` | Korean interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Korean UI strings, compiled in by crates/ui-egui/src/i18n.rs |
| `crates/ui-egui/src/i18n/ro.tsv` | Romanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; AI-assisted and not yet reviewed line by line by a native Romanian speaker; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Romanian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
| `crates/ui-egui/src/i18n/fi.tsv` | Finnish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: AI-assisted (Claude Code) clean-room translations of PdfCraft's English UI labels and user-facing messages; not yet proofread by a native Finnish speaker; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 calculation-order, text-format, print, thumbnail, layer, comment-reshape and signature labels (previously English placeholders) are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Finnish UI strings, compiled in by crates/ui-egui/src/i18n.rs |
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/square-terminal.svg` | Lucide icon "square-terminal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square-terminal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-code.svg` | Lucide icon "file-code" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-code.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-spreadsheet.svg` | Lucide icon "file-spreadsheet" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-spreadsheet.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-justify.svg` | Lucide icon "align-justify" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-justify.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-right.svg` | Lucide icon "align-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/columns-3.svg` | Lucide icon "columns-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/columns-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rows-3.svg` | Lucide icon "rows-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rows-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/flip-horizontal-2.svg` | Lucide icon "flip-horizontal-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/flip-horizontal-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/flip-vertical-2.svg` | Lucide icon "flip-vertical-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/flip-vertical-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/app-window.svg` | Lucide icon "app-window" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/app-window.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/arrow-left-right.svg` | Lucide icon "arrow-left-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/arrow-left-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/arrow-up-down.svg` | Lucide icon "arrow-up-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/arrow-up-down.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/arrow-up-right.svg` | Lucide icon "arrow-up-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/arrow-up-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/badge-check.svg` | Lucide icon "badge-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/badge-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bandage.svg` | Lucide icon "bandage" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bandage.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bell.svg` | Lucide icon "bell" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bell.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/blend.svg` | Lucide icon "blend" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/blend.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bold.svg` | Lucide icon "bold" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bold.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/book-open.svg` | Lucide icon "book-open" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/book-open.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bookmark.svg` | Lucide icon "bookmark" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bookmark.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bookmark-plus.svg` | Lucide icon "bookmark-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bookmark-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/braces.svg` | Lucide icon "braces" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/braces.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/brush-cleaning.svg` | Lucide icon "brush-cleaning" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/brush-cleaning.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/brush.svg` | Lucide icon "brush" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/brush.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/camera.svg` | Lucide icon "camera" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/camera.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/check-circle-2.svg` | Lucide icon "check-circle-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/check-circle-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/check.svg` | Lucide icon "check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevron-down.svg` | Lucide icon "chevron-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-down.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevron-left.svg` | Lucide icon "chevron-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevron-right.svg` | Lucide icon "chevron-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevron-up.svg` | Lucide icon "chevron-up" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-up.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevrons-left.svg` | Lucide icon "chevrons-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevrons-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevrons-right.svg` | Lucide icon "chevrons-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevrons-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle-check.svg` | Lucide icon "circle-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle-dashed.svg` | Lucide icon "circle-dashed" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-dashed.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle-dot.svg` | Lucide icon "circle-dot" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-dot.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle-help.svg` | Lucide icon "circle-help" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-help.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle.svg` | Lucide icon "circle" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/clock-3.svg` | Lucide icon "clock-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/clock-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/clock.svg` | Lucide icon "clock" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/clock.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/cloud-upload.svg` | Lucide icon "cloud-upload" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/cloud-upload.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/cloud.svg` | Lucide icon "cloud" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/cloud.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/code-xml.svg` | Lucide icon "code-xml" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/code-xml.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/columns-2.svg` | Lucide icon "columns-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/columns-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/compass.svg` | Lucide icon "compass" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/compass.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/contrast.svg` | Lucide icon "contrast" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/contrast.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/copy-plus.svg` | Lucide icon "copy-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/copy-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/copy.svg` | Lucide icon "copy" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/copy.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/crop.svg` | Lucide icon "crop" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/crop.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/droplet.svg` | Lucide icon "droplet" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/droplet.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/ellipsis-vertical.svg` | Lucide icon "ellipsis-vertical" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/ellipsis-vertical.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/ellipsis.svg` | Lucide icon "ellipsis" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/ellipsis.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/eraser.svg` | Lucide icon "eraser" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/eraser.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/external-link.svg` | Lucide icon "external-link" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/external-link.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/eye-off.svg` | Lucide icon "eye-off" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/eye-off.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/eye.svg` | Lucide icon "eye" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/eye.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-check.svg` | Lucide icon "file-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-down.svg` | Lucide icon "file-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-down.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-input.svg` | Lucide icon "file-input" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-input.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-lock-2.svg` | Lucide icon "file-lock-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-lock-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-minus.svg` | Lucide icon "file-minus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-minus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-output.svg` | Lucide icon "file-output" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-output.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-pen-line.svg` | Lucide icon "file-pen-line" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-pen-line.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-plus-2.svg` | Lucide icon "file-plus-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-plus-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-plus.svg` | Lucide icon "file-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-scan.svg` | Lucide icon "file-scan" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-scan.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-search.svg` | Lucide icon "file-search" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-search.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-symlink.svg` | Lucide icon "file-symlink" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-symlink.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-text.svg` | Lucide icon "file-text" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-text.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-x.svg` | Lucide icon "file-x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/files.svg` | Lucide icon "files" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/files.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/film.svg` | Lucide icon "film" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/film.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/filter.svg` | Lucide icon "filter" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/filter.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/folder-open.svg` | Lucide icon "folder-open" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/folder-open.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/folder-plus.svg` | Lucide icon "folder-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/folder-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/folder.svg` | Lucide icon "folder" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/folder.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/gauge.svg` | Lucide icon "gauge" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/gauge.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/git-compare.svg` | Lucide icon "git-compare" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/git-compare.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/globe.svg` | Lucide icon "globe" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/globe.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/grid-3x3.svg` | Lucide icon "grid-3x3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/grid-3x3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/hand.svg` | Lucide icon "hand" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/hand.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/hash.svg` | Lucide icon "hash" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/hash.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/heading.svg` | Lucide icon "heading" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/heading.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/highlighter.svg` | Lucide icon "highlighter" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/highlighter.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/house.svg` | Lucide icon "house" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/house.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/image-plus.svg` | Lucide icon "image-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/image-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/image.svg` | Lucide icon "image" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/image.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/info.svg` | Lucide icon "info" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/info.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/italic.svg` | Lucide icon "italic" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/italic.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/languages.svg` | Lucide icon "languages" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/languages.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/lasso.svg` | Lucide icon "lasso" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/lasso.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/layers.svg` | Lucide icon "layers" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/layers.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/layout-grid.svg` | Lucide icon "layout-grid" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/layout-grid.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/link-2.svg` | Lucide icon "link-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/link-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/link.svg` | Lucide icon "link" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/link.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/list-checks.svg` | Lucide icon "list-checks" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/list-checks.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/list-ordered.svg` | Lucide icon "list-ordered" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/list-ordered.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/list.svg` | Lucide icon "list" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/list.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/lock-open.svg` | Lucide icon "lock-open" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/lock-open.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/lock.svg` | Lucide icon "lock" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/lock.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/maximize-2.svg` | Lucide icon "maximize-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/maximize-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/maximize.svg` | Lucide icon "maximize" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/maximize.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/message-circle-reply.svg` | Lucide icon "message-circle-reply" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-circle-reply.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/message-square-text.svg` | Lucide icon "message-square-text" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-square-text.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/message-square.svg` | Lucide icon "message-square" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-square.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/messages-square.svg` | Lucide icon "messages-square" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/messages-square.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/mic.svg` | Lucide icon "mic" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/mic.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/minimize.svg` | Lucide icon "minimize" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minimize.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/minimize-2.svg` | Lucide icon "minimize-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minimize-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/minus.svg` | Lucide icon "minus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/moon.svg` | Lucide icon "moon" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/moon.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/mouse-pointer-2.svg` | Lucide icon "mouse-pointer-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/mouse-pointer-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/move.svg` | Lucide icon "move" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/navigation.svg` | Lucide icon "navigation" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/navigation.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/paint-bucket.svg` | Lucide icon "paint-bucket" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/paint-bucket.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/palette.svg` | Lucide icon "palette" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/palette.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/panel-left.svg` | Lucide icon "panel-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/panel-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/panel-right.svg` | Lucide icon "panel-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/panel-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/panels-top-left.svg` | Lucide icon "panels-top-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/panels-top-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/paperclip.svg` | Lucide icon "paperclip" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/paperclip.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pen-line.svg` | Lucide icon "pen-line" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pen-line.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pen-tool.svg` | Lucide icon "pen-tool" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pen-tool.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pencil.svg` | Lucide icon "pencil" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pencil.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pipette.svg` | Lucide icon "pipette" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pipette.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/plus.svg` | Lucide icon "plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/presentation.svg` | Lucide icon "presentation" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/presentation.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/printer.svg` | Lucide icon "printer" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/printer.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rectangle-horizontal.svg` | Lucide icon "rectangle-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rectangle-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/redo-2.svg` | Lucide icon "redo-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/redo-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/replace.svg` | Lucide icon "replace" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/replace.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rotate-ccw.svg` | Lucide icon "rotate-ccw" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rotate-ccw.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rotate-cw.svg` | Lucide icon "rotate-cw" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rotate-cw.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/ruler.svg` | Lucide icon "ruler" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/ruler.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/save.svg` | Lucide icon "save" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/save.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/scan-text.svg` | Lucide icon "scan-text" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/scan-text.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/scan.svg` | Lucide icon "scan" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/scan.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/scissors.svg` | Lucide icon "scissors" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/scissors.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/search.svg` | Lucide icon "search" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/search.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/send.svg` | Lucide icon "send" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/send.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/settings-2.svg` | Lucide icon "settings-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/settings-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/settings.svg` | Lucide icon "settings" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/settings.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/share-2.svg` | Lucide icon "share-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/share-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/shield-check.svg` | Lucide icon "shield-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/shield-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/signature.svg` | Lucide icon "signature" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/signature.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/sliders-horizontal.svg` | Lucide icon "sliders-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/sliders-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/sparkles.svg` | Lucide icon "sparkles" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/sparkles.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/spline.svg` | Lucide icon "spline" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/spline.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/square-dashed-mouse-pointer.svg` | Lucide icon "square-dashed-mouse-pointer" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square-dashed-mouse-pointer.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/square-dashed.svg` | Lucide icon "square-dashed" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square-dashed.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/square.svg` | Lucide icon "square" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/squares-subtract.svg` | Lucide icon "squares-subtract" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/squares-subtract.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/stamp.svg` | Lucide icon "stamp" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/stamp.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/star.svg` | Lucide icon "star" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/star.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/sticky-note.svg` | Lucide icon "sticky-note" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/sticky-note.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/strikethrough.svg` | Lucide icon "strikethrough" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/strikethrough.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/sun.svg` | Lucide icon "sun" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/sun.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/swatch-book.svg` | Lucide icon "swatch-book" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/swatch-book.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/tag.svg` | Lucide icon "tag" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/tag.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/text-cursor-input.svg` | Lucide icon "text-cursor-input" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/text-cursor-input.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/text-select.svg` | Lucide icon "text-select" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/text-select.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/trash-2.svg` | Lucide icon "trash-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/trash-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/trash.svg` | Lucide icon "trash" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/trash.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/triangle-alert.svg` | Lucide icon "triangle-alert" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/triangle-alert.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/type.svg` | Lucide icon "type" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/type.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/underline.svg` | Lucide icon "underline" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/underline.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/undo-2.svg` | Lucide icon "undo-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/undo-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/user-round.svg` | Lucide icon "user-round" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/user-round.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/wand-sparkles.svg` | Lucide icon "wand-sparkles" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/wand-sparkles.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/wrench.svg` | Lucide icon "wrench" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/wrench.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/x.svg` | Lucide icon "x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/zoom-in.svg` | Lucide icon "zoom-in" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/zoom-in.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/zoom-out.svg` | Lucide icon "zoom-out" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/zoom-out.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/square-terminal.svg` | Lucide icon "square-terminal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square-terminal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-code.svg` | Lucide icon "file-code" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-code.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-spreadsheet.svg` | Lucide icon "file-spreadsheet" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-spreadsheet.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-justify.svg` | Lucide icon "align-justify" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-justify.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/align-right.svg` | Lucide icon "align-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/columns-3.svg` | Lucide icon "columns-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/columns-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rows-3.svg` | Lucide icon "rows-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rows-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/flip-horizontal-2.svg` | Lucide icon "flip-horizontal-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/flip-horizontal-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/flip-vertical-2.svg` | Lucide icon "flip-vertical-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/flip-vertical-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/app-window.svg` | Lucide icon "app-window" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/app-window.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/arrow-left-right.svg` | Lucide icon "arrow-left-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/arrow-left-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/arrow-up-down.svg` | Lucide icon "arrow-up-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/arrow-up-down.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/arrow-up-right.svg` | Lucide icon "arrow-up-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/arrow-up-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/badge-check.svg` | Lucide icon "badge-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/badge-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bandage.svg` | Lucide icon "bandage" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bandage.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bell.svg` | Lucide icon "bell" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bell.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/blend.svg` | Lucide icon "blend" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/blend.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bold.svg` | Lucide icon "bold" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bold.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/book-open.svg` | Lucide icon "book-open" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/book-open.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bookmark.svg` | Lucide icon "bookmark" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bookmark.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bookmark-plus.svg` | Lucide icon "bookmark-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bookmark-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/braces.svg` | Lucide icon "braces" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/braces.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/brush-cleaning.svg` | Lucide icon "brush-cleaning" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/brush-cleaning.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/brush.svg` | Lucide icon "brush" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/brush.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/camera.svg` | Lucide icon "camera" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/camera.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/check-circle-2.svg` | Lucide icon "check-circle-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/check-circle-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/check.svg` | Lucide icon "check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevron-down.svg` | Lucide icon "chevron-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-down.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevron-left.svg` | Lucide icon "chevron-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevron-right.svg` | Lucide icon "chevron-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevron-up.svg` | Lucide icon "chevron-up" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevron-up.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevrons-left.svg` | Lucide icon "chevrons-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevrons-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/chevrons-right.svg` | Lucide icon "chevrons-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/chevrons-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle-check.svg` | Lucide icon "circle-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle-dashed.svg` | Lucide icon "circle-dashed" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-dashed.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle-dot.svg` | Lucide icon "circle-dot" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-dot.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle-help.svg` | Lucide icon "circle-help" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle-help.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/circle.svg` | Lucide icon "circle" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/circle.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/clock-3.svg` | Lucide icon "clock-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/clock-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/clock.svg` | Lucide icon "clock" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/clock.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/cloud-upload.svg` | Lucide icon "cloud-upload" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/cloud-upload.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/cloud.svg` | Lucide icon "cloud" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/cloud.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/code-xml.svg` | Lucide icon "code-xml" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/code-xml.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/columns-2.svg` | Lucide icon "columns-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/columns-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/compass.svg` | Lucide icon "compass" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/compass.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/contrast.svg` | Lucide icon "contrast" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/contrast.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/copy-plus.svg` | Lucide icon "copy-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/copy-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/copy.svg` | Lucide icon "copy" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/copy.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/crop.svg` | Lucide icon "crop" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/crop.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/droplet.svg` | Lucide icon "droplet" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/droplet.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/ellipsis-vertical.svg` | Lucide icon "ellipsis-vertical" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/ellipsis-vertical.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/ellipsis.svg` | Lucide icon "ellipsis" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/ellipsis.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/eraser.svg` | Lucide icon "eraser" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/eraser.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/external-link.svg` | Lucide icon "external-link" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/external-link.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/eye-off.svg` | Lucide icon "eye-off" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/eye-off.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/eye.svg` | Lucide icon "eye" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/eye.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-check.svg` | Lucide icon "file-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-down.svg` | Lucide icon "file-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-down.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-input.svg` | Lucide icon "file-input" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-input.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-lock-2.svg` | Lucide icon "file-lock-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-lock-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-minus.svg` | Lucide icon "file-minus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-minus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-output.svg` | Lucide icon "file-output" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-output.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-pen-line.svg` | Lucide icon "file-pen-line" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-pen-line.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-plus-2.svg` | Lucide icon "file-plus-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-plus-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-plus.svg` | Lucide icon "file-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-scan.svg` | Lucide icon "file-scan" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-scan.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-search.svg` | Lucide icon "file-search" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-search.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-symlink.svg` | Lucide icon "file-symlink" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-symlink.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-text.svg` | Lucide icon "file-text" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-text.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/file-x.svg` | Lucide icon "file-x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/files.svg` | Lucide icon "files" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/files.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/film.svg` | Lucide icon "film" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/film.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/filter.svg` | Lucide icon "filter" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/filter.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/folder-open.svg` | Lucide icon "folder-open" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/folder-open.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/folder-plus.svg` | Lucide icon "folder-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/folder-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/folder.svg` | Lucide icon "folder" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/folder.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/gauge.svg` | Lucide icon "gauge" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/gauge.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/git-compare.svg` | Lucide icon "git-compare" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/git-compare.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/globe.svg` | Lucide icon "globe" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/globe.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/grid-3x3.svg` | Lucide icon "grid-3x3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/grid-3x3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/hand.svg` | Lucide icon "hand" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/hand.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/hash.svg` | Lucide icon "hash" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/hash.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/heading.svg` | Lucide icon "heading" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/heading.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/highlighter.svg` | Lucide icon "highlighter" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/highlighter.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/house.svg` | Lucide icon "house" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/house.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/image-plus.svg` | Lucide icon "image-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/image-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/image.svg` | Lucide icon "image" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/image.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/info.svg` | Lucide icon "info" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/info.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/italic.svg` | Lucide icon "italic" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/italic.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/languages.svg` | Lucide icon "languages" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/languages.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/lasso.svg` | Lucide icon "lasso" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/lasso.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/layers.svg` | Lucide icon "layers" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/layers.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/layout-grid.svg` | Lucide icon "layout-grid" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/layout-grid.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/link-2.svg` | Lucide icon "link-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/link-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/link.svg` | Lucide icon "link" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/link.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/list-checks.svg` | Lucide icon "list-checks" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/list-checks.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/list-ordered.svg` | Lucide icon "list-ordered" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/list-ordered.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/list.svg` | Lucide icon "list" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/list.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/lock-open.svg` | Lucide icon "lock-open" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/lock-open.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/lock.svg` | Lucide icon "lock" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/lock.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/maximize-2.svg` | Lucide icon "maximize-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/maximize-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/maximize.svg` | Lucide icon "maximize" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/maximize.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/message-circle-reply.svg` | Lucide icon "message-circle-reply" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-circle-reply.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/message-square-text.svg` | Lucide icon "message-square-text" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-square-text.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/message-square.svg` | Lucide icon "message-square" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/message-square.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/messages-square.svg` | Lucide icon "messages-square" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/messages-square.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/mic.svg` | Lucide icon "mic" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/mic.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/minimize.svg` | Lucide icon "minimize" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minimize.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/minimize-2.svg` | Lucide icon "minimize-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minimize-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/minus.svg` | Lucide icon "minus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/minus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/moon.svg` | Lucide icon "moon" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/moon.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/mouse-pointer-2.svg` | Lucide icon "mouse-pointer-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/mouse-pointer-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/move.svg` | Lucide icon "move" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/navigation.svg` | Lucide icon "navigation" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/navigation.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/paint-bucket.svg` | Lucide icon "paint-bucket" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/paint-bucket.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/palette.svg` | Lucide icon "palette" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/palette.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/panel-left.svg` | Lucide icon "panel-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/panel-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/panel-right.svg` | Lucide icon "panel-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/panel-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/panels-top-left.svg` | Lucide icon "panels-top-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/panels-top-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/paperclip.svg` | Lucide icon "paperclip" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/paperclip.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pen-line.svg` | Lucide icon "pen-line" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pen-line.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pen-tool.svg` | Lucide icon "pen-tool" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pen-tool.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pencil.svg` | Lucide icon "pencil" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pencil.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pipette.svg` | Lucide icon "pipette" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pipette.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/plus.svg` | Lucide icon "plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/presentation.svg` | Lucide icon "presentation" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/presentation.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/printer.svg` | Lucide icon "printer" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/printer.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rectangle-horizontal.svg` | Lucide icon "rectangle-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rectangle-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/redo-2.svg` | Lucide icon "redo-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/redo-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/replace.svg` | Lucide icon "replace" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/replace.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rotate-ccw.svg` | Lucide icon "rotate-ccw" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rotate-ccw.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/rotate-cw.svg` | Lucide icon "rotate-cw" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/rotate-cw.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/ruler.svg` | Lucide icon "ruler" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/ruler.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/save.svg` | Lucide icon "save" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/save.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/scan-text.svg` | Lucide icon "scan-text" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/scan-text.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/scan.svg` | Lucide icon "scan" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/scan.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/scissors.svg` | Lucide icon "scissors" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/scissors.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/search.svg` | Lucide icon "search" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/search.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/send.svg` | Lucide icon "send" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/send.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/settings-2.svg` | Lucide icon "settings-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/settings-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/settings.svg` | Lucide icon "settings" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/settings.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/share-2.svg` | Lucide icon "share-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/share-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/shield-check.svg` | Lucide icon "shield-check" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/shield-check.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/signature.svg` | Lucide icon "signature" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/signature.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/sliders-horizontal.svg` | Lucide icon "sliders-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/sliders-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/sparkles.svg` | Lucide icon "sparkles" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/sparkles.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/spline.svg` | Lucide icon "spline" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/spline.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/square-dashed-mouse-pointer.svg` | Lucide icon "square-dashed-mouse-pointer" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square-dashed-mouse-pointer.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/square-dashed.svg` | Lucide icon "square-dashed" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square-dashed.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/square.svg` | Lucide icon "square" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/squares-subtract.svg` | Lucide icon "squares-subtract" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/squares-subtract.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/stamp.svg` | Lucide icon "stamp" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/stamp.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/star.svg` | Lucide icon "star" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/star.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/sticky-note.svg` | Lucide icon "sticky-note" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/sticky-note.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/strikethrough.svg` | Lucide icon "strikethrough" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/strikethrough.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/sun.svg` | Lucide icon "sun" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/sun.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/swatch-book.svg` | Lucide icon "swatch-book" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/swatch-book.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/tag.svg` | Lucide icon "tag" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/tag.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/text-cursor-input.svg` | Lucide icon "text-cursor-input" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/text-cursor-input.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/text-select.svg` | Lucide icon "text-select" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/text-select.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/trash-2.svg` | Lucide icon "trash-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/trash-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/trash.svg` | Lucide icon "trash" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/trash.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/triangle-alert.svg` | Lucide icon "triangle-alert" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/triangle-alert.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/type.svg` | Lucide icon "type" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/type.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/underline.svg` | Lucide icon "underline" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/underline.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/undo-2.svg` | Lucide icon "undo-2" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/undo-2.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/user-round.svg` | Lucide icon "user-round" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/user-round.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/wand-sparkles.svg` | Lucide icon "wand-sparkles" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/wand-sparkles.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/wrench.svg` | Lucide icon "wrench" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/wrench.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/x.svg` | Lucide icon "x" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/x.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/zoom-in.svg` | Lucide icon "zoom-in" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/zoom-in.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/zoom-out.svg` | Lucide icon "zoom-out" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/zoom-out.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/fonts/Inter-Regular.ttf` | Inter Regular | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels; slanted fallback glyphs in Fill & Sign typed signatures for characters Dancing Script lacks (Cyrillic, Greek), drawn as filled paths |
| `assets/fonts/Inter-Medium.ttf` | Inter Medium | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels |
| `assets/fonts/Inter-SemiBold.ttf` | Inter SemiBold | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/rsms/inter | UI font, embedded in the app; demo PDF labels |
@@ -269,31 +269,31 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `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 |
| `assets/icons/thumbs-up.svg` | Lucide icon "thumbs-up" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/thumbs-up.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `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 |
| `assets/icons/pin.svg` | Lucide icon "pin" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | 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 AND MIT | 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 AND MIT | 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 |
| `assets/icons/thumbs-up.svg` | Lucide icon "thumbs-up" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/thumbs-up.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/thumbs-down.svg` | Lucide icon "thumbs-down" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | 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 AND MIT | 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 AND MIT | 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) | 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/it/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 |
| `assets/icons/pentagon.svg` | Lucide icon "pentagon" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | 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 AND MIT | 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 | 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/it/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/icons/clipboard-paste.svg` | Lucide icon "clipboard-paste" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | 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) |
| `packaging/macos/dmg/background.svg` | PdfCraft macOS DMG window background | @XusBadia | MIT OR Apache-2.0 | Original work for this repository, derived from the PdfCraft app icon (assets/app-icon/pdfcraft.svg, referenced via an SVG image element, not copied) | Finder window of the macOS DMG; rendered to background.tiff by packaging/macos/dmg/generate.py |
| `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile |
| `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/it/pdf.rs): the legacy adbe.x509.rsa_sha1 format |
| `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n.rs |
| `crates/ui-egui/src/i18n/sl.tsv` | Slovene interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Slovene UI strings, compiled in by crates/ui-egui/src/i18n.rs |
| `vendor/vello_common/assets/probe.png` | vello_common probe reference image (rendered from probe.rgba) | Linebender vello contributors | MIT OR Apache-2.0 | vello_common 0.0.8 (crates.io), vendored in vendor/vello_common; regenerated by its vello_sparse_tests regenerate_probe_reference binary | Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file |
| `crates/ui-egui/src/i18n/nb.tsv` | Norwegian Bokmål interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Norwegian Bokmål UI strings, compiled in by crates/ui-egui/src/i18n.rs |
| `assets/icons/superscript.svg` | Lucide icon "superscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/superscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/subscript.svg` | Lucide icon "subscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/subscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pilcrow.svg` | Lucide icon "pilcrow" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pilcrow.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/move-horizontal.svg` | Lucide icon "move-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/superscript.svg` | Lucide icon "superscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/superscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/subscript.svg` | Lucide icon "subscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/subscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/pilcrow.svg` | Lucide icon "pilcrow" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pilcrow.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/move-horizontal.svg` | Lucide icon "move-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC AND MIT | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `crates/ui-egui/src/i18n/sq.tsv` | Albanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-appearance labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Albanian UI strings, compiled in by crates/ui-egui/src/i18n.rs |
| `crates/ui-egui/src/i18n/th.tsv` | Thai interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Thai UI strings, compiled in by crates/ui-egui/src/i18n.rs |
| `crates/ui-egui/src/i18n/ca.tsv` | Catalan interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, written from their meaning; no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 secured-document notice labels are contributor-original AI-assisted translations from English, not separately native-reviewed; the 2026-10-22 reply-count label is an AI-assisted translation from English, not separately native-reviewed | Catalan UI strings, compiled in by crates/ui-egui/src/i18n.rs |
+190 -190
View File
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
# Stage 1 source documents only. Destinations are logical names, not packaging instructions.
# licences lists audited full-text identifiers; omission grants no bundle applicability.
[[document]]
source = "LICENSE-APACHE"
licences = ["Apache-2.0"]
destination = "LICENSE-APACHE"
sha256 = "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594"
[[document]]
source = "LICENSE-MIT"
licences = ["MIT"]
destination = "LICENSE-MIT"
sha256 = "cd7032ea33b71427f188e34ffb4aee9d6868a57fd196037c5c8b2be03a2a0352"
[[document]]
source = "NOTICE"
destination = "NOTICE"
sha256 = "9f854f730719af962cd840529324d089e05e68a30bd245c50e33f96e009b4314"
[[document]]
source = "assets/app-icon/LICENSE-document.txt"
destination = "assets/app-icon/LICENSE-document.txt"
sha256 = "64a3137e3d5cc41e5b78d2ad8dd9ddddd6588283f9f7b3128aa6a8a50852fb9a"
[[document]]
source = "assets/app-icon/LICENSE.txt"
destination = "assets/app-icon/LICENSE.txt"
sha256 = "b768acafe6cbad8247eedf86b5b5381feefcd4ee0f8c7808b86811bc687cf37b"
[[document]]
source = "assets/fonts/OFL-DancingScript.txt"
destination = "assets/fonts/OFL-DancingScript.txt"
sha256 = "6f090277c00af96651ce6dbcc38ff1591047a3bffef486e80b6a32e8276a8201"
[[document]]
source = "assets/fonts/OFL-Inter.txt"
destination = "assets/fonts/OFL-Inter.txt"
sha256 = "262481e844521b326f5ecd053e59b98c8b2da78c8ee1bdbb6e8174305e54935a"
[[document]]
source = "assets/fonts/OFL-JetBrainsMono.txt"
destination = "assets/fonts/OFL-JetBrainsMono.txt"
sha256 = "a76abf002c49097d146e86740a3105a5d00450b1592e820a1109a8c5680cd697"
[[document]]
source = "assets/icons/LICENSE-lucide.txt"
destination = "assets/icons/LICENSE-lucide.txt"
sha256 = "b495047bd93a9b06913511076f504daba17d5bbeb3e0650f3bb53a4220329c57"
[[document]]
source = "docs/brand/LICENSE-brand.txt"
destination = "docs/brand/LICENSE-brand.txt"
sha256 = "0f33f48e7a96cd359d4e4e5b11424b13cebeb59cc70c77928186695e4173585c"
[[document]]
source = "licenses/CMap-LICENSE.txt"
licences = ["BSD-3-Clause"]
destination = "licenses/CMap-LICENSE.txt"
sha256 = "742665db9c8e1bc72603c6d319ca3e90b83bd6d95202f0cd4ef11068a07a9c29"
origin = "https://static.crates.io/crates/hayro-cmap/hayro-cmap-0.1.0.crate#assets/LICENSE.txt"
[[document]]
source = "licenses/egui-Hack-Regular.txt"
licences = ["MIT", "Bitstream-Vera"]
destination = "licenses/egui-Hack-Regular.txt"
sha256 = "47c0cccbeec7e8614548cc485588b28149e7874188df5f41b36efebcee285c87"
origin = "https://raw.githubusercontent.com/emilk/egui/0.36.2/crates/epaint_default_fonts/fonts/Hack-Regular.txt"
[[document]]
source = "licenses/egui-OFL.txt"
licences = ["OFL-1.1"]
destination = "licenses/egui-OFL.txt"
sha256 = "6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2"
origin = "https://raw.githubusercontent.com/emilk/egui/0.36.2/crates/epaint_default_fonts/fonts/OFL.txt"
[[document]]
source = "licenses/egui-UFL.txt"
licences = ["Ubuntu-font-1.0"]
destination = "licenses/egui-UFL.txt"
sha256 = "2f0015108d68627bd788d313f529c21ff4da2c2c42a5e1f3883acc83480f9002"
origin = "https://raw.githubusercontent.com/emilk/egui/0.36.2/crates/epaint_default_fonts/fonts/UFL.txt"
[[document]]
source = "licenses/egui-emoji-icon-font-mit-license.txt"
licences = ["MIT"]
destination = "licenses/egui-emoji-icon-font-mit-license.txt"
sha256 = "b9d2c1d909aa149996fd4c91dcb92b2362a04431640c1d200959da94caf8cde1"
origin = "https://raw.githubusercontent.com/emilk/egui/0.36.2/crates/epaint_default_fonts/fonts/emoji-icon-font-mit-license.txt"
[[document]]
source = "vendor/hayro-interpret/LICENSE-APACHE"
licences = ["Apache-2.0"]
destination = "vendor/hayro-interpret/LICENSE-APACHE"
sha256 = "0cec06e0e55fbc3dc5cee4fca9b607f66cb8f4e4dbcf3b3c013594dd156732e9"
[[document]]
source = "vendor/hayro-interpret/LICENSE-MIT"
licences = ["MIT"]
destination = "vendor/hayro-interpret/LICENSE-MIT"
sha256 = "51b50e6120c344dcc146bbd6375ca1c78e079d97c8bea76e67a53d6ff0efebac"
[[document]]
source = "vendor/hayro-interpret/assets/CGATS_LICENSE.txt"
destination = "vendor/hayro-interpret/assets/CGATS_LICENSE.txt"
sha256 = "469d87b5fa48f8aaf69e25f4fcac3a309c97dbb7d561b917638abdb640e004d9"
[[document]]
source = "vendor/hayro-interpret/assets/LICENSE_FOXIT"
destination = "vendor/hayro-interpret/assets/LICENSE_FOXIT"
sha256 = "b578cdd2345840ada550bd12519533812320d5f1d21cf4c1c7e1b1b0a31c98b7"
[[document]]
source = "vendor/hayro-cmap/assets/LICENSE.txt"
licences = ["BSD-3-Clause"]
destination = "vendor/hayro-cmap/assets/LICENSE.txt"
sha256 = "742665db9c8e1bc72603c6d319ca3e90b83bd6d95202f0cd4ef11068a07a9c29"
[[document]]
source = "vendor/vello_common/LICENSE-MIT"
licences = ["MIT"]
destination = "vendor/vello_common/LICENSE-MIT"
sha256 = "adf157330f722fe7cfd5964a2a6974eff851b718228c6735724a11706e6f4dec"
[[bundled]]
crate = "epaint_default_fonts"
path = "fonts/Ubuntu-Light.ttf"
licence = "Ubuntu-font-1.0"
documents = ["licenses/egui-UFL.txt"]
[[bundled]]
crate = "epaint_default_fonts"
path = "fonts/Hack-Regular.ttf"
licence = "MIT AND Bitstream-Vera"
documents = ["licenses/egui-Hack-Regular.txt"]
[[bundled]]
crate = "epaint_default_fonts"
path = "fonts/NotoEmoji-Regular.ttf"
licence = "OFL-1.1"
documents = ["licenses/egui-OFL.txt"]
[[bundled]]
crate = "epaint_default_fonts"
path = "fonts/emoji-icon-font.ttf"
licence = "MIT"
documents = ["licenses/egui-emoji-icon-font-mit-license.txt"]
[[bundled]]
crate = "epaint_default_fonts"
path = "fonts/egui-icons.ttf"
licence = "MIT"
documents = ["licenses/egui-emoji-icon-font-mit-license.txt"]
[[bundled]]
crate = "hayro-interpret"
path = "src/font/generated/metrics.rs"
licence = "MIT OR Apache-2.0"
documents = ["vendor/hayro-interpret/LICENSE-MIT", "vendor/hayro-interpret/LICENSE-APACHE"]
+1 -1
View File
@@ -12,7 +12,7 @@ Third-party material
This product bundles third-party material under its own licenses, listed below. The per-file
list (author, source, license) is in ATTRIBUTION.md.
- Lucide icons (assets/icons/*.svg) — ISC License. See assets/icons/LICENSE-lucide.txt.
- Lucide icons (assets/icons/*.svg) — ISC and MIT licences (Feather-derived icons). See assets/icons/LICENSE-lucide.txt.
The Windows PDF file icon (assets/app-icon/pdfcraft-document.svg and .ico) adapts Lucide's
file-text outline and combines it with contributor-original artwork. See
assets/app-icon/LICENSE-document.txt (ISC AND (MIT OR Apache-2.0)), packaged as
+2
View File
@@ -140,6 +140,8 @@ Search the whole document as you type, step through matches with <kbd>⌘G</kbd>
## Navigate long documents
Launch with `pdfcraft --two-paged file.pdf` (or `pdfcraft file.pdf --two-paged`) to open each command-line PDF in noncontinuous two-page view. This optional launch override does not change saved default-layout preferences or PDFs restored from the previous session. An explicit `--layout` option takes precedence on the active document regardless of argument order; `--default-layout` retains its existing behavior for subsequently opened documents.
Right-click a PDF tab to close it, reopen the last closed file-backed tab, or reveal its file. Closing still asks about unsaved changes. Reopening reads the saved file from disk; it does not restore discarded edits. Reveal selects the file in Explorer or Finder. On Linux and FreeBSD it requests `org.freedesktop.FileManager1.ShowItems`, falling back to opening the containing directory with `xdg-open` when that service is unavailable. Reopen and Reveal are unavailable on the web; Reveal is also unavailable for pathless documents.
Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) keep you oriented in long documents. Search bookmark titles in the Bookmarks panel to find nested entries even when their parents are collapsed. Matches keep their ancestors for context; Clear restores the unfiltered tree without changing its expansion state.
+3
View File
@@ -213,6 +213,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
## Progress log
- **2026-10-18 (M3, #1354, local candidate):** Integrated --two-paged with current main, retaining upstream modal/picker ownership and single-tab reuse. Native launch/opening/password regressions, negative checks against the old wrapper, visual inspection, formatting, strict scoped Clippy, layers, assets, parity and scoped WASM compilation pass. Three existing socket tests are unavailable in this workspace; one existing network test remains ignored. No push or GitHub merge; estimates unchanged.
- **2026-10-31 (M2, PR #4172 review):** Added same-machine pixel oracles for GBK Song, Hei, Kai and FangSong, including all four subset-prefixed Founder aliases. Reference PDFs use explicit Unicode mappings and independently selected installed faces; font data and rasters remain in memory. All eight cases ran; wrong-glyph and wrong-family production mutations each failed the new assertion and were restored. Validation: 422 renderer unit tests, renderer all-target Clippy with -D warnings, formatting, layers/assets/parity and renderer WASM checks passed; existing optional-patch/WASM warnings and parity notes remain. Estimates unchanged.
- **2026-10-31 (M2, PR #4172):** Integrated main fa5f9f8c, retaining both roadmap histories and raster regression groups. Restored display_config and thumbnail_config imports after the upstream explicit-import migration. Validation: 422 renderer unit tests, engine/UI all-target checks, scoped all-target Clippy with -D warnings, formatting, layers, assets, parity and renderer/engine WASM checks passed. Existing optional-patch/WASM warnings and parity notes remain; no full engine/UI runtime test rerun. Display-only font policy and estimates unchanged.
@@ -805,3 +806,5 @@ PR #2133 retains document-local Pages sidebar focus for Ctrl/Cmd+A and Escape af
## Button export-value refresh (2026-10-30)
PR #1952 retains per-widget checkbox/radio export-value editing in the split form-authoring and Prepare modules. Caption editing remains supported. Tests cover preserved appearances and selection, invalid/locked refusals, data-format round trips, and Apply/Cancel/Undo. Current-head hosted validation is pending; feature estimates are unchanged.
- **2026-10-31 (#1577):** Reconciled the page-shadow preference with split viewer, organizer, settings and control modules, preserving upstream page-gap shadow suppression. CI validation pending.
+20 -28
View File
@@ -1,6 +1,8 @@
//! PdfCraft desktop app.
//!
//! Usage: `pdfcraft [options] [files…]` (`-h` / `--help` prints a short version of this, [`USAGE`]).
//! `--two-paged` opens each command-line PDF in noncontinuous two-page view, without changing defaults.
//! An explicit `--layout` takes precedence for the active document, regardless of argument order.
//! `--create-images [images…]` stages the images in one PDF and asks for the page DPI.
//!
//! View options (applied after the files open; also the seed of the UI control channel):
@@ -40,6 +42,7 @@ mod motion;
mod reopen;
mod settings;
mod single_instance;
mod startup;
mod text_scale;
#[cfg(test)]
#[path = "windows_manifest.rs"]
@@ -63,6 +66,7 @@ Usage: pdfcraft [options] [files…]
Opens the files in the PdfCraft desktop app.
Options:
--two-paged open command-line PDFs in noncontinuous two-page view
--new-window open a window of its own instead of passing the files to a running PdfCraft
--create-images [images…] put the images in one PDF and ask for the page DPI
--control <file> enable the UI control channel (pdfcraft-cli ui --control <file> …)
@@ -218,28 +222,16 @@ fn main() -> eframe::Result {
let _ = std::io::Write::write_fmt(&mut std::io::stderr(), format_args!("pdfcraft: {report}\n"));
}
}));
let mut files = Vec::new();
let mut options: Vec<(String, String)> = Vec::new();
let mut control_file: Option<String> = None;
let mut create_images = false;
let mut new_window = false;
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--version" => return print_and_exit(&format!("pdfcraft {}", pdfcraft_engine::build_info::version())),
// Before any window or log file, so a scripted call returns (#4076).
"-h" | "--help" => return print_and_exit(USAGE),
"--control" => control_file = args.next(),
"--create-images" => create_images = true,
"--new-window" => new_window = true,
flag if flag.starts_with("--") => {
let value = args.next().unwrap_or_default();
options.push((flag.trim_start_matches("--").to_string(), value));
}
_ => files.push(path_from_arg(a)),
}
let mut startup = startup::Arguments::parse(std::env::args().skip(1));
if startup.version {
return print_and_exit(&format!("pdfcraft {}", pdfcraft_engine::build_info::version()));
}
let files = rejoin_split_paths(files, |p| std::path::Path::new(p).is_file());
if startup.help {
return print_and_exit(USAGE);
}
startup.files = rejoin_split_paths(startup.files, |p| std::path::Path::new(p).is_file());
let may_hand_off = startup.may_hand_off();
let startup::Arguments { files, options, control_file, create_images, two_paged, .. } = startup;
let integrated = cfg!(target_os = "macos");
// GNOME on Wayland leaves the window frame to the app (src/decorations.rs).
let window_controls = if cfg!(any(target_os = "macos", target_os = "windows")) { None } else { decorations::app_drawn() };
@@ -260,7 +252,6 @@ fn main() -> eframe::Result {
// Explorer double-click, a file manager or `xdg-open`) hands them to the PdfCraft already
// running and exits (#282, #317, #1726). Any option means the caller wants this launch's own
// window.
let may_hand_off = !new_window && !files.is_empty() && options.is_empty() && control_file.is_none() && !create_images;
// Absolute for this instance too: links relative to a document resolve against its folder,
// never the working directory.
let files = single_instance::absolute(&files);
@@ -270,7 +261,7 @@ fn main() -> eframe::Result {
single_instance::Claim::Alone => None,
};
let choice = renderer_choice(std::env::var("PDFCRAFT_RENDERER").ok().as_deref());
let launch = Launch { files, options, control_file, create_images, integrated, window_controls };
let launch = Launch { files, options, control_file, create_images, integrated, window_controls, two_paged };
// Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one
// that launched us as well as later ones. Lives until the event loop returns.
#[cfg(target_os = "macos")]
@@ -380,6 +371,7 @@ struct Launch {
integrated: bool,
/// The window buttons the app draws, when the desktop leaves the frame to it.
window_controls: Option<pdfcraft_ui_egui::window_controls::Layout>,
two_paged: bool,
}
/// Whether the window is a native Wayland one (not X11 or XWayland).
@@ -400,7 +392,7 @@ fn app_creator<'a>(
instance: Option<&'a single_instance::Server>,
#[cfg(target_os = "macos")] apple_events: &'a apple_events::AppleEvents,
) -> eframe::AppCreator<'a> {
let Launch { files, options, control_file, create_images, integrated, window_controls } = launch;
let Launch { files, options, control_file, create_images, integrated, window_controls, two_paged } = launch;
Box::new(move |cc| {
started.set(true);
let mut app = PdfCraftApp::new();
@@ -467,6 +459,7 @@ fn app_creator<'a>(
&[("folder", &w.folder.display().to_string()), ("error", &w.error)],
);
}
let defer_layout = two_paged && !files.is_empty() && !create_images;
if create_images {
if let Err(e) = app.begin_image_import_paths(&files) {
app.notify(e);
@@ -475,12 +468,11 @@ fn app_creator<'a>(
// With the preference on, last session's files come back first; files named on
// the command line open after them, in front (#442).
app.reopen_last_files(&files);
for f in files {
app.open_path(&f);
}
startup::open_files(&mut app, &files, two_paged);
}
for (k, v) in options {
if let Err(e) = app.set_option(&k, &v) {
let result = if defer_layout && k == "layout" { app.set_launch_layout(&v) } else { app.set_option(&k, &v) };
if let Err(e) = result {
log::warn!("--{k} {v}: {e}");
}
}
+244
View File
@@ -0,0 +1,244 @@
//! Native launch arguments and per-file view overrides (#1354).
use pdfcraft_ui_egui::PdfCraftApp;
#[derive(Default)]
pub(super) struct Arguments {
pub files: Vec<String>,
pub options: Vec<(String, String)>,
pub control_file: Option<String>,
pub create_images: bool,
pub new_window: bool,
pub two_paged: bool,
pub version: bool,
pub help: bool,
}
impl Arguments {
pub fn may_hand_off(&self) -> bool {
!self.new_window && !self.files.is_empty() && self.options.is_empty() && self.control_file.is_none() && !self.create_images && !self.two_paged
}
pub fn parse(args: impl IntoIterator<Item = String>) -> Self {
let mut parsed = Self::default();
let mut args = args.into_iter();
while let Some(a) = args.next() {
match a.as_str() {
"--version" => {
parsed.version = true;
break;
}
"-h" | "--help" => {
parsed.help = true;
break;
}
"--control" => parsed.control_file = args.next(),
"--create-images" => parsed.create_images = true,
"--new-window" => parsed.new_window = true,
"--two-paged" => parsed.two_paged = true,
flag if flag.starts_with("--two-paged=") => {
// It has no value: do not consume the following filename on malformed use.
log::warn!("{flag}: use --two-paged without a value");
}
flag if flag.starts_with("--") => {
let value = args.next().unwrap_or_default();
parsed.options.push((flag.trim_start_matches("--").to_string(), value));
}
_ => parsed.files.push(super::path_from_arg(a)),
}
}
parsed
}
}
pub(super) fn open_files(app: &mut PdfCraftApp, files: &[String], two_paged: bool) {
for path in files {
if two_paged {
// The per-file override wins over initial view without changing saved defaults.
app.open_path_with_display(path, pdfcraft_ui_egui::canvas::PageLayout::TwoUp, false);
} else {
app.open_path(path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Arguments {
Arguments::parse(args.iter().map(|s| s.to_string()))
}
#[test]
fn standalone_flag_keeps_files_in_either_position() {
for args in
[["--two-paged", "first.pdf", "second.pdf"], ["first.pdf", "--two-paged", "second.pdf"], ["first.pdf", "second.pdf", "--two-paged"]]
{
let parsed = parse(&args);
assert!(parsed.two_paged);
assert_eq!(parsed.files, ["first.pdf", "second.pdf"]);
assert!(parsed.options.is_empty());
assert!(!parsed.may_hand_off());
}
let empty = parse(&["--two-paged"]);
assert!(empty.two_paged && empty.files.is_empty());
let normal = parse(&["first.pdf", "second.pdf"]);
assert!(!normal.two_paged);
assert!(normal.may_hand_off());
for args in [
&["--new-window", "first.pdf"][..],
&["--layout", "two-page", "first.pdf"],
&["--control", "control.json", "first.pdf"],
&["--create-images", "first.png"],
] {
assert!(!parse(args).may_hand_off());
}
}
#[test]
fn existing_layout_options_are_preserved_in_either_order() {
for args in [["--two-paged", "--layout", "single", "file.pdf"], ["--layout", "single", "--two-paged", "file.pdf"]] {
let parsed = parse(&args);
assert!(parsed.two_paged);
assert_eq!(parsed.files, ["file.pdf"]);
assert_eq!(parsed.options, [("layout".into(), "single".into())]);
}
let parsed = parse(&["--two-paged", "--default-layout", "two-up", "file.pdf"]);
assert_eq!(parsed.options, [("default-layout".into(), "two-up".into())]);
}
#[test]
fn newer_options_and_version_are_preserved() {
let parsed = parse(&["--two-paged", "--new-window", "--grid-zoom", "150", "--default-mode", "read", "file:///tmp/a%20b.pdf"]);
assert!(parsed.two_paged && parsed.new_window);
assert_eq!(parsed.files, ["/tmp/a b.pdf"]);
assert_eq!(parsed.options, [("grid-zoom".into(), "150".into()), ("default-mode".into(), "read".into())]);
assert!(!parsed.may_hand_off());
let version = parse(&["--two-paged", "--version", "ignored.pdf"]);
assert!(version.version && version.files.is_empty());
}
#[test]
fn help_keeps_the_headless_early_exit() {
for flag in ["-h", "--help"] {
let args = parse(&["--two-paged", flag, "ignored.pdf"]);
assert!(args.help && args.files.is_empty());
}
}
#[test]
fn malformed_boolean_does_not_eat_a_file_or_option() {
let args = parse(&["--two-paged=false", "first.pdf", "--layout", "single", "second.pdf"]);
assert!(!args.two_paged);
assert_eq!(args.files, ["first.pdf", "second.pdf"]);
assert_eq!(args.options, [("layout".into(), "single".into())]);
}
#[test]
fn startup_override_is_per_file_and_does_not_persist() {
use std::io::Write as _;
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let path = std::env::temp_dir().join(format!("pdfcraft-two-paged-{}-{nonce}.pdf", std::process::id()));
let mut file = std::fs::OpenOptions::new().create_new(true).write(true).open(&path).unwrap();
file.write_all(include_bytes!("../../../crates/ui-egui/tests/data/form.pdf")).unwrap();
drop(file);
let second_path = path.with_extension("second.pdf");
std::fs::copy(&path, &second_path).unwrap();
let text = path.to_str().unwrap();
let second = second_path.to_str().unwrap();
for enabled in [false, true] {
let mut app = PdfCraftApp::new();
app.set_option("default-layout", "single").unwrap();
app.open_bytes("restored.pdf", None, include_bytes!("../../../crates/ui-egui/tests/data/form.pdf").to_vec()).unwrap();
let restored = app.views[0].id;
open_files(&mut app, &[text.into(), second.into()], enabled);
assert_eq!(app.views.len(), 3);
assert_eq!(app.views[0].id, restored);
assert_eq!(app.views[0].layout, pdfcraft_ui_egui::canvas::PageLayout::Single);
for view in app.views.iter().skip(1) {
assert_eq!(
view.layout,
if enabled { pdfcraft_ui_egui::canvas::PageLayout::TwoUp } else { pdfcraft_ui_egui::canvas::PageLayout::Single }
);
assert!(!view.continuous());
}
let persisted: serde_json::Value = serde_json::from_str(&app.persist()).unwrap();
assert_eq!(persisted["default_layout"], "single");
assert!(persisted.get("two_paged").is_none());
// Existing --layout applies after the flag to the active document only.
if enabled {
app.set_launch_layout("continuous").unwrap();
} else {
app.set_option("layout", "continuous").unwrap();
}
assert_eq!(app.views[2].layout, pdfcraft_ui_egui::canvas::PageLayout::Single);
assert!(app.views[2].continuous());
app.set_option("default-layout", "two-up").unwrap();
assert_eq!(app.view_defaults.layout, pdfcraft_ui_egui::canvas::PageLayout::TwoUp);
assert!(app.view_defaults.continuous);
open_files(&mut app, &[], enabled);
assert_eq!(app.views.len(), 3);
}
std::fs::remove_file(path).unwrap();
std::fs::remove_file(second_path).unwrap();
}
#[test]
fn explicit_launch_layout_preserves_argument_order_and_active_file() {
use pdfcraft_ui_egui::canvas::PageLayout;
let path = std::env::temp_dir().join(format!("pdfcraft-explicit-layout-{}.pdf", std::process::id()));
std::fs::write(&path, include_bytes!("../../../crates/ui-egui/tests/data/form.pdf")).unwrap();
let second_path = path.with_extension("second.pdf");
std::fs::copy(&path, &second_path).unwrap();
let names = [path.to_str().unwrap(), second_path.to_str().unwrap()];
for prefix in [["--two-paged", "--layout", "single"], ["--layout", "single", "--two-paged"]] {
for count in [1, 2] {
let args = Arguments::parse(prefix.into_iter().chain(names.iter().take(count).copied()).map(str::to_owned));
let mut app = PdfCraftApp::new();
app.set_option("default-layout", "continuous").unwrap();
app.open_bytes("restored.pdf", None, include_bytes!("../../../crates/ui-egui/tests/data/form.pdf").to_vec()).unwrap();
open_files(&mut app, &args.files, args.two_paged);
for (key, value) in args.options {
assert_eq!(key, "layout");
app.set_launch_layout(&value).unwrap();
}
assert_eq!(app.active, Some(count));
assert!(app.views[0].continuous());
for view in app.views.iter().skip(1).take(count - 1) {
assert_eq!(view.layout, PageLayout::TwoUp);
}
assert_eq!(app.views[count].layout, PageLayout::Single);
assert!(!app.views[count].continuous());
assert!(app.view_defaults.continuous);
}
}
std::fs::remove_file(path).unwrap();
std::fs::remove_file(second_path).unwrap();
}
}
#[cfg(test)]
mod repeated_file_tests {
use super::*;
#[test]
fn a_repeated_file_reuses_its_tab_and_receives_the_launch_override() {
use pdfcraft_ui_egui::canvas::PageLayout;
let path = std::env::temp_dir().join(format!("pdfcraft-repeated-startup-{}.pdf", std::process::id()));
std::fs::write(&path, include_bytes!("../../../crates/ui-egui/tests/data/form.pdf")).unwrap();
let name = path.to_str().unwrap().to_string();
let mut app = PdfCraftApp::new();
app.set_option("default-layout", "continuous").unwrap();
app.open_path(&name);
let original = app.views[0].id;
open_files(&mut app, &[name.clone(), name], true);
assert_eq!(app.views.len(), 1);
assert_eq!(app.views[0].id, original);
assert_eq!(app.views[0].layout, PageLayout::TwoUp);
assert!(!app.views[0].continuous());
app.set_launch_layout("single").unwrap();
assert_eq!(app.views[0].layout, PageLayout::Single);
assert!(app.view_defaults.continuous);
std::fs::remove_file(path).unwrap();
}
}
+14
View File
@@ -8,6 +8,15 @@ use super::{
};
use crate::{EditNotice, MarkKind};
/// Whether the edit, or any edit inside a batch, deletes pages.
fn deletes_pages(edit: &Edit) -> bool {
match edit {
Edit::DeletePages { .. } => true,
Edit::Batch { edits, .. } => edits.iter().any(deletes_pages),
_ => false,
}
}
impl Session {
/// Apply an edit. On success the previous state is undoable and the view data is refreshed.
pub fn apply(&mut self, id: DocId, edit: Edit) -> Result<(), EditError> {
@@ -163,6 +172,11 @@ impl Session {
// Signed documents are only ever saved incrementally: an edit that needs a full rewrite
// (applying redactions, changing security, sanitizing) would invalidate the signatures.
// So does any edit to a file rebuilt from object headers: it has no chain to append to.
// Deleted pages remain in earlier incremental revisions (#3407), so rewrite
// unsigned files in full. Signed files must retain their signed bytes.
if !signed && deletes_pages(&edit) {
next.require_full_save();
}
let rewrites = |c: &pdfcraft_cos::Document| c.full_save_required() || c.encryption_changed();
if signed && ((rewrites(&next) && !rewrites(&editor.cos)) || (next.is_modified() && next.revisions().is_empty())) {
return Err(EditError::SignedRewrite(edit.label().to_string()));
+61 -1
View File
@@ -967,9 +967,10 @@ fn revert_preserves_the_saved_pages_after_undo() {
#[test]
fn full_save_is_readable_and_smaller_after_deletes() {
let (mut s, id) = session_with(5);
let original = s.save_bytes(id).unwrap();
s.apply(id, Edit::DeletePages { pages: vec![0, 1, 2] }).unwrap();
let full = s.save_full_bytes(id).unwrap();
assert!(full.len() < s.save_bytes(id).unwrap().len());
assert!(full.len() < original.len());
let mut s2 = Session::new();
let id2 = s2.open("full.pdf", None, full, None).unwrap();
assert_eq!(page_texts(&s2, id2), ["Page 4", "Page 5"]);
@@ -9912,3 +9913,62 @@ fn repeated_backdrop_artwork_survives_background_undo_redo_and_both_save_modes()
s.apply(rewritten, Edit::RemoveMarks { kind: MarkKind::Background }).unwrap();
black(&s, rewritten);
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
#[test]
fn saving_after_deleting_pages_rewrites_the_file_without_them() {
let (mut s, id) = session_with(6);
let original = s.save_bytes(id).unwrap();
s.apply(id, Edit::DeletePages { pages: vec![1, 2, 3, 4] }).unwrap();
let saved = s.save_bytes(id).unwrap();
assert!(saved.len() < original.len(), "the file shrank: {} -> {}", original.len(), saved.len());
assert!(!saved.starts_with(original.as_slice()), "a full rewrite, not an update appended to the old file");
assert!(!contains(&saved, b"Page 3"), "a deleted page's text is not left in an earlier revision");
let mut again = Session::new();
let id2 = again.open("saved.pdf", None, saved, None).unwrap();
assert_eq!(page_texts(&again, id2), ["Page 1", "Page 6"]);
}
#[test]
fn saving_after_deleting_pages_from_a_signed_file_stays_incremental() {
let p12 = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12")).unwrap();
let digital_id = sign::pkcs12::open(&p12, "test").unwrap();
let mut s = Session::new().with_clock(|| 1_800_000_000);
let id = s.open("fixture.pdf", None, Arc::new(fixture(3)), None).unwrap();
let opts = SignOptions { page: 0, rect: Some([20.0, 20.0, 160.0, 50.0]), ..SignOptions::default() };
let signed = s.sign(id, &digital_id, opts).unwrap();
s.mark_signed(id, signed.clone(), None).unwrap();
s.apply(id, Edit::DeletePages { pages: vec![2] }).unwrap();
let saved = s.save_bytes(id).unwrap();
assert!(saved.starts_with(signed.as_ref()), "the signed bytes stay an incremental update");
assert!(s.get(id).unwrap().is_signed());
}
#[test]
fn saving_after_a_batch_that_deletes_pages_rewrites_the_file() {
let (mut s, id) = session_with(6);
let original = s.save_bytes(id).unwrap();
let batch = Edit::Batch {
label: "Delete and rotate".into(),
edits: vec![
Edit::Batch { label: "Delete pages".into(), edits: vec![Edit::DeletePages { pages: vec![1, 2, 3, 4] }] },
Edit::RotatePages { pages: vec![0], degrees: 90 },
],
};
s.apply(id, batch).unwrap();
let saved = s.save_bytes(id).unwrap();
assert!(!saved.starts_with(original.as_slice()), "a full rewrite, not an update appended to the old file");
assert!(!contains(&saved, b"Page 3"), "a deleted page's text is not left in an earlier revision");
}
#[test]
fn saving_without_deleting_pages_stays_incremental() {
let (mut s, id) = session_with(3);
let original = s.save_bytes(id).unwrap();
s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 }).unwrap();
let saved = s.save_bytes(id).unwrap();
assert!(saved.starts_with(original.as_slice()), "an edit that deletes nothing is still appended");
}
+5
View File
@@ -91,6 +91,7 @@ impl PdfCraftApp {
// A fresh install follows the desktop's appearance (#1248); saved settings override.
theme_preference: ThemePreference::System,
reading_colors: Default::default(),
show_page_edge_shadows: true,
#[cfg(target_os = "linux")]
desktop_theme: None,
#[cfg(target_os = "linux")]
@@ -133,6 +134,10 @@ impl PdfCraftApp {
pending_opens: Vec::new(),
#[cfg(not(target_arch = "wasm32"))]
next_open_token: 0,
#[cfg(not(target_arch = "wasm32"))]
launch_layout: None,
#[cfg(not(target_arch = "wasm32"))]
launch_display_target: None,
#[cfg(all(test, not(target_arch = "wasm32")))]
open_prepare_gate: None,
pinned: Default::default(),
+3
View File
@@ -32,6 +32,9 @@ impl PdfCraftApp {
};
self.set_theme_preference(preference);
}
("show_page_edge_shadows", _) => {
self.show_page_edge_shadows = value.parse::<bool>().map_err(|_| "show_page_edge_shadows must be true or false")?;
}
("scroll_speed", _) => {
self.scroll_speed = scroll_speed::ScrollSpeed::parse(value).ok_or("scroll_speed must be slow, normal, or fast")?;
}
+101 -4
View File
@@ -117,6 +117,7 @@ impl PdfCraftApp {
after_open: request.after_open,
recovered: request.recovered,
then: request.then,
display_override: None,
});
Ok(token)
}
@@ -147,6 +148,7 @@ impl PdfCraftApp {
// earlier waiting replies, without consuming a blocked receiver's result.
waiting.append(&mut self.pending_opens);
self.pending_opens = waiting;
self.settle_launch_layout();
}
#[cfg(not(target_arch = "wasm32"))]
@@ -182,6 +184,12 @@ impl PdfCraftApp {
if let Some(destination) = pending.after_open {
self.apply_remote_destination(destination);
}
if let Some((layout, continuous)) = pending.display_override
&& let Some(view) = self.views.iter_mut().find(|view| view.id == id)
{
view.set_display(layout, continuous);
self.launch_display_target = Some(id);
}
if let Some(meta) = &pending.recovered {
self.finish_recovery(meta);
}
@@ -205,6 +213,7 @@ impl PdfCraftApp {
recovered: pending.recovered,
then: pending.then,
after_open: pending.after_open,
display_override: pending.display_override,
});
}
Err(error) => self.notify_fmt("Couldn't open {name}: {e}", &[("name", &pending.name), ("e", &error.to_string())]),
@@ -239,6 +248,7 @@ impl PdfCraftApp {
recovered: None,
then: None,
after_open: None,
display_override: None,
});
return Ok(());
}
@@ -416,6 +426,8 @@ impl PdfCraftApp {
if let Some(meta) = p.recovered {
self.recoverable.push(meta);
}
#[cfg(not(target_arch = "wasm32"))]
self.settle_launch_layout();
return;
};
#[cfg(not(target_arch = "wasm32"))]
@@ -431,6 +443,11 @@ impl PdfCraftApp {
recovered: p.recovered.clone(),
then: p.then,
});
if let Ok(token) = &result
&& let Some(pending) = self.pending_opens.iter_mut().find(|pending| pending.token == *token)
{
pending.display_override = p.display_override;
}
if let Err(error) = result {
let mut prompt = p;
prompt.error = Some(error);
@@ -458,6 +475,15 @@ impl PdfCraftApp {
if let Some(destination) = after_open {
self.apply_remote_destination(destination);
}
if let Some((layout, continuous)) = p.display_override
&& let Some(view) = self.views.last_mut()
{
view.set_display(layout, continuous);
#[cfg(not(target_arch = "wasm32"))]
{
self.launch_display_target = Some(view.id);
}
}
if let Some(meta) = &p.recovered {
self.finish_recovery(meta);
}
@@ -472,9 +498,12 @@ impl PdfCraftApp {
prompt.recovered = p.recovered;
prompt.then = p.then;
prompt.after_open = after_open;
prompt.display_override = p.display_override;
}
}
}
#[cfg(not(target_arch = "wasm32"))]
self.settle_launch_layout();
}
/// Ask for the active document's permissions password, then run `then` (if any).
@@ -533,17 +562,37 @@ impl PdfCraftApp {
/// the file opened, focused its tab, was handed to the background worker, or failed.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn open_path_then(&mut self, path: &str, then: Option<&'static str>) -> OpenOutcome {
self.open_path_request(path, then, None)
}
#[cfg(not(target_arch = "wasm32"))]
fn open_path_request(&mut self, path: &str, then: Option<&'static str>, display_override: Option<(canvas::PageLayout, bool)>) -> OpenOutcome {
// One file, one tab (#2166): a file that is open already comes to the front instead,
// and the tool it was opened for runs on it. One still being opened is left to that.
if let Some(i) = self.tab_showing(path) {
self.startup_superseded = true;
self.active = Some(i);
if let Some((layout, continuous)) = display_override
&& let Some(view) = self.views.get_mut(i)
{
view.set_display(layout, continuous);
self.launch_display_target = Some(view.id);
}
if let Some(command) = then {
self.execute(command);
}
return OpenOutcome::Focused;
}
if self.pending_opens.iter().any(|p| p.path.as_deref().is_some_and(|p| same_file(p, path))) {
if let Some(pending) = self.pending_opens.iter_mut().find(|p| p.path.as_deref().is_some_and(|p| same_file(p, path))) {
if display_override.is_some() {
pending.display_override = display_override;
}
return OpenOutcome::Pending;
}
if display_override.is_some()
&& let Some(prompt) = self.password_prompt.as_mut().filter(|p| p.path.as_deref().is_some_and(|p| same_file(p, path)))
{
prompt.display_override = display_override;
return OpenOutcome::Pending;
}
let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| path.to_string());
@@ -552,18 +601,29 @@ impl PdfCraftApp {
// Before the first frame there is no interaction to keep responsive; this also preserves
// deterministic inline startup and test flows. Once live, reading and preparation use
// the bounded worker and commit on the UI thread.
if self.ctx.is_none() || self.run_inline {
// Keep flagged launch files behind an existing password prompt or picker.
if (self.ctx.is_none() || self.run_inline) && !(display_override.is_some() && (self.modal_open() || self.pickers.pending())) {
match std::fs::read(path) {
Ok(bytes) => {
let before = self.views.len();
let previous_prompt = self.password_prompt.as_ref().map(|p| p.bytes.clone());
if let Err(error) = self.open_bytes(&name, Some(path.to_string()), bytes) {
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &name), ("e", &error)]);
return OpenOutcome::Failed;
}
if let Some(prompt) = self.password_prompt.as_mut().filter(|p| p.path.as_deref() == Some(path)) {
if let Some(prompt) = self.password_prompt.as_mut().filter(|p| {
p.path.as_deref() == Some(path) && previous_prompt.as_ref().is_none_or(|bytes| !std::sync::Arc::ptr_eq(bytes, &p.bytes))
}) {
// Encrypted: the tool runs once the password is in.
prompt.then = then;
prompt.display_override = display_override;
} else if self.views.len() > before {
if let Some((layout, continuous)) = display_override
&& let Some(view) = self.views.last_mut()
{
view.set_display(layout, continuous);
self.launch_display_target = Some(view.id);
}
if let Some(doc) = self.views.last().map(|view| view.id) {
self.watch.known_as(doc, stamp);
}
@@ -595,7 +655,12 @@ impl PdfCraftApp {
recovered: None,
then,
}) {
Ok(_) => OpenOutcome::Pending,
Ok(token) => {
if let Some(pending) = self.pending_opens.iter_mut().find(|pending| pending.token == token) {
pending.display_override = display_override;
}
OpenOutcome::Pending
}
Err(error) => {
self.notify_fmt("Couldn't open {name}: {e}", &[("name", &name), ("e", &error)]);
OpenOutcome::Failed
@@ -604,6 +669,38 @@ impl PdfCraftApp {
}
}
/// Apply an explicit launch layout after the command-line display overrides settle.
#[cfg(not(target_arch = "wasm32"))]
pub fn set_launch_layout(&mut self, value: &str) -> Result<(), String> {
let (layout, continuous) = canvas::parse_display_token(value).ok_or("layout must be single, continuous, two-up or two-page")?;
self.launch_layout = Some((layout, continuous));
self.settle_launch_layout();
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn settle_launch_layout(&mut self) {
if self.pending_opens.iter().any(|open| open.display_override.is_some())
|| self.password_prompt.as_ref().is_some_and(|prompt| prompt.display_override.is_some())
{
return;
}
// Follow the existing completion/activation order, not a guessed tab index or path.
// Failed or cancelled opens cannot cause the layout to land on a restored document.
if let Some((layout, continuous)) = self.launch_layout.take()
&& let Some(id) = self.launch_display_target
&& let Some(view) = self.views.iter_mut().find(|view| view.id == id)
{
view.set_display(layout, continuous);
}
}
/// Open one native file with a transient display override, including after a password prompt.
#[cfg(not(target_arch = "wasm32"))]
pub fn open_path_with_display(&mut self, path: &str, layout: canvas::PageLayout, continuous: bool) {
self.open_path_request(path, None, Some((layout, continuous)));
}
/// The File ▸ Open picker: PDFs, and images and text that become PDFs.
#[cfg(not(target_arch = "wasm32"))]
pub(super) fn open_picker() -> rfd::AsyncFileDialog {
+4
View File
@@ -144,6 +144,7 @@ impl PdfCraftApp {
v["pinned_tools"] = serde_json::json!(self.pinned_tools);
// Set outside json! above, which is at serde_json's macro recursion limit.
v["hide_empty_highlight_hover"] = serde_json::Value::Bool(self.comment_prefs.hide_empty_highlight_hover);
v["show_page_edge_shadows"] = self.show_page_edge_shadows.into();
v["comment_tool_pinned"] = serde_json::Value::Bool(self.comment_prefs.pinned);
v["show_secured_notice"] = serde_json::Value::Bool(self.show_secured_notice);
v["organize_zoom"] = serde_json::json!(self.organize_zoom);
@@ -280,6 +281,9 @@ impl PdfCraftApp {
{
self.fill_text_color = color;
}
if let Some(show) = v["show_page_edge_shadows"].as_bool() {
self.show_page_edge_shadows = show;
}
if let Some(prefs) = RedactPrefs::from_json(&v["redact_prefs"]) {
self.redact_prefs = prefs.clone();
self.redact_defaults = prefs;
+9 -2
View File
@@ -1309,6 +1309,7 @@ struct PageEnv<'a> {
modal: bool,
navigation_enabled: bool,
paper: Color32,
show_page_edge_shadows: bool,
ppp: f32,
precise_scroll: Vec2,
prefs: &'a comments::CommentPrefs,
@@ -1465,7 +1466,7 @@ fn paint_page_surface(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFra
let info = frame.env.info;
let painter = ui.painter();
// Shadows would darken the preceding page when pages touch.
if view.show_page_gaps {
if view.show_page_gaps && frame.env.show_page_edge_shadows {
painter.add(egui::epaint::Shadow { offset: [0, 3], blur: 14, spread: 0, color: t.page_shadow }.as_shape(r, CornerRadius::ZERO));
}
painter.rect_filled(r, CornerRadius::ZERO, paper);
@@ -3678,7 +3679,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
if view.organize {
// One remembered grid size for every document, kept in the settings.
view.set_grid_zoom(organize_zoom);
organize_grid(view, info, doc.allows_assembly(), doc.dirty, unobstructed, ui, &t);
organize_grid(view, doc, unobstructed, app.show_page_edge_shadows, ui, &t);
app.organize_zoom = view.grid_zoom();
return;
}
@@ -3833,6 +3834,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
modal,
navigation_enabled,
paper,
show_page_edge_shadows: app.show_page_edge_shadows,
ppp,
precise_scroll,
prefs,
@@ -3859,9 +3861,14 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
can_paste_image,
};
let mut page_out = PageOut::default();
let fade_strength = ui.spacing().scroll.fade.strength;
if !app.show_page_edge_shadows {
ui.spacing_mut().scroll.fade.strength = 0.0;
}
let out = page_scroll(ui, scroll, egui::Id::unique(("page-view-scope", view.id)), |ui, viewport| {
draw_pages(ui, viewport, view, &env, &mut page_out, &mut app.signature_preview)
});
ui.spacing_mut().scroll.fade.strength = fade_strength;
let PageOut {
canvas_action,
clicked_link,
+19 -2
View File
@@ -267,7 +267,17 @@ fn request_sharper(
}
}
pub(super) fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool, unobstructed: bool, ui: &mut egui::Ui, t: &Tokens) {
pub(super) fn organize_grid(
view: &mut DocView,
doc: &pdfcraft_engine::Document,
unobstructed: bool,
show_page_edge_shadows: bool,
ui: &mut egui::Ui,
t: &Tokens,
) {
let info = &doc.info;
let editable = doc.allows_assembly();
let dirty = doc.dirty;
let ppp = ui.ctx().pixels_per_point();
// The page image scales with the zoom; the padding and the page number don't.
let cell = vec2(146.0 * view.grid_zoom + 44.0, 194.0 * view.grid_zoom + 88.0);
@@ -290,6 +300,10 @@ pub(super) fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool,
let (mut in_view, mut sharper): (Vec<(usize, f32)>, Vec<RenderRequest>) = (Vec::new(), Vec::new());
// The source cell can scroll out of the virtualized rows during a drag.
let mut drop = view.org_drag.is_some() && ui.input(|i| i.pointer.primary_released());
let fade_strength = ui.spacing().scroll.fade.strength;
if !show_page_edge_shadows {
ui.spacing_mut().scroll.fade.strength = 0.0;
}
egui::ScrollArea::vertical().auto_shrink([false, false]).show_viewport(ui, |ui, clip| {
apply_auto_scroll(ui, middle_gesture, auto_delta);
ui.add_space(20.0);
@@ -329,7 +343,9 @@ pub(super) fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool,
if view.selected.contains(&i) {
ui.painter().rect_stroke(c.shrink(6.0), CornerRadius::same(8), Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
}
ui.painter().rect_filled(pr.translate(vec2(0.0, 1.5)), CornerRadius::same(1), t.page_shadow);
if show_page_edge_shadows {
ui.painter().rect_filled(pr.translate(vec2(0.0, 1.5)), CornerRadius::same(1), t.page_shadow);
}
ui.painter().rect_filled(pr, CornerRadius::ZERO, Color32::WHITE);
// A page drawn larger than its thumbnail gets a render at the size drawn, while
// it is in view; until that arrives the thumbnail stands in.
@@ -507,6 +523,7 @@ pub(super) fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool,
view.org_drag = None;
}
});
ui.spacing_mut().scroll.fade.strength = fade_strength;
// Files dragged in from outside go to the gap under the pointer. Some platforms don't say
// where the pointer is until the files are dropped; then the whole grid is the target.
let pointer = ui.input(|i| i.pointer.hover_pos()).filter(|p| viewport.contains(*p));
+2
View File
@@ -28,6 +28,8 @@
//! comments, selecting text, moving comments). `ui.state` reports `pages_on_screen` to aim at.
//! - `ui.type {text}`, `ui.key {key, modifiers?}`: keyboard input to the focused widget / app.
//! - `ui.command {id}`: run a registry command (as the menu would). `ui.commands` lists them.
//! - `ui.set {key: "show_page_edge_shadows", value: "true"|"false"}` controls page
//! decoration app-wide; `ui.state.show_page_edge_shadows` reports the saved preference.
//! - `ui.set {key, value}`: view options (`--page`, `--zoom`, …), plus persistent preferences.
//! `select` accepts physical page numbers and ranges such as `1,12,16-18`; blank clears the
//! explicit selection. This is the same bounded parser as the Organize Pages field; invalid
+1
View File
@@ -141,6 +141,7 @@ impl Host for crate::PdfCraftApp {
"palette_open": self.palette_open,
"theme": format!("{:?}", self.theme),
"theme_preference": self.theme_preference,
"show_page_edge_shadows": self.show_page_edge_shadows,
"language": self.language,
"notice": self.toast.as_ref().map(|t| t.0.clone()),
"progress": self.progress_notice.as_ref().map(|p| json!({ "label": p.label, "fraction": p.fraction })),
+1
View File
@@ -2880,3 +2880,4 @@ certificate Version الإصدار
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply خيارات هذا الرد
Export value قيمة التصدير
Show Page Edge Shadows إظهار ظلال حواف الصفحات
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2568,3 +2568,4 @@ animation effects Off Изключени
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Опции за този отговор
Export value Стойност за експортиране
Show Page Edge Shadows Показване на сенки по краищата на страниците
Can't render this file because it contains an unexpected character in line 8 and column 16.
+1
View File
@@ -2912,3 +2912,4 @@ certificate Version Verze
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Možnosti této odpovědi
Export value Exportní hodnota
Show Page Edge Shadows Zobrazovat stíny okrajů stránek
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2606,3 +2606,4 @@ animation effects Off Aus
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Optionen für diese Antwort
Export value Exportwert
Show Page Edge Shadows Schatten an Seitenrändern anzeigen
Can't render this file because it contains an unexpected character in line 6 and column 51.
+1
View File
@@ -2611,3 +2611,4 @@ animation effects Off Ανενεργά
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Επιλογές για αυτήν την απάντηση
Export value Τιμή εξαγωγής
Show Page Edge Shadows Εμφάνιση σκιών στα άκρα των σελίδων
Can't render this file because it contains an unexpected character in line 485 and column 9.
+1
View File
@@ -2928,3 +2928,4 @@ theme Appearance Aspecto
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Opciones de esta respuesta
Export value Valor de exportación
Show Page Edge Shadows Mostrar sombras en los bordes de las páginas
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -2870,3 +2870,4 @@ certificate Version Version
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Options de cette réponse
Export value Valeur d’exportation
Show Page Edge Shadows Afficher les ombres des bords des pages
Can't render this file because it contains an unexpected character in line 6 and column 39.
+1
View File
@@ -2604,3 +2604,4 @@ animation effects Off Ki
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply A válasz beállításai
Export value Exportérték
Show Page Edge Shadows Oldalszéli árnyékok megjelenítése
Can't render this file because it contains an unexpected character in line 7 and column 44.
+1
View File
@@ -2837,3 +2837,4 @@ certificate SHA-256 digest Impronta SHA-256
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Opzioni per questa risposta
Export value Valore di esportazione
Show Page Edge Shadows Mostra le ombre ai bordi delle pagine
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2605,3 +2605,4 @@ animation effects Off オフ
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply この返信のオプション
Export value 書き出し値
Show Page Edge Shadows ページ端の影を表示
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2603,3 +2603,4 @@ animation effects Off Өшірулі
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Осы жауаптың параметрлері
Export value Экспорт мәні
Show Page Edge Shadows Бет жиектерінің көлеңкелерін көрсету
Can't render this file because it contains an unexpected character in line 881 and column 9.
+1
View File
@@ -2865,3 +2865,4 @@ theme Appearance Išvaizda
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Šio atsakymo parinktys
Export value Eksporto reikšmė
Show Page Edge Shadows Rodyti puslapių kraštų šešėlius
Can't render this file because it contains an unexpected character in line 404 and column 9.
+1
View File
@@ -2701,3 +2701,4 @@ animation effects Off Wyłączone
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Opcje tej odpowiedzi
Export value Wartość eksportowa
Show Page Edge Shadows Pokaż cienie krawędzi stron
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2890,3 +2890,4 @@ theme Appearance Aparência
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Opções desta resposta
Export value Valor de exportação
Show Page Edge Shadows Mostrar sombras nas bordas das páginas
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2518,3 +2518,4 @@ animation effects Off Выкл
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Параметры этого ответа
Export value Значение экспорта
Show Page Edge Shadows Показывать тени по краям страниц
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2610,3 +2610,4 @@ animation effects Off Izklopljeni
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Možnosti tega odgovora
Export value Izvozna vrednost
Show Page Edge Shadows Prikaži sence robov strani
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2514,3 +2514,4 @@ animation effects Off ఆఫ్
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply ఈ ప్రత్యుత్తరం ఎంపికలు
Export value ఎగుమతి విలువ
Show Page Edge Shadows పేజీ అంచుల నీడలను చూపించు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+1
View File
@@ -2601,3 +2601,4 @@ animation effects Off Kapalı
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Bu yanıtın seçenekleri
Export value Dışa aktarma değeri
Show Page Edge Shadows Sayfa kenarı gölgelerini göster
Can't render this file because it contains an unexpected character in line 880 and column 9.
+1
View File
@@ -2627,3 +2627,4 @@ animation effects Off Вимкнено
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Параметри цієї відповіді
Export value Значення експорту
Show Page Edge Shadows Показувати тіні на краях сторінок
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2872,3 +2872,4 @@ key Space Space
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply Tùy chọn cho câu trả lời này
Export value Giá trị xuất
Show Page Edge Shadows Hiển thị bóng viền trang
Can't render this file because it contains an unexpected character in line 490 and column 12.
+1
View File
@@ -2569,3 +2569,4 @@ animation effects Off 关
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply 此回复的选项
Export value 导出值
Show Page Edge Shadows 显示页面边缘阴影
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -2555,3 +2555,4 @@ animation effects Off 關
# Comments panel: each reply's own "…" menu (AI-assisted, not separately native-reviewed)
Options for this reply 此回覆的選項
Export value 匯出值
Show Page Edge Shadows 顯示頁面邊緣陰影
Can't render this file because it contains an unexpected character in line 9 and column 16.
+8
View File
@@ -245,6 +245,7 @@ struct PendingOpen {
after_open: Option<pdfcraft_render::RemoteDestination>,
recovered: Option<RecoveryMeta>,
then: Option<&'static str>,
display_override: Option<(canvas::PageLayout, bool)>,
}
pub struct PdfCraftApp {
@@ -297,6 +298,8 @@ pub struct PdfCraftApp {
/// The reading-colours preference (Preferences ▸ Reading colours): applied to every open
/// document, and documents open with it (Acrobat's Replace Document Colors).
pub reading_colors: pdfcraft_render::ReadingColors,
/// App-wide page decoration only; never part of a PDF edit or render request.
pub show_page_edge_shadows: bool,
/// The Linux portal is enabled only by the native host, and starts on the first System choice.
#[cfg(target_os = "linux")]
desktop_theme: Option<pdfcraft_platform::desktop_theme::DesktopTheme>,
@@ -374,6 +377,11 @@ pub struct PdfCraftApp {
pending_opens: Vec<PendingOpen>,
#[cfg(not(target_arch = "wasm32"))]
next_open_token: u64,
/// Explicit CLI layout waits for flagged launch opens, never targeting restored tabs.
#[cfg(not(target_arch = "wasm32"))]
launch_layout: Option<(canvas::PageLayout, bool)>,
#[cfg(not(target_arch = "wasm32"))]
launch_display_target: Option<DocId>,
#[cfg(all(test, not(target_arch = "wasm32")))]
open_prepare_gate: Option<std::sync::Arc<open_worker::PrepareGate>>,
/// Folders pinned to Home, and what they held when last listed.
@@ -733,6 +733,146 @@ fn startup_files_wait_for_absolute_link_consent_and_preparation() {
let _ = std::fs::remove_file(path);
}
#[test]
fn launch_layout_waits_for_cli_opens_and_preserves_restored_tabs() {
use crate::canvas::PageLayout;
// Controlled worker replies exercise both completion orders without scheduling races.
for reverse in [false, true] {
for count in [1, 2] {
for restored in [false, true] {
let mut app = PdfCraftApp::new();
app.set_option("default-layout", "continuous").unwrap();
if restored {
app.open_bytes("restored.pdf", None, fixture(1)).unwrap();
}
let offset = usize::from(restored);
let mut replies = Vec::new();
for token in 0..count {
let name = format!("cli-{token}.pdf");
let bytes = Arc::new(fixture(2));
let prepared = app.session.prepare_open(&name, None, bytes.clone(), None);
let (sender, receiver) = std::sync::mpsc::channel();
app.pending_opens.push(crate::PendingOpen {
token,
receiver,
name,
path: None,
stamp: None,
activate: true,
after_open: None,
recovered: None,
then: None,
display_override: Some((PageLayout::TwoUp, false)),
});
replies.push((
sender,
open_worker::ResultMessage { token, kind: open_worker::ResultKind::Prepared { bytes, result: Box::new(prepared) } },
));
}
app.set_launch_layout("single").unwrap();
if restored {
assert!(app.views[0].continuous(), "pending --layout must not target the restored tab");
}
if reverse {
replies.reverse();
}
for (index, (sender, reply)) in replies.into_iter().enumerate() {
let name = format!("cli-{}.pdf", reply.token);
sender.send(reply).unwrap();
app.poll_pending_opens();
let view = &app.views[index + offset];
assert_eq!(view.layout, if index + 1 == count as usize { PageLayout::Single } else { PageLayout::TwoUp });
assert!(!view.continuous());
assert_eq!(app.session.get(view.id).unwrap().name, name);
}
assert_eq!(app.active, Some(count as usize + offset - 1));
if restored {
assert_eq!(app.views[0].layout, PageLayout::Single);
assert!(app.views[0].continuous());
}
assert_eq!(app.view_defaults.layout, PageLayout::Single);
assert!(app.view_defaults.continuous);
assert_eq!(serde_json::from_str::<Value>(&app.persist()).unwrap()["default_layout"], "continuous");
// Ordinary later opens cannot inherit either launch override.
app.open_bytes("later.pdf", None, fixture(1)).unwrap();
assert!(app.views.last().unwrap().continuous());
}
}
}
}
#[test]
fn launch_layout_failed_or_cancelled_opens_do_not_target_existing_documents() {
use crate::canvas::PageLayout;
for cancel in [false, true] {
let mut app = PdfCraftApp::new();
app.set_option("default-layout", "continuous").unwrap();
app.open_bytes("restored.pdf", None, fixture(1)).unwrap();
let (sender, receiver) = std::sync::mpsc::channel();
app.pending_opens.push(crate::PendingOpen {
token: 0,
receiver,
name: "cli.pdf".into(),
path: None,
stamp: None,
activate: true,
after_open: None,
recovered: None,
then: None,
display_override: Some((PageLayout::TwoUp, false)),
});
app.set_launch_layout("single").unwrap();
assert!(app.set_launch_layout("invalid").is_err());
let kind = if cancel {
open_worker::ResultKind::Prepared { bytes: Arc::new(fixture(1)), result: Box::new(Err(pdfcraft_render::OpenError::NeedsPassword)) }
} else {
open_worker::ResultKind::ReadFailed("synthetic read failure".into())
};
sender.send(open_worker::ResultMessage { token: 0, kind }).unwrap();
app.poll_pending_opens();
if cancel {
app.submit_password(None);
}
assert!(app.launch_layout.is_none());
assert_eq!(app.views.len(), 1);
assert!(app.views[0].continuous());
app.open_bytes("later.pdf", None, fixture(1)).unwrap();
assert!(app.views[1].continuous());
}
}
#[test]
fn flagged_startup_opens_wait_for_password_ownership_before_the_first_frame() {
use crate::canvas::PageLayout;
let locked = tool_open_file("launch-locked", "pdf", &encrypted_tool_fixture());
let second = tool_open_file("launch-second", "pdf", &fixture(2));
let mut app = PdfCraftApp::new();
app.set_option("default-layout", "continuous").unwrap();
app.open_path_with_display(locked.0.to_str().unwrap(), PageLayout::TwoUp, false);
let bytes = app.password_prompt.as_ref().unwrap().bytes.clone();
app.open_path_with_display(locked.0.to_str().unwrap(), PageLayout::TwoUp, false);
assert!(app.pending_opens.is_empty(), "repeating the locked file keeps its original prompt");
app.open_path_with_display(second.0.to_str().unwrap(), PageLayout::TwoUp, false);
assert_eq!(app.pending_opens.len(), 1);
assert!(Arc::ptr_eq(&app.password_prompt.as_ref().unwrap().bytes, &bytes));
app.set_launch_layout("single").unwrap();
app.submit_password(Some("wrong".into()));
assert!(app.password_prompt.as_ref().unwrap().error.is_some());
app.submit_password(Some("tool-password".into()));
assert_eq!(app.views.len(), 1);
assert_eq!(app.views[0].layout, PageLayout::TwoUp);
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while !app.pending_opens.is_empty() {
assert!(std::time::Instant::now() < deadline);
app.poll_pending_opens();
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(app.views.len(), 2);
assert_eq!(app.views[1].layout, PageLayout::Single);
assert!(!app.views[1].continuous());
assert!(app.view_defaults.continuous);
}
/// #1928: a new file handed over by the OS (Finder, or another launch) raises the window at once,
/// before it is prepared, and is the selected tab once it opens.
#[test]
+1
View File
@@ -376,6 +376,7 @@ mod tests {
after_open: None,
recovered: None,
then: None,
display_override: None,
});
let got = Arc::new(Mutex::new(Vec::new()));
app.pickers.showing.store(true, Ordering::SeqCst);
+2
View File
@@ -331,6 +331,8 @@ pub struct PasswordPrompt {
pub(crate) then: Option<&'static str>,
/// A link destination to apply after the linked PDF is unlocked.
pub(crate) after_open: Option<pdfcraft_render::RemoteDestination>,
/// A launch-time display override, applied only after this PDF is unlocked.
pub(crate) display_override: Option<(canvas::PageLayout, bool)>,
}
#[cfg(not(target_arch = "wasm32"))]
+4
View File
@@ -355,6 +355,10 @@ fn date_language_picker_name(l: &pdfcraft_engine::dates::DateLanguage, drawable:
/// area; [`preferences_buttons`] goes below it, outside the scroll, so OK is always in reach.
pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) {
ui.label(egui::RichText::new(tl!("Preferences")).font(theme::semibold(18.0)));
ui.label(egui::RichText::new(tl!("Appearance")).font(theme::semibold(13.0)));
if ui.add(widgets::checkbox(&mut app.show_page_edge_shadows, tl!("Show Page Edge Shadows"))).changed() {
ui.ctx().request_repaint();
}
ui.horizontal(|ui| {
let label = ui.label(tl!("Interface language"));
// A native name is only drawn when these fonts can draw it; otherwise the English name
+19
View File
@@ -140,6 +140,25 @@ fn thumbnail_size_is_settable_and_inspectable_through_the_control_channel() {
assert!(ok(&mut h, &c, "ui.state", json!({}))["active"]["thumbnail_zoom"].as_f64().unwrap() > 0.75);
}
#[test]
fn page_shadows_are_an_observable_app_wide_preference() {
let (mut h, c) = harness();
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["show_page_edge_shadows"], true);
for value in ["false", "true"] {
ok(&mut h, &c, "ui.set", json!({ "key": "show_page_edge_shadows", "value": value }));
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["show_page_edge_shadows"], value == "true");
}
for value in ["off", "1", "False", ""] {
assert!(call(&mut h, &c, "ui.set", json!({ "key": "show_page_edge_shadows", "value": value })).is_err());
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["show_page_edge_shadows"], true);
}
ok(&mut h, &c, "ui.set", json!({ "key": "show_page_edge_shadows", "value": false }));
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["show_page_edge_shadows"], false);
h.state_mut().active = None;
ok(&mut h, &c, "ui.set", json!({ "key": "show_page_edge_shadows", "value": "false" }));
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["show_page_edge_shadows"], false);
}
#[test]
fn control_resize_reports_observed_dimensions_and_rejects_invalid_sizes() {
let (mut h, c) = harness();
+125
View File
@@ -2602,6 +2602,131 @@ fn encrypted(bytes: Vec<u8>, algorithm: pdfcraft_cos::Algorithm, user: &str, own
pdfcraft_cos::write_full(&doc, &Default::default()).unwrap()
}
#[test]
fn launch_display_override_survives_password_retry_without_changing_defaults() {
use pdfcraft_ui_egui::canvas::PageLayout;
use std::io::Write as _;
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let path = temp_path(&format!("startup-layout-{nonce}.pdf"));
let mut file = std::fs::OpenOptions::new().write(true).create_new(true).open(&path).unwrap();
file.write_all(&protected("reader", "owner", -4)).unwrap();
drop(file);
let mut app = PdfCraftApp::new();
app.set_option("default-layout", "continuous").unwrap();
app.open_path_with_display(path.to_str().unwrap(), PageLayout::TwoUp, false);
assert!(app.views.is_empty() && app.password_prompt.is_some());
app.submit_password(Some("wrong".into()));
assert!(app.views.is_empty() && app.password_prompt.is_some());
app.submit_password(Some("reader".into()));
assert_eq!(app.views.len(), 1);
assert_eq!(app.views[0].layout, PageLayout::TwoUp);
assert!(!app.views[0].continuous());
let persisted: serde_json::Value = serde_json::from_str(&app.persist()).unwrap();
assert_eq!(persisted["default_layout"], "continuous");
std::fs::remove_file(path).unwrap();
}
#[test]
fn asynchronous_launch_display_override_survives_retries_and_cancel() {
asynchronous_launch_display(false);
}
#[test]
fn asynchronous_launch_display_explicit_layout_survives_retries_and_cancel() {
asynchronous_launch_display(true);
}
fn asynchronous_launch_display(explicit: bool) {
use pdfcraft_ui_egui::canvas::PageLayout;
fn wait(h: &mut Harness<'static, PdfCraftApp>, stage: &str, ready: impl Fn(&PdfCraftApp) -> bool) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !ready(h.state()) {
assert!(
std::time::Instant::now() < deadline,
"{stage}: views={}, prompt={}, error={}, notice={}",
h.state().views.len(),
h.state().password_prompt.is_some(),
h.state().password_prompt.as_ref().is_some_and(|p| p.error.is_some()),
h.state().toast.is_some()
);
h.run_steps(2);
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
let path = temp_path(&format!("async-startup-layout-{explicit}.pdf"));
std::fs::write(&path, protected("reader", "owner", -4)).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.set_option("default-layout", "continuous").unwrap();
app.open_bytes("other.pdf", None, fixture(2)).unwrap();
app
});
h.run_steps(2);
assert!(!h.state().run_inline, "exercise upstream's actual background-opening path");
let other = h.state().views[0].id;
h.state_mut().open_path_with_display(path.to_str().unwrap(), PageLayout::TwoUp, false);
if explicit {
h.state_mut().set_launch_layout("single").unwrap();
}
wait(&mut h, "password prompt", |app| app.password_prompt.is_some());
// The existing document keeps its default view while this request is pending.
h.state_mut().submit_password(Some("wrong".into()));
wait(&mut h, "wrong-password result", |app| app.password_prompt.as_ref().is_some_and(|p| p.error.is_some()));
assert_eq!(h.state().views.len(), 1);
h.state_mut().submit_password(Some("reader".into()));
wait(&mut h, "first unlock", |app| app.views.len() == 2);
assert_eq!(h.state().views[0].id, other);
assert_eq!(h.state().views[0].layout, PageLayout::Single);
assert!(h.state().views[0].continuous());
assert_eq!(h.state().views[1].layout, if explicit { PageLayout::Single } else { PageLayout::TwoUp });
assert!(!h.state().views[1].continuous());
let second_path = path.with_extension("second.pdf");
std::fs::copy(&path, &second_path).unwrap();
h.state_mut().open_path_with_display(second_path.to_str().unwrap(), PageLayout::TwoUp, false);
wait(&mut h, "password prompt", |app| app.password_prompt.is_some());
h.state_mut().submit_password(None);
h.state_mut().open_path(second_path.to_str().unwrap());
wait(&mut h, "password prompt", |app| app.password_prompt.is_some());
h.state_mut().submit_password(Some("reader".into()));
wait(&mut h, "normal unlock after cancel", |app| app.views.len() == 3);
assert_eq!(h.state().views[2].layout, PageLayout::Single);
assert!(h.state().views[2].continuous());
let persisted: serde_json::Value = serde_json::from_str(&h.state().persist()).unwrap();
assert_eq!(persisted["default_layout"], "continuous");
std::fs::remove_file(path).unwrap();
std::fs::remove_file(second_path).unwrap();
}
#[test]
fn canceling_launch_display_override_does_not_affect_later_documents() {
use pdfcraft_ui_egui::canvas::PageLayout;
use std::io::Write as _;
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
let path = temp_path(&format!("startup-cancel-layout-{nonce}.pdf"));
let mut file = std::fs::OpenOptions::new().write(true).create_new(true).open(&path).unwrap();
file.write_all(&protected("reader", "owner", -4)).unwrap();
drop(file);
let mut app = PdfCraftApp::new();
app.set_option("default-layout", "continuous").unwrap();
app.open_path_with_display(path.to_str().unwrap(), PageLayout::TwoUp, false);
assert!(app.views.is_empty() && app.password_prompt.is_some());
app.submit_password(None);
assert!(app.views.is_empty() && app.password_prompt.is_none());
app.open_bytes("next.pdf", None, fixture(2)).unwrap();
assert_eq!(app.views[0].layout, PageLayout::Single);
assert!(app.views[0].continuous());
// Reopening the same protected file normally must not resurrect the canceled override.
app.open_path(path.to_str().unwrap());
app.submit_password(Some("reader".into()));
assert_eq!(app.views.len(), 2);
assert_eq!(app.views[1].layout, PageLayout::Single);
assert!(app.views[1].continuous());
let persisted: serde_json::Value = serde_json::from_str(&app.persist()).unwrap();
assert_eq!(persisted["default_layout"], "continuous");
std::fs::remove_file(path).unwrap();
}
#[test]
fn password_prompt_opens_and_security_tab_reports_the_details() {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
+353
View File
@@ -0,0 +1,353 @@
//! Decorative page shadows are app preferences, never PDF edits.
use egui_kittest::{Harness, kittest::Queryable};
use pdfcraft_ui_egui::PdfCraftApp;
#[test]
fn viewer_shadows_follow_the_preference_without_moving_pages() {
for theme in ["light", "dark"] {
for layout in ["single", "two-page", "continuous", "two-up"] {
let mut h = harness(theme);
h.state_mut().set_option("layout", layout).unwrap();
h.run_steps(3);
let rectangles = h.state().views[0].visible_page_rects().to_vec();
let original = shadows(&h, false);
assert!(!original.is_empty(), "{theme} {layout}");
h.state_mut().set_option("show_page_edge_shadows", "false").unwrap();
h.run_steps(2);
assert!(shadows(&h, false).is_empty(), "{theme} {layout}");
assert_eq!(h.state().views[0].visible_page_rects(), rectangles);
h.state_mut().set_option("zoom", "125").unwrap();
h.state_mut().set_option("page", "2").unwrap();
h.run_steps(3);
assert!(shadows(&h, false).is_empty());
h.state_mut().set_option("show_page_edge_shadows", "true").unwrap();
h.run_steps(2);
assert!(!shadows(&h, false).is_empty());
}
}
}
#[test]
fn viewport_fades_follow_the_preference_and_keep_other_scroll_areas_styled() {
for theme in ["light", "dark"] {
for organize in [false, true] {
let mut h = harness(theme);
if organize {
h.state_mut().set_option("organize", "on").unwrap();
// Make the three-page grid overflow vertically.
h.state_mut().set_option("grid-zoom", "300").unwrap();
}
h.run_steps(4);
let original_style = h.ctx.global_style().spacing.scroll.fade;
assert!(original_style.strength > 0.0);
assert!(!viewport_fades(&h).is_empty(), "bottom fade at start: {theme}, organize={organize}");
scroll_down(&mut h, 100.0);
let original = viewport_fades(&h);
let viewport = h.state().views[0].viewport_rect();
assert!(original.iter().any(|r| (r.top() - viewport.top()).abs() < 2.0), "top fade: {original:?}");
assert!(original.iter().any(|r| (r.bottom() - viewport.bottom()).abs() < 2.0), "bottom fade: {original:?}");
let pages = h.state().views[0].visible_page_rects().to_vec();
h.state_mut().set_option("show_page_edge_shadows", "false").unwrap();
h.run_steps(2);
assert!(viewport_fades(&h).is_empty(), "viewport fades remain: {theme}, organize={organize}");
assert_eq!(h.state().views[0].visible_page_rects(), pages);
assert_eq!(h.ctx.global_style().spacing.scroll.fade, original_style);
h.state_mut().set_option("show_page_edge_shadows", "true").unwrap();
h.run_steps(2);
assert_eq!(viewport_fades(&h), original);
scroll_down(&mut h, 100_000.0);
let at_end = viewport_fades(&h);
assert!(at_end.iter().any(|r| (r.top() - viewport.top()).abs() < 2.0));
assert!(!at_end.iter().any(|r| (r.bottom() - viewport.bottom()).abs() < 2.0), "no bottom fade at the end");
}
}
}
#[test]
fn horizontal_viewport_fades_also_follow_the_preference() {
let mut h = harness("dark");
h.state_mut().set_option("zoom", "400").unwrap();
h.run_steps(4);
let viewport = h.state().views[0].viewport_rect();
let horizontal: Vec<_> = viewport_fades(&h).into_iter().filter(|r| r.width() <= 21.0).collect();
assert!(horizontal.iter().any(|r| (r.left() - viewport.left()).abs() < 2.0), "left fade: {horizontal:?}");
assert!(horizontal.iter().any(|r| (r.right() - viewport.right()).abs() < 2.0), "right fade: {horizontal:?}");
let pages = h.state().views[0].visible_page_rects().to_vec();
h.state_mut().set_option("show_page_edge_shadows", "false").unwrap();
h.run_steps(2);
assert!(viewport_fades(&h).is_empty());
assert_eq!(h.state().views[0].visible_page_rects(), pages);
}
fn scroll_down(h: &mut Harness<'static, PdfCraftApp>, points: f32) {
h.hover_at(h.state().views[0].viewport_rect().center());
h.run_steps(1);
h.event(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, -points),
phase: egui::TouchPhase::Move,
modifiers: egui::Modifiers::NONE,
source: egui::MouseWheelSource::Unknown,
});
h.run_steps(12);
}
/// Inspect the actual egui painter output: untextured four-vertex gradients
/// with a transparent edge, spanning one dimension of the document viewport.
fn viewport_fades(h: &Harness<'static, PdfCraftApp>) -> Vec<egui::Rect> {
let viewport = h.state().views[0].viewport_rect();
h.output()
.shapes
.iter()
.filter_map(|s| {
let egui::Shape::Mesh(mesh) = &s.shape else { return None };
if mesh.texture_id != egui::TextureId::default() || mesh.vertices.len() != 4 || mesh.indices.len() != 6 {
return None;
}
let rect = mesh.calc_bounds();
let transparent = mesh.vertices.iter().filter(|v| v.color == egui::Color32::TRANSPARENT).count();
(transparent == 2
&& (((rect.width() - viewport.width()).abs() < 2.0 && rect.height() <= 21.0)
|| ((rect.height() - viewport.height()).abs() < 2.0 && rect.width() <= 21.0))
&& rect.intersects(viewport))
.then_some(rect)
})
.collect()
}
#[test]
fn organize_shadows_follow_the_preference() {
for theme in ["light", "dark"] {
let mut h = harness(theme);
h.state_mut().set_option("organize", "on").unwrap();
h.run_steps(3);
let original = shadows(&h, true);
assert_eq!(original.len(), 3);
h.state_mut().set_option("show_page_edge_shadows", "false").unwrap();
h.run_steps(2);
assert!(shadows(&h, true).is_empty());
h.state_mut().set_option("show_page_edge_shadows", "true").unwrap();
h.run_steps(2);
assert_eq!(shadows(&h, true), original);
}
}
#[test]
fn checkbox_and_multiple_documents_do_not_edit_pdf_state() {
let mut h = harness("light");
h.state_mut().open_bytes("second.pdf", None, pdf()).unwrap();
let ids: Vec<_> = h.state().views.iter().map(|v| v.id).collect();
h.state_mut().session.apply(ids[0], pdfcraft_engine::Edit::SetInfo { key: "Title".into(), value: "Keep existing edits".into() }).unwrap();
assert!(h.state().session.get(ids[0]).unwrap().dirty);
assert!(h.state().session.get(ids[0]).unwrap().can_undo().is_some());
let saved: Vec<_> = ids.iter().map(|id| h.state_mut().session.save_bytes(*id).unwrap()).collect();
let before: Vec<_> = h
.state()
.views
.iter()
.map(|v| {
let d = h.state().session.get(v.id).unwrap();
(
d.bytes.clone(),
d.dirty,
d.can_undo().map(str::to_owned),
d.can_redo().map(str::to_owned),
d.info.pages.iter().map(|p| (p.width, p.height, p.rotation)).collect::<Vec<_>>(),
)
})
.collect();
h.state_mut().set_option("dialog", "preferences").unwrap();
h.run_steps(3);
let checkbox = h.get_by_label("Show Page Edge Shadows");
checkbox.click();
h.run_steps(2);
assert_eq!(serde_json::from_str::<serde_json::Value>(&h.state().persist()).unwrap()["show_page_edge_shadows"], false);
h.get_by_label("Show Page Edge Shadows").focus();
h.key_press(egui::Key::Space);
h.run_steps(2);
assert_eq!(serde_json::from_str::<serde_json::Value>(&h.state().persist()).unwrap()["show_page_edge_shadows"], true);
let dialog_shadow = h.ctx.global_style().visuals.window_shadow;
assert_ne!(dialog_shadow, egui::Shadow::NONE);
h.state_mut().set_option("show_page_edge_shadows", "false").unwrap();
h.run_steps(2);
assert_eq!(h.ctx.global_style().visuals.window_shadow, dialog_shadow);
h.state_mut().set_option("dialog", "none").unwrap();
for tab in [0, 1] {
h.state_mut().active = Some(tab);
h.run_steps(2);
assert!(shadows(&h, false).is_empty());
let d = h.state().session.get(h.state().views[tab].id).unwrap();
assert_eq!(
(&d.bytes, d.dirty, d.can_undo(), d.can_redo()),
(&before[tab].0, before[tab].1, before[tab].2.as_deref(), before[tab].3.as_deref())
);
assert_eq!(d.info.pages.iter().map(|p| (p.width, p.height, p.rotation)).collect::<Vec<_>>(), before[tab].4);
}
for (id, bytes) in ids.iter().zip(&saved) {
assert_eq!(h.state_mut().session.save_bytes(*id).unwrap(), *bytes);
}
h.state_mut().open_bytes("new.pdf", None, pdf()).unwrap();
h.run_steps(2);
assert!(shadows(&h, false).is_empty());
}
#[test]
fn rendered_page_content_is_identical_with_and_without_decoration() {
let mut h = harness("dark");
h.state_mut().set_option("panel", "pages").unwrap();
h.state_mut().set_option("zoom", "175").unwrap();
h.run_steps(3);
scroll_down(&mut h, 20.0);
for _ in 0..400 {
h.run_steps(2);
if !h.state().render_pending() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(!h.state().render_pending(), "the actual PDF raster must be ready");
let rect = h.state().views[0].page_screen_rect(0).unwrap();
let fades = viewport_fades(&h);
assert_eq!(fades.iter().filter(|r| r.height() <= 21.0).count(), 2, "the screenshot comparison must contain both vertical fades");
let enabled = h.render().unwrap();
h.state_mut().set_option("show_page_edge_shadows", "false").unwrap();
h.run_steps(2);
let disabled = h.render().unwrap();
assert_eq!(h.state().views[0].page_screen_rect(0), Some(rect));
let viewport = h.state().views[0].viewport_rect();
assert!(viewport_fades(&h).is_empty());
// Only the painter's actual decorative fade strips may change over the page.
// All remaining artwork, annotation and form pixels must be identical.
for (_, page) in h.state().views[0].visible_page_rects() {
let visible = page.intersect(viewport).shrink(2.0);
if !visible.is_positive() {
continue;
}
for y in visible.top().ceil() as u32..visible.bottom().floor() as u32 {
for x in visible.left().ceil() as u32..visible.right().floor() as u32 {
if !fades.iter().any(|fade| fade.expand(1.0).contains(egui::pos2(x as f32, y as f32))) {
assert_eq!(enabled.get_pixel(x, y), disabled.get_pixel(x, y), "page content at {x},{y}");
}
}
}
}
// The top overlaps the first page's blue artwork; the bottom overlaps the
// blank second page. Sample clear of the unchanged scrollbar at the edge.
let x = rect.intersect(viewport).center().x as u32;
for (y, expected) in [(viewport.top() + 8.0, [204, 230, 255, 255]), (viewport.bottom() - 10.0, [255, 255, 255, 255])] {
assert_eq!(disabled.get_pixel(x, y as u32).0, expected, "unobscured PDF at y={y}");
assert_ne!(enabled.get_pixel(x, y as u32).0, expected, "fade overlays the PDF at y={y}");
}
// The Pages sidebar, including its thumbnails and selection borders, is unaffected.
for y in 0..enabled.height() {
for x in (0..viewport.left().floor() as u32).chain(viewport.right().ceil() as u32..enabled.width()) {
assert_eq!(enabled.get_pixel(x, y), disabled.get_pixel(x, y), "sidebar at {x},{y}");
}
}
assert_ne!(enabled.as_raw(), disabled.as_raw(), "the decoration really changes the image");
if let Ok(directory) = std::env::var("PDFCRAFT_SHADOW_SCREENSHOTS") {
enabled.save(format!("{directory}/page-shadows-enabled.png")).unwrap();
disabled.save(format!("{directory}/page-shadows-disabled.png")).unwrap();
}
}
fn harness(theme: &str) -> Harness<'static, PdfCraftApp> {
let theme = theme.to_string();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_| {
let mut app = PdfCraftApp::new();
// Save comparisons must not depend on crossing a /ModDate second boundary.
app.session = app.session.with_clock(|| 1_700_000_000);
app.set_option("language", "en").unwrap();
app.set_option("theme", &theme).unwrap();
app.open_bytes("pages.pdf", None, pdf()).unwrap();
app.set_option("zoom", "100").unwrap();
app
});
h.run_steps(3);
h
}
fn shadows(h: &Harness<'static, PdfCraftApp>, grid: bool) -> Vec<egui::Rect> {
let color = pdfcraft_ui_egui::theme::Tokens::for_kind(h.state().theme).page_shadow;
fn collect(shape: &egui::Shape, color: egui::Color32, grid: bool, out: &mut Vec<egui::Rect>) {
match shape {
egui::Shape::Vec(shapes) => {
for s in shapes {
collect(s, color, grid, out);
}
}
egui::Shape::Rect(r)
if r.fill == color
&& r.rect.width() > 20.0
&& r.rect.height() > 20.0
&& if grid {
r.blur_width == 0.0 && r.corner_radius == egui::CornerRadius::same(1)
} else {
r.blur_width == 14.0 && r.corner_radius == egui::CornerRadius::ZERO
} =>
{
out.push(r.rect)
}
_ => {}
}
}
let mut out = Vec::new();
for s in &h.output().shapes {
collect(&s.shape, color, grid, &mut out);
}
out
}
/// Synthetic pages include colored artwork, an annotation and a form field.
fn pdf() -> Vec<u8> {
let objects = [
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [7 0 R] >> >>".to_string(),
"<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 /MediaBox [0 0 300 400] >>".to_string(),
"<< /Type /Page /Parent 2 0 R /Contents 8 0 R /Annots [6 0 R 7 0 R] >>".to_string(),
"<< /Type /Page /Parent 2 0 R >>".to_string(),
"<< /Type /Page /Parent 2 0 R >>".to_string(),
"<< /Type /Annot /Subtype /Text /Rect [20 20 40 40] /Contents (Note) >>".to_string(),
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (field) /V (value) /Rect [50 50 100 70] /P 3 0 R >>".to_string(),
{
let body = "0.8 0.9 1 rg 0 0 300 400 re f 0.3 g 10 10 280 8 re f";
format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len())
},
];
let mut bytes = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, object) in objects.iter().enumerate() {
offsets.push(bytes.len());
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
}
let xref = bytes.len();
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
for offset in offsets {
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
bytes
}
#[test]
fn page_shadow_preference_round_trips_and_ignores_invalid_settings() {
let mut app = PdfCraftApp::new();
let enabled = |app: &PdfCraftApp| serde_json::from_str::<serde_json::Value>(&app.persist()).unwrap()["show_page_edge_shadows"].clone();
assert_eq!(enabled(&app), true);
app.restore("{}");
assert_eq!(enabled(&app), true);
for value in [false, true] {
app.set_option("show_page_edge_shadows", if value { "true" } else { "false" }).unwrap();
let mut restored = PdfCraftApp::new();
restored.restore(&app.persist());
assert_eq!(enabled(&restored), value);
}
for invalid in ["null", "0", "\"false\"", "[]", "{}"] {
app.restore(&format!("{{\"show_page_edge_shadows\":{invalid}}}"));
assert_eq!(enabled(&app), true);
}
app.restore("not json");
assert_eq!(enabled(&app), true);
assert!(app.set_option("show_page_edge_shadows", "maybe").is_err());
assert_eq!(enabled(&app), true);
}
+33
View File
@@ -0,0 +1,33 @@
# Source licensing self-check
Run `cargo xtask license-check` to validate the checked-out source, offline. The command
does not download assets, fetch models, inspect installers, or generate dependency notices.
It reuses `cargo xtask assets` for attribution, asset policy and locked dependency versions,
then checks the documents recorded in `LICENSES.toml`.
Each document has a repository-relative source, a unique portable logical destination and
the SHA-256 of its audited text. Destinations are validation metadata, not instructions to
copy files into packages. Required project documents and every asset's `licence_file` must
be mapped. Every dependency-bundled asset must have a document mapping matching its existing
`ATTRIBUTION.toml` licence expression. Foxit has an explicit required document. Licence
texts for egui's default fonts and CMaps are retained in `licenses/`, with their pinned
upstream origins recorded in the manifest. Existing font and asset texts stay in place.
Missing files, empty or invalid text, changed hashes, duplicate mappings, case-insensitive
destination collisions, parent-file conflicts and unsafe paths fail the check. Source
symlinks must resolve inside the checkout. OCR models need complete attribution and valid
hashes and destination names; the model binaries and downloaded licence texts need not be
present. The optional craft-fonts checkout remains governed by its own attribution and
the existing asset validator; this command does not audit that external checkout.
When changing attribution, edit `ATTRIBUTION.toml`, run `cargo xtask assets --write`. Generated attribution consistency remains checked only
by the existing asset validator. When changing licence or notice text, re-audit
its provenance before updating the corresponding hash. Do not replace a licence with
guessed terms or update a hash merely to silence a failure. `LICENSES.toml` is validated
with unknown fields rejected.
The existing Linux source-validation step in `.github/workflows/ci.yml` and `cargo xtask ci`
run `license-check` in place of `assets`; the asset validator runs once within that command.
This Stage 1 check establishes source-document consistency, not completed-package
compliance or legal sufficiency. Installer inspection, dependency-notice generation and
font-delivery packaging changes remain separate work.
+3 -1
View File
@@ -194,7 +194,7 @@ total matches the parity estimate.
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. Forms without Resources use page Properties for ActualText and layer visibility; own dictionaries stop fallback. Oversized missing inline-image samples, including header and terminator overhead, no longer pin the content window (#3107 / PR #3438); normal large streamed images retain deterministic sample boundaries. Radial luminosity fades have synthetic coverage for the existing #2132 mask guard, including per-pixel references, antialiased clips, previews, scales and tiles; no additional production fix was needed. Dotted TOC leaders no longer inflate letter tracking or hide word spaces (#3086); phrase search and selection are covered. Short tracked ellipses retain their gap samples and remain searchable. Missing spacing glyphs in non-embedded Japanese CID substitutes render blank without changing PDF advances or Unicode (#3601). The desktop viewer can use installed CJK fonts, including Windows GBK Song/Hei/Kai/FangSong and subset-prefixed Founder names; Combine thumbnails and accessibility figure previews use the same display-only fallback, without changing export or print font policy (#234, continuation of #1567, review #4172). Print previews exclude installed faces and isolate their font cache; GBK regressions cover view/print switching for inline and pooled whole-page/tile rendering. Installed-font helpers and their regression contracts have function documentation. GBK regression rasters match explicit-Unicode references with independently selected installed faces; wrong-glyph and wrong-family negative controls fail. Integration with main fa5f9f8c retains viewer fallback across the split engine session modules, renderer tests and Combine thumbnail workers. |
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. |
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). #3808: the Pages inspector offers a 50–150% thumbnail slider and buttons, also available through `ui.set`. Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. Comment hover follows individual marked lines and original annotation paint order (#3541/#3620), independently of spatial inspection sorting; synthetic rotation, differing-upper-edge overlap and unchanged-document regressions. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. Repeated window resizes by an edge and moves by the tab strip on GNOME/Wayland work without a click in between. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). #3808: the Pages inspector offers a 50–150% thumbnail slider and buttons, also available through `ui.set`. Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. Comment hover follows individual marked lines and original annotation paint order (#3541/#3620), independently of spatial inspection sorting; synthetic rotation, differing-upper-edge overlap and unchanged-document regressions. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. Repeated window resizes by an edge and moves by the tab strip on GNOME/Wayland work without a click in between. Optional --two-paged applies noncontinuous two-page layout to explicitly opened CLI files without changing defaults; explicit --layout wins. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. Wrapped revision buttons stay above Binding in Document Properties (#2739); a synthetic UI test checks layout and saved revision content. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Later native-open replies stay queued while a modal or picker owns the foreground; regressions cover recipient ownership, completion/cancellation and one-shot wakes. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; Ctrl+Q/Cmd+Q asks before discarding unsaved documents. |
@@ -290,6 +290,8 @@ Estimates unchanged.
| Date | Change | Summary |
|---|---|---|
| 2026-10-31 | patch | #1577: reconcile optional page shadows with current viewer/organizer and settings modules; CI validation pending. |
| 2026-10-31 | patch | M3 #1354: reconcile the launch override with split app/file modules and the structured open result; preserve headless help/version exits. CI validation pending. |
| 2026-10-31 | patch | M2, PR #4172: eight GBK font/alias cases compare exact pixels with independent installed-face and explicit-Unicode references; wrong-glyph and wrong-family negative controls fail. Estimates unchanged. |
| 2026-10-31 | patch | M2, PR #4172: integrate main fa5f9f8c, preserve both raster test groups and restore explicit display_config imports; display-only CJK policy and estimates unchanged. |
| 2026-10-28 | patch | M2, PR #4172: integrate main 5af10276 and retain both roadmap histories; viewer-only CJK and print/output font policies and estimates unchanged. |
+28
View File
@@ -0,0 +1,28 @@
Copyright 1990-2023 Adobe. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
Neither the name of Adobe nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+31
View File
@@ -0,0 +1,31 @@
The work in the Hack project is Copyright 2018 Source Foundry Authors and licensed under the MIT License
The work in the DejaVu project was committed to the public domain.
Bitstream Vera Sans Mono Copyright 2003 Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera"
MIT License
Copyright (c) 2018 Source Foundry Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
BITSTREAM VERA LICENSE
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions:
The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera".
This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names.
The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org.
+92
View File
@@ -0,0 +1,92 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+96
View File
@@ -0,0 +1,96 @@
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2014 John Slegers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+9
View File
@@ -20,10 +20,12 @@ APP_ID=ai.storyteller.pdfcraft
FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo
TEST=1
OCI=0
TARBALL=""
while [[ $# -gt 0 ]]; do
case "$1" in
--no-test) TEST=0; shift ;;
--oci) OCI=1; shift ;;
-h | --help) sed -n '2,14p' "$0"; exit 0 ;;
-*) echo "unknown argument: $1" >&2; exit 2 ;;
*) TARBALL="$1"; shift ;;
@@ -62,6 +64,13 @@ OUT="$DIST/$BASENAME.flatpak"
flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable
echo "wrote $OUT"
if [[ "$OCI" = 1 ]]; then
OUT_OCI="$DIST/$BASENAME-oci"
rm -rf "$OUT_OCI"
flatpak build-bundle --oci "$WORK/repo" "$OUT_OCI" "$APP_ID" stable
echo "wrote OCI layout to $OUT_OCI"
fi
if [[ "$TEST" = 1 ]]; then
flatpak install --user -y --noninteractive --reinstall "$OUT"
flatpak info --user "$APP_ID"
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Build PdfCraft as an OCI container layout and push it to GitHub Container Registry (GHCR)
# for consumption by Flatpak OCI clients (flatpak-oci-specs).
#
# Usage:
# packaging/linux/flatpak-oci.sh [--push] [TARBALL]
#
# Environment variables:
# REGISTRY Target registry image (default: ghcr.io/storytold/pdfcraft)
# TAG Target image tag (default: $VERSION-$ARCH, also tags stable and latest)
# GITHUB_TOKEN Token for GHCR authentication when --push is used
set -euo pipefail
# shellcheck source=../env.sh
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
HERE="$ROOT/packaging/linux"
PUSH=0
TARBALL=""
while [[ $# -gt 0 ]]; do
case "$1" in
--push) PUSH=1; shift ;;
-h | --help) sed -n '2,12p' "$0"; exit 0 ;;
-*) echo "unknown argument: $1" >&2; exit 2 ;;
*) TARBALL="$1"; shift ;;
esac
done
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) ;;
aarch64 | arm64) ARCH=aarch64 ;;
*) echo "unsupported architecture $ARCH" >&2; exit 2 ;;
esac
BASENAME="pdfcraft-$VERSION-linux-$ARCH"
OUT_OCI="$DIST/$BASENAME-oci"
echo "==> Building Flatpak OCI layout for $BASENAME"
"$HERE/flatpak-bundle.sh" --oci --no-test "${TARBALL:-}"
[[ -d "$OUT_OCI" ]] || { echo "error: $OUT_OCI layout not generated" >&2; exit 1; }
REGISTRY="${REGISTRY:-ghcr.io/storytold/pdfcraft}"
TAG="${TAG:-$VERSION-$ARCH}"
echo "==> OCI layout ready at $OUT_OCI (Target: $REGISTRY:$TAG)"
if [[ "$PUSH" = 1 ]]; then
command -v skopeo >/dev/null || { echo "error: skopeo is required to push OCI images (apt install skopeo)" >&2; exit 1; }
AUTH_ARGS=()
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
AUTH_ARGS=(--dest-creds "${GITHUB_ACTOR:-storytold}:$GITHUB_TOKEN")
fi
echo "==> Pushing to $REGISTRY:$TAG"
skopeo copy --format oci "${AUTH_ARGS[@]}" "oci:$OUT_OCI" "docker://$REGISTRY:$TAG"
# Also update architecture-specific stable tag
echo "==> Pushing to $REGISTRY:stable-$ARCH"
skopeo copy --format oci "${AUTH_ARGS[@]}" "oci:$OUT_OCI" "docker://$REGISTRY:stable-$ARCH"
echo "==> Push complete"
fi
+5 -5
View File
@@ -890,8 +890,8 @@ milestone = "M3"
status = "shipped"
commands = ["view.layout.two_up"]
tools = []
evidence = ["crates/ui-egui/tests/it/view.rs::two_up_layout_places_pages_side_by_side", "crates/ui-egui/tests/it/viewer.rs::two_page_view_steps_a_spread_at_a_time"]
notes = "next/previous page step a spread at a time"
evidence = ["crates/ui-egui/tests/it/view.rs::two_up_layout_places_pages_side_by_side", "crates/ui-egui/tests/it/viewer.rs::two_page_view_steps_a_spread_at_a_time", "apps/pdfcraft/src/startup.rs::startup_override_is_per_file_and_does_not_persist", "crates/ui-egui/src/platform/open_worker/tests.rs::flagged_startup_opens_wait_for_password_ownership_before_the_first_frame"]
notes = "Next/previous page step a spread at a time. Optional desktop --two-paged launches named PDFs in noncontinuous two-page view without changing saved defaults or unrelated restored tabs. Explicit --layout wins after flagged opens settle, including password retries; repeated files reuse their tab. This is a launch override, not a declarative configuration module."
[[feature]]
id = "view.layout-two-up-scrolling"
@@ -1287,7 +1287,7 @@ status = "shipped"
commands = []
tools = []
evidence = ["crates/ui-egui/tests/it/view.rs::pages_sidebar_select_all_keeps_current_page_and_keyboard_scope", "crates/ui-egui/tests/it/editing.rs::dragging_pages_panel_thumbnails_reorders_pages", "crates/ui-egui/tests/it/viewer.rs::pages_panel_thumbnails_have_a_context_menu", "crates/ui-egui/tests/it/editing.rs::split_from_pages_panel_context_menu", "crates/ui-egui/tests/it/editing.rs::deleting_pages_from_the_pages_panel_menu", "crates/ui-egui/tests/it/editing.rs::the_delete_key_deletes_pages_picked_in_the_pages_panel_but_never_every_page"]
notes = "Ctrl/Cmd+A selects every page while the Pages sidebar owns the keyboard, without navigation; Escape clears that selection. Viewer and text-field shortcuts keep their own scope. Drag the selection (or the page grabbed) to reorder, with a drop bar and edge scrolling; right-click offers Split document…, Extract pages, Cut, Copy, Paste after and Delete pages (also the Delete key over the panel). Not yet: Rotate, Insert and Page labels in the menu, dragging pages between documents, or dropping files onto the panel."
notes = "Ctrl/Cmd+A selects every page while the Pages sidebar owns the keyboard, without navigation; Escape clears that selection. Viewer and text-field shortcuts keep their own scope. Drag the selection (or the page grabbed) to reorder, with a drop bar and edge scrolling; right-click offers Split document…, Extract pages, Cut, Copy, Paste after and Delete pages (also the Delete key over the panel). Deleting pages makes the next Save a full rewrite (unsigned files), so the deleted pages leave the file; signed files stay incremental. Not yet: Rotate, Insert and Page labels in the menu, dragging pages between documents, or dropping files onto the panel."
[[feature]]
id = "view.bookmarks-panel"
@@ -9941,8 +9941,8 @@ milestone = "M14"
status = "partial"
commands = []
tools = []
evidence = ["crates/ui-egui/tests/it/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/tests/it/view.rs::default_zoom_is_used_for_new_documents", "crates/ui-egui/tests/it/view.rs::preferences_set_the_default_zoom", "crates/ui-egui/tests/it/view.rs::always_use_my_default_zoom_and_page_layout_ignores_what_the_pdf_asks_for"]
notes = "Preferences ▸ Documents and view: Default page display and Default zoom (Fit to width, Zoom to page level or a percentage) for every new tab; a PDF that asks for a layout or zoom gets it, unless Always use my default zoom and page layout is on. Missing: Acrobat's resolution, rendering and page-content options"
evidence = ["crates/ui-egui/tests/page_shadows.rs::viewport_fades_follow_the_preference_and_keep_other_scroll_areas_styled", "crates/ui-egui/tests/page_shadows.rs::rendered_page_content_is_identical_with_and_without_decoration", "crates/ui-egui/tests/page_shadows.rs::page_shadow_preference_round_trips_and_ignores_invalid_settings", "crates/ui-egui/tests/page_shadows.rs::viewer_shadows_follow_the_preference_without_moving_pages", "crates/ui-egui/tests/page_shadows.rs::organize_shadows_follow_the_preference", "crates/ui-egui/tests/it/control.rs::page_shadows_are_an_observable_app_wide_preference", "crates/ui-egui/tests/it/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/tests/it/view.rs::default_zoom_is_used_for_new_documents", "crates/ui-egui/tests/it/view.rs::preferences_set_the_default_zoom", "crates/ui-egui/tests/it/view.rs::always_use_my_default_zoom_and_page_layout_ignores_what_the_pdf_asks_for"]
notes = "Preferences ▸ Documents and view: Default page display and Default zoom (Fit to width, Zoom to page level or a percentage) for every new tab; a PDF that asks for a layout or zoom gets it, unless Always use my default zoom and page layout is on. Preferences ▸ Appearance also has an app-wide, persistent Show Page Edge Shadows checkbox (on by default), exposed through ui.set and ui.state; it affects viewer and Organize decorations, including the viewport-edge gradients painted over pages, only. Missing: Acrobat's resolution, rendering and page-content options"
[[feature]]
id = "misc.prefs-documents"
+1 -1
View File
@@ -116,7 +116,7 @@ pub fn ci(_: &[String]) -> anyhow::Result<()> {
("i18n-report", Box::new(|| i18n_report(&["--workspace".to_string()]))),
("layers", Box::new(|| layers(&[]))),
("wasm", Box::new(|| wasm(&[]))),
("assets", Box::new(|| crate::assets::run(&[]))),
("license-check", Box::new(|| crate::license_check::run(&[]))),
("deny", Box::new(|| deny(&[]))),
("parity", Box::new(|| crate::parity::run(&[]))),
];
+471
View File
@@ -0,0 +1,471 @@
//! Offline source-document checks. This does not inspect completed packages or dependencies.
use std::collections::BTreeSet;
use std::path::Path;
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use crate::assets;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Documents {
document: Vec<Document>,
bundled: Vec<Bundled>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Document {
source: String,
destination: String,
sha256: String,
/// Audited identifiers of the full licence texts in this document, not inferred from its contents.
#[serde(default)]
licences: Vec<String>,
#[serde(default)]
origin: Option<String>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Bundled {
#[serde(rename = "crate")]
krate: String,
path: String,
licence: String,
documents: Vec<String>,
}
pub fn run(args: &[String]) -> Result<()> {
if !args.is_empty() {
bail!("usage: cargo xtask license-check (no downloads or write mode)");
}
let root = assets::root();
// Reuse the existing policy and generated-attribution validator, once.
assets::run(&[])?;
validate(&root)?;
println!("license-check: source documents and licensing metadata are consistent");
Ok(())
}
fn validate(root: &Path) -> Result<()> {
let text = std::fs::read_to_string(root.join("LICENSES.toml")).context("reading LICENSES.toml")?;
let docs: Documents = toml::from_str(&text).context("parsing LICENSES.toml")?;
let attribution = assets::load(root)?;
let mut problems = check(root, &docs, &attribution);
let ocr_source = std::fs::read_to_string(root.join("crates/ocr/src/lib.rs")).context("reading OCR model filename constants")?;
problems.extend(check_required_ocr_metadata(&ocr_source, &attribution)?);
if !problems.is_empty() {
bail!("source licensing errors:\n{}", problems.join("\n"));
}
Ok(())
}
/// Read only the two filename constants consumed by the OCR application. Fail closed if their
/// declaration format changes; do not guess filenames or add a dependency on the OCR engine.
fn check_required_ocr_metadata(source: &str, attribution: &assets::Manifest) -> Result<Vec<String>> {
let mut problems = Vec::new();
for name in ["DETECTION_MODEL", "RECOGNITION_MODEL"] {
let prefix = format!("pub const {name}: &str = \"");
let declarations: Vec<_> = source.lines().map(str::trim).filter_map(|line| line.strip_prefix(&prefix)).collect();
let file = declarations.first().and_then(|value| value.strip_suffix("\";"));
let Some(file) = file.filter(|file| declarations.len() == 1 && safe_path(file) && !file.contains('/')) else {
bail!(
"OCR constant {name}: expected one literal filename declaration in crates/ocr/src/lib.rs; update the source validator if its syntax changed"
);
};
let entries: Vec<_> = attribution.fetched.iter().filter(|f| f.file == file).collect();
if entries.is_empty() {
problems.push(format!("{file}: required OCR licensing metadata missing from ATTRIBUTION.toml"));
} else {
if entries.len() != 1 {
problems.push(format!("{file}: required OCR licensing metadata must have exactly one entry"));
}
if entries.iter().any(|f| f.kind != "model") {
problems.push(format!("{file}: required OCR licensing metadata must have kind = \"model\""));
}
}
}
Ok(problems)
}
/// The audited bundle inventory currently uses identifiers and flat AND/OR expressions only.
/// Keep their semantics distinct, and fail closed on mixed operators, grouping or exceptions
/// rather than introducing a general SPDX parser or flattening an unsupported expression.
fn licence_coverage(expression: &str, covered: &BTreeSet<&str>) -> Result<bool> {
let mut tokens = expression.split_whitespace();
let Some(first) = tokens.next().filter(|id| assets::ALLOWED_LICENCES.contains(id)) else {
bail!("unsupported bundle licence expression `{expression}`");
};
let mut ids = vec![first];
let mut operator = None;
while let Some(op) = tokens.next() {
if !matches!(op, "AND" | "OR") || operator.is_some_and(|previous| previous != op) {
bail!("unsupported bundle licence expression `{expression}`; audit its applicability before extending the validator");
}
operator = Some(op);
let Some(id) = tokens.next().filter(|id| assets::ALLOWED_LICENCES.contains(id)) else {
bail!("unsupported bundle licence expression `{expression}`");
};
ids.push(id);
}
Ok(if operator == Some("OR") { ids.iter().any(|id| covered.contains(id)) } else { ids.iter().all(|id| covered.contains(id)) })
}
// A portable relative path, including on Windows; reject aliases before comparing mappings.
fn safe_path(s: &str) -> bool {
!s.is_empty()
&& !s.contains(['\\', ':'])
&& s.split('/').all(|p| {
!p.is_empty()
&& p != "."
&& p != ".."
&& !p.ends_with(['.', ' '])
&& p.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
&& !matches!(
p.split('.').next().unwrap_or_default().to_ascii_uppercase().as_str(),
"CON"
| "PRN"
| "AUX"
| "NUL"
| "COM1"
| "COM2"
| "COM3"
| "COM4"
| "COM5"
| "COM6"
| "COM7"
| "COM8"
| "COM9"
| "LPT1"
| "LPT2"
| "LPT3"
| "LPT4"
| "LPT5"
| "LPT6"
| "LPT7"
| "LPT8"
| "LPT9"
)
})
}
fn hash(s: &str) -> bool {
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
}
fn check(root: &Path, docs: &Documents, attribution: &assets::Manifest) -> Vec<String> {
let mut problems = Vec::new();
let mut sources = BTreeSet::new();
let mut destinations = BTreeSet::new();
for d in &docs.document {
if !safe_path(&d.source) || !safe_path(&d.destination) {
problems.push(format!("{}: unsafe document source or destination", d.source));
continue;
}
let new_source = sources.insert(d.source.as_str());
let new_destination = destinations.insert(d.destination.to_ascii_lowercase());
if !new_source || !new_destination {
problems.push(format!("{}: duplicate document source or destination", d.source));
}
if d.origin.as_ref().is_some_and(|u| !u.starts_with("https://") || u.trim().len() <= 8) {
problems.push(format!("{}: invalid document origin", d.source));
}
let mut identifiers = BTreeSet::new();
for id in &d.licences {
if !assets::ALLOWED_LICENCES.contains(&id.as_str()) || !identifiers.insert(id.as_str()) {
problems.push(format!("{}: invalid or duplicate audited licence identifier `{id}`", d.source));
}
}
let path = root.join(&d.source);
// Canonical confinement also catches symlinks escaping the checkout.
let confined = root.canonicalize().and_then(|r| path.canonicalize().map(|p| p.starts_with(r))).unwrap_or(false);
if !confined || !path.is_file() {
problems.push(format!("{}: required document missing or outside checkout", d.source));
continue;
}
match std::fs::read_to_string(&path) {
Ok(text) if !text.trim().is_empty() && !text.contains('\0') => {
if !hash(&d.sha256) || assets::sha256_hex(text.as_bytes()) != d.sha256 {
problems.push(format!("{}: document hash mismatch; re-audit the source before updating LICENSES.toml", d.source));
}
}
_ => problems.push(format!("{}: required document empty or invalid UTF-8 text", d.source)),
}
}
for destination in &destinations {
for (i, _) in destination.match_indices('/') {
if destination.get(..i).is_some_and(|prefix| destinations.contains(prefix)) {
problems.push(format!("{destination}: document destination conflicts with a parent file"));
}
}
}
for required in ["LICENSE-MIT", "LICENSE-APACHE", "NOTICE", "vendor/hayro-interpret/assets/LICENSE_FOXIT"]
.into_iter()
.chain(attribution.asset.iter().map(|a| a.licence_file.as_str()))
{
if !sources.contains(required) {
problems.push(format!("{required}: required document has no LICENSES.toml mapping"));
}
}
// One mapping per bundled asset, using the existing attribution's licence expression.
let mut seen = BTreeSet::new();
for b in &docs.bundled {
if !seen.insert((b.krate.as_str(), b.path.as_str())) {
problems.push(format!("{} {}: duplicate bundled mapping", b.krate, b.path));
}
if !attribution.bundled.iter().any(|a| a.krate == b.krate && a.path == b.path && a.licence == b.licence) {
problems.push(format!("{} {}: bundled mapping disagrees with ATTRIBUTION.toml", b.krate, b.path));
}
if b.documents.is_empty() || b.documents.iter().any(|d| !sources.contains(d.as_str())) {
problems.push(format!("{} {}: bundled licence documents missing", b.krate, b.path));
}
for document in docs.document.iter().filter(|d| b.documents.contains(&d.source)) {
if !document.licences.iter().any(|id| b.licence.split_whitespace().any(|token| token == id)) {
problems.push(format!("{} {}: inapplicable mapped document {}", b.krate, b.path, document.source));
}
}
let covered: BTreeSet<_> =
docs.document.iter().filter(|d| b.documents.contains(&d.source)).flat_map(|d| d.licences.iter().map(String::as_str)).collect();
match licence_coverage(&b.licence, &covered) {
Ok(true) => {}
Ok(false) => problems
.push(format!("{} {}: inapplicable licence documents; mapped audited identifiers do not cover `{}`", b.krate, b.path, b.licence)),
Err(error) => problems.push(format!("{} {}: {error}", b.krate, b.path)),
}
}
for b in &attribution.bundled {
if !seen.contains(&(b.krate.as_str(), b.path.as_str())) {
problems.push(format!("{} {}: bundled asset has no licence document mapping", b.krate, b.path));
}
}
for a in attribution.asset.iter().filter(|a| a.licence_file == "assets/icons/LICENSE-lucide.txt") {
if a.licence != "ISC AND MIT" {
problems.push(format!("{}: Lucide collection must credit ISC and Feather MIT", a.path));
}
}
check_model_metadata(attribution, &mut problems);
problems
}
fn check_model_metadata(attribution: &assets::Manifest, problems: &mut Vec<String>) {
let mut model_paths = BTreeSet::new();
for f in attribution.fetched.iter().filter(|f| f.kind == "model") {
if [f.title.as_str(), &f.author, &f.source, &f.usage, &f.licence].iter().any(|v| v.trim().is_empty())
|| !hash(&f.sha256)
|| !hash(&f.licence_sha256)
|| !f.url.starts_with("https://")
|| !f.licence_url.starts_with("https://")
{
problems.push(format!("{}: incomplete OCR model licensing metadata", f.file));
}
for path in [f.file.clone(), format!("{}.LICENCE.txt", f.file)] {
if !safe_path(&path)
|| path.contains('/')
|| path.eq_ignore_ascii_case("ATTRIBUTION.txt")
|| !model_paths.insert(path.to_ascii_lowercase())
{
problems.push(format!("{}: duplicate or unsafe OCR model destination", f.file));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
fn fixture() -> (std::path::PathBuf, Documents) {
static NEXT: AtomicU64 = AtomicU64::new(0);
let root = std::env::temp_dir().join(format!("pdfcraft-license-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed)));
std::fs::create_dir_all(&root).unwrap();
let document = ["LICENSE-MIT", "LICENSE-APACHE", "NOTICE", "vendor/hayro-interpret/assets/LICENSE_FOXIT"]
.into_iter()
.map(|p| {
let path = root.join(p);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, "synthetic licence text\n").unwrap();
Document {
source: p.into(),
destination: p.into(),
sha256: assets::sha256_hex(b"synthetic licence text\n"),
licences: match p {
"LICENSE-MIT" => vec!["MIT".into()],
"LICENSE-APACHE" => vec!["Apache-2.0".into()],
_ => vec![],
},
origin: None,
}
})
.collect();
(root, Documents { document, bundled: vec![] })
}
#[test]
fn missing_license_fails_and_restoring_recovers() {
let (root, docs) = fixture();
let m = assets::Manifest::default();
assert!(check(&root, &docs, &m).is_empty());
std::fs::remove_file(root.join("LICENSE-MIT")).unwrap();
assert!(check(&root, &docs, &m).iter().any(|p| p.contains("LICENSE-MIT: required document missing")));
std::fs::write(root.join("LICENSE-MIT"), "synthetic licence text\n").unwrap();
assert!(check(&root, &docs, &m).is_empty());
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn empty_invalid_and_changed_documents_fail() {
let (root, docs) = fixture();
for text in [b" ".as_slice(), b"\xff", b"changed licence", b"text\0"] {
std::fs::write(root.join("NOTICE"), text).unwrap();
assert!(!check(&root, &docs, &assets::Manifest::default()).is_empty());
}
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn duplicate_and_unsafe_destinations_fail() {
let (root, mut docs) = fixture();
for dest in ["license-mit", "../NOTICE", "/NOTICE", "C:/NOTICE", "a\\NOTICE", "a//NOTICE", "CON.txt", "NOTICE."] {
docs.document.get_mut(2).unwrap().destination = dest.into();
assert!(!check(&root, &docs, &assets::Manifest::default()).is_empty(), "{dest}");
}
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn removing_required_mapping_fails() {
let (root, mut docs) = fixture();
docs.document.retain(|d| d.source != "LICENSE-MIT");
assert!(check(&root, &docs, &assets::Manifest::default()).iter().any(|p| p.contains("LICENSE-MIT: required document has no")));
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn bundled_documents_must_match_attribution() {
let (root, mut docs) = fixture();
let mut m = assets::Manifest::default();
m.bundled.push(assets::Bundled {
krate: "synthetic".into(),
version: "1.0.0".into(),
path: "font.ttf".into(),
title: "test".into(),
author: "test".into(),
source: "https://example.org".into(),
licence: "MIT".into(),
kind: "font".into(),
usage: "test".into(),
sha256: "a".repeat(64),
adobe_data: false,
});
assert!(check(&root, &docs, &m).iter().any(|p| p.contains("no licence document mapping")));
docs.bundled.push(Bundled {
krate: "synthetic".into(),
path: "font.ttf".into(),
licence: "MIT".into(),
documents: vec!["LICENSE-MIT".into()],
});
assert!(check(&root, &docs, &m).is_empty());
docs.bundled.get_mut(0).unwrap().documents = vec!["LICENSE-APACHE".into()];
assert!(check(&root, &docs, &m).iter().any(|p| p.contains("inapplicable")));
docs.bundled.get_mut(0).unwrap().documents = vec!["LICENSE-MIT".into(), "LICENSE-APACHE".into()];
assert!(check(&root, &docs, &m).iter().any(|p| p.contains("inapplicable mapped document")));
docs.bundled.get_mut(0).unwrap().documents = vec!["LICENSE-MIT".into()];
docs.bundled.get_mut(0).unwrap().licence = "ISC".into();
assert!(check(&root, &docs, &m).iter().any(|p| p.contains("disagrees")));
docs.bundled.get_mut(0).unwrap().documents.clear();
assert!(check(&root, &docs, &m).iter().any(|p| p.contains("documents missing")));
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn ocr_metadata_and_destinations_are_checked_without_models() {
let (root, docs) = fixture();
let mut m = assets::Manifest::default();
m.fetched.push(assets::Fetched {
file: "synthetic.rten".into(),
url: "https://example.org/model".into(),
title: "test model".into(),
author: "test".into(),
source: "https://example.org/card".into(),
licence: "CC-BY-SA-4.0".into(),
licence_url: "https://example.org/licence".into(),
licence_sha256: "a".repeat(64),
kind: "model".into(),
usage: "test".into(),
sha256: "b".repeat(64),
});
assert!(check(&root, &docs, &m).is_empty());
m.fetched.get_mut(0).unwrap().licence_sha256 = "z".repeat(64);
assert!(check(&root, &docs, &m).iter().any(|p| p.contains("incomplete OCR")));
m.fetched.get_mut(0).unwrap().file = "../outside.rten".into();
assert!(check(&root, &docs, &m).iter().any(|p| p.contains("unsafe OCR")));
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn required_ocr_models_cannot_be_missing() {
let source = include_str!("../../crates/ocr/src/lib.rs");
let original = assets::load(&assets::root()).unwrap();
assert_eq!(original.fetched.iter().filter(|f| f.kind == "model").count(), 2);
assert!(check_required_ocr_metadata(source, &original).unwrap().is_empty());
for model in original.fetched.iter().filter(|f| f.kind == "model") {
let mut m = assets::load(&assets::root()).unwrap();
m.fetched.retain(|f| f.file != model.file);
assert!(check_required_ocr_metadata(source, &m).unwrap().iter().any(|p| p.contains(&model.file) && p.contains("missing")));
}
}
#[test]
fn required_ocr_models_cannot_be_misclassified() {
let source = include_str!("../../crates/ocr/src/lib.rs");
let original = assets::load(&assets::root()).unwrap();
assert_eq!(original.fetched.iter().filter(|f| f.kind == "model").count(), 2);
for model in original.fetched.iter().filter(|f| f.kind == "model") {
let mut m = assets::load(&assets::root()).unwrap();
m.fetched.iter_mut().find(|f| f.file == model.file).unwrap().kind = "font".into();
assert!(check_required_ocr_metadata(source, &m).unwrap().iter().any(|p| p.contains(&model.file) && p.contains("kind")));
}
}
#[test]
fn licence_alternatives_and_combined_obligations_are_distinct() {
let mit = BTreeSet::from(["MIT"]);
assert!(licence_coverage("MIT OR Apache-2.0", &mit).unwrap());
assert!(!licence_coverage("MIT AND Apache-2.0", &mit).unwrap());
let both = BTreeSet::from(["MIT", "Bitstream-Vera"]);
assert!(licence_coverage("MIT AND Bitstream-Vera", &both).unwrap());
assert!(!licence_coverage("MIT OR Apache-2.0", &BTreeSet::from(["OFL-1.1"])).unwrap());
for expression in ["", "MIT AND", "MIT OR Apache-2.0 AND ISC", "(MIT OR Apache-2.0)", "MIT WITH exception"] {
assert!(licence_coverage(expression, &both).is_err(), "{expression}");
}
}
#[test]
fn changed_ocr_constant_syntax_fails_closed() {
assert!(check_required_ocr_metadata("", &assets::Manifest::default()).is_err());
}
#[test]
fn parent_file_destination_conflicts_fail() {
let (root, mut docs) = fixture();
docs.document.get_mut(2).unwrap().destination = "LICENSE-MIT/NOTICE".into();
assert!(check(&root, &docs, &assets::Manifest::default()).iter().any(|p| p.contains("parent file")));
std::fs::remove_dir_all(root).unwrap();
}
#[cfg(unix)]
#[test]
fn symlink_escape_fails() {
let (root, docs) = fixture();
let outside = root.with_extension("outside");
std::fs::write(&outside, "synthetic licence text\n").unwrap();
std::fs::remove_file(root.join("LICENSE-MIT")).unwrap();
std::os::unix::fs::symlink(&outside, root.join("LICENSE-MIT")).unwrap();
assert!(check(&root, &docs, &assets::Manifest::default()).iter().any(|p| p.contains("outside checkout")));
std::fs::remove_dir_all(root).unwrap();
std::fs::remove_file(outside).unwrap();
}
}
+3 -1
View File
@@ -8,6 +8,7 @@ mod demo_pdf;
mod fuzz;
mod gates;
mod layers;
mod license_check;
mod macos_integration;
mod parity;
mod screenshots;
@@ -23,11 +24,12 @@ const COMMANDS: &[(&str, &str, Command)] = &[
("deny", "Dependency licences, bans, sources and advisories (deny.toml; needs cargo-deny)", gates::deny),
(
"ci",
"fmt --check, clippy -D warnings, the CLI without default features, test, i18n-report, layers, wasm, assets, deny, parity (stops at first failure)",
"fmt --check, clippy -D warnings, the CLI without default features, test, i18n-report, layers, wasm, license-check, deny, parity (stops at first failure)",
gates::ci,
),
("test", "Workspace tests: cargo nextest (cargo test when it is missing), then doctests", gates::test),
("i18n-report", "Print the strings each interface language lacks (counts and a few keys); never fails", gates::i18n_report),
("license-check", "Offline source licence documents, attribution and metadata", license_check::run),
("assets", "Enforce the asset policy (AGENTS.md §1) against ATTRIBUTION.toml; --write regenerates ATTRIBUTION.md", assets::run),
("corpus", "Fetch the pinned pdf.js test corpus into corpus/ (git-ignored) and verify it; --update-pin re-records the pin", gates::corpus),
("check", "Robustness sweep over corpus/ with pdfcraft-cli; fails on crashes or regressions vs xtask/baselines", gates::check),