123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- #!/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()
|