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.
Files changed (132) hide show
  1. docx/__init__.py +71 -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 +285 -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 +32 -0
  19. docx/image/bmp.py +43 -0
  20. docx/image/constants.py +172 -0
  21. docx/image/emf.py +46 -0
  22. docx/image/exceptions.py +13 -0
  23. docx/image/gif.py +38 -0
  24. docx/image/helpers.py +86 -0
  25. docx/image/image.py +237 -0
  26. docx/image/jpeg.py +469 -0
  27. docx/image/png.py +253 -0
  28. docx/image/svg.py +118 -0
  29. docx/image/tiff.py +289 -0
  30. docx/opc/__init__.py +0 -0
  31. docx/opc/constants.py +307 -0
  32. docx/opc/coreprops.py +142 -0
  33. docx/opc/customprops.py +59 -0
  34. docx/opc/exceptions.py +12 -0
  35. docx/opc/oxml.py +247 -0
  36. docx/opc/package.py +254 -0
  37. docx/opc/packuri.py +109 -0
  38. docx/opc/part.py +267 -0
  39. docx/opc/parts/__init__.py +0 -0
  40. docx/opc/parts/coreprops.py +48 -0
  41. docx/opc/parts/customprops.py +61 -0
  42. docx/opc/phys_pkg.py +119 -0
  43. docx/opc/pkgreader.py +254 -0
  44. docx/opc/pkgwriter.py +115 -0
  45. docx/opc/rel.py +169 -0
  46. docx/opc/shared.py +31 -0
  47. docx/opc/spec.py +24 -0
  48. docx/oxml/__init__.py +252 -0
  49. docx/oxml/comments.py +124 -0
  50. docx/oxml/coreprops.py +298 -0
  51. docx/oxml/customprops.py +141 -0
  52. docx/oxml/dml/CT_ExtensionList.py +48 -0
  53. docx/oxml/document.py +88 -0
  54. docx/oxml/drawing.py +11 -0
  55. docx/oxml/exceptions.py +10 -0
  56. docx/oxml/ns.py +111 -0
  57. docx/oxml/numbering.py +109 -0
  58. docx/oxml/parser.py +62 -0
  59. docx/oxml/section.py +537 -0
  60. docx/oxml/settings.py +138 -0
  61. docx/oxml/shape.py +426 -0
  62. docx/oxml/shared.py +52 -0
  63. docx/oxml/simpletypes.py +434 -0
  64. docx/oxml/styles.py +320 -0
  65. docx/oxml/table.py +977 -0
  66. docx/oxml/text/__init__.py +0 -0
  67. docx/oxml/text/font.py +358 -0
  68. docx/oxml/text/hyperlink.py +45 -0
  69. docx/oxml/text/pagebreak.py +278 -0
  70. docx/oxml/text/paragraph.py +106 -0
  71. docx/oxml/text/parfmt.py +392 -0
  72. docx/oxml/text/run.py +316 -0
  73. docx/oxml/xmlchemy.py +711 -0
  74. docx/package.py +119 -0
  75. docx/parts/__init__.py +0 -0
  76. docx/parts/comments.py +51 -0
  77. docx/parts/customxml.py +67 -0
  78. docx/parts/document.py +193 -0
  79. docx/parts/hdrftr.py +53 -0
  80. docx/parts/image.py +80 -0
  81. docx/parts/numbering.py +51 -0
  82. docx/parts/settings.py +50 -0
  83. docx/parts/story.py +113 -0
  84. docx/parts/styles.py +42 -0
  85. docx/py.typed +0 -0
  86. docx/section.py +479 -0
  87. docx/settings.py +35 -0
  88. docx/shape.py +103 -0
  89. docx/shared.py +382 -0
  90. docx/styles/__init__.py +40 -0
  91. docx/styles/latent.py +198 -0
  92. docx/styles/style.py +254 -0
  93. docx/styles/styles.py +136 -0
  94. docx/table.py +537 -0
  95. docx/templates/default-comments.xml +12 -0
  96. docx/templates/default-docx-template/[Content_Types].xml +17 -0
  97. docx/templates/default-docx-template/_rels/.rels +7 -0
  98. docx/templates/default-docx-template/customXml/_rels/item1.xml.rels +4 -0
  99. docx/templates/default-docx-template/customXml/item1.xml +2 -0
  100. docx/templates/default-docx-template/customXml/itemProps1.xml +6 -0
  101. docx/templates/default-docx-template/docProps/app.xml +36 -0
  102. docx/templates/default-docx-template/docProps/core.xml +13 -0
  103. docx/templates/default-docx-template/docProps/thumbnail.jpeg +0 -0
  104. docx/templates/default-docx-template/word/_rels/document.xml.rels +11 -0
  105. docx/templates/default-docx-template/word/document.xml +11 -0
  106. docx/templates/default-docx-template/word/fontTable.xml +61 -0
  107. docx/templates/default-docx-template/word/numbering.xml +201 -0
  108. docx/templates/default-docx-template/word/settings.xml +53 -0
  109. docx/templates/default-docx-template/word/styles.xml +11844 -0
  110. docx/templates/default-docx-template/word/stylesWithEffects.xml +11800 -0
  111. docx/templates/default-docx-template/word/theme/theme1.xml +318 -0
  112. docx/templates/default-docx-template/word/webSettings.xml +5 -0
  113. docx/templates/default-footer.xml +27 -0
  114. docx/templates/default-header.xml +27 -0
  115. docx/templates/default-numbering.xml +20 -0
  116. docx/templates/default-settings.xml +26 -0
  117. docx/templates/default-styles.xml +190 -0
  118. docx/templates/default.docx +0 -0
  119. docx/text/__init__.py +0 -0
  120. docx/text/font.py +466 -0
  121. docx/text/hyperlink.py +121 -0
  122. docx/text/pagebreak.py +104 -0
  123. docx/text/paragraph.py +173 -0
  124. docx/text/parfmt.py +286 -0
  125. docx/text/run.py +272 -0
  126. docx/text/tabstops.py +123 -0
  127. docx/types.py +34 -0
  128. python_docx_oss-0.1.0.dist-info/METADATA +57 -0
  129. python_docx_oss-0.1.0.dist-info/RECORD +132 -0
  130. python_docx_oss-0.1.0.dist-info/WHEEL +5 -0
  131. python_docx_oss-0.1.0.dist-info/licenses/LICENSE +21 -0
  132. 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