From 66828175784b225beb06986fa5b3f32785e9a3bd Mon Sep 17 00:00:00 2001 From: ekzyis <27162016+ekzyis@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:45:21 +0100 Subject: [PATCH] Do not remove orientation EXIF data (#683) * Keep orientation metadata * npm install piexifjs --------- Co-authored-by: ekzyis --- components/image.js | 85 ++++++++++++++++++++++++++++++++++++++++++--- package-lock.json | 11 ++++++ package.json | 1 + 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/components/image.js b/components/image.js index 695f4a33..aa847802 100644 --- a/components/image.js +++ b/components/image.js @@ -8,6 +8,7 @@ import { UPLOAD_TYPES_ALLOW } from '../lib/constants' import { useToast } from './toast' import gql from 'graphql-tag' import { useMutation } from '@apollo/client' +import piexif from 'piexifjs' export function decodeOriginalUrl (imgproxyUrl) { const parts = imgproxyUrl.split('/') @@ -153,7 +154,6 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onUpload const s3Upload = useCallback(async file => { const img = new window.Image() file = await removeExifData(file) - img.src = window.URL.createObjectURL(file) return new Promise((resolve, reject) => { img.onload = async () => { onUpload?.(file) @@ -201,6 +201,8 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onUpload onSuccess?.({ ...variables, id, name: file.name, url, file }) resolve(id) } + img.onerror = reject + img.src = window.URL.createObjectURL(file) }) }, [toaster, getSignedPOST]) @@ -235,7 +237,8 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onUpload }) // from https://stackoverflow.com/a/77472484 -const removeExifData = (file) => { +const removeExifData = async (file) => { + if (!file || !file.type.startsWith('image/')) return file const cleanBuffer = (arrayBuffer) => { let dataView = new DataView(arrayBuffer) const exifMarker = 0xffe1 @@ -260,8 +263,50 @@ const removeExifData = (file) => { modifiedBuffer.set(new Uint8Array(buffer.slice(offset + length)), offset) return modifiedBuffer.buffer } - return new Promise((resolve) => { - if (!file || !file.type.startsWith('image/')) return resolve(file) + function getOrientation (file) { + const fr = new window.FileReader() + return new Promise((resolve, reject) => { + fr.onload = function () { + const view = new DataView(this.result) + if (view.getUint16(0, false) !== 0xFFD8) { + // not JPEG + return resolve(-2) + } + const length = view.byteLength; let offset = 2 + while (offset < length) { + if (view.getUint16(offset + 2, false) <= 8) return resolve(-1) // no orientation available + const marker = view.getUint16(offset, false) + offset += 2 + if (marker === 0xFFE1) { + if (view.getUint32(offset += 2, false) !== 0x45786966) { + // no orientation available + return resolve(-1) + } + const little = view.getUint16(offset += 6, false) === 0x4949 + offset += view.getUint32(offset + 4, little) + const tags = view.getUint16(offset, little) + offset += 2 + for (let i = 0; i < tags; i++) { + if (view.getUint16(offset + (i * 12), little) === 0x0112) { + // orientation available + return resolve(view.getUint16(offset + (i * 12) + 8, little)) + } + } + } else if ((marker & 0xFF00) !== 0xFF00) { + break + } else { + offset += view.getUint16(offset, false) + } + } + // no orientation available + return resolve(-1) + } + fr.onerror = reject + fr.readAsArrayBuffer(file) + }) + } + const orientation = await getOrientation(file) + const cleanFile = await new Promise((resolve, reject) => { const fr = new window.FileReader() fr.onload = function () { const cleanedBuffer = cleanBuffer(this.result) @@ -269,6 +314,38 @@ const removeExifData = (file) => { const newFile = new File([blob], file.name, { type: file.type }) resolve(newFile) } + fr.onerror = reject fr.readAsArrayBuffer(file) }) + if (orientation <= 0) { + // not orientation available (-1) or not JPEG (-2) + return cleanFile + } + // put orientation value back in + return new Promise((resolve, reject) => { + const fr = new window.FileReader() + fr.onload = function () { + const zeroth = {} + // Orientation is of type SHORT so single int is ok, see https://piexifjs.readthedocs.io/en/latest/appendices.html + zeroth[piexif.ImageIFD.Orientation] = orientation + const exifObj = { '0th': zeroth } + const exifStr = piexif.dump(exifObj) + const inserted = piexif.insert(exifStr, this.result) + const dataUriToBuffer = (dataUri) => { + // data-uri scheme regexp from https://github.com/ragingwind/data-uri-regex/blob/a9d7474c833e8fbf5b1821fe65d8cccd6aea4536/index.js + // data:[][;charset=][;base64], + const regexp = /^(data:)([\w/+-]*)(;charset=[\w-]+|;base64){0,1},(.*)/gi + const b64 = regexp.exec(dataUri)[4] + const buf = Buffer.from(b64, 'base64') + return buf + } + const buf = dataUriToBuffer(inserted) + const blob = new Blob([buf], { type: file.type }) + const newFile = new File([blob], file.name, { type: file.type }) + resolve(newFile) + } + fr.onerror = reject + // piexifjs library needs data URI as input + fr.readAsDataURL(cleanFile) + }) } diff --git a/package-lock.json b/package-lock.json index 89e3972b..49f704cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "page-metadata-parser": "^1.1.4", "pageres": "^7.1.0", "pg-boss": "^9.0.3", + "piexifjs": "^1.0.6", "prisma": "^5.4.2", "qrcode.react": "^3.1.0", "react": "^18.2.0", @@ -12281,6 +12282,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/piexifjs": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/piexifjs/-/piexifjs-1.0.6.tgz", + "integrity": "sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==" + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -25152,6 +25158,11 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, + "piexifjs": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/piexifjs/-/piexifjs-1.0.6.tgz", + "integrity": "sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==" + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", diff --git a/package.json b/package.json index fb7df500..d4c5bbeb 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "page-metadata-parser": "^1.1.4", "pageres": "^7.1.0", "pg-boss": "^9.0.3", + "piexifjs": "^1.0.6", "prisma": "^5.4.2", "qrcode.react": "^3.1.0", "react": "^18.2.0",