Compare commits

...
Author SHA1 Message Date
Dylan M. Taylor ac3e46b748 Metafile: read WMF, EMF and EMF+ with craft-metafile; delete crates/metafile
The renderer now draws what the shared reader adds: pen caps, joins and
dashes, clip paths (SELECTCLIPPATH) and turned or skewed bitmaps.
2026-11-12 01:09:45 +00:00
21 changed files with 81 additions and 7555 deletions
Generated
+12 -10
View File
@@ -889,6 +889,17 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "craft-metafile"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=8079878ae2b166c1ea8229e40eff7e08c916e532#8079878ae2b166c1ea8229e40eff7e08c916e532"
dependencies = [
"encoding_rs",
"image",
"kurbo",
"thiserror 2.0.21",
]
[[package]]
name = "craft-pdf"
version = "0.1.0"
@@ -1201,15 +1212,6 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "deckcraft-metafile"
version = "0.18.1"
dependencies = [
"image",
"kurbo",
"thiserror 2.0.21",
]
[[package]]
name = "deckcraft-model"
version = "0.18.1"
@@ -1260,11 +1262,11 @@ dependencies = [
name = "deckcraft-render"
version = "0.18.1"
dependencies = [
"craft-metafile",
"deckcraft-anim",
"deckcraft-color",
"deckcraft-fonts",
"deckcraft-geom",
"deckcraft-metafile",
"deckcraft-model",
"deckcraft-text",
"image",
+2 -1
View File
@@ -14,7 +14,6 @@ repository = "https://github.com/storytold/deckcraft"
# internal
deckcraft-geom = { path = "crates/geom" }
deckcraft-color = { path = "crates/color" }
deckcraft-metafile = { path = "crates/metafile" }
deckcraft-model = { path = "crates/model" }
deckcraft-fonts = { path = "crates/fonts" }
deckcraft-text = { path = "crates/text" }
@@ -58,6 +57,8 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg",
# craft-media and craft-audio share types and stay on one rev.
craft-media = { git = "https://github.com/storytold/craft-libs", rev = "75895c0321d8cb6e175a42466cba050352b117c5", version = "=0.1.0", default-features = false }
craft-audio = { git = "https://github.com/storytold/craft-libs", rev = "75895c0321d8cb6e175a42466cba050352b117c5", version = "=0.1.0", default-features = false, features = ["opus-decode"] }
# WMF, EMF and EMF+ reader.
craft-metafile = { git = "https://github.com/storytold/craft-libs", rev = "8079878ae2b166c1ea8229e40eff7e08c916e532", version = "=0.1.0" }
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] }
# craft-update: app-specific pretend versions apply to startup and manual checks.
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] }
-14
View File
@@ -1,14 +0,0 @@
[package]
name = "deckcraft-metafile"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "DeckCraft metafiles (ported from WordCraft): parses Windows WMF and EMF records into a neutral vector model (paths and bitmaps)"
[lints]
workspace = true
[dependencies]
kurbo = { workspace = true }
thiserror = { workspace = true }
image = { workspace = true }
-59
View File
@@ -1,59 +0,0 @@
//! Bounds-checked little-endian access to hostile input. Every read returns `None` past the end, so
//! a truncated or lying size field stops the parse instead of panicking.
#[derive(Clone, Copy)]
pub(crate) struct Bytes<'a>(pub(crate) &'a [u8]);
impl<'a> Bytes<'a> {
pub fn slice(&self, off: usize, len: usize) -> Option<&'a [u8]> {
self.0.get(off..off.checked_add(len)?)
}
pub fn tail(&self, off: usize) -> Option<Bytes<'a>> {
self.0.get(off..).map(Bytes)
}
fn arr<const N: usize>(&self, off: usize) -> Option<[u8; N]> {
self.slice(off, N)?.try_into().ok()
}
pub fn u16(&self, off: usize) -> Option<u16> {
Some(u16::from_le_bytes(self.arr(off)?))
}
pub fn i16(&self, off: usize) -> Option<i16> {
Some(i16::from_le_bytes(self.arr(off)?))
}
pub fn u32(&self, off: usize) -> Option<u32> {
Some(u32::from_le_bytes(self.arr(off)?))
}
pub fn i32(&self, off: usize) -> Option<i32> {
Some(i32::from_le_bytes(self.arr(off)?))
}
pub fn f32(&self, off: usize) -> Option<f32> {
Some(f32::from_le_bytes(self.arr(off)?))
}
/// `n` consecutive (x, y) pairs of 16- or 32-bit signed integers starting at `off`.
pub fn points(&self, off: usize, n: usize, wide: bool) -> Option<Vec<(f64, f64)>> {
let sz = if wide { 8 } else { 4 };
let raw = self.slice(off, n.checked_mul(sz)?)?;
let mut out = Vec::with_capacity(n);
for c in raw.chunks_exact(sz) {
let p = if wide {
let x = i32::from_le_bytes(c.get(0..4)?.try_into().ok()?);
let y = i32::from_le_bytes(c.get(4..8)?.try_into().ok()?);
(f64::from(x), f64::from(y))
} else {
let x = i16::from_le_bytes(c.get(0..2)?.try_into().ok()?);
let y = i16::from_le_bytes(c.get(2..4)?.try_into().ok()?);
(f64::from(x), f64::from(y))
};
out.push(p);
}
Some(out)
}
}
File diff suppressed because it is too large Load Diff
-193
View File
@@ -1,193 +0,0 @@
//! Clipping regions for EMF and WMF (MS-EMF 2.3.12, MS-WMF 2.3.5).
//!
//! A region is a set of axis-aligned rectangles in device units: `None` is the default clip (the
//! whole surface), `Some([])` draws nothing, and `Some(rects)` is their union. That is how GDI
//! stores a region (`RGNDATA` is a list of rectangles). Counts are capped so a hostile file cannot
//! grow the set without bound; past the cap, extra rectangles are dropped, which can only hide
//! drawing.
/// Rectangles kept from one region. Past this, further rectangles are dropped.
pub(crate) const MAX_CLIP_RECTS: usize = 64;
pub(crate) const RGN_AND: u32 = 1;
pub(crate) const RGN_OR: u32 = 2;
pub(crate) const RGN_XOR: u32 = 3;
pub(crate) const RGN_DIFF: u32 = 4;
pub(crate) const RGN_COPY: u32 = 5;
/// A rectangle `[x0, y0, x1, y1]` with a positive area, or none.
pub(crate) fn normalize(rect: [f64; 4]) -> Option<[f64; 4]> {
let [l, t, r, b] = rect;
if ![l, t, r, b].iter().all(|v| v.is_finite()) {
return None;
}
let (x0, x1) = (l.min(r), l.max(r));
let (y0, y1) = (t.min(b), t.max(b));
(x1 - x0 > 1e-6 && y1 - y0 > 1e-6).then_some([x0, y0, x1, y1])
}
fn intersection(a: [f64; 4], b: [f64; 4]) -> Option<[f64; 4]> {
let (x0, y0) = (a[0].max(b[0]), a[1].max(b[1]));
let (x1, y1) = (a[2].min(b[2]), a[3].min(b[3]));
(x1 - x0 > 1e-6 && y1 - y0 > 1e-6 && [x0, y0, x1, y1].iter().all(|v| v.is_finite())).then_some([x0, y0, x1, y1])
}
/// `a` with `b` removed, as up to four rectangles.
fn subtract_one(a: [f64; 4], b: [f64; 4]) -> Vec<[f64; 4]> {
let Some(i) = intersection(a, b) else { return vec![a] };
let mut out = Vec::new();
if i[1] > a[1]
&& let Some(r) = normalize([a[0], a[1], a[2], i[1]])
{
out.push(r);
}
if i[3] < a[3]
&& let Some(r) = normalize([a[0], i[3], a[2], a[3]])
{
out.push(r);
}
if i[0] > a[0]
&& let Some(r) = normalize([a[0], i[1], i[0], i[3]])
{
out.push(r);
}
if i[2] < a[2]
&& let Some(r) = normalize([i[2], i[1], a[2], i[3]])
{
out.push(r);
}
out
}
fn cap(mut rects: Vec<[f64; 4]>) -> Vec<[f64; 4]> {
rects.truncate(MAX_CLIP_RECTS);
rects
}
fn subtract_all(subject: Vec<[f64; 4]>, cuts: &[[f64; 4]]) -> Vec<[f64; 4]> {
let mut cur = subject;
for cut in cuts {
let mut next = Vec::new();
for s in cur {
for piece in subtract_one(s, *cut) {
if next.len() >= MAX_CLIP_RECTS {
return next;
}
next.push(piece);
}
}
cur = next;
}
cur
}
/// Intersects the current region with one rectangle.
pub(crate) fn intersect(current: Option<Vec<[f64; 4]>>, rect: [f64; 4]) -> Option<Vec<[f64; 4]>> {
let Some(rect) = normalize(rect) else { return Some(Vec::new()) };
Some(match current {
None => vec![rect],
Some(rects) => cap(rects.iter().filter_map(|r| intersection(*r, rect)).collect()),
})
}
/// Removes one rectangle. `surface` is the device bounds, used when the clip is still the default
/// (the whole surface) so the remainder can be named.
pub(crate) fn exclude(current: Option<Vec<[f64; 4]>>, rect: [f64; 4], surface: Option<[f64; 4]>) -> Option<Vec<[f64; 4]>> {
let Some(rect) = normalize(rect) else { return current };
match current {
Some(rects) => Some(subtract_all(rects, &[rect])),
None => surface.and_then(normalize).map(|s| subtract_all(vec![s], &[rect])),
}
}
/// Combines `incoming` with the current region in a region mode (`RGN_*`). An unknown mode leaves
/// the region as it was. `RGN_COPY` replaces it. An `RGN_OR` or `RGN_XOR` against the default clip
/// stays the default clip: the whole surface already covers the incoming rectangles.
pub(crate) fn combine(current: Option<Vec<[f64; 4]>>, incoming: Vec<[f64; 4]>, mode: u32, surface: Option<[f64; 4]>) -> Option<Vec<[f64; 4]>> {
let incoming: Vec<[f64; 4]> = cap(incoming.iter().copied().filter_map(normalize).collect());
match mode {
RGN_COPY => Some(incoming),
RGN_AND => match current {
None => Some(incoming),
Some(rects) => {
let mut out = Vec::new();
for a in &rects {
for b in &incoming {
if out.len() >= MAX_CLIP_RECTS {
return Some(out);
}
if let Some(r) = intersection(*a, *b) {
out.push(r);
}
}
}
Some(out)
}
},
RGN_OR => match current {
None => None,
Some(mut rects) => {
rects.extend(incoming);
Some(cap(rects))
}
},
RGN_DIFF => match current {
Some(rects) => Some(subtract_all(rects, &incoming)),
None => surface.and_then(normalize).map(|s| subtract_all(vec![s], &incoming)),
},
RGN_XOR => {
let base = current?;
let mut out = subtract_all(base.clone(), &incoming);
let rest = subtract_all(incoming, &base);
for r in rest {
if out.len() >= MAX_CLIP_RECTS {
break;
}
out.push(r);
}
Some(out)
}
_ => current,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intersect_shrinks_to_the_overlap() {
let r = intersect(None, [0.0, 0.0, 100.0, 50.0]);
let r = intersect(r, [40.0, 10.0, 80.0, 40.0]);
assert_eq!(r, Some(vec![[40.0, 10.0, 80.0, 40.0]]));
assert_eq!(intersect(r, [0.0, 0.0, 10.0, 10.0]), Some(vec![]));
assert_eq!(intersect(Some(vec![[0.0, 0.0, 10.0, 20.0]]), [40.0, 0.0, 80.0, 20.0]), Some(vec![]));
}
#[test]
fn modes_copy_and_or_diff_and_xor() {
let a = Some(vec![[0.0, 0.0, 100.0, 40.0]]);
assert_eq!(combine(a.clone(), vec![[10.0, 10.0, 30.0, 20.0]], RGN_COPY, None), Some(vec![[10.0, 10.0, 30.0, 20.0]]));
let and = combine(a.clone(), vec![[50.0, 0.0, 150.0, 40.0]], RGN_AND, None);
assert_eq!(and, Some(vec![[50.0, 0.0, 100.0, 40.0]]));
let or = combine(a.clone(), vec![[200.0, 0.0, 210.0, 10.0]], RGN_OR, None).unwrap();
assert_eq!(or.len(), 2);
let diff = combine(a.clone(), vec![[20.0, 0.0, 40.0, 40.0]], RGN_DIFF, None).unwrap();
assert_eq!(diff, vec![[0.0, 0.0, 20.0, 40.0], [40.0, 0.0, 100.0, 40.0]]);
let xor = combine(a, vec![[80.0, 0.0, 120.0, 40.0]], RGN_XOR, None).unwrap();
assert!(xor.contains(&[0.0, 0.0, 80.0, 40.0]));
assert!(xor.contains(&[100.0, 0.0, 120.0, 40.0]));
}
#[test]
fn excluding_from_the_default_clip_uses_the_surface() {
let r = exclude(None, [10.0, 0.0, 20.0, 40.0], Some([0.0, 0.0, 40.0, 40.0])).unwrap();
assert_eq!(r, vec![[0.0, 0.0, 10.0, 40.0], [20.0, 0.0, 40.0, 40.0]]);
assert_eq!(exclude(None, [10.0, 0.0, 20.0, 40.0], None), None);
}
#[test]
fn a_zero_area_rectangle_clears_an_intersection() {
assert_eq!(intersect(Some(vec![[0.0, 0.0, 10.0, 10.0]]), [5.0, 5.0, 5.0, 8.0]), Some(vec![]));
}
}
-357
View File
@@ -1,357 +0,0 @@
//! Clipping drawn output to a convex device polygon (EMF+ clip rectangles, and clip paths and
//! regions that are convex; others are reduced to their bounding rectangle). Shapes wholly inside
//! pass unchanged; shapes crossing the edge are flattened and cut: fills with Sutherland–Hodgman
//! per subpath, outlines with Cyrus–Beck per segment. Bitmaps are cropped (and masked outside a
//! non-rectangular polygon) and text gets the polygon's bounding rectangle.
use crate::canvas::Raw;
use crate::dib::Rgba;
use crate::geom::{Op, P};
/// Line segments per Bézier when a crossing shape is flattened.
const CURVE_STEPS: usize = 16;
/// Point-against-edge tests one clipped item may cost (all its subpaths together); an item over
/// it is dropped, as a single polygon over it already was.
const MAX_CLIP_WORK: usize = 1_000_000;
/// The same allowance for intersecting two clip regions, a budget of its own: two convex
/// 1,000-gons cost about 1.5 million tests once the cut polygon has grown.
pub(crate) const MAX_AREA_CLIP_WORK: usize = 4 * MAX_CLIP_WORK;
/// The bounding box `[l, t, r, b]` of the points in `ops` (control points included).
pub(crate) fn ops_bbox(ops: &[Op]) -> Option<[f64; 4]> {
let mut b: Option<[f64; 4]> = None;
let mut add = |p: P| {
let v = b.get_or_insert([p.0, p.1, p.0, p.1]);
*v = [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)];
};
for op in ops {
match *op {
Op::M(p) | Op::L(p) => add(p),
Op::C(a, c, d) => {
add(a);
add(c);
add(d);
}
Op::Z => {}
}
}
b
}
pub(crate) fn intersect(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
let r = [a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])];
// An empty intersection stays empty (zero size) so it clips everything away.
if r[2] < r[0] || r[3] < r[1] { [r[0], r[1], r[0], r[1]] } else { r }
}
/// A rectangle `[l, t, r, b]` as a polygon.
pub(crate) fn rect_poly([l, t, r, b]: [f64; 4]) -> Vec<P> {
vec![(l, t), (r, t), (r, b), (l, b)]
}
/// The bounding box of a point list.
pub(crate) fn pts_bbox(pts: &[P]) -> Option<[f64; 4]> {
let (&f, rest) = pts.split_first()?;
Some(rest.iter().fold([f.0, f.1, f.0, f.1], |v, p| [v[0].min(p.0), v[1].min(p.1), v[2].max(p.0), v[3].max(p.1)]))
}
fn cross(a: P, b: P, p: P) -> f64 {
(b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0)
}
/// A convex clip polygon with positive orientation, or None when `pts` is not convex (or
/// degenerate); duplicate and collinear points are dropped.
pub(crate) fn convex(pts: &[P]) -> Option<Vec<P>> {
let mut v: Vec<P> = Vec::with_capacity(pts.len());
for &p in pts {
if !(p.0.is_finite() && p.1.is_finite()) {
return None;
}
if v.last().is_none_or(|q: &P| (q.0 - p.0).abs() > 1e-9 || (q.1 - p.1).abs() > 1e-9) {
v.push(p);
}
}
while v.len() > 1 && v.first() == v.last() {
v.pop();
}
let area: f64 = (0..v.len()).map(|i| cross((0.0, 0.0), v[i], v[(i + 1) % v.len()])).sum();
if v.len() < 3 || area.abs() < 1e-12 {
return None;
}
if area < 0.0 {
v.reverse();
}
let n = v.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let c = cross(v[(i + n - 1) % n], v[i], v[(i + 1) % n]);
if c < -1e-9 * (1.0 + area.abs()) {
return None;
}
if c.abs() > 1e-12 {
out.push(v[i]);
}
}
(out.len() >= 3).then_some(out)
}
fn inside_poly(clip: &[P], p: P) -> bool {
(0..clip.len()).all(|i| cross(clip[i], clip[(i + 1) % clip.len()], p) >= -1e-9)
}
/// Whether the clip polygon is an axis-aligned rectangle.
fn is_rect(clip: &[P]) -> bool {
pts_bbox(clip).is_some_and(|b| clip.len() == 4 && clip.iter().all(|p| (p.0 == b[0] || p.0 == b[2]) && (p.1 == b[1] || p.1 == b[3])))
}
/// Sutherland–Hodgman: `poly` cut by convex `clip`, charging each point visited against each clip
/// edge to `budget`: the polygon
/// can gain a vertex at every edge, so its cost isn't known from its input size alone. `None`
/// once the budget is spent.
pub(crate) fn clip_polygon_within(poly: &[P], clip: &[P], budget: &mut usize) -> Option<Vec<P>> {
let mut poly = poly.to_vec();
for i in 0..clip.len() {
// The work actually done: this edge visits the polygon as it now stands.
if poly.len() > *budget {
return None;
}
*budget -= poly.len();
let (a, b) = (clip[i], clip[(i + 1) % clip.len()]);
let keep = |p: P| cross(a, b, p) >= 0.0;
let cut = |p: P, q: P| {
let (cp, cq) = (cross(a, b, p), cross(a, b, q));
let t = cp / (cp - cq);
(p.0 + (q.0 - p.0) * t, p.1 + (q.1 - p.1) * t)
};
let mut next = Vec::with_capacity(poly.len() + 4);
for j in 0..poly.len() {
let (p, q) = (poly[(j + poly.len() - 1) % poly.len()], poly[j]);
match (keep(p), keep(q)) {
(true, true) => next.push(q),
(true, false) => next.push(cut(p, q)),
(false, true) => {
next.push(cut(p, q));
next.push(q);
}
(false, false) => {}
}
}
poly = next;
if poly.is_empty() {
break;
}
}
Some(poly)
}
fn disjoint(b: [f64; 4], c: [f64; 4]) -> bool {
b[2] < c[0] || b[0] > c[2] || b[3] < c[1] || b[1] > c[3] || c[2] <= c[0] || c[3] <= c[1]
}
fn bbox_inside(b: [f64; 4], clip: &[P]) -> bool {
rect_poly(b).into_iter().all(|p| inside_poly(clip, p))
}
/// Clips one drawn item to a convex device polygon; a shape that is both filled and stroked comes
/// back as two items. An empty polygon clips everything away.
pub(crate) fn clip_raw(raw: Raw, clip: &[P]) -> Vec<Raw> {
let Some(cb) = pts_bbox(clip).filter(|_| clip.len() >= 3) else { return Vec::new() };
match raw {
Raw::Path { ops, fill, stroke, even_odd, region } => {
let Some(b) = ops_bbox(&ops) else { return Vec::new() };
if bbox_inside(b, clip) {
return vec![Raw::Path { ops, fill, stroke, even_odd, region }];
}
if disjoint(b, cb) {
return Vec::new();
}
let subs = flatten(&ops);
// One allowance for the fill and the stroke together; the item goes once it is spent.
let mut budget = MAX_CLIP_WORK;
let mut out = Vec::new();
if fill.is_some() {
let Some(f) = clip_fill(&subs, clip, &mut budget) else { return Vec::new() };
if !f.is_empty() {
out.push(Raw::Path { ops: f, fill, stroke: None, even_odd, region: region.clone() });
}
}
if stroke.is_some() {
let Some(s) = clip_stroke(&subs, clip, &mut budget) else { return Vec::new() };
if !s.is_empty() {
out.push(Raw::Path { ops: s, fill: None, stroke, even_odd, region });
}
}
out
}
Raw::Pattern { ops, even_odd, tile, region } => {
let Some(b) = ops_bbox(&ops) else { return Vec::new() };
if bbox_inside(b, clip) {
return vec![Raw::Pattern { ops, even_odd, tile, region }];
}
if disjoint(b, cb) {
return Vec::new();
}
let subs = flatten(&ops);
let f = clip_fill(&subs, clip, &mut { MAX_CLIP_WORK }).unwrap_or_default();
if f.is_empty() { Vec::new() } else { vec![Raw::Pattern { ops: f, even_odd, tile, region }] }
}
Raw::Bitmap { rect, img, region } => crop(rect, img, clip).map(|(rect, img)| vec![Raw::Bitmap { rect, img, region }]).unwrap_or_default(),
Raw::Text { text: mut t, region } => {
// Text clips to a rectangle: the one around the polygon.
let cur = t.clip.map_or(cb, |[a, b, d, e]| intersect(cb, [f64::from(a), f64::from(b), f64::from(d), f64::from(e)]));
t.clip = Some(cur.map(|v| v as f32));
vec![Raw::Text { text: t, region }]
}
}
}
/// Subpaths as point lists with their closed flag.
pub(crate) fn flatten(ops: &[Op]) -> Vec<(Vec<P>, bool)> {
let mut subs: Vec<(Vec<P>, bool)> = Vec::new();
for op in ops {
match *op {
Op::M(p) => subs.push((vec![p], false)),
Op::L(p) => match subs.last_mut() {
Some(s) => s.0.push(p),
None => subs.push((vec![p], false)),
},
Op::C(a, b, d) => {
let Some(s) = subs.last_mut() else { continue };
let Some(&p0) = s.0.last() else { continue };
for i in 1..=CURVE_STEPS {
let t = i as f64 / CURVE_STEPS as f64;
let u = 1.0 - t;
let k = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t];
s.0.push((k[0] * p0.0 + k[1] * a.0 + k[2] * b.0 + k[3] * d.0, k[0] * p0.1 + k[1] * a.1 + k[2] * b.1 + k[3] * d.1));
}
}
Op::Z => {
if let Some(s) = subs.last_mut() {
s.1 = true;
// Drawing after a close starts from the subpath's first point.
let first = s.0.first().copied();
if let Some(f) = first {
subs.push((vec![f], false));
}
}
}
}
}
subs.retain(|s| s.0.len() > 1);
subs
}
/// The clipped fill of every subpath, its clipping work charged to `budget` with that of the
/// stroke: `None` (the whole item goes) once it is spent.
fn clip_fill(subs: &[(Vec<P>, bool)], clip: &[P], budget: &mut usize) -> Option<Vec<Op>> {
let mut ops = Vec::new();
for (pts, _) in subs {
let poly = clip_polygon_within(pts, clip, budget)?;
if poly.len() >= 3 {
ops.push(Op::M(poly[0]));
ops.extend(poly[1..].iter().map(|p| Op::L(*p)));
ops.push(Op::Z);
}
}
Some(ops)
}
/// The clipped outline of every subpath; `None` once `budget` is spent.
fn clip_stroke(subs: &[(Vec<P>, bool)], clip: &[P], budget: &mut usize) -> Option<Vec<Op>> {
let mut ops = Vec::new();
for (pts, closed) in subs {
let mut pts = pts.clone();
if *closed && let Some(&f) = pts.first() {
pts.push(f);
}
let mut last: Option<P> = None;
for w in pts.windows(2) {
let Some((a, b)) = segment(w[0], w[1], clip, budget)? else {
last = None;
continue;
};
if last != Some(a) {
ops.push(Op::M(a));
}
ops.push(Op::L(b));
last = Some(b);
}
}
Some(ops)
}
/// Cyrus–Beck: the part of segment `a`–`b` inside convex `clip`, one unit of `budget` per clip edge
/// visited; the outer `None` is a spent budget.
fn segment(a: P, b: P, clip: &[P], budget: &mut usize) -> Option<Option<(P, P)>> {
let d = (b.0 - a.0, b.1 - a.1);
let (mut t0, mut t1) = (0.0f64, 1.0f64);
for i in 0..clip.len() {
*budget = budget.checked_sub(1)?;
let (e0, e1) = (clip[i], clip[(i + 1) % clip.len()]);
// Inside is cross(e0, e1, p) >= 0, linear in t along the segment.
let f0 = cross(e0, e1, a);
let df = (e1.0 - e0.0) * d.1 - (e1.1 - e0.1) * d.0;
if df == 0.0 {
if f0 < 0.0 {
return Some(None);
}
} else {
let t = -f0 / df;
if df > 0.0 {
t0 = t0.max(t);
} else {
t1 = t1.min(t);
}
}
}
Some((t0 <= t1).then_some(((a.0 + d.0 * t0, a.1 + d.1 * t0), (a.0 + d.0 * t1, a.1 + d.1 * t1))))
}
/// Crops a bitmap drawn into `rect` = `[x, y, w, h]` (device, positive extents) to the clip
/// polygon's box; pixels outside a non-rectangular polygon turn transparent.
fn crop(rect: [f64; 4], img: Rgba, clip: &[P]) -> Option<([f64; 4], Rgba)> {
let [x, y, w, h] = rect;
let b = [x, y, x + w, y + h];
if bbox_inside(b, clip) {
return Some((rect, img));
}
let c = pts_bbox(clip)?;
if disjoint(b, c) || w <= 0.0 || h <= 0.0 || img.w == 0 || img.h == 0 {
return None;
}
let i = intersect(b, c);
let (kx, ky) = (img.w as f64 / w, img.h as f64 / h);
let x0 = (((i[0] - x) * kx).floor().max(0.0) as usize).min(img.w);
let x1 = (((i[2] - x) * kx).ceil().max(0.0) as usize).min(img.w);
let y0 = (((i[1] - y) * ky).floor().max(0.0) as usize).min(img.h);
let y1 = (((i[3] - y) * ky).ceil().max(0.0) as usize).min(img.h);
if x1 <= x0 || y1 <= y0 {
return None;
}
let mask = !is_rect(clip);
let total_pixels = (x1 - x0).saturating_mul(y1 - y0);
if mask && total_pixels.saturating_mul(clip.len()) > 5_000_000 {
return None;
}
let mut px = Vec::with_capacity((x1 - x0) * (y1 - y0) * 4);
for row in y0..y1 {
// Rows are top-down unless the image says otherwise.
let r = if img.bottom_up { img.h - 1 - row } else { row };
let s = (r * img.w + x0) * 4;
let start = px.len();
px.extend_from_slice(img.px.get(s..s + (x1 - x0) * 4)?);
if mask {
let cy = y + (row as f64 + 0.5) / ky;
for (k, p) in px[start..].as_chunks_mut::<4>().0.iter_mut().enumerate() {
let cx = x + ((x0 + k) as f64 + 0.5) / kx;
if !inside_poly(clip, (cx, cy)) {
p[3] = 0;
}
}
}
}
let out = [x + x0 as f64 / kx, y + y0 as f64 / ky, (x1 - x0) as f64 / kx, (y1 - y0) as f64 / ky];
Some((out, Rgba { w: x1 - x0, h: y1 - y0, bottom_up: false, px }))
}
-259
View File
@@ -1,259 +0,0 @@
//! Device-independent bitmaps. BITMAPINFOHEADER DIBs at 1/4/8/16/24/32 bpp with BI_RGB or BI_BITFIELDS
//! decode to RGBA8 rows, top-down. Compressed (RLE, JPEG, PNG) and V4/V5-only features are not decoded.
use crate::{MAX_PIXELS, bytes::Bytes};
const BI_RGB: u32 = 0;
const BI_BITFIELDS: u32 = 3;
/// Most palette entries a DIB can carry (the colour table never exceeds 2^8).
const MAX_PALETTE: usize = 256;
const ROP_SRCAND: u32 = 0x0088_00C6;
const ROP_SRCPAINT: u32 = 0x00EE_0086;
const ROP_SRCINVERT: u32 = 0x0066_0046;
/// A decoded picture. `bottom_up` records the DIB's native row order, which source rectangles use.
pub(crate) struct Rgba {
pub w: usize,
pub h: usize,
pub bottom_up: bool,
pub px: Vec<u8>,
}
/// How a decoded bitmap combines with the destination.
#[derive(Clone, Copy)]
pub(crate) enum Blend {
/// A raster operation (BITBLT, STRETCHDIB and friends).
Rop(u32),
/// EMR_ALPHABLEND: a constant alpha (0..=255), times the per-pixel alpha when `per_pixel` is set
/// (AC_SRC_ALPHA: the source colours are premultiplied by it).
Alpha { constant: u8, per_pixel: bool },
}
impl Blend {
/// Whether the decoder must keep the 32-bit alpha byte of each pixel.
pub fn per_pixel(self) -> bool {
matches!(self, Blend::Alpha { per_pixel: true, .. })
}
}
struct Header {
w: usize,
h: usize,
top_down: bool,
bpp: usize,
masks: [u32; 3],
pal_off: usize,
pal_n: usize,
}
impl Header {
fn read(info: Bytes) -> Option<Header> {
let size = info.u32(0)? as usize;
let w = info.i32(4)?;
let h = info.i32(8)?;
let bpp = info.u16(14)? as usize;
let comp = info.u32(16)?;
let clr_used = info.u32(32)? as usize;
if size < 40 || w <= 0 || h == 0 || !matches!(bpp, 1 | 4 | 8 | 16 | 24 | 32) || !matches!(comp, BI_RGB | BI_BITFIELDS) {
return None;
}
let masks = if comp == BI_BITFIELDS {
[info.u32(40)?, info.u32(44)?, info.u32(48)?]
} else if bpp == 16 {
[0x7C00, 0x03E0, 0x001F]
} else {
[0xFF_0000, 0xFF00, 0xFF]
};
let masks_len = if comp == BI_BITFIELDS && size == 40 { 12 } else { 0 };
let pal_off = size.checked_add(masks_len)?;
// Indexed formats take 2^bpp entries when clrUsed is 0. Deeper formats carry only the
// entries clrUsed names (WMF DIBs can have them), still capped.
let pal_n = match (bpp <= 8, clr_used) {
(true, 0) => 1usize << bpp,
(true, n) => n.min(1usize << bpp),
(false, n) => n.min(MAX_PALETTE),
};
Some(Header { w: w as usize, h: h.unsigned_abs() as usize, top_down: h < 0, bpp, masks, pal_off, pal_n })
}
}
/// Byte length of the info part (header, masks, palette) of a DIB, so the pixel bits follow it.
pub(crate) fn info_len(dib: Bytes) -> Option<usize> {
let h = Header::read(dib)?;
h.pal_off.checked_add(h.pal_n.checked_mul(4)?)
}
/// Decodes a DIB given as its info part (header plus masks and palette) and its pixel bits. The
/// 32-bit alpha byte is kept only when `keep_alpha` is set (per-pixel alpha blends); otherwise pixels
/// are opaque.
pub(crate) fn decode(info: Bytes, bits: Bytes, keep_alpha: bool) -> Option<Rgba> {
let hd = Header::read(info)?;
let pixels = hd.w.checked_mul(hd.h)?;
if pixels == 0 || pixels > MAX_PIXELS {
return None;
}
let stride = hd.w.checked_mul(hd.bpp)?.checked_add(31)? / 32 * 4;
let data = bits.slice(0, stride.checked_mul(hd.h)?)?;
let pal = info.slice(hd.pal_off, hd.pal_n * 4)?;
let mut px = vec![0u8; pixels * 4];
for y in 0..hd.h {
let src_row = if hd.top_down { y } else { hd.h - 1 - y };
let row = src_row * stride;
for x in 0..hd.w {
let p = pixel(data, row, x, &hd, pal, keep_alpha)?;
let o = (y * hd.w + x) * 4;
px.get_mut(o..o + 4)?.copy_from_slice(&p);
}
}
Some(Rgba { w: hd.w, h: hd.h, bottom_up: !hd.top_down, px })
}
fn pixel(data: &[u8], row: usize, x: usize, hd: &Header, pal: &[u8], keep_alpha: bool) -> Option<[u8; 4]> {
let [m_r, m_g, m_b] = hd.masks;
let [r, g, b] = match hd.bpp {
1 => {
let b = *data.get(row + x / 8)?;
palette(pal, usize::from((b >> (7 - x % 8)) & 1))
}
4 => {
let b = *data.get(row + x / 2)?;
palette(pal, usize::from(if x.is_multiple_of(2) { b >> 4 } else { b & 0x0F }))
}
8 => palette(pal, usize::from(*data.get(row + x)?)),
24 => {
let [b, g, r] = data.get(row + x * 3..row + x * 3 + 3)?.try_into().ok()?;
[r, g, b]
}
16 => {
let o = row + x * 2;
let v = u32::from(u16::from_le_bytes(data.get(o..o + 2)?.try_into().ok()?));
[field(v, m_r), field(v, m_g), field(v, m_b)]
}
_ => {
let o = row + x * 4;
let v = u32::from_le_bytes(data.get(o..o + 4)?.try_into().ok()?);
let a = if keep_alpha { (v >> 24) as u8 } else { 255 };
return Some([field(v, m_r), field(v, m_g), field(v, m_b), a]);
}
};
Some([r, g, b, 255])
}
/// Palette entries are BGRX quads; an index past the table reads as black rather than failing the image.
fn palette(pal: &[u8], i: usize) -> [u8; 3] {
pal.get(i * 4..i * 4 + 4).and_then(|c| <[u8; 4]>::try_from(c).ok()).map_or([0, 0, 0], |c| [c[2], c[1], c[0]])
}
/// Extracts one channel from a masked pixel value and scales it to 8 bits.
fn field(v: u32, mask: u32) -> u8 {
if mask == 0 {
return 0;
}
let shift = mask.trailing_zeros();
let max = u64::from(mask >> shift);
let val = u64::from((v & mask) >> shift);
((val * 255 + max / 2) / max) as u8
}
/// Clips a source rectangle `src` = `[x, y, w, h]` to the bitmap and returns the cropped pixels (top-down)
/// with the part of the destination `dst` that they fill. `src` is in DIB pixel coordinates (`y` counted
/// from the DIB's origin, so bottom-up images count from the bottom); a non-positive source size means the
/// whole bitmap. Clipping shrinks the destination by the same ratio and shifts it by the clipped-off part,
/// so the rest of the image keeps its place. A negative destination extent is kept (the caller mirrors).
pub(crate) fn clip_blit(img: &Rgba, src: [i64; 4], dst: [f64; 4]) -> Option<(Rgba, [f64; 4])> {
let (iw, ih) = (img.w as i64, img.h as i64);
let [sx, sy, sw, sh] = if src[2] > 0 && src[3] > 0 { src } else { [0, 0, iw, ih] };
// The request's top row in top-down order.
let top = if img.bottom_up { ih.saturating_sub(sy).saturating_sub(sh) } else { sy };
let (x0, x1) = (sx.max(0), sx.saturating_add(sw).min(iw));
let (r0, r1) = (top.max(0), top.saturating_add(sh).min(ih));
if x1 <= x0 || r1 <= r0 {
return None;
}
let (kx, ky) = (dst[2] / sw as f64, dst[3] / sh as f64);
let out = [dst[0] + (x0 - sx) as f64 * kx, dst[1] + (r0 - top) as f64 * ky, (x1 - x0) as f64 * kx, (r1 - r0) as f64 * ky];
let (x0, x1, r0, r1) = (x0 as usize, x1 as usize, r0 as usize, r1 as usize);
let (ow, oh) = (x1 - x0, r1 - r0);
let mut px = Vec::with_capacity(ow * oh * 4);
for row in r0..r1 {
let s = (row * img.w + x0) * 4;
px.extend_from_slice(img.px.get(s..s + ow * 4)?);
}
Some((Rgba { w: ow, h: oh, bottom_up: false, px }, out))
}
/// Applies a blend to decoded pixels before they are drawn. The picture has no destination pixels, so
/// raster operations are drawn as they look over white paper: SRCAND (destination AND source) leaves
/// white source pixels out and draws the rest; SRCPAINT and SRCINVERT leave black ones out. Other
/// operations draw the source as copied. Constant alpha scales every pixel's alpha, after a
/// per-pixel (premultiplied) source is made straight.
pub(crate) fn prepare(img: &mut Rgba, blend: Blend) {
match blend {
Blend::Rop(ROP_SRCAND) => clear_where(img, [255, 255, 255]),
Blend::Rop(ROP_SRCPAINT | ROP_SRCINVERT) => clear_where(img, [0, 0, 0]),
Blend::Rop(_) => {}
Blend::Alpha { constant, per_pixel } => {
for p in img.px.as_chunks_mut::<4>().0 {
if per_pixel {
unpremultiply(p);
}
p[3] = (u16::from(p[3]) * u16::from(constant) / 255) as u8;
}
}
}
}
/// Makes the pixels of colour `rgb` transparent: where a raster operation keeps the destination.
fn clear_where(img: &mut Rgba, rgb: [u8; 3]) {
for p in img.px.as_chunks_mut::<4>().0 {
if p[..3] == rgb {
p[3] = 0;
}
}
}
/// AC_SRC_ALPHA sources are premultiplied; the model holds straight RGBA, so divide the colour by the
/// alpha (rounded, capped at 255). A transparent pixel keeps its colour bytes.
fn unpremultiply(p: &mut [u8; 4]) {
let a = u32::from(p[3]);
if a == 0 || a == 255 {
return;
}
for c in &mut p[..3] {
*c = ((u32::from(*c) * 255 + a / 2) / a).min(255) as u8;
}
}
/// Mirrors a decoded image left-right and/or top-bottom. Rows come from the buffer as chunks, so a
/// short buffer flips only the rows it has.
pub(crate) fn flip(img: &mut Rgba, horizontal: bool, vertical: bool) {
let (w, h) = (img.w, img.h);
if w == 0 || h == 0 {
return;
}
let row = w * 4;
if horizontal {
for line in img.px.chunks_exact_mut(row) {
for x in 0..w / 2 {
for c in 0..4 {
line.swap(x * 4 + c, (w - 1 - x) * 4 + c);
}
}
}
}
if vertical {
// Rows the buffer actually holds: a short buffer flips only those.
let n = h.min(img.px.len() / row);
for y in 0..n / 2 {
swap_rows(&mut img.px, y * row, (n - 1 - y) * row, row);
}
}
}
/// Swaps the `len` bytes at `a` with those at `b` (`a < b`); does nothing when either is out of range.
fn swap_rows(px: &mut [u8], a: usize, b: usize, len: usize) {
let Some((head, tail)) = px.split_at_mut_checked(b) else { return };
if let (Some(top), Some(bottom)) = (head.get_mut(a..a.saturating_add(len)), tail.get_mut(..len)) {
top.swap_with_slice(bottom);
}
}
-478
View File
@@ -1,478 +0,0 @@
//! EMF reader (MS-EMF). Records are (type, size in bytes) followed by a payload; offsets inside a
//! record (bitmap info and bits) are relative to the record start, so each handler reads the record slice.
use std::sync::Arc;
use crate::bytes::Bytes;
use crate::canvas::{BS_HATCHED, BS_NULL, Brush, Canvas, LogFont, Obj, PS_NULL, PS_STYLE_MASK, Pen, Xform, colorref};
use crate::dib::{self, Blend};
use crate::geom::{self, ArcKind, P};
use crate::text::{ETO_GLYPH_INDEX, ETO_PDY, Font, MAX_CHARS, decode_bytes, decode_utf16};
use crate::{Error, Frame, MAX_PATH_OPS};
/// ExtTextOut options: no rectangle in the record (EMR_SMALLTEXTOUT, EmrText), and 8-bit
/// characters (EMR_SMALLTEXTOUT).
const ETO_NO_RECT: u32 = 0x0100;
const ETO_SMALL_CHARS: u32 = 0x0200;
const EMF_SIGNATURE: u32 = 0x464D_4520;
/// Hundredths of a millimetre to points.
const HMM_TO_PT: f64 = 72.0 / 2540.0;
/// SRCCOPY, the plain copy of a bitmap.
const SRCCOPY: u32 = 0x00CC_0020;
/// LogPenEx pen type bits; PS_COSMETIC pens are one device pixel wide.
const PS_TYPE_MASK: u32 = 0x000F_0000;
const PS_COSMETIC: u32 = 0;
pub(crate) fn is_emf(b: Bytes) -> bool {
b.u32(0) == Some(1) && b.u32(40) == Some(EMF_SIGNATURE)
}
/// The picture frame: `rclFrame` (0.01 mm) mapped to device units through `szlDevice` and
/// `szlMillimeters`, sized in points. Without a usable frame, the device bounds stand in (1 unit = 1 point).
pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> {
if !is_emf(input) {
return Err(Error::BadHeader);
}
let hsize = input.u32(4).ok_or(Error::BadHeader)? as usize;
let bounds = rect32(input, 8).ok_or(Error::BadHeader)?;
let [l, t, r, b] = bounds;
let frame = frame_from_header(input).or_else(|| (r > l && b > t).then_some(Frame { rect: [l, t, r - l, b - t], size: None }));
let mut cv = Canvas::new();
if r > l && b > t {
cv.set_clip_surface([l, t, r, b]);
}
if let Some(px_mm) = device_px_per_mm(input) {
cv.set_device_mm(px_mm);
}
let mut plus = crate::emfplus::Plus::new();
plus.frame = frame.as_ref().map(|f| f.rect);
let mut parsed = 0usize;
let mut pos = hsize.max(88);
while !cv.full() {
let (Some(typ), Some(size)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u32(o))) else { break };
let size = size as usize;
if size < 8 || !size.is_multiple_of(4) {
break;
}
let Some(rec) = input.slice(pos, size) else { break };
if typ == 14 {
break;
}
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;
}
if parsed == 0 {
return Err(Error::NoRecords);
}
Ok((cv, frame))
}
/// Device pixels per millimetre on each axis, from `szlDevice` (72) and `szlMillimeters` (80).
fn device_px_per_mm(input: Bytes) -> Option<(f64, f64)> {
let (dw, dh) = (input.i32(72)?, input.i32(76)?);
let (mw, mh) = (input.i32(80)?, input.i32(84)?);
(dw > 0 && dh > 0 && mw > 0 && mh > 0).then(|| (f64::from(dw) / f64::from(mw), f64::from(dh) / f64::from(mh)))
}
/// The frame from `rclFrame` (offset 24), `szlDevice` (72) and `szlMillimeters` (80) in the header.
fn frame_from_header(input: Bytes) -> Option<Frame> {
let [fl, ft, fr, fb] = rect32(input, 24)?;
if fr <= fl || fb <= ft {
return None;
}
// Device units per hundredth of a millimetre.
let (px_mm_x, px_mm_y) = device_px_per_mm(input)?;
let (sx, sy) = (px_mm_x / 100.0, px_mm_y / 100.0);
let rect = [fl * sx, ft * sy, (fr - fl) * sx, (fb - ft) * sy];
Some(Frame { rect, size: Some(((fr - fl) * HMM_TO_PT, (fb - ft) * HMM_TO_PT)) })
}
fn record(cv: &mut Canvas, typ: u32, r: Bytes) {
if cv.full() {
return;
}
let _ = match typ {
9 => xy32(r, 8).map(|p| cv.set_window_ext(p)),
10 => xy32(r, 8).map(|p| cv.set_window_org(p)),
11 => xy32(r, 8).map(|p| cv.set_viewport_ext(p)),
12 => xy32(r, 8).map(|p| cv.set_viewport_org(p)),
17 => r.u32(8).map(|m| cv.set_map_mode(m)),
19 => r.u32(8).map(|m| cv.set_polyfill(m)),
20 => r.u32(8).map(|m| cv.set_rop2(m)),
33 => {
cv.save_dc();
Some(())
}
34 => r.i32(8).map(|n| cv.restore_dc(n)),
35 => xform(r, 8).map(|x| cv.modify_world(x, 4)),
36 => xform(r, 8).zip(r.u32(32)).map(|(x, m)| cv.modify_world(x, m)),
37 => r.u32(8).map(|i| cv.select(i)),
38 => create_pen(cv, r),
95 => create_ext_pen(cv, r),
39 => create_brush(cv, r),
93 | 94 => create_dib_brush(cv, r, typ == 93),
82 => create_font(cv, r),
18 => r.u32(8).map(|m| cv.set_bk_mode(m)),
22 => r.u32(8).map(|a| cv.set_text_align(a)),
24 => r.u32(8).map(|c| cv.set_text_color(c)),
25 => r.u32(8).map(|c| cv.set_bk_color(c)),
83 | 84 => ext_text_out(cv, r, typ == 84),
108 => small_text_out(cv, r),
40 => r.u32(8).map(|i| cv.delete_object(i)),
29 => rect32(r, 8).map(|rect| cv.exclude_clip_rect(rect)),
30 => rect32(r, 8).map(|rect| cv.intersect_clip_rect(rect)),
75 => ext_select_clip(cv, r),
27 => xy32(r, 8).map(|p| cv.move_to(p)),
54 => xy32(r, 8).map(|p| cv.line_to(p)),
42 => rect32(r, 8).map(|[l, t, rr, b]| cv.shape(&geom::ellipse(l, t, rr, b), true, true)),
43 => rect32(r, 8).map(|[l, t, rr, b]| cv.shape(&geom::rect(l, t, rr, b), true, true)),
44 => {
let (cw, ch) = (r.i32(24), r.i32(28));
let (Some(cw), Some(ch)) = (cw, ch) else { return };
rect32(r, 8).map(|[l, t, rr, b]| {
cv.shape(&geom::round_rect(l, t, rr, b, f64::from(cw), f64::from(ch)), true, true);
})
}
45..=47 => arc32(r).map(|(bounds, s, e)| {
let kind = match typ {
45 => ArcKind::Arc,
46 => ArcKind::Chord,
_ => ArcKind::Pie,
};
let [l, t, rr, b] = bounds;
cv.shape(&geom::arc_shape(l, t, rr, b, s, e, kind), kind != ArcKind::Arc, true);
}),
4 | 87 => counted(r, typ == 4).map(|pts| cv.poly(&pts, false, false, true)),
3 | 86 => counted(r, typ == 3).map(|pts| cv.poly(&pts, true, true, true)),
6 | 89 => counted(r, typ == 6).map(|pts| cv.line_chain(&pts, false, true)),
// POLYBEZIER starts its own figure at the first point and leaves the current point alone;
// POLYBEZIERTO continues from the current point.
2 | 85 => counted(r, typ == 2).and_then(|pts| {
let from = *pts.first()?;
cv.bezier(from, pts.get(1..)?);
Some(())
}),
5 | 88 => counted(r, typ == 5).map(|pts| cv.bezier_to(&pts)),
7 | 90 => poly_groups(r, typ == 7).map(|(counts, pts)| cv.shape(&geom::subpaths(&pts, &counts, false), false, true)),
8 | 91 => poly_groups(r, typ == 8).map(|(counts, pts)| cv.shape(&geom::subpaths(&pts, &counts, true), true, true)),
59 => {
cv.begin_path();
Some(())
}
60 => {
cv.end_path();
Some(())
}
61 => {
cv.close_figure();
Some(())
}
62 => {
cv.paint_path(true, false);
Some(())
}
63 => {
cv.paint_path(true, true);
Some(())
}
64 => {
cv.paint_path(false, true);
Some(())
}
76 | 77 => bitblt(cv, r, typ == 77),
80 => set_dibits(cv, r),
81 => stretch_dibits(cv, r),
114 => alphablend(cv, r),
_ => Some(()),
};
}
/// EMR_EXTSELECTCLIPRGN: `cbRgnData` bytes of `RGNDATA` (rectangles in logical units) and a region
/// mode. No region data selects the default clip.
fn ext_select_clip(cv: &mut Canvas, r: Bytes) -> Option<()> {
let cb = r.u32(8)? as usize;
let mode = r.u32(12)?;
if cb == 0 {
cv.reset_clip();
return Some(());
}
let header = 16;
if cb < 32 {
return Some(());
}
// RDH_RECTANGLES is the only region type GDI writes.
if r.u32(header + 4)? != 1 {
return Some(());
}
let n = (r.u32(header + 8)? as usize).min(crate::clip::MAX_CLIP_RECTS);
let end = header.checked_add(cb)?;
let mut rects = Vec::new();
for i in 0..n {
let o = header + 32 + i * 16;
if o + 16 > end {
break;
}
rects.push(rect32(r, o)?);
}
cv.select_clip_rects(&rects, mode);
Some(())
}
/// Two little-endian i32 values at `off`, as a point.
fn xy32(r: Bytes, off: usize) -> Option<P> {
Some((f64::from(r.i32(off)?), f64::from(r.i32(off + 4)?)))
}
/// RECTL (left, top, right, bottom) as i32 values widened to f64.
fn rect32(r: Bytes, off: usize) -> Option<[f64; 4]> {
Some([f64::from(r.i32(off)?), f64::from(r.i32(off + 4)?), f64::from(r.i32(off + 8)?), f64::from(r.i32(off + 12)?)])
}
/// An i32 field as f64; 0 past the end.
fn i32s(r: Bytes, off: usize) -> f64 {
r.i32(off).map_or(0.0, f64::from)
}
/// XFORM: six f32 values in M11, M12, M21, M22, Dx, Dy order.
fn xform(r: Bytes, off: usize) -> Option<Xform> {
let mut x = [0.0f64; 6];
for (i, v) in x.iter_mut().enumerate() {
*v = f64::from(r.f32(off + i * 4)?);
}
Some(x)
}
/// ARC, CHORD, PIE: bounds at 8, start point at 24, end point at 32.
fn arc32(r: Bytes) -> Option<([f64; 4], P, P)> {
Some((rect32(r, 8)?, xy32(r, 24)?, xy32(r, 32)?))
}
/// A point count from a record, rejected when it exceeds the path budget (before any allocation).
fn budget(n: u32) -> Option<usize> {
let n = n as usize;
(n <= MAX_PATH_OPS).then_some(n)
}
/// Point count at 24, then the points (32-bit or 16-bit) at 28.
fn counted(r: Bytes, wide: bool) -> Option<Vec<P>> {
let n = budget(r.u32(24)?)?;
r.tail(28)?.points(0, n, wide)
}
/// Subpath count at 24, total point count at 28, per-subpath counts at 32, then the points.
fn poly_groups(r: Bytes, wide: bool) -> Option<(Vec<usize>, Vec<P>)> {
let n = budget(r.u32(24)?)?;
// The record must hold the n counts before anything is allocated for them.
let raw = r.slice(32, n.checked_mul(4)?)?;
let counts: Vec<usize> = raw.as_chunks::<4>().0.iter().map(|c| u32::from_le_bytes(*c) as usize).collect();
let total = counts.iter().try_fold(0usize, |a, c| a.checked_add(*c))?;
let total = budget(u32::try_from(total).ok()?)?;
let pts = r.tail(n.checked_mul(4)?.checked_add(32)?)?.points(0, total, wide)?;
Some((counts, pts))
}
/// EMR_CREATEPEN: handle, LogPen (style, width x/y, colour).
fn create_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
let slot = r.u32(8)? as usize;
let style = r.u32(12)?;
let width = r.i32(16)?;
let color = r.u32(24)?;
cv.add_object(Some(slot), Obj::Pen(Pen { style, width: f64::from(width), color: colorref(color) }));
Some(())
}
/// EMR_EXTCREATEPEN: handle, bitmap offsets (unused), LogPenEx (style, width, brush style, colour).
/// A cosmetic pen's width is in device pixels (always 1), so it is a hairline; a BS_NULL brush draws
/// no line at all.
fn create_ext_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
let slot = r.u32(8)? as usize;
let (style, width, brush, color) = (r.u32(28)?, r.u32(32)?, r.u32(36)?, r.u32(40)?);
let width = if style & PS_TYPE_MASK == PS_COSMETIC { 0.0 } else { f64::from(width) };
let style = if brush == BS_NULL { PS_NULL } else { style & PS_STYLE_MASK };
cv.add_object(Some(slot), Obj::Pen(Pen { style, width, color: colorref(color) }));
Some(())
}
/// EMR_CREATEBRUSHINDIRECT: handle, LogBrush32 (style, colour, hatch).
fn create_brush(cv: &mut Canvas, r: Bytes) -> Option<()> {
let slot = r.u32(8)? as usize;
let style = r.u32(12)?;
let color = r.u32(16)?;
let mut b = Brush::new(style, colorref(color));
if style == BS_HATCHED {
b.hatch = r.u32(20).unwrap_or(0);
}
cv.add_object(Some(slot), Obj::Brush(b));
Some(())
}
/// EMR_CREATEDIBPATTERNBRUSHPT (94) and EMR_CREATEMONOBRUSH (93): handle, colour usage, then the
/// bitmap info and bits as offset/size pairs. A mono brush (or one whose palette holds indices,
/// DIB_PAL_COLORS) takes the text and background colours. An undecodable bitmap leaves the slot
/// empty.
fn create_dib_brush(cv: &mut Canvas, r: Bytes, mono: bool) -> Option<()> {
let slot = r.u32(8)? as usize;
let usage = r.u32(12)?;
let (bo, bl) = (r.u32(16)? as usize, r.u32(20)? as usize);
let (po, pl) = (r.u32(24)? as usize, r.u32(28)? as usize);
let img = dib::decode(Bytes(r.slice(bo, bl)?), Bytes(r.slice(po, pl)?), false)?;
let b = Brush::pattern(img, mono || usage == 1)?;
cv.add_object(Some(slot), Obj::Brush(b));
Some(())
}
/// EMR_EXTCREATEFONTINDIRECTW: handle, then a LogFont (MS-EMF 2.2.13): height, width, escapement,
/// orientation, weight (i32 each), italic, underline, strike-out, charset and four more bytes, then
/// the face name in 32 UTF-16 units (NUL-terminated).
fn create_font(cv: &mut Canvas, r: Bytes) -> Option<()> {
let slot = r.u32(8)? as usize;
let height = r.i32(12)?;
let width = r.i32(16).unwrap_or(0);
let escapement = r.i32(20).unwrap_or(0);
let weight = r.i32(28).unwrap_or(0);
let byte = |o: usize| r.0.get(o).copied().unwrap_or(0);
let units: Vec<u16> = (0..32).map_while(|i| r.u16(40 + 2 * i)).take_while(|u| *u != 0).collect();
let (family, _) = decode_utf16(&units, &[]);
let font = Font {
family: family.trim().to_string(),
height: height as f32,
width: width as f32,
weight: weight.clamp(0, 1000) as u16,
italic: byte(32) != 0,
underline: byte(33) != 0,
strike: byte(34) != 0,
charset: byte(35),
};
cv.add_object(Some(slot), Obj::Font(Arc::new(LogFont { font, escapement: f64::from(escapement) })));
Some(())
}
/// The dx array at `off` for `n` characters: i32 advances, or (dx, dy) pairs under ETO_PDY (only
/// dx is used). Empty when absent or short.
fn dx_array(r: Bytes, off: usize, n: usize, opts: u32) -> Vec<f32> {
let step = if opts & ETO_PDY != 0 { 8 } else { 4 };
if off == 0 {
return Vec::new();
}
let dx: Vec<f32> = (0..n).map_while(|i| r.i32(off.checked_add(i.checked_mul(step)?)?)).map(|v| v as f32).collect();
if dx.len() == n { dx } else { Vec::new() }
}
/// EMR_EXTTEXTOUTA (83) and EMR_EXTTEXTOUTW (84): bounds, graphics mode and scales, then an EmrText
/// at 36: reference point, character count, string offset, options, the rectangle (absent under
/// ETO_NO_RECT) and the dx offset. Offsets count from the record start. Glyph-index strings name
/// glyphs of a font the reader cannot match, so they are left out.
fn ext_text_out(cv: &mut Canvas, r: Bytes, wide: bool) -> Option<()> {
let at = xy32(r, 36)?;
let n = (r.u32(44)? as usize).min(MAX_CHARS);
let off = r.u32(48)? as usize;
let opts = r.u32(52)?;
let (rect, dx_off) = if opts & ETO_NO_RECT != 0 { (None, r.u32(56)?) } else { (rect32(r, 56), r.u32(72)?) };
if opts & ETO_GLYPH_INDEX != 0 {
return None;
}
let dx = dx_array(r, dx_off as usize, n, opts);
let (text, dx) = if wide {
let raw = r.slice(off, n.checked_mul(2)?)?;
let units: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect();
decode_utf16(&units, &dx)
} else {
decode_bytes(r.slice(off, n)?, cv.font_charset(), &dx)
};
cv.text(at, text, dx, opts, rect);
Some(())
}
/// EMR_SMALLTEXTOUT (MS-EMF 2.3.5.37): x, y, character count, options, graphics mode, scales, the
/// bounds (absent under ETO_NO_RECT), then the characters: 8-bit under ETO_SMALL_CHARS, else UTF-16.
fn small_text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
let at = xy32(r, 8)?;
let n = (r.u32(16)? as usize).min(MAX_CHARS);
let opts = r.u32(20)?;
if opts & ETO_GLYPH_INDEX != 0 {
return None;
}
let (rect, off) = if opts & ETO_NO_RECT != 0 { (None, 36) } else { (rect32(r, 36), 52) };
let (text, _) = if opts & ETO_SMALL_CHARS != 0 {
decode_bytes(r.slice(off, n)?, cv.font_charset(), &[])
} else {
let raw = r.slice(off, n.checked_mul(2)?)?;
let units: Vec<u16> = raw.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes(*c)).collect();
decode_utf16(&units, &[])
};
cv.text(at, text, Vec::new(), opts, rect);
Some(())
}
/// BITBLT (76) and STRETCHBLT (77): dest at 24..40, rop at 40, source x/y at 44, bmi at 84, bits at
/// 92; STRETCHBLT adds the source w/h at 100. A source-less rop fills the destination instead.
fn bitblt(cv: &mut Canvas, r: Bytes, stretch: bool) -> Option<()> {
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 32), i32s(r, 36)];
let rop = r.u32(40)?;
if Canvas::sourceless(rop) {
cv.fill_rop(dst, rop);
return Some(());
}
let (sw, sh) = if stretch { (i32s(r, 100), i32s(r, 104)) } else { (dst[2], dst[3]) };
let src = [i32s(r, 44), i32s(r, 48), sw, sh];
blit(cv, r, dst, src, (84, 92), Blend::Rop(rop))
}
/// ALPHABLEND: laid out like STRETCHBLT, with a BLENDFUNCTION at 40. Constant alpha at 42 applies to
/// every pixel; AlphaFormat at 43 with AC_SRC_ALPHA (1) also uses the per-pixel alpha.
fn alphablend(cv: &mut Canvas, r: Bytes) -> Option<()> {
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 32), i32s(r, 36)];
let constant = *r.0.get(42)?;
let per_pixel = (*r.0.get(43)? & 0x01) != 0;
let src = [i32s(r, 44), i32s(r, 48), i32s(r, 100), i32s(r, 104)];
blit(cv, r, dst, src, (84, 92), Blend::Alpha { constant, per_pixel })
}
/// STRETCHDIBITS: dest x/y at 24, src x/y/w/h at 32..48, bmi offset/size at 48, bits at 56, rop at 68,
/// dest w/h at 72.
fn stretch_dibits(cv: &mut Canvas, r: Bytes) -> Option<()> {
let dst = [i32s(r, 24), i32s(r, 28), i32s(r, 72), i32s(r, 76)];
let src = [i32s(r, 32), i32s(r, 36), i32s(r, 40), i32s(r, 44)];
let rop = r.u32(68)?;
blit(cv, r, dst, src, (48, 56), Blend::Rop(rop))
}
/// SETDIBITSTODEVICE (MS-EMF 2.3.1.5): dest x/y at 24, src x/y at 32, size (cxSrc, cySrc) at 40, bmi
/// at 48, bits at 56, start scan at 68. The image keeps its own size; the bits begin at scan line
/// `iStartScan`, so the source y counts from there.
fn set_dibits(cv: &mut Canvas, r: Bytes) -> Option<()> {
let (cx, cy) = (i32s(r, 40), i32s(r, 44));
if cx <= 0.0 || cy <= 0.0 {
return None;
}
let dst = [i32s(r, 24), i32s(r, 28), cx, cy];
let src = [i32s(r, 32), i32s(r, 36) - f64::from(r.u32(68)?), cx, cy];
blit(cv, r, dst, src, (48, 56), Blend::Rop(SRCCOPY))
}
/// Decodes the bitmap at `at` = (offset of the bmi offset/length pair, offset of the bits pair), both
/// inside the record, and stretches the source rectangle `src` into `dst`. Both are `[x, y, w, h]`; a
/// non-positive source size means the whole bitmap. `blend` decides how it combines.
fn blit(cv: &mut Canvas, r: Bytes, dst: [f64; 4], src: [f64; 4], at: (usize, usize), blend: Blend) -> Option<()> {
let (bo, bl) = (r.u32(at.0)? as usize, r.u32(at.0 + 4)? as usize);
let (po, pl) = (r.u32(at.1)? as usize, r.u32(at.1 + 4)? as usize);
if pl == 0 {
return None;
}
let info = Bytes(r.slice(bo, bl)?);
let pixels = Bytes(r.slice(po, pl)?);
let mut img = dib::decode(info, pixels, blend.per_pixel())?;
dib::prepare(&mut img, blend);
let (img, dst) = dib::clip_blit(&img, src.map(|v| v as i64), dst)?;
cv.bitmap(dst, img);
Some(())
}
File diff suppressed because it is too large Load Diff
-118
View File
@@ -1,118 +0,0 @@
//! Path operations in `f64` and the shape builders that turn WMF/EMF rectangles, ellipses and arcs
//! into cubic Béziers (the classic 4/3·tan(Δ/4) approximation, at most 90° per piece).
use std::f64::consts::{FRAC_PI_2, PI, TAU};
pub(crate) type P = (f64, f64);
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Op {
M(P),
L(P),
C(P, P, P),
Z,
}
pub(crate) fn rect(l: f64, t: f64, r: f64, b: f64) -> Vec<Op> {
vec![Op::M((l, t)), Op::L((r, t)), Op::L((r, b)), Op::L((l, b)), Op::Z]
}
/// Corner radii are half the EMF/WMF corner ellipse size, clamped so the corners never overlap.
pub(crate) fn round_rect(l: f64, t: f64, r: f64, b: f64, ew: f64, eh: f64) -> Vec<Op> {
// Reversed corners (right < left, bottom < top) describe the same rectangle.
let (l, r) = (l.min(r), l.max(r));
let (t, b) = (t.min(b), t.max(b));
let rx = (ew / 2.0).abs().min((r - l) / 2.0);
let ry = (eh / 2.0).abs().min((b - t) / 2.0);
let mut ops = vec![Op::M((l + rx, t)), Op::L((r - rx, t))];
ops.extend(arc(r - rx, t + ry, rx, ry, -FRAC_PI_2, FRAC_PI_2));
ops.push(Op::L((r, b - ry)));
ops.extend(arc(r - rx, b - ry, rx, ry, 0.0, FRAC_PI_2));
ops.push(Op::L((l + rx, b)));
ops.extend(arc(l + rx, b - ry, rx, ry, FRAC_PI_2, FRAC_PI_2));
ops.push(Op::L((l, t + ry)));
ops.extend(arc(l + rx, t + ry, rx, ry, PI, FRAC_PI_2));
ops.push(Op::Z);
ops
}
pub(crate) fn ellipse(l: f64, t: f64, r: f64, b: f64) -> Vec<Op> {
let (cx, cy, rx, ry) = center(l, t, r, b);
let mut ops = vec![Op::M((cx + rx, cy))];
ops.extend(arc(cx, cy, rx, ry, 0.0, TAU));
ops.push(Op::Z);
ops
}
/// Which closed or open shape an ARC/CHORD/PIE record draws.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ArcKind {
Arc,
Chord,
Pie,
}
/// ARC, CHORD and PIE: the arc runs counter-clockwise on screen from the start point to the end point.
pub(crate) fn arc_shape(l: f64, t: f64, r: f64, b: f64, s: P, e: P, kind: ArcKind) -> Vec<Op> {
let (cx, cy, rx, ry) = center(l, t, r, b);
let ang = |p: P| ((p.1 - cy) / ry.max(f64::MIN_POSITIVE)).atan2((p.0 - cx) / rx.max(f64::MIN_POSITIVE));
let (a0, a1) = (ang(s), ang(e));
let mut d = (a0 - a1).rem_euclid(TAU);
if d < 1e-12 {
d = TAU;
}
let sweep = -d;
let start = (cx + rx * a0.cos(), cy + ry * a0.sin());
let mut ops = if kind == ArcKind::Pie { vec![Op::M((cx, cy)), Op::L(start)] } else { vec![Op::M(start)] };
ops.extend(arc(cx, cy, rx, ry, a0, sweep));
match kind {
ArcKind::Arc => {}
ArcKind::Chord => ops.push(Op::Z),
ArcKind::Pie => ops.extend([Op::L((cx, cy)), Op::Z]),
}
ops
}
fn center(l: f64, t: f64, r: f64, b: f64) -> (f64, f64, f64, f64) {
((l + r) / 2.0, (t + b) / 2.0, (r - l).abs() / 2.0, (b - t).abs() / 2.0)
}
/// Cubic pieces along an ellipse from parameter `a0` over signed `sweep` radians. The current point
/// must already be at `(cx + rx cos a0, cy + ry sin a0)`.
fn arc(cx: f64, cy: f64, rx: f64, ry: f64, a0: f64, sweep: f64) -> Vec<Op> {
let n = ((sweep.abs() / FRAC_PI_2).ceil() as usize).clamp(1, 8);
let step = sweep / n as f64;
let k = 4.0 / 3.0 * (step / 4.0).tan();
let pt = |t: f64| (cx + rx * t.cos(), cy + ry * t.sin());
let mut ops = Vec::with_capacity(n);
for i in 0..n {
let t0 = a0 + step * i as f64;
let t1 = t0 + step;
let (p0, p3) = (pt(t0), pt(t1));
let c1 = (p0.0 - k * rx * t0.sin(), p0.1 + k * ry * t0.cos());
let c2 = (p3.0 + k * rx * t1.sin(), p3.1 - k * ry * t1.cos());
ops.push(Op::C(c1, c2, p3));
}
ops
}
/// Consecutive subpaths: `counts[i]` points each, taken in order from `pts`. Counts that run past the
/// point list are cut short rather than failing the whole record.
pub(crate) fn subpaths(pts: &[P], counts: &[usize], close: bool) -> Vec<Op> {
let mut ops = Vec::with_capacity(pts.len() + counts.len());
let mut at = 0usize;
for &c in counts {
let end = at.saturating_add(c).min(pts.len());
let Some((first, rest)) = pts.get(at..end).and_then(|s| s.split_first()) else {
at = end;
continue;
};
ops.push(Op::M(*first));
ops.extend(rest.iter().map(|p| Op::L(*p)));
if close {
ops.push(Op::Z);
}
at = end;
}
ops
}
-306
View File
@@ -1,306 +0,0 @@
//! DeckCraft metafiles (ported from WordCraft's `crates/metafile`): reads Windows Metafiles (WMF)
//! and Enhanced Metafiles (EMF) into a neutral vector model of filled and stroked paths, pattern
//! fills, RGBA bitmaps and text strings, in a picture frame with y down.
//!
//! Text stays font-agnostic: a [`Text`] item names the font the file asks for and carries the
//! string, its advances and alignment; the renderer resolves the font and draws the glyphs.
//!
//! The reader is pure Rust over byte slices (no file system), so it builds for wasm. Every read is
//! bounds-checked and every count is capped; hostile input yields a shorter picture or an [`Error`],
//! never a panic.
//!
//! Records follow MS-WMF and MS-EMF. Rectangle clip regions (`INTERSECTCLIPRECT`,
//! `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit what later records draw. Records outside the
//! supported set are skipped silently.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
mod bytes;
mod canvas;
mod clip;
mod cliprect;
mod dib;
mod emf;
mod emfplus;
mod geom;
mod place;
mod text;
mod wmf;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::sync::Arc;
use bytes::Bytes;
use canvas::Raw;
use geom::{Op, P};
pub use place::{MAX_BITMAP_SIDE, PlacedItem};
pub use text::{Font, HAlign, SYMBOL_CHARSET, Text, VAlign};
/// Items kept from one file. Past the cap the parse stops and keeps what it has.
pub(crate) const MAX_ITEMS: usize = 200_000;
/// Object table slots (WMF handles, EMF object indices).
pub(crate) const MAX_OBJECTS: usize = 4096;
/// Largest bitmap in pixels (about 40 megapixels, 160 MB of RGBA).
pub(crate) const MAX_PIXELS: usize = 40_000_000;
/// DC save-stack depth.
pub(crate) const MAX_STACK: usize = 1024;
/// Points in one open path bracket.
pub(crate) const MAX_PATH_OPS: usize = 2_000_000;
/// Bytes of path ops and pixels kept from one file (about 256 MB).
pub(crate) const MAX_BYTES: usize = 256 * 1024 * 1024;
/// A parsed metafile. Items use coordinates in `0..width` and `0..height`, y down.
#[derive(Debug, Clone, PartialEq)]
pub struct Picture {
/// Size of the drawing in its own units: points for placeable WMF and EMF (from the frame), device
/// or logical units otherwise.
pub width: f32,
pub height: f32,
pub items: Vec<Item>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Item {
/// A filled and/or stroked path, in picture coordinates.
Path {
segs: Vec<Seg>,
fill: Option<[u8; 4]>,
/// Colour and width in picture units.
stroke: Option<([u8; 4], f32)>,
even_odd: bool,
/// Clip region in picture units, each rectangle `[x0, y0, x1, y1]`. None is the default clip.
region: Option<Vec<[f32; 4]>>,
},
/// A bitmap stretched into `rect` = `[x, y, w, h]` (picture units), RGBA8 rows top-down.
Bitmap {
rect: [f32; 4],
width: u32,
height: u32,
rgba: Vec<u8>,
/// Clip region in picture units. None is the default clip.
region: Option<Vec<[f32; 4]>>,
},
/// A path filled with a repeating tile (pattern and hatch brushes). Items filled with the same
/// brush share its tile.
Pattern {
segs: Vec<Seg>,
even_odd: bool,
tile: Arc<Tile>,
/// Clip region in picture units. None is the default clip.
region: Option<Vec<[f32; 4]>>,
},
/// A text string.
Text(Box<Text>),
}
/// A brush tile: RGBA8 rows top-down (transparent where the brush leaves the background), each
/// pixel `cell` picture units, with a tile's top-left corner at `origin`.
#[derive(Debug, Clone, PartialEq)]
pub struct Tile {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
pub cell: [f32; 2],
pub origin: [f32; 2],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Seg {
Move(f32, f32),
Line(f32, f32),
Cubic(f32, f32, f32, f32, f32, f32),
Close,
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum Error {
#[error("not a WMF or EMF file")]
NotMetafile,
#[error("metafile header is truncated or invalid")]
BadHeader,
#[error("metafile has no readable records")]
NoRecords,
}
/// The picture frame in device units (`rect` = `[x, y, w, h]`) and, when the file states one, its
/// size in picture units.
pub(crate) struct Frame {
pub rect: [f64; 4],
pub size: Option<(f64, f64)>,
}
/// Parses a WMF (placeable or plain) or EMF file.
pub fn parse(bytes: &[u8]) -> Result<Picture, Error> {
let b = Bytes(bytes);
let (cv, frame) = if emf::is_emf(b) {
emf::parse(b)?
} else if wmf::is_wmf(b) {
// A WMF converted from an EMF can carry the original in MFCOMMENT escapes; PowerPoint draws
// the WMF's own records (its dx rounding shows), so the copy is not used.
wmf::parse(b)?
} else {
return Err(Error::NotMetafile);
};
Ok(build(cv.items, frame))
}
/// True when the bytes start like a WMF (placeable or plain header) or an EMF.
pub fn is_metafile(bytes: &[u8]) -> bool {
let b = Bytes(bytes);
emf::is_emf(b) || wmf::is_wmf(b)
}
/// Maps device-space output into the picture frame. Without a usable frame the frame is the bounding
/// box of the drawing, so the origin is still the top-left of what was drawn.
fn build(raw: Vec<Raw>, frame: Option<Frame>) -> Picture {
let frame = frame.filter(|f| f.rect.iter().all(|v| v.is_finite()) && f.rect[2] > 0.0 && f.rect[3] > 0.0);
let (rect, size) = match frame {
Some(f) => (f.rect, f.size.unwrap_or((f.rect[2], f.rect[3]))),
None => match bbox(&raw) {
Some([x0, y0, x1, y1]) => ([x0, y0, x1 - x0, y1 - y0], (x1 - x0, y1 - y0)),
None => return Picture { width: 0.0, height: 0.0, items: Vec::new() },
},
};
let sx = if rect[2] > 0.0 { size.0 / rect[2] } else { 1.0 };
let sy = if rect[3] > 0.0 { size.1 / rect[3] } else { 1.0 };
let tf = move |p: P| ((p.0 - rect[0]) * sx, (p.1 - rect[1]) * sy);
let stroke_scale = (sx + sy) / 2.0;
// No stroke is wider than the picture's diagonal: anything wider is a mapping artefact.
let diag = size.0.hypot(size.1);
// Tiles shared by several fills stay shared.
let mut tiles: HashMap<*const canvas::RawTile, (Arc<canvas::RawTile>, Arc<Tile>)> = HashMap::new();
let items = raw.into_iter().filter_map(|r| match r {
Raw::Path { ops, fill, stroke, even_odd, region } => {
let segs = ops.iter().map(|op| seg(op, tf)).collect::<Option<Vec<Seg>>>()?;
if segs.len() < 2 {
return None;
}
let stroke = stroke.and_then(|(c, w)| Some((c, f((w * stroke_scale).min(diag))?)));
Some(Item::Path { segs, fill, stroke, even_odd, region: map_region(region, &tf)? })
}
Raw::Bitmap { rect: r, img, region } => {
let (x, y) = tf((r[0], r[1]));
let rect = [f(x)?, f(y)?, f(r[2] * sx)?, f(r[3] * sy)?];
Some(Item::Bitmap { rect, width: img.w as u32, height: img.h as u32, rgba: img.px, region: map_region(region, &tf)? })
}
Raw::Pattern { ops, even_odd, tile, region } => {
let segs = ops.iter().map(|op| seg(op, tf)).collect::<Option<Vec<Seg>>>()?;
if segs.len() < 2 {
return None;
}
let key = Arc::as_ptr(&tile);
let tile = match tiles.get(&key) {
Some((_, t)) => t.clone(),
None => {
let (ox, oy) = tf((0.0, 0.0));
let t = Arc::new(Tile {
width: u32::try_from(tile.w).ok()?,
height: u32::try_from(tile.h).ok()?,
rgba: tile.px.clone(),
cell: [f(tile.cell * sx)?, f(tile.cell * sy)?],
origin: [f(ox)?, f(oy)?],
});
// The raw tile is kept with its key, so the address is never reused by another.
tiles.insert(key, (tile.clone(), t.clone()));
t
}
};
Some(Item::Pattern { segs, even_odd, tile, region: map_region(region, &tf)? })
}
Raw::Text { mut text, region } => {
// Device to picture: scale by (sx, sy) after moving the frame's corner to the origin.
let [a, b, c, d, e, g] = text.transform;
let (e, g) = tf((e, g));
text.transform = [a * sx, b * sy, c * sx, d * sy, e, g];
if !text.transform.iter().all(|v| v.is_finite()) {
return None;
}
if let Some([x0, y0, x1, y1]) = text.clip {
let (p, q) = (tf((f64::from(x0), f64::from(y0))), tf((f64::from(x1), f64::from(y1))));
text.clip = Some([f(p.0.min(q.0))?, f(p.1.min(q.1))?, f(p.0.max(q.0))?, f(p.1.max(q.1))?]);
}
text.region = map_region(region, &tf)?;
Some(Item::Text(text))
}
});
let items = items.collect();
Picture { width: f(size.0).unwrap_or(0.0), height: f(size.1).unwrap_or(0.0), items }
}
fn map_region(region: Option<Vec<[f64; 4]>>, tf: &impl Fn(P) -> P) -> Option<Option<Vec<[f32; 4]>>> {
let Some(rects) = region else { return Some(None) };
let mut out = Vec::with_capacity(rects.len());
for [x0, y0, x1, y1] in rects {
let (p, q) = (tf((x0, y0)), tf((x1, y1)));
out.push([f(p.0.min(q.0))?, f(p.1.min(q.1))?, f(p.0.max(q.0))?, f(p.1.max(q.1))?]);
}
Some(Some(out))
}
fn f(v: f64) -> Option<f32> {
let v = v as f32;
v.is_finite().then_some(v)
}
fn seg(op: &Op, tf: impl Fn(P) -> P) -> Option<Seg> {
let pt = |p: P| {
let (x, y) = tf(p);
Some((f(x)?, f(y)?))
};
Some(match *op {
Op::M(p) => {
let (x, y) = pt(p)?;
Seg::Move(x, y)
}
Op::L(p) => {
let (x, y) = pt(p)?;
Seg::Line(x, y)
}
Op::C(a, b, c) => {
let (x1, y1) = pt(a)?;
let (x2, y2) = pt(b)?;
let (x3, y3) = pt(c)?;
Seg::Cubic(x1, y1, x2, y2, x3, y3)
}
Op::Z => Seg::Close,
})
}
/// Bounding box `[x0, y0, x1, y1]` over the finite points of the output.
fn bbox(raw: &[Raw]) -> Option<[f64; 4]> {
let mut b: Option<[f64; 4]> = None;
let mut grow = |p: P| {
if p.0.is_finite() && p.1.is_finite() {
b = Some(match b {
None => [p.0, p.1, p.0, p.1],
Some([x0, y0, x1, y1]) => [x0.min(p.0), y0.min(p.1), x1.max(p.0), y1.max(p.1)],
});
}
};
for r in raw {
match r {
Raw::Path { ops, .. } | Raw::Pattern { ops, .. } => {
for op in ops {
match *op {
Op::M(p) | Op::L(p) => grow(p),
Op::C(a, c, d) => {
grow(a);
grow(c);
grow(d);
}
Op::Z => {}
}
}
}
Raw::Bitmap { rect, .. } => {
grow((rect[0], rect[1]));
grow((rect[0] + rect[2], rect[1] + rect[3]));
}
Raw::Text { text: t, .. } => grow((t.transform[4], t.transform[5])),
}
}
b
}
-206
View File
@@ -1,206 +0,0 @@
//! Placing a picture on a target (a page or a device): the fitted transform, paths as kurbo paths in
//! target units and bitmaps as target rectangles. The rasteriser and the PDF writer only paint the result.
use std::sync::Arc;
use kurbo::{Affine, BezPath, Rect, Shape};
use crate::{Item, Picture, Seg};
/// Largest bitmap side the renderers draw: within the PDF image limit and the rasteriser's 16-bit sides.
pub const MAX_BITMAP_SIDE: u32 = 30_000;
/// Placed coordinates past this (target units) are far off any page; such items are left out.
const MAX_COORD: f64 = 1e6;
/// Smallest placed bitmap side (target units), so the stretch onto it is never singular.
const MIN_BITMAP_SIDE: f64 = 1e-6;
/// Maximum aggregate path elements retained in placed output.
const MAX_PLACED_ELEMENTS: usize = 500_000;
/// One item of a placed picture, in target units.
pub enum PlacedItem {
/// A filled and/or stroked path. The stroke width is in target units; 0 is a cosmetic hairline that
/// the renderer draws at its minimum width.
Path { path: BezPath, fill: Option<[u8; 4]>, stroke: Option<([u8; 4], f64)>, even_odd: bool, region: Option<Arc<BezPath>> },
/// `Picture::items[index]`, a bitmap, stretched into `rect`.
Bitmap { index: usize, rect: Rect, region: Option<Arc<BezPath>> },
/// `Picture::items[index]`, a pattern fill: `path` (target units) filled with the item's tile
/// repeated; `tile` maps tile pixels to target units.
Pattern { index: usize, path: BezPath, even_odd: bool, tile: Affine, region: Option<Arc<BezPath>> },
/// `Picture::items[index]`, a text string: `transform` maps its text space to target units;
/// `clip` (target units) bounds what it draws. `region` is the device-context clip.
Text { index: usize, transform: Affine, clip: Option<Rect>, region: Option<Arc<BezPath>> },
}
impl Picture {
/// Whether the picture has a finite, positive size, so it can be fitted into a frame.
pub fn has_size(&self) -> bool {
self.width.is_finite() && self.height.is_finite() && self.width > 0.0 && self.height > 0.0
}
/// The transform that fits the picture into `r` with the crop fractions `crop` (left, top, right,
/// bottom) removed first: the visible part fills `r`. None for a degenerate size.
pub fn fit(&self, r: Rect, crop: [f32; 4]) -> Option<Affine> {
let [cl, ct, cr, cb] = crop.map(|v| if v.is_finite() { f64::from(v.clamp(0.0, 0.95)) } else { 0.0 });
let (w, h) = (f64::from(self.width), f64::from(self.height));
let sx = r.width() / (w * (1.0 - cl - cr).max(0.05));
let sy = r.height() / (h * (1.0 - ct - cb).max(0.05));
(sx.is_finite() && sy.is_finite() && sx > 0.0 && sy > 0.0)
.then(|| Affine::translate((r.x0 - cl * w * sx, r.y0 - ct * h * sy)) * Affine::scale_non_uniform(sx, sy))
}
/// The items placed into `r` with `crop` applied. Items that are not finite or reach past ±1e6
/// target units are left out (they are far off any page), as are bitmaps with no area. Stroke
/// widths are scaled to target units (along the mean axis scale, so a non-uniform fit keeps
/// strokes even).
pub fn place(&self, r: Rect, crop: [f32; 4]) -> Vec<PlacedItem> {
let Some(tf) = self.fit(r, crop) else { return Vec::new() };
let c = tf.as_coeffs();
let k = (c[0].abs() + c[3].abs()) / 2.0;
let mut out = Vec::with_capacity(self.items.len());
let mut total_elements = 0usize;
let region_elements = std::cell::Cell::new(0usize);
let mut cached_region: Option<(*const [f32; 4], usize, Option<Option<Arc<BezPath>>>)> = None;
let mut get_region = |reg: &Option<Vec<[f32; 4]>>| -> Option<Option<Arc<BezPath>>> {
let (ptr, len) = match reg {
None => (std::ptr::null(), 0),
Some(v) => (v.as_ptr(), v.len()),
};
if let Some((c_ptr, c_len, ref res)) = cached_region
&& c_ptr == ptr
&& c_len == len
{
return res.clone();
}
let res = placed_region(&tf, reg);
// Each distinct clip region is expanded once; its elements count against the placement budget.
let n = res.as_ref().and_then(Option::as_ref).map_or(0, |p| p.elements().len());
region_elements.set(region_elements.get().saturating_add(n));
cached_region = Some((ptr, len, res.clone()));
res
};
for (index, item) in self.items.iter().enumerate() {
if total_elements.saturating_add(region_elements.get()) >= MAX_PLACED_ELEMENTS {
break;
}
match item {
Item::Path { segs, fill, stroke, even_odd, region } => {
let path = tf * bez(segs);
let Some(region) = get_region(region) else { continue };
if in_range(path.bounding_box()) {
// This item's own region is expanded by now: it counts before it is kept.
if total_elements.saturating_add(path.elements().len()).saturating_add(region_elements.get()) > MAX_PLACED_ELEMENTS {
break;
}
total_elements = total_elements.saturating_add(path.elements().len());
let stroke = stroke.map(|(c, w)| (c, f64::from(w) * k));
out.push(PlacedItem::Path { path, fill: *fill, stroke, even_odd: *even_odd, region });
}
}
Item::Pattern { segs, even_odd, tile: t, region } => {
let path = tf * bez(segs);
let Some(region) = get_region(region) else { continue };
let tile = tf
* Affine::translate((f64::from(t.origin[0]), f64::from(t.origin[1])))
* Affine::scale_non_uniform(f64::from(t.cell[0]), f64::from(t.cell[1]));
let det = tile.determinant();
if in_range(path.bounding_box()) && det.is_finite() && det.abs() > 1e-12 {
if total_elements.saturating_add(path.elements().len()).saturating_add(region_elements.get()) > MAX_PLACED_ELEMENTS {
break;
}
total_elements = total_elements.saturating_add(path.elements().len());
out.push(PlacedItem::Pattern { index, path, even_odd: *even_odd, tile, region });
}
}
Item::Text(t) => {
let transform = tf * Affine::new(t.transform);
let Some(region) = get_region(&t.region) else { continue };
let clip =
t.clip.map(|[x0, y0, x1, y1]| tf.transform_rect_bbox(Rect::new(f64::from(x0), f64::from(y0), f64::from(x1), f64::from(y1))));
let c = transform.as_coeffs();
let origin = Rect::new(c[4], c[5], c[4], c[5]);
if in_range(origin) && c.iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD) && clip.is_none_or(in_range) {
out.push(PlacedItem::Text { index, transform, clip, region });
}
}
Item::Bitmap { rect: [x, y, w, h], region, .. } => {
let (x0, y0) = (f64::from(*x), f64::from(*y));
let rect = tf.transform_rect_bbox(Rect::new(x0, y0, x0 + f64::from(*w), y0 + f64::from(*h)));
let Some(region) = get_region(region) else { continue };
if in_range(rect) && rect.width() > MIN_BITMAP_SIDE && rect.height() > MIN_BITMAP_SIDE {
out.push(PlacedItem::Bitmap { index, rect, region });
}
}
}
}
out
}
}
impl Item {
/// Takes a bitmap's pixels out of the item (its geometry stays) and returns them as (width, height,
/// RGBA) when a renderer can draw them: both sides in 1..=[`MAX_BITMAP_SIDE`] and exactly that many
/// pixels. None for paths and for bitmaps that cannot be drawn.
pub fn take_pixels(&mut self) -> Option<(u32, u32, Vec<u8>)> {
let Item::Bitmap { width, height, rgba, .. } = self else { return None };
let (w, h, px) = (*width, *height, std::mem::take(rgba));
let sides = (1..=MAX_BITMAP_SIDE).contains(&w) && (1..=MAX_BITMAP_SIDE).contains(&h);
(sides && px.len() == (w as usize).saturating_mul(h as usize).saturating_mul(4)).then_some((w, h, px))
}
}
/// The device-context clip in target units. `Some(None)` is the default clip. `None` means the
/// region does not meet the page, so the item draws nothing.
fn placed_region(tf: &Affine, region: &Option<Vec<[f32; 4]>>) -> Option<Option<Arc<BezPath>>> {
let Some(rects) = region else { return Some(None) };
let mut path = BezPath::new();
for [x0, y0, x1, y1] in rects {
let r = tf.transform_rect_bbox(Rect::new(f64::from(*x0), f64::from(*y0), f64::from(*x1), f64::from(*y1)));
if in_range(r) && r.width() > 1e-9 && r.height() > 1e-9 {
path.extend(r.to_path(0.1));
}
}
(!path.is_empty()).then_some(Some(Arc::new(path)))
}
/// Segments as a kurbo path. A segment after a close opens a new subpath at the last start point.
fn bez(segs: &[Seg]) -> BezPath {
let mut p = BezPath::new();
let mut start = (0.0, 0.0);
let mut open = false;
for s in segs {
match *s {
Seg::Move(x, y) => {
start = (f64::from(x), f64::from(y));
p.move_to(start);
open = true;
}
Seg::Line(x, y) => {
if !open {
p.move_to(start);
}
p.line_to((f64::from(x), f64::from(y)));
open = true;
}
Seg::Cubic(a, b, c, d, e, f) => {
if !open {
p.move_to(start);
}
p.curve_to((f64::from(a), f64::from(b)), (f64::from(c), f64::from(d)), (f64::from(e), f64::from(f)));
open = true;
}
Seg::Close => {
if open {
p.close_path();
}
open = false;
}
}
}
p
}
/// Whether a placed box is finite and within ±[`MAX_COORD`].
fn in_range(b: Rect) -> bool {
[b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite() && v.abs() < MAX_COORD)
}
File diff suppressed because it is too large Load Diff
-203
View File
@@ -1,203 +0,0 @@
//! Text records: the font (LOGFONT), the text string decoded from its character set, and the
//! neutral [`Text`] item. The metafile crate knows no fonts, so measuring, alignment against the
//! font's metrics and glyph drawing are left to the renderer.
/// A font chosen by a CREATEFONTINDIRECT record (MS-WMF 2.2.1.2 Font, MS-EMF 2.2.13 LogFont).
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Font {
/// The face name the file asks for (`"Arial"`, `"Wingdings"`); may be empty.
pub family: String,
/// LOGFONT height in text units: negative is the character (em) height, positive the cell
/// height (ascent plus descent), 0 the default size.
pub height: f32,
/// Average character width in text units; 0 keeps the font's own proportions.
pub width: f32,
/// 100..=900 (400 regular, 700 bold); 0 is the default weight.
pub weight: u16,
pub italic: bool,
pub underline: bool,
pub strike: bool,
/// LOGFONT character set (0 ANSI, 2 SYMBOL, 238 EASTEUROPE…).
pub charset: u8,
}
/// SYMBOL_CHARSET: byte codes are the font's own symbol codes.
pub const SYMBOL_CHARSET: u8 = 2;
/// Where the reference point sits on the string horizontally (TA_LEFT, TA_CENTER, TA_RIGHT).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HAlign {
#[default]
Left,
Center,
Right,
}
/// Where the reference point sits vertically (TA_TOP, TA_BASELINE, TA_BOTTOM): the top of the
/// character cell, the baseline or the bottom of the cell.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VAlign {
#[default]
Top,
Baseline,
Bottom,
}
/// One string drawn by EXTTEXTOUT, TEXTOUT or SMALLTEXTOUT.
#[derive(Debug, Clone, PartialEq)]
pub struct Text {
/// Text space to picture space as affine coefficients `[a, b, c, d, e, f]` (`x' = a·x + c·y + e`,
/// `y' = b·x + d·y + f`, kurbo's order). Text space has x along the baseline and y down, in the
/// metafile's logical units, with the reference point at the origin. Escapement and the
/// mapping are folded in; glyphs are never mirrored.
pub transform: [f64; 6],
pub text: String,
/// Advance after each character of `text`, in text units (the dx array). Empty when the file
/// gives none: the font's own advances apply.
pub dx: Vec<f32>,
pub font: Font,
pub color: [u8; 4],
/// Colour filled behind the character cells (OPAQUE background mode); None when transparent.
pub background: Option<[u8; 4]>,
pub h_align: HAlign,
pub v_align: VAlign,
/// Clip rectangle `[x0, y0, x1, y1]` in picture units (ETO_CLIPPED).
pub clip: Option<[f32; 4]>,
/// Clip region from the device context, in picture units. None is the default clip. Drawn
/// together with [`Text::clip`] when both are set.
pub region: Option<Vec<[f32; 4]>>,
}
/// TA_UPDATECP: the string starts at the current position, which moves past it.
pub(crate) const TA_UPDATECP: u32 = 0x0001;
/// ETO_OPAQUE fills the rectangle with the background colour; ETO_CLIPPED clips the text to it.
pub(crate) const ETO_OPAQUE: u32 = 0x0002;
pub(crate) const ETO_CLIPPED: u32 = 0x0004;
/// The string holds glyph indices, not characters (drawn only when the font matches).
pub(crate) const ETO_GLYPH_INDEX: u32 = 0x0010;
/// The dx array holds (dx, dy) pairs.
pub(crate) const ETO_PDY: u32 = 0x2000;
/// Longest string kept from one record.
pub(crate) const MAX_CHARS: usize = 16_384;
/// Horizontal and vertical alignment from a TA_* word (the vertical bits are 0 top, 8 bottom,
/// 24 baseline; the horizontal ones 0 left, 2 right, 6 center).
pub(crate) fn align(ta: u32) -> (HAlign, VAlign) {
let h = match ta & 0x06 {
6 => HAlign::Center,
2 => HAlign::Right,
_ => HAlign::Left,
};
let v = match ta & 0x18 {
0x18 => VAlign::Baseline,
0x08 => VAlign::Bottom,
_ => VAlign::Top,
};
(h, v)
}
/// Decodes 8-bit text in `charset` to one character per byte, and merges `dx` to match: a
/// double-byte character set's lead and trail bytes become one blank (its glyphs are not
/// decoded) whose advance is the sum of both. `dx` may be empty.
pub(crate) fn decode_bytes(bytes: &[u8], charset: u8, dx: &[f32]) -> (String, Vec<f32>) {
let mut s = String::with_capacity(bytes.len());
let mut out_dx = Vec::with_capacity(dx.len().min(bytes.len()));
let dbcs = matches!(charset, 128 | 129 | 130 | 134 | 136);
let mut i = 0;
while let Some(&b) = bytes.get(i) {
let n = if dbcs && b >= 0x81 && i + 1 < bytes.len() { 2 } else { 1 };
let c = if n == 2 { ' ' } else { byte_char(b, charset) };
s.push(c);
if !dx.is_empty() {
out_dx.push((i..i + n).filter_map(|k| dx.get(k)).sum());
}
i += n;
}
(s, out_dx)
}
/// UTF-16 text to a string with one dx entry per character: a surrogate pair takes the sum of
/// its two advances. Unpaired surrogates become U+FFFD.
pub(crate) fn decode_utf16(units: &[u16], dx: &[f32]) -> (String, Vec<f32>) {
let mut s = String::with_capacity(units.len());
let mut out_dx = Vec::with_capacity(dx.len().min(units.len()));
let mut i = 0;
for r in char::decode_utf16(units.iter().copied()) {
let c = r.unwrap_or(char::REPLACEMENT_CHARACTER);
let n = c.len_utf16();
s.push(c);
if !dx.is_empty() {
out_dx.push((i..i + n).filter_map(|k| dx.get(k)).sum());
}
i += n;
}
(s, out_dx)
}
/// One byte in a single-byte Windows code page. Symbol fonts keep their codes in the private-use
/// range (U+F000 + code), where symbol fonts map them.
fn byte_char(b: u8, charset: u8) -> char {
if b < 0x80 && charset != SYMBOL_CHARSET {
return char::from(b);
}
let cp = match charset {
SYMBOL_CHARSET => 0xF000 | u32::from(b),
238 => high(b, &CP1250),
204 => high(b, &CP1251),
_ => high(b, &CP1252),
};
char::from_u32(cp).unwrap_or(char::REPLACEMENT_CHARACTER)
}
/// The code point of byte `b` (0x80..=0xFF) from a table of the upper half; 0 entries are
/// undefined bytes, drawn as U+FFFD.
fn high(b: u8, table: &[u16; 128]) -> u32 {
match table.get(usize::from(b.wrapping_sub(0x80))) {
Some(0) | None => 0xFFFD,
Some(v) => u32::from(*v),
}
}
/// Windows-1252 (Western) 0x80..=0xFF: the upper half of Latin-1 with typographic marks in 0x80..=0x9F.
const CP1252: [u16; 128] = {
let mut t = [0u16; 128];
let low: [u16; 32] = [
0x20AC, 0, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0, 0x017D, 0, 0, 0x2018, 0x2019, 0x201C,
0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0, 0x017E, 0x0178,
];
let mut i = 0;
while i < 128 {
t[i] = if i < 32 { low[i] } else { 0x80 + i as u16 };
i += 1;
}
t
};
/// Windows-1250 (Central European) 0x80..=0xFF.
const CP1250: [u16; 128] = [
0x20AC, 0, 0x201A, 0, 0x201E, 0x2026, 0x2020, 0x2021, 0, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, //
0, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, //
0x00A0, 0x02C7, 0x02D8, 0x0141, 0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, //
0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5, 0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, //
0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, //
0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, //
0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, //
0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9,
];
/// Windows-1251 (Cyrillic) 0x80..=0xFF; 0xC0..=0xFF are А..я in order.
const CP1251: [u16; 128] = {
let low: [u16; 64] = [
0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, //
0x0452, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, //
0x00A0, 0x040E, 0x045E, 0x0408, 0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, //
0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5, 0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457,
];
let mut t = [0u16; 128];
let mut i = 0;
while i < 128 {
t[i] = if i < 64 { low[i] } else { 0x0410 + (i as u16 - 64) };
i += 1;
}
t
};
-383
View File
@@ -1,383 +0,0 @@
//! WMF reader (MS-WMF). A file is an optional 22-byte placeable header, a 9-word standard header, then
//! records of (size in 16-bit words, function, parameters). Parameters are read at byte 6 of each record.
use std::sync::Arc;
use crate::bytes::Bytes;
use crate::canvas::{BS_HATCHED, Brush, Canvas, LogFont, Obj, Pen, colorref};
use crate::dib;
use crate::geom::{self, ArcKind, P};
use crate::text::{ETO_CLIPPED, ETO_OPAQUE, Font, MAX_CHARS, decode_bytes};
use crate::{Error, Frame, MAX_PATH_OPS};
const PLACEABLE_KEY: u32 = 0x9AC6_CDD7;
const DEFAULT_INCH: f64 = 1440.0;
/// A placeable WMF plays into a target sized in points; brushes are laid out in 96-dpi pixels.
const PT_PER_PX: f64 = 72.0 / 96.0;
pub(crate) fn is_wmf(b: Bytes) -> bool {
if b.u32(0) == Some(PLACEABLE_KEY) {
return true;
}
matches!(b.u16(0), Some(1 | 2)) && b.u16(2) == Some(9) && matches!(b.u16(4), Some(0x100 | 0x300))
}
/// Reads all records. Returns the drawing state and the picture frame, or an error when no record
/// could be read at all.
pub(crate) fn parse(input: Bytes) -> Result<(Canvas, Option<Frame>), Error> {
let placeable = input.u32(0) == Some(PLACEABLE_KEY);
// Placeable bounds: logical corners (left, top, right, bottom) and their size in points.
let (hdr, bounds) = if placeable {
let [l, t, r, b] = [6, 8, 10, 12].map(|o| input.i16(o).map(f64::from));
let (Some(l), Some(t), Some(r), Some(b)) = (l, t, r, b) else { return Err(Error::BadHeader) };
let inch = input.u16(14).filter(|v| *v > 0).map_or(DEFAULT_INCH, f64::from);
let (w, h) = (r - l, b - t);
let bounds = (w > 0.0 && h > 0.0).then_some(([l, t, r, b], (w * 72.0 / inch, h * 72.0 / inch)));
(22, bounds)
} else {
(0, None)
};
let hsize = input.u16(hdr + 2).ok_or(Error::BadHeader)? as usize;
if hsize < 9 {
return Err(Error::BadHeader);
}
let mut pos = hdr.checked_add(hsize.checked_mul(2).ok_or(Error::BadHeader)?).ok_or(Error::BadHeader)?;
// Played into a target like Word does: the placeable bounds are the first window, the target (the
// picture size) the viewport. A plain WMF's target is its first window.
let mut cv = Canvas::new();
if let Some((c, size)) = bounds {
cv.play_into(Some([c[0], c[1], c[2] - c[0], c[3] - c[1]]), size);
cv.set_clip_surface([0.0, 0.0, size.0, size.1]);
cv.pattern_px = PT_PER_PX;
}
let mut player_viewport = bounds.is_some();
let mut parsed = 0usize;
while !cv.full() {
let (Some(words), Some(func)) = (input.u32(pos), pos.checked_add(4).and_then(|o| input.u16(o))) else { break };
let Some(len) = (words as usize).checked_mul(2).filter(|l| *l >= 6) else { break };
let Some(rec) = input.slice(pos, len) else { break };
if func == 0 {
break;
}
record(&mut cv, func, Bytes(rec));
if func == 0x020C && cv.window_set && !player_viewport && !cv.viewport_set {
let we = cv.map.window_ext();
cv.play_into(None, (we.0.abs(), we.1.abs()));
player_viewport = true;
}
parsed += 1;
let Some(next) = pos.checked_add(len) else { break };
pos = next;
}
if parsed == 0 {
return Err(Error::NoRecords);
}
let frame = match bounds {
// Played into its target: the frame is the target.
Some((_, size)) if !cv.viewport_set && cv.map.uses_extents() => Some(Frame { rect: [0.0, 0.0, size.0, size.1], size: Some(size) }),
// The file set its own viewport, so the drawing is in its device units and the logical bounds
// go through the final mapping.
Some((c, size)) => {
let (a, b) = (cv.map.device((c[0], c[1])), cv.map.device((c[2], c[3])));
let rect = [a.0.min(b.0), a.1.min(b.1), (b.0 - a.0).abs(), (b.1 - a.1).abs()];
(rect[2] > 0.0 && rect[3] > 0.0).then_some(Frame { rect, size: Some(size) })
}
None if player_viewport && !cv.viewport_set && cv.map.uses_extents() => {
let ve = cv.map.viewport_ext();
Some(Frame { rect: [0.0, 0.0, ve.0, ve.1], size: None })
}
None if cv.window_set => Some(Frame { rect: cv.map.window_rect(), size: None }),
None => None,
};
Ok((cv, frame))
}
fn record(cv: &mut Canvas, func: u16, r: Bytes) {
if cv.full() {
return;
}
let _ = match func {
0x0103 => r.u16(6).map(|m| cv.set_map_mode(u32::from(m))),
0x0106 => r.u16(6).map(|m| cv.set_polyfill(u32::from(m))),
0x0104 => r.u16(6).map(|m| cv.set_rop2(u32::from(m))),
// Y is stored before X in these records.
0x020B => xy16(r, 6).map(|p| cv.set_window_org(p)),
0x020C => xy16(r, 6).map(|p| cv.set_window_ext(p)),
0x020D => xy16(r, 6).map(|p| cv.set_viewport_org(p)),
0x020E => xy16(r, 6).map(|p| cv.set_viewport_ext(p)),
0x001E => {
cv.save_dc();
Some(())
}
0x0127 => r.i16(6).map(|n| cv.restore_dc(i32::from(n))),
0x0214 => xy16(r, 6).map(|p| cv.move_to(p)),
0x0213 => xy16(r, 6).map(|p| cv.line_to(p)),
0x0325 => counted_points(r).map(|pts| cv.poly(&pts, false, false, true)),
0x0324 => counted_points(r).map(|pts| cv.poly(&pts, true, true, true)),
0x0538 => polypolygon(r).map(|ops| cv.shape(&ops, true, true)),
0x041B => rect16(r, 6).map(|[l, t, rr, b]| cv.shape(&geom::rect(l, t, rr, b), true, true)),
0x061C => {
let [ew, eh] = [r.i16(8), r.i16(6)];
let (Some(ew), Some(eh)) = (ew, eh) else { return };
rect16(r, 10).map(|[l, t, rr, b]| {
cv.shape(&geom::round_rect(l, t, rr, b, f64::from(ew), f64::from(eh)), true, true);
})
}
0x0415 => rect16(r, 6).map(|rect| cv.exclude_clip_rect(rect)),
0x0416 => rect16(r, 6).map(|rect| cv.intersect_clip_rect(rect)),
0x0418 => rect16(r, 6).map(|[l, t, rr, b]| cv.shape(&geom::ellipse(l, t, rr, b), true, true)),
0x0817 | 0x081A | 0x0830 => arc16(r).map(|(bounds, s, e)| {
let kind = match func {
0x0817 => ArcKind::Arc,
0x081A => ArcKind::Pie,
_ => ArcKind::Chord,
};
let [l, t, rr, b] = bounds;
cv.shape(&geom::arc_shape(l, t, rr, b, s, e, kind), kind != ArcKind::Arc, true);
}),
0x02FA => create_pen(cv, r),
0x02FC => create_brush(cv, r),
0x02FB => {
let f = font(r).unwrap_or_default();
cv.add_object(None, Obj::Font(Arc::new(f)));
Some(())
}
0x0142 => {
// A pattern the reader cannot decode still takes its handle slot.
let b = dib_pattern_brush(r).map_or(Obj::Other, Obj::Brush);
cv.add_object(None, b);
Some(())
}
// Palettes, old-style pattern brushes and regions take a handle slot too, so later
// SELECTOBJECT handles count them; selecting one draws nothing.
0x00F7 | 0x01F9 | 0x06FF => {
cv.add_object(None, Obj::Other);
Some(())
}
0x0209 => r.u32(6).map(|c| cv.set_text_color(c)),
0x0201 => r.u32(6).map(|c| cv.set_bk_color(c)),
0x0102 => r.u16(6).map(|m| cv.set_bk_mode(u32::from(m))),
0x012E => r.u16(6).map(|a| cv.set_text_align(u32::from(a))),
0x0A32 => ext_text_out(cv, r),
0x0521 => text_out(cv, r),
// PATBLT: rop(4), height, width, y, x.
0x061D => r.u32(6).map(|rop| cv.fill_rop([i16s(r, 16), i16s(r, 14), i16s(r, 12), i16s(r, 10)], rop)),
0x012D => r.u16(6).map(|h| cv.select(wmf_handle(h))),
0x01F0 => r.u16(6).map(|h| cv.delete_object(u32::from(h))),
// STRETCHDIB: rop(4) usage(2) srcH srcW ySrc xSrc destH destW yDest xDest, then the DIB.
0x0F43 => {
let rop = r.u32(6).unwrap_or(0);
dib_blit(cv, r, 28, rop, [i16s(r, 26), i16s(r, 24), i16s(r, 22), i16s(r, 20)], [i16s(r, 18), i16s(r, 16), i16s(r, 14), i16s(r, 12)])
}
0x0B41 => dib_stretchblt(cv, r),
0x0940 => dib_bitblt(cv, r),
_ => Some(()),
};
}
/// A 16-bit WMF handle as a canvas object index: stock objects are `0x8000 | index`, which the
/// canvas reads with the 32-bit stock flag.
fn wmf_handle(h: u16) -> u32 {
let h = u32::from(h);
if h & 0x8000 != 0 { (h & 0x7FFF) | 0x8000_0000 } else { h }
}
/// An i16 field as f64; 0 past the end.
fn i16s(r: Bytes, off: usize) -> f64 {
r.i16(off).map_or(0.0, f64::from)
}
/// CREATEPENINDIRECT: style, width (x, y), colour. Only the width's x component is used.
fn create_pen(cv: &mut Canvas, r: Bytes) -> Option<()> {
let style = r.u16(6)?;
let width = r.i16(8)?;
let color = r.u32(12)?;
cv.add_object(None, Obj::Pen(Pen { style: u32::from(style), width: f64::from(width), color: colorref(color) }));
Some(())
}
/// CREATEBRUSHINDIRECT: style, colour, hatch.
fn create_brush(cv: &mut Canvas, r: Bytes) -> Option<()> {
let style = u32::from(r.u16(6)?);
let color = r.u32(8)?;
let mut b = Brush::new(style, colorref(color));
if style == BS_HATCHED {
b.hatch = u32::from(r.u16(12).unwrap_or(0));
}
cv.add_object(None, Obj::Brush(b));
Some(())
}
/// DIBCREATEPATTERNBRUSH: style, colour usage, then a DIB (MS-WMF 2.3.4.8). The DIB's own palette
/// gives the colours; with DIB_PAL_COLORS (indices into a palette the reader does not keep) a
/// monochrome DIB takes the text and background colours.
fn dib_pattern_brush(r: Bytes) -> Option<Brush> {
let usage = r.u16(8)?;
let dib = r.tail(10)?;
let n = dib::info_len(dib)?;
let img = dib::decode(Bytes(dib.slice(0, n)?), dib.tail(n)?, false)?;
Brush::pattern(img, usage == 1)
}
/// Font object (MS-WMF 2.2.1.2): height, width, escapement, orientation, weight (i16 each), italic,
/// underline, strike-out and charset bytes, three precision/quality bytes, pitch and family, then the
/// face name (up to 32 bytes, NUL-terminated, in the font's character set).
fn font(r: Bytes) -> Option<LogFont> {
let height = r.i16(6)?;
let width = r.i16(8).unwrap_or(0);
let escapement = r.i16(10).unwrap_or(0);
let weight = r.i16(14).unwrap_or(0);
let byte = |o: usize| r.0.get(o).copied().unwrap_or(0);
let charset = byte(19);
let name = r.0.get(24..).unwrap_or(&[]);
let name = name.get(..32).unwrap_or(name);
let name = name.split(|b| *b == 0).next().unwrap_or(&[]);
// Face names are plain ASCII in practice; other bytes go through the Western code page.
let (family, _) = decode_bytes(name, 0, &[]);
Some(LogFont {
font: Font {
family: family.trim().to_string(),
height: f32::from(height),
width: f32::from(width),
weight: weight.clamp(0, 1000) as u16,
italic: byte(16) != 0,
underline: byte(17) != 0,
strike: byte(18) != 0,
charset,
},
escapement: f64::from(escapement),
})
}
/// The charset of the selected font, for decoding 8-bit text.
fn charset(cv: &Canvas) -> u8 {
cv.font_charset()
}
/// EXTTEXTOUT (MS-WMF 2.3.3.5): y, x, string length, options, the rectangle (left, top, right,
/// bottom) when ETO_OPAQUE or ETO_CLIPPED is set, the string padded to a word, then an optional dx
/// array of i16 advances.
fn ext_text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
let (y, x) = (r.i16(6)?, r.i16(8)?);
let n = usize::from(r.u16(10)?).min(MAX_CHARS);
let opts = u32::from(r.u16(12)?);
let has_rect = opts & (ETO_OPAQUE | ETO_CLIPPED) != 0;
let rect = if has_rect { Some([i16s(r, 14), i16s(r, 16), i16s(r, 18), i16s(r, 20)]) } else { None };
let at = if has_rect { 22 } else { 14 };
// A string running past the record is cut to what the record holds.
let avail = r.0.len().saturating_sub(at);
let n = n.min(avail);
let bytes = r.slice(at, n)?;
let dx_at = at + n.next_multiple_of(2);
let dx: Vec<f32> = if r.0.len() >= dx_at + 2 * n { (0..n).filter_map(|i| r.i16(dx_at + 2 * i)).map(f32::from).collect() } else { Vec::new() };
let (text, dx) = decode_bytes(bytes, charset(cv), &dx);
cv.text((f64::from(x), f64::from(y)), text, dx, opts, rect);
Some(())
}
/// TEXTOUT (MS-WMF 2.3.3.20): string length, the string padded to a word, then y and x.
fn text_out(cv: &mut Canvas, r: Bytes) -> Option<()> {
let n = usize::from(r.u16(6)?).min(MAX_CHARS);
let bytes = r.slice(8, n)?;
let at = 8 + n.next_multiple_of(2);
let (y, x) = (r.i16(at)?, r.i16(at + 2)?);
let (text, _) = decode_bytes(bytes, charset(cv), &[]);
cv.text((f64::from(x), f64::from(y)), text, Vec::new(), 0, None);
Some(())
}
/// (x, y) from a Y-then-X pair stored at `off`.
fn xy16(r: Bytes, off: usize) -> Option<P> {
Some((f64::from(r.i16(off + 2)?), f64::from(r.i16(off)?)))
}
/// Rectangle stored as bottom, right, top, left (the order MS-WMF uses for RECTANGLE and ELLIPSE).
fn rect16(r: Bytes, off: usize) -> Option<[f64; 4]> {
let (b, rr, t, l) = (r.i16(off)?, r.i16(off + 2)?, r.i16(off + 4)?, r.i16(off + 6)?);
Some([f64::from(l), f64::from(t), f64::from(rr), f64::from(b)])
}
/// ARC, PIE and CHORD: end Y, end X, start Y, start X, then the bounding rectangle (bottom, right, top, left).
fn arc16(r: Bytes) -> Option<([f64; 4], P, P)> {
let end = xy16(r, 6)?;
let start = xy16(r, 10)?;
let bounds = rect16(r, 14)?;
Some((bounds, start, end))
}
/// A count of 16-bit points, then the points (POLYLINE, POLYGON).
fn counted_points(r: Bytes) -> Option<Vec<P>> {
let n = r.u16(6)? as usize;
r.tail(8)?.points(0, n, false)
}
/// POLYPOLYGON: polygon count, one 16-bit point count per polygon, then all points.
fn polypolygon(r: Bytes) -> Option<Vec<geom::Op>> {
let n = r.u16(6)? as usize;
let mut counts = Vec::with_capacity(n);
for i in 0..n {
counts.push(r.u16(i.checked_mul(2)?.checked_add(8)?)? as usize);
}
let total = counts.iter().try_fold(0usize, |a, c| a.checked_add(*c))?;
if total > MAX_PATH_OPS {
return None;
}
let pts = r.tail(n.checked_mul(2)?.checked_add(8)?)?.points(0, total, false)?;
Some(geom::subpaths(&pts, &counts, true))
}
/// Whether a DIBBITBLT or DIBSTRETCHBLT record carries a bitmap: without one it is exactly
/// `(function >> 8) + 3` words long (MS-WMF 2.3.1.2, 2.3.1.3).
fn has_bitmap(func: u16, r: Bytes) -> bool {
r.0.len() / 2 > usize::from(func >> 8) + 3
}
/// META_DIBBITBLT: rop(4), ySrc, xSrc, then height, width, yDest, xDest and the DIB at 22. The
/// bitmap-less form has a reserved word before the height, so its fields sit 2 bytes later; it, or
/// a source-less raster operation, fills the destination.
fn dib_bitblt(cv: &mut Canvas, r: Bytes) -> Option<()> {
let rop = r.u32(6)?;
if !has_bitmap(0x0940, r) {
cv.fill_rop([i16s(r, 22), i16s(r, 20), i16s(r, 18), i16s(r, 16)], rop);
return Some(());
}
let dst = [i16s(r, 20), i16s(r, 18), i16s(r, 16), i16s(r, 14)];
if Canvas::sourceless(rop) {
cv.fill_rop(dst, rop);
return Some(());
}
let src = [i16s(r, 12), i16s(r, 10), dst[2], dst[3]];
dib_blit(cv, r, 22, rop, dst, src)
}
/// META_DIBSTRETCHBLT: rop(4), srcH, srcW, ySrc, xSrc, then destH, destW, yDest, xDest and the DIB at
/// 26. The bitmap-less form has a reserved word after xSrc, so the destination sits 2 bytes later;
/// it, or a source-less raster operation, fills the destination.
fn dib_stretchblt(cv: &mut Canvas, r: Bytes) -> Option<()> {
let rop = r.u32(6)?;
if !has_bitmap(0x0B41, r) {
cv.fill_rop([i16s(r, 26), i16s(r, 24), i16s(r, 22), i16s(r, 20)], rop);
return Some(());
}
let dst = [i16s(r, 24), i16s(r, 22), i16s(r, 20), i16s(r, 18)];
if Canvas::sourceless(rop) {
cv.fill_rop(dst, rop);
return Some(());
}
let src = [i16s(r, 16), i16s(r, 14), i16s(r, 12), i16s(r, 10)];
dib_blit(cv, r, 26, rop, dst, src)
}
/// A DIB at `at`, stretched from source rectangle `src` into `dst` (both in logical units, pixels for `src`).
/// The raster operation `rop` decides how the pixels combine (see [`dib::prepare`]).
fn dib_blit(cv: &mut Canvas, r: Bytes, at: usize, rop: u32, dst: [f64; 4], src: [f64; 4]) -> Option<()> {
let dib = r.tail(at)?;
let n = dib::info_len(dib)?;
let info = dib.slice(0, n)?;
let bits = dib.tail(n)?;
let mut img = dib::decode(Bytes(info), bits, false)?;
dib::prepare(&mut img, dib::Blend::Rop(rop));
let (img, dst) = dib::clip_blit(&img, src.map(|v| v as i64), dst)?;
cv.bitmap(dst, img);
Some(())
}
+1 -1
View File
@@ -11,7 +11,7 @@ workspace = true
[dependencies]
deckcraft-geom = { workspace = true }
deckcraft-color = { workspace = true }
deckcraft-metafile = { workspace = true }
craft-metafile = { workspace = true }
deckcraft-model = { workspace = true }
deckcraft-anim = { workspace = true }
deckcraft-fonts = { workspace = true }
+51 -18
View File
@@ -1,5 +1,5 @@
//! Windows Metafile and Enhanced Metafile pictures (`image/x-wmf`, `image/x-emf`): parsed by
//! `deckcraft-metafile` and drawn as vectors into a bitmap of the size the slide needs, so they go
//! `craft-metafile` and drawn as vectors into a bitmap of the size the slide needs, so they go
//! through the same crop, tile and recolour path as any other picture.
//!
//! Text items name the font the file asks for; they are drawn with the face slide text would use
@@ -10,8 +10,8 @@
use std::collections::HashMap;
use std::sync::Arc;
use craft_metafile::{Cap, HAlign, Item, Join, LineStyle, PlacedClip, PlacedItem, Text, Tile, VAlign};
use deckcraft_fonts::{FontDb, FontFace};
use deckcraft_metafile::{HAlign, Item, PlacedItem, Text, Tile, VAlign};
use kurbo::{Affine, BezPath, Rect, Shape as _};
use skrifa::MetadataProvider as _;
use vello_cpu::{Pixmap, RenderContext, Resources, peniko};
@@ -28,13 +28,13 @@ const DEFAULT_EM: f64 = 12.0;
/// Is `bytes` a WMF or EMF file?
pub fn is_metafile(bytes: &[u8]) -> bool {
deckcraft_metafile::is_metafile(bytes)
craft_metafile::is_metafile(bytes)
}
/// Draw the metafile `bytes` into straight RGBA with its longer side `side` px (transparent where
/// it draws nothing). `None` when it doesn't parse or has no size.
pub fn rasterize(bytes: &[u8], side: u32) -> Option<image::RgbaImage> {
let mut pic = deckcraft_metafile::parse(bytes).ok()?;
let mut pic = craft_metafile::parse(bytes).ok()?;
if !pic.has_size() {
return None;
}
@@ -51,31 +51,39 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option<image::RgbaImage> {
ctx.push_clip_layer(&r.to_path(0.1));
for it in pic.place(r, [0.0; 4]) {
match it {
PlacedItem::Path { path, fill, stroke, even_odd, region } => clipped(&mut ctx, region.as_deref(), |ctx| {
PlacedItem::Path { path, fill, stroke, line, even_odd, region, clips } => clipped(&mut ctx, region.as_deref(), &clips, |ctx| {
if let Some(f) = fill {
ctx.set_paint(rgba(f));
ctx.set_fill_rule(if even_odd { peniko::Fill::EvenOdd } else { peniko::Fill::NonZero });
ctx.fill_path(&path);
}
// GDI pens: round caps and joins; a cosmetic (zero-width) pen is one pixel.
// The pen's caps, joins and dashes (GDI's default: round, solid); a cosmetic
// (zero-width) pen is one pixel.
if let Some((s, sw)) = stroke {
ctx.set_paint(rgba(s));
ctx.set_stroke(kurbo::Stroke::new(sw.max(1.0)).with_caps(kurbo::Cap::Round).with_join(kurbo::Join::Round));
ctx.set_stroke(pen(sw.max(1.0), &line));
ctx.stroke_path(&path);
}
}),
PlacedItem::Bitmap { index, rect, region } => clipped(&mut ctx, region.as_deref(), |ctx| {
PlacedItem::Bitmap { index, rect, transform, region, clips } => clipped(&mut ctx, region.as_deref(), &clips, |ctx| {
let Some(Some(pm)) = pixmaps.get(index) else { return };
let (bw, bh) = (pm.width().max(1) as f64, pm.height().max(1) as f64);
ctx.set_paint(vello_cpu::Image {
image: vello_cpu::ImageSource::Pixmap(pm.clone()),
sampler: peniko::ImageSampler::default().with_quality(peniko::ImageQuality::Medium),
});
ctx.set_paint_transform(Affine::translate((rect.x0, rect.y0)) * Affine::scale_non_uniform(rect.width() / bw, rect.height() / bh));
ctx.fill_rect(&rect);
let c = transform.as_coeffs();
if c[1] == 0.0 && c[2] == 0.0 {
ctx.set_paint_transform(Affine::translate((rect.x0, rect.y0)) * Affine::scale_non_uniform(rect.width() / bw, rect.height() / bh));
ctx.fill_rect(&rect);
} else {
// A turned or skewed bitmap: `transform` maps the unit square onto its place.
ctx.set_paint_transform(transform * Affine::scale_non_uniform(1.0 / bw, 1.0 / bh));
ctx.fill_path(&(transform * Rect::new(0.0, 0.0, 1.0, 1.0).to_path(0.1)));
}
ctx.reset_paint_transform();
}),
PlacedItem::Pattern { index, path, even_odd, tile, region } => clipped(&mut ctx, region.as_deref(), |ctx| {
PlacedItem::Pattern { index, path, even_odd, tile, region, clips } => clipped(&mut ctx, region.as_deref(), &clips, |ctx| {
let Some(Item::Pattern { tile: t, .. }) = pic.items.get(index) else { return };
let key = Arc::as_ptr(t);
let (pm, avg) = tiles.entry(key).or_insert_with(|| (premul(t.width, t.height, &t.rgba), average(&t.rgba))).clone();
@@ -102,7 +110,7 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option<image::RgbaImage> {
}
}
}),
PlacedItem::Text { index, transform, clip, region } => clipped(&mut ctx, region.as_deref(), |ctx| {
PlacedItem::Text { index, transform, clip, region, clips } => clipped(&mut ctx, region.as_deref(), &clips, |ctx| {
let Some(Item::Text(t)) = pic.items.get(index) else { return };
if let Some(c) = clip {
ctx.push_clip_layer(&c.to_path(0.1));
@@ -133,15 +141,40 @@ pub fn rasterize(bytes: &[u8], side: u32) -> Option<image::RgbaImage> {
image::RgbaImage::from_raw(w as u32, h as u32, px)
}
/// Draws `draw` inside the device-context clip. No region is the default clip.
fn clipped(ctx: &mut RenderContext, region: Option<&BezPath>, draw: impl FnOnce(&mut RenderContext)) {
/// Draws `draw` inside the device-context clip and every clip path. No region is the default clip.
fn clipped(ctx: &mut RenderContext, region: Option<&BezPath>, clips: &[Arc<PlacedClip>], draw: impl FnOnce(&mut RenderContext)) {
if let Some(region) = region {
ctx.push_clip_layer(region);
draw(ctx);
ctx.pop_layer();
} else {
draw(ctx);
}
for c in clips {
ctx.set_fill_rule(if c.even_odd { peniko::Fill::EvenOdd } else { peniko::Fill::NonZero });
ctx.push_clip_layer(&c.path);
}
if !clips.is_empty() {
ctx.set_fill_rule(peniko::Fill::NonZero);
}
draw(ctx);
for _ in 0..clips.len() + usize::from(region.is_some()) {
ctx.pop_layer();
}
}
/// A GDI pen as a stroke `width` wide (target units): its caps, joins, miter limit and dashes.
fn pen(width: f64, line: &LineStyle) -> kurbo::Stroke {
let cap = match line.cap {
Cap::Round => kurbo::Cap::Round,
Cap::Square => kurbo::Cap::Square,
Cap::Flat => kurbo::Cap::Butt,
};
let join = match line.join {
Join::Round => kurbo::Join::Round,
Join::Bevel => kurbo::Join::Bevel,
Join::Miter => kurbo::Join::Miter,
};
let stroke = kurbo::Stroke::new(width).with_caps(cap).with_join(join).with_miter_limit(f64::from(line.miter_limit));
// A dash pattern with no length would never advance: such a pen draws solid.
let dash: Vec<f64> = line.dash.iter().map(|d| f64::from(*d)).collect();
if dash.iter().all(|d| d.is_finite() && *d >= 0.0) && dash.iter().sum::<f64>() > 0.0 { stroke.with_dashes(0.0, dash) } else { stroke }
}
/// One glyph placed in text space (x along the baseline from the string's start, font units at
+13
View File
@@ -1962,6 +1962,19 @@ fn mf_ink(img: &image::RgbaImage) -> Option<(u32, u32, u32, u32)> {
b
}
#[test]
fn metafile_dashed_pen_leaves_gaps() {
// CREATEPENINDIRECT PS_DASH | PS_ENDCAP_FLAT, 40 units (4 px) wide, black, then a line across
// the picture at y = 72 px: 12 px dashes with 4 px gaps.
let mut pen = i16s(&[0x0201, 40, 0]);
pen.extend(0u32.to_le_bytes());
let recs = vec![(0x02FA, pen), (0x012D, i16s(&[0])), (0x0214, i16s(&[720, 100])), (0x0213, i16s(&[720, 2780]))];
let img = crate::metafile::rasterize(&wmf_with(2880, 1440, &recs), 288).expect("raster");
let ink: Vec<bool> = (20..260).map(|x| img.get_pixel(x, 72)[3] > 128).collect();
assert!(ink.iter().filter(|b| **b).count() > 120, "dashes are drawn");
assert!(ink.iter().filter(|b| !**b).count() > 30, "with gaps between them");
}
#[test]
fn metafile_text_ink_lands_at_its_reference_point() {
// 2 × 1 inch (144 × 72 pt) drawn 288 px wide: 2 px per point. A 36-point "HH", top-left
+2 -1
View File
@@ -15,7 +15,7 @@ nothing below L6 uses egui, eframe, winit or rfd.
| Layer | Crates | What they do |
|---|---|---|
| L0 | `geom`, `color`, `metafile`; standalone `bitstream` | EMU/pt units, transforms, ~180 preset shape generators re-derived from ECMA-376, adjust handles, hit testing; RGB/HSL and DrawingML colour transforms; bit-level I/O and the video decoders' memory budget (`bitstream`); EMF/WMF read into a neutral vector model (`metafile`) |
| L0 | `geom`, `color`; standalone `bitstream` | EMU/pt units, transforms, ~180 preset shape generators re-derived from ECMA-376, adjust handles, hit testing; RGB/HSL and DrawingML colour transforms; bit-level I/O and the video decoders' memory budget (`bitstream`) |
| L1 | `model`, `fonts`; `h264`, `hevc`, `vp9`, `av1` | Presentation, masters, layouts, slides, shapes, text, themes, tables, charts, transitions, animations, placeholder inheritance (`resolve`); font database (craft-fonts + system) with metric-compatible substitutes for Office fonts (Carlito for Calibri, Caladea for Cambria, Liberation for Arial/Times); video decoders |
| L2 | `text`, `anim`, `format`, `media` | Shaping (HarfRust), UAX #9 bidi, UAX #14 line breaking, autofit, caret hit testing; per-shape animation state over time, the show state machine, transition frame layouts, Morph matching; `.deckcraft` (zip + JSON + media) and outline text; media probing, decoding, mixer and clock (MP4/MOV, MKV/WebM and Ogg demuxers from craft-media and the Opus decoder from craft-audio, both in craft-libs) |
| L3 | `render`, `pptx` | `vello_cpu` raster of slides, masters and layouts (fills, outlines, effects, pictures, tables, charts, Morph frames); PPTX import/export from ECMA-376 |
@@ -81,6 +81,7 @@ macOS, Windows, Linux, FreeBSD and web artifacts.
| Date | Change | Summary |
|---|---|---|
| 2026-11-12 | trivial | The `metafile` crate is gone: `render` reads EMF/WMF with craft-metafile from craft-libs |
| 2026-11-11 | trivial | The `opus`, `ogg`, `isobmff` and `matroska` crates are gone: `media` uses craft-audio and craft-media from craft-libs |
| 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged |
| 2026-10-14 | minor | Community PR wave folded: one L2/L5/L6 row each; command registry 320 (`cmd!` registrations in `cmd/*`), 41 dialogs (titled in `dialogs.rs`), 202k lines, 24 crates (`metafile` added at L0), 1,641 tests. The per-PR rows below give the counts each PR saw on its own branch |
-2
View File
@@ -44,8 +44,6 @@ pub const TABLE: &[(&str, Class)] = &[
("av1", Class::Layer(1)),
("media", Class::Layer(2)),
("color", Class::Layer(0)),
// WMF/EMF reader (ported from WordCraft).
("metafile", Class::Layer(0)),
("model", Class::Layer(1)),
("fonts", Class::Layer(1)),
("text", Class::Layer(2)),