Engineering Journal
Pdf Processor
Pdf Processor

When A Second Renderer Fixes The Symptom Postmortem

2026-08-29

When a Second PDF Renderer Fixes the Symptom but Breaks the Architecture

TLDR: We fixed missing embedded-font glyphs by adding another PDF renderer, then removed it. The visual result was better, but the solution duplicated parsing and graphics runtimes, increased download and memory cost, returned only pixels, and bypassed the operator-native extraction architecture. The durable fix extended the existing engine and preserved a hybrid output contract.

Incident

A technical diagram rendered through a browser PDF stack showed square replacement glyphs inside its callouts. The source page looked correct in conventional readers. Extracted semantic text was also correct. The defect existed specifically in the canvas rendering used to make cropped figure images.

That distinction mattered. The squares were not OCR errors and were not text overlays accidentally reintroduced after cropping. They were pixels produced while the renderer attempted to paint a subset font whose glyph mapping or program was unavailable in that path.

The first successful experiment rendered the crop with a second PDF library. Its font handling reproduced the callouts correctly. At screenshot level, the issue looked solved.

Why the successful experiment was still wrong

The application had been designed around one engine's operator list. Its table detection, path classification, reading-order analysis, semantic links, and coordinate transforms all depended on that representation. The added renderer did not feed those algorithms. It was loaded only to create a bitmap.

That meant the product paid for two PDF parsers, two font systems, and two graphics implementations while gaining no additional editable structure. A large WebAssembly package increased transfer size and browser memory. Every page could now be interpreted twice, with different edge cases and potentially different coordinates.

More importantly, the change had not been presented as an architectural decision before implementation. Replacing a renderer is not a small fidelity patch when the renderer also defines the extraction model.

The failed correction

After removing the second engine, we extended the original operator adapter to emit paths and semantic text as SVG. That restored a single source of truth and produced editable figures.

Then we made a second mistake: whenever vector paths existed, we skipped the raster crop. This appeared consistent with a vector-first architecture, but it misunderstood the receiving editor. The editor's figure contract intentionally combined a locked raster backdrop with editable scene objects. The backdrop preserved everything not yet modeled, while the scene enabled editing.

Without the raster, gradients, complex masks, embedded imagery, and unsupported paint details vanished. The transfer was more editable but less faithful.

Root causes

The immediate technical cause was incomplete font rendering in one canvas path. The process causes were broader:

  1. We treated visual success as architectural success.
  2. We evaluated package cost after integration instead of before it.
  3. We confused vector-native extraction with vector-only presentation.
  4. We changed the producer without writing down the consumer's artifact contract.
  5. We tested the crop in isolation before testing the full transfer.
These causes reinforced each other. A screenshot test rewarded the second renderer. A vector DOM test rewarded removal of the raster. Only an end-to-end test in the receiving editor exposed what both local tests missed.

Resolution

The final design retained one PDF engine for extraction. Its adapter now records ordered path, image, and text paints, including transforms and relevant graphics state. Semantic text runs link back to source paint operations. Figure regions produce editable SVG from that display list.

The original high-resolution crop remains as a fidelity underlay. For text with a confident semantic replacement, the crop is white-masked using the same geometry as the editor's text-editing feature. Correct semantic text is rendered above it as SVG. Geometry, raster, and text share one positioning container.

The receiver again gets both parts:

The design does not claim that white masks work everywhere. Non-white backgrounds and unusual text paints remain explicit corpus work.

Verification

We used unit tests for operator-state capture and semantic linking, the existing processor suite, production builds, and a browser run against the original page. The final figure contained one raster image, one editable SVG, and the expected semantic text nodes. The replacement squares were absent.

The most important verification happened at the contract boundary: the receiving editor once again obtained a raster plus a scene, rather than one representation chosen on its behalf.

What changes next time

Renderer changes now require a short architecture review covering bundle size, peak memory, duplicated parsing, coordinate consistency, editability, and the downstream artifact contract. Fidelity fixes must be tested both as standalone renders and as transferred objects.

We will also classify corpus fixtures by paint feature, not only by document. A test page should state whether clipping, gradients, transparency, Type3 glyphs, rotation, or background color is the reason it exists. This turns vague visual regressions into bounded adapter work.

Lesson

Adding a second engine can be a valid product choice when it contributes enough distinct capability. It was not valid here because it duplicated the expensive foundation solely to generate pixels. The better boundary was already present: one operator-native extraction engine, a raster as evidence of appearance, and editable vectors as evidence of understood structure.

Read this post in the full Engineering Journal →