Exemple #1
0
class TestNotifyingList(unittest.TestCase):
    def setUp(self):
        self.values = NotifyingList([5, 7, 9, 14, 57, 3, 2])

    def test_no_listener(self):
        # No listener is registered, nothing should happen.
        self.values[3] = 13
        del self.values[5]
        self.values.append(17)
        self.values.extend([11, 22])
        self.values.insert(4, 24)
        self.values.pop()
        self.values.remove(9)
        self.values.reverse()
        self.values.sort()
        self.values += [8, 4]
        self.values *= 3
        self.values[3:4] = [8, 4]
        del self.values[3:5]

    def test_listener_interface(self):
        self.values.register_listener(ListenerInterface())
        self.failUnlessRaises(NotImplementedError, self.values.__setitem__, 3,
                              13)
        self.failUnlessRaises(NotImplementedError, self.values.__delitem__, 5)
        self.failUnlessRaises(NotImplementedError, self.values.append, 17)
        self.failUnlessRaises(NotImplementedError, self.values.extend,
                              [11, 22])
        self.failUnlessRaises(NotImplementedError, self.values.insert, 4, 24)
        self.failUnlessRaises(NotImplementedError, self.values.pop)
        self.failUnlessRaises(NotImplementedError, self.values.remove, 9)
        self.failUnlessRaises(NotImplementedError, self.values.reverse)
        self.failUnlessRaises(NotImplementedError, self.values.sort)
        self.failUnlessRaises(NotImplementedError, self.values.__iadd__,
                              [8, 4])
        self.failUnlessRaises(NotImplementedError, self.values.__imul__, 3)
        self.failUnlessRaises(NotImplementedError, self.values.__setslice__, 3,
                              4, [8, 4])
        self.failUnlessRaises(NotImplementedError, self.values.__delslice__, 3,
                              5)

    def _register_listeners(self):
        # Register a random number of listeners
        listeners = [SimpleListener() for i in xrange(random.randint(3, 20))]
        for listener in listeners:
            self.values.register_listener(listener)
        return listeners

    def test_setitem(self):
        listeners = self._register_listeners()

        self.values[3] = 13
        self.failUnlessEqual(self.values, [5, 7, 9, 13, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.failUnlessRaises(IndexError, self.values.__setitem__, 9, 27)
        self.failUnlessEqual(self.values, [5, 7, 9, 13, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_delitem(self):
        listeners = self._register_listeners()

        del self.values[5]
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.failUnlessRaises(IndexError, self.values.__delitem__, 9)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_append(self):
        listeners = self._register_listeners()

        self.values.append(17)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 17])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_extend(self):
        listeners = self._register_listeners()

        self.values.extend([11, 22])
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 11, 22])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.failUnlessRaises(TypeError, self.values.extend, 26)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 11, 22])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_insert(self):
        listeners = self._register_listeners()

        self.values.insert(4, 24)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 24, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_pop(self):
        listeners = self._register_listeners()

        self.values.pop()
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.values.pop(4)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 3])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 2)

        self.values.pop(-2)
        self.failUnlessEqual(self.values, [5, 7, 9, 3])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

        self.failUnlessRaises(IndexError, self.values.pop, 33)
        self.failUnlessEqual(self.values, [5, 7, 9, 3])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

    def test_remove(self):
        listeners = self._register_listeners()

        self.values.remove(9)
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.failUnlessRaises(ValueError, self.values.remove, 33)
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_reverse(self):
        listeners = self._register_listeners()

        self.values.reverse()
        self.failUnlessEqual(self.values, [2, 3, 57, 14, 9, 7, 5])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_sort(self):
        listeners = self._register_listeners()

        self.values.sort()
        self.failUnlessEqual(self.values, [2, 3, 5, 7, 9, 14, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.values.sort(cmp=lambda x, y: y - x)
        self.failUnlessEqual(self.values, [57, 14, 9, 7, 5, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 2)

        self.values.sort(key=lambda x: x * x)
        self.failUnlessEqual(self.values, [2, 3, 5, 7, 9, 14, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

        self.values.sort(reverse=True)
        self.failUnlessEqual(self.values, [57, 14, 9, 7, 5, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 4)

    def test_iadd(self):
        listeners = self._register_listeners()

        self.values += [44, 31, 19]
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 44, 31, 19])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_imul(self):
        listeners = self._register_listeners()

        self.values *= 3
        self.failUnlessEqual(self.values, [
            5, 7, 9, 14, 57, 3, 2, 5, 7, 9, 14, 57, 3, 2, 5, 7, 9, 14, 57, 3, 2
        ])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_setslice(self):
        listeners = self._register_listeners()

        # Basic slicing (of the form [i:j]): implemented as __setslice__.

        self.values[2:4] = [3, 4]
        self.failUnlessEqual(self.values, [5, 7, 3, 4, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.values[3:5] = [77, 8, 12]
        self.failUnlessEqual(self.values, [5, 7, 3, 77, 8, 12, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 2)

        self.values[2:5] = [1, 0]
        self.failUnlessEqual(self.values, [5, 7, 1, 0, 12, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

        self.values[0:2] = []
        self.failUnlessEqual(self.values, [1, 0, 12, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 4)

        self.values[2:2] = [7, 5]
        self.failUnlessEqual(self.values, [1, 0, 7, 5, 12, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 5)

        # With negatives indexes

        self.values[4:-2] = [9]
        self.failUnlessEqual(self.values, [1, 0, 7, 5, 9, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 6)

        self.values[-2:1] = [6, 4]
        self.failUnlessEqual(self.values, [1, 0, 7, 5, 9, 6, 4, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 7)

        self.values[-5:-2] = [8]
        self.failUnlessEqual(self.values, [1, 0, 7, 5, 8, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 8)

        # With missing (implicit) indexes

        self.values[:2] = [4]
        self.failUnlessEqual(self.values, [4, 7, 5, 8, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 9)

        self.values[4:] = [1]
        self.failUnlessEqual(self.values, [4, 7, 5, 8, 1])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 10)

        self.values[:] = [5, 7, 9, 14, 57, 3, 2]
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 11)

    def test_delslice(self):
        listeners = self._register_listeners()

        del self.values[2:3]
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        del self.values[2:2]
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        # With negatives indexes

        del self.values[4:-1]
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 2)

        del self.values[-1:5]
        self.failUnlessEqual(self.values, [5, 7, 14, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

        del self.values[-2:-1]
        self.failUnlessEqual(self.values, [5, 7, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 4)

        # With missing (implicit) indexes

        del self.values[:1]
        self.failUnlessEqual(self.values, [7, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 5)

        del self.values[1:]
        self.failUnlessEqual(self.values, [7])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 6)

        del self.values[:]
        self.failUnlessEqual(self.values, [])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 7)
Exemple #2
0
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
Exemple #3
0
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
Exemple #4
0
class TestNotifyingList(unittest.TestCase):

    def setUp(self):
        self.values = NotifyingList([5, 7, 9, 14, 57, 3, 2])

    def test_no_listener(self):
        # No listener is registered, nothing should happen.
        self.values[3] = 13
        del self.values[5]
        self.values.append(17)
        self.values.extend([11, 22])
        self.values.insert(4, 24)
        self.values.pop()
        self.values.remove(9)
        self.values.reverse()
        self.values.sort()
        self.values += [8, 4]
        self.values *= 3
        self.values[3:4] = [8, 4]
        del self.values[3:5]

    def test_listener_interface(self):
        self.values.register_listener(ListenerInterface())
        self.failUnlessRaises(NotImplementedError,
                              self.values.__setitem__, 3, 13)
        self.failUnlessRaises(NotImplementedError, self.values.__delitem__, 5)
        self.failUnlessRaises(NotImplementedError, self.values.append, 17)
        self.failUnlessRaises(NotImplementedError, self.values.extend, [11, 22])
        self.failUnlessRaises(NotImplementedError, self.values.insert, 4, 24)
        self.failUnlessRaises(NotImplementedError, self.values.pop)
        self.failUnlessRaises(NotImplementedError, self.values.remove, 9)
        self.failUnlessRaises(NotImplementedError, self.values.reverse)
        self.failUnlessRaises(NotImplementedError, self.values.sort)
        self.failUnlessRaises(NotImplementedError, self.values.__iadd__, [8, 4])
        self.failUnlessRaises(NotImplementedError, self.values.__imul__, 3)
        self.failUnlessRaises(NotImplementedError, self.values.__setslice__,
                              3, 4, [8, 4])
        self.failUnlessRaises(NotImplementedError, self.values.__delslice__,
                              3, 5)

    def _register_listeners(self):
        # Register a random number of listeners
        listeners = [SimpleListener() for i in xrange(random.randint(3, 20))]
        for listener in listeners:
            self.values.register_listener(listener)
        return listeners

    def test_setitem(self):
        listeners = self._register_listeners()

        self.values[3] = 13
        self.failUnlessEqual(self.values, [5, 7, 9, 13, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.failUnlessRaises(IndexError, self.values.__setitem__, 9, 27)
        self.failUnlessEqual(self.values, [5, 7, 9, 13, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_delitem(self):
        listeners = self._register_listeners()

        del self.values[5]
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.failUnlessRaises(IndexError, self.values.__delitem__, 9)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_append(self):
        listeners = self._register_listeners()

        self.values.append(17)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 17])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_extend(self):
        listeners = self._register_listeners()

        self.values.extend([11, 22])
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 11, 22])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.failUnlessRaises(TypeError, self.values.extend, 26)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 11, 22])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_insert(self):
        listeners = self._register_listeners()

        self.values.insert(4, 24)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 24, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_pop(self):
        listeners = self._register_listeners()

        self.values.pop()
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.values.pop(4)
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 3])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 2)

        self.values.pop(-2)
        self.failUnlessEqual(self.values, [5, 7, 9, 3])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

        self.failUnlessRaises(IndexError, self.values.pop, 33)
        self.failUnlessEqual(self.values, [5, 7, 9, 3])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

    def test_remove(self):
        listeners = self._register_listeners()

        self.values.remove(9)
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.failUnlessRaises(ValueError, self.values.remove, 33)
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_reverse(self):
        listeners = self._register_listeners()

        self.values.reverse()
        self.failUnlessEqual(self.values, [2, 3, 57, 14, 9, 7, 5])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_sort(self):
        listeners = self._register_listeners()

        self.values.sort()
        self.failUnlessEqual(self.values, [2, 3, 5, 7, 9, 14, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.values.sort(cmp=lambda x, y: y - x)
        self.failUnlessEqual(self.values, [57, 14, 9, 7, 5, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 2)

        self.values.sort(key=lambda x: x * x)
        self.failUnlessEqual(self.values, [2, 3, 5, 7, 9, 14, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

        self.values.sort(reverse=True)
        self.failUnlessEqual(self.values, [57, 14, 9, 7, 5, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 4)

    def test_iadd(self):
        listeners = self._register_listeners()

        self.values += [44, 31, 19]
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2, 44, 31, 19])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_imul(self):
        listeners = self._register_listeners()

        self.values *= 3
        self.failUnlessEqual(self.values,
                             [5, 7, 9, 14, 57, 3, 2,
                              5, 7, 9, 14, 57, 3, 2,
                              5, 7, 9, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

    def test_setslice(self):
        listeners = self._register_listeners()

        # Basic slicing (of the form [i:j]): implemented as __setslice__.

        self.values[2:4] = [3, 4]
        self.failUnlessEqual(self.values, [5, 7, 3, 4, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        self.values[3:5] = [77, 8, 12]
        self.failUnlessEqual(self.values, [5, 7, 3, 77, 8, 12, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 2)

        self.values[2:5] = [1, 0]
        self.failUnlessEqual(self.values, [5, 7, 1, 0, 12, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

        self.values[0:2] = []
        self.failUnlessEqual(self.values, [1, 0, 12, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 4)

        self.values[2:2] = [7, 5]
        self.failUnlessEqual(self.values, [1, 0, 7, 5, 12, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 5)

        # With negatives indexes

        self.values[4:-2] = [9]
        self.failUnlessEqual(self.values, [1, 0, 7, 5, 9, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 6)

        self.values[-2:1] = [6, 4]
        self.failUnlessEqual(self.values, [1, 0, 7, 5, 9, 6, 4, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 7)

        self.values[-5:-2] = [8]
        self.failUnlessEqual(self.values, [1, 0, 7, 5, 8, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 8)

        # With missing (implicit) indexes

        self.values[:2] = [4]
        self.failUnlessEqual(self.values, [4, 7, 5, 8, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 9)

        self.values[4:] = [1]
        self.failUnlessEqual(self.values, [4, 7, 5, 8, 1])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 10)

        self.values[:] = [5, 7, 9, 14, 57, 3, 2]
        self.failUnlessEqual(self.values, [5, 7, 9, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 11)

    def test_delslice(self):
        listeners = self._register_listeners()

        del self.values[2:3]
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        del self.values[2:2]
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 3, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 1)

        # With negatives indexes

        del self.values[4:-1]
        self.failUnlessEqual(self.values, [5, 7, 14, 57, 2])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 2)

        del self.values[-1:5]
        self.failUnlessEqual(self.values, [5, 7, 14, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 3)

        del self.values[-2:-1]
        self.failUnlessEqual(self.values, [5, 7, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 4)

        # With missing (implicit) indexes

        del self.values[:1]
        self.failUnlessEqual(self.values, [7, 57])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 5)

        del self.values[1:]
        self.failUnlessEqual(self.values, [7])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 6)

        del self.values[:]
        self.failUnlessEqual(self.values, [])
        for listener in listeners:
            self.failUnlessEqual(listener.changes, 7)
Exemple #5
0
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
Exemple #6
0
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