light-ocr

July 31, 2026 · View on GitHub

Local OCR for Node.js and Agents. Offline, no Python, no network calls. Returns text with coordinates and confidence.

Installing from npm? Start with the @arcships/light-ocr package guide. This document keeps the lower-level image engine, CLI contract, and source-build details.

Install

npm install @arcships/light-ocr

The install includes the PP-OCRv6 Small model and a prebuilt native runtime for your platform. No post-install scripts, no downloads — createEngine() works out of the box.

CLI

The light-ocr command is available after install.

Quick start

# Recognize text in an image (default action)
light-ocr image.png --format json

# Just text, no coordinates
light-ocr image.png --format text

# PDF pages through the renderer bundled by the main package
light-ocr report.pdf --pages 1-10 --format jsonl

# Engine info
light-ocr info --version
light-ocr info --model-info

# Voluntary system/provider diagnostics
light-ocr doctor --json

Subcommands

CommandPurpose
recognize <path> [flags]Full OCR: detection + recognition (default)
detect <path> [flags]Detection only: text region boxes, no recognition
info --model-info | --versionEngine/version diagnostics, no image read
doctor [--json]System/provider diagnostics; never reports a hostname or stable device ID

light-ocr image.png without a subcommand is implicit recognize.

Common flags

FlagValuesDescription
--formatjson | jsonl | textOutput format (default: json)
--regionx,y,w,hRestrict recognition to a pageSpace rectangle
--providerauto | cpu | apple | webgpuExecution provider (default: auto)
--no-exifDisable EXIF orientation correction
--schema-version1Request exact output schema
--quietSuppress non-error stderr

detect does not accept --format (output is always JSON with detections[]). The reserved detect --crop flag currently fails with unsupported_capability; use returned boxes with recognize --region until crop bytes are implemented.

Scenarios

Screenshot with small text:

light-ocr screenshot.png --format json
# Low confidence? Re-run on a specific region:
light-ocr recognize screenshot.png --region 100,80,640,320 --format json

Form/receipt field extraction (detect-then-recognize):

light-ocr detect receipt.png                    # find where text is
light-ocr recognize receipt.png --region 50,200,300,80 --format json  # read that area

stdin / pipe:

cat image.png | light-ocr recognize --stdin --type image/png --format json

Batch via shell:

for f in *.png; do light-ocr recognize "$f" --format jsonl; done

Output schema

{
  "schemaVersion": 1,
  "source": { "kind": "image", "mediaType": "image/png", "identity": {}, "appliedTransforms": {} },
  "pages": [{
    "index": 0,
    "width": 640,
    "height": 480,
    "coordinateSpace": "pageSpace",
    "structure": "ocr-order",
    "lines": [{ "id": "L0", "text": "HELLO", "confidence": 0.99, "box": [{"x":0,"y":0},{"x":100,"y":0},{"x":100,"y":30},{"x":0,"y":30}] }]
  }]
}
  • box is 4 points in pageSpace (top-left origin, x right, y down, post-EXIF pixels)
  • detect replaces lines with detections[] ({ id, score, box }) and sets structure: "detect"
  • --format text: recognized text only, one line per line
  • --format jsonl: one page record per line (for streaming/batch)

Exit codes

CodeMeaning
0Success
64Usage error
65Invalid argument (bad region, unsupported format/schema)
66Invalid image
67Unsupported capability
68Model/bundle error
69Resource limit exceeded
70Environment/package failure
71Inference failure
72Internal error

Node API

const { createEngine } = require('@arcships/light-ocr');

const engine = await createEngine();

const result = await engine.recognizeEncoded(imageBytes);
console.log(result.lines[0].text);        // "HELLO"
console.log(result.lines[0].confidence);   // 0.99
console.log(result.lines[0].box);           // [{x,y}, {x,y}, {x,y}, {x,y}]

await engine.close();

Detect (detection only, no recognition)

const engine = await createEngine();
const result = await engine.detect(imageBytes);
console.log(result.boxes[0].score);  // 0.95
console.log(result.boxes[0].box);    // [{x,y}, {x,y}, {x,y}, {x,y}]
await engine.close();

With options

const engine = await createEngine({
  execution: { provider: 'auto' },
  queueCapacity: 4,
});

const result = await engine.recognizeEncoded(imageBytes, {
  applyExif: true,       // EXIF orientation correction (default: true)
  region: { x: 100, y: 80, width: 640, height: 320 },  // ROI
  signal: controller.signal,
});

console.log(engine.info.execution.sessions.detection.actualProviderChain);
await engine.close();

ESM

import { createEngine } from '@arcships/light-ocr';

TypeScript types are included in the shared runtime index.d.ts.

Capabilities

  • Formats: JPEG and PNG through the image engine; PDF and multi-page image jobs through the main package's document API
  • EXIF orientation: JPEG orientation tags 1–8 automatically corrected
  • ROI: pageSpace axis-aligned rectangle, coordinates offset back to full page
  • Providers: CPU (all platforms), Apple Core ML (macOS arm64), WebGPU (Linux x64, Windows x64)
  • Offline: no network at install or runtime; model bundled in npm package
  • Concurrency: one engine = one worker thread, bounded FIFO queue, AbortSignal support
  • Platforms: macOS arm64/x64, Linux x64/arm64 (glibc), Windows x64/arm64
  • Node.js: 22, 24

Not supported

  • WebP, GIF, TIFF
  • Character-level coordinates (recognition is line-level)
  • Multi-engine fan-out as default architecture
  • Bun (Node-API lifecycle not fully verified)

Acceleration

provider: 'auto' (default) selects the best available backend:

PlatformAuto candidateNotes
macOS arm64Apple Core ML → CPUFP16 detection + recognition via Neural Engine / GPU
macOS x64CPUCore ML parity not met on Intel
Linux x64WebGPU → CPUFP32, bounded CPU partition for Concat/Gather/Slice
Windows x64WebGPU → CPUFP32, D3D12
Linux arm64CPUWebGPU plugin has no arm64 binary
Windows arm64CPUWebGPU not yet qualified

Use light-ocr info --model-info to inspect the actual provider chain, device, and qualification status.

Build from source

See CONTRIBUTING.md for prerequisites and the full build guide.

# Clone and initialize
git clone https://github.com/arcships/light-ocr.git
cd light-ocr

# Build C++ core
cmake -S . -B build-ci -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DLIGHT_OCR_DEPENDENCY_CACHE_DIR="$PWD/.cache/dependencies"
cmake --build build-ci --parallel
ctest --test-dir build-ci --output-on-failure

# Build Node addon
cd bindings/node
NODE_INCLUDE_DIR="$(node -p "require('node:path').resolve(require('node:path').dirname(process.execPath), '../include/node')")"

cmake -S ../.. -B build-node -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DLIGHT_OCR_DEPENDENCY_CACHE_DIR="$PWD/../../.cache/dependencies" \
  -DLIGHT_OCR_BUILD_NODE=ON -DLIGHT_OCR_BUILD_TESTS=ON -DLIGHT_OCR_BUILD_TOOLS=OFF \
  -DLIGHT_OCR_NODE_INCLUDE_DIR="$NODE_INCLUDE_DIR" \
  -DLIGHT_OCR_NODE_EXECUTABLE="$(command -v node)"
cmake --build build-node --target light_ocr_node --parallel

Windows builds additionally require LIGHT_OCR_NODE_LIBRARY pointing to node.lib. The build links a delay-load hook so the addon works under Electron as well as Node.

Run tests:

node --test --test-concurrency=1 bindings/node/test/*.test.cjs

License

Apache-2.0. The bundled PP-OCRv6 model inherits PaddleOCR's license terms.