Skip to main content

docvortex_core/
pixels.rs

1//! 对独立位图字节执行裁剪、通道排列及直角旋转,不持有 PDFium 指针。
2
3/// 在连续灰度图上批量取最近邻字形,越界区域沿用 Pillow crop 的黑色填充。
4pub 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
44/// 白色合成后的 RGB 图仅需首个有墨迹行,等价于差分掩码的完整 getbbox。
45pub fn blank_top_rgb(data: &[u8], width: usize, height: usize) -> f64 {
46    blank_top_channels(data, width, height, 3)
47}
48
49/// 无透明元数据的灰度图与 RGB 图使用相同近白阈值,不必先扩展三通道或合成白底。
50pub fn blank_top_gray(data: &[u8], width: usize, height: usize) -> f64 {
51    blank_top_channels(data, width, height, 1)
52}
53
54/// 已验证的独立像素缓冲逐行读取,保留全宽、面积限制和百分比边界。
55fn 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
70/// 校验步长和边界,按原方向分类语义返回连续 BGR 像素,颜色编码由调用方完成。
71pub 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
141/// 白边检查直接扫描独立位图行,忽略填充通道及行尾;含透明度的格式完整交回 Pillow 合成。
142pub 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}