structure¶
Geometry back to semantics: turns an OcrDocument into headings and paragraphs.
Features¶
- Pure
commonMain: no platform code, no coroutines, no I/O. Its only dependency ismodel. - A single pure function, so it is fully testable and entirely skippable by callers who only want raw text.
- Works on either extraction path: the digital layer and OCR produce comparable geometry, so a mixed document reconstructs as one.
- All targets: android, jvm, iosArm64, iosSimulatorArm64, iosX64.
Core Components¶
DocumentStructure¶
DocumentStructure.reconstruct(document, options = StructureOptions.Default): List<DocBlock> — blocks in reading order, empty if the document has no non-blank text.
What it does, in order:
- Sorts each page's lines into reading order (top to bottom, then left to right), because engines emit in their own order and Vision in particular reorders.
- Takes the median type size over the document as the body baseline — a median, not a mean, so that headings and page numbers cannot hide the very headings being looked for.
- Classifies a line as a heading when its size clears
headingRatio(level 1 aboveh1Ratio, otherwise level 2) and it is no longer thanmaxHeadingWords; a whole paragraph set in large type is still a paragraph. - Drops page furniture: lines that are nothing but digits, and first/last lines whose text repeats on enough pages. Repetition is measured with digits normalized away, so
Chapter 3 — page 41and… page 42count as one key; a repeat whose raw text is identical every time is treated as a running head rather than as a chapter title. - Joins lines into paragraphs, starting a new one when the vertical gap exceeds
paragraphGapRatiotimes the line height. Gaps are only meaningful within a page — across a page break the paragraph is assumed to continue. - Resolves end-of-line hyphenation by dropping a trailing hyphen (
сло-+воbecomesслово).
StructureOptions¶
headingRatio (1.25), h1Ratio (1.6), paragraphGapRatio (1.2), chromeFrequency (0.4), maxHeadingWords (20), minPagesForChromeDetection (4), dropChrome (true). Validated in init, e.g. h1Ratio must be at least headingRatio.
Presets: StructureOptions.Default and StructureOptions.KeepChrome, which keeps running headers, running footers and page numbers — one flag, because they are one decision found by two different rules.
Usage¶
val blocks = DocumentStructure.reconstruct(document)
blocks.forEach { block ->
when (block) {
is DocBlock.Heading -> println("#".repeat(block.level) + " " + block.text)
is DocBlock.Paragraph -> println(block.text)
else -> Unit
}
}
It assumes single-column, reflowable prose
Multi-column layouts, tables and lists are not handled yet — they are why DocBlock is
expected to grow. Given a two-column scan it will interleave the columns, because it
sorts by vertical position.
A 0f or NaN font size must never vote
PDFBox reports 0 pt for Type3 fonts and degenerate text matrices, so a document can mix
real sizes with zeros; letting them count drags the median below the body size and turns
ordinary prose into headings. NaN is the same trap from the other side — every
comparison against it is false, so an unreported size would sail past the ratio test.
Both are filtered out of the median and rejected as headings.