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 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
- ## 🖼️ Supported Formats & Preview
34
- **Scan2Form** now supports multiple formats:
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
- 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>`.
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
 
@@ -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
- const PORT = 3000; // Localhost only
17
- // Rule 9.1: Bind only to localhost (enforced in listen)
18
- // Rule 9.3: Secure temp directory
19
- const TEMP_DIR = path_1.default.join(__dirname, 'temp_scans');
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
- // Rule 4.2: Health Check
25
- app.get('/health', (req, res) => {
26
- res.json({ status: "ok", engine: "NAPS2-Wrapper", version: "1.0.0" });
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
- // --- Scanner Engine Selection ---
32
- // We check for 'naps2.console' first, then 'scanimage' (SANE).
33
- function getScannerEngine(callback) {
34
- (0, child_process_1.exec)('naps2.console --help', (err) => {
35
- if (!err)
36
- return callback('naps2');
37
- (0, child_process_1.exec)('scanimage --version', (err2) => {
38
- if (!err2)
39
- return callback('sane');
40
- callback(null);
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
- // 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" });
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
- getScannerEngine((engine) => {
94
- if (!engine) {
95
- return res.status(500).json({ error: "No scanner software installed (NAPS2 or SANE)." });
96
- }
97
- if (engine === 'naps2') {
98
- // NAPS2 detects format by extension
99
- const cmd = `naps2.console -o "${finalFilePath}" -v`;
100
- console.log(`Scanning with NAPS2 (${format}): ${cmd}`);
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 if (engine === 'sane') {
119
- // SANE flow: scanimage -> tiff -> sips -> target format
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, '127.0.0.1', () => {
160
- console.log(`Scan2Form Bridge running at http://127.0.0.1:${PORT}`);
161
- console.log(`Open Example: http://127.0.0.1:${PORT}/example/index.html`);
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;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
- return { success: res.ok };
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
- return { devices: data.devices || [], error: data.error };
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
- // Rule 5.2 & 5.3: Trigger Scan & Receive Blob
29
- async scan(options) {
30
- // Backward compatibility: if string, treat as inputId
31
- let config = {};
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({ format: config.format })
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 or cancelled");
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
- // Determine mime type based on format or blob
66
- const mimeType = blob.type || (config.format === 'pdf' ? 'application/pdf' : `image/${config.format}`);
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
- // Handle Input Population
71
- if (config.targetInputId) {
72
- const inputElement = document.getElementById(config.targetInputId);
73
- if (inputElement) {
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 {};
@@ -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
- return { success: res.ok };
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
- return { devices: data.devices || [], error: data.error };
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
- // Rule 5.2 & 5.3: Trigger Scan & Receive Blob
32
- async scan(options) {
33
- // Backward compatibility: if string, treat as inputId
34
- let config = {};
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({ format: config.format })
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 or cancelled");
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
- // Determine mime type based on format or blob
69
- const mimeType = blob.type || (config.format === 'pdf' ? 'application/pdf' : `image/${config.format}`);
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
- // Handle Input Population
74
- if (config.targetInputId) {
75
- const inputElement = document.getElementById(config.targetInputId);
76
- if (inputElement) {
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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scan2form",
3
- "version": "1.2.3",
3
+ "version": "1.3.0-dev.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": {