Skip to main content

rapiddoc_render/
wired.rs

1//! UNET 噪声边缘裁剪与空格占位;保持旧结构规则,输出文字始终转义。
2use rapiddoc_core::{Error, ErrorCode};
3pub type Polygon = [[f64; 2]; 4];
4fn invalid(message: &str) -> Error {
5    Error {
6        code: ErrorCode::InvalidInput,
7        message: message.into(),
8    }
9}
10fn median(values: &[f64]) -> Option<f64> {
11    if values.is_empty() {
12        return None;
13    }
14    let mut v = values.to_vec();
15    v.sort_by(f64::total_cmp);
16    let n = v.len();
17    Some(if n % 2 == 1 {
18        v[n / 2]
19    } else {
20        (v[n / 2 - 1] + v[n / 2]) / 2.
21    })
22}
23
24/// 输入为已归并/清理的逐格行文字;网格重叠报错,内部缺位按旧规则输出空 td。
25pub fn html(
26    points: &[[usize; 4]],
27    polygons: &[Polygon],
28    texts: &[Vec<String>],
29) -> Result<String, Error> {
30    render(points, polygons, texts, true, true)
31}
32
33/// 旧 UNET 不再转义已转义文本;逻辑格重叠按原 Python 的后写覆盖处理。
34pub fn legacy_html(
35    points: &[[usize; 4]],
36    polygons: &[Polygon],
37    texts: &[Vec<String>],
38) -> Result<String, Error> {
39    render(points, polygons, texts, false, false)
40}
41
42fn render(
43    points: &[[usize; 4]],
44    polygons: &[Polygon],
45    texts: &[Vec<String>],
46    escape: bool,
47    reject_overlaps: bool,
48) -> Result<String, Error> {
49    if points.len() != polygons.len() || points.len() != texts.len() {
50        return Err(invalid("UNET 逻辑格/物理格/文字数量不匹配"));
51    }
52    if points.len() > rapiddoc_core::resources::limit(2000)
53        || texts.iter().flatten().map(String::len).sum::<usize>()
54            > rapiddoc_core::resources::limit(1_000_000)
55    {
56        return Err(Error {
57            code: ErrorCode::ResourceLimit,
58            message: "UNET HTML 输入超过预算".into(),
59        });
60    }
61    if points.is_empty() {
62        return Ok("<html><body><table></table></body></html>".into());
63    }
64    let whitespace = |c: char| matches!(c,'\u{0009}'..='\u{000d}'|'\u{001c}'..='\u{0020}'|'\u{0085}'|'\u{00a0}'|'\u{1680}'|'\u{2000}'..='\u{200a}'|'\u{2028}'|'\u{2029}'|'\u{202f}'|'\u{205f}'|'\u{3000}');
65    let texts: Vec<Vec<String>> = texts
66        .iter()
67        .map(|lines| {
68            lines
69                .iter()
70                .filter(|s| !s.trim_matches(whitespace).is_empty())
71                .map(|s| {
72                    s.replace(['\r', '\n'], "")
73                        .trim_matches(whitespace)
74                        .to_owned()
75                })
76                .collect()
77        })
78        .collect();
79    if points.iter().any(|p| {
80        p[0] > p[1]
81            || (reject_overlaps && p[2] > p[3])
82            || p[1] >= rapiddoc_core::resources::limit(1000)
83            || p[3] >= rapiddoc_core::resources::limit(1000)
84    }) || polygons
85        .iter()
86        .flatten()
87        .flatten()
88        .any(|v| !v.is_finite() || v.abs() > 1_000_000.)
89    {
90        return Err(invalid("UNET HTML 坐标或跨度无效"));
91    }
92    let rows = points
93        .iter()
94        .map(|p| p[1] + 1)
95        .max()
96        .ok_or_else(|| invalid("空格坐标"))?;
97    let columns = points
98        .iter()
99        .map(|p| p[3] + 1)
100        .max()
101        .ok_or_else(|| invalid("空格坐标"))?;
102    if rows * columns > rapiddoc_core::resources::limit(100_000) {
103        return Err(Error {
104            code: ErrorCode::ResourceLimit,
105            message: "UNET HTML 网格超过预算".into(),
106        });
107    }
108    let mut grid = vec![None; rows * columns];
109    let mut row_sizes = vec![Vec::new(); rows];
110    let mut column_sizes = vec![Vec::new(); columns];
111    for (i, p) in points.iter().enumerate() {
112        for r in p[0]..=p[1] {
113            for c in p[2]..=p[3] {
114                if grid[r * columns + c].replace(i).is_some() && reject_overlaps {
115                    return Err(invalid("UNET 逻辑格重复覆盖"));
116                }
117            }
118        }
119        let poly = polygons[i];
120        let x0 = poly.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
121        let y0 = poly.iter().map(|p| p[1]).fold(f64::INFINITY, f64::min);
122        let x1 = poly.iter().map(|p| p[0]).fold(f64::NEG_INFINITY, f64::max);
123        let y1 = poly.iter().map(|p| p[1]).fold(f64::NEG_INFINITY, f64::max);
124        // Python estimates reversed/empty column intervals with max(span, 1).
125        let width = (x1 - x0) / (p[3] + 1).saturating_sub(p[2]).max(1) as f64;
126        let height = (y1 - y0) / (p[1] - p[0] + 1) as f64;
127        if width > 0. && p[2] <= p[3] {
128            for sizes in &mut column_sizes[p[2]..=p[3]] {
129                sizes.push(width);
130            }
131        }
132        if height > 0. {
133            for sizes in &mut row_sizes[p[0]..=p[1]] {
134                sizes.push(height);
135            }
136        }
137    }
138    let row_sizes: Vec<_> = row_sizes.iter().map(|v| median(v)).collect();
139    let column_sizes: Vec<_> = column_sizes.iter().map(|v| median(v)).collect();
140    let visible: Vec<_> = texts
141        .iter()
142        .map(|lines| lines.iter().any(|s| !s.is_empty()))
143        .collect();
144    let mut range = [0, rows, 0, columns]; // 右端开区间,便于保留“全被裁掉”的状态。
145    for edge in 0..4 {
146        while if edge < 2 {
147            range[0] < range[1]
148        } else {
149            range[2] < range[3]
150        } {
151            let axis = if edge < 2 { 0 } else { 1 };
152            let index = match edge {
153                0 => range[0],
154                1 => range[1] - 1,
155                2 => range[2],
156                _ => range[3] - 1,
157            };
158            let positions: Vec<_> = if axis == 0 {
159                (range[2]..range[3])
160                    .map(|c| grid[index * columns + c])
161                    .collect()
162            } else {
163                (range[0]..range[1])
164                    .map(|r| grid[r * columns + index])
165                    .collect()
166            };
167            if positions.iter().flatten().any(|&i| visible[i]) {
168                break;
169            }
170            let covered = positions.iter().flatten().count();
171            let total = positions.len();
172            let sizes = if axis == 0 { &row_sizes } else { &column_sizes };
173            let others: Vec<_> = sizes
174                .iter()
175                .enumerate()
176                .filter_map(|(i, v)| if i != index { *v } else { None })
177                .collect();
178            let abnormal = match (sizes[index], median(&others)) {
179                (Some(size), Some(reference)) if reference > 0. => {
180                    let ratio = size / reference;
181                    !(0.35..=2.5).contains(&ratio)
182                }
183                _ => false,
184            };
185            if covered != 0 && covered == total && !abnormal {
186                break;
187            }
188            match edge {
189                0 => range[0] += 1,
190                1 => range[1] -= 1,
191                2 => range[2] += 1,
192                _ => range[3] -= 1,
193            }
194        }
195    }
196    let mut output = String::from("<html><body><table>");
197    if range[0] < range[1] && range[2] < range[3] {
198        for r in range[0]..range[1] {
199            output.push_str("<tr>");
200            for c in range[2]..range[3] {
201                if let Some(i) = grid[r * columns + c] {
202                    let p = points[i];
203                    let top = p[0].max(range[0]);
204                    let left = p[2].max(range[2]);
205                    if r == top && c == left {
206                        let rs = p[1].min(range[1] - 1) - top + 1;
207                        let cs = p[3].min(range[3] - 1) - left + 1;
208                        output.push_str(&format!("<td rowspan={rs} colspan={cs}>"));
209                        output.push_str(
210                            &texts[i]
211                                .iter()
212                                .map(|s| {
213                                    if escape {
214                                        crate::escape_html(s)
215                                    } else {
216                                        s.clone()
217                                    }
218                                })
219                                .collect::<Vec<_>>()
220                                .join("<br>"),
221                        );
222                        output.push_str("</td>");
223                    }
224                } else {
225                    output.push_str("<td></td>");
226                }
227            }
228            output.push_str("</tr>");
229        }
230    }
231    output.push_str("</table></body></html>");
232    Ok(output)
233}