PSPDFKit Viewer Component for Power Apps

When talking to one of our prospective customers, he inquired if we could provide a custom component using PCF that utilized a PDF Viewer in Canvas Apps which could support Annotation, Signatures, etc. With reference to his requirements, I spoke with my new colleagues at PSPDFKit and they had a PDF Viewer available which seemed to be an exact fit for him.

In this post, I’ll walk you through the steps necessary to integrate The PSPDFKit PDF Viewer into your Power Apps Component project.

If you prefer a video tutorial, watch my tutorial at –

Requirements

  • An understanding of TypeScript and Power Apps Component Framework.
  • The latest stable version of Node.js
  • A package manager compatible with npm. This guide contains usage examples for Yarn and the npm client (installed with Node.js by default).
  • Public CDN or Static Website to upload PSPDFKit for Web distribution. Note – This is a Proof of concept demo, so we will be using the Unpkg.com for serving the Web resource files.
  • A Power Apps license is required. More information: Power Apps component framework licensing
  • System administrator privileges are required to enable the Power Apps component framework feature in the environment.
  • Power Apps Component framework feature enabled(link).
  • Visual Studio Code (VSCode) installed
  • Microsoft Power Platform CLI (Use either the Visual Studio Code extension or the MSI installer)

Now that we have our prerequisites in place let’s start building our Custom Code Component –

Creating a new component project

  • To create a new project: Open a command prompt window. Create a new folder for the project using the following command:
mkdir PSPDFKitViewerComponent
  • Open your PSPDFKitViewerComponent folder inside Visual Studio Code.
  • Open a new terminal inside Visual Studio Code using Terminal -> New Terminal.
  • At the terminal prompt, create a new component project by passing basic parameters using the command.
pac pcf init --namespace PSPDFKitViewerComponent --name PSPDFKitViewerComponent --template field
  • The above command also runs the npm install  for you to setup the project build tools.
  • Add the PSPDFKit dependency:
npm install --save pspdfkit
  • Add a new file typings.d.ts in the root of the project and add the code.
declare module "pspdfkit/dist/modern/pspdfkit"

Implementing manifest

  • The control manifest is an XML file that contains the metadata of the code component. It also defines the behavior of the code component.
<property name="document" display-name-key="document" description-key="document" of-type="SingleLine.Text" usage="input" required="true" />
    <property name="viewerwidth" display-name-key="viewerwidth" description-key="viewerwidth" of-type="SingleLine.Text" usage="input" required="true" default-value="600"/>
    <property name="viewerheight" display-name-key="viewerheight" description-key="viewerheight" of-type="SingleLine.Text" usage="input" required="true" default-value="600"/>
    <property name="pdfdocument" display-name-key="pdfdocument" description-key="pdfdocument" of-type="Multiple" usage="bound" />      
  • The overall manifest file should look something like this:
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="PSPDFKitViewerComponent" constructor="PSPDFKitViewerComponent" version="0.0.1" display-name-key="PSPDFKitViewerComponent" description-key="PSPDFKitViewerComponent description" control-type="standard">
<property name="document" display-name-key="document" description-key="document" of-type="SingleLine.Text" usage="input" required="true" />
    <property name="viewerwidth" display-name-key="viewerwidth" description-key="viewerwidth" of-type="SingleLine.Text" usage="input" required="true" default-value="600"/>
    <property name="viewerheight" display-name-key="viewerheight" description-key="viewerheight" of-type="SingleLine.Text" usage="input" required="true" default-value="600"/>
    <property name="pdfdocument" display-name-key="pdfdocument" description-key="pdfdocument" of-type="Multiple" usage="bound" />      
    <resources>
      <code path="index.ts" order="1"/>
    </resources>
  </control>
</manifest>
  • Save the changes to the ControlManifest.Input.xml file.

Integrating PSPDFKit into your project

The next step, after implementing the manifest file, is to implement the component logic using TypeScript. The component logic should be implemented inside the index.ts file as follows.

  • Open the index.ts file in the code editor of your choice.
  • Update the PSPDFKitViewerComponent class with the following code:
import {IInputs, IOutputs} from "./generated/ManifestTypes";

export class PSPDFKitViewerComponent implements ComponentFramework.StandardControl<IInputs, IOutputs> {

	private _notifyOutputChanged: () => void;
	private _instance: any;
	private div:any;
	private pdfBase64: string = "";
	private _context: ComponentFramework.Context<IInputs>;
	private _value1: any;

	/**
	 * Empty constructor.
	 */
	constructor()
	{

	}

	/**
	 * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
	 * Data-set values are not initialized here, use updateView.
	 * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
	 * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
	 * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
	 * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
	 */
	public async init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement)
	{
		this._notifyOutputChanged = notifyOutputChanged;
		this._context = context;	
		this.div = document.createElement("div");	
		this.div.classList.add("pspdfkit-container");
		this.div.style.height = context.parameters.viewerheight.raw + "px";
		this.div.style.width = context.parameters.viewerwidth.raw + "px";
		this.div.style.backgroundColor ="green";
		container.appendChild(this.div);
		await this.PSPDFKit(this._context);

	}
	public async PSPDFKit(context: ComponentFramework.Context<IInputs>)
	{
			const PSPDFKit = await import("pspdfkit/dist/modern/pspdfkit");
			//@ts-ignore
			PSPDFKit.unload(".pspdfkit-container");
			//@ts-ignore
			PSPDFKit.load({
				            disableWebAssemblyStreaming: true,
				            baseUrl : "https://unpkg.com/pspdfkit/dist/",
				            container: ".pspdfkit-container",
				            document: this.convertBase64ToArrayBuffer(context.parameters.document.raw!)
				        }).then((instance: any) => {
					this._instance = instance;
					const saveButton = {
						type: "custom",
						id: "btnSavePdf",
						title: "Save",
						onPress: async (event: any) => 
						{	
							const pdfBuffer = await this._instance.exportPDF();
							this.pdfBase64 = this.convertArrayBufferToBase64(pdfBuffer);
							this._notifyOutputChanged();
						}	
		
					};
	
				instance.setToolbarItems((items: { type: string; id: string; title: string; onPress: (event: any) => void; }[]) => {
				items.push(saveButton);
				
				return items;
					});				
			})
				.catch(console.error);
	}

	private convertArrayBufferToBase64(buffer: ArrayBuffer): string {
		
        let binary = "";
        const bytes = new Uint8Array(buffer);
        const len = bytes.length;
        for (var i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return btoa(binary);
    }


	/**
	 * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
	 * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
	 */
	public async updateView(context: ComponentFramework.Context<IInputs>)
	{
		// Add code to update control view
		
		this.div.style.height = context.parameters.viewerheight.raw + "px";
		this.div.style.width = context.parameters.viewerwidth.raw + "px";
		
		if(context.updatedProperties.indexOf("document")> -1)
   		{
		this._value1 = this.convertBase64ToArrayBuffer(context.parameters.document.raw!);
		this._context = context;
		await this.PSPDFKit(this._context);
		
		}    	
	}

	private convertBase64ToArrayBuffer(base64String: string):ArrayBuffer {
		var base64result = base64String.toString().split(',')[1];
		const binaryString = window.atob(base64result);
        const bytes = new Uint8Array(base64String.length);
        for (var i = 0; i < base64String.length; i++) {
           bytes[i] = binaryString.charCodeAt(i);
		}
		
		return bytes.buffer;
	}

	/** 
	 * It is called by the framework prior to a control receiving new data. 
	 * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
	 */
	public getOutputs(): IOutputs
	{
		let result =  {
			pdfdocument : this.pdfBase64
		};
		return result;
	}
	public destroy(): void
	{
		// Add code to cleanup control if necessary
	}
}

Build and Debugging your code components

  • After you finish adding the manifest, component logic and styling, build the code components using the following command:
npm run build
  • Then run the following command to start the debugging process.
npm start watch

Packaging your code components

Follow these steps to create and import a solution file:

  • Create a new folder Solution inside the PSPDFKitViewerComponent folder and navigate into the folder.
  • Create a new solution project in the PSPDFKitViewerComponent folder using the following command:
pac solution init --publisher-name Samples --publisher-prefix samples
  • Once the new solution project is created, you need to refer to the location where the created component is located. You can add the reference by using the following command:
pac solution add-reference --path ..\
  • To generate a zip file from your solution project, when inside the the cdsproj solution project directory, using the following command:
msbuild /t:restore
  • Again run the following command:
Msbuild

Adding code components to a canvas app

Subscribe to this blog for the latest updates about SharePoint Online, Microsoft Flow, Power Apps, and document conversion and manipulation.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s