label-converter 3.0.0

Turn HTML string into png for label printing
Documentation
use clap::ArgMatches;
use regex::Regex;
use std::{collections::HashMap, fs};

pub struct Arguments {
    pub header: String,
    pub body: String,
    pub footer: String,
    pub width: usize,
    pub height: usize,
    pub debug: bool,
    pub black: bool,
}

struct LabelConfig {
    test_css: String,
    real_css: String,
    header: String,
    paragraphs: Vec<String>,
    footer: String,
    max_height: usize,
    black: bool,
}

impl From<ArgMatches> for Arguments {
    fn from(a: ArgMatches) -> Self {
        let header: String = a
            .get_one::<String>("header")
            .unwrap_or(&"".to_string())
            .to_owned();
        let body = a.get_one::<String>("body").unwrap().to_owned();
        let footer: String = a
            .get_one::<String>("footer")
            .unwrap_or(&"".to_string())
            .to_owned();
        let width = a.get_one::<String>("width").unwrap().parse().unwrap();
        let height = a.get_one::<String>("height").unwrap().parse().unwrap();
        let debug = a.get_flag("debug");
        let black = a.get_flag("black");

        Self {
            header,
            body,
            footer,
            width,
            height,
            debug,
            black,
        }
    }
}

pub fn create(args: Arguments) -> Vec<Vec<u8>> {
    // Create CSS for clearer HTML
    let test_css = format!(
        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>"#,
        width = args.width,
    );

    let real_css = format!(
        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>"#,
        width = args.width,
        height = args.height
    );

    // Add UTF-8 tag and line below head text
    let header = if !args.header.is_empty() {
        format!(
            r#"<div id="div"><div id="header">{}<div class="border"></div></div>"#,
            args.header
        )
    } else {
        r#"<div id="div"><div id="header"></div>"#.to_string()
    };

    // Create footer
    let footer = if !args.footer.is_empty() {
        format!(
            r#"<div id="footer"><div class="border"></div>{}</div>"#,
            args.footer
        )
    } else {
        r#"<div id="footer"></div>"#.to_string()
    };

    // Split paragraps to list
    let paragraphs: Vec<String> = {
        let mut body = args.body.clone();
        let mut i: Vec<String> = Vec::new();
        let p = Regex::new(r"</p>|</h\d>|<br>").unwrap();

        for _x in p.captures_iter(&args.body) {
            i.push(body.drain(..p.find(&body).unwrap().end()).collect());
        }
        i
    };

    let config = LabelConfig {
        test_css,
        real_css,
        header,
        paragraphs,
        footer,
        max_height: args.height,
        black: args.black,
    };

    let labels = make_labels(config);

    if args.debug {
        // Write images to current folder
        for (nro, label) in labels.iter().enumerate() {
            fs::write(format!("label{}.png", nro), label)
                .expect("Can't create image in current folder");
        }
    }

    labels
}

fn make_labels(mut config: LabelConfig) -> Vec<Vec<u8>> {
    let mut labels: Vec<Vec<u8>> = Vec::new();
    let mut skip_count: usize = 0;
    // Create as many labels as necessary
    loop {
        let (custom_html, lines_in_use, skip_count_return) = generate_html(
            &config.paragraphs,
            &config.test_css,
            &config.header,
            &config.footer,
            &skip_count,
            config.black,
        );
        skip_count += skip_count_return;

        // Create a image for testing
        let png_data = generate_image(&custom_html);

        // Check test image size
        let png_size = imagesize::blob_size(&png_data).expect("Can't get test image size");
        if png_size.height > config.max_height {
            skip_count += 1;
            // Create the image, cannot do anything to fix fitting issue
            if config.paragraphs.len() - skip_count == 0 {
                let (custom_html, _, _) = generate_html(
                    &config.paragraphs,
                    &config.real_css,
                    &config.header,
                    &config.footer,
                    &skip_count,
                    config.black,
                );
                labels.push(generate_image(&custom_html));
                return labels;
            }
        } else {
            // Create the image in correct size
            let (custom_html, _, _) = generate_html(
                &config.paragraphs,
                &config.real_css,
                &config.header,
                &config.footer,
                &skip_count,
                config.black,
            );
            labels.push(generate_image(&custom_html));

            // Check if all paragraphs are included
            if skip_count == 0 {
                break;
            } else {
                // Reset skip counter and remove used paragraphs
                skip_count = 0;
                for _line in 0..=lines_in_use {
                    config.paragraphs.remove(0);
                }
            }
        }
    }
    labels
}

fn generate_html(
    paragraphs: &[String],
    css: &str,
    header: &str,
    footer: &str,
    skip_count: &usize,
    black: bool,
) -> (String, usize, usize) {
    let mut lines_in_use: usize = 0;
    let black_css = if black {
        r#" class="black""#.to_string()
    } else {
        String::new()
    };
    let mut i: String = format!(
        r#"<html><head><meta charset="UTF-8">{css}</head><body{black_css}>{header}"#,
        css = css,
        black_css = black_css,
        header = header
    );
    let skip_line: String = {
        if skip_count == &0 {
            String::new()
        } else {
            paragraphs[paragraphs.len() - skip_count - 1].to_string()
        }
    };
    for line in paragraphs {
        if line == &skip_line {
            break;
        }
        i = format!("{}{}", i, line);
        lines_in_use += 1;
    }
    let i = format!("{}</div>{}</body></html>", i, footer);
    (i, lines_in_use, 0)
}

fn generate_image(html: &str) -> Vec<u8> {
    let mut image_app = wkhtmlapp::ImgApp::new().expect("Failed to init image Application");
    let args = HashMap::from([("quiet", "true")]);

    let res = image_app
        .set_format(wkhtmlapp::ImgFormat::Png)
        .unwrap()
        .set_args(args)
        .unwrap()
        .run(wkhtmlapp::WkhtmlInput::Html(html), "label converter")
        .unwrap();
    std::fs::read(res).unwrap()
}