Edward Lukowsky
All work

Desktop app 2026

PDF for Free

A desktop PDF editor that lets you click a line of text, retype it, and save a file where the replacement is set in the document's own font.

Open pdf-for-free.com
Stack
Python, PySide6 (Qt), PyMuPDF, OpenCV
Live
pdf-for-free.com
Source
github.com/eddaboss/PDFTextEditor
Status
Shipped, Mac and Windows

Why I built it

Editing text in a PDF usually costs money, and most free tools draw a white box over the old text and type the new text on top in Helvetica, which is easy to spot.

I wanted to click a line, retype it, and get a result that matches the rest of the document. Most of the work turned out to be figuring out which font the line is set in.

Finding the right font

For a born digital PDF, the font is embedded in the file and the app reuses the document's own font buffer. That works until the embedded font is subsetted, which is common: the file only carries the glyphs the document already uses, so typing a character that was not in the original produces an empty box.

So font lookup runs in three tiers. The on screen editor and the saved file use the same lookup, so what you see while typing is what gets saved.

  • Embedded. The document's own font, reused when it covers the new glyphs. Bold, italic, and regular faces of the same family on the same page stay distinct.
  • System same family. When the embedded subset is missing a glyph, a full system face of the same family is substituted rather than rendering an empty box.
  • Base 14. Last resort, for exotic CID and symbolic cases.

Scanned pages

Scanned documents carry no font information, just pixels. A scanned glyph is a few dozen pixels, blurred by the scanner, thickened or broken up by the fax path, sitting on paper texture. That is not enough information to recover which font the document was set in, and the app does not need to know.

It needs a face that looks the same as the scanned glyphs around it at that resolution. At scan quality many faces are interchangeable, so a match that names the wrong font but reproduces the page is a success, and one that names the right font but sits a little too bold in the line is a failure.

So the pipeline ends with a rebuild and compare step instead of a classifier. A coarse pass shortlists candidates, a super resolved glyph rerank narrows them, and then the verifier renders the actual scanned word in each surviving candidate and compares it glyph by glyph against the real pixels across nine metrics: blurred silhouette and edge detail as cosine similarities, plus aspect, ink fill, stroke width, contrast, roundness, vertical to horizontal edge energy, and advance width. Each metric has its own threshold, calibrated from labeled data.

Matching the scan damage

An early version kept picking decorative display faces like Redacted and Wavefont, fonts drawn to look broken and distressed, when it compared a degraded scan against cleanly rendered candidates.

The comparison was doing what it was built to do. A damaged scan looks more like a font designed to look damaged than like a clean rendering of its own font, because the scanner damage is the strongest signal in the image.

Now the app measures the page's scan damage as a residual profile and applies the same damage to each rendered candidate before comparing. With both sides equally degraded, the comparison picks the right font. I checked the change against the clean rerank in the calibration harness before keeping it.

The harness also reports exact top one accuracy, currently 32.5% on the full font bank. That shows how well the shortlist is ordered, but it does not say whether the output looks right, so the test below is the one that has to pass.

The acceptance test

It is a leave one glyph out test. For any scanned page, it runs the page through the real OCR pipeline, then regenerates each recognized character through the actual edit path and scores the synthetic glyph against the real scanned glyph it would have replaced, on ink density, ink strength, edge roughness, and structural similarity.

That measures what a user sees: whether a replaced character fits in with its untouched neighbors. It runs on every glyph of any page I give it, so it is not limited to a prepared test file.

Why the scores are ratios

Each metric is a ratio of synthetic to real. A fax at 200 DPI and a flatbed scan at 600 DPI have very different edge roughness, so a fixed threshold would only fit whichever document I tuned it on.

As a ratio, a good match scores near 1.0 on any font and any scanner, so the test still means something on documents I have never seen. The first version of this feature looked great on my test document and failed on the second one I tried.

Limitations

There is no paragraph reflow. Edits are per line, placed at the original position, so replacing a short line with a much longer one will overrun rather than rewrap.

When an edit reuses the original subsetted embedded font, the glyphs are drawn by glyph id with no ToUnicode map. The result looks correct but does not always copy, paste, or search as text. Edits that resolve to a system font or base 14 stay fully extractable. Attaching a ToUnicode CMap is the fix and it is not done yet.

Live site