@arcships/light-ocr 0.3.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 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
@@ -131,6 +131,10 @@ class OcrEngineImpl {
131
131
  return this.#recognize('recognizeEncoded', data, options);
132
132
  }
133
133
 
134
+ detect(data, options = {}) {
135
+ return this.#recognize('detect', data, options);
136
+ }
137
+
134
138
  #recognize(nativeMethod, image, options) {
135
139
  let signal;
136
140
  let nativeOptions;
package/js/index.d.ts CHANGED
@@ -69,9 +69,17 @@ export interface RecognizeOptions {
69
69
  readonly signal?: AbortSignal;
70
70
  readonly useTextlineOrientation?: boolean;
71
71
  readonly detectionMaxSide?: number;
72
+ readonly applyExif?: boolean;
73
+ readonly region?: Rect;
72
74
  }
73
75
 
74
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
+ }
75
83
  export interface OcrLine {
76
84
  readonly text: string;
77
85
  readonly confidence: number;
@@ -256,7 +264,21 @@ export interface OcrEngine {
256
264
  readonly info: EngineInfo;
257
265
  recognize(image: RawImage, options?: RecognizeOptions): Promise<OcrResult>;
258
266
  recognizeEncoded(data: Uint8Array, options?: RecognizeOptions): Promise<OcrResult>;
267
+ detect(data: Uint8Array, options?: RecognizeOptions): Promise<DetectionResult>;
259
268
  close(): Promise<void>;
260
269
  }
261
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
+
262
284
  export function createEngine(options?: CreateEngineOptions): Promise<OcrEngine>;
@@ -17,6 +17,7 @@ function platformIdentity() {
17
17
  const identities = {
18
18
  'darwin-arm64': { id: 'macos-arm64', os: 'darwin', architecture: 'arm64' },
19
19
  'darwin-x64': { id: 'macos-x64', os: 'darwin', architecture: 'x86_64' },
20
+ 'win32-arm64': { id: 'windows-arm64', os: 'win32', architecture: 'arm64' },
20
21
  'win32-x64': { id: 'windows-x64', os: 'win32', architecture: 'x86_64' },
21
22
  };
22
23
  if (key === 'linux-x64') {
@@ -30,6 +31,17 @@ function platformIdentity() {
30
31
  key,
31
32
  );
32
33
  }
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
+ }
39
+ throw adapterError(
40
+ 'unsupported_platform',
41
+ 'light-ocr currently supports Linux arm64 with glibc only',
42
+ key,
43
+ );
44
+ }
33
45
  const identity = identities[key];
34
46
  if (!identity) {
35
47
  throw adapterError('unsupported_platform', `light-ocr does not support ${key}`, key);
@@ -41,8 +53,10 @@ function platformPackage() {
41
53
  const packages = {
42
54
  'macos-arm64': '@arcships/light-ocr-darwin-arm64',
43
55
  'macos-x64': '@arcships/light-ocr-darwin-x64',
56
+ 'windows-arm64': '@arcships/light-ocr-win32-arm64',
44
57
  'windows-x64': '@arcships/light-ocr-win32-x64',
45
58
  'linux-x64': '@arcships/light-ocr-linux-x64-gnu',
59
+ 'linux-arm64': '@arcships/light-ocr-linux-arm64-gnu',
46
60
  };
47
61
  return packages[platformIdentity().id];
48
62
  }
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.3.0"
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.3.0",
40
- "@arcships/light-ocr-darwin-x64": "0.3.0",
41
- "@arcships/light-ocr-linux-x64-gnu": "0.3.0",
42
- "@arcships/light-ocr-win32-x64": "0.3.0"
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.3.0"
56
+ "version": "0.3.2"
55
57
  }