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 native jubarte binary offers --keep-nested-comment on accept,
reject and convert --track-changes accept|reject. These bindings do not
yet dispatch that option or return its kept-thread receipts; use the native
binary when retention is needed.
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 r12
uvx jubarte-redlines convert redline.docx --revisions word
uvx jubarte-redlines read contract.docx
uvx jubarte-redlines read contract.docx --json
uvx jubarte-redlines edit contract.docx -p p12 --anchor "thirty" --content "sixty"
uvx jubarte-redlines edit contract.docx --plan plan.json --out-dir review --pdf
uvx jubarte-redlines capabilities
uvx jubarte-redlines --help
redline is an alias of compare; python -m jubarte_redlines runs the same
commands. changes lists what a document tracks, one change per line with
its short id (r12; --json for one object per line); accept and reject
take repeatable --id (r12, r1+2, header1.r1),
--author and --kind to resolve a selection and keep the rest tracked, or
everything when none is given; read prints the agent view with the
<!-- p12 --> ids edits use (--json the snapshot); edit and add write
FILE_modified.docx, or with --out-dir the bundle (clean copy, redline,
patch.diff, report.jsonl, optional PDF/PNG pages); capabilities
reports what the build can do (--json the manifest). 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 (the library's own records; `jubarte changes --json` is the CLI's listing)
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="Redline", 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, <!-- pN --> id lines
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=["r12"]) # 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
The snapshot uses schema_version: 3. changes lists logical changes:
a change Word wrote as several marks is one entry with its members,
and a deletion and an insertion by one author that touch, in either
order, are one replacement.
summary.tables and tables[]
count top-level body tables only; each index is the view's tN.
Every table has a nested array (empty for a leaf), containing its cell tables.
A nested entry has parent (for example t0.row2.cell0) and no index.
A deeper parent includes the cell-local table ordinal, for example
t0.row2.cell0.t0.row0.cell1. Paragraph ids and edit-plan schemas are unchanged.
Python nested grids are Table(index=None, parent=CELL_PATH, nested=(...));
top-level grids retain integer indices.
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("p3", find="30 days", replacement="45 days", format={"bold": True})
.comment("p3", text="Check with the client", find="45 days")
.insert_paragraph("p3", runs=["New sentence."], like="p3")
)
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). When a side
is a Word document, the comparer writes the changes, and a replacement inside
a paragraph keeps Word Compare's order, the insertion first: {+new+}[-old-].
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. A change or comment that names no author or date takes author
and date when you pass them, else the front matter's author: and
last_modified:/modified:/date:/created:, else Redline and the fixed
epoch, so the same Markdown writes the same bytes (the CLI falls back to
--reference-doc's creator and creation time, then Redline and now).
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)
Text comparisons
jubarte-redlines diff a.docx b.docx --format github
jubarte-redlines diff a.docx b.docx --format github -U0 --accept-changes
jubarte-redlines diff a.docx b.docx --format word
Line views (github, normal, context, side-by-side) retain each input’s
tracked marks. Word comparison accepts all changes in both inputs first,
then creates fresh CriticMarkup; earlier revision provenance disappears.
critic remains a representation of document content with its marks.
Long lines use a 70-character review window; --full-lines shows all text.
Text views support DOCX and Markdown and write stdout or an explicit -o text
file. CLI help and usage errors come from the shared clap-derived grammar.
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.
WebAssembly has no installed fonts: docxToPdf paints with the fonts a
document embeds and the bundled Carlito and Liberation faces, so Chinese,
Japanese or Korean text without an embedded font is not painted (see
KNOWN_ISSUES.md, item 9).
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 read and regenerate the plan from its new sha256: line |
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.14.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| jubarte_redlines-0.14.0.tar.gz | 7.9 MB | Without provenance | Details for jubarte_redlines-0.14.0.tar.gz |
Built distributions (wheels)
Total release size: 107.9 MB
Release files / jubarte_redlines-0.14.0.tar.gz
| Download URL | jubarte_redlines-0.14.0.tar.gz |
|---|---|
| Size | 7.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c2784fd0a424a1d8ac438ad1fc804003b20870e29acc0299c98e97ed5abb4286
|
|
BLAKE2b-256 checksum How to use checksums |
c258fef11f6c60fad59e3f108071c4ce203a5ae6cdd4e176c17e6565d687cdc3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jubarte_redlines-0.14.0-cp310-abi3-win_amd64.whl
| Download URL | jubarte_redlines-0.14.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 14.4 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
0a65ccfa0fc3d569ee18bb65020ef78d1319f41b04a1e115390c362687f85a40
|
|
BLAKE2b-256 checksum How to use checksums |
eb3aa5371c635abaefece32ce36d979b797f64e6813ab5748197372383cad5cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jubarte_redlines-0.14.0-cp310-abi3-musllinux_1_2_x86_64.whl
| Download URL | jubarte_redlines-0.14.0-cp310-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 14.9 MB |
| Tags | CPython 3.10 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
367532a2dab27cdd0df09202dba4409a7339878693a56f1f83f103f1a41e623b
|
|
BLAKE2b-256 checksum How to use checksums |
ee3b59cf53cd4ff622f5c80c5322a81e3009efbb2058b55d4abbda3d7a2ef346
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jubarte_redlines-0.14.0-cp310-abi3-musllinux_1_2_aarch64.whl
| Download URL | jubarte_redlines-0.14.0-cp310-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 14.3 MB |
| Tags | CPython 3.10 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
37800b8057d18017941cfc4c3a2958e390b7e47e20c3b8f0a99b3cfc1361fcfc
|
|
BLAKE2b-256 checksum How to use checksums |
671fdfe31806367ff95b887509b001be15849ff6fb46ee44348e4c249ae56604
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jubarte_redlines-0.14.0-cp310-abi3-manylinux_2_28_x86_64.whl
| Download URL | jubarte_redlines-0.14.0-cp310-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 14.8 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
211414e670bda442be1e2ca95403961ffa30073f42acc01978c4e60decbefe55
|
|
BLAKE2b-256 checksum How to use checksums |
7c591f2e1495045c0b73701f1d870c1da91bd1f784a4ad05ce24b23d9ca5cce1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jubarte_redlines-0.14.0-cp310-abi3-manylinux_2_28_aarch64.whl
| Download URL | jubarte_redlines-0.14.0-cp310-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 14.1 MB |
| Tags | CPython 3.10 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
13648f22f27a010c8f5dd35c1aa42d53ea44aedda7298488f980177e76a01a6c
|
|
BLAKE2b-256 checksum How to use checksums |
5bcbfa87d7f3b6c34095d438855063c005cd335bf9824822d08233b69b212cbb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jubarte_redlines-0.14.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | jubarte_redlines-0.14.0-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 13.5 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
6ccf245b9d6cabae5b506a4171d072012e5f5f15b1feea394f467899564b60eb
|
|
BLAKE2b-256 checksum How to use checksums |
8b864f0035185086a3ef8d0d32722d530eafb32846ec462794a019604f9068d1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jubarte_redlines-0.14.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | jubarte_redlines-0.14.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 14.0 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
2a6ddb56a300772294c8593e982f47579ece4d7f7db4333b7968f42cc8775322
|
|
BLAKE2b-256 checksum How to use checksums |
8a46291a258162c7965579da8707446fe3299f25497d2d8efe6c02aef6aa8658
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"20.04","id":"focal","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for jubarte-redlines 0.14.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.