| name | mac-ocr |
| description | Run the `mac-ocr` macOS CLI (or its Node.js API) to recognize text in images and PDFs, extract structured document content on macOS 26+, or create a searchable PDF from a scanned PDF or an image with an invisible selectable text layer. Use when asked to OCR, extract or read text from an image or PDF, stream text from a large PDF page by page, extract paragraphs/tables/lists from a document, make a scanned PDF searchable/selectable, or convert an image (PNG, JPEG, HEIC, ...) into a searchable PDF - on-device via Apple's Vision framework. Covers the default OCR action (text/JSON/JSONL output, batching, stdin, URLs, languages, regions, encrypted-PDF passwords), the macOS 26 `document`, `searchable-pdf`, and `languages` subcommands, and the Node API (`ocr`, `ocr.pages`, `ocrDocument`, `ocrDocument.pages`, `createSearchablePdf`, `supportedLanguages`). macOS only; not for editing or replacing a PDF's existing text. |
mac-ocr
mac-ocr recognizes text in images and PDFs, extracts structured documents on macOS 26+, and writes searchable PDFs on macOS via Apple's Vision framework. Three operations:
- OCR (the default action) — recognize text in images and PDFs.
document — extract structured document content on macOS 26+.
searchable-pdf — write a PDF with an invisible, selectable text layer, from a PDF or an image (image → one-page PDF sized from embedded DPI, falling back to 72 DPI).
OCR (default action — no subcommand)
OCR is the default, so a file argument alone runs recognition. mac-ocr ocr <file> also works but isn't required.
mac-ocr photo.png
mac-ocr scan.pdf
mac-ocr a.png b.png c.png
cat screenshot.png | mac-ocr
mac-ocr https://example.com/img.png
mac-ocr shots/*.png -o '[dir]/[name].txt'
mac-ocr --format jsonl scans/*.pdf
Flags
| Flag | Effect |
|---|
-f, --format <text|json|jsonl> | Output format. Default text. |
-o, --output <path> | Write to a file instead of stdout: fixed path, dir/, or a template with [name]/[ext]/[dir]/[page]/[pagecount] (e.g. '[dir]/[name].txt' writes next to each input). Quote templates — […] is a glob in zsh |
--pdf-dpi <auto|72–600> | PDF rasterization DPI. auto (default) sniffs embedded image resolution, clamped to 144–600; vector-only pages use 144. |
--password <pw> | Password for an encrypted PDF (or set MAC_OCR_PDF_PASSWORD) |
--fast | Lower accuracy, faster |
-l, --language <code> | Recognition language (BCP-47, repeatable), e.g. -l en-US -l ja-JP |
-c, --confidence <0–1> | Drop observations below threshold |
-w, --custom-words <word> | Custom vocabulary (repeatable) |
--custom-words-file <path> | Vocabulary file, one word per line |
--no-language-correction | Disable language correction |
--min-text-height <0–1> | Minimum text height relative to image |
--max-candidates <1–10> | Alternative text candidates per observation (default 1; a candidates array appears on observations only when > 1) |
--roi <x,y,w,h> | Restrict to a normalized region (top-left origin) |
Output formats
| Format | Streams? | When to use |
|---|
text (default) | ✓ | You just want text. Multi-result runs get ==> filename <== headers. |
jsonl | ✓ | Prefer for PDFs, batches, and heavy jobs. One JSON object per line; memory-bounded, incremental. |
json | ✗ | Only when a downstream tool needs one parseable array. Buffers everything — avoid for large inputs. |
JSON shape
Each result (one per image, or one per PDF page) is:
{
"source": { "type": "file", "path": "scan.pdf" },
"page": 1,
"pageCount": 3,
"width": 1224,
"height": 1584,
"text": "Full recognized text\nwith newlines",
"observations": [
{
"text": "Full recognized text",
"confidence": 1.0,
"boundingBox": { "x": 0.05, "y": 0.42, "width": 0.37
Bounding boxes are normalized 0–1 with a top-left origin. To get pixels: obs.boundingBox.x * result.width, obs.boundingBox.y * result.height.
Exit codes
| Code | Meaning |
|---|
0 | Success (even if no text found) |
1 | Runtime error (missing file, unreadable image, partial batch failure) |
64 | Invalid flag value |
document (macOS 26+)
document extracts structured document content: a convenience transcript plus paragraphs, tables, lists, line candidates, and normalized geometry. Use JSON when structure matters:
mac-ocr document receipt.jpg --format json
mac-ocr document book.pdf --format jsonl
mac-ocr document invoice.png -l en --max-candidates 3
- Requires macOS 26+. On older systems it reports an unavailable error; use ordinary OCR if structure is not required.
- Accepts
--format, --output, --pdf-dpi, --roi, and --password like OCR.
- Document languages use identifiers reported by the current macOS runtime, for example
-l en, -l zh-TW, or -l ar-SA. en-US is not accepted by the current document recognizer.
- Accepts
-w/--custom-words, --custom-words-file, --no-language-correction, --min-text-height, and --max-candidates.
- Does not accept
--fast or --confidence because the document recognizer cannot preserve those ordinary-OCR contracts.
- Do not concatenate paragraphs, tables, and lists to form text. Use root
text; structural collections are overlapping views of the document.
searchable-pdf
Writes a PDF that looks identical to the source but with selectable, searchable text. By default writes one [name].ocr.pdf per input; pass --merge to combine inputs into one PDF.
mac-ocr searchable-pdf scan.pdf
mac-ocr searchable-pdf photo.jpg
mac-ocr searchable-pdf *.pdf
mac-ocr searchable-pdf scan.pdf -o out/
mac-ocr searchable-pdf scan.pdf -o '[name]-ocr.pdf'
mac-ocr searchable-pdf scan.pdf -o -
mac-ocr searchable-pdf --merge -o lease.pdf page1.jpg page2.jpg
-o <dest>: path, [name] template, directory, or - for stdout. A fixed path or - takes a single input; multiple inputs need a directory or [name] template.
--merge combines file/URL inputs into one searchable PDF in exact argument order. It requires -o <file.pdf> or -o -; directory, template, and stdin inputs are rejected. Merged PDFs are rewritten, so annotations/outlines/metadata are not preserved.
- PDF inputs keep their original pages verbatim in non-merge mode (vector content is not re-rasterized); only an invisible text layer is added, and pages that already have selectable text are left untouched. The page is rasterized internally to feed OCR.
- Fully born-digital PDFs pass through byte-for-byte in non-merge mode (annotations/links/forms/outlines preserved). When any page needs OCR or
--merge is used, the rewrite preserves page content but not annotations, outlines, or metadata.
--ocr-all-pages overrides that skip and OCRs every page — needed for hybrid pages (a scan plus a small digital stamp/page number, which counts as "has text"); existing digital text may then appear twice in copy/search.
- Image inputs become one page, sized from embedded DPI metadata when available. Images without usable DPI metadata fall back to 72 DPI (1px = 1pt).
--image-quality <0–1> controls the visible image layer for image inputs only. OCR still uses the original full-resolution image; PDF inputs are not recompressed.
--image-page-dpi <36–2400> overrides image input page sizing only. OCR still uses the original full-resolution image; PDF inputs are unaffected. --pdf-dpi remains PDF-page rasterization for OCR.
--image-downsample-dpi <36–2400> caps the visible image layer resolution for image inputs only. OCR and page size are unaffected; PDF inputs are not downsampled.
- Advanced diagnostics:
MAC_OCR_DEBUG=1 draws searchable-PDF OCR boxes into file outputs and writes a JSONL sidecar next to each output PDF (file.pdf → file.jsonl). Use it when diagnosing missing/duplicated text: red accepted line boxes, blue word boxes, orange rejected observations; sidecar records recognition.passes.partitioned, , and /. Rejected with ; schema is diagnostic, not a public compatibility contract.
The text layer is word-level: one invisible run per recognized word, positioned from Vision's per-word geometry, so selection rectangles track the printed words. Text is searchable and copyable; width within a word remains approximate.
languages
List the recognition languages supported on this macOS version (one BCP-47 code per line). They apply to both OCR and searchable-pdf.
mac-ocr languages
mac-ocr languages --fast
Node.js API
The package also exposes a typed, promise-based API (import { ocr, ocrDocument, createSearchablePdf, supportedLanguages } from 'mac-ocr') backed by the bundled binary. Inputs are bytes (Buffer/Uint8Array/ArrayBuffer) — read files or fetch URLs in your own code.
const { text, observations } = await ocr(bytes)
for await (const page of ocr.pages(pdfBytes)) { }
const pages = await Array.fromAsync(ocr.pages(pdfBytes))
const document = await ocrDocument(bytes, { languages: ['en'] })
for await (const page of ocrDocument.pages(pdfBytes)) { }
const pdf = await createSearchablePdf(bytes)
const langs = await supportedLanguages()
ocr() and ocrDocument() throw for multi-page PDFs — use their respective .pages() iterators.
- Main-thread calls run one at a time through the shared native service. Avoid an unbounded
Promise.all() burst, and keep input bytes unchanged until each call settles.
- Options mirror the CLI:
fast, languages, confidence, customWords, languageCorrection (default true), minTextHeight, maxCandidates, regionOfInterest ({x,y,width,height} | [x,y,width,height] | "x,y,w,h"), pdfDpi, ocrStrategy, imageQuality, imagePageDpi, and imageDownsampleDpi (searchable PDF only), password, signal (AbortSignal). ocrDocument does not accept fast or confidence.
- Failures throw
MacOcrError with .kind ('usage', 'runtime', 'unavailable', …) and .stderr.
Patterns
Heavy/batch jobs → stream JSONL to disk:
mac-ocr --format jsonl large-docs/*.pdf > results.jsonl
HTTP with auth/POST/cookies — mac-ocr only does simple GET; fetch upstream and pipe:
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/img | mac-ocr -