#!/usr/bin/env python3 import os import shutil import re from operator import itemgetter from jinja2 import Environment, PackageLoader, select_autoescape from PIL import Image, ExifTags from configuration import SITE_TITLE, SITE_AUTHOR, SITE_AUTHOR_WEBSITE, THEME PICTURES_DIR_NAME = "photos" OUTPUT_DIR_NAME = "output" def list_galleries_in(path): gallery_dirs = [f for f in os.scandir(path) if f.is_dir()] for gallery_dir in gallery_dirs: photos = list(list_photos_in(gallery_dir)) if len(photos) == 0: continue photos.sort(key=itemgetter("name")) cover_index = (len(gallery_dir.name) + 42) % len(photos) gallery = { "name": gallery_dir.name, "path": gallery_dir.path, "output_path": gallery_dir.name, "url": f"{gallery_dir.name}.html", "num_photos": len(photos), "photos": photos, "cover_photo": photos[cover_index], } yield gallery def list_photos_in(gallery_dir): 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_dir.name, photo_file.name) thumb_url = os.path.join(gallery_dir.name, 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_dir(output_path, gallery) def generate_gallery_index(output_path, gallery): gallery_index_path = os.path.join(output_path, f"{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_dir(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: 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... ", end="") generate_gallery(output_path, gallery) print("✔️") print("Galleries generated 🎉") if __name__ == "__main__": main()