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