Move back

This commit is contained in:
Manav Rathi 2024-05-09 14:35:01 +05:30
parent 72fa6c653f
commit 64572d5880
No known key found for this signature in database
2 changed files with 110 additions and 115 deletions

View file

@ -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.
*

View file

@ -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.
*
* 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 width The width of the source image.
*
* @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;
return resizedDimensions;
};
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;