Merge branch 'exif' into 'master'
Store metadata in Exif tags See merge request openflexure/openflexure-microscope-server!5
This commit is contained in:
commit
86e1c82dfb
18 changed files with 1978 additions and 91 deletions
|
|
@ -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:
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
21
openflexure_microscope/camera/piexif/LICENSE.txt
Normal file
21
openflexure_microscope/camera/piexif/LICENSE.txt
Normal 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.
|
||||
11
openflexure_microscope/camera/piexif/__init__.py
Normal file
11
openflexure_microscope/camera/piexif/__init__.py
Normal 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'
|
||||
94
openflexure_microscope/camera/piexif/_common.py
Normal file
94
openflexure_microscope/camera/piexif/_common.py
Normal 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)
|
||||
346
openflexure_microscope/camera/piexif/_dump.py
Normal file
346
openflexure_microscope/camera/piexif/_dump.py
Normal 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)
|
||||
2
openflexure_microscope/camera/piexif/_exceptions.py
Normal file
2
openflexure_microscope/camera/piexif/_exceptions.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
class InvalidImageDataError(ValueError):
|
||||
pass
|
||||
638
openflexure_microscope/camera/piexif/_exif.py
Normal file
638
openflexure_microscope/camera/piexif/_exif.py
Normal 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
|
||||
60
openflexure_microscope/camera/piexif/_insert.py
Normal file
60
openflexure_microscope/camera/piexif/_insert.py
Normal 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")
|
||||
270
openflexure_microscope/camera/piexif/_load.py
Normal file
270
openflexure_microscope/camera/piexif/_load.py
Normal 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
|
||||
56
openflexure_microscope/camera/piexif/_remove.py
Normal file
56
openflexure_microscope/camera/piexif/_remove.py
Normal 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")
|
||||
45
openflexure_microscope/camera/piexif/_transplant.py
Normal file
45
openflexure_microscope/camera/piexif/_transplant.py
Normal 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")
|
||||
240
openflexure_microscope/camera/piexif/_webp.py
Normal file
240
openflexure_microscope/camera/piexif/_webp.py
Normal 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
|
||||
66
openflexure_microscope/camera/piexif/helper.py
Normal file
66
openflexure_microscope/camera/piexif/helper.py
Normal 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')
|
||||
Loading…
Add table
Add a link
Reference in a new issue