@supernovae-st/qrcode-ai-scanner 0.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/Cargo.toml +18 -0
- package/README.md +356 -0
- package/build.rs +5 -0
- package/index.d.ts +182 -0
- package/index.js +325 -0
- package/npm/darwin-arm64/qrcode-ai-scanner.darwin-arm64.node +0 -0
- package/npm/darwin-x64/qrcode-ai-scanner.darwin-x64.node +0 -0
- package/npm/linux-arm64-gnu/qrcode-ai-scanner.linux-arm64-gnu.node +0 -0
- package/npm/linux-x64-gnu/qrcode-ai-scanner.linux-x64-gnu.node +0 -0
- package/npm/linux-x64-musl/qrcode-ai-scanner.linux-x64-musl.node +0 -0
- package/npm/win32-x64-msvc/qrcode-ai-scanner.win32-x64-msvc.node +0 -0
- package/package.json +55 -0
- package/qrcode-ai-scanner.darwin-arm64.node +0 -0
- package/qrcode-ai-scanner.darwin-x64.node +0 -0
- package/qrcode-ai-scanner.linux-arm64-gnu.node +0 -0
- package/qrcode-ai-scanner.linux-x64-gnu.node +0 -0
- package/qrcode-ai-scanner.linux-x64-musl.node +0 -0
- package/qrcode-ai-scanner.win32-x64-msvc.node +0 -0
- package/src/lib.rs +343 -0
- package/test-helpers.mjs +43 -0
- package/test-ok.mjs +26 -0
package/src/lib.rs
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
use napi::bindgen_prelude::*;
|
|
2
|
+
use napi_derive::napi;
|
|
3
|
+
use qrcode_ai_scanner_core::{
|
|
4
|
+
decode_only as core_decode_only, validate as core_validate,
|
|
5
|
+
validate_fast as core_validate_fast, ErrorCorrectionLevel,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/// QR code validation result
|
|
9
|
+
#[napi(object)]
|
|
10
|
+
pub struct ValidationResult {
|
|
11
|
+
/// Scannability score from 0-100
|
|
12
|
+
pub score: u8,
|
|
13
|
+
/// Whether the QR code was successfully decoded
|
|
14
|
+
pub decodable: bool,
|
|
15
|
+
/// Decoded content of the QR code
|
|
16
|
+
pub content: Option<String>,
|
|
17
|
+
/// QR code version (1-40)
|
|
18
|
+
pub version: Option<u8>,
|
|
19
|
+
/// Error correction level (L, M, Q, H)
|
|
20
|
+
pub error_correction: Option<String>,
|
|
21
|
+
/// Number of modules in the QR code
|
|
22
|
+
pub modules: Option<u8>,
|
|
23
|
+
/// List of decoders that successfully decoded the QR
|
|
24
|
+
pub decoders_success: Vec<String>,
|
|
25
|
+
/// Whether original image was decodable
|
|
26
|
+
pub stress_original: bool,
|
|
27
|
+
/// Whether 50% downscaled image was decodable
|
|
28
|
+
pub stress_downscale_50: bool,
|
|
29
|
+
/// Whether 25% downscaled image was decodable
|
|
30
|
+
pub stress_downscale_25: bool,
|
|
31
|
+
/// Whether lightly blurred image was decodable
|
|
32
|
+
pub stress_blur_light: bool,
|
|
33
|
+
/// Whether medium blurred image was decodable
|
|
34
|
+
pub stress_blur_medium: bool,
|
|
35
|
+
/// Whether low contrast image was decodable
|
|
36
|
+
pub stress_low_contrast: bool,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Simple decode result (without stress tests)
|
|
40
|
+
#[napi(object)]
|
|
41
|
+
pub struct DecodeResult {
|
|
42
|
+
/// Decoded content of the QR code
|
|
43
|
+
pub content: String,
|
|
44
|
+
/// QR code version (1-40)
|
|
45
|
+
pub version: Option<u8>,
|
|
46
|
+
/// Error correction level (L, M, Q, H)
|
|
47
|
+
pub error_correction: Option<String>,
|
|
48
|
+
/// Number of modules in the QR code
|
|
49
|
+
pub modules: Option<u8>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Validate a QR code image and compute scannability score
|
|
53
|
+
///
|
|
54
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
55
|
+
/// @returns ValidationResult with score, content, and metadata
|
|
56
|
+
#[napi]
|
|
57
|
+
pub fn validate(image_buffer: Buffer) -> Result<ValidationResult> {
|
|
58
|
+
let result = core_validate(&image_buffer)
|
|
59
|
+
.map_err(|e| Error::from_reason(e.to_string()))?;
|
|
60
|
+
|
|
61
|
+
let (version, error_correction, modules, decoders_success) =
|
|
62
|
+
if let Some(ref meta) = result.metadata {
|
|
63
|
+
(
|
|
64
|
+
Some(meta.version),
|
|
65
|
+
Some(ec_to_string(meta.error_correction)),
|
|
66
|
+
Some(meta.modules),
|
|
67
|
+
meta.decoders_success.clone(),
|
|
68
|
+
)
|
|
69
|
+
} else {
|
|
70
|
+
(None, None, None, vec![])
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
Ok(ValidationResult {
|
|
74
|
+
score: result.score,
|
|
75
|
+
decodable: result.decodable,
|
|
76
|
+
content: result.content,
|
|
77
|
+
version,
|
|
78
|
+
error_correction,
|
|
79
|
+
modules,
|
|
80
|
+
decoders_success,
|
|
81
|
+
stress_original: result.stress_results.original,
|
|
82
|
+
stress_downscale_50: result.stress_results.downscale_50,
|
|
83
|
+
stress_downscale_25: result.stress_results.downscale_25,
|
|
84
|
+
stress_blur_light: result.stress_results.blur_light,
|
|
85
|
+
stress_blur_medium: result.stress_results.blur_medium,
|
|
86
|
+
stress_low_contrast: result.stress_results.low_contrast,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Fast decode without stress tests (for when you only need content)
|
|
91
|
+
///
|
|
92
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
93
|
+
/// @returns DecodeResult with content and basic metadata
|
|
94
|
+
#[napi]
|
|
95
|
+
pub fn decode(image_buffer: Buffer) -> Result<DecodeResult> {
|
|
96
|
+
let result = core_decode_only(&image_buffer)
|
|
97
|
+
.map_err(|e| Error::from_reason(e.to_string()))?;
|
|
98
|
+
|
|
99
|
+
let (version, error_correction, modules) = if let Some(ref meta) = result.metadata {
|
|
100
|
+
(
|
|
101
|
+
Some(meta.version),
|
|
102
|
+
Some(ec_to_string(meta.error_correction)),
|
|
103
|
+
Some(meta.modules),
|
|
104
|
+
)
|
|
105
|
+
} else {
|
|
106
|
+
(None, None, None)
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
Ok(DecodeResult {
|
|
110
|
+
content: result.content,
|
|
111
|
+
version,
|
|
112
|
+
error_correction,
|
|
113
|
+
modules,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Fast validation with reduced stress tests (~2x faster)
|
|
118
|
+
///
|
|
119
|
+
/// Good for real-time feedback during QR editing.
|
|
120
|
+
///
|
|
121
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
122
|
+
/// @returns ValidationResult with score, content, and metadata
|
|
123
|
+
#[napi]
|
|
124
|
+
pub fn validate_fast(image_buffer: Buffer) -> Result<ValidationResult> {
|
|
125
|
+
let result = core_validate_fast(&image_buffer)
|
|
126
|
+
.map_err(|e| Error::from_reason(e.to_string()))?;
|
|
127
|
+
|
|
128
|
+
let (version, error_correction, modules, decoders_success) =
|
|
129
|
+
if let Some(ref meta) = result.metadata {
|
|
130
|
+
(
|
|
131
|
+
Some(meta.version),
|
|
132
|
+
Some(ec_to_string(meta.error_correction)),
|
|
133
|
+
Some(meta.modules),
|
|
134
|
+
meta.decoders_success.clone(),
|
|
135
|
+
)
|
|
136
|
+
} else {
|
|
137
|
+
(None, None, None, vec![])
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
Ok(ValidationResult {
|
|
141
|
+
score: result.score,
|
|
142
|
+
decodable: result.decodable,
|
|
143
|
+
content: result.content,
|
|
144
|
+
version,
|
|
145
|
+
error_correction,
|
|
146
|
+
modules,
|
|
147
|
+
decoders_success,
|
|
148
|
+
stress_original: result.stress_results.original,
|
|
149
|
+
stress_downscale_50: result.stress_results.downscale_50,
|
|
150
|
+
stress_downscale_25: result.stress_results.downscale_25,
|
|
151
|
+
stress_blur_light: result.stress_results.blur_light,
|
|
152
|
+
stress_blur_medium: result.stress_results.blur_medium,
|
|
153
|
+
stress_low_contrast: result.stress_results.low_contrast,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Get only the scannability score (0-100)
|
|
158
|
+
///
|
|
159
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
160
|
+
/// @returns Score from 0 (unreadable) to 100 (highly scannable)
|
|
161
|
+
#[napi]
|
|
162
|
+
pub fn validate_score_only(image_buffer: Buffer) -> Result<u8> {
|
|
163
|
+
let result = core_validate(&image_buffer)
|
|
164
|
+
.map_err(|e| Error::from_reason(e.to_string()))?;
|
|
165
|
+
Ok(result.score)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Get score using fast validation (~2x faster)
|
|
169
|
+
///
|
|
170
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
171
|
+
/// @returns Score from 0 (unreadable) to 100 (highly scannable)
|
|
172
|
+
#[napi]
|
|
173
|
+
pub fn validate_score_fast(image_buffer: Buffer) -> Result<u8> {
|
|
174
|
+
let result = core_validate_fast(&image_buffer)
|
|
175
|
+
.map_err(|e| Error::from_reason(e.to_string()))?;
|
|
176
|
+
Ok(result.score)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn ec_to_string(ec: ErrorCorrectionLevel) -> String {
|
|
180
|
+
match ec {
|
|
181
|
+
ErrorCorrectionLevel::L => "L".to_string(),
|
|
182
|
+
ErrorCorrectionLevel::M => "M".to_string(),
|
|
183
|
+
ErrorCorrectionLevel::Q => "Q".to_string(),
|
|
184
|
+
ErrorCorrectionLevel::H => "H".to_string(),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// CONVENIENCE HELPERS - Simple one-liners for common tasks
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/// Simple summary of QR validation
|
|
193
|
+
#[napi(object)]
|
|
194
|
+
pub struct QrSummary {
|
|
195
|
+
/// Whether the QR is valid and decodable
|
|
196
|
+
pub valid: bool,
|
|
197
|
+
/// Scannability score (0-100)
|
|
198
|
+
pub score: u8,
|
|
199
|
+
/// Decoded content (empty if invalid)
|
|
200
|
+
pub content: String,
|
|
201
|
+
/// Error correction level (L/M/Q/H or "N/A")
|
|
202
|
+
pub error_correction: String,
|
|
203
|
+
/// Human-readable rating (Excellent/Good/Fair/Poor)
|
|
204
|
+
pub rating: String,
|
|
205
|
+
/// Whether this QR is production-ready (score >= 70)
|
|
206
|
+
pub production_ready: bool,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Check if QR code is valid (returns content or null)
|
|
210
|
+
///
|
|
211
|
+
/// @example
|
|
212
|
+
/// ```typescript
|
|
213
|
+
/// const content = isValid(buffer);
|
|
214
|
+
/// if (content) {
|
|
215
|
+
/// console.log(`QR contains: ${content}`);
|
|
216
|
+
/// }
|
|
217
|
+
/// ```
|
|
218
|
+
///
|
|
219
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
220
|
+
/// @returns Decoded content string, or null if QR is invalid
|
|
221
|
+
#[napi]
|
|
222
|
+
pub fn is_valid(image_buffer: Buffer) -> Option<String> {
|
|
223
|
+
core_decode_only(&image_buffer).ok().map(|r| r.content)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Get scannability score (0-100)
|
|
227
|
+
///
|
|
228
|
+
/// @example
|
|
229
|
+
/// ```typescript
|
|
230
|
+
/// const s = score(buffer);
|
|
231
|
+
/// console.log(`Scannability: ${s}/100`);
|
|
232
|
+
/// ```
|
|
233
|
+
///
|
|
234
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
235
|
+
/// @returns Score from 0 (unreadable) to 100 (highly scannable)
|
|
236
|
+
#[napi]
|
|
237
|
+
pub fn score(image_buffer: Buffer) -> u8 {
|
|
238
|
+
core_validate(&image_buffer)
|
|
239
|
+
.map(|r| r.score)
|
|
240
|
+
.unwrap_or(0)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Check if QR meets minimum score threshold
|
|
244
|
+
///
|
|
245
|
+
/// @example
|
|
246
|
+
/// ```typescript
|
|
247
|
+
/// if (passesThreshold(buffer, 70)) {
|
|
248
|
+
/// console.log('Production ready!');
|
|
249
|
+
/// }
|
|
250
|
+
/// ```
|
|
251
|
+
///
|
|
252
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
253
|
+
/// @param minScore - Minimum score required (0-100)
|
|
254
|
+
/// @returns true if score >= minScore
|
|
255
|
+
#[napi]
|
|
256
|
+
pub fn passes_threshold(image_buffer: Buffer, min_score: u8) -> bool {
|
|
257
|
+
score(image_buffer) >= min_score
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Get production readiness (score >= 70)
|
|
261
|
+
///
|
|
262
|
+
/// @example
|
|
263
|
+
/// ```typescript
|
|
264
|
+
/// if (isProductionReady(buffer)) {
|
|
265
|
+
/// await uploadQr(buffer);
|
|
266
|
+
/// }
|
|
267
|
+
/// ```
|
|
268
|
+
///
|
|
269
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
270
|
+
/// @returns true if QR is production-ready
|
|
271
|
+
#[napi]
|
|
272
|
+
pub fn is_production_ready(image_buffer: Buffer) -> bool {
|
|
273
|
+
passes_threshold(image_buffer, 70)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Get simple summary of QR validation
|
|
277
|
+
///
|
|
278
|
+
/// @example
|
|
279
|
+
/// ```typescript
|
|
280
|
+
/// const summary = summarize(buffer);
|
|
281
|
+
/// console.log(`${summary.rating}: ${summary.score}/100`);
|
|
282
|
+
/// if (summary.productionReady) {
|
|
283
|
+
/// console.log(`Content: ${summary.content}`);
|
|
284
|
+
/// }
|
|
285
|
+
/// ```
|
|
286
|
+
///
|
|
287
|
+
/// @param imageBuffer - Raw image bytes (PNG, JPEG, etc.)
|
|
288
|
+
/// @returns QrSummary with all key info
|
|
289
|
+
#[napi]
|
|
290
|
+
pub fn summarize(image_buffer: Buffer) -> QrSummary {
|
|
291
|
+
match core_validate(&image_buffer) {
|
|
292
|
+
Ok(result) => {
|
|
293
|
+
let score_val = result.score;
|
|
294
|
+
let rating = match score_val {
|
|
295
|
+
80..=100 => "Excellent",
|
|
296
|
+
60..=79 => "Good",
|
|
297
|
+
40..=59 => "Fair",
|
|
298
|
+
_ => "Poor",
|
|
299
|
+
}
|
|
300
|
+
.to_string();
|
|
301
|
+
|
|
302
|
+
QrSummary {
|
|
303
|
+
valid: result.decodable,
|
|
304
|
+
score: score_val,
|
|
305
|
+
content: result.content.unwrap_or_default(),
|
|
306
|
+
error_correction: result
|
|
307
|
+
.metadata
|
|
308
|
+
.map(|m| ec_to_string(m.error_correction))
|
|
309
|
+
.unwrap_or_else(|| "N/A".to_string()),
|
|
310
|
+
rating,
|
|
311
|
+
production_ready: score_val >= 70,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
Err(_) => QrSummary {
|
|
315
|
+
valid: false,
|
|
316
|
+
score: 0,
|
|
317
|
+
content: String::new(),
|
|
318
|
+
error_correction: "N/A".to_string(),
|
|
319
|
+
rating: "Invalid".to_string(),
|
|
320
|
+
production_ready: false,
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// Get human-readable rating for a score
|
|
326
|
+
///
|
|
327
|
+
/// @example
|
|
328
|
+
/// ```typescript
|
|
329
|
+
/// const rating = getRating(85); // "Excellent"
|
|
330
|
+
/// ```
|
|
331
|
+
///
|
|
332
|
+
/// @param score - Score from 0-100
|
|
333
|
+
/// @returns Rating string (Excellent/Good/Fair/Poor)
|
|
334
|
+
#[napi]
|
|
335
|
+
pub fn get_rating(score: u8) -> String {
|
|
336
|
+
match score {
|
|
337
|
+
80..=100 => "Excellent",
|
|
338
|
+
60..=79 => "Good",
|
|
339
|
+
40..=59 => "Fair",
|
|
340
|
+
_ => "Poor",
|
|
341
|
+
}
|
|
342
|
+
.to_string()
|
|
343
|
+
}
|
package/test-helpers.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
validate, decode, isValid, score,
|
|
5
|
+
passesThreshold, isProductionReady, summarize, getRating
|
|
6
|
+
} from './index.js';
|
|
7
|
+
|
|
8
|
+
const testDir = '../../test-qr-speed';
|
|
9
|
+
const files = readdirSync(testDir).filter(f => f.endsWith('.png')).slice(0, 5);
|
|
10
|
+
|
|
11
|
+
console.log('=== QRAISC Node.js Helpers Test ===\n');
|
|
12
|
+
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const path = join(testDir, file);
|
|
15
|
+
const buffer = readFileSync(path);
|
|
16
|
+
|
|
17
|
+
console.log('File: ' + file);
|
|
18
|
+
|
|
19
|
+
// Test isValid
|
|
20
|
+
const content = isValid(buffer);
|
|
21
|
+
console.log(' isValid(): ' + (content ? content.slice(0, 50) + '...' : 'null'));
|
|
22
|
+
|
|
23
|
+
// Test score
|
|
24
|
+
const s = score(buffer);
|
|
25
|
+
console.log(' score(): ' + s + '/100');
|
|
26
|
+
|
|
27
|
+
// Test getRating
|
|
28
|
+
console.log(' getRating(' + s + '): ' + getRating(s));
|
|
29
|
+
|
|
30
|
+
// Test passesThreshold
|
|
31
|
+
console.log(' passesThreshold(70): ' + passesThreshold(buffer, 70));
|
|
32
|
+
|
|
33
|
+
// Test isProductionReady
|
|
34
|
+
console.log(' isProductionReady(): ' + isProductionReady(buffer));
|
|
35
|
+
|
|
36
|
+
// Test summarize
|
|
37
|
+
const summary = summarize(buffer);
|
|
38
|
+
console.log(' summarize(): valid=' + summary.valid + ', score=' + summary.score + ', rating=' + summary.rating);
|
|
39
|
+
|
|
40
|
+
console.log('');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('All helpers working!');
|
package/test-ok.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { isValid, score, summarize, validate } from './index.js';
|
|
3
|
+
|
|
4
|
+
const file = '../../test-qr-speed/OK_1103ms_100_897e5090.png';
|
|
5
|
+
const buffer = readFileSync(file);
|
|
6
|
+
|
|
7
|
+
console.log('=== Test OK QR (score 100 expected) ===\n');
|
|
8
|
+
|
|
9
|
+
console.log('isValid():', isValid(buffer)?.slice(0, 60) + '...');
|
|
10
|
+
console.log('score():', score(buffer));
|
|
11
|
+
|
|
12
|
+
const summary = summarize(buffer);
|
|
13
|
+
console.log('summarize():', JSON.stringify(summary, null, 2));
|
|
14
|
+
|
|
15
|
+
console.log('\n=== Full validate() ===\n');
|
|
16
|
+
const result = validate(buffer);
|
|
17
|
+
console.log('score:', result.score);
|
|
18
|
+
console.log('content:', result.content?.slice(0, 60) + '...');
|
|
19
|
+
console.log('version:', result.version);
|
|
20
|
+
console.log('errorCorrection:', result.errorCorrection);
|
|
21
|
+
console.log('decodersSuccess:', result.decodersSuccess);
|
|
22
|
+
console.log('stressOriginal:', result.stressOriginal);
|
|
23
|
+
console.log('stressDownscale50:', result.stressDownscale50);
|
|
24
|
+
console.log('stressBlurLight:', result.stressBlurLight);
|
|
25
|
+
|
|
26
|
+
console.log('\nNode.js integration verified!');
|