open-document-lib 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. open_document_lib-1.0.0/LICENSE +21 -0
  2. open_document_lib-1.0.0/PKG-INFO +194 -0
  3. open_document_lib-1.0.0/README.md +468 -0
  4. open_document_lib-1.0.0/docs/library-api.md +154 -0
  5. open_document_lib-1.0.0/odf_lib/__init__.py +108 -0
  6. open_document_lib-1.0.0/odf_lib/citation_mapping.py +240 -0
  7. open_document_lib-1.0.0/odf_lib/odf_common.py +1625 -0
  8. open_document_lib-1.0.0/odf_lib/py.typed +0 -0
  9. open_document_lib-1.0.0/open_document_lib.egg-info/PKG-INFO +194 -0
  10. open_document_lib-1.0.0/open_document_lib.egg-info/SOURCES.txt +40 -0
  11. open_document_lib-1.0.0/open_document_lib.egg-info/dependency_links.txt +1 -0
  12. open_document_lib-1.0.0/open_document_lib.egg-info/requires.txt +16 -0
  13. open_document_lib-1.0.0/open_document_lib.egg-info/top_level.txt +1 -0
  14. open_document_lib-1.0.0/pyproject.toml +75 -0
  15. open_document_lib-1.0.0/setup.cfg +4 -0
  16. open_document_lib-1.0.0/tests/test_benchmarks.py +35 -0
  17. open_document_lib-1.0.0/tests/test_citations.py +402 -0
  18. open_document_lib-1.0.0/tests/test_corpus.py +141 -0
  19. open_document_lib-1.0.0/tests/test_cross_refs.py +391 -0
  20. open_document_lib-1.0.0/tests/test_dao_template.py +114 -0
  21. open_document_lib-1.0.0/tests/test_docs.py +53 -0
  22. open_document_lib-1.0.0/tests/test_edge_cases.py +306 -0
  23. open_document_lib-1.0.0/tests/test_examples.py +68 -0
  24. open_document_lib-1.0.0/tests/test_flat_odf.py +233 -0
  25. open_document_lib-1.0.0/tests/test_footnotes.py +309 -0
  26. open_document_lib-1.0.0/tests/test_install.py +105 -0
  27. open_document_lib-1.0.0/tests/test_lib_odf_common.py +563 -0
  28. open_document_lib-1.0.0/tests/test_libreoffice_integration.py +102 -0
  29. open_document_lib-1.0.0/tests/test_math.py +199 -0
  30. open_document_lib-1.0.0/tests/test_meta_lifecycle.py +142 -0
  31. open_document_lib-1.0.0/tests/test_odg_connectors.py +159 -0
  32. open_document_lib-1.0.0/tests/test_odg_gluepoints.py +133 -0
  33. open_document_lib-1.0.0/tests/test_odg_groups.py +171 -0
  34. open_document_lib-1.0.0/tests/test_odp_animations.py +229 -0
  35. open_document_lib-1.0.0/tests/test_odp_master.py +120 -0
  36. open_document_lib-1.0.0/tests/test_odp_transitions.py +141 -0
  37. open_document_lib-1.0.0/tests/test_ods_charts.py +271 -0
  38. open_document_lib-1.0.0/tests/test_ods_named_ranges.py +196 -0
  39. open_document_lib-1.0.0/tests/test_ods_validation.py +184 -0
  40. open_document_lib-1.0.0/tests/test_property.py +139 -0
  41. open_document_lib-1.0.0/tests/test_schema_validation.py +165 -0
  42. open_document_lib-1.0.0/tests/test_smoke.py +100 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Leiverkus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: open-document-lib
3
+ Version: 1.0.0
4
+ Summary: Standard-library toolkit for reading, editing, and writing OpenDocument Format files (ODT, ODP, ODS, ODG)
5
+ Author: Patrick Leiverkus
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/leiverkus/open-document-skills
8
+ Project-URL: Repository, https://github.com/leiverkus/open-document-skills
9
+ Project-URL: Changelog, https://github.com/leiverkus/open-document-skills/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/leiverkus/open-document-skills/issues
11
+ Keywords: opendocument,odf,odt,ods,odp,odg,libreoffice,xml,documents,spreadsheets
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Office/Business
22
+ Classifier: Topic :: Text Processing :: Markup :: XML
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: odf
28
+ Requires-Dist: odfpy>=1.4.0; extra == "odf"
29
+ Provides-Extra: scholarly
30
+ Requires-Dist: bibtexparser>=1.4; extra == "scholarly"
31
+ Provides-Extra: validate
32
+ Requires-Dist: lxml>=4.9; extra == "validate"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.9; extra == "dev"
36
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
37
+ Requires-Dist: hypothesis>=6.0; extra == "dev"
38
+ Requires-Dist: build>=1.0; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # open-document-lib
42
+
43
+ A standard-library toolkit for reading, editing, and writing **OpenDocument
44
+ Format** files — text documents (`.odt`), presentations (`.odp`),
45
+ spreadsheets (`.ods`), and drawings (`.odg`) — plus their flat (single-XML)
46
+ variants.
47
+
48
+ `open-document-lib` is the shared library behind the
49
+ [open-document-skills](https://github.com/leiverkus/open-document-skills)
50
+ agent skills. The core has **no dependencies** beyond the Python standard
51
+ library; a few helpers opt into `lxml` or `bibtexparser` when present.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pip install open-document-lib
57
+
58
+ # optional extras
59
+ pip install open-document-lib[validate] # lxml — RelaxNG schema validation
60
+ pip install open-document-lib[scholarly] # bibtexparser — BibTeX citation ingest
61
+ ```
62
+
63
+ Requires Python 3.10+. The package ships `py.typed`, so type checkers see
64
+ its annotations.
65
+
66
+ ## Quick start
67
+
68
+ ```python
69
+ from pathlib import Path
70
+ from xml.etree import ElementTree as ET
71
+ from odf_lib import (
72
+ parse_xml_from_zip, xml_bytes, write_odf_with_replacements,
73
+ replace_text_in_element, update_meta_for_edit,
74
+ )
75
+
76
+ src = Path("report.odt")
77
+ content = parse_xml_from_zip(src, "content.xml")
78
+
79
+ # Structure-preserving find/replace across the document body.
80
+ text_ns = "urn:oasis:names:tc:opendocument:xmlns:text:1.0"
81
+ for para in content.iter(f"{{{text_ns}}}p"):
82
+ replace_text_in_element(para, "{{CLIENT}}", "ACME GmbH")
83
+
84
+ # Stamp the edit into meta.xml (modification date, generator, cycle count).
85
+ meta = parse_xml_from_zip(src, "meta.xml")
86
+ # update_meta_for_edit needs a namespace map + qualified-name helper;
87
+ # the skills' *_common.py wrappers supply these.
88
+
89
+ write_odf_with_replacements(
90
+ src, Path("report-out.odt"),
91
+ {"content.xml": xml_bytes(content)},
92
+ "application/vnd.oasis.opendocument.text",
93
+ )
94
+ ```
95
+
96
+ Flat-ODF round-trip:
97
+
98
+ ```python
99
+ from odf_lib import pack_flat_odf, unpack_flat_odf
100
+
101
+ pack_flat_odf(Path("deck.odp"), Path("deck.fodp")) # ZIP → single XML
102
+ unpack_flat_odf(Path("deck.fodp"), Path("deck.odp")) # XML → ZIP package
103
+ ```
104
+
105
+ ## API reference
106
+
107
+ Everything below is exported directly from the `odf_lib` package and is
108
+ covered by semantic versioning from 1.0 onward. Anything in
109
+ `odf_lib.odf_common` that is **not** listed here (notably `_`-prefixed
110
+ helpers) is internal and may change without notice.
111
+
112
+ ### Constants
113
+
114
+ | Name | Description |
115
+ |---|---|
116
+ | `VERSION` | Library version string (also `odf_lib.__version__`). |
117
+ | `ODF_NAMESPACES` | `dict[str, str]` of ODF namespace prefixes → URIs. |
118
+ | `FLAT_EXTENSIONS` | Mapping of ODF mimetype → flat-file extension (`.fodt`, …). |
119
+
120
+ ### ZIP / XML core
121
+
122
+ | Signature | Description |
123
+ |---|---|
124
+ | `parse_xml_from_zip(path, member) -> ET.Element` | Parse one XML member of an ODF ZIP. |
125
+ | `xml_bytes(root) -> bytes` | Serialize an element to UTF-8 bytes with XML declaration. |
126
+ | `write_odf_with_replacements(input_path, output_path, replacements, mimetype_value) -> None` | Copy an ODF package, swapping named members; `mimetype` stays first and stored. |
127
+ | `pack_dir_as_odf(source_dir, output_path, mimetype_value) -> None` | Repack an extracted directory into a valid ODF file. |
128
+ | `copy_into_package(input_path, output_path, package_path, source, replacements, mimetype_value) -> None` | Add a single file to a package plus member replacements. |
129
+ | `copy_with_multiple_members(input_path, output_path, new_members, replacements, mimetype_value) -> None` | Add several new members in one pass (e.g. `Object N/` sub-packages). |
130
+ | `unpack_to_temp(path) -> tempfile.TemporaryDirectory` | Extract a package to a managed temp directory. |
131
+
132
+ ### Manifest and media
133
+
134
+ | Signature | Description |
135
+ |---|---|
136
+ | `ensure_manifest_entry(manifest_root, full_path, media_type, ns, q_fn) -> None` | Add or update a `manifest:file-entry`. |
137
+ | `media_type_for(path) -> str` | MIME type from a file extension. |
138
+ | `sniff_image_mime(path) -> str` | MIME type from magic bytes, with extension fallback. |
139
+ | `unique_picture_name(existing, image) -> str` | Collision-free `Pictures/` filename. |
140
+ | `unique_object_name(existing) -> str` | Next free `Object N` sub-package name. |
141
+
142
+ ### Metadata
143
+
144
+ | Signature | Description |
145
+ |---|---|
146
+ | `update_meta_for_edit(meta_root, ns, q_fn) -> None` | Refresh `meta:modification-date`/`generator` and bump `editing-cycles`. |
147
+
148
+ ### Flat ODF
149
+
150
+ | Signature | Description |
151
+ |---|---|
152
+ | `pack_flat_odf(input_zip, output_flat) -> None` | Convert a zipped ODF to flat single-XML form (pictures and `Object N/` sub-packages inlined). |
153
+ | `unpack_flat_odf(input_flat, output_zip) -> None` | Convert a flat ODF back to a zipped package and rebuild the manifest. |
154
+
155
+ ### Text walker, locator, insertion
156
+
157
+ | Signature | Description |
158
+ |---|---|
159
+ | `replace_text_in_element(element, old, new) -> int` | Structure-preserving find/replace across text and child tails. |
160
+ | `replace_pattern_with_element_in_element(element, pattern, factory) -> int` | Replace regex matches with generated elements. |
161
+ | `find_text_position_in_element(element, needle) -> tuple \| None` | Locate `needle`, returning `(node, "text"\|"tail", offset)`. |
162
+ | `insert_after_text_in_element(element, anchor, new_element) -> bool` | Splice an element in right after an anchor string. |
163
+ | `insert_in_paragraph(paragraph, position, new_element) -> None` | Insert at the `start` or `end` of a paragraph. |
164
+ | `wrap_text_with_pair_in_element(element, start_anchor, end_anchor, start_element, end_element) -> bool` | Wrap an intra-paragraph text range with a start/end pair. |
165
+ | `wrap_text_across_elements(elements, start_anchor, end_anchor, start_element, end_element) -> bool` | Same, spanning multiple paragraphs. |
166
+ | `ensure_sequence_declarations(text_root, names, ns) -> None` | Ensure `text:sequence-decl` entries exist. |
167
+ | `clear_children(element) -> None` | Remove all children of an element. |
168
+ | `local_name(tag) -> str` | Local name from a Clark-notation tag. |
169
+
170
+ ### Styles and pictures
171
+
172
+ | Signature | Description |
173
+ |---|---|
174
+ | `inject_styles_from_file(input_path, styles_path, output_path, mimetype_value) -> list[str]` | Replace `styles.xml`; returns dangling style references. |
175
+ | `embed_pictures(input_path, pictures, output_path, mimetype_value, ns, q_fn) -> None` | Bulk-add images to `Pictures/` and the manifest. |
176
+
177
+ ### Schema validation
178
+
179
+ | Signature | Description |
180
+ |---|---|
181
+ | `ensure_schema(name) -> Path` | Download and cache an OASIS ODF 1.3 RelaxNG schema (`content`/`manifest`). |
182
+ | `validate_against_schema(xml_bytes_input, schema_name) -> tuple[bool, list[str]]` | Validate XML bytes against a cached schema (requires `lxml`). |
183
+
184
+ ### External tooling
185
+
186
+ | Signature | Description |
187
+ |---|---|
188
+ | `find_soffice() -> str` | Locate the LibreOffice `soffice` binary; raises if absent. |
189
+ | `find_pandoc() -> str \| None` | Locate the `pandoc` binary. |
190
+ | `latex_to_mathml(latex) -> bytes` | Convert a LaTeX snippet to MathML via Pandoc. |
191
+
192
+ ## License
193
+
194
+ MIT. See [LICENSE](https://github.com/leiverkus/open-document-skills/blob/main/LICENSE).
@@ -0,0 +1,468 @@
1
+ # Open Document Skills
2
+
3
+ [![Tests](https://github.com/leiverkus/open-document-skills/actions/workflows/tests.yml/badge.svg)](https://github.com/leiverkus/open-document-skills/actions/workflows/tests.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Release](https://img.shields.io/github/v/release/leiverkus/open-document-skills)](https://github.com/leiverkus/open-document-skills/releases)
6
+
7
+ **Native ODT / ODP / ODS / ODG generation and editing for agents — no DOCX round-trips, no LibreOffice dependency for the core path.**
8
+
9
+ Four self-contained skills for Codex, Claude Code, and OpenCode that teach an agent to create, inspect, and edit OpenDocument files directly via Python (stdlib only). Edits preserve inline structure (`text:span`, `text:note`, `text:bookmark`, `text:a`), `meta.xml` is updated on every save, and flat single-XML formats (`.fodt`/`.fodp`/`.fods`/`.fodg`) give you Git-friendly diffs. LibreOffice is optional and only needed for rendering, recalculation, and PDF export.
10
+
11
+ ```bash
12
+ # Generate, edit, validate, version — all from the agent shell:
13
+ python skills/odt/scripts/create_minimal_odt.py spec.json doc.odt
14
+ python skills/odt/scripts/replace_text.py doc.odt "{{NAME}}" "Patrick" -o out.odt
15
+ python skills/odt/scripts/pack_fodt.py out.odt -o out.fodt # diff-friendly XML
16
+ python skills/odt/scripts/validate_refs.py out.odt
17
+ ```
18
+
19
+ ## Skills at a glance
20
+
21
+ | Skill | LibreOffice app | Smithery | Triggers |
22
+ | --- | --- | --- | --- |
23
+ | [`odt`](skills/odt) | Writer | [smithery.ai/skills/leiverkus/odt](https://smithery.ai/skills/leiverkus/odt) | edit ODT, footnotes, citations (BibTeX/CSL-JSON), bookmarks, cross-references, figure/table sequences, MathML formulas, render to PDF |
24
+ | [`odp`](skills/odp) | Impress | [smithery.ai/skills/leiverkus/odp](https://smithery.ai/skills/leiverkus/odp) | clone slide, edit notes, add image, animations (entrance/exit/emphasis/motion), slide transitions, master-page customization (background, header/footer, logo), render deck |
25
+ | [`ods`](skills/ods) | Calc | [smithery.ai/skills/leiverkus/ods](https://smithery.ai/skills/leiverkus/ods) | set cells/formulas, named ranges, dropdowns + data validation, embedded charts (bar/line/pie/scatter), export CSV, recalculate |
26
+ | [`odg`](skills/odg) | Draw | [smithery.ai/skills/leiverkus/odg](https://smithery.ai/skills/leiverkus/odg) | edit labels, add shape image, glue points, connectors with shape binding, groups, flowcharts, org charts, export SVG/PNG |
27
+
28
+ ## Why use these
29
+
30
+ - **Native ODF, not converted from DOCX.** No font drift, no lost styles, no PDF round-trips.
31
+ - **Stdlib-only core.** Every generator, validator, and edit script runs without `pip install` — `xml.etree.ElementTree` and `zipfile` only. LibreOffice is needed only for rendering and recalculation.
32
+ - **Structure-preserving edits.** `replace_text` keeps footnotes, hyperlinks, and inline formatting intact. `add_image` updates the manifest and `meta.xml`. `replace_cells` handles typed values and formulas.
33
+ - **Audit-friendly.** Every edit writes `meta:modification-date`, `meta:generator`, and increments `meta:editing-cycles`. Pack to `.fodt` and `git diff` works.
34
+ - **Tested.** 76 unit + integration tests run on every push; CI installs LibreOffice so the render/recalc paths are exercised too.
35
+
36
+ ## What this is not
37
+
38
+ Not a LibreOffice replacement. Not a substitute for full ODF feature coverage (tracked changes, complex TOCs, Impress animations, Calc pivots, Draw glue points, RelaxNG schema validation are explicit non-goals — see [Current Limits](#current-limits)). The goal is to make the 80% of ODF automation that agents need safe, repeatable, and dependency-light.
39
+
40
+ ## Repository Layout
41
+
42
+ ```text
43
+ skills/
44
+ odt/
45
+ SKILL.md
46
+ scripts/
47
+ odp/
48
+ SKILL.md
49
+ scripts/
50
+ ods/
51
+ SKILL.md
52
+ scripts/
53
+ odg/
54
+ SKILL.md
55
+ scripts/
56
+ tests/
57
+ fixtures/
58
+ test_smoke.py
59
+ test_edge_cases.py
60
+ test_libreoffice_integration.py
61
+ scripts/
62
+ install_skills.py
63
+ .claude-plugin/
64
+ plugin.json
65
+ examples/
66
+ README.md
67
+ build_examples.py
68
+ *.json
69
+ docs/
70
+ index.md
71
+ ```
72
+
73
+ Each skill is MIT-licensed and also contains its own `LICENSE.txt`.
74
+
75
+ ## Documentation
76
+
77
+ Detailed documentation lives in [docs/index.md](docs/index.md):
78
+
79
+ - [Installation](docs/installation.md)
80
+ - [Agent Compatibility](docs/agent-compatibility.md)
81
+ - [OpenDocument Workflows](docs/workflows.md)
82
+ - [Script Reference](docs/script-reference.md)
83
+
84
+ ## Installation
85
+
86
+ The skills are available through three channels — pick what fits your setup:
87
+
88
+ ### Smithery (recommended for individual skills)
89
+
90
+ Browse and install via [smithery.ai](https://smithery.ai). Install one or more skills directly through the Smithery UI: [odt](https://smithery.ai/skills/leiverkus/odt), [odp](https://smithery.ai/skills/leiverkus/odp), [ods](https://smithery.ai/skills/leiverkus/ods), [odg](https://smithery.ai/skills/leiverkus/odg).
91
+
92
+ ### Open Agent Skills CLI
93
+
94
+ The [vercel-labs/skills](https://github.com/vercel-labs/skills) CLI installs across Claude Code, Codex, Cursor, OpenCode, and 50+ other agents:
95
+
96
+ ```bash
97
+ # List skills in the repo
98
+ npx skills add leiverkus/open-document-skills --list
99
+
100
+ # Install all four globally for Claude Code
101
+ npx skills add leiverkus/open-document-skills --skill '*' -a claude-code -g
102
+
103
+ # Install only ODT into your project
104
+ npx skills add leiverkus/open-document-skills --skill odt
105
+ ```
106
+
107
+ ### Bundled installer (Codex / OpenCode / Claude Code)
108
+
109
+ Install all four skills at once via the bundled Python installer:
110
+
111
+ ```bash
112
+ python3 scripts/install_skills.py
113
+ ```
114
+
115
+ By default, the installer writes to `$CODEX_HOME/skills` when `CODEX_HOME` is set, otherwise to:
116
+
117
+ ```bash
118
+ ~/.codex/skills
119
+ ```
120
+
121
+ If your setup uses the older `.agents` directory, install there explicitly:
122
+
123
+ ```bash
124
+ python3 scripts/install_skills.py --target agents
125
+ ```
126
+
127
+ Existing skill directories are skipped by default. To intentionally overwrite the installed copies:
128
+
129
+ ```bash
130
+ python3 scripts/install_skills.py --target agents --replace
131
+ ```
132
+
133
+ For OpenCode global skills:
134
+
135
+ ```bash
136
+ python3 scripts/install_skills.py --target opencode
137
+ ```
138
+
139
+ To install project-local OpenCode skills:
140
+
141
+ ```bash
142
+ python3 scripts/install_skills.py --target opencode --dest .opencode/skills
143
+ ```
144
+
145
+ For Claude Code, this repository can be used as a skill-focused plugin because it contains `.claude-plugin/plugin.json` and a top-level `skills/` directory. To create a plugin bundle at a chosen destination:
146
+
147
+ ```bash
148
+ python3 scripts/install_skills.py --target claude --dest ./dist/open-document-skills
149
+ ```
150
+
151
+ Then add or install that plugin directory in Claude Code.
152
+
153
+ To install from a local checkout:
154
+
155
+ ```bash
156
+ git clone https://github.com/leiverkus/open-document-skills.git
157
+ cd open-document-skills
158
+ python3 scripts/install_skills.py
159
+ ```
160
+
161
+ ## Python library
162
+
163
+ The shared library behind the four skills is published to PyPI as
164
+ [`open-document-lib`](https://pypi.org/project/open-document-lib/). Use it
165
+ directly in any Python project — no skill bundling required:
166
+
167
+ ```bash
168
+ pip install open-document-lib
169
+ ```
170
+
171
+ ```python
172
+ from odf_lib import pack_flat_odf, replace_text_in_element, validate_against_schema
173
+ ```
174
+
175
+ The core has no dependencies beyond the standard library; `[validate]`
176
+ pulls in `lxml` for RelaxNG validation and `[scholarly]` pulls in
177
+ `bibtexparser`. See [docs/library-api.md](docs/library-api.md) for the
178
+ full public API.
179
+
180
+ ## Requirements
181
+
182
+ Core scripts use only the Python standard library.
183
+
184
+ Recommended optional tools:
185
+
186
+ - LibreOffice, for rendering/export/recalculation workflows
187
+ - `pdftoppm` from Poppler, when you want PDF pages rendered to images
188
+ - Pandoc, for some conversion fallback workflows
189
+
190
+ Install all optional tools on macOS with Homebrew:
191
+
192
+ ```bash
193
+ brew install --cask libreoffice
194
+ brew install poppler pandoc
195
+ ```
196
+
197
+ Install all optional tools on Windows with winget:
198
+
199
+ ```powershell
200
+ winget install --id TheDocumentFoundation.LibreOffice -e
201
+ winget install --id oschwartz10612.Poppler -e
202
+ winget install --id JohnMacFarlane.Pandoc -e
203
+ ```
204
+
205
+ Install all optional tools on Ubuntu with apt:
206
+
207
+ ```bash
208
+ sudo apt-get update
209
+ sudo apt-get install -y libreoffice poppler-utils pandoc
210
+ ```
211
+
212
+ LibreOffice usually provides `soffice` inside the app bundle, not directly on the shell `PATH`:
213
+
214
+ ```bash
215
+ /Applications/LibreOffice.app/Contents/MacOS/soffice
216
+ ```
217
+
218
+ The render/recalc scripts look for that macOS path automatically. They also check common Linux and Windows locations.
219
+
220
+ ## Skills
221
+
222
+ ### ODT
223
+
224
+ OpenDocument Text / LibreOffice Writer.
225
+
226
+ Focus:
227
+
228
+ - template-first document editing
229
+ - direct ODT XML generation
230
+ - headings, paragraphs, lists, tables, footnotes, images
231
+ - style/page-layout awareness
232
+ - PDF QA through LibreOffice
233
+
234
+ Useful scripts:
235
+
236
+ ```bash
237
+ python skills/odt/scripts/create_minimal_odt.py document.json output.odt
238
+ python skills/odt/scripts/extract_text.py output.odt
239
+ python skills/odt/scripts/inspect_package.py output.odt
240
+ python skills/odt/scripts/replace_text.py input.odt "{{NAME}}" "Patrick Leiverkus" -o output.odt
241
+ python skills/odt/scripts/add_image.py input.odt figure.png -o output.odt
242
+ python skills/odt/scripts/add_footnote.py input.odt --anchor "claim" --body "Source: ..." -o output.odt
243
+ python skills/odt/scripts/fill_citations.py template.odt --source refs.bib -o output.odt
244
+ python skills/odt/scripts/add_bookmark.py input.odt --name K1 --anchor "Chapter 1" -o output.odt
245
+ python skills/odt/scripts/add_math.py input.odt --latex 'E = mc^2' --anchor "Equation" -o output.odt
246
+ python skills/odt/scripts/pack_fodt.py output.odt -o output.fodt
247
+ python skills/odt/scripts/validate_refs.py output.odt
248
+ ```
249
+
250
+ Script reference: see [docs/script-reference.md](docs/script-reference.md).
251
+
252
+ ### ODP
253
+
254
+ OpenDocument Presentation / LibreOffice Impress.
255
+
256
+ Focus:
257
+
258
+ - template-first presentations
259
+ - direct ODP XML generation
260
+ - `draw:page`, speaker notes, master pages
261
+ - slide text/media inspection
262
+ - package and visual QA
263
+
264
+ Useful scripts:
265
+
266
+ ```bash
267
+ python skills/odp/scripts/create_minimal_odp.py slides.json output.odp
268
+ python skills/odp/scripts/extract_text.py output.odp
269
+ python skills/odp/scripts/inspect_package.py output.odp
270
+ python skills/odp/scripts/clone_slide.py template.odp --source-slide 1 --name "Agenda" -o output.odp
271
+ python skills/odp/scripts/add_image.py input.odp figure.png -o output.odp
272
+ python skills/odp/scripts/validate_refs.py output.odp
273
+ ```
274
+
275
+ Script reference: see [docs/script-reference.md](docs/script-reference.md).
276
+
277
+ ### ODS
278
+
279
+ OpenDocument Spreadsheet / LibreOffice Calc.
280
+
281
+ Focus:
282
+
283
+ - direct ODS XML generation
284
+ - template-first spreadsheet editing
285
+ - typed cell values
286
+ - formulas
287
+ - repeated rows/cells
288
+ - CSV export and formula QA
289
+
290
+ Useful scripts:
291
+
292
+ ```bash
293
+ python skills/ods/scripts/create_minimal_ods.py workbook.json output.ods
294
+ python skills/ods/scripts/extract_sheets.py output.ods
295
+ python skills/ods/scripts/extract_formulas.py output.ods
296
+ python skills/ods/scripts/replace_cells.py input.ods 'Data!B2=42' 'Data!C2=formula:of:=[.B2]*2' -o output.ods
297
+ python skills/ods/scripts/export_csv.py output.ods --sheet Data --output data.csv
298
+ python skills/ods/scripts/validate_refs.py output.ods
299
+ ```
300
+
301
+ Script reference: see [docs/script-reference.md](docs/script-reference.md).
302
+
303
+ ### ODG
304
+
305
+ OpenDocument Graphics / LibreOffice Draw.
306
+
307
+ Focus:
308
+
309
+ - direct ODG XML generation
310
+ - template-first diagram editing
311
+ - vector shapes, text boxes, lines, connectors, images
312
+ - geometry inspection
313
+ - PDF/SVG/PNG export QA
314
+
315
+ Useful scripts:
316
+
317
+ ```bash
318
+ python skills/odg/scripts/create_minimal_odg.py drawing.json output.odg
319
+ python skills/odg/scripts/extract_text.py output.odg
320
+ python skills/odg/scripts/extract_shapes.py output.odg
321
+ python skills/odg/scripts/inspect_package.py output.odg
322
+ python skills/odg/scripts/replace_text.py input.odg "{{LABEL}}" "Updated label" -o output.odg
323
+ python skills/odg/scripts/validate_refs.py output.odg
324
+ ```
325
+
326
+ Script reference: see [docs/script-reference.md](docs/script-reference.md).
327
+
328
+ ## Testing
329
+
330
+ Run the test suite:
331
+
332
+ ```bash
333
+ python -m unittest discover -s tests
334
+ ```
335
+
336
+ The tests create minimal ODT, ODP, ODS, and ODG files, then exercise extraction, validation, editing, media insertion, and export helpers.
337
+
338
+ LibreOffice integration tests are included. They render ODT/ODP/ODG files and recalculate ODS files when `soffice` is available. If LibreOffice is not available, those tests are skipped.
339
+
340
+ GitHub Actions runs the same suite on every push and pull request. The workflow installs LibreOffice and Poppler with `apt` on Ubuntu so the LibreOffice integration tests run in CI instead of being skipped.
341
+
342
+ Reusable example inputs live in `tests/fixtures/`:
343
+
344
+ - `odt_document.json`
345
+ - `odp_slides.json`
346
+ - `ods_workbook.json`
347
+ - `odg_drawing.json`
348
+ - `image.svg`
349
+
350
+ ## Examples
351
+
352
+ Runnable examples live in `examples/`. They are meant as a practical first test layer for users of the skills:
353
+
354
+ ```bash
355
+ python examples/build_examples.py
356
+ ```
357
+
358
+ This creates ODT, ODP, ODS, and ODG files in `examples/output/`, then validates their package references. The generated output directory is ignored by Git.
359
+
360
+ For optional LibreOffice QA:
361
+
362
+ ```bash
363
+ python examples/build_examples.py --render
364
+ ```
365
+
366
+ On macOS, add `--png` when Poppler is installed with Homebrew and PNG page previews are useful:
367
+
368
+ ```bash
369
+ python examples/build_examples.py --render --png
370
+ ```
371
+
372
+ ## LibreOffice Workflows
373
+
374
+ Some workflows are intentionally optional because they require LibreOffice:
375
+
376
+ - render ODT/ODP/ODG to PDF or images
377
+ - export ODG to SVG/PNG
378
+ - recalculate ODS formulas
379
+ - round-trip conversions from DOCX/PPTX/XLSX or Markdown/HTML
380
+
381
+ The skills treat these as QA or interoperability steps. Native ODF package generation and XML-safe edits remain the preferred path when the target deliverable is an ODF file.
382
+
383
+ ## Flat ODF (Git-friendly)
384
+
385
+ Every format has `pack_*` and `unpack_*` scripts that convert between the zipped ODF package and a flat single-XML file (`.fodt`, `.fodp`, `.fods`, `.fodg`). The flat form is part of the OASIS specification, opens directly in LibreOffice, and produces readable diffs under Git. Embedded images are inlined as base64 on pack and extracted back to `Pictures/` on unpack.
386
+
387
+ ```bash
388
+ python skills/odt/scripts/pack_fodt.py document.odt -o document.fodt
389
+ git diff document.fodt
390
+ python skills/odt/scripts/unpack_fodt.py document.fodt -o document.odt
391
+ ```
392
+
393
+ See [docs/workflows.md](docs/workflows.md#flat-odf-git-friendly) for details.
394
+
395
+ ## Performance
396
+
397
+ The helpers are pure-Python and stream through ZIP packages without loading
398
+ LibreOffice. End-to-end CLI latency on a representative laptop:
399
+
400
+ | Format | Document | Create | Edit | Validate |
401
+ |--------|----------|--------|------|----------|
402
+ | ODT | 2000 paragraphs | 55 ms | 54 ms (`replace_text`) | 48 ms |
403
+ | ODS | 100 000 cells (1000×100) | 398 ms | 428 ms (`replace_cells`) | 689 ms |
404
+ | ODP | 100 slides | 45 ms | 42 ms (`clone_slide`) | 42 ms |
405
+ | ODG | 500 shapes | 47 ms | — | 44 ms |
406
+
407
+ Every timing includes Python interpreter startup (~40 ms), so small
408
+ documents are startup-bound; large spreadsheets are the heaviest case and
409
+ still finish well under a second. Reproduce or re-measure with
410
+ [`benchmarks/run_benchmarks.py`](benchmarks/README.md). Numbers are
411
+ indicative and machine-dependent.
412
+
413
+ ## Current Limits
414
+
415
+ The scripts are intentionally small and conservative, but production-deep
416
+ across all four formats.
417
+
418
+ They cover:
419
+
420
+ - direct generation and template-based editing
421
+ - package validation, including optional RelaxNG validation against the
422
+ OASIS ODF 1.3 schema (`validate_refs.py --strict`, all four formats)
423
+ - text/formula/shape extraction
424
+ - XML-safe replacements that preserve inline `text:span`, `text:note`, `text:bookmark`, and `text:a`
425
+ - scholarly authoring — footnotes, endnotes, citations (BibTeX/CSL-JSON), cross-references, MathML
426
+ - spreadsheets — named ranges, data validation, embedded charts
427
+ - presentations — animations, slide transitions, master-page customization
428
+ - drawings — connectors with shape binding, glue points, shape groups
429
+ - image embedding with magic-byte MIME detection
430
+ - `meta.xml` lifecycle updates on every edit (`modification-date`, `generator`, `editing-cycles`)
431
+ - flat ODF (`.fodt`/`.fodp`/`.fods`/`.fodg`) roundtrip
432
+
433
+ They intentionally do **not** attempt to model every OpenDocument feature.
434
+ Out of scope — use LibreOffice for these:
435
+
436
+ - tracked changes and comments
437
+ - generated indexes and tables of contents (LibreOffice builds these from the markers the skills set)
438
+ - Calc pivot tables, conditional formatting, and cell protection
439
+ - full Impress slide-master hierarchies beyond simple page layouts
440
+ - DOCX/PPTX/XLSX import-and-edit — use `pandoc` if you need those round-trips
441
+
442
+ See [ROADMAP.md](ROADMAP.md) for what is planned next.
443
+
444
+ ## Development
445
+
446
+ Recommended loop:
447
+
448
+ ```bash
449
+ python -m unittest discover -s tests
450
+ git status --short
451
+ ```
452
+
453
+ When adding a new script or behavior:
454
+
455
+ 1. Add the smallest useful script interface.
456
+ 2. Add or update a smoke test.
457
+ 3. Run local tests.
458
+ 4. Push and let GitHub Actions verify the repo.
459
+
460
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development and release checklist.
461
+
462
+ ## Release Status
463
+
464
+ Current release: `v0.9.0` — a robustness release. Every helper is now exercised against a committed corpus of 17 LibreOffice-native ODF fixtures (`tests/test_corpus.py`), which uncovered and fixed two foreign-ODF bugs in `validate_refs` and the flat-ODF roundtrip. No new features — all four skills (ODT/ODP/ODS/ODG) remain at production-level depth. See [ROADMAP.md](ROADMAP.md) for v1.0 (PyPI publication + final polish + ecosystem maturity).
465
+
466
+ ## License
467
+
468
+ MIT. See [LICENSE](LICENSE).