Extract editable text segments from documents, modify them in memory, and write the changed text back as document bytes.
Docxtor is useful when you need one small interface for document text round-trips across plain text files, DOCX files, and text-layer PDFs.
Early library. The public API exports 219 names. The public API is not stable yet.
Import these names from docxtor. Groups describe their mechanical purpose.
| Group | Exported names |
|---|---|
| Version | __version__ |
| Document types and loading | DOCX_MIME, MD_MIME, PDF_MIME, TXT_MIME, DocumentBytes, DocumentError, DocumentKind, DetectedDocumentType, detect_document_type, document_to_bytes, load_document, PdfDocument, PdfExtractionMode, PlainTextDocument |
| DOCX text and inline editing | AddressableComment, AddressableSpan, DocxDocument, InlineSegment, InlineSegmentKind, SegmentReplacement, SpanRole, TextSegment, UnsupportedRevisionError, paragraph_to_inline_segments, rebuild_paragraph_from_inline |
| Paragraph and run locators | ParagraphLocator, ParagraphResolution, RunLocator, RunResolution |
| Comment mutations | CommentScopeBookmark, CommentAuthor, CommentMutationError, CommentMutationResult, CommentRange, CommentReplyTarget, CommentReplyResult, append_comment_reply_bytes |
| Document comparison | compare_docx_documents, BlockPair, CommentAnchor, CommentChange, CommentChangeStatus, ComparisonCoverage, ComparisonDiagnostic, DiagnosticSeverity, DocumentBlock, DocumentComment, DocumentSpan, DocumentView, DocxDocumentComparison, FormattingChange, FormattingSpan, RevisionEvent, RevisionEventStatus, RevisionReference, RevisionResolutionEvidence, TextAnchor, TextChange, TextChangeKind |
| Cross-reference diagnostics | DOCX_CROSS_REFERENCE_PROFILE_ID, CrossReferenceDiagnostic, CrossReferenceReport, CrossReferenceSeverity, diagnose_docx_cross_references |
| Mechanical facts and structural comparison | ChangeKind, ContainerCoordinate, DocxComparison, DocxFactsSnapshot, DocxStructureSnapshot, FactChange, FactDiagnostic, FactsCoverage, NamedFact, PageBreakFact, PageLayoutFacts, ParagraphFact, RelationshipFact, StoryFact, TransformPolicy, UnreadablePartFact, compare_docx, docx_facts |
| Package inventory and graph | DocumentSurface, DocxInventory, InventoryCoverage, PackagePart, SurfaceCapability, SurfaceKind, SurfaceVisibility, inventory_docx, SURFACE_LOCATOR_VERSION, PackageGraph, PackageRelationship |
| Surface mutations | SurfaceDisposition, SurfaceDispositionStatus, SurfaceMutationError, SurfaceMutationResult, SurfaceReplacement, apply_surface_replacements |
| OPC content-type validation | DOCX_CONTENT_TYPES_PROFILE_ID, ContentTypeDiagnostic, ContentTypeSeverity, ContentTypeValidation, validate_docx_content_types |
| OPC relationship validation | DOCX_RELATIONSHIP_PROFILE_ID, RelationshipDiagnostic, RelationshipSeverity, RelationshipValidation, validate_docx_relationships |
| Package reading and publication primitives | DEFAULT_PACKAGE_LIMITS, PackageEntry, PackageError, PackageLimits, normalize_docx_timestamps, parse_package_xml, read_package_entries, restore_semantically_unchanged_xml_parts, write_package_atomically |
| Package comparison | PackageChangeReport, PackagePartChange, PackagePartChangeKind, compare_docx_packages |
| Core keyword properties | read_core_keywords, remove_core_keyword_values, set_core_keywords |
| Publication receipts | PublishError, PublishReceipt |
| Physical review geometry | PhysicalCommentAnchor, PhysicalCommentRange, PhysicalCommentSpan, PhysicalParagraphGeometry, PhysicalReviewGeometry, PhysicalTextOrigin, PhysicalTextInsertion, physical_span_for_semantic_range, project_docx_review_geometry, add_physical_comment_bytes, add_physical_comment_regions_bytes |
| Review markup inventory and batches | inventory_review_markup, CommentRevisionAssociation, OperationReceipt, OperationStatus, ReviewBatchReceipt, ReviewCoverage, ReviewDiagnostic, ReviewMarkupInventory, ReviewCommand, ReviewTransactionError, apply_review_batch |
| Revision mutations | RevisionAuthor, RevisionMutationError, RevisionMutationResult, RevisionPosition, RevisionRange |
| Selective revision disposition | dispose_revisions_bytes, inspect_revision_dispositions, RevisionDecision, RevisionDisposition, RevisionDispositionCoverage, RevisionDispositionDiagnostic, RevisionDispositionInventory, RevisionDispositionReceipt, RevisionDispositionResult, RevisionDispositionTarget, RevisionSelectionError |
| Revision inventory and bulk cleanup | AcceptRevisionsError, RejectRevisionsError, Revision, RevisionInventory, RevisionInventoryCoverage, RevisionKind, RevisionOperation, RevisionOperationError, RevisionOperationReceipt, accept_all_revisions_bytes, inventory_revisions_bytes, reject_all_revisions_bytes, remove_all_comments_bytes |
| DOCX admission | DocumentPackageKind, DocxAdmissionInspection, inspect_docx_admission |
| Package and combined transactions | CombinedTransactionReceipt, apply_docx_transaction, PackageDisposition, PackageDispositionStatus, PackageMutation, PackageMutationError, PackageMutationKind, PackageTransactionReceipt, apply_package_transaction |
| DOCX creation | DocxCreationError, create_docx_from_paragraphs |
| Hyperlink fields | flatten_link_fields, instruction_is_strippable_hyperlink_field |
| Metadata inspection and sanitization | MetadataInspection, inspect_docx_metadata, sanitize_docx_metadata |
| Publication marks and appendices | BodyAppendix, DocumentMark, PublicationMarkError, append_body_appendix, has_body_appendix, has_document_mark, remove_body_appendix, stamp_document_mark, write_publication_bytes |
| Review projection | DocxReviewProjection, ReviewNoteProjection, ReviewParagraphProjection, project_docx_for_review |
| Physical review rendering | PhysicalReviewComment, PhysicalReviewEdit, PhysicalReviewer, PhysicalReviewNote, PhysicalReviewPlan, PhysicalReviewRenderError, render_physical_clean, render_physical_review |
| Format | Read | Write | Notes |
|---|---|---|---|
| TXT/Markdown/text files | yes | same text format when known | Decodes UTF-8, UTF-16, CP1250, or Latin-1 input. |
| DOCX | yes | DOCX | High-fidelity editing powered by python-docx, a third-party dependency (not Python's standard library and not a Microsoft product) for the .docx / WordprocessingML format. Stable container_id + paragraph_index, whole-segment and offset-based partial replacements via apply_replacements; partial replacements update existing w:t text nodes without splitting w:r runs or creating new formatting boundaries. |
| text layer only | Layout-preserving redaction/overlays when possible; per-page rebuild next; full text reflow only if content no longer fits. OCR is not bundled. |
load_document identifies the document kind from bytes first, then consults
the MIME type and file extension as secondary signals.
From GitHub:
python -m pip install git+https://github.com/mikolaj92/Docxtor.git@v0.19.1With uv:
uv add git+https://github.com/mikolaj92/Docxtor.git@v0.19.1Pin the current release tag v0.19.1 (matching pyproject.toml). For the
feature history of earlier releases, see the git tags and docs/releases/.
from docxtor import DocxDocument, SegmentReplacement, document_to_bytes
document = DocxDocument.open("input.docx")
document.apply_replacements(
[
SegmentReplacement(
container_id=segment.container_id,
text=segment.text.replace("old", "new"),
)
for segment in document.segments
],
strict=True,
)
output = document_to_bytes(document)
output.filename # input.anonimizowany.docx
output.content_type # application/vnd.openxmlformats-officedocument.wordprocessingml.document
output.data # bytesEach replacement targets a stable segment identifier. Offset-based partial replacements are available when a whole segment should not be rewritten.
create_docx_from_paragraphs(paragraphs) accepts an Iterable[str], creates
one paragraph per string, and returns DOCX bytes. Incomplete inventory coverage
of the created document raises DocxCreationError, a ValueError subclass,
instead of returning bytes.
flatten_link_fields(path) accepts str | Path and rewrites that same path. Work on a copy: from shutil import copyfile; from docxtor import flatten_link_fields; copyfile("input.docx", "copy.docx"); changed = flatten_link_fields("copy.docx").
It returns True when it rewrites the package, using a sibling temporary file and os.replace. False means no write occurred: the source stays byte-identical. Repeating a completed flattening returns False.
In word/document.xml, word/header*.xml, and word/footer*.xml, it unwraps URI-bearing HYPERLINK/LINK simple fields and non-nested complex fields with a separator and end, preserving display children. Relationship-backed hyperlinks become their display children; matching hyperlink relationships are removed when readable.
In customXml/*.xml, text, tails, and non-schema attribute values containing mailto:, http://, https://, file:, or ftp:// are cleared in full, ignoring case. Namespace and schema identifiers remain unchanged.
Malformed supported story XML raises before replacement, leaving the source unchanged. Unreadable customXml and malformed relationship XML remain untouched; neither necessarily prevents other parts from being rewritten.
Internal TOC fields and anchor-only hyperlinks remain. Nested or unmatched complex fields, DrawingML/VML links, linked images, attached templates, orphan external relationships, and other stories are not flattened. A True result reports rewriting, not package validity or safety; later parser validation is still required and may reject the output.
from docxtor import instruction_is_strippable_hyperlink_field imports the predicate (instruction: str) -> bool. It checks instruction text only, without opening a DOCX package. It returns True when a word-boundary HYPERLINK or LINK token and any of mailto:, http://, https://, file:, or ftp:// occur anywhere in the text, ignoring case. A full internal TOC instruction match (HYPERLINK or LINK, followed by \l and _Toc plus digits, with optional quotes and surrounding whitespace) returns False. Empty text, a missing field token, or a missing recognized URI also returns False. This does not establish safety or validity of the complete field.
from docxtor import MetadataInspection, inspect_docx_metadata, sanitize_docx_metadata
report: MetadataInspection = inspect_docx_metadata("copy.docx")
sanitize_docx_metadata("copy.docx", review_author="Reviewer", review_initials="RV")Both functions accept str | Path. Inspection does not write the file.
violations is a tuple of messages; clean means that tuple is empty.
Inspection checks non-whitespace element text in docProps/core.xml and docProps/app.xml, and flags other docProps/ parts.
Messages omit values, but element names come from input. Some unreadable inputs produce violations; other errors, including some corrupt ZIP errors, can raise exceptions.
Inspection excludes authorship in word/, some attributes, and full OOXML conformance. clean does not establish whole-document safety.
Sanitization changes the file in place and returns None. It clears XML text and tails in core/app.
It removes other docProps/ parts and matching root package relationships and content-type overrides.
In word/ XML, it replaces existing author attributes on comment, ins, del, and person, and existing comment initials; it removes person userId/providerId.
Defaults are review_author="Reviewer" and review_initials="RV"; absent attributes are not added.
The complete ZIP is written to a sibling temporary file before atomic replacement.
Malformed XML during sanitization raises before replacement, preserving the input and removing the temporary file. This is not full anonymization.
DocxDocument.inventory() enumerates every ZIP/OPC member, including orphan
parts that python-docx cannot reach through relationships. It exposes neutral
DocumentSurface records for XML text, XML attributes, and relationship targets.
Each surface has a stable ID, value hash, visibility, and mechanical capability.
Qualified context uses element_qname and attribute_qname (namespace-qualified
XML names), ancestor_qnames (root-to-parent names), and a mechanical role.
locator_version identifies the locator scheme (docxtor-surface-v1).
inventory(), facts(), and apply_surface_replacements() read the current
in-memory XML, including prior text replacements, while retaining orphan parts
and their content types. The original opening bytes remain the publication
source. Surface mutations return verified bytes in result.data; they do not
update the document instance.
inventory.coverage is InventoryCoverage.COMPLETE only when no parts are
unknown or unreadable; unknown_parts and unreadable_parts identify gaps.
Consumers must reject incomplete coverage when their policy requires it.
Docxtor reports physical facts, not PII, legal relevance, or review meaning.
Exact SurfaceReplacement mutations require the value hash observed during
inventory. Docxtor reopens the output and returns a disposition for every
requested surface. Unknown targets, stale hashes, preserve-only surfaces, and
unconfirmed writes fail before partial output is returned.
from docxtor import SurfaceReplacement
surface = next(item for item in inventory.surfaces if item.external)
result = document.apply_surface_replacements([
SurfaceReplacement(
surface_id=surface.surface_id,
expected_value_sha256=surface.value_sha256,
value="https://example.invalid/redacted",
)
])
assert not result.unresolved
result.data # verified DOCX bytesText nested in w:ins, w:del / w:delText, and inline w:hyperlink is
addressable through document.spans. Each AddressableSpan has a stable
span_id, a mechanical role (run, insertion, deletion, hyperlink),
and the same character offsets as TextSegment.text. Replacing through
SegmentReplacement(span_id=...) or paragraph offsets writes into the
existing text nodes and keeps the wrappers, w:rPr, revision author/date/id,
and hyperlink anchor/relationship. Unsupported structural revisions such as
w:moveFrom or block-level w:ins raise UnsupportedRevisionError before
any partial output is written. Docxtor does not interpret review meaning.
mc:AlternateContent story traversal selects exactly one Choice whose
Requires namespaces are in Docxtor's declared support set, otherwise the
Fallback branch. It never concatenates mutually exclusive branches.
document.alternate_content_coverage counts selected regions and reports
unsupported or unbound Requires namespaces; a non-empty limitation means
coverage is incomplete. Read projection is for inventory only: mutation of a
paragraph inside AlternateContent writes the corresponding paragraph in every
branch, and fails closed when branch paragraph structures cannot be aligned.
User-authored footnotes and endnotes are mechanical stories with stable
footnote:{id}:p:{n} and endnote:{id}:p:{n} ids. Separator and continuation-
separator notes are not user segments, and missing note parts are never created.
Comment bodies use comment:{id}:p:{n} segments; document.comments exposes text, authorship, anchors and native parent_id. Text replacement preserves identities and sidecars; empty comments and separator notes are not user segments.
append_comment_reply_bytes(data, target: CommentReplyTarget, text: str, author: CommentAuthor) -> CommentReplyResult appends a native reply.
target = CommentReplyTarget(document_sha256, parent_id) binds the exact native parent to immutable input bytes; stale or ambiguous targets raise CommentMutationError.
document.append_comment_reply(target, text, author) adopts verified bytes; document.publish() uses validated atomic replacement, preserving the destination on failure.
Supported forms are anchored comments, multi-paragraph comments identified by their last paragraph, and nested replies with proved inherited scope.
Existing bodies, IDs, authorship, anchors, links and completion attributes remain intact; absent completion stays absent, and the new reply has done=0 without reopening its parent.
Legacy threads gain missing paragraph identities and native relationships/types; durable commentsIds, extensible comments, multi-story region parents and incomplete graphs fail closed.
CommentReplyResult carries data, verified reply_id/parent_id, complete coverage, comments and an OperationReceipt with input/output hashes, created ID, affected parts and applied status.
Untouched OPC payloads remain byte-exact, including people and unrelated extensions; ZIP containers need not match. Explicit completion mutation is not provided. ReviewKit owns review meaning.
apply_package_transaction(data, mutations, *, validators=(), require_complete_coverage=True) transforms DOCX bytes in memory, without writing
files. PackageMutation requires a unique operation_id, kind, target_id,
and expected_sha256; optional fields are value, cascade, and content_type.
content_type is currently unused. PackageMutationKind supports
REPLACE_SURFACE (string value), REMOVE_SURFACE, REMOVE_RELATIONSHIP,
REPLACE_PART (bytes value), and REMOVE_PART. Surface/relationship targets use
surface_id and value_sha256; part targets use name and sha256.
Removing a part with incoming relationships requires cascade=True, which also
removes those links; part removal cleans its relationship part and type override.
Hashes must match the source inventory. Complete inventory coverage is required
before and after by default. PackageMutationError reports rejected mutations,
incomplete coverage, unexpected changes, or unconfirmed operations; package-read
and validator errors can propagate separately. No receipt or intermediate result
is returned on error. Validators receive the complete package receipt before return.
PackageTransactionReceipt contains data, inventory_before, inventory_after,
operations, surfaces, parts, and the derived unexpected dispositions.
apply_docx_transaction(data, *, text_targets=(), package_mutations=(), validators=(), require_complete_coverage=True) applies strict text replacements
first. Its package stage checks hashes and inventories against the post-text state.
CombinedTransactionReceipt contains data, normalized text_targets, and
package (the package receipt). Neither function updates an existing handle.
This example requires input.docx with a nonempty creator property and a text
segment. The two variants independently use the same original bytes; changing
body text leaves the selected metadata surface unchanged.
from docxtor import (
DocxDocument, PackageMutation, PackageMutationKind, SegmentReplacement,
apply_docx_transaction, apply_package_transaction,
)
document = DocxDocument.open("input.docx")
data = document.to_bytes()
inventory = document.inventory()
author = next(s for s in inventory.surfaces if s.part_name == "docProps/core.xml"
and s.element_qname == "{http://purl.org/dc/elements/1.1/}creator")
mutation = PackageMutation(
"replace-author", PackageMutationKind.REPLACE_SURFACE,
author.surface_id, author.value_sha256, value=author.value + " (edited)",
)
receipt = apply_package_transaction(data, [mutation])
assert not receipt.unexpected
package_bytes = receipt.data
segment = next(s for s in document.segments if s.text)
receipt = apply_docx_transaction(
data,
text_targets=[SegmentReplacement(container_id=segment.container_id, text="Updated text")],
package_mutations=[mutation],
)
combined_bytes = receipt.data
assert not receipt.package.unexpectedDocxDocument.find_text() finds overlapping exact matches across Word run
boundaries while returning source-character offsets in a stable paragraph
segment. Matches stay within one paragraph, and container_id can narrow the
search. Opt into match="normalized" to fold case, common typographic
characters, and whitespace for search only; returned text and offsets still
refer to the original document. replace_match() applies a result only while
its paragraph still matches the captured snapshot, then delegates the edit to
Docxtor's offset-based replacement machinery.
from docxtor import DocxDocument
document = DocxDocument.open("input.docx")
match = document.find_text("thirty days", match="normalized")[0]
document.replace_match(match, "business days")Search stays within paragraphs; callers choose matches and paragraph coordinates.
Consumers that need paragraph/run identity and values can stay independent of
python-docx. paragraph_resolutions includes empty paragraphs in stable global
order across body, table, header, and footer stories. Typed lookup returns
None for an unknown paragraph or run coordinate.
from docxtor import DocxDocument, ParagraphLocator, RunLocator
document = DocxDocument.open("input.docx")
for paragraph in document.paragraph_resolutions:
print(paragraph.identity.container_id, paragraph.paragraph_index, paragraph.value)
for run in paragraph.runs:
print(run.identity.run_index, run.value)
body = ParagraphLocator("body:p:0")
paragraph = document.resolve_paragraph_locator(body)
run = document.resolve_run_locator(RunLocator(body, 0))The resolution DTOs contain only typed identity/value data. Physical traversal
and python-docx objects remain internal to Docxtor.
from docxtor import DocxAdmissionInspection, inspect_docx_admission
report: DocxAdmissionInspection = inspect_docx_admission(input_bytes)DocumentPackageKind.VALID means a normalized word/document.xml member exists, not full OOXML validity; has_main_document reports that presence.
ENCRYPTED takes precedence for an OLE/CFB signature or EncryptedPackage/EncryptionInfo member; the signature alone does not prove encryption.
INVALID means no main document was found, including non-ZIP input.
has_macros detects vbaProject.bin at any package path, not macro safety or all executable content.
page_count is a positive integer from docProps/app.xml's Pages, otherwise None; it is stored metadata, not a rendered count.
diagnostics lists unreadable and unknown parts; non-ZIP input reports <package>, while the OLE shortcut reports ole_encrypted_package with both flags false and no page count.
Inspection does not modify input or decrypt it. Archive-read errors can propagate; the report is not a security or conformance verdict.
uv sync --extra dev
uv run ruff check .
uv run pytest- Replacement text inherits formatting through original text-node spans. Large length changes can move style boundaries.
- Document layout can change when replacement text length changes.
- PDF input must have a text layer. Scanned or image-only PDFs return an OCR-required error.
- PDF output is best-effort because PDF is fixed-layout, not an editable text document.
- Layout-preserving path (preferred): localized replacements are applied with
redaction/overlay boxes on the original pages so page count and positions
stay stable (typical anonymization labels such as
<PERSON>/****). - If a change cannot be located safely for redaction, that page is rebuilt in place (white-out + new text layer) instead of leaking the original text.
- Full flowing rebuild is a last resort when edited text no longer fits the original page boxes (large inserts/expansions). Page count may then shrink or grow, and original geometry is not preserved.
- Exact glyph metrics, fonts, columns, tables, headers/footers, and complex multi-column layouts are not guaranteed after any non-trivial edit.
- Non-ASCII redaction labels may be rendered as
****when a matching Unicode font is unavailable for the overlay.
MIT
SegmentReplacement targets a container_id with optional start_offset and
end_offset, or a nested span_id. apply_replacements(..., strict=True) raises
on unknown targets or bad offsets. Consumers delegate physical OOXML edits here.
Docxtor owns the physical OOXML mechanics for comments and tracked changes. It stays domain-blind: the caller supplies exact locators, ranges, author identity, and the accept/reject decision.
Both renderers take source as a DOCX path (str or Path) or bytes, take
output as the destination path, and return the published Path.
render_physical_review takes a PhysicalReviewPlan and an optional keyword-only
PhysicalReviewer (defaulting to PhysicalReviewer()) to create review markup;
render_physical_clean takes a tuple of PhysicalReviewEdit values and applies
inline edits without new tracked-change markup, skipping edits with new_paragraph=True.
Unknown locators, invalid inline ranges, and mismatched expected_text raise
PhysicalReviewRenderError before publication. Source reading and opening precede
exception handling, so not all input errors are wrapped; neither renderer guarantees
atomicity of the entire call.
Text replacement and deletion through render_physical_review or
render_physical_clean preserve structurally empty inline content controls
(w:sdt with an empty w:sdtContent). Controls inside the changed range retain
source order at the leading boundary of the replacement after acceptance;
rejection restores their original positions among the original runs. Controls
at range boundaries stay outside the edit. Nonempty controls and other opaque
objects remain protected, even when they have no visible text.
For a whole-paragraph insertion rendered by render_physical_review, the new
paragraph inherits the anchor's explicit paragraph style and direct paragraph
formatting, except that direct paragraph w:numPr, section properties, identity
attributes, and tracked property-change snapshots are not copied. Style-level
numbering can still apply through the inherited paragraph style. The inserted
text uses direct run properties from the first text run outside deleted or
moved-from revisions that has no enabled direct w:vanish (w:val="0",
"false", and "off" disable it; a missing value enables it). Visibility
inherited through paragraph or character styles is not resolved. If that run
has no effective properties after revision history is removed, paragraph-mark
run properties are used when present; otherwise the paragraph style's character
defaults apply. When the anchor has no explicit paragraph style, the inserted
paragraph leaves its style unset so the document's default paragraph style
applies.
from docxtor import (
CommentAuthor, CommentRange,
DocxDocument,
RevisionAuthor, RevisionRange,
accept_all_revisions_bytes,
reject_all_revisions_bytes,
)
doc = DocxDocument.open("input.docx")
author = CommentAuthor("Reviewer", "RV", "2026-01-01T00:00:00Z")
reviewer = RevisionAuthor("Reviewer", "2026-01-01T00:00:00Z")
added = doc.add_comment(
CommentRange("body:p:0", 0, 5, expected_text="Hello"),
"Check this range",
author,
)
doc.update_comment(added.receipt.created_ids[0], "Revised note")
doc.replace_revision(
RevisionRange("body:p:0", 6, 11, expected_text="world"),
"there",
reviewer,
)
doc.remove_comments({added.receipt.created_ids[0]})
doc.publish()
accepted = accept_all_revisions_bytes(doc.to_bytes(), drop_comments=False)
rejected = reject_all_revisions_bytes(doc.to_bytes(), drop_comments=False)DocxDocument is the open DOCX handle. Comment add/update/remove and tracked
insert/delete/replace (w:ins / w:del) run on that same file.
Repeated add_comment calls on the same sentence, including a span that already
contains native comment markers, each add another Word comment
(w:commentRangeStart / w:commentRangeEnd / comment part).
Tracked insert/delete/replace wrap the named characters when that span sits
in the middle of an existing comment, revision, bookmark, or hyperlink, and
refuse instead of reporting success for a range they did not wrap.
publish() with no arguments writes back to the path passed to
DocxDocument.open. If the handle was opened from bytes and has no path,
publish() fails closed. Docxtor does not invent a second review markup format.
inventory_review_markup() and inventory_revisions_bytes() distinguish an
empty document from incomplete coverage. Unsupported structural revisions fail
closed. apply_review_batch() returns no intermediate bytes if any command
fails.
RevisionInventory.tracking_enabled reports enabled w:trackRevisions settings
throughout the package, including orphan or relocated XML settings parts. It
recognizes the OOXML on/off values. Malformed, duplicated, misplaced or
unsupported tracking settings make coverage incomplete. Bulk acceptance and
rejection require complete revision inventories before and after transformation.
accept_all_revisions_bytes(data, *, drop_comments=True, disable_tracking=False)
and reject_all_revisions_bytes(data, *, drop_comments=True) return receipts with
output bytes and before/after inventories. Both remove comments by default;
drop_comments=False preserves them. The examples independently accept or reject
revisions from the same source bytes. Only acceptance supports disable_tracking=True;
it disables future tracking while leaving other settings and personal metadata intact.
DocxDocument.review_geometry() inventories body paragraphs, tables, headers,
footers, user notes, and paragraph-bearing text boxes. Comment bodies remain
separate context stories. Separator notes are excluded. The geometry carries
an exact source SHA-256 and ordered spans for each physical story.
physical_span_for_semantic_range() maps effective text through typed
PhysicalTextOrigin records. Inserted text participates in the effective view.
Deleted text keeps its raw source coordinates. A mapped range can include
intervening deleted text without removing or accepting that revision.
DocxDocument.add_physical_comment_regions(regions, text, author, document_sha256=source_sha256) creates one native comment for each story region.
Each region is a tuple of PhysicalCommentSpan values. The operation validates
all regions before mutation and adopts only the complete candidate. Call
publish() to atomically write the resulting handle.
The comments store their shared regionSet identity and regionSetSize in a
Docxtor namespace inside DOCX. AddressableComment.region_set_id and
region_set_size expose these values after reload. Missing members and
conflicting sizes make review inventory incomplete. No sidecar file is used.
Word ignores commentReference inside headers, footers, text boxes, footnotes,
and endnotes. This behavior is documented in
MS-OI29500 §17.13.4.5.
New comments on these stories use a visible native body display anchor and a
native hyperlink to a hidden bookmark at the exact logical target. Notes use
their body reference. Text boxes use their body host. Headers and footers use
their section reference. No body text or paragraph identity is invented.
PhysicalCommentAnchor.spans exposes the logical target. display_spans exposes
the native body display range. display_visible distinguishes a body display
anchor from incoming non-body native comments, which remain unchanged.
PhysicalReviewGeometry.scope_bookmarks exposes validated CommentScopeBookmark
ownership. Missing links, duplicate identities, incomplete groups, and targets
that cross stories fail closed. Multi-story groups encode their identity and
member count in native bookmark names, including their body member.
AddressableComment.text contains the actual Word comment text, including its
scope hyperlink label. user_note_text contains the remark without that label
only when the provider has validated the native bridge. Updating one member
updates the logical region set and retains each native scope link. Cleanup
removes only proved owned bookmarks and reports them in
OperationReceipt.removed_scope_bookmarks. Existing user bookmarks stay intact.
These structural guarantees do not claim a completed live Word rendering check.
A native reply without markers inherits its parent's proved physical scope.
The parent graph rejects unresolved identities, cycles, and conflicting scopes.
Partial markers cannot inherit a scope. PhysicalCommentAnchor.anchor_evidence
reports the proved explicit or inherited anchor. The older
AddressableComment.anchor_evidence reports marker or parent-graph provenance
without changing its legacy locator and anchor-text fields.
remove_comments() expands a selection to its complete native region sets and
all descendant replies. It removes their range markers and thread identities in
one transaction. OperationReceipt.removed_ids lists the complete selection.
Unselected comment bodies, thread records, and native revisions remain intact.
DocxDocument.map_review_spans(source_sha256, spans) follows native mutation
receipts on the same open handle. It returns current raw envelopes for captured
source spans. A tracked deletion retains its raw range. A tracked insertion
expands the envelope at its proved source position. PhysicalTextInsertion
records carry the actual physical insertion offset, including insertions next
to existing deleted text. Unknown mutations and broken snapshot chains raise.
apply_review_batch() returns its typed receipts and records the same mapping.
This history is transient execution state and is not a second document format.
inspect_revision_dispositions(bytes) inventories every XML OPC member,
including comment stories, alternative XML branches and orphan parts. Each
immutable target identifies the package part, namespace, revision kind, Word ID,
physical XPath and subtree hash. Its neutral payload text, paragraph XPath and
paragraph text provide context without deciding review meaning. The inventory
also identifies the exact input bytes and its complete set of records with
SHA-256 hashes.
Pass that inventory and explicit RevisionDecision records to
dispose_revisions_bytes. The caller chooses every decision; Docxtor does not
derive authority from authorship, text or residual Word markup.
from docxtor import (
RevisionDecision,
RevisionDisposition,
dispose_revisions_bytes,
inspect_revision_dispositions,
read_package_entries,
write_package_atomically,
)
inventory = inspect_revision_dispositions(input_bytes)
# target_ids are an explicit selection made by the caller from this inventory.
result = dispose_revisions_bytes(
input_bytes,
inventory,
[RevisionDecision(target_id, RevisionDisposition.ACCEPT) for target_id in target_ids],
)
assert all(receipt.resolved for receipt in result.receipts)
write_package_atomically("resolved.docx", read_package_entries(result.data))Supported selections are transitional w:ins and w:del wrappers directly
inside a paragraph or hyperlink, containing ordinary runs with text, tabs and
line breaks. Accepting an insertion or rejecting a deletion retains its runs;
rejecting an insertion or accepting a deletion removes its runs. Restoring a
deletion converts only that wrapper's w:delText to w:t. Formatting and all
surrounding markup are preserved.
Two directly adjacent opposite-kind wrappers form a conservative atomic inline
replacement group, recorded by group_id and required_target_ids. This is a
physical adjacency rule, without authorship or provenance inference. Both
targets must be selected with the same disposition; larger mixed chains and
pairs containing an unsupported member are preserve-only. Independent supported
targets can receive mixed accept/reject decisions in one call, even in one
paragraph.
Move/range, paragraph-mark, property, nested, alternative-branch and opaque-content revisions are inventoried as preserve-only. Selecting one fails closed. An unknown/duplicate target, absent/duplicate Word identity, stale or modified inventory, partial/conflicting replacement group, or incomplete XML coverage fails before output is returned. Valid range start/end markers may share a Word ID, while repeated endpoints or repeated wrapper IDs are ambiguous.
The result contains typed complete before/after inventories, per-selected-target
receipts, output bytes and their SHA-256, and the changed part names. Postflight
reopens the output and verifies the complete planned XML transformation, every
unselected revision subtree in source order, original OPC members and metadata,
and byte-exact untouched part payloads. Unsupported unselected revisions remain
intact. No intermediate or partially resolved output is returned. The existing
write_package_atomically publication API validates a staged package before one
atomic destination replacement; its optional validate callback can enforce
the caller's publication policy before replacement.
publish() serializes in memory, preserves semantically unchanged source XML,
normalizes ZIP timestamps, validates the complete package, runs optional
validators, and performs one atomic replace. The returned PublishReceipt
identifies the exact published bytes. A failure before replacement leaves an
existing destination byte-for-byte unchanged.
The Python library does not depend on .NET or the Open XML SDK. To independently
validate a generated DOCX, install the pinned .NET SDK and run the opt-in
validator from validation/openxml/:
dotnet run --project validation/openxml/Docxtor.OpenXmlValidation.csproj -- output.docxThe validator targets the explicitly configured Office 2019 file-format profile and emits structured findings (error ID/type, description, part URI, XPath when available), with a bounded error count. Package-open and validator execution failures are distinct from conformance findings and return non-zero. This is an independent OOXML conformance check, not a rendering, business-semantics, or complete Microsoft Office behavior guarantee; it never modifies the document.
DocxDocument.facts() and compare_docx(before, after) provide complete,
typed facts snapshots for package structure, OPC relationships, container
coordinates, fields, bookmarks, and stories.
from docxtor import compare_docx, docx_facts
before_facts = docx_facts("before.docx")
after_facts = docx_facts("after.docx")
comparison = compare_docx(before_facts, after_facts)
if not comparison.compliant:
print(comparison.changes)compare_docx detects lost containers, broken relationships, part drift, and
unintended surface modifications without parsing raw XML in consumer code.