PDF extraction is reconstruction, not reading: budget the pipeline that way
A dev.to walkthrough of pypdf and PyMuPDF internals is a useful reminder that PDFs store canvas draw operators, so every paragraph and table your pipeline emits is inferred.
The Engineer · Build desk

What happened
- A PDF has no concept of a paragraph, a column or a table, and does not store text in reading order.
- A PDF is essentially a set of vector drawing instructions for a printer canvas.
- When a word processor exports a document to PDF, it discards semantic document structure (DOM) in favour of visual position.
- Rather than storing elements such as <h2> or <table>, the PDF encoder generates low-level drawing operators: BT to begin a text object, Tf to set font and size, Td to move the cursor to a coordinate, Tj to draw a character stream, and ET to end the text object.
- The drawing stream contains no space characters, no paragraph breaks and no reading order.
Why it matters
A walkthrough published on dev.to steps through what pypdf and PyMuPDF actually do when they open a file, and its central point is structural rather than a matter of tool preference: a PDF has no concept of a paragraph, a column or a table, and does not even store text in reading order [2]. If that is true, then every ingestion pipeline that outputs paragraphs and table cells is running a reconstruction, and it should be scoped, budgeted and tested as one rather than as a library call.
The reason is in the format. According to the dev.to piece, a PDF is essentially a set of vector drawing instructions for a printer canvas [3], and when a word processor exports to PDF it discards the semantic document structure in favour of visual position [4]. Instead of a heading or table element, the encoder writes low-level operators: begin a text object, set a font at a size, move the cursor to an absolute coordinate, draw a character stream, end the object [5]. What is absent from that stream is space characters, paragraph breaks and reading order [6]. Because glyphs land at hardcoded x,y points, a generator may draw the footer first, the header second, the right-hand sidebar third and the main paragraph last [7], and a naive dump of the raw stream produces interleaved columns [8].
So the work splits into two jobs, which the article labels as decoding binary anatomy with pypdf and then spatial geometry and layout analysis with PyMuPDF [1]. The first job is plumbing with sharp edges. pypdf's reader seeks to the end of the file, reads the last 1,024 bytes to find the startxref keyword, and parses the cross-reference table into an index from object IDs to byte offsets [9]. The trailer dictionary points at the document catalog, at metadata such as Author, CreationDate and Producer, and at the total XRef entry count [10]; the root catalog exposes keys including /Pages, /Names and /AcroForm [11]. Page drawing commands live in a stream, or an array of streams, under /Contents [12], usually compressed with FlateDecode and inflated in RAM [13]. Stream strings are not UTF-8 by default but 8-bit encodings, often WinAnsiEncoding or PDFDocEncoding, which is why latin1 is used as a decode that preserves byte values instead of raising [14]. The mapping from a stream alias like /F1 to real characters sits in the font dictionary, with /BaseFont names and embedded /ToUnicode character maps [15].
The second job is where the error rate lives. PyMuPDF runs MuPDF's C engine [16] and returns, for each word, an eight-element tuple: bounding box, the word text, and block, word and line numbers [17], with (x0,y0) top-left and (x1,y1) bottom-right [18]. Four of those eight fields are geometry, one is text, and three are grouping indices [21], and the grouping exists because the snippet clusters tokens spatially to solve multi-column reading order [19]. That is a heuristic with a failure mode, not a field read out of the file.
Practical consequence: treat column detection, header and footer stripping, and table cell assignment as estimators. Keep a fixture corpus of the actual vendor documents you receive, assert on extracted output rather than on exit codes, and price the long tail into the schedule.
Watch three things in your own corpus. Files whose fonts ship no /ToUnicode map [15], since character recovery depends on that mapping. Files carrying /AcroForm data [11], where the values you want are form fields rather than drawn text. And pages leaning on /XObject entries, which the article notes hold embedded images or sub-canvases [20] and therefore push content outside the main stream you are parsing.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence42
- Adoption
- Insufficient
- Hype gap+8
- Incentives22
- Confidence48
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
The walkthrough divides parsing into Phase 1, decoding binary anatomy with pypdf, and Phase 2, spatial geometry and layout analysis with PyMuPDF (fitz).
- [2]
A PDF has no concept of a paragraph, a column or a table, and does not store text in reading order.
- [3]
A PDF is essentially a set of vector drawing instructions for a printer canvas.
- [4]
When a word processor exports a document to PDF, it discards semantic document structure (DOM) in favour of visual position.
- [5]
Rather than storing elements such as <h2> or <table>, the PDF encoder generates low-level drawing operators: BT to begin a text object, Tf to set font and size, Td to move the cursor to a coordinate, Tj to draw a character stream, and ET to end the text object.
- [6]
The drawing stream contains no space characters, no paragraph breaks and no reading order.
- [7]
Because glyphs are placed at hardcoded (x, y) coordinate points on a Cartesian canvas, a PDF generator might draw the footer first, the header second, the right-hand sidebar third and the main content paragraph last.
- [8]
A naive dump of the raw text stream produces a scrambled mess of interleaved columns.
- [9]
pypdf's PdfReader opens the binary file, seeks to the end and reads the last 1,024 bytes to locate the startxref keyword, then parses the cross-reference (XRef) table to build an internal index mapping object IDs to byte offsets in the file.
- [10]
The PDF trailer dictionary contains top-level pointers to the document catalog, to metadata including Author, CreationDate and Producer, and to the total number of entries in the XRef table.
- [11]
Resolving the root catalog exposes top-level keys such as /Pages (the root of the page tree), /Names and /AcroForm (interactive form fields).
- [12]
A page object delegates its visual drawing commands to a stream object, or an array of stream objects, specified under the /Contents key.
- [13]
In raw PDF files stream data is compressed with the FlateDecode filter (standard zlib compression), and get_data() inflates the compressed stream back into uncompressed drawing operators in RAM.
- [14]
PDF stream strings do not use UTF-8 by default but 8-bit encodings, often WinAnsiEncoding or PDFDocEncoding; decoding as latin1 prevents Python decoding exceptions while preserving byte values.
- [15]
The /Font entry of a page resource dictionary maps font aliases used inside the drawing stream, such as /F1 or /F2, to indirect font dictionary objects containing /BaseFont names and embedded /ToUnicode character maps (CMaps).
- [16]
fitz.open() uses MuPDF's C engine to parse the document structure into memory.
- [17]
page.get_text("words") executes the page stream and returns, for every detected word, an eight-element tuple of (x0, y0, x1, y1, word text, block number, word number, line number).
- [18]
In the returned word tuple, (x0, y0) is the top-left coordinate and (x1, y1) the bottom-right coordinate.
- [19]
The PyMuPDF snippet extracts coordinate-level word tokens and groups them spatially to solve multi-column reading order problems.
- [20]
Every page has a resource dictionary containing the assets needed to draw it, including /Font, /XObject (embedded images or sub-canvases) and /ColorSpace.
- [21]
Of the eight fields in a PyMuPDF word tuple, four are bounding-box geometry, one is the word text, and three are grouping indices (block, word, line).
Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toHow PDF & Document Parsers Actually Work Under the Hood
1 article · August 16, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Topics
- Python document toolingFollow
- Document parser internalsFollow
- PDF text extractionFollow
- Layout analysis heuristicsFollow