mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-28 04:27:06 -06:00
1440 lines
54 KiB
Rust
1440 lines
54 KiB
Rust
//! 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; for those this reader plays the records:
|
|
//! - brushes (solid, hatch, texture, linear and path gradients), pens, paths, images, fonts,
|
|
//! string formats and regions, including objects split across several records;
|
|
//! - rectangles, polygons, lines, ellipses, paths and Béziers;
|
|
//! - bitmaps (raw pixel formats, and PNG/JPEG/GIF/TIFF/BMP payloads) and strings (DrawString,
|
|
//! DrawDriverString with character codes);
|
|
//! - world and page transforms, save/restore and containers;
|
|
//! - arcs and pies;
|
|
//! - clipping to rectangles and convex clip paths and regions (others: their bounding rectangle);
|
|
//! - nested metafile images, and glyph-index driver strings in the Windows core fonts;
|
|
//! - cardinal splines (DrawCurve, DrawClosedCurve, FillClosedCurve), FillRegion, and Clear
|
|
//! (over the picture frame).
|
|
//!
|
|
//! Every drawing record of MS-EMFPLUS §2.3.4 is played. Not drawn: image attributes, glyph
|
|
//! indices of other fonts, and region combinations other than intersect and union (XOR, exclude
|
|
//! and complement keep the left area). Deliberately ignored: rendering hints (antialias, text
|
|
//! hint, interpolation, pixel offset, compositing; the renderer has its own), SetRenderingOrigin
|
|
//! (hatch phase), SerializableObject image effects, the terminal-server records SetTSGraphics and
|
|
//! SetTSClip, and StrokeFillPath (0x4037), which the specification lists but gives no layout for
|
|
//! and GDI+ doesn't write.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::bytes::Bytes;
|
|
use crate::canvas::Raw;
|
|
use crate::canvas::{BS_NULL, BS_SOLID, Brush, Canvas, LogFont, PS_NULL, Pen, RawTile, Xform, hatch_tile, mul};
|
|
use crate::cliprect::{clip_polygon, convex, flatten, ops_bbox, pts_bbox, rect_poly};
|
|
use crate::dib::Rgba;
|
|
use crate::geom::{Op, P};
|
|
use crate::text::{ETO_CLIPPED, Font};
|
|
use crate::{Item, Picture, Seg};
|
|
|
|
/// "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;
|
|
/// Gradient stops read from a brush.
|
|
const MAX_STOPS: usize = 256;
|
|
/// Characters per string.
|
|
const MAX_CHARS: usize = 32_768;
|
|
/// Bytes gathered for one object split across records.
|
|
const MAX_OBJECT_BYTES: usize = 64 * 1024 * 1024;
|
|
/// Nesting depth of region nodes.
|
|
const MAX_REGION_DEPTH: usize = 32;
|
|
|
|
const TA_RIGHT: u32 = 2;
|
|
const TA_CENTER: u32 = 6;
|
|
const TA_BOTTOM: u32 = 8;
|
|
const TA_BASELINE: u32 = 24;
|
|
|
|
/// What a brush paints. Coordinates are in brush space, which `xf` maps to world space.
|
|
#[derive(Clone)]
|
|
enum Fill {
|
|
Solid([u8; 4]),
|
|
Hatch { style: u32, fg: [u8; 4], bg: [u8; 4] },
|
|
Linear { rect: [f64; 4], stops: Vec<(f64, [u8; 4])>, wrap: u32, xf: Xform },
|
|
Radial { center: P, radius: P, stops: Vec<(f64, [u8; 4])>, xf: Xform },
|
|
Texture { img: Arc<Rgba>, wrap: u32, xf: Xform },
|
|
}
|
|
|
|
impl Fill {
|
|
/// One colour standing for the brush (pens and text draw solid).
|
|
fn color(&self) -> [u8; 4] {
|
|
match self {
|
|
Fill::Solid(c) | Fill::Hatch { fg: c, .. } => *c,
|
|
Fill::Linear { stops, .. } | Fill::Radial { stops, .. } => ramp(stops, 0.5),
|
|
Fill::Texture { img, .. } => img.px.get(..4).and_then(|c| c.try_into().ok()).unwrap_or([0, 0, 0, 255]),
|
|
}
|
|
}
|
|
|
|
/// The colour at a brush-space point.
|
|
fn sample(&self, p: P) -> [u8; 4] {
|
|
match self {
|
|
Fill::Solid(c) | Fill::Hatch { fg: c, .. } => *c,
|
|
Fill::Linear { rect, stops, wrap, .. } => {
|
|
let t = if rect[2] == 0.0 { 0.0 } else { (p.0 - rect[0]) / rect[2] };
|
|
ramp(stops, wrap_t(t, *wrap))
|
|
}
|
|
Fill::Radial { center, radius, stops, .. } => {
|
|
let (dx, dy) = ((p.0 - center.0) / radius.0.max(1e-9), (p.1 - center.1) / radius.1.max(1e-9));
|
|
ramp(stops, dx.hypot(dy).min(1.0))
|
|
}
|
|
Fill::Texture { img, wrap, .. } => {
|
|
let (w, h) = (img.w as f64, img.h as f64);
|
|
if w == 0.0 || h == 0.0 {
|
|
return [0, 0, 0, 0];
|
|
}
|
|
if *wrap == 4 && !(0.0..w).contains(&p.0) || *wrap == 4 && !(0.0..h).contains(&p.1) {
|
|
return [0, 0, 0, 0];
|
|
}
|
|
let x = (p.0.rem_euclid(w) as usize).min(img.w - 1);
|
|
let y = (p.1.rem_euclid(h) as usize).min(img.h - 1);
|
|
let i = (y * img.w + x) * 4;
|
|
img.px.get(i..i + 4).and_then(|c| c.try_into().ok()).unwrap_or([0, 0, 0, 0])
|
|
}
|
|
}
|
|
}
|
|
|
|
fn xf(&self) -> Xform {
|
|
match self {
|
|
Fill::Linear { xf, .. } | Fill::Radial { xf, .. } | Fill::Texture { xf, .. } => *xf,
|
|
_ => IDENTITY,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Where `t` lands under a wrap mode: tiled, mirrored (flip modes) or clamped.
|
|
fn wrap_t(t: f64, wrap: u32) -> f64 {
|
|
if !t.is_finite() {
|
|
return 0.0;
|
|
}
|
|
match wrap {
|
|
1 | 3 => {
|
|
let m = t.rem_euclid(2.0);
|
|
if m > 1.0 { 2.0 - m } else { m }
|
|
}
|
|
4 => t.clamp(0.0, 1.0),
|
|
_ => t.rem_euclid(1.0),
|
|
}
|
|
}
|
|
|
|
/// The colour at `t` along sorted stops.
|
|
fn ramp(stops: &[(f64, [u8; 4])], t: f64) -> [u8; 4] {
|
|
let (Some(first), Some(last)) = (stops.first(), stops.last()) else { return [0, 0, 0, 0] };
|
|
if t <= first.0 {
|
|
return first.1;
|
|
}
|
|
for w in stops.windows(2) {
|
|
let ((a, ca), (b, cb)) = (w[0], w[1]);
|
|
if t <= b {
|
|
let k = if b > a { (t - a) / (b - a) } else { 0.0 };
|
|
return lerp(ca, cb, k);
|
|
}
|
|
}
|
|
last.1
|
|
}
|
|
|
|
fn lerp(a: [u8; 4], b: [u8; 4], k: f64) -> [u8; 4] {
|
|
let mut c = [0u8; 4];
|
|
for i in 0..4 {
|
|
c[i] = (f64::from(a[i]) + (f64::from(b[i]) - f64::from(a[i])) * k).round().clamp(0.0, 255.0) as u8;
|
|
}
|
|
c
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct FontObj {
|
|
em: f64,
|
|
unit: u32,
|
|
style: u32,
|
|
family: String,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct Format {
|
|
flags: u32,
|
|
align: u32,
|
|
line_align: u32,
|
|
}
|
|
|
|
/// A clip area: a convex polygon (empty clips everything), or None for infinite.
|
|
type Area = Option<Vec<P>>;
|
|
|
|
enum ImageData {
|
|
Bitmap(Rgba),
|
|
/// A nested WMF or EMF (EMF+) picture.
|
|
Meta(Picture),
|
|
}
|
|
|
|
thread_local! {
|
|
/// Nesting depth of metafile images being parsed, so a file of nested metafiles cannot run
|
|
/// the stack out.
|
|
static NESTING: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
}
|
|
/// Metafile images inside metafile images, at most.
|
|
const MAX_NESTING: usize = 3;
|
|
|
|
#[derive(Clone)]
|
|
enum Obj {
|
|
Brush(Fill),
|
|
Pen {
|
|
color: [u8; 4],
|
|
width: f64,
|
|
},
|
|
Path(Vec<Op>),
|
|
Image(Arc<ImageData>),
|
|
Font(FontObj),
|
|
Format(Format),
|
|
/// A region's bounding rectangle `[l, t, r, b]` in world units; None is infinite.
|
|
Region(Area),
|
|
}
|
|
|
|
/// An object arriving in several records: its id, type, total size and the bytes so far.
|
|
struct Partial {
|
|
id: usize,
|
|
kind: u16,
|
|
total: usize,
|
|
data: Vec<u8>,
|
|
}
|
|
|
|
struct Saved {
|
|
id: u32,
|
|
world: Xform,
|
|
page: f64,
|
|
clip: Area,
|
|
}
|
|
|
|
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,
|
|
/// Convex device clip polygon; None is unclipped.
|
|
clip: Area,
|
|
objects: Vec<Option<Obj>>,
|
|
stack: Vec<Saved>,
|
|
partial: Option<Partial>,
|
|
/// The picture frame in device units (`[x, y, w, h]`), what Clear paints.
|
|
pub frame: Option<[f64; 4]>,
|
|
}
|
|
|
|
impl Plus {
|
|
pub fn new() -> Self {
|
|
Plus {
|
|
active: false,
|
|
dpi: 96.0,
|
|
world: IDENTITY,
|
|
page: 1.0,
|
|
clip: None,
|
|
objects: vec![None; MAX_OBJECTS],
|
|
stack: Vec::new(),
|
|
partial: None,
|
|
frame: None,
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
}
|
|
|
|
/// World units to device pixels before the canvas's own mapping.
|
|
fn xf(&self) -> Xform {
|
|
mul(self.world, [self.page, 0.0, 0.0, self.page, 0.0, 0.0])
|
|
}
|
|
|
|
fn painter(&self) -> Painter {
|
|
Painter { xf: self.xf(), clip: self.clip.clone() }
|
|
}
|
|
|
|
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 => {
|
|
let fill = self.brush_of(flags, r.u32(0)?)?;
|
|
let p = self.painter();
|
|
for rc in rects(r, 8, r.u32(4)? as usize, compressed)? {
|
|
p.fill(cv, &rect_ops(rc), &fill);
|
|
}
|
|
Some(())
|
|
}
|
|
0x400B => {
|
|
let pen = self.pen(id)?;
|
|
let p = self.painter();
|
|
for rc in rects(r, 4, r.u32(0)? as usize, compressed)? {
|
|
p.stroke(cv, &rect_ops(rc), pen);
|
|
}
|
|
Some(())
|
|
}
|
|
0x400C => {
|
|
let fill = self.brush_of(flags, r.u32(0)?)?;
|
|
let pts = points(r, 8, r.u32(4)? as usize, compressed)?;
|
|
self.painter().fill(cv, &poly_ops(&pts, true), &fill);
|
|
Some(())
|
|
}
|
|
0x400D => {
|
|
let pen = self.pen(id)?;
|
|
let pts = points(r, 4, r.u32(0)? as usize, compressed)?;
|
|
self.painter().stroke(cv, &poly_ops(&pts, flags & 0x2000 != 0), pen);
|
|
Some(())
|
|
}
|
|
0x400E => {
|
|
let fill = self.brush_of(flags, r.u32(0)?)?;
|
|
let rect = *rects(r, 4, 1, compressed)?.first()?;
|
|
self.painter().fill(cv, &ellipse_ops(rect), &fill);
|
|
Some(())
|
|
}
|
|
0x400F => {
|
|
let pen = self.pen(id)?;
|
|
let rect = *rects(r, 0, 1, compressed)?.first()?;
|
|
self.painter().stroke(cv, &ellipse_ops(rect), pen);
|
|
Some(())
|
|
}
|
|
0x4014 => {
|
|
let fill = self.brush_of(flags, r.u32(0)?)?;
|
|
let ops = self.path(id)?;
|
|
self.painter().fill(cv, &ops, &fill);
|
|
Some(())
|
|
}
|
|
0x4015 => {
|
|
let ops = self.path(id)?;
|
|
let pen = self.pen(r.u32(0)? as usize)?;
|
|
self.painter().stroke(cv, &ops, pen);
|
|
Some(())
|
|
}
|
|
0x4019 => {
|
|
let pen = self.pen(id)?;
|
|
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])));
|
|
self.painter().stroke(cv, &ops, pen);
|
|
Some(())
|
|
}
|
|
// Cardinal splines: FillClosedCurve, DrawClosedCurve, DrawCurve.
|
|
0x4016 => {
|
|
let fill = self.brush_of(flags, r.u32(0)?)?;
|
|
let tension = finite(r.f32(4)?)?;
|
|
let pts = points(r, 12, r.u32(8)? as usize, compressed)?;
|
|
self.painter().fill(cv, &curve_ops(&pts, tension, true, 0, usize::MAX), &fill);
|
|
Some(())
|
|
}
|
|
0x4017 => {
|
|
let pen = self.pen(id)?;
|
|
let tension = finite(r.f32(0)?)?;
|
|
let pts = points(r, 8, r.u32(4)? as usize, compressed)?;
|
|
self.painter().stroke(cv, &curve_ops(&pts, tension, true, 0, usize::MAX), pen);
|
|
Some(())
|
|
}
|
|
0x4018 => {
|
|
let pen = self.pen(id)?;
|
|
let tension = finite(r.f32(0)?)?;
|
|
let (offset, segments) = (r.u32(4)? as usize, r.u32(8)? as usize);
|
|
let pts = points(r, 16, r.u32(12)? as usize, compressed)?;
|
|
self.painter().stroke(cv, &curve_ops(&pts, tension, false, offset, segments), pen);
|
|
Some(())
|
|
}
|
|
// Clear: the whole picture in one colour, whatever the clip (a transparent clear
|
|
// leaves nothing to draw).
|
|
0x4009 => {
|
|
let c = argb(r.u32(0)?);
|
|
let [x, y, w, h] = self.frame?;
|
|
if c[3] == 0 || !(w > 0.0 && h > 0.0) {
|
|
return Some(());
|
|
}
|
|
let to_device = invert(mul(cv.world(), cv.device_affine()))?;
|
|
let p = Painter { xf: to_device, clip: None };
|
|
p.fill(cv, &rect_ops([x, y, x + w, y + h]), &Fill::Solid(c));
|
|
Some(())
|
|
}
|
|
// FillRegion: the region's area (as the clip reads it).
|
|
0x4013 => {
|
|
let fill = self.brush_of(flags, r.u32(0)?)?;
|
|
let Some(Some(Obj::Region(Some(area)))) = self.objects.get(id).cloned() else { return None };
|
|
if area.len() < 3 {
|
|
return None;
|
|
}
|
|
self.painter().fill(cv, &poly_ops(&area, true), &fill);
|
|
Some(())
|
|
}
|
|
0x401A => {
|
|
let img = self.image(id)?;
|
|
let dst = *rects(r, 24, 1, compressed)?.first()?;
|
|
self.draw_image(cv, &img, r, [dst[0], dst[1], dst[2] - dst[0], dst[3] - dst[1]])
|
|
}
|
|
0x401B => {
|
|
let img = self.image(id)?;
|
|
let pts = points(r, 28, 3, compressed)?;
|
|
let (p0, p1, p2) = (*pts.first()?, *pts.get(1)?, *pts.get(2)?);
|
|
// The destination parallelogram's top-left, top-right and bottom-left corners; a
|
|
// sheared or rotated one is drawn into its bounding rectangle's axes.
|
|
let dst = [p0.0, p0.1, p1.0 - p0.0 + (p2.0 - p0.0), p2.1 - p0.1 + (p1.1 - p0.1)];
|
|
self.draw_image(cv, &img, r, dst)
|
|
}
|
|
0x401C => self.draw_string(cv, flags, r),
|
|
0x4036 => self.draw_driver_string(cv, flags, r),
|
|
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 page = f64::from(r.f32(0)?) * unit_px(u32::from(flags & 0xFF), self.dpi);
|
|
if page.is_finite() && page > 0.0 && page < 1.0e6 {
|
|
self.page = page;
|
|
}
|
|
Some(())
|
|
}
|
|
0x4031 => {
|
|
self.clip = None;
|
|
Some(())
|
|
}
|
|
0x4032 => {
|
|
let [x, y, w, h] = floats::<4>(r, 0)?;
|
|
let area = map_area(Some(rect_poly([x, y, x + w, y + h])), mul(self.xf(), cv.device_affine()));
|
|
self.combine(flags, area)
|
|
}
|
|
0x4033 => {
|
|
let area = map_area(Some(area_of_ops(&self.path(id)?)), mul(self.xf(), cv.device_affine()));
|
|
self.combine(flags, area)
|
|
}
|
|
0x4034 => {
|
|
let Some(Some(Obj::Region(reg))) = self.objects.get(id).cloned() else { return None };
|
|
let area = map_area(reg, mul(self.xf(), cv.device_affine()));
|
|
self.combine(flags, area)
|
|
}
|
|
0x4035 => {
|
|
let [dx, dy] = floats::<2>(r, 0)?;
|
|
let a = mul(self.xf(), cv.device_affine());
|
|
let (ox, oy) = (dx * a[0] + dy * a[2], dx * a[1] + dy * a[3]);
|
|
if let Some(c) = self.clip.as_mut() {
|
|
for p in c.iter_mut() {
|
|
*p = (p.0 + ox, p.1 + oy);
|
|
}
|
|
}
|
|
Some(())
|
|
}
|
|
0x4010 => {
|
|
let fill = self.brush_of(flags, r.u32(0)?)?;
|
|
let [start, sweep] = floats::<2>(r, 4)?;
|
|
let rect = *rects(r, 12, 1, compressed)?.first()?;
|
|
self.painter().fill(cv, &arc_ops(rect, start, sweep, true), &fill);
|
|
Some(())
|
|
}
|
|
0x4011 | 0x4012 => {
|
|
let pen = self.pen(id)?;
|
|
let [start, sweep] = floats::<2>(r, 0)?;
|
|
let rect = *rects(r, 8, 1, compressed)?.first()?;
|
|
self.painter().stroke(cv, &arc_ops(rect, start, sweep, typ == 0x4011), pen);
|
|
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(Saved { id, world: self.world, page: self.page, clip: self.clip.clone() });
|
|
}
|
|
}
|
|
|
|
fn pop(&mut self, id: u32) {
|
|
if let Some(i) = self.stack.iter().rposition(|s| s.id == id) {
|
|
let s = self.stack.swap_remove(i);
|
|
(self.world, self.page, self.clip) = (s.world, s.page, s.clip);
|
|
self.stack.truncate(i);
|
|
}
|
|
}
|
|
|
|
/// Combines a new clip area with the current one under the record's combine mode: replace,
|
|
/// intersect and union (as the rectangle around both). XOR, exclude and complement leave the
|
|
/// clip as it was.
|
|
fn combine(&mut self, flags: u16, new: Area) -> Option<()> {
|
|
self.clip = match (flags >> 8) & 0xF {
|
|
0 => new,
|
|
1 => area_intersect(self.clip.take(), new),
|
|
2 => area_union(self.clip.take(), new),
|
|
_ => self.clip.take(),
|
|
};
|
|
Some(())
|
|
}
|
|
|
|
fn object(&mut self, flags: u16, r: Bytes) -> Option<()> {
|
|
let id = usize::from(flags & 0xFF);
|
|
let kind = (flags >> 8) & 0x7F;
|
|
if flags & 0x8000 != 0 {
|
|
// A continued object: the total size, then this record's share of the bytes.
|
|
let total = (r.u32(0)? as usize).min(MAX_OBJECT_BYTES);
|
|
let chunk = r.tail(4)?.0;
|
|
let p = match self.partial.take() {
|
|
Some(p) if p.id == id && p.kind == kind && p.total == total => p,
|
|
_ => Partial { id, kind, total, data: Vec::new() },
|
|
};
|
|
let mut p = p;
|
|
let room = p.total.saturating_sub(p.data.len());
|
|
p.data.extend_from_slice(&chunk[..chunk.len().min(room)]);
|
|
if p.data.len() >= p.total {
|
|
let obj = parse_object(p.kind, Bytes(&p.data));
|
|
self.store(p.id, obj);
|
|
} else {
|
|
self.partial = Some(p);
|
|
}
|
|
return Some(());
|
|
}
|
|
// The last piece of a continued object comes without the continue flag.
|
|
if let Some(mut p) = self.partial.take()
|
|
&& p.id == id
|
|
&& p.kind == kind
|
|
{
|
|
let room = p.total.saturating_sub(p.data.len());
|
|
p.data.extend_from_slice(&r.0[..r.0.len().min(room)]);
|
|
let obj = parse_object(kind, Bytes(&p.data));
|
|
self.store(id, obj);
|
|
return Some(());
|
|
}
|
|
let obj = parse_object(kind, r);
|
|
self.store(id, obj);
|
|
Some(())
|
|
}
|
|
|
|
fn store(&mut self, id: usize, obj: Option<Obj>) {
|
|
if let Some(slot) = self.objects.get_mut(id) {
|
|
*slot = obj;
|
|
}
|
|
}
|
|
|
|
fn path(&self, id: usize) -> Option<Vec<Op>> {
|
|
match self.objects.get(id)? {
|
|
Some(Obj::Path(ops)) => Some(ops.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn pen(&self, id: usize) -> Option<([u8; 4], f64)> {
|
|
match self.objects.get(id)? {
|
|
Some(Obj::Pen { color, width }) => Some((*color, *width)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn image(&self, id: usize) -> Option<Arc<ImageData>> {
|
|
match self.objects.get(id)? {
|
|
Some(Obj::Image(img)) => Some(img.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// The brush of a fill record: an ARGB value with the `S` flag, else a brush object.
|
|
fn brush_of(&self, flags: u16, v: u32) -> Option<Fill> {
|
|
if flags & 0x8000 != 0 {
|
|
return Some(Fill::Solid(argb(v)));
|
|
}
|
|
match self.objects.get(v as usize)? {
|
|
Some(Obj::Brush(f)) => Some(f.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// DrawImage and DrawImagePoints: the source rectangle (pixels) cut from the image, drawn
|
|
/// into world rectangle `dst` = `[x, y, w, h]`.
|
|
fn draw_image(&self, cv: &mut Canvas, img: &ImageData, r: Bytes, dst: [f64; 4]) -> Option<()> {
|
|
let img = match img {
|
|
ImageData::Bitmap(b) => b,
|
|
ImageData::Meta(pic) => {
|
|
self.painter().picture(cv, dst, pic);
|
|
return Some(());
|
|
}
|
|
};
|
|
let [sx, sy, sw, sh] = floats::<4>(r, 8)?;
|
|
let src = if r.u32(4)? == 2 { [sx, sy, sw, sh].map(|v| v.round().clamp(-1.0e9, 1.0e9) as i64) } else { [0; 4] };
|
|
let (part, dst) = crate::dib::clip_blit(img, src, dst)?;
|
|
self.painter().image(cv, dst, part);
|
|
Some(())
|
|
}
|
|
|
|
fn font(&self, id: usize) -> Option<FontObj> {
|
|
match self.objects.get(id)? {
|
|
Some(Obj::Font(f)) => Some(f.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// The font as a LOGFONT whose height is in world units.
|
|
fn log_font(&self, f: &FontObj) -> LogFont {
|
|
let em = if f.unit == 0 { f.em } else { f.em * unit_px(f.unit, self.dpi) / self.page };
|
|
let font = Font {
|
|
family: f.family.clone(),
|
|
height: -(em.clamp(0.0, 1.0e6) as f32),
|
|
weight: if f.style & 1 != 0 { 700 } else { 400 },
|
|
italic: f.style & 2 != 0,
|
|
underline: f.style & 4 != 0,
|
|
strike: f.style & 8 != 0,
|
|
..Font::default()
|
|
};
|
|
LogFont { font, escapement: 0.0 }
|
|
}
|
|
|
|
/// DrawString: lines of text laid out in a rectangle by the string format's alignments.
|
|
fn draw_string(&self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> {
|
|
let font = self.font(usize::from(flags & 0xFF))?;
|
|
let color = self.brush_of(flags, r.u32(0)?)?.color();
|
|
let fmt = match self.objects.get(r.u32(4)? as usize) {
|
|
Some(Some(Obj::Format(f))) => *f,
|
|
_ => Format::default(),
|
|
};
|
|
let len = (r.u32(8)? as usize).min(MAX_CHARS);
|
|
let [x, y, w, h] = floats::<4>(r, 12)?;
|
|
let units: Vec<u16> = (0..len).map(|i| r.u16(28 + i * 2)).collect::<Option<_>>()?;
|
|
let lf = self.log_font(&font);
|
|
let em = f64::from(-lf.font.height);
|
|
let wrap = w > 0.0 && fmt.flags & 0x1000 == 0;
|
|
let lines: Vec<Vec<u16>> = units
|
|
.split(|u| *u == 0x0A)
|
|
.flat_map(|l| {
|
|
let l: Vec<u16> = l.iter().copied().filter(|u| *u != 0x0D).collect();
|
|
if wrap { wrap_line(&l, w, em) } else { vec![l] }
|
|
})
|
|
.collect();
|
|
let line_h = em * 1.2;
|
|
let (ax, ta_h) = match fmt.align {
|
|
1 => (x + w / 2.0, TA_CENTER),
|
|
2 => (x + w, TA_RIGHT),
|
|
_ => (x, 0),
|
|
};
|
|
let block = line_h * lines.len() as f64;
|
|
let top = match fmt.line_align {
|
|
1 => y + (h - block) / 2.0,
|
|
2 => y + h - block,
|
|
_ => y,
|
|
};
|
|
let clip = (w > 0.0 && h > 0.0 && fmt.flags & 0x4000 == 0).then_some([x, y, x + w, y + h]);
|
|
let p = self.painter();
|
|
for (i, line) in lines.iter().enumerate() {
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
let (text, dx) = crate::text::decode_utf16(line, &[]);
|
|
let at = (ax, top + line_h * i as f64);
|
|
let (at, ta) = if fmt.line_align == 2 && lines.len() == 1 { ((ax, y + h), ta_h | TA_BOTTOM) } else { (at, ta_h) };
|
|
p.text(cv, at, text, dx, TextStyle { color, font: lf.clone(), align: ta, clip }, IDENTITY);
|
|
}
|
|
Some(())
|
|
}
|
|
|
|
/// DrawDriverString with character codes (CmapLookup): positioned glyphs on a baseline.
|
|
fn draw_driver_string(&self, cv: &mut Canvas, flags: u16, r: Bytes) -> Option<()> {
|
|
let font = self.font(usize::from(flags & 0xFF))?;
|
|
let color = self.brush_of(flags, r.u32(0)?)?.color();
|
|
let opts = r.u32(4)?;
|
|
let n = (r.u32(12)? as usize).min(MAX_CHARS);
|
|
let mut units: Vec<u16> = (0..n).map(|i| r.u16(16 + i * 2)).collect::<Option<_>>()?;
|
|
if opts & 1 == 0 {
|
|
// Glyph indices into the font. The crate knows no fonts, but the classic Windows core
|
|
// fonts keep the standard Macintosh glyph order for ASCII, so those map back.
|
|
units = glyphs_to_chars(&font.family, &units)?;
|
|
}
|
|
let pos = points(r, 16 + n * 2, n, false)?;
|
|
let m = if r.u32(8)? != 0 { matrix(r, 16 + n * 10)? } else { IDENTITY };
|
|
let lf = self.log_font(&font);
|
|
let first = *pos.first()?;
|
|
let dx: Vec<f32> = if opts & 4 != 0 {
|
|
Vec::new()
|
|
} else {
|
|
let mut d: Vec<f32> = pos.windows(2).map(|w| (w[1].0 - w[0].0) as f32).collect();
|
|
d.push(d.last().copied().unwrap_or(-lf.font.height * 0.5));
|
|
d
|
|
};
|
|
let (text, dx) = crate::text::decode_utf16(&units, &dx);
|
|
self.painter().text(cv, first, text, dx, TextStyle { color, font: lf, align: TA_BASELINE, clip: None }, m);
|
|
Some(())
|
|
}
|
|
}
|
|
|
|
/// Page unit to device pixels: world, display and pixel are 1; point, inch, document, mm.
|
|
fn unit_px(unit: u32, dpi: f64) -> f64 {
|
|
match unit {
|
|
3 => dpi / 72.0,
|
|
4 => dpi,
|
|
5 => dpi / 300.0,
|
|
6 => dpi / 25.4,
|
|
_ => 1.0,
|
|
}
|
|
}
|
|
|
|
struct TextStyle {
|
|
color: [u8; 4],
|
|
font: LogFont,
|
|
align: u32,
|
|
/// World clip rectangle `[l, t, r, b]`.
|
|
clip: Option<[f64; 4]>,
|
|
}
|
|
|
|
/// Draws through the canvas with EMF+'s transform, clip and its own pen or brush, then puts the
|
|
/// canvas's GDI state back.
|
|
struct Painter {
|
|
xf: Xform,
|
|
clip: Area,
|
|
}
|
|
|
|
impl Painter {
|
|
fn begin(&self, cv: &mut Canvas, pre: Xform) {
|
|
let world = cv.world();
|
|
cv.save_dc();
|
|
cv.modify_world(mul(mul(pre, self.xf), world), 4);
|
|
cv.set_clip(self.clip.clone());
|
|
}
|
|
|
|
fn end(&self, cv: &mut Canvas) {
|
|
cv.restore_dc(-1);
|
|
cv.set_clip(None);
|
|
}
|
|
|
|
fn no_pen(cv: &mut Canvas) {
|
|
cv.set_pen(Pen { style: PS_NULL, width: 0.0, color: [0, 0, 0, 0] });
|
|
}
|
|
|
|
fn fill(&self, cv: &mut Canvas, ops: &[Op], fill: &Fill) {
|
|
self.begin(cv, IDENTITY);
|
|
Self::no_pen(cv);
|
|
match fill {
|
|
Fill::Solid(c) => {
|
|
cv.set_brush(Brush::new(BS_SOLID, *c));
|
|
cv.shape(ops, true, false);
|
|
}
|
|
Fill::Hatch { style, fg, bg } => match hatch_tile(*style, *fg, Some(*bg)) {
|
|
Some(px) => cv.tile_shape(ops, Arc::new(RawTile { w: 8, h: 8, px, cell: cv.pattern_px })),
|
|
// GDI+ has many more hatch styles than GDI: those fill with the blend of both colours.
|
|
None => {
|
|
cv.set_brush(Brush::new(BS_SOLID, lerp(*fg, *bg, 0.5)));
|
|
cv.shape(ops, true, false);
|
|
}
|
|
},
|
|
_ => {
|
|
// Device point to brush space: invert brush space → world → device.
|
|
if let Some(inv) = invert(mul(fill.xf(), cv.device_affine())) {
|
|
cv.shade_shape(ops, &|p| fill.sample(apply(inv, p)));
|
|
}
|
|
}
|
|
}
|
|
self.end(cv);
|
|
}
|
|
|
|
fn stroke(&self, cv: &mut Canvas, ops: &[Op], (color, width): ([u8; 4], f64)) {
|
|
self.begin(cv, IDENTITY);
|
|
cv.set_pen(Pen { style: 0, width, color });
|
|
cv.set_brush(Brush::new(BS_NULL, [0, 0, 0, 0]));
|
|
cv.shape(ops, false, true);
|
|
self.end(cv);
|
|
}
|
|
|
|
fn image(&self, cv: &mut Canvas, dst: [f64; 4], img: Rgba) {
|
|
self.begin(cv, IDENTITY);
|
|
cv.bitmap(dst, img);
|
|
self.end(cv);
|
|
}
|
|
|
|
/// A nested metafile's picture stretched into world rectangle `dst` = `[x, y, w, h]`.
|
|
fn picture(&self, cv: &mut Canvas, [x, y, w, h]: [f64; 4], pic: &Picture) {
|
|
self.begin(cv, IDENTITY);
|
|
let (sx, sy) = (w / f64::from(pic.width), h / f64::from(pic.height));
|
|
let m = mul([sx, 0.0, 0.0, sy, x, y], cv.device_affine());
|
|
let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt();
|
|
let pt = |a: f32, b: f32| apply(m, (f64::from(a), f64::from(b)));
|
|
let ops = |segs: &[Seg]| -> Vec<Op> {
|
|
segs.iter()
|
|
.map(|s| match *s {
|
|
Seg::Move(a, b) => Op::M(pt(a, b)),
|
|
Seg::Line(a, b) => Op::L(pt(a, b)),
|
|
Seg::Cubic(a, b, c, d, e, f) => Op::C(pt(a, b), pt(c, d), pt(e, f)),
|
|
Seg::Close => Op::Z,
|
|
})
|
|
.collect()
|
|
};
|
|
// The nested picture's own clip regions are not carried over: each item takes the
|
|
// canvas's current clip.
|
|
for item in &pic.items {
|
|
if cv.full() {
|
|
break;
|
|
}
|
|
let raw = match item {
|
|
Item::Path { segs, fill, stroke, even_odd, .. } => Raw::Path {
|
|
ops: ops(segs),
|
|
fill: *fill,
|
|
stroke: stroke.map(|(c, w)| (c, f64::from(w) * scale)),
|
|
even_odd: *even_odd,
|
|
region: None,
|
|
},
|
|
Item::Pattern { segs, even_odd, tile, .. } => Raw::Pattern {
|
|
ops: ops(segs),
|
|
even_odd: *even_odd,
|
|
tile: Arc::new(RawTile {
|
|
w: tile.width as usize,
|
|
h: tile.height as usize,
|
|
px: tile.rgba.clone(),
|
|
cell: f64::from(tile.cell[0]) * scale,
|
|
}),
|
|
region: None,
|
|
},
|
|
Item::Bitmap { rect, width, height, rgba, .. } => {
|
|
let (a, b) = (pt(rect[0], rect[1]), pt(rect[0] + rect[2], rect[1] + rect[3]));
|
|
let mut img = Rgba { w: *width as usize, h: *height as usize, bottom_up: false, px: rgba.clone() };
|
|
crate::dib::flip(&mut img, a.0 > b.0, a.1 > b.1);
|
|
Raw::Bitmap { rect: [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()], img, region: None }
|
|
}
|
|
Item::Text(t) => {
|
|
let mut t = t.clone();
|
|
t.transform = mul(t.transform, m);
|
|
if let Some([x0, y0, x1, y1]) = t.clip {
|
|
let (p, q) = (pt(x0, y0), pt(x1, y1));
|
|
t.clip = Some([p.0.min(q.0) as f32, p.1.min(q.1) as f32, p.0.max(q.0) as f32, p.1.max(q.1) as f32]);
|
|
}
|
|
Raw::Text { text: t, region: None }
|
|
}
|
|
};
|
|
cv.push_device(raw);
|
|
}
|
|
self.end(cv);
|
|
}
|
|
|
|
fn text(&self, cv: &mut Canvas, at: P, text: String, dx: Vec<f32>, style: TextStyle, pre: Xform) {
|
|
self.begin(cv, pre);
|
|
cv.set_text_style(style.color, style.font, style.align);
|
|
let (opts, rect) = match style.clip {
|
|
Some(c) => (ETO_CLIPPED, Some(c)),
|
|
None => (0, None),
|
|
};
|
|
cv.text(at, text, dx, opts, rect);
|
|
self.end(cv);
|
|
}
|
|
}
|
|
|
|
fn apply(m: Xform, p: P) -> P {
|
|
(p.0 * m[0] + p.1 * m[2] + m[4], p.0 * m[1] + p.1 * m[3] + m[5])
|
|
}
|
|
|
|
fn invert(m: Xform) -> Option<Xform> {
|
|
let det = m[0] * m[3] - m[1] * m[2];
|
|
if !det.is_finite() || det.abs() < 1e-12 {
|
|
return None;
|
|
}
|
|
let (a, b, c, d) = (m[3] / det, -m[1] / det, -m[2] / det, m[0] / det);
|
|
Some([a, b, c, d, -(m[4] * a + m[5] * c), -(m[4] * b + m[5] * d)])
|
|
}
|
|
|
|
/// 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 floats<const N: usize>(r: Bytes, off: usize) -> Option<[f64; N]> {
|
|
let mut v = [0.0; N];
|
|
for (i, x) in v.iter_mut().enumerate() {
|
|
*x = f64::from(r.f32(off + i * 4)?);
|
|
}
|
|
v.iter().all(|x| x.is_finite()).then_some(v)
|
|
}
|
|
|
|
fn matrix(r: Bytes, off: usize) -> Option<Xform> {
|
|
floats::<6>(r, off)
|
|
}
|
|
|
|
fn parse_object(kind: u16, r: Bytes) -> Option<Obj> {
|
|
match kind {
|
|
1 => brush(r, 0).map(Obj::Brush),
|
|
2 => pen(r),
|
|
3 => path(r).map(Obj::Path),
|
|
4 => region(r),
|
|
5 => image(r).map(|i| Obj::Image(Arc::new(i))),
|
|
6 => font(r),
|
|
7 => Some(Obj::Format(Format { flags: r.u32(4)?, align: r.u32(12)?, line_align: r.u32(16)? })),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// A brush object at `off`: version, type, then the type's data.
|
|
fn brush(r: Bytes, off: usize) -> Option<Fill> {
|
|
let d = r.tail(off + 8)?;
|
|
match r.u32(off + 4)? {
|
|
0 => Some(Fill::Solid(argb(d.u32(0)?))),
|
|
1 => Some(Fill::Hatch { style: d.u32(0)?, fg: argb(d.u32(4)?), bg: argb(d.u32(8)?) }),
|
|
2 => {
|
|
let bflags = d.u32(0)?;
|
|
let wrap = d.u32(4)?;
|
|
let (xf, off) = if bflags & 0x2 != 0 { (matrix(d, 8)?, 32) } else { (IDENTITY, 8) };
|
|
let ImageData::Bitmap(img) = image(d.tail(off)?)? else { return None };
|
|
Some(Fill::Texture { img: Arc::new(img), wrap, xf })
|
|
}
|
|
3 => path_gradient(d),
|
|
4 => linear_gradient(d),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn linear_gradient(d: Bytes) -> Option<Fill> {
|
|
let bflags = d.u32(0)?;
|
|
let wrap = d.u32(4)?;
|
|
let rect = floats::<4>(d, 8)?;
|
|
let (c0, c1) = (argb(d.u32(24)?), argb(d.u32(28)?));
|
|
let mut off = 40;
|
|
let mut xf = IDENTITY;
|
|
if bflags & 0x2 != 0 {
|
|
xf = matrix(d, off)?;
|
|
off += 24;
|
|
}
|
|
let mut stops = vec![(0.0, c0), (1.0, c1)];
|
|
if bflags & 0x4 != 0 {
|
|
// Preset colours: positions, then one colour each.
|
|
let n = (d.u32(off)? as usize).min(MAX_STOPS);
|
|
let pos: Vec<f64> = (0..n).map(|i| d.f32(off + 4 + i * 4).map(f64::from)).collect::<Option<_>>()?;
|
|
let cols: Vec<[u8; 4]> = (0..n).map(|i| d.u32(off + 4 + n * 4 + i * 4).map(argb)).collect::<Option<_>>()?;
|
|
stops = pos.into_iter().zip(cols).collect();
|
|
} else if bflags & 0x8 != 0 {
|
|
// Blend factors: positions, then how far from the start colour to the end colour.
|
|
let n = (d.u32(off)? as usize).min(MAX_STOPS);
|
|
let pos: Vec<f64> = (0..n).map(|i| d.f32(off + 4 + i * 4).map(f64::from)).collect::<Option<_>>()?;
|
|
let fac: Vec<f64> = (0..n).map(|i| d.f32(off + 4 + n * 4 + i * 4).map(f64::from)).collect::<Option<_>>()?;
|
|
stops = pos.into_iter().zip(fac).map(|(p, k)| (p, lerp(c0, c1, k.clamp(0.0, 1.0)))).collect();
|
|
}
|
|
stops.retain(|s| s.0.is_finite());
|
|
stops.sort_by(|a, b| a.0.total_cmp(&b.0));
|
|
(!stops.is_empty()).then_some(Fill::Linear { rect, stops, wrap, xf })
|
|
}
|
|
|
|
/// A path gradient: from the centre colour to the (first) surrounding colour at the boundary,
|
|
/// taken as the ellipse in the boundary's bounding rectangle.
|
|
fn path_gradient(d: Bytes) -> Option<Fill> {
|
|
let bflags = d.u32(0)?;
|
|
let center_c = argb(d.u32(8)?);
|
|
let [cx, cy] = floats::<2>(d, 12)?;
|
|
let nc = (d.u32(20)? as usize).min(MAX_STOPS);
|
|
let surround = if nc > 0 { argb(d.u32(24)?) } else { center_c };
|
|
let mut off = 24 + nc * 4;
|
|
let bbox = if bflags & 0x1 != 0 {
|
|
let size = d.u32(off)? as usize;
|
|
let b = ops_bbox(&path(d.tail(off + 4)?)?)?;
|
|
off = off.checked_add(4 + size)?;
|
|
b
|
|
} else {
|
|
let n = d.u32(off)? as usize;
|
|
let pts = points(d, off + 4, n, false)?;
|
|
off = off.checked_add(4 + 8 * n.min(MAX_POINTS))?;
|
|
ops_bbox(&poly_ops(&pts, true))?
|
|
};
|
|
let xf = if bflags & 0x2 != 0 { matrix(d, off)? } else { IDENTITY };
|
|
let radius = ((bbox[2] - bbox[0]) / 2.0, (bbox[3] - bbox[1]) / 2.0);
|
|
Some(Fill::Radial { center: (cx, cy), radius, stops: vec![(0.0, center_c), (1.0, surround)], xf })
|
|
}
|
|
|
|
/// 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)?.color();
|
|
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]));
|
|
if types[i + 2] & 0x80 != 0 {
|
|
ops.push(Op::Z);
|
|
}
|
|
i += 3;
|
|
continue;
|
|
}
|
|
_ => ops.push(Op::L(p)),
|
|
}
|
|
if t & 0x80 != 0 {
|
|
ops.push(Op::Z);
|
|
}
|
|
i += 1;
|
|
}
|
|
Some(ops)
|
|
}
|
|
|
|
/// A region object as a convex area in world units (non-convex parts become their bounding
|
|
/// rectangle).
|
|
fn region(r: Bytes) -> Option<Obj> {
|
|
region_node(r, 8, 0).map(|(a, _)| Obj::Region(a))
|
|
}
|
|
|
|
/// One region node at `off`: its area and its length in bytes.
|
|
fn region_node(r: Bytes, off: usize, depth: usize) -> Option<(Area, usize)> {
|
|
if depth > MAX_REGION_DEPTH {
|
|
return None;
|
|
}
|
|
match r.u32(off)? {
|
|
0x1000_0000 => {
|
|
let [x, y, w, h] = floats::<4>(r, off + 4)?;
|
|
Some((Some(rect_poly([x.min(x + w), y.min(y + h), x.max(x + w), y.max(y + h)])), 20))
|
|
}
|
|
0x1000_0001 => {
|
|
let size = r.u32(off + 4)? as usize;
|
|
let area = path(r.tail(off + 8)?).map(|ops| area_of_ops(&ops)).unwrap_or_default();
|
|
Some((Some(area), 8usize.checked_add(size)?))
|
|
}
|
|
0x1000_0002 => Some((Some(Vec::new()), 4)),
|
|
0x1000_0003 => Some((None, 4)),
|
|
mode @ 0..=5 => {
|
|
let (a, la) = region_node(r, off + 4, depth + 1)?;
|
|
let (b, lb) = region_node(r, off.checked_add(4 + la)?, depth + 1)?;
|
|
let out = match mode {
|
|
0 => b,
|
|
1 => area_intersect(a, b),
|
|
2 => area_union(a, b),
|
|
// XOR, exclude, complement: the left area bounds the result closely enough.
|
|
_ => a,
|
|
};
|
|
Some((out, (4 + la).checked_add(lb)?))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// The area a path encloses: its outline when that is one convex figure, else the rectangle
|
|
/// around it.
|
|
fn area_of_ops(ops: &[Op]) -> Vec<P> {
|
|
let subs = flatten(ops);
|
|
if let [(pts, _)] = subs.as_slice()
|
|
&& let Some(c) = convex(pts)
|
|
{
|
|
return c;
|
|
}
|
|
ops_bbox(ops).map(rect_poly).unwrap_or_default()
|
|
}
|
|
|
|
fn area_intersect(a: Area, b: Area) -> Area {
|
|
match (a, b) {
|
|
(None, x) | (x, None) => x,
|
|
(Some(a), Some(b)) if a.len() >= 3 && b.len() >= 3 => Some(convex(&clip_polygon(&a, &b)).unwrap_or_default()),
|
|
_ => Some(Vec::new()),
|
|
}
|
|
}
|
|
|
|
fn area_union(a: Area, b: Area) -> Area {
|
|
match (a, b) {
|
|
(Some(a), Some(b)) => {
|
|
if a.len() < 3 {
|
|
return Some(b);
|
|
}
|
|
if b.len() < 3 {
|
|
return Some(a);
|
|
}
|
|
let all: Vec<P> = a.into_iter().chain(b).collect();
|
|
Some(pts_bbox(&all).map(rect_poly).unwrap_or_default())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// An area through an affine map (which keeps it convex; the orientation is put right again).
|
|
fn map_area(a: Area, m: Xform) -> Area {
|
|
a.map(|pts| {
|
|
let mapped: Vec<P> = pts.iter().map(|p| apply(m, *p)).collect();
|
|
if mapped.len() < 3 { Vec::new() } else { convex(&mapped).unwrap_or_default() }
|
|
})
|
|
}
|
|
|
|
/// An elliptical arc in `rect` from `start` sweeping `sweep` degrees (clockwise on screen), as
|
|
/// cubics; a pie adds the lines to and from the centre.
|
|
fn arc_ops([l, t, r, b]: [f64; 4], start: f64, sweep: f64, pie: bool) -> Vec<Op> {
|
|
let (cx, cy, rx, ry) = ((l + r) / 2.0, (t + b) / 2.0, (r - l) / 2.0, (b - t) / 2.0);
|
|
let sweep = sweep.clamp(-360.0, 360.0);
|
|
// GDI+ angles are along rays from the centre; turn them into the ellipse's parameter.
|
|
let param = |deg: f64| {
|
|
let a = deg.to_radians();
|
|
(rx * a.sin()).atan2(ry * a.cos())
|
|
};
|
|
let t0 = param(start);
|
|
let mut dt = if sweep.abs() >= 360.0 { sweep.signum() * std::f64::consts::TAU } else { param(start + sweep) - t0 };
|
|
if sweep.abs() < 360.0 {
|
|
while sweep > 0.0 && dt < 0.0 {
|
|
dt += std::f64::consts::TAU;
|
|
}
|
|
while sweep < 0.0 && dt > 0.0 {
|
|
dt -= std::f64::consts::TAU;
|
|
}
|
|
}
|
|
let at = |t: f64| (cx + rx * t.cos(), cy + ry * t.sin());
|
|
let mut ops = Vec::with_capacity(8);
|
|
if pie {
|
|
ops.push(Op::M((cx, cy)));
|
|
ops.push(Op::L(at(t0)));
|
|
} else {
|
|
ops.push(Op::M(at(t0)));
|
|
}
|
|
let n = ((dt.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).clamp(1, 4);
|
|
let step = dt / n as f64;
|
|
let k = 4.0 / 3.0 * (step / 4.0).tan();
|
|
for i in 0..n {
|
|
let (a, e) = (t0 + step * i as f64, t0 + step * (i + 1) as f64);
|
|
let (p0, p3) = (at(a), at(e));
|
|
let c1 = (p0.0 - k * rx * a.sin(), p0.1 + k * ry * a.cos());
|
|
let c2 = (p3.0 + k * rx * e.sin(), p3.1 - k * ry * e.cos());
|
|
ops.push(Op::C(c1, c2, p3));
|
|
}
|
|
if pie {
|
|
ops.push(Op::Z);
|
|
}
|
|
ops
|
|
}
|
|
|
|
/// A font object: em size, unit, style flags, then the family name in UTF-16.
|
|
fn font(r: Bytes) -> Option<Obj> {
|
|
let em = f64::from(r.f32(4)?);
|
|
let n = (r.u32(20)? as usize).min(64);
|
|
let units: Vec<u16> = (0..n).map(|i| r.u16(24 + i * 2)).collect::<Option<_>>()?;
|
|
let family = String::from_utf16_lossy(&units).trim_end_matches('\0').to_string();
|
|
em.is_finite().then_some(Obj::Font(FontObj { em, unit: r.u32(8)?, style: r.u32(12)?, family }))
|
|
}
|
|
|
|
/// An image object: a bitmap of raw pixels or a compressed (PNG, JPEG, GIF…) file, or a nested
|
|
/// metafile.
|
|
fn image(r: Bytes) -> Option<ImageData> {
|
|
match r.u32(4)? {
|
|
1 => bitmap(r).map(ImageData::Bitmap),
|
|
2 => metafile(r).map(ImageData::Meta),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// A metafile image: type, size, then a WMF (placeable or not) or EMF file, parsed as a picture.
|
|
fn metafile(r: Bytes) -> Option<Picture> {
|
|
let size = r.u32(12)? as usize;
|
|
let data = r.slice(16, size).or_else(|| r.tail(16).map(|t| t.0))?;
|
|
let depth = NESTING.with(|n| n.get());
|
|
if depth >= MAX_NESTING {
|
|
return None;
|
|
}
|
|
NESTING.with(|n| n.set(depth + 1));
|
|
let pic = crate::parse(data).ok();
|
|
NESTING.with(|n| n.set(depth));
|
|
pic.filter(|p| p.width > 0.0 && p.height > 0.0)
|
|
}
|
|
|
|
fn bitmap(r: Bytes) -> Option<Rgba> {
|
|
let (w, h, stride) = (r.u32(8)? as i32, r.u32(12)? as i32, r.u32(16)? as i32);
|
|
let format = r.u32(20)?;
|
|
let data = r.tail(28)?;
|
|
if r.u32(24)? == 1 {
|
|
return decode_compressed(data.0);
|
|
}
|
|
let (w, h) = (usize::try_from(w).ok()?, usize::try_from(h).ok()?);
|
|
if w == 0 || h == 0 || w.checked_mul(h)? > crate::MAX_PIXELS {
|
|
return None;
|
|
}
|
|
let bpp = ((format >> 8) & 0xFF) as usize;
|
|
let (pal, pixels) = if format & 0x0001_0000 != 0 {
|
|
// Indexed formats carry a palette first: flags, count, ARGB entries.
|
|
let n = (data.u32(4)? as usize).min(256);
|
|
let pal: Vec<[u8; 4]> = (0..n).map(|i| data.u32(8 + i * 4).map(argb)).collect::<Option<_>>()?;
|
|
(pal, data.tail(8 + n * 4)?)
|
|
} else {
|
|
(Vec::new(), data)
|
|
};
|
|
let row = (w.checked_mul(bpp)?).div_ceil(8);
|
|
let stride_abs = (stride.unsigned_abs() as usize).max(row);
|
|
let mut px = Vec::with_capacity(w * h * 4);
|
|
for y in 0..h {
|
|
let line = pixels.slice(y.checked_mul(stride_abs)?, row)?;
|
|
for x in 0..w {
|
|
let c = match (format, bpp) {
|
|
(_, 1 | 4 | 8) if !pal.is_empty() => {
|
|
let bit = x * bpp;
|
|
let byte = *line.get(bit / 8)?;
|
|
let i = (byte >> (8 - bpp - bit % 8)) & ((1u16 << bpp) - 1) as u8;
|
|
pal.get(usize::from(i)).copied().unwrap_or([0, 0, 0, 255])
|
|
}
|
|
(_, 24) => {
|
|
let p = line.get(x * 3..x * 3 + 3)?;
|
|
[p[2], p[1], p[0], 255]
|
|
}
|
|
(0x0002_2009, 32) => {
|
|
let p = line.get(x * 4..x * 4 + 4)?;
|
|
[p[2], p[1], p[0], 255]
|
|
}
|
|
(0x000E_200B, 32) => {
|
|
let p = line.get(x * 4..x * 4 + 4)?;
|
|
let a = u32::from(p[3]);
|
|
let un = |v: u8| (u32::from(v) * 255 + a / 2).checked_div(a).map_or(v, |c| c.min(255) as u8);
|
|
[un(p[2]), un(p[1]), un(p[0]), p[3]]
|
|
}
|
|
(_, 32) => {
|
|
let p = line.get(x * 4..x * 4 + 4)?;
|
|
[p[2], p[1], p[0], p[3]]
|
|
}
|
|
(0x0002_1006, 16) => {
|
|
let v = u16::from_le_bytes([*line.get(x * 2)?, *line.get(x * 2 + 1)?]);
|
|
let s = |v: u16, bits: u32| ((u32::from(v) * 255) / ((1 << bits) - 1)) as u8;
|
|
[s(v >> 11, 5), s((v >> 5) & 0x3F, 6), s(v & 0x1F, 5), 255]
|
|
}
|
|
(_, 16) => {
|
|
let v = u16::from_le_bytes([*line.get(x * 2)?, *line.get(x * 2 + 1)?]);
|
|
let s = |v: u16| ((u32::from(v & 0x1F) * 255) / 31) as u8;
|
|
[s(v >> 10), s(v >> 5), s(v), 255]
|
|
}
|
|
_ => return None,
|
|
};
|
|
px.extend_from_slice(&c);
|
|
}
|
|
}
|
|
// A negative stride stores the rows bottom-up.
|
|
Some(top_down(Rgba { w, h, bottom_up: stride < 0, px }))
|
|
}
|
|
|
|
fn top_down(mut img: Rgba) -> Rgba {
|
|
if img.bottom_up {
|
|
crate::dib::flip(&mut img, false, true);
|
|
img.bottom_up = false;
|
|
}
|
|
img
|
|
}
|
|
|
|
fn decode_compressed(bytes: &[u8]) -> Option<Rgba> {
|
|
let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
|
|
let mut limits = image::Limits::default();
|
|
limits.max_image_width = Some(1 << 15);
|
|
limits.max_image_height = Some(1 << 15);
|
|
limits.max_alloc = Some((crate::MAX_PIXELS * 4) as u64);
|
|
reader.limits(limits);
|
|
let img = reader.decode().ok()?.to_rgba8();
|
|
let (w, h) = (img.width() as usize, img.height() as usize);
|
|
if w.checked_mul(h)? > crate::MAX_PIXELS {
|
|
return None;
|
|
}
|
|
Some(Rgba { w, h, bottom_up: false, px: img.into_raw() })
|
|
}
|
|
|
|
/// `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 finite(v: f32) -> Option<f64> {
|
|
v.is_finite().then_some(f64::from(v))
|
|
}
|
|
|
|
/// A GDI+ cardinal spline through `pts` as cubic Béziers: each segment's control points lie a
|
|
/// third of `tension` (GDI+'s default 0.5) along the neighbouring chords. `closed` joins the
|
|
/// last point back to the first; an open curve draws `segments` segments from point `offset`.
|
|
fn curve_ops(pts: &[P], tension: f64, closed: bool, offset: usize, segments: usize) -> Vec<Op> {
|
|
let n = pts.len();
|
|
if n < 2 {
|
|
return Vec::new();
|
|
}
|
|
let k = tension.clamp(0.0, 10.0) / 3.0;
|
|
let at = |i: isize| -> P {
|
|
let i = if closed { i.rem_euclid(n as isize) as usize } else { i.clamp(0, n as isize - 1) as usize };
|
|
pts.get(i).copied().unwrap_or((0.0, 0.0))
|
|
};
|
|
let (first, count) = if closed { (0, n) } else { (offset.min(n - 1), segments.min(n - 1 - offset.min(n - 1))) };
|
|
let mut ops = vec![Op::M(at(first as isize))];
|
|
for s in first..first + count {
|
|
let i = s as isize;
|
|
let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
|
|
let c1 = (p1.0 + (p2.0 - p0.0) * k, p1.1 + (p2.1 - p0.1) * k);
|
|
let c2 = (p2.0 - (p3.0 - p1.0) * k, p2.1 - (p3.1 - p1.1) * k);
|
|
ops.push(Op::C(c1, c2, p2));
|
|
}
|
|
if closed {
|
|
ops.push(Op::Z);
|
|
}
|
|
ops
|
|
}
|
|
|
|
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,
|
|
]
|
|
}
|
|
|
|
/// Core fonts whose first glyphs follow the standard Macintosh order (glyph 3 is the space,
|
|
/// glyphs 3..=97 are U+0020..=U+007E).
|
|
const MAC_ORDER_FONTS: &[&str] =
|
|
&["arial", "arial black", "times new roman", "courier new", "verdana", "georgia", "tahoma", "trebuchet ms", "comic sans ms", "impact"];
|
|
|
|
/// Glyph indices of a known font back to ASCII; None when the font is unknown or a glyph is
|
|
/// outside the ASCII range.
|
|
fn glyphs_to_chars(family: &str, glyphs: &[u16]) -> Option<Vec<u16>> {
|
|
let family = family.trim().to_ascii_lowercase();
|
|
if !MAC_ORDER_FONTS.contains(&family.as_str()) {
|
|
return None;
|
|
}
|
|
glyphs.iter().map(|g| (3..=97).contains(g).then_some(g + 29)).collect()
|
|
}
|
|
|
|
/// A rough advance for a character in ems: the metafile crate has no fonts, so line breaks for
|
|
/// wrapped strings use typical proportional widths.
|
|
fn char_em(u: u16) -> f64 {
|
|
match char::from_u32(u32::from(u)).unwrap_or('x') {
|
|
' ' | 'i' | 'l' | 'j' | '.' | ',' | ':' | ';' | '\'' | '!' | '|' | 'I' | 'f' | 't' | 'r' => 0.3,
|
|
'm' | 'w' | 'M' | 'W' => 0.85,
|
|
c if c.is_ascii_uppercase() || c.is_ascii_digit() => 0.62,
|
|
c if (c as u32) >= 0x2E80 => 1.0,
|
|
_ => 0.52,
|
|
}
|
|
}
|
|
|
|
/// Greedy word wrap of one line to `width` world units at em size `em`; a word longer than the
|
|
/// line breaks between characters.
|
|
fn wrap_line(line: &[u16], width: f64, em: f64) -> Vec<Vec<u16>> {
|
|
if em <= 0.0 || !width.is_finite() {
|
|
return vec![line.to_vec()];
|
|
}
|
|
let fits = |l: &[u16]| l.iter().map(|u| char_em(*u) * em).sum::<f64>() <= width;
|
|
let mut out: Vec<Vec<u16>> = Vec::new();
|
|
let mut cur: Vec<u16> = Vec::new();
|
|
for word in line.split_inclusive(|u| *u == 0x20) {
|
|
let mut cand = cur.clone();
|
|
cand.extend_from_slice(word);
|
|
let trimmed = |v: &[u16]| v.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1);
|
|
if fits(&cand[..trimmed(&cand)]) || cur.is_empty() && fits(&word[..trimmed(word)]) {
|
|
cur = cand;
|
|
continue;
|
|
}
|
|
if !cur.is_empty() {
|
|
let n = trimmed(&cur);
|
|
cur.truncate(n);
|
|
out.push(std::mem::take(&mut cur));
|
|
}
|
|
// A word wider than the line on its own: break it between characters.
|
|
for &u in word {
|
|
cur.push(u);
|
|
if !fits(&cur[..trimmed(&cur)]) && cur.len() > 1 {
|
|
cur.pop();
|
|
out.push(std::mem::take(&mut cur));
|
|
cur.push(u);
|
|
}
|
|
}
|
|
}
|
|
let n = cur.iter().rposition(|u| *u != 0x20).map_or(0, |i| i + 1);
|
|
cur.truncate(n);
|
|
out.push(cur);
|
|
out
|
|
}
|