Refactor 1
This commit is contained in:
parent
7a0abf2268
commit
1f0c80cabc
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
Loading…
Reference in a new issue