diff --git a/crates/metafile/src/canvas.rs b/crates/metafile/src/canvas.rs index d553c8195..fa8bfdcf9 100644 --- a/crates/metafile/src/canvas.rs +++ b/crates/metafile/src/canvas.rs @@ -8,6 +8,7 @@ use crate::{MAX_BYTES, MAX_ITEMS, MAX_OBJECTS, MAX_PATH_OPS, MAX_STACK}; pub(crate) const MM_TEXT: u32 = 1; const MM_ISOTROPIC: u32 = 7; +const MM_ANISOTROPIC: u32 = 8; 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. @@ -15,6 +16,8 @@ pub(crate) const PS_STYLE_MASK: u32 = 0x0F; const BS_SOLID: u32 = 0; pub(crate) const BS_NULL: u32 = 1; const WINDING: u32 = 2; +/// SETROP2 mix mode that leaves the destination alone: outlines and fills draw nothing. +const R2_NOP: u32 = 11; const PATCOPY: u32 = 0x00F0_0021; const BLACKNESS: u32 = 0x0000_0042; const WHITENESS: u32 = 0x00FF_0062; @@ -135,6 +138,19 @@ impl Mapping { det * (sx.abs() + sy.abs()) / 2.0 } + /// True when the window-to-viewport extents set the scale (the isotropic and anisotropic modes). + pub fn uses_extents(&self) -> bool { + matches!(self.mode, MM_ISOTROPIC | MM_ANISOTROPIC) + } + + pub fn window_ext(&self) -> P { + self.we + } + + pub fn viewport_ext(&self) -> P { + self.ve + } + /// The window in device units as `[x, y, w, h]` with positive extents, used as the picture frame /// of a plain WMF. pub fn window_rect(&self) -> [f64; 4] { @@ -160,6 +176,7 @@ struct Saved { pen: Pen, brush: Brush, even_odd: bool, + nop: bool, } pub(crate) struct Canvas { @@ -167,6 +184,8 @@ pub(crate) struct Canvas { pub map: Mapping, /// True once SETWINDOWEXT was seen, so a plain WMF has a frame to use. pub window_set: bool, + /// True once the file set the viewport extent itself (not the player, see [`Canvas::play_into`]). + pub viewport_set: bool, saved: Vec, /// Bytes held by the items so far (path ops and pixels), capped at [`MAX_BYTES`]. used: usize, @@ -174,6 +193,8 @@ pub(crate) struct Canvas { pen: Pen, brush: Brush, even_odd: bool, + /// The mix mode is R2_NOP: shapes draw nothing. + nop: bool, pos: P, in_path: bool, path: Vec, @@ -186,12 +207,14 @@ impl Canvas { items: Vec::new(), map: Mapping::identity(), window_set: false, + viewport_set: false, saved: Vec::new(), used: 0, objects: Vec::new(), pen: Pen { style: 0, width: 0.0, color: [0, 0, 0, 255] }, brush: Brush { style: BS_SOLID, color: [255, 255, 255, 255] }, even_odd: true, + nop: false, pos: (0.0, 0.0), in_path: false, path: Vec::new(), @@ -232,6 +255,22 @@ impl Canvas { pub fn set_viewport_ext(&mut self, e: P) { self.map.ve = e; + self.viewport_set = true; + } + + /// Sets up the mapping the way a player does before it plays a WMF into a target: anisotropic, + /// `window` (`[x, y, w, h]` in logical units) onto a viewport of `size` at the origin. The file's + /// own window records then replace the window; the viewport stays unless the file sets one. + /// Without a window (a plain WMF) only the viewport extent is set: the map mode and viewport + /// origin stay the file's. + pub fn play_into(&mut self, window: Option<[f64; 4]>, size: P) { + if let Some([x, y, w, h]) = window { + self.map.mode = MM_ANISOTROPIC; + self.map.wo = (x, y); + self.map.we = (w, h); + self.map.vo = (0.0, 0.0); + } + self.map.ve = size; } pub fn set_map_mode(&mut self, mode: u32) { @@ -249,6 +288,11 @@ impl Canvas { self.even_odd = mode != WINDING; } + /// SETROP2: only R2_NOP changes what is drawn (nothing); the other mix modes draw as copy. + pub fn set_rop2(&mut self, mode: u32) { + self.nop = mode == R2_NOP; + } + /// EMF SETWORLDTRANSFORM (`mode` 4) and MODIFYWORLDTRANSFORM (1 identity, 2 left, 3 right, 4 set). /// An unknown mode leaves the world transform as it was. pub fn modify_world(&mut self, x: Xform, mode: u32) { @@ -265,7 +309,7 @@ impl Canvas { /// could no longer match the file. pub fn save_dc(&mut self) { if self.saved.len() < MAX_STACK { - self.saved.push(Saved { map: self.map, pen: self.pen, brush: self.brush, even_odd: self.even_odd }); + self.saved.push(Saved { map: self.map, pen: self.pen, brush: self.brush, even_odd: self.even_odd, nop: self.nop }); } else { self.full = true; } @@ -281,6 +325,7 @@ impl Canvas { self.pen = s.pen; self.brush = s.brush; self.even_odd = s.even_odd; + self.nop = s.nop; } /// Stores an object in `slot` (EMF index), or in the first free slot (WMF handle table). @@ -381,6 +426,9 @@ impl Canvas { if self.in_path { return self.push_path_ops(dev); } + if self.nop { + return; + } let fill = if fill { self.fill_color() } else { None }; let stroke = if stroke { self.stroke_pen() } else { None }; if (fill.is_none() && stroke.is_none()) || dev.len() < 2 { @@ -460,6 +508,9 @@ impl Canvas { /// FILLPATH, STROKEPATH and STROKEANDFILLPATH: paints the bracketed path and empties it. pub fn paint_path(&mut self, fill: bool, stroke: bool) { let ops = std::mem::take(&mut self.path); + if self.nop { + return; + } let fill = if fill { self.fill_color() } else { None }; let stroke = if stroke { self.stroke_pen() } else { None }; if ops.len() < 2 || (fill.is_none() && stroke.is_none()) { diff --git a/crates/metafile/src/emf.rs b/crates/metafile/src/emf.rs index 6f3248697..027cfae9f 100644 --- a/crates/metafile/src/emf.rs +++ b/crates/metafile/src/emf.rs @@ -88,6 +88,7 @@ fn record(cv: &mut Canvas, typ: u32, r: Bytes) { 12 => xy32(r, 8).map(|p| cv.set_viewport_org(p)), 17 => r.u32(8).map(|m| cv.set_map_mode(m)), 19 => r.u32(8).map(|m| cv.set_polyfill(m)), + 20 => r.u32(8).map(|m| cv.set_rop2(m)), 33 => { cv.save_dc(); Some(()) diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 9fab9bea7..4ee32578e 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -927,3 +927,92 @@ fn take_pixels_returns_only_drawable_bitmaps() { assert!(pic(1.0, 2.0).has_size()); assert!(!pic(0.0, 2.0).has_size() && !pic(f32::NAN, 2.0).has_size() && !pic(1.0, f32::INFINITY).has_size()); } + +/// The rectangle a WMF RECTANGLE record covers, from its path's corners. +fn rect_extent(item: &Item) -> (f32, f32, f32, f32) { + let (segs, ..) = path_segs(item); + let pts: Vec<(f32, f32)> = segs + .iter() + .filter_map(|s| match s { + Seg::Move(x, y) | Seg::Line(x, y) => Some((*x, *y)), + _ => None, + }) + .collect(); + let (xs, ys): (Vec, Vec) = pts.into_iter().unzip(); + let lo = |v: &[f32]| v.iter().copied().fold(f32::MAX, f32::min); + let hi = |v: &[f32]| v.iter().copied().fold(f32::MIN, f32::max); + (lo(&xs), lo(&ys), hi(&xs), hi(&ys)) +} + +#[test] +fn plain_anisotropic_wmf_without_a_viewport_fills_its_window() { + // Anisotropic, window 200 x 100, no viewport (a logo strip saved without a placeable header): the + // window is the target, not a 1 x 1 viewport that squeezes the picture to a point. + let recs = [ + wrec(0x0103, &h16(8)), + wrec(0x020B, &cat(vec![h16(0), h16(0)])), + wrec(0x020C, &cat(vec![h16(100), h16(200)])), + wrec(0x041B, &cat(vec![h16(100), h16(200), h16(0), h16(0)])), + ]; + let p = parse(&wmf(&recs, None)).unwrap(); + assert_eq!((p.width, p.height), (200.0, 100.0)); + let (x0, y0, x1, y1) = rect_extent(&p.items[0]); + assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 200.0) && close(y1, 100.0), "{x0} {y0} {x1} {y1}"); +} + +#[test] +fn placeable_wmf_window_records_replace_the_bounds() { + // The placeable bounds say 1440 square (72 points); the file then draws in a window centred on the + // origin, as a coat of arms saved by a drawing program does. The window maps onto the 72-point + // target, so the drawing fills it instead of being blown up around the bounds. + let recs = [ + wrec(0x020C, &cat(vec![h16(2000), h16(2000)])), + wrec(0x020B, &cat(vec![h16(-1000), h16(-1000)])), + wrec(0x041B, &cat(vec![h16(1000), h16(1000), h16(-1000), h16(-1000)])), + ]; + let p = parse(&wmf(&recs, Some([238, 539, 1678, 1979]))).unwrap(); + assert_eq!((p.width, p.height), (72.0, 72.0)); + let (x0, y0, x1, y1) = rect_extent(&p.items[0]); + assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 72.0) && close(y1, 72.0), "{x0} {y0} {x1} {y1}"); +} + +#[test] +fn placeable_wmf_keeps_its_target_when_the_window_changes_shape() { + // Bounds 600 x 300 (30 x 15 points), then a portrait window 100 x 200 for a bitmap-sized drawing: + // the drawing still fills the target (Word stretches it to the picture's box). + let recs = [wrec(0x0103, &h16(8)), wrec(0x020C, &cat(vec![h16(200), h16(100)])), wrec(0x041B, &cat(vec![h16(200), h16(100), h16(0), h16(0)]))]; + let p = parse(&wmf(&recs, Some([0, 0, 600, 300]))).unwrap(); + assert_eq!((p.width, p.height), (30.0, 15.0)); + let (x0, y0, x1, y1) = rect_extent(&p.items[0]); + assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 30.0) && close(y1, 15.0), "{x0} {y0} {x1} {y1}"); +} + +#[test] +fn rop2_nop_draws_nothing_until_the_mode_changes() { + // A coat of arms that first draws its bounding rectangle with the R2_NOP mix mode (no output), + // then its shapes with R2_COPYPEN. Restoring a saved state brings R2_NOP back. + let rect = |n: i32| wrec(0x041B, &cat(vec![h16(n), h16(n), h16(0), h16(0)])); + let recs = [wrec(0x0104, &h16(11)), rect(100), wrec(0x001E, &[]), wrec(0x0104, &h16(13)), rect(50), wrec(0x0127, &h16(-1)), rect(20)]; + let p = parse(&wmf(&recs, Some([0, 0, 100, 100]))).unwrap(); + assert_eq!(p.items.len(), 1, "{:?}", p.items); + let (_, _, x1, y1) = rect_extent(&p.items[0]); + // 50 of 100 units in a 5-point picture. + assert!(close(x1, 2.5) && close(y1, 2.5), "{x1} {y1}"); +} + +#[test] +fn plain_wmf_target_comes_from_a_window_that_was_read() { + // A window record too short to read, then the real one: the target is the real window, not the + // 1 x 1 default the short record left in place. A viewport origin set before it is kept. + let recs = [ + wrec(0x0103, &h16(8)), + wrec(0x020D, &cat(vec![h16(0), h16(0)])), + wrec(0x020C, &[]), + wrec(0x020C, &cat(vec![h16(100), h16(200)])), + wrec(0x041B, &cat(vec![h16(100), h16(200), h16(0), h16(0)])), + ]; + let p = parse(&wmf(&recs, None)).unwrap(); + assert_eq!((p.width, p.height), (200.0, 100.0)); + let (x0, y0, x1, y1) = rect_extent(&p.items[0]); + assert!(close(x0, 0.0) && close(y0, 0.0) && close(x1, 200.0) && close(y1, 100.0), "{x0} {y0} {x1} {y1}"); +} diff --git a/crates/metafile/src/wmf.rs b/crates/metafile/src/wmf.rs index e051471d8..d221a7655 100644 --- a/crates/metafile/src/wmf.rs +++ b/crates/metafile/src/wmf.rs @@ -37,7 +37,13 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option), Error> { return Err(Error::BadHeader); } let mut pos = hdr.checked_add(hsize.checked_mul(2).ok_or(Error::BadHeader)?).ok_or(Error::BadHeader)?; + // Played into a target like Word does: the placeable bounds are the first window, the target (the + // picture size) the viewport. A plain WMF's target is its first window. let mut cv = Canvas::new(); + if let Some((c, size)) = bounds { + cv.play_into(Some([c[0], c[1], c[2] - c[0], c[3] - c[1]]), size); + } + let mut player_viewport = bounds.is_some(); let mut parsed = 0usize; while !cv.full() { let (Some(words), Some(func)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u16(o))) else { break }; @@ -47,6 +53,11 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option), Error> { break; } record(&mut cv, func, Bytes(rec)); + if func == 0x020C && cv.window_set && !player_viewport && !cv.viewport_set { + let we = cv.map.window_ext(); + cv.play_into(None, (we.0.abs(), we.1.abs())); + player_viewport = true; + } parsed += 1; let Some(next) = pos.checked_add(len) else { break }; pos = next; @@ -55,12 +66,19 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option), Error> { return Err(Error::NoRecords); } let frame = match bounds { - // The drawing is in device units, so the logical bounds go through the final mapping. + // Played into its target: the frame is the target. + Some((_, size)) if !cv.viewport_set && cv.map.uses_extents() => Some(Frame { rect: [0.0, 0.0, size.0, size.1], size: Some(size) }), + // The file set its own viewport, so the drawing is in its device units and the logical bounds + // go through the final mapping. Some((c, size)) => { let (a, b) = (cv.map.device((c[0], c[1])), cv.map.device((c[2], c[3]))); let rect = [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()]; (rect[2] > 0.0 && rect[3] > 0.0).then_some(Frame { rect, size: Some(size) }) } + None if player_viewport && !cv.viewport_set && cv.map.uses_extents() => { + let ve = cv.map.viewport_ext(); + Some(Frame { rect: [0.0, 0.0, ve.0, ve.1], size: None }) + } None if cv.window_set => Some(Frame { rect: cv.map.window_rect(), size: None }), None => None, }; @@ -74,6 +92,7 @@ fn record(cv: &mut Canvas, func: u16, r: Bytes) { let _ = match func { 0x0103 => r.u16(6).map(|m| cv.set_map_mode(u32::from(m))), 0x0106 => r.u16(6).map(|m| cv.set_polyfill(u32::from(m))), + 0x0104 => r.u16(6).map(|m| cv.set_rop2(u32::from(m))), // Y is stored before X in these records. 0x020B => xy16(r, 6).map(|p| cv.set_window_org(p)), 0x020C => xy16(r, 6).map(|p| cv.set_window_ext(p)),