1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::{
5 FontManager, ZplError, ZplResult,
6 ast::parse_zpl,
7 engine::{backend, common, font, intr},
8};
9
10fn measure_text_dots(
12 fm: &font::FontManager,
13 font_char: char,
14 height: Option<u32>,
15 width: Option<u32>,
16 text: &str,
17) -> u32 {
18 fm.measure_text(font_char, height, width, text)
19}
20
21fn wrap_text_block<F: Fn(&str) -> u32>(text: &str, max_width: u32, measure: F) -> Vec<String> {
24 let mut lines: Vec<String> = Vec::new();
25
26 for segment in text.split("\\&") {
27 if max_width == 0 {
28 lines.push(segment.trim().to_string());
29 continue;
30 }
31
32 let mut current = String::new();
33 for word in segment.split_whitespace() {
34 let candidate = if current.is_empty() {
35 word.to_string()
36 } else {
37 format!("{} {}", current, word)
38 };
39
40 if measure(&candidate) <= max_width {
41 current = candidate;
42 continue;
43 }
44
45 if !current.is_empty() {
46 lines.push(std::mem::take(&mut current));
47 }
48
49 if measure(word) > max_width {
51 let mut piece = String::new();
52 for ch in word.chars() {
53 piece.push(ch);
54 if measure(&piece) > max_width && piece.chars().count() > 1 {
55 piece.pop();
56 lines.push(std::mem::take(&mut piece));
57 piece.push(ch);
58 }
59 }
60 current = piece;
61 } else {
62 current = word.to_string();
63 }
64 }
65 lines.push(current);
66 }
67
68 lines
69}
70
71#[derive(Debug)]
76pub struct ZplEngine {
77 instructions: Vec<common::ZplInstruction>,
78 width: common::Unit,
79 height: common::Unit,
80 resolution: common::Resolution,
81 fonts: Option<Arc<font::FontManager>>,
82}
83
84impl ZplEngine {
85 pub fn new(
96 zpl: &str,
97 width: common::Unit,
98 height: common::Unit,
99 resolution: common::Resolution,
100 ) -> ZplResult<Self> {
101 let commands = parse_zpl(zpl)?;
102 if commands.is_empty() {
103 return Err(ZplError::EmptyInput);
104 }
105
106 let instructions = intr::ZplInstructionBuilder::new(commands);
107 let instructions = instructions.build()?;
108
109 Ok(Self {
110 instructions,
111 width,
112 height,
113 resolution,
114 fonts: None,
115 })
116 }
117
118 pub fn set_fonts(&mut self, fonts: Arc<font::FontManager>) {
122 self.fonts = Some(fonts);
123 }
124
125 pub fn render<B: backend::ZplForgeBackend>(
134 &self,
135 mut backend: B,
136 variables: &HashMap<String, String>,
137 ) -> ZplResult<Vec<u8>> {
138 let w_dots = self.width.clone().to_dots(self.resolution);
139 let h_dots = self.height.clone().to_dots(self.resolution);
140 let font_manager = if let Some(fonts) = &self.fonts {
141 fonts.clone()
142 } else {
143 Arc::new(FontManager::default())
144 };
145
146 backend.setup_page(w_dots as f64, h_dots as f64, self.resolution.dpi());
147 backend.setup_font_manager(&font_manager);
148
149 self.render_instructions(&mut backend, variables, &font_manager)?;
150
151 let result = backend.finalize()?;
152
153 Ok(result)
154 }
155
156 pub fn render_pages<B: backend::ZplForgeBackend>(
166 &self,
167 mut backend: B,
168 pages_variables: &[HashMap<String, String>],
169 ) -> ZplResult<Vec<u8>> {
170 if pages_variables.is_empty() {
171 return Ok(Vec::new());
172 }
173
174 let w_dots = self.width.clone().to_dots(self.resolution);
175 let h_dots = self.height.clone().to_dots(self.resolution);
176 let font_manager = if let Some(fonts) = &self.fonts {
177 fonts.clone()
178 } else {
179 Arc::new(FontManager::default())
180 };
181
182 backend.setup_page(w_dots as f64, h_dots as f64, self.resolution.dpi());
183 backend.setup_font_manager(&font_manager);
184
185 for (page_idx, variables) in pages_variables.iter().enumerate() {
186 if page_idx > 0 {
187 backend.new_page()?;
188 }
189 self.render_instructions(&mut backend, variables, &font_manager)?;
190 }
191
192 let result = backend.finalize()?;
193
194 Ok(result)
195 }
196
197 fn render_instructions<B: backend::ZplForgeBackend>(
199 &self,
200 backend: &mut B,
201 variables: &HashMap<String, String>,
202 font_manager: &FontManager,
203 ) -> ZplResult<()> {
204 fn replace_vars<'a>(
205 s: &'a str,
206 variables: &HashMap<String, String>,
207 ) -> std::borrow::Cow<'a, str> {
208 if variables.is_empty() || !s.contains("{{") {
209 return std::borrow::Cow::Borrowed(s);
210 }
211
212 let mut result = String::new();
213 let mut last_pos = 0;
214 let mut found = false;
215 let mut cursor = 0;
216
217 while let Some(start_offset) = s[cursor..].find("{{") {
218 let start = cursor + start_offset;
219 if let Some(end_offset) = s[start + 2..].find("}}") {
220 let end = start + 2 + end_offset;
221 let key = &s[start + 2..end];
222 if let Some(value) = variables.get(key) {
223 if !found {
224 result.reserve(s.len());
225 found = true;
226 }
227 result.push_str(&s[last_pos..start]);
228 result.push_str(value);
229 last_pos = end + 2;
230 cursor = last_pos;
231 continue;
232 }
233 }
234 cursor = start + 2;
235 }
236
237 if found {
238 result.push_str(&s[last_pos..]);
239 std::borrow::Cow::Owned(result)
240 } else {
241 std::borrow::Cow::Borrowed(s)
242 }
243 }
244
245 for instruction in &self.instructions {
246 if let common::ZplInstruction::PageBreak = instruction {
247 backend.new_page()?;
248 continue;
249 }
250
251 let condition = match instruction {
252 common::ZplInstruction::PageBreak => continue,
253 common::ZplInstruction::Text { condition, .. } => condition,
254 common::ZplInstruction::GraphicBox { condition, .. } => condition,
255 common::ZplInstruction::GraphicCircle { condition, .. } => condition,
256 common::ZplInstruction::GraphicEllipse { condition, .. } => condition,
257 common::ZplInstruction::GraphicField { condition, .. } => condition,
258 common::ZplInstruction::CustomImage { condition, .. } => condition,
259 common::ZplInstruction::Code128 { condition, .. } => condition,
260 common::ZplInstruction::QRCode { condition, .. } => condition,
261 common::ZplInstruction::Code39 { condition, .. } => condition,
262 common::ZplInstruction::DataMatrix { condition, .. } => condition,
263 common::ZplInstruction::Pdf417 { condition, .. } => condition,
264 common::ZplInstruction::Barcode1D { condition, .. } => condition,
265 common::ZplInstruction::GraphicDiagonal { condition, .. } => condition,
266 };
267
268 if let Some((var, expected)) = condition
269 && variables.get(var) != Some(expected)
270 {
271 continue;
272 }
273
274 match instruction {
275 common::ZplInstruction::PageBreak => {}
276 common::ZplInstruction::Text {
277 condition: _,
278 x,
279 y,
280 font,
281 height,
282 width,
283 orientation,
284 text,
285 reverse_print,
286 color,
287 block,
288 } => {
289 let resolved = replace_vars(text, variables);
290
291 let Some(b) = block else {
292 backend.draw_text(
293 *x,
294 *y,
295 *font,
296 *height,
297 *width,
298 *orientation,
299 &resolved,
300 *reverse_print,
301 color.clone(),
302 )?;
303 continue;
304 };
305
306 let measure =
309 |s: &str| measure_text_dots(font_manager, *font, *height, *width, s);
310 let lines = wrap_text_block(&resolved, b.width, measure);
311 let n_lines = lines.len().min(b.max_lines.max(1) as usize);
312
313 let font_h = height.unwrap_or(9) as i32;
314 let line_advance = (font_h + b.line_spacing).max(1);
315 let block_span = (n_lines as i32 - 1) * line_advance;
316
317 for (i, line) in lines.iter().take(n_lines).enumerate() {
318 if line.is_empty() {
319 continue;
320 }
321 let lw = measure(line) as i32;
322 let indent = if i > 0 { b.indent as i32 } else { 0 };
323 let avail = (b.width as i32 - indent).max(0);
324 let jx = indent
325 + match b.justification {
326 'C' => (avail - lw).max(0) / 2,
327 'R' => (avail - lw).max(0),
328 _ => 0,
329 };
330 let ly = i as i32 * line_advance;
331
332 let (dx, dy) = match orientation {
334 'R' => (block_span - ly, jx),
335 'I' => (b.width as i32 - jx - lw, block_span - ly),
336 'B' => (ly, b.width as i32 - jx - lw),
337 _ => (jx, ly),
338 };
339
340 let fx = (*x as i32 + dx).max(0) as u32;
341 let fy = (*y as i32 + dy).max(0) as u32;
342 backend.draw_text(
343 fx,
344 fy,
345 *font,
346 *height,
347 *width,
348 *orientation,
349 line,
350 *reverse_print,
351 color.clone(),
352 )?;
353 }
354 }
355 common::ZplInstruction::GraphicBox {
356 condition: _,
357 x,
358 y,
359 width,
360 height,
361 thickness,
362 color,
363 custom_color,
364 rounding,
365 reverse_print,
366 } => {
367 backend.draw_graphic_box(
368 *x,
369 *y,
370 *width,
371 *height,
372 *thickness,
373 *color,
374 custom_color.clone(),
375 *rounding,
376 *reverse_print,
377 )?;
378 }
379 common::ZplInstruction::GraphicCircle {
380 condition: _,
381 x,
382 y,
383 radius,
384 thickness,
385 color,
386 custom_color,
387 reverse_print,
388 } => {
389 backend.draw_graphic_circle(
390 *x,
391 *y,
392 *radius,
393 *thickness,
394 *color,
395 custom_color.clone(),
396 *reverse_print,
397 )?;
398 }
399 common::ZplInstruction::GraphicEllipse {
400 condition: _,
401 x,
402 y,
403 width,
404 height,
405 thickness,
406 color,
407 custom_color,
408 reverse_print,
409 } => {
410 backend.draw_graphic_ellipse(
411 *x,
412 *y,
413 *width,
414 *height,
415 *thickness,
416 *color,
417 custom_color.clone(),
418 *reverse_print,
419 )?;
420 }
421 common::ZplInstruction::GraphicField {
422 condition: _,
423 x,
424 y,
425 width,
426 height,
427 data,
428 reverse_print,
429 } => {
430 backend.draw_graphic_field(*x, *y, *width, *height, data, *reverse_print)?;
431 }
432 common::ZplInstruction::Code128 {
433 condition: _,
434 x,
435 y,
436 orientation,
437 height,
438 module_width,
439 interpretation_line,
440 interpretation_line_above,
441 check_digit,
442 mode,
443 data,
444 reverse_print,
445 } => {
446 backend.draw_code128(
447 *x,
448 *y,
449 *orientation,
450 *height,
451 *module_width,
452 *interpretation_line,
453 *interpretation_line_above,
454 *check_digit,
455 *mode,
456 &replace_vars(data, variables),
457 *reverse_print,
458 )?;
459 }
460 common::ZplInstruction::QRCode {
461 condition: _,
462 x,
463 y,
464 orientation,
465 model,
466 magnification,
467 error_correction,
468 mask,
469 data,
470 reverse_print,
471 } => {
472 backend.draw_qr_code(
473 *x,
474 *y,
475 *orientation,
476 *model,
477 *magnification,
478 *error_correction,
479 *mask,
480 &replace_vars(data, variables),
481 *reverse_print,
482 )?;
483 }
484 common::ZplInstruction::Barcode1D {
485 condition: _,
486 kind,
487 x,
488 y,
489 orientation,
490 height,
491 module_width,
492 interpretation_line,
493 interpretation_line_above,
494 data,
495 reverse_print,
496 } => {
497 backend.draw_barcode_1d(
498 *kind,
499 *x,
500 *y,
501 *orientation,
502 *height,
503 *module_width,
504 *interpretation_line,
505 *interpretation_line_above,
506 &replace_vars(data, variables),
507 *reverse_print,
508 )?;
509 }
510 common::ZplInstruction::GraphicDiagonal {
511 condition: _,
512 x,
513 y,
514 width,
515 height,
516 thickness,
517 color,
518 custom_color,
519 diagonal_orientation,
520 reverse_print,
521 } => {
522 backend.draw_graphic_diagonal(
523 *x,
524 *y,
525 *width,
526 *height,
527 *thickness,
528 *color,
529 custom_color.clone(),
530 *diagonal_orientation,
531 *reverse_print,
532 )?;
533 }
534 common::ZplInstruction::DataMatrix {
535 condition: _,
536 x,
537 y,
538 orientation,
539 module_size,
540 data,
541 reverse_print,
542 } => {
543 backend.draw_datamatrix(
544 *x,
545 *y,
546 *orientation,
547 *module_size,
548 &replace_vars(data, variables),
549 *reverse_print,
550 )?;
551 }
552 common::ZplInstruction::Pdf417 {
553 condition: _,
554 x,
555 y,
556 orientation,
557 row_height,
558 module_width,
559 security_level,
560 data,
561 reverse_print,
562 } => {
563 backend.draw_pdf417(
564 *x,
565 *y,
566 *orientation,
567 *row_height,
568 *module_width,
569 *security_level,
570 &replace_vars(data, variables),
571 *reverse_print,
572 )?;
573 }
574 common::ZplInstruction::Code39 {
575 condition: _,
576 x,
577 y,
578 orientation,
579 check_digit,
580 height,
581 module_width,
582 interpretation_line,
583 interpretation_line_above,
584 data,
585 reverse_print,
586 } => {
587 backend.draw_code39(
588 *x,
589 *y,
590 *orientation,
591 *check_digit,
592 *height,
593 *module_width,
594 *interpretation_line,
595 *interpretation_line_above,
596 &replace_vars(data, variables),
597 *reverse_print,
598 )?;
599 }
600 common::ZplInstruction::CustomImage {
601 condition: _,
602 x,
603 y,
604 width,
605 height,
606 data,
607 } => {
608 backend.draw_graphic_image_custom(*x, *y, *width, *height, data)?;
609 }
610 }
611 }
612
613 Ok(())
614 }
615}