Skip to main content

textreflow

PyPI License

Layout regions in, ordered prose out. A layout detector (Docling, YOLO, Surya, LayoutParser, PaddleOCR, …) tells you where the title, the paragraphs and the tables are on each page. It does not tell you the order to read them in, or that the paragraph cut off at the bottom of the left column continues at the top of the right one. textreflow is the step in between:

PDF ──▶ layout detector ──▶ regions ──▶ textreflow ──▶ typed blocks ──▶ chunker
  • Column-aware reading order. Full-width bands (title, abstract, wide figures) first, then the left column top to bottom, then the right.
  • Paragraphs rebuilt across regions, columns and page breaks. A paragraph stays open until a sentence ends, and a figure, caption or footnote in between does not close it.
  • De-hyphenation from the document itself. construc- + tion becomes construction, but Edge- + centric stays Edge-centric when that compound appears unbroken somewhere else in the same document.
  • Structure kept as block types: title, section, authors, caption, table, formula, footnote, list.
  • Page furniture removed from the flow (running heads, page numbers, text inside figures) and returned separately, not thrown away.
  • No dependencies. Standard library only. No detector is bundled, so nothing AGPL comes along with it.
pip install textreflow

Use

from textreflow import assemble, render_txt

layout = {"pages": [{"page": 0, "width": 612, "regions": [
    {"label": "Text", "bbox": [40, 100, 290, 700], "lines": ["Models have demon-"]},
    {"label": "Text", "bbox": [320, 60, 570, 200], "lines": ["strated it."]},
]}]}

blocks, dropped = assemble(layout)
blocks   # [{"type": "paragraph", "page": 0, "text": "Models have demonstrated it."}]
dropped  # {"page_headers": [], "page_footers": [], "picture_text": []}

print(render_txt(blocks))   # tagged text: "# " title, "## " section, [TABLE] … [/TABLE], …

From Docling (install it yourself; textreflow only reads the document object):

from docling.document_converter import DocumentConverter
from textreflow import assemble
from textreflow.adapters.docling import from_docling

doc = DocumentConverter().convert("paper.pdf").document
blocks, dropped = assemble(from_docling(doc))

The blocks are exactly the input that grain-growth-chunking takes, so the two chain directly:

from grain_growth import chunk_document
chunks, skipped = chunk_document(blocks, filename="paper")

The input contract

For each page: its number, its width, and the regions found on it. A region has a label, a bounding box, and its text as lines.

{"pages": [
    {"page": 0,                                  # 0-based
     "width": 612.0,                             # same units as bbox
     "regions": [
         {"label": "Text",
          "bbox": [40.0, 100.0, 290.0, 200.0],   # x0, y0, x1, y1, top-left origin
          "lines": ["The construc-", "tion of graphs"]}],
     "swallowed_text": [                         # optional
         {"label": "Picture", "text": "axis"}]}]}
label Becomes
Title title block
Section-header section block; closes the open paragraph
Authors authors block; read on its own, never as part of a column
Text prose, merged across regions until a sentence ends
List-item consecutive items form one list block
Caption, Footnote, Formula their own blocks; an open paragraph continues past them
Table table block, one row per line
Picture nothing; a position anchor only
Page-header, Page-footer removed from the flow, returned in dropped

Regions can arrive in any order, because reading order is worked out from the boxes. Any unit works (PDF points, pixels) as long as bbox and width use the same one. lines must already be in reading order within the region: grouping words into lines is the detector's job. Put text found inside pictures in swallowed_text if you want it reported in dropped. Other keys (conf, height, …) are ignored.

Check a layout before you trust the output. A wrong label does not crash anything: the region is quietly read as prose.

from textreflow import validate_layout
validate_layout(layout)                 # raises ValueError on the first problems
validate_layout(layout, strict=False)   # returns every problem as a list

Column geometry can be tuned: assemble(layout, full_width_fraction=0.6, single_column_fraction=0.7). The defaults are the values used in the evaluation below.

Evidence

The measure is the share of paragraphs (over 40 characters) that begin with a lower-case letter. Each of these is a continuation the parser failed to join to the paragraph before it. Measured on 60 arXiv papers:

Pipeline Paragraphs Begin mid-sentence
Docling, its own document model 4,378 12.5%
Docling's layout → textreflow 3,031 2.4%
A fine-tuned YOLO layout → textreflow 3,022 4.8%

Same detector, same boxes: replacing only Docling's assembly step with textreflow cuts mid-sentence paragraphs about five-fold, from 12.5% to 2.4%.

Source: evals/Reports/parser_quality_60.json in the Anneal repository, produced by evals/scripts/parser_quality_probe.py.

What this does not show:

  • These are pooled percentages over all paragraphs. They are not a paired per-paper test and have no confidence interval, so the size of the effect per document is not established.
  • An earlier 5-paper run gave 17.4% against 2.9%. That smaller sample overstated Docling's failure rate. Use the 60-paper figures above.
  • The metric catches only missed merges. A wrong merge (two paragraphs joined because the first did not end in ., ! or ?) does not show up in it.
  • The heuristics were developed on arXiv-style one- and two-column papers. Three-column layouts, right-to-left scripts and forms have not been evaluated.

Development

pip install -e ".[test]"
pytest -q                      # 40 tests, no network, no detector needed

Citing

If this is useful in your work, please cite it. CITATION.cff in this repository gives GitHub's "Cite this repository" button everything it needs:

Choudhary, M. (2026). textreflow: reading order and paragraph reconstruction for document layout detectors (version 1.0.0) [Software]. https://github.com/mudit-choudhary/textreflow

ORCID: 0009-0003-8880-0925.

Licence

Apache-2.0. See LICENSE and NOTICE.

Metadata

Release files for textreflow 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for textreflow 1.0.0
File Size Uploaded
textreflow-1.0.0.tar.gz 23.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for textreflow 1.0.0
File Interpreter ABI Platform
textreflow-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 41.3 kB

Release files / textreflow-1.0.0.tar.gz

Download URL textreflow-1.0.0.tar.gz
Size 23.0 kB
Tags Source
SHA-256 checksum
How to use checksums
8680dda5218d699a7ac790fc94157110b6c6ccba6749ec7226b69bc6a8cd0119
BLAKE2b-256 checksum
How to use checksums
50cf5f1d0a58a3ab95f882cd0d985985bdaa18066f6a0c3cb0c9e2d01b0d49ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release files / textreflow-1.0.0-py3-none-any.whl

Download URL textreflow-1.0.0-py3-none-any.whl
Size 18.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2b5fd4b5e9bf01e8ee0622029ba17805f678d5e772e5bc7a33205e01d33cbb56
BLAKE2b-256 checksum
How to use checksums
5dcab76d7539c7afbfbedc2d80eceb77b378ac63160cb204bdeda43cb20c3482
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page