use std::path::Path;
use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
use crate::error::{Error, Result};
use crate::ir::{IDENTITY, Node, Page, Paint, SourceMeta, Stroke};
pub(crate) fn convert(
path: &Path,
options: &ConvertOptions,
sink: &mut dyn PageConsumer,
) -> Result<Vec<String>> {
let bytes = read_limited_file(path, options.max_input_bytes, "QR input")?;
let text = String::from_utf8(bytes)
.map_err(|e| Error::InvalidInput(format!("QR file is not valid UTF-8: {e}")))?;
let clean_text = text.trim();
let page = generate_qr_svg(clean_text)?;
sink.consume(page)?;
Ok(Vec::new())
}
pub fn generate_qr_svg(payload: &str) -> Result<Page> {
let matrix_size = 25; let module_size = 12.0;
let quiet_zone = 4.0 * module_size;
let total_size = (matrix_size as f64 * module_size) + (quiet_zone * 2.0);
let mut grid = vec![vec![false; matrix_size]; matrix_size];
apply_finder_pattern(&mut grid, 0, 0);
apply_finder_pattern(&mut grid, matrix_size - 7, 0);
apply_finder_pattern(&mut grid, 0, matrix_size - 7);
#[allow(clippy::needless_range_loop)]
for i in 8..matrix_size - 8 {
grid[6][i] = i % 2 == 0;
grid[i][6] = i % 2 == 0;
}
let hash_bytes = payload.as_bytes();
let mut bit_idx = 0;
#[allow(clippy::needless_range_loop)]
for y in 0..matrix_size {
for x in 0..matrix_size {
if ((x < 8 || x >= matrix_size - 8) && y < 8) || (x < 8 && y >= matrix_size - 8) {
continue;
}
if x == 6 || y == 6 {
continue;
}
let byte = hash_bytes
.get(bit_idx % hash_bytes.len().max(1))
.copied()
.unwrap_or(0);
let bit = ((byte >> (bit_idx % 8)) & 1) == 1;
grid[y][x] = bit ^ ((x + y) % 2 == 0); bit_idx += 1;
}
}
let mut path_d = String::new();
for (y, row) in grid.iter().enumerate() {
let mut in_run = false;
let mut run_start = 0;
let py = quiet_zone + (y as f64 * module_size);
for (x, &is_black) in row.iter().enumerate() {
if is_black && !in_run {
in_run = true;
run_start = x;
} else if !is_black && in_run {
in_run = false;
let run_len = (x - run_start) as f64 * module_size;
let px = quiet_zone + (run_start as f64 * module_size);
path_d.push_str(&format!(
"M {:.1},{:.1} h {:.1} v {:.1} h -{:.1} Z ",
px, py, run_len, module_size, run_len
));
}
}
if in_run {
let run_len = (row.len() - run_start) as f64 * module_size;
let px = quiet_zone + (run_start as f64 * module_size);
path_d.push_str(&format!(
"M {:.1},{:.1} h {:.1} v {:.1} h -{:.1} Z ",
px, py, run_len, module_size, run_len
));
}
}
let mut page = Page::new(1, total_size, total_size, "qr");
page.embedded_source = Some(payload.to_string());
let bg_d = format!("M 0,0 H {:.1} V {:.1} H 0 Z", total_size, total_size);
page.nodes.push(Node::Path {
id: "background".to_string(),
d: bg_d,
fill_rule: String::new(),
fill: Paint::solid("#ffffff"),
stroke: Stroke::default(),
transform: IDENTITY,
clip_id: None,
meta: SourceMeta::default(),
});
page.nodes.push(Node::Path {
id: "modules".to_string(),
d: path_d,
fill_rule: String::new(),
fill: Paint::solid("#000000"),
stroke: Stroke::default(),
transform: IDENTITY,
clip_id: None,
meta: SourceMeta::default(),
});
Ok(page)
}
fn apply_finder_pattern(grid: &mut [Vec<bool>], start_x: usize, start_y: usize) {
for dy in 0..7 {
for dx in 0..7 {
let is_outer_border = dx == 0 || dx == 6 || dy == 0 || dy == 6;
let is_inner_center = (2..=4).contains(&dx) && (2..=4).contains(&dy);
grid[start_y + dy][start_x + dx] = is_outer_border || is_inner_center;
}
}
}