開發歷程:在瀏覽器裡把 PDF 轉成 Markdown

這是 LocalPapa Notes 開發紀錄系列的第一篇。我們答應過要「公開建造」,所以就從頭說起——LocalPapa 開發紀錄裡記載的第一個工具PDF 轉 Markdown

為什麼先做這個

故事的起點很實際:人們常常需要把 PDF 裡的文字「挖」出來,貼進筆記、貼進 AI 聊天視窗,或轉成可編輯的格式。傳統做法是丟上某個線上轉換網站——但那代表你的檔案被上傳到陌生的伺服器。

對一份合約、一份病歷、一份還沒公開的報告來說,這個代價太高了。

所以我們問自己一個貫穿整個 LocalPapa 的問題:這件事,能不能完全在瀏覽器裡完成?

答案是可以。

核心難題:PDF 根本不知道什麼是「一行字」

這是多數人沒意識到的事實:PDF 裡沒有「段落」或「行」的概念。

一份 PDF 在底層其實是一堆「把這個字元畫在座標 (x, y)」的繪圖指令。對機器來說,它看到的不是文章,而是散落在畫布上、各自帶著座標的文字碎片。直接抽出來,你會得到一團順序混亂、毫無結構的字。

要轉成像樣的 Markdown,真正的工作是從座標反推出視覺結構

我們的解法

我們用 pdf.js(在瀏覽器本地解析 PDF,符合「不上傳」原則)拿到每個文字塊的 X、Y 座標與寬度,然後做三件事:

  1. 以 Y 座標群組化成「行」。 把垂直位置相近的文字碎片歸為同一行——但加上 5px 的容錯值。為什麼需要容錯?因為同一行的文字未必落在完全相同的 Y 座標:上標、不同字級、字型基線差異都會讓它們微微偏移。死板地用「Y 完全相等」來分行,會把一行字拆得七零八落。
  2. 在行內依 X 座標排序。 確保文字按照從左到右的視覺順序排列,而不是 PDF 內部的繪製順序。
  3. 偵測 X 座標間隙還原結構。 這是讓輸出「有結構感」的關鍵:當同一行裡兩個文字塊之間出現較大的水平間隙(>50px),我們就插入幾個空格。這個小小的啟發法,能保留欄位對齊與簡單表格的視覺結構——否則表格會被壓成一團糊。

最後,把整理好的文字組成 Markdown,配上即時預覽一鍵複製,方便直接貼進 AI 聊天視窗。

學到的事

  • 「提取文字」聽起來簡單,其實是還原結構的問題。 難的不是讀出字元,而是猜對它們原本的版面意圖。
  • 啟發法要誠實面對。 5px 的分行容錯、50px 的間隙門檻,都是務實的取捨,不是放諸四海皆準的真理。對乾淨的文字型 PDF 效果很好;對掃描成圖片的 PDF(裡面根本沒有文字層)就無能為力——這時需要的是 OCR,那是另一個故事。
  • 隱私不必是效能或品質的代價。 整個流程都在你的瀏覽器分頁裡跑完,檔案一個位元組都沒離開你的裝置。

一個原則的起點

回頭看,這第一個工具替後來的一切定了調:用聰明的客戶端工程,去做大家以為「一定要上傳到伺服器才能做」的事。

從那之後,同一套理念一路長成了 PDF 工具群、圖片工具、開發者小工具,乃至命理服務——如今已是 39+ 個工具。但它們全都回答著同一個問題,就是當年那一個:

這件事,能不能完全在瀏覽器裡完成?

通常,答案都是可以。

下一篇開發紀錄,我們會挑另一個工具,繼續拆解它背後的取捨。想第一個收到嗎?把 LocalPapa Notes 加入書籤吧。

Dev Log: Turning PDF into Markdown — All in the Browser

This is the first post in the LocalPapa Notes dev-log series. We promised to build in public, so let's start at the very beginning — the first tool recorded in our development log: PDF to Markdown.

Why this one first

The starting point was practical. People constantly need to pull text out of a PDF — to paste into notes, drop into an AI chat window, or convert into something editable. The usual approach is to throw the file at some online converter. But that means uploading your document to a stranger's server.

For a contract, a medical record, or an unpublished report, that's too high a price.

So we asked the question that runs through all of LocalPapa: can this be done entirely in the browser?

It can.

The core problem: a PDF doesn't know what a "line" is

Here's the thing most people don't realise: PDFs have no concept of paragraphs or lines.

Under the hood, a PDF is a pile of drawing instructions: "paint this character at coordinate (x, y)." To a machine it isn't prose — it's text fragments scattered across a canvas, each carrying its own position. Extract them naively and you get a jumbled, structureless blob.

Turning that into decent Markdown means the real work is reconstructing visual structure from coordinates.

How we solved it

We use pdf.js (which parses the PDF locally in the browser, honouring the no-upload rule) to get each text block's X, Y coordinates and width, then do three things:

  1. Group fragments into lines by Y coordinate. Cluster text at similar vertical positions into the same line — but with a 5px tolerance. Why the tolerance? Because text on the same line rarely sits at exactly the same Y: superscripts, mixed font sizes, and baseline differences nudge things slightly. Splitting strictly on "equal Y" would shatter a single line into pieces.
  2. Sort within each line by X coordinate. So text reads left-to-right in visual order, not in the PDF's internal draw order.
  3. Detect X-coordinate gaps to recover structure. This is the key to making the output feel structured: when two blocks on the same line are separated by a large horizontal gap (>50px), we insert spaces. That small heuristic preserves column alignment and simple tables — without it, tables collapse into mush.

Finally we assemble the cleaned text into Markdown, paired with a live preview and one-click copy, ready to paste straight into an AI chat.

What we learned

  • "Extract the text" sounds simple, but it's really a structure-reconstruction problem. The hard part isn't reading the characters — it's guessing their original layout intent.
  • Be honest about heuristics. The 5px line tolerance and the 50px gap threshold are pragmatic trade-offs, not universal truths. They work well on clean, text-based PDFs; they can't help with scanned image-only PDFs that have no text layer at all — that needs OCR, and that's another story.
  • Privacy doesn't have to cost performance or quality. The whole pipeline runs inside your browser tab; not a single byte of the file leaves your device.

The start of a principle

Looking back, this first tool set the tone for everything that followed: use clever client-side engineering to do things people assume "must" require a server.

Since then, that same idea grew into the PDF suite, the image tools, the developer utilities, even the divination services — 39+ tools today. But all of them answer the same question we asked back then:

Can this be done entirely in the browser?

Usually, the answer is yes.

In the next dev log, we'll pick another tool and unpack the trade-offs behind it. Want to be first to read it? Bookmark LocalPapa Notes.

探索 39+ 個隱私優先的瀏覽器工具
全程本地運算、檔案不上傳。
前往 LocalPapa →
Explore 39+ privacy-first browser tools
Everything runs locally — your files never leave your device.
Visit LocalPapa →

想看英文版?點右上角 EN 切換語言。

Prefer Chinese? Tap at the top-right to switch.