# encoding: utf-8 from __future__ import absolute_import, division, print_function 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(object): """ 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[TIFF_TAG.RESOLUTION_UNIT] if TIFF_TAG.RESOLUTION_UNIT in ifd_entries else 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(object): """ 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 = dict((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(object): """ 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) if field_type in ifd_entry_classes: entry_cls = ifd_entry_classes[field_type] else: entry_cls = _IfdEntry return entry_cls.from_stream(stream_rdr, offset) class _IfdEntry(object): """ 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'