From d25417c383daec2e959e2ab537dd49324378e8a3 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 29 Jun 2020 15:42:46 -0400 Subject: [PATCH] Parse image file info if available --- package-lock.json | 10 ++++++++ package.json | 2 ++ src/routes/files.ts | 4 +-- src/services/files.ts | 34 +++++++++++++++++++++++-- src/types/grant.d.ts | 4 --- src/types/shims.d.ts | 14 +++++++++++ src/utils/parse-iptc.ts | 56 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 116 insertions(+), 8 deletions(-) delete mode 100644 src/types/grant.d.ts create mode 100644 src/types/shims.d.ts create mode 100644 src/utils/parse-iptc.ts diff --git a/package-lock.json b/package-lock.json index f9aaeb0484..d1d9ee8f26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1952,6 +1952,11 @@ "strip-final-newline": "^2.0.0" } }, + "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==" + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -3185,6 +3190,11 @@ } } }, + "icc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/icc/-/icc-2.0.0.tgz", + "integrity": "sha512-VSTak7UAcZu1E24YFvcoHVpVg/ZUVyb0G1v0wUIibfz5mHvcFeI/Gpn8C0cAUKw5jCCGx5JBcV4gULu6hX97mA==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index 1dc4d8718e..5a15cbd902 100644 --- a/package.json +++ b/package.json @@ -74,12 +74,14 @@ "busboy": "^0.3.1", "camelcase": "^6.0.0", "dotenv": "^8.2.0", + "exif-reader": "^1.0.3", "express": "^4.17.1", "express-async-handler": "^1.1.4", "express-pino-logger": "^5.0.0", "express-session": "^1.17.1", "get-port": "^5.1.1", "grant": "^5.2.0", + "icc": "^2.0.0", "jsonwebtoken": "^8.5.1", "knex": "^0.21.1", "knex-schema-inspector": "github:knex/knex-schema-inspector", diff --git a/src/routes/files.ts b/src/routes/files.ts index fe9458beff..2a5bc2ab1a 100644 --- a/src/routes/files.ts +++ b/src/routes/files.ts @@ -5,6 +5,7 @@ import sanitizeQuery from '../middleware/sanitize-query'; import validateQuery from '../middleware/validate-query'; import * as FilesService from '../services/files'; import logger from '../logger'; +import { InvalidPayloadException } from '../exceptions'; const router = express.Router(); @@ -31,8 +32,7 @@ const multipartHandler = (operation: 'create' | 'update') => busboy.on('file', async (fieldname, fileStream, filename, encoding, mimetype) => { if (!disk) { - // @todo error - return busboy.emit('error', new Error('no storage provided')); + return busboy.emit('error', new InvalidPayloadException('No storage provided.')); } payload = { diff --git a/src/services/files.ts b/src/services/files.ts index 063e43a6bc..47f6a1ee90 100644 --- a/src/services/files.ts +++ b/src/services/files.ts @@ -4,6 +4,10 @@ import storage from '../storage'; import * as PayloadService from './payload'; import database from '../database'; import logger from '../logger'; +import sharp from 'sharp'; +import { parse as parseICC } from 'icc'; +import parseEXIF from 'exif-reader'; +import parseIPTC from '../utils/parse-iptc'; export const createFile = async ( data: Record, @@ -12,10 +16,36 @@ export const createFile = async ( ) => { const payload = await PayloadService.processValues('create', 'directus_files', data); - await ItemsService.createItem('directus_files', payload, query); + if (payload.type?.startsWith('image')) { + const pipeline = sharp(); + + pipeline.metadata().then((meta) => { + payload.width = meta.width; + payload.height = meta.height; + payload.filesize = meta.size; + payload.metadata = {}; + + if (meta.icc) { + payload.metadata.icc = parseICC(meta.icc); + } + + if (meta.exif) { + payload.metadata.exif = parseEXIF(meta.exif); + } + + if (meta.iptc) { + payload.metadata.iptc = parseIPTC(meta.iptc); + + payload.title = payload.title || payload.metadata.iptc.headline; + payload.description = payload.description || payload.metadata.iptc.caption; + } + }); + + stream.pipe(pipeline); + } - // @todo type of stream in flydrive is wrong: https://github.com/Slynova-Org/flydrive/issues/145 await storage.disk(data.storage).put(data.filename_disk, stream as any); + await ItemsService.createItem('directus_files', payload, query); }; export const readFiles = async (query: Query) => { diff --git a/src/types/grant.d.ts b/src/types/grant.d.ts deleted file mode 100644 index d91e1ffcb7..0000000000 --- a/src/types/grant.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'grant' { - const grant: any; - export default grant; -} diff --git a/src/types/shims.d.ts b/src/types/shims.d.ts new file mode 100644 index 0000000000..e6b2ea5b54 --- /dev/null +++ b/src/types/shims.d.ts @@ -0,0 +1,14 @@ +declare module 'grant' { + const grant: any; + export default grant; +} + +declare module 'icc' { + const parse: (buf: Buffer) => Record; + export { parse }; +} + +declare module 'exif-reader' { + const exifReader: (buf: Buffer) => Record; + export default exifReader; +} diff --git a/src/utils/parse-iptc.ts b/src/utils/parse-iptc.ts new file mode 100644 index 0000000000..0ff79ef079 --- /dev/null +++ b/src/utils/parse-iptc.ts @@ -0,0 +1,56 @@ +const IPTC_ENTRY_TYPES = new Map([ + [0x78, 'caption'], + [0x6e, 'credit'], + [0x19, 'keywords'], + [0x37, 'dateCreated'], + [0x50, 'byline'], + [0x55, 'bylineTitle'], + [0x7a, 'captionWriter'], + [0x69, 'headline'], + [0x74, 'copyright'], + [0x0f, 'category'], +]); + +const IPTC_ENTRY_MARKER = Buffer.from([0x1c, 0x02]); + +export default function parseIPTC(buffer: Buffer) { + if (!Buffer.isBuffer(buffer)) return {}; + + let iptc = {}; + let lastIptcEntryPos = buffer.indexOf(IPTC_ENTRY_MARKER); + + while (lastIptcEntryPos !== -1) { + lastIptcEntryPos = buffer.indexOf( + IPTC_ENTRY_MARKER, + lastIptcEntryPos + IPTC_ENTRY_MARKER.byteLength + ); + + let iptcBlockTypePos = lastIptcEntryPos + IPTC_ENTRY_MARKER.byteLength; + let iptcBlockSizePos = iptcBlockTypePos + 1; + let iptcBlockDataPos = iptcBlockSizePos + 2; + + let iptcBlockType = buffer.readUInt8(iptcBlockTypePos); + let iptcBlockSize = buffer.readUInt16BE(iptcBlockSizePos); + + if (!IPTC_ENTRY_TYPES.has(iptcBlockType)) { + continue; + } + + // if (iptcBlockSize > buffer.length - (iptcBlockDataPos + iptcBlockSize)) { + // throw new Error('Invalid IPTC directory'); + // } + + let iptcBlockTypeId = IPTC_ENTRY_TYPES.get(iptcBlockType); + let iptcData = buffer.slice(iptcBlockDataPos, iptcBlockDataPos + iptcBlockSize).toString(); + + if (iptc[iptcBlockTypeId] == null) { + iptc[iptcBlockTypeId] = iptcData; + } else if (Array.isArray(iptc[iptcBlockTypeId])) { + iptc[iptcBlockTypeId].push(iptcData); + } else { + iptc[iptcBlockTypeId] = [iptc[iptcBlockTypeId], iptcData]; + } + } + + return iptc; +}