diff options
-rw-r--r-- | doc/api.rst | 2 | ||||
-rw-r--r-- | doc/tutorial.rst | 10 | ||||
-rw-r--r-- | src/exiv2wrapper.cpp | 65 | ||||
-rw-r--r-- | src/exiv2wrapper.hpp | 11 | ||||
-rw-r--r-- | src/exiv2wrapper_python.cpp | 8 | ||||
-rw-r--r-- | src/pyexiv2/__init__.py | 2 | ||||
-rw-r--r-- | src/pyexiv2/exif.py | 75 | ||||
-rw-r--r-- | src/pyexiv2/metadata.py | 10 | ||||
-rw-r--r-- | test/metadata.py | 94 |
9 files changed, 275 insertions, 2 deletions
diff --git a/doc/api.rst b/doc/api.rst index c1e17b2..a06b552 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -27,6 +27,8 @@ pyexiv2.exif .. autoclass:: ExifTag :members: key, type, name, label, description, section_name, section_description, raw_value, value, human_value +.. autoclass:: ExifThumbnail + :members: mime_type, extension, data, set_from_file, write_to_file, erase pyexiv2.iptc ############ diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 6161285..9f5a418 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -94,6 +94,16 @@ If the tag was not present, one is created and its value is set:: >>> metadata[key] = value +The EXIF data may optionally embed a thumbnail in the JPEG or TIFF format. +The thumbnail can be accessed, set from a JPEG file or buffer, saved to disk and +erased:: + + >>> thumb = metadata.exif_thumbnail + >>> thumb.set_from_file('/tmp/thumbnail.jpg') + >>> thumb.write_to_file('/tmp/copy') + >>> thumb.erase() + >>> metadata.write() + Reading and writing IPTC tags ############################# diff --git a/src/exiv2wrapper.cpp b/src/exiv2wrapper.cpp index a5ae4aa..0d80e05 100644 --- a/src/exiv2wrapper.cpp +++ b/src/exiv2wrapper.cpp @@ -44,6 +44,8 @@ namespace exiv2wrapper void Image::_instantiate_image() { + _exifThumbnail = 0; + // If an exception is thrown, it has to be done outside of the // Py_{BEGIN,END}_ALLOW_THREADS block. Exiv2::Error error(0); @@ -117,6 +119,10 @@ Image::~Image() { delete[] _data; } + if (_exifThumbnail != 0) + { + delete _exifThumbnail; + } } void Image::readMetadata() @@ -444,6 +450,65 @@ std::string Image::getDataBuffer() const return buffer; } +Exiv2::ExifThumb* Image::_getExifThumbnail() +{ + CHECK_METADATA_READ + if (_exifThumbnail == 0) + { + _exifThumbnail = new Exiv2::ExifThumb(*_exifData); + } + return _exifThumbnail; +} + +const std::string Image::getExifThumbnailMimeType() +{ + return std::string(_getExifThumbnail()->mimeType()); +} + +const std::string Image::getExifThumbnailExtension() +{ + return std::string(_getExifThumbnail()->extension()); +} + +void Image::writeExifThumbnailToFile(const std::string& path) +{ + _getExifThumbnail()->writeFile(path); +} + +const std::string Image::getExifThumbnailData() +{ + Exiv2::DataBuf buffer = _getExifThumbnail()->copy(); + // Copy the data buffer in a string. Since the data buffer can contain null + // characters ('\x00'), the string cannot be simply constructed like that: + // data = std::string((char*) buffer.pData_); + // because it would be truncated after the first occurence of a null + // character. Therefore, it has to be copied character by character. + // First allocate the memory for the whole string... + std::string data = std::string(buffer.size_, ' '); + // ... then fill it with the raw data. + for(unsigned int i = 0; i < buffer.size_; ++i) + { + data[i] = buffer.pData_[i]; + } + return data; +} + +void Image::eraseExifThumbnail() +{ + _getExifThumbnail()->erase(); +} + +void Image::setExifThumbnailFromFile(const std::string& path) +{ + _getExifThumbnail()->setJpegThumbnail(path); +} + +void Image::setExifThumbnailFromData(const std::string& data) +{ + const Exiv2::byte* buffer = (const Exiv2::byte*) data.c_str(); + _getExifThumbnail()->setJpegThumbnail(buffer, data.size()); +} + ExifTag::ExifTag(const std::string& key, Exiv2::Exifdatum* datum, Exiv2::ExifData* data): _key(key) { diff --git a/src/exiv2wrapper.hpp b/src/exiv2wrapper.hpp index 5abf07d..5058d56 100644 --- a/src/exiv2wrapper.hpp +++ b/src/exiv2wrapper.hpp @@ -230,6 +230,15 @@ public: // Read access to the thumbnail embedded in the image. boost::python::list previews(); + // Manipulate the JPEG/TIFF thumbnail embedded in the EXIF data. + const std::string getExifThumbnailMimeType(); + const std::string getExifThumbnailExtension(); + void writeExifThumbnailToFile(const std::string& path); + const std::string getExifThumbnailData(); + void eraseExifThumbnail(); + void setExifThumbnailFromFile(const std::string& path); + void setExifThumbnailFromData(const std::string& data); + // Copy the metadata to another image. void copyMetadata(Image& other, bool exif=true, bool iptc=true, bool xmp=true) const; @@ -249,6 +258,8 @@ private: Exiv2::ExifData* _exifData; Exiv2::IptcData* _iptcData; Exiv2::XmpData* _xmpData; + Exiv2::ExifThumb* _exifThumbnail; + Exiv2::ExifThumb* _getExifThumbnail(); // true if the image's internal metadata has already been read, // false otherwise diff --git a/src/exiv2wrapper_python.cpp b/src/exiv2wrapper_python.cpp index d6a1425..5b793f7 100644 --- a/src/exiv2wrapper_python.cpp +++ b/src/exiv2wrapper_python.cpp @@ -144,6 +144,14 @@ BOOST_PYTHON_MODULE(libexiv2python) .def("_copyMetadata", &Image::copyMetadata) .def("_getDataBuffer", &Image::getDataBuffer) + + .def("_getExifThumbnailMimeType", &Image::getExifThumbnailMimeType) + .def("_getExifThumbnailExtension", &Image::getExifThumbnailExtension) + .def("_writeExifThumbnailToFile", &Image::writeExifThumbnailToFile) + .def("_getExifThumbnailData", &Image::getExifThumbnailData) + .def("_eraseExifThumbnail", &Image::eraseExifThumbnail) + .def("_setExifThumbnailFromFile", &Image::setExifThumbnailFromFile) + .def("_setExifThumbnailFromData", &Image::setExifThumbnailFromData) ; } diff --git a/src/pyexiv2/__init__.py b/src/pyexiv2/__init__.py index 395aaff..54d7ebe 100644 --- a/src/pyexiv2/__init__.py +++ b/src/pyexiv2/__init__.py @@ -60,7 +60,7 @@ A typical use of this binding would be: import libexiv2python from pyexiv2.metadata import ImageMetadata -from pyexiv2.exif import ExifValueError, ExifTag +from pyexiv2.exif import ExifValueError, ExifTag, ExifThumbnail from pyexiv2.iptc import IptcValueError, IptcTag from pyexiv2.xmp import XmpValueError, XmpTag from pyexiv2.preview import Preview diff --git a/src/pyexiv2/exif.py b/src/pyexiv2/exif.py index b265d1f..0acb775 100644 --- a/src/pyexiv2/exif.py +++ b/src/pyexiv2/exif.py @@ -414,3 +414,78 @@ class ExifTag(ListenerInterface): right = self._raw_value return '<%s = %s>' % (left, right) + +class ExifThumbnail(object): + + """ + A thumbnail image optionally embedded in the IFD1 segment of the EXIF data. + + The image is either a TIFF or a JPEG image. + """ + + def __init__(self, _metadata): + self._metadata = _metadata + + @property + def mime_type(self): + """The mime type of the preview image (e.g. ``image/jpeg``).""" + return self._metadata._image._getExifThumbnailMimeType() + + @property + def extension(self): + """The file extension of the preview image with a leading dot + (e.g. ``.jpg``).""" + return self._metadata._image._getExifThumbnailExtension() + + def write_to_file(self, path): + """ + Write the thumbnail image to a file on disk. + The file extension will be automatically appended to the path. + + :param path: path to write the thumbnail to (without an extension) + :type path: string + """ + self._metadata._image._writeExifThumbnailToFile(path) + + def _update_exif_tags_cache(self): + # Update the cache of EXIF tags + keys = self._metadata._image._exifKeys() + self._metadata._keys['exif'] = keys + cached = self._metadata._tags['exif'].keys() + for key in cached: + if key not in keys: + del self._metadata._tags['exif'][key] + + def erase(self): + """ + Delete the thumbnail from the EXIF data. + Removes all Exif.Thumbnail.*, i.e. Exif IFD1 tags. + """ + self._metadata._image._eraseExifThumbnail() + self._update_exif_tags_cache() + + def set_from_file(self, path): + """ + Set the EXIF thumbnail to the JPEG image path. + This sets only the ``Compression``, ``JPEGInterchangeFormat`` and + ``JPEGInterchangeFormatLength`` tags, which is not all the thumbnail + EXIF information mandatory according to the EXIF standard + (but it is enough to work with the thumbnail). + + :param path: path to a JPEG file to set the thumbnail to + :type path: string + """ + self._metadata._image._setExifThumbnailFromFile(path) + self._update_exif_tags_cache() + + def _get_data(self): + return self._metadata._image._getExifThumbnailData() + + def _set_data(self, data): + self._metadata._image._setExifThumbnailFromData(data) + self._update_exif_tags_cache() + + data = property(fget=_get_data, fset=_set_data, + doc='The raw thumbnail data. Setting it is restricted to ' + + 'a buffer in the JPEG format.') + diff --git a/src/pyexiv2/metadata.py b/src/pyexiv2/metadata.py index 5ec296d..a121286 100644 --- a/src/pyexiv2/metadata.py +++ b/src/pyexiv2/metadata.py @@ -36,7 +36,7 @@ from itertools import chain import libexiv2python -from pyexiv2.exif import ExifTag +from pyexiv2.exif import ExifTag, ExifThumbnail from pyexiv2.iptc import IptcTag from pyexiv2.xmp import XmpTag from pyexiv2.preview import Preview @@ -64,6 +64,7 @@ class ImageMetadata(MutableMapping): self._image = None self._keys = {'exif': None, 'iptc': None, 'xmp': None} self._tags = {'exif': {}, 'iptc': {}, 'xmp': {}} + self._exif_thumbnail = None def _instantiate_image(self, filename): # This method is meant to be overridden in unit tests to easily replace @@ -392,3 +393,10 @@ class ImageMetadata(MutableMapping): """ return self._image._getDataBuffer() + @property + def exif_thumbnail(self): + """A thumbnail image optionally embedded in the EXIF data.""" + if self._exif_thumbnail is None: + self._exif_thumbnail = ExifThumbnail(self) + return self._exif_thumbnail + diff --git a/test/metadata.py b/test/metadata.py index 9ee7b32..a23abb9 100644 --- a/test/metadata.py +++ b/test/metadata.py @@ -647,3 +647,97 @@ class TestImageMetadata(unittest.TestCase): self.assertTrue('Exif.Photo.UserComment' not in self.clean) self.assertTrue('Iptc.Application2.Caption' not in self.clean) self.assertTrue('Xmp.dc.subject' not in self.clean) + + ########################### + # Test the EXIF thumbnail # + ########################### + + def _test_thumbnail_tags(self, there): + keys = ('Exif.Thumbnail.Compression', + 'Exif.Thumbnail.JPEGInterchangeFormat', + 'Exif.Thumbnail.JPEGInterchangeFormatLength') + for key in keys: + self.assertEqual(key in self.metadata.exif_keys, there) + + def test_no_exif_thumbnail(self): + self.metadata.read() + thumb = self.metadata.exif_thumbnail + self.assertEqual(thumb.mime_type, '') + self.assertEqual(thumb.extension, '') + self.assertEqual(thumb.data, '') + self._test_thumbnail_tags(False) + + def test_set_exif_thumbnail_from_data(self): + self.metadata.read() + self._test_thumbnail_tags(False) + thumb = self.metadata.exif_thumbnail + thumb.data = EMPTY_JPG_DATA + self.assertEqual(thumb.mime_type, 'image/jpeg') + self.assertEqual(thumb.extension, '.jpg') + self.assertEqual(thumb.data, EMPTY_JPG_DATA) + self._test_thumbnail_tags(True) + + def test_set_exif_thumbnail_from_file(self): + fd, pathname = tempfile.mkstemp(suffix='.jpg') + os.write(fd, EMPTY_JPG_DATA) + os.close(fd) + self.metadata.read() + self._test_thumbnail_tags(False) + thumb = self.metadata.exif_thumbnail + thumb.set_from_file(pathname) + os.remove(pathname) + self.assertEqual(thumb.mime_type, 'image/jpeg') + self.assertEqual(thumb.extension, '.jpg') + self.assertEqual(thumb.data, EMPTY_JPG_DATA) + self._test_thumbnail_tags(True) + + def test_write_exif_thumbnail_to_file(self): + self.metadata.read() + self._test_thumbnail_tags(False) + thumb = self.metadata.exif_thumbnail + thumb.data = EMPTY_JPG_DATA + fd, pathname = tempfile.mkstemp() + os.close(fd) + os.remove(pathname) + thumb.write_to_file(pathname) + pathname = pathname + thumb.extension + fd = open(pathname) + self.assertEqual(fd.read(), EMPTY_JPG_DATA) + fd.close() + os.remove(pathname) + + def test_erase_exif_thumbnail(self): + self.metadata.read() + self._test_thumbnail_tags(False) + thumb = self.metadata.exif_thumbnail + thumb.data = EMPTY_JPG_DATA + self.assertEqual(thumb.mime_type, 'image/jpeg') + self.assertEqual(thumb.extension, '.jpg') + self.assertEqual(thumb.data, EMPTY_JPG_DATA) + self._test_thumbnail_tags(True) + thumb.erase() + self.assertEqual(thumb.mime_type, '') + self.assertEqual(thumb.extension, '') + self.assertEqual(thumb.data, '') + self._test_thumbnail_tags(False) + + def test_set_exif_thumbnail_from_invalid_data(self): + # No check on the format of the buffer is performed, therefore it will + # always work. + self.metadata.read() + self._test_thumbnail_tags(False) + thumb = self.metadata.exif_thumbnail + thumb.data = 'invalid' + self.assertEqual(thumb.mime_type, 'image/jpeg') + self._test_thumbnail_tags(True) + + def test_set_exif_thumbnail_from_inexistent_file(self): + self.metadata.read() + self._test_thumbnail_tags(False) + thumb = self.metadata.exif_thumbnail + fd, pathname = tempfile.mkstemp() + os.close(fd) + os.remove(pathname) + self.failUnlessRaises(IOError, thumb.set_from_file, pathname) + self._test_thumbnail_tags(False) + |