@arcships/light-ocr 0.3.4 → 0.5.1
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/README.md +3 -7
- package/package.json +13 -26
- package/src/cli.cjs +44 -0
- package/src/index.cjs +286 -0
- package/src/index.d.ts +21 -0
- package/{js → src}/index.mjs +1 -0
- package/bin/light-ocr.cjs +0 -594
- package/js/exif.cjs +0 -241
- package/js/index.cjs +0 -226
- package/js/index.d.ts +0 -284
- package/js/load-native.cjs +0 -499
package/README.md
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
# @arcships/light-ocr
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The default PP-OCRv6 Small entry for `light-ocr`. It exact-pins one compatible model-free runtime and one Small model package, and remains the only owner of the `light-ocr` command.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install @arcships/light-ocr
|
|
7
|
+
light-ocr info --version
|
|
7
8
|
```
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
runtime model download or postinstall compilation is used.
|
|
11
|
-
|
|
12
|
-
Documentation: https://github.com/arcships/light-ocr
|
|
13
|
-
|
|
14
|
-
License: Apache-2.0
|
|
10
|
+
`0.4.0` is the N2 topology cutover: the facade contains only Small model configuration and delegates the API, native loading, EXIF handling, and CLI implementation to the shared runtime. Tiny and Medium use separate packages and commands, so installing this package still installs only Small.
|
package/package.json
CHANGED
|
@@ -1,51 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bin": {
|
|
3
|
-
"light-ocr": "./
|
|
3
|
+
"light-ocr": "./src/cli.cjs"
|
|
4
4
|
},
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/arcships/light-ocr/issues"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@arcships/light-ocr-model-ppocrv6-small": "0.3.4"
|
|
9
|
+
"@arcships/light-ocr-model-ppocrv6-small": "0.3.4",
|
|
10
|
+
"@arcships/light-ocr-runtime": "0.1.1"
|
|
10
11
|
},
|
|
11
|
-
"description": "Offline PP-OCRv6 OCR for Node.js
|
|
12
|
+
"description": "Offline PP-OCRv6 Small OCR for Node.js — the stable default tier",
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": "^22.0.0 || ^24.0.0"
|
|
14
15
|
},
|
|
15
16
|
"exports": {
|
|
16
17
|
".": {
|
|
17
|
-
"import": "./
|
|
18
|
-
"require": "./
|
|
19
|
-
"types": "./
|
|
18
|
+
"import": "./src/index.mjs",
|
|
19
|
+
"require": "./src/index.cjs",
|
|
20
|
+
"types": "./src/index.d.ts"
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
|
-
"
|
|
24
|
-
"bin/",
|
|
24
|
+
"src/",
|
|
25
25
|
"README.md",
|
|
26
26
|
"LICENSE",
|
|
27
27
|
"NOTICE"
|
|
28
28
|
],
|
|
29
29
|
"homepage": "https://github.com/arcships/light-ocr#readme",
|
|
30
|
-
"keywords": [
|
|
31
|
-
"ocr",
|
|
32
|
-
"offline-ocr",
|
|
33
|
-
"pp-ocrv6",
|
|
34
|
-
"paddleocr",
|
|
35
|
-
"node-api",
|
|
36
|
-
"napi"
|
|
37
|
-
],
|
|
38
30
|
"license": "Apache-2.0",
|
|
39
|
-
"main": "./
|
|
40
|
-
"module": "./
|
|
31
|
+
"main": "./src/index.cjs",
|
|
32
|
+
"module": "./src/index.mjs",
|
|
41
33
|
"name": "@arcships/light-ocr",
|
|
42
34
|
"optionalDependencies": {
|
|
43
|
-
"
|
|
44
|
-
"@arcships/light-ocr-darwin-x64": "0.3.4",
|
|
45
|
-
"@arcships/light-ocr-linux-arm64-gnu": "0.3.4",
|
|
46
|
-
"@arcships/light-ocr-linux-x64-gnu": "0.3.4",
|
|
47
|
-
"@arcships/light-ocr-win32-arm64": "0.3.4",
|
|
48
|
-
"@arcships/light-ocr-win32-x64": "0.3.4"
|
|
35
|
+
"pdfium-native": "0.6.1"
|
|
49
36
|
},
|
|
50
37
|
"publishConfig": {
|
|
51
38
|
"access": "public",
|
|
@@ -56,6 +43,6 @@
|
|
|
56
43
|
"url": "git+https://github.com/arcships/light-ocr.git"
|
|
57
44
|
},
|
|
58
45
|
"type": "commonjs",
|
|
59
|
-
"types": "./
|
|
60
|
-
"version": "0.
|
|
46
|
+
"types": "./src/index.d.ts",
|
|
47
|
+
"version": "0.5.1"
|
|
61
48
|
}
|
package/src/cli.cjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const facade = require('./index.cjs');
|
|
7
|
+
|
|
8
|
+
// Try to use workspace dependencies, fallback to local paths
|
|
9
|
+
let createCli, coreVersion, loadNative;
|
|
10
|
+
try {
|
|
11
|
+
({ createCli } = require('@arcships/light-ocr-runtime/cli'));
|
|
12
|
+
({ coreVersion } = require('@arcships/light-ocr-runtime/metadata'));
|
|
13
|
+
} catch {
|
|
14
|
+
// Fallback to local runtime
|
|
15
|
+
({ createCli } = require(path.join(__dirname, '..', '..', 'runtime', 'src', 'cli.cjs')));
|
|
16
|
+
({ coreVersion } = require(path.join(__dirname, '..', '..', 'runtime', 'src', 'metadata.cjs')));
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
({ loadNative } = require('@arcships/light-ocr-runtime'));
|
|
20
|
+
} catch {
|
|
21
|
+
try {
|
|
22
|
+
({ loadNative } = require(path.join(__dirname, '..', '..', 'runtime', 'src', 'load-native.cjs')));
|
|
23
|
+
} catch {
|
|
24
|
+
// loadNative unavailable — doctor will report native as unavailable
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const packageMetadata = require('../package.json');
|
|
29
|
+
|
|
30
|
+
const cli = createCli({
|
|
31
|
+
...facade,
|
|
32
|
+
commandName: 'light-ocr',
|
|
33
|
+
packageVersion: packageMetadata.version,
|
|
34
|
+
coreVersion,
|
|
35
|
+
loadNative,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (require.main === module) {
|
|
39
|
+
cli.main(process.argv.slice(2)).then((code) => {
|
|
40
|
+
if (code !== cli.EXIT.success) process.exitCode = code;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = cli;
|
package/src/index.cjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
// Try to use workspace dependencies, fallback to local paths
|
|
6
|
+
let createModelFacade;
|
|
7
|
+
try {
|
|
8
|
+
({ createModelFacade } = require('@arcships/light-ocr-runtime/facade'));
|
|
9
|
+
} catch {
|
|
10
|
+
// Fallback to local runtime
|
|
11
|
+
const facadePath = path.join(__dirname, '..', '..', 'runtime', 'src', 'facade.cjs');
|
|
12
|
+
({ createModelFacade } = require(facadePath));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
|
|
17
|
+
const facade = createModelFacade({
|
|
18
|
+
model: 'ppocrv6-small',
|
|
19
|
+
modelPackage: '@arcships/light-ocr-model-ppocrv6-small',
|
|
20
|
+
compatibleBundleIds: [
|
|
21
|
+
'ppocrv6-small-onnx-20260714.2',
|
|
22
|
+
'ppocrv6-small-apple-20260715.1',
|
|
23
|
+
'ppocrv6-small-webgpu-20260719.1',
|
|
24
|
+
'ppocrv6-small-native-20260719.1',
|
|
25
|
+
],
|
|
26
|
+
appleBundleIds: [
|
|
27
|
+
'ppocrv6-small-apple-20260715.1',
|
|
28
|
+
'ppocrv6-small-native-20260719.1',
|
|
29
|
+
],
|
|
30
|
+
profile: {
|
|
31
|
+
tier: 'small',
|
|
32
|
+
model: 'ppocrv6-small',
|
|
33
|
+
bundleId: 'ppocrv6-small-native-20260719.1',
|
|
34
|
+
languages: 50,
|
|
35
|
+
excludedLanguages: [],
|
|
36
|
+
dictionaryEntries: 18709,
|
|
37
|
+
maturity: 'stable',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Lazy load pdfium-native
|
|
42
|
+
let pdfium = null;
|
|
43
|
+
let pdfiumLoaded = false;
|
|
44
|
+
|
|
45
|
+
function loadPdfium() {
|
|
46
|
+
if (pdfiumLoaded) return pdfium;
|
|
47
|
+
pdfiumLoaded = true;
|
|
48
|
+
try {
|
|
49
|
+
pdfium = require('pdfium-native');
|
|
50
|
+
} catch {
|
|
51
|
+
pdfium = null;
|
|
52
|
+
}
|
|
53
|
+
return pdfium;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasPdfSupport() {
|
|
57
|
+
return loadPdfium() !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function* processPdf(engine, pdfBuffer, options = {}) {
|
|
61
|
+
const pdfiumNative = loadPdfium();
|
|
62
|
+
if (!pdfiumNative) {
|
|
63
|
+
throw new facade.OcrError('unsupported_capability', 'PDF support not available. Install pdfium-native.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
pageRange,
|
|
68
|
+
dpi = 150,
|
|
69
|
+
maxPages = 100,
|
|
70
|
+
maxPagePixels = 4096 * 4096,
|
|
71
|
+
maxTotalPixels = 100 * 1024 * 1024,
|
|
72
|
+
maxFileBytes = 100 * 1024 * 1024,
|
|
73
|
+
signal,
|
|
74
|
+
ocrOptions = {}
|
|
75
|
+
} = options;
|
|
76
|
+
|
|
77
|
+
// Check file size
|
|
78
|
+
if (pdfBuffer.byteLength > maxFileBytes) {
|
|
79
|
+
throw new facade.OcrError('resource_limit_exceeded',
|
|
80
|
+
`File size ${pdfBuffer.byteLength} exceeds maxFileBytes ${maxFileBytes}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let totalPixels = 0;
|
|
84
|
+
|
|
85
|
+
// Open PDF document
|
|
86
|
+
const doc = await pdfiumNative.loadDocument(pdfBuffer);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const pageCount = doc.pageCount;
|
|
90
|
+
|
|
91
|
+
// Apply page range
|
|
92
|
+
const start = pageRange?.start ? Math.max(1, pageRange.start) : 1;
|
|
93
|
+
const end = pageRange?.end ? Math.min(pageCount, pageRange.end) : pageCount;
|
|
94
|
+
|
|
95
|
+
// Check page limits
|
|
96
|
+
if (end - start + 1 > maxPages) {
|
|
97
|
+
throw new facade.OcrError('resource_limit_exceeded',
|
|
98
|
+
`Page count ${end - start + 1} exceeds maxPages ${maxPages}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (let i = start; i <= end; i++) {
|
|
102
|
+
// Check abort signal
|
|
103
|
+
if (signal?.aborted) {
|
|
104
|
+
throw new facade.OcrError('internal_error', 'Operation aborted');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const page = await doc.getPage(i - 1); // 0-indexed
|
|
108
|
+
|
|
109
|
+
// Get page dimensions
|
|
110
|
+
const { width, height } = page;
|
|
111
|
+
const pagePixels = width * height;
|
|
112
|
+
|
|
113
|
+
// Check pixel limits
|
|
114
|
+
if (pagePixels > maxPagePixels) {
|
|
115
|
+
throw new facade.OcrError('resource_limit_exceeded',
|
|
116
|
+
`Page ${i} pixels ${pagePixels} exceeds maxPagePixels ${maxPagePixels}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
totalPixels += pagePixels;
|
|
120
|
+
if (totalPixels > maxTotalPixels) {
|
|
121
|
+
throw new facade.OcrError('resource_limit_exceeded',
|
|
122
|
+
`Total pixels ${totalPixels} exceeds maxTotalPixels ${maxTotalPixels}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Render page to PNG
|
|
126
|
+
const renderStart = Date.now();
|
|
127
|
+
const scale = dpi / 72; // PDF default is 72 DPI
|
|
128
|
+
const pngBuffer = await page.render({ scale });
|
|
129
|
+
const renderTime = (Date.now() - renderStart) * 1000;
|
|
130
|
+
|
|
131
|
+
// OCR the rendered image
|
|
132
|
+
const ocrStart = Date.now();
|
|
133
|
+
const ocrResult = await engine.recognizeEncoded(pngBuffer, ocrOptions);
|
|
134
|
+
const ocrTime = (Date.now() - ocrStart) * 1000;
|
|
135
|
+
|
|
136
|
+
// Close page to free native memory
|
|
137
|
+
await page.close();
|
|
138
|
+
|
|
139
|
+
// Build page result
|
|
140
|
+
const pageResult = {
|
|
141
|
+
index: i - 1,
|
|
142
|
+
width: ocrResult.imageWidth,
|
|
143
|
+
height: ocrResult.imageHeight,
|
|
144
|
+
coordinateSpace: 'pageSpace',
|
|
145
|
+
structure: 'ocr-order',
|
|
146
|
+
lines: ocrResult.lines.map((line, idx) => ({
|
|
147
|
+
id: `L${idx}`,
|
|
148
|
+
text: line.text,
|
|
149
|
+
confidence: line.confidence,
|
|
150
|
+
box: line.box
|
|
151
|
+
})),
|
|
152
|
+
source: {
|
|
153
|
+
kind: 'pdf',
|
|
154
|
+
mediaType: 'application/pdf',
|
|
155
|
+
identity: { pageIndex: i - 1 },
|
|
156
|
+
appliedTransforms: {
|
|
157
|
+
pdf: {
|
|
158
|
+
rotation: 0,
|
|
159
|
+
mediaBox: { x: 0, y: 0, width, height },
|
|
160
|
+
cropBox: { x: 0, y: 0, width, height },
|
|
161
|
+
dpi,
|
|
162
|
+
scale: dpi / 72
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
timingUs: {
|
|
167
|
+
total: renderTime + ocrTime,
|
|
168
|
+
decode: renderTime,
|
|
169
|
+
ocr: ocrTime
|
|
170
|
+
},
|
|
171
|
+
modelBundleId: ocrResult.modelBundleId
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
yield pageResult;
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
doc.destroy();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function* processImages(engine, imageBuffers, options = {}) {
|
|
182
|
+
const { signal, ocrOptions = {} } = options;
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < imageBuffers.length; i++) {
|
|
185
|
+
if (signal?.aborted) {
|
|
186
|
+
throw new facade.OcrError('internal_error', 'Operation aborted');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const buffer = imageBuffers[i];
|
|
190
|
+
|
|
191
|
+
const ocrStart = Date.now();
|
|
192
|
+
const ocrResult = await engine.recognizeEncoded(buffer, {
|
|
193
|
+
...ocrOptions,
|
|
194
|
+
applyExif: true
|
|
195
|
+
});
|
|
196
|
+
const ocrTime = (Date.now() - ocrStart) * 1000;
|
|
197
|
+
|
|
198
|
+
const pageResult = {
|
|
199
|
+
index: i,
|
|
200
|
+
width: ocrResult.imageWidth,
|
|
201
|
+
height: ocrResult.imageHeight,
|
|
202
|
+
coordinateSpace: 'pageSpace',
|
|
203
|
+
structure: 'ocr-order',
|
|
204
|
+
lines: ocrResult.lines.map((line, idx) => ({
|
|
205
|
+
id: `L${idx}`,
|
|
206
|
+
text: line.text,
|
|
207
|
+
confidence: line.confidence,
|
|
208
|
+
box: line.box
|
|
209
|
+
})),
|
|
210
|
+
source: {
|
|
211
|
+
kind: 'image',
|
|
212
|
+
mediaType: 'image/png',
|
|
213
|
+
identity: { index: i },
|
|
214
|
+
appliedTransforms: {
|
|
215
|
+
exif: { orientation: 1, applied: false }
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
timingUs: {
|
|
219
|
+
total: ocrTime,
|
|
220
|
+
decode: 0,
|
|
221
|
+
ocr: ocrTime
|
|
222
|
+
},
|
|
223
|
+
modelBundleId: ocrResult.modelBundleId
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
yield pageResult;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function* recognizeDocument(source, options = {}) {
|
|
231
|
+
// Create engine if not provided
|
|
232
|
+
let engine = options.engine;
|
|
233
|
+
let engineCreated = false;
|
|
234
|
+
|
|
235
|
+
if (!engine) {
|
|
236
|
+
engine = await facade.createEngine();
|
|
237
|
+
engineCreated = true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
let buffers;
|
|
242
|
+
let isPdf = false;
|
|
243
|
+
|
|
244
|
+
if (Array.isArray(source)) {
|
|
245
|
+
// Multiple images
|
|
246
|
+
buffers = [];
|
|
247
|
+
for (const s of source) {
|
|
248
|
+
if (typeof s === 'string') {
|
|
249
|
+
buffers.push(fs.readFileSync(s));
|
|
250
|
+
} else {
|
|
251
|
+
buffers.push(s);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else if (typeof source === 'string') {
|
|
255
|
+
// File path
|
|
256
|
+
const ext = path.extname(source).toLowerCase();
|
|
257
|
+
if (ext === '.pdf') {
|
|
258
|
+
isPdf = true;
|
|
259
|
+
buffers = [fs.readFileSync(source)];
|
|
260
|
+
} else {
|
|
261
|
+
buffers = [fs.readFileSync(source)];
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
// Buffer
|
|
265
|
+
isPdf = source[0] === 0x25 && source[1] === 0x50 && source[2] === 0x44 && source[3] === 0x46;
|
|
266
|
+
buffers = [source];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (isPdf) {
|
|
270
|
+
yield* processPdf(engine, buffers[0], options);
|
|
271
|
+
} else {
|
|
272
|
+
yield* processImages(engine, buffers, options);
|
|
273
|
+
}
|
|
274
|
+
} finally {
|
|
275
|
+
if (engineCreated) {
|
|
276
|
+
await engine.close();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Export facade + document capabilities
|
|
282
|
+
module.exports = {
|
|
283
|
+
...facade,
|
|
284
|
+
hasPdfSupport,
|
|
285
|
+
recognizeDocument
|
|
286
|
+
};
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from '@arcships/light-ocr-runtime';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CreateEngineOptions as RuntimeCreateEngineOptions,
|
|
5
|
+
ModelProfile,
|
|
6
|
+
OcrEngine,
|
|
7
|
+
} from '@arcships/light-ocr-runtime';
|
|
8
|
+
|
|
9
|
+
export type BuiltInModel = 'ppocrv6-small';
|
|
10
|
+
|
|
11
|
+
export type CreateEngineOptions = Omit<RuntimeCreateEngineOptions, 'bundlePath'> & {
|
|
12
|
+
readonly model?: BuiltInModel;
|
|
13
|
+
readonly bundlePath?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createEngine(options?: CreateEngineOptions): Promise<OcrEngine>;
|
|
17
|
+
export const modelProfile: ModelProfile & {
|
|
18
|
+
readonly tier: 'small';
|
|
19
|
+
readonly model: BuiltInModel;
|
|
20
|
+
readonly maturity: 'stable';
|
|
21
|
+
};
|
package/{js → src}/index.mjs
RENAMED