mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
Each embedded DIB was bounded on its own (2^26 pixels), but not their total, so a small EMF or WMF of many BI_PNG/BI_JPEG records could cost minutes of decoding. The importer now gives a file 2^28 pixels for all its bitmaps, charged from each bitmap's header before it is decoded; bitmaps past it are left out with a warning.
431 lines
19 KiB
Rust
431 lines
19 KiB
Rust
//! EMF and WMF output read back record by record: headers, record sizes, paths, pens, brushes,
|
||
//! clips, images, and what each format leaves out.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use vectorcraft_color::{Color, Gradient, GradientPaint, Paint};
|
||
use vectorcraft_doc::{
|
||
Appearance, CharStyle, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, PatternDef, TextObject,
|
||
};
|
||
use vectorcraft_geom::{Affine, PathData, Point, Rect, shapes};
|
||
|
||
use crate::*;
|
||
|
||
/// Add `n` (with a fresh id) on top of `d`'s first layer.
|
||
pub(crate) fn add(d: &mut Document, mut n: Node) {
|
||
n.id = d.alloc_id();
|
||
let layer = d.layers[0].id;
|
||
let at = d.layers[0].children().map_or(0, Vec::len);
|
||
d.insert(Some(layer), at, n).unwrap();
|
||
}
|
||
|
||
/// A document of one `w` × `h` artboard holding `nodes` on its layer.
|
||
pub(crate) fn doc_with(w: f64, h: f64, nodes: Vec<Node>) -> Document {
|
||
let mut d = Document::new(w, h);
|
||
nodes.into_iter().for_each(|n| add(&mut d, n));
|
||
d
|
||
}
|
||
|
||
pub(crate) fn path(p: PathData, fill: Paint, stroke: Paint, width: f64) -> Node {
|
||
Node::path(NodeId(0), p, Appearance::basic(fill, stroke, width))
|
||
}
|
||
|
||
pub(crate) fn red() -> Paint {
|
||
Paint::solid(Color::rgb8(255, 0, 0))
|
||
}
|
||
|
||
pub(crate) fn blue() -> Paint {
|
||
Paint::solid(Color::rgb8(0, 0, 255))
|
||
}
|
||
|
||
/// A `w` × `h` PNG of `rgba`.
|
||
pub(crate) fn png(w: u32, h: u32, rgba: [u8; 4]) -> Vec<u8> {
|
||
let img = image::RgbaImage::from_pixel(w, h, image::Rgba(rgba));
|
||
let mut out = vec![];
|
||
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
|
||
out
|
||
}
|
||
|
||
/// CRC-32 of a PNG chunk.
|
||
pub(crate) fn crc32(data: &[u8]) -> u32 {
|
||
let mut crc = !0u32;
|
||
for &b in data {
|
||
crc ^= u32::from(b);
|
||
for _ in 0..8 {
|
||
crc = if crc & 1 == 1 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 };
|
||
}
|
||
}
|
||
!crc
|
||
}
|
||
|
||
/// A 1 × 1 PNG whose IHDR claims `w` × `h` (and whose CRC matches): its size reads from the header,
|
||
/// but its pixel data no longer matches, so decoding it fails.
|
||
pub(crate) fn png_claiming(w: u32, h: u32) -> Vec<u8> {
|
||
let mut png = vec![];
|
||
image::GrayImage::new(1, 1).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||
png[16..20].copy_from_slice(&w.to_be_bytes());
|
||
png[20..24].copy_from_slice(&h.to_be_bytes());
|
||
let crc = crc32(&png[12..29]);
|
||
png[29..33].copy_from_slice(&crc.to_be_bytes());
|
||
png
|
||
}
|
||
|
||
/// An image node of `bytes` placed by `xf`, its blob added to `d`.
|
||
pub(crate) fn image_node(d: &mut Document, bytes: Vec<u8>, w: u32, h: u32, xf: Affine) -> Node {
|
||
let blob = ImageBlob::new("image/png", bytes);
|
||
let key = blob.content_key();
|
||
d.images.insert(key.clone(), blob);
|
||
Node::new(
|
||
NodeId(0),
|
||
NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default(), pixels: None })),
|
||
)
|
||
}
|
||
|
||
pub(crate) fn u32_at(b: &[u8], at: usize) -> u32 {
|
||
u32::from_le_bytes(b[at..at + 4].try_into().unwrap())
|
||
}
|
||
|
||
pub(crate) fn i32_at(b: &[u8], at: usize) -> i32 {
|
||
i32::from_le_bytes(b[at..at + 4].try_into().unwrap())
|
||
}
|
||
|
||
pub(crate) fn u16_at(b: &[u8], at: usize) -> u16 {
|
||
u16::from_le_bytes(b[at..at + 2].try_into().unwrap())
|
||
}
|
||
|
||
/// The EMF's records: (type, the whole record).
|
||
pub(crate) fn emf_records(b: &[u8]) -> Vec<(u32, &[u8])> {
|
||
let mut out = vec![];
|
||
let mut at = 0;
|
||
while at < b.len() {
|
||
let size = u32_at(b, at + 4) as usize;
|
||
assert!(size >= 8 && size.is_multiple_of(4), "record size {size} at {at}");
|
||
out.push((u32_at(b, at), &b[at..at + size]));
|
||
at += size;
|
||
}
|
||
assert_eq!(at, b.len(), "records end at the end of the file");
|
||
out
|
||
}
|
||
|
||
/// The WMF's records after both headers: (function, the whole record).
|
||
pub(crate) fn wmf_records(b: &[u8]) -> Vec<(u16, &[u8])> {
|
||
let mut out = vec![];
|
||
let mut at = 22 + 18;
|
||
while at < b.len() {
|
||
let words = u32_at(b, at) as usize;
|
||
assert!(words >= 3, "record of {words} words at {at}");
|
||
out.push((u16_at(b, at + 4), &b[at..at + words * 2]));
|
||
at += words * 2;
|
||
}
|
||
assert_eq!(at, b.len());
|
||
out
|
||
}
|
||
|
||
pub(crate) fn emf(d: &Document) -> Output {
|
||
export(d, d.artboards[0].rect, Kind::Emf).unwrap()
|
||
}
|
||
|
||
pub(crate) fn wmf(d: &Document) -> Output {
|
||
export(d, d.artboards[0].rect, Kind::Wmf).unwrap()
|
||
}
|
||
|
||
fn kinds(recs: &[(u32, &[u8])]) -> Vec<u32> {
|
||
recs.iter().map(|r| r.0).collect()
|
||
}
|
||
|
||
fn count(k: &[u32], kind: u32) -> usize {
|
||
k.iter().filter(|x| **x == kind).count()
|
||
}
|
||
|
||
#[test]
|
||
fn emf_header_frame_is_the_region_in_hundredths_of_a_millimetre() {
|
||
// 72 × 144 pt is 25.4 × 50.8 mm; a rectangle from (36, 36) to (54, 72) pt.
|
||
let d = doc_with(72.0, 144.0, vec![path(shapes::rectangle(Rect::new(36.0, 36.0, 54.0, 72.0)), red(), Paint::None, 0.0)]);
|
||
let b = &emf(&d).bytes;
|
||
assert_eq!(sniff(b), Some(Kind::Emf));
|
||
assert_eq!(u32_at(b, 0), 1);
|
||
assert_eq!(u32_at(b, 40), 0x464D_4520, "the \" EMF\" signature");
|
||
// rclFrame.
|
||
assert_eq!([i32_at(b, 24), i32_at(b, 28), i32_at(b, 32), i32_at(b, 36)], [0, 0, 2540, 5080]);
|
||
// rclBounds, in device units of 0.01 mm: the rectangle.
|
||
assert_eq!([i32_at(b, 8), i32_at(b, 12), i32_at(b, 16), i32_at(b, 20)], [1270, 1270, 1905, 2540]);
|
||
// The reference device has 100 pixels a millimetre.
|
||
let (dev, mm) = ((i32_at(b, 72), i32_at(b, 76)), (i32_at(b, 80), i32_at(b, 84)));
|
||
assert_eq!(dev, (mm.0 * 100, mm.1 * 100));
|
||
assert_eq!((u32_at(b, 100), u32_at(b, 104)), (mm.0 as u32 * 1000, mm.1 as u32 * 1000), "micrometres");
|
||
// The description names the app.
|
||
let n = u32_at(b, 60) as usize;
|
||
let off = u32_at(b, 64) as usize;
|
||
assert_eq!(off, 108);
|
||
let desc: Vec<u16> = (0..n).map(|i| u16_at(b, off + 2 * i)).collect();
|
||
assert!(String::from_utf16_lossy(&desc).starts_with("VectorCraft\0"));
|
||
// An empty picture has empty bounds.
|
||
let empty = emf(&doc_with(72.0, 72.0, vec![]));
|
||
assert_eq!([i32_at(&empty.bytes, 8), i32_at(&empty.bytes, 16)], [0, -1]);
|
||
}
|
||
|
||
#[test]
|
||
fn emf_record_sizes_add_up_to_the_header_totals() {
|
||
let mut d = doc_with(200.0, 100.0, vec![]);
|
||
let img = image_node(&mut d, png(3, 2, [0, 128, 0, 255]), 3, 2, Affine::translate((10.0, 10.0)) * Affine::scale(10.0));
|
||
add(&mut d, img);
|
||
add(&mut d, path(shapes::ellipse(Rect::new(50.0, 10.0, 150.0, 90.0)), red(), blue(), 2.0));
|
||
let b = emf(&d).bytes;
|
||
let recs = emf_records(&b);
|
||
assert_eq!(u32_at(&b, 48) as usize, b.len(), "nBytes");
|
||
assert_eq!(u32_at(&b, 52) as usize, recs.len(), "nRecords");
|
||
assert_eq!(u16_at(&b, 56), 3, "nHandles: the reserved slot, a brush and a pen");
|
||
let (last, eof) = recs.last().copied().unwrap();
|
||
assert_eq!((last, eof.len()), (14, 20), "EMR_EOF ends the file");
|
||
assert_eq!(u32_at(eof, 16), 20, "nSizeLast");
|
||
// Fixed-size records have their fixed sizes.
|
||
for (k, r) in &recs {
|
||
let fixed = match k {
|
||
18 | 19 | 34 | 37 | 40 | 58 | 67 => Some(12),
|
||
27 | 54 => Some(16),
|
||
33 | 59 | 60 | 61 => Some(8),
|
||
39 | 62 | 64 => Some(24),
|
||
35 => Some(32),
|
||
36 => Some(36),
|
||
_ => None,
|
||
};
|
||
if let Some(f) = fixed {
|
||
assert_eq!(r.len(), f, "record {k}");
|
||
}
|
||
}
|
||
// StretchDIBits: its bitmap where its offsets say, 3 × 2 pixels of 24 bits (rows of 12 bytes).
|
||
let sdib = recs.iter().find(|r| r.0 == 81).unwrap().1;
|
||
assert_eq!([u32_at(sdib, 48), u32_at(sdib, 52), u32_at(sdib, 56), u32_at(sdib, 60)], [80, 40, 120, 24]);
|
||
assert_eq!(sdib.len(), 144);
|
||
}
|
||
|
||
#[test]
|
||
fn emf_paths_keep_their_curves_brushes_and_pens() {
|
||
let mut e = path(shapes::ellipse(Rect::new(10.0, 10.0, 60.0, 40.0)), red(), blue(), 4.0);
|
||
if let Some(st) = e.appearance.stroke_mut() {
|
||
st.cap = LineCap::Round;
|
||
st.join = LineJoin::Bevel;
|
||
}
|
||
let b = emf(&doc_with(100.0, 100.0, vec![e])).bytes;
|
||
let recs = emf_records(&b);
|
||
let k = kinds(&recs);
|
||
assert!(k.contains(&5), "EMR_POLYBEZIERTO keeps the curves");
|
||
assert!(k.contains(&62) && k.contains(&64), "filled, then stroked");
|
||
let brush = recs.iter().find(|r| r.0 == 39).unwrap().1;
|
||
assert_eq!(u32_at(brush, 16), 0x0000_00ff, "a red COLORREF");
|
||
let pen = recs.iter().find(|r| r.0 == 95).unwrap().1;
|
||
let style = u32_at(pen, 28);
|
||
assert_eq!(style & 0x000f_0000, 0x0001_0000, "geometric");
|
||
assert_eq!(style & 0xf00, 0, "round caps");
|
||
assert_eq!(style & 0xf000, 0x1000, "bevel joins");
|
||
assert_eq!(u32_at(pen, 32), (4.0 * 2540.0 / 72.0f64).round() as u32, "4 pt in 0.01 mm");
|
||
assert_eq!(u32_at(pen, 40), 0x00ff_0000, "a blue COLORREF");
|
||
assert!(recs.iter().any(|r| r.0 == 19 && u32_at(r.1, 8) == 2), "WINDING for non-zero");
|
||
// One brush and one pen for two shapes of the same colours.
|
||
let two = doc_with(
|
||
100.0,
|
||
100.0,
|
||
vec![
|
||
path(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), red(), blue(), 1.0),
|
||
path(shapes::rectangle(Rect::new(20.0, 0.0, 30.0, 10.0)), red(), blue(), 1.0),
|
||
],
|
||
);
|
||
let k = kinds(&emf_records(&emf(&two).bytes));
|
||
assert_eq!((count(&k, 39), count(&k, 95)), (1, 1));
|
||
}
|
||
|
||
#[test]
|
||
fn emf_dashes_are_pen_styles() {
|
||
let mut l = path(shapes::line(Point::new(10.0, 10.0), Point::new(90.0, 10.0)), Paint::None, blue(), 2.0);
|
||
l.appearance.stroke_mut().unwrap().dash = Some(Dash { pattern: vec![6.0, 3.0], offset: 0.0, align_corners: false });
|
||
let b = emf(&doc_with(100.0, 100.0, vec![l.clone()])).bytes;
|
||
let recs = emf_records(&b);
|
||
let pen = recs.iter().find(|r| r.0 == 95).unwrap().1;
|
||
assert_eq!(u32_at(pen, 28) & 0xf, 7, "PS_USERSTYLE");
|
||
assert_eq!(u32_at(pen, 48), 2);
|
||
assert_eq!([u32_at(pen, 52), u32_at(pen, 56)], [(6.0 * 2540.0 / 72.0f64).round() as u32, (3.0 * 2540.0 / 72.0f64).round() as u32]);
|
||
// With an offset, the dashes are separate lines drawn with a solid pen.
|
||
l.appearance.stroke_mut().unwrap().dash = Some(Dash { pattern: vec![6.0, 3.0], offset: 2.0, align_corners: false });
|
||
let b = emf(&doc_with(100.0, 100.0, vec![l])).bytes;
|
||
let recs = emf_records(&b);
|
||
assert_eq!(u32_at(recs.iter().find(|r| r.0 == 95).unwrap().1, 28) & 0xf, 0);
|
||
assert!(count(&kinds(&recs), 27) > 5, "a move-to for each dash");
|
||
}
|
||
|
||
#[test]
|
||
fn emf_clips_gradients_and_images() {
|
||
let g = Paint::Gradient(Box::new(GradientPaint::new(Gradient::default())));
|
||
let mut d = doc_with(100.0, 100.0, vec![path(shapes::rectangle(Rect::new(10.0, 10.0, 60.0, 30.0)), g, Paint::None, 0.0)]);
|
||
let opaque = image_node(&mut d, png(4, 4, [10, 20, 30, 255]), 4, 4, Affine::translate((0.0, 50.0)));
|
||
let clear = image_node(&mut d, png(4, 4, [10, 20, 30, 100]), 4, 4, Affine::translate((50.0, 50.0)) * Affine::rotate(0.3));
|
||
add(&mut d, opaque);
|
||
add(&mut d, clear);
|
||
let out = emf(&d);
|
||
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
|
||
let recs = emf_records(&out.bytes);
|
||
let k = kinds(&recs);
|
||
// The region's clip, then the gradient's clip around its image.
|
||
assert_eq!(count(&k, 67), 2);
|
||
assert_eq!(count(&k, 33), count(&k, 34), "every save is restored");
|
||
// The (opaque) gradient's image and the opaque image are StretchDIBits; the translucent one
|
||
// AlphaBlend with per-pixel alpha.
|
||
assert_eq!(count(&k, 81), 2);
|
||
let ab = recs.iter().find(|r| r.0 == 114).unwrap().1;
|
||
assert_eq!(&ab[40..44], &[0, 0, 255, 1]);
|
||
// Images are placed by a world transform, reset after each.
|
||
assert_eq!((count(&k, 35), count(&k, 36)), (3, 3));
|
||
}
|
||
|
||
#[test]
|
||
fn type_is_written_as_outlines() {
|
||
let t = TextObject::point(Point::new(10.0, 50.0), "Hi", CharStyle { fill: red(), ..CharStyle::default() });
|
||
let d = doc_with(100.0, 100.0, vec![Node::new(NodeId(0), NodeKind::Text(Box::new(t)))]);
|
||
let out = emf(&d);
|
||
let k = kinds(&emf_records(&out.bytes));
|
||
assert!(k.contains(&62), "glyphs are filled paths");
|
||
assert!(!k.contains(&84) && !k.contains(&83), "no text records");
|
||
// Said so (#3850).
|
||
assert!(out.warnings.iter().any(|w| w.contains("type is written as outlines")), "{:?}", out.warnings);
|
||
}
|
||
|
||
/// Ruby (furigana) is written with its base text, as the canvas draws it: one more `MoveToEx`
|
||
/// per contour of the reading's glyphs in EMF, more polygons in WMF.
|
||
#[test]
|
||
fn ruby_is_written_with_its_type() {
|
||
let typed = |ruby: bool| {
|
||
let mut t = TextObject::point(Point::new(10.0, 60.0), "AB", CharStyle { size: 30.0, fill: red(), ..CharStyle::default() });
|
||
if ruby {
|
||
t.ruby = vec![vectorcraft_doc::RubySpan::new(0, 2, "xyz")];
|
||
}
|
||
t
|
||
};
|
||
let contours = |gs: &[vectorcraft_text::PositionedGlyph]| {
|
||
gs.iter().flat_map(|g| g.outline.elements()).filter(|e| matches!(e, kurbo::PathEl::MoveTo(_))).count()
|
||
};
|
||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &typed(true));
|
||
let reading = contours(&lay.ruby);
|
||
assert!(reading >= 3, "the reading has ink: {reading}");
|
||
let doc = |ruby: bool| doc_with(100.0, 100.0, vec![Node::new(NodeId(0), NodeKind::Text(Box::new(typed(ruby))))]);
|
||
let moves = |d: &Document| count(&kinds(&emf_records(&emf(d).bytes)), crate::emf::MOVETOEX);
|
||
assert_eq!(moves(&doc(true)), moves(&doc(false)) + reading, "the reading's contours are written");
|
||
assert!(wmf(&doc(true)).bytes.len() > wmf(&doc(false)).bytes.len(), "WMF writes the reading too");
|
||
}
|
||
|
||
#[test]
|
||
fn wmf_placeable_header_checksum_and_record_sizes() {
|
||
let d = doc_with(72.0, 144.0, vec![path(shapes::ellipse(Rect::new(10.0, 10.0, 60.0, 40.0)), red(), blue(), 1.0)]);
|
||
let out = wmf(&d);
|
||
let b = &out.bytes;
|
||
assert_eq!(sniff(b), Some(Kind::Wmf));
|
||
assert_eq!(u32_at(b, 0), 0x9AC6_CDD7);
|
||
// The box in logical units at 1440 an inch: 1 × 2 inches.
|
||
assert_eq!([u16_at(b, 6), u16_at(b, 8), u16_at(b, 10), u16_at(b, 12)], [0, 0, 1440, 2880]);
|
||
assert_eq!(u16_at(b, 14), 1440);
|
||
let xor = (0..10).fold(0u16, |a, i| a ^ u16_at(b, 2 * i));
|
||
assert_eq!(u16_at(b, 20), xor, "the checksum");
|
||
assert_eq!(crate::wmf::checksum(&b[..20]), xor);
|
||
// The WMF header: a memory metafile of 9 words, its size in words, the largest record.
|
||
assert_eq!([u16_at(b, 22), u16_at(b, 24), u16_at(b, 26)], [1, 9, 0x0300]);
|
||
assert_eq!(u32_at(b, 28) as usize * 2, b.len() - 22);
|
||
let recs = wmf_records(b);
|
||
let largest = recs.iter().map(|r| r.1.len() / 2).max().unwrap();
|
||
assert_eq!(u32_at(b, 34) as usize, largest, "MaxRecord");
|
||
assert_eq!(recs.last().map(|r| (r.0, r.1.len())), Some((0, 6)), "META_EOF ends the file");
|
||
// Objects: the null pen and brush, the brush, the pen.
|
||
assert_eq!(u16_at(b, 32), 4);
|
||
// The curve is flattened into a polygon, its outline a polygon drawn with the null brush.
|
||
let poly = recs.iter().find(|r| r.0 == 0x0538).unwrap().1;
|
||
assert_eq!(u16_at(poly, 6), 1);
|
||
assert!(u16_at(poly, 8) > 16, "many points");
|
||
assert!(recs.iter().any(|r| r.0 == 0x0324));
|
||
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
|
||
}
|
||
|
||
#[test]
|
||
fn wmf_sizes_fit_sixteen_bits() {
|
||
assert_eq!(crate::wmf::units_per_inch(612.0, 792.0), 1440);
|
||
let inch = crate::wmf::units_per_inch(3000.0, 1000.0);
|
||
assert!(inch < 1440 && f64::from(inch) * 3000.0 / 72.0 <= 32767.0);
|
||
let d = doc_with(3000.0, 1000.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 3000.0, 1000.0)), red(), Paint::None, 0.0)]);
|
||
let b = wmf(&d).bytes;
|
||
assert_eq!(u16_at(&b, 14), inch);
|
||
assert!(u16_at(&b, 10) <= 32767);
|
||
}
|
||
|
||
#[test]
|
||
fn wmf_reports_what_it_leaves_out() {
|
||
let g = Paint::Gradient(Box::new(GradientPaint::new(Gradient::default())));
|
||
let mut clip = path(shapes::rectangle(Rect::new(0.0, 0.0, 20.0, 20.0)), Paint::None, Paint::None, 0.0);
|
||
if let NodeKind::Path { clipping, .. } = &mut clip.kind {
|
||
*clipping = true;
|
||
}
|
||
let inside = path(shapes::rectangle(Rect::new(5.0, 5.0, 40.0, 40.0)), g, Paint::None, 0.0);
|
||
let group = Node::new(NodeId(0), NodeKind::Group { children: vec![Arc::new(clip), Arc::new(inside)], clip: true });
|
||
let mut d = doc_with(100.0, 100.0, vec![group]);
|
||
let img = image_node(&mut d, png(2, 2, [0, 0, 0, 10]), 2, 2, Affine::translate((50.0, 50.0)) * Affine::rotate(0.5));
|
||
add(&mut d, img);
|
||
let w = wmf(&d).warnings.join("\n");
|
||
for what in ["clipping masks", "gradients", "transparency", "rotate"] {
|
||
assert!(w.contains(what), "{what}: {w}");
|
||
}
|
||
// EMF keeps all of it.
|
||
let e = emf(&d).warnings;
|
||
assert!(e.is_empty(), "{e:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn hidden_and_offboard_art_is_left_out() {
|
||
let mut hidden = path(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), red(), Paint::None, 0.0);
|
||
hidden.visible = false;
|
||
let away = path(shapes::rectangle(Rect::new(500.0, 500.0, 510.0, 510.0)), red(), Paint::None, 0.0);
|
||
let d = doc_with(100.0, 100.0, vec![hidden, away]);
|
||
let k = kinds(&emf_records(&emf(&d).bytes));
|
||
assert!(!k.contains(&62) && !k.contains(&39));
|
||
assert!(export(&d, Rect::new(0.0, 0.0, 0.0, 10.0), Kind::Emf).is_err(), "an empty region");
|
||
}
|
||
|
||
#[test]
|
||
fn an_instance_whose_art_reaches_the_artboard_is_written() {
|
||
// The symbol's red rectangle, (100, 100) to (140, 140), drawn from (-50, -50): on the artboard,
|
||
// while the 20 × 20 square around the origin, moved to (-60, -60) to (-40, -40), is off it.
|
||
let inst = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Far".into(), xf: Affine::translate((-50.0, -50.0)) });
|
||
let mut d = doc_with(100.0, 100.0, vec![inst]);
|
||
let art = path(shapes::rectangle(Rect::new(100.0, 100.0, 140.0, 140.0)), red(), Paint::None, 0.0);
|
||
d.symbols.push(vectorcraft_doc::Symbol { name: "Far".into(), art: Arc::new(art) });
|
||
let k = kinds(&emf_records(&emf(&d).bytes));
|
||
assert!(k.contains(&39), "a brush fills the art: {k:?}");
|
||
}
|
||
|
||
/// A pattern `name` repeating `art` every point.
|
||
fn point_tiles(name: &str, art: Node) -> PatternDef {
|
||
let mut def = PatternDef::new(name, vec![Arc::new(art)]);
|
||
def.tile = Rect::new(0.0, 0.0, 1.0, 1.0);
|
||
def
|
||
}
|
||
|
||
fn pattern(name: &str) -> Paint {
|
||
Paint::Pattern { pattern: name.into(), xf: Affine::IDENTITY }
|
||
}
|
||
|
||
#[test]
|
||
fn patterns_inside_pattern_tiles_share_one_tile_budget() {
|
||
// About 3,700 tiles of a 12 pt square, each filled with about 140 tiles of a dot: over half a
|
||
// million fills when every tile is written.
|
||
let square = path(shapes::rectangle(Rect::new(0.0, 0.0, 12.0, 12.0)), pattern("dots"), Paint::None, 0.0);
|
||
let mut d = doc_with(50.0, 50.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 50.0)), pattern("squares"), Paint::None, 0.0)]);
|
||
d.patterns.push(point_tiles("dots", path(shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), red(), Paint::None, 0.0)));
|
||
d.patterns.push(point_tiles("squares", square));
|
||
let (ops, warnings) = crate::scene::Scene::new(&d, d.artboards[0].rect, true).run();
|
||
assert!(ops.len() < 300_000, "{} operations", ops.len());
|
||
assert!(warnings.iter().any(|w| w.contains("one colour")), "{warnings:?}");
|
||
// The tiles that were written are reported too (#3850).
|
||
assert!(warnings.iter().any(|w| w.contains("written as their tiles")), "{warnings:?}");
|
||
|
||
// A tile filled with its own pattern multiplies at every level down to the nesting limit.
|
||
let square = path(shapes::rectangle(Rect::new(0.0, 0.0, 4.0, 4.0)), pattern("self"), Paint::None, 0.0);
|
||
let mut d = doc_with(10.0, 10.0, vec![path(shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), pattern("self"), Paint::None, 0.0)]);
|
||
d.patterns.push(point_tiles("self", square));
|
||
let (ops, _) = crate::scene::Scene::new(&d, d.artboards[0].rect, true).run();
|
||
assert!(ops.len() < 300_000, "{} operations", ops.len());
|
||
}
|