DEV Community

Shehwar Ahmad
Shehwar Ahmad

Posted on

3 1 1

How to Create an Image to PDF Converter Using Next.js, Shadcn, and Tailwind

Image to PDF Converter Design
In this tutorial, we'll create a modern web-based image-to-PDF converter that works entirely in the browser. Our tool will allow users to drag-and-drop images, preview them, and convert them into a downloadable PDF file. We'll use Next.js for the framework, Shadcn for UI components, and Tailwind CSS for styling.

Prerequisites

  • Basic knowledge of React and Next.js
  • Node.js installed on your system
  • A code editor (VS Code recommended)

Step 1: Set Up Next.js Project

npx create-next-app@latest image-to-pdf
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Dependencies

npm install pdf-lib react-dropzone
Enter fullscreen mode Exit fullscreen mode

Add Shadcn components:

npx shadcn-ui@latest add button
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the ImageToPdf Component

Create a new file components/image-to-pdf.tsx and add the following code:

"use client";
import { useState } from "react";
import { PDFDocument } from "pdf-lib";
import { useDropzone } from "react-dropzone";
import { Button } from "@/components/ui/button";

export default function ImageToPdf() {
  const [files, setFiles] = useState<File[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    accept: { "image/*": [".jpeg", ".jpg", ".png"] },
    onDrop: (acceptedFiles) => {
      setError(null);
      setFiles((prev) => [...prev, ...acceptedFiles]);
    },
  });

  const convertToPdf = async () => {
    setIsLoading(true);
    try {
      const pdfDoc = await PDFDocument.create();

      for (const file of files) {
        const imageBytes = await file.arrayBuffer();
        const image = file.type.includes("jpeg")
          ? await pdfDoc.embedJpg(imageBytes)
          : await pdfDoc.embedPng(imageBytes);

        const page = pdfDoc.addPage([image.width, image.height]);
        const { width, height } = page.getSize();
        const scale = Math.min(width / image.width, height / image.height);

        page.drawImage(image, {
          x: 0,
          y: 0,
          width: image.width * scale,
          height: image.height * scale,
        });
      }

      const pdfBytes = await pdfDoc.save();
      const blob = new Blob([pdfBytes], { type: "application/pdf" });
      const url = URL.createObjectURL(blob);

      const link = document.createElement("a");
      link.href = url;
      link.download = `converted-${Date.now()}.pdf`;
      document.body.appendChild(link);
      link.click();
      URL.revokeObjectURL(url);
      link.remove();
    } catch (error) {
      console.error(error);
      setError("Conversion failed. Please try again with valid images.");
    } finally {
      setIsLoading(false);
    }
  };

  const removeFile = (index: number) => {
    setFiles((prev) => prev.filter((_, i) => i !== index));
  };

  return (
    <div className="bg-card rounded-xl max-w-[800px] mx-auto shadow-lg p-6 sm:p-8 w-full">
      <div className="mb-8 text-center">
        <h1 className="text-3xl font-bold mb-2">Free Image to PDF Converter</h1>
        <p>
          Convert multiple images to a single PDF file instantly in your browser
        </p>
      </div>

      <div
        {...getRootProps()}
        className={`mb-8 p-8 border-2 border-dashed rounded-lg 
        ${isDragActive ? "border-blue-500 bg-blue-50" : "border-gray-200"}`}
      >
        <input {...getInputProps()} />
        <p>
          {isDragActive
            ? "Drop images here"
            : "Drag & drop images, or click to select"}
        </p>
        <p className="text-sm mt-2">Supports: JPEG, PNG</p>
      </div>

      {files.length > 0 && (
        <div className="mb-8">
          <div className="flex items-center justify-between mb-4">
            <h2 className="text-lg font-semibold">
              Selected Images ({files.length})
            </h2>
            <button
              onClick={() => setFiles([])}
              className="text-red-600 hover:text-red-700 text-sm"
            >
              Clear All
            </button>
          </div>

          <div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
            {files.map((file, index) => (
              <div key={index} className="relative group aspect-square">
                <img
                  src={URL.createObjectURL(file)}
                  alt={`Preview ${index + 1}`}
                  className="w-full h-full object-cover rounded-lg border"
                />
                <button
                  onClick={() => removeFile(index)}
                  className="absolute top-1 right-1 bg-red-500 text-white rounded-full 
                    p-1 opacity-0 group-hover:opacity-100 transition-opacity"
                >
                  βœ•
                </button>
              </div>
            ))}
          </div>
        </div>
      )}

      <Button
        variant="default"
        onClick={convertToPdf}
        disabled={!files.length || isLoading}
        className="w-full py-6 font-semibold transition-colors"
      >
        {isLoading ? "Creating PDF..." : "Convert Now"}
      </Button>

      {error && <p className="mt-4 text-red-600 text-center">{error}</p>}

      <div className="mt-8 text-center text-sm text-gray-500">
        <p>100% secure - Files never leave your browser</p>
      </div>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Step 4: Create Home Page

Update your app/page.tsx:

import ImageToPdf from "@/components/image-to-pdf";

export default function Home() {
  return (
    <div className="flex-1 w-full flex flex-col justify-center">
      <ImageToPdf />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

You've built a modern image-to-PDF converter with essential features while maintaining security and privacy. This implementation shows how powerful client-side processing can be with modern web technologies.

Ready to try it out? Check out the live demo here:

Live Preview: Image to PDF Converter πŸš€

Neon image

Set up a Neon project in seconds and connect from a Next.js application ⚑

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Get started β†’

Top comments (4)

Collapse
 
shnew profile image
Sh β€’

I am impressed by the speed of conversion and download,
however there should be a config for quality to keep quality maximum, etc.

Collapse
 
shehwar profile image
Shehwar Ahmad β€’

Thanks for the suggestion. I will try to manage this functionality in the next update!

Collapse
 
robin-ivi profile image
Engineer Robin 🎭 β€’

Hello, Shehwar Ahmad! πŸ‘‹
Congratulations on publishing your first article! This is your space to shineβ€”keep writing, engaging, and making an impact. We're glad you're here! πŸ’‘πŸŽŠ

Collapse
 
samia-saif profile image
Samia Saif β€’

Wow interesting!!! keep up the Good work✌🏻

Neon image

Next.js applications: Set up a Neon project in seconds

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Get started β†’

πŸ‘‹ Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay