@arcships/light-ocr 0.5.3 → 0.5.4

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.
Files changed (3) hide show
  1. package/README.md +29 -3
  2. package/package.json +2 -5
  3. package/src/index.cjs +2 -261
package/README.md CHANGED
@@ -1,10 +1,36 @@
1
1
  # @arcships/light-ocr
2
2
 
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.
3
+ The stable PP-OCRv6 Small entry for local image OCR on Node.js 22 and 24.
4
4
 
5
5
  ```bash
6
6
  npm install @arcships/light-ocr
7
- light-ocr info --version
7
+ light-ocr image.png --format text
8
+ light-ocr doctor --json
8
9
  ```
9
10
 
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.
11
+ The package exact-pins one model-free runtime, the Small model, and the native
12
+ component for the current platform. It has no install script and its complete
13
+ release closure is tested with npm offline and scripts disabled.
14
+
15
+ ```js
16
+ const { createEngine } = require('@arcships/light-ocr');
17
+
18
+ const engine = await createEngine();
19
+ try {
20
+ const result = await engine.recognizeEncoded(imageBytes);
21
+ console.log(result.lines);
22
+ } finally {
23
+ await engine.close();
24
+ }
25
+ ```
26
+
27
+ PDF and multi-page processing are intentionally separate:
28
+
29
+ ```bash
30
+ npm install @arcships/light-ocr-document@next
31
+ light-ocr-document report.pdf --format jsonl
32
+ ```
33
+
34
+ The Document package is preview software and runs its pinned PDF renderer's
35
+ prebuild installer. Keeping that dependency explicit prevents PDF tooling from
36
+ changing the stable image package's installation contract.
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "dependencies": {
9
9
  "@arcships/light-ocr-model-ppocrv6-small": "0.3.4",
10
- "@arcships/light-ocr-runtime": "0.1.3"
10
+ "@arcships/light-ocr-runtime": "0.1.4"
11
11
  },
12
12
  "description": "Offline PP-OCRv6 Small OCR for Node.js — the stable default tier",
13
13
  "engines": {
@@ -31,9 +31,6 @@
31
31
  "main": "./src/index.cjs",
32
32
  "module": "./src/index.mjs",
33
33
  "name": "@arcships/light-ocr",
34
- "optionalDependencies": {
35
- "pdfium-native": "0.6.1"
36
- },
37
34
  "publishConfig": {
38
35
  "access": "public",
39
36
  "provenance": true
@@ -44,5 +41,5 @@
44
41
  },
45
42
  "type": "commonjs",
46
43
  "types": "./src/index.d.ts",
47
- "version": "0.5.3"
44
+ "version": "0.5.4"
48
45
  }
package/src/index.cjs CHANGED
@@ -1,20 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const path = require('node:path');
3
+ const { createModelFacade } = require('@arcships/light-ocr-runtime/facade');
4
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({
5
+ module.exports = createModelFacade({
18
6
  model: 'ppocrv6-small',
19
7
  modelPackage: '@arcships/light-ocr-model-ppocrv6-small',
20
8
  compatibleBundleIds: [
@@ -37,250 +25,3 @@ const facade = createModelFacade({
37
25
  maturity: 'stable',
38
26
  },
39
27
  });
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
- };