diff --git a/crates/metafile/src/emf.rs b/crates/metafile/src/emf.rs
index bf2daa3b..94c6d605 100644
--- a/crates/metafile/src/emf.rs
+++ b/crates/metafile/src/emf.rs
@@ -46,6 +46,7 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option ), Error> {
cv.set_device_mm(px_mm);
}
let mut plus = crate::emfplus::Plus::new();
+ plus.frame = frame.as_ref().map(|f| f.rect);
let mut parsed = 0usize;
let mut pos = hsize.max(88);
while !cv.full() {
diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs
index a0cbb7ad..550c2d18 100644
--- a/crates/metafile/src/emfplus.rs
+++ b/crates/metafile/src/emfplus.rs
@@ -11,10 +11,17 @@
//! - world and page transforms, save/restore and containers;
//! - 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.
+//! - nested metafile images, and glyph-index driver strings in the Windows core fonts;
+//! - cardinal splines (DrawCurve, DrawClosedCurve, FillClosedCurve), FillRegion, and Clear
+//! (over the picture frame).
//!
-//! 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).
+//! Every drawing record of MS-EMFPLUS §2.3.4 is played. Not drawn: image attributes, glyph
+//! indices of other fonts, and region combinations other than intersect and union (XOR, exclude
+//! and complement keep the left area). Deliberately ignored: rendering hints (antialias, text
+//! hint, interpolation, pixel offset, compositing; the renderer has its own), SetRenderingOrigin
+//! (hatch phase), SerializableObject image effects, the terminal-server records SetTSGraphics and
+//! SetTSClip, and StrokeFillPath (0x4037), which the specification lists but gives no layout for
+//! and GDI+ doesn't write.
use std::sync::Arc;
@@ -217,11 +224,23 @@ pub(crate) struct Plus {
objects: Vec>,
stack: Vec,
partial: Option,
+ /// The picture frame in device units (`[x, y, w, h]`), what Clear paints.
+ pub frame: Option<[f64; 4]>,
}
impl Plus {
pub fn new() -> Self {
- Plus { active: false, dpi: 96.0, world: IDENTITY, page: 1.0, clip: None, objects: vec![None; MAX_OBJECTS], stack: Vec::new(), partial: None }
+ Plus {
+ active: false,
+ dpi: 96.0,
+ world: IDENTITY,
+ page: 1.0,
+ clip: None,
+ objects: vec![None; MAX_OBJECTS],
+ stack: Vec::new(),
+ partial: None,
+ frame: None,
+ }
}
/// Plays the EMF+ records of one EMR_COMMENT payload (after the identifier).
@@ -351,6 +370,19 @@ impl Plus {
self.painter().stroke(cv, &curve_ops(&pts, tension, false, offset, segments), pen);
Some(())
}
+ // Clear: the whole picture in one colour, whatever the clip (a transparent clear
+ // leaves nothing to draw).
+ 0x4009 => {
+ let c = argb(r.u32(0)?);
+ let [x, y, w, h] = self.frame?;
+ if c[3] == 0 || !(w > 0.0 && h > 0.0) {
+ return Some(());
+ }
+ let to_device = invert(mul(cv.world(), cv.device_affine()))?;
+ let p = Painter { xf: to_device, clip: None };
+ p.fill(cv, &rect_ops([x, y, x + w, y + h]), &Fill::Solid(c));
+ Some(())
+ }
// FillRegion: the region's area (as the clip reads it).
0x4013 => {
let fill = self.brush_of(flags, r.u32(0)?)?;
diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs
index 25aa7ceb..fb7dfe36 100644
--- a/crates/metafile/src/tests.rs
+++ b/crates/metafile/src/tests.rs
@@ -1947,7 +1947,9 @@ fn curves_file() -> Vec {
// A green rectangle region, filled.
let region = cat(vec![h32(0xDBC0_1002), h32(0), h32(0x1000_0000), f32s(&[220.0, 160.0, 180.0, 30.0])]);
let fill_region = prec(0x4013, 0x8000 | 2, &h32(0xFF30_A040));
- let mut all = vec![plus_header(false), pen, fill, outline, line, prec(0x4008, 0x0400 | 2, ®ion), fill_region];
+ // A cream background first.
+ let clear = prec(0x4009, 0, &h32(0xFFF4_EED8));
+ let mut all = vec![plus_header(false), clear, pen, fill, outline, line, prec(0x4008, 0x0400 | 2, ®ion), fill_region];
all.push(prec(0x4002, 0, &[]));
emf(&[plus_comment(&all)], [0, 0, 420, 200], [0; 4])
}
@@ -1959,20 +1961,21 @@ fn emfplus_cardinal_curves_and_region_fill() {
let _ = std::fs::write(path, &bytes);
}
let p = parse(&bytes).unwrap();
- assert_eq!(p.items.len(), 4, "{:?}", p.items.len());
+ assert_eq!(p.items.len(), 5, "{:?}", p.items.len());
+ assert_eq!(path_segs(&p.items[0]).1, Some([0xF4, 0xEE, 0xD8, 0xFF]), "the clear comes first");
// The closed curve: four cubics through the diamond's corners, closed.
- let (segs, fill, ..) = path_segs(&p.items[0]);
+ let (segs, fill, ..) = path_segs(&p.items[1]);
assert_eq!(fill, Some([0xE0, 0x80, 0x20, 0xFF]));
let cubics: Vec<(f32, f32)> = segs.iter().filter_map(|s| if let Seg::Cubic(.., x, y) = s { Some((*x, *y)) } else { None }).collect();
assert_eq!(cubics.len(), 4, "{segs:?}");
assert_eq!(segs.last(), Some(&Seg::Close));
// The open curve: four cubics and no close; its controls bulge past the straight chords.
- let (segs, fill, stroke, _) = path_segs(&p.items[2]);
+ let (segs, fill, stroke, _) = path_segs(&p.items[3]);
assert_eq!((fill, stroke.map(|s| s.0)), (None, Some([0x20, 0x60, 0xC0, 0xFF])));
assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Cubic(..))).count(), 4);
assert!(!segs.contains(&Seg::Close));
// The region fill is its rectangle.
- let (segs, fill, ..) = path_segs(&p.items[3]);
+ let (segs, fill, ..) = path_segs(&p.items[4]);
assert_eq!(fill, Some([0x30, 0xA0, 0x40, 0xFF]));
assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Line(..))).count(), 3, "{segs:?}");
}
@@ -1993,3 +1996,29 @@ fn emfplus_curve_offsets_and_degenerate_input_are_safe() {
let p = plus_pic(vec![pen, one, past, nan, no_region]);
assert!(p.items.iter().all(|i| path_segs(i).0.iter().filter(|s| matches!(s, Seg::Cubic(..))).count() == 0));
}
+
+#[test]
+fn emfplus_clear_paints_the_whole_frame_and_transparent_clears_nothing() {
+ let dot = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_0000), h32(1), f32s(&[40.0, 40.0, 10.0, 10.0])]));
+ let clear = |argb: u32| prec(0x4009, 0, &h32(argb));
+ // 100 × 50 device units (0..2500 × 0..1250 hundredths of a millimetre at 0.04 units each).
+ let file = |recs: Vec>| {
+ let mut all = vec![plus_header(false)];
+ all.extend(recs);
+ parse(&emf(&[plus_comment(&all)], [0, 0, 99, 49], [0, 0, 2500, 1250])).unwrap()
+ };
+ // A translated, clipped world doesn't move or cut the clear.
+ let shift = prec(0x402D, 0, &f32s(&[30.0, 30.0]));
+ let clip = prec(0x4032, 0, &f32s(&[0.0, 0.0, 5.0, 5.0]));
+ let p = file(vec![shift, clip, clear(0xFFFF_FFFF), prec(0x4031, 0, &[]), dot.clone()]);
+ assert_eq!(p.items.len(), 2);
+ let (segs, fill, ..) = path_segs(&p.items[0]);
+ assert_eq!(fill, Some([255, 255, 255, 255]));
+ let xs: Vec = segs.iter().filter_map(|s| if let Seg::Move(x, _) | Seg::Line(x, _) = s { Some(*x) } else { None }).collect();
+ let span = xs.iter().cloned().fold(f32::MIN, f32::max) - xs.iter().cloned().fold(f32::MAX, f32::min);
+ assert!((span - p.width).abs() < 0.5, "clear spans {span} of {}", p.width);
+ // Transparent: only the dot.
+ assert_eq!(file(vec![clear(0x00FF_FFFF), dot.clone()]).items.len(), 1);
+ // No frame: nothing to clear.
+ assert_eq!(plus_pic(vec![clear(0xFFFF_FFFF), dot]).items.len(), 1);
+}
diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md
index c23fa5c4..6a3cde7a 100644
--- a/docs/file-format-parity.md
+++ b/docs/file-format-parity.md
@@ -51,6 +51,7 @@ PowerPoint opens and saves; Viewer = opens only).
| PNG, JPEG, GIF (animated), BMP, TIFF, WebP | yes | yes | `image` 0.25; animated GIFs play since #39 |
| SVG | yes (with PNG fallback) | **no** | The PNG fallback shows; SVG files can't be inserted |
| EMF / WMF | yes | partial | Drawn as vectors (paths, pens, pattern and hatch brushes, bitmaps, text in the document's fonts with the file's character advances). Rectangle clip regions (`INTERSECTCLIPRECT`, `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit later drawing; EMF+ records are not drawn. Legacy OLE objects (VML preview) and ActiveX controls show their pictures |
+| EMF / WMF | yes | partial | Drawn as vectors (paths, pens, pattern and hatch brushes, bitmaps, text in the document's fonts with the file's character advances); clip regions; EMF+ only pictures play every EMF+ drawing record (brushes incl. gradients and textures, pens, paths, curves, images, strings, transforms, clipping; see `crates/metafile/src/emfplus.rs` for the few exceptions). Legacy OLE objects (VML preview) and ActiveX controls show their pictures |
| HEIC | yes (macOS) | no | |
| Audio: MP3, AAC/M4A, ALAC, WAV, AIFF, CAF, FLAC, Ogg Vorbis, Opus | yes (Ogg/Opus partly) | yes | Pure-Rust decoders in `crates/media`, `crates/opus` |
| WMA | yes (Windows) | embedded, not playable | |
diff --git a/docs/gaps.md b/docs/gaps.md
index bdf2466f..ebba81af 100644
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -140,6 +140,8 @@ Detail in [file-format-parity.md](file-format-parity.md).
TIFF, BMP), and EMF/WMF through `deckcraft-metafile` (ported from WordCraft: paths, fills, pens,
pattern and hatch brushes, bitmaps and text; rectangle clip regions are applied, EMF+ records are
not drawn).
+ pattern and hatch brushes, bitmaps and text; EMF+ only pictures play their EMF+ records;
+ clip regions are not drawn).
Nothing reads SVG; PowerPoint's Insert Icons stores SVG with a PNG fallback.
- **Impact:** SVG icons show only their raster fallback, and SVG files can't be inserted.
- **Estimate:** 4–8 h (an SVG renderer through `resvg`-style crates).