class ExifTag(ListenerInterface): """ An EXIF tag. Here is a correspondance table between the EXIF types and the possible python types the value of a tag may take: - Ascii: :class:`datetime.datetime`, :class:`datetime.date`, string - Byte, SByte: string - Comment: string - Long, SLong: [list of] long - Short, SShort: [list of] int - Rational, SRational: [list of] :class:`fractions.Fraction` if available (Python ≥ 2.6) or :class:`pyexiv2.utils.Rational` - Undefined: string """ # According to the EXIF specification, the only accepted format for an Ascii # value representing a datetime is '%Y:%m:%d %H:%M:%S', but it seems that # others formats can be found in the wild. _datetime_formats = ('%Y:%m:%d %H:%M:%S', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%SZ') _date_formats = ('%Y:%m:%d',) def __init__(self, key, value=None, _tag=None): """ The tag can be initialized with an optional value which expected type depends on the EXIF type of the tag. :param key: the key of the tag :type key: string :param value: the value of the tag """ super(ExifTag, self).__init__() if _tag is not None: self._tag = _tag else: self._tag = libexiv2python._ExifTag(key) self._raw_value = None self._value = None self._value_cookie = False if value is not None: self._set_value(value) def _set_owner(self, metadata): self._tag._setParentImage(metadata._image) @staticmethod def _from_existing_tag(_tag): # Build a tag from an already existing libexiv2python._ExifTag. tag = ExifTag(_tag._getKey(), _tag=_tag) # Do not set the raw_value property, as it would call _tag._setRawValue # (see https://bugs.launchpad.net/pyexiv2/+bug/582445). tag._raw_value = _tag._getRawValue() tag._value_cookie = True return tag @property def key(self): """The key of the tag in the dotted form ``familyName.groupName.tagName`` where ``familyName`` = ``exif``.""" return self._tag._getKey() @property def type(self): """The EXIF type of the tag (one of Ascii, Byte, SByte, Comment, Short, SShort, Long, SLong, Rational, SRational, Undefined).""" return self._tag._getType() @property def name(self): """The name of the tag (this is also the third part of the key).""" return self._tag._getName() @property def label(self): """The title (label) of the tag.""" return self._tag._getLabel() @property def description(self): """The description of the tag.""" return self._tag._getDescription() @property def section_name(self): """The name of the tag's section.""" return self._tag._getSectionName() @property def section_description(self): """The description of the tag's section.""" return self._tag._getSectionDescription() def _get_raw_value(self): return self._raw_value def _set_raw_value(self, value): self._tag._setRawValue(value) self._raw_value = value self._value_cookie = True raw_value = property(fget=_get_raw_value, fset=_set_raw_value, doc='The raw value of the tag as a string.') def _compute_value(self): # Lazy computation of the value from the raw value. if self.type in \ ('Short', 'SShort', 'Long', 'SLong', 'Rational', 'SRational'): # May contain multiple values values = self._raw_value.split() if len(values) > 1: # Make values a notifying list values = map(self._convert_to_python, values) self._value = NotifyingList(values) self._value.register_listener(self) self._value_cookie = False return self._value = self._convert_to_python(self._raw_value) self._value_cookie = False def _get_value(self): if self._value_cookie: self._compute_value() return self._value def _set_value(self, value): if isinstance(value, (list, tuple)): raw_values = map(self._convert_to_string, value) self.raw_value = ' '.join(raw_values) else: self.raw_value = self._convert_to_string(value) if isinstance(self._value, NotifyingList): self._value.unregister_listener(self) if isinstance(value, NotifyingList): # Already a notifying list self._value = value self._value.register_listener(self) elif isinstance(value, (list, tuple)): # Make the values a notifying list self._value = NotifyingList(value) self._value.register_listener(self) else: # Single value self._value = value self._value_cookie = False value = property(fget=_get_value, fset=_set_value, doc='The value of the tag as a python object.') @property def human_value(self): """A (read-only) human-readable representation of the value of the tag.""" return self._tag._getHumanValue() or None def contents_changed(self): # Implementation of the ListenerInterface. # React on changes to the list of values of the tag. # self._value is a list of values and its contents changed. self._set_value(self._value) def _match_encoding(self, charset): encoding = sys.getdefaultencoding() if charset == 'Ascii': encoding = 'ascii' elif charset == 'Jis': encoding = 'shift_jis' elif charset == 'Unicode': # Starting from 0.20, exiv2 converts unicode comments to UTF-8 from pyexiv2 import __exiv2_version__ if __exiv2_version__ >= '0.20': encoding = 'utf-8' else: byte_order = self._tag._getByteOrder() if byte_order == 1: # little endian (II) encoding = 'utf-16le' elif byte_order == 2: # big endian (MM) encoding = 'utf-16be' elif charset == 'Undefined': pass elif charset == 'InvalidCharsetId': pass return encoding def _convert_to_python(self, value): """ Convert one raw value to its corresponding python type. :param value: the raw value to be converted :type value: string :return: the value converted to its corresponding python type :raise ExifValueError: if the conversion fails """ if self.type == 'Ascii': # The value may contain a Datetime for format in self._datetime_formats: try: t = time.strptime(value, format) except ValueError: continue else: return datetime.datetime(*t[:6]) # Or a Date (e.g. Exif.GPSInfo.GPSDateStamp) for format in self._date_formats: try: t = time.strptime(value, format) except ValueError: continue else: return datetime.date(*t[:3]) # Default to string. # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. return value elif self.type in ('Byte', 'SByte'): return value elif self.type == 'Comment': if value.startswith('charset='): charset, val = value.split(' ', 1) charset = charset.split('=')[1].strip('"') encoding = self._match_encoding(charset) return val.decode(encoding, 'replace') else: # No encoding defined. try: return value.decode('utf-8') except UnicodeError: return value elif self.type in ('Short', 'SShort'): try: return int(value) except ValueError: raise ExifValueError(value, self.type) elif self.type in ('Long', 'SLong'): try: return long(value) except ValueError: raise ExifValueError(value, self.type) elif self.type in ('Rational', 'SRational'): try: r = make_fraction(value) except (ValueError, ZeroDivisionError): raise ExifValueError(value, self.type) else: if self.type == 'Rational' and r.numerator < 0: raise ExifValueError(value, self.type) return r elif self.type == 'Undefined': # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. return undefined_to_string(value) raise ExifValueError(value, self.type) def _convert_to_string(self, value): """ Convert one value to its corresponding string representation, suitable to pass to libexiv2. :param value: the value to be converted :return: the value converted to its corresponding string representation :rtype: string :raise ExifValueError: if the conversion fails """ if self.type == 'Ascii': if isinstance(value, datetime.datetime): return value.strftime(self._datetime_formats[0]) elif isinstance(value, datetime.date): if self.key == 'Exif.GPSInfo.GPSDateStamp': # Special case return value.strftime(self._date_formats[0]) else: return value.strftime('%s 00:00:00' % self._date_formats[0]) elif isinstance(value, unicode): try: return value.encode('utf-8') except UnicodeEncodeError: raise ExifValueError(value, self.type) elif isinstance(value, str): return value else: raise ExifValueError(value, self.type) elif self.type in ('Byte', 'SByte'): if isinstance(value, unicode): try: return value.encode('utf-8') except UnicodeEncodeError: raise ExifValueError(value, self.type) elif isinstance(value, str): return value else: raise ExifValueError(value, self.type) elif self.type == 'Comment': if value is not None and self.raw_value is not None and \ self.raw_value.startswith('charset='): charset, val = self.raw_value.split(' ', 1) charset = charset.split('=')[1].strip('"') encoding = self._match_encoding(charset) try: val = value.encode(encoding) except UnicodeError: # Best effort, do not fail just because the original # encoding of the tag cannot encode the new value. pass else: return 'charset="%s" %s' % (charset, val) if isinstance(value, unicode): try: return value.encode('utf-8') except UnicodeEncodeError: raise ExifValueError(value, self.type) elif isinstance(value, str): return value else: raise ExifValueError(value, self.type) elif self.type == 'Short': if isinstance(value, int) and value >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SShort': if isinstance(value, int): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Long': if isinstance(value, (int, long)) and value >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SLong': if isinstance(value, (int, long)): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Rational': if is_fraction(value) and value.numerator >= 0: return fraction_to_string(value) else: raise ExifValueError(value, self.type) elif self.type == 'SRational': if is_fraction(value): return fraction_to_string(value) else: raise ExifValueError(value, self.type) elif self.type == 'Undefined': if isinstance(value, unicode): try: return string_to_undefined(value.encode('utf-8')) except UnicodeEncodeError: raise ExifValueError(value, self.type) elif isinstance(value, str): return string_to_undefined(value) else: raise ExifValueError(value, self.type) raise ExifValueError(value, self.type) def __str__(self): """ :return: a string representation of the EXIF tag for debugging purposes :rtype: string """ left = '%s [%s]' % (self.key, self.type) if self._raw_value is None: right = '(No value)' elif self.type == 'Undefined' and len(self._raw_value) > 100: right = '(Binary value suppressed)' else: right = self._raw_value return '<%s = %s>' % (left, right) # Support for pickling. def __getstate__(self): return (self.key, self.raw_value) def __setstate__(self, state): key, raw_value = state self._tag = libexiv2python._ExifTag(key) self.raw_value = raw_value
class IptcTag(ListenerInterface): """ An IPTC tag. This tag can have several values (tags that have the *repeatable* property). Here is a correspondance table between the IPTC types and the possible python types the value of a tag may take: - Short: int - String: string - Date: :class:`datetime.date` - Time: :class:`datetime.time` - Undefined: string """ # strptime is not flexible enough to handle all valid Time formats, we use a # custom regular expression _time_zone_re = r'(?P<sign>\+|-)(?P<ohours>\d{2}):(?P<ominutes>\d{2})' _time_re = re.compile(r'(?P<hours>\d{2}):(?P<minutes>\d{2}):(?P<seconds>\d{2})(?P<tzd>%s)' % _time_zone_re) def __init__(self, key, values=None, _tag=None): """ The tag can be initialized with an optional list of values which expected type depends on the IPTC type of the tag. :param key: the key of the tag :type key: string :param values: the values of the tag """ super(IptcTag, self).__init__() if _tag is not None: self._tag = _tag else: self._tag = libexiv2python._IptcTag(key) self._raw_values = None self._values = None self._values_cookie = False if values is not None: self._set_values(values) def _set_owner(self, metadata): self._tag._setParentImage(metadata._image) @staticmethod def _from_existing_tag(_tag): # Build a tag from an already existing libexiv2python._IptcTag tag = IptcTag(_tag._getKey(), _tag=_tag) # Do not set the raw_value property, as it would call # _tag._setRawValues # (see https://bugs.launchpad.net/pyexiv2/+bug/582445). tag._raw_values = _tag._getRawValues() tag._values_cookie = True return tag @property def key(self): """The key of the tag in the dotted form ``familyName.groupName.tagName`` where ``familyName`` = ``iptc``.""" return self._tag._getKey() @property def type(self): """The IPTC type of the tag (one of Short, String, Date, Time, Undefined).""" return self._tag._getType() @property def name(self): """The name of the tag (this is also the third part of the key).""" return self._tag._getName() @property def title(self): """The title (label) of the tag.""" return self._tag._getTitle() @property def description(self): """The description of the tag.""" return self._tag._getDescription() @property def photoshop_name(self): """The Photoshop name of the tag.""" return self._tag._getPhotoshopName() @property def repeatable(self): """Whether the tag is repeatable (accepts several values).""" return self._tag._isRepeatable() @property def record_name(self): """The name of the tag's record.""" return self._tag._getRecordName() @property def record_description(self): """The description of the tag's record.""" return self._tag._getRecordDescription() def _get_raw_values(self): return self._raw_values def _set_raw_values(self, values): if not isinstance(values, (list, tuple)): raise TypeError('Expecting a list of values') self._tag._setRawValues(values) self._raw_values = values self._values_cookie = True raw_value = property(fget=_get_raw_values, fset=_set_raw_values, doc='The raw values of the tag as a list of strings.') def _get_raw_values_deprecated(self): msg = "The 'raw_values' property is deprecated, " \ "use the 'raw_value' property instead." warnings.warn(msg, category=DeprecationWarning, stacklevel=2) return self._get_raw_values() def _set_raw_values_deprecated(self, values): msg = "The 'raw_values' property is deprecated, " \ "use the 'raw_value' property instead." warnings.warn(msg, category=DeprecationWarning, stacklevel=2) return self._set_raw_values(values) raw_values = property(fget=_get_raw_values_deprecated, fset=_set_raw_values_deprecated) def _compute_values(self): # Lazy computation of the values from the raw values self._values = \ NotifyingList(map(self._convert_to_python, self._raw_values)) self._values.register_listener(self) self._values_cookie = False def _get_values(self): if self._values_cookie: self._compute_values() return self._values def _set_values(self, values): if not isinstance(values, (list, tuple)): raise TypeError('Expecting a list of values') self.raw_value = map(self._convert_to_string, values) if isinstance(self._values, NotifyingList): self._values.unregister_listener(self) if isinstance(values, NotifyingList): # Already a notifying list self._values = values else: # Make the values a notifying list self._values = NotifyingList(values) self._values.register_listener(self) self._values_cookie = False value = property(fget=_get_values, fset=_set_values, doc='The values of the tag as a list of python objects.') def _get_values_deprecated(self): msg = "The 'values' property is deprecated, " \ "use the 'value' property instead." warnings.warn(msg, category=DeprecationWarning, stacklevel=2) return self._get_values() def _set_values_deprecated(self, values): msg = "The 'values' property is deprecated, " \ "use the 'value' property instead." warnings.warn(msg, category=DeprecationWarning, stacklevel=2) return self._set_values(values) values = property(fget=_get_values_deprecated, fset=_set_values_deprecated) def contents_changed(self): # Implementation of the ListenerInterface. # React on changes to the list of values of the tag. # The contents of self._values was changed. # The following is a quick, non optimal solution. self._set_values(self._values) def _convert_to_python(self, value): """ Convert one raw value to its corresponding python type. :param value: the raw value to be converted :type value: string :return: the value converted to its corresponding python type :raise IptcValueError: if the conversion fails """ if self.type == 'Short': try: return int(value) except ValueError: raise IptcValueError(value, self.type) elif self.type == 'String': # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. return value elif self.type == 'Date': # According to the IPTC specification, the format for a string field # representing a date is '%Y%m%d'. However, the string returned by # exiv2 using method DateValue::toString() is formatted using # pattern '%Y-%m-%d'. format = '%Y-%m-%d' try: t = time.strptime(value, format) return datetime.date(*t[:3]) except ValueError: raise IptcValueError(value, self.type) elif self.type == 'Time': # According to the IPTC specification, the format for a string field # representing a time is '%H%M%S±%H%M'. However, the string returned # by exiv2 using method TimeValue::toString() is formatted using # pattern '%H:%M:%S±%H:%M'. match = IptcTag._time_re.match(value) if match is None: raise IptcValueError(value, self.type) gd = match.groupdict() try: tzinfo = FixedOffset(gd['sign'], int(gd['ohours']), int(gd['ominutes'])) except TypeError: raise IptcValueError(value, self.type) try: return datetime.time(int(gd['hours']), int(gd['minutes']), int(gd['seconds']), tzinfo=tzinfo) except (TypeError, ValueError): raise IptcValueError(value, self.type) elif self.type == 'Undefined': # Binary data, return it unmodified return value raise IptcValueError(value, self.type) def _convert_to_string(self, value): """ Convert one value to its corresponding string representation, suitable to pass to libexiv2. :param value: the value to be converted :return: the value converted to its corresponding string representation :rtype: string :raise IptcValueError: if the conversion fails """ if self.type == 'Short': if isinstance(value, int): return str(value) else: raise IptcValueError(value, self.type) elif self.type == 'String': if isinstance(value, unicode): try: return value.encode('utf-8') except UnicodeEncodeError: raise IptcValueError(value, self.type) elif isinstance(value, str): return value else: raise IptcValueError(value, self.type) elif self.type == 'Date': if isinstance(value, (datetime.date, datetime.datetime)): # ISO 8601 date format. # According to the IPTC specification, the format for a string # field representing a date is '%Y%m%d'. However, the string # expected by exiv2's DateValue::read(string) should be # formatted using pattern '%Y-%m-%d'. return value.strftime('%Y-%m-%d') else: raise IptcValueError(value, self.type) elif self.type == 'Time': if isinstance(value, (datetime.time, datetime.datetime)): # According to the IPTC specification, the format for a string # field representing a time is '%H%M%S±%H%M'. However, the # string expected by exiv2's TimeValue::read(string) should be # formatted using pattern '%H:%M:%S±%H:%M'. r = value.strftime('%H:%M:%S') if value.tzinfo is not None and \ not (value.tzinfo.hours == 0 and value.tzinfo.minutes == 0): r += value.strftime('%Z') else: r += '+00:00' return r else: raise IptcValueError(value, self.type) elif self.type == 'Undefined': if isinstance(value, str): return value else: raise IptcValueError(value, self.type) raise IptcValueError(value, self.type) def __str__(self): """ :return: a string representation of the IPTC tag for debugging purposes :rtype: string """ left = '%s [%s]' % (self.key, self.type) if self._raw_values is None: right = '(No values)' else: right = self._raw_values return '<%s = %s>' % (left, right) # Support for pickling. def __getstate__(self): return (self.key, self.raw_value) def __setstate__(self, state): key, raw_value = state self._tag = libexiv2python._IptcTag(key) self.raw_value = raw_value
class IptcTag(ListenerInterface): """An IPTC tag. This tag can have several values (tags that have the *repeatable* property). Here is a correspondance table between the IPTC types and the possible python types the value of a tag may take: - Short: int - String: string - Date: :class:`datetime.date` - Time: :class:`datetime.time` - Undefined: string """ # strptime is not flexible enough to handle all valid Time formats, we use a # custom regular expression _time_zone_re = r'(?P<sign>\+|-)(?P<ohours>\d{2}):(?P<ominutes>\d{2})' _time_re = re.compile( r'(?P<hours>\d{2}):(?P<minutes>\d{2}):(?P<seconds>\d{2})(?P<tzd>%s)' % _time_zone_re) def __init__(self, key, values=None, _tag=None): """The tag can be initialized with an optional list of values which expected type depends on the IPTC type of the tag. Args: key -- the key of the tag values -- the values of the tag """ super(IptcTag, self).__init__() if _tag is not None: self._tag = _tag else: self._tag = libexiv2python._IptcTag(key) self._raw_values = None self._values = None self._values_cookie = False if values is not None: self._set_values(values) def _set_owner(self, metadata): self._tag._setParentImage(metadata._image) @staticmethod def _from_existing_tag(_tag): # Build a tag from an already existing libexiv2python._IptcTag tag = IptcTag(_tag._getKey(), _tag=_tag) # Do not set the raw_value property, as it would call # _tag._setRawValues # (see https://bugs.launchpad.net/pyexiv2/+bug/582445). tag._raw_values = _tag._getRawValues() tag._values_cookie = True return tag @property def key(self): """The key of the tag in the dotted form ``familyName.groupName.tagName`` where ``familyName`` = ``iptc``. """ return self._tag._getKey() @property def type(self): """The IPTC type of the tag (one of Short, String, Date, Time, Undefined). """ return self._tag._getType() @property def name(self): """The name of the tag (this is also the third part of the key). """ return self._tag._getName() @property def title(self): """The title (label) of the tag. """ return self._tag._getTitle() @property def description(self): """The description of the tag. """ return self._tag._getDescription() @property def photoshop_name(self): """The Photoshop name of the tag. """ return self._tag._getPhotoshopName() @property def repeatable(self): """Whether the tag is repeatable (accepts several values). """ return self._tag._isRepeatable() @property def record_name(self): """The name of the tag's record. """ return self._tag._getRecordName() @property def record_description(self): """The description of the tag's record. """ return self._tag._getRecordDescription() def _get_raw_values(self): return self._raw_values def _set_raw_values(self, values): if not isinstance(values, (list, tuple)): raise TypeError('Expecting a list of values') self._tag._setRawValues(values) self._raw_values = values self._values_cookie = True raw_value = property(fget=_get_raw_values, fset=_set_raw_values, doc='The raw values of the tag as a list of strings.') def _compute_values(self): # Lazy computation of the values from the raw values self._values = NotifyingList( [self._convert_to_python(v) for v in self._raw_values]) self._values.register_listener(self) self._values_cookie = False def _get_values(self): if self._values_cookie: self._compute_values() return self._values def _set_values(self, values): if not isinstance(values, (list, tuple)): raise TypeError('Expecting a list of values') self.raw_value = [self._convert_to_string(v) for v in values] if isinstance(self._values, NotifyingList): self._values.unregister_listener(self) if isinstance(values, NotifyingList): # Already a notifying list self._values = values else: # Make the values a notifying list self._values = NotifyingList(values) self._values.register_listener(self) self._values_cookie = False value = property(fget=_get_values, fset=_set_values, doc='The values of the tag as a list of python objects.') def contents_changed(self): # Implementation of the ListenerInterface. # React on changes to the list of values of the tag. # The contents of self._values was changed. # The following is a quick, non optimal solution. self._set_values(self._values) def _convert_to_python(self, value): """Convert one raw value to its corresponding python type. Args: value -- the raw value to be converted Return: the value converted to its corresponding python type Raise IptcValueError: if the conversion fails """ if self.type == 'Short': try: return int(value) except ValueError: raise IptcValueError(value, self.type) elif self.type == 'String': # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. if isinstance(value, bytes): try: value = value.decode('utf-8') except UnicodeDecodeError: # Unknow encoding, return the raw value pass return value elif self.type == 'Date': # According to the IPTC specification, the format for a string field # representing a date is '%Y%m%d'. However, the string returned by # exiv2 using method DateValue::toString() is formatted using # pattern '%Y-%m-%d'. format = '%Y-%m-%d' try: t = time.strptime(value, format) return datetime.date(*t[:3]) except ValueError: raise IptcValueError(value, self.type) elif self.type == 'Time': # According to the IPTC specification, the format for a string field # representing a time is '%H%M%S±%H%M'. However, the string returned # by exiv2 using method TimeValue::toString() is formatted using # pattern '%H:%M:%S±%H:%M'. match = IptcTag._time_re.match(value) if match is None: raise IptcValueError(value, self.type) gd = match.groupdict() try: tzinfo = FixedOffset(gd['sign'], int(gd['ohours']), int(gd['ominutes'])) except TypeError: raise IptcValueError(value, self.type) try: return datetime.time(int(gd['hours']), int(gd['minutes']), int(gd['seconds']), tzinfo=tzinfo) except (TypeError, ValueError): raise IptcValueError(value, self.type) elif self.type == 'Undefined': # Binary data, return it unmodified return value raise IptcValueError(value, self.type) def _convert_to_string(self, value): """Convert one value to its corresponding string representation, suitable to pass to libexiv2. Args: value -- the value to be converted Return: the value converted to its corresponding string representation Raise IptcValueError: if the conversion fails """ if self.type == 'Short': if isinstance(value, int): return str(value) else: raise IptcValueError(value, self.type) elif self.type == 'String': if isinstance(value, str): try: return value.encode('utf-8') except UnicodeEncodeError: raise IptcValueError(value, self.type) elif isinstance(value, bytes): return value else: raise IptcValueError(value, self.type) elif self.type == 'Date': if isinstance(value, (datetime.date, datetime.datetime)): return DateTimeFormatter.iptc_date(value) else: raise IptcValueError(value, self.type) elif self.type == 'Time': if isinstance(value, (datetime.time, datetime.datetime)): return DateTimeFormatter.iptc_time(value) else: raise IptcValueError(value, self.type) elif self.type == 'Undefined': if isinstance(value, str): return value else: raise IptcValueError(value, self.type) raise IptcValueError(value, self.type) def __str__(self): """Return a string representation of the IPTC tag for debugging purposes """ left = '%s [%s]' % (self.key, self.type) if self._raw_values is None: right = '(No values)' else: right = self._raw_values return '<%s = %s>' % (left, right) # Support for pickling. def __getstate__(self): return (self.key, self.raw_value) def __setstate__(self, state): key, raw_value = state self._tag = libexiv2python._IptcTag(key) self.raw_value = raw_value
class ExifTag(ListenerInterface): """An EXIF tag. Here is a correspondance table between the EXIF types and the possible python types the value of a tag may take: - Ascii: :class:`datetime.datetime`, :class:`datetime.date`, string - Byte, SByte: bytes - Comment: string - Long, SLong: [list of] int - Short, SShort: [list of] int - Rational, SRational: [list of] :class:`fractions.Fraction` if available (Python ≥ 2.6) or :class:`pyexiv2.utils.Rational` - Undefined: string """ # According to the EXIF specification, the only accepted format for an Ascii # value representing a datetime is '%Y:%m:%d %H:%M:%S', but it seems that # others formats can be found in the wild. _datetime_formats = ('%Y:%m:%d %H:%M:%S', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%SZ') _date_formats = ('%Y:%m:%d', ) def __init__(self, key, value=None, _tag=None): """ The tag can be initialized with an optional value which expected type depends on the EXIF type of the tag. Args: key -- the key of the tag value -- the value of the tag """ super().__init__() if _tag is not None: self._tag = _tag else: self._tag = libexiv2python._ExifTag(key) self._raw_value = None self._value = None self._value_cookie = False if value is not None: self._set_value(value) def _set_owner(self, metadata): self._tag._setParentImage(metadata._image) @staticmethod def _from_existing_tag(_tag): """Build a tag from an already existing libexiv2python._ExifTag. """ tag = ExifTag(_tag._getKey(), _tag=_tag) # Do not set the raw_value property, as it would call _tag._setRawValue # (see https://bugs.launchpad.net/pyexiv2/+bug/582445). tag._raw_value = _tag._getRawValue() tag._value_cookie = True return tag @property def key(self): """The key of the tag in the dotted form ``familyName.groupName.tagName`` where ``familyName`` = ``exif``. """ return self._tag._getKey() @property def type(self): """The EXIF type of the tag (one of Ascii, Byte, SByte, Comment, Short, SShort, Long, SLong, Rational, SRational, Undefined). """ return self._tag._getType() @property def name(self): """The name of the tag (this is also the third part of the key). """ return self._tag._getName() @property def label(self): """The title (label) of the tag. """ return self._tag._getLabel() @property def description(self): """The description of the tag. """ return self._tag._getDescription() @property def section_name(self): """The name of the tag's section. """ return self._tag._getSectionName() @property def section_description(self): """The description of the tag's section. """ return self._tag._getSectionDescription() def _get_raw_value(self): return self._raw_value def _set_raw_value(self, value): self._tag._setRawValue(value) self._raw_value = value self._value_cookie = True raw_value = property(fget=_get_raw_value, fset=_set_raw_value, doc='The raw value of the tag as a string.') def _compute_value(self): """Lazy computation of the value from the raw value. """ if self.type in ('Short', 'SShort', 'Long', 'SLong', 'Rational', 'SRational'): # May contain multiple values values = self._raw_value.split() if len(values) > 1: # Make values a notifying list values = [self._convert_to_python(v) for v in values] self._value = NotifyingList(values) self._value.register_listener(self) self._value_cookie = False return self._value = self._convert_to_python(self._raw_value) self._value_cookie = False def _get_value(self): if self._value_cookie: self._compute_value() return self._value def _set_value(self, value): if isinstance(value, (list, tuple)): raw_values = [self._convert_to_string(v) for v in value] self.raw_value = ' '.join(raw_values) else: self.raw_value = self._convert_to_string(value) if isinstance(self._value, NotifyingList): self._value.unregister_listener(self) if isinstance(value, NotifyingList): # Already a notifying list self._value = value self._value.register_listener(self) elif isinstance(value, (list, tuple)): # Make the values a notifying list self._value = NotifyingList(value) self._value.register_listener(self) else: # Single value self._value = value self._value_cookie = False value = property(fget=_get_value, fset=_set_value, doc='The value of the tag as a python object.') @property def human_value(self): """A (read-only) human-readable representation of the value of the tag. """ return self._tag._getHumanValue() or None def contents_changed(self): # Implementation of the ListenerInterface. # React on changes to the list of values of the tag. # self._value is a list of values and its contents changed. self._set_value(self._value) def _match_encoding(self, charset): # charset see: # http://www.exiv2.org/doc/classExiv2_1_1CommentValue.html # enum CharsetId { # ascii, jis, unicode, undefined, # invalidCharsetId, lastCharsetId } encoding = sys.getdefaultencoding() if charset in ('Ascii', 'ascii'): encoding = 'ascii' elif charset in ('Jis', 'jis'): encoding = 'shift_jis' elif charset in ('Unicode', 'unicode'): encoding = 'utf-8' return encoding def _convert_to_python(self, value): """ Convert one raw value to its corresponding python type. :param value: the raw value to be converted :type value: string :return: the value converted to its corresponding python type :raise ExifValueError: if the conversion fails """ if self.type == 'Ascii': # The value may contain a Datetime for format in self._datetime_formats: try: t = time.strptime(value, format) except ValueError: continue else: return datetime.datetime(*t[:6]) # Or a Date (e.g. Exif.GPSInfo.GPSDateStamp) for format in self._date_formats: try: t = time.strptime(value, format) except ValueError: continue else: return datetime.date(*t[:3]) # Default to string. # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. return value elif self.type in ('Byte', 'SByte'): if isinstance(value, bytes): return value.decode('utf-8') return value elif self.type == 'Comment': if isinstance(value, str): if value.startswith('charset='): charset, val = value.split(' ', 1) return val return value if value.startswith(b'charset='): charset = charset.split('=')[1].strip('"') encoding = self._match_encoding(charset) return val.decode(encoding, 'replace') else: # No encoding defined. try: return value.decode('utf-8') except UnicodeError: pass return value elif self.type in ('Short', 'SShort'): try: return int(value) except ValueError: raise ExifValueError(value, self.type) elif self.type in ('Long', 'SLong'): try: return int(value) except ValueError: raise ExifValueError(value, self.type) elif self.type in ('Rational', 'SRational'): try: r = make_fraction(value) except (ValueError, ZeroDivisionError): raise ExifValueError(value, self.type) else: if self.type == 'Rational' and r.numerator < 0: raise ExifValueError(value, self.type) return r elif self.type == 'Undefined': # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. return undefined_to_string(value) raise ExifValueError(value, self.type) def _convert_to_string(self, value): """ Convert one value to its corresponding string representation, suitable to pass to libexiv2. :param value: the value to be converted :return: the value converted to its corresponding string representation :rtype: string :raise ExifValueError: if the conversion fails """ if self.type == 'Ascii': if isinstance(value, datetime.datetime): return DateTimeFormatter.exif(value) elif isinstance(value, datetime.date): if self.key == 'Exif.GPSInfo.GPSDateStamp': # Special case return DateTimeFormatter.exif(value) else: return '%s 00:00:00' % DateTimeFormatter.exif(value) else: return value elif self.type in ('Byte', 'SByte'): if isinstance(value, str): try: return value.encode('utf-8') except UnicodeEncodeError: raise ExifValueError(value, self.type) elif isinstance(value, bytes): return value else: raise ExifValueError(value, self.type) elif self.type == 'Comment': return self._convert_to_bytes(value) elif self.type == 'Short': if isinstance(value, int) and value >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SShort': if isinstance(value, int): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Long': if isinstance(value, int) and value >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SLong': if isinstance(value, int): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Rational': if is_fraction(value) and value.numerator >= 0: return fraction_to_string(value) else: raise ExifValueError(value, self.type) elif self.type == 'SRational': if is_fraction(value): return fraction_to_string(value) else: raise ExifValueError(value, self.type) elif self.type == 'Undefined': if isinstance(value, str): try: return string_to_undefined(value) except UnicodeEncodeError: raise ExifValueError(value, self.type) elif isinstance(value, bytes): return string_to_undefined(value) else: raise ExifValueError(value, self.type) raise ExifValueError(value, self.type) def _convert_to_bytes(self, value): if value is None: return if isinstance(value, str): if value.startswith('charset='): charset, val = value.split(' ', 1) charset = charset.split('=')[1].strip('"') encoding = self._match_encoding(charset) else: encoding = 'utf-8' charset = 'Unicode' try: val = value.encode(encoding) except UnicodeError: pass else: #self._set_raw_value('charset=%s %s' % (charset, val)) return val elif isinstance(value, bytes): return value else: raise ExifValueError(value, self.type) def __str__(self): """ :return: a string representation of the EXIF tag for debugging purposes :rtype: string """ left = '%s [%s]' % (self.key, self.type) if self._raw_value is None: right = '(No value)' elif self.type == 'Undefined' and len(self._raw_value) > 100: right = '(Binary value suppressed)' else: right = self._raw_value return '<%s = %s>' % (left, right) # Support for pickling. def __getstate__(self): return (self.key, self.raw_value) def __setstate__(self, state): key, raw_value = state self._tag = libexiv2python._ExifTag(key) self.raw_value = raw_value