|
|
@@ -0,0 +1,119 @@
|
|
|
+export const MAX_MEDIA_SIZE_MB = 5;
|
|
|
+export const MAX_MEDIA_SIZE_BYTES = MAX_MEDIA_SIZE_MB * 1024 * 1024;
|
|
|
+
|
|
|
+const COMPRESSION_STEPS = [
|
|
|
+ { maxDimension: 2560, quality: 0.85 },
|
|
|
+ { maxDimension: 1920, quality: 0.8 },
|
|
|
+ { maxDimension: 1600, quality: 0.7 },
|
|
|
+ { maxDimension: 1280, quality: 0.6 },
|
|
|
+ { maxDimension: 1024, quality: 0.5 },
|
|
|
+];
|
|
|
+
|
|
|
+const toMb = (bytes) => (bytes / 1024 / 1024).toFixed(1);
|
|
|
+
|
|
|
+export const isImageFile = (file) => Boolean(file?.type?.startsWith("image/"));
|
|
|
+
|
|
|
+const loadImage = (file) =>
|
|
|
+ new Promise((resolve) => {
|
|
|
+ const objectUrl = URL.createObjectURL(file);
|
|
|
+ const image = new Image();
|
|
|
+
|
|
|
+ image.onload = () =>
|
|
|
+ resolve({ image, release: () => URL.revokeObjectURL(objectUrl) });
|
|
|
+ image.onerror = () => {
|
|
|
+ URL.revokeObjectURL(objectUrl);
|
|
|
+ resolve(null);
|
|
|
+ };
|
|
|
+
|
|
|
+ image.src = objectUrl;
|
|
|
+ });
|
|
|
+
|
|
|
+const drawToJpeg = (image, file, { maxDimension, quality }) =>
|
|
|
+ new Promise((resolve) => {
|
|
|
+ const scale = Math.min(1, maxDimension / Math.max(image.width, image.height));
|
|
|
+ const canvas = document.createElement("canvas");
|
|
|
+ canvas.width = Math.max(1, Math.round(image.width * scale));
|
|
|
+ canvas.height = Math.max(1, Math.round(image.height * scale));
|
|
|
+
|
|
|
+ const context = canvas.getContext("2d");
|
|
|
+ if (!context) {
|
|
|
+ resolve(null);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ context.fillStyle = "#ffffff";
|
|
|
+ context.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
|
+
|
|
|
+ canvas.toBlob(
|
|
|
+ (blob) => {
|
|
|
+ if (!blob) {
|
|
|
+ resolve(null);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const name = file.name.replace(/\.[^.]+$/, "") || "image";
|
|
|
+ resolve(
|
|
|
+ new File([blob], `${name}.jpg`, {
|
|
|
+ type: "image/jpeg",
|
|
|
+ lastModified: Date.now(),
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ "image/jpeg",
|
|
|
+ quality,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+export const compressImageToLimit = async (
|
|
|
+ file,
|
|
|
+ maxSizeBytes = MAX_MEDIA_SIZE_BYTES,
|
|
|
+) => {
|
|
|
+ const loaded = await loadImage(file);
|
|
|
+ if (!loaded) return null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ for (const step of COMPRESSION_STEPS) {
|
|
|
+ const compressed = await drawToJpeg(loaded.image, file, step);
|
|
|
+ if (compressed && compressed.size <= maxSizeBytes) return compressed;
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ loaded.release();
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+};
|
|
|
+
|
|
|
+export const prepareMediaFile = async (
|
|
|
+ file,
|
|
|
+ { maxSizeBytes = MAX_MEDIA_SIZE_BYTES } = {},
|
|
|
+) => {
|
|
|
+ if (!(file instanceof File)) {
|
|
|
+ return { file: null, error: null, compressed: false };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (file.size <= maxSizeBytes) {
|
|
|
+ return { file, error: null, compressed: false };
|
|
|
+ }
|
|
|
+
|
|
|
+ const error = {
|
|
|
+ size: toMb(file.size),
|
|
|
+ max: toMb(maxSizeBytes),
|
|
|
+ };
|
|
|
+
|
|
|
+ if (!isImageFile(file)) {
|
|
|
+ return { file: null, error: { ...error, code: "too_large" }, compressed: false };
|
|
|
+ }
|
|
|
+
|
|
|
+ const compressed = await compressImageToLimit(file, maxSizeBytes);
|
|
|
+
|
|
|
+ if (!compressed) {
|
|
|
+ return {
|
|
|
+ file: null,
|
|
|
+ error: { ...error, code: "image_too_large" },
|
|
|
+ compressed: false,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return { file: compressed, error: null, compressed: true };
|
|
|
+};
|