Manav Rathi 1 gadu atpakaļ
vecāks
revīzija
dfa50e8ed1

+ 1 - 1
desktop/src/main/ipc.ts

@@ -38,7 +38,7 @@ import {
     updateAndRestart,
     updateOnNextRestart,
 } from "./services/app-update";
-import { convertToJPEG, generateImageThumbnail } from "./services/convert";
+import { convertToJPEG, generateImageThumbnail } from "./services/image";
 import { ffmpegExec } from "./services/ffmpeg";
 import { getDirFiles } from "./services/fs";
 import {

+ 0 - 256
desktop/src/main/services/convert.ts

@@ -1,256 +0,0 @@
-/** @file Image conversions */
-
-import { existsSync } from "fs";
-import fs from "node:fs/promises";
-import path from "path";
-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 (imageData: Uint8Array) => {
-    const inputFilePath = await makeTempFilePath();
-    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);
-        }
-    }
-};
-
-const convertToJPEGCommand = (
-    inputFilePath: string,
-    outputFilePath: string,
-) => {
-    switch (process.platform) {
-        case "darwin":
-            return [
-                "sips",
-                "-s",
-                "format",
-                "jpeg",
-                inputFilePath,
-                "--out",
-                outputFilePath,
-            ];
-        case "linux":
-            return [
-                imageMagickPath(),
-                inputFilePath,
-                "-quality",
-                "100%",
-                outputFilePath,
-            ];
-        default: // "win32"
-            throw new Error(CustomErrorMessage.NotAvailable);
-    }
-};
-
-/** Path to the Linux image-magick executable bundled with our app */
-const imageMagickPath = () =>
-    path.join(isDev ? "build" : process.resourcesPath, "image-magick");
-
-const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK";
-const MAX_DIMENSION_PLACEHOLDER = "MAX_DIMENSION";
-const SAMPLE_SIZE_PLACEHOLDER = "SAMPLE_SIZE";
-const INPUT_PATH_PLACEHOLDER = "INPUT";
-const OUTPUT_PATH_PLACEHOLDER = "OUTPUT";
-const QUALITY_PLACEHOLDER = "QUALITY";
-
-const MAX_QUALITY = 70;
-const MIN_QUALITY = 50;
-
-const SIPS_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [
-    "sips",
-    "-s",
-    "format",
-    "jpeg",
-    "-s",
-    "formatOptions",
-    QUALITY_PLACEHOLDER,
-    "-Z",
-    MAX_DIMENSION_PLACEHOLDER,
-    INPUT_PATH_PLACEHOLDER,
-    "--out",
-    OUTPUT_PATH_PLACEHOLDER,
-];
-
-const IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [
-    IMAGE_MAGICK_PLACEHOLDER,
-    INPUT_PATH_PLACEHOLDER,
-    "-auto-orient",
-    "-define",
-    `jpeg:size=${SAMPLE_SIZE_PLACEHOLDER}x${SAMPLE_SIZE_PLACEHOLDER}`,
-    "-thumbnail",
-    `${MAX_DIMENSION_PLACEHOLDER}x${MAX_DIMENSION_PLACEHOLDER}>`,
-    "-unsharp",
-    "0x.5",
-    "-quality",
-    QUALITY_PLACEHOLDER,
-    OUTPUT_PATH_PLACEHOLDER,
-];
-
-export async function generateImageThumbnail(
-    dataOrPath: Uint8Array | string,
-    maxDimension: number,
-    maxSize: number,
-): Promise<Uint8Array> {
-    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 {
-        if (process.platform == "win32")
-            throw Error(
-                CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED,
-            );
-        if (!existsSync(inputFile.path)) {
-            const tempFilePath = await makeTempFilePath(inputFile.name);
-            await writeStream(tempFilePath, await inputFile.stream());
-            inputFilePath = tempFilePath;
-            createdTempInputFile = true;
-        } else {
-            inputFilePath = inputFile.path;
-        }
-        const thumbnail = await generateImageThumbnail_(
-            inputFilePath,
-            maxDimension,
-            maxSize,
-        );
-        return thumbnail;
-    } finally {
-        if (createdTempInputFile) {
-            try {
-                await deleteTempFile(inputFilePath);
-            } catch (e) {
-                log.error(`Failed to deleteTempFile ${inputFilePath}`, e);
-            }
-        }
-    }
-}
-
-async function generateImageThumbnail_(
-    inputFilePath: string,
-    width: number,
-    maxSize: number,
-): Promise<Uint8Array> {
-    let tempOutputFilePath: string;
-    let quality = MAX_QUALITY;
-    try {
-        tempOutputFilePath = await makeTempFilePath(".jpeg");
-        let thumbnail: Uint8Array;
-        do {
-            await execAsync(
-                constructThumbnailGenerationCommand(
-                    inputFilePath,
-                    tempOutputFilePath,
-                    width,
-                    quality,
-                ),
-            );
-            thumbnail = new Uint8Array(await fs.readFile(tempOutputFilePath));
-            quality -= 10;
-        } while (thumbnail.length > maxSize && quality > MIN_QUALITY);
-        return thumbnail;
-    } catch (e) {
-        log.error("Failed to generate image thumbnail", e);
-        throw e;
-    } finally {
-        try {
-            await fs.rm(tempOutputFilePath, { force: true });
-        } catch (e) {
-            log.error(
-                `Failed to remove tempOutputFile ${tempOutputFilePath}`,
-                e,
-            );
-        }
-    }
-}
-
-function constructThumbnailGenerationCommand(
-    inputFilePath: string,
-    tempOutputFilePath: string,
-    maxDimension: number,
-    quality: number,
-) {
-    let thumbnailGenerationCmd: string[];
-    if (process.platform == "darwin") {
-        thumbnailGenerationCmd = SIPS_THUMBNAIL_GENERATE_COMMAND_TEMPLATE.map(
-            (cmdPart) => {
-                if (cmdPart === INPUT_PATH_PLACEHOLDER) {
-                    return inputFilePath;
-                }
-                if (cmdPart === OUTPUT_PATH_PLACEHOLDER) {
-                    return tempOutputFilePath;
-                }
-                if (cmdPart === MAX_DIMENSION_PLACEHOLDER) {
-                    return maxDimension.toString();
-                }
-                if (cmdPart === QUALITY_PLACEHOLDER) {
-                    return quality.toString();
-                }
-                return cmdPart;
-            },
-        );
-    } else if (process.platform == "linux") {
-        thumbnailGenerationCmd =
-            IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE.map((cmdPart) => {
-                if (cmdPart === IMAGE_MAGICK_PLACEHOLDER) {
-                    return imageMagickPath();
-                }
-                if (cmdPart === INPUT_PATH_PLACEHOLDER) {
-                    return inputFilePath;
-                }
-                if (cmdPart === OUTPUT_PATH_PLACEHOLDER) {
-                    return tempOutputFilePath;
-                }
-                if (cmdPart.includes(SAMPLE_SIZE_PLACEHOLDER)) {
-                    return cmdPart.replaceAll(
-                        SAMPLE_SIZE_PLACEHOLDER,
-                        (2 * maxDimension).toString(),
-                    );
-                }
-                if (cmdPart.includes(MAX_DIMENSION_PLACEHOLDER)) {
-                    return cmdPart.replaceAll(
-                        MAX_DIMENSION_PLACEHOLDER,
-                        maxDimension.toString(),
-                    );
-                }
-                if (cmdPart === QUALITY_PLACEHOLDER) {
-                    return quality.toString();
-                }
-                return cmdPart;
-            });
-    } else {
-        throw new Error(`Unsupported OS ${process.platform}`);
-    }
-    return thumbnailGenerationCmd;
-}

+ 10 - 6
desktop/src/main/services/ffmpeg.ts

@@ -1,9 +1,11 @@
 import pathToFfmpeg from "ffmpeg-static";
 import fs from "node:fs/promises";
+import log from "../log";
 import { withTimeout } from "../utils";
 import { execAsync } from "../utils-electron";
 import { deleteTempFile, makeTempFilePath } from "../utils-temp";
 
+/* Duplicated in the web app's code (used by the WASM FFmpeg implementation). */
 const ffmpegPathPlaceholder = "FFMPEG";
 const inputPathPlaceholder = "INPUT";
 const outputPathPlaceholder = "OUTPUT";
@@ -50,15 +52,13 @@ export const ffmpegExec = async (
         inputFilePath = dataOrPath;
         isInputFileTemporary = false;
     } else {
-        inputFilePath = await makeTempFilePath(".in");
+        inputFilePath = await makeTempFilePath();
         isInputFileTemporary = true;
         await fs.writeFile(inputFilePath, dataOrPath);
     }
 
-    let outputFilePath: string | undefined;
+    const outputFilePath = await makeTempFilePath();
     try {
-        outputFilePath = await makeTempFilePath(".out");
-
         const cmd = substitutePlaceholders(
             command,
             inputFilePath,
@@ -70,8 +70,12 @@ export const ffmpegExec = async (
 
         return fs.readFile(outputFilePath);
     } finally {
-        if (isInputFileTemporary) await deleteTempFile(inputFilePath);
-        if (outputFilePath) await deleteTempFile(outputFilePath);
+        try {
+            if (isInputFileTemporary) await deleteTempFile(inputFilePath);
+            await deleteTempFile(outputFilePath);
+        } catch (e) {
+            log.error("Ignoring error when cleaning up temp files", e);
+        }
     }
 };
 

+ 160 - 0
desktop/src/main/services/image.ts

@@ -0,0 +1,160 @@
+/** @file Image format conversions and thumbnail generation */
+
+import fs from "node:fs/promises";
+import path from "path";
+import { CustomErrorMessage } from "../../types/ipc";
+import log from "../log";
+import { execAsync, isDev } from "../utils-electron";
+import { deleteTempFile, makeTempFilePath } from "../utils-temp";
+
+export const convertToJPEG = async (imageData: Uint8Array) => {
+    const inputFilePath = await makeTempFilePath();
+    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(inputFilePath);
+            deleteTempFile(outputFilePath);
+        } catch (e) {
+            log.error("Ignoring error when cleaning up temp files", e);
+        }
+    }
+};
+
+const convertToJPEGCommand = (
+    inputFilePath: string,
+    outputFilePath: string,
+) => {
+    switch (process.platform) {
+        case "darwin":
+            return [
+                "sips",
+                "-s",
+                "format",
+                "jpeg",
+                inputFilePath,
+                "--out",
+                outputFilePath,
+            ];
+
+        case "linux":
+            return [
+                imageMagickPath(),
+                inputFilePath,
+                "-quality",
+                "100%",
+                outputFilePath,
+            ];
+
+        default: // "win32"
+            throw new Error(CustomErrorMessage.NotAvailable);
+    }
+};
+
+/** Path to the Linux image-magick executable bundled with our app */
+const imageMagickPath = () =>
+    path.join(isDev ? "build" : process.resourcesPath, "image-magick");
+
+export const generateImageThumbnail = async (
+    dataOrPath: Uint8Array | string,
+    maxDimension: number,
+    maxSize: number,
+): Promise<Uint8Array> => {
+    let inputFilePath: string;
+    let isInputFileTemporary: boolean;
+    if (typeof dataOrPath == "string") {
+        inputFilePath = dataOrPath;
+        isInputFileTemporary = false;
+    } else {
+        inputFilePath = await makeTempFilePath();
+        isInputFileTemporary = true;
+    }
+
+    const outputFilePath = await makeTempFilePath(".jpeg");
+
+    // Construct the command first, it may throw NotAvailable on win32.
+    let quality = 70;
+    let command = generateImageThumbnailCommand(
+        inputFilePath,
+        outputFilePath,
+        maxDimension,
+        quality,
+    );
+
+    try {
+        if (dataOrPath instanceof Uint8Array)
+            await fs.writeFile(inputFilePath, dataOrPath);
+
+        let thumbnail: Uint8Array;
+        do {
+            await execAsync(command);
+            thumbnail = new Uint8Array(await fs.readFile(outputFilePath));
+            quality -= 10;
+            command = generateImageThumbnailCommand(
+                inputFilePath,
+                outputFilePath,
+                maxDimension,
+                quality,
+            );
+        } while (thumbnail.length > maxSize && quality > 50);
+        return thumbnail;
+    } finally {
+        try {
+            if (isInputFileTemporary) await deleteTempFile(inputFilePath);
+            deleteTempFile(outputFilePath);
+        } catch (e) {
+            log.error("Ignoring error when cleaning up temp files", e);
+        }
+    }
+};
+
+const generateImageThumbnailCommand = (
+    inputFilePath: string,
+    outputFilePath: string,
+    maxDimension: number,
+    quality: number,
+) => {
+    switch (process.platform) {
+        case "darwin":
+            return [
+                "sips",
+                "-s",
+                "format",
+                "jpeg",
+                "-s",
+                "formatOptions",
+                `${quality}`,
+                "-Z",
+                `${maxDimension}`,
+                inputFilePath,
+                "--out",
+                outputFilePath,
+            ];
+
+        case "linux":
+            return [
+                imageMagickPath(),
+                inputFilePath,
+                "-auto-orient",
+                "-define",
+                `jpeg:size=${2 * maxDimension}x${2 * maxDimension}`,
+                "-thumbnail",
+                `${maxDimension}x${maxDimension}>`,
+                "-unsharp",
+                "0x.5",
+                "-quality",
+                `${quality}`,
+                outputFilePath,
+            ];
+
+        default: // "win32"
+            throw new Error(CustomErrorMessage.NotAvailable);
+    }
+};