Skip to main content

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}