aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/docx/image
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/docx/image
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/docx/image')
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/__init__.py23
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/bmp.py43
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/constants.py172
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/exceptions.py13
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/gif.py38
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/helpers.py86
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/image.py234
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/jpeg.py429
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/png.py253
-rw-r--r--.venv/lib/python3.12/site-packages/docx/image/tiff.py289
10 files changed, 1580 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/docx/image/__init__.py b/.venv/lib/python3.12/site-packages/docx/image/__init__.py
new file mode 100644
index 00000000..d28033ef
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/__init__.py
@@ -0,0 +1,23 @@
+"""Provides objects that can characterize image streams.
+
+That characterization is as to content type and size, as a required step in including
+them in a document.
+"""
+
+from docx.image.bmp import Bmp
+from docx.image.gif import Gif
+from docx.image.jpeg import Exif, Jfif
+from docx.image.png import Png
+from docx.image.tiff import Tiff
+
+SIGNATURES = (
+ # class, offset, signature_bytes
+ (Png, 0, b"\x89PNG\x0D\x0A\x1A\x0A"),
+ (Jfif, 6, b"JFIF"),
+ (Exif, 6, b"Exif"),
+ (Gif, 0, b"GIF87a"),
+ (Gif, 0, b"GIF89a"),
+ (Tiff, 0, b"MM\x00*"), # big-endian (Motorola) TIFF
+ (Tiff, 0, b"II*\x00"), # little-endian (Intel) TIFF
+ (Bmp, 0, b"BM"),
+)
diff --git a/.venv/lib/python3.12/site-packages/docx/image/bmp.py b/.venv/lib/python3.12/site-packages/docx/image/bmp.py
new file mode 100644
index 00000000..115b01d5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/bmp.py
@@ -0,0 +1,43 @@
+from .constants import MIME_TYPE
+from .helpers import LITTLE_ENDIAN, StreamReader
+from .image import BaseImageHeader
+
+
+class Bmp(BaseImageHeader):
+ """Image header parser for BMP images."""
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return |Bmp| instance having header properties parsed from the BMP image in
+ `stream`."""
+ stream_rdr = StreamReader(stream, LITTLE_ENDIAN)
+
+ px_width = stream_rdr.read_long(0x12)
+ px_height = stream_rdr.read_long(0x16)
+
+ horz_px_per_meter = stream_rdr.read_long(0x26)
+ vert_px_per_meter = stream_rdr.read_long(0x2A)
+
+ horz_dpi = cls._dpi(horz_px_per_meter)
+ vert_dpi = cls._dpi(vert_px_per_meter)
+
+ return cls(px_width, px_height, horz_dpi, vert_dpi)
+
+ @property
+ def content_type(self):
+ """MIME content type for this image, unconditionally `image/bmp` for BMP
+ images."""
+ return MIME_TYPE.BMP
+
+ @property
+ def default_ext(self):
+ """Default filename extension, always 'bmp' for BMP images."""
+ return "bmp"
+
+ @staticmethod
+ def _dpi(px_per_meter):
+ """Return the integer pixels per inch from `px_per_meter`, defaulting to 96 if
+ `px_per_meter` is zero."""
+ if px_per_meter == 0:
+ return 96
+ return int(round(px_per_meter * 0.0254))
diff --git a/.venv/lib/python3.12/site-packages/docx/image/constants.py b/.venv/lib/python3.12/site-packages/docx/image/constants.py
new file mode 100644
index 00000000..729a828b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/constants.py
@@ -0,0 +1,172 @@
+"""Constants specific the the image sub-package."""
+
+
+class JPEG_MARKER_CODE:
+ """JPEG marker codes."""
+
+ TEM = b"\x01"
+ DHT = b"\xC4"
+ DAC = b"\xCC"
+ JPG = b"\xC8"
+
+ SOF0 = b"\xC0"
+ SOF1 = b"\xC1"
+ SOF2 = b"\xC2"
+ SOF3 = b"\xC3"
+ SOF5 = b"\xC5"
+ SOF6 = b"\xC6"
+ SOF7 = b"\xC7"
+ SOF9 = b"\xC9"
+ SOFA = b"\xCA"
+ SOFB = b"\xCB"
+ SOFD = b"\xCD"
+ SOFE = b"\xCE"
+ SOFF = b"\xCF"
+
+ RST0 = b"\xD0"
+ RST1 = b"\xD1"
+ RST2 = b"\xD2"
+ RST3 = b"\xD3"
+ RST4 = b"\xD4"
+ RST5 = b"\xD5"
+ RST6 = b"\xD6"
+ RST7 = b"\xD7"
+
+ SOI = b"\xD8"
+ EOI = b"\xD9"
+ SOS = b"\xDA"
+ DQT = b"\xDB" # Define Quantization Table(s)
+ DNL = b"\xDC"
+ DRI = b"\xDD"
+ DHP = b"\xDE"
+ EXP = b"\xDF"
+
+ APP0 = b"\xE0"
+ APP1 = b"\xE1"
+ APP2 = b"\xE2"
+ APP3 = b"\xE3"
+ APP4 = b"\xE4"
+ APP5 = b"\xE5"
+ APP6 = b"\xE6"
+ APP7 = b"\xE7"
+ APP8 = b"\xE8"
+ APP9 = b"\xE9"
+ APPA = b"\xEA"
+ APPB = b"\xEB"
+ APPC = b"\xEC"
+ APPD = b"\xED"
+ APPE = b"\xEE"
+ APPF = b"\xEF"
+
+ STANDALONE_MARKERS = (TEM, SOI, EOI, RST0, RST1, RST2, RST3, RST4, RST5, RST6, RST7)
+
+ SOF_MARKER_CODES = (
+ SOF0,
+ SOF1,
+ SOF2,
+ SOF3,
+ SOF5,
+ SOF6,
+ SOF7,
+ SOF9,
+ SOFA,
+ SOFB,
+ SOFD,
+ SOFE,
+ SOFF,
+ )
+
+ marker_names = {
+ b"\x00": "UNKNOWN",
+ b"\xC0": "SOF0",
+ b"\xC2": "SOF2",
+ b"\xC4": "DHT",
+ b"\xDA": "SOS", # start of scan
+ b"\xD8": "SOI", # start of image
+ b"\xD9": "EOI", # end of image
+ b"\xDB": "DQT",
+ b"\xE0": "APP0",
+ b"\xE1": "APP1",
+ b"\xE2": "APP2",
+ b"\xED": "APP13",
+ b"\xEE": "APP14",
+ }
+
+ @classmethod
+ def is_standalone(cls, marker_code):
+ return marker_code in cls.STANDALONE_MARKERS
+
+
+class MIME_TYPE:
+ """Image content types."""
+
+ BMP = "image/bmp"
+ GIF = "image/gif"
+ JPEG = "image/jpeg"
+ PNG = "image/png"
+ TIFF = "image/tiff"
+
+
+class PNG_CHUNK_TYPE:
+ """PNG chunk type names."""
+
+ IHDR = "IHDR"
+ pHYs = "pHYs"
+ IEND = "IEND"
+
+
+class TIFF_FLD_TYPE:
+ """Tag codes for TIFF Image File Directory (IFD) entries."""
+
+ BYTE = 1
+ ASCII = 2
+ SHORT = 3
+ LONG = 4
+ RATIONAL = 5
+
+ field_type_names = {
+ 1: "BYTE",
+ 2: "ASCII char",
+ 3: "SHORT",
+ 4: "LONG",
+ 5: "RATIONAL",
+ }
+
+
+TIFF_FLD = TIFF_FLD_TYPE
+
+
+class TIFF_TAG:
+ """Tag codes for TIFF Image File Directory (IFD) entries."""
+
+ IMAGE_WIDTH = 0x0100
+ IMAGE_LENGTH = 0x0101
+ X_RESOLUTION = 0x011A
+ Y_RESOLUTION = 0x011B
+ RESOLUTION_UNIT = 0x0128
+
+ tag_names = {
+ 0x00FE: "NewSubfileType",
+ 0x0100: "ImageWidth",
+ 0x0101: "ImageLength",
+ 0x0102: "BitsPerSample",
+ 0x0103: "Compression",
+ 0x0106: "PhotometricInterpretation",
+ 0x010E: "ImageDescription",
+ 0x010F: "Make",
+ 0x0110: "Model",
+ 0x0111: "StripOffsets",
+ 0x0112: "Orientation",
+ 0x0115: "SamplesPerPixel",
+ 0x0117: "StripByteCounts",
+ 0x011A: "XResolution",
+ 0x011B: "YResolution",
+ 0x011C: "PlanarConfiguration",
+ 0x0128: "ResolutionUnit",
+ 0x0131: "Software",
+ 0x0132: "DateTime",
+ 0x0213: "YCbCrPositioning",
+ 0x8769: "ExifTag",
+ 0x8825: "GPS IFD",
+ 0xC4A5: "PrintImageMatching",
+ }
diff --git a/.venv/lib/python3.12/site-packages/docx/image/exceptions.py b/.venv/lib/python3.12/site-packages/docx/image/exceptions.py
new file mode 100644
index 00000000..2b35187d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/exceptions.py
@@ -0,0 +1,13 @@
+"""Exceptions specific the the image sub-package."""
+
+
+class InvalidImageStreamError(Exception):
+ """The recognized image stream appears to be corrupted."""
+
+
+class UnexpectedEndOfFileError(Exception):
+ """EOF was unexpectedly encountered while reading an image stream."""
+
+
+class UnrecognizedImageError(Exception):
+ """The provided image stream could not be recognized."""
diff --git a/.venv/lib/python3.12/site-packages/docx/image/gif.py b/.venv/lib/python3.12/site-packages/docx/image/gif.py
new file mode 100644
index 00000000..e1648726
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/gif.py
@@ -0,0 +1,38 @@
+from struct import Struct
+
+from .constants import MIME_TYPE
+from .image import BaseImageHeader
+
+
+class Gif(BaseImageHeader):
+ """Image header parser for GIF images.
+
+ Note that the GIF format does not support resolution (DPI) information. Both
+ horizontal and vertical DPI default to 72.
+ """
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return |Gif| instance having header properties parsed from GIF image in
+ `stream`."""
+ px_width, px_height = cls._dimensions_from_stream(stream)
+ return cls(px_width, px_height, 72, 72)
+
+ @property
+ def content_type(self):
+ """MIME content type for this image, unconditionally `image/gif` for GIF
+ images."""
+ return MIME_TYPE.GIF
+
+ @property
+ def default_ext(self):
+ """Default filename extension, always 'gif' for GIF images."""
+ return "gif"
+
+ @classmethod
+ def _dimensions_from_stream(cls, stream):
+ stream.seek(6)
+ bytes_ = stream.read(4)
+ struct = Struct("<HH")
+ px_width, px_height = struct.unpack(bytes_)
+ return px_width, px_height
diff --git a/.venv/lib/python3.12/site-packages/docx/image/helpers.py b/.venv/lib/python3.12/site-packages/docx/image/helpers.py
new file mode 100644
index 00000000..647b3085
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/helpers.py
@@ -0,0 +1,86 @@
+from struct import Struct
+
+from .exceptions import UnexpectedEndOfFileError
+
+BIG_ENDIAN = ">"
+LITTLE_ENDIAN = "<"
+
+
+class StreamReader:
+ """Wraps a file-like object to provide access to structured data from a binary file.
+
+ Byte-order is configurable. `base_offset` is added to any base value provided to
+ calculate actual location for reads.
+ """
+
+ def __init__(self, stream, byte_order, base_offset=0):
+ super(StreamReader, self).__init__()
+ self._stream = stream
+ self._byte_order = LITTLE_ENDIAN if byte_order == LITTLE_ENDIAN else BIG_ENDIAN
+ self._base_offset = base_offset
+
+ def read(self, count):
+ """Allow pass-through read() call."""
+ return self._stream.read(count)
+
+ def read_byte(self, base, offset=0):
+ """Return the int value of the byte at the file position defined by
+ self._base_offset + `base` + `offset`.
+
+ If `base` is None, the byte is read from the current position in the stream.
+ """
+ fmt = "B"
+ return self._read_int(fmt, base, offset)
+
+ def read_long(self, base, offset=0):
+ """Return the int value of the four bytes at the file position defined by
+ self._base_offset + `base` + `offset`.
+
+ If `base` is None, the long is read from the current position in the stream. The
+ endian setting of this instance is used to interpret the byte layout of the
+ long.
+ """
+ fmt = "<L" if self._byte_order is LITTLE_ENDIAN else ">L"
+ return self._read_int(fmt, base, offset)
+
+ def read_short(self, base, offset=0):
+ """Return the int value of the two bytes at the file position determined by
+ `base` and `offset`, similarly to ``read_long()`` above."""
+ fmt = b"<H" if self._byte_order is LITTLE_ENDIAN else b">H"
+ return self._read_int(fmt, base, offset)
+
+ def read_str(self, char_count, base, offset=0):
+ """Return a string containing the `char_count` bytes at the file position
+ determined by self._base_offset + `base` + `offset`."""
+
+ def str_struct(char_count):
+ format_ = "%ds" % char_count
+ return Struct(format_)
+
+ struct = str_struct(char_count)
+ chars = self._unpack_item(struct, base, offset)
+ unicode_str = chars.decode("UTF-8")
+ return unicode_str
+
+ def seek(self, base, offset=0):
+ location = self._base_offset + base + offset
+ self._stream.seek(location)
+
+ def tell(self):
+ """Allow pass-through tell() call."""
+ return self._stream.tell()
+
+ def _read_bytes(self, byte_count, base, offset):
+ self.seek(base, offset)
+ bytes_ = self._stream.read(byte_count)
+ if len(bytes_) < byte_count:
+ raise UnexpectedEndOfFileError
+ return bytes_
+
+ def _read_int(self, fmt, base, offset):
+ struct = Struct(fmt)
+ return self._unpack_item(struct, base, offset)
+
+ def _unpack_item(self, struct, base, offset):
+ bytes_ = self._read_bytes(struct.size, base, offset)
+ return struct.unpack(bytes_)[0]
diff --git a/.venv/lib/python3.12/site-packages/docx/image/image.py b/.venv/lib/python3.12/site-packages/docx/image/image.py
new file mode 100644
index 00000000..0022b5b4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/image.py
@@ -0,0 +1,234 @@
+"""Provides objects that can characterize image streams.
+
+That characterization is as to content type and size, as a required step in including
+them in a document.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import io
+import os
+from typing import IO, Tuple
+
+from docx.image.exceptions import UnrecognizedImageError
+from docx.shared import Emu, Inches, Length, lazyproperty
+
+
+class Image:
+ """Graphical image stream such as JPEG, PNG, or GIF with properties and methods
+ required by ImagePart."""
+
+ def __init__(self, blob: bytes, filename: str, image_header: BaseImageHeader):
+ super(Image, self).__init__()
+ self._blob = blob
+ self._filename = filename
+ self._image_header = image_header
+
+ @classmethod
+ def from_blob(cls, blob: bytes) -> Image:
+ """Return a new |Image| subclass instance parsed from the image binary contained
+ in `blob`."""
+ stream = io.BytesIO(blob)
+ return cls._from_stream(stream, blob)
+
+ @classmethod
+ def from_file(cls, image_descriptor: str | IO[bytes]):
+ """Return a new |Image| subclass instance loaded from the image file identified
+ by `image_descriptor`, a path or file-like object."""
+ if isinstance(image_descriptor, str):
+ path = image_descriptor
+ with open(path, "rb") as f:
+ blob = f.read()
+ stream = io.BytesIO(blob)
+ filename = os.path.basename(path)
+ else:
+ stream = image_descriptor
+ stream.seek(0)
+ blob = stream.read()
+ filename = None
+ return cls._from_stream(stream, blob, filename)
+
+ @property
+ def blob(self):
+ """The bytes of the image 'file'."""
+ return self._blob
+
+ @property
+ def content_type(self) -> str:
+ """MIME content type for this image, e.g. ``'image/jpeg'`` for a JPEG image."""
+ return self._image_header.content_type
+
+ @lazyproperty
+ def ext(self):
+ """The file extension for the image.
+
+ If an actual one is available from a load filename it is used. Otherwise a
+ canonical extension is assigned based on the content type. Does not contain the
+ leading period, e.g. 'jpg', not '.jpg'.
+ """
+ return os.path.splitext(self._filename)[1][1:]
+
+ @property
+ def filename(self):
+ """Original image file name, if loaded from disk, or a generic filename if
+ loaded from an anonymous stream."""
+ return self._filename
+
+ @property
+ def px_width(self) -> int:
+ """The horizontal pixel dimension of the image."""
+ return self._image_header.px_width
+
+ @property
+ def px_height(self) -> int:
+ """The vertical pixel dimension of the image."""
+ return self._image_header.px_height
+
+ @property
+ def horz_dpi(self) -> int:
+ """Integer dots per inch for the width of this image.
+
+ Defaults to 72 when not present in the file, as is often the case.
+ """
+ return self._image_header.horz_dpi
+
+ @property
+ def vert_dpi(self) -> int:
+ """Integer dots per inch for the height of this image.
+
+ Defaults to 72 when not present in the file, as is often the case.
+ """
+ return self._image_header.vert_dpi
+
+ @property
+ def width(self) -> Inches:
+ """A |Length| value representing the native width of the image, calculated from
+ the values of `px_width` and `horz_dpi`."""
+ return Inches(self.px_width / self.horz_dpi)
+
+ @property
+ def height(self) -> Inches:
+ """A |Length| value representing the native height of the image, calculated from
+ the values of `px_height` and `vert_dpi`."""
+ return Inches(self.px_height / self.vert_dpi)
+
+ def scaled_dimensions(
+ self, width: int | Length | None = None, height: int | Length | None = None
+ ) -> Tuple[Length, Length]:
+ """(cx, cy) pair representing scaled dimensions of this image.
+
+ The native dimensions of the image are scaled by applying the following rules to
+ the `width` and `height` arguments.
+
+ * If both `width` and `height` are specified, the return value is (`width`,
+ `height`); no scaling is performed.
+ * If only one is specified, it is used to compute a scaling factor that is then
+ applied to the unspecified dimension, preserving the aspect ratio of the image.
+ * If both `width` and `height` are |None|, the native dimensions are returned.
+
+ The native dimensions are calculated using the dots-per-inch (dpi) value
+ embedded in the image, defaulting to 72 dpi if no value is specified, as is
+ often the case. The returned values are both |Length| objects.
+ """
+ if width is None and height is None:
+ return self.width, self.height
+
+ if width is None:
+ assert height is not None
+ scaling_factor = float(height) / float(self.height)
+ width = round(self.width * scaling_factor)
+
+ if height is None:
+ scaling_factor = float(width) / float(self.width)
+ height = round(self.height * scaling_factor)
+
+ return Emu(width), Emu(height)
+
+ @lazyproperty
+ def sha1(self):
+ """SHA1 hash digest of the image blob."""
+ return hashlib.sha1(self._blob).hexdigest()
+
+ @classmethod
+ def _from_stream(
+ cls,
+ stream: IO[bytes],
+ blob: bytes,
+ filename: str | None = None,
+ ) -> Image:
+ """Return an instance of the |Image| subclass corresponding to the format of the
+ image in `stream`."""
+ image_header = _ImageHeaderFactory(stream)
+ if filename is None:
+ filename = "image.%s" % image_header.default_ext
+ return cls(blob, filename, image_header)
+
+
+def _ImageHeaderFactory(stream: IO[bytes]):
+ """A |BaseImageHeader| subclass instance that can parse headers of image in `stream`."""
+ from docx.image import SIGNATURES
+
+ def read_32(stream: IO[bytes]):
+ stream.seek(0)
+ return stream.read(32)
+
+ header = read_32(stream)
+ for cls, offset, signature_bytes in SIGNATURES:
+ end = offset + len(signature_bytes)
+ found_bytes = header[offset:end]
+ if found_bytes == signature_bytes:
+ return cls.from_stream(stream)
+ raise UnrecognizedImageError
+
+
+class BaseImageHeader:
+ """Base class for image header subclasses like |Jpeg| and |Tiff|."""
+
+ def __init__(self, px_width: int, px_height: int, horz_dpi: int, vert_dpi: int):
+ self._px_width = px_width
+ self._px_height = px_height
+ self._horz_dpi = horz_dpi
+ self._vert_dpi = vert_dpi
+
+ @property
+ def content_type(self) -> str:
+ """Abstract property definition, must be implemented by all subclasses."""
+ msg = "content_type property must be implemented by all subclasses of " "BaseImageHeader"
+ raise NotImplementedError(msg)
+
+ @property
+ def default_ext(self) -> str:
+ """Default filename extension for images of this type.
+
+ An abstract property definition, must be implemented by all subclasses.
+ """
+ raise NotImplementedError(
+ "default_ext property must be implemented by all subclasses of " "BaseImageHeader"
+ )
+
+ @property
+ def px_width(self):
+ """The horizontal pixel dimension of the image."""
+ return self._px_width
+
+ @property
+ def px_height(self):
+ """The vertical pixel dimension of the image."""
+ return self._px_height
+
+ @property
+ def horz_dpi(self):
+ """Integer dots per inch for the width of this image.
+
+ Defaults to 72 when not present in the file, as is often the case.
+ """
+ return self._horz_dpi
+
+ @property
+ def vert_dpi(self):
+ """Integer dots per inch for the height of this image.
+
+ Defaults to 72 when not present in the file, as is often the case.
+ """
+ return self._vert_dpi
diff --git a/.venv/lib/python3.12/site-packages/docx/image/jpeg.py b/.venv/lib/python3.12/site-packages/docx/image/jpeg.py
new file mode 100644
index 00000000..b0114a99
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/jpeg.py
@@ -0,0 +1,429 @@
+"""Objects related to parsing headers of JPEG image streams.
+
+Includes both JFIF and Exif sub-formats.
+"""
+
+import io
+
+from docx.image.constants import JPEG_MARKER_CODE, MIME_TYPE
+from docx.image.helpers import BIG_ENDIAN, StreamReader
+from docx.image.image import BaseImageHeader
+from docx.image.tiff import Tiff
+
+
+class Jpeg(BaseImageHeader):
+ """Base class for JFIF and EXIF subclasses."""
+
+ @property
+ def content_type(self):
+ """MIME content type for this image, unconditionally `image/jpeg` for JPEG
+ images."""
+ return MIME_TYPE.JPEG
+
+ @property
+ def default_ext(self):
+ """Default filename extension, always 'jpg' for JPG images."""
+ return "jpg"
+
+
+class Exif(Jpeg):
+ """Image header parser for Exif image format."""
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return |Exif| instance having header properties parsed from Exif image in
+ `stream`."""
+ markers = _JfifMarkers.from_stream(stream)
+ # print('\n%s' % markers)
+
+ px_width = markers.sof.px_width
+ px_height = markers.sof.px_height
+ horz_dpi = markers.app1.horz_dpi
+ vert_dpi = markers.app1.vert_dpi
+
+ return cls(px_width, px_height, horz_dpi, vert_dpi)
+
+
+class Jfif(Jpeg):
+ """Image header parser for JFIF image format."""
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return a |Jfif| instance having header properties parsed from image in
+ `stream`."""
+ markers = _JfifMarkers.from_stream(stream)
+
+ px_width = markers.sof.px_width
+ px_height = markers.sof.px_height
+ horz_dpi = markers.app0.horz_dpi
+ vert_dpi = markers.app0.vert_dpi
+
+ return cls(px_width, px_height, horz_dpi, vert_dpi)
+
+
+class _JfifMarkers:
+ """Sequence of markers in a JPEG file, perhaps truncated at first SOS marker for
+ performance reasons."""
+
+ def __init__(self, markers):
+ super(_JfifMarkers, self).__init__()
+ self._markers = list(markers)
+
+ def __str__(self): # pragma: no cover
+ """Returns a tabular listing of the markers in this instance, which can be handy
+ for debugging and perhaps other uses."""
+ header = " offset seglen mc name\n======= ====== == ====="
+ tmpl = "%7d %6d %02X %s"
+ rows = []
+ for marker in self._markers:
+ rows.append(
+ tmpl
+ % (
+ marker.offset,
+ marker.segment_length,
+ ord(marker.marker_code),
+ marker.name,
+ )
+ )
+ lines = [header] + rows
+ return "\n".join(lines)
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return a |_JfifMarkers| instance containing a |_JfifMarker| subclass instance
+ for each marker in `stream`."""
+ marker_parser = _MarkerParser.from_stream(stream)
+ markers = []
+ for marker in marker_parser.iter_markers():
+ markers.append(marker)
+ if marker.marker_code == JPEG_MARKER_CODE.SOS:
+ break
+ return cls(markers)
+
+ @property
+ def app0(self):
+ """First APP0 marker in image markers."""
+ for m in self._markers:
+ if m.marker_code == JPEG_MARKER_CODE.APP0:
+ return m
+ raise KeyError("no APP0 marker in image")
+
+ @property
+ def app1(self):
+ """First APP1 marker in image markers."""
+ for m in self._markers:
+ if m.marker_code == JPEG_MARKER_CODE.APP1:
+ return m
+ raise KeyError("no APP1 marker in image")
+
+ @property
+ def sof(self):
+ """First start of frame (SOFn) marker in this sequence."""
+ for m in self._markers:
+ if m.marker_code in JPEG_MARKER_CODE.SOF_MARKER_CODES:
+ return m
+ raise KeyError("no start of frame (SOFn) marker in image")
+
+
+class _MarkerParser:
+ """Service class that knows how to parse a JFIF stream and iterate over its
+ markers."""
+
+ def __init__(self, stream_reader):
+ super(_MarkerParser, self).__init__()
+ self._stream = stream_reader
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return a |_MarkerParser| instance to parse JFIF markers from `stream`."""
+ stream_reader = StreamReader(stream, BIG_ENDIAN)
+ return cls(stream_reader)
+
+ def iter_markers(self):
+ """Generate a (marker_code, segment_offset) 2-tuple for each marker in the JPEG
+ `stream`, in the order they occur in the stream."""
+ marker_finder = _MarkerFinder.from_stream(self._stream)
+ start = 0
+ marker_code = None
+ while marker_code != JPEG_MARKER_CODE.EOI:
+ marker_code, segment_offset = marker_finder.next(start)
+ marker = _MarkerFactory(marker_code, self._stream, segment_offset)
+ yield marker
+ start = segment_offset + marker.segment_length
+
+
+class _MarkerFinder:
+ """Service class that knows how to find the next JFIF marker in a stream."""
+
+ def __init__(self, stream):
+ super(_MarkerFinder, self).__init__()
+ self._stream = stream
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return a |_MarkerFinder| instance to find JFIF markers in `stream`."""
+ return cls(stream)
+
+ def next(self, start):
+ """Return a (marker_code, segment_offset) 2-tuple identifying and locating the
+ first marker in `stream` occuring after offset `start`.
+
+ The returned `segment_offset` points to the position immediately following the
+ 2-byte marker code, the start of the marker segment, for those markers that have
+ a segment.
+ """
+ position = start
+ while True:
+ # skip over any non-\xFF bytes
+ position = self._offset_of_next_ff_byte(start=position)
+ # skip over any \xFF padding bytes
+ position, byte_ = self._next_non_ff_byte(start=position + 1)
+ # 'FF 00' sequence is not a marker, start over if found
+ if byte_ == b"\x00":
+ continue
+ # this is a marker, gather return values and break out of scan
+ marker_code, segment_offset = byte_, position + 1
+ break
+ return marker_code, segment_offset
+
+ def _next_non_ff_byte(self, start):
+ """Return an offset, byte 2-tuple for the next byte in `stream` that is not
+ '\xFF', starting with the byte at offset `start`.
+
+ If the byte at offset `start` is not '\xFF', `start` and the returned `offset`
+ will be the same.
+ """
+ self._stream.seek(start)
+ byte_ = self._read_byte()
+ while byte_ == b"\xFF":
+ byte_ = self._read_byte()
+ offset_of_non_ff_byte = self._stream.tell() - 1
+ return offset_of_non_ff_byte, byte_
+
+ def _offset_of_next_ff_byte(self, start):
+ """Return the offset of the next '\xFF' byte in `stream` starting with the byte
+ at offset `start`.
+
+ Returns `start` if the byte at that offset is a hex 255; it does not necessarily
+ advance in the stream.
+ """
+ self._stream.seek(start)
+ byte_ = self._read_byte()
+ while byte_ != b"\xFF":
+ byte_ = self._read_byte()
+ offset_of_ff_byte = self._stream.tell() - 1
+ return offset_of_ff_byte
+
+ def _read_byte(self):
+ """Return the next byte read from stream.
+
+ Raise Exception if stream is at end of file.
+ """
+ byte_ = self._stream.read(1)
+ if not byte_: # pragma: no cover
+ raise Exception("unexpected end of file")
+ return byte_
+
+
+def _MarkerFactory(marker_code, stream, offset):
+ """Return |_Marker| or subclass instance appropriate for marker at `offset` in
+ `stream` having `marker_code`."""
+ if marker_code == JPEG_MARKER_CODE.APP0:
+ marker_cls = _App0Marker
+ elif marker_code == JPEG_MARKER_CODE.APP1:
+ marker_cls = _App1Marker
+ elif marker_code in JPEG_MARKER_CODE.SOF_MARKER_CODES:
+ marker_cls = _SofMarker
+ else:
+ marker_cls = _Marker
+ return marker_cls.from_stream(stream, marker_code, offset)
+
+
+class _Marker:
+ """Base class for JFIF marker classes.
+
+ Represents a marker and its segment occuring in a JPEG byte stream.
+ """
+
+ def __init__(self, marker_code, offset, segment_length):
+ super(_Marker, self).__init__()
+ self._marker_code = marker_code
+ self._offset = offset
+ self._segment_length = segment_length
+
+ @classmethod
+ def from_stream(cls, stream, marker_code, offset):
+ """Return a generic |_Marker| instance for the marker at `offset` in `stream`
+ having `marker_code`."""
+ if JPEG_MARKER_CODE.is_standalone(marker_code):
+ segment_length = 0
+ else:
+ segment_length = stream.read_short(offset)
+ return cls(marker_code, offset, segment_length)
+
+ @property
+ def marker_code(self):
+ """The single-byte code that identifies the type of this marker, e.g. ``'\xE0'``
+ for start of image (SOI)."""
+ return self._marker_code
+
+ @property
+ def name(self): # pragma: no cover
+ return JPEG_MARKER_CODE.marker_names[self._marker_code]
+
+ @property
+ def offset(self): # pragma: no cover
+ return self._offset
+
+ @property
+ def segment_length(self):
+ """The length in bytes of this marker's segment."""
+ return self._segment_length
+
+
+class _App0Marker(_Marker):
+ """Represents a JFIF APP0 marker segment."""
+
+ def __init__(
+ self, marker_code, offset, length, density_units, x_density, y_density
+ ):
+ super(_App0Marker, self).__init__(marker_code, offset, length)
+ self._density_units = density_units
+ self._x_density = x_density
+ self._y_density = y_density
+
+ @property
+ def horz_dpi(self):
+ """Horizontal dots per inch specified in this marker, defaults to 72 if not
+ specified."""
+ return self._dpi(self._x_density)
+
+ @property
+ def vert_dpi(self):
+ """Vertical dots per inch specified in this marker, defaults to 72 if not
+ specified."""
+ return self._dpi(self._y_density)
+
+ def _dpi(self, density):
+ """Return dots per inch corresponding to `density` value."""
+ if self._density_units == 1:
+ dpi = density
+ elif self._density_units == 2:
+ dpi = int(round(density * 2.54))
+ else:
+ dpi = 72
+ return dpi
+
+ @classmethod
+ def from_stream(cls, stream, marker_code, offset):
+ """Return an |_App0Marker| instance for the APP0 marker at `offset` in
+ `stream`."""
+ # field off type notes
+ # ------------------ --- ----- -------------------
+ # segment length 0 short
+ # JFIF identifier 2 5 chr 'JFIF\x00'
+ # major JPEG version 7 byte typically 1
+ # minor JPEG version 8 byte typically 1 or 2
+ # density units 9 byte 1=inches, 2=cm
+ # horz dots per unit 10 short
+ # vert dots per unit 12 short
+ # ------------------ --- ----- -------------------
+ segment_length = stream.read_short(offset)
+ density_units = stream.read_byte(offset, 9)
+ x_density = stream.read_short(offset, 10)
+ y_density = stream.read_short(offset, 12)
+ return cls(
+ marker_code, offset, segment_length, density_units, x_density, y_density
+ )
+
+
+class _App1Marker(_Marker):
+ """Represents a JFIF APP1 (Exif) marker segment."""
+
+ def __init__(self, marker_code, offset, length, horz_dpi, vert_dpi):
+ super(_App1Marker, self).__init__(marker_code, offset, length)
+ self._horz_dpi = horz_dpi
+ self._vert_dpi = vert_dpi
+
+ @classmethod
+ def from_stream(cls, stream, marker_code, offset):
+ """Extract the horizontal and vertical dots-per-inch value from the APP1 header
+ at `offset` in `stream`."""
+ # field off len type notes
+ # -------------------- --- --- ----- ----------------------------
+ # segment length 0 2 short
+ # Exif identifier 2 6 6 chr 'Exif\x00\x00'
+ # TIFF byte order 8 2 2 chr 'II'=little 'MM'=big endian
+ # meaning of universe 10 2 2 chr '*\x00' or '\x00*' depending
+ # IFD0 off fr/II or MM 10 16 long relative to ...?
+ # -------------------- --- --- ----- ----------------------------
+ segment_length = stream.read_short(offset)
+ if cls._is_non_Exif_APP1_segment(stream, offset):
+ return cls(marker_code, offset, segment_length, 72, 72)
+ tiff = cls._tiff_from_exif_segment(stream, offset, segment_length)
+ return cls(marker_code, offset, segment_length, tiff.horz_dpi, tiff.vert_dpi)
+
+ @property
+ def horz_dpi(self):
+ """Horizontal dots per inch specified in this marker, defaults to 72 if not
+ specified."""
+ return self._horz_dpi
+
+ @property
+ def vert_dpi(self):
+ """Vertical dots per inch specified in this marker, defaults to 72 if not
+ specified."""
+ return self._vert_dpi
+
+ @classmethod
+ def _is_non_Exif_APP1_segment(cls, stream, offset):
+ """Return True if the APP1 segment at `offset` in `stream` is NOT an Exif
+ segment, as determined by the ``'Exif\x00\x00'`` signature at offset 2 in the
+ segment."""
+ stream.seek(offset + 2)
+ exif_signature = stream.read(6)
+ return exif_signature != b"Exif\x00\x00"
+
+ @classmethod
+ def _tiff_from_exif_segment(cls, stream, offset, segment_length):
+ """Return a |Tiff| instance parsed from the Exif APP1 segment of
+ `segment_length` at `offset` in `stream`."""
+ # wrap full segment in its own stream and feed to Tiff()
+ stream.seek(offset + 8)
+ segment_bytes = stream.read(segment_length - 8)
+ substream = io.BytesIO(segment_bytes)
+ return Tiff.from_stream(substream)
+
+
+class _SofMarker(_Marker):
+ """Represents a JFIF start of frame (SOFx) marker segment."""
+
+ def __init__(self, marker_code, offset, segment_length, px_width, px_height):
+ super(_SofMarker, self).__init__(marker_code, offset, segment_length)
+ self._px_width = px_width
+ self._px_height = px_height
+
+ @classmethod
+ def from_stream(cls, stream, marker_code, offset):
+ """Return an |_SofMarker| instance for the SOFn marker at `offset` in stream."""
+ # field off type notes
+ # ------------------ --- ----- ----------------------------
+ # segment length 0 short
+ # Data precision 2 byte
+ # Vertical lines 3 short px_height
+ # Horizontal lines 5 short px_width
+ # ------------------ --- ----- ----------------------------
+ segment_length = stream.read_short(offset)
+ px_height = stream.read_short(offset, 3)
+ px_width = stream.read_short(offset, 5)
+ return cls(marker_code, offset, segment_length, px_width, px_height)
+
+ @property
+ def px_height(self):
+ """Image height in pixels."""
+ return self._px_height
+
+ @property
+ def px_width(self):
+ """Image width in pixels."""
+ return self._px_width
diff --git a/.venv/lib/python3.12/site-packages/docx/image/png.py b/.venv/lib/python3.12/site-packages/docx/image/png.py
new file mode 100644
index 00000000..dd3cf819
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/png.py
@@ -0,0 +1,253 @@
+from .constants import MIME_TYPE, PNG_CHUNK_TYPE
+from .exceptions import InvalidImageStreamError
+from .helpers import BIG_ENDIAN, StreamReader
+from .image import BaseImageHeader
+
+
+class Png(BaseImageHeader):
+ """Image header parser for PNG images."""
+
+ @property
+ def content_type(self):
+ """MIME content type for this image, unconditionally `image/png` for PNG
+ images."""
+ return MIME_TYPE.PNG
+
+ @property
+ def default_ext(self):
+ """Default filename extension, always 'png' for PNG images."""
+ return "png"
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return a |Png| instance having header properties parsed from image in
+ `stream`."""
+ parser = _PngParser.parse(stream)
+
+ px_width = parser.px_width
+ px_height = parser.px_height
+ horz_dpi = parser.horz_dpi
+ vert_dpi = parser.vert_dpi
+
+ return cls(px_width, px_height, horz_dpi, vert_dpi)
+
+
+class _PngParser:
+ """Parses a PNG image stream to extract the image properties found in its chunks."""
+
+ def __init__(self, chunks):
+ super(_PngParser, self).__init__()
+ self._chunks = chunks
+
+ @classmethod
+ def parse(cls, stream):
+ """Return a |_PngParser| instance containing the header properties parsed from
+ the PNG image in `stream`."""
+ chunks = _Chunks.from_stream(stream)
+ return cls(chunks)
+
+ @property
+ def px_width(self):
+ """The number of pixels in each row of the image."""
+ IHDR = self._chunks.IHDR
+ return IHDR.px_width
+
+ @property
+ def px_height(self):
+ """The number of stacked rows of pixels in the image."""
+ IHDR = self._chunks.IHDR
+ return IHDR.px_height
+
+ @property
+ def horz_dpi(self):
+ """Integer dots per inch for the width of this image.
+
+ Defaults to 72 when not present in the file, as is often the case.
+ """
+ pHYs = self._chunks.pHYs
+ if pHYs is None:
+ return 72
+ return self._dpi(pHYs.units_specifier, pHYs.horz_px_per_unit)
+
+ @property
+ def vert_dpi(self):
+ """Integer dots per inch for the height of this image.
+
+ Defaults to 72 when not present in the file, as is often the case.
+ """
+ pHYs = self._chunks.pHYs
+ if pHYs is None:
+ return 72
+ return self._dpi(pHYs.units_specifier, pHYs.vert_px_per_unit)
+
+ @staticmethod
+ def _dpi(units_specifier, px_per_unit):
+ """Return dots per inch value calculated from `units_specifier` and
+ `px_per_unit`."""
+ if units_specifier == 1 and px_per_unit:
+ return int(round(px_per_unit * 0.0254))
+ return 72
+
+
+class _Chunks:
+ """Collection of the chunks parsed from a PNG image stream."""
+
+ def __init__(self, chunk_iterable):
+ super(_Chunks, self).__init__()
+ self._chunks = list(chunk_iterable)
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return a |_Chunks| instance containing the PNG chunks in `stream`."""
+ chunk_parser = _ChunkParser.from_stream(stream)
+ chunks = list(chunk_parser.iter_chunks())
+ return cls(chunks)
+
+ @property
+ def IHDR(self):
+ """IHDR chunk in PNG image."""
+ match = lambda chunk: chunk.type_name == PNG_CHUNK_TYPE.IHDR # noqa
+ IHDR = self._find_first(match)
+ if IHDR is None:
+ raise InvalidImageStreamError("no IHDR chunk in PNG image")
+ return IHDR
+
+ @property
+ def pHYs(self):
+ """PHYs chunk in PNG image, or |None| if not present."""
+ match = lambda chunk: chunk.type_name == PNG_CHUNK_TYPE.pHYs # noqa
+ return self._find_first(match)
+
+ def _find_first(self, match):
+ """Return first chunk in stream order returning True for function `match`."""
+ for chunk in self._chunks:
+ if match(chunk):
+ return chunk
+ return None
+
+
+class _ChunkParser:
+ """Extracts chunks from a PNG image stream."""
+
+ def __init__(self, stream_rdr):
+ super(_ChunkParser, self).__init__()
+ self._stream_rdr = stream_rdr
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return a |_ChunkParser| instance that can extract the chunks from the PNG
+ image in `stream`."""
+ stream_rdr = StreamReader(stream, BIG_ENDIAN)
+ return cls(stream_rdr)
+
+ def iter_chunks(self):
+ """Generate a |_Chunk| subclass instance for each chunk in this parser's PNG
+ stream, in the order encountered in the stream."""
+ for chunk_type, offset in self._iter_chunk_offsets():
+ chunk = _ChunkFactory(chunk_type, self._stream_rdr, offset)
+ yield chunk
+
+ def _iter_chunk_offsets(self):
+ """Generate a (chunk_type, chunk_offset) 2-tuple for each of the chunks in the
+ PNG image stream.
+
+ Iteration stops after the IEND chunk is returned.
+ """
+ chunk_offset = 8
+ while True:
+ chunk_data_len = self._stream_rdr.read_long(chunk_offset)
+ chunk_type = self._stream_rdr.read_str(4, chunk_offset, 4)
+ data_offset = chunk_offset + 8
+ yield chunk_type, data_offset
+ if chunk_type == "IEND":
+ break
+ # incr offset for chunk len long, chunk type, chunk data, and CRC
+ chunk_offset += 4 + 4 + chunk_data_len + 4
+
+
+def _ChunkFactory(chunk_type, stream_rdr, offset):
+ """Return a |_Chunk| subclass instance appropriate to `chunk_type` parsed from
+ `stream_rdr` at `offset`."""
+ chunk_cls_map = {
+ PNG_CHUNK_TYPE.IHDR: _IHDRChunk,
+ PNG_CHUNK_TYPE.pHYs: _pHYsChunk,
+ }
+ chunk_cls = chunk_cls_map.get(chunk_type, _Chunk)
+ return chunk_cls.from_offset(chunk_type, stream_rdr, offset)
+
+
+class _Chunk:
+ """Base class for specific chunk types.
+
+ Also serves as the default chunk type.
+ """
+
+ def __init__(self, chunk_type):
+ super(_Chunk, self).__init__()
+ self._chunk_type = chunk_type
+
+ @classmethod
+ def from_offset(cls, chunk_type, stream_rdr, offset):
+ """Return a default _Chunk instance that only knows its chunk type."""
+ return cls(chunk_type)
+
+ @property
+ def type_name(self):
+ """The chunk type name, e.g. 'IHDR', 'pHYs', etc."""
+ return self._chunk_type
+
+
+class _IHDRChunk(_Chunk):
+ """IHDR chunk, contains the image dimensions."""
+
+ def __init__(self, chunk_type, px_width, px_height):
+ super(_IHDRChunk, self).__init__(chunk_type)
+ self._px_width = px_width
+ self._px_height = px_height
+
+ @classmethod
+ def from_offset(cls, chunk_type, stream_rdr, offset):
+ """Return an _IHDRChunk instance containing the image dimensions extracted from
+ the IHDR chunk in `stream` at `offset`."""
+ px_width = stream_rdr.read_long(offset)
+ px_height = stream_rdr.read_long(offset, 4)
+ return cls(chunk_type, px_width, px_height)
+
+ @property
+ def px_width(self):
+ return self._px_width
+
+ @property
+ def px_height(self):
+ return self._px_height
+
+
+class _pHYsChunk(_Chunk):
+ """PYHs chunk, contains the image dpi information."""
+
+ def __init__(self, chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifier):
+ super(_pHYsChunk, self).__init__(chunk_type)
+ self._horz_px_per_unit = horz_px_per_unit
+ self._vert_px_per_unit = vert_px_per_unit
+ self._units_specifier = units_specifier
+
+ @classmethod
+ def from_offset(cls, chunk_type, stream_rdr, offset):
+ """Return a _pHYsChunk instance containing the image resolution extracted from
+ the pHYs chunk in `stream` at `offset`."""
+ horz_px_per_unit = stream_rdr.read_long(offset)
+ vert_px_per_unit = stream_rdr.read_long(offset, 4)
+ units_specifier = stream_rdr.read_byte(offset, 8)
+ return cls(chunk_type, horz_px_per_unit, vert_px_per_unit, units_specifier)
+
+ @property
+ def horz_px_per_unit(self):
+ return self._horz_px_per_unit
+
+ @property
+ def vert_px_per_unit(self):
+ return self._vert_px_per_unit
+
+ @property
+ def units_specifier(self):
+ return self._units_specifier
diff --git a/.venv/lib/python3.12/site-packages/docx/image/tiff.py b/.venv/lib/python3.12/site-packages/docx/image/tiff.py
new file mode 100644
index 00000000..1194929a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/docx/image/tiff.py
@@ -0,0 +1,289 @@
+from .constants import MIME_TYPE, TIFF_FLD, TIFF_TAG
+from .helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader
+from .image import BaseImageHeader
+
+
+class Tiff(BaseImageHeader):
+ """Image header parser for TIFF images.
+
+ Handles both big and little endian byte ordering.
+ """
+
+ @property
+ def content_type(self):
+ """Return the MIME type of this TIFF image, unconditionally the string
+ ``image/tiff``."""
+ return MIME_TYPE.TIFF
+
+ @property
+ def default_ext(self):
+ """Default filename extension, always 'tiff' for TIFF images."""
+ return "tiff"
+
+ @classmethod
+ def from_stream(cls, stream):
+ """Return a |Tiff| instance containing the properties of the TIFF image in
+ `stream`."""
+ parser = _TiffParser.parse(stream)
+
+ px_width = parser.px_width
+ px_height = parser.px_height
+ horz_dpi = parser.horz_dpi
+ vert_dpi = parser.vert_dpi
+
+ return cls(px_width, px_height, horz_dpi, vert_dpi)
+
+
+class _TiffParser:
+ """Parses a TIFF image stream to extract the image properties found in its main
+ image file directory (IFD)"""
+
+ def __init__(self, ifd_entries):
+ super(_TiffParser, self).__init__()
+ self._ifd_entries = ifd_entries
+
+ @classmethod
+ def parse(cls, stream):
+ """Return an instance of |_TiffParser| containing the properties parsed from the
+ TIFF image in `stream`."""
+ stream_rdr = cls._make_stream_reader(stream)
+ ifd0_offset = stream_rdr.read_long(4)
+ ifd_entries = _IfdEntries.from_stream(stream_rdr, ifd0_offset)
+ return cls(ifd_entries)
+
+ @property
+ def horz_dpi(self):
+ """The horizontal dots per inch value calculated from the XResolution and
+ ResolutionUnit tags of the IFD; defaults to 72 if those tags are not present."""
+ return self._dpi(TIFF_TAG.X_RESOLUTION)
+
+ @property
+ def vert_dpi(self):
+ """The vertical dots per inch value calculated from the XResolution and
+ ResolutionUnit tags of the IFD; defaults to 72 if those tags are not present."""
+ return self._dpi(TIFF_TAG.Y_RESOLUTION)
+
+ @property
+ def px_height(self):
+ """The number of stacked rows of pixels in the image, |None| if the IFD contains
+ no ``ImageLength`` tag, the expected case when the TIFF is embeded in an Exif
+ image."""
+ return self._ifd_entries.get(TIFF_TAG.IMAGE_LENGTH)
+
+ @property
+ def px_width(self):
+ """The number of pixels in each row in the image, |None| if the IFD contains no
+ ``ImageWidth`` tag, the expected case when the TIFF is embeded in an Exif
+ image."""
+ return self._ifd_entries.get(TIFF_TAG.IMAGE_WIDTH)
+
+ @classmethod
+ def _detect_endian(cls, stream):
+ """Return either BIG_ENDIAN or LITTLE_ENDIAN depending on the endian indicator
+ found in the TIFF `stream` header, either 'MM' or 'II'."""
+ stream.seek(0)
+ endian_str = stream.read(2)
+ return BIG_ENDIAN if endian_str == b"MM" else LITTLE_ENDIAN
+
+ def _dpi(self, resolution_tag):
+ """Return the dpi value calculated for `resolution_tag`, which can be either
+ TIFF_TAG.X_RESOLUTION or TIFF_TAG.Y_RESOLUTION.
+
+ The calculation is based on the values of both that tag and the
+ TIFF_TAG.RESOLUTION_UNIT tag in this parser's |_IfdEntries| instance.
+ """
+ ifd_entries = self._ifd_entries
+
+ if resolution_tag not in ifd_entries:
+ return 72
+
+ # resolution unit defaults to inches (2)
+ resolution_unit = ifd_entries.get(TIFF_TAG.RESOLUTION_UNIT, 2)
+
+ if resolution_unit == 1: # aspect ratio only
+ return 72
+ # resolution_unit == 2 for inches, 3 for centimeters
+ units_per_inch = 1 if resolution_unit == 2 else 2.54
+ dots_per_unit = ifd_entries[resolution_tag]
+ return int(round(dots_per_unit * units_per_inch))
+
+ @classmethod
+ def _make_stream_reader(cls, stream):
+ """Return a |StreamReader| instance with wrapping `stream` and having "endian-
+ ness" determined by the 'MM' or 'II' indicator in the TIFF stream header."""
+ endian = cls._detect_endian(stream)
+ return StreamReader(stream, endian)
+
+
+class _IfdEntries:
+ """Image File Directory for a TIFF image, having mapping (dict) semantics allowing
+ "tag" values to be retrieved by tag code."""
+
+ def __init__(self, entries):
+ super(_IfdEntries, self).__init__()
+ self._entries = entries
+
+ def __contains__(self, key):
+ """Provides ``in`` operator, e.g. ``tag in ifd_entries``"""
+ return self._entries.__contains__(key)
+
+ def __getitem__(self, key):
+ """Provides indexed access, e.g. ``tag_value = ifd_entries[tag_code]``"""
+ return self._entries.__getitem__(key)
+
+ @classmethod
+ def from_stream(cls, stream, offset):
+ """Return a new |_IfdEntries| instance parsed from `stream` starting at
+ `offset`."""
+ ifd_parser = _IfdParser(stream, offset)
+ entries = {e.tag: e.value for e in ifd_parser.iter_entries()}
+ return cls(entries)
+
+ def get(self, tag_code, default=None):
+ """Return value of IFD entry having tag matching `tag_code`, or `default` if no
+ matching tag found."""
+ return self._entries.get(tag_code, default)
+
+
+class _IfdParser:
+ """Service object that knows how to extract directory entries from an Image File
+ Directory (IFD)"""
+
+ def __init__(self, stream_rdr, offset):
+ super(_IfdParser, self).__init__()
+ self._stream_rdr = stream_rdr
+ self._offset = offset
+
+ def iter_entries(self):
+ """Generate an |_IfdEntry| instance corresponding to each entry in the
+ directory."""
+ for idx in range(self._entry_count):
+ dir_entry_offset = self._offset + 2 + (idx * 12)
+ ifd_entry = _IfdEntryFactory(self._stream_rdr, dir_entry_offset)
+ yield ifd_entry
+
+ @property
+ def _entry_count(self):
+ """The count of directory entries, read from the top of the IFD header."""
+ return self._stream_rdr.read_short(self._offset)
+
+
+def _IfdEntryFactory(stream_rdr, offset):
+ """Return an |_IfdEntry| subclass instance containing the value of the directory
+ entry at `offset` in `stream_rdr`."""
+ ifd_entry_classes = {
+ TIFF_FLD.ASCII: _AsciiIfdEntry,
+ TIFF_FLD.SHORT: _ShortIfdEntry,
+ TIFF_FLD.LONG: _LongIfdEntry,
+ TIFF_FLD.RATIONAL: _RationalIfdEntry,
+ }
+ field_type = stream_rdr.read_short(offset, 2)
+ EntryCls = ifd_entry_classes.get(field_type, _IfdEntry)
+ return EntryCls.from_stream(stream_rdr, offset)
+
+
+class _IfdEntry:
+ """Base class for IFD entry classes.
+
+ Subclasses are differentiated by value type, e.g. ASCII, long int, etc.
+ """
+
+ def __init__(self, tag_code, value):
+ super(_IfdEntry, self).__init__()
+ self._tag_code = tag_code
+ self._value = value
+
+ @classmethod
+ def from_stream(cls, stream_rdr, offset):
+ """Return an |_IfdEntry| subclass instance containing the tag and value of the
+ tag parsed from `stream_rdr` at `offset`.
+
+ Note this method is common to all subclasses. Override the ``_parse_value()``
+ method to provide distinctive behavior based on field type.
+ """
+ tag_code = stream_rdr.read_short(offset, 0)
+ value_count = stream_rdr.read_long(offset, 4)
+ value_offset = stream_rdr.read_long(offset, 8)
+ value = cls._parse_value(stream_rdr, offset, value_count, value_offset)
+ return cls(tag_code, value)
+
+ @classmethod
+ def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
+ """Return the value of this field parsed from `stream_rdr` at `offset`.
+
+ Intended to be overridden by subclasses.
+ """
+ return "UNIMPLEMENTED FIELD TYPE" # pragma: no cover
+
+ @property
+ def tag(self):
+ """Short int code that identifies this IFD entry."""
+ return self._tag_code
+
+ @property
+ def value(self):
+ """Value of this tag, its type being dependent on the tag."""
+ return self._value
+
+
+class _AsciiIfdEntry(_IfdEntry):
+ """IFD entry having the form of a NULL-terminated ASCII string."""
+
+ @classmethod
+ def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
+ """Return the ASCII string parsed from `stream_rdr` at `value_offset`.
+
+ The length of the string, including a terminating '\x00' (NUL) character, is in
+ `value_count`.
+ """
+ return stream_rdr.read_str(value_count - 1, value_offset)
+
+
+class _ShortIfdEntry(_IfdEntry):
+ """IFD entry expressed as a short (2-byte) integer."""
+
+ @classmethod
+ def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
+ """Return the short int value contained in the `value_offset` field of this
+ entry.
+
+ Only supports single values at present.
+ """
+ if value_count == 1:
+ return stream_rdr.read_short(offset, 8)
+ else: # pragma: no cover
+ return "Multi-value short integer NOT IMPLEMENTED"
+
+
+class _LongIfdEntry(_IfdEntry):
+ """IFD entry expressed as a long (4-byte) integer."""
+
+ @classmethod
+ def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
+ """Return the long int value contained in the `value_offset` field of this
+ entry.
+
+ Only supports single values at present.
+ """
+ if value_count == 1:
+ return stream_rdr.read_long(offset, 8)
+ else: # pragma: no cover
+ return "Multi-value long integer NOT IMPLEMENTED"
+
+
+class _RationalIfdEntry(_IfdEntry):
+ """IFD entry expressed as a numerator, denominator pair."""
+
+ @classmethod
+ def _parse_value(cls, stream_rdr, offset, value_count, value_offset):
+ """Return the rational (numerator / denominator) value at `value_offset` in
+ `stream_rdr` as a floating-point number.
+
+ Only supports single values at present.
+ """
+ if value_count == 1:
+ numerator = stream_rdr.read_long(value_offset)
+ denominator = stream_rdr.read_long(value_offset, 4)
+ return numerator / denominator
+ else: # pragma: no cover
+ return "Multi-value Rational NOT IMPLEMENTED"