SickGear/lib/hachoir/parser/image/exif.py
2023-02-09 13:41:15 +00:00

469 lines
19 KiB
Python

"""
EXIF metadata parser; also parses TIFF file headers.
Author: Victor Stinner, Robert Xiao
References:
- Exif 2.2 Specification (JEITA CP-3451)
http://www.exif.org/Exif2-2.PDF
- TIFF 6.0 Specification
http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf
"""
from hachoir.field import (FieldSet, SeekableFieldSet, ParserError,
UInt8, UInt16, UInt32,
Int8, Int16, Int32,
Float32, Float64,
Enum, String, Bytes, SubFile,
NullBits, NullBytes)
from hachoir.core.endian import LITTLE_ENDIAN, BIG_ENDIAN
from hachoir.core.tools import createDict
def rationalFactory(class_name, size, field_class):
class Rational(FieldSet):
static_size = size
def createFields(self):
yield field_class(self, "numerator")
yield field_class(self, "denominator")
def createValue(self):
return float(self["numerator"].value) / self["denominator"].value
cls = Rational
cls.__name__ = class_name
return cls
RationalInt32 = rationalFactory("RationalInt32", 64, Int32)
RationalUInt32 = rationalFactory("RationalUInt32", 64, UInt32)
class ASCIIString(String):
def __init__(self, parent, name, nbytes, description=None, strip=' \0', charset='ISO-8859-1', *args, **kwargs):
String.__init__(self, parent, name, nbytes, description,
strip, charset, *args, **kwargs)
class IFDTag(UInt16):
def getTag(self):
return self.parent.TAG_INFO.get(self.value, (hex(self.value), hex(self.value)))
def createDisplay(self):
return self.getTag()[0]
class ValueArray(FieldSet):
def __init__(self, parent, name, type, count, description=None):
FieldSet.__init__(self, parent, name, size=type.static_size * count, description=description)
self.array_type = type
self.array_count = count
def createFields(self):
for i in range(self.array_count):
yield self.array_type(self, "data[]")
def createValue(self):
return [f.value for f in self]
def createDisplay(self):
if self.array_count > 16:
return '<Array of %d %s>' % (self.array_count, self.array_type.__name__)
else:
return '[' + ', '.join(f.display for f in self) + ']'
class BasicIFDEntry(FieldSet):
TYPE_BYTE = 0
TYPE_UNDEFINED = 7
TYPE_RATIONAL = 5
TYPE_SIGNED_RATIONAL = 10
TYPE_INFO = {
1: (UInt8, "BYTE (8 bits)"),
2: (ASCIIString, "ASCII (8 bits)"),
3: (UInt16, "SHORT (16 bits)"),
4: (UInt32, "LONG (32 bits)"),
5: (RationalUInt32, "RATIONAL (2x LONG, 64 bits)"),
6: (Int8, "SBYTE (8 bits)"),
7: (Bytes, "UNDEFINED (8 bits)"),
8: (Int16, "SSHORT (16 bits)"),
9: (Int32, "SLONG (32 bits)"),
10: (RationalInt32, "SRATIONAL (2x SLONG, 64 bits)"),
11: (Float32, "FLOAT (32 bits)"),
12: (Float64, "DOUBLE (64 bits)"),
}
ENTRY_FORMAT = createDict(TYPE_INFO, 0)
TYPE_NAME = createDict(TYPE_INFO, 1)
TAG_INFO = {}
def createFields(self):
yield IFDTag(self, "tag", "Tag")
yield Enum(UInt16(self, "type", "Type"), self.TYPE_NAME)
self.value_cls = self.ENTRY_FORMAT.get(self['type'].value, Bytes)
if issubclass(self.value_cls, Bytes):
self.value_size = 8
else:
self.value_size = self.value_cls.static_size
yield UInt32(self, "count", "Count")
count = self['count'].value
totalsize = self.value_size * count
if count == 0:
yield NullBytes(self, "padding", 4)
elif totalsize <= 32:
name = "value"
if issubclass(self.value_cls, Bytes):
yield self.value_cls(self, name, count)
elif count == 1:
yield self.value_cls(self, name)
else:
yield ValueArray(self, name, self.value_cls, count)
if totalsize < 32:
yield NullBits(self, "padding", 32 - totalsize)
else:
yield UInt32(self, "offset", "Value offset")
def createValue(self):
if "value" in self:
return self['value'].value
return None
def createDescription(self):
return "Entry: " + self["tag"].getTag()[1]
class IFDEntry(BasicIFDEntry):
EXIF_IFD_POINTER = 0x8769
GPS_IFD_POINTER = 0x8825
INTEROP_IFD_POINTER = 0xA005
SUBIFD_POINTERS = 0x014A
TAG_INFO = {
# image data structure
0x0100: ("ImageWidth", "Image width"),
0x0101: ("ImageLength", "Image height"),
0x0102: ("BitsPerSample", "Number of bits per component"),
0x0103: ("Compression", "Compression scheme"),
0x0106: ("PhotometricInterpretation", "Pixel composition"),
0x0112: ("Orientation", "Orientation of image"),
0x0115: ("SamplesPerPixel", "Number of components"),
0x011C: ("PlanarConfiguration", "Image data arrangement"),
0x0212: ("YCbCrSubSampling", "Subsampling ratio of Y to C"),
0x0213: ("YCbCrPositioning", "Y and C positioning"),
0x011A: ("XResolution", "Image resolution in width direction"),
0x011B: ("YResolution", "Image resolution in height direction"),
0x0128: ("ResolutionUnit", "Unit of X and Y resolution"),
# recording offset
0x0111: ("StripOffsets", "Image data location"),
0x0116: ("RowsPerStrip", "Number of rows per strip"),
0x0117: ("StripByteCounts", "Bytes per compressed strip"),
0x0201: ("JPEGInterchangeFormat", "Offset to JPEG SOI"),
0x0202: ("JPEGInterchangeFormatLength", "Bytes of JPEG data"),
# image data characteristics
0x012D: ("TransferFunction", "Transfer function"),
0x013E: ("WhitePoint", "White point chromaticity"),
0x013F: ("PrimaryChromaticities", "Chromaticities of primaries"),
0x0211: ("YCbCrCoefficients", "Color space transformation matrix coefficients"),
0x0214: ("ReferenceBlackWhite", "Pair of black and white reference values"),
# other tags
0x0132: ("DateTime", "File change date and time"),
0x010E: ("ImageDescription", "Image title"),
0x010F: ("Make", "Image input equipment manufacturer"),
0x0110: ("Model", "Image input equipment model"),
0x0131: ("Software", "Software used"),
0x013B: ("Artist", "Person who created the image"),
0x8298: ("Copyright", "Copyright holder"),
0x02bc: ("XMPPacket", "XMP Packet"),
# TIFF-specific tags
0x00FE: ("NewSubfileType", "NewSubfileType"),
0x00FF: ("SubfileType", "SubfileType"),
0x0107: ("Threshholding", "Threshholding"),
0x0108: ("CellWidth", "CellWidth"),
0x0109: ("CellLength", "CellLength"),
0x010A: ("FillOrder", "FillOrder"),
0x010D: ("DocumentName", "DocumentName"),
0x0118: ("MinSampleValue", "MinSampleValue"),
0x0119: ("MaxSampleValue", "MaxSampleValue"),
0x011D: ("PageName", "PageName"),
0x011E: ("XPosition", "XPosition"),
0x011F: ("YPosition", "YPosition"),
0x0120: ("FreeOffsets", "FreeOffsets"),
0x0121: ("FreeByteCounts", "FreeByteCounts"),
0x0122: ("GrayResponseUnit", "GrayResponseUnit"),
0x0123: ("GrayResponseCurve", "GrayResponseCurve"),
0x0124: ("T4Options", "T4Options"),
0x0125: ("T6Options", "T6Options"),
0x0129: ("PageNumber", "PageNumber"),
0x013C: ("HostComputer", "HostComputer"),
0x013D: ("Predictor", "Predictor"),
0x0140: ("ColorMap", "ColorMap"),
0x0141: ("HalftoneHints", "HalftoneHints"),
0x0142: ("TileWidth", "TileWidth"),
0x0143: ("TileLength", "TileLength"),
0x0144: ("TileOffsets", "TileOffsets"),
0x0145: ("TileByteCounts", "TileByteCounts"),
SUBIFD_POINTERS: ("SubIFDs", "SubIFDs"),
0x014C: ("InkSet", "InkSet"),
0x014D: ("InkNames", "InkNames"),
0x014E: ("NumberOfInks", "NumberOfInks"),
0x0150: ("DotRange", "DotRange"),
0x0151: ("TargetPrinter", "TargetPrinter"),
0x0152: ("ExtraSamples", "ExtraSamples"),
0x0153: ("SampleFormat", "SampleFormat"),
0x0154: ("SMinSampleValue", "SMinSampleValue"),
0x0155: ("SMaxSampleValue", "SMaxSampleValue"),
0x0156: ("TransferRange", "TransferRange"),
0x0200: ("JPEGProc", "JPEGProc"),
0x0203: ("JPEGRestartInterval", "JPEGRestartInterval"),
0x0205: ("JPEGLosslessPredictors", "JPEGLosslessPredictors"),
0x0206: ("JPEGPointTransforms", "JPEGPointTransforms"),
0x0207: ("JPEGQTables", "JPEGQTables"),
0x0208: ("JPEGDCTables", "JPEGDCTables"),
0x0209: ("JPEGACTables", "JPEGACTables"),
# IFD pointers
EXIF_IFD_POINTER: ("IFDExif", "Exif IFD Pointer"),
GPS_IFD_POINTER: ("IFDGPS", "GPS IFD Pointer"),
INTEROP_IFD_POINTER: ("IFDInterop", "Interoperability IFD Pointer"),
}
class ExifIFDEntry(BasicIFDEntry):
TAG_INFO = {
# version
0x9000: ("ExifVersion", "Exif version"),
0xA000: ("FlashpixVersion", "Supported Flashpix version"),
# image data characteristics
0xA001: ("ColorSpace", "Color space information"),
# image configuration
0x9101: ("ComponentsConfiguration", "Meaning of each component"),
0x9102: ("CompressedBitsPerPixel", "Image compression mode"),
0xA002: ("PixelXDimension", "Valid image width"),
0xA003: ("PixelYDimension", "Valid image height"),
# user information
0x927C: ("MakerNote", "Manufacturer notes"),
0x9286: ("UserComment", "User comments"),
# related file information
0xA004: ("RelatedSoundFile", "Related audio file"),
# date and time
0x9003: ("DateTimeOriginal", "Date and time of original data generation"),
0x9004: ("DateTimeDigitized", "Date and time of digital data generation"),
0x9290: ("SubSecTime", "DateTime subseconds"),
0x9291: ("SubSecTimeOriginal", "DateTimeOriginal subseconds"),
0x9292: ("SubSecTimeDigitized", "DateTimeDigitized subseconds"),
# picture-taking conditions
0x829A: ("ExposureTime", "Exposure time"),
0x829D: ("FNumber", "F number"),
0x8822: ("ExposureProgram", "Exposure program"),
0x8824: ("SpectralSensitivity", "Spectral sensitivity"),
0x8827: ("ISOSpeedRatings", "ISO speed rating"),
0x8828: ("OECF", "Optoelectric conversion factor"),
0x9201: ("ShutterSpeedValue", "Shutter speed"),
0x9202: ("ApertureValue", "Aperture"),
0x9203: ("BrightnessValue", "Brightness"),
0x9204: ("ExposureBiasValue", "Exposure bias"),
0x9205: ("MaxApertureValue", "Maximum lens aperture"),
0x9206: ("SubjectDistance", "Subject distance"),
0x9207: ("MeteringMode", "Metering mode"),
0x9208: ("LightSource", "Light source"),
0x9209: ("Flash", "Flash"),
0x920A: ("FocalLength", "Lens focal length"),
0x9214: ("SubjectArea", "Subject area"),
0xA20B: ("FlashEnergy", "Flash energy"),
0xA20C: ("SpatialFrequencyResponse", "Spatial frequency response"),
0xA20E: ("FocalPlaneXResolution", "Focal plane X resolution"),
0xA20F: ("FocalPlaneYResolution", "Focal plane Y resolution"),
0xA210: ("FocalPlaneResolutionUnit", "Focal plane resolution unit"),
0xA214: ("SubjectLocation", "Subject location"),
0xA215: ("ExposureIndex", "Exposure index"),
0xA217: ("SensingMethod", "Sensing method"),
0xA300: ("FileSource", "File source"),
0xA301: ("SceneType", "Scene type"),
0xA302: ("CFAPattern", "CFA pattern"),
0xA401: ("CustomRendered", "Custom image processing"),
0xA402: ("ExposureMode", "Exposure mode"),
0xA403: ("WhiteBalance", "White balance"),
0xA404: ("DigitalZoomRatio", "Digital zoom ratio"),
0xA405: ("FocalLengthIn35mmFilm", "Focal length in 35 mm film"),
0xA406: ("SceneCaptureType", "Scene capture type"),
0xA407: ("GainControl", "Gain control"),
0xA408: ("Contrast", "Contrast"),
0xA409: ("Saturation", "Saturation"),
0xA40A: ("Sharpness", "Sharpness"),
0xA40B: ("DeviceSettingDescription", "Device settings description"),
0xA40C: ("SubjectDistanceRange", "Subject distance range"),
# other tags
0xA420: ("ImageUniqueID", "Unique image ID"),
}
class GPSIFDEntry(BasicIFDEntry):
TAG_INFO = {
0x0000: ("GPSVersionID", "GPS tag version"),
0x0001: ("GPSLatitudeRef", "North or South Latitude"),
0x0002: ("GPSLatitude", "Latitude"),
0x0003: ("GPSLongitudeRef", "East or West Longitude"),
0x0004: ("GPSLongitude", "Longitude"),
0x0005: ("GPSAltitudeRef", "Altitude reference"),
0x0006: ("GPSAltitude", "Altitude"),
0x0007: ("GPSTimeStamp", "GPS time (atomic clock)"),
0x0008: ("GPSSatellites", "GPS satellites used for measurement"),
0x0009: ("GPSStatus", "GPS receiver status"),
0x000A: ("GPSMeasureMode", "GPS measurement mode"),
0x000B: ("GPSDOP", "Measurement precision"),
0x000C: ("GPSSpeedRef", "Speed unit"),
0x000D: ("GPSSpeed", "Speed of GPS receiver"),
0x000E: ("GPSTrackRef", "Reference for direction of movement"),
0x000F: ("GPSTrack", "Direction of movement"),
0x0010: ("GPSImgDirectionRef", "Reference for direction of image"),
0x0011: ("GPSImgDirection", "Direction of image"),
0x0012: ("GPSMapDatum", "Geodetic survey data used"),
0x0013: ("GPSDestLatitudeRef", "Reference for latitude of destination"),
0x0014: ("GPSDestLatitude", "Latitude of destination"),
0x0015: ("GPSDestLongitudeRef", "Reference for longitude of destination"),
0x0016: ("GPSDestLongitude", "Longitude of destination"),
0x0017: ("GPSDestBearingRef", "Reference for bearing of destination"),
0x0018: ("GPSDestBearing", "Bearing of destination"),
0x0019: ("GPSDestDistanceRef", "Reference for distance to destination"),
0x001A: ("GPSDestDistance", "Distance to destination"),
0x001B: ("GPSProcessingMethod", "Name of GPS processing method"),
0x001C: ("GPSAreaInformation", "Name of GPS area"),
0x001D: ("GPSDateStamp", "GPS date"),
0x001E: ("GPSDifferential", "GPS differential correction"),
}
class InteropIFDEntry(BasicIFDEntry):
TAG_INFO = {
0x0001: ("InteroperabilityIndex", "Interoperability Identification"),
}
class IFD(SeekableFieldSet):
EntryClass = IFDEntry
def __init__(self, parent, name, base_addr):
self.base_addr = base_addr
SeekableFieldSet.__init__(self, parent, name)
def createFields(self):
yield UInt16(self, "count", "Number of entries")
count = self["count"].value
if count == 0:
raise ParserError("IFDs cannot be empty.")
for i in range(count):
yield self.EntryClass(self, "entry[]")
yield UInt32(self, "next", "Offset to next IFD")
for i in range(count):
entry = self['entry[%d]' % i]
if 'offset' not in entry:
continue
self.seekByte(entry['offset'].value +
self.base_addr // 8, relative=False)
count = entry['count'].value
name = "value[%s]" % i
if issubclass(entry.value_cls, Bytes):
yield entry.value_cls(self, name, count)
elif entry['tag'].display == "XMPPacket":
yield String(self, name, count)
elif count == 1:
yield entry.value_cls(self, name)
else:
yield ValueArray(self, name, entry.value_cls, count)
def getEntryValues(self, entry):
n = int(entry.name.rsplit('[', 1)[1].strip(']'))
if 'offset' in entry:
field = 'value[%d]' % n
base = self
else:
field = 'value'
base = entry
if field not in base:
return None
elif isinstance(base[field], ValueArray):
return list(base[field])
else:
return [base[field]]
class ExifIFD(IFD):
EntryClass = ExifIFDEntry
class GPSIFD(IFD):
EntryClass = GPSIFDEntry
class InteropIFD(IFD):
EntryClass = InteropIFDEntry
IFD_TAGS = {
IFDEntry.EXIF_IFD_POINTER: ('exif', ExifIFD),
IFDEntry.GPS_IFD_POINTER: ('exif_gps', GPSIFD),
IFDEntry.INTEROP_IFD_POINTER: ('exif_interop', InteropIFD),
}
def TIFF(self):
iff_start = self.absolute_address + self.current_size
yield String(self, "endian", 2, "Endian ('II' or 'MM')", charset="ASCII")
if self["endian"].value not in ("II", "MM"):
raise ParserError("Invalid endian!")
if self["endian"].value == "II":
self.endian = LITTLE_ENDIAN
else:
self.endian = BIG_ENDIAN
yield UInt16(self, "version", "TIFF version number")
yield UInt32(self, "img_dir_ofs", "Next image directory offset")
offsets = [(self['img_dir_ofs'].value, 'ifd[]', IFD)]
while offsets:
offset, name, klass = offsets.pop(0)
self.seekByte(offset + iff_start // 8, relative=False)
ifd = klass(self, name, iff_start)
yield ifd
for entry in ifd.array('entry'):
tag = entry['tag'].value
if tag in IFD_TAGS:
name, klass = IFD_TAGS[tag]
offsets.append((ifd.getEntryValues(entry)[
0].value, name + '[]', klass))
elif tag == IFDEntry.SUBIFD_POINTERS:
for val in ifd.getEntryValues(entry):
offsets.append((val.value, ifd.name + '[]', IFD))
if ifd['next'].value != 0:
offsets.append((ifd['next'].value, 'ifd[]', IFD))
class Exif(SeekableFieldSet):
def createFields(self):
# Headers
yield String(self, "header", 6, "Header (Exif\\0\\0)", charset="ASCII")
if self["header"].value != "Exif\0\0":
raise ParserError("Invalid EXIF signature!")
iff_start = self.absolute_address + self.current_size
ifds = []
for field in TIFF(self):
yield field
if isinstance(field, IFD):
ifds.append(field)
for ifd in ifds:
data = {}
for i, entry in enumerate(ifd.array('entry')):
data[entry['tag'].display] = entry
if 'JPEGInterchangeFormat' in data and 'JPEGInterchangeFormatLength' in data:
offs = ifd.getEntryValues(
data['JPEGInterchangeFormat'])[0].value
size = ifd.getEntryValues(
data['JPEGInterchangeFormatLength'])[0].value
if size == 0:
continue
self.seekByte(offs + iff_start // 8, relative=False)
yield SubFile(self, "thumbnail[]", size, "Thumbnail (JPEG file)", mime_type="image/jpeg")