EXIF title/caption capture (#294)
This commit is contained in:
parent
56a989afe6
commit
320f562cbc
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -20,6 +20,7 @@
|
||||
"depluralizes",
|
||||
"Eterna",
|
||||
"exif",
|
||||
"exifr",
|
||||
"exiftool",
|
||||
"favicons",
|
||||
"Favoriting",
|
||||
|
||||
19
package.json
19
package.json
@ -12,9 +12,9 @@
|
||||
"@ai-sdk/openai": "^1.3.23",
|
||||
"@aws-sdk/client-s3": "3.864.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.864.0",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-visually-hidden": "^1.2.3",
|
||||
"@upstash/ratelimit": "^2.0.6",
|
||||
"@upstash/redis": "^1.35.3",
|
||||
@ -28,6 +28,7 @@
|
||||
"culori": "^4.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"exifr": "^7.1.3",
|
||||
"extract-colors": "^4.2.1",
|
||||
"fast-average-color": "^9.5.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@ -43,7 +44,7 @@
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sharp": "^0.34.3",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.3.4",
|
||||
"swr": "^2.3.6",
|
||||
"ts-exif-parser": "^0.2.2",
|
||||
"use-debounce": "^10.0.5",
|
||||
"viewerjs": "^1.11.7"
|
||||
@ -55,15 +56,15 @@
|
||||
"@stylistic/eslint-plugin": "^5.2.3",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/jest-dom": "^6.7.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/culori": "^4.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/pg": "^8.15.5",
|
||||
"@types/react": "19.1.9",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"cross-fetch": "^4.1.0",
|
||||
@ -73,7 +74,7 @@
|
||||
"jest": "^30.0.5",
|
||||
"jest-environment-jsdom": "^30.0.5",
|
||||
"postcss": "8.5.6",
|
||||
"tailwindcss": "4.1.11",
|
||||
"tailwindcss": "4.1.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
|
||||
724
pnpm-lock.yaml
generated
724
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -37,8 +37,8 @@ export default function PhotoEditPageClient({
|
||||
setIsPending,
|
||||
updatedTitle,
|
||||
setUpdatedTitle,
|
||||
hasTextContent,
|
||||
setHasTextContent,
|
||||
shouldConfirmAiTextGeneration,
|
||||
setShouldConfirmAiTextGeneration,
|
||||
aiContent,
|
||||
} = usePhotoFormParent({
|
||||
photoForm,
|
||||
@ -59,7 +59,10 @@ export default function PhotoEditPageClient({
|
||||
accessory={
|
||||
<div className="flex gap-2">
|
||||
{hasAiTextGeneration &&
|
||||
<AiButton {...{ aiContent, shouldConfirm: hasTextContent }} />}
|
||||
<AiButton {...{
|
||||
aiContent,
|
||||
shouldConfirm: shouldConfirmAiTextGeneration,
|
||||
}} />}
|
||||
<ExifCaptureButton
|
||||
photoUrl={photo.url}
|
||||
onSync={setUpdatedExifData}
|
||||
@ -77,7 +80,7 @@ export default function PhotoEditPageClient({
|
||||
uniqueFilms={uniqueFilms}
|
||||
aiContent={hasAiTextGeneration ? aiContent : undefined}
|
||||
onTitleChange={setUpdatedTitle}
|
||||
onTextContentChange={setHasTextContent}
|
||||
onFormDataChange={setShouldConfirmAiTextGeneration}
|
||||
onFormStatusChange={setIsPending}
|
||||
/>
|
||||
</AdminChildPage>
|
||||
|
||||
@ -38,10 +38,11 @@ export default function UploadPageClient({
|
||||
setIsPending,
|
||||
updatedTitle,
|
||||
setUpdatedTitle,
|
||||
hasTextContent,
|
||||
setHasTextContent,
|
||||
shouldConfirmAiTextGeneration,
|
||||
setShouldConfirmAiTextGeneration,
|
||||
aiContent,
|
||||
} = usePhotoFormParent({
|
||||
photoForm: formDataFromExif,
|
||||
textFieldsToAutoGenerate,
|
||||
imageThumbnailBase64,
|
||||
});
|
||||
@ -61,7 +62,10 @@ export default function UploadPageClient({
|
||||
: blobId}
|
||||
breadcrumbEllipsis
|
||||
accessory={hasAiTextGeneration &&
|
||||
<AiButton {...{ aiContent, shouldConfirm: hasTextContent }} />}
|
||||
<AiButton {...{
|
||||
aiContent,
|
||||
shouldConfirm: shouldConfirmAiTextGeneration,
|
||||
}} />}
|
||||
isLoading={pending}
|
||||
>
|
||||
<PhotoForm
|
||||
@ -72,8 +76,8 @@ export default function UploadPageClient({
|
||||
aiContent={hasAiTextGeneration ? aiContent : undefined}
|
||||
shouldStripGpsData={shouldStripGpsData}
|
||||
onTitleChange={setUpdatedTitle}
|
||||
onTextContentChange={setHasTextContent}
|
||||
onFormStatusChange={setIsPending}
|
||||
onFormDataChange={setShouldConfirmAiTextGeneration}
|
||||
/>
|
||||
</AdminChildPage>
|
||||
);
|
||||
|
||||
@ -49,7 +49,7 @@ import {
|
||||
import { TAG_FAVS, isPhotoFav, isTagFavs } from '@/tag';
|
||||
import { convertPhotoToPhotoDbInsert, Photo } from '.';
|
||||
import { runAuthenticatedAdminServerAction } from '@/auth/server';
|
||||
import { AiImageQuery, getAiImageQuery } from './ai';
|
||||
import { AiImageQuery, getAiImageQuery, getAiTextFieldsToGenerate } from './ai';
|
||||
import { streamOpenAiImageQuery } from '@/platforms/openai';
|
||||
import {
|
||||
AI_TEXT_AUTO_GENERATED_FIELDS,
|
||||
@ -96,8 +96,8 @@ export const createPhotoAction = async (formData: FormData) =>
|
||||
// - addUploadsAction
|
||||
const addUpload = async ({
|
||||
url,
|
||||
title,
|
||||
tags,
|
||||
title: _title,
|
||||
tags: _tags,
|
||||
favorite,
|
||||
hidden,
|
||||
excludeFromFeeds,
|
||||
@ -138,24 +138,30 @@ const addUpload = async ({
|
||||
onStreamUpdate?.('Generating AI text');
|
||||
}
|
||||
|
||||
const title = _title || formDataFromExif.title;
|
||||
const caption = formDataFromExif.caption;
|
||||
const tags = _tags || formDataFromExif.tags;
|
||||
|
||||
const {
|
||||
title: aiTitle,
|
||||
caption,
|
||||
caption: aiCaption,
|
||||
tags: aiTags,
|
||||
semanticDescription,
|
||||
} = await generateAiImageQueries(
|
||||
imageResizedBase64,
|
||||
Boolean(title)
|
||||
? AI_TEXT_AUTO_GENERATED_FIELDS
|
||||
.filter(field => field !== 'title')
|
||||
: AI_TEXT_AUTO_GENERATED_FIELDS,
|
||||
getAiTextFieldsToGenerate(
|
||||
AI_TEXT_AUTO_GENERATED_FIELDS,
|
||||
Boolean(title),
|
||||
Boolean(caption),
|
||||
Boolean(tags),
|
||||
),
|
||||
title,
|
||||
);
|
||||
|
||||
const form: Partial<PhotoFormData> = {
|
||||
...formDataFromExif,
|
||||
title: title || aiTitle,
|
||||
caption,
|
||||
caption: caption || aiCaption,
|
||||
tags: tags || aiTags,
|
||||
excludeFromFeeds,
|
||||
hidden,
|
||||
@ -190,9 +196,7 @@ const addUpload = async ({
|
||||
};
|
||||
|
||||
export const addUploadAction = async (args: Parameters<typeof addUpload>[0]) =>
|
||||
runAuthenticatedAdminServerAction(async () => {
|
||||
await addUpload(args);
|
||||
});
|
||||
runAuthenticatedAdminServerAction(() => addUpload(args));
|
||||
|
||||
export const addUploadsAction = async ({
|
||||
uploadUrls,
|
||||
@ -536,7 +540,7 @@ export const syncPhotoAction = async (photoId: string, isBatch?: boolean) =>
|
||||
|
||||
const formDataFromPhoto = convertPhotoToFormData(photo);
|
||||
|
||||
// Don't overwrite manually configured fujifilm meta with null data
|
||||
// Don't overwrite manually configured meta with null data
|
||||
FIELDS_TO_NOT_OVERWRITE_WITH_NULL_DATA_ON_SYNC.forEach(field => {
|
||||
if (!formDataFromExif[field] && formDataFromPhoto[field]) {
|
||||
delete formDataFromExif[field];
|
||||
|
||||
@ -40,6 +40,21 @@ export const parseAiAutoGeneratedFieldsString = (
|
||||
}
|
||||
};
|
||||
|
||||
export const getAiTextFieldsToGenerate = (
|
||||
textFieldsToGenerate: AiAutoGeneratedField[],
|
||||
excludeTitle?: boolean,
|
||||
excludeCaption?: boolean,
|
||||
excludeTags?: boolean,
|
||||
excludeSemantic?: boolean,
|
||||
): AiAutoGeneratedField[] => {
|
||||
return textFieldsToGenerate.filter(field =>
|
||||
!(excludeTitle && field === 'title') &&
|
||||
!(excludeCaption && field === 'caption') &&
|
||||
!(excludeTags && field === 'tags') &&
|
||||
!(excludeSemantic && field === 'semantic'),
|
||||
);
|
||||
};
|
||||
|
||||
export type AiImageQuery =
|
||||
'title' |
|
||||
'caption' |
|
||||
|
||||
@ -61,7 +61,7 @@ const getColorDataFromImageUrl = async (
|
||||
url: string,
|
||||
isBatch?: boolean,
|
||||
): Promise<PhotoColorData> => {
|
||||
const ai = AI_CONTENT_GENERATION_ENABLED
|
||||
const ai = AI_CONTENT_GENERATION_ENABLED
|
||||
? await getColorFromAI(url, isBatch)
|
||||
: undefined;
|
||||
const average = await getAverageColorFromImageUrl(url);
|
||||
@ -95,7 +95,7 @@ export const getColorFieldsForPhotoDbInsert = async (
|
||||
...args: Parameters<typeof getColorFieldsForImageUrl>
|
||||
) => {
|
||||
const { colorData, ...rest } = await getColorFieldsForImageUrl(...args) ?? {};
|
||||
if (colorData) {
|
||||
if (colorData !== undefined) {
|
||||
return {
|
||||
colorData: JSON.stringify(colorData),
|
||||
...rest,
|
||||
@ -109,10 +109,12 @@ export const getColorFieldsForPhotoForm = async (
|
||||
) => {
|
||||
const { colorSort, ...rest } =
|
||||
await getColorFieldsForPhotoDbInsert(...args) ?? {};
|
||||
return {
|
||||
colorSort: `${colorSort}`,
|
||||
...rest,
|
||||
};
|
||||
if (colorSort !== undefined) {
|
||||
return {
|
||||
colorSort: `${colorSort}`,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getColorFromAI = async (
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
FormMeta,
|
||||
PhotoFormData,
|
||||
convertFormKeysToLabels,
|
||||
formHasTextContent,
|
||||
getChangedFormFields,
|
||||
getFormErrors,
|
||||
isFormValid,
|
||||
@ -65,7 +64,7 @@ export default function PhotoForm({
|
||||
aiContent,
|
||||
shouldStripGpsData,
|
||||
onTitleChange,
|
||||
onTextContentChange,
|
||||
onFormDataChange,
|
||||
onFormStatusChange,
|
||||
}: {
|
||||
type?: 'create' | 'edit'
|
||||
@ -78,7 +77,7 @@ export default function PhotoForm({
|
||||
aiContent?: AiContent
|
||||
shouldStripGpsData?: boolean
|
||||
onTitleChange?: (updatedTitle: string) => void
|
||||
onTextContentChange?: (hasContent: boolean) => void,
|
||||
onFormDataChange?: (formData: Partial<PhotoFormData>) => void,
|
||||
onFormStatusChange?: (pending: boolean) => void
|
||||
}) {
|
||||
const [formData, setFormData] =
|
||||
@ -188,8 +187,8 @@ export default function PhotoForm({
|
||||
[aiContent?.semanticDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
onTextContentChange?.(formHasTextContent(formData));
|
||||
}, [onTextContentChange, formData]);
|
||||
onFormDataChange?.(formData);
|
||||
}, [onFormDataChange, formData]);
|
||||
|
||||
const isFieldGeneratingAi = (key: keyof PhotoFormData) => {
|
||||
switch (key) {
|
||||
|
||||
@ -85,6 +85,7 @@ const FORM_METADATA = (
|
||||
label: 'title',
|
||||
capitalize: true,
|
||||
validateStringMaxLength: STRING_MAX_LENGTH_SHORT,
|
||||
shouldNotOverwriteWithNullDataOnSync: true,
|
||||
},
|
||||
caption: {
|
||||
label: 'caption',
|
||||
@ -245,12 +246,12 @@ export const isFormValid = (formData: Partial<PhotoFormData>) =>
|
||||
(!validateStringMaxLength || (formData[key]?.length ?? 0) <= validateStringMaxLength),
|
||||
);
|
||||
|
||||
export const formHasTextContent = ({
|
||||
export const formHasExistingAiTextContent = ({
|
||||
title,
|
||||
caption,
|
||||
tags,
|
||||
semanticDescription,
|
||||
}: Partial<PhotoFormData>) =>
|
||||
}: Partial<PhotoFormData> = {}) =>
|
||||
Boolean(title || caption || tags || semanticDescription);
|
||||
|
||||
// CREATE FORM DATA: FROM PHOTO
|
||||
|
||||
@ -15,38 +15,55 @@ import type { ExifData } from 'ts-exif-parser';
|
||||
|
||||
export const convertExifToFormData = (
|
||||
data: ExifData,
|
||||
dataExifr?: any,
|
||||
film?: FujifilmSimulation,
|
||||
recipeData?: FujifilmRecipe,
|
||||
): Omit<
|
||||
Record<keyof PhotoExif, string | undefined>,
|
||||
'takenAt' | 'takenAtNaive'
|
||||
> => ({
|
||||
aspectRatio: getAspectRatioFromExif(data).toString(),
|
||||
make: data.tags?.Make,
|
||||
model: data.tags?.Model,
|
||||
focalLength: data.tags?.FocalLength?.toString(),
|
||||
focalLengthIn35MmFormat: data.tags?.FocalLengthIn35mmFormat?.toString(),
|
||||
lensMake: data.tags?.LensMake,
|
||||
lensModel: data.tags?.LensModel,
|
||||
fNumber: (
|
||||
data.tags?.FNumber?.toString() ||
|
||||
convertApertureValueToFNumber(data.tags?.ApertureValue)
|
||||
),
|
||||
iso: data.tags?.ISO?.toString() || data.tags?.ISOSpeed?.toString(),
|
||||
exposureTime: data.tags?.ExposureTime?.toString(),
|
||||
exposureCompensation: data.tags?.ExposureCompensation?.toString(),
|
||||
latitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLatitude?.toString() : undefined,
|
||||
longitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
|
||||
film,
|
||||
recipeData: JSON.stringify(recipeData),
|
||||
...data.tags?.DateTimeOriginal && {
|
||||
takenAt: convertTimestampWithOffsetToPostgresString(
|
||||
data.tags.DateTimeOriginal,
|
||||
getOffsetFromExif(data),
|
||||
): Partial<Record<keyof PhotoExif, string | undefined>> => {
|
||||
let title: string | undefined = dataExifr?.title?.value;
|
||||
let caption: string | undefined;
|
||||
const description: string | undefined =
|
||||
data.tags?.ImageDescription ||
|
||||
dataExifr?.ImageDescription ||
|
||||
dataExifr?.description?.value;
|
||||
const tags: string[] | undefined = dataExifr?.subject;
|
||||
|
||||
if (title && title !== description) {
|
||||
caption = description;
|
||||
} else {
|
||||
title = description;
|
||||
}
|
||||
|
||||
return {
|
||||
aspectRatio: getAspectRatioFromExif(data).toString(),
|
||||
make: data.tags?.Make,
|
||||
model: data.tags?.Model,
|
||||
focalLength: data.tags?.FocalLength?.toString(),
|
||||
focalLengthIn35MmFormat: data.tags?.FocalLengthIn35mmFormat?.toString(),
|
||||
lensMake: data.tags?.LensMake,
|
||||
lensModel: data.tags?.LensModel,
|
||||
fNumber: (
|
||||
data.tags?.FNumber?.toString() ||
|
||||
convertApertureValueToFNumber(data.tags?.ApertureValue)
|
||||
),
|
||||
takenAtNaive:
|
||||
convertTimestampToNaivePostgresString(data.tags.DateTimeOriginal),
|
||||
},
|
||||
});
|
||||
iso: data.tags?.ISO?.toString() || data.tags?.ISOSpeed?.toString(),
|
||||
exposureTime: data.tags?.ExposureTime?.toString(),
|
||||
exposureCompensation: data.tags?.ExposureCompensation?.toString(),
|
||||
latitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLatitude?.toString() : undefined,
|
||||
longitude:
|
||||
!GEO_PRIVACY_ENABLED ? data.tags?.GPSLongitude?.toString() : undefined,
|
||||
film,
|
||||
recipeData: JSON.stringify(recipeData),
|
||||
...data.tags?.DateTimeOriginal && {
|
||||
takenAt: convertTimestampWithOffsetToPostgresString(
|
||||
data.tags.DateTimeOriginal,
|
||||
getOffsetFromExif(data),
|
||||
),
|
||||
takenAtNaive:
|
||||
convertTimestampToNaivePostgresString(data.tags.DateTimeOriginal),
|
||||
},
|
||||
...title && { title },
|
||||
...caption && { caption },
|
||||
...Array.isArray(tags) && { tags: tags.join(', ') },
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { PhotoFormData, formHasTextContent } from '.';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { PhotoFormData, formHasExistingAiTextContent } from '.';
|
||||
import useAiImageQueries from '../ai/useAiImageQueries';
|
||||
import { AiAutoGeneratedField } from '../ai';
|
||||
import { AiAutoGeneratedField, getAiTextFieldsToGenerate } from '../ai';
|
||||
|
||||
export default function usePhotoFormParent({
|
||||
photoForm,
|
||||
textFieldsToAutoGenerate,
|
||||
textFieldsToAutoGenerate: _textFieldsToAutoGenerate = [],
|
||||
imageThumbnailBase64,
|
||||
}: {
|
||||
photoForm?: Partial<PhotoFormData>
|
||||
@ -14,8 +14,29 @@ export default function usePhotoFormParent({
|
||||
}) {
|
||||
const [pending, setIsPending] = useState(false);
|
||||
const [updatedTitle, setUpdatedTitle] = useState('');
|
||||
const [hasTextContent, setHasTextContent] =
|
||||
useState(photoForm ? formHasTextContent(photoForm) : false);
|
||||
const [shouldConfirmAiTextGeneration, _setShouldConfirmAiTextGeneration] =
|
||||
useState(formHasExistingAiTextContent(photoForm));
|
||||
|
||||
const setShouldConfirmAiTextGeneration = useCallback(
|
||||
(updatedFormData: Partial<PhotoFormData>) => {
|
||||
_setShouldConfirmAiTextGeneration(
|
||||
formHasExistingAiTextContent(updatedFormData),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Don't auto-generate titles when they can be captured from EXIF data
|
||||
const textFieldsToAutoGenerate = useMemo(() =>
|
||||
getAiTextFieldsToGenerate(
|
||||
_textFieldsToAutoGenerate,
|
||||
Boolean(photoForm?.title),
|
||||
Boolean(photoForm?.caption),
|
||||
Boolean(photoForm?.tags),
|
||||
), [
|
||||
_textFieldsToAutoGenerate,
|
||||
photoForm?.title,
|
||||
photoForm?.caption,
|
||||
photoForm?.tags,
|
||||
]);
|
||||
|
||||
const aiContent = useAiImageQueries(
|
||||
textFieldsToAutoGenerate,
|
||||
@ -27,8 +48,8 @@ export default function usePhotoFormParent({
|
||||
setIsPending,
|
||||
updatedTitle,
|
||||
setUpdatedTitle,
|
||||
hasTextContent,
|
||||
setHasTextContent,
|
||||
shouldConfirmAiTextGeneration,
|
||||
setShouldConfirmAiTextGeneration,
|
||||
aiContent,
|
||||
};
|
||||
}
|
||||
|
||||
@ -68,6 +68,10 @@ export interface PhotoExif {
|
||||
recipeData?: string
|
||||
takenAt?: string
|
||||
takenAtNaive?: string
|
||||
// Photo meta potentially located in EXIF/XMP data
|
||||
title?: string
|
||||
caption?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
// Raw db insert
|
||||
@ -76,7 +80,6 @@ export interface PhotoDbInsert extends PhotoExif {
|
||||
url: string
|
||||
extension: string
|
||||
blurData?: string
|
||||
title?: string
|
||||
caption?: string
|
||||
semanticDescription?: string
|
||||
tags?: string[]
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
import { PhotoDbInsert } from '.';
|
||||
import { convertExifToFormData } from './form/server';
|
||||
import { getColorFieldsForPhotoForm } from './color/server';
|
||||
import exifr from 'exifr';
|
||||
|
||||
const IMAGE_WIDTH_RESIZE = 200;
|
||||
const IMAGE_WIDTH_BLUR = 200;
|
||||
@ -58,6 +59,7 @@ export const extractImageDataFromBlobPath = async (
|
||||
const extension = getExtensionFromStorageUrl(url);
|
||||
|
||||
let exifData: ExifData | undefined;
|
||||
let exifrData: any | undefined;
|
||||
let film: FujifilmSimulation | undefined;
|
||||
let recipe: FujifilmRecipe | undefined;
|
||||
let blurData: string | undefined;
|
||||
@ -80,6 +82,7 @@ export const extractImageDataFromBlobPath = async (
|
||||
// Data for form
|
||||
parser.enableBinaryFields(false);
|
||||
exifData = parser.parse();
|
||||
exifrData = await exifr.parse(fileBytes, { xmp: true });
|
||||
|
||||
// Capture film simulation for Fujifilm cameras
|
||||
if (isExifForFujifilm(exifData)) {
|
||||
@ -126,7 +129,7 @@ export const extractImageDataFromBlobPath = async (
|
||||
url,
|
||||
},
|
||||
...generateBlurData && { blurData },
|
||||
...convertExifToFormData(exifData, film, recipe),
|
||||
...convertExifToFormData(exifData, exifrData, film, recipe),
|
||||
...colorFields,
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user