ps-python-docx 1.3.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 +68 -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 +275 -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 +23 -0
- docx/image/bmp.py +43 -0
- docx/image/constants.py +172 -0
- docx/image/exceptions.py +13 -0
- docx/image/gif.py +38 -0
- docx/image/helpers.py +86 -0
- docx/image/image.py +234 -0
- docx/image/jpeg.py +425 -0
- docx/image/png.py +253 -0
- docx/image/tiff.py +289 -0
- docx/opc/__init__.py +0 -0
- docx/opc/constants.py +306 -0
- docx/opc/coreprops.py +142 -0
- docx/opc/exceptions.py +12 -0
- docx/opc/oxml.py +247 -0
- docx/opc/package.py +219 -0
- docx/opc/packuri.py +109 -0
- docx/opc/part.py +247 -0
- docx/opc/parts/__init__.py +0 -0
- docx/opc/parts/coreprops.py +48 -0
- docx/opc/phys_pkg.py +119 -0
- docx/opc/pkgreader.py +254 -0
- docx/opc/pkgwriter.py +115 -0
- docx/opc/rel.py +153 -0
- docx/opc/shared.py +31 -0
- docx/opc/spec.py +24 -0
- docx/oxml/__init__.py +261 -0
- docx/oxml/comments.py +124 -0
- docx/oxml/coreprops.py +298 -0
- docx/oxml/document.py +88 -0
- docx/oxml/drawing.py +11 -0
- docx/oxml/exceptions.py +10 -0
- docx/oxml/ns.py +109 -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 +299 -0
- docx/oxml/shared.py +52 -0
- docx/oxml/simpletypes.py +434 -0
- docx/oxml/styles.py +341 -0
- docx/oxml/table.py +977 -0
- docx/oxml/text/__init__.py +0 -0
- docx/oxml/text/font.py +333 -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 +307 -0
- docx/oxml/xmlchemy.py +696 -0
- docx/package.py +110 -0
- docx/parts/__init__.py +0 -0
- docx/parts/comments.py +51 -0
- docx/parts/document.py +182 -0
- docx/parts/hdrftr.py +53 -0
- docx/parts/image.py +80 -0
- docx/parts/numbering.py +32 -0
- docx/parts/settings.py +50 -0
- docx/parts/story.py +95 -0
- docx/parts/styles.py +42 -0
- docx/parts/theme.py +53 -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 +264 -0
- docx/styles/styles.py +147 -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-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 +472 -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 +257 -0
- docx/text/tabstops.py +123 -0
- docx/theme.py +67 -0
- docx/types.py +34 -0
- ps_python_docx-1.3.0.dist-info/METADATA +77 -0
- ps_python_docx-1.3.0.dist-info/RECORD +126 -0
- ps_python_docx-1.3.0.dist-info/WHEEL +5 -0
- ps_python_docx-1.3.0.dist-info/licenses/LICENSE +20 -0
- ps_python_docx-1.3.0.dist-info/top_level.txt +1 -0
docx/image/jpeg.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Objects related to parsing headers of JPEG image streams.
|
|
2
|
+
|
|
3
|
+
Includes both JFIF and Exif sub-formats.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import io
|
|
7
|
+
|
|
8
|
+
from docx.image.constants import JPEG_MARKER_CODE, MIME_TYPE
|
|
9
|
+
from docx.image.helpers import BIG_ENDIAN, StreamReader
|
|
10
|
+
from docx.image.image import BaseImageHeader
|
|
11
|
+
from docx.image.tiff import Tiff
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Jpeg(BaseImageHeader):
|
|
15
|
+
"""Base class for JFIF and EXIF subclasses."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def content_type(self):
|
|
19
|
+
"""MIME content type for this image, unconditionally `image/jpeg` for JPEG
|
|
20
|
+
images."""
|
|
21
|
+
return MIME_TYPE.JPEG
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def default_ext(self):
|
|
25
|
+
"""Default filename extension, always 'jpg' for JPG images."""
|
|
26
|
+
return "jpg"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Exif(Jpeg):
|
|
30
|
+
"""Image header parser for Exif image format."""
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_stream(cls, stream):
|
|
34
|
+
"""Return |Exif| instance having header properties parsed from Exif image in
|
|
35
|
+
`stream`."""
|
|
36
|
+
markers = _JfifMarkers.from_stream(stream)
|
|
37
|
+
# print('\n%s' % markers)
|
|
38
|
+
|
|
39
|
+
px_width = markers.sof.px_width
|
|
40
|
+
px_height = markers.sof.px_height
|
|
41
|
+
horz_dpi = markers.app1.horz_dpi
|
|
42
|
+
vert_dpi = markers.app1.vert_dpi
|
|
43
|
+
|
|
44
|
+
return cls(px_width, px_height, horz_dpi, vert_dpi)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Jfif(Jpeg):
|
|
48
|
+
"""Image header parser for JFIF image format."""
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_stream(cls, stream):
|
|
52
|
+
"""Return a |Jfif| instance having header properties parsed from image in
|
|
53
|
+
`stream`."""
|
|
54
|
+
markers = _JfifMarkers.from_stream(stream)
|
|
55
|
+
|
|
56
|
+
px_width = markers.sof.px_width
|
|
57
|
+
px_height = markers.sof.px_height
|
|
58
|
+
horz_dpi = markers.app0.horz_dpi
|
|
59
|
+
vert_dpi = markers.app0.vert_dpi
|
|
60
|
+
|
|
61
|
+
return cls(px_width, px_height, horz_dpi, vert_dpi)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _JfifMarkers:
|
|
65
|
+
"""Sequence of markers in a JPEG file, perhaps truncated at first SOS marker for
|
|
66
|
+
performance reasons."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, markers):
|
|
69
|
+
super(_JfifMarkers, self).__init__()
|
|
70
|
+
self._markers = list(markers)
|
|
71
|
+
|
|
72
|
+
def __str__(self): # pragma: no cover
|
|
73
|
+
"""Returns a tabular listing of the markers in this instance, which can be handy
|
|
74
|
+
for debugging and perhaps other uses."""
|
|
75
|
+
header = " offset seglen mc name\n======= ====== == ====="
|
|
76
|
+
tmpl = "%7d %6d %02X %s"
|
|
77
|
+
rows = []
|
|
78
|
+
for marker in self._markers:
|
|
79
|
+
rows.append(
|
|
80
|
+
tmpl
|
|
81
|
+
% (
|
|
82
|
+
marker.offset,
|
|
83
|
+
marker.segment_length,
|
|
84
|
+
ord(marker.marker_code),
|
|
85
|
+
marker.name,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
lines = [header] + rows
|
|
89
|
+
return "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_stream(cls, stream):
|
|
93
|
+
"""Return a |_JfifMarkers| instance containing a |_JfifMarker| subclass instance
|
|
94
|
+
for each marker in `stream`."""
|
|
95
|
+
marker_parser = _MarkerParser.from_stream(stream)
|
|
96
|
+
markers = []
|
|
97
|
+
for marker in marker_parser.iter_markers():
|
|
98
|
+
markers.append(marker)
|
|
99
|
+
if marker.marker_code == JPEG_MARKER_CODE.SOS:
|
|
100
|
+
break
|
|
101
|
+
return cls(markers)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def app0(self):
|
|
105
|
+
"""First APP0 marker in image markers."""
|
|
106
|
+
for m in self._markers:
|
|
107
|
+
if m.marker_code == JPEG_MARKER_CODE.APP0:
|
|
108
|
+
return m
|
|
109
|
+
raise KeyError("no APP0 marker in image")
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def app1(self):
|
|
113
|
+
"""First APP1 marker in image markers."""
|
|
114
|
+
for m in self._markers:
|
|
115
|
+
if m.marker_code == JPEG_MARKER_CODE.APP1:
|
|
116
|
+
return m
|
|
117
|
+
raise KeyError("no APP1 marker in image")
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def sof(self):
|
|
121
|
+
"""First start of frame (SOFn) marker in this sequence."""
|
|
122
|
+
for m in self._markers:
|
|
123
|
+
if m.marker_code in JPEG_MARKER_CODE.SOF_MARKER_CODES:
|
|
124
|
+
return m
|
|
125
|
+
raise KeyError("no start of frame (SOFn) marker in image")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class _MarkerParser:
|
|
129
|
+
"""Service class that knows how to parse a JFIF stream and iterate over its
|
|
130
|
+
markers."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, stream_reader):
|
|
133
|
+
super(_MarkerParser, self).__init__()
|
|
134
|
+
self._stream = stream_reader
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_stream(cls, stream):
|
|
138
|
+
"""Return a |_MarkerParser| instance to parse JFIF markers from `stream`."""
|
|
139
|
+
stream_reader = StreamReader(stream, BIG_ENDIAN)
|
|
140
|
+
return cls(stream_reader)
|
|
141
|
+
|
|
142
|
+
def iter_markers(self):
|
|
143
|
+
"""Generate a (marker_code, segment_offset) 2-tuple for each marker in the JPEG
|
|
144
|
+
`stream`, in the order they occur in the stream."""
|
|
145
|
+
marker_finder = _MarkerFinder.from_stream(self._stream)
|
|
146
|
+
start = 0
|
|
147
|
+
marker_code = None
|
|
148
|
+
while marker_code != JPEG_MARKER_CODE.EOI:
|
|
149
|
+
marker_code, segment_offset = marker_finder.next(start)
|
|
150
|
+
marker = _MarkerFactory(marker_code, self._stream, segment_offset)
|
|
151
|
+
yield marker
|
|
152
|
+
start = segment_offset + marker.segment_length
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class _MarkerFinder:
|
|
156
|
+
"""Service class that knows how to find the next JFIF marker in a stream."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, stream):
|
|
159
|
+
super(_MarkerFinder, self).__init__()
|
|
160
|
+
self._stream = stream
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def from_stream(cls, stream):
|
|
164
|
+
"""Return a |_MarkerFinder| instance to find JFIF markers in `stream`."""
|
|
165
|
+
return cls(stream)
|
|
166
|
+
|
|
167
|
+
def next(self, start):
|
|
168
|
+
"""Return a (marker_code, segment_offset) 2-tuple identifying and locating the
|
|
169
|
+
first marker in `stream` occuring after offset `start`.
|
|
170
|
+
|
|
171
|
+
The returned `segment_offset` points to the position immediately following the
|
|
172
|
+
2-byte marker code, the start of the marker segment, for those markers that have
|
|
173
|
+
a segment.
|
|
174
|
+
"""
|
|
175
|
+
position = start
|
|
176
|
+
while True:
|
|
177
|
+
# skip over any non-\xFF bytes
|
|
178
|
+
position = self._offset_of_next_ff_byte(start=position)
|
|
179
|
+
# skip over any \xFF padding bytes
|
|
180
|
+
position, byte_ = self._next_non_ff_byte(start=position + 1)
|
|
181
|
+
# 'FF 00' sequence is not a marker, start over if found
|
|
182
|
+
if byte_ == b"\x00":
|
|
183
|
+
continue
|
|
184
|
+
# this is a marker, gather return values and break out of scan
|
|
185
|
+
marker_code, segment_offset = byte_, position + 1
|
|
186
|
+
break
|
|
187
|
+
return marker_code, segment_offset
|
|
188
|
+
|
|
189
|
+
def _next_non_ff_byte(self, start):
|
|
190
|
+
"""Return an offset, byte 2-tuple for the next byte in `stream` that is not
|
|
191
|
+
'\xff', starting with the byte at offset `start`.
|
|
192
|
+
|
|
193
|
+
If the byte at offset `start` is not '\xff', `start` and the returned `offset`
|
|
194
|
+
will be the same.
|
|
195
|
+
"""
|
|
196
|
+
self._stream.seek(start)
|
|
197
|
+
byte_ = self._read_byte()
|
|
198
|
+
while byte_ == b"\xff":
|
|
199
|
+
byte_ = self._read_byte()
|
|
200
|
+
offset_of_non_ff_byte = self._stream.tell() - 1
|
|
201
|
+
return offset_of_non_ff_byte, byte_
|
|
202
|
+
|
|
203
|
+
def _offset_of_next_ff_byte(self, start):
|
|
204
|
+
"""Return the offset of the next '\xff' byte in `stream` starting with the byte
|
|
205
|
+
at offset `start`.
|
|
206
|
+
|
|
207
|
+
Returns `start` if the byte at that offset is a hex 255; it does not necessarily
|
|
208
|
+
advance in the stream.
|
|
209
|
+
"""
|
|
210
|
+
self._stream.seek(start)
|
|
211
|
+
byte_ = self._read_byte()
|
|
212
|
+
while byte_ != b"\xff":
|
|
213
|
+
byte_ = self._read_byte()
|
|
214
|
+
offset_of_ff_byte = self._stream.tell() - 1
|
|
215
|
+
return offset_of_ff_byte
|
|
216
|
+
|
|
217
|
+
def _read_byte(self):
|
|
218
|
+
"""Return the next byte read from stream.
|
|
219
|
+
|
|
220
|
+
Raise Exception if stream is at end of file.
|
|
221
|
+
"""
|
|
222
|
+
byte_ = self._stream.read(1)
|
|
223
|
+
if not byte_: # pragma: no cover
|
|
224
|
+
raise Exception("unexpected end of file")
|
|
225
|
+
return byte_
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _MarkerFactory(marker_code, stream, offset):
|
|
229
|
+
"""Return |_Marker| or subclass instance appropriate for marker at `offset` in
|
|
230
|
+
`stream` having `marker_code`."""
|
|
231
|
+
if marker_code == JPEG_MARKER_CODE.APP0:
|
|
232
|
+
marker_cls = _App0Marker
|
|
233
|
+
elif marker_code == JPEG_MARKER_CODE.APP1:
|
|
234
|
+
marker_cls = _App1Marker
|
|
235
|
+
elif marker_code in JPEG_MARKER_CODE.SOF_MARKER_CODES:
|
|
236
|
+
marker_cls = _SofMarker
|
|
237
|
+
else:
|
|
238
|
+
marker_cls = _Marker
|
|
239
|
+
return marker_cls.from_stream(stream, marker_code, offset)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class _Marker:
|
|
243
|
+
"""Base class for JFIF marker classes.
|
|
244
|
+
|
|
245
|
+
Represents a marker and its segment occuring in a JPEG byte stream.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def __init__(self, marker_code, offset, segment_length):
|
|
249
|
+
super(_Marker, self).__init__()
|
|
250
|
+
self._marker_code = marker_code
|
|
251
|
+
self._offset = offset
|
|
252
|
+
self._segment_length = segment_length
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def from_stream(cls, stream, marker_code, offset):
|
|
256
|
+
"""Return a generic |_Marker| instance for the marker at `offset` in `stream`
|
|
257
|
+
having `marker_code`."""
|
|
258
|
+
if JPEG_MARKER_CODE.is_standalone(marker_code):
|
|
259
|
+
segment_length = 0
|
|
260
|
+
else:
|
|
261
|
+
segment_length = stream.read_short(offset)
|
|
262
|
+
return cls(marker_code, offset, segment_length)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def marker_code(self):
|
|
266
|
+
"""The single-byte code that identifies the type of this marker, e.g. ``'\xe0'``
|
|
267
|
+
for start of image (SOI)."""
|
|
268
|
+
return self._marker_code
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def name(self): # pragma: no cover
|
|
272
|
+
return JPEG_MARKER_CODE.marker_names[self._marker_code]
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def offset(self): # pragma: no cover
|
|
276
|
+
return self._offset
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def segment_length(self):
|
|
280
|
+
"""The length in bytes of this marker's segment."""
|
|
281
|
+
return self._segment_length
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class _App0Marker(_Marker):
|
|
285
|
+
"""Represents a JFIF APP0 marker segment."""
|
|
286
|
+
|
|
287
|
+
def __init__(self, marker_code, offset, length, density_units, x_density, y_density):
|
|
288
|
+
super(_App0Marker, self).__init__(marker_code, offset, length)
|
|
289
|
+
self._density_units = density_units
|
|
290
|
+
self._x_density = x_density
|
|
291
|
+
self._y_density = y_density
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def horz_dpi(self):
|
|
295
|
+
"""Horizontal dots per inch specified in this marker, defaults to 72 if not
|
|
296
|
+
specified."""
|
|
297
|
+
return self._dpi(self._x_density)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def vert_dpi(self):
|
|
301
|
+
"""Vertical dots per inch specified in this marker, defaults to 72 if not
|
|
302
|
+
specified."""
|
|
303
|
+
return self._dpi(self._y_density)
|
|
304
|
+
|
|
305
|
+
def _dpi(self, density):
|
|
306
|
+
"""Return dots per inch corresponding to `density` value."""
|
|
307
|
+
if self._density_units == 1:
|
|
308
|
+
dpi = density
|
|
309
|
+
elif self._density_units == 2:
|
|
310
|
+
dpi = int(round(density * 2.54))
|
|
311
|
+
else:
|
|
312
|
+
dpi = 72
|
|
313
|
+
return dpi
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def from_stream(cls, stream, marker_code, offset):
|
|
317
|
+
"""Return an |_App0Marker| instance for the APP0 marker at `offset` in
|
|
318
|
+
`stream`."""
|
|
319
|
+
# field off type notes
|
|
320
|
+
# ------------------ --- ----- -------------------
|
|
321
|
+
# segment length 0 short
|
|
322
|
+
# JFIF identifier 2 5 chr 'JFIF\x00'
|
|
323
|
+
# major JPEG version 7 byte typically 1
|
|
324
|
+
# minor JPEG version 8 byte typically 1 or 2
|
|
325
|
+
# density units 9 byte 1=inches, 2=cm
|
|
326
|
+
# horz dots per unit 10 short
|
|
327
|
+
# vert dots per unit 12 short
|
|
328
|
+
# ------------------ --- ----- -------------------
|
|
329
|
+
segment_length = stream.read_short(offset)
|
|
330
|
+
density_units = stream.read_byte(offset, 9)
|
|
331
|
+
x_density = stream.read_short(offset, 10)
|
|
332
|
+
y_density = stream.read_short(offset, 12)
|
|
333
|
+
return cls(marker_code, offset, segment_length, density_units, x_density, y_density)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class _App1Marker(_Marker):
|
|
337
|
+
"""Represents a JFIF APP1 (Exif) marker segment."""
|
|
338
|
+
|
|
339
|
+
def __init__(self, marker_code, offset, length, horz_dpi, vert_dpi):
|
|
340
|
+
super(_App1Marker, self).__init__(marker_code, offset, length)
|
|
341
|
+
self._horz_dpi = horz_dpi
|
|
342
|
+
self._vert_dpi = vert_dpi
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def from_stream(cls, stream, marker_code, offset):
|
|
346
|
+
"""Extract the horizontal and vertical dots-per-inch value from the APP1 header
|
|
347
|
+
at `offset` in `stream`."""
|
|
348
|
+
# field off len type notes
|
|
349
|
+
# -------------------- --- --- ----- ----------------------------
|
|
350
|
+
# segment length 0 2 short
|
|
351
|
+
# Exif identifier 2 6 6 chr 'Exif\x00\x00'
|
|
352
|
+
# TIFF byte order 8 2 2 chr 'II'=little 'MM'=big endian
|
|
353
|
+
# meaning of universe 10 2 2 chr '*\x00' or '\x00*' depending
|
|
354
|
+
# IFD0 off fr/II or MM 10 16 long relative to ...?
|
|
355
|
+
# -------------------- --- --- ----- ----------------------------
|
|
356
|
+
segment_length = stream.read_short(offset)
|
|
357
|
+
if cls._is_non_Exif_APP1_segment(stream, offset):
|
|
358
|
+
return cls(marker_code, offset, segment_length, 72, 72)
|
|
359
|
+
tiff = cls._tiff_from_exif_segment(stream, offset, segment_length)
|
|
360
|
+
return cls(marker_code, offset, segment_length, tiff.horz_dpi, tiff.vert_dpi)
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def horz_dpi(self):
|
|
364
|
+
"""Horizontal dots per inch specified in this marker, defaults to 72 if not
|
|
365
|
+
specified."""
|
|
366
|
+
return self._horz_dpi
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def vert_dpi(self):
|
|
370
|
+
"""Vertical dots per inch specified in this marker, defaults to 72 if not
|
|
371
|
+
specified."""
|
|
372
|
+
return self._vert_dpi
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
def _is_non_Exif_APP1_segment(cls, stream, offset):
|
|
376
|
+
"""Return True if the APP1 segment at `offset` in `stream` is NOT an Exif
|
|
377
|
+
segment, as determined by the ``'Exif\x00\x00'`` signature at offset 2 in the
|
|
378
|
+
segment."""
|
|
379
|
+
stream.seek(offset + 2)
|
|
380
|
+
exif_signature = stream.read(6)
|
|
381
|
+
return exif_signature != b"Exif\x00\x00"
|
|
382
|
+
|
|
383
|
+
@classmethod
|
|
384
|
+
def _tiff_from_exif_segment(cls, stream, offset, segment_length):
|
|
385
|
+
"""Return a |Tiff| instance parsed from the Exif APP1 segment of
|
|
386
|
+
`segment_length` at `offset` in `stream`."""
|
|
387
|
+
# wrap full segment in its own stream and feed to Tiff()
|
|
388
|
+
stream.seek(offset + 8)
|
|
389
|
+
segment_bytes = stream.read(segment_length - 8)
|
|
390
|
+
substream = io.BytesIO(segment_bytes)
|
|
391
|
+
return Tiff.from_stream(substream)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class _SofMarker(_Marker):
|
|
395
|
+
"""Represents a JFIF start of frame (SOFx) marker segment."""
|
|
396
|
+
|
|
397
|
+
def __init__(self, marker_code, offset, segment_length, px_width, px_height):
|
|
398
|
+
super(_SofMarker, self).__init__(marker_code, offset, segment_length)
|
|
399
|
+
self._px_width = px_width
|
|
400
|
+
self._px_height = px_height
|
|
401
|
+
|
|
402
|
+
@classmethod
|
|
403
|
+
def from_stream(cls, stream, marker_code, offset):
|
|
404
|
+
"""Return an |_SofMarker| instance for the SOFn marker at `offset` in stream."""
|
|
405
|
+
# field off type notes
|
|
406
|
+
# ------------------ --- ----- ----------------------------
|
|
407
|
+
# segment length 0 short
|
|
408
|
+
# Data precision 2 byte
|
|
409
|
+
# Vertical lines 3 short px_height
|
|
410
|
+
# Horizontal lines 5 short px_width
|
|
411
|
+
# ------------------ --- ----- ----------------------------
|
|
412
|
+
segment_length = stream.read_short(offset)
|
|
413
|
+
px_height = stream.read_short(offset, 3)
|
|
414
|
+
px_width = stream.read_short(offset, 5)
|
|
415
|
+
return cls(marker_code, offset, segment_length, px_width, px_height)
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def px_height(self):
|
|
419
|
+
"""Image height in pixels."""
|
|
420
|
+
return self._px_height
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def px_width(self):
|
|
424
|
+
"""Image width in pixels."""
|
|
425
|
+
return self._px_width
|
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
|