TypeScript Tutorial: Building a Simple Interactive PDF Reader

In today’s digital world, PDFs are ubiquitous. From official documents and eBooks to brochures and reports, we encounter them daily. Wouldn’t it be handy to build your own simple, interactive PDF reader directly in your browser using TypeScript? This tutorial will guide you through the process, providing a practical introduction to TypeScript and web development principles, and also give you a functional tool at the end.

Why Build a PDF Reader?

Creating a PDF reader offers several advantages:

  • Learning Opportunity: It’s a fantastic way to solidify your understanding of TypeScript, web APIs, and front-end development.
  • Customization: You can tailor the reader to your specific needs, adding features like annotations or highlighting.
  • Practical Skill: It provides a valuable skill set applicable to various web development projects.

This tutorial is designed for beginners and intermediate developers. We’ll break down the process into manageable steps, explaining each concept in clear, concise language. We will use the PDF.js library, which is the official PDF reader by Mozilla.

Prerequisites

Before we begin, make sure you have the following:

  • A Code Editor: Visual Studio Code (VS Code) is highly recommended, but you can use any editor you prefer.
  • Node.js and npm: Install Node.js from nodejs.org. npm (Node Package Manager) comes bundled with Node.js.
  • Basic HTML, CSS, and JavaScript knowledge: Familiarity with these languages will be helpful.
  • TypeScript installed globally: You can install it using npm: npm install -g typescript

Setting Up Your Project

Let’s start by setting up our project directory. Open your terminal or command prompt and execute the following commands:

mkdir pdf-reader-tutorial
cd pdf-reader-tutorial
npm init -y
npm install pdfjs-dist --save

This will create a new directory, initialize a Node.js project, and install the `pdfjs-dist` package, which contains the PDF.js library. Next, create the following files in your project directory:

  • index.html
  • index.ts
  • style.css

HTML Structure (index.html)

Let’s create the basic HTML structure for our PDF reader in index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple PDF Reader</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <canvas id="pdf-canvas"></canvas>
        <div class="controls">
            <button id="prev-page">Previous</button>
            <button id="next-page">Next</button>
            <span id="page-num">Page: <span id="current-page">1</span> / <span id="total-pages">?</span></span>
        </div>
    </div>
    <script src="index.js"></script>
</body>
</html>

This HTML provides the basic layout:

  • A <canvas> element to render the PDF pages.
  • Buttons for navigation (previous and next page).
  • A display for the current page number and total pages.

CSS Styling (style.css)

Next, let’s add some basic styling to make our PDF reader look presentable. Add the following to style.css:


body {
    font-family: sans-serif;
    margin: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background-color: #f0f0f0;
}

.container {
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    padding: 20px;
    text-align: center;
}

#pdf-canvas {
    border: 1px solid #ccc;
    margin-bottom: 10px;
}

.controls {
    margin-top: 10px;
}

button {
    padding: 8px 16px;
    border: none;
    background-color: #007bff;
    color: white;
    border-radius: 4px;
    cursor: pointer;
    margin: 0 5px;
}

button:hover {
    background-color: #0056b3;
}

This CSS provides basic styling for the layout, canvas, and controls. Feel free to customize this to your liking.

TypeScript Implementation (index.ts)

Now, let’s dive into the core of our PDF reader – the TypeScript code. We’ll use the `pdfjs-dist` library to parse and render the PDF. Open index.ts and add the following code:


import * as pdfjsLib from 'pdfjs-dist';
// @ts-ignore
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
const prevPageButton = document.getElementById('prev-page') as HTMLButtonElement;
const nextPageButton = document.getElementById('next-page') as HTMLButtonElement;
const currentPageSpan = document.getElementById('current-page') as HTMLSpanElement;
const totalPagesSpan = document.getElementById('total-pages') as HTMLSpanElement;

let pdfDoc: pdfjsLib.PDFDocumentProxy | null = null;
let currentPage = 1;
let totalPages = 0;

async function loadPdf(pdfPath: string) {
    try {
        const loadingTask = pdfjsLib.getDocument(pdfPath);
        const pdf = await loadingTask.promise;
        pdfDoc = pdf;
        totalPages = pdf.numPages;
        totalPagesSpan.textContent = String(totalPages);
        renderPage(currentPage);
    } catch (error) {
        console.error('Error loading PDF:', error);
    }
}

async function renderPage(pageNum: number) {
    if (!pdfDoc) return;

    try {
        const page = await pdfDoc.getPage(pageNum);
        const viewport = page.getViewport({ scale: 1.5 }); // Adjust scale for zoom

        canvas.width = viewport.width;
        canvas.height = viewport.height;

        const renderContext = {
            canvasContext: canvas.getContext('2d')!,
            viewport: viewport,
        };
        const renderTask = page.render(renderContext);
        await renderTask.promise;
        currentPageSpan.textContent = String(pageNum);
    } catch (error) {
        console.error('Error rendering page:', error);
    }
}

function setupNavigation() {
    prevPageButton.addEventListener('click', () => {
        if (currentPage > 1) {
            currentPage--;
            renderPage(currentPage);
        }
    });

    nextPageButton.addEventListener('click', () => {
        if (currentPage < totalPages) {
            currentPage++;
            renderPage(currentPage);
        }
    });
}

// Load a sample PDF (replace with your PDF file path)
loadPdf('sample.pdf'); // You will need to replace this with your PDF file
setupNavigation();

Let’s break down the code:

  • Import Statements: We import the `pdfjs-dist` library. The `// @ts-ignore` is necessary because the default build of pdfjs-dist has some issues with TypeScript’s module resolution.
  • DOM Element References: We get references to the HTML elements we created earlier.
  • `pdfDoc`, `currentPage`, `totalPages`: These variables store the PDF document object, the current page number, and the total number of pages.
  • `loadPdf(pdfPath: string)`: This asynchronous function loads the PDF from the specified path. It uses `pdfjsLib.getDocument()` to load the PDF and updates the `totalPages` and renders the first page.
  • `renderPage(pageNum: number)`: This asynchronous function renders a specific page of the PDF. It uses `pdfDoc.getPage()` to get a page object, sets up the canvas dimensions, and renders the page using `page.render()`. The `scale` property in `getViewport` is essential for controlling the zoom level.
  • `setupNavigation()`: This function sets up event listeners for the previous and next page buttons to handle navigation.
  • `loadPdf(‘sample.pdf’)` and `setupNavigation()`: These lines initiate the PDF loading and set up the navigation controls. Make sure you replace ‘sample.pdf’ with the path to your PDF file.

Compiling and Running Your Code

To compile your TypeScript code, run the following command in your terminal:

tsc index.ts

This will generate an index.js file. Before running the code, make sure you have a PDF file named ‘sample.pdf’ in the same directory as your HTML, CSS, and JavaScript files. You can find sample PDFs online, or create a simple one yourself.

Now, open index.html in your web browser. You should see the first page of your PDF displayed in the canvas, and you should be able to navigate through the pages using the buttons.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to fix them:

  • Incorrect PDF Path: Make sure the path to your PDF file (‘sample.pdf’ in our example) is correct relative to your HTML file. Double-check the file name and location.
  • Missing `pdfjs-dist` Installation: Ensure you’ve installed the `pdfjs-dist` package using npm. Run npm install pdfjs-dist --save in your project directory.
  • TypeScript Compilation Errors: If you encounter TypeScript errors, carefully review the error messages. They usually indicate type mismatches or incorrect syntax. Use your code editor’s suggestions to fix the issues. Also, double-check your import statements.
  • Canvas Not Displaying: If the canvas is not displaying anything, check the following:
    • Make sure the canvas has a width and height set correctly, which is handled by the `renderPage` function.
    • Verify that the PDF is loading successfully (check the console for errors).
    • Check your CSS for any styling that might be hiding the canvas.
  • Navigation Not Working: If the navigation buttons don’t work, check the following:
    • Make sure the event listeners are correctly attached to the buttons in `setupNavigation()`.
    • Verify that the `currentPage` and `totalPages` variables are being updated correctly.
    • Check for any errors in the `renderPage()` function.

Adding More Features

Once you have a basic PDF reader, you can add more features to enhance its functionality:

  • Zooming: Implement zoom functionality to allow users to zoom in and out of the PDF pages. You can modify the `scale` value in the `getViewport` options.
  • Page Number Input: Add an input field where users can directly enter the page number to jump to.
  • Annotations: Allow users to add annotations (e.g., highlighting, notes) to the PDF.
  • Search: Implement a search feature to find specific text within the PDF.
  • Loading Indicator: Add a loading indicator to display while the PDF is loading or pages are rendering.
  • Error Handling: Improve error handling to provide more informative messages to the user.

Key Takeaways

  • You’ve learned how to use the `pdfjs-dist` library to load and render PDFs in the browser.
  • You’ve gained practical experience with TypeScript, including working with asynchronous functions and DOM manipulation.
  • You’ve built a functional PDF reader from scratch.
  • You’ve learned about common mistakes and how to troubleshoot them.

FAQ

Here are some frequently asked questions about building a PDF reader with TypeScript:

  1. Can I use this code in a production environment? Yes, but you may need to optimize the code for performance and security. Consider using a framework like React or Angular for more complex applications.
  2. How do I handle different PDF file formats? The `pdfjs-dist` library supports various PDF formats. Ensure the PDF file you are using is valid and not corrupted.
  3. How can I improve the performance of the PDF reader? You can improve performance by:
    • Caching rendered pages.
    • Optimizing the rendering process.
    • Using lazy loading for pages.
  4. What are the security considerations? Be careful when handling user-uploaded PDF files. Validate the files and sanitize any user input to prevent potential security vulnerabilities.
  5. Can I use this with a different PDF library? Yes, you can. However, the code will need to be adapted based on the API of the chosen library.

By following this tutorial, you’ve taken your first steps towards creating a functional PDF reader using TypeScript. The skills and knowledge you’ve gained can be applied to many other web development projects. Remember, practice and experimentation are key. Continue to explore and expand upon this foundation, and don’t hesitate to experiment with the various features and functionalities mentioned. Happy coding!