Source code for madam.exif
import datetime
import io
import shutil
import tempfile
from fractions import Fraction
from typing import Any, Callable, Dict, IO, Iterable, Mapping, Optional, Tuple
import piexif
from bidict import bidict
from madam.core import MetadataProcessor, UnsupportedFormatError
from madam.mime import MimeType
def _convert_sequence(dec_enc: Tuple[Callable, Callable]) -> Tuple[Callable, Callable]:
return lambda exif_values: tuple(map(dec_enc[0], exif_values)), \
lambda values: list(map(dec_enc[1], values))
def _convert_first(dec_enc: Tuple[Callable, Callable]) -> Tuple[Callable, Callable]:
return lambda exif_values: dec_enc[0](exif_values[0]), \
lambda value: [dec_enc[1](value)]
def _convert_mapping(mapping: Mapping) -> Tuple[Callable, Callable]:
bidi = bidict(mapping)
return lambda exif_value: bidi[exif_value], \
lambda value: bidi.inv[value]
[docs]class ExifMetadataProcessor(MetadataProcessor):
"""
Represents a metadata processor for Exif metadata.
"""
supported_mime_types = {
MimeType('image/jpeg'),
MimeType('image/webp'),
}
metadata_to_exif = bidict({
'aperture': ('Exif', piexif.ExifIFD.ApertureValue),
'artist': ('0th', piexif.ImageIFD.Artist),
'brightness': ('Exif', piexif.ExifIFD.BrightnessValue),
'camera.manufacturer': ('0th', piexif.ImageIFD.Make),
'camera.model': ('0th', piexif.ImageIFD.Model),
'description': ('0th', piexif.ImageIFD.ImageDescription),
'exposure_time': ('Exif', piexif.ExifIFD.ExposureTime),
'firmware': ('0th', piexif.ImageIFD.Software),
'fnumber': ('Exif', piexif.ExifIFD.FNumber),
'focal_length': ('Exif', piexif.ExifIFD.FocalLength),
'focal_length_35mm': ('Exif', piexif.ExifIFD.FocalLengthIn35mmFilm),
'gps.altitude': ('GPS', piexif.GPSIFD.GPSAltitude),
'gps.altitude_ref': ('GPS', piexif.GPSIFD.GPSAltitudeRef),
'gps.latitude': ('GPS', piexif.GPSIFD.GPSLatitude),
'gps.latitude_ref': ('GPS', piexif.GPSIFD.GPSLatitudeRef),
'gps.longitude': ('GPS', piexif.GPSIFD.GPSLongitude),
'gps.longitude_ref': ('GPS', piexif.GPSIFD.GPSLongitudeRef),
'gps.map_datum': ('GPS', piexif.GPSIFD.GPSMapDatum),
'gps.speed': ('GPS', piexif.GPSIFD.GPSSpeed),
'gps.speed_ref': ('GPS', piexif.GPSIFD.GPSSpeedRef),
'gps.date_stamp': ('GPS', piexif.GPSIFD.GPSDateStamp),
'gps.time_stamp': ('GPS', piexif.GPSIFD.GPSTimeStamp),
'lens.manufacturer': ('Exif', piexif.ExifIFD.LensMake),
'lens.model': ('Exif', piexif.ExifIFD.LensModel),
'orientation': ('0th', piexif.ImageIFD.Orientation),
'shutter_speed': ('Exif', piexif.ExifIFD.ShutterSpeedValue),
'software': ('0th', piexif.ImageIFD.ProcessingSoftware),
})
__STRING = lambda exif_val: exif_val.decode('utf-8'), lambda value: value.encode('utf-8')
__INT = int, int
__RATIONAL = lambda exif_val: float(Fraction(*exif_val)), \
lambda value: (Fraction(value).limit_denominator().numerator,
Fraction(value).limit_denominator().denominator)
__DATE = lambda exif_val: datetime.datetime.strptime(exif_val.decode('utf-8'), '%Y:%m:%d').date(), \
lambda value: value.strftime('%Y:%m:%d')
__TIME = lambda exif_val: datetime.time(*map(lambda v: round(float(Fraction(*v))), exif_val)), \
lambda value: ((value.hour, 1), (value.minute, 1), (value.second, 1))
converters: Dict[str, Tuple[Callable, Callable]] = {
'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({b'N': 'north', b'S': 'south'}),
'gps.longitude': _convert_sequence(__RATIONAL),
'gps.longitude_ref': _convert_mapping({b'E': 'east', b'W': 'west'}),
'gps.map_datum': __STRING,
'gps.speed': __RATIONAL,
'gps.speed_ref': _convert_mapping({b'K': 'km/h', b'M': 'mph', b'N': 'kn'}),
'gps.date_stamp': __DATE,
'gps.time_stamp': __TIME,
'lens.manufacturer': __STRING,
'lens.model': __STRING,
'orientation': __INT,
'shutter_speed': __RATIONAL,
'software': __STRING,
}
[docs] def __init__(self, config: Optional[Mapping[str, Any]] = None) -> None:
"""
Initializes a new `ExifMetadataProcessor`.
:param config: Mapping with settings
"""
super().__init__(config)
@property
def formats(self) -> Iterable[str]:
return {'exif'}
[docs] def read(self, file: IO) -> Mapping[str, Mapping]:
with tempfile.NamedTemporaryFile(mode='wb') as tmp:
tmp.write(file.read())
tmp.flush()
try:
metadata = piexif.load(tmp.name)
except (piexif.InvalidImageDataError, ValueError):
raise UnsupportedFormatError('Unsupported file format.')
metadata_by_format = {}
for metadata_format in self.formats:
format_metadata = {}
for ifd_key, ifd_values in metadata.items():
if not isinstance(ifd_values, dict):
continue
for exif_key, exif_value in ifd_values.items():
madam_key = ExifMetadataProcessor.metadata_to_exif.inv.get((ifd_key, exif_key))
if madam_key is None:
continue
convert_to_madam, _ = ExifMetadataProcessor.converters[madam_key]
format_metadata[madam_key] = convert_to_madam(exif_value)
if format_metadata:
metadata_by_format[metadata_format] = format_metadata
return metadata_by_format
[docs] def strip(self, file: IO) -> IO:
result = io.BytesIO()
with tempfile.NamedTemporaryFile(mode='w+b') as tmp:
tmp.write(file.read())
tmp.flush()
try:
metadata = piexif.load(tmp.name)
if any(metadata.values()):
open(tmp.name, 'rb').read()
piexif.remove(tmp.name)
except (piexif.InvalidImageDataError, ValueError, UnboundLocalError):
raise UnsupportedFormatError('Unsupported file format.')
tmp.seek(0)
shutil.copyfileobj(tmp, result)
result.seek(0)
return result
[docs] def combine(self, essence: IO, metadata_by_format: Mapping[str, Mapping]) -> IO:
result = io.BytesIO()
with tempfile.NamedTemporaryFile(mode='w+b') as tmp:
tmp.write(essence.read())
tmp.flush()
try:
exif_metadata = piexif.load(tmp.name)
except (piexif.InvalidImageDataError, ValueError):
raise UnsupportedFormatError('Unsupported essence format.')
for metadata_format, metadata in metadata_by_format.items():
if metadata_format not in self.formats:
raise UnsupportedFormatError(f'Metadata format {metadata_format!r} is not supported.')
for madam_key, madam_value in metadata.items():
if madam_key not in ExifMetadataProcessor.metadata_to_exif:
continue
ifd_key, exif_key = ExifMetadataProcessor.metadata_to_exif[madam_key]
if ifd_key not in exif_metadata:
exif_metadata[ifd_key] = {}
_, convert_to_exif = ExifMetadataProcessor.converters[madam_key]
exif_metadata[ifd_key][exif_key] = convert_to_exif(madam_value)
try:
piexif.insert(piexif.dump(exif_metadata), tmp.name)
except (piexif.InvalidImageDataError, ValueError):
raise UnsupportedFormatError(f'Could not write metadata: {metadata_by_format!r}')
tmp.seek(0)
shutil.copyfileobj(tmp, result)
result.seek(0)
return result