scan2form 1.1.1 → 1.2.1

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/README.md CHANGED
@@ -19,10 +19,27 @@ import { Scan2Form } from 'scan2form';
19
19
 
20
20
  const scanner = new Scan2Form();
21
21
 
22
- // Triggers the scan and puts the file into <input type="file" id="my-input" style="display: none;"/>
22
+ // Triggers the scan and puts the file into <input type="file" id="my-input" />
23
23
  await scanner.scanToInput('my-input');
24
+
25
+ // OR use the new advanced options (Images & Preview)
26
+ await scanner.scan({
27
+ targetInputId: 'my-input',
28
+ format: 'jpeg', // 'pdf' | 'jpeg' | 'png' | 'jpg'
29
+ previewElementId: 'scan-preview' // ID of an <img>, <iframe>, <object>, or <embed> tag
30
+ });
24
31
  ```
25
32
 
33
+ ## 🖼️ Supported Formats & Preview
34
+ **Scan2Form** now supports multiple formats:
35
+ * **PDF** (Default)
36
+ * **JPEG / JPG**
37
+ * **PNG**
38
+
39
+ To preview a scan immediately, simply provide the ID of an HTML element in `previewElementId`.
40
+ * For images, use an `<img>` tag.
41
+ * For PDFs, use `<iframe>`, `<embed>`, or `<object>`.
42
+
26
43
  That's it! The file input is now populated with a PDF, just as if the user uploaded it manually.
27
44
 
28
45
  ---
@@ -58,6 +75,11 @@ npx scan2form-server
58
75
  * Keep this running while scanning.
59
76
  * It runs locally at `http://127.0.0.1:3000`.
60
77
 
78
+ **Alternative:**
79
+ We've included shortcut scripts in the package. If you have the files locally:
80
+ * **Windows:** Double-click `start-server.bat`
81
+ * **Mac/Linux:** Run `./start-server.sh`
82
+
61
83
  ---
62
84
 
63
85
  ## 🛠️ System Requirements
@@ -80,49 +80,59 @@ app.get('/devices', (req, res) => {
80
80
  });
81
81
  app.post('/scan', async (req, res) => {
82
82
  const scanId = (0, uuid_1.v4)();
83
- const finalPdfPath = path_1.default.join(TEMP_DIR, `scan_${scanId}.pdf`);
83
+ // Default to PDF if not specified
84
+ const format = (req.body.format || 'pdf').toLowerCase();
85
+ // Usage: format can be 'pdf', 'jpg', 'jpeg', 'png'
86
+ const allowedFormats = ['pdf', 'jpg', 'jpeg', 'png'];
87
+ if (!allowedFormats.includes(format)) {
88
+ return res.status(400).json({ error: "Invalid format. Supported: pdf, jpg, png" });
89
+ }
90
+ // Map format to file extension
91
+ const ext = format === 'jpeg' ? 'jpg' : format;
92
+ const finalFilePath = path_1.default.join(TEMP_DIR, `scan_${scanId}.${ext}`);
84
93
  getScannerEngine((engine) => {
85
94
  if (!engine) {
86
95
  return res.status(500).json({ error: "No scanner software installed (NAPS2 or SANE)." });
87
96
  }
88
97
  if (engine === 'naps2') {
89
- const cmd = `naps2.console -o "${finalPdfPath}" -v`;
90
- console.log(`Scanning with NAPS2: ${cmd}`);
98
+ // NAPS2 detects format by extension
99
+ const cmd = `naps2.console -o "${finalFilePath}" -v`;
100
+ console.log(`Scanning with NAPS2 (${format}): ${cmd}`);
91
101
  (0, child_process_1.exec)(cmd, (error, stdout, stderr) => {
92
102
  if (error) {
93
103
  console.error(`NAPS2 Error: ${error.message}`);
94
- // Return the actual error details to the client
95
104
  const errorDetail = stderr || error.message;
96
105
  return res.status(500).json({ error: "Scan failed", details: errorDetail });
97
106
  }
98
- if (fs_1.default.existsSync(finalPdfPath)) {
99
- res.sendFile(finalPdfPath, () => {
100
- fs_1.default.unlink(finalPdfPath, (err) => { if (err)
107
+ if (fs_1.default.existsSync(finalFilePath)) {
108
+ res.sendFile(finalFilePath, () => {
109
+ fs_1.default.unlink(finalFilePath, (err) => { if (err)
101
110
  console.error("Cleanup error:", err); });
102
111
  });
103
112
  }
104
113
  else {
105
- res.status(500).json({ error: "Scan completed but PDF not found.", details: "Output file missing." });
114
+ res.status(500).json({ error: "Scan completed but file not found.", details: "Output file missing." });
106
115
  }
107
116
  });
108
117
  }
109
118
  else if (engine === 'sane') {
110
- // SANE flow: scanimage -> tiff -> sips -> pdf
119
+ // SANE flow: scanimage -> tiff -> sips -> target format
111
120
  const tempTiffPath = path_1.default.join(TEMP_DIR, `scan_${scanId}.tiff`);
112
- // Default to batch access or single scan. `scanimage --format=tiff > output.tiff`
113
121
  const cmd = `scanimage --format=tiff --mode Color --resolution 300 > "${tempTiffPath}"`;
114
122
  console.log(`Scanning with SANE: ${cmd}`);
115
123
  (0, child_process_1.exec)(cmd, (error, stdout, stderr) => {
116
124
  if (error) {
117
- // scanimage writes progress to stderr, so it might not be a real error unless exit code != 0.
118
- // But exec gives error on non-zero exit.
119
125
  console.error(`SANE Error: ${error.message}`);
120
126
  const errorDetail = stderr || error.message;
121
127
  return res.status(500).json({ error: "Scan failed", details: errorDetail });
122
128
  }
123
- // Convert TIFF to PDF using Mac's 'sips' or ImageMagick 'convert'
124
- // Since we are targeting Mac fallback, we use 'sips'
125
- const convertCmd = `sips -s format pdf "${tempTiffPath}" --out "${finalPdfPath}"`;
129
+ // Convert TIFF to Target Format using 'sips'
130
+ // sips support: pdf, jpeg, png
131
+ let sipsFormat = format;
132
+ if (format === 'jpg')
133
+ sipsFormat = 'jpeg';
134
+ const convertCmd = `sips -s format ${sipsFormat} "${tempTiffPath}" --out "${finalFilePath}"`;
135
+ console.log(`Converting: ${convertCmd}`);
126
136
  (0, child_process_1.exec)(convertCmd, (cErr, cOut, cStderr) => {
127
137
  // Cleanup TIFF immediately
128
138
  if (fs_1.default.existsSync(tempTiffPath))
@@ -131,14 +141,14 @@ app.post('/scan', async (req, res) => {
131
141
  console.error(`Conversion Error: ${cStderr}`);
132
142
  return res.status(500).json({ error: "Image conversion failed" });
133
143
  }
134
- if (fs_1.default.existsSync(finalPdfPath)) {
135
- res.sendFile(finalPdfPath, () => {
136
- fs_1.default.unlink(finalPdfPath, (err) => { if (err)
144
+ if (fs_1.default.existsSync(finalFilePath)) {
145
+ res.sendFile(finalFilePath, () => {
146
+ fs_1.default.unlink(finalFilePath, (err) => { if (err)
137
147
  console.error("Cleanup error:", err); });
138
148
  });
139
149
  }
140
150
  else {
141
- res.status(500).json({ error: "Conversion completed but PDF not found." });
151
+ res.status(500).json({ error: "Conversion completed but file not found." });
142
152
  }
143
153
  });
144
154
  });
@@ -26,29 +26,77 @@ export class Scan2Form {
26
26
  }
27
27
  }
28
28
  // Rule 5.2 & 5.3: Trigger Scan & Receive Blob
29
- async scanToInput(inputId) {
30
- const inputElement = document.getElementById(inputId);
31
- if (!inputElement)
32
- throw new Error("Input element not found");
29
+ async scan(options) {
30
+ // Backward compatibility: if string, treat as inputId
31
+ let config = {};
32
+ if (typeof options === 'string') {
33
+ config = { targetInputId: options, format: 'pdf' };
34
+ }
35
+ else {
36
+ config = { format: 'pdf', ...options };
37
+ }
33
38
  try {
34
- const response = await fetch(`${this.bridgeUrl}/scan`, { method: 'POST' });
35
- if (!response.ok)
36
- throw new Error("Scan failed or cancelled at device");
39
+ const response = await fetch(`${this.bridgeUrl}/scan`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ format: config.format })
43
+ });
44
+ if (!response.ok) {
45
+ const errData = await response.json();
46
+ throw new Error(errData.details || "Scan failed or cancelled");
47
+ }
37
48
  const blob = await response.blob();
49
+ // Determine mime type based on format or blob
50
+ const mimeType = blob.type || (config.format === 'pdf' ? 'application/pdf' : `image/${config.format}`);
38
51
  // Rule 5.4: Inject into DataTransfer
39
- const file = new File([blob], `scanned_doc_${Date.now()}.pdf`, { type: 'application/pdf' });
40
- const dataTransfer = new DataTransfer();
41
- dataTransfer.items.add(file);
42
- inputElement.files = dataTransfer.files;
43
- // Trigger change event so frameworks (React/Vue) detect the update
44
- inputElement.dispatchEvent(new Event('change', { bubbles: true }));
52
+ const ext = config.format === 'jpeg' ? 'jpg' : config.format;
53
+ const file = new File([blob], `scanned_doc_${Date.now()}.${ext}`, { type: mimeType });
54
+ // Handle Input Population
55
+ if (config.targetInputId) {
56
+ const inputElement = document.getElementById(config.targetInputId);
57
+ if (inputElement) {
58
+ const dataTransfer = new DataTransfer();
59
+ dataTransfer.items.add(file);
60
+ inputElement.files = dataTransfer.files;
61
+ inputElement.dispatchEvent(new Event('change', { bubbles: true }));
62
+ }
63
+ }
64
+ // Handle Preview
65
+ if (config.previewElementId) {
66
+ this.handlePreview(config.previewElementId, file);
67
+ }
45
68
  return { success: true, file: file };
46
69
  }
47
70
  catch (error) {
48
71
  console.error("Scan2Form Error:", error);
49
- // Alerting might be annoying in a library, maybe optional? Leaving as is for now but usually libraries shouldn't alert.
50
- // alert("Ensure Scan2Form Bridge is running!");
51
72
  return { success: false, error: error.message || "An unknown error occurred during scan." };
52
73
  }
53
74
  }
75
+ /**
76
+ * Alias for scan() to maintain backward compatibility, but now supports options.
77
+ */
78
+ async scanToInput(inputIdOrOptions) {
79
+ return this.scan(inputIdOrOptions);
80
+ }
81
+ handlePreview(elementId, file) {
82
+ const el = document.getElementById(elementId);
83
+ if (!el)
84
+ return;
85
+ const url = URL.createObjectURL(file);
86
+ const tagName = el.tagName.toLowerCase();
87
+ if (tagName === 'img') {
88
+ el.src = url;
89
+ }
90
+ else if (tagName === 'iframe') {
91
+ el.src = url;
92
+ }
93
+ else if (tagName === 'embed') {
94
+ el.src = url;
95
+ el.type = file.type;
96
+ }
97
+ else if (tagName === 'object') {
98
+ el.data = url;
99
+ el.type = file.type;
100
+ }
101
+ }
54
102
  }
@@ -29,30 +29,78 @@ class Scan2Form {
29
29
  }
30
30
  }
31
31
  // Rule 5.2 & 5.3: Trigger Scan & Receive Blob
32
- async scanToInput(inputId) {
33
- const inputElement = document.getElementById(inputId);
34
- if (!inputElement)
35
- throw new Error("Input element not found");
32
+ async scan(options) {
33
+ // Backward compatibility: if string, treat as inputId
34
+ let config = {};
35
+ if (typeof options === 'string') {
36
+ config = { targetInputId: options, format: 'pdf' };
37
+ }
38
+ else {
39
+ config = { format: 'pdf', ...options };
40
+ }
36
41
  try {
37
- const response = await fetch(`${this.bridgeUrl}/scan`, { method: 'POST' });
38
- if (!response.ok)
39
- throw new Error("Scan failed or cancelled at device");
42
+ const response = await fetch(`${this.bridgeUrl}/scan`, {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify({ format: config.format })
46
+ });
47
+ if (!response.ok) {
48
+ const errData = await response.json();
49
+ throw new Error(errData.details || "Scan failed or cancelled");
50
+ }
40
51
  const blob = await response.blob();
52
+ // Determine mime type based on format or blob
53
+ const mimeType = blob.type || (config.format === 'pdf' ? 'application/pdf' : `image/${config.format}`);
41
54
  // Rule 5.4: Inject into DataTransfer
42
- const file = new File([blob], `scanned_doc_${Date.now()}.pdf`, { type: 'application/pdf' });
43
- const dataTransfer = new DataTransfer();
44
- dataTransfer.items.add(file);
45
- inputElement.files = dataTransfer.files;
46
- // Trigger change event so frameworks (React/Vue) detect the update
47
- inputElement.dispatchEvent(new Event('change', { bubbles: true }));
55
+ const ext = config.format === 'jpeg' ? 'jpg' : config.format;
56
+ const file = new File([blob], `scanned_doc_${Date.now()}.${ext}`, { type: mimeType });
57
+ // Handle Input Population
58
+ if (config.targetInputId) {
59
+ const inputElement = document.getElementById(config.targetInputId);
60
+ if (inputElement) {
61
+ const dataTransfer = new DataTransfer();
62
+ dataTransfer.items.add(file);
63
+ inputElement.files = dataTransfer.files;
64
+ inputElement.dispatchEvent(new Event('change', { bubbles: true }));
65
+ }
66
+ }
67
+ // Handle Preview
68
+ if (config.previewElementId) {
69
+ this.handlePreview(config.previewElementId, file);
70
+ }
48
71
  return { success: true, file: file };
49
72
  }
50
73
  catch (error) {
51
74
  console.error("Scan2Form Error:", error);
52
- // Alerting might be annoying in a library, maybe optional? Leaving as is for now but usually libraries shouldn't alert.
53
- // alert("Ensure Scan2Form Bridge is running!");
54
75
  return { success: false, error: error.message || "An unknown error occurred during scan." };
55
76
  }
56
77
  }
78
+ /**
79
+ * Alias for scan() to maintain backward compatibility, but now supports options.
80
+ */
81
+ async scanToInput(inputIdOrOptions) {
82
+ return this.scan(inputIdOrOptions);
83
+ }
84
+ handlePreview(elementId, file) {
85
+ const el = document.getElementById(elementId);
86
+ if (!el)
87
+ return;
88
+ const url = URL.createObjectURL(file);
89
+ const tagName = el.tagName.toLowerCase();
90
+ if (tagName === 'img') {
91
+ el.src = url;
92
+ }
93
+ else if (tagName === 'iframe') {
94
+ el.src = url;
95
+ }
96
+ else if (tagName === 'embed') {
97
+ el.src = url;
98
+ el.type = file.type;
99
+ }
100
+ else if (tagName === 'object') {
101
+ el.data = url;
102
+ el.type = file.type;
103
+ }
104
+ }
57
105
  }
58
106
  exports.Scan2Form = Scan2Form;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scan2form",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Local offline bridge allowing web browsers to access physical scanners (WIA, TWAIN, SANE).",
5
5
  "main": "dist/scanner-client.js",
6
6
  "scripts": {
@@ -18,7 +18,9 @@
18
18
  "files": [
19
19
  "dist",
20
20
  "README.md",
21
- "LICENSE"
21
+ "LICENSE",
22
+ "start-server.bat",
23
+ "start-server.sh"
22
24
  ],
23
25
  "keywords": [
24
26
  "scanner",
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ echo Starting Scan2Form Server...
3
+ call npx scan2form-server
4
+ pause
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ echo "Starting Scan2Form Server..."
3
+ npx scan2form-server