diff --git a/crates/metafile/src/canvas.rs b/crates/metafile/src/canvas.rs index 2745a465..f286dc61 100644 --- a/crates/metafile/src/canvas.rs +++ b/crates/metafile/src/canvas.rs @@ -332,8 +332,8 @@ pub(crate) struct Canvas { clip: Option>, /// Device bounds of the picture, so excluding from the default clip has a surface to cut. surface: Option<[f64; 4]>, - /// EMF+ device clip rectangle `[l, t, r, b]`: drawing is cut to it geometrically. - plus_clip: Option<[f64; 4]>, + /// EMF+ convex device clip polygon: drawing is cut to it geometrically. + plus_clip: Option>, } impl Canvas { @@ -374,12 +374,13 @@ impl Canvas { } fn push(&mut self, raw: Raw) { - match self.plus_clip { + match self.plus_clip.take() { None => self.push_raw(raw), Some(c) => { - for r in crate::cliprect::clip_raw(raw, c) { + for r in crate::cliprect::clip_raw(raw, &c) { self.push_raw(r); } + self.plus_clip = Some(c); } } } @@ -509,8 +510,8 @@ impl Canvas { self.brush = brush; } - /// Sets the device clip rectangle `[l, t, r, b]` (None: unclipped). - pub fn set_clip(&mut self, clip: Option<[f64; 4]>) { + /// Sets the EMF+ convex device clip polygon (None: unclipped). + pub fn set_clip(&mut self, clip: Option>) { self.plus_clip = clip; } @@ -531,6 +532,11 @@ impl Canvas { self.text.font = Some(Arc::new(font)); } + /// Pushes an item already in device units (a nested metafile's drawing). + pub fn push_device(&mut self, raw: Raw) { + self.push(raw); + } + /// Fills logical `ops` with a ready-made tile, laid from the device origin. pub fn tile_shape(&mut self, ops: &[Op], tile: Arc) { let dev: Vec = ops.iter().map(|o| self.map_op(*o)).collect(); diff --git a/crates/metafile/src/cliprect.rs b/crates/metafile/src/cliprect.rs index 652ad35b..8290dd37 100644 --- a/crates/metafile/src/cliprect.rs +++ b/crates/metafile/src/cliprect.rs @@ -1,7 +1,8 @@ -//! Geometric clipping of drawn output to a device rectangle (EMF+ clip rectangles, paths and regions are -//! reduced to their bounding rectangle). Shapes wholly inside pass unchanged; shapes crossing the -//! edge are flattened and cut: fills with Sutherland–Hodgman per subpath, outlines with -//! Liang–Barsky per segment. Bitmaps are cropped and text gets a clip rectangle. +//! Clipping drawn output to a convex device polygon (EMF+ clip rectangles, and clip paths and +//! regions that are convex; others are reduced to their bounding rectangle). Shapes wholly inside +//! pass unchanged; shapes crossing the edge are flattened and cut: fills with Sutherland–Hodgman +//! per subpath, outlines with Cyrus–Beck per segment. Bitmaps are cropped (and masked outside a +//! non-rectangular polygon) and text gets the polygon's bounding rectangle. use crate::canvas::Raw; use crate::dib::Rgba; @@ -37,63 +38,158 @@ pub(crate) fn intersect(a: [f64; 4], b: [f64; 4]) -> [f64; 4] { if r[2] < r[0] || r[3] < r[1] { [r[0], r[1], r[0], r[1]] } else { r } } -fn inside(b: [f64; 4], c: [f64; 4]) -> bool { - b[0] >= c[0] && b[1] >= c[1] && b[2] <= c[2] && b[3] <= c[3] +/// A rectangle `[l, t, r, b]` as a polygon. +pub(crate) fn rect_poly([l, t, r, b]: [f64; 4]) -> Vec

{ + vec![(l, t), (r, t), (r, b), (l, b)] +} + +/// The bounding box of a point list. +pub(crate) fn pts_bbox(pts: &[P]) -> Option<[f64; 4]> { + let (&f, rest) = pts.split_first()?; + Some(rest.iter().fold([f.0, f.1, f.0, f.1], |v, p| [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)])) +} + +fn cross(a: P, b: P, p: P) -> f64 { + (b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0) +} + +/// A convex clip polygon with positive orientation, or None when `pts` is not convex (or +/// degenerate); duplicate and collinear points are dropped. +pub(crate) fn convex(pts: &[P]) -> Option> { + let mut v: Vec

= Vec::with_capacity(pts.len()); + for &p in pts { + if !(p.0.is_finite() && p.1.is_finite()) { + return None; + } + if v.last().is_none_or(|q: &P| (q.0 - p.0).abs() > 1e-9 || (q.1 - p.1).abs() > 1e-9) { + v.push(p); + } + } + while v.len() > 1 && v.first() == v.last() { + v.pop(); + } + let area: f64 = (0..v.len()).map(|i| cross((0.0, 0.0), v[i], v[(i + 1) % v.len()])).sum(); + if v.len() < 3 || area.abs() < 1e-12 { + return None; + } + if area < 0.0 { + v.reverse(); + } + let n = v.len(); + let mut out = Vec::with_capacity(n); + for i in 0..n { + let c = cross(v[(i + n - 1) % n], v[i], v[(i + 1) % n]); + if c < -1e-9 * (1.0 + area.abs()) { + return None; + } + if c.abs() > 1e-12 { + out.push(v[i]); + } + } + (out.len() >= 3).then_some(out) +} + +fn inside_poly(clip: &[P], p: P) -> bool { + (0..clip.len()).all(|i| cross(clip[i], clip[(i + 1) % clip.len()], p) >= -1e-9) +} + +/// Whether the clip polygon is an axis-aligned rectangle. +fn is_rect(clip: &[P]) -> bool { + pts_bbox(clip).is_some_and(|b| clip.len() == 4 && clip.iter().all(|p| (p.0 == b[0] || p.0 == b[2]) && (p.1 == b[1] || p.1 == b[3]))) +} + +/// Sutherland–Hodgman: `poly` cut by convex `clip`. +pub(crate) fn clip_polygon(poly: &[P], clip: &[P]) -> Vec

{ + let mut poly = poly.to_vec(); + for i in 0..clip.len() { + let (a, b) = (clip[i], clip[(i + 1) % clip.len()]); + let keep = |p: P| cross(a, b, p) >= 0.0; + let cut = |p: P, q: P| { + let (cp, cq) = (cross(a, b, p), cross(a, b, q)); + let t = cp / (cp - cq); + (p.0 + (q.0 - p.0) * t, p.1 + (q.1 - p.1) * t) + }; + let mut next = Vec::with_capacity(poly.len() + 4); + for j in 0..poly.len() { + let (p, q) = (poly[(j + poly.len() - 1) % poly.len()], poly[j]); + match (keep(p), keep(q)) { + (true, true) => next.push(q), + (true, false) => next.push(cut(p, q)), + (false, true) => { + next.push(cut(p, q)); + next.push(q); + } + (false, false) => {} + } + } + poly = next; + if poly.is_empty() { + break; + } + } + poly } fn disjoint(b: [f64; 4], c: [f64; 4]) -> bool { b[2] < c[0] || b[0] > c[2] || b[3] < c[1] || b[1] > c[3] || c[2] <= c[0] || c[3] <= c[1] } -/// Clips one drawn item; a shape that is both filled and stroked comes back as two items. -pub(crate) fn clip_raw(raw: Raw, c: [f64; 4]) -> Vec { +fn bbox_inside(b: [f64; 4], clip: &[P]) -> bool { + rect_poly(b).into_iter().all(|p| inside_poly(clip, p)) +} + +/// Clips one drawn item to a convex device polygon; a shape that is both filled and stroked comes +/// back as two items. An empty polygon clips everything away. +pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec { + let Some(cb) = pts_bbox(clip).filter(|_| clip.len() >= 3) else { return Vec::new() }; match raw { - Raw::Path { ops, fill, stroke, even_odd, region } => { + Raw::Path { ops, fill, stroke, even_odd } => { let Some(b) = ops_bbox(&ops) else { return Vec::new() }; - if inside(b, c) { - return vec![Raw::Path { ops, fill, stroke, even_odd, region }]; + if bbox_inside(b, clip) { + return vec![Raw::Path { ops, fill, stroke, even_odd }]; } - if disjoint(b, c) { + if disjoint(b, cb) { return Vec::new(); } let subs = flatten(&ops); let mut out = Vec::new(); if fill.is_some() { - let f = clip_fill(&subs, c); + let f = clip_fill(&subs, clip); if !f.is_empty() { - out.push(Raw::Path { ops: f, fill, stroke: None, even_odd, region: region.clone() }); + out.push(Raw::Path { ops: f, fill, stroke: None, even_odd }); } } if stroke.is_some() { - let s = clip_stroke(&subs, c); + let s = clip_stroke(&subs, clip); if !s.is_empty() { - out.push(Raw::Path { ops: s, fill: None, stroke, even_odd, region }); + out.push(Raw::Path { ops: s, fill: None, stroke, even_odd }); } } out } - Raw::Pattern { ops, even_odd, tile, region } => { + Raw::Pattern { ops, even_odd, tile } => { let Some(b) = ops_bbox(&ops) else { return Vec::new() }; - if inside(b, c) { - return vec![Raw::Pattern { ops, even_odd, tile, region }]; + if bbox_inside(b, clip) { + return vec![Raw::Pattern { ops, even_odd, tile }]; } - if disjoint(b, c) { + if disjoint(b, cb) { return Vec::new(); } - let f = clip_fill(&flatten(&ops), c); - if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile, region }] } + let f = clip_fill(&flatten(&ops), clip); + if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile }] } } - Raw::Bitmap { rect, img, region } => crop(rect, img, c).map(|(rect, img)| vec![Raw::Bitmap { rect, img, region }]).unwrap_or_default(), - Raw::Text { text: mut t, region } => { - let cur = t.clip.map_or(c, |[a, b, d, e]| intersect(c, [f64::from(a), f64::from(b), f64::from(d), f64::from(e)])); + Raw::Bitmap { rect, img } => crop(rect, img, clip).map(|(rect, img)| vec![Raw::Bitmap { rect, img }]).unwrap_or_default(), + Raw::Text(mut t) => { + // Text clips to a rectangle: the one around the polygon. + let cur = t.clip.map_or(cb, |[a, b, d, e]| intersect(cb, [f64::from(a), f64::from(b), f64::from(d), f64::from(e)])); t.clip = Some(cur.map(|v| v as f32)); - vec![Raw::Text { text: t, region }] + vec![Raw::Text(t)] } } } /// Subpaths as point lists with their closed flag. -fn flatten(ops: &[Op]) -> Vec<(Vec

, bool)> { +pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec

, bool)> { let mut subs: Vec<(Vec

, bool)> = Vec::new(); for op in ops { match *op { @@ -128,45 +224,10 @@ fn flatten(ops: &[Op]) -> Vec<(Vec

, bool)> { subs } -fn clip_fill(subs: &[(Vec

, bool)], c: [f64; 4]) -> Vec { +fn clip_fill(subs: &[(Vec

, bool)], clip: &[P]) -> Vec { let mut ops = Vec::new(); for (pts, _) in subs { - let mut poly = pts.clone(); - // Left, top, right, bottom edges in turn. - for edge in 0..4 { - let keep = |p: P| match edge { - 0 => p.0 >= c[0], - 1 => p.1 >= c[1], - 2 => p.0 <= c[2], - _ => p.1 <= c[3], - }; - let cut = |a: P, b: P| { - let t = match edge { - 0 => (c[0] - a.0) / (b.0 - a.0), - 1 => (c[1] - a.1) / (b.1 - a.1), - 2 => (c[2] - a.0) / (b.0 - a.0), - _ => (c[3] - a.1) / (b.1 - a.1), - }; - (a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t) - }; - let mut next = Vec::with_capacity(poly.len() + 4); - for i in 0..poly.len() { - let (a, b) = (poly[(i + poly.len() - 1) % poly.len()], poly[i]); - match (keep(a), keep(b)) { - (true, true) => next.push(b), - (true, false) => next.push(cut(a, b)), - (false, true) => { - next.push(cut(a, b)); - next.push(b); - } - (false, false) => {} - } - } - poly = next; - if poly.is_empty() { - break; - } - } + let poly = clip_polygon(pts, clip); if poly.len() >= 3 { ops.push(Op::M(poly[0])); ops.extend(poly[1..].iter().map(|p| Op::L(*p))); @@ -176,7 +237,7 @@ fn clip_fill(subs: &[(Vec

, bool)], c: [f64; 4]) -> Vec { ops } -fn clip_stroke(subs: &[(Vec

, bool)], c: [f64; 4]) -> Vec { +fn clip_stroke(subs: &[(Vec

, bool)], clip: &[P]) -> Vec { let mut ops = Vec::new(); for (pts, closed) in subs { let mut pts = pts.clone(); @@ -185,7 +246,7 @@ fn clip_stroke(subs: &[(Vec

, bool)], c: [f64; 4]) -> Vec { } let mut last: Option

= None; for w in pts.windows(2) { - let Some((a, b)) = segment(w[0], w[1], c) else { + let Some((a, b)) = segment(w[0], w[1], clip) else { last = None; continue; }; @@ -199,34 +260,40 @@ fn clip_stroke(subs: &[(Vec

, bool)], c: [f64; 4]) -> Vec { ops } -/// Liang–Barsky: the part of segment `a`–`b` inside `c`. -fn segment(a: P, b: P, c: [f64; 4]) -> Option<(P, P)> { - let (dx, dy) = (b.0 - a.0, b.1 - a.1); +/// Cyrus–Beck: the part of segment `a`–`b` inside convex `clip`. +fn segment(a: P, b: P, clip: &[P]) -> Option<(P, P)> { + let d = (b.0 - a.0, b.1 - a.1); let (mut t0, mut t1) = (0.0f64, 1.0f64); - for (p, q) in [(-dx, a.0 - c[0]), (dx, c[2] - a.0), (-dy, a.1 - c[1]), (dy, c[3] - a.1)] { - if p == 0.0 { - if q < 0.0 { + for i in 0..clip.len() { + let (e0, e1) = (clip[i], clip[(i + 1) % clip.len()]); + // Inside is cross(e0, e1, p) >= 0, linear in t along the segment. + let f0 = cross(e0, e1, a); + let df = (e1.0 - e0.0) * d.1 - (e1.1 - e0.1) * d.0; + if df == 0.0 { + if f0 < 0.0 { return None; } } else { - let r = q / p; - if p < 0.0 { - t0 = t0.max(r); + let t = -f0 / df; + if df > 0.0 { + t0 = t0.max(t); } else { - t1 = t1.min(r); + t1 = t1.min(t); } } } - (t0 <= t1).then_some(((a.0 + dx * t0, a.1 + dy * t0), (a.0 + dx * t1, a.1 + dy * t1))) + (t0 <= t1).then_some(((a.0 + d.0 * t0, a.1 + d.1 * t0), (a.0 + d.0 * t1, a.1 + d.1 * t1))) } -/// Crops a bitmap drawn into `rect` = `[x, y, w, h]` (device, positive extents) to `c`. -fn crop(rect: [f64; 4], img: Rgba, c: [f64; 4]) -> Option<([f64; 4], Rgba)> { +/// Crops a bitmap drawn into `rect` = `[x, y, w, h]` (device, positive extents) to the clip +/// polygon's box; pixels outside a non-rectangular polygon turn transparent. +fn crop(rect: [f64; 4], img: Rgba, clip: &[P]) -> Option<([f64; 4], Rgba)> { let [x, y, w, h] = rect; let b = [x, y, x + w, y + h]; - if inside(b, c) { + if bbox_inside(b, clip) { return Some((rect, img)); } + let c = pts_bbox(clip)?; if disjoint(b, c) || w <= 0.0 || h <= 0.0 || img.w == 0 || img.h == 0 { return None; } @@ -239,12 +306,23 @@ fn crop(rect: [f64; 4], img: Rgba, c: [f64; 4]) -> Option<([f64; 4], Rgba)> { if x1 <= x0 || y1 <= y0 { return None; } + let mask = !is_rect(clip); let mut px = Vec::with_capacity((x1 - x0) * (y1 - y0) * 4); for row in y0..y1 { // Rows are top-down unless the image says otherwise. let r = if img.bottom_up { img.h - 1 - row } else { row }; let s = (r * img.w + x0) * 4; + let start = px.len(); px.extend_from_slice(img.px.get(s..s + (x1 - x0) * 4)?); + if mask { + let cy = y + (row as f64 + 0.5) / ky; + for (k, p) in px[start..].as_chunks_mut::<4>().0.iter_mut().enumerate() { + let cx = x + ((x0 + k) as f64 + 0.5) / kx; + if !inside_poly(clip, (cx, cy)) { + p[3] = 0; + } + } + } } let out = [x + x0 as f64 / kx, y + y0 as f64 / ky, (x1 - x0) as f64 / kx, (y1 - y0) as f64 / ky]; Some((out, Rgba { w: x1 - x0, h: y1 - y0, bottom_up: false, px })) diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs index f327a7a7..a8c93775 100644 --- a/crates/metafile/src/emfplus.rs +++ b/crates/metafile/src/emfplus.rs @@ -9,19 +9,23 @@ //! - bitmaps (raw pixel formats, and PNG/JPEG/GIF/TIFF/BMP payloads) and strings (DrawString, //! DrawDriverString with character codes); //! - world and page transforms, save/restore and containers; -//! - clipping to rectangles, and to the bounding rectangle of clip paths and regions. +//! - arcs and pies; +//! - clipping to rectangles and convex clip paths and regions (others: their bounding rectangle); +//! - nested metafile images, and glyph-index driver strings in the Windows core fonts. //! -//! Not drawn yet: nested metafile images, glyph-index driver strings, arcs and pies, image -//! attributes, and region combinations other than intersect and union. +//! Not drawn yet: image attributes, glyph indices of other fonts, and region combinations other +//! than intersect and union (XOR, exclude and complement keep the left area). use std::sync::Arc; use crate::bytes::Bytes; +use crate::canvas::Raw; use crate::canvas::{BS_NULL, BS_SOLID, Brush, Canvas, LogFont, PS_NULL, Pen, RawTile, Xform, hatch_tile, mul}; -use crate::cliprect::{intersect, ops_bbox}; +use crate::cliprect::{clip_polygon, convex, flatten, ops_bbox, pts_bbox, rect_poly}; use crate::dib::Rgba; use crate::geom::{Op, P}; use crate::text::{ETO_CLIPPED, Font}; +use crate::{Item, Picture, Seg}; /// "EMF+" as the comment identifier of an EMR_COMMENT holding EMF+ records. pub(crate) const EMF_PLUS: u32 = 0x2B46_4D45; @@ -155,6 +159,23 @@ struct Format { line_align: u32, } +/// A clip area: a convex polygon (empty clips everything), or None for infinite. +type Area = Option>; + +enum ImageData { + Bitmap(Rgba), + /// A nested WMF or EMF (EMF+) picture. + Meta(Picture), +} + +thread_local! { + /// Nesting depth of metafile images being parsed, so a file of nested metafiles cannot run + /// the stack out. + static NESTING: std::cell::Cell = const { std::cell::Cell::new(0) }; +} +/// Metafile images inside metafile images, at most. +const MAX_NESTING: usize = 3; + #[derive(Clone)] enum Obj { Brush(Fill), @@ -163,11 +184,11 @@ enum Obj { width: f64, }, Path(Vec), - Image(Arc), + Image(Arc), Font(FontObj), Format(Format), /// A region's bounding rectangle `[l, t, r, b]` in world units; None is infinite. - Region(Option<[f64; 4]>), + Region(Area), } /// An object arriving in several records: its id, type, total size and the bytes so far. @@ -182,7 +203,7 @@ struct Saved { id: u32, world: Xform, page: f64, - clip: Option<[f64; 4]>, + clip: Area, } pub(crate) struct Plus { @@ -191,8 +212,8 @@ pub(crate) struct Plus { dpi: f64, world: Xform, page: f64, - /// Device clip rectangle `[l, t, r, b]`; None is unclipped. - clip: Option<[f64; 4]>, + /// Convex device clip polygon; None is unclipped. + clip: Area, objects: Vec>, stack: Vec, partial: Option, @@ -227,7 +248,7 @@ impl Plus { } fn painter(&self) -> Painter { - Painter { xf: self.xf(), clip: self.clip } + Painter { xf: self.xf(), clip: self.clip.clone() } } fn record(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> { @@ -351,28 +372,43 @@ impl Plus { } 0x4032 => { let [x, y, w, h] = floats::<4>(r, 0)?; - let dev = self.device_rect(cv, [x, y, x + w, y + h]); - self.combine(flags, Some(dev)) + let area = map_area(Some(rect_poly([x, y, x + w, y + h])), mul(self.xf(), cv.device_affine())); + self.combine(flags, area) } 0x4033 => { - let b = ops_bbox(&self.path(id)?)?; - let dev = self.device_rect(cv, b); - self.combine(flags, Some(dev)) + let area = map_area(Some(area_of_ops(&self.path(id)?)), mul(self.xf(), cv.device_affine())); + self.combine(flags, area) } 0x4034 => { let Some(Some(Obj::Region(reg))) = self.objects.get(id).cloned() else { return None }; - let dev = reg.map(|b| self.device_rect(cv, b)); - self.combine(flags, dev) + let area = map_area(reg, mul(self.xf(), cv.device_affine())); + self.combine(flags, area) } 0x4035 => { let [dx, dy] = floats::<2>(r, 0)?; let a = mul(self.xf(), cv.device_affine()); let (ox, oy) = (dx * a[0] + dy * a[2], dx * a[1] + dy * a[3]); if let Some(c) = self.clip.as_mut() { - *c = [c[0] + ox, c[1] + oy, c[2] + ox, c[3] + oy]; + for p in c.iter_mut() { + *p = (p.0 + ox, p.1 + oy); + } } Some(()) } + 0x4010 => { + let fill = self.brush_of(flags, r.u32(0)?)?; + let [start, sweep] = floats::<2>(r, 4)?; + let rect = *rects(r, 12, 1, compressed)?.first()?; + self.painter().fill(cv, &arc_ops(rect, start, sweep, true), &fill); + Some(()) + } + 0x4011 | 0x4012 => { + let pen = self.pen(id)?; + let [start, sweep] = floats::<2>(r, 0)?; + let rect = *rects(r, 8, 1, compressed)?.first()?; + self.painter().stroke(cv, &arc_ops(rect, start, sweep, typ == 0x4011), pen); + Some(()) + } _ => None, } } @@ -385,48 +421,27 @@ impl Plus { fn push(&mut self, id: u32) { if self.stack.len() < MAX_STACK { - self.stack.push(Saved { id, world: self.world, page: self.page, clip: self.clip }); + self.stack.push(Saved { id, world: self.world, page: self.page, clip: self.clip.clone() }); } } fn pop(&mut self, id: u32) { if let Some(i) = self.stack.iter().rposition(|s| s.id == id) { - let s = &self.stack[i]; + let s = self.stack.swap_remove(i); (self.world, self.page, self.clip) = (s.world, s.page, s.clip); self.stack.truncate(i); } } - /// A world rectangle `[l, t, r, b]` as the device rectangle around it. - fn device_rect(&self, cv: &Canvas, [l, t, r, b]: [f64; 4]) -> [f64; 4] { - let a = mul(self.xf(), cv.device_affine()); - let pts = [(l, t), (r, t), (l, b), (r, b)].map(|p| apply(a, p)); - let xs = pts.map(|p| p.0); - let ys = pts.map(|p| p.1); - let min = |v: [f64; 4]| v.iter().copied().fold(f64::INFINITY, f64::min); - let max = |v: [f64; 4]| v.iter().copied().fold(f64::NEG_INFINITY, f64::max); - [min(xs), min(ys), max(xs), max(ys)] - } - - /// Combines a new clip area (None: infinite) with the current one under the record's combine - /// mode: replace, intersect and union (as the rectangle around both). XOR, exclude and - /// complement leave the clip as it was. - fn combine(&mut self, flags: u16, new: Option<[f64; 4]>) -> Option<()> { - if new.is_some_and(|c| !c.iter().all(|v| v.is_finite())) { - return None; - } + /// Combines a new clip area with the current one under the record's combine mode: replace, + /// intersect and union (as the rectangle around both). XOR, exclude and complement leave the + /// clip as it was. + fn combine(&mut self, flags: u16, new: Area) -> Option<()> { self.clip = match (flags >> 8) & 0xF { 0 => new, - 1 => match (self.clip, new) { - (None, n) => n, - (c, None) => c, - (Some(a), Some(b)) => Some(intersect(a, b)), - }, - 2 => match (self.clip, new) { - (Some(a), Some(b)) => Some([a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]), - _ => None, - }, - _ => self.clip, + 1 => area_intersect(self.clip.take(), new), + 2 => area_union(self.clip.take(), new), + _ => self.clip.take(), }; Some(()) } @@ -489,7 +504,7 @@ impl Plus { } } - fn image(&self, id: usize) -> Option> { + fn image(&self, id: usize) -> Option> { match self.objects.get(id)? { Some(Obj::Image(img)) => Some(img.clone()), _ => None, @@ -509,7 +524,14 @@ impl Plus { /// DrawImage and DrawImagePoints: the source rectangle (pixels) cut from the image, drawn /// into world rectangle `dst` = `[x, y, w, h]`. - fn draw_image(&self, cv: &mut Canvas, img: &Rgba, r: Bytes, dst: [f64; 4]) -> Option<()> { + fn draw_image(&self, cv: &mut Canvas, img: &ImageData, r: Bytes, dst: [f64; 4]) -> Option<()> { + let img = match img { + ImageData::Bitmap(b) => b, + ImageData::Meta(pic) => { + self.painter().picture(cv, dst, pic); + return Some(()); + } + }; let [sx, sy, sw, sh] = floats::<4>(r, 8)?; let src = if r.u32(4)? == 2 { [sx, sy, sw, sh].map(|v| v.round().clamp(-1.0e9, 1.0e9) as i64) } else { [0; 4] }; let (part, dst) = crate::dib::clip_blit(img, src, dst)?; @@ -552,7 +574,14 @@ impl Plus { let units: Vec = (0..len).map(|i| r.u16(28 + i * 2)).collect::>()?; let lf = self.log_font(&font); let em = f64::from(-lf.font.height); - let lines: Vec<&[u16]> = units.split(|u| *u == 0x0A).collect(); + let wrap = w > 0.0 && fmt.flags & 0x1000 == 0; + let lines: Vec> = units + .split(|u| *u == 0x0A) + .flat_map(|l| { + let l: Vec = l.iter().copied().filter(|u| *u != 0x0D).collect(); + if wrap { wrap_line(&l, w, em) } else { vec![l] } + }) + .collect(); let line_h = em * 1.2; let (ax, ta_h) = match fmt.align { 1 => (x + w / 2.0, TA_CENTER), @@ -568,11 +597,10 @@ impl Plus { let clip = (w > 0.0 && h > 0.0 && fmt.flags & 0x4000 == 0).then_some([x, y, x + w, y + h]); let p = self.painter(); for (i, line) in lines.iter().enumerate() { - let line: Vec = line.iter().copied().filter(|u| *u != 0x0D).collect(); if line.is_empty() { continue; } - let (text, dx) = crate::text::decode_utf16(&line, &[]); + let (text, dx) = crate::text::decode_utf16(line, &[]); let at = (ax, top + line_h * i as f64); let (at, ta) = if fmt.line_align == 2 && lines.len() == 1 { ((ax, y + h), ta_h | TA_BOTTOM) } else { (at, ta_h) }; p.text(cv, at, text, dx, TextStyle { color, font: lf.clone(), align: ta, clip }, IDENTITY); @@ -585,12 +613,13 @@ impl Plus { let font = self.font(usize::from(flags & 0xFF))?; let color = self.brush_of(flags, r.u32(0)?)?.color(); let opts = r.u32(4)?; - if opts & 1 == 0 { - // Glyph indices into the font: the metafile crate knows no fonts to map them back. - return None; - } let n = (r.u32(12)? as usize).min(MAX_CHARS); - let units: Vec = (0..n).map(|i| r.u16(16 + i * 2)).collect::>()?; + let mut units: Vec = (0..n).map(|i| r.u16(16 + i * 2)).collect::>()?; + if opts & 1 == 0 { + // Glyph indices into the font. The crate knows no fonts, but the classic Windows core + // fonts keep the standard Macintosh glyph order for ASCII, so those map back. + units = glyphs_to_chars(&font.family, &units)?; + } let pos = points(r, 16 + n * 2, n, false)?; let m = if r.u32(8)? != 0 { matrix(r, 16 + n * 10)? } else { IDENTITY }; let lf = self.log_font(&font); @@ -631,7 +660,7 @@ struct TextStyle { /// canvas's GDI state back. struct Painter { xf: Xform, - clip: Option<[f64; 4]>, + clip: Area, } impl Painter { @@ -639,7 +668,7 @@ impl Painter { let world = cv.world(); cv.save_dc(); cv.modify_world(mul(mul(pre, self.xf), world), 4); - cv.set_clip(self.clip); + cv.set_clip(self.clip.clone()); } fn end(&self, cv: &mut Canvas) { @@ -691,6 +720,62 @@ impl Painter { self.end(cv); } + /// A nested metafile's picture stretched into world rectangle `dst` = `[x, y, w, h]`. + fn picture(&self, cv: &mut Canvas, [x, y, w, h]: [f64; 4], pic: &Picture) { + self.begin(cv, IDENTITY); + let (sx, sy) = (w / f64::from(pic.width), h / f64::from(pic.height)); + let m = mul([sx, 0.0, 0.0, sy, x, y], cv.device_affine()); + let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt(); + let pt = |a: f32, b: f32| apply(m, (f64::from(a), f64::from(b))); + let ops = |segs: &[Seg]| -> Vec { + segs.iter() + .map(|s| match *s { + Seg::Move(a, b) => Op::M(pt(a, b)), + Seg::Line(a, b) => Op::L(pt(a, b)), + Seg::Cubic(a, b, c, d, e, f) => Op::C(pt(a, b), pt(c, d), pt(e, f)), + Seg::Close => Op::Z, + }) + .collect() + }; + for item in &pic.items { + if cv.full() { + break; + } + let raw = match item { + Item::Path { segs, fill, stroke, even_odd } => { + Raw::Path { ops: ops(segs), fill: *fill, stroke: stroke.map(|(c, w)| (c, f64::from(w) * scale)), even_odd: *even_odd } + } + Item::Pattern { segs, even_odd, tile } => Raw::Pattern { + ops: ops(segs), + even_odd: *even_odd, + tile: Arc::new(RawTile { + w: tile.width as usize, + h: tile.height as usize, + px: tile.rgba.clone(), + cell: f64::from(tile.cell[0]) * scale, + }), + }, + Item::Bitmap { rect, width, height, rgba } => { + let (a, b) = (pt(rect[0], rect[1]), pt(rect[0] + rect[2], rect[1] + rect[3])); + let mut img = Rgba { w: *width as usize, h: *height as usize, bottom_up: false, px: rgba.clone() }; + crate::dib::flip(&mut img, a.0 > b.0, a.1 > b.1); + Raw::Bitmap { rect: [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()], img } + } + Item::Text(t) => { + let mut t = t.clone(); + t.transform = mul(t.transform, m); + if let Some([x0, y0, x1, y1]) = t.clip { + let (p, q) = (pt(x0, y0), pt(x1, y1)); + t.clip = Some([p.0.min(q.0) as f32, p.1.min(q.1) as f32, p.0.max(q.0) as f32, p.1.max(q.1) as f32]); + } + Raw::Text(t) + } + }; + cv.push_device(raw); + } + self.end(cv); + } + fn text(&self, cv: &mut Canvas, at: P, text: String, dx: Vec, style: TextStyle, pre: Xform) { self.begin(cv, pre); cv.set_text_style(style.color, style.font, style.align); @@ -756,7 +841,8 @@ fn brush(r: Bytes, off: usize) -> Option { let bflags = d.u32(0)?; let wrap = d.u32(4)?; let (xf, off) = if bflags & 0x2 != 0 { (matrix(d, 8)?, 32) } else { (IDENTITY, 8) }; - Some(Fill::Texture { img: Arc::new(image(d.tail(off)?)?), wrap, xf }) + let ImageData::Bitmap(img) = image(d.tail(off)?)? else { return None }; + Some(Fill::Texture { img: Arc::new(img), wrap, xf }) } 3 => path_gradient(d), 4 => linear_gradient(d), @@ -879,56 +965,133 @@ fn path(r: Bytes) -> Option> { Some(ops) } -/// A region object reduced to its bounding rectangle (None: infinite). +/// A region object as a convex area in world units (non-convex parts become their bounding +/// rectangle). fn region(r: Bytes) -> Option { - region_node(r, 8, 0).map(|(b, _)| Obj::Region(b)) + region_node(r, 8, 0).map(|(a, _)| Obj::Region(a)) } -/// One region node at `off`: its bounds (None: infinite; an empty node is a zero rectangle) and -/// its length in bytes. -fn region_node(r: Bytes, off: usize, depth: usize) -> Option<(Option<[f64; 4]>, usize)> { +/// One region node at `off`: its area and its length in bytes. +fn region_node(r: Bytes, off: usize, depth: usize) -> Option<(Area, usize)> { if depth > MAX_REGION_DEPTH { return None; } match r.u32(off)? { 0x1000_0000 => { let [x, y, w, h] = floats::<4>(r, off + 4)?; - Some((Some([x.min(x + w), y.min(y + h), x.max(x + w), y.max(y + h)]), 20)) + Some((Some(rect_poly([x.min(x + w), y.min(y + h), x.max(x + w), y.max(y + h)])), 20)) } 0x1000_0001 => { let size = r.u32(off + 4)? as usize; - let b = ops_bbox(&path(r.tail(off + 8)?)?).unwrap_or([0.0; 4]); - Some((Some(b), 8usize.checked_add(size)?)) + let area = path(r.tail(off + 8)?).map(|ops| area_of_ops(&ops)).unwrap_or_default(); + Some((Some(area), 8usize.checked_add(size)?)) } - 0x1000_0002 => Some((Some([0.0; 4]), 4)), + 0x1000_0002 => Some((Some(Vec::new()), 4)), 0x1000_0003 => Some((None, 4)), - mode @ 1..=5 => { + mode @ 0..=5 => { let (a, la) = region_node(r, off + 4, depth + 1)?; let (b, lb) = region_node(r, off.checked_add(4 + la)?, depth + 1)?; let out = match mode { - 1 => match (a, b) { - (None, x) | (x, None) => x, - (Some(a), Some(b)) => Some(intersect(a, b)), - }, - 2 => match (a, b) { - (Some(a), Some(b)) => Some([a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]), - _ => None, - }, + 0 => b, + 1 => area_intersect(a, b), + 2 => area_union(a, b), // XOR, exclude, complement: the left area bounds the result closely enough. _ => a, }; Some((out, (4 + la).checked_add(lb)?)) } - // Replace: the right-hand area. - 0 => { - let (_, la) = region_node(r, off + 4, depth + 1)?; - let (b, lb) = region_node(r, off.checked_add(4 + la)?, depth + 1)?; - Some((b, (4 + la).checked_add(lb)?)) + _ => None, + } +} + +/// The area a path encloses: its outline when that is one convex figure, else the rectangle +/// around it. +fn area_of_ops(ops: &[Op]) -> Vec

{ + let subs = flatten(ops); + if let [(pts, _)] = subs.as_slice() + && let Some(c) = convex(pts) + { + return c; + } + ops_bbox(ops).map(rect_poly).unwrap_or_default() +} + +fn area_intersect(a: Area, b: Area) -> Area { + match (a, b) { + (None, x) | (x, None) => x, + (Some(a), Some(b)) if a.len() >= 3 && b.len() >= 3 => Some(convex(&clip_polygon(&a, &b)).unwrap_or_default()), + _ => Some(Vec::new()), + } +} + +fn area_union(a: Area, b: Area) -> Area { + match (a, b) { + (Some(a), Some(b)) => { + if a.len() < 3 { + return Some(b); + } + if b.len() < 3 { + return Some(a); + } + let all: Vec

= a.into_iter().chain(b).collect(); + Some(pts_bbox(&all).map(rect_poly).unwrap_or_default()) } _ => None, } } +/// An area through an affine map (which keeps it convex; the orientation is put right again). +fn map_area(a: Area, m: Xform) -> Area { + a.map(|pts| { + let mapped: Vec

= pts.iter().map(|p| apply(m, *p)).collect(); + if mapped.len() < 3 { Vec::new() } else { convex(&mapped).unwrap_or_default() } + }) +} + +/// An elliptical arc in `rect` from `start` sweeping `sweep` degrees (clockwise on screen), as +/// cubics; a pie adds the lines to and from the centre. +fn arc_ops([l, t, r, b]: [f64; 4], start: f64, sweep: f64, pie: bool) -> Vec { + let (cx, cy, rx, ry) = ((l + r) / 2.0, (t + b) / 2.0, (r - l) / 2.0, (b - t) / 2.0); + let sweep = sweep.clamp(-360.0, 360.0); + // GDI+ angles are along rays from the centre; turn them into the ellipse's parameter. + let param = |deg: f64| { + let a = deg.to_radians(); + (rx * a.sin()).atan2(ry * a.cos()) + }; + let t0 = param(start); + let mut dt = if sweep.abs() >= 360.0 { sweep.signum() * std::f64::consts::TAU } else { param(start + sweep) - t0 }; + if sweep.abs() < 360.0 { + while sweep > 0.0 && dt < 0.0 { + dt += std::f64::consts::TAU; + } + while sweep < 0.0 && dt > 0.0 { + dt -= std::f64::consts::TAU; + } + } + let at = |t: f64| (cx + rx * t.cos(), cy + ry * t.sin()); + let mut ops = Vec::with_capacity(8); + if pie { + ops.push(Op::M((cx, cy))); + ops.push(Op::L(at(t0))); + } else { + ops.push(Op::M(at(t0))); + } + let n = ((dt.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).clamp(1, 4); + let step = dt / n as f64; + let k = 4.0 / 3.0 * (step / 4.0).tan(); + for i in 0..n { + let (a, e) = (t0 + step * i as f64, t0 + step * (i + 1) as f64); + let (p0, p3) = (at(a), at(e)); + let c1 = (p0.0 - k * rx * a.sin(), p0.1 + k * ry * a.cos()); + let c2 = (p3.0 + k * rx * e.sin(), p3.1 - k * ry * e.cos()); + ops.push(Op::C(c1, c2, p3)); + } + if pie { + ops.push(Op::Z); + } + ops +} + /// A font object: em size, unit, style flags, then the family name in UTF-16. fn font(r: Bytes) -> Option { let em = f64::from(r.f32(4)?); @@ -938,12 +1101,31 @@ fn font(r: Bytes) -> Option { em.is_finite().then_some(Obj::Font(FontObj { em, unit: r.u32(8)?, style: r.u32(12)?, family })) } -/// An image object: a bitmap of raw pixels or a compressed (PNG, JPEG, GIF…) file. Metafile -/// images are not drawn yet. -fn image(r: Bytes) -> Option { - if r.u32(4)? != 1 { +/// An image object: a bitmap of raw pixels or a compressed (PNG, JPEG, GIF…) file, or a nested +/// metafile. +fn image(r: Bytes) -> Option { + match r.u32(4)? { + 1 => bitmap(r).map(ImageData::Bitmap), + 2 => metafile(r).map(ImageData::Meta), + _ => None, + } +} + +/// A metafile image: type, size, then a WMF (placeable or not) or EMF file, parsed as a picture. +fn metafile(r: Bytes) -> Option { + let size = r.u32(12)? as usize; + let data = r.slice(16, size).or_else(|| r.tail(16).map(|t| t.0))?; + let depth = NESTING.with(|n| n.get()); + if depth >= MAX_NESTING { return None; } + NESTING.with(|n| n.set(depth + 1)); + let pic = crate::parse(data).ok(); + NESTING.with(|n| n.set(depth)); + pic.filter(|p| p.width > 0.0 && p.height > 0.0) +} + +fn bitmap(r: Bytes) -> Option { let (w, h, stride) = (r.u32(8)? as i32, r.u32(12)? as i32, r.u32(16)? as i32); let format = r.u32(20)?; let data = r.tail(28)?; @@ -1086,3 +1268,68 @@ fn ellipse_ops([l, t, r, b]: [f64; 4]) -> Vec { Op::Z, ] } + +/// Core fonts whose first glyphs follow the standard Macintosh order (glyph 3 is the space, +/// glyphs 3..=97 are U+0020..=U+007E). +const MAC_ORDER_FONTS: &[&str] = + &["arial", "arial black", "times new roman", "courier new", "verdana", "georgia", "tahoma", "trebuchet ms", "comic sans ms", "impact"]; + +/// Glyph indices of a known font back to ASCII; None when the font is unknown or a glyph is +/// outside the ASCII range. +fn glyphs_to_chars(family: &str, glyphs: &[u16]) -> Option> { + let family = family.trim().to_ascii_lowercase(); + if !MAC_ORDER_FONTS.contains(&family.as_str()) { + return None; + } + glyphs.iter().map(|g| (3..=97).contains(g).then_some(g + 29)).collect() +} + +/// A rough advance for a character in ems: the metafile crate has no fonts, so line breaks for +/// wrapped strings use typical proportional widths. +fn char_em(u: u16) -> f64 { + match char::from_u32(u32::from(u)).unwrap_or('x') { + ' ' | 'i' | 'l' | 'j' | '.' | ',' | ':' | ';' | '\'' | '!' | '|' | 'I' | 'f' | 't' | 'r' => 0.3, + 'm' | 'w' | 'M' | 'W' => 0.85, + c if c.is_ascii_uppercase() || c.is_ascii_digit() => 0.62, + c if (c as u32) >= 0x2E80 => 1.0, + _ => 0.52, + } +} + +/// Greedy word wrap of one line to `width` world units at em size `em`; a word longer than the +/// line breaks between characters. +fn wrap_line(line: &[u16], width: f64, em: f64) -> Vec> { + if em <= 0.0 || !width.is_finite() { + return vec![line.to_vec()]; + } + let fits = |l: &[u16]| l.iter().map(|u| char_em(*u) * em).sum::() <= width; + let mut out: Vec> = Vec::new(); + let mut cur: Vec = Vec::new(); + for word in line.split_inclusive(|u| *u == 0x20) { + let mut cand = cur.clone(); + cand.extend_from_slice(word); + let trimmed = |v: &[u16]| v.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1); + if fits(&cand[..trimmed(&cand)]) || cur.is_empty() && fits(&word[..trimmed(word)]) { + cur = cand; + continue; + } + if !cur.is_empty() { + let n = trimmed(&cur); + cur.truncate(n); + out.push(std::mem::take(&mut cur)); + } + // A word wider than the line on its own: break it between characters. + for &u in word { + cur.push(u); + if !fits(&cur[..trimmed(&cur)]) && cur.len() > 1 { + cur.pop(); + out.push(std::mem::take(&mut cur)); + cur.push(u); + } + } + } + let n = cur.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1); + cur.truncate(n); + out.push(cur); + out +} diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 81993a9e..ac8c2abc 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -1799,10 +1799,16 @@ fn emfplus_driver_string_uses_character_codes_and_positions() { assert_eq!(t.dx, vec![6.0, 6.0]); assert_eq!(t.v_align, VAlign::Baseline); assert!(close(t.transform[4] as f32, 5.0) && close(t.transform[5] as f32, 30.0)); - // Glyph indices (no CmapLookup) are skipped. - let gi = prec(0x4036, 0x8000, &cat(vec![h32(0xFF00_0000), h32(0), h32(0), h32(1), h16(36), f32s(&[0.0, 0.0])])); - let font = prec(0x4008, 0x0600, &font_obj("Arial", 10.0, 0, 0)); - assert!(plus_pic(vec![font, gi]).items.is_empty()); +} + +#[test] +fn emfplus_glyph_index_driver_strings_map_back_in_core_fonts_only() { + // Glyph indices (no CmapLookup): 36, 69 are "Ab" in Arial's standard glyph order. + let gi = || prec(0x4036, 0x8000, &cat(vec![h32(0xFF00_0000), h32(0), h32(0), h32(2), h16(36), h16(69), f32s(&[0.0, 0.0, 6.0, 0.0])])); + let p = plus_pic(vec![prec(0x4008, 0x0600, &font_obj("Arial", 10.0, 0, 0)), gi()]); + assert!(matches!(&p.items[0], Item::Text(t) if t.text == "Ab"), "{:?}", p.items); + // An unknown font's glyph indices cannot be mapped: nothing is drawn. + assert!(plus_pic(vec![prec(0x4008, 0x0600, &font_obj("Calibri", 10.0, 0, 0)), gi()]).items.is_empty()); } #[test] @@ -1868,3 +1874,62 @@ fn emfplus_region_clip_intersects_rectangles() { let p = plus_pic(vec![prec(0x4008, 0x0400 | 6, ®ion), prec(0x4034, 6, &[]), fill]); assert_eq!((p.width, p.height), (20.0, 20.0)); } + +#[test] +fn emfplus_pie_and_arc() { + // A quarter pie from 0 to 90 degrees in a 0..100 circle: centre, then (100, 50) round to (50, 100). + let pie = prec(0x4010, 0x8000, &cat(vec![h32(0xFF00_0000), f32s(&[0.0, 90.0]), f32s(&[0.0, 0.0, 100.0, 100.0])])); + let p = plus_pic(vec![pie]); + let (segs, fill, ..) = path_segs(&p.items[0]); + assert_eq!(fill, Some([0, 0, 0, 255])); + assert_eq!(segs[0], Seg::Move(0.0, 0.0)); + assert_eq!(segs[1], Seg::Line(50.0, 0.0)); + assert!(matches!(segs[2], Seg::Cubic(.., x, y) if close(x, 0.0) && close(y, 50.0)), "{segs:?}"); + assert_eq!(segs[3], Seg::Close); + // A half-circle arc stroke is two cubics. + let pen = prec(0x4008, 0x0200, &solid_pen(0xFF00_0000, 1.0)); + let arc = prec(0x4012, 0, &cat(vec![f32s(&[180.0, -180.0]), f32s(&[0.0, 0.0, 100.0, 100.0])])); + let p = plus_pic(vec![pen, arc]); + let (segs, ..) = path_segs(&p.items[0]); + assert_eq!(segs.len(), 3, "{segs:?}"); +} + +#[test] +fn emfplus_convex_clip_path_cuts_to_the_shape_not_its_box() { + // A diamond clip path (0,50) (50,0) (100,50) (50,100): the corner of a full square fill is cut away. + let diamond = cat(vec![h32(0xDBC0_1002), h32(4), h32(0), f32s(&[0.0, 50.0, 50.0, 0.0, 100.0, 50.0, 50.0, 100.0]), vec![0, 1, 1, 0x81]]); + let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[0.0, 0.0, 100.0, 100.0])])); + let p = plus_pic(vec![prec(0x4008, 0x0300 | 9, &diamond), prec(0x4033, 9, &[]), fill]); + let (segs, ..) = path_segs(&p.items[0]); + let pts: Vec<(f32, f32)> = segs.iter().filter_map(|s| if let Seg::Move(x, y) | Seg::Line(x, y) = s { Some((*x, *y)) } else { None }).collect(); + assert!(!pts.iter().any(|(x, y)| close(*x, 0.0) && close(*y, 0.0)), "corner kept: {pts:?}"); + assert!(pts.iter().all(|(x, y)| (x - 50.0).abs() + (y - 50.0).abs() <= 50.0 + 1e-3), "{pts:?}"); +} + +#[test] +fn emfplus_draw_string_wraps_to_the_layout_width() { + let font = prec(0x4008, 0x0600, &font_obj("Arial", 10.0, 0, 0)); + let text = "alpha beta gamma delta"; + let s = prec( + 0x401C, + 0x8000, + &cat(vec![h32(0xFF00_0000), h32(9), h32(text.encode_utf16().count() as u32), f32s(&[0.0, 0.0, 60.0, 100.0]), u16s(text)]), + ); + let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_FFFF), h32(1), f32s(&[0.0, 0.0, 60.0, 100.0])])); + let p = plus_pic(vec![fill, font, s]); + let lines: Vec<&str> = p.items.iter().filter_map(|i| if let Item::Text(t) = i { Some(t.text.as_str()) } else { None }).collect(); + assert_eq!(lines, vec!["alpha beta", "gamma delta"]); +} + +#[test] +fn emfplus_nested_metafile_image_draws_its_items_into_the_destination() { + // The nested picture: a plain EMF with a 10 x 10 rectangle (white brush, black pen). + let inner = emf(&[erec(43, &cat(vec![e32(0), e32(0), e32(10), e32(10)]))], [0; 4], [0; 4]); + let obj = cat(vec![h32(0xDBC0_1002), h32(2), h32(3), h32(inner.len() as u32), inner]); + let p = plus_pic(vec![prec(0x4008, 0x0500 | 3, &obj), draw_image(3, [0.0, 0.0, 40.0, 20.0])]); + assert_eq!(p.items.len(), 1); + assert_eq!((p.width, p.height), (40.0, 20.0)); + let (segs, fill, ..) = path_segs(&p.items[0]); + assert_eq!(fill, Some([255, 255, 255, 255])); + assert!(segs.contains(&Seg::Line(40.0, 20.0)), "{segs:?}"); +}