TypeScript Tutorial: Building a Simple Interactive PDF Annotation Tool

In the digital age, PDFs are ubiquitous. From contracts and reports to eBooks and presentations, we interact with them daily. But what if you need to highlight, comment, or draw on a PDF document? While dedicated PDF editors exist, building a simple, interactive annotation tool can be a fantastic learning experience, especially when using TypeScript. This tutorial will guide you through creating such a tool, providing a solid understanding of TypeScript concepts and how they apply to real-world scenarios. We’ll focus on the core functionalities: loading a PDF, adding highlights, inserting text comments, and saving the annotated document. This project will not only improve your TypeScript skills but also give you a practical tool to use.

Why Build a PDF Annotation Tool?

Creating a PDF annotation tool offers several advantages:

  • Practical Application: You’ll learn how to apply TypeScript to a tangible problem, making the learning process more engaging.
  • Understanding of Libraries: You’ll gain experience integrating and using external libraries like PDF.js (for PDF rendering) and potentially a canvas-based drawing library.
  • Frontend Development Skills: You’ll practice frontend development skills, including HTML, CSS, and JavaScript (TypeScript).
  • Problem-Solving: You’ll encounter and solve real-world problems related to PDF manipulation and user interaction.

Setting Up Your Development Environment

Before we dive into the code, let’s set up the environment. You’ll need:

  • Node.js and npm (Node Package Manager): These are essential for managing project dependencies and running the development server. Download and install them from the official Node.js website (nodejs.org).
  • A Code Editor: Visual Studio Code (VS Code) is highly recommended due to its excellent TypeScript support, but you can use any editor you prefer (Sublime Text, Atom, etc.).
  • TypeScript Compiler: You’ll need to install TypeScript globally or locally within your project. We’ll use the latter approach.

Let’s create a new project directory and initialize it with npm:

mkdir pdf-annotation-tool
cd pdf-annotation-tool
npm init -y

This creates a package.json file, which will track our project dependencies.

Next, install TypeScript and PDF.js as development dependencies:

npm install typescript pdfjs-dist --save-dev

Now, create a tsconfig.json file in your project root. This file configures the TypeScript compiler. A basic configuration looks like this:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Explanation of the options:

  • target: Specifies the JavaScript version to compile to (ES5, ES6, etc.).
  • module: Specifies the module system to use (commonjs, esnext, etc.).
  • outDir: Specifies the output directory for the compiled JavaScript files.
  • strict: Enables strict type checking.
  • esModuleInterop: Enables interoperability between CommonJS and ES modules.
  • skipLibCheck: Skips type checking of declaration files.
  • forceConsistentCasingInFileNames: Enforces consistent casing in file names.
  • include: Specifies the files to include in the compilation.

Finally, let’s create our project structure. Create a src directory and inside it, create an index.ts file. This is where our main application logic will reside. We’ll also need an index.html file in the project root to serve as the entry point for our application. Create a basic HTML file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PDF Annotation Tool</title>
</head>
<body>
    <canvas id="pdf-canvas"></canvas>
    <script src="./dist/index.js"></script>
</body>
</html>

Loading and Rendering a PDF with PDF.js

Now, let’s start coding in index.ts. First, we’ll import PDF.js and define a function to load and render a PDF. We’ll use a sample PDF for demonstration. You can find many free sample PDFs online.

import * as pdfjsLib from 'pdfjs-dist';

async function renderPDF(pdfUrl: string, canvasId: string) {
  try {
    const loadingTask = pdfjsLib.getDocument(pdfUrl);
    const pdf = await loadingTask.promise;

    const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
    const context = canvas.getContext('2d') as CanvasRenderingContext2D;

    const page = await pdf.getPage(1);
    const viewport = page.getViewport({ scale: 1.0 });

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

    const renderTask = page.render({
      canvasContext: context,
      viewport: viewport,
    });
    await renderTask.promise;

    console.log('Page rendered');
  } catch (error) {
    console.error('Error rendering PDF:', error);
  }
}

// Example usage: Replace with your PDF URL
renderPDF('your-pdf-file.pdf', 'pdf-canvas');

Key points:

  • We import pdfjsLib from pdfjs-dist.
  • getDocument() loads the PDF.
  • We get the first page using getPage(1).
  • getViewport() defines the size and orientation of the page.
  • We set the canvas dimensions to match the page dimensions.
  • render() renders the page onto the canvas.

Important: Replace 'your-pdf-file.pdf' with the actual URL or path to your PDF file. You might need to serve the PDF file from a local server (e.g., using a simple HTTP server like http-server) to avoid CORS issues if loading the PDF from a different domain.

To compile the TypeScript code, run:

tsc

This will create a dist folder containing the compiled JavaScript file (index.js). Open index.html in your browser. You should see the first page of your PDF rendered on the canvas.

Adding Highlight Functionality

Now, let’s add the ability to highlight text on the PDF. We’ll need to listen for mouse events (mousedown, mousemove, mouseup) on the canvas to track the user’s highlighting action. We’ll store the start and end coordinates of the highlight and then draw a rectangle on the canvas.

import * as pdfjsLib from 'pdfjs-dist';

// ... (previous code)

let isDrawing = false;
let startX: number | null = null;
let startY: number | null = null;

canvas.addEventListener('mousedown', (e: MouseEvent) => {
  isDrawing = true;
  startX = e.offsetX;
  startY = e.offsetY;
});

canvas.addEventListener('mousemove', (e: MouseEvent) => {
  if (!isDrawing || startX === null || startY === null) return;

  const currentX = e.offsetX;
  const currentY = e.offsetY;

  // Clear previous highlight (optional, for real-time update)
  renderPDF('your-pdf-file.pdf', 'pdf-canvas'); // Redraw the PDF

  context.fillStyle = 'rgba(255, 255, 0, 0.3)'; // Semi-transparent yellow
  context.fillRect(Math.min(startX, currentX), Math.min(startY, currentY), Math.abs(currentX - startX), Math.abs(currentY - startY));
});

canvas.addEventListener('mouseup', () => {
  isDrawing = false;
  startX = null;
  startY = null;
  // In a real application, you'd save the highlight data here
});

Explanation:

  • isDrawing: A boolean flag to indicate whether the user is currently drawing.
  • startX, startY: Store the starting coordinates of the highlight.
  • mousedown: Sets isDrawing to true and captures the starting coordinates.
  • mousemove: If isDrawing is true, it calculates the current coordinates and draws a semi-transparent yellow rectangle representing the highlight. It clears the previous highlight by redrawing the PDF (a basic implementation; optimizing this is crucial for performance in a real app).
  • mouseup: Sets isDrawing to false and resets the starting coordinates. This is where you’d save the highlight data (coordinates, color, etc.) in a real application.

Important: The current implementation redraws the entire PDF on every mousemove event, which is inefficient. In a production environment, you would only redraw the highlighted area or use a separate layer for annotations to improve performance.

Adding Text Comment Functionality

Next, let’s add the ability to add text comments. This involves:

  • Allowing the user to click on a point on the canvas.
  • Displaying a text input field at that point.
  • Saving the text entered by the user.
import * as pdfjsLib from 'pdfjs-dist';

// ... (previous code)

canvas.addEventListener('click', (e: MouseEvent) => {
  const x = e.offsetX;
  const y = e.offsetY;

  const commentInput = document.createElement('input');
  commentInput.type = 'text';
  commentInput.style.position = 'absolute';
  commentInput.style.left = `${x}px`;
  commentInput.style.top = `${y}px`;
  commentInput.style.zIndex = '10'; // Ensure it's on top of the canvas

  document.body.appendChild(commentInput);

  commentInput.addEventListener('blur', () => {
    const commentText = commentInput.value;
    // Save the commentText and coordinates (x, y)
    console.log('Comment:', commentText, 'at', x, y);
    commentInput.remove(); // Remove the input field
  });
});

Explanation:

  • click event listener: Captures the click coordinates.
  • Creates a text input element: Positions it at the click coordinates and adds some basic styling.
  • Appends the input to the document body: This makes the input visible.
  • blur event listener: This is triggered when the input loses focus (e.g., the user clicks outside the input). It saves the comment text and coordinates and removes the input field.

Saving Annotated PDFs

Saving the annotated PDF is the most complex part. Since we can’t directly modify the original PDF file in the browser, we’ll need a backend server to handle the saving. This tutorial won’t cover the backend implementation, but here’s the general process and the frontend part that interacts with the backend:

  1. Collect Annotation Data: When a highlight is created or a comment is added, store the necessary data (coordinates, text, color, etc.). You’ll need a data structure to hold this information (e.g., an array of objects).
  2. Send Data to the Backend: Use the fetch API to send the annotation data to the backend server in JSON format.
  3. Backend Processing: The backend server will receive the data and use a PDF library (e.g., PDFKit in Node.js, iText in Java, or similar) to:
    • Load the original PDF.
    • Apply the annotations (highlights, text, etc.).
    • Save the modified PDF to a new file.
  4. Frontend Download (Optional): If the backend returns a URL to the saved PDF, you can use JavaScript to trigger a download.

Example of sending annotation data (highlights) to the backend using fetch:

// Inside the mouseup event handler (after highlighting is complete)
canvas.addEventListener('mouseup', () => {
  isDrawing = false;
  startX = null;
  startY = null;

  // Assuming you have an array called highlights that stores the highlight data
  const highlights = []; // Populate this with highlight data (x, y, width, height)

  // Send data to the backend
  fetch('/api/save-annotations', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ highlights: highlights, comments: [] }), // Include comments data if applicable
  })
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      return response.blob(); // Get the response as a blob (binary data)
    })
    .then(blob => {
      // Create a temporary URL for the blob
      const url = window.URL.createObjectURL(blob);

      // Create a download link
      const a = document.createElement('a');
      a.href = url;
      a.download = 'annotated-pdf.pdf'; // Set the filename
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      window.URL.revokeObjectURL(url); // Clean up the URL
    })
    .catch(error => {
      console.error('Error saving annotations:', error);
    });
});

Important considerations for saving:

  • Backend Implementation: The backend is critical for actually saving the PDF. The code above assumes a /api/save-annotations endpoint.
  • Security: Implement appropriate security measures on the backend to prevent unauthorized access and protect against malicious uploads.
  • File Size: Be mindful of the file size of the saved PDF, especially if the annotations are complex.
  • Error Handling: Implement robust error handling on both the frontend and backend to handle network errors, server errors, and file processing errors.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Incorrect PDF Path: Make sure the path to your PDF file is correct. Use relative paths or absolute URLs, and ensure the server is configured to serve the PDF file. Check the browser’s developer console for any network errors.
  • CORS Issues: If the PDF is on a different domain, you might encounter CORS (Cross-Origin Resource Sharing) errors. Serve the PDF from the same domain as your application or configure CORS headers on your server to allow cross-origin requests.
  • Performance Issues: Redrawing the entire PDF on every mousemove or update can lead to performance issues, especially with large PDFs. Optimize the drawing by redrawing only the changed areas or using a separate layer for annotations. Consider using techniques like requestAnimationFrame for smoother updates.
  • Incorrect Canvas Dimensions: Ensure the canvas dimensions match the PDF page dimensions. Use page.getViewport({ scale: 1.0 }) to get the correct dimensions.
  • Typing Errors: TypeScript helps prevent many errors, but typos and incorrect type assignments can still occur. Use your editor’s auto-completion and type checking features to catch these errors early.
  • Missing Dependencies: Make sure you have installed all the required dependencies (pdfjs-dist, etc.) using npm.
  • Event Handling Issues: Pay attention to event handling, especially with mouse events. Ensure you are correctly capturing and using the event coordinates (offsetX, offsetY).

Key Takeaways

In this tutorial, we’ve explored the fundamentals of building a PDF annotation tool with TypeScript. We’ve covered loading and rendering PDFs using PDF.js, adding highlight and text comment functionalities, and the challenges of saving annotated documents. While we didn’t implement the full saving functionality (which requires a backend), you’ve gained a solid understanding of the frontend components and the overall architecture. This project helps in understanding how to interact with external libraries, manage user interactions, and apply your TypeScript knowledge to a real-world problem.

FAQ

  1. Can I use this tool with any PDF? Yes, in theory, but some PDFs might have complex structures or security restrictions that could affect rendering or annotation.
  2. How can I improve performance? Optimize drawing operations by redrawing only the changed areas, using a separate annotation layer, and employing techniques like requestAnimationFrame.
  3. What about mobile support? You’ll need to adapt the touch event handling (touchstart, touchmove, touchend) to make the tool work on touch devices.
  4. How do I handle different PDF page sizes? The code provided automatically adjusts the canvas size to the PDF page size. Ensure you’re handling page navigation (if you want multi-page support).
  5. Where can I find sample PDFs for testing? Many websites offer free sample PDFs for testing purposes. Search for “sample PDF” or “test PDF” online.

Building a PDF annotation tool is a rewarding project that combines frontend development with practical problem-solving. While the implementation has its complexities, the core concepts—loading and rendering PDFs, handling user interactions, and saving annotations—provide a solid foundation for further exploration. By working through this tutorial, you’ve not only learned valuable TypeScript skills but also gained insights into how to build a functional and useful application. It’s a stepping stone to more complex PDF manipulation tasks and a great way to expand your frontend development toolkit. As you continue to refine and enhance the tool, you’ll gain a deeper understanding of the intricacies of web development and the power of TypeScript in creating interactive and engaging user experiences.