1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-06-25 02:36:26 +02:00

exif solution as prebuild step

This commit is contained in:
Matthias Kretschmann 2023-09-05 02:26:50 +01:00
parent a219076cd1
commit edea2b320a
Signed by: m
GPG Key ID: 606EEEF3C479A91F
9 changed files with 254 additions and 138 deletions

3
.gitignore vendored
View File

@ -15,4 +15,5 @@ src/images/icons/
# autogenerated stuff
public/get/
.config/redirects.json
.config/redirects.json
.config/exif.json

View File

@ -7,7 +7,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"prebuild": "npm run create:icons && npm run create:redirects && npm run move:downloads",
"prebuild": "npm run create:icons && npm run create:redirects && npm run move:downloads && npm run create:exif",
"start": "npm run prebuild && astro dev --config .config/astro.config.mjs",
"build": "npm run prebuild && astro build --config .config/astro.config.mjs",
"preview": "astro preview",
@ -24,6 +24,7 @@
"new": "ts-node --esm scripts/new/index.ts",
"create:icons": "ts-node --esm scripts/create-icons/index.ts",
"create:redirects": "ts-node --esm scripts/redirect-from.ts",
"create:exif": "ts-node --esm scripts/create-exif/index.ts",
"move:downloads": "ts-node --esm scripts/move-downloads.ts"
},
"dependencies": {

View File

@ -0,0 +1,89 @@
//
// Extract EXIF & IPTC data from images
// write to json file `.config/exif.json`
//
import getCoordinates from 'dms2dec'
import Fraction from 'fraction.js'
import type { ExifFormatted, FastExif } from './types.ts'
function formatGps(gpsData: FastExif['gps']): {
latitude: string
longitude: string
} {
if (!gpsData) return { latitude: '', longitude: '' }
const { GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude } = gpsData
const GPSdec = getCoordinates(
GPSLatitude as number[],
GPSLatitudeRef as string,
GPSLongitude as number[],
GPSLongitudeRef as string
)
const latitude = GPSdec[0]
const longitude = GPSdec[1]
return { latitude, longitude }
}
function formatExposure(exposureMode: number) {
if (exposureMode === null || exposureMode === undefined) 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
}
export function formatExif(exifData: FastExif): ExifFormatted | undefined {
if (!exifData?.exif) return
const { Model } = exifData.image as { Model: string }
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 as number)
const shutterspeed = `${n}/${d}s`
// GPS
let latitude
let longitude
if (exifData.gps) ({ latitude, longitude } = formatGps(exifData.gps))
// Exposure
const exposureValue = (ExposureBiasValue || ExposureMode) as number
const exposure = formatExposure(exposureValue)
return {
iso,
model: Model,
fstop,
shutterspeed,
focalLength,
lensModel: LensModel,
exposure,
gps: { latitude, longitude }
}
}

View File

@ -1,148 +1,80 @@
import type { Actions, Node, NodePluginArgs } from 'gatsby'
import getCoordinates from 'dms2dec'
import fs from 'node:fs/promises'
import path from 'node:path'
import { read } from 'fast-exif'
import Fraction from 'fraction.js'
import fs from 'fs'
import iptc from 'node-iptc'
import ora from 'ora'
import type { Exif, ExifFormatted } from './types.ts'
import { formatExif } from './format.ts'
export const createExif = async (
node: Node,
actions: Actions,
createNodeId: NodePluginArgs['createNodeId']
) => {
if (!node?.absolutePath) return
const imageFolder = path.join(process.cwd(), 'content', 'photos')
const outputFilePath = '.config/exif.json'
const spinner = ora('Extracting EXIF metadata from all photos').start()
async function readOutExif(filePath: string): Promise<Exif | undefined> {
if (!filePath) return
try {
// exif
const exifData = (await read(
node.absolutePath as string,
true
)) as Queries.ImageExif
const exifData = await read(filePath, true)
if (!exifData) return
// iptc
const file = fs.readFileSync(node.absolutePath as string)
const file = await fs.readFile(filePath)
const iptcData = iptc(file)
createNodes(exifData, iptcData, node, actions, createNodeId)
} catch (error: any) {
console.error(`${node.name}: ${error.message}`)
}
}
// format before output
const exifDataFormatted = formatExif(exifData)
const imageId = path.basename(filePath, path.extname(filePath))
function createNodes(
exifData: Queries.ImageExif,
iptcData: any,
node: Node,
actions: Actions,
createNodeId: NodePluginArgs['createNodeId']
) {
const { createNodeField, createNode, createParentChildLink } = actions
const exifDataFormatted = formatExif(exifData)
const exif = {
...exifData,
iptc: { ...iptcData },
formatted: { ...exifDataFormatted }
}
const exifNode: any = {
id: createNodeId(`${node.id} >> ImageExif`),
children: [],
...exif,
parent: node.id,
internal: {
contentDigest: `${node.internal.contentDigest}`,
type: 'ImageExif'
const exif = {
image: imageId,
exif: { ...exifDataFormatted } as ExifFormatted,
iptc: { ...iptcData }
}
}
// 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: Queries.ImageExif) {
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 }
return exif
} catch (error: any) {
console.error(`${filePath}: ${error.message}`)
}
}
function formatGps(gpsData: Queries.ImageExif['gps']): {
latitude: string
longitude: string
} {
if (!gpsData) return { latitude: '', longitude: '' }
async function processImages(folderPath: string): Promise<Exif[]> {
const allExif: Exif[] = []
const { GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude } = gpsData
try {
const files = await fs.readdir(folderPath, { recursive: true })
const GPSdec = getCoordinates(
GPSLatitude,
GPSLatitudeRef,
GPSLongitude,
GPSLongitudeRef
)
for (const file of files) {
const filePath = path.join(folderPath, file)
const stats = await fs.stat(filePath)
const latitude = GPSdec[0]
const longitude = GPSdec[1]
if (stats.isFile()) {
// Check if it's an image file based on its file extension
const fileExtension = path.extname(filePath).toLowerCase()
return { latitude, longitude }
}
function formatExposure(exposureMode: Queries.ImageExifExif['ExposureMode']) {
if (exposureMode === null || exposureMode === undefined) 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`
if (fileExtension === '.jpg' || fileExtension === '.jpeg') {
const exif = await readOutExif(filePath)
if (!exif) continue
allExif.push(exif)
}
}
}
} catch (err) {
console.error('Error:', (err as Error).message)
}
return exposure
return allExif
}
try {
const allExif = await processImages(imageFolder)
const allExifJSON = JSON.stringify(allExif, null, 2)
// Write the redirects object to the output file
fs.writeFile(outputFilePath, allExifJSON, 'utf-8')
spinner.succeed(`Extracted EXIF data from ${allExif.length} photos`)
} catch (error: any) {
spinner.fail((error as Error).message)
}

View File

@ -0,0 +1,24 @@
export type FastExif = {
image?: Record<string, unknown> | undefined
thumbnail?: Record<string, unknown> | undefined
exif?: Record<string, unknown> | undefined
gps?: Record<string, unknown> | undefined
interoperability?: Record<string, unknown> | undefined
}
export type ExifFormatted = {
iso: string
model: any
fstop: string
shutterspeed: string
focalLength: string
lensModel: any
exposure: string | undefined
gps: { latitude: string | undefined; longitude: string | undefined }
}
export type Exif = {
image: string
exif: ExifFormatted
iptc: any
}

8
src/@types/dmsdec/index.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare module 'dms2dec' {
export default function dms2dec(
lat: readonly number[],
latRef: string,
lon: readonly number[],
lonRef: string
): [latDec: string, lonDec: string]
}

71
src/@types/node-iptc/index.d.ts vendored Normal file
View File

@ -0,0 +1,71 @@
declare module 'node-iptc' {
export default function iptc(buffer: Buffer): IptcData
}
type IptcData = {
object_type_reference?: string
object_attribute_reference?: string
object_name?: string
edit_status?: string
editorial_update?: string
urgency?: string
subject_reference?: string
category?: string
supplemental_categories?: string[]
fixture_id?: string[]
keywords?: string[]
content_location_code?: string[]
content_location_name?: string[]
release_date?: string
release_time?: string
expiration_date?: string
expiration_time?: string
special_instructions?: string
action_advised?: string
reference_service?: string[]
reference_date?: string[]
reference_number?: string[]
date_created?: string
time_created?: string
digital_date_created?: string
digital_time_created?: string
originating_program?: string
program_version?: string
object_cycle?: string
by_line?: string[]
caption?: string // not in spec, but observed in situ
by_line_title?: string[]
city?: string
sub_location?: string
province_or_state?: string
country_or_primary_location_code?: string
country_or_primary_location_name?: string
original_transmission_reference?: string
headline?: string
credit?: string
source?: string
copyright_notice?: string
contact?: string
local_caption?: string
caption_writer?: string[]
rasterized_caption?: string
image_type?: string
image_orientation?: string
language_identifier?: string
audio_type?: string
audio_sampling_rate?: string
audio_sampling_resolution?: string
audio_duration?: string
audio_outcue?: string
job_id?: string
master_document_id?: string
short_document_id?: string
unique_document_id?: string
owner_id?: string
object_preview_file_format?: string
object_preview_file_format_version?: string
object_preview_data?: string
date_time?: Date
}

View File

@ -1,13 +1,2 @@
declare module 'pigeon-maps'
declare module 'pigeon-marker'
declare module 'unified'
declare module 'node-iptc'
declare module 'dms2dec' {
export default function dms2dec(
lat: readonly number[],
latRef: string,
lon: readonly number[],
lonRef: string
): [latDec: string, lonDec: string]
}

View File

@ -13,7 +13,8 @@
"@images/*": ["src/images/*"],
"@lib/*": ["src/lib/*"],
"@content/*": ["content/*"]
}
},
"typeRoots": ["./src/@types", "./node_modules/@types"]
}
// "exclude": ["node_modules", "public", "dist", "./**/*.js"],
// "include": [