pax_global_header00006660000000000000000000000064136415245650014525gustar00rootroot0000000000000052 comment=23a6f850d8281163b76598176e88a954d9b6d6f1 py3exiv2-0.7.2/000077500000000000000000000000001364152456500132245ustar00rootroot00000000000000py3exiv2-0.7.2/MANIFEST.in000066400000000000000000000005351364152456500147650ustar00rootroot00000000000000include README include setup.py include src/exiv2wrapper.hpp include src/exiv2wrapper.cpp include src/exiv2wrapper_python.cpp include src/pyexiv2/__init__.py include src/pyexiv2/exif.py include src/pyexiv2/iptc.py include src/pyexiv2/metadata.py include src/pyexiv2/preview.py include src/pyexiv2/utils.py include src/pyexiv2/xmp.py exclude test/* py3exiv2-0.7.2/PKG-INFO000066400000000000000000000040041364152456500143170ustar00rootroot00000000000000Metadata-Version: 1.1 Name: py3exiv2 Version: 0.7.2 Summary: A Python3 binding to the library exiv2 Home-page: https://launchpad.net/py3exiv2 Author: Vincent Vande Vyvre Author-email: vincent.vandevyvre@oqapy.eu License: GPL-3 Description: python3-exiv2 ============= A Python 3 binding to the library exiv2. python3-exiv2 is a Python 3 binding to exiv2, the C++ library for manipulation of EXIF, IPTC and XMP image metadata. It is a python 3 module that allows your scripts to read and write metadata (EXIF, IPTC, XMP, thumbnails) embedded in image files (JPEG, TIFF, ...). It is designed as a high-level interface to the functionalities offered by libexiv2. Using python’s built-in data types and standard modules, it provides easy manipulation of image metadata. python3-exiv2 is distributed under the GPL version 3 license. The main content of the code was initially written by Olivier Tilloy for Python 2 under the name pyexiv2. py3exiv2 depends on the following libraries: * python (≥ 3.2) * boost.python3 (http://www.boost.org/libs/python/doc/index.html) * exiv2 (http://www.exiv2.org/) Build depends: * python-all-dev (≥ 3.2) * libexiv2-dev (≥ 0.20) * libboost-python-dev (≥ 1.48) * g++ Keywords: exiv2 pyexiv2 EXIF IPTC XMP image metadata Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Programming Language :: C++ Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 py3exiv2-0.7.2/README000066400000000000000000000017251364152456500141110ustar00rootroot00000000000000python3-exiv2 ============= A Python 3 binding to the library exiv2. python3-exiv2 is a Python 3 binding to exiv2, the C++ library for manipulation of EXIF, IPTC and XMP image metadata. It is a python 3 module that allows your scripts to read and write metadata (EXIF, IPTC, XMP, thumbnails) embedded in image files (JPEG, TIFF, ...). It is designed as a high-level interface to the functionalities offered by libexiv2. Using python’s built-in data types and standard modules, it provides easy manipulation of image metadata. python3-exiv2 is distributed under the GPL version 3 license. The main content of the code was initially written by Olivier Tilloy for Python 2 under the name pyexiv2. py3exiv2 depends on the following libraries: * python (≥ 3.2) * boost.python3 (http://www.boost.org/libs/python/doc/index.html) * exiv2 (http://www.exiv2.org/) Build depends: * python-all-dev (≥ 3.2) * libexiv2-dev (≥ 0.20) * libboost-python-dev (≥ 1.48) * g++ py3exiv2-0.7.2/setup.cfg000066400000000000000000000000461364152456500150450ustar00rootroot00000000000000[egg_info] tag_build = tag_date = 0 py3exiv2-0.7.2/setup.py000066400000000000000000000046031364152456500147410ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # Replacement setup.py for py3exiv2, that allows building on OSX # https://gist.github.com/ndevenish/6410cab393bd8dec1b016061ddb5573b import sys import os import glob import platform from setuptools import setup, find_packages, Extension from codecs import open from os import path here = path.abspath(path.dirname(__file__)) # Get the long description from the relevant file with open(path.join(here, 'README'), encoding='utf-8') as f: long_description = f.read() def get_libboost_osx(): places = ["/usr/local/lib/"] for place in places: lib = place + "libboost_python3*.dylib" files = glob.glob(lib) for f in files: if not "-mt" in f: return os.path.basename(f).replace("lib", "").split(".")[0] print("NOT FOUND", files) sys.exit() if platform.system() == "Darwin": boostlib = get_libboost_osx() print(boostlib) else: python_version = str(sys.version_info.major) + str(sys.version_info.minor) boostlib = 'boost_python' + python_version setup( name='py3exiv2', version='0.7.2', description='A Python3 binding to the library exiv2', long_description=long_description, url='https://launchpad.net/py3exiv2', author='Vincent Vande Vyvre', author_email='vincent.vandevyvre@oqapy.eu', license='GPL-3', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: C++', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9' ], keywords='exiv2 pyexiv2 EXIF IPTC XMP image metadata', packages=find_packages('src'), package_dir={'': 'src'}, package_data={'':['src/*.cpp', 'src/*.hpp', 'src/pyexiv2/*.py']}, ext_modules=[ Extension('libexiv2python', ['src/exiv2wrapper.cpp', 'src/exiv2wrapper_python.cpp'], libraries=[boostlib, 'exiv2'], extra_compile_args=['-g'] ) ], ) py3exiv2-0.7.2/src/000077500000000000000000000000001364152456500140135ustar00rootroot00000000000000py3exiv2-0.7.2/src/exiv2wrapper.cpp000066400000000000000000001612221364152456500171610ustar00rootroot00000000000000// ***************************************************************************** /* * Copyright (C) 2006-2012 Olivier Tilloy * Copyright (C) 2015-2020 Vincent Vande Vyvre * * 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. */ /* Maintainer: Vincent Vande Vyvre */ // ***************************************************************************** #include "exiv2wrapper.hpp" #include "boost/python/stl_iterator.hpp" #include // Custom error codes for Exiv2 exceptions #define METADATA_NOT_READ 101 #define NON_REPEATABLE 102 #define KEY_NOT_FOUND 103 #define INVALID_VALUE 104 #define EXISTING_PREFIX 105 #define BUILTIN_NS 106 #define NOT_REGISTERED 107 #if EXIV2_MAJOR_VERSION >= 1 || (EXIV2_MAJOR_VERSION == 0 && EXIV2_MINOR_VERSION >= 27) #define HAVE_EXIV2_ERROR_CODE #endif // Custom macros #ifdef HAVE_EXIV2_ERROR_CODE #define CHECK_METADATA_READ \ if (!_dataRead) throw Exiv2::Error(Exiv2::kerErrorMessage, "metadata not read"); #else #define CHECK_METADATA_READ \ if (!_dataRead) throw Exiv2::Error(METADATA_NOT_READ); #endif 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. #ifdef HAVE_EXIV2_ERROR_CODE Exiv2::Error error = Exiv2::Error(Exiv2::kerSuccess); #else Exiv2::Error error(0); #endif // Release the GIL to allow other python threads to run // while opening the file. Py_BEGIN_ALLOW_THREADS try { if (_data != 0) { _image = Exiv2::ImageFactory::open(_data, _size); } else { _image = Exiv2::ImageFactory::open(_filename); } } catch (Exiv2::Error& err) { //std::cout << " Caught Exiv2 exception '" << err.code() << "'\n"; error = err; } // Re-acquire the GIL Py_END_ALLOW_THREADS if (error.code() == 0) { assert(_image.get() != 0); _dataRead = false; } else { throw error; } } // Base constructor Image::Image(const std::string& filename) { _filename = filename; _data = 0; _instantiate_image(); } // From buffer constructor Image::Image(const std::string& buffer, unsigned long size) { // Deep copy of the data buffer _data = new Exiv2::byte[size]; for (unsigned long i = 0; i < size; ++i) { _data[i] = buffer[i]; } _size = size; _instantiate_image(); } // Copy constructor Image::Image(const Image& image) { _filename = image._filename; _instantiate_image(); } Image::~Image() { if (_data != 0) { delete[] _data; } if (_exifThumbnail != 0) { delete _exifThumbnail; } } void Image::readMetadata() { // If an exception is thrown, it has to be done outside of the // Py_{BEGIN,END}_ALLOW_THREADS block. #ifdef HAVE_EXIV2_ERROR_CODE Exiv2::Error error = Exiv2::Error(Exiv2::kerSuccess); #else Exiv2::Error error(0); #endif // Release the GIL to allow other python threads to run // while reading metadata. Py_BEGIN_ALLOW_THREADS try { _image->readMetadata(); _exifData = &_image->exifData(); _iptcData = &_image->iptcData(); _xmpData = &_image->xmpData(); _dataRead = true; } catch (Exiv2::Error& err) { //std::cout << " Caught Exiv2 exception '" << err.code() << "'\n"; error = err; } // Re-acquire the GIL Py_END_ALLOW_THREADS if (error.code() != 0) { throw error; } } void Image::writeMetadata() { CHECK_METADATA_READ // If an exception is thrown, it has to be done outside of the // Py_{BEGIN,END}_ALLOW_THREADS block. #ifdef HAVE_EXIV2_ERROR_CODE Exiv2::Error error = Exiv2::Error(Exiv2::kerSuccess); #else Exiv2::Error error(0); #endif // Release the GIL to allow other python threads to run // while writing metadata. Py_BEGIN_ALLOW_THREADS try { _image->writeMetadata(); } catch (Exiv2::Error& err) { //std::cout << "Caught Exiv2 exception '" << err.code() << "'\n"; error = err; } // Re-acquire the GIL Py_END_ALLOW_THREADS if (error.code() != 0) { throw error; } } unsigned int Image::pixelWidth() const { CHECK_METADATA_READ return _image->pixelWidth(); } unsigned int Image::pixelHeight() const { CHECK_METADATA_READ return _image->pixelHeight(); } std::string Image::mimeType() const { CHECK_METADATA_READ return _image->mimeType(); } boost::python::list Image::exifKeys() { CHECK_METADATA_READ boost::python::list keys; for(Exiv2::ExifMetadata::iterator i = _exifData->begin(); i != _exifData->end(); ++i) { keys.append(i->key()); } return keys; } const ExifTag Image::getExifTag(std::string key) { CHECK_METADATA_READ Exiv2::ExifKey exifKey = Exiv2::ExifKey(key); if(_exifData->findKey(exifKey) == _exifData->end()) #ifdef HAVE_EXIV2_ERROR_CODE { throw Exiv2::Error(Exiv2::kerInvalidKey, key); } #else { throw Exiv2::Error(KEY_NOT_FOUND, key); } #endif return ExifTag(key, &(*_exifData)[key], _exifData, _image->byteOrder()); } void Image::deleteExifTag(std::string key) { CHECK_METADATA_READ Exiv2::ExifKey exifKey = Exiv2::ExifKey(key); Exiv2::ExifMetadata::iterator datum = _exifData->findKey(exifKey); if(datum == _exifData->end()) #ifdef HAVE_EXIV2_ERROR_CODE { throw Exiv2::Error(Exiv2::kerInvalidKey, key); } #else { throw Exiv2::Error(KEY_NOT_FOUND, key); } #endif _exifData->erase(datum); } boost::python::list Image::iptcKeys() { CHECK_METADATA_READ boost::python::list keys; for(Exiv2::IptcMetadata::iterator i = _iptcData->begin(); i != _iptcData->end(); ++i) { // The key is appended to the list if and only if it is not already // present. if (keys.count(i->key()) == 0) { keys.append(i->key()); } } return keys; } const IptcTag Image::getIptcTag(std::string key) { CHECK_METADATA_READ Exiv2::IptcKey iptcKey = Exiv2::IptcKey(key); if(_iptcData->findKey(iptcKey) == _iptcData->end()) #ifdef HAVE_EXIV2_ERROR_CODE { throw Exiv2::Error(Exiv2::kerInvalidKey, key); } #else { throw Exiv2::Error(KEY_NOT_FOUND, key); } #endif return IptcTag(key, _iptcData); } void Image::deleteIptcTag(std::string key) { CHECK_METADATA_READ Exiv2::IptcKey iptcKey = Exiv2::IptcKey(key); Exiv2::IptcMetadata::iterator dataIterator = _iptcData->findKey(iptcKey); if (dataIterator == _iptcData->end()) #ifdef HAVE_EXIV2_ERROR_CODE { throw Exiv2::Error(Exiv2::kerInvalidKey, key); } #else { throw Exiv2::Error(KEY_NOT_FOUND, key); } #endif while (dataIterator != _iptcData->end()) { if (dataIterator->key() == key) { dataIterator = _iptcData->erase(dataIterator); } else { ++dataIterator; } } } boost::python::list Image::xmpKeys() { CHECK_METADATA_READ boost::python::list keys; for(Exiv2::XmpMetadata::iterator i = _xmpData->begin(); i != _xmpData->end(); ++i) { keys.append(i->key()); } return keys; } const XmpTag Image::getXmpTag(std::string key) { CHECK_METADATA_READ Exiv2::XmpKey xmpKey = Exiv2::XmpKey(key); if(_xmpData->findKey(xmpKey) == _xmpData->end()) #ifdef HAVE_EXIV2_ERROR_CODE { throw Exiv2::Error(Exiv2::kerInvalidKey, key); } #else { throw Exiv2::Error(KEY_NOT_FOUND, key); } #endif return XmpTag(key, &(*_xmpData)[key]); } void Image::deleteXmpTag(std::string key) { CHECK_METADATA_READ Exiv2::XmpKey xmpKey = Exiv2::XmpKey(key); Exiv2::XmpMetadata::iterator i = _xmpData->findKey(xmpKey); if(i != _xmpData->end()) { _xmpData->erase(i); } else #ifdef HAVE_EXIV2_ERROR_CODE { throw Exiv2::Error(Exiv2::kerInvalidKey, key); } #else { throw Exiv2::Error(KEY_NOT_FOUND, key); } #endif } const std::string Image::getComment() const { CHECK_METADATA_READ return _image->comment(); } void Image::setComment(const std::string& comment) { CHECK_METADATA_READ _image->setComment(comment); } void Image::clearComment() { CHECK_METADATA_READ _image->clearComment(); } boost::python::list Image::previews() { CHECK_METADATA_READ boost::python::list previews; Exiv2::PreviewManager pm(*_image); Exiv2::PreviewPropertiesList props = pm.getPreviewProperties(); for (Exiv2::PreviewPropertiesList::const_iterator i = props.begin(); i != props.end(); ++i) { previews.append(Preview(pm.getPreviewImage(*i))); } return previews; } void Image::copyMetadata(Image& other, bool exif, bool iptc, bool xmp) const { CHECK_METADATA_READ if (!other._dataRead) { #ifdef HAVE_EXIV2_ERROR_CODE { throw Exiv2::Error(Exiv2::kerErrorMessage, "metadata not read"); } #else { throw Exiv2::Error(METADATA_NOT_READ); } #endif } if (exif) other._image->setExifData(*_exifData); if (iptc) other._image->setIptcData(*_iptcData); if (xmp) other._image->setXmpData(*_xmpData); } boost::python::object Image::getDataBuffer() const { std::string buffer; // Release the GIL to allow other python threads to run // while reading the image data. Py_BEGIN_ALLOW_THREADS Exiv2::BasicIo& io = _image->io(); unsigned long size = io.size(); long pos = -1; if (io.isopen()) { // Remember the current position in the stream pos = io.tell(); // Go to the beginning of the stream io.seek(0, Exiv2::BasicIo::beg); } else { io.open(); } // Copy the data buffer in a string. Since the data buffer can contain null // characters ('\x00'), the string cannot be simply constructed like that: // buffer = std::string((char*) previewImage.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... buffer.resize(size, ' '); // ... then fill it with the raw data. for (unsigned long i = 0; i < size; ++i) { io.read((Exiv2::byte*) &buffer[i], 1); } if (pos == -1) { // The stream was initially closed io.close(); } else { // Reset to the initial position in the stream io.seek(pos, Exiv2::BasicIo::beg); } // Re-acquire the GIL Py_END_ALLOW_THREADS return boost::python::object(boost::python::handle<>( PyBytes_FromStringAndSize(buffer.c_str(), buffer.size()) )); } Exiv2::ByteOrder Image::getByteOrder() const { CHECK_METADATA_READ return _image->byteOrder(); } 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); } boost::python::list Image::getExifThumbnailData() { Exiv2::DataBuf buffer = _getExifThumbnail()->copy(); // Copy the data buffer in a list. boost::python::list data; for(unsigned int i = 0; i < buffer.size_; ++i) { unsigned int datum = buffer.pData_[i]; data.append(datum); } 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()); } const std::string Image::getIptcCharset() const { CHECK_METADATA_READ const char* charset = _iptcData->detectCharset(); if (charset != 0) { return std::string(charset); } else { return std::string(); } } ExifTag::ExifTag(const std::string& key, Exiv2::Exifdatum* datum, Exiv2::ExifData* data, Exiv2::ByteOrder byteOrder): _key(key), _byteOrder(byteOrder) { if (datum != 0 && data != 0) { _datum = datum; _data = data; } else { _datum = new Exiv2::Exifdatum(_key); _data = 0; } // Conditional code, exiv2 0.21 changed APIs we need // (see https://bugs.launchpad.net/pyexiv2/+bug/684177). #if EXIV2_MAJOR_VERSION >= 1 || (EXIV2_MAJOR_VERSION == 0 && EXIV2_MINOR_VERSION >= 21) Exiv2::ExifKey exifKey(key); _type = Exiv2::TypeInfo::typeName(exifKey.defaultTypeId()); // Where available, extract the type from the metadata, it is more reliable // than static type information. The exception is for user comments, for // which we’d rather keep the 'Comment' type instead of 'Undefined'. if ((_data != 0) && (_type != "Comment")) { const char* typeName = _datum->typeName(); if (typeName != 0) { _type = typeName; } } _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)); // Where available, extract the type from the metadata, it is more reliable // than static type information. The exception is for user comments, for // which we’d rather keep the 'Comment' type instead of 'Undefined'. if ((_data != 0) && (_type != "Comment")) { const char* typeName = _datum->typeName(); if (typeName != 0) { _type = typeName; } } _name = Exiv2::ExifTags::tagName(tag, ifd); _label = Exiv2::ExifTags::tagLabel(tag, ifd); _description = Exiv2::ExifTags::tagDesc(tag, ifd); _sectionName = Exiv2::ExifTags::sectionName(tag, ifd); _sectionDescription = Exiv2::ExifTags::sectionDesc(tag, ifd); #endif } ExifTag::~ExifTag() { if (_data == 0) { delete _datum; } } void ExifTag::setRawValue(const std::string& value) { int result = _datum->setValue(value); if (result != 0) #ifdef HAVE_EXIV2_ERROR_CODE { std::string message("Invalid value: "); message += value; throw Exiv2::Error(Exiv2::kerInvalidDataset, message); } #else { throw Exiv2::Error(INVALID_VALUE); } #endif } void ExifTag::setParentImage(Image& image) { Exiv2::ExifData* data = image.getExifData(); if (data == _data) { // The parent image is already the one passed as a parameter. // This happens when replacing a tag by itself. In this case, don’t do // anything (see https://bugs.launchpad.net/pyexiv2/+bug/622739). return; } _data = data; Exiv2::Value::AutoPtr value = _datum->getValue(); delete _datum; _datum = &(*_data)[_key.key()]; _datum->setValue(value.get()); _byteOrder = image.getByteOrder(); } const std::string ExifTag::getKey() { return _key.key(); } const std::string ExifTag::getType() { return _type; } const std::string ExifTag::getName() { return _name; } const std::string ExifTag::getLabel() { return _label; } const std::string ExifTag::getDescription() { return _description; } const std::string ExifTag::getSectionName() { return _sectionName; } const std::string ExifTag::getSectionDescription() { return _sectionDescription; } const std::string ExifTag::getRawValue() { return _datum->toString(); } 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) { _from_data = (data != 0); if (_from_data) { _data = data; } else { _data = new Exiv2::IptcData(); _data->add(Exiv2::Iptcdatum(_key)); } Exiv2::IptcMetadata::iterator iterator = _data->findKey(_key); const uint16_t tag = iterator->tag(); const uint16_t record = iterator->record(); _type = Exiv2::TypeInfo::typeName(Exiv2::IptcDataSets::dataSetType(tag, record)); _name = Exiv2::IptcDataSets::dataSetName(tag, record); _title = Exiv2::IptcDataSets::dataSetTitle(tag, record); _description = Exiv2::IptcDataSets::dataSetDesc(tag, record); // What is the photoshop name anyway? Where is it used? _photoshopName = Exiv2::IptcDataSets::dataSetPsName(tag, record); _repeatable = Exiv2::IptcDataSets::dataSetRepeatable(tag, record); _recordName = Exiv2::IptcDataSets::recordName(record); _recordDescription = Exiv2::IptcDataSets::recordDesc(record); if (_from_data) { // Check that we are not trying to assign multiple values to a tag that // is not repeatable. unsigned int nb_values = 0; for(Exiv2::IptcMetadata::iterator iterator = _data->begin(); iterator != _data->end(); ++iterator) { if (iterator->key() == key) { ++nb_values; if (!_repeatable && (nb_values > 1)) #ifdef HAVE_EXIV2_ERROR_CODE { std::string mssg("Tag not repeatable: "); mssg += key; throw Exiv2::Error(Exiv2::kerErrorMessage, mssg); } #else { throw Exiv2::Error(NON_REPEATABLE); } #endif } } } } IptcTag::~IptcTag() { if (!_from_data) { delete _data; } } void IptcTag::setRawValues(const boost::python::list& values) { if (!_repeatable && (boost::python::len(values) > 1)) { // The tag is not repeatable but we are trying to assign it more than // one value. #ifdef HAVE_EXIV2_ERROR_CODE { throw Exiv2::Error(Exiv2::kerInvalidDataset, "Tag not repeatable"); } #else { throw Exiv2::Error(NON_REPEATABLE); } #endif } unsigned int index = 0; unsigned int max = boost::python::len(values); Exiv2::IptcMetadata::iterator iterator = _data->findKey(_key); while (index < max) { std::string value = boost::python::extract(values[index++]); if (iterator != _data->end()) { // Override an existing value int result = iterator->setValue(value); if (result != 0) #ifdef HAVE_EXIV2_ERROR_CODE { std::string mssg("Invalid value: "); mssg += value; // there's no invalid value error in libexiv2, so we use // kerInvalidDataset wich raise a Python ValueError throw Exiv2::Error(Exiv2::kerInvalidDataset, mssg); } #else { throw Exiv2::Error(INVALID_VALUE); } #endif // Jump to the next datum matching the key ++iterator; while ((iterator != _data->end()) && (iterator->key() != _key.key())) { ++iterator; } } else { // Append a new value Exiv2::Iptcdatum datum(_key); int result = datum.setValue(value); if (result != 0) #ifdef HAVE_EXIV2_ERROR_CODE { std::string mssg("Invalid value: "); mssg += value; throw Exiv2::Error(Exiv2::kerErrorMessage, mssg); } #else { throw Exiv2::Error(INVALID_VALUE); } #endif int state = _data->add(datum); if (state == 6) #ifdef HAVE_EXIV2_ERROR_CODE { std::string mssg("Tag not repeatable: "); mssg += _key.key(); throw Exiv2::Error(Exiv2::kerErrorMessage, mssg); } #else { throw Exiv2::Error(NON_REPEATABLE); } #endif // Reset iterator that has been invalidated by appending a datum iterator = _data->end(); } } // Erase the remaining values if any while (iterator != _data->end()) { if (iterator->key() == _key.key()) { iterator = _data->erase(iterator); } else { ++iterator; } } } void IptcTag::setParentImage(Image& image) { Exiv2::IptcData* data = image.getIptcData(); if (data == _data) { // The parent image is already the one passed as a parameter. // This happens when replacing a tag by itself. In this case, don’t do // anything (see https://bugs.launchpad.net/pyexiv2/+bug/622739). return; } const boost::python::list values = getRawValues(); delete _data; _from_data = true; _data = data; setRawValues(values); } const std::string IptcTag::getKey() { return _key.key(); } const std::string IptcTag::getType() { return _type; } const std::string IptcTag::getName() { return _name; } const std::string IptcTag::getTitle() { return _title; } const std::string IptcTag::getDescription() { return _description; } const std::string IptcTag::getPhotoshopName() { return _photoshopName; } const bool IptcTag::isRepeatable() { return _repeatable; } const std::string IptcTag::getRecordName() { return _recordName; } const std::string IptcTag::getRecordDescription() { return _recordDescription; } const boost::python::list IptcTag::getRawValues() { boost::python::list values; for(Exiv2::IptcMetadata::iterator iterator = _data->begin(); iterator != _data->end(); ++iterator) { if (iterator->key() == _key.key()) { values.append(iterator->toString()); } } return values; } XmpTag::XmpTag(const std::string& key, Exiv2::Xmpdatum* datum): _key(key) { _from_datum = (datum != 0); if (_from_datum) { _datum = datum; _exiv2_type = datum->typeName(); } else { _datum = new Exiv2::Xmpdatum(_key); _exiv2_type = Exiv2::TypeInfo::typeName(Exiv2::XmpProperties::propertyType(_key)); } const char* title = Exiv2::XmpProperties::propertyTitle(_key); if (title != 0) { _title = title; } const char* description = Exiv2::XmpProperties::propertyDesc(_key); if (description != 0) { _description = description; } const Exiv2::XmpPropertyInfo* info = Exiv2::XmpProperties::propertyInfo(_key); if (info != 0) { _name = info->name_; _type = info->xmpValueType_; } } XmpTag::~XmpTag() { if (!_from_datum) { delete _datum; } } void XmpTag::setTextValue(const std::string& value) { _datum->setValue(value); } void XmpTag::setArrayValue(const boost::python::list& values) { // Reset the value _datum->setValue(0); for(boost::python::stl_input_iterator iterator(values); iterator != boost::python::stl_input_iterator(); ++iterator) { _datum->setValue(*iterator); } } void XmpTag::setLangAltValue(const boost::python::dict& values) { // Reset the value _datum->setValue(0); for(boost::python::stl_input_iterator iterator(values); iterator != boost::python::stl_input_iterator(); ++iterator) { std::string key = *iterator; std::string value = boost::python::extract(values.get(key)); _datum->setValue("lang=\"" + key + "\" " + value); } } void XmpTag::setParentImage(Image& image) { Exiv2::Xmpdatum* datum = &(*image.getXmpData())[_key.key()]; if (datum == _datum) { // The parent image is already the one passed as a parameter. // This happens when replacing a tag by itself. In this case, don’t do // anything (see https://bugs.launchpad.net/pyexiv2/+bug/622739). return; } Exiv2::Value::AutoPtr value = _datum->getValue(); delete _datum; _from_datum = true; _datum = &(*image.getXmpData())[_key.key()]; _datum->setValue(value.get()); } const std::string XmpTag::getKey() { return _key.key(); } const std::string XmpTag::getExiv2Type() { return _exiv2_type; } const std::string XmpTag::getType() { return _type; } const std::string XmpTag::getName() { return _name; } const std::string XmpTag::getTitle() { return _title; } const std::string XmpTag::getDescription() { return _description; } const std::string XmpTag::getTextValue() { return dynamic_cast(&_datum->value())->value_; } const boost::python::list XmpTag::getArrayValue() { #ifdef HAVE_EXIV2_ERROR_CODE // We can't use &_datum->value())->value_ because value_ is private in // this context (change in libexiv2 0.27) const Exiv2::XmpArrayValue* xav = dynamic_cast(&_datum->value()); boost::python::list rvalue; for(int i = 0; i < xav->count(); ++i) { std::string value = xav->toString(i); rvalue.append(value); } return rvalue; #else std::vector value = dynamic_cast(&_datum->value())->value_; boost::python::list rvalue; for(std::vector::const_iterator i = value.begin(); i != value.end(); ++i) { rvalue.append(*i); } return rvalue; #endif } const boost::python::dict XmpTag::getLangAltValue() { Exiv2::LangAltValue::ValueType value = dynamic_cast(&_datum->value())->value_; boost::python::dict rvalue; for (Exiv2::LangAltValue::ValueType::const_iterator i = value.begin(); i != value.end(); ++i) { rvalue[i->first] = i->second; } return rvalue; } Preview::Preview(const Exiv2::PreviewImage& previewImage) { _mimeType = previewImage.mimeType(); _extension = previewImage.extension(); _size = previewImage.size(); _dimensions = boost::python::make_tuple(previewImage.width(), previewImage.height()); // 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*) previewImage.pData()); // because it would be truncated after the first occurence of a null // character. Therefore, it has to be copied character by character. const Exiv2::byte* pData = previewImage.pData(); // First allocate the memory for the whole string... _data = std::string(_size, ' '); // ... then fill it with the raw data. for(unsigned int i = 0; i < _size; ++i) { _data[i] = pData[i]; } } boost::python::object Preview::getData() const { return boost::python::object(boost::python::handle<>( PyBytes_FromStringAndSize(_data.c_str(), _size) )); } void Preview::writeToFile(const std::string& path) const { std::string filename = path + _extension; std::ofstream fd(filename.c_str(), std::ios::out | std::ios::binary); fd << _data; fd.close(); } #ifdef HAVE_EXIV2_ERROR_CODE void translateExiv2Error(Exiv2::Error const& error) { // Use the Python 'C' API to set up an exception object const char* message = error.what(); // The type of the Python exception depends on the error code // Warning: this piece of code should be updated in case the error codes // defined by Exiv2 (file 'src/error.cpp') are changed switch (error.code()) { case 1: // kerErrorMessage Unidentified error PyErr_SetString(PyExc_RuntimeError, message); break; case 2: // kerCallFailed {path}: Call to `{function}' failed: {strerror} // May be raised when reading a file PyErr_SetString(PyExc_RuntimeError, message); break; case 3: // kerNotAnImage This does not look like a {image type} image // May be raised by readMetadata() PyErr_SetString(PyExc_IOError, message); break; case 4: // kerInvalidDataset Invalid dataset name `{dataset name}' PyErr_SetString(PyExc_ValueError, message); break; case 5: // kerInvalidRecord Invalid record name `{record name}' // May be raised when instantiating an IptcKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 6: // kerInvalidKey Invalid key `{key}' // May be raised when instantiating an ExifKey, an IptcKey or an // XmpKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 7: // kerInvalidTag // Invalid tag name or ifdId `{tag name}', ifdId {ifdId} // May be raised when instantiating an ExifKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 8: // kerValueNotSet Value not set // May be raised when calling value() on a datum PyErr_SetString(PyExc_ValueError, message); break; case 9: // kerDataSourceOpenFailed // {path}: Failed to open the data source: {strerror} // May be raised by readMetadata() PyErr_SetString(PyExc_IOError, message); break; case 10: // kerFileOpenFailed // {path}: Failed to open file ({mode}): {strerror} // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 11: // kerFileOpenFailed // {path}: The file contains data of an unknown image type // May be raised when opening an image PyErr_SetString(PyExc_TypeError, message); break; case 12: // kerMemoryContainsUnknownImageType //The memory contains data of an unknown image type // May be raised when instantiating an image from a data buffer PyErr_SetString(PyExc_IOError, message); break; case 13: // kerUnsupportedImageType Image type {image type} is not supported // May be raised when creating a new image PyErr_SetString(PyExc_IOError, message); break; case 14: // kerFailedToReadImageData Failed to read image data // May be raised by readMetadata() PyErr_SetString(PyExc_IOError, message); break; case 15: // kerNotAJpeg This does not look like a JPEG image // May be raised by readMetadata() PyErr_SetString(PyExc_IOError, message); break; case 17: // kerFileRenameFailed // {old path}: Failed to rename file to {new path}: {strerror} // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 18: // kerTransferFailed {path}: Transfer failed: {strerror} // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 19: // kerMemoryTransferFailed Memory transfer failed: {strerror} // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 20: // kerInputDataReadFailed Failed to read input data // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 21: // kerImageWriteFailed Failed to write image // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 22: // kerNoImageInInputData Input data does not contain a valid image // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 23: // kerInvalidIfdId Invalid ifdId {ifdId} // May be raised when instantiating an ExifKey from a tag and // IFD item string PyErr_SetString(PyExc_KeyError, message); break; case 24: // kerValueTooLarge // Entry::setValue: Value too large {tag}, {size}, {requested} PyErr_SetString(PyExc_ValueError, message); break; case 25: // kerDataAreaValueTooLarge // Entry::setDataArea: Value too large {tag}, {size}, {requested} PyErr_SetString(PyExc_ValueError, message); break; case 26: // kerOffsetOutOfRange Offset out of range // May be raised by writeMetadata() (TIFF) PyErr_SetString(PyExc_IOError, message); break; case 27: // kerUnsupportedDataAreaOffsetType // Unsupported data area offset type // May be raised by writeMetadata() (TIFF) PyErr_SetString(PyExc_IOError, message); break; case 28: // kerInvalidCharset Invalid charset: `{charset name}' // May be raised when instantiating a CommentValue from a string PyErr_SetString(PyExc_ValueError, message); break; case 29: // kerUnsupportedDateFormat Unsupported date format // May be raised when instantiating a DateValue from a string PyErr_SetString(PyExc_ValueError, message); break; case 30: // kerUnsupportedTimeFormat Unsupported time format // May be raised when instantiating a TimeValue from a string PyErr_SetString(PyExc_ValueError, message); break; case 31: // kerWritingImageFormatUnsupported // Writing to {image format} images is not supported // May be raised by writeMetadata() for certain image types PyErr_SetString(PyExc_IOError, message); break; case 32: // kerInvalidSettingForImage // Setting {metadata type} in {image format} images is not supported // May be raised when setting certain types of metadata for certain // image types that don't support them PyErr_SetString(PyExc_ValueError, message); break; case 33: // kerNotACrwImage This does not look like a CRW image // May be raised by readMetadata() (CRW) PyErr_SetString(PyExc_IOError, message); break; case 34: // kerFunctionNotSupported {function}: Not supported PyErr_SetString(PyExc_IOError, message); break; case 35: // kerNoNamespaceInfoForXmpPrefix // No namespace info available for XMP prefix `{prefix}' // May be raised when retrieving property info for an XmpKey PyErr_SetString(PyExc_KeyError, message); break; case 36: // kerNoPrefixForNamespace // No prefix registered for namespace `{namespace}', needed for // property path `{property path}' // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_KeyError, message); break; case 37: // kerTooLargeJpegSegment // Size of {type of metadata} JPEG segment is larger than // 65535 bytes // May be raised by writeMetadata() (JPEG) PyErr_SetString(PyExc_ValueError, message); break; case 38: // kerUnhandledXmpdatum // Unhandled Xmpdatum {key} of type {value type} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_TypeError, message); break; case 39: // kerUnhandledXmpNode // Unhandled XMP node {key} with opt={XMP Toolkit option flags} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_TypeError, message); break; case 40: // kerXMPToolkitError // XMP Toolkit error {error id}: {error message} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_RuntimeError, message); break; case 41: // kerDecodeLangAltPropertyFailed // Failed to decode Lang Alt property {property path} // with opt={XMP Toolkit option flags} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 42: // kerDecodeLangAltQualifierFailed // Failed to decode Lang Alt qualifier {qualifier path} // with opt={XMP Toolkit option flags} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 43: // kerEncodeLangAltPropertyFailed // Failed to encode Lang Alt property {key} // May be raised by writeMetadata() PyErr_SetString(PyExc_ValueError, message); break; case 44: // kerPropertyNameIdentificationFailed // Failed to determine property name from path {property path}, // namespace {namespace} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_KeyError, message); break; case 45: // kerSchemaNamespaceNotRegistered // Schema namespace {namespace} is not registered with // the XMP Toolkit // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 46: // kerNoNamespaceForPrefix // No namespace registered for prefix `{prefix}' // May be raised when instantiating an XmpKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 47: // kerAliasesNotSupported // Aliases are not supported. Please send this XMP packet // to ahuggel@gmx.net `{namespace}', `{property path}', `{value}' // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 48: // kerInvalidXmpText // Invalid XmpText type `{type}' // May be raised when instantiating an XmpTextValue from a string PyErr_SetString(PyExc_TypeError, message); break; case 49: // kerTooManyTiffDirectoryEntries // TIFF directory {TIFF directory name} has too many entries // May be raised by writeMetadata() (TIFF) PyErr_SetString(PyExc_IOError, message); break; // Added in py3exiv2 case 50: // kerMultipleTiffArrayElementTagsInDirectory // Multiple TIFF array element tags {number} in one directory") // May be raised by readMetadata() (TIFF) PyErr_SetString(PyExc_IOError, message); break; case 51: // kerWrongTiffArrayElementTagType // TIFF array element tag {number} has wrong type") }, // %1=tag number // May be raised by readMetadata() (TIFF) PyErr_SetString(PyExc_TypeError, message); break; // Added in libexiv2 0.27 case 52: // kerInvalidKeyXmpValue {key} has invalid XMP value type {type} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 53: // kerInvalidIccProfile Not a valid ICC Profile PyErr_SetString(PyExc_ValueError, message); break; case 54: // kerInvalidXMP Not valid XMP PyErr_SetString(PyExc_TypeError, message); break; case 55: // kerTiffDirectoryTooLarge tiff directory length is too large PyErr_SetString(PyExc_ValueError, message); break; case 56: // kerInvalidTypeValue // Invalid type value detected in Image::printIFDStructure PyErr_SetString(PyExc_TypeError, message); break; case 57: // kerInvalidMalloc // Invalid memory allocation request PyErr_SetString(PyExc_MemoryError, message); break; case 58: // kerCorruptedMetadata Corrupted image metadata PyErr_SetString(PyExc_IOError, message); break; case 59: // kerArithmeticOverflow Arithmetic operation overflow PyErr_SetString(PyExc_OverflowError, message); break; case 60: // kerMallocFailed Memory allocation failed PyErr_SetString(PyExc_MemoryError, message); break; // Default handler default: PyErr_SetString(PyExc_RuntimeError, message); } } #else void translateExiv2Error(Exiv2::Error const& error) { // Use the Python 'C' API to set up an exception object const char* message = error.what(); // The type of the Python exception depends on the error code // Warning: this piece of code should be updated in case the error codes // defined by Exiv2 (file 'src/error.cpp') are changed switch (error.code()) { // Exiv2 error codes case 2: // {path}: Call to `{function}' failed: {strerror} // May be raised when reading a file PyErr_SetString(PyExc_RuntimeError, message); break; case 3: // This does not look like a {image type} image // May be raised by readMetadata() PyErr_SetString(PyExc_IOError, message); break; case 4: // Invalid dataset name `{dataset name}' // May be raised when instantiating an IptcKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 5: // Invalid record name `{record name}' // May be raised when instantiating an IptcKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 6: // Invalid key `{key}' // May be raised when instantiating an ExifKey, an IptcKey or an // XmpKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 7: // Invalid tag name or ifdId `{tag name}', ifdId {ifdId} // May be raised when instantiating an ExifKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 8: // Value not set // May be raised when calling value() on a datum PyErr_SetString(PyExc_ValueError, message); break; case 9: // {path}: Failed to open the data source: {strerror} // May be raised by readMetadata() PyErr_SetString(PyExc_IOError, message); break; case 10: // {path}: Failed to open file ({mode}): {strerror} // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 11: // {path}: The file contains data of an unknown image type // May be raised when opening an image PyErr_SetString(PyExc_TypeError, message); break; case 12: // The memory contains data of an unknown image type // May be raised when instantiating an image from a data buffer PyErr_SetString(PyExc_IOError, message); break; case 13: // Image type {image type} is not supported // May be raised when creating a new image PyErr_SetString(PyExc_IOError, message); break; case 14: // Failed to read image data // May be raised by readMetadata() PyErr_SetString(PyExc_IOError, message); break; case 15: // This does not look like a JPEG image // May be raised by readMetadata() PyErr_SetString(PyExc_IOError, message); break; case 17: // {old path}: Failed to rename file to {new path}: {strerror} // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 18: // {path}: Transfer failed: {strerror} // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 19: // Memory transfer failed: {strerror} // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 20: // Failed to read input data // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 21: // Failed to write image // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 22: // Input data does not contain a valid image // May be raised by writeMetadata() PyErr_SetString(PyExc_IOError, message); break; case 23: // Invalid ifdId {ifdId} // May be raised when instantiating an ExifKey from a tag and // IFD item string PyErr_SetString(PyExc_KeyError, message); break; case 26: // Offset out of range // May be raised by writeMetadata() (TIFF) PyErr_SetString(PyExc_IOError, message); break; case 27: // Unsupported data area offset type // May be raised by writeMetadata() (TIFF) PyErr_SetString(PyExc_IOError, message); break; case 28: // Invalid charset: `{charset name}' // May be raised when instantiating a CommentValue from a string PyErr_SetString(PyExc_ValueError, message); break; case 29: // Unsupported date format // May be raised when instantiating a DateValue from a string PyErr_SetString(PyExc_ValueError, message); break; case 30: // Unsupported time format // May be raised when instantiating a TimeValue from a string PyErr_SetString(PyExc_ValueError, message); break; case 31: // Writing to {image format} images is not supported // May be raised by writeMetadata() for certain image types PyErr_SetString(PyExc_IOError, message); break; case 32: // Setting {metadata type} in {image format} images is not supported // May be raised when setting certain types of metadata for certain // image types that don't support them PyErr_SetString(PyExc_ValueError, message); break; case 33: // This does not look like a CRW image // May be raised by readMetadata() (CRW) PyErr_SetString(PyExc_IOError, message); break; case 35: // No namespace info available for XMP prefix `{prefix}' // May be raised when retrieving property info for an XmpKey PyErr_SetString(PyExc_KeyError, message); break; case 36: // No prefix registered for namespace `{namespace}', needed for // property path `{property path}' // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_KeyError, message); break; case 37: // Size of {type of metadata} JPEG segment is larger than // 65535 bytes // May be raised by writeMetadata() (JPEG) PyErr_SetString(PyExc_ValueError, message); break; case 38: // Unhandled Xmpdatum {key} of type {value type} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_TypeError, message); break; case 39: // Unhandled XMP node {key} with opt={XMP Toolkit option flags} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_TypeError, message); break; case 40: // XMP Toolkit error {error id}: {error message} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_RuntimeError, message); break; case 41: // Failed to decode Lang Alt property {property path} // with opt={XMP Toolkit option flags} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 42: // Failed to decode Lang Alt qualifier {qualifier path} // with opt={XMP Toolkit option flags} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 43: // Failed to encode Lang Alt property {key} // May be raised by writeMetadata() PyErr_SetString(PyExc_ValueError, message); break; case 44: // Failed to determine property name from path {property path}, // namespace {namespace} // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_KeyError, message); break; case 45: // Schema namespace {namespace} is not registered with // the XMP Toolkit // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 46: // No namespace registered for prefix `{prefix}' // May be raised when instantiating an XmpKey from a string PyErr_SetString(PyExc_KeyError, message); break; case 47: // Aliases are not supported. Please send this XMP packet // to ahuggel@gmx.net `{namespace}', `{property path}', `{value}' // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; case 48: // Invalid XmpText type `{type}' // May be raised when instantiating an XmpTextValue from a string PyErr_SetString(PyExc_TypeError, message); break; case 49: // TIFF directory {TIFF directory name} has too many entries // May be raised by writeMetadata() (TIFF) PyErr_SetString(PyExc_IOError, message); break; // Added in py3exiv2 case 50: // Multiple TIFF array element tags %1 in one directory") // May be raised by readMetadata() (TIFF) PyErr_SetString(PyExc_IOError, message); break; case 51: // TIFF array element tag %1 has wrong type") }, // %1=tag number // May be raised by readMetadata() (TIFF) PyErr_SetString(PyExc_TypeError, message); break; case 52: // %1 has invalid XMP value type `%2' // May be raised by readMetadata() when reading the XMP data PyErr_SetString(PyExc_ValueError, message); break; // Custom error codes case METADATA_NOT_READ: PyErr_SetString(PyExc_IOError, "Image metadata has not been read yet"); break; case NON_REPEATABLE: PyErr_SetString(PyExc_KeyError, "Tag is not repeatable"); break; case KEY_NOT_FOUND: PyErr_SetString(PyExc_KeyError, "Tag not set"); break; case INVALID_VALUE: PyErr_SetString(PyExc_ValueError, "Invalid value"); break; case EXISTING_PREFIX: PyErr_SetString(PyExc_KeyError, "A namespace with this prefix already exists"); break; case BUILTIN_NS: PyErr_SetString(PyExc_KeyError, "Cannot unregister a builtin namespace"); break; case NOT_REGISTERED: PyErr_SetString(PyExc_KeyError, "No namespace registered under this name"); break; // Default handler default: PyErr_SetString(PyExc_RuntimeError, message); } } #endif bool initialiseXmpParser() { if (!Exiv2::XmpParser::initialize()) return false; std::string prefix("py3exiv2"); std::string name("www.py3exiv2.tuxfamily.org/"); try { const std::string& ns = Exiv2::XmpProperties::ns(prefix); } catch (Exiv2::Error& error) { // No namespace exists with the requested prefix, it is safe to // register a new one. Exiv2::XmpProperties::registerNs(name, prefix); } return true; } bool closeXmpParser() { std::string name("www.py3exiv2.tuxfamily.org/"); const std::string& prefix = Exiv2::XmpProperties::prefix(name); if (prefix != "") { Exiv2::XmpProperties::unregisterNs(name); } Exiv2::XmpParser::terminate(); return true; } void registerXmpNs(const std::string& name, const std::string& prefix) { try { const std::string& ns = Exiv2::XmpProperties::ns(prefix); } catch (Exiv2::Error& error) { // No namespace exists with the requested prefix, it is safe to // register a new one. Exiv2::XmpProperties::registerNs(name, prefix); return; } #ifdef HAVE_EXIV2_ERROR_CODE { std::string mssg("Namespace already exists: "); mssg += prefix; throw Exiv2::Error(Exiv2::kerInvalidKey, mssg); } #else { throw Exiv2::Error(EXISTING_PREFIX, prefix); } #endif } void unregisterXmpNs(const std::string& name) { const std::string& prefix = Exiv2::XmpProperties::prefix(name); if (prefix != "") { Exiv2::XmpProperties::unregisterNs(name); try { const Exiv2::XmpNsInfo* info = Exiv2::XmpProperties::nsInfo(prefix); } catch (Exiv2::Error& error) { // The namespace has been successfully unregistered. return; } // The namespace hasn’t been unregistered because it’s builtin. #ifdef HAVE_EXIV2_ERROR_CODE { std::string mssg("Can't unregister builtin namespace: "); mssg += name; throw Exiv2::Error(Exiv2::kerInvalidKey, mssg); } #else { throw Exiv2::Error(BUILTIN_NS, name); } #endif } else #ifdef HAVE_EXIV2_ERROR_CODE { std::string mssg("Namespace does not exists: "); mssg += name; throw Exiv2::Error(Exiv2::kerInvalidKey, mssg); } #else { throw Exiv2::Error(NOT_REGISTERED, name); } #endif } void unregisterAllXmpNs() { // Unregister all custom namespaces. Exiv2::XmpProperties::unregisterNs(); } } // End of namespace exiv2wrapper py3exiv2-0.7.2/src/exiv2wrapper.hpp000066400000000000000000000205131364152456500171630ustar00rootroot00000000000000// ***************************************************************************** /* * Copyright (C) 2006-2010 Olivier Tilloy * Copyright (C) 2015-2020 Vincent Vande Vyvre * * This file is part of the py3exiv2 distribution. * * py3exiv2 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. * * py3exiv2 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 py3exiv2; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. */ /* Maintainer: Vincent Vande Vyvre */ // ***************************************************************************** #ifndef __exiv2wrapper__ #define __exiv2wrapper__ #include #include "exiv2/exiv2.hpp" #include "boost/python.hpp" namespace exiv2wrapper { class Image; class ExifTag { public: // Constructor ExifTag(const std::string& key, Exiv2::Exifdatum* datum=0, Exiv2::ExifData* data=0, Exiv2::ByteOrder byteOrder=Exiv2::invalidByteOrder); ~ExifTag(); void setRawValue(const std::string& value); void setParentImage(Image& image); const std::string getKey(); const std::string getType(); const std::string getName(); const std::string getLabel(); const std::string getDescription(); const std::string getSectionName(); const std::string getSectionDescription(); const std::string getRawValue(); const std::string getHumanValue(); int getByteOrder(); private: Exiv2::ExifKey _key; Exiv2::Exifdatum* _datum; Exiv2::ExifData* _data; std::string _type; std::string _name; std::string _label; std::string _description; std::string _sectionName; std::string _sectionDescription; int _byteOrder; }; class IptcTag { public: // Constructor IptcTag(const std::string& key, Exiv2::IptcData* data=0); ~IptcTag(); void setRawValues(const boost::python::list& values); void setParentImage(Image& image); const std::string getKey(); const std::string getType(); const std::string getName(); const std::string getTitle(); const std::string getDescription(); const std::string getPhotoshopName(); const bool isRepeatable(); const std::string getRecordName(); const std::string getRecordDescription(); const boost::python::list getRawValues(); private: Exiv2::IptcKey _key; bool _from_data; // whether the tag is built from an existing IptcData Exiv2::IptcData* _data; std::string _type; std::string _name; std::string _title; std::string _description; std::string _photoshopName; bool _repeatable; std::string _recordName; std::string _recordDescription; }; class XmpTag { public: // Constructor XmpTag(const std::string& key, Exiv2::Xmpdatum* datum=0); ~XmpTag(); void setTextValue(const std::string& value); void setArrayValue(const boost::python::list& values); void setLangAltValue(const boost::python::dict& values); void setParentImage(Image& image); const std::string getKey(); const std::string getExiv2Type(); const std::string getType(); const std::string getName(); const std::string getTitle(); const std::string getDescription(); const std::string getTextValue(); const boost::python::list getArrayValue(); const boost::python::dict getLangAltValue(); private: Exiv2::XmpKey _key; bool _from_datum; // whether the tag is built from an existing Xmpdatum Exiv2::Xmpdatum* _datum; std::string _exiv2_type; std::string _type; std::string _name; std::string _title; std::string _description; }; class Preview { public: Preview(const Exiv2::PreviewImage& previewImage); boost::python::object getData() const; void writeToFile(const std::string& path) const; std::string _mimeType; std::string _extension; unsigned int _size; boost::python::tuple _dimensions; std::string _data; const Exiv2::byte* pData; }; class Image { public: // Constructors Image(const std::string& filename); Image(const std::string& buffer, unsigned long size); Image(const Image& image); ~Image(); void readMetadata(); void writeMetadata(); // Read-only access to the dimensions of the picture. unsigned int pixelWidth() const; unsigned int pixelHeight() const; // Read-only access to the MIME type of the image. std::string mimeType() const; // Read and write access to the EXIF tags. // For a complete list of the available EXIF tags, see // libexiv2's documentation (http://exiv2.org/tags.html). // Return a list of all the keys of available EXIF tags set in the // image. boost::python::list exifKeys(); // Return the required EXIF tag. // Throw an exception if the tag is not set. const ExifTag getExifTag(std::string key); // Delete the required EXIF tag. // Throw an exception if the tag was not set. void deleteExifTag(std::string key); // Read and write access to the IPTC tags. // For a complete list of the available IPTC tags, see // libexiv2's documentation (http://exiv2.org/iptc.html). // Returns a list of all the keys of available IPTC tags set in the // image. This list has no duplicates: each of its items is unique, // even if a tag is present more than once. boost::python::list iptcKeys(); // Return the required IPTC tag. // Throw an exception if the tag is not set. const IptcTag getIptcTag(std::string key); // Delete (all the repetitions of) the required IPTC tag. // Throw an exception if the tag was not set. void deleteIptcTag(std::string key); boost::python::list xmpKeys(); // Return the required XMP tag. // Throw an exception if the tag is not set. const XmpTag getXmpTag(std::string key); // Delete the required XMP tag. // Throw an exception if the tag was not set. void deleteXmpTag(std::string key); // Comment const std::string getComment() const; void setComment(const std::string& comment); void clearComment(); // 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); boost::python::list 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; // Return the image data buffer. boost::python::object getDataBuffer() const; // Accessors Exiv2::ExifData* getExifData() { return _exifData; }; Exiv2::IptcData* getIptcData() { return _iptcData; }; Exiv2::XmpData* getXmpData() { return _xmpData; }; Exiv2::ByteOrder getByteOrder() const; const std::string getIptcCharset() const; private: std::string _filename; Exiv2::byte* _data; long _size; Exiv2::Image::AutoPtr _image; 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 bool _dataRead; void _instantiate_image(); }; // Translate an Exiv2 generic exception into a Python exception void translateExiv2Error(Exiv2::Error const& error); // Functions to manipulate custom XMP namespaces bool initialiseXmpParser(); bool closeXmpParser(); void registerXmpNs(const std::string& name, const std::string& prefix); void unregisterXmpNs(const std::string& name); void unregisterAllXmpNs(); } // End of namespace exiv2wrapper #endif py3exiv2-0.7.2/src/exiv2wrapper_python.cpp000066400000000000000000000144011364152456500205560ustar00rootroot00000000000000// ***************************************************************************** /* * Copyright (C) 2006-2012 Olivier Tilloy * Copyright (C) 2015-2020 Vincent Vande Vyvre * * 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. */ /* Maintainer: Vincent Vande Vyvre */ // ***************************************************************************** #include #include "exiv2wrapper.hpp" #include "exiv2/exv_conf.h" #include "exiv2/version.hpp" #include using namespace boost::python; using namespace exiv2wrapper; BOOST_PYTHON_MODULE(libexiv2python) { scope().attr("exiv2_version_info") = \ boost::python::make_tuple(EXIV2_MAJOR_VERSION, EXIV2_MINOR_VERSION, EXIV2_PATCH_VERSION); register_exception_translator(&translateExiv2Error); // Swallow all warnings and error messages written by libexiv2 to stderr // (if it was compiled with DEBUG or without SUPPRESS_WARNINGS). // See https://bugs.launchpad.net/pyexiv2/+bug/507620. std::cerr.rdbuf(NULL); class_("_ExifTag", init()) .def("_setRawValue", &ExifTag::setRawValue) .def("_setParentImage", &ExifTag::setParentImage) .def("_getKey", &ExifTag::getKey) .def("_getType", &ExifTag::getType) .def("_getName", &ExifTag::getName) .def("_getLabel", &ExifTag::getLabel) .def("_getDescription", &ExifTag::getDescription) .def("_getSectionName", &ExifTag::getSectionName) .def("_getSectionDescription", &ExifTag::getSectionDescription) .def("_getRawValue", &ExifTag::getRawValue) .def("_getHumanValue", &ExifTag::getHumanValue) .def("_getByteOrder", &ExifTag::getByteOrder) ; class_("_IptcTag", init()) .def("_setRawValues", &IptcTag::setRawValues) .def("_setParentImage", &IptcTag::setParentImage) .def("_getKey", &IptcTag::getKey) .def("_getType", &IptcTag::getType) .def("_getName", &IptcTag::getName) .def("_getTitle", &IptcTag::getTitle) .def("_getDescription", &IptcTag::getDescription) .def("_getPhotoshopName", &IptcTag::getPhotoshopName) .def("_isRepeatable", &IptcTag::isRepeatable) .def("_getRecordName", &IptcTag::getRecordName) .def("_getRecordDescription", &IptcTag::getRecordDescription) .def("_getRawValues", &IptcTag::getRawValues) ; class_("_XmpTag", init()) .def("_setTextValue", &XmpTag::setTextValue) .def("_setArrayValue", &XmpTag::setArrayValue) .def("_setLangAltValue", &XmpTag::setLangAltValue) .def("_setParentImage", &XmpTag::setParentImage) .def("_getKey", &XmpTag::getKey) .def("_getExiv2Type", &XmpTag::getExiv2Type) .def("_getType", &XmpTag::getType) .def("_getName", &XmpTag::getName) .def("_getTitle", &XmpTag::getTitle) .def("_getDescription", &XmpTag::getDescription) .def("_getTextValue", &XmpTag::getTextValue) .def("_getArrayValue", &XmpTag::getArrayValue) .def("_getLangAltValue", &XmpTag::getLangAltValue) ; class_("_Preview", init()) .def_readonly("mime_type", &Preview::_mimeType) .def_readonly("extension", &Preview::_extension) .def_readonly("size", &Preview::_size) .def_readonly("dimensions", &Preview::_dimensions) .def_readonly("data", &Preview::_data) .def("get_data", &Preview::getData) .def("write_to_file", &Preview::writeToFile) ; class_("_Image", init()) .def(init()) .def("_readMetadata", &Image::readMetadata) .def("_writeMetadata", &Image::writeMetadata) .def("_getPixelWidth", &Image::pixelWidth) .def("_getPixelHeight", &Image::pixelHeight) .def("_getMimeType", &Image::mimeType) .def("_exifKeys", &Image::exifKeys) .def("_getExifTag", &Image::getExifTag) .def("_deleteExifTag", &Image::deleteExifTag) .def("_iptcKeys", &Image::iptcKeys) .def("_getIptcTag", &Image::getIptcTag) .def("_deleteIptcTag", &Image::deleteIptcTag) .def("_xmpKeys", &Image::xmpKeys) .def("_getXmpTag", &Image::getXmpTag) .def("_deleteXmpTag", &Image::deleteXmpTag) .def("_getComment", &Image::getComment) .def("_setComment", &Image::setComment) .def("_clearComment", &Image::clearComment) .def("_previews", &Image::previews) .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) .def("_getIptcCharset", &Image::getIptcCharset) ; def("_initialiseXmpParser", initialiseXmpParser); def("_closeXmpParser", closeXmpParser); def("_registerXmpNs", registerXmpNs, args("name", "prefix")); def("_unregisterXmpNs", unregisterXmpNs, args("name")); def("_unregisterAllXmpNs", unregisterAllXmpNs); } py3exiv2-0.7.2/src/py3exiv2.egg-info/000077500000000000000000000000001364152456500171765ustar00rootroot00000000000000py3exiv2-0.7.2/src/py3exiv2.egg-info/PKG-INFO000066400000000000000000000040041364152456500202710ustar00rootroot00000000000000Metadata-Version: 1.1 Name: py3exiv2 Version: 0.7.2 Summary: A Python3 binding to the library exiv2 Home-page: https://launchpad.net/py3exiv2 Author: Vincent Vande Vyvre Author-email: vincent.vandevyvre@oqapy.eu License: GPL-3 Description: python3-exiv2 ============= A Python 3 binding to the library exiv2. python3-exiv2 is a Python 3 binding to exiv2, the C++ library for manipulation of EXIF, IPTC and XMP image metadata. It is a python 3 module that allows your scripts to read and write metadata (EXIF, IPTC, XMP, thumbnails) embedded in image files (JPEG, TIFF, ...). It is designed as a high-level interface to the functionalities offered by libexiv2. Using python’s built-in data types and standard modules, it provides easy manipulation of image metadata. python3-exiv2 is distributed under the GPL version 3 license. The main content of the code was initially written by Olivier Tilloy for Python 2 under the name pyexiv2. py3exiv2 depends on the following libraries: * python (≥ 3.2) * boost.python3 (http://www.boost.org/libs/python/doc/index.html) * exiv2 (http://www.exiv2.org/) Build depends: * python-all-dev (≥ 3.2) * libexiv2-dev (≥ 0.20) * libboost-python-dev (≥ 1.48) * g++ Keywords: exiv2 pyexiv2 EXIF IPTC XMP image metadata Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Programming Language :: C++ Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 py3exiv2-0.7.2/src/py3exiv2.egg-info/SOURCES.txt000066400000000000000000000006101364152456500210570ustar00rootroot00000000000000MANIFEST.in README setup.py src/exiv2wrapper.cpp src/exiv2wrapper.hpp src/exiv2wrapper_python.cpp src/py3exiv2.egg-info/PKG-INFO src/py3exiv2.egg-info/SOURCES.txt src/py3exiv2.egg-info/dependency_links.txt src/py3exiv2.egg-info/top_level.txt src/pyexiv2/__init__.py src/pyexiv2/exif.py src/pyexiv2/iptc.py src/pyexiv2/metadata.py src/pyexiv2/preview.py src/pyexiv2/utils.py src/pyexiv2/xmp.pypy3exiv2-0.7.2/src/py3exiv2.egg-info/dependency_links.txt000066400000000000000000000000011364152456500232440ustar00rootroot00000000000000 py3exiv2-0.7.2/src/py3exiv2.egg-info/top_level.txt000066400000000000000000000000271364152456500217270ustar00rootroot00000000000000libexiv2python pyexiv2 py3exiv2-0.7.2/src/pyexiv2/000077500000000000000000000000001364152456500154215ustar00rootroot00000000000000py3exiv2-0.7.2/src/pyexiv2/__init__.py000066400000000000000000000066241364152456500175420ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ****************************************************************************** # # Copyright (C) 2006-2011 Olivier Tilloy # Copyright (C) 2015-2020 Vincent Vande Vyvre # # This file is part of the py3exiv2 distribution. # # py3exiv2 is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # version 3 as published by the Free Software Foundation. # # py3exiv2 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 py3exiv2; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. # # Maintainer: Vincent Vande Vyvre # # ****************************************************************************** """ Manipulation of EXIF, IPTC and XMP metadata and thumbnails embedded in images. The ImageMetadata class provides read/write access to all the metadata and the various thumbnails embedded in an image file such as JPEG and TIFF files. Metadata is accessed through tag classes (ExifTag, IptcTag, XmpTag) and the tag values are conveniently wrapped in python objects. For example, a tag containing a date/time information for the image (e.g. Exif.Photo.DateTimeOriginal) will be represented by a python datetime.datetime object. This module is a python layer on top of the low-level python binding of the C++ library Exiv2, libexiv2python. A typical use of this binding would be: >>> import pyexiv2 >>> metadata = pyexiv2.ImageMetadata('test/smiley.jpg') >>> metadata.read() >>> print(metadata.exif_keys) ['Exif.Image.ImageDescription', 'Exif.Image.XResolution', 'Exif.Image.YResolution', 'Exif.Image.ResolutionUnit', 'Exif.Image.Software', 'Exif.Image.DateTime', 'Exif.Image.Artist', 'Exif.Image.Copyright', 'Exif.Image.ExifTag', 'Exif.Photo.Flash', 'Exif.Photo.PixelXDimension', 'Exif.Photo.PixelYDimension'] >>> print(metadata['Exif.Image.DateTime'].value) 2004-07-13 21:23:44 >>> import datetime >>> metadata['Exif.Image.DateTime'].value = datetime.datetime.today() >>> metadata.write() """ import libexiv2python from pyexiv2.metadata import ImageMetadata from pyexiv2.exif import ExifValueError, ExifTag, ExifThumbnail from pyexiv2.iptc import IptcValueError, IptcTag from pyexiv2.xmp import (XmpValueError, XmpTag, register_namespace, unregister_namespace, unregister_namespaces) from pyexiv2.preview import Preview from pyexiv2.utils import (FixedOffset, NotifyingList, undefined_to_string, string_to_undefined, GPSCoordinate) def _make_version(version_info): return '.'.join([str(i) for i in version_info]) #: A tuple containing the three components of the version number: major, minor, micro. version_info = (0, 7, 2) #: The version of the module as a string (major.minor.micro). __version__ = _make_version(version_info) #: A tuple containing the three components of the version number of libexiv2: major, minor, micro. exiv2_version_info = libexiv2python.exiv2_version_info #: The version of libexiv2 as a string (major.minor.micro). __exiv2_version__ = _make_version(exiv2_version_info) py3exiv2-0.7.2/src/pyexiv2/exif.py000066400000000000000000000440571364152456500167400ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ****************************************************************************** # # Copyright (C) 2006-2011 Olivier Tilloy # Copyright (C) 2015-2020 Vincent Vande Vyvre # # This file is part of the py3exiv2 distribution. # # py3exiv2 is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # version 3 as published by the Free Software Foundation. # # py3exiv2 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 py3exiv2; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. # # Maintainer: Vincent Vande Vyvre # # ****************************************************************************** """ EXIF specific code. """ import libexiv2python from pyexiv2.utils import (is_fraction, make_fraction, fraction_to_string, NotifyingList, ListenerInterface, undefined_to_string, string_to_undefined, DateTimeFormatter) import time import datetime import sys class ExifValueError(ValueError): """Exception raised when failing to parse the *value* of an EXIF tag. """ def __init__(self, value, type_): """Instanciate the ExifValueError. Args: value -- the value that fails to be parsed type_ -- the EXIF type of the tag """ self.value = value self.type = type_ def __str__(self): return 'Invalid value for EXIF type [%s]: [%s]' %(self.type, self.value) class ExifTag(ListenerInterface): """An EXIF tag. Here is a correspondance table between the EXIF types and the possible python types the value of a tag may take: - Ascii: :class:`datetime.datetime`, :class:`datetime.date`, string - Byte, SByte: bytes - Comment: string - Long, SLong: [list of] int - Short, SShort: [list of] int - Rational, SRational: [list of] :class:`fractions.Fraction` if available (Python ≥ 2.6) or :class:`pyexiv2.utils.Rational` - Undefined: string """ # According to the EXIF specification, the only accepted format for an Ascii # value representing a datetime is '%Y:%m:%d %H:%M:%S', but it seems that # others formats can be found in the wild. _datetime_formats = ('%Y:%m:%d %H:%M:%S', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%SZ') _date_formats = ('%Y:%m:%d',) def __init__(self, key, value=None, _tag=None): """ The tag can be initialized with an optional value which expected type depends on the EXIF type of the tag. Args: key -- the key of the tag value -- the value of the tag """ super().__init__() if _tag is not None: self._tag = _tag else: self._tag = libexiv2python._ExifTag(key) self._raw_value = None self._value = None self._value_cookie = False if value is not None: self._set_value(value) def _set_owner(self, metadata): self._tag._setParentImage(metadata._image) @staticmethod def _from_existing_tag(_tag): """Build a tag from an already existing libexiv2python._ExifTag. """ tag = ExifTag(_tag._getKey(), _tag=_tag) # Do not set the raw_value property, as it would call _tag._setRawValue # (see https://bugs.launchpad.net/pyexiv2/+bug/582445). tag._raw_value = _tag._getRawValue() tag._value_cookie = True return tag @property def key(self): """The key of the tag in the dotted form ``familyName.groupName.tagName`` where ``familyName`` = ``exif``. """ return self._tag._getKey() @property def type(self): """The EXIF type of the tag (one of Ascii, Byte, SByte, Comment, Short, SShort, Long, SLong, Rational, SRational, Undefined). """ return self._tag._getType() @property def name(self): """The name of the tag (this is also the third part of the key). """ return self._tag._getName() @property def label(self): """The title (label) of the tag. """ return self._tag._getLabel() @property def description(self): """The description of the tag. """ return self._tag._getDescription() @property def section_name(self): """The name of the tag's section. """ return self._tag._getSectionName() @property def section_description(self): """The description of the tag's section. """ return self._tag._getSectionDescription() def _get_raw_value(self): return self._raw_value def _set_raw_value(self, value): self._tag._setRawValue(value) self._raw_value = value self._value_cookie = True raw_value = property(fget=_get_raw_value, fset=_set_raw_value, doc='The raw value of the tag as a string.') def _compute_value(self): """Lazy computation of the value from the raw value. """ if self.type in ('Short', 'SShort', 'Long', 'SLong', 'Rational', 'SRational'): # May contain multiple values values = self._raw_value.split() if len(values) > 1: # Make values a notifying list values = [self._convert_to_python(v) for v in values] self._value = NotifyingList(values) self._value.register_listener(self) self._value_cookie = False return self._value = self._convert_to_python(self._raw_value) self._value_cookie = False def _get_value(self): if self._value_cookie: self._compute_value() return self._value def _set_value(self, value): if isinstance(value, (list, tuple)): raw_values = [self._convert_to_string(v) for v in value] self.raw_value = ' '.join(raw_values) else: self.raw_value = self._convert_to_string(value) if isinstance(self._value, NotifyingList): self._value.unregister_listener(self) if isinstance(value, NotifyingList): # Already a notifying list self._value = value self._value.register_listener(self) elif isinstance(value, (list, tuple)): # Make the values a notifying list self._value = NotifyingList(value) self._value.register_listener(self) else: # Single value self._value = value self._value_cookie = False value = property(fget=_get_value, fset=_set_value, doc='The value of the tag as a python object.') @property def human_value(self): """A (read-only) human-readable representation of the value of the tag. """ return self._tag._getHumanValue() or None def contents_changed(self): # Implementation of the ListenerInterface. # React on changes to the list of values of the tag. # self._value is a list of values and its contents changed. self._set_value(self._value) def _match_encoding(self, charset): # charset see: # http://www.exiv2.org/doc/classExiv2_1_1CommentValue.html # enum CharsetId { # ascii, jis, unicode, undefined, # invalidCharsetId, lastCharsetId } encoding = sys.getdefaultencoding() if charset in ('Ascii', 'ascii'): encoding = 'ascii' elif charset in ('Jis', 'jis'): encoding = 'shift_jis' elif charset in ('Unicode', 'unicode'): encoding = 'utf-8' return encoding def _convert_to_python(self, value): """ Convert one raw value to its corresponding python type. :param value: the raw value to be converted :type value: string :return: the value converted to its corresponding python type :raise ExifValueError: if the conversion fails """ if self.type == 'Ascii': # The value may contain a Datetime for format in self._datetime_formats: try: t = time.strptime(value, format) except ValueError: continue else: return datetime.datetime(*t[:6]) # Or a Date (e.g. Exif.GPSInfo.GPSDateStamp) for format in self._date_formats: try: t = time.strptime(value, format) except ValueError: continue else: return datetime.date(*t[:3]) # Default to string. # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. return value elif self.type in ('Byte', 'SByte'): if isinstance(value, bytes): return value.decode('utf-8') return value elif self.type == 'Comment': if isinstance(value, str): if value.startswith('charset='): charset, val = value.split(' ', 1) return val return value if value.startswith(b'charset='): charset = charset.split('=')[1].strip('"') encoding = self._match_encoding(charset) return val.decode(encoding, 'replace') else: # No encoding defined. try: return value.decode('utf-8') except UnicodeError: pass return value elif self.type in ('Short', 'SShort'): try: return int(value) except ValueError: raise ExifValueError(value, self.type) elif self.type in ('Long', 'SLong'): try: return int(value) except ValueError: raise ExifValueError(value, self.type) elif self.type in ('Rational', 'SRational'): try: r = make_fraction(value) except (ValueError, ZeroDivisionError): raise ExifValueError(value, self.type) else: if self.type == 'Rational' and r.numerator < 0: raise ExifValueError(value, self.type) return r elif self.type == 'Undefined': # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. return undefined_to_string(value) raise ExifValueError(value, self.type) def _convert_to_string(self, value): """ Convert one value to its corresponding string representation, suitable to pass to libexiv2. :param value: the value to be converted :return: the value converted to its corresponding string representation :rtype: string :raise ExifValueError: if the conversion fails """ if self.type == 'Ascii': if isinstance(value, datetime.datetime): return DateTimeFormatter.exif(value) elif isinstance(value, datetime.date): if self.key == 'Exif.GPSInfo.GPSDateStamp': # Special case return DateTimeFormatter.exif(value) else: return '%s 00:00:00' % DateTimeFormatter.exif(value) else: return value elif self.type in ('Byte', 'SByte'): if isinstance(value, str): try: return value.encode('utf-8') except UnicodeEncodeError: raise ExifValueError(value, self.type) elif isinstance(value, bytes): return value else: raise ExifValueError(value, self.type) elif self.type == 'Comment': return self._convert_to_bytes(value) elif self.type == 'Short': if isinstance(value, int) and value >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SShort': if isinstance(value, int): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Long': if isinstance(value, int) and value >= 0: return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'SLong': if isinstance(value, int): return str(value) else: raise ExifValueError(value, self.type) elif self.type == 'Rational': if is_fraction(value) and value.numerator >= 0: return fraction_to_string(value) else: raise ExifValueError(value, self.type) elif self.type == 'SRational': if is_fraction(value): return fraction_to_string(value) else: raise ExifValueError(value, self.type) elif self.type == 'Undefined': if isinstance(value, str): try: return string_to_undefined(value) except UnicodeEncodeError: raise ExifValueError(value, self.type) elif isinstance(value, bytes): return string_to_undefined(value) else: raise ExifValueError(value, self.type) raise ExifValueError(value, self.type) def _convert_to_bytes(self, value): if value is None: return if isinstance(value, str): if value.startswith('charset='): charset, val = value.split(' ', 1) charset = charset.split('=')[1].strip('"') encoding = self._match_encoding(charset) else: encoding = 'utf-8' charset = 'Unicode' try: val = value.encode(encoding) except UnicodeError: pass else: #self._set_raw_value('charset=%s %s' % (charset, val)) return val elif isinstance(value, bytes): return value else: raise ExifValueError(value, self.type) def __str__(self): """ :return: a string representation of the EXIF tag for debugging purposes :rtype: string """ left = '%s [%s]' % (self.key, self.type) if self._raw_value is None: right = '(No value)' elif self.type == 'Undefined' and len(self._raw_value) > 100: right = '(Binary value suppressed)' else: right = self._raw_value return '<%s = %s>' % (left, right) # Support for pickling. def __getstate__(self): return (self.key, self.raw_value) def __setstate__(self, state): key, raw_value = state self._tag = libexiv2python._ExifTag(key) self.raw_value = raw_value 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 self._metadata._tags['exif'].keys(): 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): buf_ = self._metadata._image._getExifThumbnailData() return buf_ 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.') py3exiv2-0.7.2/src/pyexiv2/iptc.py000066400000000000000000000270731364152456500167430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ****************************************************************************** # # Copyright (C) 2006-2011 Olivier Tilloy # Copyright (C) 2015-2020 Vincent Vande Vyvre # # This file is part of the py3exiv2 distribution. # # py3exiv2 is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # version 3 as published by the Free Software Foundation. # # py3exiv2 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 py3exiv2; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. # # Maintainer: Vincent Vande Vyvre # # ****************************************************************************** """ IPTC specific code. """ import libexiv2python from pyexiv2.utils import ListenerInterface, NotifyingList, \ FixedOffset, DateTimeFormatter import time import datetime import re import warnings class IptcValueError(ValueError): """ Exception raised when failing to parse the *value* of an IPTC tag. :attribute value: the value that fails to be parsed :type value: string :attribute type: the IPTC type of the tag :type type: string """ def __init__(self, value, type): self.value = value self.type = type def __str__(self): return 'Invalid value for IPTC type [%s]: [%s]' % \ (self.type, self.value) class IptcTag(ListenerInterface): """An IPTC tag. This tag can have several values (tags that have the *repeatable* property). Here is a correspondance table between the IPTC types and the possible python types the value of a tag may take: - Short: int - String: string - Date: :class:`datetime.date` - Time: :class:`datetime.time` - Undefined: string """ # strptime is not flexible enough to handle all valid Time formats, we use a # custom regular expression _time_zone_re = r'(?P\+|-)(?P\d{2}):(?P\d{2})' _time_re = re.compile(r'(?P\d{2}):(?P\d{2}):(?P\d{2})(?P%s)' % _time_zone_re) def __init__(self, key, values=None, _tag=None): """The tag can be initialized with an optional list of values which expected type depends on the IPTC type of the tag. Args: key -- the key of the tag values -- the values of the tag """ super(IptcTag, self).__init__() if _tag is not None: self._tag = _tag else: self._tag = libexiv2python._IptcTag(key) self._raw_values = None self._values = None self._values_cookie = False if values is not None: self._set_values(values) def _set_owner(self, metadata): self._tag._setParentImage(metadata._image) @staticmethod def _from_existing_tag(_tag): # Build a tag from an already existing libexiv2python._IptcTag tag = IptcTag(_tag._getKey(), _tag=_tag) # Do not set the raw_value property, as it would call # _tag._setRawValues # (see https://bugs.launchpad.net/pyexiv2/+bug/582445). tag._raw_values = _tag._getRawValues() tag._values_cookie = True return tag @property def key(self): """The key of the tag in the dotted form ``familyName.groupName.tagName`` where ``familyName`` = ``iptc``. """ return self._tag._getKey() @property def type(self): """The IPTC type of the tag (one of Short, String, Date, Time, Undefined). """ return self._tag._getType() @property def name(self): """The name of the tag (this is also the third part of the key). """ return self._tag._getName() @property def title(self): """The title (label) of the tag. """ return self._tag._getTitle() @property def description(self): """The description of the tag. """ return self._tag._getDescription() @property def photoshop_name(self): """The Photoshop name of the tag. """ return self._tag._getPhotoshopName() @property def repeatable(self): """Whether the tag is repeatable (accepts several values). """ return self._tag._isRepeatable() @property def record_name(self): """The name of the tag's record. """ return self._tag._getRecordName() @property def record_description(self): """The description of the tag's record. """ return self._tag._getRecordDescription() def _get_raw_values(self): return self._raw_values def _set_raw_values(self, values): if not isinstance(values, (list, tuple)): raise TypeError('Expecting a list of values') self._tag._setRawValues(values) self._raw_values = values self._values_cookie = True raw_value = property(fget=_get_raw_values, fset=_set_raw_values, doc='The raw values of the tag as a list of strings.') def _compute_values(self): # Lazy computation of the values from the raw values self._values = NotifyingList([self._convert_to_python(v) for v in self._raw_values]) self._values.register_listener(self) self._values_cookie = False def _get_values(self): if self._values_cookie: self._compute_values() return self._values def _set_values(self, values): if not isinstance(values, (list, tuple)): raise TypeError('Expecting a list of values') self.raw_value = [self._convert_to_string(v) for v in values] if isinstance(self._values, NotifyingList): self._values.unregister_listener(self) if isinstance(values, NotifyingList): # Already a notifying list self._values = values else: # Make the values a notifying list self._values = NotifyingList(values) self._values.register_listener(self) self._values_cookie = False value = property(fget=_get_values, fset=_set_values, doc='The values of the tag as a list of python objects.') def contents_changed(self): # Implementation of the ListenerInterface. # React on changes to the list of values of the tag. # The contents of self._values was changed. # The following is a quick, non optimal solution. self._set_values(self._values) def _convert_to_python(self, value): """Convert one raw value to its corresponding python type. Args: value -- the raw value to be converted Return: the value converted to its corresponding python type Raise IptcValueError: if the conversion fails """ if self.type == 'Short': try: return int(value) except ValueError: raise IptcValueError(value, self.type) elif self.type == 'String': # There is currently no charset conversion. # TODO: guess the encoding and decode accordingly into unicode # where relevant. if isinstance(value, bytes): try: value = value.decode('utf-8') except UnicodeDecodeError: # Unknow encoding, return the raw value pass return value elif self.type == 'Date': # According to the IPTC specification, the format for a string field # representing a date is '%Y%m%d'. However, the string returned by # exiv2 using method DateValue::toString() is formatted using # pattern '%Y-%m-%d'. format = '%Y-%m-%d' try: t = time.strptime(value, format) return datetime.date(*t[:3]) except ValueError: raise IptcValueError(value, self.type) elif self.type == 'Time': # According to the IPTC specification, the format for a string field # representing a time is '%H%M%S±%H%M'. However, the string returned # by exiv2 using method TimeValue::toString() is formatted using # pattern '%H:%M:%S±%H:%M'. match = IptcTag._time_re.match(value) if match is None: raise IptcValueError(value, self.type) gd = match.groupdict() try: tzinfo = FixedOffset(gd['sign'], int(gd['ohours']), int(gd['ominutes'])) except TypeError: raise IptcValueError(value, self.type) try: return datetime.time(int(gd['hours']), int(gd['minutes']), int(gd['seconds']), tzinfo=tzinfo) except (TypeError, ValueError): raise IptcValueError(value, self.type) elif self.type == 'Undefined': # Binary data, return it unmodified return value raise IptcValueError(value, self.type) def _convert_to_string(self, value): """Convert one value to its corresponding string representation, suitable to pass to libexiv2. Args: value -- the value to be converted Return: the value converted to its corresponding string representation Raise IptcValueError: if the conversion fails """ if self.type == 'Short': if isinstance(value, int): return str(value) else: raise IptcValueError(value, self.type) elif self.type == 'String': if isinstance(value, str): try: return value.encode('utf-8') except UnicodeEncodeError: raise IptcValueError(value, self.type) elif isinstance(value, bytes): return value else: raise IptcValueError(value, self.type) elif self.type == 'Date': if isinstance(value, (datetime.date, datetime.datetime)): return DateTimeFormatter.iptc_date(value) else: raise IptcValueError(value, self.type) elif self.type == 'Time': if isinstance(value, (datetime.time, datetime.datetime)): return DateTimeFormatter.iptc_time(value) else: raise IptcValueError(value, self.type) elif self.type == 'Undefined': if isinstance(value, str): return value else: raise IptcValueError(value, self.type) raise IptcValueError(value, self.type) def __str__(self): """Return a string representation of the IPTC tag for debugging purposes """ left = '%s [%s]' % (self.key, self.type) if self._raw_values is None: right = '(No values)' else: right = self._raw_values return '<%s = %s>' % (left, right) # Support for pickling. def __getstate__(self): return (self.key, self.raw_value) def __setstate__(self, state): key, raw_value = state self._tag = libexiv2python._IptcTag(key) self.raw_value = raw_value py3exiv2-0.7.2/src/pyexiv2/metadata.py000066400000000000000000000470221364152456500175600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ****************************************************************************** # # Copyright (C) 2006-2011 Olivier Tilloy # Copyright (C) 2015-2020 Vincent Vande Vyvre # # This file is part of the py3exiv2 distribution. # # py3exiv2 is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # version 3 as published by the Free Software Foundation. # # py3exiv2 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 py3exiv2; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. # # Maintainer: Vincent Vande Vyvre # # ****************************************************************************** """ Provide the ImageMetadata class. """ import os import sys import codecs from errno import ENOENT from itertools import chain if sys.version_info < (3, 3): from collections import MutableMapping else: from collections.abc import MutableMapping import libexiv2python from pyexiv2.exif import ExifTag, ExifThumbnail from pyexiv2.iptc import IptcTag from pyexiv2.xmp import XmpTag from pyexiv2.preview import Preview class ImageMetadata(MutableMapping): """A container for all the metadata embedded in an image. It provides convenient methods for the manipulation of EXIF, IPTC and XMP metadata embedded in image files such as JPEG and TIFF files, using Python types. It also provides access to the previews embedded in an image. """ def __init__(self, filename): """Instanciate the ImageMeatadata class. Args: filename: str(path to an image file) """ self.filename = filename 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): """Instanciate the exiv2 image. Args: filename -- str(path to an image file) """ # This method is meant to be overridden in unit tests to easily replace # the internal image reference by a mock. if not os.path.exists(filename) or not os.path.isfile(filename): raise IOError(ENOENT, os.strerror(ENOENT), filename) # Remember the reference timestamps before doing any access to the file stat = os.stat(filename) self._atime = stat.st_atime self._mtime = stat.st_mtime return libexiv2python._Image(filename) @classmethod def from_buffer(cls, buffer_): """Instantiate an image container from an image memoryview. Args: buffer_ -- a memoryview containing image data as bytes """ obj = cls(None) 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. It is necessary to call this method once before attempting to access 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() def write(self, preserve_timestamps=False): """Write the metadata back to the image. Args: preserve_timestamps -- whether to preserve the file's original timestamps (access time and modification time) Type: boolean """ self._image._writeMetadata() if self.filename is None: return if preserve_timestamps: # Revert to the original timestamps os.utime(self.filename, (self._atime, self._mtime)) else: # Reset the reference timestamps stat = os.stat(self.filename) self._atime = stat.st_atime self._mtime = stat.st_mtime @property def dimensions(self): """A tuple containing the width and height of the image, expressed in pixels. """ return (self._image._getPixelWidth(), self._image._getPixelHeight()) @property def mime_type(self): """The mime type of the image, as a string. """ return self._image._getMimeType() @property def exif_keys(self): """Return the list of the keys of the available EXIF tags. """ if self._keys['exif'] is None: self._keys['exif'] = self._image._exifKeys() return self._keys['exif'] @property def iptc_keys(self): """Return the list of the keys of the available IPTC tags. """ if self._keys['iptc'] is None: self._keys['iptc'] = self._image._iptcKeys() return self._keys['iptc'] @property def xmp_keys(self): """Return the list of the keys of the available XMP tags. """ if self._keys['xmp'] is None: self._keys['xmp'] = self._image._xmpKeys() return self._keys['xmp'] def _get_exif_tag(self, key): """Return the EXIF tag for the given key. Throw a KeyError if the tag doesn't exist. Args: key -- the exif key """ try: return self._tags['exif'][key] except KeyError: _tag = self._image._getExifTag(key) tag = ExifTag._from_existing_tag(_tag) self._tags['exif'][key] = tag return tag def _get_iptc_tag(self, key): """Return the IPTC tag for the given key. Throw a KeyError if the tag doesn't exist. Args: key -- the iptc key """ try: return self._tags['iptc'][key] except KeyError: _tag = self._image._getIptcTag(key) tag = IptcTag._from_existing_tag(_tag) self._tags['iptc'][key] = tag return tag def _get_xmp_tag(self, key): """Return the XMP tag for the given key. Throw a KeyError if the tag doesn't exist. Args: key -- the xmp key """ try: return self._tags['xmp'][key] except KeyError: _tag = self._image._getXmpTag(key) tag = XmpTag._from_existing_tag(_tag) self._tags['xmp'][key] = tag return tag def __getitem__(self, key): """Return a metadata tag for a given key. Raise KeyError if the tag doesn't exist Args: key -- metadata key in the dotted form ``familyName.groupName.tagName`` where ``familyName`` may be one of ``exif``, ``iptc`` or ``xmp``. """ family = key.split('.')[0].lower() if family in ('exif', 'iptc', 'xmp'): return getattr(self, '_get_%s_tag' % family)(key) else: raise KeyError(key) def _set_exif_tag(self, key, tag_or_value): """Set an EXIF tag. If the tag already exists, its value is overwritten. Args: key -- the EXIF key tag_or_value -- an ExifTag instance or the value of the data """ if isinstance(tag_or_value, ExifTag): tag = tag_or_value else: # As a handy shortcut, accept direct value assignment. tag = ExifTag(key, tag_or_value) tag._set_owner(self) self._tags['exif'][tag.key] = tag if tag.key not in self.exif_keys: self._keys['exif'].append(tag.key) def _set_iptc_tag(self, key, tag_or_values): """Set an IPTC tag. If the tag already exists, its value is overwritten. Args: key -- the IPTC key tag_or_value -- an IptcTag instance or the value of the data """ if isinstance(tag_or_values, IptcTag): tag = tag_or_values else: # As a handy shortcut, accept direct value assignment. tag = IptcTag(key, tag_or_values) tag._set_owner(self) self._tags['iptc'][tag.key] = tag if tag.key not in self.iptc_keys: self._keys['iptc'].append(tag.key) def _set_xmp_tag(self, key, tag_or_value): """Set an XMP tag. If the tag already exists, its value is overwritten. Args: key -- the XMP key tag_or_value -- an XmpTag instance or the value of the data """ if isinstance(tag_or_value, XmpTag): tag = tag_or_value else: # As a handy shortcut, accept direct value assignment. tag = XmpTag(key, tag_or_value) tag._set_owner(self) self._tags['xmp'][tag.key] = tag if tag.key not in self.xmp_keys: self._keys['xmp'].append(tag.key) def __setitem__(self, key, tag_or_value): """Set a metadata tag for a given key. If the tag was previously set, it is overwritten. As a handy shortcut, a value may be passed instead of a fully formed tag. The corresponding tag object will be instantiated. Raise KeyError if the key is invalid Args: key -- metadata key in the dotted form ``familyName.groupName.tagName`` where ``familyName`` may be one of ``exif``, ``iptc`` or ``xmp``. tag_or_value -- an instance of the corresponding family of metadata tag or a value Type: pyexiv2.exif.ExifTag instance or pyexiv2.iptc.IptcTag instance or pyexiv2.xmp.XmpTag instance or any valid value type """ family = key.split('.')[0].lower() if family in ('exif', 'iptc', 'xmp'): return getattr(self, '_set_%s_tag' % family)(key, tag_or_value) else: raise KeyError(key) def _delete_exif_tag(self, key): """Delete an EXIF tag. Throw a KeyError if the tag doesn't exist. Args: key -- the EXIF key """ if key not in self.exif_keys: raise KeyError('Cannot delete an inexistent tag') self._image._deleteExifTag(key) try: del self._tags['exif'][key] except KeyError: # The tag was not cached. pass if self._keys['exif'] is not None: self._keys['exif'].remove(key) def _delete_iptc_tag(self, key): """Delete an IPTC tag. Throw a KeyError if the tag doesn't exist. Args: key -- the IPTC key """ if key not in self.iptc_keys: raise KeyError('Cannot delete an inexistent tag') self._image._deleteIptcTag(key) try: del self._tags['iptc'][key] except KeyError: # The tag was not cached. pass if self._keys['iptc'] is not None: self._keys['iptc'].remove(key) def _delete_xmp_tag(self, key): """Delete an XMP tag. Throw a KeyError if the tag doesn't exist. Args: key -- the XMP key """ if key not in self.xmp_keys: raise KeyError('Cannot delete an inexistent tag') self._image._deleteXmpTag(key) try: del self._tags['xmp'][key] except KeyError: # The tag was not cached. pass if self._keys['xmp'] is not None: self._keys['xmp'].remove(key) def __delitem__(self, key): """Delete a metadata tag for a given key. Raise KeyError if the tag with the given key doesn't exist Args: key -- the metadata key in the dotted form ``familyName.groupName.tagName`` where ``familyName`` may be one of ``exif``, ``iptc`` or ``xmp``. """ family = key.split('.')[0].lower() if family in ('exif', 'iptc', 'xmp'): return getattr(self, '_delete_%s_tag' % family)(key) else: raise KeyError(key) def __iter__(self): return chain(self.exif_keys, self.iptc_keys, self.xmp_keys) def __len__(self): return len([x for x in self]) def _get_comment(self): return self._image._getComment() def _set_comment(self, comment): if comment is not None: self._image._setComment(comment) else: self._del_comment() def _del_comment(self): self._image._clearComment() comment = property(fget=_get_comment, fset=_set_comment, fdel=_del_comment, doc='The image comment.') @property def previews(self): """List of the previews available in the image, sorted by increasing size. """ return [Preview(preview) for preview in self._image._previews()] def copy(self, other, exif=True, iptc=True, xmp=True, comment=True): """Copy the metadata to another image. The metadata in the destination is overridden. In particular, if the destination contains e.g. EXIF data and the source doesn't, it will be erased in the destination, unless explicitly omitted. Args: other -- the destination metadata to copy to (it must have been read beforehand) Type: pyexiv2.metadata.ImageMetadata instance exif -- whether to copy the EXIF metadata, default True iptc -- whether to copy the IPTC metadata, default True xmp -- whether to copy the XMP metadata, default True comment -- whether to copy the image comment, default True """ self._image._copyMetadata(other._image, exif, iptc, xmp) # Empty the cache where needed if exif: other._keys['exif'] = None other._tags['exif'] = {} if iptc: other._keys['iptc'] = None other._tags['iptc'] = {} if xmp: other._keys['xmp'] = None other._tags['xmp'] = {} if comment: other.comment = self.comment @property def buffer(self): """ The image buffer as a string. If metadata has been modified, the data won't be up-to-date until :meth:`.write` has been called. """ 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 def _get_iptc_charset(self): value = self._image._getIptcCharset() if value != '': return value.lower() else: return None def _set_iptc_charset(self, charset): if charset is None: self._del_iptc_charset() return try: name = codecs.lookup(charset).name except LookupError as error: raise ValueError(error) else: charsets = {'utf-8': '\x1b%G'} try: self['Iptc.Envelope.CharacterSet'] = (charsets[name],) except KeyError: raise ValueError('Unhandled charset: %s' % name) def _del_iptc_charset(self): try: del self['Iptc.Envelope.CharacterSet'] except KeyError: pass iptc_charset = property(fget=_get_iptc_charset, fset=_set_iptc_charset, fdel=_del_iptc_charset, doc='An optional character set the IPTC data'\ ' is encoded in.') # Some convenient functions ------------------------------------------- def get_iso(self): """Returns the ISO value as integer. """ try: return self["Exif.Photo.ISOSpeedRatings"].value except KeyError: return def get_shutter_speed(self, float_=False): """Returns the exposure time as rational or float. Args: float_ -- if False, default, the value is returned as rational otherwise as float """ try: speed = self['Exif.Photo.ExposureTime'].value except KeyError: return if float_: if speed.denominator: return speed.numerator / speed.denominator return float(numerator) return speed def get_focal_length(self): """Returns the focal length as float. """ try: focal = self['Exif.Photo.FocalLength'].value if focal.denominator: return round(focal.numerator / focal.denominator, 2) else: return float(focal.numerator) except KeyError: return def get_aperture(self): """Returns the fNumber as float. """ try: fnumber = self["Exif.Photo.FNumber"].value if fnumber.denominator: return round(fnumber.numerator / fnumber.denominator, 2) else: return float(fnumber.numerator) except KeyError: return def get_orientation(self): """Returns the orientation of the image as integer. If the tag is not set, the value 1 is returned. """ try: return self["Exif.Image.Orientation"].value except KeyError: return 1 def get_exposure_data(self, float_=False): """Returns the exposure parameters of the image. The values are returned as a dict which contains: "iso": the ISO value "speed": the exposure time "focal": the focal length "aperture": the fNumber "orientation": the orientation of the image When a tag is not set, the value will be None Args: float_ -- if False, default, the value of the exposure time is returned as rational otherwise as float """ data = {"iso": self.get_iso(), "speed": self.get_shutter_speed(float_), "focal": self.get_focal_length(), "aperture": self.get_aperture(), "orientation": self.get_orientation()} return data def get_rights_data(self): """Returns the author and copyright info. The values are returned as a dict which contains: "creator": the value of Xmp.dc.creator "artist": the value of Exif.Image.Artist "rights": the value of Xmp.dc.rights "copyright": the value of Exif.Image.Copyright "marked": the value of Xmp.xmpRights.Marked "usage": the value of Xmp.xmpRights.UsageTerms When a tag is not set, the value will be None """ tags = [('creator', 'Xmp.dc.creator'), ('artist', 'Exif.Image.Artist'), ('rights', 'Xmp.dc.rights'), ('copyright', 'Exif.Image.Copyright'), ('marked', 'Xmp.xmpRights.Marked'), ('usage', 'Xmp.xmpRights.UsageTerms')] rights = {} for tag in tags: try: rights[tag[0]] = self[tag[1]].value except KeyError: rights[tag[0]] = None return rights py3exiv2-0.7.2/src/pyexiv2/preview.py000066400000000000000000000045201364152456500174550ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ****************************************************************************** # # Copyright (C) 2010 Olivier Tilloy # Copyright (C) 2015-2020 Vincent Vande Vyvre # # This file is part of the py3exiv2 distribution. # # py3exiv2 is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # version 3 as published by the Free Software Foundation. # # py3exiv2 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 py3exiv2; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. # # Maintainer: Vincent Vande Vyvre # # ****************************************************************************** """ Provide the Preview class. """ import sys class Preview(object): """A preview image (properties and data buffer) embedded in image metadata. """ def __init__(self, preview): self.__preview = preview @property def mime_type(self): """The mime type of the preview image (e.g. ``image/jpeg``). """ return self.__preview.mime_type @property def extension(self): """The file extension of the preview image with a leading dot. """ return self.__preview.extension @property def size(self): """The size of the preview image in bytes. """ return self.__preview.size @property def dimensions(self): """A tuple containing the width and height of the preview image in pixels. """ return self.__preview.dimensions @property def data(self): """The preview image data buffer. """ return self.__preview.get_data() def write_to_file(self, path): """Write the preview image to a file on disk. The file extension will be automatically appended to the path. Args: path -- path to write the preview to (without an extension) """ return self.__preview.write_to_file(path) py3exiv2-0.7.2/src/pyexiv2/utils.py000066400000000000000000000465601364152456500171460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ****************************************************************************** # # Copyright (C) 2006-2012 Olivier Tilloy # Copyright (C) 2015-2020 Vincent Vande Vyvre # # This file is part of the py3exiv2 distribution. # # py3exiv2 is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # version 3 as published by the Free Software Foundation. # # py3exiv2 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 py3exiv2; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. # # Maintainer: Vincent Vande Vyvre # # ****************************************************************************** """ Utilitary classes and functions. """ import datetime import re from fractions import Fraction class FixedOffset(datetime.tzinfo): """Define a fixed positive or negative offset of a local time from UTC. """ def __init__(self, sign='+', hours=0, minutes=0): """Initialize an offset from a sign ('+' or '-') and an absolute value expressed in hours and minutes. No check on the validity of those values is performed, it is the responsibility of the caller to pass valid values. Args: sign -- the sign of the offset ('+' or '-') hours -- an absolute number of hours minutes -- an absolute number of minutes """ self.sign = sign self.hours = hours self.minutes = minutes def utcoffset(self, dt): """Return offset of local time from UTC, in minutes east of UTC. If local time is west of UTC, this value will be negative. Args: dt -- the local datetime.time instance Return: a whole number of minutes in the range -1439 to 1439 inclusive """ total = self.hours * 60 + self.minutes if self.sign == '-': total = -total return datetime.timedelta(minutes = total) def dst(self, dt): """Return the daylight saving time (DST) adjustment. In this implementation, it is always nil. Args: dt -- the local datetime.time instance Return: the DST adjustment (always nil) """ return datetime.timedelta(0) def tzname(self, dt): """Return a string representation of the offset in the format '±%H:%M'. If the offset is nil, the representation is, by convention, 'Z'. Args: dt -- the local datetime.time instance Return: a human-readable representation of the offset """ if self.hours == 0 and self.minutes == 0: return 'Z' else: return '%s%02d:%02d' % (self.sign, self.hours, self.minutes) def __equal__(self, other): """Test equality between this offset and another offset. Args: other -- another FixedOffset instance Return: True if the offset are equal, False otherwise """ return (self.sign == other.sign) and (self.hours == other.hours) and \ (self.minutes == other.minutes) def undefined_to_string(undefined): """Convert an undefined string into its corresponding sequence of bytes. The undefined string must contain the ascii codes of a sequence of bytes, separated by white spaces (e.g. "48 50 50 49" will be converted into "0221"). The Undefined type is part of the EXIF specification. Args: undefined -- an undefined string Return: the corresponding decoded string """ if not undefined: return '' return ''.join([chr(int(x)) for x in undefined.rstrip().split(' ')]) def string_to_undefined(sequence): """Convert a string into its undefined form. The undefined form contains a sequence of ascii codes separated by white spaces (e.g. "0221" will be converted into "48 50 50 49"). The Undefined type is part of the EXIF specification. Args: sequence -- a sequence of bytes Return: the corresponding undefined string """ return " ".join([str(ord(s)) for s in sequence]) def is_fraction(obj): """Test whether the object is a valid fraction. """ return isinstance(obj, Fraction) def match_string(string): """Match a string against the expected format for a :class:`Fraction` (``[-]numerator/denominator``) and return the numerator and denominator as a tuple. Args: string -- a string representation of a rational number Return: a tuple (numerator, denominator) Raise ValueError: if the format of the string is invalid """ format_re = re.compile(r'(?P-?\d+)/(?P\d+)') match = format_re.match(string) if match is None: raise ValueError('Invalid format for a rational: %s' % string) gd = match.groupdict() return (int(gd['numerator']), int(gd['denominator'])) def make_fraction(*args): """Make a fraction. Raise TypeError: if the arguments do not match the expected format for a fraction """ if len(args) == 1: numerator, denominator = match_string(args[0]) elif len(args) == 2: numerator = args[0] denominator = args[1] else: raise TypeError('Invalid format for a fraction: %s' % str(args)) if denominator == 0 and numerator == 0: # Null rationals are often stored as '0/0'. # We want to be fault-tolerant in this specific case # (see https://bugs.launchpad.net/pyexiv2/+bug/786253). denominator = 1 return Fraction(numerator, denominator) def fraction_to_string(fraction): """Return a string representation of a fraction. The returned string is always in the form '[numerator]/[denominator]'. Raise TypeError: if the argument is not a valid fraction """ if isinstance(fraction, Fraction): # fractions.Fraction.__str__ returns '0' for a null numerator. return '%s/%s' % (fraction.numerator, fraction.denominator) else: raise TypeError('Not a fraction') class ListenerInterface(object): """ Interface that an object that wants to listen to changes on another object should implement. """ def contents_changed(self): """ React on changes on the object observed. Override to implement specific behaviours. """ raise NotImplementedError() class NotifyingList(list): """A simplistic implementation of a notifying list. Any changes to the list are notified in a synchronous way to all previously registered listeners. A listener must implement the class ListenerInterface. """ # Useful documentation: # file:///usr/share/doc/python2.5/html/lib/typesseq-mutable.html # http://docs.python.org/reference/datamodel.html#additional-methods-for-emulation-of-sequence-types def __init__(self, items=[]): super(NotifyingList, self).__init__(items) self._listeners = set() def register_listener(self, listener): """Register a new listener to be notified of changes. Args: listener -- any ListenerInterface instance that listens for changes """ self._listeners.add(listener) def unregister_listener(self, listener): """Unregister a previously registered listener. Args: listener -- a previously registered listener Raise KeyError: if the listener was not previously registered """ self._listeners.remove(listener) def _notify_listeners(self, *args): for listener in self._listeners: listener.contents_changed(*args) def __setitem__(self, index, item): # FIXME: support slice arguments for extended slicing super(NotifyingList, self).__setitem__(index, item) self._notify_listeners() def __delitem__(self, index): # FIXME: support slice arguments for extended slicing super(NotifyingList, self).__delitem__(index) self._notify_listeners() def append(self, item): super(NotifyingList, self).append(item) self._notify_listeners() def extend(self, items): super(NotifyingList, self).extend(items) self._notify_listeners() def insert(self, index, item): super(NotifyingList, self).insert(index, item) self._notify_listeners() def pop(self, index=None): if index is None: item = super(NotifyingList, self).pop() else: item = super(NotifyingList, self).pop(index) self._notify_listeners() return item def remove(self, item): super(NotifyingList, self).remove(item) self._notify_listeners() def reverse(self): super(NotifyingList, self).reverse() self._notify_listeners() def sort(self, key=None, reverse=False): super(NotifyingList, self).sort(key=key, reverse=reverse) self._notify_listeners() def __iadd__(self, other): self = super(NotifyingList, self).__iadd__(other) self._notify_listeners() return self def __imul__(self, coefficient): self = super(NotifyingList, self).__imul__(coefficient) self._notify_listeners() return self def setslice__(self, i, j, items): # __setslice__ is deprecated but needs to be overridden for completeness super(NotifyingList, self).__setslice__(i, j, items) self._notify_listeners() def delslice__(self, i, j): # __delslice__ is deprecated but needs to be overridden for completeness deleted = self[i:j] super(NotifyingList, self).__delslice__(i, j) if deleted: self._notify_listeners() class GPSCoordinate(object): """A class representing GPS coordinates (e.g. a latitude or a longitude). Its attributes (degrees, minutes, seconds, direction) are read-only properties. """ _format_re = re.compile(r'(?P-?\d+),' '(?P\d+)(,(?P\d+)|\.(?P\d+))' '(?P[NSEW])') def __init__(self, degrees, minutes, seconds, direction): """Instanciate a GPSCoordinate object. Args: degrees -- int(degrees) minutes -- int(minutes) seconds -- int(seconds) direction -- str('N', 'S', 'E' or 'W') Raise ValueError: if any of the parameter is not in the expected range of values """ if direction not in ('N', 'S', 'E', 'W'): raise ValueError('Invalid direction: %s' % direction) self._direction = direction if (direction in ('N', 'S') and (degrees < 0 or degrees > 90)) or \ (direction in ('E', 'W') and (degrees < 0 or degrees > 180)): raise ValueError('Invalid value for degrees: %d' % degrees) self._degrees = degrees if minutes < 0 or minutes > 60: raise ValueError('Invalid value for minutes: %d' % minutes) self._minutes = minutes if seconds < 0 or seconds > 60: raise ValueError('Invalid value for seconds: %d' % seconds) self._seconds = seconds @property def degrees(self): """The degrees component of the coordinate. """ return self._degrees @property def minutes(self): """The minutes component of the coordinate. """ return self._minutes @property def seconds(self): """The seconds component of the coordinate. """ return self._seconds @property def direction(self): """The direction component of the coordinate. """ return self._direction @staticmethod def from_string(string): """Instantiate a :class:`GPSCoordinate` from a string formatted as ``DDD,MM,SSk`` or ``DDD,MM.mmk`` where ``DDD`` is a number of degrees, ``MM`` is a number of minutes, ``SS`` is a number of seconds, ``mm`` is a fraction of minutes, and ``k`` is a single character N, S, E, W indicating a direction (north, south, east, west). Args: string -- a string representation of a GPS coordinate Return: the GPSCoordinate parsed Raise ValueError: if the format of the string is invalid """ match = GPSCoordinate._format_re.match(string) if match is None: raise ValueError('Invalid format for a GPS coordinate: %s' % string) gd = match.groupdict() fraction = gd['fraction'] if fraction is not None: seconds = int(round(int(fraction[:2]) * 0.6)) else: seconds = int(gd['seconds']) return GPSCoordinate(int(gd['degrees']), int(gd['minutes']), seconds, gd['direction']) def __eq__(self, other): """Compare two GPS coordinates for equality. Two coordinates are equal if and only if all their components are equal. Args: other -- the GPSCoordinate instance to compare to self for equality Return: True if equal, False otherwise """ return (self._degrees == other._degrees) and \ (self._minutes == other._minutes) and \ (self._seconds == other._seconds) and \ (self._direction == other._direction) def __str__(self): """Return a string representation of the GPS coordinate conforming to the XMP specification """ return '%d,%d,%d%s' % (self._degrees, self._minutes, self._seconds, self._direction) class DateTimeFormatter(object): """Convenience object that exposes static methods to convert a date, time or datetime object to a string representation suitable for various metadata standards. This is needed because python’s `strftime() `_ doesn’t work for years before 1900. This class mostly exists for internal usage only. Clients should never need to use it. """ @staticmethod def timedelta_to_offset(t): """Convert a time delta to a string.. Args: t -- a datetime.timedelta instance Return: a string representation of the time delta in the form `±%H:%M` """ # timedelta.total_seconds() is only available starting with Python 3.2 seconds = t.total_seconds() hours = int(seconds / 3600) minutes = abs(int((seconds - hours * 3600) / 60)) return '%+03d:%02d' % (hours, minutes) @staticmethod def exif(d): """Convert a date/time object to a string representation conforming to libexiv2’s internal representation for the EXIF standard. Args: d -- a datetime.datetime or datetime.date instance Return: a string representation conforming to the EXIF standard Raise TypeError: if the parameter is not a datetime or a date object """ if isinstance(d, datetime.datetime): return '%04d:%02d:%02d %02d:%02d:%02d' % \ (d.year, d.month, d.day, d.hour, d.minute, d.second) elif isinstance(d, datetime.date): return '%04d:%02d:%02d' % (d.year, d.month, d.day) else: raise TypeError('expecting an object of type ' 'datetime.datetime or datetime.date') @staticmethod def iptc_date(d): """Convert a date object to a string representation conforming to libexiv2’s internal representation for the IPTC standard. Args: d -- a datetime.datetime or datetime.date instance Return: a string representation conforming to the IPTC standard Raise TypeError: if the parameter is not a datetime or a date object """ if isinstance(d, (datetime.date, datetime.datetime)): # ISO 8601 date format. # According to the IPTC specification, the format for a string # field representing a date is '%Y%m%d'. However, the string # expected by exiv2's DateValue::read(string) should be # formatted using pattern '%Y-%m-%d'. return '%04d-%02d-%02d' % (d.year, d.month, d.day) else: raise TypeError('expecting an object of type ' 'datetime.datetime or datetime.date') @staticmethod def iptc_time(d): """Convert a time object to a string representation conforming to libexiv2’s internal representation for the IPTC standard. Args: d -- a datetime.datetime or time instance Return: a string representation conforming to the IPTC standard Raise TypeError: if the parameter is not a datetime or a time object """ if isinstance(d, (datetime.time, datetime.datetime)): # According to the IPTC specification, the format for a string # field representing a time is '%H%M%S±%H%M'. However, the # string expected by exiv2's TimeValue::read(string) should be # formatted using pattern '%H:%M:%S±%H:%M'. r = '%02d:%02d:%02d' % (d.hour, d.minute, d.second) if d.tzinfo is not None: t = d.utcoffset() if t is not None: r += DateTimeFormatter.timedelta_to_offset(t) else: r += '+00:00' return r else: raise TypeError('expecting an object of type ' 'datetime.datetime or datetime.time') @staticmethod def xmp(d): """Convert a date/time object to a string representation conforming to libexiv2’s internal representation for the XMP standard. Args: d -- a datetime.datetime or datetime.date instance Return: a string representation conforming to the XMP standard Raise TypeError: if the parameter is not a datetime or a date object """ if isinstance(d, datetime.datetime): t = d.utcoffset() if d.tzinfo is None or t is None or t == datetime.timedelta(0): tz = 'Z' else: tz = DateTimeFormatter.timedelta_to_offset(t) if d.hour == 0 and d.minute == 0 and \ d.second == 0 and d.microsecond == 0 and \ (d.tzinfo is None or d.utcoffset() == datetime.timedelta(0)): return '%04d-%02d-%02d' % (d.year, d.month, d.day) elif d.second == 0 and d.microsecond == 0: return '%04d-%02d-%02dT%02d:%02d%s' % \ (d.year, d.month, d.day, d.hour, d.minute, tz) elif d.microsecond == 0: return '%04d-%02d-%02dT%02d:%02d:%02d%s' % \ (d.year, d.month, d.day, d.hour, d.minute, d.second, tz) else: r = '%04d-%02d-%02dT%02d:%02d:%02d.' % \ (d.year, d.month, d.day, d.hour, d.minute, d.second) r += str(int(d.microsecond) / 1E6)[2:] r += tz return r elif isinstance(d, datetime.date): return '%04d-%02d-%02d' % (d.year, d.month, d.day) else: raise TypeError('expecting an object of type ' 'datetime.datetime or datetime.date') py3exiv2-0.7.2/src/pyexiv2/xmp.py000066400000000000000000000461361364152456500166110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ****************************************************************************** # # Copyright (C) 2006-2011 Olivier Tilloy # Copyright (C) 2015-2020 Vincent Vande Vyvre # # This file is part of the py3exiv2 distribution. # # py3exiv2 is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # version 3 as published by the Free Software Foundation. # # py3exiv2 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 py3exiv2; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA. # # Maintainer: Vincent Vande Vyvre # # ****************************************************************************** """ XMP specific code. """ import libexiv2python from pyexiv2.utils import (FixedOffset, is_fraction, make_fraction, GPSCoordinate, DateTimeFormatter) import datetime import re class XmpValueError(ValueError): """ Exception raised when failing to parse the *value* of an XMP tag. :attribute value: the value that fails to be parsed :type value: string :attribute type: the XMP type of the tag :type type: string """ def __init__(self, value, type_): self.value = value self.type = type_ def __str__(self): return 'Invalid value for XMP type [%s]: [%s]' % \ (self.type, self.value) class XmpTag(object): """Define an XMP tag. Here is a correspondance table between the XMP types and the possible python types the value of a tag may take: - alt, bag, seq: list of the contained simple type - lang alt: dict of (language-code: value) - Boolean: boolean - Colorant: *[not implemented yet]* - Date: :class:`datetime.date`, :class:`datetime.datetime` - Dimensions: *[not implemented yet]* - Font: *[not implemented yet]* - GPSCoordinate: :class:`pyexiv2.utils.GPSCoordinate` - Integer: int - Locale: *[not implemented yet]* - MIMEType: 2-tuple of strings - Rational: :class:`fractions.Fraction` - Real: *[not implemented yet]* - AgentName, ProperName, Text: unicode string - Thumbnail: *[not implemented yet]* - URI, URL: string - XPath: *[not implemented yet]* """ # strptime is not flexible enough to handle all valid Date formats, we use a # custom regular expression _time_zone_re = r'$|Z|((?P\+|-)(?P\d{2}):(?P\d{2}))' _time_re = r'(?P\d{2})(:(?P\d{2})(:(?P\d{2})(.(?P\d+))?)?(?P%s))?' % _time_zone_re _date_re = re.compile(r'(?P\d{4})(-(?P\d{2})(-(?P\d{2})(T(?P