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/stylesheet.py
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
"""TSS stylesheet registration and TSWP character-style synthesis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field as dataclass_field, fields
|
|
6
|
+
import struct
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from .iwa import ArchiveSegment, MessageInfo
|
|
10
|
+
from .metadata import (
|
|
11
|
+
package_revision_identifier,
|
|
12
|
+
register_component_object,
|
|
13
|
+
register_external_reference,
|
|
14
|
+
)
|
|
15
|
+
from .protobuf import Message
|
|
16
|
+
from .text import (
|
|
17
|
+
STYLE_CHAR_PROPERTIES,
|
|
18
|
+
STYLE_SUPER,
|
|
19
|
+
TYPE_CHARACTER_STYLE,
|
|
20
|
+
TYPE_PARAGRAPH_STYLE,
|
|
21
|
+
LocatedObject,
|
|
22
|
+
add_object_reference,
|
|
23
|
+
make_reference,
|
|
24
|
+
parse_reference,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .pages import PagesDocument
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
TYPE_STYLESHEET = 401
|
|
32
|
+
STYLESHEET_STYLES = 1
|
|
33
|
+
STYLESHEET_PARENT_TO_CHILDREN = 5
|
|
34
|
+
STYLE_PARENT = 3
|
|
35
|
+
STYLE_ANONYMOUS = 4
|
|
36
|
+
STYLE_PARA_PROPERTIES = 12
|
|
37
|
+
|
|
38
|
+
CHAR_BOLD = 1
|
|
39
|
+
CHAR_ITALIC = 2
|
|
40
|
+
CHAR_FONT_SIZE = 3
|
|
41
|
+
CHAR_FONT_NAME_NULL = 4
|
|
42
|
+
CHAR_FONT_NAME = 5
|
|
43
|
+
CHAR_FONT_COLOR_NULL = 6
|
|
44
|
+
CHAR_FONT_COLOR = 7
|
|
45
|
+
CHAR_SUPERSCRIPT = 10
|
|
46
|
+
CHAR_UNDERLINE = 11
|
|
47
|
+
CHAR_STRIKE = 12
|
|
48
|
+
CHAR_BASELINE_SHIFT = 14
|
|
49
|
+
CHAR_BACKGROUND_COLOR_NULL = 25
|
|
50
|
+
CHAR_BACKGROUND_COLOR = 26
|
|
51
|
+
CHAR_TSD_STROKE_NULL = 43
|
|
52
|
+
CHAR_TSD_STROKE = 44
|
|
53
|
+
CHAR_TSD_FILL_NULL = 45
|
|
54
|
+
CHAR_TSD_FILL = 46
|
|
55
|
+
CHAR_TSD_FILL_SHOULD_FILL_TEXT_CONTAINER = 47
|
|
56
|
+
|
|
57
|
+
KNOWN_CHARACTER_FIELDS = {
|
|
58
|
+
CHAR_BOLD,
|
|
59
|
+
CHAR_ITALIC,
|
|
60
|
+
CHAR_FONT_SIZE,
|
|
61
|
+
CHAR_FONT_NAME_NULL,
|
|
62
|
+
CHAR_FONT_NAME,
|
|
63
|
+
CHAR_FONT_COLOR_NULL,
|
|
64
|
+
CHAR_FONT_COLOR,
|
|
65
|
+
CHAR_SUPERSCRIPT,
|
|
66
|
+
CHAR_UNDERLINE,
|
|
67
|
+
CHAR_STRIKE,
|
|
68
|
+
CHAR_BASELINE_SHIFT,
|
|
69
|
+
CHAR_BACKGROUND_COLOR_NULL,
|
|
70
|
+
CHAR_BACKGROUND_COLOR,
|
|
71
|
+
CHAR_TSD_FILL_NULL,
|
|
72
|
+
CHAR_TSD_FILL,
|
|
73
|
+
CHAR_TSD_FILL_SHOULD_FILL_TEXT_CONTAINER,
|
|
74
|
+
}
|
|
75
|
+
CACHEABLE_CHARACTER_FIELDS = KNOWN_CHARACTER_FIELDS - {
|
|
76
|
+
CHAR_FONT_NAME_NULL,
|
|
77
|
+
CHAR_FONT_COLOR_NULL,
|
|
78
|
+
CHAR_BACKGROUND_COLOR_NULL,
|
|
79
|
+
CHAR_TSD_FILL_NULL,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
PARA_ALIGNMENT = 1
|
|
83
|
+
PARA_FIRST_LINE_INDENT = 7
|
|
84
|
+
PARA_LEFT_INDENT = 11
|
|
85
|
+
PARA_LINE_SPACING_NULL = 12
|
|
86
|
+
PARA_LINE_SPACING = 13
|
|
87
|
+
PARA_PAGE_BREAK_BEFORE = 14
|
|
88
|
+
PARA_RIGHT_INDENT = 19
|
|
89
|
+
PARA_SPACE_AFTER = 20
|
|
90
|
+
PARA_SPACE_BEFORE = 21
|
|
91
|
+
PARA_TABS_NULL = 24
|
|
92
|
+
PARA_TABS = 25
|
|
93
|
+
|
|
94
|
+
KNOWN_PARAGRAPH_FIELDS = {
|
|
95
|
+
PARA_ALIGNMENT,
|
|
96
|
+
PARA_FIRST_LINE_INDENT,
|
|
97
|
+
PARA_LEFT_INDENT,
|
|
98
|
+
PARA_LINE_SPACING,
|
|
99
|
+
PARA_PAGE_BREAK_BEFORE,
|
|
100
|
+
PARA_RIGHT_INDENT,
|
|
101
|
+
PARA_SPACE_AFTER,
|
|
102
|
+
PARA_SPACE_BEFORE,
|
|
103
|
+
PARA_TABS,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class RGBColor:
|
|
109
|
+
"""An sRGB color represented by 8-bit red, green, and blue channels."""
|
|
110
|
+
|
|
111
|
+
red: int
|
|
112
|
+
green: int
|
|
113
|
+
blue: int
|
|
114
|
+
|
|
115
|
+
def __post_init__(self) -> None:
|
|
116
|
+
if any(channel < 0 or channel > 255 for channel in self):
|
|
117
|
+
raise ValueError("RGB channels must be in range 0..255")
|
|
118
|
+
|
|
119
|
+
def __iter__(self):
|
|
120
|
+
return iter((self.red, self.green, self.blue))
|
|
121
|
+
|
|
122
|
+
def __str__(self) -> str:
|
|
123
|
+
return f"{self.red:02X}{self.green:02X}{self.blue:02X}"
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def from_string(cls, value: str) -> "RGBColor":
|
|
127
|
+
value = value.removeprefix("#")
|
|
128
|
+
if len(value) != 6:
|
|
129
|
+
raise ValueError("RGB color must contain six hexadecimal digits")
|
|
130
|
+
return cls(*(int(value[index : index + 2], 16) for index in (0, 2, 4)))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class CharacterProperties:
|
|
135
|
+
bold: bool | None = None
|
|
136
|
+
italic: bool | None = None
|
|
137
|
+
underline: bool | None = None
|
|
138
|
+
strike: bool | None = None
|
|
139
|
+
font_name: str | None = None
|
|
140
|
+
font_size: float | None = None
|
|
141
|
+
color: RGBColor | None = None
|
|
142
|
+
vertical_align: int | None = None
|
|
143
|
+
baseline_shift: float | None = None
|
|
144
|
+
background_color: RGBColor | None = None
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def override_count(self) -> int:
|
|
148
|
+
return sum(getattr(self, item.name) is not None for item in fields(self))
|
|
149
|
+
|
|
150
|
+
def with_value(self, name: str, value) -> "CharacterProperties":
|
|
151
|
+
if name not in {item.name for item in fields(self)}:
|
|
152
|
+
raise AttributeError(name)
|
|
153
|
+
values = {item.name: getattr(self, item.name) for item in fields(self)}
|
|
154
|
+
values[name] = value
|
|
155
|
+
return CharacterProperties(**values)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True)
|
|
159
|
+
class LineSpacing:
|
|
160
|
+
mode: int
|
|
161
|
+
amount: float
|
|
162
|
+
baseline_rule: float | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class ParagraphTab:
|
|
167
|
+
position: float
|
|
168
|
+
alignment: int = 0
|
|
169
|
+
leader: str | None = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class ParagraphProperties:
|
|
174
|
+
alignment: int | None = None
|
|
175
|
+
line_spacing: LineSpacing | None = None
|
|
176
|
+
page_break_before: bool | None = None
|
|
177
|
+
space_before: float | None = None
|
|
178
|
+
space_after: float | None = None
|
|
179
|
+
left_indent: float | None = None
|
|
180
|
+
right_indent: float | None = None
|
|
181
|
+
first_line_indent: float | None = None
|
|
182
|
+
tabs: tuple[ParagraphTab, ...] | None = None
|
|
183
|
+
unknown_fields: tuple[bytes, ...] = dataclass_field(default=(), repr=False)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def override_count(self) -> int:
|
|
187
|
+
known = sum(
|
|
188
|
+
getattr(self, item.name) is not None
|
|
189
|
+
for item in fields(self)
|
|
190
|
+
if item.name != "unknown_fields"
|
|
191
|
+
)
|
|
192
|
+
return known + len(self.unknown_fields)
|
|
193
|
+
|
|
194
|
+
def with_value(self, name: str, value) -> "ParagraphProperties":
|
|
195
|
+
public = {item.name for item in fields(self) if item.name != "unknown_fields"}
|
|
196
|
+
if name not in public:
|
|
197
|
+
raise AttributeError(name)
|
|
198
|
+
values = {item.name: getattr(self, item.name) for item in fields(self)}
|
|
199
|
+
values[name] = value
|
|
200
|
+
null_marker = {
|
|
201
|
+
"line_spacing": PARA_LINE_SPACING_NULL,
|
|
202
|
+
"tabs": PARA_TABS_NULL,
|
|
203
|
+
}.get(name)
|
|
204
|
+
if null_marker is not None:
|
|
205
|
+
values["unknown_fields"] = tuple(
|
|
206
|
+
raw
|
|
207
|
+
for raw in self.unknown_fields
|
|
208
|
+
if Message.parse(raw).fields[0].number != null_marker
|
|
209
|
+
)
|
|
210
|
+
return ParagraphProperties(**values)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _float_bits(value: float) -> int:
|
|
214
|
+
return int.from_bytes(struct.pack("<f", float(value)), "little")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _bits_float(value: int) -> float:
|
|
218
|
+
return struct.unpack("<f", int(value).to_bytes(4, "little"))[0]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def encode_color(color: RGBColor) -> bytes:
|
|
222
|
+
message = Message()
|
|
223
|
+
message.add_varint(1, 1) # RGB model
|
|
224
|
+
message.add_fixed32(3, _float_bits(color.red / 255.0))
|
|
225
|
+
message.add_fixed32(4, _float_bits(color.green / 255.0))
|
|
226
|
+
message.add_fixed32(5, _float_bits(color.blue / 255.0))
|
|
227
|
+
message.add_fixed32(6, _float_bits(1.0))
|
|
228
|
+
message.add_varint(12, 1) # sRGB
|
|
229
|
+
# Pages 15 writes this additional 1.0 field in every sample RGB color.
|
|
230
|
+
message.add_fixed32(13, _float_bits(1.0))
|
|
231
|
+
return message.encode()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def decode_color(data: bytes) -> RGBColor | None:
|
|
235
|
+
message = Message.parse(data)
|
|
236
|
+
model = message.one(1)
|
|
237
|
+
channels = [message.one(number) for number in (3, 4, 5)]
|
|
238
|
+
if model is None or int(model.value) != 1 or any(item is None for item in channels):
|
|
239
|
+
return None
|
|
240
|
+
return RGBColor(*(max(0, min(255, round(_bits_float(item.value) * 255))) for item in channels))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def encode_fill(color: RGBColor) -> bytes:
|
|
244
|
+
fill = Message()
|
|
245
|
+
fill.add_bytes(1, encode_color(color))
|
|
246
|
+
return fill.encode()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def decode_fill(data: bytes) -> RGBColor | None:
|
|
250
|
+
fill = Message.parse(data)
|
|
251
|
+
color = fill.one(1)
|
|
252
|
+
return None if color is None else decode_color(bytes(color.value))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def encode_character_properties(props: CharacterProperties) -> bytes:
|
|
256
|
+
message = Message()
|
|
257
|
+
if props.bold is not None:
|
|
258
|
+
message.add_varint(CHAR_BOLD, int(props.bold))
|
|
259
|
+
if props.italic is not None:
|
|
260
|
+
message.add_varint(CHAR_ITALIC, int(props.italic))
|
|
261
|
+
if props.font_size is not None:
|
|
262
|
+
message.add_fixed32(CHAR_FONT_SIZE, _float_bits(props.font_size))
|
|
263
|
+
if props.font_name is not None:
|
|
264
|
+
message.add_bytes(CHAR_FONT_NAME, props.font_name.encode("utf-8"))
|
|
265
|
+
if props.color is not None:
|
|
266
|
+
message.add_bytes(CHAR_FONT_COLOR, encode_color(props.color))
|
|
267
|
+
if props.vertical_align is not None:
|
|
268
|
+
message.add_varint(CHAR_SUPERSCRIPT, props.vertical_align)
|
|
269
|
+
if props.underline is not None:
|
|
270
|
+
message.add_varint(CHAR_UNDERLINE, int(props.underline))
|
|
271
|
+
if props.strike is not None:
|
|
272
|
+
message.add_varint(CHAR_STRIKE, int(props.strike))
|
|
273
|
+
if props.baseline_shift is not None:
|
|
274
|
+
message.add_fixed32(CHAR_BASELINE_SHIFT, _float_bits(props.baseline_shift))
|
|
275
|
+
if props.background_color is not None:
|
|
276
|
+
# The Pages-authored character-background fixture contains field 26
|
|
277
|
+
# alone: no null marker and no TSD fill/container fields.
|
|
278
|
+
message.add_bytes(CHAR_BACKGROUND_COLOR, encode_color(props.background_color))
|
|
279
|
+
# Foreground character color uses field 7 plus a synchronized TSD fill.
|
|
280
|
+
if props.color is not None:
|
|
281
|
+
message.add_bytes(CHAR_TSD_FILL, encode_fill(props.color))
|
|
282
|
+
return message.encode()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def decode_character_properties(data: bytes) -> CharacterProperties:
|
|
286
|
+
message = Message.parse(data)
|
|
287
|
+
|
|
288
|
+
def boolean(number: int) -> bool | None:
|
|
289
|
+
field = message.one(number)
|
|
290
|
+
return None if field is None else bool(field.value)
|
|
291
|
+
|
|
292
|
+
size = message.one(CHAR_FONT_SIZE)
|
|
293
|
+
name = message.one(CHAR_FONT_NAME)
|
|
294
|
+
color = message.one(CHAR_FONT_COLOR)
|
|
295
|
+
vertical_align = message.one(CHAR_SUPERSCRIPT)
|
|
296
|
+
baseline_shift = message.one(CHAR_BASELINE_SHIFT)
|
|
297
|
+
background_color = message.one(CHAR_BACKGROUND_COLOR)
|
|
298
|
+
font_color_null = message.one(CHAR_FONT_COLOR_NULL)
|
|
299
|
+
background_color_null = message.one(CHAR_BACKGROUND_COLOR_NULL)
|
|
300
|
+
fill = message.one(CHAR_TSD_FILL)
|
|
301
|
+
fill_null = message.one(CHAR_TSD_FILL_NULL)
|
|
302
|
+
fill_container = message.one(CHAR_TSD_FILL_SHOULD_FILL_TEXT_CONTAINER)
|
|
303
|
+
fill_color = (
|
|
304
|
+
None
|
|
305
|
+
if fill_null is not None and bool(fill_null.value)
|
|
306
|
+
else decode_fill(bytes(fill.value))
|
|
307
|
+
if fill
|
|
308
|
+
else None
|
|
309
|
+
)
|
|
310
|
+
fills_background = bool(fill_container.value) if fill_container else False
|
|
311
|
+
decoded_font_color = None
|
|
312
|
+
if font_color_null is None or not bool(font_color_null.value):
|
|
313
|
+
if color is not None:
|
|
314
|
+
decoded_font_color = decode_color(bytes(color.value))
|
|
315
|
+
elif not fills_background:
|
|
316
|
+
decoded_font_color = fill_color
|
|
317
|
+
decoded_background_color = None
|
|
318
|
+
if background_color_null is None or not bool(background_color_null.value):
|
|
319
|
+
if background_color is not None:
|
|
320
|
+
decoded_background_color = decode_color(bytes(background_color.value))
|
|
321
|
+
elif fills_background:
|
|
322
|
+
decoded_background_color = fill_color
|
|
323
|
+
return CharacterProperties(
|
|
324
|
+
bold=boolean(CHAR_BOLD),
|
|
325
|
+
italic=boolean(CHAR_ITALIC),
|
|
326
|
+
underline=boolean(CHAR_UNDERLINE),
|
|
327
|
+
strike=boolean(CHAR_STRIKE),
|
|
328
|
+
font_name=bytes(name.value).decode("utf-8", "replace") if name else None,
|
|
329
|
+
font_size=_bits_float(size.value) if size else None,
|
|
330
|
+
color=decoded_font_color,
|
|
331
|
+
vertical_align=int(vertical_align.value) if vertical_align else None,
|
|
332
|
+
baseline_shift=(
|
|
333
|
+
_bits_float(baseline_shift.value) if baseline_shift else None
|
|
334
|
+
),
|
|
335
|
+
background_color=decoded_background_color,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def replace_character_background(
|
|
340
|
+
data: bytes, color: RGBColor | None
|
|
341
|
+
) -> bytes:
|
|
342
|
+
"""Replace only the Pages character-background fields in raw properties.
|
|
343
|
+
|
|
344
|
+
Paragraph styles can carry character properties in field 11. Keeping the
|
|
345
|
+
raw message here is important: changing a paragraph background must not
|
|
346
|
+
discard character-property fields that this library does not yet model.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
message = Message.parse(data)
|
|
350
|
+
message.fields = [
|
|
351
|
+
field
|
|
352
|
+
for field in message.fields
|
|
353
|
+
if field.number not in (CHAR_BACKGROUND_COLOR_NULL, CHAR_BACKGROUND_COLOR)
|
|
354
|
+
]
|
|
355
|
+
if color is not None:
|
|
356
|
+
message.add_bytes(CHAR_BACKGROUND_COLOR, encode_color(color))
|
|
357
|
+
return message.encode()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def character_style_properties(style: LocatedObject | None) -> CharacterProperties:
|
|
361
|
+
if style is None:
|
|
362
|
+
return CharacterProperties()
|
|
363
|
+
field = style.message.one(STYLE_CHAR_PROPERTIES)
|
|
364
|
+
return CharacterProperties() if field is None else decode_character_properties(bytes(field.value))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def encode_paragraph_properties(props: ParagraphProperties) -> bytes:
|
|
368
|
+
message = Message()
|
|
369
|
+
if props.alignment is not None:
|
|
370
|
+
message.add_varint(PARA_ALIGNMENT, props.alignment)
|
|
371
|
+
if props.first_line_indent is not None:
|
|
372
|
+
message.add_fixed32(PARA_FIRST_LINE_INDENT, _float_bits(props.first_line_indent))
|
|
373
|
+
if props.left_indent is not None:
|
|
374
|
+
message.add_fixed32(PARA_LEFT_INDENT, _float_bits(props.left_indent))
|
|
375
|
+
if props.line_spacing is not None:
|
|
376
|
+
spacing = Message()
|
|
377
|
+
spacing.add_varint(1, props.line_spacing.mode)
|
|
378
|
+
spacing.add_fixed32(2, _float_bits(props.line_spacing.amount))
|
|
379
|
+
if props.line_spacing.baseline_rule is not None:
|
|
380
|
+
spacing.add_fixed32(3, _float_bits(props.line_spacing.baseline_rule))
|
|
381
|
+
message.add_bytes(PARA_LINE_SPACING, spacing.encode())
|
|
382
|
+
if props.page_break_before is not None:
|
|
383
|
+
message.add_varint(PARA_PAGE_BREAK_BEFORE, int(props.page_break_before))
|
|
384
|
+
if props.right_indent is not None:
|
|
385
|
+
message.add_fixed32(PARA_RIGHT_INDENT, _float_bits(props.right_indent))
|
|
386
|
+
if props.space_after is not None:
|
|
387
|
+
message.add_fixed32(PARA_SPACE_AFTER, _float_bits(props.space_after))
|
|
388
|
+
if props.space_before is not None:
|
|
389
|
+
message.add_fixed32(PARA_SPACE_BEFORE, _float_bits(props.space_before))
|
|
390
|
+
if props.tabs is not None:
|
|
391
|
+
tabs = Message()
|
|
392
|
+
for tab in props.tabs:
|
|
393
|
+
item = Message()
|
|
394
|
+
item.add_fixed32(1, _float_bits(tab.position))
|
|
395
|
+
if tab.alignment:
|
|
396
|
+
item.add_varint(2, tab.alignment)
|
|
397
|
+
if tab.leader:
|
|
398
|
+
item.add_bytes(3, tab.leader.encode("utf-8"))
|
|
399
|
+
tabs.add_bytes(1, item.encode())
|
|
400
|
+
message.add_bytes(PARA_TABS, tabs.encode())
|
|
401
|
+
for raw in props.unknown_fields:
|
|
402
|
+
message.fields.extend(Message.parse(raw).fields)
|
|
403
|
+
return message.encode()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def decode_paragraph_properties(data: bytes) -> ParagraphProperties:
|
|
407
|
+
message = Message.parse(data)
|
|
408
|
+
|
|
409
|
+
def floating(number: int) -> float | None:
|
|
410
|
+
item = message.one(number)
|
|
411
|
+
return None if item is None else _bits_float(item.value)
|
|
412
|
+
|
|
413
|
+
alignment = message.one(PARA_ALIGNMENT)
|
|
414
|
+
page_break_before = message.one(PARA_PAGE_BREAK_BEFORE)
|
|
415
|
+
spacing_field = message.one(PARA_LINE_SPACING)
|
|
416
|
+
spacing = None
|
|
417
|
+
if spacing_field is not None:
|
|
418
|
+
spacing_message = Message.parse(bytes(spacing_field.value))
|
|
419
|
+
mode = spacing_message.one(1)
|
|
420
|
+
amount = spacing_message.one(2)
|
|
421
|
+
baseline = spacing_message.one(3)
|
|
422
|
+
if mode is not None and amount is not None:
|
|
423
|
+
spacing = LineSpacing(
|
|
424
|
+
int(mode.value),
|
|
425
|
+
_bits_float(amount.value),
|
|
426
|
+
_bits_float(baseline.value) if baseline else None,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
tabs_field = message.one(PARA_TABS)
|
|
430
|
+
tabs = None
|
|
431
|
+
if tabs_field is not None:
|
|
432
|
+
parsed_tabs = Message.parse(bytes(tabs_field.value))
|
|
433
|
+
values = []
|
|
434
|
+
for raw in parsed_tabs.get(1):
|
|
435
|
+
tab = Message.parse(bytes(raw.value))
|
|
436
|
+
position = tab.one(1)
|
|
437
|
+
if position is None:
|
|
438
|
+
continue
|
|
439
|
+
alignment_field = tab.one(2)
|
|
440
|
+
leader = tab.one(3)
|
|
441
|
+
values.append(
|
|
442
|
+
ParagraphTab(
|
|
443
|
+
_bits_float(position.value),
|
|
444
|
+
int(alignment_field.value) if alignment_field else 0,
|
|
445
|
+
bytes(leader.value).decode("utf-8", "replace") if leader else None,
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
tabs = tuple(values)
|
|
449
|
+
|
|
450
|
+
unknown = tuple(
|
|
451
|
+
item.original or item.encode()
|
|
452
|
+
for item in message.fields
|
|
453
|
+
if item.number not in KNOWN_PARAGRAPH_FIELDS
|
|
454
|
+
)
|
|
455
|
+
return ParagraphProperties(
|
|
456
|
+
alignment=int(alignment.value) if alignment else None,
|
|
457
|
+
line_spacing=spacing,
|
|
458
|
+
page_break_before=(
|
|
459
|
+
bool(page_break_before.value) if page_break_before is not None else None
|
|
460
|
+
),
|
|
461
|
+
space_before=floating(PARA_SPACE_BEFORE),
|
|
462
|
+
space_after=floating(PARA_SPACE_AFTER),
|
|
463
|
+
left_indent=floating(PARA_LEFT_INDENT),
|
|
464
|
+
right_indent=floating(PARA_RIGHT_INDENT),
|
|
465
|
+
first_line_indent=floating(PARA_FIRST_LINE_INDENT),
|
|
466
|
+
tabs=tabs,
|
|
467
|
+
unknown_fields=unknown,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def is_anonymous_style(style: LocatedObject | None) -> bool:
|
|
472
|
+
if style is None:
|
|
473
|
+
return False
|
|
474
|
+
super_field = style.message.one(STYLE_SUPER)
|
|
475
|
+
if super_field is None:
|
|
476
|
+
return False
|
|
477
|
+
marker = Message.parse(bytes(super_field.value)).one(STYLE_ANONYMOUS)
|
|
478
|
+
return marker is not None and int(marker.value) == 1
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def style_name(style: LocatedObject | None) -> str | None:
|
|
482
|
+
if style is None:
|
|
483
|
+
return None
|
|
484
|
+
super_field = style.message.one(STYLE_SUPER)
|
|
485
|
+
if super_field is None:
|
|
486
|
+
return None
|
|
487
|
+
name = Message.parse(bytes(super_field.value)).one(1)
|
|
488
|
+
return None if name is None else bytes(name.value).decode("utf-8", "replace")
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def style_identifier(style: LocatedObject | None) -> str | None:
|
|
492
|
+
if style is None:
|
|
493
|
+
return None
|
|
494
|
+
super_field = style.message.one(STYLE_SUPER)
|
|
495
|
+
if super_field is None:
|
|
496
|
+
return None
|
|
497
|
+
identifier = Message.parse(bytes(super_field.value)).one(2)
|
|
498
|
+
return (
|
|
499
|
+
None
|
|
500
|
+
if identifier is None
|
|
501
|
+
else bytes(identifier.value).decode("utf-8", "replace")
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def paragraph_style_properties(style: LocatedObject | None) -> ParagraphProperties:
|
|
506
|
+
if style is None or not is_anonymous_style(style):
|
|
507
|
+
return ParagraphProperties()
|
|
508
|
+
field = style.message.one(STYLE_PARA_PROPERTIES)
|
|
509
|
+
return ParagraphProperties() if field is None else decode_paragraph_properties(bytes(field.value))
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class Stylesheet:
|
|
513
|
+
"""Mutation facade for a document's TSS.StylesheetArchive."""
|
|
514
|
+
|
|
515
|
+
def __init__(self, document: "PagesDocument"):
|
|
516
|
+
self.document = document
|
|
517
|
+
matches = [obj for obj in document.object_messages() if obj.info.type_id == TYPE_STYLESHEET]
|
|
518
|
+
if len(matches) != 1:
|
|
519
|
+
raise ValueError(f"expected one TSS.StylesheetArchive, found {len(matches)}")
|
|
520
|
+
self.archive = matches[0]
|
|
521
|
+
self._cache: dict[CharacterProperties, LocatedObject] = {}
|
|
522
|
+
self._paragraph_cache: dict[
|
|
523
|
+
tuple[int, bytes, ParagraphProperties], LocatedObject
|
|
524
|
+
] = {}
|
|
525
|
+
self._index_styles()
|
|
526
|
+
|
|
527
|
+
def _index_styles(self) -> None:
|
|
528
|
+
registered = {
|
|
529
|
+
parse_reference(bytes(field.value)) for field in self.archive.message.get(STYLESHEET_STYLES)
|
|
530
|
+
}
|
|
531
|
+
for obj in self.document.object_messages():
|
|
532
|
+
if obj.info.type_id != TYPE_CHARACTER_STYLE or obj.segment.identifier not in registered:
|
|
533
|
+
continue
|
|
534
|
+
raw = obj.message.one(STYLE_CHAR_PROPERTIES)
|
|
535
|
+
if raw is None:
|
|
536
|
+
continue
|
|
537
|
+
message = Message.parse(bytes(raw.value))
|
|
538
|
+
if all(field.number in CACHEABLE_CHARACTER_FIELDS for field in message.fields):
|
|
539
|
+
self._cache.setdefault(decode_character_properties(bytes(raw.value)), obj)
|
|
540
|
+
for obj in self.document.object_messages():
|
|
541
|
+
if obj.info.type_id != TYPE_PARAGRAPH_STYLE or obj.segment.identifier not in registered:
|
|
542
|
+
continue
|
|
543
|
+
if not is_anonymous_style(obj):
|
|
544
|
+
continue
|
|
545
|
+
try:
|
|
546
|
+
parent = self.parent_identifier(obj)
|
|
547
|
+
except ValueError:
|
|
548
|
+
continue
|
|
549
|
+
character = obj.message.one(STYLE_CHAR_PROPERTIES)
|
|
550
|
+
character_raw = bytes(character.value) if character else b""
|
|
551
|
+
paragraph = obj.message.one(STYLE_PARA_PROPERTIES)
|
|
552
|
+
props = (
|
|
553
|
+
decode_paragraph_properties(bytes(paragraph.value))
|
|
554
|
+
if paragraph
|
|
555
|
+
else ParagraphProperties()
|
|
556
|
+
)
|
|
557
|
+
self._paragraph_cache.setdefault((parent, character_raw, props), obj)
|
|
558
|
+
|
|
559
|
+
def style(self, identifier: int | None) -> LocatedObject | None:
|
|
560
|
+
if identifier is None:
|
|
561
|
+
return None
|
|
562
|
+
obj = self.document.objects().get(identifier)
|
|
563
|
+
return obj if obj is not None and obj.info.type_id == TYPE_CHARACTER_STYLE else None
|
|
564
|
+
|
|
565
|
+
def paragraph_style(self, identifier: int | None) -> LocatedObject | None:
|
|
566
|
+
if identifier is None:
|
|
567
|
+
return None
|
|
568
|
+
obj = self.document.objects().get(identifier)
|
|
569
|
+
return obj if obj is not None and obj.info.type_id == TYPE_PARAGRAPH_STYLE else None
|
|
570
|
+
|
|
571
|
+
@staticmethod
|
|
572
|
+
def parent_identifier(style: LocatedObject) -> int:
|
|
573
|
+
super_field = style.message.one(STYLE_SUPER)
|
|
574
|
+
if super_field is None:
|
|
575
|
+
raise ValueError("style lacks TSS.StyleArchive super")
|
|
576
|
+
super_message = Message.parse(bytes(super_field.value))
|
|
577
|
+
parent = super_message.one(STYLE_PARENT)
|
|
578
|
+
if parent is None:
|
|
579
|
+
raise ValueError("style template has no parent style")
|
|
580
|
+
identifier = parse_reference(bytes(parent.value))
|
|
581
|
+
if identifier is None:
|
|
582
|
+
raise ValueError("style parent reference is empty")
|
|
583
|
+
return identifier
|
|
584
|
+
|
|
585
|
+
def _template(self) -> LocatedObject:
|
|
586
|
+
choices: list[LocatedObject] = []
|
|
587
|
+
for obj in self.document.object_messages():
|
|
588
|
+
if obj.info.type_id != TYPE_CHARACTER_STYLE:
|
|
589
|
+
continue
|
|
590
|
+
super_field = obj.message.one(STYLE_SUPER)
|
|
591
|
+
if super_field is None:
|
|
592
|
+
continue
|
|
593
|
+
super_message = Message.parse(bytes(super_field.value))
|
|
594
|
+
anonymous = super_message.one(STYLE_ANONYMOUS)
|
|
595
|
+
if anonymous is not None and int(anonymous.value) == 1 and super_message.one(STYLE_PARENT):
|
|
596
|
+
choices.append(obj)
|
|
597
|
+
if not choices:
|
|
598
|
+
raise ValueError("document has no anonymous character style with a parent")
|
|
599
|
+
choices.sort(key=lambda obj: obj.segment.identifier)
|
|
600
|
+
return choices[0]
|
|
601
|
+
|
|
602
|
+
def _paragraph_template(self) -> LocatedObject:
|
|
603
|
+
choices = [
|
|
604
|
+
obj
|
|
605
|
+
for obj in self.document.object_messages()
|
|
606
|
+
if obj.info.type_id == TYPE_PARAGRAPH_STYLE
|
|
607
|
+
and is_anonymous_style(obj)
|
|
608
|
+
and obj.message.one(STYLE_SUPER)
|
|
609
|
+
]
|
|
610
|
+
choices = [obj for obj in choices if Message.parse(bytes(obj.message.one(1).value)).one(3)]
|
|
611
|
+
if not choices:
|
|
612
|
+
raise ValueError("document has no anonymous paragraph style with a parent")
|
|
613
|
+
choices.sort(key=lambda obj: obj.segment.identifier)
|
|
614
|
+
return choices[0]
|
|
615
|
+
|
|
616
|
+
def default_paragraph_style(self) -> LocatedObject:
|
|
617
|
+
named = []
|
|
618
|
+
for obj in self.document.object_messages():
|
|
619
|
+
if obj.info.type_id != TYPE_PARAGRAPH_STYLE or is_anonymous_style(obj):
|
|
620
|
+
continue
|
|
621
|
+
super_field = obj.message.one(STYLE_SUPER)
|
|
622
|
+
if super_field is None:
|
|
623
|
+
continue
|
|
624
|
+
name = Message.parse(bytes(super_field.value)).one(1)
|
|
625
|
+
if name is not None:
|
|
626
|
+
named.append((bytes(name.value).decode("utf-8", "replace"), obj))
|
|
627
|
+
for name, obj in named:
|
|
628
|
+
if name == "Body":
|
|
629
|
+
return obj
|
|
630
|
+
if not named:
|
|
631
|
+
raise ValueError("document has no named paragraph style")
|
|
632
|
+
return named[0][1]
|
|
633
|
+
|
|
634
|
+
def named_paragraph_style(self, name: str) -> LocatedObject:
|
|
635
|
+
for obj in self.document.object_messages():
|
|
636
|
+
if obj.info.type_id != TYPE_PARAGRAPH_STYLE or is_anonymous_style(obj):
|
|
637
|
+
continue
|
|
638
|
+
super_field = obj.message.one(STYLE_SUPER)
|
|
639
|
+
if super_field is None:
|
|
640
|
+
continue
|
|
641
|
+
name_field = Message.parse(bytes(super_field.value)).one(1)
|
|
642
|
+
if name_field is not None and bytes(name_field.value).decode("utf-8", "replace") == name:
|
|
643
|
+
return obj
|
|
644
|
+
raise KeyError(f"paragraph style {name!r} was not found")
|
|
645
|
+
|
|
646
|
+
def named_character_style(self, name: str) -> LocatedObject:
|
|
647
|
+
for obj in self.document.object_messages():
|
|
648
|
+
if obj.info.type_id != TYPE_CHARACTER_STYLE or is_anonymous_style(obj):
|
|
649
|
+
continue
|
|
650
|
+
if style_name(obj) == name:
|
|
651
|
+
return obj
|
|
652
|
+
raise KeyError(f"character style {name!r} was not found")
|
|
653
|
+
|
|
654
|
+
def named_styles(self) -> list[LocatedObject]:
|
|
655
|
+
registered = {
|
|
656
|
+
parse_reference(bytes(field.value))
|
|
657
|
+
for field in self.archive.message.get(STYLESHEET_STYLES)
|
|
658
|
+
}
|
|
659
|
+
return [
|
|
660
|
+
obj
|
|
661
|
+
for obj in self.document.object_messages()
|
|
662
|
+
if obj.segment.identifier in registered
|
|
663
|
+
and obj.info.type_id in (TYPE_CHARACTER_STYLE, TYPE_PARAGRAPH_STYLE)
|
|
664
|
+
and not is_anonymous_style(obj)
|
|
665
|
+
and style_name(obj) is not None
|
|
666
|
+
]
|
|
667
|
+
|
|
668
|
+
def register_style(self, style: LocatedObject, parent_identifier: int) -> None:
|
|
669
|
+
identifier = style.segment.identifier
|
|
670
|
+
existing = {
|
|
671
|
+
parse_reference(bytes(field.value)) for field in self.archive.message.get(STYLESHEET_STYLES)
|
|
672
|
+
}
|
|
673
|
+
if identifier not in existing:
|
|
674
|
+
self.archive.message.add_bytes(STYLESHEET_STYLES, make_reference(identifier))
|
|
675
|
+
|
|
676
|
+
selected_field = None
|
|
677
|
+
selected = None
|
|
678
|
+
for field in self.archive.message.get(STYLESHEET_PARENT_TO_CHILDREN):
|
|
679
|
+
entry = Message.parse(bytes(field.value))
|
|
680
|
+
parent = entry.one(1)
|
|
681
|
+
if parent and parse_reference(bytes(parent.value)) == parent_identifier:
|
|
682
|
+
selected_field, selected = field, entry
|
|
683
|
+
break
|
|
684
|
+
if selected is None:
|
|
685
|
+
selected = Message()
|
|
686
|
+
selected.add_bytes(1, make_reference(parent_identifier))
|
|
687
|
+
selected_field = self.archive.message.add_bytes(
|
|
688
|
+
STYLESHEET_PARENT_TO_CHILDREN, selected.encode()
|
|
689
|
+
)
|
|
690
|
+
children = {parse_reference(bytes(field.value)) for field in selected.get(2)}
|
|
691
|
+
if identifier not in children:
|
|
692
|
+
selected.add_bytes(2, make_reference(identifier))
|
|
693
|
+
selected_field.set_bytes(selected.encode())
|
|
694
|
+
|
|
695
|
+
add_object_reference(self.archive.info, identifier)
|
|
696
|
+
|
|
697
|
+
def synthesize_character_style(
|
|
698
|
+
self,
|
|
699
|
+
props: CharacterProperties,
|
|
700
|
+
*,
|
|
701
|
+
experimental_allow_paragraph_background: bool = False,
|
|
702
|
+
) -> tuple[LocatedObject, dict]:
|
|
703
|
+
if props.override_count == 0:
|
|
704
|
+
raise ValueError("cannot synthesize an empty character style")
|
|
705
|
+
if (
|
|
706
|
+
props.background_color is not None
|
|
707
|
+
and not experimental_allow_paragraph_background
|
|
708
|
+
):
|
|
709
|
+
raise ValueError(
|
|
710
|
+
"Pages field-26 background is paragraph-scoped; use "
|
|
711
|
+
"Paragraph.background_color or Paragraph.highlight_color"
|
|
712
|
+
)
|
|
713
|
+
cached = self._cache.get(props)
|
|
714
|
+
if cached is not None:
|
|
715
|
+
return cached, {"style_cache_hit": True, "style_id": cached.segment.identifier}
|
|
716
|
+
|
|
717
|
+
template = self._template()
|
|
718
|
+
package = self.document.package_metadata()
|
|
719
|
+
identifier = self.document.allocate_object_identifier()
|
|
720
|
+
payload = Message.parse(template.message.encode())
|
|
721
|
+
properties = payload.one(STYLE_CHAR_PROPERTIES)
|
|
722
|
+
encoded_properties = encode_character_properties(props)
|
|
723
|
+
if properties is None:
|
|
724
|
+
payload.add_bytes(STYLE_CHAR_PROPERTIES, encoded_properties)
|
|
725
|
+
else:
|
|
726
|
+
properties.set_bytes(encoded_properties)
|
|
727
|
+
override_count = payload.one(10)
|
|
728
|
+
if override_count is None:
|
|
729
|
+
payload.add_varint(10, props.override_count)
|
|
730
|
+
else:
|
|
731
|
+
override_count.set_varint(props.override_count)
|
|
732
|
+
|
|
733
|
+
header = Message.parse(template.segment.header.encode())
|
|
734
|
+
header_identifier = header.one(1)
|
|
735
|
+
if header_identifier is None:
|
|
736
|
+
raise ValueError("style ArchiveInfo lacks identifier")
|
|
737
|
+
header_identifier.set_varint(identifier)
|
|
738
|
+
info_fields = header.get(2)
|
|
739
|
+
if len(info_fields) != 1:
|
|
740
|
+
raise ValueError("style template must contain exactly one MessageInfo")
|
|
741
|
+
info_field = info_fields[0]
|
|
742
|
+
info_message = Message.parse(bytes(info_field.value))
|
|
743
|
+
length_field = info_message.one(3)
|
|
744
|
+
if length_field is None:
|
|
745
|
+
raise ValueError("style MessageInfo lacks payload length")
|
|
746
|
+
new_info = MessageInfo(info_field, info_message, TYPE_CHARACTER_STYLE, int(length_field.value))
|
|
747
|
+
new_info.update_length(len(payload.encode()))
|
|
748
|
+
segment = ArchiveSegment(header, b"", identifier, [new_info], [payload], [b""])
|
|
749
|
+
self.document.iwa_files[template.filename].segments.append(segment)
|
|
750
|
+
created = LocatedObject(template.filename, segment, new_info, payload)
|
|
751
|
+
|
|
752
|
+
parent_identifier = self.parent_identifier(created)
|
|
753
|
+
self.register_style(created, parent_identifier)
|
|
754
|
+
revision = package_revision_identifier(package.message)
|
|
755
|
+
signature = repr(props)
|
|
756
|
+
object_uuid, component_id = register_component_object(
|
|
757
|
+
package.message,
|
|
758
|
+
"DocumentStylesheet",
|
|
759
|
+
identifier,
|
|
760
|
+
uuid_seed=f"python-pages:{revision}:{identifier}:{signature}",
|
|
761
|
+
)
|
|
762
|
+
register_external_reference(package.message, "Document", component_id, identifier)
|
|
763
|
+
self._cache[props] = created
|
|
764
|
+
return created, {
|
|
765
|
+
"style_cache_hit": False,
|
|
766
|
+
"style_template_id": template.segment.identifier,
|
|
767
|
+
"style_id": identifier,
|
|
768
|
+
"parent_style_id": parent_identifier,
|
|
769
|
+
"override_count": props.override_count,
|
|
770
|
+
"object_uuid": object_uuid,
|
|
771
|
+
"component_id": component_id,
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
def paragraph_style_context(
|
|
775
|
+
self, identifier: int | None
|
|
776
|
+
) -> tuple[int, bytes, ParagraphProperties]:
|
|
777
|
+
style = self.paragraph_style(identifier)
|
|
778
|
+
if style is None:
|
|
779
|
+
return self.default_paragraph_style().segment.identifier, b"", ParagraphProperties()
|
|
780
|
+
if not is_anonymous_style(style):
|
|
781
|
+
return style.segment.identifier, b"", ParagraphProperties()
|
|
782
|
+
parent = self.parent_identifier(style)
|
|
783
|
+
character = style.message.one(STYLE_CHAR_PROPERTIES)
|
|
784
|
+
character_raw = bytes(character.value) if character else b""
|
|
785
|
+
return parent, character_raw, paragraph_style_properties(style)
|
|
786
|
+
|
|
787
|
+
def synthesize_paragraph_style(
|
|
788
|
+
self,
|
|
789
|
+
parent_identifier: int,
|
|
790
|
+
props: ParagraphProperties,
|
|
791
|
+
*,
|
|
792
|
+
character_properties: bytes = b"",
|
|
793
|
+
) -> tuple[LocatedObject, dict]:
|
|
794
|
+
key = (parent_identifier, character_properties, props)
|
|
795
|
+
cached = self._paragraph_cache.get(key)
|
|
796
|
+
if cached is not None:
|
|
797
|
+
return cached, {"style_cache_hit": True, "style_id": cached.segment.identifier}
|
|
798
|
+
|
|
799
|
+
if props.override_count == 0 and not character_properties:
|
|
800
|
+
parent = self.paragraph_style(parent_identifier)
|
|
801
|
+
if parent is None:
|
|
802
|
+
raise ValueError(f"paragraph parent style {parent_identifier} was not found")
|
|
803
|
+
return parent, {"style_cache_hit": True, "style_id": parent_identifier}
|
|
804
|
+
|
|
805
|
+
template = self._paragraph_template()
|
|
806
|
+
package = self.document.package_metadata()
|
|
807
|
+
identifier = self.document.allocate_object_identifier()
|
|
808
|
+
payload = Message.parse(template.message.encode())
|
|
809
|
+
super_field = payload.one(STYLE_SUPER)
|
|
810
|
+
if super_field is None:
|
|
811
|
+
raise ValueError("paragraph style template lacks TSS.StyleArchive super")
|
|
812
|
+
super_message = Message.parse(bytes(super_field.value))
|
|
813
|
+
parent_field = super_message.one(STYLE_PARENT)
|
|
814
|
+
if parent_field is None:
|
|
815
|
+
super_message.add_bytes(STYLE_PARENT, make_reference(parent_identifier))
|
|
816
|
+
else:
|
|
817
|
+
parent_field.set_bytes(make_reference(parent_identifier))
|
|
818
|
+
anonymous = super_message.one(STYLE_ANONYMOUS)
|
|
819
|
+
if anonymous is None:
|
|
820
|
+
super_message.add_varint(STYLE_ANONYMOUS, 1)
|
|
821
|
+
else:
|
|
822
|
+
anonymous.set_varint(1)
|
|
823
|
+
super_field.set_bytes(super_message.encode())
|
|
824
|
+
|
|
825
|
+
payload.fields = [
|
|
826
|
+
item for item in payload.fields if item.number not in (10, 11, STYLE_PARA_PROPERTIES)
|
|
827
|
+
]
|
|
828
|
+
total_overrides = props.override_count
|
|
829
|
+
if character_properties:
|
|
830
|
+
total_overrides += len(Message.parse(character_properties).fields)
|
|
831
|
+
payload.add_varint(10, total_overrides)
|
|
832
|
+
if character_properties:
|
|
833
|
+
payload.add_bytes(STYLE_CHAR_PROPERTIES, character_properties)
|
|
834
|
+
if props.override_count:
|
|
835
|
+
payload.add_bytes(STYLE_PARA_PROPERTIES, encode_paragraph_properties(props))
|
|
836
|
+
elif character_properties:
|
|
837
|
+
# Pages-authored anonymous type-2022 styles with character-only
|
|
838
|
+
# overrides retain an explicit empty ParagraphStylePropertiesArchive
|
|
839
|
+
# field. Match that observed wire shape exactly.
|
|
840
|
+
payload.add_bytes(STYLE_PARA_PROPERTIES, b"")
|
|
841
|
+
|
|
842
|
+
header = Message.parse(template.segment.header.encode())
|
|
843
|
+
header_identifier = header.one(1)
|
|
844
|
+
if header_identifier is None:
|
|
845
|
+
raise ValueError("paragraph style ArchiveInfo lacks identifier")
|
|
846
|
+
header_identifier.set_varint(identifier)
|
|
847
|
+
info_fields = header.get(2)
|
|
848
|
+
if len(info_fields) != 1:
|
|
849
|
+
raise ValueError("paragraph style template must contain exactly one MessageInfo")
|
|
850
|
+
info_field = info_fields[0]
|
|
851
|
+
info_message = Message.parse(bytes(info_field.value))
|
|
852
|
+
type_field = info_message.one(1)
|
|
853
|
+
length_field = info_message.one(3)
|
|
854
|
+
if (
|
|
855
|
+
type_field is None
|
|
856
|
+
or int(type_field.value) != TYPE_PARAGRAPH_STYLE
|
|
857
|
+
or length_field is None
|
|
858
|
+
):
|
|
859
|
+
raise ValueError("paragraph style template has incompatible MessageInfo")
|
|
860
|
+
new_info = MessageInfo(
|
|
861
|
+
info_field,
|
|
862
|
+
info_message,
|
|
863
|
+
TYPE_PARAGRAPH_STYLE,
|
|
864
|
+
int(length_field.value),
|
|
865
|
+
)
|
|
866
|
+
new_info.update_length(len(payload.encode()))
|
|
867
|
+
segment = ArchiveSegment(header, b"", identifier, [new_info], [payload], [b""])
|
|
868
|
+
self.document.iwa_files[template.filename].segments.append(segment)
|
|
869
|
+
created = LocatedObject(template.filename, segment, new_info, payload)
|
|
870
|
+
|
|
871
|
+
self.register_style(created, parent_identifier)
|
|
872
|
+
revision = package_revision_identifier(package.message)
|
|
873
|
+
object_uuid, component_id = register_component_object(
|
|
874
|
+
package.message,
|
|
875
|
+
"DocumentStylesheet",
|
|
876
|
+
identifier,
|
|
877
|
+
uuid_seed=(
|
|
878
|
+
f"python-pages:{revision}:{identifier}:paragraph:"
|
|
879
|
+
f"{parent_identifier}:{character_properties.hex()}:{props!r}"
|
|
880
|
+
),
|
|
881
|
+
)
|
|
882
|
+
register_external_reference(package.message, "Document", component_id, identifier)
|
|
883
|
+
self._paragraph_cache[key] = created
|
|
884
|
+
return created, {
|
|
885
|
+
"style_cache_hit": False,
|
|
886
|
+
"style_template_id": template.segment.identifier,
|
|
887
|
+
"style_id": identifier,
|
|
888
|
+
"parent_style_id": parent_identifier,
|
|
889
|
+
"override_count": total_overrides,
|
|
890
|
+
"object_uuid": object_uuid,
|
|
891
|
+
"component_id": component_id,
|
|
892
|
+
}
|