PDF text structure
A PDF’s text layer is a set of positioned glyphs; doc2mark turns it into Markdown with PyMuPDF
(1.27.1 or later) and deterministic rules. The rules keep the text as printed unless there is
strong evidence to change it (verbatim first): when in doubt, a line stays where it is and as
it is. Tables are described in Tables, and which pages are OCR’d in When pages and pictures are OCR’d.
Each page after the first starts with a <!-- page N --> comment in the Markdown, and every
json_content item carries its page.
Headings
The title is
#: the largest heading of the first page with text, when no other heading of the document is as large. It appears at most once (a page that was OCR’d can still carry its own page title).Section headings are
##and deeper, ranked by font size across the document and by decimal outline depth (1.2below1), without skipped levels. Bold body-size lines, slightly larger lines in capitals or in colour (the non-bold headings of Word 2013 and later) and第一條-style CJK headings count. A heading is one line without bold or italic markup. Thejson_contentitemstext:title/text:sectioncarry the samelevel.Uppercase labels, chart labels, drop caps, running headers and text set across the page’s reading direction (a vertical arXiv stamp) are not headings.
Lists, captions, footnotes, superscripts
Lists. Bullets become
- `` (nested by indentation); ``*and+keep their character, and a bullet that carries meaning stays in the item (- ✓ Approved). Numbered items keep their number. Letters, roman numerals and CJK or parenthesised numbers stay in the item text (- a) ...) and make a list only in a sequence, so a loneA. SmithorE. coliline is text. A lead-in line and the paragraph after a list are separate paragraphs.Captions. Numbered labels (
Figure 3:,Table 2,圖1) and short caption-shaped text attached to an image or table are captions, written in italics line by line; body text next to a table or image is not.Superscripts are written
10^6^,mc^2^,$1.2bn^3^(footnote marks included), never fused into the number. Raised ordinals and marks stay inline (1st,ACME™).Footnotes. A footnote at the foot of a page whose number is raised becomes
[^1]: ...; every line of a multi-line footnote is kept. The mark in the body text stays a superscript (^1^), not a[^1]reference.
Words and lines
Ligature glyphs are expanded (
fibecomesfi).A word broken at a line-end hyphen is joined and keeps the hyphen (
top-down); the hyphen is removed only when the document spells the word without it elsewhere (invest-+mentnext toinvestment).CJK lines of one paragraph are joined without spaces; short stacked lines stay apart.
Bold and italic mark only the styled words. Overprinted, fake-bold and shadowed text is emitted once.
Escaping
PDF and Office text that looks like Markdown or HTML is escaped so it renders as printed: #
of patients becomes \# of patients, a leading - or 1. that is not a list marker
becomes \- / 1\., a < that would open a tag becomes < (x < 5 stays),
and the & of an entity-like sequence becomes &. The escaping helpers are in
doc2mark.utils.markdown. Table cells and OCR text have their own rules (Tables,
OCR).
Reading order
A page is read top to bottom. A
/Rotatepage is read as displayed, so a table follows its title and the running header and page number stay at the edges.When a page clearly shows columns (two or three, equal or unequal widths), they are read one after the other. Text, a picture or a drawing across the columns (a title, an abstract, a full-width figure) keeps its place between the columns above and below it, also when images are not extracted. Running headers open the page; footnotes and page numbers close it.
A sidebar or pull-quote beside the main text (much narrower, with fewer lines) comes after the text it stands beside, as a whole.
Columns need clear evidence: a vertical strip of white space between items that stand side by side, running text on both sides, and nothing reaching across the strip. Text set out in rows (a form’s labels and values, terms level with their definitions, parallel texts whose paragraphs start level on both sides) keeps its order, as do pages that mix column layouts and pages with more than 400 text blocks, tables and pictures. Reordering never drops or repeats text.
PyMuPDF output
From 1.26.7 PyMuPDF prints a recommendation of its pymupdf_layout package to stdout the first
time tables are searched, and MuPDF prints errors about damaged files (MuPDF error: library
error: zlib error: ...) to stdout. The doc2mark CLI switches the recommendation off and
logs MuPDF’s messages on stderr, so its stdout holds only the document. The library leaves
PyMuPDF’s settings alone: a program that writes to stdout itself can call
pymupdf.no_recommend_layout() and pymupdf.set_messages(...). With a PyMuPDF older than
1.27.1 forced into the environment, each missing capability is logged once and a fallback is
used. If the PDF pipeline itself cannot be imported (a broken installation), a basic PyMuPDF
text extraction runs instead, with a warning: ### Page N sections, no tables, no
json_content.
Example
from doc2mark import load
result = load("report.pdf")
print(result.content[:300])
for item in result.json_content:
if item["type"] in ("text:title", "text:section"):
print(item["level"], item["content"])