Source code for madam.exiv2

import datetime
import io
import shutil
import tempfile
from fractions import Fraction

import pyexiv2
from bidict import bidict

from madam.core import MetadataProcessor, UnsupportedFormatError


def _convert_sequence(dec_enc):
    return lambda exiv2_values: tuple(map(dec_enc[0], exiv2_values)), \
           lambda values: list(map(dec_enc[1], values))


def _convert_first(dec_enc):
    return lambda exiv2_values: dec_enc[0](exiv2_values[0]), \
           lambda value: [dec_enc[1](value)]


def _convert_mapping(mapping):
    bidi = bidict(mapping)
    return lambda exiv2_value: bidi[exiv2_value], \
           lambda value: bidi.inv[value]


[docs]class Exiv2MetadataProcessor(MetadataProcessor): """ Represents a metadata processor using the exiv2 library. """ metadata_to_exiv2 = bidict({ # Exif 'aperture': 'Exif.Photo.ApertureValue', 'artist': 'Exif.Image.Artist', 'brightness': 'Exif.Photo.BrightnessValue', 'camera.manufacturer': 'Exif.Image.Make', 'camera.model': 'Exif.Image.Model', 'description': 'Exif.Image.ImageDescription', 'exposure_time': 'Exif.Photo.ExposureTime', 'firmware': 'Exif.Image.Software', 'fnumber': 'Exif.Photo.FNumber', 'focal_length': 'Exif.Photo.FocalLength', 'focal_length_35mm': 'Exif.Photo.FocalLengthIn35mmFilm', 'gps.altitude': 'Exif.GPSInfo.GPSAltitude', 'gps.altitude_ref': 'Exif.GPSInfo.GPSAltitudeRef', 'gps.latitude': 'Exif.GPSInfo.GPSLatitude', 'gps.latitude_ref': 'Exif.GPSInfo.GPSLatitudeRef', 'gps.longitude': 'Exif.GPSInfo.GPSLongitude', 'gps.longitude_ref': 'Exif.GPSInfo.GPSLongitudeRef', 'gps.map_datum': 'Exif.GPSInfo.GPSMapDatum', 'gps.speed': 'Exif.GPSInfo.GPSSpeed', 'gps.speed_ref': 'Exif.GPSInfo.GPSSpeedRef', 'gps.date_stamp': 'Exif.GPSInfo.GPSDateStamp', 'gps.time_stamp': 'Exif.GPSInfo.GPSTimeStamp', 'lens.manufacturer': 'Exif.Photo.LensMake', 'lens.model': 'Exif.Photo.LensModel', 'shutter_speed': 'Exif.Photo.ShutterSpeedValue', 'software': 'Exif.Image.ProcessingSoftware', # IPTC 'bylines': 'Iptc.Application2.Byline', 'byline_titles': 'Iptc.Application2.BylineTitle', 'caption': 'Iptc.Application2.Caption', 'contacts': 'Iptc.Application2.Contact', 'copyright': 'Iptc.Application2.Copyright', 'creation_date': 'Iptc.Application2.DateCreated', 'creation_time': 'Iptc.Application2.TimeCreated', 'credit': 'Iptc.Application2.Credit', 'expiration_date': 'Iptc.Application2.ExpirationDate', 'expiration_time': 'Iptc.Application2.ExpirationTime', 'headline': 'Iptc.Application2.Headline', 'image_orientation': 'Iptc.Application2.ImageOrientation', 'keywords': 'Iptc.Application2.Keywords', 'language': 'Iptc.Application2.Language', 'release_date': 'Iptc.Application2.ReleaseDate', 'release_time': 'Iptc.Application2.ReleaseTime', 'source': 'Iptc.Application2.Source', 'subjects': 'Iptc.Application2.Subject', }) __STRING = str, str __INT = int, int __RATIONAL = float, lambda value: Fraction(value).limit_denominator() __DATE = lambda exiv2_value: exiv2_value, lambda value: value __TIME = lambda exiv2_value: exiv2_value.replace(tzinfo=None), lambda value: value converters = { # Exif 'aperture': __RATIONAL, 'artist': __STRING, 'brightness': __RATIONAL, 'camera.manufacturer': __STRING, 'camera.model': __STRING, 'description': __STRING, 'exposure_time': __RATIONAL, 'firmware': __STRING, 'fnumber': __RATIONAL, 'focal_length': __RATIONAL, 'focal_length_35mm': __INT, 'gps.altitude': __RATIONAL, 'gps.altitude_ref': _convert_mapping({'0': 'm_above_sea_level', '1': 'm_below_sea_level'}), 'gps.latitude': _convert_sequence(__RATIONAL), 'gps.latitude_ref': _convert_mapping({'N': 'north', 'S': 'south'}), 'gps.longitude': _convert_sequence(__RATIONAL), 'gps.longitude_ref': _convert_mapping({'E': 'east', 'W': 'west'}), 'gps.map_datum': __STRING, 'gps.speed': __RATIONAL, 'gps.speed_ref': _convert_mapping({'K': 'km/h', 'M': 'mph', 'N': 'kn'}), 'gps.date_stamp': __DATE, 'gps.time_stamp': (lambda exiv2_val: datetime.time(*map(round, exiv2_val)), lambda val: [Fraction(val.hour), Fraction(val.minute), Fraction(val.second)]), 'lens.manufacturer': __STRING, 'lens.model': __STRING, 'shutter_speed': __RATIONAL, 'software': __STRING, # IPTC 'bylines': _convert_sequence(__STRING), 'byline_titles': _convert_sequence(__STRING), 'caption': _convert_first(__STRING), 'contacts': _convert_sequence(__STRING), 'copyright': _convert_first(__STRING), 'creation_date': _convert_first(__DATE), 'creation_time': _convert_first(__TIME), 'credit': _convert_first(__STRING), 'expiration_date': _convert_first(__DATE), 'expiration_time': _convert_first(__TIME), 'headline': _convert_first(__STRING), 'image_orientation': _convert_first(__STRING), 'keywords': _convert_sequence(__STRING), 'language': _convert_first(__STRING), 'release_date': _convert_first(__DATE), 'release_time': _convert_first(__TIME), 'source': _convert_first(__STRING), 'subjects': _convert_sequence(__STRING), } @property def formats(self): return 'exif', 'iptc' def read(self, file): with tempfile.NamedTemporaryFile() as tmp: tmp.write(file.read()) tmp.flush() metadata = pyexiv2.ImageMetadata(tmp.name) try: metadata.read() except OSError: raise UnsupportedFormatError('Unknown file format.') metadata_by_format = {} for metadata_format in self.formats: format_metadata = {} for exiv2_key in getattr(metadata, metadata_format + '_keys'): madam_key = Exiv2MetadataProcessor.metadata_to_exiv2.inv.get(exiv2_key) if madam_key is None: continue exiv2_value = metadata[exiv2_key].value convert_to_madam, _ = Exiv2MetadataProcessor.converters[madam_key] format_metadata[madam_key] = convert_to_madam(exiv2_value) if format_metadata: metadata_by_format[metadata_format] = format_metadata return metadata_by_format def strip(self, file): result = io.BytesIO() with tempfile.NamedTemporaryFile() as tmp: tmp.write(file.read()) tmp.flush() metadata = pyexiv2.ImageMetadata(tmp.name) try: metadata.read() except OSError: raise UnsupportedFormatError('Unknown file format.') try: metadata.clear() metadata.write() except OSError: raise UnsupportedFormatError('Unknown file format.') tmp.seek(0) shutil.copyfileobj(tmp, result) result.seek(0) return result def combine(self, essence, metadata_by_format): result = io.BytesIO() with tempfile.NamedTemporaryFile() as tmp: tmp.write(essence.read()) tmp.flush() exiv2_metadata = pyexiv2.ImageMetadata(tmp.name) try: exiv2_metadata.read() except OSError: raise UnsupportedFormatError('Unknown essence format.') for metadata_format, metadata in metadata_by_format.items(): if metadata_format not in self.formats: raise UnsupportedFormatError('Metadata format %r is not supported.' % metadata_format) for madam_key, madam_value in metadata.items(): exiv2_key = Exiv2MetadataProcessor.metadata_to_exiv2.get(madam_key) if exiv2_key is None: continue _, convert_to_exiv2 = Exiv2MetadataProcessor.converters[madam_key] exiv2_metadata[exiv2_key] = convert_to_exiv2(madam_value) try: exiv2_metadata.write() tmp.flush() tmp.seek(0) except OSError: raise UnsupportedFormatError('Could not write metadata: %r' % metadata_by_format) shutil.copyfileobj(tmp, result) result.seek(0) return result