Generate PDF, SVG, and PNG Files in React using a WebAssembly-Based API
| Quick Start Guide | |
|---|---|
| Tutorial Concept |
Learn how to build a React TypeScript applications to generate and export PDF, SVG, and PNG files directly in the browser. This tutorial shows how to configure a WebAssembly-based JavaScript API to create a simple PDF from scratch, draw text and graphics, work with formatted text and custom fonts, load SVG and raster image assets, and reuse the same drawing logic across multiple output formats. |
| What You Will Need |
npm package: @mescius/ds-pdf or download the latest release |
| Controls Referenced |
Document Solutions for PDF JS - WebAssembly-Based JavaScript API |
Document Solutions for PDF JS, or DsPdfJS (@mescius/ds-pdf), is a JavaScript and TypeScript library for working with PDF documents, SVG documents, and raster images such as PNG and JPEG. These formats can easily be converted between one another, making it possible to programmatically open existing PDFs and export pages as SVG or PNG files, or load SVG content and draw it to a PDF page as vector graphics and text.
DsPdfJS supports a broad set of PDF features, including merging and splitting PDFs, reordering pages, working with text, annotations, links, AcroForms, redactions, encryption, metadata, and more. It also includes APIs for creating or loading SVG documents and for resizing, transforming, and applying effects to bitmap images.
A key part of DsPdfJS is its shared drawing model. The DrawingContext class provides a 2D rendering surface for PDF pages, SVG documents, and raster images, with an API that feels similar to drawing on an HTML canvas. Because PDF, SVG, and bitmap outputs can use the same drawing approach, you can write reusable logic that targets multiple document formats.
DsPdfJS also includes an advanced text layout engine. You can format text as paragraphs and columns, use multiple fonts and text formats in the same paragraph, draw vertical (East Asian) and right-to-left text, use font collections and font fallbacks, and apply various justification and alignment options. The size of a text block is easy to measure. DsPdfJS uses the latest Unicode standards to handle bidirectional and vertical text layout, text segmentation, normalization, and line breaks. Fourteen standard PDF fonts are always preloaded, not just for PDFs, but also for drawing to SVG and bitmap output. Other fonts must be loaded from user code as binary data, then parsed in DsPdfJS. TTF, TTC, OTF, and WOFF fonts are currently supported.
Under the hood, DsPdfJS is powered by a WebAssembly module DsPdf.wasm written in C++. This implementation was developed from the ground up based on the experience gained from building Document Solutions for PDF .NET and Document Solutions for Imaging .NET. WebAssembly is supported by modern web browsers and JavaScript runtimes such as Node.js, making DsPdfJS usable in both the browser client-side browser and server-side. The DsPdf.wasm file is about 10 MB, comparable to the size of many font files. In client-side scenarios, the .wasm file can be downloaded once and then efficiently cached by the browser. As a developer, you do not need to work directly with the .wasm file in your application code. The JavaScript and TypeScript API handles that complexity, you just need to configure the WebAssembly file path and manage the lifetime of WebAssembly-backed objects. Fortunately, memory management is straightforward using the @withObjectManager decorator or a couple of utility methods.
In this tutorial, we will build a simple React app in TypeScript using DsPdfJS to draw text and graphics in PDF, SVG, and PNG formats. Feel free to either follow along with the blog below, or download the entire project directly from our GitHub.
Developers Guide to Generating PDF, SVG, & PNG Files in React
- What You’ll Build
- Prerequisites
- Create a React TypeScript App with Vite
- Understanding the React App and DsPdfJS Helper Class
- Understanding ObjectManager and Memory Management
- Add Advanced Drawing APIs
- Next Steps
Download a Free Trial of Document Solutions for PDF JS Today!
What You’ll Build
This tutorial creates a React TypeScript application using Vite. The application starts with a simple PDF generation example and then expands into a more advanced sample that draws formatted text, shapes, gradients, SVG content, and raster images.
The final application includes buttons for:
- Simple PDF
- Draw PDF
- Draw SVG
- Draw PNG
The sample demonstrates how to:
- Configure DsPdfJS in a React TypeScript project
- Connect to the DsPdfJS WebAssembly module
- Create a PDF document from scratch
- Draw text to a PDF page
- Save the generated files in the browser
- Load fonts, SVG files, and image files from the public folder
- Draw formatted text and measure text layout
- Draw rectangles, ellipses, gradients, and styled borders
- Reuse the same drawing logic for PDF, SVG, and PNG output
- Manage WebAssembly-backed object lifetimes
Follow Along: Download the Finished Sample Application from GitHub
Prerequisites
Before getting started, make sure you have installed:
- Node.js
- Visual Studio Code
- A modern browser with WebAssembly support
This sample uses Vite as a lightweight development server.
Create a React TypeScript App with Vite
Assuming you have installed Node.js and Visual Studio Code, create a React app using Vite as a lightweight development server:
npm create vite@latest dspdfjs-getting-started -- --template react-ts
If necessary, type q and press Enter in the Vite console to exit.
The dspdfjs-getting-started project will be created in the current directory.
Step 1: Clean Up the Starter Project
Delete the following unnecessary items from the dspdfjs-getting-started project:
public/icons.svg
src/assets
src/App.css
Then open the dspdfjs-getting-started folder in Visual Studio Code.
Step 2: Install and Configure DsPdfJS
Open the package.json file and add a reference to @mescius/ds-pdf:
"dependencies": {
"@mescius/ds-pdf": "^9.1.2",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
Open a new Terminal window in Visual Studio Code and run:
npm i
This will install the @mescius/ds-pdf npm package.
Step 3: Enable TypeScript Decorators
Open the tsconfig.app.json file and enable TypeScript decorators and metadata:
"compilerOptions": {
...
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
This is needed because the sample uses the @withObjectManager decorator for memory management.
Step 4: Create the React App Component
Open src/App.tsx in the editor and replace its content with the following code. This component manages the DsPdfJS loading state, connects to the WebAssembly module when the app starts, and provides the first button users will click to generate a sample PDF:
import { Component } from "react";
import { Demos } from "./Demos";
interface State {
loading: boolean,
error: boolean,
demoError: string
}
export class App extends Component<{}, State> {
constructor(props: {}) {
super(props);
this.state = { loading: true, error: false, demoError: "" };
}
async componentDidMount() {
if (await Demos.connect())
this.setState({ loading: false, error: false });
else
this.setState({ loading: false, error: true });
}
componentWillUnmount() {
Demos.disconnect();
}
// runs a demo method, showing any errors on the page
async runDemo(demo: () => Promise<void>) {
this.setState({ demoError: "" });
try {
await demo();
} catch (e) {
this.setState({ demoError: e instanceof Error ? e.message : String(e) });
}
}
render() {
let s, disabled;
if (this.state.error) {
s = "DsPdfJS initialization error...";
disabled = true;
} else if (this.state.loading) {
s = "DsPdfJS loading..."
disabled = true;
} else {
s = `Loaded DsPdfJS version ${Demos.apiVersion}`;
disabled = false;
}
return (
<div>
<div>{s}</div>
<button disabled={disabled} onClick={async () => { await this.runDemo(() => Demos.simplePdf()); }}>Simple PDF</button>
{this.state.demoError && <div style={{ color: "red" }}>{this.state.demoError}</div>}
</div>
)
}
}
export default App
Step 5: Create the DsPdfJS Helper Class
Add a new file src/Demos.ts with the following content:
import {
connectDsPdf,
disconnectDsPdf,
DsPdf,
DsPdfConfig,
withObjectManager,
PdfDocument
} from '@mescius/ds-pdf';
export class Demos
{
//#region helper methods
/**
* Gets the version of the DsPdf WebAssembly module,
* or "Unknown" if not connected.
* **/
static get apiVersion(): string {
return DsPdf.instance?.version || "Unknown";
}
/**
* Indicates whether the DsPdf WebAssembly module is currently connected.
* **/
static get isConnected(): boolean {
if (DsPdf.instance) {
return DsPdf.instance.isConnected;
}
return false;
}
/**
* Connects to the DsPdf WebAssembly module if not already connected.
* @returns A promise that resolves to true if the connection is successful,
* or to false in case of any errors.
**/
static async connect(): Promise<boolean> {
if (!this.isConnected) {
DsPdfConfig.wasmUrl =
"node_modules/@mescius/ds-pdf/assets/DsPdf.wasm";
return await connectDsPdf();
}
return true;
}
/** Disconnects from the DsPdf WebAssembly module. **/
static disconnect() {
if (this.isConnected) {
disconnectDsPdf();
}
}
/**
* Loads a binary file with specified file name from the public folder.
* Do not use cache by default to avoid loading stale content.
* @param fileName The name of the file to load.
* @param cached Whether to load the cached content of the file.
* @returns A promise resolving to the loaded file as a Uint8Array.
**/
static async loadFile(fileName: string, cached: boolean = false): Promise<Uint8Array> {
const response = await fetch(fileName,
{ cache: cached ? 'force-cache' : 'no-cache' });
if (!response.ok) {
throw new Error(`Unable to load file: ${response.statusText}`);
}
return new Uint8Array(await response.arrayBuffer());
}
/**
* Saves a file with specified name, binary content, and MIME type
* by triggering a download in the browser.
* @param fileName The name of the file to save.
* @param bytes The content of the file as a Uint8Array.
* @param mimeType The MIME type of the file.
**/
static saveFile(fileName: string, bytes: Uint8Array, mimeType: string) {
const blob = new Blob([bytes as BlobPart], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log(`saved '${mimeType}' file as ${fileName}`);
}
//#endregion
@withObjectManager
static async simplePdf() {
// create a new PDF document
const doc = new PdfDocument();
// add a new page with default size to the document and
// get the page drawing context
const ctx = doc.newPageContext();
// draw some text on the page at coordinates (72, 72)
// using the default font, size, and foreColor.
ctx.drawText({ text: "Hello World!" }, 72, 72);
// convert the document to a byte array and save as simple.pdf
this.saveFile("simple.pdf", doc.savePdf(), "application/pdf");
}
}
Step 6: Configure VS Code Debugging
Now you can build this project from the Visual Studio Code menu: Terminal > Run Build Task > npm build
Open the Run and Debug tab in Visual Studio Code, then click create a launch.json file.
Select: Web App (Chrome) > Chrome: Launch
Change the resulting vscode/launch.json file to the following:
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Chrome",
"request": "launch",
"type": "chrome",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}",
"preLaunchTask": "start vite"
}
]
}
Then add a tasks.json file to the .vscode folder with the following content:
{
"version": "2.0.0",
"tasks": [
{
"label": "start vite",
"type": "npm",
"script": "dev",
"isBackground": true,
"problemMatcher": {
"owner": "custom",
"pattern": [
{
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": "Local:.*http://localhost:5173"
}
}
}
]
}
Step 7: Run the Application
The application is ready to run.
Press F5 to start debugging and view the app in Chrome.

If you click the Simple PDF button, a new PDF document will be generated and saved to the Downloads folder as simple.pdf.

Understanding the React App and DsPdfJS Helper Class
In the application above, the App class is derived from the React Component class. While it is often recommended to convert class components to React Hooks, the componentDidMount() and componentWillUnmount() methods are convenient for initializing and finalizing DsPdfJS.
The App class also provides the UI for the simple web app. Each button runs its demo through the runDemo() helper, which clears any previous error, awaits the demo, and stores the message of any thrown exception in the demoError state so it can be shown on the page.
Most of the important DsPdfJS logic is in the Demos class. There, we define helper methods for connecting to and disconnecting from the DsPdfJS WebAssembly module.
Notice how the path to the DsPdf.wasm file is set in the connect() method:
DsPdfConfig.wasmUrl = "node_modules/@mescius/ds-pdf/assets/DsPdf.wasm";
This is done before executing the connectDsPdf() global method.
The product license can also be assigned in this method:
await DsPdfConfig.setLicenseKey("MY_LICENSE_KEY");
There are some restrictions with PDF loading and generation if the license key is not set.
Download a Free Trial and Request a 30-Day Evaluation Key
The loadFile() helper method can be used for loading fonts, images, and PDF files from the web server. DsPdfJS is unable to load files directly from the client machine unless the user submits those files manually.
The last helper method is saveFile(), which saves the provided byte array as a file of the specified MIME type, such as an image or PDF document.
The simplePdf() method generates a new PDF document from scratch and saves it using the saveFile() helper method.
Understanding ObjectManager and Memory Management
As shown above, the simplePdf() method has the associated @withObjectManager decorator. This decorator defines the scope for all objects, such as PdfDocument and PdfContext, created in this method so these objects are automatically released at the end of the method.
You can rewrite the simplePdf() method using the pushObjectManager() and popObjectManager() global methods instead of the decorator:
static async simplePdf() {
pushObjectManager();
try {
const doc = new PdfDocument();
const ctx = doc.newPageContext();
ctx.drawText({ text: "Hello World!" }, 72, 72);
this.saveFile("simple.pdf", doc.savePdf(), "application/pdf");
} finally {
popObjectManager();
}
}
It is also possible to define the scope for individual objects by associating those objects with specific ObjectManager instances. Objects such as a PdfDocument remain alive until their owning ObjectManager is disposed.
We can rewrite the above method as follows:
static async simplePdf() {
const om = new ObjectManager();
try {
const doc = new PdfDocument(om);
const ctx = doc.newPageContext();
ctx.drawText({ text: "Hello World!" }, 72, 72);
this.saveFile("simple.pdf", doc.savePdf(), "application/pdf");
} finally {
om.dispose();
}
}
For some environments, not including Safari, the following syntax is also allowed:
static async simplePdf() {
using om = new ObjectManager();
const doc = new PdfDocument(om);
const ctx = doc.newPageContext();
ctx.drawText({ text: "Hello World!" }, 72, 72);
this.saveFile("simple.pdf", doc.savePdf(), "application/pdf");
}
Using the @withObjectManager decorator is still the simplest option.
Add Advanced Drawing APIs
Now, let’s add a more advanced example of drawing text and graphics to various formats.
Step 1: Add the Needed Imports
Add the following import at the beginning of Demos.ts:
import {
connectDsPdf,
disconnectDsPdf,
DsPdf,
DsPdfConfig,
withObjectManager,
PdfDocument,
DrawingContext,
StandardPdfFont,
Font,
LinearGradientBrush,
AngleUnits,
DashStyle,
PenLineCap,
Format,
Layout,
SvgDocument,
Image,
SvgContext,
BmpContext
} from '@mescius/ds-pdf';
Step 2: Draw Shared Content to PDF, SVG, and PNG
Then, add the following methods to the Demos class:
- The
drawContent()method accepts a targetDrawingContext, so the same drawing code can render to a PDF page, an SVG document, or a bitmap (saved as PNG or JPEG) while demonstrating text measurement, formatted text, custom fonts, gradients, transformations, styled borders, and basic shapes. - The
drawPdf()method creates a two-page PDF document. The first page uses the shared drawContent() method, while the second page loads and draws an SVG file and a raster image while preserving the image’s aspect ratio. - The
drawSvg()method creates an SVG drawing context, scales the output, draws the shared content, and saves the result asdrawSvgTest.svg. - The
drawPng()method creates a bitmap drawing context, draws the shared content, and saves the result asdrawPngTest.png.
/**
* Draws the same text and graphics to PDF, SVG, and PNG.
* @param ctx The target drawing context.
**/
@withObjectManager
static async drawContent(ctx: DrawingContext) {
const Inch = 72;
let x = Inch;
let y = Inch;
// get a pre-defined standard PDF font
// and load a custom font from a file
const helvFont = Font.getPdfFont(StandardPdfFont.HelveticaItalic);
const notoFont =
Font.load(await this.loadFile("NotoSerif-Regular.ttf"));
// create a text format object
const tf = new Format({
font: notoFont,
fontSize: 15
});
// draw text with limited max width and word wrapping, and rectangles
// showing the measured text size and the max width
const text =
"Test string to demo the measureText() function used with drawText().";
const maxWidth = Inch * 3;
const textSize = ctx.measureText(text, tf, maxWidth);
ctx.drawRect(x, y, maxWidth, textSize.height,
{ lineWidth: 3, lineColor: "PeachPuff" });
ctx.drawRect(x, y, textSize.width, textSize.height,
{ lineWidth: 1, lineColor: "OrangeRed" });
ctx.drawText(text, tf, x, y, maxWidth);
// size of the text block is calculated with ctx.measureText()
y += textSize.height + 10;
// the Layout object can be used to layout multiple text
// fragments with different formatting
const tl = new Layout({
defaultFormat: new Format(tf, { font: helvFont }),
maxWidth: ctx.width - Inch * 2,
firstLineIndent: Inch * 0.5,
paragraphSpacing: Inch * 0.1,
lineSpacingScaleFactor: 0.8
});
tl.append("First test string added to the Layout object. ");
tl.appendLine(
"Second test string added to Layout, continuing the same paragraph.");
tl.append({
text: "Third test string added to Layout, a new paragraph. ",
foreColor: "DarkRed"
});
const notoBI =
Font.load(await this.loadFile("NotoSerif-BoldItalic.ttf"));
tl.append({
text: "Fourth test string, with a different char formatting. ",
font: notoBI,
foreColor: "DarkSeaGreen"
});
tl.append("Fifth test string, using the Layout's default format.");
ctx.drawLayout(tl, x, y);
// size of the Layout object is calculated in ctx.drawLayout(),
// or you can call tl.performLayout() to calculate it before drawing
// so we use tl.contentHeight to position the next drawing
// after the text block.
y += tl.contentHeight + 10;
// draw an ellipse with a linear gradient fill and styled border
ctx.pushTransform();
ctx.translate(x + 40, y);
ctx.rotate(30, AngleUnits.Degrees);
const linearBrush = new LinearGradientBrush({
startColor: "Turquoise",
startPoint: { x: 0.2, y: 0.2 },
gradientStops: [{ color: "Yellow", offset: 0.5 }],
endColor: "SeaGreen",
endPoint: { x: 0.8, y: 0.8 },
});
ctx.drawEllipse({ width: 150, height: 100 }, {
fillBrush: linearBrush,
lineColor: "RoyalBlue",
lineStyle: DashStyle.DashDot,
lineCap: PenLineCap.Round,
lineWidth: 3
});
ctx.popTransform();
// draw a rectangle around the page content
ctx.drawRect({
left: 1, top: 1, right: (ctx.width - 1), bottom: (ctx.height - 1)
}, { lineColor: "ForestGreen", lineWidth: 2 });
}
@withObjectManager
static async drawPdf() {
const doc = new PdfDocument();
// first page
let ctx = doc.newPageContext({ width: 720, height: 450 });
await this.drawContent(ctx);
// second page
ctx = doc.newPageContext({ width: 720, height: 800 });
const maxWidth = ctx.width - 144;
let x = 72;
let y = 72;
// load and draw an SVG file with text and graphics
const svgDoc = SvgDocument.load(await this.loadFile("units.svg"));
const sz = svgDoc.getIntrinsicSize()!;
svgDoc.sansSerifFont =
Font.load(await this.loadFile("NotoSans-Light.ttf"));
ctx.drawSvg(svgDoc, x, y);
y += sz.height + 20;
// load and draw an image file, keeping its aspect ratio
const img = Image.load(await this.loadFile("grand-canyon.jpg"));
ctx.drawImage(img, x, y, maxWidth, maxWidth, { keepAspectRatio: true });
this.saveFile("drawPdfTest.pdf", doc.savePdf(), "application/pdf");
}
@withObjectManager
static async drawSvg() {
const ctx = new SvgContext(720, 450);
const scaleFactor = 1.5; // to enlarge the resulting SVG
ctx.scale(scaleFactor);
await this.drawContent(ctx);
ctx.width *= scaleFactor;
ctx.height *= scaleFactor;
const svg = ctx.toSvgDocument();
this.saveFile("drawSvgTest.svg", svg.save(), "image/svg+xml");
}
@withObjectManager
static async drawPng() {
// scale factor (1.5) can be used to create a bitmap with better quality
const ctx = new BmpContext(720, 450, 1.5, "White");
await this.drawContent(ctx);
const bmp = ctx.bitmap;
this.saveFile("drawPngTest.png", bmp.saveAsPng(), "image/png");
}
Step 3: Add Buttons for Each Output Format
Add three additional buttons in the App.tsx file:
<button disabled={disabled} onClick={async () => { await this.runDemo(() => Demos.drawPdf()); }}>Draw PDF</button>
<button disabled={disabled} onClick={async () => { await this.runDemo(() => Demos.drawSvg()); }}>Draw SVG</button>
<button disabled={disabled} onClick={async () => { await this.runDemo(() => Demos.drawPng()); }}>Draw PNG</button>
After adding these buttons, the app will allow users to generate a simple PDF, a more advanced PDF, an SVG file, and a PNG file.
Step 4: Add Required Fonts and Image Assets
This sample uses a few additional fonts and images. Add the following files to the application’s public folder:
units.svg
NotoSans-Light.ttf
NotoSerif-BoldItalic.ttf
NotoSerif-Regular.ttf
grand-canyon.jpg
You can find these files in the public folder of the sample in the GitHub repository.
Step 5: Build and Run the Final Sample
Build and run the project now.
The application should show a web page with four buttons:

Clicking the Draw SVG button saves the drawSvgTest.svg file with the following output:

The Draw PNG button saves the same image in PNG format.
The Draw PDF button creates a PDF document with two pages. The first page looks like the SVG and PNG output shown above. The second page contains a drawn SVG image and a raster image.

As you can see in the source code, we use the same drawContent() method for drawing text and graphics to all three formats. Drawing contexts are created individually for each format, with minor differences related to scaling and background filling.
The @withObjectManager decorator is also applied to the drawContent() method because the fonts, formats, layouts, and brushes created in this method are not used by other methods and can be released at the end of the method.
If you cloned or downloaded the dspdfjs-getting-started sample from the GitHub repository, run the following command in the Visual Studio Code Terminal to restore the references before building and running the project:
npm i
Next Steps
This tutorial introduced the basics of using Document Solutions for PDF JS (DsPdfJS) in a React TypeScript application to programmatically generate and export PDF, SVG, and PNG files directly in the browser. You configured the DsPdfJS WebAssembly module, created a simple PDF from scratch, drew text to a PDF page, managed WebAssembly object lifetimes, and expanded the sample to reuse the same drawing logic across multiple output formats. You also worked with formatted text, text measurement, custom fonts, gradients, SVG content, and raster images to create richer document output.
Download the finished tutorial sample application here from our GitHub.
You can view more projects similar to the above by visiting the online DsPdfJS demos or checking out the DsPdfJS GitHub samples, and you can learn more about the product by viewing the documentation.
Ready to dive deeper into Document Solutions for PDF JS? Get started with a 30-day free trial here!