201 lines
7.5 KiB
Python
201 lines
7.5 KiB
Python
"""Extract EXIF from JPEG files."""
|
|
|
|
from typing import BinaryIO, Tuple
|
|
|
|
from exifread.core.exceptions import InvalidExif
|
|
from exifread.core.utils import ord_
|
|
from exifread.exif_log import get_logger
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
def _increment_base(data, base) -> int:
|
|
return ord_(data[base + 2]) * 256 + ord_(data[base + 3]) + 2
|
|
|
|
|
|
def _get_initial_base(fh: BinaryIO, data: bytes, fake_exif: int) -> Tuple[int, int]:
|
|
base = 2
|
|
logger.debug(
|
|
"data[2]=0x%X data[3]=0x%X data[6:10]=%s",
|
|
ord_(data[2]),
|
|
ord_(data[3]),
|
|
data[6:10],
|
|
)
|
|
while ord_(data[2]) == 0xFF and data[6:10] in (b"JFIF", b"JFXX", b"OLYM", b"Phot"):
|
|
length = ord_(data[4]) * 256 + ord_(data[5])
|
|
logger.debug(" Length offset is %s", length)
|
|
fh.read(length - 8)
|
|
# fake an EXIF beginning of file
|
|
# I don't think this is used. --gd
|
|
data = b"\xff\x00" + fh.read(10)
|
|
fake_exif = 1
|
|
if base > 2:
|
|
logger.debug(" Added to base")
|
|
base = base + length + 4 - 2
|
|
else:
|
|
logger.debug(" Added to zero")
|
|
base = length + 4
|
|
logger.debug(" Set segment base to 0x%X", base)
|
|
return base, fake_exif
|
|
|
|
|
|
def _get_base(base: int, data: bytes) -> int:
|
|
# pylint: disable=too-many-statements
|
|
while True:
|
|
logger.debug(" Segment base 0x%X", base)
|
|
if data[base : base + 2] == b"\xff\xe1":
|
|
# APP1
|
|
logger.debug(" APP1 at base 0x%X", base)
|
|
logger.debug(
|
|
" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])
|
|
)
|
|
logger.debug(" Code: %s", data[base + 4 : base + 8])
|
|
if data[base + 4 : base + 8] == b"Exif":
|
|
logger.debug(
|
|
" Decrement base by 2 to get to pre-segment header (for compatibility with later code)"
|
|
)
|
|
base -= 2
|
|
break
|
|
increment = _increment_base(data, base)
|
|
logger.debug(" Increment base by %s", increment)
|
|
base += increment
|
|
elif data[base : base + 2] == b"\xff\xe0":
|
|
# APP0
|
|
logger.debug(" APP0 at base 0x%X", base)
|
|
logger.debug(
|
|
" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])
|
|
)
|
|
logger.debug(" Code: %s", data[base + 4 : base + 8])
|
|
increment = _increment_base(data, base)
|
|
logger.debug(" Increment base by %s", increment)
|
|
base += increment
|
|
elif data[base : base + 2] == b"\xff\xe2":
|
|
# APP2
|
|
logger.debug(" APP2 at base 0x%X", base)
|
|
logger.debug(
|
|
" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])
|
|
)
|
|
logger.debug(" Code: %s", data[base + 4 : base + 8])
|
|
increment = _increment_base(data, base)
|
|
logger.debug(" Increment base by %s", increment)
|
|
base += increment
|
|
elif data[base : base + 2] == b"\xff\xee":
|
|
# APP14
|
|
logger.debug(" APP14 Adobe segment at base 0x%X", base)
|
|
logger.debug(
|
|
" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])
|
|
)
|
|
logger.debug(" Code: %s", data[base + 4 : base + 8])
|
|
increment = _increment_base(data, base)
|
|
logger.debug(" Increment base by %s", increment)
|
|
base += increment
|
|
logger.debug(
|
|
" There is useful EXIF-like data here, but we have no parser for it."
|
|
)
|
|
elif data[base : base + 2] == b"\xff\xdb":
|
|
logger.debug(
|
|
" JPEG image data at base 0x%X No more segments are expected.", base
|
|
)
|
|
break
|
|
elif data[base : base + 2] == b"\xff\xd8":
|
|
# APP12
|
|
logger.debug(" FFD8 segment at base 0x%X", base)
|
|
logger.debug(
|
|
" Got 0x%X 0x%X and %s instead",
|
|
ord_(data[base]),
|
|
ord_(data[base + 1]),
|
|
data[4 + base : 10 + base],
|
|
)
|
|
logger.debug(
|
|
" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])
|
|
)
|
|
logger.debug(" Code: %s", data[base + 4 : base + 8])
|
|
increment = _increment_base(data, base)
|
|
logger.debug(" Increment base by %s", increment)
|
|
base += increment
|
|
elif data[base : base + 2] == b"\xff\xec":
|
|
# APP12
|
|
logger.debug(
|
|
" APP12 XMP (Ducky) or Pictureinfo segment at base 0x%X", base
|
|
)
|
|
logger.debug(
|
|
" Got 0x%X and 0x%X instead", ord_(data[base]), ord_(data[base + 1])
|
|
)
|
|
logger.debug(
|
|
" Length: 0x%X 0x%X", ord_(data[base + 2]), ord_(data[base + 3])
|
|
)
|
|
logger.debug("Code: %s", data[base + 4 : base + 8])
|
|
increment = _increment_base(data, base)
|
|
logger.debug(" Increment base by %s", increment)
|
|
base += increment
|
|
logger.debug(
|
|
(
|
|
" There is useful EXIF-like data here (quality, comment, copyright), "
|
|
"but we have no parser for it."
|
|
)
|
|
)
|
|
else:
|
|
try:
|
|
increment = _increment_base(data, base)
|
|
logger.debug(
|
|
" Got 0x%X and 0x%X instead",
|
|
ord_(data[base]),
|
|
ord_(data[base + 1]),
|
|
)
|
|
except IndexError as err:
|
|
raise InvalidExif(
|
|
"Unexpected/unhandled segment type or file content."
|
|
) from err
|
|
logger.debug(" Increment base by %s", increment)
|
|
base += increment
|
|
return base
|
|
|
|
|
|
def find_jpeg_exif(fh: BinaryIO, data: bytes, fake_exif: int) -> Tuple[int, bytes, int]:
|
|
logger.debug(
|
|
"JPEG format recognized data[0:2]=0x%X%X", ord_(data[0]), ord_(data[1])
|
|
)
|
|
|
|
base, fake_exif = _get_initial_base(fh, data, fake_exif)
|
|
|
|
# Big ugly patch to deal with APP2 (or other) data coming before APP1
|
|
fh.seek(0)
|
|
# in theory, this could be insufficient since 64K is the maximum size--gd
|
|
data = fh.read(base + 4000)
|
|
|
|
base = _get_base(base, data)
|
|
|
|
fh.seek(base + 12)
|
|
if ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base] == b"Exif":
|
|
# detected EXIF header
|
|
offset = fh.tell()
|
|
endian = fh.read(1)
|
|
# HACK TEST: endian = 'M'
|
|
elif ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base + 1] == b"Ducky":
|
|
# detected Ducky header.
|
|
logger.debug(
|
|
"EXIF-like header (normally 0xFF and code): 0x%X and %s",
|
|
ord_(data[2 + base]),
|
|
data[6 + base : 10 + base + 1],
|
|
)
|
|
offset = fh.tell()
|
|
endian = fh.read(1)
|
|
elif ord_(data[2 + base]) == 0xFF and data[6 + base : 10 + base + 1] == b"Adobe":
|
|
# detected APP14 (Adobe)
|
|
logger.debug(
|
|
"EXIF-like header (normally 0xFF and code): 0x%X and %s",
|
|
ord_(data[2 + base]),
|
|
data[6 + base : 10 + base + 1],
|
|
)
|
|
offset = fh.tell()
|
|
endian = fh.read(1)
|
|
else:
|
|
# no EXIF information
|
|
msg = "No EXIF header expected data[2+base]==0xFF and data[6+base:10+base]===Exif (or Duck)"
|
|
msg += " Did get 0x%X and %r" % (
|
|
ord_(data[2 + base]),
|
|
data[6 + base : 10 + base + 1],
|
|
)
|
|
raise InvalidExif(msg)
|
|
return offset, endian, fake_exif
|