csv2qr/
lib.rs

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    // Build the CSV reader and iterate over each record.
44    let mut rdr = csv::Reader::from_path(csv_path)?;
45    for result in rdr.records() {
46        // The iterator yields Result<StringRecord, Error>, so we check the
47        // error here.
48        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        // Create a document and set the default font family
88        let mut doc = genpdf::Document::new(font_family.clone());
89        // Change the default settings
90        doc.set_title(r.title.clone());
91        // Customize the pages
92        let mut decorator = genpdf::SimplePageDecorator::new();
93        decorator.set_margins(20);
94        doc.set_page_decorator(decorator);
95        // Add one or more elements
96
97        let image = genpdf::elements::Image::from_path(&r.intermediate_path)
98            .expect("Failed to load image")
99            .with_alignment(genpdf::Alignment::Center); // Center the image on the page.
100        doc.push(image);
101
102        let mut label = genpdf::elements::Paragraph::new(r.title.clone());
103        label.set_alignment(genpdf::Alignment::Center); // Center the image on the page.
104        doc.push(label);
105
106        // Render the document and write it to a file
107        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                // Use default decoder
172                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}