metadata-scrubber-tool:
added functionality for wiping exif data from jpeg images
This commit is contained in:
parent
a5047a552c
commit
ceb5ef6d55
|
|
@ -0,0 +1,2 @@
|
||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# Utility functions for commands
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import piexif
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from src.services.metadata_handler import MetadataHandler
|
||||||
|
from src.utils.jpeg_metadata import JpegProcessaor
|
||||||
|
|
||||||
|
|
||||||
|
class ImageHandler(MetadataHandler):
|
||||||
|
def __init__(self, filepath: str):
|
||||||
|
super().__init__(filepath)
|
||||||
|
self.processors = {".jpeg": JpegProcessaor(), ".jpg": JpegProcessaor()}
|
||||||
|
self.tags_to_delete = []
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
"""Extracts metadata into a standard dictionary."""
|
||||||
|
with Image.open(Path(self.filepath)) as img:
|
||||||
|
extension = Path(self.filepath).suffix
|
||||||
|
processor = self.processors.get(extension)
|
||||||
|
|
||||||
|
if not processor:
|
||||||
|
raise ValueError(f"Unsupported format: {extension}")
|
||||||
|
|
||||||
|
self.metadata = processor.get_metadata(img)["data"]
|
||||||
|
self.tags_to_delete = processor.get_metadata(img)["tags_to_delete"]
|
||||||
|
return self.metadata
|
||||||
|
|
||||||
|
def wipe(self) -> None:
|
||||||
|
"""Wipes internal metadata state."""
|
||||||
|
with Image.open(Path(self.filepath)) as img:
|
||||||
|
extension = Path(self.filepath).suffix
|
||||||
|
processor = self.processors.get(extension)
|
||||||
|
|
||||||
|
if not processor:
|
||||||
|
raise ValueError(f"Unsupported format: {extension}")
|
||||||
|
|
||||||
|
self.processed_metadata = processor.delete_metadata(
|
||||||
|
img, self.tags_to_delete
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, output_path: Optional[str] = None) -> None:
|
||||||
|
"""Writes the changes to a copy of the original file."""
|
||||||
|
destination_dir = Path(output_path or "/archive/")
|
||||||
|
|
||||||
|
# creates the destination directory if it doesn't exist
|
||||||
|
destination_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination_file_path = (
|
||||||
|
destination_dir / f"processed_{Path(self.filepath).name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# copies the original file to the destination directory
|
||||||
|
shutil.copy2(self.filepath, destination_file_path)
|
||||||
|
|
||||||
|
# writes the processed metadata to the image in the destination directory
|
||||||
|
with Image.open(destination_file_path) as img:
|
||||||
|
exif_bytes = piexif.dump(self.processed_metadata)
|
||||||
|
img.save(destination_file_path, exif=exif_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
test = ImageHandler(r"C:\Users\Xheri\development\testimage\20221201_090615.jpg")
|
||||||
|
test.read()
|
||||||
|
test.wipe()
|
||||||
|
test.save()
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataHandler(ABC):
|
||||||
|
def __init__(self, filepath: str):
|
||||||
|
self.filepath = filepath
|
||||||
|
self.metadata: Dict[str, Any] = {}
|
||||||
|
self.processed_metadata: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read(self) -> Dict[str, Any]:
|
||||||
|
"""Extracts metadata into a standard dictionary."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def wipe(self) -> None:
|
||||||
|
"""Updates internal metadata state."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save(self, output_path: Optional[str] = None) -> None:
|
||||||
|
"""Writes the changes to a file."""
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import piexif
|
||||||
|
|
||||||
|
|
||||||
|
class JpegProcessaor:
|
||||||
|
def __init__(self):
|
||||||
|
self.tags_to_delete = []
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
def get_metadata(self, img):
|
||||||
|
if "exif" in img.info:
|
||||||
|
exif_dict = piexif.load(img.info["exif"])
|
||||||
|
for ifd, value in exif_dict.items():
|
||||||
|
# this exclude thumbnail IFD (its the thumbnails blob data. it can be removed but the image will take a couple more seconds to load)
|
||||||
|
if not isinstance(exif_dict[ifd], dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# iterate through the IFD
|
||||||
|
for tag, tag_value in exif_dict[ifd].items():
|
||||||
|
tag_info = piexif.TAGS[ifd].get(tag, {})
|
||||||
|
|
||||||
|
# Get the human-readable name for the tag
|
||||||
|
tag_name = (
|
||||||
|
tag_info.get("name", "Unknown Tag")
|
||||||
|
if tag_info
|
||||||
|
else "Unknown Tag"
|
||||||
|
)
|
||||||
|
|
||||||
|
# exculudes tags that are necessary for image display integrity
|
||||||
|
if (
|
||||||
|
tag_name == "Orientation"
|
||||||
|
or tag_name == "ColorSpace"
|
||||||
|
or tag_name == "ExifTag"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# save to list and dict
|
||||||
|
self.tags_to_delete.append(tag)
|
||||||
|
self.data[tag_name] = tag_value
|
||||||
|
|
||||||
|
return {"data": self.data, "tags_to_delete": self.tags_to_delete}
|
||||||
|
|
||||||
|
def delete_metadata(self, img, tags_to_delete):
|
||||||
|
exif_dict = piexif.load(img.info["exif"])
|
||||||
|
for ifd, value in exif_dict.items():
|
||||||
|
# exclude thumbnail IFD (its the thumbnails blob data so i dont wanna deal with that)
|
||||||
|
if not isinstance(exif_dict[ifd], dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# iterate through and delete tags
|
||||||
|
for tag in list(exif_dict[ifd]):
|
||||||
|
if tag in tags_to_delete:
|
||||||
|
del exif_dict[ifd][tag]
|
||||||
|
|
||||||
|
return exif_dict
|
||||||
Loading…
Reference in New Issue