diff --git a/crates/metafile/README.md b/crates/metafile/README.md index edd9750bf..ba255c36d 100644 --- a/crates/metafile/README.md +++ b/crates/metafile/README.md @@ -79,7 +79,10 @@ decoder is where the size, pixel and work limits have to be enforced. ### EMF+ (MS-EMFPLUS) -An EMF+ only file is played from its EMF+ records. A dual file is played from its EMF records. +A file with EMF+ records is played from them, as GDI+ plays it. In a dual file (one that also +draws the picture with plain EMF records, as a fallback for GDI) the plain records are skipped, +except those between an `EmfPlusGetDC` record and the next EMF+ record. What the EMF+ reader does +not read (the table's last row) is therefore lost in a dual file even where its EMF records carry it. | Feature | Read | |---|---| diff --git a/crates/metafile/src/emf.rs b/crates/metafile/src/emf.rs index cf3bc7a90..81a8dd7fe 100644 --- a/crates/metafile/src/emf.rs +++ b/crates/metafile/src/emf.rs @@ -63,7 +63,7 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option), 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; diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs index 1e145ffd1..5c45721a7 100644 --- a/crates/metafile/src/emfplus.rs +++ b/crates/metafile/src/emfplus.rs @@ -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. Office 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; @@ -216,8 +217,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+ record. + gdi: bool, dpi: f64, world: Xform, page: f64, @@ -234,7 +237,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, @@ -247,8 +251,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 }; @@ -261,6 +272,8 @@ impl Plus { break; } let Some(rec) = tail.slice(12, dsize) else { break }; + // Any EMF+ record ends a GetDC window; EmfPlusGetDC opens it again for its own. + self.gdi = false; let _ = self.record(cv, typ, flags, Bytes(rec)); let Some(next) = pos.checked_add(size) else { break }; pos = next; @@ -278,15 +291,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 done = match typ { 0x4008 => self.object(flags, r, cv.used().saturating_add(self.clip_bytes())), @@ -1629,7 +1642,6 @@ mod cost_tests { #[test] fn pieces_of_a_continued_object_count_against_the_shared_budget() { let (mut plus, mut cv) = (Plus::new(), Canvas::new()); - plus.active = true; // Image object (kind 5), id 0, continued: 1000 bytes announced, 100 delivered. let mut rec = 1000u32.to_le_bytes().to_vec(); rec.extend([0u8; 100]); @@ -1640,7 +1652,6 @@ mod cost_tests { #[test] fn current_and_saved_clips_count_against_the_shared_budget() { let (mut plus, mut cv) = (Plus::new(), Canvas::new()); - plus.active = true; let pts = vec![(0.0, 0.0); 1000]; plus.clip = Some(pts.clone()); plus.stack.push(Saved { id: 1, world: IDENTITY, page: 1.0, clip: Some(pts) }); diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index 67634f1fd..806c95662 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -2041,12 +2041,37 @@ 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] +fn emfplus_record_after_get_dc_in_one_comment_closes_the_plain_window() { + let gdi = erec(43, &cat(vec![e32(0), e32(0), e32(9), e32(9)])); + let get_dc = prec(0x4004, 0, &[]); + let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_0000), h32(1), f32s(&[0.0, 0.0, 5.0, 5.0])])); + let p = parse(&emf(&[plus_comment(&[plus_header(true), get_dc, fill]), gdi], [0; 4], [0; 4])).unwrap(); + assert_eq!(p.items.len(), 1); + assert_eq!(path_segs(&p.items[0]).1, Some([255, 0, 0, 255])); +} + +#[test] +fn an_emf_without_an_emfplus_header_plays_its_plain_records() { + let gdi = erec(43, &cat(vec![e32(0), e32(0), e32(9), e32(9)])); + let p = parse(&emf(&[gdi], [0; 4], [0; 4])).unwrap(); + assert_eq!(p.items.len(), 1); } #[test]