mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Metafile: draw EMF+ only pictures
EMF+ only files (no GDI fallback) used to render as empty pictures. Play their vector records: solid brushes and pens, paths, rects, polygons, lines, ellipses and Beziers under world/page transforms with save/restore and containers. Dual files keep drawing their EMF records. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,7 +16,7 @@ const STOCK: u32 = 0x8000_0000;
|
||||
pub(crate) const PS_NULL: u32 = 5;
|
||||
/// Pen style bits; the rest of the style word holds end-cap, join and type flags.
|
||||
pub(crate) const PS_STYLE_MASK: u32 = 0x0F;
|
||||
const BS_SOLID: u32 = 0;
|
||||
pub(crate) const BS_SOLID: u32 = 0;
|
||||
pub(crate) const BS_NULL: u32 = 1;
|
||||
pub(crate) const BS_HATCHED: u32 = 2;
|
||||
/// BS_PATTERN, BS_DIBPATTERN and BS_DIBPATTERNPT: the brush paints a bitmap tile.
|
||||
@@ -442,6 +442,20 @@ impl Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
/// The current world transform (EMF+ draws through it).
|
||||
pub fn world(&self) -> Xform {
|
||||
self.map.world
|
||||
}
|
||||
|
||||
/// Sets the pen and brush directly (EMF+ records name their own pen and brush).
|
||||
pub fn set_pen(&mut self, pen: Pen) {
|
||||
self.pen = pen;
|
||||
}
|
||||
|
||||
pub fn set_brush(&mut self, brush: Brush) {
|
||||
self.brush = brush;
|
||||
}
|
||||
|
||||
/// Pushes the drawing state. Past [`MAX_STACK`] levels the parse stops, since later restores
|
||||
/// could no longer match the file.
|
||||
pub fn save_dc(&mut self) {
|
||||
|
||||
@@ -42,6 +42,7 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> {
|
||||
if let Some(px_mm) = device_px_per_mm(input) {
|
||||
cv.set_device_mm(px_mm);
|
||||
}
|
||||
let mut plus = crate::emfplus::Plus::new();
|
||||
let mut parsed = 0usize;
|
||||
let mut pos = hsize.max(88);
|
||||
while !cv.full() {
|
||||
@@ -54,7 +55,13 @@ pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> {
|
||||
if typ == 14 {
|
||||
break;
|
||||
}
|
||||
record(&mut cv, typ, Bytes(rec));
|
||||
if typ == 70 && Bytes(rec).u32(12) == Some(crate::emfplus::EMF_PLUS) {
|
||||
if let Some(data) = Bytes(rec).tail(16) {
|
||||
plus.comment(&mut cv, data);
|
||||
}
|
||||
} else {
|
||||
record(&mut cv, typ, Bytes(rec));
|
||||
}
|
||||
parsed += 1;
|
||||
let Some(next) = pos.checked_add(size) else { break };
|
||||
pos = next;
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
//! EMF+ (MS-EMFPLUS): GDI+ drawing records carried in EMF comment records.
|
||||
//!
|
||||
//! Most EMF+ files are *dual*: the same picture is also drawn with plain EMF records, which
|
||||
//! [`crate::emf`] already plays, so this reader ignores their EMF+ part. An *EMF+ only* file has
|
||||
//! nothing else to draw, so it used to show as an empty picture; for those this reader plays the
|
||||
//! vector records: solid brushes and pens, paths, rectangles, polygons, lines, ellipses and
|
||||
//! Béziers, under the world and page transforms, save/restore and containers. Images, text,
|
||||
//! gradient brushes, clipping and arcs are not drawn yet.
|
||||
|
||||
use crate::bytes::Bytes;
|
||||
use crate::canvas::{BS_NULL, BS_SOLID, Brush, Canvas, PS_NULL, Pen, Xform, mul};
|
||||
use crate::geom::{Op, P};
|
||||
|
||||
/// "EMF+" as the comment identifier of an EMR_COMMENT holding EMF+ records.
|
||||
pub(crate) const EMF_PLUS: u32 = 0x2B46_4D45;
|
||||
const IDENTITY: Xform = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
|
||||
/// GDI+ allows 64 objects; ids are a byte.
|
||||
const MAX_OBJECTS: usize = 64;
|
||||
const MAX_STACK: usize = 64;
|
||||
/// Points per path or point list read, as the plain EMF reader caps paths.
|
||||
const MAX_POINTS: usize = 100_000;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Obj {
|
||||
Brush([u8; 4]),
|
||||
Pen { color: [u8; 4], width: f64 },
|
||||
Path(Vec<Op>),
|
||||
}
|
||||
|
||||
pub(crate) struct Plus {
|
||||
/// An EMF+ header said the file is EMF+ only, so these records are the picture.
|
||||
active: bool,
|
||||
dpi: f64,
|
||||
world: Xform,
|
||||
page: f64,
|
||||
objects: Vec<Option<Obj>>,
|
||||
stack: Vec<(u32, Xform, f64)>,
|
||||
}
|
||||
|
||||
impl Plus {
|
||||
pub fn new() -> Self {
|
||||
Plus { active: false, dpi: 96.0, world: IDENTITY, page: 1.0, objects: vec![None; MAX_OBJECTS], stack: Vec::new() }
|
||||
}
|
||||
|
||||
/// Plays the EMF+ records of one EMR_COMMENT payload (after the identifier).
|
||||
pub fn comment(&mut self, cv: &mut Canvas, data: Bytes) {
|
||||
let mut pos = 0usize;
|
||||
while !cv.full() {
|
||||
let (Some(typ), Some(flags), Some(size), Some(dsize)) = (data.u16(pos), data.u16(pos + 2), data.u32(pos + 4), data.u32(pos + 8)) else {
|
||||
break;
|
||||
};
|
||||
let (size, dsize) = (size as usize, dsize as usize);
|
||||
if size < 12 || dsize > size - 12 {
|
||||
break;
|
||||
}
|
||||
let Some(rec) = data.slice(pos + 12, dsize) else { break };
|
||||
let _ = self.record(cv, typ, flags, Bytes(rec));
|
||||
let Some(next) = pos.checked_add(size) else { break };
|
||||
pos = next;
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&mut self, cv: &mut Canvas, typ: u16, flags: u16, r: Bytes) -> Option<()> {
|
||||
if typ == 0x4001 {
|
||||
// Header: dual files also draw with EMF records, which win.
|
||||
self.active = flags & 1 == 0;
|
||||
if let Some(d) = r.u32(8).map(f64::from).filter(|d| *d > 0.0 && *d < 100_000.0) {
|
||||
self.dpi = d;
|
||||
}
|
||||
return Some(());
|
||||
}
|
||||
if !self.active {
|
||||
return None;
|
||||
}
|
||||
let compressed = flags & 0x4000 != 0;
|
||||
let id = usize::from(flags & 0xFF);
|
||||
match typ {
|
||||
0x4008 => self.object(flags, r),
|
||||
0x400A => self.fill_rects(cv, flags, r),
|
||||
0x400B => self.with_pen(id, |p| p.draw_rects(cv, compressed, r)),
|
||||
0x400C => self.fill_polygon(cv, flags, r),
|
||||
0x400D => self.with_pen(id, |p| {
|
||||
let pts = points(r, 4, r.u32(0)? as usize, compressed)?;
|
||||
p.shape(cv, poly_ops(&pts, flags & 0x2000 != 0), false, true);
|
||||
Some(())
|
||||
}),
|
||||
0x400E => self.fill_ellipse(cv, flags, r),
|
||||
0x400F => self.with_pen(id, |p| {
|
||||
let rect = rects(r, 0, 1, compressed)?;
|
||||
p.shape(cv, ellipse_ops(*rect.first()?), false, true);
|
||||
Some(())
|
||||
}),
|
||||
0x4014 => self.fill_path(cv, flags, r),
|
||||
0x4015 => {
|
||||
let path = self.path(id);
|
||||
r.u32(0).and_then(|pen| {
|
||||
self.with_pen(pen as usize, |p| {
|
||||
p.shape(cv, path?, false, true);
|
||||
Some(())
|
||||
})
|
||||
})
|
||||
}
|
||||
0x4019 => self.with_pen(id, |p| {
|
||||
let pts = points(r, 4, r.u32(0)? as usize, compressed)?;
|
||||
let (first, rest) = pts.split_first()?;
|
||||
let mut ops = vec![Op::M(*first)];
|
||||
ops.extend(rest.as_chunks::<3>().0.iter().map(|c| Op::C(c[0], c[1], c[2])));
|
||||
p.shape(cv, ops, false, true);
|
||||
Some(())
|
||||
}),
|
||||
0x4025 | 0x4028 => r.u32(0).map(|i| self.push(i)),
|
||||
0x4027 => r.u32(24).map(|i| self.push(i)),
|
||||
0x4026 | 0x4029 => r.u32(0).map(|i| self.pop(i)),
|
||||
0x402A => matrix(r, 0).map(|m| self.world = m),
|
||||
0x402B => {
|
||||
self.world = IDENTITY;
|
||||
Some(())
|
||||
}
|
||||
0x402C => matrix(r, 0).and_then(|m| self.apply(m, flags)),
|
||||
0x402D => self.apply([1.0, 0.0, 0.0, 1.0, f64::from(r.f32(0)?), f64::from(r.f32(4)?)], flags),
|
||||
0x402E => self.apply([f64::from(r.f32(0)?), 0.0, 0.0, f64::from(r.f32(4)?), 0.0, 0.0], flags),
|
||||
0x402F => {
|
||||
let a = f64::from(r.f32(0)?).to_radians();
|
||||
self.apply([a.cos(), a.sin(), -a.sin(), a.cos(), 0.0, 0.0], flags)
|
||||
}
|
||||
0x4030 => {
|
||||
let scale = f64::from(r.f32(0)?);
|
||||
let unit = match flags & 0xFF {
|
||||
3 => self.dpi / 72.0,
|
||||
4 => self.dpi,
|
||||
5 => self.dpi / 300.0,
|
||||
6 => self.dpi / 25.4,
|
||||
_ => 1.0,
|
||||
};
|
||||
let page = scale * unit;
|
||||
if page.is_finite() && page > 0.0 && page < 1.0e6 {
|
||||
self.page = page;
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Without the `A` flag a transform is applied before the world transform (prepended).
|
||||
fn apply(&mut self, m: Xform, flags: u16) -> Option<()> {
|
||||
self.world = if flags & 0x2000 != 0 { mul(self.world, m) } else { mul(m, self.world) };
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn push(&mut self, id: u32) {
|
||||
if self.stack.len() < MAX_STACK {
|
||||
self.stack.push((id, self.world, self.page));
|
||||
}
|
||||
}
|
||||
|
||||
fn pop(&mut self, id: u32) {
|
||||
if let Some(i) = self.stack.iter().rposition(|s| s.0 == id) {
|
||||
let (_, w, p) = self.stack[i];
|
||||
self.stack.truncate(i);
|
||||
self.world = w;
|
||||
self.page = p;
|
||||
}
|
||||
}
|
||||
|
||||
fn object(&mut self, flags: u16, r: Bytes) -> Option<()> {
|
||||
if flags & 0x8000 != 0 {
|
||||
// Continued (split) objects are images and large paths; not read yet.
|
||||
return None;
|
||||
}
|
||||
let id = usize::from(flags & 0xFF);
|
||||
let obj = match (flags >> 8) & 0x7F {
|
||||
1 => brush(r, 0).map(|(c, _)| Obj::Brush(c)),
|
||||
2 => pen(r),
|
||||
3 => path(r).map(Obj::Path),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(slot) = self.objects.get_mut(id) {
|
||||
*slot = obj;
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn path(&self, id: usize) -> Option<Vec<Op>> {
|
||||
match self.objects.get(id)? {
|
||||
Some(Obj::Path(ops)) => Some(ops.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The brush colour of a fill record: an ARGB value with the `S` flag, else a brush object.
|
||||
fn brush_of(&self, flags: u16, v: u32) -> Option<[u8; 4]> {
|
||||
if flags & 0x8000 != 0 {
|
||||
return Some(argb(v));
|
||||
}
|
||||
match self.objects.get(v as usize)? {
|
||||
Some(Obj::Brush(c)) => Some(*c),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_pen<T>(&mut self, id: usize, f: impl FnOnce(&mut Painter) -> Option<T>) -> Option<T> {
|
||||
let Some(Some(Obj::Pen { color, width })) = self.objects.get(id).cloned() else { return None };
|
||||
let mut p = Painter { xf: mul(self.world, [self.page, 0.0, 0.0, self.page, 0.0, 0.0]), pen: Some((color, width)), fill: None };
|
||||
f(&mut p)
|
||||
}
|
||||
|
||||
fn filler(&self, color: [u8; 4]) -> Painter {
|
||||
Painter { xf: mul(self.world, [self.page, 0.0, 0.0, self.page, 0.0, 0.0]), pen: None, fill: Some(color) }
|
||||
}
|
||||
|
||||
fn fill_rects(&mut self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> {
|
||||
let c = self.brush_of(flags, r.u32(0)?)?;
|
||||
let rs = rects(r, 8, r.u32(4)? as usize, flags & 0x4000 != 0)?;
|
||||
let p = self.filler(c);
|
||||
for rc in rs {
|
||||
p.shape(cv, rect_ops(rc), true, false);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn fill_polygon(&mut self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> {
|
||||
let c = self.brush_of(flags, r.u32(0)?)?;
|
||||
let pts = points(r, 8, r.u32(4)? as usize, flags & 0x4000 != 0)?;
|
||||
self.filler(c).shape(cv, poly_ops(&pts, true), true, false);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn fill_ellipse(&mut self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> {
|
||||
let c = self.brush_of(flags, r.u32(0)?)?;
|
||||
let rect = *rects(r, 4, 1, flags & 0x4000 != 0)?.first()?;
|
||||
self.filler(c).shape(cv, ellipse_ops(rect), true, false);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn fill_path(&mut self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> {
|
||||
let c = self.brush_of(flags, r.u32(0)?)?;
|
||||
let ops = self.path(usize::from(flags & 0xFF))?;
|
||||
self.filler(c).shape(cv, ops, true, false);
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws through the canvas with EMF+'s transform and its own pen or brush, then puts the
|
||||
/// canvas's GDI state back.
|
||||
struct Painter {
|
||||
xf: Xform,
|
||||
pen: Option<([u8; 4], f64)>,
|
||||
fill: Option<[u8; 4]>,
|
||||
}
|
||||
|
||||
impl Painter {
|
||||
fn shape(&self, cv: &mut Canvas, ops: Vec<Op>, fill: bool, stroke: bool) {
|
||||
let world = cv.world();
|
||||
cv.save_dc();
|
||||
cv.modify_world(mul(self.xf, world), 4);
|
||||
let (pc, pw) = self.pen.unwrap_or(([0, 0, 0, 0], 0.0));
|
||||
cv.set_pen(Pen { style: if self.pen.is_some() { 0 } else { PS_NULL }, width: pw, color: pc });
|
||||
cv.set_brush(match self.fill {
|
||||
Some(c) => Brush::new(BS_SOLID, c),
|
||||
None => Brush::new(BS_NULL, [0, 0, 0, 0]),
|
||||
});
|
||||
cv.shape(&ops, fill, stroke);
|
||||
cv.restore_dc(-1);
|
||||
}
|
||||
|
||||
fn draw_rects(&self, cv: &mut Canvas, compressed: bool, r: Bytes) -> Option<()> {
|
||||
for rc in rects(r, 4, r.u32(0)? as usize, compressed)? {
|
||||
self.shape(cv, rect_ops(rc), false, true);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
/// ARGB (0xAARRGGBB) to RGBA.
|
||||
fn argb(v: u32) -> [u8; 4] {
|
||||
[(v >> 16) as u8, (v >> 8) as u8, v as u8, (v >> 24) as u8]
|
||||
}
|
||||
|
||||
fn matrix(r: Bytes, off: usize) -> Option<Xform> {
|
||||
let mut m = [0.0; 6];
|
||||
for (i, v) in m.iter_mut().enumerate() {
|
||||
*v = f64::from(r.f32(off + i * 4)?);
|
||||
}
|
||||
m.iter().all(|v| v.is_finite()).then_some(m)
|
||||
}
|
||||
|
||||
/// A brush object at `off`: its colour and byte length. Only solid brushes are read; other kinds
|
||||
/// (hatch, texture, gradients) are given their first colour so shapes still show.
|
||||
fn brush(r: Bytes, off: usize) -> Option<([u8; 4], usize)> {
|
||||
let kind = r.u32(off + 4)?;
|
||||
let c = argb(r.u32(off + 8)?);
|
||||
let c = if kind == 1 { argb(r.u32(off + 12)?) } else { c };
|
||||
Some((c, 12))
|
||||
}
|
||||
|
||||
/// A pen object: version, type, then pen data (flags, unit, width, optional fields), then a brush.
|
||||
fn pen(r: Bytes) -> Option<Obj> {
|
||||
let pflags = r.u32(8)?;
|
||||
let width = f64::from(r.f32(16)?);
|
||||
let mut off = 20usize;
|
||||
for (bit, len) in [(0x1, 24), (0x2, 4), (0x4, 4), (0x8, 4), (0x10, 4), (0x20, 4), (0x40, 4), (0x80, 4)] {
|
||||
if pflags & bit != 0 {
|
||||
off += len;
|
||||
}
|
||||
}
|
||||
for bit in [0x100, 0x200, 0x400] {
|
||||
if pflags & bit != 0 {
|
||||
off += if bit == 0x200 { 4 } else { 4 + 4 * (r.u32(off)? as usize).min(MAX_POINTS) };
|
||||
}
|
||||
}
|
||||
for bit in [0x800, 0x1000] {
|
||||
if pflags & bit != 0 {
|
||||
off += 4 + (r.u32(off)? as usize).min(1 << 20);
|
||||
}
|
||||
}
|
||||
let (color, _) = brush(r, off)?;
|
||||
Some(Obj::Pen { color, width: if width.is_finite() { width.clamp(0.0, 1.0e5) } else { 0.0 } })
|
||||
}
|
||||
|
||||
/// A path object: points, then one type byte per point (start, line, Bézier; 0x80 closes).
|
||||
fn path(r: Bytes) -> Option<Vec<Op>> {
|
||||
let n = (r.u32(4)? as usize).min(MAX_POINTS);
|
||||
let pflags = r.u32(8)?;
|
||||
if pflags & 0x1800 != 0 {
|
||||
// Relative (0x0800) and run-length encoded (0x1000) points: not read yet.
|
||||
return None;
|
||||
}
|
||||
let compressed = pflags & 0x4000 != 0;
|
||||
let pts = points(r, 12, n, compressed)?;
|
||||
let toff = 12 + n * if compressed { 4 } else { 8 };
|
||||
let types = r.slice(toff, n)?;
|
||||
let mut ops = Vec::with_capacity(n + 4);
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
let (t, p) = (types[i], pts[i]);
|
||||
match t & 0x07 {
|
||||
0 => ops.push(Op::M(p)),
|
||||
3 if i + 2 < n => {
|
||||
ops.push(Op::C(p, pts[i + 1], pts[i + 2]));
|
||||
let last = types[i + 2];
|
||||
i += 2;
|
||||
if last & 0x80 != 0 {
|
||||
ops.push(Op::Z);
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
_ => ops.push(Op::L(p)),
|
||||
}
|
||||
if t & 0x80 != 0 {
|
||||
ops.push(Op::Z);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Some(ops)
|
||||
}
|
||||
|
||||
/// `n` points at `off`: 16-bit integers when compressed, else 32-bit floats.
|
||||
fn points(r: Bytes, off: usize, n: usize, compressed: bool) -> Option<Vec<P>> {
|
||||
let n = n.min(MAX_POINTS);
|
||||
let mut v = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
v.push(if compressed {
|
||||
(f64::from(r.i16(off + i * 4)?), f64::from(r.i16(off + i * 4 + 2)?))
|
||||
} else {
|
||||
let (x, y) = (f64::from(r.f32(off + i * 8)?), f64::from(r.f32(off + i * 8 + 4)?));
|
||||
if !(x.is_finite() && y.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
(x, y)
|
||||
});
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// `n` rectangles (x, y, w, h) at `off`, as `[l, t, r, b]`.
|
||||
fn rects(r: Bytes, off: usize, n: usize, compressed: bool) -> Option<Vec<[f64; 4]>> {
|
||||
let pts = points(r, off, n.min(MAX_POINTS / 2).checked_mul(2)?, compressed)?;
|
||||
Some(pts.as_chunks::<2>().0.iter().map(|c| [c[0].0, c[0].1, c[0].0 + c[1].0, c[0].1 + c[1].1]).collect())
|
||||
}
|
||||
|
||||
fn rect_ops([l, t, r, b]: [f64; 4]) -> Vec<Op> {
|
||||
crate::geom::rect(l, t, r, b)
|
||||
}
|
||||
|
||||
fn poly_ops(pts: &[P], close: bool) -> Vec<Op> {
|
||||
let mut ops: Vec<Op> = pts.iter().enumerate().map(|(i, p)| if i == 0 { Op::M(*p) } else { Op::L(*p) }).collect();
|
||||
if close {
|
||||
ops.push(Op::Z);
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
/// An ellipse in its bounding box, as four cubic arcs.
|
||||
fn ellipse_ops([l, t, r, b]: [f64; 4]) -> Vec<Op> {
|
||||
let (cx, cy, rx, ry) = ((l + r) / 2.0, (t + b) / 2.0, (r - l) / 2.0, (b - t) / 2.0);
|
||||
let k = 0.552_284_75;
|
||||
let (ox, oy) = (rx * k, ry * k);
|
||||
vec![
|
||||
Op::M((cx + rx, cy)),
|
||||
Op::C((cx + rx, cy + oy), (cx + ox, cy + ry), (cx, cy + ry)),
|
||||
Op::C((cx - ox, cy + ry), (cx - rx, cy + oy), (cx - rx, cy)),
|
||||
Op::C((cx - rx, cy - oy), (cx - ox, cy - ry), (cx, cy - ry)),
|
||||
Op::C((cx + ox, cy - ry), (cx + rx, cy - oy), (cx + rx, cy)),
|
||||
Op::Z,
|
||||
]
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod bytes;
|
||||
mod canvas;
|
||||
mod dib;
|
||||
mod emf;
|
||||
mod emfplus;
|
||||
mod geom;
|
||||
mod place;
|
||||
mod text;
|
||||
|
||||
@@ -1518,3 +1518,109 @@ fn pattern_brush_pixels_count_against_the_byte_budget() {
|
||||
let p = wmf200(&recs);
|
||||
assert!(p.items.is_empty(), "the parse stopped at the budget: {:?}", p.items.len());
|
||||
}
|
||||
|
||||
// ---- EMF+ ----
|
||||
|
||||
fn f32s(v: &[f32]) -> Vec<u8> {
|
||||
v.iter().flat_map(|x| x.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
/// One EMF+ record: type, flags, size, data size, data (padded to 4).
|
||||
fn prec(typ: u16, flags: u16, data: &[u8]) -> Vec<u8> {
|
||||
let mut d = data.to_vec();
|
||||
while !d.len().is_multiple_of(4) {
|
||||
d.push(0);
|
||||
}
|
||||
let mut v = typ.to_le_bytes().to_vec();
|
||||
v.extend(flags.to_le_bytes());
|
||||
v.extend(((12 + d.len()) as u32).to_le_bytes());
|
||||
v.extend((d.len() as u32).to_le_bytes());
|
||||
v.extend(d);
|
||||
v
|
||||
}
|
||||
|
||||
/// An EMR_COMMENT carrying EMF+ records.
|
||||
fn plus_comment(recs: &[Vec<u8>]) -> Vec<u8> {
|
||||
let body = recs.concat();
|
||||
let mut p = ((body.len() + 4) as u32).to_le_bytes().to_vec();
|
||||
p.extend(0x2B46_4D45u32.to_le_bytes());
|
||||
p.extend(body);
|
||||
erec(70, &p)
|
||||
}
|
||||
|
||||
/// EMF+ header: dual (bit 0) or EMF+ only, 96 dpi.
|
||||
fn plus_header(dual: bool) -> Vec<u8> {
|
||||
prec(0x4001, u16::from(dual), &cat(vec![h32(0xDBC0_1002), h32(0), h32(96), h32(96)]))
|
||||
}
|
||||
|
||||
fn solid_pen(argb: u32, width: f32) -> Vec<u8> {
|
||||
cat(vec![h32(0xDBC0_1002), h32(0), h32(0), h32(0), f32s(&[width]), h32(0xDBC0_1002), h32(0), h32(argb)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emfplus_only_fill_rects_with_argb_colour() {
|
||||
let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFF00_80FF), h32(1), f32s(&[10.0, 20.0, 30.0, 40.0])]));
|
||||
let p = parse(&emf(&[plus_comment(&[plus_header(false), fill])], [0; 4], [0; 4])).unwrap();
|
||||
assert_eq!(p.items.len(), 1);
|
||||
let (segs, fill, stroke, _) = path_segs(&p.items[0]);
|
||||
assert_eq!(fill, Some([0x00, 0x80, 0xFF, 0xFF]));
|
||||
assert_eq!(stroke, None);
|
||||
// Geometry is placed relative to the drawing's bounding box (no frame given).
|
||||
assert_eq!((p.width, p.height), (30.0, 40.0));
|
||||
assert_eq!(segs[0], Seg::Move(0.0, 0.0));
|
||||
assert!(segs.contains(&Seg::Line(30.0, 40.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emfplus_dual_files_draw_only_the_emf_records() {
|
||||
let fill = prec(0x400A, 0x8000, &cat(vec![h32(0xFFFF_0000), h32(1), f32s(&[0.0, 0.0, 5.0, 5.0])]));
|
||||
let gdi = erec(43, &cat(vec![e32(0), e32(0), e32(9), e32(9)]));
|
||||
let p = parse(&emf(&[plus_comment(&[plus_header(true), fill]), gdi], [0; 4], [0; 4])).unwrap();
|
||||
assert_eq!(p.items.len(), 1);
|
||||
assert_eq!(path_segs(&p.items[0]).1, Some([255, 255, 255, 255]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emfplus_pen_object_draws_lines_under_a_scale_transform() {
|
||||
let pen = prec(0x4008, 0x0200 | 3, &solid_pen(0xFF00_FF00, 2.0));
|
||||
let scale = prec(0x402E, 0, &f32s(&[2.0, 3.0]));
|
||||
let lines = prec(0x400D, 3, &cat(vec![h32(2), f32s(&[1.0, 1.0, 4.0, 2.0])]));
|
||||
let p = parse(&emf(&[plus_comment(&[plus_header(false), pen, scale, lines])], [0; 4], [0; 4])).unwrap();
|
||||
let (segs, fill, stroke, _) = path_segs(&p.items[0]);
|
||||
// (1,1)-(4,2) scaled by (2,3) is (2,3)-(8,6), placed relative to its bounding box.
|
||||
assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(6.0, 3.0)]);
|
||||
assert_eq!(fill, None);
|
||||
let (c, w) = stroke.unwrap();
|
||||
assert_eq!(c, [0, 255, 0, 255]);
|
||||
assert!(w > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emfplus_path_object_fills_with_brush_object_and_restore_undoes_transform() {
|
||||
let brush = prec(0x4008, 0x0100 | 1, &cat(vec![h32(0xDBC0_1002), h32(0), h32(0x8012_3456)]));
|
||||
// Triangle: start, line, line|close.
|
||||
let path = prec(0x4008, 0x0300 | 2, &cat(vec![h32(0xDBC0_1002), h32(3), h32(0), f32s(&[0.0, 0.0, 10.0, 0.0, 0.0, 10.0]), vec![0, 1, 0x81]]));
|
||||
let save = prec(0x4025, 0, &h32(7));
|
||||
let mv = prec(0x402D, 0, &f32s(&[100.0, 0.0]));
|
||||
let restore = prec(0x4026, 0, &h32(7));
|
||||
let fill = prec(0x4014, 2, &h32(1));
|
||||
let p = parse(&emf(&[plus_comment(&[plus_header(false), brush, path, save, mv, restore, fill])], [0; 4], [0; 4])).unwrap();
|
||||
let (segs, fill, _, _) = path_segs(&p.items[0]);
|
||||
assert_eq!(fill, Some([0x12, 0x34, 0x56, 0x80]));
|
||||
assert_eq!(segs, &vec![Seg::Move(0.0, 0.0), Seg::Line(10.0, 0.0), Seg::Line(0.0, 10.0), Seg::Close]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emfplus_garbage_records_never_panic() {
|
||||
let mut s = 0x1234_5678u32;
|
||||
for _ in 0..300 {
|
||||
let mut recs = vec![plus_header(false)];
|
||||
for _ in 0..6 {
|
||||
s = s.wrapping_mul(1_103_515_245).wrapping_add(12_345);
|
||||
let typ = 0x4000 + (s >> 16) as u16 % 0x40;
|
||||
let data: Vec<u8> = (0..(s % 64)).map(|i| (s.rotate_left(i)) as u8).collect();
|
||||
recs.push(prec(typ, (s >> 8) as u16, &data));
|
||||
}
|
||||
let _ = parse(&emf(&[plus_comment(&recs)], [0; 4], [0; 4]));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user