Merge PR #635 from NikitkaRa: Metafile: EMF+ Clear, and the EMF+ record coverage written down

This commit is contained in:
Brandon Thomas
2026-10-14 13:57:48 -04:00
5 changed files with 74 additions and 9 deletions
+1
View File
@@ -46,6 +46,7 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), 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() {
+36 -4
View File
@@ -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<Option<Obj>>,
stack: Vec<Saved>,
partial: Option<Partial>,
/// 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)?)?;
+34 -5
View File
@@ -1947,7 +1947,9 @@ fn curves_file() -> Vec<u8> {
// 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, &region), 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, &region), 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<Vec<u8>>| {
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<f32> = 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);
}
+1
View File
@@ -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 | |
+2
View File
@@ -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).