← Back to blog

In-Browser PDF Generation: Canvas Rendering, Vector Graphics, and WASM Compilers

PetexSpace

Generating production-grade Portable Document Format (PDF) files has traditionally been relegated to backend server clusters running headless Chromium instances or native C libraries. As web browsers have evolved into robust application runtimes, document pipelines increasingly generate high-fidelity, paginated, and secure PDF documents directly on the user's device.

However, the architecture of in-browser PDF generation is fractured. Developers frequently default to quick DOM rasterization hacks that degrade document accessibility, bloat file sizes, and blur text under high magnification.

Achieving true publishing fidelity requires understanding the ISO 32000-1 specification, constructing low-level vector graphics streams, subsetting TrueType fonts in memory, and running WebAssembly-compiled native document engines directly within the client browser.

1. Deconstructing the PDF/A Specification and Low-Level Object Model

The Portable Document Format is not a markup language or a structured document tree like HTML or XML. Derived from PostScript, PDF is an indexed binary file format designed to describe the exact visual placement of text, vector shapes, and raster images on a two-dimensional fixed canvas, independent of output devices.

+-----------------------------------------------------------------------+
|                    ISO 32000-1 Physical File Layout                   |
+-----------------------------------------------------------------------+
|  %PDF-1.7                                 <-- Header Magic Bytes      |
|  %âãÏÓ                                    <-- Binary Marker (>127)    |
|                                                                       |
|  1 0 obj                                  <-- Object 1: Catalog       |
|  << /Type /Catalog /Pages 2 0 R >>                                    |
|  endobj                                                               |
|                                                                       |
|  2 0 obj                                  <-- Object 2: Pages Tree    |
|  << /Type /Pages /Kids [3 0 R] /Count 1 >>                            |
|  endobj                                                               |
|                                                                       |
|  3 0 obj                                  <-- Object 3: Page Node     |
|  << /Type /Page /Parent 2 0 R                                         |
|     /MediaBox [0 0 612 792]                                           |
|     /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>         |
|  endobj                                                               |
|                                                                       |
|  4 0 obj                                  <-- Object 4: Stream Data   |
|  << /Length 78 /Filter /FlateDecode >>                                |
|  stream                                                               |
|  ... [Deflate-compressed vector and text operator bytes] ...          |
|  endstream                                                            |
|  endobj                                                               |
|                                                                       |
|  xref                                     <-- Cross-Reference Table   |
|  0 5                                                                  |
|  0000000000 65535 f                                                   |
|  0000000015 00000 n                                                   |
|  0000000074 00000 n                                                   |
|  0000000135 00000 n                                                   |
|  0000000310 00000 n                                                   |
|                                                                       |
|  trailer                                  <-- Trailer Dictionary      |
|  << /Size 5 /Root 1 0 R >>                                            |
|  startxref                                                            |
|  450                                      <-- Byte Offset to xref     |
|  %%EOF                                    <-- End-of-File Sentinel    |
+-----------------------------------------------------------------------+

The Seven Fundamental PDF Primitives

Every PDF document is constructed from seven fundamental data types:

  1. Booleans: true and false.
  2. Numbers: Integer (e.g., 42) or real floating-point numbers (e.g., 3.14159).
  3. Strings: Enclosed in parentheses for literal ASCII/PDFDocEncoding ((Sample Text)) or angle brackets for hexadecimal representations (<48656C6C6F>).
  4. Names: Atomic identifiers prefixed by a forward slash (e.g., /Type, /Font, /MediaBox).
  5. Arrays: Ordered collections of objects enclosed in square brackets (e.g., [0 0 612 792]).
  6. Dictionaries: Key-value lookup tables enclosed in double angle brackets (<< /Key /Value >>), where keys must be Names.
  7. Streams: Sequences of arbitrary binary bytes enclosed between stream and endstream keywords, accompanied by a dictionary specifying length and compression filters (/FlateDecode).

The Document Graph and the Cross-Reference Table

A valid PDF forms a directed acyclic graph rooted at the Catalog dictionary (/Catalog). The Catalog points to the root of the /Pages tree, which branches down into individual /Page leaf nodes. Each page specifies a bounding dimension (/MediaBox), associated resources (/Resources, such as fonts, images, and color spaces), and one or more /Contents streams holding the drawing operations.

Crucially, PDF parsers do not read files sequentially from top to bottom. Instead, a parser begins at the physical end of the file, locates the %%EOF marker, reads the startxref byte offset, and inspects the Cross-Reference Table (xref). The xref table specifies the exact byte position of every indirect object (obj ... endobj) in the file.

This architecture enables random-access rendering, allowing viewers to fetch and render page 250 without reading or parsing the preceding 249 pages.

2. DOM Rasterization vs Vector-Native Generation: The Fidelity Battle

When product teams implement PDF exports in web applications, they typically choose between two fundamentally divergent paths: DOM rasterization or native vector generation.

+-----------------------------------------------------------------------+
|             DOM Rasterization vs. Native Vector Assembly              |
+-----------------------------------------------------------------------+
| Approach A: DOM Rasterization (html2canvas, window.print)             |
|   DOM HTML Elements                                                   |
|         |                                                             |
|         v                                                             |
|   [ Canvas Rasterization Engine ] ---> Converts text & CSS to pixels  |
|         |                                                             |
|         v                                                             |
|   [ JPEG/PNG Compression ] ----------> Injected into single PDF image |
|                                                                       |
|   Drawbacks: 72-96 DPI blur, unselectable text, no hyperlinks, massive|
|              file size, non-conforming PDF/A accessibility.           |
+-----------------------------------------------------------------------+
| Approach B: Vector-Native Generation (PetexSpace Architecture)        |
|   Document Abstract Syntax Tree                                       |
|         |                                                             |
|         v                                                             |
|   [ In-Memory PDF Writer ]                                            |
|   - Subsets TrueType font glyphs (.ttf -> .cff/.ttf subset)          |
|   - Emits compact text operators: BT /F1 12 Tf 72 712 Td (Text) Tj ET |
|   - Emits mathematical vector paths: 100 100 m 200 200 l S            |
|         |                                                             |
|         v                                                             |
|   Crisp, infinitely scalable, selectable, searchable, lightweight.    |
+-----------------------------------------------------------------------+

The Pitfalls of Canvas and DOM Rasterization

The rasterization workflow (commonly driven by html2canvas paired with simple wrapper libraries) serializes the live DOM, draws it onto an HTML5 <canvas>, and encapsulates the resulting raster image inside a bare-bones PDF document wrapper.

While straightforward to implement, this pattern exhibits severe architectural flaws:

  • Resolution Degradation: Standard screens render at 72 to 96 DPI. Printing requires at least 300 to 600 DPI. Upscaling canvas captures consumes massive browser memory while leaving text visibly blurry.
  • Loss of Text Semantics: Because characters are converted into static pixels, readers cannot search for keywords, highlight sentences, copy text, or navigate using screen readers.
  • Loss of Hyperlinks and Metadata: Hyperlinks, structural outlines, table-of-contents bookmarks, and document metadata are discarded.
  • File Size Bloat: A 10-page rasterized document routinely reaches 25 to 50 megabytes, whereas a vector-native equivalent containing embedded fonts is typically under 400 kilobytes.

Vector Drawing Operators: Direct PDF Instruction Emission

In contrast, vector-native PDF generation constructs the content stream by emitting low-level PostScript-derived drawing operators:

BT                          % Begin Text Object
/F1 12 Tf                   % Set Font /F1 at 12pt size
1 0 0 1 72 720 Tm           % Set Text Matrix: Translate to x=72, y=720
(Hello, PetexSpace!) Tj     % Show Text String
ET                          % End Text Object

0.2 0.4 0.8 rg              % Set RGB fill color (Non-stroking)
100 600 200 150 re          % Define rectangle at x=100, y=600, w=200, h=150
f                           % Fill rectangle path

Because text is emitted as character codes referencing vector glyph outlines, the document remains infinitely scalable, fully searchable, accessible to screen readers, and orders of magnitude smaller on disk.

3. Typography, TrueType Font Subsetting, and Unicode CMaps

The most intricate engineering challenge in building an in-browser vector PDF generator is font management. A standard TrueType (.ttf) or OpenType (.otf) font file for a comprehensive typeface family (such as Noto Sans or Roboto) frequently exceeds 20 megabytes when supporting international Unicode scripts.

Embedding entire font files into every generated document produces unacceptable file bloat. The generator must perform real-time font subsetting in browser memory.

+-----------------------------------------------------------------------+
|                 In-Memory Font Subsetting Pipeline                    |
+-----------------------------------------------------------------------+
|  Original TrueType Font (.ttf) [~25 MB, 15,000 glyphs]                |
|       |                                                               |
|       v                                                               |
|  [ Ingest Document Text Stream ] ---> Identify unique characters      |
|                                       Used: { 'P', 'e', 't', 'x', ... }|
|       |                                                               |
|       v                                                               |
|  [ Glyph Harvester & SFNT Builder ]                                   |
|    - Parse SFNT tables: 'head', 'hhea', 'maxp', 'glyf', 'loca'        |
|    - Extract only required glyph outlines and composite dependencies   |
|    - Renumber glyph IDs starting from 0 to N                          |
|    - Recalculate checksums & table offsets                            |
|       |                                                               |
|       v                                                               |
|  [ Output SubsetFont.ttf ] [~18 KB, 42 glyphs]                        |
|       |                                                               |
|       +--> Embed into /FontFile2 Stream                               |
|       +--> Synthesize /ToUnicode CMap for reverse text search         |
+-----------------------------------------------------------------------+

Deconstructing the SFNT Structure

TrueType fonts are packaged within an SFNT container consisting of a table directory followed by specialized tables:

  • head: Font header containing global metadata, bounding box limits, and the indexToLocFormat flag.
  • hhea and hmtx: Horizontal header and horizontal metrics defining character advance widths and side bearings.
  • maxp: Maximum profile specifying the total count of glyphs in the font.
  • loca: Location table storing byte offsets to individual glyph data inside the glyf table.
  • glyf: Raw vector curve data (Bézier curves and coordinates) for each character glyph.
  • cmap: Character-to-glyph mapping table translating Unicode code points to font-internal Glyph IDs (GID).

Constructing the SubsetFont in TypeScript

The subsetting engine inspects every character rendered in the document, resolves its corresponding GID through the font's cmap, and constructs a new, minimal SFNT binary in virtual memory containing only those referenced glyphs:

// Subsetting helper extracting glyph dependencies
export class FontSubsetter {
  private usedGlyphIds = new Set<number>([0]); // Always preserve GID 0 (.notdef)

  public markGlyphUsed(unicodeChar: string, cmapTable: Map<number, number>): void {
    const codePoint = unicodeChar.codePointAt(0);
    if (codePoint !== undefined) {
      const gid = cmapTable.get(codePoint) ?? 0;
      this.usedGlyphIds.add(gid);
    }
  }

  public assembleSubsetFont(originalFontBytes: Uint8Array): Uint8Array {
    // 1. Traverse 'glyf' table to collect composite sub-glyphs
    // 2. Build new sequential glyph index mapping (0 .. N-1)
    // 3. Rebuild 'loca', 'glyf', 'hmtx', and 'maxp' tables
    // 4. Recalculate SFNT table directory and 4-byte checksums
    return new Uint8Array(); // Returns optimized binary font slice
  }
}

The Role of /ToUnicode CMaps

When a PDF viewer displays text, it renders glyph indices mapped by font metrics. However, when a user copies text to their clipboard or an indexing engine crawls the document, the viewer must map those arbitrary glyph indices back to standard Unicode characters.

This mapping is governed by an embedded /ToUnicode CMap stream. Without a valid /ToUnicode mapping, copied text appears as scrambled gibberish:

/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
/CMapName /Custom-ToUnicode def
/CMapType 2 def
1 begincodespacerange
<0001> <00FF>
endcodespacerange
1 beginbfrange
<0001> <0001> <0050> % Maps Glyph 1 to Unicode 'P' (U+0050)
endbfrange
endcmap
CMapName currentdict /CMap defineresource pop
end
end

4. WebAssembly-Accelerated PDF Assembly Engines

While writing basic PDF files using pure JavaScript libraries is viable for simple forms, advanced document engineering requires full typographic layout engines. Complex requirements like Knuth-style paragraph justification, hyphenation dictionaries, nested table pagination, and PDF/A compliance are best handled by native libraries compiled to WebAssembly.

+-----------------------------------------------------------------------+
|                    WebAssembly PDF Engine Pipeline                    |
+-----------------------------------------------------------------------+
|   Main Application Thread                                             |
|        |                                                              |
|        | Document Spec (JSON AST: sections, tables, styles)           |
|        v                                                              |
|   Dedicated Dedicated Worker                                          |
|   +---------------------------------------------------------------+   |
|   |  Native Compiled Engine (Rust / C++ via wasm32-unknown-emscripten)|
|   |  - HarfBuzz WASM (Bidirectional complex text shaping)         |   |
|   |  - Libdeflate WASM (High-throughput FlateDecode compression)   |   |
|   |  - Pagination Engine (Widow/Orphan break optimization)        |   |
|   |                                                               |   |
|   |  Linear Memory Buffer:                                        |   |
|   |  [ Object Graph ] -> [ Subsetter ] -> [ Deflate ] -> [ XRef ] |   |
|   +---------------------------------------------------------------+   |
|        |                                                              |
|        | Transferable ArrayBuffer (Zero-Copy)                         |
|        v                                                              |
|   Blob / URL.createObjectURL (Instant in-browser preview / download)  |
+-----------------------------------------------------------------------+

HarfBuzz Text Shaping in WebAssembly

When typesetting complex scripts (such as Arabic, Devanagari, or connected Latin ligatures like "fi" and "fl"), character rendering cannot rely on simple character-to-glyph substitutions. Glyphs shift shape, reposition vertically, and combine based on neighboring context.

Compiling the industry-standard HarfBuzz text shaping engine to WebAssembly enables accurate bidirectional text shaping directly inside the browser. HarfBuzz consumes Unicode code points and OpenType font tables, outputting exact glyph indices and sub-pixel (x,y)(x, y) coordinate offsets for every character.

High-Throughput Stream Compression via Libdeflate

PDF content streams and embedded font binaries must be compressed using the FlateDecode algorithm (RFC 1951 Deflate). While the browser provides native CompressionStream APIs, their availability across worker contexts and compression ratio tuning can be unpredictable.

Integrating a WebAssembly build of libdeflate provides ultra-fast, multi-threaded compression directly inside the worker thread, reducing PDF generation times from seconds to single-digit milliseconds.

5. Privacy-Preserving Web Tools: The PetexSpace Document Model

The PetexSpace architecture for in-browser PDF tools eliminates the security, privacy, and latency risks inherent in traditional cloud-based document platforms. You can also convert documents client-side without transmitting sensitive source files across the internet.

+-----------------------------------------------------------------------+
|                  PetexSpace Local Document Pipeline                   |
+-----------------------------------------------------------------------+
|  User Data Ingest (Client-Side Browser Sandbox)                       |
|       |                                                               |
|       v                                                               |
|  [ Web Worker: Vector Engine & HarfBuzz WASM ]                        |
|       |                                                               |
|       v                                                               |
|  [ FlateStream Compression & PDF Assembly ]                           |
|       |                                                               |
|       v                                                               |
|  [ In-Memory Binary Buffer (Uint8Array) ]                             |
|       |                                                               |
|       +---> Blob (type: 'application/pdf')                            |
|       |                                                               |
|       v                                                               |
|  [ URL.createObjectURL(blob) ] ------------------------------------+  |
|                                                                    |  |
|  +-----------------------------------------------------------------+  |
|  | Immediate Native Display via PDF.js Canvas                         |
|  | Instantaneous Direct Local File Download                           |
|  | ZERO Bytes Transmitted Outside Local Host                          |
|  +--------------------------------------------------------------------+
+-----------------------------------------------------------------------+

The Zero-Transmission Data Boundary

When an enterprise converts financial spreadsheets, legal contracts, or medical records using legacy web converters, those files are uploaded to remote multi-tenant servers. This exposes corporate assets to data retention risks, server misconfigurations, and regulatory compliance breaches under GDPR and HIPAA.

PetexSpace implements client-side processing across every document tool. The PDF creation pipeline operates entirely within the isolated memory space of the user's browser:

// Deterministic in-memory PDF export trigger
export function exportDocumentToLocalFile(pdfBytes: Uint8Array, filename: string): void {
  // Construct ephemeral blob pointing to in-memory byte buffer
  const blob = new Blob([pdfBytes], { type: 'application/pdf' });
  const objectUrl = URL.createObjectURL(blob);

  // Trigger synthetic anchor click for local browser download
  const link = document.createElement('a');
  link.href = objectUrl;
  link.download = filename;
  document.body.appendChild(link);
  link.click();

  // Clean up references and release memory immediately
  document.body.removeChild(link);
  setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
}

Eliminating Network Latency

Beyond security advantages, in-browser execution delivers distinct performance benefits. Eliminating the roundtrip upload and download stages removes network bandwidth bottlenecks entirely. Multi-hundred-page documents compile, paginate, subset, and render in milliseconds, enabling instant live previews as users edit content.

By combining WebAssembly execution, TrueType font subsetting, and low-level vector generation, modern web applications can deliver desktop-class document engineering tools with absolute data privacy.