Merge track r1-app (fredapps-r1)

ROADMAP.md, docs/roadmap.md: keep next's status line; keep both progress-log entries
This commit is contained in:
aiongg
2026-10-16 14:12:15 +00:00
11 changed files with 313 additions and 22 deletions
+1
View File
@@ -158,6 +158,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
## Progress log
- **2026-10-16 (M10, #1201 partial):** Lossless image export applies each component's Decode pair. Indexed values round and clip before palette lookup. Color-key masks keep raw samples. The existing 16-bit precision limit remains. JPEG and mask Decode, ICC transforms, and calibrated colors remain outside this fix. Estimates unchanged.
- **2026-10-16 (M3, #2739):** Wrapped revision buttons stay above Binding in Document Properties. A synthetic regression checks control bounds and opens a saved revision at two viewport widths. 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-15 (M8, #2447):** Removing links or comments (Remove Hidden Information, Sanitize) also drops the tagged-PDF structure tree's object references to them, so the full rewrite no longer keeps a removed link's URL or a comment's text. Regression seen to fail without the fix. Estimates unchanged.
+13 -2
View File
@@ -70,6 +70,10 @@ fn main() {
app.host_save = Some(bridge.save_fn());
app.host_dirty = Some(bridge.dirty_fn());
bridge.post_ready();
} else if query_param("host").as_deref() == Some("parent") {
eframe::web_sys::console::warn_1(
&"PdfCraft: ?host=parent needs a framing page on the same origin; saves download instead".into(),
);
}
Ok(Box::new(app))
}),
@@ -130,7 +134,8 @@ async fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
/// - `pdfcraft:dirty` (to the parent): `{ dirty }` whenever unsaved work appears or goes away.
/// - `pdfcraft:failed` (to the parent): `{ error }` when the app couldn't start.
///
/// Only the parent window on the app's own origin is heard or answered.
/// Only the parent window on the app's own origin is heard or answered. With a parent on
/// another origin there is no bridge: the app runs as if unframed and saves download.
#[cfg(target_arch = "wasm32")]
mod host {
use eframe::wasm_bindgen::{JsCast, JsValue, closure::Closure};
@@ -143,7 +148,9 @@ mod host {
}
impl Bridge {
/// `None` when the page isn't framed (there is no parent to talk to).
/// `None` when the page isn't framed (there is no parent to talk to), or when the parent
/// is on another origin: messages only go to this origin, so the browser would drop
/// every save without an error while the document showed as saved (#2167).
pub fn new() -> Option<Self> {
let window = web_sys::window()?;
let parent = window.parent().ok().flatten()?;
@@ -151,6 +158,10 @@ mod host {
return None;
}
let origin = window.location().origin().ok()?;
// Reading another origin's location throws, which is `None` too.
if parent.location().origin().ok()? != origin {
return None;
}
Some(Self { window, parent, origin })
}
+1
View File
@@ -377,6 +377,7 @@ fn app_creator<'a>(
if let Some(json) = cc.storage.and_then(|s| s.get_string("pdfcraft").or_else(|| s.get_string(LEGACY_STORAGE_KEY))) {
app.restore(&json);
}
app.window_state = pdfcraft_ui_egui::window_state::WindowState::started_with(cc.storage);
app.integrated_titlebar = integrated;
app.window_controls = window_controls;
app.system_text_scale = text_scale::system();
+13 -10
View File
@@ -354,17 +354,20 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
let ends = doc.revision_ends();
if ends.len() > 1 {
ui.label(egui::RichText::new(tl!("Revisions")).color(t.text_muted));
ui.horizontal_wrapped(|ui| {
ui.label(ends.len().to_string());
for n in (1..ends.len()).rev().take(12) {
if ui
.small_button(crate::i18n::fmt(tl!("View revision {n}"), &[("n", &n.to_string())]))
.on_hover_text(tl!("Open the file as it was saved then"))
.clicked()
{
open_revision = Some(n);
// Reserve the full wrapped height before the grid starts the next row.
ui.vertical(|ui| {
ui.horizontal_wrapped(|ui| {
ui.label(ends.len().to_string());
for n in (1..ends.len()).rev().take(12) {
if ui
.small_button(crate::i18n::fmt(tl!("View revision {n}"), &[("n", &n.to_string())]))
.on_hover_text(tl!("Open the file as it was saved then"))
.clicked()
{
open_revision = Some(n);
}
}
}
});
});
ui.end_row();
}
+16 -2
View File
@@ -978,13 +978,15 @@ fn is_form_edit(edit: &Edit) -> bool {
/// classic Outlook (`Temporary Internet Files\Content.Outlook\` on older Windows), and the
/// `Microsoft.OutlookForWindows` package folder for the new Outlook. Saving there either fails,
/// because Outlook can still hold the file, or appears to work while the edits stay out of the
/// email and are deleted with the cache. Matched on either separator and in any case, so it can
/// be tested on every platform.
/// email and are deleted with the cache. Apple Mail opens one from `Mail Downloads/<UUID>/` in
/// its sandbox container, where other apps can't write, so saving there fails (#2751). Matched on
/// either separator and in any case, so it can be tested on every platform.
pub(crate) fn is_mail_temp_copy(path: &str) -> bool {
let p = path.replace('\\', "/").to_ascii_lowercase();
p.contains("/inetcache/content.outlook/")
|| p.contains("/temporary internet files/content.outlook/")
|| p.contains("/microsoft.outlookforwindows")
|| p.contains("/library/containers/com.apple.mail/data/library/mail downloads/")
}
/// Write via a temporary file in the same directory and rename over the target, so a crash or
@@ -1302,6 +1304,16 @@ mod tests {
}
}
#[test]
fn apple_mail_attachment_copies_are_recognised() {
for p in [
"/Users/me/Library/Containers/com.apple.mail/Data/Library/Mail Downloads/6F1C2A9E-3B4D-4E5F-8A7B-9C0D1E2F3A4B/booking.pdf",
"/Users/me/library/containers/COM.APPLE.MAIL/data/library/mail downloads/6F1C2A9E/Booking (2).pdf",
] {
assert!(is_mail_temp_copy(p), "{p}");
}
}
#[test]
fn ordinary_paths_are_not_mail_copies() {
for p in [
@@ -1309,6 +1321,8 @@ mod tests {
r"C:\Users\me\OneDrive - Contoso\Content.Outlook notes\Quote.pdf",
r"C:\Users\me\AppData\Local\Microsoft\Windows\INetCache\IE\Quote.pdf",
"/home/me/Documents/Quote.pdf",
"/Users/me/Downloads/Mail Downloads/booking.pdf",
"/Users/me/Library/Containers/com.apple.Notes/Data/Library/Mail Downloads/booking.pdf",
"Quote.pdf",
] {
assert!(!is_mail_temp_copy(p), "{p}");
+15
View File
@@ -142,6 +142,8 @@ pub mod updates;
mod wheel_pager;
mod widgets;
pub mod window_controls;
#[cfg(not(target_arch = "wasm32"))]
pub mod window_state;
#[cfg(all(test, not(target_arch = "wasm32")))]
mod open_worker_tests;
@@ -744,6 +746,9 @@ pub struct PdfCraftApp {
dialog_epoch: u64,
/// The egui context, for commands that change window or theme state.
ctx: Option<egui::Context>,
/// The window's size and place kept across a minimized close (desktop).
#[cfg(not(target_arch = "wasm32"))]
pub window_state: window_state::WindowState,
styled: bool,
fonts_ready: bool,
/// The CJK fallback order the installed fonts follow (see `theme::installed_font_definitions`).
@@ -1033,6 +1038,8 @@ impl PdfCraftApp {
dialog_seen: None,
dialog_epoch: 0,
ctx: None,
#[cfg(not(target_arch = "wasm32"))]
window_state: Default::default(),
styled: false,
fonts_ready: false,
fonts_cjk: theme::CjkPreference::Default,
@@ -2917,11 +2924,19 @@ impl eframe::App for PdfCraftApp {
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
// eframe has just stored the window as it is now: not while it is minimized.
#[cfg(not(target_arch = "wasm32"))]
{
let minimized = self.ctx.as_ref().is_some_and(|c| c.input(|i| i.viewport().minimized == Some(true)));
self.window_state.keep(storage, minimized);
}
storage.set_string("pdfcraft", self.persist());
}
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.ctx = Some(ctx.clone());
#[cfg(not(target_arch = "wasm32"))]
self.window_state.repair(ctx);
self.dialog_epoch();
// Notices raised outside `ui` (opened files, OS events, the control channel) translate too.
let lang = i18n::Lang::from_pref(&self.language);
+47 -7
View File
@@ -55,9 +55,8 @@ fn reveal(path: &std::path::Path) -> Result<(), String> {
return Err("The file no longer exists".into());
}
let platform = std::env::consts::OS;
let run = |program: &str, args: &[std::ffi::OsString]| {
std::process::Command::new(program).args(args).output().map(|o| o.status.success()).map_err(|e| e.to_string())
};
let run =
|program: &str, args: &[std::ffi::OsString]| reveal_process(program, args).output().map(|o| o.status.success()).map_err(|e| e.to_string());
if matches!(platform, "linux" | "freebsd") {
// `dbus-send --print-reply` waits up to 2 s for the file manager (then xdg-open may run),
// so it runs off the UI thread; a failure there is only logged.
@@ -71,6 +70,24 @@ fn reveal(path: &std::path::Path) -> Result<(), String> {
reveal_via(platform, &path, run)
}
/// The process that runs `program` with `args`. Windows: `reveal_command` already quotes what
/// Explorer needs quoted, and Rust's own quoting of an argument with a space breaks `/select,`
/// (#2579), so the arguments go on the command line as written.
#[cfg(not(target_arch = "wasm32"))]
fn reveal_process(program: &str, args: &[std::ffi::OsString]) -> std::process::Command {
let mut command = std::process::Command::new(program);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt as _;
for arg in args {
command.raw_arg(arg);
}
}
#[cfg(not(windows))]
command.args(args);
command
}
#[cfg(not(target_arch = "wasm32"))]
fn reveal_via(
platform: &str,
@@ -95,9 +112,16 @@ fn reveal_via(
fn reveal_command(platform: &str, path: &std::path::Path) -> Result<(&'static str, Vec<std::ffi::OsString>), String> {
match platform {
"windows" => {
// Explorer uses /select,<object>, rather than a separate option value.
let mut selection = std::ffi::OsString::from("/select,");
// Explorer reads `/select,"<object>"`: the path quoted after the comma (#2579). Quoting
// the whole argument instead ("/select,C:\a b.pdf", what an ordinary argument with a
// space becomes) opens Documents. A Windows path can't contain `"`, so the quotes
// can't be closed early; one that does is refused.
if path.as_os_str().as_encoded_bytes().contains(&b'"') {
return Err("The file name can't be shown in File Explorer".into());
}
let mut selection = std::ffi::OsString::from("/select,\"");
selection.push(path);
selection.push("\"");
Ok(("explorer.exe", vec![selection]))
}
"macos" => Ok(("open", vec!["-R".into(), path.as_os_str().into()])),
@@ -221,6 +245,19 @@ mod tests {
assert!(reveal_via("linux", path, |_, _| Err("command missing".into())).is_err());
}
/// #2579 (review of #2625): the `/select,"…"` argument reaches the process exactly as written.
/// `cmd /c echo` prints its command line back; with Rust's own quoting (`args`) it would come
/// back wrapped in quotes with the inner ones escaped, which Explorer reads as no selection.
#[cfg(windows)]
#[test]
fn the_reveal_argument_reaches_the_process_as_written() {
let (_, args) = reveal_command("windows", std::path::Path::new(r"C:\Reports\annual report.pdf")).unwrap();
let mut echo: Vec<std::ffi::OsString> = ["/d", "/c", "echo"].map(std::ffi::OsString::from).to_vec();
echo.extend(args);
let out = reveal_process("cmd", &echo).output().unwrap();
assert_eq!(String::from_utf8_lossy(&out.stdout).trim_end(), r#"/select,"C:\Reports\annual report.pdf""#);
}
#[test]
fn reveal_arguments_preserve_spaces_and_unicode() {
let path = std::path::Path::new("/tmp/年度 report.pdf");
@@ -229,10 +266,13 @@ mod tests {
assert_eq!(args, [std::ffi::OsString::from("-R"), path.as_os_str().into()]);
let (exe, args) = reveal_command("windows", path).unwrap();
assert_eq!(exe, "explorer.exe");
assert_eq!(args, [std::ffi::OsString::from("/select,/tmp/年度 report.pdf")]);
assert_eq!(args, [std::ffi::OsString::from("/select,\"/tmp/年度 report.pdf\"")]);
// #2579: the path is quoted after the comma, never the whole argument, which Explorer
// reads as no selection and answers by opening Documents.
let special = std::path::Path::new(r"C:\Reports\年度 report, #1 & (draft).pdf");
let (_, windows) = reveal_command("windows", special).unwrap();
assert_eq!(windows, [std::ffi::OsString::from(r"/select,C:\Reports\年度 report, #1 & (draft).pdf")]);
assert_eq!(windows, [std::ffi::OsString::from(r#"/select,"C:\Reports\年度 report, #1 & (draft).pdf""#)]);
assert!(reveal_command("windows", std::path::Path::new(r#"C:\a" b.pdf"#)).is_err(), "a quote can't end the path early");
let (exe, args) = reveal_command("linux", path).unwrap();
assert_eq!(exe, "dbus-send");
assert_eq!(args[6], "array:string:file:///tmp/%E5%B9%B4%E5%BA%A6%20report.pdf");
+137
View File
@@ -0,0 +1,137 @@
//! The window's saved size and place across starts. eframe stores them just before
//! `App::save`, from the window as it is at that moment. A minimized window has neither: Windows
//! reports it as 0 × 0 at (-32000, -32000), and the next start then opened at egui's 64-point
//! floor in the screen's corner. So while the window is minimized the last settings it was shown
//! with are put back, and a window that opens at that floor is given a usable size.
/// eframe's storage key for the window settings.
const WINDOW_KEY: &str = "window";
/// The size egui opens a stored size below (its floor for "ridiculously small" windows), in points.
const EGUI_FLOOR: f32 = 64.0;
/// The size a window that opened at [`EGUI_FLOOR`] is given (apps/pdfcraft opens a first start at
/// this size too), at most nine tenths of the monitor.
const USABLE: egui::Vec2 = egui::vec2(1440.0, 920.0);
#[derive(Debug, Default)]
pub struct WindowState {
/// The settings eframe last stored for a shown (not minimized) window.
shown: Option<String>,
/// The size at start has been checked ([`Self::repair`]).
checked: bool,
}
impl WindowState {
/// The settings the window started with: what to put back if it is minimized before eframe
/// stores a shown one.
pub fn started_with(storage: Option<&dyn eframe::Storage>) -> Self {
Self { shown: storage.and_then(|s| s.get_string(WINDOW_KEY)), checked: false }
}
/// After eframe has stored the window: remember it while the window is shown, and while it
/// is minimized put the last shown settings back (or none, so the next start opens at the
/// default size).
pub fn keep(&mut self, storage: &mut dyn eframe::Storage, minimized: bool) {
if !minimized {
self.shown = storage.get_string(WINDOW_KEY);
} else if let Some(shown) = &self.shown {
storage.set_string(WINDOW_KEY, shown.clone());
} else {
storage.remove_string(WINDOW_KEY);
}
}
/// Once the window's size is known: a shown window at egui's floor in both directions came
/// from settings stored while it was minimized (before this fix), so give it a usable size.
pub fn repair(&mut self, ctx: &egui::Context) {
if self.checked {
return;
}
let (rect, minimized, monitor) = ctx.input(|i| (i.viewport().inner_rect, i.viewport().minimized, i.viewport().monitor_size));
let Some(rect) = rect else { return };
if minimized == Some(true) {
return;
}
self.checked = true;
if rect.width() <= EGUI_FLOOR + 0.5 && rect.height() <= EGUI_FLOOR + 0.5 {
// The stored settings are the broken ones: never put them back.
self.shown = None;
let size = monitor.map_or(USABLE, |m| USABLE.min(m * 0.9));
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(size));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use eframe::Storage as _;
#[derive(Default)]
struct Store(std::collections::HashMap<String, String>);
impl eframe::Storage for Store {
fn get_string(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
fn set_string(&mut self, key: &str, value: String) {
self.0.insert(key.to_string(), value);
}
fn remove_string(&mut self, key: &str) {
self.0.remove(key);
}
fn flush(&mut self) {}
}
const SHOWN: &str = "(outer_position_pixels:Some((x:200.0,y:200.0)),inner_size_points:Some((x:1320.0,y:940.0)))";
const MINIMIZED: &str = "(outer_position_pixels:Some((x:-32000.0,y:-32000.0)),inner_size_points:Some((x:0.0,y:0.0)))";
/// Closing PdfCraft while it was minimized stored a 0 × 0 window off screen.
#[test]
fn a_minimized_window_keeps_the_last_shown_settings() {
let mut store = Store::default();
store.set_string(WINDOW_KEY, SHOWN.into());
let mut state = WindowState::started_with(Some(&store));
// eframe stores the minimized window, then the app saves.
store.set_string(WINDOW_KEY, MINIMIZED.into());
state.keep(&mut store, true);
assert_eq!(store.get_string(WINDOW_KEY).as_deref(), Some(SHOWN));
// Shown again and resized: that is what is kept from then on.
let moved = SHOWN.replace("200.0", "300.0");
store.set_string(WINDOW_KEY, moved.clone());
state.keep(&mut store, false);
store.set_string(WINDOW_KEY, MINIMIZED.into());
state.keep(&mut store, true);
assert_eq!(store.get_string(WINDOW_KEY), Some(moved));
}
#[test]
fn with_nothing_shown_yet_a_minimized_save_stores_no_window() {
let mut store = Store::default();
let mut state = WindowState::started_with(Some(&store));
store.set_string(WINDOW_KEY, MINIMIZED.into());
state.keep(&mut store, true);
assert_eq!(store.get_string(WINDOW_KEY), None, "the next start opens at the default size");
}
fn repaired(size: egui::Vec2, minimized: Option<bool>) -> Vec<egui::ViewportCommand> {
let ctx = egui::Context::default();
let mut input = egui::RawInput::default();
let info = input.viewports.entry(egui::ViewportId::ROOT).or_default();
info.inner_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, size));
info.minimized = minimized;
info.monitor_size = Some(egui::vec2(2674.0, 1504.0));
let mut state = WindowState { shown: Some(MINIMIZED.into()), checked: false };
let out = ctx.run_logic(&input, |ctx| state.repair(ctx));
let commands = out.viewport_commands.get(&egui::ViewportId::ROOT).cloned().unwrap_or_default();
// Repaired: the settings it started with are not put back by a minimized close.
assert_eq!(state.shown.is_none(), !commands.is_empty());
commands
}
#[test]
fn a_window_at_eguis_floor_gets_a_usable_size() {
assert_eq!(repaired(egui::vec2(64.0, 64.0), Some(false)), [egui::ViewportCommand::InnerSize(USABLE)]);
assert!(repaired(egui::vec2(820.0, 520.0), Some(false)).is_empty(), "a small window someone chose stays");
assert!(repaired(egui::vec2(64.0, 64.0), Some(true)).is_empty(), "not while minimized");
}
}
+64
View File
@@ -932,6 +932,70 @@ fn an_earlier_revision_opens_from_document_properties() {
);
}
#[test]
fn wrapped_revision_buttons_stay_above_binding_and_open_the_saved_revision() {
use pdfcraft_engine::{Edit, Session};
let mut bytes = std::sync::Arc::new(fixture(1));
// Reopen each save so the next edit appends to the preceding saved revision.
for revision in 2..=13 {
let mut source = Session::new().with_clock(|| 1_700_000_000);
let id = source.open("revisions.pdf", None, bytes, None).unwrap();
source.apply(id, Edit::SetInfo { key: "Title".into(), value: format!("Revision {revision}") }).unwrap();
bytes = source.save_bytes(id).unwrap();
}
for width in [1400.0, 720.0] {
let pdf = bytes.clone();
let mut h = Harness::builder().with_size(egui::vec2(width, 900.0)).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("revisions.pdf", None, pdf.to_vec()).unwrap();
app
});
let s = h.state();
let doc = s.session.get(s.views[s.active.unwrap()].id).unwrap();
assert_eq!(doc.revision_ends().len(), 13, "the fixture contains thirteen saved revisions");
h.state_mut().dialog = Some(Dialog::Properties(pdfcraft_ui_egui::PropsTab::Advanced));
// Let the grid and the centered dialog settle before measuring or clicking.
h.run_steps(6);
let buttons: Vec<_> = (1..13).map(|n| (n, h.get_by_label(&format!("View revision {n}")).rect())).collect();
let first_row = h.get_by_label("View revision 12").rect();
let lower_revision = buttons
.iter()
.find(|(n, rect)| *n > 1 && rect.top() > first_row.bottom())
.map(|(n, _)| *n)
.expect("revision buttons wrap to a lower row");
let binding = h.get_by_label("Binding").rect();
let left = h.get_by_label("Left Edge").rect();
let right = h.get_by_label("Right Edge").rect();
// Properties requests a fixed width. Measure its card in each viewport.
let card = h.ctx.data(|d| d.get_temp::<egui::Rect>(egui::Id::new("dialog").with("card"))).expect("Properties is shown");
for (n, rect) in &buttons {
for (label, below) in [("Binding", binding), ("Left Edge", left), ("Right Edge", right)] {
assert!(rect.bottom() <= below.top(), "revision {n} overlaps {label} at viewport width {width}: {rect:?}, {below:?}");
}
}
for (n, rect) in &buttons {
assert!(
rect.left() >= card.left() && rect.right() <= card.right(),
"revision {n} escapes Properties horizontally at viewport width {width}: {rect:?}, measured card {card:?}"
);
}
h.get_by_label(&format!("View revision {lower_revision}")).scroll_to_me();
h.run_steps(3);
h.get_by_label(&format!("View revision {lower_revision}")).click();
h.run_steps(3);
let s = h.state();
assert!(s.dialog.is_none(), "opening a revision closes Properties");
let view = &s.views[s.active.unwrap()];
let doc = s.session.get(view.id).unwrap();
assert_eq!(doc.name, format!("revisions (revision {lower_revision}).pdf"));
assert_eq!(doc.info_value("Title"), Some(format!("Revision {lower_revision}")));
let snapshot = view.snapshot.as_ref().expect("the saved revision opens as a snapshot");
assert_eq!(snapshot.kind, pdfcraft_ui_egui::canvas::SnapshotKind::Revision);
assert_eq!(snapshot.revision, lower_revision);
}
}
#[test]
fn advanced_search_lists_results_with_context() {
let mut h = harness();
+1 -1
View File
@@ -101,7 +101,7 @@ total matches the parity estimate.
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. |
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. |
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. Wrapped revision buttons stay above Binding in Document Properties (#2739); a synthetic UI test checks layout and saved revision content. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Later native-open replies stay queued while a modal or picker owns the foreground; regressions cover recipient ownership, completion/cancellation and one-shot wakes. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; Ctrl+Q/Cmd+Q asks before discarding unsaved documents. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation.<br>Candidate: coordinate control clicks pair press/release across slow frames; coalesced primary presses retain text multi-click selection. Regression cases cover slow-frame button activation, disabled controls and text multi-click selection; estimates unchanged. |
+5
View File
@@ -111,6 +111,11 @@
Resource 1 is the app's lion; resource 2 is the PDF document (build.rs). -->
<RegistryValue Root="HKLM" Key="Software\Classes\PdfCraft.Document\DefaultIcon" Value="&quot;[#PdfcraftExe]&quot;,-2" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\PdfCraft.Document\shell\open\command" Value="&quot;[#PdfcraftExe]&quot; &quot;%1&quot;" Type="string" />
<!-- No overlay on thumbnails and in the preview pane: without TypeOverlay, Windows paints
the default program's icon (the lion) over the lower-right corner of every PDF
thumbnail, covering part of the page. An empty string means none ("Thumbnail
Handlers", Microsoft Learn). -->
<RegistryValue Root="HKLM" Key="Software\Classes\PdfCraft.Document" Name="TypeOverlay" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.pdf\OpenWithProgids" Name="PdfCraft.Document" Value="" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\pdfcraft.exe" Name="FriendlyAppName" Value="PdfCraft" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\pdfcraft.exe\DefaultIcon" Value="&quot;[#PdfcraftExe]&quot;,-2" Type="string" />