Scan it, read it.#
PARqore Document Intelligence is the paid step up from a plain document scanner: it doesn't just capture the page, it reads it. Ask it to recognize the page and the scan comes back with on-device OCR — full text plus blocks, lines, and words, each with a position, and confidence on the lines and words. It can also save the captured pages as a PDF. It also runs OCR on any image your app already has, no camera needed. Everything runs on-device (Apple Vision / Google ML Kit). Nothing is uploaded.
How this differs from the free scanner#
Free scanner plugins give you the platform's camera UI: a clean, cropped image of the document. That's where they stop. Document Intelligence adds the intelligence: on-device OCR with structured output (blocks, lines, and words, each with a position, and confidence on lines and words), a PDF of the captured pages, OCR on existing images via recognize(), and multi-page scans up to your configured limit. If you just need a picture of a document, the free option is fine. If you need the words inside it, this is the plugin.
Overview#
partek/document-intelligence gives a NativePHP Mobile app two things: a way to present the platform's own document-scanner UI so a user can capture a paper document with automatic edge detection, perspective correction, and cropping, and a way to run on-device OCR — either on the pages that scanner just captured, or on any existing image the app already has.
use PARTek\DocumentIntelligence\DTO\ScanOptions;use PARTek\DocumentIntelligence\Facades\DocumentIntelligence; $scanId = DocumentIntelligence::scan( ScanOptions::make(20)->outputPdf()->recognizeText());
What this is not#
- Not cloud OCR. Every recognition operation — from
scan(..., recognizeText: true)and fromrecognize()— runs entirely on-device via Apple Vision or Google ML Kit. No image or text is uploaded to run OCR, and this plugin has no server component of its own. - Not guaranteed handwriting recognition. The underlying platform OCR engines are built for printed text. Handwriting may or may not recognize usefully depending on the platform, the handwriting, and factors this plugin does not control — don't build a feature that depends on it working.
- Not identity-document verification. This plugin scans and reads text from a document; it does not verify that a document is genuine, unaltered, or belongs to a particular person. Don't use it as the sole check in an identity-verification flow.
- Not a generic receipt/invoice parser.
OcrResultgives you text, structure (blocks/lines/elements), positions, and confidence — it does not know what a "total" or a "line item" is. Extracting structured fields from a receipt or invoice is application code you write against the OCR output, not something this plugin does for you. - Not a guarantee of financial or legal accuracy. OCR output can misread characters, especially on low-quality captures. Don't feed
OcrResulttext directly into anything financial or legal without a human or application-level review step appropriate to the stakes.
Installation#
composer require partek/document-intelligence
Register the plugin:
php artisan native:plugin:register partek/document-intelligence
If you haven't published NativePHP's plugin provider yet:
php artisan vendor:publish --tag=nativephp-plugins-provider
Optionally publish the config file if you want to change storage_root, output_subdirectory, default_retention, max_pages, or max_image_dimension:
php artisan vendor:publish --tag=document-intelligence-config
Rebuild after installing or after a manifest change:
php artisan native:run
iOS requires NSCameraUsageDescription (declared in nativephp.json and applied automatically) since the scanner UI uses the camera. See Platform behavior for the Android permission story.
Usage#
scan() is asynchronous — it returns a scan id, not a result. Read this first.#
DocumentIntelligence::scan(ScanOptions $options): ?string presents the native scanner UI and returns immediately with the new scan's id (or null if the native call wasn't even accepted) — not the scanned pages, not a PDF, not OCR text. Presenting a multi-page scanner is an interactive flow with no fixed duration — the user might capture one page or twenty, take thirty seconds or five minutes, or back out entirely — so there is no synchronous "here's your document" value for scan() to return. The real outcome arrives later as an event (ScanProgress, ScanCompleted, ScanCancelled, or ScanFailed), and once ScanCompleted fires you call result($scanId) — a separate, synchronous call — to fetch the actual DocumentScanResult.
use Native\Mobile\Attributes\On;use PARTek\DocumentIntelligence\DTO\ScanOptions;use PARTek\DocumentIntelligence\Events\ScanCancelled;use PARTek\DocumentIntelligence\Events\ScanCompleted;use PARTek\DocumentIntelligence\Events\ScanFailed;use PARTek\DocumentIntelligence\Facades\DocumentIntelligence; class ScanScreen extends NativeComponent{ public ?string $scanId = null; public function startScan(): void { // $this->scanId here means only "the scanner UI was requested and // accepted" — NOT "the user has finished scanning." null means the // native call itself was rejected (see Troubleshooting). $this->scanId = DocumentIntelligence::scan( ScanOptions::make(20)->outputPdf()->recognizeText() ); } #[On(ScanCompleted::class)] public function onCompleted(string $scanId, int $pageCount): void { // The event does not carry the pages themselves — fetch them. $result = DocumentIntelligence::result($scanId); // $result->pages, $result->pdfPath, $result->pages[0]->ocr, etc. } #[On(ScanCancelled::class)] public function onCancelled(string $scanId): void { $this->scanId = null; // a normal outcome, not an error } #[On(ScanFailed::class)] public function onFailed(string $scanId, string $errorCode, string $errorMessage): void { $this->scanId = null; }}
recognize() is synchronous — it returns the OCR result directly#
Unlike scan(), DocumentIntelligence::recognize(string $path): ?OcrResult has no UI to present. It's a bounded, non-interactive ML operation on a single existing image — the same shape of work as decoding an image or resizing it — so there's nothing indeterminate about its duration, and it returns the OcrResult (or null on failure) directly, with no event round-trip needed. This mirrors NativeMaps::visibleRegion() in the sibling nativephp-native-maps plugin: a synchronous bridge call/response, not an asynchronous UI flow.
use PARTek\DocumentIntelligence\Facades\DocumentIntelligence; $ocr = DocumentIntelligence::recognize('app/uploads/receipt.jpg'); if ($ocr !== null) { echo $ocr->fullText();}
$path is validated by PathGuard before this method does anything else — see Security.
purgeTemporaryFiles() — pure PHP cleanup, no native call#
use PARTek\DocumentIntelligence\Facades\DocumentIntelligence; // Deletes this plugin's own captured pages/PDFs older than the default// retention (config('document-intelligence.default_retention'), '1 day').$deleted = DocumentIntelligence::purgeTemporaryFiles(); // Or an explicit cutoff:$deleted = DocumentIntelligence::purgeTemporaryFiles(now()->subWeek());
This only deletes files this plugin itself wrote under its own output subdirectory (config('document-intelligence.output_subdirectory'), one folder per scan id) — it never touches anything else under storage_root, and it never runs automatically. Call it from your own scheduled task if you want periodic cleanup.
Cancelling a scan#
DocumentIntelligence::cancel($scanId); // bool — accepted, not confirmed; watch for ScanCancelled
Cancelling a scan that's already finished is a no-op success, not an error.
JavaScript (Vue / React / Inertia)#
resources/js/index.js mirrors the PHP API for apps that never touch a PHP request cycle to scan or recognize:
import { scan, result, cancel, recognize, Events } from '../../vendor/partek/document-intelligence/resources/js/index.js'; // Same async-acceptance contract as the PHP facade — the resolved value is// the new scan's id, not the scanned pages.const scanId = await scan({ maxPages: 20, outputPdf: true, recognizeText: true }); // Call after your app's own event bridge tells you ScanCompleted fired.const documentScanResult = await result(scanId); await cancel(scanId); // No UI, no event — resolves directly with the OcrResult wire shape.const ocrResult = await recognize('app/uploads/receipt.jpg');
Each call POSTs to /_native/api/call with an X-CSRF-TOKEN header (read from <meta name="csrf-token">) and throws on an HTTP error or a {status: 'error'} response. Events gives you the fully-qualified PHP class name string for each of the four native-side events, for whatever event-bridging your app already uses — the events themselves are always dispatched in PHP (see Events); this plugin does not fire a separate JS-only event.
Events#
| Event | Fires | Properties |
|---|---|---|
ScanStarted |
Synchronously in PHP, the instant scan() is called — before the native bridge call, so a listener sees every scan that was attempted even if native never responds. |
scanId (string) |
ScanProgress |
The native scanner reports progress during an in-flight scan. | scanId (string), currentPage (int), totalPages (int) |
ScanCompleted |
The scan finished successfully. Does not carry the pages themselves — call result($scanId) to fetch the DocumentScanResult. |
scanId (string), pageCount (int) |
ScanCancelled |
The user dismissed the scanner UI without capturing any pages, or cancelled after capturing some. A normal, first-class outcome, not an error — check result($scanId) for whatever was captured before cancellation, if anything. |
scanId (string) |
ScanFailed |
The native side reported a failure. | scanId (string), errorCode (string), errorMessage (string — always the sanitized message the native layer reported, never a raw platform exception, stack trace, or filesystem path) |
ScanProgress, ScanCompleted, ScanCancelled, and ScanFailed are populated from a native event payload bound by constructor parameter name (the same mechanism nativephp/mobile's NativeComponent::makeEventInstance() uses for every other plugin in this portfolio) — which only works with flat scalar constructor parameters. All four have flat, no-nested-DTO constructors by design. ScanStarted is dispatched directly in PHP from inside scan() itself, so it isn't bound this way, but it happens to also be flat scalars (just scanId).
Methods#
scan(ScanOptions $options): ?string#
Presents the native multi-page document scanner UI. Returns the new scan's id if the native call was accepted, null otherwise. See Usage.
result(string $scanId): ?DocumentScanResult#
Fetches the full result of a scan — call this after ScanCompleted fires. Returns null if the scan id is unknown or its result has expired or been purged.
cancel(string $scanId): bool#
Requests cancellation of an in-flight scan by id. Returns whether the native side accepted the request, not confirmation that it actually stopped — watch for ScanCancelled.
recognize(string $path): ?OcrResult#
Runs OCR on an existing image. $path is validated by PathGuard before anything else happens. Returns the result directly (no events) — null if recognition failed (see your app's logs), since "no text found" and "recognition failed" both plausibly mean "nothing to show the user."
purgeTemporaryFiles(?\DateTimeInterface $olderThan = null): int#
Deletes this plugin's own output files older than $olderThan (default: config('document-intelligence.default_retention')) from its output subdirectory. Pure PHP filesystem cleanup — no native call. Returns the number of files deleted.
Result shapes#
ScanOptions (builder passed to scan())#
ScanOptions::make(20) // maxPages, default 20 ->outputPdf() // bool, default false — also generate a PDF from the captured pages ->recognizeText() // bool, default false — run OCR on each captured page ->imageFormat(ImageFormat::Png) // ImageFormat::Jpeg (default) or ImageFormat::Png ->quality(0.8); // 0.0–1.0 JPEG compression quality; ignored for Png (always lossless)
maxPages has a hardcoded sanity ceiling of 1000 inside the DTO itself (InvalidScanOptionsException above that) — the real, operator-configurable ceiling is config('document-intelligence.max_pages') (default 100), enforced by DocumentIntelligence::scan() itself: a requested maxPages above the configured limit is silently clamped down to it before the native call is made, it does not throw.
Build one ScanOptions per scan() call — it's a fluent value object, not meant to be shared/reused across calls.
DocumentScanResult (returned by result())#
final readonly class DocumentScanResult{ public string $id; public array $pages; // ScannedPage[] public ?string $pdfPath; // null unless ScanOptions::outputPdf() was set public function pageCount(): int;}
DocumentScanResult::fromFixture('invoice') loads the bundled resources/fixtures/invoice.json fixture for deterministic tests — see Testing.
ScannedPage#
final readonly class ScannedPage{ public int $index; public string $imagePath; // always inside config('document-intelligence.storage_root') public int $widthPixels; public int $heightPixels; public int $rotationDegrees; // informational; normally 0 — the scanner already corrected orientation public ?OcrPage $ocr; // only populated when ScanOptions::recognizeText() was set}
$ocr is null on every page unless the ScanOptions passed to scan() had ->recognizeText() enabled.
The OCR hierarchy — OcrResult → OcrPage → OcrBlock → OcrLine → OcrElement#
final readonly class OcrResult{ public array $pages; // OcrPage[] public ?string $recognizedLanguage; public function fullText(): string; // every page's text joined with a blank line between pages} final readonly class OcrPage{ public int $index; public string $text; public array $blocks; // OcrBlock[] public int $widthPixels; public int $heightPixels; public ?string $recognizedLanguage;} final readonly class OcrBlock // a paragraph-like grouping of lines{ public string $text; public array $lines; // OcrLine[] public BoundingBox $boundingBox; public ?float $confidence;} final readonly class OcrLine{ public string $text; public array $elements; // OcrElement[] public BoundingBox $boundingBox; public ?float $confidence;} final readonly class OcrElement // the smallest unit — typically a single word{ public string $text; public BoundingBox $boundingBox; public ?float $confidence;}
Blocks/lines/elements are grouped the way the platform's OCR engine grouped them as documented design intent, not something this plugin infers or re-groups itself — treat the exact grouping boundaries as platform behavior, not a guaranteed cross-platform algorithm.
confidence is nullable at every level (OcrBlock, OcrLine, OcrElement) — never fabricated when the platform doesn't supply one. Per the DTOs' own docblocks, iOS Vision is documented to always supply a confidence value; ML Kit on Android is documented to not supply one for every level. Don't assume confidence is present; always null-check before using it.
A worked example, summarizing the shape of the shipped resources/fixtures/invoice.json fixture (one page, three text blocks — an invoice number, a "Bill To" line, and a total — each with one line and 2–4 word-level elements, all with bounding boxes and confidence scores):
$result = DocumentScanResult::fromFixture('invoice');$page = $result->pages[0]; $page->ocr->text; // "INVOICE #1042\nBill To: Acme Corp\nTotal: $450.00"$page->ocr->blocks[0]->text; // "INVOICE #1042"$page->ocr->blocks[0]->confidence; // 0.98$page->ocr->blocks[0]->lines[0]->elements[0]->text; // "INVOICE"$page->ocr->blocks[0]->boundingBox->x; // 0.1 (normalized, top-left origin)
Bounding box coordinates#
BoundingBox ($x, $y, $width, $height) is always normalized 0.0–1.0, origin top-left — matching both Vision's and ML Kit's own normalized-rect convention — never raw pixels. Values are relative to the already-upright, already-rotation-corrected page image (see ScannedPage::$rotationDegrees, normally 0) — a box is never expressed relative to the original, unrotated capture.
To convert to pixel coordinates, multiply by the containing page's widthPixels/heightPixels:
$pixelX = $boundingBox->x * $ocrPage->widthPixels;$pixelY = $boundingBox->y * $ocrPage->heightPixels;$pixelWidth = $boundingBox->width * $ocrPage->widthPixels;$pixelHeight = $boundingBox->height * $ocrPage->heightPixels;
Platform behavior#
The bridge function names, event names, and DTO shapes below are the fixed contract between this package and the native plugin code (resources/ios/, resources/android/). The core round trip is real-device verified, both platforms (see "Verification status" below) — the caveats in this section are about specific deeper mechanics that verification didn't cover, not the plugin as a whole.
| Behavior | iOS | Android |
|---|---|---|
| Scanner UI | VisionKit (VNDocumentCameraViewController) |
ML Kit Document Scanner |
| OCR engine | Vision | ML Kit Text Recognition |
| Minimum OS version | 18.0 | API 26 |
| Camera permission | NSCameraUsageDescription (declared in nativephp.json, applied automatically) |
Handled internally by the platform's own scanner UI — nativephp.json's Android permissions array is empty as of this writing (confirmed current, not stale). Check nativephp.json in your installed version rather than assuming none is required going forward. |
| Confidence scores | Documented to be supplied at every OCR level | Documented to not be supplied at every level — confidence is nullable for exactly this reason |
| Processing location | On-device only | On-device only |
Verification status: v1.0.1 (2026-09-20) confirmed the core scan/OCR round trip on real hardware, both platforms — a physical Galaxy A54 (scan, ScanCompleted, result(), recognize(), purgeTemporaryFiles()) and a physical iPad (a real six-page scan, ScanCompleted, result() returning a PDF, recognize() returning real text). See CHANGELOG.md.
What's still design intent, not independently verified: exact output fidelity — crop quality, text-recognition accuracy on documents beyond what's been tested, exact block-grouping boundaries, and exactly when a confidence score is or isn't present at each OCR level. Treat those specifics as documented intent until you've checked them against your own real build. The Testing section, by contrast, is fully accurate — it exercises real, passing PHP code today.
Security#
- Paths are validated with
PathGuardbefore native scan/OCR calls; traversal outsidestorage_rootis rejected. - OCR/scan run on-device only — this plugin does not upload document images.
- Error messages returned to the app are sanitized.
Report a vulnerability privately: email [email protected] with impact, repro steps, and plugin/NativePHP versions. Do not open a public GitHub issue. Please allow reasonable time for a fix before disclosure.
Testing#
There are two ways to test code that uses this plugin. DocumentIntelligence::fake() replaces the facade, which suits service classes and plain PHP tests. The test helpers work with NativePHP's component test harness (Native::test()), which suits screens: the screen calls the real facade, the helpers answer the bridge the way the device does, and they deliver the native events.
Testing with the fake#
DocumentIntelligence::fake() swaps the bound manager for FakeDocumentIntelligence, which records every scan()/recognize()/cancel() call instead of crossing the native bridge. Unlike a typical fake, result() and recognize() return the bundled invoice fixture by default (not null) — useful for exercising your UI without any extra setup. PathGuard validation still runs for recognize() — that's plain PHP logic, not a native call — so the fake still catches an unsafe or missing path in a test.
use PARTek\DocumentIntelligence\DTO\DocumentScanResult;use PARTek\DocumentIntelligence\DTO\OcrPage;use PARTek\DocumentIntelligence\DTO\OcrResult;use PARTek\DocumentIntelligence\DTO\ScanOptions;use PARTek\DocumentIntelligence\Facades\DocumentIntelligence; it('scans with the requested options', function () { DocumentIntelligence::fake(); DocumentIntelligence::scan(ScanOptions::make(10)->outputPdf()); DocumentIntelligence::assertScanned(fn (array $params) => $params['options']['max_pages'] === 10);}); it('result() returns the invoice fixture by default', function () { DocumentIntelligence::fake(); $result = DocumentIntelligence::result('any-scan-id'); expect($result->id)->toBe('fixture-invoice');}); it('withScanResult() overrides the default fixture', function () { $fake = DocumentIntelligence::fake(); $fake->withScanResult(DocumentScanResult::fromArray(['id' => 'custom-scan', 'pages' => []])); expect(DocumentIntelligence::result('any-id')->id)->toBe('custom-scan');}); it('withOcrResult() overrides the default OCR result', function () { $fake = DocumentIntelligence::fake(); $fake->withOcrResult(new OcrResult([new OcrPage(0, 'custom text', [], 10, 10)])); expect(DocumentIntelligence::recognize('path.jpg')->fullText())->toBe('custom text');}); it('reports failure when the native side rejects the call', function () { $fake = DocumentIntelligence::fake()->failing(); expect(DocumentIntelligence::scan(ScanOptions::make()))->toBeNull(); $fake->assertScanned(); // still recorded as attempted, even though it "failed"}); it('asserts nothing was scanned', function () { DocumentIntelligence::fake(); DocumentIntelligence::assertNothingScanned();});
| Assertion | Checks |
|---|---|
assertScanned(?callable $callback = null) |
A scan() call happened; the optional callback receives the call's params array (e.g. $params['options']['max_pages']) to narrow the match. |
assertRecognized(?callable $callback = null) |
A recognize() call happened; the optional callback receives the resolved image path. |
assertCancelled(?string $scanId = null) |
A cancel() call happened; pass a scan id to require that specific one. |
assertNothingScanned() |
No scan()/recognize()/cancel() call was issued at all. |
->failing(bool $failing = true) (chainable off fake()) makes every subsequent call report failure, as if native rejected it — useful for testing your app's own failure-handling UI without a real device.
Test helpers#
On NativePHP Mobile 4.0 or later, the plugin registers these helpers on Native::fakeBridge() and on the Native::test() harness whenever the app runs under a test runner (APP_ENV=testing). Production boots never register them, and there is nothing extra to install.
use App\NativeComponents\ScanScreen;use Native\Mobile\Testing\Native; it('shows the captured pages', function () { Native::fakeBridge()->withDocumentScanner(); Native::test(ScanScreen::class) ->tap('Scan') ->assertDocumentScanStarted() ->progressDocumentScan(1, 1) ->completeDocumentScan() ->assertSee('Captured 1 page(s).');});
Script the bridge first. With nothing scripted, the fake bridge answers null and the facade treats that as a rejected call: scan() returns null and cancel() returns false. The bridge assertions would still pass while your screen runs its failure branch.
| Helper | Called on | What it does |
|---|---|---|
withDocumentScanner() |
bridge | scan() and cancel() are accepted. Both answer {accepted: true}, as the native side does. |
withDocumentScanResult(array $result, ?string $scanId = null) |
bridge | result() returns $result, in the {id, pages, pdf_path} shape (for example DocumentScanResult::fromFixture('invoice')->toArray()). With $scanId, the result is for that scan only; without it, every scan id gets it. A result for the exact id wins, and result() returns null for an id with nothing scripted. id always echoes the requested scan id. |
withRecognizedDocumentText(array $result) |
bridge | recognize() returns $result, in the {pages, recognized_language} shape. PathGuard still runs first, so the image must exist inside storage_root. |
assertDocumentScanStarted() |
bridge | scan() reached the bridge. |
assertDocumentScanCancelled(?string $scanId = null) |
bridge | cancel() reached the bridge, for any scan or for $scanId. |
assertDocumentTextRecognized() |
bridge | recognize() reached the bridge. |
completeDocumentScan(array $pages = [], ?string $scanId = null) |
harness | Scripts result() for the scan, then delivers ScanCompleted with the matching pageCount. |
progressDocumentScan(int $currentPage, int $totalPages, ?string $scanId = null) |
harness | Delivers ScanProgress. |
cancelDocumentScan(?string $scanId = null) |
harness | Delivers ScanCancelled, as when the user dismisses the scanner. |
failDocumentScan(string $errorCode = 'scan_failed', string $errorMessage = '', ?string $scanId = null) |
harness | Delivers ScanFailed. |
Bridge helpers work on Native::fakeBridge() or straight off the harness. Called off the harness, they return the harness, so the chain continues.
- Scan ids. The harness helpers use the
scan_idof the lastscan()call unless you pass$scanId, and fail with a message naming the helper if no scan was started. - What
completeDocumentScan()scripts. It builds the result the way the device writes it: page images understorage_path('app/document-intelligence/<scanId>/'), apdf_pathonly if the scan asked foroutputPdf(), and OCR text only if it asked forrecognizeText(). The default page is the bundledinvoicefixture's; pass$pages(wire arrays orScannedPageobjects) to replace it. If you scripted a result withwithDocumentScanResult()and pass no pages, that result is kept. - Error codes. The native side sends
unsupported_device,presentation_failed,processing_failedandwrite_failedon both platforms,already_scanningon iOS, andscan_in_progress,invalid_options,unavailableandno_pages_capturedon Android. The defaultscan_failedis a placeholder, so pass the code your screen handles. - With
DocumentIntelligence::fake(). The fake replaces the facade, so nothing reaches the bridge while it is active: the bridge helpers see no calls, and the harness helpers can't find a scan id. The harness helpers still work if you pass the id, e.g.->completeDocumentScan(scanId: $screen->get('scanId')), andresult()then answers from the fake.
Troubleshooting#
recognize() throws UnsafePathException. The path you gave resolves outside config('document-intelligence.storage_root') (which defaults to the app's whole storage_path() tree) — move the file somewhere inside it, or widen/adjust storage_root in config/document-intelligence.php if that's the intended location.
recognize() throws InvalidImageSourceException. The path exists inside storage_root but the file itself is missing, unreadable, or is a directory rather than a file.
result($scanId) returns null. Either the scan id is unknown to native code, or its result has expired/been purged (e.g. by purgeTemporaryFiles() or by the app being restarted long after the scan). There is no way to recover a purged result — the user needs to scan again.
scan() returns null immediately. The native call was never accepted — check that the plugin is registered (php artisan native:plugin:register partek/document-intelligence) and that you've rebuilt (php artisan native:run) since installing or updating it. This is a PHP-side "the call didn't get through" signal, distinct from ScanFailed, which means it did get through and then failed natively.
The scanner is unsupported on this device. Expect a ScanFailed (or, depending on how the OS surfaces it, a ScanCancelled) rather than a distinct "unsupported" event from this plugin — check errorCode/errorMessage on ScanFailed for whatever detail the native layer provides.
ScanOptions::maxPages()/quality() throws InvalidScanOptionsException immediately. These are PHP-side sanity checks (maxPages 1–1000, quality 0.0–1.0) that run before any native call — they're intentionally stricter than nothing, but looser than config('document-intelligence.max_pages'), which is enforced separately (by clamping, not throwing) inside scan().
Performance & limits#
max_pages(config, default 100) is the real ceiling on how many pages a singlescan()can request —ScanOptions::maxPages()only enforces a much higher sanity ceiling (1000) on its own; the manager clamps down to the configured value.max_image_dimension(config, default 8000px) bounds the pixel dimensionsrecognize()will accept for an existing image source.- Per the product requirements this plugin was built against, the native layer is expected to avoid loading every full-resolution page into memory at once when handling a multi-page scan or a large
recognize()source — this is a design requirement for the native implementation, not something verified here against running code.
Complete example#
See examples/ for a full NativeComponent screen (DocumentScanScreen.php + Blade view) that calls scan(), listens for the four outcome events, fetches the result, and renders scanned pages and OCR text — including a commented-out illustration of how an app would explicitly upload or persist a result, since this plugin never does that itself.
License#
Proprietary commercial. All rights reserved © 2026 PARTek. Unauthorized copying or redistribution is prohibited except under a written license with PARTek. The full license text ships in the LICENSE file included with this package (not linked from a private GitHub URL).