├── 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 | {{ exif.DateTimeOriginal | readableDate }}
24 |
25 |
26 |
27 |
28 |
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 | Key
40 | Value
41 |
42 |
43 | {%- for name, item in image %}
44 |
45 | image.{{ name }}
46 | {{ item }}
47 |
48 | {%- endfor %}
49 |
50 | {%- for name, item in exif %}
51 |
52 | exif.{{ name }}
53 | {{ item }}
54 |
55 | {%- endfor %}
56 |
57 | {% for name, item in thumbnail %}
58 |
59 | thumbnail.{{ name }}
60 | {{ item }}
61 |
62 | {%- endfor %}
63 |
64 |
65 |
66 | {%- if prevItemIndex >= 0 %}
67 | {%- set prev = collections.exifPhotos[prevItemIndex] %}
68 | Previous Photo
69 | {%- endif %}
70 |
71 | {%- if nextItemIndex < collections.exifPhotos.length %}
72 | {%- set next = collections.exifPhotos[nextItemIndex] %}
73 | Next Photo
74 | {%- endif %}
75 |
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 |
48 |
49 | {% if pagination.href.previous %}
50 | Previous
51 | {% endif %}
52 | {%- for pageEntry in pagination.pages %}
53 | {{ loop.index }}
54 | {%- endfor %}
55 | {% if pagination.href.next %}
56 | Next
57 | {% endif %}
58 |
59 |
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 | ---
--------------------------------------------------------------------------------