Static type analysis

This commit is contained in:
Joel Collins 2020-11-30 13:36:45 +00:00
parent 3aebb8bead
commit 7866ec0f47
63 changed files with 1825 additions and 2722 deletions

View file

@ -1,3 +1,11 @@
from . import capture, capture_manager
from .capture import THUMBNAIL_SIZE, CaptureObject
from .capture_manager import CaptureManager
__all__ = [
"capture",
"capture_manager",
"THUMBNAIL_SIZE",
"CaptureObject",
"CaptureManager",
]

View file

@ -6,12 +6,13 @@ import logging
import os
import uuid
from collections import OrderedDict
from typing import Callable, Dict, List, Optional
import dateutil.parser
import piexif
from piexif import InvalidImageDataError
from PIL import Image
from openflexure_microscope.captures import piexif
from openflexure_microscope.captures.piexif import InvalidImageDataError
from openflexure_microscope.json import JSONEncoder
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
@ -39,7 +40,7 @@ def build_captures_from_exif(capture_path):
logging.debug("Reloading capture %s...", (f))
capture = capture_from_path(f)
if capture:
captures[capture.id] = capture
captures[str(capture.id)] = capture
logging.info("%s capture files successfully reloaded", (len(captures)))
@ -68,33 +69,42 @@ class CaptureObject(object):
Serves to simplify modifying properties of on-disk capture data.
"""
def __init__(self, filepath) -> None:
def __init__(self, filepath: str) -> None:
"""Create a new StreamObject, to manage capture data."""
# Stream for buffering capture data
self.stream = io.BytesIO()
self.stream: io.BytesIO = io.BytesIO()
# Store a nice ID
self.id = uuid.uuid4() #: str: Unique capture ID
self.id: uuid.UUID = uuid.uuid4() #: str: Unique capture ID
logging.debug("Created CaptureObject %s", (self.id))
self.time = datetime.datetime.now()
self.time: datetime.datetime = datetime.datetime.now()
# Create file name. Default to UUID
self.format = None
self.file = filepath
self.file: str = filepath
# Placeholders for split file info
self.format: str = ""
self.basename: str = ""
self.filefolder: str = ""
self.name: str = ""
# Replace placeholders with actual split file info
self.split_file_path(self.file)
if not os.path.exists(self.filefolder):
os.makedirs(self.filefolder)
# Dataset information
self._dataset = None
self.dataset: Dict[str, str] = {}
# Dictionary for storing custom annotations
# Can be modified via the web API
self.annotations = {}
self.annotations: Dict[str, str] = {}
# List for storing tags
# Can be modified via the web API
self.tags = []
self.tags: List[str] = []
# On-delete callback
self.on_delete: Optional[Callable] = None
def write(self, s):
logging.debug("Writing to %s", self)
@ -112,7 +122,7 @@ class CaptureObject(object):
def open(self, mode):
return open(self.file, mode)
def split_file_path(self, filepath):
def split_file_path(self, filepath: str):
"""
Take a full file path, and split it into separated class properties.
@ -125,10 +135,10 @@ class CaptureObject(object):
self.basename = os.path.splitext(self.name)[0]
self.format = self.name.split(".")[-1]
def _read_exif(self):
def _read_exif(self) -> dict:
return piexif.load(self.file)
def _decode_usercomment(self, exif_dict: dict):
def _decode_usercomment(self, exif_dict: dict) -> dict:
if "Exif" not in exif_dict:
raise InvalidImageDataError
if piexif.ExifIFD.UserComment not in exif_dict["Exif"]:
@ -150,15 +160,17 @@ class CaptureObject(object):
)
def sync_basic_metadata(self):
exif_dict = self._read_exif()
exif_dict: dict = self._read_exif()
metadata_dict = self._decode_usercomment(exif_dict) or {}
image_metadata = metadata_dict.get("image")
metadata_dict: dict = self._decode_usercomment(exif_dict) or {}
self.dataset = metadata_dict.get("dataset")
image_metadata: Optional[dict] = metadata_dict.get("image")
if not image_metadata:
raise InvalidImageDataError("No capture metadata found")
self.id = image_metadata.get("id")
self.id = uuid.UUID(image_metadata.get("id"))
self.format = image_metadata.get("format")
self.time = dateutil.parser.isoparse(
image_metadata.get("time") or image_metadata.get("acquisitionDate")
@ -166,24 +178,11 @@ class CaptureObject(object):
self.tags = image_metadata.get("tags")
self.annotations = image_metadata.get("annotations")
dataset = metadata_dict.get("dataset")
if dataset:
self._dataset = {
"id": dataset.get("id"),
"name": dataset.get("name"),
"type": dataset.get("type"),
}
def read_full_metadata(self):
def read_full_metadata(self) -> dict:
logging.info("Reading full capture metadata from %s...", self.file)
exif_dict = self._read_exif()
return self._decode_usercomment(exif_dict)
@property
def dataset(self):
"""Read-only dataset property"""
return self._dataset
@property
def exists(self) -> bool:
"""Check if capture data file exists on disk."""
@ -193,7 +192,7 @@ class CaptureObject(object):
return False
# HANDLE TAGS
def put_tags(self, tags: list):
def put_tags(self, tags: List[str]):
"""
Add a new tag to the ``tags`` list attribute.
@ -218,7 +217,7 @@ class CaptureObject(object):
# HANDLE ANNOTATIONS
def put_annotations(self, data: dict) -> None:
def put_annotations(self, data: Dict[str, str]):
"""
Merge annotations from a passed dictionary into the capture metadata, and saves.
@ -227,7 +226,7 @@ class CaptureObject(object):
"""
self.put_and_save(annotations=data)
def delete_annotation(self, key: str) -> None:
def delete_annotation(self, key: str):
# Update in-memory annotations list
if key in self.annotations:
del self.annotations[key]
@ -237,7 +236,7 @@ class CaptureObject(object):
# HANDLE METADATA
def put_metadata(self, data: dict) -> None:
def put_metadata(self, data: dict):
"""
Merge root metadata from a passed dictionary into the capture metadata, and saves.
@ -246,10 +245,19 @@ class CaptureObject(object):
"""
self.put_and_save(metadata=data)
# HANDLE DATASET
def put_dataset(self, data: dict):
self.put_and_save(dataset=data)
# BULK OPERATIONS
def put_and_save(
self, tags: list = None, annotations: dict = None, metadata: dict = None
self,
tags: Optional[List[str]] = None,
annotations: Optional[Dict[str, str]] = None,
dataset: Optional[Dict[str, str]] = None,
metadata: Optional[dict] = None,
):
"""
Batch-write tags, metadata, and annotations in a single disk operation
@ -258,6 +266,8 @@ class CaptureObject(object):
tags = []
if not annotations:
annotations = {}
if not dataset:
dataset = {}
if not metadata:
metadata = {}
@ -269,6 +279,9 @@ class CaptureObject(object):
# Update in-memory annotations dictionary
self.annotations.update(annotations)
# Update in-memory dataset dictionary
self.dataset.update(dataset)
# Write new data to file EXIF, if supported
if self.format.upper() in EXIF_FORMATS and self.exists:
logging.info("Writing Exif data to %s", self.file)
@ -281,6 +294,8 @@ class CaptureObject(object):
metadata_dict.get("image", {})["tags"] = self.tags
# Add new annotations to exif dictionary
metadata_dict.get("image", {})["annotations"] = self.annotations
# Set new dataset info in exif dictionary
metadata_dict["dataset"] = self.dataset
# Add new custom metadata to exif dictionary
metadata_dict.update(metadata)
@ -310,7 +325,7 @@ class CaptureObject(object):
return self.read_full_metadata()
@property
def data(self) -> io.BytesIO:
def data(self) -> Optional[io.BytesIO]:
"""
Return a BytesIO object of the capture data.
"""
@ -321,16 +336,17 @@ class CaptureObject(object):
d = io.BytesIO(f.read()) # Load bytes from file
d.seek(0) # Rewind loaded bytestream
# Create a copy of the bytestream bytes
data = io.BytesIO(d.getbuffer())
return io.BytesIO(d.getbuffer())
else:
data = None
return data # Read and return bytes data
return None
@property
def binary(self) -> bytes:
def binary(self) -> Optional[bytes]:
"""Return a byte string of the capture data."""
return self.data.getvalue()
if self.data:
return self.data.getvalue()
else:
return None
@property
def thumbnail(self) -> io.BytesIO:
@ -354,7 +370,7 @@ class CaptureObject(object):
# FILE MANAGEMENT
def save(self) -> None:
def save(self):
"""Write stream to file, and save/update metadata file"""
# If a stream OR file exists, save the metadata file
if self.exists:
@ -367,6 +383,9 @@ class CaptureObject(object):
logging.info("Deleting file %s", (self.file))
os.remove(self.file)
if self.on_delete:
self.on_delete(self, self.id)
return True
else:
return False

View file

@ -3,11 +3,12 @@ import logging
import os
import shutil
from collections import OrderedDict
from typing import Dict, List, MutableMapping, Optional, Union, ValuesView
from uuid import UUID
from labthings import StrictLock
from openflexure_microscope.paths import data_file_path
from openflexure_microscope.utilities import entry_by_uuid
from .capture import CaptureObject, build_captures_from_exif
@ -15,40 +16,18 @@ BASE_CAPTURE_PATH = data_file_path("micrographs")
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, "tmp")
def last_entry(object_list: list):
"""Return the last entry of a list, if the list contains items."""
if object_list: # If any images have been captured
return object_list[-1] # Return the latest captured image
else:
return None
def generate_basename():
"""Return a default filename based on the capture datetime"""
return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
def generate_numbered_basename(obj_list: list) -> str:
initial_basename = generate_basename()
basename = initial_basename
# Handle clashing
iterator = 1
while basename in [obj.basename for obj in obj_list]:
basename = initial_basename + "_{}".format(iterator)
iterator += 1
return basename
class CaptureManager:
def __init__(self):
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
self.paths: Dict[str, str] = {
"default": BASE_CAPTURE_PATH,
"temp": TEMP_CAPTURE_PATH,
}
self.lock = StrictLock(timeout=1, name="Captures")
self.lock: StrictLock = StrictLock(timeout=1, name="Captures")
# Capture data
self.images = OrderedDict()
self.videos = OrderedDict()
self.images: MutableMapping[str, CaptureObject] = OrderedDict()
self.videos: MutableMapping[str, CaptureObject] = OrderedDict()
# FILE MANAGEMENT
@ -96,41 +75,28 @@ class CaptureManager:
# RETURNING CAPTURES
@property
def image(self):
"""Return the latest captured image."""
return last_entry(self.images.values())
@property
def video(self):
"""Return the latest recorded video."""
return last_entry(self.videos.values())
def image_from_id(self, image_id):
def image_from_id(self, image_id: Union[str, int, UUID]) -> Optional[CaptureObject]:
"""Return an image StreamObject with a matching ID."""
logging.warning("image_from_id is deprecated. Access captures as a dictionary.")
return entry_by_uuid(image_id, self.images.values())
return entry_by_uuid(image_id, self.images)
def video_from_id(self, video_id):
def video_from_id(self, video_id: Union[str, int, UUID]) -> Optional[CaptureObject]:
"""Return a video StreamObject with a matching ID."""
logging.warning("video_from_id is deprecated. Access captures as a dictionary.")
return entry_by_uuid(video_id, self.videos.values())
return entry_by_uuid(video_id, self.videos)
# CREATING NEW CAPTURES
def _new_output(self, temporary, filename, folder, fmt):
# Generate file name
if not filename:
filename = generate_numbered_basename(self.images.values())
logging.debug(filename)
def _new_output(self, temporary: bool, filename: str, folder: str, fmt: str):
filename = "{}.{}".format(filename, fmt)
# Generate folder
base_folder = self.paths["temp"] if temporary else self.paths["default"]
base_folder: str = self.paths["temp"] if temporary else self.paths["default"]
folder = os.path.join(base_folder, folder)
# Generate file path
filepath = os.path.join(folder, filename)
filepath: str = os.path.join(folder, filename)
# Create capture object
output = CaptureObject(filepath=filepath)
@ -143,7 +109,7 @@ class CaptureManager:
def new_image(
self,
temporary: bool = True,
filename: str = None,
filename: Optional[str] = None,
folder: str = "",
fmt: str = "jpeg",
):
@ -158,23 +124,28 @@ class CaptureManager:
folder (str): Name of the folder in which to store the capture.
fmt (str): Format of the capture.
"""
# Generate file name
if not filename:
filename = generate_numbered_basename(self.images.values())
# Create a new output object
output = self._new_output(temporary, filename, folder, fmt)
# Add an on-delete callback
output.on_delete = self.remove_image
# Update capture list
capture_key = str(output.id)
logging.debug("Adding image %s with key %s", output, capture_key)
self.images[capture_key] = output
logging.debug("Adding image %s with key %s", output, output.id)
self.images[str(output.id)] = output
return output
def new_video(
self,
temporary: bool = False,
filename: str = None,
filename: Optional[str] = None,
folder: str = "",
fmt: str = "h264",
):
) -> CaptureObject:
"""
Create a new video capture object.
@ -186,12 +157,72 @@ class CaptureManager:
folder (str): Name of the folder in which to store the capture.
fmt (str): Format of the capture.
"""
# Generate file name
if not filename:
filename = generate_numbered_basename(self.videos.values())
# Create a new output object
output = self._new_output(temporary, filename, folder, fmt)
# Add an on-delete callback
output.on_delete = self.remove_video
# Update capture list
capture_key = str(output.id)
logging.debug("Adding video %s with key %s", output, capture_key)
self.videos[capture_key] = output
logging.debug("Adding video %s with key %s", output, output.id)
self.videos[str(output.id)] = output
return output
def remove_image(self, capture_obj: CaptureObject, capture_id: str):
logging.info("Deleting capture %s", capture_id)
if capture_id in self.images:
logging.info("Deleting capture object %s", capture_obj)
del self.images[capture_id]
def remove_video(self, capture_obj: CaptureObject, capture_id: str):
logging.info("Deleting capture %s", capture_id)
if capture_id in self.images:
logging.info("Deleting capture object %s", capture_obj)
del self.videos[capture_id]
def entry_by_uuid(
entry_id: Union[str, int, UUID], object_dict: MutableMapping[str, CaptureObject]
) -> Optional[CaptureObject]:
"""Return an object from a list, if <object>.id matches id argument."""
if isinstance(entry_id, str):
key: str = entry_id
elif isinstance(entry_id, UUID):
key = str(entry_id)
elif isinstance(entry_id, int):
key = str(UUID(int=entry_id))
else:
raise TypeError("Argument entry_id must be a string, integer, or UUID object.")
return object_dict.get(key)
def generate_basename() -> str:
"""Return a default filename based on the capture datetime"""
return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
def generate_numbered_basename(
obj_list: Union[ValuesView[CaptureObject], List[CaptureObject]]
) -> str:
"""
This function prevents rapid captures from having clashing generated names.
Our generated names are a datetime string going as far as seconds,
so if you create 2 captures within 1 second then they would have a name
clash. This method handles appending sequential integers to names
that would clash.
"""
initial_basename = generate_basename()
basename = initial_basename
# Handle clashing
iterator = 1
while basename in [obj.basename for obj in obj_list]:
basename = initial_basename + "_{}".format(iterator)
iterator += 1
return basename

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014, 2015 hMatoba
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,9 +0,0 @@
from ._dump import dump
from ._exceptions import *
from ._exif import *
from ._insert import insert
from ._load import load
from ._remove import remove
from ._transplant import transplant
VERSION = "1.1.2"

View file

@ -1,97 +0,0 @@
import struct
from ._exceptions import InvalidImageDataError
def split_into_segments(data):
"""Slices JPEG meta data into a list from JPEG binary data.
"""
if data[0:2] != b"\xff\xd8":
raise InvalidImageDataError("Given data isn't JPEG.")
head = 2
segments = [b"\xff\xd8"]
while 1:
if data[head : head + 2] == b"\xff\xda":
segments.append(data[head:])
break
else:
length = struct.unpack(">H", data[head + 2 : head + 4])[0]
endPoint = head + length + 2
seg = data[head:endPoint]
segments.append(seg)
head = endPoint
if head >= len(data):
raise InvalidImageDataError("Wrong JPEG data.")
return segments
def read_exif_from_file(filename):
"""Slices JPEG meta data into a list from JPEG binary data.
"""
f = open(filename, "rb")
data = f.read(6)
if data[0:2] != b"\xff\xd8":
raise InvalidImageDataError("Given data isn't JPEG.")
head = data[2:6]
HEAD_LENGTH = 4
exif = None
while len(head) == HEAD_LENGTH:
length = struct.unpack(">H", head[2:4])[0]
if head[:2] == b"\xff\xe1":
segment_data = f.read(length - 2)
if segment_data[:4] != b"Exif":
head = f.read(HEAD_LENGTH)
continue
exif = head + segment_data
break
elif head[0:1] == b"\xff":
f.read(length - 2)
head = f.read(HEAD_LENGTH)
else:
break
f.close()
return exif
def get_exif_seg(segments):
"""Returns Exif from JPEG meta data list
"""
for seg in segments:
if seg[0:2] == b"\xff\xe1" and seg[4:10] == b"Exif\x00\x00":
return seg
return None
def merge_segments(segments, exif=b""):
"""Merges Exif with APP0 and APP1 manipulations.
"""
if (
segments[1][0:2] == b"\xff\xe0"
and segments[2][0:2] == b"\xff\xe1"
and segments[2][4:10] == b"Exif\x00\x00"
):
if exif:
segments[2] = exif
segments.pop(1)
elif exif is None:
segments.pop(2)
else:
segments.pop(1)
elif segments[1][0:2] == b"\xff\xe0":
if exif:
segments[1] = exif
elif segments[1][0:2] == b"\xff\xe1" and segments[1][4:10] == b"Exif\x00\x00":
if exif:
segments[1] = exif
elif exif is None:
segments.pop(1)
else:
if exif:
segments.insert(1, exif)
return b"".join(segments)

View file

@ -1,369 +0,0 @@
import copy
import numbers
import struct
from ._common import split_into_segments
from ._exif import TAGS, TYPES, ExifIFD, ImageIFD
TIFF_HEADER_LENGTH = 8
def dump(exif_dict_original):
"""
py:function:: piexif.load(data)
Return exif as bytes.
:param dict exif: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes})
:return: Exif
:rtype: bytes
"""
exif_dict = copy.deepcopy(exif_dict_original)
header = b"Exif\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08"
exif_is = False
gps_is = False
interop_is = False
first_is = False
if "0th" in exif_dict:
zeroth_ifd = exif_dict["0th"]
else:
zeroth_ifd = {}
if (
("Exif" in exif_dict)
and len(exif_dict["Exif"])
or ("Interop" in exif_dict)
and len(exif_dict["Interop"])
):
zeroth_ifd[ImageIFD.ExifTag] = 1
exif_is = True
exif_ifd = exif_dict["Exif"]
if ("Interop" in exif_dict) and len(exif_dict["Interop"]):
exif_ifd[ExifIFD.InteroperabilityTag] = 1
interop_is = True
interop_ifd = exif_dict["Interop"]
elif ExifIFD.InteroperabilityTag in exif_ifd:
exif_ifd.pop(ExifIFD.InteroperabilityTag)
elif ImageIFD.ExifTag in zeroth_ifd:
zeroth_ifd.pop(ImageIFD.ExifTag)
if ("GPS" in exif_dict) and len(exif_dict["GPS"]):
zeroth_ifd[ImageIFD.GPSTag] = 1
gps_is = True
gps_ifd = exif_dict["GPS"]
elif ImageIFD.GPSTag in zeroth_ifd:
zeroth_ifd.pop(ImageIFD.GPSTag)
if (
("1st" in exif_dict)
and ("thumbnail" in exif_dict)
and (exif_dict["thumbnail"] is not None)
):
first_is = True
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] = 1
exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength] = 1
first_ifd = exif_dict["1st"]
zeroth_set = _dict_to_bytes(zeroth_ifd, "0th", 0)
zeroth_length = (
len(zeroth_set[0]) + exif_is * 12 + gps_is * 12 + 4 + len(zeroth_set[1])
)
if exif_is:
exif_set = _dict_to_bytes(exif_ifd, "Exif", zeroth_length)
exif_length = len(exif_set[0]) + interop_is * 12 + len(exif_set[1])
else:
exif_bytes = b""
exif_length = 0
if gps_is:
gps_set = _dict_to_bytes(gps_ifd, "GPS", zeroth_length + exif_length)
gps_bytes = b"".join(gps_set)
gps_length = len(gps_bytes)
else:
gps_bytes = b""
gps_length = 0
if interop_is:
offset = zeroth_length + exif_length + gps_length
interop_set = _dict_to_bytes(interop_ifd, "Interop", offset)
interop_bytes = b"".join(interop_set)
interop_length = len(interop_bytes)
else:
interop_bytes = b""
interop_length = 0
if first_is:
offset = zeroth_length + exif_length + gps_length + interop_length
first_set = _dict_to_bytes(first_ifd, "1st", offset)
thumbnail = _get_thumbnail(exif_dict["thumbnail"])
thumbnail_max_size = 64000
if len(thumbnail) > thumbnail_max_size:
raise ValueError("Given thumbnail is too large. max 64kB")
else:
first_bytes = b""
if exif_is:
pointer_value = TIFF_HEADER_LENGTH + zeroth_length
pointer_str = struct.pack(">I", pointer_value)
key = ImageIFD.ExifTag
key_str = struct.pack(">H", key)
type_str = struct.pack(">H", TYPES.Long)
length_str = struct.pack(">I", 1)
exif_pointer = key_str + type_str + length_str + pointer_str
else:
exif_pointer = b""
if gps_is:
pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length
pointer_str = struct.pack(">I", pointer_value)
key = ImageIFD.GPSTag
key_str = struct.pack(">H", key)
type_str = struct.pack(">H", TYPES.Long)
length_str = struct.pack(">I", 1)
gps_pointer = key_str + type_str + length_str + pointer_str
else:
gps_pointer = b""
if interop_is:
pointer_value = TIFF_HEADER_LENGTH + zeroth_length + exif_length + gps_length
pointer_str = struct.pack(">I", pointer_value)
key = ExifIFD.InteroperabilityTag
key_str = struct.pack(">H", key)
type_str = struct.pack(">H", TYPES.Long)
length_str = struct.pack(">I", 1)
interop_pointer = key_str + type_str + length_str + pointer_str
else:
interop_pointer = b""
if first_is:
pointer_value = (
TIFF_HEADER_LENGTH
+ zeroth_length
+ exif_length
+ gps_length
+ interop_length
)
first_ifd_pointer = struct.pack(">L", pointer_value)
thumbnail_pointer = (
pointer_value + len(first_set[0]) + 24 + 4 + len(first_set[1])
)
thumbnail_p_bytes = b"\x02\x01\x00\x04\x00\x00\x00\x01" + struct.pack(
">L", thumbnail_pointer
)
thumbnail_length_bytes = b"\x02\x02\x00\x04\x00\x00\x00\x01" + struct.pack(
">L", len(thumbnail)
)
first_bytes = (
first_set[0]
+ thumbnail_p_bytes
+ thumbnail_length_bytes
+ b"\x00\x00\x00\x00"
+ first_set[1]
+ thumbnail
)
else:
first_ifd_pointer = b"\x00\x00\x00\x00"
zeroth_bytes = (
zeroth_set[0] + exif_pointer + gps_pointer + first_ifd_pointer + zeroth_set[1]
)
if exif_is:
exif_bytes = exif_set[0] + interop_pointer + exif_set[1]
return header + zeroth_bytes + exif_bytes + gps_bytes + interop_bytes + first_bytes
def _get_thumbnail(jpeg):
segments = split_into_segments(jpeg)
while b"\xff\xe0" <= segments[1][0:2] <= b"\xff\xef":
segments.pop(1)
thumbnail = b"".join(segments)
return thumbnail
def _pack_byte(*args):
return struct.pack("B" * len(args), *args)
def _pack_signed_byte(*args):
return struct.pack("b" * len(args), *args)
def _pack_short(*args):
return struct.pack(">" + "H" * len(args), *args)
def _pack_signed_short(*args):
return struct.pack(">" + "h" * len(args), *args)
def _pack_long(*args):
return struct.pack(">" + "L" * len(args), *args)
def _pack_slong(*args):
return struct.pack(">" + "l" * len(args), *args)
def _pack_float(*args):
return struct.pack(">" + "f" * len(args), *args)
def _pack_double(*args):
return struct.pack(">" + "d" * len(args), *args)
def _value_to_bytes(raw_value, value_type, offset):
four_bytes_over = b""
value_str = b""
if value_type == TYPES.Byte:
length = len(raw_value)
if length <= 4:
value_str = _pack_byte(*raw_value) + b"\x00" * (4 - length)
else:
value_str = struct.pack(">I", offset)
four_bytes_over = _pack_byte(*raw_value)
elif value_type == TYPES.Short:
length = len(raw_value)
if length <= 2:
value_str = _pack_short(*raw_value) + b"\x00\x00" * (2 - length)
else:
value_str = struct.pack(">I", offset)
four_bytes_over = _pack_short(*raw_value)
elif value_type == TYPES.Long:
length = len(raw_value)
if length <= 1:
value_str = _pack_long(*raw_value)
else:
value_str = struct.pack(">I", offset)
four_bytes_over = _pack_long(*raw_value)
elif value_type == TYPES.SLong:
length = len(raw_value)
if length <= 1:
value_str = _pack_slong(*raw_value)
else:
value_str = struct.pack(">I", offset)
four_bytes_over = _pack_slong(*raw_value)
elif value_type == TYPES.Ascii:
try:
new_value = raw_value.encode("latin1") + b"\x00"
except: # pylint: disable=W0702
try:
new_value = raw_value + b"\x00"
except TypeError as e:
raise ValueError("Got invalid type to convert.") from e
length = len(new_value)
if length > 4:
value_str = struct.pack(">I", offset)
four_bytes_over = new_value
else:
value_str = new_value + b"\x00" * (4 - length)
elif value_type == TYPES.Rational:
if isinstance(raw_value[0], numbers.Integral):
length = 1
num, den = raw_value
new_value = struct.pack(">L", num) + struct.pack(">L", den)
elif isinstance(raw_value[0], tuple):
length = len(raw_value)
new_value = b""
for _, val in enumerate(raw_value):
num, den = val
new_value += struct.pack(">L", num) + struct.pack(">L", den)
value_str = struct.pack(">I", offset)
four_bytes_over = new_value
elif value_type == TYPES.SRational:
if isinstance(raw_value[0], numbers.Integral):
length = 1
num, den = raw_value
new_value = struct.pack(">l", num) + struct.pack(">l", den)
elif isinstance(raw_value[0], tuple):
length = len(raw_value)
new_value = b""
for _, val in enumerate(raw_value):
num, den = val
new_value += struct.pack(">l", num) + struct.pack(">l", den)
value_str = struct.pack(">I", offset)
four_bytes_over = new_value
elif value_type == TYPES.Undefined:
length = len(raw_value)
if length > 4:
value_str = struct.pack(">I", offset)
try:
four_bytes_over = b"" + raw_value
except TypeError as e:
raise ValueError("Got invalid type to convert.") from e
else:
try:
value_str = raw_value + b"\x00" * (4 - length)
except TypeError as e:
raise ValueError("Got invalid type to convert.") from e
elif value_type == TYPES.SByte: # Signed Byte
length = len(raw_value)
if length <= 4:
value_str = _pack_signed_byte(*raw_value) + b"\x00" * (4 - length)
else:
value_str = struct.pack(">I", offset)
four_bytes_over = _pack_signed_byte(*raw_value)
elif value_type == TYPES.SShort: # Signed Short
length = len(raw_value)
if length <= 2:
value_str = _pack_signed_short(*raw_value) + b"\x00\x00" * (2 - length)
else:
value_str = struct.pack(">I", offset)
four_bytes_over = _pack_signed_short(*raw_value)
elif value_type == TYPES.Float:
length = len(raw_value)
if length <= 1:
value_str = _pack_float(*raw_value)
else:
value_str = struct.pack(">I", offset)
four_bytes_over = _pack_float(*raw_value)
elif value_type == TYPES.DFloat: # Double
length = len(raw_value)
value_str = struct.pack(">I", offset)
four_bytes_over = _pack_double(*raw_value)
length_str = struct.pack(">I", length)
return length_str, value_str, four_bytes_over
def _dict_to_bytes(ifd_dict, ifd, ifd_offset):
tag_count = len(ifd_dict)
entry_header = struct.pack(">H", tag_count)
if ifd in ("0th", "1st"):
entries_length = 2 + tag_count * 12 + 4
else:
entries_length = 2 + tag_count * 12
entries = b""
values = b""
for _, key in enumerate(sorted(ifd_dict)):
if (ifd == "0th") and (key in (ImageIFD.ExifTag, ImageIFD.GPSTag)):
continue
elif (ifd == "Exif") and (key == ExifIFD.InteroperabilityTag):
continue
elif (ifd == "1st") and (
key
in (ImageIFD.JPEGInterchangeFormat, ImageIFD.JPEGInterchangeFormatLength)
):
continue
raw_value = ifd_dict[key]
key_str = struct.pack(">H", key)
value_type = TAGS[ifd][key]["type"]
type_str = struct.pack(">H", value_type)
four_bytes_over = b""
if isinstance(raw_value, numbers.Integral) or isinstance(raw_value, float):
raw_value = (raw_value,)
offset = TIFF_HEADER_LENGTH + entries_length + ifd_offset + len(values)
try:
length_str, value_str, four_bytes_over = _value_to_bytes(
raw_value, value_type, offset
)
except ValueError as e:
raise ValueError(
'"dump" got wrong type of exif value.\n'
+ "{0} in {1} IFD. Got as {2}.".format(key, ifd, type(ifd_dict[key]))
) from e
entries += key_str + type_str + length_str + value_str
values += four_bytes_over
return (entry_header + entries, values)

View file

@ -1,2 +0,0 @@
class InvalidImageDataError(ValueError):
pass

View file

@ -1,649 +0,0 @@
class TYPES:
Byte = 1
Ascii = 2
Short = 3
Long = 4
Rational = 5
SByte = 6
Undefined = 7
SShort = 8
SLong = 9
SRational = 10
Float = 11
DFloat = 12
TAGS = {
"Image": {
11: {"name": "ProcessingSoftware", "type": TYPES.Ascii},
254: {"name": "NewSubfileType", "type": TYPES.Long},
255: {"name": "SubfileType", "type": TYPES.Short},
256: {"name": "ImageWidth", "type": TYPES.Long},
257: {"name": "ImageLength", "type": TYPES.Long},
258: {"name": "BitsPerSample", "type": TYPES.Short},
259: {"name": "Compression", "type": TYPES.Short},
262: {"name": "PhotometricInterpretation", "type": TYPES.Short},
263: {"name": "Threshholding", "type": TYPES.Short},
264: {"name": "CellWidth", "type": TYPES.Short},
265: {"name": "CellLength", "type": TYPES.Short},
266: {"name": "FillOrder", "type": TYPES.Short},
269: {"name": "DocumentName", "type": TYPES.Ascii},
270: {"name": "ImageDescription", "type": TYPES.Ascii},
271: {"name": "Make", "type": TYPES.Ascii},
272: {"name": "Model", "type": TYPES.Ascii},
273: {"name": "StripOffsets", "type": TYPES.Long},
274: {"name": "Orientation", "type": TYPES.Short},
277: {"name": "SamplesPerPixel", "type": TYPES.Short},
278: {"name": "RowsPerStrip", "type": TYPES.Long},
279: {"name": "StripByteCounts", "type": TYPES.Long},
282: {"name": "XResolution", "type": TYPES.Rational},
283: {"name": "YResolution", "type": TYPES.Rational},
284: {"name": "PlanarConfiguration", "type": TYPES.Short},
290: {"name": "GrayResponseUnit", "type": TYPES.Short},
291: {"name": "GrayResponseCurve", "type": TYPES.Short},
292: {"name": "T4Options", "type": TYPES.Long},
293: {"name": "T6Options", "type": TYPES.Long},
296: {"name": "ResolutionUnit", "type": TYPES.Short},
301: {"name": "TransferFunction", "type": TYPES.Short},
305: {"name": "Software", "type": TYPES.Ascii},
306: {"name": "DateTime", "type": TYPES.Ascii},
315: {"name": "Artist", "type": TYPES.Ascii},
316: {"name": "HostComputer", "type": TYPES.Ascii},
317: {"name": "Predictor", "type": TYPES.Short},
318: {"name": "WhitePoint", "type": TYPES.Rational},
319: {"name": "PrimaryChromaticities", "type": TYPES.Rational},
320: {"name": "ColorMap", "type": TYPES.Short},
321: {"name": "HalftoneHints", "type": TYPES.Short},
322: {"name": "TileWidth", "type": TYPES.Short},
323: {"name": "TileLength", "type": TYPES.Short},
324: {"name": "TileOffsets", "type": TYPES.Short},
325: {"name": "TileByteCounts", "type": TYPES.Short},
330: {"name": "SubIFDs", "type": TYPES.Long},
332: {"name": "InkSet", "type": TYPES.Short},
333: {"name": "InkNames", "type": TYPES.Ascii},
334: {"name": "NumberOfInks", "type": TYPES.Short},
336: {"name": "DotRange", "type": TYPES.Byte},
337: {"name": "TargetPrinter", "type": TYPES.Ascii},
338: {"name": "ExtraSamples", "type": TYPES.Short},
339: {"name": "SampleFormat", "type": TYPES.Short},
340: {"name": "SMinSampleValue", "type": TYPES.Short},
341: {"name": "SMaxSampleValue", "type": TYPES.Short},
342: {"name": "TransferRange", "type": TYPES.Short},
343: {"name": "ClipPath", "type": TYPES.Byte},
344: {"name": "XClipPathUnits", "type": TYPES.Long},
345: {"name": "YClipPathUnits", "type": TYPES.Long},
346: {"name": "Indexed", "type": TYPES.Short},
347: {"name": "JPEGTables", "type": TYPES.Undefined},
351: {"name": "OPIProxy", "type": TYPES.Short},
512: {"name": "JPEGProc", "type": TYPES.Long},
513: {"name": "JPEGInterchangeFormat", "type": TYPES.Long},
514: {"name": "JPEGInterchangeFormatLength", "type": TYPES.Long},
515: {"name": "JPEGRestartInterval", "type": TYPES.Short},
517: {"name": "JPEGLosslessPredictors", "type": TYPES.Short},
518: {"name": "JPEGPointTransforms", "type": TYPES.Short},
519: {"name": "JPEGQTables", "type": TYPES.Long},
520: {"name": "JPEGDCTables", "type": TYPES.Long},
521: {"name": "JPEGACTables", "type": TYPES.Long},
529: {"name": "YCbCrCoefficients", "type": TYPES.Rational},
530: {"name": "YCbCrSubSampling", "type": TYPES.Short},
531: {"name": "YCbCrPositioning", "type": TYPES.Short},
532: {"name": "ReferenceBlackWhite", "type": TYPES.Rational},
700: {"name": "XMLPacket", "type": TYPES.Byte},
18246: {"name": "Rating", "type": TYPES.Short},
18249: {"name": "RatingPercent", "type": TYPES.Short},
32781: {"name": "ImageID", "type": TYPES.Ascii},
33421: {"name": "CFARepeatPatternDim", "type": TYPES.Short},
33422: {"name": "CFAPattern", "type": TYPES.Byte},
33423: {"name": "BatteryLevel", "type": TYPES.Rational},
33432: {"name": "Copyright", "type": TYPES.Ascii},
33434: {"name": "ExposureTime", "type": TYPES.Rational},
34377: {"name": "ImageResources", "type": TYPES.Byte},
34665: {"name": "ExifTag", "type": TYPES.Long},
34675: {"name": "InterColorProfile", "type": TYPES.Undefined},
34853: {"name": "GPSTag", "type": TYPES.Long},
34857: {"name": "Interlace", "type": TYPES.Short},
34858: {"name": "TimeZoneOffset", "type": TYPES.Long},
34859: {"name": "SelfTimerMode", "type": TYPES.Short},
37387: {"name": "FlashEnergy", "type": TYPES.Rational},
37388: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined},
37389: {"name": "Noise", "type": TYPES.Undefined},
37390: {"name": "FocalPlaneXResolution", "type": TYPES.Rational},
37391: {"name": "FocalPlaneYResolution", "type": TYPES.Rational},
37392: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short},
37393: {"name": "ImageNumber", "type": TYPES.Long},
37394: {"name": "SecurityClassification", "type": TYPES.Ascii},
37395: {"name": "ImageHistory", "type": TYPES.Ascii},
37397: {"name": "ExposureIndex", "type": TYPES.Rational},
37398: {"name": "TIFFEPStandardID", "type": TYPES.Byte},
37399: {"name": "SensingMethod", "type": TYPES.Short},
40091: {"name": "XPTitle", "type": TYPES.Byte},
40092: {"name": "XPComment", "type": TYPES.Byte},
40093: {"name": "XPAuthor", "type": TYPES.Byte},
40094: {"name": "XPKeywords", "type": TYPES.Byte},
40095: {"name": "XPSubject", "type": TYPES.Byte},
50341: {"name": "PrintImageMatching", "type": TYPES.Undefined},
50706: {"name": "DNGVersion", "type": TYPES.Byte},
50707: {"name": "DNGBackwardVersion", "type": TYPES.Byte},
50708: {"name": "UniqueCameraModel", "type": TYPES.Ascii},
50709: {"name": "LocalizedCameraModel", "type": TYPES.Byte},
50710: {"name": "CFAPlaneColor", "type": TYPES.Byte},
50711: {"name": "CFALayout", "type": TYPES.Short},
50712: {"name": "LinearizationTable", "type": TYPES.Short},
50713: {"name": "BlackLevelRepeatDim", "type": TYPES.Short},
50714: {"name": "BlackLevel", "type": TYPES.Rational},
50715: {"name": "BlackLevelDeltaH", "type": TYPES.SRational},
50716: {"name": "BlackLevelDeltaV", "type": TYPES.SRational},
50717: {"name": "WhiteLevel", "type": TYPES.Short},
50718: {"name": "DefaultScale", "type": TYPES.Rational},
50719: {"name": "DefaultCropOrigin", "type": TYPES.Short},
50720: {"name": "DefaultCropSize", "type": TYPES.Short},
50721: {"name": "ColorMatrix1", "type": TYPES.SRational},
50722: {"name": "ColorMatrix2", "type": TYPES.SRational},
50723: {"name": "CameraCalibration1", "type": TYPES.SRational},
50724: {"name": "CameraCalibration2", "type": TYPES.SRational},
50725: {"name": "ReductionMatrix1", "type": TYPES.SRational},
50726: {"name": "ReductionMatrix2", "type": TYPES.SRational},
50727: {"name": "AnalogBalance", "type": TYPES.Rational},
50728: {"name": "AsShotNeutral", "type": TYPES.Short},
50729: {"name": "AsShotWhiteXY", "type": TYPES.Rational},
50730: {"name": "BaselineExposure", "type": TYPES.SRational},
50731: {"name": "BaselineNoise", "type": TYPES.Rational},
50732: {"name": "BaselineSharpness", "type": TYPES.Rational},
50733: {"name": "BayerGreenSplit", "type": TYPES.Long},
50734: {"name": "LinearResponseLimit", "type": TYPES.Rational},
50735: {"name": "CameraSerialNumber", "type": TYPES.Ascii},
50736: {"name": "LensInfo", "type": TYPES.Rational},
50737: {"name": "ChromaBlurRadius", "type": TYPES.Rational},
50738: {"name": "AntiAliasStrength", "type": TYPES.Rational},
50739: {"name": "ShadowScale", "type": TYPES.SRational},
50740: {"name": "DNGPrivateData", "type": TYPES.Byte},
50741: {"name": "MakerNoteSafety", "type": TYPES.Short},
50778: {"name": "CalibrationIlluminant1", "type": TYPES.Short},
50779: {"name": "CalibrationIlluminant2", "type": TYPES.Short},
50780: {"name": "BestQualityScale", "type": TYPES.Rational},
50781: {"name": "RawDataUniqueID", "type": TYPES.Byte},
50827: {"name": "OriginalRawFileName", "type": TYPES.Byte},
50828: {"name": "OriginalRawFileData", "type": TYPES.Undefined},
50829: {"name": "ActiveArea", "type": TYPES.Short},
50830: {"name": "MaskedAreas", "type": TYPES.Short},
50831: {"name": "AsShotICCProfile", "type": TYPES.Undefined},
50832: {"name": "AsShotPreProfileMatrix", "type": TYPES.SRational},
50833: {"name": "CurrentICCProfile", "type": TYPES.Undefined},
50834: {"name": "CurrentPreProfileMatrix", "type": TYPES.SRational},
50879: {"name": "ColorimetricReference", "type": TYPES.Short},
50931: {"name": "CameraCalibrationSignature", "type": TYPES.Byte},
50932: {"name": "ProfileCalibrationSignature", "type": TYPES.Byte},
50934: {"name": "AsShotProfileName", "type": TYPES.Byte},
50935: {"name": "NoiseReductionApplied", "type": TYPES.Rational},
50936: {"name": "ProfileName", "type": TYPES.Byte},
50937: {"name": "ProfileHueSatMapDims", "type": TYPES.Long},
50938: {"name": "ProfileHueSatMapData1", "type": TYPES.Float},
50939: {"name": "ProfileHueSatMapData2", "type": TYPES.Float},
50940: {"name": "ProfileToneCurve", "type": TYPES.Float},
50941: {"name": "ProfileEmbedPolicy", "type": TYPES.Long},
50942: {"name": "ProfileCopyright", "type": TYPES.Byte},
50964: {"name": "ForwardMatrix1", "type": TYPES.SRational},
50965: {"name": "ForwardMatrix2", "type": TYPES.SRational},
50966: {"name": "PreviewApplicationName", "type": TYPES.Byte},
50967: {"name": "PreviewApplicationVersion", "type": TYPES.Byte},
50968: {"name": "PreviewSettingsName", "type": TYPES.Byte},
50969: {"name": "PreviewSettingsDigest", "type": TYPES.Byte},
50970: {"name": "PreviewColorSpace", "type": TYPES.Long},
50971: {"name": "PreviewDateTime", "type": TYPES.Ascii},
50972: {"name": "RawImageDigest", "type": TYPES.Undefined},
50973: {"name": "OriginalRawFileDigest", "type": TYPES.Undefined},
50974: {"name": "SubTileBlockSize", "type": TYPES.Long},
50975: {"name": "RowInterleaveFactor", "type": TYPES.Long},
50981: {"name": "ProfileLookTableDims", "type": TYPES.Long},
50982: {"name": "ProfileLookTableData", "type": TYPES.Float},
51008: {"name": "OpcodeList1", "type": TYPES.Undefined},
51009: {"name": "OpcodeList2", "type": TYPES.Undefined},
51022: {"name": "OpcodeList3", "type": TYPES.Undefined},
60606: {"name": "ZZZTestSlong1", "type": TYPES.SLong},
60607: {"name": "ZZZTestSlong2", "type": TYPES.SLong},
60608: {"name": "ZZZTestSByte", "type": TYPES.SByte},
60609: {"name": "ZZZTestSShort", "type": TYPES.SShort},
60610: {"name": "ZZZTestDFloat", "type": TYPES.DFloat},
},
"Exif": {
33434: {"name": "ExposureTime", "type": TYPES.Rational},
33437: {"name": "FNumber", "type": TYPES.Rational},
34850: {"name": "ExposureProgram", "type": TYPES.Short},
34852: {"name": "SpectralSensitivity", "type": TYPES.Ascii},
34855: {"name": "ISOSpeedRatings", "type": TYPES.Short},
34856: {"name": "OECF", "type": TYPES.Undefined},
34864: {"name": "SensitivityType", "type": TYPES.Short},
34865: {"name": "StandardOutputSensitivity", "type": TYPES.Long},
34866: {"name": "RecommendedExposureIndex", "type": TYPES.Long},
34867: {"name": "ISOSpeed", "type": TYPES.Long},
34868: {"name": "ISOSpeedLatitudeyyy", "type": TYPES.Long},
34869: {"name": "ISOSpeedLatitudezzz", "type": TYPES.Long},
36864: {"name": "ExifVersion", "type": TYPES.Undefined},
36867: {"name": "DateTimeOriginal", "type": TYPES.Ascii},
36868: {"name": "DateTimeDigitized", "type": TYPES.Ascii},
36880: {"name": "OffsetTime", "type": TYPES.Ascii},
36881: {"name": "OffsetTimeOriginal", "type": TYPES.Ascii},
36882: {"name": "OffsetTimeDigitized", "type": TYPES.Ascii},
37121: {"name": "ComponentsConfiguration", "type": TYPES.Undefined},
37122: {"name": "CompressedBitsPerPixel", "type": TYPES.Rational},
37377: {"name": "ShutterSpeedValue", "type": TYPES.SRational},
37378: {"name": "ApertureValue", "type": TYPES.Rational},
37379: {"name": "BrightnessValue", "type": TYPES.SRational},
37380: {"name": "ExposureBiasValue", "type": TYPES.SRational},
37381: {"name": "MaxApertureValue", "type": TYPES.Rational},
37382: {"name": "SubjectDistance", "type": TYPES.Rational},
37383: {"name": "MeteringMode", "type": TYPES.Short},
37384: {"name": "LightSource", "type": TYPES.Short},
37385: {"name": "Flash", "type": TYPES.Short},
37386: {"name": "FocalLength", "type": TYPES.Rational},
37396: {"name": "SubjectArea", "type": TYPES.Short},
37500: {"name": "MakerNote", "type": TYPES.Undefined},
37510: {"name": "UserComment", "type": TYPES.Undefined},
37520: {"name": "SubSecTime", "type": TYPES.Ascii},
37521: {"name": "SubSecTimeOriginal", "type": TYPES.Ascii},
37522: {"name": "SubSecTimeDigitized", "type": TYPES.Ascii},
37888: {"name": "Temperature", "type": TYPES.SRational},
37889: {"name": "Humidity", "type": TYPES.Rational},
37890: {"name": "Pressure", "type": TYPES.Rational},
37891: {"name": "WaterDepth", "type": TYPES.SRational},
37892: {"name": "Acceleration", "type": TYPES.Rational},
37893: {"name": "CameraElevationAngle", "type": TYPES.SRational},
40960: {"name": "FlashpixVersion", "type": TYPES.Undefined},
40961: {"name": "ColorSpace", "type": TYPES.Short},
40962: {"name": "PixelXDimension", "type": TYPES.Long},
40963: {"name": "PixelYDimension", "type": TYPES.Long},
40964: {"name": "RelatedSoundFile", "type": TYPES.Ascii},
40965: {"name": "InteroperabilityTag", "type": TYPES.Long},
41483: {"name": "FlashEnergy", "type": TYPES.Rational},
41484: {"name": "SpatialFrequencyResponse", "type": TYPES.Undefined},
41486: {"name": "FocalPlaneXResolution", "type": TYPES.Rational},
41487: {"name": "FocalPlaneYResolution", "type": TYPES.Rational},
41488: {"name": "FocalPlaneResolutionUnit", "type": TYPES.Short},
41492: {"name": "SubjectLocation", "type": TYPES.Short},
41493: {"name": "ExposureIndex", "type": TYPES.Rational},
41495: {"name": "SensingMethod", "type": TYPES.Short},
41728: {"name": "FileSource", "type": TYPES.Undefined},
41729: {"name": "SceneType", "type": TYPES.Undefined},
41730: {"name": "CFAPattern", "type": TYPES.Undefined},
41985: {"name": "CustomRendered", "type": TYPES.Short},
41986: {"name": "ExposureMode", "type": TYPES.Short},
41987: {"name": "WhiteBalance", "type": TYPES.Short},
41988: {"name": "DigitalZoomRatio", "type": TYPES.Rational},
41989: {"name": "FocalLengthIn35mmFilm", "type": TYPES.Short},
41990: {"name": "SceneCaptureType", "type": TYPES.Short},
41991: {"name": "GainControl", "type": TYPES.Short},
41992: {"name": "Contrast", "type": TYPES.Short},
41993: {"name": "Saturation", "type": TYPES.Short},
41994: {"name": "Sharpness", "type": TYPES.Short},
41995: {"name": "DeviceSettingDescription", "type": TYPES.Undefined},
41996: {"name": "SubjectDistanceRange", "type": TYPES.Short},
42016: {"name": "ImageUniqueID", "type": TYPES.Ascii},
42032: {"name": "CameraOwnerName", "type": TYPES.Ascii},
42033: {"name": "BodySerialNumber", "type": TYPES.Ascii},
42034: {"name": "LensSpecification", "type": TYPES.Rational},
42035: {"name": "LensMake", "type": TYPES.Ascii},
42036: {"name": "LensModel", "type": TYPES.Ascii},
42037: {"name": "LensSerialNumber", "type": TYPES.Ascii},
42240: {"name": "Gamma", "type": TYPES.Rational},
},
"GPS": {
0: {"name": "GPSVersionID", "type": TYPES.Byte},
1: {"name": "GPSLatitudeRef", "type": TYPES.Ascii},
2: {"name": "GPSLatitude", "type": TYPES.Rational},
3: {"name": "GPSLongitudeRef", "type": TYPES.Ascii},
4: {"name": "GPSLongitude", "type": TYPES.Rational},
5: {"name": "GPSAltitudeRef", "type": TYPES.Byte},
6: {"name": "GPSAltitude", "type": TYPES.Rational},
7: {"name": "GPSTimeStamp", "type": TYPES.Rational},
8: {"name": "GPSSatellites", "type": TYPES.Ascii},
9: {"name": "GPSStatus", "type": TYPES.Ascii},
10: {"name": "GPSMeasureMode", "type": TYPES.Ascii},
11: {"name": "GPSDOP", "type": TYPES.Rational},
12: {"name": "GPSSpeedRef", "type": TYPES.Ascii},
13: {"name": "GPSSpeed", "type": TYPES.Rational},
14: {"name": "GPSTrackRef", "type": TYPES.Ascii},
15: {"name": "GPSTrack", "type": TYPES.Rational},
16: {"name": "GPSImgDirectionRef", "type": TYPES.Ascii},
17: {"name": "GPSImgDirection", "type": TYPES.Rational},
18: {"name": "GPSMapDatum", "type": TYPES.Ascii},
19: {"name": "GPSDestLatitudeRef", "type": TYPES.Ascii},
20: {"name": "GPSDestLatitude", "type": TYPES.Rational},
21: {"name": "GPSDestLongitudeRef", "type": TYPES.Ascii},
22: {"name": "GPSDestLongitude", "type": TYPES.Rational},
23: {"name": "GPSDestBearingRef", "type": TYPES.Ascii},
24: {"name": "GPSDestBearing", "type": TYPES.Rational},
25: {"name": "GPSDestDistanceRef", "type": TYPES.Ascii},
26: {"name": "GPSDestDistance", "type": TYPES.Rational},
27: {"name": "GPSProcessingMethod", "type": TYPES.Undefined},
28: {"name": "GPSAreaInformation", "type": TYPES.Undefined},
29: {"name": "GPSDateStamp", "type": TYPES.Ascii},
30: {"name": "GPSDifferential", "type": TYPES.Short},
31: {"name": "GPSHPositioningError", "type": TYPES.Rational},
},
"Interop": {1: {"name": "InteroperabilityIndex", "type": TYPES.Ascii}},
}
TAGS["0th"] = TAGS["Image"]
TAGS["1st"] = TAGS["Image"]
class ImageIFD:
"""Exif tag number reference - 0th IFD"""
ProcessingSoftware = 11
NewSubfileType = 254
SubfileType = 255
ImageWidth = 256
ImageLength = 257
BitsPerSample = 258
Compression = 259
PhotometricInterpretation = 262
Threshholding = 263
CellWidth = 264
CellLength = 265
FillOrder = 266
DocumentName = 269
ImageDescription = 270
Make = 271
Model = 272
StripOffsets = 273
Orientation = 274
SamplesPerPixel = 277
RowsPerStrip = 278
StripByteCounts = 279
XResolution = 282
YResolution = 283
PlanarConfiguration = 284
GrayResponseUnit = 290
GrayResponseCurve = 291
T4Options = 292
T6Options = 293
ResolutionUnit = 296
TransferFunction = 301
Software = 305
DateTime = 306
Artist = 315
HostComputer = 316
Predictor = 317
WhitePoint = 318
PrimaryChromaticities = 319
ColorMap = 320
HalftoneHints = 321
TileWidth = 322
TileLength = 323
TileOffsets = 324
TileByteCounts = 325
SubIFDs = 330
InkSet = 332
InkNames = 333
NumberOfInks = 334
DotRange = 336
TargetPrinter = 337
ExtraSamples = 338
SampleFormat = 339
SMinSampleValue = 340
SMaxSampleValue = 341
TransferRange = 342
ClipPath = 343
XClipPathUnits = 344
YClipPathUnits = 345
Indexed = 346
JPEGTables = 347
OPIProxy = 351
JPEGProc = 512
JPEGInterchangeFormat = 513
JPEGInterchangeFormatLength = 514
JPEGRestartInterval = 515
JPEGLosslessPredictors = 517
JPEGPointTransforms = 518
JPEGQTables = 519
JPEGDCTables = 520
JPEGACTables = 521
YCbCrCoefficients = 529
YCbCrSubSampling = 530
YCbCrPositioning = 531
ReferenceBlackWhite = 532
XMLPacket = 700
Rating = 18246
RatingPercent = 18249
ImageID = 32781
CFARepeatPatternDim = 33421
CFAPattern = 33422
BatteryLevel = 33423
Copyright = 33432
ExposureTime = 33434
ImageResources = 34377
ExifTag = 34665
InterColorProfile = 34675
GPSTag = 34853
Interlace = 34857
TimeZoneOffset = 34858
SelfTimerMode = 34859
FlashEnergy = 37387
SpatialFrequencyResponse = 37388
Noise = 37389
FocalPlaneXResolution = 37390
FocalPlaneYResolution = 37391
FocalPlaneResolutionUnit = 37392
ImageNumber = 37393
SecurityClassification = 37394
ImageHistory = 37395
ExposureIndex = 37397
TIFFEPStandardID = 37398
SensingMethod = 37399
XPTitle = 40091
XPComment = 40092
XPAuthor = 40093
XPKeywords = 40094
XPSubject = 40095
PrintImageMatching = 50341
DNGVersion = 50706
DNGBackwardVersion = 50707
UniqueCameraModel = 50708
LocalizedCameraModel = 50709
CFAPlaneColor = 50710
CFALayout = 50711
LinearizationTable = 50712
BlackLevelRepeatDim = 50713
BlackLevel = 50714
BlackLevelDeltaH = 50715
BlackLevelDeltaV = 50716
WhiteLevel = 50717
DefaultScale = 50718
DefaultCropOrigin = 50719
DefaultCropSize = 50720
ColorMatrix1 = 50721
ColorMatrix2 = 50722
CameraCalibration1 = 50723
CameraCalibration2 = 50724
ReductionMatrix1 = 50725
ReductionMatrix2 = 50726
AnalogBalance = 50727
AsShotNeutral = 50728
AsShotWhiteXY = 50729
BaselineExposure = 50730
BaselineNoise = 50731
BaselineSharpness = 50732
BayerGreenSplit = 50733
LinearResponseLimit = 50734
CameraSerialNumber = 50735
LensInfo = 50736
ChromaBlurRadius = 50737
AntiAliasStrength = 50738
ShadowScale = 50739
DNGPrivateData = 50740
MakerNoteSafety = 50741
CalibrationIlluminant1 = 50778
CalibrationIlluminant2 = 50779
BestQualityScale = 50780
RawDataUniqueID = 50781
OriginalRawFileName = 50827
OriginalRawFileData = 50828
ActiveArea = 50829
MaskedAreas = 50830
AsShotICCProfile = 50831
AsShotPreProfileMatrix = 50832
CurrentICCProfile = 50833
CurrentPreProfileMatrix = 50834
ColorimetricReference = 50879
CameraCalibrationSignature = 50931
ProfileCalibrationSignature = 50932
AsShotProfileName = 50934
NoiseReductionApplied = 50935
ProfileName = 50936
ProfileHueSatMapDims = 50937
ProfileHueSatMapData1 = 50938
ProfileHueSatMapData2 = 50939
ProfileToneCurve = 50940
ProfileEmbedPolicy = 50941
ProfileCopyright = 50942
ForwardMatrix1 = 50964
ForwardMatrix2 = 50965
PreviewApplicationName = 50966
PreviewApplicationVersion = 50967
PreviewSettingsName = 50968
PreviewSettingsDigest = 50969
PreviewColorSpace = 50970
PreviewDateTime = 50971
RawImageDigest = 50972
OriginalRawFileDigest = 50973
SubTileBlockSize = 50974
RowInterleaveFactor = 50975
ProfileLookTableDims = 50981
ProfileLookTableData = 50982
OpcodeList1 = 51008
OpcodeList2 = 51009
OpcodeList3 = 51022
NoiseProfile = 51041
ZZZTestSlong1 = 60606
ZZZTestSlong2 = 60607
ZZZTestSByte = 60608
ZZZTestSShort = 60609
ZZZTestDFloat = 60610
class ExifIFD:
"""Exif tag number reference - Exif IFD"""
ExposureTime = 33434
FNumber = 33437
ExposureProgram = 34850
SpectralSensitivity = 34852
ISOSpeedRatings = 34855
OECF = 34856
SensitivityType = 34864
StandardOutputSensitivity = 34865
RecommendedExposureIndex = 34866
ISOSpeed = 34867
ISOSpeedLatitudeyyy = 34868
ISOSpeedLatitudezzz = 34869
ExifVersion = 36864
DateTimeOriginal = 36867
DateTimeDigitized = 36868
OffsetTime = 36880
OffsetTimeOriginal = 36881
OffsetTimeDigitized = 36882
ComponentsConfiguration = 37121
CompressedBitsPerPixel = 37122
ShutterSpeedValue = 37377
ApertureValue = 37378
BrightnessValue = 37379
ExposureBiasValue = 37380
MaxApertureValue = 37381
SubjectDistance = 37382
MeteringMode = 37383
LightSource = 37384
Flash = 37385
FocalLength = 37386
Temperature = 37888
Humidity = 37889
Pressure = 37890
WaterDepth = 37891
Acceleration = 37892
CameraElevationAngle = 37893
SubjectArea = 37396
MakerNote = 37500
UserComment = 37510
SubSecTime = 37520
SubSecTimeOriginal = 37521
SubSecTimeDigitized = 37522
FlashpixVersion = 40960
ColorSpace = 40961
PixelXDimension = 40962
PixelYDimension = 40963
RelatedSoundFile = 40964
InteroperabilityTag = 40965
FlashEnergy = 41483
SpatialFrequencyResponse = 41484
FocalPlaneXResolution = 41486
FocalPlaneYResolution = 41487
FocalPlaneResolutionUnit = 41488
SubjectLocation = 41492
ExposureIndex = 41493
SensingMethod = 41495
FileSource = 41728
SceneType = 41729
CFAPattern = 41730
CustomRendered = 41985
ExposureMode = 41986
WhiteBalance = 41987
DigitalZoomRatio = 41988
FocalLengthIn35mmFilm = 41989
SceneCaptureType = 41990
GainControl = 41991
Contrast = 41992
Saturation = 41993
Sharpness = 41994
DeviceSettingDescription = 41995
SubjectDistanceRange = 41996
ImageUniqueID = 42016
CameraOwnerName = 42032
BodySerialNumber = 42033
LensSpecification = 42034
LensMake = 42035
LensModel = 42036
LensSerialNumber = 42037
Gamma = 42240
class GPSIFD:
"""Exif tag number reference - GPS IFD"""
GPSVersionID = 0
GPSLatitudeRef = 1
GPSLatitude = 2
GPSLongitudeRef = 3
GPSLongitude = 4
GPSAltitudeRef = 5
GPSAltitude = 6
GPSTimeStamp = 7
GPSSatellites = 8
GPSStatus = 9
GPSMeasureMode = 10
GPSDOP = 11
GPSSpeedRef = 12
GPSSpeed = 13
GPSTrackRef = 14
GPSTrack = 15
GPSImgDirectionRef = 16
GPSImgDirection = 17
GPSMapDatum = 18
GPSDestLatitudeRef = 19
GPSDestLatitude = 20
GPSDestLongitudeRef = 21
GPSDestLongitude = 22
GPSDestBearingRef = 23
GPSDestBearing = 24
GPSDestDistanceRef = 25
GPSDestDistance = 26
GPSProcessingMethod = 27
GPSAreaInformation = 28
GPSDateStamp = 29
GPSDifferential = 30
GPSHPositioningError = 31
class InteropIFD:
"""Exif tag number reference - Interoperability IFD"""
InteroperabilityIndex = 1

View file

@ -1,61 +0,0 @@
import io
import struct
import sys
from . import _webp
from ._common import merge_segments, split_into_segments
from ._exceptions import InvalidImageDataError
def insert(exif, image, new_file=None):
"""
py:function:: piexif.insert(exif_bytes, filename)
Insert exif into JPEG.
:param bytes exif_bytes: Exif as bytes
:param str filename: JPEG
"""
if exif[0:6] != b"\x45\x78\x69\x66\x00\x00":
raise ValueError("Given data is not exif data")
output_file = False
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2
maybe_image = sys.version_info >= (3, 0, 0) or isinstance(image, str)
if maybe_image and image[0:2] == b"\xff\xd8":
image_data = image
file_type = "jpeg"
elif maybe_image and image[0:4] == b"RIFF" and image[8:12] == b"WEBP":
image_data = image
file_type = "webp"
else:
with open(image, "rb") as f:
image_data = f.read()
if image_data[0:2] == b"\xff\xd8":
file_type = "jpeg"
elif image_data[0:4] == b"RIFF" and image_data[8:12] == b"WEBP":
file_type = "webp"
else:
raise InvalidImageDataError
output_file = True
if file_type == "jpeg":
exif = b"\xff\xe1" + struct.pack(">H", len(exif) + 2) + exif
segments = split_into_segments(image_data)
new_data = merge_segments(segments, exif)
elif file_type == "webp":
exif = exif[6:]
new_data = _webp.insert(image_data, exif)
if isinstance(new_file, io.BytesIO):
new_file.write(new_data)
new_file.seek(0)
elif new_file:
with open(new_file, "wb+") as f:
f.write(new_data)
elif output_file:
with open(image, "wb+") as f:
f.write(new_data)
else:
raise ValueError("Give a 3rd argument to 'insert' to output file")

View file

@ -1,324 +0,0 @@
import struct
import sys
from . import _webp
from ._common import get_exif_seg, read_exif_from_file, split_into_segments
from ._exceptions import InvalidImageDataError
from ._exif import TAGS, TYPES, ExifIFD, ImageIFD
LITTLE_ENDIAN = b"\x49\x49"
def load(input_data, key_is_name=False):
"""
py:function:: piexif.load(filename)
Return exif data as dict. Keys(IFD name), be contained, are "0th", "Exif", "GPS", "Interop", "1st", and "thumbnail". Without "thumbnail", the value is dict(tag name/tag value). "thumbnail" value is JPEG as bytes.
:param str filename: JPEG or TIFF
:return: Exif data({"0th":dict, "Exif":dict, "GPS":dict, "Interop":dict, "1st":dict, "thumbnail":bytes})
:rtype: dict
"""
exif_dict = {
"0th": {},
"Exif": {},
"GPS": {},
"Interop": {},
"1st": {},
"thumbnail": None,
}
exifReader = _ExifReader(input_data)
if exifReader.tiftag is None:
return exif_dict
if exifReader.tiftag[0:2] == LITTLE_ENDIAN:
exifReader.endian_mark = "<"
else:
exifReader.endian_mark = ">"
pointer = struct.unpack(exifReader.endian_mark + "L", exifReader.tiftag[4:8])[0]
exif_dict["0th"] = exifReader.get_ifd_dict(pointer, "0th")
first_ifd_pointer = exif_dict["0th"].pop("first_ifd_pointer")
if ImageIFD.ExifTag in exif_dict["0th"]:
pointer = exif_dict["0th"][ImageIFD.ExifTag]
exif_dict["Exif"] = exifReader.get_ifd_dict(pointer, "Exif")
if ImageIFD.GPSTag in exif_dict["0th"]:
pointer = exif_dict["0th"][ImageIFD.GPSTag]
exif_dict["GPS"] = exifReader.get_ifd_dict(pointer, "GPS")
if ExifIFD.InteroperabilityTag in exif_dict["Exif"]:
pointer = exif_dict["Exif"][ExifIFD.InteroperabilityTag]
exif_dict["Interop"] = exifReader.get_ifd_dict(pointer, "Interop")
if first_ifd_pointer != b"\x00\x00\x00\x00":
pointer = struct.unpack(exifReader.endian_mark + "L", first_ifd_pointer)[0]
exif_dict["1st"] = exifReader.get_ifd_dict(pointer, "1st")
if (
ImageIFD.JPEGInterchangeFormat in exif_dict["1st"]
and ImageIFD.JPEGInterchangeFormatLength in exif_dict["1st"]
):
end = (
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat]
+ exif_dict["1st"][ImageIFD.JPEGInterchangeFormatLength]
)
thumb = exifReader.tiftag[
exif_dict["1st"][ImageIFD.JPEGInterchangeFormat] : end
]
exif_dict["thumbnail"] = thumb
if key_is_name:
exif_dict = _get_key_name_dict(exif_dict)
return exif_dict
class _ExifReader(object):
def __init__(self, data):
# Prevents "UnicodeWarning: Unicode equal comparison failed" warnings on Python 2
maybe_image = sys.version_info >= (3, 0, 0) or isinstance(data, str)
if maybe_image and data[0:2] == b"\xff\xd8": # JPEG
segments = split_into_segments(data)
app1 = get_exif_seg(segments)
if app1:
self.tiftag = app1[10:]
else:
self.tiftag = None
elif maybe_image and data[0:2] in (b"\x49\x49", b"\x4d\x4d"): # TIFF
self.tiftag = data
elif maybe_image and data[0:4] == b"RIFF" and data[8:12] == b"WEBP":
self.tiftag = _webp.get_exif(data)
elif maybe_image and data[0:4] == b"Exif": # Exif
self.tiftag = data[6:]
else:
with open(data, "rb") as f:
magic_number = f.read(2)
if magic_number == b"\xff\xd8": # JPEG
app1 = read_exif_from_file(data)
if app1:
self.tiftag = app1[10:]
else:
self.tiftag = None
elif magic_number in (b"\x49\x49", b"\x4d\x4d"): # TIFF
with open(data, "rb") as f:
self.tiftag = f.read()
else:
with open(data, "rb") as f:
header = f.read(12)
if header[0:4] == b"RIFF" and header[8:12] == b"WEBP":
with open(data, "rb") as f:
file_data = f.read()
self.tiftag = _webp.get_exif(file_data)
else:
raise InvalidImageDataError("Given file is neither JPEG nor TIFF.")
self.endian_mark = None
def get_ifd_dict(self, pointer, ifd_name, read_unknown=False):
ifd_dict = {}
tag_count = struct.unpack(
self.endian_mark + "H", self.tiftag[pointer : pointer + 2]
)[0]
offset = pointer + 2
if ifd_name in ["0th", "1st"]:
t = "Image"
else:
t = ifd_name
p_and_value = []
for x in range(tag_count):
pointer = offset + 12 * x
tag = struct.unpack(
self.endian_mark + "H", self.tiftag[pointer : pointer + 2]
)[0]
value_type = struct.unpack(
self.endian_mark + "H", self.tiftag[pointer + 2 : pointer + 4]
)[0]
value_num = struct.unpack(
self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8]
)[0]
value = self.tiftag[pointer + 8 : pointer + 12]
p_and_value.append((pointer, value_type, value_num, value))
v_set = (value_type, value_num, value, tag)
if tag in TAGS[t]:
ifd_dict[tag] = self.convert_value(v_set)
elif read_unknown:
ifd_dict[tag] = (v_set[0], v_set[1], v_set[2], self.tiftag)
# else:
# pass
if ifd_name == "0th":
pointer = offset + 12 * tag_count
ifd_dict["first_ifd_pointer"] = self.tiftag[pointer : pointer + 4]
return ifd_dict
def convert_value(self, val):
data = None
t = val[0]
length = val[1]
value = val[2]
if t == TYPES.Byte: # BYTE
if length > 4:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(
"B" * length, self.tiftag[pointer : pointer + length]
)
else:
data = struct.unpack("B" * length, value[0:length])
elif t == TYPES.Ascii: # ASCII
if length > 4:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = self.tiftag[pointer : pointer + length - 1]
else:
data = value[0 : length - 1]
elif t == TYPES.Short: # SHORT
if length > 2:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(
self.endian_mark + "H" * length,
self.tiftag[pointer : pointer + length * 2],
)
else:
data = struct.unpack(
self.endian_mark + "H" * length, value[0 : length * 2]
)
elif t == TYPES.Long: # LONG
if length > 1:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(
self.endian_mark + "L" * length,
self.tiftag[pointer : pointer + length * 4],
)
else:
data = struct.unpack(self.endian_mark + "L" * length, value)
elif t == TYPES.Rational: # RATIONAL
pointer = struct.unpack(self.endian_mark + "L", value)[0]
if length > 1:
data = tuple(
(
struct.unpack(
self.endian_mark + "L",
self.tiftag[pointer + x * 8 : pointer + 4 + x * 8],
)[0],
struct.unpack(
self.endian_mark + "L",
self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8],
)[0],
)
for x in range(length)
)
else:
data = (
struct.unpack(
self.endian_mark + "L", self.tiftag[pointer : pointer + 4]
)[0],
struct.unpack(
self.endian_mark + "L", self.tiftag[pointer + 4 : pointer + 8]
)[0],
)
elif t == TYPES.SByte: # SIGNED BYTES
if length > 4:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(
"b" * length, self.tiftag[pointer : pointer + length]
)
else:
data = struct.unpack("b" * length, value[0:length])
elif t == TYPES.Undefined: # UNDEFINED BYTES
if length > 4:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = self.tiftag[pointer : pointer + length]
else:
data = value[0:length]
elif t == TYPES.SShort: # SIGNED SHORT
if length > 2:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(
self.endian_mark + "h" * length,
self.tiftag[pointer : pointer + length * 2],
)
else:
data = struct.unpack(
self.endian_mark + "h" * length, value[0 : length * 2]
)
elif t == TYPES.SLong: # SLONG
if length > 1:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(
self.endian_mark + "l" * length,
self.tiftag[pointer : pointer + length * 4],
)
else:
data = struct.unpack(self.endian_mark + "l" * length, value)
elif t == TYPES.SRational: # SRATIONAL
pointer = struct.unpack(self.endian_mark + "L", value)[0]
if length > 1:
data = tuple(
(
struct.unpack(
self.endian_mark + "l",
self.tiftag[pointer + x * 8 : pointer + 4 + x * 8],
)[0],
struct.unpack(
self.endian_mark + "l",
self.tiftag[pointer + 4 + x * 8 : pointer + 8 + x * 8],
)[0],
)
for x in range(length)
)
else:
data = (
struct.unpack(
self.endian_mark + "l", self.tiftag[pointer : pointer + 4]
)[0],
struct.unpack(
self.endian_mark + "l", self.tiftag[pointer + 4 : pointer + 8]
)[0],
)
elif t == TYPES.Float: # FLOAT
if length > 1:
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(
self.endian_mark + "f" * length,
self.tiftag[pointer : pointer + length * 4],
)
else:
data = struct.unpack(self.endian_mark + "f" * length, value)
elif t == TYPES.DFloat: # DOUBLE
pointer = struct.unpack(self.endian_mark + "L", value)[0]
data = struct.unpack(
self.endian_mark + "d" * length,
self.tiftag[pointer : pointer + length * 8],
)
else:
raise ValueError(
"Exif might be wrong. Got incorrect value "
+ "type to decode.\n"
+ "tag: "
+ str(val[3])
+ "\ntype: "
+ str(t)
)
if isinstance(data, tuple) and (len(data) == 1):
return data[0]
else:
return data
def _get_key_name_dict(exif_dict):
new_dict = {
"0th": {
TAGS["Image"][n]["name"]: value for n, value in exif_dict["0th"].items()
},
"Exif": {
TAGS["Exif"][n]["name"]: value for n, value in exif_dict["Exif"].items()
},
"1st": {
TAGS["Image"][n]["name"]: value for n, value in exif_dict["1st"].items()
},
"GPS": {TAGS["GPS"][n]["name"]: value for n, value in exif_dict["GPS"].items()},
"Interop": {
TAGS["Interop"][n]["name"]: value
for n, value in exif_dict["Interop"].items()
},
"thumbnail": exif_dict["thumbnail"],
}
return new_dict

View file

@ -1,57 +0,0 @@
import io
from . import _webp
from ._common import get_exif_seg, split_into_segments
def remove(src, new_file=None):
"""
py:function:: piexif.remove(filename)
Remove exif from JPEG.
:param str filename: JPEG
"""
output_is_file = False
if src[0:2] == b"\xff\xd8":
src_data = src
file_type = "jpeg"
elif src[0:4] == b"RIFF" and src[8:12] == b"WEBP":
src_data = src
file_type = "webp"
else:
with open(src, "rb") as f:
src_data = f.read()
output_is_file = True
if src_data[0:2] == b"\xff\xd8":
file_type = "jpeg"
elif src_data[0:4] == b"RIFF" and src_data[8:12] == b"WEBP":
file_type = "webp"
if file_type == "jpeg":
segments = split_into_segments(src_data)
exif = get_exif_seg(segments)
if exif:
new_data = src_data.replace(exif, b"")
else:
new_data = src_data
elif file_type == "webp":
try:
new_data = _webp.remove(src_data)
except ValueError:
new_data = src_data
except Exception as e:
print(e)
raise ValueError("Error occurred.") from e
if isinstance(new_file, io.BytesIO):
new_file.write(new_data)
new_file.seek(0)
elif new_file:
with open(new_file, "wb+") as f:
f.write(new_data)
elif output_is_file:
with open(src, "wb+") as f:
f.write(new_data)
else:
raise ValueError("Give a second argument to 'remove' to output file")

View file

@ -1,45 +0,0 @@
import io
from ._common import get_exif_seg, merge_segments, split_into_segments
def transplant(exif_src, image, new_file=None):
"""
py:function:: piexif.transplant(filename1, filename2)
Transplant exif from filename1 to filename2.
:param str filename1: JPEG
:param str filename2: JPEG
"""
if exif_src[0:2] == b"\xff\xd8":
src_data = exif_src
else:
with open(exif_src, "rb") as f:
src_data = f.read()
segments = split_into_segments(src_data)
exif = get_exif_seg(segments)
if exif is None:
raise ValueError("not found exif in input")
output_file = False
if image[0:2] == b"\xff\xd8":
image_data = image
else:
with open(image, "rb") as f:
image_data = f.read()
output_file = True
segments = split_into_segments(image_data)
new_data = merge_segments(segments, exif)
if isinstance(new_file, io.BytesIO):
new_file.write(new_data)
new_file.seek(0)
elif new_file:
with open(new_file, "wb+") as f:
f.write(new_data)
elif output_file:
with open(image, "wb+") as f:
f.write(new_data)
else:
raise ValueError("Give a 3rd argument to 'transplant' to output file")

View file

@ -1,270 +0,0 @@
import struct
def split(data):
if data[0:4] != b"RIFF" or data[8:12] != b"WEBP":
raise ValueError("Not WebP")
webp_length_bytes = data[4:8]
webp_length = struct.unpack("<L", webp_length_bytes)[0]
RIFF_HEADER_SIZE = 8
file_size = RIFF_HEADER_SIZE + webp_length
start = 12
pointer = start
CHUNK_FOURCC_LENGTH = 4
LENGTH_BYTES_LENGTH = 4
chunks = []
while pointer + CHUNK_FOURCC_LENGTH + LENGTH_BYTES_LENGTH < file_size:
fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
pointer += CHUNK_FOURCC_LENGTH
chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH]
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
pointer += LENGTH_BYTES_LENGTH
chunk_data = data[pointer : pointer + chunk_length]
chunks.append(
{"fourcc": fourcc, "length_bytes": chunk_length_bytes, "data": chunk_data}
)
padding = 1 if chunk_length % 2 else 0
pointer += chunk_length + padding
return chunks
def merge_chunks(chunks):
merged = b"".join(
[
chunk["fourcc"]
+ chunk["length_bytes"]
+ chunk["data"]
+ (len(chunk["data"]) % 2) * b"\x00"
for chunk in chunks
]
)
return merged
def _get_size_from_vp8x(chunk):
width_minus_one_bytes = chunk["data"][-6:-3] + b"\x00"
width_minus_one = struct.unpack("<L", width_minus_one_bytes)[0]
width = width_minus_one + 1
height_minus_one_bytes = chunk["data"][-3:] + b"\x00"
height_minus_one = struct.unpack("<L", height_minus_one_bytes)[0]
height = height_minus_one + 1
return (width, height)
def _get_size_from_vp8(chunk):
BEGIN_CODE = b"\x9d\x01\x2a"
begin_index = chunk["data"].find(BEGIN_CODE)
if begin_index == -1:
ValueError("wrong VP8")
else:
BEGIN_CODE_LENGTH = len(BEGIN_CODE)
LENGTH_BYTES_LENGTH = 2
length_start = begin_index + BEGIN_CODE_LENGTH
width_bytes = chunk["data"][length_start : length_start + LENGTH_BYTES_LENGTH]
width = struct.unpack("<H", width_bytes)[0]
height_bytes = chunk["data"][
length_start + LENGTH_BYTES_LENGTH : length_start + 2 * LENGTH_BYTES_LENGTH
]
height = struct.unpack("<H", height_bytes)[0]
return (width, height)
def _vp8L_contains_alpha(chunk_data):
flag = ord(chunk_data[4:5]) >> 5 - 1 & ord(b"\x01")
contains = 1 * flag
return contains
def _get_size_from_vp8L(chunk):
b1 = chunk["data"][1:2]
b2 = chunk["data"][2:3]
b3 = chunk["data"][3:4]
b4 = chunk["data"][4:5]
width_minus_one = (ord(b2) & ord(b"\x3F")) << 8 | ord(b1)
width = width_minus_one + 1
height_minus_one = (
(ord(b4) & ord(b"\x0F")) << 10 | ord(b3) << 2 | (ord(b2) & ord(b"\xC0")) >> 6
)
height = height_minus_one + 1
return (width, height)
def _get_size_from_anmf(chunk):
width_minus_one_bytes = chunk["data"][6:9] + b"\x00"
width_minus_one = struct.unpack("<L", width_minus_one_bytes)[0]
width = width_minus_one + 1
height_minus_one_bytes = chunk["data"][9:12] + b"\x00"
height_minus_one = struct.unpack("<L", height_minus_one_bytes)[0]
height = height_minus_one + 1
return (width, height)
def set_vp8x(chunks):
width = None
height = None
flags = [
b"0",
b"0",
b"0",
b"0",
b"0",
b"0",
b"0",
b"0",
] # [0, 0, ICC, Alpha, EXIF, XMP, Anim, 0]
for chunk in chunks:
if chunk["fourcc"] == b"VP8X":
width, height = _get_size_from_vp8x(chunk)
elif chunk["fourcc"] == b"VP8 ":
width, height = _get_size_from_vp8(chunk)
elif chunk["fourcc"] == b"VP8L":
is_rgba = _vp8L_contains_alpha(chunk["data"])
if is_rgba:
flags[3] = b"1"
width, height = _get_size_from_vp8L(chunk)
elif chunk["fourcc"] == b"ANMF":
width, height = _get_size_from_anmf(chunk)
elif chunk["fourcc"] == b"ICCP":
flags[2] = b"1"
elif chunk["fourcc"] == b"ALPH":
flags[3] = b"1"
elif chunk["fourcc"] == b"EXIF":
flags[4] = b"1"
elif chunk["fourcc"] == b"XMP ":
flags[5] = b"1"
elif chunk["fourcc"] == b"ANIM":
flags[6] = b"1"
width_minus_one = width - 1
height_minus_one = height - 1
if chunks[0]["fourcc"] == b"VP8X":
chunks.pop(0)
header_bytes = b"VP8X"
length_bytes = b"\x0a\x00\x00\x00"
flags_bytes = struct.pack("B", int(b"".join(flags), 2))
padding_bytes = b"\x00\x00\x00"
width_bytes = struct.pack("<L", width_minus_one)[:3]
height_bytes = struct.pack("<L", height_minus_one)[:3]
data_bytes = flags_bytes + padding_bytes + width_bytes + height_bytes
vp8x_chunk = {
"fourcc": header_bytes,
"length_bytes": length_bytes,
"data": data_bytes,
}
chunks.insert(0, vp8x_chunk)
return chunks
def get_file_header(chunks):
WEBP_HEADER_LENGTH = 4
FOURCC_LENGTH = 4
LENGTH_BYTES_LENGTH = 4
length = WEBP_HEADER_LENGTH
for chunk in chunks:
data_length = struct.unpack("<L", chunk["length_bytes"])[0]
data_length += 1 if data_length % 2 else 0
length += FOURCC_LENGTH + LENGTH_BYTES_LENGTH + data_length
length_bytes = struct.pack("<L", length)
riff = b"RIFF"
webp_header = b"WEBP"
file_header = riff + length_bytes + webp_header
return file_header
def get_exif(data):
if data[0:4] != b"RIFF" or data[8:12] != b"WEBP":
raise ValueError("Not WebP")
if data[12:16] != b"VP8X":
raise ValueError("doesnot have exif")
webp_length_bytes = data[4:8]
webp_length = struct.unpack("<L", webp_length_bytes)[0]
RIFF_HEADER_SIZE = 8
file_size = RIFF_HEADER_SIZE + webp_length
start = 12
pointer = start
CHUNK_FOURCC_LENGTH = 4
LENGTH_BYTES_LENGTH = 4
while pointer < file_size:
fourcc = data[pointer : pointer + CHUNK_FOURCC_LENGTH]
pointer += CHUNK_FOURCC_LENGTH
chunk_length_bytes = data[pointer : pointer + LENGTH_BYTES_LENGTH]
chunk_length = struct.unpack("<L", chunk_length_bytes)[0]
if chunk_length % 2:
chunk_length += 1
pointer += LENGTH_BYTES_LENGTH
if fourcc == b"EXIF":
return data[pointer : pointer + chunk_length]
pointer += chunk_length
return None # if there isn't exif, return None.
def insert_exif_into_chunks(chunks, exif_bytes):
EXIF_HEADER = b"EXIF"
exif_length_bytes = struct.pack("<L", len(exif_bytes))
exif_chunk = {
"fourcc": EXIF_HEADER,
"length_bytes": exif_length_bytes,
"data": exif_bytes,
}
xmp_index = None
animation_index = None
for index, chunk in enumerate(chunks):
if chunk["fourcc"] == b"EXIF":
chunks.pop(index)
for index, chunk in enumerate(chunks):
if chunk["fourcc"] == b"XMP ":
xmp_index = index
elif chunk["fourcc"] == b"ANIM":
animation_index = index
if xmp_index is not None:
chunks.insert(xmp_index, exif_chunk)
elif animation_index is not None:
chunks.insert(animation_index, exif_chunk)
else:
chunks.append(exif_chunk)
return chunks
def insert(webp_bytes, exif_bytes):
chunks = split(webp_bytes)
chunks = insert_exif_into_chunks(chunks, exif_bytes)
chunks = set_vp8x(chunks)
file_header = get_file_header(chunks)
merged = merge_chunks(chunks)
new_webp_bytes = file_header + merged
return new_webp_bytes
def remove(webp_bytes):
chunks = split(webp_bytes)
for index, chunk in enumerate(chunks):
if chunk["fourcc"] == b"EXIF":
chunks.pop(index)
chunks = set_vp8x(chunks)
file_header = get_file_header(chunks)
merged = merge_chunks(chunks)
new_webp_bytes = file_header + merged
return new_webp_bytes

View file

@ -1,76 +0,0 @@
class UserComment:
#
# Names of encodings that we publicly support.
#
ASCII = "ascii"
JIS = "jis"
UNICODE = "unicode"
ENCODINGS = (ASCII, JIS, UNICODE)
#
# The actual encodings accepted by the standard library differ slightly from
# the above.
#
_JIS = "shift_jis"
_UNICODE = "utf_16_be"
_PREFIX_SIZE = 8
#
# From Table 9: Character Codes and their Designation
#
_ASCII_PREFIX = b"\x41\x53\x43\x49\x49\x00\x00\x00"
_JIS_PREFIX = b"\x4a\x49\x53\x00\x00\x00\x00\x00"
_UNICODE_PREFIX = b"\x55\x4e\x49\x43\x4f\x44\x45\x00"
_UNDEFINED_PREFIX = b"\x00\x00\x00\x00\x00\x00\x00\x00"
@classmethod
def load(cls, data):
"""
Convert "UserComment" value in exif format to str.
:param bytes data: "UserComment" value from exif
:return: u"foobar"
:rtype: str(Unicode)
:raises: ValueError if the data does not conform to the EXIF specification,
or the encoding is unsupported.
"""
if len(data) < cls._PREFIX_SIZE:
raise ValueError("not enough data to decode UserComment")
prefix = data[: cls._PREFIX_SIZE]
body = data[cls._PREFIX_SIZE :]
if prefix == cls._UNDEFINED_PREFIX:
raise ValueError("prefix is UNDEFINED, unable to decode UserComment")
try:
encoding = {
cls._ASCII_PREFIX: cls.ASCII,
cls._JIS_PREFIX: cls._JIS,
cls._UNICODE_PREFIX: cls._UNICODE,
}[prefix]
except KeyError as e:
raise ValueError("unable to determine appropriate encoding") from e
return body.decode(encoding, errors="replace")
@classmethod
def dump(cls, data, encoding="ascii"):
"""
Convert str to appropriate format for "UserComment".
:param data: Like u"foobar"
:param str encoding: "ascii", "jis", or "unicode"
:return: b"ASCII\x00\x00\x00foobar"
:rtype: bytes
:raises: ValueError if the encoding is unsupported.
"""
if encoding not in cls.ENCODINGS:
raise ValueError(
"encoding %r must be one of %r" % (encoding, cls.ENCODINGS)
)
prefix = {
cls.ASCII: cls._ASCII_PREFIX,
cls.JIS: cls._JIS_PREFIX,
cls.UNICODE: cls._UNICODE_PREFIX,
}[encoding]
internal_encoding = {cls.UNICODE: cls._UNICODE, cls.JIS: cls._JIS}.get(
encoding, encoding
)
return prefix + data.encode(internal_encoding, errors="replace")