ql_label/utils.rs
1//! Utility functions for image processing and two-color printing.
2//!
3//! This module provides functions to convert various image formats to the
4//! bitmap format required by Brother P-Touch printers.
5
6use crate::Matrix;
7
8/// Container for two-color (black and red) bitmap data.
9///
10/// This structure holds separate bitmap matrices for black and red colors,
11/// used for two-color printing on compatible printers like QL-820NWB.
12///
13/// Both matrices must have identical dimensions and represent 1-bit bitmap data
14/// packed into bytes (8 pixels per byte).
15#[derive(Debug, Clone)]
16pub struct TwoColorMatrix {
17 pub black: Matrix,
18 pub red: Matrix,
19}
20
21impl TwoColorMatrix {
22 /// Create a new TwoColorMatrix from black and red bitmap data.
23 ///
24 /// # Arguments
25 /// * `black` - Matrix containing black pixel data
26 /// * `red` - Matrix containing red pixel data
27 ///
28 /// # Returns
29 /// * `Ok(TwoColorMatrix)` - Successfully created two-color matrix
30 /// * `Err(String)` - Error message if dimensions don't match
31 ///
32 /// # Example
33 /// ```rust
34 /// # use ptouch::{TwoColorMatrix, Matrix};
35 /// let black_data: Matrix = vec![vec![0xFF; 90]; 300]; // 300 lines, 90 bytes each
36 /// let red_data: Matrix = vec![vec![0x00; 90]; 300]; // Same dimensions
37 ///
38 /// let two_color = TwoColorMatrix::new(black_data, red_data)?;
39 /// # Ok::<(), String>(())
40 /// ```
41 pub fn new(black: Matrix, red: Matrix) -> Result<Self, String> {
42 if black.len() != red.len() {
43 return Err("Black and red matrices must have the same height".to_string());
44 }
45
46 for (i, (black_row, red_row)) in black.iter().zip(red.iter()).enumerate() {
47 if black_row.len() != red_row.len() {
48 return Err(format!("Row {} has mismatched widths", i));
49 }
50 }
51
52 Ok(TwoColorMatrix { black, red })
53 }
54
55 /// Convert two-color data to alternating matrix format for printing.
56 ///
57 /// This method interleaves black and red rows to create a single matrix
58 /// where black and red lines alternate. This format is required by the
59 /// printer's two-color raster commands.
60 ///
61 /// # Returns
62 /// Matrix with double the height, alternating between black and red rows
63 ///
64 /// # Example
65 /// ```rust
66 /// # use ptouch::{TwoColorMatrix, Matrix};
67 /// # let black_data: Matrix = vec![vec![0xFF; 90]; 2];
68 /// # let red_data: Matrix = vec![vec![0x00; 90]; 2];
69 /// let two_color = TwoColorMatrix::new(black_data, red_data)?;
70 /// let alternating = two_color.to_alternating_matrix();
71 /// assert_eq!(alternating.len(), 4); // 2 * 2 original rows
72 /// # Ok::<(), String>(())
73 /// ```
74 pub fn to_alternating_matrix(&self) -> Matrix {
75 let mut result = Matrix::new();
76
77 for (black_row, red_row) in self.black.iter().zip(self.red.iter()) {
78 result.push(black_row.clone());
79 result.push(red_row.clone());
80 }
81
82 result
83 }
84}
85
86/// Convert grayscale image to 1-bit bitmap for normal-width printers (720 pixels).
87///
88/// This function processes grayscale image data and converts it to the 1-bit
89/// bitmap format required by Brother P-Touch printers. Pixels are packed
90/// 8 per byte with proper bit ordering for the printer.
91///
92/// # Arguments
93/// * `threshold` - Grayscale threshold (0-255). Pixels below this become black (1)
94/// * `length` - Image height in pixels
95/// * `bytes` - Grayscale image data (width × height bytes)
96///
97/// # Returns
98/// Matrix containing 1-bit bitmap data (`Vec<Vec<u8>>`)
99///
100/// # Example
101/// ```rust
102/// # use ptouch::{step_filter_normal, Matrix};
103/// let width = 720;
104/// let height = 100;
105/// let grayscale_data = vec![128u8; (width * height) as usize]; // Gray image
106///
107/// let bitmap = step_filter_normal(80, height, grayscale_data);
108/// assert_eq!(bitmap.len(), height as usize);
109/// assert_eq!(bitmap[0].len(), 90); // 720 pixels / 8 = 90 bytes
110/// ```
111pub fn step_filter_normal(threshold: u8, length: u32, bytes: Vec<u8>) -> Matrix {
112 step_filter(threshold, crate::NORMAL_PRINTER_WIDTH, length, bytes)
113}
114
115/// Convert grayscale image to 1-bit bitmap for wide printers (1296 pixels).
116///
117/// Similar to `step_filter_normal` but designed for wide printers like QL-1100 series.
118/// Processes images with 1296 pixel width instead of 720.
119///
120/// # Arguments
121/// * `threshold` - Grayscale threshold (0-255). Pixels below this become black (1)
122/// * `length` - Image height in pixels
123/// * `bytes` - Grayscale image data (width × height bytes)
124///
125/// # Returns
126/// Matrix containing 1-bit bitmap data (`Vec<Vec<u8>>`)
127///
128/// # Example
129/// ```rust
130/// # use ptouch::{step_filter_wide, Matrix, WIDE_PRINTER_WIDTH};
131/// let width = WIDE_PRINTER_WIDTH;
132/// let height = 100;
133/// let grayscale_data = vec![128u8; (width * height) as usize];
134///
135/// let bitmap = step_filter_wide(80, height, grayscale_data);
136/// assert_eq!(bitmap.len(), height as usize);
137/// assert_eq!(bitmap[0].len(), 162); // 1296 pixels / 8 = 162 bytes
138/// ```
139pub fn step_filter_wide(threshold: u8, length: u32, bytes: Vec<u8>) -> Matrix {
140 step_filter(threshold, crate::WIDE_PRINTER_WIDTH, length, bytes)
141}
142
143fn step_filter(threshold: u8, width: u32, length: u32, bytes: Vec<u8>) -> Matrix {
144 // convert to black and white data
145 // threshold = 80 seems to work fine if original data is monochrome.
146 // TODO: Add support for a dithering algorithm to print photos
147 //
148 // width must be
149 let mut bw: Vec<Vec<u8>> = Vec::new();
150
151 for y in 0..length {
152 let mut buf: Vec<u8> = Vec::new();
153 for x in 0..(width / 8) {
154 let index = (1 + y) * width - (1 + x) * 8;
155 let mut tmp: u8 = 0x00;
156 for i in 0..8 {
157 let pixel = bytes[(index + i) as usize];
158 let value: u8 = if pixel > threshold { 0 } else { 1 };
159 tmp = tmp | (value << i);
160 }
161 buf.push(tmp);
162 }
163 bw.push(buf);
164 }
165
166 bw
167}
168
169/// Convert RGB image data to two-color bitmap for printing.
170///
171/// This function analyzes RGB pixel data and separates it into black and red
172/// components suitable for two-color printing. Uses color detection algorithms
173/// to classify pixels as red, black, or white (not printed).
174///
175/// # Color Detection Rules
176/// - **Red pixels**: R > 200, G < 100, B < 100
177/// - **Black pixels**: Brightness < 128 (excluding red pixels)
178/// - **White pixels**: Everything else (not printed)
179///
180/// # Arguments
181/// * `width` - Image width in pixels
182/// * `height` - Image height in pixels
183/// * `rgb_data` - RGB image data (width × height × 3 bytes)
184///
185/// # Returns
186/// * `Ok(TwoColorMatrix)` - Successfully converted image data
187/// * `Err(String)` - Error if data size doesn't match dimensions
188///
189/// # Example
190/// ```rust
191/// # use ptouch::{convert_rgb_to_two_color};
192/// let width = 720;
193/// let height = 100;
194/// // Create simple RGB data: red stripe at top, black at bottom
195/// let mut rgb_data = vec![];
196/// for y in 0..height {
197/// for x in 0..width {
198/// if y < height / 2 {
199/// rgb_data.extend_from_slice(&[255, 0, 0]); // Red
200/// } else {
201/// rgb_data.extend_from_slice(&[0, 0, 0]); // Black
202/// }
203/// }
204/// }
205///
206/// let two_color = convert_rgb_to_two_color(width, height, &rgb_data)?;
207/// # Ok::<(), String>(())
208/// ```
209pub fn convert_rgb_to_two_color(
210 width: u32,
211 height: u32,
212 rgb_data: &[u8],
213) -> Result<TwoColorMatrix, String> {
214 if rgb_data.len() != (width * height * 3) as usize {
215 return Err("RGB data size doesn't match width * height * 3".to_string());
216 }
217
218 let mut black_matrix = Matrix::new();
219 let mut red_matrix = Matrix::new();
220
221 for y in 0..height {
222 let mut black_row = vec![0u8; (width + 7) as usize / 8];
223 let mut red_row = vec![0u8; (width + 7) as usize / 8];
224
225 for x in 0..(width / 8) {
226 // Use same indexing as step_filter to match existing behavior
227 let base_index = (1 + y) * width - (1 + x) * 8;
228 let mut black_byte: u8 = 0x00;
229 let mut red_byte: u8 = 0x00;
230
231 for i in 0..8 {
232 let pixel_index = ((base_index + i) * 3) as usize;
233 if pixel_index + 2 < rgb_data.len() {
234 let r = rgb_data[pixel_index];
235 let g = rgb_data[pixel_index + 1];
236 let b = rgb_data[pixel_index + 2];
237
238 if is_red_pixel(r, g, b) {
239 red_byte |= 1 << i;
240 } else if is_black_pixel(r, g, b) {
241 black_byte |= 1 << i;
242 }
243 }
244 }
245
246 black_row[x as usize] = black_byte;
247 red_row[x as usize] = red_byte;
248 }
249
250 black_matrix.push(black_row);
251 red_matrix.push(red_row);
252 }
253
254 TwoColorMatrix::new(black_matrix, red_matrix)
255}
256
257fn is_red_pixel(r: u8, g: u8, b: u8) -> bool {
258 r > 200 && g < 100 && b < 100
259}
260
261fn is_black_pixel(r: u8, g: u8, b: u8) -> bool {
262 let brightness = ((r as u32 + g as u32 + b as u32) / 3) as u8;
263 brightness < 128 && !is_red_pixel(r, g, b)
264}