#!/usr/bin/env python3 import jinja2 import PIL.Image import PIL.ImageOps import argparse import json import yaml import os import re import hashlib import glob from datetime import datetime imagereg = re.compile("([a-z0-9]*)\\..*") thumbsize = 480 def hashfile(fname): md5 = hashlib.md5() print(fname) with open(fname, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): md5.update(chunk) return md5 def parse_res(res): return int(res) class FileLoader(jinja2.BaseLoader): def get_source(self, environment, template): if not os.path.exists(template): raise jinja2.TemplateNotFound(template) mtime = os.path.getmtime(template) with open(template, "r") as f: source = f.read() return source, template, lambda: mtime == os.path.getmtime(template) class ImageLocation: def __init__(self, folder, extension): self.folder = folder self.stuff = {} self.ext = extension self.unused = set() self.existing = {} if not os.path.isdir(folder): os.mkdir(folder) self.look() self.tmpname = os.path.join(folder, "tmp") def look(self): for name in os.listdir(self.folder): m = imagereg.match(name) if m is not None: fhash = m.group(1) self.unused.add(fhash) self.existing[fhash] = name def convert(self, imgname, resolution): # First determine the hash from settings thash = hashlib.md5( bytes( f"{resolution}+{self.ext}" + imgname, encoding="utf8") ).hexdigest() # Now check if it exists if thash in self.existing: print(f"Skipping file {imgname}[{thash}]") return self.existing[thash], thash # Okay convert it fname = f"{thash}.{self.ext}" print(f"Converting file {imgname} to {fname}") tmpname = f"{self.tmpname}.{self.ext}" with PIL.Image.open(imgname) as im: target_height = resolution im = PIL.ImageOps.exif_transpose(im) if im.size[0] > target_height: res = (int((im.size[0] / im.size[1]) * target_height), target_height) im = im.resize(res, PIL.Image.ANTIALIAS) im.save(tmpname) os.rename(tmpname, os.path.join(self.folder, fname)) self.existing[thash] = fname self.unused.add(thash) return fname, thash def fetch(self, imgname, resolution, relto=None): # Check if we already have it fname, hash = self.convert(imgname, resolution) # Mark the hash as used self.unused.remove(hash) if relto is not None: full = os.path.join(self.folder, fname) return os.path.relpath(full, start=relto) return fname def clean(self): print("Running cleanup") for uhash in self.unused: fname = os.path.join(self.folder, self.existing[uhash]) print(f"Removing file {fname}") os.remove(fname) class Image: def __init__(self, filename): self.name = os.path.basename(filename) self.filename = filename self.description = "" self.url = "" self.thumburl = "" self.fullurl = "" self.file = filename self.taken = None self.load_metadata() def load_metadata(self): try: with PIL.Image.open(self.filename) as im: taken_str = im.getexif()[36867] except KeyError: print(f"Could not load date from image {self.filename}") return self.taken = datetime.strptime(taken_str, "%Y:%m:%d %H:%M:%S") def value_or_default(val, default=None): if val is None: return default return val class Loader: def __init__(self, config_file, loadpath): self.config = Loader.__load_config(config_file) self.loadpath = loadpath self.images = [] def __load_config(config_file): config = {} with open(config_file, "r") as f: config = yaml.safe_load(f) print(f"Loaded config: {config}") return config def load(self, imagehook=None, clear=False): # Clear if neccesary if clear: self.images = [] # First find all images from glob for image in glob.iglob(os.path.join(self.loadpath, self.config["imageglob"])): img = Image(image) img.description = self.config["info"].get(img.name, "") if imagehook is not None: img = imagehook(img) self.images.append(img) self.images = sorted(self.images, key=lambda img: value_or_default(img.taken, datetime.min), reverse=True) def to_json(self): jsonable = [img.__dict__ for img in self.images] return json.dumps(jsonable) parser = argparse.ArgumentParser() parser.add_argument("--dest", "-d", default="dist", help="where to put resulting files") parser.add_argument("--size", "-s", type=int, default="1080", help="size to scale web images to") parser.add_argument("--commit", "-g", help="git commit hash to announce") parser.add_argument("--clean", help="clean unused image files", action="store_true") parser.add_argument("--config", "-c", default=None, help="where to load image definitions from") parser.add_argument("--template", "-t", default="index.html.j2", help="html template to use") parser.add_argument("--cgit", "-w", help="cgit repo base url") parser.add_argument("load", help="where to load full size images from") parser.add_argument("--ext", "-e", default="png", help="image extension to use") args = parser.parse_args() if args.config is None: args.config = os.path.join(args.load, "imginfo.yml") context = { "cgit": args.cgit, "git": args.commit } loader = Loader(args.config, args.load) loc = ImageLocation(os.path.join(args.dest, "imgs"), args.ext) def imgload(img): if context["cgit"] is not None: img.fullurl = os.path.join(context["cgit"], "plain", img.name) img.url = loc.fetch(img.file, args.size, relto=args.dest) img.thumburl = loc.fetch(img.file, thumbsize, relto=args.dest) return img loader.load(imgload, clear=True) with open(os.path.join(args.dest, "imgs", "index.json"), "w") as f: f.write(loader.to_json())