mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
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
442 lines
15 KiB
Rust
442 lines
15 KiB
Rust
//! 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
|
||
}
|
||
}
|