ctucx.git: gallery

static-site-generator for image-galleries [used @ photos.ctu.cx]

commit 01904e9911dfef9f8f3aabbf749c071f0465537c
parent 61e1ad6c3a4b55c927892102df5ddc8cec9a4219
Author: ctucx <c@ctu.cx>
Date: Sun, 24 May 2020 18:44:49 +0200

support for file-based configuration
5 files changed, 225 insertions(+), 112 deletions(-)
M
README.md
|
9
++++-----
A
sample.config
|
18
++++++++++++++++++
M
src/assets/album.html
|
36
++++++++++++++++++++++--------------
M
src/assets/picture.html
|
78
+++++++++++++++++++++++++++++++++++++++++++++---------------------------------
M
src/gallery.nim
|
196
+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
diff --git a/README.md b/README.md
@@ -1,6 +1,5 @@
-# How to ctucx picture_gallery
-
-a little Example for https://git.ctu.cx/ctucx/gallery/
+# ctucx' gallery
+a little introduction into this software.
 
 ## Install
 ```shell=

@@ -11,11 +10,11 @@ a little Example for https://git.ctu.cx/ctucx/gallery/
 
 ## Use the Programm
 ```shell=
-[user@pc ~]$ ./gallery <source_dir> <target_dir>
+[user@pc ~]$ ./gallery <config.file>
 ```
 
 ## Dependencies
 Dependencies: ImageMagick, nim >= 1.0.6, nimble
 
 ## Infos
-When running the program, a source folder and a destination folder must be specified, then the program creates all necessary HTML and CSS objects to display the pages correctly and creates images in the following sizes: medium: 1920x1080, thumbnail: 200x200
+When running the program, a config file must be specified, then the program creates all necessary HTML and CSS objects to display the pages correctly and creates images in the following sizes: medium: 1920x1080, thumbnail: 200x200
diff --git a/sample.config b/sample.config
@@ -0,0 +1,18 @@
+SourceDir=/home/ctucx/Pictures/Bahnbilder
+TargetDir=./out
+
+[Site]
+Author=ctucx
+Name="ctucx' sample gallery"
+Description="a short discription for your site"
+Tags="a list of tags for seo stuff" 
+ShowOriginalsButton=false
+SymlinkOriginals=false
+EnableJS=true   ;if disabled no exif data will get parsed and keyboard navigation does not work
+
+[Thumbnail]
+MediumMaxWidth=1920
+MediumMaxHeight=1080
+ThumbMaxWidth=200
+ThumbMaxHeight=200
+ThumbQuality=90
diff --git a/src/assets/album.html b/src/assets/album.html
@@ -2,17 +2,22 @@
 <html>
 	<head>
 		<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
-		<title>ctucx' pics</title>
 
-		<link type="text/css" rel="stylesheet" href="/style.css">
-		<link rel="shortcut icon" href="/favicon.ico">
+		<title>{{name}} | {{SiteName}}</title>
 
+		<meta name="description" content="{{SiteDescription}}">
+		<meta name="keywords" content="{{SiteTags}}">
+		<meta name="author" content="{{SiteAuthor}}">
+
+        <meta name="generator" content="https://git.ctu.cx/ctucx/gallery">
 		<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0">
 		<meta name="apple-mobile-web-app-status-bar-style" content="black">
 		<meta name="apple-mobile-web-app-capable" content="yes">
+
+		<link type="text/css" rel="stylesheet" href="/style.css">
+		<link rel="shortcut icon" href="/favicon.ico">
 	</head>
 	<body>
-
 		<header class="header">
 			{{#isSubalbum}}
 			<a class="button" title="back" href="..">

@@ -110,22 +115,25 @@
 
 			{{#pictures}}
 			<a href="{{name}}/" class="photo">
-				<img src="./thumbnails/{{name}}.png" alt="Photo thumbnail" width="200" height="200">
+				<img src="./thumbnails/{{name}}.png" alt="Photo thumbnail" width="{{ThumbThumbMaxWidth}}" height="{{ThumbThumbMaxHeight}}">
 				<span class="overlay">
 					<h1>{{name}}</h1>
-					<p><span title="Camera Date"><svg class="iconic "><use xlink:href="/iconic.svg#camera-slr"></use></svg></span></p>
+					<!--<p><span title="Camera Date"><svg class="iconic "><use xlink:href="/iconic.svg#camera-slr"></use></svg></span></p>-->
 				</span>
 			</a>
 			{{/pictures}}
 
 		</div>
+		{{#isSubalbum}}
+    	{{#SiteEnableJS}}
+		<script type="text/javascript">
+			window.onkeyup = function(e) {
+				if (e.keyCode == 27) window.location = "..";
+				if (e.keyCode == 32) document.getElementById("toggle").checked = true;
+			}
+		</script>
+		{{/SiteEnableJS}}
+		{{/isSubalbum}}
+
 	</body>
-	{{#isSubalbum}}
-	<script type="text/javascript">
-		window.onkeyup = function(e) {
-			if (e.keyCode == 27) window.location = "..";
-			if (e.keyCode == 32) document.getElementById("toggle").checked = true;
-		}
-	</script>
-	{{/isSubalbum}}
 </html>
diff --git a/src/assets/picture.html b/src/assets/picture.html
@@ -2,11 +2,13 @@
 <html>
 	<head>
 		<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
-		<title>ctucx' pics</title>
+		<title>{{name}} | {{SiteName}}</title>
 
-		<link type="text/css" rel="stylesheet" href="/style.css">
-		<link rel="shortcut icon" href="favicon.ico">
+		<meta name="description" content="{{SiteDescription}}">
+		<meta name="keywords" content="{{SiteTags}}">
+		<meta name="author" content="{{SiteAuthor}}">
 
+        <meta name="generator" content="https://git.ctu.cx/ctucx/gallery">
 		<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0">
 		<meta name="apple-mobile-web-app-status-bar-style" content="black">
 		<meta name="apple-mobile-web-app-capable" content="yes">

@@ -18,10 +20,13 @@
 	</head>
 	<body>
 		<header class="header header-view" style="display: flex;">
-			<!--<div class="header-toolbar headerer-toolbar-photo header-toolbar-visible">-->
 			<a class="button" href=".." id="button_close" title="Close Photo"><svg class="iconic"><use xlink:href="/iconic.svg#chevron-left"></use></svg></a>
 			<a class="header-title">{{name}}<svg class="iconic "><use xlink:href="/iconic.svg#caret-bottom"></use></svg></a>
-			<a class="button" href="/originals/{{orig}}" title="Download"><svg class="iconic"><use xlink:href="/iconic.svg#cloud-download"></use></svg></a>
+
+			{{#SiteShowOrigBtn}}
+			<a class="button" href="/originals{{orig}}" title="Download"><svg class="iconic"><use xlink:href="/iconic.svg#cloud-download"></use></svg></a>
+			{{/SiteShowOrigBtn}}
+			
 			<a class="header-divider"></a>
 
 			<input type="checkbox" id="toggle">

@@ -125,6 +130,11 @@
 							</tr>
 						</tbody>
 					</table>
+					{{^SiteEnableJS}}
+					<p>
+					Exif parsing is disabled by owner of this site.
+					</p>
+					{{/SiteEnableJS}}
 				</div>
 			</div>
 		</header>

@@ -148,33 +158,35 @@
 			</div>
 			{{/hasNext}}
 		</div>
+		{{#SiteEnableJS}}
+		<script type="text/javascript" src="/exif.js"></script>
+		<script type="text/javascript">
+			window.onload=getExif;
+			window.onkeyup = function(e) {
+				if (e.keyCode == 27) window.location = "..";
+				if (e.keyCode == 39) window.location = "../{{next_name}}";
+				if (e.keyCode == 37) window.location = "../{{prev_name}}";
+				if (e.keyCode == 32) document.getElementById("toggle").checked = true;
+			}
+
+			function getExif() {
+				let image = document.getElementById("image");
+
+				EXIF.getData(image, function() {
+					document.getElementById("attr_resolution").innerHTML       = EXIF.getTag(this, "PixelXDimension") + 'x' + EXIF.getTag(this, "PixelYDimension");
+
+					document.getElementById("attr_captured").innerHTML         = EXIF.getTag(this, "DateTimeOriginal");
+					document.getElementById("attr_make").innerHTML             = EXIF.getTag(this, "Make");
+					document.getElementById("attr_type/model").innerHTML       = EXIF.getTag(this, "Model");
+					document.getElementById("attr_shutter-speed").innerHTML    = EXIF.getTag(this, "ExposureTime") + ' s';
+					document.getElementById("attr_shutter-program").innerHTML  = EXIF.getTag(this, "ExposureProgram");
+					document.getElementById("attr_aperture").innerHTML         = EXIF.getTag(this, "FNumber");
+					document.getElementById("attr_focal-length").innerHTML     = EXIF.getTag(this, "FocalLength") + 'mm';
+					document.getElementById("attr_iso").innerHTML              = EXIF.getTag(this, "ISOSpeedRatings");
+					document.getElementById("attr_flash").innerHTML            = EXIF.getTag(this, "Flash");
+	    		});
+			}
+		</script>
+		{{/SiteEnableJS}}
 	</body>
-	<script type="text/javascript" src="/exif.js"></script>
-	<script type="text/javascript">
-		window.onload=getExif;
-		window.onkeyup = function(e) {
-			if (e.keyCode == 27) window.location = "..";
-			if (e.keyCode == 39) window.location = "../{{next_name}}";
-			if (e.keyCode == 37) window.location = "../{{prev_name}}";
-			if (e.keyCode == 32) document.getElementById("toggle").checked = true;
-		}
-
-		function getExif() {
-			let image = document.getElementById("image");
-
-			EXIF.getData(image, function() {
-				document.getElementById("attr_resolution").innerHTML       = EXIF.getTag(this, "PixelXDimension") + 'x' + EXIF.getTag(this, "PixelYDimension");
-
-				document.getElementById("attr_captured").innerHTML         = EXIF.getTag(this, "DateTimeOriginal");
-				document.getElementById("attr_make").innerHTML             = EXIF.getTag(this, "Make");
-				document.getElementById("attr_type/model").innerHTML       = EXIF.getTag(this, "Model");
-				document.getElementById("attr_shutter-speed").innerHTML    = EXIF.getTag(this, "ExposureTime") + ' s';
-				document.getElementById("attr_shutter-program").innerHTML  = EXIF.getTag(this, "ExposureProgram");
-				document.getElementById("attr_aperture").innerHTML         = EXIF.getTag(this, "FNumber");
-				document.getElementById("attr_focal-length").innerHTML     = EXIF.getTag(this, "FocalLength") + 'mm';
-				document.getElementById("attr_iso").innerHTML              = EXIF.getTag(this, "ISOSpeedRatings");
-				document.getElementById("attr_flash").innerHTML            = EXIF.getTag(this, "Flash");
-    		});
-		}
-	</script>
 </html>
diff --git a/src/gallery.nim b/src/gallery.nim
@@ -1,4 +1,4 @@
-import os, osproc, options, json, strutils, sequtils, random, algorithm
+import os, osproc, options, json, strutils, sequtils, random, algorithm, parsecfg, tables
 import moustachu
 
 const asset_exif_js      = staticRead"./assets/exif.js"

@@ -24,6 +24,31 @@ type
         desc*:     Option[string]
         size*:     BiggestInt
 
+###
+#
+# Utils
+#
+###
+
+proc mergeJson(a: JsonNode, b: JsonNode): JsonNode =
+    result = a
+    for key, val in b:
+        result[key] = val 
+
+
+proc sortAlbums(x, y: Album): int =
+  if x.name < y.name: -1
+  elif x.name == y.name: 0
+  else: 1
+
+
+proc sortPictures(x, y: Picture): int =
+  if x.name < y.name: -1
+  elif x.name == y.name: 0
+  else: 1
+
+
+
 
 proc createPicture(path: string): Picture = 
     let allowedExtensions = @[".jpg", ".jpeg", ".JPG", ".JPEG"]

@@ -38,16 +63,6 @@ proc createPicture(path: string): Picture =
     result.filename   = lastPathPart(path)
     result.size       = getFileSize(path)
 
-proc sortAlbums(x, y: Album): int =
-  if x.name < y.name: -1
-  elif x.name == y.name: 0
-  else: 1
-
-
-proc sortPictures(x, y: Picture): int =
-  if x.name < y.name: -1
-  elif x.name == y.name: 0
-  else: 1
 
 proc createAlbum(path: string, isRoot: bool): Album = 
     result.name    = lastPathPart(path)

@@ -70,34 +85,34 @@ proc createAlbum(path: string, isRoot: bool): Album =
     result.pictures.sort(sortPictures)
 
 
-proc placeAssets(targetDir: string) =
+proc placeAssets(targetDir: string, enableJS: bool) =
     echo "============"
     echo "Create Assets in target dir"
 
     discard existsOrCreateDir(targetDir)
-    writeFile(joinPath(targetDir, "exif.js"), asset_exif_js)
     writeFile(joinPath(targetDir, "style.css"), asset_style_css)
     writeFile(joinPath(targetDir, "no_images.svg"), asset_noimages_svg)
     writeFile(joinPath(targetDir, "iconic.svg"), asset_iconic_svg)
+    if enableJS: writeFile(joinPath(targetDir, "exif.js"), asset_exif_js)
 
 
-proc generateWebsite(sourceDir: string, targetDir: string, album: Album) =
+proc generateWebsite(sourceDir: string, targetDir: string, album: Album, config: JsonNode) =
     echo "============"
     echo "Create Album:" & album.name
     discard existsOrCreateDir(targetDir)
     discard existsOrCreateDir(joinPath(targetDir, "thumbnails"))
     discard existsOrCreateDir(joinPath(targetDir, "medium"))
 
-    var templateContext = %* {
-            "name":         album.name,
-            "description":  "-",
-            "numAlbums":    album.subalbums.len,
-            "numPictures":  album.pictures.len,
-            "isSubalbum":   true,
-            "hasSubalbums": false,
-            "subalbums":    [],
-            "pictures":     []
-        }
+    var templateContext = mergeJson(%* {
+        "name":         album.name,
+        "description":  "-",
+        "numAlbums":    album.subalbums.len,
+        "numPictures":  album.pictures.len,
+        "isSubalbum":   true,
+        "hasSubalbums": false,
+        "subalbums":    [],
+        "pictures":     []
+    }, config)
 
     var smallThumbnails  = newSeq[string]()
     var mediumThumbnails = newSeq[string]()

@@ -107,7 +122,7 @@ proc generateWebsite(sourceDir: string, targetDir: string, album: Album) =
     if album.subalbums.len > 0: templateContext["hasSubalbums"] = %true
 
     for subalbum in album.subalbums:
-        generateWebsite(sourceDir, joinPath(targetDir,subalbum.name), subalbum)
+        generateWebsite(sourceDir, joinPath(targetDir, subalbum.name), subalbum, config)
 
         var thumbnail1 = "/no_images.svg"
         var thumbnail2 = "/no_images.svg"

@@ -119,25 +134,25 @@ proc generateWebsite(sourceDir: string, targetDir: string, album: Album) =
             thumbnail3 = subalbum.name & "/thumbnails/" & subalbum.pictures[rand(0..subalbum.pictures.len-1)].name & ".png"
 
         templateContext["subalbums"].add(%* {
-                "name":        subalbum.name,
-                "numAlbums":   subalbum.subalbums.len,
-                "numPictures": subalbum.pictures.len,
-                "thumbnail1":  thumbnail1,
-                "thumbnail2":  thumbnail2,
-                "thumbnail3":  thumbnail3
-            })
+            "name":        subalbum.name,
+            "numAlbums":   subalbum.subalbums.len,
+            "numPictures": subalbum.pictures.len,
+            "thumbnail1":  thumbnail1,
+            "thumbnail2":  thumbnail2,
+            "thumbnail3":  thumbnail3
+        })
 
     for index, picture in album.pictures:
         discard existsOrCreateDir(joinPath(targetDir, picture.name))
 
-        var pictureTemplateContext = %* {
-                "name":        picture.name,
-                "orig":        picture.path.replace(sourceDir, "") & "/" & picture.filename,
-                "description": "-",
-                "hasPrev":     false,
-                "hasNext":     false,
-                "size":        (picture.size.int/1000/1000)
-            }
+        var pictureTemplateContext = mergeJson(%* {
+            "name":        picture.name,
+            "orig":        picture.path.replace(sourceDir, "") & "/" & picture.filename,
+            "description": "-",
+            "hasPrev":     false,
+            "hasNext":     false,
+            "size":        (picture.size.int/1000/1000)
+        }, config)
 
         if not picture.desc.isNone: pictureTemplateContext["description"] = %picture.desc.get
 

@@ -149,19 +164,19 @@ proc generateWebsite(sourceDir: string, targetDir: string, album: Album) =
             pictureTemplateContext["hasNext"]   = %true
             pictureTemplateContext["next_name"] = %album.pictures[index+1].name
 
+
         echo "Generate picture page: " & picture.name
         writeFile(joinPath(targetDir, picture.name, "index.html"), render(asset_picture_html, pictureTemplateContext))
 
-
         if not fileExists(targetDir & "/thumbnails/" & picture.name & ".png"):
-            smallThumbnails.add("/usr/bin/env mogrify -strip -quality 90 -format png -path " & quoteShell(joinPath(targetDir, "thumbnails")) & " -thumbnail 200x200^ -gravity center -extent 200x200 " & quoteShell(joinPath(picture.path, picture.filename)))
+            smallThumbnails.add("/usr/bin/env mogrify -strip -quality " & $config["ThumbThumbQuality"].getInt & " -format png -path " & quoteShell(joinPath(targetDir, "thumbnails")) & " -thumbnail " & $config["ThumbThumbMaxWidth"].getInt & "x" & $config["ThumbThumbMaxHeight"].getInt & "^ -gravity center -extent " & $config["ThumbThumbMaxWidth"].getInt & "x" & $config["ThumbThumbMaxHeight"].getInt & " " & quoteShell(joinPath(picture.path, picture.filename)))
 
         if not fileExists(targetDir & "/medium/" & picture.name & ".jpg"):
-            mediumThumbnails.add("/usr/bin/env mogrify -format jpg -path " & quoteShell(joinPath(targetDir, "medium")) & " -resize 1920x\\> " & quoteShell(joinPath(picture.path, picture.filename)))
+            mediumThumbnails.add("/usr/bin/env mogrify -format jpg -path " & quoteShell(joinPath(targetDir, "medium")) & " -resize " & $config["ThumbMediumMaxWidth"].getInt & "x\\> " & quoteShell(joinPath(picture.path, picture.filename)))
 
         templateContext["pictures"].add(%* {
-                "name":        picture.name,
-            })
+            "name": picture.name,
+        })
 
     echo "Generate small thumbnails!"
     discard execProcesses(smallThumbnails)

@@ -175,28 +190,89 @@ proc generateWebsite(sourceDir: string, targetDir: string, album: Album) =
 
 
 proc main = 
-  randomize()
+    randomize()
+
+    if (execProcess("/usr/bin/env mogrify -v").contains("No such file or directory")):
+        echo "It seems like you don't have ImageMagick installed, which is mandatory to use this tool.\nBye!"
+        quit()
+
+    if not fileExists(paramStr(1)):
+        echo "The given config-file doesn't exist. Do u wanna write a default one to it? [y/N]"
+
+        if readLine(stdin) == "y":
+            var config = newConfig()
+            config.setSectionKey("", "SourceDir", "./foobar")
+            config.setSectionKey("", "TargetDir", "./out")
+
+            config.setSectionKey("Site",        "Author",                 "ctucx")
+            config.setSectionKey("Site",        "Name",                   "ctucx' bahnbilder")
+            config.setSectionKey("Site",        "Description",            "a short discription for your site")
+            config.setSectionKey("Site",        "Tags",                   "a list of tags for seo stuff")
+            config.setSectionKey("Site",        "ShowOriginalsButton",    "true")
+            config.setSectionKey("Site",        "SymlinkOriginals",       "false")
+
+            config.setSectionKey("Thumbnails",  "MediumMaxWidth",         "1920")
+            config.setSectionKey("Thumbnails",  "MediumMaxHeight",        "1080")
+            config.setSectionKey("Thumbnails",  "ThumbMaxWidth",          "200")
+            config.setSectionKey("Thumbnails",  "ThumbMaxHeight",         "200")
+            config.setSectionKey("Thumbnails",  "ThumbQuality",           "90")
+            config.setSectionKey("Thumbnails",  "EnableJS",               "true")
+
+            config.writeConfig(paramStr(1))
+            echo "Have written a default config to this file: " & paramStr(1)
+            echo "Please check it and rerun this program."
+            quit()
+
+    var config = %* {}
+
+    try:
+        var configFile = loadConfig(paramStr(1))
+        config     = %* {
+            "SourceDir":             configFile.getSectionValue("",          "SourceDir"),
+            "TargetDir":             configFile.getSectionValue("",          "TargetDir"),
+            "SiteName":              configFile.getSectionValue("Site",      "Name"),
+            "SiteAuthor":            configFile.getSectionValue("Site",      "Author"),
+            "SiteDescription":       configFile.getSectionValue("Site",      "Description"),
+            "SiteTags":              configFile.getSectionValue("Site",      "Tags"),
+            "SiteShowOrigBtn":       configFile.getSectionValue("Site",      "ShowOriginalsButton").parseBool,     #not implemented yet
+            "SiteSymlinkOrig":       configFile.getSectionValue("Site",      "SymlinkOriginals").parseBool,        #not implemented yet
+            "SiteEnableJS":          configFile.getSectionValue("Site",      "EnableJS").parseBool,
+            "ThumbMediumMaxWidth":   configFile.getSectionValue("Thumbnail", "MediumMaxWidth").parseInt,          #not implemented yet
+            "ThumbMediumMaxHeight":  configFile.getSectionValue("Thumbnail", "MediumMaxHeight").parseInt,         #not implemented yet
+            "ThumbThumbMaxWidth":    configFile.getSectionValue("Thumbnail", "ThumbMaxWidth").parseInt,           #not implemented yet
+            "ThumbThumbMaxHeight":   configFile.getSectionValue("Thumbnail", "ThumbMaxHeight").parseInt,          #not implemented yet
+            "ThumbThumbQuality":     configFile.getSectionValue("Thumbnail", "ThumbQuality").parseInt             #not implemented yet
+        }
+
+    except ValueError:
+        let
+         e = getCurrentException()
+         msg = getCurrentExceptionMsg()
 
-  if (execProcess("/usr/bin/env mogrify -v").contains("No such file or directory")):
-    echo "It seems like you don't have ImageMagick installed, which is mandatory to use this tool.\nBye!"
-    quit()
+        echo "Got exception while parsing config: ", repr(e), " with message ", msg        
+        quit()
 
-  let sourceDir = normalizedPath(paramStr(1))
-  let targetDir = normalizedPath(paramStr(2))
+    except:
+        echo "Unknown exception while parsing of configuration!"
+        quit()
 
-  if not dirExists(sourceDir):
-    echo "The source directory does not exist!\nBye!"
-    quit()
+    if not dirExists(config["SourceDir"].getStr):
+        echo "The source directory does not exist!\nBye!"
+        quit()
 
-  if not dirExists(targetDir):
-    echo "The target directory does not exist!\nBye!"
-    quit()
+    if not dirExists(config["TargetDir"].getStr):
+        echo "The target directory does not exist!\nBye!"
+        quit()
 
   
+    let mainAlbum = createAlbum(config["SourceDir"].getStr, true)
 
-  let mainAlbum = createAlbum(sourceDir, true)
+    if config["SiteSymlinkOrig"].getBool != false:
+        if not symlinkExists(joinPath(config["TargetDir"].getStr, "originals")):
+            createSymlink(config["SourceDir"].getStr, joinPath(config["TargetDir"].getStr, "originals"))
+ 
+    placeAssets(config["TargetDir"].getStr, config["SiteEnableJS"].getBool)
+    generateWebsite(config["SourceDir"].getStr, config["TargetDir"].getStr, mainAlbum, config)
 
-  placeAssets(targetDir)
-  generateWebsite(sourceDir, targetDir, mainAlbum)
 
 main()