Merge branch 'exif' into 'master'

Store metadata in Exif tags

See merge request openflexure/openflexure-microscope-server!5
This commit is contained in:
Joel Collins 2019-02-19 13:13:21 +00:00
commit 86e1c82dfb
18 changed files with 1978 additions and 91 deletions

View file

@ -3,7 +3,9 @@ Capture Object
By default, all image and video capture data are stored to instances of :py:class:`openflexure_microscope.camera.capture.CaptureObject`. This class mostly wraps up complexity associated with moving data between disk and memory.
The class also includes some convenience features such as automatically cleaning up captures marked as temporary, handling metadata files and file names, and generating image thumbnails. Often, the most recent capture wants to be stored in memory, for applications that involve immediately processing the capture data in some way. Rather than manually keep track of the location of the capture data, you can use the CaptureObject ``data`` attribute, which automatically returns capture data from the fastest available location. Below are details of available methods and attributes.
The class also includes some convenience features such as automatically cleaning up captures marked as temporary, handling metadata tags and file names, and generating image thumbnails. Often, the most recent capture wants to be stored in memory, for applications that involve immediately processing the capture data in some way. Rather than manually keep track of the location of the capture data, you can use the CaptureObject ``data`` attribute, which automatically returns capture data from the fastest available location. Additionally, the class handles storing capture metadata to Exif tags in supported formats.
Below are details of available methods and attributes.
.. automodule:: openflexure_microscope.camera.capture
:members:

View file

@ -127,10 +127,10 @@ class ListAPI(MicroscopeView):
resize=resize,
bayer=bayer)
output.put_metadata(metadata)
metadata.update({'Position': self.microscope.state['stage']['position']})
for tag in tags:
output.put_tag(str(tag))
output.put_metadata(metadata)
output.put_tags(tags)
return jsonify(output.state)

View file

@ -1,13 +1,16 @@
from openflexure_microscope.api.utilities import JsonPayload
from openflexure_microscope.api.v1.views import MicroscopeView
from flask import jsonify
from flask import jsonify, request
class GPUPreviewAPI(MicroscopeView):
def post(self, operation):
"""
Create a new image capture.
Start or stop the onboard GPU preview.
Optional "window" parameter can be passed to control the position and size of the preview window,
in the format ``[x, y, width, height]``.
.. :quickref: GPU Preview; Start/stop preview
@ -18,6 +21,10 @@ class GPUPreviewAPI(MicroscopeView):
POST /camera/preview/start HTTP/1.1
Accept: application/json
{
"window": [0, 0, 480, 320],
}
.. sourcecode:: http
POST /camera/preview/stop HTTP/1.1
@ -29,7 +36,16 @@ class GPUPreviewAPI(MicroscopeView):
:status 200: preview started/stopped
"""
if operation == "start":
self.microscope.camera.start_preview()
payload = JsonPayload(request)
window = payload.param('window', default=[])
fullscreen = False
if len(window) != 4 or not all(isinstance(n, int) for n in window):
fullscreen = True
window = None
self.microscope.camera.start_preview(fullscreen=fullscreen, window=window)
elif operation == "stop":
self.microscope.camera.stop_preview()
return jsonify(self.microscope.state)

View file

@ -242,26 +242,6 @@ class BaseCamera(object):
with open(db_path, 'w') as outfile:
yaml.safe_dump(capture_state, outfile)
def validate_captures(self, capture_list):
# Purge captures with missing data files
valid_captures = []
for capture in capture_list:
if capture.file_exists:
valid_captures.append(capture)
else:
if os.path.isfile(capture.metadata_file):
logging.warning("Capture data missing. Deleting metadata file {}.".format(capture.metadata_file))
os.remove(capture.metadata_file)
# Validate and repair captures
for capture in valid_captures:
# If the captures metadata file is missing
if not os.path.isfile(capture.metadata_file):
# Recreate the metadata file
capture.save_metadata()
return valid_captures
def load_capture_db(self, db_path):
if os.path.isfile(db_path):
# Load list of capture dictionary representations from db_path
@ -269,9 +249,11 @@ class BaseCamera(object):
capture_dict_list = yaml.load(infile)
# Create capture object list, and validate captures
capture_list = self.validate_captures(
[capture_from_dict(capture_dict) for capture_dict in capture_dict_list]
)
capture_list = []
for capture_dict in capture_dict_list:
if os.path.isfile(capture_dict['path']):
capture_list.append(capture_from_dict(capture_dict))
return capture_list

View file

@ -3,14 +3,16 @@ import io
import os
import glob
import datetime
import copy
import yaml
import logging
from PIL import Image
import atexit
pil_formats = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF']
thumbnail_size = (200, 150)
from openflexure_microscope.camera import piexif
PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF']
EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF']
THUMBNAIL_SIZE = (200, 150)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') #: str: Base path to store all captures
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') #: str: Base path to store all temporary captures (automatically emptied)
@ -30,6 +32,40 @@ def clear_tmp():
logging.debug("Cleared {}.".format(TEMP_CAPTURE_PATH))
def pull_usercomment_dict(filepath):
"""
Reads UserComment Exif data from a file, and returns the contained bytes as a dictionary.
Args:
filepath: Path to the Exif-containing file
"""
exif_dict = piexif.load(filepath)
if 'Exif' in exif_dict and 37510 in exif_dict['Exif']:
return yaml.load(exif_dict['Exif'][37510].decode())
else:
return {}
def extract_with_priority(key, best_dict, backup_dict):
"""
Extracts a value from one of two dictionaries, prioritising one over the other.
Second dictionary is used only if the key doesn't exist in the first.
Args:
key: Key to search for
best_dict: Ideal dictionary to use
backup_dict: Fallback dictionary
"""
if key in best_dict:
return best_dict[key]
elif key in backup_dict:
logging.warning("Key {} not found in primary dictionary. Falling back to backup.".format(key))
return backup_dict[key]
else:
logging.error("Key {} not found in either dictionary!".format(key))
return None
def capture_from_dict(capture_dict):
"""
Creates an instance of CaptureObject from a dictionary of capture information.
@ -39,22 +75,29 @@ def capture_from_dict(capture_dict):
Args:
capture_dict (dict): Dictionary containing capture information
"""
capture = CaptureObject(create_metadata_file=False) # Create a placeholder capture
global EXIF_FORMATS
capture = CaptureObject() # Create a placeholder capture
# Get inherent capture information from database
capture.file = capture_dict['path']
capture.split_file_path(capture.file)
capture.temporary = capture_dict['temporary']
if capture.format.upper() in EXIF_FORMATS:
md_exif = pull_usercomment_dict(capture.file)
else:
md_exif = {}
md_database = capture_dict['metadata']
# Populate capture parameters
capture.temporary = capture_dict['temporary']
capture.id = capture_dict['metadata']['id']
capture.timestring = capture_dict['metadata']['time']
capture.format = capture_dict['metadata']['format']
capture.id = extract_with_priority('id', md_exif, md_database)
capture.timestring = extract_with_priority('time', md_exif, md_database)
capture.format = extract_with_priority('format', md_exif, md_database)
if 'custom' in capture_dict['metadata']:
capture._metadata = capture_dict['metadata']['custom']
if 'tags' in capture_dict['metadata']:
capture.tags = capture_dict['metadata']['tags']
capture.file = capture_dict['metadata']['path']
capture.split_file_path(capture.file)
capture._metadata = extract_with_priority('custom', md_exif, md_database)
capture.tags = extract_with_priority('tags', md_exif, md_database)
return capture
@ -72,8 +115,7 @@ class CaptureObject(object):
temporary: bool = False,
filename: str = '',
folder: str = '',
fmt: str = '',
create_metadata_file: bool = True) -> None:
fmt: str = '') -> None:
"""Create a new StreamObject, to manage capture data."""
# Store a nice ID
@ -107,7 +149,7 @@ class CaptureObject(object):
self.tags = [] #: list: List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
# Initialise the capture stream
self.initialise_capture(create_metadata_file=create_metadata_file)
self.initialise_capture()
# Log if created by context manager
self.context_manager = False
@ -136,14 +178,12 @@ class CaptureObject(object):
logging.info("Cleaning up {}".format(self.id))
self.close()
def initialise_capture(self, create_metadata_file=True):
def initialise_capture(self):
"""
Initialise the capture object. Creates a file name and builds a file path,
creates an in-memory byte stream for capture data, and creates a metadata
file on disk.
Args:
create_metadata_file (bool): Skips creating a metadata file if False (useful for captures being processed but not stored)
"""
self.build_file_path(self.filename, self.folder)
@ -159,8 +199,7 @@ class CaptureObject(object):
self.stream = self.file
# Save initial metadata file
if create_metadata_file:
self.save_metadata()
self.save_metadata()
def build_file_path(
self,
@ -206,10 +245,11 @@ class CaptureObject(object):
Args:
filepath (str): String of the full file path, including file format extension
"""
self.filefolder, self.filename = os.path.split(
filepath) # Split the full file path into a folder and a filename
self.filefolder, self.filename = os.path.split(filepath) # Split the full file path into a folder and a filename
self.basename = os.path.splitext(self.filename)[0] # Split the filename out from it's file extension
self.metadataname = "{}.yaml".format(self.basename)
if not self.format:
self.format = self.filename.split('.')[-1]
def lock(self):
"""Set locked flag to True."""
@ -240,16 +280,18 @@ class CaptureObject(object):
return False
# HANDLE TAGS
def put_tag(self, tag: str):
def put_tags(self, tags: list):
"""
Add a new tag to the ``tags`` list attribute.
Args:
tag (str): Tag to be added
tags (list): List of tags to be added
"""
if not tag in self.tags:
self.tags.append(tag)
self.save_metadata()
for tag in tags:
if tag not in self.tags:
self.tags.append(tag)
self.save_metadata()
def delete_tag(self, tag: str):
"""
@ -261,14 +303,9 @@ class CaptureObject(object):
if tag in self.tags:
self.tags = [new_tag for new_tag in self.tags if new_tag != tag]
# HANDLE METADATA
self.save_metadata()
@property
def metadata_file(self) -> str:
"""
Path to the associated on-disk metadata YAML file.
"""
return "{}.yaml".format(os.path.splitext(self.file)[0])
# HANDLE METADATA
def put_metadata(self, data: dict) -> None:
"""
@ -282,19 +319,25 @@ class CaptureObject(object):
def save_metadata(self) -> None:
"""
Save metadata to on-disk metadata YAML file
Save metadata to exif, if supported
"""
logging.debug("Writing metadata to file {}".format(self.metadata_file))
with open(self.metadata_file, 'w') as outfile:
yaml.dump(self.metadata, outfile, default_flow_style=False)
global EXIF_FORMATS
def delete_metadata(self) -> None:
"""
Delete on-disk metadata YAML file, if it exists
"""
if os.path.isfile(self.metadata_file):
logging.info("Deleting file {}".format(self.metadata_file))
os.remove(self.metadata_file)
print(self.format)
print(EXIF_FORMATS)
if self.format.upper() in EXIF_FORMATS and self.file_exists:
logging.debug("Writing exif data to capture file")
# Extract current Exif data
exif_dict = piexif.load(self.file)
# Serialize metadata
metadata_string = yaml.safe_dump(self.metadata)
# Insert metadata into exif_dict
exif_dict['Exif'][piexif.ExifIFD.UserComment] = metadata_string.encode()
# Convert new exif dict to exif bytes
exif_bytes = piexif.dump(exif_dict)
# Insert exif into file
piexif.insert(exif_bytes, self.file)
@property
def metadata(self) -> dict:
@ -331,8 +374,7 @@ class CaptureObject(object):
"""
# Create basic state dictionary
d = {'locked': self.locked, 'temporary': self.temporary, 'metadata': self.metadata,
'metadata_path': self.metadata_file}
d = {'path': self.file, 'locked': self.locked, 'temporary': self.temporary, 'metadata': self.metadata}
# Check bytestream
if self.stream_exists:
@ -385,14 +427,14 @@ class CaptureObject(object):
"""
Returns a thumbnail of the capture data, for supported image formats.
"""
global thumbnail_size
global THUMBNAIL_SIZE
# If no thumbnail exists, try and make one
if not self.thumb_bytes:
logging.info("Building thumbnail")
self.thumb_bytes = io.BytesIO()
if self.format.upper() in pil_formats:
if self.format.upper() in PIL_FORMATS:
im = Image.open(self.data)
im.thumbnail(thumbnail_size)
im.thumbnail(THUMBNAIL_SIZE)
im.save(self.thumb_bytes, self.format)
self.thumb_bytes.seek(0)
else:
@ -445,9 +487,6 @@ class CaptureObject(object):
def delete_file(self) -> bool:
"""If the StreamObject has been saved, delete the file."""
if os.path.isfile(self.metadata_file):
logging.info("Deleting file {}".format(self.metadata_file))
os.remove(self.metadata_file)
if os.path.isfile(self.file):
logging.info("Deleting file {}".format(self.file))
@ -462,7 +501,6 @@ class CaptureObject(object):
logging.info("Deleting {}".format(self.id))
self.delete_stream()
self.delete_file()
self.delete_metadata()
def shunt(self):
"""Demote the StreamObject from being stored in memory."""

View file

@ -225,9 +225,9 @@ class StreamingCamera(BaseCamera):
# LAUNCH ACTIONS
def start_preview(self) -> bool:
def start_preview(self, fullscreen=True, window=None) -> bool:
"""Start the on board GPU camera preview."""
self.camera.start_preview()
self.camera.start_preview(fullscreen=fullscreen, window=window)
self.state['preview_active'] = True
return True

View file

@ -0,0 +1,21 @@
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

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

View file

@ -0,0 +1,94 @@
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 1:
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

@ -0,0 +1,346 @@
import copy
import numbers
import struct
from ._common import *
from ._exif import *
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:
try:
new_value = raw_value + b"\x00"
except TypeError:
raise ValueError("Got invalid type to convert.")
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 n, 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 n, 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:
raise ValueError("Got invalid type to convert.")
else:
try:
value_str = raw_value + b"\x00" * (4 - length)
except TypeError:
raise ValueError("Got invalid type to convert.")
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 n, 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:
raise ValueError(
'"dump" got wrong type of exif value.\n' +
'{0} in {1} IFD. Got as {2}.'.format(key, ifd, type(ifd_dict[key]))
)
entries += key_str + type_str + length_str + value_str
values += four_bytes_over
return (entry_header + entries, values)

View file

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

View file

@ -0,0 +1,638 @@
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

@ -0,0 +1,60 @@
import io
import struct
import sys
from ._common import *
from ._exceptions import InvalidImageDataError
from piexif import _webp
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

@ -0,0 +1,270 @@
import struct
import sys
from ._common import *
from ._exceptions import InvalidImageDataError
from ._exif import *
from piexif import _webp
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.")
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

@ -0,0 +1,56 @@
import io
from ._common import *
from piexif import _webp
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 e:
print(e.args)
raise ValueError("Error occurred.")
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

@ -0,0 +1,45 @@
import io
from ._common import *
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

@ -0,0 +1,240 @@
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
chunks = []
exif = b""
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

@ -0,0 +1,66 @@
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:
raise ValueError('unable to determine appropriate encoding')
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')