python-docx-oss 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.
- docx/__init__.py +71 -0
- docx/api.py +37 -0
- docx/blkcntnr.py +101 -0
- docx/comments.py +163 -0
- docx/dml/__init__.py +0 -0
- docx/dml/color.py +112 -0
- docx/document.py +285 -0
- docx/drawing/__init__.py +59 -0
- docx/enum/__init__.py +0 -0
- docx/enum/base.py +150 -0
- docx/enum/dml.py +103 -0
- docx/enum/section.py +86 -0
- docx/enum/shape.py +19 -0
- docx/enum/style.py +452 -0
- docx/enum/table.py +136 -0
- docx/enum/text.py +367 -0
- docx/exceptions.py +18 -0
- docx/image/__init__.py +32 -0
- docx/image/bmp.py +43 -0
- docx/image/constants.py +172 -0
- docx/image/emf.py +46 -0
- docx/image/exceptions.py +13 -0
- docx/image/gif.py +38 -0
- docx/image/helpers.py +86 -0
- docx/image/image.py +237 -0
- docx/image/jpeg.py +469 -0
- docx/image/png.py +253 -0
- docx/image/svg.py +118 -0
- docx/image/tiff.py +289 -0
- docx/opc/__init__.py +0 -0
- docx/opc/constants.py +307 -0
- docx/opc/coreprops.py +142 -0
- docx/opc/customprops.py +59 -0
- docx/opc/exceptions.py +12 -0
- docx/opc/oxml.py +247 -0
- docx/opc/package.py +254 -0
- docx/opc/packuri.py +109 -0
- docx/opc/part.py +267 -0
- docx/opc/parts/__init__.py +0 -0
- docx/opc/parts/coreprops.py +48 -0
- docx/opc/parts/customprops.py +61 -0
- docx/opc/phys_pkg.py +119 -0
- docx/opc/pkgreader.py +254 -0
- docx/opc/pkgwriter.py +115 -0
- docx/opc/rel.py +169 -0
- docx/opc/shared.py +31 -0
- docx/opc/spec.py +24 -0
- docx/oxml/__init__.py +252 -0
- docx/oxml/comments.py +124 -0
- docx/oxml/coreprops.py +298 -0
- docx/oxml/customprops.py +141 -0
- docx/oxml/dml/CT_ExtensionList.py +48 -0
- docx/oxml/document.py +88 -0
- docx/oxml/drawing.py +11 -0
- docx/oxml/exceptions.py +10 -0
- docx/oxml/ns.py +111 -0
- docx/oxml/numbering.py +109 -0
- docx/oxml/parser.py +62 -0
- docx/oxml/section.py +537 -0
- docx/oxml/settings.py +138 -0
- docx/oxml/shape.py +426 -0
- docx/oxml/shared.py +52 -0
- docx/oxml/simpletypes.py +434 -0
- docx/oxml/styles.py +320 -0
- docx/oxml/table.py +977 -0
- docx/oxml/text/__init__.py +0 -0
- docx/oxml/text/font.py +358 -0
- docx/oxml/text/hyperlink.py +45 -0
- docx/oxml/text/pagebreak.py +278 -0
- docx/oxml/text/paragraph.py +106 -0
- docx/oxml/text/parfmt.py +392 -0
- docx/oxml/text/run.py +316 -0
- docx/oxml/xmlchemy.py +711 -0
- docx/package.py +119 -0
- docx/parts/__init__.py +0 -0
- docx/parts/comments.py +51 -0
- docx/parts/customxml.py +67 -0
- docx/parts/document.py +193 -0
- docx/parts/hdrftr.py +53 -0
- docx/parts/image.py +80 -0
- docx/parts/numbering.py +51 -0
- docx/parts/settings.py +50 -0
- docx/parts/story.py +113 -0
- docx/parts/styles.py +42 -0
- docx/py.typed +0 -0
- docx/section.py +479 -0
- docx/settings.py +35 -0
- docx/shape.py +103 -0
- docx/shared.py +382 -0
- docx/styles/__init__.py +40 -0
- docx/styles/latent.py +198 -0
- docx/styles/style.py +254 -0
- docx/styles/styles.py +136 -0
- docx/table.py +537 -0
- docx/templates/default-comments.xml +12 -0
- docx/templates/default-docx-template/[Content_Types].xml +17 -0
- docx/templates/default-docx-template/_rels/.rels +7 -0
- docx/templates/default-docx-template/customXml/_rels/item1.xml.rels +4 -0
- docx/templates/default-docx-template/customXml/item1.xml +2 -0
- docx/templates/default-docx-template/customXml/itemProps1.xml +6 -0
- docx/templates/default-docx-template/docProps/app.xml +36 -0
- docx/templates/default-docx-template/docProps/core.xml +13 -0
- docx/templates/default-docx-template/docProps/thumbnail.jpeg +0 -0
- docx/templates/default-docx-template/word/_rels/document.xml.rels +11 -0
- docx/templates/default-docx-template/word/document.xml +11 -0
- docx/templates/default-docx-template/word/fontTable.xml +61 -0
- docx/templates/default-docx-template/word/numbering.xml +201 -0
- docx/templates/default-docx-template/word/settings.xml +53 -0
- docx/templates/default-docx-template/word/styles.xml +11844 -0
- docx/templates/default-docx-template/word/stylesWithEffects.xml +11800 -0
- docx/templates/default-docx-template/word/theme/theme1.xml +318 -0
- docx/templates/default-docx-template/word/webSettings.xml +5 -0
- docx/templates/default-footer.xml +27 -0
- docx/templates/default-header.xml +27 -0
- docx/templates/default-numbering.xml +20 -0
- docx/templates/default-settings.xml +26 -0
- docx/templates/default-styles.xml +190 -0
- docx/templates/default.docx +0 -0
- docx/text/__init__.py +0 -0
- docx/text/font.py +466 -0
- docx/text/hyperlink.py +121 -0
- docx/text/pagebreak.py +104 -0
- docx/text/paragraph.py +173 -0
- docx/text/parfmt.py +286 -0
- docx/text/run.py +272 -0
- docx/text/tabstops.py +123 -0
- docx/types.py +34 -0
- python_docx_oss-0.1.0.dist-info/METADATA +57 -0
- python_docx_oss-0.1.0.dist-info/RECORD +132 -0
- python_docx_oss-0.1.0.dist-info/WHEEL +5 -0
- python_docx_oss-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_docx_oss-0.1.0.dist-info/top_level.txt +1 -0
docx/image/png.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from .constants import MIME_TYPE, PNG_CHUNK_TYPE
|
|
2
|
+
from .exceptions import InvalidImageStreamError
|
|
3
|
+
from .helpers import BIG_ENDIAN, StreamReader
|
|
4
|
+
from .image import BaseImageHeader
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Png(BaseImageHeader):
|
|
8
|
+
"""Image header parser for PNG images."""
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def content_type(self):
|
|
12
|
+
"""MIME content type for this image, unconditionally `image/png` for PNG
|
|
13
|
+
images."""
|
|
14
|
+
return MIME_TYPE.PNG
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def default_ext(self):
|
|
18
|
+
"""Default filename extension, always 'png' for PNG images."""
|
|
19
|
+
return "png"
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_stream(cls, stream):
|
|
23
|
+
"""Return a |Png| instance having header properties parsed from image in
|
|
24
|
+
`stream`."""
|
|
25
|
+
parser = _PngParser.parse(stream)
|
|
26
|
+
|
|
27
|
+
px_width = parser.px_width
|
|
28
|
+
px_height = parser.px_height
|
|
29
|
+
horz_dpi = parser.horz_dpi
|
|
30
|
+
vert_dpi = parser.vert_dpi
|
|
31
|
+
|
|
32
|
+
return cls(px_width, px_height, horz_dpi, vert_dpi)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _PngParser:
|
|
36
|
+
"""Parses a PNG image stream to extract the image properties found in its chunks."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, chunks):
|
|
39
|
+
super(_PngParser, self).__init__()
|
|
40
|
+
self._chunks = chunks
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def parse(cls, stream):
|
|
44
|
+
"""Return a |_PngParser| instance containing the header properties parsed from
|
|
45
|
+
the PNG image in `stream`."""
|
|
46
|
+
chunks = _Chunks.from_stream(stream)
|
|
47
|
+
return cls(chunks)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def px_width(self):
|
|
51
|
+
"""The number of pixels in each row of the image."""
|
|
52
|
+
IHDR = self._chunks.IHDR
|
|
53
|
+
return IHDR.px_width
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def px_height(self):
|
|
57
|
+
"""The number of stacked rows of pixels in the image."""
|
|
58
|
+
IHDR = self._chunks.IHDR
|
|
59
|
+
return IHDR.px_height
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def horz_dpi(self):
|
|
63
|
+
"""Integer dots per inch for the width of this image.
|
|
64
|
+
|
|
65
|
+
Defaults to 72 when not present in the file, as is often the case.
|
|
66
|
+
"""
|
|
67
|
+
pHYs = self._chunks.pHYs
|
|
68
|
+
if pHYs is None:
|
|
69
|
+
return 72
|
|
70
|
+
return self._dpi(pHYs.units_specifier, pHYs.horz_px_per_unit)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def vert_dpi(self):
|
|
74
|
+
"""Integer dots per inch for the height of this image.
|
|
75
|
+
|
|
76
|
+
Defaults to 72 when not present in the file, as is often the case.
|
|
77
|
+
"""
|
|
78
|
+
pHYs = self._chunks.pHYs
|
|
79
|
+
if pHYs is None:
|
|
80
|
+
return 72
|
|
81
|
+
return self._dpi(pHYs.units_specifier, pHYs.vert_px_per_unit)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _dpi(units_specifier, px_per_unit):
|
|
85
|
+
"""Return dots per inch value calculated from `units_specifier` and
|
|
86
|
+
`px_per_unit`."""
|
|
87
|
+
if units_specifier == 1 and px_per_unit:
|
|
88
|
+
return int(round(px_per_unit * 0.0254))
|
|
89
|
+
return 72
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _Chunks:
|
|
93
|
+
"""Collection of the chunks parsed from a PNG image stream."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, chunk_iterable):
|
|
96
|
+
super(_Chunks, self).__init__()
|
|
97
|
+
self._chunks = list(chunk_iterable)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_stream(cls, stream):
|
|
101
|
+
"""Return a |_Chunks| instance containing the PNG chunks in `stream`."""
|
|
102
|
+
chunk_parser = _ChunkParser.from_stream(stream)
|
|
103
|
+
chunks = list(chunk_parser.iter_chunks())
|
|
104
|
+
return cls(chunks)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def IHDR(self):
|
|
108
|
+
"""IHDR chunk in PNG image."""
|
|
109
|
+
match = lambda chunk: chunk.type_name == PNG_CHUNK_TYPE.IHDR # noqa
|
|
110
|
+
IHDR = self._find_first(match)
|
|
111
|
+
if IHDR is None:
|
|
112
|
+
raise InvalidImageStreamError("no IHDR chunk in PNG image")
|
|
113
|
+
return IHDR
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def pHYs(self):
|
|
117
|
+
"""PHYs chunk in PNG image, or |None| if not present."""
|
|
118
|
+
match = lambda chunk: chunk.type_name == PNG_CHUNK_TYPE.pHYs # noqa
|
|
119
|
+
return self._find_first(match)
|
|
120
|
+
|
|
121
|
+
def _find_first(self, match):
|
|
122
|
+
"""Return first chunk in stream order returning True for function `match`."""
|
|
123
|
+
for chunk in self._chunks:
|
|
124
|
+
if match(chunk):
|
|
125
|
+
return chunk
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _ChunkParser:
|
|
130
|
+
"""Extracts chunks from a PNG image stream."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, stream_rdr):
|
|
133
|
+
super(_ChunkParser, self).__init__()
|
|
134
|
+
self._stream_rdr = stream_rdr
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_stream(cls, stream):
|
|
138
|
+
"""Return a |_ChunkParser| instance that can extract the chunks from the PNG
|
|
139
|
+
image in `stream`."""
|
|
140
|
+
stream_rdr = StreamReader(stream, BIG_ENDIAN)
|
|
141
|
+
return cls(stream_rdr)
|
|
142
|
+
|
|
143
|
+
def iter_chunks(self):
|
|
144
|
+
"""Generate a |_Chunk| subclass instance for each chunk in this parser's PNG
|
|
145
|
+
stream, in the order encountered in the stream."""
|
|
146
|
+
for chunk_type, offset in self._iter_chunk_offsets():
|
|
147
|
+
chunk = _ChunkFactory(chunk_type, self._stream_rdr, offset)
|
|
148
|
+
yield chunk
|
|
149
|
+
|
|
150
|
+
def _iter_chunk_offsets(self):
|
|
151
|
+
"""Generate a (chunk_type, chunk_offset) 2-tuple for each of the chunks in the
|
|
152
|
+
PNG image stream.
|
|
153
|
+
|
|
154
|
+
Iteration stops after the IEND chunk is returned.
|
|
155
|
+
"""
|
|
156
|
+
chunk_offset = 8
|
|
157
|
+
while True:
|
|
158
|
+
chunk_data_len = self._stream_rdr.read_long(chunk_offset)
|
|
159
|
+
chunk_type = self._stream_rdr.read_str(4, chunk_offset, 4)
|
|
160
|
+
data_offset = chunk_offset + 8
|
|
161
|
+
yield chunk_type, data_offset
|
|
162
|
+
if chunk_type == "IEND":
|
|
163
|
+
break
|
|
164
|
+
# incr offset for chunk len long, chunk type, chunk data, and CRC
|
|
165
|
+
chunk_offset += 4 + 4 + chunk_data_len + 4
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _ChunkFactory(chunk_type, stream_rdr, offset):
|
|
169
|
+
"""Return a |_Chunk| subclass instance appropriate to `chunk_type` parsed from
|
|
170
|
+
`stream_rdr` at `offset`."""
|
|
171
|
+
chunk_cls_map = {
|
|
172
|
+
PNG_CHUNK_TYPE.IHDR: _IHDRChunk,
|
|
173
|
+
PNG_CHUNK_TYPE.pHYs: _pHYsChunk,
|
|
174
|
+
}
|
|
175
|
+
chunk_cls = chunk_cls_map.get(chunk_type, _Chunk)
|
|
176
|
+
return chunk_cls.from_offset(chunk_type, stream_rdr, offset)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class _Chunk:
|
|
180
|
+
"""Base class for specific chunk types.
|
|
181
|
+
|
|
182
|
+
Also serves as the default chunk type.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(self, chunk_type):
|
|
186
|
+
super(_Chunk, self).__init__()
|
|
187
|
+
self._chunk_type = chunk_type
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def from_offset(cls, chunk_type, stream_rdr, offset):
|
|
191
|
+
"""Return a default _Chunk instance that only knows its chunk type."""
|
|
192
|
+
return cls(chunk_type)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def type_name(self):
|
|
196
|
+
"""The chunk type name, e.g. 'IHDR', 'pHYs', etc."""
|
|
197
|
+
return self._chunk_type
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class _IHDRChunk(_Chunk):
|
|
201
|
+
"""IHDR chunk, contains the image dimensions."""
|
|
202
|
+
|
|
203
|
+
def __init__(self, chunk_type, px_width, px_height):
|
|
204
|
+
super(_IHDRChunk, self).__init__(chunk_type)
|
|
205
|
+
self._px_width = px_width
|
|
206
|
+
self._px_height = px_height
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def from_offset(cls, chunk_type, stream_rdr, offset):
|
|
210
|
+
"""Return an _IHDRChunk instance containing the image dimensions extracted from
|
|
211
|
+
the IHDR chunk in `stream` at `offset`."""
|
|
212
|
+
px_width = stream_rdr.read_long(offset)
|
|
213
|
+
px_height = stream_rdr.read_long(offset, 4)
|
|
214
|
+
return cls(chunk_type, px_width, px_height)
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def px_width(self):
|
|
218
|
+
return self._px_width
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def px_height(self):
|
|
222
|
+
return self._px_height
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class _pHYsChunk(_Chunk):
|
|
226
|
+
"""PYHs chunk, contains the image dpi information."""
|
|
227
|
+
|
|
228
|
+
def __init__(self, chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifier):
|
|
229
|
+
super(_pHYsChunk, self).__init__(chunk_type)
|
|
230
|
+
self._horz_px_per_unit = horz_px_per_unit
|
|
231
|
+
self._vert_px_per_unit = vert_px_per_unit
|
|
232
|
+
self._units_specifier = units_specifier
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def from_offset(cls, chunk_type, stream_rdr, offset):
|
|
236
|
+
"""Return a _pHYsChunk instance containing the image resolution extracted from
|
|
237
|
+
the pHYs chunk in `stream` at `offset`."""
|
|
238
|
+
horz_px_per_unit = stream_rdr.read_long(offset)
|
|
239
|
+
vert_px_per_unit = stream_rdr.read_long(offset, 4)
|
|
240
|
+
units_specifier = stream_rdr.read_byte(offset, 8)
|
|
241
|
+
return cls(chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifier)
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def horz_px_per_unit(self):
|
|
245
|
+
return self._horz_px_per_unit
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def vert_px_per_unit(self):
|
|
249
|
+
return self._vert_px_per_unit
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def units_specifier(self):
|
|
253
|
+
return self._units_specifier
|
docx/image/svg.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
|
|
4
|
+
from .constants import MIME_TYPE
|
|
5
|
+
from .image import BaseImageHeader
|
|
6
|
+
|
|
7
|
+
BASE_PX = 72
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Svg(BaseImageHeader):
|
|
11
|
+
"""
|
|
12
|
+
Image header parser for SVG images.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_stream(cls, stream):
|
|
17
|
+
"""
|
|
18
|
+
Return |Svg| instance having header properties parsed from SVG image
|
|
19
|
+
in *stream*.
|
|
20
|
+
"""
|
|
21
|
+
px_width, px_height = cls._dimensions_from_stream(stream)
|
|
22
|
+
return cls(px_width, px_height, BASE_PX, BASE_PX)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def content_type(self):
|
|
26
|
+
"""
|
|
27
|
+
MIME content type for this image, unconditionally `image/svg+xml` for
|
|
28
|
+
SVG images.
|
|
29
|
+
"""
|
|
30
|
+
return MIME_TYPE.SVG
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def default_ext(self):
|
|
34
|
+
"""
|
|
35
|
+
Default filename extension, always 'svg' for SVG images.
|
|
36
|
+
"""
|
|
37
|
+
return "svg"
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def _parse_svg_unit_value(cls, value_str: str) -> float:
|
|
41
|
+
"""
|
|
42
|
+
Parse an SVG length value and return it in points (which are 1/72 of an inch).
|
|
43
|
+
Handles units like px, pt, cm, mm, in, pc.
|
|
44
|
+
"""
|
|
45
|
+
if not value_str:
|
|
46
|
+
return 0.0
|
|
47
|
+
|
|
48
|
+
value_str = value_str.strip().lower()
|
|
49
|
+
|
|
50
|
+
# Regex to separate the numeric value from the unit, allowing for spaces
|
|
51
|
+
match = re.match(r"^(-?\d*\.?\d+)\s*([a-z%]*)$", value_str)
|
|
52
|
+
if not match:
|
|
53
|
+
# Handle case where there is no unit (treat as px, as per SVG spec)
|
|
54
|
+
try:
|
|
55
|
+
return float(value_str)
|
|
56
|
+
except ValueError:
|
|
57
|
+
return 0.0
|
|
58
|
+
|
|
59
|
+
value, unit = match.groups()
|
|
60
|
+
numeric_value = float(value)
|
|
61
|
+
|
|
62
|
+
# ---conversion factors to points (assuming 72 DPI, where 1pt = 1px)---
|
|
63
|
+
# This matches the existing behavior of the Svg class.
|
|
64
|
+
# 1 inch = BASE_PX points
|
|
65
|
+
# 1 cm = 1/2.54 inch = BASE_PX/2.54 points
|
|
66
|
+
# 1 mm = 1/25.4 inch = BASE_PX/25.4 points
|
|
67
|
+
# 1 pc = 12 points
|
|
68
|
+
# 1 px = 1 point (at BASE_PX DPI)
|
|
69
|
+
|
|
70
|
+
if unit in ("pt", "px", ""):
|
|
71
|
+
return numeric_value
|
|
72
|
+
elif unit == "in":
|
|
73
|
+
return numeric_value * float(BASE_PX)
|
|
74
|
+
elif unit == "pc":
|
|
75
|
+
return numeric_value * 12.0
|
|
76
|
+
elif unit == "cm":
|
|
77
|
+
return numeric_value * float(BASE_PX) / 2.54
|
|
78
|
+
elif unit == "mm":
|
|
79
|
+
return numeric_value * float(BASE_PX) / 25.4
|
|
80
|
+
# ---treat other units as px for now---
|
|
81
|
+
else:
|
|
82
|
+
return numeric_value
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def _dimensions_from_stream(cls, stream):
|
|
86
|
+
stream.seek(0)
|
|
87
|
+
data = stream.read()
|
|
88
|
+
root = ET.fromstring(data)
|
|
89
|
+
|
|
90
|
+
width_str = root.attrib.get("width")
|
|
91
|
+
height_str = root.attrib.get("height")
|
|
92
|
+
|
|
93
|
+
if width_str and height_str:
|
|
94
|
+
width = round(cls._parse_svg_unit_value(width_str))
|
|
95
|
+
height = round(cls._parse_svg_unit_value(height_str))
|
|
96
|
+
return width, height
|
|
97
|
+
|
|
98
|
+
if root.attrib.get("viewBox"):
|
|
99
|
+
return cls._calculate_scaled_dimensions(root.attrib["viewBox"])
|
|
100
|
+
|
|
101
|
+
return 0, 0
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def _calculate_scaled_dimensions(
|
|
105
|
+
cls, viewbox: str, base_px: int = BASE_PX
|
|
106
|
+
) -> tuple[int, int]:
|
|
107
|
+
_, _, logical_width, logical_height = map(float, viewbox.split())
|
|
108
|
+
|
|
109
|
+
aspect_ratio = logical_width / logical_height
|
|
110
|
+
|
|
111
|
+
if aspect_ratio >= 1:
|
|
112
|
+
final_width = base_px
|
|
113
|
+
final_height = base_px / aspect_ratio
|
|
114
|
+
else:
|
|
115
|
+
final_height = base_px
|
|
116
|
+
final_width = base_px * aspect_ratio
|
|
117
|
+
|
|
118
|
+
return round(final_width), round(final_height)
|
docx/image/tiff.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from .constants import MIME_TYPE, TIFF_FLD, TIFF_TAG
|
|
2
|
+
from .helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader
|
|
3
|
+
from .image import BaseImageHeader
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Tiff(BaseImageHeader):
|
|
7
|
+
"""Image header parser for TIFF images.
|
|
8
|
+
|
|
9
|
+
Handles both big and little endian byte ordering.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def content_type(self):
|
|
14
|
+
"""Return the MIME type of this TIFF image, unconditionally the string
|
|
15
|
+
``image/tiff``."""
|
|
16
|
+
return MIME_TYPE.TIFF
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def default_ext(self):
|
|
20
|
+
"""Default filename extension, always 'tiff' for TIFF images."""
|
|
21
|
+
return "tiff"
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_stream(cls, stream):
|
|
25
|
+
"""Return a |Tiff| instance containing the properties of the TIFF image in
|
|
26
|
+
`stream`."""
|
|
27
|
+
parser = _TiffParser.parse(stream)
|
|
28
|
+
|
|
29
|
+
px_width = parser.px_width
|
|
30
|
+
px_height = parser.px_height
|
|
31
|
+
horz_dpi = parser.horz_dpi
|
|
32
|
+
vert_dpi = parser.vert_dpi
|
|
33
|
+
|
|
34
|
+
return cls(px_width, px_height, horz_dpi, vert_dpi)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _TiffParser:
|
|
38
|
+
"""Parses a TIFF image stream to extract the image properties found in its main
|
|
39
|
+
image file directory (IFD)"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, ifd_entries):
|
|
42
|
+
super(_TiffParser, self).__init__()
|
|
43
|
+
self._ifd_entries = ifd_entries
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def parse(cls, stream):
|
|
47
|
+
"""Return an instance of |_TiffParser| containing the properties parsed from the
|
|
48
|
+
TIFF image in `stream`."""
|
|
49
|
+
stream_rdr = cls._make_stream_reader(stream)
|
|
50
|
+
ifd0_offset = stream_rdr.read_long(4)
|
|
51
|
+
ifd_entries = _IfdEntries.from_stream(stream_rdr, ifd0_offset)
|
|
52
|
+
return cls(ifd_entries)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def horz_dpi(self):
|
|
56
|
+
"""The horizontal dots per inch value calculated from the XResolution and
|
|
57
|
+
ResolutionUnit tags of the IFD; defaults to 72 if those tags are not present."""
|
|
58
|
+
return self._dpi(TIFF_TAG.X_RESOLUTION)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def vert_dpi(self):
|
|
62
|
+
"""The vertical dots per inch value calculated from the XResolution and
|
|
63
|
+
ResolutionUnit tags of the IFD; defaults to 72 if those tags are not present."""
|
|
64
|
+
return self._dpi(TIFF_TAG.Y_RESOLUTION)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def px_height(self):
|
|
68
|
+
"""The number of stacked rows of pixels in the image, |None| if the IFD contains
|
|
69
|
+
no ``ImageLength`` tag, the expected case when the TIFF is embeded in an Exif
|
|
70
|
+
image."""
|
|
71
|
+
return self._ifd_entries.get(TIFF_TAG.IMAGE_LENGTH)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def px_width(self):
|
|
75
|
+
"""The number of pixels in each row in the image, |None| if the IFD contains no
|
|
76
|
+
``ImageWidth`` tag, the expected case when the TIFF is embeded in an Exif
|
|
77
|
+
image."""
|
|
78
|
+
return self._ifd_entries.get(TIFF_TAG.IMAGE_WIDTH)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def _detect_endian(cls, stream):
|
|
82
|
+
"""Return either BIG_ENDIAN or LITTLE_ENDIAN depending on the endian indicator
|
|
83
|
+
found in the TIFF `stream` header, either 'MM' or 'II'."""
|
|
84
|
+
stream.seek(0)
|
|
85
|
+
endian_str = stream.read(2)
|
|
86
|
+
return BIG_ENDIAN if endian_str == b"MM" else LITTLE_ENDIAN
|
|
87
|
+
|
|
88
|
+
def _dpi(self, resolution_tag):
|
|
89
|
+
"""Return the dpi value calculated for `resolution_tag`, which can be either
|
|
90
|
+
TIFF_TAG.X_RESOLUTION or TIFF_TAG.Y_RESOLUTION.
|
|
91
|
+
|
|
92
|
+
The calculation is based on the values of both that tag and the
|
|
93
|
+
TIFF_TAG.RESOLUTION_UNIT tag in this parser's |_IfdEntries| instance.
|
|
94
|
+
"""
|
|
95
|
+
ifd_entries = self._ifd_entries
|
|
96
|
+
|
|
97
|
+
if resolution_tag not in ifd_entries:
|
|
98
|
+
return 72
|
|
99
|
+
|
|
100
|
+
# resolution unit defaults to inches (2)
|
|
101
|
+
resolution_unit = ifd_entries.get(TIFF_TAG.RESOLUTION_UNIT, 2)
|
|
102
|
+
|
|
103
|
+
if resolution_unit == 1: # aspect ratio only
|
|
104
|
+
return 72
|
|
105
|
+
# resolution_unit == 2 for inches, 3 for centimeters
|
|
106
|
+
units_per_inch = 1 if resolution_unit == 2 else 2.54
|
|
107
|
+
dots_per_unit = ifd_entries[resolution_tag]
|
|
108
|
+
return int(round(dots_per_unit * units_per_inch))
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def _make_stream_reader(cls, stream):
|
|
112
|
+
"""Return a |StreamReader| instance with wrapping `stream` and having "endian-
|
|
113
|
+
ness" determined by the 'MM' or 'II' indicator in the TIFF stream header."""
|
|
114
|
+
endian = cls._detect_endian(stream)
|
|
115
|
+
return StreamReader(stream, endian)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class _IfdEntries:
|
|
119
|
+
"""Image File Directory for a TIFF image, having mapping (dict) semantics allowing
|
|
120
|
+
"tag" values to be retrieved by tag code."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, entries):
|
|
123
|
+
super(_IfdEntries, self).__init__()
|
|
124
|
+
self._entries = entries
|
|
125
|
+
|
|
126
|
+
def __contains__(self, key):
|
|
127
|
+
"""Provides ``in`` operator, e.g. ``tag in ifd_entries``"""
|
|
128
|
+
return self._entries.__contains__(key)
|
|
129
|
+
|
|
130
|
+
def __getitem__(self, key):
|
|
131
|
+
"""Provides indexed access, e.g. ``tag_value = ifd_entries[tag_code]``"""
|
|
132
|
+
return self._entries.__getitem__(key)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_stream(cls, stream, offset):
|
|
136
|
+
"""Return a new |_IfdEntries| instance parsed from `stream` starting at
|
|
137
|
+
`offset`."""
|
|
138
|
+
ifd_parser = _IfdParser(stream, offset)
|
|
139
|
+
entries = {e.tag: e.value for e in ifd_parser.iter_entries()}
|
|
140
|
+
return cls(entries)
|
|
141
|
+
|
|
142
|
+
def get(self, tag_code, default=None):
|
|
143
|
+
"""Return value of IFD entry having tag matching `tag_code`, or `default` if no
|
|
144
|
+
matching tag found."""
|
|
145
|
+
return self._entries.get(tag_code, default)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class _IfdParser:
|
|
149
|
+
"""Service object that knows how to extract directory entries from an Image File
|
|
150
|
+
Directory (IFD)"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, stream_rdr, offset):
|
|
153
|
+
super(_IfdParser, self).__init__()
|
|
154
|
+
self._stream_rdr = stream_rdr
|
|
155
|
+
self._offset = offset
|
|
156
|
+
|
|
157
|
+
def iter_entries(self):
|
|
158
|
+
"""Generate an |_IfdEntry| instance corresponding to each entry in the
|
|
159
|
+
directory."""
|
|
160
|
+
for idx in range(self._entry_count):
|
|
161
|
+
dir_entry_offset = self._offset + 2 + (idx * 12)
|
|
162
|
+
ifd_entry = _IfdEntryFactory(self._stream_rdr, dir_entry_offset)
|
|
163
|
+
yield ifd_entry
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def _entry_count(self):
|
|
167
|
+
"""The count of directory entries, read from the top of the IFD header."""
|
|
168
|
+
return self._stream_rdr.read_short(self._offset)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _IfdEntryFactory(stream_rdr, offset):
|
|
172
|
+
"""Return an |_IfdEntry| subclass instance containing the value of the directory
|
|
173
|
+
entry at `offset` in `stream_rdr`."""
|
|
174
|
+
ifd_entry_classes = {
|
|
175
|
+
TIFF_FLD.ASCII: _AsciiIfdEntry,
|
|
176
|
+
TIFF_FLD.SHORT: _ShortIfdEntry,
|
|
177
|
+
TIFF_FLD.LONG: _LongIfdEntry,
|
|
178
|
+
TIFF_FLD.RATIONAL: _RationalIfdEntry,
|
|
179
|
+
}
|
|
180
|
+
field_type = stream_rdr.read_short(offset, 2)
|
|
181
|
+
EntryCls = ifd_entry_classes.get(field_type, _IfdEntry)
|
|
182
|
+
return EntryCls.from_stream(stream_rdr, offset)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class _IfdEntry:
|
|
186
|
+
"""Base class for IFD entry classes.
|
|
187
|
+
|
|
188
|
+
Subclasses are differentiated by value type, e.g. ASCII, long int, etc.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, tag_code, value):
|
|
192
|
+
super(_IfdEntry, self).__init__()
|
|
193
|
+
self._tag_code = tag_code
|
|
194
|
+
self._value = value
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_stream(cls, stream_rdr, offset):
|
|
198
|
+
"""Return an |_IfdEntry| subclass instance containing the tag and value of the
|
|
199
|
+
tag parsed from `stream_rdr` at `offset`.
|
|
200
|
+
|
|
201
|
+
Note this method is common to all subclasses. Override the ``_parse_value()``
|
|
202
|
+
method to provide distinctive behavior based on field type.
|
|
203
|
+
"""
|
|
204
|
+
tag_code = stream_rdr.read_short(offset, 0)
|
|
205
|
+
value_count = stream_rdr.read_long(offset, 4)
|
|
206
|
+
value_offset = stream_rdr.read_long(offset, 8)
|
|
207
|
+
value = cls._parse_value(stream_rdr, offset, value_count, value_offset)
|
|
208
|
+
return cls(tag_code, value)
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
|
|
212
|
+
"""Return the value of this field parsed from `stream_rdr` at `offset`.
|
|
213
|
+
|
|
214
|
+
Intended to be overridden by subclasses.
|
|
215
|
+
"""
|
|
216
|
+
return "UNIMPLEMENTED FIELD TYPE" # pragma: no cover
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def tag(self):
|
|
220
|
+
"""Short int code that identifies this IFD entry."""
|
|
221
|
+
return self._tag_code
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def value(self):
|
|
225
|
+
"""Value of this tag, its type being dependent on the tag."""
|
|
226
|
+
return self._value
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class _AsciiIfdEntry(_IfdEntry):
|
|
230
|
+
"""IFD entry having the form of a NULL-terminated ASCII string."""
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
|
|
234
|
+
"""Return the ASCII string parsed from `stream_rdr` at `value_offset`.
|
|
235
|
+
|
|
236
|
+
The length of the string, including a terminating '\x00' (NUL) character, is in
|
|
237
|
+
`value_count`.
|
|
238
|
+
"""
|
|
239
|
+
return stream_rdr.read_str(value_count - 1, value_offset)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class _ShortIfdEntry(_IfdEntry):
|
|
243
|
+
"""IFD entry expressed as a short (2-byte) integer."""
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
|
|
247
|
+
"""Return the short int value contained in the `value_offset` field of this
|
|
248
|
+
entry.
|
|
249
|
+
|
|
250
|
+
Only supports single values at present.
|
|
251
|
+
"""
|
|
252
|
+
if value_count == 1:
|
|
253
|
+
return stream_rdr.read_short(offset, 8)
|
|
254
|
+
else: # pragma: no cover
|
|
255
|
+
return "Multi-value short integer NOT IMPLEMENTED"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class _LongIfdEntry(_IfdEntry):
|
|
259
|
+
"""IFD entry expressed as a long (4-byte) integer."""
|
|
260
|
+
|
|
261
|
+
@classmethod
|
|
262
|
+
def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
|
|
263
|
+
"""Return the long int value contained in the `value_offset` field of this
|
|
264
|
+
entry.
|
|
265
|
+
|
|
266
|
+
Only supports single values at present.
|
|
267
|
+
"""
|
|
268
|
+
if value_count == 1:
|
|
269
|
+
return stream_rdr.read_long(offset, 8)
|
|
270
|
+
else: # pragma: no cover
|
|
271
|
+
return "Multi-value long integer NOT IMPLEMENTED"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class _RationalIfdEntry(_IfdEntry):
|
|
275
|
+
"""IFD entry expressed as a numerator, denominator pair."""
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
|
|
279
|
+
"""Return the rational (numerator / denominator) value at `value_offset` in
|
|
280
|
+
`stream_rdr` as a floating-point number.
|
|
281
|
+
|
|
282
|
+
Only supports single values at present.
|
|
283
|
+
"""
|
|
284
|
+
if value_count == 1:
|
|
285
|
+
numerator = stream_rdr.read_long(value_offset)
|
|
286
|
+
denominator = stream_rdr.read_long(value_offset, 4)
|
|
287
|
+
return numerator / denominator
|
|
288
|
+
else: # pragma: no cover
|
|
289
|
+
return "Multi-value Rational NOT IMPLEMENTED"
|
docx/opc/__init__.py
ADDED
|
File without changes
|