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 #[cfg(feature = "pdf")]
202 pub fn to_pdf(&self) -> ZplResult<Vec<u8>> {
203 self.render(
204 crate::forge::pdf_native::PdfNativeBackend::new(),
205 &HashMap::new(),
206 )
207 }
208
209 #[cfg(feature = "png")]
214 pub fn to_png(&self) -> ZplResult<Vec<u8>> {
215 self.render(crate::forge::png::PngBackend::new(), &HashMap::new())
216 }
217
218 fn render_instructions<B: backend::ZplForgeBackend>(
220 &self,
221 backend: &mut B,
222 variables: &HashMap<String, String>,
223 font_manager: &FontManager,
224 ) -> ZplResult<()> {
225 fn replace_vars<'a>(
226 s: &'a str,
227 variables: &HashMap<String, String>,
228 ) -> std::borrow::Cow<'a, str> {
229 if variables.is_empty() || !s.contains("{{") {
230 return std::borrow::Cow::Borrowed(s);
231 }
232
233 let mut result = String::new();
234 let mut last_pos = 0;
235 let mut found = false;
236 let mut cursor = 0;
237
238 while let Some(start_offset) = s[cursor..].find("{{") {
239 let start = cursor + start_offset;
240 if let Some(end_offset) = s[start + 2..].find("}}") {
241 let end = start + 2 + end_offset;
242 let key = &s[start + 2..end];
243 if let Some(value) = variables.get(key) {
244 if !found {
245 result.reserve(s.len());
246 found = true;
247 }
248 result.push_str(&s[last_pos..start]);
249 result.push_str(value);
250 last_pos = end + 2;
251 cursor = last_pos;
252 continue;
253 }
254 }
255 cursor = start + 2;
256 }
257
258 if found {
259 result.push_str(&s[last_pos..]);
260 std::borrow::Cow::Owned(result)
261 } else {
262 std::borrow::Cow::Borrowed(s)
263 }
264 }
265
266 for instruction in &self.instructions {
267 if let common::ZplInstruction::PageBreak = instruction {
268 backend.new_page()?;
269 continue;
270 }
271
272 let condition = match instruction {
273 common::ZplInstruction::PageBreak => continue,
274 common::ZplInstruction::Text { condition, .. } => condition,
275 common::ZplInstruction::GraphicBox { condition, .. } => condition,
276 common::ZplInstruction::GraphicCircle { condition, .. } => condition,
277 common::ZplInstruction::GraphicEllipse { condition, .. } => condition,
278 common::ZplInstruction::GraphicField { condition, .. } => condition,
279 common::ZplInstruction::CustomImage { condition, .. } => condition,
280 common::ZplInstruction::Code128 { condition, .. } => condition,
281 common::ZplInstruction::QRCode { condition, .. } => condition,
282 common::ZplInstruction::Code39 { condition, .. } => condition,
283 common::ZplInstruction::DataMatrix { condition, .. } => condition,
284 common::ZplInstruction::Pdf417 { condition, .. } => condition,
285 common::ZplInstruction::Barcode1D { condition, .. } => condition,
286 common::ZplInstruction::GraphicDiagonal { condition, .. } => condition,
287 common::ZplInstruction::MicroPdf417 { condition, .. } => condition,
288 common::ZplInstruction::AztecCode { condition, .. } => condition,
289 };
290
291 if let Some((var, expected)) = condition
292 && variables.get(var) != Some(expected)
293 {
294 continue;
295 }
296
297 match instruction {
298 common::ZplInstruction::PageBreak => {}
299 common::ZplInstruction::Text {
300 condition: _,
301 x,
302 y,
303 font,
304 height,
305 width,
306 orientation,
307 text,
308 reverse_print,
309 color,
310 block,
311 } => {
312 let resolved = replace_vars(text, variables);
313
314 let Some(b) = block else {
315 backend.draw_text(
316 *x,
317 *y,
318 *font,
319 *height,
320 *width,
321 *orientation,
322 &resolved,
323 *reverse_print,
324 color.clone(),
325 )?;
326 continue;
327 };
328
329 let measure =
332 |s: &str| measure_text_dots(font_manager, *font, *height, *width, s);
333 let lines = wrap_text_block(&resolved, b.width, measure);
334 let n_lines = lines.len().min(b.max_lines.max(1) as usize);
335
336 let font_h = height.unwrap_or(9) as i32;
337 let line_advance = (font_h + b.line_spacing).max(1);
338 let block_span = (n_lines as i32 - 1) * line_advance;
339
340 for (i, line) in lines.iter().take(n_lines).enumerate() {
341 if line.is_empty() {
342 continue;
343 }
344 let lw = measure(line) as i32;
345 let indent = if i > 0 { b.indent as i32 } else { 0 };
346 let avail = (b.width as i32 - indent).max(0);
347 let jx = indent
348 + match b.justification {
349 'C' => (avail - lw).max(0) / 2,
350 'R' => (avail - lw).max(0),
351 _ => 0,
352 };
353 let ly = i as i32 * line_advance;
354
355 let (dx, dy) = match orientation {
357 'R' => (block_span - ly, jx),
358 'I' => (b.width as i32 - jx - lw, block_span - ly),
359 'B' => (ly, b.width as i32 - jx - lw),
360 _ => (jx, ly),
361 };
362
363 let fx = (*x as i32 + dx).max(0) as u32;
364 let fy = (*y as i32 + dy).max(0) as u32;
365 backend.draw_text(
366 fx,
367 fy,
368 *font,
369 *height,
370 *width,
371 *orientation,
372 line,
373 *reverse_print,
374 color.clone(),
375 )?;
376 }
377 }
378 common::ZplInstruction::GraphicBox {
379 condition: _,
380 x,
381 y,
382 width,
383 height,
384 thickness,
385 color,
386 custom_color,
387 rounding,
388 reverse_print,
389 } => {
390 backend.draw_graphic_box(
391 *x,
392 *y,
393 *width,
394 *height,
395 *thickness,
396 *color,
397 custom_color.clone(),
398 *rounding,
399 *reverse_print,
400 )?;
401 }
402 common::ZplInstruction::GraphicCircle {
403 condition: _,
404 x,
405 y,
406 radius,
407 thickness,
408 color,
409 custom_color,
410 reverse_print,
411 } => {
412 backend.draw_graphic_circle(
413 *x,
414 *y,
415 *radius,
416 *thickness,
417 *color,
418 custom_color.clone(),
419 *reverse_print,
420 )?;
421 }
422 common::ZplInstruction::GraphicEllipse {
423 condition: _,
424 x,
425 y,
426 width,
427 height,
428 thickness,
429 color,
430 custom_color,
431 reverse_print,
432 } => {
433 backend.draw_graphic_ellipse(
434 *x,
435 *y,
436 *width,
437 *height,
438 *thickness,
439 *color,
440 custom_color.clone(),
441 *reverse_print,
442 )?;
443 }
444 common::ZplInstruction::GraphicField {
445 condition: _,
446 x,
447 y,
448 width,
449 height,
450 data,
451 reverse_print,
452 } => {
453 backend.draw_graphic_field(*x, *y, *width, *height, data, *reverse_print)?;
454 }
455 common::ZplInstruction::Code128 {
456 condition: _,
457 x,
458 y,
459 orientation,
460 height,
461 module_width,
462 interpretation_line,
463 interpretation_line_above,
464 check_digit,
465 mode,
466 data,
467 reverse_print,
468 } => {
469 backend.draw_code128(
470 *x,
471 *y,
472 *orientation,
473 *height,
474 *module_width,
475 *interpretation_line,
476 *interpretation_line_above,
477 *check_digit,
478 *mode,
479 &replace_vars(data, variables),
480 *reverse_print,
481 )?;
482 }
483 common::ZplInstruction::QRCode {
484 condition: _,
485 x,
486 y,
487 orientation,
488 model,
489 magnification,
490 error_correction,
491 mask,
492 data,
493 reverse_print,
494 } => {
495 backend.draw_qr_code(
496 *x,
497 *y,
498 *orientation,
499 *model,
500 *magnification,
501 *error_correction,
502 *mask,
503 &replace_vars(data, variables),
504 *reverse_print,
505 )?;
506 }
507 common::ZplInstruction::Barcode1D {
508 condition: _,
509 kind,
510 x,
511 y,
512 orientation,
513 height,
514 module_width,
515 ratio,
516 check_digit,
517 interpretation_line,
518 interpretation_line_above,
519 data,
520 reverse_print,
521 } => {
522 backend.draw_barcode_1d(
523 *kind,
524 *x,
525 *y,
526 *orientation,
527 *height,
528 *module_width,
529 *ratio,
530 *check_digit,
531 *interpretation_line,
532 *interpretation_line_above,
533 &replace_vars(data, variables),
534 *reverse_print,
535 )?;
536 }
537 common::ZplInstruction::GraphicDiagonal {
538 condition: _,
539 x,
540 y,
541 width,
542 height,
543 thickness,
544 color,
545 custom_color,
546 diagonal_orientation,
547 reverse_print,
548 } => {
549 backend.draw_graphic_diagonal(
550 *x,
551 *y,
552 *width,
553 *height,
554 *thickness,
555 *color,
556 custom_color.clone(),
557 *diagonal_orientation,
558 *reverse_print,
559 )?;
560 }
561 common::ZplInstruction::DataMatrix {
562 condition: _,
563 x,
564 y,
565 orientation,
566 module_size,
567 data,
568 reverse_print,
569 } => {
570 backend.draw_datamatrix(
571 *x,
572 *y,
573 *orientation,
574 *module_size,
575 &replace_vars(data, variables),
576 *reverse_print,
577 )?;
578 }
579 common::ZplInstruction::Pdf417 {
580 condition: _,
581 x,
582 y,
583 orientation,
584 row_height,
585 module_width,
586 security_level,
587 data,
588 reverse_print,
589 } => {
590 backend.draw_pdf417(
591 *x,
592 *y,
593 *orientation,
594 *row_height,
595 *module_width,
596 *security_level,
597 &replace_vars(data, variables),
598 *reverse_print,
599 )?;
600 }
601 common::ZplInstruction::Code39 {
602 condition: _,
603 x,
604 y,
605 orientation,
606 check_digit,
607 height,
608 module_width,
609 ratio,
610 interpretation_line,
611 interpretation_line_above,
612 data,
613 reverse_print,
614 } => {
615 backend.draw_code39(
616 *x,
617 *y,
618 *orientation,
619 *check_digit,
620 *height,
621 *module_width,
622 *ratio,
623 *interpretation_line,
624 *interpretation_line_above,
625 &replace_vars(data, variables),
626 *reverse_print,
627 )?;
628 }
629 common::ZplInstruction::CustomImage {
630 condition: _,
631 x,
632 y,
633 width,
634 height,
635 data,
636 } => {
637 backend.draw_graphic_image_custom(*x, *y, *width, *height, data)?;
638 }
639 common::ZplInstruction::MicroPdf417 {
640 condition: _,
641 x,
642 y,
643 orientation,
644 height,
645 mode,
646 data,
647 reverse_print: _,
648 } => {
649 backend.draw_micropdf417(
650 *x,
651 *y,
652 *orientation,
653 *height,
654 *mode,
655 &replace_vars(data, variables),
656 false,
657 )?;
658 }
659 common::ZplInstruction::AztecCode {
660 condition: _,
661 x,
662 y,
663 orientation,
664 magnification,
665 data,
666 reverse_print: _,
667 } => {
668 backend.draw_aztec_code(
669 *x,
670 *y,
671 *orientation,
672 *magnification,
673 &replace_vars(data, variables),
674 false,
675 )?;
676 }
677 }
678 }
679
680 Ok(())
681 }
682}