scan2form 1.1.1 → 1.2.0
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 +23 -1
- package/dist/bridge-server.js +29 -19
- package/dist/esm/scanner-client.js +63 -15
- package/dist/scanner-client.js +63 -15
- package/package.json +4 -2
- package/start-server.bat +4 -0
- package/start-server.sh +3 -0
- package/dist/temp_scans/scan_7ff561dc-a4a9-4306-b9ab-220d2c011586.tiff +0 -0
- package/dist/temp_scans/scan_dc151e30-2330-451e-9840-a1a38e4edcef.tiff +0 -0
- package/dist/temp_scans/scan_e172e1d8-094d-47d8-beff-5c1fadaaaa56.tiff +0 -0
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"
|
|
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
|
package/dist/bridge-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
90
|
-
console
|
|
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(
|
|
99
|
-
res.sendFile(
|
|
100
|
-
fs_1.default.unlink(
|
|
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
|
|
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 ->
|
|
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
|
|
124
|
-
//
|
|
125
|
-
|
|
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(
|
|
135
|
-
res.sendFile(
|
|
136
|
-
fs_1.default.unlink(
|
|
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
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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`, {
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
}
|
package/dist/scanner-client.js
CHANGED
|
@@ -29,30 +29,78 @@ class Scan2Form {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
// Rule 5.2 & 5.3: Trigger Scan & Receive Blob
|
|
32
|
-
async
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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`, {
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
package/start-server.bat
ADDED
package/start-server.sh
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|