msdoc2docx 0.7.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.
- doc2docx/__init__.py +6 -0
- doc2docx/__main__.py +4 -0
- doc2docx/binary/__init__.py +4 -0
- doc2docx/binary/reader.py +87 -0
- doc2docx/cfb/__init__.py +5 -0
- doc2docx/cfb/constants.py +9 -0
- doc2docx/cfb/container.py +370 -0
- doc2docx/cfb/directory.py +135 -0
- doc2docx/cfb/header.py +108 -0
- doc2docx/cli.py +100 -0
- doc2docx/converter.py +321 -0
- doc2docx/diagnostics.py +105 -0
- doc2docx/errors.py +37 -0
- doc2docx/model/__init__.py +57 -0
- doc2docx/model/document.py +806 -0
- doc2docx/msdoc/__init__.py +24 -0
- doc2docx/msdoc/document_properties.py +31 -0
- doc2docx/msdoc/fib.py +246 -0
- doc2docx/msdoc/fonts.py +132 -0
- doc2docx/msdoc/formatting.py +467 -0
- doc2docx/msdoc/headers.py +184 -0
- doc2docx/msdoc/pieces.py +407 -0
- doc2docx/msdoc/sections.py +259 -0
- doc2docx/msdoc/sprm.py +753 -0
- doc2docx/msdoc/styles.py +288 -0
- doc2docx/ooxml/__init__.py +3 -0
- doc2docx/ooxml/package.py +1163 -0
- msdoc2docx-0.7.0.dist-info/METADATA +84 -0
- msdoc2docx-0.7.0.dist-info/RECORD +33 -0
- msdoc2docx-0.7.0.dist-info/WHEEL +5 -0
- msdoc2docx-0.7.0.dist-info/entry_points.txt +2 -0
- msdoc2docx-0.7.0.dist-info/licenses/LICENSE +21 -0
- msdoc2docx-0.7.0.dist-info/top_level.txt +1 -0
doc2docx/msdoc/sprm.py
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
"""Bounded parsing and application of supported MS-DOC property modifiers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
import struct
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
from ..errors import InvalidWordDocument
|
|
10
|
+
from ..model import (
|
|
11
|
+
BorderProperties,
|
|
12
|
+
CharacterProperties,
|
|
13
|
+
ParagraphProperties,
|
|
14
|
+
ShadingProperties,
|
|
15
|
+
TableBorders,
|
|
16
|
+
TableCellDefinition,
|
|
17
|
+
TableCellMarginOverride,
|
|
18
|
+
TableRowProperties,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True, frozen=True)
|
|
23
|
+
class PropertyModifier:
|
|
24
|
+
opcode: int
|
|
25
|
+
operand: bytes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_FIXED_OPERAND_LENGTHS = {0: 1, 1: 1, 2: 2, 3: 4, 4: 2, 5: 2, 7: 3}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_grpprl(
|
|
32
|
+
data: bytes,
|
|
33
|
+
*,
|
|
34
|
+
label: str,
|
|
35
|
+
allow_trailing_zero_padding: bool = False,
|
|
36
|
+
) -> tuple[PropertyModifier, ...]:
|
|
37
|
+
"""Parse a complete grpprl, using Sprm.spra to bound every operand."""
|
|
38
|
+
|
|
39
|
+
modifiers: list[PropertyModifier] = []
|
|
40
|
+
position = 0
|
|
41
|
+
while position < len(data):
|
|
42
|
+
if len(data) - position < 2:
|
|
43
|
+
if allow_trailing_zero_padding and data[position:] == b"\0":
|
|
44
|
+
break
|
|
45
|
+
raise InvalidWordDocument(
|
|
46
|
+
f"{label} ends with a truncated Sprm at byte {position}"
|
|
47
|
+
)
|
|
48
|
+
opcode = struct.unpack_from("<H", data, position)[0]
|
|
49
|
+
position += 2
|
|
50
|
+
spra = opcode >> 13
|
|
51
|
+
if spra == 6:
|
|
52
|
+
if position >= len(data):
|
|
53
|
+
raise InvalidWordDocument(
|
|
54
|
+
f"{label} Sprm 0x{opcode:04X} has no variable-length prefix"
|
|
55
|
+
)
|
|
56
|
+
if opcode == 0xD608:
|
|
57
|
+
if position > len(data) - 2:
|
|
58
|
+
raise InvalidWordDocument(
|
|
59
|
+
f"{label} sprmTDefTable has no 16-bit length prefix"
|
|
60
|
+
)
|
|
61
|
+
byte_count = struct.unpack_from("<H", data, position)[0]
|
|
62
|
+
# TDefTableOperand.cb counts the remainder incremented by one;
|
|
63
|
+
# including the two-byte cb itself gives cb+1 operand bytes.
|
|
64
|
+
operand_length = byte_count + 1
|
|
65
|
+
else:
|
|
66
|
+
byte_count = data[position]
|
|
67
|
+
operand_length = 1 + byte_count
|
|
68
|
+
# sprmPChgTabs permits 0xFF with an implicit size. We do not yet
|
|
69
|
+
# consume tab-stop operands, so retaining the remainder as one
|
|
70
|
+
# opaque operand is safer than guessing boundaries.
|
|
71
|
+
if opcode == 0xC615 and byte_count == 0xFF:
|
|
72
|
+
modifiers.append(PropertyModifier(opcode, data[position:]))
|
|
73
|
+
position = len(data)
|
|
74
|
+
continue
|
|
75
|
+
else:
|
|
76
|
+
operand_length = _FIXED_OPERAND_LENGTHS.get(spra)
|
|
77
|
+
if operand_length is None:
|
|
78
|
+
raise InvalidWordDocument(
|
|
79
|
+
f"{label} Sprm 0x{opcode:04X} has invalid spra {spra}"
|
|
80
|
+
)
|
|
81
|
+
operand_end = position + operand_length
|
|
82
|
+
if operand_end > len(data):
|
|
83
|
+
raise InvalidWordDocument(
|
|
84
|
+
f"{label} Sprm 0x{opcode:04X} operand exceeds grpprl"
|
|
85
|
+
)
|
|
86
|
+
modifiers.append(PropertyModifier(opcode, data[position:operand_end]))
|
|
87
|
+
position = operand_end
|
|
88
|
+
return tuple(modifiers)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_CHARACTER_TOGGLES = {
|
|
92
|
+
0x0835: "bold",
|
|
93
|
+
0x0836: "italic",
|
|
94
|
+
0x0837: "strike",
|
|
95
|
+
0x083A: "small_caps",
|
|
96
|
+
0x083B: "caps",
|
|
97
|
+
0x083C: "hidden",
|
|
98
|
+
0x2A53: "double_strike",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_UNDERLINES = {
|
|
102
|
+
0x00: "none",
|
|
103
|
+
0x01: "single",
|
|
104
|
+
0x02: "words",
|
|
105
|
+
0x03: "double",
|
|
106
|
+
0x04: "dotted",
|
|
107
|
+
0x06: "thick",
|
|
108
|
+
0x07: "dash",
|
|
109
|
+
0x09: "dotDash",
|
|
110
|
+
0x0A: "dotDotDash",
|
|
111
|
+
0x0B: "wave",
|
|
112
|
+
0x14: "dottedHeavy",
|
|
113
|
+
0x17: "dashedHeavy",
|
|
114
|
+
0x19: "dashDotHeavy",
|
|
115
|
+
0x1A: "dashDotDotHeavy",
|
|
116
|
+
0x1B: "wavyHeavy",
|
|
117
|
+
0x27: "dashLong",
|
|
118
|
+
0x2B: "wavyDouble",
|
|
119
|
+
0x37: "dashLongHeavy",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_ICO_RGB = {
|
|
123
|
+
0x01: "000000",
|
|
124
|
+
0x02: "0000FF",
|
|
125
|
+
0x03: "00FFFF",
|
|
126
|
+
0x04: "00FF00",
|
|
127
|
+
0x05: "FF00FF",
|
|
128
|
+
0x06: "FF0000",
|
|
129
|
+
0x07: "FFFF00",
|
|
130
|
+
0x08: "FFFFFF",
|
|
131
|
+
0x09: "000080",
|
|
132
|
+
0x0A: "008080",
|
|
133
|
+
0x0B: "008000",
|
|
134
|
+
0x0C: "800080",
|
|
135
|
+
0x0D: "800000",
|
|
136
|
+
0x0E: "808000",
|
|
137
|
+
0x0F: "808080",
|
|
138
|
+
0x10: "C0C0C0",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_ICO_HIGHLIGHT = {
|
|
142
|
+
0x00: "none",
|
|
143
|
+
0x01: "black",
|
|
144
|
+
0x02: "blue",
|
|
145
|
+
0x03: "cyan",
|
|
146
|
+
0x04: "green",
|
|
147
|
+
0x05: "magenta",
|
|
148
|
+
0x06: "red",
|
|
149
|
+
0x07: "yellow",
|
|
150
|
+
0x08: "white",
|
|
151
|
+
0x09: "darkBlue",
|
|
152
|
+
0x0A: "darkCyan",
|
|
153
|
+
0x0B: "darkGreen",
|
|
154
|
+
0x0C: "darkMagenta",
|
|
155
|
+
0x0D: "darkRed",
|
|
156
|
+
0x0E: "darkYellow",
|
|
157
|
+
0x0F: "darkGray",
|
|
158
|
+
0x10: "lightGray",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_BORDER_STYLES = {
|
|
162
|
+
0x00: "none",
|
|
163
|
+
0x01: "single",
|
|
164
|
+
0x03: "double",
|
|
165
|
+
0x05: "single",
|
|
166
|
+
0x06: "dotted",
|
|
167
|
+
0x07: "dashed",
|
|
168
|
+
0x08: "dotDash",
|
|
169
|
+
0x09: "dotDotDash",
|
|
170
|
+
0x0A: "triple",
|
|
171
|
+
0x0B: "thinThickSmallGap",
|
|
172
|
+
0x0C: "thickThinSmallGap",
|
|
173
|
+
0x0D: "thinThickThinSmallGap",
|
|
174
|
+
0x0E: "thinThickMediumGap",
|
|
175
|
+
0x0F: "thickThinMediumGap",
|
|
176
|
+
0x10: "thinThickThinMediumGap",
|
|
177
|
+
0x11: "thinThickLargeGap",
|
|
178
|
+
0x12: "thickThinLargeGap",
|
|
179
|
+
0x13: "thinThickThinLargeGap",
|
|
180
|
+
0x14: "wave",
|
|
181
|
+
0x15: "doubleWave",
|
|
182
|
+
0x16: "dashSmallGap",
|
|
183
|
+
0x17: "dashDotStroked",
|
|
184
|
+
0x18: "threeDEmboss",
|
|
185
|
+
0x19: "threeDEngrave",
|
|
186
|
+
0x1A: "outset",
|
|
187
|
+
0x1B: "inset",
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_SHADING_PATTERNS = {
|
|
191
|
+
0x00: "clear",
|
|
192
|
+
0x01: "solid",
|
|
193
|
+
0x02: "pct5",
|
|
194
|
+
0x03: "pct10",
|
|
195
|
+
0x04: "pct20",
|
|
196
|
+
0x05: "pct25",
|
|
197
|
+
0x06: "pct30",
|
|
198
|
+
0x07: "pct40",
|
|
199
|
+
0x08: "pct50",
|
|
200
|
+
0x09: "pct60",
|
|
201
|
+
0x0A: "pct70",
|
|
202
|
+
0x0B: "pct75",
|
|
203
|
+
0x0C: "pct80",
|
|
204
|
+
0x0D: "pct90",
|
|
205
|
+
0x0E: "horzStripe",
|
|
206
|
+
0x0F: "vertStripe",
|
|
207
|
+
0x10: "reverseDiagStripe",
|
|
208
|
+
0x11: "diagStripe",
|
|
209
|
+
0x12: "horzCross",
|
|
210
|
+
0x13: "diagCross",
|
|
211
|
+
0x14: "thinHorzStripe",
|
|
212
|
+
0x15: "thinVertStripe",
|
|
213
|
+
0x16: "thinReverseDiagStripe",
|
|
214
|
+
0x17: "thinDiagStripe",
|
|
215
|
+
0x18: "thinHorzCross",
|
|
216
|
+
0x19: "thinDiagCross",
|
|
217
|
+
0x25: "pct12",
|
|
218
|
+
0x26: "pct15",
|
|
219
|
+
0x2B: "pct35",
|
|
220
|
+
0x2E: "pct45",
|
|
221
|
+
0x31: "pct55",
|
|
222
|
+
0x33: "pct62",
|
|
223
|
+
0x34: "pct65",
|
|
224
|
+
0x39: "pct85",
|
|
225
|
+
0x3C: "pct95",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_CELL_MARGIN_SIDES = (
|
|
229
|
+
(0x01, "top"),
|
|
230
|
+
(0x02, "left"),
|
|
231
|
+
(0x04, "bottom"),
|
|
232
|
+
(0x08, "right"),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _parse_brc80(data: bytes) -> BorderProperties | None:
|
|
237
|
+
if len(data) != 4:
|
|
238
|
+
raise InvalidWordDocument("Brc80 must contain exactly four bytes")
|
|
239
|
+
if data == b"\xFF\xFF\xFF\xFF" or data[1] in (0x00, 0xFF):
|
|
240
|
+
return None
|
|
241
|
+
style = _BORDER_STYLES.get(data[1])
|
|
242
|
+
if style is None:
|
|
243
|
+
return None
|
|
244
|
+
color = "auto" if data[2] == 0 else _ICO_RGB.get(data[2], "auto")
|
|
245
|
+
return BorderProperties(
|
|
246
|
+
style=style,
|
|
247
|
+
size_eighth_points=max(data[0], 2),
|
|
248
|
+
color=color,
|
|
249
|
+
space_points=data[3] & 0x1F,
|
|
250
|
+
shadow=bool(data[3] & 0x20),
|
|
251
|
+
frame=bool(data[3] & 0x40),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _parse_table_borders80(operand: bytes) -> TableBorders:
|
|
256
|
+
if len(operand) != 25 or operand[0] != 0x18:
|
|
257
|
+
raise InvalidWordDocument("sprmTTableBorders80 operand must contain 24 bytes")
|
|
258
|
+
values = [
|
|
259
|
+
_parse_brc80(operand[1 + index * 4 : 5 + index * 4])
|
|
260
|
+
for index in range(6)
|
|
261
|
+
]
|
|
262
|
+
return TableBorders(*values)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _parse_cssa(
|
|
266
|
+
operand: bytes,
|
|
267
|
+
) -> tuple[int, int, tuple[str, ...], int | None]:
|
|
268
|
+
if len(operand) != 7 or operand[0] != 6:
|
|
269
|
+
raise InvalidWordDocument("CSSAOperand must contain exactly six data bytes")
|
|
270
|
+
first_cell, limit_cell, side_flags, width_type, width = struct.unpack_from(
|
|
271
|
+
"<BBBBH",
|
|
272
|
+
operand,
|
|
273
|
+
1,
|
|
274
|
+
)
|
|
275
|
+
if first_cell > limit_cell or limit_cell > 63:
|
|
276
|
+
raise InvalidWordDocument("CSSAOperand contains an invalid cell range")
|
|
277
|
+
if not side_flags or side_flags & ~0x0F:
|
|
278
|
+
raise InvalidWordDocument("CSSAOperand contains invalid cell sides")
|
|
279
|
+
if width_type not in (0, 3):
|
|
280
|
+
raise InvalidWordDocument("CSSAOperand contains an unsupported width type")
|
|
281
|
+
if width_type == 0 and width:
|
|
282
|
+
raise InvalidWordDocument("CSSAOperand ftsNil width must be zero")
|
|
283
|
+
if width > 31680:
|
|
284
|
+
raise InvalidWordDocument("CSSAOperand cell margin exceeds 22 inches")
|
|
285
|
+
sides = tuple(name for mask, name in _CELL_MARGIN_SIDES if side_flags & mask)
|
|
286
|
+
return first_cell, limit_cell, sides, width if width_type == 3 else None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _parse_shd80(data: bytes) -> tuple[ShadingProperties | None, bool]:
|
|
290
|
+
if len(data) != 2:
|
|
291
|
+
raise InvalidWordDocument("Shd80 must contain exactly two bytes")
|
|
292
|
+
value = struct.unpack("<H", data)[0]
|
|
293
|
+
if value in (0x0000, 0xFFFF):
|
|
294
|
+
return None, False
|
|
295
|
+
foreground_index = value & 0x1F
|
|
296
|
+
background_index = (value >> 5) & 0x1F
|
|
297
|
+
pattern_index = (value >> 10) & 0x3F
|
|
298
|
+
pattern = _SHADING_PATTERNS.get(pattern_index)
|
|
299
|
+
unsupported = pattern is None
|
|
300
|
+
if pattern is None:
|
|
301
|
+
pattern = "clear"
|
|
302
|
+
foreground = (
|
|
303
|
+
"auto" if foreground_index == 0 else _ICO_RGB.get(foreground_index, "auto")
|
|
304
|
+
)
|
|
305
|
+
background = (
|
|
306
|
+
"auto" if background_index == 0 else _ICO_RGB.get(background_index, "auto")
|
|
307
|
+
)
|
|
308
|
+
unsupported = unsupported or foreground_index > 0x10 or background_index > 0x10
|
|
309
|
+
return ShadingProperties(pattern, foreground, background), unsupported
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _parse_def_table_shd80(
|
|
313
|
+
operand: bytes,
|
|
314
|
+
) -> tuple[tuple[ShadingProperties | None, ...], bool]:
|
|
315
|
+
if not operand or operand[0] != len(operand) - 1 or operand[0] % 2:
|
|
316
|
+
raise InvalidWordDocument("DefTableShd80Operand has an invalid byte count")
|
|
317
|
+
if operand[0] > 126:
|
|
318
|
+
raise InvalidWordDocument("DefTableShd80Operand has too many cells")
|
|
319
|
+
values: list[ShadingProperties | None] = []
|
|
320
|
+
unsupported = False
|
|
321
|
+
for position in range(1, len(operand), 2):
|
|
322
|
+
shading, approximated = _parse_shd80(operand[position : position + 2])
|
|
323
|
+
values.append(shading)
|
|
324
|
+
unsupported = unsupported or approximated
|
|
325
|
+
return tuple(values), unsupported
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _parse_tdef_table(operand: bytes) -> TableRowProperties:
|
|
329
|
+
if len(operand) < 3:
|
|
330
|
+
raise InvalidWordDocument("sprmTDefTable operand is truncated")
|
|
331
|
+
byte_count = struct.unpack_from("<H", operand)[0]
|
|
332
|
+
if len(operand) != byte_count + 1:
|
|
333
|
+
raise InvalidWordDocument(
|
|
334
|
+
f"sprmTDefTable cb {byte_count} does not match {len(operand)} bytes"
|
|
335
|
+
)
|
|
336
|
+
column_count = operand[2]
|
|
337
|
+
if column_count > 63:
|
|
338
|
+
raise InvalidWordDocument(
|
|
339
|
+
f"sprmTDefTable has invalid column count {column_count}"
|
|
340
|
+
)
|
|
341
|
+
boundaries_end = 3 + 2 * (column_count + 1)
|
|
342
|
+
if boundaries_end > len(operand):
|
|
343
|
+
raise InvalidWordDocument("sprmTDefTable column boundaries are truncated")
|
|
344
|
+
boundaries = struct.unpack_from(f"<{column_count + 1}h", operand, 3)
|
|
345
|
+
if any(current < previous for previous, current in zip(boundaries, boundaries[1:])):
|
|
346
|
+
raise InvalidWordDocument("sprmTDefTable column boundaries are decreasing")
|
|
347
|
+
|
|
348
|
+
descriptor_data = operand[boundaries_end:]
|
|
349
|
+
if len(descriptor_data) % 20:
|
|
350
|
+
raise InvalidWordDocument("sprmTDefTable TC80 array is truncated")
|
|
351
|
+
descriptor_count = min(len(descriptor_data) // 20, column_count)
|
|
352
|
+
definitions: list[TableCellDefinition] = []
|
|
353
|
+
for index in range(descriptor_count):
|
|
354
|
+
start = index * 20
|
|
355
|
+
tcgrf, preferred_width = struct.unpack_from("<HH", descriptor_data, start)
|
|
356
|
+
horizontal_value = tcgrf & 0x03
|
|
357
|
+
vertical_value = (tcgrf >> 5) & 0x03
|
|
358
|
+
alignment_value = (tcgrf >> 7) & 0x03
|
|
359
|
+
width_type = (tcgrf >> 9) & 0x07
|
|
360
|
+
definitions.append(
|
|
361
|
+
TableCellDefinition(
|
|
362
|
+
preferred_width_twips=(
|
|
363
|
+
preferred_width if width_type == 3 else None
|
|
364
|
+
),
|
|
365
|
+
horizontal_merge=(
|
|
366
|
+
"continue"
|
|
367
|
+
if horizontal_value == 1
|
|
368
|
+
else "restart" if horizontal_value in (2, 3) else None
|
|
369
|
+
),
|
|
370
|
+
vertical_merge={1: "continue", 3: "restart"}.get(vertical_value),
|
|
371
|
+
vertical_alignment={1: "center", 2: "bottom"}.get(
|
|
372
|
+
alignment_value
|
|
373
|
+
),
|
|
374
|
+
fit_text=True if tcgrf & 0x1000 else None,
|
|
375
|
+
no_wrap=True if tcgrf & 0x2000 else None,
|
|
376
|
+
borders=TableBorders(
|
|
377
|
+
top=_parse_brc80(descriptor_data[start + 4 : start + 8]),
|
|
378
|
+
left=_parse_brc80(descriptor_data[start + 8 : start + 12]),
|
|
379
|
+
bottom=_parse_brc80(descriptor_data[start + 12 : start + 16]),
|
|
380
|
+
right=_parse_brc80(descriptor_data[start + 16 : start + 20]),
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
definitions.extend(
|
|
385
|
+
TableCellDefinition() for _ in range(column_count - len(definitions))
|
|
386
|
+
)
|
|
387
|
+
return TableRowProperties(
|
|
388
|
+
cell_boundaries_twips=tuple(boundaries),
|
|
389
|
+
cell_definitions=tuple(definitions),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def apply_character_modifiers(
|
|
394
|
+
modifiers: tuple[PropertyModifier, ...],
|
|
395
|
+
*,
|
|
396
|
+
initial_properties: CharacterProperties | None = None,
|
|
397
|
+
base_properties: CharacterProperties | None = None,
|
|
398
|
+
font_names: dict[int, str] | None = None,
|
|
399
|
+
style_properties_at: Callable[[int], CharacterProperties] | None = None,
|
|
400
|
+
) -> tuple[CharacterProperties, set[int], int]:
|
|
401
|
+
properties = initial_properties or CharacterProperties()
|
|
402
|
+
paragraph_style_properties = base_properties or CharacterProperties()
|
|
403
|
+
style_baseline = paragraph_style_properties
|
|
404
|
+
font_names = font_names or {}
|
|
405
|
+
unsupported: set[int] = set()
|
|
406
|
+
style_relative_toggle_count = 0
|
|
407
|
+
for modifier in modifiers:
|
|
408
|
+
opcode = modifier.opcode
|
|
409
|
+
operand = modifier.operand
|
|
410
|
+
if opcode == 0x2A33: # sprmCPlain
|
|
411
|
+
properties = CharacterProperties()
|
|
412
|
+
style_baseline = paragraph_style_properties
|
|
413
|
+
elif opcode == 0x4A30: # sprmCIstd
|
|
414
|
+
style_id = struct.unpack("<H", operand)[0]
|
|
415
|
+
properties = CharacterProperties(style_id=style_id)
|
|
416
|
+
style_baseline = paragraph_style_properties
|
|
417
|
+
if style_properties_at is not None:
|
|
418
|
+
style_baseline = merge_character_properties(
|
|
419
|
+
paragraph_style_properties,
|
|
420
|
+
style_properties_at(style_id),
|
|
421
|
+
)
|
|
422
|
+
elif opcode in _CHARACTER_TOGGLES:
|
|
423
|
+
value = operand[0]
|
|
424
|
+
if value in (0x00, 0x01):
|
|
425
|
+
properties = replace(
|
|
426
|
+
properties,
|
|
427
|
+
**{_CHARACTER_TOGGLES[opcode]: bool(value)},
|
|
428
|
+
)
|
|
429
|
+
elif value in (0x80, 0x81):
|
|
430
|
+
style_relative_toggle_count += 1
|
|
431
|
+
attribute = _CHARACTER_TOGGLES[opcode]
|
|
432
|
+
style_value = bool(getattr(style_baseline, attribute))
|
|
433
|
+
properties = replace(
|
|
434
|
+
properties,
|
|
435
|
+
**{attribute: style_value if value == 0x80 else not style_value},
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
unsupported.add(opcode)
|
|
439
|
+
elif opcode in (0x4A4F, 0x4A50, 0x4A51, 0x4A5E):
|
|
440
|
+
font_index = struct.unpack("<H", operand)[0]
|
|
441
|
+
font_name = font_names.get(font_index)
|
|
442
|
+
if font_name is None:
|
|
443
|
+
unsupported.add(opcode)
|
|
444
|
+
continue
|
|
445
|
+
attribute = {
|
|
446
|
+
0x4A4F: "ascii_font",
|
|
447
|
+
0x4A50: "east_asia_font",
|
|
448
|
+
0x4A51: "high_ansi_font",
|
|
449
|
+
0x4A5E: "complex_script_font",
|
|
450
|
+
}[opcode]
|
|
451
|
+
properties = replace(properties, **{attribute: font_name})
|
|
452
|
+
elif opcode == 0x2A3E:
|
|
453
|
+
underline = _UNDERLINES.get(operand[0])
|
|
454
|
+
if underline is None:
|
|
455
|
+
unsupported.add(opcode)
|
|
456
|
+
else:
|
|
457
|
+
properties = replace(properties, underline=underline)
|
|
458
|
+
elif opcode == 0x2A42:
|
|
459
|
+
if operand[0] == 0:
|
|
460
|
+
properties = replace(properties, color="auto")
|
|
461
|
+
elif operand[0] in _ICO_RGB:
|
|
462
|
+
properties = replace(properties, color=_ICO_RGB[operand[0]])
|
|
463
|
+
else:
|
|
464
|
+
unsupported.add(opcode)
|
|
465
|
+
elif opcode == 0x2A0C:
|
|
466
|
+
highlight = _ICO_HIGHLIGHT.get(operand[0])
|
|
467
|
+
if highlight is None:
|
|
468
|
+
unsupported.add(opcode)
|
|
469
|
+
else:
|
|
470
|
+
properties = replace(properties, highlight=highlight)
|
|
471
|
+
elif opcode == 0x4A43:
|
|
472
|
+
size = struct.unpack("<H", operand)[0]
|
|
473
|
+
if 2 <= size <= 3276:
|
|
474
|
+
properties = replace(properties, size_half_points=size)
|
|
475
|
+
else:
|
|
476
|
+
unsupported.add(opcode)
|
|
477
|
+
elif opcode == 0x4845:
|
|
478
|
+
position = struct.unpack("<h", operand)[0]
|
|
479
|
+
properties = replace(properties, position_half_points=position)
|
|
480
|
+
elif opcode == 0x2A48:
|
|
481
|
+
vertical_align = {0: "baseline", 1: "superscript", 2: "subscript"}.get(
|
|
482
|
+
operand[0]
|
|
483
|
+
)
|
|
484
|
+
if vertical_align is None:
|
|
485
|
+
unsupported.add(opcode)
|
|
486
|
+
else:
|
|
487
|
+
properties = replace(properties, vertical_align=vertical_align)
|
|
488
|
+
elif opcode == 0x6870:
|
|
489
|
+
if operand[3] == 0xFF:
|
|
490
|
+
properties = replace(properties, color="auto")
|
|
491
|
+
else:
|
|
492
|
+
properties = replace(
|
|
493
|
+
properties,
|
|
494
|
+
color=f"{operand[0]:02X}{operand[1]:02X}{operand[2]:02X}",
|
|
495
|
+
)
|
|
496
|
+
else:
|
|
497
|
+
unsupported.add(opcode)
|
|
498
|
+
return properties, unsupported, style_relative_toggle_count
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
_JUSTIFICATION = {
|
|
502
|
+
0: "left",
|
|
503
|
+
1: "center",
|
|
504
|
+
2: "right",
|
|
505
|
+
3: "both",
|
|
506
|
+
4: "distribute",
|
|
507
|
+
5: "mediumKashida",
|
|
508
|
+
7: "highKashida",
|
|
509
|
+
8: "lowKashida",
|
|
510
|
+
9: "thaiDistribute",
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def apply_paragraph_modifiers(
|
|
515
|
+
modifiers: tuple[PropertyModifier, ...],
|
|
516
|
+
*,
|
|
517
|
+
style_id: int | None,
|
|
518
|
+
initial_properties: ParagraphProperties | None = None,
|
|
519
|
+
) -> tuple[ParagraphProperties, set[int]]:
|
|
520
|
+
properties = initial_properties or ParagraphProperties(style_id=style_id)
|
|
521
|
+
unsupported: set[int] = set()
|
|
522
|
+
for modifier in modifiers:
|
|
523
|
+
opcode = modifier.opcode
|
|
524
|
+
operand = modifier.operand
|
|
525
|
+
if opcode == 0x4600:
|
|
526
|
+
properties = replace(
|
|
527
|
+
properties,
|
|
528
|
+
style_id=struct.unpack("<H", operand)[0],
|
|
529
|
+
)
|
|
530
|
+
elif opcode == 0x2416:
|
|
531
|
+
properties = replace(properties, in_table=bool(operand[0]))
|
|
532
|
+
elif opcode == 0x2417:
|
|
533
|
+
properties = replace(properties, table_terminating=bool(operand[0]))
|
|
534
|
+
elif opcode == 0x6649:
|
|
535
|
+
depth = struct.unpack("<i", operand)[0]
|
|
536
|
+
if depth < 0:
|
|
537
|
+
unsupported.add(opcode)
|
|
538
|
+
else:
|
|
539
|
+
properties = replace(properties, table_depth=depth)
|
|
540
|
+
elif opcode == 0x664A:
|
|
541
|
+
delta = struct.unpack("<i", operand)[0]
|
|
542
|
+
depth = properties.effective_table_depth + delta
|
|
543
|
+
if depth < 0:
|
|
544
|
+
unsupported.add(opcode)
|
|
545
|
+
else:
|
|
546
|
+
properties = replace(properties, table_depth=depth)
|
|
547
|
+
elif opcode == 0x244B:
|
|
548
|
+
properties = replace(properties, inner_table_cell=bool(operand[0]))
|
|
549
|
+
elif opcode == 0x244C:
|
|
550
|
+
properties = replace(properties, inner_table_row=bool(operand[0]))
|
|
551
|
+
elif opcode == 0x5400:
|
|
552
|
+
alignment = {0: "left", 1: "center", 2: "right"}.get(
|
|
553
|
+
struct.unpack("<H", operand)[0]
|
|
554
|
+
)
|
|
555
|
+
if alignment is None:
|
|
556
|
+
unsupported.add(opcode)
|
|
557
|
+
else:
|
|
558
|
+
row = properties.table_row or TableRowProperties()
|
|
559
|
+
properties = replace(
|
|
560
|
+
properties,
|
|
561
|
+
table_row=replace(row, alignment=alignment),
|
|
562
|
+
)
|
|
563
|
+
elif opcode == 0x9601:
|
|
564
|
+
row = properties.table_row or TableRowProperties()
|
|
565
|
+
properties = replace(
|
|
566
|
+
properties,
|
|
567
|
+
table_row=replace(
|
|
568
|
+
row,
|
|
569
|
+
left_indent_twips=struct.unpack("<h", operand)[0],
|
|
570
|
+
),
|
|
571
|
+
)
|
|
572
|
+
elif opcode == 0x9602:
|
|
573
|
+
gap = struct.unpack("<h", operand)[0]
|
|
574
|
+
if gap < 0:
|
|
575
|
+
unsupported.add(opcode)
|
|
576
|
+
else:
|
|
577
|
+
row = properties.table_row or TableRowProperties()
|
|
578
|
+
properties = replace(
|
|
579
|
+
properties,
|
|
580
|
+
table_row=replace(row, gap_half_twips=gap),
|
|
581
|
+
)
|
|
582
|
+
elif opcode in (0x3403, 0x3466):
|
|
583
|
+
row = properties.table_row or TableRowProperties()
|
|
584
|
+
properties = replace(
|
|
585
|
+
properties,
|
|
586
|
+
table_row=replace(row, cant_split=bool(operand[0])),
|
|
587
|
+
)
|
|
588
|
+
elif opcode == 0x3404:
|
|
589
|
+
row = properties.table_row or TableRowProperties()
|
|
590
|
+
properties = replace(
|
|
591
|
+
properties,
|
|
592
|
+
table_row=replace(row, is_header=bool(operand[0])),
|
|
593
|
+
)
|
|
594
|
+
elif opcode == 0x9407:
|
|
595
|
+
height = struct.unpack("<h", operand)[0]
|
|
596
|
+
row = properties.table_row or TableRowProperties()
|
|
597
|
+
properties = replace(
|
|
598
|
+
properties,
|
|
599
|
+
table_row=replace(
|
|
600
|
+
row,
|
|
601
|
+
height_twips=abs(height) if height else None,
|
|
602
|
+
height_rule="exact" if height < 0 else "atLeast",
|
|
603
|
+
),
|
|
604
|
+
)
|
|
605
|
+
elif opcode == 0xD605:
|
|
606
|
+
row = properties.table_row or TableRowProperties()
|
|
607
|
+
properties = replace(
|
|
608
|
+
properties,
|
|
609
|
+
table_row=replace(row, borders=_parse_table_borders80(operand)),
|
|
610
|
+
)
|
|
611
|
+
elif opcode == 0xD608:
|
|
612
|
+
definition = _parse_tdef_table(operand)
|
|
613
|
+
row = properties.table_row or TableRowProperties()
|
|
614
|
+
properties = replace(
|
|
615
|
+
properties,
|
|
616
|
+
table_row=replace(
|
|
617
|
+
row,
|
|
618
|
+
cell_boundaries_twips=definition.cell_boundaries_twips,
|
|
619
|
+
cell_definitions=definition.cell_definitions,
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
elif opcode == 0xD609:
|
|
623
|
+
shadings, approximated = _parse_def_table_shd80(operand)
|
|
624
|
+
row = properties.table_row or TableRowProperties()
|
|
625
|
+
properties = replace(
|
|
626
|
+
properties,
|
|
627
|
+
table_row=replace(row, cell_shadings=shadings),
|
|
628
|
+
)
|
|
629
|
+
if approximated:
|
|
630
|
+
unsupported.add(opcode)
|
|
631
|
+
elif opcode in (0xD632, 0xD634):
|
|
632
|
+
first_cell, limit_cell, sides, width = _parse_cssa(operand)
|
|
633
|
+
row = properties.table_row or TableRowProperties()
|
|
634
|
+
if opcode == 0xD634:
|
|
635
|
+
if (first_cell, limit_cell) != (0, 1):
|
|
636
|
+
unsupported.add(opcode)
|
|
637
|
+
else:
|
|
638
|
+
margins = replace(
|
|
639
|
+
row.default_cell_margins,
|
|
640
|
+
**{side: width for side in sides},
|
|
641
|
+
)
|
|
642
|
+
properties = replace(
|
|
643
|
+
properties,
|
|
644
|
+
table_row=replace(row, default_cell_margins=margins),
|
|
645
|
+
)
|
|
646
|
+
else:
|
|
647
|
+
override = TableCellMarginOverride(
|
|
648
|
+
first_cell,
|
|
649
|
+
limit_cell,
|
|
650
|
+
sides,
|
|
651
|
+
width,
|
|
652
|
+
)
|
|
653
|
+
properties = replace(
|
|
654
|
+
properties,
|
|
655
|
+
table_row=replace(
|
|
656
|
+
row,
|
|
657
|
+
cell_margin_overrides=(
|
|
658
|
+
*row.cell_margin_overrides,
|
|
659
|
+
override,
|
|
660
|
+
),
|
|
661
|
+
),
|
|
662
|
+
)
|
|
663
|
+
elif opcode in (0x2403, 0x2461):
|
|
664
|
+
justification = _JUSTIFICATION.get(operand[0])
|
|
665
|
+
if justification is None:
|
|
666
|
+
unsupported.add(opcode)
|
|
667
|
+
else:
|
|
668
|
+
properties = replace(properties, justification=justification)
|
|
669
|
+
elif opcode == 0x2405:
|
|
670
|
+
properties = replace(properties, keep_lines=bool(operand[0]))
|
|
671
|
+
elif opcode == 0x2406:
|
|
672
|
+
properties = replace(properties, keep_next=bool(operand[0]))
|
|
673
|
+
elif opcode == 0x2407:
|
|
674
|
+
properties = replace(properties, page_break_before=bool(operand[0]))
|
|
675
|
+
elif opcode in (0x840F, 0x845E):
|
|
676
|
+
properties = replace(
|
|
677
|
+
properties,
|
|
678
|
+
left_indent_twips=struct.unpack("<h", operand)[0],
|
|
679
|
+
)
|
|
680
|
+
elif opcode in (0x840E, 0x845D):
|
|
681
|
+
properties = replace(
|
|
682
|
+
properties,
|
|
683
|
+
right_indent_twips=struct.unpack("<h", operand)[0],
|
|
684
|
+
)
|
|
685
|
+
elif opcode in (0x8411, 0x8460):
|
|
686
|
+
properties = replace(
|
|
687
|
+
properties,
|
|
688
|
+
first_line_indent_twips=struct.unpack("<h", operand)[0],
|
|
689
|
+
)
|
|
690
|
+
elif opcode == 0xA413:
|
|
691
|
+
properties = replace(
|
|
692
|
+
properties,
|
|
693
|
+
space_before_twips=struct.unpack("<H", operand)[0],
|
|
694
|
+
)
|
|
695
|
+
elif opcode == 0xA414:
|
|
696
|
+
properties = replace(
|
|
697
|
+
properties,
|
|
698
|
+
space_after_twips=struct.unpack("<H", operand)[0],
|
|
699
|
+
)
|
|
700
|
+
elif opcode == 0x6412:
|
|
701
|
+
raw_line, multiple = struct.unpack("<HH", operand)
|
|
702
|
+
signed_line = struct.unpack("<h", operand[:2])[0]
|
|
703
|
+
if multiple == 1 and signed_line >= 0:
|
|
704
|
+
properties = replace(
|
|
705
|
+
properties,
|
|
706
|
+
line_spacing_twips=raw_line,
|
|
707
|
+
line_rule="auto",
|
|
708
|
+
)
|
|
709
|
+
elif multiple == 0 and signed_line < 0:
|
|
710
|
+
properties = replace(
|
|
711
|
+
properties,
|
|
712
|
+
line_spacing_twips=-signed_line,
|
|
713
|
+
line_rule="exact",
|
|
714
|
+
)
|
|
715
|
+
elif multiple == 0:
|
|
716
|
+
properties = replace(
|
|
717
|
+
properties,
|
|
718
|
+
line_spacing_twips=raw_line,
|
|
719
|
+
line_rule="atLeast",
|
|
720
|
+
)
|
|
721
|
+
else:
|
|
722
|
+
unsupported.add(opcode)
|
|
723
|
+
else:
|
|
724
|
+
unsupported.add(opcode)
|
|
725
|
+
return properties, unsupported
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def merge_character_properties(
|
|
729
|
+
base: CharacterProperties,
|
|
730
|
+
overlay: CharacterProperties,
|
|
731
|
+
) -> CharacterProperties:
|
|
732
|
+
"""Overlay only explicitly specified character-property values."""
|
|
733
|
+
|
|
734
|
+
updates = {
|
|
735
|
+
field_name: getattr(overlay, field_name)
|
|
736
|
+
for field_name in CharacterProperties.__dataclass_fields__
|
|
737
|
+
if getattr(overlay, field_name) is not None
|
|
738
|
+
}
|
|
739
|
+
return replace(base, **updates) if updates else base
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def merge_paragraph_properties(
|
|
743
|
+
base: ParagraphProperties,
|
|
744
|
+
overlay: ParagraphProperties,
|
|
745
|
+
) -> ParagraphProperties:
|
|
746
|
+
"""Overlay only explicitly specified paragraph-property values."""
|
|
747
|
+
|
|
748
|
+
updates = {
|
|
749
|
+
field_name: getattr(overlay, field_name)
|
|
750
|
+
for field_name in ParagraphProperties.__dataclass_fields__
|
|
751
|
+
if getattr(overlay, field_name) is not None
|
|
752
|
+
}
|
|
753
|
+
return replace(base, **updates) if updates else base
|