node-hp-scan-to 1.1.0 → 1.2.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.
Files changed (42) hide show
  1. package/README.md +74 -6
  2. package/dist/Destination.js +10 -9
  3. package/dist/Destination.js.map +1 -1
  4. package/dist/Event.d.ts +10 -6
  5. package/dist/Event.js +18 -2
  6. package/dist/Event.js.map +1 -1
  7. package/dist/HPApi.d.ts +13 -4
  8. package/dist/HPApi.js +198 -61
  9. package/dist/HPApi.js.map +1 -1
  10. package/dist/Job.d.ts +13 -1
  11. package/dist/Job.js +32 -0
  12. package/dist/Job.js.map +1 -1
  13. package/dist/JpegUtil.d.ts +26 -0
  14. package/dist/JpegUtil.js +238 -0
  15. package/dist/JpegUtil.js.map +1 -0
  16. package/dist/PathHelper.d.ts +5 -0
  17. package/dist/PathHelper.js +79 -0
  18. package/dist/PathHelper.js.map +1 -0
  19. package/dist/ScanContent.d.ts +12 -0
  20. package/dist/ScanContent.js +82 -0
  21. package/dist/ScanContent.js.map +1 -0
  22. package/dist/ScanStatus.d.ts +1 -1
  23. package/dist/ScanStatus.js.map +1 -1
  24. package/dist/WalkupScanToCompDestinations.js +3 -3
  25. package/dist/WalkupScanToCompDestinations.js.map +1 -1
  26. package/dist/WalkupScanToCompEvent.d.ts +10 -0
  27. package/dist/WalkupScanToCompEvent.js +17 -0
  28. package/dist/WalkupScanToCompEvent.js.map +1 -0
  29. package/dist/index.js +406 -102
  30. package/dist/index.js.map +1 -1
  31. package/package.json +26 -13
  32. package/src/Destination.ts +8 -7
  33. package/src/Event.ts +21 -7
  34. package/src/HPApi.ts +117 -43
  35. package/src/Job.ts +39 -1
  36. package/src/JpegUtil.ts +319 -0
  37. package/src/PathHelper.ts +44 -0
  38. package/src/ScanContent.ts +34 -0
  39. package/src/ScanStatus.ts +1 -1
  40. package/src/WalkupScanToCompDestinations.ts +4 -5
  41. package/src/WalkupScanToCompEvent.ts +18 -0
  42. package/src/index.ts +390 -76
package/src/index.ts CHANGED
@@ -1,18 +1,25 @@
1
1
  #!/usr/bin/env node
2
+ // noinspection XmlDeprecatedElement,HtmlDeprecatedTag
3
+
2
4
  "use strict";
3
5
 
4
6
  import os from "os";
5
- import fs from "fs";
6
- import path from "path";
7
- import util from "util";
8
-
9
- import Bonjour, { Service } from "bonjour";
7
+ import fs from "fs/promises";
8
+ import { Command } from "commander";
9
+ import Bonjour from "bonjour";
10
10
 
11
11
  import Destination from "./Destination";
12
12
  import ScanJobSettings from "./ScanJobSettings";
13
13
  import Event from "./Event";
14
14
  import HPApi from "./HPApi";
15
15
  import Job from "./Job";
16
+ import WalkupScanDestination from "./WalkupScanDestination";
17
+ import WalkupScanToCompDestination from "./WalkupScanToCompDestination";
18
+ import JpegUtil from "./JpegUtil";
19
+ import PathHelper from "./PathHelper";
20
+ import { createPdfFrom, ScanContent, ScanPage } from "./ScanContent";
21
+
22
+ const program = new Command();
16
23
 
17
24
  function delay(t: number): Promise<void> {
18
25
  return new Promise(function (resolve) {
@@ -20,10 +27,13 @@ function delay(t: number): Promise<void> {
20
27
  });
21
28
  }
22
29
 
23
- async function waitForScanEvent(resourceURI: string): Promise<Event> {
30
+ async function waitForScanEvent(
31
+ resourceURI: string,
32
+ afterEtag: string | null = null
33
+ ): Promise<Event> {
24
34
  console.log("Start listening for new ScanEvent");
25
35
 
26
- let eventTable = await HPApi.getEvents();
36
+ let eventTable = await HPApi.getEvents(afterEtag ?? "");
27
37
  let acceptedScanEvent = null;
28
38
  let currentEtag = eventTable.etag;
29
39
  while (acceptedScanEvent == null) {
@@ -31,7 +41,10 @@ async function waitForScanEvent(resourceURI: string): Promise<Event> {
31
41
  currentEtag = eventTable.etag;
32
42
 
33
43
  acceptedScanEvent = eventTable.eventTable.events.find(
34
- (ev) => ev.isScanEvent && ev.resourceURI === resourceURI
44
+ (ev) =>
45
+ ev.isScanEvent &&
46
+ ev.destinationURI &&
47
+ ev.destinationURI.indexOf(resourceURI) >= 0
35
48
  );
36
49
  }
37
50
  return acceptedScanEvent;
@@ -44,9 +57,19 @@ async function waitPrinterUntilItIsReadyToUploadOrCompleted(
44
57
  let isReadyToUpload = false;
45
58
  do {
46
59
  job = await HPApi.getJob(jobUrl);
47
- isReadyToUpload =
48
- job.pageState === "ReadyToUpload" || job.jobState === "Completed";
49
- await delay(200);
60
+ if (job.jobState === "Canceled") {
61
+ return job;
62
+ } else if (
63
+ job.pageState === "ReadyToUpload" ||
64
+ job.jobState === "Completed"
65
+ ) {
66
+ isReadyToUpload = true;
67
+ } else if (job.jobState == "Processing") {
68
+ isReadyToUpload = false;
69
+ } else {
70
+ console.log(`Unknown jobState: ${job.jobState}`);
71
+ }
72
+ await delay(300);
50
73
  } while (!isReadyToUpload);
51
74
  return job;
52
75
  }
@@ -57,7 +80,8 @@ async function register(): Promise<string> {
57
80
  const toComp = await HPApi.getWalkupScanToCompCaps();
58
81
 
59
82
  if (toComp) {
60
- const walkupScanDestinations = await HPApi.getWalkupScanToCompDestinations();
83
+ const walkupScanDestinations =
84
+ await HPApi.getWalkupScanToCompDestinations();
61
85
  const destinations = walkupScanDestinations.destinations;
62
86
 
63
87
  console.log(
@@ -94,40 +118,144 @@ async function register(): Promise<string> {
94
118
  return resourceURI;
95
119
  }
96
120
 
97
- async function getNextFile(
98
- folder: string,
99
- currentPageNumber: string
100
- ): Promise<string> {
101
- return path.join(folder, `scanPage${currentPageNumber}.jpg`);
102
- }
103
-
104
- async function saveScan(event: Event): Promise<void> {
105
- let shortcut = "";
106
- let contentType = "";
121
+ async function TryGetDestination(event: Event) {
107
122
  //this code can in some cases be executed before the user actually chooses between Document or Photo
108
123
  //so lets fetch the contentType (Document or Photo) until we get a value
109
- let i = 0;
110
- while (shortcut == "") {
111
- const destination = await HPApi.getDestination(event.resourceURI);
112
- shortcut = destination.shortcut;
113
- if (shortcut !== "") {
114
- contentType = destination.getContentType();
115
- console.log("Selected shortcut: " + shortcut);
124
+ let destination: WalkupScanDestination | WalkupScanToCompDestination | null =
125
+ null;
126
+
127
+ for (let i = 0; i < 20; i++) {
128
+ const destinationURI = event.destinationURI;
129
+ if (destinationURI) {
130
+ destination = await HPApi.getDestination(destinationURI);
131
+
132
+ const shortcut = destination.shortcut;
133
+ if (shortcut !== "") {
134
+ return destination;
135
+ }
116
136
  } else {
117
- await new Promise((resolve) => setTimeout(resolve, 1000)); //wait 1s
118
- i += 1;
119
- if (i > 20) {
120
- return;
121
- } //prevent endless loop
137
+ console.log(`No destination URI found`);
122
138
  }
139
+
140
+ console.log(`No shortcut yet available, attempt: ${i + 1}/20`);
141
+ await new Promise((resolve) => setTimeout(resolve, 1000)); //wait 1s
123
142
  }
124
143
 
125
- const scanStatus = await HPApi.getScanStatus();
126
- console.log("Afd is : " + scanStatus.adfState);
144
+ console.log("Failing to detect destination shortcut");
145
+ console.log(JSON.stringify(destination));
146
+ return null;
147
+ }
127
148
 
128
- let inputSource = scanStatus.getInputSource();
149
+ async function fixJpegSize(filePath: string): Promise<number | null> {
150
+ const buffer: Buffer = await fs.readFile(filePath);
129
151
 
130
- let scanJobSettings = new ScanJobSettings(inputSource, contentType);
152
+ let height = JpegUtil.fixSizeWithDNL(buffer);
153
+ if (height != null) {
154
+ // rewrite the fixed file
155
+ await fs.writeFile(filePath, buffer);
156
+ return height;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ function createScanPage(
162
+ job: Job,
163
+ currentPageNumber: number,
164
+ filePath: string,
165
+ sizeFixed: number | null
166
+ ): ScanPage {
167
+ let height = sizeFixed ?? job.imageHeight;
168
+ return {
169
+ path: filePath,
170
+ pageNumber: currentPageNumber,
171
+ width: job.imageWidth,
172
+ height,
173
+ xResolution: job.xResolution,
174
+ yResolution: job.yResolution,
175
+ };
176
+ }
177
+
178
+ async function handleProcessingState(
179
+ job: Job,
180
+ inputSource: "Adf" | "Platen",
181
+ folder: string,
182
+ scanCount: number,
183
+ currentPageNumber: number
184
+ ): Promise<ScanPage | null> {
185
+ if (
186
+ job.pageState == "ReadyToUpload" &&
187
+ job.binaryURL != null &&
188
+ job.currentPageNumber != null
189
+ ) {
190
+ console.log(
191
+ `Ready to download page job page ${job.currentPageNumber} at:`,
192
+ job.binaryURL
193
+ );
194
+
195
+ const destinationFilePath = PathHelper.getFileForPage(
196
+ folder,
197
+ scanCount,
198
+ currentPageNumber,
199
+ program.opts().pattern,
200
+ "jpg"
201
+ );
202
+ const filePath = await HPApi.downloadPage(
203
+ job.binaryURL,
204
+ destinationFilePath
205
+ );
206
+ console.log("Page downloaded to:", filePath);
207
+
208
+ let sizeFixed: null | number = null;
209
+ if (inputSource == "Adf") {
210
+ sizeFixed = await fixJpegSize(filePath);
211
+ if (sizeFixed == null) {
212
+ console.log(
213
+ `File size has not been fixed, DNF may not have been found and approximate height is: ${job.imageHeight}`
214
+ );
215
+ }
216
+ }
217
+ return createScanPage(job, currentPageNumber, filePath, sizeFixed);
218
+ } else {
219
+ console.log(`Unknown pageState: ${job.pageState}`);
220
+ await delay(200);
221
+ return null;
222
+ }
223
+ }
224
+
225
+ async function waitScanRequest(compEventURI: string): Promise<boolean> {
226
+ const waitMax = 50;
227
+ for (let i = 0; i < waitMax; i++) {
228
+ let walkupScanToCompEvent = await HPApi.getWalkupScanToCompEvent(
229
+ compEventURI
230
+ );
231
+ let message = walkupScanToCompEvent.eventType;
232
+ if (message === "HostSelected") {
233
+ // this ok to wait
234
+ } else if (message === "ScanRequested") {
235
+ break;
236
+ } else if (message === "ScanNewPageRequested") {
237
+ break;
238
+ } else if (message === "ScanPagesComplete") {
239
+ console.log("no more page to scan, scan is finished");
240
+ return false;
241
+ } else {
242
+ console.log(`Unknown eventType: ${message}`);
243
+ return false;
244
+ }
245
+
246
+ console.log(`Waiting user input: ${i + 1}/${waitMax}`);
247
+ await new Promise((resolve) => setTimeout(resolve, 1000)); //wait 1s
248
+ }
249
+ return true;
250
+ }
251
+
252
+ async function executeScanJob(
253
+ scanJobSettings: ScanJobSettings,
254
+ inputSource: "Adf" | "Platen",
255
+ folder: string,
256
+ scanCount: number,
257
+ scanJobContent: ScanContent
258
+ ) {
131
259
  const jobUrl = await HPApi.postJob(scanJobSettings);
132
260
 
133
261
  console.log("New job created:", jobUrl);
@@ -141,56 +269,219 @@ async function saveScan(event: Event): Promise<void> {
141
269
  }
142
270
 
143
271
  if (job.jobState === "Processing") {
144
- if (
145
- job.pageState == "ReadyToUpload" &&
146
- job.binaryURL != null &&
147
- job.currentPageNumber != null
148
- ) {
149
- console.log(
150
- `Ready to download page ${job.currentPageNumber} at:`,
151
- job.binaryURL
152
- );
153
-
154
- const folder = await util.promisify(fs.mkdtemp)(
155
- path.join(os.tmpdir(), "scan-to-pc")
156
- );
157
- console.log(`Target folder: ${folder}`);
158
-
159
- const destinationFilePath = await getNextFile(
160
- folder,
161
- job.currentPageNumber
162
- );
163
- const filePath = await HPApi.downloadPage(
164
- job.binaryURL,
165
- destinationFilePath
166
- );
167
- console.log("Page downloaded to:", filePath);
168
- } else {
169
- console.log(`Unknown pageState: ${job.pageState}`);
170
- await delay(200);
272
+ const page = await handleProcessingState(
273
+ job,
274
+ inputSource,
275
+ folder,
276
+ scanCount,
277
+ scanJobContent.elements.length + 1
278
+ );
279
+ if (page != null) {
280
+ scanJobContent.elements.push(page);
171
281
  }
282
+ } else if (job.jobState === "Canceled") {
283
+ console.log("Job cancelled by device");
284
+ break;
172
285
  } else {
173
- console.log(`Unknown jobState: ${job.jobState}`);
286
+ console.log(`Unhandled jobState: ${job.jobState}`);
174
287
  await delay(200);
175
288
  }
176
289
  }
177
- console.log(`Job state: ${job.jobState}, totalPages: ${job.totalPageNumber}`);
290
+ console.log(
291
+ `Job state: ${job.jobState}, totalPages: ${job.totalPageNumber}:`
292
+ );
293
+ }
294
+
295
+ async function waitScanNewPageRequest(compEventURI: string): Promise<boolean> {
296
+ let startNewScanJob = false;
297
+ let wait = true;
298
+ while (wait) {
299
+ await new Promise((resolve) => setTimeout(resolve, 1000)); //wait 1s
300
+
301
+ let walkupScanToCompEvent = await HPApi.getWalkupScanToCompEvent(
302
+ compEventURI
303
+ );
304
+ let message = walkupScanToCompEvent.eventType;
305
+
306
+ if (message === "ScanNewPageRequested") {
307
+ startNewScanJob = true;
308
+ wait = false;
309
+ } else if (message === "ScanPagesComplete") {
310
+ wait = false;
311
+ } else if (message === "ScanRequested") {
312
+ // continue waiting
313
+ } else {
314
+ wait = false;
315
+ console.log(`Unknown eventType: ${message}`);
316
+ }
317
+ }
318
+ return startNewScanJob;
319
+ }
320
+
321
+ async function executeScanJobs(
322
+ scanJobSettings: ScanJobSettings,
323
+ inputSource: "Adf" | "Platen",
324
+ folder: string,
325
+ scanCount: number,
326
+ scanJobContent: ScanContent,
327
+ firstEvent: Event
328
+ ) {
329
+ await executeScanJob(
330
+ scanJobSettings,
331
+ inputSource,
332
+ folder,
333
+ scanCount,
334
+ scanJobContent
335
+ );
336
+ let lastEvent = firstEvent;
337
+ if (
338
+ lastEvent.compEventURI &&
339
+ inputSource !== "Adf" &&
340
+ lastEvent.destinationURI
341
+ ) {
342
+ lastEvent = await waitForScanEvent(
343
+ lastEvent.destinationURI,
344
+ lastEvent.agingStamp
345
+ );
346
+ if (!lastEvent.compEventURI) {
347
+ return;
348
+ }
349
+ let startNewScanJob = await waitScanNewPageRequest(lastEvent.compEventURI);
350
+ while (startNewScanJob) {
351
+ await executeScanJob(
352
+ scanJobSettings,
353
+ inputSource,
354
+ folder,
355
+ scanCount,
356
+ scanJobContent
357
+ );
358
+ if (!lastEvent.destinationURI) {
359
+ break;
360
+ }
361
+ lastEvent = await waitForScanEvent(
362
+ lastEvent.destinationURI,
363
+ lastEvent.agingStamp
364
+ );
365
+ if (!lastEvent.compEventURI) {
366
+ return;
367
+ }
368
+ startNewScanJob = await waitScanNewPageRequest(lastEvent.compEventURI);
369
+ }
370
+ }
371
+ }
372
+
373
+ async function mergeToPdf(folder: string, scanCount: number, scanJobContent: ScanContent) {
374
+ const pdfFilePath = PathHelper.getFileForScan(
375
+ folder,
376
+ scanCount,
377
+ program.opts().pattern,
378
+ "pdf"
379
+ );
380
+ await createPdfFrom(scanJobContent, pdfFilePath);
381
+ scanJobContent.elements.forEach((e) => fs.unlink(e.path));
382
+ return pdfFilePath;
383
+ }
384
+
385
+ function displayPdfScan(pdfFilePath: string, scanJobContent: ScanContent) {
386
+ console.log(
387
+ `The following page(s) have been rendered inside '${pdfFilePath}': `
388
+ );
389
+ scanJobContent.elements.forEach((e) =>
390
+ console.log(
391
+ `\t- page ${e.pageNumber.toString().padStart(3, " ")} - ${e.width}x${
392
+ e.height
393
+ }`
394
+ )
395
+ );
178
396
  }
179
397
 
398
+ function displayJpegScan(scanJobContent: ScanContent) {
399
+ scanJobContent.elements.forEach((e) =>
400
+ console.log(
401
+ `\t- page ${e.pageNumber.toString().padStart(3, " ")} - ${e.width}x${
402
+ e.height
403
+ } - ${e.path}`
404
+ )
405
+ );
406
+ }
407
+
408
+ async function saveScan(
409
+ event: Event,
410
+ folder: string,
411
+ scanCount: number
412
+ ): Promise<void> {
413
+ if (event.compEventURI) {
414
+ const proceedToScan = await waitScanRequest(event.compEventURI);
415
+ if (!proceedToScan) {
416
+ return;
417
+ }
418
+ }
419
+
420
+ const destination = await TryGetDestination(event);
421
+ if (!destination) {
422
+ console.log("No shortcut selected!");
423
+ return;
424
+ }
425
+ console.log("Selected shortcut: " + destination.shortcut);
426
+
427
+ const contentType = destination.getContentType();
428
+ const toPdf =
429
+ destination.shortcut === "SavePDF" || destination.shortcut === "EmailPDF";
430
+
431
+ const scanStatus = await HPApi.getScanStatus();
432
+ console.log("Afd is : " + scanStatus.adfState);
433
+
434
+ let inputSource = scanStatus.getInputSource();
435
+
436
+ let scanJobSettings = new ScanJobSettings(inputSource, contentType);
437
+
438
+ let scanJobContent: ScanContent = { elements: [] };
439
+
440
+ await executeScanJobs(
441
+ scanJobSettings,
442
+ inputSource,
443
+ folder,
444
+ scanCount,
445
+ scanJobContent,
446
+ event
447
+ );
448
+
449
+ console.log(
450
+ `Scan of page(s) completed totalPages: ${scanJobContent.elements.length}:`
451
+ );
452
+
453
+ if (toPdf) {
454
+ const pdfFilePath = await mergeToPdf(folder, scanCount, scanJobContent);
455
+ displayPdfScan(pdfFilePath, scanJobContent);
456
+ } else {
457
+ displayJpegScan(scanJobContent);
458
+ }
459
+ }
460
+
461
+ let iteration = 0;
180
462
  async function init() {
463
+ const folder = await PathHelper.getOutputFolder(program.opts().directory);
464
+ console.log(`Target folder: ${folder}`);
465
+
466
+ let scanCount = 0;
467
+
181
468
  let keepActive = true;
182
469
  let errorCount = 0;
183
470
  while (keepActive) {
471
+ console.log(`Running iteration: ${iteration} - errorCount: ${errorCount}`);
184
472
  try {
185
473
  let resourceURI = await register();
186
474
 
187
475
  console.log("Waiting scan event for:", resourceURI);
188
476
  const event = await waitForScanEvent(resourceURI);
189
- console.log("Scan event captured");
190
- await saveScan(event);
477
+
478
+ scanCount++;
479
+ console.log(`Scan event captured, saving scan #${scanCount}`);
480
+ await saveScan(event, folder, scanCount);
191
481
  } catch (e) {
192
482
  errorCount++;
193
483
  console.error(e);
484
+ console.log(e);
194
485
  }
195
486
 
196
487
  if (errorCount === 50) {
@@ -201,10 +492,6 @@ async function init() {
201
492
  }
202
493
  }
203
494
 
204
- interface OfficeJetBonjourService extends Service {
205
- addresses?: string[];
206
- }
207
-
208
495
  function findOfficejetIp(): Promise<string> {
209
496
  return new Promise((resolve) => {
210
497
  const bonjour = Bonjour();
@@ -213,10 +500,10 @@ function findOfficejetIp(): Promise<string> {
213
500
  {
214
501
  type: "http",
215
502
  },
216
- (service: OfficeJetBonjourService) => {
503
+ (service) => {
217
504
  console.log(".");
218
505
  if (
219
- service.name.startsWith("Officejet 6500 E710n-z") && //modify for your printer, i.e. "Deskjet 3520 series"
506
+ service.name.startsWith(program.opts().name) &&
220
507
  service.port === 80 &&
221
508
  service.type === "http" &&
222
509
  service.addresses != null
@@ -233,7 +520,34 @@ function findOfficejetIp(): Promise<string> {
233
520
  }
234
521
 
235
522
  async function main() {
236
- const ip = await findOfficejetIp();
523
+ program.option(
524
+ "-ip, --address <ip>",
525
+ "IP address of the printer (this overrides -p)"
526
+ );
527
+ program.option(
528
+ "-n, --name <name>",
529
+ "Name of the printer for service discovery",
530
+ "HP Smart Tank Plus 570 series"
531
+ ); //or i.e. 'Deskjet 3520 series'
532
+ program.option(
533
+ "-d, --directory <dir>",
534
+ "Directory where scans are saved (defaults to /tmp/scan-to-pc<random>)"
535
+ );
536
+ program.option(
537
+ "-p, --pattern <pattern>",
538
+ 'Pattern for filename (i.e. "scan"_dd.mm.yyyy_hh:MM:ss, without this its scanPage<number>)'
539
+ );
540
+ program.option("-D, --debug", "Enable debug");
541
+ program.parse(process.argv);
542
+
543
+ let ip = program.opts().address || "192.168.1.53";
544
+ if (!ip) {
545
+ ip = await findOfficejetIp();
546
+ }
547
+
548
+ const debug = program.opts().debug != null;
549
+
550
+ HPApi.setDebug(debug);
237
551
  HPApi.setPrinterIP(ip);
238
552
  await init();
239
553
  }