1use 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
24pub 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
33pub 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 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]; 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}