ctucx.git: gallery

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

commit 52b8eb22f222d1ce98804ec92815659b067ec96b
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
src/assets/album.html
|
191
++++++++++++++++++++++++++++++++++++++++---------------------------------------
A
src/assets/albums.js
|
55
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
src/assets/justified-layout.min.js
|
14
++++++++++++++
M
src/assets/picture.html
|
304
++++++++++++++++++++++++++++++++++++-------------------------------------------
M
src/assets/style.css
|
495
++++++++++++++++++++++---------------------------------------------------------
M
src/gallery.nim
|
200
++++++++++++++++++++++++++++++++++---------------------------------------------
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&&currentRow.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(&quot;thumbnails/{{prev_name}}.png&quot;);">
+				<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(&quot;thumbnails/small/{{prev_name}}.jpg&quot;);">
 					<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(&quot;thumbnails/{{next_name}}.png&quot;);">
+				<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(&quot;thumbnails/small/{{next_name}}.jpg&quot;);">
 					<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)