diff --git a/src/components/tools/image-converter/index.tsx b/src/components/tools/image-converter/index.tsx index ef1100c..9c2a96b 100644 --- a/src/components/tools/image-converter/index.tsx +++ b/src/components/tools/image-converter/index.tsx @@ -34,6 +34,7 @@ import { } from "@/components/ui/select"; import ToolsWrapper from "@/components/wrappers/ToolsWrapper"; +import { PDFDocument } from "pdf-lib"; export default function ImageConverter() { interface IFile { @@ -46,6 +47,7 @@ export default function ImageConverter() { status: "pending" | "processing" | "success" | "error"; error?: string; // Optional error message for failed conversions } + interface IResult { id: number; originalName: string; @@ -68,6 +70,10 @@ export default function ImageConverter() { const [convertedImages, setConvertedImages] = useState([]); const [previewImage, setPreviewImage] = useState(null); + const [pdfPageSize, setPdfPageSize] = useState("a4"); + const [pdfOrientation, setPdfOrientation] = useState("portrait"); + const [pdfMargin, setPdfMargin] = useState(20); + const fileInputRef = useRef(null); const canvasRef = useRef(null); @@ -92,8 +98,24 @@ export default function ImageConverter() { mimeType: "image/x-icon", hasQuality: false, }, + { + value: "pdf", + label: "PDF", + mimeType: "application/pdf", + hasQuality: false, + }, ]; + type PageSizeKey = "a4" | "a3" | "a5" | "letter" | "legal"; + + const pdfPageSizes: Record = { + a4: { width: 595, height: 842 }, + a3: { width: 842, height: 1191 }, + a5: { width: 420, height: 595 }, + letter: { width: 612, height: 792 }, + legal: { width: 612, height: 1008 }, + }; + const selectedOutputFormat = useMemo(() => { return outputFormats.find((f) => f.value === outputFormat); }, [outputFormat]); @@ -102,6 +124,10 @@ export default function ImageConverter() { return selectedOutputFormat?.hasQuality; }, [selectedOutputFormat]); + const showPdfSettings = useMemo(() => { + return outputFormat === "pdf"; + }, [outputFormat]); + const handleFileSelect = useCallback( (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); @@ -204,6 +230,109 @@ export default function ImageConverter() { return { width: Math.round(width), height: Math.round(height) }; }; + const getOptimalImageData = async ( + canvas: HTMLCanvasElement, + originalFileType: string, + ): Promise<{ bytes: Uint8Array; format: "jpeg" | "png" }> => { + const useJpeg = + originalFileType === "image/jpeg" || + originalFileType === "image/jpg" || + originalFileType === "image/bmp"; + + if (useJpeg) { + const jpegBytes = await new Promise((resolve) => { + canvas.toBlob( + async (blob) => { + if (blob) { + const arrayBuffer = await blob.arrayBuffer(); + resolve(new Uint8Array(arrayBuffer)); + } + }, + "image/jpeg", + 1, + ); + }); + return { bytes: jpegBytes, format: "jpeg" }; + } else { + const pngBytes = await new Promise((resolve) => { + canvas.toBlob(async (blob) => { + if (blob) { + const arrayBuffer = await blob.arrayBuffer(); + resolve(new Uint8Array(arrayBuffer)); + } + }, "image/png"); + }); + return { bytes: pngBytes, format: "png" }; + } + }; + + const createPDF = async ( + images: { + canvas: HTMLCanvasElement; + name: string; + originalType?: string; + }[], + ) => { + try { + const pdfDoc = await PDFDocument.create(); + + const pageSize = pdfPageSizes[pdfPageSize as PageSizeKey]; + const isLandscape = pdfOrientation === "landscape"; + const pageWidth = isLandscape ? pageSize.height : pageSize.width; + const pageHeight = isLandscape ? pageSize.width : pageSize.height; + + for (const imageData of images) { + const page = pdfDoc.addPage([pageWidth, pageHeight]); + + const { bytes: imageBytes, format } = await getOptimalImageData( + imageData.canvas, + imageData.originalType || "image/png", + ); + + const image = + format === "jpeg" + ? await pdfDoc.embedJpg(imageBytes) + : await pdfDoc.embedPng(imageBytes); + + const imageDims = image.scale(1); + + // Calculate dimensions to fit page with margins + const availableWidth = pageWidth - pdfMargin * 2; + const availableHeight = pageHeight - pdfMargin * 2; + + // Scale image to fit page while maintaining aspect ratio + const scaleX = availableWidth / imageDims.width; + const scaleY = availableHeight / imageDims.height; + const scale = Math.min(scaleX, scaleY); + + const scaledWidth = imageDims.width * scale; + const scaledHeight = imageDims.height * scale; + + // Center image on page + const x = (pageWidth - scaledWidth) / 2; + const y = (pageHeight - scaledHeight) / 2; + + // Draw the image + page.drawImage(image, { + x: x, + y: y, + width: scaledWidth, + height: scaledHeight, + }); + } + + // Serialize the PDF + const pdfBytes = await pdfDoc.save(); + return new Blob([pdfBytes], { type: "application/pdf" }); + } catch (error) { + console.error("Error creating PDF:", error); + throw new Error( + "Failed to create PDF: " + + (error instanceof Error ? error.message : "Unknown error"), + ); + } + }; + const convertImage = async (fileData: IFile) => { try { const img = (await loadImage(fileData.preview)) as HTMLImageElement; @@ -230,7 +359,14 @@ export default function ImageConverter() { // Draw image ctx.drawImage(img, 0, 0, width, height); - // Convert to blob + // Handle PDF conversion + if (outputFormat === "pdf") { + return await createPDF([ + { canvas, name: fileData.name, originalType: fileData.type }, + ]); + } + + // Convert to blob for other formats return new Promise((resolve) => { const qualityValue = showQualitySlider ? quality[0] / 100 : undefined; canvas.toBlob(resolve, selectedOutputFormat?.mimeType, qualityValue); @@ -252,46 +388,100 @@ export default function ImageConverter() { const results: IResult[] = []; try { - for (const fileData of selectedFiles) { - try { + // For PDF, combine all images into one PDF + if (outputFormat === "pdf" && selectedFiles.length > 1) { + const images = []; + + for (const fileData of selectedFiles) { setSelectedFiles((prev) => prev.map((f) => f.id === fileData.id ? { ...f, status: "processing" } : f, ), ); - const convertedBlob = (await convertImage(fileData)) as Blob; - const convertedUrl = URL.createObjectURL(convertedBlob); + const img = (await loadImage(fileData.preview)) as HTMLImageElement; + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; - const result: IResult = { - id: fileData.id, - originalName: fileData.name, - convertedName: `${fileData.name.split(".")[0]}.${outputFormat}`, - originalSize: fileData.size, - convertedSize: convertedBlob.size, - convertedUrl, - blob: convertedBlob, - status: "success", - }; + const { width, height } = calculateDimensions( + img.naturalWidth, + img.naturalHeight, + resizeWidth, + resizeHeight, + ); - results.push(result); + canvas.width = width; + canvas.height = height; + ctx.fillStyle = "#FFFFFF"; + ctx.fillRect(0, 0, width, height); + ctx.drawImage(img, 0, 0, width, height); - setSelectedFiles((prev) => - prev.map((f) => - f.id === fileData.id ? { ...f, status: "success" } : f, - ), - ); - } catch (error) { - console.error("Error converting file:", fileData.name, error); - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - setSelectedFiles((prev) => - prev.map((f) => - f.id === fileData.id - ? { ...f, status: "error", error: errorMessage } - : f, - ), - ); + images.push({ canvas, name: fileData.name }); + } + + const pdfBlob = await createPDF(images); + const convertedUrl = URL.createObjectURL(pdfBlob); + + const result: IResult = { + id: Date.now(), + originalName: "Multiple Images", + convertedName: `converted_images.pdf`, + originalSize: selectedFiles.reduce((sum, f) => sum + f.size, 0), + convertedSize: pdfBlob.size, + convertedUrl, + blob: pdfBlob, + status: "success", + }; + + results.push(result); + + // Mark all files as success + setSelectedFiles((prev) => + prev.map((f) => ({ ...f, status: "success" })), + ); + } else { + // Convert each file individually + for (const fileData of selectedFiles) { + try { + setSelectedFiles((prev) => + prev.map((f) => + f.id === fileData.id ? { ...f, status: "processing" } : f, + ), + ); + + const convertedBlob = (await convertImage(fileData)) as Blob; + const convertedUrl = URL.createObjectURL(convertedBlob); + + const result: IResult = { + id: fileData.id, + originalName: fileData.name, + convertedName: `${fileData.name.split(".")[0]}.${outputFormat}`, + originalSize: fileData.size, + convertedSize: convertedBlob.size, + convertedUrl, + blob: convertedBlob, + status: "success", + }; + + results.push(result); + + setSelectedFiles((prev) => + prev.map((f) => + f.id === fileData.id ? { ...f, status: "success" } : f, + ), + ); + } catch (error) { + console.error("Error converting file:", fileData.name, error); + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + setSelectedFiles((prev) => + prev.map((f) => + f.id === fileData.id + ? { ...f, status: "error", error: errorMessage } + : f, + ), + ); + } } } @@ -531,6 +721,70 @@ export default function ImageConverter() { )} + {/* PDF Settings */} + {showPdfSettings && ( +
+

PDF Settings

+
+
+ + +
+ +
+ + +
+ +
+ + setPdfMargin(value[0])} + max={50} + min={0} + step={5} + className="w-full" + /> +
+
+ + {selectedFiles.length > 1 && ( + + + Multiple images will be combined into a single PDF with + each image on a separate page. + + + )} +
+ )} + {/* Resize Options */}
@@ -596,7 +850,7 @@ export default function ImageConverter() { > {processing ? "Converting..." - : `Convert ${selectedFiles.length} Image${selectedFiles.length !== 1 ? "s" : ""}`} + : `Convert ${selectedFiles.length} Image${selectedFiles.length !== 1 ? "s" : ""} to ${outputFormat.toUpperCase()}`} @@ -654,19 +908,21 @@ export default function ImageConverter() {

- + {result.convertedName.endsWith(".pdf") ? null : ( + + )}
+
+

PDF Settings

+
+

• Multiple images will be combined into a single PDF

+

• Each image appears on a separate page

+

• Images are automatically scaled to fit the page

+

• Choose page size and orientation for best results

+
+
+

Resize Options

@@ -832,6 +1101,15 @@ export default function ImageConverter() { Windows icons, favicons

+
+
+ PDF + Document +
+

+ Professional documents +

+
@@ -856,6 +1134,7 @@ export default function ImageConverter() {
  • • High quality: PNG or BMP
  • • Photos: JPEG (95-100% quality)
  • • Logos: PNG with transparency
  • +
  • • Documents: PDF
  • • Archive: Original format