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).
-