@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/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
+ }
@@ -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!');