mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 21:51:36 +00:00
307 lines
13 KiB
Rust
307 lines
13 KiB
Rust
//! WMF records (MS-WMF) played on a [`Player`]. A placeable header gives the picture's box in
|
|
//! logical units and the units an inch; without one, the first window origin and extent are the
|
|
//! box, at 1440 units an inch.
|
|
|
|
use vectorcraft_geom::{Affine, Point, Rect, shapes};
|
|
|
|
use super::{ArcKind, BrushObj, FontObj, Obj, PenObj, Player};
|
|
use crate::bytes::Reader;
|
|
use crate::dib::{self, Alpha};
|
|
use crate::wmf::PLACEABLE_KEY;
|
|
use crate::{Imported, Kind};
|
|
|
|
/// Units an inch when the file doesn't say (twips).
|
|
const DEFAULT_INCH: f64 = 1440.0;
|
|
/// Records that change nothing VectorCraft keeps (background, raster and mapping details, palettes,
|
|
/// escapes).
|
|
const IGNORED: &[u16] = &[
|
|
0x0102, 0x0103, 0x0104, 0x0105, 0x0107, 0x0108, 0x0201, 0x020A, 0x020B, 0x020C, 0x020D, 0x020E, 0x020F, 0x0211, 0x0410, 0x0412, 0x0231, 0x0234,
|
|
0x0035, 0x0037, 0x0139, 0x0436, 0x0626,
|
|
];
|
|
|
|
fn point(r: &mut Reader) -> Option<Point> {
|
|
Some(Point::new(f64::from(r.i16()?), f64::from(r.i16()?)))
|
|
}
|
|
|
|
/// A point stored y first.
|
|
fn point_yx(r: &mut Reader) -> Option<Point> {
|
|
let y = f64::from(r.i16()?);
|
|
Some(Point::new(f64::from(r.i16()?), y))
|
|
}
|
|
|
|
/// A rectangle stored bottom, right, top, left.
|
|
fn rect_brtl(r: &mut Reader) -> Option<Rect> {
|
|
let (b, rt, t, l) = (r.i16()?, r.i16()?, r.i16()?, r.i16()?);
|
|
Some(Rect::new(f64::from(l), f64::from(t), f64::from(rt), f64::from(b)))
|
|
}
|
|
|
|
fn color(r: &mut Reader) -> Option<[u8; 3]> {
|
|
let [red, g, b, _] = r.u32()?.to_le_bytes();
|
|
Some([red, g, b])
|
|
}
|
|
|
|
fn points(r: &mut Reader, n: usize) -> Option<Vec<Point>> {
|
|
if r.rest().len() < n * 4 {
|
|
return None;
|
|
}
|
|
(0..n).map(|_| point(r)).collect()
|
|
}
|
|
|
|
/// ANSI text (read as Latin-1).
|
|
fn ansi(b: &[u8]) -> String {
|
|
b.iter().map(|c| char::from(*c)).collect()
|
|
}
|
|
|
|
/// The records' start, and the box and units an inch of a placeable header.
|
|
fn header(bytes: &[u8]) -> Option<(usize, Option<(Rect, f64)>)> {
|
|
let mut r = Reader::new(bytes);
|
|
let placeable = if r.u32()? == PLACEABLE_KEY {
|
|
r.skip(2)?;
|
|
let (l, t, rt, b) = (r.i16()?, r.i16()?, r.i16()?, r.i16()?);
|
|
let inch = r.u16()?;
|
|
let inch = if inch == 0 { DEFAULT_INCH } else { f64::from(inch) };
|
|
Some((Rect::new(f64::from(l), f64::from(t), f64::from(rt), f64::from(b)).abs(), inch))
|
|
} else {
|
|
None
|
|
};
|
|
let start = if placeable.is_some() { 22 } else { 0 };
|
|
let mut h = Reader::new(bytes.get(start..)?);
|
|
h.skip(2)?;
|
|
let words = usize::from(h.u16()?);
|
|
if words < 9 {
|
|
return None;
|
|
}
|
|
Some((start + words * 2, placeable))
|
|
}
|
|
|
|
/// The first window origin and extent, as the picture's box when there is no placeable header.
|
|
fn window(bytes: &[u8], mut at: usize) -> Option<Rect> {
|
|
let (mut org, mut ext) = (None, None);
|
|
while let Some(head) = bytes.get(at..at + 6) {
|
|
let words = u32::from_le_bytes([head[0], head[1], head[2], head[3]]) as usize;
|
|
let function = u16::from_le_bytes([head[4], head[5]]);
|
|
if words < 3 || function == 0 {
|
|
break;
|
|
}
|
|
let mut r = Reader::new(bytes.get(at + 6..).unwrap_or_default());
|
|
match function {
|
|
0x020B if org.is_none() => org = point_yx(&mut r),
|
|
0x020C if ext.is_none() => ext = point_yx(&mut r),
|
|
_ => {}
|
|
}
|
|
if org.is_some() && ext.is_some() {
|
|
break;
|
|
}
|
|
at = at.saturating_add(words.saturating_mul(2));
|
|
}
|
|
let o = org.unwrap_or(Point::ZERO);
|
|
let e = ext?;
|
|
Some(Rect::new(o.x, o.y, o.x + e.x, o.y + e.y).abs())
|
|
}
|
|
|
|
/// Logical → document for a placeable file whose window (`org`, `ext`) is mapped onto its box `b`;
|
|
/// a negative extent flips that axis, as in GDI.
|
|
fn window_map(b: Rect, k: f64, org: Point, ext: Point) -> Affine {
|
|
let ratio = |box_len: f64, e: f64| if e.abs() > 1e-12 { box_len / e } else { 1.0 };
|
|
Affine::scale_non_uniform(k * ratio(b.width(), ext.x), k * ratio(b.height(), ext.y)) * Affine::translate(-org.to_vec2())
|
|
}
|
|
|
|
pub(super) fn play(bytes: &[u8]) -> Result<Imported, String> {
|
|
let (start, placeable) = header(bytes).ok_or("the WMF header is damaged")?;
|
|
let (frame, inch) = match placeable {
|
|
Some((b, inch)) if b.width() > 0.0 && b.height() > 0.0 => (Some(b), inch),
|
|
_ => (window(bytes, start).filter(|w| w.width() > 0.0 && w.height() > 0.0), DEFAULT_INCH),
|
|
};
|
|
let k = 72.0 / inch;
|
|
let origin = frame.map_or(Point::ZERO, |f| f.origin());
|
|
let artboard = frame.map_or(Rect::new(0.0, 0.0, 1.0, 1.0), |f| Rect::new(0.0, 0.0, f.width() * k, f.height() * k));
|
|
let mut p = Player::new(Kind::Wmf, Affine::scale(k) * Affine::translate(-origin.to_vec2()), 25.4 / inch, artboard);
|
|
// With a placeable header, the window records map the window onto the box.
|
|
let placed = placeable.map(|(b, _)| b).filter(|b| b.width() > 0.0 && b.height() > 0.0);
|
|
let mut win = (placed.map_or(Point::ZERO, |b| b.origin()), placed.map_or(Point::ZERO, |b| Point::new(b.width(), b.height())));
|
|
let mut at = start;
|
|
let mut ended = false;
|
|
while let Some(head) = bytes.get(at..at + 6) {
|
|
let words = u32::from_le_bytes([head[0], head[1], head[2], head[3]]) as usize;
|
|
let function = u16::from_le_bytes([head[4], head[5]]);
|
|
if function == 0 {
|
|
ended = true;
|
|
break;
|
|
}
|
|
let Some(rec) = words.checked_mul(2).and_then(|n| bytes.get(at..at.checked_add(n)?)).filter(|_| words >= 3) else { break };
|
|
if !matches!(function, 0x0213 | 0x0214) {
|
|
p.flush();
|
|
}
|
|
if let (Some(b), 0x020B | 0x020C) = (placed, function) {
|
|
if let Some(v) = point_yx(&mut Reader::new(rec.get(6..).unwrap_or_default())) {
|
|
if function == 0x020B {
|
|
win.0 = v;
|
|
} else {
|
|
win.1 = v;
|
|
}
|
|
p.dev_to_doc = window_map(b, k, win.0, win.1);
|
|
}
|
|
} else if record(&mut p, function, rec.get(6..).unwrap_or_default()).is_none() {
|
|
p.skip();
|
|
}
|
|
at += rec.len();
|
|
}
|
|
if !ended {
|
|
p.warn("the file ends early: what it held was imported");
|
|
}
|
|
let mut out = p.finish();
|
|
// No box given: the art's bounds.
|
|
if frame.is_none()
|
|
&& let Some(b) = out.document.layers.first().and_then(|l| vectorcraft_doc::live::nodes_bounds(l.children().map_or(&[][..], |c| c.as_slice())))
|
|
&& let Some(a) = out.document.artboards.first_mut()
|
|
{
|
|
a.rect = Rect::from_origin_size(b.origin(), (b.width().max(1.0), b.height().max(1.0)));
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
/// An image record's source rectangle (from the bottom for a bottom-up DIB) and destination.
|
|
fn blit(p: &mut Player, rop: u32, src: (i16, i16, i16, i16), dest: (i16, i16, i16, i16), packed: Option<&[u8]>) {
|
|
let (xd, yd, wd, hd) = (f64::from(dest.0), f64::from(dest.1), f64::from(dest.2), f64::from(dest.3));
|
|
let d = Rect::new(xd, yd, xd + wd, yd + hd);
|
|
let Some(packed) = packed else { return p.pattern_blit(d, rop) };
|
|
let parts = dib::split(packed);
|
|
let img = parts.ok_or_else(|| "damaged".to_string()).and_then(|(bmi, bits)| dib::decode(bmi, bits, Alpha::Ignore));
|
|
let bottom_up = parts.and_then(|(bmi, _)| bmi.get(8..12)).is_some_and(|h| i32::from_le_bytes([h[0], h[1], h[2], h[3]]) > 0);
|
|
let ih = img.as_ref().map_or(0.0, |i| f64::from(i.height));
|
|
let (xs, ys, ws, hs) = (f64::from(src.0), f64::from(src.1), f64::from(src.2), f64::from(src.3));
|
|
let top = if bottom_up { ih - ys - hs } else { ys };
|
|
p.image(img, Rect::new(xs, top, xs + ws, top + hs), d, rop, 1.0);
|
|
}
|
|
|
|
/// Play one record (`r`: its parameters). `None`: unknown or damaged.
|
|
fn record(p: &mut Player, function: u16, body: &[u8]) -> Option<()> {
|
|
let mut r = Reader::new(body);
|
|
match function {
|
|
0x001E => p.save(),
|
|
0x0127 => p.restore(i32::from(r.i16()?)),
|
|
0x0106 => p.set_fill_mode(r.u16()? == 1),
|
|
0x0209 => p.set_text_color(color(&mut r)?),
|
|
0x012E => p.set_text_align(u32::from(r.u16()?)),
|
|
0x0214 => p.move_to(point_yx(&mut r)?),
|
|
0x0213 => {
|
|
let pt = point_yx(&mut r)?;
|
|
p.poly_to(&[pt], false);
|
|
}
|
|
0x0324 | 0x0325 => {
|
|
let n = usize::try_from(r.i16()?).ok()?;
|
|
let pts = points(&mut r, n)?;
|
|
p.poly(&pts, function == 0x0324, false);
|
|
}
|
|
0x0538 => {
|
|
let n = usize::from(r.u16()?);
|
|
if r.rest().len() < n * 2 {
|
|
return None;
|
|
}
|
|
let counts: Vec<usize> = (0..n).map(|_| r.u16().map(usize::from)).collect::<Option<_>>()?;
|
|
let mut polys = Vec::with_capacity(n);
|
|
for c in counts {
|
|
polys.push(points(&mut r, c)?);
|
|
}
|
|
p.poly_poly(&polys, true);
|
|
}
|
|
0x041B => p.figure(shapes::rectangle(rect_brtl(&mut r)?.abs()).to_bezpath(), true),
|
|
0x0418 => p.figure(shapes::ellipse(rect_brtl(&mut r)?.abs()).to_bezpath(), true),
|
|
0x061C => {
|
|
let (h, w) = (f64::from(r.i16()?), f64::from(r.i16()?));
|
|
p.figure(super::round_rect(rect_brtl(&mut r)?, w, h), true);
|
|
}
|
|
0x0817 | 0x081A | 0x0830 => {
|
|
let (end, start) = (point_yx(&mut r)?, point_yx(&mut r)?);
|
|
let b = rect_brtl(&mut r)?;
|
|
let kind = match function {
|
|
0x0817 => ArcKind::Open,
|
|
0x081A => ArcKind::Pie,
|
|
_ => ArcKind::Chord,
|
|
};
|
|
p.arc(b, start, end, kind);
|
|
}
|
|
0x0416 => p.clip_rect(rect_brtl(&mut r)?),
|
|
0x0415 => p.exclude_clip(),
|
|
0x012D => p.select(u32::from(r.u16()?)),
|
|
0x01F0 => p.delete(u32::from(r.u16()?)),
|
|
0x02FA => {
|
|
let style = u32::from(r.u16()?);
|
|
let width = f64::from(r.i16()?);
|
|
r.skip(2)?;
|
|
p.create(Obj::Pen(PenObj { style, width, color: color(&mut r)?, entries: vec![] }));
|
|
}
|
|
0x02FC => {
|
|
let style = u32::from(r.u16()?);
|
|
p.create(Obj::Brush(BrushObj::from_log(style, color(&mut r)?)));
|
|
}
|
|
0x02FB => {
|
|
let height = f64::from(r.i16()?);
|
|
r.skip(2)?;
|
|
let escapement = f64::from(r.i16()?);
|
|
r.skip(2)?;
|
|
let weight = i32::from(r.i16()?);
|
|
let italic = r.bytes(1)?.first().is_some_and(|b| *b != 0);
|
|
r.skip(7)?;
|
|
let face: Vec<u8> = r.rest().iter().take(32).take_while(|b| **b != 0).copied().collect();
|
|
p.create(Obj::Font(FontObj { height, escapement, weight, italic, face: ansi(&face) }));
|
|
}
|
|
0x00F7 | 0x06FF => p.create(Obj::Other),
|
|
0x01F9 | 0x0142 => p.create(Obj::Brush(BrushObj::Pattern)),
|
|
0x0521 => {
|
|
let n = usize::try_from(r.i16()?).ok()?;
|
|
let s = ansi(r.bytes(n)?);
|
|
r.skip(n % 2)?;
|
|
let at = point_yx(&mut r)?;
|
|
p.text(at, &s);
|
|
}
|
|
0x0A32 => {
|
|
let at = point_yx(&mut r)?;
|
|
let n = usize::try_from(r.i16()?).ok()?;
|
|
let opts = r.u16()?;
|
|
// ETO_OPAQUE or ETO_CLIPPED: a rectangle comes first.
|
|
if opts & 0x6 != 0 {
|
|
r.skip(8)?;
|
|
}
|
|
let s = ansi(r.bytes(n)?);
|
|
p.text(at, &s);
|
|
}
|
|
0x0F43 => {
|
|
let rop = r.u32()?;
|
|
r.skip(2)?;
|
|
let (hs, ws, ys, xs) = (r.i16()?, r.i16()?, r.i16()?, r.i16()?);
|
|
let (hd, wd, yd, xd) = (r.i16()?, r.i16()?, r.i16()?, r.i16()?);
|
|
blit(p, rop, (xs, ys, ws, hs), (xd, yd, wd, hd), Some(r.rest()));
|
|
}
|
|
0x0B41 => {
|
|
let rop = r.u32()?;
|
|
let (hs, ws, ys, xs) = (r.i16()?, r.i16()?, r.i16()?, r.i16()?);
|
|
// Without a bitmap, a reserved word comes before the destination.
|
|
let with_bitmap = body.len() > 22;
|
|
if !with_bitmap {
|
|
r.skip(2)?;
|
|
}
|
|
let (hd, wd, yd, xd) = (r.i16()?, r.i16()?, r.i16()?, r.i16()?);
|
|
blit(p, rop, (xs, ys, ws, hs), (xd, yd, wd, hd), with_bitmap.then(|| r.rest()));
|
|
}
|
|
0x0940 => {
|
|
let rop = r.u32()?;
|
|
let (ys, xs) = (r.i16()?, r.i16()?);
|
|
let with_bitmap = body.len() > 20;
|
|
if !with_bitmap {
|
|
r.skip(2)?;
|
|
}
|
|
let (h, w, yd, xd) = (r.i16()?, r.i16()?, r.i16()?, r.i16()?);
|
|
blit(p, rop, (xs, ys, w, h), (xd, yd, w, h), with_bitmap.then(|| r.rest()));
|
|
}
|
|
0x061D => {
|
|
let rop = r.u32()?;
|
|
let (h, w, y, x) = (f64::from(r.i16()?), f64::from(r.i16()?), f64::from(r.i16()?), f64::from(r.i16()?));
|
|
p.pattern_blit(Rect::new(x, y, x + w, y + h), rop);
|
|
}
|
|
f if IGNORED.contains(&f) => {}
|
|
_ => return None,
|
|
}
|
|
Some(())
|
|
}
|