Skip to main content

zpl_forge/tools/
mod.rs

1//! High-level decoding and data manipulation utilities for ZPL assets.
2
3#[cfg(any(feature = "png", feature = "pdf"))]
4use crate::ZplError;
5#[cfg(any(feature = "png", feature = "pdf"))]
6use crate::ZplResult;
7#[cfg(any(feature = "png", feature = "pdf"))]
8use image::GenericImageView;
9
10/// Decodes ZPL compressed image data, typically used in the `^GF` (Graphic Field) command.
11///
12/// This function handles:
13/// - Hexadecimal nibbles.
14/// - Repeat characters: `G..Y` (1..19) and `g..z` (20..400).
15/// - Special row markers: `:` (repeat previous row), `,` (fill rest of row with 0x00), and `!` (fill rest of row with 0xFF).
16///
17/// It is commonly used when parsing raw ZPL streams to reconstruct the bitmap image intended for printing.
18///
19/// # Arguments
20/// * `encoded_str` - The ZPL-encoded string.
21/// * `bytes_per_row` - The number of bytes in a single row of the image.
22pub fn zpl_decode(encoded_str: &str, bytes_per_row: usize) -> Vec<u8> {
23    let mut output: Vec<u8> = Vec::new();
24    let mut multiplier: usize = 0;
25    let mut high_nibble: Option<u8> = None;
26    let mut last_was_row_terminator = false;
27
28    // Safety limit to avoid OOM from malformed input or memory exhaustion attacks.
29    const MAX_DECODED_SIZE: usize = 10 * 1024 * 1024; // 10MB limit
30
31    for c in encoded_str.chars() {
32        if output.len() > MAX_DECODED_SIZE {
33            break;
34        }
35
36        match c {
37            'G'..='Y' => multiplier = multiplier.saturating_add((c as usize - 'G' as usize) + 1),
38            'g'..='z' => {
39                multiplier = multiplier.saturating_add(((c as usize - 'g' as usize) + 1) * 20)
40            }
41
42            ':' => {
43                if let Some(high) = high_nibble {
44                    output.push(high << 4);
45                    high_nibble = None;
46                }
47
48                if bytes_per_row > 0 {
49                    let current_row_pos = output.len() % bytes_per_row;
50                    let total_repeats = if multiplier == 0 { 1 } else { multiplier };
51                    let mut repeats_done = 0;
52
53                    if current_row_pos > 0 {
54                        let missing = bytes_per_row - current_row_pos;
55                        let current_row_start = output.len() - current_row_pos;
56
57                        if current_row_start >= bytes_per_row {
58                            let prev_row_start = current_row_start - bytes_per_row;
59                            let copy_start = prev_row_start + current_row_pos;
60                            let copy_end = prev_row_start + bytes_per_row;
61
62                            let suffix = output[copy_start..copy_end].to_vec();
63                            output.extend_from_slice(&suffix);
64                        } else {
65                            output.extend(std::iter::repeat_n(0x00, missing));
66                        }
67                        repeats_done += 1;
68                    }
69
70                    if repeats_done < total_repeats {
71                        let remaining = total_repeats - repeats_done;
72                        // Avoid massive memory allocations
73                        let remaining = remaining.min(1000);
74
75                        if output.len() >= bytes_per_row {
76                            let start = output.len() - bytes_per_row;
77                            let last_row = output[start..].to_vec();
78                            for _ in 0..remaining {
79                                if output.len() + last_row.len() > MAX_DECODED_SIZE {
80                                    break;
81                                }
82                                output.extend_from_slice(&last_row);
83                            }
84                        } else {
85                            let empty_row = vec![0u8; bytes_per_row];
86                            for _ in 0..remaining {
87                                if output.len() + empty_row.len() > MAX_DECODED_SIZE {
88                                    break;
89                                }
90                                output.extend_from_slice(&empty_row);
91                            }
92                        }
93                    }
94                }
95                multiplier = 0;
96                last_was_row_terminator = true;
97            }
98
99            c if c.is_ascii_hexdigit() => {
100                let val = c.to_digit(16).unwrap_or(0) as u8;
101                let count = if multiplier == 0 { 1 } else { multiplier };
102                multiplier = 0;
103
104                // Protection against extreme repeat counts
105                let count = count.min(10000);
106
107                for _ in 0..count {
108                    if output.len() >= MAX_DECODED_SIZE {
109                        break;
110                    }
111                    if let Some(high) = high_nibble {
112                        output.push((high << 4) | val);
113                        high_nibble = None;
114                    } else {
115                        high_nibble = Some(val);
116                    }
117                }
118                last_was_row_terminator = false;
119            }
120
121            ',' => {
122                if let Some(high) = high_nibble {
123                    output.push(high << 4);
124                    high_nibble = None;
125                }
126
127                if bytes_per_row > 0 {
128                    let current_row_pos = output.len() % bytes_per_row;
129                    if current_row_pos != 0 {
130                        let padding = bytes_per_row - current_row_pos;
131                        output.extend(std::iter::repeat_n(0x00, padding));
132                    } else if last_was_row_terminator {
133                        output.extend(std::iter::repeat_n(0x00, bytes_per_row));
134                    }
135                }
136                multiplier = 0;
137                last_was_row_terminator = true;
138            }
139
140            '!' => {
141                if let Some(high) = high_nibble {
142                    output.push((high << 4) | 0x0F);
143                    high_nibble = None;
144                }
145
146                if bytes_per_row > 0 {
147                    let current_row_pos = output.len() % bytes_per_row;
148                    if current_row_pos != 0 {
149                        let padding = bytes_per_row - current_row_pos;
150                        output.extend(std::iter::repeat_n(0xFF, padding));
151                    } else if last_was_row_terminator {
152                        output.extend(std::iter::repeat_n(0xFF, bytes_per_row));
153                    }
154                }
155                multiplier = 0;
156                last_was_row_terminator = true;
157            }
158
159            _ => {}
160        }
161    }
162
163    if let Some(high) = high_nibble {
164        output.push(high << 4);
165    }
166
167    output
168}
169
170/// Encodes raw image bytes into a ZPL-compatible hexadecimal string for use with the `^GF` command.
171///
172/// This function converts common image formats (PNG, JPEG, etc.) to a black-and-white bitmap (1 bit per pixel).
173/// It applies Zebra's standard ASCII compression (repeat characters G-z) to reduce string size.
174/// A pixel is considered black (1) if its luminance is below 50%, otherwise it is white (0).
175///
176/// This is commonly used to embed custom logos, icons, or external graphics into a ZPL label format.
177///
178/// # Arguments
179/// * `image_bytes` - The raw bytes of the image (e.g., from a file).
180///
181/// # Returns
182/// A `ZplResult` containing a tuple with:
183/// 1. The encoded string (hexadecimal with ASCII compression).
184/// 2. Total number of bytes in the bitmap.
185/// 3. Bytes per row (required by the `^GF` command).
186#[cfg(any(feature = "png", feature = "pdf"))]
187pub fn zpl_encode(image_bytes: &[u8]) -> ZplResult<(String, usize, usize)> {
188    let img = image::load_from_memory(image_bytes)
189        .map_err(|e| ZplError::ImageError(format!("Failed to load image from bytes: {}", e)))?;
190
191    let (width, height) = img.dimensions();
192    let luma_img = img.to_luma8();
193    let bytes_per_row = (width as usize).div_ceil(8);
194    let total_bytes = bytes_per_row * height as usize;
195    let mut bitmap = vec![0u8; total_bytes];
196
197    for (y, row) in luma_img.rows().enumerate() {
198        let row_offset = y * bytes_per_row;
199        for (x, pixel) in row.enumerate() {
200            // In ZPL ^GF: 1 is black, 0 is white.
201            // luminance < 128 means dark/black.
202            if pixel.0[0] < 128 {
203                let byte_idx = row_offset + (x / 8);
204                let bit_idx = 7 - (x % 8);
205                bitmap[byte_idx] |= 1 << bit_idx;
206            }
207        }
208    }
209
210    const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF";
211    let mut hex_str = String::with_capacity(bitmap.len() * 2);
212    for byte in &bitmap {
213        hex_str.push(HEX_UPPER[(byte >> 4) as usize] as char);
214        hex_str.push(HEX_UPPER[(byte & 0x0F) as usize] as char);
215    }
216    let mut encoded = String::new();
217    let chars: Vec<char> = hex_str.chars().collect();
218
219    let mut i = 0;
220    while i < chars.len() {
221        let mut count = 1;
222        while i + count < chars.len() && chars[i + count] == chars[i] && count < 400 {
223            count += 1;
224        }
225
226        if count > 1 {
227            let mut remaining = count;
228
229            // Use multiples of 20 (g-z)
230            while remaining >= 20 {
231                let factor = (remaining / 20).min(20);
232                let repeat_char = (b'g' + (factor as u8) - 1) as char;
233                encoded.push(repeat_char);
234                remaining -= factor * 20;
235            }
236
237            // Use units (G-Y)
238            if remaining > 0 {
239                let repeat_char = (b'G' + (remaining as u8) - 1) as char;
240                encoded.push(repeat_char);
241            }
242
243            encoded.push(chars[i]);
244        } else {
245            encoded.push(chars[i]);
246        }
247
248        i += count;
249    }
250
251    Ok((encoded, total_bytes, bytes_per_row))
252}