Skip to content

Commit 31b04d8

Browse files
committed
add error correction
1 parent b1df9b0 commit 31b04d8

6 files changed

Lines changed: 96 additions & 15 deletions

File tree

wasm_core/src/images.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ mod tests {
530530
];
531531
let results = convert_image_batch(batch).expect("batch convert");
532532
assert_eq!(results.len(), 2);
533-
let first = results.get(0).expect("first result");
533+
let first = results.first().expect("first result");
534534
let second = results.get(1).expect("second result");
535535
assert_eq!(first.file_name, "first.png");
536536
assert!(first.error.is_none(), "unexpected error: {:?}", first.error);

wasm_core/src/lib.rs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ fn generate_ssh_key_internal(
538538
})
539539
}
540540

541-
#[derive(Deserialize, Default)]
541+
#[derive(Serialize, Deserialize, Default)]
542542
#[serde(rename_all = "camelCase")]
543543
struct QrRequest {
544544
otp_account: Option<String>,
@@ -551,9 +551,10 @@ struct QrRequest {
551551
wifi_pass: Option<String>,
552552
wifi_ssid: Option<String>,
553553
custom_string: Option<String>,
554+
qr_ecc: Option<String>,
554555
}
555556

556-
#[derive(Serialize, Debug)]
557+
#[derive(Serialize, Deserialize, Debug)]
557558
#[serde(rename_all = "camelCase")]
558559
struct QrResponse {
559560
kind: String,
@@ -612,13 +613,14 @@ fn generate_qr_code_internal(
612613
) -> Result<QrResponse, String> {
613614
let qr_kind = parse_qr_kind(kind)?;
614615
let content = build_qr_payload(qr_kind, &payload)?;
616+
let ecc_level = parse_qr_ecc_level(payload.qr_ecc.as_deref());
615617
let fmt = format.trim().to_ascii_lowercase();
616618

617619
let (data_base64, mime) = match fmt.as_str() {
618-
"png" => encode_bitmap_qr(&content, ImageFormat::Png)?,
619-
"jpg" | "jpeg" => encode_bitmap_qr(&content, ImageFormat::Jpeg)?,
620-
"webp" => encode_bitmap_qr(&content, ImageFormat::WebP)?,
621-
"svg" => encode_svg_qr(&content)?,
620+
"png" => encode_bitmap_qr(&content, ImageFormat::Png, ecc_level)?,
621+
"jpg" | "jpeg" => encode_bitmap_qr(&content, ImageFormat::Jpeg, ecc_level)?,
622+
"webp" => encode_bitmap_qr(&content, ImageFormat::WebP, ecc_level)?,
623+
"svg" => encode_svg_qr(&content, ecc_level)?,
622624
_ => return Err("format must be png, jpg, svg, or webp".into()),
623625
};
624626

@@ -751,22 +753,35 @@ fn escape_wifi_field(value: &str) -> String {
751753
escaped
752754
}
753755

754-
fn render_qr_code_matrix(content: &str) -> Result<QrCode, String> {
755-
QrCode::with_error_correction_level(content.as_bytes(), EcLevel::Q)
756+
fn parse_qr_ecc_level(raw: Option<&str>) -> EcLevel {
757+
match raw.unwrap_or("Q").trim().to_ascii_uppercase().as_str() {
758+
"L" => EcLevel::L,
759+
"M" => EcLevel::M,
760+
"H" => EcLevel::H,
761+
_ => EcLevel::Q, // Default and fallback
762+
}
763+
}
764+
765+
fn render_qr_code_matrix(content: &str, ecc: EcLevel) -> Result<QrCode, String> {
766+
QrCode::with_error_correction_level(content.as_bytes(), ecc)
756767
.map_err(|err| format!("invalid QR content: {err}"))
757768
}
758769

759-
fn render_qr_image(content: &str) -> Result<ImageBuffer<Luma<u8>, Vec<u8>>, String> {
760-
let code = render_qr_code_matrix(content)?;
770+
fn render_qr_image(content: &str, ecc: EcLevel) -> Result<ImageBuffer<Luma<u8>, Vec<u8>>, String> {
771+
let code = render_qr_code_matrix(content, ecc)?;
761772
Ok(code
762773
.render::<Luma<u8>>()
763774
.min_dimensions(QR_CODE_SIZE, QR_CODE_SIZE)
764775
.max_dimensions(QR_CODE_SIZE, QR_CODE_SIZE)
765776
.build())
766777
}
767778

768-
fn encode_bitmap_qr(content: &str, format: ImageFormat) -> Result<(String, String), String> {
769-
let image = render_qr_image(content)?;
779+
fn encode_bitmap_qr(
780+
content: &str,
781+
format: ImageFormat,
782+
ecc: EcLevel,
783+
) -> Result<(String, String), String> {
784+
let image = render_qr_image(content, ecc)?;
770785
let mut buffer = Vec::new();
771786
let mut cursor = Cursor::new(&mut buffer);
772787
DynamicImage::ImageLuma8(image)
@@ -781,8 +796,8 @@ fn encode_bitmap_qr(content: &str, format: ImageFormat) -> Result<(String, Strin
781796
Ok((STANDARD.encode(buffer), mime.into()))
782797
}
783798

784-
fn encode_svg_qr(content: &str) -> Result<(String, String), String> {
785-
let code = render_qr_code_matrix(content)?;
799+
fn encode_svg_qr(content: &str, ecc: EcLevel) -> Result<(String, String), String> {
800+
let code = render_qr_code_matrix(content, ecc)?;
786801
let svg_text = code
787802
.render::<svg::Color>()
788803
.min_dimensions(QR_CODE_SIZE, QR_CODE_SIZE)

wasm_core/src/lib_tests.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,38 @@ fn parse_qr_codes_batch_processes_multiple_images() {
258258
assert_eq!(results[1].results[0].payload, "beta");
259259
}
260260

261+
#[test]
262+
fn generate_qr_code_accepts_custom_ecc_level() {
263+
let req = QrRequest {
264+
otp_account: None,
265+
otp_secret: None,
266+
otp_issuer: None,
267+
otp_algorithm: None,
268+
otp_period: None,
269+
otp_digits: None,
270+
wifi_type: None,
271+
wifi_pass: None,
272+
wifi_ssid: None,
273+
custom_string: Some("hello-ecc".into()),
274+
qr_ecc: Some("L".into()),
275+
};
276+
277+
let res = generate_qr_code_internal("custom", "png", req).expect("generate qr");
278+
assert_eq!(res.kind, "custom");
279+
// Decode and ensure ECC level is Low (L).
280+
let bytes = base64::engine::general_purpose::STANDARD
281+
.decode(res.data_base64.as_bytes())
282+
.expect("decode png");
283+
let entries = parse_qr_codes_native(&bytes).expect("decode");
284+
assert_eq!(entries[0].payload, "hello-ecc");
285+
let ecc_lower = entries[0].ecc_level.to_lowercase();
286+
assert!(
287+
ecc_lower.contains('l') || ecc_lower.contains("low") || ecc_lower == "1",
288+
"expected ECC level Low, got {}",
289+
entries[0].ecc_level
290+
);
291+
}
292+
261293
#[test]
262294
fn convert_timestamp_internal_from_sql_datetime() {
263295
let map = convert_timestamp_internal("sql_datetime", "2025-01-02 03:04:05").unwrap();

wasm_core/tests/e2e_wasm.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,20 @@ fn qr_parse_allows_multiple_files_and_batch_results() {
212212
);
213213
}
214214

215+
#[wasm_bindgen_test]
216+
fn qr_generator_exposes_ecc_selector() {
217+
const INDEX_HTML: &str =
218+
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../www/index.html"));
219+
assert!(
220+
INDEX_HTML.contains("id=\"qrEccLevel\""),
221+
"QR generator should expose ECC level selector"
222+
);
223+
assert!(
224+
INDEX_HTML.contains("Q (25%)"),
225+
"ECC selector should default to Q label"
226+
);
227+
}
228+
215229
#[wasm_bindgen_test]
216230
fn ssl_inspector_workspace_is_wired() {
217231
const INDEX_HTML: &str =

www/index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1781,6 +1781,17 @@ <h2>QR Code</h2>
17811781
/>
17821782
</label>
17831783
</div>
1784+
<div class="qr-row">
1785+
<label>
1786+
<span>Error correction</span>
1787+
<select id="qrEccLevel">
1788+
<option value="L">L (7%)</option>
1789+
<option value="M">M (15%)</option>
1790+
<option value="Q" selected>Q (25%)</option>
1791+
<option value="H">H (30%)</option>
1792+
</select>
1793+
</label>
1794+
</div>
17841795
<div class="qr-row qr-format-row">
17851796
<label>
17861797
<span>Format</span>

www/main.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,7 @@ const state = {
838838
qr: {
839839
mode: 'otp',
840840
format: 'png',
841+
ecc: 'Q',
841842
otpAccount: '',
842843
otpSecret: '',
843844
otpIssuer: '',
@@ -1089,6 +1090,7 @@ function cacheElements() {
10891090
elements.qrParseFile = document.getElementById('qrParseFile');
10901091
elements.qrParseDrop = document.getElementById('qrParseDrop');
10911092
elements.qrParseResults = document.getElementById('qrParseResults');
1093+
elements.qrEccLevel = document.getElementById('qrEccLevel');
10921094
elements.coderWorkspace = document.getElementById('coderWorkspace');
10931095
elements.coderInput = document.getElementById('coderInput');
10941096
elements.coderResults = document.getElementById('coderResults');
@@ -1441,6 +1443,7 @@ function bindUI() {
14411443
elements.qrParseFile?.click();
14421444
});
14431445
elements.qrParseResults?.addEventListener('click', handleQrParseResultsClick);
1446+
elements.qrEccLevel?.addEventListener('change', handleQrFieldChange);
14441447
window.addEventListener('hashchange', () => syncToolFromHash(false));
14451448
elements.coderInput?.addEventListener('input', () => scheduleCoder());
14461449
elements.coderModeText?.addEventListener('change', () => setCoderInputMode('text'));
@@ -5399,6 +5402,7 @@ function activateQrTool() {
53995402
if (elements.qrWifiPass) elements.qrWifiPass.value = state.qr.wifiPass || '';
54005403
if (elements.qrCustomString) elements.qrCustomString.value = state.qr.customString || '';
54015404
if (elements.qrFormat) elements.qrFormat.value = state.qr.format || 'png';
5405+
if (elements.qrEccLevel) elements.qrEccLevel.value = state.qr.ecc || 'Q';
54025406
updateQrModeVisibility();
54035407
if (state.qr.lastResult) {
54045408
renderQrResult(state.qr.lastResult);
@@ -5454,6 +5458,9 @@ function handleQrFieldChange(event) {
54545458
case 'qrCustomString':
54555459
state.qr.customString = value;
54565460
break;
5461+
case 'qrEccLevel':
5462+
state.qr.ecc = value || 'Q';
5463+
break;
54575464
default:
54585465
break;
54595466
}
@@ -5479,6 +5486,7 @@ function buildQrPayload() {
54795486
}
54805487
return {
54815488
customString: elements.qrCustomString?.value || state.qr.customString || '',
5489+
qrEcc: elements.qrEccLevel?.value || state.qr.ecc || 'Q',
54825490
};
54835491
}
54845492

@@ -5537,6 +5545,7 @@ function renderQrResult(result) {
55375545
const meta = [];
55385546
if (result.kind) meta.push(`Kind: ${result.kind.toUpperCase()}`);
55395547
if (result.format) meta.push(`Format: ${result.format.toUpperCase()}`);
5548+
if (state.qr.ecc) meta.push(`ECC: ${state.qr.ecc}`);
55405549
meta.push(`Size: ${size} × ${height}`);
55415550
elements.qrPreview.innerHTML = `
55425551
<div class="qr-image-frame">

0 commit comments

Comments
 (0)