See every page side by side: 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)
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 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
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:
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
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:
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:
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):
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:
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(this engine, plus a CLI) - npm / WebAssembly:
jubarte-wasm(Node and browser builds) - MCP server:
pip install 'jubarte-redlines[mcp]'thenjubarte-mcp --root .(setup for Claude Code, Codex and Gemini CLI)
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:
export JUBARTE_FONT_DIR=/path/to/jubarte/fonts
export JUBARTE_FONT_INDEX=/path/to/font-index.tsv
Set:
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:
Comparison modes
The default comparison mode is:
--mode word
It targets Word-like comparison behavior.
Classic PowerTools behavior is available with:
--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.94+ |
| 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 before scripting a binary download.
A Word 97-2003 .doc converts to .docx with the native binary's
jubarte convert old.doc (or jubarte::legacy_doc::doc_to_docx in Rust):
text, headings, lists, bold, italic and tables. Every other command, and
the Python and JavaScript packages (the npm command line too), refuse a .doc (or an encrypted document of any Word
version) 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
(AGPL-3.0-only).
Review the license requirements before embedding or deploying the software in a product or network service.
Links
Engine comparison site ·
jandira.tech · arthur.law ·
Cicero · LinkedIn ·
contact@arthur.law
Metadata
Release files for jubarte-redlines 0.11.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jubarte_redlines-0.11.3.tar.gz | 6.1 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| jubarte_redlines-0.11.3-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| jubarte_redlines-0.11.3-cp310-abi3-musllinux_1_2_x86_64.whl | CPython 3.10 | abi3 | Linux musl 1.2+ x86-64 | Details |
| jubarte_redlines-0.11.3-cp310-abi3-musllinux_1_2_aarch64.whl | CPython 3.10 | abi3 | Linux musl 1.2+ ARM64 | Details |
| jubarte_redlines-0.11.3-cp310-abi3-manylinux_2_28_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| jubarte_redlines-0.11.3-cp310-abi3-manylinux_2_28_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| jubarte_redlines-0.11.3-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| jubarte_redlines-0.11.3-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 77.4 MB
Release files / jubarte_redlines-0.11.3.tar.gz
| Download URL | jubarte_redlines-0.11.3.tar.gz |
|---|---|
| Size | 6.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8f07bad86a66308824297ea7efc94b3ec04d98f5dae55b3056b13c779ec0ef99
|
|
BLAKE2b-256 checksum How to use checksums |
9afc4145c9f88e9b644dc4ef8de5852060e6c31537d5fdf14d17543975bac2b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jubarte_redlines-0.11.3-cp310-abi3-win_amd64.whl
| Download URL | jubarte_redlines-0.11.3-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 10.5 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
93c73e83a369505a1bf0835529715f8bc0e32bf4a05603be0dff350c7b3f23ac
|
|
BLAKE2b-256 checksum How to use checksums |
de04a640d990533b7dc29b0469d66108805287651b4b63a5d0dbb15da0ca98e7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jubarte_redlines-0.11.3-cp310-abi3-musllinux_1_2_x86_64.whl
| Download URL | jubarte_redlines-0.11.3-cp310-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 10.6 MB |
| Tags | CPython 3.10 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
421667bd20066c775943c1526bc96734eb91ab86490b151a994b6466ee78d53e
|
|
BLAKE2b-256 checksum How to use checksums |
9d87358864f46846edd3c68d5ea0309ce3aa0e7bc48002fcedbf6f5b5bad6b17
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jubarte_redlines-0.11.3-cp310-abi3-musllinux_1_2_aarch64.whl
| Download URL | jubarte_redlines-0.11.3-cp310-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 10.1 MB |
| Tags | CPython 3.10 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
a0c9c42b8e5f1beeedb4a68bc111e5d1eb81a6667552b784331ad2c39276fc94
|
|
BLAKE2b-256 checksum How to use checksums |
ad09ef68e4fd3fd6628ad80d4c244e9bdad653c3a8bdbb1ff6acf71904205aa1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jubarte_redlines-0.11.3-cp310-abi3-manylinux_2_28_x86_64.whl
| Download URL | jubarte_redlines-0.11.3-cp310-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 10.4 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
7668985835d59058afae4a2aaadc873e88cca3fcc5ae132493df408d58a1c529
|
|
BLAKE2b-256 checksum How to use checksums |
c16f5b717e827f83339526f0d525b7d050d0ef95abfe4ec5af1d7984e332f46a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jubarte_redlines-0.11.3-cp310-abi3-manylinux_2_28_aarch64.whl
| Download URL | jubarte_redlines-0.11.3-cp310-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 10.0 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
73b1ecf828f9f5b2e1f6c9901b93796c97af4a3b2f1f731899f1a80a88a625ce
|
|
BLAKE2b-256 checksum How to use checksums |
15d8c591508768af32ae219217d4b585b3e12e570891868382e6ea28f980fc55
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jubarte_redlines-0.11.3-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | jubarte_redlines-0.11.3-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 9.7 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
5a382376522eaa9978bfd4fdc6405d44af2c0ccfab55e5393c54305b74d271ef
|
|
BLAKE2b-256 checksum How to use checksums |
5df07a4e621777c2a41ffc5f040f368b8b6228d1f6bef68fab6dde4f0e991192
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / jubarte_redlines-0.11.3-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | jubarte_redlines-0.11.3-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 10.1 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
456a1af68411b7c4e46718d464dab48fefc001b932c3ab94c6f2d501179051d3
|
|
BLAKE2b-256 checksum How to use checksums |
0b62293d0ca3e8dda2a855538e947bf0c69aa554c2f7829cd7839fb53f7f0eab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|