119 lines
3.7 KiB
Python
119 lines
3.7 KiB
Python
"""Utilities to find the EXIF offset and endian."""
|
|
|
|
import struct
|
|
from typing import BinaryIO, Dict, Tuple
|
|
|
|
from exifread.core.exceptions import ExifNotFound, InvalidExif
|
|
from exifread.core.heic import HEICExifFinder, find_heic_tiff
|
|
from exifread.core.jpeg import find_jpeg_exif
|
|
from exifread.core.jxl import JXLExifFinder
|
|
from exifread.core.utils import ord_
|
|
from exifread.exif_log import get_logger
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
ENDIAN_TYPES: Dict[str, str] = {
|
|
"I": "Intel",
|
|
"M": "Motorola",
|
|
"\x01": "Adobe Ducky",
|
|
"b": "XMP/Adobe unknown",
|
|
}
|
|
|
|
|
|
def get_endian_str(endian_bytes) -> Tuple[str, str]:
|
|
endian_str = chr(ord_(endian_bytes[0]))
|
|
return endian_str, ENDIAN_TYPES.get(endian_str, "Unknown")
|
|
|
|
|
|
def find_tiff_exif(fh: BinaryIO) -> Tuple[int, bytes]:
|
|
logger.debug("TIFF format recognized in data[0:2]")
|
|
fh.seek(0)
|
|
endian = fh.read(1)
|
|
fh.read(1)
|
|
offset = 0
|
|
return offset, endian
|
|
|
|
|
|
def find_webp_exif(fh: BinaryIO) -> Tuple[int, bytes]:
|
|
logger.debug("WebP format recognized in data[0:4], data[8:12]")
|
|
# file specification: https://developers.google.com/speed/webp/docs/riff_container
|
|
data = fh.read(5)
|
|
if data[0:4] == b"VP8X" and data[4] & 8:
|
|
# https://developers.google.com/speed/webp/docs/riff_container#extended_file_format
|
|
fh.seek(13, 1)
|
|
while True:
|
|
data = fh.read(8) # Chunk FourCC (32 bits) and Chunk Size (32 bits)
|
|
if len(data) != 8:
|
|
raise InvalidExif("Invalid webp file chunk header.")
|
|
if data[0:4] == b"EXIF":
|
|
fh.seek(6, 1)
|
|
offset = fh.tell()
|
|
endian = fh.read(1)
|
|
return offset, endian
|
|
size = struct.unpack("<L", data[4:8])[0]
|
|
fh.seek(size, 1)
|
|
raise ExifNotFound("Webp file does not have exif data.")
|
|
|
|
|
|
def find_png_exif(fh: BinaryIO, data: bytes) -> Tuple[int, bytes]:
|
|
logger.debug("PNG format recognized in data[0:8]=%s", data[:8].hex())
|
|
fh.seek(8)
|
|
|
|
while True:
|
|
data = fh.read(8)
|
|
chunk = data[4:8]
|
|
logger.debug("PNG found chunk %s", chunk.decode("ascii"))
|
|
|
|
if chunk in (b"", b"IEND"):
|
|
break
|
|
if chunk == b"eXIf":
|
|
offset = fh.tell()
|
|
return offset, fh.read(1)
|
|
|
|
chunk_size = int.from_bytes(data[:4], "big")
|
|
fh.seek(fh.tell() + chunk_size + 4)
|
|
|
|
raise ExifNotFound("PNG file does not have exif data.")
|
|
|
|
|
|
def find_jxl_exif(fh: BinaryIO) -> Tuple[int, bytes]:
|
|
logger.debug("JPEG XL format recognized in data[0:12]")
|
|
|
|
fh.seek(0)
|
|
jxl = JXLExifFinder(fh)
|
|
offset, endian = jxl.find_exif()
|
|
if offset > 0:
|
|
return offset, endian
|
|
|
|
raise ExifNotFound("JPEG XL file does not have exif data.")
|
|
|
|
|
|
def determine_type(fh: BinaryIO) -> Tuple[int, bytes, int]:
|
|
# by default do not fake an EXIF beginning
|
|
fake_exif = 0
|
|
|
|
data = fh.read(12)
|
|
if data[0:2] in [b"II", b"MM"]:
|
|
# it's a TIFF file
|
|
offset, endian = find_tiff_exif(fh)
|
|
elif data[4:12] in [b"ftypheic", b"ftypavif", b"ftypmif1"]:
|
|
fh.seek(0)
|
|
heic = HEICExifFinder(fh)
|
|
offset, endian = heic.find_exif()
|
|
if offset == 0:
|
|
offset, endian = find_heic_tiff(fh)
|
|
# It's a HEIC file with a TIFF header
|
|
elif data[0:4] == b"RIFF" and data[8:12] == b"WEBP":
|
|
offset, endian = find_webp_exif(fh)
|
|
elif data[0:2] == b"\xff\xd8":
|
|
# it's a JPEG file
|
|
offset, endian, fake_exif = find_jpeg_exif(fh, data, fake_exif)
|
|
elif data[0:8] == b"\x89PNG\r\n\x1a\n":
|
|
offset, endian = find_png_exif(fh, data)
|
|
elif data == b"\0\0\0\x0cJXL\x20\x0d\x0a\x87\x0a":
|
|
offset, endian = find_jxl_exif(fh)
|
|
else:
|
|
raise ExifNotFound("File format not recognized.")
|
|
return offset, endian, fake_exif
|