Refactor 1

This commit is contained in:
Manav Rathi 2024-04-23 10:21:39 +05:30
parent 7a0abf2268
commit 1f0c80cabc
No known key found for this signature in database
8 changed files with 111 additions and 70 deletions

View file

@ -147,8 +147,12 @@ export const attachIPCHandlers = () => {
ipcMain.handle( ipcMain.handle(
"generateImageThumbnail", "generateImageThumbnail",
(_, imageData: Uint8Array, maxDimension: number, maxSize: number) => (
generateImageThumbnail(imageData, maxDimension, maxSize), _,
dataOrPath: Uint8Array | string,
maxDimension: number,
maxSize: number,
) => generateImageThumbnail(dataOrPath, maxDimension, maxSize),
); );
ipcMain.handle( ipcMain.handle(

View file

@ -48,17 +48,19 @@ export const ffmpegExec = async (
let inputFilePath: string; let inputFilePath: string;
let isInputFileTemporary: boolean; let isInputFileTemporary: boolean;
if (typeof dataOrPath == "string") { if (dataOrPath instanceof Uint8Array) {
inputFilePath = dataOrPath;
isInputFileTemporary = false;
} else {
inputFilePath = await makeTempFilePath(); inputFilePath = await makeTempFilePath();
isInputFileTemporary = true; isInputFileTemporary = true;
await fs.writeFile(inputFilePath, dataOrPath); } else {
inputFilePath = dataOrPath;
isInputFileTemporary = false;
} }
const outputFilePath = await makeTempFilePath(); const outputFilePath = await makeTempFilePath();
try { try {
if (dataOrPath instanceof Uint8Array)
await fs.writeFile(inputFilePath, dataOrPath);
const cmd = substitutePlaceholders( const cmd = substitutePlaceholders(
command, command,
inputFilePath, inputFilePath,

View file

@ -63,14 +63,23 @@ const imageMagickPath = () =>
path.join(isDev ? "build" : process.resourcesPath, "image-magick"); path.join(isDev ? "build" : process.resourcesPath, "image-magick");
export const generateImageThumbnail = async ( export const generateImageThumbnail = async (
imageData: Uint8Array, dataOrPath: Uint8Array | string,
maxDimension: number, maxDimension: number,
maxSize: number, maxSize: number,
): Promise<Uint8Array> => { ): Promise<Uint8Array> => {
const inputFilePath = await makeTempFilePath(); let inputFilePath: string;
let isInputFileTemporary: boolean;
if (dataOrPath instanceof Uint8Array) {
inputFilePath = await makeTempFilePath();
isInputFileTemporary = true;
} else {
inputFilePath = dataOrPath;
isInputFileTemporary = false;
}
const outputFilePath = await makeTempFilePath(".jpeg"); const outputFilePath = await makeTempFilePath(".jpeg");
// Construct the command first, it may throw NotAvailable on win32. // Construct the command first, it may throw `NotAvailable` on win32.
let quality = 70; let quality = 70;
let command = generateImageThumbnailCommand( let command = generateImageThumbnailCommand(
inputFilePath, inputFilePath,
@ -80,8 +89,8 @@ export const generateImageThumbnail = async (
); );
try { try {
if (imageData instanceof Uint8Array) if (dataOrPath instanceof Uint8Array)
await fs.writeFile(inputFilePath, imageData); await fs.writeFile(inputFilePath, dataOrPath);
let thumbnail: Uint8Array; let thumbnail: Uint8Array;
do { do {
@ -98,7 +107,7 @@ export const generateImageThumbnail = async (
return thumbnail; return thumbnail;
} finally { } finally {
try { try {
await deleteTempFile(inputFilePath); if (isInputFileTemporary) await deleteTempFile(inputFilePath);
await deleteTempFile(outputFilePath); await deleteTempFile(outputFilePath);
} catch (e) { } catch (e) {
log.error("Ignoring error when cleaning up temp files", e); log.error("Ignoring error when cleaning up temp files", e);

View file

@ -128,13 +128,13 @@ const convertToJPEG = (imageData: Uint8Array): Promise<Uint8Array> =>
ipcRenderer.invoke("convertToJPEG", imageData); ipcRenderer.invoke("convertToJPEG", imageData);
const generateImageThumbnail = ( const generateImageThumbnail = (
imageData: Uint8Array, dataOrPath: Uint8Array | string,
maxDimension: number, maxDimension: number,
maxSize: number, maxSize: number,
): Promise<Uint8Array> => ): Promise<Uint8Array> =>
ipcRenderer.invoke( ipcRenderer.invoke(
"generateImageThumbnail", "generateImageThumbnail",
imageData, dataOrPath,
maxDimension, maxDimension,
maxSize, maxSize,
); );

View file

@ -1,5 +1,5 @@
import log from "@/next/log"; import log from "@/next/log";
import { CustomErrorMessage, type Electron } from "@/next/types/ipc"; import { type Electron } from "@/next/types/ipc";
import { withTimeout } from "@ente/shared/utils"; import { withTimeout } from "@ente/shared/utils";
import { FILE_TYPE } from "constants/file"; import { FILE_TYPE } from "constants/file";
import { BLACK_THUMBNAIL_BASE64 } from "constants/upload"; import { BLACK_THUMBNAIL_BASE64 } from "constants/upload";
@ -13,30 +13,6 @@ const maxThumbnailDimension = 720;
/** Maximum size (in bytes) of the generated thumbnail */ /** Maximum size (in bytes) of the generated thumbnail */
const maxThumbnailSize = 100 * 1024; // 100 KB const maxThumbnailSize = 100 * 1024; // 100 KB
class ModuleState {
/**
* This will be set to true if we get an error from the Node.js side of our
* desktop app telling us that native JPEG conversion is not available for
* the current OS/arch combination. That way, we can stop pestering it again
* and again (saving an IPC round-trip).
*
* Note the double negative when it is used.
*/
isNativeThumbnailCreationNotAvailable = false;
}
const moduleState = new ModuleState();
interface GeneratedThumbnail {
/** The JPEG data of the generated thumbnail */
thumbnail: Uint8Array;
/**
* `true` if this is a fallback (all black) thumbnail we're returning since
* thumbnail generation failed for some reason.
*/
hasStaticThumbnail: boolean;
}
/** /**
* Generate a JPEG thumbnail for the given image or video data. * Generate a JPEG thumbnail for the given image or video data.
* *
@ -46,19 +22,18 @@ interface GeneratedThumbnail {
* itself that might not yet have support for more exotic formats. * itself that might not yet have support for more exotic formats.
* *
* @param blob The data (blob) of the file whose thumbnail we want to generate. * @param blob The data (blob) of the file whose thumbnail we want to generate.
* @param fileTypeInfo The type of the file whose {@link blob} we were given. * @param fileTypeInfo The type information for the file.
* *
* @return {@link GeneratedThumbnail}, a thin wrapper for the raw JPEG bytes of * @return The JPEG data of the generated thumbnail.
* the generated thumbnail.
*/ */
export const generateThumbnail = async ( export const generateThumbnail = async (
blob: Blob, blob: Blob,
fileTypeInfo: FileTypeInfo, fileTypeInfo: FileTypeInfo,
): Promise<GeneratedThumbnail> => { ): Promise<Uint8Array> => {
try { try {
const thumbnail = const thumbnail =
fileTypeInfo.fileType === FILE_TYPE.IMAGE fileTypeInfo.fileType === FILE_TYPE.IMAGE
? await generateImageThumbnail(blob, fileTypeInfo) ? await generateImageThumbnailUsingCanvas(blob, fileTypeInfo)
: await generateVideoThumbnail(blob); : await generateVideoThumbnail(blob);
if (thumbnail.length == 0) throw new Error("Empty thumbnail"); if (thumbnail.length == 0) throw new Error("Empty thumbnail");
@ -73,38 +48,54 @@ export const generateThumbnail = async (
* A fallback, black, thumbnail for use in cases where thumbnail generation * A fallback, black, thumbnail for use in cases where thumbnail generation
* fails. * fails.
*/ */
const fallbackThumbnail = () => export const fallbackThumbnail = () =>
Uint8Array.from(atob(BLACK_THUMBNAIL_BASE64), (c) => c.charCodeAt(0)); Uint8Array.from(atob(BLACK_THUMBNAIL_BASE64), (c) => c.charCodeAt(0));
const generateImageThumbnail = async ( /**
blob: Blob, * Generate a JPEG thumbnail for the given file using native tools.
*
* This function only works when we're running in the context of our desktop
* app, and this dependency is enforced by the need to pass the {@link electron}
* object which we use to perform IPC with the Node.js side of our desktop app.
*
* @param fileOrPath Either the image or video File, or the path to the image or
* video file on the user's local filesystem, whose thumbnail we want to
* generate.
*
* @param fileTypeInfo The type information for the file.
*
* @return The JPEG data of the generated thumbnail.
*
* @see {@link generateThumbnail}.
*/
export const generateThumbnailNative = async (
electron: Electron,
fileOrPath: File | string,
fileTypeInfo: FileTypeInfo, fileTypeInfo: FileTypeInfo,
) => { ): Promise<GeneratedThumbnail> => {
const electron = globalThis.electron; try {
const available = !moduleState.isNativeThumbnailCreationNotAvailable; const thumbnail =
if (electron && available) { fileTypeInfo.fileType === FILE_TYPE.IMAGE
try { ? await generateImageThumbnailNative(electron, fileOrPath)
return await generateImageThumbnailInElectron(electron, blob); : await generateVideoThumbnail(blob);
} catch (e) {
if (e.message == CustomErrorMessage.NotAvailable) {
moduleState.isNativeThumbnailCreationNotAvailable = true;
} else {
log.error("Native thumbnail creation failed", e);
}
}
}
return await generateImageThumbnailUsingCanvas(blob, fileTypeInfo); if (thumbnail.length == 0) throw new Error("Empty thumbnail");
return { thumbnail, hasStaticThumbnail: false };
} catch (e) {
log.error(`Failed to generate ${fileTypeInfo.exactType} thumbnail`, e);
return { thumbnail: fallbackThumbnail(), hasStaticThumbnail: true };
}
}; };
const generateImageThumbnailInElectron = async ( const generateImageThumbnailNative = async (
electron: Electron, electron: Electron,
blob: Blob, fileOrPath: File | string,
): Promise<Uint8Array> => { ): Promise<Uint8Array> => {
const startTime = Date.now(); const startTime = Date.now();
const data = new Uint8Array(await blob.arrayBuffer());
const jpegData = await electron.generateImageThumbnail( const jpegData = await electron.generateImageThumbnail(
data, fileOrPath instanceof File
? new Uint8Array(await fileOrPath.arrayBuffer())
: fileOrPath,
maxThumbnailDimension, maxThumbnailDimension,
maxThumbnailSize, maxThumbnailSize,
); );

View file

@ -359,6 +359,22 @@ const readAsset = async (
: await readFile(fileTypeInfo, file); : await readFile(fileTypeInfo, file);
}; };
// TODO(MR): Merge with the uploader
class ModuleState {
/**
* This will be set to true if we get an error from the Node.js side of our
* desktop app telling us that native JPEG conversion is not available for
* the current OS/arch combination. That way, we can stop pestering it again
* and again (saving an IPC round-trip).
*
* Note the double negative when it is used.
*/
isNativeThumbnailCreationNotAvailable = false;
}
const moduleState = new ModuleState();
async function readFile( async function readFile(
fileTypeInfo: FileTypeInfo, fileTypeInfo: FileTypeInfo,
rawFile: File | ElectronFile, rawFile: File | ElectronFile,
@ -380,6 +396,20 @@ async function readFile(
filedata = await getUint8ArrayView(rawFile); filedata = await getUint8ArrayView(rawFile);
} }
const electron = globalThis.electron;
const available = !moduleState.isNativeThumbnailCreationNotAvailable;
if (electron && available) {
try {
return await generateImageThumbnailInElectron(electron, blob);
} catch (e) {
if (e.message == CustomErrorMessage.NotAvailable) {
moduleState.isNativeThumbnailCreationNotAvailable = true;
} else {
log.error("Native thumbnail creation failed", e);
}
}
}
if (filedata instanceof Uint8Array) { if (filedata instanceof Uint8Array) {
} else { } else {

View file

@ -114,7 +114,12 @@ export interface UploadURL {
export interface FileInMemory { export interface FileInMemory {
filedata: Uint8Array | DataStream; filedata: Uint8Array | DataStream;
/** The JPEG data of the generated thumbnail */
thumbnail: Uint8Array; thumbnail: Uint8Array;
/**
* `true` if this is a fallback (all black) thumbnail we're returning since
* thumbnail generation failed for some reason.
*/
hasStaticThumbnail: boolean; hasStaticThumbnail: boolean;
} }

View file

@ -221,8 +221,8 @@ export interface Electron {
* not yet possible, this function will throw an error with the * not yet possible, this function will throw an error with the
* {@link CustomErrorMessage.NotAvailable} message. * {@link CustomErrorMessage.NotAvailable} message.
* *
* @param imageData The raw image data (the contents of the image file) * @param dataOrPath The raw image data (the contents of the image file), or
* whose thumbnail we want to generate. * the path to the image file, whose thumbnail we want to generate.
* @param maxDimension The maximum width or height of the generated * @param maxDimension The maximum width or height of the generated
* thumbnail. * thumbnail.
* @param maxSize Maximum size (in bytes) of the generated thumbnail. * @param maxSize Maximum size (in bytes) of the generated thumbnail.
@ -230,7 +230,7 @@ export interface Electron {
* @returns JPEG data of the generated thumbnail. * @returns JPEG data of the generated thumbnail.
*/ */
generateImageThumbnail: ( generateImageThumbnail: (
imageData: Uint8Array, dataOrPath: Uint8Array | string,
maxDimension: number, maxDimension: number,
maxSize: number, maxSize: number,
) => Promise<Uint8Array>; ) => Promise<Uint8Array>;