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

react-native-receipt-scanner

v1.0.0

Published

Receipt document scanner for React Native. Wraps ML Kit (Android) and VisionKit + Vision (iOS) with a unified API for scanning, interactive perspective-crop, JPEG compression, EXIF extraction, and on-device OCR.

Readme

react-native-receipt-scanner

Abstract receipt scanning and on-device OCR pipeline

CI npm version npm downloads MIT license React Native New Architecture On-device OCR

Receipt document scanner for React Native. Wraps ML Kit (Android) and VisionKit + Vision (iOS) with a unified API for scanning, interactive perspective-crop, JPEG compression, EXIF extraction, and on-device OCR.

Built exclusively for the React Native New Architecture — no legacy bridge, no base64 blobs.

Why this library?

| | react-native-receipt-scanner | @dariyd/react-native-document-scanner | react-native-document-scanner-plugin | | ------------------------------ | :------------------------------: | :-----------------------------------: | :---------------------------------------: | | New Architecture (TurboModule) | ✅ iOS + Android | ✅ iOS only | ⚠️ JS only — native still uses old bridge | | Camera scan | ✅ | ✅ | ✅ | | Gallery import + crop editor | ✅ Interactive perspective-crop | ✅ | ✅ | | On-device OCR | ✅ Multilingual (BCP 47) | ❌ | ❌ | | EXIF metadata | ✅ | ✅ | ❌ | | GPS EXIF | ✅ opt-in | ✅ opt-in | ❌ | | Result format | file:// URI only | URI or Base64 | URI or Base64 | | Min iOS | 16.0 | 13.0 | 13.0 | | Min Android | API 24 (7.0) | API 21 (5.0) | API 21 (5.0) | | Google Play Services required | ✅ Android only | ✅ Android only | ✅ Android only |

What makes this library different

On-device OCR. Neither competitor extracts text from scanned documents. This library runs the Vision framework (iOS) and ML Kit Text Recognition (Android) on-device — no network call, no external API key. The ocrText field on each image is the plain-text content of that page.

Caller-chosen OCR languages. Pass ordered BCP 47 hints via ocrLanguages (default ["ko-KR", "en-US"]). iOS forwards the ordered list to Vision; Android resolves it to one ML Kit script family — Latin, Korean, Japanese, Chinese, or Devanagari — with Korean bundled and the rest delivered through Play services. getOcrCapabilities() reports what the current device can serve. See Multilingual OCR.

Interactive perspective-crop in gallery mode. When source: "gallery" is used on iOS, VNDetectRectangles automatically locates the document corners. A drag-handle overlay (RNCropEditorViewController) lets the user correct the crop before the image is processed. The result is a perspective-corrected JPEG — not a raw photo.

TurboModule on both platforms. The module calls go through JSI on iOS and the codegen-generated NativeReceiptScannerSpec on Android. There is no asynchronous bridge serialization for the call path.

file:// URIs only. Images are written to the app cache directory and returned as file:// paths. Base64 is never used — it doubles memory usage and is slower to transfer across the bridge.


Requirements

| Platform | Minimum version | Notes | | ------------ | -------------------- | ---------------------------------------------------------------------------- | | iOS | 16.0 | VNDocumentCameraViewController requires iOS 13+; Korean OCR requires iOS 16+ | | Android | API 24 (Android 7.0) | ML Kit Document Scanner requires Google Play Services | | React Native | 0.77+ | New Architecture must be enabled |


Installation

yarn add react-native-receipt-scanner

iOS — CocoaPods

cd ios && pod install

Android

No extra steps. The ML Kit Document Scanner dependency is included automatically via Gradle.

Note: The ML Kit Document Scanner downloads its model from Google Play Services on first use. This requires an active internet connection on first launch and does not work on AOSP devices without Google Play Services (e.g., most Android emulators without Play Store).


Setup

iOS — Info.plist

The camera scanner (source: "camera") requires camera access. The gallery picker (source: "gallery") uses the system photo picker (PHPickerViewController) and does not require a photo library permission. Add the following key to ios/<YourApp>/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Used to scan receipt documents.</string>

Note: includeGpsExif: true does not require NSLocationWhenInUseUsageDescription. The library only copies the GPS dictionary already embedded in the source image's EXIF — there is no CLLocationManager call.

Android — AndroidManifest.xml

No extra entries are required. The ML Kit scanner handles its own camera permission prompt internally, and the gallery flow uses the system photo picker — neither requires a declared storage or media permission.


Usage

Basic camera scan

import { scan } from "react-native-receipt-scanner";

const result = await scan();

if (result.status === "success") {
  for (const image of result.images) {
    console.log(image.uri); // file:///...receipt_123.jpg
    console.log(image.ocrText); // extracted text (when ocr: true)
  }
}

Gallery import with perspective-crop

With source: "gallery", the user selects a photo and is presented with an interactive crop editor. The document corners are detected automatically; the user can adjust them before confirming.

const result = await scan({ source: "gallery" });

Localizing the crop editor

The crop editor shows an instruction plus two buttons: a cancel button (left) and a confirm button (right).

On iOS, add the following keys to each locale's Localizable.strings in your Xcode project:

/* ios/en.lproj/Localizable.strings */
"RNReceiptScanner_cropInstruction" = "Drag the corners to frame the document";
"RNReceiptScanner_cancelButton"    = "Cancel";
"RNReceiptScanner_confirmButton"   = "Use Photo";

/* ios/ko.lproj/Localizable.strings */
"RNReceiptScanner_cropInstruction" = "문서의 네 모서리를 맞춰 주세요";
"RNReceiptScanner_cancelButton"    = "취소";
"RNReceiptScanner_confirmButton"   = "사진 사용";

On Android, override the same resource names in the host app's strings.xml files.

The camera scanner uses the platform scanner UI and is not affected.

Multi-page scan

const result = await scan({ source: "camera", maxPages: 5 });

if (result.status === "success") {
  console.log(`Scanned ${result.images.length} pages`);
}

Long receipts — merging OCR across pages

A receipt too long for one frame is captured across several overlapping pages, or saved as a run of screenshots and picked from the gallery. mergeOcrPages assembles their OCR text into one ordered string and removes the text duplicated where two captures overlap.

const result = await scan({ maxPages: 6, mergeOcrPages: true });

const merged = result.mergedOcr;
if (merged && !merged.isComplete) {
  // A seam that could not be proven; both pages' text is still in merged.text.
  console.log(`unproven seams: ${merged.unmatchedBoundaryIndexes.join(", ")}`);
}

Capture each page so it repeats at least a couple of lines from the end of the previous one — the merge proves a seam from repeated text, so with no overlap there is nothing to match. There is no upper limit: the deepest repeated run wins, so a generous overlap is strictly safer than a thin one. With no provable overlap the boundary is reported and every line is kept.

The page JPEGs are never combined into one tall image. Android caps the processed long edge at 3,072 px and iOS Vision's minimum text height is a fraction of the whole image height, so a taller composite recovers less text, not more. See ADR-008.

Two behaviours worth knowing:

  • An unproven seam emits both pages' lines in full and reports its boundary index — the merge does not drop text to make a result look clean.
  • One case does lose content, and cannot be detected. If a receipt prints the same block of lines twice and the page split falls between the two copies, the text is identical to what a real overlap produces, so the later copy is removed as a duplicate and isComplete still reports true. Overlapping your captures generously makes the real overlap deeper than any repeated block, which is the practical defence.
  • isComplete only covers the pages that came back. Both iOS paths can silently drop a page that fails to process, and the JS layer cannot see that — so it means "everything returned joined up", not "the whole receipt is here". Android fails the whole scan instead, so it does not have this gap.

OCR — extract text from the document

OCR runs on-device and is enabled by default. Set ocr: false to skip it (faster, smaller result):

// With OCR (default)
const result = await scan({ ocr: true });

// Without OCR
const result = await scan({ ocr: false });

Multilingual OCR and capability discovery

Choose native OCR with ordered BCP 47 hints, and inspect the active platform without opening scanner UI:

import { getOcrCapabilities, scan } from "react-native-receipt-scanner";

const capabilities = await getOcrCapabilities();
const result = await scan({
  ocrLanguages: ["es-ES", "en-US"],
  ocrFloor: false,
});

ocrLanguages defaults to ["ko-KR", "en-US"]. These hints select native OCR only. They do not infer a receipt country or parse receipt / invoice fields.

On iOS, capabilities contains the ordered Vision identifiers currently supported by the active request. On Android, it contains script model states ("ready" or "download-required"); querying it never starts a download. The web fallback returns { platform: "web", defaultLanguages: ["ko-KR", "en-US"], supported: false }.

Non-Korean and non-English scripts exposed by a provider are provider-supported but uncalibrated until fixture coverage exists.

EXIF metadata

const result = await scan({ includeExif: true });

if (result.status === "success") {
  const { exif } = result.images[0];
  console.log(exif?.orientation); // always 1 — pixels are pre-rotated; do not re-rotate
  console.log(exif?.dateTimeOriginal); // "2024:12:25 10:30:00"
  console.log(exif?.make); // "Apple"
  console.log(exif?.model); // "iPhone 15 Pro"
  console.log(exif?.software); // iOS camera: OS version ("26.4.2"); editors / generators: tool name; usually empty on Android camera and screenshots
}

Note: Output JPEG pixels are always written orientation-normalized, so exif.orientation is always 1. Callers must not re-rotate.

iOS metadata limitation: includeRawExif controls the returned exif.raw field; it does not sanitize the output JPEG. With includeExif: true, gallery JPEGs preserve the source EXIF and TIFF dictionaries, while GPS copying remains controlled by includeGpsExif. Review or remove file metadata before sharing images when your application requires a strict metadata allowlist.

Note: On the gallery path EXIF is read from the original source image. On the camera path (VNDocumentCameraViewController on iOS, GmsDocumentScanner on Android) the source EXIF is stripped during the scan, so the library synthesizes make / model / dateTimeOriginal from the device — software and gps are absent unless the source carried them.

Cancelled state

When the user dismisses the scanner or picker without scanning, status is "cancelled" and images is an empty array:

const result = await scan();

if (result.status === "cancelled") {
  console.log("User cancelled");
}

Auto-rotate — handling sideways captures

A receipt photo that comes in rotated 90° / 180° / 270° (typically from a gallery import where the user or an editor rotated the image) is detected during OCR and the output JPEG is rotated upright before being returned. width / height reflect the post-rotation dimensions; exif.orientation stays 1 because the rotation is baked into the pixels.

// Default: rotation detected → output is upright
const result = await scan({ source: "gallery" });
console.log(result.images[0].width, result.images[0].height);
// → e.g. 1176 × 3530 (upright) even if the source was 3530 × 1176

// Disable to keep the original pixel orientation
const result = await scan({ source: "gallery", autoRotate: false });

The detector runs only when ocr: true (OCR is the rotation signal). Portrait inputs pay near-zero cost; landscape inputs trigger a few extra fast OCR probes (~300–450 ms total).

Rejected state — non-receipt images

When ocr: true (default), the library applies a conservative OCR floor (12 characters / 2 lines) and treats captures that fail the threshold as rejected. Use this to prompt the user when they shoot a wall, the floor, or a blank page instead of a receipt:

const result = await scan();

if (result.status === "rejected") {
  // Every image failed the OCR floor — likely not a receipt
  alert("영수증 텍스트를 인식하지 못했습니다. 다시 촬영해 주세요.");
  console.log(result.rejectedImages[0]?.ocrQuality);
  // → { textLength: 3, lineCount: 1 }
}

// Tighten or relax:
await scan({ ocrFloor: { minLines: 3 } }); // require store + date + total
await scan({ ocrFloor: false }); // disable the floor entirely

In multi-image mode, partial rejects are returned via rejectedImages alongside status: "success":

const result = await scan({ source: "gallery", maxPages: 5 });
if (result.status === "success") {
  console.log(`accepted: ${result.images.length}`);
  console.log(`rejected: ${result.rejectedImages.length}`);
}

API reference

scan(options?): Promise<ScanReceiptResult>

| Option | Type | Default | Description | | ------------------- | ----------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | source | 'camera' \| 'gallery' | 'camera' | Open document camera or system image picker | | maxPages | number | 1 | Maximum pages per scan session | | quality | number (0.0–1.0) | 0.82 | JPEG compression quality after crop | | includeExif | boolean | true | Attach EXIF metadata to each result image | | includeGpsExif | boolean | false | Include GPS coordinates in EXIF (off by default — privacy risk) | | ocr | boolean | true | Run on-device text recognition and return ocrText | | ocrLanguages | readonly string[] | ["ko-KR", "en-US"] | Ordered BCP 47 hints that select native OCR. Empty arrays or entries reject while OCR is enabled. These hints do not infer a receipt country or parse receipt / invoice fields. | | cropAutoConfirm | boolean | false | iOS gallery only: skip the crop editor when document detection confidence is ≥ 0.85 and apply the detected corners automatically | | ocrFloor | OcrFloor \| false | conservative default (12 chars / 2 lines) | Reject blank / non-text captures (e.g. landscape photo of a wall). Pass false to disable. Only applies when ocr: true. | | autoRotate | boolean | true | Detect 90° / 180° / 270° content rotation via OCR confidence and rotate the output JPEG to upright. Only applies when ocr: true. | | includeRawExif | boolean | false | Include the full raw EXIF / TIFF / GPS dictionary on exif.raw. Off by default to keep IPC payloads small. GPS keys excluded when includeGpsExif: false. | | minimumTextHeight | number (0.0–1.0) | 0 | iOS only: minimum text height as a fraction of image height for Vision OCR (0 = platform default ≈ 1/32). Lower it to recover small receipt line items; Android (ML Kit) has no equivalent and ignores it. Only applies when ocr: true. | | ocrGeometry | boolean | false | Attach per-line text boxes to ocrLines, in output-image pixels. Enable it to draw text-region overlays. Off by default because a long receipt can carry hundreds of lines. Only applies when ocr: true. | | mergeOcrPages | boolean | false | Assemble the pages' OCR text into one ordered string on mergedOcr, removing text duplicated where captures overlap. For a receipt too long for one frame. Requires ocr: true and an integer maxPages >= 2; page JPEGs are never combined. |

Result types

type ScanReceiptResult = {
  status: "success" | "cancelled" | "rejected";
  images: ReceiptImage[]; // always an array — empty when none
  rejectedImages: ReceiptImage[]; // always an array — empty when none
  mergedOcr?: MergedOcrResult; // with mergeOcrPages: true, unless the scan was cancelled
};

type MergedOcrResult = {
  text: string; // pages joined in capture order, proven overlap removed once
  isComplete: boolean; // covers only the pages that came back — see "Long receipts" above
  pageUris: string[]; // native capture order; the index space for both arrays below
  unmatchedBoundaryIndexes: number[]; // i = the seam between pageUris[i] and pageUris[i + 1]
  rejectedPageIndexes: number[]; // no usable text, or rejected by the OCR floor
};

type ReceiptImage = {
  uri: string; // file:// path — never base64
  width: number;
  height: number;
  fileName: string;
  mimeType: "image/jpeg";
  fileSize: number;
  imageOrigin: ImageOrigin; // always present; classifies how the source image was produced
  ocrText?: string; // present when options.ocr === true
  ocrQuality?: OcrQuality; // present when options.ocr === true
  ocrLines?: OcrLine[]; // present when options.ocrGeometry === true
  exif?: ReceiptExif; // present when options.includeExif === true
};

type OcrLine = {
  text: string; // this line's recognized text
  frame: { x: number; y: number; width: number; height: number }; // output-image pixels, top-left origin
  confidence?: number; // 0–1, when the platform reports one
};

type ImageOrigin = "camera" | "screenshot" | "download" | "unknown";

type OcrFloor = {
  minTextLength?: number; // default 12
  minLines?: number; // default 2
  minConfidence?: number; // default 0 — iOS only
};

type OcrQuality = {
  textLength: number; // trimmed character count
  lineCount: number; // non-empty line count
  confidence?: number; // mean recognition confidence (iOS only)
};

type ReceiptExif = {
  // image metadata
  orientation?: number; // always 1 — pixels are written orientation-normalized; raw.Orientation preserves the source value
  colorSpace?: number;
  lightSource?: number;
  exifVersion?: string;

  // device + software
  make?: string;
  model?: string;
  software?: string; // TIFF Software tag — iOS camera writes OS version; Samsung etc. may write firmware ID; editors/generators write their name

  // timestamps
  dateTime?: string;
  dateTimeOriginal?: string;
  dateTimeDigitized?: string;

  // camera settings (numeric, normalized across platforms)
  exposureTime?: number; // seconds
  fNumber?: number;
  iso?: number; // single value, not array
  focalLength?: number; // mm
  flash?: number;
  whiteBalance?: number;
  exposureMode?: number;
  exposureProgram?: number;
  meteringMode?: number;

  // gps (when includeGpsExif: true)
  gps?: {
    latitude: number;
    longitude: number;
    altitude?: number;
    timestamp?: string;
    speed?: number;
    heading?: number;
  };

  // raw passthrough (when includeRawExif: true)
  raw?: Record<string, string | number | Array<string | number>>;
};
// Pull every available EXIF tag (e.g. for migrations from @lodev09/react-native-exify)
const result = await scan({ source: "gallery", includeRawExif: true });
console.log(result.images[0].exif?.raw);
// → { Make: "samsung", Software: "F741NKSS3CZCS", Orientation: "6",
//     DateTimeOriginal: "...", FNumber: "1.8", ISOSpeedRatings: "100", ... }

Error codes

| Code | Trigger | | ---------------------------------------- | ------------------------------------------------------------------------------ | | SCAN_IN_PROGRESS | scan() called while a previous call has not yet resolved | | NO_ACTIVITY | No foreground activity (Android) / presented view controller (iOS) | | NOT_SUPPORTED | VNDocumentCameraViewController not supported on this device (iOS) | | SCANNER_INIT_FAILED | ML Kit scanner failed to initialize (Android) | | SCAN_FAILED | Unexpected activity result code (Android) | | SCAN_RESULT_ERROR | ML Kit scan result could not be parsed (Android) | | PROCESSING_FAILED | Image processing or OCR failed | | CAMERA_FAILED | VNDocumentCameraViewController reported an error (iOS) | | INVALID_OCR_LANGUAGE | OCR is enabled and a hint is empty or not a valid BCP 47 tag | | OCR_LANGUAGE_NOT_SUPPORTED | A requested language or script is not supported by the provider | | OCR_LANGUAGE_COMBINATION_NOT_SUPPORTED | Android receives more than one non-Latin script | | OCR_MODEL_INSTALL_FAILED | Android cannot prepare the selected OCR model before scanner UI | | INVALID_MERGE_OPTION | mergeOcrPages is set with ocr: false, or maxPages is not an integer >= 2 |


Architecture docs


Planned

| Feature | Notes | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | PDF export with searchable text layer | Android: GmsDocumentScannerOptions.RESULT_FORMAT_PDF already produces a multi-page PDF — cost is near-zero. iOS: compose pages with PDFKit and overlay the OCR result as an invisible text layer so the file is copy-pasteable and full-text-searchable in any PDF viewer. New ScanReceiptOptions.outputFormat: "jpeg" \| "pdf" option; result carries a pdfUri alongside images. |


Contributing

License

MIT