Files
website/tools/content/static_files.py
T

107 lines
3.6 KiB
Python

import argparse
import shutil
import sys
from pathlib import Path
from PIL import Image, ImageOps
from pillow_heif import register_heif_opener
from jsonfile import PROJECT_ROOT
FILES_DIR = PROJECT_ROOT / "static" / "files"
ORIGINALS_DIR = PROJECT_ROOT / "tools" / "content" / ".cache" / "heic-originals"
HEIC_EXTENSIONS = {".heic", ".heif"}
PHOTO_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
JPEG_QUALITY = 90
GPS_TAG = 0x8825
register_heif_opener()
def visible_files(folder: Path, extensions: set[str]) -> list[Path]:
return sorted(
path
for path in folder.rglob("*")
if path.is_file() and path.suffix.lower() in extensions and not any(part.startswith(".") for part in path.relative_to(folder).parts)
)
def save_without_metadata(image: Image.Image, target: Path) -> None:
upright = ImageOps.exif_transpose(image)
if target.suffix.lower() in {".jpg", ".jpeg"}:
upright.convert("RGB").save(target, "JPEG", quality=JPEG_QUALITY, optimize=True)
else:
upright.save(target)
def keep_original(source: Path) -> Path:
backup = ORIGINALS_DIR / source.relative_to(FILES_DIR)
backup.parent.mkdir(parents=True, exist_ok=True)
shutil.move(source, backup)
return backup
def convert_heic(source: Path, delete_originals: bool) -> str:
target = source.with_suffix(".jpg")
if target.exists():
return f"skipped {source.relative_to(FILES_DIR)}, {target.name} already exists"
with Image.open(source) as image:
save_without_metadata(image, target)
if delete_originals:
source.unlink()
return f"converted {target.relative_to(FILES_DIR)}"
return f"converted {target.relative_to(FILES_DIR)}, original kept in {keep_original(source).relative_to(PROJECT_ROOT)}"
def has_location(path: Path) -> bool:
try:
with Image.open(path) as image:
return bool(image.getexif().get_ifd(GPS_TAG))
except OSError:
return False
def strip_location(path: Path) -> str:
with Image.open(path) as image:
image.load()
save_without_metadata(image, path)
return f"removed location from {path.relative_to(FILES_DIR)}"
def report(action) -> int:
try:
print(action())
return 0
except (OSError, ValueError) as error:
print(f"Error: {error}", file=sys.stderr)
return 1
def main() -> int:
parser = argparse.ArgumentParser(description="Convert HEIC photos in static/files to JPEG and remove location data from photos")
parser.add_argument("--delete-originals", action="store_true", help="delete converted HEIC files instead of moving them to tools/content/.cache")
parser.add_argument("--keep-location", action="store_true", help="leave GPS data in existing photos")
arguments = parser.parse_args()
if not FILES_DIR.exists():
print(f"{FILES_DIR.relative_to(PROJECT_ROOT)} does not exist", file=sys.stderr)
return 1
heic_files = visible_files(FILES_DIR, HEIC_EXTENSIONS)
located_photos = [] if arguments.keep_location else [photo for photo in visible_files(FILES_DIR, PHOTO_EXTENSIONS) if has_location(photo)]
if not heic_files and not located_photos:
print("Nothing to do: no HEIC files and no photos with location data")
return 0
failed = 0
for source in heic_files:
failed += report(lambda: convert_heic(source, arguments.delete_originals))
for photo in located_photos:
failed += report(lambda: strip_location(photo))
print(f"Done: {len(heic_files) + len(located_photos) - failed} changed, {failed} failed")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())