webp conversion

This commit is contained in:
Michael Freno
2026-01-07 21:38:53 -05:00
parent 8f241ce611
commit 54dd5c9fc7
2 changed files with 93 additions and 19 deletions

View File

@@ -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
); );
}; };

View File

@@ -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
}); });