DEV Community

Vikram Bhattacharjee
Vikram Bhattacharjee

Posted on

Why editing one word in a PDF is so much harder than it looks

I spent the last few months building a PDF editor. The feature that sounded easiest — change a word, leave everything else alone — turned out to be the one that ate most of the time. Here is what I learned, mostly by getting it wrong.

A PDF does not contain sentences
The first assumption to give up is that a PDF stores text. It stores drawing instructions. A line of text is typically a sequence like this:

BT
/F1 11 Tf
72 700 Td
[(W) -30 (e) 15 (l) -20 (come)] TJ
ET
Enter fullscreen mode Exit fullscreen mode

That TJ array is one visual word split into fragments with kerning adjustments between them. There is no string "Welcome" anywhere in the file. There is no paragraph, no line, and no notion that these glyphs belong together. Everything a human sees as structure is inferred by whatever is reading the file.

So "find and replace" is not a string operation. It is: reconstruct the logical text from positioned glyphs, locate your target inside it, work out which drawing operations produced those particular glyphs, remove them, and draw something new in a way that looks like it was always there.

Replacing text means recovering four things
To make a replacement invisible you need the original font, size, baseline, and colour. None is reliably available.

Size is not simply the number after Tf. The text matrix can scale it. A heading declared as Tf 1 inside a matrix scaled by 24 renders at 24pt. Read the Tf value alone and you get 1.

Baseline matters more than people expect. A couple of points too high and the eye catches it instantly, even when the font and size are perfect.

Colour requires care in scanned documents. Sample one page-wide background colour and you will eventually paint white blocks onto cream paper.

I ended up estimating these from the surrounding line — a weighted median of the size, font and baseline of neighbouring runs — and using the estimate only when the exact run cannot be matched. That fallback path is where nearly every bug lived.

The bug that taught me the most
Replacements on academic papers came out in the wrong typeface: a light sans-serif dropped into a serif document. Obvious to any reader, and I could not reproduce it.

I built test PDFs that I was sure matched the failing case: embedded fonts, subsetted, bold headings. They all passed. I "fixed" the bug three times against those files. It kept failing on real documents.

Eventually I stopped generating test files and downloaded a real paper from arXiv. It failed immediately, every time. The cause was one line:

let serif = low.contains("times") || low.contains("roman") || ...
LaTeX embeds a Times clone named NimbusRomNo9L. It contains Rom. It does not contain roman. So the font was classified as sans-serif and replaced with Helvetica. Computer Modern has the same problem: CMR10 and CMBX12 never contain the word "roman" either.

The same naming assumption broke weight detection. Nimbus calls its bold weight -Medi, not -Bold, so headings also came out at regular weight.

Two string comparisons, and they only showed up on documents my generated corpus could not produce. My test files were written by a library that emits clean TrueType with tidy names. Real documents come from LaTeX, from Word, from twenty-year-old scanners, and they are full of abbreviations nobody documents.

Subset fonts, and why fallback is unavoidable
A PDF usually embeds only a subset of each font — just the glyphs already used on the page. That is why file sizes stay sane, and it is also a trap. If the original text never contained a z, the embedded font has no z. Replace a word with one that needs it and you cannot draw with the document's own font at all.

So you always need a fallback, and the fallback needs to look close. That means having real font families installed on the server. Ship a container with only one font family and every fallback lands on it, whatever the document looked like.

What actually helped
Start from text that already exists. Letting someone draw a box and drop text into it means guessing font, size and baseline from a rectangle. Detecting the text runs first and letting them pick one means those values are known rather than inferred. Same interface, far fewer guesses.

Test on documents you did not create. Generated test files share your assumptions, which is exactly what makes them useless for finding this class of bug. One real arXiv paper found more than a week of synthetic cases.

Measure the output, not the process. "Did the edit apply" is not the question. The question is whether the replacement is the right size, on the right baseline, and not overlapping its neighbours — which means rendering the result and comparing.

Try to break it
The editor is at marqpdf.com. Open a PDF and it outlines every line it finds; click one and type, or say what to change.

If you have a PDF that breaks it — an unusual font, a government form, a multi-column layout, something produced by software nobody has heard of — I would genuinely like to see it. Those are worth more than any test file I can write.

Top comments (2)

Collapse
 
reidmarlow profile image
Reid Marlow

This matches the bit that always surprises people when they first touch PDFs. A PDF edit is closer to patching a little graphics program than changing text in a document. The painful cases for me are ligatures and copied text order, because the visual result can look fine while extraction turns into junk.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.