mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
M14: edits let go of the previous working file before writing the next (gap 13) (#1279)
* M0: keep the camera edge test NaN-safe under clippy's manual_range_contains Clippy 1.97 flags `d < 0.35 || d > PI - 0.35` in the camera edge hit test. Its suggested `!(0.35..=PI - 0.35).contains(&d)` would count a NaN direction as a hit, which the two comparisons did not, so the NaN check is kept explicitly. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011AsuqvnFXhY9ASL1bA2jtm * M14: edits let go of the previous working file before writing the next (gap 13) Every edit writes a new working file: the original plus one appended revision. The previous one stayed alive in the working bytes, the display bytes and the renderer while the next was written, so after a few edits a 1 GiB file peaked at three copies. Each refresh path (full, comments/form, content, undo, redo, after save) now releases them first: the working and display bytes fall back to the original (already held by the editor) and the renderer is replaced by an empty one until the refresh rebuilds it. A refresh that still fails after rollback draws the original instead of nothing. Save still writes from the editor, never from these. Measured on a synthetic 1 GiB file (release build, Linux): five edits peak at 2.1 GiB instead of 3.1 GiB, in 8.5 s instead of 16 s. Saving still peaks at three copies; reading from disk on demand stays blocked on the renderer (hayro parses one contiguous buffer, gap 2). gaps.md, the parity notes and the roadmap record both. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011AsuqvnFXhY9ASL1bA2jtm --------- Co-authored-by: Claude <[email protected]>
This commit is contained in:
1 parent
c52072b678
commit
1970935587
7 files changed
+123
-11
No files matched your search
@@ -137,6 +137,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-12 (M14, gap 13, large-file memory):** Measured on synthetic 1 GiB and 100,000-page files. Edits let go of the previous working file (working bytes, display bytes, renderer) before writing the next, so a 1 GiB file peaks at two copies while editing instead of three (five edits: 3.1 → 2.1 GiB, 16 → 8.5 s). Saving still peaks at three; reading from disk on demand is blocked on the renderer (gap 2). Estimates unchanged.
|
||||
- **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.
|
||||
|
||||
|
||||
@@ -232,7 +232,8 @@ fn support(a: Point, b: Point, edges: &[Option<Edge>], w: usize, h: usize) -> f6
|
||||
edges.get(y as usize * w + x as usize).copied().flatten().is_some_and(|e| {
|
||||
// Either way across: dark-to-light or light-to-dark.
|
||||
let d = (e.dir - normal).rem_euclid(std::f32::consts::PI);
|
||||
d < 0.35 || d > std::f32::consts::PI - 0.35
|
||||
// A NaN direction is no hit, as the two comparisons it replaces said.
|
||||
!d.is_nan() && !(0.35..=std::f32::consts::PI - 0.35).contains(&d)
|
||||
})
|
||||
});
|
||||
hits += hit as usize;
|
||||
|
||||
@@ -3208,7 +3208,9 @@ impl Session {
|
||||
ed.keys = keys;
|
||||
}
|
||||
Self::adopt_keys(doc);
|
||||
let _ = guard(|| Self::refresh(doc));
|
||||
if !matches!(guard(|| Self::refresh(doc)), Ok(Ok(()))) {
|
||||
Self::show_released(doc);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
doc.dirty = true;
|
||||
@@ -3237,7 +3239,7 @@ impl Session {
|
||||
let current_keys = std::mem::replace(&mut editor.keys, keys);
|
||||
editor.redo.push((label.clone(), current, current_keys, scope));
|
||||
Self::adopt_keys(doc);
|
||||
Self::refresh_scoped(doc, scope)?;
|
||||
Self::refresh_scoped(doc, scope).inspect_err(|_| Self::show_released(doc))?;
|
||||
doc.dirty = true;
|
||||
doc.generation += 1;
|
||||
Ok(label)
|
||||
@@ -3252,7 +3254,7 @@ impl Session {
|
||||
let current_keys = std::mem::replace(&mut editor.keys, keys);
|
||||
editor.undo.push((label.clone(), current, current_keys, scope));
|
||||
Self::adopt_keys(doc);
|
||||
Self::refresh_scoped(doc, scope)?;
|
||||
Self::refresh_scoped(doc, scope).inspect_err(|_| Self::show_released(doc))?;
|
||||
doc.dirty = true;
|
||||
doc.generation += 1;
|
||||
Ok(label)
|
||||
@@ -3277,6 +3279,10 @@ impl Session {
|
||||
if scope == Scope::Content {
|
||||
return Self::refresh_content(doc, None);
|
||||
}
|
||||
// Metadata edits keep the renderer, which holds the working file anyway.
|
||||
if scope != Scope::Metadata {
|
||||
Self::release_working(doc);
|
||||
}
|
||||
let Some(editor) = doc.editor.as_mut() else { return Ok(()) };
|
||||
// Comment edits, and form edits (a script may change annotation properties), may change
|
||||
// annotation dictionaries, invalidating cached appearances.
|
||||
@@ -3369,6 +3375,7 @@ impl Session {
|
||||
/// from undo/redo, re-reads both: always correct, rarely paid). The cached display
|
||||
/// appearances are kept: content edits leave every annotation dictionary alone.
|
||||
fn refresh_content(doc: &mut Document, edit: Option<&Edit>) -> Result<(), EditError> {
|
||||
Self::release_working(doc);
|
||||
let Some(editor) = doc.editor.as_mut() else { return Ok(()) };
|
||||
// A script may have laid an XFA form out again (rows added, subforms shown).
|
||||
if doc.xfa.is_some()
|
||||
@@ -3401,6 +3408,28 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Let go of the working file before the next one is written (gap 13). The working file is
|
||||
/// the original plus one appended revision, so on a 1 GiB file keeping the old one (in the
|
||||
/// working bytes, the display bytes and the renderer) while writing the new one held three
|
||||
/// copies at once; this keeps two. Until the refresh finishes, the working and display bytes
|
||||
/// are the original's (already held by the editor, so free) and the renderer draws nothing.
|
||||
/// A failed edit rolls back and refreshes again, which replaces all three; where a refresh
|
||||
/// still fails, [`Self::show_released`] draws the original rather than nothing. Save writes
|
||||
/// from the editor, never from these.
|
||||
fn release_working(doc: &mut Document) {
|
||||
let Some(original) = doc.editor.as_ref().map(|e| e.cos.bytes().clone()) else { return };
|
||||
// Dropping the pool stops its workers; each lets go of the file when it exits.
|
||||
doc.renderer = RenderPool::new_inline(Arc::new(Vec::new()), doc.config.clone());
|
||||
doc.display = original.clone();
|
||||
doc.bytes = original;
|
||||
}
|
||||
|
||||
/// After a refresh failed: draw the display bytes it left (the original, once released),
|
||||
/// so the page still shows a document.
|
||||
fn show_released(doc: &mut Document) {
|
||||
doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone());
|
||||
}
|
||||
|
||||
/// Rebuild working bytes, inspection and renderer from the current edit state.
|
||||
fn refresh(doc: &mut Document) -> Result<(), EditError> {
|
||||
// A full refresh may follow structural or annotation changes: cached appearances
|
||||
@@ -3408,6 +3437,7 @@ impl Session {
|
||||
if let Some(editor) = doc.editor.as_mut() {
|
||||
editor.appearances.clear();
|
||||
}
|
||||
Self::release_working(doc);
|
||||
let Some(editor) = doc.editor.as_mut() else { return Ok(()) };
|
||||
// A script may have laid an XFA form out again (rows added, subforms shown).
|
||||
if doc.xfa.is_some()
|
||||
@@ -3484,7 +3514,7 @@ impl Session {
|
||||
}
|
||||
doc.dirty = false;
|
||||
doc.generation += 1;
|
||||
Self::refresh(doc)
|
||||
Self::refresh(doc).inspect_err(|_| Self::show_released(doc))
|
||||
}
|
||||
|
||||
/// Enter the permissions (owner) password of a document opened with its user password,
|
||||
@@ -3542,7 +3572,7 @@ impl Session {
|
||||
Self::adopt_keys(doc);
|
||||
doc.dirty = false;
|
||||
doc.generation += 1;
|
||||
Self::refresh(doc)
|
||||
Self::refresh(doc).inspect_err(|_| Self::show_released(doc))
|
||||
}
|
||||
|
||||
/// The document's file changed on disk (a LaTeX or Typst build rewrote it, #431): open
|
||||
|
||||
@@ -3881,3 +3881,72 @@ fn fonts_added_by_a_content_edit_show_after_refresh_fonts() {
|
||||
session.refresh_fonts(id);
|
||||
assert_eq!(format!("{:?}", session.get(id).unwrap().info.fonts), fonts);
|
||||
}
|
||||
|
||||
/// Gap 13: every edit writes a new working file (the original plus one revision). Before the
|
||||
/// next one is written, the current one is let go of by the working bytes, the display bytes and
|
||||
/// the renderer: on a 1 GiB file, keeping it is a whole extra gigabyte at the peak of each edit.
|
||||
#[test]
|
||||
fn the_working_file_is_let_go_of_before_the_next_is_written() {
|
||||
let (mut s, id) = session_with(2);
|
||||
s.apply(id, Edit::RotatePages { pages: vec![1], degrees: 90 }).unwrap();
|
||||
let doc = s.doc_mut(id).unwrap();
|
||||
let (working, original) = (Arc::downgrade(&doc.bytes), doc.editor.as_ref().unwrap().cos.bytes().clone());
|
||||
assert!(!Arc::ptr_eq(&doc.bytes, &original), "an edited working file is a buffer of its own");
|
||||
Session::release_working(doc);
|
||||
assert!(Arc::ptr_eq(&doc.bytes, &original) && Arc::ptr_eq(&doc.display, &original));
|
||||
// The old renderer's workers let go of the file when they exit, just after.
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
while working.upgrade().is_some() {
|
||||
assert!(std::time::Instant::now() < deadline, "the working file is still held");
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
// The refresh that follows brings everything back.
|
||||
Session::refresh(doc).unwrap();
|
||||
assert_eq!(s.get(id).unwrap().info.pages[1].rotation, 90);
|
||||
let _ = shown(&s, id, 1);
|
||||
}
|
||||
|
||||
/// Every refresh path (full, comments, content, undo, redo) lets go of the previous working file
|
||||
/// and still shows the current state.
|
||||
#[test]
|
||||
fn edits_let_go_of_the_previous_working_file() {
|
||||
let (mut s, id) = session_with(2);
|
||||
let shape = Shape::Rectangle { rect: [20.0, 20.0, 60.0, 60.0] };
|
||||
let comment = || {
|
||||
Edit::AddAnnotation(NewAnnotation {
|
||||
page: 0,
|
||||
style: Style::default_for(&shape),
|
||||
shape: shape.clone(),
|
||||
contents: String::new(),
|
||||
author: "Test".into(),
|
||||
})
|
||||
};
|
||||
let watermark =
|
||||
|| Edit::AddWatermark { pages: vec![0], settings: Watermark { text: "Draft".into(), ..Watermark::default() }, file: None, replace: false };
|
||||
s.apply(id, Edit::RotatePages { pages: vec![1], degrees: 90 }).unwrap();
|
||||
// A full refresh, a comment refresh, a content refresh, then undo and redo.
|
||||
let steps: [(&str, Box<dyn Fn(&mut Session)>); 5] = [
|
||||
("rotate", Box::new(|s: &mut Session| s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 }).unwrap())),
|
||||
("comment", Box::new(move |s: &mut Session| s.apply(id, comment()).unwrap())),
|
||||
("watermark", Box::new(move |s: &mut Session| s.apply(id, watermark()).unwrap())),
|
||||
("undo", Box::new(|s: &mut Session| drop(s.undo(id).unwrap()))),
|
||||
("redo", Box::new(|s: &mut Session| drop(s.redo(id).unwrap()))),
|
||||
];
|
||||
for (name, step) in steps {
|
||||
let (bytes, display) = {
|
||||
let doc = s.get(id).unwrap();
|
||||
(Arc::downgrade(&doc.bytes), Arc::downgrade(&doc.display))
|
||||
};
|
||||
step(&mut s);
|
||||
// The old renderer's workers let go of the file when they exit, just after.
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
|
||||
while bytes.upgrade().is_some() || display.upgrade().is_some() {
|
||||
assert!(std::time::Instant::now() < deadline, "{name}: the previous working file is still held");
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
// What is shown is still the current state.
|
||||
let doc = s.get(id).unwrap();
|
||||
assert_eq!(doc.info.annotations.len(), 1);
|
||||
let _ = shown(&s, id, 0);
|
||||
}
|
||||
+13
-2
@@ -1,6 +1,6 @@
|
||||
# Where PdfCraft falls short of Acrobat Pro
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Word invoice regression coverage, #773; Office fidelity still partial; gap 2: hayro patch counts after the native-gradient backport) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (gap 13: large-file memory measured, edits keep two copies instead of three, file-backed input blocked on gap 2) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (gap 1 closed by #836; fixed issues removed from gaps 3, 6 and 16) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (document-font subset completion and curved/shared image clips in the contributor fork; gap 2: hayro patch counts after the native-gradient backport) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
|
||||
@@ -218,7 +218,17 @@ update the numbers.
|
||||
memory budget, with budgets checked on Tier-1 platforms.
|
||||
- **Evidence:** #307 brought a 9,156-page file from > 10 GiB to ≈ 500 MiB inspection, but
|
||||
file-backed input and global memory budgets remain open; blurry fonts at common resolutions
|
||||
(#730).
|
||||
(#730). Measured 2026-10-12 on synthetic files (release build, Linux): a 1 GiB file opens at
|
||||
1.0 GiB peak in 0.7 s, so viewing holds the whole file once; after edits it held two copies and
|
||||
peaked at three (3.1 GiB after five edits), because the previous working file stayed alive while
|
||||
the next was written. Edits now let go of it first: five edits peak at 2.1 GiB (8.5 s, was 16 s).
|
||||
Saving still peaks at three copies (the saved file is assembled in memory beside the working
|
||||
one), and display appearances for comments without one add a copy. A 100,000-page file (28 MB)
|
||||
opens at 403 MiB in 5.5 s and each edit re-inspects it for ≈ 5 s.
|
||||
- **Blocked part:** reading a GB file from disk on demand (`ByteSource`) needs every reader to
|
||||
take ranges, but the `hayro` renderer parses one contiguous buffer (`Pdf::new(PdfData)`), so it
|
||||
waits on gap 2's renderer decision. Until then the budget is one copy of the file to view and two
|
||||
to edit.
|
||||
- **Estimate:** 25–45 h. **Doc:** this file.
|
||||
|
||||
### 14. Arabic not mirrored; no Hindi, Indonesian, Korean, Vietnamese
|
||||
@@ -309,6 +319,7 @@ update the numbers.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Gap 13: measured large-file memory; edits no longer keep the previous working file while writing the next (peak 3 → 2 copies); file-backed input recorded as blocked on gap 2 |
|
||||
| 2026-10-11 | minor | Word invoice failure #773 has synthetic regression coverage; broader Office conversion remains gap 8. |
|
||||
| 2026-10-11 | minor | Contributor fork: mixed-style visual regions, source-font space reuse and a read-only textless background. Embedded sfnt source faces and Mac subset Unicode mappings keep edit baselines stable; live frames follow Apply/Save geometry as text grows or shrinks. Engine/tool/UI regressions and private all-page checks cover the changes; full matching font import, CFF, range styles, shaping and private Chinese typing checks added; legacy programs and general fidelity remain open; estimates unchanged |
|
||||
| 2026-10-11 | patch | Gap 2: 23 hayro-interpret and 10 hayro patches after axial and radial shadings became native gradients (backport of hayro 0.8) |
|
||||
|
||||
+2
-2
@@ -94,9 +94,9 @@ total matches the parity estimate.
|
||||
| M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 80–140 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA |
|
||||
| M12 | Accessibility, compare, measure, search, XFA | 200–380 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools).<br>Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export. Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, geospatial/3D measurement, search index, XFA |
|
||||
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers<br>Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation |
|
||||
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Traditional Chinese accessibility statuses, rule semantics and Taiwan form/table terms have catalog regressions. Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation |
|
||||
| M14 | 1.0 polish: performance, localization, installers | 120–250 | 15% | 80–150 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`. Catalogs of ≈ 2,000–2,140 entries for Japanese, Simplified and Traditional Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian, Ukrainian, Italian, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Signed and notarized macOS builds, signed Windows installers (x64, x86, ARM64), AppImage, deb, rpm, Flatpak and FreeBSD packages. #307 (bounded large-PDF memory): shared resource indexes and parser metadata, lazy inspection, bounded caches; a 9,156-page file inspects in ≈ 1.8 s at ≈ 500 MiB instead of exceeding 10 GiB. Edits let go of the previous working file before writing the next (a 1 GiB file peaks at two copies while editing, not three; gap 13). Shortcut display uses localized modifier/key names across menus, toolbar, palette, Help and tooltips (#496). Traditional Chinese accessibility statuses, rule semantics and Taiwan form/table terms have catalog regressions. Missing: a right-to-left layout, a Chinese UI face in releases, Hindi/Indonesian/Korean/Vietnamese, file-backed input and global memory budgets, performance budgets, keyboard-only operation |
|
||||
| | **Total** | **2,085–3,840** | **≈ 49% ready** | **≈ 770–1,400** | Area view of the same work: [target-app-parity.md](target-app-parity.md#by-feature-area) |
|
||||
|
||||
## Upcoming milestones, ranked
|
||||
|
||||
@@ -395,7 +395,7 @@ status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = "ByteSource (M1.5); files are buffered whole today"
|
||||
notes = "ByteSource (M1.5); files are buffered whole today. Viewing holds the file once and editing twice: edits let go of the previous working file before writing the next (a 1 GiB file peaked at three copies after a few edits, now two). Saving still peaks at three. Reading from disk on demand waits on the renderer (hayro parses one contiguous buffer, gap 2)."
|
||||
|
||||
[[feature]]
|
||||
id = "core.pdf20-utf8-strings"
|
||||
|
||||
Reference in new issue
Block a user