Building a PDF Creation Library in MQL5 (Part 2): Why PDFs Break

Why this article was written
Part 1 typed a working PDF by hand: 592 bytes, five objects. Seven of the numbers in it are not data at all — they are byte positions, promises about where something sits in the file.
Nothing in a PDF checks those promises. And here is the uncomfortable part: break them and the file still opens. This article breaks the Part 1 file three different ways, opens all three in two independent engines, and gets correct text out of every one.
That is the trap. "It opened" is not evidence that a file is correct.
What is inside
A viewer reads a PDF backwards. Six steps: seek to the end, search backwards
for startxref, jump to the offset it names, expect to land on xref, read the
table and trailer, then look up the catalog. Steps 1 to 4 are pure byte
arithmetic — which is exactly why they can be wrong.
Position is metadata. The one thing a PDF never records about itself is where
its own objects are. 5 0 obj does not know it is at byte 341. Only the
cross-reference table claims that, and the table is just a list of integers
nobody verifies.
Twenty bytes, one of them invisible. Each xref entry is exactly 20 bytes: ten offset digits, a space, five generation digits, a space, a flag, a two-byte line ending. That trailing space is load-bearing. Delete it and entries become 19 bytes, which misaligns every entry after the first one.
Three breakages, on purpose. Change (Hello, PDF!) to (Hello, world!) and
three numbers go stale at once — /Length, the object offset and startxref.
Strip the padding space. Point startxref into the middle of an object. All
three files open fine.
What real viewers actually do. PDFium (the engine inside Chrome and Edge) and
pypdf both silently fall back to a repair scan: give up on the table, read
the whole file looking for N 0 obj. Turn pypdf's warnings on and it admits it —
incorrect startxref pointer, ignoring wrong pointing object. The article is
specific about what that costs you: linear scan time on large files, guessed
duplicates, consumers that do not repair (PDF/A validators, some print
pipelines), and compressed xref streams a scan cannot rebuild at all.
The four checks, and PdfValidate.mqh. The first permanent file of the
library. It verifies that %PDF- sits at byte 0, that startxref lands on
xref, that entry i really points at i 0 obj in 20-byte entries, and that
each /Length reaches its endstream. It reports the object number, the
expected position and the actual one.
One detail worth noting: the naive version of check 4 wrongly flagged the
known-good file, because endstream may follow an optional line ending. The
article keeps the mistake in rather than quietly fixing it.
An honest note on scope
The validator checks structure, not content. A file with a missing font and a
blank page will pass every one of these checks. It can also flag perfectly valid
files that use features it does not know — xref streams, multiple xref runs,
indirect /Length, nonzero generations. It never looks at the trailer, /Root,
the page tree or %%EOF.
What it will do is tell you whether a file's byte arithmetic adds up — and from Part 3 onward, every part of this series uses it to test its own output.
PdfValidate.mqh Include/MQLPDF/ the four structural checks <- today
HelloPdf.mqh Include/MQLPDF/ Part 1's builder (demo only)
Demo02_BreakIt.mq5 Scripts/MQLPDF/ writes the five test files
Demo02_Validate.mq5 Scripts/MQLPDF/ runs the validator on themMore in this series
Generating real PDF documents from MetaTrader in pure MQL5 — statements, trade tickets and performance reports — with no DLL and no external library. About this series
- Part 1Writing a PDF by HandSeptember 24, 2026
- Part 2Why PDFs Break — you are hereOctober 19, 2026
- Part 3The Byte-Exact WriterOctober 29, 2026
Continue reading on MQL5.com
The full article with complete source code is published on MQL5.com.


