#!/usr/bin/env python3 import os import shutil import re import yaml from operator import itemgetter from jinja2 import Environment, PackageLoader, select_autoescape from PIL import Image, ExifTags from dotenv import load_dotenv from configuration import SITE_TITLE, SITE_AUTHOR, SITE_AUTHOR_WEBSITE, THEME # Load env var from .env file load_dotenv(dotenv_path=".env") PICTURES_DIR_NAME = "photos" OUTPUT_DIR_NAME = "output" # Configure YAML to accept environment variables in metadata files # Based on https://stackoverflow.com/a/27232341 solution pattern = re.compile(r"^ENV\[\'(.*)\'\]$") yaml.add_implicit_resolver("!pathex", pattern, Loader=yaml.SafeLoader) def pathex_constructor(loader, node): value = loader.construct_scalar(node) env_var, = pattern.match(value).groups() return os.getenv(env_var) yaml.add_constructor("!pathex", pathex_constructor, yaml.SafeLoader) def list_galleries_in(path): gallery_dirs = [f for f in os.scandir(path) if f.is_dir()] for gallery_dir in gallery_dirs: metadata = {} metadata_path = os.path.join(gallery_dir.path, "metadata.yml") if os.path.exists(metadata_path): with open(metadata_path, "r") as metadata_file: metadata = yaml.safe_load(metadata_file) private = False url = f"{gallery_dir.name}.html" output_path = gallery_dir.name password = metadata.get("password", None) if password: private = True url = f"{gallery_dir.name}-{password}.html" output_path = f"{output_path}-{password}" photos = list(list_photos_in(gallery_dir, output_path)) if len(photos) == 0: continue photos.sort(key=itemgetter("name")) # Try to get cover from metadata, if it doesn't exist, take one by # default. cover_photo = None cover_name = metadata.get("cover") if cover_name: cover_photo = find_photo(photos, cover_name) if cover_photo is None: cover_index = (len(gallery_dir.name) + 42) % len(photos) cover_photo = photos[cover_index] gallery = { "name": metadata.get("name", gallery_dir.name), "path": gallery_dir.path, "output_path": output_path, "url": url, "num_photos": len(photos), "photos": photos, "cover_photo": cover_photo, "private": private, } yield gallery def find_photo(photos, photo_name): for photo in photos: if photo["name"] == photo_name: return photo return None def list_photos_in(gallery_dir, gallery_output_path): photo_files = [ f for f in os.scandir(gallery_dir) if re.match(".+\.jpg", f.name, re.I) ] for photo_file in photo_files: url = os.path.join(gallery_output_path, photo_file.name) thumb_url = os.path.join(gallery_output_path, f"thumb_{photo_file.name}") photo = { "name": photo_file.name, "path": photo_file.path, "url": url, "thumb_url": thumb_url, } yield photo def generate_output_dir(): output_path = os.path.join(os.curdir, OUTPUT_DIR_NAME) if not os.path.isdir(output_path): os.mkdir(output_path) return output_path def generate_style(output_path): style_path = os.path.join(os.curdir, THEME, "style") style_output_path = os.path.join(output_path, "style") if os.path.isdir(style_output_path): shutil.rmtree(style_output_path) shutil.copytree(style_path, style_output_path) def generate_index(output_path, galleries): index_path = os.path.join(output_path, "index.html") theme_path = os.path.join(os.curdir, THEME) jinja_env = Environment( loader=PackageLoader("boop", theme_path), autoescape=select_autoescape(["html"]) ) index_template = jinja_env.get_template("index.html.j2") with open(index_path, "w") as index_file: index_file.write( index_template.render( galleries=galleries, site_title=SITE_TITLE, site_author=SITE_AUTHOR, site_author_website=SITE_AUTHOR_WEBSITE, ) ) def generate_gallery(output_path, gallery): generate_gallery_index(output_path, gallery) generate_gallery_photos(output_path, gallery) def generate_gallery_index(output_path, gallery): gallery_index_path = os.path.join(output_path, gallery["url"]) theme_path = os.path.join(os.curdir, THEME) jinja_env = Environment( loader=PackageLoader("boop", theme_path), autoescape=select_autoescape(["html"]) ) gallery_template = jinja_env.get_template("gallery.html.j2") with open(gallery_index_path, "w") as gallery_file: gallery_file.write( gallery_template.render( gallery=gallery, site_title=SITE_TITLE, site_author=SITE_AUTHOR, site_author_website=SITE_AUTHOR_WEBSITE, ) ) def generate_gallery_photos(output_path, gallery): gallery_output_path = os.path.join(output_path, gallery["output_path"]) if not os.path.isdir(gallery_output_path): os.mkdir(gallery_output_path) for photo in gallery["photos"]: photo_output_path = os.path.join(output_path, photo["url"]) if not os.path.exists(photo_output_path): shutil.copyfile(photo["path"], photo_output_path) thumb_output_path = os.path.join(output_path, photo["thumb_url"]) if not os.path.exists(thumb_output_path): generate_thumb_file(thumb_output_path, photo) def generate_thumb_file(output_path, photo): orientation_key = get_orientation_exif_key() size = (440, 264) with Image.open(photo["path"]) as image: # First, make sure image is correctly oriented exif = image._getexif() if exif[orientation_key] == 3: image = image.rotate(180, expand=True) elif exif[orientation_key] == 6: image = image.rotate(270, expand=True) elif exif[orientation_key] == 8: image = image.rotate(90, expand=True) w, h = image.size if w > size[0] and h > size[1]: # If the original file is larger in width AND height, we resize # first the image to the lowest size accepted (both width and # height stays greater or equal to requested size). # E.g. 1200x900 is resized to 440x330 # 1200x600 is resized to 528x264 if size[0] / size[1] <= w / h: w = int(max(size[1] * w / h, 1)) h = 264 else: h = int(max(size[0] * h / w, 1)) w = 440 new_size = (w, h) image.draft(None, new_size) image = image.resize(new_size, Image.BICUBIC) # We now have an image with at least w = 440 OR h = 264 (unless one of # the size is smaller). But the image can still be larger than # requested size, so we have to crop the image in the middle. crop_box = None if w > size[0]: left = (w - size[0]) / 2 right = left + size[0] crop_box = (left, 0, right, h) elif h > size[1]: upper = (h - size[1]) / 2 lower = upper + size[1] crop_box = (0, upper, w, lower) if crop_box is not None: image = image.crop(crop_box) # And we save the final image. image.save(output_path) def get_orientation_exif_key(): for (key, tag) in ExifTags.TAGS.items(): if tag == "Orientation": return key def main(): print("Loading galleries... ", end="") pictures_folder = os.path.join(os.curdir, PICTURES_DIR_NAME) galleries = list(list_galleries_in(pictures_folder)) if len(galleries) == 0: print(f"No galleries found in {pictures_folder}") return galleries.sort(key=itemgetter("name")) print(f"{len(galleries)} galleries found.") print("Generating output folder... ", end="") output_path = generate_output_dir() print("✔️") print("Generating style folder... ", end="") generate_style(output_path) print("✔️") print("Generating index file... ", end="") generate_index(output_path, galleries) print("✔️") for gallery in galleries: print(f"Generating {gallery['name']} gallery ({gallery['url']})... ", end="") generate_gallery(output_path, gallery) print("✔️") print("Galleries generated 🎉") if __name__ == "__main__": main()