mirror of
https://github.com/kremalicious/blog.git
synced 2024-11-15 09:35:21 +01:00
Merge pull request #182 from kremalicious/feature/exif
Refactor EXIF & IPTC extraction
This commit is contained in:
commit
545bb59c7a
@ -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 |
BIN
content/photos/2019-11-02-orszaghaz-i.jpg
Normal file
BIN
content/photos/2019-11-02-orszaghaz-i.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 MiB |
9
content/photos/2019-11-02-orszaghaz-i.md
Normal file
9
content/photos/2019-11-02-orszaghaz-i.md
Normal 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.
|
BIN
content/photos/2019-11-03-orszaghaz-ii.jpg
Normal file
BIN
content/photos/2019-11-03-orszaghaz-ii.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 MiB |
9
content/photos/2019-11-03-orszaghaz-ii.md
Normal file
9
content/photos/2019-11-03-orszaghaz-ii.md
Normal 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.
|
@ -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
143
gatsby/createExif.js
Normal 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
|
||||
}
|
@ -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 }
|
||||
}
|
||||
})
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
||||
|
27
package.json
27
package.json
@ -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
11
src/@types/Image.d.ts
vendored
@ -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
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { render } from '@testing-library/react'
|
||||
import Exif from './Exif'
|
||||
|
||||
const exif = {
|
||||
formatted: {
|
||||
iso: '500',
|
||||
model: 'Canon',
|
||||
fstop: '7.2',
|
||||
@ -13,6 +14,7 @@ const exif = {
|
||||
exposure: '200',
|
||||
gps: { latitude: '41.89007222222222', longitude: '12.491516666666666' }
|
||||
}
|
||||
}
|
||||
|
||||
describe('Exif', () => {
|
||||
it('renders without crashing', () => {
|
||||
|
@ -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>
|
||||
|
@ -79,6 +79,7 @@ export const pageQuery = graphql`
|
||||
}
|
||||
fields {
|
||||
exif {
|
||||
formatted {
|
||||
iso
|
||||
model
|
||||
fstop
|
||||
@ -93,6 +94,7 @@ export const pageQuery = graphql`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
author
|
||||
updated
|
||||
tags
|
||||
|
Loading…
Reference in New Issue
Block a user