@arcships/light-ocr 0.2.0 → 0.3.2
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.
- package/js/exif.cjs +241 -0
- package/js/index.cjs +22 -8
- package/js/index.d.ts +106 -0
- package/js/load-native.cjs +458 -39
- package/package.json +8 -6
package/js/exif.cjs
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Minimal JPEG EXIF orientation parser (D106, D-N1-5).
|
|
4
|
+
// Reads the orientation tag from the JPEG APP1 segment and applies the
|
|
5
|
+
// corresponding pixel transform. Zero-dependency, stb-style.
|
|
6
|
+
//
|
|
7
|
+
// Only orientation (tag 0x0112) is read. Other EXIF fields are skipped.
|
|
8
|
+
// PNG eXIf is not handled here; PNG has no EXIF orientation in v1.
|
|
9
|
+
//
|
|
10
|
+
// References:
|
|
11
|
+
// - JEITA CP-3451C (Exif 2.3) section 4.6.4 (APP1 structure)
|
|
12
|
+
// - TIFF tag 0x0112 Orientation
|
|
13
|
+
|
|
14
|
+
// Parse the EXIF orientation value from a JPEG buffer.
|
|
15
|
+
// Returns 1..8 if found, or 1 (normal) if not present / not a JPEG.
|
|
16
|
+
function parseExifOrientation(buffer) {
|
|
17
|
+
if (!buffer || buffer.length < 4) return 1;
|
|
18
|
+
// JPEG must start with SOI marker 0xFFD8
|
|
19
|
+
if (buffer[0] !== 0xff || buffer[1] !== 0xd8) return 1;
|
|
20
|
+
|
|
21
|
+
let offset = 2;
|
|
22
|
+
while (offset + 1 < buffer.length) {
|
|
23
|
+
// Each marker: 0xFF then marker code
|
|
24
|
+
if (buffer[offset] !== 0xff) return 1;
|
|
25
|
+
const marker = buffer[offset + 1];
|
|
26
|
+
offset += 2;
|
|
27
|
+
|
|
28
|
+
// SOI (D8), EOI (D9), RSTn (D0-D7), TEM (01): no payload
|
|
29
|
+
if (marker === 0xd8 || marker === 0xd9) return 1;
|
|
30
|
+
if (marker >= 0xd0 && marker <= 0xd7) continue;
|
|
31
|
+
if (marker === 0x01) continue;
|
|
32
|
+
|
|
33
|
+
// SOS (DA): start of scan — EXIF would be before this
|
|
34
|
+
if (marker === 0xda) return 1;
|
|
35
|
+
|
|
36
|
+
// All other markers have a 2-byte length (including the length bytes)
|
|
37
|
+
if (offset + 1 >= buffer.length) return 1;
|
|
38
|
+
const length = (buffer[offset] << 8) | buffer[offset + 1];
|
|
39
|
+
if (length < 2 || offset + length > buffer.length) return 1;
|
|
40
|
+
|
|
41
|
+
// APP1 marker is 0xFFE1
|
|
42
|
+
if (marker === 0xe1) {
|
|
43
|
+
const orientation = tryParseApp1(buffer, offset, length);
|
|
44
|
+
if (orientation) return orientation;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
offset += length;
|
|
48
|
+
}
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Try to parse APP1 as EXIF. Returns orientation 1..8 or 0 if not EXIF.
|
|
53
|
+
function tryParseApp1(buffer, dataOffset, segmentLength) {
|
|
54
|
+
// APP1 data starts after the 2-byte length field
|
|
55
|
+
// EXIF header: "Exif\0\0" (6 bytes)
|
|
56
|
+
const exifHeader = dataOffset + 2;
|
|
57
|
+
if (exifHeader + 6 > dataOffset + segmentLength) return 0;
|
|
58
|
+
if (buffer[exifHeader] !== 0x45 || buffer[exifHeader + 1] !== 0x78 ||
|
|
59
|
+
buffer[exifHeader + 2] !== 0x69 || buffer[exifHeader + 3] !== 0x66 ||
|
|
60
|
+
buffer[exifHeader + 4] !== 0x00 || buffer[exifHeader + 5] !== 0x00) {
|
|
61
|
+
return 0; // not EXIF (could be XMP)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// TIFF header starts here
|
|
65
|
+
const tiffStart = exifHeader + 6;
|
|
66
|
+
if (tiffStart + 8 > dataOffset + segmentLength) return 0;
|
|
67
|
+
|
|
68
|
+
// Byte order: II (little-endian) or MM (big-endian)
|
|
69
|
+
const littleEndian = buffer[tiffStart] === 0x49 && buffer[tiffStart + 1] === 0x49;
|
|
70
|
+
const bigEndian = buffer[tiffStart] === 0x4d && buffer[tiffStart + 1] === 0x4d;
|
|
71
|
+
if (!littleEndian && !bigEndian) return 0;
|
|
72
|
+
const le = littleEndian;
|
|
73
|
+
|
|
74
|
+
// Magic number 42 (0x002A)
|
|
75
|
+
const magic = readU16(buffer, tiffStart + 2, le);
|
|
76
|
+
if (magic !== 0x002a) return 0;
|
|
77
|
+
|
|
78
|
+
// Offset to IFD0 from TIFF start
|
|
79
|
+
const ifdOffset = tiffStart + readU32(buffer, tiffStart + 4, le);
|
|
80
|
+
if (ifdOffset + 2 > dataOffset + segmentLength) return 0;
|
|
81
|
+
|
|
82
|
+
const entryCount = readU16(buffer, ifdOffset, le);
|
|
83
|
+
for (let i = 0; i < entryCount; i++) {
|
|
84
|
+
const entryOffset = ifdOffset + 2 + i * 12;
|
|
85
|
+
if (entryOffset + 12 > dataOffset + segmentLength) break;
|
|
86
|
+
const tag = readU16(buffer, entryOffset, le);
|
|
87
|
+
if (tag === 0x0112) { // Orientation
|
|
88
|
+
const type = readU16(buffer, entryOffset + 2, le);
|
|
89
|
+
const count = readU32(buffer, entryOffset + 4, le);
|
|
90
|
+
if (type === 3 && count === 1) { // SHORT
|
|
91
|
+
const value = readU16(buffer, entryOffset + 8, le);
|
|
92
|
+
if (value >= 1 && value <= 8) return value;
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readU16(buffer, offset, littleEndian) {
|
|
101
|
+
if (littleEndian) return buffer[offset] | (buffer[offset + 1] << 8);
|
|
102
|
+
return (buffer[offset] << 8) | buffer[offset + 1];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readU32(buffer, offset, littleEndian) {
|
|
106
|
+
if (littleEndian) {
|
|
107
|
+
return (buffer[offset]) |
|
|
108
|
+
(buffer[offset + 1] << 8) |
|
|
109
|
+
(buffer[offset + 2] << 16) |
|
|
110
|
+
(buffer[offset + 3] << 24);
|
|
111
|
+
}
|
|
112
|
+
return (buffer[offset] << 24) |
|
|
113
|
+
(buffer[offset + 1] << 16) |
|
|
114
|
+
(buffer[offset + 2] << 8) |
|
|
115
|
+
(buffer[offset + 3]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Apply EXIF orientation to RGB pixel data.
|
|
119
|
+
// input: { data: Uint8Array (RGB), width, height }
|
|
120
|
+
// orientation: 1..8
|
|
121
|
+
// Returns { data, width, height } in pageSpace (orientation-corrected).
|
|
122
|
+
function applyOrientation(pixels, orientation) {
|
|
123
|
+
if (orientation === 1) return pixels; // normal, no transform
|
|
124
|
+
|
|
125
|
+
const { data, width: w, height: h } = pixels;
|
|
126
|
+
const channels = 3;
|
|
127
|
+
|
|
128
|
+
switch (orientation) {
|
|
129
|
+
case 2: // flip horizontal
|
|
130
|
+
return flipHorizontal(data, w, h, channels);
|
|
131
|
+
case 3: // rotate 180
|
|
132
|
+
return rotate180(data, w, h, channels);
|
|
133
|
+
case 4: // flip vertical
|
|
134
|
+
return flipVertical(data, w, h, channels);
|
|
135
|
+
case 5: // transpose (flip horizontal + rotate 270 CW)
|
|
136
|
+
return transpose(data, w, h, channels);
|
|
137
|
+
case 6: // rotate 90 CW
|
|
138
|
+
return rotate90CW(data, w, h, channels);
|
|
139
|
+
case 7: // transverse (flip horizontal + rotate 90 CW)
|
|
140
|
+
return transverse(data, w, h, channels);
|
|
141
|
+
case 8: // rotate 90 CCW (= 270 CW)
|
|
142
|
+
return rotate90CCW(data, w, h, channels);
|
|
143
|
+
default:
|
|
144
|
+
return pixels;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function alloc(w, h, channels) {
|
|
149
|
+
return { data: new Uint8Array(w * h * channels), width: w, height: h };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function flipHorizontal(data, w, h, c) {
|
|
153
|
+
const out = alloc(w, h, c);
|
|
154
|
+
for (let y = 0; y < h; y++) {
|
|
155
|
+
for (let x = 0; x < w; x++) {
|
|
156
|
+
const src = (y * w + x) * c;
|
|
157
|
+
const dst = (y * w + (w - 1 - x)) * c;
|
|
158
|
+
for (let i = 0; i < c; i++) out.data[dst + i] = data[src + i];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function flipVertical(data, w, h, c) {
|
|
165
|
+
const out = alloc(w, h, c);
|
|
166
|
+
for (let y = 0; y < h; y++) {
|
|
167
|
+
for (let x = 0; x < w; x++) {
|
|
168
|
+
const src = (y * w + x) * c;
|
|
169
|
+
const dst = ((h - 1 - y) * w + x) * c;
|
|
170
|
+
for (let i = 0; i < c; i++) out.data[dst + i] = data[src + i];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function rotate180(data, w, h, c) {
|
|
177
|
+
const out = alloc(w, h, c);
|
|
178
|
+
for (let y = 0; y < h; y++) {
|
|
179
|
+
for (let x = 0; x < w; x++) {
|
|
180
|
+
const src = (y * w + x) * c;
|
|
181
|
+
const dst = ((h - 1 - y) * w + (w - 1 - x)) * c;
|
|
182
|
+
for (let i = 0; i < c; i++) out.data[dst + i] = data[src + i];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function rotate90CW(data, w, h, c) {
|
|
189
|
+
// new dimensions: h x w
|
|
190
|
+
const out = alloc(h, w, c);
|
|
191
|
+
for (let y = 0; y < h; y++) {
|
|
192
|
+
for (let x = 0; x < w; x++) {
|
|
193
|
+
const src = (y * w + x) * c;
|
|
194
|
+
// (x, y) -> (h-1-y, x) in the new w'=h, h'=w grid
|
|
195
|
+
const dst = (x * h + (h - 1 - y)) * c;
|
|
196
|
+
for (let i = 0; i < c; i++) out.data[dst + i] = data[src + i];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function rotate90CCW(data, w, h, c) {
|
|
203
|
+
const out = alloc(h, w, c);
|
|
204
|
+
for (let y = 0; y < h; y++) {
|
|
205
|
+
for (let x = 0; x < w; x++) {
|
|
206
|
+
const src = (y * w + x) * c;
|
|
207
|
+
// (x, y) -> (y, w-1-x)
|
|
208
|
+
const dst = ((w - 1 - x) * h + y) * c;
|
|
209
|
+
for (let i = 0; i < c; i++) out.data[dst + i] = data[src + i];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function transpose(data, w, h, c) {
|
|
216
|
+
// transpose: (x,y) -> (y,x)
|
|
217
|
+
const out = alloc(h, w, c);
|
|
218
|
+
for (let y = 0; y < h; y++) {
|
|
219
|
+
for (let x = 0; x < w; x++) {
|
|
220
|
+
const src = (y * w + x) * c;
|
|
221
|
+
const dst = (x * h + y) * c;
|
|
222
|
+
for (let i = 0; i < c; i++) out.data[dst + i] = data[src + i];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function transverse(data, w, h, c) {
|
|
229
|
+
// transverse: (x,y) -> (h-1-y, w-1-x)
|
|
230
|
+
const out = alloc(h, w, c);
|
|
231
|
+
for (let y = 0; y < h; y++) {
|
|
232
|
+
for (let x = 0; x < w; x++) {
|
|
233
|
+
const src = (y * w + x) * c;
|
|
234
|
+
const dst = ((h - 1 - y) * h + (w - 1 - x)) * c;
|
|
235
|
+
for (let i = 0; i < c; i++) out.data[dst + i] = data[src + i];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { parseExifOrientation, applyOrientation };
|
package/js/index.cjs
CHANGED
|
@@ -7,7 +7,10 @@ const { loadNative } = require('./load-native.cjs');
|
|
|
7
7
|
|
|
8
8
|
const DEFAULT_MODEL = 'ppocrv6-small';
|
|
9
9
|
const MODEL_PACKAGE = '@arcships/light-ocr-model-ppocrv6-small';
|
|
10
|
-
const
|
|
10
|
+
const CPU_BUNDLE_ID = 'ppocrv6-small-onnx-20260714.2';
|
|
11
|
+
const APPLE_BUNDLE_ID = 'ppocrv6-small-apple-20260715.1';
|
|
12
|
+
const WEBGPU_BUNDLE_ID = 'ppocrv6-small-webgpu-20260719.1';
|
|
13
|
+
const NATIVE_BUNDLE_ID = 'ppocrv6-small-native-20260719.1';
|
|
11
14
|
|
|
12
15
|
class OcrError extends Error {
|
|
13
16
|
constructor(code, message, detail) {
|
|
@@ -49,7 +52,7 @@ function abortReason(signal) {
|
|
|
49
52
|
: signal.reason;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
function resolveBuiltInBundle(model) {
|
|
55
|
+
function resolveBuiltInBundle(model, requireApple) {
|
|
53
56
|
if (model !== DEFAULT_MODEL) {
|
|
54
57
|
throw new OcrError(
|
|
55
58
|
'invalid_argument',
|
|
@@ -76,11 +79,14 @@ function resolveBuiltInBundle(model) {
|
|
|
76
79
|
cause instanceof Error ? cause.message : String(cause),
|
|
77
80
|
);
|
|
78
81
|
}
|
|
79
|
-
|
|
82
|
+
const compatibleBundleIds = requireApple
|
|
83
|
+
? [APPLE_BUNDLE_ID, NATIVE_BUNDLE_ID]
|
|
84
|
+
: [CPU_BUNDLE_ID, APPLE_BUNDLE_ID, WEBGPU_BUNDLE_ID, NATIVE_BUNDLE_ID];
|
|
85
|
+
if (!compatibleBundleIds.includes(manifest.bundleId)) {
|
|
80
86
|
throw new OcrError(
|
|
81
87
|
'package_load_failed',
|
|
82
88
|
'The installed model package is incompatible with this light-ocr release',
|
|
83
|
-
`expected ${
|
|
89
|
+
`expected ${compatibleBundleIds.join(' or ')}, received ${String(manifest.bundleId)}`,
|
|
84
90
|
);
|
|
85
91
|
}
|
|
86
92
|
return path.dirname(manifestPath);
|
|
@@ -101,7 +107,8 @@ function resolveCreateOptions(options) {
|
|
|
101
107
|
}
|
|
102
108
|
if (hasBundlePath) return options;
|
|
103
109
|
const model = hasModel ? options.model : DEFAULT_MODEL;
|
|
104
|
-
const
|
|
110
|
+
const requireApple = options.execution?.provider === 'apple';
|
|
111
|
+
const resolved = { ...options, bundlePath: resolveBuiltInBundle(model, requireApple) };
|
|
105
112
|
delete resolved.model;
|
|
106
113
|
return resolved;
|
|
107
114
|
}
|
|
@@ -124,6 +131,10 @@ class OcrEngineImpl {
|
|
|
124
131
|
return this.#recognize('recognizeEncoded', data, options);
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
detect(data, options = {}) {
|
|
135
|
+
return this.#recognize('detect', data, options);
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
#recognize(nativeMethod, image, options) {
|
|
128
139
|
let signal;
|
|
129
140
|
let nativeOptions;
|
|
@@ -196,13 +207,16 @@ class OcrEngineImpl {
|
|
|
196
207
|
}
|
|
197
208
|
}
|
|
198
209
|
|
|
199
|
-
let
|
|
210
|
+
let nativeRuntime;
|
|
200
211
|
|
|
201
212
|
async function createEngine(options) {
|
|
202
213
|
try {
|
|
203
214
|
const resolvedOptions = resolveCreateOptions(options);
|
|
204
|
-
if (!
|
|
205
|
-
const nativeEngine = await binding.createEngine(
|
|
215
|
+
if (!nativeRuntime) nativeRuntime = loadNative();
|
|
216
|
+
const nativeEngine = await nativeRuntime.binding.createEngine(
|
|
217
|
+
resolvedOptions,
|
|
218
|
+
nativeRuntime.runtimePolicy,
|
|
219
|
+
);
|
|
206
220
|
return new OcrEngineImpl(nativeEngine);
|
|
207
221
|
} catch (error) {
|
|
208
222
|
throw normalizeNativeError(error);
|
package/js/index.d.ts
CHANGED
|
@@ -3,12 +3,28 @@
|
|
|
3
3
|
export type PixelFormat = 'gray8' | 'rgb8' | 'bgr8' | 'rgba8';
|
|
4
4
|
export type DetectionStrategy = 'bounded' | 'tiled' | 'upstreamExact';
|
|
5
5
|
export type BuiltInModel = 'ppocrv6-small';
|
|
6
|
+
export type ExecutionProvider = 'auto' | 'cpu' | 'apple' | 'webgpu';
|
|
7
|
+
export type SessionFallback = 'error' | 'cpu';
|
|
8
|
+
export type CpuPartition = 'allow' | 'forbid';
|
|
9
|
+
export type PerformanceHint = 'latency' | 'throughput';
|
|
10
|
+
/** WebGPU accepts auto/fp32; fp16 is reserved for the Apple provider. */
|
|
11
|
+
export type Precision = 'auto' | 'fp32' | 'fp16';
|
|
6
12
|
|
|
7
13
|
export interface DetectionOptions {
|
|
8
14
|
readonly strategy?: DetectionStrategy;
|
|
9
15
|
readonly maxSide?: number;
|
|
10
16
|
}
|
|
11
17
|
|
|
18
|
+
export interface ExecutionOptions {
|
|
19
|
+
/** Only providers shipped and qualified by this release appear in this union. */
|
|
20
|
+
readonly provider?: ExecutionProvider;
|
|
21
|
+
readonly sessionFallback?: SessionFallback;
|
|
22
|
+
readonly cpuPartition?: CpuPartition;
|
|
23
|
+
readonly deviceId?: number;
|
|
24
|
+
readonly performanceHint?: PerformanceHint;
|
|
25
|
+
readonly precision?: Precision;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
export interface RawImage {
|
|
13
29
|
readonly data: Uint8Array;
|
|
14
30
|
readonly width: number;
|
|
@@ -43,6 +59,7 @@ export interface CreateEngineOptions {
|
|
|
43
59
|
readonly queueCapacity?: number;
|
|
44
60
|
readonly maxPendingInputBytes?: number;
|
|
45
61
|
readonly detection?: DetectionOptions;
|
|
62
|
+
readonly execution?: ExecutionOptions;
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
export interface RecognizeOptions {
|
|
@@ -52,9 +69,17 @@ export interface RecognizeOptions {
|
|
|
52
69
|
readonly signal?: AbortSignal;
|
|
53
70
|
readonly useTextlineOrientation?: boolean;
|
|
54
71
|
readonly detectionMaxSide?: number;
|
|
72
|
+
readonly applyExif?: boolean;
|
|
73
|
+
readonly region?: Rect;
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
export interface Point { readonly x: number; readonly y: number }
|
|
77
|
+
export interface Rect {
|
|
78
|
+
readonly x: number;
|
|
79
|
+
readonly y: number;
|
|
80
|
+
readonly width: number;
|
|
81
|
+
readonly height: number;
|
|
82
|
+
}
|
|
58
83
|
export interface OcrLine {
|
|
59
84
|
readonly text: string;
|
|
60
85
|
readonly confidence: number;
|
|
@@ -67,6 +92,9 @@ export interface RecognitionBatchShape {
|
|
|
67
92
|
readonly batchSize: number;
|
|
68
93
|
readonly height: number;
|
|
69
94
|
readonly width: number;
|
|
95
|
+
readonly computeUnit: 'cpu' | 'ane' | 'gpu';
|
|
96
|
+
readonly modelId: string;
|
|
97
|
+
readonly shapeBucket: string;
|
|
70
98
|
}
|
|
71
99
|
export interface DetectionPassShape {
|
|
72
100
|
readonly tileOrdinal: number;
|
|
@@ -121,13 +149,76 @@ export interface TiledDetectionInfo {
|
|
|
121
149
|
readonly mergeIouThreshold: 0.5;
|
|
122
150
|
readonly mergeIosThreshold: 0.8;
|
|
123
151
|
}
|
|
152
|
+
export interface ProviderCapabilityInfo {
|
|
153
|
+
readonly provider: string;
|
|
154
|
+
readonly packageIncluded: boolean;
|
|
155
|
+
readonly deviceAvailable: boolean;
|
|
156
|
+
/** True only when this exact hardware family has reviewed qualification evidence. */
|
|
157
|
+
readonly deviceValidated: boolean;
|
|
158
|
+
}
|
|
159
|
+
export interface SessionExecutionInfo {
|
|
160
|
+
readonly requestedProvider: string;
|
|
161
|
+
readonly actualProviderChain: readonly string[];
|
|
162
|
+
readonly device: string;
|
|
163
|
+
readonly deviceFamily: string;
|
|
164
|
+
readonly operatingSystem: string;
|
|
165
|
+
readonly precision: string;
|
|
166
|
+
readonly shapePolicy: string;
|
|
167
|
+
readonly modelId: string;
|
|
168
|
+
readonly modelSha256: string;
|
|
169
|
+
readonly runtime: string;
|
|
170
|
+
readonly runtimeVersion: string;
|
|
171
|
+
readonly providerVersion: string;
|
|
172
|
+
readonly modelCacheStatus: string;
|
|
173
|
+
readonly qualificationId: string;
|
|
174
|
+
/** False means the open macOS compatibility path is experimental on this device. */
|
|
175
|
+
readonly deviceValidated: boolean;
|
|
176
|
+
readonly sessionFallback: boolean;
|
|
177
|
+
readonly fallbackReason?: string;
|
|
178
|
+
}
|
|
179
|
+
export type CreationReason =
|
|
180
|
+
| 'adapter_unavailable' | 'model_compute_unsupported'
|
|
181
|
+
| 'device_memory_insufficient' | 'driver_version_unsupported'
|
|
182
|
+
| 'package_corrupt' | 'artifact_hash_mismatch' | 'provider_abi_mismatch'
|
|
183
|
+
| 'internal_assertion_failed' | 'unrecoverable_load_failed';
|
|
184
|
+
export type CreationAttemptStatus = 'selected' | 'skipped' | 'fatal';
|
|
185
|
+
export interface CreationAttempt {
|
|
186
|
+
readonly provider: string;
|
|
187
|
+
readonly status: CreationAttemptStatus;
|
|
188
|
+
readonly creationReason?: CreationReason;
|
|
189
|
+
readonly errorCode?: CoreErrorCode;
|
|
190
|
+
}
|
|
191
|
+
export interface CreationTrace {
|
|
192
|
+
readonly requestedProvider: string;
|
|
193
|
+
readonly policyId?: string;
|
|
194
|
+
readonly policyVersion?: number;
|
|
195
|
+
readonly orderedCandidates: readonly string[];
|
|
196
|
+
readonly attempts: readonly CreationAttempt[];
|
|
197
|
+
readonly selectedProvider?: string;
|
|
198
|
+
}
|
|
199
|
+
export interface ExecutionInfo {
|
|
200
|
+
readonly requestedProvider: ExecutionProvider;
|
|
201
|
+
readonly sessionFallback: SessionFallback;
|
|
202
|
+
readonly cpuPartition: CpuPartition;
|
|
203
|
+
readonly deviceId?: number;
|
|
204
|
+
readonly performanceHint: PerformanceHint;
|
|
205
|
+
readonly requestedPrecision: Precision;
|
|
206
|
+
readonly providerCapabilities: readonly ProviderCapabilityInfo[];
|
|
207
|
+
readonly selectionTrace: CreationTrace;
|
|
208
|
+
readonly sessions: {
|
|
209
|
+
readonly detection: SessionExecutionInfo;
|
|
210
|
+
readonly recognition: SessionExecutionInfo;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
124
213
|
export interface EngineInfo {
|
|
125
214
|
readonly coreVersion: string;
|
|
126
215
|
readonly modelBundleId: string;
|
|
127
216
|
readonly modelBundleSchemaVersion: string;
|
|
128
217
|
readonly normalizedConfigSchemaVersion: string;
|
|
129
218
|
readonly backend: string;
|
|
219
|
+
/** @deprecated Use execution.sessions for stage-specific provider details. */
|
|
130
220
|
readonly executionProvider: string;
|
|
221
|
+
readonly execution: ExecutionInfo;
|
|
131
222
|
readonly capabilities: {
|
|
132
223
|
readonly detection: boolean;
|
|
133
224
|
readonly recognition: boolean;
|
|
@@ -166,13 +257,28 @@ export class OcrError extends Error {
|
|
|
166
257
|
readonly name: 'OcrError';
|
|
167
258
|
readonly code: OcrErrorCode;
|
|
168
259
|
readonly detail?: string;
|
|
260
|
+
readonly creationTrace?: CreationTrace;
|
|
169
261
|
}
|
|
170
262
|
|
|
171
263
|
export interface OcrEngine {
|
|
172
264
|
readonly info: EngineInfo;
|
|
173
265
|
recognize(image: RawImage, options?: RecognizeOptions): Promise<OcrResult>;
|
|
174
266
|
recognizeEncoded(data: Uint8Array, options?: RecognizeOptions): Promise<OcrResult>;
|
|
267
|
+
detect(data: Uint8Array, options?: RecognizeOptions): Promise<DetectionResult>;
|
|
175
268
|
close(): Promise<void>;
|
|
176
269
|
}
|
|
177
270
|
|
|
271
|
+
export interface DetectionBox {
|
|
272
|
+
readonly score: number;
|
|
273
|
+
readonly box: readonly [Point, Point, Point, Point];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface DetectionResult {
|
|
277
|
+
readonly boxes: readonly DetectionBox[];
|
|
278
|
+
readonly imageWidth: number;
|
|
279
|
+
readonly imageHeight: number;
|
|
280
|
+
readonly modelBundleId: string;
|
|
281
|
+
readonly timingUs: TimingUs;
|
|
282
|
+
}
|
|
283
|
+
|
|
178
284
|
export function createEngine(options?: CreateEngineOptions): Promise<OcrEngine>;
|
package/js/load-native.cjs
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
3
4
|
const fs = require('node:fs');
|
|
4
5
|
const path = require('node:path');
|
|
5
6
|
|
|
6
|
-
function candidatePaths() {
|
|
7
|
-
const candidates = [];
|
|
8
|
-
if (process.env.LIGHT_OCR_NODE_BINARY) {
|
|
9
|
-
candidates.push(path.resolve(process.env.LIGHT_OCR_NODE_BINARY));
|
|
10
|
-
}
|
|
11
|
-
candidates.push(path.join(__dirname, 'native', 'light_ocr_node.node'));
|
|
12
|
-
candidates.push(
|
|
13
|
-
path.join(
|
|
14
|
-
__dirname,
|
|
15
|
-
'..',
|
|
16
|
-
'prebuilds',
|
|
17
|
-
`${process.platform}-${process.arch}`,
|
|
18
|
-
'light_ocr_node.node',
|
|
19
|
-
),
|
|
20
|
-
);
|
|
21
|
-
return candidates;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
7
|
function adapterError(code, message, detail, cause) {
|
|
25
8
|
const error = new Error(message, cause === undefined ? undefined : { cause });
|
|
26
9
|
error.name = 'OcrError';
|
|
@@ -29,52 +12,488 @@ function adapterError(code, message, detail, cause) {
|
|
|
29
12
|
return error;
|
|
30
13
|
}
|
|
31
14
|
|
|
32
|
-
function
|
|
15
|
+
function platformIdentity() {
|
|
33
16
|
const key = `${process.platform}-${process.arch}`;
|
|
34
|
-
const
|
|
35
|
-
'darwin-arm64': '
|
|
36
|
-
'darwin-x64': '
|
|
37
|
-
'win32-
|
|
17
|
+
const identities = {
|
|
18
|
+
'darwin-arm64': { id: 'macos-arm64', os: 'darwin', architecture: 'arm64' },
|
|
19
|
+
'darwin-x64': { id: 'macos-x64', os: 'darwin', architecture: 'x86_64' },
|
|
20
|
+
'win32-arm64': { id: 'windows-arm64', os: 'win32', architecture: 'arm64' },
|
|
21
|
+
'win32-x64': { id: 'windows-x64', os: 'win32', architecture: 'x86_64' },
|
|
38
22
|
};
|
|
39
23
|
if (key === 'linux-x64') {
|
|
40
24
|
const report = process.report?.getReport?.();
|
|
41
25
|
if (report?.header?.glibcVersionRuntime) {
|
|
42
|
-
return '
|
|
26
|
+
return { id: 'linux-x64', os: 'linux', architecture: 'x86_64', libc: 'glibc' };
|
|
43
27
|
}
|
|
44
28
|
throw adapterError(
|
|
45
29
|
'unsupported_platform',
|
|
46
30
|
'light-ocr currently supports Linux x64 with glibc only',
|
|
47
|
-
|
|
31
|
+
key,
|
|
48
32
|
);
|
|
49
33
|
}
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
if (key === 'linux-arm64') {
|
|
35
|
+
const report = process.report?.getReport?.();
|
|
36
|
+
if (report?.header?.glibcVersionRuntime) {
|
|
37
|
+
return { id: 'linux-arm64', os: 'linux', architecture: 'arm64', libc: 'glibc' };
|
|
38
|
+
}
|
|
52
39
|
throw adapterError(
|
|
53
40
|
'unsupported_platform',
|
|
54
|
-
|
|
41
|
+
'light-ocr currently supports Linux arm64 with glibc only',
|
|
55
42
|
key,
|
|
56
43
|
);
|
|
57
44
|
}
|
|
58
|
-
|
|
45
|
+
const identity = identities[key];
|
|
46
|
+
if (!identity) {
|
|
47
|
+
throw adapterError('unsupported_platform', `light-ocr does not support ${key}`, key);
|
|
48
|
+
}
|
|
49
|
+
return identity;
|
|
59
50
|
}
|
|
60
51
|
|
|
61
|
-
function
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
function platformPackage() {
|
|
53
|
+
const packages = {
|
|
54
|
+
'macos-arm64': '@arcships/light-ocr-darwin-arm64',
|
|
55
|
+
'macos-x64': '@arcships/light-ocr-darwin-x64',
|
|
56
|
+
'windows-arm64': '@arcships/light-ocr-win32-arm64',
|
|
57
|
+
'windows-x64': '@arcships/light-ocr-win32-x64',
|
|
58
|
+
'linux-x64': '@arcships/light-ocr-linux-x64-gnu',
|
|
59
|
+
'linux-arm64': '@arcships/light-ocr-linux-arm64-gnu',
|
|
60
|
+
};
|
|
61
|
+
return packages[platformIdentity().id];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function exactKeys(value, expected, field) {
|
|
65
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
66
|
+
throw adapterError('package_load_failed', `${field} must be an object`);
|
|
67
|
+
}
|
|
68
|
+
const actual = Object.keys(value).sort();
|
|
69
|
+
const wanted = [...expected].sort();
|
|
70
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
71
|
+
throw adapterError('package_load_failed', `${field} fields are invalid`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function safeArtifactPath(root, value, field) {
|
|
76
|
+
if (typeof value !== 'string' || value === '' || value.includes('\0')) {
|
|
77
|
+
throw adapterError('package_load_failed', `${field} must be a package-relative path`);
|
|
78
|
+
}
|
|
79
|
+
const normalized = value.replaceAll('\\', '/');
|
|
80
|
+
if (
|
|
81
|
+
path.posix.isAbsolute(normalized) ||
|
|
82
|
+
/^[A-Za-z]:/.test(normalized) ||
|
|
83
|
+
normalized.split('/').some((part) => part === '..' || part === '' || part === '.')
|
|
84
|
+
) {
|
|
85
|
+
throw adapterError('package_load_failed', `${field} escapes the native package`, value);
|
|
86
|
+
}
|
|
87
|
+
const resolved = path.resolve(root, ...normalized.split('/'));
|
|
88
|
+
const relative = path.relative(path.resolve(root), resolved);
|
|
89
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
90
|
+
throw adapterError('package_load_failed', `${field} escapes the native package`, value);
|
|
91
|
+
}
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
65
94
|
|
|
66
|
-
|
|
95
|
+
function sha256(filename) {
|
|
96
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filename)).digest('hex');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function verifyArtifact(root, artifact, field) {
|
|
100
|
+
exactKeys(artifact, ['path', 'bytes', 'sha256'], field);
|
|
101
|
+
const filename = safeArtifactPath(root, artifact.path, `${field}.path`);
|
|
102
|
+
let stats;
|
|
67
103
|
try {
|
|
68
|
-
|
|
104
|
+
stats = fs.lstatSync(filename);
|
|
69
105
|
} catch (cause) {
|
|
106
|
+
throw adapterError('package_load_failed', 'Descriptor artifact is missing', artifact.path, cause);
|
|
107
|
+
}
|
|
108
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
109
|
+
throw adapterError('package_load_failed', 'Descriptor artifact is not a regular file', artifact.path);
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 1 || stats.size !== artifact.bytes) {
|
|
112
|
+
throw adapterError('package_load_failed', 'Descriptor artifact byte count mismatch', artifact.path);
|
|
113
|
+
}
|
|
114
|
+
if (!/^[a-f0-9]{64}$/.test(artifact.sha256 || '') || sha256(filename) !== artifact.sha256) {
|
|
115
|
+
throw adapterError('package_load_failed', 'Descriptor artifact hash mismatch', artifact.path);
|
|
116
|
+
}
|
|
117
|
+
return filename;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sameArtifact(left, right) {
|
|
121
|
+
return left?.path === right?.path && left?.bytes === right?.bytes &&
|
|
122
|
+
left?.sha256 === right?.sha256;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateRuntimeDescriptor(descriptorPath) {
|
|
126
|
+
const absoluteDescriptor = path.resolve(descriptorPath);
|
|
127
|
+
const nativeDirectory = path.dirname(absoluteDescriptor);
|
|
128
|
+
if (
|
|
129
|
+
path.basename(absoluteDescriptor) !== 'runtime-descriptor.json' ||
|
|
130
|
+
path.basename(nativeDirectory) !== 'native'
|
|
131
|
+
) {
|
|
70
132
|
throw adapterError(
|
|
71
133
|
'package_load_failed',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
134
|
+
'Native runtime descriptor must use the package native/runtime-descriptor.json path',
|
|
135
|
+
absoluteDescriptor,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
let descriptor;
|
|
139
|
+
try {
|
|
140
|
+
const nativeStats = fs.lstatSync(nativeDirectory);
|
|
141
|
+
if (!nativeStats.isDirectory() || nativeStats.isSymbolicLink()) {
|
|
142
|
+
throw new Error('native payload root is not a regular directory');
|
|
143
|
+
}
|
|
144
|
+
const stats = fs.lstatSync(absoluteDescriptor);
|
|
145
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new Error('not a regular file');
|
|
146
|
+
descriptor = JSON.parse(fs.readFileSync(absoluteDescriptor, 'utf8'));
|
|
147
|
+
} catch (cause) {
|
|
148
|
+
throw adapterError(
|
|
149
|
+
'package_load_failed',
|
|
150
|
+
'Unable to read the native runtime descriptor',
|
|
151
|
+
absoluteDescriptor,
|
|
75
152
|
cause,
|
|
76
153
|
);
|
|
77
154
|
}
|
|
155
|
+
exactKeys(
|
|
156
|
+
descriptor,
|
|
157
|
+
['schemaVersion', 'platform', 'runtime', 'qualificationOnly', 'released',
|
|
158
|
+
'autoPolicy', 'providers', 'addon'],
|
|
159
|
+
'runtime descriptor',
|
|
160
|
+
);
|
|
161
|
+
if (descriptor.schemaVersion !== '2.0') {
|
|
162
|
+
throw adapterError('package_load_failed', 'Unsupported native runtime descriptor schema');
|
|
163
|
+
}
|
|
164
|
+
const root = path.dirname(path.dirname(absoluteDescriptor));
|
|
165
|
+
const expected = platformIdentity();
|
|
166
|
+
const expectedPlatformKeys = expected.libc
|
|
167
|
+
? ['id', 'os', 'architecture', 'libc']
|
|
168
|
+
: ['id', 'os', 'architecture'];
|
|
169
|
+
exactKeys(descriptor.platform, expectedPlatformKeys, 'platform');
|
|
170
|
+
const actual = descriptor.platform;
|
|
171
|
+
if (
|
|
172
|
+
actual.id !== expected.id ||
|
|
173
|
+
actual.os !== expected.os ||
|
|
174
|
+
actual.architecture !== expected.architecture ||
|
|
175
|
+
(expected.libc && actual.libc !== expected.libc)
|
|
176
|
+
) {
|
|
177
|
+
throw adapterError('package_load_failed', 'Native runtime descriptor platform mismatch');
|
|
178
|
+
}
|
|
179
|
+
if (
|
|
180
|
+
typeof descriptor.released !== 'boolean' ||
|
|
181
|
+
typeof descriptor.qualificationOnly !== 'boolean' ||
|
|
182
|
+
descriptor.qualificationOnly === descriptor.released
|
|
183
|
+
) {
|
|
184
|
+
throw adapterError('package_load_failed', 'Native runtime descriptor release flags are invalid');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
exactKeys(descriptor.autoPolicy, ['id', 'version', 'providers'], 'autoPolicy');
|
|
188
|
+
const policy = descriptor.autoPolicy;
|
|
189
|
+
if (
|
|
190
|
+
typeof policy.id !== 'string' || policy.id === '' ||
|
|
191
|
+
!Number.isSafeInteger(policy.version) || policy.version < 1 || policy.version > 0xffffffff ||
|
|
192
|
+
!Array.isArray(policy.providers) || policy.providers.length === 0 ||
|
|
193
|
+
policy.providers.length > 3 || policy.providers.at(-1) !== 'cpu' ||
|
|
194
|
+
new Set(policy.providers).size !== policy.providers.length
|
|
195
|
+
) {
|
|
196
|
+
throw adapterError('package_load_failed', 'Native runtime descriptor Auto policy is invalid');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
exactKeys(descriptor.runtime, ['flavor', 'kind', 'version', 'abi', 'artifacts'], 'runtime');
|
|
200
|
+
const runtime = descriptor.runtime;
|
|
201
|
+
const expectedRuntimes = {
|
|
202
|
+
cpu: { kind: 'onnxruntime-cpu', version: '1.22.0', abi: 'onnxruntime-c-api-22' },
|
|
203
|
+
webgpu: {
|
|
204
|
+
kind: 'onnxruntime-plugin-webgpu',
|
|
205
|
+
version: '1.24.4',
|
|
206
|
+
abi: 'onnxruntime-c-api-24-plugin-ep-0.1',
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
const expectedRuntime = expectedRuntimes[runtime.flavor];
|
|
210
|
+
if (
|
|
211
|
+
!expectedRuntime || runtime.kind !== expectedRuntime.kind ||
|
|
212
|
+
runtime.version !== expectedRuntime.version || runtime.abi !== expectedRuntime.abi ||
|
|
213
|
+
!Array.isArray(runtime.artifacts) || runtime.artifacts.length === 0
|
|
214
|
+
) {
|
|
215
|
+
throw adapterError('package_load_failed', 'Native runtime descriptor ABI identity is invalid');
|
|
216
|
+
}
|
|
217
|
+
if (runtime.flavor === 'webgpu' && !['linux', 'win32'].includes(actual.os)) {
|
|
218
|
+
throw adapterError('package_load_failed', 'WebGPU runtime is not supported on this platform');
|
|
219
|
+
}
|
|
220
|
+
if (runtime.flavor !== 'webgpu' && descriptor.qualificationOnly) {
|
|
221
|
+
throw adapterError('package_load_failed', 'CPU runtime cannot be qualification-only');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const addon = verifyArtifact(root, descriptor.addon, 'addon');
|
|
225
|
+
const runtimePaths = new Set();
|
|
226
|
+
const verifiedRuntime = new Map();
|
|
227
|
+
runtime.artifacts.forEach((artifact, index) => {
|
|
228
|
+
const filename = verifyArtifact(root, artifact, `runtime.artifacts[${index}]`);
|
|
229
|
+
if (runtimePaths.has(artifact.path)) {
|
|
230
|
+
throw adapterError('package_load_failed', 'Runtime artifact inventory contains a duplicate path');
|
|
231
|
+
}
|
|
232
|
+
runtimePaths.add(artifact.path);
|
|
233
|
+
verifiedRuntime.set(artifact.path, filename);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
exactKeys(descriptor.providers, Object.keys(descriptor.providers), 'providers');
|
|
237
|
+
const availableProviders = Object.keys(descriptor.providers);
|
|
238
|
+
if (
|
|
239
|
+
availableProviders.length === 0 ||
|
|
240
|
+
new Set(availableProviders).size !== availableProviders.length ||
|
|
241
|
+
availableProviders.some((provider) => !['cpu', 'apple', 'webgpu'].includes(provider)) ||
|
|
242
|
+
!descriptor.providers.cpu
|
|
243
|
+
) {
|
|
244
|
+
throw adapterError('package_load_failed', 'Native runtime descriptor provider policy is invalid');
|
|
245
|
+
}
|
|
246
|
+
let webgpuLibrary = '';
|
|
247
|
+
let webgpuProviderBytes = 0;
|
|
248
|
+
let webgpuProviderSha256 = '';
|
|
249
|
+
for (const [providerId, provider] of Object.entries(descriptor.providers)) {
|
|
250
|
+
const expectedKeys = providerId === 'webgpu'
|
|
251
|
+
? ['runtimeProvider', 'providerVersion', 'qualificationId', 'providerLibrary', 'artifacts']
|
|
252
|
+
: ['runtimeProvider', 'qualificationId', 'artifacts'];
|
|
253
|
+
exactKeys(provider, expectedKeys, `providers.${providerId}`);
|
|
254
|
+
if (
|
|
255
|
+
typeof provider.qualificationId !== 'string' || provider.qualificationId === '' ||
|
|
256
|
+
!Array.isArray(provider.artifacts) || provider.artifacts.length === 0
|
|
257
|
+
) {
|
|
258
|
+
throw adapterError('package_load_failed', `Provider ${providerId} identity is invalid`);
|
|
259
|
+
}
|
|
260
|
+
const expectedProviderName = {
|
|
261
|
+
cpu: 'CPUExecutionProvider',
|
|
262
|
+
apple: 'CoreML',
|
|
263
|
+
webgpu: 'WebGpuExecutionProvider',
|
|
264
|
+
}[providerId];
|
|
265
|
+
if (provider.runtimeProvider !== expectedProviderName) {
|
|
266
|
+
throw adapterError('package_load_failed', `Provider ${providerId} runtime identity is invalid`);
|
|
267
|
+
}
|
|
268
|
+
const providerPaths = new Set();
|
|
269
|
+
provider.artifacts.forEach((artifact, index) => {
|
|
270
|
+
verifyArtifact(root, artifact, `providers.${providerId}.artifacts[${index}]`);
|
|
271
|
+
if (providerPaths.has(artifact.path)) {
|
|
272
|
+
throw adapterError('package_load_failed', `Provider ${providerId} has duplicate artifacts`);
|
|
273
|
+
}
|
|
274
|
+
providerPaths.add(artifact.path);
|
|
275
|
+
if (providerId !== 'apple' && !runtimePaths.has(artifact.path)) {
|
|
276
|
+
throw adapterError('package_load_failed', `Provider ${providerId} artifact is outside runtime inventory`);
|
|
277
|
+
}
|
|
278
|
+
if (providerId === 'apple' && artifact.path !== descriptor.addon.path) {
|
|
279
|
+
throw adapterError('package_load_failed', 'Apple provider artifact must be the native addon');
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
if (providerId === 'webgpu') {
|
|
283
|
+
if (provider.providerVersion !== '0.1.0') {
|
|
284
|
+
throw adapterError('package_load_failed', 'WebGPU provider version is invalid');
|
|
285
|
+
}
|
|
286
|
+
webgpuLibrary = verifyArtifact(root, provider.providerLibrary, 'providers.webgpu.providerLibrary');
|
|
287
|
+
const declared = provider.artifacts.find(
|
|
288
|
+
(artifact) => artifact.path === provider.providerLibrary.path,
|
|
289
|
+
);
|
|
290
|
+
const expectedBasename = actual.os === 'win32'
|
|
291
|
+
? 'onnxruntime_providers_webgpu.dll'
|
|
292
|
+
: 'libonnxruntime_providers_webgpu.so';
|
|
293
|
+
if (
|
|
294
|
+
!declared || !sameArtifact(declared, provider.providerLibrary) ||
|
|
295
|
+
path.basename(webgpuLibrary) !== expectedBasename
|
|
296
|
+
) {
|
|
297
|
+
throw adapterError('package_load_failed', 'WebGPU provider library contract is invalid');
|
|
298
|
+
}
|
|
299
|
+
webgpuProviderBytes = provider.providerLibrary.bytes;
|
|
300
|
+
webgpuProviderSha256 = provider.providerLibrary.sha256;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const coreName = actual.os === 'win32'
|
|
305
|
+
? 'onnxruntime.dll'
|
|
306
|
+
: actual.os === 'darwin'
|
|
307
|
+
? 'libonnxruntime.1.22.0.dylib'
|
|
308
|
+
: 'libonnxruntime.so.1';
|
|
309
|
+
const runtimeNames = [...verifiedRuntime.values()].map((filename) => path.basename(filename)).sort();
|
|
310
|
+
const expectedRuntimeNames = runtime.flavor === 'webgpu'
|
|
311
|
+
? actual.os === 'win32'
|
|
312
|
+
? ['dxcompiler.dll', 'dxil.dll', 'onnxruntime.dll', 'onnxruntime_providers_webgpu.dll']
|
|
313
|
+
: ['libonnxruntime.so.1', 'libonnxruntime_providers_webgpu.so']
|
|
314
|
+
: [coreName];
|
|
315
|
+
if (
|
|
316
|
+
runtimeNames.length !== expectedRuntimeNames.length ||
|
|
317
|
+
runtimeNames.some((name, index) => name !== expectedRuntimeNames[index])
|
|
318
|
+
) {
|
|
319
|
+
throw adapterError('package_load_failed', 'Native runtime artifact set is incomplete');
|
|
320
|
+
}
|
|
321
|
+
const cpuArtifacts = descriptor.providers.cpu.artifacts;
|
|
322
|
+
if (
|
|
323
|
+
cpuArtifacts.length !== 1 ||
|
|
324
|
+
path.basename(safeArtifactPath(root, cpuArtifacts[0].path, 'CPU artifact path')) !== coreName
|
|
325
|
+
) {
|
|
326
|
+
throw adapterError('package_load_failed', 'CPU provider does not reference the core runtime');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const actualPayload = new Set();
|
|
330
|
+
const inventory = (directory, relativeDirectory) => {
|
|
331
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
332
|
+
const relative = `${relativeDirectory}/${entry.name}`;
|
|
333
|
+
const filename = path.join(directory, entry.name);
|
|
334
|
+
if (entry.isSymbolicLink()) {
|
|
335
|
+
throw adapterError('package_load_failed', 'Native runtime payload contains a symlink', relative);
|
|
336
|
+
}
|
|
337
|
+
if (entry.isDirectory()) {
|
|
338
|
+
inventory(filename, relative);
|
|
339
|
+
} else if (entry.isFile() && relative !== 'native/runtime-descriptor.json') {
|
|
340
|
+
actualPayload.add(relative);
|
|
341
|
+
} else if (!entry.isFile()) {
|
|
342
|
+
throw adapterError('package_load_failed', 'Native runtime payload is not a regular file', relative);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
inventory(nativeDirectory, 'native');
|
|
347
|
+
const referenced = new Set([descriptor.addon.path, ...runtimePaths]);
|
|
348
|
+
if (
|
|
349
|
+
actualPayload.size !== referenced.size ||
|
|
350
|
+
[...actualPayload].some((filename) => !referenced.has(filename))
|
|
351
|
+
) {
|
|
352
|
+
throw adapterError('package_load_failed', 'Native runtime descriptor payload inventory mismatch');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const expectedPolicy = runtime.flavor === 'webgpu'
|
|
356
|
+
? ['webgpu', 'cpu']
|
|
357
|
+
: actual.id === 'macos-arm64'
|
|
358
|
+
? ['apple', 'cpu']
|
|
359
|
+
: ['cpu'];
|
|
360
|
+
const expectedAvailable = runtime.flavor === 'webgpu'
|
|
361
|
+
? ['cpu', 'webgpu']
|
|
362
|
+
: actual.id === 'macos-arm64'
|
|
363
|
+
? ['apple', 'cpu']
|
|
364
|
+
: ['cpu'];
|
|
365
|
+
const sortedAvailable = [...availableProviders].sort();
|
|
366
|
+
if (
|
|
367
|
+
policy.providers.length !== expectedPolicy.length ||
|
|
368
|
+
policy.providers.some((provider, index) => provider !== expectedPolicy[index]) ||
|
|
369
|
+
sortedAvailable.length !== expectedAvailable.length ||
|
|
370
|
+
sortedAvailable.some((provider, index) => provider !== expectedAvailable[index]) ||
|
|
371
|
+
policy.providers.some((provider) => !availableProviders.includes(provider))
|
|
372
|
+
) {
|
|
373
|
+
throw adapterError(
|
|
374
|
+
'package_load_failed',
|
|
375
|
+
'Native runtime descriptor providers disagree with platform capabilities',
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const providerQualificationIds = sortedAvailable.map(
|
|
379
|
+
(providerId) => descriptor.providers[providerId].qualificationId,
|
|
380
|
+
);
|
|
381
|
+
const runtimePolicy = Object.freeze({
|
|
382
|
+
id: policy.id,
|
|
383
|
+
version: policy.version,
|
|
384
|
+
platformId: actual.id,
|
|
385
|
+
runtimeFlavor: runtime.flavor,
|
|
386
|
+
runtimeVersion: runtime.version,
|
|
387
|
+
runtimeAbi: runtime.abi,
|
|
388
|
+
qualificationOnly: descriptor.qualificationOnly,
|
|
389
|
+
released: descriptor.released,
|
|
390
|
+
orderedCandidates: Object.freeze([...policy.providers]),
|
|
391
|
+
availableProviders: Object.freeze(sortedAvailable),
|
|
392
|
+
providerQualificationIds: Object.freeze(providerQualificationIds),
|
|
393
|
+
webgpuProviderLibrary: webgpuLibrary,
|
|
394
|
+
webgpuProviderBytes,
|
|
395
|
+
webgpuProviderSha256,
|
|
396
|
+
});
|
|
397
|
+
return {
|
|
398
|
+
addon,
|
|
399
|
+
descriptor: Object.freeze(descriptor),
|
|
400
|
+
descriptorPath: absoluteDescriptor,
|
|
401
|
+
runtimePolicy,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function resolveDevelopmentInput() {
|
|
406
|
+
if (!process.env.LIGHT_OCR_NODE_BINARY) return undefined;
|
|
407
|
+
const binary = path.resolve(process.env.LIGHT_OCR_NODE_BINARY);
|
|
408
|
+
const descriptor = process.env.LIGHT_OCR_RUNTIME_DESCRIPTOR
|
|
409
|
+
? path.resolve(process.env.LIGHT_OCR_RUNTIME_DESCRIPTOR)
|
|
410
|
+
: path.join(path.dirname(binary), 'runtime-descriptor.json');
|
|
411
|
+
return { binary, descriptor };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function validateNativeContract(binding, runtimePolicy) {
|
|
415
|
+
const contract = binding?.runtimeContract;
|
|
416
|
+
const fields = [
|
|
417
|
+
'policyId',
|
|
418
|
+
'policyVersion',
|
|
419
|
+
'platformId',
|
|
420
|
+
'runtimeFlavor',
|
|
421
|
+
'runtimeVersion',
|
|
422
|
+
'runtimeAbi',
|
|
423
|
+
'qualificationOnly',
|
|
424
|
+
'released',
|
|
425
|
+
];
|
|
426
|
+
const policyFields = {
|
|
427
|
+
policyId: runtimePolicy.id,
|
|
428
|
+
policyVersion: runtimePolicy.version,
|
|
429
|
+
platformId: runtimePolicy.platformId,
|
|
430
|
+
runtimeFlavor: runtimePolicy.runtimeFlavor,
|
|
431
|
+
runtimeVersion: runtimePolicy.runtimeVersion,
|
|
432
|
+
runtimeAbi: runtimePolicy.runtimeAbi,
|
|
433
|
+
qualificationOnly: runtimePolicy.qualificationOnly,
|
|
434
|
+
released: runtimePolicy.released,
|
|
435
|
+
};
|
|
436
|
+
if (
|
|
437
|
+
!contract ||
|
|
438
|
+
typeof contract !== 'object' ||
|
|
439
|
+
fields.some((field) => contract[field] !== policyFields[field]) ||
|
|
440
|
+
!Array.isArray(contract.orderedCandidates) ||
|
|
441
|
+
!Array.isArray(contract.availableProviders) ||
|
|
442
|
+
!Array.isArray(contract.providerQualificationIds) ||
|
|
443
|
+
contract.orderedCandidates.length !== runtimePolicy.orderedCandidates.length ||
|
|
444
|
+
contract.orderedCandidates.some(
|
|
445
|
+
(provider, index) => provider !== runtimePolicy.orderedCandidates[index],
|
|
446
|
+
) ||
|
|
447
|
+
contract.availableProviders.length !== runtimePolicy.availableProviders.length ||
|
|
448
|
+
contract.availableProviders.some(
|
|
449
|
+
(provider, index) => provider !== runtimePolicy.availableProviders[index],
|
|
450
|
+
) ||
|
|
451
|
+
contract.providerQualificationIds.length !== runtimePolicy.providerQualificationIds.length ||
|
|
452
|
+
contract.providerQualificationIds.some(
|
|
453
|
+
(qualificationId, index) => qualificationId !== runtimePolicy.providerQualificationIds[index],
|
|
454
|
+
)
|
|
455
|
+
) {
|
|
456
|
+
throw adapterError(
|
|
457
|
+
'package_load_failed',
|
|
458
|
+
'Runtime descriptor is incompatible with the native addon ABI or capabilities',
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function loadNative() {
|
|
464
|
+
const development = resolveDevelopmentInput();
|
|
465
|
+
let input;
|
|
466
|
+
if (development) {
|
|
467
|
+
input = development;
|
|
468
|
+
} else {
|
|
469
|
+
const packageName = platformPackage();
|
|
470
|
+
try {
|
|
471
|
+
const binary = require.resolve(packageName);
|
|
472
|
+
input = { binary, descriptor: path.join(path.dirname(binary), 'runtime-descriptor.json') };
|
|
473
|
+
} catch (cause) {
|
|
474
|
+
throw adapterError(
|
|
475
|
+
'package_load_failed',
|
|
476
|
+
`Unable to locate ${packageName}`,
|
|
477
|
+
'Reinstall @arcships/light-ocr without --omit=optional and verify that the current platform is supported.',
|
|
478
|
+
cause,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!fs.existsSync(input.binary)) {
|
|
484
|
+
throw adapterError('package_load_failed', 'Native addon is missing', input.binary);
|
|
485
|
+
}
|
|
486
|
+
const verified = validateRuntimeDescriptor(input.descriptor);
|
|
487
|
+
if (path.resolve(input.binary) !== path.resolve(verified.addon)) {
|
|
488
|
+
throw adapterError('package_load_failed', 'Runtime descriptor addon path mismatch', input.binary);
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const binding = require(verified.addon);
|
|
492
|
+
validateNativeContract(binding, verified.runtimePolicy);
|
|
493
|
+
return Object.freeze({ binding, runtimePolicy: verified.runtimePolicy });
|
|
494
|
+
} catch (cause) {
|
|
495
|
+
throw adapterError('package_load_failed', 'Unable to load the verified native addon', '', cause);
|
|
496
|
+
}
|
|
78
497
|
}
|
|
79
498
|
|
|
80
|
-
module.exports = { loadNative };
|
|
499
|
+
module.exports = { loadNative, validateRuntimeDescriptor };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"url": "https://github.com/arcships/light-ocr/issues"
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@arcships/light-ocr-model-ppocrv6-small": "0.2
|
|
6
|
+
"@arcships/light-ocr-model-ppocrv6-small": "0.3.2"
|
|
7
7
|
},
|
|
8
8
|
"description": "Offline PP-OCRv6 OCR for Node.js, powered by an embeddable C++ core",
|
|
9
9
|
"engines": {
|
|
@@ -36,10 +36,12 @@
|
|
|
36
36
|
"module": "./js/index.mjs",
|
|
37
37
|
"name": "@arcships/light-ocr",
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@arcships/light-ocr-darwin-arm64": "0.2
|
|
40
|
-
"@arcships/light-ocr-darwin-x64": "0.2
|
|
41
|
-
"@arcships/light-ocr-linux-
|
|
42
|
-
"@arcships/light-ocr-
|
|
39
|
+
"@arcships/light-ocr-darwin-arm64": "0.3.2",
|
|
40
|
+
"@arcships/light-ocr-darwin-x64": "0.3.2",
|
|
41
|
+
"@arcships/light-ocr-linux-arm64-gnu": "0.3.2",
|
|
42
|
+
"@arcships/light-ocr-linux-x64-gnu": "0.3.2",
|
|
43
|
+
"@arcships/light-ocr-win32-arm64": "0.3.2",
|
|
44
|
+
"@arcships/light-ocr-win32-x64": "0.3.2"
|
|
43
45
|
},
|
|
44
46
|
"publishConfig": {
|
|
45
47
|
"access": "public",
|
|
@@ -51,5 +53,5 @@
|
|
|
51
53
|
},
|
|
52
54
|
"type": "commonjs",
|
|
53
55
|
"types": "./js/index.d.ts",
|
|
54
|
-
"version": "0.2
|
|
56
|
+
"version": "0.3.2"
|
|
55
57
|
}
|