107 lines
3.6 KiB
Python
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())
|