zpl_forge/tools/
mod.rs

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