diff --git a/ROADMAP.md b/ROADMAP.md index b1283e7751..cbdb11995b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,6 +2,8 @@ **Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away +> **Last reviewed:** 2026-10-20 · **Last updated:** 2026-10-20 · **Change:** minor (M12 #3673: Character encoding accepts Identity-H/V with Adobe CJK collections; estimates unchanged) + > **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M5, PR #1971: bounded nested FDF export excludes source page/page-tree dictionaries; refreshed against upstream 4db6ac4a; estimates unchanged) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7: the Edit text caret is as tall as the row it is on; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M6: CSV tab-containing headers; estimates unchanged) @@ -172,6 +174,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-20 (M12, #3673):** Character encoding accepts Identity-H/V fonts with Adobe CJK collections and recognizes predefined non-Identity CMaps independently. Unknown CMaps and foreign registries remain refused; synthetic checker and read-only automation regressions passed after failing before the fix. Per-glyph mapping validation and embedded-font extraction remain separate. Estimates unchanged. + - **2026-10-19 (M14, #3373):** The main menu and its long submenus scroll in short windows, keeping their commands reachable. Estimates unchanged. - **2026-10-19 (M3, #3387):** Save-prompt shortcuts yield to later consent and utility dialogs; a synthetic UI regression checks that an edited document stays open. Estimates unchanged. diff --git a/crates/a11y/README.md b/crates/a11y/README.md index 0a653f1316..96cdad13cc 100644 --- a/crates/a11y/README.md +++ b/crates/a11y/README.md @@ -16,6 +16,10 @@ let html = report_html(&report, "file.pdf", "2026-10-02", "en", &in_english); / - Each result is Passed, Failed (with findings and pages), Needs manual check, or Skipped. - **Content checks** walk the page content streams (and form XObjects) tracking marked content: painting outside MCID-marked or `/Artifact` content is untagged. +- **Character encoding** accepts a predefined non-Identity CJK CMap or an Adobe CJK + character collection (`GB1`, `CNS1`, `Japan1`, `Korea1`, `KR`), including Identity-H/V. + Unknown CMaps and foreign registries do not establish that route. This is a font-level + declaration check, not validation of every ToUnicode entry or used glyph. - **Structure checks** read the structure tree through the role map; table regularity counts `ColSpan`/`RowSpan` attributes. - The report is a self-contained HTML page. Fixes that edit the document (language, title, tab diff --git a/crates/a11y/src/content.rs b/crates/a11y/src/content.rs index e8a7db377f..5811061ec4 100644 --- a/crates/a11y/src/content.rs +++ b/crates/a11y/src/content.rs @@ -360,8 +360,47 @@ fn full_name(doc: &Document, field: ObjRef) -> String { parts.join(".") } -/// Whether text in this font can be mapped to Unicode (ToUnicode, a standard encoding, or a -/// predefined CJK character collection). +/// Predefined CJK CMaps (ISO 32000-2 §9.7.5.2, Table 116), excluding Identity-H/V. +/// Test complete names: neither a custom CMap nor a nonexistent vertical variant is predefined. +fn predefined_cjk_cmap(name: &[u8]) -> bool { + if matches!(name, b"H" | b"V" | b"83pv-RKSJ-H" | b"90pv-RKSJ-H" | b"KSCpc-EUC-H" | b"UniAKR-UTF8-H" | b"UniAKR-UTF16-H" | b"UniAKR-UTF32-H") { + return true; + } + let Some(base) = name.strip_suffix(b"-H").or_else(|| name.strip_suffix(b"-V")) else { return false }; + matches!( + base, + b"GB-EUC" + | b"GBpc-EUC" + | b"GBK-EUC" + | b"GBKp-EUC" + | b"GBK2K" + | b"UniGB-UCS2" + | b"UniGB-UTF16" + | b"B5pc" + | b"HKscs-B5" + | b"ETen-B5" + | b"ETenms-B5" + | b"CNS-EUC" + | b"UniCNS-UCS2" + | b"UniCNS-UTF16" + | b"90ms-RKSJ" + | b"90msp-RKSJ" + | b"Add-RKSJ" + | b"EUC" + | b"Ext-RKSJ" + | b"UniJIS-UCS2" + | b"UniJIS-UCS2-HW" + | b"UniJIS-UTF16" + | b"KSC-EUC" + | b"KSCms-UHC" + | b"KSCms-UHC-HW" + | b"UniKS-UCS2" + | b"UniKS-UTF16" + ) +} + +/// Whether text in this font declares a Unicode mapping route (ToUnicode, a standard encoding, +/// a predefined CJK CMap, or an Adobe CJK character collection). This does not validate every glyph. fn maps_to_unicode(doc: &Document, f: &Dict) -> bool { if f.contains(b"ToUnicode") { return true; @@ -369,16 +408,22 @@ fn maps_to_unicode(doc: &Document, f: &Dict) -> bool { let enc = f.get(b"Encoding").map(|e| doc.resolve(e)); match f.name(b"Subtype") { Some(b"Type0") => { - let identity = matches!(enc.as_deref(), Some(Object::Name(n)) if n.starts_with(b"Identity")); - let ordering = f + // ISO 32000-2 §9.10.2: either a predefined non-Identity CMap or the descendant's + // Adobe CJK collection supplies the route. Identity-H/V do not rule out the latter. + if matches!(enc.as_deref(), Some(Object::Name(n)) if predefined_cjk_cmap(n)) { + return true; + } + let system = f .get(b"DescendantFonts") .map(|d| doc.resolve(d)) .and_then(|d| d.as_array().and_then(|a| a.first().cloned())) .and_then(|d| doc.resolve(&d).as_dict().cloned()) - .and_then(|d| d.get(b"CIDSystemInfo").map(|c| doc.resolve(c)).and_then(|c| c.as_dict().cloned())) - .and_then(|c| c.get(b"Ordering").map(|o| doc.resolve(o)).and_then(|o| o.as_string().map(|s| s.bytes.clone()))); - let known = matches!(ordering.as_deref(), Some(b"Japan1" | b"GB1" | b"CNS1" | b"Korea1" | b"KR")); - !identity && known + .and_then(|d| d.get(b"CIDSystemInfo").map(|c| doc.resolve(c)).and_then(|c| c.as_dict().cloned())); + let Some(system) = system else { return false }; + let registry = system.get(b"Registry").map(|r| doc.resolve(r)); + let ordering = system.get(b"Ordering").map(|o| doc.resolve(o)); + matches!(registry.as_deref(), Some(Object::String(s)) if s.bytes == b"Adobe") + && matches!(ordering.as_deref(), Some(Object::String(s)) if matches!(s.bytes.as_slice(), b"Japan1" | b"GB1" | b"CNS1" | b"Korea1" | b"KR")) } Some(b"Type3") => enc.is_some(), _ => match enc.as_deref() { diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index fcd681efc9..0889a7e356 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -536,6 +536,126 @@ fn scans_identity_fonts_and_long_documents() { assert_eq!(some.result(Rule::CharacterEncoding).unwrap().findings[0].page, Some(3)); } +fn cid_encoding_pdf(encoding: &str, system: &str, to_unicode: bool) -> Document { + let unicode = if to_unicode { "/ToUnicode 7 0 R" } else { "" }; + pdf( + &[ + "<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".into(), + stream("/P << /MCID 0 >> BDC BT /F1 12 Tf <002200230024> Tj ET EMC"), + format!("<< /Type /Font /Subtype /Type0 /BaseFont /SyntheticCID /Encoding {encoding} /DescendantFonts [6 0 R] {unicode} >>"), + format!("<< /Type /Font /Subtype /CIDFontType0 /BaseFont /SyntheticCID /CIDSystemInfo {system} >>"), + stream( + "/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /SyntheticUnicode def /CMapType 2 def 1 begincodespacerange <0000> endcodespacerange 3 beginbfchar <0022> <0041> <0023> <0042> <0024> <0043> endbfchar endcmap CMapName currentdict /CMap defineresource pop end end", + ), + ], + "", + ) +} + +#[test] +fn character_encoding_accepts_identity_cjk_collections() { + for encoding in ["/Identity-H", "/Identity-V"] { + for ordering in ["Japan1", "GB1", "CNS1", "Korea1", "KR"] { + let system = format!("<< /Registry (Adobe) /Ordering ({ordering}) /Supplement 0 >>"); + let doc = cid_encoding_pdf(encoding, &system, false); + let report = checked(&doc, &[Rule::CharacterEncoding]); + let result = report.result(Rule::CharacterEncoding).unwrap(); + assert_eq!(result.status, Status::Passed, "{encoding}, Adobe-{ordering}: {:?}", result.findings); + assert!(result.findings.is_empty()); + } + } +} + +#[test] +fn character_encoding_accepts_predefined_cmaps_without_a_known_collection() { + // The predefined CMap and the descendant's character collection are alternative routes. + let system = "<< /Registry (Private) /Ordering (Custom) /Supplement 0 >>"; + for name in [ + "GB-EUC-H", + "GB-EUC-V", + "GBK2K-H", + "GBK2K-V", + "UniGB-UTF16-H", + "UniGB-UTF16-V", + "B5pc-H", + "B5pc-V", + "UniCNS-UCS2-H", + "UniCNS-UCS2-V", + "83pv-RKSJ-H", + "90pv-RKSJ-H", + "90ms-RKSJ-H", + "90ms-RKSJ-V", + "H", + "V", + "UniJIS-UCS2-HW-H", + "UniJIS-UCS2-HW-V", + "KSCpc-EUC-H", + "KSC-EUC-V", + "KSCms-UHC-HW-H", + "KSCms-UHC-HW-V", + "UniKS-UTF16-H", + "UniKS-UTF16-V", + "UniAKR-UTF8-H", + "UniAKR-UTF16-H", + "UniAKR-UTF32-H", + ] { + let doc = cid_encoding_pdf(&format!("/{name}"), system, false); + let report = checked(&doc, &[Rule::CharacterEncoding]); + assert_eq!(status(&report, Rule::CharacterEncoding), Status::Passed, "{name}"); + } +} + +#[test] +fn character_encoding_rejects_unknown_cmaps_and_foreign_collections() { + for encoding in + ["/Identity-H", "/Identity-V", "/Custom-H", "/UniCustom-UTF16-H", "/UniJIS-UCS2-HW-extra-H", "/83pv-RKSJ-V", "/KSCpc-EUC-V", "null"] + { + for system in [ + "<< /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>", + "<< /Registry (Adobe) /Ordering (Custom) /Supplement 0 >>", + "<< /Registry (Private) /Ordering (Japan1) /Supplement 0 >>", + "<< /Registry (adobe) /Ordering (Japan1) /Supplement 0 >>", + "<< /Ordering (Japan1) /Supplement 0 >>", + "<< /Registry /Adobe /Ordering /Japan1 /Supplement 0 >>", + "null", + ] { + let doc = cid_encoding_pdf(encoding, system, false); + let report = checked(&doc, &[Rule::CharacterEncoding]); + let result = report.result(Rule::CharacterEncoding).unwrap(); + assert_eq!(result.status, Status::Failed, "{encoding}, {system}"); + assert_eq!(result.findings.len(), 1); + assert_eq!(result.findings[0].page, Some(0)); + assert!(result.findings[0].message.contains("SyntheticCID")); + } + } + let doc = cid_encoding_pdf("/Identity-H", "<< /Registry (Private) /Ordering (Custom) /Supplement 0 >>", true); + assert_eq!(status(&checked(&doc, &[Rule::CharacterEncoding]), Rule::CharacterEncoding), Status::Passed, "ToUnicode is still sufficient"); +} + +#[test] +fn character_encoding_resolves_indirect_cid_metadata() { + use pdfcraft_cos::{Dict, ObjRef, PdfString}; + let mut doc = cid_encoding_pdf("/Identity-H", "null", false); + let registry = doc.add(Object::String(PdfString::literal(b"Adobe".to_vec()))); + let ordering = doc.add(Object::String(PdfString::literal(b"Japan1".to_vec()))); + let mut system = Dict::new(); + system.set(b"Registry".to_vec(), Object::Ref(registry)); + system.set(b"Ordering".to_vec(), Object::Ref(ordering)); + let system = doc.add(Object::Dict(system)); + doc.update_dict(ObjRef::new(6, 0), |d| d.set(b"CIDSystemInfo".to_vec(), Object::Ref(system))).unwrap(); + let descendants = doc.add(Object::Array(vec![Object::Ref(ObjRef::new(6, 0))])); + let encoding = doc.add(Object::Name(b"Identity-V".to_vec())); + doc.update_dict(ObjRef::new(5, 0), |d| { + d.set(b"DescendantFonts".to_vec(), Object::Ref(descendants)); + d.set(b"Encoding".to_vec(), Object::Ref(encoding)); + }) + .unwrap(); + let report = checked(&doc, &[Rule::CharacterEncoding]); + assert_eq!(status(&report, Rule::CharacterEncoding), Status::Passed); +} + #[test] fn rules_have_ids_and_the_report_lists_them() { assert_eq!(Rule::ALL.len(), 32); diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index cbf40f1962..d49428789e 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -2294,6 +2294,63 @@ fn exporting_images_and_text_through_tools() { assert!(a.call("doc_export_text", &json!({ "doc": doc, "path": "/etc/x.txt" })).is_err(), "confined to the root"); } +#[test] +fn accessibility_character_encoding_accepts_identity_cjk_through_tools() { + use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, SaveOptions, Stream, write_full}; + use std::sync::Arc; + + let dir = workdir("a11y-identity-cjk"); + let mut a = auto(&dir); + for encoding in ["Identity-H", "Identity-V"] { + let mut source = Document::open(Arc::new(fixture(1))).unwrap(); + let mut system = Dict::new(); + system.set(b"Registry".to_vec(), Object::String(PdfString::literal(b"Adobe".to_vec()))); + system.set(b"Ordering".to_vec(), Object::String(PdfString::literal(b"Japan1".to_vec()))); + system.set(b"Supplement".to_vec(), Object::Int(0)); + let system = source.add(Object::Dict(system)); + let mut descriptor = Dict::new(); + descriptor.set(b"Type".to_vec(), Object::Name(b"FontDescriptor".to_vec())); + descriptor.set(b"FontName".to_vec(), Object::Name(b"SyntheticCID".to_vec())); + descriptor.set(b"Flags".to_vec(), Object::Int(6)); + descriptor.set(b"FontBBox".to_vec(), Object::Array([0, -200, 1000, 800].into_iter().map(Object::Int).collect())); + descriptor.set(b"ItalicAngle".to_vec(), Object::Int(0)); + descriptor.set(b"Ascent".to_vec(), Object::Int(800)); + descriptor.set(b"Descent".to_vec(), Object::Int(-200)); + descriptor.set(b"CapHeight".to_vec(), Object::Int(700)); + descriptor.set(b"StemV".to_vec(), Object::Int(80)); + let descriptor = source.add(Object::Dict(descriptor)); + let mut cid = Dict::new(); + cid.set(b"Type".to_vec(), Object::Name(b"Font".to_vec())); + cid.set(b"Subtype".to_vec(), Object::Name(b"CIDFontType0".to_vec())); + cid.set(b"BaseFont".to_vec(), Object::Name(b"SyntheticCID".to_vec())); + cid.set(b"CIDSystemInfo".to_vec(), Object::Ref(system)); + cid.set(b"FontDescriptor".to_vec(), Object::Ref(descriptor)); + cid.set(b"DW".to_vec(), Object::Int(1000)); + let cid = source.add(Object::Dict(cid)); + source + .update_dict(ObjRef::new(3, 0), |font| { + font.set(b"Subtype".to_vec(), Object::Name(b"Type0".to_vec())); + font.set(b"BaseFont".to_vec(), Object::Name(b"SyntheticCID".to_vec())); + font.set(b"Encoding".to_vec(), Object::Name(encoding.as_bytes().to_vec())); + font.set(b"DescendantFonts".to_vec(), Object::Array(vec![Object::Ref(cid)])); + }) + .unwrap(); + source.set(ObjRef::new(5, 0), Object::Stream(Stream::flate(Dict::new(), b"BT /F1 24 Tf 20 150 Td <002200230024> Tj ET"))); + let name = format!("{encoding}.pdf"); + let bytes = write_full(&source, &SaveOptions::default()).unwrap(); + std::fs::write(dir.join(&name), &bytes).unwrap(); + let doc = ok(&mut a, "doc_open", json!({ "path": name }))["doc"].as_u64().unwrap(); + assert_eq!(page_text(&mut a, doc), ["ABC"], "the collection maps the CIDs to Unicode without ToUnicode"); + let report = ok(&mut a, "accessibility_check", json!({ "doc": doc, "rules": ["character-encoding"] })); + let result = report["results"].as_array().unwrap().iter().find(|r| r["rule"] == "character-encoding").unwrap(); + assert_eq!(result["status"], "passed", "{encoding}: {result}"); + assert_eq!(result["findings"], json!([])); + assert_eq!(ok(&mut a, "doc_info", json!({ "doc": doc }))["document"]["dirty"], false); + assert_eq!(std::fs::read(dir.join(name)).unwrap(), bytes, "checking is read-only"); + } + std::fs::remove_dir_all(dir).unwrap(); +} + #[test] fn accessibility_check_report_and_fixes_through_tools() { let dir = workdir("a11y"); diff --git a/docs/gaps.md b/docs/gaps.md index 2682d090ac..b201a17fee 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,5 +1,7 @@ # Where PdfCraft falls short of Acrobat Pro +> **Last reviewed:** 2026-10-20 · **Last updated:** 2026-10-20 · **Change:** minor (gap 6 #3673: Character encoding CJK mapping routes corrected; no readiness re-estimate) + > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (gap 12: reading order is scored, not just word F1) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M1: raw-deflate fallback preserves decoding-limit errors; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (gap 14: Vietnamese catalog shipped; Hindi/Indonesian/Korean and Arabic RTL remain) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -144,6 +146,11 @@ update the numbers. - **Fix verified (2026-10-14):** raw-deflate fallback returns a decoding-limit error instead of accepting an empty partial stream when a misleading zlib header triggers a retry. Synthetic filter and COS stream regressions cover refusal at the limit and decoding within it. +- **Fix verified (2026-10-20, #3673):** Character encoding accepts Identity-H/V fonts + whose Adobe CJK collection supplies the Unicode mapping, independently of predefined + non-Identity CMaps. Unknown CMaps and foreign registries remain refused. Synthetic checker + and read-only automation regressions cover the change; per-glyph validation and the + separate embedded-font extraction gap remain open. No readiness or backlog re-count. - **Impact:** every one is a user who lost trust. Crash reports outrank feature work (`AGENTS.md` §4). - **Estimate:** 45–80 h (≈ 0.2–0.4 h per issue). **Doc:** this file. @@ -382,6 +389,7 @@ update the numbers. | Date | Change | Summary | |---|---|---| +| 2026-10-20 | minor | Gap 6 #3673: Character encoding accepts Identity-H/V with Adobe CJK collections, or a predefined non-Identity CMap; negative and indirect-reference regressions and a read-only automation test passed after failing before the fix. Per-glyph validation and embedded-font extraction remain separate. Scores and estimates unchanged. | | 2026-10-15 | minor | Gap 12: the text oracle scores reading order as well as word F1, which is a bag of words and cannot see order; the first measurement separated 18 improvements from 6 regressions in a change F1 called unchanged on 26 of 29 files. Estimates unchanged. | | 2026-10-14 | minor | M1: raw-deflate fallback preserves decoding-limit errors in strict and tolerant decoding; synthetic filter and COS stream regressions. Estimates unchanged. | | 2026-10-15 | patch | Gap 24: record localization and installers as partial (unusable Chinese, Telugu and Arabic; unsigned Linux packages) | diff --git a/docs/roadmap.md b/docs/roadmap.md index 5d73511e9c..87cbba9ca3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,7 @@ # PdfCraft roadmap: milestones and what's next +> **Last reviewed:** 2026-10-20 · **Last updated:** 2026-10-20 · **Change:** minor (M12 #3673: Character encoding accepts Identity-H/V with Adobe CJK collections; estimates unchanged) + > **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M5, PR #1971: bounded nested FDF export excludes source page/page-tree dictionaries; refreshed against upstream 4db6ac4a; estimates unchanged) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M14/#2383 GPU-limit candidate; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7: the Edit text caret is as tall as the row it is on; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -147,9 +149,9 @@ total matches the parity estimate. | M9 | Signatures (PAdES, validation) | 160–280 | 60% | 40–70 | Done: new `sign` crate (DER, X.509, CMS, PKCS #12 on RustCrypto; aws-lc-rs for RSA private keys); PAdES B-B signing (visible, invisible, existing fields, certification with DocMDP); validation with trust store and changes-after-signing classification; self-signed digital IDs; Signatures panel, message bar, sign dialogs; agent tools. Checked with pdfsig and OpenSSL.
Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites, page validation overlays separate from original signature artwork (baked Not Verified captions remain; #79 open), snapshot tabs that reuse instead of multiplying. Missing: timestamps (B-T), LTV (DSS, OCSP, CRL), FieldMDP, smart cards, PKCS #11 | | M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).
Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773); printing on the web through the browser's print dialog, sheets drawn by PdfCraft (gap 4). Lossless image export applies per-component Decode, including Indexed mapping (#1201 partial). Existing 16-bit precision and color-management limits remain. #3120: Office export reads form XObjects 50 levels deep, as the renderer does, and up to 50 000 a page; pages past that are listed in `truncated_pages`. Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import | | 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). Locatable figure, annotation and form findings highlight their page bounds.
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded, text-only auto-tag proposal tool for one page. Marking a figure decorative rewrites only that stream (#1324). An XFA radio item whose value contains a space is written back unchanged (#1319). Missing: writing proposed tags and parent tree, field autotag, visual Tags/Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA | +| 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). Locatable figure, annotation and form findings highlight their page bounds. Character encoding accepts Identity-H/V with Adobe CJK collections and predefined CJK CMaps independently (#3673); per-glyph validation and embedded-font extraction remain separate.
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded, text-only auto-tag proposal tool for one page. Marking a figure decorative rewrites only that stream (#1324). An XFA radio item whose value contains a space is written back unchanged (#1319). Missing: writing proposed tags and parent tree, field autotag, visual Tags/Order/Content panels, Reading Order tool, 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
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. CLI robustness scans visit each directory once through overlapping roots and symlink aliases. | -| 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). Locatable figure, annotation and form findings highlight their page bounds.
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded text-only auto-tag proposal tool; text-only Apply writes marked content, paragraph tags and a parent tree, then opens the Tags panel for review. Missing: mixed-content and field autotag, heading/list/table/figure classification, Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA
Done since: CLI info, text and render use dynamic XFA layout pages, with synthetic count/text/pixel and copy-permission regressions (#1645). | +| 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). Locatable figure, annotation and form findings highlight their page bounds. Character encoding accepts Identity-H/V with Adobe CJK collections and predefined CJK CMaps independently (#3673); per-glyph validation and embedded-font extraction remain separate.
Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded text-only auto-tag proposal tool; text-only Apply writes marked content, paragraph tags and a parent tree, then opens the Tags panel for review. Missing: mixed-content and field autotag, heading/list/table/figure classification, Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA
Done since: CLI info, text and render use dynamic XFA layout pages, with synthetic count/text/pixel and copy-permission regressions (#1645). | | 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
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, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Italian has 2,528 entries: every key of the other catalogs that the code looks up. 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). Lithuanian adds a 2,578-entry catalog and date names, reviewed by a native-speaking contributor. Validated candidate (macOS raster probe; Windows runtime unverified): the Windows policy changes only embedded JetBrains Mono; horizontal alpha-edge energy per ink improves 2.78%, 3.87% and 1.59% at 100%, 125% and 150%, and is down 0.79% at 200%. Inter is pixel-identical; layout hashes, glyph coverage and the 8 MiB atlas are unchanged. Timing and RSS ranges overlap, with no improvement claimed. Estimates unchanged. Candidate #2383 clamps GPU requests to supported limits while retaining the adapter texture extent; synthetic V3D/downlevel and Windows GPU-required tests pass, but Raspberry Pi startup is unverified. Estimates unchanged. 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 Linux/Unix UI fallback fonts also use Fontconfig directories, including Nix store paths, with bounded discovery and the existing candidate allowlist; relative includes and digit-prefixed config filtering have isolated regressions. | | 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, Brazilian Portuguese and Arabic (logical-order catalog put into display order; layout not yet mirrored); Czech covers the menus. Italian has 2,528 entries: every key of the other catalogs that the code looks up. 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). Lithuanian adds a 2,578-entry catalog and date names, reviewed by a native-speaking contributor. Validated candidate (macOS raster probe; Windows runtime unverified): the Windows policy changes only embedded JetBrains Mono; horizontal alpha-edge energy per ink improves 2.78%, 3.87% and 1.59% at 100%, 125% and 150%, and is down 0.79% at 200%. Inter is pixel-identical; layout hashes, glyph coverage and the 8 MiB atlas are unchanged. Timing and RSS ranges overlap, with no improvement claimed. Estimates unchanged. 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 XDG desktops use the configuration base for preferences with non-overwriting migration and original-data preservation (#1395). | @@ -200,6 +202,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st | Date | Change | Summary | |---|---|---| +| 2026-10-20 | minor | M12 #3673: Character encoding accepts Identity-H/V with Adobe CJK collections, or a predefined non-Identity CMap; unknown names and foreign registries do not establish a mapping route. Synthetic checker and read-only automation regressions passed after failing before the fix. Per-glyph validation and embedded-font extraction remain separate; estimates unchanged. | | 2026-10-18 | minor | M5, PR #1971: nested source `/Page` and `/Pages` dictionaries export as null without traversing page content or the page tree. Synthetic saved/reopened three-page regression retains unrelated metadata and aliases. FDF import semantics, copied-graph limits and estimates unchanged. | | 2026-10-15 | minor | M5: budget retained FDF entries before cloning; discovery keeps lazy array owners and annotation positions, with alias/cache-eviction stress fixtures. Limits cover the copied graph; source parsing/page discovery retain existing COS/parser limits, with no total source-memory bound. FDF import semantics and estimates unchanged. | | 2026-10-15 | minor | M7: Edit text caret height follows its row, with a kittest regression; estimates unchanged | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9119063e20..39dc6be519 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,5 +1,7 @@ # PdfCraft parity with Adobe Acrobat Pro +> **Last reviewed:** 2026-10-20 · **Last updated:** 2026-10-20 · **Change:** minor (M12 #3673: Character encoding CJK mapping routes corrected; scores and estimates unchanged) + > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (localization ≈59% → ≈68%; weighted readiness 49.25 → 49.7 ≈ 50%) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) The authoritative assessment of how close PdfCraft is to Acrobat Pro. [ROADMAP.md](../ROADMAP.md) @@ -292,6 +294,7 @@ licensed OCR or AI model. | Date | Change | Summary | |---|---|---| +| 2026-10-20 | minor | M12 #3673: the Character encoding rule accepts Identity-H/V fonts with Adobe CJK collections, or a predefined non-Identity CMap, while rejecting unknown names and foreign registries. Synthetic checker and read-only automation regressions passed after failing before the fix; per-glyph mapping validation and embedded-font extraction remain separate. Breadth, readiness and hours unchanged. | | 2026-10-12 | minor | Exclusive image crops resize with their pixels across nested states, intervening operators and content streams; shared clips remain fixed. Synthetic engine/tool regressions and private image save/reopen checks; estimates unchanged. | | 2026-10-11 | minor | Web printing shipped (gap 4): Hardware, Platforms and L Print text updated. Text only: scores, readiness and hours not re-judged | | 2026-10-11 | minor | Contributor fork: document-font selection and subset completion, CFF, style ranges, contextual/bidi save/reopen and all-page Chinese keyboard acceptance; curve/hole hit testing (shared clips stay fixed, as on main). Scope remains bounded; percentages unchanged | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 7aca7d5c8b..15e1d7c57c 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -8600,8 +8600,8 @@ milestone = "M12" status = "shipped" commands = ["a11y.check"] tools = ["accessibility_check"] -evidence = ["crates/a11y/src/tests.rs::scans_identity_fonts_and_long_documents"] -notes = "rule character-encoding: ToUnicode, a standard encoding, or a predefined CJK collection; each font once" +evidence = ["crates/a11y/src/tests.rs::scans_identity_fonts_and_long_documents", "crates/a11y/src/tests.rs::character_encoding_accepts_identity_cjk_collections", "crates/a11y/src/tests.rs::character_encoding_accepts_predefined_cmaps_without_a_known_collection", "crates/a11y/src/tests.rs::character_encoding_rejects_unknown_cmaps_and_foreign_collections", "crates/a11y/src/tests.rs::character_encoding_resolves_indirect_cid_metadata", "crates/automation/tests/automation.rs::accessibility_character_encoding_accepts_identity_cjk_through_tools"] +notes = "rule character-encoding: ToUnicode, a standard encoding, a predefined non-Identity CJK CMap, or Adobe GB1/CNS1/Japan1/Korea1/KR; Identity-H/V accepted for those collections (#3673), indirect metadata resolved and unknown CMaps/foreign registries refused; each font once. Font-level declarations only: ToUnicode contents and every used glyph are not validated; embedded-font extraction gaps remain separate." [[feature]] id = "a11y.rule-tagged-multimedia"