mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Metafile: EMF+ cardinal curves and region fills
DrawCurve, DrawClosedCurve and FillClosedCurve (GDI+ cardinal splines, the smoothed lines of charts and freehand shapes) draw as cubic Béziers with the record's tension, offset and segment count; FillRegion fills the region object's area. These records were skipped, so the shapes were missing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -328,6 +328,39 @@ impl Plus {
|
||||
self.painter().stroke(cv, &ops, pen);
|
||||
Some(())
|
||||
}
|
||||
// Cardinal splines: FillClosedCurve, DrawClosedCurve, DrawCurve.
|
||||
0x4016 => {
|
||||
let fill = self.brush_of(flags, r.u32(0)?)?;
|
||||
let tension = finite(r.f32(4)?)?;
|
||||
let pts = points(r, 12, r.u32(8)? as usize, compressed)?;
|
||||
self.painter().fill(cv, &curve_ops(&pts, tension, true, 0, usize::MAX), &fill);
|
||||
Some(())
|
||||
}
|
||||
0x4017 => {
|
||||
let pen = self.pen(id)?;
|
||||
let tension = finite(r.f32(0)?)?;
|
||||
let pts = points(r, 8, r.u32(4)? as usize, compressed)?;
|
||||
self.painter().stroke(cv, &curve_ops(&pts, tension, true, 0, usize::MAX), pen);
|
||||
Some(())
|
||||
}
|
||||
0x4018 => {
|
||||
let pen = self.pen(id)?;
|
||||
let tension = finite(r.f32(0)?)?;
|
||||
let (offset, segments) = (r.u32(4)? as usize, r.u32(8)? as usize);
|
||||
let pts = points(r, 16, r.u32(12)? as usize, compressed)?;
|
||||
self.painter().stroke(cv, &curve_ops(&pts, tension, false, offset, segments), pen);
|
||||
Some(())
|
||||
}
|
||||
// FillRegion: the region's area (as the clip reads it).
|
||||
0x4013 => {
|
||||
let fill = self.brush_of(flags, r.u32(0)?)?;
|
||||
let Some(Some(Obj::Region(Some(area)))) = self.objects.get(id).cloned() else { return None };
|
||||
if area.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
self.painter().fill(cv, &poly_ops(&area, true), &fill);
|
||||
Some(())
|
||||
}
|
||||
0x401A => {
|
||||
let img = self.image(id)?;
|
||||
let dst = *rects(r, 24, 1, compressed)?.first()?;
|
||||
@@ -1246,6 +1279,38 @@ fn rect_ops([l, t, r, b]: [f64; 4]) -> Vec<Op> {
|
||||
crate::geom::rect(l, t, r, b)
|
||||
}
|
||||
|
||||
fn finite(v: f32) -> Option<f64> {
|
||||
v.is_finite().then_some(f64::from(v))
|
||||
}
|
||||
|
||||
/// A GDI+ cardinal spline through `pts` as cubic Béziers: each segment's control points lie a
|
||||
/// third of `tension` (GDI+'s default 0.5) along the neighbouring chords. `closed` joins the
|
||||
/// last point back to the first; an open curve draws `segments` segments from point `offset`.
|
||||
fn curve_ops(pts: &[P], tension: f64, closed: bool, offset: usize, segments: usize) -> Vec<Op> {
|
||||
let n = pts.len();
|
||||
if n < 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
let k = tension.clamp(0.0, 10.0) / 3.0;
|
||||
let at = |i: isize| -> P {
|
||||
let i = if closed { i.rem_euclid(n as isize) as usize } else { i.clamp(0, n as isize - 1) as usize };
|
||||
pts.get(i).copied().unwrap_or((0.0, 0.0))
|
||||
};
|
||||
let (first, count) = if closed { (0, n) } else { (offset.min(n - 1), segments.min(n - 1 - offset.min(n - 1))) };
|
||||
let mut ops = vec![Op::M(at(first as isize))];
|
||||
for s in first..first + count {
|
||||
let i = s as isize;
|
||||
let (p0, p1, p2, p3) = (at(i - 1), at(i), at(i + 1), at(i + 2));
|
||||
let c1 = (p1.0 + (p2.0 - p0.0) * k, p1.1 + (p2.1 - p0.1) * k);
|
||||
let c2 = (p2.0 - (p3.0 - p1.0) * k, p2.1 - (p3.1 - p1.1) * k);
|
||||
ops.push(Op::C(c1, c2, p2));
|
||||
}
|
||||
if closed {
|
||||
ops.push(Op::Z);
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
fn poly_ops(pts: &[P], close: bool) -> Vec<Op> {
|
||||
let mut ops: Vec<Op> = pts.iter().enumerate().map(|(i, p)| if i == 0 { Op::M(*p) } else { Op::L(*p) }).collect();
|
||||
if close {
|
||||
|
||||
@@ -1849,3 +1849,63 @@ fn emfplus_nested_metafile_image_draws_its_items_into_the_destination() {
|
||||
assert_eq!(fill, Some([255, 255, 255, 255]));
|
||||
assert!(segs.contains(&Seg::Line(40.0, 20.0)), "{segs:?}");
|
||||
}
|
||||
|
||||
/// Curves and a region fill, as an EMF+ only file (also written to `$DECKCRAFT_EMF_OUT` to look at).
|
||||
fn curves_file() -> Vec<u8> {
|
||||
let pen = prec(0x4008, 0x0200 | 1, &solid_pen(0xFF20_60C0, 3.0));
|
||||
// A closed curve through a diamond's corners, filled orange.
|
||||
let diamond = f32s(&[100.0, 10.0, 190.0, 100.0, 100.0, 190.0, 10.0, 100.0]);
|
||||
let fill = prec(0x4016, 0x8000, &cat(vec![h32(0xFFE0_8020), f32s(&[0.5]), h32(4), diamond.clone()]));
|
||||
let outline = prec(0x4017, 1, &cat(vec![f32s(&[0.5]), h32(4), diamond]));
|
||||
// An open curve (a smoothed line chart): all four segments of five points.
|
||||
let line =
|
||||
prec(0x4018, 1, &cat(vec![f32s(&[0.5]), h32(0), h32(4), h32(5), f32s(&[210.0, 150.0, 260.0, 60.0, 310.0, 120.0, 360.0, 30.0, 410.0, 90.0])]));
|
||||
// A green rectangle region, filled.
|
||||
let region = cat(vec![h32(0xDBC0_1002), h32(0), h32(0x1000_0000), f32s(&[220.0, 160.0, 180.0, 30.0])]);
|
||||
let fill_region = prec(0x4013, 0x8000 | 2, &h32(0xFF30_A040));
|
||||
let mut all = vec![plus_header(false), pen, fill, outline, line, prec(0x4008, 0x0400 | 2, ®ion), fill_region];
|
||||
all.push(prec(0x4002, 0, &[]));
|
||||
emf(&[plus_comment(&all)], [0, 0, 420, 200], [0; 4])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emfplus_cardinal_curves_and_region_fill() {
|
||||
let bytes = curves_file();
|
||||
if let Ok(path) = std::env::var("DECKCRAFT_EMF_OUT") {
|
||||
let _ = std::fs::write(path, &bytes);
|
||||
}
|
||||
let p = parse(&bytes).unwrap();
|
||||
assert_eq!(p.items.len(), 4, "{:?}", p.items.len());
|
||||
// The closed curve: four cubics through the diamond's corners, closed.
|
||||
let (segs, fill, ..) = path_segs(&p.items[0]);
|
||||
assert_eq!(fill, Some([0xE0, 0x80, 0x20, 0xFF]));
|
||||
let cubics: Vec<(f32, f32)> = segs.iter().filter_map(|s| if let Seg::Cubic(.., x, y) = s { Some((*x, *y)) } else { None }).collect();
|
||||
assert_eq!(cubics.len(), 4, "{segs:?}");
|
||||
assert_eq!(segs.last(), Some(&Seg::Close));
|
||||
// The open curve: four cubics and no close; its controls bulge past the straight chords.
|
||||
let (segs, fill, stroke, _) = path_segs(&p.items[2]);
|
||||
assert_eq!((fill, stroke.map(|s| s.0)), (None, Some([0x20, 0x60, 0xC0, 0xFF])));
|
||||
assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Cubic(..))).count(), 4);
|
||||
assert!(!segs.contains(&Seg::Close));
|
||||
// The region fill is its rectangle.
|
||||
let (segs, fill, ..) = path_segs(&p.items[3]);
|
||||
assert_eq!(fill, Some([0x30, 0xA0, 0x40, 0xFF]));
|
||||
assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Line(..))).count(), 3, "{segs:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emfplus_curve_offsets_and_degenerate_input_are_safe() {
|
||||
let pen = prec(0x4008, 0x0200 | 1, &solid_pen(0xFF00_0000, 1.0));
|
||||
// Segments from point 2 of 5, asking for more than there are: two segments.
|
||||
let line = prec(0x4018, 1, &cat(vec![f32s(&[0.5]), h32(2), h32(99), h32(5), f32s(&[0.0, 0.0, 10.0, 5.0, 20.0, 0.0, 30.0, 5.0, 40.0, 0.0])]));
|
||||
let p = plus_pic(vec![pen.clone(), line]);
|
||||
let (segs, ..) = path_segs(&p.items[0]);
|
||||
assert_eq!(segs.iter().filter(|s| matches!(s, Seg::Cubic(..))).count(), 2, "{segs:?}");
|
||||
// One point, an offset past the end, a NaN tension, a missing region: nothing, no panic.
|
||||
let one = prec(0x4018, 1, &cat(vec![f32s(&[0.5]), h32(0), h32(1), h32(1), f32s(&[1.0, 1.0])]));
|
||||
let past = prec(0x4018, 1, &cat(vec![f32s(&[0.5]), h32(9), h32(9), h32(2), f32s(&[0.0, 0.0, 9.0, 9.0])]));
|
||||
let nan = prec(0x4017, 1, &cat(vec![f32s(&[f32::NAN]), h32(3), f32s(&[0.0, 0.0, 9.0, 9.0, 0.0, 9.0])]));
|
||||
let no_region = prec(0x4013, 0x8000 | 7, &h32(0xFF00_0000));
|
||||
let p = plus_pic(vec![pen, one, past, nan, no_region]);
|
||||
assert!(p.items.iter().all(|i| path_segs(i).0.iter().filter(|s| matches!(s, Seg::Cubic(..))).count() == 0));
|
||||
}
|
||||
|
||||
@@ -1711,3 +1711,18 @@ fn east_asian_vertical_text_stands_ideographs_upright() {
|
||||
let (w, h) = ink(deckcraft_model::text::TextDir::Vertical);
|
||||
assert!(h > w * 3, "vert: stroke on its side {w}x{h}");
|
||||
}
|
||||
|
||||
/// Renders the EMF at `$DECKCRAFT_EMF_IN` to `$DECKCRAFT_EMF_PNG` (a look-at helper; does nothing
|
||||
/// without them).
|
||||
#[test]
|
||||
fn render_an_emf_file_to_look_at() {
|
||||
let (Ok(src), Ok(out)) = (std::env::var("DECKCRAFT_EMF_IN"), std::env::var("DECKCRAFT_EMF_PNG")) else { return };
|
||||
let Ok(bytes) = std::fs::read(&src) else { return };
|
||||
let mut p = Presentation::default();
|
||||
let media = p.add_media("image1.emf", "image/x-emf", bytes);
|
||||
let fill = deckcraft_model::style::PictureFill { media, ..Default::default() };
|
||||
let sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(40.0, 40.0, 840.0, 400.0)), kind: ShapeKind::Picture { fill }, ..Default::default() };
|
||||
std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh];
|
||||
let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() });
|
||||
let _ = std::fs::write(out, img.to_png());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user