SickGear/lib/hachoir_parser/image/exif.py

362 lines
12 KiB
Python
Raw Normal View History

"""
EXIF metadata parser (can be found in a JPEG picture for example)
Author: Victor Stinner
"""
from lib.hachoir_core.field import (FieldSet, ParserError,
UInt8, UInt16, UInt32,
Int32, Enum, String,
Bytes, SubFile,
NullBytes, createPaddingField)
from lib.hachoir_core.endian import LITTLE_ENDIAN, BIG_ENDIAN, NETWORK_ENDIAN
from lib.hachoir_core.text_handler import textHandler, hexadecimal
from lib.hachoir_core.tools import createDict
MAX_COUNT = 1000
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 BasicIFDEntry(FieldSet):
TYPE_BYTE = 0
TYPE_UNDEFINED = 7
TYPE_RATIONAL = 5
TYPE_SIGNED_RATIONAL = 10
TYPE_INFO = {
1: (UInt8, "BYTE (8 bits)"),
2: (String, "ASCII (8 bits)"),
3: (UInt16, "SHORT (16 bits)"),
4: (UInt32, "LONG (32 bits)"),
5: (RationalUInt32, "RATIONAL (2x LONG, 64 bits)"),
7: (Bytes, "UNDEFINED (8 bits)"),
9: (Int32, "SIGNED LONG (32 bits)"),
10: (RationalInt32, "SRATIONAL (2x SIGNED LONGs, 64 bits)"),
}
ENTRY_FORMAT = createDict(TYPE_INFO, 0)
TYPE_NAME = createDict(TYPE_INFO, 1)
def createFields(self):
yield Enum(textHandler(UInt16(self, "tag", "Tag"), hexadecimal), self.TAG_NAME)
yield Enum(textHandler(UInt16(self, "type", "Type"), hexadecimal), self.TYPE_NAME)
yield UInt32(self, "count", "Count")
if self["type"].value not in (self.TYPE_BYTE, self.TYPE_UNDEFINED) \
and MAX_COUNT < self["count"].value:
raise ParserError("EXIF: Invalid count value (%s)" % self["count"].value)
value_size, array_size = self.getSizes()
# Get offset/value
if not value_size:
yield NullBytes(self, "padding", 4)
elif value_size <= 32:
if 1 < array_size:
name = "value[]"
else:
name = "value"
kw = {}
cls = self.value_cls
if cls is String:
args = (self, name, value_size/8, "Value")
kw["strip"] = " \0"
kw["charset"] = "ISO-8859-1"
elif cls is Bytes:
args = (self, name, value_size/8, "Value")
else:
args = (self, name, "Value")
for index in xrange(array_size):
yield cls(*args, **kw)
size = array_size * value_size
if size < 32:
yield NullBytes(self, "padding", (32-size)//8)
else:
yield UInt32(self, "offset", "Value offset")
def getSizes(self):
"""
Returns (value_size, array_size): value_size in bits and
array_size in number of items.
"""
# Create format
self.value_cls = self.ENTRY_FORMAT.get(self["type"].value, Bytes)
# Set size
count = self["count"].value
if self.value_cls in (String, Bytes):
return 8 * count, 1
else:
return self.value_cls.static_size * count, count
class ExifEntry(BasicIFDEntry):
OFFSET_JPEG_SOI = 0x0201
EXIF_IFD_POINTER = 0x8769
TAG_WIDTH = 0xA002
TAG_HEIGHT = 0xA003
TAG_GPS_LATITUDE_REF = 0x0001
TAG_GPS_LATITUDE = 0x0002
TAG_GPS_LONGITUDE_REF = 0x0003
TAG_GPS_LONGITUDE = 0x0004
TAG_GPS_ALTITUDE_REF = 0x0005
TAG_GPS_ALTITUDE = 0x0006
TAG_GPS_TIMESTAMP = 0x0007
TAG_GPS_DATESTAMP = 0x001d
TAG_IMG_TITLE = 0x010e
TAG_FILE_TIMESTAMP = 0x0132
TAG_SOFTWARE = 0x0131
TAG_CAMERA_MODEL = 0x0110
TAG_CAMERA_MANUFACTURER = 0x010f
TAG_ORIENTATION = 0x0112
TAG_EXPOSURE = 0x829A
TAG_FOCAL = 0x829D
TAG_BRIGHTNESS = 0x9203
TAG_APERTURE = 0x9205
TAG_USER_COMMENT = 0x9286
TAG_NAME = {
# GPS
0x0000: "GPS version ID",
0x0001: "GPS latitude ref",
0x0002: "GPS latitude",
0x0003: "GPS longitude ref",
0x0004: "GPS longitude",
0x0005: "GPS altitude ref",
0x0006: "GPS altitude",
0x0007: "GPS timestamp",
0x0008: "GPS satellites",
0x0009: "GPS status",
0x000a: "GPS measure mode",
0x000b: "GPS DOP",
0x000c: "GPS speed ref",
0x000d: "GPS speed",
0x000e: "GPS track ref",
0x000f: "GPS track",
0x0010: "GPS img direction ref",
0x0011: "GPS img direction",
0x0012: "GPS map datum",
0x0013: "GPS dest latitude ref",
0x0014: "GPS dest latitude",
0x0015: "GPS dest longitude ref",
0x0016: "GPS dest longitude",
0x0017: "GPS dest bearing ref",
0x0018: "GPS dest bearing",
0x0019: "GPS dest distance ref",
0x001a: "GPS dest distance",
0x001b: "GPS processing method",
0x001c: "GPS area information",
0x001d: "GPS datestamp",
0x001e: "GPS differential",
0x0100: "Image width",
0x0101: "Image height",
0x0102: "Number of bits per component",
0x0103: "Compression scheme",
0x0106: "Pixel composition",
TAG_ORIENTATION: "Orientation of image",
0x0115: "Number of components",
0x011C: "Image data arrangement",
0x0212: "Subsampling ratio Y to C",
0x0213: "Y and C positioning",
0x011A: "Image resolution width direction",
0x011B: "Image resolution in height direction",
0x0128: "Unit of X and Y resolution",
0x0111: "Image data location",
0x0116: "Number of rows per strip",
0x0117: "Bytes per compressed strip",
0x0201: "Offset to JPEG SOI",
0x0202: "Bytes of JPEG data",
0x012D: "Transfer function",
0x013E: "White point chromaticity",
0x013F: "Chromaticities of primaries",
0x0211: "Color space transformation matrix coefficients",
0x0214: "Pair of blank and white reference values",
TAG_FILE_TIMESTAMP: "File change date and time",
TAG_IMG_TITLE: "Image title",
TAG_CAMERA_MANUFACTURER: "Camera (Image input equipment) manufacturer",
TAG_CAMERA_MODEL: "Camera (Input input equipment) model",
TAG_SOFTWARE: "Software",
0x013B: "File change date and time",
0x8298: "Copyright holder",
0x8769: "Exif IFD Pointer",
TAG_EXPOSURE: "Exposure time",
TAG_FOCAL: "F number",
0x8822: "Exposure program",
0x8824: "Spectral sensitivity",
0x8827: "ISO speed rating",
0x8828: "Optoelectric conversion factor OECF",
0x9201: "Shutter speed",
0x9202: "Aperture",
TAG_BRIGHTNESS: "Brightness",
0x9204: "Exposure bias",
TAG_APERTURE: "Maximum lens aperture",
0x9206: "Subject distance",
0x9207: "Metering mode",
0x9208: "Light source",
0x9209: "Flash",
0x920A: "Lens focal length",
0x9214: "Subject area",
0xA20B: "Flash energy",
0xA20C: "Spatial frequency response",
0xA20E: "Focal plane X resolution",
0xA20F: "Focal plane Y resolution",
0xA210: "Focal plane resolution unit",
0xA214: "Subject location",
0xA215: "Exposure index",
0xA217: "Sensing method",
0xA300: "File source",
0xA301: "Scene type",
0xA302: "CFA pattern",
0xA401: "Custom image processing",
0xA402: "Exposure mode",
0xA403: "White balance",
0xA404: "Digital zoom ratio",
0xA405: "Focal length in 35 mm film",
0xA406: "Scene capture type",
0xA407: "Gain control",
0xA408: "Contrast",
0x9000: "Exif version",
0xA000: "Supported Flashpix version",
0xA001: "Color space information",
0x9101: "Meaning of each component",
0x9102: "Image compression mode",
TAG_WIDTH: "Valid image width",
TAG_HEIGHT: "Valid image height",
0x927C: "Manufacturer notes",
TAG_USER_COMMENT: "User comments",
0xA004: "Related audio file",
0x9003: "Date and time of original data generation",
0x9004: "Date and time of digital data generation",
0x9290: "DateTime subseconds",
0x9291: "DateTimeOriginal subseconds",
0x9292: "DateTimeDigitized subseconds",
0xA420: "Unique image ID",
0xA005: "Interoperability IFD Pointer"
}
def createDescription(self):
return "Entry: %s" % self["tag"].display
def sortExifEntry(a,b):
return int( a["offset"].value - b["offset"].value )
class ExifIFD(FieldSet):
def seek(self, offset):
"""
Seek to byte address relative to parent address.
"""
padding = offset - (self.address + self.current_size)/8
if 0 < padding:
return createPaddingField(self, padding*8)
else:
return None
def createFields(self):
offset_diff = 6
yield UInt16(self, "count", "Number of entries")
entries = []
next_chunk_offset = None
count = self["count"].value
if not count:
return
while count:
addr = self.absolute_address + self.current_size
next = self.stream.readBits(addr, 32, NETWORK_ENDIAN)
if next in (0, 0xF0000000):
break
entry = ExifEntry(self, "entry[]")
yield entry
if entry["tag"].value in (ExifEntry.EXIF_IFD_POINTER, ExifEntry.OFFSET_JPEG_SOI):
next_chunk_offset = entry["value"].value + offset_diff
if 32 < entry.getSizes()[0]:
entries.append(entry)
count -= 1
yield UInt32(self, "next", "Next IFD offset")
try:
entries.sort( sortExifEntry )
except TypeError:
raise ParserError("Unable to sort entries!")
value_index = 0
for entry in entries:
padding = self.seek(entry["offset"].value + offset_diff)
if padding is not None:
yield padding
value_size, array_size = entry.getSizes()
if not array_size:
continue
cls = entry.value_cls
if 1 < array_size:
name = "value_%s[]" % entry.name
else:
name = "value_%s" % entry.name
desc = "Value of \"%s\"" % entry["tag"].display
if cls is String:
for index in xrange(array_size):
yield cls(self, name, value_size/8, desc, strip=" \0", charset="ISO-8859-1")
elif cls is Bytes:
for index in xrange(array_size):
yield cls(self, name, value_size/8, desc)
else:
for index in xrange(array_size):
yield cls(self, name, desc)
value_index += 1
if next_chunk_offset is not None:
padding = self.seek(next_chunk_offset)
if padding is not None:
yield padding
def createDescription(self):
return "Exif IFD (id %s)" % self["id"].value
class Exif(FieldSet):
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!")
yield String(self, "byte_order", 2, "Byte order", charset="ASCII")
if self["byte_order"].value not in ("II", "MM"):
raise ParserError("Invalid endian!")
if self["byte_order"].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")
while not self.eof:
addr = self.absolute_address + self.current_size
tag = self.stream.readBits(addr, 16, NETWORK_ENDIAN)
if tag == 0xFFD8:
size = (self._size - self.current_size) // 8
yield SubFile(self, "thumbnail", size, "Thumbnail (JPEG file)", mime_type="image/jpeg")
break
elif tag == 0xFFFF:
break
yield ExifIFD(self, "ifd[]", "IFD")
padding = self.seekBit(self._size)
if padding is not None:
yield padding