diff options
-rwxr-xr-x | build.py | 245 | ||||
-rw-r--r-- | index.html.j2 | 50 | ||||
-rw-r--r-- | shell.nix | 7 |
3 files changed, 302 insertions, 0 deletions
diff --git a/build.py b/build.py new file mode 100755 index 0000000..f018b94 --- /dev/null +++ b/build.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +import jinja2 +import PIL.Image +import PIL.ImageOps +import argparse +import yaml +import os +import re +import hashlib +import glob +from datetime import datetime + +imagereg = re.compile("([a-z0-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): + 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): + 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}") + + tmpname = f"{self.tmpname}.{settings['ext']}" + with PIL.Image.open(imgname) as im: + target_height = settings["res"] + 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, 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 Image: + def __init__(self, filename): + self.name = os.path.basename(filename) + self.filename = filename + self.description = "" + self.scaledpath = "" + self.fullurl = "" + + 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) + + +class Renderer: + def __init__(self, loader, resolution, extension): + self.loader = loader + self.scaled = None + + self.settings = { + "res": parse_res(resolution), + "ext": extension, + } + + def build_to(self, dest, template, context, loc=None, clean=False): + # Load images + if loc is None: + loc = ImageLocation(os.path.join(dest, "imgs")) + + def imgproc(img): + if context["cgit"] is not None: + img.fullurl = os.path.join(context["cgit"], "plain", img.name) + + img.scaledpath = loc.fetch(img.filename, self.settings, relto=dest) + + return img + + self.loader.load(imgproc, clear=True) + + jenv = jinja2.Environment( + loader=FileLoader(), + autoescape=jinja2.select_autoescape(['html', 'xml']) + ) + + tmpl = jenv.get_template(template) + + with open(os.path.join(dest, "index.html"), "w") as f: + f.write(tmpl.render({ + "ctx": context, + "images": self.loader.images + })) + + 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="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="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 + } + +loader = Loader(args.config, args.load) +rend = Renderer(loader, args.size, args.ext) +rend.build_to(args.dest, args.template, context, clean=args.clean) diff --git a/index.html.j2 b/index.html.j2 new file mode 100644 index 0000000..ba58b70 --- /dev/null +++ b/index.html.j2 @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Jtle images</title> + <link rel="icon" href="/favicon.png"> + <meta name="robots" content="noindex,nofollow"> + <style> + img { + width: 50%; + } + .post { + background-color: lightgray; + margin-bottom: 20px; + padding: 10px; + } + </style> + </head> + <body> + <p>Billeder taget på forskellige ferier etc, som jeg bruger til baggrund.</p> + + {% if ctx.cgit is not none %} + <p>Klik på et billede for at få fuld opløsning. + Højere opløsning samt licens kan også findes her <a href="{{ cgit }}/tree">her</a>.</p> + {% endif %} + + {% for img in images %} + <div class="post"> + {% if img.taken is not none %} + <i>Taget den {{ img.taken }}</i> + {% endif %} + {% if img.description is not none %} + <p>{{ img.description }}</p> + {% endif %} + {% if img.fullurl is not none %} + <a href="{{ img.fullurl }}"> + <img src="{{ img.scaledpath }}" /> + </a> + {% else %} + <img src="{{ img.scaledpath }}" /> + {% endif %} + </div> + {% endfor %} + + + {% if ctx.git is not none %} + <p>Bygget fra commit {{ ctx.git }}</p> + {% endif %} + </body> +</html> diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d452969 --- /dev/null +++ b/shell.nix @@ -0,0 +1,7 @@ +{ pkgs ? import <nixpkgs> {}, pythonPackages ? pkgs.python38Packages }: + +pkgs.mkShell { + buildInputs = with pythonPackages; [ + jinja2 schema pillow pyyaml + ]; +} |