Files
vectorcraft/crates/metafile/src/emf.rs
bflatasticandClaude Opus 5.5 ab1c6b0516 M4.94: EMF and WMF export from Export As and document.export
New crates/metafile (L3, pure Rust): hand-written EMF and WMF writers over
one shared document walk. EMF keeps Bezier paths, solid brushes, geometric
pens (width, caps, joins, miter limit, user-style dashes), clipping masks as
path clips, images (StretchDIBits, or AlphaBlend with transparency) placed by
a world transform, type as outlines; gradients become images clipped to their
shape and pattern fills their tiles clipped to it. The header's frame is the
artboard in 0.01 mm on a 100 px/mm reference device. WMF flattens curves into
polygons in 16-bit units behind a placeable header (with its checksum),
numbers objects as players do, and reports what it leaves out (clipping,
transparency, gradients, rotated images).

document.export {format: emf|wmf} (and serialize, export) writes one artboard,
or one file per artboard with useArtboards (false: the art's bounds); raster
effects are rendered to images first, as for PDF. Export As lists EMF and WMF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:24:24 +07:00

504 lines
18 KiB
Rust

//! The EMF writer: an EMR_HEADER whose frame is the region in 0.01 mm, then the drawing in device
//! units of 0.01 mm (the reference device has 100 pixels a millimetre) and an EMR_EOF.
//!
//! Each fill and stroke is a path bracket (move-to, poly-line-to and poly-Bézier-to runs, close
//! figure) filled with a solid brush or stroked with a geometric pen; brushes and pens are made
//! when the colour or pen changes and deleted when replaced. Clips are path clips inside a saved
//! DC; images are StretchDIBits (opaque) or AlphaBlend (with transparency) under a world transform
//! that places their pixels.
use kurbo::PathEl;
use vectorcraft_doc::{LineCap, LineJoin};
use vectorcraft_geom::{Affine, BezPath, FillRule, Point, Rect};
use crate::bytes::Out;
use crate::dib::{self, Rgba};
use crate::scene::{Op, Pen};
// Record types (MS-EMF 2.1.1).
pub(crate) const HEADER: u32 = 1;
pub(crate) const POLYBEZIERTO: u32 = 5;
pub(crate) const POLYLINETO: u32 = 6;
pub(crate) const EOF: u32 = 14;
pub(crate) const SETBKMODE: u32 = 18;
pub(crate) const SETPOLYFILLMODE: u32 = 19;
pub(crate) const MOVETOEX: u32 = 27;
pub(crate) const SAVEDC: u32 = 33;
pub(crate) const RESTOREDC: u32 = 34;
pub(crate) const SETWORLDTRANSFORM: u32 = 35;
pub(crate) const MODIFYWORLDTRANSFORM: u32 = 36;
pub(crate) const SELECTOBJECT: u32 = 37;
pub(crate) const CREATEBRUSHINDIRECT: u32 = 39;
pub(crate) const DELETEOBJECT: u32 = 40;
pub(crate) const SETMITERLIMIT: u32 = 58;
pub(crate) const BEGINPATH: u32 = 59;
pub(crate) const ENDPATH: u32 = 60;
pub(crate) const CLOSEFIGURE: u32 = 61;
pub(crate) const FILLPATH: u32 = 62;
pub(crate) const STROKEPATH: u32 = 64;
pub(crate) const SELECTCLIPPATH: u32 = 67;
pub(crate) const STRETCHDIBITS: u32 = 81;
pub(crate) const EXTCREATEPEN: u32 = 95;
pub(crate) const ALPHABLEND: u32 = 114;
/// The `" EMF"` signature in the header.
pub(crate) const SIGNATURE: u32 = 0x464D_4520;
/// Stock objects (selected to release ours before deleting them).
const WHITE_BRUSH: u32 = 0x8000_0000;
const NULL_PEN: u32 = 0x8000_0008;
/// Our two object slots (0 is reserved).
const BRUSH: u32 = 1;
const PEN: u32 = 2;
pub(crate) const PS_GEOMETRIC: u32 = 0x0001_0000;
pub(crate) const PS_USERSTYLE: u32 = 7;
pub(crate) const PS_ENDCAP_SQUARE: u32 = 0x100;
pub(crate) const PS_ENDCAP_FLAT: u32 = 0x200;
pub(crate) const PS_JOIN_BEVEL: u32 = 0x1000;
pub(crate) const PS_JOIN_MITER: u32 = 0x2000;
/// Raster operation: copy the source.
pub(crate) const SRCCOPY: u32 = 0x00CC_0020;
/// Device units (0.01 mm) per point.
pub(crate) const UNITS_PER_PT: f64 = 2540.0 / 72.0;
/// Longest dash pattern a geometric pen takes (longer ones are drawn as separate dashes).
const MAX_STYLE: usize = 16;
/// Header size with both extensions.
const HEADER_SIZE: usize = 108;
/// Coordinates are kept within this (far beyond any page).
const MAX_COORD: f64 = 1e9;
/// Does `bytes` start with an EMF header?
pub(crate) fn is_emf(bytes: &[u8]) -> bool {
let u32_at = |at: usize| bytes.get(at..at + 4).and_then(|b| b.try_into().ok()).map(u32::from_le_bytes);
u32_at(0) == Some(HEADER) && u32_at(40) == Some(SIGNATURE)
}
/// A COLORREF (0x00bbggrr).
pub(crate) fn colorref(rgb: [u8; 3]) -> u32 {
u32::from_le_bytes([rgb[0], rgb[1], rgb[2], 0])
}
/// What a pen object is made of, to tell when the next stroke needs another.
#[derive(Clone, PartialEq)]
struct PenKey {
style: u32,
width: u32,
color: u32,
entries: Vec<u32>,
}
/// A device-space bounding box (inclusive).
type Bounds = [i32; 4];
fn union(a: Option<Bounds>, b: Bounds) -> Bounds {
match a {
None => b,
Some(a) => [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])],
}
}
struct Writer {
out: Out,
records: u32,
/// Document → device units.
to_dev: Affine,
/// The frame in device units: what the region clip lets through.
frame: Bounds,
/// Everything drawn, in device units.
bounds: Option<Bounds>,
brush: Option<u32>,
pen: Option<PenKey>,
fill_mode: Option<u32>,
miter: Option<u32>,
}
/// Write `ops` as an EMF of `region` (document space), described as `title`.
pub(crate) fn write(ops: &[Op], region: Rect, title: &str) -> Vec<u8> {
let k = UNITS_PER_PT;
let frame = [0, 0, (region.width() * k).round() as i32, (region.height() * k).round() as i32];
let mut w = Writer {
out: Out::default(),
records: 0,
to_dev: Affine::scale(k) * Affine::translate(-region.origin().to_vec2()),
frame,
bounds: None,
brush: None,
pen: None,
fill_mode: None,
miter: None,
};
w.header(title);
// Dash gaps and hatches show what is below.
w.record(SETBKMODE, |o| o.u32(1));
for op in ops {
w.op(op);
}
w.release();
w.record(EOF, |o| {
o.u32(0);
o.u32(16);
o.u32(20);
});
w.finish()
}
impl Writer {
/// Append one record: its type, size and `body`, padded to 4 bytes.
fn record(&mut self, kind: u32, body: impl FnOnce(&mut Out)) {
let start = self.out.len();
self.out.u32(kind);
self.out.u32(0);
body(&mut self.out);
self.out.pad(4);
let size = u32::try_from(self.out.len() - start).unwrap_or(u32::MAX);
self.out.set_u32(start + 4, size);
self.records += 1;
}
fn header(&mut self, title: &str) {
let desc: Vec<u16> =
"VectorCraft".encode_utf16().chain([0]).chain(title.chars().take(200).collect::<String>().encode_utf16()).chain([0, 0]).collect();
let f = self.frame;
// The reference device: 100 pixels a millimetre, as large as the frame.
let mm = [(f[2] / 100).max(1) + 1, (f[3] / 100).max(1) + 1];
self.record(HEADER, |o| {
// Bounds, patched in `finish`.
o.bytes(&[0; 16]);
for v in f {
o.i32(v);
}
o.u32(SIGNATURE);
o.u32(0x0001_0000);
// Bytes, records and handles, patched in `finish`.
o.u32(0);
o.u32(0);
o.u16(3);
o.u16(0);
o.u32(desc.len() as u32);
o.u32(HEADER_SIZE as u32);
o.u32(0);
o.i32(mm[0] * 100);
o.i32(mm[1] * 100);
o.i32(mm[0]);
o.i32(mm[1]);
// No pixel format, not OpenGL; the device in micrometres.
o.u32(0);
o.u32(0);
o.u32(0);
o.u32(mm[0] as u32 * 1000);
o.u32(mm[1] as u32 * 1000);
for c in &desc {
o.u16(*c);
}
});
}
/// Patch the header's bounds, size and record count.
fn finish(mut self) -> Vec<u8> {
let b = self.bounds.map(|b| [b[0].max(self.frame[0]), b[1].max(self.frame[1]), b[2].min(self.frame[2]), b[3].min(self.frame[3])]);
// An empty picture has bounds (0, 0, -1, -1).
let b = b.filter(|b| b[0] <= b[2] && b[1] <= b[3]).unwrap_or([0, 0, -1, -1]);
for (i, v) in b.iter().enumerate() {
self.out.set_i32(8 + 4 * i, *v);
}
let size = u32::try_from(self.out.len()).unwrap_or(u32::MAX);
self.out.set_u32(48, size);
self.out.set_u32(52, self.records);
self.out.0
}
fn dev(&self, p: Point) -> [i32; 2] {
let q = self.to_dev * p;
let c = |v: f64| if v.is_finite() { v.round().clamp(-MAX_COORD, MAX_COORD) as i32 } else { 0 };
[c(q.x), c(q.y)]
}
fn grow(&mut self, b: Bounds) {
self.bounds = Some(union(self.bounds, b));
}
fn op(&mut self, op: &Op) {
match op {
Op::Fill { path, rule, rgb } => {
self.fill_mode(*rule);
self.brush(*rgb);
if let Some(b) = self.path(path) {
self.grow(b);
self.record(FILLPATH, |o| b.iter().for_each(|v| o.i32(*v)));
}
}
Op::Stroke { path, pen } => {
let dashed;
let path = if self.pen(pen) {
path
} else {
dashed = crate::scene::dashed(path, pen.dash.as_ref());
&dashed
};
if let Some(b) = self.path(path) {
let half = (pen.width * UNITS_PER_PT / 2.0).ceil() as i32;
let b = [b[0] - half, b[1] - half, b[2] + half, b[3] + half];
self.grow(b);
self.record(STROKEPATH, |o| b.iter().for_each(|v| o.i32(*v)));
}
}
Op::Image { image, xf, opacity } => self.image(image, *xf, *opacity),
Op::Clip { path, rule } => {
self.release();
self.record(SAVEDC, |_| {});
self.fill_mode(*rule);
if self.path(path).is_some() {
// RGN_AND: inside the clip already set.
self.record(SELECTCLIPPATH, |o| o.u32(1));
}
}
Op::Unclip => {
self.release();
self.record(RESTOREDC, |o| o.i32(-1));
// The DC's fill mode and miter limit are back to what they were at the save.
self.fill_mode = None;
self.miter = None;
}
}
}
fn fill_mode(&mut self, rule: FillRule) {
// ALTERNATE (even-odd) or WINDING (non-zero).
let mode = if rule == FillRule::EvenOdd { 1 } else { 2 };
if self.fill_mode != Some(mode) {
self.record(SETPOLYFILLMODE, |o| o.u32(mode));
self.fill_mode = Some(mode);
}
}
/// Select a solid brush of `rgb`, made when the colour changes.
fn brush(&mut self, rgb: [u8; 3]) {
let color = colorref(rgb);
if self.brush == Some(color) {
return;
}
self.drop_brush();
self.record(CREATEBRUSHINDIRECT, |o| {
o.u32(BRUSH);
// BS_SOLID, the colour, no hatch.
o.u32(0);
o.u32(color);
o.u32(0);
});
self.record(SELECTOBJECT, |o| o.u32(BRUSH));
self.brush = Some(color);
}
fn drop_brush(&mut self) {
if self.brush.take().is_some() {
self.record(SELECTOBJECT, |o| o.u32(WHITE_BRUSH));
self.record(DELETEOBJECT, |o| o.u32(BRUSH));
}
}
fn drop_pen(&mut self) {
if self.pen.take().is_some() {
self.record(SELECTOBJECT, |o| o.u32(NULL_PEN));
self.record(DELETEOBJECT, |o| o.u32(PEN));
}
}
/// Release our objects (before a DC is saved or restored, which would bring back deleted ones).
fn release(&mut self) {
self.drop_brush();
self.drop_pen();
}
/// Select a geometric pen for `p`, made when it changes. False when `p`'s dashes must be
/// drawn as separate lines (the pen selected is solid): a pen's dashes start at the path's
/// start, every entry at least a unit long, 16 entries at most.
fn pen(&mut self, p: &Pen) -> bool {
let k = UNITS_PER_PT;
let entries: Option<Vec<u32>> = p.dash.as_ref().map(|d| {
let mut v: Vec<f64> = d.pattern.clone();
if v.len() % 2 == 1 {
v.extend_from_within(..);
}
v.iter().map(|x| (x * k).round().clamp(0.0, f64::from(u32::MAX)) as u32).collect()
});
let native = match (&entries, &p.dash) {
(Some(e), Some(d)) => d.offset.abs() < 1e-9 && e.len() <= MAX_STYLE && e.iter().all(|x| *x >= 1),
_ => true,
};
let entries = if native { entries.unwrap_or_default() } else { vec![] };
let cap = match p.cap {
LineCap::Butt => PS_ENDCAP_FLAT,
LineCap::Round => 0,
LineCap::Square => PS_ENDCAP_SQUARE,
};
let join = match p.join {
LineJoin::Miter => PS_JOIN_MITER,
LineJoin::Round => 0,
LineJoin::Bevel => PS_JOIN_BEVEL,
};
let style = PS_GEOMETRIC | cap | join | if entries.is_empty() { 0 } else { PS_USERSTYLE };
let key = PenKey { style, width: (p.width * k).round().clamp(1.0, 1e9) as u32, color: colorref(p.rgb), entries };
if p.join == LineJoin::Miter {
let m = p.miter.round().clamp(1.0, 1000.0) as u32;
if self.miter != Some(m) {
self.record(SETMITERLIMIT, |o| o.u32(m));
self.miter = Some(m);
}
}
if self.pen.as_ref() != Some(&key) {
self.drop_pen();
let n = key.entries.len() as u32;
self.record(EXTCREATEPEN, |o| {
o.u32(PEN);
// No pattern bitmap: its offsets point past the record, its sizes are 0.
let end = 52 + 4 * n;
o.u32(end);
o.u32(0);
o.u32(end);
o.u32(0);
o.u32(key.style);
o.u32(key.width);
// BS_SOLID, the colour, no hatch.
o.u32(0);
o.u32(key.color);
o.u32(0);
o.u32(n);
key.entries.iter().for_each(|e| o.u32(*e));
});
self.record(SELECTOBJECT, |o| o.u32(PEN));
self.pen = Some(key);
}
native
}
/// `bp` as a path bracket → its device bounds (`None`: nothing to draw).
fn path(&mut self, bp: &BezPath) -> Option<Bounds> {
if !bp.elements().iter().any(|e| !matches!(e, PathEl::MoveTo(_) | PathEl::ClosePath)) {
return None;
}
self.record(BEGINPATH, |_| {});
let mut bounds: Option<Bounds> = None;
let mut run: Vec<[i32; 2]> = vec![];
let mut curves = false;
let mut last = Point::ZERO;
let mut start = Point::ZERO;
for el in bp.elements() {
let (pts, is_curve): (Vec<Point>, bool) = match *el {
PathEl::MoveTo(p) => {
self.flush_run(&mut run, curves);
let d = self.dev(p);
bounds = Some(union(bounds, [d[0], d[1], d[0], d[1]]));
self.record(MOVETOEX, |o| d.iter().for_each(|v| o.i32(*v)));
(last, start) = (p, p);
continue;
}
PathEl::ClosePath => {
self.flush_run(&mut run, curves);
self.record(CLOSEFIGURE, |_| {});
last = start;
continue;
}
PathEl::LineTo(p) => (vec![p], false),
PathEl::QuadTo(c, p) => (vec![last + (c - last) * (2.0 / 3.0), p + (c - p) * (2.0 / 3.0), p], true),
PathEl::CurveTo(a, b, p) => (vec![a, b, p], true),
};
if is_curve != curves {
self.flush_run(&mut run, curves);
curves = is_curve;
}
for p in &pts {
let d = self.dev(*p);
bounds = Some(union(bounds, [d[0], d[1], d[0], d[1]]));
run.push(d);
}
last = pts.last().copied().unwrap_or(last);
}
self.flush_run(&mut run, curves);
self.record(ENDPATH, |_| {});
bounds
}
/// Write the points of a run of lines or curves, then empty it.
fn flush_run(&mut self, run: &mut Vec<[i32; 2]>, curves: bool) {
if run.is_empty() {
return;
}
let pts = std::mem::take(run);
let b = pts.iter().fold(None, |acc, p| Some(union(acc, [p[0], p[1], p[0], p[1]]))).unwrap_or([0, 0, -1, -1]);
self.record(if curves { POLYBEZIERTO } else { POLYLINETO }, |o| {
b.iter().for_each(|v| o.i32(*v));
o.u32(pts.len() as u32);
for p in &pts {
o.i32(p[0]);
o.i32(p[1]);
}
});
}
/// An image placed by `xf` (its pixels → document): StretchDIBits when opaque, else AlphaBlend,
/// drawn at its pixel size under a world transform.
fn image(&mut self, img: &Rgba, xf: Affine, opacity: f32) {
if img.width == 0 || img.height == 0 || opacity <= 0.0 {
return;
}
let m = self.to_dev * xf;
if !m.as_coeffs().iter().all(|v| v.is_finite()) || m.determinant().abs() < 1e-12 {
return;
}
let (w, h) = (f64::from(img.width), f64::from(img.height));
let corners = [Point::ZERO, Point::new(w, 0.0), Point::new(0.0, h), Point::new(w, h)].map(|p| self.dev(xf * p));
let b = corners.iter().fold(None, |acc, p| Some(union(acc, [p[0], p[1], p[0], p[1]]))).unwrap_or([0, 0, -1, -1]);
self.grow(b);
let [a, bb, c, d, e, f] = m.as_coeffs();
self.record(SETWORLDTRANSFORM, |o| [a, bb, c, d, e, f].iter().for_each(|v| o.f32(*v as f32)));
let (iw, ih) = (img.width as i32, img.height as i32);
if img.opaque() && opacity >= 0.998 {
let (bmi, bits) = dib::rgb24(img, 1.0);
self.record(STRETCHDIBITS, |o| {
b.iter().for_each(|v| o.i32(*v));
// Destination and source rectangles.
[0, 0, 0, 0, iw, ih].iter().for_each(|v| o.i32(*v));
o.u32(80);
o.u32(bmi.len() as u32);
o.u32(80 + bmi.len() as u32);
o.u32(bits.len() as u32);
// DIB_RGB_COLORS, SRCCOPY, the destination size.
o.u32(0);
o.u32(SRCCOPY);
o.i32(iw);
o.i32(ih);
o.bytes(&bmi);
o.bytes(&bits);
});
} else {
let (bmi, bits) = dib::bgra32(img, opacity);
self.record(ALPHABLEND, |o| {
b.iter().for_each(|v| o.i32(*v));
[0, 0, iw, ih].iter().for_each(|v| o.i32(*v));
// AC_SRC_OVER, no flags, constant alpha 255, AC_SRC_ALPHA (per-pixel alpha).
o.bytes(&[0, 0, 255, 1]);
o.i32(0);
o.i32(0);
// The source's own transform: identity.
[1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0].iter().for_each(|v| o.f32(*v));
o.u32(0);
o.u32(0);
o.u32(108);
o.u32(bmi.len() as u32);
o.u32(108 + bmi.len() as u32);
o.u32(bits.len() as u32);
o.i32(iw);
o.i32(ih);
o.bytes(&bmi);
o.bytes(&bits);
});
}
// MWT_IDENTITY: back to drawing in device units.
self.record(MODIFYWORLDTRANSFORM, |o| {
[1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0].iter().for_each(|v| o.f32(*v));
o.u32(1);
});
}
}