Blog Post

Open and Annotate PDFs in a TypeScript App

Illustration: Open and Annotate PDFs in a TypeScript App

Among the growing galaxy of frameworks, transpilers, linters, and other development tools that have populated the web frontend toolset in recent years, TypeScript has consistently stood out, largely due to its growing popularity. TypeScript is backed by Microsoft, and the use of this compiler keeps spreading as new versions with increased robustness and support are released.

Dynamic typing (the ability to change a variable’s data type at will) and loose typing (where a handful of data types account for very different values) have been regarded as both strengths and weaknesses of JavaScript, but it depends on your use case. Meanwhile TypeScript, as its name implies, emphasizes type enforcement: Whenever you use a variable, you must specify its type as exactly as possible. Once this type is set and known, you cannot change it afterward.

Regardless of whether or not you like TypeScript, it’s hardly contestable that enforcing typing helps not only with writing more predictable and testable code, but also with avoiding runtime errors that can be caught by a compiler when using types. On the whole, type enforcement contributes to an enhanced developer experience, and it mitigates the pain of scaling and maintaining large applications.

With TypeScript (and also with Flow, a type-checking tool), a variable keeps the same nature as it’s moved around your application.

TypeScript comes with a lot more features out of the box, like ES6+ syntax, which simplifies the development workflow by removing the need for commonly used plugins and loaders for writing modern JavaScript.

Here at PSPDFKit, we’re fans of type checking, so it was only a matter of time before we addressed TypeScript, which can be used to integrate our SDK without too much hassle.

Setting Things Up

In the following example, we’ll embed our SDK in a TypeScript application. We will follow the process step by step, in addition to providing the entire code — available from a fully functional repository — for you to experiment with and use in your next PDF-powered TypeScript application.

Open your favorite development folder and create a subfolder for our example — something like pspdfkit-web-example-typescript. cd to it, and then run the following:

npm init

You’ll be prompted for some initialization data. Usually the default values will be good enough, so you can even skip the prompts using npm init -y.

TypeScript can compile .ts files with its own tsc compiler without needing any external tool. Nevertheless, we’ll use webpack to help us automate handling assets and building the app, bringing us closer to a real-world scenario where we usually need to deal with files that aren’t JavaScript or TypeScript:

npm install -D copy-webpack-plugin cross-env eslint html-webpack-plugin prettier ts-loader typescript webpack webpack-cli webpack-dev-server ncp

Now it’s time to download (and install) the PSPDFKit for Web library:

npm install --save pspdfkit

We’ll be loading the library from the browser, so we need to copy it somewhere where it will be accessible. Let’s copy these files to the src folder so that TypeScript can find them:

mkdir src
cp ./node_modules/pspdfkit/dist/pspdfkit.js src/pspdfkit.js
cp -R ./node_modules/pspdfkit/dist/pspdfkit-lib src/pspdfkit-lib

Now let’s start creating the actual example:

touch src/index.ts

Notice the extension of the file: .ts. That’s the extension TypeScript will recognize by default, and index.ts is the file where our typing journey will begin.

In order to make our example work, we’ll need some help for the boring, more bureaucratic tasks: compiling, bundling assets, copying files, etc. We’ll let TypeScript and webpack handle all those for us using the settings provided in the example files, and instead we’ll focus on the coding part.

The challenge is not as hard as it might seem: We just need to import the PSPDFKit for Web library into our module and call PSPDFKit.load() with a PDF document URL and a DOM selector to render the PDF in.

Sounds straightforward? Too easy? That’s because it is:

import * as PSPDFKit from "./pspdfkit";

PSPDFKit.load({
  document: "example.pdf",
  container: ".container"
})
  .then(instance => {
    instance.addEventListener("annotations.change", () => {
      console.log("example.pdf loaded!");
    });
  })
  .catch(console.error);

This module will launch PSPDFKit for Web and load and render example.pdf in an HTML file such as this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <title>PSPDFKit for Web - TypeScript example</title>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <div class="container"></div>
    <script src="index.js"></script>
  </body>
</html>

The CSS file referenced by the HTML above can be as thin as this:

.container {
  height: 100vh;
}

In any case, we need to specify some height for the container. Otherwise, it won’t be possible to render the PDF and the interface in it.

Now you might be thinking: “Wait a minute… That’s just plain ES6+ JavaScript! After all that wordiness about type checking — and what a _great idea it was to teach JavaScript to recognize each variable’s nature — where are the types? I want my types!”_

Or maybe you’re just reading on, patiently waiting for this example to grow into something meaningful. Wise you! We’re getting there.

If you run the above code through the TypeScript compiler, it’ll do its job and spit out an index.js file without any complaint. Try it!

npm run build

That’s because TypeScript is a superset of JavaScript — which in fact means that the TypeScript compiler understands JavaScript (but not necessarily the other way around).

But we want the compiler to complain at our type looseness. We want it to point to our functions, arguments, and objects and ask for proper typing. However, by default, the TypeScript compiler will not do nothing of the sort.

That’s because we need to tell it about our preferences. We need to set a flag in tsconfig.json, which is, you guessed it, the configuration file for the TypeScript compiler. This should do it:

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

There are more options to be set in that file, but we won’t deal with them in this article. You can check them out in the provided example tsconfig.json file and in the TypeScript documentation.

Once that flag is set and the compiler is launched again, the whining party begins. This is the first complaint tsc will yell at our naked JavaScript code:

Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i @types/node`.

Some would say it’s still being too kind with us. It even suggests a definitive solution to our data typing problems!

But we do as we’re told and:

npm install -D @types/node

Those are type definitions for Node: We were trying to use process.env, which was of an unknown type. By providing the necessary information in our devDependencies, we can now move on to the next step.

Running the compiler again will show more errors (did you expect otherwise?):

Could not find a declaration file for module './pspdfkit'. './src/pspdfkit.js' implicitly has an 'any' type.
Parameter 'instance' implicitly has an 'any' type.

Once the noImplicitAny flag has been set in tsconfig.json, TypeScript won’t put up with any code, internal or external, that isn’t properly labeled with its type: what it is, what it takes, what it returns. Now suddenly it wants to know everything!

The problem here is that, to date, we don’t have a type definition file for PSPDFKit for Web, which is something we’ll address very soon.

But we don’t want to give up just because of this little problem, so we’ll create our own definition file for the pspdfkit module! Well, I will. Here you go:

export as namespace PSPDFKit;

export function load({
  document,
  container,
}: {
  document: string;
  container: string;
}): Promise<Instance>;

export function unload(container: string): void;

export type Instance = {
  addEventListener: (event: string, callback: Function) => void
};

It’s named pspdfkit.d.ts: Module type definition files must have the extension .d.ts with the module name as a base name in order for TypeScript to recognize them automatically.

Notice that this file only exports the type definitions for a small subset of the PSPDFKit for Web SDK API — just the methods and objects we’ll be using in our example, which is what TypeScript will ask for.

It’s time to run the compiler again and… voilà! The PDF is up and rendering in all its glory.

It wasn’t that hard, was it? I’m sure we can go further with little effort, now that we’ve tamed the TypeScript compiler beast with just a small bunch of type definitions.

So let’s do it! Let’s add a file browser to our little app so we can open just about any file we want from our local file system instead of rendering the same PDF over and over again.

It should be easy. We’ll edit the <body /> content in index.html, like so:

<body>
  <div>
    <input type="file" class="chooseFile" />
  </div>
  <br />
  <div class="container"></div>
</body>

And then, in index.ts, we’ll wrap our loading code in a reusable function to be called when the app is opened and whenever we choose a new file:

function load(document) {
  console.log(`Loading ${document}...`);
  PSPDFKit.load({
    document,
    container: ".container"
  })
    .then(instance => {
      instance.addEventListener("annotations.change", () => {
        console.log(`${document} loaded!`);
      });
    })
    .catch(console.error);
}

let objectUrl = "";
document.addEventListener("change", function(event) {
  if (
    event.target &&
    event.target.className === "chooseFile" &&
    event.target.files instanceof FileList
  ) {
    PSPDFKit.unload(".container");
    if (objectUrl) {
      URL.revokeObjectURL(objectUrl);
    }
    load(URL.createObjectURL(event.target.files[0]));
  }
});

load("example.pdf");

This will result in the following:

Parameter 'document' implicitly has an 'any' type.

Duh! Let’s type our function:

function load(document: string) {

But then also:

Property 'className' does not exist on type 'EventTarget'.
Property 'files' does not exist on type 'EventTarget'.

One would think that TypeScript should be aware that the target property (which it correctly identifies implicitly as of the EventTarget type, without us needing to do it) has the className and files properties, as it can only reference the input[type="file] element we’ve added to index.html.

The truth is that it can’t infer the type from the information it has, and there can be cases where EventTarget doesn’t have className (like when it’s the document itself), and obviously there are cases where it doesn’t include files. How do we make TypeScript aware of this?

One way to solve this problem is to create an interface definition for our event that includes the HTML element type information we expect to receive in our event handler:

interface HTMLInputEvent extends Event {
  target: HTMLInputElement & EventTarget;
}

let objectUrl: string = "";
document.addEventListener("change", function(event: HTMLInputEvent) {
  if (
    event.target &&
    event.target.className === "chooseFile" &&
    event.target.files instanceof FileList
  ) {
    PSPDFKit.unload(".container");
    if (objectUrl) {
      URL.revokeObjectURL(objectUrl);
    }
    objectUrl = URL.createObjectURL(event.target.files[0]);
    load(objectUrl);
  }
});

With this information, TypeScript can correctly identify the event as having all the types needed by our event handler.

Does this work now? Of course! Below is the final result of our typing efforts, which may give you an idea of how easy it is to integrate PSPDFKit for Web in any TypeScript project.

Article Header

Annotate the PDF

We were able to set up our example and get it running, but it would be great if we could make it do something a bit more complicated and cooler. For example, we could add annotations to it using PSPDFKit for Web’s API instead of the UI. This API allows us to add annotations of different types, as well as modify the document, add a digital signature, and much more.

Let’s focus on making this example simple to get you started. Just keep in mind there’s a whole set of features you can also play with.

We will add a custom button to the toolbar by setting the PSPDFKit.Configuration#toolbarItems option. Our button will include an onPress event handler that will take care of calling the necessary API methods to create annotations with random properties:

let instance: PSPDFKit.Instance = null;
function load(document: string) {
  console.log(`Loading ${document}...`);
  PSPDFKit.load({
    document,
    container: ".container",
    baseUrl: "",
    toolbarItems: [
      {
        type: "custom",
        title: "Random annotation",
        className: "randomAnnotation",
        name: "randomAnnotation",
        onPress: () => {
          // Get the dimensions of page `0`.
          const { width, height } = instance.pageInfoForIndex(0);
          // Create a rectangle annotation on page `0` with a random position
          // and random dimensions.
          const left =
            Math.random() *
            (width - PSPDFKit.Options.MIN_SHAPE_ANNOTATION_SIZE);
          const top =
            Math.random() *
            (height - PSPDFKit.Options.MIN_SHAPE_ANNOTATION_SIZE);
          const annotationProperties = {
            boundingBox: new PSPDFKit.Geometry.Rect({
              left,
              top,
              width: Math.random() * (width - left),
              height: Math.random() * (height - top)
            }),
            strokeColor: new PSPDFKit.Color({
              r: Math.random() * 255,
              g: Math.random() * 255,
              b: Math.random() * 255
            }),
            fillColor: new PSPDFKit.Color({
              r: Math.random() * 255,
              g: Math.random() * 255,
              b: Math.random() * 255
            }),
            strokeDashArray: [[1, 1], [3, 3], [6, 6], null][
              Math.floor(Math.random() * 4)
            ],
            strokeWidth: Math.random() * 30
          };
          const annotationClass = [
            PSPDFKit.Annotations.RectangleAnnotation,
            PSPDFKit.Annotations.EllipseAnnotation
          ][Math.floor(Math.random() * 2)];
          instance.create(
            new annotationClass({
              ...annotationProperties,
              pageIndex: 0
            })
          );
        }
      }
    ]
  })
    .then(_instance => {
      instance = _instance;
      _instance.addEventListener("annotations.change", () => {
        console.log(`${document} loaded!`);
      });
    })
    .catch(console.error);
}

Our definition file will also need some updating, as we are now using more components of the API:

export as namespace PSPDFKit;

export interface ToolbarItem {
  type: string;
  title?: string;
  className?: string;
  name?: string;
  onPress?: Function;
  selected?: boolean;
  disabled?: boolean;
}

export interface Configuration {
  document: string;
  container: string;
  baseUrl: string;
  toolbarItems: Array<PSPDFKit.ToolbarItem>;
}

export function load(configuration: Configuration): Promise<Instance>;

export function unload(container: string): void;

export const Options: {
  MIN_SHAPE_ANNOTATION_SIZE: number;
};

export interface PageInfo {
  height: number;
  width: number;
}

export namespace Geometry {
  export class Rect {
    left: number;
    top: number;
    width: number;
    height: number;
    constructor({ left, top, width, height }: any);
  }
}

export class Annotation {
  pageIndex: number;
  boundingBox: Geometry.Rect;
}

export class ShapeAnnotation extends Annotation {
  strokeColor: PSPDFKit.Color;
  fillColor: PSPDFKit.Color;
  strokeDashArray: number[];
  strokeWidth: number;
  constructor({
    strokeColor,
    fillColor,
    strokeDashArray,
    strokeWidth,
    pageIndex,
    boundingBox
  }: any);
}

export class Color {
  r: number;
  g: number;
  b: number;
  constructor({ r, g, b }: any);
}

export namespace Annotations {
  export class RectangleAnnotation extends ShapeAnnotation {}
  export class EllipseAnnotation extends ShapeAnnotation {}
}
export class Instance {
  addEventListener: (event: string, callback: Function) => void;
  pageInfoForIndex: (pageIndex: number) => PageInfo | null | undefined;
  create: (annotation: Annotation) => Promise<Annotation>;
}

We can now rebuild the application and reload, and in the main toolbar, we will only see our custom button. Each time it’s pressed, a new rectangle or ellipse annotation with random property values will be added to the document’s page 0.

Do you want to find out more about PSPDFKit’s features, and play with its powerful API? Then go to the guides and API docs and get started!

Information

If you want thorough instructions on how to build a TypeScript PDF viewer, check out the How to Build a TypeScript PDF Viewer with PDF.js blog post.

Conclusion

I hope you found this example useful. Don’t hesitate to contact support if you need any help with it or with your PSPDFKit for Web installation.

You can also follow the link to the example GitHub repo and play with it or modify it to suit your own implementation.

Share Post
Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
PRODUCTS  |  Web • Releases • Components

PSPDFKit for Web 2024.3 Features New Stamps and Signing UI, Export to Office Formats, and More

PRODUCTS  |  Web • Releases • Components

PSPDFKit for Web 2024.2 Features New Unified UI Icons, Shadow DOM, and Tab Ordering

PRODUCTS  |  Web

Now Available for Public Preview: New Document Authoring Experience Provides Glimpse into the Future of Editing