Make local resizing EXIF orientation aware
This commit is contained in:
parent
f321bc775c
commit
d9c6b8107e
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -9,6 +9,7 @@
|
|||||||
"CredentialsSignin",
|
"CredentialsSignin",
|
||||||
"Eterna",
|
"Eterna",
|
||||||
"exif",
|
"exif",
|
||||||
|
"exifr",
|
||||||
"exiftool",
|
"exiftool",
|
||||||
"ghijklmnopqrstuv",
|
"ghijklmnopqrstuv",
|
||||||
"hgetall",
|
"hgetall",
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"eslint": "8.54.0",
|
"eslint": "8.54.0",
|
||||||
"eslint-config-next": "14.0.3",
|
"eslint-config-next": "14.0.3",
|
||||||
|
"exifr": "^7.1.3",
|
||||||
"framer-motion": "^10.16.5",
|
"framer-motion": "^10.16.5",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -62,6 +62,9 @@ dependencies:
|
|||||||
eslint-config-next:
|
eslint-config-next:
|
||||||
specifier: 14.0.3
|
specifier: 14.0.3
|
||||||
version: 14.0.3(eslint@8.54.0)(typescript@5.3.2)
|
version: 14.0.3(eslint@8.54.0)(typescript@5.3.2)
|
||||||
|
exifr:
|
||||||
|
specifier: ^7.1.3
|
||||||
|
version: 7.1.3
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^10.16.5
|
specifier: ^10.16.5
|
||||||
version: 10.16.5(react-dom@18.2.0)(react@18.2.0)
|
version: 10.16.5(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -3616,6 +3619,10 @@ packages:
|
|||||||
strip-final-newline: 2.0.0
|
strip-final-newline: 2.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/exifr@7.1.3:
|
||||||
|
resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/exit@0.1.2:
|
/exit@0.1.2:
|
||||||
resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
|
resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { blobToImage } from '@/utility/blob';
|
import { blobToImage } from '@/utility/blob';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { CopyExif } from '@/lib/CopyExif';
|
import { CopyExif } from '@/lib/CopyExif';
|
||||||
|
import exifr from 'exifr';
|
||||||
import { cc } from '@/utility/css';
|
import { cc } from '@/utility/css';
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';
|
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';
|
||||||
@ -91,41 +92,94 @@ export default function ImageInput({
|
|||||||
hasMultipleUploads: files.length > 1,
|
hasMultipleUploads: files.length > 1,
|
||||||
isLastBlob: i === files.length - 1,
|
isLastBlob: i === files.length - 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const canvas = ref.current;
|
const canvas = ref.current;
|
||||||
if (!(maxSize && canvas)) {
|
|
||||||
// No need to process
|
// Specify wide gamut to avoid data loss while resizing
|
||||||
await onBlobReady?.({
|
const ctx = canvas?.getContext(
|
||||||
...callbackArgs,
|
'2d', { colorSpace: 'display-p3' }
|
||||||
blob: file,
|
);
|
||||||
});
|
|
||||||
} else {
|
if (maxSize && canvas && ctx) {
|
||||||
// Process images that need resizing
|
// Process images that need resizing
|
||||||
const image = await blobToImage(file);
|
const image = await blobToImage(file);
|
||||||
|
|
||||||
setImage(image);
|
setImage(image);
|
||||||
const { naturalWidth, naturalHeight } = image;
|
|
||||||
const ratio = naturalWidth / naturalHeight;
|
ctx.save();
|
||||||
|
|
||||||
|
let orientation = await exifr.orientation(file) ?? 1;
|
||||||
|
// Reverse engineer orientation
|
||||||
|
// so preserved EXIF data can be copied
|
||||||
|
switch (orientation) {
|
||||||
|
case 1: orientation = 1; break;
|
||||||
|
case 2: orientation = 1; break;
|
||||||
|
case 3: orientation = 3; break;
|
||||||
|
case 4: orientation = 1; break;
|
||||||
|
case 5: orientation = 1; break;
|
||||||
|
case 6: orientation = 8; break;
|
||||||
|
case 7: orientation = 1; break;
|
||||||
|
case 8: orientation = 6; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = image.width / image.height;
|
||||||
|
|
||||||
const width =
|
const width =
|
||||||
Math.round(ratio >= 1 ? maxSize : maxSize * ratio);
|
Math.round(ratio >= 1 ? maxSize : maxSize * ratio);
|
||||||
const height =
|
const height =
|
||||||
Math.round(ratio >= 1 ? maxSize / ratio : maxSize);
|
Math.round(ratio >= 1 ? maxSize / ratio : maxSize);
|
||||||
|
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
|
||||||
|
// Orientation transforms from:
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// https://gist.github.com/SagiMedina/f00a57de4e211456225d3114fd10b0d0
|
||||||
|
|
||||||
// Specify wide gamut to avoid data loss while resizing
|
switch(orientation) {
|
||||||
const ctx = canvas.getContext(
|
case 2:
|
||||||
'2d',
|
ctx.translate(width, 0);
|
||||||
{ colorSpace: 'display-p3' },
|
ctx.scale(-1, 1);
|
||||||
);
|
break;
|
||||||
|
case 3:
|
||||||
ctx?.drawImage(
|
ctx.translate(width, height);
|
||||||
image,
|
ctx.rotate((180 / 180) * Math.PI);
|
||||||
0,
|
break;
|
||||||
0,
|
case 4:
|
||||||
canvas.width,
|
ctx.translate(0, height);
|
||||||
canvas.height,
|
ctx.scale(1, -1);
|
||||||
);
|
break;
|
||||||
|
case 5:
|
||||||
|
canvas.width = height;
|
||||||
|
canvas.height = width;
|
||||||
|
ctx.rotate((90 / 180) * Math.PI);
|
||||||
|
ctx.scale(1, -1);
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
canvas.width = height;
|
||||||
|
canvas.height = width;
|
||||||
|
ctx.rotate((90 / 180) * Math.PI);
|
||||||
|
ctx.translate(0, -height);
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
canvas.width = height;
|
||||||
|
canvas.height = width;
|
||||||
|
ctx.rotate((270 / 180) * Math.PI);
|
||||||
|
ctx.translate(-width, height);
|
||||||
|
ctx.scale(1, -1);
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
canvas.width = height;
|
||||||
|
canvas.height = width;
|
||||||
|
ctx.translate(0, width);
|
||||||
|
ctx.rotate((270 / 180) * Math.PI);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(image, 0, 0, width, height);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
canvas.toBlob(
|
canvas.toBlob(
|
||||||
async blob => {
|
async blob => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
@ -139,6 +193,12 @@ export default function ImageInput({
|
|||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
quality,
|
quality,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// No need to process
|
||||||
|
await onBlobReady?.({
|
||||||
|
...callbackArgs,
|
||||||
|
blob: file,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user