Introduction to Annotations

Annotations

PSPDFKit for Web supports most common annotation types:

These are standard annotations (as defined in the PDF Reference) that can be read and written by many apps, including Adobe Acrobat and Apple’s Preview.app.

Working with Annotations in Code

When PSPDFKit for Web is initializing (while the load() promise resolves), it will start to load annotations from the server or the raw PDF. These annotations will be made available via the Instance#getAnnotations API.

The API will return an immutable snapshot of the currently available annotations in the UI. This means that the returned list could include invalid annotations. As an example, consider the following workflow:

  1. The user creates a new text annotation on a page.
  2. Now the user double-clicks the annotation and removes the text, making the annotation invalid since it does not have any text. But since the annotation is not yet deselected, it remains visible.
  3. Next, the user updates the color of the text by using the annotation toolbar. The annotation will still be invalid even though a change occurred.
  4. At the end, the user decides to type more text and deselects the annotation again. The annotation is now valid.

To allow for very fine control, we decided to always expose a current snapshot that completely reflects the UI. Check out the Detecting If Annotations Have Changed guide to find out the difference between a UI update and a save callback.

Because of the fact that PSPDFKit for Web loads annotations on demand, the Instance#getAnnotations API returns a Promise that will resolve to the annotation once it is loaded. For example, if you access annotations on the last page of a large PDF, we first make the HTTP request to load these annotations, since we don’t initialize all data up front. When the request is complete, we can resolve the Promise and return the annotations:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { Point } = PSPDFKit.Geometry;

PSPDFKit.load(configuration).then(async instance => {
  const annotations = await instance.getAnnotations(0);
  annotations.forEach(annotation => console.log(annotation.pageIndex));

  // Filter annotations by type.
  const inkAnnotations = annotations.filter(
    annotation => annotation instanceof PSPDFKit.Annotations.InkAnnotation
  );

  // Filter annotations at a specific point.
  const point = new Point({ x: 20, y: 30 });
  const annotationsAtPointInPage = annotationsOnFirstPage.filter(
    annotation => annotation.boundingBox.isPointInside(point)
  );

  // Get the number of annotations on the first page.
  const totalAnnotations = annotations.size;
});
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PSPDFKit.load(configuration).then(function(instance) {
  var annotations = instance
    .getAnnotations(0)
    .then(function(annotations) {
      annotations.forEach(function(annotation) {
        console.log(annotation.pageIndex);
      });

      // Filter annotations by type.
      var inkAnnotations = annotations.filter(function(annotation) {
        return annotation instanceof PSPDFKit.Annotations.InkAnnotation;
      });

      // Filter annotations at a specific point.
      var point = new PSPDFKit.Geometry.Point({ x: 20, y: 30 });
      var annotationsAtPointInPage = inkAnnotations.filter(function(
        annotation
      ) {
        return annotation.boundingBox.isPointInside(point);
      });

      // Get the number of annotations on the first page.
      var totalAnnotations = annotations.size;
    });
});

Creating Annotations

Since PSPDFKit for Web implements an optimistic UI, you can easily create annotations on the client without going back to the server.

Whenever you create an annotation via our API, the client generates a ULID for the annotation. This allows you and the client to work with the ID that the server will use in the future even though the server doesn’t know about it yet.

Annotations can be created by using the annotation constructors and the Instance#createAnnotation endpoint. This will return a promise that will resolve to the annotation with the generated ID set.

After creating an annotation, it might not be synced to the server automatically. More information on this topic can be found in the Annotation-Saving Mechanism guide:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const { List, Rect } = PSPDFKit.Immutable;
const { DrawingPoint } = PSPDFKit.Geometry;
const { InkAnnotation } = PSPDFKit.Annotations;

PSPDFKit.load(configuration).then(async instance => {
  var annotation = new InkAnnotation({
    pageIndex: 0,
    boundingBox: new Rect({ width: 100, height: 100 }),
    lines: List([
      List([
        new DrawingPoint({ x: 0, y: 0 }),
        new DrawingPoint({ x: 100, y: 100 })
      ])
    ])
  });

  const createdAnnotation = await instance.createAnnotation(annotation);
  console.log(createdAnnotation.id); // => '01BS964AM5Z01J9MKBK64F22BQ'
});
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PSPDFKit.load(configuration).then(function(instance) {
  var annotation = new PSPDFKit.Annotations.InkAnnotation({
    pageIndex: 0,
    boundingBox: new PSPDFKit.Geometry.Rect({ width: 100, height: 100 }),
    lines: PSPDFKit.Immutable.List([
      PSPDFKit.Immutable.List([
        new PSPDFKit.Geometry.DrawingPoint({ x: 0, y: 0 }),
        new PSPDFKit.Geometry.DrawingPoint({ x: 100, y: 100 })
      ])
    ])
  });

  instance.createAnnotation(annotation).then(function(createdAnnotation) {
    console.log(createdAnnotation.id); // => '01BS964AM5Z01J9MKBK64F22BQ'
  });
});

If you want to ensure that the annotation has been persisted by the backend, you can use the Instance.ensureAnnotationSaved endpoint. This endpoint takes an annotation record and returns a promise that resolves to the annotation in its persisted state as soon as it is saved by the server. This means if you update the annotation before it has been saved by the server, Instance.ensureAnnotationSaved will return the updated annotation, even though Instance.ensureAnnotationSaved might have been called before the annotation was updated. Since this method takes exactly the same argument that is returned by the Instance#createAnnotation method, you can easily chain the calls. The ID can be used with the backend API and is unique inside a document:

Copy
1
2
3
4
5
6
7
8
9
PSPDFKit.load(configuration).then(async instance => {
  const createdAnnotation = await instance.createAnnotation(
    newAnnotation
  );
  const savedAnnotation = await instance.ensureAnnotationSaved(
    createdAnnotation
  );
  console.log(savedAnnotation.id); // => '01BS964AM5Z01J9MKBK64F22BQ'
});
Copy
1
2
3
4
5
6
7
8
PSPDFKit.load(configuration).then(function(instance) {
  instance
    .createAnnotation(newAnnotation)
    .then(instance.ensureAnnotationSaved)
    .then(function(savedAnnotation) {
      console.log(savedAnnotation.id); // => '01BS964AM5Z01J9MKBK64F22BQ'
    });
});
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PSPDFKit.load({
  ...configuration,
  autoSaveMode: PSPDFKit.AutoSaveMode.DISABLED
}).then(async instance => {
  console.log(newAnnotation.text); // => 'foo'
  const createdAnnotation = await instance.createAnnotation(
    newAnnotation
  );
  const savedAnnotationPromise = instance.ensureAnnotationSaved(
    createdAnnotation
  );
  const updatedAnnotation = await instance.updateAnnotation(
    createdAnnotation.set("text", "bar")
  );
  instance.saveAnnotations();
  const savedAnnotation = await savedAnnotationPromise;
  console.log(savedAnnotation.text); // => 'bar'
});

Updating and Removing Annotations

In addition to an API for creating annotations, we also provide an API for updating and removing those annotations. Following the optimistic UI approach, the changes will be visible instantly in the UI but are not persisted until the annotations are saved to the server.

To update and remove annotations, you might use Instance#updateAnnotation and Instance#deleteAnnotation. The latter API requires the annotation’s ID only:

Copy
1
2
3
4
5
6
7
8
9
PSPDFKit.load(configuration).then(async instance => {
  const annotations = await instance.getAnnotations(0);
  const annotation = annotations.get(0);
  const updatedAnnotation = annotation.set("opacity", 0.5);
  await instance.updateAnnotation(updatedAnnotation);
  await instance.deleteAnnotation(updatedAnnotation.id);

  console.log("Annotation deleted.");
});
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
PSPDFKit.load(configuration).then(function(instance) {
  return instance.getAnnotations(0).then(function(annotations) {
    var annotation = annotations.get(0);
    var updatedAnnotation = annotation.set("opacity", 0.5);
    return instance.updateAnnotation(updatedAnnotation).then(function() {
      return instance
        .deleteAnnotation(updatedAnnotation.id)
        .then(function() {
          console.log("Annotation deleted.");
        });
    });
  });
});

Right now, it is not possible to modify WidgetAnnotations. These are part of the forms API and are handled separately.

Creating Annotation Attachments

In order to create annotations that refer to an attachment like image annotations do, the attachment has to be created first.

Attachments can be created by using the Instance#createAttachment endpoint. This will return a promise that will resolve to the attachment ID:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PSPDFKit.load(configuration).then(async instance => {
  const imageAttachmentId = await instance.createAttachment(
    attachmentBlob
  );
  const imageAnnotation = new PSPDFKit.Annotations.ImageAnnotation({
    imageAttachmentId: imageAttachmentId,
    contentType: "image/png",
    pageIndex: 0,
    boundingBox: new PSPDFKit.Geometry.Rect({
      width: 100,
      height: 100,
      top: 100,
      left: 100
    })
  });
  const createdImageAnnotation = await instance.createAnnotation(
    imageAnnotation
  );
  console.log(createdAnnotation.id); // => '01BS964AM5Z01J9MKBK64F22BQ'
});
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PSPDFKit.load(configuration).then(function(instance) {
  instance
    .createAttachment(attachmentBlob)
    .then(function(imageAttachmentId) {
      var imageAnnotation = new PSPDFKit.Annotations.ImageAnnotation({
        imageAttachmentId: imageAttachmentId,
        contentType: "image/png",
        pageIndex: 0,
        boundingBox: new PSPDFKit.Geometry.Rect({
          width: 100,
          height: 100,
          top: 100,
          left: 100
        })
      });

      instance
        .createAnnotation(imageAnnotation)
        .then(function(createdAnnotation) {
          console.log(createdAnnotation.id); // => '01BS964AM5Z01J9MKBK64F22BQ'
        });
    });
});

Standalone Deployments

In a PSPDFKit for Web installation that is deployed standalone, annotations will be stored in memory until they are exported. Please see the guide on importing and exporting for more information.

Annotation Callbacks

In order to detect annotation changes, we provide specific callbacks whenever:

  • Anything inside our annotations changes.
  • An annotation gets created.
  • An annotation gets deleted.
  • An annotation gets updated.

Learn more about annotation callbacks in the Detecting If Annotations Have Changed guide.

Annotation Saving

We’ve already discussed the optimistic UI approach to updating annotations that PSPDFKit for Web uses internally. Sometimes, however, it might still be required to persist annotations to the server and wait for a globally unique identifier. The Annotation-Saving Mechanism guide article describes how you can modify the saving behavior and listen for save-specific events.