#!/usr/bin/env python3 # vim:tabstop=4 softtabstop=4 shiftwidth=4 textwidth=160 smarttab expandtab colorcolumn=160 import argparse from datetime import datetime import exifread import os import PIL from PIL import Image from progress.bar import Bar import shutil import sys class ProgressBar(Bar): suffix = "%(percent).0f%% [%(elapsed_td)s/%(eta_td)s]" def rotate_image(image, exif_tag): if "Image Orientation" not in exif_tag: return image orientation = exif_tag["Image Orientation"].values if 3 in orientation: image = image.transpose(Image.ROTATE_180) if 6 in orientation: image = image.transpose(Image.ROTATE_270) if 8 in orientation: image = image.transpose(Image.ROTATE_90) return image def format_f(value, precision=1): if value % 1 == 0: return f"{value:.0f}" return f"{value:.{precision}f}" def format_fsi(exif_tag): entries = list() try: f_num = float(exif_tag["EXIF FNumber"].values[0]) entries.append(f"""f/{format_f(f_num)}""") except (KeyError, ZeroDivisionError): pass try: exposure = float(exif_tag["EXIF ExposureTime"].values[0]) if exposure >= 1: entries.append(f"""{format_f(exposure)}s""") elif exposure >= 1e-3: entries.append( f"""{format_f(exposure * 1e3)}ms""" ) else: entries.append( f"""{format_f(exposure * 1e6)}µs""" ) except (KeyError, ZeroDivisionError): pass try: focal_length = float(exif_tag["EXIF FocalLength"].values[0]) entry = f"{format_f(focal_length)}mm" try: focal_length35 = float(exif_tag["EXIF FocalLengthIn35mmFilm"].values[0]) entry += f" (≙ {format_f(focal_length35)}mm)" except (KeyError, ZeroDivisionError): pass entries.append(f"""{entry}""") except (KeyError, ZeroDivisionError): pass try: iso = exif_tag["EXIF ISOSpeedRatings"].values[0] entries.append(f"""ISO{iso}""") except KeyError: pass return " ".join(entries) def format_make_model_lens(exif_tag): try: make = exif_tag["Image Make"].values model = exif_tag["Image Model"].values except KeyError: return None if model.startswith(make): model = model[len(make) :] if model[0] == " ": model = model[1:] try: lens = exif_tag["EXIF LensModel"] if lens: model += f" + {lens}" except KeyError: # Unknown or built-in lens pass return f"""{make} {model}""" def format_exif(exif_tag): exif_lines = list() try: dt = datetime.strptime( exif_tag["EXIF DateTimeOriginal"].values, "%Y:%m:%d %H:%M:%S" ) exif_lines.append( dt.strftime("""%d.%m.%Y %H:%M""") ) except (KeyError, ValueError): try: dt = datetime.strptime( exif_tag["Image DateTimeOriginal"].values, "%Y:%m:%d %H:%M:%S" ) exif_lines.append( dt.strftime("""%d.%m.%Y %H:%M""") ) except (KeyError, ValueError): pass exif_lines.append(format_make_model_lens(exif_tag)) exif_lines.append(format_fsi(exif_tag)) return """ """.join(filter(bool, exif_lines)) def copy_files(base_dir): for directory in ".data/css .data/js .thumbnails".split(): os.makedirs(directory, exist_ok=True) boxwidth = f"{args.size * args.spacing}px" boxheight = f"{args.size * args.spacing}px" imgwidth = f"{args.size}px" imgheight = f"{args.size}px" css_files = ["glightbox.min.css"] js_files = ["glightbox.min.js"] for css_file in css_files: shutil.copy(f"{base_dir}/css/{css_file}", f".data/css/{css_file}") for js_file in js_files: shutil.copy(f"{base_dir}/js/{js_file}", f".data/js/{js_file}") with open(f"{base_dir}/css/main.css", "r") as f: main_css = f.read() main_css = main_css.replace("/* $boxwidth */", boxwidth) main_css = main_css.replace("/* $boxheight */", boxheight) main_css = main_css.replace("/* $imgwidth */", imgwidth) main_css = main_css.replace("/* $imgheight */", imgheight) with open(".data/css/main.css", "w") as f: f.write(main_css) def create_thumbnail_html(index, filename, title): buf = """
\n""" buf += f"""""" buf += f"""{filename}""" buf += "" buf += "
" buf += f"""
\n""" buf += f"""

{filename}""" buf += f"""{title}

\n""" buf += "
\n" return buf if __name__ == "__main__": parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__ ) parser.add_argument( "--html-include", metavar="FILE", type=str, help="file with HTML to include before thumbnail list", ) parser.add_argument("--size", type=int, default=250, help="Thumbnail size [px]") parser.add_argument( "--spacing", type=float, default=1.1, help="Thumbnail spacing ratio" ) parser.add_argument("--title", type=str, help="HTML title", default="") parser.add_argument("images", type=str, nargs="+") args = parser.parse_args() base_dir = "/".join(os.path.realpath(sys.argv[0]).split("/")[:-2]) copy_files(f"{base_dir}/share") with open(f"{base_dir}/share/html_start", "r") as f: html_buf = f.read().replace("", args.title) if args.html_include: with open(args.html_include, "r") as f: html_buf += f.read() filenames = args.images for i, filename in enumerate(ProgressBar(max=len(filenames)).iter(filenames)): with open(filename, "rb") as f: exif_tag = exifread.process_file(f) try: im = Image.open(filename) except PIL.UnidentifiedImageError: continue im = rotate_image(im, exif_tag) im.thumbnail((args.size * 2, args.size * 2)) im.save(f".thumbnails/{filename}", "JPEG") html_buf += create_thumbnail_html(i, filename, format_exif(exif_tag)) with open(f"{base_dir}/share/html_end", "r") as f: html_buf += f.read() with open("index.html", "w") as f: f.write(html_buf)