
Security News
arXiv Is Rate Limiting Authors Following a Flood of AI Slop Submissions
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.
PDF → Markdown decompiler for the browser and Node: native glyph geometry in milliseconds, render-diff-guided on-device OCR only where the text layer can't explain the ink.
A PDF decompiler, not an image reader. PDF → Markdown in the browser or Node, in milliseconds for born-digital pages — with on-device OCR spent only on the pixels the text layer can't explain.
Try it in your browser → — drop any PDF; it never leaves the tab.
import { decompile } from 'pdffr';
const { markdown, stats } = await decompile(file);
// stats.firstOutputMs ≈ 150ms for a typical report; the file never leaves the tab
npx pdffr report.pdf -o report.md
Every PDF→Markdown tool sits at one of two extremes:
| Approach | Speed | Quality | Problem |
|---|---|---|---|
Text-layer extraction (pdfminer, pdf.js getTextContent) | instant | poor | PDF has no paragraphs, headings, tables or reading order — you get a soup of positioned strings |
| Render + OCR / vision model (Textract, LlamaParse, VLMs) | slow, paid, cloud | high | Rasterizes a page that was already digital, then asks a model to re-read pixels the file could have told it exactly |
~80% of real-world PDFs are born-digital: every glyph's exact coordinates, size and font are already in the file. pdffr treats PDF as what it is — a drawing program — and decompiles the drawing back into structure:
**bold**/*italic*/<sup>, math fonts and sub/superscripts transliterated to $LaTeX$, rotated text re-framed upright, running header/footer stripping, hyphenation repair. No rasterization. Milliseconds per page.pdffr/node runs the identical pipeline on the server or the command line, with @napi-rs/canvas standing in for the DOM.npm install pdffr pdfjs-dist tesseract.js
# Node / CLI additionally:
npm install @napi-rs/canvas
pdfjs-dist and tesseract.js are peer dependencies; @napi-rs/canvas is an optional peer used only by the Node entry.
import { decompile, warmOcr, setPdfWorkerSrc } from 'pdffr';
// Bundled apps: point pdf.js at its worker. Without this, pdffr falls back to the jsdelivr build.
setPdfWorkerSrc(new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href);
// Optional: pre-load OCR workers while the user is still choosing a file.
warmOcr();
const result = await decompile(file, {
ocr: true, // escalate unexplained ink to on-device OCR (default true)
lang: 'eng', // tesseract language(s): 'deu', 'eng+ara', 'chi_sim', …
concurrency: 4, // pages decompiled in parallel
onPage(page, md) {
// streams: first the native pass, then again as OCR regions land
render(page, md);
},
onEvent(e) {
// every trace line, page (re)emit, and stats update
if (e.type === 'trace') console.log(e.kind, e.msg);
},
});
result.markdown; // the whole document
result.pages[0].blocks; // typed blocks: heading | para | math | list | table
result.stats; // firstOutputMs, nativeDoneMs, totalMs, ocrRegions, nativeChars, ...
import { decompileFile, terminateOcr } from 'pdffr/node';
const { markdown } = await decompileFile('invoice.pdf', { lang: 'deu' });
await terminateOcr(); // let the process exit once the tesseract workers are done
pdffr scan.pdf # markdown on stdout, progress on stderr
pdffr scan.pdf -o scan.md --lang eng+fra
pdffr paper.pdf --no-ocr -q # native text only, silent
decompile(input, options?) → Promise<DecompileResult> — input is an ArrayBuffer, Uint8Array, Blob or File. Options: ocr, lang, concurrency, pool, onPage, onEvent, pdfWorkerSrc.decompileFile(path, options?) — Node only.warmOcr(lang?) / terminateOcr() — pre-load or shut down the shared tesseract pool.ocrPool(lang?) — the shared OcrPool; pass your own via options.pool to control worker count.runPipeline(buffer, emit, { ocr, concurrency, escalate }) — the streaming core, if you want raw events.blocksToMarkdown(blocks) — render typed blocks yourself.setPdfWorkerSrc(url) — configure pdf.js's worker.Types: Block, ListItem, Run, Region, Rules, PageState, Stats, PipelineEvent.
| Package | What it is |
|---|---|
pdffr-mcp | MCP server for Claude Desktop / Claude Code / Cursor / any agent: pdf_to_markdown, pdf_outline, pdf_tables |
pdffr-langchain | LangChain.js document loader — one Markdown Document per page |
pdffr-llamaindex | LlamaIndex.TS reader — one Markdown Document per page |
{ "mcpServers": { "pdffr": { "command": "npx", "args": ["-y", "pdffr-mcp"] } } }
getTextContent ─► runs (x, y, w, h, size, bold, italic, math font, rotation)
│
getOperatorList ─► exact bitmap rects + ruling lines (CTM walk), font resolution
│
suspicious? (bitmaps, or thin coverage)
│ no │ yes
▼ ▼
structure pass render page once (print intent)
ink mask − native glyph boxes = residual
regions = bitmap rects ∪ residual components
large regions split along their ink
─► OCR pool (2×/3× upsampling for small crops,
second read of doubtful words,
text-plausibility gate)
─► OCR runs join the same structure pass
Structure pass: rotated runs re-framed upright (a dominant rotation turns the whole page; a minority is a sidebar group) → buildLines (math spans → LaTeX) → orderRuns (XY-cut: tall prose gutter → vertical cut; largest whitespace band → horizontal cut; ruled and aligned tables detected first as atomic boxes) → toBlocks (headings, lists with nesting, paragraphs by leading, display math, tables, furniture stripping) → markdown.
npm install
npm run dev
The playground in demo/ shows each page with the engine's decisions drawn on it — text it read straight from the file, regions it sent to OCR and what came back — beside the decompiled document. It opens on a sample report; drop any PDF onto it. Four canonical samples ship with it: a born-digital report (headings, bold runs, a list, a table, a two-column page, running header and page numbers), a full-page scan of the same report, a mixed document with a scanned insert inside native text, and one page each of a ruled table, a rotated sidebar and equations.
docs/architecture.md — the pipeline, the render-diff oracle, the shared IR, and every heuristic with its threshold and rationale.CONTRIBUTING.md — layout of the code, the one rule for new heuristics, how to add a test.CHANGELOG.mdnpm test # vitest: unit tests + Node end-to-end runs on the sample PDFs
npm run typecheck
npm run format
npm run build # library to dist/, demo to dist-demo/
CI runs typecheck, format check, tests and the build on every push.
Early. It is accurate on the documents it was built against (reports, Word exports with screenshots, scans, two-column layouts, ruled tables, rotated sidebars, simple equations) and will have gaps on others. What it handles today:
$…$ and display $$…$$ LaTeX.Also: letter-spaced headings, label columns (**KSA-UAE tension** — paragraph layouts become headings over their paragraphs), card/lane layouts, fractions drawn with a bar, multi-line display math, paragraphs cut by a page break, bold recovered from OCR stroke weight.
Known limitations:
aligned blocks are not reconstructed.x2 with no italic or math font) is not recognised as math.Bug reports with a PDF attached are the fastest way to improve it.
MIT © Amer Sarhan
FAQs
PDF → Markdown decompiler for the browser and Node: native glyph geometry in milliseconds, render-diff-guided on-device OCR only where the text layer can't explain the ink.
The npm package pdffr receives a total of 14 weekly downloads. As such, pdffr popularity was classified as not popular.
We found that pdffr demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.

Research
/Security News
A new GhostAction wave hits hundreds of GitHub repos, expanding CI/CD secret theft to cloud and AI credentials in source code and git history.

Research
/Security News
Tensorlake npm SDK version 0.5.144 was compromised in a ChainDrop / Shai-Hulud attack, delivering credential-stealing malware.