feat: ADD optional exif data removable in files #18

This commit is contained in:
Caffeine Fueled 2026-07-04 07:59:35 +02:00
parent 5b6edac236
commit 7a8ecafc3a
Signed by: cf7
GPG key ID: CA295D643074C68C
4 changed files with 99 additions and 3 deletions

View file

@ -12,6 +12,16 @@ from config import (BLOG_TITLE, BLOG_DESCRIPTION, THEME, EXCLUDE_SECTIONS_FROM_M
BASE_URL, AUTHOR_NAME, AUTHOR_EMAIL, FEED_MAX_ITEMS,
BLOGROLL_PATH, ROOT_PAGE)
# STRIP_IMAGE_EXIF is a newer setting; older config.py files may not define it.
# Default to enabled so the privacy feature works without a config update.
try:
from config import STRIP_IMAGE_EXIF
except ImportError:
STRIP_IMAGE_EXIF = True
# Raster image formats whose metadata (EXIF/GPS/XMP/text chunks) is stripped on copy
STRIP_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.tiff', '.tif', '.bmp'}
class SSGGGenerator:
def __init__(self, items_dir='items', output_dir='output', theme=None, blog_title=None, blog_description=None):
self.items_dir = Path(items_dir)
@ -29,6 +39,7 @@ class SSGGGenerator:
self.hide_logo = HIDE_LOGO
self.hide_title = HIDE_TITLE
self.logo_path = LOGO_PATH
self.strip_image_exif = STRIP_IMAGE_EXIF
# Setup Jinja2
self.env = Environment(loader=FileSystemLoader(self.templates_dir))
@ -383,6 +394,38 @@ class SSGGGenerator:
print(f"✓ Generated {output_path}")
def strip_image_metadata(self, src_path, dest_path):
"""Re-encode an image to dest_path without any metadata (EXIF/GPS/XMP/text).
Pillow does not carry metadata across a save unless it is passed
explicitly, so re-saving produces a clean image. Returns True on
success, or False if the image should be copied verbatim instead
(unsupported/animated) or re-encoding failed.
"""
from PIL import Image
try:
with Image.open(src_path) as img:
img.load()
# Animated images (GIF/WebP/APNG) are copied as-is to avoid
# dropping frames; they don't carry camera EXIF/GPS anyway.
if getattr(img, 'is_animated', False):
return False
save_kwargs = {}
if img.format == 'JPEG':
# 'keep' reuses the original quantization tables, so there
# is no generational quality loss from re-encoding.
save_kwargs['quality'] = 'keep'
# Saving without exif=/pnginfo= drops all metadata.
img.save(dest_path, format=img.format, **save_kwargs)
return True
except Exception as e:
print(f" ⚠ Could not strip metadata from {src_path.name} ({e}); copying as-is")
return False
def copy_assets(self):
"""Copy theme assets and images to output directory"""
import shutil
@ -395,14 +438,43 @@ class SSGGGenerator:
shutil.copytree(self.assets_dir, dest_dir)
print(f"✓ Copied theme assets to output")
# Copy images
# Copy images (stripping EXIF/metadata when enabled)
images_dir = Path('images')
if images_dir.exists():
dest_dir = self.output_dir / 'images'
if dest_dir.exists():
shutil.rmtree(dest_dir)
shutil.copytree(images_dir, dest_dir)
print(f"✓ Copied images/ to output")
dest_dir.mkdir(parents=True, exist_ok=True)
# Only strip if enabled and Pillow is importable; otherwise copy as-is.
pillow_available = self.strip_image_exif
if self.strip_image_exif:
try:
import PIL # noqa: F401
except ImportError:
pillow_available = False
print(" ⚠ STRIP_IMAGE_EXIF is enabled but Pillow is not installed; "
"copying images without stripping (pip install pillow)")
stripped = 0
for item in images_dir.rglob('*'):
if not item.is_file():
continue
rel_path = item.relative_to(images_dir)
dest_path = dest_dir / rel_path
dest_path.parent.mkdir(parents=True, exist_ok=True)
if (pillow_available
and item.suffix.lower() in STRIP_IMAGE_EXTENSIONS
and self.strip_image_metadata(item, dest_path)):
stripped += 1
else:
shutil.copy2(item, dest_path)
if pillow_available:
print(f"✓ Copied images/ to output ({stripped} stripped of EXIF/metadata)")
else:
print(f"✓ Copied images/ to output")
# Copy static files (GPG keys, .well-known, etc.)
static_dir = Path('static')