1use image::{
2 ImageEncoder,
3 codecs::{jpeg::JpegEncoder, png::PngEncoder},
4};
5
6use crate::{Color, CorrectionLevel, Error, Logo, Result, qr_matrix::QrMatrix};
7
8pub struct RenderedImage {
10 pub pixels: Vec<u8>,
12 pub width: u32,
13 pub height: u32,
14}
15
16impl RenderedImage {
17 pub fn to_png(&self) -> Result<Vec<u8>> {
19 let mut buf = Vec::new();
20 PngEncoder::new(&mut buf)
21 .write_image(
22 &self.pixels,
23 self.width,
24 self.height,
25 image::ExtendedColorType::Rgba8,
26 )
27 .map_err(|e| Error::ImageEncode(e.to_string()))?;
28 Ok(buf)
29 }
30
31 pub fn to_jpeg(&self, quality: u8) -> Result<Vec<u8>> {
33 let rgb: Vec<u8> = self
35 .pixels
36 .chunks_exact(4)
37 .flat_map(|px| [px[0], px[1], px[2]])
38 .collect();
39 let mut buf = Vec::new();
40 JpegEncoder::new_with_quality(&mut buf, quality)
41 .write_image(
42 &rgb,
43 self.width,
44 self.height,
45 image::ExtendedColorType::Rgb8,
46 )
47 .map_err(|e| Error::ImageEncode(e.to_string()))?;
48 Ok(buf)
49 }
50}
51
52pub fn render_qr(
62 message: &[u8],
63 correction: CorrectionLevel,
64 size: u32,
65 fg: Color,
66 bg: Color,
67 quiet_zone: u32,
68 logo: Option<&Logo>,
69) -> Result<RenderedImage> {
70 let matrix = QrMatrix::encode(message, correction)?;
71 render_from_matrix(&matrix, size, fg, bg, quiet_zone, logo)
72}
73
74pub fn render_ur_qr(
79 ur_string: &str,
80 correction: CorrectionLevel,
81 size: u32,
82 fg: Color,
83 bg: Color,
84 quiet_zone: u32,
85 logo: Option<&Logo>,
86) -> Result<RenderedImage> {
87 let upper = ur_string.to_ascii_uppercase();
88 render_qr(upper.as_bytes(), correction, size, fg, bg, quiet_zone, logo)
89}
90
91pub(crate) fn render_from_matrix(
94 matrix: &QrMatrix,
95 size: u32,
96 fg: Color,
97 bg: Color,
98 quiet_zone: u32,
99 logo: Option<&Logo>,
100) -> Result<RenderedImage> {
101 let qr_modules = matrix.width();
102 let total_modules = qr_modules + 2 * quiet_zone as usize;
103 let pixels_per_module = (size as usize / total_modules).max(1);
104 let compositing_size = total_modules * pixels_per_module;
105 let qz_px = quiet_zone as usize * pixels_per_module;
106
107 let mut pixels = vec![0u8; compositing_size * compositing_size * 4];
109 for px in pixels.chunks_exact_mut(4) {
111 px[0] = bg.r;
112 px[1] = bg.g;
113 px[2] = bg.b;
114 px[3] = bg.a;
115 }
116
117 for row in 0..qr_modules {
119 for col in 0..qr_modules {
120 let color = if matrix.is_dark(col, row) { fg } else { bg };
121 let px = qz_px + col * pixels_per_module;
122 let py = qz_px + row * pixels_per_module;
123 fill_rect(
124 &mut pixels,
125 compositing_size,
126 px,
127 py,
128 pixels_per_module,
129 pixels_per_module,
130 color,
131 );
132 }
133 }
134
135 if let Some(logo) = logo {
137 composite_logo(
138 &mut pixels,
139 compositing_size,
140 qr_modules,
141 pixels_per_module,
142 qz_px,
143 bg,
144 logo,
145 );
146 }
147
148 let pixels = if compositing_size as u32 != size {
150 nearest_neighbor_scale(
151 &pixels,
152 compositing_size as u32,
153 compositing_size as u32,
154 size,
155 size,
156 )
157 } else {
158 pixels
159 };
160
161 Ok(RenderedImage { pixels, width: size, height: size })
162}
163
164fn fill_rect(
166 pixels: &mut [u8],
167 stride: usize,
168 x: usize,
169 y: usize,
170 w: usize,
171 h: usize,
172 color: Color,
173) {
174 for row in y..y + h {
175 for col in x..x + w {
176 let offset = (row * stride + col) * 4;
177 pixels[offset] = color.r;
178 pixels[offset + 1] = color.g;
179 pixels[offset + 2] = color.b;
180 pixels[offset + 3] = color.a;
181 }
182 }
183}
184
185fn composite_logo(
190 pixels: &mut [u8],
191 compositing_size: usize,
192 module_count: usize,
193 pixels_per_module: usize,
194 qz_px: usize,
195 bg: Color,
196 logo: &Logo,
197) {
198 let layout =
199 LogoLayout::new(module_count, logo.fraction, logo.clear_border);
200
201 if layout.logo_modules == 0 {
202 return;
203 }
204
205 let clear_color = if bg.is_transparent() {
207 Color::WHITE
208 } else {
209 bg
210 };
211
212 let center_module = module_count as f64 / 2.0;
213 let qr_px = module_count * pixels_per_module;
214
215 let start_module = (module_count - layout.cleared_modules) / 2;
217 match logo.clear_shape {
218 crate::LogoClearShape::Square => {
219 let clear_pixels = layout.cleared_modules * pixels_per_module;
220 let clear_origin = qz_px + (qr_px - clear_pixels) / 2;
221 fill_rect(
222 pixels,
223 compositing_size,
224 clear_origin,
225 clear_origin,
226 clear_pixels,
227 clear_pixels,
228 clear_color,
229 );
230 }
231 crate::LogoClearShape::Circle => {
232 let radius = layout.cleared_modules as f64 / 2.0;
233 for row in 0..layout.cleared_modules {
234 for col in 0..layout.cleared_modules {
235 let mx = (start_module + col) as f64 + 0.5;
236 let my = (start_module + row) as f64 + 0.5;
237 let dx = mx - center_module;
238 let dy = my - center_module;
239 if dx * dx + dy * dy <= radius * radius {
240 let px =
241 qz_px + (start_module + col) * pixels_per_module;
242 let py =
243 qz_px + (start_module + row) * pixels_per_module;
244 fill_rect(
245 pixels,
246 compositing_size,
247 px,
248 py,
249 pixels_per_module,
250 pixels_per_module,
251 clear_color,
252 );
253 }
254 }
255 }
256 }
257 }
258
259 let logo_pixels = layout.logo_modules * pixels_per_module;
261 let logo_origin = qz_px + (qr_px - logo_pixels) / 2;
262
263 let scaled = bilinear_scale(
266 &logo.pixels,
267 logo.width,
268 logo.height,
269 logo_pixels as u32,
270 logo_pixels as u32,
271 );
272
273 for row in 0..logo_pixels {
275 for col in 0..logo_pixels {
276 let src_offset = (row * logo_pixels + col) * 4;
277 let dst_x = logo_origin + col;
278 let dst_y = logo_origin + row;
279 let dst_offset = (dst_y * compositing_size + dst_x) * 4;
280
281 let sa = scaled[src_offset + 3] as u32;
282 if sa == 0 {
283 continue;
284 }
285 if sa == 255 {
286 pixels[dst_offset] = scaled[src_offset];
287 pixels[dst_offset + 1] = scaled[src_offset + 1];
288 pixels[dst_offset + 2] = scaled[src_offset + 2];
289 pixels[dst_offset + 3] = 255;
290 } else {
291 let da = pixels[dst_offset + 3] as u32;
292 let inv_sa = 255 - sa;
293 let out_a = sa + da * inv_sa / 255;
294 if out_a > 0 {
295 for c in 0..3 {
296 let sc = scaled[src_offset + c] as u32;
297 let dc = pixels[dst_offset + c] as u32;
298 pixels[dst_offset + c] =
299 ((sc * sa + dc * da * inv_sa / 255) / out_a) as u8;
300 }
301 pixels[dst_offset + 3] = out_a.min(255) as u8;
302 }
303 }
304 }
305 }
306}
307
308struct LogoLayout {
311 logo_modules: usize,
312 cleared_modules: usize,
313}
314
315impl LogoLayout {
316 fn new(module_count: usize, fraction: f64, clear_border: usize) -> Self {
317 let mut logo = (module_count as f64 * fraction).round() as usize;
318 if logo.is_multiple_of(2) {
320 logo += 1;
321 }
322 let mut cleared = logo + 2 * clear_border;
323 let max_cleared = (module_count as f64 * 0.40).floor() as usize;
325 if cleared > max_cleared {
326 cleared = max_cleared;
327 logo = cleared.saturating_sub(2 * clear_border);
328 }
329 if logo.is_multiple_of(2) && logo > 0 {
331 logo -= 1;
332 }
333 Self { logo_modules: logo, cleared_modules: cleared }
334 }
335}
336
337fn nearest_neighbor_scale(
339 src: &[u8],
340 src_w: u32,
341 src_h: u32,
342 dst_w: u32,
343 dst_h: u32,
344) -> Vec<u8> {
345 let mut dst = vec![0u8; (dst_w * dst_h * 4) as usize];
346 for y in 0..dst_h {
347 let sy = (y * src_h / dst_h).min(src_h - 1);
348 for x in 0..dst_w {
349 let sx = (x * src_w / dst_w).min(src_w - 1);
350 let si = (sy * src_w + sx) as usize * 4;
351 let di = (y * dst_w + x) as usize * 4;
352 dst[di..di + 4].copy_from_slice(&src[si..si + 4]);
353 }
354 }
355 dst
356}
357
358fn bilinear_scale(
360 src: &[u8],
361 src_w: u32,
362 src_h: u32,
363 dst_w: u32,
364 dst_h: u32,
365) -> Vec<u8> {
366 let mut dst = vec![0u8; (dst_w * dst_h * 4) as usize];
367 for y in 0..dst_h {
368 let fy = y as f64 * (src_h - 1) as f64 / (dst_h - 1).max(1) as f64;
369 let y0 = fy.floor() as u32;
370 let y1 = (y0 + 1).min(src_h - 1);
371 let wy = fy - y0 as f64;
372
373 for x in 0..dst_w {
374 let fx = x as f64 * (src_w - 1) as f64 / (dst_w - 1).max(1) as f64;
375 let x0 = fx.floor() as u32;
376 let x1 = (x0 + 1).min(src_w - 1);
377 let wx = fx - x0 as f64;
378
379 let i00 = (y0 * src_w + x0) as usize * 4;
380 let i10 = (y0 * src_w + x1) as usize * 4;
381 let i01 = (y1 * src_w + x0) as usize * 4;
382 let i11 = (y1 * src_w + x1) as usize * 4;
383
384 let di = (y * dst_w + x) as usize * 4;
385 for c in 0..4 {
386 let v = src[i00 + c] as f64 * (1.0 - wx) * (1.0 - wy)
387 + src[i10 + c] as f64 * wx * (1.0 - wy)
388 + src[i01 + c] as f64 * (1.0 - wx) * wy
389 + src[i11 + c] as f64 * wx * wy;
390 dst[di + c] = v.round() as u8;
391 }
392 }
393 }
394 dst
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn logo_layout_basic() {
403 let l = LogoLayout::new(25, 0.25, 1);
404 assert_eq!(l.logo_modules, 7);
406 assert_eq!(l.cleared_modules, 9);
408 }
409
410 #[test]
411 fn logo_layout_cap_at_40_pct() {
412 let l = LogoLayout::new(21, 0.5, 2);
414 assert!(l.cleared_modules <= 8);
417 }
418
419 #[test]
420 fn render_basic_qr() {
421 let img = render_qr(
422 b"HELLO",
423 CorrectionLevel::Low,
424 256,
425 Color::BLACK,
426 Color::WHITE,
427 1,
428 None,
429 )
430 .unwrap();
431 assert_eq!(img.width, 256);
432 assert_eq!(img.height, 256);
433 assert_eq!(img.pixels.len(), 256 * 256 * 4);
434 }
435
436 #[test]
437 fn render_to_png() {
438 let img = render_qr(
439 b"TEST",
440 CorrectionLevel::Medium,
441 128,
442 Color::BLACK,
443 Color::WHITE,
444 1,
445 None,
446 )
447 .unwrap();
448 let png = img.to_png().unwrap();
449 assert_eq!(&png[..4], &[137, 80, 78, 71]);
451 }
452
453 #[test]
454 fn render_ur_qr_uppercases() {
455 let img = render_ur_qr(
456 "ur:bytes/hdcxdwinvezm",
457 CorrectionLevel::Low,
458 256,
459 Color::BLACK,
460 Color::WHITE,
461 1,
462 None,
463 )
464 .unwrap();
465 assert_eq!(img.width, 256);
466 }
467}