From 340b3dda89684306494a0096fb62d15c1ac498e2 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 18 Feb 2019 10:53:18 +0000 Subject: [PATCH 1/9] Initial testing for exif metadata --- openflexure_microscope/camera/base.py | 11 --- openflexure_microscope/camera/capture.py | 104 +++++++++++++---------- 2 files changed, 59 insertions(+), 56 deletions(-) diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 16869005..7500a168 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -248,17 +248,6 @@ class BaseCamera(object): 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 diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 0ee440cb..45f0d50d 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -9,6 +9,8 @@ import logging from PIL import Image import atexit +import piexif + pil_formats = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] thumbnail_size = (200, 150) @@ -30,6 +32,30 @@ 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): + 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 +65,24 @@ def capture_from_dict(capture_dict): Args: capture_dict (dict): Dictionary containing capture information """ - capture = CaptureObject(create_metadata_file=False) # Create a placeholder capture + + 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'] + + md_exif = pull_usercomment_dict(capture.file) + 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 +100,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 +134,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 +163,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 +184,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, @@ -263,13 +287,6 @@ class CaptureObject(object): # HANDLE 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]) - def put_metadata(self, data: dict) -> None: """ Merge metadata from a passed dictionary into the capture metadata, and saves. @@ -282,19 +299,21 @@ class CaptureObject(object): def save_metadata(self) -> None: """ - Save metadata to on-disk metadata YAML file + Save metadata to exif """ - 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) - 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) + if 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 +350,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: @@ -445,9 +463,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 +477,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.""" From 05f52bbdeb87fe49ce99e25c4290cce427551a4b Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 18 Feb 2019 11:30:38 +0000 Subject: [PATCH 2/9] Process all initial tags in one call --- .../api/v1/blueprints/camera/capture.py | 4 +--- openflexure_microscope/camera/capture.py | 12 +++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py index 56557f07..80ebed34 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ b/openflexure_microscope/api/v1/blueprints/camera/capture.py @@ -128,9 +128,7 @@ class ListAPI(MicroscopeView): bayer=bayer) output.put_metadata(metadata) - - for tag in tags: - output.put_tag(str(tag)) + output.put_tags(tags) return jsonify(output.state) diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 45f0d50d..77cac513 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -264,16 +264,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 + tag (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 not tag in self.tags: + self.tags.append(tag) + + self.save_metadata() def delete_tag(self, tag: str): """ From ab70a3857c0dea71361198e057752b6d14dd0d08 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 18 Feb 2019 11:48:56 +0000 Subject: [PATCH 3/9] Only use exif if supported format --- openflexure_microscope/camera/capture.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 77cac513..52516b28 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -11,8 +11,9 @@ import atexit import piexif -pil_formats = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] -thumbnail_size = (200, 150) +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) @@ -65,6 +66,7 @@ def capture_from_dict(capture_dict): Args: capture_dict (dict): Dictionary containing capture information """ + global EXIF_FORMATS capture = CaptureObject() # Create a placeholder capture @@ -73,7 +75,11 @@ def capture_from_dict(capture_dict): capture.split_file_path(capture.file) capture.temporary = capture_dict['temporary'] - md_exif = pull_usercomment_dict(capture.file) + if capture.format in EXIF_FORMATS: + md_exif = pull_usercomment_dict(capture.file) + else: + md_exif = {} + md_database = capture_dict['metadata'] # Populate capture parameters @@ -301,10 +307,11 @@ class CaptureObject(object): def save_metadata(self) -> None: """ - Save metadata to exif + Save metadata to exif, if supported """ + global EXIF_FORMATS - if self.file_exists: + if self.format 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) @@ -405,14 +412,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: From e8fb3b378c82a896d1bd4b3e119ba6b77f12e00e Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 18 Feb 2019 12:11:33 +0000 Subject: [PATCH 4/9] Fixed missing capture validation --- openflexure_microscope/camera/base.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 7500a168..266dcd2b 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -242,15 +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) - - 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 @@ -258,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 From 184e3d95582230881197f3743c4e69f0fefb06b2 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 18 Feb 2019 17:25:50 +0000 Subject: [PATCH 5/9] Fixed format checks for loading exif data --- openflexure_microscope/camera/capture.py | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 52516b28..e5ac01d4 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -3,7 +3,6 @@ import io import os import glob import datetime -import copy import yaml import logging from PIL import Image @@ -47,6 +46,16 @@ def pull_usercomment_dict(filepath): 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: @@ -75,7 +84,7 @@ def capture_from_dict(capture_dict): capture.split_file_path(capture.file) capture.temporary = capture_dict['temporary'] - if capture.format in EXIF_FORMATS: + if capture.format.upper() in EXIF_FORMATS: md_exif = pull_usercomment_dict(capture.file) else: md_exif = {} @@ -236,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.""" @@ -275,10 +285,10 @@ class CaptureObject(object): Add a new tag to the ``tags`` list attribute. Args: - tag (list): List of tags to be added + tags (list): List of tags to be added """ for tag in tags: - if not tag in self.tags: + if tag not in self.tags: self.tags.append(tag) self.save_metadata() @@ -293,6 +303,8 @@ class CaptureObject(object): if tag in self.tags: self.tags = [new_tag for new_tag in self.tags if new_tag != tag] + self.save_metadata() + # HANDLE METADATA def put_metadata(self, data: dict) -> None: @@ -311,7 +323,10 @@ class CaptureObject(object): """ global EXIF_FORMATS - if self.format in EXIF_FORMATS and self.file_exists: + 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) From c9c3e8fdfc7e8defb8ec3387b3ee6d48dd399217 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 19 Feb 2019 10:35:52 +0000 Subject: [PATCH 6/9] Mention metadata storage in exif tags --- docs/source/capture.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/capture.rst b/docs/source/capture.rst index 1ed90b8c..4b0477cc 100644 --- a/docs/source/capture.rst +++ b/docs/source/capture.rst @@ -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: \ No newline at end of file From 51cd7767004e6730e5e7ae66f85e3e02a612afaa Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 19 Feb 2019 10:43:33 +0000 Subject: [PATCH 7/9] Store stage position in metadata --- openflexure_microscope/api/v1/blueprints/camera/capture.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py index 80ebed34..861737a3 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ b/openflexure_microscope/api/v1/blueprints/camera/capture.py @@ -127,6 +127,8 @@ class ListAPI(MicroscopeView): resize=resize, bayer=bayer) + metadata.update({'Position': self.microscope.state['stage']['position']}) + output.put_metadata(metadata) output.put_tags(tags) From 366b771b725012e74611c12b82ad1efdc5f90c3d Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 19 Feb 2019 11:04:12 +0000 Subject: [PATCH 8/9] Added size and position params to GPU preview route --- .../api/v1/blueprints/camera/preview.py | 22 ++++++++++++++++--- openflexure_microscope/camera/pi.py | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/openflexure_microscope/api/v1/blueprints/camera/preview.py b/openflexure_microscope/api/v1/blueprints/camera/preview.py index 2150712f..11ea28f8 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/preview.py +++ b/openflexure_microscope/api/v1/blueprints/camera/preview.py @@ -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) diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index f39034e6..df49fc40 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -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 From c6eb6148cc35d9de48114b08f306ea61fa48c248 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 19 Feb 2019 12:02:11 +0000 Subject: [PATCH 9/9] Packaged basic Piexif module --- openflexure_microscope/camera/capture.py | 2 +- .../camera/piexif/LICENSE.txt | 21 + .../camera/piexif/__init__.py | 11 + .../camera/piexif/_common.py | 94 +++ openflexure_microscope/camera/piexif/_dump.py | 346 ++++++++++ .../camera/piexif/_exceptions.py | 2 + openflexure_microscope/camera/piexif/_exif.py | 638 ++++++++++++++++++ .../camera/piexif/_insert.py | 60 ++ openflexure_microscope/camera/piexif/_load.py | 270 ++++++++ .../camera/piexif/_remove.py | 56 ++ .../camera/piexif/_transplant.py | 45 ++ openflexure_microscope/camera/piexif/_webp.py | 240 +++++++ .../camera/piexif/helper.py | 66 ++ 13 files changed, 1850 insertions(+), 1 deletion(-) create mode 100644 openflexure_microscope/camera/piexif/LICENSE.txt create mode 100644 openflexure_microscope/camera/piexif/__init__.py create mode 100644 openflexure_microscope/camera/piexif/_common.py create mode 100644 openflexure_microscope/camera/piexif/_dump.py create mode 100644 openflexure_microscope/camera/piexif/_exceptions.py create mode 100644 openflexure_microscope/camera/piexif/_exif.py create mode 100644 openflexure_microscope/camera/piexif/_insert.py create mode 100644 openflexure_microscope/camera/piexif/_load.py create mode 100644 openflexure_microscope/camera/piexif/_remove.py create mode 100644 openflexure_microscope/camera/piexif/_transplant.py create mode 100644 openflexure_microscope/camera/piexif/_webp.py create mode 100644 openflexure_microscope/camera/piexif/helper.py diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index e5ac01d4..fb8cce2d 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -8,7 +8,7 @@ import logging from PIL import Image import atexit -import piexif +from openflexure_microscope.camera import piexif PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF'] diff --git a/openflexure_microscope/camera/piexif/LICENSE.txt b/openflexure_microscope/camera/piexif/LICENSE.txt new file mode 100644 index 00000000..be0bbced --- /dev/null +++ b/openflexure_microscope/camera/piexif/LICENSE.txt @@ -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. diff --git a/openflexure_microscope/camera/piexif/__init__.py b/openflexure_microscope/camera/piexif/__init__.py new file mode 100644 index 00000000..95cc5de1 --- /dev/null +++ b/openflexure_microscope/camera/piexif/__init__.py @@ -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' diff --git a/openflexure_microscope/camera/piexif/_common.py b/openflexure_microscope/camera/piexif/_common.py new file mode 100644 index 00000000..28e45871 --- /dev/null +++ b/openflexure_microscope/camera/piexif/_common.py @@ -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) diff --git a/openflexure_microscope/camera/piexif/_dump.py b/openflexure_microscope/camera/piexif/_dump.py new file mode 100644 index 00000000..4617a69a --- /dev/null +++ b/openflexure_microscope/camera/piexif/_dump.py @@ -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) diff --git a/openflexure_microscope/camera/piexif/_exceptions.py b/openflexure_microscope/camera/piexif/_exceptions.py new file mode 100644 index 00000000..bde09086 --- /dev/null +++ b/openflexure_microscope/camera/piexif/_exceptions.py @@ -0,0 +1,2 @@ +class InvalidImageDataError(ValueError): + pass diff --git a/openflexure_microscope/camera/piexif/_exif.py b/openflexure_microscope/camera/piexif/_exif.py new file mode 100644 index 00000000..abdf0bc4 --- /dev/null +++ b/openflexure_microscope/camera/piexif/_exif.py @@ -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 diff --git a/openflexure_microscope/camera/piexif/_insert.py b/openflexure_microscope/camera/piexif/_insert.py new file mode 100644 index 00000000..6d74836a --- /dev/null +++ b/openflexure_microscope/camera/piexif/_insert.py @@ -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") \ No newline at end of file diff --git a/openflexure_microscope/camera/piexif/_load.py b/openflexure_microscope/camera/piexif/_load.py new file mode 100644 index 00000000..61760289 --- /dev/null +++ b/openflexure_microscope/camera/piexif/_load.py @@ -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 \ No newline at end of file diff --git a/openflexure_microscope/camera/piexif/_remove.py b/openflexure_microscope/camera/piexif/_remove.py new file mode 100644 index 00000000..6ba49116 --- /dev/null +++ b/openflexure_microscope/camera/piexif/_remove.py @@ -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") \ No newline at end of file diff --git a/openflexure_microscope/camera/piexif/_transplant.py b/openflexure_microscope/camera/piexif/_transplant.py new file mode 100644 index 00000000..0abf7f11 --- /dev/null +++ b/openflexure_microscope/camera/piexif/_transplant.py @@ -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") \ No newline at end of file diff --git a/openflexure_microscope/camera/piexif/_webp.py b/openflexure_microscope/camera/piexif/_webp.py new file mode 100644 index 00000000..f9cc42ea --- /dev/null +++ b/openflexure_microscope/camera/piexif/_webp.py @@ -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("> 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("