Spaces:
Paused
Paused
| 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. | |
| """ | |
| def content_type(self): | |
| """Return the MIME type of this TIFF image, unconditionally the string | |
| ``image/tiff``.""" | |
| return MIME_TYPE.TIFF | |
| def default_ext(self): | |
| """Default filename extension, always 'tiff' for TIFF images.""" | |
| return "tiff" | |
| 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 | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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)) | |
| 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) | |
| 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 | |
| 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 | |
| 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) | |
| 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 | |
| def tag(self): | |
| """Short int code that identifies this IFD entry.""" | |
| return self._tag_code | |
| 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.""" | |
| 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.""" | |
| 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.""" | |
| 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.""" | |
| 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" | |