├── src ├── _photos │ ├── SamplePhoto1.jpg │ ├── SamplePhoto2.jpg │ ├── SamplePhoto3.jpg │ ├── SamplePhoto4.jpg │ └── SamplePhoto5.JPG ├── _exifdata │ ├── _exifdata.json │ ├── 05_11_2019_084433.md │ ├── 02_08_2019_101420.md │ ├── 02_01_2019_094929.md │ ├── 14_07_2019_100357.md │ └── 05_11_2019_084102.md ├── photos │ ├── blur │ │ ├── 02_01_2019_094929.jpg │ │ ├── 02_08_2019_101420.jpg │ │ ├── 05_11_2019_084102.jpg │ │ ├── 05_11_2019_084433.jpg │ │ └── 14_07_2019_100357.JPG │ ├── w520 │ │ ├── 02_01_2019_094929.jpg │ │ ├── 02_08_2019_101420.jpg │ │ ├── 05_11_2019_084102.jpg │ │ ├── 05_11_2019_084433.jpg │ │ └── 14_07_2019_100357.JPG │ └── w960 │ │ ├── 02_01_2019_094929.jpg │ │ ├── 02_08_2019_101420.jpg │ │ ├── 05_11_2019_084102.jpg │ │ ├── 05_11_2019_084433.jpg │ │ └── 14_07_2019_100357.JPG ├── tags.njk ├── _includes │ └── layouts │ │ ├── photo.njk │ │ └── base.njk └── index.njk ├── utils ├── FormatDate.js ├── GetDateFromPhoto.js ├── WriteExifFile.js └── OptimisePhoto.js ├── package.json ├── node-exif-photos.js ├── .gitignore ├── .eleventy.js └── README.md /src/_photos/SamplePhoto1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/_photos/SamplePhoto1.jpg -------------------------------------------------------------------------------- /src/_photos/SamplePhoto2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/_photos/SamplePhoto2.jpg -------------------------------------------------------------------------------- /src/_photos/SamplePhoto3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/_photos/SamplePhoto3.jpg -------------------------------------------------------------------------------- /src/_photos/SamplePhoto4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/_photos/SamplePhoto4.jpg -------------------------------------------------------------------------------- /src/_photos/SamplePhoto5.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/_photos/SamplePhoto5.JPG -------------------------------------------------------------------------------- /src/_exifdata/_exifdata.json: -------------------------------------------------------------------------------- 1 | {"layout":"layouts/photo.njk","permalink":"photo/{{ exif.DateTimeOriginal | dateUrl }}/index.html"} 2 | -------------------------------------------------------------------------------- /src/photos/blur/02_01_2019_094929.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/blur/02_01_2019_094929.jpg -------------------------------------------------------------------------------- /src/photos/blur/02_08_2019_101420.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/blur/02_08_2019_101420.jpg -------------------------------------------------------------------------------- /src/photos/blur/05_11_2019_084102.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/blur/05_11_2019_084102.jpg -------------------------------------------------------------------------------- /src/photos/blur/05_11_2019_084433.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/blur/05_11_2019_084433.jpg -------------------------------------------------------------------------------- /src/photos/blur/14_07_2019_100357.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/blur/14_07_2019_100357.JPG -------------------------------------------------------------------------------- /src/photos/w520/02_01_2019_094929.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w520/02_01_2019_094929.jpg -------------------------------------------------------------------------------- /src/photos/w520/02_08_2019_101420.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w520/02_08_2019_101420.jpg -------------------------------------------------------------------------------- /src/photos/w520/05_11_2019_084102.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w520/05_11_2019_084102.jpg -------------------------------------------------------------------------------- /src/photos/w520/05_11_2019_084433.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w520/05_11_2019_084433.jpg -------------------------------------------------------------------------------- /src/photos/w520/14_07_2019_100357.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w520/14_07_2019_100357.JPG -------------------------------------------------------------------------------- /src/photos/w960/02_01_2019_094929.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w960/02_01_2019_094929.jpg -------------------------------------------------------------------------------- /src/photos/w960/02_08_2019_101420.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w960/02_08_2019_101420.jpg -------------------------------------------------------------------------------- /src/photos/w960/05_11_2019_084102.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w960/05_11_2019_084102.jpg -------------------------------------------------------------------------------- /src/photos/w960/05_11_2019_084433.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w960/05_11_2019_084433.jpg -------------------------------------------------------------------------------- /src/photos/w960/14_07_2019_100357.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrissy-dev/jamstack-photo-website/HEAD/src/photos/w960/14_07_2019_100357.JPG -------------------------------------------------------------------------------- /utils/FormatDate.js: -------------------------------------------------------------------------------- 1 | const { 2 | DateTime 3 | } = require('luxon'); 4 | 5 | module.exports = async function (date) { 6 | return DateTime.fromJSDate(new Date(date), { 7 | zone: 'utc' 8 | }).toFormat('dd_LL_yyyy_hhmmss'); 9 | } -------------------------------------------------------------------------------- /utils/GetDateFromPhoto.js: -------------------------------------------------------------------------------- 1 | const exif = require('fast-exif'); 2 | 3 | module.exports = async function (file) { 4 | return await exif.read(file).then(data => { 5 | return data.exif.DateTimeOriginal; 6 | }).catch(console.error); 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jamstack-photo-website", 3 | "version": "0.0.1", 4 | "description": "This is a proof-of concept workflow that extracts EXIF data embeded in photos to generate a static website using Eleventy", 5 | "scripts": { 6 | "start": "node node-exif-photos && ELEVENTY_ENV=development eleventy --serve --watch", 7 | "build": "node node-exif-photos && del dist && ELEVENTY_ENV=production eleventy" 8 | }, 9 | "author": "Chris Collins", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@11ty/eleventy": "^0.10.0-beta.1", 13 | "del-cli": "^3.0.0", 14 | "fast-exif": "^1.0.1", 15 | "fast-glob": "^3.1.1", 16 | "jimp": "^0.9.3", 17 | "luxon": "^1.21.3", 18 | "node-iptc": "^1.0.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /utils/WriteExifFile.js: -------------------------------------------------------------------------------- 1 | const exif = require('fast-exif'); 2 | const formatDate = require('./FormatDate'); 3 | const fs = require('fs-extra'); 4 | const getDateFromPhoto = require('./GetDateFromPhoto'); 5 | const path = require('path'); 6 | var iptc = require('node-iptc') 7 | 8 | 9 | let GetIPTCData = async function(file) { 10 | let readFile = fs.readFileSync(file) 11 | return iptc(readFile); 12 | } 13 | 14 | module.exports = async function (file) { 15 | 16 | let newName = await getDateFromPhoto(file); 17 | newName = await formatDate(newName); 18 | let iptcData = await GetIPTCData(file); 19 | 20 | if(iptcData.keywords) { 21 | iptcData = { 22 | ...iptcData, 23 | tags: iptcData.keywords 24 | } 25 | } 26 | 27 | let exifData = await exif.read(file).then(data => { 28 | return { 29 | ...{ 30 | image_path: newName + path.extname(file) 31 | }, 32 | ...data, 33 | ...iptcData 34 | } 35 | }).catch(console.error); 36 | 37 | 38 | fs.writeFile(`src/_exifdata/${newName}.md`, `---json\n${JSON.stringify(exifData)}\n---`, function (err) { 39 | if (err) { 40 | return console.log(err); 41 | } 42 | }); 43 | } -------------------------------------------------------------------------------- /utils/OptimisePhoto.js: -------------------------------------------------------------------------------- 1 | const formatDate = require('./FormatDate'); 2 | const fs = require('fs-extra'); 3 | const getDateFromPhoto = require('./GetDateFromPhoto'); 4 | const JIMP = require('jimp'); 5 | const path = require('path'); 6 | 7 | module.exports = async function (file, widths, outputDir) { 8 | let newName = await getDateFromPhoto(file); 9 | newName = await formatDate(newName) + path.extname(file); 10 | 11 | widths.map(width => { 12 | fs.exists(`${outputDir}/w${width}/${newName}`, async function (exists) { 13 | if (!exists) { 14 | const image = await JIMP.read(file); 15 | image.resize(width, JIMP.AUTO); // resize 16 | image.quality(80); // set JPEG quality 17 | image.writeAsync(`${outputDir}/w${width}/${newName}`); 18 | console.log(`Generated: ${outputDir}/w${width}/${newName}`); 19 | } 20 | }); 21 | }); 22 | 23 | fs.exists(`${outputDir}/blur/${newName}`, async function (exists) { 24 | if (!exists) { 25 | const image = await JIMP.read(file); 26 | image.resize(20, JIMP.AUTO); // resize 27 | image.quality(80); // set JPEG quality 28 | image.gaussian(3); // fast blur the image by r pixels 29 | image.writeAsync(`${outputDir}/blur/${newName}`); 30 | console.log(`Generated: ${outputDir}/blur/${newName}`); 31 | } 32 | }); 33 | } -------------------------------------------------------------------------------- /node-exif-photos.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const glob = require("fast-glob"); 3 | const optimisePhoto = require("./utils/OptimisePhoto"); 4 | const writeExifFile = require("./utils/WriteExifFile"); 5 | 6 | (async function () { 7 | const OPTIONS = { 8 | widths: [520, 960], 9 | input_photos_dir: "src/_photos", 10 | output_photos_dir: "src/photos" 11 | }; 12 | 13 | var files = await glob([ 14 | `${OPTIONS.input_photos_dir}/*.{jpg,jpeg,JPG,JPEG}`, 15 | ]); 16 | 17 | // Copy the contents _exifdata/_exifdata.json file 18 | var templateDataFile = fs.readFileSync('src/_exifdata/_exifdata.json', 'utf8'); 19 | 20 | // Empty the _exifdata folder - regenerate files based on current photos tree 21 | fs.emptyDirSync('src/_exifdata'); 22 | 23 | // Re-write the file to enable a collection 24 | fs.writeJson('src/_exifdata/_exifdata.json', JSON.parse(templateDataFile), err => { 25 | if (err) return console.error(err) 26 | console.log('Wrote _exifdata.json file.') 27 | }) 28 | 29 | files.forEach(async (file) => { 30 | // Creates optimised versions for each item in OPTIONS.widths, always creates a 20px wide blur 31 | // These are available in the same folder w{width} eg. /w320/dd_LL_yyyy_hhmmss.jpg 32 | await optimisePhoto(file, OPTIONS.widths, OPTIONS.output_photos_dir); 33 | 34 | // // Writes a file with JSON frontmatter exposing the exif data 35 | await writeExifFile(file, OPTIONS.input_photos_dir); 36 | }) 37 | })(); -------------------------------------------------------------------------------- /src/tags.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "layouts/base.njk" 3 | pagination: 4 | data: collections 5 | size: 1 6 | alias: tag 7 | permalink: /tags/{{ tag | slug }}/ 8 | --- 9 | 10 | {% set taglist = collections[ tag ] %} 11 | 12 |
13 |

JAMStack Photo site

14 |

An example site generated using exif data extracted from photos

15 | 16 | {# This should be a ul :| #} 17 | {% for tag in collections.tagList %} 18 | {% set tagUrl %}/tags/{{ tag | slug }}/{% endset %} 19 | #{{ tag | slug }}, 20 | {% endfor %} 21 | 22 |

{{ taglist.length }} photos tagged: {{ tag }}

23 | 24 | 48 | 49 | 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /src/_exifdata/05_11_2019_084433.md: -------------------------------------------------------------------------------- 1 | ---json 2 | {"image_path":"05_11_2019_084433.jpg","image":{"ImageDescription":"Finnieston Crane","Make":"Apple","Model":"iPhone X","Orientation":1,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"Software":"","ModifyDate":"2019-11-05T08:44:33.000Z","Artist":"Christopher Collins","TileWidth":512,"TileLength":512,"YCbCrSubSampling":[2,2],"Copyright":"Christopher Collins","ExifOffset":316,"GPSInfo":946},"thumbnail":{"Compression":6,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"ThumbnailOffset":1178,"ThumbnailLength":5328},"exif":{"ExposureTime":0.0011737089201877935,"FNumber":1.8,"ExposureProgram":2,"ISO":20,"ExifVersion":{"type":"Buffer","data":[48,50,51,49]},"DateTimeOriginal":"2019-11-05T08:44:33.000Z","DateTimeDigitized":"2019-11-05T08:44:33.000Z","ComponentsConfiguration":{"type":"Buffer","data":[1,2,3,0]},"ShutterSpeedValue":9.734351873955598,"ApertureValue":1.6959938128383605,"BrightnessValue":9.163551797040169,"ExposureBiasValue":0,"MeteringMode":5,"Flash":24,"FocalLength":4,"SubjectArea":[2015,1511,2217,1330],"UserComment":{"type":"Buffer","data":[65,83,67,73,73,0,0,0,67,104,114,105,115,116,111,112,104,101,114,32,67,111,108,108,105,110,115]},"SubSecTimeOriginal":"402","SubSecTimeDigitized":"402","FlashpixVersion":{"type":"Buffer","data":[48,49,48,48]},"PixelXDimension":3884,"PixelYDimension":2912,"SensingMethod":2,"SceneType":{"type":"Buffer","data":[1]},"ExposureMode":0,"WhiteBalance":0,"FocalLengthIn35mmFormat":28,"SceneCaptureType":0,"ImageUniqueID":"a0ab290cab6867e80000000000000000","LensSpecification":[4,6,1.8,2.4],"LensMake":"Apple","LensModel":"iPhone X back dual camera 4mm f/1.8"},"gps":{"GPSVersionID":[2,2,0,0],"GPSLatitudeRef":"\u0000","GPSLongitudeRef":"\u0000","GPSSpeedRef":"K","GPSSpeed":0.8394017814155031,"GPSImgDirectionRef":"\u0000","GPSDestBearingRef":"T","GPSDestBearing":273.0406646261478,"GPSHPositioningError":24},"by_line":["Chris Collins"],"copyright_notice":"Chris Collins","contact":"hello@chriscollins.me","keywords":["glasgow","scotland"],"tags":["glasgow","scotland"]} 3 | --- -------------------------------------------------------------------------------- /src/_exifdata/02_08_2019_101420.md: -------------------------------------------------------------------------------- 1 | ---json 2 | {"image_path":"02_08_2019_101420.jpg","image":{"ImageDescription":"Queens Park, Shawlands","Make":"Fujifilm","Model":"X-T10","Orientation":1,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"Software":"","Artist":"Christopher Collins","YCbCrSubSampling":[1,1],"Copyright":"Christopher Collins","ExifOffset":266,"GPSInfo":962},"thumbnail":{"Compression":6,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"ThumbnailOffset":1118,"ThumbnailLength":3913},"exif":{"ExposureTime":0.034482758620689655,"FNumber":0,"ExposureProgram":3,"ISO":800,"SensitivityType":1,"ExifVersion":{"type":"Buffer","data":[48,50,51,48]},"DateTimeOriginal":"2019-08-02T22:14:20.000Z","DateTimeDigitized":"2019-08-02T22:14:20.000Z","ComponentsConfiguration":{"type":"Buffer","data":[1,2,3,0]},"CompressedBitsPerPixel":3.2,"ShutterSpeedValue":4.95,"ApertureValue":0,"BrightnessValue":-3.05,"ExposureBiasValue":0,"MaxApertureValue":0,"MeteringMode":3,"LightSource":0,"Flash":16,"FocalLength":21,"UserComment":{"type":"Buffer","data":[65,83,67,73,73,0,0,0,67,104,114,105,115,116,111,112,104,101,114,32,67,111,108,108,105,110,115]},"FlashpixVersion":{"type":"Buffer","data":[48,49,48,48]},"ColorSpace":1,"PixelXDimension":1776,"PixelYDimension":1184,"FocalPlaneXResolution":2092,"FocalPlaneYResolution":2092,"FocalPlaneResolutionUnit":3,"SensingMethod":2,"FileSource":{"type":"Buffer","data":[3]},"SceneType":{"type":"Buffer","data":[1]},"CustomRendered":0,"ExposureMode":0,"WhiteBalance":0,"FocalLengthIn35mmFormat":32,"SceneCaptureType":0,"Sharpness":2,"SubjectDistanceRange":0,"ImageUniqueID":"b4bc7209347891250000000000000000","LensSpecification":[21,21,0,0]},"gps":{"GPSVersionID":[2,2,0,0],"GPSLatitudeRef":"\u0000","GPSLongitudeRef":"\u0000","GPSDOP":65},"date_created":"20190802","time_created":"221420+0000","digital_date_created":"20190802","digital_time_created":"221420+0000","by_line":["Chris Collins"],"copyright_notice":"Chris Collins","contact":"hello@chriscollins.me","keywords":["glasgow","scotland"],"date_time":"2019-09-02T22:14:20.000Z","tags":["glasgow","scotland"]} 3 | --- -------------------------------------------------------------------------------- /src/_exifdata/02_01_2019_094929.md: -------------------------------------------------------------------------------- 1 | ---json 2 | {"image_path":"02_01_2019_094929.jpg","image":{"ImageDescription":"Jack on his first Cobbler trip","Make":"FUJIFILM","Model":"X100F","Orientation":1,"XResolution":240,"YResolution":240,"ResolutionUnit":2,"Software":"","ModifyDate":"2019-01-05T19:49:07.000Z","Artist":"Christopher Collins","YCbCrPositioning":1,"Copyright":"Christopher Collins","ExifOffset":306,"GPSInfo":938},"thumbnail":{"Compression":6,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"ThumbnailOffset":1130,"ThumbnailLength":4969},"exif":{"ExposureTime":0.00625,"FNumber":2.8,"ExposureProgram":2,"ISO":400,"SensitivityType":1,"ExifVersion":{"type":"Buffer","data":[48,50,51,48]},"DateTimeOriginal":"2019-01-02T09:49:29.000Z","DateTimeDigitized":"2019-01-02T09:49:29.000Z","ComponentsConfiguration":{"type":"Buffer","data":[1,2,3,0]},"ShutterSpeedValue":7.321927997619756,"ApertureValue":2.9708539982335758,"BrightnessValue":6.27,"ExposureBiasValue":-0.67,"MaxApertureValue":2,"MeteringMode":5,"LightSource":0,"Flash":0,"FocalLength":23,"UserComment":{"type":"Buffer","data":[65,83,67,73,73,0,0,0,67,104,114,105,115,116,111,112,104,101,114,32,67,111,108,108,105,110,115]},"FlashpixVersion":{"type":"Buffer","data":[48,49,48,48]},"ColorSpace":1,"PixelXDimension":4095,"PixelYDimension":2730,"FocalPlaneXResolution":2553.2146017699115,"FocalPlaneYResolution":2553.2146017699115,"FocalPlaneResolutionUnit":3,"SensingMethod":2,"FileSource":{"type":"Buffer","data":[3]},"SceneType":{"type":"Buffer","data":[1]},"CustomRendered":0,"ExposureMode":0,"WhiteBalance":0,"SceneCaptureType":0,"Sharpness":2,"SubjectDistanceRange":0,"ImageUniqueID":"d680e424f96cd3ef0000000000000000","BodySerialNumber":""},"gps":{"GPSVersionID":[2,2,0,0],"GPSLatitudeRef":"\u0000","GPSLongitudeRef":"\u0000","GPSTimeStamp":[20,32,12.28],"GPSDOP":65},"by_line":["Chris Collins"],"digital_date_created":"20190102","digital_time_created":"094929","date_created":"20190102","time_created":"094929","copyright_notice":"Chris Collins","contact":"hello@chriscollins.me","keywords":["mountains","scotland"],"date_time":"2019-02-02T09:49:29.000Z","tags":["mountains","scotland"]} 3 | --- -------------------------------------------------------------------------------- /src/_includes/layouts/photo.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "layouts/base.njk" 3 | --- 4 | 5 | {# 6 | Stolen from: https://github.com/duncan/website/blob/master/content/_includes/post.njk 7 | Thanks :) 8 | #} 9 | {%- set currentItemIndex = 0 %} 10 | {%- for item in collections.exifPhotos %} 11 | {%- if item.url == page.url %} 12 | {%- set currentItemIndex = loop.index0 %} 13 | {%- endif %} 14 | {%- endfor %} 15 | 16 | {%- set prevItemIndex = currentItemIndex - 1 %} 17 | {%- set nextItemIndex = currentItemIndex + 1 %} 18 | 19 |
20 | Back to photos 21 | 22 |

{{ image.ImageDescription }}

23 | 24 | 25 |
26 | 27 |
28 | {{ image.ImageDescription }} 29 |
30 |

Taken using a {{ image.Make }} {{ image.Model }} ISO: {{ exif.ISO }} ƒ/{{exif.FNumber }}

31 |
32 |
33 | 34 |

Available meta tags

35 |

Some stuff isn't usable and needs some work to become usable.

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {%- for name, item in image %} 44 | 45 | 46 | 47 | 48 | {%- endfor %} 49 | 50 | {%- for name, item in exif %} 51 | 52 | 53 | 54 | 55 | {%- endfor %} 56 | 57 | {% for name, item in thumbnail %} 58 | 59 | 60 | 61 | 62 | {%- endfor %} 63 |
KeyValue
image.{{ name }}{{ item }}
exif.{{ name }}{{ item }}
thumbnail.{{ name }}{{ item }}
64 | 65 | 76 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | dist 107 | 108 | .DS_Store -------------------------------------------------------------------------------- /src/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | pagination: 4 | data: collections.exifPhotos 5 | size: 4 6 | alias: photoList 7 | --- 8 | 9 |
10 |

JAMStack Photo site

11 |

An example site generated using exif data extracted from photos

12 | 13 | {# This should be a ul :| #} 14 | {% for tag in collections.tagList %} 15 | {% set tagUrl %}/tags/{{ tag | slug }}/{% endset %} 16 | #{{ tag | slug }}, 17 | {% endfor %} 18 | 19 |

{{ collections.exifPhotos.length }} photos

20 | 21 | 22 | 46 | 47 | 60 | 61 | 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /.eleventy.js: -------------------------------------------------------------------------------- 1 | const { 2 | DateTime 3 | } = require("luxon"); 4 | 5 | module.exports = function (eleventyConfig) { 6 | // Folders to copy to build dir (See. 1.1) 7 | eleventyConfig.addPassthroughCopy("src/photos"); 8 | 9 | // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string 10 | eleventyConfig.addFilter('htmlDateString', (dateObj) => { 11 | return DateTime.fromJSDate(new Date(dateObj), { 12 | zone: 'utc' 13 | }).toFormat('dd/LL/yyyy'); 14 | }); 15 | 16 | eleventyConfig.addFilter('dateUrl', (dateObj) => { 17 | return DateTime.fromJSDate(new Date(dateObj), { 18 | zone: 'utc' 19 | }).toFormat('dd-LL-yyyy-mmssms'); 20 | }); 21 | 22 | eleventyConfig.addFilter("readableDate", dateObj => { 23 | return DateTime.fromJSDate(new Date(dateObj), { 24 | zone: 'utc' 25 | }).toFormat("dd.MM.yy"); 26 | }); 27 | 28 | eleventyConfig.addCollection("exifPhotos", function (collection) { 29 | return collection.getFilteredByGlob("src/_exifdata/*.md").sort(function (a, b) { 30 | return new Date(b.data.exif.DateTimeOriginal) - new Date(a.data.exif.DateTimeOriginal); 31 | }); 32 | }); 33 | 34 | eleventyConfig.addCollection("tagList", function(collection) { 35 | let tagSet = new Set(); 36 | collection.getAll().forEach(function(item) { 37 | if( "tags" in item.data ) { 38 | let tags = item.data.tags; 39 | 40 | tags = tags.filter(function(item) { 41 | switch(item) { 42 | // this list should match the `filter` list in tags.njk 43 | case "all": 44 | case "nav": 45 | case "post": 46 | case "posts": 47 | return false; 48 | } 49 | 50 | return true; 51 | }); 52 | 53 | for (const tag of tags) { 54 | tagSet.add(tag); 55 | } 56 | } 57 | }); 58 | 59 | // returning an array in addCollection works in Eleventy 0.5.3 60 | return [...tagSet]; 61 | }); 62 | 63 | 64 | return { 65 | dir: { 66 | input: "src/", 67 | output: "dist", 68 | includes: "_includes" 69 | }, 70 | templateFormats: ["html", "md", "njk", "yml"], 71 | htmlTemplateEngine: "njk", 72 | 73 | // 1.1 Enable eleventy to pass dirs specified above 74 | passthroughFileCopy: true 75 | }; 76 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JAMStack Photo Website 2 | 3 | Demo sites: 4 | 5 | - [jamstack-photo-website.netlify.com](https://jamstack-photo-website.netlify.com) - This project deploying to Netlify 6 | - [scottishstoater.com](https://www.scottishstoater.com) 7 | 8 | ## What is this? 9 | 10 | This is a proof-of-concept workflow that extracts EXIF data embeded in photos to generate a static website using [Eleventy](https://11ty.dev). 11 | 12 | There is a few reasons behind exploring this approach: 13 | 14 | - Treat your photos as content by utilising and extracting the information they already hold. 15 | - Speed of maintaining your website. Drop in a photo and go. 16 | - Owning your data. Easily self host and control your own photo gallery. 17 | 18 | ## A note on privacy 19 | 20 | I'm an advocate for protecting users privacy and this project is all about owning your own data. As pointed out by [@zachleath in this tweet](https://twitter.com/eleven_ty/status/1209543773425942529?s=20) EXIF data can hold a lot of personally identifiable information - it's up to you what you display but only what you choose to display is included in the site. All photo files are stripped of embedded meta data during the build. 21 | 22 | ## What's the script doing? 23 | 24 | 1. Looks for photos in the top level of a specified folder (eg. src/_photos) 25 | 2. Resizes and optimises each photo based on widths the user has supplied and outputs the results into a seperate specified folder (eg. src/photos/w???/*.jpg). 26 | 3. Rename each resized photo to a standard format based on it's date (dd_LL_yyyy_hhmmss) 27 | 4. Extract the EXIF data and IPTC data and creates a file in `src/_exifdata` for Eleventy to pick up as part of a collection, named (dd_LL_yyyy_hhmmss). 28 | _This last step was originially piped into Eleventy using a data file. That approach still works but you don't get text based version control of the exif data._ 29 | 30 | ## Photo requirements 31 | 32 | Each source photo must have an EXIF value for `DateTimeOriginal` - anything else is at your disposal. 33 | 34 | ## Getting started 35 | 36 | 1. Install dependencies 37 | 38 | ``` 39 | npm install 40 | ``` 41 | 42 | 2. Drop a few photos that meet the [minimal requirements](#Photo-requirements) in `src/_photos`. 43 | 44 | 3. Start the development server 45 | This step runs `node-exif-photos` then `eleventy`. 46 | 47 | ``` 48 | npm start 49 | ``` 50 | 51 | ### Generate a build 52 | 53 | ``` 54 | npm run build 55 | ``` 56 | 57 | ## Known Limitations 58 | 59 | - Only JPG support. 60 | - Not commiting the resized files will regenerate them on a pipeline, with a lot of photos this will take a while. Commiting the files solves this. 61 | - Adding lots of files at once will take a long time, try adding a max of 20 at a time. 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/_includes/layouts/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% if title %}{{ title }}{% else %}Photos{% endif %} 9 | {% if description %} 10 | {% endif %} 11 | 102 | 103 | 104 | 105 | {{ content | safe }} 106 | 107 | 153 | 154 | -------------------------------------------------------------------------------- /src/_exifdata/14_07_2019_100357.md: -------------------------------------------------------------------------------- 1 | ---json 2 | {"image_path":"14_07_2019_100357.JPG","image":{"ImageDescription":"SEC Armadillo ","Make":"FUJIFILM","Model":"X100F","Orientation":1,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"Software":"","ModifyDate":"2019-07-14T10:03:57.000Z","Artist":"Christopher Collins","YCbCrSubSampling":[2,1],"YCbCrPositioning":2,"Copyright":"Christopher Collins","ExifOffset":408,"PrintIM":{"type":"Buffer","data":[80,114,105,110,116,73,77,0,48,50,53,48,0,0,3,0,2,0,1,0,0,0,3,0,34,0,0,0,1,1,0,0,0,0,9,17,0,0,16,39,0,0,11,15,0,0,16,39,0,0,151,5,0,0,16,39,0,0,176,8,0,0,16,39,0,0,1,28,0,0,16,39,0,0,94,2,0,0,16,39,0,0,139,0,0,0,16,39,0,0,203,3,0,0,16,39,0,0,229,27,0,0,16,39,0,0]}},"thumbnail":{"Compression":6,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"ThumbnailOffset":2112,"ThumbnailLength":4128},"exif":{"ExposureTime":0.00025,"FNumber":2,"ExposureProgram":3,"ISO":400,"SensitivityType":1,"ExifVersion":{"type":"Buffer","data":[48,50,51,48]},"DateTimeOriginal":"2019-07-14T10:03:57.000Z","DateTimeDigitized":"2019-07-14T10:03:57.000Z","ComponentsConfiguration":{"type":"Buffer","data":[1,2,3,0]},"CompressedBitsPerPixel":4.8,"ShutterSpeedValue":12.05,"ApertureValue":2,"BrightnessValue":10.29,"ExposureBiasValue":0,"MaxApertureValue":2,"MeteringMode":1,"LightSource":0,"Flash":0,"FocalLength":23,"MakerNote":{"type":"Buffer","data":[70,85,74,73,70,73,76,77,12,0,0,0,53,0,0,0,7,0,4,0,0,0,48,49,51,48,16,0,2,0,48,0,0,0,142,2,0,0,0,16,2,0,8,0,0,0,190,2,0,0,1,16,3,0,1,0,0,0,132,0,0,0,2,16,3,0,1,0,0,0,0,0,0,0,3,16,3,0,1,0,0,0,128,1,0,0,10,16,9,0,2,0,0,0,198,2,0,0,11,16,3,0,1,0,0,0,0,1,0,0,14,16,3,0,1,0,0,0,0,0,0,0,16,16,3,0,1,0,0,0,0,128,0,0,17,16,10,0,1,0,0,0,206,2,0,0,33,16,3,0,1,0,0,0,0,0,0,0,34,16,3,0,1,0,0,0,0,1,0,0,35,16,3,0,2,0,0,0,21,10,209,7,38,16,3,0,1,0,0,0,64,0,0,0,43,16,3,0,1,0,0,0,17,0,0,0,44,16,4,0,1,0,0,0,1,0,2,0,45,16,4,0,1,0,0,0,1,1,0,0,46,16,4,0,1,0,0,0,2,1,0,0,48,16,3,0,1,0,0,0,0,0,0,0,49,16,3,0,1,0,0,0,0,1,0,0,50,16,3,0,1,0,0,0,1,0,0,0,64,16,9,0,1,0,0,0,224,255,255,255,65,16,9,0,1,0,0,0,16,0,0,0,68,16,4,0,1,0,0,0,0,0,0,0,69,16,4,0,1,0,0,0,1,0,0,0,70,16,4,0,1,0,0,0,1,0,0,0,71,16,9,0,1,0,0,0,0,0,0,0,80,16,3,0,1,0,0,0,1,0,0,0,0,17,3,0,1,0,0,0,0,0,0,0,1,17,3,0,1,0,0,0,0,0,0,0,3,17,4,0,1,0,0,0,0,0,0,0,0,18,3,0,1,0,0,0,0,0,0,0,0,19,3,0,1,0,0,0,0,0,0,0,1,19,3,0,1,0,0,0,0,0,0,0,2,19,3,0,1,0,0,0,0,0,0,0,3,19,3,0,1,0,0,0,0,0,0,0,4,19,3,0,1,0,0,0,0,0,0,0,5,19,3,0,1,0,0,0,0,0,0,0,0,20,3,0,1,0,0,0,1,0,0,0,1,20,3,0,1,0,0,0,0,6,0,0,2,20,3,0,1,0,0,0,0,0,0,0,8,20,7,0,4,0,0,0,48,51,48,48,9,20,7,0,4,0,0,0,48,49,48,48,10,20,3,0,1,0,0,0,0,0,0,0,11,20,3,0,1,0,0,0,200,0,0,0,48,20,7,0,129,0,0,0,214,2,0,0,49,20,4,0,1,0,0,0,0,0,0,0,54,20,3,0,1,0,0,0,0,0,0,0,56,20,3,0,1,0,0,0,33,140,0,0,57,20,2,0,48,0,0,0,88,3,0,0,0,65,3,0,1,0,0,0,0,0,0,0,0,66,3,0,1,0,0,0,0,0,0,0,0,0,0,0,70,70,68,84,50,51,57,50,49,54,48,54,32,32,32,32,32,53,57,51,53,51,50,51,50,51,55,51,52,49,55,48,56,49,56,55,66,57,48,51,48,49,49,65,53,48,51,0,70,73,78,69,32,32,32,0,0,0,0,0,0,0,0,0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"UserComment":{"type":"Buffer","data":[65,83,67,73,73,0,0,0,67,104,114,105,115,116,111,112,104,101,114,32,67,111,108,108,105,110,115]},"FlashpixVersion":{"type":"Buffer","data":[48,49,48,48]},"ColorSpace":1,"PixelXDimension":4898,"PixelYDimension":3265,"InteropOffset":1988,"FocalPlaneXResolution":2564,"FocalPlaneYResolution":2564,"FocalPlaneResolutionUnit":3,"SensingMethod":2,"FileSource":{"type":"Buffer","data":[3]},"SceneType":{"type":"Buffer","data":[1]},"CustomRendered":0,"ExposureMode":0,"WhiteBalance":0,"SceneCaptureType":0,"Sharpness":2,"SubjectDistanceRange":0,"ImageUniqueID":"6da5cebf8271bf330000000000000000","BodySerialNumber":""},"by_line":["Chris Collins"],"copyright_notice":"Chris Collins","contact":"hello@chriscollins.me","keywords":["glasgow","scotland"],"tags":["glasgow","scotland"]} 3 | --- -------------------------------------------------------------------------------- /src/_exifdata/05_11_2019_084102.md: -------------------------------------------------------------------------------- 1 | ---json 2 | {"image_path":"05_11_2019_084102.jpg","image":{"ImageDescription":"Clyde Arc","Make":"Apple","Model":"iPhone X","Orientation":1,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"Software":"","ModifyDate":"2019-11-05T08:41:02.000Z","Artist":"Christopher Collins","TileWidth":512,"TileLength":512,"YCbCrSubSampling":[1,1],"Copyright":"Christopher Collins","ExifOffset":308,"GPSInfo":2128},"thumbnail":{"Compression":6,"XResolution":72,"YResolution":72,"ResolutionUnit":2,"ThumbnailOffset":2284,"ThumbnailLength":5644},"exif":{"ExposureTime":0.0012919896640826874,"FNumber":1.8,"ExposureProgram":2,"ISO":20,"ExifVersion":{"type":"Buffer","data":[48,50,51,49]},"DateTimeOriginal":"2019-11-05T08:41:02.000Z","DateTimeDigitized":"2019-11-05T08:41:02.000Z","ComponentsConfiguration":{"type":"Buffer","data":[1,2,3,0]},"ShutterSpeedValue":9.59617820945946,"ApertureValue":1.6959938128383605,"BrightnessValue":9.04413185869215,"ExposureBiasValue":0,"MeteringMode":3,"Flash":24,"FocalLength":4,"SubjectArea":[2722,1808,753,756],"MakerNote":{"type":"Buffer","data":[65,112,112,108,101,32,105,79,83,0,0,1,77,77,0,25,0,1,0,9,0,0,0,1,0,0,0,11,0,2,0,7,0,0,2,46,0,0,1,64,0,3,0,7,0,0,0,104,0,0,3,110,0,4,0,9,0,0,0,1,0,0,0,1,0,5,0,9,0,0,0,1,0,0,0,181,0,6,0,9,0,0,0,1,0,0,0,190,0,7,0,9,0,0,0,1,0,0,0,1,0,8,0,10,0,0,0,3,0,0,3,214,0,12,0,10,0,0,0,2,0,0,3,238,0,13,0,9,0,0,0,1,0,0,0,20,0,14,0,9,0,0,0,1,0,0,0,4,0,16,0,9,0,0,0,1,0,0,0,1,0,17,0,2,0,0,0,37,0,0,3,254,0,20,0,9,0,0,0,1,0,0,0,1,0,23,0,9,0,0,0,1,0,0,32,0,0,25,0,9,0,0,0,1,0,0,0,0,0,26,0,2,0,0,0,6,0,0,4,36,0,31,0,9,0,0,0,1,0,0,0,0,0,32,0,2,0,0,0,37,0,0,4,42,0,33,0,10,0,0,0,1,0,0,4,80,0,35,0,9,0,0,0,2,0,0,4,88,0,37,0,9,0,0,0,1,0,0,0,0,0,38,0,9,0,0,0,1,0,0,0,0,0,39,0,10,0,0,0,1,0,0,4,96,0,43,0,2,0,0,0,37,0,0,4,104,0,0,0,0,98,112,108,105,115,116,48,48,79,17,2,0,125,0,131,0,141,0,158,0,175,0,188,0,192,0,194,0,196,0,193,0,188,0,180,0,182,0,189,0,199,0,204,0,120,0,124,0,130,0,145,0,155,0,161,0,166,0,178,0,177,0,167,0,162,0,160,0,162,0,183,0,222,0,244,0,121,0,127,0,132,0,139,0,143,0,145,0,151,0,162,0,157,0,149,0,144,0,146,0,159,0,178,0,197,0,210,0,142,0,146,0,144,0,141,0,142,0,146,0,143,0,142,0,145,0,143,0,142,0,157,0,178,0,202,0,208,0,212,0,194,0,177,0,160,0,144,0,140,0,144,0,150,0,149,0,144,0,135,0,141,0,168,0,212,0,1,1,46,1,19,1,21,1,21,1,1,1,229,0,200,0,196,0,196,0,187,0,166,0,164,0,176,0,177,0,221,0,17,1,19,1,245,0,209,0,0,1,62,1,104,1,117,1,100,1,184,0,18,1,230,0,227,0,230,0,47,1,176,1,99,1,242,0,212,0,131,0,152,0,185,0,202,0,230,0,89,1,174,0,166,1,122,1,73,1,245,0,198,1,189,1,102,1,74,1,103,1,130,0,148,0,127,0,137,0,160,0,181,0,129,0,53,1,28,2,17,2,58,1,156,1,45,1,105,1,107,1,125,1,56,0,68,0,74,0,79,0,95,0,173,0,86,0,166,0,183,0,166,0,117,0,224,0,169,0,138,0,219,0,96,1,25,0,20,0,27,0,33,0,36,0,21,0,24,0,23,0,20,0,19,0,25,0,30,0,36,0,27,0,67,0,63,0,13,0,13,0,19,0,27,0,40,0,44,0,49,0,100,0,126,0,120,0,49,0,92,0,58,0,29,0,76,0,135,0,30,0,38,0,51,0,60,0,67,0,116,0,66,0,177,0,241,0,225,0,125,0,246,0,242,0,158,0,50,0,106,0,61,0,76,0,91,0,99,0,112,0,131,0,54,0,60,0,82,0,83,0,70,0,142,0,134,0,85,0,27,0,70,0,77,0,85,0,95,0,93,0,88,0,79,0,37,0,21,0,36,0,35,0,29,0,19,0,25,0,42,0,25,0,33,0,65,0,61,0,52,0,47,0,42,0,42,0,24,0,29,0,37,0,36,0,39,0,36,0,28,0,19,0,20,0,23,0,0,8,0,0,0,0,0,0,2,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,12,98,112,108,105,115,116,48,48,212,1,2,3,4,5,6,7,8,85,102,108,97,103,115,85,118,97,108,117,101,89,116,105,109,101,115,99,97,108,101,85,101,112,111,99,104,16,1,19,0,3,118,182,74,132,253,254,18,59,154,202,0,16,0,8,17,23,29,39,45,47,56,61,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,63,255,255,147,23,0,0,109,156,255,255,247,228,0,13,158,35,0,0,92,142,0,3,203,11,0,0,12,71,0,0,0,128,0,0,49,89,0,0,1,0,53,49,49,52,66,66,54,53,45,69,56,56,66,45,52,68,48,70,45,56,69,55,70,45,48,70,65,68,57,70,65,56,52,69,69,52,0,0,113,56,50,53,115,0,49,54,67,55,53,50,51,51,45,49,66,68,49,45,52,53,51,53,45,56,54,52,68,45,65,66,54,66,49,56,55,51,65,49,66,68,0,0,0,0,0,0,0,0,0,1,0,0,1,233,32,0,0,138,0,0,0,0,0,0,0,1,50,68,53,49,53,66,53,67,45,54,65,56,49,45,52,57,51,50,45,57,68,69,55,45,52,70,70,54,53,68,50,50,70,54,68,52,0,0]},"UserComment":{"type":"Buffer","data":[65,83,67,73,73,0,0,0,67,104,114,105,115,116,111,112,104,101,114,32,67,111,108,108,105,110,115]},"SubSecTimeOriginal":"491","SubSecTimeDigitized":"491","FlashpixVersion":{"type":"Buffer","data":[48,49,48,48]},"ColorSpace":1,"PixelXDimension":3654,"PixelYDimension":3024,"SensingMethod":2,"SceneType":{"type":"Buffer","data":[1]},"ExposureMode":0,"WhiteBalance":0,"FocalLengthIn35mmFormat":28,"SceneCaptureType":0,"ImageUniqueID":"492b5f6cd678db840000000000000000","LensSpecification":[4,6,1.8,2.4],"LensMake":"Apple","LensModel":"iPhone X back dual camera 4mm f/1.8"},"gps":{"GPSVersionID":[2,2,0,0],"GPSLatitudeRef":"\u0000","GPSLongitudeRef":"\u0000","GPSDOP":1},"date_created":"20191105","time_created":"084102+0000","digital_date_created":"20191105","digital_time_created":"084102+0000","by_line":["Chris Collins"],"copyright_notice":"Chris Collins","contact":"hello@chriscollins.me","keywords":["glasgow","scotland"],"date_time":"2019-12-05T08:41:02.000Z","tags":["glasgow","scotland"]} 3 | --- --------------------------------------------------------------------------------