From cff6570ebbbe0e2a561b532c375eaa00a5c7d528 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 22 Jul 2024 15:23:37 +0530 Subject: [PATCH] Move to a layer that should be dealing with the piexifjs internals --- web/apps/photos/src/services/export/index.ts | 5 +- web/apps/photos/src/utils/file/index.ts | 65 +----------------- .../new/photos/services/exif-update.ts | 67 +++++++++++++++++-- 3 files changed, 69 insertions(+), 68 deletions(-) diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 4400471c31..7efb6556cc 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -4,6 +4,7 @@ import { FILE_TYPE } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; import type { Metadata } from "@/media/types/file"; import downloadManager from "@/new/photos/services/download"; +import { updateExifIfNeededAndPossible } from "@/new/photos/services/exif-update"; import { exportMetadataDirectoryName, exportTrashDirectoryName, @@ -36,7 +37,7 @@ import { getCollectionUserFacingName, getNonEmptyPersonalCollections, } from "utils/collection"; -import { getPersonalFiles, updateExifIfNeeded } from "utils/file"; +import { getPersonalFiles } from "utils/file"; import { getAllLocalCollections } from "../collectionService"; import { migrateExport } from "./migration"; @@ -970,7 +971,7 @@ class ExportService { try { const fileUID = getExportRecordFileUID(file); const originalFileStream = await downloadManager.getFile(file); - const updatedFileStream = await updateExifIfNeeded( + const updatedFileStream = await updateExifIfNeededAndPossible( file, originalFileStream, ); diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 394468294c..fe4587e605 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -1,10 +1,9 @@ -import { lowercaseExtension } from "@/base/file"; import log from "@/base/log"; import { type Electron } from "@/base/types/ipc"; import { FILE_TYPE } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; import DownloadManager from "@/new/photos/services/download"; -import { setJPEGExifDateTimeOriginal } from "@/new/photos/services/exif-update"; +import { updateExifIfNeededAndPossible } from "@/new/photos/services/exif-update"; import { EncryptedEnteFile, EnteFile, @@ -74,7 +73,7 @@ export async function downloadFile(file: EnteFile) { new File([fileBlob], file.metadata.title), ); fileBlob = await new Response( - await updateExifIfNeeded(file, fileBlob.stream()), + await updateExifIfNeededAndPossible(file, fileBlob.stream()), ).blob(); fileBlob = new Blob([fileBlob], { type: fileType.mimeType }); const tempURL = URL.createObjectURL(fileBlob); @@ -86,64 +85,6 @@ export async function downloadFile(file: EnteFile) { } } -/** - * Return a new stream after applying Exif updates if applicable to the given - * stream, otherwise return the original. - * - * This function is meant to provide a stream that can be used to download (or - * export) a file to the user's computer after applying any Exif updates to the - * original file's data. - * - * - This only updates JPEG files. - * - * - For JPEG files, the DateTimeOriginal Exif entry is updated to reflect the - * time that the user edited within Ente. - * - * @param enteFile The {@link EnteFile} whose data we want. - * - * @param stream A {@link ReadableStream} containing the original data for - * {@link enteFile}. - * - * @returns A new {@link ReadableStream} with updates if any updates were - * needed, otherwise return the original stream. - */ -export const updateExifIfNeeded = async ( - enteFile: EnteFile, - stream: ReadableStream, -): Promise> => { - // Not an image. - if (enteFile.metadata.fileType != FILE_TYPE.IMAGE) return stream; - // Time was not edited. - if (!enteFile.pubMagicMetadata?.data.editedTime) return stream; - - const fileName = enteFile.metadata.title; - const extension = lowercaseExtension(fileName); - // Not a JPEG (likely). - if (extension != "jpeg" && extension != "jpg") return stream; - - const blob = await new Response(stream).blob(); - try { - const updatedBlob = await setJPEGExifDateTimeOriginal( - blob, - new Date(enteFile.pubMagicMetadata.data.editedTime / 1000), - ); - return updatedBlob.stream(); - } catch (e) { - log.error(`Failed to modify Exif date for ${fileName}`, e); - // We used the file's extension to determine if this was a JPEG, but - // this is not a guarantee. Misnamed files, while rare, do exist. So in - // that is the error thrown by the underlying library, fallback to the - // original instead of causing the entire download or export to fail. - if ( - e instanceof Error && - e.message.endsWith("Given file is neither JPEG nor TIFF") - ) { - return blob.stream(); - } - throw e; - } -}; - /** Segment the given {@link files} into lists indexed by their collection ID */ export const groupFilesBasedOnCollectionID = (files: EnteFile[]) => { const result = new Map(); @@ -507,7 +448,7 @@ async function downloadFileDesktop( const fs = electron.fs; const stream = await DownloadManager.getFile(file); - const updatedStream = await updateExifIfNeeded(file, stream); + const updatedStream = await updateExifIfNeededAndPossible(file, stream); if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { const fileBlob = await new Response(updatedStream).blob(); diff --git a/web/packages/new/photos/services/exif-update.ts b/web/packages/new/photos/services/exif-update.ts index 7440d7ce7a..4941cac04d 100644 --- a/web/packages/new/photos/services/exif-update.ts +++ b/web/packages/new/photos/services/exif-update.ts @@ -1,4 +1,66 @@ +import { lowercaseExtension } from "@/base/file"; +import log from "@/base/log"; +import { FILE_TYPE } from "@/media/file-type"; import piexif from "piexifjs"; +import type { EnteFile } from "../types/file"; + +/** + * Return a new stream after applying Exif updates if applicable to the given + * stream, otherwise return the original. + * + * This function is meant to provide a stream that can be used to download (or + * export) a file to the user's computer after applying any Exif updates to the + * original file's data. + * + * - This only updates JPEG files. + * + * - For JPEG files, the DateTimeOriginal Exif entry is updated to reflect the + * time that the user edited within Ente. + * + * @param enteFile The {@link EnteFile} whose data we want. + * + * @param stream A {@link ReadableStream} containing the original data for + * {@link enteFile}. + * + * @returns A new {@link ReadableStream} with updates if any updates were + * needed, otherwise return the original stream. + */ +export const updateExifIfNeededAndPossible = async ( + enteFile: EnteFile, + stream: ReadableStream, +): Promise> => { + // Not an image. + if (enteFile.metadata.fileType != FILE_TYPE.IMAGE) return stream; + // Time was not edited. + if (!enteFile.pubMagicMetadata?.data.editedTime) return stream; + + const fileName = enteFile.metadata.title; + const extension = lowercaseExtension(fileName); + // Not a JPEG (likely). + if (extension != "jpeg" && extension != "jpg") return stream; + + const blob = await new Response(stream).blob(); + try { + const updatedBlob = await setJPEGExifDateTimeOriginal( + blob, + new Date(enteFile.pubMagicMetadata.data.editedTime / 1000), + ); + return updatedBlob.stream(); + } catch (e) { + log.error(`Failed to modify Exif date for ${fileName}`, e); + // We used the file's extension to determine if this was a JPEG, but + // this is not a guarantee. Misnamed files, while rare, do exist. So in + // that is the error thrown by the underlying library, fallback to the + // original instead of causing the entire download or export to fail. + if ( + e instanceof Error && + e.message.endsWith("Given file is neither JPEG nor TIFF") + ) { + return blob.stream(); + } + throw e; + } +}; /** * Return a new blob with the "DateTimeOriginal" Exif tag set to the given @@ -11,10 +73,7 @@ import piexif from "piexifjs"; * * @returns A new blob derived from {@link jpegBlob} but with the updated date. */ -export const setJPEGExifDateTimeOriginal = async ( - jpegBlob: Blob, - date: Date, -) => { +const setJPEGExifDateTimeOriginal = async (jpegBlob: Blob, date: Date) => { let dataURL = await blobToDataURL(jpegBlob); // Since we pass a Blob without an associated type, we get back a generic // data URL of the form "data:application/octet-stream;base64,...".