mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Rarely set fields move into one optional boxed NodeExtras, and large NodeKind payloads (images, live shapes, blends, envelopes) are boxed. Every object, and every undo step that copies it, shrinks: a 50k-path move keeps ~24.5 MB per step instead of ~40 MB. Files stay byte-identical (Node keeps its flat JSON through a borrowed Serialize and a from-wire Deserialize, no flatten), and loading is slightly faster. Undo and redo reuse their step's size estimate instead of walking the document again (8.3 -> 5.6 ms for a 50k-path move). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc # Conflicts: # crates/engine/src/cmd/graph.rs # crates/svg/src/export.rs
895 lines
31 KiB
Rust
895 lines
31 KiB
Rust
//! The metafile reader: the format parsers (`emf`, `wmf`) play their records on a [`Player`], a
|
||
//! small model of the device context they draw into (objects, transforms, the current position,
|
||
//! the path bracket, the clip), which turns what they draw into document objects.
|
||
//!
|
||
//! Fills and strokes become paths with a fill and a stroke; lines drawn one after another with the
|
||
//! same pen join into one path. Clips become clipping groups around what they clip. Images keep
|
||
//! their pixels (as PNG), text becomes point type in the font the file names. Records the reader
|
||
//! doesn't know are counted and skipped, with one warning.
|
||
|
||
mod emf;
|
||
mod wmf;
|
||
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_color::{Color, Paint};
|
||
use vectorcraft_doc::clipnest::{Clip, Drawn, MAX_NEST, deep_clips, nest};
|
||
use vectorcraft_doc::{
|
||
Appearance, AppearanceItem, CharStyle, Dash, Document, FillLayer, ImageBlob, ImageObject, Justify, LayerColor, LineCap, LineJoin, Node, NodeKind,
|
||
StrokeLayer, TextObject,
|
||
};
|
||
use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, PathEl, Point, Rect, Shape, Vec2, shapes};
|
||
|
||
use crate::dib::Rgba;
|
||
use crate::{Imported, Kind};
|
||
|
||
/// Most objects an import makes (a hostile file can't exhaust memory with them).
|
||
const MAX_NODES: usize = 1 << 20;
|
||
/// Objects and clips reaching further than this from the origin (points, well inside the canvas)
|
||
/// are left out: they come from damaged or hostile coordinates.
|
||
const LIMIT: f64 = 1e6;
|
||
/// Most nested saved device contexts.
|
||
const MAX_SAVED: usize = 1024;
|
||
/// Width of a cosmetic pen (one pixel of a 96 ppi screen), points.
|
||
const HAIRLINE: f64 = 0.75;
|
||
/// Curves made for arcs and ellipses are this close to the true curve (logical units are
|
||
/// transformed after, so it is relative).
|
||
const ARC_TOLERANCE: f64 = 0.01;
|
||
|
||
/// Raster operations.
|
||
pub(crate) const SRCCOPY: u32 = 0x00CC_0020;
|
||
const PATCOPY: u32 = 0x00F0_0021;
|
||
const WHITENESS: u32 = 0x00FF_0062;
|
||
const BLACKNESS: u32 = 0x0000_0042;
|
||
|
||
const SKIPPED_ROPS: &str = "images drawn with raster operations other than a plain copy (masks, inversions) were left out";
|
||
const HATCHES: &str = "hatched brushes are filled with their colour";
|
||
const PATTERN_BRUSHES: &str = "pattern brushes are filled grey";
|
||
const EXCLUDED_CLIPS: &str = "clip areas cut out of the clip (excluded rectangles, differences) are ignored";
|
||
const BAD_IMAGE: &str = "an image that couldn't be read was left out";
|
||
const FAR_AWAY: &str = "objects far outside the picture were left out";
|
||
|
||
/// Is `r` finite and within [`LIMIT`]?
|
||
fn sane(r: Rect) -> bool {
|
||
[r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite() && v.abs() <= LIMIT)
|
||
}
|
||
|
||
/// A pen: its style flags (`PS_*`), width in logical units, colour and dash entries.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub(crate) struct PenObj {
|
||
pub style: u32,
|
||
pub width: f64,
|
||
pub color: [u8; 3],
|
||
/// PS_USERSTYLE entries, logical units.
|
||
pub entries: Vec<f64>,
|
||
}
|
||
|
||
impl PenObj {
|
||
pub const NULL: PenObj = PenObj { style: PS_NULL, width: 0.0, color: [0; 3], entries: vec![] };
|
||
pub fn solid(color: [u8; 3]) -> Self {
|
||
Self { style: 0, width: 0.0, color, entries: vec![] }
|
||
}
|
||
}
|
||
|
||
pub(crate) const PS_NULL: u32 = 5;
|
||
const PS_GEOMETRIC: u32 = 0x0001_0000;
|
||
|
||
/// A brush.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub(crate) enum BrushObj {
|
||
Null,
|
||
Solid([u8; 3]),
|
||
Hatched([u8; 3]),
|
||
/// A bitmap pattern (written grey).
|
||
Pattern,
|
||
}
|
||
|
||
impl BrushObj {
|
||
/// From a LOGBRUSH's style and colour.
|
||
pub fn from_log(style: u32, color: [u8; 3]) -> Self {
|
||
match style {
|
||
0 => Self::Solid(color),
|
||
1 => Self::Null,
|
||
2 => Self::Hatched(color),
|
||
_ => Self::Pattern,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A font: its height in logical units (negative: the character height, positive: the cell
|
||
/// height), escapement (tenths of a degree), weight, italics and face.
|
||
#[derive(Clone, Debug, Default, PartialEq)]
|
||
pub(crate) struct FontObj {
|
||
pub height: f64,
|
||
pub escapement: f64,
|
||
pub weight: i32,
|
||
pub italic: bool,
|
||
pub face: String,
|
||
}
|
||
|
||
/// A GDI object.
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) enum Obj {
|
||
Pen(PenObj),
|
||
Brush(BrushObj),
|
||
Font(FontObj),
|
||
/// Palettes, regions, colour spaces: they take a slot but don't change the drawing.
|
||
Other,
|
||
}
|
||
|
||
/// The state records change, saved and restored as a whole.
|
||
#[derive(Clone, Debug)]
|
||
struct Dc {
|
||
pen: PenObj,
|
||
brush: BrushObj,
|
||
font: FontObj,
|
||
text_color: [u8; 3],
|
||
text_align: u32,
|
||
fill_rule: FillRule,
|
||
miter: f64,
|
||
/// Arcs run counterclockwise (the default).
|
||
arc_ccw: bool,
|
||
/// Logical → page (EMF world transform).
|
||
world: Affine,
|
||
map_mode: u32,
|
||
win_org: Vec2,
|
||
win_ext: Vec2,
|
||
vp_org: Vec2,
|
||
vp_ext: Vec2,
|
||
clips: Vec<Arc<Clip>>,
|
||
}
|
||
|
||
impl Default for Dc {
|
||
fn default() -> Self {
|
||
Self {
|
||
pen: PenObj::solid([0; 3]),
|
||
brush: BrushObj::Solid([255; 3]),
|
||
font: FontObj::default(),
|
||
text_color: [0; 3],
|
||
text_align: 0,
|
||
fill_rule: FillRule::EvenOdd,
|
||
miter: 10.0,
|
||
arc_ccw: true,
|
||
world: Affine::IDENTITY,
|
||
map_mode: 1,
|
||
win_org: Vec2::ZERO,
|
||
win_ext: Vec2::new(1.0, 1.0),
|
||
vp_org: Vec2::ZERO,
|
||
vp_ext: Vec2::new(1.0, 1.0),
|
||
clips: vec![],
|
||
}
|
||
}
|
||
}
|
||
|
||
/// How an arc closes.
|
||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||
pub(crate) enum ArcKind {
|
||
Open,
|
||
Chord,
|
||
Pie,
|
||
}
|
||
|
||
pub(crate) struct Player {
|
||
kind: Kind,
|
||
/// Device units (EMF) or logical units (WMF) → document points.
|
||
dev_to_doc: Affine,
|
||
/// Millimetres per device pixel (EMF's fixed mapping modes).
|
||
mm_per_px: f64,
|
||
dc: Dc,
|
||
saved: Vec<Dc>,
|
||
objects: Vec<Option<Obj>>,
|
||
/// The lowest object slot that may be free (WMF numbering).
|
||
free_hint: usize,
|
||
/// The path being recorded (document space), whether a figure is open in it and where that
|
||
/// figure started (logical).
|
||
path: Option<BezPath>,
|
||
figure: bool,
|
||
figure_start: Point,
|
||
/// The artboard (document space): clips holding all of it clip nothing worth keeping.
|
||
frame: Rect,
|
||
/// The current position (logical).
|
||
pos: Point,
|
||
/// Lines drawn one after another outside a path bracket, not drawn yet.
|
||
pending: BezPath,
|
||
out: Drawn,
|
||
doc: Document,
|
||
warnings: Vec<String>,
|
||
skipped: usize,
|
||
next_clip: u32,
|
||
/// The file holds EMF+ records (which aren't read).
|
||
pub emf_plus: bool,
|
||
}
|
||
|
||
fn rgb(c: [u8; 3]) -> Paint {
|
||
Paint::solid(Color::rgb8(c[0], c[1], c[2]))
|
||
}
|
||
|
||
/// The arc of the ellipse in `r` from the ray through `start` to the ray through `end`.
|
||
pub(crate) fn arc_path(r: Rect, start: Point, end: Point, ccw: bool, kind: ArcKind) -> BezPath {
|
||
let r = r.abs();
|
||
let c = r.center();
|
||
let radii = Vec2::new(r.width() / 2.0, r.height() / 2.0);
|
||
let mut bp = BezPath::new();
|
||
if radii.x <= 0.0 || radii.y <= 0.0 {
|
||
return bp;
|
||
}
|
||
let angle = |p: Point| ((p.y - c.y) / radii.y).atan2((p.x - c.x) / radii.x);
|
||
let (a0, a1) = (angle(start), angle(end));
|
||
let tau = std::f64::consts::TAU;
|
||
// Counterclockwise on a y-down device is towards smaller angles.
|
||
let sweep = if ccw {
|
||
let s = (a0 - a1).rem_euclid(tau);
|
||
-(if s == 0.0 { tau } else { s })
|
||
} else {
|
||
let s = (a1 - a0).rem_euclid(tau);
|
||
if s == 0.0 { tau } else { s }
|
||
};
|
||
let arc = kurbo::Arc { center: c, radii, start_angle: a0, sweep_angle: sweep, x_rotation: 0.0 };
|
||
let tol = ARC_TOLERANCE * radii.x.max(radii.y).max(1e-9);
|
||
if kind == ArcKind::Pie {
|
||
bp.move_to(c);
|
||
let first = c + Vec2::new(radii.x * a0.cos(), radii.y * a0.sin());
|
||
bp.line_to(first);
|
||
arc.append_iter(tol).for_each(|el| bp.push(el));
|
||
} else {
|
||
kurbo::Shape::path_elements(&arc, tol).for_each(|el| bp.push(el));
|
||
}
|
||
if kind != ArcKind::Open {
|
||
bp.close_path();
|
||
}
|
||
bp
|
||
}
|
||
|
||
/// A rectangle with elliptical corners `rx` × `ry`.
|
||
pub(crate) fn round_rect(r: Rect, rx: f64, ry: f64) -> BezPath {
|
||
let r = r.abs();
|
||
let (rx, ry) = ((rx / 2.0).abs().min(r.width() / 2.0), (ry / 2.0).abs().min(r.height() / 2.0));
|
||
if rx <= 0.0 || ry <= 0.0 {
|
||
return shapes::rectangle(r).to_bezpath();
|
||
}
|
||
// A rectangle with round corners of radius `ry`, squeezed across.
|
||
let s = ry / rx;
|
||
let squeezed = Rect::new(r.x0 * s, r.y0, r.x1 * s, r.y1);
|
||
let mut bp = shapes::rounded_rectangle(squeezed, ry).to_bezpath();
|
||
bp.apply_affine(Affine::scale_non_uniform(1.0 / s, 1.0));
|
||
bp
|
||
}
|
||
|
||
impl Player {
|
||
fn new(kind: Kind, dev_to_doc: Affine, mm_per_px: f64, artboard: Rect) -> Self {
|
||
let side = |v: f64| if v.is_finite() { v.clamp(1.0, LIMIT) } else { 1.0 };
|
||
let (w, h) = (side(artboard.width()), side(artboard.height()));
|
||
let mut doc = Document::new(w, h);
|
||
doc.layers.clear();
|
||
Self {
|
||
kind,
|
||
dev_to_doc,
|
||
mm_per_px,
|
||
dc: Dc::default(),
|
||
saved: vec![],
|
||
objects: vec![],
|
||
free_hint: 0,
|
||
path: None,
|
||
figure: false,
|
||
figure_start: Point::ZERO,
|
||
frame: artboard,
|
||
pos: Point::ZERO,
|
||
pending: BezPath::new(),
|
||
out: vec![],
|
||
doc,
|
||
warnings: vec![],
|
||
skipped: 0,
|
||
next_clip: 0,
|
||
emf_plus: false,
|
||
}
|
||
}
|
||
|
||
pub fn warn(&mut self, w: &str) {
|
||
if !self.warnings.iter().any(|x| x == w) {
|
||
self.warnings.push(w.to_string());
|
||
}
|
||
}
|
||
|
||
/// A record of a kind the reader doesn't know.
|
||
pub fn skip(&mut self) {
|
||
self.skipped += 1;
|
||
}
|
||
|
||
// ---------- mapping ----------
|
||
|
||
/// Logical → document.
|
||
fn xf(&self) -> Affine {
|
||
if self.kind == Kind::Wmf {
|
||
return self.dev_to_doc;
|
||
}
|
||
let d = &self.dc;
|
||
let page_to_dev = match d.map_mode {
|
||
// Fixed modes: millimetres a unit, y up.
|
||
2..=6 => {
|
||
let mm = [0.1, 0.01, 0.254, 0.0254, 25.4 / 1440.0][(d.map_mode - 2) as usize];
|
||
let s = mm / self.mm_per_px.max(1e-12);
|
||
Affine::translate(d.vp_org) * Affine::scale_non_uniform(s, -s) * Affine::translate(-d.win_org)
|
||
}
|
||
7 | 8 => {
|
||
let ratio = |v: f64, w: f64| if w.abs() > 1e-12 && v.is_finite() { v / w } else { 1.0 };
|
||
let (mut sx, mut sy) = (ratio(d.vp_ext.x, d.win_ext.x), ratio(d.vp_ext.y, d.win_ext.y));
|
||
if d.map_mode == 7 {
|
||
// Isotropic: the same scale both ways, the smaller one.
|
||
let s = sx.abs().min(sy.abs());
|
||
(sx, sy) = (s.copysign(sx), s.copysign(sy));
|
||
}
|
||
Affine::translate(d.vp_org) * Affine::scale_non_uniform(sx, sy) * Affine::translate(-d.win_org)
|
||
}
|
||
_ => Affine::translate(d.vp_org - d.win_org),
|
||
};
|
||
self.dev_to_doc * page_to_dev * d.world
|
||
}
|
||
|
||
/// How much logical lengths grow in the document.
|
||
fn scale(&self) -> f64 {
|
||
let s = self.xf().determinant().abs().sqrt();
|
||
if s.is_finite() { s } else { 0.0 }
|
||
}
|
||
|
||
pub fn set_map_mode(&mut self, m: u32) {
|
||
self.dc.map_mode = m;
|
||
}
|
||
pub fn set_window_org(&mut self, x: f64, y: f64) {
|
||
self.dc.win_org = Vec2::new(x, y);
|
||
}
|
||
pub fn set_window_ext(&mut self, x: f64, y: f64) {
|
||
self.dc.win_ext = Vec2::new(x, y);
|
||
}
|
||
pub fn set_viewport_org(&mut self, x: f64, y: f64) {
|
||
self.dc.vp_org = Vec2::new(x, y);
|
||
}
|
||
pub fn set_viewport_ext(&mut self, x: f64, y: f64) {
|
||
self.dc.vp_ext = Vec2::new(x, y);
|
||
}
|
||
pub fn scale_ext(&mut self, window: bool, x: (f64, f64), y: (f64, f64)) {
|
||
let e = if window { &mut self.dc.win_ext } else { &mut self.dc.vp_ext };
|
||
if x.1 != 0.0 && y.1 != 0.0 {
|
||
*e = Vec2::new(e.x * x.0 / x.1, e.y * y.0 / y.1);
|
||
}
|
||
}
|
||
/// `SetWorldTransform` / `ModifyWorldTransform` (`mode`: 1 identity, 2 left, 3 right, 4 set).
|
||
pub fn world(&mut self, x: Affine, mode: u32) {
|
||
if !x.as_coeffs().iter().all(|v| v.is_finite()) {
|
||
return;
|
||
}
|
||
self.dc.world = match mode {
|
||
1 => Affine::IDENTITY,
|
||
2 => self.dc.world * x,
|
||
3 => x * self.dc.world,
|
||
_ => x,
|
||
};
|
||
}
|
||
|
||
// ---------- state ----------
|
||
|
||
pub fn set_fill_mode(&mut self, alternate: bool) {
|
||
self.dc.fill_rule = if alternate { FillRule::EvenOdd } else { FillRule::NonZero };
|
||
}
|
||
pub fn set_miter(&mut self, m: f64) {
|
||
if m.is_finite() && m >= 1.0 {
|
||
self.dc.miter = m;
|
||
}
|
||
}
|
||
pub fn set_arc_ccw(&mut self, ccw: bool) {
|
||
self.dc.arc_ccw = ccw;
|
||
}
|
||
pub fn set_text_color(&mut self, c: [u8; 3]) {
|
||
self.dc.text_color = c;
|
||
}
|
||
pub fn set_text_align(&mut self, a: u32) {
|
||
self.dc.text_align = a;
|
||
}
|
||
|
||
pub fn save(&mut self) {
|
||
if self.saved.len() < MAX_SAVED {
|
||
self.saved.push(self.dc.clone());
|
||
}
|
||
}
|
||
|
||
/// `RestoreDC(n)`: a negative `n` counts back from the latest save, a positive one is a save's
|
||
/// number.
|
||
pub fn restore(&mut self, n: i32) {
|
||
let len = self.saved.len();
|
||
let keep = if n < 0 { len.checked_sub(n.unsigned_abs() as usize) } else { (n as usize).checked_sub(1).filter(|k| *k < len) };
|
||
if let Some(k) = keep {
|
||
self.saved.truncate(k + 1);
|
||
if let Some(dc) = self.saved.pop() {
|
||
self.dc = dc;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- objects ----------
|
||
|
||
/// Put `obj` in slot `index` (EMF numbers its objects).
|
||
pub fn create_at(&mut self, index: u32, obj: Obj) {
|
||
let i = index as usize;
|
||
if i == 0 || i > u16::MAX as usize {
|
||
return;
|
||
}
|
||
if self.objects.len() <= i {
|
||
self.objects.resize(i + 1, None);
|
||
}
|
||
if let Some(s) = self.objects.get_mut(i) {
|
||
*s = Some(obj);
|
||
}
|
||
}
|
||
|
||
/// Put `obj` in the lowest free slot (WMF).
|
||
pub fn create(&mut self, obj: Obj) {
|
||
let start = self.free_hint.min(self.objects.len());
|
||
let i = self.objects.iter().skip(start).position(Option::is_none).map_or(self.objects.len(), |p| p + start);
|
||
if i > u16::MAX as usize {
|
||
return;
|
||
}
|
||
if i == self.objects.len() {
|
||
self.objects.push(None);
|
||
}
|
||
if let Some(s) = self.objects.get_mut(i) {
|
||
*s = Some(obj);
|
||
}
|
||
self.free_hint = i + 1;
|
||
}
|
||
|
||
pub fn delete(&mut self, index: u32) {
|
||
if let Some(s) = self.objects.get_mut(index as usize) {
|
||
*s = None;
|
||
self.free_hint = self.free_hint.min(index as usize);
|
||
}
|
||
}
|
||
|
||
/// Select object `index`, or a stock object (EMF: the high bit set).
|
||
pub fn select(&mut self, index: u32) {
|
||
if self.kind == Kind::Emf && index & 0x8000_0000 != 0 {
|
||
match index & 0x7fff_ffff {
|
||
0 | 18 => self.dc.brush = BrushObj::Solid([255; 3]),
|
||
1 => self.dc.brush = BrushObj::Solid([0xc0; 3]),
|
||
2 => self.dc.brush = BrushObj::Solid([0x80; 3]),
|
||
3 => self.dc.brush = BrushObj::Solid([0x40; 3]),
|
||
4 => self.dc.brush = BrushObj::Solid([0; 3]),
|
||
5 => self.dc.brush = BrushObj::Null,
|
||
6 => self.dc.pen = PenObj::solid([255; 3]),
|
||
7 | 19 => self.dc.pen = PenObj::solid([0; 3]),
|
||
8 => self.dc.pen = PenObj::NULL,
|
||
10..=17 => self.dc.font = FontObj::default(),
|
||
_ => {}
|
||
}
|
||
return;
|
||
}
|
||
match self.objects.get(index as usize).cloned().flatten() {
|
||
Some(Obj::Pen(p)) => self.dc.pen = p,
|
||
Some(Obj::Brush(b)) => self.dc.brush = b,
|
||
Some(Obj::Font(f)) => self.dc.font = f,
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
// ---------- output ----------
|
||
|
||
fn push(&mut self, mut node: Node) {
|
||
if self.out.len() >= MAX_NODES {
|
||
return;
|
||
}
|
||
if !node.visual_bounds().is_none_or(sane) {
|
||
return self.warn(FAR_AWAY);
|
||
}
|
||
node.id = self.doc.alloc_id();
|
||
self.out.push((self.dc.clips.clone(), node));
|
||
}
|
||
|
||
fn fill_paint(&mut self) -> Option<Paint> {
|
||
match self.dc.brush.clone() {
|
||
BrushObj::Null => None,
|
||
BrushObj::Solid(c) => Some(rgb(c)),
|
||
BrushObj::Hatched(c) => {
|
||
self.warn(HATCHES);
|
||
Some(rgb(c))
|
||
}
|
||
BrushObj::Pattern => {
|
||
self.warn(PATTERN_BRUSHES);
|
||
Some(rgb([0x80; 3]))
|
||
}
|
||
}
|
||
}
|
||
|
||
fn stroke_layer(&self) -> Option<StrokeLayer> {
|
||
let p = &self.dc.pen;
|
||
let kind = p.style & 0xf;
|
||
if kind == PS_NULL {
|
||
return None;
|
||
}
|
||
// Geometric pens, and other pens wider than 1, have a width in logical units; the others
|
||
// are one pixel wide.
|
||
let w = p.width * self.scale();
|
||
let width = if w > 0.0 && w.is_finite() && (p.style & PS_GEOMETRIC != 0 || p.width > 1.0) { w } else { HAIRLINE };
|
||
let mut st = StrokeLayer::new(rgb(p.color), width);
|
||
// Round caps and joins unless the style asks for others.
|
||
st.cap = match p.style & 0xf00 {
|
||
0x100 => LineCap::Square,
|
||
0x200 => LineCap::Butt,
|
||
_ => LineCap::Round,
|
||
};
|
||
st.join = match p.style & 0xf000 {
|
||
0x1000 => LineJoin::Bevel,
|
||
0x2000 => LineJoin::Miter,
|
||
_ => LineJoin::Round,
|
||
};
|
||
st.miter_limit = self.dc.miter;
|
||
let unit = width.max(HAIRLINE);
|
||
let pattern: Vec<f64> = match kind {
|
||
1 => vec![3.0 * unit, unit],
|
||
2 => vec![unit, unit],
|
||
3 => vec![3.0 * unit, unit, unit, unit],
|
||
4 => vec![3.0 * unit, unit, unit, unit, unit, unit],
|
||
7 => p.entries.iter().map(|e| e * self.scale()).collect(),
|
||
_ => vec![],
|
||
};
|
||
let dash = Dash { pattern, offset: 0.0, align_corners: false };
|
||
st.dash = dash.is_dashed().then_some(dash);
|
||
Some(st)
|
||
}
|
||
|
||
/// Paint `bp` (document space) with the brush and the pen.
|
||
fn draw(&mut self, bp: BezPath, fill: bool, stroke: bool) {
|
||
if bp.elements().is_empty() {
|
||
return;
|
||
}
|
||
let mut items = vec![];
|
||
if fill && let Some(p) = self.fill_paint() {
|
||
items.push(AppearanceItem::Fill(FillLayer::new(p)));
|
||
}
|
||
if stroke && let Some(st) = self.stroke_layer() {
|
||
items.push(AppearanceItem::Stroke(st));
|
||
}
|
||
if items.is_empty() {
|
||
return;
|
||
}
|
||
let mut n = Node::path(vectorcraft_doc::NodeId(0), PathData::from_bezpath(&bp), Appearance { items, ..Appearance::default() });
|
||
if let NodeKind::Path { rule, .. } = &mut n.kind {
|
||
*rule = self.dc.fill_rule;
|
||
}
|
||
self.push(n);
|
||
}
|
||
|
||
/// Draw the lines drawn one after another so far.
|
||
pub fn flush(&mut self) {
|
||
if !self.pending.elements().is_empty() {
|
||
let bp = std::mem::take(&mut self.pending);
|
||
self.draw(bp, false, true);
|
||
}
|
||
}
|
||
|
||
/// A figure in logical units: added to the path being recorded, else drawn (filled when it is
|
||
/// an area).
|
||
pub fn figure(&mut self, mut bp: BezPath, area: bool) {
|
||
bp.apply_affine(self.xf());
|
||
match &mut self.path {
|
||
Some(path) => {
|
||
path.extend(bp);
|
||
self.figure = false;
|
||
}
|
||
None => self.draw(bp, area, true),
|
||
}
|
||
}
|
||
|
||
/// Polygon / Polyline (and Bézier ones): `pts` in logical units.
|
||
pub fn poly(&mut self, pts: &[Point], closed: bool, bezier: bool) {
|
||
let Some((first, rest)) = pts.split_first() else { return };
|
||
let mut bp = BezPath::new();
|
||
bp.move_to(*first);
|
||
if bezier {
|
||
for c in rest.as_chunks::<3>().0 {
|
||
bp.curve_to(c[0], c[1], c[2]);
|
||
}
|
||
} else {
|
||
rest.iter().for_each(|p| bp.line_to(*p));
|
||
}
|
||
if closed {
|
||
bp.close_path();
|
||
}
|
||
self.figure(bp, closed);
|
||
}
|
||
|
||
/// Several polygons (one area) or polylines.
|
||
pub fn poly_poly(&mut self, polys: &[Vec<Point>], closed: bool) {
|
||
let mut bp = BezPath::new();
|
||
for p in polys {
|
||
let Some((first, rest)) = p.split_first() else { continue };
|
||
bp.move_to(*first);
|
||
rest.iter().for_each(|q| bp.line_to(*q));
|
||
if closed {
|
||
bp.close_path();
|
||
}
|
||
}
|
||
self.figure(bp, closed);
|
||
}
|
||
|
||
pub fn move_to(&mut self, p: Point) {
|
||
self.pos = p;
|
||
self.figure = false;
|
||
}
|
||
|
||
/// LineTo / PolylineTo / PolyBezierTo: from the current position through `pts` (logical).
|
||
pub fn poly_to(&mut self, pts: &[Point], bezier: bool) {
|
||
let Some(last) = pts.last().copied() else { return };
|
||
let xf = self.xf();
|
||
let start = xf * self.pos;
|
||
let target = match &mut self.path {
|
||
Some(path) => {
|
||
if !self.figure {
|
||
path.move_to(start);
|
||
self.figure = true;
|
||
self.figure_start = self.pos;
|
||
}
|
||
path
|
||
}
|
||
None => {
|
||
if self.pending.elements().last().and_then(|e| e.end_point()) != Some(start) {
|
||
self.pending.move_to(start);
|
||
}
|
||
&mut self.pending
|
||
}
|
||
};
|
||
if bezier {
|
||
for c in pts.as_chunks::<3>().0 {
|
||
target.curve_to(xf * c[0], xf * c[1], xf * c[2]);
|
||
}
|
||
} else {
|
||
pts.iter().for_each(|p| target.line_to(xf * *p));
|
||
}
|
||
self.pos = last;
|
||
}
|
||
|
||
pub fn arc(&mut self, r: Rect, start: Point, end: Point, kind: ArcKind) {
|
||
let bp = arc_path(r, start, end, self.dc.arc_ccw, kind);
|
||
self.figure(bp, kind != ArcKind::Open);
|
||
}
|
||
|
||
// ---------- paths and clips ----------
|
||
|
||
pub fn begin_path(&mut self) {
|
||
self.path = Some(BezPath::new());
|
||
self.figure = false;
|
||
}
|
||
pub fn end_path(&mut self) {
|
||
// The path stays until it is filled, stroked or made the clip.
|
||
}
|
||
pub fn close_figure(&mut self) {
|
||
if let Some(p) = &mut self.path
|
||
&& self.figure
|
||
{
|
||
p.close_path();
|
||
self.figure = false;
|
||
self.pos = self.figure_start;
|
||
}
|
||
}
|
||
pub fn abort_path(&mut self) {
|
||
self.path = None;
|
||
}
|
||
|
||
/// FillPath / StrokePath / StrokeAndFillPath.
|
||
pub fn paint_path(&mut self, fill: bool, stroke: bool) {
|
||
if let Some(mut bp) = self.path.take() {
|
||
// Filling closes the open figure.
|
||
if fill && self.figure {
|
||
bp.close_path();
|
||
}
|
||
self.draw(bp, fill, stroke);
|
||
}
|
||
}
|
||
|
||
fn push_clip(&mut self, path: BezPath, rule: FillRule, replace: bool) {
|
||
if replace {
|
||
self.dc.clips.clear();
|
||
}
|
||
if !sane(path.bounding_box()) {
|
||
return self.warn(FAR_AWAY);
|
||
}
|
||
// A rectangle around the whole picture (what writers clip to first) clips nothing in it.
|
||
let lines = path.elements().iter().all(|e| !matches!(e, PathEl::QuadTo(..) | PathEl::CurveTo(..)));
|
||
let around = path.bounding_box().inflate(0.5, 0.5);
|
||
if lines && path.elements().len() <= 6 && around.contains(self.frame.origin()) && around.contains(Point::new(self.frame.x1, self.frame.y1)) {
|
||
return;
|
||
}
|
||
if self.dc.clips.len() >= MAX_NEST {
|
||
return self.warn(&deep_clips());
|
||
}
|
||
self.next_clip += 1;
|
||
self.dc.clips.push(Arc::new(Clip { id: self.next_clip, region: Some((path, rule)) }));
|
||
}
|
||
|
||
/// SelectClipPath (`mode`: 1 and, 5 copy; the others cut out, which isn't read).
|
||
pub fn clip_path(&mut self, mode: u32) {
|
||
let Some(bp) = self.path.take() else { return };
|
||
match mode {
|
||
1 => self.push_clip(bp, self.dc.fill_rule, false),
|
||
5 => self.push_clip(bp, self.dc.fill_rule, true),
|
||
_ => self.warn(EXCLUDED_CLIPS),
|
||
}
|
||
}
|
||
|
||
/// IntersectClipRect (logical units).
|
||
pub fn clip_rect(&mut self, r: Rect) {
|
||
let mut bp = shapes::rectangle(r.abs()).to_bezpath();
|
||
bp.apply_affine(self.xf());
|
||
self.push_clip(bp, FillRule::NonZero, false);
|
||
}
|
||
|
||
/// A clip region of `rects` in device units (`mode` as [`Self::clip_path`]); none with mode
|
||
/// copy removes the clip.
|
||
pub fn clip_region(&mut self, rects: &[Rect], mode: u32) {
|
||
if rects.is_empty() {
|
||
if mode == 5 {
|
||
self.dc.clips.clear();
|
||
}
|
||
return;
|
||
}
|
||
let mut bp = BezPath::new();
|
||
for r in rects {
|
||
bp.extend(shapes::rectangle(r.abs()).to_bezpath());
|
||
}
|
||
bp.apply_affine(self.dev_to_doc);
|
||
match mode {
|
||
1 => self.push_clip(bp, FillRule::NonZero, false),
|
||
5 => self.push_clip(bp, FillRule::NonZero, true),
|
||
_ => self.warn(EXCLUDED_CLIPS),
|
||
}
|
||
}
|
||
|
||
pub fn exclude_clip(&mut self) {
|
||
self.warn(EXCLUDED_CLIPS);
|
||
}
|
||
|
||
// ---------- blits, images and text ----------
|
||
|
||
/// A raster operation without a source over `dest` (logical): the brush, white or black.
|
||
pub fn pattern_blit(&mut self, dest: Rect, rop: u32) {
|
||
let brush = match rop {
|
||
PATCOPY => self.dc.brush.clone(),
|
||
WHITENESS => BrushObj::Solid([255; 3]),
|
||
BLACKNESS => BrushObj::Solid([0; 3]),
|
||
_ => return self.skip(),
|
||
};
|
||
let pen = std::mem::replace(&mut self.dc.pen, PenObj::NULL);
|
||
let outer = std::mem::replace(&mut self.dc.brush, brush);
|
||
let mut bp = shapes::rectangle(dest.abs()).to_bezpath();
|
||
bp.apply_affine(self.xf());
|
||
self.draw(bp, true, false);
|
||
self.dc.pen = pen;
|
||
self.dc.brush = outer;
|
||
}
|
||
|
||
/// An image whose source rectangle `src` (pixels, from the top-left) is drawn into `dest`
|
||
/// (logical: origin and signed size) with raster operation `rop`, at `opacity`.
|
||
pub fn image(&mut self, img: Result<Rgba, String>, src: Rect, dest: Rect, rop: u32, opacity: f64) {
|
||
if rop != SRCCOPY {
|
||
self.warn(SKIPPED_ROPS);
|
||
return;
|
||
}
|
||
let img = match img {
|
||
Ok(i) => i,
|
||
Err(_) => return self.warn(BAD_IMAGE),
|
||
};
|
||
let img = crop(img, src);
|
||
if img.width == 0 || img.height == 0 {
|
||
return;
|
||
}
|
||
let Ok(png) = img.to_png() else { return self.warn(BAD_IMAGE) };
|
||
let blob = ImageBlob::new("image/png", png);
|
||
let key = blob.content_key();
|
||
self.doc.images.entry(key.clone()).or_insert(blob);
|
||
let (w, h) = (f64::from(img.width), f64::from(img.height));
|
||
let xf = self.xf() * Affine::translate((dest.x0, dest.y0)) * Affine::scale_non_uniform(dest.width() / w, dest.height() / h);
|
||
if !xf.as_coeffs().iter().all(|v| v.is_finite()) || xf.determinant().abs() < 1e-12 {
|
||
return;
|
||
}
|
||
let im = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() };
|
||
let mut n = Node::new(vectorcraft_doc::NodeId(0), NodeKind::Image(Box::new(im)));
|
||
n.opacity = opacity.clamp(0.0, 1.0) as f32;
|
||
self.push(n);
|
||
}
|
||
|
||
/// Text at `at` (logical; the current position with TA_UPDATECP) in the current font.
|
||
pub fn text(&mut self, at: Point, s: &str) {
|
||
let s: String = s.chars().filter(|c| *c != '\0').collect();
|
||
if s.trim().is_empty() {
|
||
return;
|
||
}
|
||
let align = self.dc.text_align;
|
||
let at = if align & 1 != 0 { self.pos } else { at };
|
||
let f = self.dc.font.clone();
|
||
let k = self.scale();
|
||
// A negative height is the em; a positive one the cell (the em and the internal leading).
|
||
let em = if f.height < 0.0 { -f.height } else { f.height * 0.85 };
|
||
let size = if em > 0.0 && (em * k).is_finite() { (em * k).clamp(0.5, 5000.0) } else { 12.0 };
|
||
let mut style = CharStyle { size, fill: rgb(self.dc.text_color), stroke: Paint::None, ..CharStyle::default() };
|
||
if !f.face.trim().is_empty() {
|
||
style.font_family = f.face.trim().to_string();
|
||
}
|
||
style.font_style = match (f.weight >= 600, f.italic) {
|
||
(true, true) => "Bold Italic",
|
||
(true, false) => "Bold",
|
||
(false, true) => "Italic",
|
||
(false, false) => "Regular",
|
||
}
|
||
.into();
|
||
let angle = -(f.escapement / 10.0).to_radians();
|
||
let angle = if angle.is_finite() { angle } else { 0.0 };
|
||
// TA_BASELINE, TA_BOTTOM or TA_TOP (the default): where the baseline is.
|
||
let drop = match align & 0x18 {
|
||
0x18 => 0.0,
|
||
0x08 => -0.2 * size,
|
||
_ => 0.8 * size,
|
||
};
|
||
let origin = self.xf() * at;
|
||
let mut t = TextObject::point(Point::ZERO, &s, style);
|
||
t.xf = Affine::translate(origin.to_vec2()) * Affine::rotate(angle) * Affine::translate((0.0, drop));
|
||
t.para.justify = match align & 0x06 {
|
||
0x06 => Justify::Center,
|
||
0x02 => Justify::Right,
|
||
_ => Justify::Left,
|
||
};
|
||
t.cached_bounds = Some(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).bounds);
|
||
self.push(Node::new(vectorcraft_doc::NodeId(0), NodeKind::Text(Box::new(t))));
|
||
}
|
||
|
||
// ---------- the document ----------
|
||
|
||
/// The document: what was drawn on one layer, clipped art in clipping groups.
|
||
fn finish(mut self) -> Imported {
|
||
self.flush();
|
||
if self.skipped > 0 {
|
||
let n = self.skipped;
|
||
self.warnings.push(format!(
|
||
"{n} record{} of kinds VectorCraft doesn't read {} skipped",
|
||
if n == 1 { "" } else { "s" },
|
||
if n == 1 { "was" } else { "were" }
|
||
));
|
||
}
|
||
let out = std::mem::take(&mut self.out);
|
||
if out.is_empty() && self.emf_plus {
|
||
self.warnings.push("the picture is drawn with EMF+ records, which aren't read yet: nothing was imported".into());
|
||
}
|
||
let children = nest(&mut self.doc, out);
|
||
let mut layer = Node::layer(self.doc.alloc_id(), "Layer 1", LayerColor::Preset(0));
|
||
if let Some(c) = layer.children_mut() {
|
||
*c = children;
|
||
}
|
||
self.doc.layers = vec![Arc::new(layer)];
|
||
Imported { document: self.doc, kind: self.kind, warnings: self.warnings }
|
||
}
|
||
}
|
||
|
||
/// `img` cut to `src` (pixels from the top-left, within the image).
|
||
fn crop(img: Rgba, src: Rect) -> Rgba {
|
||
let (w, h) = (img.width, img.height);
|
||
let s = src.abs();
|
||
let clamp = |v: f64, max: u32| if v.is_finite() { v.round().clamp(0.0, f64::from(max)) as u32 } else { 0 };
|
||
let (x0, y0, x1, y1) = (clamp(s.x0, w), clamp(s.y0, h), clamp(s.x1, w), clamp(s.y1, h));
|
||
if (x0, y0, x1, y1) == (0, 0, w, h) || x1 <= x0 || y1 <= y0 {
|
||
return img;
|
||
}
|
||
let mut pixels = Vec::with_capacity(((x1 - x0) * (y1 - y0) * 4) as usize);
|
||
for y in y0..y1 {
|
||
let row = (y * w) as usize * 4;
|
||
if let Some(p) = img.pixels.get(row + x0 as usize * 4..row + x1 as usize * 4) {
|
||
pixels.extend_from_slice(p);
|
||
}
|
||
}
|
||
Rgba { width: x1 - x0, height: y1 - y0, pixels }
|
||
}
|
||
|
||
/// Read a metafile.
|
||
pub(crate) fn import(bytes: &[u8]) -> Result<Imported, String> {
|
||
match crate::sniff(bytes) {
|
||
Some(Kind::Emf) => emf::play(bytes),
|
||
Some(Kind::Wmf) => wmf::play(bytes),
|
||
None => Err("not an EMF or WMF file".into()),
|
||
}
|
||
}
|