Files
vectorcraft/crates/metafile/src/wmf.rs
T
theironchefandClaude Opus 5.5 945dcbae1d Lint: as_chunks for constant chunk sizes, as clippy 1.99 asks
Rust 1.99's clippy adds chunks_exact_to_as_chunks, and cargo clippy
--workspace --all-targets -D warnings (cargo xtask ci) failed on its eight
hits in metafile, pdf and eps. as_chunks gives fixed-size arrays, so the
loops read the same bytes without indexing into slices, and encrypt's
CBC loop takes the remainder from as_chunks instead of the iterator.
as_chunks is stable since 1.88.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
2026-10-05 12:14:56 -07:00

442 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The WMF writer: a placeable header (the picture's size and logical units an inch, with its
//! checksum), the WMF header, then the drawing in 16-bit logical units with an anisotropic window
//! the size of the picture, and the end-of-file record.
//!
//! Curves are flattened into polygons (fills) and polylines (strokes, with pens of the stroke's
//! width, caps and joins; dashes as separate lines). Objects take the lowest free slot of the
//! object table, as players number them: slots 0 and 1 hold a null pen and a null brush, our pen
//! and brush come after and are deleted when replaced. Images are StretchDIB records of 24-bit
//! bitmaps composited over white.
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::emf::{PS_ENDCAP_FLAT, PS_ENDCAP_SQUARE, PS_JOIN_BEVEL, PS_JOIN_MITER, SRCCOPY, colorref};
use crate::scene::{Op, Pen};
/// The key a placeable header starts with.
pub(crate) const PLACEABLE_KEY: u32 = 0x9AC6_CDD7;
// Record functions (MS-WMF 2.1.1.1).
pub(crate) const EOF: u16 = 0x0000;
pub(crate) const SETBKMODE: u16 = 0x0102;
pub(crate) const SETMAPMODE: u16 = 0x0103;
pub(crate) const SETPOLYFILLMODE: u16 = 0x0106;
pub(crate) const SETWINDOWORG: u16 = 0x020B;
pub(crate) const SETWINDOWEXT: u16 = 0x020C;
pub(crate) const SELECTOBJECT: u16 = 0x012D;
pub(crate) const DELETEOBJECT: u16 = 0x01F0;
pub(crate) const CREATEPENINDIRECT: u16 = 0x02FA;
pub(crate) const CREATEBRUSHINDIRECT: u16 = 0x02FC;
pub(crate) const POLYGON: u16 = 0x0324;
pub(crate) const POLYLINE: u16 = 0x0325;
pub(crate) const POLYPOLYGON: u16 = 0x0538;
pub(crate) const STRETCHDIB: u16 = 0x0F43;
/// Most logical units an inch (twips, what office apps use).
const MAX_INCH: f64 = 1440.0;
/// The largest 16-bit coordinate.
const MAX_I16: f64 = 32767.0;
/// Most points a polygon or polyline record holds (its count is 16-bit).
const MAX_POINTS: usize = 32767;
/// Curves are flattened to within this many logical units.
const FLATNESS: f64 = 0.25;
/// The null pen's and the null brush's slots.
const NULL_PEN: u16 = 0;
const NULL_BRUSH: u16 = 1;
const TRANSLUCENT_IMAGES: &str = "WMF has no transparency: images are written over white";
const TURNED_IMAGES: &str = "WMF can't rotate or skew images: they are written upright, filling their bounds";
/// Does `bytes` start like a WMF (placeable, or a plain memory or disk metafile header)?
pub(crate) fn is_wmf(bytes: &[u8]) -> bool {
let u16_at = |at: usize| bytes.get(at..at + 2).and_then(|b| b.try_into().ok()).map(u16::from_le_bytes);
let placeable = bytes.get(..4).is_some_and(|b| b == PLACEABLE_KEY.to_le_bytes());
placeable || (matches!(u16_at(0), Some(1 | 2)) && u16_at(2) == Some(9) && matches!(u16_at(4), Some(0x0100 | 0x0300)))
}
/// The logical units an inch for a picture `w` × `h` points: the most, up to 1440, that keep its
/// size within 16 bits.
pub(crate) fn units_per_inch(w: f64, h: f64) -> u16 {
let side_in = w.max(h) / 72.0;
let fit = if side_in > 0.0 { (MAX_I16 / side_in).floor() } else { MAX_INCH };
fit.clamp(1.0, MAX_INCH) as u16
}
/// The XOR of the first ten 16-bit words of a placeable header.
pub(crate) fn checksum(header: &[u8]) -> u16 {
header.as_chunks::<2>().0.iter().take(10).fold(0, |acc, w| acc ^ u16::from_le_bytes(*w))
}
/// `bp` flattened to within `tol`: each subpath's points and whether it closes.
fn flatten(bp: &BezPath, tol: f64) -> Vec<(Vec<Point>, bool)> {
let mut out: Vec<(Vec<Point>, bool)> = vec![];
let mut cur: Vec<Point> = vec![];
kurbo::flatten(bp.iter(), tol, |el| match el {
PathEl::MoveTo(p) => {
if cur.len() > 1 {
out.push((std::mem::take(&mut cur), false));
}
cur = vec![p];
}
PathEl::LineTo(p) => cur.push(p),
PathEl::ClosePath => {
if cur.len() > 1 {
out.push((std::mem::take(&mut cur), true));
}
cur.clear();
}
_ => {}
});
if cur.len() > 1 {
out.push((cur, false));
}
out
}
/// What a pen object is made of.
#[derive(Clone, Copy, PartialEq)]
struct PenKey {
style: u16,
width: i16,
color: u32,
}
struct Writer {
/// The records after the headers.
out: Out,
max_record: u32,
to_dev: Affine,
/// Logical units per point.
k: f64,
/// Which object slots are taken.
slots: Vec<bool>,
brush: Option<(u16, u32)>,
pen: Option<(u16, PenKey)>,
sel_pen: u16,
sel_brush: u16,
fill_mode: Option<u16>,
}
/// Write `ops` (curves flattened, no clips) as a placeable WMF of `region` (document space).
pub(crate) fn write(ops: &[Op], region: Rect, warnings: &mut Vec<String>) -> Vec<u8> {
let inch = units_per_inch(region.width(), region.height());
let k = f64::from(inch) / 72.0;
let size = |v: f64| (v * k).round().clamp(1.0, MAX_I16) as i16;
let (w, h) = (size(region.width()), size(region.height()));
let mut wr = Writer {
out: Out::default(),
max_record: 0,
to_dev: Affine::scale(k) * Affine::translate(-region.origin().to_vec2()),
k,
slots: vec![],
brush: None,
pen: None,
sel_pen: NULL_PEN,
sel_brush: NULL_BRUSH,
fill_mode: None,
};
// MM_ANISOTROPIC, a window the size of the picture, transparent background.
wr.record(SETMAPMODE, |o| o.u16(8));
wr.record(SETWINDOWORG, |o| {
o.i16(0);
o.i16(0);
});
wr.record(SETWINDOWEXT, |o| {
o.i16(h);
o.i16(w);
});
wr.record(SETBKMODE, |o| o.u16(1));
// The null pen and brush (slots 0 and 1), selected while ours change.
wr.alloc();
wr.record(CREATEPENINDIRECT, |o| {
o.u16(5);
o.i16(0);
o.i16(0);
o.u32(0);
});
wr.alloc();
wr.record(CREATEBRUSHINDIRECT, |o| {
o.u16(1);
o.u32(0);
o.u16(0);
});
wr.select(NULL_PEN);
wr.select(NULL_BRUSH);
let mut translucent = false;
let mut turned = false;
for op in ops {
match op {
Op::Fill { path, rule, rgb } => wr.fill(path, *rule, *rgb),
Op::Stroke { path, pen } => wr.stroke(path, pen),
Op::Image { image, xf, opacity } => {
translucent |= !image.opaque() || *opacity < 0.998;
turned |= !wr.image(image, *xf, *opacity);
}
// The scene gives formats without clipping none.
Op::Clip { .. } | Op::Unclip => {}
}
}
if let Some((slot, _)) = wr.brush.take() {
wr.select(NULL_BRUSH);
wr.delete(slot);
}
if let Some((slot, _)) = wr.pen.take() {
wr.select(NULL_PEN);
wr.delete(slot);
}
wr.record(EOF, |_| {});
for (on, w) in [(translucent, TRANSLUCENT_IMAGES), (turned, TURNED_IMAGES)] {
if on && !warnings.iter().any(|x| x == w) {
warnings.push(w.to_string());
}
}
wr.finish(w, h, inch)
}
impl Writer {
/// Append one record: its size in 16-bit words, its function and `body`.
fn record(&mut self, function: u16, body: impl FnOnce(&mut Out)) {
let start = self.out.len();
self.out.u32(0);
self.out.u16(function);
body(&mut self.out);
self.out.pad(2);
let words = u32::try_from((self.out.len() - start) / 2).unwrap_or(u32::MAX);
self.out.set_u32(start, words);
self.max_record = self.max_record.max(words);
}
/// The placeable header, the WMF header, then the records.
fn finish(self, w: i16, h: i16, inch: u16) -> Vec<u8> {
let mut o = Out::default();
o.u32(PLACEABLE_KEY);
o.u16(0);
for v in [0, 0, w, h] {
o.i16(v);
}
o.u16(inch);
o.u32(0);
let sum = checksum(&o.0);
o.u16(sum);
// A memory metafile, 9 words of header, version 3.
o.u16(1);
o.u16(9);
o.u16(0x0300);
o.u32(u32::try_from((18 + self.out.len()) / 2).unwrap_or(u32::MAX));
o.u16(u16::try_from(self.slots.len()).unwrap_or(u16::MAX));
o.u32(self.max_record);
o.u16(0);
o.bytes(&self.out.0);
o.0
}
/// The lowest free object slot, taken.
fn alloc(&mut self) -> u16 {
let i = match self.slots.iter().position(|t| !t) {
Some(i) => i,
None => {
self.slots.push(false);
self.slots.len() - 1
}
};
if let Some(s) = self.slots.get_mut(i) {
*s = true;
}
u16::try_from(i).unwrap_or(u16::MAX)
}
fn select(&mut self, slot: u16) {
self.record(SELECTOBJECT, |o| o.u16(slot));
}
fn delete(&mut self, slot: u16) {
self.record(DELETEOBJECT, |o| o.u16(slot));
if let Some(s) = self.slots.get_mut(usize::from(slot)) {
*s = false;
}
}
fn dev(&self, p: Point) -> [i16; 2] {
let q = self.to_dev * p;
let c = |v: f64| if v.is_finite() { v.round().clamp(-MAX_I16, MAX_I16) as i16 } else { 0 };
[c(q.x), c(q.y)]
}
/// The device points of `pts`, repeats dropped, thinned to what one record holds.
fn points(&self, pts: &[Point]) -> Vec<[i16; 2]> {
let mut v: Vec<[i16; 2]> = vec![];
for p in pts {
let d = self.dev(*p);
if v.last() != Some(&d) {
v.push(d);
}
}
if v.len() > MAX_POINTS {
let step = v.len().div_ceil(MAX_POINTS - 1);
let last = v.last().copied();
v = v.into_iter().step_by(step).collect();
v.extend(last);
}
v
}
fn select_pen(&mut self, slot: u16) {
if self.sel_pen != slot {
self.select(slot);
self.sel_pen = slot;
}
}
fn select_brush(&mut self, slot: u16) {
if self.sel_brush != slot {
self.select(slot);
self.sel_brush = slot;
}
}
fn use_brush(&mut self, rgb: [u8; 3]) {
let color = colorref(rgb);
let slot = match self.brush {
Some((slot, c)) if c == color => slot,
old => {
if let Some((slot, _)) = old {
self.select_brush(NULL_BRUSH);
self.delete(slot);
}
let slot = self.alloc();
self.record(CREATEBRUSHINDIRECT, |o| {
o.u16(0);
o.u32(color);
o.u16(0);
});
self.brush = Some((slot, color));
slot
}
};
self.select_brush(slot);
}
fn use_pen(&mut self, p: &Pen) {
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 key = PenKey { style: (cap | join) as u16, width: (p.width * self.k).round().clamp(1.0, MAX_I16) as i16, color: colorref(p.rgb) };
let slot = match self.pen {
Some((slot, k)) if k == key => slot,
old => {
if let Some((slot, _)) = old {
self.select_pen(NULL_PEN);
self.delete(slot);
}
let slot = self.alloc();
self.record(CREATEPENINDIRECT, |o| {
o.u16(key.style);
o.i16(key.width);
o.i16(0);
o.u32(key.color);
});
self.pen = Some((slot, key));
slot
}
};
self.select_pen(slot);
}
fn fill(&mut self, bp: &BezPath, rule: FillRule, rgb: [u8; 3]) {
let polys: Vec<Vec<[i16; 2]>> = flatten(bp, FLATNESS / self.k).iter().map(|(p, _)| self.points(p)).filter(|p| p.len() > 2).collect();
if polys.is_empty() {
return;
}
// 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.u16(mode));
self.fill_mode = Some(mode);
}
self.use_brush(rgb);
self.select_pen(NULL_PEN);
for chunk in polys.chunks(usize::from(u16::MAX)) {
self.record(POLYPOLYGON, |o| {
o.u16(chunk.len() as u16);
chunk.iter().for_each(|p| o.u16(p.len() as u16));
for p in chunk.iter().flatten() {
o.i16(p[0]);
o.i16(p[1]);
}
});
}
}
fn stroke(&mut self, bp: &BezPath, pen: &Pen) {
let path = crate::scene::dashed(bp, pen.dash.as_ref());
let lines: Vec<(Vec<[i16; 2]>, bool)> =
flatten(&path, FLATNESS / self.k).iter().map(|(p, closed)| (self.points(p), *closed)).filter(|(p, _)| p.len() > 1).collect();
if lines.is_empty() {
return;
}
self.use_pen(pen);
self.select_brush(NULL_BRUSH);
for (pts, closed) in lines {
// A closed subpath is a polygon (with the null brush, its outline), so it joins at
// its start.
self.record(if closed { POLYGON } else { POLYLINE }, |o| {
o.i16(pts.len() as i16);
for p in &pts {
o.i16(p[0]);
o.i16(p[1]);
}
});
}
}
/// An image placed by `xf`, upright: false when `xf` rotates or skews it (then it fills its
/// bounds).
fn image(&mut self, img: &Rgba, xf: Affine, opacity: f32) -> bool {
if img.width == 0 || img.height == 0 || opacity <= 0.0 {
return true;
}
let m = self.to_dev * xf;
if !m.as_coeffs().iter().all(|v| v.is_finite()) {
return true;
}
let [a, b, c, d, e, f] = m.as_coeffs();
let (w, h) = (f64::from(img.width), f64::from(img.height));
let upright = b.abs() < 1e-9 && c.abs() < 1e-9;
let (x, y, dw, dh) = if upright {
(e, f, a * w, d * h)
} else {
let r = m.transform_rect_bbox(Rect::new(0.0, 0.0, w, h));
(r.x0, r.y0, r.width(), r.height())
};
let s = |v: f64| v.round().clamp(-MAX_I16, MAX_I16) as i16;
let (bmi, bits) = dib::rgb24(img, opacity);
self.record(STRETCHDIB, |o| {
o.u32(SRCCOPY);
// DIB_RGB_COLORS; the source and destination rectangles, last field first.
o.u16(0);
o.i16(img.height as i16);
o.i16(img.width as i16);
o.i16(0);
o.i16(0);
o.i16(s(dh));
o.i16(s(dw));
o.i16(s(y));
o.i16(s(x));
o.bytes(&bmi);
o.bytes(&bits);
});
upright
}
}