From 1d412ff3ee7dffec86c3e5301c711bdfc0dbd7d1 Mon Sep 17 00:00:00 2001 From: theodubus <80580619+theodubus@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:57:07 +0200 Subject: [PATCH 1/2] add demo --- README.md | 6 ++ docs/demo-invoice.pdf | Bin 0 -> 17960 bytes frontend/src/components/PdfViewer.tsx | 16 +++- scripts/.gitkeep | 0 scripts/make_demo_pdf.py | 110 ++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 docs/demo-invoice.pdf delete mode 100644 scripts/.gitkeep create mode 100644 scripts/make_demo_pdf.py diff --git a/README.md b/README.md index 0438138..162834d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ blocked with a structured failure report. For the security model and limitations, see [docs/SECURITY.md](docs/SECURITY.md). +

+ +

+ --- ## Requirements diff --git a/docs/demo-invoice.pdf b/docs/demo-invoice.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d1478f0f1ce7d02272344663cdc56eb2accddeb4 GIT binary patch literal 17960 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/PdfViewer.tsx b/frontend/src/components/PdfViewer.tsx index 9867a36..90eca21 100644 --- a/frontend/src/components/PdfViewer.tsx +++ b/frontend/src/components/PdfViewer.tsx @@ -651,13 +651,23 @@ function highlightPhonePresetMatches( const phoneRegex = new RegExp(PHONE_PRESET_REGEX.source, PHONE_PRESET_REGEX.flags); for (const match of fullText.matchAll(phoneRegex)) { if (typeof match.index !== "number") continue; - const value = match[0] ?? ""; + let value = match[0] ?? ""; if (!value) continue; - if (!isLikelyPhonePresetMatch(value)) continue; - const matchStart = match.index; + let matchStart = match.index; const matchEnd = match.index + value.length; + // The phone regex's leading \b can't capture a preceding "+" (e.g. "+33 …"), + // so the match looks national and gets rejected below. Re-attach an + // immediately-preceding "+" so the preview matches what the backend redacts. + const intlPrefix = /\+\s*$/.exec(fullText.slice(0, matchStart)); + if (intlPrefix) { + matchStart -= intlPrefix[0].length; + value = "+" + value; + } + + if (!isLikelyPhonePresetMatch(value)) continue; + for (const item of nodes) { const localStart = Math.max(matchStart, item.start) - item.start; const localEnd = Math.min(matchEnd, item.end) - item.start; diff --git a/scripts/.gitkeep b/scripts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/make_demo_pdf.py b/scripts/make_demo_pdf.py new file mode 100644 index 0000000..4774e2e --- /dev/null +++ b/scripts/make_demo_pdf.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Generate a synthetic demo PDF for the README showcase. + +A fake invoice with obviously-fake PII (``@example.com``, test card ``4111...``), +a vector logo (graphics-removal demo) and an embedded bitmap "scan" whose text +is NOT selectable (manual-rectangle + 'pixels'/'Precise' mode demo). No real data. + +Run: python scripts/make_demo_pdf.py +Out: docs/demo-invoice.pdf +""" +from __future__ import annotations + +from pathlib import Path + +import pymupdf + +OUT = Path(__file__).resolve().parent.parent / "docs" / "demo-invoice.pdf" + +BLACK = (0, 0, 0) +GREY = (0.35, 0.35, 0.35) +BLUE = (0.16, 0.32, 0.62) + + +def build_scan_bitmap() -> bytes: + """A small raster image with baked-in text (simulates a scanned ID card).""" + scan = pymupdf.open() + p = scan.new_page(width=320, height=120) + p.draw_rect(p.rect, color=BLACK, fill=(0.96, 0.95, 0.90)) + p.insert_text((14, 30), "SCANNED ID (bitmap)", fontsize=12, color=BLACK) + p.insert_text((14, 58), "Name: Jean Dupont", fontsize=12, color=BLACK) + p.insert_text((14, 84), "ID No: FR-9988-7766", fontsize=12, color=BLACK) + png = p.get_pixmap(dpi=150).tobytes("png") + scan.close() + return png + + +def main() -> None: + doc = pymupdf.open() + page = doc.new_page(width=595, height=842) # A4 portrait + + # Vector logo dot -> demonstrates graphics removal when a rect touches it. + page.draw_circle((520, 60), 16, color=BLUE, fill=BLUE) + + # Header (non-sensitive). + page.insert_text((50, 66), "ACME Analytics SARL", fontsize=22, color=BLUE) + page.insert_text((50, 92), "INVOICE #2026-0042 Date: 2026-06-09", fontsize=11, color=GREY) + + # Bill-to block (sensitive -> text selection / email & phone presets). + y = 150 + for line in [ + "Bill to: Jean Dupont", + "Email: jean.dupont@example.com", + "Phone: +33 6 12 34 56 78", + "Address: 12 Rue de la Paix, 75002 Paris", + ]: + page.insert_text((50, y), line, fontsize=12, color=BLACK) + y += 24 + + # Payment (sensitive -> credit-card preset, Luhn-valid test number). + y += 12 + for line in [ + "Card on file: 4111 1111 1111 1111", + "Client ref: CUST-4521", + ]: + page.insert_text((50, y), line, fontsize=12, color=BLACK) + y += 24 + + # Line items (non-sensitive -> must REMAIN after redaction). + y += 24 + page.insert_text((50, y), "Items", fontsize=13, color=BLUE) + y += 22 + for line in [ + "Consulting services ............ 1 200,00 EUR", + "Data audit ..................... 800,00 EUR", + "Total .......................... 2 000,00 EUR", + ]: + page.insert_text((60, y), line, fontsize=11, color=BLACK) + y += 20 + + # Embedded bitmap "scan" -> the 'pixels' / 'Precise' mode showcase. + page.insert_image(pymupdf.Rect(50, 600, 370, 720), stream=build_scan_bitmap()) + page.insert_text( + (50, 736), + "Fig. 1 - scanned ID. The text above is a BITMAP (not selectable): use a", + fontsize=8, + color=GREY, + ) + page.insert_text( + (50, 748), + "manual rectangle + 'Precise' image mode to actually erase those pixels.", + fontsize=8, + color=GREY, + ) + + # Footer (non-sensitive). + page.insert_text( + (50, 800), + "Thank you for your business. ACME Analytics SARL - demo document, fake data.", + fontsize=9, + color=GREY, + ) + + OUT.parent.mkdir(parents=True, exist_ok=True) + doc.save(str(OUT), garbage=4, deflate=True) + doc.close() + print(f"wrote {OUT}") + + +if __name__ == "__main__": + main() From 6eedabf2cbe6d732698014fdbaca1fc25039703f Mon Sep 17 00:00:00 2001 From: theodubus <80580619+theodubus@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:01:04 +0200 Subject: [PATCH 2/2] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 162834d..037f231 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ blocked with a structured failure report. For the security model and limitations, see [docs/SECURITY.md](docs/SECURITY.md).

-