Axiomatic ToolsFree Fast Essential
All posts

Proving a browser preview matches the server, to four decimal places

Our form-field detector runs in Python on the server. Its preview runs in your browser. They agree to 0.004pt — and we can show it, because both are the same engine underneath.

Axiomatic Tools Teamengineeringpdftesting

Make PDF Fillable finds the underscore blanks on a scanned-looking form and turns each into a real text field. Its only control used to be a number — minimum field width, in points — with no way to see what that number excluded on a document whose blanks you would have to measure with a ruler to guess at.

So we built an editor that draws the fields on the page before you run it. Which raises the question the rest of this post is about: how do you know the drawing is right?

The preview and the tool are not the same program

Detection runs in a Python worker using PyMuPDF. The preview runs in your browser in JavaScript. Two languages, two processes, two codebases — and a preview that disagrees with the tool is worse than no preview, because it is a confident lie.

The thing that makes it tractable: PyMuPDF is MuPDF. It is a binding over the same C library that mupdf.js — which we already ship for in-browser PDF rendering — is compiled from. Both sides can be made to walk the same structured text and see the same characters in the same places, because underneath they are the same text extractor.

That turns "does the preview lie?" from an argument into a measurement.

The measurement

We generate a form, run the real handler on it, and record the field rectangles it produces. Then we walk the same PDF with mupdf.js exactly as the editor does, apply the shared detection rule, and compare.

minFieldWidthPt=40: mupdf.js 6 fields, worker 6
  [ 88.68,  99.10, 235.46, 115.59]  vs worker  [ 88.68,  99.10, 235.46, 115.59]
  [ 86.67, 129.10, 166.74, 145.59]  vs worker  [ 86.67, 129.10, 166.74, 145.59]
  [103.34, 189.10, 276.81, 205.59]  vs worker  [103.34, 189.10, 276.81, 205.59]
  ...
worst disagreement anywhere: 0.0040pt

Four thousandths of a point — and that residual is entirely the two decimal places we rounded the fixture to. Rendered in the browser, the boxes land within 0.01pt of the worker's own output, which is the rounding in the CSS percentages.

Those worker rectangles are checked into the test fixture alongside the spans PyMuPDF handed the handler, so the parity test is a genuine cross-language comparison rather than one implementation agreeing with itself.

Building the preview found the bug

The detector boxed the whole text span. On an ordinary form line drawn in one font — Name: ______________________ — that is a single span, so the field landed on top of the label as well as the blank. Three of the four fields on our test form did it. A span can hold more than one blank, too: Start: ____ End: ____ is also one span, and became a single field stretched across both.

get_text("dict") gives a box per span. get_text("rawdict") gives one per character. With that, each maximal run of three or more underscores becomes its own field, measured on its own characters. The option finally measures what its name always claimed: Ref: ___ is 45.4pt of span holding 20.1pt of blank, and at a 40pt threshold it used to become a field two characters could not fit in.

We would not have found any of that from the code. We found it from drawing it.

Where they still differ, and saying so

One divergence survives, and it is documented rather than hidden: PyMuPDF inserts a synthetic space where a line has a horizontal gap, and mupdf.js does not. On the test form it lands between spans — after Signed: and before the blank — and changes nothing, because a space is not an underscore. It could only matter on a document that drew one blank as separately positioned glyphs far enough apart to earn a synthetic space, and nothing writes underscores that way.

That is the shape we want these notes to take. Not "the preview is exact", which is a claim no one can check, but: here is where they agree, here is the number, and here is the one case where they would not.

The rule we ended up with

A preview either matches the real output exactly, or says explicitly where it cannot. An approximation presented as exact is the one outcome worse than showing nothing — because a number with no error bar gets believed.