mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-13 09:23:22 -06:00
* An item a parent overrode and deleted is gone from the pages that use it (#1469) In InDesign, removing an inherited item from a parent page means overriding it there and deleting the override: the parent's page keeps the item in its override list with no copy of its own. Only the page being drawn was checked, so the pages that use that parent still drew the base's item -- a signature block from parent B over the notice text of every page using parent E. The override lists of the whole parent chain are checked, not just the page's own. An override always names an item of a parent further back, so one set covers the chain. Rebased onto the post-split main: the parent-item loop moved from render's lib.rs to draw.rs and the tests to tests.rs. One thing this does not touch: blendspace.rs checks `page.overridden` the same way in two places, for deciding which spreads hold transparency. A deleted override there would count an absent item as present, which can only cost an unnecessary flatten rather than draw the wrong thing, so it is left for a maintainer to judge rather than widened into this fix. * Parent items: one set of overridden items for drawing, PDF and PPTX export, hit-testing, snapping and frame edges * Override All: an item a parent removed is not brought back as a copy
776 lines
39 KiB
Rust
776 lines
39 KiB
Rust
use super::*;
|
||
use designcraft_doc::build::NewDocument;
|
||
use designcraft_doc::{Fill, ParaFormat};
|
||
|
||
#[test]
|
||
fn eps_source_decodes_like_its_preview() {
|
||
let source = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 20 10\n%%EOF\n";
|
||
let mut tiff = Vec::new();
|
||
image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(3, 2, image::Rgba([255, 0, 0, 128])))
|
||
.write_to(&mut std::io::Cursor::new(&mut tiff), image::ImageFormat::Tiff)
|
||
.unwrap();
|
||
let mut dos = vec![0xC5, 0xD0, 0xD3, 0xC6];
|
||
for value in [30usize, source.len(), 0, 0, 30 + source.len(), tiff.len()] {
|
||
dos.extend((value as u32).to_le_bytes());
|
||
}
|
||
dos.extend([0xFF, 0xFF]);
|
||
dos.extend_from_slice(source);
|
||
dos.extend_from_slice(&tiff);
|
||
for source in [source.as_slice(), dos.as_slice()] {
|
||
let proxy = designcraft_images::eps_proxy(source).unwrap().0;
|
||
let actual = decode_pixmap(source).unwrap();
|
||
let expected = decode_pixmap(&proxy).unwrap();
|
||
assert_eq!((actual.width(), actual.height()), (expected.width(), expected.height()));
|
||
assert_eq!(actual.data(), expected.data());
|
||
assert_eq!(image_size(source), image_size(&proxy));
|
||
assert_eq!(image_mime(source), "application/postscript");
|
||
}
|
||
let corrupt = b"%!PS-Adobe-3.0 EPSF-3.0\nno bounding box\n";
|
||
assert!(decode_pixmap(corrupt).is_none());
|
||
assert!(image_size(corrupt).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn footnote_vector_objects_are_painted() {
|
||
use designcraft_doc::{AnchorPosition, AnchoredObject, ItemId, Shape, SpreadRef};
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let (_, sid) =
|
||
d.add_text_frame(SpreadRef::Doc(0), Rect::new(20.0, 20.0, 300.0, 300.0), d.default_layer(), "Body", ParaFormat::default()).unwrap();
|
||
let mut item =
|
||
Item::new(ItemId(d.alloc()), d.default_layer(), Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 12.0, 12.0)));
|
||
item.fill = Fill::swatch(designcraft_color::swatch::BLACK);
|
||
// Open contours with a fill are implicitly closed for painting. A single
|
||
// open contour must not hide the whole compound glyph.
|
||
item.path.subpaths[0].closed = false;
|
||
let story = d.story_mut(sid).unwrap();
|
||
story.insert_note(4, "", ParaFormat::default());
|
||
Arc::make_mut(&mut story.notes[0]).text.insert_object(0, AnchoredObject::new(item, AnchorPosition::Inline { y_offset: 0.0 }));
|
||
let cache = Cache::new();
|
||
let cs = cache.get(&d, sid, None);
|
||
let note = &cs.frames[0].notes[0];
|
||
let object = ¬e.text.frames[0].objects[0];
|
||
let centre = note.origin + object.origin.to_vec2() + designcraft_geom::Vec2::new(6.0, 6.0);
|
||
let image = Renderer::new().render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
let pixel = image.pixel(centre.x as u32, centre.y as u32);
|
||
assert!(pixel[0] < 80 && pixel[1] < 80 && pixel[2] < 80, "{pixel:?}");
|
||
}
|
||
|
||
/// `Rendered` has public fields: a buffer that doesn't match the stated size used to panic
|
||
/// in `to_png`.
|
||
/// A canvas 65535 pixels wide (any render wider than that was clamped to it) panicked inside
|
||
/// vello_cpu when it snapped the width up to its tiles; a huge page export now declines.
|
||
#[test]
|
||
fn very_wide_renders_do_not_panic() {
|
||
let doc = Document::new(&NewDocument::default());
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
let img = r.render(&doc, &cache, &[], (70_000, 4), Affine::IDENTITY, &RenderOptions::default());
|
||
assert_eq!(img.width, MAX_SIDE);
|
||
let page = [Placed { spread: SpreadRef::Doc(0), xf: Affine::IDENTITY }];
|
||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||
let img = r.render(&doc, &cache, &page, (70_000, 64), Affine::scale(120.0), &opts);
|
||
assert_eq!(img.width, MAX_SIDE);
|
||
assert!(r.render_page(&doc, &cache, 0, 500.0, true, &RenderOptions::default()).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn to_png_survives_a_mismatched_buffer() {
|
||
let short = Rendered { width: 4, height: 4, pixels: vec![255; 10] };
|
||
let png = short.to_png();
|
||
assert_eq!(image::load_from_memory(&png).map(|i| (i.width(), i.height())).ok(), Some((4, 4)));
|
||
assert!(Rendered { width: 0, height: 0, pixels: vec![] }.to_png().len() < 1024);
|
||
}
|
||
|
||
#[test]
|
||
fn proof_view_simulates_the_target() {
|
||
use designcraft_color::cms::{ProofSetup, ProofTarget};
|
||
let red = |a: u8| Rendered { width: 1, height: 1, pixels: vec![a, 0, 0, a] };
|
||
let mut img = red(255);
|
||
proof_view(&mut img, &ProofSetup { target: ProofTarget::Protanopia, ..Default::default() });
|
||
assert!(img.pixels[1] > 40, "protanopia turns pure red olive: {:?}", img.pixels);
|
||
let mut img = red(255);
|
||
proof_view(&mut img, &ProofSetup { target: ProofTarget::MonitorRgb, ..Default::default() });
|
||
assert!(img.pixels[0] > 245 && img.pixels[1] < 10, "monitor RGB is no simulation: {:?}", img.pixels);
|
||
// Transparent pixels stay transparent; premultiplication is kept.
|
||
let mut img = Rendered { width: 2, height: 1, pixels: vec![0, 0, 0, 0, 100, 0, 0, 128] };
|
||
proof_view(&mut img, &ProofSetup { target: ProofTarget::WorkingCmyk, ..Default::default() });
|
||
assert_eq!(&img.pixels[..4], &[0, 0, 0, 0]);
|
||
assert!(img.pixels[4] <= 128 && img.pixels[7] == 128);
|
||
}
|
||
|
||
#[test]
|
||
fn renders_page_with_text_and_fill() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let (fid, _) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 576.0, 300.0), lid, "Hello DesignCraft", ParaFormat::default()).unwrap();
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut box_ = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(36.0, 400.0, 136.0, 500.0)));
|
||
box_.fill = Fill::swatch("C=100 M=0 Y=0 K=0");
|
||
d.insert_item(SpreadRef::Doc(0), box_, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let img = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
assert_eq!((img.width, img.height), (612, 792));
|
||
// Paper is white, the box is cyan-ish, text drew some dark pixels.
|
||
assert_eq!(img.pixel(300, 700), [255, 255, 255, 255]);
|
||
let c = img.pixel(80, 450);
|
||
assert!(c[0] < 80 && c[2] > 150, "{c:?}");
|
||
let dark = (36..300).flat_map(|x| (36..60).map(move |y| (x, y))).filter(|&(x, y)| img.pixel(x, y)[0] < 100).count();
|
||
assert!(dark > 50, "text pixels: {dark}");
|
||
let _ = fid;
|
||
assert!(r.stats.glyphs >= 15);
|
||
}
|
||
|
||
/// An inside-aligned stroke is the stroke of the path inset by half its weight: a band from the
|
||
/// edge inwards, its rounded corners concentric with the frame's.
|
||
#[test]
|
||
fn inside_strokes_are_inset_by_half_the_weight() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 300.0, 300.0)));
|
||
it.stroke = designcraft_doc::Stroke {
|
||
swatch: designcraft_color::swatch::BLACK.into(),
|
||
weight: 10.0,
|
||
align: designcraft_doc::StrokeAlign::Inside,
|
||
..Default::default()
|
||
};
|
||
it.corners = designcraft_geom::corners::CornerOptions::uniform(designcraft_geom::corners::CornerShape::Rounded, 20.0);
|
||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let img = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
let ink = |x: u32, y: u32| img.pixel(x, y)[0] < 64;
|
||
let paper = |x: u32, y: u32| img.pixel(x, y)[0] > 192;
|
||
// Straight edges: ink from the edge to 10 pt inside, nothing outside.
|
||
assert!(paper(97, 200) && ink(101, 200) && ink(108, 200) && paper(112, 200));
|
||
assert!(ink(298, 200) && paper(302, 200) && paper(288, 200));
|
||
// Rounded corner centred at (120, 120): ink between radii 10 and 20, paper outside the arc
|
||
// and inside the band.
|
||
assert!(paper(103, 103) && ink(108, 108) && paper(114, 114));
|
||
}
|
||
|
||
#[test]
|
||
fn arrowheads_draw_at_line_ends() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let path = designcraft_geom::shapes::line(designcraft_geom::Point::new(100.0, 400.0), designcraft_geom::Point::new(400.0, 400.0));
|
||
let mut line = Item::new(id, lid, designcraft_doc::Shape::GraphicLine, path);
|
||
line.stroke = designcraft_doc::Stroke {
|
||
swatch: designcraft_color::swatch::BLACK.into(),
|
||
weight: 2.0,
|
||
end: designcraft_doc::Arrowhead::TriangleWide,
|
||
..Default::default()
|
||
};
|
||
d.insert_item(SpreadRef::Doc(0), line, None).unwrap();
|
||
// A dashed line with a gap colour: the gaps are painted.
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let path = designcraft_geom::shapes::line(designcraft_geom::Point::new(100.0, 500.0), designcraft_geom::Point::new(400.0, 500.0));
|
||
let mut dashed = Item::new(id, lid, designcraft_doc::Shape::GraphicLine, path);
|
||
dashed.stroke = designcraft_doc::Stroke {
|
||
swatch: designcraft_color::swatch::BLACK.into(),
|
||
weight: 4.0,
|
||
kind: designcraft_doc::StrokeType::Dashed { pattern: vec![12.0, 4.0] },
|
||
gap_swatch: "C=100 M=0 Y=0 K=0".into(),
|
||
..Default::default()
|
||
};
|
||
d.insert_item(SpreadRef::Doc(0), dashed, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let img = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
// The head is 6.4 × weight wide: ink 4 pt above the line near the tip, none at the start.
|
||
assert!(img.pixel(393, 396)[0] < 128, "{:?}", img.pixel(393, 396));
|
||
assert_eq!(img.pixel(105, 396), [255, 255, 255, 255]);
|
||
// Nothing past the tip.
|
||
assert_eq!(img.pixel(403, 400), [255, 255, 255, 255]);
|
||
let (dash, gap) = (img.pixel(106, 500), img.pixel(114, 500));
|
||
assert!(dash[0] < 80 && dash[2] < 80, "dash {dash:?}");
|
||
assert!(gap[0] < 80 && gap[2] > 150, "gap {gap:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn overprint_preview_mixes_inks() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
for (sw, op) in [("C=0 M=0 Y=100 K=0", false), ("C=0 M=100 Y=0 K=0", true)] {
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it =
|
||
Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)));
|
||
it.fill = Fill { overprint: op, ..Fill::swatch(sw) };
|
||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||
}
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let px = |on: bool, r: &mut Renderer| {
|
||
r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions { overprint_preview: on, ..Default::default() }).unwrap().pixel(150, 150)
|
||
};
|
||
let off = px(false, &mut r);
|
||
let on = px(true, &mut r);
|
||
assert!(off[2] > 100, "magenta knocks out: {off:?}");
|
||
assert!(on[2] < 60 && on[0] > 180, "magenta over yellow → red: {on:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn cmyk_blend_space_shows_transparent_spreads_in_cmyk() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
d.swatches.push(designcraft_color::swatch::Swatch::color("Green", designcraft_color::Color::rgb8(0, 255, 0)));
|
||
let lid = d.default_layer();
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)));
|
||
it.fill = Fill::swatch("Green");
|
||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let px = |d: &Document, r: &mut Renderer| {
|
||
r.render_page(d, &cache, 0, 1.0, false, &RenderOptions { blend_space_view: true, ..Default::default() }).unwrap().pixel(150, 150)
|
||
};
|
||
// No transparency on the spread: shown as is.
|
||
assert_eq!(px(&d, &mut r)[1], 255);
|
||
// With transparency in a CMYK blend space the bright green leaves the RGB gamut's edge.
|
||
d.item_mut(id).unwrap().opacity = 0.99;
|
||
let cmyk = px(&d, &mut r);
|
||
assert!(cmyk[0] > 20 || cmyk[1] < 240, "{cmyk:?}");
|
||
d.settings.blend_space = designcraft_doc::BlendSpace::Rgb;
|
||
let rgb = px(&d, &mut r);
|
||
assert!(rgb[1] > 245 && rgb[0] < 10, "{rgb:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn blend_modes_blend_in_cmyk_on_screen_in_a_cmyk_blend_space() {
|
||
// A lilac (RGB) rectangle, and a 100% K one over its lower half and the paper below it,
|
||
// in Color Dodge: in CMYK the black clears the lilac's inks (white), over the paper it
|
||
// shows as it is; in RGB, dodging with black changes nothing.
|
||
let mut d = Document::new(&NewDocument::default());
|
||
d.swatches.push(designcraft_color::swatch::Swatch::color("Lilac", designcraft_color::Color::rgb8(171, 154, 227)));
|
||
let lid = d.default_layer();
|
||
let rect = |d: &mut Document, r: Rect, fill: &str| {
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(r));
|
||
it.fill = Fill::swatch(fill);
|
||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||
id
|
||
};
|
||
rect(&mut d, Rect::new(100.0, 100.0, 300.0, 200.0), "Lilac");
|
||
let black = rect(&mut d, Rect::new(150.0, 150.0, 250.0, 250.0), designcraft_color::swatch::BLACK);
|
||
d.item_mut(black).unwrap().blend = DcBlend::ColorDodge;
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let px = |d: &Document, r: &mut Renderer, x: u32, y: u32| {
|
||
r.render_page(d, &cache, 0, 1.0, false, &RenderOptions { blend_space_view: true, ..Default::default() }).unwrap().pixel(x, y)
|
||
};
|
||
let over_lilac = px(&d, &mut r, 200, 175);
|
||
assert!(over_lilac[..3].iter().all(|v| *v > 235), "dodged to white: {over_lilac:?}");
|
||
let over_paper = px(&d, &mut r, 200, 225);
|
||
assert!(over_paper[..3].iter().all(|v| *v < 90), "black as it is: {over_paper:?}");
|
||
let lilac = px(&d, &mut r, 120, 150);
|
||
assert!(lilac[2] > lilac[1] + 25, "the lilac beside it: {lilac:?}");
|
||
// Inside a group, the group's blend mode.
|
||
let g = designcraft_doc::ItemId(d.alloc());
|
||
let it = d.remove_item(black).unwrap();
|
||
let mut grp = Item::new(g, lid, designcraft_doc::Shape::Group, Default::default());
|
||
let mut inner = (*it).clone();
|
||
inner.blend = DcBlend::Normal;
|
||
grp.content = Content::Group { items: vec![Arc::new(inner)] };
|
||
grp.blend = DcBlend::ColorDodge;
|
||
d.insert_item(SpreadRef::Doc(0), grp, None).unwrap();
|
||
let grouped = px(&d, &mut r, 200, 175);
|
||
assert!(grouped[..3].iter().all(|v| *v > 235), "{grouped:?}");
|
||
// An RGB blend space: still lilac under the black (lightened a little: [Black] shows as a
|
||
// dark grey in RGB).
|
||
d.settings.blend_space = designcraft_doc::BlendSpace::Rgb;
|
||
let rgb = px(&d, &mut r, 200, 175);
|
||
assert!(rgb[2] > rgb[1] + 25 && rgb[1] < 220, "{rgb:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn knockout_group_hides_its_own_objects() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let mut kids = Vec::new();
|
||
for (sw, x) in [("C=0 M=0 Y=100 K=0", 100.0), ("C=0 M=100 Y=0 K=0", 150.0)] {
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it =
|
||
Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(x, 100.0, x + 100.0, 200.0)));
|
||
it.fill = Fill::swatch(sw);
|
||
it.opacity = 0.5;
|
||
kids.push(std::sync::Arc::new(it));
|
||
}
|
||
let gid = designcraft_doc::ItemId(d.alloc());
|
||
let mut g = Item::new(gid, lid, designcraft_doc::Shape::Group, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 250.0, 200.0)));
|
||
g.content = designcraft_doc::Content::Group { items: kids };
|
||
d.insert_item(SpreadRef::Doc(0), g, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let overlap = |d: &Document, r: &mut Renderer| r.render_page(d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap().pixel(175, 150);
|
||
let plain = overlap(&d, &mut r);
|
||
d.item_mut(gid).unwrap().knockout = true;
|
||
let ko = overlap(&d, &mut r);
|
||
let magenta_only = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap().pixel(225, 150);
|
||
assert_ne!(plain, ko, "{plain:?} {ko:?}");
|
||
for c in 0..3 {
|
||
assert!((ko[c] as i32 - magenta_only[c] as i32).abs() <= 3, "the overlap shows magenta alone: {ko:?} vs {magenta_only:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn black_overprints_only_with_the_preference() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
for (sw, r) in
|
||
[("C=100 M=0 Y=0 K=0", Rect::new(100.0, 100.0, 200.0, 200.0)), (designcraft_color::swatch::BLACK, Rect::new(150.0, 100.0, 250.0, 200.0))]
|
||
{
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(r));
|
||
it.fill = Fill::swatch(sw);
|
||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||
}
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let o = RenderOptions { overprint_preview: true, ..Default::default() };
|
||
let on = r.render_page(&d, &cache, 0, 1.0, false, &o).unwrap().pixel(175, 150);
|
||
let alone = r.render_page(&d, &cache, 0, 1.0, false, &o).unwrap().pixel(225, 150);
|
||
d.settings.overprint_black = false;
|
||
let off = r.render_page(&d, &cache, 0, 1.0, false, &o).unwrap().pixel(175, 150);
|
||
assert_eq!(off, alone, "knocks out: black alone");
|
||
assert_ne!(on, alone, "overprints: the cyan shows through");
|
||
}
|
||
|
||
#[test]
|
||
fn appearance_of_black_on_screen() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)));
|
||
it.fill = Fill::swatch(designcraft_color::swatch::BLACK);
|
||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let px = |rich: bool, r: &mut Renderer| {
|
||
r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions { rich_black: rich, ..Default::default() }).unwrap().pixel(150, 150)
|
||
};
|
||
assert!(px(false, &mut r)[0] > 20, "accurate: a dark grey");
|
||
assert_eq!(px(true, &mut r)[..3], [0, 0, 0]);
|
||
}
|
||
|
||
#[test]
|
||
fn display_performance_fast_draws_grey_boxes() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
// A 4×4 red PNG placed at 100,100 (100×100 pt).
|
||
let mut png = Vec::new();
|
||
image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 0, 0, 255])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||
let aid = designcraft_doc::AssetId(d.alloc());
|
||
d.assets.insert(
|
||
aid,
|
||
Arc::new(designcraft_doc::Asset {
|
||
id: aid,
|
||
name: "red.png".into(),
|
||
mime: "image/png".into(),
|
||
data: Arc::new(png),
|
||
pixels: Some((4, 4)),
|
||
..Default::default()
|
||
}),
|
||
);
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)));
|
||
it.content = designcraft_doc::Content::Graphic(designcraft_doc::Graphic {
|
||
layer_options: None,
|
||
asset: aid,
|
||
size: (100.0, 100.0),
|
||
xf: Affine::translate((100.0, 100.0)),
|
||
clip: None,
|
||
auto_fit: Default::default(),
|
||
fit_align: 4,
|
||
crop: [0.0; 4],
|
||
wrap: Default::default(),
|
||
fill: Default::default(),
|
||
});
|
||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let px = |q: DisplayQuality, r: &mut Renderer| {
|
||
r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions { quality: q, ..Default::default() }).unwrap().pixel(150, 150)
|
||
};
|
||
assert_eq!(px(DisplayQuality::High, &mut r)[..3], [255, 0, 0]);
|
||
assert_eq!(px(DisplayQuality::Typical, &mut r)[..3], [255, 0, 0]);
|
||
assert_eq!(px(DisplayQuality::Fast, &mut r)[..3], [178, 178, 178]);
|
||
}
|
||
|
||
#[test]
|
||
fn clipping_path_hides_the_image_outside_it() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let mut png = Vec::new();
|
||
image::RgbaImage::from_pixel(4, 4, image::Rgba([255, 0, 0, 255])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||
let aid = designcraft_doc::AssetId(d.alloc());
|
||
d.assets.insert(
|
||
aid,
|
||
Arc::new(designcraft_doc::Asset {
|
||
page: 0,
|
||
id: aid,
|
||
name: "red.png".into(),
|
||
mime: "image/png".into(),
|
||
link: None,
|
||
data: Arc::new(png),
|
||
pixels: Some((4, 4)),
|
||
..Default::default()
|
||
}),
|
||
);
|
||
// A 100 pt image at 100,100 clipped by a triangle with its apex at the top middle.
|
||
let triangle = designcraft_geom::PathData::single(designcraft_geom::path::SubPath::polyline(
|
||
&[designcraft_geom::Point::new(0.0, 100.0), designcraft_geom::Point::new(50.0, 0.0), designcraft_geom::Point::new(100.0, 100.0)],
|
||
true,
|
||
));
|
||
let mut clip = designcraft_doc::ClippingPath::new(designcraft_doc::ClippingKind::PhotoshopPath, triangle);
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)));
|
||
it.content = designcraft_doc::Content::Graphic(designcraft_doc::Graphic {
|
||
layer_options: None,
|
||
asset: aid,
|
||
size: (100.0, 100.0),
|
||
xf: Affine::translate((100.0, 100.0)),
|
||
auto_fit: Default::default(),
|
||
fit_align: 4,
|
||
crop: [0.0; 4],
|
||
wrap: Default::default(),
|
||
clip: Some(Arc::new(clip.clone())),
|
||
fill: Default::default(),
|
||
});
|
||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let page = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
assert_eq!(page.pixel(150, 170)[..3], [255, 0, 0], "inside the triangle");
|
||
assert_eq!(page.pixel(110, 110)[..3], [255, 255, 255], "inside the frame, outside the triangle: paper");
|
||
// Inverted: the corners show and the triangle is paper.
|
||
clip.invert = true;
|
||
if let Some(designcraft_doc::Content::Graphic(g)) = d.item_mut(id).map(|it| &mut it.content) {
|
||
g.clip = Some(Arc::new(clip));
|
||
}
|
||
let cache = Cache::new();
|
||
let page = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
assert_eq!(page.pixel(150, 170)[..3], [255, 255, 255]);
|
||
assert_eq!(page.pixel(110, 110)[..3], [255, 0, 0]);
|
||
}
|
||
|
||
#[test]
|
||
fn missing_fonts_are_highlighted_on_screen_only() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 100.0), lid, "Missing font", ParaFormat::default()).unwrap();
|
||
d.story_mut(sid).unwrap().format_chars(0..12, |f| f.over.font_family = Some("No Such Family".into()));
|
||
let cache = Cache::new();
|
||
let cs = cache.get(&d, sid, None);
|
||
assert!(cs.styles.iter().any(|s| s.missing_font));
|
||
let l = &cs.frames[0].lines[0];
|
||
// A point inside the line box but between glyph strokes: the space after "Missing".
|
||
let g = l.glyphs.iter().find(|g| g.byte == 7).unwrap();
|
||
let (x, y) = ((g.x + g.adv / 2.0) as u32, (l.baseline - l.ascent * 0.5) as u32);
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let plain = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
let shown = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions { highlight_missing_fonts: true, ..Default::default() }).unwrap();
|
||
assert_eq!(plain.pixel(x, y)[1], 255, "no highlight in output");
|
||
let p = shown.pixel(x, y);
|
||
assert!(p[0] > 240 && p[1] < 200, "pink on screen: {p:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn table_edges_paint_gap_colour_between_dashes_and_stripes() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 400.0), lid, "", ParaFormat::default()).unwrap();
|
||
let mut t = designcraft_doc::Table::new(1, 1, 1, 0, 0, 200.0);
|
||
t.rows[0].height = 60.0;
|
||
t.rows[0].mode = designcraft_doc::RowHeightMode::Exactly;
|
||
let gap = |kind| designcraft_doc::CellStroke { weight: 8.0, kind, gap_color: "C=100 M=0 Y=0 K=0".into(), ..Default::default() };
|
||
let cell = t.cell_mut(0, 0).unwrap();
|
||
cell.strokes[0] = gap(designcraft_doc::StrokeType::Dashed { pattern: vec![12.0, 6.0] });
|
||
cell.strokes[2] = gap(designcraft_doc::StrokeType::ThickThin);
|
||
cell.border_overrides = [true; 4];
|
||
cell.stroke_defined = [true; 4];
|
||
d.story_mut(sid).unwrap().insert_table(0, t);
|
||
let cache = Cache::new();
|
||
let rect = cache.get(&d, sid, None).frames[0].tables[0].cell(0, 0).unwrap().rect;
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let img = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
let ink = |p: [u8; 4]| p[0] < 80 && p[2] < 80;
|
||
let cyan = |p: [u8; 4]| p[0] < 80 && p[2] > 150;
|
||
let y = rect.y0.round() as u32;
|
||
let (dash, space) = (img.pixel((rect.x0 + 6.0) as u32, y), img.pixel((rect.x0 + 15.0) as u32, y));
|
||
assert!(ink(dash), "dash {dash:?}");
|
||
assert!(cyan(space), "gap {space:?}");
|
||
let column: Vec<_> = (rect.y1 as u32 - 5..=rect.y1 as u32 + 5).map(|y| img.pixel(rect.center().x as u32, y)).collect();
|
||
assert!(column.iter().any(|p| ink(*p)) && column.iter().any(|p| cyan(*p)), "stripes and gap: {column:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn degenerate_dash_patterns_draw_solid_and_fill_types_close_table_corners() {
|
||
let fine = designcraft_doc::StrokeType::Dashed { pattern: vec![1e-6, 1e-6] };
|
||
// Expanding these would take ~10^8 dashes per edge.
|
||
assert!(text::cell_stroke(&fine, 4.0, 200.0).dash_pattern.is_empty());
|
||
assert!(text::cell_stroke(&designcraft_doc::StrokeType::Dotted, 1e-6, 200.0).dash_pattern.is_empty());
|
||
assert!(!text::cell_stroke(&designcraft_doc::StrokeType::Dashed { pattern: vec![3.0, 2.0] }, 1.0, 200.0).dash_pattern.is_empty());
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let (fid, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 400.0), lid, "", ParaFormat::default()).unwrap();
|
||
d.item_mut(fid).unwrap().stroke = designcraft_doc::Stroke { weight: 2.0, kind: fine.clone(), ..Default::default() };
|
||
let mut t = designcraft_doc::Table::new(1, 2, 2, 0, 0, 200.0);
|
||
for r in 0..2 {
|
||
t.rows[r].height = 60.0;
|
||
t.rows[r].mode = designcraft_doc::RowHeightMode::Exactly;
|
||
}
|
||
let stroke = |kind| designcraft_doc::CellStroke { weight: 8.0, kind, gap_color: "C=100 M=0 Y=0 K=0".into(), ..Default::default() };
|
||
for cell in &mut t.cells {
|
||
cell.strokes = std::array::from_fn(|_| stroke(designcraft_doc::StrokeType::ThickThin));
|
||
cell.border_overrides = [true; 4];
|
||
cell.stroke_defined = [true; 4];
|
||
}
|
||
if let Some(cell) = t.cell_mut(1, 0) {
|
||
cell.strokes[2] = stroke(fine);
|
||
}
|
||
d.story_mut(sid).unwrap().insert_table(0, t);
|
||
let cache = Cache::new();
|
||
let frag = cache.get(&d, sid, None).frames[0].tables[0].clone();
|
||
let (top, bottom) = (frag.cell(0, 0).unwrap().rect, frag.cell(1, 0).unwrap().rect);
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let img = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
let ink = |p: [u8; 4]| p[0] < 80;
|
||
let solid: Vec<_> = (2..10).map(|i| img.pixel((bottom.x0 + 20.0 + i as f64 * 0.5) as u32, bottom.y1.round() as u32)).collect();
|
||
assert!(solid.iter().all(|p| ink(*p)), "the degenerate dashed edge draws solid: {solid:?}");
|
||
// Outside the table's top-left corner, inside both edges' reach.
|
||
let corner = img.pixel((top.x0 - 3.0) as u32, (top.y0 - 3.0) as u32);
|
||
assert!(ink(corner), "corner closed: {corner:?}");
|
||
let edge = frag.strokes.iter().find(|s| s.a == designcraft_geom::Point::new(top.x0, top.y0) && s.a.y == s.b.y).unwrap();
|
||
let (a, b) = frag.corner_capped(edge);
|
||
assert_eq!((a, b), (designcraft_geom::Point::new(top.x0 - 4.0, top.y0), edge.b), "only table corners extend");
|
||
}
|
||
|
||
#[test]
|
||
fn table_gradient_fill_is_continuous_across_cells() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let mut gradient = designcraft_color::Gradient::default();
|
||
gradient.stops[0].color = designcraft_color::Color::rgb(1.0, 0.0, 0.0);
|
||
gradient.stops[1].color = designcraft_color::Color::rgb(0.0, 0.0, 1.0);
|
||
d.swatches.push(designcraft_color::Swatch {
|
||
name: "Table gradient".into(),
|
||
value: designcraft_color::SwatchValue::Gradient { gradient },
|
||
locked: false,
|
||
named: true,
|
||
hidden: false,
|
||
});
|
||
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 300.0), d.default_layer(), "", ParaFormat::default()).unwrap();
|
||
let mut t = designcraft_doc::Table::new(1, 1, 2, 0, 0, 200.0);
|
||
t.rows[0].height = 40.0;
|
||
t.rows[0].mode = designcraft_doc::RowHeightMode::Exactly;
|
||
for cell in &mut t.cells {
|
||
cell.fill = "Table gradient".into();
|
||
}
|
||
d.story_mut(sid).unwrap().insert_table(0, t);
|
||
let cache = Cache::new();
|
||
let rect = cache.get(&d, sid, None).frames[0].tables[0].rect;
|
||
let mut renderer = Renderer::new();
|
||
renderer.threads = 0;
|
||
let img = renderer.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
let y = rect.center().y as u32;
|
||
let a = img.pixel((rect.x0 + 20.0) as u32, y);
|
||
let b = img.pixel((rect.x1 - 20.0) as u32, y);
|
||
assert!(i16::from(a[0]) - i16::from(a[2]) > 80, "red end: {a:?}");
|
||
assert!(i16::from(b[2]) - i16::from(b[0]) > 80, "blue end: {b:?}");
|
||
let left = img.pixel((rect.center().x - 2.0) as u32, y);
|
||
let right = img.pixel((rect.center().x + 2.0) as u32, y);
|
||
assert!((i16::from(left[0]) - i16::from(right[0])).abs() < 20, "no gradient restart between cells");
|
||
}
|
||
|
||
#[test]
|
||
fn renders_tables() {
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(36.0, 36.0, 336.0, 400.0), lid, "", ParaFormat::default()).unwrap();
|
||
let mut t = designcraft_doc::Table::new(5, 3, 3, 1, 0, 300.0);
|
||
for c in 0..3 {
|
||
let cell = t.cell_mut(0, c).unwrap();
|
||
cell.fill = "C=100 M=0 Y=0 K=0".into();
|
||
cell.text.insert(0, "Head");
|
||
t.cell_mut(2, c).unwrap().text.insert(0, "WWWWWWWW");
|
||
}
|
||
d.story_mut(sid).unwrap().insert_table(0, t);
|
||
let cache = Cache::new();
|
||
let cs = cache.get(&d, sid, None);
|
||
let tf = &cs.frames[0].tables[0];
|
||
let head = tf.cell(0, 1).unwrap().rect;
|
||
let body = tf.cell(2, 0).unwrap().rect;
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let img = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
// Header fill (cyan) near the right edge of the middle header cell.
|
||
let c = img.pixel((head.x1 - 3.0) as u32, (head.y0 + 3.0) as u32);
|
||
assert!(c[0] < 80 && c[2] > 150, "{c:?}");
|
||
// The table border (black) on the left edge.
|
||
let e = img.pixel(body.x0.round() as u32, body.center().y as u32);
|
||
assert!(e[0] < 200, "{e:?}");
|
||
// Cell text drew dark pixels inside the body cell.
|
||
let dark = ((body.x0 + 5.0) as u32..(body.x1 - 2.0) as u32)
|
||
.flat_map(|x| ((body.y0 + 2.0) as u32..(body.y1 - 2.0) as u32).map(move |y| (x, y)))
|
||
.filter(|&(x, y)| img.pixel(x, y)[0] < 100)
|
||
.count();
|
||
assert!(dark > 30, "cell text pixels: {dark}");
|
||
}
|
||
|
||
/// A one-page PDF (612 x 792 pt) that draws `content`.
|
||
fn one_page_pdf(content: &str) -> Vec<u8> {
|
||
let objects = [
|
||
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
|
||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>".to_string(),
|
||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
||
];
|
||
let mut out = b"%PDF-1.4\n".to_vec();
|
||
let mut offsets = Vec::new();
|
||
for (i, o) in objects.iter().enumerate() {
|
||
offsets.push(out.len());
|
||
out.extend(format!("{} 0 obj\n{o}\nendobj\n", i + 1).bytes());
|
||
}
|
||
let xref = out.len();
|
||
out.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).bytes());
|
||
for o in offsets {
|
||
out.extend(format!("{o:010} 00000 n \n").bytes());
|
||
}
|
||
out.extend(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).bytes());
|
||
out
|
||
}
|
||
|
||
/// A Bounding Box crop is the exact extent of what the page draws, strokes included, found
|
||
/// without rasterizing the page: PDF export of a document that places a complex illustration
|
||
/// cropped that way spent seconds rendering it just to measure it.
|
||
#[test]
|
||
fn content_crop_box_is_measured_without_rendering_the_page() {
|
||
// A filled rectangle 100..300 x 532..592 (PDF y up) and a 10 pt horizontal line at
|
||
// y 100 from x 400 to 500, butt capped. Clipped away: a rectangle off to the left.
|
||
let pdf = one_page_pdf("0 0 0 rg 100 532 200 60 re f 10 w 400 100 m 500 100 l S q 0 0 50 50 re W n 100 0 10 10 re f Q");
|
||
let before = PDF_RENDERS.with(std::cell::Cell::get);
|
||
let (frac, size) = pdf_crop_box(&pdf, 0, designcraft_doc::PdfCrop::ContentVisible).unwrap();
|
||
assert_eq!(PDF_RENDERS.with(std::cell::Cell::get), before, "the page was rasterized");
|
||
let want = [100.0 / 612.0, 200.0 / 792.0, 500.0 / 612.0, 697.0 / 792.0];
|
||
assert!(frac.iter().zip(want).all(|(a, b)| (a - b).abs() < 1e-6), "{frac:?} vs {want:?}");
|
||
assert!((size.0 - 400.0).abs() < 1e-3 && (size.1 - 497.0).abs() < 1e-3, "{size:?}");
|
||
}
|
||
|
||
/// In InDesign, removing an inherited item from a parent page means overriding it there and
|
||
/// deleting the override: the parent's page keeps the item in its override list with no copy
|
||
/// of its own. The pages that use that parent still drew the base's item — a signature block
|
||
/// from parent B over the notice text of every page using parent E (#1469).
|
||
#[test]
|
||
fn an_item_a_parent_overrode_and_deleted_is_gone_from_the_pages_that_use_it() {
|
||
let mut d = Document::new(&NewDocument { pages: 1, facing_pages: false, ..Default::default() });
|
||
let lid = d.default_layer();
|
||
// Parent A: a black box. Parent B: based on A, nothing of its own.
|
||
let a = d.parents[0].id;
|
||
let margins = d.parents[0].pages[0].margins;
|
||
let b = d.add_parent("B", "Parent", 1, 0.0, margins);
|
||
let box_id = designcraft_doc::ItemId(d.alloc());
|
||
let rect = Rect::new(100.0, 100.0, 200.0, 160.0);
|
||
let mut it = Item::new(box_id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(rect));
|
||
it.fill = designcraft_doc::Fill::swatch(designcraft_color::swatch::BLACK);
|
||
d.insert_item(SpreadRef::Parent(0), it, None).unwrap();
|
||
{
|
||
let bp = std::sync::Arc::make_mut(&mut d.parents[1]);
|
||
bp.parent.as_mut().unwrap().based_on = Some(a);
|
||
}
|
||
std::sync::Arc::make_mut(&mut d.spreads[0]).pages[0].parent = Some(b);
|
||
let ink = |d: &Document, r: &mut Renderer| {
|
||
let img = r.render_page(d, &Cache::new(), 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
img.pixel(rect.center().x as u32, rect.center().y as u32)[0] < 64
|
||
};
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
assert!(ink(&d, &mut r), "the page shows parent A's box through parent B");
|
||
|
||
// B overrides the box and the override is deleted: B's page keeps it in its override list.
|
||
std::sync::Arc::make_mut(&mut d.parents[1]).pages[0].overridden.push(box_id);
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
assert!(!ink(&d, &mut r), "the box is removed on B, so the page using B does not show it");
|
||
|
||
// Removing the override on B brings it back, which is how InDesign's Remove All Local
|
||
// Overrides behaves.
|
||
std::sync::Arc::make_mut(&mut d.parents[1]).pages[0].overridden.clear();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
assert!(ink(&d, &mut r), "the box shows again");
|
||
}
|
||
|
||
/// An inline object is painted under its frame's text, as InDesign draws it: a picture
|
||
/// whose frame reaches down into the next line (a negative Y offset) doesn't hide that line.
|
||
#[test]
|
||
fn inline_objects_are_painted_under_the_text() {
|
||
use designcraft_doc::{AnchorPosition, AnchoredObject, ItemId, Shape, SpreadRef};
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let (fid, sid) =
|
||
d.add_text_frame(SpreadRef::Doc(0), Rect::new(20.0, 20.0, 300.0, 300.0), d.default_layer(), "A\nWWWW", ParaFormat::default()).unwrap();
|
||
let mut item =
|
||
Item::new(ItemId(d.alloc()), d.default_layer(), Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 250.0, 60.0)));
|
||
item.fill = Fill::swatch(designcraft_color::swatch::PAPER);
|
||
d.story_mut(sid).unwrap().insert_object(1, AnchoredObject::new(item, AnchorPosition::Inline { y_offset: -40.0 }));
|
||
let cache = Cache::new();
|
||
let cs = cache.get(&d, sid, None);
|
||
let (ft, obj) = (&cs.frames[0], &cs.frames[0].objects[0]);
|
||
let l = ft.lines.iter().find(|l| l.range.start > 1).unwrap();
|
||
// The second line lies inside the white box.
|
||
assert!(obj.origin.y + obj.size.1 > l.baseline && obj.origin.y < l.baseline - l.ascent, "{:?} {}", obj.origin, l.baseline);
|
||
let m = d.text_xf(d.item(fid).unwrap());
|
||
let image = Renderer::new().render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
let (a, b) = (m * designcraft_geom::Point::new(l.x0, l.baseline - l.ascent), m * designcraft_geom::Point::new(l.end_x, l.baseline));
|
||
let dark =
|
||
(a.y as u32..b.y as u32).flat_map(|y| (a.x as u32..b.x as u32).map(move |x| (x, y))).filter(|&(x, y)| image.pixel(x, y)[0] < 100).count();
|
||
assert!(dark > 20, "the line's glyphs show over the box: {dark} dark pixels");
|
||
}
|
||
|
||
#[test]
|
||
fn dotted_strokes_draw_every_dot() {
|
||
// A 4 pt dotted line 96 pt long: dots every 8 pt, 13 of them.
|
||
let mut d = Document::new(&NewDocument::default());
|
||
let lid = d.default_layer();
|
||
let id = designcraft_doc::ItemId(d.alloc());
|
||
let path = designcraft_geom::shapes::line(designcraft_geom::Point::new(100.0, 400.0), designcraft_geom::Point::new(196.0, 400.0));
|
||
let mut line = Item::new(id, lid, designcraft_doc::Shape::GraphicLine, path);
|
||
line.stroke = designcraft_doc::Stroke {
|
||
swatch: designcraft_color::swatch::BLACK.into(),
|
||
weight: 4.0,
|
||
kind: designcraft_doc::StrokeType::Dotted,
|
||
..Default::default()
|
||
};
|
||
d.insert_item(SpreadRef::Doc(0), line, None).unwrap();
|
||
let cache = Cache::new();
|
||
let mut r = Renderer::new();
|
||
r.threads = 0;
|
||
let img = r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions::default()).unwrap();
|
||
for i in 0..13 {
|
||
let x = 100 + i * 8;
|
||
assert!(img.pixel(x, 400)[0] < 128, "dot {i} at x={x}: {:?}", img.pixel(x, 400));
|
||
if i < 12 {
|
||
assert_eq!(img.pixel(x + 4, 400), [255, 255, 255, 255], "gap after dot {i}");
|
||
}
|
||
}
|
||
// Table and paragraph rules: one round dot per period.
|
||
let stroke = text::cell_stroke(&designcraft_doc::StrokeType::Dotted, 4.0, 96.0);
|
||
let edge = kurbo::Line::new((0.0, 0.0), (96.0, 0.0));
|
||
let outline = kurbo::stroke(kurbo::Shape::path_elements(&edge, 0.01), &stroke, &kurbo::StrokeOpts::default(), 0.01);
|
||
let dots = outline.elements().iter().filter(|e| matches!(e, kurbo::PathEl::MoveTo(_))).count();
|
||
assert_eq!(dots, 13);
|
||
}
|