label-converter 1.0.0

Turn HTML string into png for label printing
Documentation
use clap::ArgMatches;
use headless_chrome::{protocol::page::ScreenshotFormat, Browser};
use regex::Regex;
use std::{fs, process, usize};

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

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

impl From<ArgMatches> for Arguments {
    fn from(a: ArgMatches) -> Self {
        let header = a.value_of_t_or_exit("header");
        let body = a.value_of_t_or_exit("body");
        let footer = a.value_of_t_or_exit("footer");
        let width = a.value_of_t_or_exit("width");
        let height = a.value_of_t_or_exit("height");
        let base64 = a.is_present("base64");
        let debug = a.is_present("debug");
        Self {
            header,
            body,
            footer,
            width,
            height,
            base64,
            debug,
        }
    }
}

pub fn create(args: Arguments) {
    /*
    TODO:
    - https://froala.com/online-html-editor/
    */

    // Get current work directory location for label.html file
    let html_path: String = format!(
        "file://{}/label.html",
        std::env::current_dir()
            .expect("Couldn't get current working directory")
            .to_str()
            .expect("Couldn't turn path to str")
    );

    // Create CSS for clearer HTML
    let test_css = format!(
        r#"<style>body {{ width: fit-content; height: fit-content; position: relative; }} #div {{ position: relative; }} .border {{ border-left: {width}px solid grey; height: 2px; }} #header {{ padding-top: 1px; }} #footer * {{ margin: 0; }}</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; }}</style>"#,
        width = args.width,
        height = args.height
    );

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

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

    // 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>|</h2>|</h1>").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_width: args.width,
        max_height: args.height,
        html_path,
    };

    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");
        }
    }

    // Print labels as bytes in Json list
    let mut json: String = match serde_json::to_string(&labels) {
        Ok(i) => i,
        _ => {
            eprintln!("Error creating json list");
            process::exit(1);
        }
    };
    if args.base64 {
        json = base64::encode(json);
    }
    println!("{}", json);
}

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,
        );
        skip_count += skip_count_return;

        // Save html for image testing
        save_html(custom_html);

        // Create a image for testing
        let png_data = generate_image(&config.html_path);

        // Check test image size
        let png_size = imagesize::blob_size(&png_data).expect("Can't get test image size");
        if png_size.width > config.max_width || 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,
                );
                save_html(custom_html);
                labels.push(generate_image(&config.html_path));
                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,
            );
            save_html(custom_html);
            labels.push(generate_image(&config.html_path));

            // 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,
) -> (String, usize, usize) {
    let mut lines_in_use: usize = 0;
    let mut i: String = format!(
        r#"<html><head><meta charset="UTF-8">{css}</head><body>{header}"#,
        css = 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_path: &str) -> Vec<u8> {
    // Open new browser instance
    let bro = Browser::default().expect("Can't create new headless browser instance");

    // Open new empty tab
    let tab = bro
        .wait_for_initial_tab()
        .expect("Can't open new empty tab");

    // Open label.html file
    tab.navigate_to(html_path).unwrap();

    // Take screenshot of the <body> element
    tab.wait_for_element("body")
        .expect("<body> not found in the lable.html file")
        .capture_screenshot(ScreenshotFormat::PNG)
        .expect("Can't take screenshot of the <body>")
}

fn save_html(html: String) {
    fs::write("label.html", html).expect("Can't create label.html file in current folder");
}