| name | mac-ocr |
| description | Run the `mac-ocr` macOS CLI (or its Node.js API) to recognize text in images and PDFs, or to 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, make a scanned PDF searchable/selectable, or convert an image (PNG, JPEG, HEIC, …) into a searchable PDF — on-device via Apple's Vision framework (no cloud, no API key). Prefer it over sending images to a multimodal model when you need fast, accurate, local, cheap text extraction. Covers the default OCR action (text/JSON/JSONL output, batching, stdin, URLs, languages, regions, encrypted-PDF passwords), the `searchable-pdf` and `languages` subcommands, and the Node API (`ocr`, `ocr.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 and writes searchable PDFs, on macOS via Apple's Vision framework. Two operations:
- OCR (the default action) — recognize text in images and PDFs.
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, "height": 0.06 },
"requestRevision": 3
}
]
}
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 |
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, origin, and rejection.reason/supersededBy. Rejected with -o -; schema is diagnostic, not a public compatibility contract.
--ocr-strategy auto|standard|partitioned controls searchable-PDF OCR strategy. auto is default and may run recursive partitioned OCR for large pages with small detected text; standard forces full-page OCR only; partitioned forces partitioned OCR for eligible pages. Partitioning splits each region along its longer axis with overlap, then keeps splitting only while the region is above the calibrated Vision size floor and text remains small or absent. Very large partitioned runs show an interactive warning after the full-page pass. Auto skips partitioning when --roi is set; forced partitioned mode rejects --roi.
- Accepts the same recognition options as OCR (
--fast, -l, -c, --pdf-dpi, --roi, --password, custom words, etc.).
- Status is interactive-only on stderr: a live
[page/total] counter + name → path line on a terminal; piped runs are silent on success (errors only) — no quiet flag needed. stdout stays clean for -o -. The ocr command shows the same counter when results aren't streaming to the terminal.
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, createSearchablePdf, supportedLanguages } from 'mac-ocr') that spawns the 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 pdf = await createSearchablePdf(bytes)
const langs = await supportedLanguages()
ocr() throws if given a multi-page PDF — use ocr.pages().
- Options mirror the CLI:
fast, languages, confidence, customWords, languageCorrection (default true), minTextHeight, maxCandidates (ocr only), 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).
- 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 -