From e373dffa208c862e029099285e8873a0b97d88c8 Mon Sep 17 00:00:00 2001 From: Kyle Kassen <85040622+KyleKassen@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:34:55 -0500 Subject: [PATCH] M8: redact labelled Italian international slash numbers --- ROADMAP.md | 5 ++- crates/automation/tests/it/automation.rs | 22 ++++++++++ crates/redact/README.md | 4 ++ crates/redact/src/patterns.rs | 56 ++++++++++++++++++++++-- docs/roadmap.md | 5 ++- parity/acrobat-features.toml | 4 +- 6 files changed, 87 insertions(+), 9 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5634d6bf2..129a963ec 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away -> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-22 · **Change:** minor (M14, #3558: rebase guarded macOS per-file association recovery onto current main; estimates unchanged) +> **Last reviewed:** 2026-10-23 | **Last updated:** 2026-10-23 | **Change:** minor (M8, #3205 follow-up: labelled Italian international phone numbers with a slash match whole; estimates unchanged) > **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-21 · **Change:** minor (M3, #3808: the Pages inspector has an independent 50–150% thumbnail size slider and buttons; selection and current-page following are retained, with headless ui.set coverage. Estimates unchanged.) > **Last reviewed:** 2026-10-20 | **Last updated:** 2026-10-20 | **Change:** minor (M11 #3507: image merging preserves content stream boundaries; estimates unchanged) > **Last reviewed:** 2026-10-20 | **Last updated:** 2026-10-20 | **Change:** minor (M10, #3532 partial: vector printing retains existing appearances with absent Subtype; estimates unchanged) @@ -191,6 +191,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-23 (M8, #3205 follow-up):** Phone-pattern redaction matches complete labelled Italian geographic numbers such as `Tel. +39 0426/123456`, with synthetic matcher and mark/apply/save/reopen coverage; unlabelled international slash numbers remain unsupported. + - **2026-10-22 (M4, #3916 partial):** Document edits and Undo/Redo cancel an Organize page drag. Pointer-release regressions check exact page order and save/reopen. - **2026-10-22 (M14, macOS association recovery candidate):** Successful desktop open/save/sign can queue recovery of an existing per-file binding to the running PDFCraft bundle through NSWorkspace. Metadata-only watcher changes retain undo and avoid false overwrite prompts; real same-length/same-mtime edits remain protected. Rebased on 0.12.0 and reuses #3042's shared atomic metadata preservation; the bounded recovery parser does not narrow save limits. Prior native Preview TEST-signature and Finder workflows passed on macOS 27.0.1; other versions remain unqualified. Estimates unchanged. - **2026-10-13 (M7, free image rotation):** A selected page image has a rotate knob (Shift: 15 degree steps) and `image_edit` a `rotate_by` action; the angle is written as one undoable `cm`. Snapping to page edges while moving is in #1740; resize snapping is not done. Estimates unchanged. @@ -640,6 +642,7 @@ Newest first: the date, what moved, and the new overall estimate. | Date | Change | Summary | |---|---|---| +| 2026-10-23 | minor | M8, #3205 follow-up: phone-pattern redaction includes the +39 country code on labelled Italian geographic numbers with a slash; dates, protocol identifiers and whole-run guards remain. Estimates unchanged. | | 2026-10-22 | minor | M4, #3916 partial: document edits and Undo/Redo cancel Organize drags. Pointer-release and save/reopen regressions check page order. Estimates unchanged. | | 2026-10-20 | minor | M4: initial-view window and interface options resolve indirect boolean ViewerPreferences values; synthetic true/false and save/reopen regressions retain defaults for malformed entries. Estimates unchanged. | | 2026-10-20 | minor | M6, #3502: form detection preserves comments across content streams; estimates unchanged. | diff --git a/crates/automation/tests/it/automation.rs b/crates/automation/tests/it/automation.rs index 9fd5c4269..535dfdc36 100644 --- a/crates/automation/tests/it/automation.rs +++ b/crates/automation/tests/it/automation.rs @@ -3942,6 +3942,28 @@ fn redacting_through_tools() { assert!(matches!(a.call("redact_mark", &json!({ "doc": doc })), Err(ToolError::InvalidArgs(_)))); } +#[test] +/// Mark, apply and save remove the complete international number while retaining labels and controls. +fn redacting_italian_international_slash_numbers_through_tools() { + let dir = workdir("redact-italian-international-slash"); + std::fs::write(dir.join("phones.txt"), "A tel. +39 0426/123456\nB tel. 0426/123456\nC protocol 2024/123456\nD on 01/10/2026").unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_create", json!({ "from": "text", "path": "phones.txt" }))["doc"].as_u64().unwrap(); + let marked = ok(&mut a, "redact_mark", json!({ "doc": doc, "pattern": "phone" })); + assert_eq!(marked["marked"], 2, "both complete phone numbers need marks"); + assert!(page_text(&mut a, doc)[0].contains("+39 0426/123456"), "marks alone remove nothing"); + assert_eq!(ok(&mut a, "redact_apply", json!({ "doc": doc }))["applied"], 2); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "redacted.pdf" })); + let reopened = ok(&mut a, "doc_open", json!({ "path": "redacted.pdf" }))["doc"].as_u64().unwrap(); + let text = &page_text(&mut a, reopened)[0]; + assert!(!text.contains("+39") && !text.contains("0426"), "{text}"); + for label in ["A tel.", "B tel."] { + assert!(text.lines().any(|line| line.trim() == label), "only the surrounding label remains: {text}"); + } + assert!(text.contains("protocol 2024/123456") && text.contains("on 01/10/2026"), "unrelated digit runs remain: {text}"); + std::fs::remove_dir_all(dir).unwrap(); +} + /// One page with the text "Secret 1234" and a Redact annotation that has neither a usable /// QuadPoints array nor a Rect, so there is no area to redact. fn redact_pdf_with_unreadable_mark() -> Vec { diff --git a/crates/redact/README.md b/crates/redact/README.md index cca3d51fd..34c9f4fe1 100644 --- a/crates/redact/README.md +++ b/crates/redact/README.md @@ -89,6 +89,10 @@ read in full is refused, not cleaned in part. Both make the next save a full rew `patterns` finds phone numbers, e-mail addresses, credit card numbers (Luhn-checked), US Social Security numbers and dates for Search & Redact. +A phone label such as `Tel.` or `Fax:` also enables Italian international geographic numbers with a slash, such as `Tel. +39 0426/123456` or `Fax: +390426/123456` (#3205 follow-up). +The country code and national number are matched together, with the significant initial `0` and 6 to 11 national digits described in the [AGCOM numbering plan published by ITU](https://www.itu.int/dms_pub/itu-t/oth/02/02/t020200006b0001pdfe.pdf). +This narrow case accepts at most one space after `+39` and one slash immediately after the area code; international slash numbers without a phone label, or with country codes other than +39, remain unsupported. +Patterns are heuristic: dates, protocol identifiers and longer number runs are retained, and users still need to review the marks before applying them. `codes` holds the redaction code sets (U.S. FOIA and U.S. Privacy Act exemptions): `CodeSet::from_id("foia")?.overlay(&["(b)(6)"])` gives the overlay text for the picked codes. diff --git a/crates/redact/src/patterns.rs b/crates/redact/src/patterns.rs index f707f179a..7f910f8de 100644 --- a/crates/redact/src/patterns.rs +++ b/crates/redact/src/patterns.rs @@ -158,15 +158,30 @@ fn phone(s: &[char], i: usize) -> Option { if seps == 0 && !labelled(s, i) || digit(s.get(end)) || run_continues(s, end.checked_sub(1)?, 1, &SEPS) { return None; } - // Only an area code with a trunk 0 takes a slash, so "2024/123456" is no phone number; and - // only one, right after it ("0426/123456", not "0426/12/3456" or "0426 12/3456"). let slash = run.contains(&'/'); - if run.iter().filter(|c| **c == '/').count() > 1 || slash && !run.iter().take_while(|c| **c != '/').all(char::is_ascii_digit) { + // Italian geographic numbers retain their initial 0 after +39. For a labelled number, + // apply the national slash rule after that country code and an optional separating space. + // AGCOM's E.164 plan gives geographic numbers 6 to 11 national digits (#3205). + let slash_run = if plus && slash { + if !labelled(s, i) || !(8..=13).contains(&n) { + return None; + } + let national = run.strip_prefix(&['3', '9'])?; + national.strip_prefix(&[' ']).unwrap_or(national) + } else { + run + }; + // Only an area code beginning with 0 takes a slash, so "2024/123456" is no phone number; + // and only one, right after it (not "0426/12/3456" or "0426 12/3456"). + if run.iter().filter(|c| **c == '/').count() > 1 + || slash + && (slash_run.first() != Some(&'0') || !digit(slash_run.get(1)) || !slash_run.iter().take_while(|c| **c != '/').all(char::is_ascii_digit)) + { return None; } let ok = if plus { // E.164: a `+` country code and up to 15 digits in all ("+49 2151 123456"). - !slash && (8..=15).contains(&n) + (8..=15).contains(&n) } else { // North American: 10 digits, 11 with a country code ("(555) 123-4567"). let nanp = (10..=11).contains(&n) && !slash; @@ -505,6 +520,39 @@ mod tests { ); } + #[test] + /// Labelled Italian geographic numbers match completely while dates and other digit runs stay unmarked. + fn labelled_italian_international_slash_numbers_match_whole() { + for (text, number) in [ + ("tel. +39 0426/123456", "+39 0426/123456"), + ("Fax: +390426/123456", "+390426/123456"), + ("Telefono +39 02/12345678", "+39 02/12345678"), + ("tel. +39 02/1234", "+39 02/1234"), + ("tel. +39 0426/1234567", "+39 0426/1234567"), + ] { + assert_eq!(found(Pattern::Phone, text), [number], "{text}"); + } + for text in [ + "+39 0426/123456", + "tel. +39 2024/123456", + "tel. +39 0/123456789", + "tel. +39 0426/12/3456", + "tel. +39 0426 12/3456", + "tel. +39 0426/ 123456", + "tel. +39 02/123", + "tel. +39 0426/12345678", + "tel. +39 0426/1234567890", + "tel. +39 0426/123456", + "tel. +39-0426/123456", + "tel. +39 -0426/123456", + "tel. +40 0426/123456", + "protocol 2024/123456", + "on 01/10/2026", + ] { + assert!(found(Pattern::Phone, text).is_empty(), "{text}"); + } + } + #[test] fn the_tail_of_a_longer_number_is_not_a_phone_number() { // #1884: the last groups of a French social security number, an IBAN and a SIRET used diff --git a/docs/roadmap.md b/docs/roadmap.md index bb542aefc..2d1a563cb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # PdfCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-22 · **Change:** minor (M14, #3558: rebase guarded macOS per-file association recovery onto current main; estimates unchanged) +> **Last reviewed:** 2026-10-23 | **Last updated:** 2026-10-23 | **Change:** minor (M8, #3205 follow-up: labelled Italian international phone numbers with a slash match whole; estimates unchanged) > **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-20 | **Last updated:** 2026-10-20 | **Change:** minor (M10, #3532 partial: vector printing retains existing appearances with absent Subtype; estimates unchanged) @@ -182,7 +182,7 @@ total matches the parity estimate. | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Existing simple field-font encodings and Differences are respected when generating appearances; an unencodable value uses a separate Helvetica appearance font without changing the source font. Synthetic reopen regressions cover punctuation, subset fallback and multiline text. Exact font metrics/layout and broader Unicode fallback remain incomplete. | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done; XFDF values retain text across XML comments and processing instructions.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA
Done since: Tab and Shift-Tab wrapping to the same field or another widget of that field refresh the newly opened editor from the accepted value before Save. Synthetic UI regressions cover both directions, same-frame Save, validation refusal and undo/redo. Renaming a field renames it in the calculations that read it (field lists, notation, literal `getField` names), in one undo step, and says how many changed. Prepare a form ▸ Calculation order, a page of its own: each calculated field's formula and value, what is wrong (fields that calculate too early, loops, fields missing from `/CO`) and Fix order, Up/Down, Alt+↑/↓ and drag, and on the page each field's step and arrows to and from the field in focus (`form_calculation_order` headlessly). | | M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Missing: editing existing text and images in place (the longest pole), image/PDF watermarks | -| M8 | Security + redaction | 100–180 | 68% | 15–30 | Done: opening protected documents, permissions, Protect Using Password, Remove security; redaction (mark text/areas/pages, Search & Redact with patterns, apply removing glyphs, image pixels, vectors, XObject content, annotations and fields, verification, full rewrite on save); Remove hidden information and Sanitize.
Done since: redaction codes (U.S. FOIA and Privacy Act sets as overlay text, in Redaction properties and `redact_mark`). Redaction cleans indirect and form structure references, preserving surviving form tags. Redaction retires inherited resource dictionaries, so a parent `/Pages` can't keep a removed XObject reachable; unredacted pages are left untouched. Image redaction removes alternate images and unsupported masks only on affected images. Changed marked-content spans lose replacement descriptions; untouched spans keep theirs. Hidden-text cleanup retires unused named descriptions across inherited/shared page and form resources (64 focused tests and scoped Rust 1.96 Clippy/wasm checks pass; publication CI pending). The redaction audit counts a filled cover that hides half of any one word of a long line (#2662), applied or not. Missing: certificate security, custom redaction code sets and pattern locales, DCT re-encoding | +| M8 | Security + redaction | 100–180 | 68% | 15–30 | Done: opening protected documents, permissions, Protect Using Password, Remove security; redaction (mark text/areas/pages, Search & Redact with patterns, apply removing glyphs, image pixels, vectors, XObject content, annotations and fields, verification, full rewrite on save); Remove hidden information and Sanitize.
Done since: redaction codes (U.S. FOIA and Privacy Act sets as overlay text, in Redaction properties and `redact_mark`). Redaction cleans indirect and form structure references, preserving surviving form tags. Redaction retires inherited resource dictionaries, so a parent `/Pages` can't keep a removed XObject reachable; unredacted pages are left untouched. Image redaction removes alternate images and unsupported masks only on affected images. Changed marked-content spans lose replacement descriptions; untouched spans keep theirs. Hidden-text cleanup retires unused named descriptions across inherited/shared page and form resources (64 focused tests and scoped Rust 1.96 Clippy/wasm checks pass; publication CI pending). The redaction audit counts a filled cover that hides half of any one word of a long line (#2662), applied or not. Phone-pattern redaction also matches labelled Italian geographic numbers with `+39` and a slash after the area code (#3205 follow-up); unlabelled international slash numbers remain unsupported. Missing: certificate security, custom redaction code sets and pattern locales, DCT re-encoding | | 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. #3532 partial: vector printing retains existing appearances with absent Subtype through a print-owned Form copy; source artwork stays unchanged. #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. Done since: dotted-leader contents entries stay in separate complete paragraphs (#1757) #2156 candidate: last selected print paper persists across sessions; other print preferences unchanged. | | 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). Content stream boundaries preserve comments, split operands and graphics state when image fragments merge. Done since: reject invalid image and soft-mask dimensions with checked sizing (#2178). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA | @@ -239,6 +239,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st | Date | Change | Summary | |---|---|---| +| 2026-10-23 | minor | M8, #3205 follow-up: phone-pattern redaction includes the +39 country code on labelled Italian geographic numbers with a slash; dates, protocol identifiers and whole-run guards remain. Estimates unchanged. | | 2026-10-22 | minor | M4, #3916 partial: edits and Undo/Redo cancel Organize page drags. Pointer-release and save/reopen regressions check page order. Estimates unchanged. | | 2026-10-22 | minor | M14 #3558: rebased macOS association recovery; reuse #3042 shared metadata preservation instead of a duplicate save wrapper. Estimates unchanged. | | 2026-10-20 | minor | M4: initial-view window and interface options resolve indirect boolean ViewerPreferences values; synthetic true/false and save/reopen regressions retain defaults for malformed entries. Estimates unchanged. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 4ec39848c..7473f166a 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5637,8 +5637,8 @@ milestone = "M8" status = "shipped" commands = ["redact.search"] tools = ["redact_mark"] -evidence = ["crates/redact/src/patterns.rs::phone_numbers", "crates/redact/src/patterns.rs::slashed_and_labelled_unseparated_phone_numbers", "crates/redact/src/patterns.rs::credit_cards_need_a_valid_checksum", "crates/automation/tests/it/automation.rs::redacting_through_tools"] -notes = "phone (North American, E.164 with + country code, national with trunk 0, a slash after a trunk-0 area code; numbers without separators only after a phone label such as Tel. or cell.), e-mail, credit card (Luhn), SSN, dates (English US forms); text drawn in a font whose built-in encoding we hold no table for (Symbol, ZapfDingbats, MacExpert) has no Unicode mapping, so a pattern search cannot see it -- it is skipped rather than mis-decoded" +evidence = ["crates/redact/src/patterns.rs::labelled_italian_international_slash_numbers_match_whole", "crates/automation/tests/it/automation.rs::redacting_italian_international_slash_numbers_through_tools", "crates/redact/src/patterns.rs::phone_numbers", "crates/redact/src/patterns.rs::slashed_and_labelled_unseparated_phone_numbers", "crates/redact/src/patterns.rs::credit_cards_need_a_valid_checksum", "crates/automation/tests/it/automation.rs::redacting_through_tools"] +notes = "phone (North American, E.164 with + country code, national with trunk 0, a slash after a trunk-0 area code; labelled Italian +39 geographic numbers with one slash after the area code match with their complete country code (#3205 follow-up), with or without one space after +39; international slash numbers without a phone label, or with country codes other than +39, remain unsupported; numbers without separators only after a phone label such as Tel. or cell.), e-mail, credit card (Luhn), SSN, dates (English US forms); text drawn in a font whose built-in encoding we hold no table for (Symbol, ZapfDingbats, MacExpert) has no Unicode mapping, so a pattern search cannot see it -- it is skipped rather than mis-decoded" [[feature]] id = "protect.redact-pattern-locales"