6KFVCGKKXDPZMNUP6ZN6M7MJQ25PDER42VV5FAY45OHJZWO4VEMQC G475G2KUDACOYKXN4FSMA7LQBMVIV6QEPYYAGBXQGWLYX3YJDZTAC QX7XVNQYOTBDULUCLFRGSATHL7KWBSEV6QWNFY353S7ZIR6UGJWQC L3PX32UXE2SC4JK3VXLL654DHBZGVXSBLZRQIGUIB4RIXEVHU5IQC ZRQFW3UNHIZLLXHXHK4EHVK74IN7RRGASB4WKYJAS2FJA5EHA2XAC TOLGMXUXRC74FQHKSAFLRNEG44GPLBMORKRDPU35EB2F424AN5QAC HOTFGCPMWQATBEXUZXAAW7TILKKQJGJMLAF72URDK4H632C3JXEQC MCPXLE6LIQRLQA2OBQSVIH4O3HAEZZWKPQMEDERPBHN6IVOAVXVAC 6Q2LBYYTS5Z67TKMOMJRBRST5UIOW3CGRSDGCDQBO3F3DNNPVGIQC 7CRKZALXJV7SBMSXU44VATIRC6XBWNRVDZISX3XKGJAZNXUZOBKAC 6WPF42RDWKN2YKPNQNRFMKIO4TJBQCUKRPLVY4V6ULN6KXPBQOUQC PJQXKAUOGPICDWXZAZN2KANNYXTUM7YTQ7G35NDZIEZQBTZYC3ZQC UW4WSPWD7TVQWURMTLRNW6LKVFLUVQT7NBUP5A4WMAIVHSLUPH7AC A4YDN3ZM5CNM43BMK4BAEB6I2GVQQZUKBCZYMYCRVZSS6EXRY3VAC R4THF7FOB3LE7JE7DMA5UL6PJIYZMNUWH6TLUKDHNB2X47V4MAEQC TYCBCSGHUAHVPEH5E3LW7KURDC5W4CZ73WSBVRTOWTBSKUNPATUQC RICRV7DJAG46H3D5ZWYZO5QTIMWOFX3PBDNOGDL2SGYCHAE4XHBQC # @thumbsup/theme-card-flow[](https://www.npmjs.com/package/@thumbsup/theme-flow)[](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 filecontents = 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**```javascriptfunction 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**```javascriptgallery.settings.ATTRIBUTE// for example:gallery.settings.logogallery.settings.authorgallery.settings.application.....```
/*--------------------------------------------------------------------------------Standardised metadata for a given image or videoThis is based on parsing "provider data" such as Exiftool or Picasa--------------------------------------------------------------------------------*/const moment = require('moment')const path = require('path')// mime type for videosconst MIME_VIDEO_REGEX = /^video\/.*$/// standard EXIF date format, which is different from ISO8601const EXIF_DATE_FORMAT = 'YYYY:MM:DD HH:mm:ssZ'// infer dates from files with a date-looking filenameconst FILENAME_DATE_REGEX = /\d{4}[_\-.\s]?(\d{2}[_\-.\s]?){5}\..{3,4}/// moment ignores non-numeric characters when parsingconst FILENAME_DATE_FORMAT = 'YYYYMMDD HHmmss'class Metadata {constructor(exiftool, picasa, opts) {// standardise metadatathis.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.widththis.height = size.heightthis.exif = opts ? (opts.embedExif ? exiftool.EXIF : undefined) : undefinedthis.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 metadataconst metadate = getMetaDate(exif)if (metadate) return metadate.valueOf()// next, check if the filename looks like a dateconst namedate = getFilenameDate(exif)if (namedate) return namedate.valueOf()// otherwise, fallback to the last modified datereturn 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 keywordsreturn []}function video(exif) {return MIME_VIDEO_REGEX.test(exif.File['MIMEType'])}function animated(exif) {if (exif.File['MIMEType'] !== 'image/gif') return falseif (exif.GIF && exif.GIF.FrameCount > 0) return truereturn false}function rating(exif) {if (!exif.XMP) return 0return exif.XMP['Rating'] || 0}function favourite(picasa) {return picasaValue(picasa, 'star') === 'yes'}function tagValue(exif, type, name) {if (!exif[type]) return nullreturn exif[type][name]}function picasaValue(picasa, name) {if (typeof picasa !== 'object') return nullreturn 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.ImageSizeconst 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 optionsconst 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 pathsopts['input'] = path.resolve(opts['input'])opts['output'] = path.resolve(opts['output'])// By default, use relative links to the input folderif (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 --downloadif (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-fromreplaceInArray(opts['albums-from'], 'folders', '%path')replaceInArray(opts['albums-from'], 'date', `{${opts['albums-date-format']}}`)// Convert deprecated --cssif (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 argif (opts['gm-args']) {opts['gm-args'] = opts['gm-args'].map(val => `-${val}`)}// All options as an objectreturn {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 assetsconst themeDir = opts.themePath || localThemePath(opts.theme)const theme = new Theme(themeDir, opts.output, {stylesheetName: 'theme.css',customStylesPath: opts.themeStyle})// and finally render each pageconst gallery = galleryModel(rootAlbum, opts)const tasks = createRenderingTasks(theme, rootAlbum, gallery, [])// now build everythingasync.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 albumconst thisAlbumTask = next => {theme.render(album, {gallery: gallery,breadcrumbs: breadcrumbs}, next)}const tasks = [thisAlbumTask]// and all nested albumsalbum.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/metaSOURCE=$1TARGET=$2echo "Scanning: $SOURCE, write data to $TARGET"exiftool -all -g2 -j -c %+.8f -ext jpg -ext mp4 -r -n -w $TARGET/%d%f.json $SOURCEecho "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 directorysaveMetadata.sh extracts all metadata form the selected fotos and save this to a json filesetDate.sh sample script to update all dates for the selected fotos
saveGPSData.sh, creates a GPS data list from the selected directorysaveMetadata.sh, extracts all metadata form the selected fotos and save this to a json filesetDate.sh, sample script to update all dates for the selected fotosupdateMetadata.sh, updates the metadata for photos
##RequireInstall 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 + filenameconst 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/' + filenameconsole.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 metatagsif (fields && fields.metatags) {parseObjectProperties(fields.metatags, function(prop) {try {if (prop.deep === 0) {section = prop.labeld[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```
```bashthumbsup --config config.json
## Optional modify the Thumbsup metadata.js ##```js...class Metadata {constructor(exiftool, picasa, opts) {// standardise metadatathis.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.widththis.height = size.heightthis.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 configurationconfig.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)