sane-wasm 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.
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ScanImageReader = exports.ScanDataReader = void 0;
13
+ const _1 = require(".");
14
+ class EventBaseClass {
15
+ constructor() {
16
+ this._firing = false;
17
+ this._listeners = {};
18
+ }
19
+ /**
20
+ * Add event listener.
21
+ */
22
+ on(type, listener) {
23
+ var _a;
24
+ if (!this._listeners[type]) {
25
+ this._listeners[type] = [];
26
+ }
27
+ (_a = this._listeners[type]) === null || _a === void 0 ? void 0 : _a.push(listener);
28
+ }
29
+ /**
30
+ * Fire event.
31
+ */
32
+ fire(type, ...args) {
33
+ var _a;
34
+ // if already firing an event, queue for later
35
+ if (this._firing) {
36
+ return Promise.resolve().then(() => this.fire(type, ...args));
37
+ }
38
+ // fire now
39
+ this._firing = true;
40
+ try {
41
+ (_a = this._listeners[type]) === null || _a === void 0 ? void 0 : _a.forEach(fn => fn.apply(this, args));
42
+ }
43
+ finally {
44
+ this._firing = false;
45
+ }
46
+ return null;
47
+ }
48
+ }
49
+ /**
50
+ * Raw data reader that automatically handles SANE's read code flow.
51
+ *
52
+ * Use {@link ScanDataReader.on} to listen to events, available event types
53
+ * are declared on {@link ScanDataReaderEventMap}.
54
+ *
55
+ * A device should already be open with sane_open(), the reader will call
56
+ * sane_start() do the scanning and call sane_stop().
57
+ *
58
+ * Other SANE functions cannot be used while scanning.
59
+ *
60
+ * Scan readers are single use.
61
+ *
62
+ * {@link https://sane-project.gitlab.io/standard/1.06/api.html#code-flow}
63
+ */
64
+ class ScanDataReader extends EventBaseClass {
65
+ constructor(lib) {
66
+ super();
67
+ this._used = false;
68
+ this._killed = false;
69
+ this._lib = lib;
70
+ }
71
+ _readPromise(parameters) {
72
+ return new Promise((resolve, reject) => {
73
+ const read = () => __awaiter(this, void 0, void 0, function* () {
74
+ try {
75
+ if (this._killed) {
76
+ yield this._lib.sane_cancel(); // ignore status
77
+ }
78
+ // reads are non-blocking because of how emscripten works
79
+ // this causes sane_read() to immediately return with
80
+ // 0 bytes of data, this goes against the SANE spec,
81
+ // blocking I/O by default
82
+ // the only option right now is to poll the read using
83
+ // setTimeout(), this is not ideal
84
+ // currently even emscripten's asyncify does not help here,
85
+ // but in the future it might help support blocking I/O
86
+ // XXX: are we in the future now? can we improve this
87
+ // https://github.com/emscripten-core/emscripten/issues/13214
88
+ const { status, data } = yield this._lib.sane_read(); // non-blocking
89
+ if (status === _1.SANEStatus.GOOD) {
90
+ if (parameters && data.length) {
91
+ this.fire('data', parameters, data);
92
+ }
93
+ }
94
+ else if (status === _1.SANEStatus.CANCELLED ||
95
+ // special case for EOF after sane_cancel, after a successful
96
+ // page read (EOF) and subsequent call to sane_cancel we expected a
97
+ // CANCELLED status but the test backend never changes from EOF,
98
+ // this is different on the pixma backend (it changes to CANCELLED),
99
+ // it's unknown what is the correct option, or what happens on other
100
+ // backends, handle special case as cancelled for now
101
+ // XXX: more research
102
+ // https://sane-project.gitlab.io/standard/1.06/api.html#code-flow
103
+ (status === _1.SANEStatus.EOF && this._killed)) {
104
+ if (this._killed instanceof Error) {
105
+ reject(this._killed);
106
+ return;
107
+ }
108
+ resolve();
109
+ return;
110
+ }
111
+ else if (status === _1.SANEStatus.EOF) {
112
+ // image finished
113
+ this._killed = true;
114
+ }
115
+ else {
116
+ // TODO: handle document feeder / multi-page scans
117
+ this._killed = new Error(`Status ${_1.SANEStatus[status]} during sane_read().`);
118
+ }
119
+ setTimeout(read, data && data.length > 0 ? 10 : 200);
120
+ }
121
+ catch (e) {
122
+ const ee = e instanceof Error ? e : new Error("Unknown error while scanning.");
123
+ if (this._killed) {
124
+ // already killed, but sane_read is still going? die
125
+ reject(ee);
126
+ return;
127
+ }
128
+ this._killed = ee;
129
+ setTimeout(read, 200);
130
+ }
131
+ });
132
+ setTimeout(read, 200);
133
+ });
134
+ }
135
+ /**
136
+ * Start scanning operation.
137
+ */
138
+ start() {
139
+ if (this._used) {
140
+ // readers are single use
141
+ return {
142
+ status: _1.SANEStatus.INVAL,
143
+ parameters: null,
144
+ promise: Promise.reject(new Error("Scan readers cannot be reused.")),
145
+ };
146
+ }
147
+ else {
148
+ const { status } = this._lib.sane_start();
149
+ if (status !== _1.SANEStatus.GOOD) {
150
+ return {
151
+ status,
152
+ parameters: null,
153
+ promise: Promise.reject(new Error(`Status ${_1.SANEStatus[status]} during sane_start().`)),
154
+ };
155
+ }
156
+ this._used = true;
157
+ }
158
+ // after successful start, sane_cancel needs to be called and we need
159
+ // to wait for sane_read, so subsequent fails need to use this._killed
160
+ // and run the read loop
161
+ const { status, parameters } = this._lib.sane_get_parameters();
162
+ if (status !== _1.SANEStatus.GOOD) {
163
+ this._killed = new Error(`Status ${_1.SANEStatus[status]} during sane_get_parameters().`);
164
+ return {
165
+ status, parameters,
166
+ promise: this._readPromise(parameters),
167
+ };
168
+ }
169
+ try {
170
+ this.fire('start', parameters);
171
+ }
172
+ catch (e) {
173
+ this._killed = e instanceof Error ? e : new Error("Unknown error while starting the scan.");
174
+ }
175
+ return {
176
+ status, parameters,
177
+ promise: this._readPromise(parameters).finally(() => {
178
+ this.fire('stop', parameters, this._killed instanceof Error ? this._killed : null);
179
+ }),
180
+ };
181
+ }
182
+ /**
183
+ * Cancel scanning operation.
184
+ */
185
+ cancel() {
186
+ if (this._used && !this._killed) {
187
+ this._killed = true;
188
+ }
189
+ }
190
+ }
191
+ exports.ScanDataReader = ScanDataReader;
192
+ /**
193
+ * Image reader that automatically generates RGBA data while reading from
194
+ * SANE's API using {@link ScanDataReader}. It supports the most common scan
195
+ * modes and image formats. More formats can be added in the future.
196
+ *
197
+ * Use {@link ScanImageReader.on} to listen to events, available event types
198
+ * are declared on {@link ScanImageReaderEventMap}.
199
+ *
200
+ * A device should already be open with sane_open(), the reader will call
201
+ * sane_start() do the scanning and call sane_stop().
202
+ *
203
+ * Other SANE functions cannot be used while scanning.
204
+ *
205
+ * Scan readers are single use.
206
+ *
207
+ * {@link https://sane-project.gitlab.io/standard/1.06/api.html#code-flow}
208
+ */
209
+ class ScanImageReader extends ScanDataReader {
210
+ constructor(lib) {
211
+ super(lib);
212
+ this._line = 0;
213
+ this._allData = new Uint8ClampedArray();
214
+ this._remainder = new Uint8ClampedArray();
215
+ this.on('start', this._onStart);
216
+ this.on('data', this._onData);
217
+ this.on('stop', this._onStop);
218
+ }
219
+ _onStart(parameters) {
220
+ if (parameters.format !== _1.SANEFrame.GRAY && parameters.format !== _1.SANEFrame.RGB) {
221
+ throw new Error(`Invalid format (${JSON.stringify(parameters)}).`);
222
+ }
223
+ if (parameters.depth !== 1 && parameters.depth !== 8) {
224
+ throw new Error(`Invalid bit depth (${JSON.stringify(parameters)}).`);
225
+ }
226
+ if (!parameters.last_frame) {
227
+ throw new Error(`Invalid scanner (3-pass) (${JSON.stringify(parameters)}).`);
228
+ }
229
+ if (parameters.lines < 0) {
230
+ throw new Error(`Invalid scanner (hand-scanner) (${JSON.stringify(parameters)}).`);
231
+ }
232
+ if (parameters.bytes_per_line <= 0 || parameters.pixels_per_line <= 0 || parameters.lines <= 0) {
233
+ throw new Error(`Unexpected image size (${JSON.stringify(parameters)}).`);
234
+ }
235
+ const channels = parameters.format === _1.SANEFrame.GRAY ? 1 : 3;
236
+ const bits = parameters.pixels_per_line * parameters.depth * channels;
237
+ if (parameters.bytes_per_line * 8 < bits) {
238
+ throw new Error(`Unexpected byte count (${JSON.stringify(parameters)}).`);
239
+ }
240
+ // what do we support then?
241
+ // we support GRAY and RGB single-pass, 1 and 8-bit, known height
242
+ // that should cover most modern scanners
243
+ this._allData = new Uint8ClampedArray(parameters.lines * parameters.pixels_per_line * 4);
244
+ }
245
+ _onData(parameters, data) {
246
+ const dataIn = new Uint8ClampedArray(this._remainder.length + data.length);
247
+ dataIn.set(this._remainder, 0);
248
+ dataIn.set(data, this._remainder.length);
249
+ // line count, number of full available lines
250
+ const lc = Math.floor(dataIn.length / parameters.bytes_per_line);
251
+ if (!lc) {
252
+ // not enough data for a full line
253
+ this._remainder = dataIn;
254
+ return;
255
+ }
256
+ const dataOut = new Uint8ClampedArray(lc * parameters.pixels_per_line * 4); // RGBA (8-bit)
257
+ let ik = 0; // consumed data on dataIn
258
+ if (parameters.depth === 8) {
259
+ if (parameters.format === _1.SANEFrame.GRAY) {
260
+ // 8-bit / 1 channel
261
+ for (let l = 0, o = 0; l < lc; l++, ik += parameters.bytes_per_line) { // for lines
262
+ for (let p = 0, i = ik; p < parameters.pixels_per_line; p++, i++, o += 4) { // for pixels
263
+ dataOut[o] = dataOut[o + 1] = dataOut[o + 2] = dataIn[i];
264
+ dataOut[o + 3] = 0xff; // alpha
265
+ }
266
+ }
267
+ }
268
+ else {
269
+ // 8-bit / 3 channels
270
+ for (let l = 0, o = 0; l < lc; l++, ik += parameters.bytes_per_line) { // for lines
271
+ for (let p = 0, i = ik; p < parameters.pixels_per_line; p++, i += 3, o += 4) { // for pixels
272
+ dataOut.set(dataIn.subarray(i, i + 3), o);
273
+ dataOut[o + 3] = 0xff; // alpha
274
+ }
275
+ }
276
+ }
277
+ }
278
+ else { // parameters.depth == 1
279
+ if (parameters.format === _1.SANEFrame.GRAY) {
280
+ // 1-bit / 1 channel
281
+ for (let l = 0, o = 0; l < lc; l++, ik += parameters.bytes_per_line) { // for lines
282
+ for (let p = 0, i = ik; p < parameters.pixels_per_line; i++) { // for in bytes
283
+ for (let mask = 0x80; mask !== 0 && p < parameters.pixels_per_line; mask >>= 1, p++, o += 4) { // for pixels
284
+ dataOut[o] = dataOut[o + 1] = dataOut[o + 2] = dataIn[i] & mask ? 0x00 : 0xff;
285
+ dataOut[o + 3] = 0xff; // alpha
286
+ }
287
+ }
288
+ }
289
+ }
290
+ else {
291
+ // 1-bit / 3 channels
292
+ // niche format, probably very few (if any) scanners support this
293
+ // according to the test backend the bits are inverted on this format, if compared
294
+ // with 1-bit gray (1 channel), it remains to be seen if this is correct in the wild
295
+ for (let l = 0, o = 0; l < lc; l++, ik += parameters.bytes_per_line) { // for lines
296
+ for (let p = 0, i = ik; p < parameters.pixels_per_line; i += 3) { // for in bytes
297
+ for (let mask = 0x80; mask !== 0 && p < parameters.pixels_per_line; mask >>= 1, p++, o += 4) { // for pixels
298
+ // rbg is interlaced byte-by-byte not bit-by-bit
299
+ // so 3 bytes have the R-G-B for 8 pixels
300
+ dataOut[o] = dataIn[i] & mask ? 0xff : 0x00;
301
+ dataOut[o + 1] = dataIn[i + 1] & mask ? 0xff : 0x00;
302
+ dataOut[o + 2] = dataIn[i + 2] & mask ? 0xff : 0x00;
303
+ dataOut[o + 3] = 0xff; // alpha
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+ this._remainder = dataIn.subarray(ik);
310
+ this.fire('line', parameters, dataOut, this._line);
311
+ this._allData.set(dataOut, this._line * parameters.pixels_per_line * 4);
312
+ this._line += lc;
313
+ }
314
+ _onStop(parameters, error) {
315
+ if (!error) {
316
+ this.fire('image', parameters, this._allData);
317
+ }
318
+ }
319
+ }
320
+ exports.ScanImageReader = ScanImageReader;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sane-wasm",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "A project to bring the SANE API to the web.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",