6 Коміти 56fa156923 ... 12f103473b

Автор SHA1 Опис Дата
  Lars 12f103473b Store meta added 1 рік тому
  Lars e72f78ed09 publish script 1 рік тому
  Lars 8a02a9f1ec masterimg support for image description 1 рік тому
  Lars 00f3b821cc Galleries and page adjusted 1 рік тому
  Lars 2a9458645b Automatic gallery and download generation script 1 рік тому
  Lars db04b4c911 masterimg.py added 1 рік тому

+ 2 - 0
.gitignore

@@ -1,3 +1,5 @@
 public/*
 resources/*
 *.lock
+content/*/
+downloads/*

+ 2 - 2
content/_index.md

@@ -1,7 +1,7 @@
 ---
 #lastmod: 2023-07-05
-title: Lars's Photography Endeavors
-description: Welcome to my photography portfolio
+title: Lars's Electric Endeavors
+description: Welcome to the photography section
 menus:
   main:
     name: Home

+ 6 - 4
content/about.md

@@ -9,12 +9,14 @@ menus:
 
 All photos were captured with a Nikon Z5 and processed with [Rawtherapee](https://www.rawtherapee.com/). 
 
-You can purchase digital downloads of the pictures without watermarks, as well as the raw files and Rawtherapee settings I used for processing, in the [store](https://ko-fi.com/larsp)
+I bring the camera on hikes and trips to take images of whatever interesting sight I stumble on.
 
-I like to bring the camera on hikes and trips to capture punchy and artistic images of whatever interesting sight I stumble on.
+I like tinkering with old and weird lenses. Fixing them, cleaning them, finding their strong and weak points and using it all for artistic gain. A mirrorless camera is great for this as the short flange distance allows adapters to be made for all sorts of mounts. And if the camera sports sensor shift stabilization, you get optical image stabilization for any old lens.
 
-I like tinkering with old and weird lenses. Fixing them, cleaning them, finding their strong and weak points and using it all for artistic gain. A mirrorless camera is great for this as the short flange distance allows adapters to be made for all sorts of mounts. And with a camera that sports sensor shift stabilization, you get optical image stabilization for any old lens.
+---
+
+This page was made with [Hugo](https://gohugo.io/) and the [hugo-theme-gallery](https://github.com/nicokaiser/hugo-theme-gallery)
 
-Watermark imprinting and rescaling was done automatically by the masterimg python script. The page was made with [Hugo](https://gohugo.io/) and the [hugo-theme-gallery](https://github.com/nicokaiser/hugo-theme-gallery)
+You can purchase digital downloads of the pictures without watermarks, including the raw files and Rawtherapee settings used for processing, in the [shop](https://ko-fi.com/larsp/shop).
 
 Contact: contact@larsee.com

+ 0 - 10
content/nature/index.md

@@ -1,10 +0,0 @@
----
-# description: 
-featured_image: Pretty sun on road_hq.jpg
-menus: "main"
-sort_by: Name # Exif.Date
-sort_order: desc
-title: Nature
-#type: gallery
-weight: 3
----

+ 70 - 0
galleries.yaml

@@ -0,0 +1,70 @@
+
+
+
+#freezing cold = ["DSC_2743", "DSC_2744", "DSC_2757"]
+#nature = ["DSC_2273", "DSC_2296", "DSC_2300", "DSC_2911"]
+#nice_view = ["DSC_2395", "DSC_2617", "DSC_2619", "DSC_2661", "DSC_2705"]
+#various = ["DSC_0263", "DSC_2341", "DSC_3079", "DSC_2323", "DSC_3073", "DSC_0897", "DSC_2769"]
+#textures = ["DSC_2932", "DSC_2948", "DSC_3124", "DSC_3161", "DSC_3212", "DSC_3485", "DSC_3487"]
+
+picture_root: "/home/lars/Nasbox/Media/BestBest"
+
+galleries:
+  - folder_name: nice-view
+    title: Nice View
+    photos:
+      - "2024-batch1/DSC_2395"
+      - "2024-batch2/DSC_3240"
+      - "2024-batch1/DSC_2661"
+      - "2024-batch1/DSC_2705"
+      - "2024-batch2/DSC_3299"
+      - "2024-batch1/DSC_2619"
+      - "2024-batch2/DSC_3304"
+
+  - folder_name: nature
+    title: Nature
+    cover_photo: "2024-batch1/DSC_2273"
+    photos:
+      - "2024-batch1/DSC_2273"
+      - "2024-batch1/DSC_2297"
+      - "2024-batch1/DSC_2300"
+      - "2024-batch2/DSC_1081"
+      - "2024-batch1/DSC_2911"
+
+  - folder_name: cold-out-there
+    title: It's cold out there
+    description: Snowy foggy landscapes have a special charm
+    cover_photo: "2024-batch1/DSC_2744"
+    photos:
+      - "2024-batch1/DSC_2744"
+      - "2024-batch1/DSC_2757"
+      - "2024-batch2/DSC_3035"
+      - "2024-batch1/DSC_2743"
+  
+  - folder_name: various
+    title: "Various"
+    cover_photo: "2024-batch1/DSC_0263"
+    photos:
+      - "2024-batch1/DSC_0263"
+      - "2024-batch2/DSC_1120"
+      - "2024-batch1/DSC_2323"
+      - "2024-batch1/DSC_0897"
+      - "2024-batch1/DSC_3073"
+      - "2024-batch1/DSC_2769"
+      - "2024-batch1/DSC_3079"
+      - "2024-batch2/DSC_3139"
+      - "2024-batch2/DSC_0971"
+
+  - folder_name: texture-like
+    title: Texture Like
+    description: Images with a texture like quality
+    cover_photo: "2024-batch1/DSC_3124"
+    photos:
+      - "2024-batch1/DSC_3124"
+      - "2024-batch1/DSC_3212"
+      - "2024-batch1/DSC_2932"
+      - "2024-batch1/DSC_2948"
+      - "2024-batch1/DSC_3161"
+      - "2024-batch1/DSC_3485"
+      - "2024-batch1/DSC_3487"
+

+ 5 - 4
hugo.toml

@@ -1,4 +1,5 @@
-baseURL = 'https://example.org/'
+
+baseURL = 'https://larsee.com/photography/'
 
 copyright = "© 2024 by Lars Ole Pontoppidan"
 defaultContentLanguage = "en"
@@ -22,7 +23,7 @@ theme = "gallery"
   section = ["HTML"]
 
 [imaging]
-  quality = 75
+  quality = 93
   resampleFilter = "CatmullRom"
   [imaging.exif]
     disableDate = false
@@ -36,8 +37,8 @@ theme = "gallery"
     weight = 4
 
   [[menu.footer]]
-    name = "Store"
-    url = "https://ko-fi.com/larsp"
+    name = "Shop"
+    url = "https://ko-fi.com/larsp/shop"
     weight = 3
 
 

+ 202 - 0
make_galleries.py

@@ -0,0 +1,202 @@
+#!/usr/bin/python3
+
+from typing import List, Optional
+from pydantic import BaseModel
+import yaml
+import shutil
+import zipfile
+import os
+import sys
+from datetime import datetime as dt
+
+# ----
+
+yaml_filename = 'galleries.yaml'
+content_dir = "./content"
+zip_dir = "./downloads"
+
+# ----
+
+def makeThankYou(gallery:str, date_generated:str):
+    return """Thank you for purchasing the "%s" gallery from larsee.com/photography!
+
+By purchasing this product you are granted a license to use the images for any purpose.
+
+This archive was generated: %s. In the future the gallery contents might change.
+
+Don't hesitate to contact me if you have questions or other inquiries. This is my private email: lars@f-w.dk
+
+Sincerely,
+Lars Ole Pontoppidan
+""" % (gallery, date_generated)
+
+# ----
+
+class Gallery(BaseModel):
+    folder_name: str
+    title: str
+    description: Optional[str] = None
+    cover_photo: Optional[str] = None
+    photos: List[str]
+
+class Galleries(BaseModel):
+    picture_root: str
+    galleries: List[Gallery]
+
+
+def loadGalleries(yaml_file_path: str) -> Galleries:
+    with open(yaml_file_path, 'r') as file:
+        data = yaml.safe_load(file)
+    return Galleries(**data)
+
+def fileTitle(s:str) -> str:
+    sp = s.split("/")
+    return sp[-1]
+
+class Photo():
+    def __init__(self, photo_root:str, source_file:str, prefix:str, index:int):
+        sp = source_file.split("/")
+        self.photoPath = sp[0]
+        self.photoName = sp[1]
+        self.destName = prefix + "_%02d.jpg" % index
+        self.photoRoot = photo_root
+        
+    def getDestJpgName(self):
+        return self.destName
+    
+    def getHqFileSource(self):
+        return os.path.join(self.photoRoot, self.photoPath, "hq", self.photoName + "_hq.jpg")
+
+    def getRawFilePath(self):
+        opt1 = os.path.join(self.photoRoot, self.photoPath, self.photoName + ".NEF")
+        opt2 = os.path.join(self.photoRoot, self.photoPath, self.photoName + ".JPG")
+        if os.path.isfile(opt1):
+            return opt1
+        elif os.path.isfile(opt2):
+            return opt2
+        else:
+            raise Exception("Can't find source file for: " + self.photoName)
+    
+    def getPp3FilePath(self):
+        return self.getRawFilePath() + ".pp3"
+   
+    def getTifFilePath(self):
+        return os.path.join(self.photoRoot, self.photoPath, "converted", self.photoName + ".tif")
+
+
+class PhotosInGallery():
+    def __init__(self, photo_root:str, gallery:Gallery):
+        self.photos:List[Photo] = []
+        self.cover_photo:Optional[Photo] = None
+
+        for idx, photo in enumerate(gallery.photos, start=1):
+            p = Photo(photo_root, photo, gallery.folder_name, idx)
+            self.photos.append(p)
+            if photo == gallery.cover_photo:
+                self.cover_photo = p
+    
+
+def makeIndexFile(weight:int, weight_rev:int, gallery:Gallery, photos:PhotosInGallery):
+    values = {
+        "title" : gallery.title,
+        "weight" : weight,
+        "menus" : {"main" : {"weight" : weight_rev}},
+    }
+    if gallery.description is not None:
+        values["description"] = gallery.description
+
+    if photos.cover_photo is not None:
+        values["featured_image"] = photos.cover_photo.getDestJpgName()
+
+    return "---\n" + yaml.dump(values, default_flow_style=False) + "---\n"
+
+def makeGalleries(yaml_root:Galleries):
+    print("Purging old galleries")
+    for item in os.listdir(content_dir):
+        item_path = os.path.join(content_dir, item)
+        if os.path.isdir(item_path):
+            print(f"  deleting folder: {item_path}")
+            shutil.rmtree(item_path)
+
+    print("Setting up galleries")
+    for idx, gallery in enumerate(yaml_root.galleries):
+        path = os.path.join(content_dir, gallery.folder_name)
+        weight = len(yaml_root.galleries) - idx
+        print("  making dir: " + path)
+        os.makedirs(path, exist_ok=True)
+
+        photos = PhotosInGallery(yaml_root.picture_root, gallery)
+
+        index = makeIndexFile(weight, idx + 1, gallery, photos)
+        with open(os.path.join(path, "index.md"), "w") as f:
+            f.write(index)
+
+        # Link in the pictures
+        for photo in photos.photos:
+            #os.symlink(photo.getHqFileSource(), os.path.join(path, photo.getSimpleJpgName()))
+            shutil.copyfile(photo.getHqFileSource(), os.path.join(path, photo.getDestJpgName()))
+
+def makeZipDownloads(yaml_root:Galleries):
+    
+    if os.path.isdir(zip_dir):
+        print("Purging old downloads")
+        shutil.rmtree(zip_dir)
+    
+    os.makedirs(zip_dir, exist_ok=True)
+
+    # Get the current date and time
+    dt_now = dt.now()
+
+    print("Making download zips, archive date: " + dt_now.strftime("%B %d, %Y"))
+
+    for gallery in yaml_root.galleries:
+        photos = PhotosInGallery(yaml_root.picture_root, gallery)
+
+        # Create a ZIP file for the gallery
+        zip_path = os.path.join(zip_dir, "Gallery %s.zip" % gallery.folder_name)
+        print("  making zip: " + zip_path)
+
+        #gallery_folder = "%s_%s" % (gallery.folder_name, dt_now.strftime("%b_%d_%Y"))
+        gallery_folder = gallery.folder_name
+
+        with zipfile.ZipFile(zip_path, 'w') as myzip:
+            def addToZip(path:str, sub_folder:Optional[str] = None):
+                if sub_folder is None:
+                    myzip.write(path, os.path.join(gallery_folder, os.path.basename(path)))
+                else:
+                    myzip.write(path, os.path.join(gallery_folder, sub_folder, os.path.basename(path)))
+
+            for photo in photos.photos:
+                addToZip(photo.getRawFilePath())
+                addToZip(photo.getPp3FilePath())
+                addToZip(photo.getTifFilePath(), "converted")
+        
+            myzip.writestr('Thank you.txt', makeThankYou(gallery.title, dt_now.strftime("%B %d, %Y")))
+
+def main(galleries:bool, zips:bool):
+    print("Loading " + yaml_filename)
+    yaml_root = loadGalleries(yaml_filename)
+
+    if galleries:
+        makeGalleries(yaml_root)
+    if zips:
+        makeZipDownloads(yaml_root)
+
+def usage():
+    print("""
+Usage:
+    make_galleries OPTIONS
+          
+Options:
+    -g     Make galleries
+    -z     Make download zips
+""")
+    
+if __name__ == "__main__":
+    zips = "-z" in sys.argv
+    galleries = "-g" in sys.argv
+
+    if zips == False and galleries == False:
+        usage()
+    else:
+        main(galleries, zips)

+ 366 - 0
masterimg.py

@@ -0,0 +1,366 @@
+#!/usr/bin/python3
+
+import sys
+import os
+import multiprocessing
+from datetime import datetime as dt
+from PIL import Image
+import piexif
+import math
+import yaml
+from dataclasses import dataclass
+
+from typing import Optional
+
+# Requires pip install piexif, pillow, pyyaml
+
+@dataclass
+class OutputDef:
+    MaxDimension:int # max dim. wins over min dim.
+    MinDimension:int
+    OutputFormat:str
+    OutputQuality:int
+    OutputPath:str
+    OutputSuffix:str
+
+
+SocialMediaDef = OutputDef(1600, 1080, "JPEG", 96, "sm", "_sm.jpg")
+HqDef = OutputDef(2560, 1920, "JPEG", 98, "hq", "_hq.jpg")
+FullDef = OutputDef(3600, 2700, "JPEG", 98, "full", "_full.jpg")
+
+# --------
+
+_verbose = False
+
+WmScale = 1.6
+
+class Config:
+    def __init__(self, yaml_filename:str):
+        with open(yaml_filename) as f:
+            cfg = yaml.safe_load(f)
+            #cfg = yaml.safload(f.read(), Loader=yaml.SafeLoader)
+        self.CfgDict:dict = cfg
+        self.Copyright:str = cfg["Copyright"]
+        self.UserComment:str = cfg["UserComment"]
+        self.WmFile:str = cfg["WatermarkFile"]
+
+    def getFileSpecs(self, filename:str) -> dict:
+        cfg = self.CfgDict.get(filename)
+        if type(cfg) is dict:
+            return cfg
+        else:
+            return {}
+
+
+global _config
+_config:Config = None
+
+# ------
+
+class Watermark:
+    WidthRatio = 0.25 / WmScale
+    Xpos = 1.0 / WmScale
+    Ypos = 0.9 / WmScale
+    WmColorWhite = 240
+    WmColorBlack = 20
+    TranspBaseWhite = 1.2
+    TranspBaseBlack = 1.2
+    TranspStdevScale = 0.02
+    TranspIntensScale = 0.0025
+    TranspBase2 = 0.17
+    InvertWm = False
+
+    def __init__(self, wm_black:str, wm_white:Optional[str]=None):
+        self.WmBlack = wm_black
+        self.WmWhite = wm_black if wm_white is None else wm_black
+        wm_tmp = Image.open(wm_black)
+        self.WmWidth = wm_tmp.size[0]
+        self.WmHeight = wm_tmp.size[1]
+
+    @staticmethod
+    def _getIntensityVariance(img:Image, area):
+        # Calculate intensity and variance on area of img
+        cropped_area = img.crop(area)
+
+        gray_pixels = [0.21 * pixel[0] + 0.72 * pixel[1] + 0.07 * pixel[2] for pixel in cropped_area.getdata()]
+
+        mean = sum(gray_pixels) / len(gray_pixels)
+        var = sum((x - mean) ** 2 for x in gray_pixels) / len(gray_pixels)
+        return (round(mean), var)
+
+    @staticmethod
+    def _getLoHiMean(img:Image, area):
+        # Calculate lo and high 10th percentile, and mean, on area of img
+        cropped_area = img.crop(area)
+        gray_pixels = [0.21 * pixel[0] + 0.72 * pixel[1] + 0.07 * pixel[2] for pixel in cropped_area.getdata()]
+        gray_pixels.sort()
+
+        mean = sum(gray_pixels) / len(gray_pixels)
+        i_low = len(gray_pixels) // 10
+        i_high = len(gray_pixels) - i_low
+
+        return (gray_pixels[i_low], gray_pixels[i_high], round(mean))
+
+    def _planWatermark(self, main_image, watermark_area):
+        # Check out image intensity behind watermark        
+        (gray_intens, gray_var) = self._getIntensityVariance(main_image, watermark_area)
+        
+        gray_stdev = math.sqrt(gray_var)
+        
+        if gray_intens < 128:
+            wm_color = 255
+            transparency = 1 - (1 / (self.TranspBaseWhite + 
+                                     (gray_intens * self.TranspIntensScale) + gray_stdev * self.TranspStdevScale))
+        else:
+            wm_color = 0
+            transparency = 1 - (1 / (self.TranspBaseBlack + 
+                                     (255 - gray_intens) * self.TranspIntensScale + gray_stdev * self.TranspStdevScale))
+        if _verbose:
+            print("Watermark plan: intens: %d, st.dev.: %d, wm_color: %d, transp: %0.2f" % (gray_intens, gray_stdev, wm_color, transparency))
+        
+        return (wm_color, transparency)
+
+    def _planWatermark2(self, main_image, watermark_area):
+        (low, high, avg) = self._getLoHiMean(main_image, watermark_area)
+
+        f1 = (1 - self.TranspBase2) / 2 / 255
+        f2 = (1 - self.TranspBase2) / 2 / 255
+
+        if avg < 128:
+            wm_color = 255
+            transparency = self.TranspBase2 + (high - low) * f1 + high * f2
+        else:
+            wm_color = 0
+            transparency = self.TranspBase2 + (high - low) * f1 + (255 - low) * f2
+
+        if _verbose:
+            print("Watermark plan: intens: %d, lo: %d, hi: %d, wm_color: %d, transp: %0.2f" % (avg, low, high, wm_color, transparency))
+        
+        return (wm_color, transparency)
+
+    def apply(self, main_image:Image, pos:str="bl"):
+        if not pos in ["tl", "tr", "bl", "br"]:
+            raise Exception("Invalid watermark position")
+        
+        # Calculate an average image dimension by a weighted average,
+        # weighing the smallest dimension double:
+        max_dim = max(main_image.size[0], main_image.size[1])
+        min_dim = min(main_image.size[0], main_image.size[1])
+        avg_dimension = (max_dim + min_dim * 2) / 3
+
+        # This is used to size the water mark
+        wm_width = avg_dimension * self.WidthRatio
+        wm_size = (int(wm_width), int(self.WmHeight * wm_width / self.WmWidth))
+
+        # X and Y offset are specified in multiplum of watermark height
+        wm_x = round(wm_size[1] * self.Xpos)
+        wm_y = round(wm_size[1] * self.Ypos)
+
+        if "r" in pos: # right (otherwise l for left)
+            wm_x = main_image.size[0] - wm_size[0] - wm_x
+
+        if "b" in pos: # bottom (otherwise t for top)
+            wm_y = main_image.size[1] - wm_size[1] - wm_y
+
+        watermark_area = (wm_x, wm_y, wm_x + wm_size[0], wm_y + wm_size[1])
+
+        # Plan color and transparency of watermark by checking out the watermark area
+        (wm_color, transparency) = self._planWatermark2(main_image, watermark_area)
+
+        if wm_color > 128:
+            watermark = Image.open(self.WmWhite).resize(wm_size, Image.LANCZOS)
+        else:
+            watermark = Image.open(self.WmBlack).resize(wm_size, Image.LANCZOS)
+        
+        # Make watermark transparent
+        watermark = watermark.convert("RGBA")
+        pixels = watermark.getdata()
+
+        if self.InvertWm:
+            new_pixels = [(wm_color, wm_color, wm_color, round((255 - pixel[3]) * transparency)) for pixel in pixels]
+        else:
+            new_pixels = [(wm_color, wm_color, wm_color, round(pixel[3] * transparency)) for pixel in pixels]
+
+        watermark.putdata(new_pixels)
+
+        # Paste the watermark onto the main image
+        main_image.paste(watermark, (wm_x, wm_y), watermark)
+
+
+def makeExifBytes(copyright:Optional[str], user_comment:Optional[str], image_description:Optional[str]) -> bytes:
+    exif = {
+        "0th": {},
+        "Exif": {},
+        "GPS": {},
+        "1st": {},
+        "thumbnail": None
+    }
+
+    if copyright is not None:
+        exif["0th"][piexif.ImageIFD.Copyright] = copyright.encode("ascii")
+    if user_comment is not None:
+        #  = b'\0\0\0\0\0\0\0\0' + user_comment.encode('utf-8')
+        exif["Exif"][piexif.ExifIFD.UserComment] = b"ASCII\0\0\0" + user_comment.encode('ascii')
+    if image_description is not None:
+        exif['0th'][piexif.ImageIFD.ImageDescription] = image_description.encode('utf-8')
+
+    return piexif.dump(exif)
+
+def resizeSaveOutput(input_filename:str, out_name:str, img:Image, exif_bytes:bytes, output_def:OutputDef):
+    
+    if output_def.MaxDimension == 0:
+        scale = None
+    else:
+        scale1 = output_def.MinDimension / min(img.size[0], img.size[1])
+        scale2 = output_def.MaxDimension / max(img.size[0], img.size[1])
+        scale = min(scale1, scale2)
+
+    if scale is not None and scale < 1.0:
+        width = round(img.size[0] * scale)
+        height = round(img.size[1] * scale)
+        scale_msg = "scaled by %0.2f to: %dx%d" % (scale, width, height)
+        img_scaled = img.resize((width, height), Image.LANCZOS)
+    else:
+        scale_msg = "not rescaled: %dx%d" % (img.size[0], img.size[1])
+        img_scaled = img
+
+    # in_path = os.path.dirname(input_filename)
+    # if in_path == "":
+    #     in_path = "."
+    # out_path = in_path + "/" + output_def.OutputPath
+    out_path = "./" + output_def.OutputPath
+    in_fname = os.path.basename(input_filename)
+    out_fname = out_name + output_def.OutputSuffix
+    out_full = out_path + "/" + out_fname
+
+    if not os.path.isdir(out_path):
+        print("Creating dir: " + out_path)
+        os.makedirs(out_path, exist_ok=True)
+
+    img_scaled.save(out_full, output_def.OutputFormat, quality=output_def.OutputQuality, exif=exif_bytes)
+
+    size = os.path.getsize(out_full) // 1000
+    print("%s %s, saved as %s (%d KB)" % (in_fname, scale_msg, 
+        output_def.OutputPath + "/" + out_fname, size))
+
+
+def process_image(main_image_path:str, watermark:Watermark):
+    try:
+        img_name = os.path.basename(main_image_path).split(".")[0]
+        img_cfg = _config.getFileSpecs(img_name)
+
+        if "year" in img_cfg:
+            year = int(img_cfg["year"])
+        else:
+            mtime = os.path.getmtime(main_image_path)
+            year = dt.fromtimestamp(mtime).year
+        
+        print("Input: %s, (year: %d to be used for copyright notice)" % (main_image_path, year))
+
+        exif = makeExifBytes(_config.Copyright % year, _config.UserComment, img_cfg.get("title"))
+
+        # Load the main image
+        main_image = Image.open(main_image_path)
+
+        # Save full output without water mark
+        out_name = img_cfg.get("name", img_name)
+        resizeSaveOutput(main_image_path, out_name, main_image, exif, FullDef)
+
+        # Make reduced quality watermarked outputs
+        wm_spec = img_cfg.get("watermark", "bl")
+        if wm_spec != False and wm_spec != "off":
+            watermark.apply(main_image, wm_spec)
+
+        resizeSaveOutput(main_image_path, out_name, main_image, exif, SocialMediaDef)
+        resizeSaveOutput(main_image_path, out_name, main_image, exif, HqDef)
+
+    except Exception as e:
+        print("ERROR: Processing %s failed: %s" % (main_image_path, str(e)))
+
+# ----
+
+def process(num_processes:int, filenames:list):
+    print("Processes: %d" % (num_processes))
+    watermark = Watermark(_config.WmFile)
+   
+    # Creating a pool of processes
+    with multiprocessing.Pool(processes=num_processes) as pool:
+        for filename in filenames:
+            pool.apply_async(process_image, args=(filename, watermark))
+        pool.close()
+        pool.join()
+
+# ----
+        
+def welcome():
+    print("masterimg")
+
+def usage():
+    print("""
+Usage:
+    masterimg OPTIONS FILES ...
+          
+Options:
+    -h          Show this help
+    -v          More verbose
+    -p NUM      Specify number of processes (default: 6)
+""")
+
+class ShowHelpException(Exception):
+    pass
+
+class Params:
+    def __init__(self):
+        self.Flags = {"-v": False}
+        self.Ints = {"-p": 6}
+        self.Strings = {}
+        self.Floats = {}
+        self.Args = []
+        self.ArgsMin = 1
+
+    @staticmethod
+    def parse(cmds:list):
+        p = Params()
+        while len(cmds) > 0 and cmds[0].startswith("-"):
+            if cmds[0] == "-h" or cmds[0] == "-H":
+                raise ShowHelpException
+            elif cmds[0] in p.Flags:
+                p.Flags[cmds[0]] = True
+            elif cmds[0] in p.Floats:
+                p.Floats[cmds[0]] = float(cmds[1])
+                cmds.pop(0)
+            elif cmds[0] in p.Ints:
+                p.Ints[cmds[0]] = int(cmds[1])
+                cmds.pop(0)
+            elif cmds[0] in p.Strings:
+                p.Strings[cmds[0]] = cmds[1]
+                cmds.pop(0)
+            else:
+                raise ValueError("Unknown option: " + cmds[0])
+            cmds.pop(0)
+        if len(cmds) < p.ArgsMin:
+            raise ShowHelpException
+        p.Args = cmds
+        return p
+
+def main(args):
+    welcome()
+    try:
+        if len(args) == 0:
+            raise ShowHelpException
+        p = Params.parse(args)
+        global _verbose
+        _verbose = p.Flags["-v"]
+        global _config
+        _config = Config("masterimg.yaml")
+        print("Read masterimg.yaml")
+        process(p.Ints["-p"], p.Args)
+    except ShowHelpException as e:
+        usage()
+    #except Exception as e:
+    #    print(str(e))
+
+if __name__ == "__main__":
+    import sys
+    main(sys.argv[1:])
+

+ 10 - 0
publish

@@ -0,0 +1,10 @@
+#!/usr/bin/iinstall
+
+export LANGUAGE=
+export LC_ALL=C
+rm -rf public
+./make_galleries.py -g
+hugo
+du -hs public
+rsync -av --delete public/ lars@larsee.com:/var/www/larsee.com/html/photography
+

BIN
store_meta/Gallery Its cold out there preview.jpg


BIN
store_meta/Gallery Nature preview.jpg


BIN
store_meta/Gallery Nice View preview.jpg


BIN
store_meta/Gallery Texture Like preview.jpg


BIN
store_meta/Gallery Various preview.jpg


+ 30 - 0
store_meta/kofi product text.txt

@@ -0,0 +1,30 @@
+Name
+----
+Gallery download: Texture LIke
+
+Description:
+------------
+A digital download of the uncompressed full resolution pictures without watermarks from the gallery: Texture Like, as seen on: https://larsee.com/photography/texture-like/ 
+
+The download also contains the raw files from the camera and the Rawtherapee settings that were used for processing.
+
+Purchasing this product grants you a license to use the images for any purpose.
+
+
+Product Summary:
+----------------
+Digital download of gallery: Texture Like
+
+
+
+
+
+--- not all raw files there ---
+
+A digital download of the uncompressed full resolution pictures without watermarks from the gallery: Nature, as seen on: https://larsee.com/photography/nature/ 
+
+The download also contains the raw files (where available) from the camera and the Rawtherapee settings that were used for processing.
+
+Purchasing this product grants you a license to use the images for any purpose.
+
+