#!/usr/bin/env python3 import os import shutil import re import subprocess import yaml from operator import attrgetter, itemgetter from pathlib import Path from jinja2 import Environment, PackageLoader, select_autoescape from PIL import Image from configuration import OUTPUT_DIR, PICTURES_DIR, SIZES, THEME_DIR FNULL = open(os.devnull, "w") jinja_env = Environment( loader=PackageLoader("revelateur", THEME_DIR), autoescape=select_autoescape(["html"]), ) def neighborhood(iterable, first=None, last=None): """ Yield the (previous, current, next) items given an iterable. You can specify a `first` and/or `last` item for bounds. """ iterator = iter(iterable) previous = first current = next(iterator) # Throws StopIteration if empty. for next_ in iterator: yield (previous, current, next_) previous = current current = next_ yield (previous, current, last) def list_galleries_in(path): gallery_dirs = [f for f in os.scandir(path) if f.is_dir()] for gallery_dir in gallery_dirs: gallery_path = Path(gallery_dir.path) metadata = {} metadata_path = gallery_path / "metadata.yml" if metadata_path.exists(): metadata = yaml.safe_load(metadata_path.read_text()) private = False slug = gallery_dir.name password = metadata.get("password", None) if password: private = True slug = f"{slug}-{password}" photos = list(list_photos_in(gallery_dir, slug)) if not photos: continue photos.sort(key=itemgetter("name")) cover_photo = find_photo(photos, metadata.get("cover", "01")) gallery = { "name": metadata.get("name", gallery_dir.name), "type": metadata.get("type", "photo"), "path": gallery_path, "slug": slug, "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_slug): photo_files = [ f for f in os.scandir(gallery_dir) if re.match(".+\.jpg", f.name, re.I) ] for photo_file in sorted(photo_files, key=attrgetter("name")): with Image.open(photo_file.path) as image: width, height = image.size name = Path(photo_file.name).stem page_url = f"{gallery_slug}-{name}.html" photo = { "gallery_slug": gallery_slug, "name": name, "path": photo_file.path, "url": Path(gallery_slug) / photo_file.name, "page_url": page_url, "width": width, "height": height, "narrow": height > width, "wide": height * 2 < width, } yield photo def create_output_dir(): if not OUTPUT_DIR.exists(): os.mkdir(OUTPUT_DIR) def copy_theme_folder(name, target=None): local_path = THEME_DIR / name output_path = OUTPUT_DIR / (target if target is not None else name) if output_path.exists() and target is None: shutil.rmtree(output_path) shutil.copytree(local_path, output_path, dirs_exist_ok=True) def generate_index(output_path, galleries): index_template = jinja_env.get_template("index.html.j2") content = index_template.render(galleries=galleries, sizes=SIZES) (OUTPUT_DIR / "index.html").write_text(content) def generate_gallery(output_path, gallery): generate_gallery_photos(output_path, gallery) generate_gallery_pages(output_path, gallery) def generate_gallery_pages(output_path, gallery): photo_template = jinja_env.get_template("media.html.j2") for previous, photo, next_ in neighborhood(gallery["photos"]): content = photo_template.render( photo=photo, previous=previous, next=next_, gallery=gallery, sizes=SIZES ) (OUTPUT_DIR / photo["page_url"]).write_text(content) def generate_gallery_photos(output_path, gallery): gallery_output_path_jpg = output_path / gallery["slug"] / "jpg" gallery_output_path_webp = output_path / gallery["slug"] / "webp" os.makedirs(gallery_output_path_jpg, exist_ok=True) os.makedirs(gallery_output_path_webp, exist_ok=True) for photo in gallery["photos"]: # Let's not share the original file for now. # photo_output_path = output_path / photo["url"] # if not photo_output_path.exists(): # shutil.copyfile(photo["path"], photo_output_path) for width, height in SIZES: jpg_output_path = generate_jpg_photo_for_size( gallery_output_path_jpg, photo, width, height ) generate_webp_photo_for_size( gallery_output_path_webp, jpg_output_path, photo, width, height ) def generate_webp_photo_for_size( gallery_output_path, jpg_output_path, photo, width, height ): output_path = gallery_output_path / f"{photo['name']}_{width}x{height}.webp" if output_path.exists(): return command = ["cwebp", "-q", "80", jpg_output_path, "-o", output_path] subprocess.Popen(command, stdout=FNULL, stderr=subprocess.STDOUT).communicate() def generate_jpg_photo_for_size(gallery_output_path, photo, width, height): output_path = gallery_output_path / f"{photo['name']}_{width}x{height}.jpg" if output_path.exists(): return output_path with Image.open(photo["path"]) as image: w, h = image.size if w > width and h > height: # 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. in case of (440, 264): # 1200x900 is resized to 440x330 # 1200x600 is resized to 528x264 if width / height <= w / h: w = int(max(height * w / h, 1)) h = height else: h = int(max(width * h / w, 1)) w = width new_size = (w, h) image.draft(None, new_size) image = image.resize(new_size, Image.BICUBIC) image.save(output_path) return output_path def main(): print("Loading galleries... ", end="") galleries = list(list_galleries_in(PICTURES_DIR)) if not galleries: print(f"No galleries found in {PICTURES_DIR}") return galleries.sort(key=itemgetter("path"), reverse=True) print(f"{len(galleries)} galleries found.") print("Creating output folder... ", end="") create_output_dir() print("✔️") print("Copying style folder... ", end="") copy_theme_folder("style") print("✔️") print("Copying scripts folder... ", end="") copy_theme_folder("scripts") print("✔️") print("Copying root (favicon, etc) folder... ", end="") copy_theme_folder("root", "") print("✔️") print("Generating index file... ", end="") generate_index(OUTPUT_DIR, galleries) print("✔️") for gallery in galleries: print( ( f"Generating {gallery['name']} gallery " f"(http://localhost:8080/{gallery['cover_photo']['page_url']})... " ), end="", ) generate_gallery(OUTPUT_DIR, gallery) print("✔️") print("Galleries generated 🎉") if __name__ == "__main__": main()