@andresxid/xid_camera_ocr_sdk_react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,459 @@
1
+ import * as react from 'react';
2
+
3
+ /** Default `opencv.js` URL (the bundled copy shipped with the package). */
4
+ declare function defaultOpencvUrl(): string;
5
+
6
+ /**
7
+ * Public SDK types.
8
+ */
9
+ /**
10
+ * Shape hint produced by the detector. `INE` = ID-1 card shape (INE, licence,
11
+ * cédula…), `PASSPORT_MX` = TD3 passport shape of any country, `OTHER` = another
12
+ * quadrilateral. The hint is never a nationality or document-type claim; the
13
+ * backend decides the real type.
14
+ */
15
+ type DocumentType = 'INE' | 'PASSPORT_MX' | 'OTHER';
16
+ interface CornerPoint {
17
+ x: number;
18
+ y: number;
19
+ }
20
+ /** User-facing guidance for the current frame. */
21
+ type FrameHint = 'no_document' | 'too_small' | 'too_dark' | 'glare' | 'blurry' | 'hold_still' | 'ready'
22
+ /** OpenCV failed to load or the worker crashed: auto-capture is off, `captureNow()` gives the unwarped frame. */
23
+ | 'detector_unavailable';
24
+ interface XidCameraOcrSessionOptions {
25
+ /** `<video>` element that renders the camera stream. */
26
+ videoElement: HTMLVideoElement;
27
+ /**
28
+ * Shape hints the host is interested in. Detection and capture are NOT
29
+ * filtered by this list (any quadrilateral in [1.25, 1.75] aspect is
30
+ * capturable); wrappers use it to size the fallback guide rectangle.
31
+ */
32
+ documentTypes?: DocumentType[];
33
+ /** When `true`, captures automatically once a stable, good-quality quad is seen. */
34
+ autoCapture?: boolean;
35
+ /** Consecutive stable frames required before auto-capture. Default 10 (≈ 1 s at 10 fps). */
36
+ stableFramesRequired?: number;
37
+ /** JPEG quality of the captured image (0–1). Default 0.92. */
38
+ captureQuality?: number;
39
+ /** Long side of the rectified capture: `standard` = 1024 px, `high` = 1600 px. Default `standard`. */
40
+ captureResolution?: 'standard' | 'high';
41
+ /**
42
+ * URL of the OpenCV.js asset the worker loads lazily. Defaults to the
43
+ * `opencv.js` file shipped next to the SDK bundle.
44
+ */
45
+ opencvUrl?: string;
46
+ /**
47
+ * URL of a self-hosted copy of `dist/cv-worker.js` (spawned as a module worker).
48
+ * Defaults to the worker shipped with the package. Needed, together with
49
+ * `opencvUrl`, for the CJS build and non-Vite bundlers.
50
+ */
51
+ workerUrl?: string;
52
+ /** Implemented by the host: receives the Blob, uploads it, returns the backend JSON. */
53
+ onUpload: (blob: Blob, detectedType: DocumentType | null) => Promise<ExtractResult>;
54
+ /** Optional UI feedback events. */
55
+ onFrameAnalyzed?: (state: FrameState) => void;
56
+ onCaptured?: (blob: Blob, detectedType: DocumentType | null) => void;
57
+ onResult?: (result: ExtractResult) => void;
58
+ onError?: (err: Error) => void;
59
+ }
60
+ interface FrameState {
61
+ detected: boolean;
62
+ documentType: DocumentType | null;
63
+ confidence: number;
64
+ aspectRatio: number | null;
65
+ quality: FrameQuality | null;
66
+ /**
67
+ * Document corners (TL, TR, BR, BL) in analysis-frame pixels. Present only
68
+ * when a quad was detected. Scale by `frameSize` to draw an overlay.
69
+ */
70
+ corners: [CornerPoint, CornerPoint, CornerPoint, CornerPoint] | null;
71
+ /** Size of the analysis frame (NOT the native video size). */
72
+ frameSize: {
73
+ width: number;
74
+ height: number;
75
+ } | null;
76
+ /** Guidance for the user, derived from quality + stability. */
77
+ hint?: FrameHint;
78
+ /** `true` while the camera torch has been enabled by the SDK. Absent when the track has no torch. */
79
+ torch?: boolean;
80
+ }
81
+ interface FrameQuality {
82
+ brightness: number;
83
+ blurScore: number;
84
+ glareRatio: number;
85
+ docFillRatio: number;
86
+ ok: boolean;
87
+ }
88
+ interface ExtractResult {
89
+ [key: string]: unknown;
90
+ }
91
+ interface SessionState {
92
+ status: 'idle' | 'starting' | 'loading_detector' | 'streaming' | 'capturing' | 'uploading' | 'done' | 'stopped' | 'error';
93
+ lastFrame: FrameState | null;
94
+ lastResult: ExtractResult | null;
95
+ lastError: Error | null;
96
+ }
97
+ type SessionStatus = SessionState['status'];
98
+
99
+ /**
100
+ * Pure geometry helpers (no OpenCV, no DOM). Unit-tested in Vitest.
101
+ * Corner order everywhere is TL, TR, BR, BL.
102
+ */
103
+
104
+ interface Point {
105
+ x: number;
106
+ y: number;
107
+ }
108
+ type Quad = [Point, Point, Point, Point];
109
+ declare const ID1_RATIO: number;
110
+ declare const TD3_RATIO: number;
111
+ /**
112
+ * Orders 4 points as TL, TR, BR, BL (y-down). Sorts by angle around the centroid
113
+ * (clockwise on screen), then rotates the cycle so TL is the point with the
114
+ * smallest x + y (ties: smallest y). Robust to a card rotated exactly 45°.
115
+ */
116
+ declare function orderCorners(points: Point[]): Quad;
117
+ /** Aspect ratio of the rectified quad: mean(long sides) / mean(short sides), ≥ 1. */
118
+ declare function rectifiedAspect(quad: Quad): number;
119
+ /** Nearest nominal ratio within ±5 %, else OTHER. Bands: ID-1 1.507–1.665, TD3 1.349–1.491. */
120
+ declare function typeHintFromAspect(aspect: number): DocumentType;
121
+ declare function isCapturableAspect(aspect: number): boolean;
122
+
123
+ /** Messages exchanged between `cv-worker-client.ts` (main thread) and `cv-worker.ts`. */
124
+
125
+ interface ImageDataLike {
126
+ data: Uint8ClampedArray;
127
+ width: number;
128
+ height: number;
129
+ }
130
+ interface CvDetectionResult {
131
+ detected: boolean;
132
+ documentType: DocumentType | null;
133
+ aspectRatio: number | null;
134
+ corners: Quad | null;
135
+ boundingBox: {
136
+ x: number;
137
+ y: number;
138
+ width: number;
139
+ height: number;
140
+ } | null;
141
+ /** Quality measured inside the quad; null when nothing was detected. */
142
+ quality: FrameQuality | null;
143
+ /** Whole-frame mean brightness 0–1 (for the torch decision when no quad). */
144
+ frameBrightness: number;
145
+ score: number | null;
146
+ /**
147
+ * How the winning quad was found: `direct` (a native 4-vertex contour), `reduced`
148
+ * (a 5–6-vertex hull collapsed to a quad) or `lowres` (the half-resolution fallback
149
+ * pass; wins over `reduced`). Non-direct winners are less precise and carry a
150
+ * discounted `score`. Undefined when nothing was detected.
151
+ */
152
+ provenance?: 'direct' | 'reduced' | 'lowres';
153
+ }
154
+
155
+ /**
156
+ * CVWorkerClient — async wrapper over the OpenCV Web Worker.
157
+ * One module worker; request/response by incremental id. No skip-if-busy:
158
+ * the session guarantees a single frame in flight (spec §5.1).
159
+ *
160
+ * Pixel buffers are copied before being transferred, so the caller's frame
161
+ * is never detached and the client never touches a transferred buffer again.
162
+ */
163
+
164
+ interface CVWorkerClientOptions {
165
+ /**
166
+ * Main-thread deadline for `init()`: rejects even if the worker thread hangs
167
+ * and never reports `init-error`. Default 30 s.
168
+ */
169
+ initTimeoutMs?: number;
170
+ }
171
+
172
+ declare class CVWorkerClient {
173
+ private readonly initTimeoutMs;
174
+ private worker;
175
+ private readyPromise;
176
+ private rejectInit;
177
+ private initTimer;
178
+ private ready;
179
+ private pending;
180
+ private nextId;
181
+ constructor(options?: CVWorkerClientOptions);
182
+ /**
183
+ * Spawns the worker (`workerUrl`, default: the packaged `cv-worker.js`) and loads
184
+ * OpenCV from `opencvUrl`. While a load is pending or after it succeeded, returns
185
+ * the same promise (different urls are ignored).
186
+ * After any failure a later call retries with a fresh Worker.
187
+ */
188
+ init(opencvUrl?: string, workerUrl?: string): Promise<void>;
189
+ isReady(): boolean;
190
+ /** `opts.prior`: the previous frame's corners, used by the detector to rank candidates. */
191
+ analyze(img: ImageDataLike, opts?: {
192
+ prior?: Quad;
193
+ }): Promise<CvDetectionResult>;
194
+ warp(img: ImageDataLike, corners: Quad, docType: DocumentType | null, longSide?: number): Promise<ImageData>;
195
+ /**
196
+ * Stops the worker and rejects every pending request. Idempotent. A pending
197
+ * `init()` is left unsettled (callers that stop mid-load are not told it failed).
198
+ */
199
+ terminate(): void;
200
+ /** Failure path: shuts down, then rejects a pending `init()` with the same error. */
201
+ private fail;
202
+ private clearInitTimer;
203
+ private shutdown;
204
+ private request;
205
+ }
206
+
207
+ type WorkerClient = Pick<CVWorkerClient, 'init' | 'isReady' | 'analyze' | 'warp' | 'terminate'>;
208
+ /**
209
+ * @internal Test seam (second constructor argument). Not part of the public,
210
+ * documented API and may change without notice.
211
+ */
212
+ interface XidCameraOcrSessionDeps {
213
+ createWorkerClient(opts: {
214
+ initTimeoutMs: number;
215
+ }): WorkerClient;
216
+ attachCamera(video: HTMLVideoElement): Promise<MediaStream>;
217
+ detachCamera(video: HTMLVideoElement, stream: MediaStream | null): void;
218
+ sampleFrame(video: HTMLVideoElement, targetWidth: number): ImageDataLike | null;
219
+ grabFrame(video: HTMLVideoElement): ImageDataLike | null;
220
+ encodeJpeg(img: ImageData, quality: number): Promise<Blob>;
221
+ snapshotToBlob(video: HTMLVideoElement, quality: number): Promise<Blob>;
222
+ supportsTorch(track: MediaStreamTrack): boolean;
223
+ setTorch(track: MediaStreamTrack, on: boolean): Promise<boolean>;
224
+ now(): number;
225
+ }
226
+ declare class XidCameraOcrSession {
227
+ private readonly opts;
228
+ private readonly deps;
229
+ private stream;
230
+ private track;
231
+ private cv;
232
+ /** Bumped by start()/stop(); any async continuation from an older generation is dropped. */
233
+ private generation;
234
+ private timer;
235
+ private frameCallback;
236
+ /** The in-flight analyze request, so a manual capture never overlaps it. */
237
+ private inFlight;
238
+ /** 0 = not armed yet (armed on the first analysed frame and after each capture). */
239
+ private warmupUntil;
240
+ private capturing;
241
+ /** Set once a capture has been taken; the auto-shutter stays off until the next start(). */
242
+ private autoShutterSpent;
243
+ private torchSupported;
244
+ private torchOn;
245
+ private torchRejected;
246
+ /** True when any frame of the current stable run came from a weak (`reduced`/`lowres`) winner. */
247
+ private weakRun;
248
+ /**
249
+ * Temporal prior for the detector: the previous analysed frame's corners and the size
250
+ * of that frame. Cleared on a frame without detection, a frame-size change, start(),
251
+ * stop() and after a capture.
252
+ */
253
+ private prior;
254
+ private readonly stability;
255
+ state: SessionState;
256
+ constructor(options: XidCameraOcrSessionOptions, deps?: Partial<XidCameraOcrSessionDeps>);
257
+ start(): Promise<void>;
258
+ stop(): void;
259
+ captureNow(): Promise<void>;
260
+ getState(): SessionState;
261
+ /** Waits `delayMs`, then for the next video frame when the browser exposes rVFC. */
262
+ private scheduleNext;
263
+ private tick;
264
+ private analyzeFrame;
265
+ /** Non-fatal detector failure: drop the worker, stop the loop, keep the camera and manual capture. */
266
+ private detectorUnavailable;
267
+ private updateTorch;
268
+ /** Reprojects a sample-frame quad onto the full video frame; null when degenerate. */
269
+ private fullFrameQuad;
270
+ /** `fullQuad` is already in full-frame pixels; null means "unwarped frame". */
271
+ private doCapture;
272
+ private captureBlob;
273
+ /** Clears every timer and frame callback, turns the torch off, closes the camera and drops the worker. */
274
+ private release;
275
+ private setStatus;
276
+ private emitError;
277
+ }
278
+
279
+ /**
280
+ * Document detection with OpenCV (worker side). Mat work only — geometry and
281
+ * quality are pure modules. Every Mat is `.delete()`d in `finally`.
282
+ * Pipeline (spec §5.2): grey → CLAHE → bilateral → auto-Canny → dilate →
283
+ * findContours(RETR_LIST) → convex hull → approxPolyDP (ε = 2 % perimeter) → 4 convex
284
+ * vertices, ≤ 1 corner on the frame border, angles ∈ [70°, 110°] → score
285
+ * 0.5·area + 0.3·rectangularity + 0.2·aspect → veto a winner nested in a border-cut
286
+ * outline → quality on the winner inset by the dilation band.
287
+ *
288
+ * RETR_LIST, not RETR_EXTERNAL: the zero padding draws an edge around the whole
289
+ * frame, and RETR_EXTERNAL would return only that outer contour (area > 95 %,
290
+ * rejected) and hide the document inside it.
291
+ */
292
+
293
+ interface DetectOptions {
294
+ /** Previous frame's corners (TL, TR, BR, BL) in analysed-frame pixels; ranking only. */
295
+ prior?: Quad;
296
+ }
297
+ declare function detectDocumentCV(cv: any, imageData: ImageDataLike, _acceptedTypes?: DocumentType[], opts?: DetectOptions): CvDetectionResult;
298
+
299
+ /**
300
+ * Perspective warp with OpenCV: rectifies the document to its canonical rectangle.
301
+ *
302
+ * - warpDocumentImageData: ImageData in / ImageData out, worker-safe (no DOM).
303
+ * - warpDocument / warpedSnapshotToBlob: canvas based, main thread only.
304
+ *
305
+ * Corners are in the full-resolution camera frame (not the analysis sample); OpenCV
306
+ * computes the 3x3 homography and warpPerspective returns the document head-on.
307
+ */
308
+
309
+ /**
310
+ * Pure warp: ImageData → ImageData, no DOM dependency (runs inside the Web Worker).
311
+ * INE / PASSPORT_MX warp to their nominal ratio; OTHER / null warp to the measured
312
+ * rectified aspect, clamped to the capturable range so a degenerate quad cannot yield
313
+ * a 0 px or absurd output.
314
+ */
315
+ declare function warpDocumentImageData(cv: any, source: ImageDataLike, corners: Quad, docType: DocumentType | null, longSide?: number): ImageData;
316
+ /** Canvas-based warp (main thread only): canvas → warpDocumentImageData → new canvas. */
317
+ declare function warpDocument(cv: any, sourceCanvas: HTMLCanvasElement, corners: Quad, docType: DocumentType | null, longSide?: number): HTMLCanvasElement;
318
+ /** Rectified snapshot as a JPEG Blob; without corners, a plain snapshot of the canvas. */
319
+ declare function warpedSnapshotToBlob(cv: any, sourceCanvas: HTMLCanvasElement, corners: Quad | null, docType: DocumentType | null, quality?: number, longSide?: number): Promise<Blob>;
320
+
321
+ /**
322
+ * Frame quality measured INSIDE the detected quad (spec §5.4). Pure TS over a
323
+ * grayscale buffer so it runs in the worker right after detection (one round
324
+ * trip) and in Vitest without OpenCV.
325
+ */
326
+
327
+ interface GrayFrame {
328
+ data: Uint8Array | Uint8ClampedArray;
329
+ width: number;
330
+ height: number;
331
+ }
332
+ declare const QUALITY_THRESHOLDS: {
333
+ readonly brightnessMin: 0.18;
334
+ readonly brightnessMax: 0.9;
335
+ readonly blurMin: 60;
336
+ readonly glareMax: 0.04;
337
+ readonly fillMin: 0.25;
338
+ readonly glarePixel: 248;
339
+ };
340
+ declare function measureQuality(gray: GrayFrame, quad: Quad): FrameQuality;
341
+
342
+ /**
343
+ * Camera helpers — getUserMedia + video element wiring.
344
+ */
345
+ declare function attachCamera(videoElement: HTMLVideoElement, constraints?: MediaStreamConstraints): Promise<MediaStream>;
346
+ declare function detachCamera(videoElement: HTMLVideoElement, stream: MediaStream | null): void;
347
+ /** Captura un snapshot del video y devuelve un Blob JPEG. */
348
+ declare function snapshotToBlob(videoElement: HTMLVideoElement, quality?: number): Promise<Blob>;
349
+ /** Downscaled RGBA frame for analysis (default 640 px wide). Reuses one canvas. */
350
+ declare function sampleFrame(videoElement: HTMLVideoElement, targetWidth?: number): ImageData | null;
351
+ declare function supportsTorch(track: MediaStreamTrack): boolean;
352
+ /** Best-effort torch toggle. Resolves `true` when the constraint was accepted. */
353
+ declare function setTorch(track: MediaStreamTrack, on: boolean): Promise<boolean>;
354
+
355
+ declare function decideHint(a: {
356
+ detected: boolean;
357
+ quality: FrameQuality | null;
358
+ stableFrames: number;
359
+ stableFramesRequired: number;
360
+ }): FrameHint;
361
+ /** Every user-visible text of the component. `retake`, `capture`, `tip_*` and `tips_dismiss` are UI copy that `copyFor` never returns. */
362
+ type HintCopyKey = FrameHint | 'loading_detector' | 'capturing' | 'uploading' | 'done' | 'error' | 'retake' | 'idle' | 'starting' | 'capture' | 'tip_light' | 'tip_glare' | 'tip_focus' | 'tips_dismiss';
363
+ type HintCopy = Record<HintCopyKey, string>;
364
+ /** Default texts (es-MX). Product copy: do not reword. */
365
+ declare const DEFAULT_TEXTS_ES: Readonly<HintCopy>;
366
+ /** English texts (the 0.4.0 copy plus the new keys). */
367
+ declare const DEFAULT_TEXTS_EN: Readonly<HintCopy>;
368
+ /** Full text set: es-MX defaults with the given overrides applied per key. */
369
+ declare function resolveTexts(overrides?: Partial<HintCopy>): HintCopy;
370
+ declare function copyFor(status: SessionStatus, hint: FrameHint | undefined, overrides?: Partial<HintCopy>): string;
371
+
372
+ /**
373
+ * Stability tracker (pure). A quad is stable when, frame after frame, every
374
+ * corner moves ≤ 1.5 % of the frame diagonal and the type hint is unchanged.
375
+ * Any frame without a detection resets the count (spec §5.3).
376
+ *
377
+ * The type hint has hysteresis: camera jitter can flick the measured aspect
378
+ * across a band edge (INE ↔ OTHER), so a different hint is only a candidate
379
+ * until it persists `hintSwitchFrames` consecutive frames. While pending,
380
+ * geometry keeps counting and the committed hint is reported; on commit the
381
+ * count restarts at 1 (the document really changed).
382
+ */
383
+
384
+ interface StabilityInput {
385
+ quad: Quad;
386
+ typeHint: DocumentType;
387
+ }
388
+ interface StabilitySnapshot {
389
+ stableFrames: number;
390
+ isStable: boolean;
391
+ quad: Quad | null;
392
+ typeHint: DocumentType | null;
393
+ }
394
+ declare class StabilityTracker {
395
+ private readonly required;
396
+ private readonly maxRatio;
397
+ private readonly switchFrames;
398
+ private current;
399
+ private candidate;
400
+ constructor(opts?: {
401
+ stableFramesRequired?: number;
402
+ maxDisplacementRatio?: number;
403
+ hintSwitchFrames?: number;
404
+ });
405
+ get snapshot(): StabilitySnapshot;
406
+ reset(): void;
407
+ update(input: StabilityInput | null, frameSize: {
408
+ width: number;
409
+ height: number;
410
+ }): StabilitySnapshot;
411
+ }
412
+
413
+ /**
414
+ * Theme variables for the `<XidCameraOcr />` component.
415
+ *
416
+ * The component reads every colour, font and radius as `var(--xco-*, <default>)`
417
+ * and writes a variable only when the `theme` prop sets it, so the stylesheet
418
+ * defaults below are the single source of truth for the fallbacks.
419
+ */
420
+
421
+ type XcoThemeKey = 'accent' | 'warning' | 'danger' | 'background' | 'mask' | 'hintBg' | 'hintFg' | 'buttonBg' | 'buttonFg' | 'shutter' | 'overlayBg' | 'overlayFg' | 'fontFamily' | 'radius';
422
+ type XidCameraOcrTheme = Partial<Record<XcoThemeKey, string>>;
423
+ declare const XCO_VARS: Readonly<Record<XcoThemeKey, {
424
+ name: `--xco-${string}`;
425
+ fallback: string;
426
+ }>>;
427
+
428
+ interface UseXidCameraOcrOptions extends Omit<XidCameraOcrSessionOptions, 'videoElement'> {
429
+ /** When `true`, starts the stream automatically on mount. Default `true`. */
430
+ autoStart?: boolean;
431
+ }
432
+ interface UseXidCameraOcrReturn {
433
+ videoRef: React.RefObject<HTMLVideoElement>;
434
+ status: SessionState['status'];
435
+ frame: FrameState | null;
436
+ result: ExtractResult | null;
437
+ error: Error | null;
438
+ start: () => Promise<void>;
439
+ stop: () => void;
440
+ captureNow: () => Promise<void>;
441
+ /** Stops the current run and starts a new one (re-arms the auto-shutter). */
442
+ retake: () => Promise<void>;
443
+ }
444
+ declare function useXidCameraOcr(options: UseXidCameraOcrOptions): UseXidCameraOcrReturn;
445
+
446
+ interface XidCameraOcrProps extends UseXidCameraOcrOptions {
447
+ className?: string;
448
+ style?: React.CSSProperties;
449
+ hideStatusOverlay?: boolean;
450
+ /** Per-key override of the es-MX default texts. */
451
+ texts?: Partial<HintCopy>;
452
+ /** Theme variables written to the root as `--xco-*` custom properties. */
453
+ theme?: XidCameraOcrTheme;
454
+ /** Show the capture tips card. Default `true`. */
455
+ showTips?: boolean;
456
+ }
457
+ declare function XidCameraOcr({ className, style, hideStatusOverlay, texts: textOverrides, theme, showTips, ...sessionOptions }: XidCameraOcrProps): react.JSX.Element;
458
+
459
+ export { CVWorkerClient, type CornerPoint, type CvDetectionResult, DEFAULT_TEXTS_EN, DEFAULT_TEXTS_ES, type DocumentType, type ExtractResult, type FrameHint, type FrameQuality, type FrameState, type HintCopy, type HintCopyKey, ID1_RATIO, type Point, QUALITY_THRESHOLDS, type Quad, type SessionState, type SessionStatus, StabilityTracker, TD3_RATIO, type UseXidCameraOcrOptions, type UseXidCameraOcrReturn, XCO_VARS, type XcoThemeKey, XidCameraOcr, type XidCameraOcrProps, XidCameraOcrSession, type XidCameraOcrSessionOptions, type XidCameraOcrTheme, attachCamera, copyFor, decideHint, defaultOpencvUrl, detachCamera, detectDocumentCV, isCapturableAspect, measureQuality, orderCorners, rectifiedAspect, resolveTexts, sampleFrame, setTorch, snapshotToBlob, supportsTorch, typeHintFromAspect, useXidCameraOcr, warpDocument, warpDocumentImageData, warpedSnapshotToBlob };