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."""