label_converter/
lib.rs

1use clap::ArgMatches;
2use regex::Regex;
3use std::{collections::HashMap, fs};
4
5pub struct Arguments {
6    pub header: String,
7    pub body: String,
8    pub footer: String,
9    pub width: usize,
10    pub height: usize,
11    pub debug: bool,
12    pub black: bool,
13}
14
15struct LabelConfig {
16    test_css: String,
17    real_css: String,
18    header: String,
19    paragraphs: Vec<String>,
20    footer: String,
21    max_height: usize,
22    black: bool,
23}
24
25impl From<ArgMatches> for Arguments {
26    fn from(a: ArgMatches) -> Self {
27        let header: String = a
28            .get_one::<String>("header")
29            .unwrap_or(&"".to_string())
30            .to_owned();
31        let body = a.get_one::<String>("body").unwrap().to_owned();
32        let footer: String = a
33            .get_one::<String>("footer")
34            .unwrap_or(&"".to_string())
35            .to_owned();
36        let width = a.get_one::<String>("width").unwrap().parse().unwrap();
37        let height = a.get_one::<String>("height").unwrap().parse().unwrap();
38        let debug = a.get_flag("debug");
39        let black = a.get_flag("black");
40
41        Self {
42            header,
43            body,
44            footer,
45            width,
46            height,
47            debug,
48            black,
49        }
50    }
51}
52
53pub fn create(args: Arguments) -> Vec<Vec<u8>> {
54    // Create CSS for clearer HTML
55    let test_css = format!(
56        r#"<style>body {{ width: {width}px; height: fit-content; position: relative; }} #div {{ position: relative; }} .border {{ border-left: {width}px solid grey; height: 2px; }} #header {{ padding-top: 1px; }}</style>"#,
57        width = args.width,
58    );
59
60    let real_css = format!(
61        r#"<style>body {{ width: {width}px; height: {height}px; position: relative; }} #div {{ position: relative; }} .border {{ border-left: {width}px solid grey; height: 2px; }} #header {{ padding-top: 1px; }} #footer {{ position: absolute; bottom: 0; }} .black {{ filter: grayscale(100%); }} .black * {{ color: unset !important; background-color: unset !important; }}</style>"#,
62        width = args.width,
63        height = args.height
64    );
65
66    // Add UTF-8 tag and line below head text
67    let header = if !args.header.is_empty() {
68        format!(
69            r#"<div id="div"><div id="header">{}<div class="border"></div></div>"#,
70            args.header
71        )
72    } else {
73        r#"<div id="div"><div id="header"></div>"#.to_string()
74    };
75
76    // Create footer
77    let footer = if !args.footer.is_empty() {
78        format!(
79            r#"<div id="footer"><div class="border"></div>{}</div>"#,
80            args.footer
81        )
82    } else {
83        r#"<div id="footer"></div>"#.to_string()
84    };
85
86    // Split paragraps to list
87    let paragraphs: Vec<String> = {
88        let mut body = args.body.clone();
89        let mut i: Vec<String> = Vec::new();
90        let p = Regex::new(r"</p>|</h\d>|<br>").unwrap();
91
92        for _x in p.captures_iter(&args.body) {
93            i.push(body.drain(..p.find(&body).unwrap().end()).collect());
94        }
95        i
96    };
97
98    let config = LabelConfig {
99        test_css,
100        real_css,
101        header,
102        paragraphs,
103        footer,
104        max_height: args.height,
105        black: args.black,
106    };
107
108    let labels = make_labels(config);
109
110    if args.debug {
111        // Write images to current folder
112        for (nro, label) in labels.iter().enumerate() {
113            fs::write(format!("label{}.png", nro), label)
114                .expect("Can't create image in current folder");
115        }
116    }
117
118    labels
119}
120
121fn make_labels(mut config: LabelConfig) -> Vec<Vec<u8>> {
122    let mut labels: Vec<Vec<u8>> = Vec::new();
123    let mut skip_count: usize = 0;
124    // Create as many labels as necessary
125    loop {
126        let (custom_html, lines_in_use, skip_count_return) = generate_html(
127            &config.paragraphs,
128            &config.test_css,
129            &config.header,
130            &config.footer,
131            &skip_count,
132            config.black,
133        );
134        skip_count += skip_count_return;
135
136        // Create a image for testing
137        let png_data = generate_image(&custom_html);
138
139        // Check test image size
140        let png_size = imagesize::blob_size(&png_data).expect("Can't get test image size");
141        if png_size.height > config.max_height {
142            skip_count += 1;
143            // Create the image, cannot do anything to fix fitting issue
144            if config.paragraphs.len() - skip_count == 0 {
145                let (custom_html, _, _) = generate_html(
146                    &config.paragraphs,
147                    &config.real_css,
148                    &config.header,
149                    &config.footer,
150                    &skip_count,
151                    config.black,
152                );
153                labels.push(generate_image(&custom_html));
154                return labels;
155            }
156        } else {
157            // Create the image in correct size
158            let (custom_html, _, _) = generate_html(
159                &config.paragraphs,
160                &config.real_css,
161                &config.header,
162                &config.footer,
163                &skip_count,
164                config.black,
165            );
166            labels.push(generate_image(&custom_html));
167
168            // Check if all paragraphs are included
169            if skip_count == 0 {
170                break;
171            } else {
172                // Reset skip counter and remove used paragraphs
173                skip_count = 0;
174                for _line in 0..=lines_in_use {
175                    config.paragraphs.remove(0);
176                }
177            }
178        }
179    }
180    labels
181}
182
183fn generate_html(
184    paragraphs: &[String],
185    css: &str,
186    header: &str,
187    footer: &str,
188    skip_count: &usize,
189    black: bool,
190) -> (String, usize, usize) {
191    let mut lines_in_use: usize = 0;
192    let black_css = if black {
193        r#" class="black""#.to_string()
194    } else {
195        String::new()
196    };
197    let mut i: String = format!(
198        r#"<html><head><meta charset="UTF-8">{css}</head><body{black_css}>{header}"#,
199        css = css,
200        black_css = black_css,
201        header = header
202    );
203    let skip_line: String = {
204        if skip_count == &0 {
205            String::new()
206        } else {
207            paragraphs[paragraphs.len() - skip_count - 1].to_string()
208        }
209    };
210    for line in paragraphs {
211        if line == &skip_line {
212            break;
213        }
214        i = format!("{}{}", i, line);
215        lines_in_use += 1;
216    }
217    let i = format!("{}</div>{}</body></html>", i, footer);
218    (i, lines_in_use, 0)
219}
220
221fn generate_image(html: &str) -> Vec<u8> {
222    let mut image_app = wkhtmlapp::ImgApp::new().expect("Failed to init image Application");
223    let args = HashMap::from([("quiet", "true")]);
224
225    let res = image_app
226        .set_format(wkhtmlapp::ImgFormat::Png)
227        .unwrap()
228        .set_args(args)
229        .unwrap()
230        .run(wkhtmlapp::WkhtmlInput::Html(html), "label converter")
231        .unwrap();
232    std::fs::read(res).unwrap()
233}