1
0
Fork 0

Merge pull request #182 from kremalicious/feature/exif

Refactor EXIF & IPTC extraction
This commit is contained in:
Matthias Kretschmann 2019-11-04 20:15:50 +01:00 committed by GitHub
commit 545bb59c7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 244 additions and 158 deletions

View File

@ -45,7 +45,7 @@ The whole [blog](https://kremalicious.com) is a React-based Single Page App buil
### 🎆 EXIF extraction
Automatically extracts EXIF metadata from my photos on build time. For minimal overhead, [fast-exif](https://github.com/titarenko/fast-exif) parses every JPG file upon Gatsby file node creation and adds the extracted EXIF data as node fields.
Automatically extracts EXIF & IPTC metadata from my photos on build time. For minimal overhead, [fast-exif](https://github.com/titarenko/fast-exif) & [node-iptc](https://github.com/derekbaron/node-iptc) parse every JPG file upon Gatsby file node creation and add the extracted data as node fields.
This way, EXIF data is only extracted at build time and can be simply queried with GraphQL at run time.
@ -56,7 +56,7 @@ In the end looks like this, including location display with [pigeon-maps](https:
If you want to know how this works, have a look at the respective component under
- [`src/components/atoms/Exif.jsx`](src/components/atoms/Exif.jsx)
- the EXIF node fields creation [`gatsby/createExifFields.js`](gatsby/createExifFields.js) running in [`gatsby-node.js`](gatsby-node.js)
- the EXIF node fields creation [`gatsby/createExif.js`](gatsby/createExif.js) running in [`gatsby-node.js`](gatsby-node.js)
### 💰 Cryptocurrency donation via Web3/MetaMask

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,9 @@
---
type: photo
date: 2019-11-02T13:18:59.000Z
title: Országház I
image: 2019-11-02-orszaghaz-i.jpg
---
The Hungarian Parliament Building seen from across the Danube in Budapest, Hungary.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

@ -0,0 +1,9 @@
---
type: photo
date: 2019-11-03T12:05:22.000Z
title: Országház II
image: 2019-11-03-orszaghaz-ii.jpg
---
Inside the Hungarian Parliament Building in Budapest, Hungary.

View File

@ -1,6 +1,6 @@
const webpack = require('webpack')
const { createMarkdownFields } = require('./gatsby/createMarkdownFields')
const { createExifFields } = require('./gatsby/createExifFields')
const { createExif } = require('./gatsby/createExif')
const {
generatePostPages,
generateTagPages,
@ -9,17 +9,15 @@ const {
const { generateJsonFeed } = require('./gatsby/feeds')
const { itemsPerPage } = require('./config')
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
exports.onCreateNode = ({ node, actions, getNode, createNodeId }) => {
// Markdown files
if (node.internal.type === 'MarkdownRemark') {
createMarkdownFields(node, createNodeField, getNode)
createMarkdownFields(node, actions, getNode)
}
// Image files
if (node.internal.mediaType === 'image/jpeg') {
createExifFields(node, createNodeField)
createExif(node, actions, createNodeId)
}
}

143
gatsby/createExif.js Normal file
View File

@ -0,0 +1,143 @@
const fs = require('fs')
const util = require('util')
const fastExif = require('fast-exif')
const Fraction = require('fraction.js')
const getCoordinates = require('dms2dec')
const iptc = require('node-iptc')
const readFile = util.promisify(fs.readFile)
exports.createExif = async (node, actions, createNodeId) => {
try {
// exif
const exifData = await fastExif.read(node.absolutePath, true)
if (!exifData) return
// iptc
const file = await readFile(node.absolutePath)
const iptcData = iptc(file)
createNodes(exifData, iptcData, node, actions, createNodeId)
} catch (error) {
console.error(`${node.name}: ${error.message}`)
}
}
function createNodes(exifData, iptcData, node, actions, createNodeId) {
const { createNodeField, createNode, createParentChildLink } = actions
const exifDataFormatted = formatExif(exifData)
const exif = {
...exifData,
iptc: {
...iptcData
},
formatted: {
...exifDataFormatted
}
}
const exifNode = {
id: createNodeId(`${node.id} >> ImageExif`),
children: [],
...exif,
parent: node.id,
internal: {
contentDigest: `${node.internal.contentDigest}`,
type: 'ImageExif'
}
}
// add exif fields to existing type file
createNodeField({
node,
name: 'exif',
value: exif
})
// create new nodes from all exif data
// allowing to be queried with imageExif & AllImageExif
createNode(exifNode)
createParentChildLink({
parent: node,
child: exifNode
})
}
function formatExif(exifData) {
if (!exifData.exif) return
const { Model } = exifData.image
const {
ISO,
FNumber,
ExposureTime,
FocalLength,
FocalLengthIn35mmFormat,
ExposureBiasValue,
ExposureMode,
LensModel
} = exifData.exif
const iso = `ISO ${ISO}`
const fstop = `ƒ ${FNumber}`
const focalLength = `${FocalLengthIn35mmFormat || FocalLength}mm`
// Shutter speed
const { n, d } = new Fraction(ExposureTime)
const shutterspeed = `${n}/${d}s`
// GPS
let latitude
let longitude
if (exifData.gps) ({ latitude, longitude } = formatGps(exifData.gps))
// Exposure
const exposure = formatExposure(ExposureBiasValue || ExposureMode)
return {
iso,
model: Model,
fstop,
shutterspeed,
focalLength,
lensModel: LensModel,
exposure,
gps: { latitude, longitude }
}
}
function formatGps(gpsData) {
if (!gpsData) return
const { GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude } = gpsData
const GPSdec = getCoordinates(
GPSLatitude,
GPSLatitudeRef,
GPSLongitude,
GPSLongitudeRef
)
const latitude = GPSdec[0]
const longitude = GPSdec[1]
return { latitude, longitude }
}
function formatExposure(exposureMode) {
if (!exposureMode) return
const exposureShortened = parseFloat(exposureMode.toFixed(2))
let exposure
if (exposureMode === 0) {
exposure = `+/- ${exposureShortened} ev`
} else if (exposureMode > 0) {
exposure = `+ ${exposureShortened} ev`
} else {
exposure = `${exposureShortened} ev`
}
return exposure
}

View File

@ -1,94 +0,0 @@
const fastExif = require('fast-exif')
const Fraction = require('fraction.js')
const dms2dec = require('dms2dec')
exports.createExifFields = async (node, createNodeField) => {
let exifData
try {
exifData = await fastExif.read(node.absolutePath, true)
if (!exifData) return
constructExifFields(exifData, createNodeField, node)
} catch (error) {
// console.error(`${node.name}: ${error.message}`)
return null // just silently fail when exif can't be extracted
}
}
const getGps = gpsData => {
if (!gpsData) return
const { GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude } = gpsData
const GPSdec = dms2dec(
GPSLatitude,
GPSLatitudeRef,
GPSLongitude,
GPSLongitudeRef
)
const latitude = GPSdec[0]
const longitude = GPSdec[1]
return { latitude, longitude }
}
const getExposure = exposureMode => {
const exposureShortened = parseFloat(exposureMode.toFixed(2))
let exposure
if (exposureMode === 0) {
exposure = `+/- ${exposureShortened} ev`
} else if (exposureMode > 0) {
exposure = `+ ${exposureShortened} ev`
} else {
exposure = `${exposureShortened} ev`
}
return exposure
}
const constructExifFields = (exifData, createNodeField, node) => {
const { Model } = exifData.image
const {
ISO,
FNumber,
ExposureTime,
FocalLength,
FocalLengthIn35mmFormat,
ExposureBiasValue,
ExposureMode,
LensModel
} = exifData.exif
const iso = `ISO ${ISO}`
const fstop = `ƒ ${FNumber}`
const focalLength = `${FocalLengthIn35mmFormat || FocalLength}mm`
// Shutter speed
const { n, d } = new Fraction(ExposureTime)
const shutterspeed = `${n}/${d}s`
// GPS
let latitude
let longitude
if (exifData.gps) ({ latitude, longitude } = getGps(exifData.gps))
// Exposure
const exposure = getExposure(ExposureBiasValue || ExposureMode)
// add exif fields to type File
createNodeField({
node,
name: 'exif',
value: {
iso,
model: Model,
fstop,
shutterspeed,
focalLength,
lensModel: LensModel,
exposure,
gps: { latitude, longitude }
}
})
}

View File

@ -2,6 +2,28 @@ const path = require('path')
const { createFilePath } = require('gatsby-source-filesystem')
const { repoContentPath } = require('../config')
// Create slug, date & github file link for posts from file path values
exports.createMarkdownFields = (node, actions, getNode) => {
const { createNodeField } = actions
const fileNode = getNode(node.parent)
const parsedFilePath = path.parse(fileNode.relativePath)
const slugOriginal = createFilePath({ node, getNode })
createSlug(node, createNodeField, slugOriginal, parsedFilePath)
createDate(node, createNodeField, slugOriginal)
// github file link
const type = fileNode.sourceInstanceName
const file = fileNode.relativePath
const githubLink = `${repoContentPath}/${type}/${file}`
createNodeField({
node,
name: 'githubLink',
value: githubLink
})
}
function createSlug(node, createNodeField, slugOriginal, parsedFilePath) {
let slug
@ -32,24 +54,3 @@ function createDate(node, createNodeField, slugOriginal) {
value: date
})
}
// Create slug, date & github file link for posts from file path values
exports.createMarkdownFields = (node, createNodeField, getNode) => {
const fileNode = getNode(node.parent)
const parsedFilePath = path.parse(fileNode.relativePath)
const slugOriginal = createFilePath({ node, getNode })
createSlug(node, createNodeField, slugOriginal, parsedFilePath)
createDate(node, createNodeField, slugOriginal)
// github file link
const type = fileNode.sourceInstanceName
const file = fileNode.relativePath
const githubLink = `${repoContentPath}/${type}/${file}`
createNodeField({
node,
name: 'githubLink',
value: githubLink
})
}

View File

@ -35,18 +35,18 @@
"dms2dec": "^1.1.0",
"fast-exif": "^1.0.1",
"fraction.js": "^4.0.12",
"gatsby": "^2.17.6",
"gatsby": "^2.17.7",
"gatsby-image": "^2.2.30",
"gatsby-plugin-catch-links": "^2.1.15",
"gatsby-plugin-feed": "^2.3.19",
"gatsby-plugin-lunr": "^1.5.2",
"gatsby-plugin-manifest": "^2.2.25",
"gatsby-plugin-manifest": "^2.2.26",
"gatsby-plugin-matomo": "^0.7.2",
"gatsby-plugin-meta-redirect": "^1.1.1",
"gatsby-plugin-offline": "^3.0.17",
"gatsby-plugin-react-helmet": "^3.1.13",
"gatsby-plugin-sass": "^2.1.20",
"gatsby-plugin-sharp": "^2.2.34",
"gatsby-plugin-sharp": "^2.2.36",
"gatsby-plugin-sitemap": "^2.2.19",
"gatsby-plugin-svgr": "^2.0.2",
"gatsby-plugin-typescript": "^2.1.15",
@ -57,11 +57,11 @@
"gatsby-remark-copy-linked-files": "^2.1.28",
"gatsby-remark-images": "^3.1.28",
"gatsby-remark-smartypants": "^2.1.14",
"gatsby-remark-vscode": "^1.2.0",
"gatsby-remark-vscode": "^1.3.0",
"gatsby-source-filesystem": "^2.1.35",
"gatsby-source-graphql": "^2.1.21",
"gatsby-transformer-remark": "^2.6.32",
"gatsby-transformer-sharp": "^2.3.1",
"gatsby-transformer-sharp": "^2.3.2",
"graphql": "^14.5.8",
"intersection-observer": "^0.7.0",
"js-scrypt": "^0.2.0",
@ -79,7 +79,7 @@
"react-transition-group": "^4.3.0",
"remark": "^11.0.1",
"remark-react": "^6.0.0",
"slugify": "^1.3.4",
"slugify": "^1.3.6",
"use-dark-mode": "^2.3.1",
"web3": "^1.2.2"
},
@ -88,21 +88,20 @@
"@babel/preset-env": "^7.6.3",
"@babel/preset-typescript": "^7.6.0",
"@svgr/webpack": "^4.3.3",
"@testing-library/jest-dom": "^4.2.0",
"@testing-library/react": "^9.3.0",
"@testing-library/jest-dom": "^4.2.3",
"@testing-library/react": "^9.3.2",
"@types/classnames": "^2.2.9",
"@types/jest": "^24.0.20",
"@types/jest": "^24.0.21",
"@types/lunr": "^2.3.2",
"@types/node": "^12.11.7",
"@types/node": "^12.12.5",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.3",
"@types/react-helmet": "^5.0.13",
"@types/react-helmet": "^5.0.14",
"@types/react-modal": "^3.10.0",
"@types/react-transition-group": "^4.2.3",
"@types/shortid": "0.0.29",
"@types/web3": "^1.0.20",
"@typescript-eslint/eslint-plugin": "^2.6.0",
"@typescript-eslint/parser": "^2.6.0",
"@typescript-eslint/eslint-plugin": "^2.6.1",
"@typescript-eslint/parser": "^2.6.1",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"eslint": "^6.6.0",

11
src/@types/Image.d.ts vendored
View File

@ -15,7 +15,7 @@ export interface ImageNode {
}
}
export interface Exif {
export interface ExifFormatted {
iso: string
model: string
fstop: string
@ -28,3 +28,12 @@ export interface Exif {
longitude: string
}
}
export interface Exif {
formatted?: ExifFormatted
exif?: any
image?: any
thumbnail?: any
gps?: any
iptc?: any
}

View File

@ -4,14 +4,16 @@ import { render } from '@testing-library/react'
import Exif from './Exif'
const exif = {
iso: '500',
model: 'Canon',
fstop: '7.2',
shutterspeed: '200',
focalLength: '200',
lensModel: 'Hello',
exposure: '200',
gps: { latitude: '41.89007222222222', longitude: '12.491516666666666' }
formatted: {
iso: '500',
model: 'Canon',
fstop: '7.2',
shutterspeed: '200',
focalLength: '200',
lensModel: 'Hello',
exposure: '200',
gps: { latitude: '41.89007222222222', longitude: '12.491516666666666' }
}
}
describe('Exif', () => {

View File

@ -4,7 +4,15 @@ import styles from './Exif.module.scss'
import { Exif as ExifMeta } from '../../@types/Image'
export default function Exif({ exif }: { exif: ExifMeta }) {
const { iso, model, fstop, shutterspeed, focalLength, exposure, gps } = exif
const {
iso,
model,
fstop,
shutterspeed,
focalLength,
exposure,
gps
} = exif.formatted
return (
<aside className={styles.exif}>
@ -16,7 +24,7 @@ export default function Exif({ exif }: { exif: ExifMeta }) {
{exposure && <span title="Exposure">{exposure}</span>}
{iso && <span title="ISO">{iso}</span>}
</div>
{gps.latitude && (
{gps && gps.latitude && (
<div className={styles.map}>
<ExifMap gps={gps} />
</div>

View File

@ -79,16 +79,18 @@ export const pageQuery = graphql`
}
fields {
exif {
iso
model
fstop
shutterspeed
focalLength
lensModel
exposure
gps {
latitude
longitude
formatted {
iso
model
fstop
shutterspeed
focalLength
lensModel
exposure
gps {
latitude
longitude
}
}
}
}