File & PDF Tools

Extract PDF Text

Extract an embedded PDF text layer and export it as TXT.

Extract PDF text
Read the embedded text layer; scanned pages are not OCRed.
Local processing

Extract PDF Text

Read an existing PDF text layer and export UTF-8 TXT with optional page separators.

Limits

  • No OCR is performed, so image-only scans usually return no text
  • Complex columns, tables, and reading order may not be reconstructed
  • Password-protected files are not supported

Extract PDF Text reads the embedded text layer of a PDF page by page with pdf.js and writes the runs it finds into a UTF-8 .txt file, entirely in the browser. It suits born-digital documents, such as invoices, reports, and contracts, whose fonts carry a ToUnicode CMap so the glyphs map back to real characters.

Lines are reconstructed, not stored

A PDF content stream holds no lines and no paragraphs: it issues text-showing operators such as Tj and TJ that paint glyph runs at coordinates, and pdf.js rebuilds the structure afterwards. When the gap between two runs falls between 0.102 and 0.6 times the font size, a space is appended inside the current item; above 0.6 the item is closed and a separate whitespace item is emitted, so a wide column gap and an ordinary word gap end up as the same single space. A font change, a new content stream, or a marked-content boundary also closes the item. This tool then appends one space after every run whose hasEOL is false, collapses repeated spaces, strips space before newlines, and trims. Unmerged neighbours therefore always come back with a space between them.

Glyph codes have to be mapped back to Unicode

Operands of Tj are glyph codes, not characters. A composite Type0 font under /Identity-H writes two-byte CIDs, and the only bridge to Unicode is the ToUnicode CMap embedded in the font, whose beginbfchar and beginbfrange blocks pair each code with a UTF-16BE value. With no such CMap, pdf.js falls back to glyph names and built-in encodings, which is why subset fonts from older typesetting tools display perfectly yet extract as mojibake. Two further artifacts follow: ligatures stay single characters, so a printed fi can arrive as U+FB01, and a hyphenation hyphen remains glued inside the broken word. Scanned pages contain no text operators at all and extract as an empty string, unless the file already carries an OCR layer drawn in invisible text render mode 3, which is read like any other text.

Page expression, separators, and the exported file

The page field splits on commas and every token must match ^(\d+)(?:\s*-\s*(\d+))?$, counted from 1 with both ends inclusive. Reversed ranges are legal and extract backwards; repeats are dropped, so 1-3,3 yields three pages; a malformed or out-of-range token aborts the run before the first page is read, which is why a bad range never leaves a half-written file. Once the document loads, the field is prefilled as 1-N, so pressing the button untouched extracts the whole document. Separators are literal text lines such as --- Page 3 ---, and pages are joined by one blank line whether separators are on or off. The download is a text/plain;charset=utf-8 Blob named after the source stem, so report.pdf becomes report.txt.

Worked example

Two pages of an invoice, separators on

Input
invoice.pdf, 2 pages, page field 1-2, separators on. Page 1 draws Invoice #2041, Total due: 128.50 USD, and TypeScript with Type set in Helvetica and Script in Times-Roman flush against it; page 2 draws Thank you.
Output
--- Page 1 ---
Invoice #2041
Total due: 128.50 USD
Type Script

--- Page 2 ---
Thank you

The two halves of TypeScript stay separate items because the font changes between them, and the joiner adds one space after every item without hasEOL, so the txt gains a space the page never shows: full-text search has to use Type Script. Separator lines are ordinary text inside the file and pages sit one blank line apart. The save name comes from the source stem, not from the page range, giving invoice.txt.

Limits and pitfalls

  • No OCR is performed: a page whose content stream carries no text operators extracts to an empty string, so a scan without an existing invisible OCR layer cannot be made searchable here.
  • Files above 100 MB are rejected before parsing, and encrypted or password-protected PDFs fail outright, because the reading path has no password prompt.
  • Repeated page numbers collapse silently, so 1-3,3 returns three pages, and a reversed range such as 5-1 extracts backwards instead of being reported as an error.
  • A font change mid-word inserts a space (Type Script), and a wide column gap shrinks to one space, because both are normalized to a single ASCII space.
  • Separator lines such as --- Page 1 --- are part of the txt and must be stripped before a parser consumes it; the character count in the panel is a UTF-16 length, not a byte count.

Related tools

When it goes wrong

With the separator box unticked, Extract text finishes and nothing appears at all — no result panel, no error, no download button.

CauseThe result panel is mounted behind v-if="textOutput". A scanned page carries no text-showing operators, so getTextContent() yields items with no str, every page normalizes to an empty string, and joining the pages with a blank line leaves textOutput empty. An empty string is falsy, so the panel never renders and the run looks like it did nothing.

FixTick Add page separators and run again: headings such as --- Page 1 --- prove the pages were read, and empty bodies underneath confirm the file has no text layer. This tool has no OCR pass, so send the scan through an OCR step first.

The tab pauses the moment I pick the PDF, long before I press Extract text.

CauseselectFile opens the document straight away: openPdfForReading reads the whole File into a Uint8Array and runs getDocument only to learn numPages and prefill the page field. Pressing Extract text reads the same File and parses it a second time, so one extraction crosses the bytes twice.

FixExtract once and download before touching the dropzone again — selectFile sets textOutput back to an empty string, so re-picking the file discards the text you already had instead of appending to it.

Copy does nothing on our intranet page served over plain http://: no message, no error, the text never reaches the clipboard.

CausecopyText awaits navigator.clipboard.writeText with no try/catch. The Clipboard API is only exposed in a secure context, so over http navigator.clipboard is undefined, the call throws, and the rejection is unhandled, which leaves the error line of the panel empty.

FixUse Download TXT, or select the text inside the result box and press Ctrl/Cmd+C. Anything you edit in that box is what the download writes, because it is bound to textOutput with v-model.

A PDF that was cut off mid-download reports the bare pdf.js sentence The PDF file is empty, i.e. its size is zero bytes, even with the interface set to Chinese.

CausefriendlyFileError translates only a fixed list: the 100 MB gate, the total-size gate, the page-syntax and page-range codes, anything matching /encrypted|password/, and anything matching /failed to parse|no pdf header|invalid.*(?:pdf|file)|(?:pdf|file).*header/. pdf.js raises InvalidPDFException with that zero-byte sentence, which matches none of those patterns, so the raw message is returned verbatim in both locales.

FixCheck the file outside the browser with ls -l budget.pdf and re-download or re-export it, then drop it again; a zero-byte file cannot be repaired here.

I cleared the page field to see what would happen and it extracted the entire 240-page document.

CauseparsePageSelection returns before tokenizing when the trimmed expression is empty, handing back every page index because defaultAll defaults to true and this component passes no options. A blank field means all pages, not none.

FixLeave 1-N in place for the whole file and type an explicit list such as 12-18,44 for a subset; there is no setting that extracts zero pages, you simply do not press the button.

One concrete scenario

A 240-page report where the separators outweigh the decision

The file is 18 MB, so it clears the 104,857,600-byte gate, and the page field arrives prefilled as 1-240. Leaving Add page separators ticked writes one heading per page: --- Page 1 --- costs 15 bytes with its newline, pages 10-99 cost 16, pages 100-240 cost 17, so the headings alone are 3,972 bytes, plus 239 blank-line joiners between pages, so 4,211 bytes of the txt carry no document text at all. The counter above the result box counts UTF-16 code units, so a 386,000-character Chinese body reports 386,000 while the UTF-8 file is 1,158,000 bytes. Turn separators off whenever a script consumes the txt; keep them for a human reader, and multiply the counter by three for CJK byte estimates.

Frequently Asked Questions

Can it extract text from a scanned PDF?
Not directly — scans are images with no text layer, and this tool does not OCR. The test: if you cannot select the text with your mouse, there is no layer. Run a scan through an OCR tool first to produce a searchable PDF, then extract.
Why is the extracted text out of order or jumbled?
PDF stores text in drawing order, not reading order: two-column papers, tables, and artistic layouts come out in object order. The tool reorders by position where possible; for complex layouts, extract per page and tidy manually.
Chinese text extracts as mojibake — why?
Usually the embedded font lacks a ToUnicode mapping (common in exports from legacy typesetting systems): it displays fine but cannot map back to Unicode, and only OCR can recover it. Modern exports from Word or InDesign extract Chinese without trouble.

Content reviewed: