Draw a black rectangle over a name in a PDF, export the file, then reopen it with pdf.js and call getTextContent(). The name comes back. Word for word, character for character.
That was the first thing I ran into when I started building a redaction tool that runs entirely in the browser. A black rectangle is a drawing instruction. It sits on a layer above the page. The text object underneath does not know or care that something was painted over it.
Most tools in this space deal with that in one of two ways. Some rewrite the content stream and genuinely remove the text. Some render the page to an image and throw the text layer away. Both approaches work. The problem is what happens after: the tool says "done", and you have no way to know which of the two just happened, or whether it actually worked on the region you marked.
Three different zeros
If you build a checker that counts how many characters can still be copied out of the area a user marked, you end up with three situations that all report zero:
- The box missed. You drew over whitespace. There is no text there, so zero characters is the correct answer to a question nobody meant to ask.
- The page is a scan. There is no text layer at all. Zero characters is correct, but it tells you nothing about whether the black box covers what you think it covers.
- It worked. The text is gone.
Two of those zeros mean "there was nothing to delete". One means "deleted". A tool that only ever reports zero cannot tell you which one you are looking at, and the difference matters a lot: in case 2 you are trusting a rectangle you drew by eye, over an image you cannot read the text of.
So the check has to say which zero it is, out loud.
How to count characters inside a rectangle
pdf.js gives you the text items for a page, but the coordinates are in PDF space, not canvas space. Text position lives in item.transform, a six-value matrix, and the useful parts are the translation values. Util.transform converts them into the viewport coordinate system you drew the box in.
import { Util } from 'pdfjs-dist'; const page = await doc.getPage(pageNumber); const viewport = page.getViewport({ scale: 1 }); const content = await page.getTextContent(); let charsInBox = 0; for (const item of content.items) { if (!item.str) continue; const tx = Util.transform(viewport.transform, item.transform); const x = tx[4]; const y = tx[5]; const w = item.width * Math.abs(tx[0]); const h = item.height * Math.abs(tx[3]); // box is { x, y, w, h } in the same top-down viewport space if (x < box.x + box.w && x + w > box.x && y < box.y + box.h && y + h > box.y) { charsInBox += item.str.length; } } That is the whole idea. Everything else is bookkeeping you have to add around it: which page, which box, how many boxes hit something, and whether the page has any text at all.
I made the check a button instead of an automatic step
Upload, redact, check, report. That flow would have been smoother, and I did not build it.
The check is a button the user presses on purpose. My reasoning: if the tool checks silently, people read the result as "the tool says it is fine" and move on. If someone presses a button and gets back 50 characters can still be copied out of 1 marked area., they have to decide what to do about that number. It belongs to them, not to a step they did not notice running.
The same logic applies to the export. After flattening, the tool reopens its own output and counts again. It does not say "done" and stop there. It says 210 extractable characters remain, on 2 pages. or 0 extractable characters across 3 pages. Those are different sentences because they are different outcomes.
The cost of making text stop being copyable
In a browser there is only one reliable way to make text stop being extractable: make the page stop being text.
If the text object exists in the content stream, pdf.js will find it. So will any other PDF reader, and so will anyone who opens the file with a text editor and looks at the objects. Painting over it does not remove it.
So the redaction path is: render the marked page at 2x scale, fill the boxes with black, encode the result as JPEG at quality 0.9, then embed that image as a full-page content stream on a fresh page. Pages you did not mark are copied through unchanged.
The cost of that is real, and it is not small:
- That page is no longer searchable.
- Nobody can select or extract text from it.
- File size goes up, because a JPEG covering a whole page is usually larger than the text it replaced.
- If the page had selectable text that other people needed, that text is gone now.
I put that on the page instead of in a footnote. A redaction tool that hides the tradeoff is asking to be trusted about the wrong thing.
Numbers from my own test files
Concrete results, not marketing claims.
- A 3-page test file, one page marked: the export turned that one page into an image and copied the other two through unchanged. Re-checking the output found
210 extractable characters remain, on 2 pages.That is the honest outcome. I redacted page 1, pages 2 and 3 still have their text, and the checker says so rather than reporting zero. - The same file with all 3 pages marked:
0 extractable characters across 3 pages. - A 2-page scan with no text layer at all: pressing the check before exporting told me the page had no text layer. It did not report zero characters and leave me guessing.
- A box drawn over empty whitespace: the check said the box missed.
- A box over a line containing a fake name and a fake SSN:
50 characters can still be copied out of 1 marked area.
That last number is the one I keep coming back to. Fifty characters is a short line. If a tool had painted a black bar over it and told me the redaction was complete, I would have believed it.
What the other tools do
I looked at the direct competitors before writing any of this. Several are in a similar position: client-side, free, no upload, redaction that happens in the tab.
The one closest to identical in stack and pitch — same library pair, same "no upload" claim, showing up in search results for these queries — had a toolbar with Open PDF, Undo, Clear All, Prev, Next, Download, and one status message on save: Saved as ... — text layer permanently destroyed.
No character count. No re-check of the output. No mention of what happens on a scanned page.
I do not read that as dishonesty. I read it as an unexamined assumption: a box was drawn, so the text must be gone. The assumption holds in most cases and quietly fails in exactly the three cases at the top of this post.
What the check will not tell you
It counts characters that can still be copied out of the regions you marked. It does not tell you that a redaction satisfies any legal, medical, or regulatory standard, and it cannot. What number is low enough for your situation depends on your situation, your counterparty, and possibly your lawyer. A browser tab is not the place to answer that.
If the number is zero and the page has no text layer, the tool will say the page has no text layer. That is the point: you should be able to tell "nothing left to copy" apart from "nothing there to begin with".
The tool is at privacypdf.org/redact-pdf. It runs in the tab, nothing is uploaded, and there is no account. If you try it, start with the check button: draw a box over something you know is in the file, press Check, and look at the number before you export anything.
