Building a Document Viewer with react-pdf

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

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

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

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">Previousspan>
            <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">Nextspan>
            <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>
  );
}

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`;

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

  // ...
}

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

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

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
designing-the-user-experience-of-passkeys-on-google-accounts

Designing the user experience of passkeys on Google accounts

Next Post
how-to-create-a-project-initiation-document-(template-included)

How to Create a Project Initiation Document (Template Included)

Related Posts