diff --git a/README.md b/README.md index c3897a0..1bfbde9 100644 --- a/README.md +++ b/README.md @@ -35,15 +35,54 @@ with open("redline.docx", "wb") as f: f.write(redline_bytes) ``` -That's the whole thing. The rest of this README covers the second (legacy) engine, -comparison settings, and how the packages are built and distributed. +That's the whole thing. The rest of this README covers the other engines, comparison +settings, and how the packages are built and distributed. + +### ๐Ÿ†• New in 0.3.0: `docxdiff`, an optional next-generation engine + +Docxodus now ships a **second comparison algorithm** alongside the classic one. `docxdiff` is a +structure-aware engine that models the document as a tree rather than a stream of runs, so it +produces finer-grained redlines and tracks structural edits โ€” table cell and row properties, section +properties, header and footer content โ€” that the classic algorithm reports coarsely or not at all. + +**It is off by default, so nothing about your existing calls changes engines.** Opting in is one +keyword argument: + +```python +engine.run_redline("Reviewer", original, modified, engine="docxdiff") +``` + +**Please try it and tell us what you find.** It is new, and the two algorithms legitimately disagree +about how to describe the same edit โ€” on this project's own test fixtures `docxdiff` reports 11 +revisions where the classic engine reports 9. Neither is wrong; they segment the same changes +differently. Before adopting it for production redlines, compare its output against your own +documents. See [Choosing an engine](#choosing-an-engine) for the trade-offs and the settings it does +not support. + +> **Note:** the default algorithm is unchanged, but the Docxodus binary behind it moved from v5.4.2 +> to v7.0.0 in this release and carries upstream `WmlComparer` fixes of its own (header references, +> table anchoring). Redline output on the default path can therefore differ from 0.2.1 independently +> of this new flag. Diff a representative document if byte-level stability matters to you. ## Comparison Engines -Python-Redlines provides **two comparison engines**. `DocxodusEngine` is the default and -recommended choice; `XmlPowerToolsEngine` remains available as a legacy option. +Python-Redlines gives you **three ways to compare**, across two engine classes. `DocxodusEngine` +carries two interchangeable algorithms in one binary; `XmlPowerToolsEngine` is a separate, legacy +package. + +| # | Choice | How to select it | Algorithm | Status | +|---|---|---|---|---| +| 1 | **Docxodus ยท `wmlcomparer`** | `DocxodusEngine()` | Modernized `WmlComparer` | โœ… **Default.** Stable, recommended | +| 2 | **Docxodus ยท `docxdiff`** | `DocxodusEngine()` + `engine="docxdiff"` | Structure-aware IR diff | ๐Ÿ†• New in 0.3.0. Opt-in, seeking feedback | +| 3 | **Open-XML-PowerTools** | `XmlPowerToolsEngine()` | Original `WmlComparer` | ๐Ÿ—„๏ธ Legacy. Upstream archived | + +Choices 1 and 3 are cousins: both descend from Microsoft's `WmlComparer`, which is why choice 1 is +named `wmlcomparer`. Choice 1 is Docxodus's actively-maintained fork of it; choice 3 is the original, +unmaintained code. Choice 2 shares nothing with either but the output format โ€” it is a new engine. -### `DocxodusEngine` โ€” Default (Recommended) +**If you are unsure, use choice 1.** It is the default and requires no arguments. + +### 1. `DocxodusEngine` with `wmlcomparer` โ€” the default **[Docxodus](https://github.com/JSv4/Docxodus)** is a modernized .NET 10.0 fork of Open-XML-PowerTools with significant improvements: @@ -61,7 +100,25 @@ engine = DocxodusEngine() redline_bytes, stdout, stderr = engine.run_redline("AuthorName", original_bytes, modified_bytes) ``` -### `XmlPowerToolsEngine` โ€” Legacy +### 2. `DocxodusEngine` with `docxdiff` โ€” new, opt-in + +The same class and the same binary, selected per call. `docxdiff` models the document as an +intermediate representation, which lets it attribute a change to the exact paragraph, cell, row, or +section it touched. + +```python +from python_redlines import DocxodusEngine + +engine = DocxodusEngine() +redline_bytes, stdout, stderr = engine.run_redline( + "AuthorName", original_bytes, modified_bytes, engine="docxdiff", +) +``` + +Three settings do not exist for this algorithm and raise `ValueError` rather than being silently +ignored โ€” see [Choosing an engine](#choosing-an-engine). + +### 3. `XmlPowerToolsEngine` โ€” legacy Wraps the original [Open-XML-PowerTools](https://github.com/OpenXmlDev/Open-Xml-PowerTools) `WmlComparer`. This engine remains available for backward compatibility and for users who prefer the original comparison behavior. @@ -76,8 +133,9 @@ redline_bytes, stdout, stderr = engine.run_redline("AuthorName", original_bytes, > **Note:** Open-XML-PowerTools was archived by Microsoft and is no longer maintained. It uses an older > version of the Open XML SDK. While it works for many purposes, Docxodus is the recommended engine going forward. -Both engines share the same API โ€” the only difference is the class you instantiate and the stdout format -(see [Stdout Differences](#stdout-differences) below). +All three share the same call signature โ€” `run_redline(author, original, modified)` returning +`(bytes, stdout, stderr)`. They differ in the class you instantiate, which keyword arguments they +accept, and their stdout format (see [Stdout Differences](#stdout-differences) below). ## Getting Started @@ -93,7 +151,7 @@ as extras: ```commandline pip install python-redlines[docxodus] # Docxodus engine pip install python-redlines[ooxmlpowertools] # Open-XML-PowerTools engine -pip install python-redlines[all] # both engines +pip install python-redlines[all] # both engine packages ``` Prebuilt wheels are available for Linux, macOS, and Windows (x64 and arm64); `pip` @@ -108,7 +166,9 @@ See the [Quick Start](#quick-start) above for a minimal example, or the ## Comparison Settings (DocxodusEngine only) -`DocxodusEngine` supports fine-grained control over the comparison via keyword arguments to `run_redline()`: +`DocxodusEngine` supports fine-grained control over the comparison via keyword arguments to +`run_redline()`. Which arguments are available depends on the algorithm you select โ€” the second +table below is the authoritative matrix. `XmlPowerToolsEngine` accepts none of them. ```python from python_redlines import DocxodusEngine @@ -123,51 +183,86 @@ redline_bytes, stdout, stderr = engine.run_redline( ) ``` +### What each setting does + | Setting | Type | Default | Description | |---|---|---|---| | `engine` | str | `"wmlcomparer"` | Comparison algorithm: `"wmlcomparer"` or `"docxdiff"` | -| `detail_threshold` | float | 0.0 | Comparison granularity (0.0โ€“1.0, lower = more detailed) | -| `case_insensitive` | bool | False | Ignore case differences | -| `detect_moves` | bool | False | Enable move detection | -| `simplify_move_markup` | bool | False | Convert moves to del/ins for Word compatibility | -| `move_similarity_threshold` | float | 0.8 | Jaccard threshold for move matching (0.0โ€“1.0) | -| `move_minimum_word_count` | int | 3 | Minimum words for move detection | -| `detect_format_changes` | bool | True | Detect formatting-only changes | -| `conflate_spaces` | bool | True | Treat breaking/non-breaking spaces the same | +| `detail_threshold` | float | `0.0` | Comparison granularity (0.0โ€“1.0, lower = more detailed) | +| `case_insensitive` | bool | `False` | Ignore case differences | +| `detect_moves` | bool | `False` | Enable move detection | +| `simplify_move_markup` | bool | `False` | Convert moves to del/ins for Word compatibility | +| `move_similarity_threshold` | float | `0.8` | Jaccard threshold for move matching (0.0โ€“1.0) | +| `move_minimum_word_count` | int | `3` | Minimum words for move detection | +| `detect_format_changes` | bool | `True` | Detect formatting-only changes | +| `conflate_spaces` | bool | `True` | Treat breaking/non-breaking spaces the same | | `date_time` | str | now | Custom ISO 8601 timestamp for revisions | -> **Warning:** (WmlComparer only) Move detection can cause Word to display "unreadable content" warnings due to a known +### Which engine accepts which setting + +| Setting | Docxodus ยท `wmlcomparer` | Docxodus ยท `docxdiff` | `XmlPowerToolsEngine` | +|---|:---:|:---:|:---:| +| `engine` | โœ… | โœ… | โ€” ignored | +| `detail_threshold` | โœ… | โŒ `ValueError` | โ€” ignored | +| `case_insensitive` | โœ… | โœ… | โ€” ignored | +| `detect_moves` | โœ… | โœ… | โ€” ignored | +| `simplify_move_markup` | โœ… | โŒ `ValueError` | โ€” ignored | +| `move_similarity_threshold` | โœ… | โœ… | โ€” ignored | +| `move_minimum_word_count` | โœ… | โœ… | โ€” ignored | +| `detect_format_changes` | โœ… | โŒ `ValueError` | โ€” ignored | +| `conflate_spaces` | โœ… | โœ… | โ€” ignored | +| `date_time` | โœ… | โœ… | โ€” ignored | + +**โŒ `ValueError`** โ€” `docxdiff` has no equivalent of these three settings. The underlying CLI accepts +and silently discards them, so Python rejects them up front rather than let you believe a setting took +effect when it did not. The check is on the *keyword being present*, whatever its value: pass +`detect_format_changes=True` (its default) with `engine="docxdiff"` and you still get a `ValueError`. +Drop the keyword, or use `engine="wmlcomparer"`. + +**โ€” ignored** โ€” `XmlPowerToolsEngine` silently discards every keyword argument, including `engine`. +This is long-standing behavior, not new. Passing `engine="docxdiff"` to it does nothing. + +> **Warning:** (`wmlcomparer` only) Move detection can cause Word to display "unreadable content" warnings due to a known > ID collision bug. When using `detect_moves=True`, always set `simplify_move_markup=True` as well. > This converts move markup to regular del/ins (loses green move styling but ensures Word compatibility). -> **Note:** These settings are only available on `DocxodusEngine`. `XmlPowerToolsEngine` ignores -> extra keyword arguments. - ### Choosing an engine -`DocxodusEngine` wraps two comparison algorithms in one binary. `wmlcomparer` is the default -and is the lineage inherited from Open-XML-PowerTools. `docxdiff` is Docxodus's newer -structure-aware IR engine, which produces finer-grained markup โ€” on the same pair of documents -it reports 11 revisions where `wmlcomparer` reports 9. +`DocxodusEngine` wraps two comparison algorithms in one binary, selected per call: ```python -engine.run_redline("Reviewer", original, modified, engine="docxdiff") +engine.run_redline("Reviewer", original, modified) # wmlcomparer (default) +engine.run_redline("Reviewer", original, modified, engine="docxdiff") # opt in ``` -`docxdiff` does not implement `detail_threshold`, `simplify_move_markup`, or -`detect_format_changes`. Passing any of them alongside `engine="docxdiff"` raises `ValueError` -rather than silently ignoring them. It does honour `detect_moves`, `case_insensitive`, -`conflate_spaces`, `move_similarity_threshold`, `move_minimum_word_count`, and `date_time`. - -Move markup differs between the two. `docxdiff` renders moves natively and rejects -`simplify_move_markup`, so the Word-compatibility mitigation described in the warning above is -unavailable there; whether Word's ID-collision warning affects DocxDiff's native move markup is -untested. If you need moves lowered to plain del/ins for maximum Word compatibility, use -`engine="wmlcomparer"` with `simplify_move_markup=True`. +| | Reach for `wmlcomparer` | Reach for `docxdiff` | +|---|---|---| +| **When** | You want the long-established algorithm | You want finer-grained, structure-aware redlines | +| **Maturity** | Years of production use | New in 0.3.0 โ€” evaluate on your documents first | +| **Granularity knob** | `detail_threshold` tunes it | Not applicable; granularity is structural | +| **Moves** | Can be lowered to del/ins via `simplify_move_markup` | Rendered natively; cannot be lowered | +| **Structural edits** | Reported coarsely | Attributed to the paragraph, cell, row, or section | + +**The two disagree about revision counts, and that is expected.** On this project's own fixtures, +`wmlcomparer` reports 9 revisions and `docxdiff` reports 11 for the same pair of documents. They +segment the same edits differently โ€” a single reworded sentence may be one revision to one engine and +two to the other. Do not treat a changed count as a defect; do compare the rendered redline against +your own documents before switching. + +**Move markup differs.** `docxdiff` renders moves natively and rejects `simplify_move_markup`, so the +Word-compatibility mitigation in the warning above is unavailable there. Whether Word's ID-collision +warning affects `docxdiff`'s native move markup is untested. If you need moves lowered to plain +del/ins for maximum Word compatibility, use `engine="wmlcomparer"` with `simplify_move_markup=True`. + +**Feedback wanted.** `docxdiff` stays off by default precisely so that adopting 0.3.0 cannot change +your output. If you try it, please +[open an issue](https://github.com/JSv4/Python-Redlines/issues) with what you found โ€” especially +documents where its redline reads worse than `wmlcomparer`'s. ## Architecture Overview -Both engines follow the same pattern: a Python wrapper class invokes a self-contained C# binary via subprocess. +Both engine classes follow the same pattern: a Python wrapper class invokes a self-contained C# binary +via subprocess. `DocxodusEngine`'s two algorithms are one binary selected by a CLI flag, not two binaries. The repository is a **monorepo of three separately-published packages**: