Skip to main content

pdfrum_page/shading/
mesh.rs

1//! Mesh shading streams, types 4 to 7 (ISO 32000-1 §8.7.4.5.5-.8).
2//!
3//! One bit-packed stream carrying vertices, colours and — for every type but
4//! the lattice — an edge flag per record. Several details are quirks:
5//!
6//! - **The flag is masked to two bits** regardless of `/BitsPerFlag`, so it
7//!   is always 0..=3 and flag 3 is reachable. Type 4 treats flag 3
8//!   identically to flag 2, because it only special-cases flag 1.
9//! - **`/Decode` must be exactly `4 + 2 * components` long**, not merely long
10//!   enough. With any function present, `components` is 1 and the length is
11//!   exactly 6.
12//! - **With functions present, the "colour" read from the stream is not a
13//!   colour**: it is one parametric value `t` that the functions map later.
14//! - **Types 6 and 7 do not byte-align between patches**, unlike Gouraud's
15//!   per-vertex alignment.
16//! - **A patch reusing an edge sources its shared points at
17//!   `old[(flag * 3 + i) % 12]`** — modulo 12 even in the sixteen-point
18//!   tensor case, so the four interior points are never reused.
19
20use super::ShadingKind;
21use crate::color::{ColorSpace, Rgb};
22use crate::function::{BitReader, Function};
23use kurbo::Point;
24use std::sync::Arc;
25
26/// The most colour components a mesh may carry.
27pub const MAX_COMPONENTS: usize = 8;
28
29/// Coordinate widths the format allows.
30const VALID_COORD_BITS: [u32; 8] = [1, 2, 4, 8, 12, 16, 24, 32];
31
32/// Component widths the format allows. Note 24 and 32 are **not** here, even
33/// though they are legal for coordinates.
34const VALID_COMPONENT_BITS: [u32; 6] = [1, 2, 4, 8, 12, 16];
35
36/// Flag widths the format allows.
37const VALID_FLAG_BITS: [u32; 3] = [2, 4, 8];
38
39/// One vertex: where it is and what colour it carries.
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct Vertex {
42    /// Position in the shading's own coordinate space.
43    pub point: Point,
44    /// The colour, or — when functions are present — the parametric value in
45    /// the red slot with the other two zero.
46    pub color: Rgb,
47}
48
49/// A triangle from a type 4 or type 5 mesh.
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct Triangle {
52    /// Its three corners.
53    pub vertices: [Vertex; 3],
54}
55
56/// A Coons or tensor patch: twelve or sixteen control points and four corner
57/// colours.
58#[derive(Debug, Clone, PartialEq)]
59pub struct Patch {
60    /// Control points, twelve for a Coons patch and sixteen for a tensor one.
61    pub points: Box<[Point]>,
62    /// The four corner colours.
63    pub colors: [Rgb; 4],
64}
65
66/// A decoded mesh.
67#[derive(Debug, Clone, PartialEq, Default)]
68pub struct Mesh {
69    /// Triangles, from types 4 and 5.
70    pub triangles: Vec<Triangle>,
71    /// Patches, from types 6 and 7.
72    pub patches: Vec<Patch>,
73    /// The first colour component's decode range, `/Decode[4]` and
74    /// `/Decode[5]`.
75    ///
76    /// It is here because a mesh under a `/Function` reads exactly one
77    /// parametric value per colour and this range is its domain: the ramp is
78    /// sampled across it, and each vertex's value is mapped into the ramp's
79    /// 256 entries relative to it. `[0.0, 1.0]` is the common case but by no
80    /// means the only one — `[1, 2]`, `[0, 255]` and `[-128, 127]` all occur
81    /// in the corpus — and assuming the unit interval silently reads the
82    /// wrong end of the ramp for every one of them.
83    pub component_range: [f32; 2],
84}
85
86impl Mesh {
87    /// The bounding box of every vertex and control point, or `None` when the
88    /// mesh is empty.
89    #[must_use]
90    pub fn bounds(&self) -> Option<kurbo::Rect> {
91        let mut rect: Option<kurbo::Rect> = None;
92        let mut add = |p: Point| {
93            let r = kurbo::Rect::from_points(p, p);
94            rect = Some(match rect {
95                Some(existing) => existing.union(r),
96                None => r,
97            });
98        };
99        for t in &self.triangles {
100            for v in t.vertices {
101                add(v.point);
102            }
103        }
104        for p in &self.patches {
105            for point in &p.points {
106                add(*point);
107            }
108        }
109        rect
110    }
111}
112
113/// The bit widths and decode ranges a mesh stream declares.
114#[derive(Debug, Clone, PartialEq)]
115pub struct MeshParams {
116    /// `/BitsPerCoordinate`.
117    pub coord_bits: u32,
118    /// `/BitsPerComponent`.
119    pub component_bits: u32,
120    /// `/BitsPerFlag`, unvalidated for the lattice type which has no flags.
121    pub flag_bits: u32,
122    /// How many colour components each record carries — **1** whenever any
123    /// function is present.
124    pub components: usize,
125    /// `/Decode`, `[xmin, xmax, ymin, ymax, c0min, c0max, …]`.
126    pub decode: Box<[f32]>,
127    /// `2^coord_bits - 1`.
128    pub coord_max: u32,
129    /// `2^component_bits - 1`.
130    pub component_max: u32,
131}
132
133impl MeshParams {
134    /// Validate the widths and the `/Decode` length.
135    ///
136    /// `kind` decides whether `/BitsPerFlag` is validated at all: the lattice
137    /// type reads no edge flags, so its value is never checked.
138    #[must_use]
139    pub fn new(
140        coord_bits: u32,
141        component_bits: u32,
142        flag_bits: u32,
143        components: usize,
144        decode: &[f32],
145        kind: ShadingKind,
146    ) -> Option<Self> {
147        if !VALID_COORD_BITS.contains(&coord_bits)
148            || !VALID_COMPONENT_BITS.contains(&component_bits)
149            || (kind.reads_edge_flags() && !VALID_FLAG_BITS.contains(&flag_bits))
150            || components > MAX_COMPONENTS
151        {
152            return None;
153        }
154        // Exactly, not at least.
155        if decode.len() != 4 + 2 * components {
156            return None;
157        }
158        Some(Self {
159            coord_bits,
160            component_bits,
161            flag_bits,
162            components,
163            decode: decode.into(),
164            coord_max: if coord_bits >= 32 {
165                u32::MAX
166            } else {
167                (1u32 << coord_bits) - 1
168            },
169            component_max: if component_bits >= 32 {
170                u32::MAX
171            } else {
172                (1u32 << component_bits) - 1
173            },
174        })
175    }
176
177    /// The first colour component's decode range — `/Decode[4]`, `/Decode[5]`.
178    ///
179    /// Under a `/Function` this is the parametric value's domain, which is
180    /// what a ramp is sampled across. The length check in [`Self::new`] has
181    /// already guaranteed both entries exist.
182    #[must_use]
183    pub fn component_range(&self) -> [f32; 2] {
184        [
185            self.decode.get(4).copied().unwrap_or(0.0),
186            self.decode.get(5).copied().unwrap_or(0.0),
187        ]
188    }
189}
190
191/// A cursor over a mesh stream.
192pub struct MeshReader<'a> {
193    bits: BitReader<'a>,
194    params: &'a MeshParams,
195    space: &'a ColorSpace,
196    functions: &'a [Arc<Function>],
197}
198
199impl<'a> MeshReader<'a> {
200    /// A reader over `data`.
201    #[must_use]
202    pub fn new(
203        data: &'a [u8],
204        params: &'a MeshParams,
205        space: &'a ColorSpace,
206        functions: &'a [Arc<Function>],
207    ) -> Self {
208        Self {
209            bits: BitReader::new(data),
210            params,
211            space,
212            functions,
213        }
214    }
215
216    /// Whether a flag still fits.
217    #[must_use]
218    pub fn can_read_flag(&self) -> bool {
219        self.bits.remaining() >= u64::from(self.params.flag_bits)
220    }
221
222    /// Whether a coordinate pair still fits.
223    ///
224    /// Note the formulation: the *halved* remainder is compared against one
225    /// coordinate's width, which is the C++'s way of asking for two.
226    #[must_use]
227    pub fn can_read_coords(&self) -> bool {
228        self.bits.remaining() / 2 >= u64::from(self.params.coord_bits)
229    }
230
231    /// Whether a colour still fits.
232    ///
233    /// The C++ divides the remainder by the *component width* and compares
234    /// against the component count, which is a different question from
235    /// "`components * width` bits remain" only when the division truncates.
236    /// Reproduced, with a guard the C++ does not need because its zero case
237    /// is unreachable.
238    #[must_use]
239    pub fn can_read_color(&self) -> bool {
240        if self.params.component_bits == 0 {
241            return false;
242        }
243        self.bits.remaining() / u64::from(self.params.component_bits)
244            >= self.params.components as u64
245    }
246
247    /// Read an edge flag, **masked to two bits**.
248    pub fn read_flag(&mut self) -> u8 {
249        u8::try_from(self.bits.read(self.params.flag_bits) & 0x03).unwrap_or(0)
250    }
251
252    /// Read one coordinate pair, x fully before y.
253    pub fn read_coords(&mut self) -> Point {
254        let decode = |raw: u32, min: f32, max: f32, max_raw: u32| -> f64 {
255            if self.params.coord_bits == 32 {
256                // Forced to `f64` so a 32-bit raw value does not lose
257                // precision on the way through.
258                f64::from(min)
259                    + f64::from(raw) * (f64::from(max) - f64::from(min)) / f64::from(max_raw)
260            } else {
261                #[expect(
262                    clippy::cast_precision_loss,
263                    reason = "below 32 bits the raw value is exact in f32, matching the C++"
264                )]
265                let v = min + (raw as f32) * (max - min) / (max_raw as f32);
266                f64::from(v)
267            }
268        };
269        let at = |i: usize| self.params.decode.get(i).copied().unwrap_or(0.0);
270        let raw_x = self.bits.read(self.params.coord_bits);
271        let raw_y = self.bits.read(self.params.coord_bits);
272        Point::new(
273            decode(raw_x, at(0), at(1), self.params.coord_max),
274            decode(raw_y, at(2), at(3), self.params.coord_max),
275        )
276    }
277
278    /// Read one colour.
279    ///
280    /// With functions present the result is **not a colour**: the single
281    /// parametric value lands in the red slot and the rest are zero.
282    pub fn read_color(&mut self) -> Rgb {
283        let mut comps = [0.0f32; MAX_COMPONENTS];
284        for i in 0..self.params.components.min(MAX_COMPONENTS) {
285            let raw = self.bits.read(self.params.component_bits);
286            let min = self.params.decode.get(4 + i * 2).copied().unwrap_or(0.0);
287            let max = self
288                .params
289                .decode
290                .get(4 + i * 2 + 1)
291                .copied()
292                .unwrap_or(0.0);
293            #[expect(
294                clippy::cast_precision_loss,
295                reason = "component widths cap at 16 bits, exact in f32"
296            )]
297            let v = min + (raw as f32) * (max - min) / (self.params.component_max as f32);
298            if let Some(slot) = comps.get_mut(i) {
299                *slot = v;
300            }
301        }
302        if self.functions.is_empty() {
303            return self
304                .space
305                .try_to_rgb(comps.get(..self.params.components).unwrap_or(&[]))
306                .unwrap_or(Rgb::BLACK);
307        }
308        Rgb {
309            r: comps.first().copied().unwrap_or(0.0),
310            g: 0.0,
311            b: 0.0,
312        }
313    }
314
315    /// Read one flagged vertex: flag, coordinates, colour, then byte-align.
316    pub fn read_vertex(&mut self) -> Option<(u8, Vertex)> {
317        if !self.can_read_flag() {
318            return None;
319        }
320        let flag = self.read_flag();
321        if !self.can_read_coords() {
322            return None;
323        }
324        let point = self.read_coords();
325        if !self.can_read_color() {
326            return None;
327        }
328        let color = self.read_color();
329        self.bits.byte_align();
330        Some((flag, Vertex { point, color }))
331    }
332
333    /// Read one lattice row of `count` vertices — no flags.
334    ///
335    /// **Any failure discards the whole row**, which callers treat as the end
336    /// of the mesh.
337    pub fn read_vertex_row(&mut self, count: usize) -> Vec<Vertex> {
338        let mut row = Vec::with_capacity(count);
339        for _ in 0..count {
340            if !self.can_read_coords() {
341                return Vec::new();
342            }
343            let point = self.read_coords();
344            if !self.can_read_color() {
345                return Vec::new();
346            }
347            let color = self.read_color();
348            self.bits.byte_align();
349            row.push(Vertex { point, color });
350        }
351        row
352    }
353
354    /// Decode a free-form Gouraud mesh (type 4).
355    #[must_use]
356    pub fn read_free_form(&mut self) -> Vec<Triangle> {
357        let mut out = Vec::new();
358        let mut previous: [Option<Vertex>; 3] = [None; 3];
359        loop {
360            let Some((flag, vertex)) = self.read_vertex() else {
361                return out;
362            };
363            if flag == 0 {
364                // Start a fresh triangle: two more vertices follow
365                // unconditionally, and **their flags are read and discarded**.
366                let (Some((_, b)), Some((_, c))) = (self.read_vertex(), self.read_vertex()) else {
367                    return out;
368                };
369                previous = [Some(vertex), Some(b), Some(c)];
370            } else {
371                let (Some(p0), Some(p1), Some(p2)) = (previous[0], previous[1], previous[2]) else {
372                    return out;
373                };
374                // Flag 1 keeps the last edge; flags 2 **and 3** keep the
375                // first-and-last one, because only flag 1 is special-cased.
376                previous = if flag == 1 {
377                    [Some(p1), Some(p2), Some(vertex)]
378                } else {
379                    [Some(p0), Some(p2), Some(vertex)]
380                };
381            }
382            let (Some(a), Some(b), Some(c)) = (previous[0], previous[1], previous[2]) else {
383                return out;
384            };
385            out.push(Triangle {
386                vertices: [a, b, c],
387            });
388        }
389    }
390
391    /// Decode a lattice-form Gouraud mesh (type 5).
392    #[must_use]
393    pub fn read_lattice(&mut self, per_row: usize) -> Vec<Triangle> {
394        // Fewer than two vertices per row cannot form a quad.
395        if per_row < 2 {
396            return Vec::new();
397        }
398        let mut out = Vec::new();
399        let mut previous = self.read_vertex_row(per_row);
400        if previous.is_empty() {
401            return out;
402        }
403        loop {
404            let row = self.read_vertex_row(per_row);
405            if row.is_empty() {
406                return out;
407            }
408            for i in 0..per_row - 1 {
409                let (Some(a), Some(b), Some(c), Some(d)) = (
410                    previous.get(i),
411                    previous.get(i + 1),
412                    row.get(i),
413                    row.get(i + 1),
414                ) else {
415                    continue;
416                };
417                out.push(Triangle {
418                    vertices: [*a, *b, *c],
419                });
420                out.push(Triangle {
421                    vertices: [*b, *d, *c],
422                });
423            }
424            previous = row;
425        }
426    }
427
428    /// Decode a Coons (type 6) or tensor (type 7) patch mesh.
429    ///
430    /// `point_count` is a loop **invariant**: a flagged patch reuses four
431    /// points and two colours from its predecessor, which moves where this
432    /// patch starts reading, never how many records the format has.
433    //
434    // [oracle-bug] cpdf_streamcontentparser.cpp:120-122 gets that backwards
435    // in its bbox helper: inside `while (!stream.IsEOF())` it runs
436    // `point_count -= 4; color_count -= 2;` on every flagged patch, mutating
437    // the very variables declared as the record shape at :94-109. The
438    // subtraction is therefore **cumulative and permanent** — after the first
439    // flagged patch every later patch under-reads by four points, after the
440    // second by eight, and the counts run to zero and below. The bbox that
441    // results is too small, and §8.7.4.5.5-7 give the bbox no licence to omit
442    // a declared control point. That it is a slip rather than a reading of
443    // the spec is settled by PDFium's own second copy of the same loop:
444    // `cpdf_rendershading.cpp:900-917` uses per-iteration `iStartPoint`/
445    // `iStartColor` locals against an untouched `point_count` — correct, and
446    // what this function does. pdf.js has no counterpart (it composites mesh
447    // patches on a canvas and needs no bbox helper), so the oracle's own
448    // renderer is the independent reading here.
449    #[must_use]
450    pub fn read_patches(&mut self, kind: ShadingKind) -> Vec<Patch> {
451        // A tensor patch carries four extra interior points.
452        let point_count = if kind == ShadingKind::TensorMesh {
453            16
454        } else {
455            12
456        };
457        let mut out: Vec<Patch> = Vec::new();
458        let mut coords = vec![Point::ZERO; point_count];
459        let mut colors = [Rgb::BLACK; 4];
460        loop {
461            if !self.can_read_flag() {
462                return out;
463            }
464            let flag = self.read_flag();
465            // A flagged patch reuses one edge: four points and two colours
466            // come from its predecessor.
467            let (start_point, start_color) = if flag == 0 { (0, 0) } else { (4, 2) };
468            if flag != 0 {
469                let Some(previous) = out.last() else {
470                    return out;
471                };
472                for i in 0..4 {
473                    // The modulo is 12 even for a sixteen-point tensor patch,
474                    // so the four interior points are never reused.
475                    let source = (usize::from(flag) * 3 + i) % 12;
476                    if let (Some(slot), Some(p)) = (coords.get_mut(i), previous.points.get(source))
477                    {
478                        *slot = *p;
479                    }
480                }
481                if let (Some(slot), Some(c)) =
482                    (colors.first_mut(), previous.colors.get(usize::from(flag)))
483                {
484                    *slot = *c;
485                }
486                let next = previous
487                    .colors
488                    .get((usize::from(flag) + 1) % 4)
489                    .copied()
490                    .unwrap_or(Rgb::BLACK);
491                if let Some(slot) = colors.get_mut(1) {
492                    *slot = next;
493                }
494            }
495            // The inner breaks leave the remaining points and colours at
496            // their previous values rather than resetting them, and the patch
497            // is still emitted.
498            for i in start_point..point_count {
499                if !self.can_read_coords() {
500                    break;
501                }
502                let p = self.read_coords();
503                if let Some(slot) = coords.get_mut(i) {
504                    *slot = p;
505                }
506            }
507            for i in start_color..4 {
508                if !self.can_read_color() {
509                    break;
510                }
511                let c = self.read_color();
512                if let Some(slot) = colors.get_mut(i) {
513                    *slot = c;
514                }
515            }
516            out.push(Patch {
517                points: coords.clone().into(),
518                colors,
519            });
520            // No byte alignment between patches, unlike Gouraud's per-vertex
521            // alignment.
522        }
523    }
524}
525
526/// The four interior control points a Coons patch implies, from
527/// ISO 32000-2 §8.7.4.5.8.
528///
529/// A Coons patch states only its twelve boundary points; the surface's
530/// interior is derived, and the derivation is what makes a Coons patch and a
531/// tensor patch with these interiors identical.
532#[must_use]
533pub fn coons_interior(boundary: &[Point]) -> [Point; 4] {
534    let p = |i: usize| boundary.get(i).copied().unwrap_or(Point::ZERO);
535    // The boundary in the grid naming the formula uses.
536    let (p00, p01, p02, p03) = (p(0), p(1), p(2), p(3));
537    let (p13, p23) = (p(4), p(5));
538    let (p33, p32, p31, p30) = (p(6), p(7), p(8), p(9));
539    let (p20, p10) = (p(10), p(11));
540    let blend = |a: Point, b: Point, c: Point, d: Point, e: Point, f: Point, g: Point, h: Point| {
541        Point::new(
542            (-4.0 * a.x + 6.0 * (b.x + c.x) - 2.0 * (d.x + e.x) + 3.0 * (f.x + g.x) - h.x) / 9.0,
543            (-4.0 * a.y + 6.0 * (b.y + c.y) - 2.0 * (d.y + e.y) + 3.0 * (f.y + g.y) - h.y) / 9.0,
544        )
545    };
546    [
547        blend(p00, p01, p10, p03, p30, p31, p13, p33),
548        blend(p03, p02, p13, p00, p33, p32, p10, p30),
549        blend(p30, p31, p20, p33, p00, p01, p23, p03),
550        blend(p33, p32, p23, p30, p03, p02, p20, p00),
551    ]
552}
553
554#[cfg(test)]
555mod tests {
556    // Test fixtures quote the oracle's own vectors, compare floats exactly
557    // where the behaviour being pinned is exact, and index arrays whose
558    // length the fixture itself fixes.
559    #![allow(
560        clippy::unreadable_literal,
561        clippy::float_cmp,
562        clippy::indexing_slicing,
563        clippy::cast_precision_loss,
564        clippy::cast_possible_truncation,
565        reason = "test fixtures quote oracle vectors verbatim and compare exactly"
566    )]
567
568    use super::{MAX_COMPONENTS, MeshParams, MeshReader, ShadingKind};
569    use crate::color::ColorSpace;
570
571    fn params(components: usize, decode: &[f32]) -> Option<MeshParams> {
572        MeshParams::new(8, 8, 8, components, decode, ShadingKind::FreeFormMesh)
573    }
574
575    #[test]
576    fn bit_widths_are_validated_per_field() {
577        // Coordinates allow 24 and 32; components do not.
578        assert!(MeshParams::new(24, 8, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_some());
579        assert!(MeshParams::new(32, 8, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_some());
580        assert!(MeshParams::new(8, 24, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
581        assert!(MeshParams::new(8, 32, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
582        // Three is legal for neither.
583        assert!(MeshParams::new(3, 8, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
584        assert!(MeshParams::new(8, 3, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
585        // Flags allow only 2, 4 and 8.
586        assert!(MeshParams::new(8, 8, 3, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_none());
587        assert!(MeshParams::new(8, 8, 2, 1, &[0.0; 6], ShadingKind::FreeFormMesh).is_some());
588        // …and a type with no flags never checks them.
589        assert!(MeshParams::new(8, 8, 3, 1, &[0.0; 6], ShadingKind::LatticeMesh).is_some());
590    }
591
592    #[test]
593    fn the_decode_length_must_be_exact() {
594        // One component wants exactly six entries.
595        assert!(params(1, &[0.0; 6]).is_some());
596        assert!(params(1, &[0.0; 5]).is_none());
597        assert!(params(1, &[0.0; 7]).is_none());
598        // Three components want ten.
599        assert!(params(3, &[0.0; 10]).is_some());
600        assert!(params(3, &[0.0; 8]).is_none());
601    }
602
603    #[test]
604    fn more_than_eight_components_is_refused() {
605        assert!(params(MAX_COMPONENTS, &[0.0; 20]).is_some());
606        assert!(params(MAX_COMPONENTS + 1, &[0.0; 22]).is_none());
607    }
608
609    #[test]
610    fn flags_are_masked_to_two_bits() {
611        let p = MeshParams::new(8, 8, 8, 1, &[0.0; 6], ShadingKind::FreeFormMesh).expect("params");
612        // A flag byte of 0xFF masks down to 3, which is a reachable value.
613        let data = [0xFFu8; 8];
614        let space = ColorSpace::DeviceGray;
615        let mut reader = MeshReader::new(&data, &p, &space, &[]);
616        assert_eq!(reader.read_flag(), 3);
617    }
618
619    #[test]
620    fn a_lattice_row_shorter_than_two_yields_nothing() {
621        let p = MeshParams::new(
622            8,
623            8,
624            8,
625            1,
626            &[0.0f32, 1.0, 0.0, 1.0, 0.0, 1.0],
627            ShadingKind::LatticeMesh,
628        )
629        .expect("params");
630        let data = [0u8; 64];
631        let space = ColorSpace::DeviceGray;
632        let mut reader = MeshReader::new(&data, &p, &space, &[]);
633        assert!(reader.read_lattice(1).is_empty());
634        assert!(reader.read_lattice(0).is_empty());
635    }
636
637    #[test]
638    fn free_form_flag_three_behaves_as_flag_two() {
639        let p = MeshParams::new(
640            8,
641            8,
642            8,
643            1,
644            &[0.0f32, 255.0, 0.0, 255.0, 0.0, 1.0],
645            ShadingKind::FreeFormMesh,
646        )
647        .expect("params");
648        let space = ColorSpace::DeviceGray;
649        // Three flag-0 vertices, then one flag-2 and one flag-3 vertex.
650        let mut data = Vec::new();
651        for (flag, x, y) in [(0u8, 0u8, 0u8), (0, 10, 0), (0, 0, 10)] {
652            data.extend_from_slice(&[flag, x, y, 128]);
653        }
654        data.extend_from_slice(&[2, 20, 20, 128]);
655        let mut reader = MeshReader::new(&data, &p, &space, &[]);
656        let with_two = reader.read_free_form();
657
658        let mut data3 = Vec::new();
659        for (flag, x, y) in [(0u8, 0u8, 0u8), (0, 10, 0), (0, 0, 10)] {
660            data3.extend_from_slice(&[flag, x, y, 128]);
661        }
662        data3.extend_from_slice(&[3, 20, 20, 128]);
663        let mut reader = MeshReader::new(&data3, &p, &space, &[]);
664        let with_three = reader.read_free_form();
665        assert_eq!(with_two, with_three);
666        assert_eq!(with_two.len(), 2);
667    }
668
669    #[test]
670    fn a_truncated_stream_stops_rather_than_reading_past_the_end() {
671        let p = MeshParams::new(
672            8,
673            8,
674            8,
675            1,
676            &[0.0f32, 255.0, 0.0, 255.0, 0.0, 1.0],
677            ShadingKind::FreeFormMesh,
678        )
679        .expect("params");
680        let space = ColorSpace::DeviceGray;
681        // A flag and one coordinate, then nothing.
682        let data = [0u8, 5];
683        let mut reader = MeshReader::new(&data, &p, &space, &[]);
684        assert!(reader.read_free_form().is_empty());
685    }
686
687    #[test]
688    fn coons_interiors_are_derived_from_the_boundary() {
689        // A unit square's boundary produces interiors inside it.
690        let boundary: Vec<kurbo::Point> = (0..12)
691            .map(|i| {
692                let t = f64::from(i) / 12.0;
693                kurbo::Point::new(t, t)
694            })
695            .collect();
696        let interior = super::coons_interior(&boundary);
697        for p in interior {
698            assert!(p.x.is_finite() && p.y.is_finite());
699        }
700    }
701}