webp conversion
This commit is contained in:
@@ -58,13 +58,71 @@ export function createIsMobile(
|
|||||||
return () => isMobile(windowWidth());
|
return () => isMobile(windowWidth());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an image to WebP format without resizing
|
||||||
|
* @param file Original image file
|
||||||
|
* @param quality WebP quality (0-1), default 0.85
|
||||||
|
* @returns Converted image as WebP Blob
|
||||||
|
*/
|
||||||
|
export async function convertToWebP(
|
||||||
|
file: File | Blob,
|
||||||
|
quality: number = 0.85
|
||||||
|
): Promise<Blob> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
reject(new Error("Failed to get canvas context"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
} else {
|
||||||
|
reject(new Error("Failed to create blob from canvas"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"image/webp",
|
||||||
|
quality
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
reject(new Error("Failed to load image"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
if (e.target?.result) {
|
||||||
|
img.src = e.target.result as string;
|
||||||
|
} else {
|
||||||
|
reject(new Error("Failed to read file"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(new Error("FileReader error"));
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resizes an image file to a maximum width/height while maintaining aspect ratio
|
* Resizes an image file to a maximum width/height while maintaining aspect ratio
|
||||||
* @param file Original image file
|
* @param file Original image file
|
||||||
* @param maxWidth Maximum width in pixels
|
* @param maxWidth Maximum width in pixels
|
||||||
* @param maxHeight Maximum height in pixels
|
* @param maxHeight Maximum height in pixels
|
||||||
* @param quality JPEG quality (0-1), default 0.85
|
* @param quality WebP quality (0-1), default 0.85
|
||||||
* @returns Resized image as Blob
|
* @returns Resized image as WebP Blob
|
||||||
*/
|
*/
|
||||||
export async function resizeImage(
|
export async function resizeImage(
|
||||||
file: File | Blob,
|
file: File | Blob,
|
||||||
@@ -110,7 +168,7 @@ export async function resizeImage(
|
|||||||
reject(new Error("Failed to create blob from canvas"));
|
reject(new Error("Failed to create blob from canvas"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"image/jpeg",
|
"image/webp",
|
||||||
quality
|
quality
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* S3 Upload Utility for SolidStart
|
* S3 Upload Utility for SolidStart
|
||||||
* Uploads files to S3 using pre-signed URLs from tRPC
|
* Uploads files to S3 using pre-signed URLs from tRPC
|
||||||
|
* Automatically converts images to WebP format for better compression
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
import { resizeImage } from "~/lib/resize-utils";
|
import { resizeImage, convertToWebP } from "~/lib/resize-utils";
|
||||||
|
|
||||||
export default async function AddImageToS3(
|
export default async function AddImageToS3(
|
||||||
file: Blob | File,
|
file: Blob | File,
|
||||||
@@ -14,46 +15,61 @@ export default async function AddImageToS3(
|
|||||||
try {
|
try {
|
||||||
const filename = (file as File).name;
|
const filename = (file as File).name;
|
||||||
|
|
||||||
const { uploadURL, key } = await api.misc.getPreSignedURL.mutate({
|
|
||||||
type,
|
|
||||||
title,
|
|
||||||
filename
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("url: " + uploadURL, "key: " + key);
|
|
||||||
|
|
||||||
const ext = /^.+\.([^.]+)$/.exec(filename);
|
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||||
let contentType = "application/octet-stream";
|
let contentType = "application/octet-stream";
|
||||||
|
let isImage = false;
|
||||||
|
let isVideo = false;
|
||||||
|
|
||||||
if (ext) {
|
if (ext) {
|
||||||
const extension = ext[1].toLowerCase();
|
const extension = ext[1].toLowerCase();
|
||||||
if (["mp4", "webm", "mov", "quicktime"].includes(extension)) {
|
if (["mp4", "webm", "mov", "quicktime"].includes(extension)) {
|
||||||
contentType =
|
contentType =
|
||||||
extension === "mov" ? "video/quicktime" : `video/${extension}`;
|
extension === "mov" ? "video/quicktime" : `video/${extension}`;
|
||||||
} else {
|
isVideo = true;
|
||||||
contentType = `image/${extension}`;
|
} else if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
||||||
|
isImage = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fileToUpload: Blob | File = file;
|
||||||
|
let finalFilename = filename;
|
||||||
|
|
||||||
|
// Convert images to WebP for better compression
|
||||||
|
if (isImage) {
|
||||||
|
contentType = "image/webp";
|
||||||
|
fileToUpload = await convertToWebP(file, 0.85);
|
||||||
|
finalFilename = filename.replace(/\.[^.]+$/, ".webp");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uploadURL, key } = await api.misc.getPreSignedURL.mutate({
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
filename: finalFilename
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("url: " + uploadURL, "key: " + key);
|
||||||
|
|
||||||
const uploadResponse = await fetch(uploadURL, {
|
const uploadResponse = await fetch(uploadURL, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": contentType
|
"Content-Type": contentType
|
||||||
},
|
},
|
||||||
body: file as File
|
body: fileToUpload
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
if (!uploadResponse.ok) {
|
||||||
throw new Error("Failed to upload file to S3");
|
throw new Error("Failed to upload file to S3");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only create thumbnails for images
|
// Create thumbnails for images (blog posts only)
|
||||||
const isImage = contentType.startsWith("image/");
|
|
||||||
if (type === "blog" && isImage) {
|
if (type === "blog" && isImage) {
|
||||||
try {
|
try {
|
||||||
const thumbnail = await resizeImage(file, 200, 200, 0.8);
|
const thumbnail = await resizeImage(file, 200, 200, 0.8);
|
||||||
|
|
||||||
const thumbnailFilename = filename.replace(/(\.[^.]+)$/, "-small$1");
|
const thumbnailFilename = finalFilename.replace(
|
||||||
|
/(\.[^.]+)$/,
|
||||||
|
"-small$1"
|
||||||
|
);
|
||||||
|
|
||||||
const { uploadURL: thumbnailUploadURL } =
|
const { uploadURL: thumbnailUploadURL } =
|
||||||
await api.misc.getPreSignedURL.mutate({
|
await api.misc.getPreSignedURL.mutate({
|
||||||
@@ -65,7 +81,7 @@ export default async function AddImageToS3(
|
|||||||
const thumbnailUploadResponse = await fetch(thumbnailUploadURL, {
|
const thumbnailUploadResponse = await fetch(thumbnailUploadURL, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "image/jpeg"
|
"Content-Type": "image/webp"
|
||||||
},
|
},
|
||||||
body: thumbnail
|
body: thumbnail
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user