#!/usr/bin/env python3 import jinja2 from PIL import Image import argparse from pathlib import Path import yaml import os import re import hashlib imagereg = re.compile("([a-z0-9]*)\\..*") resreg = re.compile("([0-9]*)x([0-9]*)") 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): m = resreg.match(res) return (int(m.group(1)), int(m.group(2))) class FileLoader(jinja2.BaseLoader): def get_source(self, environment, template): if not os.path.exists(template): raise TemplateNotFound(template) mtime = os.path.getmtime(template) with open(template, "r") as f: source = f.read() return source, template, lambda: mtime == getmtime(template) class ImageLocation: def __init__(self, folder): self.folder = folder self.stuff = {} 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, settings): # First determine the hash from settings thash = hashlib.md5(bytes( f"{settings['res']}+{settings['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}.{settings['ext']}" print(f"Converting file {imgname} to {fname}") im = Image.open(imgname) im = im.resize(settings["res"], Image.ANTIALIAS) tmpname = f"{self.tmpname}.{settings['ext']}" 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, settings, relto=None): # Check if we already have it fname, hash = self.convert(imgname, settings) # 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 Renderer: def __init__(self, config_file, loadpath, resolution, extension): self.config = Renderer.__load_config(config_file) self.loadpath = loadpath self.scaled = None self.settings = { "res": parse_res(resolution), "ext": extension, } 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 imgfetch_gen(self, loc, dest): def fn(img): return loc.fetch(img, self.settings, relto=dest) return fn def build_to(self, dest, template, context, loc=None, clean=False): if loc is None: loc = ImageLocation(os.path.join(dest, "imgs")) jenv = jinja2.Environment( loader = FileLoader(), autoescape=jinja2.select_autoescape(['html', 'xml']) ) jenv.globals.update( imgfetch=self.imgfetch_gen(loc, dest) ) tmpl = jenv.get_template(template) with open(os.path.join(dest, "index.html"), "w") as f: f.write(tmpl.render({**self.config, **context})) if clean: loc.clean() parser = argparse.ArgumentParser() parser.add_argument("--dest", "-d", default="build", help="where to put resulting files") parser.add_argument("--size", "-s", default="1920x1080", 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="imginfo.yml", 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", "-l", default=".", help="where to load full size images from") parser.add_argument("--ext", "-e", default="png", help="image extension to use") args = parser.parse_args() context = { "cgit": args.cgit, "git": args.commit } rend = Renderer(args.config, args.load, args.size, args.ext) rend.build_to(args.dest, args.template, context, clean=args.clean)