mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 04:28:20 -06:00
Preserve literal spaces in page-label prefixes (#2355)
* M0: update rustls to 0.23.46 Co-authored-by: Codex <codex@openai.com> * M2: preserve literal page-label prefix spaces Keep label prefixes separate from document metadata trimming and cover literal spaces through public inspection. Co-authored-by: Codex <codex@openai.com> --------- Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Codex
sanyamsingh001
Brandon Thomas
parent
1262aa8cf7
commit
c10e7b8f0c
@@ -158,6 +158,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
## Progress log
|
||||
- **2026-10-13 (G14, Vietnamese localization):** Added Vietnamese (`vi`, Tiếng Việt) as an interface and date language with a 2,591-entry catalog (`plural_none`) covering 100% of the current localizable key union (plus `theme`/`Appearance`). Locale tags `vi` / `vi-VN` / `vi_VN.UTF-8` resolve; calendar months use numeric `Tháng 1`…`12` / `tháng 1`…`12`. Native-speaker community review recorded. The twelve-language localization mean moves from ≈59% to ≈68%; weighted readiness ≈49% → ≈50%. Status stays `partial`: engine/OS details, document contents, filenames, CLI/MCP and external help stay outside the catalog by design.
|
||||
|
||||
- **2026-10-13 (M2, page-label prefixes):** Preserve literal leading and trailing whitespace in page-label prefixes without changing generic metadata normalization. Nine original public-inspection cases cover prefix spaces, encodings and empty-prefix controls. Existing empty-prefix fallback remains a compatibility policy, not a conformance claim. Estimates unchanged.
|
||||
- **2026-10-14 (M14, Italian catalog):** Italian gains the 201 entries the other catalogs had and the code looks up: the sentences the accessibility checker, preflight, signature validation and page-range parser word, certificate and calendar labels, digital-ID dialog messages and five plurals. It now has 2,528 entries. 64 keys other catalogs carry but no code looks up (tooltip texts with a fixed shortcut, older wordings, generic labels only Lithuanian has) were left out. AI-assisted, written from the meaning of the English; native review welcome. Estimates unchanged.
|
||||
- **2026-10-15 (M3, #2166 part 1):** Save asks before writing over a version of the file the document did not come from (something else wrote it since the open or the last save), with Save as… to keep it, and the changed-on-disk notice says what Reload and Save each do; a file that is open already comes to the front instead of opening twice; a running instance's autosaves are locked, so a second instance neither offers nor discards them. Part 2 (every writer checked and atomic) is #2182.
|
||||
- **2026-10-14 (M1, #2166):** No PDF leaves the engine unchecked and no writer truncates: new files from Combine, Extract, Split and Create, signed, timestamped and LTV-embedded files and the OCR batch output reopen with their pages before anyone writes them, as saves already did; the desktop app, the automation tools (with or without `--root`) and the CLI's edit, combine, extract and split all write through the one staging-file writer in `pdfcraft-platform`, so a failed write or `--out` naming the input never leaves a half-written file. Special files such as `/dev/stdout` are still written as given.
|
||||
|
||||
@@ -984,11 +984,19 @@ impl<'a> Inspector<'a> {
|
||||
for (start, d) in ranges {
|
||||
let first = d.get(b"St").ok().and_then(|o| self.resolve(o).as_i64().ok()).unwrap_or(1).max(1) as u64;
|
||||
let prefix = match d.get(b"P").ok().map(|o| self.resolve(o)) {
|
||||
Some(Object::String(raw, _) | Object::Name(raw)) => {
|
||||
Some(Object::String(raw, _)) => {
|
||||
if raw.len() > MAX_PREFIX_BYTES {
|
||||
return Err("a raw prefix exceeds 2050 bytes");
|
||||
}
|
||||
self.text(d, b"P").unwrap_or_default()
|
||||
// A label prefix is literal text, not trimmed document metadata.
|
||||
text_string(raw).trim_matches('\0').to_string()
|
||||
}
|
||||
Some(Object::Name(raw)) => {
|
||||
if raw.len() > MAX_PREFIX_BYTES {
|
||||
return Err("a raw prefix exceeds 2050 bytes");
|
||||
}
|
||||
// Retain the existing name compatibility and boundary-NUL handling.
|
||||
String::from_utf8_lossy(raw).trim_matches('\0').to_string()
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
@@ -1940,6 +1948,78 @@ mod tests {
|
||||
assert!(info.warnings[0].contains("physical page numbers"), "{:?}", info.warnings);
|
||||
}
|
||||
|
||||
// Contributor-original, blank three-page PDFs. The public importer reads both labels and metadata.
|
||||
fn page_prefix_space_pdf(prefix: Vec<u8>, decimal: bool, indirect: bool) -> Vec<u8> {
|
||||
use lopdf::dictionary;
|
||||
let mut doc = Document::with_version("1.7");
|
||||
let pages_id = doc.new_object_id();
|
||||
let kids: Vec<Object> = (0..3)
|
||||
.map(|_| {
|
||||
doc.add_object(dictionary! {
|
||||
"Type" => "Page",
|
||||
"Parent" => pages_id,
|
||||
"MediaBox" => vec![0.into(), 0.into(), 200.into(), 300.into()],
|
||||
"Resources" => Dictionary::new(),
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
doc.objects.insert(pages_id, dictionary! { "Type" => "Pages", "Kids" => kids, "Count" => 3 }.into());
|
||||
let prefix = Object::String(prefix, lopdf::StringFormat::Hexadecimal);
|
||||
let prefix = if indirect { Object::Reference(doc.add_object(prefix)) } else { prefix };
|
||||
let mut label = Dictionary::new();
|
||||
label.set("P", prefix);
|
||||
if decimal {
|
||||
label.set("S", Object::Name(b"D".to_vec()));
|
||||
}
|
||||
let labels = dictionary! { "Nums" => vec![Object::Integer(0), Object::Dictionary(label)] };
|
||||
let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id, "PageLabels" => labels });
|
||||
let info_id = doc.add_object(dictionary! { "Title" => Object::string_literal(" Metadata title ") });
|
||||
doc.trailer.set("Root", catalog_id);
|
||||
doc.trailer.set("Info", info_id);
|
||||
let mut bytes = Vec::new();
|
||||
doc.save_to(&mut bytes).expect("write original page-prefix fixture");
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_label_prefix_spaces_survive_public_inspection() {
|
||||
let mut utf16 = vec![0xfe, 0xff];
|
||||
utf16.extend(" Ω ".encode_utf16().flat_map(u16::to_be_bytes));
|
||||
for (raw, prefix, decimal, indirect) in [
|
||||
(b" leading".to_vec(), " leading", true, false),
|
||||
(b"trailing ".to_vec(), "trailing ", true, false),
|
||||
(b" both ".to_vec(), " both ", false, false),
|
||||
(b" ".to_vec(), " ", false, false),
|
||||
(b" Caf\xe9 ".to_vec(), " Café ", true, false),
|
||||
(utf16.clone(), " Ω ", true, false),
|
||||
(utf16, " Ω ", false, true),
|
||||
(Vec::new(), "", true, false),
|
||||
(Vec::new(), "", false, false),
|
||||
] {
|
||||
let bytes = page_prefix_space_pdf(raw, decimal, indirect);
|
||||
let info = inspect(Arc::new(bytes), None).expect("public import of original page-prefix fixture");
|
||||
let expected: Vec<String> = (1..=3)
|
||||
.map(|n| {
|
||||
if decimal {
|
||||
format!("{prefix}{n}")
|
||||
} else if prefix.is_empty() {
|
||||
n.to_string()
|
||||
} else {
|
||||
prefix.to_owned()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
info.pages.iter().map(|p| p.label.clone()).collect::<Vec<_>>(),
|
||||
expected,
|
||||
"{prefix:?}, decimal={decimal}, indirect={indirect}"
|
||||
);
|
||||
assert!(info.warnings.is_empty(), "{:?}", info.warnings);
|
||||
assert_eq!(info.title.as_deref(), Some("Metadata title"), "page-prefix decoding must not change metadata normalization");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_label_limits_keep_normal_pdf_numbering() {
|
||||
for (style, first, expected) in [
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ total matches the parity estimate.
|
||||
| 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. |
|
||||
| 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. #2080 candidate: search-result jumps enter per-tab history; rail and control-channel Back/Forward; no exact-view or restart persistence. |
|
||||
| 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. A custom text match that ends on a reconstructed separator stops before the next glyph (#1315). 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); Lab Bradford adaptation uses LMS white-point ratios, covered by independent neutral/chromatic and direct/Indexed render oracles; 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 |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); Lab Bradford adaptation uses LMS white-point ratios, covered by independent neutral/chromatic and direct/Indexed render oracles; 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. Literal page-label prefix whitespace is preserved; nine public-inspection cases cover prefix spaces, encodings and empty-prefix controls |
|
||||
| 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). |
|
||||
| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>Done since: Organize opens directly into the page overview, with checkbox selection, an editable page-range selection (#1092), 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. Extract and Split preserve shared Type 3 glyph resource graphs, retaining conservative pruning limits. Undo back to the opened or last saved file is clean; Revert opens that saved file after an undo; a later save appends to it so the saved revision stays in the file (#1310, #1311, #1312). Missing: page transitions, recovery on the web |
|
||||
| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>Done since: Organize opens directly into the page overview, with checkbox selection, an editable page-range selection (#1092), 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. Extract and Split preserve shared Type 3 glyph resource graphs, retaining conservative pruning limits. Missing: page transitions, recovery on the web M4: Insert-from-file preserves source bookmarks; integration tests retained with current main. |
|
||||
|
||||
Reference in New Issue
Block a user