RetypePDF

ยท RetypePDF ยท 4 min read

How Text Replacement in a PDF Actually Works

Most "PDF editors" put a white box over the old words. Real replacement rewrites the drawing instructions inside the page. Here is the whole mechanism, end to end.

A page is a program

A PDF page is not a document with paragraphs. It is a list of drawing instructions โ€” a content stream. Here is a real one, decompressed:

BT /F1 11 Tf 60 720 Td (Acme Trading Ltd) Tj ET

Read it right to left of each operator:

TokenMeaning
BT โ€ฆ ETbegin / end a text block
/F1 11 Tfuse font F1 at 11 points
60 720 Tdmove the text cursor to x=60, y=720
(Acme Trading Ltd) Tjdraw this string, here

There are no words, no lines, no flow. Just "draw these bytes at this position". That is why a PDF looks identical everywhere โ€” and why editing it is not like editing text.

Step 1: get to the instructions

Content streams are compressed with deflate (/Filter /FlateDecode). The stream's dictionary says how many bytes; inflate them and you have the operators as plain bytes. Modern files add a twist: objects themselves can live inside compressed object streams (/Type /ObjStm), so sometimes you decompress a stream just to find the page that points at another stream.

Step 2: find the text

Two operators draw text. Tj takes one string. TJ takes an array of strings and numbers:

[(A) 74 (cme Trading Ltd)] TJ

The 74 pulls the next chunk left by 74 thousandths of the font size โ€” kerning. So one visual word can be several fragments. A replacer has to treat the array as one run of text, or "Acme" will never match anything.

Step 3: the bytes are not what they look like

This is the part everyone gets wrong. The bytes inside ( ) are not UTF-8. They are codes in the font's encoding.

So replacement is really: decode the string through the font's encoding, match against what the user typed, encode the new text back through the same encoding. Both directions, same table.

Step 4: the trap โ€” subset fonts

PDFs embed only the glyphs a document uses. Ten different letters on the page? Ten glyphs in the font. If the replacement contains a letter the original never used, the font has no drawing for it.

Write it anyway and you get the nastiest failure in this whole area: the page shows empty boxes, but copy-paste and search still return the right characters. The data is correct. The pixels lie. Every automated test that checks extracted text passes.

The fix is to check before writing. TrueType fonts carry their character map in the cmap table โ€” parse it (formats 0, 4, 6 and 12 cover what PDFs embed), build the set of characters the font can draw, and if the new text needs one that is missing: refuse, and name the missing characters. A refused edit costs the user ten seconds. A document that looks broken to its reader costs much more, later, to someone else. Longer write-up here.

Step 5: write, without pretending

Escape (, ) and \, splice the new string into the operator, done? Almost. Two honesty rules matter more than the splicing:

Text does not reflow. The instruction says "draw at x=60". A longer replacement draws longer โ€” to the right, past where the old text ended. Nothing moves to make room. Short, same-length replacements (a name, a date, a reference) behave perfectly; rewriting a paragraph does not. Say so instead of letting users find out.

Count matched and changed separately. Occurrences found and occurrences actually rewritten differ whenever an edit was refused. Report both numbers. A tool that only says "done" is hiding the interesting column.

Step 6: rebuild the file

The stream is now a different length, so its /Length is wrong, so the byte offsets of every object after it are wrong, so the cross-reference table that maps object numbers to offsets is wrong. The simplest correct move: re-deflate every stream, re-emit every object, write a fresh xref and trailer. Flatten object streams while you are at it. The output is a clean, classic PDF that any reader from any decade can open.

What this cannot do

The whole algorithm, in one paragraph

Inflate the content streams. Parse the text operators into runs, merging kerned fragments. Decode each run through its font's encoding. Match the user's text against the decoded runs. For each match, encode the replacement through the same encoding โ€” refusing if the subset font cannot draw it. Splice, re-deflate, rewrite offsets, emit a clean file. Then verify with a renderer that what changed is what you meant โ€” because in PDF, the data and the pixels can disagree in ways your tests will not see.

About 600 lines of dependency-free JavaScript, as it turns out. The editing is the small half. Knowing when to refuse is the rest.

RetypePDF edits the text inside a PDF in your browser. Nothing is uploaded. No account, no price.

Try it