Metadata-Version: 2.4
Name: docspine
Version: 0.2.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Office/Business :: Office Suites
Classifier: Topic :: Text Processing :: Markup
Requires-Dist: ocrspine-models>=0.0.1,<0.1
Requires-Dist: pytest>=8 ; extra == 'test'
Provides-Extra: test
License-File: LICENSE
License-File: NOTICE
Summary: Pure-Rust Word (.docx / OOXML) structural parser with strong table support and local image OCR, via PyO3.
Keywords: docx,word,ooxml,office,table,rust,ocr
Author: docspine contributors
License-Expression: Apache-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/VoldemortGin/docspine
Project-URL: Issues, https://github.com/VoldemortGin/docspine/issues
Project-URL: Repository, https://github.com/VoldemortGin/docspine

# docspine

[![PyPI](https://img.shields.io/pypi/v/docspine.svg)](https://pypi.org/project/docspine/)

A pure-Rust Word (`.docx`) parser with Python bindings (PyO3 / maturin,
abi3-py311). A `.docx` file is OOXML — a zip archive of XML parts — and docspine
walks `word/document.xml` directly to produce a structured, information-preserving
model: paragraphs (styled runs), **tables** (rows, cells, merges, fills, nesting),
and embedded pictures. Tables are a first-class focus. Embedded images can
additionally be OCR'd locally, offline, and deterministically via the sibling
[`ocrspine`](https://github.com/VoldemortGin/ocrspine) crate (PP-OCRv5 through
`tract-onnx` — no cloud, no network), and an image that *is* a table can be
reconstructed into a grid from its OCR word boxes. A parsed document can also
be **exported to PDF** (`to_pdf()` / `save_pdf()`) with flowed layout and
pagination through the shared pure-Rust `pdf-typeset` engine from
[`pdfspine`](https://github.com/VoldemortGin/pdfspine) — no LibreOffice, no
cloud converter.

docspine is the document-engine sibling of [`pdfspine`](https://github.com/VoldemortGin/pdfspine)
(PDF) and [`pptspine`](https://github.com/VoldemortGin/pptspine) (PowerPoint),
all sharing the same `ocrspine` OCR core.

## Capabilities

| Area | Status |
| --- | --- |
| Body blocks: paragraphs + tables in document order | parsed |
| Paragraphs: runs, text, style name, alignment, list level | parsed |
| Run styling: font, size, bold, italic, underline, color | parsed |
| **Tables: rows, cells, cell paragraphs** | parsed |
| **Table merges: `gridSpan` (horizontal)** | parsed |
| **Table merges: `vMerge` restart / continue (vertical)** | parsed |
| **Nested tables (a table inside a cell)** | parsed |
| Cell shading/fill, cell width (dxa), table grid columns | parsed |
| Row height, header rows | parsed |
| Embedded pictures: `r:embed` rel → media name + raw bytes + EMU extent | parsed |
| Image OCR (embedded pictures → words + boxes) | working (`ocr_image`) |
| Image-table reconstruction from OCR boxes → grid | working (`reconstruct_image_table`) |
| PDF export: `to_pdf()` / `save_pdf()` — flowed layout + pagination; per-section page geometry (`sectPr`), styles.xml + theme effective styles, numbering engine, table fidelity (borders/merges/margins/vAlign, cross-page) | working |
| Legacy binary `.doc` (OLE/CFB) | probe + typed downgrade (full body deferred) |

Parsing is tolerant: unknown elements are skipped, missing attributes become
`None`, and malformed input yields a typed `DocError` rather than a panic.

### docx first; legacy `.doc` deferred

Modern `.docx` (OOXML) is the **primary target**. The old binary `.doc` is a
Microsoft compound document (OLE/CFB): rebuilding its body from the binary FIB +
piece table is large, fiddly, and shares almost nothing with the docx path. So
docspine ships **detection + a clean typed downgrade** today (a `.doc` byte
stream yields `DocUnsupportedError`, and `probe_doc` reports the CFB streams when
built with the `legacy-doc` feature); full `.doc` body reconstruction is a
follow-up, not a blocker.

## Install

```bash
pip install docspine
```

docspine is **on PyPI**. OCR works out of the box: the PP-OCRv5 weights ship in
the shared [`ocrspine-models`](https://pypi.org/project/ocrspine-models/) data
package — a runtime dependency `pip` pulls in automatically — so a plain
`pip install docspine` finds the OCR models with no extra setup (it no longer
needs a sibling `../ocrspine/models` checkout or `OCRSPINE_MODELS`). To build from
source instead, see below.

## Build (from the package root)

```bash
uv venv .venv
VIRTUAL_ENV="$(pwd)/.venv" uv pip install maturin pytest
# Structural parsing needs no models. The OCR path resolves models from a
# sibling ../ocrspine/models by default (or set OCRSPINE_MODELS).
OCRSPINE_MODELS="$(cd ../ocrspine && pwd)/models" \
  VIRTUAL_ENV="$(pwd)/.venv" .venv/bin/maturin develop --release
```

## Use from Python

```python
import docspine

doc = docspine.open("report.docx")
print(doc.block_count)

for block in doc.body():            # list[dict], introspectable
    if block["kind"] == "paragraph":
        for run in block["runs"]:
            print(run["text"], run["bold"], run["color"])
    elif block["kind"] == "table":
        for row in block["rows"]:
            for cell in row["cells"]:
                print(cell["text"], "span", cell["grid_span"], cell["v_merge"])

# Run OCR on raw image bytes (PNG/JPEG), offline:
items = docspine.ocr_image(open("scan.png", "rb").read())
print(" ".join(i["text"] for i in items))

# Reconstruct a table that lives inside an image into a grid:
for table in docspine.reconstruct_image_table(open("table.png", "rb").read()):
    for cell in table["cells"]:
        print(cell["row"], cell["col"], cell["text"])
```

## Export to PDF

```python
doc = docspine.open("report.docx")
doc.save_pdf("report.pdf")         # flowed layout + pagination
pdf_bytes = doc.to_pdf()           # or in-memory bytes

# Optional: map a requested font family to a local font file (or to another
# installed family), layered on top of the built-in substitution table:
doc.save_pdf("report.pdf", font_map={"Calibri": "/path/to/Carlito.ttf"})
```

Per-section page geometry from `sectPr` is honored (paper size, orientation,
margins per section). Rendering is deterministic and fully offline. Missing
fonts degrade gracefully: an available face is substituted and a Python
`UserWarning` is emitted **once per warning kind** — the export never fails on
a missing font.

## Rust workspace

```
crates/
  doc-core    domain model + geometry (twip/EMU) + typed DocError. No IO/zip/XML.
  doc-parse   OOXML reader: zip extract + quick-xml walk -> Document.
              Legacy binary .doc probing behind the `legacy-doc` feature.
  doc-ocr     image-OCR bridge over ocrspine (PaddleOcr) + image-table reconstruction.
  doc-render  docx -> PDF renderer over the shared pdf-typeset engine (from pdfspine).
  py-bindings PyO3 _core extension (the FFI chokepoint); `ocr` feature gates OCR.
```

## Deferred / follow-up

- Full legacy binary `.doc` (OLE/CFB / `[MS-DOC]`) body reconstruction (FIB,
  piece table, CHPX/PAPX). Today: detection + typed downgrade + `probe_doc`.
- Richer styling (theme colors, styles.xml inheritance), headers/footers,
  footnotes/endnotes, comments, fields, hyperlinks targets, SmartArt/charts.

