Move back
This commit is contained in:
parent
72fa6c653f
commit
64572d5880
2 changed files with 110 additions and 115 deletions
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue