diff --git a/.changeset/long-coats-hear.md b/.changeset/long-coats-hear.md new file mode 100644 index 0000000..0d4bafd --- /dev/null +++ b/.changeset/long-coats-hear.md @@ -0,0 +1,7 @@ +--- +'@tidaltheory/lens': minor +--- + +Add option to store IPTC metadata + +Only stores `object_name` (as `title`) and `caption` currently. diff --git a/package-lock.json b/package-lock.json index b200435..962a9aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,17 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "exif-reader": "1.0.3", "globby": "13.1.2", "is-plain-object": "5.0.0", - "lowdb": "3.0.0", + "lowdb": "5.0.3", "multimatch": "6.0.0", "node-vibrant": "3.2.1-alpha.1", "ora": "6.1.2", "pkg-conf": "4.0.0", "sade": "1.8.1", - "sharp": "0.30.7" + "sharp": "0.31.1", + "ts-node-iptc": "1.0.11" }, "bin": { "lens": "bin/index.js" @@ -7714,6 +7716,11 @@ "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" }, + "node_modules/exif-reader": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-1.0.3.tgz", + "integrity": "sha512-tWMBj1+9jUSibgR/kv/GQ/fkR0biaN9GEZ5iPdf7jFeH//d2bSzgPoaWf1OfMv4MXFD4upwvpCCyeMvSyLWSfA==" + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -12079,14 +12086,14 @@ } }, "node_modules/lowdb": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-3.0.0.tgz", - "integrity": "sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-5.0.3.tgz", + "integrity": "sha512-26qk7jgHbLGytYoZOf7/hgcvksYkRpXSnCJTSbnXApDExMkOw/vjWyjDBba4HYbuZN23MIf4Dc8q99QIgzQthA==", "dependencies": { - "steno": "^2.1.0" + "steno": "^3.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/typicode" @@ -15013,9 +15020,9 @@ } }, "node_modules/sharp": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", - "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-GR8M1wBwOiFKLkm9JPun27OQnNRZdHfSf9VwcdZX6UrRmM1/XnOrLFTF0GAil+y/YK4E6qcM/ugxs80QirsHxg==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", @@ -15028,7 +15035,7 @@ "tunnel-agent": "^0.6.0" }, "engines": { - "node": ">=12.13.0" + "node": ">=14.15.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -15837,11 +15844,11 @@ } }, "node_modules/steno": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/steno/-/steno-2.1.0.tgz", - "integrity": "sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/steno/-/steno-3.0.0.tgz", + "integrity": "sha512-uZtn7Ht9yXLiYgOsmo8btj4+f7VxyYheMt8g6F1ANjyqByQXEE2Gygjgenp3otHH1TlHsS4JAaRGv5wJ1wvMNw==", "engines": { - "node": "^14.13.1 || >=16.0.0" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/typicode" @@ -16503,6 +16510,11 @@ "node": ">=8" } }, + "node_modules/ts-node-iptc": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ts-node-iptc/-/ts-node-iptc-1.0.11.tgz", + "integrity": "sha512-6f10Fae8EjQwEOY4OuIDEiRVZfwVnnWq9ipb7B7UB71diTEz4lgH8R5vAvRt4B9G15K3/Q/8XdA0OX4vC27Fxg==" + }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -24361,6 +24373,11 @@ "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" }, + "exif-reader": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-1.0.3.tgz", + "integrity": "sha512-tWMBj1+9jUSibgR/kv/GQ/fkR0biaN9GEZ5iPdf7jFeH//d2bSzgPoaWf1OfMv4MXFD4upwvpCCyeMvSyLWSfA==" + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -27703,11 +27720,11 @@ } }, "lowdb": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-3.0.0.tgz", - "integrity": "sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-5.0.3.tgz", + "integrity": "sha512-26qk7jgHbLGytYoZOf7/hgcvksYkRpXSnCJTSbnXApDExMkOw/vjWyjDBba4HYbuZN23MIf4Dc8q99QIgzQthA==", "requires": { - "steno": "^2.1.0" + "steno": "^3.0.0" } }, "lower-case": { @@ -29914,9 +29931,9 @@ } }, "sharp": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", - "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-GR8M1wBwOiFKLkm9JPun27OQnNRZdHfSf9VwcdZX6UrRmM1/XnOrLFTF0GAil+y/YK4E6qcM/ugxs80QirsHxg==", "requires": { "color": "^4.2.3", "detect-libc": "^2.0.1", @@ -30564,9 +30581,9 @@ "dev": true }, "steno": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/steno/-/steno-2.1.0.tgz", - "integrity": "sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/steno/-/steno-3.0.0.tgz", + "integrity": "sha512-uZtn7Ht9yXLiYgOsmo8btj4+f7VxyYheMt8g6F1ANjyqByQXEE2Gygjgenp3otHH1TlHsS4JAaRGv5wJ1wvMNw==" }, "stream-transform": { "version": "2.1.3", @@ -31067,6 +31084,11 @@ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, + "ts-node-iptc": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ts-node-iptc/-/ts-node-iptc-1.0.11.tgz", + "integrity": "sha512-6f10Fae8EjQwEOY4OuIDEiRVZfwVnnWq9ipb7B7UB71diTEz4lgH8R5vAvRt4B9G15K3/Q/8XdA0OX4vC27Fxg==" + }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", diff --git a/package.json b/package.json index 67b95f8..deb6fc9 100644 --- a/package.json +++ b/package.json @@ -39,19 +39,24 @@ "setup": "zazen configure" }, "lint-staged": { - "*.{js,ts}": "eslint --fix", + "*.{js,ts}": [ + "eslint --fix", + "prettier --write" + ], "package.json": "prettier --write" }, "dependencies": { + "exif-reader": "1.0.3", "globby": "13.1.2", "is-plain-object": "5.0.0", - "lowdb": "3.0.0", + "lowdb": "5.0.3", "multimatch": "6.0.0", "node-vibrant": "3.2.1-alpha.1", "ora": "6.1.2", "pkg-conf": "4.0.0", "sade": "1.8.1", - "sharp": "0.30.7" + "sharp": "0.31.1", + "ts-node-iptc": "1.0.11" }, "devDependencies": { "@changesets/changelog-github": "0.4.7", diff --git a/src/cli.ts b/src/cli.ts index 01d6763..8aa5282 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,13 +4,15 @@ import path, { parse } from 'node:path' import process from 'node:process' import { globby } from 'globby' -import { JSONFile, Low } from 'lowdb' +import { Low } from 'lowdb' +import { JSONFile } from 'lowdb/node' import ora, { oraPromise } from 'ora' import sade from 'sade' import sharp from 'sharp' +import { IptcParser } from 'ts-node-iptc' import { PackageJson } from 'type-fest' -import { ImageRecord, ImageThumbnails } from '../types/types.js' +import { ImageMeta, ImageRecord, ImageThumbnails } from '../types/types.js' import { loadConfig } from './lib/context.js' import { getDominantPalette } from './lib/dominant.js' @@ -74,7 +76,8 @@ prog.command('add ') processed++ let sharpImage = sharp(source) - let { width, height } = await sharpImage.metadata() + let { width, height, iptc } = await sharpImage.metadata() + let iptcData = IptcParser.readIPTCData(iptc) let fingerprint = await generateFingerprint(sharpImage) let dominantPalette = await oraPromise( getDominantPalette(sharpImage), @@ -156,6 +159,12 @@ prog.command('add ') } } + let entryMeta: ImageMeta + if (config.includeMetadata) { + entryMeta.title = iptcData.object_name + entryMeta.caption = iptcData.caption + } + spinner.start('Adding entry to library...') /** @@ -167,6 +176,7 @@ prog.command('add ') formats, colors: dominantPalette, thumbnails: entryThumbnails, + meta: entryMeta, } let store = options.store || config.store || 'imagemeta.json' diff --git a/types/types.ts b/types/types.ts index cb6726e..9a81e32 100644 --- a/types/types.ts +++ b/types/types.ts @@ -37,6 +37,13 @@ interface ImageColors { dominant: string } +export interface ImageMeta { + /** Title field from the image’s EXIF data. */ + title?: string + /** Description field from the image’s EXIF data. */ + caption?: string +} + /** Data for each image stored in the library. */ export interface ImageRecord extends ImageFile { colors: ImageColors @@ -44,6 +51,7 @@ export interface ImageRecord extends ImageFile { blurhash?: string /** Resized versions, keyed to thumbnail size label. */ thumbnails?: ImageThumbnails + meta?: ImageMeta } type ThumbResizeOptions = RequireAtLeastOne @@ -59,6 +67,7 @@ export interface LensConfig { /** Use the filename as a subdirectory for generated files. */ useFilenameDirectory?: boolean thumbnails?: ThumbnailOption[] + includeMetadata?: boolean } export interface PathParts { diff --git a/zazen.config.ts b/zazen.config.ts index 5c8788f..bdeb922 100644 --- a/zazen.config.ts +++ b/zazen.config.ts @@ -26,6 +26,7 @@ export default { 'error', { allowObject: true }, ], + 'n/file-extension-in-import': 'off', }, }), }