mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-01 08:53:37 +00:00
362 lines
12 KiB
Python
362 lines
12 KiB
Python
|
"""
|
||
|
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
|
||
|
|
||
|
|