1use anyhow::{anyhow, bail, Context, Result};
4
5use crate::mesh::{Field, Mesh};
6
7use super::*;
8
9pub(super) struct Legacy<'a> {
10 pub(super) data: &'a [u8],
11 pub(super) at: usize,
12 pub(super) binary: bool,
13}
14
15impl<'a> Legacy<'a> {
16 pub(super) fn rest(&self) -> &'a [u8] {
18 self.data.get(self.at..).unwrap_or_default()
19 }
20
21 pub(super) fn raw_line(&mut self) -> Option<&'a [u8]> {
23 let rest = self.rest();
24 if rest.is_empty() {
25 return None;
26 }
27 let end = rest.iter().position(|&b| b == b'\n').unwrap_or(rest.len());
28 self.at += (end + 1).min(rest.len());
29 Some(rest.get(..end).unwrap_or(rest))
30 }
31
32 pub(super) fn line(&mut self) -> Result<Option<Vec<&'a str>>> {
35 while let Some(raw) = self.raw_line() {
36 let text = std::str::from_utf8(raw)
37 .map_err(|_| anyhow!("unexpected binary data in a VTK section header"))?;
38 let tokens: Vec<&str> = text.split_ascii_whitespace().collect();
39 if !tokens.is_empty() {
40 return Ok(Some(tokens));
41 }
42 }
43 Ok(None)
44 }
45
46 pub(super) fn peek(&self, keyword: &str) -> bool {
48 let rest = self.rest();
49 let start = rest
50 .iter()
51 .position(|b| !b.is_ascii_whitespace())
52 .unwrap_or(rest.len());
53 rest.get(start..)
54 .and_then(|r| r.get(..keyword.len()))
55 .is_some_and(|w| w.eq_ignore_ascii_case(keyword.as_bytes()))
56 }
57
58 pub(super) fn remaining(&self) -> usize {
59 self.data.len().saturating_sub(self.at)
60 }
61
62 pub(super) fn values(
64 &mut self,
65 count: usize,
66 ty: Num,
67 f: &mut dyn FnMut(f64) -> Result<()>,
68 ) -> Result<()> {
69 if self.binary {
70 let need = count.checked_mul(ty.size()).context("data too large")?;
71 let bytes = self
72 .data
73 .get(self.at..)
74 .and_then(|r| r.get(..need))
75 .context("the VTK file is truncated (binary data shorter than declared)")?;
76 self.at += need;
77 each_binary(bytes, ty, true, count, f)
78 } else {
79 for _ in 0..count {
80 let rest = self.rest();
81 let start = rest
82 .iter()
83 .position(|b| !b.is_ascii_whitespace())
84 .context("the VTK file is truncated (fewer values than declared)")?;
85 let token = rest.get(start..).unwrap_or_default();
86 let len = token
87 .iter()
88 .position(|b| b.is_ascii_whitespace())
89 .unwrap_or(token.len());
90 self.at += start + len;
91 f(number(token.get(..len).unwrap_or(token))?)?;
92 }
93 Ok(())
94 }
95 }
96
97 pub(super) fn fits(&self, count: usize, ty: Num) -> bool {
101 if self.binary {
102 count <= self.remaining() / ty.size()
103 } else {
104 count <= self.remaining() / 2 + 1
105 }
106 }
107
108 pub(super) fn tuples(&mut self, tuples: usize, components: usize, ty: Num) -> Result<Vec<f32>> {
109 if components == 0 || components > MAX_COMPONENTS {
110 bail!("unsupported number of components ({components})");
111 }
112 let total = tuples.checked_mul(components).context("data too large")?;
113 if !self.fits(total, ty) {
114 bail!("the VTK file is truncated (data shorter than declared)");
115 }
116 let mut t = Tuples::new(tuples, components, self.remaining());
117 self.values(total, ty, &mut |v| t.push(v))?;
118 Ok(t.out)
119 }
120
121 pub(super) fn skip(&mut self, total: usize, ty: Num) -> Result<()> {
122 if !self.fits(total, ty) {
123 bail!("the VTK file is truncated (data shorter than declared)");
124 }
125 self.values(total, ty, &mut |_| Ok(()))
126 }
127
128 pub(super) fn cells(
130 &mut self,
131 grid: &mut Grid,
132 a: usize,
133 b: usize,
134 poly_type: Option<u8>,
135 ) -> Result<()> {
136 grid.check_cells(a, b)?;
137 let base = grid.conn.len();
138 let first_cell = grid.cell_count();
139 if self.peek("OFFSETS") {
140 let t = self.line()?.context("truncated VTK file")?;
142 let ty = Num::legacy(t.get(1).copied())?;
143 if !self.fits(a, ty) {
144 bail!("the VTK file is truncated (offsets)");
145 }
146 let mut offsets: Vec<usize> = Vec::with_capacity(a);
147 self.values(a, ty, &mut |v| {
148 offsets.push(index(v)? as usize);
149 Ok(())
150 })?;
151 let t = self
152 .line()?
153 .context("truncated VTK file (no CONNECTIVITY)")?;
154 if !t
155 .first()
156 .is_some_and(|k| k.eq_ignore_ascii_case("CONNECTIVITY"))
157 {
158 bail!("CONNECTIVITY expected after OFFSETS");
159 }
160 let ty = Num::legacy(t.get(1).copied())?;
161 if !self.fits(b, ty) {
162 bail!("the VTK file is truncated (connectivity)");
163 }
164 grid.conn.reserve(b);
165 self.values(b, ty, &mut |v| {
166 grid.conn.push(index(v)?);
167 Ok(())
168 })?;
169 if offsets.first().is_some_and(|&o| o != 0) || offsets.last().is_some_and(|&o| o != b) {
170 bail!("invalid cell offsets");
171 }
172 for w in offsets.windows(2) {
173 let [from, to] = *w else { continue };
174 if to < from {
175 bail!("invalid cell offsets");
176 }
177 grid.ends.push(base + to);
178 }
179 } else {
180 let ty = if self.binary { Num::I32 } else { Num::F64 };
182 if !self.fits(b, ty) {
183 bail!("the VTK file is truncated (cells)");
184 }
185 grid.conn.reserve(b.saturating_sub(a));
186 let mut left = 0usize;
187 let mut seen = 0usize;
188 let (conn, ends) = (&mut grid.conn, &mut grid.ends);
189 self.values(b, ty, &mut |v| {
190 let v = index(v)?;
191 if left == 0 {
192 left = v as usize;
193 seen += 1;
194 if left == 0 {
195 ends.push(conn.len());
196 }
197 } else {
198 conn.push(v);
199 left -= 1;
200 if left == 0 {
201 ends.push(conn.len());
202 }
203 }
204 Ok(())
205 })?;
206 if left != 0 || seen != a || grid.ends.len() != first_cell + a {
207 bail!("the cell list does not match its declared size");
208 }
209 }
210 if let Some(ty) = poly_type {
211 let added = grid.cell_count() - first_cell;
212 grid.types.extend(std::iter::repeat_n(ty, added));
213 }
214 Ok(())
215 }
216}
217
218pub fn vtk_legacy(data: &[u8]) -> Result<Mesh> {
220 let mut r = Legacy {
221 data,
222 at: 0,
223 binary: false,
224 };
225 let first = r.raw_line().unwrap_or_default();
226 if !String::from_utf8_lossy(first)
227 .trim_start()
228 .to_ascii_lowercase()
229 .starts_with("# vtk")
230 {
231 bail!("Not a legacy VTK file (no '# vtk DataFile' header).");
232 }
233 r.raw_line(); let mode = r.line()?.context("the VTK file is truncated")?;
235 let first = |t: &[&str]| t.first().copied().unwrap_or_default().to_ascii_uppercase();
236 r.binary = match first(&mode).as_str() {
237 "ASCII" => false,
238 "BINARY" => true,
239 other => bail!("unknown VTK file type '{}'", short(other)),
240 };
241 let dataset = r.line()?.context("the VTK file is truncated")?;
242 let kind = dataset.get(1).map(|s| s.to_ascii_uppercase());
243 if first(&dataset) != "DATASET" {
244 bail!("DATASET expected in the VTK file");
245 }
246 let poly = match kind.as_deref() {
247 Some("UNSTRUCTURED_GRID") => false,
248 Some("POLYDATA") => true,
249 Some(other) => bail!(
250 "The 3D preview reads UNSTRUCTURED_GRID and POLYDATA VTK files, not {}.",
251 short(other)
252 ),
253 None => bail!("DATASET without a type in the VTK file"),
254 };
255 let mut grid = Grid::default();
256 let mut attr: Option<(bool, usize)> = None;
258 while let Some(t) = r.line()? {
259 let key = first(&t);
260 let target = |grid: &mut Grid, attr: Option<(bool, usize)>, field: Field| {
261 if let Some((is_point, n)) = attr {
262 if field.values.len() == n {
263 let list = if is_point {
264 &mut grid.point_fields
265 } else {
266 &mut grid.cell_fields
267 };
268 Grid::add_field(list, field);
269 }
270 }
271 };
272 match key.as_str() {
273 "POINTS" => {
274 let n = count(t.get(1).copied(), "point count")?;
275 let ty = Num::legacy(t.get(2).copied())?;
276 if n > MAX_NODES {
277 bail!(
278 "The result has more than {MAX_NODES} points – too many for the preview."
279 );
280 }
281 if !grid.points.is_empty() {
282 bail!("the VTK file has two POINTS sections");
283 }
284 let total = n * 3;
285 if !r.fits(total, ty) {
286 bail!("the VTK file is truncated (points)");
287 }
288 grid.points.reserve(total);
289 let points = &mut grid.points;
290 r.values(total, ty, &mut |v| {
291 points.push(v as f32);
292 Ok(())
293 })?;
294 }
295 "CELLS" if !poly => {
296 let a = count(t.get(1).copied(), "cell count")?;
297 let b = count(t.get(2).copied(), "cell list size")?;
298 r.cells(&mut grid, a, b, None)?;
299 }
300 "VERTICES" | "LINES" | "POLYGONS" | "TRIANGLE_STRIPS" if poly => {
301 let a = count(t.get(1).copied(), "cell count")?;
302 let b = count(t.get(2).copied(), "cell list size")?;
303 let ty = match key.as_str() {
304 "VERTICES" => POLY_VERTEX,
305 "LINES" => POLY_LINE,
306 "POLYGONS" => POLYGON,
307 _ => TRIANGLE_STRIP,
308 };
309 r.cells(&mut grid, a, b, Some(ty))?;
310 }
311 "CELL_TYPES" if !poly => {
312 let n = count(t.get(1).copied(), "cell type count")?;
313 let ty = if r.binary { Num::I32 } else { Num::F64 };
314 if n > MAX_CELLS || !r.fits(n, ty) {
315 bail!("the VTK file is truncated (cell types)");
316 }
317 let types = &mut grid.types;
318 types.reserve(n);
319 r.values(n, ty, &mut |v| {
320 types.push(if (0.0..=255.0).contains(&v) {
321 v as u8
322 } else {
323 0
324 });
325 Ok(())
326 })?;
327 }
328 "POINT_DATA" | "CELL_DATA" => {
329 let n = count(t.get(1).copied(), "attribute count")?;
330 attr = Some((key == "POINT_DATA", n));
331 }
332 "SCALARS" | "VECTORS" | "NORMALS" | "TENSORS" | "TENSORS6" => {
333 let (_, n) = attr.context("data values before POINT_DATA / CELL_DATA")?;
334 let name = t.get(1).copied().unwrap_or("values").to_string();
335 let ty = Num::legacy(t.get(2).copied())?;
336 let components = match key.as_str() {
337 "SCALARS" => t.get(3).and_then(|c| c.parse().ok()).unwrap_or(1),
338 "TENSORS" => 9,
339 "TENSORS6" => 6,
340 _ => 3,
341 };
342 if key == "SCALARS" && r.peek("LOOKUP_TABLE") {
343 r.line()?;
344 }
345 if key == "NORMALS" {
346 r.skip(n.checked_mul(3).context("data too large")?, ty)?;
347 } else {
348 let values = r.tuples(n, components, ty)?;
349 let name = Reduce::of(components).label(&name);
350 target(
351 &mut grid,
352 attr,
353 Field {
354 name,
355 unit: String::new(),
356 values,
357 },
358 );
359 }
360 }
361 "TEXTURE_COORDINATES" => {
362 let (_, n) = attr.context("data values before POINT_DATA / CELL_DATA")?;
363 let dim = count(t.get(2).copied(), "dimension")?.min(MAX_COMPONENTS);
364 let ty = Num::legacy(t.get(3).copied())?;
365 r.skip(n.checked_mul(dim).context("data too large")?, ty)?;
366 }
367 "COLOR_SCALARS" => {
368 let (_, n) = attr.context("data values before POINT_DATA / CELL_DATA")?;
369 let k = count(t.get(2).copied(), "colour components")?.min(MAX_COMPONENTS);
370 let ty = if r.binary { Num::U8 } else { Num::F64 };
371 r.skip(n.checked_mul(k).context("data too large")?, ty)?;
372 }
373 "LOOKUP_TABLE" => {
374 let n = count(t.get(2).copied(), "lookup table size")?;
375 let ty = if r.binary { Num::U8 } else { Num::F64 };
376 r.skip(n.checked_mul(4).context("data too large")?, ty)?;
377 }
378 "FIELD" => {
379 let arrays = count(t.get(2).copied(), "array count")?;
380 for _ in 0..arrays {
381 while r.peek("METADATA") {
383 r.line()?;
384 skip_metadata(&mut r);
385 }
386 let a = r.line()?.context("the VTK file is truncated (FIELD)")?;
387 let name = a.first().copied().unwrap_or_default().to_string();
388 let components = count(a.get(1).copied(), "component count")?;
389 let tuples = count(a.get(2).copied(), "tuple count")?;
390 let ty = Num::legacy(a.get(3).copied())?;
391 let values = r.tuples(tuples, components, ty)?;
392 let name = Reduce::of(components).label(&name);
393 target(
394 &mut grid,
395 attr,
396 Field {
397 name,
398 unit: String::new(),
399 values,
400 },
401 );
402 }
403 }
404 "METADATA" => skip_metadata(&mut r),
405 "DATASET" => bail!("the VTK file has two DATASET sections"),
406 other => bail!("unsupported VTK section '{}'", short(other)),
407 }
408 }
409 grid.into_mesh()
410}
411
412pub(super) fn skip_metadata(r: &mut Legacy<'_>) {
414 while let Some(line) = r.raw_line() {
415 if line.iter().all(|b| b.is_ascii_whitespace()) {
416 break;
417 }
418 }
419}