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 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 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 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 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 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 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 let png_data = generate_image(&custom_html);
138
139 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 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 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 if skip_count == 0 {
170 break;
171 } else {
172 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}