1pub fn glyph_masks(
5 data: &[u8],
6 width: usize,
7 height: usize,
8 boxes: &[[i64; 4]],
9) -> Vec<Option<(Vec<u8>, f64)>> {
10 boxes
11 .iter()
12 .map(|b| {
13 let w = b[2] - b[0];
14 let h = b[3] - b[1];
15 if w < 6 || h < 9 {
16 return None;
17 }
18 let mut mask = vec![0u8; 128];
19 let mut count = 0;
20 for y in 0..32 {
21 for x in 0..32 {
22 let sx = b[0] + (((x as f64 + 0.5) * w as f64) / 32.0) as i64;
23 let sy = b[1] + (((y as f64 + 0.5) * h as f64) / 32.0) as i64;
24 let value = if sx < 0 || sy < 0 || sx >= width as i64 || sy >= height as i64 {
25 0
26 } else {
27 data[sy as usize * width + sx as usize]
28 };
29 if value < 128 {
30 let i = y * 32 + x;
31 mask[i / 8] |= 1 << (i % 8);
32 count += 1;
33 }
34 }
35 }
36 if !(0.08..=0.85).contains(&(count as f64 / 1024.0)) {
37 return None;
38 }
39 Some((mask, w as f64 / h as f64))
40 })
41 .collect()
42}
43
44pub fn blank_top_rgb(data: &[u8], width: usize, height: usize) -> f64 {
46 blank_top_channels(data, width, height, 3)
47}
48
49pub fn blank_top_gray(data: &[u8], width: usize, height: usize) -> f64 {
51 blank_top_channels(data, width, height, 1)
52}
53
54fn blank_top_channels(data: &[u8], width: usize, height: usize, channels: usize) -> f64 {
56 if width == 0 || height == 0 || width.saturating_mul(height) > 4_000_000 {
57 return 0.0;
58 }
59 let top = data
60 .chunks_exact(width * channels)
61 .position(|row| row.iter().any(|v| *v < 250));
62 let fraction = top.unwrap_or(0) as f64 / height as f64;
63 if (0.03..=0.12).contains(&fraction) {
64 fraction
65 } else {
66 0.0
67 }
68}
69
70pub fn crop_bgr(
72 data: &[u8],
73 width: usize,
74 height: usize,
75 stride: usize,
76 mode: &str,
77 bbox: (usize, usize, usize, usize),
78 angle: u16,
79) -> Result<(Vec<u8>, usize, usize), &'static str> {
80 let (channels, rgb) = match mode {
81 "BGR" => (3, false),
82 "BGRX" | "BGRA" => (4, false),
83 "RGB" => (3, true),
84 "RGBX" | "RGBA" => (4, true),
85 "L" => (1, false),
86 _ => return Err("unsupported bitmap mode"),
87 };
88 if width.checked_mul(channels).is_none_or(|n| n > stride)
89 || height.checked_mul(stride).is_none_or(|n| n > data.len())
90 {
91 return Err("invalid bitmap buffer or stride");
92 }
93 let (x0, y0, x1, y1) = bbox;
94 if x0 >= x1 || y0 >= y1 || x1 > width || y1 > height {
95 return Err("invalid crop bounds");
96 }
97 if ![0, 90, 180, 270].contains(&angle) {
98 return Err("invalid crop angle");
99 }
100 let (w, h) = (x1 - x0, y1 - y0);
101 let (ow, oh) = if angle == 90 || angle == 270 {
102 (h, w)
103 } else {
104 (w, h)
105 };
106 let len = ow
107 .checked_mul(oh)
108 .and_then(|n| n.checked_mul(3))
109 .ok_or("crop size overflow")?;
110 let mut output = vec![0; len];
111 if mode == "BGR" && angle == 0 {
112 for y in 0..h {
113 let start = (y0 + y) * stride + x0 * 3;
114 output[y * w * 3..(y + 1) * w * 3].copy_from_slice(&data[start..start + w * 3]);
115 }
116 } else {
117 for y in 0..h {
118 for x in 0..w {
119 let source = (y0 + y) * stride + (x0 + x) * channels;
120 let (dx, dy) = match angle {
121 90 => (y, w - 1 - x),
122 180 => (w - 1 - x, h - 1 - y),
123 270 => (h - 1 - y, x),
124 _ => (x, y),
125 };
126 let target = (dy * ow + dx) * 3;
127 let pixel = if channels == 1 {
128 [data[source]; 3]
129 } else if rgb {
130 [data[source + 2], data[source + 1], data[source]]
131 } else {
132 [data[source], data[source + 1], data[source + 2]]
133 };
134 output[target..target + 3].copy_from_slice(&pixel);
135 }
136 }
137 }
138 Ok((output, ow, oh))
139}
140
141pub fn blank_top_bitmap(
143 data: &[u8],
144 width: usize,
145 height: usize,
146 stride: usize,
147 mode: &str,
148) -> Option<f64> {
149 let (channels, colors) = match mode {
150 "RGB" | "BGR" => (3usize, 3usize),
151 "RGBX" | "BGRX" => (4, 3),
152 "L" => (1, 1),
153 _ => return None,
154 };
155 if width.checked_mul(channels).is_none_or(|v| v > stride)
156 || height.checked_mul(stride).is_none_or(|v| v > data.len())
157 {
158 return None;
159 }
160 if width == 0 || height == 0 || width.saturating_mul(height) > 4_000_000 {
161 return Some(0.0);
162 }
163 let top = (0..height).find(|&y| {
164 (0..width).any(|x| {
165 data[y * stride + x * channels..y * stride + x * channels + colors]
166 .iter()
167 .any(|&v| v < 250)
168 })
169 });
170 let fraction = top.unwrap_or(0) as f64 / height as f64;
171 Some(if (0.03..=0.12).contains(&fraction) {
172 fraction
173 } else {
174 0.0
175 })
176}