# lopdf operator-dispatch patch — measured impact
Two ICDAR-2013-corpus benchmark runs on 2026-05-07, same machine, same
release build profile, single-threaded, warm cache, 2 warm iterations
per file (median):
- [`icdar2013-results-prepatch.csv`](./icdar2013-results-prepatch.csv) —
upstream `lopdf` 0.39.0 verbatim from crates.io. Produced by
commenting out the `[patch.crates-io]` block in `../Cargo.toml` and
re-running `python scripts/bench_icdar2013.py`.
- [`icdar2013-results.csv`](./icdar2013-results.csv) — `lopdf` 0.39.0
with the operator-dispatch patch applied (see
[`../lopdf-fork/CHANGES.md`](../../lopdf-fork/CHANGES.md) and
[`lopdf-patch.diff`](./lopdf-patch.diff)).
## Aggregate impact
| spectre total time (strict) | 309.70 ms | 309.65 ms | **−0.05 ms (no perf cost)** |
| pymupdf total time | 373.02 ms | 368.23 ms | −4.8 ms (run-to-run noise) |
| pdfminer.six total time | 8334.77 ms | 8301.33 ms | within noise |
| pdfplumber total time | 12709.88 ms | 12857.65 ms | within noise |
| Spectre char count (sum) | 541,412 | 561,179 | **+19,767 chars** |
| Mean Δ vs pymupdf | **−3.7%** | **−0.1%** | **+3.6 pts** |
| Median Δ vs pymupdf | **−0.1%** | **+0.6%** | +0.7 pts |
| Within ±10% band | **56/65** | **60/65** | **+4 PDFs** |
The patch is char-count-improving and timing-flat. Spectre's wall time
shifted by 0.05 ms (well below run-to-run noise of ~3% on totals at this
scale); the apparent ratio shift from `1.20×` (pre) to `1.19×` (post)
vs pymupdf reflects pymupdf timing 1.3% faster on the second run, not
patch impact on spectre.
## Per-PDF impact — the patch moved 12 PDFs by ≥2 percentage points
All 12 movers are positive (more chars extracted). The 4 in the
"targeted" group are the ones the investigation was scoped to:
| `us-038.pdf` | −68.8% | **−0.3%** | +7,353 | targeted; `'` op |
| `us-039.pdf` | −50.0% | **−1.0%** | +4,338 | targeted; `'` op |
| `us-034.pdf` | −47.9% | **−0.8%** | +2,692 | targeted; `'` op |
| `us-001.pdf` | −10.7% | **−5.4%** | +744 | targeted; `'` op (residual is whitespace + ligatures, separate concern) |
| `us-028.pdf` | −5.3% | **+0.2%** | +555 | unexpected positive |
| `us-027.pdf` | −2.3% | **+0.6%** | +417 | unexpected positive |
| `us-004.pdf` | −8.2% | **−1.3%** | +377 | unexpected positive |
| `us-029.pdf` | −3.1% | **+0.6%** | +269 | unexpected positive |
| `us-022.pdf` | +10.4% | **+13.8%** | +246 | unexpected; was already over-extraction outlier, patch increased it |
| `us-016.pdf` | −1.9% | **+0.5%** | +227 | unexpected positive |
| `us-035a.pdf` | −7.6% | **−4.4%** | +152 | unexpected positive |
| `us-032.pdf` | +0.2% | **+5.3%** | +132 | unexpected; was at near-exact parity, patch moved it into the in-band over-extraction direction |
**The "broader than targeted" finding**: 8 additional PDFs (us-004,
us-016, us-027, us-028, us-029, us-032, us-035a, us-022) moved by ≥2
percentage points despite not being in the original investigation
target list. The affected operators (`'`, `"`, `T*`) are more
pervasive in real-world PDFs than the minimal repro implies.
**The two over-extraction-direction movers (us-022, us-032)**:
both are explained by the same mechanism as the existing over-extraction
tail (eu-002, eu-025, us-022, us-037): spectre's positional text
emission preserves layout whitespace and surfaces ornamental glyphs
(test-pattern marks, watermarks) that pymupdf's reading-order extractor
coalesces or filters as invisible. The patch slightly amplified this
on us-022 and us-032 because the new line-break operators (`'`/`T*`)
emit `\n` where pymupdf would treat the same operator as an in-line
continuation in some layouts. This is the deliberate text-extraction
vs. faithful-rendering tradeoff — the alternative is to track full
text-state (Tm position math, leading parameter, reading order) which
is explicitly out of scope for this patch (see
`../lopdf-fork/CHANGES.md` § "Scope: operator dispatch only").
Spot-checked us-032 manually: the +5.3% is whitespace + alphabet-pattern
test marks visible at the document head, not duplicate content.
## Run-to-run timing noise
These timings are summed across 65 PDFs (3 strict-fail PDFs included as
fail-time, 2 strict-fails produce 0 chars and are excluded from the
parity-percentage stats). Total wall time is ~310 ms, so OS scheduling
noise visibly perturbs the long ratios — between this morning's two
post-patch runs (00:13 and 00:57 PT, same machine, same code), the
pdfminer ratio shifted from 27.33× to 26.81× and the pdfplumber ratio
from 41.26× to 41.52×. The `1.19×` pymupdf ratio is stable because
both tools are within ~60 ms of each other and shift together.
The per-PDF data in the CSVs is the verifiable artifact. Anyone
re-running the harness can produce their own aggregate from the
underlying samples.