Skip to content

Send a teammate a PDF: the gateway extracts its text - #26

Merged
wolfqing merged 1 commit into
mainfrom
feat/attachments-pdf
Nov 10, 2026
Merged

wolfqing merged 1 commit into
mainfrom
feat/attachments-pdf

Conversation

@wolfqing

Copy link
Copy Markdown
Owner

Summary

A teammate can now read a PDF you send it. This is the third piece of #12, after mentions (#19) and the upload API (#24).

read_file cannot read a PDF, so the gateway extracts the text when a PDF is uploaded and puts it beside the document on the teammate's computer: uploads/1a2b3c4d-report.pdf and uploads/1a2b3c4d-report.pdf.txt, one heading per page. The teammate is told where both are.

When there is nothing to read

The upload is accepted either way, and both you and the teammate are told which case it is. The upload's reply carries text:

text Meaning What the teammate reads
ok All of the text is there where the document and its text are
truncated Only the beginning: over 200 pages or 2 million characters the same; the text file's first line says where it stops
none No text in the document, as in a scan "It has no text to read; it is probably a scan."
failed It could not be parsed, or took over 20 seconds "Its text could not be extracted."

Decisions worth a look

  • A new dependency: unpdf 1.4. MIT, no native code, about 2 MB. Its 2.x needs Node 24, and this project still supports 22, so Dependabot is told to skip that major. The official pdfjs-dist is 37 MB and brings an optional native canvas.
  • Extraction runs in a worker thread with limits on memory, time, pages and size. A PDF is a file from someone else, and the parser is large. Whatever goes wrong there ends as failed; the upload still succeeds and the gateway keeps running.
  • The worker is started from a string, with the library's path resolved by the main thread. That gives one code path under tsx, vitest, the built gateway and a packaged desktop app.
  • Look-alike characters are put right. A PDF printed by Chrome on macOS came out with ⽬ (U+2F6C) where the page says 目, and ⻛ (U+2EDB) for 风. They read fine and cannot be searched. Kangxi radicals go through NFKC. The radicals supplement block needs a table, which is generated from Unicode 18.0 EquivalentUnifiedIdeograph.txt. Latin ligatures are spelled out. Nothing else is touched, so full-width punctuation stays.
  • Schema v9 adds pages and text_state to uploads.

Limits of this approach

  • A scan yields no text. There is no OCR.
  • Tables come out as lines of text without their grid.
  • The PDF itself is not shown to the model, also on providers that could take one natively.

Verification

  • Gateway suite: 781 passed, 1 skipped. Web suite: 151 passed. Typecheck and build pass.
  • A real PDF with Chinese and English text and a table, printed by Chrome: extracted through tsx, through the built gateway, and end to end through a running gateway with curl. The teammate's computer received the document byte for byte and the text file.
  • A PDF with no text comes back as none.
  • Not run on Node 22 locally (this machine has 24); CI runs 22.

🤖 Generated with Claude Code

read_file cannot read a PDF, so a PDF sent to a teammate was something it
could not open. The gateway now extracts the text when a PDF is uploaded
and puts it beside the document on the teammate's computer
(uploads/<id8>-report.pdf and .pdf.txt, one heading per page).

- Extraction runs in a worker thread with a memory ceiling, a 20 second
  limit and caps of 200 pages and 2 million characters. A scan (no text),
  a document that cannot be parsed and one that hits a limit are all
  accepted and recorded as such: `text` in the upload's reply is ok,
  truncated, none or failed, and the teammate is told the same in the
  conversation. Pages are not rendered.
- The worker is started from a string with eval, with the library path
  resolved by the main thread, so the same code path runs under tsx,
  vitest, the built gateway and a packaged app.
- Extracted text has CJK radical look-alikes and Latin ligatures replaced
  by the real characters. A PDF printed by Chrome on macOS came out with
  U+2F6C for 目 and U+2EDB for 风, which read fine and cannot be searched.
  The Kangxi block goes through NFKC; the radicals supplement needs a
  table, generated from Unicode 18.0 EquivalentUnifiedIdeograph.txt.
- Schema v9 adds `pages` and `text_state` to uploads. Removing an upload,
  the daily clean-up of unsent uploads and the folder check all cover the
  text file too.
- New dependency: unpdf ^1.4.0 (MIT, no native code, 2 MB). Its 2.x needs
  Node 24 and this project still supports 22, so Dependabot skips that
  major for now.

Verified with the suites, a real PDF with Chinese and English text printed
by Chrome (through tsx, through the built gateway, and through a running
gateway with curl), and a scan-like PDF.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@wolfqing
wolfqing merged commit c8e826f into main Nov 10, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant