scan2form 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yousuf Omer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Scan2Form 🖨️ -> 🌐
2
+
3
+ **Scan documents from a physical scanner directly into your web form.**
4
+
5
+ Typically, browsers can't access scanners. **Scan2Form** solves this by running a tiny local bridge on your computer.
6
+
7
+ ---
8
+
9
+ ## 🚀 Quick Start for Developers
10
+
11
+ **1. Install the package**
12
+ ```bash
13
+ npm install scan2form
14
+ ```
15
+
16
+ **2. Add to your frontend code**
17
+ ```javascript
18
+ import { Scan2Form } from 'scan2form';
19
+
20
+ const scanner = new Scan2Form();
21
+
22
+ // Triggers the scan and puts the file into <input type="file" id="my-input" />
23
+ await scanner.scanToInput('my-input');
24
+ ```
25
+
26
+ That's it! The file input is now populated with a PDF, just as if the user uploaded it manually.
27
+
28
+ ---
29
+
30
+ ## 🖥️ Setup for End-Users
31
+
32
+ To make scanning work, the user needs two things installed on their computer:
33
+
34
+ ### 1. The Scanner Engine
35
+ We rely on a local scanning engine. You can use either **NAPS2** (Recommended for Windows) or **SANE** (Recommended for macOS/Linux).
36
+
37
+ **Option A: NAPS2 (Windows)**
38
+ * **[Download NAPS2](https://github.com/cyanfish/naps2/releases#:~:text=naps2%2D8.2.1%2Dwin%2Darm64.exe)** and install it.
39
+ * **Important:** Ensure `naps2.console` is available in your system PATH.
40
+ * [How to configure NAPS2 Command Line](https://www.naps2.com/doc/command-line)
41
+
42
+ > **Tip (Windows PowerShell):** You can set up an alias to make it simple:
43
+ > ```powershell
44
+ > function naps2.console { . "C:\Program Files\NAPS2\NAPS2.Console.exe" $args }
45
+ > ```
46
+
47
+ **Option B: SANE (macOS / Linux)**
48
+ * **macOS:** Install via Homebrew: `brew install sane-backends`
49
+ * **Linux:** Install via apt: `sudo apt-get install sane-utils`
50
+ * Verify installation by running `scanimage --version` in your terminal.
51
+
52
+ ### 2. The Bridge Server
53
+ This tiny server listens for commands from your website.
54
+ ```bash
55
+ # Run this command in your terminal
56
+ npx scan2form-server
57
+ ```
58
+ * Keep this running while scanning.
59
+ * It runs locally at `http://127.0.0.1:3000`.
60
+
61
+ ---
62
+
63
+ ## 🛠️ System Requirements
64
+ * **Node.js**: v16+
65
+ * **OS**: Windows, Mac, or Linux
66
+ * **Scanner**: Any scanner supported by your OS drivers.
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.app = void 0;
8
+ const express_1 = __importDefault(require("express"));
9
+ const cors_1 = __importDefault(require("cors"));
10
+ const child_process_1 = require("child_process");
11
+ const path_1 = __importDefault(require("path"));
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const uuid_1 = require("uuid");
14
+ const app = (0, express_1.default)();
15
+ 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
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" });
27
+ });
28
+ // Serve example for testing
29
+ app.use('/example', express_1.default.static(path_1.default.join(__dirname, '../example')));
30
+ 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
+ });
80
+ });
81
+ app.post('/scan', async (req, res) => {
82
+ const scanId = (0, uuid_1.v4)();
83
+ const finalPdfPath = path_1.default.join(TEMP_DIR, `scan_${scanId}.pdf`);
84
+ getScannerEngine((engine) => {
85
+ if (!engine) {
86
+ return res.status(500).json({ error: "No scanner software installed (NAPS2 or SANE)." });
87
+ }
88
+ if (engine === 'naps2') {
89
+ const cmd = `naps2.console -o "${finalPdfPath}" -v`;
90
+ console.log(`Scanning with NAPS2: ${cmd}`);
91
+ (0, child_process_1.exec)(cmd, (error, stdout, stderr) => {
92
+ if (error) {
93
+ console.error(`NAPS2 Error: ${error.message}`);
94
+ return res.status(500).json({ error: "Scan failed", details: stderr });
95
+ }
96
+ if (fs_1.default.existsSync(finalPdfPath)) {
97
+ res.sendFile(finalPdfPath, () => {
98
+ fs_1.default.unlink(finalPdfPath, (err) => { if (err)
99
+ console.error("Cleanup error:", err); });
100
+ });
101
+ }
102
+ else {
103
+ res.status(500).json({ error: "Scan completed but PDF not found." });
104
+ }
105
+ });
106
+ }
107
+ else if (engine === 'sane') {
108
+ // SANE flow: scanimage -> tiff -> sips -> pdf
109
+ const tempTiffPath = path_1.default.join(TEMP_DIR, `scan_${scanId}.tiff`);
110
+ // Default to batch access or single scan. `scanimage --format=tiff > output.tiff`
111
+ const cmd = `scanimage --format=tiff --mode Color --resolution 300 > "${tempTiffPath}"`;
112
+ console.log(`Scanning with SANE: ${cmd}`);
113
+ (0, child_process_1.exec)(cmd, (error, stdout, stderr) => {
114
+ if (error) {
115
+ // scanimage writes progress to stderr, so it might not be a real error unless exit code != 0.
116
+ // But exec gives error on non-zero exit.
117
+ console.error(`SANE Error: ${error.message}`);
118
+ return res.status(500).json({ error: "Scan failed", details: stderr });
119
+ }
120
+ // Convert TIFF to PDF using Mac's 'sips' or ImageMagick 'convert'
121
+ // Since we are targeting Mac fallback, we use 'sips'
122
+ const convertCmd = `sips -s format pdf "${tempTiffPath}" --out "${finalPdfPath}"`;
123
+ (0, child_process_1.exec)(convertCmd, (cErr, cOut, cStderr) => {
124
+ // Cleanup TIFF immediately
125
+ if (fs_1.default.existsSync(tempTiffPath))
126
+ fs_1.default.unlinkSync(tempTiffPath);
127
+ if (cErr) {
128
+ console.error(`Conversion Error: ${cStderr}`);
129
+ return res.status(500).json({ error: "Image conversion failed" });
130
+ }
131
+ if (fs_1.default.existsSync(finalPdfPath)) {
132
+ res.sendFile(finalPdfPath, () => {
133
+ fs_1.default.unlink(finalPdfPath, (err) => { if (err)
134
+ console.error("Cleanup error:", err); });
135
+ });
136
+ }
137
+ else {
138
+ res.status(500).json({ error: "Conversion completed but PDF not found." });
139
+ }
140
+ });
141
+ });
142
+ }
143
+ });
144
+ });
145
+ if (require.main === module) {
146
+ app.listen(PORT, '127.0.0.1', () => {
147
+ console.log(`Scan2Form Bridge running at http://127.0.0.1:${PORT}`);
148
+ console.log(`Open Example: http://127.0.0.1:${PORT}/example/index.html`);
149
+ });
150
+ }
@@ -0,0 +1,54 @@
1
+ export class Scan2Form {
2
+ constructor(bridgeUrl = 'http://127.0.0.1:3000') {
3
+ this.bridgeUrl = bridgeUrl;
4
+ }
5
+ // Rule 5.1: Detect Bridge
6
+ async isAvailable() {
7
+ try {
8
+ const res = await fetch(`${this.bridgeUrl}/health`);
9
+ return res.ok;
10
+ }
11
+ catch (e) {
12
+ return false;
13
+ }
14
+ }
15
+ // List available scanners
16
+ async getDevices() {
17
+ try {
18
+ const res = await fetch(`${this.bridgeUrl}/devices`);
19
+ if (!res.ok)
20
+ return { devices: [], error: res.statusText };
21
+ const data = await res.json();
22
+ return { devices: data.devices || [], error: data.error };
23
+ }
24
+ catch (e) {
25
+ return { devices: [], error: "Bridge unreachable" };
26
+ }
27
+ }
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");
33
+ 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");
37
+ const blob = await response.blob();
38
+ // 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 }));
45
+ return { success: true, file: file };
46
+ }
47
+ catch (error) {
48
+ 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
+ return { success: false, error: error };
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Scan2Form = void 0;
4
+ class Scan2Form {
5
+ constructor(bridgeUrl = 'http://127.0.0.1:3000') {
6
+ this.bridgeUrl = bridgeUrl;
7
+ }
8
+ // Rule 5.1: Detect Bridge
9
+ async isAvailable() {
10
+ try {
11
+ const res = await fetch(`${this.bridgeUrl}/health`);
12
+ return res.ok;
13
+ }
14
+ catch (e) {
15
+ return false;
16
+ }
17
+ }
18
+ // List available scanners
19
+ async getDevices() {
20
+ try {
21
+ const res = await fetch(`${this.bridgeUrl}/devices`);
22
+ if (!res.ok)
23
+ return { devices: [], error: res.statusText };
24
+ const data = await res.json();
25
+ return { devices: data.devices || [], error: data.error };
26
+ }
27
+ catch (e) {
28
+ return { devices: [], error: "Bridge unreachable" };
29
+ }
30
+ }
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");
36
+ 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");
40
+ const blob = await response.blob();
41
+ // 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 }));
48
+ return { success: true, file: file };
49
+ }
50
+ catch (error) {
51
+ 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
+ return { success: false, error: error };
55
+ }
56
+ }
57
+ }
58
+ exports.Scan2Form = Scan2Form;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "scan2form",
3
+ "version": "1.0.0",
4
+ "description": "Local offline bridge allowing web browsers to access physical scanners (WIA, TWAIN, SANE).",
5
+ "main": "dist/scanner-client.js",
6
+ "scripts": {
7
+ "start": "node dist/bridge-server.js",
8
+ "example": "npm run build && npm start",
9
+ "dev": "nodemon src/bridge-server.ts",
10
+ "build": "tsc && tsc src/scanner-client.ts --target es2020 --module es2020 --moduleResolution node --outDir dist/esm",
11
+ "test": "jest",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "bin": {
15
+ "scan2form-server": "dist/bridge-server.js"
16
+ },
17
+ "types": "./dist/scanner-client.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "keywords": [
24
+ "scanner",
25
+ "twain",
26
+ "wia",
27
+ "sane",
28
+ "web-scanning",
29
+ "local-bridge"
30
+ ],
31
+ "author": "jodeveloper8",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "cors": "^2.8.5",
35
+ "express": "^4.18.2",
36
+ "uuid": "^9.0.1"
37
+ },
38
+ "devDependencies": {
39
+ "@types/cors": "^2.8.17",
40
+ "@types/express": "^4.17.21",
41
+ "@types/jest": "^30.0.0",
42
+ "@types/node": "^20.10.6",
43
+ "@types/supertest": "^6.0.3",
44
+ "@types/uuid": "^9.0.7",
45
+ "jest": "^30.2.0",
46
+ "nodemon": "^3.0.2",
47
+ "supertest": "^7.2.2",
48
+ "ts-jest": "^29.4.6",
49
+ "ts-node": "^10.9.2",
50
+ "typescript": "^5.3.3"
51
+ }
52
+ }