← Back to list

Building a Fully Offline PDF Parser, Browser OCR, and Spatial DOCX Generator

We’ve all been there: you need to build a feature that extracts text from a PDF, inspects the page layout, and drops the output into a…

Ahsan Mubariz · 2026-06-18 11:24 · 0 claps · 3.3 min read
#wasm #pdf #liteparse #tesseract
Open on Medium ↗

Building a Fully Offline PDF Parser, Browser OCR, and Spatial DOCX Generator

We’ve all been there: you need to build a feature that extracts text from a PDF, inspects the page layout, and drops the output into a styled Microsoft Word (.docx) file. Usually, you'd spin up an API server, slap on some Python libraries, pay a cloud provider for processing power, and worry about data privacy.

But what if you could do all of that fully client-side, 100% offline, directly in the browser sandbox?

In this post, we’ll walk through how we built LiteParse Web — a client-side PDF processing pipeline using WebAssembly, Web Workers, and custom spatial layout mapping. We’ll also dive into the technical solutions we implemented to bypass runtime limitations, align differing coordinate systems, and generate high-quality spatial Word files.

The Stack: WebAssembly Meets Client-Side OCR

We wanted a fast, modern UI with robust layout extraction. Here’s what we picked:

  • Astro.js + Tailwind CSS for a super-clean, modern dark-mode interface.
  • **@llamaindex/liteparse-wasm** to handle the heavy lifting of extracting text and structure locally using WebAssembly.
  • PDF.js for rendering PDF pages to HTML5 canvases.
  • Tesseract.js for browser-based Optical Character Recognition (OCR) running inside Web Workers.
  • **docx** (npm library) for programmatically generating OpenXML Word documents.

🛠️ The Architecture & Solutions

1. Browser-Safe OCR Orchestration

The LiteParse WASM engine is written in Rust. By default, its OCR features attempt to run asynchronous operations that require a native Tokio runtime. When executed directly inside browser-based WebAssembly, this triggers a there is no reactor running panic.

To keep the application fully sandboxed and robust, we designed a client-side coordinator:

  • Digital PDFs: We run the WASM engine with ocrEnabled: false to extract text instantly.
  • Scanned/Image PDFs: If digital extraction returns little to no text, we run a custom JS-orchestrated OCR flow. We render the page using PDF.js onto an HTML5 canvas, read the pixel data, and feed it into Tesseract.js worker threads.

This hybrid pipeline provides high-performance native parsing for digital PDFs and fallback OCR for scanned documents without crashing the main thread.

2. Projecting Bottom-Up Coordinates to Top-Down HTML

Dealing with coordinates across document standards means translating spatial layouts:

  • PDFs place (0, 0) at the bottom-left of the page (bottom-up layout).
  • HTML Canvases and Word Documents place (0, 0) at the top-left (top-down layout).

To align these two spaces, we project the word boundaries returned by the OCR worker back into standard PDF points ($1\text{ inch} = 72\text{ pt}$):

$$\text{renderScale} = \frac{\text{DPI}}{72}$$

$$\text{pdfY}{\text{top}} = \frac{\text{canvasHeight} — \text{canvasY}{\text{pixel}}}{\text{renderScale}}$$

With this projection, we can render interactive bounding boxes directly over the page visualization and ensure they line up with the original text.

3. Creating Clean Spatial Word Files (Line Grouping)

The docx library allows absolute positioning on the page using OpenXML text frames (measured in twips, where $1\text{ pt} = 20\text{ twips}$). However, since our parser extracts items at a word level, generating a separate text frame for every single word results in thousands of overlapping micro-boxes. Word processors render these poorly, and they are nearly impossible to edit.

To produce a clean, readable layout, we built a vertical-proximity clustering algorithm:

  • Vertical Proximity Clustering: We sort all text blocks from top to bottom (descending Y). If two consecutive words have vertical coordinates within a threshold (e.g., 4pt), we group them on the same line.
  • Horizontal Sorting: We sort the grouped words left-to-right (ascending X).
  • String Merging: We join the words with spaces and calculate a single bounding box for the entire line:

$\text{Line Width} = (\text{Last Word’s } X + \text{Width}) — \text{First Word’s } X$

$\text{Line Height} = \text{Maximum height of any word in the cluster}$

function groupIntoLines(textItems, yTolerance = 4) {
  items.sort((a, b) => b.y - a.y || a.x - b.x);

  const lines = [];
  let currentLine = [items[0]];

  for (let i = 1; i < items.length; i++) {
    const prev = currentLine[0];
    const curr = items[i];
    if (Math.abs(curr.y - prev.y) <= yTolerance) {
      currentLine.push(curr);
    } else {
      lines.push(currentLine);
      currentLine = [curr];
    }
  }
  lines.push(currentLine);

  return lines.map(words => {
    words.sort((a, b) => a.x - b.x);
    return {
      text: words.map(w => w.text).join(' '),
      x: words[0].x,
      y: words[0].y,
      width: (words[words.length - 1].x + words[words.length - 1].width) - words[0].x,
      height: Math.max(...words.map(w => w.height))
    };
  });
}

Generating a single text frame per line creates a document that matches the original PDF layout visually while remaining editable and clean in Microsoft Word.

👁️ Interactive DOCX Preview in the Browser

To tie the experience together, we added a DOCX Preview tab.

Instead of forcing the user to download the file blindly, this tab renders an A4 layout sheet inside the dark-themed dashboard. Using our line-grouping coordinates, it absolute-positions HTML elements on a simulated white page, matching the exact layout scale. What you see in the browser is exactly what you get when you hit Export DOCX.

🏁 Wrapping Up

Building client-side applications with heavy document parsing and OCR is now completely viable. By combining Rust WebAssembly engines, browser-based OCR fallback workers, and a bit of math to handle line grouping and coordinate mapping, you can deliver premium tools that are fast, private, and offline-first.

you can check the full-code implementation here


메타데이터
post_id
31645a8d9c10
slug
building-a-fully-offline-pdf-parser-browser-ocr-and-spatial-docx-generator-31645a8d9c10
url
https://medium.com/@ahsanmubariz/building-a-fully-offline-pdf-parser-browser-ocr-and-spatial-docx-generator-31645a8d9c10
canonical_url
https://medium.com/@ahsanmubariz/building-a-fully-offline-pdf-parser-browser-ocr-and-spatial-docx-generator-31645a8d9c10
author_url
https://medium.com/@ahsanmubariz
status
ok
fetched_at
2026-06-22 00:13:37