mirror of
https://github.com/kremalicious/blog.git
synced 2024-06-28 16:48:00 +02:00
exif solution as prebuild step
This commit is contained in:
parent
a219076cd1
commit
edea2b320a
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -16,3 +16,4 @@ src/images/icons/
|
|||
# autogenerated stuff
|
||||
public/get/
|
||||
.config/redirects.json
|
||||
.config/exif.json
|
|
@ -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": {
|
||||
|
|
89
scripts/create-exif/format.ts
Normal file
89
scripts/create-exif/format.ts
Normal 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 }
|
||||
}
|
||||
}
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
function createNodes(
|
||||
exifData: Queries.ImageExif,
|
||||
iptcData: any,
|
||||
node: Node,
|
||||
actions: Actions,
|
||||
createNodeId: NodePluginArgs['createNodeId']
|
||||
) {
|
||||
const { createNodeField, createNode, createParentChildLink } = actions
|
||||
// format before output
|
||||
const exifDataFormatted = formatExif(exifData)
|
||||
const imageId = path.basename(filePath, path.extname(filePath))
|
||||
|
||||
const exif = {
|
||||
...exifData,
|
||||
iptc: { ...iptcData },
|
||||
formatted: { ...exifDataFormatted }
|
||||
image: imageId,
|
||||
exif: { ...exifDataFormatted } as ExifFormatted,
|
||||
iptc: { ...iptcData }
|
||||
}
|
||||
|
||||
const exifNode: any = {
|
||||
id: createNodeId(`${node.id} >> ImageExif`),
|
||||
children: [],
|
||||
...exif,
|
||||
parent: node.id,
|
||||
internal: {
|
||||
contentDigest: `${node.internal.contentDigest}`,
|
||||
type: 'ImageExif'
|
||||
return exif
|
||||
} catch (error: any) {
|
||||
console.error(`${filePath}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// add exif fields to existing type file
|
||||
createNodeField({ node, name: 'exif', value: exif })
|
||||
async function processImages(folderPath: string): Promise<Exif[]> {
|
||||
const allExif: Exif[] = []
|
||||
|
||||
// create new nodes from all exif data
|
||||
// allowing to be queried with imageExif & AllImageExif
|
||||
createNode(exifNode)
|
||||
createParentChildLink({ parent: node, child: exifNode })
|
||||
}
|
||||
try {
|
||||
const files = await fs.readdir(folderPath, { recursive: true })
|
||||
|
||||
function formatExif(exifData: Queries.ImageExif) {
|
||||
if (!exifData.exif) return
|
||||
for (const file of files) {
|
||||
const filePath = path.join(folderPath, file)
|
||||
const stats = await fs.stat(filePath)
|
||||
|
||||
const { Model } = exifData.image
|
||||
const {
|
||||
ISO,
|
||||
FNumber,
|
||||
ExposureTime,
|
||||
FocalLength,
|
||||
FocalLengthIn35mmFormat,
|
||||
ExposureBiasValue,
|
||||
ExposureMode,
|
||||
LensModel
|
||||
} = exifData.exif
|
||||
if (stats.isFile()) {
|
||||
// Check if it's an image file based on its file extension
|
||||
const fileExtension = path.extname(filePath).toLowerCase()
|
||||
|
||||
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 }
|
||||
if (fileExtension === '.jpg' || fileExtension === '.jpeg') {
|
||||
const exif = await readOutExif(filePath)
|
||||
if (!exif) continue
|
||||
allExif.push(exif)
|
||||
}
|
||||
}
|
||||
|
||||
function formatGps(gpsData: Queries.ImageExif['gps']): {
|
||||
latitude: string
|
||||
longitude: string
|
||||
} {
|
||||
if (!gpsData) return { latitude: '', longitude: '' }
|
||||
|
||||
const { GPSLatitudeRef, GPSLatitude, GPSLongitudeRef, GPSLongitude } = gpsData
|
||||
|
||||
const GPSdec = getCoordinates(
|
||||
GPSLatitude,
|
||||
GPSLatitudeRef,
|
||||
GPSLongitude,
|
||||
GPSLongitudeRef
|
||||
)
|
||||
|
||||
const latitude = GPSdec[0]
|
||||
const longitude = GPSdec[1]
|
||||
|
||||
return { latitude, longitude }
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error:', (err as Error).message)
|
||||
}
|
||||
|
||||
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`
|
||||
return allExif
|
||||
}
|
||||
|
||||
return exposure
|
||||
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)
|
||||
}
|
||||
|
|
24
scripts/create-exif/types.ts
Normal file
24
scripts/create-exif/types.ts
Normal 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
8
src/@types/dmsdec/index.d.ts
vendored
Normal 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
71
src/@types/node-iptc/index.d.ts
vendored
Normal 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
|
||||
}
|
11
src/@types/node_modules.d.ts
vendored
11
src/@types/node_modules.d.ts
vendored
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
|
Loading…
Reference in New Issue
Block a user