Create custom plugins
Learn how to create custom plugins to extend the PDF Viewer SDK with interactive functionality.
The PDF Viewer SDK provides a Plugin API that lets you create custom interactive layers and behaviors in the document view. Plugins can add annotations, handle user interactions, render custom overlays, and integrate seamlessly with the viewer.
Steps to create a custom plugin:
- Initialize the Viewer
- Understand the Plugin interface
- Create a custom plugin class
- Register the plugin
- Add UI controls to trigger the plugin
- Activate and deactivate the plugin
- Full example
If you prefer to test a demo or would like to explore the basic functionalities of this SDK, review Getting started with the PDF Viewer SDK and initialize the viewer.
Initialize the Viewer
Before creating plugins, initialize the PDF Viewer SDK and create a viewer instance.
import { PdfToolsViewer } from '@pdftools/pdf-web-viewer';
const container = document.getElementById('viewer-container');
const viewer = new PdfToolsViewer();
await viewer.initialize(
{
licenseKey: 'your-license-key',
},
container
);
Understand the Plugin interface
Plugins implement the UI.Plugin interface from the PDF Web SDK. This interface defines the contract that all plugins must follow.
interface Plugin {
// Unique identifier for the plugin
readonly id: string;
// The document view associated with the plugin
documentView: DocumentView;
// Called when the plugin is activated
activate(): void;
// Called when the plugin is deactivated or deregistered
deactivate(): void;
}
Key concepts:
- Plugin ID: Each plugin must have a unique identifier used for registration and activation
- DocumentView: Provides access to the document, pages, and rendering context
- Active state: Only one plugin can be active at a time
- Lifecycle: Plugins activate when needed and deactivate when done or when another plugin activates
Create a custom plugin class
Let’s create a “Click-to-Place Image” plugin that allows users to select an image file and place it on the PDF document by clicking.
Create a custom layer
First, create a custom layer class that extends UI.Layer. This layer handles rendering on each page.
import { Layer } from '@pdftools/pdf-web-sdk/es6/UI';
const LAYER_ID = 'click-to-place-image-layer';
class ClickToPlaceImageLayer extends Layer<HTMLCanvasElement> {
protected initializeNativeElement(): void {
this._nativeElement = document.createElement('canvas');
this._nativeElement.style.position = 'absolute';
this._nativeElement.style.touchAction = 'none';
this._nativeElement.classList.add(LAYER_ID);
}
protected onSizeChange(): void {
this._nativeElement.width = this._size.width;
this._nativeElement.height = this._size.height;
}
protected render(): void {}
}
Create the plugin class
Now create the plugin class that implements UI.Plugin:
import { Pdf, UI } from '@pdftools/pdf-web-sdk';
import { StampAnnotation } from '@pdftools/pdf-web-sdk/es6/Pdf/Annotations';
import { Size } from '@pdftools/pdf-web-sdk/es6/Pdf/Geometry';
import {
DocumentViewPoint,
PdfPoint,
Point,
} from '@pdftools/pdf-web-sdk/es6/Pdf/Geometry/Point';
export class ClickToPlaceImagePlugin implements UI.Plugin {
id: string = 'click-to-place-image-plugin';
private _active: boolean = false;
private _documentView: UI.DocumentView = null;
private _registeredImage: Pdf.PdfImage = null;
private _aspectRatio: number = 1;
public get active(): boolean {
return this._active;
}
public get documentView(): UI.DocumentView {
return this._documentView;
}
public set documentView(documentView: UI.DocumentView) {
this._documentView = documentView;
// Listen for new pages added to the viewport and create layers for them
this._documentView.addEventListener(
'pageAddedToViewport',
(pageNumber) => {
if (!this._active) return;
const documentViewPage =
this._documentView.slidingWindow.get(pageNumber);
const layer = new ClickToPlaceImageLayer(
LAYER_ID,
documentViewPage
);
documentViewPage.interactiveLayers = [
...documentViewPage.interactiveLayers,
layer,
];
}
);
}
/**
* Activates the plugin, enabling image placement on click.
* @returns {void}
*/
activate(): void {
this._active = true;
// Add event listeners
this.addEventListeners();
this._documentView.slidingWindow.forEach((documentViewPage) => {
const layer = new ClickToPlaceImageLayer(
LAYER_ID,
documentViewPage
);
documentViewPage.interactiveLayers = [
...documentViewPage.interactiveLayers,
layer,
];
});
this._documentView.scrollContainer.style.cursor = 'crosshair';
}
/**
* Deactivates the plugin, disabling image placement.
* @returns {void}
*/
deactivate(): void {
if (!this._active) return;
this._active = false;
// Remove event listeners
this.removeEventListeners();
this._documentView.slidingWindow.forEach((documentViewPage) => {
documentViewPage.interactiveLayers =
documentViewPage.interactiveLayers.filter(
(l) => l.id !== LAYER_ID
);
});
// Reset cursor and clear registered image and aspect ratio
this._documentView.scrollContainer.style.cursor = 'auto';
this._registeredImage = null;
this._aspectRatio = 1;
}
/**
* Sets the image to be placed by the plugin.
* @param {File} file - The image file to be placed.
* @returns {Promise<void>}
*/
public async setImage(file: File): Promise<void> {
const image = new Image();
const imageUrl = URL.createObjectURL(file);
await new Promise<void>((resolve, reject) => {
image.onload = () => resolve();
image.onerror = () =>
reject(new Error('Failed to load image file'));
image.src = imageUrl;
});
this._aspectRatio = image.width / image.height;
URL.revokeObjectURL(imageUrl);
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// Register image in the PDF document
this._registeredImage =
await this._documentView.pdfDocument.registerImage(uint8Array);
}
/**
* Adds event listeners for the plugin.
* @returns {void}
*/
private addEventListeners(): void {
if (!this._documentView) return;
this._documentView.addEventListener(
'pagePointerDown',
this.handlePageClick
);
}
/**
* Removes event listeners for the plugin.
* @returns {void}
*/
private removeEventListeners(): void {
if (!this._documentView) return;
this._documentView.removeEventListener(
'pagePointerDown',
this.handlePageClick
);
}
/**
* Handles page click events to place the image annotation.
* @param {number} pageNumber - The page number where the click occurred.
* @param {PointerEvent} event - The pointer event.
* @returns {void}
*/
private handlePageClick = (pageNumber: number, event: PointerEvent) => {
if (!this._registeredImage) return;
const page =
this._documentView.pdfDocument.pages.getByNumber(pageNumber);
try {
// Calculate the bounding box for the image annotation
const pdfRect = this.calculateImageBoundingBox(
event,
page,
pageNumber
);
// Create the image annotation
const annotation = this.createImageAnnotation(pdfRect, page);
// Add the annotation to the document
this._documentView.pdfDocument.annotations.add(annotation);
} catch (error) {
console.error('Error creating image annotation:', error);
return;
}
};
/**
* Creates an image annotation on the specified page.
* @param {Pdf.Geometry.Rectangle<PdfPoint>} boundingBox - The bounding box for the annotation.
* @param {Pdf.Page} page - The page to add the annotation to.
* @returns {StampAnnotation}
*/
private createImageAnnotation(
boundingBox: Pdf.Geometry.Rectangle<PdfPoint>,
page: Pdf.Page
): StampAnnotation {
if (!this._registeredImage) {
throw new Error('No image registered for annotation creation.');
}
if (!page) {
throw new Error('Page is required to create image annotation.');
}
if (!boundingBox) {
throw new Error(
'Bounding box is required to create image annotation.'
);
}
return new Pdf.Annotations.StampAnnotation({
page,
author: 'User',
boundingBox,
data: {
subtype: Pdf.Annotations.StampAnnotationSubtype.Image,
image: this._registeredImage,
},
});
}
/**
* Calculates the bounding box for the image annotation based on the click event.
* @param {PointerEvent} event - The pointer event.
* @param {Pdf.Page} page - The page where the click occurred.
* @param {number} pageNumber - The page number where the click occurred.
* @returns {Pdf.Geometry.Rectangle<PdfPoint>}
*/
private calculateImageBoundingBox(
event: PointerEvent,
page: Pdf.Page,
pageNumber: number
): Pdf.Geometry.Rectangle<PdfPoint> {
if (!this._documentView) {
throw new Error('Document view is not set.');
}
const documentViewPage =
this._documentView.slidingWindow.get(pageNumber);
if (!page || !documentViewPage) {
throw new Error('Invalid page or document view page.');
}
const canvas = documentViewPage.interactiveLayers.find(
(l) => l.id === LAYER_ID
)?.nativeElement as HTMLCanvasElement;
if (!canvas) {
throw new Error('Canvas layer not found for image placement.');
}
const rect = canvas.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const clickY = event.clientY - rect.top;
const clickPoint = new Point(clickX, clickY) as DocumentViewPoint;
const width = DEFAULT_IMAGE_WIDTH;
const height = width / this._aspectRatio;
const topLeft = new Point(
clickPoint.x - width / 2,
clickPoint.y - height / 2
) as DocumentViewPoint;
const documentViewRect = new Pdf.Geometry.Rectangle(
topLeft,
new Size(width, height)
);
return page.transformDocumentViewPointRectangleToPdfPointRectangle(
documentViewRect,
documentViewPage.size.width,
documentViewPage.size.height,
Pdf.Rotation.None
);
}
}
Key implementation details:
- Custom layer: Creates canvas layers on each page for handling interactions.
- Image registration: Uses
document.registerImage()to register the image with the PDF. - Coordinate conversion: Uses
page.transformDocumentViewPointRectangleToPdfPointRectangle()for accurate placement. - Guard in deactivate: Prevents cleanup from running when the plugin wasn’t actually active (this is important because the plugin manager calls
deactivate()on all plugins before activation).
Register the plugin
Register the plugin with the viewer after a document is opened.
When a document opens, the Viewer internally initializes all registered plugins with the latest documentView instance.
const PLUGIN_ID = 'click-to-place-image-plugin';
// Initialize and register the ClickToPlaceImagePlugin
const imagePlugin = initAndRegisterImagePlugin(
viewer,
CLICK_TO_PLACE_IMAGE_PLUGIN_ID
);
/**
* Registers the ClickToPlaceImagePlugin with the given viewer if not already registered.
* @param {PdfToolsViewer} viewer - The PDF viewer instance to register the plugin with.
* @param {string} pluginId - The unique ID for the plugin.
* @returns {ClickToPlaceImagePlugin} The registered ClickToPlaceImagePlugin instance.
*/
function initAndRegisterImagePlugin(
viewer: PdfToolsViewer,
pluginId: string
): ClickToPlaceImagePlugin {
// Check if plugin is already registered
if (!viewer.plugins.get(pluginId)) {
// Create a new instance of the plugin
const imagePlugin = new ClickToPlaceImagePlugin();
// Register the plugin with the viewer
viewer.plugins.register(imagePlugin);
return imagePlugin;
}
return null;
}
You must register plugins after a document is opened and initialized. Many plugins need to access documentView.pdfDocument during activation, which is only available after calling documentView.initialize(pdfDocument). Note that registering a plugin makes it available for activation, but doesn’t activate it automatically.
Add UI controls to trigger the plugin
Create a button and file input to trigger the plugin. The button can be added to the viewer’s toolbar or elsewhere in your UI.
// Create a custom file input button and append it to the toolbar
const button = createFileInputButton();
appendButtonToToolbar(button);
const fileInput = button.querySelector(
'input[type="file"]'
) as HTMLInputElement;
/**
* Creates a button element with a hidden file input for image selection.
* @returns {HTMLButtonElement} The created button element.
*/
function createFileInputButton(): HTMLButtonElement {
// Create hidden file input and insert button in toolbar
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.hidden = true;
const buttonEl = document.createElement('button');
buttonEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#10D183" class="bi bi-camera" viewBox="0 0 16 16">
<path d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4z"/>
<path d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5m0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"/>
</svg>
`;
buttonEl.style.cssText =
'all: unset; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center;';
buttonEl.appendChild(fileInput);
return buttonEl;
}
/**
* Appends the given button to the viewer's left toolbar.
* @param {HTMLButtonElement} button - The button element to append.
* @returns {void}
*/
function appendButtonToToolbar(button: HTMLButtonElement) {
// Query the viewer's left toolbar and append the button
const pdfToolsViewerEl = document.querySelector('pdftools-viewer');
const pdfToolsLeftToolbarEl = pdfToolsViewerEl.shadowRoot.querySelector(
'pdftools-left-toolbar'
);
pdfToolsLeftToolbarEl.shadowRoot
.querySelector('.toolbar')
.appendChild(button);
}
Add event listeners to support button click and file selection
// Add event listeners
button.addEventListener('click', () => {
fileInput.click(); // Trigger hidden file input click to upload image
});
fileInput.addEventListener('change', async (e) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) {
return;
}
// Activate the plugin and set the selected image
// @NOTE: In this example, we activate the plugin when an image has been selected.
// Additionally, pass the selected image to a plugin so that it can register it for use inside the PDF.
activatePlugin(viewer, CLICK_TO_PLACE_IMAGE_PLUGIN_ID);
await imagePlugin.setImage(file);
// Reset input so same file can be selected again
target.value = '';
});
/**
* Activates the plugin with the given ID in the viewer if not already active.
* @param {PdfToolsViewer} viewer - The PDF viewer instance.
* @param {string} pluginId - The unique ID of the plugin to activate.
* @returns {void}
*/
function activatePlugin(viewer: PdfToolsViewer, pluginId: string): void {
if (!viewer.plugins.isActive(pluginId)) {
viewer.plugins.activate(pluginId);
}
}
Activate and deactivate the plugin
Plugins can be activated and deactivated programmatically using the Plugins API.
Plugin activation example
const PLUGIN_ID = 'click-to-place-image-plugin';
// Activate plugin by ID
viewer.plugins.activate(PLUGIN_ID);
// Check if plugin is active
const isActive = viewer.plugins.isActive(PLUGIN_ID);
// Get currently active plugin ID
const activePluginId = viewer.plugins.getActive();
// Deactivate plugin
viewer.plugins.deactivate(PLUGIN_ID);
When a plugin is activated, any previously active plugin is automatically deactivated. Only one plugin can be active at a time.
Plugin deactivation example
Add keyboard support to deactivate the plugin:
// On 'Escape' key press, deactivate the plugin if active
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
deactivatePlugin(viewer, CLICK_TO_PLACE_IMAGE_PLUGIN_ID);
}
});
/**
* Deactivates the plugin with the given ID in the viewer if it is currently active.
* @param {PdfToolsViewer} viewer - The PDF viewer instance.
* @param {string} pluginId - The unique ID of the plugin to deactivate.
* @returns {void}
*/
function deactivatePlugin(viewer: PdfToolsViewer, pluginId: string): void {
if (viewer.plugins.isActive(pluginId)) {
viewer.plugins.deactivate(pluginId);
}
}
Listen to plugin events
The Plugins API emits events for plugin lifecycle changes:
// Listen for plugin activation
viewer.plugins.addEventListener('activated', (pluginId) => {
console.log('Plugin activated:', pluginId);
});
// Listen for plugin deactivation
viewer.plugins.addEventListener('deactivated', (pluginId) => {
console.log('Plugin deactivated:', pluginId);
});
// Listen for plugin registration
viewer.plugins.addEventListener('registered', (pluginId) => {
console.log('Plugin registered:', pluginId);
});
// Listen for plugin deregistration
viewer.plugins.addEventListener('deregistered', (pluginId) => {
console.log('Plugin deregistered:', pluginId);
});
Full example
This complete example demonstrates how to create, register, and use a custom click-to-place image plugin. The example consists of three files that work together:
- ClickToPlaceImagePlugin.ts: The plugin class implementation with all the core functionality.
- index.ts: The application code that imports the plugin, registers it with the viewer, and sets up UI controls.
- index.html: The HTML page that loads the application.
All three files are necessary to implement this plugin. The plugin class is defined in a separate file for better organization, then imported and used in the main application code.
ClickToPlaceImagePlugin.ts
import { Pdf, UI } from '@pdftools/pdf-web-sdk';
import { StampAnnotation } from '@pdftools/pdf-web-sdk/es6/Pdf/Annotations';
import { Size } from '@pdftools/pdf-web-sdk/es6/Pdf/Geometry';
import {
DocumentViewPoint,
PdfPoint,
Point,
} from '@pdftools/pdf-web-sdk/es6/Pdf/Geometry/Point';
import { Layer } from '@pdftools/pdf-web-sdk/es6/UI';
// Constants
const LAYER_ID = 'click-to-place-image-layer';
const DEFAULT_IMAGE_WIDTH = 100;
/**
* Layer for handling image placement interactions.
*/
class ClickToPlaceImageLayer extends Layer<HTMLCanvasElement> {
protected initializeNativeElement(): void {
this._nativeElement = document.createElement('canvas');
this._nativeElement.style.position = 'absolute';
this._nativeElement.style.touchAction = 'none';
this._nativeElement.classList.add(LAYER_ID);
}
protected onSizeChange(): void {
this._nativeElement.width = this._size.width;
this._nativeElement.height = this._size.height;
}
protected render(): void {}
}
/**
* A plugin that lets users click to place an image annotation on the document.
*/
export class ClickToPlaceImagePlugin implements UI.Plugin {
id: string = 'click-to-place-image-plugin';
private _active: boolean = false;
private _documentView: UI.DocumentView = null;
private _registeredImage: Pdf.PdfImage = null;
private _aspectRatio: number = 1;
public get active(): boolean {
return this._active;
}
public get documentView(): UI.DocumentView {
return this._documentView;
}
public set documentView(documentView: UI.DocumentView) {
this._documentView = documentView;
// Listen for new pages added to the viewport and create layers for them
this._documentView.addEventListener(
'pageAddedToViewport',
(pageNumber) => {
if (!this._active) return;
const documentViewPage =
this._documentView.slidingWindow.get(pageNumber);
const layer = new ClickToPlaceImageLayer(
LAYER_ID,
documentViewPage
);
documentViewPage.interactiveLayers = [
...documentViewPage.interactiveLayers,
layer,
];
}
);
}
/**
* Activates the plugin, enabling image placement on click.
* @returns {void}
*/
activate(): void {
this._active = true;
// Add event listeners
this.addEventListeners();
this._documentView.slidingWindow.forEach((documentViewPage) => {
const layer = new ClickToPlaceImageLayer(
LAYER_ID,
documentViewPage
);
documentViewPage.interactiveLayers = [
...documentViewPage.interactiveLayers,
layer,
];
});
this._documentView.scrollContainer.style.cursor = 'crosshair';
}
/**
* Deactivates the plugin, disabling image placement.
* @returns {void}
*/
deactivate(): void {
if (!this._active) return;
this._active = false;
// Remove event listeners
this.removeEventListeners();
this._documentView.slidingWindow.forEach((documentViewPage) => {
documentViewPage.interactiveLayers =
documentViewPage.interactiveLayers.filter(
(l) => l.id !== LAYER_ID
);
});
// Reset cursor and clear registered image and aspect ratio
this._documentView.scrollContainer.style.cursor = 'auto';
this._registeredImage = null;
this._aspectRatio = 1;
}
/**
* Sets the image to be placed by the plugin.
* @param {File} file - The image file to be placed.
* @returns {Promise<void>}
*/
public async setImage(file: File): Promise<void> {
const image = new Image();
const imageUrl = URL.createObjectURL(file);
await new Promise<void>((resolve, reject) => {
image.onload = () => resolve();
image.onerror = () =>
reject(new Error('Failed to load image file'));
image.src = imageUrl;
});
this._aspectRatio = image.width / image.height;
URL.revokeObjectURL(imageUrl);
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// Register image in the PDF document
this._registeredImage =
await this._documentView.pdfDocument.registerImage(uint8Array);
}
/**
* Adds event listeners for the plugin.
* @returns {void}
*/
private addEventListeners(): void {
if (!this._documentView) return;
this._documentView.addEventListener(
'pagePointerDown',
this.handlePageClick
);
}
/**
* Removes event listeners for the plugin.
* @returns {void}
*/
private removeEventListeners(): void {
if (!this._documentView) return;
this._documentView.removeEventListener(
'pagePointerDown',
this.handlePageClick
);
}
/**
* Handles page click events to place the image annotation.
* @param {number} pageNumber - The page number where the click occurred.
* @param {PointerEvent} event - The pointer event.
* @returns {void}
*/
private handlePageClick = (pageNumber: number, event: PointerEvent) => {
if (!this._registeredImage) return;
const page =
this._documentView.pdfDocument.pages.getByNumber(pageNumber);
try {
// Calculate the bounding box for the image annotation
const pdfRect = this.calculateImageBoundingBox(
event,
page,
pageNumber
);
// Create the image annotation
const annotation = this.createImageAnnotation(pdfRect, page);
// Add the annotation to the document
this._documentView.pdfDocument.annotations.add(annotation);
} catch (error) {
console.error('Error creating image annotation:', error);
return;
}
};
/**
* Creates an image annotation on the specified page.
* @param {Pdf.Geometry.Rectangle<PdfPoint>} boundingBox - The bounding box for the annotation.
* @param {Pdf.Page} page - The page to add the annotation to.
* @returns {StampAnnotation}
*/
private createImageAnnotation(
boundingBox: Pdf.Geometry.Rectangle<PdfPoint>,
page: Pdf.Page
): StampAnnotation {
if (!this._registeredImage) {
throw new Error('No image registered for annotation creation.');
}
if (!page) {
throw new Error('Page is required to create image annotation.');
}
if (!boundingBox) {
throw new Error(
'Bounding box is required to create image annotation.'
);
}
return new Pdf.Annotations.StampAnnotation({
page,
author: 'User',
boundingBox,
data: {
subtype: Pdf.Annotations.StampAnnotationSubtype.Image,
image: this._registeredImage,
},
});
}
/**
* Calculates the bounding box for the image annotation based on the click event.
* @param {PointerEvent} event - The pointer event.
* @param {Pdf.Page} page - The page where the click occurred.
* @param {number} pageNumber - The page number where the click occurred.
* @returns {Pdf.Geometry.Rectangle<PdfPoint>}
*/
private calculateImageBoundingBox(
event: PointerEvent,
page: Pdf.Page,
pageNumber: number
): Pdf.Geometry.Rectangle<PdfPoint> {
if (!this._documentView) {
throw new Error('Document view is not set.');
}
const documentViewPage =
this._documentView.slidingWindow.get(pageNumber);
if (!page || !documentViewPage) {
throw new Error('Invalid page or document view page.');
}
const canvas = documentViewPage.interactiveLayers.find(
(l) => l.id === LAYER_ID
)?.nativeElement as HTMLCanvasElement;
if (!canvas) {
throw new Error('Canvas layer not found for image placement.');
}
const rect = canvas.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const clickY = event.clientY - rect.top;
const clickPoint = new Point(clickX, clickY) as DocumentViewPoint;
const width = DEFAULT_IMAGE_WIDTH;
const height = width / this._aspectRatio;
const topLeft = new Point(
clickPoint.x - width / 2,
clickPoint.y - height / 2
) as DocumentViewPoint;
const documentViewRect = new Pdf.Geometry.Rectangle(
topLeft,
new Size(width, height)
);
return page.transformDocumentViewPointRectangleToPdfPointRectangle(
documentViewRect,
documentViewPage.size.width,
documentViewPage.size.height,
Pdf.Rotation.None
);
}
}
index.ts
import { PdfToolsViewer } from '@pdftools/pdf-web-viewer';
import { ClickToPlaceImagePlugin } from './ClickToPlaceImagePlugin';
async function init() {
const container = document.getElementById('viewer-container');
const viewer = new PdfToolsViewer();
await viewer.initialize(
{
licenseKey: 'your-license-key',
},
container
);
const PLUGIN_ID = 'click-to-place-image-plugin';
// Initialize and register the ClickToPlaceImagePlugin
const imagePlugin = initAndRegisterImagePlugin(viewer, PLUGIN_ID);
// Create a custom file input button and append it to the toolbar
const button = createFileInputButton();
appendButtonToToolbar(button);
const fileInput = button.querySelector('input[type="file"]') as HTMLInputElement;
// Add event listeners
button.addEventListener('click', () => {
fileInput.click(); // Trigger hidden file input click to upload image
});
fileInput.addEventListener('change', async (e) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
// Activate the plugin and set the selected image
activatePlugin(viewer, PLUGIN_ID);
await imagePlugin.setImage(file);
// Reset input so same file can be selected again
target.value = '';
});
// On 'Escape' key press, deactivate the plugin if active
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
deactivatePlugin(viewer, PLUGIN_ID);
}
});
// Listen to plugin activation events
viewer.plugins.addEventListener('activated', (pluginId) => {
// Update button color when plugin is activated
if (pluginId === PLUGIN_ID) {
button.querySelector('svg').style.fill = '#0D8FF2';
}
});
// Listen to plugin deactivation events
viewer.plugins.addEventListener('deactivated', (pluginId) => {
// Update button color when plugin is deactivated
if (pluginId === PLUGIN_ID) {
button.querySelector('svg').style.fill = '#10D183';
}
});
}
/**
* Activates the plugin with the given ID in the viewer if not already active.
*/
function activatePlugin(viewer: PdfToolsViewer, pluginId: string): void {
if (!viewer.plugins.isActive(pluginId)) {
viewer.plugins.activate(pluginId);
}
}
/**
* Deactivates the plugin with the given ID in the viewer if it is currently active.
*/
function deactivatePlugin(viewer: PdfToolsViewer, pluginId: string): void {
if (viewer.plugins.isActive(pluginId)) {
viewer.plugins.deactivate(pluginId);
}
}
/**
* Registers the ClickToPlaceImagePlugin with the given viewer if not already registered.
*/
function initAndRegisterImagePlugin(
viewer: PdfToolsViewer,
pluginId: string
): ClickToPlaceImagePlugin {
if (!viewer.plugins.get(pluginId)) {
const imagePlugin = new ClickToPlaceImagePlugin();
viewer.plugins.register(imagePlugin);
return imagePlugin;
}
return null;
}
/**
* Creates a button element with a hidden file input for image selection.
*/
function createFileInputButton(): HTMLButtonElement {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.hidden = true;
const buttonEl = document.createElement('button');
buttonEl.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#10D183" viewBox="0 0 16 16">
<path d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4z"/>
<path d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5m0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"/>
</svg>
`;
buttonEl.style.cssText =
'all: unset; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center;';
buttonEl.appendChild(fileInput);
return buttonEl;
}
/**
* Appends the given button to the viewer's left toolbar.
*/
function appendButtonToToolbar(button: HTMLButtonElement): void {
const pdfToolsViewerEl = document.querySelector('pdftools-viewer');
const pdfToolsLeftToolbarEl = pdfToolsViewerEl.shadowRoot.querySelector(
'pdftools-left-toolbar'
);
pdfToolsLeftToolbarEl.shadowRoot.querySelector('.toolbar').appendChild(button);
}
init();
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Plugin Example</title>
</head>
<body>
<div id="viewer-container"></div>
<style>
body {
margin: 0;
min-height: 100vh;
display: flex;
}
#viewer-container {
flex: 1;
height: 100vh;
}
</style>
<script type="module" src="./index.ts"></script>
</body>
</html>
Plugin lifecycle
Understanding the plugin lifecycle helps ensure proper resource management:
- Registration: Plugin is registered with
viewer.plugins.register(plugin)and becomes available for use. - Activation: Plugin is activated with
viewer.plugins.activate(pluginId), calling theactivate()method. - Active state: Plugin handles user interactions and renders content while active
- Deactivation: Plugin is deactivated when another plugin activates or via
viewer.plugins.deactivate(pluginId). - Cleanup: Resources are released in the
deactivate()method.
The plugin manager calls deactivate() on all registered plugins before activating a new one. If needed, guard your deactivate() method with if (!this._active) return; to prevent unintentional cleanup of resources.
Best practices
Follow these best practices when creating custom plugins:
- Unique IDs: Use descriptive, unique plugin IDs to avoid conflicts.
- Resource cleanup: Always remove event listeners and layers in
deactivate(). - Guard deactivate: Add
if (!this._active) return;at the start ofdeactivate()to prevent premature cleanup. - Lazy registration: Register plugins after a document is opened to ensure
pdfDocumentis available. - Error handling: Wrap SDK operations in try-catch blocks for robust error handling.
- State management: Track plugin state carefully to avoid inconsistent behavior.
- Coordinate systems: Remember to convert between canvas and PDF coordinate spaces using
transformDocumentViewPointRectangleToPdfPointRectangle(). - Single responsibility: Keep plugins focused on a single, well-defined task.
Further reading
- UI.Plugin Interface - Plugin interface specification.
- UI.Layer Class - Layer rendering documentation.
- Manage events - Event handling patterns.
- Manage annotations - Working with PDF annotations.