Removed in-memory full metadata from CaptureObject

This commit is contained in:
Joel Collins 2020-11-06 12:09:05 +00:00
parent 1c7b27a29f
commit 9dab2421a4

View file

@ -18,29 +18,6 @@ EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
THUMBNAIL_SIZE = (200, 150) THUMBNAIL_SIZE = (200, 150)
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
"""
try:
exif_dict = piexif.load(filepath)
except InvalidImageDataError:
logging.warning("Invalid data at {}. Skipping.".format(filepath))
return None
if "Exif" in exif_dict and piexif.ExifIFD.UserComment in exif_dict["Exif"]:
try:
return json.loads(exif_dict["Exif"][piexif.ExifIFD.UserComment].decode())
except json.decoder.JSONDecodeError:
logging.error(
"Capture %s has old, corrupt, or missing OpenFlexure metadata. Unable to reload to server.",
filepath,
)
else:
return None
def make_file_list(directory, formats): def make_file_list(directory, formats):
files = [] files = []
for fmt in formats: for fmt in formats:
@ -72,37 +49,18 @@ def build_captures_from_exif(capture_path):
def capture_from_path(path): def capture_from_path(path):
exif_dict = pull_usercomment_dict(path) # Create a placeholder capture
if exif_dict: capture = CaptureObject(filepath=path)
# Create a placeholder capture
capture = CaptureObject(filepath=path)
# Build file path information # Build file path information
capture.split_file_path(capture.file) capture.split_file_path(capture.file)
# Image metadata
try:
image_metadata = exif_dict.pop("image")
except KeyError:
logging.error(
"Unable to obtain valid 2.0 OpenFlexure metadata from file %s", path
)
return None
# Populate capture parameters
capture.id = image_metadata.get("id")
capture.time = dateutil.parser.isoparse(
image_metadata.get("time") or image_metadata.get("acquisitionDate")
)
capture.format = image_metadata.get("format")
capture.tags = image_metadata.get("tags")
capture.annotations = image_metadata.get("annotations")
# Since we popped the "image" key, we dump whatever is left in _metadata
capture.set_metadata(exif_dict)
# Check and sync basic metadata
try:
capture.sync_basic_metadata()
return capture return capture
else: except (InvalidImageDataError, json.decoder.JSONDecodeError):
logging.error("Invalid metadata at {}. Skipping.".format(path))
return None return None
@ -131,10 +89,8 @@ class CaptureObject(object):
if not os.path.exists(self.filefolder): if not os.path.exists(self.filefolder):
os.makedirs(self.filefolder) os.makedirs(self.filefolder)
# Dictionary for adding top-level metadata # Dataset information
# This can ONLY be modified by the server application self._dataset = None
# Top level metadata cannot be modified via the web API
self._metadata = {}
# Dictionary for storing custom annotations # Dictionary for storing custom annotations
# Can be modified via the web API # Can be modified via the web API
self.annotations = {} self.annotations = {}
@ -147,10 +103,12 @@ class CaptureObject(object):
self.stream.write(s) self.stream.write(s)
def flush(self): def flush(self):
logging.info("Writing to disk %s", self.file) logging.info("Writing image data to disk %s", self.file)
with open(self.file, "wb") as outfile: with open(self.file, "wb") as outfile:
outfile.write(self.stream.getbuffer()) outfile.write(self.stream.getbuffer())
self.stream.close() self.stream.close()
logging.info("Writing metadata to disk %s", self.file)
self._init_metadata()
logging.info("Finished writing to disk %s", self.file) logging.info("Finished writing to disk %s", self.file)
def open(self, mode): def open(self, mode):
@ -169,6 +127,65 @@ class CaptureObject(object):
self.basename = os.path.splitext(self.name)[0] self.basename = os.path.splitext(self.name)[0]
self.format = self.name.split(".")[-1] self.format = self.name.split(".")[-1]
def _read_exif(self):
return piexif.load(self.file)
def _decode_usercomment(self, exif_dict: dict):
if "Exif" not in exif_dict:
raise InvalidImageDataError
if piexif.ExifIFD.UserComment not in exif_dict["Exif"]:
return {}
return json.loads(exif_dict["Exif"][piexif.ExifIFD.UserComment].decode())
def _init_metadata(self):
self.put_and_save(
metadata={
"image": {
"id": self.id,
"name": self.name,
"time": self.time.isoformat(),
"format": self.format,
"tags": self.tags,
"annotations": self.annotations,
}
}
)
def sync_basic_metadata(self):
exif_dict = self._read_exif()
metadata_dict = self._decode_usercomment(exif_dict) or {}
image_metadata = metadata_dict.get("image")
if not image_metadata:
raise InvalidImageDataError("No capture metadata found")
self.id = image_metadata.get("id")
self.format = image_metadata.get("format")
self.time = dateutil.parser.isoparse(
image_metadata.get("time") or image_metadata.get("acquisitionDate")
)
self.tags = image_metadata.get("tags")
self.annotations = image_metadata.get("annotations")
dataset = metadata_dict.get("dataset")
if dataset:
self._dataset = {
"id": dataset.get("id"),
"name": dataset.get("name"),
"type": dataset.get("type"),
}
def read_full_metadata(self):
logging.info("Reading full capture metadata from %s...", self.file)
exif_dict = self._read_exif()
return self._decode_usercomment(exif_dict)
@property
def dataset(self):
"""Read-only dataset property"""
return self._dataset
@property @property
def exists(self) -> bool: def exists(self) -> bool:
"""Check if capture data file exists on disk.""" """Check if capture data file exists on disk."""
@ -177,21 +194,6 @@ class CaptureObject(object):
else: else:
return False return False
@property
def dataset(self) -> str:
"""
If capture is part of a dataset, return basic dataset info.
Otherwise return None
"""
dataset = self.metadata.get("dataset")
if not dataset:
return None
return {
"id": dataset.get("id"),
"name": dataset.get("name"),
"type": dataset.get("type"),
}
# HANDLE TAGS # HANDLE TAGS
def put_tags(self, tags: list): def put_tags(self, tags: list):
""" """
@ -200,11 +202,7 @@ class CaptureObject(object):
Args: Args:
tags (list): List of tags to be added tags (list): List of tags to be added
""" """
for tag in tags: self.put_and_save(tags=tags)
if tag not in self.tags:
self.tags.append(tag)
self.save_metadata()
def delete_tag(self, tag: str): def delete_tag(self, tag: str):
""" """
@ -213,10 +211,12 @@ class CaptureObject(object):
Args: Args:
tag (str): Tag to be removed tag (str): Tag to be removed
""" """
# Update in-memory tag list
if tag in self.tags: if tag in self.tags:
self.tags = [new_tag for new_tag in self.tags if new_tag != tag] self.tags = [new_tag for new_tag in self.tags if new_tag != tag]
self.save_metadata() # Write in-memory metadata to file
self.put_and_save()
# HANDLE ANNOTATIONS # HANDLE ANNOTATIONS
@ -227,13 +227,15 @@ class CaptureObject(object):
Args: Args:
data (dict): Dictionary of metadata to be added data (dict): Dictionary of metadata to be added
""" """
self.annotations.update(data) self.put_and_save(annotations=data)
self.save_metadata()
def delete_annotation(self, key: str) -> None: def delete_annotation(self, key: str) -> None:
# Update in-memory annotations list
if key in self.annotations: if key in self.annotations:
del self.annotations[key] del self.annotations[key]
self.save_metadata()
# Write in-memory metadata to file
self.put_and_save()
# HANDLE METADATA # HANDLE METADATA
@ -244,17 +246,7 @@ class CaptureObject(object):
Args: Args:
data (dict): Dictionary of metadata to be added data (dict): Dictionary of metadata to be added
""" """
self._metadata.update(data) self.put_and_save(metadata=data)
self.save_metadata()
def set_metadata(self, data: dict) -> None:
"""
Write root metadata from a passed dictionary into the capture metadata.
Args:
data (dict): Dictionary of metadata to be added
"""
self._metadata = data
# BULK OPERATIONS # BULK OPERATIONS
@ -271,35 +263,42 @@ class CaptureObject(object):
if not metadata: if not metadata:
metadata = {} metadata = {}
# Tags # Update in-memory tags array
for tag in tags: for tag in tags:
if tag not in self.tags: if tag not in self.tags:
self.tags.append(tag) self.tags.append(tag)
# Annotations
# Update in-memory annotations dictionary
self.annotations.update(annotations) self.annotations.update(annotations)
# Metadata
self._metadata.update(metadata)
self.save_metadata() # Write new data to file EXIF, if supported
def save_metadata(self) -> None:
"""
Save metadata to exif, if supported
"""
if self.format.upper() in EXIF_FORMATS and self.exists: if self.format.upper() in EXIF_FORMATS and self.exists:
logging.debug("Writing exif data to capture file") logging.info("Writing Exif data to %s", self.file)
# Extract current Exif data # Extract current Exif data
exif_dict = piexif.load(self.file) exif_dict = self._read_exif()
metadata_dict = self._decode_usercomment(exif_dict) or {}
# Add new tags to exif dictionary
metadata_dict.get("image", {})["tags"] = self.tags
# Add new annotations to exif dictionary
metadata_dict.get("image", {})["annotations"] = self.annotations
# Add new custom metadata to exif dictionary
metadata_dict.update(metadata)
# Serialize metadata # Serialize metadata
metadata_string = json.dumps(self.metadata, cls=JSONEncoder) metadata_string = json.dumps(metadata_dict, cls=JSONEncoder)
logging.debug("Saving metadata string to file: %s", metadata_string) logging.debug("Saving metadata string to file: %s", metadata_string)
# Insert metadata into exif_dict # Insert metadata into exif_dict
exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode() exif_dict["Exif"][piexif.ExifIFD.UserComment] = metadata_string.encode()
# Convert new exif dict to exif bytes # Convert new exif dict to exif bytes
exif_bytes = piexif.dump(exif_dict) exif_bytes = piexif.dump(exif_dict)
# Insert exif into file # Insert exif into file
piexif.insert(exif_bytes, self.file) piexif.insert(exif_bytes, self.file)
logging.info("Finished saving metadata to %s", self.file) logging.info("Finished writing Exif data to %s", self.file)
# PROPERTIES # PROPERTIES
@ -309,42 +308,13 @@ class CaptureObject(object):
Create basic metadata dictionary from basic capture data, Create basic metadata dictionary from basic capture data,
and any added custom metadata and tags. and any added custom metadata and tags.
""" """
d = {
"image": {
"id": self.id,
"name": self.name,
"time": self.time.isoformat(),
"format": self.format,
"tags": self.tags,
"annotations": self.annotations,
},
**self._metadata,
}
# Add custom metadata to dictionary # Add custom metadata to dictionary
return d return self.read_full_metadata()
@property
def state(self) -> dict:
"""
Return a dictionary of objects full state, including metadata.
"""
# Create basic state dictionary
d = {"path": self.file, "name": self.name, "metadata": self.metadata}
# Combined availability of data
if self.exists:
d["available"] = True
else:
d["available"] = False
return d
@property @property
def data(self) -> io.BytesIO: def data(self) -> io.BytesIO:
""" """
Return a byte string of the capture data. Return a BytesIO object of the capture data.
""" """
if self.exists: # If data file exists if self.exists: # If data file exists
@ -390,7 +360,7 @@ class CaptureObject(object):
"""Write stream to file, and save/update metadata file""" """Write stream to file, and save/update metadata file"""
# If a stream OR file exists, save the metadata file # If a stream OR file exists, save the metadata file
if self.exists: if self.exists:
self.save_metadata() self.put_and_save()
def delete(self) -> bool: def delete(self) -> bool:
"""If the StreamObject has been saved, delete the file.""" """If the StreamObject has been saved, delete the file."""