diff options
-rwxr-xr-x | cross-compile.sh | 20 | ||||
-rw-r--r-- | doc/developers.rst | 6 | ||||
-rw-r--r-- | src/exiv2wrapper.cpp | 35 | ||||
-rw-r--r-- | src/exiv2wrapper.hpp | 8 | ||||
-rw-r--r-- | src/exiv2wrapper_python.cpp | 1 | ||||
-rw-r--r-- | src/pyexiv2/exif.py | 93 | ||||
-rw-r--r-- | src/pyexiv2/iptc.py | 12 | ||||
-rw-r--r-- | src/pyexiv2/metadata.py | 16 | ||||
-rw-r--r-- | src/pyexiv2/utils.py | 10 | ||||
-rw-r--r-- | src/pyexiv2/xmp.py | 5 | ||||
-rw-r--r-- | test/ReadMetadataTestCase.py | 4 | ||||
-rwxr-xr-x | test/TestsRunner.py | 3 | ||||
-rw-r--r-- | test/data/MD5SUMS | 7 | ||||
-rw-r--r-- | test/data/empty.jpg | bin | 0 -> 332 bytes | |||
-rw-r--r-- | test/data/usercomment-ascii.jpg | bin | 0 -> 514 bytes | |||
-rw-r--r-- | test/data/usercomment-unicode-ii.jpg | bin | 0 -> 520 bytes | |||
-rw-r--r-- | test/data/usercomment-unicode-mm.jpg | bin | 0 -> 520 bytes | |||
-rw-r--r-- | test/exif.py | 17 | ||||
-rw-r--r-- | test/metadata.py | 75 | ||||
-rw-r--r-- | test/testutils.py | 19 | ||||
-rw-r--r-- | test/usercomment.py | 135 | ||||
-rw-r--r-- | test/xmp.py | 27 |
22 files changed, 409 insertions, 84 deletions
diff --git a/cross-compile.sh b/cross-compile.sh index e64b46c..5f9ce27 100755 --- a/cross-compile.sh +++ b/cross-compile.sh @@ -44,7 +44,7 @@ ARCHIVER=$PLATFORM-ar BUILD=i586-linux # zlib (for exiv2) -wget http://gnuwin32.sourceforge.net/downlinks/zlib-lib-zip.php +wget --trust-server-names=on http://gnuwin32.sourceforge.net/downlinks/zlib-lib-zip.php unzip -d zlib zlib-*.zip # iconv (for exiv2) @@ -56,7 +56,7 @@ make -j3 install cd .. # expat (for exiv2) -wget http://sourceforge.net/projects/expat/files/expat/2.0.1/expat-2.0.1.tar.gz/download +wget --trust-server-names=on http://sourceforge.net/projects/expat/files/expat/2.0.1/expat-2.0.1.tar.gz/download tar xf expat-2.0.1.tar.gz cd expat-2.0.1 ./configure --disable-shared --disable-visibility --target=$PLATFORM --host=$PLATFORM --build=$BUILD --prefix=$BASE/expat @@ -64,22 +64,22 @@ make -j3 install cd .. # exiv2 -wget http://exiv2.org/exiv2-0.20.tar.gz -tar xf exiv2-0.20.tar.gz -cd exiv2-0.20 +wget http://exiv2.org/exiv2-0.21.tar.gz +tar xf exiv2-0.21.tar.gz +cd exiv2-0.21 ./configure --disable-shared --disable-visibility --target=$PLATFORM --host=$PLATFORM --build=$BUILD --disable-nls --with-zlib=$BASE/zlib --with-libiconv-prefix=$BASE/libiconv --with-expat=$BASE/expat --prefix=$BASE/exiv2 make -j3 install cd .. # python -wget http://python.org/ftp/python/2.7/python-2.7.msi -7z x python-2.7.msi -opython +wget http://python.org/ftp/python/2.7.1/python-2.7.1.msi +7z x python-2.7.1.msi -opython 7z x python/python -opython # boost-python -wget http://sourceforge.net/projects/boost/files/boost/1.44.0/boost_1_44_0.tar.bz2/download -tar xf boost_1_44_0.tar.bz2 -cd boost_1_44_0 +wget --trust-server-names=on http://sourceforge.net/projects/boost/files/boost/1.45.0/boost_1_45_0.tar.bz2/download +tar xf boost_1_45_0.tar.bz2 +cd boost_1_45_0 echo "using gcc : : $COMPILER : <compileflags>-I$BASE/python <archiver>$ARCHIVER ;" >> tools/build/v2/user-config.jam bjam install -j 3 --prefix=$BASE/boost --with-python toolset=gcc link=static cd .. diff --git a/doc/developers.rst b/doc/developers.rst index c1f8a75..15b18f6 100644 --- a/doc/developers.rst +++ b/doc/developers.rst @@ -104,9 +104,9 @@ Read the comments in the header of the script to know the pre-requisites. The result of the compilation is a DLL, ``libexiv2python.pyd``, in the build directory. This file and the ``pyexiv2`` folder in ``src`` should be copied to -the system-wide site directory of a Python 2.6 setup -(typically ``C:\Python26\Lib\site-packages\``) or to the user site directory -(``%APPDATA%\Python\Python26\site-packages\``). +the system-wide site directory of a Python 2.7 setup +(typically ``C:\Python27\Lib\site-packages\``) or to the user site directory +(``%APPDATA%\Python\Python27\site-packages\``). The top-level directory of the branch also contains an NSIS installer script named ``win32-installer.nsi``. diff --git a/src/exiv2wrapper.cpp b/src/exiv2wrapper.cpp index 4512222..4df23e9 100644 --- a/src/exiv2wrapper.cpp +++ b/src/exiv2wrapper.cpp @@ -233,7 +233,7 @@ const ExifTag Image::getExifTag(std::string key) throw Exiv2::Error(KEY_NOT_FOUND, key); } - return ExifTag(key, &(*_exifData)[key], _exifData); + return ExifTag(key, &(*_exifData)[key], _exifData, _image->byteOrder()); } void Image::deleteExifTag(std::string key) @@ -453,6 +453,12 @@ std::string Image::getDataBuffer() const return buffer; } +Exiv2::ByteOrder Image::getByteOrder() const +{ + CHECK_METADATA_READ + return _image->byteOrder(); +} + Exiv2::ExifThumb* Image::_getExifThumbnail() { CHECK_METADATA_READ @@ -513,7 +519,10 @@ void Image::setExifThumbnailFromData(const std::string& data) } -ExifTag::ExifTag(const std::string& key, Exiv2::Exifdatum* datum, Exiv2::ExifData* data): _key(key) +ExifTag::ExifTag(const std::string& key, + Exiv2::Exifdatum* datum, Exiv2::ExifData* data, + Exiv2::ByteOrder byteOrder): + _key(key), _byteOrder(byteOrder) { if (datum != 0 && data != 0) { @@ -526,6 +535,20 @@ ExifTag::ExifTag(const std::string& key, Exiv2::Exifdatum* datum, Exiv2::ExifDat _data = 0; } +// Conditional code, exiv2 0.21 changed APIs we need +// (see https://bugs.launchpad.net/pyexiv2/+bug/684177). +#if EXIV2_TEST_VERSION(0,21,0) + Exiv2::ExifKey exifKey(key); + _type = Exiv2::TypeInfo::typeName(exifKey.defaultTypeId()); + _name = exifKey.tagName(); + _label = exifKey.tagLabel(); + _description = exifKey.tagDesc(); + _sectionName = Exiv2::ExifTags::sectionName(exifKey); + // The section description is not exposed in the API any longer + // (see http://dev.exiv2.org/issues/744). For want of anything better, + // fall back on the section’s name. + _sectionDescription = _sectionName; +#else const uint16_t tag = _datum->tag(); const Exiv2::IfdId ifd = _datum->ifdId(); _type = Exiv2::TypeInfo::typeName(Exiv2::ExifTags::tagType(tag, ifd)); @@ -534,6 +557,7 @@ ExifTag::ExifTag(const std::string& key, Exiv2::Exifdatum* datum, Exiv2::ExifDat _description = Exiv2::ExifTags::tagDesc(tag, ifd); _sectionName = Exiv2::ExifTags::sectionName(tag, ifd); _sectionDescription = Exiv2::ExifTags::sectionDesc(tag, ifd); +#endif } ExifTag::~ExifTag() @@ -568,6 +592,8 @@ void ExifTag::setParentImage(Image& image) delete _datum; _datum = &(*_data)[_key.key()]; _datum->setValue(value); + + _byteOrder = image.getByteOrder(); } const std::string ExifTag::getKey() @@ -615,6 +641,11 @@ const std::string ExifTag::getHumanValue() return _datum->print(_data); } +int ExifTag::getByteOrder() +{ + return _byteOrder; +} + IptcTag::IptcTag(const std::string& key, Exiv2::IptcData* data): _key(key) { diff --git a/src/exiv2wrapper.hpp b/src/exiv2wrapper.hpp index 3c87628..f3a0bd6 100644 --- a/src/exiv2wrapper.hpp +++ b/src/exiv2wrapper.hpp @@ -42,7 +42,9 @@ class ExifTag { public: // Constructor - ExifTag(const std::string& key, Exiv2::Exifdatum* datum=0, Exiv2::ExifData* data=0); + ExifTag(const std::string& key, + Exiv2::Exifdatum* datum=0, Exiv2::ExifData* data=0, + Exiv2::ByteOrder byteOrder=Exiv2::invalidByteOrder); ~ExifTag(); @@ -58,6 +60,7 @@ public: const std::string getSectionDescription(); const std::string getRawValue(); const std::string getHumanValue(); + int getByteOrder(); private: Exiv2::ExifKey _key; @@ -69,6 +72,7 @@ private: std::string _description; std::string _sectionName; std::string _sectionDescription; + int _byteOrder; }; @@ -250,6 +254,8 @@ public: Exiv2::IptcData* getIptcData() { return _iptcData; }; Exiv2::XmpData* getXmpData() { return _xmpData; }; + Exiv2::ByteOrder getByteOrder() const; + private: std::string _filename; Exiv2::byte* _data; diff --git a/src/exiv2wrapper_python.cpp b/src/exiv2wrapper_python.cpp index bd20774..ac51fae 100644 --- a/src/exiv2wrapper_python.cpp +++ b/src/exiv2wrapper_python.cpp @@ -64,6 +64,7 @@ BOOST_PYTHON_MODULE(libexiv2python) .def("_getSectionDescription", &ExifTag::getSectionDescription) .def("_getRawValue", &ExifTag::getRawValue) .def("_getHumanValue", &ExifTag::getHumanValue) + .def("_getByteOrder", &ExifTag::getByteOrder) ; class_<IptcTag>("_IptcTag", init<std::string>()) diff --git a/src/pyexiv2/exif.py b/src/pyexiv2/exif.py index 0acb775..0e4d308 100644 --- a/src/pyexiv2/exif.py +++ b/src/pyexiv2/exif.py @@ -30,11 +30,13 @@ EXIF specific code. import libexiv2python -from pyexiv2.utils import Rational, NotifyingList, ListenerInterface, \ +from pyexiv2.utils import Rational, Fraction, \ + NotifyingList, ListenerInterface, \ undefined_to_string, string_to_undefined import time import datetime +import sys class ExifValueError(ValueError): @@ -225,6 +227,31 @@ class ExifTag(ListenerInterface): # 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. @@ -263,10 +290,17 @@ class ExifTag(ListenerInterface): return value elif self.type == 'Comment': - # There is currently no charset conversion. - # TODO: guess the encoding and decode accordingly into unicode - # where relevant. - return value + 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: @@ -311,89 +345,106 @@ class ExifTag(ListenerInterface): :raise ExifValueError: if the conversion fails """ if self.type == 'Ascii': - if type(value) is datetime.datetime: + if isinstance(value, datetime.datetime): return value.strftime(self._datetime_formats[0]) - elif type(value) is datetime.date: + 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 type(value) is unicode: + elif isinstance(value, unicode): try: return value.encode('utf-8') except UnicodeEncodeError: raise ExifValueError(value, self.type) - elif type(value) is str: + elif isinstance(value, str): return value else: raise ExifValueError(value, self.type) elif self.type in ('Byte', 'SByte'): - if type(value) is unicode: + if isinstance(value, unicode): try: return value.encode('utf-8') except UnicodeEncodeError: raise ExifValueError(value, self.type) - elif type(value) is str: + elif isinstance(value, str): return value else: raise ExifValueError(value, self.type) elif self.type == 'Comment': - if type(value) is unicode: + 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 type(value) is str: + elif isinstance(value, str): return value else: raise ExifValueError(value, self.type) elif self.type == 'Short': - if type(value) is int and value >= 0: + if isinstance(value, int) and value >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SShort': - if type(value) is int: + if isinstance(value, int): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Long': - if type(value) in (int, long) and value >= 0: + if isinstance(value, (int, long)) and value >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SLong': - if type(value) in (int, long): + if isinstance(value, (int, long)): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Rational': - if type(value) is Rational and value.numerator >= 0: + if (isinstance(value, Rational) or \ + (Fraction is not None and isinstance(value, Fraction))) \ + and value.numerator >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SRational': - if type(value) is Rational: + if isinstance(value, Rational) or \ + (Fraction is not None and isinstance(value, Fraction)): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Undefined': - if type(value) is unicode: + if isinstance(value, unicode): try: return string_to_undefined(value.encode('utf-8')) except UnicodeEncodeError: raise ExifValueError(value, self.type) - elif type(value) is str: + elif isinstance(value, str): return string_to_undefined(value) else: raise ExifValueError(value, self.type) diff --git a/src/pyexiv2/iptc.py b/src/pyexiv2/iptc.py index e30921c..1e5429e 100644 --- a/src/pyexiv2/iptc.py +++ b/src/pyexiv2/iptc.py @@ -317,24 +317,24 @@ class IptcTag(ListenerInterface): :raise IptcValueError: if the conversion fails """ if self.type == 'Short': - if type(value) is int: + if isinstance(value, int): return str(value) else: raise IptcValueError(value, self.type) elif self.type == 'String': - if type(value) is unicode: + if isinstance(value, unicode): try: return value.encode('utf-8') except UnicodeEncodeError: raise IptcValueError(value, self.type) - elif type(value) is str: + elif isinstance(value, str): return value else: raise IptcValueError(value, self.type) elif self.type == 'Date': - if type(value) in (datetime.date, datetime.datetime): + 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 @@ -345,7 +345,7 @@ class IptcTag(ListenerInterface): raise IptcValueError(value, self.type) elif self.type == 'Time': - if type(value) in (datetime.time, datetime.datetime): + if isinstance(value, (datetime.time, datetime.datetime)): r = value.strftime('%H%M%S') if value.tzinfo is not None: r += value.strftime('%z') @@ -356,7 +356,7 @@ class IptcTag(ListenerInterface): raise IptcValueError(value, self.type) elif self.type == 'Undefined': - if type(value) is str: + if isinstance(value, str): return value else: raise IptcValueError(value, self.type) diff --git a/src/pyexiv2/metadata.py b/src/pyexiv2/metadata.py index a121286..8adeab6 100644 --- a/src/pyexiv2/metadata.py +++ b/src/pyexiv2/metadata.py @@ -61,7 +61,7 @@ class ImageMetadata(MutableMapping): self.filename = filename if filename is not None: self.filename = filename.encode(sys.getfilesystemencoding()) - self._image = None + self.__image = None self._keys = {'exif': None, 'iptc': None, 'xmp': None} self._tags = {'exif': {}, 'iptc': {}, 'xmp': {}} self._exif_thumbnail = None @@ -86,9 +86,15 @@ class ImageMetadata(MutableMapping): :type buffer: string """ obj = cls(None) - obj._image = libexiv2python._Image(buffer, len(buffer)) + obj.__image = libexiv2python._Image(buffer, len(buffer)) return obj + @property + def _image(self): + if self.__image is None: + raise IOError('Image metadata has not been read yet') + return self.__image + def read(self): """ Read the metadata embedded in the associated image. @@ -96,9 +102,9 @@ class ImageMetadata(MutableMapping): the metadata (an exception will be raised if trying to access metadata before calling this method). """ - if self._image is None: - self._image = self._instantiate_image(self.filename) - self._image._readMetadata() + if self.__image is None: + self.__image = self._instantiate_image(self.filename) + self.__image._readMetadata() def write(self, preserve_timestamps=False): """ diff --git a/src/pyexiv2/utils.py b/src/pyexiv2/utils.py index 64138d2..5b0a3d5 100644 --- a/src/pyexiv2/utils.py +++ b/src/pyexiv2/utils.py @@ -31,6 +31,16 @@ Utilitary classes and functions. import datetime import re +# Support for fractions.Fraction as a replacement for the Rational class is not +# implemented yet as we have to support versions of Python < 2.6 +# (see https://launchpad.net/bugs/514415). +# However, it doesn’t hurt to accept Fraction objects as values when the module +# is available (see https://launchpad.net/bugs/683232). +try: + from fractions import Fraction +except ImportError: + Fraction = None + class FixedOffset(datetime.tzinfo): diff --git a/src/pyexiv2/xmp.py b/src/pyexiv2/xmp.py index c6488a1..8223a24 100644 --- a/src/pyexiv2/xmp.py +++ b/src/pyexiv2/xmp.py @@ -30,7 +30,7 @@ XMP specific code. import libexiv2python -from pyexiv2.utils import FixedOffset, Rational, GPSCoordinate +from pyexiv2.utils import FixedOffset, Rational, Fraction, GPSCoordinate import datetime import re @@ -435,7 +435,8 @@ class XmpTag(object): raise XmpValueError(value, type) elif type == 'Rational': - if isinstance(value, Rational): + if isinstance(value, Rational) or \ + (Fraction is not None and isinstance(value, Fraction)): return str(value) else: raise XmpValueError(value, type) diff --git a/test/ReadMetadataTestCase.py b/test/ReadMetadataTestCase.py index 1ca9e5d..40f0843 100644 --- a/test/ReadMetadataTestCase.py +++ b/test/ReadMetadataTestCase.py @@ -38,12 +38,12 @@ class ReadMetadataTestCase(unittest.TestCase): """ def check_type_and_value(self, tag, etype, evalue): - self.assertEqual(type(tag.value), etype) + self.assert_(isinstance(tag.value, etype)) self.assertEqual(tag.value, evalue) def check_type_and_values(self, tag, etype, evalues): for value in tag.value: - self.assertEqual(type(value), etype) + self.assert_(isinstance(value, etype)) self.assertEqual(tag.value, evalues) def assertCorrectFile(self, filename, md5sum): diff --git a/test/TestsRunner.py b/test/TestsRunner.py index 22b94b9..8585295 100755 --- a/test/TestsRunner.py +++ b/test/TestsRunner.py @@ -39,6 +39,7 @@ from metadata import TestImageMetadata from buffer import TestBuffer from encoding import TestEncodings from utils import TestConversions +from usercomment import TestUserCommentReadWrite, TestUserCommentAdd def run_unit_tests(): @@ -56,6 +57,8 @@ def run_unit_tests(): suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestBuffer)) suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestEncodings)) suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestConversions)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestUserCommentReadWrite)) + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestUserCommentAdd)) # Run the test suite return unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/test/data/MD5SUMS b/test/data/MD5SUMS index c1c998e..c664932 100644 --- a/test/data/MD5SUMS +++ b/test/data/MD5SUMS @@ -1,3 +1,6 @@ -64d4b7eab1e78f1f6bfb3c966e99eef2 test/data/exiv2-bug540.jpg +e8525211f62a0944db9072f73750258c empty.jpg +64d4b7eab1e78f1f6bfb3c966e99eef2 exiv2-bug540.jpg c066958457c685853293058f9bf129c1 smiley1.jpg - +ad29ac65fb6f63c8361aaed6cb02f8c7 usercomment-ascii.jpg +13b7cc09129a8677f2cf18634f5abd3c usercomment-unicode-ii.jpg +7addfed7823c556ba489cd4ab2037200 usercomment-unicode-mm.jpg diff --git a/test/data/empty.jpg b/test/data/empty.jpg Binary files differnew file mode 100644 index 0000000..be0e8b3 --- /dev/null +++ b/test/data/empty.jpg diff --git a/test/data/usercomment-ascii.jpg b/test/data/usercomment-ascii.jpg Binary files differnew file mode 100644 index 0000000..dca5ed3 --- /dev/null +++ b/test/data/usercomment-ascii.jpg diff --git a/test/data/usercomment-unicode-ii.jpg b/test/data/usercomment-unicode-ii.jpg Binary files differnew file mode 100644 index 0000000..f177d52 --- /dev/null +++ b/test/data/usercomment-unicode-ii.jpg diff --git a/test/data/usercomment-unicode-mm.jpg b/test/data/usercomment-unicode-mm.jpg Binary files differnew file mode 100644 index 0000000..8035912 --- /dev/null +++ b/test/data/usercomment-unicode-mm.jpg diff --git a/test/exif.py b/test/exif.py index 4ddb360..1f929b7 100644 --- a/test/exif.py +++ b/test/exif.py @@ -27,7 +27,7 @@ import unittest from pyexiv2.exif import ExifTag, ExifValueError -from pyexiv2.utils import Rational +from pyexiv2.utils import Rational, Fraction import datetime @@ -135,6 +135,10 @@ class TestExifTag(unittest.TestCase): tag = ExifTag('Exif.Photo.UserComment') self.assertEqual(tag.type, 'Comment') self.assertEqual(tag._convert_to_python('A comment'), 'A comment') + for charset in ('Ascii', 'Jis', 'Unicode', 'Undefined', 'InvalidCharsetId'): + self.assertEqual(tag._convert_to_python('charset="%s" A comment' % charset), 'A comment') + for charset in ('Ascii', 'Jis', 'Undefined', 'InvalidCharsetId'): + self.failIfEqual(tag._convert_to_python('charset="%s" déjà vu' % charset), u'déjà vu') def test_convert_to_string_comment(self): # Valid values @@ -142,6 +146,12 @@ class TestExifTag(unittest.TestCase): self.assertEqual(tag.type, 'Comment') self.assertEqual(tag._convert_to_string('A comment'), 'A comment') self.assertEqual(tag._convert_to_string(u'A comment'), 'A comment') + charsets = ('Ascii', 'Jis', 'Unicode', 'Undefined') + for charset in charsets: + tag.raw_value = 'charset="%s" foo' % charset + self.assertEqual(tag._convert_to_string('A comment'), + 'charset="%s" A comment' % charset) + self.assertEqual(tag._convert_to_string('déjà vu'), 'déjà vu') # Invalid values self.failUnlessRaises(ExifValueError, tag._convert_to_string, None) @@ -263,6 +273,8 @@ class TestExifTag(unittest.TestCase): tag = ExifTag('Exif.Image.XResolution') self.assertEqual(tag.type, 'Rational') self.assertEqual(tag._convert_to_string(Rational(5, 3)), '5/3') + if Fraction is not None: + self.assertEqual(tag._convert_to_string(Fraction('1.6')), '8/5') # Invalid values self.failUnlessRaises(ExifValueError, tag._convert_to_string, 'invalid') @@ -287,6 +299,9 @@ class TestExifTag(unittest.TestCase): self.assertEqual(tag.type, 'SRational') self.assertEqual(tag._convert_to_string(Rational(5, 3)), '5/3') self.assertEqual(tag._convert_to_string(Rational(-5, 3)), '-5/3') + if Fraction is not None: + self.assertEqual(tag._convert_to_string(Fraction('1.6')), '8/5') + self.assertEqual(tag._convert_to_string(Fraction('-1.6')), '-8/5') # Invalid values self.failUnlessRaises(ExifValueError, tag._convert_to_string, 'invalid') diff --git a/test/metadata.py b/test/metadata.py index a4994d4..bd43c6f 100644 --- a/test/metadata.py +++ b/test/metadata.py @@ -35,25 +35,7 @@ import os import tempfile import time import unittest - - -EMPTY_JPG_DATA = \ - '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb' \ - '\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' \ - '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' \ - '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' \ - '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x0b\x08' \ - '\x00\x01\x00\x01\x01\x01\x11\x00\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01' \ - '\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06' \ - '\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05' \ - '\x05\x04\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13Qa' \ - '\x07"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br\x82\t\n\x16\x17' \ - "\x18\x19\x1a%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x83\x84\x85" \ - '\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5' \ - '\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5' \ - '\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2\xe3\xe4' \ - '\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xda' \ - '\x00\x08\x01\x01\x00\x00?\x00\x92\xbf\xff\xd9' +from testutils import EMPTY_JPG_DATA class TestImageMetadata(unittest.TestCase): @@ -83,8 +65,41 @@ class TestImageMetadata(unittest.TestCase): # Test general methods ###################### + def test_not_read_raises(self): + # http://bugs.launchpad.net/pyexiv2/+bug/687373 + self.assertRaises(IOError, self.metadata.write) + self.assertRaises(IOError, self.metadata.__getattribute__, 'dimensions') + self.assertRaises(IOError, self.metadata.__getattribute__, 'mime_type') + self.assertRaises(IOError, self.metadata.__getattribute__, 'exif_keys') + self.assertRaises(IOError, self.metadata.__getattribute__, 'iptc_keys') + self.assertRaises(IOError, self.metadata.__getattribute__, 'xmp_keys') + self.assertRaises(IOError, self.metadata._get_exif_tag, 'Exif.Image.Make') + self.assertRaises(IOError, self.metadata._get_iptc_tag, 'Iptc.Application2.Caption') + self.assertRaises(IOError, self.metadata._get_xmp_tag, 'Xmp.dc.format') + self.assertRaises(IOError, self.metadata._set_exif_tag, 'Exif.Image.Make', 'foobar') + self.assertRaises(IOError, self.metadata._set_iptc_tag, 'Iptc.Application2.Caption', ['foobar']) + self.assertRaises(IOError, self.metadata._set_xmp_tag, 'Xmp.dc.format', ('foo', 'bar')) + self.assertRaises(IOError, self.metadata._delete_exif_tag, 'Exif.Image.Make') + self.assertRaises(IOError, self.metadata._delete_iptc_tag, 'Iptc.Application2.Caption') + self.assertRaises(IOError, self.metadata._delete_xmp_tag, 'Xmp.dc.format') + self.assertRaises(IOError, self.metadata._get_comment) + self.assertRaises(IOError, self.metadata._set_comment, 'foobar') + self.assertRaises(IOError, self.metadata._del_comment) + self.assertRaises(IOError, self.metadata.__getattribute__, 'previews') + other = ImageMetadata(self.pathname) + self.assertRaises(IOError, self.metadata.copy, other) + self.assertRaises(IOError, self.metadata.__getattribute__, 'buffer') + thumb = self.metadata.exif_thumbnail + self.assertRaises(IOError, thumb.__getattribute__, 'mime_type') + self.assertRaises(IOError, thumb.__getattribute__, 'extension') + self.assertRaises(IOError, thumb.write_to_file, '/tmp/foobar.jpg') + self.assertRaises(IOError, thumb.erase) + self.assertRaises(IOError, thumb.set_from_file, '/tmp/foobar.jpg') + self.assertRaises(IOError, thumb.__getattribute__, 'data') + self.assertRaises(IOError, thumb.__setattr__, 'data', EMPTY_JPG_DATA) + def test_read(self): - self.assertEqual(self.metadata._image, None) + self.assertRaises(IOError, self.metadata.__getattribute__, '_image') self.metadata.read() self.failIfEqual(self.metadata._image, None) @@ -119,7 +134,11 @@ class TestImageMetadata(unittest.TestCase): stat2 = os.stat(self.pathname) atime2 = round(stat2.st_atime) mtime2 = round(stat2.st_mtime) - self.failIfEqual(atime2, atime) + # It is not safe to assume that atime will have been modified when the + # file has been read, as it may depend on mount options (e.g. noatime, + # relatime). + # See discussion at http://bugs.launchpad.net/pyexiv2/+bug/624999. + #self.failIfEqual(atime2, atime) self.failIfEqual(mtime2, mtime) metadata.comment = 'Yesterday' time.sleep(1.1) @@ -147,7 +166,7 @@ class TestImageMetadata(unittest.TestCase): # Get an existing tag key = 'Exif.Image.Make' tag = self.metadata._get_exif_tag(key) - self.assertEqual(type(tag), ExifTag) + self.assert_(isinstance(tag, ExifTag)) self.assertEqual(self.metadata._tags['exif'][key], tag) # Try to get an nonexistent tag key = 'Exif.Photo.Sharpness' @@ -255,7 +274,7 @@ class TestImageMetadata(unittest.TestCase): # Get an existing tag key = 'Iptc.Application2.DateCreated' tag = self.metadata._get_iptc_tag(key) - self.assertEqual(type(tag), IptcTag) + self.assert_(isinstance(tag, IptcTag)) self.assertEqual(self.metadata._tags['iptc'][key], tag) # Try to get an nonexistent tag key = 'Iptc.Application2.Copyright' @@ -363,7 +382,7 @@ class TestImageMetadata(unittest.TestCase): # Get an existing tag key = 'Xmp.dc.subject' tag = self.metadata._get_xmp_tag(key) - self.assertEqual(type(tag), XmpTag) + self.assert_(isinstance(tag, XmpTag)) self.assertEqual(self.metadata._tags['xmp'][key], tag) # Try to get an nonexistent tag key = 'Xmp.xmp.Label' @@ -464,13 +483,13 @@ class TestImageMetadata(unittest.TestCase): # Get existing tags key = 'Exif.Image.DateTime' tag = self.metadata[key] - self.assertEqual(type(tag), ExifTag) + self.assert_(isinstance(tag, ExifTag)) key = 'Iptc.Application2.Caption' tag = self.metadata[key] - self.assertEqual(type(tag), IptcTag) + self.assert_(isinstance(tag, IptcTag)) key = 'Xmp.dc.format' tag = self.metadata[key] - self.assertEqual(type(tag), XmpTag) + self.assert_(isinstance(tag, XmpTag)) # Try to get nonexistent tags keys = ('Exif.Image.SamplesPerPixel', 'Iptc.Application2.FixtureId', 'Xmp.xmp.Rating', 'Wrong.Noluck.Raise') @@ -701,7 +720,7 @@ class TestImageMetadata(unittest.TestCase): os.remove(pathname) thumb.write_to_file(pathname) pathname = pathname + thumb.extension - fd = open(pathname) + fd = open(pathname, 'rb') self.assertEqual(fd.read(), EMPTY_JPG_DATA) fd.close() os.remove(pathname) diff --git a/test/testutils.py b/test/testutils.py index 1fe9573..e093a3e 100644 --- a/test/testutils.py +++ b/test/testutils.py @@ -31,6 +31,25 @@ import os.path import hashlib +EMPTY_JPG_DATA = \ + '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb' \ + '\x00C\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' \ + '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' \ + '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' \ + '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x0b\x08' \ + '\x00\x01\x00\x01\x01\x01\x11\x00\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01' \ + '\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06' \ + '\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05' \ + '\x05\x04\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13Qa' \ + '\x07"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br\x82\t\n\x16\x17' \ + "\x18\x19\x1a%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x83\x84\x85" \ + '\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5' \ + '\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5' \ + '\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2\xe3\xe4' \ + '\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xda' \ + '\x00\x08\x01\x01\x00\x00?\x00\x92\xbf\xff\xd9' + + def get_absolute_file_path(filepath): """ Return the absolute file path for the file path given in argument, diff --git a/test/usercomment.py b/test/usercomment.py new file mode 100644 index 0000000..cc8ff1a --- /dev/null +++ b/test/usercomment.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +# ****************************************************************************** +# +# Copyright (C) 2010 Olivier Tilloy <olivier@tilloy.net> +# +# This file is part of the pyexiv2 distribution. +# +# pyexiv2 is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# pyexiv2 is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyexiv2; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. +# +# Author: Olivier Tilloy <olivier@tilloy.net> +# +# ****************************************************************************** + +from pyexiv2.metadata import ImageMetadata + +import unittest +import testutils +import os +import tempfile +from testutils import EMPTY_JPG_DATA + + +class TestUserCommentReadWrite(unittest.TestCase): + + checksums = { + 'usercomment-ascii.jpg': 'ad29ac65fb6f63c8361aaed6cb02f8c7', + 'usercomment-unicode-ii.jpg': '13b7cc09129a8677f2cf18634f5abd3c', + 'usercomment-unicode-mm.jpg': '7addfed7823c556ba489cd4ab2037200', + } + + def _read_image(self, filename): + filepath = testutils.get_absolute_file_path(os.path.join('data', filename)) + self.assert_(testutils.CheckFileSum(filepath, self.checksums[filename])) + m = ImageMetadata(filepath) + m.read() + return m + + def _expected_raw_value(self, endianness, value): + from pyexiv2 import __exiv2_version__ + if __exiv2_version__ >= '0.20': + return value + else: + encodings = {'ii': 'utf-16le', 'mm': 'utf-16be'} + return value.decode('utf-8').encode(encodings[endianness]) + + def test_read_ascii(self): + m = self._read_image('usercomment-ascii.jpg') + tag = m['Exif.Photo.UserComment'] + self.assertEqual(tag.raw_value, 'charset="Ascii" deja vu') + self.assertEqual(tag.value, u'deja vu') + + def test_read_unicode_little_endian(self): + m = self._read_image('usercomment-unicode-ii.jpg') + tag = m['Exif.Photo.UserComment'] + self.assertEqual(tag.raw_value, 'charset="Unicode" %s' % self._expected_raw_value('ii', 'déjà vu')) + self.assertEqual(tag.value, u'déjà vu') + + def test_read_unicode_big_endian(self): + m = self._read_image('usercomment-unicode-mm.jpg') + tag = m['Exif.Photo.UserComment'] + self.assertEqual(tag.raw_value, 'charset="Unicode" %s' % self._expected_raw_value('mm', 'déjà vu')) + self.assertEqual(tag.value, u'déjà vu') + + def test_write_ascii(self): + m = self._read_image('usercomment-ascii.jpg') + tag = m['Exif.Photo.UserComment'] + tag.value = 'foo bar' + self.assertEqual(tag.raw_value, 'charset="Ascii" foo bar') + self.assertEqual(tag.value, u'foo bar') + + def test_write_unicode_over_ascii(self): + m = self._read_image('usercomment-ascii.jpg') + tag = m['Exif.Photo.UserComment'] + tag.value = u'déjà vu' + self.assertEqual(tag.raw_value, 'déjà vu') + self.assertEqual(tag.value, u'déjà vu') + + def test_write_unicode_little_endian(self): + m = self._read_image('usercomment-unicode-ii.jpg') + tag = m['Exif.Photo.UserComment'] + tag.value = u'DÉJÀ VU' + self.assertEqual(tag.raw_value, 'charset="Unicode" %s' % self._expected_raw_value('ii', 'DÉJÀ VU')) + self.assertEqual(tag.value, u'DÉJÀ VU') + + def test_write_unicode_big_endian(self): + m = self._read_image('usercomment-unicode-mm.jpg') + tag = m['Exif.Photo.UserComment'] + tag.value = u'DÉJÀ VU' + self.assertEqual(tag.raw_value, 'charset="Unicode" %s' % self._expected_raw_value('mm', 'DÉJÀ VU')) + self.assertEqual(tag.value, u'DÉJÀ VU') + + +class TestUserCommentAdd(unittest.TestCase): + + def setUp(self): + # Create an empty image file + fd, self.pathname = tempfile.mkstemp(suffix='.jpg') + os.write(fd, EMPTY_JPG_DATA) + os.close(fd) + + def tearDown(self): + os.remove(self.pathname) + + def _test_add_comment(self, value): + metadata = ImageMetadata(self.pathname) + metadata.read() + key = 'Exif.Photo.UserComment' + metadata[key] = value + metadata.write() + + metadata = ImageMetadata(self.pathname) + metadata.read() + self.assert_(key in metadata.exif_keys) + tag = metadata[key] + self.assertEqual(tag.value, value) + + def test_add_comment_ascii(self): + self._test_add_comment('deja vu') + + def test_add_comment_unicode(self): + self._test_add_comment(u'déjà vu') + diff --git a/test/xmp.py b/test/xmp.py index cfcf2fd..147bd93 100644 --- a/test/xmp.py +++ b/test/xmp.py @@ -28,7 +28,7 @@ import unittest from pyexiv2.xmp import XmpTag, XmpValueError, register_namespace, \ unregister_namespace, unregister_namespaces -from pyexiv2.utils import FixedOffset +from pyexiv2.utils import FixedOffset, Rational, Fraction from pyexiv2.metadata import ImageMetadata import datetime @@ -283,6 +283,31 @@ class TestXmpTag(unittest.TestCase): # Invalid values self.failUnlessRaises(XmpValueError, tag._convert_to_string, None, 'URL') + def test_convert_to_python_rational(self): + # Valid values + tag = XmpTag('Xmp.xmpDM.videoPixelAspectRatio') + self.assertEqual(tag.type, 'Rational') + self.assertEqual(tag._convert_to_python('5/3', 'Rational'), Rational(5, 3)) + self.assertEqual(tag._convert_to_python('-5/3', 'Rational'), Rational(-5, 3)) + + # Invalid values + self.failUnlessRaises(XmpValueError, tag._convert_to_python, 'invalid', 'Rational') + self.failUnlessRaises(XmpValueError, tag._convert_to_python, '5 / 3', 'Rational') + self.failUnlessRaises(XmpValueError, tag._convert_to_python, '5/-3', 'Rational') + + def test_convert_to_string_rational(self): + # Valid values + tag = XmpTag('Xmp.xmpDM.videoPixelAspectRatio') + self.assertEqual(tag.type, 'Rational') + self.assertEqual(tag._convert_to_string(Rational(5, 3), 'Rational'), '5/3') + self.assertEqual(tag._convert_to_string(Rational(-5, 3), 'Rational'), '-5/3') + if Fraction is not None: + self.assertEqual(tag._convert_to_string(Fraction('1.6'), 'Rational'), '8/5') + self.assertEqual(tag._convert_to_string(Fraction('-1.6'), 'Rational'), '-8/5') + + # Invalid values + self.failUnlessRaises(XmpValueError, tag._convert_to_string, 'invalid', 'Rational') + # TODO: other types |