AiOffice
AiOffice is an AI-native, declarative document engine. It lets an agent describe the document it wants, validates that intent as a strict spec, and compiles it into office formats without exposing low-level Word object APIs.
The 0.1.0 release is an intentionally small, usable vertical slice of the larger
AiOffice architecture:
- strict AiOffice Document Spec 1.0 draft models;
- stable semantic node IDs;
- a Python API and convenience builder;
- JSON and Markdown input;
- JSON, Markdown, semantic HTML, and DOCX output;
- machine-readable validation diagnostics;
- atomic, revision-checked document patches;
- a CLI shared with the Python core.
The development branch is now 0.2.0.dev83. It adds lossless DOCX opening, semantic
projection over a native package, persistent native identities, local revision
workspaces, copy-on-write native parts, exact text-range formatting, AI-addressable
named styles, document defaults, ordered page/section models, reusable header/footer
parts, structured dynamic fields, explicit table geometry, logical merged cells,
rich table-cell paragraphs, explicit table/cell border control, paragraph
background/border surfaces, conservative body and header/footer image projection,
verified asset extraction, selective native image metadata and geometry updates,
bounded rectangular source cropping, native picture rotation and
horizontal/vertical mirroring, direct-RGB picture outlines with preset dash styles,
fixed picture opacity with native thousandth-percentage precision,
direct-RGB picture outer shadows with explicit native geometry,
strict inline DrawingML/VML mc:AlternateContent compatibility evidence and
strict offset-floating compatibility projection plus dual-branch resizing,
conservative offset/alignment/percentage floating-image anchor projection,
optional Office 2010 relative width/height rules with absolute extent fallbacks,
square/no-wrap/top-and-bottom/tight/through text wrapping, selective floating-anchor layout
updates plus positioning and wrapping-mode switches,
occurrence-scoped copy-on-write image replacement, addressable native inline and
floating image insertion, direct image-paragraph layout formatting, semantic diffs,
isolated LibreOffice/Poppler native rendering, safe reusable native header/footer
creation, cloning, and binding, root append plus bidirectional stable-ID native
paragraph/heading/page-break/list/table insertion and block reordering, consistent
multi-page evidence, page occupancy diagnostics, visual-regression contracts,
fidelity reports, and an independent byte-level fidelity verifier that proves only
the intended native parts changed and that the result is still valid OPC.
Workbook, presentation, PDF editing, and MCP remain planned.
Install
pip install aioffice
AiOffice requires Python 3.11 or newer.
Python quick start
from aioffice.documents import DocumentBuilder
doc = (
DocumentBuilder(title="Project Report", theme="business-clean")
.heading("Project Report", id="report_title")
.paragraph("The first delivery milestone is complete.", id="status")
.bullet_list(["Validated spec", "Generated DOCX", "Published HTML preview"])
.build()
)
validation = doc.validate()
assert validation.valid
doc.export("report.json")
doc.export("report.md")
doc.export("report.html")
doc.export("report.docx")
Open an existing DOCX without rebuilding its unknown or unsupported parts:
import aioffice
doc = aioffice.open("existing.docx", roundtrip="preserve_unknown")
assert doc.origin == "native"
result = doc.apply([
{
"op": "text.replace",
"target": "#para_000001",
"search": "Draft",
"replacement": "Approved",
}
], dry_run=True)
assert result.success
print(result.fidelity)
result.document.export("updated.docx")
Exporting an imported DOCX without changes returns the exact original package bytes. When a supported edit is applied, AiOffice rewrites only the affected native part and preserves untouched part payloads.
Every native patch also attaches an independent, machine-readable fidelity proof.
Instead of trusting the copy-on-write bookkeeping, AiOffice re-reads the source and
result bytes, hashes every part, and reports the byte-level truth so an agent can gate
its commit on verification.verified:
verification = result.verification
assert verification.verified # only intended parts changed, OPC valid
assert "/word/document.xml" in verification.modified_parts
assert verification.undeclared_changes == [] # nothing changed unexpectedly
assert verification.opc_valid # content types + relationships resolve
# Prove the current native document re-exports exactly, or compare two packages:
snapshot = doc.verify_fidelity()
assert snapshot.byte_identical
Untouched parts are proven byte-identical (not merely counted), modified/added/removed
parts are classified explicitly, any change outside the declared affected set is flagged
in undeclared_changes, and the result package's content-type coverage and internal
relationship targets are validated. See
native fidelity verification.
Image bytes deliberately stay out of the JSON Spec. A simple body, header, or footer
paragraph containing exactly one supported embedded DrawingML picture is projected
as an AI-addressable image block with inline or conservative floating placement,
physical extent, optional rectangular source crop, transform, direct-RGB outline,
opacity and outer shadow, alternative text, media type, filename, byte count, and
SHA-256 asset identity:
image = next(
node for node in doc.inspect()["nodes"]
if node["type"] == "image"
)
verified = doc.read_image(image["id"])
assert verified.sha256 == image["asset"]["sha256"]
verified.write("extracted/" + verified.filename)
Body images appear in inspect()["nodes"]; reusable header/footer images appear in
inspect()["header_footers"][...]["blocks"]. Both use the same stable image ID and
verified read, update, replacement, and paragraph-layout APIs. Header/footer
insertion and deletion are not advertised: create or clone the complete reusable
part first, then update or replace its projected image in a subsequent Patch.
For a supported floating picture, image["floating"] preserves explicit horizontal
and vertical reference frames with offset, alignment, or percentage positioning, supported
square/no-wrap/top-and-bottom/tight/through wrapping, optional parent-anchor
distances, separate wrap-local distances, parent and wrap-child effect extents,
ordered native tight/through polygons, relative height, behind-text behavior,
anchor locking, cell layout, and overlap policy.
image.anchor.update can selectively change or switch those proven groups without
rebuilding the drawing or touching its image bytes, relationship, crop, extent,
accessibility metadata, or Office 2010 anchor identities. Native DOCX rendering
remains the visual authority.
result = doc.apply([
{
"op": "image.anchor.update",
"target": f"#{image['id']}",
"set": {
"horizontal": {
"relative_to": "page",
"offset": {"value": 1, "unit": "in"},
},
"vertical": {
"relative_to": "paragraph",
"offset": {"value": 12, "unit": "pt"},
},
"behind_text": False,
"allow_overlap": True,
},
}
])
assert result.success
Horizontal, vertical, and wrap changes replace their complete grouped value; scalar flags and relative height remain independently selectable. The operation requires an attached native DOCX and an image already projected as a supported floating anchor—it never converts an inline or opaque drawing.
Supported projected images can be resized, cropped, transformed, outlined, made translucent, given an outer shadow, or given accessible metadata without rewriting their binary part or relationship:
result = doc.apply([
{
"op": "image.update",
"target": f"#{image['id']}",
"set": {
"width": {"value": 3, "unit": "in"},
"crop": {
"left": 12.5,
"top": 5,
"right": 12.5,
"bottom": 5,
},
"outline": {
"width": {"value": 1.5, "unit": "pt"},
"color": "#2457A7",
"dash": "solid",
},
"opacity": 72.5,
"shadow": {
"color": "#000000",
"opacity": 40,
"blur_radius": {"value": 6, "unit": "pt"},
"distance": {"value": 3, "unit": "pt"},
"direction_degrees_clockwise": 45,
"alignment": "center",
"rotate_with_shape": False,
},
"alt_text": "Quarterly revenue by region",
"title": "Revenue chart",
},
}
])
assert result.success
result.document.export("updated.docx")
Setting one dimension preserves the current aspect ratio; setting both applies the
exact requested extent. Crop edges are percentage points of the original source,
quantized to Word's 0.001-percentage-point precision. The crop object is replaced
as a whole; omitted edges mean zero. Use "clear": ["crop"] to reveal the complete
source. Outline width is quantized to the nearest native EMU; color is one explicit
sRGB hex value and dash is one DrawingML preset exposed by the
image-outline schema. Both transform and outline are complete replacement groups.
Use "clear": ["outline"] to remove the direct line. Opacity is expressed in
percentage points from 0 inclusive to 100 exclusive, quantized to 0.001; clear it
to restore fully opaque identity. Shadow is a complete replacement group backed by
one direct a:effectLst/a:outerShdw; clear it to remove the effect. It accepts one
direct sRGB color, 0.001-percentage-point opacity, explicit blur/distance lengths,
clockwise direction, nine-point alignment, rotate-with-shape behavior, and optional
four-edge inline effect-extent layout evidence. Floating shadows reuse
floating.anchor_effect_extent.
alt_text and title are also clearable. The
native patch updates the minimal DrawingML geometry while preserving image bytes and
package relationships. It requires the attached native DOCX, so a detached JSON
snapshot cannot perform this operation.
LibreOffice 26.8 renders the tested direct black outer shadow, retains its native
effect on save, and may quantize shadow lengths or add effect-extent evidence. It
still ignores the tested a:alphaModFix picture opacity. A LibreOffice save may
also wrap a drawing in mc:AlternateContent. Dev49 projects only the strictly
proven Requires="wps" DrawingML choice plus canonical VML picture fallback,
including a bounded floating form whose physical column/paragraph offsets, square
wrap, zero distances, anchor identity, and VML margins agree. The proof is exposed
as alternate_content, and only width/height are synchronized across both
branches. Replacement is advertised only when the VML fallback bytes and media type
equal the DrawingML choice. image.anchor.update, crop, effects, accessibility
fields, alignment/percentage-positioned wrappers, and unfamiliar fallback
structures remain fail-closed. A header/footer containing a strictly projected
wrapper can be cloned: AiOffice copies its complete local relationship graph,
shares both media targets, rebases DrawingML and VML occurrence identities, and
preserves the lexical wps namespace required for compatibility selection. AiOffice
preserves untouched native XML exactly;
Microsoft Word/Office remains the final authority for cross-producer visual
approval.
The projected image ID also addresses its native host paragraph. Reuse
paragraph.format to control layout around an existing picture without touching its
DrawingML or bytes:
result = doc.apply([
{
"op": "paragraph.format",
"target": f"#{image['id']}",
"set": {
"alignment": "center",
"spacing_before": {"value": 10, "unit": "pt"},
"spacing_after": {"value": 12, "unit": "pt"},
"keep_together": True,
},
}
])
assert result.success
The same strict ParagraphStyle fields and set/clear semantics used by text
paragraphs apply to the image paragraph, including indentation, page-flow controls,
solid background, and supported physical borders. The operation appears on each
projected image's supported_operations list.
Image binaries also use an explicit out-of-band write path:
result = doc.replace_image(
image["id"],
"assets/revenue-chart.png",
media_type="image/png",
)
assert result.success
result.document.export("replaced.docx")
AiOffice signature-checks and bounds the raster input, creates a content-addressed native image part and a new relationship for only that occurrence, and preserves its stable image ID, displayed extent, source crop, transform, outline, opacity, shadow, alternative text, and title. Other occurrences that shared the old image remain unchanged. Raw JSON Patch cannot carry the binary.
New inline or conservative floating pictures use the same bounded asset channel and require explicit layout:
result = doc.insert_image_after(
"#status",
"assets/expert-workflow.png",
width={"value": 3, "unit": "in"},
height={"value": 1.5, "unit": "in"},
alt_text="Expert workflow with three approval stages",
image_id="expert_workflow",
outline={
"width": {"value": 1, "unit": "pt"},
"color": "#2457A7",
"dash": "solid",
},
opacity=85,
shadow={
"color": "#000000",
"opacity": 40,
"blur_radius": {"value": 6, "unit": "pt"},
"distance": {"value": 3, "unit": "pt"},
},
paragraph_style={"alignment": "center"},
)
assert result.success
result.document.export("inserted.docx")
Inline remains the default. Pass the same strict layout object used by projected floating pictures to create a canonical editable anchor:
result = doc.insert_image_after(
"#status",
"assets/expert-workflow.png",
width={"value": 3, "unit": "in"},
height={"value": 1.5, "unit": "in"},
alt_text="Expert workflow with three approval stages",
image_id="floating_workflow",
floating={
"horizontal": {
"relative_to": "column",
"offset": {"value": 1, "unit": "in"},
},
"vertical": {
"relative_to": "paragraph",
"offset": {"value": 12, "unit": "pt"},
},
"anchor_distances": {
"top": {"value": 4, "unit": "pt"},
"right": {"value": 8, "unit": "pt"},
"bottom": {"value": 4, "unit": "pt"},
"left": {"value": 8, "unit": "pt"},
},
"wrap": {
"mode": "square",
"side": "both_sides",
},
"relative_height": 1024,
"behind_text": False,
"locked": False,
"layout_in_cell": True,
"allow_overlap": True,
},
)
Each horizontal or vertical position uses exactly one mode: an explicit-unit
offset, a semantic alignment, or percentage_offset in percentage points of
the selected frame. Percentage values have 0.001-percentage-point native
precision and may be negative. For example, a picture centered within the margins
and the physical page uses:
floating = {
"horizontal": {"relative_to": "margin", "alignment": "center"},
"vertical": {"relative_to": "page", "alignment": "center"},
# The same complete wrap and flag fields shown above are still required.
}
The Office 2010 percentage form is equally explicit:
floating = {
"horizontal": {"relative_to": "page", "percentage_offset": 50},
"vertical": {"relative_to": "margin", "percentage_offset": 12.5},
# The same complete wrap and flag fields shown above are still required.
}
Floating pictures may also carry independent Office 2010 relative-size rules:
floating = {
"horizontal": {"relative_to": "page", "alignment": "center"},
"vertical": {"relative_to": "page", "alignment": "center"},
"relative_size": {
"width": {"relative_to": "margin", "percentage": 75},
"height": {"relative_to": "page", "percentage": 40},
},
# The same complete wrap and flag fields shown above are still required.
}
relative_size.width and .height are independent; at least one is required.
Percentages use 0.001-percentage-point native precision and cannot be negative.
The image node's ordinary width and height remain the exact wp:extent
fallback, so a relative rule never destroys the producer's absolute geometry.
wrap.mode accepts square, none, top_and_bottom, tight, or through.
Square, tight, and through require side; the other modes must omit it.
Tight/through additionally require an ordered polygon whose coordinates remain raw
OOXML signed integers rather than being mislabeled as physical lengths.
anchor_distances preserves the optional
distance attributes on wp:anchor. Square and top-and-bottom wrap may separately
carry wrap-element distances and effect_extent; anchor_effect_extent preserves
the parent value. AiOffice keeps these native sources distinct because the wrap
child extent overrides the parent for its wrapping boundary.
For none, behind_text chooses whether the picture is behind or in front of text.
The target must be a mapped top-level body node. AiOffice inserts after its last native element, which keeps multi-paragraph lists addressable as one semantic node. The new paragraph, DrawingML geometry, relationship, asset and identity manifest are created atomically. Explicit width, height and nonblank alternative text avoid model-side DPI guessing and inaccessible output.
The equivalent CLI is:
aioffice extract-image existing.docx IMAGE_ID -o extracted.png
aioffice replace-image existing.docx IMAGE_ID replacement.png -o replaced.docx
aioffice insert-image-after existing.docx TARGET replacement.png \
--width 3 --width-unit in --height 1.5 --height-unit in \
--alt-text "Expert workflow" --outline image-outline.json \
--opacity 85 --shadow image-shadow.json \
--floating-layout floating-layout.json \
-o inserted.docx
Persistent workspaces expose the same operations through
Workspace.replace_image(...), Workspace.insert_image_after(...), and matching
CLI commands, recording verified asset and insertion metadata but never base64 in the
revision log.
The read path re-resolves the trusted native paragraph and its story-local OPC relationship, then verifies the asset record, media type, size, and content hash before returning bytes. Mixed text/picture paragraphs, active simple-position anchors, malformed relative-size rules, unsupported wrap-specific effects, linked images, multiple pictures, negative or overconstrained crop rectangles, malformed transforms, unsupported outline fills, joins, compound lines, arrowheads or custom dashes, malformed opacity, unsupported or malformed shadows, other picture effects, drawings in tables, complex header/footer drawings, standalone or unrecognized VML, OLE, and embedded objects remain explicit opaque native content. They are preserved losslessly and rendered through the native provider rather than flattened into a misleading image model. See the native image and asset contract.
AiOffice-generated DOCX files embed a versioned identity manifest. Artifact IDs, semantic node IDs, native anchors, and revisions therefore survive export and reopen. Third-party documents can keep the same guarantees through a local workspace:
from aioffice import Workspace
workspace = Workspace.init("project")
doc = workspace.import_document("existing.docx")
result = workspace.apply(
doc.id,
[{
"op": "text.replace",
"target": f"#{doc.to_spec()['content'][0]['id']}",
"search": "Draft",
"replacement": "Approved",
}],
base_revision=doc.revision,
idempotency_key="approve-first-paragraph",
)
assert result.success
revision_one = workspace.checkout(doc.id, revision=1)
revision_two = workspace.open_document(doc.id)
Use workspace.reconcile_document(...) to preview an externally edited DOCX. A
commit is refused when native identity is ambiguous. The detailed invariants are in
the native round-trip architecture.
Native DOCX lowering in this development version supports text.replace,
paragraph.format, text.format, node.remove, style.define, style.apply,
style.format, section.format, field.update, image.insert_after,
image.replace, image.update, image.anchor.update, table.format,
table.column.format, and table.cell.format. Ask the artifact before planning an
edit:
capabilities = doc.capabilities()
assert "text.replace" in capabilities["operations"]
Formatting values always include units. This prevents an agent from confusing points, pixels, inches, and native OOXML twips:
result = doc.apply([
{
"op": "paragraph.format",
"target": "#para_000001",
"set": {
"alignment": "justify",
"spacing_after": {"value": 8, "unit": "pt"},
"line_spacing": {"rule": "multiple", "value": 1.25},
},
},
{
"op": "text.format",
"target": "#para_000001",
"match": {
"text": "重要结论",
"occurrence": 1,
},
"set": {
"font_size": {"value": 10.5, "unit": "pt"},
"color": "#1F4E78",
},
},
])
assert result.success
print(result.diff.summary)
text.format can target the whole node, an exact text occurrence, or a half-open
Unicode code-point range such as
{"range": {"start": 4, "end": 10, "unit": "unicode_codepoint"}}. Imported
mixed Word runs and hyperlinks are projected as rich TextSpan content, so an
agent can inspect and edit local formatting without losing link targets.
Named styles are stable, AI-addressable layout rules rather than copied formatting.
The resolver applies document defaults, the complete based_on chain, node direct
formatting, and finally span formatting:
doc = (
DocumentBuilder()
.define_style({
"id": "Executive",
"name": "Executive",
"semantic_role": "custom",
"based_on": "Normal",
"paragraph_style": {
"background_color": "#EAF2F8",
"borders": {
"left": {
"style": "single",
"width": {"value": 3, "unit": "pt"},
"color": "#1F4E78",
"space": {"value": 8, "unit": "pt"},
},
},
"spacing_after": {"value": 14, "unit": "pt"},
"keep_together": True,
},
"text_style": {
"font_size": {"value": 13, "unit": "pt"},
"color": "#7A1F5B",
"bold": True,
},
})
.paragraph("Board decision", id="decision", style_ref="Executive")
.build()
)
result = doc.apply([
{
"op": "style.format",
"target": "@Executive",
"paragraph": {
"set": {"spacing_after": {"value": 18, "unit": "pt"}}
},
"text": {
"set": {"color": "#1F4E78"},
"clear": ["bold"],
},
}
])
Imported w:style definitions, w:docDefaults, inheritance links, quick-style
metadata, and paragraph w:pStyle references are projected into the Spec. Native
style patches update only supported properties in word/styles.xml; unknown style
XML and every untouched package part remain byte-for-byte preserved.
paragraph_style.background_color creates a solid paragraph-wide surface.
paragraph_style.borders controls top/right/bottom/left edges with the same strict
border line model used by tables. Border edges inherit independently through the
named-style chain: a direct bottom edge can override a style while its other edges
continue to inherit. Clearing removes direct XML; style: "none" explicitly
suppresses an inherited edge. Pattern/theme shading and Word's between/bar
borders remain native-only and losslessly preserved. See
the paragraph surface contract.
Sections are ordered, AI-addressable page regions. The first section starts at the document root; each later section is anchored at its first content node. Page size, orientation, margins, gutter, header/footer distance, columns, vertical alignment, first-page behavior, and Word section-start type all use strict values:
doc = DocumentBuilder(
sections=[
{
"id": "cover_section",
"start_at": None,
"layout": {
"page_size": {"preset": "letter"},
"margin_top": {"value": 1, "unit": "in"},
"margin_right": {"value": 1, "unit": "in"},
"margin_bottom": {"value": 1, "unit": "in"},
"margin_left": {"value": 1, "unit": "in"},
},
},
{
"id": "analysis_section",
"start_at": "analysis",
"layout": {
"start_type": "next_page",
"page_size": {
"preset": "a4",
"orientation": "landscape",
},
"columns": {
"count": 2,
"spacing": {"value": 24, "unit": "pt"},
"separator": True,
},
"page_number_start": 1,
"page_number_format": "lower_roman",
},
},
]
).paragraph("Cover", id="cover").paragraph("Analysis", id="analysis").build()
result = doc.apply([
{
"op": "section.format",
"target": "#analysis_section",
"set": {"margin_left": {"value": 18, "unit": "mm"}},
"clear": ["footer_distance"],
}
])
Split an existing section before a stable top-level content node when the following content needs a different page design:
result = doc.apply([
{
"op": "section.insert_before",
"target": "#wide_appendix",
"section": {
"id": "wide_appendix_section",
"layout": {
"page_size": {
"preset": "a4",
"orientation": "landscape",
},
"margin_left": {"value": 18, "unit": "mm"},
"margin_right": {"value": 18, "unit": "mm"},
},
},
}
])
The new section inherits the containing section's remaining layout and
header/footer bindings, defaults to a next_page break, and can be formatted or
prepended to later in the same Patch. Imported DOCX lowering inserts one hidden
boundary paragraph before the target's complete native range, copies the old
section properties for the preceding content, and patches only the requested
properties on the existing boundary. See
native section insertion.
Generated multi-section DOCX uses Word's native section placement rules. Imported
paragraph-level and final body-level w:sectPr elements are projected separately,
and section.format changes only the selected native section properties. Unknown
section XML remains native and untouched. See
the page and section contract.
Headers and footers use reusable parts rather than copied strings. Each section may
explicitly bind default, first, and even header/footer variants; a missing
binding means “inherit the same slot from the previous section”:
doc = DocumentBuilder(
settings={"even_and_odd_headers": True},
header_footers=[
{
"id": "report_header",
"kind": "header",
"content": [
{
"id": "report_header_text",
"type": "paragraph",
"text": "Confidential",
}
],
},
{
"id": "report_footer",
"kind": "footer",
"content": [
{
"id": "report_footer_text",
"type": "paragraph",
"content": [
{"text": "Page "},
{
"id": "current_page",
"type": "field",
"kind": "page_number",
"cached_result": "1",
},
{"text": " of "},
{
"id": "total_pages",
"type": "field",
"kind": "page_count",
"cached_result": "1",
},
],
}
],
},
],
sections=[
{
"id": "main_section",
"header_footer": {
"header_default": "report_header",
"footer_default": "report_footer",
},
}
],
).paragraph("Report body", id="body").build()
An imported DOCX can receive a new reusable region and bind it in one atomic Patch:
result = doc.apply([
{
"op": "header_footer.create",
"part": {
"id": "appendix_header",
"kind": "header",
"content": [
{
"id": "appendix_header_text",
"type": "paragraph",
"text": "Appendix",
}
],
},
},
{
"op": "section.header_footer.bind",
"target": "#appendix_section",
"set": {"header_default": "appendix_header"},
},
])
Native lowering allocates a collision-free part URI, document relationship, content-type override, optional hyperlink relationships, and persistent identities. It preserves every unrelated package part and refuses caller-supplied native references. See native header/footer creation.
When a section shares a native region that must diverge, clone the reusable part instead of reconstructing it from JSON:
result = doc.apply([
{
"op": "header_footer.clone",
"target": "#report_header",
"part": {
"id": "appendix_header",
"metadata": {"role": "appendix"},
},
},
{
"op": "section.header_footer.bind",
"target": "#appendix_section",
"set": {"header_default": "appendix_header"},
},
])
Native cloning copies the complete supported headerN.xml or footerN.xml story
and its part-local relationships, shares relationship targets such as images, and
rebases paragraph and DrawingML identities that must be unique. The source remains
byte-for-byte unchanged. Edit the cloned part in a subsequent Patch. See
native header/footer cloning.
An existing section can explicitly reuse another projected native region, or clear one slot so Word inherits it from the previous section:
result = doc.apply([
{
"op": "section.header_footer.bind",
"target": "#appendix_section",
"set": {
"header_default": "appendix_header",
"footer_default": "report_footer",
},
"clear": ["header_first"],
}
])
For imported DOCX, the operation changes only the selected
w:headerReference/w:footerReference children. It never reconstructs the region,
copies content, or rewrites document relationships. The part must already be
projected in the document and have exactly one internal, type-compatible
relationship. See native header/footer binding.
The paragraph IDs inside a header/footer are regular edit selectors. text.replace,
text.format, and paragraph.format lower directly into the referenced
headerN.xml or footerN.xml part. A conservative one-picture inline paragraph is
instead exposed by its image ID for image.update, paragraph.format, verified
extraction, and occurrence-scoped replace_image(). PAGE, NUMPAGES, SECTION, and
SECTIONPAGES are structured fields with their own stable IDs. Their displayed result
is explicitly a non-authoritative cache:
result = doc.apply([
{
"op": "field.update",
"target": "#current_page",
"set": {"number_format": "upper_roman"},
}
])
Generated fields are marked dirty and update_fields_on_open is enabled unless
explicitly disabled. Unknown field instructions remain structured but read-only;
complex drawings, objects, tables, and malformed field structures remain opaque. See
the dynamic field contract and
the header/footer contract.
Document tables keep semantic column keys and stable column/row/cell IDs while
exposing layout geometry in explicit units. The values form remains a compact
input shorthand and is normalized to cells:
table_doc = DocumentBuilder().table(
id="metrics",
columns=[
{
"id": "metric_column",
"key": "metric",
"title": "Metric",
"width": {"value": 120, "unit": "pt"},
},
{
"id": "value_column",
"key": "value",
"title": "Value",
"data_type": "number",
"width": {"value": 180, "unit": "pt"},
},
],
rows=[
{
"id": "revenue_row",
"values": {"metric": "Revenue", "value": 42},
"allow_break_across_pages": False,
}
],
layout={
"preferred_width": {"mode": "percent", "value": 90},
"alignment": "center",
"algorithm": "fixed",
"repeat_header": True,
"cell_margin_left": {"value": 6, "unit": "pt"},
"cell_margin_right": {"value": 6, "unit": "pt"},
"borders": {
"top": {
"style": "single",
"width": {"value": 1.5, "unit": "pt"},
"color": "#1F4E78",
},
"right": {
"style": "single",
"width": {"value": 1.5, "unit": "pt"},
"color": "#1F4E78",
},
"bottom": {
"style": "single",
"width": {"value": 1.5, "unit": "pt"},
"color": "#1F4E78",
},
"left": {
"style": "single",
"width": {"value": 1.5, "unit": "pt"},
"color": "#1F4E78",
},
"inside_horizontal": {
"style": "single",
"width": {"value": 0.5, "unit": "pt"},
"color": "#D9E2F3",
},
"inside_vertical": {"style": "none"},
},
},
).build()
result = table_doc.apply(
[
{
"op": "table.column.format",
"target": "#metrics",
"column": "#value_column",
"set": {"width": {"value": 200, "unit": "pt"}},
}
]
)
Logical cells can span rows or columns, contain multiple rich paragraphs, and carry cell-local formatting:
from aioffice import Document
rich_table = Document.from_spec({
"content": [{
"id": "summary_table",
"type": "table",
"columns": [
{"key": "summary", "title": "Summary"},
{"key": "detail", "title": "Detail"},
],
"rows": [{
"id": "summary_row",
"cells": [{
"id": "summary_cell",
"column_key": "summary",
"column_span": 2,
"content": [{
"id": "summary_text",
"type": "paragraph",
"content": [
{"type": "text", "text": "Approved", "marks": ["strong"]},
{"type": "text", "text": " for release"},
],
}],
"format": {
"vertical_alignment": "center",
"background_color": "#EAF2F8",
"margin_left": {"value": 8, "unit": "pt"},
},
}],
}],
}],
})
result = rich_table.apply([{
"op": "table.cell.format",
"target": "#summary_table",
"cell": "#summary_cell",
"set": {
"background_color": "#FFF2CC",
"borders": {
"bottom": {
"style": "double",
"width": {"value": 2, "unit": "pt"},
"color": "#C00000",
},
},
},
}])
Imported DOCX grids are analyzed as logical cells before gridSpan and vMerge
are exposed. Regular grids support selective column widths; merged grids reject
column-width mutation but still support formatting a mapped anchor cell. Supported
cell paragraphs use the normal text and paragraph operations. Cells containing
drawings, nested tables, dynamic fields, or malformed content fall back to a
read-only text projection while their native XML remains intact. See
the table layout contract and
the table cell contract.
Border edges use explicit styles, widths, colors, and optional spacing. Clearing the
borders property removes known direct border XML so table styles can apply again;
{"style": "none"} writes an explicit no-border edge. A direct cell edge wins over
the conflicting table perimeter or internal-grid edge.
doc.render() defaults to a semantic HTML preview whose contract explicitly reports
fidelity="approximate" and verification_status="preview_only". A local
LibreOffice and Poppler installation enables native-compatible PDF and page PNG
evidence:
pdf = doc.render(format="pdf", provider="libreoffice")
pdf.write("report-render.pdf")
assert pdf.metadata["page_count"] >= 1
page = doc.render(
format="png",
provider="libreoffice",
options={"page_number": 1, "dpi": 144},
)
page.write("report-page-1.png")
For an entire document, render the PDF only once and derive a bounded, consistent page set from it:
evidence = doc.render_pages(
options={"dpi": 144},
analyze=True,
max_pages=100,
)
paths = evidence.write("evidence", stem="report")
assert len(evidence.pages) == evidence.page_count
Page analysis reports the background, ink ratio, content bounding box, four-side
whitespace, apparent blank pages, and visible content near a page edge. It requires
pip install "aioffice[render]"; rendering pages without analysis does not require
Pillow.
Each job uses an isolated LibreOffice user profile and reports engine versions,
source/output hashes, page count, font-environment hash, page dimensions, and
diagnostics. Native evidence still reports verification_status="unverified":
successful rendering proves that inspectable pages exist, not that an aesthetic
review has passed. See the native rendering contract
and style, diff, and rendering contracts.
The equivalent CLI workflow is:
aioffice render report.docx --format pdf -o report-render.pdf
aioffice render report.docx --format png --page 1 --dpi 144 -o report-page-1.png
aioffice render-pages report.docx --analyze --output-directory evidence
You can also create a document directly from the strict spec:
from aioffice.documents import Document
doc = Document.from_spec({
"metadata": {"title": "Project Report"},
"theme": {"ref": "business-clean"},
"content": [
{"type": "heading", "level": 1, "text": "Project Report"},
{"type": "paragraph", "text": "The first milestone is complete."},
],
})
Atomic patch
Patches never mutate the source Document. A successful result contains the next
logical revision:
result = doc.apply(
[
{
"op": "text.replace",
"target": "#status",
"search": "complete",
"replacement": "approved",
}
],
base_revision=doc.revision,
dry_run=True,
)
assert result.success
preview = result.document
Imported DOCX documents can receive a new paragraph, heading, explicit page break, bullet or numbered list, or complete semantic table without rebuilding their existing content:
result = doc.apply([
{
"op": "node.insert_after",
"target": "#executive_summary",
"content": {
"id": "recommendation",
"type": "paragraph",
"content": [
{"type": "text", "text": "Recommendation: ", "marks": ["strong"]},
{"type": "text", "text": "approve the proposed plan."},
],
"paragraph_style": {
"spacing_before": {"value": 8, "unit": "pt"},
"spacing_after": {"value": 8, "unit": "pt"},
},
},
}
])
assert result.success
Only the new native block is compiled. Existing XML, DrawingML, relationships, and
unsupported native features remain untouched. A caller-selected new ID can be
targeted again later in the same Patch; an omitted ID is returned in change
evidence. Rich text, direct formatting, internal/external hyperlinks, and normalized
document fields are supported in text blocks. Use node.insert_before for symmetric
placement, including insertion at the beginning of the document. Inserting before a
later section's first node safely rebinds that section's start_at. Use
node.append with target $ when the AI should add content to the last section
without first discovering the final content ID; native lowering inserts it before
the terminal body w:sectPr. See
native text insertion.
Use the same operations with {"type": "page_break"} to insert one native
w:p/w:r/w:br pagination control. The break has its own stable ID, can be targeted
later in the Patch, and is verified by native rendering rather than approximated by
the JSON projection.
Bullet and numbered lists are also ordinary stable-ID blocks:
result = doc.apply([
{
"op": "node.insert_after",
"target": "#recommendation",
"content": {
"id": "release_steps",
"type": "ordered_list",
"items": [
"Validate the evidence",
"Approve the release",
"Notify stakeholders",
],
},
}
])
assert result.success
AiOffice creates a fresh single-level abstractNum and num for each inserted
list, so numbering restarts deterministically and cannot accidentally continue an
adjacent native list. The list's contiguous w:p range has one stable root ID and
can immediately anchor another insertion, move as a group, or be removed. Existing
numbering definitions remain unchanged. See
native list insertion.
Tables use the same stable-ID placement contract:
result = doc.apply([
{
"op": "node.insert_after",
"target": "#recommendation",
"content": {
"id": "decision_table",
"type": "table",
"columns": [
{"id": "metric_column", "key": "metric", "title": "Metric"},
{"id": "value_column", "key": "value", "title": "Value"},
],
"rows": [
{
"id": "growth_row",
"cells": [
{
"id": "growth_label",
"column_key": "metric",
"value": "Growth",
},
{
"id": "growth_value",
"column_key": "value",
"value": "18%",
},
],
}
],
"layout": {
"style_ref": "TableGrid",
"algorithm": "fixed",
"repeat_header": True,
},
},
},
{
"op": "table.cell.format",
"target": "#decision_table",
"cell": "#growth_value",
"set": {"background_color": "#E2F0D9"},
},
])
assert result.success
AiOffice compiles only the new w:tbl, assigns native references to its columns,
rows, cells, and rich cell paragraphs, and leaves every existing body element
untouched. Regular and merged cells, explicit geometry, direct table/cell formatting,
and internal/external links in rich cell paragraphs are supported. See
native table insertion.
Existing top-level content can be reordered without reconstructing it or addressing an array index:
result = doc.apply([
{
"op": "node.move_before",
"target": "#risk_table",
"before": "#executive_summary",
}
])
assert result.success
For imported DOCX, AiOffice moves the target's complete mapped XML range. A
multi-paragraph list remains one contiguous unit, DrawingML and unknown XML stay in
their original elements, and every native reference is reindexed. node.move_after
and node.move_before cover every relative position without array indexes. The
conservative dev31 boundary permits moves only within one semantic section, refuses
moving a section start anchor, and rebinds section.start_at when prepending within
a later section. Native elements carrying w:sectPr remain immovable. See
the structural editing contract.
node.remove uses the same native-authority boundary. It removes the complete mapped
XML range, refuses native section carriers, attaches an identity manifest on the
first structural edit to a third-party DOCX, and preserves now-unreferenced
relationships or parts rather than guessing that they are safe to delete.
Semantic documents support text.replace, paragraph.format, text.format,
node.append, node.insert_after, node.insert_before, node.move_after,
node.move_before, node.remove, node.update, style.define, style.apply, style.format,
header_footer.create, section.header_footer.bind, section.insert_before,
section.format, field.update,
table.format, table.column.format, and table.cell.format. Imported DOCX
documents support incremental before/after insertion for paragraphs, headings, page
breaks, bullet/ordered lists, and tables, plus root append and safe native image
operations reported by capabilities().
Selectors use stable content, section, header/footer block, field, image, table,
column, row, cell, or rich cell-paragraph identities in this release.
CLI
aioffice inspect examples/report.json
aioffice capabilities existing.docx
aioffice verify original.docx modified.docx --declared-affected /word/document.xml
aioffice validate examples/report.json
aioffice build examples/report.json --output report.docx
aioffice export examples/report.json --to report.html
aioffice schema --output document.schema.json
aioffice schema --kind named-style --output named-style.schema.json
aioffice schema --kind paragraph-style --output paragraph-style.schema.json
aioffice schema --kind paragraph-borders --output paragraph-borders.schema.json
aioffice schema --kind document-defaults --output document-defaults.schema.json
aioffice schema --kind page-size --output page-size.schema.json
aioffice schema --kind section-layout --output section-layout.schema.json
aioffice schema --kind document-section --output document-section.schema.json
aioffice schema --kind document-settings --output document-settings.schema.json
aioffice schema --kind document-field --output document-field.schema.json
aioffice schema --kind table-width --output table-width.schema.json
aioffice schema --kind table-layout --output table-layout.schema.json
aioffice schema --kind table-column --output table-column.schema.json
aioffice schema --kind table-cell --output table-cell.schema.json
aioffice schema --kind table-cell-format --output table-cell-format.schema.json
aioffice schema --kind border-line --output border-line.schema.json
aioffice schema --kind table-borders --output table-borders.schema.json
aioffice schema --kind table-cell-borders --output table-cell-borders.schema.json
aioffice schema --kind header-footer-bindings --output header-footer-bindings.schema.json
aioffice schema --kind header-footer-part --output header-footer-part.schema.json
aioffice schema --kind text-range --output text-range.schema.json
aioffice workspace init project
aioffice workspace import existing.docx --root project
aioffice workspace list --root project
aioffice workspace capabilities ARTIFACT_ID --root project
aioffice workspace inspect ARTIFACT_ID --root project
aioffice workspace apply ARTIFACT_ID patch.json --root project
aioffice workspace reconcile ARTIFACT_ID edited.docx --root project
aioffice workspace reconcile ARTIFACT_ID edited.docx --root project --commit
aioffice workspace export ARTIFACT_ID updated.docx --root project
Patch files may be an operation array or an envelope:
{
"base_revision": 1,
"idempotency_key": "agent-task-001",
"operations": [
{
"op": "text.replace",
"target": "#status",
"search": "第一阶段",
"replacement": "第二阶段"
}
]
}
Preview or commit it without overwriting the input:
aioffice apply examples/report.json patch.json --dry-run
aioffice apply examples/report.json patch.json --output updated.json
Capability matrix and documentation drift
AiOffice's real capabilities live in a machine-readable registry, not in prose. The registry is the single source of truth; the documentation is generated from it and is checked for drift in CI.
# Print the current capability matrix (also written to docs/capabilities.md)
aioffice capability-doc
# Fail (exit 1) if any doc claims a supported capability is still "future"/"planned"
aioffice capability-drift
Status vocabulary: supported (safe for AI edits), partial (caveats apply),
opaque (preserved losslessly through the native layer, not directly editable by AI),
planned (known gap from the expert audit). When you ship a feature, update the
registry in src/aioffice/capabilities/registry.py and regenerate the doc — never
edit docs/capabilities.md by hand.
Development and release
python -m pip install -e ".[dev,render]"
python -m unittest discover -s tests -v
ruff check src tests
pyright src
python -m build
python -m twine check dist/*
Production releases use PyPI Trusted Publishing. The tag must match the package
version in src/aioffice/_version.py; pushing it starts
.github/workflows/publish.yml:
git tag vX.Y.Z
git push origin vX.Y.Z
No long-lived PyPI API token is stored in GitHub.
The current spec is a draft. Compatibility will be maintained within the 0.1.x
series where practical, but the public model can still evolve before 1.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aioffice-0.2.0.tar.gz.
File metadata
- Download URL: aioffice-0.2.0.tar.gz
- Upload date:
- Size: 642.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5204b30acc84ee09c68686cd5fc262f8fe1cd842b5ce87e6063ec2b138043943
|
|
| MD5 |
75d00bcc7429425a251521fc3b74bd1b
|
|
| BLAKE2b-256 |
1f9e62a4e7aebd5b1ef2d2520b8c7c068415f77f7020bda1122971b849299909
|
Provenance
The following attestation bundles were made for aioffice-0.2.0.tar.gz:
Publisher:
publish.yml on HuiTurn/aioffice
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aioffice-0.2.0.tar.gz -
Subject digest:
5204b30acc84ee09c68686cd5fc262f8fe1cd842b5ce87e6063ec2b138043943 - Sigstore transparency entry: 2257114804
- Sigstore integration time:
-
Permalink:
HuiTurn/aioffice@ac5d4e286aec3def2229018d3ac56f95cd0bcff2 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/HuiTurn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ac5d4e286aec3def2229018d3ac56f95cd0bcff2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file aioffice-0.2.0-py3-none-any.whl.
File metadata
- Download URL: aioffice-0.2.0-py3-none-any.whl
- Upload date:
- Size: 384.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19d1115d7fbb57b05b696f76719a36f9bd13214551ef6fb2464fec6524206b01
|
|
| MD5 |
0b655889f6281ff9b6afbe04b74e9ebf
|
|
| BLAKE2b-256 |
8f758e37cfc8d1805ffcacffc7d8aae82d1de25fe5ee65c9757a76e67211efff
|
Provenance
The following attestation bundles were made for aioffice-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on HuiTurn/aioffice
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aioffice-0.2.0-py3-none-any.whl -
Subject digest:
19d1115d7fbb57b05b696f76719a36f9bd13214551ef6fb2464fec6524206b01 - Sigstore transparency entry: 2257114817
- Sigstore integration time:
-
Permalink:
HuiTurn/aioffice@ac5d4e286aec3def2229018d3ac56f95cd0bcff2 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/HuiTurn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ac5d4e286aec3def2229018d3ac56f95cd0bcff2 -
Trigger Event:
push
-
Statement type: