diff --git a/crates/metafile/src/canvas.rs b/crates/metafile/src/canvas.rs index 4ffa56bf..99f42071 100644 --- a/crates/metafile/src/canvas.rs +++ b/crates/metafile/src/canvas.rs @@ -16,7 +16,7 @@ const STOCK: u32 = 0x8000_0000; pub(crate) const PS_NULL: u32 = 5; /// Pen style bits; the rest of the style word holds end-cap, join and type flags. pub(crate) const PS_STYLE_MASK: u32 = 0x0F; -const BS_SOLID: u32 = 0; +pub(crate) const BS_SOLID: u32 = 0; pub(crate) const BS_NULL: u32 = 1; pub(crate) const BS_HATCHED: u32 = 2; /// BS_PATTERN, BS_DIBPATTERN and BS_DIBPATTERNPT: the brush paints a bitmap tile. @@ -442,6 +442,20 @@ impl Canvas { } } + /// The current world transform (EMF+ draws through it). + pub fn world(&self) -> Xform { + self.map.world + } + + /// Sets the pen and brush directly (EMF+ records name their own pen and brush). + pub fn set_pen(&mut self, pen: Pen) { + self.pen = pen; + } + + pub fn set_brush(&mut self, brush: Brush) { + self.brush = brush; + } + /// Pushes the drawing state. Past [`MAX_STACK`] levels the parse stops, since later restores /// could no longer match the file. pub fn save_dc(&mut self) { diff --git a/crates/metafile/src/emf.rs b/crates/metafile/src/emf.rs index 709537f7..21ad9b62 100644 --- a/crates/metafile/src/emf.rs +++ b/crates/metafile/src/emf.rs @@ -42,6 +42,7 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option), Error> { if let Some(px_mm) = device_px_per_mm(input) { cv.set_device_mm(px_mm); } + let mut plus = crate::emfplus::Plus::new(); let mut parsed = 0usize; let mut pos = hsize.max(88); while !cv.full() { @@ -54,7 +55,13 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option), Error> { if typ == 14 { break; } - record(&mut cv, typ, Bytes(rec)); + if typ == 70 && Bytes(rec).u32(12) == Some(crate::emfplus::EMF_PLUS) { + if let Some(data) = Bytes(rec).tail(16) { + plus.comment(&mut cv, data); + } + } else { + record(&mut cv, typ, Bytes(rec)); + } parsed += 1; let Some(next) = pos.checked_add(size) else { break }; pos = next; diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs new file mode 100644 index 00000000..3aef2842 --- /dev/null +++ b/crates/metafile/src/emfplus.rs @@ -0,0 +1,408 @@ +//! EMF+ (MS-EMFPLUS): GDI+ drawing records carried in EMF comment records. +//! +//! Most EMF+ files are *dual*: the same picture is also drawn with plain EMF records, which +//! [`crate::emf`] already plays, so this reader ignores their EMF+ part. An *EMF+ only* file has +//! nothing else to draw, so it used to show as an empty picture; for those this reader plays the +//! vector records: solid brushes and pens, paths, rectangles, polygons, lines, ellipses and +//! Béziers, under the world and page transforms, save/restore and containers. Images, text, +//! gradient brushes, clipping and arcs are not drawn yet. + +use crate::bytes::Bytes; +use crate::canvas::{BS_NULL, BS_SOLID, Brush, Canvas, PS_NULL, Pen, Xform, mul}; +use crate::geom::{Op, P}; + +/// "EMF+" as the comment identifier of an EMR_COMMENT holding EMF+ records. +pub(crate) const EMF_PLUS: u32 = 0x2B46_4D45; +const IDENTITY: Xform = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]; +/// GDI+ allows 64 objects; ids are a byte. +const MAX_OBJECTS: usize = 64; +const MAX_STACK: usize = 64; +/// Points per path or point list read, as the plain EMF reader caps paths. +const MAX_POINTS: usize = 100_000; + +#[derive(Clone, Debug)] +enum Obj { + Brush([u8; 4]), + Pen { color: [u8; 4], width: f64 }, + Path(Vec), +} + +pub(crate) struct Plus { + /// An EMF+ header said the file is EMF+ only, so these records are the picture. + active: bool, + dpi: f64, + world: Xform, + page: f64, + objects: Vec>, + stack: Vec<(u32, Xform, f64)>, +} + +impl Plus { + pub fn new() -> Self { + Plus { active: false, dpi: 96.0, world: IDENTITY, page: 1.0, objects: vec![None; MAX_OBJECTS], stack: Vec::new() } + } + + /// Plays the EMF+ records of one EMR_COMMENT payload (after the identifier). + pub fn comment(&mut self, cv: &mut Canvas, data: Bytes) { + let mut pos = 0usize; + while !cv.full() { + let (Some(typ), Some(flags), Some(size), Some(dsize)) = (data.u16(pos), data.u16(pos + 2), data.u32(pos + 4), data.u32(pos + 8)) else { + break; + }; + let (size, dsize) = (size as usize, dsize as usize); + if size < 12 || dsize > size - 12 { + break; + } + let Some(rec) = data.slice(pos + 12, dsize) else { break }; + let _ = self.record(cv, typ, flags, Bytes(rec)); + let Some(next) = pos.checked_add(size) else { break }; + pos = next; + } + } + + fn record(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> { + if typ == 0x4001 { + // Header: dual files also draw with EMF records, which win. + self.active = flags & 1 == 0; + if let Some(d) = r.u32(8).map(f64::from).filter(|d| *d > 0.0 && *d < 100_000.0) { + self.dpi = d; + } + return Some(()); + } + if !self.active { + return None; + } + let compressed = flags & 0x4000 != 0; + let id = usize::from(flags & 0xFF); + match typ { + 0x4008 => self.object(flags, r), + 0x400A => self.fill_rects(cv, flags, r), + 0x400B => self.with_pen(id, |p| p.draw_rects(cv, compressed, r)), + 0x400C => self.fill_polygon(cv, flags, r), + 0x400D => self.with_pen(id, |p| { + let pts = points(r, 4, r.u32(0)? as usize, compressed)?; + p.shape(cv, poly_ops(&pts, flags & 0x2000 != 0), false, true); + Some(()) + }), + 0x400E => self.fill_ellipse(cv, flags, r), + 0x400F => self.with_pen(id, |p| { + let rect = rects(r, 0, 1, compressed)?; + p.shape(cv, ellipse_ops(*rect.first()?), false, true); + Some(()) + }), + 0x4014 => self.fill_path(cv, flags, r), + 0x4015 => { + let path = self.path(id); + r.u32(0).and_then(|pen| { + self.with_pen(pen as usize, |p| { + p.shape(cv, path?, false, true); + Some(()) + }) + }) + } + 0x4019 => self.with_pen(id, |p| { + let pts = points(r, 4, r.u32(0)? as usize, compressed)?; + let (first, rest) = pts.split_first()?; + let mut ops = vec![Op::M(*first)]; + ops.extend(rest.as_chunks::<3>().0.iter().map(|c| Op::C(c[0], c[1], c[2]))); + p.shape(cv, ops, false, true); + Some(()) + }), + 0x4025 | 0x4028 => r.u32(0).map(|i| self.push(i)), + 0x4027 => r.u32(24).map(|i| self.push(i)), + 0x4026 | 0x4029 => r.u32(0).map(|i| self.pop(i)), + 0x402A => matrix(r, 0).map(|m| self.world = m), + 0x402B => { + self.world = IDENTITY; + Some(()) + } + 0x402C => matrix(r, 0).and_then(|m| self.apply(m, flags)), + 0x402D => self.apply([1.0, 0.0, 0.0, 1.0, f64::from(r.f32(0)?), f64::from(r.f32(4)?)], flags), + 0x402E => self.apply([f64::from(r.f32(0)?), 0.0, 0.0, f64::from(r.f32(4)?), 0.0, 0.0], flags), + 0x402F => { + let a = f64::from(r.f32(0)?).to_radians(); + self.apply([a.cos(), a.sin(), -a.sin(), a.cos(), 0.0, 0.0], flags) + } + 0x4030 => { + let scale = f64::from(r.f32(0)?); + let unit = match flags & 0xFF { + 3 => self.dpi / 72.0, + 4 => self.dpi, + 5 => self.dpi / 300.0, + 6 => self.dpi / 25.4, + _ => 1.0, + }; + let page = scale * unit; + if page.is_finite() && page > 0.0 && page < 1.0e6 { + self.page = page; + } + Some(()) + } + _ => None, + } + } + + /// Without the `A` flag a transform is applied before the world transform (prepended). + fn apply(&mut self, m: Xform, flags: u16) -> Option<()> { + self.world = if flags & 0x2000 != 0 { mul(self.world, m) } else { mul(m, self.world) }; + Some(()) + } + + fn push(&mut self, id: u32) { + if self.stack.len() < MAX_STACK { + self.stack.push((id, self.world, self.page)); + } + } + + fn pop(&mut self, id: u32) { + if let Some(i) = self.stack.iter().rposition(|s| s.0 == id) { + let (_, w, p) = self.stack[i]; + self.stack.truncate(i); + self.world = w; + self.page = p; + } + } + + fn object(&mut self, flags: u16, r: Bytes) -> Option<()> { + if flags & 0x8000 != 0 { + // Continued (split) objects are images and large paths; not read yet. + return None; + } + let id = usize::from(flags & 0xFF); + let obj = match (flags >> 8) & 0x7F { + 1 => brush(r, 0).map(|(c, _)| Obj::Brush(c)), + 2 => pen(r), + 3 => path(r).map(Obj::Path), + _ => None, + }; + if let Some(slot) = self.objects.get_mut(id) { + *slot = obj; + } + Some(()) + } + + fn path(&self, id: usize) -> Option> { + match self.objects.get(id)? { + Some(Obj::Path(ops)) => Some(ops.clone()), + _ => None, + } + } + + /// The brush colour of a fill record: an ARGB value with the `S` flag, else a brush object. + fn brush_of(&self, flags: u16, v: u32) -> Option<[u8; 4]> { + if flags & 0x8000 != 0 { + return Some(argb(v)); + } + match self.objects.get(v as usize)? { + Some(Obj::Brush(c)) => Some(*c), + _ => None, + } + } + + fn with_pen(&mut self, id: usize, f: impl FnOnce(&mut Painter) -> Option) -> Option { + let Some(Some(Obj::Pen { color, width })) = self.objects.get(id).cloned() else { return None }; + let mut p = Painter { xf: mul(self.world, [self.page, 0.0, 0.0, self.page, 0.0, 0.0]), pen: Some((color, width)), fill: None }; + f(&mut p) + } + + fn filler(&self, color: [u8; 4]) -> Painter { + Painter { xf: mul(self.world, [self.page, 0.0, 0.0, self.page, 0.0, 0.0]), pen: None, fill: Some(color) } + } + + fn fill_rects(&mut self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> { + let c = self.brush_of(flags, r.u32(0)?)?; + let rs = rects(r, 8, r.u32(4)? as usize, flags & 0x4000 != 0)?; + let p = self.filler(c); + for rc in rs { + p.shape(cv, rect_ops(rc), true, false); + } + Some(()) + } + + fn fill_polygon(&mut self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> { + let c = self.brush_of(flags, r.u32(0)?)?; + let pts = points(r, 8, r.u32(4)? as usize, flags & 0x4000 != 0)?; + self.filler(c).shape(cv, poly_ops(&pts, true), true, false); + Some(()) + } + + fn fill_ellipse(&mut self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> { + let c = self.brush_of(flags, r.u32(0)?)?; + let rect = *rects(r, 4, 1, flags & 0x4000 != 0)?.first()?; + self.filler(c).shape(cv, ellipse_ops(rect), true, false); + Some(()) + } + + fn fill_path(&mut self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> { + let c = self.brush_of(flags, r.u32(0)?)?; + let ops = self.path(usize::from(flags & 0xFF))?; + self.filler(c).shape(cv, ops, true, false); + Some(()) + } +} + +/// Draws through the canvas with EMF+'s transform and its own pen or brush, then puts the +/// canvas's GDI state back. +struct Painter { + xf: Xform, + pen: Option<([u8; 4], f64)>, + fill: Option<[u8; 4]>, +} + +impl Painter { + fn shape(&self, cv: &mut Canvas, ops: Vec, fill: bool, stroke: bool) { + let world = cv.world(); + cv.save_dc(); + cv.modify_world(mul(self.xf, world), 4); + let (pc, pw) = self.pen.unwrap_or(([0, 0, 0, 0], 0.0)); + cv.set_pen(Pen { style: if self.pen.is_some() { 0 } else { PS_NULL }, width: pw, color: pc }); + cv.set_brush(match self.fill { + Some(c) => Brush::new(BS_SOLID, c), + None => Brush::new(BS_NULL, [0, 0, 0, 0]), + }); + cv.shape(&ops, fill, stroke); + cv.restore_dc(-1); + } + + fn draw_rects(&self, cv: &mut Canvas, compressed: bool, r: Bytes) -> Option<()> { + for rc in rects(r, 4, r.u32(0)? as usize, compressed)? { + self.shape(cv, rect_ops(rc), false, true); + } + Some(()) + } +} + +/// ARGB (0xAARRGGBB) to RGBA. +fn argb(v: u32) -> [u8; 4] { + [(v >> 16) as u8, (v >> 8) as u8, v as u8, (v >> 24) as u8] +} + +fn matrix(r: Bytes, off: usize) -> Option { + let mut m = [0.0; 6]; + for (i, v) in m.iter_mut().enumerate() { + *v = f64::from(r.f32(off + i * 4)?); + } + m.iter().all(|v| v.is_finite()).then_some(m) +} + +/// A brush object at `off`: its colour and byte length. Only solid brushes are read; other kinds +/// (hatch, texture, gradients) are given their first colour so shapes still show. +fn brush(r: Bytes, off: usize) -> Option<([u8; 4], usize)> { + let kind = r.u32(off + 4)?; + let c = argb(r.u32(off + 8)?); + let c = if kind == 1 { argb(r.u32(off + 12)?) } else { c }; + Some((c, 12)) +} + +/// A pen object: version, type, then pen data (flags, unit, width, optional fields), then a brush. +fn pen(r: Bytes) -> Option { + let pflags = r.u32(8)?; + let width = f64::from(r.f32(16)?); + let mut off = 20usize; + for (bit, len) in [(0x1, 24), (0x2, 4), (0x4, 4), (0x8, 4), (0x10, 4), (0x20, 4), (0x40, 4), (0x80, 4)] { + if pflags & bit != 0 { + off += len; + } + } + for bit in [0x100, 0x200, 0x400] { + if pflags & bit != 0 { + off += if bit == 0x200 { 4 } else { 4 + 4 * (r.u32(off)? as usize).min(MAX_POINTS) }; + } + } + for bit in [0x800, 0x1000] { + if pflags & bit != 0 { + off += 4 + (r.u32(off)? as usize).min(1 << 20); + } + } + let (color, _) = brush(r, off)?; + Some(Obj::Pen { color, width: if width.is_finite() { width.clamp(0.0, 1.0e5) } else { 0.0 } }) +} + +/// A path object: points, then one type byte per point (start, line, Bézier; 0x80 closes). +fn path(r: Bytes) -> Option> { + let n = (r.u32(4)? as usize).min(MAX_POINTS); + let pflags = r.u32(8)?; + if pflags & 0x1800 != 0 { + // Relative (0x0800) and run-length encoded (0x1000) points: not read yet. + return None; + } + let compressed = pflags & 0x4000 != 0; + let pts = points(r, 12, n, compressed)?; + let toff = 12 + n * if compressed { 4 } else { 8 }; + let types = r.slice(toff, n)?; + let mut ops = Vec::with_capacity(n + 4); + let mut i = 0; + while i < n { + let (t, p) = (types[i], pts[i]); + match t & 0x07 { + 0 => ops.push(Op::M(p)), + 3 if i + 2 < n => { + ops.push(Op::C(p, pts[i + 1], pts[i + 2])); + let last = types[i + 2]; + i += 2; + if last & 0x80 != 0 { + ops.push(Op::Z); + } + i += 1; + continue; + } + _ => ops.push(Op::L(p)), + } + if t & 0x80 != 0 { + ops.push(Op::Z); + } + i += 1; + } + Some(ops) +} + +/// `n` points at `off`: 16-bit integers when compressed, else 32-bit floats. +fn points(r: Bytes, off: usize, n: usize, compressed: bool) -> Option> { + let n = n.min(MAX_POINTS); + let mut v = Vec::with_capacity(n); + for i in 0..n { + v.push(if compressed { + (f64::from(r.i16(off + i * 4)?), f64::from(r.i16(off + i * 4 + 2)?)) + } else { + let (x, y) = (f64::from(r.f32(off + i * 8)?), f64::from(r.f32(off + i * 8 + 4)?)); + if !(x.is_finite() && y.is_finite()) { + return None; + } + (x, y) + }); + } + Some(v) +} + +/// `n` rectangles (x, y, w, h) at `off`, as `[l, t, r, b]`. +fn rects(r: Bytes, off: usize, n: usize, compressed: bool) -> Option> { + let pts = points(r, off, n.min(MAX_POINTS / 2).checked_mul(2)?, compressed)?; + Some(pts.as_chunks::<2>().0.iter().map(|c| [c[0].0, c[0].1, c[0].0 + c[1].0, c[0].1 + c[1].1]).collect()) +} + +fn rect_ops([l, t, r, b]: [f64; 4]) -> Vec { + crate::geom::rect(l, t, r, b) +} + +fn poly_ops(pts: &[P], close: bool) -> Vec { + let mut ops: Vec = pts.iter().enumerate().map(|(i, p)| if i == 0 { Op::M(*p) } else { Op::L(*p) }).collect(); + if close { + ops.push(Op::Z); + } + ops +} + +/// An ellipse in its bounding box, as four cubic arcs. +fn ellipse_ops([l, t, r, b]: [f64; 4]) -> Vec { + let (cx, cy, rx, ry) = ((l + r) / 2.0, (t + b) / 2.0, (r - l) / 2.0, (b - t) / 2.0); + let k = 0.552_284_75; + let (ox, oy) = (rx * k, ry * k); + vec![ + Op::M((cx + rx, cy)), + Op::C((cx + rx, cy + oy), (cx + ox, cy + ry), (cx, cy + ry)), + Op::C((cx - ox, cy + ry), (cx - rx, cy + oy), (cx - rx, cy)), + Op::C((cx - rx, cy - oy), (cx - ox, cy - ry), (cx, cy - ry)), + Op::C((cx + ox, cy - ry), (cx + rx, cy - oy), (cx + rx, cy)), + Op::Z, + ] +} diff --git a/crates/metafile/src/lib.rs b/crates/metafile/src/lib.rs index d7d98501..cdf18154 100644 --- a/crates/metafile/src/lib.rs +++ b/crates/metafile/src/lib.rs @@ -17,6 +17,7 @@ mod bytes; mod canvas; mod dib; mod emf; +mod emfplus; mod geom; mod place; mod text; diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 271cc83b..e3c4c923 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -1518,3 +1518,109 @@ fn pattern_brush_pixels_count_against_the_byte_budget() { let p = wmf200(&recs); assert!(p.items.is_empty(), "the parse stopped at the budget: {:?}", p.items.len()); } + +// ---- EMF+ ---- + +fn f32s(v: &[f32]) -> Vec { + v.iter().flat_map(|x| x.to_le_bytes()).collect() +} + +/// One EMF+ record: type, flags, size, data size, data (padded to 4). +fn prec(typ: u16, flags: u16, data: &[u8]) -> Vec { + let mut d = data.to_vec(); + while !d.len().is_multiple_of(4) { + d.push(0); + } + let mut v = typ.to_le_bytes().to_vec(); + v.extend(flags.to_le_bytes()); + v.extend(((12 + d.len()) as u32).to_le_bytes()); + v.extend((d.len() as u32).to_le_bytes()); + v.extend(d); + v +} + +/// An EMR_COMMENT carrying EMF+ records. +fn plus_comment(recs: &[Vec]) -> Vec { + let body = recs.concat(); + let mut p = ((body.len() + 4) as u32).to_le_bytes().to_vec(); + p.extend(0x2B46_4D45u32.to_le_bytes()); + p.extend(body); + erec(70, &p) +} + +/// EMF+ header: dual (bit 0) or EMF+ only, 96 dpi. +fn plus_header(dual: bool) -> Vec { + prec(0x4001, u16::from(dual), &cat(vec![h32(0xDBC0_1002), h32(0), h32(96), h32(96)])) +} + +fn solid_pen(argb: u32, width: f32) -> Vec { + cat(vec![h32(0xDBC0_1002), h32(0), h32(0), h32(0), f32s(&[width]), h32(0xDBC0_1002), h32(0), h32(argb)]) +} + +#[test] +fn emfplus_only_fill_rects_with_argb_colour() { + let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_80FF), h32(1), f32s(&[10.0, 20.0, 30.0, 40.0])])); + let p = parse(&emf(&[plus_comment(&[plus_header(false), fill])], [0; 4], [0; 4])).unwrap(); + assert_eq!(p.items.len(), 1); + let (segs, fill, stroke, _) = path_segs(&p.items[0]); + assert_eq!(fill, Some([0x00, 0x80, 0xFF, 0xFF])); + assert_eq!(stroke, None); + // Geometry is placed relative to the drawing's bounding box (no frame given). + assert_eq!((p.width, p.height), (30.0, 40.0)); + assert_eq!(segs[0], Seg::Move(0.0, 0.0)); + assert!(segs.contains(&Seg::Line(30.0, 40.0))); +} + +#[test] +fn emfplus_dual_files_draw_only_the_emf_records() { + let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_0000), h32(1), f32s(&[0.0, 0.0, 5.0, 5.0])])); + let gdi = erec(43, &cat(vec![e32(0), e32(0), e32(9), e32(9)])); + let p = parse(&emf(&[plus_comment(&[plus_header(true), fill]), gdi], [0; 4], [0; 4])).unwrap(); + assert_eq!(p.items.len(), 1); + assert_eq!(path_segs(&p.items[0]).1, Some([255, 255, 255, 255])); +} + +#[test] +fn emfplus_pen_object_draws_lines_under_a_scale_transform() { + let pen = prec(0x4008, 0x0200 | 3, &solid_pen(0xFF00_FF00, 2.0)); + let scale = prec(0x402E, 0, &f32s(&[2.0, 3.0])); + let lines = prec(0x400D, 3, &cat(vec![h32(2), f32s(&[1.0, 1.0, 4.0, 2.0])])); + let p = parse(&emf(&[plus_comment(&[plus_header(false), pen, scale, lines])], [0; 4], [0; 4])).unwrap(); + let (segs, fill, stroke, _) = path_segs(&p.items[0]); + // (1,1)-(4,2) scaled by (2,3) is (2,3)-(8,6), placed relative to its bounding box. + assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(6.0, 3.0)]); + assert_eq!(fill, None); + let (c, w) = stroke.unwrap(); + assert_eq!(c, [0, 255, 0, 255]); + assert!(w > 0.0); +} + +#[test] +fn emfplus_path_object_fills_with_brush_object_and_restore_undoes_transform() { + let brush = prec(0x4008, 0x0100 | 1, &cat(vec![h32(0xDBC0_1002), h32(0), h32(0x8012_3456)])); + // Triangle: start, line, line|close. + let path = prec(0x4008, 0x0300 | 2, &cat(vec![h32(0xDBC0_1002), h32(3), h32(0), f32s(&[0.0, 0.0, 10.0, 0.0, 0.0, 10.0]), vec![0, 1, 0x81]])); + let save = prec(0x4025, 0, &h32(7)); + let mv = prec(0x402D, 0, &f32s(&[100.0, 0.0])); + let restore = prec(0x4026, 0, &h32(7)); + let fill = prec(0x4014, 2, &h32(1)); + let p = parse(&emf(&[plus_comment(&[plus_header(false), brush, path, save, mv, restore, fill])], [0; 4], [0; 4])).unwrap(); + let (segs, fill, _, _) = path_segs(&p.items[0]); + assert_eq!(fill, Some([0x12, 0x34, 0x56, 0x80])); + assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(10.0, 0.0), Seg::Line(0.0, 10.0), Seg::Close]); +} + +#[test] +fn emfplus_garbage_records_never_panic() { + let mut s = 0x1234_5678u32; + for _ in 0..300 { + let mut recs = vec![plus_header(false)]; + for _ in 0..6 { + s = s.wrapping_mul(1_103_515_245).wrapping_add(12_345); + let typ = 0x4000 + (s >> 16) as u16 % 0x40; + let data: Vec = (0..(s % 64)).map(|i| (s.rotate_left(i)) as u8).collect(); + recs.push(prec(typ, (s >> 8) as u16, &data)); + } + let _ = parse(&emf(&[plus_comment(&recs)], [0; 4], [0; 4])); + } +}