Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ATTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).

## In this repository (272)
## In this repository (274)

| Asset | Title | Author | Licence | Source | Used for |
|---|---|---|---|---|---|
Expand Down Expand Up @@ -280,6 +280,8 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `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 | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.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 | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.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 |
| `assets/icons/scroll-continuous.svg` | Continuous scrolling indicator | PdfCraft local mod contributors (Codex-generated) | MIT OR Apache-2.0 | Contributor-original SVG generated for the page-display rail on 2026-10-16; colour follows the UI theme | Right-rail scrolling mode toggle |
| `assets/icons/scroll-normal.svg` | Normal page scrolling indicator | PdfCraft local mod contributors (Codex-generated) | MIT OR Apache-2.0 | Contributor-original SVG generated for the page-display rail on 2026-10-16; colour follows the UI theme | Right-rail scrolling mode toggle |

## Compiled in through dependencies (6)

Expand Down
22 changes: 22 additions & 0 deletions ATTRIBUTION.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3376,3 +3376,25 @@ licence_file = "vendor/vello_common/LICENSE-MIT"
kind = "image"
usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file"
sha256 = "78c59a0cfd2c829909d77006f94e34dac1ae5d1f90e7f10f72e84de541fef697"

[[asset]]
path = "assets/icons/scroll-continuous.svg"
title = "Continuous scrolling indicator"
author = "PdfCraft local mod contributors (Codex-generated)"
source = "Contributor-original SVG generated for the page-display rail on 2026-10-16; colour follows the UI theme"
licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "icon"
usage = "Right-rail scrolling mode toggle"
sha256 = "25d1bb17776ce38ac0af3c59c996cc4e0e196fc141fe8ac5d6186f6c0693105f"

[[asset]]
path = "assets/icons/scroll-normal.svg"
title = "Normal page scrolling indicator"
author = "PdfCraft local mod contributors (Codex-generated)"
source = "Contributor-original SVG generated for the page-display rail on 2026-10-16; colour follows the UI theme"
licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "icon"
usage = "Right-rail scrolling mode toggle"
sha256 = "daf792ab90d53535c7cf8b26a7f0c95aff660ed9fc2fdea096bc0c6af8d066b5"
4 changes: 3 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
| 8 | 1.0 polish: performance, right-to-left, missing languages, keyboard-only (M14) | 55–95 |

## Progress log

- **2026-10-16 (M3, page-display rail toggles):** The right rail has independent icon-only toggles for single/two-page layout and continuous/normal scrolling, plus a gear for the existing Page display menu. Buttons use the neutral fit-toggle style; original scrolling SVGs are attributed under the project licence. Focused regressions cover repeated toggling without editing document bytes or changing the other mode; native Spanish clicks and screenshots verify both icon states and the settings menu. Estimates unchanged.

- **2026-10-15 (M2, gap 12):** `cargo xtask text-oracle` scores reading order as well as word F1: the longest common subsequence of the two word sequences over the longer one. F1 is a bag of words and cannot see order at all -- measuring #2431, a change that moves text and nothing else, it called 26 of the 29 files whose output changed identical, where the order score separated 18 improvements from 6 regressions. Reports, does not gate; a disagreement with Poppler, not a verdict. Estimates unchanged.
- **2026-10-15 (M2, #2305 follow-up):** WinAnsiEncoding 0xA0 and 0xAD and MacRomanEncoding 0xCA extract as the ordinary space and hyphen Annex D.2 names them, in the vendored hayro tables (patch 30) as well as `crates/fonts`, so search, copy and redaction see characters that were hidden behind U+00A0 and U+00AD. Across the pdf.js corpus the soft hyphens we extract fall 117 to 13 and the non-breaking spaces 30 to 26; the rest come from `/ToUnicode`. An explicit `/Differences` name keeps its glyph list value, as pdftotext does. Estimates unchanged.
- **2026-10-13 (community, M3, #1569 review):** Pixel-only Wayland wheel, wheel-tilt and continuous input use standalone moved events, so Ctrl zoom does not remain latched after the modifier is released (#991); vendored winit patch (13), with a policy test. Estimates unchanged.
Expand Down Expand Up @@ -308,7 +311,6 @@ Newest first: the date, what moved, and the new overall estimate.
- **2026-10-11 (M14, #307, Plan 2):** Forms now return terminal fields and the page-widget adoption count from one enumeration, and engine open reuses that result while retaining XFA script-button marking. The 12,000-page/no-form PDF (1,442,052 bytes) and 2,500-orphan-widget PDF (673,432 bytes) produce byte-identical `doc_open`/`doc_info` outputs against upstream `33c14e07cfd742a82bf03baa6b103c01008fa68c`, including the orphan repair note. After one warm-up per binary and three interleaved base/candidate rounds, median open times were 0.16/0.15 s and 0.08/0.07 s, with median peak RSS 62.69/59.98 MiB and 33.28/32.20 MiB. Workspace tests, Clippy, formatting, focused forms/engine/organize tests, and the WASM check pass. A behavior-preserving local binding in `organize` also lets the workspace Clippy gate pass. No estimate change.
- **2026-10-11 (M14, Traditional Chinese semantics):** Corrected accessibility pass/fail labels, the inverted script-accessibility rule, Taiwan row/column terminology and form choice/read-only labels; added the missing table-row explanation. Catalog regressions cover the rule metadata and preserve field names. Milestone and effort estimates unchanged.


- **2026-10-11 (M14, no silent `?`):** Text PdfCraft's fonts can't draw (Chinese, Greek, Czech's ř in Helvetica) is never written into a PDF as `?` without a word. Form values and button captions, text boxes, callouts, Fill & Sign text, measurement labels and dynamic stamps' by-lines are kept as typed, as Acrobat keeps them: the appearance draws `?` there, the annotation is marked (`PCUndrawn`), and the edit says so (the interface shows a translated notice; tools return `warnings`). A form also sets `/NeedAppearances`, so other viewers redraw the field from its value (not in a PDF/A document, which forbids it); a restyle doesn't replace a comment another program drew with those characters. Where `?` would be permanent, the edit is refused with the character and what to do instead: flattening what PdfCraft drew with `?`, redaction overlay text (also checked before anything is redacted), typed signatures through `fill_sign_add`, visible signature details, and new documents from text (with the line). Reports PdfCraft writes from documents (comment summaries, compare reports) write such characters as `[U+XXXX]` and say so; a dynamic XFA form's own text is laid out and reported. One end-to-end test drives every path through the tools. Not yet: drawing those characters (embedding a font that has them). Overall estimate unchanged.
- **2026-10-10 (M14, interface size):** Preferences ▸ Interface size scales the whole interface (50–250 %, setting `ui_scale`, control option `ui-scale`); 50 % brings an interface the system draws at 200 % back to 100 % (#456). Auto, the default, follows the desktop's text scaling: on Linux and the BSDs the desktop app reads GNOME's `text-scaling-factor` once at launch, so with Large Text (1.25) on a 1× display the interface no longer draws 13 px labels that show their pixels next to the system's text. The size is egui's zoom factor, so `pixels_per_point` grows with it and page rasters stay one texel per screen pixel; egui's own ⌘+/⌘− interface zoom is turned off so those keys stay page zoom. Still missing: KDE's font DPI and Windows' "Make text bigger" aren't read (both scale only text there), and egui's text is still greyscale-antialiased without the system's gamma, so small text looks rougher than GTK's at 100 %.
- **2026-10-10 (M14, #496):** Shortcut displays share a UI formatter across toolbar search, registry menus, command palette, Help and tooltips. Modifier and key names use a dedicated catalog context in every bundled translation; missing entries keep the English key name rather than a command translation. German shows Strg/Umschalt/Entf, macOS retains its symbols, and runtime language changes refresh the display. Regression coverage checks localized layout widths, English/German switching, platform conventions and unchanged command execution. Milestone and effort estimates unchanged.
Expand Down
1 change: 1 addition & 0 deletions assets/icons/scroll-continuous.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/scroll-normal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 15 additions & 2 deletions crates/ui-egui/src/chrome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,15 +675,28 @@ pub fn right_rail(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
if icons::button(ui, "zoom-in", 32.0, false, &tip).clicked() {
view.zoom_step(true);
}
// Between rotate and zoom, as in Acrobat. Its command runs once `view` is free.
// Keep both layouts directly reachable. Retain the popup for scrolling,
// cover-page and zoom options; commands run once `view` is free.
let tip = crate::i18n::fmt(tl!("Page display: {layout}"), &[("layout", tl!(view.layout.label()))]);
let resp = icons::button(ui, view.layout.icon(), 32.0, false, &tip);
let resp = icons::button(ui, "settings", 32.0, false, &tip);
page_display = egui::Popup::menu(&resp)
.show(|ui| {
ui.set_min_width(220.0);
rail_view_menu(ui, view)
})
.and_then(|r| r.inner);
let continuous = view.continuous();
let scroll_icon = if continuous { "scroll-continuous" } else { "scroll-normal" };
if icons::button(ui, scroll_icon, 32.0, false, tl!("Continuous scrolling")).clicked() {
page_display = Some("view.continuous");
}
let next_layout = match view.layout {
PageLayout::Single => PageLayout::TwoUp,
PageLayout::TwoUp => PageLayout::Single,
};
if icons::button(ui, view.layout.icon(), 32.0, false, tl!(next_layout.label())).clicked() {
page_display = Some(next_layout.command());
}
let tip = crate::commands::key_tip(ui.ctx(), tl!("Rotate view clockwise ({key})"), crate::commands::ROTATE_CW);
if icons::button(ui, "rotate-cw", 32.0, false, &tip).clicked() {
view.rotate_view(true);
Expand Down
5 changes: 4 additions & 1 deletion crates/ui-egui/src/icon_data.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
//! Lucide icons (ISC licence, see assets/icons/LICENSE-lucide.txt), embedded and tinted at runtime.
//! Embedded icons, tinted at runtime. Lucide icons use ISC; local scrolling icons use MIT OR Apache-2.0.
//! See ATTRIBUTION.toml for per-file provenance.
//! Generated from the contents of assets/icons/.

pub static ICONS: &[(&str, &[u8])] = &[
("scroll-continuous", include_bytes!("../../../assets/icons/scroll-continuous.svg")),
("scroll-normal", include_bytes!("../../../assets/icons/scroll-normal.svg")),
("accessibility", include_bytes!("../../../assets/icons/accessibility.svg")),
("align-center", include_bytes!("../../../assets/icons/align-center.svg")),
("align-justify", include_bytes!("../../../assets/icons/align-justify.svg")),
Expand Down
2 changes: 1 addition & 1 deletion crates/ui-egui/src/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ mod tests {

#[test]
fn shared_catalog_preserves_existing_icons_and_fallback_uri() {
assert_eq!(ICONS.len(), 186);
assert_eq!(ICONS.len(), 188);
assert!(exists("minimize-2"));
for (name, original) in ICONS {
let expected =
Expand Down
53 changes: 53 additions & 0 deletions crates/ui-egui/tests/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,59 @@ fn cover_page_command_needs_two_page_view() {
assert!(h.state().views[0].cover);
}

#[test]
fn rail_layout_button_alternates_without_a_menu() {
use pdfcraft_ui_egui::canvas::PageLayout;

let (mut h, c) = harness();
let before = h.state().session.get(h.state().views[0].id).unwrap().bytes.clone();
let mut button_rect = None;
for continuous in [false, true] {
ok(&mut h, &c, "ui.set", json!({"key": "continuous", "value": if continuous { "on" } else { "off" }}));
for (label, previous_label, layout) in [
("Two-page view", "Single page", PageLayout::TwoUp),
("Single page", "Two-page view", PageLayout::Single),
("Two-page view", "Single page", PageLayout::TwoUp),
("Single page", "Two-page view", PageLayout::Single),
] {
let found = ok(&mut h, &c, "ui.inspect", json!({"query": label}));
let widgets = found["widgets"].as_array().unwrap();
let button = widgets.iter().find(|w| w["label"] == label && w["clickable"] == true).unwrap();
if let Some(rect) = &button_rect {
assert_eq!(&button["rect"], rect);
} else {
button_rect = Some(button["rect"].clone());
}
let previous = ok(&mut h, &c, "ui.inspect", json!({"query": previous_label}));
assert!(!previous["widgets"].as_array().unwrap().iter().any(|w| w["label"] == previous_label && w["clickable"] == true));
ok(&mut h, &c, "ui.click", json!({"label": label}));
assert_eq!(h.state().views[0].layout, layout);
assert_eq!(h.state().views[0].continuous, continuous);
}
}
let doc = h.state().session.get(h.state().views[0].id).unwrap();
assert_eq!(doc.bytes, before);
assert!(!doc.dirty);
}

#[test]
fn rail_continuous_button_toggles_without_changing_page_layout_or_document() {
let (mut h, c) = harness();
let before = h.state().session.get(h.state().views[0].id).unwrap().bytes.clone();
for layout in ["single", "two-page"] {
ok(&mut h, &c, "ui.set", json!({"key": "layout", "value": layout}));
let base_layout = h.state().views[0].layout;
for expected in [true, false, true, false] {
ok(&mut h, &c, "ui.click", json!({"label": "Continuous scrolling"}));
assert_eq!(h.state().views[0].continuous(), expected);
assert_eq!(h.state().views[0].layout, base_layout);
}
}
let doc = h.state().session.get(h.state().views[0].id).unwrap();
assert_eq!(doc.bytes, before);
assert!(!doc.dirty);
}

#[test]
fn commands_keys_and_typing() {
let (mut h, c) = harness();
Expand Down
8 changes: 4 additions & 4 deletions crates/ui-egui/tests/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ fn rail_page_display_menu_offers_acrobats_view_choices() {
let pick = |h: &mut Harness<'static, PdfCraftApp>, item: &str| {
h.get_by_label_contains("Page display:").click();
h.run_steps(3);
h.get_by_label(item).click();
h.get_all_by_label(item).last().unwrap().click();
h.run_steps(4);
};
let shown = |h: &Harness<'static, PdfCraftApp>| (h.state().views[0].layout, h.state().views[0].continuous(), h.state().views[0].fit);
Expand Down Expand Up @@ -668,7 +668,7 @@ fn rail_page_display_button_switches_layouts() {
h.run_steps(4);
h.get_by_label_contains("Page display").click();
h.run_steps(3);
h.get_by_label("Single page").click();
h.get_all_by_label("Single page").last().unwrap().click();
h.run_steps(4);
assert_eq!(h.state().views[0].layout, pdfcraft_ui_egui::canvas::PageLayout::Single);
}
Expand All @@ -682,7 +682,7 @@ fn view_menu_page_display_uses_the_same_commands() {
h.run_steps(2);
h.get_by_label("View ⏵").hover();
h.run_steps(3);
h.get_by_label("Single page").click();
h.get_all_by_label("Single page").last().unwrap().click();
h.run_steps(4);
assert_eq!(h.state().views[0].layout, pdfcraft_ui_egui::canvas::PageLayout::Single);
}
Expand All @@ -693,7 +693,7 @@ fn palette_runs_page_display_commands_from_the_keyboard() {
let mut h = harness(&[]);
h.state_mut().set_option("palette", "single page").unwrap();
h.run_steps(3);
h.get_by_label("Single page");
h.get_all_by_label("Single page").last().unwrap();
h.key_press(Key::Enter);
h.run_steps(3);
assert!(!h.state().palette_open, "Enter runs the top hit and closes the palette");
Expand Down
Loading