~osomon/pyexiv2/pyexiv2-0.3

« back to all changes in this revision

Viewing changes to src/pyexiv2/main.py

  • Committer: Olivier Tilloy
  • Date: 2009-10-18 00:27:32 UTC
  • mto: This revision was merged to the branch mainline in revision 232.
  • Revision ID: olivier@tilloy.net-20091018002732-rlkz6hdfj6wib5kp
Experimental code split into various python modules.

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
#
26
26
# ******************************************************************************
27
27
 
28
 
"""
29
 
Manipulation of EXIF, IPTC and XMP metadata and thumbnails embedded in images.
30
 
 
31
 
The L{ImageMetadata} class provides read/write access to all the metadata and
32
 
the various thumbnails embedded in an image file such as JPEG and TIFF files.
33
 
 
34
 
Metadata is accessed through subclasses of L{MetadataTag} and the tag values are
35
 
conveniently wrapped in python objects.
36
 
A tag containing a date/time information for the image
37
 
(e.g. C{Exif.Photo.DateTimeOriginal}) will be represented by a python
38
 
C{datetime.datetime} object.
39
 
 
40
 
This module is a python layer on top of the low-level python binding of the
41
 
C++ library Exiv2, libpyexiv2.
42
 
 
43
 
A typical use of this binding would be:
44
 
 
45
 
>>> import pyexiv2
46
 
>>> metadata = pyexiv2.ImageMetadata('test/smiley.jpg')
47
 
>>> metadata.read()
48
 
>>> print metadata.exif_keys
49
 
['Exif.Image.ImageDescription', 'Exif.Image.XResolution',
50
 
 'Exif.Image.YResolution', 'Exif.Image.ResolutionUnit', 'Exif.Image.Software',
51
 
 'Exif.Image.DateTime', 'Exif.Image.Artist', 'Exif.Image.Copyright',
52
 
 'Exif.Image.ExifTag', 'Exif.Photo.Flash', 'Exif.Photo.PixelXDimension',
53
 
 'Exif.Photo.PixelYDimension']
54
 
>>> print metadata['Exif.Image.DateTime'].value
55
 
2004-07-13 21:23:44
56
 
>>> import datetime
57
 
>>> metadata['Exif.Image.DateTime'].value = datetime.datetime.today()
58
 
>>> metadata.write()
59
 
"""
60
 
 
61
 
import libexiv2python
62
 
 
63
 
import os
64
 
import time
65
 
import datetime
66
 
import re
67
 
 
68
 
 
69
 
__version__ =  (0, 2, 1)
70
 
 
71
 
__exiv2_version__ = libexiv2python.__exiv2_version__
72
 
 
73
 
 
74
 
class FixedOffset(datetime.tzinfo):
75
 
 
76
 
    """
77
 
    Fixed positive or negative offset from a local time east from UTC.
78
 
 
79
 
    @ivar sign:    the sign of the offset ('+' or '-')
80
 
    @type sign:    C{str}
81
 
    @ivar hours:   the absolute number of hours of the offset
82
 
    @type hours:   C{int}
83
 
    @ivar minutes: the absolute number of minutes of the offset
84
 
    @type minutes: C{int}
85
 
 
86
 
    """
87
 
 
88
 
    def __init__(self, sign='+', hours=0, minutes=0):
89
 
        """
90
 
        Initialize an offset from a sign ('+' or '-') and an absolute value
91
 
        expressed in hours and minutes.
92
 
        No check on the validity of those values is performed, it is the
93
 
        responsibility of the caller to pass valid values.
94
 
 
95
 
        @param sign:    the sign of the offset ('+' or '-')
96
 
        @type sign:     C{str}
97
 
        @param hours:   an absolute number of hours
98
 
        @type hours:    C{int}
99
 
        @param minutes: an absolute number of minutes
100
 
        @type minutes:  C{int}
101
 
        """
102
 
        self.sign = sign
103
 
        self.hours = hours
104
 
        self.minutes = minutes
105
 
 
106
 
    def utcoffset(self, dt):
107
 
        """
108
 
        Return offset of local time from UTC, in minutes east of UTC.
109
 
        If local time is west of UTC, this value will be negative.
110
 
 
111
 
        @param dt: the local time
112
 
        @type dt:  C{datetime.time}
113
 
 
114
 
        @return: a whole number of minutes in the range -1439 to 1439 inclusive
115
 
        @rtype:  C{datetime.timedelta}
116
 
        """
117
 
        total = self.hours * 60 + self.minutes
118
 
        if self.sign == '-':
119
 
            total = -total
120
 
        return datetime.timedelta(minutes = total)
121
 
 
122
 
    def dst(self, dt):
123
 
        """
124
 
        Return the daylight saving time (DST) adjustment.
125
 
        In this implementation, it is always nil.
126
 
 
127
 
        @param dt: the local time
128
 
        @type dt:  C{datetime.time}
129
 
 
130
 
        @return: the DST adjustment (always nil)
131
 
        @rtype:  C{datetime.timedelta}
132
 
        """
133
 
        return datetime.timedelta(0)
134
 
 
135
 
    def tzname(self, dt):
136
 
        """
137
 
        Return a string representation of the offset in the format '±%H:%M'.
138
 
        If the offset is nil, the representation is, by convention, 'Z'.
139
 
 
140
 
        @param dt: the local time
141
 
        @type dt:  C{datetime.time}
142
 
 
143
 
        @return: a human-readable representation of the offset
144
 
        @rtype:  C{str}
145
 
        """
146
 
        if self.hours == 0 and self.minutes == 0:
147
 
            return 'Z'
148
 
        else:
149
 
            return '%s%02d:%02d' % (self.sign, self.hours, self.minutes)
150
 
 
151
 
    def __equal__(self, other):
152
 
        """
153
 
        Test equality between this offset and another offset.
154
 
 
155
 
        @param other: another offset
156
 
        @type other:  L{FixedOffset}
157
 
 
158
 
        @return: C{True} if the offset are equal, C{False} otherwise
159
 
        @rtype:  C{bool}
160
 
        """
161
 
        return (self.sign == other.sign) and (self.hours == other.hours) and \
162
 
            (self.minutes == other.minutes)
163
 
 
164
 
 
165
 
def UndefinedToString(undefined):
166
 
    """
167
 
    Convert an undefined string into its corresponding sequence of bytes.
168
 
    The undefined string must contain the ascii codes of a sequence of bytes,
169
 
    each followed by a blank space (e.g. "48 50 50 49 " will be converted into
170
 
    "0221").
171
 
    The Undefined type is part of the EXIF specification.
172
 
 
173
 
    @param undefined: an undefined string
174
 
    @type undefined:  C{str}
175
 
 
176
 
    @return: the corresponding decoded string
177
 
    @rtype:  C{str}
178
 
    """
179
 
    return ''.join(map(lambda x: chr(int(x)), undefined.rstrip().split(' ')))
180
 
 
181
 
 
182
 
def StringToUndefined(sequence):
183
 
    """
184
 
    Convert a string into its undefined form.
185
 
    The undefined form contains a sequence of ascii codes, each followed by a
186
 
    blank space (e.g. "0221" will be converted into "48 50 50 49 ").
187
 
    The Undefined type is part of the EXIF specification.
188
 
 
189
 
    @param sequence: a sequence of bytes
190
 
    @type sequence:  C{str}
191
 
 
192
 
    @return: the corresponding undefined string
193
 
    @rtype:  C{str}
194
 
    """
195
 
    return ''.join(map(lambda x: '%d ' % ord(x), sequence))
196
 
 
197
 
 
198
 
class Rational(object):
199
 
 
200
 
    """
201
 
    A class representing a rational number.
202
 
 
203
 
    Its numerator and denominator are read-only properties.
204
 
    """
205
 
 
206
 
    _format_re = re.compile(r'(?P<numerator>-?\d+)/(?P<denominator>\d+)')
207
 
 
208
 
    def __init__(self, numerator, denominator):
209
 
        """
210
 
        Constructor.
211
 
 
212
 
        @param numerator:   the numerator
213
 
        @type numerator:    C{long}
214
 
        @param denominator: the denominator
215
 
        @type denominator:  C{long}
216
 
 
217
 
        @raise ZeroDivisionError: if the denominator equals zero
218
 
        """
219
 
        if denominator == 0:
220
 
            msg = 'Denominator of a rational number cannot be zero.'
221
 
            raise ZeroDivisionError(msg)
222
 
        self._numerator = long(numerator)
223
 
        self._denominator = long(denominator)
224
 
 
225
 
    @property
226
 
    def numerator(self):
227
 
        return self._numerator
228
 
 
229
 
    @property
230
 
    def denominator(self):
231
 
        return self._denominator
232
 
 
233
 
    @staticmethod
234
 
    def from_string(string):
235
 
        """
236
 
        Instantiate a Rational from a string formatted as
237
 
        C{[-]numerator/denominator}.
238
 
 
239
 
        @param string: a string representation of a rational number
240
 
        @type string:  C{str}
241
 
 
242
 
        @return: the rational number parsed
243
 
        @rtype:  L{Rational}
244
 
 
245
 
        @raise ValueError: if the format of the string is invalid
246
 
        """
247
 
        match = Rational._format_re.match(string)
248
 
        if match is None:
249
 
            raise ValueError('Invalid format for a rational: %s' % string)
250
 
        gd = match.groupdict()
251
 
        return Rational(long(gd['numerator']), long(gd['denominator']))
252
 
 
253
 
    def to_float(self):
254
 
        """
255
 
        @return: a floating point number approximation of the value
256
 
        @rtype:  C{float}
257
 
        """
258
 
        return float(self._numerator) / self._denominator
259
 
 
260
 
    def __eq__(self, other):
261
 
        """
262
 
        Compare two rational numbers for equality.
263
 
 
264
 
        Two rational numbers are equal if their reduced forms are equal.
265
 
 
266
 
        @param other: the rational number to compare to self for equality
267
 
        @type other:  L{Rational}
268
 
        
269
 
        @return: C{True} if equal, C{False} otherwise
270
 
        @rtype:  C{bool}
271
 
        """
272
 
        return (self._numerator * other._denominator) == \
273
 
               (other._numerator * self._denominator)
274
 
 
275
 
    def __str__(self):
276
 
        """
277
 
        Return a string representation of the rational number.
278
 
        """
279
 
        return '%d/%d' % (self._numerator, self._denominator)
280
 
 
281
 
 
282
 
class ListenerInterface(object):
283
 
 
284
 
    """
285
 
    Interface that an object that wants to listen to changes on another object
286
 
    should implement.
287
 
    """
288
 
 
289
 
    def contents_changed(self):
290
 
        """
291
 
        React on changes on the object observed.
292
 
        Override to implement specific behaviours.
293
 
        """
294
 
        raise NotImplementedError()
295
 
 
296
 
 
297
 
class NotifyingList(list):
298
 
 
299
 
    """
300
 
    A simplistic implementation of a notifying list.
301
 
    Any changes to the list are notified in a synchronous way to all previously
302
 
    registered listeners. A listener must implement the L{ListenerInterface}.
303
 
    """
304
 
 
305
 
    # Useful documentation:
306
 
    # file:///usr/share/doc/python2.5/html/lib/typesseq-mutable.html
307
 
    # http://docs.python.org/reference/datamodel.html#additional-methods-for-emulation-of-sequence-types
308
 
 
309
 
    def __init__(self, items=[]):
310
 
        super(NotifyingList, self).__init__(items)
311
 
        self._listeners = set()
312
 
 
313
 
    def register_listener(self, listener):
314
 
        """
315
 
        Register a new listener to be notified of changes.
316
 
 
317
 
        @param listener: any object that listens for changes
318
 
        @type listener:  any class that implements the L{ListenerInterface}
319
 
        """
320
 
        self._listeners.add(listener)
321
 
 
322
 
    def unregister_listener(self, listener):
323
 
        """
324
 
        Unregister a previously registered listener.
325
 
 
326
 
        @param listener: a previously registered listener
327
 
        @type listener:  any class that implements the L{ListenerInterface}
328
 
 
329
 
        @raise KeyError: if the listener was not previously registered
330
 
        """
331
 
        self._listeners.remove(listener)
332
 
 
333
 
    def _notify_listeners(self, *args):
334
 
        for listener in self._listeners:
335
 
            listener.contents_changed(*args)
336
 
 
337
 
    def __setitem__(self, index, item):
338
 
        # FIXME: support slice arguments for extended slicing
339
 
        super(NotifyingList, self).__setitem__(index, item)
340
 
        self._notify_listeners()
341
 
 
342
 
    def __delitem__(self, index):
343
 
        # FIXME: support slice arguments for extended slicing
344
 
        super(NotifyingList, self).__delitem__(index)
345
 
        self._notify_listeners()
346
 
 
347
 
    def append(self, item):
348
 
        super(NotifyingList, self).append(item)
349
 
        self._notify_listeners()
350
 
 
351
 
    def extend(self, items):
352
 
        super(NotifyingList, self).extend(items)
353
 
        self._notify_listeners()
354
 
 
355
 
    def insert(self, index, item):
356
 
        super(NotifyingList, self).insert(index, item)
357
 
        self._notify_listeners()
358
 
 
359
 
    def pop(self, index=None):
360
 
        if index is None:
361
 
            item = super(NotifyingList, self).pop()
362
 
        else:
363
 
            item = super(NotifyingList, self).pop(index)
364
 
        self._notify_listeners()
365
 
        return item
366
 
 
367
 
    def remove(self, item):
368
 
        super(NotifyingList, self).remove(item)
369
 
        self._notify_listeners()
370
 
 
371
 
    def reverse(self):
372
 
        super(NotifyingList, self).reverse()
373
 
        self._notify_listeners()
374
 
 
375
 
    def sort(self, cmp=None, key=None, reverse=False):
376
 
        super(NotifyingList, self).sort(cmp, key, reverse)
377
 
        self._notify_listeners()
378
 
 
379
 
    def __iadd__(self, other):
380
 
        self = super(NotifyingList, self).__iadd__(other)
381
 
        self._notify_listeners()
382
 
        return self
383
 
 
384
 
    def __imul__(self, coefficient):
385
 
        self = super(NotifyingList, self).__imul__(coefficient)
386
 
        self._notify_listeners()
387
 
        return self
388
 
 
389
 
    def __setslice__(self, i, j, items):
390
 
        # __setslice__ is deprecated but needs to be overridden for completeness
391
 
        super(NotifyingList, self).__setslice__(i, j, items)
392
 
        self._notify_listeners()
393
 
 
394
 
    def __delslice__(self, i, j):
395
 
        # __delslice__ is deprecated but needs to be overridden for completeness
396
 
        deleted = self[i:j]
397
 
        super(NotifyingList, self).__delslice__(i, j)
398
 
        if deleted:
399
 
            self._notify_listeners()
400
 
 
401
 
 
402
 
class MetadataTag(object):
403
 
 
404
 
    """
405
 
    A generic metadata tag.
406
 
    It is meant to be subclassed to implement specific tag types behaviours.
407
 
 
408
 
    @ivar key:         a unique key that identifies the tag
409
 
    @type key:         C{str}
410
 
    @ivar name:        the short internal name that identifies the tag within
411
 
                       its scope
412
 
    @type name:        C{str}
413
 
    @ivar label:       a human readable label for the tag
414
 
    @type label:       C{str}
415
 
    @ivar description: a description of the function of the tag
416
 
    @type description: C{str}
417
 
    @ivar type:        the data type name
418
 
    @type type:        C{str}
419
 
    @ivar raw_value:   the raw value of the tag as provided by exiv2
420
 
    @type raw_value:   C{str}
421
 
    @ivar metadata:    reference to the containing metadata if any
422
 
    @type metadata:    L{pyexiv2.ImageMetadata}
423
 
    """
424
 
 
425
 
    def __init__(self, key, name, label, description, type, value):
426
 
        self.key = key
427
 
        self.name = name
428
 
        # FIXME: all attributes that may contain a localized string should be
429
 
        #        unicode.
430
 
        self.label = label
431
 
        self.description = description
432
 
        self.type = type
433
 
        self.raw_value = value
434
 
        self.metadata = None
435
 
 
436
 
    def __str__(self):
437
 
        """
438
 
        Return a string representation of the value of the tag suitable to pass
439
 
        to libexiv2 to set it.
440
 
 
441
 
        @rtype: C{str}
442
 
        """
443
 
        return self.raw_value
444
 
 
445
 
    def __repr__(self):
446
 
        """
447
 
        Return a string representation of the tag for debugging purposes.
448
 
 
449
 
        @rtype: C{str}
450
 
        """
451
 
        return '<%s [%s] = %s>' % (self.key, self.type, self.raw_value)
452
 
 
453
 
 
454
 
class ExifValueError(ValueError):
455
 
 
456
 
    """
457
 
    Exception raised when failing to parse the value of an EXIF tag.
458
 
 
459
 
    @ivar value: the value that fails to be parsed
460
 
    @type value: C{str}
461
 
    @ivar type:  the EXIF type of the tag
462
 
    @type type:  C{str}
463
 
    """
464
 
 
465
 
    def __init__(self, value, type):
466
 
        self.value = value
467
 
        self.type = type
468
 
 
469
 
    def __str__(self):
470
 
        return 'Invalid value for EXIF type [%s]: [%s]' % \
471
 
               (self.type, self.value)
472
 
 
473
 
 
474
 
class ExifTag(MetadataTag, ListenerInterface):
475
 
 
476
 
    """
477
 
    An EXIF metadata tag.
478
 
    This tag has an additional field that contains the value of the tag
479
 
    formatted as a human readable string.
480
 
 
481
 
    @ivar fvalue: the value of the tag formatted as a human readable string
482
 
    @type fvalue: C{str}
483
 
    """
484
 
 
485
 
    # According to the EXIF specification, the only accepted format for an Ascii
486
 
    # value representing a datetime is '%Y:%m:%d %H:%M:%S', but it seems that
487
 
    # others formats can be found in the wild.
488
 
    _datetime_formats = ('%Y:%m:%d %H:%M:%S',
489
 
                         '%Y-%m-%d %H:%M:%S',
490
 
                         '%Y-%m-%dT%H:%M:%SZ')
491
 
 
492
 
    _date_formats = ('%Y:%m:%d',)
493
 
 
494
 
    def __init__(self, key, name, label, description, type, value, fvalue):
495
 
        super(ExifTag, self).__init__(key, name, label,
496
 
                                      description, type, value)
497
 
        self.fvalue = fvalue
498
 
        self._init_values()
499
 
 
500
 
    def _init_values(self):
501
 
        # Initial conversion of the raw values to their corresponding python
502
 
        # types.
503
 
        if self.type in ('Short', 'Long', 'SLong', 'Rational', 'SRational'):
504
 
            # May contain multiple values
505
 
            values = self.raw_value.split()
506
 
            if len(values) > 1:
507
 
                # Make values a notifying list
508
 
                values = map(self._convert_to_python, values)
509
 
                self._value = NotifyingList(values)
510
 
                self._value.register_listener(self)
511
 
                return
512
 
        self._value = self._convert_to_python(self.raw_value)
513
 
 
514
 
    def _get_value(self):
515
 
        return self._value
516
 
 
517
 
    def _set_value(self, new_value):
518
 
        if self.metadata is not None:
519
 
            if isinstance(new_value, (list, tuple)):
520
 
                raw_values = map(self._convert_to_string, new_value)
521
 
                raw_value = ' '.join(raw_values)
522
 
            else:
523
 
                raw_value = self._convert_to_string(new_value)
524
 
            self.metadata._set_exif_tag_value(self.key, raw_value)
525
 
 
526
 
        if isinstance(self._value, NotifyingList):
527
 
            self._value.unregister_listener(self)
528
 
 
529
 
        if isinstance(new_value, NotifyingList):
530
 
            # Already a notifying list
531
 
            self._value = new_value
532
 
            self._value.register_listener(self)
533
 
        elif isinstance(new_value, (list, tuple)):
534
 
            # Make the values a notifying list 
535
 
            self._value = NotifyingList(new_value)
536
 
            self._value.register_listener(self)
537
 
        else:
538
 
            # Single value
539
 
            self._value = new_value
540
 
 
541
 
    def _del_value(self):
542
 
        if self.metadata is not None:
543
 
            self.metadata._delete_exif_tag(self.key)
544
 
 
545
 
        if isinstance(self._value, NotifyingList):
546
 
            self._value.unregister_listener(self)
547
 
 
548
 
        del self._value
549
 
 
550
 
    """the value of the tag converted to its corresponding python type"""
551
 
    value = property(fget=_get_value, fset=_set_value, fdel=_del_value,
552
 
                     doc=None)
553
 
 
554
 
    def contents_changed(self):
555
 
        """
556
 
        Implementation of the L{ListenerInterface}.
557
 
        React on changes to the list of values of the tag.
558
 
        """
559
 
        # self._value is a list of value and its contents changed.
560
 
        self._set_value(self._value)
561
 
 
562
 
    def _convert_to_python(self, value):
563
 
        """
564
 
        Convert one raw value to its corresponding python type.
565
 
 
566
 
        @param value:  the raw value to be converted
567
 
        @type value:   C{str}
568
 
 
569
 
        @return: the value converted to its corresponding python type
570
 
        @rtype:  depends on C{self.type} (DOCME)
571
 
 
572
 
        @raise ExifValueError: if the conversion fails
573
 
        """
574
 
        if self.type == 'Ascii':
575
 
            # The value may contain a Datetime
576
 
            for format in self._datetime_formats:
577
 
                try:
578
 
                    t = time.strptime(value, format)
579
 
                except ValueError:
580
 
                    continue
581
 
                else:
582
 
                    return datetime.datetime(*t[:6])
583
 
            # Or a Date (e.g. Exif.GPSInfo.GPSDateStamp)
584
 
            for format in self._date_formats:
585
 
                try:
586
 
                    t = time.strptime(value, format)
587
 
                except ValueError:
588
 
                    continue
589
 
                else:
590
 
                    return datetime.date(*t[:3])
591
 
            # Default to string.
592
 
            # There is currently no charset conversion.
593
 
            # TODO: guess the encoding and decode accordingly into unicode
594
 
            # where relevant.
595
 
            return value
596
 
 
597
 
        elif self.type == 'Byte':
598
 
            return value
599
 
 
600
 
        elif self.type == 'Short':
601
 
            try:
602
 
                return int(value)
603
 
            except ValueError:
604
 
                raise ExifValueError(value, self.type)
605
 
 
606
 
        elif self.type in ('Long', 'SLong'):
607
 
            try:
608
 
                return long(value)
609
 
            except ValueError:
610
 
                raise ExifValueError(value, self.type)
611
 
 
612
 
        elif self.type in ('Rational', 'SRational'):
613
 
            try:
614
 
                r = Rational.from_string(value)
615
 
            except (ValueError, ZeroDivisionError):
616
 
                raise ExifValueError(value, self.type)
617
 
            else:
618
 
                if self.type == 'Rational' and r.numerator < 0:
619
 
                    raise ExifValueError(value, self.type)
620
 
                return r
621
 
 
622
 
        elif self.type == 'Undefined':
623
 
            # There is currently no charset conversion.
624
 
            # TODO: guess the encoding and decode accordingly into unicode
625
 
            # where relevant.
626
 
            return self.fvalue
627
 
 
628
 
        raise ExifValueError(value, self.type)
629
 
 
630
 
    def _convert_to_string(self, value):
631
 
        """
632
 
        Convert one value to its corresponding string representation, suitable
633
 
        to pass to libexiv2.
634
 
 
635
 
        @param value: the value to be converted
636
 
        @type value:  depends on C{self.type} (DOCME)
637
 
 
638
 
        @return: the value converted to its corresponding string representation
639
 
        @rtype:  C{str}
640
 
 
641
 
        @raise ExifValueError: if the conversion fails
642
 
        """
643
 
        if self.type == 'Ascii':
644
 
            if type(value) is datetime.datetime:
645
 
                return value.strftime(self._datetime_formats[0])
646
 
            elif type(value) is datetime.date:
647
 
                if self.key == 'Exif.GPSInfo.GPSDateStamp':
648
 
                    # Special case
649
 
                    return value.strftime(self._date_formats[0])
650
 
                else:
651
 
                    return value.strftime('%s 00:00:00' % self._date_formats[0])
652
 
            elif type(value) is unicode:
653
 
                try:
654
 
                    return value.encode('utf-8')
655
 
                except UnicodeEncodeError:
656
 
                    raise ExifValueError(value, self.type)
657
 
            elif type(value) is str:
658
 
                return value
659
 
            else:
660
 
                raise ExifValueError(value, self.type) 
661
 
 
662
 
        elif self.type == 'Byte':
663
 
            if type(value) is unicode:
664
 
                try:
665
 
                    return value.encode('utf-8')
666
 
                except UnicodeEncodeError:
667
 
                    raise ExifValueError(value, self.type)
668
 
            elif type(value) is str:
669
 
                return value
670
 
            else:
671
 
                raise ExifValueError(value, self.type)
672
 
 
673
 
        elif self.type == 'Short':
674
 
            if type(value) is int and value >= 0:
675
 
                return str(value)
676
 
            else:
677
 
                raise ExifValueError(value, self.type)
678
 
 
679
 
        elif self.type == 'Long':
680
 
            if type(value) in (int, long) and value >= 0:
681
 
                return str(value)
682
 
            else:
683
 
                raise ExifValueError(value, self.type)
684
 
 
685
 
        elif self.type == 'SLong':
686
 
            if type(value) in (int, long):
687
 
                return str(value)
688
 
            else:
689
 
                raise ExifValueError(value, self.type)
690
 
 
691
 
        elif self.type == 'Rational':
692
 
            if type(value) is Rational and value.numerator >= 0:
693
 
                return str(value)
694
 
            else:
695
 
                raise ExifValueError(value, self.type)
696
 
 
697
 
        elif self.type == 'SRational':
698
 
            if type(value) is Rational:
699
 
                return str(value)
700
 
            else:
701
 
                raise ExifValueError(value, self.type)
702
 
 
703
 
        elif self.type == 'Undefined':
704
 
            if type(value) is unicode:
705
 
                try:
706
 
                    return value.encode('utf-8')
707
 
                except UnicodeEncodeError:
708
 
                    raise ExifValueError(value, self.type)
709
 
            elif type(value) is str:
710
 
                return value
711
 
            else:
712
 
                raise ExifValueError(value, self.type)
713
 
 
714
 
        raise ExifValueError(value, self.type)
715
 
 
716
 
    def __str__(self):
717
 
        """
718
 
        Return a string representation of the value of the EXIF tag suitable to
719
 
        pass to libexiv2 to set it.
720
 
 
721
 
        @rtype: C{str}
722
 
        """
723
 
        return self._convert_to_string(self.value)
724
 
 
725
 
    def __repr__(self):
726
 
        """
727
 
        Return a string representation of the EXIF tag for debugging purposes.
728
 
 
729
 
        @rtype: C{str}
730
 
        """
731
 
        left = '%s [%s]' % (self.key, self.type)
732
 
        if self.type == 'Undefined' and len(self._value) > 100:
733
 
            right = '(Binary value suppressed)'
734
 
        else:
735
 
            right = self.fvalue
736
 
        return '<%s = %s>' % (left, right)
737
 
 
738
 
 
739
 
class IptcValueError(ValueError):
740
 
 
741
 
    """
742
 
    Exception raised when failing to parse the value of an IPTC tag.
743
 
 
744
 
    @ivar value: the value that fails to be parsed
745
 
    @type value: C{str}
746
 
    @ivar type:  the IPTC type of the tag
747
 
    @type type:  C{str}
748
 
    """
749
 
 
750
 
    def __init__(self, value, type):
751
 
        self.value = value
752
 
        self.type = type
753
 
 
754
 
    def __str__(self):
755
 
        return 'Invalid value for IPTC type [%s]: [%s]' % \
756
 
               (self.type, self.value)
757
 
 
758
 
 
759
 
class IptcTag(MetadataTag):
760
 
 
761
 
    """
762
 
    An IPTC metadata tag.
763
 
    This tag can have several values (tags that have the repeatable property).
764
 
    """
765
 
 
766
 
    # strptime is not flexible enough to handle all valid Time formats, we use a
767
 
    # custom regular expression
768
 
    _time_zone_re = r'(?P<sign>\+|-)(?P<ohours>\d{2}):(?P<ominutes>\d{2})'
769
 
    _time_re = re.compile(r'(?P<hours>\d{2}):(?P<minutes>\d{2}):(?P<seconds>\d{2})(?P<tzd>%s)' % _time_zone_re)
770
 
 
771
 
    def __init__(self, key, name, label, description, type, values):
772
 
        super(IptcTag, self).__init__(key, name, label,
773
 
                                      description, type, values)
774
 
        self._init_values()
775
 
 
776
 
    def _init_values(self):
777
 
        # Initial conversion of the raw values to their corresponding python
778
 
        # types.
779
 
        values = map(self._convert_to_python, self.raw_value)
780
 
        # Make values a notifying list
781
 
        self._values = NotifyingList(values)
782
 
        self._values.register_listener(self)
783
 
 
784
 
    def _get_values(self):
785
 
        return self._values
786
 
 
787
 
    def _set_values(self, new_values):
788
 
        if self.metadata is not None:
789
 
            raw_values = map(self._convert_to_string, new_values)
790
 
            self.metadata._set_iptc_tag_values(self.key, raw_values)
791
 
        # Make values a notifying list if needed
792
 
        if isinstance(new_values, NotifyingList):
793
 
            self._values = new_values
794
 
        else:
795
 
            self._values = NotifyingList(new_values)
796
 
 
797
 
    def _del_values(self):
798
 
        if self.metadata is not None:
799
 
            self.metadata._delete_iptc_tag(self.key)
800
 
        del self._values
801
 
 
802
 
    """the list of values of the tag converted to their corresponding python
803
 
    type"""
804
 
    values = property(fget=_get_values, fset=_set_values, fdel=_del_values,
805
 
                     doc=None)
806
 
 
807
 
    def contents_changed(self):
808
 
        """
809
 
        Implementation of the L{ListenerInterface}.
810
 
        React on changes to the list of values of the tag.
811
 
        """
812
 
        # The contents of self._values was changed.
813
 
        # The following is a quick, non optimal solution.
814
 
        self._set_values(self._values)
815
 
 
816
 
    def _convert_to_python(self, value):
817
 
        """
818
 
        Convert one raw value to its corresponding python type.
819
 
 
820
 
        @param value: the raw value to be converted
821
 
        @type value:  C{str}
822
 
 
823
 
        @return: the value converted to its corresponding python type
824
 
        @rtype:  depends on C{self.type} (DOCME)
825
 
 
826
 
        @raise IptcValueError: if the conversion fails
827
 
        """
828
 
        if self.type == 'Short':
829
 
            try:
830
 
                return int(value)
831
 
            except ValueError:
832
 
                raise IptcValueError(value, self.type)
833
 
 
834
 
        elif self.type == 'String':
835
 
            # There is currently no charset conversion.
836
 
            # TODO: guess the encoding and decode accordingly into unicode
837
 
            # where relevant.
838
 
            return value
839
 
 
840
 
        elif self.type == 'Date':
841
 
            # According to the IPTC specification, the format for a string field
842
 
            # representing a date is '%Y%m%d'. However, the string returned by
843
 
            # exiv2 using method DateValue::toString() is formatted using
844
 
            # pattern '%Y-%m-%d'.
845
 
            format = '%Y-%m-%d'
846
 
            try:
847
 
                t = time.strptime(value, format)
848
 
                return datetime.date(*t[:3])
849
 
            except ValueError:
850
 
                raise IptcValueError(value, self.type)
851
 
 
852
 
        elif self.type == 'Time':
853
 
            # According to the IPTC specification, the format for a string field
854
 
            # representing a time is '%H%M%S±%H%M'. However, the string returned
855
 
            # by exiv2 using method TimeValue::toString() is formatted using
856
 
            # pattern '%H:%M:%S±%H:%M'.
857
 
            match = IptcTag._time_re.match(value)
858
 
            if match is None:
859
 
                raise IptcValueError(value, self.type)
860
 
            gd = match.groupdict()
861
 
            try:
862
 
                tzinfo = FixedOffset(gd['sign'], int(gd['ohours']),
863
 
                                     int(gd['ominutes']))
864
 
            except TypeError:
865
 
                raise IptcValueError(value, self.type)
866
 
            try:
867
 
                return datetime.time(int(gd['hours']), int(gd['minutes']),
868
 
                                     int(gd['seconds']), tzinfo=tzinfo)
869
 
            except (TypeError, ValueError):
870
 
                raise IptcValueError(value, self.type)
871
 
 
872
 
        elif self.type == 'Undefined':
873
 
            # Binary data, return it unmodified
874
 
            return value
875
 
 
876
 
        raise IptcValueError(value, self.type)
877
 
 
878
 
    def _convert_to_string(self, value):
879
 
        """
880
 
        Convert one value to its corresponding string representation, suitable
881
 
        to pass to libexiv2.
882
 
 
883
 
        @param value: the value to be converted
884
 
        @type value:  depends on C{self.type} (DOCME)
885
 
 
886
 
        @return: the value converted to its corresponding string representation
887
 
        @rtype:  C{str}
888
 
 
889
 
        @raise IptcValueError: if the conversion fails
890
 
        """
891
 
        if self.type == 'Short':
892
 
            if type(value) is int:
893
 
                return str(value)
894
 
            else:
895
 
                raise IptcValueError(value, self.type)
896
 
 
897
 
        elif self.type == 'String':
898
 
            if type(value) is unicode:
899
 
                try:
900
 
                    return value.encode('utf-8')
901
 
                except UnicodeEncodeError:
902
 
                    raise IptcValueError(value, self.type)
903
 
            elif type(value) is str:
904
 
                return value
905
 
            else:
906
 
                raise IptcValueError(value, self.type)
907
 
 
908
 
        elif self.type == 'Date':
909
 
            if type(value) in (datetime.date, datetime.datetime):
910
 
                # ISO 8601 date format
911
 
                return value.strftime('%Y%m%d')
912
 
            else:
913
 
                raise IptcValueError(value, self.type)
914
 
 
915
 
        elif self.type == 'Time':
916
 
            if type(value) in (datetime.time, datetime.datetime):
917
 
                r = value.strftime('%H%M%S')
918
 
                if value.tzinfo is not None:
919
 
                    r += value.strftime('%z')
920
 
                else:
921
 
                    r += '+0000'
922
 
                return r
923
 
            else:
924
 
                raise IptcValueError(value, self.type)
925
 
 
926
 
        elif self.type == 'Undefined':
927
 
            if type(value) is str:
928
 
                return value
929
 
            else:
930
 
                raise IptcValueError(value, self.type)
931
 
 
932
 
        raise IptcValueError(value, self.type)
933
 
 
934
 
    def to_string(self):
935
 
        """
936
 
        Return a list of string representations of the values of the IPTC tag
937
 
        suitable to pass to libexiv2 to set it.
938
 
 
939
 
        @rtype: C{list} of C{str}
940
 
        """
941
 
        return map(self._convert_to_string, self.values)
942
 
 
943
 
    def __str__(self):
944
 
        """
945
 
        Return a string representation of the list of values of the IPTC tag.
946
 
 
947
 
        @rtype: C{str}
948
 
        """
949
 
        return ', '.join(self.to_string())
950
 
 
951
 
    def __repr__(self):
952
 
        """
953
 
        Return a string representation of the IPTC tag for debugging purposes.
954
 
 
955
 
        @rtype: C{str}
956
 
        """
957
 
        return '<%s [%s] = %s>' % (self.key, self.type, str(self))
958
 
 
959
 
 
960
 
class XmpValueError(ValueError):
961
 
 
962
 
    """
963
 
    Exception raised when failing to parse the value of an XMP tag.
964
 
 
965
 
    @ivar value: the value that fails to be parsed
966
 
    @type value: C{str}
967
 
    @ivar type: the XMP type of the tag
968
 
    @type type: C{str}
969
 
    """
970
 
    def __init__(self, value, type):
971
 
        self.value = value
972
 
        self.type = type
973
 
 
974
 
    def __str__(self):
975
 
        return 'Invalid value for XMP type [%s]: [%s]' % \
976
 
               (self.type, self.value)
977
 
 
978
 
 
979
 
class XmpTag(MetadataTag):
980
 
 
981
 
    """
982
 
    An XMP metadata tag.
983
 
    """
984
 
 
985
 
    # strptime is not flexible enough to handle all valid Date formats, we use a
986
 
    # custom regular expression
987
 
    _time_zone_re = r'Z|((?P<sign>\+|-)(?P<ohours>\d{2}):(?P<ominutes>\d{2}))'
988
 
    _time_re = r'(?P<hours>\d{2})(:(?P<minutes>\d{2})(:(?P<seconds>\d{2})(.(?P<decimal>\d+))?)?(?P<tzd>%s))?' % _time_zone_re
989
 
    _date_re = re.compile(r'(?P<year>\d{4})(-(?P<month>\d{2})(-(?P<day>\d{2})(T(?P<time>%s))?)?)?' % _time_re)
990
 
 
991
 
    def __init__(self, key, name, label, description, type, value):
992
 
        super(XmpTag, self).__init__(key, name, label, description, type, value)
993
 
        self._value = XmpTag._convert_to_python(value, type)
994
 
 
995
 
    def _get_value(self):
996
 
        return self._value
997
 
 
998
 
    def _set_value(self, new_value):
999
 
        if self.metadata is not None:
1000
 
            raw_value = XmpTag._convert_to_string(new_value, self.type)
1001
 
            self.metadata._set_xmp_tag_value(self.key, raw_value)
1002
 
        self._value = new_value
1003
 
 
1004
 
    def _del_value(self):
1005
 
        if self.metadata is not None:
1006
 
            self.metadata._delete_xmp_tag(self.key)
1007
 
        del self._value
1008
 
 
1009
 
    """the value of the tag converted to its corresponding python type"""
1010
 
    value = property(fget=_get_value, fset=_set_value, fdel=_del_value,
1011
 
                     doc=None)
1012
 
 
1013
 
    @staticmethod
1014
 
    def _convert_to_python(value, xtype):
1015
 
        """
1016
 
        Convert a raw value to its corresponding python type.
1017
 
 
1018
 
        @param value: the raw value to be converted
1019
 
        @type value:  C{str}
1020
 
        @param xtype: the XMP type of the value
1021
 
        @type xtype:  C{str}
1022
 
 
1023
 
        @return: the value converted to its corresponding python type
1024
 
        @rtype:  depends on xtype (DOCME)
1025
 
 
1026
 
        @raise XmpValueError: if the conversion fails
1027
 
        """
1028
 
        if xtype.startswith('bag '):
1029
 
            # FIXME: make the value a notifying list.
1030
 
            if value == '':
1031
 
                return []
1032
 
            values = value.split(', ')
1033
 
            return map(lambda x: XmpTag._convert_to_python(x, xtype[4:]), values)
1034
 
 
1035
 
        elif xtype == 'Boolean':
1036
 
            if value == 'True':
1037
 
                return True
1038
 
            elif value == 'False':
1039
 
                return False
1040
 
            else:
1041
 
                raise XmpValueError(value, xtype)
1042
 
 
1043
 
        elif xtype == 'Choice':
1044
 
            # TODO
1045
 
            raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1046
 
 
1047
 
        elif xtype == 'Colorant':
1048
 
            # TODO
1049
 
            raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1050
 
 
1051
 
        elif xtype == 'Date':
1052
 
            match = XmpTag._date_re.match(value)
1053
 
            if match is None:
1054
 
                raise XmpValueError(value, xtype)
1055
 
            gd = match.groupdict()
1056
 
            if gd['month'] is not None:
1057
 
                month = int(gd['month'])
1058
 
            else:
1059
 
                month = 1
1060
 
            if gd['day'] is not None:
1061
 
                day = int(gd['day'])
1062
 
            else:
1063
 
                day = 1
1064
 
            if gd['time'] is None:
1065
 
                try:
1066
 
                    return datetime.date(int(gd['year']), month, day)
1067
 
                except ValueError:
1068
 
                    raise XmpValueError(value, xtype)
1069
 
            else:
1070
 
                if gd['minutes'] is None:
1071
 
                    # Malformed time
1072
 
                    raise XmpValueError(value, xtype)
1073
 
                if gd['seconds'] is not None:
1074
 
                    seconds = int(gd['seconds'])
1075
 
                else:
1076
 
                    seconds = 0
1077
 
                if gd['decimal'] is not None:
1078
 
                    microseconds = int(float('0.%s' % gd['decimal']) * 1E6)
1079
 
                else:
1080
 
                    microseconds = 0
1081
 
                if gd['tzd'] == 'Z':
1082
 
                    tzinfo = FixedOffset()
1083
 
                else:
1084
 
                    tzinfo = FixedOffset(gd['sign'], int(gd['ohours']),
1085
 
                                         int(gd['ominutes']))
1086
 
                try:
1087
 
                    return datetime.datetime(int(gd['year']), month, day,
1088
 
                                             int(gd['hours']), int(gd['minutes']),
1089
 
                                             seconds, microseconds, tzinfo)
1090
 
                except ValueError:
1091
 
                    raise XmpValueError(value, xtype)
1092
 
 
1093
 
        elif xtype == 'Dimensions':
1094
 
            # TODO
1095
 
            raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1096
 
 
1097
 
        elif xtype == 'Font':
1098
 
            # TODO
1099
 
            raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1100
 
 
1101
 
        elif xtype == 'Integer':
1102
 
            try:
1103
 
                return int(value)
1104
 
            except ValueError:
1105
 
                raise XmpValueError(value, xtype)
1106
 
 
1107
 
        elif xtype == 'Lang Alt':
1108
 
            matches = value.split('lang="')
1109
 
            nb = len(matches)
1110
 
            if nb < 2 or matches[0] != '':
1111
 
                raise XmpValueError(value, xtype)
1112
 
            result = {}
1113
 
            for i, match in enumerate(matches[1:]):
1114
 
                try:
1115
 
                    qualifier, text = match.split('" ', 1)
1116
 
                except ValueError:
1117
 
                    raise XmpValueError(value, xtype)
1118
 
                else:
1119
 
                    if not text.rstrip().endswith(','):
1120
 
                        if (i < nb - 2):
1121
 
                            # If not the last match, it should end with a comma
1122
 
                            raise XmpValueError(value, xtype)
1123
 
                        else:
1124
 
                            result[qualifier] = text
1125
 
                            try:
1126
 
                                result[qualifier] = unicode(text, 'utf-8')
1127
 
                            except TypeError:
1128
 
                                raise XmpValueError(value, xtype)
1129
 
                    else:
1130
 
                        try:
1131
 
                            result[qualifier] = unicode(text.rstrip()[:-1], 'utf-8')
1132
 
                        except TypeError:
1133
 
                            raise XmpValueError(value, xtype)
1134
 
            return result
1135
 
 
1136
 
        elif xtype == 'Locale':
1137
 
            # TODO
1138
 
            # See RFC 3066
1139
 
            raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1140
 
 
1141
 
        elif xtype == 'MIMEType':
1142
 
            try:
1143
 
                mtype, msubtype = value.split('/', 1)
1144
 
            except ValueError:
1145
 
                raise XmpValueError(value, xtype)
1146
 
            else:
1147
 
                return {'type': mtype, 'subtype': msubtype}
1148
 
 
1149
 
        elif xtype == 'Real':
1150
 
            # TODO
1151
 
            raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1152
 
 
1153
 
        elif xtype in ('ProperName', 'Text'):
1154
 
            try:
1155
 
                return unicode(value, 'utf-8')
1156
 
            except TypeError:
1157
 
                raise XmpValueError(value, xtype)
1158
 
 
1159
 
        elif xtype == 'Thumbnail':
1160
 
            # TODO
1161
 
            raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1162
 
 
1163
 
        elif xtype in ('URI', 'URL'):
1164
 
            return value
1165
 
 
1166
 
        elif xtype == 'XPath':
1167
 
            # TODO
1168
 
            raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1169
 
 
1170
 
        raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1171
 
 
1172
 
    @staticmethod
1173
 
    def _convert_to_string(value, xtype):
1174
 
        """
1175
 
        Convert a value to its corresponding string representation, suitable to
1176
 
        pass to libexiv2.
1177
 
 
1178
 
        @param value: the value to be converted
1179
 
        @type value:  depends on xtype (DOCME)
1180
 
        @param xtype: the XMP type of the value
1181
 
        @type xtype:  C{str}
1182
 
 
1183
 
        @return: the value converted to its corresponding string representation
1184
 
        @rtype:  C{str}
1185
 
 
1186
 
        @raise XmpValueError: if the conversion fails
1187
 
        """
1188
 
        if xtype.startswith('bag '):
1189
 
            if type(value) in (list, tuple):
1190
 
                return ', '.join(map(lambda x: XmpTag._convert_to_string(x, xtype[4:]), value))
1191
 
            else:
1192
 
                raise XmpValueError(value, xtype)
1193
 
 
1194
 
        elif xtype == 'Boolean':
1195
 
            if type(value) is bool:
1196
 
                return str(value)
1197
 
            else:
1198
 
                raise XmpValueError(value, xtype)
1199
 
 
1200
 
        elif xtype == 'Date':
1201
 
            if type(value) is datetime.date:
1202
 
                return value.isoformat()
1203
 
            elif type(value) is datetime.datetime:
1204
 
                if value.hour == 0 and value.minute == 0 and \
1205
 
                    value.second == 0 and value.microsecond == 0 and \
1206
 
                    (value.tzinfo is None or value.tzinfo == FixedOffset()):
1207
 
                    return value.strftime('%Y-%m-%d')
1208
 
                elif value.second == 0 and value.microsecond == 0:
1209
 
                    return value.strftime('%Y-%m-%dT%H:%M%Z')
1210
 
                elif value.microsecond == 0:
1211
 
                    return value.strftime('%Y-%m-%dT%H:%M:%S%Z')
1212
 
                else:
1213
 
                    r = value.strftime('%Y-%m-%dT%H:%M:%S.')
1214
 
                    r += str(int(value.microsecond) / 1E6)[2:]
1215
 
                    r += value.strftime('%Z')
1216
 
                    return r
1217
 
            else:
1218
 
                raise XmpValueError(value, xtype)
1219
 
 
1220
 
        elif xtype == 'Integer':
1221
 
            if type(value) in (int, long):
1222
 
                return str(value)
1223
 
            else:
1224
 
                raise XmpValueError(value, xtype)
1225
 
 
1226
 
        elif xtype == 'Lang Alt':
1227
 
            if type(value) is dict and len(value) > 0:
1228
 
                r = ''
1229
 
                for key, avalue in value.iteritems():
1230
 
                    if type(key) is unicode:
1231
 
                        try:
1232
 
                            rkey = key.encode('utf-8')
1233
 
                        except UnicodeEncodeError:
1234
 
                            raise XmpValueError(value, xtype)
1235
 
                    elif type(key) is str:
1236
 
                        rkey = key
1237
 
                    else:
1238
 
                        raise XmpValueError(value, xtype)
1239
 
                    if type(avalue) is unicode:
1240
 
                        try:
1241
 
                            ravalue = avalue.encode('utf-8')
1242
 
                        except UnicodeEncodeError:
1243
 
                            raise XmpValueError(value, xtype)
1244
 
                    elif type(avalue) is str:
1245
 
                        ravalue = avalue
1246
 
                    else:
1247
 
                        raise XmpValueError(value, xtype)
1248
 
                    r += 'lang="%s" %s, ' % (rkey, ravalue)
1249
 
                return r[:-2]
1250
 
            else:
1251
 
                raise XmpValueError(value, xtype)
1252
 
 
1253
 
        elif xtype == 'MIMEType':
1254
 
            if type(value) is dict:
1255
 
                try:
1256
 
                    return '%s/%s' % (value['type'], value['subtype'])
1257
 
                except KeyError:
1258
 
                    raise XmpValueError(value, xtype)
1259
 
            else:
1260
 
                raise XmpValueError(value, xtype)
1261
 
 
1262
 
        elif xtype in ('ProperName', 'Text', 'URI', 'URL'):
1263
 
            if type(value) is unicode:
1264
 
                try:
1265
 
                    return value.encode('utf-8')
1266
 
                except UnicodeEncodeError:
1267
 
                    raise XmpValueError(value, xtype)
1268
 
            elif type(value) is str:
1269
 
                return value
1270
 
            else:
1271
 
                raise XmpValueError(value, xtype)
1272
 
 
1273
 
        raise NotImplementedError('XMP conversion for type [%s]' % xtype)
1274
 
 
1275
 
    def to_string(self):
1276
 
        """
1277
 
        Return a string representation of the XMP tag suitable to pass to
1278
 
        libexiv2 to set the value of the tag.
1279
 
 
1280
 
        @rtype: C{str}
1281
 
        """
1282
 
        return XmpTag._convert_to_string(self.value, self.type)
1283
 
 
1284
 
    def __str__(self):
1285
 
        """
1286
 
        Return a string representation of the XMP tag for debugging purposes.
1287
 
 
1288
 
        @rtype: C{str}
1289
 
        """
1290
 
        r = 'Key = ' + self.key + os.linesep + \
1291
 
            'Name = ' + self.name + os.linesep + \
1292
 
            'Label = ' + self.label + os.linesep + \
1293
 
            'Description = ' + self.description + os.linesep + \
1294
 
            'Type = ' + self.type + os.linesep + \
1295
 
            'Values = ' + str(self.values)
1296
 
        return r
1297
 
 
1298
 
 
1299
 
class ImageMetadata(object):
1300
 
 
1301
 
    """
1302
 
    A container for all the metadata attached to an image.
1303
 
 
1304
 
    It provides convenient methods for the manipulation of EXIF, IPTC and XMP
1305
 
    metadata embedded in image files such as JPEG and TIFF files, using Python
1306
 
    types.
1307
 
    It also provides access to the thumbnails embedded in an image.
1308
 
    """
1309
 
 
1310
 
    def __init__(self, filename):
1311
 
        """
1312
 
        @param filename: absolute path to an image file
1313
 
        @type filename:  C{str} or C{unicode}
1314
 
        """
1315
 
        self.filename = filename
1316
 
        if type(filename) is unicode:
1317
 
            self.filename = filename.encode('utf-8')
1318
 
        self._image = None
1319
 
        self._keys = {'exif': None, 'iptc': None, 'xmp': None}
1320
 
        self._tags = {'exif': {}, 'iptc': {}, 'xmp': {}}
1321
 
 
1322
 
    def _instantiate_image(self, filename):
1323
 
        # This method is meant to be overridden in unit tests to easily replace
1324
 
        # the internal image reference by a mock.
1325
 
        return libexiv2python.Image(filename)
1326
 
 
1327
 
    def read(self):
1328
 
        """
1329
 
        Read the metadata embedded in the associated image file.
1330
 
        It is necessary to call this method once before attempting to access
1331
 
        the metadata (an exception will be raised if trying to access metadata
1332
 
        before calling this method).
1333
 
        """
1334
 
        if self._image is None:
1335
 
            self._image = self._instantiate_image(self.filename)
1336
 
        self._image.readMetadata()
1337
 
 
1338
 
    def write(self):
1339
 
        """
1340
 
        Write the metadata back to the associated image file.
1341
 
        """
1342
 
        self._image.writeMetadata()
1343
 
 
1344
 
    """List the keys of the available EXIF tags embedded in the image."""
1345
 
    @property
1346
 
    def exif_keys(self):
1347
 
        if self._keys['exif'] is None:
1348
 
            self._keys['exif'] = self._image.exifKeys()
1349
 
        return self._keys['exif']
1350
 
 
1351
 
    """List the keys of the available IPTC tags embedded in the image."""
1352
 
    @property
1353
 
    def iptc_keys(self):
1354
 
        if self._keys['iptc'] is None:
1355
 
            self._keys['iptc'] = self._image.iptcKeys()
1356
 
        return self._keys['iptc']
1357
 
 
1358
 
    """List the keys of the available XMP tags embedded in the image."""
1359
 
    @property
1360
 
    def xmp_keys(self):
1361
 
        if self._keys['xmp'] is None:
1362
 
            self._keys['xmp'] = self._image.xmpKeys()
1363
 
        return self._keys['xmp']
1364
 
 
1365
 
    def _get_exif_tag(self, key):
1366
 
        # Return the EXIF tag for the given key.
1367
 
        # Throw a KeyError if the tag doesn't exist.
1368
 
        try:
1369
 
            return self._tags['exif'][key]
1370
 
        except KeyError:
1371
 
            tag = ExifTag(*self._image.getExifTag(key))
1372
 
            tag.metadata = self
1373
 
            self._tags['exif'][key] = tag
1374
 
            return tag
1375
 
 
1376
 
    def _get_iptc_tag(self, key):
1377
 
        # Return the IPTC tag for the given key.
1378
 
        # Throw a KeyError if the tag doesn't exist.
1379
 
        try:
1380
 
            return self._tags['iptc'][key]
1381
 
        except KeyError:
1382
 
            tag = IptcTag(*self._image.getIptcTag(key))
1383
 
            tag.metadata = self
1384
 
            self._tags['iptc'][key] = tag
1385
 
            return tag
1386
 
 
1387
 
    def _get_xmp_tag(self, key):
1388
 
        # Return the XMP tag for the given key.
1389
 
        # Throw a KeyError if the tag doesn't exist.
1390
 
        try:
1391
 
            return self._tags['xmp'][key]
1392
 
        except KeyError:
1393
 
            tag = XmpTag(*self._image.getXmpTag(key))
1394
 
            tag.metadata = self
1395
 
            self._tags['xmp'][key] = tag
1396
 
            return tag
1397
 
 
1398
 
    def __getitem__(self, key):
1399
 
        """
1400
 
        Get a metadata tag for a given key.
1401
 
 
1402
 
        @param key: a metadata key in the dotted form C{family.group.tag} where
1403
 
                    family may be C{exif}, C{iptc} or C{xmp}.
1404
 
        @type key:  C{str}
1405
 
 
1406
 
        @return: the metadata tag corresponding to the key
1407
 
        @rtype:  a subclass of L{pyexiv2.MetadataTag}
1408
 
 
1409
 
        @raise KeyError: if the tag doesn't exist
1410
 
        """
1411
 
        family = key.split('.')[0].lower()
1412
 
        try:
1413
 
            return getattr(self, '_get_%s_tag' % family)(key)
1414
 
        except AttributeError:
1415
 
            raise KeyError(key)
1416
 
 
1417
 
    def _set_exif_tag(self, tag):
1418
 
        # Set an EXIF tag. If the tag already exists, its value is overwritten.
1419
 
        if type(tag) is not ExifTag:
1420
 
            raise TypeError('Expecting an ExifTag')
1421
 
        self._image.setExifTagValue(tag.key, str(tag))
1422
 
        self._tags['exif'][tag.key] = tag
1423
 
        tag.metadata = self
1424
 
 
1425
 
    def _set_exif_tag_value(self, key, value):
1426
 
        # Overwrite the tag value for an already existing tag.
1427
 
        # The tag is already in cache.
1428
 
        # Warning: this is not meant to be called directly as it doesn't update
1429
 
        # the internal cache (which would leave the object in an inconsistent
1430
 
        # state).
1431
 
        if key not in self.exif_keys:
1432
 
            raise KeyError('Cannot set the value of an inexistent tag')
1433
 
        if type(value) is not str:
1434
 
            raise TypeError('Expecting a string')
1435
 
        self._image.setExifTagValue(key, value)
1436
 
 
1437
 
    def _set_iptc_tag(self, tag):
1438
 
        # Set an IPTC tag. If the tag already exists, its values are
1439
 
        # overwritten.
1440
 
        if type(tag) is not IptcTag:
1441
 
            raise TypeError('Expecting an IptcTag')
1442
 
        self._image.setIptcTagValues(tag.key, tag.to_string())
1443
 
        self._tags['iptc'][tag.key] = tag
1444
 
        tag.metadata = self
1445
 
 
1446
 
    def _set_iptc_tag_values(self, key, values):
1447
 
        # Overwrite the tag values for an already existing tag.
1448
 
        # The tag is already in cache.
1449
 
        # Warning: this is not meant to be called directly as it doesn't update
1450
 
        # the internal cache (which would leave the object in an inconsistent
1451
 
        # state).
1452
 
        # FIXME: this is sub-optimal as it sets all the values regardless of how
1453
 
        # many of them really changed. Need to implement the same method with an
1454
 
        # index/range parameter (here and in the C++ wrapper).
1455
 
        if key not in self.iptc_keys:
1456
 
            raise KeyError('Cannot set the value of an inexistent tag')
1457
 
        if type(values) is not list or not \
1458
 
            reduce(lambda x, y: x and type(y) is str, values, True):
1459
 
            raise TypeError('Expecting a list of strings')
1460
 
        self._image.setIptcTagValues(key, values)
1461
 
 
1462
 
    def _set_xmp_tag(self, tag):
1463
 
        # Set an XMP tag. If the tag already exists, its value is overwritten.
1464
 
        if type(tag) is not XmpTag:
1465
 
            raise TypeError('Expecting an XmpTag')
1466
 
        self._image.setXmpTagValue(tag.key, tag.to_string())
1467
 
        self._tags['xmp'][tag.key] = tag
1468
 
        tag.metadata = self
1469
 
 
1470
 
    def _set_xmp_tag_value(self, key, value):
1471
 
        # Overwrite the tag value for an already existing tag.
1472
 
        # The tag is already in cache.
1473
 
        # Warning: this is not meant to be called directly as it doesn't update
1474
 
        # the internal cache (which would leave the object in an inconsistent
1475
 
        # state).
1476
 
        if key not in self.xmp_keys:
1477
 
            raise KeyError('Cannot set the value of an inexistent tag')
1478
 
        if type(value) is not str:
1479
 
            raise TypeError('Expecting a string')
1480
 
        self._image.setXmpTagValue(key, value)
1481
 
 
1482
 
    def __setitem__(self, key, tag):
1483
 
        """
1484
 
        Set a metadata tag for a given key.
1485
 
        If the tag was previously set, it is overwritten.
1486
 
 
1487
 
        @param key: a metadata key in the dotted form C{family.group.tag} where
1488
 
                    family may be C{exif}, C{iptc} or C{xmp}.
1489
 
        @type key:  C{str}
1490
 
        @param tag: a metadata tag
1491
 
        @type tag:  a subclass of L{pyexiv2.MetadataTag}
1492
 
 
1493
 
        @raise KeyError: if the key is invalid
1494
 
        """
1495
 
        family = key.split('.')[0].lower()
1496
 
        try:
1497
 
            return getattr(self, '_set_%s_tag' % family)(tag)
1498
 
        except AttributeError:
1499
 
            raise KeyError(key)
1500
 
 
1501
 
    def _delete_exif_tag(self, key):
1502
 
        # Delete an EXIF tag.
1503
 
        # Throw a KeyError if the tag doesn't exist.
1504
 
        if key not in self.exif_keys:
1505
 
            raise KeyError('Cannot delete an inexistent tag')
1506
 
        self._image.deleteExifTag(key)
1507
 
        try:
1508
 
            del self._tags['exif'][key]
1509
 
        except KeyError:
1510
 
            # The tag was not cached.
1511
 
            pass
1512
 
 
1513
 
    def _delete_iptc_tag(self, key):
1514
 
        # Delete an IPTC tag.
1515
 
        # Throw a KeyError if the tag doesn't exist.
1516
 
        if key not in self.iptc_keys:
1517
 
            raise KeyError('Cannot delete an inexistent tag')
1518
 
        self._image.deleteIptcTag(key)
1519
 
        try:
1520
 
            del self._tags['iptc'][key]
1521
 
        except KeyError:
1522
 
            # The tag was not cached.
1523
 
            pass
1524
 
 
1525
 
    def _delete_xmp_tag(self, key):
1526
 
        # Delete an XMP tag.
1527
 
        # Throw a KeyError if the tag doesn't exist.
1528
 
        if key not in self.xmp_keys:
1529
 
            raise KeyError('Cannot delete an inexistent tag')
1530
 
        self._image.deleteXmpTag(key)
1531
 
        try:
1532
 
            del self._tags['xmp'][key]
1533
 
        except KeyError:
1534
 
            # The tag was not cached.
1535
 
            pass
1536
 
 
1537
 
    def __delitem__(self, key):
1538
 
        """
1539
 
        Delete a metadata tag for a given key.
1540
 
 
1541
 
        @param key: a metadata key in the dotted form C{family.group.tag} where
1542
 
                    family may be C{exif}, C{iptc} or C{xmp}.
1543
 
        @type key:  C{str}
1544
 
 
1545
 
        @raise KeyError: if the tag with the given key doesn't exist
1546
 
        """
1547
 
        family = key.split('.')[0].lower()
1548
 
        try:
1549
 
            return getattr(self, '_delete_%s_tag' % family)(key)
1550
 
        except AttributeError:
1551
 
            raise KeyError(key)
 
28
import sys
 
29
 
 
30
from pyexiv2.metadata import ImageMetadata
1552
31
 
1553
32
 
1554
33
if __name__ == '__main__':