python-pages 0.1.0__py3-none-any.whl
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.
- pages/__init__.py +65 -0
- pages/api.py +1869 -0
- pages/cli.py +43 -0
- pages/comments.py +168 -0
- pages/components.py +403 -0
- pages/core_properties.py +126 -0
- pages/data/default.pages +0 -0
- pages/document.py +215 -0
- pages/hyperlinks.py +57 -0
- pages/iwa.py +101 -0
- pages/metadata.py +117 -0
- pages/pages.py +365 -0
- pages/pictures.py +549 -0
- pages/protobuf.py +162 -0
- pages/snappy.py +140 -0
- pages/stylesheet.py +892 -0
- pages/tables.py +493 -0
- pages/text.py +294 -0
- python_pages-0.1.0.dist-info/METADATA +271 -0
- python_pages-0.1.0.dist-info/RECORD +24 -0
- python_pages-0.1.0.dist-info/WHEEL +5 -0
- python_pages-0.1.0.dist-info/entry_points.txt +2 -0
- python_pages-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_pages-0.1.0.dist-info/top_level.txt +1 -0
pages/api.py
ADDED
|
@@ -0,0 +1,1869 @@
|
|
|
1
|
+
"""Small python-docx-shaped object model backed by Pages IWA storage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .core_properties import CoreProperties
|
|
9
|
+
from .document import (
|
|
10
|
+
DOCUMENT_BOTTOM_MARGIN,
|
|
11
|
+
DOCUMENT_FOOTER_MARGIN,
|
|
12
|
+
DOCUMENT_HEADER_MARGIN,
|
|
13
|
+
DOCUMENT_LEFT_MARGIN,
|
|
14
|
+
DOCUMENT_ORIENTATION,
|
|
15
|
+
DOCUMENT_PAGE_HEIGHT,
|
|
16
|
+
DOCUMENT_PAGE_WIDTH,
|
|
17
|
+
DOCUMENT_RIGHT_MARGIN,
|
|
18
|
+
DOCUMENT_TOP_MARGIN,
|
|
19
|
+
SECTION_FIRST_PAGE_DIFFERENT,
|
|
20
|
+
DocumentLayout,
|
|
21
|
+
)
|
|
22
|
+
from .pages import PagesDocument
|
|
23
|
+
from .stylesheet import (
|
|
24
|
+
CharacterProperties,
|
|
25
|
+
LineSpacing,
|
|
26
|
+
ParagraphProperties,
|
|
27
|
+
ParagraphTab,
|
|
28
|
+
RGBColor,
|
|
29
|
+
character_style_properties,
|
|
30
|
+
paragraph_style_properties,
|
|
31
|
+
replace_character_background,
|
|
32
|
+
is_anonymous_style,
|
|
33
|
+
style_identifier,
|
|
34
|
+
style_name,
|
|
35
|
+
)
|
|
36
|
+
from .text import (
|
|
37
|
+
STORAGE_TABLE_CHAR_STYLE,
|
|
38
|
+
STORAGE_TABLE_PARA_STYLE,
|
|
39
|
+
TYPE_CHARACTER_STYLE,
|
|
40
|
+
TYPE_PARAGRAPH_STYLE,
|
|
41
|
+
AttributeRun,
|
|
42
|
+
add_object_reference,
|
|
43
|
+
codepoint_to_utf16,
|
|
44
|
+
encode_attribute_table,
|
|
45
|
+
parse_attribute_table,
|
|
46
|
+
replace_style_range,
|
|
47
|
+
replace_storage_text,
|
|
48
|
+
style_at,
|
|
49
|
+
utf16_to_codepoint,
|
|
50
|
+
)
|
|
51
|
+
from .components import append_attachment_run
|
|
52
|
+
from .tables import CellStorage, TableStore, body_table_stores, clone_body_table
|
|
53
|
+
from .pictures import ImageStore, body_image_stores, clone_body_image
|
|
54
|
+
from .hyperlinks import HyperlinkStore, storage_hyperlinks
|
|
55
|
+
from .comments import CommentStore, storage_comments
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Length(float):
|
|
59
|
+
"""Length stored internally in points."""
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def pt(self) -> float:
|
|
63
|
+
return float(self)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def inches(self) -> float:
|
|
67
|
+
return float(self) / 72.0
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def cm(self) -> float:
|
|
71
|
+
return self.inches * 2.54
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Pt(Length):
|
|
75
|
+
"""Point measure compatible with the common ``Pt(12)`` calling shape."""
|
|
76
|
+
|
|
77
|
+
def __new__(cls, value: float):
|
|
78
|
+
return super().__new__(cls, float(value))
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def pt(self) -> float:
|
|
82
|
+
return float(self)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Inches(Length):
|
|
86
|
+
def __new__(cls, value: float):
|
|
87
|
+
return super().__new__(cls, float(value) * 72.0)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Cm(Length):
|
|
91
|
+
def __new__(cls, value: float):
|
|
92
|
+
return super().__new__(cls, float(value) * 72.0 / 2.54)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class WD_PARAGRAPH_ALIGNMENT(IntEnum):
|
|
96
|
+
LEFT = 0
|
|
97
|
+
CENTER = 1
|
|
98
|
+
RIGHT = 2
|
|
99
|
+
JUSTIFY = 3
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
WD_ALIGN_PARAGRAPH = WD_PARAGRAPH_ALIGNMENT
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class WD_LINE_SPACING(IntEnum):
|
|
106
|
+
SINGLE = 0
|
|
107
|
+
ONE_POINT_FIVE = 1
|
|
108
|
+
DOUBLE = 2
|
|
109
|
+
AT_LEAST = 3
|
|
110
|
+
EXACTLY = 4
|
|
111
|
+
MULTIPLE = 5
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class WD_TAB_ALIGNMENT(IntEnum):
|
|
115
|
+
LEFT = 0
|
|
116
|
+
CENTER = 1
|
|
117
|
+
RIGHT = 2
|
|
118
|
+
DECIMAL = 3
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class WD_TAB_LEADER(IntEnum):
|
|
122
|
+
SPACES = 0
|
|
123
|
+
DOTS = 1
|
|
124
|
+
DASHES = 2
|
|
125
|
+
LINES = 3
|
|
126
|
+
HEAVY = 4
|
|
127
|
+
MIDDLE_DOT = 5
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class WD_ORIENTATION(IntEnum):
|
|
131
|
+
PORTRAIT = 0
|
|
132
|
+
LANDSCAPE = 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
WD_ORIENT = WD_ORIENTATION
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class WD_STYLE_TYPE(IntEnum):
|
|
139
|
+
PARAGRAPH = 1
|
|
140
|
+
CHARACTER = 2
|
|
141
|
+
TABLE = 3
|
|
142
|
+
LIST = 4
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class WD_COLOR_INDEX(IntEnum):
|
|
146
|
+
"""Standard python-docx highlight-color palette."""
|
|
147
|
+
|
|
148
|
+
INHERITED = -1
|
|
149
|
+
AUTO = 0
|
|
150
|
+
BLACK = 1
|
|
151
|
+
BLUE = 2
|
|
152
|
+
TURQUOISE = 3
|
|
153
|
+
BRIGHT_GREEN = 4
|
|
154
|
+
PINK = 5
|
|
155
|
+
RED = 6
|
|
156
|
+
YELLOW = 7
|
|
157
|
+
WHITE = 8
|
|
158
|
+
DARK_BLUE = 9
|
|
159
|
+
TEAL = 10
|
|
160
|
+
GREEN = 11
|
|
161
|
+
VIOLET = 12
|
|
162
|
+
DARK_RED = 13
|
|
163
|
+
DARK_YELLOW = 14
|
|
164
|
+
GRAY_50 = 15
|
|
165
|
+
GRAY_25 = 16
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
WD_COLOR = WD_COLOR_INDEX
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
_API_TO_PAGES_ALIGNMENT = {
|
|
172
|
+
WD_PARAGRAPH_ALIGNMENT.LEFT: 0,
|
|
173
|
+
WD_PARAGRAPH_ALIGNMENT.RIGHT: 1,
|
|
174
|
+
WD_PARAGRAPH_ALIGNMENT.CENTER: 2,
|
|
175
|
+
WD_PARAGRAPH_ALIGNMENT.JUSTIFY: 3,
|
|
176
|
+
}
|
|
177
|
+
_PAGES_TO_API_ALIGNMENT = {value: key for key, value in _API_TO_PAGES_ALIGNMENT.items()}
|
|
178
|
+
_LEADER_TO_STRING = {
|
|
179
|
+
WD_TAB_LEADER.SPACES: None,
|
|
180
|
+
WD_TAB_LEADER.DOTS: ".",
|
|
181
|
+
WD_TAB_LEADER.DASHES: "-",
|
|
182
|
+
WD_TAB_LEADER.LINES: "_",
|
|
183
|
+
WD_TAB_LEADER.HEAVY: "•",
|
|
184
|
+
WD_TAB_LEADER.MIDDLE_DOT: "·",
|
|
185
|
+
}
|
|
186
|
+
_STRING_TO_LEADER = {value: key for key, value in _LEADER_TO_STRING.items()}
|
|
187
|
+
_HIGHLIGHT_TO_RGB = {
|
|
188
|
+
WD_COLOR_INDEX.AUTO: RGBColor(0x00, 0x00, 0x00),
|
|
189
|
+
WD_COLOR_INDEX.BLACK: RGBColor(0x00, 0x00, 0x00),
|
|
190
|
+
WD_COLOR_INDEX.BLUE: RGBColor(0x00, 0x00, 0xFF),
|
|
191
|
+
WD_COLOR_INDEX.TURQUOISE: RGBColor(0x00, 0xFF, 0xFF),
|
|
192
|
+
WD_COLOR_INDEX.BRIGHT_GREEN: RGBColor(0x00, 0xFF, 0x00),
|
|
193
|
+
WD_COLOR_INDEX.PINK: RGBColor(0xFF, 0x00, 0xFF),
|
|
194
|
+
WD_COLOR_INDEX.RED: RGBColor(0xFF, 0x00, 0x00),
|
|
195
|
+
WD_COLOR_INDEX.YELLOW: RGBColor(0xFF, 0xFF, 0x00),
|
|
196
|
+
WD_COLOR_INDEX.WHITE: RGBColor(0xFF, 0xFF, 0xFF),
|
|
197
|
+
WD_COLOR_INDEX.DARK_BLUE: RGBColor(0x00, 0x00, 0x80),
|
|
198
|
+
WD_COLOR_INDEX.TEAL: RGBColor(0x00, 0x80, 0x80),
|
|
199
|
+
WD_COLOR_INDEX.GREEN: RGBColor(0x00, 0x80, 0x00),
|
|
200
|
+
WD_COLOR_INDEX.VIOLET: RGBColor(0x80, 0x00, 0x80),
|
|
201
|
+
WD_COLOR_INDEX.DARK_RED: RGBColor(0x80, 0x00, 0x00),
|
|
202
|
+
WD_COLOR_INDEX.DARK_YELLOW: RGBColor(0x80, 0x80, 0x00),
|
|
203
|
+
WD_COLOR_INDEX.GRAY_50: RGBColor(0x80, 0x80, 0x80),
|
|
204
|
+
WD_COLOR_INDEX.GRAY_25: RGBColor(0xC0, 0xC0, 0xC0),
|
|
205
|
+
}
|
|
206
|
+
_RGB_TO_HIGHLIGHT = {
|
|
207
|
+
rgb: color
|
|
208
|
+
for color, rgb in _HIGHLIGHT_TO_RGB.items()
|
|
209
|
+
if color is not WD_COLOR_INDEX.AUTO
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_SCRIPT_SUPERSCRIPT = 1
|
|
213
|
+
_SCRIPT_SUBSCRIPT = 2
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ColorFormat:
|
|
217
|
+
def __init__(self, run: "Run"):
|
|
218
|
+
self._run = run
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def rgb(self) -> RGBColor | None:
|
|
222
|
+
return self._run._properties.color
|
|
223
|
+
|
|
224
|
+
@rgb.setter
|
|
225
|
+
def rgb(self, value: RGBColor | str | tuple[int, int, int] | None) -> None:
|
|
226
|
+
if isinstance(value, str):
|
|
227
|
+
value = RGBColor.from_string(value)
|
|
228
|
+
elif isinstance(value, tuple):
|
|
229
|
+
value = RGBColor(*value)
|
|
230
|
+
if value is not None and not isinstance(value, RGBColor):
|
|
231
|
+
raise TypeError("rgb must be RGBColor, a 3-tuple, a hex string, or None")
|
|
232
|
+
self._run._set_property("color", value)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class Font:
|
|
236
|
+
def __init__(self, run: "Run"):
|
|
237
|
+
self._run = run
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def name(self) -> str | None:
|
|
241
|
+
return self._run._properties.font_name
|
|
242
|
+
|
|
243
|
+
@name.setter
|
|
244
|
+
def name(self, value: str | None) -> None:
|
|
245
|
+
if value is not None and not isinstance(value, str):
|
|
246
|
+
raise TypeError("font name must be a string or None")
|
|
247
|
+
self._run._set_property("font_name", value)
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def size(self) -> Pt | None:
|
|
251
|
+
value = self._run._properties.font_size
|
|
252
|
+
return None if value is None else Pt(value)
|
|
253
|
+
|
|
254
|
+
@size.setter
|
|
255
|
+
def size(self, value: Pt | float | int | None) -> None:
|
|
256
|
+
self._run._set_property("font_size", None if value is None else float(value))
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def color(self) -> ColorFormat:
|
|
260
|
+
return ColorFormat(self._run)
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def bold(self) -> bool | None:
|
|
264
|
+
return self._run._properties.bold
|
|
265
|
+
|
|
266
|
+
@bold.setter
|
|
267
|
+
def bold(self, value: bool | None) -> None:
|
|
268
|
+
self._run._set_bool("bold", value)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def italic(self) -> bool | None:
|
|
272
|
+
return self._run._properties.italic
|
|
273
|
+
|
|
274
|
+
@italic.setter
|
|
275
|
+
def italic(self, value: bool | None) -> None:
|
|
276
|
+
self._run._set_bool("italic", value)
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def underline(self) -> bool | None:
|
|
280
|
+
return self._run._properties.underline
|
|
281
|
+
|
|
282
|
+
@underline.setter
|
|
283
|
+
def underline(self, value: bool | None) -> None:
|
|
284
|
+
self._run._set_bool("underline", value)
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def strike(self) -> bool | None:
|
|
288
|
+
return self._run._properties.strike
|
|
289
|
+
|
|
290
|
+
@strike.setter
|
|
291
|
+
def strike(self, value: bool | None) -> None:
|
|
292
|
+
self._run._set_bool("strike", value)
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def subscript(self) -> bool | None:
|
|
296
|
+
value = self._run._properties.vertical_align
|
|
297
|
+
return None if value is None else value == _SCRIPT_SUBSCRIPT
|
|
298
|
+
|
|
299
|
+
@subscript.setter
|
|
300
|
+
def subscript(self, value: bool | None) -> None:
|
|
301
|
+
self._set_script(value, _SCRIPT_SUBSCRIPT, "subscript")
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def superscript(self) -> bool | None:
|
|
305
|
+
value = self._run._properties.vertical_align
|
|
306
|
+
return None if value is None else value == _SCRIPT_SUPERSCRIPT
|
|
307
|
+
|
|
308
|
+
@superscript.setter
|
|
309
|
+
def superscript(self, value: bool | None) -> None:
|
|
310
|
+
self._set_script(value, _SCRIPT_SUPERSCRIPT, "superscript")
|
|
311
|
+
|
|
312
|
+
def _set_script(self, value: bool | None, target: int, name: str) -> None:
|
|
313
|
+
if value is not None and not isinstance(value, bool):
|
|
314
|
+
raise TypeError(f"{name} must be True, False, or None")
|
|
315
|
+
current = self._run._properties.vertical_align
|
|
316
|
+
if value is None:
|
|
317
|
+
replacement = None
|
|
318
|
+
elif value:
|
|
319
|
+
replacement = target
|
|
320
|
+
elif current == target:
|
|
321
|
+
replacement = None
|
|
322
|
+
else:
|
|
323
|
+
return
|
|
324
|
+
self._run._set_property("vertical_align", replacement)
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def highlight_color(self) -> WD_COLOR_INDEX | None:
|
|
328
|
+
paragraph = self._run._paragraph
|
|
329
|
+
if paragraph is None:
|
|
330
|
+
return None
|
|
331
|
+
return paragraph.highlight_color
|
|
332
|
+
|
|
333
|
+
@highlight_color.setter
|
|
334
|
+
def highlight_color(self, value: WD_COLOR_INDEX | int | None) -> None:
|
|
335
|
+
if value is None:
|
|
336
|
+
color = None
|
|
337
|
+
else:
|
|
338
|
+
try:
|
|
339
|
+
palette = WD_COLOR_INDEX(value)
|
|
340
|
+
except ValueError as error:
|
|
341
|
+
raise ValueError(f"unsupported highlight color: {value!r}") from error
|
|
342
|
+
color = (
|
|
343
|
+
None
|
|
344
|
+
if palette is WD_COLOR_INDEX.INHERITED
|
|
345
|
+
else _HIGHLIGHT_TO_RGB[palette]
|
|
346
|
+
)
|
|
347
|
+
paragraph = self._run._paragraph
|
|
348
|
+
if paragraph is None:
|
|
349
|
+
raise NotImplementedError(
|
|
350
|
+
"Pages character background is paragraph-scoped; this run has no paragraph"
|
|
351
|
+
)
|
|
352
|
+
paragraph.background_color = color
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class Run:
|
|
356
|
+
def __init__(
|
|
357
|
+
self,
|
|
358
|
+
document: "Document",
|
|
359
|
+
start_utf16: int,
|
|
360
|
+
end_utf16: int,
|
|
361
|
+
paragraph: "Paragraph | None" = None,
|
|
362
|
+
):
|
|
363
|
+
self._document = document
|
|
364
|
+
self._start_utf16 = start_utf16
|
|
365
|
+
self._end_utf16 = end_utf16
|
|
366
|
+
self._paragraph = paragraph
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def text(self) -> str:
|
|
370
|
+
return self._document._text_between(self._start_utf16, self._end_utf16)
|
|
371
|
+
|
|
372
|
+
@text.setter
|
|
373
|
+
def text(self, value: str) -> None:
|
|
374
|
+
if "\n" in value or "\r" in value:
|
|
375
|
+
raise ValueError("Run.text cannot contain paragraph separators")
|
|
376
|
+
delta = self._document._replace_text(self._start_utf16, self._end_utf16, value)
|
|
377
|
+
self._end_utf16 += delta
|
|
378
|
+
if self._paragraph is not None:
|
|
379
|
+
self._paragraph._end_utf16 += delta
|
|
380
|
+
self._paragraph._next_start_utf16 += delta
|
|
381
|
+
|
|
382
|
+
def clear(self) -> "Run":
|
|
383
|
+
"""Remove this run's text while retaining its character formatting."""
|
|
384
|
+
self.text = ""
|
|
385
|
+
return self
|
|
386
|
+
|
|
387
|
+
def delete(self) -> None:
|
|
388
|
+
"""Remove this run's text from its paragraph."""
|
|
389
|
+
self.clear()
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def _properties(self) -> CharacterProperties:
|
|
393
|
+
return self._document._properties_at(self._start_utf16)
|
|
394
|
+
|
|
395
|
+
def _set_property(self, name: str, value) -> None:
|
|
396
|
+
props = self._properties.with_value(name, value)
|
|
397
|
+
self._document._apply_properties(self._start_utf16, self._end_utf16, props)
|
|
398
|
+
|
|
399
|
+
def _set_bool(self, name: str, value: bool | None) -> None:
|
|
400
|
+
if value is not None and not isinstance(value, bool):
|
|
401
|
+
raise TypeError(f"{name} must be True, False, or None")
|
|
402
|
+
self._set_property(name, value)
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def font(self) -> Font:
|
|
406
|
+
return Font(self)
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def bold(self) -> bool | None:
|
|
410
|
+
return self.font.bold
|
|
411
|
+
|
|
412
|
+
@bold.setter
|
|
413
|
+
def bold(self, value: bool | None) -> None:
|
|
414
|
+
self.font.bold = value
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def italic(self) -> bool | None:
|
|
418
|
+
return self.font.italic
|
|
419
|
+
|
|
420
|
+
@italic.setter
|
|
421
|
+
def italic(self, value: bool | None) -> None:
|
|
422
|
+
self.font.italic = value
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def underline(self) -> bool | None:
|
|
426
|
+
return self.font.underline
|
|
427
|
+
|
|
428
|
+
@underline.setter
|
|
429
|
+
def underline(self, value: bool | None) -> None:
|
|
430
|
+
self.font.underline = value
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def style(self) -> "Style | None":
|
|
434
|
+
return self._document._style_from_id(
|
|
435
|
+
self._document._character_style_id_at(self._start_utf16),
|
|
436
|
+
WD_STYLE_TYPE.CHARACTER,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
@style.setter
|
|
440
|
+
def style(self, value: "Style | str | None") -> None:
|
|
441
|
+
identifier = self._document._coerce_style_id(
|
|
442
|
+
value, WD_STYLE_TYPE.CHARACTER, none_identifier=None
|
|
443
|
+
)
|
|
444
|
+
self._document._apply_character_style_id(
|
|
445
|
+
self._start_utf16, self._end_utf16, identifier
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class TabStop:
|
|
450
|
+
def __init__(self, spec: ParagraphTab):
|
|
451
|
+
self._spec = spec
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def position(self) -> Pt:
|
|
455
|
+
return Pt(self._spec.position)
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
def alignment(self) -> WD_TAB_ALIGNMENT:
|
|
459
|
+
return WD_TAB_ALIGNMENT(self._spec.alignment)
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def leader(self) -> WD_TAB_LEADER:
|
|
463
|
+
return _STRING_TO_LEADER.get(self._spec.leader, WD_TAB_LEADER.SPACES)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class TabStops:
|
|
467
|
+
def __init__(self, paragraph: "Paragraph"):
|
|
468
|
+
self._paragraph = paragraph
|
|
469
|
+
|
|
470
|
+
def _specs(self) -> tuple[ParagraphTab, ...]:
|
|
471
|
+
return self._paragraph._paragraph_properties.tabs or ()
|
|
472
|
+
|
|
473
|
+
def __len__(self) -> int:
|
|
474
|
+
return len(self._specs())
|
|
475
|
+
|
|
476
|
+
def __iter__(self):
|
|
477
|
+
return iter([TabStop(spec) for spec in self._specs()])
|
|
478
|
+
|
|
479
|
+
def __getitem__(self, index: int) -> TabStop:
|
|
480
|
+
return TabStop(self._specs()[index])
|
|
481
|
+
|
|
482
|
+
def add_tab_stop(
|
|
483
|
+
self,
|
|
484
|
+
position: Length | float,
|
|
485
|
+
alignment: WD_TAB_ALIGNMENT = WD_TAB_ALIGNMENT.LEFT,
|
|
486
|
+
leader: WD_TAB_LEADER = WD_TAB_LEADER.SPACES,
|
|
487
|
+
) -> TabStop:
|
|
488
|
+
try:
|
|
489
|
+
alignment = WD_TAB_ALIGNMENT(alignment)
|
|
490
|
+
leader = WD_TAB_LEADER(leader)
|
|
491
|
+
except ValueError as error:
|
|
492
|
+
raise ValueError("unsupported tab alignment or leader") from error
|
|
493
|
+
spec = ParagraphTab(float(position), int(alignment), _LEADER_TO_STRING[leader])
|
|
494
|
+
values = tuple(sorted(self._specs() + (spec,), key=lambda item: item.position))
|
|
495
|
+
self._paragraph._set_paragraph_property("tabs", values)
|
|
496
|
+
return TabStop(spec)
|
|
497
|
+
|
|
498
|
+
def clear_all(self) -> None:
|
|
499
|
+
self._paragraph._set_paragraph_property("tabs", None)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class ParagraphFormat:
|
|
503
|
+
def __init__(self, paragraph: "Paragraph"):
|
|
504
|
+
self._paragraph = paragraph
|
|
505
|
+
|
|
506
|
+
@property
|
|
507
|
+
def alignment(self) -> WD_PARAGRAPH_ALIGNMENT | None:
|
|
508
|
+
value = self._paragraph._paragraph_properties.alignment
|
|
509
|
+
return _PAGES_TO_API_ALIGNMENT.get(value)
|
|
510
|
+
|
|
511
|
+
@alignment.setter
|
|
512
|
+
def alignment(self, value: WD_PARAGRAPH_ALIGNMENT | None) -> None:
|
|
513
|
+
if value is None:
|
|
514
|
+
encoded = None
|
|
515
|
+
else:
|
|
516
|
+
try:
|
|
517
|
+
encoded = _API_TO_PAGES_ALIGNMENT[WD_PARAGRAPH_ALIGNMENT(value)]
|
|
518
|
+
except (KeyError, ValueError) as error:
|
|
519
|
+
raise ValueError(f"unsupported paragraph alignment: {value!r}") from error
|
|
520
|
+
self._paragraph._set_paragraph_property("alignment", encoded)
|
|
521
|
+
|
|
522
|
+
@staticmethod
|
|
523
|
+
def _length(value: float | None) -> Pt | None:
|
|
524
|
+
return None if value is None else Pt(value)
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def first_line_indent(self) -> Pt | None:
|
|
528
|
+
return self._length(self._paragraph._paragraph_properties.first_line_indent)
|
|
529
|
+
|
|
530
|
+
@first_line_indent.setter
|
|
531
|
+
def first_line_indent(self, value: Length | float | None) -> None:
|
|
532
|
+
self._paragraph._set_paragraph_property(
|
|
533
|
+
"first_line_indent", None if value is None else float(value)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def left_indent(self) -> Pt | None:
|
|
538
|
+
return self._length(self._paragraph._paragraph_properties.left_indent)
|
|
539
|
+
|
|
540
|
+
@left_indent.setter
|
|
541
|
+
def left_indent(self, value: Length | float | None) -> None:
|
|
542
|
+
self._paragraph._set_paragraph_property(
|
|
543
|
+
"left_indent", None if value is None else float(value)
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
@property
|
|
547
|
+
def right_indent(self) -> Pt | None:
|
|
548
|
+
return self._length(self._paragraph._paragraph_properties.right_indent)
|
|
549
|
+
|
|
550
|
+
@right_indent.setter
|
|
551
|
+
def right_indent(self, value: Length | float | None) -> None:
|
|
552
|
+
self._paragraph._set_paragraph_property(
|
|
553
|
+
"right_indent", None if value is None else float(value)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
@property
|
|
557
|
+
def space_before(self) -> Pt | None:
|
|
558
|
+
return self._length(self._paragraph._paragraph_properties.space_before)
|
|
559
|
+
|
|
560
|
+
@space_before.setter
|
|
561
|
+
def space_before(self, value: Length | float | None) -> None:
|
|
562
|
+
self._paragraph._set_paragraph_property(
|
|
563
|
+
"space_before", None if value is None else float(value)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def space_after(self) -> Pt | None:
|
|
568
|
+
return self._length(self._paragraph._paragraph_properties.space_after)
|
|
569
|
+
|
|
570
|
+
@space_after.setter
|
|
571
|
+
def space_after(self, value: Length | float | None) -> None:
|
|
572
|
+
self._paragraph._set_paragraph_property(
|
|
573
|
+
"space_after", None if value is None else float(value)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
@property
|
|
577
|
+
def line_spacing(self) -> float | Pt | None:
|
|
578
|
+
spacing = self._paragraph._paragraph_properties.line_spacing
|
|
579
|
+
if spacing is None:
|
|
580
|
+
return None
|
|
581
|
+
return spacing.amount if spacing.mode == 0 else Pt(spacing.amount)
|
|
582
|
+
|
|
583
|
+
@line_spacing.setter
|
|
584
|
+
def line_spacing(self, value: Length | float | None) -> None:
|
|
585
|
+
if value is None:
|
|
586
|
+
spacing = None
|
|
587
|
+
elif isinstance(value, Length):
|
|
588
|
+
spacing = LineSpacing(2, float(value))
|
|
589
|
+
else:
|
|
590
|
+
spacing = LineSpacing(0, float(value))
|
|
591
|
+
self._paragraph._set_paragraph_property("line_spacing", spacing)
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def line_spacing_rule(self) -> WD_LINE_SPACING | None:
|
|
595
|
+
spacing = self._paragraph._paragraph_properties.line_spacing
|
|
596
|
+
if spacing is None:
|
|
597
|
+
return None
|
|
598
|
+
if spacing.mode == 0:
|
|
599
|
+
if spacing.amount == 1.0:
|
|
600
|
+
return WD_LINE_SPACING.SINGLE
|
|
601
|
+
if spacing.amount == 1.5:
|
|
602
|
+
return WD_LINE_SPACING.ONE_POINT_FIVE
|
|
603
|
+
if spacing.amount == 2.0:
|
|
604
|
+
return WD_LINE_SPACING.DOUBLE
|
|
605
|
+
return WD_LINE_SPACING.MULTIPLE
|
|
606
|
+
if spacing.mode == 1:
|
|
607
|
+
return WD_LINE_SPACING.AT_LEAST
|
|
608
|
+
if spacing.mode == 2:
|
|
609
|
+
return WD_LINE_SPACING.EXACTLY
|
|
610
|
+
return WD_LINE_SPACING.MULTIPLE
|
|
611
|
+
|
|
612
|
+
@line_spacing_rule.setter
|
|
613
|
+
def line_spacing_rule(self, value: WD_LINE_SPACING | None) -> None:
|
|
614
|
+
if value is None:
|
|
615
|
+
self._paragraph._set_paragraph_property("line_spacing", None)
|
|
616
|
+
return
|
|
617
|
+
value = WD_LINE_SPACING(value)
|
|
618
|
+
current = self._paragraph._paragraph_properties.line_spacing
|
|
619
|
+
if value == WD_LINE_SPACING.SINGLE:
|
|
620
|
+
spacing = LineSpacing(0, 1.0)
|
|
621
|
+
elif value == WD_LINE_SPACING.ONE_POINT_FIVE:
|
|
622
|
+
spacing = LineSpacing(0, 1.5)
|
|
623
|
+
elif value == WD_LINE_SPACING.DOUBLE:
|
|
624
|
+
spacing = LineSpacing(0, 2.0)
|
|
625
|
+
elif value == WD_LINE_SPACING.AT_LEAST:
|
|
626
|
+
spacing = LineSpacing(1, current.amount if current else 12.0)
|
|
627
|
+
elif value == WD_LINE_SPACING.EXACTLY:
|
|
628
|
+
spacing = LineSpacing(2, current.amount if current else 12.0)
|
|
629
|
+
else:
|
|
630
|
+
spacing = LineSpacing(0, current.amount if current else 1.0)
|
|
631
|
+
self._paragraph._set_paragraph_property("line_spacing", spacing)
|
|
632
|
+
|
|
633
|
+
@property
|
|
634
|
+
def tab_stops(self) -> TabStops:
|
|
635
|
+
return TabStops(self._paragraph)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
class Paragraph:
|
|
639
|
+
def __init__(
|
|
640
|
+
self,
|
|
641
|
+
document: "Document",
|
|
642
|
+
start_utf16: int,
|
|
643
|
+
end_utf16: int,
|
|
644
|
+
next_start_utf16: int,
|
|
645
|
+
):
|
|
646
|
+
self._document = document
|
|
647
|
+
self._start_utf16 = start_utf16
|
|
648
|
+
self._end_utf16 = end_utf16
|
|
649
|
+
self._next_start_utf16 = next_start_utf16
|
|
650
|
+
|
|
651
|
+
@property
|
|
652
|
+
def style_id(self) -> int | None:
|
|
653
|
+
return self._document._paragraph_style_id_at(self._start_utf16)
|
|
654
|
+
|
|
655
|
+
@property
|
|
656
|
+
def style(self) -> "Style | None":
|
|
657
|
+
return self._document._style_from_id(
|
|
658
|
+
self.style_id, WD_STYLE_TYPE.PARAGRAPH
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
@style.setter
|
|
662
|
+
def style(self, value: "Style | str | None") -> None:
|
|
663
|
+
default = self._document._package.stylesheet().default_paragraph_style()
|
|
664
|
+
identifier = self._document._coerce_style_id(
|
|
665
|
+
value,
|
|
666
|
+
WD_STYLE_TYPE.PARAGRAPH,
|
|
667
|
+
none_identifier=default.segment.identifier,
|
|
668
|
+
)
|
|
669
|
+
self._document._apply_paragraph_style_id(
|
|
670
|
+
self._start_utf16, self._next_start_utf16, identifier
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
@property
|
|
674
|
+
def _paragraph_properties(self) -> ParagraphProperties:
|
|
675
|
+
return self._document._paragraph_properties_at(self._start_utf16)
|
|
676
|
+
|
|
677
|
+
@property
|
|
678
|
+
def background_color(self) -> RGBColor | None:
|
|
679
|
+
"""Direct Pages character background carried by this paragraph style."""
|
|
680
|
+
|
|
681
|
+
return self._document._paragraph_character_properties_at(
|
|
682
|
+
self._start_utf16
|
|
683
|
+
).background_color
|
|
684
|
+
|
|
685
|
+
@background_color.setter
|
|
686
|
+
def background_color(
|
|
687
|
+
self, value: RGBColor | str | tuple[int, int, int] | None
|
|
688
|
+
) -> None:
|
|
689
|
+
if isinstance(value, str):
|
|
690
|
+
value = RGBColor.from_string(value)
|
|
691
|
+
elif isinstance(value, tuple):
|
|
692
|
+
value = RGBColor(*value)
|
|
693
|
+
if value is not None and not isinstance(value, RGBColor):
|
|
694
|
+
raise TypeError(
|
|
695
|
+
"background_color must be RGBColor, a 3-tuple, a hex string, or None"
|
|
696
|
+
)
|
|
697
|
+
self._document._apply_paragraph_background(
|
|
698
|
+
self._start_utf16,
|
|
699
|
+
self._next_start_utf16,
|
|
700
|
+
value,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
@property
|
|
704
|
+
def highlight_color(self) -> WD_COLOR_INDEX | None:
|
|
705
|
+
color = self.background_color
|
|
706
|
+
return None if color is None else _RGB_TO_HIGHLIGHT.get(color)
|
|
707
|
+
|
|
708
|
+
@highlight_color.setter
|
|
709
|
+
def highlight_color(self, value: WD_COLOR_INDEX | int | None) -> None:
|
|
710
|
+
if value is None:
|
|
711
|
+
color = None
|
|
712
|
+
else:
|
|
713
|
+
try:
|
|
714
|
+
palette = WD_COLOR_INDEX(value)
|
|
715
|
+
except ValueError as error:
|
|
716
|
+
raise ValueError(f"unsupported highlight color: {value!r}") from error
|
|
717
|
+
color = (
|
|
718
|
+
None
|
|
719
|
+
if palette is WD_COLOR_INDEX.INHERITED
|
|
720
|
+
else _HIGHLIGHT_TO_RGB[palette]
|
|
721
|
+
)
|
|
722
|
+
self.background_color = color
|
|
723
|
+
|
|
724
|
+
def _set_paragraph_property(self, name: str, value) -> None:
|
|
725
|
+
props = self._paragraph_properties.with_value(name, value)
|
|
726
|
+
self._document._apply_paragraph_properties(
|
|
727
|
+
self._start_utf16,
|
|
728
|
+
self._next_start_utf16,
|
|
729
|
+
props,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
@property
|
|
733
|
+
def text(self) -> str:
|
|
734
|
+
return self._document._text_between(self._start_utf16, self._end_utf16)
|
|
735
|
+
|
|
736
|
+
@property
|
|
737
|
+
def runs(self) -> list[Run]:
|
|
738
|
+
if self._start_utf16 == self._end_utf16:
|
|
739
|
+
return []
|
|
740
|
+
boundaries = {self._start_utf16, self._end_utf16}
|
|
741
|
+
boundaries.update(
|
|
742
|
+
run.character_index
|
|
743
|
+
for run in self._document._character_runs()
|
|
744
|
+
if self._start_utf16 < run.character_index < self._end_utf16
|
|
745
|
+
)
|
|
746
|
+
ordered = sorted(boundaries)
|
|
747
|
+
return [
|
|
748
|
+
Run(self._document, start, end, self)
|
|
749
|
+
for start, end in zip(ordered, ordered[1:])
|
|
750
|
+
]
|
|
751
|
+
|
|
752
|
+
@property
|
|
753
|
+
def hyperlinks(self) -> list["Hyperlink"]:
|
|
754
|
+
return [
|
|
755
|
+
hyperlink
|
|
756
|
+
for hyperlink in self._document.hyperlinks
|
|
757
|
+
if self._start_utf16 <= hyperlink._start_utf16
|
|
758
|
+
and hyperlink._end_utf16 <= self._end_utf16
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
def add_run(self, text: str | None = None, style=None) -> Run:
|
|
762
|
+
value = "" if text is None else text
|
|
763
|
+
if not isinstance(value, str):
|
|
764
|
+
raise TypeError("run text must be a string or None")
|
|
765
|
+
if "\n" in value or "\r" in value:
|
|
766
|
+
raise ValueError("add_run text cannot contain paragraph separators")
|
|
767
|
+
start = self._end_utf16
|
|
768
|
+
delta = self._document._replace_text(start, start, value)
|
|
769
|
+
self._end_utf16 += delta
|
|
770
|
+
self._next_start_utf16 += delta
|
|
771
|
+
run = Run(self._document, start, start + delta, self)
|
|
772
|
+
if style is not None:
|
|
773
|
+
run.style = style
|
|
774
|
+
elif delta:
|
|
775
|
+
# Inserting at an existing run boundary shifts that boundary with
|
|
776
|
+
# the new text. A python-docx add_run() without an explicit style
|
|
777
|
+
# starts with inherited formatting, so restore the no-direct-style
|
|
778
|
+
# boundary across the inserted range.
|
|
779
|
+
self._document._apply_character_style_id(start, start + delta, None)
|
|
780
|
+
return run
|
|
781
|
+
|
|
782
|
+
def clear(self) -> "Paragraph":
|
|
783
|
+
"""Remove paragraph content while retaining its paragraph mark and style."""
|
|
784
|
+
delta = self._document._replace_text(
|
|
785
|
+
self._start_utf16, self._end_utf16, ""
|
|
786
|
+
)
|
|
787
|
+
self._end_utf16 += delta
|
|
788
|
+
self._next_start_utf16 += delta
|
|
789
|
+
return self
|
|
790
|
+
|
|
791
|
+
def delete(self) -> None:
|
|
792
|
+
"""Remove this paragraph and its paragraph separator when possible."""
|
|
793
|
+
paragraphs = self._document.paragraphs
|
|
794
|
+
if len(paragraphs) == 1:
|
|
795
|
+
self.clear()
|
|
796
|
+
return
|
|
797
|
+
position = next(
|
|
798
|
+
(
|
|
799
|
+
index
|
|
800
|
+
for index, paragraph in enumerate(paragraphs)
|
|
801
|
+
if paragraph._start_utf16 == self._start_utf16
|
|
802
|
+
),
|
|
803
|
+
None,
|
|
804
|
+
)
|
|
805
|
+
if position is None:
|
|
806
|
+
raise ValueError("paragraph no longer exists in the document")
|
|
807
|
+
if position < len(paragraphs) - 1:
|
|
808
|
+
start, end = self._start_utf16, self._next_start_utf16
|
|
809
|
+
else:
|
|
810
|
+
start, end = self._start_utf16 - 1, self._end_utf16
|
|
811
|
+
self._document._replace_text(start, end, "")
|
|
812
|
+
self._end_utf16 = self._start_utf16
|
|
813
|
+
self._next_start_utf16 = self._start_utf16
|
|
814
|
+
|
|
815
|
+
@property
|
|
816
|
+
def paragraph_format(self) -> ParagraphFormat:
|
|
817
|
+
return ParagraphFormat(self)
|
|
818
|
+
|
|
819
|
+
@property
|
|
820
|
+
def alignment(self) -> WD_PARAGRAPH_ALIGNMENT | None:
|
|
821
|
+
return self.paragraph_format.alignment
|
|
822
|
+
|
|
823
|
+
@alignment.setter
|
|
824
|
+
def alignment(self, value: WD_PARAGRAPH_ALIGNMENT | None) -> None:
|
|
825
|
+
self.paragraph_format.alignment = value
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
class HeaderFooterParagraph:
|
|
829
|
+
"""A Pages header/footer fragment exposed with paragraph text semantics."""
|
|
830
|
+
|
|
831
|
+
def __init__(self, layout: DocumentLayout, storage):
|
|
832
|
+
self._layout = layout
|
|
833
|
+
self._storage = storage
|
|
834
|
+
|
|
835
|
+
@property
|
|
836
|
+
def text(self) -> str:
|
|
837
|
+
return self._layout.storage_text(self._storage)
|
|
838
|
+
|
|
839
|
+
@text.setter
|
|
840
|
+
def text(self, value: str) -> None:
|
|
841
|
+
self._layout.set_storage_text(self._storage, value)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
class HeaderFooter:
|
|
845
|
+
"""Header/footer proxy over Pages' left, center, and right fragments."""
|
|
846
|
+
|
|
847
|
+
def __init__(self, layout: DocumentLayout, kind: str, *, footer: bool):
|
|
848
|
+
self._layout = layout
|
|
849
|
+
self._kind = kind
|
|
850
|
+
self._footer = footer
|
|
851
|
+
|
|
852
|
+
@property
|
|
853
|
+
def paragraphs(self) -> list[HeaderFooterParagraph]:
|
|
854
|
+
return [
|
|
855
|
+
HeaderFooterParagraph(self._layout, storage)
|
|
856
|
+
for storage in self._layout.header_footer_storages(
|
|
857
|
+
self._kind, footer=self._footer
|
|
858
|
+
)
|
|
859
|
+
]
|
|
860
|
+
|
|
861
|
+
@property
|
|
862
|
+
def text(self) -> str:
|
|
863
|
+
return "\t".join(paragraph.text for paragraph in self.paragraphs)
|
|
864
|
+
|
|
865
|
+
@text.setter
|
|
866
|
+
def text(self, value: str) -> None:
|
|
867
|
+
self.paragraphs[0].text = value
|
|
868
|
+
|
|
869
|
+
@property
|
|
870
|
+
def is_linked_to_previous(self) -> bool:
|
|
871
|
+
return False
|
|
872
|
+
|
|
873
|
+
@is_linked_to_previous.setter
|
|
874
|
+
def is_linked_to_previous(self, value: bool) -> None:
|
|
875
|
+
if value:
|
|
876
|
+
raise NotImplementedError("Pages single-section mode has no previous section")
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
class Section:
|
|
880
|
+
"""python-docx-shaped facade over the package-wide Pages page settings."""
|
|
881
|
+
|
|
882
|
+
_LENGTH_FIELDS = {
|
|
883
|
+
"page_width": DOCUMENT_PAGE_WIDTH,
|
|
884
|
+
"page_height": DOCUMENT_PAGE_HEIGHT,
|
|
885
|
+
"left_margin": DOCUMENT_LEFT_MARGIN,
|
|
886
|
+
"right_margin": DOCUMENT_RIGHT_MARGIN,
|
|
887
|
+
"top_margin": DOCUMENT_TOP_MARGIN,
|
|
888
|
+
"bottom_margin": DOCUMENT_BOTTOM_MARGIN,
|
|
889
|
+
"header_distance": DOCUMENT_HEADER_MARGIN,
|
|
890
|
+
"footer_distance": DOCUMENT_FOOTER_MARGIN,
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
def __init__(self, layout: DocumentLayout):
|
|
894
|
+
self._layout = layout
|
|
895
|
+
|
|
896
|
+
def _length(self, name: str) -> Pt | None:
|
|
897
|
+
value = self._layout.fixed32(self._LENGTH_FIELDS[name])
|
|
898
|
+
return None if value is None else Pt(value)
|
|
899
|
+
|
|
900
|
+
def _set_length(self, name: str, value: Length | float | None) -> None:
|
|
901
|
+
self._layout.set_fixed32(
|
|
902
|
+
self._LENGTH_FIELDS[name], None if value is None else float(value)
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
@property
|
|
906
|
+
def page_width(self) -> Pt | None:
|
|
907
|
+
return self._length("page_width")
|
|
908
|
+
|
|
909
|
+
@page_width.setter
|
|
910
|
+
def page_width(self, value: Length | float | None) -> None:
|
|
911
|
+
self._set_length("page_width", value)
|
|
912
|
+
|
|
913
|
+
@property
|
|
914
|
+
def page_height(self) -> Pt | None:
|
|
915
|
+
return self._length("page_height")
|
|
916
|
+
|
|
917
|
+
@page_height.setter
|
|
918
|
+
def page_height(self, value: Length | float | None) -> None:
|
|
919
|
+
self._set_length("page_height", value)
|
|
920
|
+
|
|
921
|
+
@property
|
|
922
|
+
def left_margin(self) -> Pt | None:
|
|
923
|
+
return self._length("left_margin")
|
|
924
|
+
|
|
925
|
+
@left_margin.setter
|
|
926
|
+
def left_margin(self, value: Length | float | None) -> None:
|
|
927
|
+
self._set_length("left_margin", value)
|
|
928
|
+
|
|
929
|
+
@property
|
|
930
|
+
def right_margin(self) -> Pt | None:
|
|
931
|
+
return self._length("right_margin")
|
|
932
|
+
|
|
933
|
+
@right_margin.setter
|
|
934
|
+
def right_margin(self, value: Length | float | None) -> None:
|
|
935
|
+
self._set_length("right_margin", value)
|
|
936
|
+
|
|
937
|
+
@property
|
|
938
|
+
def top_margin(self) -> Pt | None:
|
|
939
|
+
return self._length("top_margin")
|
|
940
|
+
|
|
941
|
+
@top_margin.setter
|
|
942
|
+
def top_margin(self, value: Length | float | None) -> None:
|
|
943
|
+
self._set_length("top_margin", value)
|
|
944
|
+
|
|
945
|
+
@property
|
|
946
|
+
def bottom_margin(self) -> Pt | None:
|
|
947
|
+
return self._length("bottom_margin")
|
|
948
|
+
|
|
949
|
+
@bottom_margin.setter
|
|
950
|
+
def bottom_margin(self, value: Length | float | None) -> None:
|
|
951
|
+
self._set_length("bottom_margin", value)
|
|
952
|
+
|
|
953
|
+
@property
|
|
954
|
+
def header_distance(self) -> Pt | None:
|
|
955
|
+
return self._length("header_distance")
|
|
956
|
+
|
|
957
|
+
@header_distance.setter
|
|
958
|
+
def header_distance(self, value: Length | float | None) -> None:
|
|
959
|
+
self._set_length("header_distance", value)
|
|
960
|
+
|
|
961
|
+
@property
|
|
962
|
+
def footer_distance(self) -> Pt | None:
|
|
963
|
+
return self._length("footer_distance")
|
|
964
|
+
|
|
965
|
+
@footer_distance.setter
|
|
966
|
+
def footer_distance(self, value: Length | float | None) -> None:
|
|
967
|
+
self._set_length("footer_distance", value)
|
|
968
|
+
|
|
969
|
+
@property
|
|
970
|
+
def orientation(self) -> WD_ORIENTATION:
|
|
971
|
+
return WD_ORIENTATION(self._layout.varint(DOCUMENT_ORIENTATION))
|
|
972
|
+
|
|
973
|
+
@orientation.setter
|
|
974
|
+
def orientation(self, value: WD_ORIENTATION | None) -> None:
|
|
975
|
+
self._layout.set_varint(
|
|
976
|
+
DOCUMENT_ORIENTATION,
|
|
977
|
+
None if value is None else int(WD_ORIENTATION(value)),
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
@property
|
|
981
|
+
def different_first_page_header_footer(self) -> bool:
|
|
982
|
+
return self._layout.section_flag(SECTION_FIRST_PAGE_DIFFERENT)
|
|
983
|
+
|
|
984
|
+
@different_first_page_header_footer.setter
|
|
985
|
+
def different_first_page_header_footer(self, value: bool) -> None:
|
|
986
|
+
if not isinstance(value, bool):
|
|
987
|
+
raise TypeError("different_first_page_header_footer must be bool")
|
|
988
|
+
self._layout.set_section_flag(SECTION_FIRST_PAGE_DIFFERENT, value)
|
|
989
|
+
|
|
990
|
+
@property
|
|
991
|
+
def header(self) -> HeaderFooter:
|
|
992
|
+
return HeaderFooter(self._layout, "primary", footer=False)
|
|
993
|
+
|
|
994
|
+
@property
|
|
995
|
+
def footer(self) -> HeaderFooter:
|
|
996
|
+
return HeaderFooter(self._layout, "primary", footer=True)
|
|
997
|
+
|
|
998
|
+
@property
|
|
999
|
+
def first_page_header(self) -> HeaderFooter:
|
|
1000
|
+
return HeaderFooter(self._layout, "first", footer=False)
|
|
1001
|
+
|
|
1002
|
+
@property
|
|
1003
|
+
def first_page_footer(self) -> HeaderFooter:
|
|
1004
|
+
return HeaderFooter(self._layout, "first", footer=True)
|
|
1005
|
+
|
|
1006
|
+
@property
|
|
1007
|
+
def even_page_header(self) -> HeaderFooter:
|
|
1008
|
+
return HeaderFooter(self._layout, "even", footer=False)
|
|
1009
|
+
|
|
1010
|
+
@property
|
|
1011
|
+
def even_page_footer(self) -> HeaderFooter:
|
|
1012
|
+
return HeaderFooter(self._layout, "even", footer=True)
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
class TableCellFont:
|
|
1016
|
+
"""Read-only font facade for a table cell's effective text style."""
|
|
1017
|
+
|
|
1018
|
+
def __init__(self, run: "TableCellRun"):
|
|
1019
|
+
self._run = run
|
|
1020
|
+
|
|
1021
|
+
@property
|
|
1022
|
+
def bold(self) -> bool | None:
|
|
1023
|
+
return self._run._properties.bold
|
|
1024
|
+
|
|
1025
|
+
@property
|
|
1026
|
+
def italic(self) -> bool | None:
|
|
1027
|
+
return self._run._properties.italic
|
|
1028
|
+
|
|
1029
|
+
@property
|
|
1030
|
+
def underline(self) -> bool | None:
|
|
1031
|
+
return self._run._properties.underline
|
|
1032
|
+
|
|
1033
|
+
@property
|
|
1034
|
+
def strike(self) -> bool | None:
|
|
1035
|
+
return self._run._properties.strike
|
|
1036
|
+
|
|
1037
|
+
@property
|
|
1038
|
+
def name(self) -> str | None:
|
|
1039
|
+
return self._run._properties.font_name
|
|
1040
|
+
|
|
1041
|
+
@property
|
|
1042
|
+
def size(self) -> Pt | None:
|
|
1043
|
+
value = self._run._properties.font_size
|
|
1044
|
+
return None if value is None else Pt(value)
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
class TableCellRun:
|
|
1048
|
+
def __init__(self, cell: "TableCell"):
|
|
1049
|
+
self._cell = cell
|
|
1050
|
+
|
|
1051
|
+
@property
|
|
1052
|
+
def text(self) -> str:
|
|
1053
|
+
return self._cell.text
|
|
1054
|
+
|
|
1055
|
+
@text.setter
|
|
1056
|
+
def text(self, value: str) -> None:
|
|
1057
|
+
self._cell.text = value
|
|
1058
|
+
|
|
1059
|
+
@property
|
|
1060
|
+
def _properties(self) -> CharacterProperties:
|
|
1061
|
+
return self._cell._properties
|
|
1062
|
+
|
|
1063
|
+
@property
|
|
1064
|
+
def font(self) -> TableCellFont:
|
|
1065
|
+
return TableCellFont(self)
|
|
1066
|
+
|
|
1067
|
+
@property
|
|
1068
|
+
def bold(self) -> bool | None:
|
|
1069
|
+
return self.font.bold
|
|
1070
|
+
|
|
1071
|
+
@property
|
|
1072
|
+
def italic(self) -> bool | None:
|
|
1073
|
+
return self.font.italic
|
|
1074
|
+
|
|
1075
|
+
@property
|
|
1076
|
+
def underline(self) -> bool | None:
|
|
1077
|
+
return self.font.underline
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
class TableCellParagraph:
|
|
1081
|
+
def __init__(self, cell: "TableCell"):
|
|
1082
|
+
self._cell = cell
|
|
1083
|
+
|
|
1084
|
+
@property
|
|
1085
|
+
def text(self) -> str:
|
|
1086
|
+
return self._cell.text
|
|
1087
|
+
|
|
1088
|
+
@property
|
|
1089
|
+
def runs(self) -> list[TableCellRun]:
|
|
1090
|
+
return [] if not self.text else [TableCellRun(self._cell)]
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
class TableCell:
|
|
1094
|
+
"""python-docx-shaped cell facade over one TST tile coordinate."""
|
|
1095
|
+
|
|
1096
|
+
def __init__(self, table: "Table", row: int, column: int):
|
|
1097
|
+
self._table = table
|
|
1098
|
+
self._row = row
|
|
1099
|
+
self._column = column
|
|
1100
|
+
|
|
1101
|
+
@property
|
|
1102
|
+
def _storage(self) -> CellStorage | None:
|
|
1103
|
+
return self._table._store.cell_storage(self._row, self._column)
|
|
1104
|
+
|
|
1105
|
+
@property
|
|
1106
|
+
def _properties(self) -> CharacterProperties:
|
|
1107
|
+
return self._table._store.character_properties(
|
|
1108
|
+
self._row, self._column, self._storage
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
@property
|
|
1112
|
+
def text(self) -> str:
|
|
1113
|
+
storage = self._storage
|
|
1114
|
+
if storage is None:
|
|
1115
|
+
return ""
|
|
1116
|
+
if storage.cell_type != 3 or storage.string_id is None:
|
|
1117
|
+
raise NotImplementedError("this TST cell value type is not textual")
|
|
1118
|
+
return self._table._store.string(storage.string_id)
|
|
1119
|
+
|
|
1120
|
+
@text.setter
|
|
1121
|
+
def text(self, value: str) -> None:
|
|
1122
|
+
if not isinstance(value, str):
|
|
1123
|
+
raise TypeError("cell text must be a string")
|
|
1124
|
+
storage = self._storage
|
|
1125
|
+
if storage is None:
|
|
1126
|
+
raise NotImplementedError(
|
|
1127
|
+
"creating storage for an omitted empty cell is deferred"
|
|
1128
|
+
)
|
|
1129
|
+
if storage.cell_type != 3 or storage.string_id is None:
|
|
1130
|
+
raise NotImplementedError("retyping non-text TST cells is deferred")
|
|
1131
|
+
self._table._store.set_string(storage.string_id, value)
|
|
1132
|
+
|
|
1133
|
+
@property
|
|
1134
|
+
def paragraphs(self) -> list[TableCellParagraph]:
|
|
1135
|
+
return [TableCellParagraph(self)]
|
|
1136
|
+
|
|
1137
|
+
@property
|
|
1138
|
+
def runs(self) -> list[TableCellRun]:
|
|
1139
|
+
return self.paragraphs[0].runs
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
class TableRow:
|
|
1143
|
+
def __init__(self, table: "Table", index: int):
|
|
1144
|
+
self._table = table
|
|
1145
|
+
self._index = index
|
|
1146
|
+
|
|
1147
|
+
@property
|
|
1148
|
+
def cells(self) -> list[TableCell]:
|
|
1149
|
+
return [
|
|
1150
|
+
self._table.cell(self._index, column)
|
|
1151
|
+
for column in range(len(self._table.columns))
|
|
1152
|
+
]
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
class TableColumn:
|
|
1156
|
+
def __init__(self, table: "Table", index: int):
|
|
1157
|
+
self._table = table
|
|
1158
|
+
self._index = index
|
|
1159
|
+
|
|
1160
|
+
@property
|
|
1161
|
+
def cells(self) -> list[TableCell]:
|
|
1162
|
+
return [
|
|
1163
|
+
self._table.cell(row, self._index)
|
|
1164
|
+
for row in range(len(self._table.rows))
|
|
1165
|
+
]
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
class Table:
|
|
1169
|
+
def __init__(self, store: TableStore):
|
|
1170
|
+
self._store = store
|
|
1171
|
+
|
|
1172
|
+
@property
|
|
1173
|
+
def name(self) -> str:
|
|
1174
|
+
return self._store.name
|
|
1175
|
+
|
|
1176
|
+
@property
|
|
1177
|
+
def rows(self) -> list[TableRow]:
|
|
1178
|
+
return [TableRow(self, row) for row in range(self._store.rows)]
|
|
1179
|
+
|
|
1180
|
+
@property
|
|
1181
|
+
def columns(self) -> list[TableColumn]:
|
|
1182
|
+
return [TableColumn(self, column) for column in range(self._store.columns)]
|
|
1183
|
+
|
|
1184
|
+
def cell(self, row: int, column: int) -> TableCell:
|
|
1185
|
+
if (
|
|
1186
|
+
not 0 <= row < self._store.rows
|
|
1187
|
+
or not 0 <= column < self._store.columns
|
|
1188
|
+
):
|
|
1189
|
+
raise IndexError("table cell index is out of range")
|
|
1190
|
+
return TableCell(self, row, column)
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
class InlineShape:
|
|
1194
|
+
"""Inline Pages image with package-backed media access."""
|
|
1195
|
+
|
|
1196
|
+
def __init__(self, store: ImageStore):
|
|
1197
|
+
self._store = store
|
|
1198
|
+
|
|
1199
|
+
@property
|
|
1200
|
+
def filename(self) -> str:
|
|
1201
|
+
return self._store.filename
|
|
1202
|
+
|
|
1203
|
+
@property
|
|
1204
|
+
def blob(self) -> bytes:
|
|
1205
|
+
return self._store.blob
|
|
1206
|
+
|
|
1207
|
+
@property
|
|
1208
|
+
def thumbnail_blob(self) -> bytes | None:
|
|
1209
|
+
return self._store.thumbnail_blob
|
|
1210
|
+
|
|
1211
|
+
@property
|
|
1212
|
+
def width(self) -> Pt | None:
|
|
1213
|
+
size = self._store.natural_size
|
|
1214
|
+
return None if size is None else Pt(size[0])
|
|
1215
|
+
|
|
1216
|
+
@property
|
|
1217
|
+
def height(self) -> Pt | None:
|
|
1218
|
+
size = self._store.natural_size
|
|
1219
|
+
return None if size is None else Pt(size[1])
|
|
1220
|
+
|
|
1221
|
+
def replace(
|
|
1222
|
+
self,
|
|
1223
|
+
path: str | Path,
|
|
1224
|
+
thumbnail_path: str | Path | None = None,
|
|
1225
|
+
) -> "InlineShape":
|
|
1226
|
+
thumbnail = (
|
|
1227
|
+
None if thumbnail_path is None else Path(thumbnail_path).read_bytes()
|
|
1228
|
+
)
|
|
1229
|
+
self._store.replace(Path(path).read_bytes(), thumbnail)
|
|
1230
|
+
return self
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
class Hyperlink:
|
|
1234
|
+
"""Text-range facade for a TSWP.HyperlinkFieldArchive."""
|
|
1235
|
+
|
|
1236
|
+
def __init__(self, document: "Document", store: HyperlinkStore):
|
|
1237
|
+
self._document = document
|
|
1238
|
+
self._store = store
|
|
1239
|
+
self._start_utf16 = store.start_utf16
|
|
1240
|
+
self._end_utf16 = store.end_utf16
|
|
1241
|
+
|
|
1242
|
+
@property
|
|
1243
|
+
def text(self) -> str:
|
|
1244
|
+
return self._document._text_between(self._start_utf16, self._end_utf16)
|
|
1245
|
+
|
|
1246
|
+
@text.setter
|
|
1247
|
+
def text(self, value: str) -> None:
|
|
1248
|
+
if not isinstance(value, str):
|
|
1249
|
+
raise TypeError("hyperlink text must be a string")
|
|
1250
|
+
if "\n" in value or "\r" in value:
|
|
1251
|
+
raise ValueError("hyperlink text cannot contain paragraph separators")
|
|
1252
|
+
delta = self._document._replace_text(
|
|
1253
|
+
self._start_utf16, self._end_utf16, value
|
|
1254
|
+
)
|
|
1255
|
+
self._end_utf16 += delta
|
|
1256
|
+
|
|
1257
|
+
@property
|
|
1258
|
+
def url(self) -> str:
|
|
1259
|
+
return self._store.url
|
|
1260
|
+
|
|
1261
|
+
@url.setter
|
|
1262
|
+
def url(self, value: str) -> None:
|
|
1263
|
+
self._store.url = value
|
|
1264
|
+
|
|
1265
|
+
@property
|
|
1266
|
+
def address(self) -> str:
|
|
1267
|
+
return self.url
|
|
1268
|
+
|
|
1269
|
+
@address.setter
|
|
1270
|
+
def address(self, value: str) -> None:
|
|
1271
|
+
self.url = value
|
|
1272
|
+
|
|
1273
|
+
@property
|
|
1274
|
+
def runs(self) -> list[Run]:
|
|
1275
|
+
return [Run(self._document, self._start_utf16, self._end_utf16)]
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
class Style:
|
|
1279
|
+
"""Named Pages style exposed with the common python-docx attributes."""
|
|
1280
|
+
|
|
1281
|
+
def __init__(self, document: "Document", obj):
|
|
1282
|
+
self._document = document
|
|
1283
|
+
self._obj = obj
|
|
1284
|
+
|
|
1285
|
+
@property
|
|
1286
|
+
def name(self) -> str:
|
|
1287
|
+
return style_name(self._obj) or str(self._obj.segment.identifier)
|
|
1288
|
+
|
|
1289
|
+
@property
|
|
1290
|
+
def style_id(self) -> str:
|
|
1291
|
+
return style_identifier(self._obj) or str(self._obj.segment.identifier)
|
|
1292
|
+
|
|
1293
|
+
@property
|
|
1294
|
+
def type(self) -> WD_STYLE_TYPE:
|
|
1295
|
+
return (
|
|
1296
|
+
WD_STYLE_TYPE.CHARACTER
|
|
1297
|
+
if self._obj.info.type_id == TYPE_CHARACTER_STYLE
|
|
1298
|
+
else WD_STYLE_TYPE.PARAGRAPH
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
class Styles:
|
|
1303
|
+
def __init__(self, document: "Document"):
|
|
1304
|
+
self._document = document
|
|
1305
|
+
|
|
1306
|
+
def _items(self) -> list[Style]:
|
|
1307
|
+
return [
|
|
1308
|
+
Style(self._document, obj)
|
|
1309
|
+
for obj in self._document._package.stylesheet().named_styles()
|
|
1310
|
+
]
|
|
1311
|
+
|
|
1312
|
+
def __len__(self) -> int:
|
|
1313
|
+
return len(self._items())
|
|
1314
|
+
|
|
1315
|
+
def __iter__(self):
|
|
1316
|
+
return iter(self._items())
|
|
1317
|
+
|
|
1318
|
+
def __getitem__(self, name: str) -> Style:
|
|
1319
|
+
for style in self._items():
|
|
1320
|
+
if style.name == name:
|
|
1321
|
+
return style
|
|
1322
|
+
raise KeyError(f"style {name!r} was not found")
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
class Comment:
|
|
1326
|
+
"""Pages-backed comment or reply."""
|
|
1327
|
+
|
|
1328
|
+
def __init__(self, document: "Document", store: CommentStore):
|
|
1329
|
+
self._document = document
|
|
1330
|
+
self._store = store
|
|
1331
|
+
|
|
1332
|
+
@property
|
|
1333
|
+
def comment_id(self) -> int:
|
|
1334
|
+
return self._store.comment_id
|
|
1335
|
+
|
|
1336
|
+
@property
|
|
1337
|
+
def text(self) -> str:
|
|
1338
|
+
return self._store.text
|
|
1339
|
+
|
|
1340
|
+
@text.setter
|
|
1341
|
+
def text(self, value: str) -> None:
|
|
1342
|
+
self._store.text = value
|
|
1343
|
+
|
|
1344
|
+
@property
|
|
1345
|
+
def author(self) -> str:
|
|
1346
|
+
return self._store.author
|
|
1347
|
+
|
|
1348
|
+
@property
|
|
1349
|
+
def annotation_color(self) -> RGBColor | None:
|
|
1350
|
+
"""Review-highlight color assigned to this comment's author."""
|
|
1351
|
+
return self._store.annotation_color
|
|
1352
|
+
|
|
1353
|
+
@property
|
|
1354
|
+
def initials(self) -> None:
|
|
1355
|
+
"""Pages stores no separate initials value for an annotation author."""
|
|
1356
|
+
return None
|
|
1357
|
+
|
|
1358
|
+
@property
|
|
1359
|
+
def timestamp(self):
|
|
1360
|
+
return self._store.timestamp
|
|
1361
|
+
|
|
1362
|
+
@property
|
|
1363
|
+
def replies(self) -> list["Comment"]:
|
|
1364
|
+
return [Comment(self._document, reply) for reply in self._store.replies]
|
|
1365
|
+
|
|
1366
|
+
@property
|
|
1367
|
+
def anchor_start(self) -> int | None:
|
|
1368
|
+
return self._store.start_utf16
|
|
1369
|
+
|
|
1370
|
+
@property
|
|
1371
|
+
def anchor_end(self) -> int | None:
|
|
1372
|
+
return self._store.end_utf16
|
|
1373
|
+
|
|
1374
|
+
@property
|
|
1375
|
+
def anchor_text(self) -> str:
|
|
1376
|
+
if self.anchor_start is None or self.anchor_end is None:
|
|
1377
|
+
return ""
|
|
1378
|
+
return self._document._text_between(self.anchor_start, self.anchor_end)
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
class Comments:
|
|
1382
|
+
"""Collection of root comments anchored in the Pages body storage."""
|
|
1383
|
+
|
|
1384
|
+
def __init__(self, document: "Document"):
|
|
1385
|
+
self._document = document
|
|
1386
|
+
|
|
1387
|
+
def _items(self) -> list[Comment]:
|
|
1388
|
+
return [
|
|
1389
|
+
Comment(self._document, store)
|
|
1390
|
+
for store in storage_comments(
|
|
1391
|
+
self._document._package, self._document._storage
|
|
1392
|
+
)
|
|
1393
|
+
]
|
|
1394
|
+
|
|
1395
|
+
def __len__(self) -> int:
|
|
1396
|
+
return len(self._items())
|
|
1397
|
+
|
|
1398
|
+
def __iter__(self):
|
|
1399
|
+
return iter(self._items())
|
|
1400
|
+
|
|
1401
|
+
def __getitem__(self, index: int) -> Comment:
|
|
1402
|
+
return self._items()[index]
|
|
1403
|
+
|
|
1404
|
+
def get(self, comment_id: int) -> Comment | None:
|
|
1405
|
+
return next(
|
|
1406
|
+
(item for item in self._items() if item.comment_id == comment_id),
|
|
1407
|
+
None,
|
|
1408
|
+
)
|
|
1409
|
+
|
|
1410
|
+
def add_comment(self, *args, **kwargs):
|
|
1411
|
+
raise NotImplementedError(
|
|
1412
|
+
"creating a Pages comment requires author and collaboration-state "
|
|
1413
|
+
"graphs beyond the anchored comment objects"
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
class AnnotationHighlight:
|
|
1418
|
+
"""Review annotation range associated with one Pages comment."""
|
|
1419
|
+
|
|
1420
|
+
def __init__(self, comment: Comment):
|
|
1421
|
+
self._comment = comment
|
|
1422
|
+
|
|
1423
|
+
@property
|
|
1424
|
+
def start_utf16(self) -> int:
|
|
1425
|
+
assert self._comment.anchor_start is not None
|
|
1426
|
+
return self._comment.anchor_start
|
|
1427
|
+
|
|
1428
|
+
@property
|
|
1429
|
+
def end_utf16(self) -> int:
|
|
1430
|
+
assert self._comment.anchor_end is not None
|
|
1431
|
+
return self._comment.anchor_end
|
|
1432
|
+
|
|
1433
|
+
@property
|
|
1434
|
+
def text(self) -> str:
|
|
1435
|
+
return self._comment.anchor_text
|
|
1436
|
+
|
|
1437
|
+
@property
|
|
1438
|
+
def color(self) -> RGBColor | None:
|
|
1439
|
+
return self._comment.annotation_color
|
|
1440
|
+
|
|
1441
|
+
@property
|
|
1442
|
+
def comment(self) -> Comment:
|
|
1443
|
+
return self._comment
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
class Document:
|
|
1447
|
+
"""Open a Pages package and expose paragraphs and character-style runs."""
|
|
1448
|
+
|
|
1449
|
+
def __init__(self, path: str | Path | None = None):
|
|
1450
|
+
if path is None:
|
|
1451
|
+
path = Path(__file__).with_name("data") / "default.pages"
|
|
1452
|
+
self._package = PagesDocument.load(path)
|
|
1453
|
+
self._storage = self._package.body_storage()
|
|
1454
|
+
self._text = self._package.text(self._storage)
|
|
1455
|
+
self._core_properties = CoreProperties(self._package)
|
|
1456
|
+
self._tables: list[Table] | None = None
|
|
1457
|
+
self._inline_shapes: list[InlineShape] | None = None
|
|
1458
|
+
self._styles = Styles(self)
|
|
1459
|
+
self._comments = Comments(self)
|
|
1460
|
+
|
|
1461
|
+
@property
|
|
1462
|
+
def paragraphs(self) -> list[Paragraph]:
|
|
1463
|
+
result: list[Paragraph] = []
|
|
1464
|
+
start_cp = 0
|
|
1465
|
+
for end_cp in [
|
|
1466
|
+
index for index, char in enumerate(self._text) if char in ("\n", "\r")
|
|
1467
|
+
] + [len(self._text)]:
|
|
1468
|
+
start = codepoint_to_utf16(self._text, start_cp)
|
|
1469
|
+
end = codepoint_to_utf16(self._text, end_cp)
|
|
1470
|
+
next_start = end + (1 if end_cp < len(self._text) else 0)
|
|
1471
|
+
result.append(Paragraph(self, start, end, next_start))
|
|
1472
|
+
start_cp = end_cp + 1
|
|
1473
|
+
return result
|
|
1474
|
+
|
|
1475
|
+
def save(self, path: str | Path) -> None:
|
|
1476
|
+
self._package.save(path)
|
|
1477
|
+
|
|
1478
|
+
@property
|
|
1479
|
+
def sections(self) -> list[Section]:
|
|
1480
|
+
return [Section(self._package.document_layout(self._storage))]
|
|
1481
|
+
|
|
1482
|
+
@property
|
|
1483
|
+
def core_properties(self) -> CoreProperties:
|
|
1484
|
+
return self._core_properties
|
|
1485
|
+
|
|
1486
|
+
@property
|
|
1487
|
+
def styles(self) -> Styles:
|
|
1488
|
+
return self._styles
|
|
1489
|
+
|
|
1490
|
+
@property
|
|
1491
|
+
def comments(self) -> Comments:
|
|
1492
|
+
return self._comments
|
|
1493
|
+
|
|
1494
|
+
@property
|
|
1495
|
+
def annotation_highlights(self) -> list[AnnotationHighlight]:
|
|
1496
|
+
return [
|
|
1497
|
+
AnnotationHighlight(comment)
|
|
1498
|
+
for comment in self.comments
|
|
1499
|
+
if comment.anchor_start is not None and comment.anchor_end is not None
|
|
1500
|
+
]
|
|
1501
|
+
|
|
1502
|
+
def add_comment(self, *args, **kwargs):
|
|
1503
|
+
return self.comments.add_comment(*args, **kwargs)
|
|
1504
|
+
|
|
1505
|
+
@property
|
|
1506
|
+
def tables(self) -> list[Table]:
|
|
1507
|
+
if self._tables is None:
|
|
1508
|
+
self._tables = [
|
|
1509
|
+
Table(store)
|
|
1510
|
+
for store in body_table_stores(self._package, self._storage)
|
|
1511
|
+
]
|
|
1512
|
+
return list(self._tables)
|
|
1513
|
+
|
|
1514
|
+
def add_table(self, rows: int, cols: int, style=None) -> Table:
|
|
1515
|
+
if (
|
|
1516
|
+
not isinstance(rows, int)
|
|
1517
|
+
or isinstance(rows, bool)
|
|
1518
|
+
or not isinstance(cols, int)
|
|
1519
|
+
or isinstance(cols, bool)
|
|
1520
|
+
):
|
|
1521
|
+
raise TypeError("rows and cols must be integers")
|
|
1522
|
+
if rows <= 0 or cols <= 0:
|
|
1523
|
+
raise ValueError("rows and cols must be positive")
|
|
1524
|
+
if rows > 256:
|
|
1525
|
+
raise ValueError("template-backed Pages tables currently support at most 256 rows")
|
|
1526
|
+
if style is not None:
|
|
1527
|
+
raise NotImplementedError(
|
|
1528
|
+
"named table styles are not exposed by the Pages stylesheet facade"
|
|
1529
|
+
)
|
|
1530
|
+
store, clone = clone_body_table(self._package, self._storage, rows, cols)
|
|
1531
|
+
position = codepoint_to_utf16(self._text, len(self._text))
|
|
1532
|
+
self._replace_text(position, position, "\n\ufffc")
|
|
1533
|
+
append_attachment_run(self._storage, position + 1, clone.root.segment.identifier)
|
|
1534
|
+
table = Table(store)
|
|
1535
|
+
if self._tables is None:
|
|
1536
|
+
self._tables = [
|
|
1537
|
+
Table(item)
|
|
1538
|
+
for item in body_table_stores(self._package, self._storage)
|
|
1539
|
+
]
|
|
1540
|
+
else:
|
|
1541
|
+
self._tables.append(table)
|
|
1542
|
+
return table
|
|
1543
|
+
|
|
1544
|
+
@property
|
|
1545
|
+
def inline_shapes(self) -> list[InlineShape]:
|
|
1546
|
+
if self._inline_shapes is None:
|
|
1547
|
+
self._inline_shapes = [
|
|
1548
|
+
InlineShape(store)
|
|
1549
|
+
for store in body_image_stores(self._package, self._storage)
|
|
1550
|
+
]
|
|
1551
|
+
return list(self._inline_shapes)
|
|
1552
|
+
|
|
1553
|
+
@property
|
|
1554
|
+
def pictures(self) -> list[InlineShape]:
|
|
1555
|
+
"""Convenience alias for the Pages-backed inline-shapes collection."""
|
|
1556
|
+
return self.inline_shapes
|
|
1557
|
+
|
|
1558
|
+
@property
|
|
1559
|
+
def hyperlinks(self) -> list[Hyperlink]:
|
|
1560
|
+
return [
|
|
1561
|
+
Hyperlink(self, store)
|
|
1562
|
+
for store in storage_hyperlinks(self._package, self._storage)
|
|
1563
|
+
]
|
|
1564
|
+
|
|
1565
|
+
def add_picture(
|
|
1566
|
+
self,
|
|
1567
|
+
image_path_or_stream,
|
|
1568
|
+
width: Length | float | None = None,
|
|
1569
|
+
height: Length | float | None = None,
|
|
1570
|
+
) -> InlineShape:
|
|
1571
|
+
if hasattr(image_path_or_stream, "read"):
|
|
1572
|
+
data = image_path_or_stream.read()
|
|
1573
|
+
filename = Path(getattr(image_path_or_stream, "name", "image.png")).name
|
|
1574
|
+
else:
|
|
1575
|
+
path = Path(image_path_or_stream)
|
|
1576
|
+
data = path.read_bytes()
|
|
1577
|
+
filename = path.name
|
|
1578
|
+
if not isinstance(data, bytes):
|
|
1579
|
+
raise TypeError("image stream must return bytes")
|
|
1580
|
+
for name, value in (("width", width), ("height", height)):
|
|
1581
|
+
if value is not None and (isinstance(value, bool) or float(value) <= 0):
|
|
1582
|
+
raise ValueError(f"{name} must be positive")
|
|
1583
|
+
store, clone = clone_body_image(
|
|
1584
|
+
self._package,
|
|
1585
|
+
self._storage,
|
|
1586
|
+
data,
|
|
1587
|
+
filename,
|
|
1588
|
+
width=None if width is None else float(width),
|
|
1589
|
+
height=None if height is None else float(height),
|
|
1590
|
+
)
|
|
1591
|
+
position = codepoint_to_utf16(self._text, len(self._text))
|
|
1592
|
+
self._replace_text(position, position, "\n\ufffc")
|
|
1593
|
+
append_attachment_run(self._storage, position + 1, clone.root.segment.identifier)
|
|
1594
|
+
shape = InlineShape(store)
|
|
1595
|
+
if self._inline_shapes is None:
|
|
1596
|
+
self._inline_shapes = [
|
|
1597
|
+
InlineShape(item)
|
|
1598
|
+
for item in body_image_stores(self._package, self._storage)
|
|
1599
|
+
]
|
|
1600
|
+
else:
|
|
1601
|
+
self._inline_shapes.append(shape)
|
|
1602
|
+
return shape
|
|
1603
|
+
|
|
1604
|
+
def add_paragraph(self, text: str = "", style=None) -> Paragraph:
|
|
1605
|
+
if not isinstance(text, str):
|
|
1606
|
+
raise TypeError("paragraph text must be a string")
|
|
1607
|
+
if "\n" in text or "\r" in text:
|
|
1608
|
+
raise ValueError("add_paragraph text cannot contain paragraph separators")
|
|
1609
|
+
stylesheet = self._package.stylesheet()
|
|
1610
|
+
if style is None:
|
|
1611
|
+
style_obj = stylesheet.default_paragraph_style()
|
|
1612
|
+
elif isinstance(style, str):
|
|
1613
|
+
style_obj = stylesheet.named_paragraph_style(style)
|
|
1614
|
+
elif isinstance(style, int):
|
|
1615
|
+
style_obj = stylesheet.paragraph_style(style)
|
|
1616
|
+
if style_obj is None:
|
|
1617
|
+
raise KeyError(f"paragraph style {style} was not found")
|
|
1618
|
+
else:
|
|
1619
|
+
raise TypeError("style must be a paragraph style name, object ID, or None")
|
|
1620
|
+
|
|
1621
|
+
old_end = codepoint_to_utf16(self._text, len(self._text))
|
|
1622
|
+
value = "\n" + text
|
|
1623
|
+
self._replace_text(old_end, old_end, value)
|
|
1624
|
+
start = old_end + 1
|
|
1625
|
+
end = start + codepoint_to_utf16(text, len(text))
|
|
1626
|
+
table = self._storage.message.one(STORAGE_TABLE_PARA_STYLE)
|
|
1627
|
+
if table is None:
|
|
1628
|
+
table = self._storage.message.add_bytes(STORAGE_TABLE_PARA_STYLE, b"")
|
|
1629
|
+
runs = parse_attribute_table(bytes(table.value))
|
|
1630
|
+
runs.append(AttributeRun(start, style_obj.segment.identifier))
|
|
1631
|
+
runs.sort(key=lambda item: item.character_index)
|
|
1632
|
+
table.set_bytes(encode_attribute_table(runs))
|
|
1633
|
+
add_object_reference(self._storage.info, style_obj.segment.identifier)
|
|
1634
|
+
return Paragraph(self, start, end, end)
|
|
1635
|
+
|
|
1636
|
+
def add_heading(self, text: str = "", level: int = 1) -> Paragraph:
|
|
1637
|
+
"""Append a heading using the closest built-in Pages heading style."""
|
|
1638
|
+
if not isinstance(level, int) or isinstance(level, bool):
|
|
1639
|
+
raise TypeError("heading level must be an integer")
|
|
1640
|
+
if level < 0 or level > 9:
|
|
1641
|
+
raise ValueError("heading level must be in range 0..9")
|
|
1642
|
+
style = "Title" if level == 0 else f"Heading {level}" if level > 1 else "Heading"
|
|
1643
|
+
try:
|
|
1644
|
+
return self.add_paragraph(text, style=style)
|
|
1645
|
+
except KeyError:
|
|
1646
|
+
# The supplied Pages theme defines Heading through Heading 3. Pages
|
|
1647
|
+
# has no fixed nine-level hierarchy, so deeper docx levels use the
|
|
1648
|
+
# deepest available heading style.
|
|
1649
|
+
return self.add_paragraph(text, style="Heading 3")
|
|
1650
|
+
|
|
1651
|
+
def add_page_break(self) -> Paragraph:
|
|
1652
|
+
"""Append the Pages page-break control and return a post-break proxy."""
|
|
1653
|
+
position = codepoint_to_utf16(self._text, len(self._text))
|
|
1654
|
+
self._replace_text(position, position, "\x05")
|
|
1655
|
+
after_break = position + 1
|
|
1656
|
+
return Paragraph(self, after_break, after_break, after_break)
|
|
1657
|
+
|
|
1658
|
+
def _character_runs(self) -> list[AttributeRun]:
|
|
1659
|
+
field = self._storage.message.one(STORAGE_TABLE_CHAR_STYLE)
|
|
1660
|
+
return [] if field is None else parse_attribute_table(bytes(field.value))
|
|
1661
|
+
|
|
1662
|
+
def _paragraph_runs(self) -> list[AttributeRun]:
|
|
1663
|
+
field = self._storage.message.one(STORAGE_TABLE_PARA_STYLE)
|
|
1664
|
+
return [] if field is None else parse_attribute_table(bytes(field.value))
|
|
1665
|
+
|
|
1666
|
+
def _paragraph_style_id_at(self, index: int) -> int | None:
|
|
1667
|
+
return style_at(self._paragraph_runs(), index)
|
|
1668
|
+
|
|
1669
|
+
def _character_style_id_at(self, index: int) -> int | None:
|
|
1670
|
+
return style_at(self._character_runs(), index)
|
|
1671
|
+
|
|
1672
|
+
def _style_from_id(
|
|
1673
|
+
self, identifier: int | None, expected: WD_STYLE_TYPE
|
|
1674
|
+
) -> Style | None:
|
|
1675
|
+
obj = (
|
|
1676
|
+
self._package.objects().get(identifier)
|
|
1677
|
+
if identifier is not None
|
|
1678
|
+
else None
|
|
1679
|
+
)
|
|
1680
|
+
expected_type = (
|
|
1681
|
+
TYPE_CHARACTER_STYLE
|
|
1682
|
+
if expected == WD_STYLE_TYPE.CHARACTER
|
|
1683
|
+
else TYPE_PARAGRAPH_STYLE
|
|
1684
|
+
)
|
|
1685
|
+
seen: set[int] = set()
|
|
1686
|
+
while (
|
|
1687
|
+
obj is not None
|
|
1688
|
+
and obj.info.type_id == expected_type
|
|
1689
|
+
and is_anonymous_style(obj)
|
|
1690
|
+
):
|
|
1691
|
+
if obj.segment.identifier in seen:
|
|
1692
|
+
return None
|
|
1693
|
+
seen.add(obj.segment.identifier)
|
|
1694
|
+
try:
|
|
1695
|
+
parent = self._package.stylesheet().parent_identifier(obj)
|
|
1696
|
+
except ValueError:
|
|
1697
|
+
return None
|
|
1698
|
+
obj = self._package.objects().get(parent)
|
|
1699
|
+
return (
|
|
1700
|
+
None
|
|
1701
|
+
if obj is None or obj.info.type_id != expected_type
|
|
1702
|
+
else Style(self, obj)
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1705
|
+
def _coerce_style_id(
|
|
1706
|
+
self,
|
|
1707
|
+
value: Style | str | None,
|
|
1708
|
+
expected: WD_STYLE_TYPE,
|
|
1709
|
+
*,
|
|
1710
|
+
none_identifier: int | None,
|
|
1711
|
+
) -> int | None:
|
|
1712
|
+
if value is None:
|
|
1713
|
+
return none_identifier
|
|
1714
|
+
style = self.styles[value] if isinstance(value, str) else value
|
|
1715
|
+
if not isinstance(style, Style):
|
|
1716
|
+
raise TypeError("style must be a style name, Style, or None")
|
|
1717
|
+
if style.type != expected:
|
|
1718
|
+
raise ValueError(f"style {style.name!r} has the wrong style type")
|
|
1719
|
+
return style._obj.segment.identifier
|
|
1720
|
+
|
|
1721
|
+
def _apply_character_style_id(
|
|
1722
|
+
self, start_utf16: int, end_utf16: int, identifier: int | None
|
|
1723
|
+
) -> None:
|
|
1724
|
+
if end_utf16 <= start_utf16:
|
|
1725
|
+
return
|
|
1726
|
+
table = self._storage.message.one(STORAGE_TABLE_CHAR_STYLE)
|
|
1727
|
+
if table is None:
|
|
1728
|
+
table = self._storage.message.add_bytes(STORAGE_TABLE_CHAR_STYLE, b"")
|
|
1729
|
+
runs = parse_attribute_table(bytes(table.value))
|
|
1730
|
+
table.set_bytes(
|
|
1731
|
+
encode_attribute_table(
|
|
1732
|
+
replace_style_range(runs, start_utf16, end_utf16, identifier)
|
|
1733
|
+
)
|
|
1734
|
+
)
|
|
1735
|
+
if identifier is not None:
|
|
1736
|
+
add_object_reference(self._storage.info, identifier)
|
|
1737
|
+
|
|
1738
|
+
def _apply_paragraph_style_id(
|
|
1739
|
+
self, start_utf16: int, end_utf16: int, identifier: int
|
|
1740
|
+
) -> None:
|
|
1741
|
+
table = self._storage.message.one(STORAGE_TABLE_PARA_STYLE)
|
|
1742
|
+
if table is None:
|
|
1743
|
+
table = self._storage.message.add_bytes(STORAGE_TABLE_PARA_STYLE, b"")
|
|
1744
|
+
runs = parse_attribute_table(bytes(table.value))
|
|
1745
|
+
if end_utf16 <= start_utf16:
|
|
1746
|
+
updated = [run for run in runs if run.character_index != start_utf16]
|
|
1747
|
+
updated.append(AttributeRun(start_utf16, identifier))
|
|
1748
|
+
updated.sort(key=lambda run: run.character_index)
|
|
1749
|
+
else:
|
|
1750
|
+
updated = replace_style_range(
|
|
1751
|
+
runs, start_utf16, end_utf16, identifier, compact=False
|
|
1752
|
+
)
|
|
1753
|
+
table.set_bytes(encode_attribute_table(updated))
|
|
1754
|
+
add_object_reference(self._storage.info, identifier)
|
|
1755
|
+
|
|
1756
|
+
def _paragraph_properties_at(self, index: int) -> ParagraphProperties:
|
|
1757
|
+
style = self._package.stylesheet().paragraph_style(self._paragraph_style_id_at(index))
|
|
1758
|
+
return paragraph_style_properties(style)
|
|
1759
|
+
|
|
1760
|
+
def _paragraph_character_properties_at(
|
|
1761
|
+
self, index: int
|
|
1762
|
+
) -> CharacterProperties:
|
|
1763
|
+
style = self._package.stylesheet().paragraph_style(
|
|
1764
|
+
self._paragraph_style_id_at(index)
|
|
1765
|
+
)
|
|
1766
|
+
return character_style_properties(style)
|
|
1767
|
+
|
|
1768
|
+
def _text_between(self, start_utf16: int, end_utf16: int) -> str:
|
|
1769
|
+
start = utf16_to_codepoint(self._text, start_utf16)
|
|
1770
|
+
end = utf16_to_codepoint(self._text, end_utf16)
|
|
1771
|
+
return self._text[start:end]
|
|
1772
|
+
|
|
1773
|
+
def _replace_text(self, start_utf16: int, end_utf16: int, value: str) -> int:
|
|
1774
|
+
delta = replace_storage_text(self._storage, start_utf16, end_utf16, value)
|
|
1775
|
+
start = utf16_to_codepoint(self._text, start_utf16)
|
|
1776
|
+
end = utf16_to_codepoint(self._text, end_utf16)
|
|
1777
|
+
self._text = self._text[:start] + value + self._text[end:]
|
|
1778
|
+
return delta
|
|
1779
|
+
|
|
1780
|
+
def _properties_at(self, index: int) -> CharacterProperties:
|
|
1781
|
+
style_id = style_at(self._character_runs(), index)
|
|
1782
|
+
return character_style_properties(self._package.stylesheet().style(style_id))
|
|
1783
|
+
|
|
1784
|
+
def _apply_properties(
|
|
1785
|
+
self, start_utf16: int, end_utf16: int, props: CharacterProperties
|
|
1786
|
+
) -> None:
|
|
1787
|
+
table = self._storage.message.one(STORAGE_TABLE_CHAR_STYLE)
|
|
1788
|
+
if table is None:
|
|
1789
|
+
table = self._storage.message.add_bytes(STORAGE_TABLE_CHAR_STYLE, b"")
|
|
1790
|
+
runs = parse_attribute_table(bytes(table.value))
|
|
1791
|
+
style_id = None
|
|
1792
|
+
if props.override_count:
|
|
1793
|
+
style, _ = self._package.synthesize_character_style(props)
|
|
1794
|
+
style_id = style.segment.identifier
|
|
1795
|
+
add_object_reference(self._storage.info, style_id)
|
|
1796
|
+
table.set_bytes(
|
|
1797
|
+
encode_attribute_table(replace_style_range(runs, start_utf16, end_utf16, style_id))
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
def _apply_paragraph_properties(
|
|
1801
|
+
self,
|
|
1802
|
+
start_utf16: int,
|
|
1803
|
+
next_start_utf16: int,
|
|
1804
|
+
props: ParagraphProperties,
|
|
1805
|
+
) -> None:
|
|
1806
|
+
table = self._storage.message.one(STORAGE_TABLE_PARA_STYLE)
|
|
1807
|
+
if table is None:
|
|
1808
|
+
table = self._storage.message.add_bytes(STORAGE_TABLE_PARA_STYLE, b"")
|
|
1809
|
+
runs = parse_attribute_table(bytes(table.value))
|
|
1810
|
+
target_positions = [
|
|
1811
|
+
position
|
|
1812
|
+
for position, run in enumerate(runs)
|
|
1813
|
+
if start_utf16 <= run.character_index < next_start_utf16
|
|
1814
|
+
]
|
|
1815
|
+
updated = list(runs)
|
|
1816
|
+
if not target_positions or runs[target_positions[0]].character_index != start_utf16:
|
|
1817
|
+
updated.append(AttributeRun(start_utf16, style_at(runs, start_utf16)))
|
|
1818
|
+
target_positions = [len(updated) - 1, *target_positions]
|
|
1819
|
+
for position in target_positions:
|
|
1820
|
+
run = updated[position]
|
|
1821
|
+
parent, character_raw, _ = self._package.stylesheet().paragraph_style_context(
|
|
1822
|
+
run.object_id
|
|
1823
|
+
)
|
|
1824
|
+
style, _ = self._package.synthesize_paragraph_style(
|
|
1825
|
+
parent,
|
|
1826
|
+
props,
|
|
1827
|
+
character_properties=character_raw,
|
|
1828
|
+
)
|
|
1829
|
+
style_id = style.segment.identifier
|
|
1830
|
+
updated[position] = AttributeRun(run.character_index, style_id)
|
|
1831
|
+
add_object_reference(self._storage.info, style_id)
|
|
1832
|
+
updated.sort(key=lambda item: item.character_index)
|
|
1833
|
+
table.set_bytes(encode_attribute_table(updated))
|
|
1834
|
+
|
|
1835
|
+
def _apply_paragraph_background(
|
|
1836
|
+
self,
|
|
1837
|
+
start_utf16: int,
|
|
1838
|
+
next_start_utf16: int,
|
|
1839
|
+
color: RGBColor | None,
|
|
1840
|
+
) -> None:
|
|
1841
|
+
table = self._storage.message.one(STORAGE_TABLE_PARA_STYLE)
|
|
1842
|
+
if table is None:
|
|
1843
|
+
table = self._storage.message.add_bytes(STORAGE_TABLE_PARA_STYLE, b"")
|
|
1844
|
+
runs = parse_attribute_table(bytes(table.value))
|
|
1845
|
+
target_positions = [
|
|
1846
|
+
position
|
|
1847
|
+
for position, run in enumerate(runs)
|
|
1848
|
+
if start_utf16 <= run.character_index < next_start_utf16
|
|
1849
|
+
]
|
|
1850
|
+
updated = list(runs)
|
|
1851
|
+
if not target_positions or runs[target_positions[0]].character_index != start_utf16:
|
|
1852
|
+
updated.append(AttributeRun(start_utf16, style_at(runs, start_utf16)))
|
|
1853
|
+
target_positions = [len(updated) - 1, *target_positions]
|
|
1854
|
+
for position in target_positions:
|
|
1855
|
+
run = updated[position]
|
|
1856
|
+
parent, character_raw, paragraph_props = (
|
|
1857
|
+
self._package.stylesheet().paragraph_style_context(run.object_id)
|
|
1858
|
+
)
|
|
1859
|
+
character_raw = replace_character_background(character_raw, color)
|
|
1860
|
+
style, _ = self._package.synthesize_paragraph_style(
|
|
1861
|
+
parent,
|
|
1862
|
+
paragraph_props,
|
|
1863
|
+
character_properties=character_raw,
|
|
1864
|
+
)
|
|
1865
|
+
style_id = style.segment.identifier
|
|
1866
|
+
updated[position] = AttributeRun(run.character_index, style_id)
|
|
1867
|
+
add_object_reference(self._storage.info, style_id)
|
|
1868
|
+
updated.sort(key=lambda item: item.character_index)
|
|
1869
|
+
table.set_bytes(encode_attribute_table(updated))
|