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.
Files changed (126) hide show
  1. docx/__init__.py +68 -0
  2. docx/api.py +37 -0
  3. docx/blkcntnr.py +101 -0
  4. docx/comments.py +163 -0
  5. docx/dml/__init__.py +0 -0
  6. docx/dml/color.py +112 -0
  7. docx/document.py +275 -0
  8. docx/drawing/__init__.py +59 -0
  9. docx/enum/__init__.py +0 -0
  10. docx/enum/base.py +150 -0
  11. docx/enum/dml.py +103 -0
  12. docx/enum/section.py +86 -0
  13. docx/enum/shape.py +19 -0
  14. docx/enum/style.py +452 -0
  15. docx/enum/table.py +136 -0
  16. docx/enum/text.py +367 -0
  17. docx/exceptions.py +18 -0
  18. docx/image/__init__.py +23 -0
  19. docx/image/bmp.py +43 -0
  20. docx/image/constants.py +172 -0
  21. docx/image/exceptions.py +13 -0
  22. docx/image/gif.py +38 -0
  23. docx/image/helpers.py +86 -0
  24. docx/image/image.py +234 -0
  25. docx/image/jpeg.py +425 -0
  26. docx/image/png.py +253 -0
  27. docx/image/tiff.py +289 -0
  28. docx/opc/__init__.py +0 -0
  29. docx/opc/constants.py +306 -0
  30. docx/opc/coreprops.py +142 -0
  31. docx/opc/exceptions.py +12 -0
  32. docx/opc/oxml.py +247 -0
  33. docx/opc/package.py +219 -0
  34. docx/opc/packuri.py +109 -0
  35. docx/opc/part.py +247 -0
  36. docx/opc/parts/__init__.py +0 -0
  37. docx/opc/parts/coreprops.py +48 -0
  38. docx/opc/phys_pkg.py +119 -0
  39. docx/opc/pkgreader.py +254 -0
  40. docx/opc/pkgwriter.py +115 -0
  41. docx/opc/rel.py +153 -0
  42. docx/opc/shared.py +31 -0
  43. docx/opc/spec.py +24 -0
  44. docx/oxml/__init__.py +261 -0
  45. docx/oxml/comments.py +124 -0
  46. docx/oxml/coreprops.py +298 -0
  47. docx/oxml/document.py +88 -0
  48. docx/oxml/drawing.py +11 -0
  49. docx/oxml/exceptions.py +10 -0
  50. docx/oxml/ns.py +109 -0
  51. docx/oxml/numbering.py +109 -0
  52. docx/oxml/parser.py +62 -0
  53. docx/oxml/section.py +537 -0
  54. docx/oxml/settings.py +138 -0
  55. docx/oxml/shape.py +299 -0
  56. docx/oxml/shared.py +52 -0
  57. docx/oxml/simpletypes.py +434 -0
  58. docx/oxml/styles.py +341 -0
  59. docx/oxml/table.py +977 -0
  60. docx/oxml/text/__init__.py +0 -0
  61. docx/oxml/text/font.py +333 -0
  62. docx/oxml/text/hyperlink.py +45 -0
  63. docx/oxml/text/pagebreak.py +278 -0
  64. docx/oxml/text/paragraph.py +106 -0
  65. docx/oxml/text/parfmt.py +392 -0
  66. docx/oxml/text/run.py +307 -0
  67. docx/oxml/xmlchemy.py +696 -0
  68. docx/package.py +110 -0
  69. docx/parts/__init__.py +0 -0
  70. docx/parts/comments.py +51 -0
  71. docx/parts/document.py +182 -0
  72. docx/parts/hdrftr.py +53 -0
  73. docx/parts/image.py +80 -0
  74. docx/parts/numbering.py +32 -0
  75. docx/parts/settings.py +50 -0
  76. docx/parts/story.py +95 -0
  77. docx/parts/styles.py +42 -0
  78. docx/parts/theme.py +53 -0
  79. docx/py.typed +0 -0
  80. docx/section.py +479 -0
  81. docx/settings.py +35 -0
  82. docx/shape.py +103 -0
  83. docx/shared.py +382 -0
  84. docx/styles/__init__.py +40 -0
  85. docx/styles/latent.py +198 -0
  86. docx/styles/style.py +264 -0
  87. docx/styles/styles.py +147 -0
  88. docx/table.py +537 -0
  89. docx/templates/default-comments.xml +12 -0
  90. docx/templates/default-docx-template/[Content_Types].xml +17 -0
  91. docx/templates/default-docx-template/_rels/.rels +7 -0
  92. docx/templates/default-docx-template/customXml/_rels/item1.xml.rels +4 -0
  93. docx/templates/default-docx-template/customXml/item1.xml +2 -0
  94. docx/templates/default-docx-template/customXml/itemProps1.xml +6 -0
  95. docx/templates/default-docx-template/docProps/app.xml +36 -0
  96. docx/templates/default-docx-template/docProps/core.xml +13 -0
  97. docx/templates/default-docx-template/docProps/thumbnail.jpeg +0 -0
  98. docx/templates/default-docx-template/word/_rels/document.xml.rels +11 -0
  99. docx/templates/default-docx-template/word/document.xml +11 -0
  100. docx/templates/default-docx-template/word/fontTable.xml +61 -0
  101. docx/templates/default-docx-template/word/numbering.xml +201 -0
  102. docx/templates/default-docx-template/word/settings.xml +53 -0
  103. docx/templates/default-docx-template/word/styles.xml +11844 -0
  104. docx/templates/default-docx-template/word/stylesWithEffects.xml +11800 -0
  105. docx/templates/default-docx-template/word/theme/theme1.xml +318 -0
  106. docx/templates/default-docx-template/word/webSettings.xml +5 -0
  107. docx/templates/default-footer.xml +27 -0
  108. docx/templates/default-header.xml +27 -0
  109. docx/templates/default-settings.xml +26 -0
  110. docx/templates/default-styles.xml +190 -0
  111. docx/templates/default.docx +0 -0
  112. docx/text/__init__.py +0 -0
  113. docx/text/font.py +472 -0
  114. docx/text/hyperlink.py +121 -0
  115. docx/text/pagebreak.py +104 -0
  116. docx/text/paragraph.py +173 -0
  117. docx/text/parfmt.py +286 -0
  118. docx/text/run.py +257 -0
  119. docx/text/tabstops.py +123 -0
  120. docx/theme.py +67 -0
  121. docx/types.py +34 -0
  122. ps_python_docx-1.3.0.dist-info/METADATA +77 -0
  123. ps_python_docx-1.3.0.dist-info/RECORD +126 -0
  124. ps_python_docx-1.3.0.dist-info/WHEEL +5 -0
  125. ps_python_docx-1.3.0.dist-info/licenses/LICENSE +20 -0
  126. 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