import os, osproc, options, json, strutils, random, algorithm, parsecfg, tables, math, times import moustachu import nimjpg type Config* = object sourceDir*: string targetDir*: string mogrifyCmd*: string siteName*: string siteAuthor*: string siteTags*: string siteDescription*: string showOriginalsButton*: bool symlinkOriginals*: bool enableJS*: bool albumSortOrder*: SortOrder pictureSortOrder*: SortOrder pictureFormatStr*: string thumbMediumWidth*: int thumbMediumHeight*: int thumbSmallHeight*: int thumbSmallQuality*: int Album* = object name*: string path*: string visible*: bool desc*: Option[string] subalbums*: seq[Album] pictures*: seq[Picture] Picture* = object name*: string path*: string desc*: Option[string] exif*: Table[string, string] width*: int height*: int thumbWidth*: int thumbHeight*: int filename*: string filetype*: string filesize*: BiggestInt const asset_exif_js = staticRead"./assets/exif.js" asset_justified_layout_js = staticRead"./assets/justified-layout.min.js" asset_albums_js = staticRead"./assets/albums.js" asset_style_css = staticRead"./assets/style.css" asset_noimages_svg = staticRead"./assets/no_images.svg" asset_iconic_svg = staticRead"./assets/iconic.svg" asset_album_html = staticRead"./assets/album.html" asset_picture_html = staticRead"./assets/picture.html" var config* {.threadvar.}: Config ### # # 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 config.albumSortOrder != Descending: return cmp(x.name, y.name) else: return cmp(y.name, x.name) proc sortPictures(x, y: Picture): int = if config.pictureSortOrder != Descending: return cmp(x.name, y.name) else: return cmp(y.name, x.name) proc isaRound* [T: float32|float64](value: T, places: int = 0): float = if places == 0: result = round(value) else: result = value * pow(10.0, T(places)) result = floor(result) result = result / pow(10.0, T(places)) proc placeAssets(targetDir: string, enableJS: bool) = echo "============" echo "Create Assets in target dir" discard existsOrCreateDir(targetDir) 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) writeFile(joinPath(targetDir, "justified-layout.min.js"), asset_justified_layout_js) writeFile(joinPath(targetDir, "albums.js"), asset_albums_js) proc removeOrphans (targetDir: string) = echo "Checking for orphaned files and folders..." let sourceDir = joinPath(config.sourceDir, targetDir.replace(config.targetDir, "")) #Albums for album in walkDir(targetDir): if album.kind != pcDir: continue var targetDir = targetDir normalizePath(targetDir) var dirname = album.path.replace(targetDir & "/", "") if dirname == "thumbnails": continue if dirname == "medium": continue if not dirExists(joinPath(sourceDir, dirname)): echo "Removing orphaned folder(" & joinPath(sourceDir, dirname) & ") from gallery!" removeDir(joinPath(targetDir, dirname)) #Photos for photo in walkDir(joinPath(targetDir, "/thumbnails/medium")): let (dir, name, ext) = splitFile(photo.path) let filename = name & ext if not fileExists(joinPath(sourceDir, filename)): echo "Removing orphaned file(" & joinPath(sourceDir, filename) & ") from gallery!" removeFile(joinPath(targetDir, name & ".html")) removeFile(joinPath(targetDir, "thumbnails/small", name & ".jpg")) removeFile(joinPath(targetDir, "thumbnails/medium", filename)) ### # # create # ### proc createPicture(path: string): Picture = let allowedExtensions = @[".jpg", ".jpeg", ".JPG", ".JPEG"] (dir, name, ext) = splitFile(path) if not allowedExtensions.contains(ext): return if fileExists(joinPath(dir, name, ".txt")): result.desc = some(readFile(joinPath(dir, name,".txt"))) let file = open(path) jpgMetadata = collect_jpg(file) if jpgMetadata.exifData.isSome(): result.exif = jpgMetadata.exifData.get result.width = int(jpgMetadata.sofData.get.width) result.height = int(jpgMetadata.sofData.get.height) result.thumbWidth = toInt(toFloat(result.width) / (result.height / config.thumbSmallHeight)) result.thumbHeight = config.thumbSmallHeight result.name = name result.path = dir result.filename = lastPathPart(path) result.filetype = ext.replace(".", "") result.filesize = getFileSize(path) proc createAlbum(path: string, isRoot: bool): Album = result.name = lastPathPart(path) if not isRoot: result.path = path if not fileExists(joinPath(path, ".nomedia")): result.visible = true if fileExists(joinPath(path, ".description")): result.desc = some(readFile(joinPath(path, ".description"))) for element in walkDir(path): if element.kind == pcDir: let album = createAlbum(element.path, false) if album.name != "": result.subalbums.add(album) else: let picture = createPicture(element.path) if picture.name != "": result.pictures.add(picture) result.subalbums.sort(sortAlbums) result.pictures.sort(sortPictures) ### # # generate # ### proc generateWebsite(targetDir: string, album: Album) = echo "============" echo "Create Album:" & album.name discard existsOrCreateDir(targetDir) discard existsOrCreateDir(joinPath(targetDir, "thumbnails")) discard existsOrCreateDir(joinPath(targetDir, "thumbnails/small")) discard existsOrCreateDir(joinPath(targetDir, "thumbnails/medium")) var templateContext = mergeJson(%* { "name": album.name, "description": "-", "numAlbums": album.subalbums.len, "numPictures": album.pictures.len, "isSubalbum": true, "hasSubalbums": false, "showDivider": false, "subalbums": [], "pictures": [] }, %config) var smallThumbnails = newSeq[string]() mediumThumbnails = newSeq[string]() bigThumbnails = newSeq[string]() if album.path == "": templateContext["isSubalbum"] = %false if album.subalbums.len != 0: templateContext["hasSubalbums"] = %true if not album.desc.isNone: templateContext["description"] = %album.desc.get if album.subalbums.len > 0 and album.pictures.len > 0: templateContext["showDivider"] = %true for subalbum in album.subalbums: generateWebsite(joinPath(targetDir, subalbum.name), subalbum) var thumbnail1 = "/no_images.svg" thumbnail2 = "/no_images.svg" thumbnail3 = "/no_images.svg" if subalbum.pictures.len > 0: thumbnail1 = subalbum.name & "/thumbnails/small/" & subalbum.pictures[rand(0..subalbum.pictures.len-1)].name & ".jpg" thumbnail2 = subalbum.name & "/thumbnails/small/" & subalbum.pictures[rand(0..subalbum.pictures.len-1)].name & ".jpg" thumbnail3 = subalbum.name & "/thumbnails/small/" & subalbum.pictures[rand(0..subalbum.pictures.len-1)].name & ".jpg" templateContext["subalbums"].add(%* { "name": subalbum.name, "numAlbums": subalbum.subalbums.len, "numPictures": subalbum.pictures.len, "thumbnail1": thumbnail1, "thumbnail2": thumbnail2, "thumbnail3": thumbnail3 }) for index, picture in album.pictures: var takestamp: DateTime if picture.exif.hasKey("DateTimeOriginal"): takestamp = picture.exif["DateTimeOriginal"].parse("yyyy:MM:dd HH:mm:ss") var pictureTemplateContext = mergeJson(%* { "name": picture.name, "orig": joinPath("/originals", picture.path.replace(config.sourceDir, ""), picture.filename), "filename": picture.filename, "width": picture.width, "height": picture.height, "make": picture.exif.getOrDefault("Make", "-"), "model": picture.exif.getOrDefault("Model", "-"), "lensModel": picture.exif.getOrDefault("LensModel", "-"), "shutterSpeed": picture.exif.getOrDefault("ExposureTime", "-"), "shutterProgram": picture.exif.getOrDefault("ExposureProgram", "-"), "aperture": picture.exif.getOrDefault("FNumber", "-"), "focalLength": picture.exif.getOrDefault("FocalLength", "-"), "iso": picture.exif.getOrDefault("ISOSpeedRatings", "-"), "flash": picture.exif.getOrDefault("Flash", "-"), "description": "-", "hasPrev": false, "hasNext": false, "filesize": isaRound((picture.filesize.int/1000/1000), 2) }, %config) if not picture.desc.isNone: pictureTemplateContext["description"] = %picture.desc.get if picture.exif.hasKey("FocalLengthIn35mmFilm"): pictureTemplateContext["focalLength35mm"] = %(picture.exif["FocalLengthIn35mmFilm"]) if $takestamp != "Uninitialized DateTime": pictureTemplateContext["takestamp"] = %takestamp.format("yyyy-MM-dd HH:mm:ss") else: pictureTemplateContext["takestamp"] = %"-" if index > 0: pictureTemplateContext["hasPrev"] = %true pictureTemplateContext["prev_name"] = %album.pictures[index-1].name pictureTemplateContext["prev_filename"] = %album.pictures[index-1].filename if index+1 < album.pictures.len: pictureTemplateContext["hasNext"] = %true pictureTemplateContext["next_name"] = %album.pictures[index+1].name pictureTemplateContext["next_filename"] = %album.pictures[index+1].filename echo "Generate picture page: " & picture.name writeFile(joinPath(targetDir, picture.name & ".html"), render(asset_picture_html, pictureTemplateContext)) if not fileExists(joinPath(targetDir, "thumbnails/small", picture.name & ".jpg")): smallThumbnails.add([config.mogrifyCmd, "-format", "jpg", "-interlace", "plane", "-quality", $config.thumbSmallQuality, "-path", quoteShell(joinPath(targetDir, "thumbnails/small")), "-thumbnail", "x" & $config.thumbSmallHeight, quoteShell(joinPath(picture.path, picture.filename)) ].join(" ")) if not fileExists(joinPath(targetDir, "thumbnails/medium", picture.filename)): mediumThumbnails.add([config.mogrifyCmd, "-strip", "-interlace", "plane", "-format", $picture.filetype, "-path", quoteShell(joinPath(targetDir, "thumbnails/medium")), "-resize", $config.thumbMediumWidth & "x\\>", quoteShell(joinPath(picture.path, picture.filename)) ].join(" ")) var picAlbumTemplateContext = %* { "name": picture.name, "width": picture.thumbWidth, "height": picture.thumbHeight, } if $takestamp != "Uninitialized DateTime": picAlbumTemplateContext["takestamp"] = %takestamp.format(config.pictureFormatStr) templateContext["pictures"].add(picAlbumTemplateContext) echo "Generate small thumbnails!" discard execProcesses(smallThumbnails) echo "Generate medium thumbnails!" discard execProcesses(mediumThumbnails) echo "Generate album page!" writeFile(joinPath(targetDir, "index.html"), render(asset_album_html, templateContext)) removeOrphans(targetDir) echo "\n" proc main = randomize() init_jpg() if paramCount() == 0: echo "No config-file given! Exiting..." quit(QuitFailure) if not fileExists(paramStr(1)): stdout.write "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("", "MogrifyCmd", "/usr/bin/mogrify") config.setSectionKey("Site", "Author", "Max Musermann") config.setSectionKey("Site", "Name", "ctucx gallery sample") 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("Site", "EnableJS", "true") config.setSectionKey("Album", "SortOrder", "Descending") config.setSectionKey("Picture", "SortOrder", "Ascending") config.setSectionKey("Picture", "DateTimeFormatStr", "yyyy-MM-dd") config.setSectionKey("Thumbnails", "SmallWidth", "200") config.setSectionKey("Thumbnails", "SmallHeight", "200") config.setSectionKey("Thumbnails", "SmallQuality", "90") config.writeConfig(paramStr(1)) echo "Have written a default config to this file: " & paramStr(1) echo "Please check it and rerun this program." quit() else: quit() try: let configFile = loadConfig(paramStr(1)) config = Config( sourceDir: normalizedPath(configFile.getSectionValue("", "SourceDir", "")), targetDir: normalizedPath(configFile.getSectionValue("", "TargetDir", "")), mogrifyCmd: configFile.getSectionValue("", "MogrifyCmd", "/usr/bin/mogrify"), siteName: configFile.getSectionValue("Site", "Name", "default title - change me plese"), siteAuthor: configFile.getSectionValue("Site", "Author", "Max Mustermann"), siteTags: configFile.getSectionValue("Site", "Tags", ""), siteDescription: configFile.getSectionValue("Site", "Description", ""), showOriginalsButton: configFile.getSectionValue("Site", "ShowOriginalsButton", "true").parseBool, symlinkOriginals: configFile.getSectionValue("Site", "SymlinkOriginals", "true").parseBool, enableJS: configFile.getSectionValue("Site", "EnableJS", "true").parseBool, albumSortOrder: parseEnum[SortOrder](configFile.getSectionValue("Album", "SortOrder", "Descending")), pictureSortOrder: parseEnum[SortOrder](configFile.getSectionValue("Picture", "SortOrder", "Ascending")), pictureFormatStr: configFile.getSectionValue("Picture", "DateTimeFormatStr", "yyyy-MM-dd"), thumbMediumWidth: configFile.getSectionValue("Thumbnails", "MediumWidth", "0").parseInt, thumbMediumHeight: configFile.getSectionValue("Thumbnails", "MediumHeight", "0").parseInt, thumbSmallHeight: configFile.getSectionValue("Thumbnails", "SmallHeight", "0").parseInt, thumbSmallQuality: configFile.getSectionValue("Thumbnails", "SmallQuality", "90").parseInt ) if config.sourceDir == "": echo "Config-value 'SourceDir' has to be set!" quit(QuitFailure) if config.targetDir == "": echo "Config-value 'TargetDir' has to be set!" quit(QuitFailure) if (execProcess(config.mogrifyCmd & " -v").contains("No such file or directory")): echo "It seems like you don't have ImageMagick installed or the specified path in the config is incorrect.\nBye!" quit(QuitFailure) if config.thumbSmallHeight == 0: config.thumbSmallHeight = 200 if config.thumbMediumWidth == 0 and config.thumbMediumHeight == 0: config.thumbMediumWidth = 1920 config.thumbMediumHeight = 1080 except ValueError: let e = getCurrentException() msg = getCurrentExceptionMsg() echo "Got exception while parsing config: ", repr(e), " with message ", msg quit(QuitFailure) except: echo "Unknown exception while parsing of configuration!" quit(QuitFailure) if not dirExists(config.sourceDir): echo "The source directory does not exist!\nBye!" quit(QuitFailure) if not dirExists(config.targetDir): echo "The target directory does not exist!\nBye!" quit(QuitFailure) echo "Collect files and Exif metadata..." let mainAlbum = createAlbum(config.sourceDir, true) if config.symlinkOriginals != false: if not symlinkExists(joinPath(config.targetDir, "originals")): createSymlink(config.sourceDir, joinPath(config.targetDir, "originals")) placeAssets(config.targetDir, config.enableJS) generateWebsite(config.targetDir, mainAlbum) main()