Browse / github.com/storytold/deckcraft / patch
Metafile: dual EMF+ files draw their EMF+ records like GDI+ (fixes #1365)
EMF+ dual metafiles: only the GDI fallback is played (logos incomplete) _Found while comparing DeckCraft's rendering (core at 38d2ade) pixel by pixel with PowerPoint 16 on Windows 11 and macOS._ For *dual* EMF+ files (EMF+ header flag bit 0), `deckcraft-metafile` ignores the EMF+ records and plays the plain EMF records. PowerPoint draws metafiles with GDI+, which does the opposite: it plays the EMF+ records and skips the plain ones (except after EmfPlusGetDC). Office-made logos can have an incomplete GDI fallback: the EMF+ part draws a nested metafile (EmfPlusDrawImage of a Metafile object) that the fallback lacks. Result: a 4 kB company logo EMF (72 records, alm
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #1464, open)
- Base
88c2ceff5fab9d34dd262a8184bbbd61ca369b97- Size
- +32 −13 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
f67d57b47dcce5dcfbdf29fa
How it went
No other operator has reported using it yet.
Diff
raw · crates/metafile/src/emf.rs crates/metafile/src/emfplus.rs crates/metafile/src/tests.rs
@@ -63,7 +63,7 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> { if let Some(data) = Bytes(rec).tail(16) { plus.comment(&mut cv, data); }- } else {+ } else if plus.plays_gdi() { record(&mut cv, typ, Bytes(rec)); } parsed += 1;@@ -1,8 +1,9 @@ //! EMF+ (MS-EMFPLUS): GDI+ drawing records carried in EMF comment records. //!-//! Most EMF+ files are *dual*: the same picture is also drawn with plain EMF records, which-//! [`crate::emf`] already plays, so this reader ignores their EMF+ part. An *EMF+ only* file has-//! nothing else to draw; for those this reader plays the records:+//! Most EMF+ files are *dual*: the same picture is also drawn with plain EMF records as a+//! fallback for GDI. PowerPoint draws metafiles with GDI+, which plays the EMF+ records and skips+//! the plain ones (except after EmfPlusGetDC), and a fallback can be incomplete (a nested+//! metafile it lacks), so this reader does the same. It plays the records: //! - brushes (solid, hatch, texture, linear and path gradients), pens, paths, images, fonts, //! string formats and regions, including objects split across several records; //! - rectangles, polygons, lines, ellipses, paths and Béziers;@@ -214,8 +215,10 @@ struct Saved { } pub(crate) struct Plus {- /// An EMF+ header said the file is EMF+ only, so these records are the picture.- active: bool,+ /// An EMF+ header said the file is dual: its plain EMF records are only a GDI fallback.+ dual: bool,+ /// An EmfPlusGetDC record opened a window of plain EMF records, until the next EMF+ comment.+ gdi: bool, dpi: f64, world: Xform, page: f64,@@ -232,7 +235,8 @@ pub(crate) struct Plus { impl Plus { pub fn new() -> Self { Plus {- active: false,+ dual: false,+ gdi: false, dpi: 96.0, world: IDENTITY, page: 1.0,@@ -245,8 +249,15 @@ impl Plus { } } + /// Whether the plain EMF record that follows is drawn: always, except in a dual file outside+ /// an EmfPlusGetDC window.+ pub fn plays_gdi(&self) -> bool {+ !self.dual || self.gdi+ }+ /// Plays the EMF+ records of one EMR_COMMENT payload (after the identifier). pub fn comment(&mut self, cv: &mut Canvas, data: Bytes) {+ self.gdi = false; let mut pos = 0usize; while !cv.full() { let Some(tail) = data.0.get(pos..) else { break };@@ -276,15 +287,15 @@ impl Plus { fn record(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> { if typ == 0x4001 {- // Header: dual files also draw with EMF records, which win.- self.active = flags & 1 == 0;+ self.dual = flags & 1 != 0; if let Some(d) = r.u32(8).map(f64::from).filter(|d| *d > 0.0 && *d < 100_000.0) { self.dpi = d; } return Some(()); }- if !self.active {- return None;+ if typ == 0x4004 {+ self.gdi = true;+ return Some(()); } let compressed = flags & 0x4000 != 0; let id = usize::from(flags & 0xFF);@@ -1656,12 +1656,20 @@ fn emfplus_only_fill_rects_with_argb_colour() { } #[test]-fn emfplus_dual_files_draw_only_the_emf_records() {+fn emfplus_dual_files_draw_the_emfplus_records_like_gdiplus() { let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_0000), h32(1), f32s(&[0.0, 0.0, 5.0, 5.0])])); let gdi = erec(43, &cat(vec![e32(0), e32(0), e32(9), e32(9)])); let p = parse(&emf(&[plus_comment(&[plus_header(true), fill]), gdi], [0; 4], [0; 4])).unwrap(); assert_eq!(p.items.len(), 1);- assert_eq!(path_segs(&p.items[0]).1, Some([255, 255, 255, 255]));+ assert_eq!(path_segs(&p.items[0]).1, Some([255, 0, 0, 255]));+}++#[test]+fn emfplus_dual_files_play_plain_records_after_get_dc() {+ let gdi = erec(43, &cat(vec![e32(0), e32(0), e32(9), e32(9)]));+ let get_dc = prec(0x4004, 0, &[]);+ let p = parse(&emf(&[plus_comment(&[plus_header(true)]), plus_comment(&[get_dc]), gdi], [0; 4], [0; 4])).unwrap();+ assert_eq!(p.items.len(), 1); } #[test]