mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #635 from NikitkaRa: Metafile: EMF+ Clear, and the EMF+ record coverage written down
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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)?)?;
|
||||
|
||||
@@ -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, ®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<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);
|
||||
}
|
||||
|
||||
@@ -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 | |
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user