Merge PR #588 from NikitkaRa: Metafile: EMF+ arcs, convex clip paths, wrapped strings, nested metafiles

# Conflicts:
#	crates/metafile/src/canvas.rs
#	crates/metafile/src/clip.rs
#	crates/metafile/src/emfplus.rs
This commit is contained in:
Brandon Thomas
2026-10-14 13:56:02 -04:00
4 changed files with 574 additions and 178 deletions
+12 -6
View File
@@ -332,8 +332,8 @@ pub(crate) struct Canvas {
clip: Option<Vec<[f64; 4]>>,
/// 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<Vec<P>>,
}
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<Vec<P>>) {
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<RawTile>) {
let dev: Vec<Op> = ops.iter().map(|o| self.map_op(*o)).collect();
+158 -80
View File
@@ -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<P> {
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<Vec<P>> {
let mut v: Vec<P> = 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<P> {
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<Raw> {
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<Raw> {
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<P>, bool)> {
pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec<P>, bool)> {
let mut subs: Vec<(Vec<P>, bool)> = Vec::new();
for op in ops {
match *op {
@@ -128,45 +224,10 @@ fn flatten(ops: &[Op]) -> Vec<(Vec<P>, bool)> {
subs
}
fn clip_fill(subs: &[(Vec<P>, bool)], c: [f64; 4]) -> Vec<Op> {
fn clip_fill(subs: &[(Vec<P>, bool)], clip: &[P]) -> Vec<Op> {
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<P>, bool)], c: [f64; 4]) -> Vec<Op> {
ops
}
fn clip_stroke(subs: &[(Vec<P>, bool)], c: [f64; 4]) -> Vec<Op> {
fn clip_stroke(subs: &[(Vec<P>, bool)], clip: &[P]) -> Vec<Op> {
let mut ops = Vec::new();
for (pts, closed) in subs {
let mut pts = pts.clone();
@@ -185,7 +246,7 @@ fn clip_stroke(subs: &[(Vec<P>, bool)], c: [f64; 4]) -> Vec<Op> {
}
let mut last: Option<P> = 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<P>, bool)], c: [f64; 4]) -> Vec<Op> {
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 }))
+335 -88
View File
@@ -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<Vec<P>>;
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<usize> = 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<Op>),
Image(Arc<Rgba>),
Image(Arc<ImageData>),
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<Option<Obj>>,
stack: Vec<Saved>,
partial: Option<Partial>,
@@ -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<Arc<Rgba>> {
fn image(&self, id: usize) -> Option<Arc<ImageData>> {
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<u16> = (0..len).map(|i| r.u16(28 + i * 2)).collect::<Option<_>>()?;
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<Vec<u16>> = units
.split(|u| *u == 0x0A)
.flat_map(|l| {
let l: Vec<u16> = 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<u16> = 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<u16> = (0..n).map(|i| r.u16(16 + i * 2)).collect::<Option<_>>()?;
let mut units: Vec<u16> = (0..n).map(|i| r.u16(16 + i * 2)).collect::<Option<_>>()?;
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<Op> {
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<f32>, 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<Fill> {
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<Vec<Op>> {
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<Obj> {
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<P> {
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<P> = 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<P> = 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<Op> {
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<Obj> {
let em = f64::from(r.f32(4)?);
@@ -938,12 +1101,31 @@ fn font(r: Bytes) -> Option<Obj> {
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<Rgba> {
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<ImageData> {
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<Picture> {
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<Rgba> {
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> {
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<Vec<u16>> {
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<Vec<u16>> {
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::<f64>() <= width;
let mut out: Vec<Vec<u16>> = Vec::new();
let mut cur: Vec<u16> = 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
}
+69 -4
View File
@@ -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, &region), 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:?}");
}