All projects ~/laksh/projects/carbon
Side project, Jul – Aug 2026

Carbon

Copying a table out of a PDF loses the table. I got tired of rebuilding them by hand.

View on GitHub ↗
RoleSolo project
Timeline6 weeks
Measured Over7,000+ pages
Stack
PythonPyObjCPyMuPDFQuartzmacOS
pasted without Carbon
pasted with Carbon
91%of copies keep their formatting
< 0.8sper copy, end to end
0characters invented or lost

Why I built it

You select a table in a PDF, hit copy, paste it into Google Docs, and what lands is plain text. Bold is gone, the columns have run together, there are no cells, and a numbered list comes through as a paragraph with digits in it.

Rebuilding it by hand takes longer than the copy saved. I wanted one hotkey that pasted it with the formatting intact. I assumed it would take a weekend.

What it does

It sits in the background on macOS. Select anything inside a PDF, press a hotkey, paste. It arrives in Google Docs, Word or Excel looking like the original: bold, real table cells, column widths, merged cells, tab stops, lists.

How it works

What happens when you copy from a PDF

  1. You select something in a PDF and press the hotkey.

    Carbon runs in the background with no window and catches the hotkey in any app.

    Quartz event tapPyObjC
  2. Carbon works out exactly what you selected.

    It uses where your drag started and ended, matched to the position of each letter on the page.

    PyMuPDF
  3. It rebuilds the layout.

    Lines, paragraphs, columns and table cells are worked out from where each letter sits.

    Python
  4. It checks that no text was lost.

    Every character in the result is compared against the selection.

    Python
  5. It puts the result on the clipboard.

    Each app gets the format it reads: Google Docs gets its JSON format, Word gets its own.

    AppKit pasteboard

You paste as usual. The whole round trip takes under 0.8 seconds.

The layout rules. Glyphs sharing a baseline are a line. A vertical gap bigger than the document's own line spacing is a paragraph break. Whitespace that holds its column down the length of a page is a column edge. A cell is where a column edge and a line boundary intersect.

The hard parts

The obvious approach, and why it fails

My first version took the selected text, searched the page for it, and read the formatting from the match. That gave the wrong result whenever the same words appeared twice on a page, and it could not tell where a selection that started mid-sentence began.

So the app never searches for text. It derives the selection from the drag itself. It is more work up front and it is correct every time.

A PDF does not contain a table

A PDF does not store a table, a paragraph or a list. It stores glyphs at coordinates. The reader infers the rows, columns and paragraphs from where the text sits on the page.

So everything is rebuilt from geometry, and the thresholds have to be relative: a paragraph gap in a tightly spaced paper is smaller than one in a loosely spaced report.

Google Docs ignores the HTML you hand it

My first version put HTML on the clipboard, which is how most apps pass formatting to each other, and Google Docs pasted it as if the formatting were not there. To find out what Docs expects, I copied a table out of a Google Doc and read what had landed on my clipboard. Docs writes the same content three ways: as JSON, as HTML, and as plain text laid out wrong for a table. When you paste back into Docs, it reads the JSON and skips the other two.

After that I copied many different kinds of content out of Docs and compared the JSON each time until I could see how it encodes structure. Carbon now writes that same format, and that is what keeps tab stops, column widths and merged cells intact after a paste. I did the same for Microsoft Word, which has its own private clipboard format.

Verifying that nothing is lost

If it drops a line once in fifty, you have to check its output every time, and then it has saved you nothing.

So every copy runs a character-level integrity check comparing what goes onto the clipboard against what was selected. Over more than 7,000 pages the full original formatting comes back 91% of the time. In the rest, the formatting it could not rebuild falls back to plain text, with no words dropped.

Back to all projects Next: Peg Solitaire →