Merge pull request #22 from sambecker/resize-orientation
EXIF-orientation aware local resizing
This commit is contained in:
commit
b325211022
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -9,6 +9,7 @@
|
||||
"CredentialsSignin",
|
||||
"Eterna",
|
||||
"exif",
|
||||
"exifr",
|
||||
"exiftool",
|
||||
"ghijklmnopqrstuv",
|
||||
"hgetall",
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
"date-fns": "^2.30.0",
|
||||
"eslint": "8.54.0",
|
||||
"eslint-config-next": "14.0.3",
|
||||
"exifr": "^7.1.3",
|
||||
"framer-motion": "^10.16.5",
|
||||
"jest": "^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:
|
||||
specifier: 14.0.3
|
||||
version: 14.0.3(eslint@8.54.0)(typescript@5.3.2)
|
||||
exifr:
|
||||
specifier: ^7.1.3
|
||||
version: 7.1.3
|
||||
framer-motion:
|
||||
specifier: ^10.16.5
|
||||
version: 10.16.5(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -3616,6 +3619,10 @@ packages:
|
||||
strip-final-newline: 2.0.0
|
||||
dev: false
|
||||
|
||||
/exifr@7.1.3:
|
||||
resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==}
|
||||
dev: false
|
||||
|
||||
/exit@0.1.2:
|
||||
resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache';
|
||||
import {
|
||||
IMAGE_OG_SMALL_SIZE,
|
||||
IMAGE_OG_DIMENSION_SMALL,
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
} from '@/photo/image-response';
|
||||
import FilmSimulationImageResponse from
|
||||
@ -28,7 +28,7 @@ export async function GET(
|
||||
getImageCacheHeadersForAuth(await auth()),
|
||||
]);
|
||||
|
||||
const { width, height } = IMAGE_OG_SMALL_SIZE;
|
||||
const { width, height } = IMAGE_OG_DIMENSION_SMALL;
|
||||
|
||||
return new ImageResponse(
|
||||
<FilmSimulationImageResponse {...{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache';
|
||||
import {
|
||||
IMAGE_OG_SMALL_SIZE,
|
||||
IMAGE_OG_DIMENSION_SMALL,
|
||||
MAX_PHOTOS_TO_SHOW_OG,
|
||||
} from '@/photo/image-response';
|
||||
import HomeImageResponse from '@/photo/image-response/HomeImageResponse';
|
||||
@ -21,7 +21,7 @@ export async function GET() {
|
||||
getIBMPlexMonoMedium(),
|
||||
]);
|
||||
|
||||
const { width, height } = IMAGE_OG_SMALL_SIZE;
|
||||
const { width, height } = IMAGE_OG_DIMENSION_SMALL;
|
||||
|
||||
return new ImageResponse(
|
||||
<HomeImageResponse {...{ photos, width, height, fontFamily }}/>,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth, getPhotoCached } from '@/cache';
|
||||
import { IMAGE_OG_SIZE } from '@/photo/image-response';
|
||||
import { IMAGE_OG_DIMENSION } from '@/photo/image-response';
|
||||
import PhotoImageResponse from '@/photo/image-response/PhotoImageResponse';
|
||||
import { getIBMPlexMonoMedium } from '@/site/font';
|
||||
import { ImageResponse } from 'next/og';
|
||||
@ -23,7 +23,7 @@ export async function GET(
|
||||
|
||||
if (!photo) { return new Response('Photo not found', { status: 404 }); }
|
||||
|
||||
const { width, height } = IMAGE_OG_SIZE;
|
||||
const { width, height } = IMAGE_OG_DIMENSION;
|
||||
|
||||
return new ImageResponse(
|
||||
<PhotoImageResponse {...{ photo, width, height, fontFamily }} />,
|
||||
|
||||
@ -2,7 +2,7 @@ import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache';
|
||||
import { getCameraFromKey } from '@/camera';
|
||||
import {
|
||||
IMAGE_OG_SMALL_SIZE,
|
||||
IMAGE_OG_DIMENSION_SMALL,
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
} from '@/photo/image-response';
|
||||
import CameraImageResponse from '@/photo/image-response/CameraImageResponse';
|
||||
@ -30,7 +30,7 @@ export async function GET(
|
||||
getImageCacheHeadersForAuth(await auth()),
|
||||
]);
|
||||
|
||||
const { width, height } = IMAGE_OG_SMALL_SIZE;
|
||||
const { width, height } = IMAGE_OG_DIMENSION_SMALL;
|
||||
|
||||
return new ImageResponse(
|
||||
<CameraImageResponse {...{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache';
|
||||
import {
|
||||
IMAGE_OG_SMALL_SIZE,
|
||||
IMAGE_OG_DIMENSION_SMALL,
|
||||
MAX_PHOTOS_TO_SHOW_PER_TAG,
|
||||
} from '@/photo/image-response';
|
||||
import TagImageResponse from '@/photo/image-response/TagImageResponse';
|
||||
@ -26,7 +26,7 @@ export async function GET(
|
||||
getImageCacheHeadersForAuth(await auth()),
|
||||
]);
|
||||
|
||||
const { width, height } = IMAGE_OG_SMALL_SIZE;
|
||||
const { width, height } = IMAGE_OG_DIMENSION_SMALL;
|
||||
|
||||
return new ImageResponse(
|
||||
<TagImageResponse {...{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache';
|
||||
import {
|
||||
IMAGE_OG_SIZE,
|
||||
IMAGE_OG_DIMENSION,
|
||||
MAX_PHOTOS_TO_SHOW_TEMPLATE_TIGHT,
|
||||
} from '@/photo/image-response';
|
||||
import TemplateImageResponse from
|
||||
@ -25,7 +25,7 @@ export async function GET() {
|
||||
getImageCacheHeadersForAuth(await auth()),
|
||||
]);
|
||||
|
||||
const { width, height } = IMAGE_OG_SIZE;
|
||||
const { width, height } = IMAGE_OG_DIMENSION;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { auth } from '@/auth';
|
||||
import { getImageCacheHeadersForAuth, getPhotosCached } from '@/cache';
|
||||
import {
|
||||
GRID_OG_SIZE,
|
||||
GRID_OG_DIMENSION,
|
||||
MAX_PHOTOS_TO_SHOW_TEMPLATE,
|
||||
} from '@/photo/image-response';
|
||||
import TemplateImageResponse from
|
||||
@ -22,7 +22,7 @@ export async function GET() {
|
||||
getImageCacheHeadersForAuth(await auth()),
|
||||
]);
|
||||
|
||||
const { width, height } = GRID_OG_SIZE;
|
||||
const { width, height } = GRID_OG_DIMENSION;
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { blobToImage } from '@/utility/blob';
|
||||
import { useRef, useState } from 'react';
|
||||
import { CopyExif } from '@/lib/CopyExif';
|
||||
import exifr from 'exifr';
|
||||
import { cc } from '@/utility/css';
|
||||
import Spinner from './Spinner';
|
||||
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';
|
||||
@ -91,41 +92,94 @@ export default function ImageInput({
|
||||
hasMultipleUploads: files.length > 1,
|
||||
isLastBlob: i === files.length - 1,
|
||||
};
|
||||
|
||||
const canvas = ref.current;
|
||||
if (!(maxSize && canvas)) {
|
||||
// No need to process
|
||||
await onBlobReady?.({
|
||||
...callbackArgs,
|
||||
blob: file,
|
||||
});
|
||||
} else {
|
||||
|
||||
// Specify wide gamut to avoid data loss while resizing
|
||||
const ctx = canvas?.getContext(
|
||||
'2d', { colorSpace: 'display-p3' }
|
||||
);
|
||||
|
||||
if (maxSize && canvas && ctx) {
|
||||
// Process images that need resizing
|
||||
const image = await blobToImage(file);
|
||||
|
||||
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 =
|
||||
Math.round(ratio >= 1 ? maxSize : maxSize * ratio);
|
||||
const height =
|
||||
Math.round(ratio >= 1 ? maxSize / ratio : maxSize);
|
||||
|
||||
|
||||
canvas.width = width;
|
||||
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
|
||||
const ctx = canvas.getContext(
|
||||
'2d',
|
||||
{ colorSpace: 'display-p3' },
|
||||
);
|
||||
|
||||
ctx?.drawImage(
|
||||
image,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
);
|
||||
switch(orientation) {
|
||||
case 2:
|
||||
ctx.translate(width, 0);
|
||||
ctx.scale(-1, 1);
|
||||
break;
|
||||
case 3:
|
||||
ctx.translate(width, height);
|
||||
ctx.rotate((180 / 180) * Math.PI);
|
||||
break;
|
||||
case 4:
|
||||
ctx.translate(0, 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(
|
||||
async blob => {
|
||||
if (blob) {
|
||||
@ -139,6 +193,12 @@ export default function ImageInput({
|
||||
'image/jpeg',
|
||||
quality,
|
||||
);
|
||||
} else {
|
||||
// No need to process
|
||||
await onBlobReady?.({
|
||||
...callbackArgs,
|
||||
blob: file,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import { cc } from '@/utility/css';
|
||||
import Link from 'next/link';
|
||||
import { BiError } from 'react-icons/bi';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import { IMAGE_OG_SIZE } from '../photo/image-response';
|
||||
import { IMAGE_OG_DIMENSION } from '../photo/image-response';
|
||||
|
||||
export type OGLoadingState = 'unloaded' | 'loading' | 'loaded' | 'failed';
|
||||
|
||||
@ -44,7 +44,7 @@ export default function OGTile({
|
||||
}
|
||||
}, [loadingStateExternal, loadingStateInternal]);
|
||||
|
||||
const { width, height, ratio } = IMAGE_OG_SIZE;
|
||||
const { width, height, aspectRatio } = IMAGE_OG_DIMENSION;
|
||||
|
||||
return (
|
||||
<Link
|
||||
@ -59,7 +59,7 @@ export default function OGTile({
|
||||
>
|
||||
<div
|
||||
className="relative"
|
||||
style={{ aspectRatio: ratio }}
|
||||
style={{ aspectRatio }}
|
||||
>
|
||||
{loadingState === 'loading' &&
|
||||
<div className={cc(
|
||||
|
||||
@ -18,9 +18,9 @@ import {
|
||||
generateLocalPostgresString,
|
||||
} from '@/utility/date';
|
||||
import { toastSuccess, toastWarning } from '@/toast';
|
||||
import { getDimensionsFromSize } from '@/utility/size';
|
||||
|
||||
const THUMBNAIL_WIDTH = 300;
|
||||
const THUMBNAIL_HEIGHT = 200;
|
||||
const THUMBNAIL_SIZE = 300;
|
||||
|
||||
export default function PhotoForm({
|
||||
initialPhotoForm,
|
||||
@ -67,6 +67,11 @@ export default function PhotoForm({
|
||||
}
|
||||
}, [updatedExifData]);
|
||||
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
} = getDimensionsFromSize(THUMBNAIL_SIZE, formData.aspectRatio);
|
||||
|
||||
// Generate local date strings when
|
||||
// none can be harvested from EXIF
|
||||
useEffect(() => {
|
||||
@ -99,29 +104,36 @@ export default function PhotoForm({
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-[38rem]">
|
||||
<NextImage
|
||||
alt="Upload"
|
||||
src={url}
|
||||
className={cc(
|
||||
'border rounded-md overflow-hidden',
|
||||
'border-gray-200 dark:border-gray-700'
|
||||
)}
|
||||
width={THUMBNAIL_WIDTH}
|
||||
height={THUMBNAIL_HEIGHT}
|
||||
priority
|
||||
/>
|
||||
<CanvasBlurCapture
|
||||
imageUrl={url}
|
||||
width={THUMBNAIL_WIDTH}
|
||||
height={THUMBNAIL_HEIGHT}
|
||||
onCapture={updateBlurData}
|
||||
/>
|
||||
{debugBlur && formData.blurData &&
|
||||
<img
|
||||
alt="blur"
|
||||
src={formData.blurData}
|
||||
width={1000}
|
||||
/>}
|
||||
<div className="flex gap-2">
|
||||
<NextImage
|
||||
alt="Upload"
|
||||
src={url}
|
||||
className={cc(
|
||||
'border rounded-md overflow-hidden',
|
||||
'border-gray-200 dark:border-gray-700'
|
||||
)}
|
||||
width={width}
|
||||
height={height}
|
||||
priority
|
||||
/>
|
||||
<CanvasBlurCapture
|
||||
imageUrl={url}
|
||||
width={width}
|
||||
height={height}
|
||||
onCapture={updateBlurData}
|
||||
/>
|
||||
{debugBlur && formData.blurData &&
|
||||
<img
|
||||
alt="blur"
|
||||
src={formData.blurData}
|
||||
className={cc(
|
||||
'border rounded-md overflow-hidden',
|
||||
'border-gray-200 dark:border-gray-700'
|
||||
)}
|
||||
width={width}
|
||||
height={height}
|
||||
/>}
|
||||
</div>
|
||||
<form
|
||||
action={type === 'create' ? createPhotoAction : updatePhotoAction}
|
||||
className="space-y-6 pb-12"
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
convertTimestampToNaivePostgresString,
|
||||
convertTimestampWithOffsetToPostgresString,
|
||||
} from '@/utility/date';
|
||||
import { getOffsetFromExif } from '@/utility/exif';
|
||||
import { getAspectRatioFromExif, getOffsetFromExif } from '@/utility/exif';
|
||||
import { toFixedNumber } from '@/utility/number';
|
||||
import { convertStringToArray } from '@/utility/string';
|
||||
import { generateNanoid } from '@/utility/nanoid';
|
||||
@ -97,10 +97,7 @@ export const convertExifToFormData = (
|
||||
data: ExifData,
|
||||
filmSimulation?: FilmSimulation,
|
||||
): Record<keyof PhotoExif, string | undefined> => ({
|
||||
aspectRatio: (
|
||||
(data.imageSize?.width ?? 3.0) /
|
||||
(data.imageSize?.height ?? 2.0)
|
||||
).toString(),
|
||||
aspectRatio: getAspectRatioFromExif(data).toString(),
|
||||
make: data.tags?.Make,
|
||||
model: data.tags?.Model,
|
||||
focalLength: data.tags?.FocalLength?.toString(),
|
||||
|
||||
@ -1,35 +1,36 @@
|
||||
import { NextImageSize } from '@/services/next-image';
|
||||
import { getDimensionsFromSize } from '@/utility/size';
|
||||
|
||||
export const MAX_PHOTOS_TO_SHOW_OG = 12;
|
||||
export const MAX_PHOTOS_TO_SHOW_PER_TAG = 6;
|
||||
export const MAX_PHOTOS_TO_SHOW_TEMPLATE = 16;
|
||||
export const MAX_PHOTOS_TO_SHOW_TEMPLATE_TIGHT = 12;
|
||||
|
||||
interface OGImageDimension {
|
||||
width: NextImageSize
|
||||
height: number
|
||||
aspectRatio: number
|
||||
}
|
||||
|
||||
// 16:9 og image ratio
|
||||
const IMAGE_OG_RATIO = 16 / 9;
|
||||
const IMAGE_OG_WIDTH: NextImageSize = 1080;
|
||||
const IMAGE_OG_HEIGHT = IMAGE_OG_WIDTH * (1 / IMAGE_OG_RATIO);
|
||||
export const IMAGE_OG_SIZE = {
|
||||
width: IMAGE_OG_WIDTH,
|
||||
height: IMAGE_OG_HEIGHT,
|
||||
ratio: IMAGE_OG_RATIO,
|
||||
};
|
||||
export const IMAGE_OG_DIMENSION = getDimensionsFromSize(
|
||||
IMAGE_OG_WIDTH,
|
||||
IMAGE_OG_RATIO,
|
||||
) as OGImageDimension;
|
||||
|
||||
// 16:9 og image ratio, small
|
||||
const IMAGE_OG_SMALL_WIDTH: NextImageSize = 828;
|
||||
const IMAGE_OG_SMALL_HEIGHT = IMAGE_OG_SMALL_WIDTH * (1 / IMAGE_OG_RATIO);
|
||||
export const IMAGE_OG_SMALL_SIZE = {
|
||||
width: IMAGE_OG_SMALL_WIDTH,
|
||||
height: IMAGE_OG_SMALL_HEIGHT,
|
||||
ratio: IMAGE_OG_RATIO,
|
||||
};
|
||||
export const IMAGE_OG_DIMENSION_SMALL = getDimensionsFromSize(
|
||||
IMAGE_OG_SMALL_WIDTH,
|
||||
IMAGE_OG_RATIO,
|
||||
) as OGImageDimension;
|
||||
|
||||
// 3:2 og grid ratio
|
||||
const GRID_OG_RATIO = 1.33;
|
||||
// 4:3 og grid ratio
|
||||
const GRID_OG_RATIO = 4 / 3;
|
||||
const GRID_OG_WIDTH: NextImageSize = 2048;
|
||||
const GRID_OG_HEIGHT = GRID_OG_WIDTH * (1 / GRID_OG_RATIO);
|
||||
export const GRID_OG_SIZE = {
|
||||
width: GRID_OG_WIDTH,
|
||||
height: GRID_OG_HEIGHT,
|
||||
ratio: GRID_OG_RATIO,
|
||||
};
|
||||
export const GRID_OG_DIMENSION = getDimensionsFromSize(
|
||||
GRID_OG_WIDTH,
|
||||
GRID_OG_RATIO,
|
||||
) as OGImageDimension;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ExifData } from 'ts-exif-parser';
|
||||
import { OrientationTypes, type ExifData } from 'ts-exif-parser';
|
||||
import { formatNumberToFraction } from './number';
|
||||
|
||||
const OFFSET_REGEX = /[+-]\d\d:\d\d/;
|
||||
@ -10,6 +10,27 @@ export const getOffsetFromExif = (data: ExifData) =>
|
||||
OFFSET_REGEX.test(value)
|
||||
) as string | undefined;
|
||||
|
||||
export const getAspectRatioFromExif = (data: ExifData): number => {
|
||||
// Using '||' operator to handle `Orientation` unexpectedly being '0'
|
||||
const orientation = data.tags?.Orientation || OrientationTypes.TOP_LEFT;
|
||||
|
||||
const width = data.imageSize?.width ?? 3.0;
|
||||
const height = data.imageSize?.height ?? 2.0;
|
||||
|
||||
switch (orientation) {
|
||||
case OrientationTypes.TOP_LEFT:
|
||||
case OrientationTypes.TOP_RIGHT:
|
||||
case OrientationTypes.BOTTOM_RIGHT:
|
||||
case OrientationTypes.BOTTOM_LEFT:
|
||||
case OrientationTypes.LEFT_TOP:
|
||||
case OrientationTypes.RIGHT_BOTTOM:
|
||||
return width / height;
|
||||
case OrientationTypes.RIGHT_TOP:
|
||||
case OrientationTypes.LEFT_BOTTOM:
|
||||
return height / width;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatFocalLength = (focalLength?: number) =>
|
||||
focalLength ? `${focalLength}mm` : undefined;
|
||||
|
||||
|
||||
29
src/utility/size.ts
Normal file
29
src/utility/size.ts
Normal file
@ -0,0 +1,29 @@
|
||||
const DEFAULT_ASPECT_RATIO = 3.0 / 2.0;
|
||||
|
||||
export const getDimensionsFromSize = (
|
||||
size: number,
|
||||
aspectRatioRaw?: string | number,
|
||||
): {
|
||||
width: number
|
||||
height: number
|
||||
aspectRatio: number
|
||||
} => {
|
||||
const aspectRatio = typeof aspectRatioRaw === 'string'
|
||||
? parseFloat(aspectRatioRaw)
|
||||
: aspectRatioRaw || DEFAULT_ASPECT_RATIO;
|
||||
|
||||
let width = size;
|
||||
let height = size;
|
||||
|
||||
if (aspectRatio > 1) {
|
||||
height = size / aspectRatio;
|
||||
} else if (aspectRatio < 1) {
|
||||
width = size * aspectRatio;
|
||||
}
|
||||
|
||||
return {
|
||||
width: Math.round(width),
|
||||
height: Math.round(height),
|
||||
aspectRatio,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user