Metadata-Version: 2.4
Name: jubarte-redlines
Version: 0.11.2
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Legal Industry
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Rust
Classifier: Topic :: Office/Business :: Office Suites
Classifier: Topic :: Text Processing :: Markup :: XML
Classifier: Typing :: Typed
Requires-Dist: mcp>=2.2,<3 ; extra == 'mcp'
Provides-Extra: mcp
License-File: LICENSE
Summary: Word-faithful DOCX redline engine — compare two Word documents into a tracked-changes document that opens cleanly in Microsoft Word; list, accept, or reject revisions; render DOCX to PDF
Keywords: docx,redline,track-changes,tracked-changes,word,microsoft-word,openxml,ooxml,compare,diff,pdf,docx-to-pdf
Author: Jandira Technologies, LLC
License-Expression: AGPL-3.0-only
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/jandira-tech/jubarte-redlines/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/jandira-tech/jubarte-redlines
Project-URL: Issues, https://github.com/jandira-tech/jubarte-redlines/issues
Project-URL: Repository, https://github.com/jandira-tech/jubarte-redlines

<!--
SPDX-FileCopyrightText: 2026 Jandira Technologies, LLC

SPDX-License-Identifier: AGPL-3.0-only
-->

<!-- Generated by scripts/library_readmes.py from README.md and jubarte-python/README.fragment.md for v0.11.2 (README.md sha256 8ca8adedab4405ae). Edit those, not this file. -->

> **See every page side by side: [jandira-tech.github.io/neurotic_docx_bench](https://jandira-tech.github.io/neurotic_docx_bench/)**  
> jubarte vs Microsoft Word, docxide-pdf, LibreOffice, PyMuPDF Pro, MiniPdf, rdocx and office2pdf on 808 documents, DOCX to PDF, scored per page.

# jubarte-redlines (Python)

[![PyPI](https://img.shields.io/pypi/v/jubarte-redlines.svg)](https://pypi.org/project/jubarte-redlines/)
[![CI](https://github.com/jandira-tech/jubarte-redlines/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jandira-tech/jubarte-redlines/actions/workflows/ci.yml)
[![license](https://img.shields.io/badge/license-AGPL--3.0--only-blue.svg)](https://github.com/jandira-tech/jubarte-redlines/blob/v0.11.2/LICENSE)

Word-faithful DOCX **redline** engine: compare two Word documents into a
tracked-changes document that opens cleanly in Microsoft Word; list, accept,
or reject revisions; render DOCX to PDF.

Python bindings (PyO3 + maturin) for the Rust
[`jubarte-redlines`](https://github.com/jandira-tech/jubarte-redlines) engine.
Pure compute, no Word/LibreOffice dependency, no network, no temp files —
documents go in and come out as `bytes` (whole DOCX/PDF packages), revision
metadata comes back as parsed records (`get_revisions` → `list[dict]`,
`get_revisions_json` → JSON string), and the GIL is released while the
engine runs.

## Install

```sh
pip install jubarte-redlines
# or: uv add jubarte-redlines
```

Prebuilt wheels are `abi3` (one wheel per platform, CPython ≥ 3.10).

## Command line

The wheel installs a `jubarte-redlines` command; `uvx` runs it without
installing anything:

```sh
uvx jubarte-redlines redline original.docx modified.docx -o redline.docx --author Legal
uvx jubarte-redlines changes redline.docx
uvx jubarte-redlines accept redline.docx -o clean.docx --kind formatting
uvx jubarte-redlines reject redline.docx -o original-again.docx --id body:rev:12
uvx jubarte-redlines convert redline.docx --revisions word
uvx jubarte-redlines inspect contract.docx --json
uvx jubarte-redlines text contract.docx
uvx jubarte-redlines edit contract.docx --plan plan.json --out-dir review --pdf
uvx jubarte-redlines capabilities --json
uvx jubarte-redlines --help
```

`redline` is an alias of `compare`; `python -m jubarte_redlines` runs the same
commands. `revisions` and `changes` list what a document tracks (`--json` for
one object per line); `accept` and `reject` take repeatable `--id`, `--author`
and `--kind` to resolve a selection and keep the rest tracked, or everything
when none is given; `text` prints the Markdown with the `[body:p:N]` ids edit
plans use, `inspect` the paragraph/package snapshot; `edit` applies an edit plan
to `--out-dir` (clean copy, redline, `patch.diff`, `report.jsonl`, optional
PDF/PNG pages); `capabilities` reports what the build can do. Exit codes: 0
success, 1 error, 2 usage, 3 edit plan refused. Inputs are `.docx`: save a Word
97-2003 `.doc` as `.docx` first.

## Library

```python
from pathlib import Path
from jubarte_redlines import (
    compare_documents,
    get_revisions,
    accept_revisions,
    reject_revisions,
    docx_to_pdf,
)

original = Path("original.docx").read_bytes()
modified = Path("modified.docx").read_bytes()

# Word-mode compare → tracked-changes DOCX (w:ins / w:del)
redline = compare_documents(original, modified, author="Reviewer")
Path("redline.docx").write_bytes(redline)

# List revisions (same shape as the CLI `jubarte revisions --json`)
for rev in get_revisions(redline):
    print(rev["type"], rev["author"], repr(rev.get("text")))

# Accept / reject every tracked revision, package-wide
clean = accept_revisions(redline)   # ≙ modified content
base = reject_revisions(redline)    # ≙ original content

# Render to PDF (Word-style layout)
Path("redline.pdf").write_bytes(docx_to_pdf(redline))
```

`compare_documents(original, modified, author="jubarte", date=None)` stamps
revisions with a fixed epoch date by default so output is deterministic;
pass an ISO-8601 `date` to override. Errors raise
`jubarte_redlines.JubarteError`.

Both inputs are admitted under a size budget before anything is inflated
(512 MiB per file and per part, 2 GiB inflated, 10,000 entries, XML depth
256). `input_limits` overrides it key by key, for example
`compare_documents(a, b, input_limits={"max_uncompressed_bytes": 256 << 20})`;
the keys are `max_compressed_bytes`, `max_entries`, `max_part_bytes`,
`max_uncompressed_bytes` and `max_xml_depth`. A package past the budget
raises `JubarteError` with `INPUT_LIMIT`, and an unknown key raises
`invalid input limits`. `get_revisions_json` and
`CompareOptions(input_limits=...)` take the same mapping.

`read()` returns a `Document` — an immutable snapshot, path I/O done once, no
operation mutating it or writing files:

```python
from jubarte_redlines import read, EditPlan, diff

doc = read("contract.docx")
doc.markdown()    # body and every header/footer/notes story, [body:p:N] ids
doc.inspect()     # Snapshot: summary + paragraphs (ids, style, spans, limitations) + tables
doc.sha256()      # the source_sha256 guard an edit plan carries
doc.changes()     # each tracked change, with the id accept/reject and plans take
doc.accept(ids=["body:rev:12"])   # or reject(...): resolve a selection,
                                  # keep the rest tracked (no selection = all)
result = doc.render(pdf=True, png_dpi=144)  # Rendered: pdf, pngs, page report
```

`doc.compare(other, author=...)` is `compare_documents`; `doc.to_pdf()` and
`doc.to_png(dpi=...)` are the one-shot renderers.

An `EditPlan` builds the tracked-changes transaction `doc.edit(plan)` applies —
all-or-nothing: a refused plan raises `EditPlanError` (stable `code`, the
failed operation, every operation's outcome) and produces nothing.
`doc.preview(plan)` resolves and reports without applying:

```python
plan = (
    EditPlan(author="Reviewer")
    .for_document(doc)             # bind to this snapshot; STALE_SOURCE if it changed
    .replace("body:p:3", find="30 days", replacement="45 days", format={"bold": True})
    .comment("body:p:3", text="Check with the client", find="45 days")
    .insert_paragraph("body:p:3", runs=["New sentence."], like="body:p:3")
)
out = doc.edit(plan)               # EditResult: clean, redline, report, diff
```

`replace`, `insert`, `delete`, `comment` and `format_run` edit run text; `insert_paragraph`,
`delete_paragraph`, `format_paragraph`, `merge_paragraphs` and `rewrite` work on
whole paragraphs; `insert_table(paragraph, rows=[[...], ...])` adds a table and
`list_paragraphs([...], kind_of_list="decimal")` (wire kind `list`) numbers
paragraphs; `insert_footnote` adds a footnote after an anchor;
`insert_image` adds a picture paragraph; `page_setup` changes the page size,
orientation and margins;
`resolving(accept={...}, reject={...})` settles existing
tracked changes first. `plan.to_json()` is exactly what `edit --plan` reads.

`diff(old, new)` shows the
changed paragraphs between two documents — or a document and Markdown text —
as `[-old-]{+new+}` marks (`format="critic"` for CriticMarkup):

```python
print(diff(read("v1.docx"), read("v2.docx")))
```

`doc.append(other)` puts `other` after `doc` on a new page and
returns `Appended(document, warnings)`: images, links, headers, styles, lists
and notes come along. Comments are dropped (`COMMENTS_DROPPED` in `warnings`)
unless `comments="carry"`, which brings the comments `other`'s body anchors
with their threads and resolution. `section_break="continuous"` joins on the
same page and `keep_sections=True` keeps `other`'s page setup, headers and
footers.

Before a document goes out, or after an agent edited it:

```python
doc.validate()        # Word-validity findings beyond the schema; () is a pass
fixed = doc.repair()  # Repaired: document, repaired, remaining
doc.audit_tracked(read("original.docx"), author="Reviewer")  # untracked or foreign edits
doc.audit("a11y,structure")  # accessibility, style and structure findings by paragraph id
out = doc.scrub()     # authors as "Author", no rsids, document properties or comments
doc.update_fields()   # PAGEREF, REF, NUMPAGES, SEQ and TOC results from jubarte's layout
```

`from_markdown(text, author=..., track_changes="all")` writes Markdown as a
Word document, CriticMarkup becoming tracked changes and comments, as
`jubarte convert draft.md` does; `reference=` lends a document's styles and
page setup. `diff_render(a, b, dpi=100)` says which pages of two documents
look different, pixel for pixel, with magenta overlays on the changed pages.

`Document.from_bytes(data)` holds bytes you already have and `to_bytes()`
returns them; `inspect_json()` is the engine's inspect snapshot as JSON text,
unchanged (`inspect()` decodes it). `scrub` takes `author_alias`, `rsids`,
`docprops` and `comments` to choose what goes.

## Also available as

- **Rust crate**: [`jubarte-redlines`](https://crates.io/crates/jubarte-redlines) (this engine, plus a CLI)
- **npm / WebAssembly**: [`jubarte-wasm`](https://www.npmjs.com/package/jubarte-wasm) (Node and browser builds)
- **MCP server**: `pip install 'jubarte-redlines[mcp]'` then `jubarte-mcp --root .` ([setup for Claude Code, Codex and Gemini CLI](https://github.com/jandira-tech/jubarte-redlines/blob/main/docs/adoption/mcp.md))

## Rendering and fonts

The PDF engine reconstructs Word-oriented layout directly; it does not call
Microsoft Word or LibreOffice.

The documented rendering surface includes:

- tracked changes, and comments as Word's balloons in the markup pane
  (a resolved comment faded, a reply numbered under its thread);
- paragraph/run formatting and advanced line layout;
- CJK and RTL/complex-script text;
- installed and embedded fonts;
- merged, vertically merged, autofit and floating tables;
- DrawingML/VML graphics, text boxes, charts and SmartArt;
- raster and EMF/WMF images;
- headers, footers and floating frames;
- sections, columns, backgrounds and page borders;
- footnotes and selected fields.

Rendering fidelity still depends on fonts and on document features.

Supplemental font lookup can be configured with:

```sh
export JUBARTE_FONT_DIR=/path/to/jubarte/fonts
export JUBARTE_FONT_INDEX=/path/to/font-index.tsv
```

Set:

```sh
export JUBARTE_FONT_INDEX=off
```

to disable the persistent font index.

Default per-user font directories are documented in the installation section
of this repository.

For known Word-layout differences, see:

- [`docs/WORD_LAYOUT_RULES.md`](https://github.com/jandira-tech/jubarte-redlines/blob/v0.11.2/docs/WORD_LAYOUT_RULES.md)
- [`docs/WORD_DIFFERENCES.md`](https://github.com/jandira-tech/jubarte-redlines/blob/v0.11.2/docs/WORD_DIFFERENCES.md)
- [`docs/WORD_COMMENT_BALLOONS.md`](https://github.com/jandira-tech/jubarte-redlines/blob/v0.11.2/docs/WORD_COMMENT_BALLOONS.md)

## Comparison modes

The default comparison mode is:

```sh
--mode word
```

It targets Word-like comparison behavior.

Classic PowerTools behavior is available with:

```sh
--mode powertools
```

Choose explicitly if exact comparison semantics matter to a regression test
or downstream workflow.

## Supported environments

| Surface | Supported/tested target |
|---|---|
| Rust / CLI | CI tests Linux, macOS and Windows |
| Rust toolchain | Rust 1.88+ |
| Python | CPython 3.10+ |
| Python release wheels | macOS x86_64/arm64, manylinux_2_28 x86_64/arm64, musllinux_1_2 x86_64/arm64, Windows x86_64 (abi3, CPython 3.10+) |
| Node | Node 18+ |
| Browser | WebAssembly builds |
| CLI release workflow | Linux x86_64/arm64, macOS x86_64/arm64, Windows x86_64 targets |

Release assets can vary by tag. Check the
[release page](https://github.com/jandira-tech/jubarte-redlines/releases)
before scripting a binary download.

Legacy binary `.doc` files are not supported: a `.doc`, or an encrypted
document of any Word version, is refused with `LEGACY_DOC` and the hint to
save it as `.docx` without a password, and RTF with `UNSUPPORTED_PACKAGE`.
Convert them to `.docx` first.

## Troubleshooting

| Problem | What to check |
|---|---|
| PDF layout differs from Word | Verify required fonts; inspect `--font-report`; read `docs/WORD_DIFFERENCES.md` |
| Text is missing or wraps differently | Check font resolution and embedded/system font availability |
| Edit exits with a refusal | Read `report.jsonl` / stdout for `STALE_SOURCE`, `ANCHOR_NOT_FOUND`, `AMBIGUOUS_ANCHOR`, `OVERLAPPING_EDITS`, `UNSUPPORTED_STRUCTURE`, etc. |
| `STALE_SOURCE` | Re-run `jubarte inspect` and regenerate the plan from the new `source_sha256` |
| `AMBIGUOUS_ANCHOR` | Use a paragraph ID or a more specific exact anchor |
| Document already contains revisions | List them with `jubarte changes`; explicitly accept/reject them or set an edit-plan revision policy |
| Comment is present in DOCX but missing from rendered markup | Check the documented comment-balloon limitations and `docs/WORD_COMMENT_BALLOONS.md` |
| Output page count is not exactly Word's | The renderer targets Word behavior but is independent; inspect known differences before treating page count as an invariant |
| Prebuilt archive is absent for a platform | Install with Cargo or build from source; then check the release assets for a later tag |
| Need exact package differences | Use `jubarte debug diff` and the `text`, `runs`, `xml` or `render` debug catalogs |

## License

`jubarte-redlines` is licensed under
[GNU Affero General Public License v3.0 only](https://github.com/jandira-tech/jubarte-redlines/blob/v0.11.2/LICENSE)
(`AGPL-3.0-only`).

Review the license requirements before embedding or deploying the software in
a product or network service.

## Links

[Engine comparison site](https://jandira-tech.github.io/neurotic_docx_bench/) ·
[jandira.tech](https://www.jandira.tech) · [arthur.law](https://arthur.law) ·
[Cicero](https://www.cicero.im) · [LinkedIn](https://linkedin.com/in/arthrod) ·
`contact@arthur.law`
- [GitHub](https://github.com/jandira-tech/jubarte-redlines)
- [crates.io](https://crates.io/crates/jubarte-redlines)
- [docs.rs](https://docs.rs/jubarte-redlines)
- [PyPI](https://pypi.org/project/jubarte-redlines/)
- [npm](https://www.npmjs.com/package/jubarte-wasm)
- [Releases](https://github.com/jandira-tech/jubarte-redlines/releases)
- [npm CLI](https://www.npmjs.com/package/jubarte-redlines)

