1#[cfg(not(test))]
2use log::{debug, warn};
3#[cfg(test)]
4use std::{println as debug, println as warn};
5
6use std::{fs, path};
7use thiserror::Error;
8
9pub type Error = Box<dyn std::error::Error + Send + Sync>;
10pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Error, Debug)]
13pub enum CsvToQrError {
14 #[error("Parsing failure")]
15 ParseError,
16 #[error("Something weird happened")]
17 RuntimeError,
18}
19
20#[derive(Debug)]
21pub struct Record {
22 title: String,
23 value: String,
24 intermediate_path: path::PathBuf,
25 final_path: path::PathBuf,
26}
27
28const CALLING_CODE: &[u8] = include_bytes!("resources/CallingCode-Regular.ttf");
29
30fn calc_output_path(title: &str, base_path: &path::Path, extension: &str) -> path::PathBuf {
31 let mut path = path::PathBuf::from(base_path);
32 let file_name = title.replace(' ', "_");
33 let file_name_encoded = urlencoding::encode(&file_name);
34 path.push(file_name_encoded.to_string());
35 path.set_extension(extension);
36
37 path
38}
39
40pub fn parse_records(csv_path: &path::Path, output_base_path: &path::Path) -> Result<Vec<Record>> {
41 let mut records = Vec::new();
42
43 let mut rdr = csv::Reader::from_path(csv_path)?;
45 for result in rdr.records() {
46 let record = result?;
49 debug!("{:?}", record);
50
51 if record.len() != 2 {
52 return Err(Box::new(CsvToQrError::ParseError));
53 }
54
55 let title = record[0].trim().to_string();
56 records.push(Record {
57 title: title.clone(),
58 value: record[1].trim().to_string(),
59 intermediate_path: calc_output_path(&title, output_base_path, "png"),
60 final_path: calc_output_path(&title, output_base_path, "pdf"),
61 })
62 }
63
64 Ok(records)
65}
66
67pub fn generate_qrs(records: &Vec<Record>, ecc_level: qrcode_generator::QrCodeEcc) -> Result<()> {
68 for r in records {
69 debug!("processing {:?}", r);
70
71 qrcode_generator::to_png_to_file(r.value.clone(), ecc_level, 1024, &r.intermediate_path)?;
72 }
73
74 Ok(())
75}
76
77pub fn generate_pdf(records: &Vec<Record>, save_intermediate: bool) -> Result<()> {
78 let font_data = genpdf::fonts::FontData::new(CALLING_CODE.to_vec(), None)?;
79 let font_family = genpdf::fonts::FontFamily {
80 regular: font_data.clone(),
81 bold: font_data.clone(),
82 italic: font_data.clone(),
83 bold_italic: font_data,
84 };
85
86 for r in records {
87 let mut doc = genpdf::Document::new(font_family.clone());
89 doc.set_title(r.title.clone());
91 let mut decorator = genpdf::SimplePageDecorator::new();
93 decorator.set_margins(20);
94 doc.set_page_decorator(decorator);
95 let image = genpdf::elements::Image::from_path(&r.intermediate_path)
98 .expect("Failed to load image")
99 .with_alignment(genpdf::Alignment::Center); doc.push(image);
101
102 let mut label = genpdf::elements::Paragraph::new(r.title.clone());
103 label.set_alignment(genpdf::Alignment::Center); doc.push(label);
105
106 doc.render_to_file(&r.final_path)
108 .expect("Failed to write PDF file");
109
110 if !save_intermediate {
111 match fs::remove_file(&r.intermediate_path) {
112 Ok(_) => {}
113 Err(e) => {
114 warn!("failed to delete {:?}, {}", r.intermediate_path, e);
115 }
116 }
117 }
118 }
119
120 Ok(())
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_parse_records() -> Result<()> {
129 let mut example_path = project_root::get_project_root()?;
130 example_path.push("example/example.csv");
131
132 let output_base_path = project_root::get_project_root()?;
133
134 let records = parse_records(&example_path, &output_base_path)?;
135 assert_eq!(4, records.len());
136 assert_eq!(records[0].title, "Hack the planet");
137 assert_eq!(records[0].value, "https://youtu.be/u3CKgkyc7Qo");
138 assert_eq!(records[1].title, "Prodigy - Mind Fields");
139 assert_eq!(records[1].value, "https://youtu.be/7mKieArPRkw");
140 assert_eq!(records[2].title, "I am not a martyr I'm a problem");
141 assert_eq!(
142 records[2].value,
143 "https://youtu.be/7Azv0G85lh8?si=awP06dwWDUcuOBaD&t=46"
144 );
145 assert_eq!(records[3].title, "This is what I do");
146 assert_eq!(records[3].value, "https://youtu.be/YPL41OkVABk");
147
148 Ok(())
149 }
150
151 #[test]
152 fn test_generate_qrs() -> Result<()> {
153 let mut example_path = project_root::get_project_root()?;
154 example_path.push("example/example.csv");
155
156 let tmp_dir = tempdir::TempDir::new("csv2qr")?;
157
158 let records = parse_records(&example_path, &tmp_dir.path())?;
159 let ecc_levels = [
160 qrcode_generator::QrCodeEcc::Low,
161 qrcode_generator::QrCodeEcc::Medium,
162 qrcode_generator::QrCodeEcc::High,
163 qrcode_generator::QrCodeEcc::Quartile,
164 ];
165 for ecc in ecc_levels {
166 generate_qrs(&records, ecc)?;
167
168 for r in &records {
169 let img = image::open(&r.intermediate_path)?;
170
171 let decoder = bardecoder::default_decoder();
173
174 let results = decoder.decode(&img);
175 assert_eq!(1, results.len());
176 assert_eq!(r.value, *results[0].as_ref().unwrap());
177 }
178 }
179
180 Ok(())
181 }
182}