If you've ever tried to use a Text-to-Speech (TTS) tool to listen to a research paper, you already know the pain. You upload an arXiv paper, hit play, and the voice starts reading:
"Introduction ... Abstract ... 1. Background ... We present a new methodology for ... the state of the art in ... "
It reads straight across the page, blindly mashing the left and right columns together into an incomprehensible word salad. Even expensive tools like Speechify or standard open-source options like Chrome's built-in Read Aloud suffer from this.
I got frustrated enough that I spent a weekend building my own solution: a Chrome Extension called Read PDF Aloud. The goal was simple: properly parse two-column PDFs and read them entirely offline for privacy. Here is how I built it.
The Tech Stack
I wanted this to be a lightweight client-side tool. No backend, no cloud processing, no sending sensitive research or legal documents to a third-party server.
- Manifest V3 Chrome Extension
- PDF.js (Mozilla) – To extract text and exact positional coordinates.
- Web Speech API – The browser's native
speechSynthesisengine for 100% offline TTS.
Under the Hood: Spatial Parsing
Most PDF readers fail because they rely on the raw text extraction order from the PDF file. Instead of doing that, I used PDF.js to grab the bounding box (transform matrix) of every single text item on the page, and then applied spatial sorting.
First, we filter out headers and footers (top and bottom 7% of the page) so the TTS doesn't randomly read page numbers in the middle of a sentence:
const W = viewport.width;
const H = viewport.height;
// Exclude top 7% and bottom 7%
const contentItems = textContent.items.filter(item => {
const ty = item.transform[5];
return ty > H * 0.07 && ty < H * 0.93;
});
Next, we run a simple heuristic to detect if the page actually has a two-column layout. We count how many text blocks sit purely on the left half of the page, how many on the right, and how many cross the middle boundary.
let leftCount = 0;
let rightCount = 0;
let crossCount = 0;
contentItems.forEach(item => {
const tx = item.transform[4];
const w = item.width || 0;
const mid = W * 0.5;
if (tx + w < mid) leftCount++;
else if (tx > mid) rightCount++;
else crossCount++;
});
// If there are distinct left/right blocks and very few crossing the middle:
const isTwoColumn = leftCount > 5 && rightCount > 5 && (crossCount / contentItems.length) < 0.15;
If isTwoColumn is true, we partition the items into leftItems and rightItems, sort them separately by their Y-coordinate (top to bottom), and then append the right column to the end of the left column. Suddenly, research papers read perfectly in order!
The Challenge: Table Cells and Missing Pauses
One major issue I hit was data tables. When you have a table row like A-10 15 4.2%, the Web Speech API reads it as a single rushed sentence ("Atenfifteenfourpointtwo...").
Since PDF.js just gives us raw text chunks, I had to calculate the horizontal gap between words. If the gap is unusually large, we inject a hidden comma. The Web Speech API naturally pauses on commas, which creates a perfect, structured reading flow for tables!
// PDF.js item.width can sometimes be inflated by trailing spaces.
// We use an estimated width for safety.
const estimatedWidth = lastStr.trim().length * minHeight * 0.45;
const effectiveWidth = Math.min(lastWidth, estimatedWidth);
const xGap = tx - (lastX + effectiveWidth);
if (xGap > minHeight * 0.8) {
// Inject a comma to force the TTS engine to pause between table cells!
pageText += ", \u2003\u2003 ";
} else if (pageText.length > 0 && !pageText.endsWith("\n") && !pageText.endsWith(" ")) {
pageText += " ";
}
Security & Permissions
Because this is aimed at academics, analysts, and lawyers who read confidential documents, privacy was non-negotiable.
By handling all the parsing locally via PDF.js and pushing the text directly to the native window.speechSynthesis, the extension requires exactly zero external HTTP requests. No API keys, no server costs, and the permissions in manifest.json are kept strictly to local storage and active tab execution.
The Result
Building this taught me a lot about the quirks of PDF structures. If you deal with two-column PDFs or just want an offline bimodal reader (it highlights sentences and words as it speaks), you can check out the result on the Chrome Web Store: Read PDF Aloud: PDF Text to Speech & Voice Reader.
Have you ever tried parsing PDFs in JS? Let me know in the comments if you've found better ways to handle mixed layouts (like tables spanning across columns).
Top comments (0)