diff --git a/src/app/(tools)/size-compressor/compressor-tool.tsx b/src/app/(tools)/size-compressor/compressor-tool.tsx new file mode 100644 index 0000000..76fb460 --- /dev/null +++ b/src/app/(tools)/size-compressor/compressor-tool.tsx @@ -0,0 +1,258 @@ +"use client"; +import { useState, type ChangeEvent, useEffect } from "react"; + +export default function ImageSizeCompressor() { + const [images, setImages] = useState([]); + const [quality, setQuality] = useState(0.8); + const [compressedPreview, setCompressedPreview] = useState( + null, + ); + const [originalSize, setOriginalSize] = useState(""); + const [compressedSize, setCompressedSize] = useState(""); + const [isCompressing, setIsCompressing] = useState(false); + + function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; + } + + async function compressImage(image: File, quality: number): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + const imageUrl = URL.createObjectURL(image); + + img.onload = () => { + const canvas = document.createElement("canvas"); + let width = img.width; + let height = img.height; + const maxDimension = 1920; + + if (width > maxDimension || height > maxDimension) { + if (width > height) { + height = (height / width) * maxDimension; + width = maxDimension; + } else { + width = (width / height) * maxDimension; + height = maxDimension; + } + } + + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext("2d"); + if (!ctx) return reject(new Error("Could not get canvas context")); + + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob( + (blob) => { + if (!blob) return reject(new Error("Could not create blob")); + + const compressedFile = new File([blob], image.name, { + type: "image/jpeg", + lastModified: Date.now(), + }); + + resolve(compressedFile); + }, + "image/jpeg", + quality, + ); + }; + + img.onerror = () => reject(new Error("Could not load image")); + img.src = imageUrl; + }); + } + + function handleImageUpload(e: ChangeEvent) { + if (!e.target.files) return; + + const newFiles = Array.from(e.target.files); + setImages((prev) => [...prev, ...newFiles]); + } + + useEffect(() => { + if (images[0] === undefined) return; + setOriginalSize(formatFileSize(images[0].size)); + + async function generateCompressedPreview() { + if (images[0] === undefined) return; + setIsCompressing(true); + try { + const compressedFile = await compressImage(images[0], quality); + setCompressedPreview(URL.createObjectURL(compressedFile)); + setCompressedSize(formatFileSize(compressedFile.size)); + } finally { + setIsCompressing(false); + } + } + + const debounceTimeout = setTimeout(() => { + void generateCompressedPreview(); + }, 300); + + return () => { + clearTimeout(debounceTimeout); + }; + }, [images, quality]); + + function removeImage(index: number) { + setImages((prev) => prev.filter((_, i) => i !== index)); + if (index === 0) { + setCompressedPreview(null); + setOriginalSize(""); + setCompressedSize(""); + } + } + + async function handleCompress() { + try { + setIsCompressing(true); + const compressedFiles = await Promise.all( + images.map((image) => compressImage(image, quality)), + ); + + compressedFiles.forEach((file, index) => { + const link = document.createElement("a"); + link.href = URL.createObjectURL(file); + link.download = `compressed_${images[index]?.name ?? `image_${index}`}`; + link.click(); + URL.revokeObjectURL(link.href); + }); + } catch (error) { + console.error("Error compressing images:", error); + } finally { + setIsCompressing(false); + } + } + + function onChangeQuality(e: ChangeEvent) { + setQuality(parseFloat(e.target.value)); + } + + function onCancel() { + setImages([]); + setCompressedPreview(null); + setOriginalSize(""); + setCompressedSize(""); + } + + if (images.length === 0) { + return ( +
+

Compress your images to reduce file size.

+
+ +
+
+ ); + } + + return ( +
+
+ {images.map((image, index) => ( +
+ {`Preview + +
+ ))} +
+ +
+ + +
+ + {images.length > 0 && ( +
+
+ Original + Original preview + {originalSize} +
+
+ Compressed Preview +
+ Compressed preview + {isCompressing && ( +
+
Compressing...
+
+ )} +
+ {compressedSize} +
+
+ )} + +
+ + +
+
+ ); +} diff --git a/src/app/(tools)/size-compressor/opengraph-image.tsx b/src/app/(tools)/size-compressor/opengraph-image.tsx new file mode 100644 index 0000000..b4e5491 --- /dev/null +++ b/src/app/(tools)/size-compressor/opengraph-image.tsx @@ -0,0 +1,19 @@ +import { GenerateImage } from "@/app/utils/og-generator"; + +export const runtime = "edge"; + +export const alt = "Image Size Compressor - QuickPic"; +export const contentType = "image/png"; + +export const size = { + width: 1200, + height: 630, +}; + +// Image generation +export default async function Image() { + return await GenerateImage({ + title: "Image Size Compressor", + description: "Compress the size of your images.", + }); +} diff --git a/src/app/(tools)/size-compressor/page.tsx b/src/app/(tools)/size-compressor/page.tsx new file mode 100644 index 0000000..3612654 --- /dev/null +++ b/src/app/(tools)/size-compressor/page.tsx @@ -0,0 +1,10 @@ +import ImageSizeCompressorTool from "./compressor-tool"; + +export const metadata = { + title: "Image Size Compressor - QuickPic", + description: "Compress the size of your images.", +}; + +export default function ImageSizeCompressorPage() { + return ; +} diff --git a/src/app/(tools)/size-compressor/twitter-image.tsx b/src/app/(tools)/size-compressor/twitter-image.tsx new file mode 100644 index 0000000..b4e5491 --- /dev/null +++ b/src/app/(tools)/size-compressor/twitter-image.tsx @@ -0,0 +1,19 @@ +import { GenerateImage } from "@/app/utils/og-generator"; + +export const runtime = "edge"; + +export const alt = "Image Size Compressor - QuickPic"; +export const contentType = "image/png"; + +export const size = { + width: 1200, + height: 630, +}; + +// Image generation +export default async function Image() { + return await GenerateImage({ + title: "Image Size Compressor", + description: "Compress the size of your images.", + }); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 5d093b4..ee5d92b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -26,6 +26,9 @@ export default function Home() { Corner Rounder + + Image size compressor +