6KFVCGKKXDPZMNUP6ZN6M7MJQ25PDER42VV5FAY45OHJZWO4VEMQC
G475G2KUDACOYKXN4FSMA7LQBMVIV6QEPYYAGBXQGWLYX3YJDZTAC
QX7XVNQYOTBDULUCLFRGSATHL7KWBSEV6QWNFY353S7ZIR6UGJWQC
L3PX32UXE2SC4JK3VXLL654DHBZGVXSBLZRQIGUIB4RIXEVHU5IQC
ZRQFW3UNHIZLLXHXHK4EHVK74IN7RRGASB4WKYJAS2FJA5EHA2XAC
TOLGMXUXRC74FQHKSAFLRNEG44GPLBMORKRDPU35EB2F424AN5QAC
HOTFGCPMWQATBEXUZXAAW7TILKKQJGJMLAF72URDK4H632C3JXEQC
MCPXLE6LIQRLQA2OBQSVIH4O3HAEZZWKPQMEDERPBHN6IVOAVXVAC
6Q2LBYYTS5Z67TKMOMJRBRST5UIOW3CGRSDGCDQBO3F3DNNPVGIQC
7CRKZALXJV7SBMSXU44VATIRC6XBWNRVDZISX3XKGJAZNXUZOBKAC
6WPF42RDWKN2YKPNQNRFMKIO4TJBQCUKRPLVY4V6ULN6KXPBQOUQC
PJQXKAUOGPICDWXZAZN2KANNYXTUM7YTQ7G35NDZIEZQBTZYC3ZQC
UW4WSPWD7TVQWURMTLRNW6LKVFLUVQT7NBUP5A4WMAIVHSLUPH7AC
A4YDN3ZM5CNM43BMK4BAEB6I2GVQQZUKBCZYMYCRVZSS6EXRY3VAC
R4THF7FOB3LE7JE7DMA5UL6PJIYZMNUWH6TLUKDHNB2X47V4MAEQC
TYCBCSGHUAHVPEH5E3LW7KURDC5W4CZ73WSBVRTOWTBSKUNPATUQC
RICRV7DJAG46H3D5ZWYZO5QTIMWOFX3PBDNOGDL2SGYCHAE4XHBQC
# @thumbsup/theme-card-flow
[![NPM](https://img.shields.io/npm/v/@thumbsup/theme-flow.svg?style=flat)](https://www.npmjs.com/package/@thumbsup/theme-flow)
[![Travis CI](https://travis-ci.org/thumbsup/theme-flow.svg?branch=master)](https://travis-ci.org/thumbsup/theme-flow)
**Update**
To get the settings from the JSON file to be available in the application,
the two files of the thumbsUp application must be adapted.
**../thumbsup/bin/options.js**
```javascript
// add this lines to Misc options section
'settings': {
group: 'Application settings:',
description: 'JSON config file (one key per argument)',
'default': null
},
....
return {
.....
watermarkPosition: opts['watermark-position'],
embedExif: opts['embed-exif'],
settings: getSettings(opts['settings']), // NEW for settings
}
**
* read the config settings
* @param {*} filename
*/
function getSettings(filename) {
var contents,
jsonContent = { // default settings
"logo": null,
"tabHome": "Home",
"application": "tumbsUp Fotobook",
"appVersion": "2.1.0",
"albumPrefix": "Album",
"albumsPrefix": "Alben",
"albumCardTitle": "Show Album ",
"copyright": null,
"footerLeft": null,
"footerCenter": null,
"footerRight": null
};
if (filename) {
var fs = require("fs");
try {
// try to get the settings from the file
contents = fs.readFileSync(filename, 'utf-8');
jsonContent = JSON.parse(contents);
return jsonContent;
} catch (ex) {
console.log("ERROR application settings", filename);
}
}
return jsonContent;
}
```
**.../thumbsup/src/website/website.js**
```javascript
function galleryModel(rootAlbum, opts) {
return {
home: rootAlbum,
title: opts.title,
footer: opts.footer,
thumbSize: opts.thumbSize,
largeSize: opts.largeSize,
googleAnalytics: opts.googleAnalytics,
settings: opts.settings // NEW for application settings, see opitons.js
}
}
```
**sample settings**
```
{
"author": "Peter Siebler",
"logo": "public/assets/avatar.jpg",
"application": "tumbsUp Fotobook",
"appVersion": "2.1.0",
"tabHome": "Start",
"albumPrefix": "Album",
"albumsPrefix": "Alben",
"albumCardTitle": "Öffne Album ",
"copyright": "2019 Peter Siebler. All rights reserved."
}
```
**usage**
```javascript
gallery.settings.ATTRIBUTE
// for example:
gallery.settings.logo
gallery.settings.author
gallery.settings.application
.....
```
/*
--------------------------------------------------------------------------------
Standardised metadata for a given image or video
This is based on parsing "provider data" such as Exiftool or Picasa
--------------------------------------------------------------------------------
*/
const moment = require('moment')
const path = require('path')
// mime type for videos
const MIME_VIDEO_REGEX = /^video\/.*$/
// standard EXIF date format, which is different from ISO8601
const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'
// infer dates from files with a date-looking filename
const FILENAME_DATE_REGEX = /\d{4}[_\-.\s]?(\d{2}[_\-.\s]?){5}\..{3,4}/
// moment ignores non-numeric characters when parsing
const FILENAME_DATE_FORMAT = 'YYYYMMDD HHmmss'
class Metadata {
constructor(exiftool, picasa, opts) {
// standardise metadata
this.date = getDate(exiftool)
this.caption = caption(exiftool)
this.keywords = keywords(exiftool, picasa)
this.video = video(exiftool)
this.animated = animated(exiftool)
this.rating = rating(exiftool)
this.favourite = favourite(picasa)
const size = dimensions(exiftool)
this.width = size.width
this.height = size.height
this.exif = opts ? (opts.embedExif ? exiftool.EXIF : undefined) : undefined
this.all = exiftool
// metadata could also include fields like
// - lat = 51.5
// - long = 0.12
// - country = "England"
// - city = "London"
// - aperture = 1.8
}
}
function getDate(exif) {
// first, check if there's a valid date in the metadata
const metadate = getMetaDate(exif)
if (metadate) return metadate.valueOf()
// next, check if the filename looks like a date
const namedate = getFilenameDate(exif)
if (namedate) return namedate.valueOf()
// otherwise, fallback to the last modified date
return moment(exif.File.FileModifyDate, EXIF_DATE_FORMAT).valueOf()
}
function getMetaDate(exif) {
const date = tagValue(exif, 'EXIF', 'DateTimeOriginal') ||
tagValue(exif, 'H264', 'DateTimeOriginal') ||
tagValue(exif, 'QuickTime', 'ContentCreateDate') ||
tagValue(exif, 'QuickTime', 'CreationDate') ||
tagValue(exif, 'QuickTime', 'CreateDate')
if (date) {
const parsed = moment(date, EXIF_DATE_FORMAT)
if (parsed.isValid()) return parsed
}
return null
}
function getFilenameDate(exif) {
const filename = path.basename(exif.SourceFile)
if (FILENAME_DATE_REGEX.test(filename)) {
const parsed = moment(filename, FILENAME_DATE_FORMAT)
if (parsed.isValid()) return parsed
}
return null
}
function caption(exif, picasa) {
return picasaValue(picasa, 'caption') ||
tagValue(exif, 'EXIF', 'ImageDescription') ||
tagValue(exif, 'IPTC', 'Caption-Abstract') ||
tagValue(exif, 'IPTC', 'Headline') ||
tagValue(exif, 'XMP', 'Description') ||
tagValue(exif, 'XMP', 'Title') ||
tagValue(exif, 'XMP', 'Label')
}
function keywords(exif, picasa) {
// try Picasa (comma-separated)
const picasaValues = picasaValue(picasa, 'keywords')
if (picasaValues) return picasaValues.split(',')
// try IPTC (string or array)
const iptcValues = tagValue(exif, 'IPTC', 'Keywords')
if (iptcValues) return makeArray(iptcValues)
// no keywords
return []
}
function video(exif) {
return MIME_VIDEO_REGEX.test(exif.File['MIMEType'])
}
function animated(exif) {
if (exif.File['MIMEType'] !== 'image/gif') return false
if (exif.GIF && exif.GIF.FrameCount > 0) return true
return false
}
function rating(exif) {
if (!exif.XMP) return 0
return exif.XMP['Rating'] || 0
}
function favourite(picasa) {
return picasaValue(picasa, 'star') === 'yes'
}
function tagValue(exif, type, name) {
if (!exif[type]) return null
return exif[type][name]
}
function picasaValue(picasa, name) {
if (typeof picasa !== 'object') return null
return picasa[name]
}
function makeArray(value) {
return Array.isArray(value) ? value : [value]
}
function dimensions(exif) {
// Use the Composite field to avoid having to check all possible tag groups (EXIF, QuickTime, ASF...)
if (!exif.Composite) {
return {
width: null,
height: null
}
} else {
const size = exif.Composite.ImageSize
const x = size.indexOf('x')
return {
width: parseInt(size.substr(0, x), 10),
height: parseInt(size.substr(x + 1), 10)
}
}
}
module.exports = Metadata
const messages = require('./messages')
const path = require('path')
const yargs = require('yargs')
const os = require('os')
const OPTIONS = {
// ------------------------------------
// Required arguments
// ------------------------------------
'input': {
group: 'Required:',
description: 'Path to the folder with all photos/videos',
normalize: true,
demand: true
},
'output': {
group: 'Required:',
description: 'Output path for the static website',
normalize: true,
demand: true
},
// ------------------------------------
// Input options
// ------------------------------------
'include-photos': {
group: 'Input options:',
description: 'Include photos in the gallery',
type: 'boolean',
'default': true
},
'include-videos': {
group: 'Input options:',
description: 'Include videos in the gallery',
type: 'boolean',
'default': true
},
'include-raw-photos': {
group: 'Input options:',
description: 'Include raw photos in the gallery',
type: 'boolean',
'default': false
},
'include': {
group: 'Input options:',
description: 'Glob pattern of files to include',
type: 'array'
},
'exclude': {
group: 'Input options:',
description: 'Glob pattern of files to exclude',
type: 'array'
},
// ------------------------------------
// Output options
// ------------------------------------
'thumb-size': {
group: 'Output options:',
description: 'Pixel size of the square thumbnails',
type: 'number',
'default': 120
},
'large-size': {
group: 'Output options:',
description: 'Pixel height of the fullscreen photos',
type: 'number',
'default': 1000
},
'photo-quality': {
group: 'Output options:',
description: 'Quality of the resized/converted photos',
type: 'number',
'default': 90
},
'video-quality': {
group: 'Output options:',
description: 'Quality of the converted video (percent)',
type: 'number',
'default': 75
},
'video-bitrate': {
group: 'Output options:',
description: 'Bitrate of the converted videos (e.g. 120k)',
type: 'string',
'default': null
},
'video-format': {
group: 'Output options:',
description: 'Video output format',
choices: ['mp4', 'webm'],
'default': 'mp4'
},
'photo-preview': {
group: 'Output options:',
description: 'How lightbox photos are generated',
choices: ['resize', 'copy', 'symlink', 'link'],
'default': 'resize'
},
'video-preview': {
group: 'Output options:',
description: 'How lightbox videos are generated',
choices: ['resize', 'copy', 'symlink', 'link'],
'default': 'resize'
},
'photo-download': {
group: 'Output options:',
description: 'How downloadable photos are generated',
choices: ['resize', 'copy', 'symlink', 'link'],
'default': 'resize'
},
'video-download': {
group: 'Output options:',
description: 'How downloadable videos are generated',
choices: ['resize', 'copy', 'symlink', 'link'],
'default': 'resize'
},
'link-prefix': {
group: 'Output options:',
description: 'Path or URL prefix for "linked" photos and videos',
type: 'string'
},
'cleanup': {
group: 'Output options:',
description: 'Remove any output file that\'s no longer needed',
type: 'boolean',
'default': false
},
'concurrency': {
group: 'Output options:',
description: 'Number of parallel parsing/processing operations',
type: 'number',
'default': os.cpus().length
},
'output-structure': {
group: 'Output options:',
description: 'File and folder structure for output media',
choices: ['folders', 'suffix'],
'default': 'folders'
},
'gm-args': {
group: 'Output options:',
description: 'Custom image processing arguments for GraphicsMagick',
type: 'array'
},
'watermark': {
group: 'Output options:',
description: 'Path to a transparent PNG to be overlaid on all images',
type: 'string'
},
'watermark-position': {
group: 'Output options:',
description: 'Position of the watermark',
choices: [
'Repeat', 'Center', 'NorthWest', 'North', 'NorthEast',
'West', 'East', 'SouthWest', 'South', 'SouthEast'
]
},
// ------------------------------------
// Album options
// ------------------------------------
'albums-from': {
group: 'Album options:',
description: 'How files are grouped into albums',
type: 'array',
'default': ['%path']
},
'sort-albums-by': {
group: 'Album options:',
description: 'How to sort albums',
choices: ['title', 'start-date', 'end-date'],
'default': 'start-date'
},
'sort-albums-direction': {
group: 'Album options:',
description: 'Album sorting direction',
choices: ['asc', 'desc'],
'default': 'asc'
},
'sort-media-by': {
group: 'Album options:',
description: 'How to sort photos and videos',
choices: ['filename', 'date'],
'default': 'date'
},
'sort-media-direction': {
group: 'Album options:',
description: 'Media sorting direction',
choices: ['asc', 'desc'],
'default': 'asc'
},
'album-zip-files': {
group: 'Album options:',
description: 'Create a ZIP file per album',
type: 'boolean',
'default': false
},
// ------------------------------------
// Website options
// ------------------------------------
'index': {
group: 'Website options:',
description: 'Filename of the home page',
'default': 'index.html'
},
'albums-output-folder': {
group: 'Website options:',
description: 'Output subfolder for HTML albums (default: website root)',
'default': '.'
},
'theme': {
group: 'Website options:',
description: 'Name of a built-in gallery theme',
choices: ['classic', 'cards', 'mosaic', 'flow'],
'default': 'classic'
},
'theme-path': {
group: 'Website options:',
description: 'Path to a custom theme',
normalize: true
},
'theme-style': {
group: 'Website options:',
description: 'Path to a custom LESS/CSS file for additional styles',
normalize: true
},
'title': {
group: 'Website options:',
description: 'Website title',
'default': 'Photo album'
},
'footer': {
group: 'Website options:',
description: 'Text or HTML footer',
'default': null
},
'google-analytics': {
group: 'Website options:',
description: 'Code for Google Analytics tracking',
type: 'string'
},
'embed-exif': {
group: 'Website options:',
description: 'Embed the exif metadata for each image into the gallery page',
type: 'boolean',
'default': false
},
// ------------------------------------
// Misc options
// ------------------------------------
'config': {
group: 'Misc options:',
description: 'JSON config file (one key per argument)',
normalize: true
},
'log': {
group: 'Misc options:',
description: 'Print a detailed text log',
choices: [null, 'info', 'debug', 'trace'],
'default': null
},
'usage-stats': {
group: 'Misc options:',
description: 'Enable anonymous usage statistics',
type: 'boolean',
'default': true
},
'dry-run': {
group: 'Misc options:',
description: "Update the index, but don't create the media files / website",
type: 'boolean',
'default': false
},
// ------------------------------------
// Applciation settings ! added by PS
// ------------------------------------
'settings': {
group: 'Application settings:',
description: 'JSON config file (one key per argument)',
'default': null
},
// ------------------------------------
// Deprecated options
// ------------------------------------
'original-photos': {
group: 'Deprecated:',
description: 'Copy and allow download of full-size photos',
type: 'boolean'
},
'original-videos': {
group: 'Deprecated:',
description: 'Copy and allow download of full-size videos',
type: 'boolean'
},
'albums-date-format': {
group: 'Deprecated:',
description: 'How albums are named in <date> mode [moment.js pattern]'
},
'css': {
group: 'Deprecated:',
description: 'Path to a custom provided CSS/LESS file for styling',
normalize: true
},
'download-photos': {
group: 'Deprecated:',
description: 'Target of the photo download links',
choices: ['large', 'copy', 'symlink', 'link']
},
'download-videos': {
group: 'Deprecated:',
description: 'Target of the video download links',
choices: ['large', 'copy', 'symlink', 'link']
},
'download-link-prefix': {
group: 'Deprecated:',
description: 'Path or URL prefix for linked downloads',
type: 'string'
}
}
// explicitly pass <process.argv> so we can unit test this logic
// otherwise it pre-loads all process arguments on require()
exports.get = (args) => {
const opts = yargs(args)
.usage(messages.USAGE())
.wrap(null)
.help('help')
.config('config')
.options(OPTIONS)
.epilogue(messages.CONFIG_USAGE())
.argv
// Warn users when they use deprecated options
const deprecated = Object.keys(OPTIONS).filter(name => OPTIONS[name].group === 'Deprecated:')
const specified = deprecated.filter(name => typeof opts[name] !== 'undefined')
if (specified.length > 0) {
const warnings = specified.map(name => `Warning: --${name} is deprecated`)
console.error(warnings.join('\n') + '\n')
}
// Make input/output folder absolute paths
opts['input'] = path.resolve(opts['input'])
opts['output'] = path.resolve(opts['output'])
// By default, use relative links to the input folder
if (opts['download-link-prefix']) opts['link-prefix'] = opts['download-link-prefix']
if (!opts['link-prefix']) {
opts['link-prefix'] = path.relative(opts['output'], opts['input'])
}
// Convert deprecated --download
if (opts['original-photos']) opts['download-photos'] = 'copy'
if (opts['original-videos']) opts['download-videos'] = 'copy'
if (opts['download-photos']) opts['photo-download'] = opts['download-photos']
if (opts['download-videos']) opts['video-download'] = opts['download-videos']
if (opts['photo-download'] === 'large') opts['photo-download'] = 'resize'
if (opts['video-download'] === 'large') opts['video-download'] = 'resize'
// Convert deprecated --albums-from
replaceInArray(opts['albums-from'], 'folders', '%path')
replaceInArray(opts['albums-from'], 'date', `{${opts['albums-date-format']}}`)
// Convert deprecated --css
if (opts['css']) opts['theme-style'] = opts['css']
// Add a dash prefix to any --gm-args value
// We can't specify the prefix on the CLI otherwise the parser thinks it's a thumbsup arg
if (opts['gm-args']) {
opts['gm-args'] = opts['gm-args'].map(val => `-${val}`)
}
// All options as an object
return {
input: opts['input'],
output: opts['output'],
includePhotos: opts['include-photos'],
includeVideos: opts['include-videos'],
includeRawPhotos: opts['include-raw-photos'],
include: opts['include'],
exclude: opts['exclude'],
cleanup: opts['cleanup'],
title: opts['title'],
thumbSize: opts['thumb-size'],
largeSize: opts['large-size'],
photoQuality: opts['photo-quality'],
videoQuality: opts['video-quality'],
videoBitrate: opts['video-bitrate'],
videoFormat: opts['video-format'],
photoPreview: opts['photo-preview'],
videoPreview: opts['video-preview'],
photoDownload: opts['photo-download'],
videoDownload: opts['video-download'],
linkPrefix: opts['link-prefix'],
albumsFrom: opts['albums-from'],
albumsDateFormat: opts['albums-date-format'],
sortAlbumsBy: opts['sort-albums-by'],
sortAlbumsDirection: opts['sort-albums-direction'],
sortMediaBy: opts['sort-media-by'],
sortMediaDirection: opts['sort-media-direction'],
albumZipFiles: opts['album-zip-files'],
theme: opts['theme'],
themePath: opts['theme-path'],
themeStyle: opts['theme-style'],
css: opts['css'],
googleAnalytics: opts['google-analytics'],
index: opts['index'],
footer: opts['footer'],
albumsOutputFolder: opts['albums-output-folder'],
usageStats: opts['usage-stats'],
log: opts['log'],
dryRun: opts['dry-run'],
concurrency: opts['concurrency'],
outputStructure: opts['output-structure'],
gmArgs: opts['gm-args'],
watermark: opts['watermark'],
watermarkPosition: opts['watermark-position'],
embedExif: opts['embed-exif'],
settings: getSettings(opts['settings']), // NEW for settings
}
}
function replaceInArray(list, match, replacement) {
for (var i = 0; i < list.length; ++i) {
if (list[i] === match) {
list[i] = replacement
}
}
}
/**
* read the config settings
* @param {*} filename
*/
function getSettings(filename) {
var contents,
jsonContent = {
"logo": null,
"tabHome": "Home",
"application": "tumbsUp Fotobook",
"appVersion": "2.1.0",
"albumPrefix": "Album",
"albumsPrefix": "Alben",
"albumCardTitle": "Show Album ",
"copyright": null,
"footerLeft": null,
"footerCenter": null,
"footerRight": null
};
if (filename) {
var fs = require("fs");
try {
contents = fs.readFileSync(filename, 'utf-8');
jsonContent = JSON.parse(contents);
return jsonContent;
} catch (ex) {
console.log("ERROR application settings", filename);
}
}
return jsonContent;
}
const path = require('path')
const async = require('async')
const resolvePkg = require('resolve-pkg')
const Theme = require('./theme')
exports.build = function(rootAlbum, opts, callback) {
// create the base layer assets
// such as shared JS libs, common handlebars helpers, CSS reset...
const baseDir = path.join(__dirname, 'theme-base')
const base = new Theme(baseDir, opts.output, {
stylesheetName: 'core.css'
})
// then create the actual theme assets
const themeDir = opts.themePath || localThemePath(opts.theme)
const theme = new Theme(themeDir, opts.output, {
stylesheetName: 'theme.css',
customStylesPath: opts.themeStyle
})
// and finally render each page
const gallery = galleryModel(rootAlbum, opts)
const tasks = createRenderingTasks(theme, rootAlbum, gallery, [])
// now build everything
async.series([
next => base.prepare(next),
next => theme.prepare(next),
next => async.parallel(tasks, next)
], callback)
}
function galleryModel(rootAlbum, opts) {
return {
home: rootAlbum,
title: opts.title,
footer: opts.footer,
thumbSize: opts.thumbSize,
largeSize: opts.largeSize,
googleAnalytics: opts.googleAnalytics,
settings: opts.settings // NEW for application settings, see opitons.js
}
}
function createRenderingTasks(theme, album, gallery, breadcrumbs) {
// a function to render this album
const thisAlbumTask = next => {
theme.render(album, {
gallery: gallery,
breadcrumbs: breadcrumbs
}, next)
}
const tasks = [thisAlbumTask]
// and all nested albums
album.albums.forEach(function(nested) {
const nestedTasks = createRenderingTasks(theme, nested, gallery, breadcrumbs.concat([album]))
Array.prototype.push.apply(tasks, nestedTasks)
})
return tasks
}
function localThemePath(themeName) {
const local = resolvePkg(`@thumbsup/theme-${themeName}`, { cwd: __dirname })
if (!local) {
throw new Error(`Could not find a built-in theme called ${themeName}`)
}
return local
}
exiftool -all -g2 -j -c %+.8f -ext jpg -ext mp4 -r -n -w $TARGET/%d%f.json $SOURCE
exiftool $FILES \
-FileName \
-title \
-artist \
-description \
-createdate \
-comment \
-keywords \
-copyright \
-copyrightNotice \
-make \
-model \
-SourceResolution \
-ShutterSpeedValue \
-FNumber \
-ExposureTime \
-ISOSpeedRatings \
-FocalLength \
-LightValue \
-LensModel \
-FileSize \
-CreateDate \
-FileType \
-CanonModelID \
-LensID \
-Artist \
-Quality \
-RecordMode \
-Compression \
-ExposureMode \
-ExposureTime \
-Aperture \
-ISO \
-FocalLength \
-HyperfocalDistance \
-Flash \
-WhiteBalance \
-DriveMode \
-FocusMode \
-AFAreaMode \
-NumAFPoints \
-ValidAFPoints \
-AspectRatio \
-ColorTemperature \
-PictureStyle \
-ColorSpace \
-ImageSize \
-Megapixels \
-city \
-country \
-countrycode \
-GPSLatitude \
-GPSLatitudeRef \
-GPSLongitude \
-GPSLongitudeRef \
-GPSAltitude \
-GPSAltitudeRef \
$PARAMS \
-w $TARGET/%d%f.json $SOURCE
-if '($GPSLatitude)' \
-filename \
-title \
-artist \
-description \
-city \
-country \
-countrycode \
-createdate \
-make \
-model \
-GPSLatitude \
-GPSLatitudeRef \
-GPSLongitude \
-GPSLongitudeRef \
-GPSAltitude \
-GPSAltitudeRef \
-x SourceFile \
-comment \
-keywords \
$PARAMS \
$SOURCE > $TARGET
-if '($GPSLatitude)' \
-filename \
-GPSDateTime \
-GPSLatitude \
-GPSLatitudeRef \
-GPSLongitude \
-GPSLongitudeRef \
-GPSAltitude \
-GPSAltitudeRef \
-title \
-artist \
-description \
-city \
-country \
-countrycode \
-createdate \
-comment \
-keywords \
-make \
-model \
$PARAMS \
$SOURCE > $TARGET
#!/bin/bash
## ./saveAllMetadata.sh ../../tmp2 tmp2/meta
SOURCE=$1
TARGET=$2
echo "Scanning: $SOURCE, write data to $TARGET"
exiftool -all -g2 -j -c %+.8f -ext jpg -ext mp4 -r -n -w $TARGET/%d%f.json $SOURCE
echo "All well done..."
[{
"SourceFile": "../../tmp2/Scotland/Bridges/201806121D0001.jpg",
"FileName": "201806121D0001.jpg",
"GPSLatitude": 55.8880555555556,
"GPSLatitudeRef": "N",
"GPSLongitude": -4.24166666666667,
"GPSLongitudeRef": "W",
"Artist": "Peter Siebler",
"City": "Glasgow",
"CreateDate": "1982:02:10 08:00:00",
"Copyright": "peter.jpg",
"Make": "Sony",
"Model": "HX-50"
},
{
"SourceFile": "../../tmp2/Scotland/Bridges/_cover.jpg",
"FileName": "_cover.jpg",
"GPSLatitude": 51.5001524,
"GPSLatitudeRef": "N",
"GPSLongitude": 0.1262362,
"GPSLongitudeRef": "W",
"Title": "Scotland",
"Artist": "Peter Siebler",
"Description": "Die Zugreise durch die grünen Hügel von Schottland. ",
"City": "Glasgow",
"Country": "England",
"CountryCode": "GB",
"CreateDate": "1982:02:10 08:00:00",
"Copyright": "peter.jpg",
"CopyrightNotice": "peter.jpg",
"Make": "Sony",
"Model": "HX-50"
},
{
"SourceFile": "../../tmp2/Scotland/Bridges/test.jpg",
"FileName": "test.jpg",
"GPSLatitude": 47.4622222222222,
"GPSLatitudeRef": "N",
"GPSLongitude": 9.63305555555556,
"GPSLongitudeRef": "E",
"Artist": "Peter Siebler",
"City": "Hoechst",
"Make": "Sony",
"Model": "HX-50"
},
{
"SourceFile": "../../tmp2/Scotland/Bridges/test2.jpg",
"FileName": "test2.jpg",
"GPSLatitude": 48.6173916666667,
"GPSLatitudeRef": "N",
"GPSLongitude": 15.2027116666667,
"GPSLongitudeRef": "E",
"GPSAltitude": 512.4968,
"Title": "Stift Zwettl Kreuzgang Nordflügel 02",
"Artist": "Uoaei1",
"City": "Gemeinde Zwettl-Niederösterreich",
"Country": "Österreich",
"CountryCode": "AT",
"CreateDate": "2014:09:24 12:17:44",
"Keywords": "Zwettl",
"Copyright": "Uoaei1",
"CopyrightNotice": "Uoaei1",
"Make": "NIKON CORPORATION",
"Model": "NIKON D7100",
"FNumber": 8,
"ExposureTime": 0.02222222222,
"FocalLength": 15,
"LensModel": "8.0-16.0 mm f/4.5-5.6"
},
{
"SourceFile": "../../tmp2/Scotland/Bridges/test3.jpg",
"FileName": "test3.jpg",
"GPSLatitude": 55.8880555555556,
"GPSLatitudeRef": "N",
"GPSLongitude": -4.24166666666667,
"GPSLongitudeRef": "W",
"Artist": "Peter Siebler",
"City": "Glasgow",
"CreateDate": "1982:02:10 08:00:00",
"Copyright": "peter.jpg",
"Make": "Sony",
"Model": "HX-50"
}]
saveGPSData.sh creates a GPS data list from the selected directory
saveMetadata.sh extracts all metadata form the selected fotos and save this to a json file
setDate.sh sample script to update all dates for the selected fotos
saveGPSData.sh, creates a GPS data list from the selected directory
saveMetadata.sh, extracts all metadata form the selected fotos and save this to a json file
setDate.sh, sample script to update all dates for the selected fotos
updateMetadata.sh, updates the metadata for photos
##Require
Install theese node module for tools:
```
npm i --save node-exiftool del concat fs-extra gulp \
gulp-clean-css gulp-filter gulp-if \
gulp-uglify gulp-useref readable-stream
```
checkMeta('data-datetimeoriginal', meta.exif.DateTimeOriginal);
checkMeta('data-fnumber', meta.exif.FNumber);
checkMeta('data-exposuretime', meta.exif.ExposureTime);
checkMeta('data-isospeedratings', meta.exif.ISO);
checkMeta('data-focallength', meta.exif.FocalLength);
checkMeta('data-lens', meta.exif.Lens);
checkMeta('data-model', meta.exif.Model);
checkMeta('data-make', meta.exif.Make);
checkMeta('data-gpslatitude', meta.exif.GPSLatitude);
checkMeta('data-gpslongitude', meta.exif.GPSLongitude);
checkMeta('data-gpslatituderef', meta.exif.GPSLatitudeRef);
checkMeta('data-gpslongituderef', meta.exif.GPSLongitudeRef);
checkMeta('data-datetimeoriginal', meta.exif.DateTimeOriginal)
checkMeta('data-fnumber', meta.exif.FNumber)
checkMeta('data-exposuretime', meta.exif.ExposureTime)
checkMeta('data-isospeedratings', meta.exif.ISO)
checkMeta('data-focallength', meta.exif.FocalLength)
checkMeta('data-lens', meta.exif.Lens)
checkMeta('data-model', meta.exif.Model)
checkMeta('data-make', meta.exif.Make)
checkMeta('data-gpslatitude', meta.exif.GPSLatitude)
checkMeta('data-gpslongitude', meta.exif.GPSLongitude)
checkMeta('data-gpslatituderef', meta.exif.GPSLatitudeRef)
checkMeta('data-gpslongituderef', meta.exif.GPSLongitudeRef)
const filename = meta.all.SourceFile.replace(".", "-") + ".json";
const sourceFile = config.folders.input + '/' + filename;
const targetFile = config.folders.output + '/media/large/' + filename;
const filename = meta.all.SourceFile.replace(".", "-") + ".json"
const sourceFile = config.folders.input + filename
const targetFile = config.folders.output + 'media/large/' + filename
return 'media/large/' + filename;
} catch (err) {
return null;
} catch (e) {
// console.error(" ✕︎ " + "Cannot write file ", filename)
return null
}
return 'media/large/' + filename
console.log(' ✔︎ copyMetaFiles:', sourceFile)
}
/**
* helper method to parse the settings properties
* for the metadata
* @param {*} obj
* @param {*} parse
*/
function parseObjectProperties(obj, parse) {
for (var k in obj) {
if (typeof obj[k] === 'object' && obj[k] !== null) {
parse({ "label": k, deep: 0 })
parseObjectProperties(obj[k], parse)
} else if (obj.hasOwnProperty(k)) {
parse({ "label": k, "field": obj[k], deep: 1 })
}
}
}
/**
* get all metadata fields for the json file, mapped
* with the settings meta fields.
*
* Optional if no meta fields are defined, all meta data
* will be returned
*
* @param {*} data
* @param {*} fields
*/
function mapMetaData(data, fields) {
var d = {},
section = null,
f = []
// first check if we can find metatags
if (fields && fields.metatags) {
parseObjectProperties(fields.metatags, function(prop) {
try {
if (prop.deep === 0) {
section = prop.label
d[section] = {}
}
if (prop.deep === 1) {
f = prop.field.split('.')
d[section][prop.label] = data[f[0]][f[1]]
}
} catch (e) {}
})
return JSON.stringify(d)
function writeMetadata(data) {
const filename = data.filename.replace('.', '_') + '.json';
var content = JSON.stringify(data);
function writeMetadata(data, config) {
const filename = config.writeMetaData.target + data.SourceFile.replace('.', '_') + '.json'
const webfile = config.writeMetaData.linkfolder + data.File.FileName.replace('.', '_') + '.json'
"devDependencies": {
"concat": "^1.0.3",
"del": "^4.1.1",
"fs-extra": "^8.0.1",
"gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0",
"gulp-data": "^1.3.1",
"gulp-debug": "^4.0.0",
"gulp-exif": "^0.10.0",
"gulp-extend": "^0.3.0",
"gulp-filter": "^6.0.0",
"gulp-if": "^2.0.2",
"gulp-tap": "^1.0.1",
"gulp-uglify": "^3.0.2",
"gulp-useref": "^3.1.6",
"node-exiftool": "^2.3.0",
"readable-stream": "^3.4.0"
}
"devDependencies": {}
# Requirements
- Install Thumbsup see: https://github.com/thumbsup/thumbsup.
- min. Version 2.10.1 (master branch) !!
- Add the theme-card-flow to the Thumbsup folder.
- Update the config.json
- Update appsettings.json
- **OPTIONAL**
- add cover images for the albums and galleries
- add EXIF Data (Artist, Copyright, ImageDescription, DocumentName) to the cover image
- Update the avatar images (see public/assets/avatar*.*)
- Update the header backimage (see public/assets/header.jpg)
- run thumbsup to build the fotobook application
- **Node Modules**
+ npm install --save-dev fs-extra del node-exiftool
+ others optional see package.json
## Usage
# Install package ´thumbsup´ locally
```node
## install tools
> brew install exiftool
> brew install graphicsmagick
> brew install ffmpeg
> brew install gifsicle
> brew install dcraw
## install thumbsup in the current photoapp folder
> npm --save install https://github.com/thumbsup/thumbsup.git
```
```bash
thumbsup --config config.json
## Optional modify the Thumbsup metadata.js ##
```js
...
class Metadata {
constructor(exiftool, picasa, opts) {
// standardise metadata
this.date = getDate(exiftool)
this.caption = caption(exiftool)
this.keywords = keywords(exiftool, picasa)
this.video = video(exiftool)
this.animated = animated(exiftool)
this.rating = rating(exiftool)
this.favourite = favourite(picasa)
const size = dimensions(exiftool)
this.width = size.width
this.height = size.height
this.exif = opts ? (opts.embedExif ? exiftool.EXIF : undefined) : undefined
config.json
{
"input": "PATH TO OUTPUT",
"output": "./fotobuch",
"title": "Fotobuch AUTOR",
"include-raw-photos": false,
"photo-quality": 90,
"thumb-size": 360,
"large-size": 1200,
"embed-exif": true,
"sort-albums-by": "title",
"sort-albums-direction": "desc",
"theme-path": "PROJECTFOLDER/theme-cards-flow/theme",
"theme-settings": "PATH TO PROPERTYFILE.json"
"cleanup": true
this.all = exiftool // <--- Add this to get all metadata
// metadata could also include fields like
// - lat = 51.5
// - long = 0.12
// - country = "England"
// - city = "London"
// - aperture = 1.8
}
...
* **OPTIONAL**
- modify the metadata.js
- add cover images for the albums and galleries
- add EXIF Data (Artist, Copyright, ImageDescription, DocumentName) to the cover image
- Update the avatar images (see public/assets/avatar*.*)
- Update the header backimage (see public/assets/header.jpg)
- run thumbsup to build the fotobook application
## Usage
```bash
## run tumbsup
> node_modules/thumbsup/bin/thumbsup.js --config config.json
## Cover Text & Image
- For this application, the first photo is checked if the EXIF data (Artist, Copyright, ImageDescription, DocumentName) is set there.
If these are present, then the cover teaser for gallery or albums is displayed.
### Configuration sample
```
## sample for configuration
config.json
{
"input": "PATH TO OUTPUT",
"output": "./fotobuch",
"title": "Fotobuch AUTOR",
"include-raw-photos": false,
"photo-quality": 90,
"thumb-size": 360,
"large-size": 1200,
"embed-exif": true,
"sort-albums-by": "title",
"sort-albums-direction": "desc",
"theme-path": "PROJECTFOLDER/theme-cards-flow/theme",
"theme-settings": "PATH TO PROPERTYFILE.json"
"cleanup": true
}
- If the ImageDescription is empty, no Cover Background Image will be displayed.
## sample for theme-settings
{
"author": "AUTORNAME",
"logo": "public/assets/avatar.jpg",
"application": "tumbsUp Fotobook",
"appVersion": "2.1.1",
"assets": "public/assets/",
"tabHome": "Start",
"albumPrefix": "Album",
"albumsPrefix": "Alben",
"albumCardTitle": "Öffne Album ",
"copyright": "2019 ..... All rights reserved.",
"navigation": "navi/fbtemp2.json",
"footer": "<span>Testlab Fotopalbum</span>",
"folders": {
"input": "PATH FOTOS",
"output": "OUTPUT"
},
"writeMetaData": {
"target": "METADATAFOLDER",
"linkfolder": "meta/"
}
}
## Cover Text & Image
- For this application, the first photo is checked if the EXIF data (Artist, Copyright, ImageDescription, DocumentName) is set there.
- If these are present, then the cover teaser for gallery or albums is displayed.
- If the ImageDescription is empty, no Cover Background Image will be displayed.
- if ... a simpe cover w/o a background image will be displayed.
## Used Plugins
- [Justified Gallery 4.0.0-alpha](https://github.com/videojs/video.js)
- [Lightgallery - v1.2.14 - 2016-01-18](http://sachinchoolur.github.io/lightGallery/)
- [Video.js 7.5.5](https://github.com/miromannino/Justified-Gallery)