DEV Community

Cover image for Building a Document Viewer with react-pdf
Marc Seitz
Marc Seitz

Posted on

Building a Document Viewer with react-pdf

What you will find in this article?

PDF viewers have become essential components in many web applications. For instance, they are widely used in educational platforms, online libraries, and any other applications that involve document viewing. In this post, we will explore how we can create a beautiful page-by-page PDF viewer using react-pdf.

Document GIF

Papermark - the open-source DocSend alternative.

Before we kick off, let me introduce you to Papermark. It's an open-source project for securely sharing documents that features a beautiful full-screen viewer for PDF documents.

I would be absolutely thrilled if you could give us a star! Don't forget to share your thoughts in the comments section ❤️

https://github.com/mfts/papermark

Papermark Analytics

Setup the project

Let's set up our project environment. We will be setting up a Next.js app and installing the required libraries.

Set up tea

It's a good idea to have a package manager handy, like tea. It'll handle your development environment and simplify your (programming) life!

sh <(curl https://tea.xyz)

# --- OR ---
# using brew
brew install teaxyz/pkgs/tea-cli
Enter fullscreen mode Exit fullscreen mode

tea frees you to focus on your code, as it takes care of installing node, npm, vercel and any other packages you may need. The best part is, tea installs all packages in a dedicated directory (default: ~/.tea), keeping your system files neat and tidy.

Set up Next.js with TypeScript and Tailwindcss

We will use create-next-app to generate a new Next.js project. We will also be using TypeScript and Tailwind CSS, so make sure to select those options when prompted.

npx create-next-app

# ---
# you'll be asked the following prompts
What is your project named?  my-app
Would you like to add TypeScript with this project?  Y/N
# select `Y` for typescript
Would you like to use ESLint with this project?  Y/N
# select `Y` for ESLint
Would you like to use Tailwind CSS with this project? Y/N
# select `Y` for Tailwind CSS
Would you like to use the `src/ directory` with this project? Y/N
# select `N` for `src/` directory
What import alias would you like configured? `@/*`
# enter `@/*` for import alias
Enter fullscreen mode Exit fullscreen mode

Install react-pdf

There are actually two npm packages called "react-pdf": one is for displaying PDFs and one is for generating PDFs. Today we are focusing on the one to generate PDFs: https://github.com/wojtekmaj/react-pdf.

# Navigate to your Next.js repo
cd my-app

# Install react-pdf
npm install react-pdf
Enter fullscreen mode Exit fullscreen mode

Building the application

Now that we have our setup in place, we are ready to start building our application.

Set up the PDF Viewer

The ability to programmatically configure pipes and datasources for Tinybird offers a significant advantage. This flexibility enables us to treat our data infrastructure as code, meaning that the entire configuration can be committed into a version control system. For an open-source project like Papermark, this capability is highly beneficial. It fosters transparency and collaboration, as contributors can readily understand the data structure without any ambiguity.

We set up Tinybird pipes and datasource as follows:

// components/pdfviewer.tsx
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
import { useEffect, useRef, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";

pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;

export default function PDFViewer(props: any) {
  const [numPages, setNumPages] = useState<number>(0);
  const [pageNumber, setPageNumber] = useState<number>(1); // start on first page
  const [loading, setLoading] = useState(true);
  const [pageWidth, setPageWidth] = useState(0);

  function onDocumentLoadSuccess({
    numPages: nextNumPages,
  }: {
    numPages: number;
  }) {
    setNumPages(nextNumPages);
  }

  function onPageLoadSuccess() {
    setPageWidth(window.innerWidth);
    setLoading(false);
  }

  const options = {
    cMapUrl: "cmaps/",
    cMapPacked: true,
    standardFontDataUrl: "standard_fonts/",
  };

  // Go to next page
  function goToNextPage() {
    setPageNumber((prevPageNumber) => prevPageNumber + 1);
  }

  function goToPreviousPage() {
    setPageNumber((prevPageNumber) => prevPageNumber - 1);
  }


  return (
    <>
      <Nav pageNumber={pageNumber} numPages={numPages} />
      <div
        hidden={loading}
        style={{ height: "calc(100vh - 64px)" }}
        className="flex items-center"
      >
        <div
          className={`flex items-center justify-between w-full absolute z-10 px-2`}
        >
          <button
            onClick={goToPreviousPage}
            disabled={pageNumber <= 1}
            className="relative h-[calc(100vh - 64px)] px-2 py-24 text-gray-400 hover:text-gray-50 focus:z-20"
          >
            <span className="sr-only">Previous</span>
            <ChevronLeftIcon className="h-10 w-10" aria-hidden="true" />
          </button>
          <button
            onClick={goToNextPage}
            disabled={pageNumber >= numPages!}
            className="relative h-[calc(100vh - 64px)] px-2 py-24 text-gray-400 hover:text-gray-50 focus:z-20"
          >
            <span className="sr-only">Next</span>
            <ChevronRightIcon className="h-10 w-10" aria-hidden="true" />
          </button>
        </div>

        <div className="h-full flex justify-center mx-auto">
          <Document
            file={props.file}
            onLoadSuccess={onDocumentLoadSuccess}
            options={options}
            renderMode="canvas"
            className=""
          >
            <Page
              className=""
              key={pageNumber}
              pageNumber={pageNumber}
              renderAnnotationLayer={false}
              renderTextLayer={false}
              onLoadSuccess={onPageLoadSuccess}
              onRenderError={() => setLoading(false)}
              width={Math.max(pageWidth * 0.8, 390)}
            />
          </Document>
        </div>
      </div>
    </>
  );
}


function Nav({pageNumber, numPages}: {pageNumber: number, numPages: number}) {
  return (
    <nav className="bg-black">
      <div className="mx-auto px-2 sm:px-6 lg:px-8">
        <div className="relative flex h-16 items-center justify-between">
          <div className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
            <div className="flex flex-shrink-0 items-center">
              <p className="text-2xl font-bold tracking-tighter text-white">
                Papermark
              </p>
            </div>
          </div>
          <div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
            <div className="bg-gray-900 text-white rounded-md px-3 py-2 text-sm font-medium">
              <span>{pageNumber}</span>
              <span className="text-gray-400"> / {numPages}</span>
            </div>
          </div>
        </div>
      </div>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's break it down what's happening here:

From react-pdf, we are using Document and Page component. In addition, we are loading the pre-packaged version of pdfjs to initialize a worker in the browser.

import { Document, Page, pdfjs } from "react-pdf";

pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
Enter fullscreen mode Exit fullscreen mode

Next, we are writing two function that 1) count the total number of pages (once) and 2) adjust the page width as needed (per page)

// ...
export default function PDFViewer(props: any) {
  // useState variables

  function onDocumentLoadSuccess({
    numPages: nextNumPages,
  }: {
    numPages: number;
  }) {
    setNumPages(nextNumPages);
  }

  function onPageLoadSuccess() {
    setPageWidth(window.innerWidth);
    setLoading(false);
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Next, we use the Document and Page components. This is the core of the PDF viewer. Important to note that Document takes a file prop, that can be a URL, base64 content, Uint8Array, and more. In my case, I'm loading a URL to a file.

// ...
export default function PDFViewer(props: any) {
  // ...
  return (
    <div className="h-full flex justify-center mx-auto">
      <Document
        file={props.file}
        onLoadSuccess={onDocumentLoadSuccess}
        options={options}
        renderMode="canvas"
        className=""
      >
        <Page
          className=""
          key={pageNumber}
          pageNumber={pageNumber}
          renderAnnotationLayer={false}
          renderTextLayer={false}
          onLoadSuccess={onPageLoadSuccess}
          onRenderError={() => setLoading(false)}
          width={Math.max(pageWidth * 0.8, 390)}
        />
      </Document>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

We also added a navigation bar with showing the current page number and buttons for navigating to the next / previous page of the document.

You can use the PDF Viewer component anywhere in your application to visualize PDFs beautifully.

Tada 🎉 The PDF Viewer component is ready!

PDF Viewer in application

Conclusion

That's it! We've built a beautiful PDF Viewer component for displaying PDF documents using react-pdf, and Next.js.

Thank you for reading. I'm Marc, an open-source advocate. I am building papermark.io - the open-source alternative to DocSend with millisecond-accurate page analytics.

Help me out!

If you found this article helpful and got to understand react-pdf better, I would be eternally grateful if you could give us a star! And don't forget to share your thoughts in the comments ❤️

https://github.com/mfts/papermark

Cat GIF

Top comments (7)

Collapse
 
_bkeren profile image
''

does this have right to left support? What about Arabic letters?

Collapse
 
mfts profile image
Marc Seitz

absolutely! it renders the pdf with a html canvas element and the pdf document can be in any language or text direction :)

Collapse
 
shnai0 profile image
Iuliia Shnai

And this one adjusts to different screen sizes?

Collapse
 
mfts profile image
Marc Seitz

Yes that's exactly why I'm fetching the page.width on pageload :)

Collapse
 
shnai0 profile image
Iuliia Shnai

Got it

Collapse
 
nevodavid profile image
Nevo David

Great article, and amazing cover!

Collapse
 
mfts profile image
Marc Seitz

Thanks Nevo 🤩