473 lines
16 KiB
Python
Executable File
473 lines
16 KiB
Python
Executable File
#!/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"""<h1>{title}</h1>\n"""
|
|
|
|
def par(self, text):
|
|
return f"""<p>{text}</p>\n"""
|
|
|
|
def ul(self):
|
|
return f"""<ul>\n"""
|
|
|
|
def endUl(self):
|
|
return f"""</ul>\n"""
|
|
|
|
def li(self, text):
|
|
return f"""<li> {text}\n"""
|
|
|
|
def div(self, divclass):
|
|
return f"""<div class="{divclass}">\n"""
|
|
|
|
def endDiv(self):
|
|
return f"""</div>\n"""
|
|
|
|
def img(self, src, alt=None, width=None, title=None):
|
|
return f"""<img src="{src}" alt="{alt}" title="{title}" />\n"""
|
|
|
|
def aimg(self, src, dest, alt=None, width=None, title=None):
|
|
return f"""<a href = "{dest}"><img src="{src}" alt="{alt}" title="{title}"/></a>\n"""
|
|
|
|
def link(self, dest, data):
|
|
return f"""<a href="{dest}">{data}</a>"""
|
|
|
|
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}, <a href=https://creativecommons.org/licenses/by-nc-sa/4.0/>{LICENCE}</a>")
|
|
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 +="<br><br>"
|
|
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 +="<br><br>"
|
|
buf += self.div("div")
|
|
buf += '<p><table>\n'
|
|
|
|
if self.datetime:
|
|
buf += f"<tr><td><b>Time</b></td><td>{self.datetime.strftime('%Y-%m-%d %H:%M')}</td></tr>\n"
|
|
|
|
if self.geo:
|
|
buf += f"<tr><td><b>Place</b></td><td><a href = https://www.openstreetmap.org/?mlat={self.lat}&mlon={self.lon}map=17/{self.lat}/{self.lon}>{self.place}</a></td></tr>\n"
|
|
|
|
buf += f"<tr><td><b></b></td><td></td></tr>\n"
|
|
|
|
if self.make:
|
|
buf += f"<tr><td><b>Camera</b></td><td>{self.make} {self.model}</td></tr>\n"
|
|
|
|
if self.lens_model:
|
|
buf += (
|
|
f"<tr><td><b>Lens Model</b></td><td>{self.lens_model}</td></tr>\n"
|
|
)
|
|
|
|
if self.focal_length:
|
|
buf += f"<tr><td><b>Focal Length</b></td><td>{self.focal_length}</td></tr>\n"
|
|
|
|
if self.f_num:
|
|
buf += (
|
|
f"<tr><td><b>Aperture</b></td><td>{self.f_num}</td></tr>\n"
|
|
)
|
|
|
|
if self.exposure:
|
|
buf += (
|
|
f"<tr><td><b>Exposure Time</b></td><td>{self.exposure}</td></tr>\n"
|
|
)
|
|
|
|
if self.iso:
|
|
buf += f"<tr><td><b>ISO</b></td><td>{self.iso}</td></tr>\n"
|
|
|
|
buf += f"<tr><td><b></b></td><td></td></tr>\n"
|
|
|
|
if self.software:
|
|
buf += f"<tr><td><b>Software</b></td><td>{self.software}</td></tr>\n"
|
|
|
|
buf += "</table></p>\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)
|