diff --git a/conf/header.html b/conf/header.html index df833b8..632890a 100644 --- a/conf/header.html +++ b/conf/header.html @@ -53,8 +53,8 @@ hr {color:#fff;} .topbar a {color:#fff; text-decoration:none;} - - + +
Chrissys random stuff. @@ -66,6 +66,7 @@
  • Main
  • Projects
  • Blog
  • +
  • Photography
  • Contact
  • RSS-Feed
  • diff --git a/gps_location.py b/gps_location.py new file mode 100755 index 0000000..aaab50d --- /dev/null +++ b/gps_location.py @@ -0,0 +1,125 @@ +#!/usr/bin/python3 +"""!@brief: helper routines for dealing with GPS Data from exif +@author: Luzia Christiane Tesar +@date Mi 12. Aug 13:12:33 CEST 2026 +""" + +import os +import sys +import requests +import json +from datetime import datetime +from PIL import Image +from PIL.ExifTags import TAGS, GPSTAGS + +REV_API_URL = "https://nominatim.openstreetmap.org/reverse" +headers = {"User-Agent": "Purrrrrpic :3"} + + +def get_exif_data(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: + exif_data[decoded] = value + + except: + pass + return exif_data + + +def _get_if_exist(data, key): + if key in data: + return data[key] + + return None + + +def _convert_to_degress(value): + """Helper function to convert the GPS coordinates stored in the EXIF to degress in float format""" + d = float(value[0]) + + m = float(value[1]) + + s = float(value[2]) + + return d + (m / 60.0) + (s / 3600.0) + + +def get_lat_lon(exif_data): + """Returns the latitude and longitude, if available, from the provided exif_data (obtained through get_exif_data above)""" + lat = None + lon = None + + if "GPSInfo" in exif_data: + gps_info = exif_data["GPSInfo"] + gps_latitude = _get_if_exist(gps_info, "GPSLatitude") + gps_latitude_ref = _get_if_exist(gps_info, "GPSLatitudeRef") + gps_longitude = _get_if_exist(gps_info, "GPSLongitude") + gps_longitude_ref = _get_if_exist(gps_info, "GPSLongitudeRef") + + if gps_latitude and gps_latitude_ref and gps_longitude and gps_longitude_ref: + lat = _convert_to_degress(gps_latitude) + if gps_latitude_ref != "N": + lat = 0 - lat + + lon = _convert_to_degress(gps_longitude) + if gps_longitude_ref != "E": + lon = 0 - lon + + return lat, lon + +def getReverseAPI(lat, lon): + try: + payload = {"lat": lat, "lon": lon, "format": "json", "addressdetails": "1"} + r = requests.get(REV_API_URL, headers=headers, params=payload) + return r.json() + except requests.exceptions.HTTPError as errh: + print ("Http Error:",errh) + except requests.exceptions.ConnectionError as errc: + print ("Error Connecting:",errc) + except requests.exceptions.Timeout as errt: + print ("Timeout Error:",errt) + except requests.exceptions.RequestException as err: + print ("OOps: Something Else",err) + +def getCity(geo): + return geo["address"]["city"] or "" + + +def getCountry(geo): + return geo["address"]["country"] or "" + + +def getRoad(geo): + return geo["address"]["road"] or "" + + +def getHouseNumber(geo): + return geo["address"]["house_number"] or "" + + +def getPostCode(geo): + return geo["address"]["postcode"] or "" + + +if __name__ == "__main__": + + img = Image.open("eh23/IMG20260403174725.jpg") + exifdata = get_exif_data(img) + lat, lon = get_lat_lon(exifdata) + geo = getReverseAPI(lat, lon) + print(getCity(geo)) + print(getCountry(geo)) + print(getRoad(geo)) diff --git a/purrpic.py b/purrpic.py index bc13fc5..f289b24 100644 --- a/purrpic.py +++ b/purrpic.py @@ -8,33 +8,61 @@ import os import sys import glob -# import exif 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 BUILD_DIR = Path("./build").resolve() CONF_DIR = Path("./conf").resolve() -THUMBSIZE = (300, 300) -GLOBALNAME = "Chrissys random Imagelibrary" +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, **kwargs): + def __init__(self): self.header = Path(CONF_DIR / "header.html").read_text() self.footer = Path(CONF_DIR / "footer.html").read_text() - for key, value in kwargs.items(): - setattr(self, key, value) + self.leaflet = Path(CONF_DIR / "leaflet.html").read_text() + + self.lat = None + self.lon = None + self.geo = 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.f_num = None + self.flash = None + self.focal_length = 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""" @@ -77,12 +105,75 @@ class HTML: 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 "LensModel" in exifdata: + self.lens_model = exifdata["LensModel"] + + if "ExposureTime" in exifdata: + self.exposure = exifdata["ExposureTime"] + + 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) + + def writeGrid(self, imglist, linklist): + try: + buf: "" + buf += self.div("row") + # imglist = glob.glob(str(odir) + "/.thumbnails/*") + 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: - title=meta['title'] - desc=meta['description'] + title = meta["title"] + desc = meta["description"] buf = self.headermeta(title) buf += self.title(title) if len(desc) > 0: @@ -92,17 +183,15 @@ class HTML: buf += self.li(f"© {AUTHOR}, {LICENCE}") buf += self.endUl() buf += self.div("row") - #imglist = glob.glob(str(odir) + "/.thumbnails/*") - imglist =[] + imglist = [] for img in Path.iterdir(odir / ".thumbnails"): imglist.append(img) for n in range(0, 3): buf += self.div("column") - breakpoint() for i in range(n, len(imglist), 3): buf += self.aimg( - ".thumbnails/" + imglist[i].name, - imglist[i].name) + ".thumbnails/" + imglist[i].name, f"{imglist[i].stem}.html" + ) buf += self.endDiv() buf += self.endDiv() buf += self.footer @@ -111,37 +200,109 @@ class HTML: except: pass - def writeDetail(self, img, previmg, nextimg): - buf = self.headermeta(title) - buf += self.title(title) - buf += self.img(image) - buf += self.footer - with open(f"build/{self.outfolder}/index.html", "w", encoding="utf-8") as file: - file.write(buf) + def writeDetail(self, odir): + try: + breakpoint() + 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 += self.endDiv() + buf += self.endDiv() + buf += self.img(img) + 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() - def writeMain(self, titles): - with open(f"{str(BUILD_DIR)}/index.html", "w", encoding="utf-8") as file: - title = "Chrissys random Imagelibrary" - file.write(self.headermeta(title)) - file.write(self.title(title)) + 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 += self.div("div") + buf += '

    \n' + + if self.datetime: + buf += f"\n" + + if self.geo: + 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" + + if self.software: + buf += f"\n" + + buf += "
    Time {self.datetime.strftime('%Y-%m-%d %H:%M')}
    Place {self.geo['address']['road']}, {self.geo['address']['town']}
    Camera{self.make} {self.model}
    Lens Model {self.lens_model}
    Focal Length {self.focal_length}
    Aperture {self.focal_length}
    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: + breakpoint() + file.write(self.headermeta(GLOBALNAME)) + file.write(self.title(GLOBALNAME)) file.write(self.div("row")) - dirs = list( - filter(lambda x: x.is_dir() and x not in exclude, os.scandir(BUILD_DIR)) - ) + dirs = metadata.keys() + dirlist = [] + for d in dirs: + dirlist.append(d) for n in range(0, 3): file.write(self.div("column")) - for i in range(n, len(dirs), 3): - thumbs = os.listdir( - str(BUILD_DIR) + "/" + dirs[i].name + "/.thumbnails" - ) + for i in range(n, len(dirlist), 3): + thumbs = os.listdir(BUILD_DIR / dirlist[i] / ".thumbnails") file.write(self.div("desc")) - file.write(titles[dirs[i].name]) + file.write(metadata[dirlist[i]]["title"]) file.write(self.endDiv()) file.write( self.aimg( - str(dirs[i].name) + "/.thumbnails/" + thumbs[0], - str(dirs[i].name) + "/", - ), + dirlist[i] + "/.thumbnails/" + thumbs[0], dirlist[i] + "/" + ) ) file.write(self.endDiv()) file.write(self.endDiv()) @@ -153,6 +314,31 @@ class EXIF: 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): @@ -161,16 +347,18 @@ class IMG: def createThumb(img, thumbdir): - file = Path(img).stem with Image.open(img) as im: im.thumbnail(THUMBSIZE) - im.convert("RGB").save(str(thumbdir) + "/" + file + ".jpg", "JPEG") + im.convert("RGB").save(str(thumbdir) + "/" + img.stem + ".jpg", "JPEG") def processImage(img, odir): - file = Path(img).stem + breakpoint() with Image.open(img) as im: - im.convert("RGB").save(str(odir) + "/" + file + ".jpg", "JPEG") + exif = im.getexif() + exif[315] = AUTHOR + exif[33432] = COPYRIGHT + im.save(str(odir) + "/" + img.stem + ".jpg", "JPEG", exif=exif) def generateDir(path): @@ -180,6 +368,7 @@ def generateDir(path): Path.mkdir(thumbdir, parents=True, exist_ok=True) return odir, thumbdir + def parseIni(file): meta = {"title": None, "category": None, "description": [], "files": []} try: @@ -187,14 +376,14 @@ def parseIni(file): for line in f: if line.startswith("desc = "): meta["description"].append(line.removeprefix("desc = ").strip()) - elif line.endswith(".jpg\n" or ".png\n" or ".jpeg\n"): + 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() else: - next + continue except: pass return meta @@ -202,8 +391,7 @@ def parseIni(file): def processDir(path): try: - if Path.is_file(path /".purrpic.ini"): - breakpoint() + if Path.is_file(path / ".purrpic.ini"): meta = parseIni(path / ".purrpic.ini") odir, thumbdir = generateDir(path) for img in meta["files"]: @@ -213,9 +401,21 @@ def processDir(path): except: pass + def writeIndex(odir, meta): html = HTML() - html.writeIndex(odir, meta) + html.writeIndex(odir, meta) + + +def writeMain(metadata): + html = HTML() + html.writeMain(metadata) + + +def writeDetail(odir): + html = HTML() + html.writeDetail(odir) + if __name__ == "__main__": @@ -235,26 +435,19 @@ if __name__ == "__main__": type=lambda p: Path(p).resvolve(), help="", ) - parser.add_argument( - "--start", - "-s", - type=str, - help="", - ) + args = parser.parse_args() - breakpoint() if args.outpath: BUILD_DIR = args.outpath metadata = {} for d in args.inpath.iterdir(): if d.is_dir() and d.name not in exclude: - # processDir(d.name) try: - odir, thumbdir, meta = processDir(d) + odir, _, meta = processDir(d) metadata[d.name] = meta writeIndex(odir, meta) + writeDetail(odir) except: - next -# html = HTML() -# html.writeMain(metadata) + continue + writeMain(metadata)