Blog Post

Leveraging WebAssembly in JavaScript for High-Performance Document Processing

Jonathan D. Rhyne
Illustration: Leveraging WebAssembly in JavaScript for High-Performance Document Processing

In today’s digital age, document processing tasks like document conversion (DOCX to PDF and HTML to PDF), PDF manipulation, and image processing have become increasingly demanding, especially in enterprise environments where performance, scalability, and reliability are paramount. Traditional JavaScript has its limits when it comes to handling these resource-intensive operations, leading developers to seek out more efficient solutions. Enter WebAssembly (Wasm) — a game-changing technology that allows you to run code written in languages like C, C++, and Rust, directly in the browser, with near-native performance.

This post will explore how WebAssembly can be integrated into JavaScript frameworks to significantly boost the performance of document processing tasks. It’ll walk through some practical examples and showcase how PSPDFKit, an enterprise-level commercial framework, uses WebAssembly to deliver high-performance document processing capabilities.

What Is WebAssembly?

WebAssembly (Wasm) is a low-level binary instruction format that allows code to run at near-native speed in the browser. It’s designed to be a complement to JavaScript, not a replacement. WebAssembly provides a way to execute computationally intensive tasks more efficiently than JavaScript, making it ideal for applications like video processing, gaming, and document processing.

Why Use WebAssembly for Document Processing?

Document processing tasks such as OCR and PDF manipulation require significant computational resources. JavaScript, while versatile, isn’t optimized for heavy computations and can struggle with performance, particularly in scenarios involving large documents or real-time processing.

WebAssembly, on the other hand, is designed for high performance. By leveraging WebAssembly, you can offload the heavy lifting from JavaScript, ensuring faster execution and a smoother user experience. This makes it particularly valuable in enterprise applications where performance is a critical factor.

Setting Up WebAssembly in a JavaScript Project

Let’s start with a simple example to demonstrate how WebAssembly can be integrated into a JavaScript project. This example uses WebAssembly to perform a basic image processing task: converting an image to grayscale.

Step 1 — Writing the WebAssembly Module in C

First, write a small C program that converts an image to grayscale. This C code will be compiled to WebAssembly:

// grayscale.c
#include <stdint.h>

void to_grayscale(uint8_t* rgbaImage, int width, int height) {
    for (int i = 0; i < width * height * 4; i += 4) {
        uint8_t r = rgbaImage[i];
        uint8_t g = rgbaImage[i + 1];
        uint8_t b = rgbaImage[i + 2];

        // Correct grayscale formula.
        uint8_t gray = (uint8_t)(0.299 * r + 0.587 * g + 0.114 * b);

        rgbaImage[i] = gray;       // Red channel.
        rgbaImage[i + 1] = gray;   // Green channel.
        rgbaImage[i + 2] = gray;   // Blue channel.
        // Alpha channel remains unchanged (rgbaImage[i + 3]).
    }
}

Step 2 — Compiling C to WebAssembly

Next, compile the C code to WebAssembly using Emscripten, a popular toolchain for compiling C/C++ code to WebAssembly:

emcc grayscale.c -o grayscale.js -s EXPORTED_FUNCTIONS="['_to_grayscale']" -s MODULARIZE

This command will generate two files: grayscale.js (a JavaScript wrapper), and grayscale.wasm (the WebAssembly binary).

Step 3 — Integrating WebAssembly with JavaScript

Now, integrate the generated WebAssembly module into a JavaScript project:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1.0"
		/>
		<title>WebAssembly Image Processing</title>
	</head>
	<body>
		<input type="file" id="upload" />
		<canvas id="canvas"></canvas>

		<script src="grayscale.js"></script>
		<script>
			document
				.getElementById('upload')
				.addEventListener('change', async (event) => {
					const file = event.target.files[0];
					const img = new Image();
					img.src = URL.createObjectURL(file);
					await img.decode();

					const canvas = document.getElementById('canvas');
					const ctx = canvas.getContext('2d');
					canvas.width = img.width;
					canvas.height = img.height;
					ctx.drawImage(img, 0, 0);

					const imageData = ctx.getImageData(
						0,
						0,
						canvas.width,
						canvas.height,
					);
					const pixels = imageData.data;

					const wasmModule = await grayscale(); // Load the WebAssembly module
					const to_grayscale = wasmModule._to_grayscale;

					const pixelPtr = wasmModule._malloc(pixels.length);
					wasmModule.HEAPU8.set(pixels, pixelPtr);
					to_grayscale(pixelPtr, canvas.width, canvas.height);
					const grayscalePixels = wasmModule.HEAPU8.subarray(
						pixelPtr,
						pixelPtr + pixels.length,
					);
					imageData.data.set(grayscalePixels);
					ctx.putImageData(imageData, 0, 0);
					wasmModule._free(pixelPtr);
				});
		</script>
	</body>
</html>

In this code:

  • You load an image from the user’s file system, draw it on a canvas, and then use WebAssembly to convert the image to grayscale.

  • The heavy computation of converting each pixel to grayscale is done in WebAssembly, taking advantage of its performance benefits.

Advanced Use Case: Document Processing with WebAssembly

While the grayscale example is simple, the principles scale up to more complex document processing tasks like OCR and PDF manipulation. At PSPDFKit, we’ve embraced WebAssembly to optimize performance across our suite of document processing tools. Here’s how WebAssembly is used in an enterprise context.

Example: PDF Manipulation

Imagine you’re building a web application that needs to allow users to manipulate PDFs, e.g. merging, splitting, and reordering pages. These operations, while straightforward, can be CPU-intensive, especially when dealing with large documents.

Using WebAssembly with PSPDFKit

PSPDFKit, a leading document processing SDK, uses WebAssembly under the hood to perform these tasks efficiently in the browser. Here’s a simplified example of how you might integrate PDF manipulation using PSPDFKit and WebAssembly.

  1. First, add PSPDFKit to your project. Use the following commands based on your package manager:

npm install pspdfkit
  1. Copy the PSPDFKit distribution files into your project’s assets directory:

cp -R ./node_modules/pspdfkit/dist/ ./assets/

Ensure your assets directory contains the pspdfkit.js file and a pspdfkit-lib directory with the necessary library assets.

  1. Create an HTML file with the following content:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1.0"
		/>
		<title>PSPDFKit with WebAssembly</title>
	</head>
	<body>
		<div id="pspdfkit-container" style="height: 100vh;"></div>
		<script src="script.js" type="module"></script>
	</body>
</html>
  1. Create a script.js file with the following code:

import './assets/pspdfkit.js';

// We need to inform PSPDFKit where to look for its library assets, i.e. the location of the `pspdfkit-lib` directory.
const baseUrl = `${window.location.protocol}//${window.location.host}/assets/`;

async function loadAndMergeDocuments(documents, configuration) {
	try {
		// Load the first document in a headless instance.
		const instance = await PSPDFKit.load({
			...configuration,
			document: documents[0],
			headless: true,
		});

		// Fetch the rest of the documents as blobs.
		const documentBlobs = await Promise.all(
			documents
				.slice(1)
				.map((url) => fetch(url).then((result) => result.blob())),
		);

		// Initialize the target page index for the first imported document.
		let afterPageIndex = instance.totalPageCount - 1;

		// Create `importDocument` operations for each additional document.
		const mergeDocumentOperations = await Promise.all(
			documentBlobs.map(async (blob, idx) => {
				const operation = {
					type: 'importDocument',
					afterPageIndex,
					treatImportedDocumentAsOnePage: false,
					document: blob,
				};

				// If not the last document, calculate the next `afterPageIndex`.
				if (idx < documentBlobs.length - 1) {
					const documentInstance = await PSPDFKit.load({
						...configuration,
						document: await blob.arrayBuffer(),
						headless: true,
					});
					afterPageIndex += documentInstance.totalPageCount - 1;
					PSPDFKit.unload(documentInstance);
				}
				return operation;
			}),
		);

		// Export the merged document with all operations applied.
		const mergedDocument = await instance.exportPDFWithOperations(
			mergeDocumentOperations,
		);

		// Clean up the headless instance.
		PSPDFKit.unload(instance);

		// Load the merged document into a new viewer instance.
		PSPDFKit.load({
			...configuration,
			document: mergedDocument,
		}).then((instance) => {
			// Optional: Reorder pages within the merged document.
			instance.applyOperations([
				{
					type: 'movePages',
					pageIndexes: [0, 4], // Move pages 0 and 4.
					beforePageIndex: 7, // The specified pages will be moved after page 7.
				},
			]);

			// Export the final modified PDF.
			instance.exportPDF().then((pdf) => {
				const blob = new Blob([pdf], { type: 'application/pdf' });
				const url = URL.createObjectURL(blob);
				const a = document.createElement('a');
				a.href = url;
				a.download = 'modified.pdf';
				document.body.appendChild(a);
				a.click();
				document.body.removeChild(a);
			});
		});
	} catch (error) {
		console.error(
			'Error loading and merging documents:',
			error.message,
		);
	}
}

// Example usage:
const documents = [
	'first_document.pdf',
	'second_document.pdf',
	'third_document.pdf',
];
const configuration = {
	baseUrl,
	container: '#pspdfkit-container',
};

loadAndMergeDocuments(documents, configuration);
  1. Upload the three PDF files with the following names:

  • first_document.pdf

  • second_document.pdf

  • third_document.pdf

These files will be used by the script to demonstrate loading, merging, and modifying PDF documents using PSPDFKit and WebAssembly.

This setup shows how WebAssembly can be used with PSPDFKit in JavaScript to handle complex document processing tasks efficiently. By following these instructions, you can integrate high-performance PDF operations into your web applications with ease.

Why PSPDFKit?

PSPDFKit’s use of WebAssembly allows it to handle these complex operations with speed and precision directly in the browser, without requiring round trips to a server. This not only improves performance, but it also enhances security, as sensitive documents never leave the user’s environment.

With PSPDFKit, developers can integrate sophisticated document processing features into their applications, all while leveraging the power of WebAssembly to ensure high performance and a seamless user experience.

Conclusion

WebAssembly is a powerful tool for enhancing the performance of JavaScript applications, especially for computationally intensive tasks like document processing. By offloading these tasks to WebAssembly, you can achieve near-native performance, making your applications faster and more responsive.

PSPDFKit is a perfect example of how WebAssembly can be used in an enterprise-level document processing framework. Whether you’re dealing with OCR, PDF manipulation, or image processing, WebAssembly provides the performance boost necessary to handle these tasks efficiently in the browser.

As the demand for high-performance web applications continues to grow, integrating WebAssembly into your JavaScript projects is not just a trend — it’s a necessity for staying ahead in the competitive landscape of enterprise software development.

Author
Jonathan D. Rhyne Co-Founder and CEO

Jonathan joined Nutrient in 2014. As CEO, Jonathan defines the company’s vision and strategic goals, bolsters the team culture, and steers product direction. When he’s not working, he enjoys being a dad, photography, and soccer.

Related products
Share post
Free trial Ready to get started?
Free trial