npx skills add ...
npx skills add dfk1352/libreoffice-skills --skill libreoffice-writer
Use when creating, editing, formatting, exporting, or extracting LibreOffice Writer (.odt) documents via UNO, including session-based edits, structured text targets, tables, images, lists, patch workflows, and snapshots.
npx skills add dfk1352/libreoffice-skills --skill libreoffice-writer
Use the bundled writer modules for UNO-backed Writer document work.
All paths must be absolute. Bundled modules live under scripts/ in this
skill directory, so set PYTHONPATH=<skill_base_dir>/scripts.
If setup or runtime issues appear, check references/troubleshooting.md.
WriterTarget| Kind | Supported fields | Use |
|---|---|---|
text | text, after, before, occurrence | Read, replace, delete, or format matched text |
insertion | text, after, before, occurrence | Insert at a resolved boundary or after a matched span |
table | name or index | Update/delete a table |
image | name or index | Update/delete an image |
list | text, after, before, occurrence | Replace/delete one logical list block |
target to read the full document or append inserted content at the end.after and before to constrain a search window.occurrence when repeated text is expected; otherwise matching must be unique.text, after, and before anchors; single-word anchors are often too brittle for realistic prose edits.name; use index only when order is stable.Notes:
align apply to the full paragraph containing the match, not just the exact matched span.level is zero-based nesting.ordered=True uses a numbering style; ordered=False uses bullets.set_metadata() accepts a dictionary of key-value pairs.title, author, subject, keywords, and description.keywords may be a comma-separated string; get_metadata() returns them as one comma-separated string.Use patch() or session.patch() to apply ordered operations.
insert_textreplace_textdelete_textformat_textinsert_tableupdate_tabledelete_tableinsert_imageupdate_imagedelete_imageinsert_listreplace_listdelete_listtarget.* fields for target definition.target.* fields; do not use a bare target.kind = insertion without anchors.format.* fields for formatting payloads.list.ordered plus JSON items for list operations.items and data must be valid JSON.<<TAG ... TAG for multiline text or JSON.atomic stops on first failure, resets the session, and persists nothing.best_effort keeps successful earlier operations and records failures.PatchApplyResult fields:
modeoverall_status = "ok" | "partial" | "failed"operations = list of PatchOperationResultdocument_persistedFor standalone patch(path, ...), document_persisted means the changes were
saved to disk. For session.patch(...), it means the patch produced successful
mutations in the current open session state.
Use snapshots to verify layout after formatting, list edits, image placement, or table changes.
session.export() while a document is already open in a WriterSession.export_document() when exporting an existing file without keeping a session open.get_metadata() normalizes keywords into one comma-and-space separated string.occurrence for repeated text and then getting an ambiguity error.after / before bounds when possible.align to apply only to a phrase; Writer applies paragraph alignment to the containing paragraph.items or data patch fields.session.export() or other methods after session.close()."start" and "end" paragraph alignment options require LibreOffice 26.2+.from writer import WriterTarget
WriterTarget(
kind="text" | "insertion" | "table" | "image" | "list",
text=None,
after=None,
before=None,
occurrence=None,
name=None,
index=None,
)from writer import TextFormatting
TextFormatting(
bold=None,
italic=None,
underline=None,
font_name=None,
font_size=None,
color=None, # named color or integer
align=None, # "left" | "center" | "right" | "justify" | "start" | "end"
line_spacing=None,
spacing_before=None,
spacing_after=None,
)from writer import ListItem
ListItem(text="Confirm scope", level=0)[operation]
type = format_text
target.kind = text
target.text = Quarterly revenue grew 18%.
target.after = Financial Summary
target.before = Action Items
format.bold = true
format.align = center
[operation]
type = insert_list
target.kind = insertion
target.after = Action Items
list.ordered = false
items <<JSON
[
{"text": "Confirm scope", "level": 0},
{"text": "Review output", "level": 0},
{"text": "Update packaging", "level": 1}
]
JSONfrom pathlib import Path
from writer import ListItem, TextFormatting, WriterSession, WriterTarget
from writer.core import create_document
output = str(Path("test-output/report.odt").resolve())
create_document(output)
with WriterSession(output) as session:
session.insert_text(
"Executive Summary\n\n"
"Financial Summary\n\n"
"Quarterly revenue grew 18%.\n\n"
"Action Items"
)
session.format_text(
WriterTarget(
kind="text",
text="Quarterly revenue grew 18%.",
after="Financial Summary",
before="Action Items",
),
TextFormatting(bold=True, align="center"),
)
session.insert_list(
[
ListItem(text="Confirm scope", level=0),
ListItem(text="Review output", level=0),
ListItem(text="Update packaging", level=1),
],
ordered=False,
target=WriterTarget(kind="insertion", after="Action Items"),
)from writer import patch
result = patch(
"/abs/path/report.odt",
"""
[operation]
type = replace_text
target.kind = text
target.text = Draft
new_text = Final
[operation]
type = update_table
target.kind = table
target.name = Summary
data = [["Metric", "Value"], ["Revenue", "$2M"]]
[operation]
type = replace_list
target.kind = list
target.text = Confirm scope
items = [{"text": "Approve release", "level": 0}, {"text": "Notify team", "level": 1}]
list.ordered = true
""",
mode="best_effort",
)
print(result.overall_status)from pathlib import Path
from writer import snapshot_page
result = snapshot_page(doc_path, "/tmp/page1.png", page=1, dpi=150)
print(result.file_path, result.width, result.height)
Path(result.file_path).unlink(missing_ok=True) # clean up used snapshots