{"course":{"slug":"agents-and-automation","title":"Agents and Automation","url":"https://addaly.com/learn/agents-and-automation","level":"intermediate","track":"build"},"lesson":{"slug":"parsing-real-documents","title":"PDFs, scans and the layout problem","url":"https://addaly.com/learn/agents-and-automation/parsing-real-documents","position":22,"of":72,"minutes":10,"answer":"A PDF has no tables and no reading order, only positioned glyphs, so check first whether there is a text layer at all, because that single fact decides your entire pipeline.","keyIdea":"A PDF has no tables and no reading order, only positioned glyphs, so check first whether there is a text layer at all, because that single fact decides your entire pipeline."},"body":{"format":"markdown","text":"## The first question: is there text in there?\n\nTwo documents can look identical on screen and be completely different objects. One is a PDF with a text layer: the characters are stored as characters, and extracting them is exact and free. The other is a photograph of a page wrapped in a PDF container, where the only content is pixels.\n\nFind out before you design anything:\n\n```bash\npdftotext -layout invoice.pdf - | head -40\n```\n\nText comes out, and you are in the easy world. Nothing comes out, or a few stray words, and you need OCR. In a real supplier folder you will find both, plus hybrids where somebody scanned page two of a digital document, so the pipeline must decide per file rather than per source.\n\n> Figure: Decide per file, never per source. Steps: 1. pdftotext -layout (Ten seconds and free. This one fact decides the entire pipeline.); 2. Text came out (Characters stored as characters. Extraction is exact; go straight to validating the arithmetic.); 3. Nothing came out (Pixels only. Deskew, greyscale, threshold, upscale, then Tesseract. Preprocessing beats engine choice.); 4. Either way, check the quote (Every extracted value carries a verbatim line; confirm that line appears in the page text.); 5. Store bytes, hash, text, page number (So a figure disputed in six months is a two-minute answer rather than a lost argument.). A real supplier folder holds both kinds, plus hybrids where page two of a digital document was scanned. Branch on the file in front of you, not on who sent it.\n\nThat `-layout` flag matters more than it looks. Without it, a two-column page interleaves the columns into nonsense. `pdftotext` ships with **Poppler** and is free on every platform; `pypdf` and `pdfplumber` do the same from Python, and `pdfplumber` additionally gives you the coordinates of every word, which is what you want if you ever need to prove where a number came from.\n\n## There are no tables in a PDF\n\nThis is the fact that surprises people. The format stores glyphs at positions. A table is something a human eye assembles from alignment and whitespace; nothing in the file says \"row\" or \"column\". Every table extractor (`pdfplumber`, **Camelot**, **Tabula**) is inferring structure from geometry, and each infers differently on merged cells, wrapped text and rows without ruling lines.\n\nSo expect table extraction to be around 80 to 95 per cent right on real documents and build for that rather than treating a failure as a bug. Validate structurally: does the number of columns match the header, do the line items sum to the printed total. Arithmetic is the cheapest table validator ever invented and it catches nearly every misparse.\n\n## OCR, and where the accuracy actually comes from\n\n**Tesseract** is free, offline, and good, on clean input. Typed black text scanned at 300 dpi is close to perfect. A phone photo of a crumpled thermal receipt under a desk lamp is a different task, and no engine handles it well.\n\nThe lever is preprocessing, not engine choice. Deskew, convert to greyscale, threshold, upscale small text. ImageMagick does all of it in one line and free:\n\n```bash\nconvert receipt.jpg -colorspace Gray -deskew 40% -resize 200% -threshold 60% clean.png\ntesseract clean.png - --psm 6\n```\n\nThe `--psm` page-segmentation mode is worth a half hour of experimenting; mode 6 (\"a single uniform block\") often beats the default on receipts and forms. Teams who spend a day on preprocessing typically get more improvement than teams who spend a week comparing OCR engines.\n\n## Vision models read documents now\n\nMultimodal models handle layout-heavy pages (forms, statements, invoices with a shifting design) better than a rules-based pipeline, and they will describe a page structure that no extractor infers correctly.\n\nTwo honest limitations. They transcribe digits wrongly sometimes, and the wrong digit arrives inside a fluent, well-formed, entirely plausible answer, which is worse than an OCR engine returning garbage you can see is garbage. And unless you explicitly ask for it, there is no provenance: no coordinates, no quote, nothing tying the number to a place on the page.\n\nSo ask for the evidence. Request a verbatim quote of the surrounding line with every field, and then check mechanically that the quote appears in the OCR text of that page. Cross-check totals. Both are cheap and both convert a confident fabrication into a caught error.\n\n## Email is its own parsing problem\n\nAn email is a MIME tree with a plain-text part, an HTML part, attachments, and possibly an entirely different message inline. Two rules save most of the trouble.\n\n**Prefer the plain-text part, and strip the quoted history.** Reply chains carry last month's amounts, last month's dates and last month's requests. An extractor pointed at a full thread will confidently pull a figure from a message three replies down. The `talon` library does reply-stripping reasonably; a hand-written rule that cuts at the first line matching `On .* wrote:` or a run of `>` characters gets you most of the way.\n\n**Cut signature blocks before extraction.** Otherwise every message yields the sender's office address as the delivery address, over and over, and it will look correct.\n\n## Keep the trail\n\nStore the original bytes, a hash, the extracted text, and for each extracted field the page number it came from. When somebody disputes a figure in six months, the ability to open page three and point at it is the difference between a two-minute answer and a lost argument."},"check":{"question":"A supplier’s invoices used to extract perfectly and now come out empty, though they look unchanged when opened. What should you check first, and why does it decide the rest of the pipeline?","options":[{"text":"Whether the file still has a text layer, since a scanned page contains only pixels and needs OCR rather than text extraction.","correct":true},{"text":"Whether the table structure changed, since table extractors infer rows from geometry and break when the layout shifts.","correct":false,"tempting":"A layout change degrades table parsing but still yields text; getting nothing at all points at the presence of characters, not their arrangement."},{"text":"Whether the PDF is encrypted or password-protected, which blocks the extractor from reading the content.","correct":false,"tempting":"Worth checking, but protection normally raises an explicit error rather than returning a silently empty result."},{"text":"Whether the file size grew, since larger files exceed the extractor’s default page limit.","correct":false,"tempting":"Invents a limit that these tools do not impose; size correlates with scanning but is not the thing that decides the pipeline."}]},"prev":{"slug":"where-the-data-lives","title":"Files, inboxes, sheets and APIs","url":"https://addaly.com/learn/agents-and-automation/where-the-data-lives"},"next":{"slug":"extraction-that-holds-up","title":"Turning a document into a row you can defend","url":"https://addaly.com/learn/agents-and-automation/extraction-that-holds-up"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'PDFs, scans and the layout problem', in Agents and Automation. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/agents-and-automation/parsing-real-documents (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"PDFs, scans and the layout problem","courseTitle":"Agents and Automation","module":{"index":3,"slug":"data-plumbing","title":"The data underneath: sources, documents, records"},"answer":null,"keyIdea":"A PDF has no tables and no reading order, only positioned glyphs, so check first whether there is a text layer at all, because that single fact decides your entire pipeline.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"flow","steps":[{"label":"pdftotext -layout","detail":"Ten seconds and free. This one fact decides the entire pipeline."},{"label":"Text came out","detail":"Characters stored as characters. Extraction is exact; go straight to validating the arithmetic."},{"label":"Nothing came out","detail":"Pixels only. Deskew, greyscale, threshold, upscale, then Tesseract. Preprocessing beats engine choice."},{"label":"Either way, check the quote","detail":"Every extracted value carries a verbatim line; confirm that line appears in the page text."},{"label":"Store bytes, hash, text, page number","detail":"So a figure disputed in six months is a two-minute answer rather than a lost argument."}],"title":"Decide per file, never per source","caption":"A real supplier folder holds both kinds, plus hybrids where page two of a digital document was scanned. Branch on the file in front of you, not on who sent it."},"alt":"Figure: Decide per file, never per source. Steps: 1. pdftotext -layout (Ten seconds and free. This one fact decides the entire pipeline.); 2. Text came out (Characters stored as characters. Extraction is exact; go straight to validating the arithmetic.); 3. Nothing came out (Pixels only. Deskew, greyscale, threshold, upscale, then Tesseract. Preprocessing beats engine choice.); 4. Either way, check the quote (Every extracted value carries a verbatim line; confirm that line appears in the page text.); 5. Store bytes, hash, text, page number (So a figure disputed in six months is a two-minute answer rather than a lost argument.). A real supplier folder holds both kinds, plus hybrids where page two of a digital document was scanned. Branch on the file in front of you, not on who sent it."}},"blocks":[{"t":"h2","id":"the-first-question-is-there-text-in-there","runs":[{"text":"The first question: is there text in there?"}]},{"t":"p","runs":[{"text":"Two documents can look identical on screen and be completely different objects. One is a PDF with a text layer: the characters are stored as characters, and extracting them is exact and free. The other is a photograph of a page wrapped in a PDF container, where the only content is pixels."}]},{"t":"p","runs":[{"text":"Find out before you design anything:"}]},{"t":"code","lang":"bash","label":"Bash","text":"pdftotext -layout invoice.pdf - | head -40"},{"t":"p","runs":[{"text":"Text comes out, and you are in the easy world. Nothing comes out, or a few stray words, and you need OCR. In a real supplier folder you will find both, plus hybrids where somebody scanned page two of a digital document, so the pipeline must decide per file rather than per source."}]},{"t":"p","runs":[{"text":"That "},{"text":"-layout","code":true},{"text":" flag matters more than it looks. Without it, a two-column page interleaves the columns into nonsense. "},{"text":"pdftotext","code":true},{"text":" ships with "},{"text":"Poppler","b":true},{"text":" and is free on every platform; "},{"text":"pypdf","code":true},{"text":" and "},{"text":"pdfplumber","code":true},{"text":" do the same from Python, and "},{"text":"pdfplumber","code":true},{"text":" additionally gives you the coordinates of every word, which is what you want if you ever need to prove where a number came from."}]},{"t":"h2","id":"there-are-no-tables-in-a-pdf","runs":[{"text":"There are no tables in a PDF"}]},{"t":"p","runs":[{"text":"This is the fact that surprises people. The format stores glyphs at positions. A table is something a human eye assembles from alignment and whitespace; nothing in the file says “row” or “column”. Every table extractor ("},{"text":"pdfplumber","code":true},{"text":", "},{"text":"Camelot","b":true},{"text":", "},{"text":"Tabula","b":true},{"text":") is inferring structure from geometry, and each infers differently on merged cells, wrapped text and rows without ruling lines."}]},{"t":"p","runs":[{"text":"So expect table extraction to be around 80 to 95 per cent right on real documents and build for that rather than treating a failure as a bug. Validate structurally: does the number of columns match the header, do the line items sum to the printed total. Arithmetic is the cheapest table validator ever invented and it catches nearly every misparse."}]},{"t":"h2","id":"ocr-and-where-the-accuracy-actually-comes-from","runs":[{"text":"OCR, and where the accuracy actually comes from"}]},{"t":"p","runs":[{"text":"Tesseract","b":true},{"text":" is free, offline, and good, on clean input. Typed black text scanned at 300 dpi is close to perfect. A phone photo of a crumpled thermal receipt under a desk lamp is a different task, and no engine handles it well."}]},{"t":"p","runs":[{"text":"The lever is preprocessing, not engine choice. Deskew, convert to greyscale, threshold, upscale small text. ImageMagick does all of it in one line and free:"}]},{"t":"code","lang":"bash","label":"Bash","text":"convert receipt.jpg -colorspace Gray -deskew 40% -resize 200% -threshold 60% clean.png\ntesseract clean.png - --psm 6"},{"t":"p","runs":[{"text":"The "},{"text":"--psm","code":true},{"text":" page-segmentation mode is worth a half hour of experimenting; mode 6 (“a single uniform block”) often beats the default on receipts and forms. Teams who spend a day on preprocessing typically get more improvement than teams who spend a week comparing OCR engines."}]},{"t":"h2","id":"vision-models-read-documents-now","runs":[{"text":"Vision models read documents now"}]},{"t":"p","runs":[{"text":"Multimodal models handle layout-heavy pages (forms, statements, invoices with a shifting design) better than a rules-based pipeline, and they will describe a page structure that no extractor infers correctly."}]},{"t":"p","runs":[{"text":"Two honest limitations. They transcribe digits wrongly sometimes, and the wrong digit arrives inside a fluent, well-formed, entirely plausible answer, which is worse than an OCR engine returning garbage you can see is garbage. And unless you explicitly ask for it, there is no provenance: no coordinates, no quote, nothing tying the number to a place on the page."}]},{"t":"p","runs":[{"text":"So ask for the evidence. Request a verbatim quote of the surrounding line with every field, and then check mechanically that the quote appears in the OCR text of that page. Cross-check totals. Both are cheap and both convert a confident fabrication into a caught error."}]},{"t":"h2","id":"email-is-its-own-parsing-problem","runs":[{"text":"Email is its own parsing problem"}]},{"t":"p","runs":[{"text":"An email is a MIME tree with a plain-text part, an HTML part, attachments, and possibly an entirely different message inline. Two rules save most of the trouble."}]},{"t":"p","runs":[{"text":"Prefer the plain-text part, and strip the quoted history.","b":true},{"text":" Reply chains carry last month’s amounts, last month’s dates and last month’s requests. An extractor pointed at a full thread will confidently pull a figure from a message three replies down. The "},{"text":"talon","code":true},{"text":" library does reply-stripping reasonably; a hand-written rule that cuts at the first line matching "},{"text":"On .* wrote:","code":true},{"text":" or a run of "},{"text":">","code":true},{"text":" characters gets you most of the way."}]},{"t":"p","runs":[{"text":"Cut signature blocks before extraction.","b":true},{"text":" Otherwise every message yields the sender’s office address as the delivery address, over and over, and it will look correct."}]},{"t":"h2","id":"keep-the-trail","runs":[{"text":"Keep the trail"}]},{"t":"p","runs":[{"text":"Store the original bytes, a hash, the extracted text, and for each extracted field the page number it came from. When somebody disputes a figure in six months, the ability to open page three and point at it is the difference between a two-minute answer and a lost argument."}]}],"check":{"question":[{"text":"A supplier’s invoices used to extract perfectly and now come out empty, though they look unchanged when opened. What should you check first, and why does it decide the rest of the pipeline?"}],"options":[{"text":[{"text":"Whether the file still has a text layer, since a scanned page contains only pixels and needs OCR rather than text extraction."}],"correct":true,"tempting":null},{"text":[{"text":"Whether the table structure changed, since table extractors infer rows from geometry and break when the layout shifts."}],"correct":false,"tempting":[{"text":"A layout change degrades table parsing but still yields text; getting nothing at all points at the presence of characters, not their arrangement."}]},{"text":[{"text":"Whether the PDF is encrypted or password-protected, which blocks the extractor from reading the content."}],"correct":false,"tempting":[{"text":"Worth checking, but protection normally raises an explicit error rather than returning a silently empty result."}]},{"text":[{"text":"Whether the file size grew, since larger files exceed the extractor’s default page limit."}],"correct":false,"tempting":[{"text":"Invents a limit that these tools do not impose; size correlates with scanning but is not the thing that decides the pipeline."}]}],"order":[1,3,2,0],"why":null},"prev":{"slug":"where-the-data-lives","title":"Files, inboxes, sheets and APIs","minutes":9},"next":{"slug":"extraction-that-holds-up","title":"Turning a document into a row you can defend","minutes":10}}}