scan2form 1.2.3 → 1.3.0-dev.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 +10 -16
- package/dist/bridge-server.js +66 -132
- package/dist/config.js +14 -0
- package/dist/engines/naps2-engine.js +43 -0
- package/dist/engines/sane-engine.js +77 -0
- package/dist/engines/scanner-engine.js +2 -0
- package/dist/errors.js +13 -0
- package/dist/esm/scanner-client.js +25 -80
- package/dist/esm/types.js +1 -0
- package/dist/scanner-client.js +25 -80
- package/dist/types.js +2 -0
- package/dist/utils.js +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,26 +19,20 @@ import { Scan2Form } from 'scan2form';
|
|
|
19
19
|
|
|
20
20
|
const scanner = new Scan2Form();
|
|
21
21
|
|
|
22
|
-
// Triggers the scan.
|
|
23
|
-
// If your input has accept="image/*", it scans as JPEG. Default is PDF.
|
|
22
|
+
// Triggers the scan and populates the input with a PDF.
|
|
24
23
|
await scanner.scanToInput('my-input');
|
|
25
|
-
|
|
26
|
-
// OR use advanced options (e.g. for preview)
|
|
27
|
-
await scanner.scan({
|
|
28
|
-
targetInputId: 'my-input',
|
|
29
|
-
previewElementId: 'scan-preview'
|
|
30
|
-
});
|
|
31
24
|
```
|
|
32
25
|
|
|
33
|
-
##
|
|
34
|
-
|
|
35
|
-
* **PDF** (Default)
|
|
36
|
-
* **JPEG / JPG**
|
|
37
|
-
* **PNG**
|
|
26
|
+
## 🛠️ Handling Previews
|
|
27
|
+
Since `scanToInput` returns the file object, you can easily display a preview yourself:
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
```javascript
|
|
30
|
+
const result = await scanner.scanToInput('my-input');
|
|
31
|
+
if (result.success) {
|
|
32
|
+
const url = URL.createObjectURL(result.file);
|
|
33
|
+
document.getElementById('my-preview-iframe').src = url;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
42
36
|
|
|
43
37
|
That's it! The file input is now populated with a PDF, just as if the user uploaded it manually.
|
|
44
38
|
|
package/dist/bridge-server.js
CHANGED
|
@@ -7,157 +7,91 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.app = void 0;
|
|
8
8
|
const express_1 = __importDefault(require("express"));
|
|
9
9
|
const cors_1 = __importDefault(require("cors"));
|
|
10
|
-
const child_process_1 = require("child_process");
|
|
11
10
|
const path_1 = __importDefault(require("path"));
|
|
12
11
|
const fs_1 = __importDefault(require("fs"));
|
|
13
12
|
const uuid_1 = require("uuid");
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const naps2_engine_1 = require("./engines/naps2-engine");
|
|
15
|
+
const sane_engine_1 = require("./engines/sane-engine");
|
|
16
|
+
const errors_1 = require("./errors");
|
|
14
17
|
const app = (0, express_1.default)();
|
|
15
18
|
exports.app = app;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!fs_1.default.existsSync(TEMP_DIR))
|
|
21
|
-
fs_1.default.mkdirSync(TEMP_DIR);
|
|
22
|
-
app.use((0, cors_1.default)()); // Allow browser to hit localhost
|
|
19
|
+
// Ensure temp directory exists
|
|
20
|
+
if (!fs_1.default.existsSync(config_1.CONFIG.TEMP_DIR))
|
|
21
|
+
fs_1.default.mkdirSync(config_1.CONFIG.TEMP_DIR);
|
|
22
|
+
app.use((0, cors_1.default)());
|
|
23
23
|
app.use(express_1.default.json());
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Engine Selection Strategy
|
|
25
|
+
const engines = [new naps2_engine_1.Naps2Engine(), new sane_engine_1.SaneEngine()];
|
|
26
|
+
let cachedEngine = null;
|
|
27
|
+
async function getEngine() {
|
|
28
|
+
if (cachedEngine)
|
|
29
|
+
return cachedEngine;
|
|
30
|
+
for (const engine of engines) {
|
|
31
|
+
if (await engine.isAvailable()) {
|
|
32
|
+
console.log(`Using Scanner Engine: ${engine.name}`);
|
|
33
|
+
cachedEngine = engine;
|
|
34
|
+
return engine;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new errors_1.ScanError('NO_ENGINE', 'No supported scanner software found (NAPS2 or SANE).', null, 500);
|
|
38
|
+
}
|
|
39
|
+
// --- Endpoints ---
|
|
40
|
+
app.get('/health', async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const engine = await getEngine();
|
|
43
|
+
res.json({ status: "ok", engine: engine.name, version: "2.0.0" });
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
res.status(503).json({ status: "error", error: "No Engine Available" });
|
|
47
|
+
}
|
|
27
48
|
});
|
|
28
|
-
// Serve example for testing
|
|
29
49
|
app.use('/example', express_1.default.static(path_1.default.join(__dirname, '../example')));
|
|
30
50
|
app.use('/dist', express_1.default.static(path_1.default.join(__dirname, '../dist')));
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
// --- Endpoints ---
|
|
45
|
-
app.get('/devices', (req, res) => {
|
|
46
|
-
getScannerEngine((engine) => {
|
|
47
|
-
if (engine === 'naps2') {
|
|
48
|
-
(0, child_process_1.exec)('naps2.console --list', (error, stdout, stderr) => {
|
|
49
|
-
if (error) {
|
|
50
|
-
console.error("NAPS2 List Error:", stderr);
|
|
51
|
-
return res.json([]);
|
|
52
|
-
}
|
|
53
|
-
const devices = stdout.split('\n').filter(line => line.trim().length > 0);
|
|
54
|
-
res.json({ devices });
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
else if (engine === 'sane') {
|
|
58
|
-
(0, child_process_1.exec)('scanimage -L', (error, stdout, stderr) => {
|
|
59
|
-
if (error) {
|
|
60
|
-
console.error("SANE List Error:", stderr);
|
|
61
|
-
return res.json([]);
|
|
62
|
-
}
|
|
63
|
-
// scanimage -L output: "device `epsonds:libusb:002:003' is a Epson DS-530 II"
|
|
64
|
-
// We want to return a friendly name or the ID.
|
|
65
|
-
const devices = stdout.split('\n')
|
|
66
|
-
.filter(line => line.includes('is a'))
|
|
67
|
-
.map(line => {
|
|
68
|
-
// Cleanup string
|
|
69
|
-
return line.replace('device `', '').replace(`'`, '');
|
|
70
|
-
});
|
|
71
|
-
res.json({ devices });
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
const msg = "No scanner software found. Please install NAPS2 (Windows) or SANE (Mac/Linux).";
|
|
76
|
-
console.error(msg);
|
|
77
|
-
res.json({ devices: [], error: msg });
|
|
78
|
-
}
|
|
79
|
-
});
|
|
51
|
+
app.get('/devices', async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const engine = await getEngine();
|
|
54
|
+
const devices = await engine.listDevices();
|
|
55
|
+
res.json({ devices });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error("Device List Error:", error);
|
|
59
|
+
const status = error instanceof errors_1.ScanError ? error.httpStatus : 500;
|
|
60
|
+
res.status(status).json({ devices: [], error: error.message });
|
|
61
|
+
}
|
|
80
62
|
});
|
|
81
63
|
app.post('/scan', async (req, res) => {
|
|
82
64
|
const scanId = (0, uuid_1.v4)();
|
|
83
|
-
// Default to PDF if not specified
|
|
84
65
|
const format = (req.body.format || 'pdf').toLowerCase();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return res.status(400).json({ error: "Invalid format. Supported: pdf, jpg, png" });
|
|
66
|
+
const deviceId = req.body.deviceId;
|
|
67
|
+
if (!config_1.CONFIG.ALLOWED_FORMATS.includes(format)) {
|
|
68
|
+
return res.status(400).json({ error: `Invalid format. Supported: ${config_1.CONFIG.ALLOWED_FORMATS.join(', ')}` });
|
|
89
69
|
}
|
|
90
|
-
// Map format to file extension
|
|
91
70
|
const ext = format === 'jpeg' ? 'jpg' : format;
|
|
92
|
-
const finalFilePath = path_1.default.join(TEMP_DIR, `scan_${scanId}.${ext}`);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
(0, child_process_1.exec)(cmd, (error, stdout, stderr) => {
|
|
102
|
-
if (error) {
|
|
103
|
-
console.error(`NAPS2 Error: ${error.message}`);
|
|
104
|
-
const errorDetail = stderr || error.message;
|
|
105
|
-
return res.status(500).json({ error: "Scan failed", details: errorDetail });
|
|
106
|
-
}
|
|
107
|
-
if (fs_1.default.existsSync(finalFilePath)) {
|
|
108
|
-
res.sendFile(finalFilePath, () => {
|
|
109
|
-
fs_1.default.unlink(finalFilePath, (err) => { if (err)
|
|
110
|
-
console.error("Cleanup error:", err); });
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
res.status(500).json({ error: "Scan completed but file not found.", details: "Output file missing." });
|
|
115
|
-
}
|
|
71
|
+
const finalFilePath = path_1.default.join(config_1.CONFIG.TEMP_DIR, `scan_${scanId}.${ext}`);
|
|
72
|
+
try {
|
|
73
|
+
const engine = await getEngine();
|
|
74
|
+
console.log(`Starting scan with ${engine.name}...`);
|
|
75
|
+
await engine.scan({ format, deviceId }, finalFilePath);
|
|
76
|
+
if (fs_1.default.existsSync(finalFilePath)) {
|
|
77
|
+
res.sendFile(finalFilePath, () => {
|
|
78
|
+
fs_1.default.unlink(finalFilePath, (err) => { if (err)
|
|
79
|
+
console.error("Cleanup error:", err); });
|
|
116
80
|
});
|
|
117
81
|
}
|
|
118
|
-
else
|
|
119
|
-
|
|
120
|
-
const tempTiffPath = path_1.default.join(TEMP_DIR, `scan_${scanId}.tiff`);
|
|
121
|
-
const cmd = `scanimage --format=tiff --mode Color --resolution 300 > "${tempTiffPath}"`;
|
|
122
|
-
console.log(`Scanning with SANE: ${cmd}`);
|
|
123
|
-
(0, child_process_1.exec)(cmd, (error, stdout, stderr) => {
|
|
124
|
-
if (error) {
|
|
125
|
-
console.error(`SANE Error: ${error.message}`);
|
|
126
|
-
const errorDetail = stderr || error.message;
|
|
127
|
-
return res.status(500).json({ error: "Scan failed", details: errorDetail });
|
|
128
|
-
}
|
|
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}`);
|
|
136
|
-
(0, child_process_1.exec)(convertCmd, (cErr, cOut, cStderr) => {
|
|
137
|
-
// Cleanup TIFF immediately
|
|
138
|
-
if (fs_1.default.existsSync(tempTiffPath))
|
|
139
|
-
fs_1.default.unlinkSync(tempTiffPath);
|
|
140
|
-
if (cErr) {
|
|
141
|
-
console.error(`Conversion Error: ${cStderr}`);
|
|
142
|
-
return res.status(500).json({ error: "Image conversion failed" });
|
|
143
|
-
}
|
|
144
|
-
if (fs_1.default.existsSync(finalFilePath)) {
|
|
145
|
-
res.sendFile(finalFilePath, () => {
|
|
146
|
-
fs_1.default.unlink(finalFilePath, (err) => { if (err)
|
|
147
|
-
console.error("Cleanup error:", err); });
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
res.status(500).json({ error: "Conversion completed but file not found." });
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
});
|
|
82
|
+
else {
|
|
83
|
+
throw new errors_1.ScanError('FILE_MISSING', 'Scan finished but output file is missing.', null, 500);
|
|
155
84
|
}
|
|
156
|
-
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error("Scan Error:", error);
|
|
88
|
+
const status = error instanceof errors_1.ScanError ? error.httpStatus : 500;
|
|
89
|
+
res.status(status).json({ error: "Scan failed", details: error.message });
|
|
90
|
+
}
|
|
157
91
|
});
|
|
158
92
|
if (require.main === module) {
|
|
159
|
-
app.listen(PORT,
|
|
160
|
-
console.log(`Scan2Form Bridge running at http
|
|
161
|
-
console.log(`Open Example: http
|
|
93
|
+
app.listen(config_1.CONFIG.PORT, config_1.CONFIG.HOST, () => {
|
|
94
|
+
console.log(`Scan2Form Bridge running at http://${config_1.CONFIG.HOST}:${config_1.CONFIG.PORT}`);
|
|
95
|
+
console.log(`Open Example: http://${config_1.CONFIG.HOST}:${config_1.CONFIG.PORT}/example/index.html`);
|
|
162
96
|
});
|
|
163
97
|
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CONFIG = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
exports.CONFIG = {
|
|
9
|
+
PORT: process.env.PORT ? parseInt(process.env.PORT) : 3000,
|
|
10
|
+
HOST: process.env.HOST || '127.0.0.1',
|
|
11
|
+
TEMP_DIR: process.env.TEMP_DIR || path_1.default.join(__dirname, 'temp_scans'),
|
|
12
|
+
SCAN_TIMEOUT_MS: process.env.SCAN_TIMEOUT_MS ? parseInt(process.env.SCAN_TIMEOUT_MS) : 60000,
|
|
13
|
+
ALLOWED_FORMATS: ['pdf', 'jpg', 'jpeg', 'png'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Naps2Engine = void 0;
|
|
4
|
+
const utils_1 = require("../utils");
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
class Naps2Engine {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.name = 'naps2';
|
|
9
|
+
}
|
|
10
|
+
async isAvailable() {
|
|
11
|
+
try {
|
|
12
|
+
await (0, utils_1.runCommand)('naps2.console', ['--help'], 5000);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async listDevices() {
|
|
20
|
+
try {
|
|
21
|
+
const stdout = await (0, utils_1.runCommand)('naps2.console', ['--list']);
|
|
22
|
+
return stdout.split('\n')
|
|
23
|
+
.filter(line => line.trim().length > 0)
|
|
24
|
+
.map(line => ({ name: line.trim() }));
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
throw new errors_1.ScanError('DEVICE_LIST_FAILED', 'Failed to list NAPS2 devices', e.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async scan(options, outputPath) {
|
|
31
|
+
try {
|
|
32
|
+
const args = ['-o', outputPath, '-v'];
|
|
33
|
+
// If deviceId is provided, NAPS2 might support it via specific flags or profile,
|
|
34
|
+
// but for now we stick to default behavior or profile usage if previously configured.
|
|
35
|
+
// If explicit device name selection is needed, NAPS2 usually uses profiles.
|
|
36
|
+
await (0, utils_1.runCommand)('naps2.console', args);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
throw new errors_1.ScanError('SCAN_FAILED', 'NAPS2 scan failed', e.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.Naps2Engine = Naps2Engine;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SaneEngine = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const utils_1 = require("../utils");
|
|
10
|
+
const errors_1 = require("../errors");
|
|
11
|
+
class SaneEngine {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.name = 'sane';
|
|
14
|
+
}
|
|
15
|
+
async isAvailable() {
|
|
16
|
+
try {
|
|
17
|
+
await (0, utils_1.runCommand)('scanimage', ['--version'], 5000);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async listDevices() {
|
|
25
|
+
try {
|
|
26
|
+
const stdout = await (0, utils_1.runCommand)('scanimage', ['-L']);
|
|
27
|
+
return stdout.split('\n')
|
|
28
|
+
.filter(line => line.includes('is a'))
|
|
29
|
+
.map(line => {
|
|
30
|
+
const name = line.replace('device `', '').replace(`'`, '');
|
|
31
|
+
return { name };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
throw new errors_1.ScanError('DEVICE_LIST_FAILED', 'Failed to list SANE devices', e.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async scan(options, outputPath) {
|
|
39
|
+
const tempTiffPath = outputPath.replace(/\.\w+$/, '.tiff');
|
|
40
|
+
try {
|
|
41
|
+
// SANE Scan
|
|
42
|
+
const args = ['--format=tiff', '--mode', 'Color', '--resolution', '300'];
|
|
43
|
+
if (options.deviceId) {
|
|
44
|
+
args.push('-d', options.deviceId);
|
|
45
|
+
// Note: Device ID handling needs care given the string format,
|
|
46
|
+
// but for SANE 'deviceId' is usually the name/address.
|
|
47
|
+
}
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
const fileStream = fs_1.default.createWriteStream(tempTiffPath);
|
|
50
|
+
const child = (0, child_process_1.spawn)('scanimage', args);
|
|
51
|
+
child.stdout.pipe(fileStream);
|
|
52
|
+
let stderr = '';
|
|
53
|
+
child.stderr.on('data', d => stderr += d);
|
|
54
|
+
child.on('close', (code) => {
|
|
55
|
+
if (code === 0)
|
|
56
|
+
resolve();
|
|
57
|
+
else
|
|
58
|
+
reject(new Error(stderr || `SANE failed with code ${code}`));
|
|
59
|
+
});
|
|
60
|
+
child.on('error', reject);
|
|
61
|
+
});
|
|
62
|
+
// Conversion
|
|
63
|
+
let sipsFormat = options.format;
|
|
64
|
+
if (options.format === 'jpg')
|
|
65
|
+
sipsFormat = 'jpeg';
|
|
66
|
+
await (0, utils_1.runCommand)('sips', ['-s', 'format', sipsFormat, tempTiffPath, '--out', outputPath]);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
throw new errors_1.ScanError('SCAN_FAILED', 'SANE scan failed', e.message);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (fs_1.default.existsSync(tempTiffPath))
|
|
73
|
+
fs_1.default.unlinkSync(tempTiffPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.SaneEngine = SaneEngine;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScanError = void 0;
|
|
4
|
+
class ScanError extends Error {
|
|
5
|
+
constructor(code, message, details, httpStatus = 500) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
this.httpStatus = httpStatus;
|
|
10
|
+
this.name = 'ScanError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.ScanError = ScanError;
|
|
@@ -2,85 +2,57 @@ export class Scan2Form {
|
|
|
2
2
|
constructor(bridgeUrl = 'http://127.0.0.1:3000') {
|
|
3
3
|
this.bridgeUrl = bridgeUrl;
|
|
4
4
|
}
|
|
5
|
-
// Rule 5.1: Detect Bridge
|
|
6
5
|
async isAvailable() {
|
|
7
6
|
try {
|
|
8
7
|
const res = await fetch(`${this.bridgeUrl}/health`);
|
|
9
|
-
|
|
8
|
+
if (res.ok)
|
|
9
|
+
return { success: true };
|
|
10
|
+
return { success: false, error: res.statusText };
|
|
10
11
|
}
|
|
11
12
|
catch (e) {
|
|
12
13
|
return { success: false, error: e.message || "Network Error" };
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
|
-
// List available scanners
|
|
16
16
|
async getDevices() {
|
|
17
17
|
try {
|
|
18
18
|
const res = await fetch(`${this.bridgeUrl}/devices`);
|
|
19
|
-
if (!res.ok)
|
|
20
|
-
return { devices: [], error: res.statusText };
|
|
21
19
|
const data = await res.json();
|
|
22
|
-
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
return { devices: [], error: data.error || data.message || res.statusText };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
devices: Array.isArray(data.devices)
|
|
25
|
+
? data.devices.map((d) => typeof d === 'string' ? d : d.name)
|
|
26
|
+
: [],
|
|
27
|
+
error: data.error
|
|
28
|
+
};
|
|
23
29
|
}
|
|
24
30
|
catch (e) {
|
|
25
31
|
return { devices: [], error: "Bridge unreachable" };
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (typeof options === 'string') {
|
|
33
|
-
config = { targetInputId: options };
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
config = { ...options };
|
|
37
|
-
}
|
|
38
|
-
// Rule 5.6: Auto-detect format from Input "accept" attribute if not specified
|
|
39
|
-
if (!config.format && config.targetInputId) {
|
|
40
|
-
const input = document.getElementById(config.targetInputId);
|
|
41
|
-
if (input && input.accept) {
|
|
42
|
-
const accept = input.accept.toLowerCase();
|
|
43
|
-
if (accept.includes('image/png')) {
|
|
44
|
-
config.format = 'png';
|
|
45
|
-
}
|
|
46
|
-
else if (accept.includes('image/jpeg') || accept.includes('image/jpg') || accept.includes('image/*')) {
|
|
47
|
-
config.format = 'jpeg'; // Default image format
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Default to PDF
|
|
52
|
-
if (!config.format)
|
|
53
|
-
config.format = 'pdf';
|
|
34
|
+
async scanToInput(inputId, options) {
|
|
35
|
+
const inputElement = document.getElementById(inputId);
|
|
36
|
+
if (!inputElement)
|
|
37
|
+
throw new Error("Input element not found");
|
|
54
38
|
try {
|
|
55
39
|
const response = await fetch(`${this.bridgeUrl}/scan`, {
|
|
56
40
|
method: 'POST',
|
|
57
41
|
headers: { 'Content-Type': 'application/json' },
|
|
58
|
-
body: JSON.stringify(
|
|
42
|
+
body: JSON.stringify(options || {})
|
|
59
43
|
});
|
|
60
44
|
if (!response.ok) {
|
|
61
|
-
const errData = await response.json();
|
|
62
|
-
throw new Error(errData.details || "Scan failed
|
|
45
|
+
const errData = await response.json().catch(() => ({}));
|
|
46
|
+
throw new Error(errData.error || errData.details || "Scan failed");
|
|
63
47
|
}
|
|
64
48
|
const blob = await response.blob();
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
// Rule 5.4: Inject into DataTransfer
|
|
68
|
-
const ext = config.format === 'jpeg' ? 'jpg' : config.format;
|
|
49
|
+
const mimeType = blob.type || 'application/pdf';
|
|
50
|
+
const ext = mimeType === 'image/jpeg' ? 'jpg' : (mimeType.split('/')[1] || 'pdf');
|
|
69
51
|
const file = new File([blob], `scanned_doc_${Date.now()}.${ext}`, { type: mimeType });
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const dataTransfer = new DataTransfer();
|
|
75
|
-
dataTransfer.items.add(file);
|
|
76
|
-
inputElement.files = dataTransfer.files;
|
|
77
|
-
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Handle Preview
|
|
81
|
-
if (config.previewElementId) {
|
|
82
|
-
this.handlePreview(config.previewElementId, file);
|
|
83
|
-
}
|
|
52
|
+
const dataTransfer = new DataTransfer();
|
|
53
|
+
dataTransfer.items.add(file);
|
|
54
|
+
inputElement.files = dataTransfer.files;
|
|
55
|
+
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
84
56
|
return { success: true, file: file };
|
|
85
57
|
}
|
|
86
58
|
catch (error) {
|
|
@@ -88,31 +60,4 @@ export class Scan2Form {
|
|
|
88
60
|
return { success: false, error: error.message || "An unknown error occurred during scan." };
|
|
89
61
|
}
|
|
90
62
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Alias for scan() to maintain backward compatibility, but now supports options.
|
|
93
|
-
*/
|
|
94
|
-
async scanToInput(inputIdOrOptions) {
|
|
95
|
-
return this.scan(inputIdOrOptions);
|
|
96
|
-
}
|
|
97
|
-
handlePreview(elementId, file) {
|
|
98
|
-
const el = document.getElementById(elementId);
|
|
99
|
-
if (!el)
|
|
100
|
-
return;
|
|
101
|
-
const url = URL.createObjectURL(file);
|
|
102
|
-
const tagName = el.tagName.toLowerCase();
|
|
103
|
-
if (tagName === 'img') {
|
|
104
|
-
el.src = url;
|
|
105
|
-
}
|
|
106
|
-
else if (tagName === 'iframe') {
|
|
107
|
-
el.src = url;
|
|
108
|
-
}
|
|
109
|
-
else if (tagName === 'embed') {
|
|
110
|
-
el.src = url;
|
|
111
|
-
el.type = file.type;
|
|
112
|
-
}
|
|
113
|
-
else if (tagName === 'object') {
|
|
114
|
-
el.data = url;
|
|
115
|
-
el.type = file.type;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
63
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/scanner-client.js
CHANGED
|
@@ -5,85 +5,57 @@ class Scan2Form {
|
|
|
5
5
|
constructor(bridgeUrl = 'http://127.0.0.1:3000') {
|
|
6
6
|
this.bridgeUrl = bridgeUrl;
|
|
7
7
|
}
|
|
8
|
-
// Rule 5.1: Detect Bridge
|
|
9
8
|
async isAvailable() {
|
|
10
9
|
try {
|
|
11
10
|
const res = await fetch(`${this.bridgeUrl}/health`);
|
|
12
|
-
|
|
11
|
+
if (res.ok)
|
|
12
|
+
return { success: true };
|
|
13
|
+
return { success: false, error: res.statusText };
|
|
13
14
|
}
|
|
14
15
|
catch (e) {
|
|
15
16
|
return { success: false, error: e.message || "Network Error" };
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
// List available scanners
|
|
19
19
|
async getDevices() {
|
|
20
20
|
try {
|
|
21
21
|
const res = await fetch(`${this.bridgeUrl}/devices`);
|
|
22
|
-
if (!res.ok)
|
|
23
|
-
return { devices: [], error: res.statusText };
|
|
24
22
|
const data = await res.json();
|
|
25
|
-
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
return { devices: [], error: data.error || data.message || res.statusText };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
devices: Array.isArray(data.devices)
|
|
28
|
+
? data.devices.map((d) => typeof d === 'string' ? d : d.name)
|
|
29
|
+
: [],
|
|
30
|
+
error: data.error
|
|
31
|
+
};
|
|
26
32
|
}
|
|
27
33
|
catch (e) {
|
|
28
34
|
return { devices: [], error: "Bridge unreachable" };
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (typeof options === 'string') {
|
|
36
|
-
config = { targetInputId: options };
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
config = { ...options };
|
|
40
|
-
}
|
|
41
|
-
// Rule 5.6: Auto-detect format from Input "accept" attribute if not specified
|
|
42
|
-
if (!config.format && config.targetInputId) {
|
|
43
|
-
const input = document.getElementById(config.targetInputId);
|
|
44
|
-
if (input && input.accept) {
|
|
45
|
-
const accept = input.accept.toLowerCase();
|
|
46
|
-
if (accept.includes('image/png')) {
|
|
47
|
-
config.format = 'png';
|
|
48
|
-
}
|
|
49
|
-
else if (accept.includes('image/jpeg') || accept.includes('image/jpg') || accept.includes('image/*')) {
|
|
50
|
-
config.format = 'jpeg'; // Default image format
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// Default to PDF
|
|
55
|
-
if (!config.format)
|
|
56
|
-
config.format = 'pdf';
|
|
37
|
+
async scanToInput(inputId, options) {
|
|
38
|
+
const inputElement = document.getElementById(inputId);
|
|
39
|
+
if (!inputElement)
|
|
40
|
+
throw new Error("Input element not found");
|
|
57
41
|
try {
|
|
58
42
|
const response = await fetch(`${this.bridgeUrl}/scan`, {
|
|
59
43
|
method: 'POST',
|
|
60
44
|
headers: { 'Content-Type': 'application/json' },
|
|
61
|
-
body: JSON.stringify(
|
|
45
|
+
body: JSON.stringify(options || {})
|
|
62
46
|
});
|
|
63
47
|
if (!response.ok) {
|
|
64
|
-
const errData = await response.json();
|
|
65
|
-
throw new Error(errData.details || "Scan failed
|
|
48
|
+
const errData = await response.json().catch(() => ({}));
|
|
49
|
+
throw new Error(errData.error || errData.details || "Scan failed");
|
|
66
50
|
}
|
|
67
51
|
const blob = await response.blob();
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
// Rule 5.4: Inject into DataTransfer
|
|
71
|
-
const ext = config.format === 'jpeg' ? 'jpg' : config.format;
|
|
52
|
+
const mimeType = blob.type || 'application/pdf';
|
|
53
|
+
const ext = mimeType === 'image/jpeg' ? 'jpg' : (mimeType.split('/')[1] || 'pdf');
|
|
72
54
|
const file = new File([blob], `scanned_doc_${Date.now()}.${ext}`, { type: mimeType });
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const dataTransfer = new DataTransfer();
|
|
78
|
-
dataTransfer.items.add(file);
|
|
79
|
-
inputElement.files = dataTransfer.files;
|
|
80
|
-
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// Handle Preview
|
|
84
|
-
if (config.previewElementId) {
|
|
85
|
-
this.handlePreview(config.previewElementId, file);
|
|
86
|
-
}
|
|
55
|
+
const dataTransfer = new DataTransfer();
|
|
56
|
+
dataTransfer.items.add(file);
|
|
57
|
+
inputElement.files = dataTransfer.files;
|
|
58
|
+
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
87
59
|
return { success: true, file: file };
|
|
88
60
|
}
|
|
89
61
|
catch (error) {
|
|
@@ -91,32 +63,5 @@ class Scan2Form {
|
|
|
91
63
|
return { success: false, error: error.message || "An unknown error occurred during scan." };
|
|
92
64
|
}
|
|
93
65
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Alias for scan() to maintain backward compatibility, but now supports options.
|
|
96
|
-
*/
|
|
97
|
-
async scanToInput(inputIdOrOptions) {
|
|
98
|
-
return this.scan(inputIdOrOptions);
|
|
99
|
-
}
|
|
100
|
-
handlePreview(elementId, file) {
|
|
101
|
-
const el = document.getElementById(elementId);
|
|
102
|
-
if (!el)
|
|
103
|
-
return;
|
|
104
|
-
const url = URL.createObjectURL(file);
|
|
105
|
-
const tagName = el.tagName.toLowerCase();
|
|
106
|
-
if (tagName === 'img') {
|
|
107
|
-
el.src = url;
|
|
108
|
-
}
|
|
109
|
-
else if (tagName === 'iframe') {
|
|
110
|
-
el.src = url;
|
|
111
|
-
}
|
|
112
|
-
else if (tagName === 'embed') {
|
|
113
|
-
el.src = url;
|
|
114
|
-
el.type = file.type;
|
|
115
|
-
}
|
|
116
|
-
else if (tagName === 'object') {
|
|
117
|
-
el.data = url;
|
|
118
|
-
el.type = file.type;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
66
|
}
|
|
122
67
|
exports.Scan2Form = Scan2Form;
|
package/dist/types.js
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCommand = runCommand;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
function runCommand(command, args, timeoutMs = config_1.CONFIG.SCAN_TIMEOUT_MS) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = (0, child_process_1.spawn)(command, args);
|
|
9
|
+
let stdout = '';
|
|
10
|
+
let stderr = '';
|
|
11
|
+
const timer = setTimeout(() => {
|
|
12
|
+
child.kill();
|
|
13
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms: ${command} ${args.join(' ')}`));
|
|
14
|
+
}, timeoutMs);
|
|
15
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
16
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
17
|
+
child.on('close', (code) => {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
if (code === 0) {
|
|
20
|
+
resolve(stdout);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
reject(new Error(stderr || `Command failed with code ${code}`));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
child.on('error', (err) => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
reject(err);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|