mirror of
https://github.com/kremalicious/blog.git
synced 2024-12-22 17:23:50 +01:00
refactor exif
* add all exif & IPTC data to files * create nodes for all exif data
This commit is contained in:
parent
74ccd935a0
commit
acfdba0a66
@ -45,7 +45,7 @@ The whole [blog](https://kremalicious.com) is a React-based Single Page App buil
|
|||||||
|
|
||||||
### 🎆 EXIF extraction
|
### 🎆 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.
|
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
|
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)
|
- [`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
|
### 💰 Cryptocurrency donation via Web3/MetaMask
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 24 KiB |
@ -1,6 +1,6 @@
|
|||||||
const webpack = require('webpack')
|
const webpack = require('webpack')
|
||||||
const { createMarkdownFields } = require('./gatsby/createMarkdownFields')
|
const { createMarkdownFields } = require('./gatsby/createMarkdownFields')
|
||||||
const { createExifFields } = require('./gatsby/createExifFields')
|
const { createExif } = require('./gatsby/createExif')
|
||||||
const {
|
const {
|
||||||
generatePostPages,
|
generatePostPages,
|
||||||
generateTagPages,
|
generateTagPages,
|
||||||
@ -9,17 +9,15 @@ const {
|
|||||||
const { generateJsonFeed } = require('./gatsby/feeds')
|
const { generateJsonFeed } = require('./gatsby/feeds')
|
||||||
const { itemsPerPage } = require('./config')
|
const { itemsPerPage } = require('./config')
|
||||||
|
|
||||||
exports.onCreateNode = ({ node, actions, getNode }) => {
|
exports.onCreateNode = ({ node, actions, getNode, createNodeId }) => {
|
||||||
const { createNodeField } = actions
|
|
||||||
|
|
||||||
// Markdown files
|
// Markdown files
|
||||||
if (node.internal.type === 'MarkdownRemark') {
|
if (node.internal.type === 'MarkdownRemark') {
|
||||||
createMarkdownFields(node, createNodeField, getNode)
|
createMarkdownFields(node, actions, getNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image files
|
// Image files
|
||||||
if (node.internal.mediaType === 'image/jpeg') {
|
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 { createFilePath } = require('gatsby-source-filesystem')
|
||||||
const { repoContentPath } = require('../config')
|
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) {
|
function createSlug(node, createNodeField, slugOriginal, parsedFilePath) {
|
||||||
let slug
|
let slug
|
||||||
|
|
||||||
@ -32,24 +54,3 @@ function createDate(node, createNodeField, slugOriginal) {
|
|||||||
value: date
|
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
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
|
iso: string
|
||||||
model: string
|
model: string
|
||||||
fstop: string
|
fstop: string
|
||||||
@ -28,3 +28,12 @@ export interface Exif {
|
|||||||
longitude: string
|
longitude: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Exif {
|
||||||
|
formatted: ExifFormatted
|
||||||
|
exif: any
|
||||||
|
image: any
|
||||||
|
thumbnail?: any
|
||||||
|
gps?: any
|
||||||
|
iptc?: any
|
||||||
|
}
|
||||||
|
@ -4,7 +4,15 @@ import styles from './Exif.module.scss'
|
|||||||
import { Exif as ExifMeta } from '../../@types/Image'
|
import { Exif as ExifMeta } from '../../@types/Image'
|
||||||
|
|
||||||
export default function Exif({ exif }: { exif: ExifMeta }) {
|
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 (
|
return (
|
||||||
<aside className={styles.exif}>
|
<aside className={styles.exif}>
|
||||||
|
@ -79,16 +79,18 @@ export const pageQuery = graphql`
|
|||||||
}
|
}
|
||||||
fields {
|
fields {
|
||||||
exif {
|
exif {
|
||||||
iso
|
formatted {
|
||||||
model
|
iso
|
||||||
fstop
|
model
|
||||||
shutterspeed
|
fstop
|
||||||
focalLength
|
shutterspeed
|
||||||
lensModel
|
focalLength
|
||||||
exposure
|
lensModel
|
||||||
gps {
|
exposure
|
||||||
latitude
|
gps {
|
||||||
longitude
|
latitude
|
||||||
|
longitude
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user