diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index ed6ea48cb..e55a26f15 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -141,14 +141,18 @@ export const attachIPCHandlers = () => { // - Conversion - ipcMain.handle("convertToJPEG", (_, fileName, imageData) => - convertToJPEG(fileName, imageData), + ipcMain.handle("convertToJPEG", (_, imageData: Uint8Array) => + convertToJPEG(imageData), ); ipcMain.handle( "generateImageThumbnail", - (_, inputFile, maxDimension, maxSize) => - generateImageThumbnail(inputFile, maxDimension, maxSize), + ( + _, + dataOrPath: Uint8Array | string, + maxDimension: number, + maxSize: number, + ) => generateImageThumbnail(dataOrPath, maxDimension, maxSize), ); ipcMain.handle( diff --git a/desktop/src/main/services/convert.ts b/desktop/src/main/services/convert.ts index 71e8f419e..689d73fb0 100644 --- a/desktop/src/main/services/convert.ts +++ b/desktop/src/main/services/convert.ts @@ -3,20 +3,17 @@ import { existsSync } from "fs"; import fs from "node:fs/promises"; import path from "path"; -import { CustomErrorMessage, ElectronFile } from "../../types/ipc"; +import { CustomErrorMessage } from "../../types/ipc"; import log from "../log"; import { writeStream } from "../stream"; import { execAsync, isDev } from "../utils-electron"; import { deleteTempFile, makeTempFilePath } from "../utils-temp"; -export const convertToJPEG = async ( - fileName: string, - imageData: Uint8Array, -): Promise => { - const inputFilePath = await makeTempFilePath(fileName); +export const convertToJPEG = async (imageData: Uint8Array) => { + const inputFilePath = await makeTempFilePath(); const outputFilePath = await makeTempFilePath(".jpeg"); - // Construct the command first, it may throw on NotAvailable on win32. + // Construct the command first, it may throw NotAvailable on win32. const command = convertToJPEGCommand(inputFilePath, outputFilePath); try { @@ -106,10 +103,28 @@ const IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [ ]; export async function generateImageThumbnail( - inputFile: File | ElectronFile, + dataOrPath: Uint8Array | string, maxDimension: number, maxSize: number, ): Promise { + const inputFilePath = await makeTempFilePath(fileName); + const outputFilePath = await makeTempFilePath(".jpeg"); + + // Construct the command first, it may throw NotAvailable on win32. + const command = convertToJPEGCommand(inputFilePath, outputFilePath); + + try { + await fs.writeFile(inputFilePath, imageData); + await execAsync(command); + return new Uint8Array(await fs.readFile(outputFilePath)); + } finally { + try { + deleteTempFile(outputFilePath); + deleteTempFile(inputFilePath); + } catch (e) { + log.error("Ignoring error when cleaning up temp files", e); + } + } let inputFilePath = null; let createdTempInputFile = null; try { diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index c2af31abc..728e8d012 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -124,20 +124,17 @@ const fsIsDir = (dirPath: string): Promise => // - Conversion -const convertToJPEG = ( - fileName: string, - imageData: Uint8Array, -): Promise => - ipcRenderer.invoke("convertToJPEG", fileName, imageData); +const convertToJPEG = (imageData: Uint8Array): Promise => + ipcRenderer.invoke("convertToJPEG", imageData); const generateImageThumbnail = ( - inputFile: File | ElectronFile, + dataOrPath: Uint8Array | string, maxDimension: number, maxSize: number, ): Promise => ipcRenderer.invoke( "generateImageThumbnail", - inputFile, + dataOrPath, maxDimension, maxSize, ); diff --git a/web/apps/photos/src/services/upload/thumbnail.ts b/web/apps/photos/src/services/upload/thumbnail.ts index 9aad060b6..a23e68a2e 100644 --- a/web/apps/photos/src/services/upload/thumbnail.ts +++ b/web/apps/photos/src/services/upload/thumbnail.ts @@ -59,15 +59,12 @@ export const generateThumbnail = async ( const thumbnail = fileTypeInfo.fileType === FILE_TYPE.IMAGE ? await generateImageThumbnail(blob, fileTypeInfo) - : await generateVideoThumbnail(blob, fileTypeInfo); + : await generateVideoThumbnail(blob); if (thumbnail.length == 0) throw new Error("Empty thumbnail"); return { thumbnail, hasStaticThumbnail: false }; } catch (e) { - log.error( - `Failed to generate thumbnail for format ${fileTypeInfo.exactType}`, - e, - ); + log.error(`Failed to generate ${fileTypeInfo.exactType} thumbnail`, e); return { thumbnail: fallbackThumbnail(), hasStaticThumbnail: true }; } }; diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 5c715fa48..a6cb640b6 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -292,14 +292,12 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { return imageBlob; } - let jpegBlob: Blob | undefined; - const available = !moduleState.isNativeJPEGConversionNotAvailable; if (isElectron() && available && isSupportedRawFormat(exactType)) { // If we're running in our desktop app, see if our Node.js layer can // convert this into a JPEG using native tools for us. try { - jpegBlob = await nativeConvertToJPEG(fileName, imageBlob); + return await nativeConvertToJPEG(imageBlob); } catch (e) { if (e.message == CustomErrorMessage.NotAvailable) { moduleState.isNativeJPEGConversionNotAvailable = true; @@ -309,12 +307,12 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { } } - if (!jpegBlob && isFileHEIC(exactType)) { + if (!isFileHEIC(exactType)) { // If it is an HEIC file, use our web HEIC converter. - jpegBlob = await heicToJPEG(imageBlob); + return await heicToJPEG(imageBlob); } - return jpegBlob; + return undefined; } catch (e) { log.error( `Failed to get renderable image for ${JSON.stringify(fileTypeInfo ?? fileName)}`, @@ -324,7 +322,7 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { } }; -const nativeConvertToJPEG = async (fileName: string, imageBlob: Blob) => { +const nativeConvertToJPEG = async (imageBlob: Blob) => { const startTime = Date.now(); const imageData = new Uint8Array(await imageBlob.arrayBuffer()); const electron = globalThis.electron; @@ -332,8 +330,8 @@ const nativeConvertToJPEG = async (fileName: string, imageBlob: Blob) => { // the main thread since workers don't have access to the `window` (and // thus, to the `window.electron`) object. const jpegData = electron - ? await electron.convertToJPEG(fileName, imageData) - : await workerBridge.convertToJPEG(fileName, imageData); + ? await electron.convertToJPEG(imageData) + : await workerBridge.convertToJPEG(imageData); log.debug(() => `Native JPEG conversion took ${Date.now() - startTime} ms`); return new Blob([jpegData]); }; diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index ef10a43fe..b7ad3c0f5 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -204,14 +204,10 @@ export interface Electron { * yet possible, this function will throw an error with the * {@link CustomErrorMessage.NotAvailable} message. * - * @param fileName The name of the file whose data we're being given. * @param imageData The raw image data (the contents of the image file). * @returns JPEG data of the converted image. */ - convertToJPEG: ( - fileName: string, - imageData: Uint8Array, - ) => Promise; + convertToJPEG: (imageData: Uint8Array) => Promise; /** * Generate a JPEG thumbnail for the given image. @@ -224,14 +220,16 @@ export interface Electron { * not yet possible, this function will throw an error with the * {@link CustomErrorMessage.NotAvailable} message. * - * @param inputFile The file whose thumbnail we want. + * @param dataOrPath The data-of or path-to the image whose thumbnail we + * want. * @param maxDimension The maximum width or height of the generated * thumbnail. * @param maxSize Maximum size (in bytes) of the generated thumbnail. + * * @returns JPEG data of the generated thumbnail. */ generateImageThumbnail: ( - inputFile: File | ElectronFile, + dataOrPath: Uint8Array | string, maxDimension: number, maxSize: number, ) => Promise; diff --git a/web/packages/next/worker/comlink-worker.ts b/web/packages/next/worker/comlink-worker.ts index 7bae126a4..5929e5361 100644 --- a/web/packages/next/worker/comlink-worker.ts +++ b/web/packages/next/worker/comlink-worker.ts @@ -44,8 +44,8 @@ const workerBridge = { logToDisk, // Needed by ML worker getAuthToken: () => ensureLocalUser().then((user) => user.token), - convertToJPEG: (fileName: string, imageData: Uint8Array) => - ensureElectron().convertToJPEG(fileName, imageData), + convertToJPEG: (imageData: Uint8Array) => + ensureElectron().convertToJPEG(imageData), detectFaces: (input: Float32Array) => ensureElectron().detectFaces(input), faceEmbedding: (input: Float32Array) => ensureElectron().faceEmbedding(input),