npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

readany

v0.2.1

Published

Route and read documents into clean Markdown — Word, Excel, PowerPoint, OpenDocument, RTF, EPUB, CSV and text-layer PDF. Names the pages it could not read instead of dropping them silently. Text recognition is not included: it needs a native runtime.

Readme

readany

Route and read documents into clean Markdown. Word, Excel, PowerPoint, OpenDocument, RTF, EPUB, CSV and text-layer PDF — one call, no service, no API key, no account.

This package does not recognise text from pixels. It holds the routing, the readers and the whole geometric half of the OCR pipeline — decode, crop, flatten, orient, deskew — but the detector and the recogniser are Rust traits (Detector, Recognizer) with no implementation here. They need a native neural runtime, which WebAssembly is the wrong shape for. The reference implementation of those traits lives in readany-ocr, which is not published. The same is true of Rasterise, the trait that turns a PDF page into pixels: PDFium is native C++, and readany-pdfium is not published either.

So a page made only of pixels comes back named in unresolved_pages, never silently skipped. That is the point of the package, not a gap in it.

npm install readany
const fs = require('fs');
const { read, inspect, toMarkdown } = require('readany');

console.log(toMarkdown(fs.readFileSync('report.docx')));

In a browser or any ESM bundler, use the web entry point and initialise once:

import init, { read } from 'readany/web';

await init();                     // fetches and instantiates the .wasm
const doc = read(new Uint8Array(await file.arrayBuffer()));

Both entry points share one .wasm binary and expose the same functions. Only the loading differs: Node reads the blob from disk synchronously, the browser fetches it.

It tells you what it could not read

This is the reason it exists. Give most converters a PDF where some pages were typed and some were scanned, and they return the typed pages, exit successfully, and never mention the rest. You cannot tell a whole document from a partial one.

const doc = read(fs.readFileSync('mixed.pdf'));

doc.markdown          // the pages that could be read
doc.complete          // false
doc.unresolved_pages  // [3]
doc.receipt           // "4 page(s): 3 extracted, 0 recognised, 1 unresolved, 0 marked for a person, in <n>ms"

Every page is accounted for, and each one says where its text came from:

doc.pages[0]          // { number: 1, markdown: "...", origin: "text",
                      //   confidence: null, human_regions: [] }
doc.pages[2].origin   // "unresolved" — needed recognition, did not get it

origin is "text" (read from the file's own text layer), "ocr" (recognised from pixels) or "unresolved". In this package it is never "ocr", because nothing here can recognise; the value exists so that code written against this package keeps working unchanged the day the work moves to a service that can.

Ask strict: true to throw instead of returning a partial document.

Look before you spend

Routing is separate from reading, so you can ask what a file will cost before paying for it.

const plan = inspect(bytes);

plan.kind                  // "office" | "pdf" | "image" | "unknown"
plan.summary               // "mixed PDF, 4 pages, 1 need OCR"
plan.needs_ocr             // true
plan.ocr_page_count        // 1
plan.pdf.pages_needing_ocr // [3]
plan.pdf.kind              // "text" | "mixed" | "scanned"

Options

Every call takes an optional second argument. An unrecognised key is an error rather than a shrug, so a mistyped option cannot silently do nothing.

read(bytes, {
  filename: 'export.csv',   // consulted only when the bytes have no signature
  page_markers: true,       // insert <!-- Page N --> between pages
  strict: false,            // throw rather than return a partial document
  limits: { pages: 500 },   // raise one ceiling, keep the rest
  // no_limits: true        // remove all ceilings; cannot be combined with `limits`
});

filename is asked last and never overrides what the bytes said. It matters for one real case: a one-column CSV has no signature and is indistinguishable from prose without a name.

Refusals you can act on

A hostile file is refused before it is read, against documented ceilings. Failures throw a real Error with a kind, so you never have to match on English:

try {
  read(bytes);
} catch (e) {
  e.name        // "ReadError"
  e.kind        // "too_large" | "password_required" | "unsupported" | ...
  e.limit       // "input size"        (kind === "too_large")
  e.allowed     // 52428800
  e.found       // 62914560, or null when the true value was never computed
}

defaultLimits() returns the ceilings in force so you can show a person why a file was refused without hard-coding numbers.

Note found: null. "We stopped counting at the cap" and "it was zero" are different facts, and an archive bomb is caught by the second kind — the bytes are counted as they are produced, never trusted from the header.

Straightening a page without recognising it

prepare runs the half of the OCR pipeline that needs no model. It is useful on its own: a browser can decode, crop, flatten, orient and deskew a photograph locally, then send a corrected grayscale page to whatever does the recognising, instead of sending the original photograph.

const page = prepare(bytes, { profile: 'photograph' });
page.width, page.height   // the straightened page
page.pixels               // Uint8Array, 8-bit grayscale, row-major
page.skew                 // degrees corrected

The profile matters more than it sounds. "photograph" crops the document out of its background by texture; on a bank statement that cuts off the amount column, because right-aligned figures on white read as background. Use "rendered_page" for anything that came out of a PDF and "scanned_page" for anything off a scanner or photocopier.

page.rotation is a guess. The orientation heuristic was measured wrong on every one of five real phone photographs, and the retry that corrects it in the full engine needs a recogniser this package does not have. Trust it for scans, not for photographs.

What it reads

| Format | Extensions | |---|---| | Word | .doc .docx .docm | | PowerPoint | .ppt .pps .pot .pptx .pptm .ppsx .ppsm | | Excel | .xls .xlsx .xlsm .xlsb | | OpenDocument | .odt .ods .odp | | Rich Text | .rtf | | EPUB | .epub | | CSV | .csv | | PDF | .pdf, where a text layer exists |

The format is read from the file's own bytes, so a mislabelled file still works.

Images decode from JPEG, PNG, TIFF, BMP and WebP for prepare. HEIC does not decode: the only usable decoder is LGPL, and static linking it is a real licence question. iOS and Android decode HEIC in the platform already.

What it does not do

Recognise text from pixels. A scan, a photograph or a fax is reported in unresolved_pages. See the first paragraph for why.

Rasterise a PDF page. A scanned PDF page cannot even be handed to a recogniser from here, because turning it into pixels needs a renderer with heavy native dependencies.

Read handwriting — a decided never, in every build. No small model does it on a CPU, and every honest engine says so.

Notice handwriting. This is worth separating from the line above, because the full engine does do it and this package cannot. Marking a region as handwritten is a judgement made from ink — stroke width, its variation, how far the baseline wanders — measured on a crop that only a recogniser's detection pass produces. With no recogniser there are no crops, so human_pages is always empty here and [handwritten] can never appear in the markdown. Do not read an empty human_pages from this package as "nothing was written on this page by hand." It means nobody looked.

How it works

WebAssembly, compiled from Rust. There is no server, no account and no per-page fee, and your documents never leave the machine they are on — which is a plain consequence of there being nothing to send them to, not a policy that could change.

Built on anydoc and pdf-inspector, both MIT.

The full engine, its measurements and the reasoning behind them: github.com/AllanTumu/readany.

License

MIT.

The .wasm statically links about a hundred Rust crates, all under permissive licences — MIT, Apache-2.0, BSD, Zlib, Unlicense — every one of which requires its notice to travel with a binary. They are in THIRD-PARTY-LICENSES.md, shipped in this package and regenerated from the dependency tree at every build.