aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xcross-compile.sh20
-rw-r--r--doc/developers.rst6
-rw-r--r--src/exiv2wrapper.cpp35
-rw-r--r--src/exiv2wrapper.hpp8
-rw-r--r--src/exiv2wrapper_python.cpp1
-rw-r--r--src/pyexiv2/exif.py93
-rw-r--r--src/pyexiv2/iptc.py12
-rw-r--r--src/pyexiv2/metadata.py16
-rw-r--r--src/pyexiv2/utils.py10
-rw-r--r--src/pyexiv2/xmp.py5
-rw-r--r--test/ReadMetadataTestCase.py4
-rwxr-xr-xtest/TestsRunner.py3
-rw-r--r--test/data/MD5SUMS7
-rw-r--r--test/data/empty.jpgbin0 -> 332 bytes
-rw-r--r--test/data/usercomment-ascii.jpgbin0 -> 514 bytes
-rw-r--r--test/data/usercomment-unicode-ii.jpgbin0 -> 520 bytes
-rw-r--r--test/data/usercomment-unicode-mm.jpgbin0 -> 520 bytes
-rw-r--r--test/exif.py17
-rw-r--r--test/metadata.py75
-rw-r--r--test/testutils.py19
-rw-r--r--test/usercomment.py135
-rw-r--r--test/xmp.py27
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
new file mode 100644
index 0000000..be0e8b3
--- /dev/null
+++ b/test/data/empty.jpg
Binary files differ
diff --git a/test/data/usercomment-ascii.jpg b/test/data/usercomment-ascii.jpg
new file mode 100644
index 0000000..dca5ed3
--- /dev/null
+++ b/test/data/usercomment-ascii.jpg
Binary files differ
diff --git a/test/data/usercomment-unicode-ii.jpg b/test/data/usercomment-unicode-ii.jpg
new file mode 100644
index 0000000..f177d52
--- /dev/null
+++ b/test/data/usercomment-unicode-ii.jpg
Binary files differ
diff --git a/test/data/usercomment-unicode-mm.jpg b/test/data/usercomment-unicode-mm.jpg
new file mode 100644
index 0000000..8035912
--- /dev/null
+++ b/test/data/usercomment-unicode-mm.jpg
Binary files differ
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