1use bc_ur::{MultipartEncoder, UR};
2
3use crate::{
4 Color, CorrectionLevel, Error, Logo, Result,
5 qr_matrix::{QrMatrix, check_qr_density},
6 render::{RenderedImage, render_from_matrix},
7};
8
9pub struct AnimateParams {
11 pub max_fragment_len: usize,
14 pub correction: Option<CorrectionLevel>,
17 pub size: u32,
19 pub foreground: Color,
21 pub background: Color,
23 pub quiet_zone: u32,
25 pub logo: Option<Logo>,
27 pub fps: f64,
29 pub cycles: u32,
32 pub frame_count: Option<usize>,
36 pub max_modules: Option<usize>,
39}
40
41impl Default for AnimateParams {
42 fn default() -> Self {
43 Self {
44 max_fragment_len: 40,
45 correction: None,
46 size: 512,
47 foreground: Color::BLACK,
48 background: Color::WHITE,
49 quiet_zone: 1,
50 logo: None,
51 fps: 8.0,
52 cycles: 3,
53 frame_count: None,
54 max_modules: None,
55 }
56 }
57}
58
59impl AnimateParams {
60 fn effective_correction(&self) -> CorrectionLevel {
61 self.correction.unwrap_or(if self.logo.is_some() {
62 CorrectionLevel::High
63 } else {
64 CorrectionLevel::Low
65 })
66 }
67}
68
69pub struct QrFrame {
71 pub image: RenderedImage,
73 pub index: usize,
75}
76
77pub fn generate_frames(
82 ur: &UR,
83 params: &AnimateParams,
84) -> Result<Vec<QrFrame>> {
85 let mut encoder = MultipartEncoder::new(ur, params.max_fragment_len)?;
86 let parts_count = encoder.parts_count();
87 let total_frames = if let Some(n) = params.frame_count {
88 n
89 } else {
90 parts_count * params.cycles as usize
91 };
92
93 if total_frames < parts_count {
95 return Err(Error::InsufficientFrames {
96 requested: total_frames,
97 fragments: parts_count,
98 });
99 }
100
101 let correction = params.effective_correction();
102 let mut frames = Vec::with_capacity(total_frames);
103
104 for i in 0..total_frames {
105 let part = encoder.next_part()?;
106 let index = encoder.current_index();
107 let upper = part.to_ascii_uppercase();
108 let matrix = QrMatrix::encode(upper.as_bytes(), correction)?;
109
110 if i == 0
113 && let Some(limit) = params.max_modules
114 {
115 check_qr_density(matrix.width(), limit)?;
116 }
117
118 let image = render_from_matrix(
119 &matrix,
120 params.size,
121 params.foreground,
122 params.background,
123 params.quiet_zone,
124 params.logo.as_ref(),
125 )?;
126 frames.push(QrFrame { image, index });
127 }
128
129 Ok(frames)
130}
131
132pub fn encode_animated_gif(frames: &[QrFrame], fps: f64) -> Result<Vec<u8>> {
138 if frames.is_empty() {
139 return Err(Error::InvalidParameter("no frames to encode".into()));
140 }
141
142 let width = frames[0].image.width as u16;
143 let height = frames[0].image.height as u16;
144 let delay_cs = (100.0 / fps).round() as u16; let mut buf = Vec::new();
147 {
148 let mut encoder = gif::Encoder::new(&mut buf, width, height, &[])
149 .map_err(|e| Error::GifEncode(format!("GIF init: {e}")))?;
150
151 encoder
152 .set_repeat(gif::Repeat::Infinite)
153 .map_err(|e| Error::GifEncode(format!("GIF set repeat: {e}")))?;
154
155 for frame_data in frames {
156 let rgba = &frame_data.image.pixels;
157 let (palette, indexed) =
158 quantize_frame(rgba, width as u32, height as u32);
159
160 let mut frame = gif::Frame {
161 width,
162 height,
163 delay: delay_cs,
164 palette: Some(palette),
165 ..Default::default()
166 };
167 frame.buffer = std::borrow::Cow::Owned(indexed);
168
169 encoder.write_frame(&frame).map_err(|e| {
170 Error::GifEncode(format!("GIF write frame: {e}"))
171 })?;
172 }
173 }
174
175 Ok(buf)
176}
177
178pub fn write_frame_pngs(
180 frames: &[QrFrame],
181 output_dir: &std::path::Path,
182) -> Result<()> {
183 std::fs::create_dir_all(output_dir)?;
184 for (i, frame) in frames.iter().enumerate() {
185 let path = output_dir.join(format!("{:04}.png", i));
186 let png = frame.image.to_png()?;
187 std::fs::write(&path, &png)?;
188 }
189 Ok(())
190}
191
192fn quantize_frame(rgba: &[u8], width: u32, height: u32) -> (Vec<u8>, Vec<u8>) {
197 let mut unique_colors: Vec<[u8; 4]> = Vec::new();
199 let mut seen = std::collections::HashSet::new();
200
201 for px in rgba.chunks_exact(4) {
202 let key = [px[0], px[1], px[2], px[3]];
203 if seen.insert(key) {
204 unique_colors.push(key);
205 if unique_colors.len() > 256 {
206 break;
207 }
208 }
209 }
210
211 if unique_colors.len() <= 256 {
212 let palette: Vec<u8> = unique_colors
214 .iter()
215 .flat_map(|c| [c[0], c[1], c[2]])
216 .collect();
217
218 let indexed: Vec<u8> = rgba
219 .chunks_exact(4)
220 .map(|px| {
221 unique_colors
222 .iter()
223 .position(|c| {
224 c[0] == px[0]
225 && c[1] == px[1]
226 && c[2] == px[2]
227 && c[3] == px[3]
228 })
229 .unwrap_or(0) as u8
230 })
231 .collect();
232
233 (palette, indexed)
234 } else {
235 let nq = color_quant::NeuQuant::new(10, 256, rgba);
237 let palette: Vec<u8> = (0..256)
238 .flat_map(|i| {
239 if let Some(c) = nq.lookup(i) {
240 [c[0], c[1], c[2]]
241 } else {
242 [0, 0, 0]
243 }
244 })
245 .collect();
246
247 let indexed: Vec<u8> = rgba
248 .chunks_exact(4)
249 .map(|px| nq.index_of(px) as u8)
250 .collect();
251
252 let _ = (width, height); (palette, indexed)
255 }
256}