In-browser redaction desk. Open a PDF or image, review the marks Blackout proposes, draw boxes over anything it missed, then export a copy with the black bars burned into the pixels.
People paste offer letters into LLM chats for negotiation advice and forward contracts with every name and number intact, because redacting properly meant a paid desktop tool or a site that uploads the file. Blackout tries to make the safe path the quick one: mark, review, burn, under a minute, nothing leaving the tab.
A deployed copy runs at blackout-redaction.netlify.app. "Checkout the demo" loads a fake contract if you have nothing at hand. Privacy notes and terms sit at /legal.html.
The document stays in the tab. netlify.toml sends connect-src 'self', so
the page cannot open a request to another origin even if a dependency tries.
Netlify serves static HTML, JavaScript, CSS, and WASM; no function receives
the file.
Plenty of tools redact PDFs, and several also run in the browser. Blackout differs on four points:
- The no-upload claim is enforced, not promised. The Content-Security-Policy
header (
connect-src 'self') makes the browser refuse requests to any other origin, and anyone can watch the Network panel to check. Client-side competitors ask you to take their word for it. - Bars are checked against pixels. Placement from font metrics drifts, so the export renders each page and re-checks every bar against the ink it is about to cover, growing it until the surrounding paper is blank. Tools that rasterize without checking can ship a bar sitting a few points off its text.
- Detection is structured, not a keyword search. Luhn for card numbers, mod-97 for IBANs, invalid-area checks for SSNs, label pairing for IDs and birth dates, with a review ledger, burn preview, and word-snapped drawing on top.
- The whole thing is MIT-licensed with 80 tests. A privacy tool you cannot read is a trust exercise; this one you can audit, fork, and run offline.
Needs Node.js 18 or newer (20 LTS works). The same commands run on Windows and macOS.
git clone https://github.com/neerajmg/blackout.git
cd blackout
npm install
npm run devOpen the URL Vite prints, usually http://localhost:5173.
| Script | Runs |
|---|---|
npm run dev |
Local desk with hot reload. No backend. |
npm run build |
Hashed assets into dist/ |
npm run preview |
Serves dist/, the closest match to Netlify |
npm test |
80 tests in 15 files |
predev and prebuild copy the OCR engine into public/ocr (16 MB,
gitignored) by running scripts/copy-ocr-assets.mjs.
Chrome, Edge, or Firefox on Windows 10 or later, and macOS. Safari 16.4+
usually works. Burning needs OffscreenCanvas inside a worker.
| Kind | How it is found |
|---|---|
| Pattern | |
| Phone | 10 or more digits in a phone shape, including +44 and +91 forms |
| SSN | ###-##-####, plus a bare 9-digit run next to an SSN label, both with invalid-area checks |
| Card number | 13 to 19 digits passing Luhn, BIN 3 to 6 |
| IBAN | Structure and mod-97, any case |
| IPv4 | Dotted quad, skipped after version or build |
| Street address | Number plus street suffix (St, Ave, Rd, and the rest) |
| Internal label | INTERNAL USE ONLY, NOT FOR DISTRIBUTION, ATTORNEY-CLIENT, TRADE SECRET at any case; CONFIDENTIAL and PROPRIETARY only when stamped in upper or title case |
| Employee ID | EMP-12345, EID, labeled IDs |
| Passport ID | Labeled Passport: AB1234567 |
| Account number | Labeled account or routing digits |
| Name | Honorifics, and labeled Name: / Signed by: / Prepared by: |
| Date of birth | A date next to a DOB or born-on label |
Names, amounts, dates, places, and URLs sit behind the "Also mark" checkboxes
and stay off until you turn them on. Dates read 21-Jan-2025, 21/Jan/2025,
21 Jan 2025, Jan-21-2025, 2025-01-21, 21.01.2025, and the numeric
slash forms; a date next to a birth label is core and does not need the
checkbox.
Names drop a label that a table cell glued to the front, so a service-record
row reading Name Priya Raman marks the person and not the word Name. Job
titles are not names: Senior Director and Product Manager I are left for
you to cover by hand if the designation matters. Unlabeled surnames in running
prose are often missed; the canvas is there for those.
Widget values and note contents are not part of the page text pdf.js returns,
so Blackout reads them from getAnnotations() and treats each one as a single
block. A hit inside a form field marks the whole field rectangle, and the desk
reports how many fields the file has.
pdf.js reports a whole line as one run with one width. Splitting that width evenly per character puts the box for a value several points right of its ink, because a capital M is wider than the average character. Blackout measures each word with the page's own font when a canvas is available, and with Helvetica advance widths otherwise.
Metrics alone still drift under kerning and font substitution, so the parse and export workers render the page and then grow every bar outward, up to 6 points per side, for as long as the column immediately outside it still holds ink. A bar ends on blank paper. Marks you draw and marks the detectors propose go through the same pass, and the check runs again at export against the pixels each bar is about to cover.
- Open a file, or press "Checkout the demo".
- Automatic marks show as translucent bars with a type caption. "Burn preview" shows them solid, the way the export will look.
- Drag on a page to cover a miss. Over text, the box snaps to the words under the drag.
- Click a mark and press Delete to drop a false positive, or use the Delete button on its ledger row.
- "Mark this text" marks every word-bounded occurrence of a phrase across all pages.
- "Burn file" writes the new PDF. Exporting with zero marks asks for confirmation first.
- "New file" drops the session. State lives in RAM, and nothing is written to IndexedDB.
Keys: ← → or j k change page, Delete drops the selected mark, + - zoom, 0 returns to 100%, l toggles labels, o shows the original page.
public/samples/ holds three fake documents for trying the desk, and the
intake screen links them next to the demo button:
- sample-offer-letter.pdf — name, honorifics, DOB, SSN, salary figures, employee ID, labeled address.
- sample-invoice.pdf — IBAN, card number, account and routing digits, UK-format phone, amounts, dates.
- sample-scan.png — the same kind of content as a flat image with no text layer, so it goes through OCR.
Every person and number in them is invented; phones use the reserved
fictional ranges and the SSNs are published specimen numbers. Regenerate the
PDFs with node scripts/make-samples.mjs.
Each page is rendered at 144 DPI, capped at 2000 px on the long side, the bars
are painted on top, and the result is embedded as a JPEG. The source content
stream is not copied, so selectable text, attachments, URI links, JavaScript,
and form values from the original are absent from the output. Page size and
/Rotate carry over. Title, author, subject, and keywords are empty, and the
producer reads Blackout.
Reopening a burned file in Blackout finds zero automatic marks, because no text is left to scan.
Typed text comes from pdf.js in a worker. Pages with almost no extractable text
go to tesseract.js, which starts on its own when a file needs it. The worker
script, the WASM core, and the English training data are served from /ocr on
this origin after scripts/copy-ocr-assets.mjs copies them out of
node_modules, so OCR runs offline and contacts no CDN.
If OCR fails, the desk shows a line saying so and you draw those boxes yourself.
Measured in Chromium on an M-series Mac, over a generated 60-page file with 40 lines of PII per page:
| Step | Time |
|---|---|
| Parse, detect, and ink-snap 60 pages | 2.5 s |
| Burn 60 pages | 0.9 s |
| Jump to page 60 and paint | 0.1 s |
Detection and PDF parsing run in a Web Worker. The visible page renders first at screen scale with a capped device pixel ratio, and thumbnails paint at 96 px when they scroll into view. Session state stays in RAM, so closing the tab discards the file. The tesseract chunk is code-split and idle until a page needs it.
- Detection covers US formats. Aadhaar, SIN, driver licence, and MAC patterns are not in the table above.
- OCR marks what it can read. A handwritten note usually needs a hand-drawn box.
- Drawing assumes a pointer. Under 900 px the panels stack; drawing on a touch screen is untested.
- Encrypted PDFs are refused with a message asking you to unlock the file first.
- 40 MB per file. TIFF opens only where the browser can decode it; PNG, JPG, and PDF are the reliable paths.
Automatic detection is incomplete and is not a legal guarantee. Read every page before you share the file.
This is a static site. Do not add a Netlify Function that accepts PDFs. blackout-redaction.netlify.app is this repo deployed exactly this way.
npm run buildproducesdist/.- Drag and drop: Netlify dashboard, Sites, Add new site, Deploy manually, drop
the
distfolder. - From git: import the repo.
netlify.tomlalready sets the build command, the publish directory, the SPA redirect, long-cache headers for hashed assets, and the Content-Security-Policy.
After deploying, open DevTools, load a file, and confirm the Network panel shows no request leaving the origin.
npm test
npm run buildThree suites cover the parts that decide whether a bar hides its text:
tests/glyph-widths.test.jschecks word placement against pdf-lib font metrics.tests/ink-snap.test.jschecks bar growth against synthetic pixel maps.tests/annotations.test.jschecks that form-field text reaches the detectors.
MIT. See LICENSE.
