python-pages 0.1.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.
- python_pages-0.1.0/LICENSE +21 -0
- python_pages-0.1.0/PKG-INFO +271 -0
- python_pages-0.1.0/README.md +248 -0
- python_pages-0.1.0/pyproject.toml +38 -0
- python_pages-0.1.0/setup.cfg +4 -0
- python_pages-0.1.0/src/pages/__init__.py +65 -0
- python_pages-0.1.0/src/pages/api.py +1869 -0
- python_pages-0.1.0/src/pages/cli.py +43 -0
- python_pages-0.1.0/src/pages/comments.py +168 -0
- python_pages-0.1.0/src/pages/components.py +403 -0
- python_pages-0.1.0/src/pages/core_properties.py +126 -0
- python_pages-0.1.0/src/pages/data/default.pages +0 -0
- python_pages-0.1.0/src/pages/document.py +215 -0
- python_pages-0.1.0/src/pages/hyperlinks.py +57 -0
- python_pages-0.1.0/src/pages/iwa.py +101 -0
- python_pages-0.1.0/src/pages/metadata.py +117 -0
- python_pages-0.1.0/src/pages/pages.py +365 -0
- python_pages-0.1.0/src/pages/pictures.py +549 -0
- python_pages-0.1.0/src/pages/protobuf.py +162 -0
- python_pages-0.1.0/src/pages/snappy.py +140 -0
- python_pages-0.1.0/src/pages/stylesheet.py +892 -0
- python_pages-0.1.0/src/pages/tables.py +493 -0
- python_pages-0.1.0/src/pages/text.py +294 -0
- python_pages-0.1.0/src/python_pages.egg-info/PKG-INFO +271 -0
- python_pages-0.1.0/src/python_pages.egg-info/SOURCES.txt +38 -0
- python_pages-0.1.0/src/python_pages.egg-info/dependency_links.txt +1 -0
- python_pages-0.1.0/src/python_pages.egg-info/entry_points.txt +2 -0
- python_pages-0.1.0/src/python_pages.egg-info/top_level.txt +1 -0
- python_pages-0.1.0/tests/test_api.py +157 -0
- python_pages-0.1.0/tests/test_comments.py +50 -0
- python_pages-0.1.0/tests/test_creation_properties.py +89 -0
- python_pages-0.1.0/tests/test_end_to_end.py +100 -0
- python_pages-0.1.0/tests/test_hyperlinks_styles.py +70 -0
- python_pages-0.1.0/tests/test_paragraph_format.py +142 -0
- python_pages-0.1.0/tests/test_pictures.py +64 -0
- python_pages-0.1.0/tests/test_python_docx_compat.py +118 -0
- python_pages-0.1.0/tests/test_python_docx_ledger.py +27 -0
- python_pages-0.1.0/tests/test_sections.py +86 -0
- python_pages-0.1.0/tests/test_tables.py +61 -0
- python_pages-0.1.0/tests/test_text_editing.py +208 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuki Tamura
|
|
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,271 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-pages
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A python-docx-inspired API for Apple Pages documents
|
|
5
|
+
Author-email: Yuki Tamura <isoparametric@isoparametric.xyz>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/isoparametric/python-pages
|
|
8
|
+
Project-URL: Issues, https://github.com/isoparametric/python-pages/issues
|
|
9
|
+
Keywords: apple-pages,document,iwork,python-docx
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Office/Business :: Office Suites
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# python-pages
|
|
25
|
+
|
|
26
|
+
`python-pages` is a Python library for reading and editing modern Apple Pages
|
|
27
|
+
documents with an object model inspired by
|
|
28
|
+
[`python-docx`](https://python-docx.readthedocs.io/). The distribution name is
|
|
29
|
+
`python-pages`; the import name is `pages`:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from pages import Document
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The library edits `.pages` packages directly. It does not launch Pages, use
|
|
36
|
+
AppleScript, or automate the GUI. Its runtime is dependency-free and consists
|
|
37
|
+
of a ZIP container layer, a lossless protobuf-wire layer, an iWork Snappy/IWA
|
|
38
|
+
layer, and a public document API.
|
|
39
|
+
|
|
40
|
+
This project is alpha-quality. Keep the source document and inspect important
|
|
41
|
+
outputs in Pages before using them in production.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
From a local checkout:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
python3 -m venv .venv
|
|
49
|
+
source .venv/bin/activate
|
|
50
|
+
python3 -m pip install --upgrade pip
|
|
51
|
+
python3 -m pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The editable install provides both `import pages` and the `python-pages`
|
|
55
|
+
command. To run without installing, prefix commands with `PYTHONPATH=src`.
|
|
56
|
+
|
|
57
|
+
The project requires Python 3.11 or newer. There are no runtime dependencies.
|
|
58
|
+
|
|
59
|
+
The requested `pages` import name is also used by an unrelated PyPI static-site
|
|
60
|
+
generator. Install `python-pages` in a dedicated virtual environment and do not
|
|
61
|
+
co-install that distribution; both projects would otherwise provide the same
|
|
62
|
+
top-level import name.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pages import (
|
|
68
|
+
Document,
|
|
69
|
+
Inches,
|
|
70
|
+
Pt,
|
|
71
|
+
RGBColor,
|
|
72
|
+
WD_ALIGN_PARAGRAPH,
|
|
73
|
+
WD_COLOR_INDEX,
|
|
74
|
+
WD_ORIENT,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
document = Document("input.pages")
|
|
78
|
+
|
|
79
|
+
paragraph = document.paragraphs[0]
|
|
80
|
+
run = paragraph.runs[0]
|
|
81
|
+
print(paragraph.text, run.text)
|
|
82
|
+
|
|
83
|
+
# Character formatting uses python-docx-style tri-state values.
|
|
84
|
+
run.bold = True
|
|
85
|
+
run.italic = False
|
|
86
|
+
run.underline = None
|
|
87
|
+
run.font.name = "Helvetica-Bold"
|
|
88
|
+
run.font.size = Pt(14)
|
|
89
|
+
run.font.color.rgb = RGBColor(0x22, 0x66, 0xCC)
|
|
90
|
+
run.font.strike = True
|
|
91
|
+
run.font.subscript = False
|
|
92
|
+
run.font.superscript = True
|
|
93
|
+
|
|
94
|
+
# Pages stores the observed text background at paragraph granularity.
|
|
95
|
+
paragraph.highlight_color = WD_COLOR_INDEX.YELLOW
|
|
96
|
+
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
97
|
+
paragraph.paragraph_format.line_spacing = Pt(24)
|
|
98
|
+
paragraph.paragraph_format.space_before = Pt(12)
|
|
99
|
+
paragraph.paragraph_format.left_indent = Inches(0.25)
|
|
100
|
+
|
|
101
|
+
paragraph.add_run(" Appended text")
|
|
102
|
+
document.add_paragraph("A new body paragraph")
|
|
103
|
+
document.add_heading("A new heading", level=1)
|
|
104
|
+
document.add_page_break()
|
|
105
|
+
|
|
106
|
+
section = document.sections[0]
|
|
107
|
+
section.orientation = WD_ORIENT.LANDSCAPE
|
|
108
|
+
section.page_width, section.page_height = section.page_height, section.page_width
|
|
109
|
+
section.left_margin = Inches(0.75)
|
|
110
|
+
section.header.paragraphs[0].text = "Header text"
|
|
111
|
+
|
|
112
|
+
document.save("output.pages")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`Document()` without a path starts with a bundled minimal Pages template.
|
|
116
|
+
The template contains blank body text/previews and a neutral white placeholder
|
|
117
|
+
for an otherwise unreachable image graph. The input path is never modified;
|
|
118
|
+
changes are written only by `save()`.
|
|
119
|
+
|
|
120
|
+
## API coverage
|
|
121
|
+
|
|
122
|
+
The public API intentionally follows python-docx naming where Pages has a
|
|
123
|
+
reasonable counterpart:
|
|
124
|
+
|
|
125
|
+
| Area | python-pages support |
|
|
126
|
+
|---|---|
|
|
127
|
+
| Documents | `Document(path)`, `Document()`, `save()` |
|
|
128
|
+
| Text | `paragraphs`, `Paragraph.text`, `runs`, `Run.text`, insertion and deletion |
|
|
129
|
+
| Fonts | bold, italic, underline, strike, name, size, RGB color, subscript, superscript |
|
|
130
|
+
| Paragraphs | alignment, line spacing, spacing before/after, indents, tab stops, named styles |
|
|
131
|
+
| Sections | one Pages section with page size, margins, orientation, headers, and footers |
|
|
132
|
+
| Tables | read rows/cells, edit materialized text cells, clone a blank table with `add_table()` |
|
|
133
|
+
| Pictures | inspect/replace PNG images and clone an inline image with `add_picture()` |
|
|
134
|
+
| Links and styles | inspect/edit existing hyperlinks; enumerate/apply named paragraph and character styles |
|
|
135
|
+
| Properties | python-docx-shaped `core_properties` facade |
|
|
136
|
+
| Comments | read comments, replies, authors, anchors, and edit existing comment text |
|
|
137
|
+
|
|
138
|
+
Examples for additional object types:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
table = document.tables[0]
|
|
142
|
+
print([[cell.text for cell in row.cells] for row in table.rows])
|
|
143
|
+
table.cell(1, 1).text = "Updated cell"
|
|
144
|
+
new_table = document.add_table(rows=3, cols=4)
|
|
145
|
+
|
|
146
|
+
picture = document.inline_shapes[0]
|
|
147
|
+
print(picture.filename, picture.width, picture.height)
|
|
148
|
+
picture.replace("replacement.png", "replacement-256px-thumbnail.png")
|
|
149
|
+
new_picture = document.add_picture("new-image.png", width=Inches(4))
|
|
150
|
+
|
|
151
|
+
hyperlink = document.hyperlinks[0]
|
|
152
|
+
hyperlink.text = "Project website"
|
|
153
|
+
hyperlink.address = "https://example.com"
|
|
154
|
+
|
|
155
|
+
for comment in document.comments:
|
|
156
|
+
print(comment.author, comment.anchor_text, comment.text)
|
|
157
|
+
print([reply.text for reply in comment.replies])
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Differences from python-docx
|
|
161
|
+
|
|
162
|
+
`python-pages` is API-inspired, not a byte-format adapter for DOCX. Important
|
|
163
|
+
differences follow from the Pages data model:
|
|
164
|
+
|
|
165
|
+
- Pages page geometry is package-wide, so `Document.sections` contains one
|
|
166
|
+
section. Multiple DOCX sections and linked-to-previous behavior are not
|
|
167
|
+
emulated.
|
|
168
|
+
- A Pages header or footer is exposed as its left, center, and right text
|
|
169
|
+
fragments rather than as a WordprocessingML story part.
|
|
170
|
+
- The observed Pages text-background property belongs to an anonymous
|
|
171
|
+
paragraph style. `Run.font.highlight_color` is retained for call-shape
|
|
172
|
+
compatibility but reads or writes the run's entire containing paragraph.
|
|
173
|
+
- `core_properties` values without a native Pages counterpart are stored in
|
|
174
|
+
`Metadata/PythonPagesCoreProperties.plist`. Documents written by the earlier
|
|
175
|
+
project name remain readable.
|
|
176
|
+
- `add_table()` needs an existing Pages-authored table in the target document
|
|
177
|
+
as a structural template. It supports one tile of up to 256 rows and does
|
|
178
|
+
not implement cell merge or sparse-cell materialization.
|
|
179
|
+
- `add_picture()` needs an existing Pages-authored image graph in the target
|
|
180
|
+
document. New and replacement media are PNG; automatic thumbnails support
|
|
181
|
+
non-interlaced 8-bit RGB/RGBA PNG.
|
|
182
|
+
- Existing comments and replies can be read and edited, but creating a new
|
|
183
|
+
comment is intentionally disabled because Pages also writes collaboration
|
|
184
|
+
and view-state objects whose lifecycle is not yet established.
|
|
185
|
+
- Existing hyperlinks can be edited. Creating a new hyperlink is not yet part
|
|
186
|
+
of the public API.
|
|
187
|
+
- Pages formats are undocumented and can change between application versions.
|
|
188
|
+
The implementation preserves unknown protobuf fields where possible, but it
|
|
189
|
+
cannot guarantee support for every historical or future Pages variant.
|
|
190
|
+
|
|
191
|
+
The detailed compatibility ledger and reverse-engineering evidence are in
|
|
192
|
+
[REPORT.md](REPORT.md).
|
|
193
|
+
|
|
194
|
+
## Command-line interface
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
python-pages inspect input.pages
|
|
198
|
+
python-pages bold input.pages output.pages "unique body substring"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Without installation:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
PYTHONPATH=src python3 -m pages.cli inspect input.pages
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The `bold` helper expects an unambiguous substring unless `--occurrence` is
|
|
208
|
+
provided. It converts Python string offsets to the UTF-16 offsets used by Pages.
|
|
209
|
+
|
|
210
|
+
## Optional test fixtures are not distributed
|
|
211
|
+
|
|
212
|
+
The test suite is self-contained and passes from a standalone checkout. Private
|
|
213
|
+
`.pages` files used for Pages.app interoperability checks can contain
|
|
214
|
+
copyrighted or sensitive content, so `samples/` and generated `output/`
|
|
215
|
+
packages are excluded from Git and distributions.
|
|
216
|
+
|
|
217
|
+
To enable the additional real-document tests, place a Pages-authored package at
|
|
218
|
+
`samples/local_fixture.pages`. The tests activate automatically with
|
|
219
|
+
`unittest.skipUnless` when the file is present. It must contain:
|
|
220
|
+
|
|
221
|
+
- at least one inline PNG image;
|
|
222
|
+
- at least one nonempty table;
|
|
223
|
+
- at least one hyperlink; and
|
|
224
|
+
- at least one comment with a reply.
|
|
225
|
+
|
|
226
|
+
No particular headings, formatted runs, or document text are required by these
|
|
227
|
+
optional tests. Use only content you are allowed to publish and do not commit
|
|
228
|
+
the fixture.
|
|
229
|
+
|
|
230
|
+
To enable the python-docx compatibility-ledger audit, place the upstream source
|
|
231
|
+
tree at `ref/python-docx/`. Its tests must be under `ref/python-docx/tests/`;
|
|
232
|
+
the ledger test also activates automatically when that directory is present.
|
|
233
|
+
|
|
234
|
+
Run the suite with:
|
|
235
|
+
|
|
236
|
+
```sh
|
|
237
|
+
python3 -m unittest discover -s tests -v
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Use the decoded-IWA audit tool for every write path:
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
PYTHONPATH=src python3 tools/diff_iwa.py before.pages after.pages
|
|
244
|
+
unzip -t after.pages
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Architecture
|
|
248
|
+
|
|
249
|
+
- Low level: `snappy.py`, `protobuf.py`, and `iwa.py` provide lossless wire
|
|
250
|
+
parsing and 64-KiB iWork frame handling.
|
|
251
|
+
- Middle level: `pages.py`, `metadata.py`, `stylesheet.py`, `text.py`, and the
|
|
252
|
+
component-specific modules traverse and mutate Pages object graphs.
|
|
253
|
+
- High level: `api.py` provides the python-docx-shaped object model exported by
|
|
254
|
+
`pages`.
|
|
255
|
+
|
|
256
|
+
Synthesized styles are registered in the stylesheet, parent/child map,
|
|
257
|
+
MessageInfo references, PackageMetadata UUID map, and cross-component reference
|
|
258
|
+
map. Untouched ZIP members and IWA payloads are preserved byte-for-byte; changed
|
|
259
|
+
members are re-encoded into valid Pages packages.
|
|
260
|
+
|
|
261
|
+
## Project status
|
|
262
|
+
|
|
263
|
+
The compatibility ledger accounts for all 59 python-docx test files and 662
|
|
264
|
+
discovered behavior methods as migrated, partially migrated, or format-specific
|
|
265
|
+
exclusions. When `ref/python-docx/` is present, the optional ledger test verifies
|
|
266
|
+
that the upstream checkout contains behavior tests. The dependency-free test
|
|
267
|
+
suite runs standalone; optional fixture and ledger tests activate automatically
|
|
268
|
+
when their local inputs are present.
|
|
269
|
+
|
|
270
|
+
`python-pages` is an independent project and is not affiliated with Apple or
|
|
271
|
+
the python-docx project.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# python-pages
|
|
2
|
+
|
|
3
|
+
`python-pages` is a Python library for reading and editing modern Apple Pages
|
|
4
|
+
documents with an object model inspired by
|
|
5
|
+
[`python-docx`](https://python-docx.readthedocs.io/). The distribution name is
|
|
6
|
+
`python-pages`; the import name is `pages`:
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from pages import Document
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The library edits `.pages` packages directly. It does not launch Pages, use
|
|
13
|
+
AppleScript, or automate the GUI. Its runtime is dependency-free and consists
|
|
14
|
+
of a ZIP container layer, a lossless protobuf-wire layer, an iWork Snappy/IWA
|
|
15
|
+
layer, and a public document API.
|
|
16
|
+
|
|
17
|
+
This project is alpha-quality. Keep the source document and inspect important
|
|
18
|
+
outputs in Pages before using them in production.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
From a local checkout:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
python3 -m venv .venv
|
|
26
|
+
source .venv/bin/activate
|
|
27
|
+
python3 -m pip install --upgrade pip
|
|
28
|
+
python3 -m pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The editable install provides both `import pages` and the `python-pages`
|
|
32
|
+
command. To run without installing, prefix commands with `PYTHONPATH=src`.
|
|
33
|
+
|
|
34
|
+
The project requires Python 3.11 or newer. There are no runtime dependencies.
|
|
35
|
+
|
|
36
|
+
The requested `pages` import name is also used by an unrelated PyPI static-site
|
|
37
|
+
generator. Install `python-pages` in a dedicated virtual environment and do not
|
|
38
|
+
co-install that distribution; both projects would otherwise provide the same
|
|
39
|
+
top-level import name.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pages import (
|
|
45
|
+
Document,
|
|
46
|
+
Inches,
|
|
47
|
+
Pt,
|
|
48
|
+
RGBColor,
|
|
49
|
+
WD_ALIGN_PARAGRAPH,
|
|
50
|
+
WD_COLOR_INDEX,
|
|
51
|
+
WD_ORIENT,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
document = Document("input.pages")
|
|
55
|
+
|
|
56
|
+
paragraph = document.paragraphs[0]
|
|
57
|
+
run = paragraph.runs[0]
|
|
58
|
+
print(paragraph.text, run.text)
|
|
59
|
+
|
|
60
|
+
# Character formatting uses python-docx-style tri-state values.
|
|
61
|
+
run.bold = True
|
|
62
|
+
run.italic = False
|
|
63
|
+
run.underline = None
|
|
64
|
+
run.font.name = "Helvetica-Bold"
|
|
65
|
+
run.font.size = Pt(14)
|
|
66
|
+
run.font.color.rgb = RGBColor(0x22, 0x66, 0xCC)
|
|
67
|
+
run.font.strike = True
|
|
68
|
+
run.font.subscript = False
|
|
69
|
+
run.font.superscript = True
|
|
70
|
+
|
|
71
|
+
# Pages stores the observed text background at paragraph granularity.
|
|
72
|
+
paragraph.highlight_color = WD_COLOR_INDEX.YELLOW
|
|
73
|
+
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
74
|
+
paragraph.paragraph_format.line_spacing = Pt(24)
|
|
75
|
+
paragraph.paragraph_format.space_before = Pt(12)
|
|
76
|
+
paragraph.paragraph_format.left_indent = Inches(0.25)
|
|
77
|
+
|
|
78
|
+
paragraph.add_run(" Appended text")
|
|
79
|
+
document.add_paragraph("A new body paragraph")
|
|
80
|
+
document.add_heading("A new heading", level=1)
|
|
81
|
+
document.add_page_break()
|
|
82
|
+
|
|
83
|
+
section = document.sections[0]
|
|
84
|
+
section.orientation = WD_ORIENT.LANDSCAPE
|
|
85
|
+
section.page_width, section.page_height = section.page_height, section.page_width
|
|
86
|
+
section.left_margin = Inches(0.75)
|
|
87
|
+
section.header.paragraphs[0].text = "Header text"
|
|
88
|
+
|
|
89
|
+
document.save("output.pages")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`Document()` without a path starts with a bundled minimal Pages template.
|
|
93
|
+
The template contains blank body text/previews and a neutral white placeholder
|
|
94
|
+
for an otherwise unreachable image graph. The input path is never modified;
|
|
95
|
+
changes are written only by `save()`.
|
|
96
|
+
|
|
97
|
+
## API coverage
|
|
98
|
+
|
|
99
|
+
The public API intentionally follows python-docx naming where Pages has a
|
|
100
|
+
reasonable counterpart:
|
|
101
|
+
|
|
102
|
+
| Area | python-pages support |
|
|
103
|
+
|---|---|
|
|
104
|
+
| Documents | `Document(path)`, `Document()`, `save()` |
|
|
105
|
+
| Text | `paragraphs`, `Paragraph.text`, `runs`, `Run.text`, insertion and deletion |
|
|
106
|
+
| Fonts | bold, italic, underline, strike, name, size, RGB color, subscript, superscript |
|
|
107
|
+
| Paragraphs | alignment, line spacing, spacing before/after, indents, tab stops, named styles |
|
|
108
|
+
| Sections | one Pages section with page size, margins, orientation, headers, and footers |
|
|
109
|
+
| Tables | read rows/cells, edit materialized text cells, clone a blank table with `add_table()` |
|
|
110
|
+
| Pictures | inspect/replace PNG images and clone an inline image with `add_picture()` |
|
|
111
|
+
| Links and styles | inspect/edit existing hyperlinks; enumerate/apply named paragraph and character styles |
|
|
112
|
+
| Properties | python-docx-shaped `core_properties` facade |
|
|
113
|
+
| Comments | read comments, replies, authors, anchors, and edit existing comment text |
|
|
114
|
+
|
|
115
|
+
Examples for additional object types:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
table = document.tables[0]
|
|
119
|
+
print([[cell.text for cell in row.cells] for row in table.rows])
|
|
120
|
+
table.cell(1, 1).text = "Updated cell"
|
|
121
|
+
new_table = document.add_table(rows=3, cols=4)
|
|
122
|
+
|
|
123
|
+
picture = document.inline_shapes[0]
|
|
124
|
+
print(picture.filename, picture.width, picture.height)
|
|
125
|
+
picture.replace("replacement.png", "replacement-256px-thumbnail.png")
|
|
126
|
+
new_picture = document.add_picture("new-image.png", width=Inches(4))
|
|
127
|
+
|
|
128
|
+
hyperlink = document.hyperlinks[0]
|
|
129
|
+
hyperlink.text = "Project website"
|
|
130
|
+
hyperlink.address = "https://example.com"
|
|
131
|
+
|
|
132
|
+
for comment in document.comments:
|
|
133
|
+
print(comment.author, comment.anchor_text, comment.text)
|
|
134
|
+
print([reply.text for reply in comment.replies])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Differences from python-docx
|
|
138
|
+
|
|
139
|
+
`python-pages` is API-inspired, not a byte-format adapter for DOCX. Important
|
|
140
|
+
differences follow from the Pages data model:
|
|
141
|
+
|
|
142
|
+
- Pages page geometry is package-wide, so `Document.sections` contains one
|
|
143
|
+
section. Multiple DOCX sections and linked-to-previous behavior are not
|
|
144
|
+
emulated.
|
|
145
|
+
- A Pages header or footer is exposed as its left, center, and right text
|
|
146
|
+
fragments rather than as a WordprocessingML story part.
|
|
147
|
+
- The observed Pages text-background property belongs to an anonymous
|
|
148
|
+
paragraph style. `Run.font.highlight_color` is retained for call-shape
|
|
149
|
+
compatibility but reads or writes the run's entire containing paragraph.
|
|
150
|
+
- `core_properties` values without a native Pages counterpart are stored in
|
|
151
|
+
`Metadata/PythonPagesCoreProperties.plist`. Documents written by the earlier
|
|
152
|
+
project name remain readable.
|
|
153
|
+
- `add_table()` needs an existing Pages-authored table in the target document
|
|
154
|
+
as a structural template. It supports one tile of up to 256 rows and does
|
|
155
|
+
not implement cell merge or sparse-cell materialization.
|
|
156
|
+
- `add_picture()` needs an existing Pages-authored image graph in the target
|
|
157
|
+
document. New and replacement media are PNG; automatic thumbnails support
|
|
158
|
+
non-interlaced 8-bit RGB/RGBA PNG.
|
|
159
|
+
- Existing comments and replies can be read and edited, but creating a new
|
|
160
|
+
comment is intentionally disabled because Pages also writes collaboration
|
|
161
|
+
and view-state objects whose lifecycle is not yet established.
|
|
162
|
+
- Existing hyperlinks can be edited. Creating a new hyperlink is not yet part
|
|
163
|
+
of the public API.
|
|
164
|
+
- Pages formats are undocumented and can change between application versions.
|
|
165
|
+
The implementation preserves unknown protobuf fields where possible, but it
|
|
166
|
+
cannot guarantee support for every historical or future Pages variant.
|
|
167
|
+
|
|
168
|
+
The detailed compatibility ledger and reverse-engineering evidence are in
|
|
169
|
+
[REPORT.md](REPORT.md).
|
|
170
|
+
|
|
171
|
+
## Command-line interface
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
python-pages inspect input.pages
|
|
175
|
+
python-pages bold input.pages output.pages "unique body substring"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Without installation:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
PYTHONPATH=src python3 -m pages.cli inspect input.pages
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The `bold` helper expects an unambiguous substring unless `--occurrence` is
|
|
185
|
+
provided. It converts Python string offsets to the UTF-16 offsets used by Pages.
|
|
186
|
+
|
|
187
|
+
## Optional test fixtures are not distributed
|
|
188
|
+
|
|
189
|
+
The test suite is self-contained and passes from a standalone checkout. Private
|
|
190
|
+
`.pages` files used for Pages.app interoperability checks can contain
|
|
191
|
+
copyrighted or sensitive content, so `samples/` and generated `output/`
|
|
192
|
+
packages are excluded from Git and distributions.
|
|
193
|
+
|
|
194
|
+
To enable the additional real-document tests, place a Pages-authored package at
|
|
195
|
+
`samples/local_fixture.pages`. The tests activate automatically with
|
|
196
|
+
`unittest.skipUnless` when the file is present. It must contain:
|
|
197
|
+
|
|
198
|
+
- at least one inline PNG image;
|
|
199
|
+
- at least one nonempty table;
|
|
200
|
+
- at least one hyperlink; and
|
|
201
|
+
- at least one comment with a reply.
|
|
202
|
+
|
|
203
|
+
No particular headings, formatted runs, or document text are required by these
|
|
204
|
+
optional tests. Use only content you are allowed to publish and do not commit
|
|
205
|
+
the fixture.
|
|
206
|
+
|
|
207
|
+
To enable the python-docx compatibility-ledger audit, place the upstream source
|
|
208
|
+
tree at `ref/python-docx/`. Its tests must be under `ref/python-docx/tests/`;
|
|
209
|
+
the ledger test also activates automatically when that directory is present.
|
|
210
|
+
|
|
211
|
+
Run the suite with:
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
python3 -m unittest discover -s tests -v
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Use the decoded-IWA audit tool for every write path:
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
PYTHONPATH=src python3 tools/diff_iwa.py before.pages after.pages
|
|
221
|
+
unzip -t after.pages
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Architecture
|
|
225
|
+
|
|
226
|
+
- Low level: `snappy.py`, `protobuf.py`, and `iwa.py` provide lossless wire
|
|
227
|
+
parsing and 64-KiB iWork frame handling.
|
|
228
|
+
- Middle level: `pages.py`, `metadata.py`, `stylesheet.py`, `text.py`, and the
|
|
229
|
+
component-specific modules traverse and mutate Pages object graphs.
|
|
230
|
+
- High level: `api.py` provides the python-docx-shaped object model exported by
|
|
231
|
+
`pages`.
|
|
232
|
+
|
|
233
|
+
Synthesized styles are registered in the stylesheet, parent/child map,
|
|
234
|
+
MessageInfo references, PackageMetadata UUID map, and cross-component reference
|
|
235
|
+
map. Untouched ZIP members and IWA payloads are preserved byte-for-byte; changed
|
|
236
|
+
members are re-encoded into valid Pages packages.
|
|
237
|
+
|
|
238
|
+
## Project status
|
|
239
|
+
|
|
240
|
+
The compatibility ledger accounts for all 59 python-docx test files and 662
|
|
241
|
+
discovered behavior methods as migrated, partially migrated, or format-specific
|
|
242
|
+
exclusions. When `ref/python-docx/` is present, the optional ledger test verifies
|
|
243
|
+
that the upstream checkout contains behavior tests. The dependency-free test
|
|
244
|
+
suite runs standalone; optional fixture and ledger tests activate automatically
|
|
245
|
+
when their local inputs are present.
|
|
246
|
+
|
|
247
|
+
`python-pages` is an independent project and is not affiliated with Apple or
|
|
248
|
+
the python-docx project.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "python-pages"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A python-docx-inspired API for Apple Pages documents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Yuki Tamura", email = "isoparametric@isoparametric.xyz" }]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
keywords = ["apple-pages", "document", "iwork", "python-docx"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Office/Business :: Office Suites",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/isoparametric/python-pages"
|
|
29
|
+
Issues = "https://github.com/isoparametric/python-pages/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
python-pages = "pages.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
pages = ["data/*.pages"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Dependency-free Apple Pages IWA reading and character formatting."""
|
|
2
|
+
|
|
3
|
+
from .api import (
|
|
4
|
+
AnnotationHighlight,
|
|
5
|
+
Cm,
|
|
6
|
+
Comment,
|
|
7
|
+
Comments,
|
|
8
|
+
Document,
|
|
9
|
+
Inches,
|
|
10
|
+
InlineShape,
|
|
11
|
+
Hyperlink,
|
|
12
|
+
Style,
|
|
13
|
+
Styles,
|
|
14
|
+
Length,
|
|
15
|
+
Pt,
|
|
16
|
+
Section,
|
|
17
|
+
Table,
|
|
18
|
+
TableCell,
|
|
19
|
+
WD_ALIGN_PARAGRAPH,
|
|
20
|
+
WD_COLOR,
|
|
21
|
+
WD_COLOR_INDEX,
|
|
22
|
+
WD_LINE_SPACING,
|
|
23
|
+
WD_PARAGRAPH_ALIGNMENT,
|
|
24
|
+
WD_TAB_ALIGNMENT,
|
|
25
|
+
WD_TAB_LEADER,
|
|
26
|
+
WD_ORIENT,
|
|
27
|
+
WD_ORIENTATION,
|
|
28
|
+
WD_STYLE_TYPE,
|
|
29
|
+
)
|
|
30
|
+
from .pages import PagesDocument
|
|
31
|
+
from .core_properties import CoreProperties
|
|
32
|
+
from .stylesheet import CharacterProperties, ParagraphProperties, RGBColor
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AnnotationHighlight",
|
|
36
|
+
"CharacterProperties",
|
|
37
|
+
"Cm",
|
|
38
|
+
"Comment",
|
|
39
|
+
"Comments",
|
|
40
|
+
"CoreProperties",
|
|
41
|
+
"Document",
|
|
42
|
+
"Inches",
|
|
43
|
+
"InlineShape",
|
|
44
|
+
"Hyperlink",
|
|
45
|
+
"Style",
|
|
46
|
+
"Styles",
|
|
47
|
+
"Length",
|
|
48
|
+
"PagesDocument",
|
|
49
|
+
"ParagraphProperties",
|
|
50
|
+
"Pt",
|
|
51
|
+
"RGBColor",
|
|
52
|
+
"Section",
|
|
53
|
+
"Table",
|
|
54
|
+
"TableCell",
|
|
55
|
+
"WD_ALIGN_PARAGRAPH",
|
|
56
|
+
"WD_COLOR",
|
|
57
|
+
"WD_COLOR_INDEX",
|
|
58
|
+
"WD_LINE_SPACING",
|
|
59
|
+
"WD_PARAGRAPH_ALIGNMENT",
|
|
60
|
+
"WD_TAB_ALIGNMENT",
|
|
61
|
+
"WD_TAB_LEADER",
|
|
62
|
+
"WD_ORIENT",
|
|
63
|
+
"WD_ORIENTATION",
|
|
64
|
+
"WD_STYLE_TYPE",
|
|
65
|
+
]
|