Files
vectorcraft/crates/render/src/tests_placed_document.rs
T
dotCooCoo 3a1d407f1a Place: a placed PDF or .ai can stay linked to its file (preference, off by default)
File › Place with Link kept a link only to raster images and VectorCraft
documents. A PDF, .ai or .ait page was always embedded as a group, so
Update Links never saw a newer version of the file.

A new preference, File Handling › Link Placed PDF and .ai Files
(linkPlacedPdf), is off by default. With it on, Link places the page as
a placed document, as it places a VectorCraft document: the object
keeps the file's bytes, a preview, the page number and the page's box
(crop, art, trim, bleed or media, stored as page_box). Links check,
update and relink it, Break Link and Object › Expand give the group
placing it without Link gives, every output draws the page's vectors,
and saving and reopening keep the link. With the preference off, or
with a password or textAs: "outlines", the page is embedded as before.
EPS and PostScript .ai files are still embedded.

Package copies a placed PDF or .ai file as it is. The Place dialog's
Link tooltip describes a PDF or .ai file as a document while the
preference is on. Relinking a placed document to a file that is not a
VectorCraft document, a PDF or a PDF-compatible .ai file says so.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 21:52:23 +07:00

185 lines
8.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Placed documents on screen: drawn from bitmaps made in the background (a placeholder until
//! then), the same as drawn exactly; exports draw them exactly at once.
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, ImageBlob, LinkInfo, Node, NodeId, NodeKind, PlacedDocument};
use vectorcraft_geom::shapes;
use super::*;
/// Every file reads as a 40×20 artboard filled blue; a file whose bytes start with `loop` also
/// places itself (the same key) in its middle.
fn blue_page(p: &PlacedDocument, bytes: &[u8]) -> Option<(Document, Node, Rect)> {
let mut d = Document::new(40.0, 20.0);
d.layers.clear();
let blue = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0);
let mut children = vec![Arc::new(Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 20.0)), blue))];
if bytes.starts_with(b"loop") {
d.images.insert(p.key.clone(), ImageBlob::new("application/json", bytes.to_vec()));
let me = PlacedDocument { xf: Affine::translate((10.0, 5.0)) * Affine::scale(0.5), ..p.clone() };
children.push(Arc::new(Node::new(NodeId(2), NodeKind::PlacedDocument(Box::new(me)))));
}
Some((d, Node::new(NodeId(0), NodeKind::Group { children, clip: false }), Rect::new(0.0, 0.0, 40.0, 20.0)))
}
/// A 100×100 document showing one placed file (`bytes` under `key`) in the box (10, 10)–(90, 50).
fn doc_with(key: &str, bytes: &[u8]) -> Document {
vectorcraft_doc::placed_document::set_loader(blue_page);
let mut d = Document::new(100.0, 100.0);
d.images.insert(key.into(), ImageBlob::new("application/json", bytes.to_vec()));
let p = PlacedDocument {
link: LinkInfo::new("/art/card.vectorcraft"),
key: key.into(),
bounding: false,
page_box: Default::default(),
width: 40.0,
height: 20.0,
xf: Affine::translate((10.0, 10.0)) * Affine::scale(2.0),
placement: Default::default(),
};
let l = d.layers[0].id;
d.insert(Some(l), 0, Node::new(NodeId(50), NodeKind::PlacedDocument(Box::new(p)))).unwrap();
d
}
fn doc(key: &str) -> Document {
doc_with(key, format!("{{\"doc\": \"{key}\"}}").as_bytes())
}
fn render(d: &Document, progressive: bool) -> Rendered {
let opts = RenderOptions { background: Some([255, 255, 255, 255]), progressive_placed: progressive, ..Default::default() };
Renderer::new().render(d, 100, 100, Affine::IDENTITY, &opts)
}
/// Wait for the background bitmaps (at most 10 s).
fn settle() {
let t = std::time::Instant::now();
while placed_document::busy() && t.elapsed().as_secs() < 10 {
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(!placed_document::busy(), "bitmaps still coming");
}
#[test]
fn a_placed_document_draws_exactly_with_its_own_art() {
let d = doc("exact-1");
let r = render(&d, false);
assert_eq!(&r.pixel(50, 30)[..3], &[0, 0, 255], "blue inside the box");
assert_eq!(&r.pixel(95, 95)[..3], &[255, 255, 255], "nothing outside");
}
#[test]
fn an_enveloped_placed_document_shows_its_file_once_read() {
let mut d = doc("enveloped-1");
let placed = d.remove(NodeId(50)).unwrap();
let NodeKind::PlacedDocument(p) = &placed.kind else { panic!("a placed document") };
let p = (**p).clone();
let kind = vectorcraft_doc::EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
let envelope = NodeKind::Envelope {
content: vec![placed],
kind: Box::new(kind),
fidelity: 50.0,
editing: false,
options: Default::default(),
frame: Box::new(Affine::IDENTITY),
};
let l = d.layers[0].id;
d.insert(Some(l), 0, Node::new(NodeId(60), envelope)).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
// Exporting the document as PDF, SVG or EPS reads the file.
d.with_placed_art();
assert!(vectorcraft_doc::placed_document::read_cached(&p).is_some(), "the file is read");
let after = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
assert!(after != before, "the file's art shows");
assert!(after == Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
#[test]
fn a_placed_document_with_a_drop_shadow_shows_its_file_once_read() {
let mut d = doc("shadowed-1");
let n = d.node_mut(NodeId(50)).unwrap();
let params = serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100});
n.appearance.effects.push(vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params, visible: true });
let NodeKind::PlacedDocument(p) = &n.kind else { panic!("a placed document") };
let p = (**p).clone();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
// Exporting the document as PDF, SVG or EPS reads the file.
d.with_placed_art();
assert!(vectorcraft_doc::placed_document::read_cached(&p).is_some(), "the file is read");
let after = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
assert!(after != before, "the file's art shows");
assert!(after == Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
#[test]
fn on_screen_a_placed_document_shows_a_placeholder_then_its_bitmap() {
let d = doc("progressive-1");
let generation = placed_document::generation();
let first = render(&d, true);
let shown = &first.pixel(50, 30)[..3];
assert!(shown == [0, 0, 255] || shown != [255, 255, 255], "a placeholder or the art: {shown:?}");
settle();
assert!(placed_document::generation() > generation || shown == [0, 0, 255], "a bitmap was made");
let later = render(&d, true);
let exact = render(&d, false);
assert_eq!(&later.pixel(50, 30)[..3], &[0, 0, 255]);
for (x, y) in [(12, 12), (50, 30), (88, 48), (95, 95), (5, 5)] {
let (a, b) = (later.pixel(x, y), exact.pixel(x, y));
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 24), "at ({x}, {y}): {a:?} vs {b:?}");
}
}
#[test]
fn copies_of_one_placed_document_share_its_bitmap() {
let mut d = doc("shared-1");
let n = d.node(NodeId(50)).unwrap().clone();
let l = d.layers[0].id;
let mut copy = n.clone();
copy.id = NodeId(51);
copy.transform(Affine::translate((0.0, 45.0)), false);
d.insert(Some(l), 1, copy).unwrap();
render(&d, true);
settle();
let r = render(&d, true);
assert!(!placed_document::pending_for("shared-1"), "nothing more to make");
assert_eq!(&r.pixel(50, 30)[..3], &[0, 0, 255]);
assert_eq!(&r.pixel(50, 75)[..3], &[0, 0, 255]);
}
#[test]
fn a_placed_document_kept_as_its_preview_draws_the_preview() {
let mut d = doc("preview-1");
// Only the preview (a 20×10 green PNG) is there, and the file can't be read here.
let img = image::RgbaImage::from_pixel(20, 10, image::Rgba([0, 200, 0, 255]));
let mut png = vec![];
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
let mut blob = ImageBlob::new("image/png", png);
blob.proxy = Some(blob.bytes.clone());
d.images.insert("preview-1".into(), blob);
let exact = render(&d, false);
assert_eq!(&exact.pixel(50, 30)[..3], &[0, 200, 0], "the preview, in the box");
assert_eq!(&exact.pixel(95, 95)[..3], &[255, 255, 255]);
// On screen past the preview's resolution (it has 0.5 px a point; this is 2): still the preview.
render(&d, true);
settle();
let shown = render(&d, true);
assert_eq!(&shown.pixel(50, 30)[..3], &[0, 200, 0]);
}
#[test]
fn a_document_that_places_itself_draws_a_few_levels_deep_then_stops() {
let d = doc_with("loop-1", b"loop");
let t = std::time::Instant::now();
let r = render(&d, false);
assert!(t.elapsed().as_secs() < 10);
assert_eq!(&r.pixel(50, 30)[..3], &[0, 0, 255]);
// On screen too.
render(&d, true);
settle();
assert_eq!(&render(&d, true).pixel(50, 30)[..3], &[0, 0, 255]);
}