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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .document_properties import WordDocumentSettings, read_document_settings
|
|
2
|
+
from .fib import FileInformationBlock
|
|
3
|
+
from .fonts import read_font_table
|
|
4
|
+
from .formatting import FormattingMap, read_formatting
|
|
5
|
+
from .headers import HeaderFooterCollection, read_header_footer_stories
|
|
6
|
+
from .pieces import Piece, PieceTable, read_piece_table
|
|
7
|
+
from .sections import read_sections
|
|
8
|
+
from .styles import read_style_sheet
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"FileInformationBlock",
|
|
12
|
+
"FormattingMap",
|
|
13
|
+
"HeaderFooterCollection",
|
|
14
|
+
"Piece",
|
|
15
|
+
"PieceTable",
|
|
16
|
+
"WordDocumentSettings",
|
|
17
|
+
"read_document_settings",
|
|
18
|
+
"read_font_table",
|
|
19
|
+
"read_formatting",
|
|
20
|
+
"read_header_footer_stories",
|
|
21
|
+
"read_piece_table",
|
|
22
|
+
"read_sections",
|
|
23
|
+
"read_style_sheet",
|
|
24
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Minimal parsing of document-wide settings from the MS-DOC DOP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import struct
|
|
7
|
+
|
|
8
|
+
from ..errors import InvalidWordDocument
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True, frozen=True)
|
|
12
|
+
class WordDocumentSettings:
|
|
13
|
+
even_and_odd_headers: bool = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_document_settings(
|
|
17
|
+
table_stream: bytes,
|
|
18
|
+
*,
|
|
19
|
+
offset: int,
|
|
20
|
+
size: int,
|
|
21
|
+
) -> WordDocumentSettings:
|
|
22
|
+
if size == 0:
|
|
23
|
+
return WordDocumentSettings()
|
|
24
|
+
if offset < 0 or size < 0 or offset > len(table_stream) - size:
|
|
25
|
+
raise InvalidWordDocument(
|
|
26
|
+
f"DOP range [{offset}, {offset + size}) exceeds Table stream"
|
|
27
|
+
)
|
|
28
|
+
if size < 2:
|
|
29
|
+
raise InvalidWordDocument("DOP is truncated before DopBase flags")
|
|
30
|
+
flags = struct.unpack_from("<H", table_stream, offset)[0]
|
|
31
|
+
return WordDocumentSettings(even_and_odd_headers=bool(flags & 0x0001))
|
doc2docx/msdoc/fib.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""File Information Block parsing for Word 97-2003 binary documents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ..binary import BinaryReader
|
|
8
|
+
from ..errors import BinaryBoundsError, InvalidWordDocument
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FIB_IDENT = 0xA5EC
|
|
12
|
+
# FibRgFcLcb97 includes the deprecated fcPlcPad/lcbPlcPad pair after
|
|
13
|
+
# fcPlcfSed/lcbPlcfSed. Therefore fcClx/lcbClx is pair 33 (zero-based).
|
|
14
|
+
FCLCB97_STSHF_INDEX = 1
|
|
15
|
+
FCLCB97_PLCF_SED_INDEX = 6
|
|
16
|
+
FCLCB97_PLCF_HDD_INDEX = 11
|
|
17
|
+
FCLCB97_PLCF_BTE_CHPX_INDEX = 12
|
|
18
|
+
FCLCB97_PLCF_BTE_PAPX_INDEX = 13
|
|
19
|
+
FCLCB97_STTBF_FFN_INDEX = 15
|
|
20
|
+
FCLCB97_DOP_INDEX = 31
|
|
21
|
+
FCLCB97_CLX_INDEX = 33
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True, frozen=True)
|
|
25
|
+
class FibBase:
|
|
26
|
+
w_ident: int
|
|
27
|
+
n_fib: int
|
|
28
|
+
lid: int
|
|
29
|
+
flags: int
|
|
30
|
+
n_fib_back: int
|
|
31
|
+
l_key: int
|
|
32
|
+
environment: int
|
|
33
|
+
flags2: int
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_template(self) -> bool:
|
|
37
|
+
return bool(self.flags & 0x0001)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_complex(self) -> bool:
|
|
41
|
+
return bool(self.flags & 0x0004)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def has_pictures(self) -> bool:
|
|
45
|
+
return bool(self.flags & 0x0008)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_encrypted(self) -> bool:
|
|
49
|
+
return bool(self.flags & 0x0100)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def uses_1table(self) -> bool:
|
|
53
|
+
return bool(self.flags & 0x0200)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_write_reserved(self) -> bool:
|
|
57
|
+
return bool(self.flags & 0x0800)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_obfuscated(self) -> bool:
|
|
61
|
+
return bool(self.flags & 0x8000)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def table_stream_name(self) -> str:
|
|
65
|
+
return "1Table" if self.uses_1table else "0Table"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(slots=True, frozen=True)
|
|
69
|
+
class FcLcb:
|
|
70
|
+
fc: int
|
|
71
|
+
lcb: int
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(slots=True, frozen=True)
|
|
75
|
+
class FileInformationBlock:
|
|
76
|
+
base: FibBase
|
|
77
|
+
fib_rg_w: tuple[int, ...]
|
|
78
|
+
fib_rg_lw: tuple[int, ...]
|
|
79
|
+
fib_rg_fc_lcb: tuple[FcLcb, ...]
|
|
80
|
+
fib_rg_csw_new: tuple[int, ...]
|
|
81
|
+
fib_size: int
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def n_fib(self) -> int:
|
|
85
|
+
return self.fib_rg_csw_new[0] if self.fib_rg_csw_new else self.base.n_fib
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def ccp_text(self) -> int:
|
|
89
|
+
if len(self.fib_rg_lw) <= 3:
|
|
90
|
+
raise InvalidWordDocument("FIB does not contain FibRgLw97.ccpText")
|
|
91
|
+
return self.fib_rg_lw[3]
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def ccp_footnotes(self) -> int:
|
|
95
|
+
return self.fib_rg_lw[4] if len(self.fib_rg_lw) > 4 else 0
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def ccp_headers(self) -> int:
|
|
99
|
+
return self.fib_rg_lw[5] if len(self.fib_rg_lw) > 5 else 0
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def header_story_cp_start(self) -> int:
|
|
103
|
+
return self.ccp_text + self.ccp_footnotes
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def cb_mac(self) -> int:
|
|
107
|
+
if not self.fib_rg_lw:
|
|
108
|
+
raise InvalidWordDocument("FIB does not contain FibRgLw97.cbMac")
|
|
109
|
+
return self.fib_rg_lw[0]
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def secondary_story_character_counts(self) -> dict[str, int]:
|
|
113
|
+
names_and_indexes = (
|
|
114
|
+
("footnotes", 4),
|
|
115
|
+
("headers", 5),
|
|
116
|
+
("comments", 7),
|
|
117
|
+
("endnotes", 8),
|
|
118
|
+
("textboxes", 9),
|
|
119
|
+
("header_textboxes", 10),
|
|
120
|
+
)
|
|
121
|
+
return {
|
|
122
|
+
name: self.fib_rg_lw[index]
|
|
123
|
+
for name, index in names_and_indexes
|
|
124
|
+
if index < len(self.fib_rg_lw) and self.fib_rg_lw[index]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def clx(self) -> FcLcb:
|
|
129
|
+
if len(self.fib_rg_fc_lcb) <= FCLCB97_CLX_INDEX:
|
|
130
|
+
raise InvalidWordDocument("FIB does not contain fcClx/lcbClx")
|
|
131
|
+
return self.fib_rg_fc_lcb[FCLCB97_CLX_INDEX]
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def stshf(self) -> FcLcb:
|
|
135
|
+
if len(self.fib_rg_fc_lcb) <= FCLCB97_STSHF_INDEX:
|
|
136
|
+
return FcLcb(0, 0)
|
|
137
|
+
return self.fib_rg_fc_lcb[FCLCB97_STSHF_INDEX]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def plcf_sed(self) -> FcLcb:
|
|
141
|
+
if len(self.fib_rg_fc_lcb) <= FCLCB97_PLCF_SED_INDEX:
|
|
142
|
+
return FcLcb(0, 0)
|
|
143
|
+
return self.fib_rg_fc_lcb[FCLCB97_PLCF_SED_INDEX]
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def plcf_hdd(self) -> FcLcb:
|
|
147
|
+
if len(self.fib_rg_fc_lcb) <= FCLCB97_PLCF_HDD_INDEX:
|
|
148
|
+
return FcLcb(0, 0)
|
|
149
|
+
return self.fib_rg_fc_lcb[FCLCB97_PLCF_HDD_INDEX]
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def dop(self) -> FcLcb:
|
|
153
|
+
if len(self.fib_rg_fc_lcb) <= FCLCB97_DOP_INDEX:
|
|
154
|
+
return FcLcb(0, 0)
|
|
155
|
+
return self.fib_rg_fc_lcb[FCLCB97_DOP_INDEX]
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def sttbf_ffn(self) -> FcLcb:
|
|
159
|
+
if len(self.fib_rg_fc_lcb) <= FCLCB97_STTBF_FFN_INDEX:
|
|
160
|
+
return FcLcb(0, 0)
|
|
161
|
+
return self.fib_rg_fc_lcb[FCLCB97_STTBF_FFN_INDEX]
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def plcf_bte_chpx(self) -> FcLcb:
|
|
165
|
+
if len(self.fib_rg_fc_lcb) <= FCLCB97_PLCF_BTE_CHPX_INDEX:
|
|
166
|
+
return FcLcb(0, 0)
|
|
167
|
+
return self.fib_rg_fc_lcb[FCLCB97_PLCF_BTE_CHPX_INDEX]
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def plcf_bte_papx(self) -> FcLcb:
|
|
171
|
+
if len(self.fib_rg_fc_lcb) <= FCLCB97_PLCF_BTE_PAPX_INDEX:
|
|
172
|
+
return FcLcb(0, 0)
|
|
173
|
+
return self.fib_rg_fc_lcb[FCLCB97_PLCF_BTE_PAPX_INDEX]
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def parse(cls, word_document_stream: bytes) -> "FileInformationBlock":
|
|
177
|
+
reader = BinaryReader(word_document_stream, label="WordDocument FIB")
|
|
178
|
+
try:
|
|
179
|
+
w_ident = reader.u16()
|
|
180
|
+
n_fib = reader.u16()
|
|
181
|
+
reader.skip(2) # unused
|
|
182
|
+
lid = reader.u16()
|
|
183
|
+
reader.skip(2) # pnNext
|
|
184
|
+
flags = reader.u16()
|
|
185
|
+
n_fib_back = reader.u16()
|
|
186
|
+
l_key = reader.u32()
|
|
187
|
+
environment = reader.u8()
|
|
188
|
+
flags2 = reader.u8()
|
|
189
|
+
reader.skip(12) # reserved3 through reserved6
|
|
190
|
+
|
|
191
|
+
if w_ident != FIB_IDENT:
|
|
192
|
+
raise InvalidWordDocument(
|
|
193
|
+
f"WordDocument FIB has wIdent 0x{w_ident:04X}; expected 0xA5EC"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
csw = reader.u16()
|
|
197
|
+
fib_rg_w = tuple(reader.u16() for _ in range(csw))
|
|
198
|
+
|
|
199
|
+
cslw = reader.u16()
|
|
200
|
+
fib_rg_lw = tuple(reader.u32() for _ in range(cslw))
|
|
201
|
+
|
|
202
|
+
cb_rg_fc_lcb = reader.u16()
|
|
203
|
+
fib_rg_fc_lcb = tuple(
|
|
204
|
+
FcLcb(reader.u32(), reader.u32()) for _ in range(cb_rg_fc_lcb)
|
|
205
|
+
)
|
|
206
|
+
if reader.remaining >= 2:
|
|
207
|
+
csw_new = reader.u16()
|
|
208
|
+
fib_rg_csw_new = tuple(reader.u16() for _ in range(csw_new))
|
|
209
|
+
else:
|
|
210
|
+
fib_rg_csw_new = ()
|
|
211
|
+
except BinaryBoundsError as exc:
|
|
212
|
+
raise InvalidWordDocument(f"truncated or invalid FIB: {exc}") from exc
|
|
213
|
+
|
|
214
|
+
if csw < 14:
|
|
215
|
+
raise InvalidWordDocument(
|
|
216
|
+
f"FIB FibRgW has {csw} words; Word 97 layout requires at least 14"
|
|
217
|
+
)
|
|
218
|
+
if cslw < 11:
|
|
219
|
+
raise InvalidWordDocument(
|
|
220
|
+
f"FIB FibRgLw has {cslw} values; expected at least 11"
|
|
221
|
+
)
|
|
222
|
+
if cb_rg_fc_lcb <= FCLCB97_CLX_INDEX:
|
|
223
|
+
raise InvalidWordDocument(
|
|
224
|
+
f"FIB has {cb_rg_fc_lcb} fc/lcb pairs and no CLX entry"
|
|
225
|
+
)
|
|
226
|
+
ccp_text = fib_rg_lw[3]
|
|
227
|
+
if ccp_text >= 0x7FFFFFFF:
|
|
228
|
+
raise InvalidWordDocument(f"invalid main-story character count {ccp_text}")
|
|
229
|
+
|
|
230
|
+
return cls(
|
|
231
|
+
base=FibBase(
|
|
232
|
+
w_ident,
|
|
233
|
+
n_fib,
|
|
234
|
+
lid,
|
|
235
|
+
flags,
|
|
236
|
+
n_fib_back,
|
|
237
|
+
l_key,
|
|
238
|
+
environment,
|
|
239
|
+
flags2,
|
|
240
|
+
),
|
|
241
|
+
fib_rg_w=fib_rg_w,
|
|
242
|
+
fib_rg_lw=fib_rg_lw,
|
|
243
|
+
fib_rg_fc_lcb=fib_rg_fc_lcb,
|
|
244
|
+
fib_rg_csw_new=fib_rg_csw_new,
|
|
245
|
+
fib_size=reader.position,
|
|
246
|
+
)
|
doc2docx/msdoc/fonts.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""MS-DOC SttbfFfn and FFN font-table parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
from ..errors import InvalidWordDocument
|
|
8
|
+
from ..model import FontDefinition
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_FONT_FAMILIES = {
|
|
12
|
+
0: "auto",
|
|
13
|
+
1: "roman",
|
|
14
|
+
2: "swiss",
|
|
15
|
+
3: "modern",
|
|
16
|
+
4: "script",
|
|
17
|
+
5: "decorative",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_FONT_PITCHES = {
|
|
21
|
+
0: "default",
|
|
22
|
+
1: "fixed",
|
|
23
|
+
2: "variable",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _decode_xsz_ffn(data: bytes, *, label: str) -> tuple[str, ...]:
|
|
28
|
+
if len(data) % 2:
|
|
29
|
+
raise InvalidWordDocument(f"{label} font names have an odd byte count")
|
|
30
|
+
code_units = struct.unpack(f"<{len(data) // 2}H", data)
|
|
31
|
+
names: list[str] = []
|
|
32
|
+
start = 0
|
|
33
|
+
for index, value in enumerate(code_units):
|
|
34
|
+
if value != 0:
|
|
35
|
+
continue
|
|
36
|
+
raw_name = data[start * 2 : index * 2]
|
|
37
|
+
names.append(raw_name.decode("utf-16le", errors="replace"))
|
|
38
|
+
start = index + 1
|
|
39
|
+
if start != len(code_units):
|
|
40
|
+
raise InvalidWordDocument(f"{label} font name is not null-terminated")
|
|
41
|
+
return tuple(names)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_ffn(index: int, data: bytes) -> FontDefinition:
|
|
45
|
+
# ffid(1), wWeight(2), chs(1), ixchSzAlt(1), PANOSE(10), FONTSIGNATURE(24)
|
|
46
|
+
if len(data) < 41:
|
|
47
|
+
raise InvalidWordDocument(
|
|
48
|
+
f"SttbfFfn font {index} has {len(data)} bytes; expected at least 41"
|
|
49
|
+
)
|
|
50
|
+
ffid = data[0]
|
|
51
|
+
weight = struct.unpack_from("<h", data, 1)[0]
|
|
52
|
+
charset = data[3]
|
|
53
|
+
alternate_index = data[4]
|
|
54
|
+
panose = data[5:15]
|
|
55
|
+
signature = data[15:39]
|
|
56
|
+
names = _decode_xsz_ffn(data[39:], label=f"SttbfFfn font {index}")
|
|
57
|
+
if not names or not names[0]:
|
|
58
|
+
raise InvalidWordDocument(f"SttbfFfn font {index} has no primary name")
|
|
59
|
+
|
|
60
|
+
alternate_name: str | None = None
|
|
61
|
+
if alternate_index:
|
|
62
|
+
# ixchSzAlt is an index in UTF-16 code units from xszFfn. Real-world
|
|
63
|
+
# files normally place the alternate name immediately after the first
|
|
64
|
+
# terminator; prefer the indexed value but tolerate that common layout.
|
|
65
|
+
name_data = data[39:]
|
|
66
|
+
byte_offset = alternate_index * 2
|
|
67
|
+
if byte_offset < len(name_data):
|
|
68
|
+
tail = _decode_xsz_ffn(
|
|
69
|
+
name_data[byte_offset:],
|
|
70
|
+
label=f"SttbfFfn font {index} alternate name",
|
|
71
|
+
)
|
|
72
|
+
alternate_name = tail[0] if tail and tail[0] else None
|
|
73
|
+
elif len(names) > 1:
|
|
74
|
+
alternate_name = names[1] or None
|
|
75
|
+
elif len(names) > 1:
|
|
76
|
+
alternate_name = names[1] or None
|
|
77
|
+
|
|
78
|
+
return FontDefinition(
|
|
79
|
+
index=index,
|
|
80
|
+
name=names[0],
|
|
81
|
+
alternate_name=alternate_name,
|
|
82
|
+
charset=charset,
|
|
83
|
+
family=_FONT_FAMILIES.get((ffid >> 4) & 0x07),
|
|
84
|
+
pitch=_FONT_PITCHES.get(ffid & 0x03),
|
|
85
|
+
weight=weight if weight > 0 else None,
|
|
86
|
+
panose=panose,
|
|
87
|
+
signature=signature,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def read_font_table(
|
|
92
|
+
table_stream: bytes,
|
|
93
|
+
*,
|
|
94
|
+
offset: int,
|
|
95
|
+
size: int,
|
|
96
|
+
) -> tuple[FontDefinition, ...]:
|
|
97
|
+
"""Parse the non-extended STTB used by SttbfFfn."""
|
|
98
|
+
|
|
99
|
+
if size == 0:
|
|
100
|
+
return ()
|
|
101
|
+
if offset < 0 or size < 0 or offset > len(table_stream) - size:
|
|
102
|
+
raise InvalidWordDocument(
|
|
103
|
+
f"SttbfFfn range [{offset}, {offset + size}) exceeds Table stream"
|
|
104
|
+
)
|
|
105
|
+
data = table_stream[offset : offset + size]
|
|
106
|
+
if len(data) < 4:
|
|
107
|
+
raise InvalidWordDocument("SttbfFfn is truncated")
|
|
108
|
+
count, extra_size = struct.unpack_from("<HH", data)
|
|
109
|
+
if count == 0xFFFF:
|
|
110
|
+
raise InvalidWordDocument("extended SttbfFfn layout is not valid for MS-DOC")
|
|
111
|
+
if extra_size:
|
|
112
|
+
raise InvalidWordDocument(
|
|
113
|
+
f"SttbfFfn has unsupported cbExtra value {extra_size}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
fonts: list[FontDefinition] = []
|
|
117
|
+
position = 4
|
|
118
|
+
for index in range(count):
|
|
119
|
+
if position >= len(data):
|
|
120
|
+
raise InvalidWordDocument(f"SttbfFfn is truncated before font {index}")
|
|
121
|
+
byte_count = data[position]
|
|
122
|
+
position += 1
|
|
123
|
+
end = position + byte_count
|
|
124
|
+
if end > len(data):
|
|
125
|
+
raise InvalidWordDocument(f"SttbfFfn font {index} exceeds the table")
|
|
126
|
+
fonts.append(_parse_ffn(index, data[position:end]))
|
|
127
|
+
position = end
|
|
128
|
+
if position != len(data) and any(data[position:]):
|
|
129
|
+
raise InvalidWordDocument(
|
|
130
|
+
f"SttbfFfn has {len(data) - position} unexpected trailing bytes"
|
|
131
|
+
)
|
|
132
|
+
return tuple(fonts)
|