Manav Rathi hace 1 año
padre
commit
64572d5880
Se han modificado 2 ficheros con 110 adiciones y 115 borrados
  1. 94 4
      web/apps/photos/src/services/upload/thumbnail.ts
  2. 16 111
      web/packages/media/image.ts

+ 94 - 4
web/apps/photos/src/services/upload/thumbnail.ts

@@ -1,10 +1,9 @@
 import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type";
-import {
-    generateImageThumbnailUsingCanvas,
-    generateVideoThumbnailUsingCanvas,
-} from "@/media/image";
+import { scaledImageDimensions } from "@/media/image";
 import log from "@/next/log";
 import { type Electron } from "@/next/types/ipc";
+import { ensure } from "@/utils/ensure";
+import { withTimeout } from "@/utils/promise";
 import * as ffmpeg from "services/ffmpeg";
 import { heicToJPEG } from "services/heic-convert";
 import { toDataOrPathOrZipEntry, type DesktopUploadItem } from "./types";
@@ -48,6 +47,64 @@ const generateImageThumbnailWeb = async (
     return generateImageThumbnailUsingCanvas(blob);
 };
 
+const generateImageThumbnailUsingCanvas = async (blob: Blob) => {
+    const canvas = document.createElement("canvas");
+    const canvasCtx = ensure(canvas.getContext("2d"));
+
+    const imageURL = URL.createObjectURL(blob);
+    await withTimeout(
+        new Promise((resolve, reject) => {
+            const image = new Image();
+            image.setAttribute("src", imageURL);
+            image.onload = () => {
+                try {
+                    URL.revokeObjectURL(imageURL);
+                    const { width, height } = scaledImageDimensions(
+                        image.width,
+                        image.height,
+                        maxThumbnailDimension,
+                    );
+                    canvas.width = width;
+                    canvas.height = height;
+                    canvasCtx.drawImage(image, 0, 0, width, height);
+                    resolve(undefined);
+                } catch (e: unknown) {
+                    reject(e);
+                }
+            };
+        }),
+        30 * 1000,
+    );
+
+    return await compressedJPEGData(canvas);
+};
+
+const compressedJPEGData = async (canvas: HTMLCanvasElement) => {
+    let blob: Blob | undefined | null;
+    let prevSize = Number.MAX_SAFE_INTEGER;
+    let quality = 0.7;
+
+    do {
+        if (blob) prevSize = blob.size;
+        blob = await new Promise((resolve) => {
+            canvas.toBlob((blob) => resolve(blob), "image/jpeg", quality);
+        });
+        quality -= 0.1;
+    } while (
+        quality >= 0.5 &&
+        blob &&
+        blob.size > maxThumbnailSize &&
+        percentageSizeDiff(blob.size, prevSize) >= 10
+    );
+
+    return new Uint8Array(await ensure(blob).arrayBuffer());
+};
+
+const percentageSizeDiff = (
+    newThumbnailSize: number,
+    oldThumbnailSize: number,
+) => ((oldThumbnailSize - newThumbnailSize) * 100) / oldThumbnailSize;
+
 const generateVideoThumbnailWeb = async (blob: Blob) => {
     try {
         return await ffmpeg.generateVideoThumbnailWeb(blob);
@@ -60,6 +117,39 @@ const generateVideoThumbnailWeb = async (blob: Blob) => {
     }
 };
 
+export const generateVideoThumbnailUsingCanvas = async (blob: Blob) => {
+    const canvas = document.createElement("canvas");
+    const canvasCtx = ensure(canvas.getContext("2d"));
+
+    const videoURL = URL.createObjectURL(blob);
+    await withTimeout(
+        new Promise((resolve, reject) => {
+            const video = document.createElement("video");
+            video.preload = "metadata";
+            video.src = videoURL;
+            video.addEventListener("loadeddata", () => {
+                try {
+                    URL.revokeObjectURL(videoURL);
+                    const { width, height } = scaledImageDimensions(
+                        video.videoWidth,
+                        video.videoHeight,
+                        maxThumbnailDimension,
+                    );
+                    canvas.width = width;
+                    canvas.height = height;
+                    canvasCtx.drawImage(video, 0, 0, width, height);
+                    resolve(undefined);
+                } catch (e) {
+                    reject(e);
+                }
+            });
+        }),
+        30 * 1000,
+    );
+
+    return await compressedJPEGData(canvas);
+};
+
 /**
  * Generate a JPEG thumbnail for the given file or path using native tools.
  *

+ 16 - 111
web/packages/media/image.ts

@@ -1,128 +1,33 @@
-import { ensure } from "@/utils/ensure";
-import { withTimeout } from "@/utils/promise";
-
-/** Maximum width or height of the generated thumbnail */
-const maxThumbnailDimension = 720;
-/** Maximum size (in bytes) of the generated thumbnail */
-const maxThumbnailSize = 100 * 1024; // 100 KB
-
-export const generateImageThumbnailUsingCanvas = async (blob: Blob) => {
-    const canvas = document.createElement("canvas");
-    const canvasCtx = ensure(canvas.getContext("2d"));
-
-    const imageURL = URL.createObjectURL(blob);
-    await withTimeout(
-        new Promise((resolve, reject) => {
-            const image = new Image();
-            image.setAttribute("src", imageURL);
-            image.onload = () => {
-                try {
-                    URL.revokeObjectURL(imageURL);
-                    const { width, height } = scaledThumbnailDimensions(
-                        image.width,
-                        image.height,
-                        maxThumbnailDimension,
-                    );
-                    canvas.width = width;
-                    canvas.height = height;
-                    canvasCtx.drawImage(image, 0, 0, width, height);
-                    resolve(undefined);
-                } catch (e: unknown) {
-                    // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
-                    reject(e);
-                }
-            };
-        }),
-        30 * 1000,
-    );
-
-    return await compressedJPEGData(canvas);
-};
-
-export const generateVideoThumbnailUsingCanvas = async (blob: Blob) => {
-    const canvas = document.createElement("canvas");
-    const canvasCtx = ensure(canvas.getContext("2d"));
-
-    const videoURL = URL.createObjectURL(blob);
-    await withTimeout(
-        new Promise((resolve, reject) => {
-            const video = document.createElement("video");
-            video.preload = "metadata";
-            video.src = videoURL;
-            video.addEventListener("loadeddata", () => {
-                try {
-                    URL.revokeObjectURL(videoURL);
-                    const { width, height } = scaledThumbnailDimensions(
-                        video.videoWidth,
-                        video.videoHeight,
-                        maxThumbnailDimension,
-                    );
-                    canvas.width = width;
-                    canvas.height = height;
-                    canvasCtx.drawImage(video, 0, 0, width, height);
-                    resolve(undefined);
-                } catch (e) {
-                    // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
-                    reject(e);
-                }
-            });
-        }),
-        30 * 1000,
-    );
-
-    return await compressedJPEGData(canvas);
-};
-
 /**
- * Compute the size of the thumbnail to create for an image with the given
- * {@link width} and {@link height}.
+ * Compute optimal dimensions for a resized version of an image while
+ * maintaining aspect ratio of the source image.
+ *
+ * @param width The width of the source image.
  *
- * This function calculates a new size of an image for limiting it to maximum
- * width and height (both specified by {@link maxDimension}), while maintaining
- * aspect ratio.
+ * @param height The height of the source image.
+ *
+ * @param maxDimension The maximum width of height of the resized image.
+ *
+ * This function returns a new size limiting it to maximum width and height
+ * (both specified by {@link maxDimension}), while maintaining aspect ratio of
+ * the source {@link width} and {@link height}.
  *
  * It returns `{0, 0}` for invalid inputs.
  */
-const scaledThumbnailDimensions = (
+export const scaledImageDimensions = (
     width: number,
     height: number,
     maxDimension: number,
 ): { width: number; height: number } => {
-    if (width === 0 || height === 0) return { width: 0, height: 0 };
+    if (width == 0 || height == 0) return { width: 0, height: 0 };
     const widthScaleFactor = maxDimension / width;
     const heightScaleFactor = maxDimension / height;
     const scaleFactor = Math.min(widthScaleFactor, heightScaleFactor);
-    const thumbnailDimensions = {
+    const resizedDimensions = {
         width: Math.round(width * scaleFactor),
         height: Math.round(height * scaleFactor),
     };
-    if (thumbnailDimensions.width === 0 || thumbnailDimensions.height === 0)
+    if (resizedDimensions.width == 0 || resizedDimensions.height == 0)
         return { width: 0, height: 0 };
-    return thumbnailDimensions;
-};
-
-const compressedJPEGData = async (canvas: HTMLCanvasElement) => {
-    let blob: Blob | undefined | null;
-    let prevSize = Number.MAX_SAFE_INTEGER;
-    let quality = 0.7;
-
-    do {
-        if (blob) prevSize = blob.size;
-        blob = await new Promise((resolve) => {
-            canvas.toBlob((blob) => resolve(blob), "image/jpeg", quality);
-        });
-        quality -= 0.1;
-    } while (
-        quality >= 0.5 &&
-        blob &&
-        blob.size > maxThumbnailSize &&
-        percentageSizeDiff(blob.size, prevSize) >= 10
-    );
-
-    return new Uint8Array(await ensure(blob).arrayBuffer());
+    return resizedDimensions;
 };
-
-const percentageSizeDiff = (
-    newThumbnailSize: number,
-    oldThumbnailSize: number,
-) => ((oldThumbnailSize - newThumbnailSize) * 100) / oldThumbnailSize;