diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index ab293483..45ede1bb 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "fbc7790b5e9afd591b3bfbe0582f004f345b42d988989d81fdf38f312331fa01" +sha256 = "bd7d565bff34f6f73b90b0884f480b9f39b106d5c96953e17c68619f03385a57" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "fc734b2f2530561f8630f5d7112a237201012789b1d9167d3068b4ddb7b22f43" +sha256 = "de11e8f3b126092b584beb8b22ea8952ff6cbc79d42c417663843be93c807bc4" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "379d0dc6203104e0f07ea0ed1e74a365c8eec7f9cf2bf525078dac0cf58b6372" +sha256 = "8ddfd156833e29d8584dab85520264d8e5d77bfcdb5ea994577acc1a9644936b" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b416309efba61009d4ed92c932cf1b23bb4f3a53d0d7d35cf61bf9c3d7a011ef" +sha256 = "67abad667c05dc10bc9bffab5302978bc73b8314f63f78b543cdece4b5cea6d7" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "066efa560b0dc5554c85194dcfb3190e03fce75145d52151bdcf7734f1829f7e" +sha256 = "a8d35613c888904ce6e984560c06d54a3c6f9828fa840c733eb9f2f893b8c0ee" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9061bbb9b21008b4c312b5158ed3c338cfbcb3b563e9e57b81cf72d075c9eadf" +sha256 = "9f8a39143000bc909ee24617518c41f3aefaaeace341002e403d94a9eb51c984" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "068211fb4b246e1cb0ecb66598101f1c974e0313e1323e6acd522817f7f3bb57" +sha256 = "a099eb8c6c4bdbd62dc32efef97b4bef718c0170492e6921568d48e46499c964" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a2228f72e63c83959ea0ee7b1641bcf31e13b3c58fab62caebbd50f8d8d5c231" +sha256 = "e59dc41d7628503fd241689efb69641bcf31733a204f3cd492ac88d66c76c6a0" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8c2b1625fb84fa7c9279d2ca0d6a22c808514d6361cf3c547a7ee21c64d170ae" +sha256 = "c6ab6587ad3ecd56efb019a18eb71f92fca33c43ead8eb9fc3262b8c56e5beb1" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "394d322e782343f95c3b9cce603a77b7e3a6625000540a17f45b55b7233e7662" +sha256 = "1fa0e8bba340d78ecf2a3c9ae319b34e99811082950ebf4aee5e7f5eb0bdab22" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5c5c0420cf58767312b132b9f9cb3c23131b58b831feae3e47b421f1081fe60b" +sha256 = "4297893b78fa25368d9bb6e10485fb3ede866ed470e528001de0120ea2f0915b" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1d130f3f7112ac99693c5048a96405f3371dbe893d5c618f2910b8e80b6be28c" +sha256 = "a84ccfd96c9704d1630d6b42b1c50298a0ea5791b063ff2bbe1ccc993f87cc47" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a6ff8309a391acc428cb67e834196e14919ab5cc0f2d9befcbfb532d2ec4ca82" +sha256 = "da6aee2a47ec15193a3d88f8116f2b81ec1ac815b9481eb9c36d48518625df30" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "40391d7be25e2a4e59cb1419df733935e69dd728fb0a3404575edc6807f27924" +sha256 = "122cc1b0a346a8ff4cb7ede77775afbd326e82c9bfd6bda06f895ac100702144" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "27fa6fae5ad0005f90a13612aa1eb8189061110eab8d6b6e7a83899af54f13e9" +sha256 = "5eb3ccd3c7613fd54b4a217ddaf32f29a37efb29373787a3178209ae3aca12a5" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f4e6217dfc370ffa947c07b8c23bfa7d8f3824d881d7d51062b471b95f378c82" +sha256 = "ffbf8a2c117d75b008361e882c96c9d7cad8844e791800a60174eacf96e3c89a" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c3f539ce99a39a6f7a89740da9b7aab9643014613bfef1891efe777dc5cf057a" +sha256 = "424e5f16bead85250d142e791b5e8a874c9905025be052ed8aef1f1645e5585a" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1cf9e140b348f1e8ceb399a8f079cae392085d9f432e3e1b9d94cd1039a1c4b8" +sha256 = "63d26bec54aa6e9f9c4b6e4dc16b872a41ace7feec825c888633992ca1507148" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9d52789a71fe9b0a7cc74cfd6e9e6d0a8b3d9b9a866922676e376f1431d1cae3" +sha256 = "0b330a5e739159d02a8214af4655e954cba59861ca30b914614ffb0f2735e52d" [[asset]] path = "assets/icons/accessibility.svg" @@ -3339,4 +3339,4 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c5e3a75359a8faadb9efa9ac4ff31963cf9124c20269a1e21715bcabde5e1292" +sha256 = "7071df48ebdb362914f404deebebc84252550d10d08f333892584230efa6c1d6" diff --git a/ROADMAP.md b/ROADMAP.md index bbc80241..2fd38f60 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,8 @@ **Stage: alpha** · next: beta, ~26% (ready for real work 49% → 75%) and ~400–700 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (shared print CI reconciliation and driver duplex capability; M10, #1028: OCR reads pictures on pages that have text, on request); earlier: M14 #307 Plan 3 uses one marker traversal on open and full refresh, and the combined scanner borrows metadata instead of cloning scan-only containers; includes a behavior-preserving `organize` binding for the workspace Clippy gate · earlier: minor (localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M2 short same-line redraws split before placement; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (M10, #1028: OCR reads pictures on pages that have text, on request); earlier: M14 #307 Plan 3 uses one marker traversal on open and full refresh, and the combined scanner borrows metadata instead of cloning scan-only containers; includes a behavior-preserving `organize` binding for the workspace Clippy gate · earlier: minor (localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (#594 paragraph drags validated in the Linux test environment; localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; earlier: second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (architectural paper controls from #599; localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; earlier: second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (closed issues synced: #774, #756 and the #799–#821 batch; earlier: localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; earlier: second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -136,6 +137,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-12 (M2, short text redraws):** Same-line redraws that restart a matching three-glyph prefix after more than one glyph-height of progress split before placement, so short doubled words no longer interleave. The existing redraw-position tolerance is reused; controls cover negative TJ movement, combining marks, RTL, CJK, vertical and turned text. Estimates unchanged. - **2026-10-11 (M10.5, focused follow-up to #599):** Carry the architectural/ANSI/large ISO paper presets, current-page matching at 100%, and previewed PDF scale into the current print dialog and `doc_print`. Retains the #847/#1007 spooler and background discovery. Driver substitution, physical calibration, vector Windows printing and managed-endpoint restrictions remain separate gaps. Overall estimates unchanged. Newest first: the date, what moved, and the new overall estimate. diff --git a/apps/pdfcraft/tests/winit_x11_dpi.rs b/apps/pdfcraft/tests/winit_x11_dpi.rs new file mode 100644 index 00000000..fed0cf3e --- /dev/null +++ b/apps/pdfcraft/tests/winit_x11_dpi.rs @@ -0,0 +1,14 @@ +#[test] +fn winit_carries_the_x11_server_dpi_fallback() { + // The behavior tests live beside the policy in winit (util::randr::tests). + // This guards PdfCraft's vendored copy, which workspace tests do not test itself. + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let randr = std::fs::read_to_string(root.join("vendor/winit/src/platform_impl/linux/x11/util/randr.rs")).unwrap(); + assert!(randr.contains("let screen = self.default_root();")); + assert!(randr.contains("default_dpi_factor(\n self.get_xft_dpi(),")); + assert!(randr.contains("screen.width_in_millimeters.into()")); + assert!(randr.contains("calc_dpi_factor(screen_pixels, screen_mm)")); + assert!(randr.contains("server_dpi_wins_over_the_panels_physical_density")); + let desktop = std::fs::read_to_string(root.join("packaging/linux/ai.storyteller.pdfcraft.desktop")).unwrap(); + assert!(!desktop.contains("WINIT_X11_SCALE_FACTOR")); +} diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index de791e5a..ddd7b9ac 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -740,6 +740,10 @@ const REDRAW_CANDIDATES: usize = 32; /// Pair tests that the collision sweep may make per glyph of a segment. Ordinary text overlaps each glyph with about one /// neighbour, so it stays far below this. A segment that needs more is dense, and [`collide`] joins its runs instead. const COLLIDE_PAIRS_PER_GLYPH: usize = 16; +/// Glyph positions within a fifth of their height are considered the same for redraws. +const REDRAW_POSITION_TOLERANCE: f32 = 0.2; +/// A reset needs three matching glyphs at the start of the run, keeping local kerning from splitting it. +const REDRAW_PREFIX_GLYPHS: usize = 3; /// Reusable buffers, so a page allocates them once rather than per segment. Glyph placement works in them, one /// segment at a time, when the segment is emitted. @@ -818,13 +822,44 @@ fn redraws(glyphs: &[&TextGlyph], a: Run, b: Run) -> bool { a.len() == b.len() && a.range().zip(b.range()).all(|(i, j)| match (glyphs.get(i), glyphs.get(j)) { (Some(p), Some(g)) => { - let tol = 0.2 * (g.rect[3] - g.rect[1]).max(0.1); + let tol = REDRAW_POSITION_TOLERANCE * (g.rect[3] - g.rect[1]).max(0.1); p.text == g.text && (p.rect[0] - g.rect[0]).abs() < tol && (p.rect[1] - g.rect[1]).abs() < tol } _ => false, }) } +fn within_position_tolerance(a: f32, b: f32, tolerance: f32) -> bool { + (a - b).abs().partial_cmp(&tolerance) == Some(std::cmp::Ordering::Less) +} + +/// Whether `next` restarts `start` after the run has advanced by a glyph height. The fixed prefix +/// bounds this check and requires more than a single backward adjustment to split a run. +fn restarts_run(glyphs: &[&TextGlyph], start: usize, next: usize, progress: f32) -> bool { + let (Some(first), Some(candidate)) = (glyphs.get(start), glyphs.get(next)) else { return false }; + let tol = REDRAW_POSITION_TOLERANCE * (candidate.rect[3] - candidate.rect[1]).max(0.1); + if first.text != candidate.text + || !within_position_tolerance(first.rect[0], candidate.rect[0], tol) + || !within_position_tolerance(first.rect[1], candidate.rect[1], tol) + { + return false; + } + let h = (first.rect[3] - first.rect[1]).max(0.1); + if !progress.is_finite() || progress <= h { + return false; + } + for offset in 1..REDRAW_PREFIX_GLYPHS { + let (Some(a), Some(b)) = (start.checked_add(offset).and_then(|i| glyphs.get(i)), next.checked_add(offset).and_then(|i| glyphs.get(i))) else { + return false; + }; + let tol = REDRAW_POSITION_TOLERANCE * (b.rect[3] - b.rect[1]).max(0.1); + if a.text != b.text || !within_position_tolerance(a.rect[0], b.rect[0], tol) || !within_position_tolerance(a.rect[1], b.rect[1], tol) { + return false; + } + } + true +} + /// Puts a segment's glyphs in visual reading order into `sc.placed`, and its forced word breaks into `sc.breaks`. /// /// Glyphs are placed by x. Runs that collide, meaning that glyphs of different runs overprint each other, stay whole @@ -1059,6 +1094,8 @@ fn arrange(glyphs: &[TextGlyph]) -> (Vec, Vec, Vec) { // 1. Segments in content order. Every run is a range of consecutive glyphs. let mut segs: Vec = Vec::new(); + let mut run_start_x = 0.0; + let mut furthest_progress = 0.0; for i in 0..n { let g = glyphs[i].rect; let cont = segs.last().is_some_and(|s| { @@ -1067,12 +1104,19 @@ fn arrange(glyphs: &[TextGlyph]) -> (Vec, Vec, Vec) { let gap = g[0] - glyphs[p].rect[2]; (cy(i) - cy(p)).abs() < ph * 0.5 && gap < ph * 3.0 && g[0] > glyphs[p].rect[0] - ph * 2.0 }); - if cont && let Some(s) = segs.last_mut() { + let reset = cont && segs.last().is_some_and(|s| restarts_run(&glyphs, s.first.start, i, furthest_progress)); + if cont + && !reset + && let Some(s) = segs.last_mut() + { s.first.end = i + 1; union(&mut s.bbox, &g); s.h = s.h.max(height(i)); + furthest_progress = furthest_progress.max((g[0] - run_start_x).abs()).max((g[2] - run_start_x).abs()); } else { segs.push(Seg::new(Run { start: i, end: i + 1 }, g, height(i))); + run_start_x = g[0]; + furthest_progress = 0.0; } } // A run that redraws an earlier run at the same place (doubled or shadowed text) is dropped, as a doubled glyph is @@ -1606,6 +1650,69 @@ mod tests { extract_page(&text_page(ops), 0, &InterpreterSettings::default()).unwrap().plain_text() } + #[test] + fn short_doubled_word_at_same_origin_does_not_interleave() { + let ops = "BT /F1 12 Tf 50 Tz 1 0 0 1 100 200 Tm (world) Tj ET\n".repeat(2); + assert_single_word(page_glyphs(&ops)); + } + + #[test] + fn short_near_shadow_does_not_interleave() { + let ops = "BT /F1 12 Tf 50 Tz 1 0 0 1 100 200 Tm (world) Tj ET\nBT /F1 12 Tf 50 Tz 1 0 0 1 101 200 Tm (world) Tj ET\n"; + assert_single_word(page_glyphs(ops)); + } + + #[test] + fn turned_short_doubled_word_does_not_interleave() { + let one = turned(37.0, 1, 12.0, 100.0, 200.0, "50 Tz (world) Tj"); + assert_single_word(page_glyphs(&(one.clone() + &one))); + } + + #[test] + fn large_backward_tj_adjustment_stays_in_one_run() { + let text = page_glyphs("BT /F1 12 Tf 50 Tz 1 0 0 1 100 200 Tm [(wor) 3000 (ld)] TJ ET"); + assert_eq!(text.plain_text(), "ld wor"); + assert_eq!(text.glyphs.iter().map(|g| g.text.as_str()).collect::>(), ["l", "d", "w", "o", "r"]); + assert!(text.line_of.iter().all(|line| *line == 0)); + assert_eq!(text.space_before, [false, false, true, false, false]); + } + + #[test] + fn reset_prefix_requires_one_glyph_height_of_progress() { + let mut glyphs = Vec::new(); + word(&mut glyphs, "abc", 10.0, 10.0, 6.0); + word(&mut glyphs, "abc", 10.0, 10.0, 6.0); + let glyphs: Vec<&TextGlyph> = glyphs.iter().collect(); + assert!(!restarts_run(&glyphs, 0, 3, 9.99)); + assert!(!restarts_run(&glyphs, 0, 3, 10.0)); + assert!(restarts_run(&glyphs, 0, 3, 10.01)); + } + + #[test] + fn reset_prefix_uses_the_existing_redraw_position_tolerance() { + let mut near = Vec::new(); + word(&mut near, "abc", 10.0, 10.0, 6.0); + word(&mut near, "abc", 11.99, 10.0, 6.0); + let near: Vec<&TextGlyph> = near.iter().collect(); + assert!(restarts_run(&near, 0, 3, 10.01)); + + let mut far = Vec::new(); + word(&mut far, "abc", 10.0, 10.0, 6.0); + word(&mut far, "abc", 12.01, 10.0, 6.0); + let far: Vec<&TextGlyph> = far.iter().collect(); + assert!(!restarts_run(&far, 0, 3, 10.01)); + } + + #[test] + fn reset_prefix_rejects_non_finite_positions() { + let mut glyphs = Vec::new(); + word(&mut glyphs, "abc", 10.0, 10.0, 6.0); + word(&mut glyphs, "abc", 10.0, 10.0, 6.0); + glyphs[3].rect[0] = f32::NAN; + let glyphs: Vec<&TextGlyph> = glyphs.iter().collect(); + assert!(!restarts_run(&glyphs, 0, 3, 10.01)); + } + #[test] fn turned_lines_group_along_their_baseline() { // Two lines 13 pt apart, words separated by TJ offsets (no space glyphs), the second @@ -1703,6 +1810,13 @@ mod tests { extract_page(&text_page(ops), 0, &InterpreterSettings::default()).unwrap() } + fn assert_single_word(text: PageText) { + assert_eq!(text.plain_text(), "world"); + assert_eq!(text.glyphs.iter().map(|g| g.text.as_str()).collect::(), "world"); + assert_eq!(text.line_of, [0; 5]); + assert_eq!(text.space_before, [false; 5]); + } + #[test] fn runs_printed_over_each_other_stay_whole_at_any_angle() { // Two runs that start at one point (exact overprint), that start 40 pt along and 3 pt down diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index c8bf4a87..8e4f0399 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2995,6 +2995,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { view.viewport_screen = out.inner_rect; view.auto_scroll.paint(ui, avail); + if unobstructed && matches!(tool, QuickTool::Select | QuickTool::ColumnSelect) { + selection_actions(ui, avail, view, info, prefs, allowed && !comments_hidden); + } // Collect all main-page demand, including cached surfaces, for byte admission later. // Pan-settle debounce for tiled pages: each tile costs a full page interpret, so holding @@ -3263,6 +3266,92 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { quick_bar(app, avail, ui); } +#[derive(Clone, Copy)] +enum SelectionAction { + Note, + Markup(comments::CommentTool), + Replace, + Copy, +} + +/// Actions on an existing text selection, next to the selected words. +fn selection_actions(ui: &mut egui::Ui, viewport: Rect, view: &mut DocView, info: &DocInfo, prefs: &comments::CommentPrefs, can_comment: bool) { + if ui.input(|input| input.pointer.is_decidedly_dragging()) { + return; + } + let Some(selection) = view.selection else { return }; + let Some(text) = view.texts.get(&selection.page) else { return }; + let Some(xf) = view.page_xform(selection.page) else { return }; + let Some(last) = selection.rects(text).last().copied() else { return }; + let selected = selection.text(text); + if selected.is_empty() { + return; + } + let rect = xf.view_rect(last); + if !viewport.intersects(rect) { + return; + } + let x = rect.left().clamp(viewport.left() + 8.0, (viewport.right() - 520.0).max(viewport.left() + 8.0)); + let y = if rect.bottom() + 48.0 < viewport.bottom() { rect.bottom() + 6.0 } else { (rect.top() - 44.0).max(viewport.top() + 6.0) }; + let mut chosen = None; + egui::Area::new(egui::Id::new(("text-selection-actions", view.id.0))).order(egui::Order::Foreground).fixed_pos(pos2(x, y)).show(ui.ctx(), |ui| { + egui::Frame::popup(ui.style()).show(ui, |ui| { + ui.horizontal(|ui| { + for (label, action) in [ + (tl!("Add comment"), SelectionAction::Note), + (tl!("Highlight"), SelectionAction::Markup(comments::CommentTool::Highlight)), + (tl!("Underline"), SelectionAction::Markup(comments::CommentTool::Underline)), + (tl!("Strikethrough"), SelectionAction::Markup(comments::CommentTool::StrikeOut)), + (tl!("Replace text"), SelectionAction::Replace), + ] { + if ui.add_enabled(can_comment, egui::Button::new(label)).clicked() { + chosen = Some(action); + } + } + if ui.button(tl!("Copy")).clicked() { + chosen = Some(SelectionAction::Copy); + } + }); + }); + }); + let Some(action) = chosen else { return }; + if let SelectionAction::Copy = action { + ui.ctx().copy_text(selected); + return; + } + let Some((page, quads)) = view.selection_quads(info) else { return }; + let last = quads.last().copied().unwrap_or_default(); + let at = [last[2].max(last[6]), last[1].max(last[3])]; + view.clear_selection(); + match action { + SelectionAction::Note | SelectionAction::Replace => { + if matches!(action, SelectionAction::Replace) { + view.comments.replace = Some(quads); + } + view.comments.composer = Some(comments::Composer { + page, + at, + kind: if matches!(action, SelectionAction::Replace) { comments::ComposerKind::Replace } else { comments::ComposerKind::Note }, + text: String::new(), + focus: true, + }); + } + SelectionAction::Markup(tool) => { + // Every markup button carries a markup tool; anything else adds nothing. + let Some(kind) = tool.markup() else { return }; + view.pending_edit = Some(Edit::AddAnnotation(pdfcraft_engine::NewAnnotation { + page, + shape: pdfcraft_engine::Shape::TextMarkup { kind, quads }, + style: prefs.style(tool), + contents: String::new(), + author: prefs.author.clone(), + })); + } + // Copied above. + SelectionAction::Copy => {} + } +} + /// Run a push button's action (the ones that need no JavaScript engine). fn run_button(app: &mut PdfCraftApp, index: usize, name: &str, action: pdfcraft_engine::form_scripts::ButtonAction) { use pdfcraft_engine::form_scripts::ButtonAction as B; diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index af045c28..a90d8b3e 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2274,3 +2274,4 @@ scroll speed Fast سريعة Black and white أبيض وأسود This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… لا يقبل برنامج تشغيل هذه الطابعة إعدادات الطباعة على الوجهين من البرامج الأخرى، لذا يُطبَّق إعداده الخاص. غيّره من خصائص… Tools panel side جانب لوحة الأدوات + Add comment إضافة تعليق diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index a8772278..1e4b614a 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2216,3 +2216,4 @@ scroll speed Fast Висока Black and white Черно-бяло This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйверът на този принтер не приема настройки за двустранен печат от други програми и прилага собствената си настройка. Променете я от Свойства… Tools panel side Страна на панела с инструменти + Add comment Добавяне на коментар diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index cc52011e..04aabd83 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2330,3 +2330,4 @@ scroll speed Fast Rychlá This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Písmo tohoto pole neumí zobrazit „{c}“ ({code}), proto PdfCraft místo něj ukazuje ?. Hodnota se uloží a jiné prohlížeče PDF pole podle ní překreslí. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Písma, kterými PdfCraft píše, neumí zobrazit „{c}“ ({code}), proto se ve všech prohlížečích PDF zobrazí jako ?. Text se uloží. Tools panel side Strana panelu nástrojů + Add comment Přidat komentář diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 9513ca9a..df3af2b4 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2254,3 +2254,4 @@ scroll speed Fast Schnell Black and white Schwarzweiß This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Der Treiber dieses Druckers übernimmt keine Einstellungen für beidseitigen Druck von anderen Programmen, daher gilt seine eigene Einstellung. Ändern unter Eigenschaften… Tools panel side Seite des Werkzeugbereichs + Add comment Kommentar hinzufügen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index d2c9ba2a..9d624ba7 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2272,3 +2272,4 @@ scroll speed Fast Γρήγορη This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Η γραμματοσειρά αυτού του πεδίου δεν μπορεί να σχεδιάσει το «{c}» ({code}), γι' αυτό το PdfCraft δείχνει ? στη θέση του. Η τιμή αποθηκεύεται και άλλα προγράμματα προβολής PDF επανασχεδιάζουν το πεδίο από αυτήν. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Οι γραμματοσειρές με τις οποίες γράφει το PdfCraft δεν μπορούν να σχεδιάσουν το «{c}» ({code}), γι' αυτό εμφανίζεται ως ? σε κάθε πρόγραμμα προβολής PDF. Το κείμενο αποθηκεύεται. Tools panel side Πλευρά πλαισίου εργαλείων + Add comment Προσθήκη σχολίου diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 360f12ff..4c9d3f30 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -85,6 +85,8 @@ Display theme Tema de visualización Side panels Paneles laterales Enable Acrobat JavaScript Activar Acrobat JavaScript + Flatten Fill & Sign when saving Acoplar Rellenar y firmar al guardar + Text, marks and signatures become part of the page. Other comments stay editable. El texto, las marcas y las firmas pasan a formar parte de la página. Los demás comentarios siguen siendo editables. OK Aceptar Join our Discord Únete a nuestro Discord Close Cerrar @@ -656,7 +658,7 @@ Social Security Number Número de la Seguridad Social Arbitrary Mask Máscara arbitraria Mask: Máscara: - 9 digit, A letter, O letter or digit, X any character 9 dígito, A letra, O letra o dígito, X cualquier carácter + 9 digit, A letter, O letter or digit, X any character 9: dígito; A: letra; O: letra o dígito; X: cualquier carácter Example: {sample} Ejemplo: {sample} Field value is not validated El valor del campo no se valida Field value is in range: El valor del campo está en el intervalo: @@ -785,6 +787,7 @@ Rotation Rotación Appear behind page Mostrar detrás de la página Appear on top of page Mostrar encima de la página + Offset from centre Desplazamiento desde el centro From colour A partir de un color Page Range Options Opciones de intervalo de páginas Preview: page {p} of {n} Vista previa: página {p} de {n} @@ -923,6 +926,7 @@ Remove hidden information, metadata, scripts and embedded content, and rewrite the files. Quita la información oculta, los metadatos, los scripts y el contenido incrustado, y reescribe los archivos. Type text Escribe texto Text to insert Texto que insertar + Inserted text Texto insertado Replacement text Texto de reemplazo Edit text… Editar texto… Add a sticky note here Añadir una nota adhesiva aquí @@ -933,6 +937,7 @@ This comment's appearance can't be changed yet. El aspecto de este comentario todavía no se puede cambiar. Icon Icono Thickness Grosor + Line ending Extremo de línea Modified Fecha de modificación No status has been set. No se ha establecido ningún estado. Compare Files Comparar archivos @@ -1226,7 +1231,7 @@ 1 mark 1 marca {n} marks {n} marcas Dynamic Dinámicos - Sign Here Firme aquí + Sign Here Firma aquí Standard Business Empresariales estándar Choose a stamp, then click on the page to place it. Elige un sello y haz clic en la página para colocarlo. Note Nota @@ -1584,6 +1589,7 @@ Looking for printers… Buscando impresoras… Sending to {printer}… Enviando a {printer}… Choose a folder for the searchable files Elige una carpeta para los archivos con búsqueda + Files are combined in this order. Leave Pages empty to take every page. Los archivos se combinan en este orden. Deja Páginas vacío para incluir todas las páginas. Files are combined from top to bottom. Leave Pages empty to take every page. Los archivos se combinan de arriba abajo. Deja Páginas vacío para incluir todas las páginas. Unlock… Desbloquear… Select a password-protected file first Selecciona primero un archivo protegido con contraseña @@ -1610,6 +1616,11 @@ Add folder and subfolders Añadir carpeta y subcarpetas No PDFs, images or text files found in {folder} No se encontraron PDF, imágenes ni archivos de texto en {folder} Added the first {n} files in {folder} Se añadieron los primeros {n} archivos de {folder} +comment menu Edit Editar + Undo {action} Deshacer {action} + Redo {action} Rehacer {action} +certificate Subject Titular +action wizard Start Iniciar Click to sort, drag to move the column. Drag its edge to resize it. Haz clic para ordenar; arrastra para mover la columna. Arrastra su borde para cambiar el ancho. Reset columns Restablecer columnas Remove {n} files Quitar {n} archivos @@ -2031,7 +2042,73 @@ Default page display Presentación de página predeterminada Default zoom Zoom predeterminado Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Se usa cuando un PDF no pide una presentación ni un zoom. El desplazamiento continuo nunca salta de página en página. + Show Mostrar + Sort Ordenar + Reverse the order Invertir el orden + Grab bag Miscelánea + Table Tabla + Username Nombre de usuario + Display name Nombre para mostrar + Real name Nombre real + Name (A–Z) Nombre (A–Z) + Merged PRs PR fusionados + PRs PR + Commits Commits + Lines added Líneas añadidas + +LOC +LOC + Lines deleted Líneas eliminadas + −LOC −LOC + Line delta Diferencia de líneas + ΔLOC ΔLOC + Binary assets added Recursos binarios añadidos + +Bin +Bin + Binary assets removed Recursos binarios eliminados + −Bin −Bin + First commit Primer commit + First Primero + Last commit Último commit + Last Último + No contributor data was built into this copy. Esta copia no incluye datos de colaboradores. + No model credits were built into this copy. Esta copia no incluye créditos de modelos. + {n} contributors · {c} commits {n} colaboradores · {c} commits + Company Empresa + Model Modelo + % of all commits % de todos los commits + Lines +/− Líneas +/− + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} commits, +{added} / −{deleted} líneas (Δ {delta}), +{bin_added} / −{bin_deleted} recursos binarios, {first} – {last} Reopen the files that were open when PdfCraft last closed Volver a abrir los archivos que estaban abiertos al cerrar PdfCraft por última vez + Add {what} Añadir {what} + Use {what} Usar {what} + Place saved {what} Colocar {what} desde lo guardado + Remove saved {what} Quitar {what} de lo guardado + A link Un enlace + A button Un botón + A script Un script + {who} in this document tried to open an address PdfCraft won't open: {e}. {who} de este documento intentó abrir una dirección que PdfCraft no abre: {e}. + {who} in this document tried to open an address PdfCraft won't open: {e}. Only web (http, https) and email (mailto) links open from documents. {who} de este documento intentó abrir una dirección que PdfCraft no abre: {e}. Desde los documentos solo se abren enlaces web (http, https) y de correo (mailto). + Another file dialog is still open. Finish with it first. Todavía hay otro cuadro de archivos abierto. Termina primero con ese. + Couldn't show the file picker. Please try again. No se pudo mostrar el selector de archivos. Inténtalo de nuevo. + The document changed while you were choosing a file, so nothing was changed. El documento cambió mientras elegías un archivo, así que no se modificó nada. + The document was closed before it could be saved. El documento se cerró antes de poder guardarlo. + The pages were extracted but not deleted, because the document changed while you were choosing a folder. Las páginas se extrajeron pero no se eliminaron, porque el documento cambió mientras elegías una carpeta. + That didn't work: an internal error stopped it ({m}). No ha funcionado: un error interno lo ha detenido ({m}). + The Windows store's digital IDs couldn't be listed: {e} No se pudieron listar los ID digitales del almacén de Windows: {e} + Align fields Alinear campos + Distribute fields Distribuir campos + Match field sizes Igualar el tamaño de los campos + Mark for redaction Marcar para censurar + Mark pages for redaction Marcar páginas para censurar + Change document properties Cambiar propiedades del documento + Mark differences Marcar diferencias + Run form script Ejecutar script del formulario + Scripted content must be usable with a keyboard and assistive technology. Check the document's scripts. El contenido con scripts debe poder usarse con teclado y con tecnologías de asistencia. Revisa los scripts del documento. + Table rows belong directly in a table or its head, body or foot. Fix the table's tags. Las filas de una tabla van directamente en la tabla o en su encabezado, cuerpo o pie. Corrige las etiquetas de la tabla. + Basic constraints Restricciones básicas + SHA-1 digest Huella SHA-1 + SHA-256 digest Huella SHA-256 + Squiggly Ondulado + Home / End Inicio / Fin + H / Space (hold) H / Espacio (mantener) Reload documents when their files change on disk Recargar los documentos cuando sus archivos cambien en el disco This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Este archivo ha cambiado en el disco. Recárgalo para ver la nueva versión; se perderán tus cambios sin guardar. Reload Recargar @@ -2049,6 +2126,11 @@ Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Elige una imagen PNG o JPEG (hasta 4 MiB). Se conserva la transparencia. Couldn't import the signature image: {e} No se pudo importar la imagen de la firma: {e} Go to page… Ir a la página… + About Acerca de + Contributors Colaboradores + Models Modelos + Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Continúa solo si confías en este documento. Una dirección puede llevar información del documento, como lo que escribiste en su formulario. + Print single-sided. Keep the sheets in order, cut at the marks, then stack the piles left to right, top to bottom. Imprime a una cara. Mantén las hojas en orden, corta por las marcas y apila los montones de izquierda a derecha y de arriba abajo. For page sizes and margins. Para tamaños de página y márgenes. The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Las fuentes de texto PDF no pueden mostrar «{c}» ({code}). Elija texto compatible. The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Las fuentes estándar no pueden mostrar «{c}» ({code}). Por ahora solo se pueden añadir como texto caracteres de Europa occidental. @@ -2237,10 +2319,8 @@ Change Author Cambiar el autor Change Subject Cambiar el asunto Change Keywords Cambiar las palabras clave - Run form script Ejecutar el script del formulario Save as {level} Guardar como {level} Create links Crear enlaces - Mark differences Marcar diferencias # Multi-object selection and movement (#844) @plural {n} object selected|{n} objects selected {n} objeto seleccionado|{n} objetos seleccionados @@ -2288,19 +2368,22 @@ key Space Espacio Tools Herramientas Inspector Inspector Select page {label} Seleccionar la página {label} - Rotate page {label} counterclockwise Girar la página {label} en sentido antihorario - Rotate page {label} clockwise Girar la página {label} en sentido horario + Rotate page {label} counterclockwise Girar la página {label} a la izquierda + Rotate page {label} clockwise Girar la página {label} a la derecha Delete page {label} Eliminar la página {label} Mouse wheel scroll speed Velocidad de desplazamiento de la rueda del ratón scroll speed Slow Lenta scroll speed Normal Normal scroll speed Fast Rápida Enhance camera image Mejorar foto de cámara - Drag the corners onto the document's edges, then choose Enhance page. Arrastre las esquinas hasta los bordes del documento y elija Mejorar página. + Drag the corners onto the document's edges, then choose Enhance page. Arrastra las esquinas hasta los bordes del documento y elige Mejorar página. Enhance page Mejorar página + Certificate authority Autoridad de certificación + End entity Entidad final Clean-up Limpieza Original Original Greyscale Escala de grises Black and white Blanco y negro This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… El controlador de esta impresora no acepta los ajustes de doble cara de otros programas, así que se aplica su propio ajuste. Cámbielo en Propiedades… Tools panel side Lado del panel de herramientas + Add comment Añadir comentario diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index f703cc2c..a46de69b 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2253,3 +2253,4 @@ scroll speed Fast Rapide Black and white Noir et blanc This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Le pilote de cette imprimante n'accepte pas les réglages recto verso des autres programmes ; son propre réglage s'applique. Modifiez-le dans Propriétés… Tools panel side Côté du panneau Outils + Add comment Ajouter un commentaire diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 7b0080f0..e6199cc0 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2252,3 +2252,4 @@ scroll speed Fast Gyors Black and white Fekete-fehér This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… A nyomtató illesztőprogramja nem fogadja el más programoktól a kétoldalas nyomtatás beállítását, ezért a saját beállítása érvényes. Módosítás: Tulajdonságok… Tools panel side Az Eszközök panel oldala + Add comment Megjegyzés hozzáadása diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 643b55b7..eaf35bfd 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2262,3 +2262,4 @@ scroll speed Fast Veloce Black and white Bianco e nero This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Il driver di questa stampante non accetta le impostazioni fronte-retro da altri programmi, quindi vale la sua impostazione. Modificala in Proprietà… Tools panel side Lato del pannello Strumenti + Add comment Aggiungi commento diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 55f8a961..69d9efc4 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2253,3 +2253,4 @@ scroll speed Fast 速い Black and white 白黒 This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… このプリンターのドライバーは他のプログラムからの両面印刷の設定を受け付けないため、ドライバー自身の設定が使われます。変更先: プロパティ… Tools panel side ツールパネルの位置 + Add comment コメントを追加 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index ed4ecb04..84d67da5 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2254,3 +2254,4 @@ scroll speed Fast Жылдам This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Бұл өрістің қаріпі «{c}» ({code}) таңбасын сала алмайды, сондықтан PdfCraft оның орнына ? көрсетеді. Мән сақталады, ал басқа PDF қарау құралдары өрісті соған қарап қайта салады. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft жазатын қаріптер «{c}» ({code}) таңбасын сала алмайды, сондықтан ол барлық PDF қарау құралдарында ? болып көрінеді. Мәтін сақталады. Tools panel side Құралдар панелінің жағы + Add comment Түсініктеме қосу diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 83ce917a..a4e84375 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2236,3 +2236,4 @@ scroll speed Fast Szybka Black and white Czarno-biały This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Sterownik tej drukarki nie przyjmuje ustawień druku dwustronnego z innych programów, więc obowiązuje jego własne ustawienie. Zmień je w oknie Właściwości… Tools panel side Strona panelu narzędzi + Add comment Dodaj komentarz diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f0248695..5d0181e3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2267,3 +2267,4 @@ scroll speed Fast Rápida Black and white Preto e branco This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… O driver desta impressora não aceita configurações de frente e verso de outros programas, então vale a configuração dele. Altere em Propriedades… Tools panel side Lado do painel Ferramentas + Add comment Adicionar comentário diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c1ecd621..74ab50f7 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2166,3 +2166,4 @@ scroll speed Fast Высокая Black and white Чёрно-белый This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйвер этого принтера не принимает настройки двусторонней печати от других программ, поэтому действует его собственная настройка. Измените её в окне Свойства… Tools panel side Сторона панели инструментов + Add comment Добавить комментарий diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index 13831665..e7139b4d 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2261,3 +2261,4 @@ scroll speed Fast Hitro This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Pisava tega polja ne more prikazati »{c}« ({code}), zato PdfCraft namesto njega prikaže ?. Vrednost se shrani, drugi pregledovalniki PDF pa polje znova narišejo iz nje. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Pisave, s katerimi piše PdfCraft, ne morejo prikazati »{c}« ({code}), zato se v vseh pregledovalnikih PDF prikaže kot ?. Besedilo se shrani. Tools panel side Stran podokna z orodji + Add comment Dodaj komentar diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 0c427920..2ea346a8 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2169,3 +2169,4 @@ scroll speed Fast వేగంగా Greyscale గ్రేస్కేల్ Black and white నలుపు తెలుపు Tools panel side టూల్స్ ప్యానెల్ వైపు + Add comment వ్యాఖ్యను జోడించు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 85f7e661..fccb7171 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2252,3 +2252,4 @@ scroll speed Fast Hızlı This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Bu alanın yazı tipi “{c}” ({code}) karakterini çizemiyor, bu yüzden PdfCraft yerine ? gösteriyor. Değer kaydedilir ve diğer PDF görüntüleyiciler alanı ondan yeniden çizer. The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft'ın yazdığı yazı tipleri “{c}” ({code}) karakterini çizemiyor, bu yüzden her PDF görüntüleyicide ? olarak görünür. Metin kaydedilir. Tools panel side Araçlar panelinin yeri + Add comment Yorum ekle diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 719b9304..d13b557e 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2275,3 +2275,4 @@ scroll speed Fast Швидка Black and white Чорно-білий This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… Драйвер цього принтера не приймає налаштування двобічного друку від інших програм, тому діє його власне налаштування. Змініть його у вікні Властивості… Tools panel side Бік панелі інструментів + Add comment Додати коментар diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2c706c98..effe1c73 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2217,3 +2217,4 @@ scroll speed Fast 快 Black and white 黑白 This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… 此打印机的驱动程序不接受其他程序的双面打印设置,因此使用其自身设置。更改位置:属性… Tools panel side 工具面板位置 + Add comment 添加注释 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 62d8f945..e13725cf 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2130,3 +2130,4 @@ scroll speed Fast 快 Black and white 黑白 This printer's driver doesn't take two-sided settings from other programs, so its own setting applies. Change it in Properties… 此印表機的驅動程式不接受其他程式的雙面列印設定,因此使用其自身設定。變更位置:屬性… Tools panel side 工具面板位置 + Add comment 新增註解 diff --git a/crates/ui-egui/tests/comments.rs b/crates/ui-egui/tests/comments.rs index 1bf2dd3c..d3026447 100644 --- a/crates/ui-egui/tests/comments.rs +++ b/crates/ui-egui/tests/comments.rs @@ -151,6 +151,44 @@ fn a_selection_made_first_is_marked_when_a_tool_is_picked() { assert!(h.state().views[0].selected_text().is_none(), "the selection is consumed"); } +#[test] +fn selected_text_shows_actions_and_underlines_without_changing_tools() { + let mut h = harness(|_| {}); + h.state_mut().views[0].select_text(0, 4, 8); + h.run_steps(3); + h.get_by_label("Add comment"); + h.get_by_label("Replace text"); + h.get_by_label("Underline").click(); + h.run_steps(3); + let c = comments(&h); + assert_eq!(c.len(), 1); + assert_eq!(c[0].subtype, "Underline"); + assert!(h.state().views[0].selected_text().is_none()); + assert_eq!(h.state().quick_tool, QuickTool::Select); +} + +#[test] +fn selected_text_replace_action_opens_replacement_composer() { + let mut h = harness(|_| {}); + h.state_mut().views[0].select_text(0, 4, 8); + h.run_steps(3); + h.get_by_label("Replace text").click(); + h.run_steps(2); + h.get_by_label("Replacement text"); + assert!(h.state().views[0].selected_text().is_none()); +} + +#[test] +fn selected_text_comment_action_opens_note_composer() { + let mut h = harness(|_| {}); + h.state_mut().views[0].select_text(0, 4, 8); + h.run_steps(3); + h.get_by_label("Add comment").click(); + h.run_steps(2); + assert!(matches!(h.state().views[0].comments.composer.as_ref().map(|c| &c.kind), Some(pdfcraft_ui_egui::comments::ComposerKind::Note))); + assert!(h.state().views[0].selected_text().is_none()); +} + #[test] fn shapes_and_ink_are_drawn_by_dragging() { let mut h = harness(|app| app.set_option("quick", "square").unwrap()); diff --git a/docs/roadmap.md b/docs/roadmap.md index 2477ea75..14d55ce8 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,6 @@ # PdfCraft roadmap: milestones and what's next +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M2: short same-line redraws split before placement; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (M10, #1028: OCR reads pictures on pages that have text, on request; earlier: direct Organize overview and page actions; localized shortcut labels, #496; alpha gate added; convert-and-share re-judged with the cross-app rule → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (M13/#971: bound per-page search match allocation; text cache byte budget and extraction warnings remain open) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (#594 paragraph drags validated in the Linux test environment; localized shortcut labels, #496; alpha gate added; convert-and-share re-judged with the cross-app rule → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -75,7 +76,7 @@ total matches the parity estimate. |---|---|---|---|---|---| | M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 98% | 1–2 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates | | M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 80% | 20–40 | Done:
- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;
- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.
Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.
Full saves now pack objects into compressed object streams. Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs | -| 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. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings | +| 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). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
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). | | M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: Organize opens directly into the page overview, with checkbox selection and per-page rotate/delete controls; Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Missing: page transitions, recovery on the web | | 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
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). Renderer-creation errors return to the existing OpenGL startup fallback; a real-GPU regression covers errors and device reuse. The underlying x86 DX12 shader compiler failure remains unresolved; see `windows-renderer-startup.md`. | @@ -139,6 +140,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | M2: short same-line redraws split before placement; estimates unchanged. | | 2026-10-11 | minor | M13/#971: cap materialized text-search matches to the requested limit plus one continuation match. | | 2026-10-11 | minor | M7: #594 ordinary paragraph drags validated in the Linux test environment with source-loss baseline failures, UI/engine regressions, native save/reopen and all eight CI gates. Estimates unchanged. | | 2026-10-11 | minor | M4: direct Organize overview, checkbox selection and individual page actions. Estimates unchanged. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 6b7ab582..657ebb3c 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1490,8 +1490,8 @@ milestone = "M3" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/ui.rs::drag_selects_text_and_copy_returns_it", "crates/ui-egui/tests/ui.rs::selection_works_in_every_view_rotation", "crates/ui-egui/tests/ui_group/text_select.rs::quick_clicks_select_a_word_then_the_line_then_the_page", "crates/ui-egui/tests/ui_group/text_select.rs::quick_clicks_with_the_highlighter_mark_only_the_word", "crates/ui-egui/tests/ui_group/text_select.rs::shift_click_extends_the_selection_from_its_anchor"] -notes = "Drag to select; ⇧-click extends the selection to the click, keeping its anchor (same page only); with the Select tool, quick clicks select a word (two), the line (three) or all the text on the page (four). The markup and Redact tools mark a double-clicked word and stop there. Missing: selection across pages (dragging or ⇧-clicking onto another page), paragraph selection, and ⌘A across the whole document (it selects the current page)." +evidence = ["crates/ui-egui/tests/ui.rs::drag_selects_text_and_copy_returns_it", "crates/ui-egui/tests/ui.rs::selection_works_in_every_view_rotation", "crates/ui-egui/tests/ui_group/text_select.rs::quick_clicks_select_a_word_then_the_line_then_the_page", "crates/ui-egui/tests/ui_group/text_select.rs::quick_clicks_with_the_highlighter_mark_only_the_word", "crates/ui-egui/tests/ui_group/text_select.rs::shift_click_extends_the_selection_from_its_anchor", "crates/ui-egui/tests/comments.rs::selected_text_shows_actions_and_underlines_without_changing_tools", "crates/ui-egui/tests/comments.rs::selected_text_replace_action_opens_replacement_composer", "crates/ui-egui/tests/comments.rs::selected_text_comment_action_opens_note_composer"] +notes = "Drag to select; ⇧-click extends the selection to the click, keeping its anchor (same page only); with the Select tool, quick clicks select a word (two), the line (three) or all the text on the page (four). A floating action bar offers comment, highlight, underline, strikethrough, replace text and copy after selection. The markup and Redact tools mark a double-clicked word and stop there. Missing: selection across pages (dragging or ⇧-clicking onto another page), paragraph selection, and ⌘A across the whole document (it selects the current page). Acrobat's other selection context actions such as bookmark, link and export selection remain outside this bar." [[feature]] id = "view.column-select" diff --git a/vendor/README.md b/vendor/README.md index e2c3aae8..65c14d60 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -18,7 +18,7 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001). | hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW and RunLength never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB); (8) high-precision numbers (jsPDF MediaBox coordinates with over 19 digits) parse without overflowing the u64 mantissa: fractional digits past it are dropped, integer digits past it scale the value (a real, kept finite); (9) large dictionary indexes are built on first use and shared across repeated reads within each PDF (#307); the validated-byte cache is bounded, reader contexts remain separate, repairs and inline/content dictionaries bypass sharing, and small dictionaries stay eager; (10) decoded object-stream slots publish once under contention, cold loading rejects recursive dependencies, and xref repair serializes without holding recursive read guards; this permits one parser per native render pool with private worker caches; (11) `sync.rs`: a poisoned lock is recovered instead of unwrapped (a worker panic no longer cascades into every reader of the same PDF), and xref repair takes a blocking write lock instead of asserting a non-blocking one; (12) `SegmentList` (`util.rs`) returns `None` for an out-of-range index instead of panicking; (13) a page's `/UserUnit` scales `render_dimensions` and `initial_transform`, so the page and everything on it are sized in points, as Acrobat shows them; it is read as Acrobat reads it (the page's own entry, not inherited; a number from 1 up, at most 75,000; anything else counts as 1), and a page without it keeps exactly the transform it had | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths`; (7) `raster::tests::decompression_bombs_stop_at_the_stream_limit`, `raster::tests::page_contents_and_nested_forms_share_one_budget`; (8) `inspect::tests::jspdf_high_precision_mediabox_does_not_overflow_dimensions`, `object::number::tests::high_precision_float_does_not_overflow_mantissa` (hayro-syntax); (9) `object::dict::lazy_index_tests`, `raster::tests::large_resource_indexes_preserve_pixels_and_text`; (10) `data::tests`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`, `raster::tests::shared_parser_is_lazy_and_initializes_once_under_contention`; (11) `data::tests::panicking_initializer_wakes_waiters_and_prevents_retries`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`; (12) `util::tests::segment_list_rejects_out_of_range_indices`; (13) `raster::tests::user_unit_scales_the_page_and_everything_on_it` | | lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding` | | hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW and RunLength never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB); (8) high-precision numbers (jsPDF MediaBox coordinates with over 19 digits) parse without overflowing the u64 mantissa: fractional digits past it are dropped, integer digits past it scale the value (a real, kept finite); (9) large dictionary indexes are built on first use and shared across repeated reads within each PDF (#307); the validated-byte cache is bounded, reader contexts remain separate, repairs and inline/content dictionaries bypass sharing, and small dictionaries stay eager; (10) decoded object-stream slots publish once under contention, cold loading rejects recursive dependencies, and xref repair serializes without holding recursive read guards; this permits one parser per native render pool with private worker caches; (11) `sync.rs`: a poisoned lock is recovered instead of unwrapped (a worker panic no longer cascades into every reader of the same PDF), and xref repair takes a blocking write lock instead of asserting a non-blocking one; (12) `SegmentList` (`util.rs`) returns `None` for an out-of-range index instead of panicking; (13) a page's `/UserUnit` scales `render_dimensions` and `initial_transform`, so the page and everything on it are sized in points, as Acrobat shows them; it is read as Acrobat reads it (the page's own entry, not inherited; a number from 1 up, at most 75,000; anything else counts as 1), and a page without it keeps exactly the transform it had; (14) direct arrays and dictionaries share a reader-local nesting cap of 128, counted while skipping and while parsing a dictionary (its lenient repair reads, rather than skips, an object found where a key belongs, so `<< << …` recursed through that read); deeper objects return `None` instead of overflowing the stack (including unused trailer values, trailer entries, content operands and marked-content properties); a chain of 65 direct stitching functions now stops at this cap, before the 64-function guard | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths`; (7) `raster::tests::decompression_bombs_stop_at_the_stream_limit`, `raster::tests::page_contents_and_nested_forms_share_one_budget`; (8) `inspect::tests::jspdf_high_precision_mediabox_does_not_overflow_dimensions`, `object::number::tests::high_precision_float_does_not_overflow_mantissa` (hayro-syntax); (9) `object::dict::lazy_index_tests`, `raster::tests::large_resource_indexes_preserve_pixels_and_text`; (10) `data::tests`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`, `raster::tests::shared_parser_is_lazy_and_initializes_once_under_contention`; (11) `data::tests::panicking_initializer_wakes_waiters_and_prevents_retries`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`; (12) `util::tests::segment_list_rejects_out_of_range_indices`; (13) `raster::tests::user_unit_scales_the_page_and_everything_on_it`; (14) `raster::tests::deeply_nested_direct_objects_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_in_content_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_in_the_trailer_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_as_marked_content_properties_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_are_read_up_to_the_nesting_cap`, `raster::tests::nesting_at_the_cap_fits_a_512_kib_stack` (native only), `raster::tests::function_limits_depth_guard_refuses_a_chain_of_references` | -| winit | 0.30.13 | Apache-2.0 | (1) on Windows 11 (build 22000 and later), `WM_DPICHANGED` applies the rect Windows suggests. It used to nudge the window onto the monitor `MonitorFromWindow` names, which during a drag is still the monitor being left; the nudge sent another `WM_DPICHANGED`, so a window dragged to a monitor with another scale factor grew and stayed on the wrong monitor at the wrong scale (#324). Backport of winit master 488c036a ([winit#4341](https://github.com/rust-windowing/winit/pull/4341)), which no 0.30 release has; Windows 10 keeps the old adjustment, as on winit master. (2) on Wayland, a seat's text input is destroyed when the seat goes away, not whenever it loses a capability: losing any capability (a touch screen or tablet unplugged, a remote-desktop session ending) destroyed it, and only an added capability recreated it, so IME stopped working until restart. Port of [winit#4747](https://github.com/rust-windowing/winit/pull/4747); (3) on Windows, an IME composition reports its cursor from `GCS_CURSORPOS` only when `WM_IME_COMPOSITION` says it is set, and at the end of the composition otherwise: the Microsoft Korean IME never sets it, so the cursor read as 0 (egui then drew no caret while composing; it now draws it after the syllable). Port of [winit#4746](https://github.com/rust-windowing/winit/pull/4746); (4) on Windows, a hidden window is not maximized until it is shown, and a hidden window shown while maximized gets one `ShowWindow(SW_MAXIMIZE)`: `SW_MAXIMIZE` shows a hidden window, so PdfCraft, which eframe keeps hidden until its first frame is drawn, appeared unpainted (white) when it restored a maximized window, and could lose its focus. Port of [winit#4587](https://github.com/rust-windowing/winit/pull/4587) (the later, focus-safe form of [winit#4575](https://github.com/rust-windowing/winit/pull/4575)); (5) on X11, when allowing or disallowing IME replaces the focused window's input context (egui does it each time a text field gains or loses focus), the new context is focused: no focus event follows, so it stayed unfocused until the window lost and regained focus. Port of [winit#4727](https://github.com/rust-windowing/winit/pull/4727); (6) on macOS, `insertText` commits through IME when a composition was started (`pending_commit`, set by `setMarkedText`) rather than when marked text is left: input methods that end a composition with `unmarkText` before `insertText` lost the text, and Apple Korean dropped the key typed after a syllable (`한` then `5` or `;`); other `insertText` calls stay `KeyboardInput`; a commit outside a key press (a candidate clicked with the mouse, dictation) clears the marked text and leaves no key to swallow, as in [winit#4748](https://github.com/rust-windowing/winit/pull/4748) (without it the next key, such as Backspace, was lost). `validAttributesForMarkedText` lists the underline and marked-clause attributes, without which some input methods don't compose. Port of [winit#4650](https://github.com/rust-windowing/winit/pull/4650), the 0.30 backport of [winit#4651](https://github.com/rust-windowing/winit/pull/4651) (merged on winit master), with the underline attribute spelled as AppKit's `NSUnderlineStyleAttributeName` (`NSUnderline`); (7) on macOS, disallowing IME with a composition in progress (focus leaving a text field) tells the input method to discard its marked text (`discardMarkedText`): it kept composing it once IME was allowed again, so the abandoned syllable came back (`하`, then `가` typed in the next field, became `학`, then `하` + `가`; Japanese `か` then `さ` became `傘`). Port of [winit#4745](https://github.com/rust-windowing/winit/pull/4745); (8) on macOS, text inserted outside a key press without a composition (the emoji picker) is reported as `Ime::Commit`; it was dropped. Port of the emoji-picker part of [winit#4749](https://github.com/rust-windowing/winit/pull/4749) (0.30 backport of [winit#4748](https://github.com/rust-windowing/winit/pull/4748)); its `selectedRange` change for dictation is left out because it switches on press-and-hold for every held key (an accent menu instead of key repeat, and the chosen accent added after the repeated letters); (9) on macOS, a key whose text was committed through IME isn't also sent as `KeyboardInput` unless AppKit forwards it as a command (Enter): Apple Korean answers Space with no syllable in progress with `setMarkedText(" ")`, `insertText(" ")`, `setMarkedText("")`, and the last call reset winit's state, so the space arrived twice. Same goal as [winit#4478](https://github.com/rust-windowing/winit/pull/4478), whose changes don't reach this call order (macOS 14); to be proposed upstream there; (10) on macOS, when the first key in a fresh app reaches Apple Korean's `insertText` as a raw compatibility jamo instead of starting a composition, that key is interpreted once more, synchronously, before it is sent as `KeyboardInput` (g k s typed ㅎㅏㄴ instead of 한); only an unmodified, non-repeated first key whose one callback inserts that same jamo, with IME enabled and nothing composed, focus and input source unchanged, is retried (`ime_startup.rs`). Port of [winit#4744](https://github.com/rust-windowing/winit/pull/4744), with its policy tests. The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build | (1) `pdfcraft` `tests::winit_carries_the_windows_11_monitor_scale_fix` keeps the patch in the build; the drag itself needs two monitors with different scale factors on Windows 11; (2) `pdfcraft` `tests/winit_wayland_text_input.rs`; the capability change itself needs a Wayland session and an input method; (3) `pdfcraft` `tests/winit_windows_ime_cursor.rs`; composing itself needs the Microsoft Korean IME; (4) `pdfcraft` `tests/winit_windows_hidden_maximized.rs`; the start-up itself needs Windows and a window that was closed maximized; (5) `pdfcraft` `tests/winit_x11_ime_focus.rs`; switching fields itself needs an X11 session and an input method (IBus, fcitx5); (6) `pdfcraft` `tests/winit_macos_ime_commit.rs`; composing itself needs macOS and an input method; (7) `pdfcraft` `tests/winit_macos_ime_discard.rs`; composing itself needs macOS and an input method; (8) `pdfcraft` `tests/winit_macos_emoji_picker.rs`; the picker itself needs macOS; (9) `pdfcraft` `tests/winit_macos_korean_space.rs`; composing itself needs macOS and Apple Korean; (10) `pdfcraft` `tests/winit_macos_korean_first_key.rs` and `ime_startup.rs`'s own tests (run them with `rustc --test`); typing itself needs macOS and Apple Korean | +| winit | 0.30.13 | Apache-2.0 | (1) on Windows 11 (build 22000 and later), `WM_DPICHANGED` applies the rect Windows suggests. It used to nudge the window onto the monitor `MonitorFromWindow` names, which during a drag is still the monitor being left; the nudge sent another `WM_DPICHANGED`, so a window dragged to a monitor with another scale factor grew and stayed on the wrong monitor at the wrong scale (#324). Backport of winit master 488c036a ([winit#4341](https://github.com/rust-windowing/winit/pull/4341)), which no 0.30 release has; Windows 10 keeps the old adjustment, as on winit master. (2) on Wayland, a seat's text input is destroyed when the seat goes away, not whenever it loses a capability: losing any capability (a touch screen or tablet unplugged, a remote-desktop session ending) destroyed it, and only an added capability recreated it, so IME stopped working until restart. Port of [winit#4747](https://github.com/rust-windowing/winit/pull/4747); (3) on Windows, an IME composition reports its cursor from `GCS_CURSORPOS` only when `WM_IME_COMPOSITION` says it is set, and at the end of the composition otherwise: the Microsoft Korean IME never sets it, so the cursor read as 0 (egui then drew no caret while composing; it now draws it after the syllable). Port of [winit#4746](https://github.com/rust-windowing/winit/pull/4746); (4) on Windows, a hidden window is not maximized until it is shown, and a hidden window shown while maximized gets one `ShowWindow(SW_MAXIMIZE)`: `SW_MAXIMIZE` shows a hidden window, so PdfCraft, which eframe keeps hidden until its first frame is drawn, appeared unpainted (white) when it restored a maximized window, and could lose its focus. Port of [winit#4587](https://github.com/rust-windowing/winit/pull/4587) (the later, focus-safe form of [winit#4575](https://github.com/rust-windowing/winit/pull/4575)); (5) on X11, when allowing or disallowing IME replaces the focused window's input context (egui does it each time a text field gains or loses focus), the new context is focused: no focus event follows, so it stayed unfocused until the window lost and regained focus. Port of [winit#4727](https://github.com/rust-windowing/winit/pull/4727); (6) on macOS, `insertText` commits through IME when a composition was started (`pending_commit`, set by `setMarkedText`) rather than when marked text is left: input methods that end a composition with `unmarkText` before `insertText` lost the text, and Apple Korean dropped the key typed after a syllable (`한` then `5` or `;`); other `insertText` calls stay `KeyboardInput`; a commit outside a key press (a candidate clicked with the mouse, dictation) clears the marked text and leaves no key to swallow, as in [winit#4748](https://github.com/rust-windowing/winit/pull/4748) (without it the next key, such as Backspace, was lost). `validAttributesForMarkedText` lists the underline and marked-clause attributes, without which some input methods don't compose. Port of [winit#4650](https://github.com/rust-windowing/winit/pull/4650), the 0.30 backport of [winit#4651](https://github.com/rust-windowing/winit/pull/4651) (merged on winit master), with the underline attribute spelled as AppKit's `NSUnderlineStyleAttributeName` (`NSUnderline`); (7) on macOS, disallowing IME with a composition in progress (focus leaving a text field) tells the input method to discard its marked text (`discardMarkedText`): it kept composing it once IME was allowed again, so the abandoned syllable came back (`하`, then `가` typed in the next field, became `학`, then `하` + `가`; Japanese `か` then `さ` became `傘`). Port of [winit#4745](https://github.com/rust-windowing/winit/pull/4745); (8) on macOS, text inserted outside a key press without a composition (the emoji picker) is reported as `Ime::Commit`; it was dropped. Port of the emoji-picker part of [winit#4749](https://github.com/rust-windowing/winit/pull/4749) (0.30 backport of [winit#4748](https://github.com/rust-windowing/winit/pull/4748)); its `selectedRange` change for dictation is left out because it switches on press-and-hold for every held key (an accent menu instead of key repeat, and the chosen accent added after the repeated letters); (9) on macOS, a key whose text was committed through IME isn't also sent as `KeyboardInput` unless AppKit forwards it as a command (Enter): Apple Korean answers Space with no syllable in progress with `setMarkedText(" ")`, `insertText(" ")`, `setMarkedText("")`, and the last call reset winit's state, so the space arrived twice. Same goal as [winit#4478](https://github.com/rust-windowing/winit/pull/4478), whose changes don't reach this call order (macOS 14); to be proposed upstream there; (10) on macOS, when the first key in a fresh app reaches Apple Korean's `insertText` as a raw compatibility jamo instead of starting a composition, that key is interpreted once more, synchronously, before it is sent as `KeyboardInput` (g k s typed ㅎㅏㄴ instead of 한); only an unmodified, non-repeated first key whose one callback inserts that same jamo, with IME enabled and nothing composed, focus and input source unchanged, is retried (`ime_startup.rs`). Port of [winit#4744](https://github.com/rust-windowing/winit/pull/4744), with its policy tests.; (11) on X11 without `Xft.dpi`, the scale factor comes from the X screen's configured DPI (its size in pixels and millimetres) before the output's physical density from RandR: Xorg can deliberately run a dense panel at 96 DPI, and only winit applications then looked zoomed; `Xft.dpi` still wins, `WINIT_X11_SCALE_FACTOR` still overrides, and a screen reporting no size keeps the RandR fallback. PdfCraft patch (#1150, replacing the launcher pin proposed in #908). The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build | (1) `pdfcraft` `tests::winit_carries_the_windows_11_monitor_scale_fix` keeps the patch in the build; the drag itself needs two monitors with different scale factors on Windows 11; (2) `pdfcraft` `tests/winit_wayland_text_input.rs`; the capability change itself needs a Wayland session and an input method; (3) `pdfcraft` `tests/winit_windows_ime_cursor.rs`; composing itself needs the Microsoft Korean IME; (4) `pdfcraft` `tests/winit_windows_hidden_maximized.rs`; the start-up itself needs Windows and a window that was closed maximized; (5) `pdfcraft` `tests/winit_x11_ime_focus.rs`; switching fields itself needs an X11 session and an input method (IBus, fcitx5); (6) `pdfcraft` `tests/winit_macos_ime_commit.rs`; composing itself needs macOS and an input method; (7) `pdfcraft` `tests/winit_macos_ime_discard.rs`; composing itself needs macOS and an input method; (8) `pdfcraft` `tests/winit_macos_emoji_picker.rs`; the picker itself needs macOS; (9) `pdfcraft` `tests/winit_macos_korean_space.rs`; composing itself needs macOS and Apple Korean; (10) `pdfcraft` `tests/winit_macos_korean_first_key.rs` and `ime_startup.rs`'s own tests (run them with `rustc --test`); typing itself needs macOS and Apple Korean; (11) `pdfcraft` `tests/winit_x11_dpi.rs` keeps the patch in the build, and `util::randr` tests the policy (run them in the vendored crate); the scale itself needs an X11 session | | egui-wgpu | 0.36.2 | MIT OR Apache-2.0 | (1) `winit::surface_fit`: a window's surface is configured within the device's `max_texture_dimension_2d`, both sides shrunk by one factor that egui then draws at (`pixels_per_point × render_scale`), so the whole window is drawn into the surface (on DX12 it is stretched over the window and lines up with the pointer; wgpu's GL backend copies it unscaled into a corner); the longer side becomes exactly the limit; the depth and MSAA textures follow, and a recreated surface starts again from the window's own size. egui-wgpu configured the window's size as is, and `Surface::configure` panics beyond the limit: a window restored at another monitor's scale (opening an Outlook attachment on a mixed-DPI laptop asked an 8192-pixel device for 8600 × 3423, #577) or stretched across monitors closed PdfCraft, before its window appeared when it happened at start-up ([emilk/egui#8361](https://github.com/emilk/egui/issues/8361)). The published crate leaves out egui's licence files; `LICENSE-APACHE` and `LICENSE-MIT` are from the 0.36.2 tag; (2) `winit::catch_errors`: a new window's surface is configured inside wgpu error scopes (validation, out of memory, internal), and a failure is returned as `WgpuError::ConfigureSurface` (for the root window at start-up eframe returns it as `Error::Wgpu`, which PdfCraft retries with OpenGL); nothing is kept for the window. wgpu's default handler panicked instead, and eframe configures the root surface before the app is created, so a driver that can't configure one closed PdfCraft at launch (a 2015 Intel GPU through wgpu's GL backend timed out waiting for the GPU to go idle, `GpuWaitTimeout`, #519). Unchanged, so still a panic: an error in a later reconfigure (resize, a lost surface), a device lost during the first configure (wgpu reports it only to a callback), and a window shown at zero size (first configured on resize) | (1) `pdfcraft` `tests::a_window_larger_than_the_gpu_limit_gets_a_surface_within_it`, `tests::surface_fit_never_empties_or_overflows_a_surface`, and `tests::egui_wgpu_carries_the_surface_size_fit`, which keeps the patch in the build and in the paths that size a surface; (2) `pdfcraft` `tests::a_gpu_error_while_a_window_is_set_up_is_returned_not_a_panic` (a real device: an error inside `catch_errors` is returned, not a panic) and `tests::egui_wgpu_returns_a_surface_it_cannot_configure_as_an_error`, which keeps it around `add_surface`'s configure | Renderer startup errors: `egui-wgpu::RenderState::create` also wraps `Renderer::new` in diff --git a/vendor/winit/src/platform_impl/linux/x11/util/randr.rs b/vendor/winit/src/platform_impl/linux/x11/util/randr.rs index d10c97e5..b418816a 100644 --- a/vendor/winit/src/platform_impl/linux/x11/util/randr.rs +++ b/vendor/winit/src/platform_impl/linux/x11/util/randr.rs @@ -142,14 +142,14 @@ impl XConnection { dpi_override }, EnvVarDPI::NotSet => { - if let Some(dpi) = self.get_xft_dpi() { - dpi / 96. - } else { - calc_dpi_factor( - (crtc.width.into(), crtc.height.into()), - (output_info.mm_width as _, output_info.mm_height as _), - ) - } + let screen = self.default_root(); + default_dpi_factor( + self.get_xft_dpi(), + (screen.width_in_pixels.into(), screen.height_in_pixels.into()), + (screen.width_in_millimeters.into(), screen.height_in_millimeters.into()), + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), + ) }, }; @@ -184,3 +184,71 @@ impl XConnection { Ok(self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?.mode) } } + +// PdfCraft patch: without Xft DPI, use the X server's configured screen DPI before +// the output's physical density. Xorg can deliberately run a dense panel at 96 DPI; +// using its EDID dimensions instead makes only winit applications appear zoomed. +// Keep configured HiDPI screens and the explicit `randr` override working. +fn default_dpi_factor( + xft_dpi: Option, + screen_pixels: (u32, u32), + screen_mm: (u64, u64), + output_pixels: (u32, u32), + output_mm: (u64, u64), +) -> f64 { + if let Some(dpi) = xft_dpi { + return dpi / 96.; + } + if screen_pixels.0 != 0 && screen_pixels.1 != 0 && screen_mm.0 != 0 && screen_mm.1 != 0 { + calc_dpi_factor(screen_pixels, screen_mm) + } else { + calc_dpi_factor(output_pixels, output_mm) + } +} + +#[cfg(test)] +mod tests { + use super::{calc_dpi_factor, default_dpi_factor}; + + const PIXELS: (u32, u32) = (1920, 1080); + const SCREEN_96: (u64, u64) = (508, 285); + const SCREEN_144: (u64, u64) = (339, 190); + const SCREEN_192: (u64, u64) = (254, 143); + const PANEL: (u64, u64) = (381, 214); + + #[test] + fn server_dpi_wins_over_the_panels_physical_density() { + assert_eq!(default_dpi_factor(None, PIXELS, SCREEN_96, PIXELS, PANEL), 1.0); + assert_eq!(default_dpi_factor(None, PIXELS, SCREEN_144, PIXELS, PANEL), 1.5); + assert_eq!(default_dpi_factor(None, PIXELS, SCREEN_192, PIXELS, PANEL), 2.0); + } + + #[test] + fn xft_dpi_still_has_priority() { + for dpi in [96.0, 120.0, 144.0, 192.0] { + assert_eq!( + default_dpi_factor(Some(dpi), PIXELS, SCREEN_192, PIXELS, PANEL), + dpi / 96.0, + ); + } + } + + #[test] + fn missing_screen_dimensions_keep_the_randr_fallback() { + let physical_scale = calc_dpi_factor(PIXELS, PANEL); + assert!(physical_scale > 1.0); + for screen_mm in [(0, 285), (508, 0), (0, 0)] { + assert_eq!(default_dpi_factor(None, PIXELS, screen_mm, PIXELS, PANEL), physical_scale); + } + for screen_pixels in [(0, 1080), (1920, 0), (0, 0)] { + assert_eq!(default_dpi_factor(None, screen_pixels, SCREEN_96, PIXELS, PANEL), physical_scale); + } + assert_eq!(default_dpi_factor(None, PIXELS, (0, 0), PIXELS, (0, 0)), 1.0); + } + + #[test] + fn explicit_randr_calculation_still_uses_output_dimensions() { + assert_eq!(calc_dpi_factor(PIXELS, PANEL), 4.0 / 3.0); + assert_eq!(calc_dpi_factor(PIXELS, SCREEN_192), 2.0); + } +}