#!/usr/bin/python3 """!@brief:generates static html imagelibrary @author: Luzia Christiane Tesar @date Mo 13. Jul 20:52:54 CEST 2026 """ import os import sys from PIL import Image from PIL.ExifTags import TAGS, GPSTAGS import argparse import configparser from pathlib import Path import datetime from gps_location import get_lat_lon, getReverseAPI, getPlace from fractions import Fraction BUILD_DIR = Path("/home/chrissy/repos/purrpic/build").resolve() CONF_DIR = Path("/home/chrissy/repos/purrpic//conf").resolve() THUMBSIZE = (800, 800) GLOBALNAME = "Chrissys random Photogallery" LICENCE = "CC BY-NC-SA 4.0" AUTHOR = "Luzia Christiane Tesar" LANGUAGE = "en" DESCRIPTION = "" TODAY = datetime.datetime.today().strftime("%Y-%m-%d") CANONICALURL = "https://dm2lct.radio" STYLESHEET = "https://dm2lct.radio/config/main.css" LOGO = "https://dm2lct.radio/config/logo.png" COPYRIGHT = f"© {datetime.datetime.today().year} by {AUTHOR}. This work is licensed under {LICENCE}. To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/" exclude = set([".thumbnails", ".git", "conf", "build"]) class HTML: def __init__(self): self.header = Path(CONF_DIR / "header.html").read_text() self.footer = Path(CONF_DIR / "footer.html").read_text() self.leaflet = Path(CONF_DIR / "leaflet.html").read_text() self.lat = None self.lon = None self.geo = None self.place = None self.datetime = None self.file_link = None self.make = None self.model = None self.focus = None self.caption = None self.ev = None self.exposure = None self.exposure_mode = None self.exposure_program = None self.exposure_bias = None self.f_num = None self.flash = None self.focal_length = None self.focal_length_35mm = None self.focus_distance = None self.focus_mode = None self.iso = None self.lv = None self.software = None self.subject_distance = None self.lens_model = None def title(self, title): return f"""

{title}

\n""" def par(self, text): return f"""

{text}

\n""" def ul(self): return f"""\n""" def li(self, text): return f"""
  • {text}\n""" def div(self, divclass): return f"""
    \n""" def endDiv(self): return f"""
    \n""" def img(self, src, alt=None, width=None, title=None): return f"""{alt}\n""" def aimg(self, src, dest, alt=None, width=None, title=None): return f"""{alt}\n""" def link(self, dest, data): return f"""{data}""" def headermeta(self, title): header = self.header.replace("{{title}}", title) header = header.replace("{{cannonical_url}}", CANONICALURL) header = header.replace("{{global_name}}", GLOBALNAME) header = header.replace("{{global_name}}", GLOBALNAME) header = header.replace("{{author}}", AUTHOR) header = header.replace("{{language}}", LANGUAGE) header = header.replace("{{description}}", DESCRIPTION) header = header.replace("{{published_date}}", TODAY) header = header.replace("{{modified_date}}", TODAY) header = header.replace("{{canonical_url}}", CANONICALURL) header = header.replace("{{stylesheet}}", STYLESHEET) header = header.replace("{{logo}}", LOGO) return header def leaflet(self, lat, lon, zoom=10): lfhtml = self.leaflet.replace("{LAT}", lat) lfhtml = lfhtml.replace("{LON}", lon) lfhtml = lfhtml.replace("{ZOOM}", zoom) return lfhtml def getImgInfo(self, img): exif = EXIF() exifdata = exif.getData(img) if "Make" in exifdata: self.make = exifdata["Make"] if "Model" in exifdata: self.model = exifdata["Model"] if "DateTimeOriginal" in exifdata: self.datetime = datetime.datetime.strptime( exifdata["DateTimeOriginal"], "%Y:%m:%d %H:%M:%S" ) if "FocalLength" in exifdata: self.focal_length = exifdata["FocalLength"] if 'FocalLengthIn35mmFilm' in exifdata and exifdata.get('FocalLengthIn35mmFilm') != 0: self.focal_length_35mm = exifdata.get('FocalLengthIn35mmFilm') if "LensModel" in exifdata: self.lens_model = exifdata["LensModel"] if "ExposureTime" in exifdata: self.exposure = f"1/{round(1/float(exifdata.get('ExposureTime')))}" if 'ExposureBiasValue' in exifdata: self.exposure_bias = exifdata.get('ExposureBiasValue') if "FNumber" in exifdata: self.f_num = exifdata["FNumber"] if "ISOSpeedRatings" in exifdata: self.iso = exifdata["ISOSpeedRatings"] if "Software" in exifdata: self.software = exifdata["Software"] if "GPSInfo" in exifdata: self.lat, self.lon = get_lat_lon(exifdata) self.geo = getReverseAPI(self.lat, self.lon) self.place = getPlace(self.geo) def writeGrid(self, imglist, linklist): try: buf: "" buf += self.div("row") imglist = [] for img in Path.iterdir(odir / ".thumbnails"): imglist.append(img) for n in range(0, 3): buf += self.div("column") for i in range(n, len(imglist), 3): buf += self.aimg(".thumbnails/" + imglist[i].name, imglist[i].name) buf += self.endDiv() buf += self.endDiv() except: pass return buf def writeIndex(self, odir, meta) -> None: try: print(f"Writing index.html for {odir}") title = meta["title"] desc = meta["description"] buf = self.headermeta(title) buf += self.title(title) if len(desc) > 0: buf += self.ul() for line in desc: buf += self.li(line) buf += self.li(f"© {AUTHOR}, {LICENCE}") buf += self.endUl() buf += self.div("row") imglist = [] for img in Path.iterdir(odir / ".thumbnails"): imglist.append(img) for n in range(0, 3): buf += self.div("column") for i in range(n, len(imglist), 3): buf += self.aimg( ".thumbnails/" + imglist[i].name, f"{imglist[i].stem}.html" ) buf += self.endDiv() buf += self.endDiv() buf += self.footer with open(f"{odir}/index.html", "w", encoding="utf-8") as file: file.write(buf) except: pass def writeDetail(self, odir): try: imglist = [] for file in Path.iterdir(odir): if file.suffix in [".jpg", ".png", "JPEG", "PNG"]: imglist.append(file) else: continue for img in imglist: with Image.open(img) as i: self.getImgInfo(i) buf = self.headermeta(GLOBALNAME) buf += self.div("div") buf += self.div("divleft") buf += self.link("index.html", "Back") buf +="

    " buf += self.endDiv() buf += self.endDiv() buf += self.aimg(img.name, img.name) buf += self.div("div") if imglist.index(img) != 0: buf += self.div("divleft") buf += self.link( f"{imglist[(imglist.index(img) - 1) % len(imglist)].stem}.html", "Perv", ) buf += self.endDiv() if imglist.index(img) != len(imglist) - 1: buf += self.div("divright") buf += self.link( f"{imglist[(imglist.index(img) + 1) % len(imglist)].stem}.html", "Next", ) buf += self.endDiv() buf += self.endDiv() buf +="

    " buf += self.div("div") buf += '

    \n' if self.datetime: buf += f"\n" if self.geo: buf += f"\n" buf += f"\n" if self.make: buf += f"\n" if self.lens_model: buf += ( f"\n" ) if self.focal_length: buf += f"\n" if self.f_num: buf += ( f"\n" ) if self.exposure: buf += ( f"\n" ) if self.iso: buf += f"\n" buf += f"\n" if self.software: buf += f"\n" buf += "
    Time{self.datetime.strftime('%Y-%m-%d %H:%M')}
    Place{self.place}
    Camera{self.make} {self.model}
    Lens Model{self.lens_model}
    Focal Length{self.focal_length}
    Aperture{self.f_num}
    Exposure Time{self.exposure}
    ISO{self.iso}
    Software{self.software}

    \n" buf += self.endDiv() buf += self.footer with open(odir / f"{img.stem}.html", "w", encoding="utf-8") as file: file.write(buf) except: pass def writeMain(self, metadata): with open(BUILD_DIR / "index.html", "w", encoding="utf-8") as file: file.write(self.headermeta(GLOBALNAME)) file.write(self.title(GLOBALNAME)) file.write(self.div("row")) dirs = metadata.keys() dirlist = [] for d in dirs: dirlist.append(d) dirlist.sort(key=lambda x:metadata[x]["date"], reverse=True) for n in range(0, 3): file.write(self.div("column")) for i in range(n, len(dirlist), 3): thumbs = os.listdir(BUILD_DIR / dirlist[i] / ".thumbnails") file.write(self.div("desc")) file.write(metadata[dirlist[i]]["title"]) file.write(self.endDiv()) file.write( self.aimg( dirlist[i] + "/.thumbnails/" + thumbs[0], dirlist[i] + "/" ) ) file.write(self.endDiv()) file.write(self.endDiv()) file.write(self.footer) class EXIF: def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) def getData(self, image): """Returns a dictionary from the exif data of an PIL Image item. Also converts the GPS Tags""" exif_data = {} try: info = image._getexif() if info: for tag, value in info.items(): decoded = TAGS.get(tag, tag) if decoded == "GPSInfo": gps_data = {} for t in value: sub_decoded = GPSTAGS.get(t, t) gps_data[sub_decoded] = value[t] exif_data[decoded] = gps_data else: if type(value) == str: exif_data[decoded] = value.strip("\x00") else: exif_data[decoded] = value except: pass return exif_data class IMG: def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) def createThumb(img, thumbdir): with Image.open(img) as im: im.thumbnail(THUMBSIZE) im.convert("RGB").save(str(thumbdir) + "/" + img.stem + ".jpg", "JPEG") def processImage(img, odir): with Image.open(img) as im: exif = im.getexif() exif[315] = AUTHOR exif[33432] = COPYRIGHT im.save(str(odir) + "/" + img.stem + ".jpg", "JPEG", exif=exif) def generateDir(path): odir = BUILD_DIR / path.name thumbdir = BUILD_DIR / path.name / ".thumbnails" Path.mkdir(odir, parents=True, exist_ok=True) Path.mkdir(thumbdir, parents=True, exist_ok=True) return odir, thumbdir def parseIni(file): meta = {"title": None, "category": None, "date": None, "description": [], "files": []} try: with open(file, "r") as f: for line in f: if line.startswith("desc = "): meta["description"].append(line.removeprefix("desc = ").strip()) elif line.endswith((".jpg\n", ".png\n", ".jpeg\n")): meta["files"].append(line.strip()) elif line.startswith("title = "): meta["title"] = line.removeprefix("title = ").strip() elif line.startswith("category = "): meta["category"] = line.removeprefix("category = ").strip() elif line.startswith("date = "): meta["date"] = datetime.datetime.strptime(line.removeprefix("date = ").strip(),"%Y-%m-%d") else: continue except: pass return meta def processDir(path): try: if Path.is_file(path / ".purrpic.ini"): print(f"Processing {path}") meta = parseIni(path / ".purrpic.ini") odir, thumbdir = generateDir(path) for img in meta["files"]: createThumb(path / img, thumbdir) processImage(path / img, odir) return odir, thumbdir, meta except: pass def writeIndex(odir, meta): html = HTML() html.writeIndex(odir, meta) def writeMain(metadata): html = HTML() html.writeMain(metadata) def writeDetail(odir): html = HTML() html.writeDetail(odir) if __name__ == "__main__": parser = argparse.ArgumentParser( prog="PurrPic", description="Creates a static image gallery from a given Filelist", epilog="Meow! =^.^=", ) parser.add_argument( "--inpath", type=lambda p: Path(p).resolve(), help="", ) parser.add_argument( "--outpath", type=lambda p: Path(p).resvolve(), help="", ) args = parser.parse_args() if args.outpath: BUILD_DIR = args.outpath metadata = {} for d in args.inpath.iterdir(): if d.is_dir() and d.name not in exclude: try: odir, _, meta = processDir(d) metadata[d.name] = meta writeIndex(odir, meta) writeDetail(odir) except: continue writeMain(metadata)