commit 52b8eb22f222d1ce98804ec92815659b067ec96b
parent 41f75257bb3d329e6823910f92e72de9d50c69be
Author: Leah (ctucx) <leah@ctu.cx>
Date: Sat, 13 Mar 2021 18:10:03 +0100
parent 41f75257bb3d329e6823910f92e72de9d50c69be
Author: Leah (ctucx) <leah@ctu.cx>
Date: Sat, 13 Mar 2021 18:10:03 +0100
generate jpg thumbnails, use nimjpg/nimexif for exif-parsing, remove exif.js, use flicker's justified-layout library for nice looking layout
6 files changed, 527 insertions(+), 732 deletions(-)
M
|
191
++++++++++++++++++++++++++++++++++++++++---------------------------------------
M
|
304
++++++++++++++++++++++++++++++++++++-------------------------------------------
M
|
495
++++++++++++++++++++++---------------------------------------------------------
diff --git a/src/assets/album.html b/src/assets/album.html @@ -1,8 +1,7 @@ <!DOCTYPE HTML> -<html> +<html lang="en"> <head> - <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> - + <meta charset="utf-8"> <title>{{name}} | {{siteName}}</title> <meta name="description" content="{{siteDescription}}"> @@ -15,121 +14,123 @@ <meta name="apple-mobile-web-app-capable" content="yes"> <link type="text/css" rel="stylesheet" href="/style.css"> + {{#enableJS}} + <link rel="preload" href="/albums.js" as="script"> + <script src="/justified-layout.min.js"></script> + {{/enableJS}} <link rel="shortcut icon" href="/favicon.ico"> </head> <body> - <header class="header"> - {{#isSubalbum}} - <a class="button" title="back" href=".."> - <svg class="iconic"><use xlink:href="/iconic.svg#chevron-left"></use></svg> - </a> - {{/isSubalbum}} + <header> + <div class="flex"> + {{#isSubalbum}} + <a class="button" id="back" href=".."> + <svg class="iconic"><use xlink:href="/iconic.svg#chevron-left"></use></svg> + </a> + {{/isSubalbum}} - <a class="header-title">{{name}}</a> + <a class="title">{{name}}</a> - {{#isSubalbum}} - <input type="checkbox" id="toggle"> - <label for="toggle" style="display: block;" class="button button--info" title="Info"> - <svg class="iconic"><use xlink:href="/iconic.svg#info"></use></svg> - </label> - - <div class="sidebar"> - <div class="sidebar-header"> - <h1>About</h1> - </div> + {{#isSubalbum}} + <input type="checkbox" id="toggle"> + <label for="toggle" style="display: block;" class="button info" title="Info"> + <svg class="iconic"><use xlink:href="/iconic.svg#info"></use></svg> + </label> - <div class="sidebar-wrapper"> - <div class="sidebar-divider"> - <h1>Basics</h1> + <div class="sidebar"> + <div class="sidebar-header"> + <h1>About</h1> </div> - - <table> - <tbody> - <tr> - <td>Title</td> - <td><span class="attr_title">{{name}}</span></td> - </tr> + + <div class="sidebar-wrapper"> + <div class="sidebar-divider"> + <h1>Basics</h1> + </div> + + <table> + <tbody> + <tr> + <td>Title</td> + <td><span class="attr_title">{{name}}</span></td> + </tr> + + <tr> + <td>Description</td> + <td><span class="attr_description">{{description}}</span></td> + </tr> + </tbody> + </table> - <tr> - <td>Description</td> - <td><span class="attr_description">{{description}}</span></td> - </tr> - </tbody> - </table> - - <div class="sidebar-divider"> - <h1>Album</h1> + <div class="sidebar-divider"> + <h1>Album</h1> + </div> + + <table> + <tbody> + <tr> + <td>Created</td> + <td><span class="attr_created">-</span></td> + </tr> + + <tr> + <td>Subalbums</td> + <td><span class="attr_subalbums">{{numAlbums}}</span></td> + </tr> + + <tr> + <td>Pictures</td> + <td><span class="attr_pictures">{{numPictures}}</span></td> + </tr> + </tbody> + </table> </div> - - <table> - <tbody> - <tr> - <td>Created</td> - <td><span class="attr_created">-</span></td> - </tr> - - <tr> - <td>Subalbums</td> - <td><span class="attr_subalbums">{{numAlbums}}</span></td> - </tr> - - <tr> - <td>Pictures</td> - <td><span class="attr_pictures">{{numPictures}}</span></td> - </tr> - </tbody> - </table> </div> + {{/isSubalbum}} </div> - {{/isSubalbum}} </header> - <div class="content contentZoomIn"> - {{#showDivider}} + <main class="zoomIn"> + {{#hasSubalbums}} <div class="divider"> <h1>Albums</h1> </div> - {{/showDivider}} - - {{#subalbums}} - <a href="{{name}}/" class="album" style="width: {{thumbnail3w_css}}px; height: {{thumbnail3h_css}}px;"> - <img src="./{{thumbnail1}}" alt="Photo thumbnail" width="{{thumbnail1w}}" height="{{thumbnail1h}}"> - <img src="./{{thumbnail2}}" alt="Photo thumbnail" width="{{thumbnail2w}}" height="{{thumbnail2h}}"> - <img src="./{{thumbnail3}}" alt="Photo thumbnail" width="{{thumbnail3w}}" height="{{thumbnail3h}}"> - <span class="overlay" style="width: {{thumbnail1w_css}}px;"> - <h1>{{name}}</h1> - <p>{{numPictures}} Pictures - {{numAlbums}} Albums</p> - </span> - </a> - {{/subalbums}} + + <div id="albums"> + {{#subalbums}} + <a href="{{name}}/" class="album"> + <img src="{{thumbnail1}}" alt="Photo thumbnail"> + <img src="{{thumbnail2}}" alt="Photo thumbnail"> + <img src="{{thumbnail3}}" alt="Photo thumbnail"> + <span class="overlay"> + <h1>{{name}}</h1> + <p>{{numPictures}} Pictures - {{numAlbums}} Albums</p> + </span> + </a> + {{/subalbums}} + </div> - {{#showDivider}} <div class="divider"> <h1>Photos</h1> </div> - {{/showDivider}} - - {{#pictures}} - <a href="{{name}}.html" class="photo" style="width: {{width_css}}px; height: {{height_css}}px;"> - <img src="thumbnails/{{name}}.png" alt="Photo thumbnail" width="{{width}}" height="{{height}}"> - <span class="overlay" style="width: {{width_css}}px;"> - <h1>{{name}}</h1> - <!--<p><span title="Camera Date"><svg class="iconic "><use xlink:href="/iconic.svg#camera-slr"></use></svg></span></p>--> - </span> - </a> - {{/pictures}} + {{/hasSubalbums}} - </div> + <div id="photos" class="flex"> + {{#pictures}} + <a href="{{name}}.html" class="photo" data-width="{{width}}" data-height="{{height}}" data-name="{{name}}"> + <img src="thumbnails/small/{{name}}.jpg" alt="Photo thumbnail"> + <span class="overlay"> + <h1>{{name}}</h1> + <p><span title="Camera Date"><svg class="iconic "><use xlink:href="/iconic.svg#camera-slr"></use></svg>{{takedate}}</span></p> + </span> + </a> + {{/pictures}} + </div> + </main> {{#isSubalbum}} - {{#enableJS}} - <script type="text/javascript"> - window.onkeyup = function(e) { - if (e.keyCode == 27) window.location = ".."; - if (e.keyCode == 32) document.getElementById("toggle").checked = true; - } - </script> - {{/enableJS}} {{/isSubalbum}} + {{#enableJS}} + <script src="/albums.js"></script> + {{/enableJS}} </body> </html>
diff --git a/src/assets/albums.js b/src/assets/albums.js @@ -0,0 +1,55 @@ +const resizeHandler = () => { + const photosElement = document.getElementById('photos'); + const photos = document.querySelectorAll('.photo'); + const containerWidth = parseFloat(photosElement.getBoundingClientRect().width, 10); + let ratio = []; + + photos.forEach((element, index) => { + ratio[index] = element.dataset.height > 0 ? element.dataset.width / element.dataset.height : 1; + }); + + let layoutGeometry = require('justified-layout')(ratio, { + containerWidth: containerWidth, + containerPadding: 0, + targetRowHeight: 200 + }); + + photosElement.style.height = layoutGeometry.containerHeight + 'px'; + + photos.forEach((element, index) => { + element.style.top = layoutGeometry.boxes[index].top + 'px'; + element.style.width = layoutGeometry.boxes[index].width + 'px'; + element.style.height = layoutGeometry.boxes[index].height + 'px'; + element.style.left = layoutGeometry.boxes[index].left + 'px'; + element.children[1].style.width = layoutGeometry.boxes[index].width + 'px'; + }); +}; + +const keyHandler = (event) => { + if (event.ctrlKey === true || event.altKey === true) return; + switch (event.key) { + case "Escape": + document.getElementById("back").click(); + break; + + case " ": + event.preventDefault(); + event.stopPropagation(); + + const element = document.getElementById("toggle"); + element.checked = !element.checked; + + break; + }; +}; + +resizeHandler() +document.addEventListener('keydown', keyHandler); +document.addEventListener('DOMContentLoaded', () => { + const photosElement = document.getElementById("photos") + photosElement.classList.remove('flex'); + photosElement.classList.add('justified'); + + window.onresize = resizeHandler; + resizeHandler(); +});
diff --git a/src/assets/justified-layout.min.js b/src/assets/justified-layout.min.js @@ -0,0 +1,13 @@ +require=function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r}()({1:[function(require,module,exports){ +/*! + * Copyright 2019 SmugMug, Inc. + * Licensed under the terms of the MIT license. Please see LICENSE file in the project root for terms. + * @license + */ +var Row=module.exports=function(params){this.top=params.top;this.left=params.left;this.width=params.width;this.spacing=params.spacing;this.targetRowHeight=params.targetRowHeight;this.targetRowHeightTolerance=params.targetRowHeightTolerance;this.minAspectRatio=this.width/params.targetRowHeight*(1-params.targetRowHeightTolerance);this.maxAspectRatio=this.width/params.targetRowHeight*(1+params.targetRowHeightTolerance);this.edgeCaseMinRowHeight=params.edgeCaseMinRowHeight;this.edgeCaseMaxRowHeight=params.edgeCaseMaxRowHeight;this.widowLayoutStyle=params.widowLayoutStyle;this.isBreakoutRow=params.isBreakoutRow;this.items=[];this.height=0};Row.prototype={addItem:function(itemData){var newItems=this.items.concat(itemData),rowWidthWithoutSpacing=this.width-(newItems.length-1)*this.spacing,newAspectRatio=newItems.reduce(function(sum,item){return sum+item.aspectRatio},0),targetAspectRatio=rowWidthWithoutSpacing/this.targetRowHeight,previousRowWidthWithoutSpacing,previousAspectRatio,previousTargetAspectRatio;if(this.isBreakoutRow){if(this.items.length===0){if(itemData.aspectRatio>=1){this.items.push(itemData);this.completeLayout(rowWidthWithoutSpacing/itemData.aspectRatio,"justify");return true}}}if(newAspectRatio<this.minAspectRatio){this.items.push(Object.assign({},itemData));return true}else if(newAspectRatio>this.maxAspectRatio){if(this.items.length===0){this.items.push(Object.assign({},itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}previousRowWidthWithoutSpacing=this.width-(this.items.length-1)*this.spacing;previousAspectRatio=this.items.reduce(function(sum,item){return sum+item.aspectRatio},0);previousTargetAspectRatio=previousRowWidthWithoutSpacing/this.targetRowHeight;if(Math.abs(newAspectRatio-targetAspectRatio)>Math.abs(previousAspectRatio-previousTargetAspectRatio)){this.completeLayout(previousRowWidthWithoutSpacing/previousAspectRatio,"justify");return false}else{this.items.push(Object.assign({},itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}}else{this.items.push(Object.assign({},itemData));this.completeLayout(rowWidthWithoutSpacing/newAspectRatio,"justify");return true}},isLayoutComplete:function(){return this.height>0},completeLayout:function(newHeight,widowLayoutStyle){var itemWidthSum=this.left,rowWidthWithoutSpacing=this.width-(this.items.length-1)*this.spacing,clampedToNativeRatio,clampedHeight,errorWidthPerItem,roundedCumulativeErrors,singleItemGeometry,centerOffset;if(typeof widowLayoutStyle==="undefined"||["justify","center","left"].indexOf(widowLayoutStyle)<0){widowLayoutStyle="left"}clampedHeight=Math.max(this.edgeCaseMinRowHeight,Math.min(newHeight,this.edgeCaseMaxRowHeight));if(newHeight!==clampedHeight){this.height=clampedHeight;clampedToNativeRatio=rowWidthWithoutSpacing/clampedHeight/(rowWidthWithoutSpacing/newHeight)}else{this.height=newHeight;clampedToNativeRatio=1}this.items.forEach(function(item){item.top=this.top;item.width=item.aspectRatio*this.height*clampedToNativeRatio;item.height=this.height;item.left=itemWidthSum;itemWidthSum+=item.width+this.spacing},this);if(widowLayoutStyle==="justify"){itemWidthSum-=this.spacing+this.left;errorWidthPerItem=(itemWidthSum-this.width)/this.items.length;roundedCumulativeErrors=this.items.map(function(item,i){return Math.round((i+1)*errorWidthPerItem)});if(this.items.length===1){singleItemGeometry=this.items[0];singleItemGeometry.width-=Math.round(errorWidthPerItem)}else{this.items.forEach(function(item,i){if(i>0){item.left-=roundedCumulativeErrors[i-1];item.width-=roundedCumulativeErrors[i]-roundedCumulativeErrors[i-1]}else{item.width-=roundedCumulativeErrors[i]}})}}else if(widowLayoutStyle==="center"){centerOffset=(this.width-itemWidthSum)/2;this.items.forEach(function(item){item.left+=centerOffset+this.spacing},this)}},forceComplete:function(fitToWidth,rowHeight){if(typeof rowHeight==="number"){this.completeLayout(rowHeight,this.widowLayoutStyle)}else{this.completeLayout(this.targetRowHeight,this.widowLayoutStyle)}},getItems:function(){return this.items}}},{}],"justified-layout":[function(require,module,exports){ +/*! + * Copyright 2019 SmugMug, Inc. + * Licensed under the terms of the MIT license. Please see LICENSE file in the project root for terms. + * @license + */ +"use strict";var Row=require("./row");function createNewRow(layoutConfig,layoutData){var isBreakoutRow;if(layoutConfig.fullWidthBreakoutRowCadence!==false){if((layoutData._rows.length+1)%layoutConfig.fullWidthBreakoutRowCadence===0){isBreakoutRow=true}}return new Row({top:layoutData._containerHeight,left:layoutConfig.containerPadding.left,width:layoutConfig.containerWidth-layoutConfig.containerPadding.left-layoutConfig.containerPadding.right,spacing:layoutConfig.boxSpacing.horizontal,targetRowHeight:layoutConfig.targetRowHeight,targetRowHeightTolerance:layoutConfig.targetRowHeightTolerance,edgeCaseMinRowHeight:.5*layoutConfig.targetRowHeight,edgeCaseMaxRowHeight:2*layoutConfig.targetRowHeight,rightToLeft:false,isBreakoutRow:isBreakoutRow,widowLayoutStyle:layoutConfig.widowLayoutStyle})}function addRow(layoutConfig,layoutData,row){layoutData._rows.push(row);layoutData._layoutItems=layoutData._layoutItems.concat(row.getItems());layoutData._containerHeight+=row.height+layoutConfig.boxSpacing.vertical;return row.items}function computeLayout(layoutConfig,layoutData,itemLayoutData){var laidOutItems=[],itemAdded,currentRow,nextToLastRowHeight;if(layoutConfig.forceAspectRatio){itemLayoutData.forEach(function(itemData){itemData.forcedAspectRatio=true;itemData.aspectRatio=layoutConfig.forceAspectRatio})}itemLayoutData.some(function(itemData,i){if(isNaN(itemData.aspectRatio)){throw new Error("Item "+i+" has an invalid aspect ratio")}if(!currentRow){currentRow=createNewRow(layoutConfig,layoutData)}itemAdded=currentRow.addItem(itemData);if(currentRow.isLayoutComplete()){laidOutItems=laidOutItems.concat(addRow(layoutConfig,layoutData,currentRow));if(layoutData._rows.length>=layoutConfig.maxNumRows){currentRow=null;return true}currentRow=createNewRow(layoutConfig,layoutData);if(!itemAdded){itemAdded=currentRow.addItem(itemData);if(currentRow.isLayoutComplete()){laidOutItems=laidOutItems.concat(addRow(layoutConfig,layoutData,currentRow));if(layoutData._rows.length>=layoutConfig.maxNumRows){currentRow=null;return true}currentRow=createNewRow(layoutConfig,layoutData)}}}});if(currentRow&¤tRow.getItems().length&&layoutConfig.showWidows){if(layoutData._rows.length){if(layoutData._rows[layoutData._rows.length-1].isBreakoutRow){nextToLastRowHeight=layoutData._rows[layoutData._rows.length-1].targetRowHeight}else{nextToLastRowHeight=layoutData._rows[layoutData._rows.length-1].height}currentRow.forceComplete(false,nextToLastRowHeight)}else{currentRow.forceComplete(false)}laidOutItems=laidOutItems.concat(addRow(layoutConfig,layoutData,currentRow));layoutConfig._widowCount=currentRow.getItems().length}layoutData._containerHeight=layoutData._containerHeight-layoutConfig.boxSpacing.vertical;layoutData._containerHeight=layoutData._containerHeight+layoutConfig.containerPadding.bottom;return{containerHeight:layoutData._containerHeight,widowCount:layoutConfig._widowCount,boxes:layoutData._layoutItems}}module.exports=function(input,config){var layoutConfig={};var layoutData={};var defaults={containerWidth:1060,containerPadding:10,boxSpacing:10,targetRowHeight:320,targetRowHeightTolerance:.25,maxNumRows:Number.POSITIVE_INFINITY,forceAspectRatio:false,showWidows:true,fullWidthBreakoutRowCadence:false,widowLayoutStyle:"left"};var containerPadding={};var boxSpacing={};config=config||{};layoutConfig=Object.assign(defaults,config);containerPadding.top=!isNaN(parseFloat(layoutConfig.containerPadding.top))?layoutConfig.containerPadding.top:layoutConfig.containerPadding;containerPadding.right=!isNaN(parseFloat(layoutConfig.containerPadding.right))?layoutConfig.containerPadding.right:layoutConfig.containerPadding;containerPadding.bottom=!isNaN(parseFloat(layoutConfig.containerPadding.bottom))?layoutConfig.containerPadding.bottom:layoutConfig.containerPadding;containerPadding.left=!isNaN(parseFloat(layoutConfig.containerPadding.left))?layoutConfig.containerPadding.left:layoutConfig.containerPadding;boxSpacing.horizontal=!isNaN(parseFloat(layoutConfig.boxSpacing.horizontal))?layoutConfig.boxSpacing.horizontal:layoutConfig.boxSpacing;boxSpacing.vertical=!isNaN(parseFloat(layoutConfig.boxSpacing.vertical))?layoutConfig.boxSpacing.vertical:layoutConfig.boxSpacing;layoutConfig.containerPadding=containerPadding;layoutConfig.boxSpacing=boxSpacing;layoutData._layoutItems=[];layoutData._awakeItems=[];layoutData._inViewportItems=[];layoutData._leadingOrphans=[];layoutData._trailingOrphans=[];layoutData._containerHeight=layoutConfig.containerPadding.top;layoutData._rows=[];layoutData._orphans=[];layoutConfig._widowCount=0;return computeLayout(layoutConfig,layoutData,input.map(function(item){if(item.width&&item.height){return{aspectRatio:item.width/item.height}}else{return{aspectRatio:item}}}))}},{"./row":1}]},{},[]);+ \ No newline at end of file
diff --git a/src/assets/picture.html b/src/assets/picture.html @@ -1,7 +1,7 @@ <!DOCTYPE HTML> -<html> +<html lang="en"> <head> - <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> + <meta charset="utf-8"> <title>{{name}} | {{siteName}}</title> <meta name="description" content="{{siteDescription}}"> @@ -19,132 +19,133 @@ <link rel="shortcut icon" href="favicon.ico"> </head> <body> - <header class="header header-view" style="display: flex;"> - <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> - - {{#showOriginalsButton}} - <a class="button" href="{{orig}}" title="Download"><svg class="iconic"><use xlink:href="/iconic.svg#cloud-download"></use></svg></a> - {{/showOriginalsButton}} - - <a class="header-divider"></a> - - <input type="checkbox" id="toggle"> - <label for="toggle" style="display: block;" class="button button--info" title="Info"> - <svg class="iconic"><use xlink:href="/iconic.svg#info"></use></svg> - </label> - - <div class="sidebar"> - <div class="sidebar-header"> - <h1>About</h1> - </div> - - <div class="sidebar-wrapper"> - <div class="sidebar-divider"> - <h1>Basics</h1> + <header class="transparent"> + <div class="flex"> + <a class="button" href="./" title="Close Photo"><svg class="iconic"><use xlink:href="/iconic.svg#chevron-left"></use></svg></a> + + <a class="title">{{name}}</a> + + {{#showOriginalsButton}} + <a class="button" href="{{orig}}" title="Download"><svg class="iconic"><use xlink:href="/iconic.svg#cloud-download"></use></svg></a> + {{/showOriginalsButton}} + + <input type="checkbox" id="toggle"> + <label for="toggle" style="display: block;" class="button button-info" title="Info"> + <svg class="iconic"><use xlink:href="/iconic.svg#info"></use></svg> + </label> + + <div class="sidebar"> + <div class="sidebar-header"> + <h1>About</h1> </div> - <table> - <tbody> - <tr> - <td>Title</td> - <td><span id="attr_title">{{name}}</span></td> - </tr> + <div class="sidebar-wrapper"> + <div class="sidebar-divider"> + <h1>Basics</h1> + </div> + + <table> + <tbody> + <tr> + <td>Title</td> + <td>{{name}}</td> + </tr> + + <tr> + <td>Description</td> + <td>{{description}}</td> + </tr> + + </tbody> + </table> - <tr> - <td>Description</td> - <td><span id="attr_description">{{description}}</span></td> - </tr> - - </tbody> - </table> - - <div class="sidebar-divider"> - <h1>Image</h1> - </div> - - <table> - <tbody> - <tr> - <td>Size</td> - <td><span id="attr_size">{{size}}MB</span></td> - </tr> - - <tr> - <td>Resolution</td> - <td><span id="attr_resolution" id="resolution">-</span></td> - </tr> - - </tbody> - </table> - - <div class="sidebar-divider"> - <h1>Camera</h1> - </div> + <div class="sidebar-divider"> + <h1>Image</h1> + </div> - <table> - <tbody> - <tr> - <td>Captured</td> - <td><span id="attr_captured">-</span></td> - </tr> - - <tr> - <td>Make</td> - <td><span id="attr_make">-</span></td> - </tr> - - <tr> - <td>Type/Model</td> - <td><span id="attr_type/model">-</span></td> - </tr> - - <tr> - <td>Shutter Speed</td> - <td><span id="attr_shutter-speed">-</span></td> - </tr> - - <tr> - <td>Shutter Program</td> - <td><span id="attr_shutter-program">-</span></td> - </tr> - - <tr> - <td>Aperture</td> - <td><span id="attr_aperture">-</span></td> - </tr> - - <tr> - <td>Focal Length</td> - <td><span id="attr_focal-length">-</span></td> - </tr> - - <tr> - <td>ISO</td> - <td><span id="attr_iso">-</span></td> - </tr> - - <tr> - <td>Flash</td> - <td><span id="attr_flash">-</span></td> - </tr> - </tbody> - </table> - {{^enableJS}} - <p> - Exif parsing is disabled by owner of this site. - </p> - {{/enableJS}} + <table> + <tbody> + <tr> + <td>Size</td> + <td>{{filesize}} MiB</td> + </tr> + + <tr> + <td>Resolution</td> + <td>{{width}} x {{height}}</td> + </tr> + + </tbody> + </table> + + <div class="sidebar-divider"> + <h1>Camera</h1> + </div> + + <table> + <tbody> + <tr> + <td>Captured</td> + <td>{{takestamp}}</td> + </tr> + + <tr> + <td>Make</td> + <td>{{make}}</td> + </tr> + + <tr> + <td>Type/Model</td> + <td>{{model}}</td> + </tr> + + <tr> + <td>Lens Model</td> + <td>{{lensModel}}</td> + </tr> + + <tr> + <td>Shutter Speed</td> + <td>{{shutterSpeed}}</td> + </tr> + + <tr> + <td>Shutter Program</td> + <td>{{shutterProgram}}</td> + </tr> + + <tr> + <td>Aperture</td> + <td>{{aperture}}</td> + </tr> + + <tr> + <td>Focal Length</td> + <td>{{focalLength}}{{#focalLength35mm}} (<i>f</i><sub>35</sub> = {{focalLength35mm}}){{/focalLength35mm}}</td> + </tr> + + <tr> + <td>ISO</td> + <td>{{iso}}</td> + </tr> + + <tr> + <td>Flash</td> + <td>{{flash}}</td> + </tr> + </tbody> + </table> + </div> </div> </div> </header> <div id="imageview" class="fadeIn full" style="display: block;"> - <img id="image" class="" src="medium/{{filename}}"> + <img id="image" class="" src="thumbnails/medium/{{filename}}"> {{#hasPrev}} <div class="arrow_wrapper arrow_wrapper--previous"> - <a id="previous" href="{{prev_name}}.html" style="background-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url("thumbnails/{{prev_name}}.png");"> + <a id="previous" href="{{prev_name}}.html" style="background-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url("thumbnails/small/{{prev_name}}.jpg");"> <svg class="iconic "><use xlink:href="/iconic.svg#caret-left"></use></svg> </a> </div> @@ -152,62 +153,35 @@ {{#hasNext}} <div class="arrow_wrapper arrow_wrapper--next"> - <a id="next" href="{{next_name}}.html" style="background-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url("thumbnails/{{next_name}}.png");"> + <a id="next" href="{{next_name}}.html" style="background-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url("thumbnails/small/{{next_name}}.jpg");"> <svg class="iconic "><use xlink:href="/iconic.svg#caret-right"></use></svg> </a> </div> {{/hasNext}} </div> {{#enableJS}} - <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 = "./"; - - {{#hasNext}} - if (e.keyCode == 39) window.location = "./{{next_name}}.html"; - {{/hasNext}} - - {{^hasNext}} - if (e.keyCode == 39) window.location = "./"; - {{/hasNext}} - - {{#hasPrev}} - if (e.keyCode == 37) window.location = "./{{prev_name}}.html"; - {{/hasPrev}} - - {{^hasPrev}} - if (e.keyCode == 37) window.location = "./"; - {{/hasPrev}} - - if (e.keyCode == 32) document.getElementById("toggle").checked = true; - } - - function formatShutterSpeed (d) { - if (d >= 1) { - return Math.round(d*10)/10 + 's'; - } - return '1/' + Math.round(1/d) + 's'; - } - - 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 = formatShutterSpeed(EXIF.getTag(this, "ExposureTime")); - 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"); - }); - } + document.addEventListener('keyup', (event) => { + if (event.ctrlKey === true || event.altKey === true) return; + switch (event.key) { + case "Escape": + window.location = "./"; + break; + + case "ArrowLeft": + document.getElementById("previous").click(); + break; + + case "ArrowRight": + document.getElementById("next").click(); + break; + + case " ": + const element = document.getElementById("toggle"); + element.checked = !element.checked; + break; + }; + }); </script> {{/enableJS}} </body>
diff --git a/src/assets/style.css b/src/assets/style.css @@ -1,4 +1,4 @@ -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { +a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, html, iframe, img, ins, kbd, label, legend, li, mark, menu, nav, object, ol, output, p, pre, q, ruby, s, samp, section, small, span, strike, strong, summary, sup, table, tbody, td, tfoot, th, thead, time, tr, tt, u, ul, var, video { margin: 0; padding: 0; border: 0; @@ -19,21 +19,6 @@ ol, ul { list-style: none; } -blockquote, q { - quotes: none; -} - -blockquote:before, blockquote:after, q:before, q:after { - content: ''; - content: none; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - - * { -webkit-user-select: none; -moz-user-select: none; @@ -56,16 +41,6 @@ body { font-smoothing: antialiased; } -body.view { - background-color: #0f0f0f; -} - -input { - -webkit-user-select: text !important; - -moz-user-select: text !important; - user-select: text !important; -} - .iconic { width: 100%; height: 100%; @@ -160,119 +135,126 @@ input { * *****/ -.content { - display: flex; - flex-wrap: wrap; - padding: 0 0 33px; - width: 100%; - min-height: calc(100% - 83px); - -webkit-overflow-scrolling: touch; +main { + padding: 50px 30px 33px 0; + width: calc(100% - 30px); + transition: margin-left .5s; + max-width: calc(100vw - 10px); } -.content::before { - content: ''; - position: absolute; - left: 0; - width: 100%; - height: 1px; - background: rgba(255, 255, 255, 0.02); +main .flex { + display: flex; + flex-wrap: wrap; } -.content-sidebar { - width: calc(100% - 300px); +main .justified { + position: relative; + display: block; } -.content.contentZoomIn .album, .content.contentZoomIn .photo { - animation-name: zoomIn; + +main .justified > .photo { + position: absolute; + margin: 0; } -.content.contentZoomIn .divider { - animation-name: fadeIn; +main .justified > .photo img { + width: 100%; + height: 100%; + border: 0; } -.content.contentZoomOut .album, .content.contentZoomOut .photo { - animation-name: zoomOut; +.justified > .photo .overlay { + bottom: 0; + margin: 0; } -.content.contentZoomOut .divider { - animation-name: fadeOut; +main #photos, +main #albums { + margin: 30px 0 0 30px; } -.content .album, -.content .photo { +.album, +.photo { display: block; position: relative; width: 202px; height: 202px; - margin: 30px 0 0 30px; + margin: 10px 0 0 10px; cursor: default; + animation-name: zoomIn; animation-duration: .2s; animation-fill-mode: forwards; animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); cursor: pointer; } -.content .album img, -.content .photo img { +.album img, +.photo img { position: absolute; background: #222; color: #222; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); - border: 1px solid rgba(255, 255, 255, 0.5); transition: opacity .3s ease-out, transform .3s ease-out, border-color .3s ease-out; + width: 200px; + height: 200px; + border: 1px solid rgba(255,255,255,.5); + object-fit: cover; } -.content .album:hover img, .content .album.active img, -.content .photo:hover img, -.content .photo.active img { +.album:hover img, +.album.active img, +.photo:hover img, +.photo.active img { border-color: #2293EC; } -.content .album:active img, -.content .photo:active img { +.album:active img, +.photo:active img { transition: none; border-color: #0f6ab2; -} +} -.content .album img:first-child, -.content .album img:nth-child(2) { +.album img:first-child, +.album img:nth-child(2) { transform: rotate(0) translateY(0) translateX(0); opacity: 0; } -.content .album:hover img:nth-child(1), .content .album:hover img:nth-child(2) { +.album:hover img:nth-child(1), +.album:hover img:nth-child(2) { opacity: 1; will-change: transform; } -.content .album:hover img:nth-child(1) { +.album:hover img:nth-child(1) { transform: rotate(-2deg) translateY(10px) translateX(-12px); } -.content .album:hover img:nth-child(2) { +.album:hover img:nth-child(2) { transform: rotate(5deg) translateY(-8px) translateX(12px); } -.content .album .overlay, -.content .photo .overlay { +.album .overlay, +.photo .overlay { position: absolute; - margin: 0 1px; width: 200px; background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.6)); bottom: 1px; + margin: 0 1px; } -.content .photo .overlay { +.photo .overlay { opacity: 0; } -.content .photo:hover .overlay, -.content .photo.active .overlay { +.photo:hover .overlay, +.photo.active .overlay { opacity: 1; } -.content .album .overlay h1, -.content .photo .overlay h1 { +.album .overlay h1, +.photo .overlay h1 { min-height: 19px; width: 180px; margin: 12px 0 5px 15px; @@ -285,8 +267,8 @@ input { text-overflow: ellipsis; } -.content .album .overlay p, -.content .photo .overlay p { +.album .overlay p, +.photo .overlay p { display: block; margin: 0 0 12px 15px; font-size: 11px; @@ -294,14 +276,14 @@ input { text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); } -.content .photo .overlay p .iconic { +.photo .overlay p .iconic { fill: #ccc; margin: 0 5px 0 0; width: 8px; height: 8px; } -.content .divider { +main .divider { margin: 50px 0 0; padding: 10px 0 0; width: 100%; @@ -313,320 +295,116 @@ input { animation-timing-function: cubic-bezier(0.51, 0.92, 0.24, 1); } -.content .divider:first-child { +main .divider:first-child { margin-top: 10px; border-top: 0; box-shadow: none; } -.content .divider h1 { +main .divider h1 { margin: 0 0 0 30px; color: rgba(255, 255, 255, 0.6); font-size: 14px; font-weight: bold; } -.no_content { - position: absolute; - top: 50%; - left: 50%; - padding-top: 20px; - color: rgba(255, 255, 255, 0.35); - text-align: center; - transform: translateX(-50%) translateY(-50%); -} - -.no_content .iconic { - fill: rgba(255, 255, 255, 0.3); - margin: 0 0 10px; - width: 50px; - height: 50px; -} - -.no_content p { - font-size: 16px; - font-weight: bold; -} - - /***** * * Header * *****/ -.header { - position: fixed; - height: 49px; - width: 100%; - background: linear-gradient(to bottom, #222222, #1a1a1a); - border-bottom: 1px solid #0f0f0f; - z-index: 1; - transition: transform .3s ease-out; - display: flex; - align-items: center; - position: relative; - box-sizing: border-box; - padding: 0 10px; -} - -.header-hidden { - transform: translateY(-60px); -} - -.header-view { - background: none; - border-bottom: none; -} - -.header-title { - width: 100%; - padding: 16px 0; - color: #fff; - font-size: 16px; - font-weight: bold; - text-align: center; - cursor: default; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +header { + position:fixed; + height:49px; + width:100%; + z-index:1; + transition:transform .3s ease-out; + background:linear-gradient(to bottom,#222,#1a1a1a); + border-bottom:1px solid #0f0f0f; } -.header-title .iconic { - display: none; - margin: 0 0 0 5px; - width: 10px; - height: 10px; - fill: rgba(255, 255, 255, 0.5); - transition: fill .2s ease-out; -} -.header-title:hover .iconic { - fill: white; -} -.header-title:active .iconic { - transition: none; - fill: rgba(255, 255, 255, 0.8); +.transparent { + background: none; + border-bottom: none; } -.header .button { - flex-shrink: 0; - padding: 16px 8px; - height: 15px; - cursor: pointer; -} -.header .button .iconic { - width: 15px; - height: 15px; - fill: rgba(255, 255, 255, 0.5); - transition: fill .2s ease-out; +header .flex { + -webkit-box-align:center; + -ms-flex-align:center; + align-items:center; + position:relative; + -webkit-box-sizing:border-box; + box-sizing:border-box; + width:100%; + height:100%; + padding:0 10px; + display:-webkit-box; + display:-ms-flexbox; + display:flex } -.header .button:hover .iconic { - fill: white; +header .title{ + width:100%; + padding:16px 0; + color:#fff; + font-size:16px; + font-weight:700; + text-align:center; + cursor:default; + overflow:hidden; + white-space:nowrap; + -o-text-overflow:ellipsis; + text-overflow:ellipsis; + -webkit-transition:margin-left .5s; + -o-transition:margin-left .5s; + transition:margin-left .5s } -.header .button:active .iconic { - transition: none; - fill: rgba(255, 255, 255, 0.8); +header .title .iconic{ + display:none; + margin:0 0 0 5px; + width:10px; + height:10px; + fill:rgba(255,255,255,.5); + -webkit-transition:fill .2s ease-out; + -o-transition:fill .2s ease-out; + transition:fill .2s ease-out } -.header .button-eye.active .iconic { - fill: #ff9737; +header .title:hover .iconic{ + fill:#fff } -.header .button-info.active .iconic { - fill: #2293EC; +header .button{ + -ms-flex-negative:0; + flex-shrink:0; + padding:16px 8px; + height:15px } -.header-divider { - flex-shrink: 0; - width: 14px; +header .button .iconic{ + width:15px; + height:15px; + fill:rgba(255,255,255,.5); + -webkit-transition:fill .2s ease-out; + -o-transition:fill .2s ease-out; + transition:fill .2s ease-out } -.header-search { - flex-shrink: 0; - width: 80px; - margin: 0; - padding: 5px 12px 6px 12px; - background-color: #1d1d1d; - color: #fff; - border: 1px solid rgba(0, 0, 0, 0.9); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04); - outline: none; - border-radius: 50px; - opacity: .6; - transition: opacity .3s ease-out, box-shadow .3s ease-out, width .2s ease-out; +header .button:hover .iconic{ + fill:#fff } -.header-search:focus { - width: 140px; - border-color: #2293EC; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0); - opacity: 1; +header .button:active .iconic{ + -webkit-transition:none; + -o-transition:none; + transition:none; + fill:rgba(255,255,255,.8) } -.header-search:focus ~ #clearSearch { - opacity: 1; -} - -.header-search::-ms-clear { - display: none; -} - -.header-clear { - position: absolute; - top: 13px; - right: 60px; - padding: 0; - color: rgba(255, 255, 255, 0.5); - font-size: 20px; - opacity: 0; - transition: color .2s ease-out; - cursor: default; -} - -.header-clear:hover { - color: white; -} - - -/***** - * - * Header - * - *****/ - -.header { - position: fixed; - height: 49px; - width: 100%; - background: linear-gradient(to bottom, #222222, #1a1a1a); - border-bottom: 1px solid #0f0f0f; - z-index: 1; - transition: transform .3s ease-out; - display: flex; - align-items: center; - position: relative; - box-sizing: border-box; - padding: 0 10px; -} - -.header-hidden { - transform: translateY(-60px); -} - -.header-view { - background: none; - border-bottom: none; -} - -.header-title { - width: 100%; - padding: 16px 0; - color: #fff; - font-size: 16px; - font-weight: bold; - text-align: center; - cursor: default; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.header-title .iconic { - display: none; - margin: 0 0 0 5px; - width: 10px; - height: 10px; - fill: rgba(255, 255, 255, 0.5); - transition: fill .2s ease-out; -} -.header-title:hover .iconic { - fill: white; -} -.header-title:active .iconic { - transition: none; - fill: rgba(255, 255, 255, 0.8); -} - -.header .button { - flex-shrink: 0; - padding: 16px 8px; - height: 15px; -} -.header .button .iconic { - width: 15px; - height: 15px; - fill: rgba(255, 255, 255, 0.5); - transition: fill .2s ease-out; -} - -.header .button:hover .iconic { - fill: white; -} - -.header .button:active .iconic { - transition: none; - fill: rgba(255, 255, 255, 0.8); -} - -.header .button-eye.active .iconic { - fill: #ff9737; -} - -.header .button-info.active .iconic { - fill: #2293EC; -} - -.header-divider { - flex-shrink: 0; - width: 14px; -} - -.header-search { - flex-shrink: 0; - width: 80px; - margin: 0; - padding: 5px 12px 6px 12px; - background-color: #1d1d1d; - color: #fff; - border: 1px solid rgba(0, 0, 0, 0.9); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04); - outline: none; - border-radius: 50px; - opacity: .6; - transition: opacity .3s ease-out, box-shadow .3s ease-out, width .2s ease-out; -} - -.header-search:focus { - width: 140px; - border-color: #2293EC; - box-shadow: 0 1px 0 rgba(255, 255, 255, 0); - opacity: 1; -} - -.header-search:focus ~ #clearSearch { - opacity: 1; -} - -.header-search::-ms-clear { - display: none; -} - -.header-clear { - position: absolute; - top: 13px; - right: 60px; - padding: 0; - color: rgba(255, 255, 255, 0.5); - font-size: 20px; - opacity: 0; - transition: color .2s ease-out; - cursor: default; -} - -.header-clear:hover { - color: white; +input[type=checkbox]:checked + .button-info .iconic { + fill:#2293ec } @@ -739,7 +517,7 @@ input[type=checkbox]:checked ~ .sidebar { * *****/ - #imageview { +#imageview { position: fixed; display: none; top: 0; @@ -849,4 +627,4 @@ input[type=checkbox]:checked ~ .sidebar { max-width: calc(100% - 40px); max-height: calc(100% - 80px); } -}- \ No newline at end of file +}
diff --git a/src/gallery.nim b/src/gallery.nim @@ -1,5 +1,6 @@ -import os, osproc, options, json, strutils, random, algorithm, parsecfg +import os, osproc, options, json, strutils, random, algorithm, parsecfg, tables, math import moustachu +import nimjpg type Config* = object @@ -29,6 +30,8 @@ type Picture* = object name*: string path*: string + desc*: Option[string] + exif*: Table[string, string] width*: int height*: int thumbWidth*: int @@ -36,14 +39,15 @@ type filename*: string filetype*: string filesize*: BiggestInt - desc*: Option[string] -const asset_exif_js = staticRead"./assets/exif.js" -const asset_style_css = staticRead"./assets/style.css" -const asset_noimages_svg = staticRead"./assets/no_images.svg" -const asset_iconic_svg = staticRead"./assets/iconic.svg" -const asset_album_html = staticRead"./assets/album.html" -const asset_picture_html = staticRead"./assets/picture.html" +const asset_exif_js = staticRead"./assets/exif.js" +const asset_justified_layout_js = staticRead"./assets/justified-layout.min.js" +const asset_albums_js = staticRead"./assets/albums.js" +const asset_style_css = staticRead"./assets/style.css" +const asset_noimages_svg = staticRead"./assets/no_images.svg" +const asset_iconic_svg = staticRead"./assets/iconic.svg" +const asset_album_html = staticRead"./assets/album.html" +const asset_picture_html = staticRead"./assets/picture.html" var config* {.threadvar.}: Config ### @@ -69,6 +73,13 @@ proc sortPictures(x, y: Picture): int = elif x.name == y.name: 0 else: 1 +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)) @@ -80,14 +91,12 @@ proc createPicture(path: string): Picture = if fileExists(joinPath(dir, name, ".txt")): result.desc = some(readFile(joinPath(dir, name,".txt"))) - let size = execProcess("/usr/bin/identify", args=[ - "-ping", - "-format", "'%w %h'", - quoteShell(path) - ], options={poUsePath}).replace("\n", "").replace("'", "").split(" ") + let file = open(path) + let jpgMetadata = collect_jpg(file) - result.width = parseInt(size[0]) - result.height = parseInt(size[1]) + result.exif = jpgMetadata.exifData.get + result.width = int(jpgMetadata.sofData.get.width) + result.height = int(jpgMetadata.sofData.get.height) if config.thumbSmallWidth == 0: result.thumbWidth = toInt(toFloat(result.width) / (result.height / config.thumbSmallHeight)) @@ -138,6 +147,8 @@ proc placeAssets(targetDir: string, enableJS: bool) = 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) + if enableJS: writeFile(joinPath(targetDir, "justified-layout.min.js"), asset_justified_layout_js) + if enableJS: writeFile(joinPath(targetDir, "albums.js"), asset_albums_js) proc removeOrphans (targetDir: string) = @@ -161,15 +172,15 @@ proc removeOrphans (targetDir: string) = removeDir(joinPath(targetDir, dirname)) #Photos - for photo in walkDir(joinPath(targetDir, "medium")): + 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, "medium", filename)) - removeFile(joinPath(targetDir, "thumbnails", name & ".png")) + removeFile(joinPath(targetDir, "thumbnails/small", name & ".jpg")) + removeFile(joinPath(targetDir, "thumbnails/medium", filename)) proc generateWebsite(targetDir: string, album: Album) = @@ -177,7 +188,8 @@ proc generateWebsite(targetDir: string, album: Album) = echo "Create Album:" & album.name discard existsOrCreateDir(targetDir) discard existsOrCreateDir(joinPath(targetDir, "thumbnails")) - discard existsOrCreateDir(joinPath(targetDir, "medium")) + discard existsOrCreateDir(joinPath(targetDir, "thumbnails/small")) + discard existsOrCreateDir(joinPath(targetDir, "thumbnails/medium")) var templateContext = mergeJson(%* { "name": album.name, @@ -185,88 +197,69 @@ proc generateWebsite(targetDir: string, album: Album) = "numAlbums": album.subalbums.len, "numPictures": album.pictures.len, "isSubalbum": true, - "showDivider": false, + "hasSubalbums": false, "subalbums": [], "pictures": [] }, %config) var smallThumbnails = newSeq[string]() var mediumThumbnails = newSeq[string]() + var bigThumbnails = newSeq[string]() - if album.path == "": templateContext["isSubalbum"] = %false - if album.subalbums.len > 0 and album.pictures.len > 0: templateContext["showDivider"] = %true - if not album.desc.isNone: templateContext["description"] = %album.desc.get + if album.path == "": templateContext["isSubalbum"] = %false + if album.subalbums.len != 0: templateContext["hasSubalbums"] = %true + if not album.desc.isNone: templateContext["description"] = %album.desc.get for subalbum in album.subalbums: generateWebsite(joinPath(targetDir, subalbum.name), subalbum) var thumbnail1 = "/no_images.svg" - var thumbnail1w = 200 - var thumbnail1h = 200 - var thumbnail2 = "/no_images.svg" - var thumbnail2w = 200 - var thumbnail2h = 200 - var thumbnail3 = "/no_images.svg" - var thumbnail3w = 200 - var thumbnail3h = 200 - var thumbnail3w_css = 202 - var thumbnail3h_css = 202 if subalbum.pictures.len > 0: - let pic1 = rand(0..subalbum.pictures.len-1) - let pic2 = rand(0..subalbum.pictures.len-1) - let pic3 = rand(0..subalbum.pictures.len-1) - - thumbnail1 = subalbum.name & "/thumbnails/" & subalbum.pictures[pic1].name & ".png" - thumbnail1w = subalbum.pictures[pic1].thumbWidth - thumbnail1h = subalbum.pictures[pic1].thumbHeight - - thumbnail2 = subalbum.name & "/thumbnails/" & subalbum.pictures[pic2].name & ".png" - thumbnail2w = subalbum.pictures[pic2].thumbWidth - thumbnail2h = subalbum.pictures[pic2].thumbHeight - - thumbnail3 = subalbum.name & "/thumbnails/" & subalbum.pictures[pic3].name & ".png" - thumbnail3w = subalbum.pictures[pic3].thumbWidth - thumbnail3h = subalbum.pictures[pic3].thumbHeight - thumbnail3w_css = subalbum.pictures[pic3].thumbWidth + 2 - thumbnail3h_css = subalbum.pictures[pic3].thumbHeight + 2 + 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, - "thumbnail1w": thumbnail1w, - "thumbnail1h": thumbnail1h, - "thumbnail2": thumbnail2, - "thumbnail2w": thumbnail2w, - "thumbnail2h": thumbnail2h, - - "thumbnail3": thumbnail3, - "thumbnail3w": thumbnail3w, - "thumbnail3h": thumbnail3h, - "thumbnail3w_css": thumbnail3w_css, - "thumbnail3h_css": thumbnail3h_css + "thumbnail3": thumbnail3 }) for index, picture in album.pictures: - var width, height: int + let takestamp = picture.exif.getOrDefault("DateTimeOriginal", "").split(" ") 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, - "description": "-", - "hasPrev": false, - "hasNext": false, - "filesize": (picture.filesize.int/1000/1000) + "name": picture.name, + "orig": joinPath("/originals", picture.path.replace(config.sourceDir, ""), picture.filename), + "filename": picture.filename, + "width": picture.width, + "height": picture.thumbHeight, + "thumbWidth": picture.thumbWidth, + "thumbHeight": picture.height, + "takestamp": takestamp[0].replace(":", "-") & " " & takestamp[1], + "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 picture.exif.hasKey("FocalLengthIn35mmFilm"): + pictureTemplateContext["focalLength35mm"] = %(picture.exif["FocalLengthIn35mmFilm"]) + if not picture.desc.isNone: pictureTemplateContext["description"] = %picture.desc.get if index > 0: @@ -281,54 +274,32 @@ proc generateWebsite(targetDir: string, album: Album) = echo "Generate picture page: " & picture.name writeFile(joinPath(targetDir, picture.name & ".html"), render(asset_picture_html, pictureTemplateContext)) - if not fileExists(joinPath(targetDir, "thumbnails", picture.name & ".png")): - if config.thumbSmallWidth == 0: - smallThumbnails.add(["/usr/bin/env mogrify", - "-quality", $config.thumbSmallQuality, - "-format", "png", - "-path", quoteShell(joinPath(targetDir, "thumbnails")), - "-thumbnail", "x" & $config.thumbSmallHeight, - quoteShell(joinPath(picture.path, picture.filename)) - ].join(" ")) - - elif config.thumbSmallHeight == 0: - smallThumbnails.add(["/usr/bin/env mogrify", - "-strip", - "-quality", $config.thumbSmallQuality, - "-format", "png", - "-path", quoteShell(joinPath(targetDir, "thumbnails")), - "-thumbnail", $config.thumbSmallHeight & "x", - quoteShell(joinPath(picture.path, picture.filename)) - ].join(" ")) - - else: - if not fileExists(joinPath(targetDir, "thumbnails", picture.name & ".png")): - smallThumbnails.add(["/usr/bin/env mogrify", - "-quality", $config.thumbSmallQuality, - "-format", "png", - "-path", quoteShell(joinPath(targetDir, "thumbnails")), - "-thumbnail", $config.thumbSmallWidth & "x" & $config.thumbSmallHeight & "^", - "-gravity", "center", - "-extent", $config.thumbSmallWidth & "x" & $config.thumbSmallHeight, - quoteShell(joinPath(picture.path, picture.filename)) - ].join(" ")) - - if not fileExists(joinPath(targetDir, "medium", picture.filename)): - echo "Generate medium thumbnail!" - discard execProcess("/usr/bin/mogrify", args=[ - "-format", picture.filetype, - "-path", quoteShell(joinPath(targetDir, "medium")), - "-resize", $config.thumbMediumWidth & "x>", + if not fileExists(joinPath(targetDir, "thumbnails/small", picture.name & ".jpg")): + smallThumbnails.add(["/usr/bin/env mogrify", + "-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(["/usr/bin/env mogrify", + "-strip", + "-interlace", "plane", + "-format", $picture.format, + "-path", quoteShell(joinPath(targetDir, "thumbnails/medium")), + "-resize", $config.thumbMediumWidth & "x\\>", quoteShell(joinPath(picture.path, picture.filename)) - ], options={poUsePath}) + ].join(" ")) templateContext["pictures"].add(%* { "name": picture.name, "width": picture.thumbWidth, "height": picture.thumbHeight, - "width_css": picture.thumbWidth+2, - "height_css": picture.thumbHeight+2 + "takedate": takestamp[0].replace(":", "-") }) echo "Generate small thumbnails!" @@ -345,6 +316,7 @@ proc generateWebsite(targetDir: string, album: Album) = proc main = randomize() + init_jpg() 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!" @@ -444,6 +416,8 @@ proc main = createSymlink(config.sourceDir, joinPath(config.targetDir, "originals")) placeAssets(config.targetDir, config.enableJS) + + echo "Collect files and Exif metadata..." generateWebsite(config.targetDir, mainAlbum)