@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 +241 -0
- package/js/index.cjs +4 -0
- package/js/index.d.ts +22 -0
- package/js/load-native.cjs +14 -0
- 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
|
@@ -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>;
|
package/js/load-native.cjs
CHANGED
|
@@ -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.
|
|
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.
|
|
40
|
-
"@arcships/light-ocr-darwin-x64": "0.3.
|
|
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.3.
|
|
56
|
+
"version": "0.3.2"
|
|
55
57
|
}
|