mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
This commit is contained in:
@@ -285,7 +285,7 @@ fn deep(n: usize) -> Result<(), String> {
|
||||
let key = format!("photo {i}");
|
||||
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", photo));
|
||||
let xf = Affine::translate((f64::from(i % 2) * 800.0, f64::from(i / 2) * 600.0)) * Affine::scale(800.0 / 6000.0);
|
||||
let image = vectorcraft_doc::ImageObject { key, width: 6000, height: 4000, xf, link: None, placement: Default::default() };
|
||||
let image = vectorcraft_doc::ImageObject { key, width: 6000, height: 4000, xf, link: None, placement: Default::default(), pixels: None };
|
||||
let id = d.alloc_id();
|
||||
d.insert(l, usize::MAX, Node::new(id, vectorcraft_doc::NodeKind::Image(Box::new(image)))).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
@@ -917,7 +917,7 @@ impl<'a> Scene<'a> {
|
||||
self.warn(NO_IMAGES);
|
||||
return;
|
||||
}
|
||||
let Some(i) = self.image_def(&im.key) else { return };
|
||||
let Some(i) = self.image_def(im) else { return };
|
||||
let Some((dw, dh)) = self.images.get(i).map(|d| (f64::from(d.size.0), f64::from(d.size.1))) else { return };
|
||||
let (w, h) = (f64::from(im.width.max(1)), f64::from(im.height.max(1)));
|
||||
let map = self.to_dxf * im.xf;
|
||||
@@ -959,12 +959,13 @@ impl<'a> Scene<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The image file for image `key`, encoded once in the chosen format (`None`: left out).
|
||||
fn image_def(&mut self, key: &str) -> Option<usize> {
|
||||
/// The image file for image `im`, encoded once in the chosen format (`None`: left out).
|
||||
fn image_def(&mut self, im: &ImageObject) -> Option<usize> {
|
||||
let key = im.key.as_str();
|
||||
if let Some(i) = self.by_key.get(key) {
|
||||
return *i;
|
||||
}
|
||||
let made = self.encode_image(key);
|
||||
let made = self.encode_image(im);
|
||||
let i = match made {
|
||||
Ok((file, size)) => {
|
||||
let handle = self.handle();
|
||||
@@ -981,9 +982,9 @@ impl<'a> Scene<'a> {
|
||||
i
|
||||
}
|
||||
|
||||
/// Image `key` as a file in the chosen format, and its size in pixels.
|
||||
fn encode_image(&self, key: &str) -> Result<(DxfImage, (u32, u32)), String> {
|
||||
let blob = self.doc.images.get(key).filter(|b| !b.bytes.is_empty()).ok_or_else(|| "an image has no pixels and was left out".to_string())?;
|
||||
/// Image `im` as a file in the chosen format, and its size in pixels.
|
||||
fn encode_image(&self, im: &ImageObject) -> Result<(DxfImage, (u32, u32)), String> {
|
||||
let blob = self.doc.image_blob(im).filter(|b| !b.bytes.is_empty()).ok_or_else(|| "an image has no pixels and was left out".to_string())?;
|
||||
let img = image::load_from_memory(&blob.bytes).map_err(|_| UNDECODED.to_string())?;
|
||||
let size = (img.width(), img.height());
|
||||
let mut bytes = vec![];
|
||||
|
||||
+10
-1
@@ -536,6 +536,7 @@ fn images_link_png_or_jpeg_files() {
|
||||
xf: Affine::translate((10.0, 20.0)) * Affine::scale(5.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
vec![Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))]
|
||||
});
|
||||
@@ -569,7 +570,15 @@ fn images_without_pixels_are_left_out_with_one_note() {
|
||||
["raster-1", "raster-2"]
|
||||
.into_iter()
|
||||
.map(|key| {
|
||||
let im = ImageObject { key: key.into(), width: 4, height: 2, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject {
|
||||
key: key.into(),
|
||||
width: 4,
|
||||
height: 2,
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -1173,7 +1173,7 @@ mod tests {
|
||||
d.insert(Some(l), 0, Node::path(path, shapes::rectangle(Rect::new(20.0, 20.0, 60.0, 60.0)), Appearance::default_art())).unwrap();
|
||||
// 100×50 pixels, scaled to 100×100 and moved to (10, 10): over the whole path.
|
||||
let xf = vectorcraft_geom::Affine::translate((10.0, 10.0)) * vectorcraft_geom::Affine::scale_non_uniform(1.0, 2.0);
|
||||
let im = crate::ImageObject { key: "k".into(), width: 100, height: 50, xf, link: None, placement: Default::default() };
|
||||
let im = crate::ImageObject { key: "k".into(), width: 100, height: 50, xf, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 1, Node::new(image, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let path_only = HitOptions { path_only: true, ..Default::default() };
|
||||
let hit = |p: Point, opt| hit_test(&d, p, opt).map(|h| h.leaf);
|
||||
|
||||
+47
-1
@@ -1949,10 +1949,14 @@ impl Document {
|
||||
pub fn pattern_mut(&mut self, name: &str) -> Option<&mut PatternDef> {
|
||||
self.patterns.iter_mut().find(|p| p.name == name)
|
||||
}
|
||||
/// Deep-clone `node` with fresh ids for it and all descendants.
|
||||
/// Deep-clone `node` with fresh ids for it and all descendants, its images' own pixels taken
|
||||
/// in ([`Self::adopt_pixels`]).
|
||||
pub fn reid(&mut self, node: &Node) -> Node {
|
||||
let mut n = node.clone();
|
||||
n.id = self.alloc_id();
|
||||
if let NodeKind::Image(im) = &mut n.kind {
|
||||
self.adopt_image_pixels(im);
|
||||
}
|
||||
// A copied annotation must never bind its source ids to unrelated art in a different
|
||||
// document. Keep the measured geometry; new measurements can be linked after pasting.
|
||||
if let Some(e) = n.extras_if_any()
|
||||
@@ -1977,6 +1981,48 @@ impl Document {
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// The pixels image `im` shows: its own ([`ImageObject::pixels`]) or this document's.
|
||||
pub fn image_blob<'a>(&'a self, im: &'a ImageObject) -> Option<&'a ImageBlob> {
|
||||
im.pixels.as_deref().or_else(|| self.images.get(&im.key))
|
||||
}
|
||||
|
||||
/// Take the pixels the images in `n` (its opacity masks' art included) hold themselves (art
|
||||
/// made outside a document) into [`Self::images`]: under the image's key, or a free one when
|
||||
/// the key shows other pixels.
|
||||
pub fn adopt_pixels(&mut self, n: &mut Node) {
|
||||
// Only what holds such pixels is copied out of the art it shares.
|
||||
fn holds(n: &Node) -> bool {
|
||||
matches!(&n.kind, NodeKind::Image(im) if im.pixels.is_some())
|
||||
|| n.extras().mask.as_ref().is_some_and(|m| holds(&m.art))
|
||||
|| n.children().into_iter().flatten().any(|c| holds(c))
|
||||
}
|
||||
if let NodeKind::Image(im) = &mut n.kind {
|
||||
self.adopt_image_pixels(im);
|
||||
}
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut())
|
||||
&& holds(&m.art)
|
||||
{
|
||||
self.adopt_pixels(Arc::make_mut(&mut m.art));
|
||||
}
|
||||
for c in n.children_mut().into_iter().flatten() {
|
||||
if holds(c) {
|
||||
self.adopt_pixels(Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn adopt_image_pixels(&mut self, im: &mut ImageObject) {
|
||||
let Some(blob) = im.pixels.take() else { return };
|
||||
let mut key = im.key.clone();
|
||||
let mut i = 1;
|
||||
while self.images.get(&key).is_some_and(|b| b.bytes != blob.bytes) {
|
||||
i += 1;
|
||||
key = format!("{}-{i}", im.key);
|
||||
}
|
||||
self.images.entry(key.clone()).or_insert_with(|| (*blob).clone());
|
||||
im.key = key;
|
||||
}
|
||||
|
||||
/// Saved selections as a file gives them, made safe to list and recall: at most
|
||||
/// [`SavedSelection::MAX`], each with a clean, unique name and naming objects this document
|
||||
/// has, each once. (An id it doesn't have would select whatever object takes that id later.)
|
||||
|
||||
@@ -377,6 +377,7 @@ mod tests {
|
||||
xf: Affine::IDENTITY,
|
||||
link,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -433,6 +433,11 @@ pub struct ImageObject {
|
||||
/// Links panel → Placement Options: how a file read again takes this image's place.
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
pub placement: crate::links::PlacementOptions,
|
||||
/// The pixels of an image in art made outside a document (type's outlines, a glyph's art),
|
||||
/// which has no [`crate::Document::images`] to keep them in. A document takes them in
|
||||
/// ([`crate::Document::adopt_pixels`]): no image in one holds its own.
|
||||
#[serde(skip)]
|
||||
pub pixels: Option<Arc<crate::ImageBlob>>,
|
||||
}
|
||||
|
||||
/// Written with its `type` first, then its fields (an internally tagged enum); read by
|
||||
@@ -2543,7 +2548,8 @@ mod tests {
|
||||
/// A group holding a path, a compound, an image and a nested group, as a file writes it.
|
||||
fn nested_group() -> Node {
|
||||
let path = |id| Node::path(NodeId(id), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
let image = ImageObject { key: "k".into(), width: 2, height: 3, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let image =
|
||||
ImageObject { key: "k".into(), width: 2, height: 3, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
let inner = Node::new(NodeId(5), NodeKind::Group { children: vec![Arc::new(path(6))], clip: true });
|
||||
let compound = Node::new(NodeId(3), NodeKind::Compound { children: vec![Arc::new(path(4))], rule: FillRule::EvenOdd });
|
||||
let kids = vec![Arc::new(path(2)), Arc::new(compound), Arc::new(Node::new(NodeId(7), NodeKind::Image(Box::new(image)))), Arc::new(inner)];
|
||||
|
||||
@@ -146,7 +146,7 @@ impl PlacedDocument {
|
||||
pub fn preview_image(&self, bytes: &[u8]) -> Option<ImageObject> {
|
||||
let (w, h) = image_size(bytes).filter(|(w, h)| *w > 0 && *h > 0)?;
|
||||
let px = Affine::scale_non_uniform(self.width / w as f64, self.height / h as f64);
|
||||
Some(ImageObject { key: self.key.clone(), width: w, height: h, xf: self.xf * px, link: None, placement: Default::default() })
|
||||
Some(ImageObject { key: self.key.clone(), width: w, height: h, xf: self.xf * px, link: None, placement: Default::default(), pixels: None })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ fn read_preview(p: &PlacedDocument, bytes: &Arc<Vec<u8>>) -> Option<Arc<ReadDocu
|
||||
let mime = if bytes.starts_with(&[0xff, 0xd8]) { "image/jpeg" } else { "image/png" };
|
||||
doc.images.insert("preview".into(), ImageBlob { mime: mime.into(), bytes: bytes.clone(), proxy: None });
|
||||
let xf = Affine::scale_non_uniform(frame.width() / w as f64, frame.height() / h as f64);
|
||||
let im = ImageObject { key: "preview".into(), width: w, height: h, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "preview".into(), width: w, height: h, xf, link: None, placement: Default::default(), pixels: None };
|
||||
let art = Node::new(art_id(), NodeKind::Group { children: vec![Arc::new(Node::new(art_id(), NodeKind::Image(Box::new(im))))], clip: false });
|
||||
let e = Arc::new(ReadDocument { key: p.key.clone(), preview: true, bytes: bytes.clone(), frame, art: Arc::new(art), doc: Arc::new(doc) });
|
||||
cache().insert(e.clone());
|
||||
@@ -637,6 +637,7 @@ mod tests {
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
d.symbols.push(Symbol { name: "Star".into(), art: Arc::new(art) });
|
||||
|
||||
@@ -65,7 +65,9 @@ pub fn recolor_node(n: &mut Node, reach: Reach, v: &mut dyn ColorVisitor) -> usi
|
||||
}
|
||||
changed += ch as usize;
|
||||
}
|
||||
NodeKind::Image(im) if reach.images && im.link.is_none() => {
|
||||
// An image holding its own pixels is a glyph's (emoji in outlined type): it keeps its
|
||||
// colours, as the glyph does in type.
|
||||
NodeKind::Image(im) if reach.images && im.link.is_none() && im.pixels.is_none() => {
|
||||
if let Some(k) = v.image(&im.key) {
|
||||
im.key = k;
|
||||
changed += 1;
|
||||
|
||||
@@ -103,6 +103,7 @@ mod tests {
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
source.visible = false;
|
||||
|
||||
@@ -145,8 +145,9 @@ pub(crate) fn glyph_outlines(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::Bez
|
||||
(runs, all)
|
||||
}
|
||||
|
||||
/// [`glyph_outlines`], the bounds of `t`'s layout in text space, and the art of its colour glyphs
|
||||
/// (COLR layers, SVG documents), which have no outlines, placed in the document, with their runs.
|
||||
/// [`glyph_outlines`], the bounds of `t`'s layout in text space, and its colour glyphs, which have
|
||||
/// no outlines, placed in the document with their runs: bitmap glyphs (emoji) as images holding
|
||||
/// their pixels, then COLR and SVG glyphs as their art, as the renderer paints them.
|
||||
fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::BezPath, Rect, Vec<(usize, Node)>) {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let layout = vectorcraft_text::layout(db, t);
|
||||
@@ -165,14 +166,15 @@ fn laid_out(t: &vectorcraft_doc::TextObject) -> (Vec<kurbo::BezPath>, kurbo::Bez
|
||||
}
|
||||
all.extend(bar.iter());
|
||||
}
|
||||
let colours = layout.glyphs.iter().filter_map(|g| Some((g.run, g.placed_colour(t.xf)?))).collect();
|
||||
let images = layout.glyphs.iter().filter_map(|g| Some((g.run, g.placed_image_node(t.xf)?)));
|
||||
let colours = images.chain(layout.glyphs.iter().filter_map(|g| Some((g.run, g.placed_colour(t.xf)?)))).collect();
|
||||
(runs, all, layout.bounds, colours)
|
||||
}
|
||||
|
||||
/// Type as glyph outlines in document space: one path per run with the run's fill and stroke,
|
||||
/// its colour glyphs' art, and (when the object has its own fills/strokes) the whole outline with
|
||||
/// the object's items below the characters under them and the others over them, as the renderer
|
||||
/// paints them. Each run's path is placed by the type's transform, which also scales its stroke
|
||||
/// over its colour glyphs (images, art), and (when the object has its own fills/strokes) the whole
|
||||
/// outline with the object's items below the characters under them and the others over them, as
|
||||
/// the renderer paints them. Each run's path is placed by the type's transform, which also scales its stroke
|
||||
/// weights and dashes and transforms its pattern tiles; its unplaced gradients are first fixed to
|
||||
/// the layout's bounds in text space. The object's own unplaced gradients are fixed to the
|
||||
/// bounding box of those bounds in the document. The group is marked as type's outlines
|
||||
|
||||
@@ -262,6 +262,7 @@ fn stacked_transform_copies_stay_within_the_image_budget() {
|
||||
xf: Affine::translate((10.0, 0.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
)
|
||||
};
|
||||
@@ -458,6 +459,7 @@ fn transform_on_a_group_moves_and_copies_its_images() {
|
||||
xf: Affine::translate((10.0, 0.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
let mut g = Node::group(id, vec![Arc::new(sq), Arc::new(image)]);
|
||||
@@ -479,6 +481,7 @@ fn transform_on_a_group_moves_and_copies_its_images() {
|
||||
xf: Affine::translate((10.0, 0.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
))],
|
||||
);
|
||||
@@ -531,6 +534,7 @@ fn images_and_symbol_instances_share_the_copy_budget() {
|
||||
xf: Affine::translate((10.0, 0.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
let instance = Node::new(id, NodeKind::SymbolInstance { symbol: "s".into(), xf: Affine::IDENTITY });
|
||||
@@ -560,6 +564,7 @@ fn transform_copies_of_images_stop_at_the_reach_limit() {
|
||||
xf: Affine::translate((10.0, 0.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
let mut g = Node::group(id, vec![Arc::new(image)]);
|
||||
|
||||
@@ -421,8 +421,15 @@ fn import_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let img = fileio::raster_image(&data_param(p, C)?).map_err(|e| bad(C, e.to_string()))?;
|
||||
let (sx, sy) = pt_per_px(img.ppi);
|
||||
let (w, h) = (img.width, img.height);
|
||||
let image =
|
||||
ImageObject { key: img.key.clone(), width: w, height: h, xf: Affine::scale_non_uniform(sx, sy), link: None, placement: Default::default() };
|
||||
let image = ImageObject {
|
||||
key: img.key.clone(),
|
||||
width: w,
|
||||
height: h,
|
||||
xf: Affine::scale_non_uniform(sx, sy),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let mut clip = Clipboard { nodes: vec![Node::new(NodeId(1), NodeKind::Image(Box::new(image)))], ..Default::default() };
|
||||
clip.images.insert(img.key, img.blob);
|
||||
let count = s.load_clipboard(clip, p)?;
|
||||
|
||||
@@ -558,10 +558,12 @@ pub(crate) fn expand_node(d: &mut Document, id: NodeId, brushes: &[vectorcraft_b
|
||||
let mut listed = vec![];
|
||||
let m = expanded_node(d, n, brushes, &mut listed);
|
||||
out.extend(listed.into_iter().map(|(id, _)| id));
|
||||
if let Some(m) = m
|
||||
&& let Some(slot) = d.node_mut(id)
|
||||
{
|
||||
*slot = m;
|
||||
if let Some(mut m) = m {
|
||||
// Art made from type takes its emoji's pixels into the document.
|
||||
d.adopt_pixels(&mut m);
|
||||
if let Some(slot) = d.node_mut(id) {
|
||||
*slot = m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1274,7 +1274,15 @@ impl Builder {
|
||||
let sy = img.height as f64 / f64::from(r.height.max(1));
|
||||
let xf = to_doc * affine(img.transform) * Affine::scale_non_uniform(sx, sy);
|
||||
self.doc.images.entry(r.key.clone()).or_insert(r.blob);
|
||||
Some(NodeKind::Image(Box::new(ImageObject { key: r.key, width: r.width, height: r.height, xf, link: None, placement: Default::default() })))
|
||||
Some(NodeKind::Image(Box::new(ImageObject {
|
||||
key: r.key,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,8 @@ fn flatten_images(d: &mut Document, images: &[(usize, NodeId)]) -> Result<()> {
|
||||
let png = flat.png;
|
||||
let key = super::super::menucmds::unique_key(d, "flattened");
|
||||
d.images.insert(key.clone(), ImageBlob::new("image/png", png));
|
||||
let image = ImageObject { key, width: flat.width, height: flat.height, xf: flat.xf, link: None, placement: Default::default() };
|
||||
let image =
|
||||
ImageObject { key, width: flat.width, height: flat.height, xf: flat.xf, link: None, placement: Default::default(), pixels: None };
|
||||
let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image)));
|
||||
let mut group = super::super::rasterfx::clip_group(d, flat.clip, FillRule::NonZero, image);
|
||||
group.name = src.node(id).and_then(|n| n.name.clone());
|
||||
|
||||
@@ -658,8 +658,10 @@ pub(super) fn raster_doc(name: &str, bytes: &[u8]) -> Result<Document> {
|
||||
let layer = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::scale_non_uniform(sx, sy);
|
||||
let mut n =
|
||||
Node::new(id, NodeKind::Image(Box::new(ImageObject { key: key.clone(), width, height, xf, link: None, placement: Default::default() })));
|
||||
let mut n = Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key: key.clone(), width, height, xf, link: None, placement: Default::default(), pixels: None })),
|
||||
);
|
||||
n.name = Some(name.to_string());
|
||||
d.images.insert(key, blob);
|
||||
d.insert(Some(layer), 0, n).map_err(err)?;
|
||||
|
||||
@@ -248,6 +248,7 @@ fn a_raster_export_says_nothing_of_images_another_thread_could_not_decode() {
|
||||
xf: vectorcraft_geom::Affine::scale(20.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let (id, layer) = (broken.alloc_id(), broken.layers[0].id);
|
||||
broken.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
|
||||
@@ -1136,7 +1136,15 @@ fn raster_node(d: &mut Document, r: Raster) -> Node {
|
||||
d.images.insert(key.clone(), r.blob);
|
||||
let image = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: r.width, height: r.height, xf: r.xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
xf: r.xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
let Some(path) = r.clip else { return image };
|
||||
super::rasterfx::clip_group(d, path, FillRule::NonZero, image)
|
||||
|
||||
@@ -1334,6 +1334,8 @@ fn expand_envelope(d: &mut Document, id: NodeId) -> Result<()> {
|
||||
let n = d.node(id).cloned().ok_or(EngineError::NoNode(id))?;
|
||||
let mut g = vectorcraft_render::effects::expanded_live_group(Some(d), &n);
|
||||
fix_ids(d, &mut g);
|
||||
// Outlined type's emoji take their pixels into the document.
|
||||
d.adopt_pixels(&mut g);
|
||||
// Outlined type repeats its object's id on its pieces.
|
||||
vectorcraft_render::effects::fresh_ids(d, &mut g, &mut Default::default());
|
||||
*d.node_mut(id).ok_or(EngineError::NoNode(id))? = g;
|
||||
|
||||
@@ -469,7 +469,7 @@ impl Rasterized {
|
||||
let key = unique_key(d, "raster");
|
||||
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", self.png));
|
||||
let id = d.alloc_id();
|
||||
let image = ImageObject { key, width: self.width, height: self.height, xf: self.xf, link: None, placement: Default::default() };
|
||||
let image = ImageObject { key, width: self.width, height: self.height, xf: self.xf, link: None, placement: Default::default(), pixels: None };
|
||||
let node = Node::new(id, NodeKind::Image(Box::new(image)));
|
||||
match self.clip {
|
||||
Some((path, rule)) => super::rasterfx::clip_group(d, path, rule, node),
|
||||
@@ -721,6 +721,7 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
xf: Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
@@ -788,6 +789,7 @@ fn trim_image(s: &mut Session, id: NodeId, im: ImageObject) -> Result<Value> {
|
||||
xf: im.xf * Affine::translate((f64::from(x0), f64::from(y0))),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -385,6 +385,7 @@ fn build(d: &mut Document, l: Loaded, link: Option<LinkInfo>) -> Node {
|
||||
xf: Affine::scale_non_uniform(sx, sy),
|
||||
link,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ fn bitmap_document(cmd: &str, doc: &Document, set: &PrintSettings, rich_black: b
|
||||
let key = unique_key(&out, "print-bitmap");
|
||||
out.images.insert(key.clone(), ImageBlob::new("image/png", img.to_png().map_err(EngineError::Other)?));
|
||||
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
||||
let image = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() };
|
||||
let image = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default(), pixels: None };
|
||||
images.push(Arc::new(Node::new(out.alloc_id(), NodeKind::Image(Box::new(image)))));
|
||||
}
|
||||
let mut layer = Node::layer(out.alloc_id(), "Bitmap", LayerColor::Preset(0));
|
||||
|
||||
@@ -225,7 +225,15 @@ pub(crate) fn effect_image(out: &mut Document, whole: &Node, knockout: Option<&N
|
||||
let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale);
|
||||
let mut image = Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
image.name = Some("Raster effect".into());
|
||||
Some(image)
|
||||
|
||||
@@ -92,6 +92,7 @@ mod tests {
|
||||
xf: Affine::scale(72.0 / ppi),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{AreaFit, Document, ImageObject, Justify, Node, NodeId, NodeKind, RubyAlign, RubyPosition, RubySpan, TextObject};
|
||||
use vectorcraft_doc::{AreaFit, Document, Justify, Node, NodeId, NodeKind, RubyAlign, RubyPosition, RubySpan, TextObject};
|
||||
use vectorcraft_geom::{Affine, PathData, Vec2};
|
||||
|
||||
use super::edit::selected_roots;
|
||||
@@ -559,18 +559,10 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
children.push(Arc::new(Node::new(id, NodeKind::SymbolInstance { symbol: art.symbol.clone(), xf })));
|
||||
continue;
|
||||
}
|
||||
// Colour bitmap glyphs (emoji) become embedded images, in place; glyphs that show
|
||||
// the same image share its pixels, under a key of their own if another image has
|
||||
// taken theirs.
|
||||
if let Some((image, to_text)) = g.placed_image() {
|
||||
let mut key = format!("glyph-{}", image.blob.content_key());
|
||||
if d.images.get(&key).is_some_and(|b| b.bytes != image.blob.bytes) {
|
||||
key = super::menucmds::unique_key(d, "glyph");
|
||||
}
|
||||
d.images.entry(key.clone()).or_insert_with(|| image.blob.clone());
|
||||
let im =
|
||||
ImageObject { key, width: image.width, height: image.height, xf: t.xf * to_text, link: None, placement: Default::default() };
|
||||
children.push(Arc::new(Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)))));
|
||||
// Colour bitmap glyphs (emoji) become embedded images, in place, their pixels taken
|
||||
// into the document: glyphs that show the same image share them.
|
||||
if let Some(image) = g.placed_image_node(t.xf) {
|
||||
children.push(Arc::new(d.reid(&image)));
|
||||
continue;
|
||||
}
|
||||
// COLR glyphs become groups of their layers, in place, with ids of the document's.
|
||||
|
||||
@@ -42,6 +42,7 @@ fn red_image(s: &mut Session) -> NodeId {
|
||||
xf: vectorcraft_geom::Affine::scale(10.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
|
||||
@@ -1976,7 +1976,15 @@ fn add_image(s: &mut Session, r: &vectorcraft_trace::Raster, xf: Affine) -> Node
|
||||
0,
|
||||
Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key: "img1".into(), width: w, height: h, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "img1".into(),
|
||||
width: w,
|
||||
height: h,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
),
|
||||
)?;
|
||||
sel.set([id]);
|
||||
|
||||
@@ -168,3 +168,63 @@ fn the_canvas_draws_svg_glyphs_as_their_art() {
|
||||
assert_eq!(img.pixel(14, 40), [255, 0, 0, 255], "its own red");
|
||||
assert_eq!(img.pixel(22, 40), [0, 0, 0, 255], "the type's black fill");
|
||||
}
|
||||
|
||||
/// Expand Appearance of type with an effect keeps its emoji as images, their pixels taken into
|
||||
/// the document; the PDF of the type before expanding draws them too.
|
||||
#[test]
|
||||
fn expand_appearance_keeps_the_emoji_of_type_with_effects() {
|
||||
use vectorcraft_text::test_fonts::{COLOUR_CHAR, COLOUR_LARGE, COLOUR_PPEM, colour_bitmap_family, colour_bitmap_font, solid_png};
|
||||
vectorcraft_text::FontDb::global().add_font(colour_bitmap_font(false).unwrap());
|
||||
let mut s = session();
|
||||
let r = s
|
||||
.execute("text.create", &json!({"x": 10, "y": 50, "text": format!("{COLOUR_CHAR}F"), "size": 32, "font": colour_bitmap_family(false)}))
|
||||
.unwrap();
|
||||
let t = NodeId(r["id"].as_u64().unwrap());
|
||||
// Turned, not only moved: type that a Transform only moves stays type in exports.
|
||||
s.execute("effect.apply", &json!({"ids": [t.0], "effect": "distort.transform", "params": {"rotate": 10}})).unwrap();
|
||||
let pdf = vectorcraft_pdf::export_with_report(&s.doc().unwrap().doc, &vectorcraft_pdf::PdfOptions::uncompressed()).unwrap();
|
||||
assert!(pdf.warnings.is_empty(), "{:?}", pdf.warnings);
|
||||
assert!(pdf.bytes.windows(14).any(|w| w == b"/Subtype/Image"), "the emoji, in the PDF");
|
||||
let r = ids(&s.execute("effect.expandAppearance", &json!({})).unwrap());
|
||||
let mut images = vec![];
|
||||
for id in r {
|
||||
node(&s, id).walk(&mut |n| {
|
||||
if let NodeKind::Image(im) = &n.kind {
|
||||
images.push((**im).clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let glyph = vectorcraft_doc::ImageBlob::png(solid_png(COLOUR_PPEM.into(), COLOUR_LARGE));
|
||||
assert_eq!(images.len(), 1, "the emoji");
|
||||
assert!(images[0].pixels.is_none(), "the document holds them");
|
||||
assert_eq!(d.images[&images[0].key].bytes, glyph.bytes);
|
||||
vectorcraft_testkit::invariants::check_document(d).unwrap();
|
||||
}
|
||||
|
||||
/// Emoji type in a warp envelope: SVG export writes each emoji's pixels once for all the warp's
|
||||
/// pieces, and Object › Expand takes them into the document.
|
||||
#[test]
|
||||
fn an_envelope_shares_and_expands_the_emoji_of_its_type() {
|
||||
use vectorcraft_text::test_fonts::{COLOUR_CHAR, colour_bitmap_family, colour_bitmap_font};
|
||||
vectorcraft_text::FontDb::global().add_font(colour_bitmap_font(false).unwrap());
|
||||
let mut s = session();
|
||||
let r = s
|
||||
.execute("text.create", &json!({"x": 10, "y": 50, "text": COLOUR_CHAR.to_string(), "size": 32, "font": colour_bitmap_family(false)}))
|
||||
.unwrap();
|
||||
s.execute("select.set", &json!({"ids": [r["id"]]})).unwrap();
|
||||
let e = NodeId(s.execute("object.envelope.makeWithWarp", &json!({"style": "arch", "bend": 50})).unwrap()["id"].as_u64().unwrap());
|
||||
let svg = vectorcraft_svg::export_full(&s.doc().unwrap().doc, &Default::default(), None).svg;
|
||||
assert_eq!(svg.matches("data:image/png").count(), 1, "one copy of the pixels");
|
||||
s.execute("object.envelope.expand", &json!({})).unwrap();
|
||||
let mut images = vec![];
|
||||
node(&s, e).walk(&mut |n| {
|
||||
if let NodeKind::Image(im) = &n.kind {
|
||||
images.push((**im).clone());
|
||||
}
|
||||
});
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert!(images.len() > 1, "the warp's pieces");
|
||||
assert!(images.iter().all(|im| im.pixels.is_none() && d.images.contains_key(&im.key)), "the document holds them");
|
||||
vectorcraft_testkit::invariants::check_document(d).unwrap();
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ fn image_node(s: &mut Session) -> NodeId {
|
||||
xf: Default::default(),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
|
||||
@@ -122,7 +122,7 @@ fn red_image(s: &mut Session) -> NodeId {
|
||||
d.images.insert("pic".into(), vectorcraft_doc::ImageBlob::png(png));
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::translate((100.0, 100.0)) * Affine::scale(10.0);
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 4, height: 4, xf, link: None, placement: Default::default() };
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 4, height: 4, xf, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
|
||||
@@ -160,7 +160,7 @@ fn clicking_an_image_samples_its_pixel() {
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let xf = vectorcraft_geom::Affine::translate((300.0, 0.0)) * vectorcraft_geom::Affine::scale(100.0);
|
||||
let im = ImageObject { key: "px".into(), width: 2, height: 1, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "px".into(), width: 2, height: 1, xf, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).map_err(|e| crate::EngineError::Other(e.to_string()))
|
||||
})
|
||||
.unwrap();
|
||||
@@ -186,7 +186,7 @@ fn cmyk_doc_with_image(px: &[[u8; 4]]) -> Session {
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let xf = vectorcraft_geom::Affine::translate((300.0, 0.0)) * vectorcraft_geom::Affine::scale(100.0);
|
||||
let im = ImageObject { key: "px".into(), width: w, height: 1, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "px".into(), width: w, height: 1, xf, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).map_err(|e| crate::EngineError::Other(e.to_string()))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -462,6 +462,7 @@ fn relative_paths_are_written_against_the_saved_file() {
|
||||
xf: Default::default(),
|
||||
link: Some(LinkInfo::new(format!("{root}{sep}art{sep}a.png"))),
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(Some(lid), 0, vectorcraft_doc::Node::new(nid, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let rel = |dest: String| {
|
||||
|
||||
@@ -1103,7 +1103,7 @@ fn two_colour_image(s: &mut Session, clear_right: bool, linked: bool) -> NodeId
|
||||
let id = d.alloc_id();
|
||||
let xf = vectorcraft_geom::Affine::translate((100.0, 50.0)) * vectorcraft_geom::Affine::scale(2.0);
|
||||
let link = linked.then(|| vectorcraft_doc::LinkInfo { path: "/tmp/pic.png".into(), ..Default::default() });
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 8, height: 4, xf, link, placement: Default::default() };
|
||||
let im = vectorcraft_doc::ImageObject { key: "pic".into(), width: 8, height: 4, xf, link, placement: Default::default(), pixels: None };
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
|
||||
@@ -890,7 +890,7 @@ fn create_outlines_keeps_an_image_already_under_a_glyph_images_key() {
|
||||
vectorcraft_text::FontDb::global().add_font(colour_bitmap_font(false).unwrap());
|
||||
let mut s = session();
|
||||
let glyph = vectorcraft_doc::ImageBlob::png(solid_png(COLOUR_PPEM.into(), COLOUR_LARGE));
|
||||
let taken = format!("glyph-{}", glyph.content_key());
|
||||
let taken = glyph.content_key();
|
||||
let other = vectorcraft_doc::ImageBlob::png(solid_png(4, [0, 255, 0, 255]));
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).images.insert(taken.clone(), other.clone());
|
||||
s.execute("text.create", &json!({"x": 10, "y": 50, "text": COLOUR_CHAR.to_string(), "size": 32, "font": colour_bitmap_family(false)})).unwrap();
|
||||
|
||||
@@ -103,7 +103,15 @@ fn groups_layers_type_images_and_symbols_keep_their_raster_effects() {
|
||||
let xf = Affine::translate((40.0, 50.0)) * Affine::scale(2.5);
|
||||
let im = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key: "pic".into(), width: 40, height: 30, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "pic".into(),
|
||||
width: 40,
|
||||
height: 30,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
add(&mut d, with(im, shadow()));
|
||||
assert_written("image", &d);
|
||||
|
||||
@@ -283,7 +283,7 @@ fn the_eyedropper_takes_an_image_pixel_into_the_working_rgb_space() {
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let xf = Affine::translate((300.0, 0.0)) * Affine::scale(100.0);
|
||||
let im = ImageObject { key: "px".into(), width: 1, height: 1, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "px".into(), width: 1, height: 1, xf, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).map_err(|e| vectorcraft_engine::EngineError::Other(e.to_string()))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -889,7 +889,7 @@ fn gradient_mesh_from_images_with_junk_params() {
|
||||
d.images.insert("pic".into(), ImageBlob::png(bytes.clone()));
|
||||
let id = d.alloc_id();
|
||||
let link = linked.then(|| LinkInfo { path: "/nonexistent/pic.png".into(), ..Default::default() });
|
||||
let im = ImageObject { key: "pic".into(), width: *w, height: *h, xf: *xf, link, placement: Default::default() };
|
||||
let im = ImageObject { key: "pic".into(), width: *w, height: *h, xf: *xf, link, placement: Default::default(), pixels: None };
|
||||
d.insert(d.default_layer(), 0, Node::new(id, NodeKind::Image(Box::new(im))))?;
|
||||
sel.set([id]);
|
||||
Ok(())
|
||||
|
||||
@@ -2192,7 +2192,7 @@ impl<'a> Reader<'a> {
|
||||
let mask = alpha.and_then(|a| self.alpha_mask(a, w, h, xf));
|
||||
let key = blob.content_key();
|
||||
self.doc.images.entry(key.clone()).or_insert(blob);
|
||||
self.add_image_masked(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() }, mask, false)
|
||||
self.add_image_masked(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default(), pixels: None }, mask, false)
|
||||
}
|
||||
|
||||
/// An image's alpha channel (`w` × `h` bytes) as a grayscale image over the image (`xf`): its
|
||||
@@ -2208,7 +2208,7 @@ impl<'a> Reader<'a> {
|
||||
self.doc.images.entry(key.clone()).or_insert(blob);
|
||||
let art = Node::new(
|
||||
self.doc.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default(), pixels: None })),
|
||||
);
|
||||
Some(OpacityMask::new(art, true))
|
||||
}
|
||||
@@ -2253,7 +2253,10 @@ impl<'a> Reader<'a> {
|
||||
let key = blob.content_key();
|
||||
self.doc.images.entry(key.clone()).or_insert(blob);
|
||||
self.warn(LINKED_PREVIEWS);
|
||||
self.add_image(ImageObject { key, width: w, height: h, xf, link: Some(LinkInfo::new(path)), placement: Default::default() }, hidden)
|
||||
self.add_image(
|
||||
ImageObject { key, width: w, height: h, xf, link: Some(LinkInfo::new(path)), placement: Default::default(), pixels: None },
|
||||
hidden,
|
||||
)
|
||||
}
|
||||
|
||||
// ---- the document ----------------------------------------------------------------------
|
||||
|
||||
@@ -1225,7 +1225,7 @@ impl Interp<'_> {
|
||||
fn add_image(&mut self, blob: ImageBlob, w: u32, h: u32, xf: Affine, mask: Option<OpacityMask>) -> Res {
|
||||
let key = blob.content_key();
|
||||
self.out.doc.images.entry(key.clone()).or_insert(blob);
|
||||
let im = ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default(), pixels: None };
|
||||
let clips = self.g.clips.clone();
|
||||
self.out.push(Node::new(NodeId(0), NodeKind::Image(Box::new(im))).with_mask(mask.map(Box::new)), &clips);
|
||||
Ok(())
|
||||
@@ -1242,7 +1242,7 @@ impl Interp<'_> {
|
||||
let blob = ImageBlob::new("image/png", png);
|
||||
let key = blob.content_key();
|
||||
self.out.doc.images.entry(key.clone()).or_insert(blob);
|
||||
let im = ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default(), pixels: None };
|
||||
Some(OpacityMask::new(Node::new(self.out.doc.alloc_id(), NodeKind::Image(Box::new(im))), true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1634,6 +1634,7 @@ mod tests {
|
||||
xf: Affine::scale(1.0 / 1.6),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
page.insert(Some(page.layers[0].id), usize::MAX, image).unwrap();
|
||||
|
||||
@@ -48,8 +48,10 @@ fn image_doc(img: image::RgbaImage, frame: Rect) -> Option<Document> {
|
||||
let key = blob.content_key();
|
||||
doc.images.insert(key.clone(), blob);
|
||||
let xf = Affine::scale_non_uniform(frame.width() / f64::from(w), frame.height() / f64::from(h));
|
||||
let mut n =
|
||||
Node::new(doc.alloc_id(), NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default() })));
|
||||
let mut n = Node::new(
|
||||
doc.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: w, height: h, xf, link: None, placement: Default::default(), pixels: None })),
|
||||
);
|
||||
n.name = Some("Preview".into());
|
||||
let layer = doc.layers.first().map(|l| l.id)?;
|
||||
doc.insert(Some(layer), 0, n).ok()?;
|
||||
|
||||
@@ -598,6 +598,7 @@ fn rich_doc() -> Document {
|
||||
xf: Affine::new([10.0, 0.0, 0.0, 10.0, 110.0, 110.0]),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
add(&mut d, n);
|
||||
|
||||
@@ -678,7 +678,7 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
NodeKind::Image(im) => {
|
||||
self.lock(locked);
|
||||
self.image(self.doc.images.get(&im.key), im.width, im.height, im.xf);
|
||||
self.image(self.doc.image_blob(im), im.width, im.height, im.xf);
|
||||
}
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
self.warn(LIVE);
|
||||
|
||||
@@ -288,7 +288,7 @@ impl<'a> Scene<'a> {
|
||||
self.shape(n, &bp, *rule);
|
||||
}
|
||||
NodeKind::Text(t) => self.text(n, t),
|
||||
NodeKind::Image(im) => self.image(&im.key, self.doc.images.get(&im.key), im.width, im.height, im.xf),
|
||||
NodeKind::Image(im) => self.image(&im.key, self.doc.image_blob(im), im.width, im.height, im.xf),
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
|
||||
&& self.nest < MAX_NEST
|
||||
|
||||
@@ -268,6 +268,7 @@ fn png_doc() -> Document {
|
||||
xf: Affine::translate((10.0, 10.0)) * Affine::scale(10.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
let layer = d.layers[0].id;
|
||||
|
||||
@@ -62,8 +62,15 @@ fn sample() -> Document {
|
||||
let blob = ImageBlob::new("image/png", png);
|
||||
let key = blob.content_key();
|
||||
doc.images.insert(key.clone(), blob);
|
||||
let im =
|
||||
ImageObject { key, width: 2, height: 1, xf: Affine::new([10.0, 0.0, 0.0, 10.0, 170.0, 70.0]), link: None, placement: Default::default() };
|
||||
let im = ImageObject {
|
||||
key,
|
||||
width: 2,
|
||||
height: 1,
|
||||
xf: Affine::new([10.0, 0.0, 0.0, 10.0, 170.0, 70.0]),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let im = Node::new(doc.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
doc.insert(Some(front), usize::MAX, im).unwrap();
|
||||
if let Some(l) = doc.layers.get_mut(1) {
|
||||
@@ -114,7 +121,15 @@ fn as_printed(doc: &Document, o: &LegacyAiOptions) -> Document {
|
||||
let Some((image, to_text)) = g.placed_image() else { continue };
|
||||
let key = image.blob.content_key();
|
||||
images.insert(key.clone(), image.blob.clone());
|
||||
let im = ImageObject { key, width: image.width, height: image.height, xf: t.xf * to_text, link: None, placement: Default::default() };
|
||||
let im = ImageObject {
|
||||
key,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
xf: t.xf * to_text,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
nodes.push(Arc::new(Node::new(vectorcraft_doc::NodeId(0), NodeKind::Image(Box::new(im)))));
|
||||
}
|
||||
// COLR glyphs: their layers.
|
||||
@@ -264,8 +279,10 @@ fn a_rotated_image_keeps_its_place() {
|
||||
doc.images.insert(key.clone(), blob);
|
||||
// Turned 90° clockwise on the page: the pixel rows run down, the next row is to the left.
|
||||
let xf = Affine::new([0.0, 10.0, -10.0, 0.0, 100.0, 40.0]);
|
||||
let im =
|
||||
Node::new(doc.alloc_id(), NodeKind::Image(Box::new(ImageObject { key, width: 2, height: 1, xf, link: None, placement: Default::default() })));
|
||||
let im = Node::new(
|
||||
doc.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: 2, height: 1, xf, link: None, placement: Default::default(), pixels: None })),
|
||||
);
|
||||
doc.layers.clear();
|
||||
let layer = doc.add_layer(Some("Art"));
|
||||
doc.insert(Some(layer), usize::MAX, im).unwrap();
|
||||
@@ -297,7 +314,10 @@ fn picture(doc: &mut Document, r: Rect) -> Node {
|
||||
let key = blob.content_key();
|
||||
doc.images.insert(key.clone(), blob);
|
||||
let xf = Affine::new([r.width() / 16.0, 0.0, 0.0, r.height() / 16.0, r.x0, r.y0]);
|
||||
Node::new(doc.alloc_id(), NodeKind::Image(Box::new(ImageObject { key, width: 16, height: 16, xf, link: None, placement: Default::default() })))
|
||||
Node::new(
|
||||
doc.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: 16, height: 16, xf, link: None, placement: Default::default(), pixels: None })),
|
||||
)
|
||||
}
|
||||
|
||||
/// A layer `Below` holding a clipping group (a circle clipping an image, as in #2692), a group
|
||||
@@ -411,7 +431,7 @@ fn empty_images_get_the_size_note_and_undecodable_ones_the_decode_note() {
|
||||
let blob = ImageBlob::new("image/png", bytes);
|
||||
let key = blob.content_key();
|
||||
doc.images.insert(key.clone(), blob);
|
||||
let im = ImageObject { key, width: 4, height: 2, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key, width: 4, height: 2, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
let node = Node::new(doc.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
doc.insert(Some(layer), usize::MAX, node).unwrap();
|
||||
let out = export_legacy_ai(&doc, &LegacyAiOptions::default()).unwrap();
|
||||
|
||||
@@ -725,7 +725,7 @@ mod tests {
|
||||
d.images.insert("k".into(), ImageBlob::new("image/png", vec![1, 2, 3, 250]));
|
||||
// Only images in use are saved.
|
||||
let img = d.alloc_id();
|
||||
let im = ImageObject { key: "k".into(), width: 1, height: 1, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "k".into(), width: 1, height: 1, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 1, Node::new(img, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let bytes = save(&d, true);
|
||||
assert!(sniff(&bytes));
|
||||
|
||||
@@ -12,7 +12,7 @@ use vectorcraft_geom::Affine;
|
||||
|
||||
fn image(d: &mut Document, key: &str, link: Option<LinkInfo>) {
|
||||
let (id, layer) = (d.alloc_id(), d.layers[0].id);
|
||||
let im = ImageObject { key: key.into(), width: 600, height: 300, xf: Affine::IDENTITY, link, placement: Default::default() };
|
||||
let im = ImageObject { key: key.into(), width: 600, height: 300, xf: Affine::IDENTITY, link, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ fn image_node(d: &mut Document, key: &str) -> Node {
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ fn doc_with_image(key: &str, len: usize) -> Document {
|
||||
d.images.insert(key.into(), ImageBlob::new("image/png", bytes));
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: key.into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: key.into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ proptest! {
|
||||
d.images.insert(key.clone(), ImageBlob::new("image/png", data.clone()));
|
||||
let layer = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: key.clone(), width: 1, height: 1, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: key.clone(), width: 1, height: 1, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
for pretty in [false, true] {
|
||||
let back = load(&save(&d, pretty)).unwrap();
|
||||
|
||||
@@ -30,6 +30,7 @@ fn linked_doc() -> Document {
|
||||
xf: Affine::IDENTITY,
|
||||
link: Some(LinkInfo::new("/art/photo.png")),
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let mut blob = ImageBlob::new("image/png", vec![1, 2, 3, 4, 5, 6]);
|
||||
|
||||
@@ -20,7 +20,8 @@ fn doc_with(images: &[(&str, &str, usize)]) -> Document {
|
||||
let bytes: Vec<u8> = (0..len).map(|i| ((i * 7 + n * 13) % 251) as u8).collect();
|
||||
d.images.insert(key.into(), ImageBlob::new(mime, bytes));
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: key.into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im =
|
||||
ImageObject { key: key.into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
}
|
||||
d
|
||||
|
||||
@@ -788,7 +788,7 @@ impl Player {
|
||||
if !xf.as_coeffs().iter().all(|v| v.is_finite()) || xf.determinant().abs() < 1e-12 {
|
||||
return;
|
||||
}
|
||||
let im = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key, width: img.width, height: img.height, xf, link: None, placement: Default::default(), pixels: None };
|
||||
let mut n = Node::new(vectorcraft_doc::NodeId(0), NodeKind::Image(Box::new(im)));
|
||||
n.opacity = opacity.clamp(0.0, 1.0) as f32;
|
||||
self.push(n);
|
||||
|
||||
@@ -433,19 +433,20 @@ impl<'a> Scene<'a> {
|
||||
|
||||
/// A placed image, decoded once per image.
|
||||
fn image(&mut self, im: &ImageObject) {
|
||||
let Some(img) = self.decoded(&im.key) else { return };
|
||||
let Some(img) = self.decoded(im) else { return };
|
||||
let (dw, dh) = (f64::from(img.width), f64::from(img.height));
|
||||
let xf = im.xf * Affine::scale_non_uniform(f64::from(im.width.max(1)) / dw, f64::from(im.height.max(1)) / dh);
|
||||
self.ops.push(Op::Image { image: img, xf, opacity: self.opacity.clamp(0.0, 1.0) });
|
||||
}
|
||||
|
||||
/// Image `key`'s pixels (scaled down to what a metafile stores), `None` with a warning when it
|
||||
/// Image `im`'s pixels (scaled down to what a metafile stores), `None` with a warning when it
|
||||
/// has none or can't be decoded.
|
||||
fn decoded(&mut self, key: &str) -> Option<Arc<Rgba>> {
|
||||
fn decoded(&mut self, im: &ImageObject) -> Option<Arc<Rgba>> {
|
||||
let key = im.key.as_str();
|
||||
if let Some(i) = self.images.get(key) {
|
||||
return i.clone();
|
||||
}
|
||||
let blob = self.doc.images.get(key).filter(|b| !b.bytes.is_empty());
|
||||
let blob = self.doc.image_blob(im).filter(|b| !b.bytes.is_empty());
|
||||
let img = blob
|
||||
.and_then(|b| image::load_from_memory(&b.bytes).ok())
|
||||
.map(|i| Rgba::fitted(i.to_rgba8()))
|
||||
|
||||
@@ -51,7 +51,10 @@ pub(crate) fn image_node(d: &mut Document, bytes: Vec<u8>, w: u32, h: u32, xf: A
|
||||
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() })))
|
||||
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 {
|
||||
|
||||
@@ -1849,11 +1849,12 @@ impl Exporter<'_> {
|
||||
font
|
||||
}
|
||||
|
||||
/// Image `key` placed `size` points wide and high, resampled and compressed as the Compression
|
||||
/// Image `im` placed `size` points wide and high, resampled and compressed as the Compression
|
||||
/// settings say ([`crate::images::recode`], [`crate::images::recode_cmyk`] for CMYK images);
|
||||
/// cached by key (and size when images are resampled). `None`, with [`UNDECODED`] among the
|
||||
/// warnings, for an image that is missing, can't be decoded or is in [`Self::left_out`].
|
||||
fn load_image(&mut self, key: &str, size: (f64, f64)) -> Option<Image> {
|
||||
fn load_image(&mut self, im: &vectorcraft_doc::ImageObject, size: (f64, f64)) -> Option<Image> {
|
||||
let key = im.key.as_str();
|
||||
let c = self.compression;
|
||||
let sized = [c.color.downsample, c.gray.downsample, c.mono.downsample].iter().any(|d| *d != crate::Downsample::None);
|
||||
let cache = if sized { format!("{key}\u{0}{:.3}x{:.3}", size.0, size.1) } else { key.to_string() };
|
||||
@@ -1863,7 +1864,7 @@ impl Exporter<'_> {
|
||||
let interpolate = self.interpolate;
|
||||
let doc = self.doc;
|
||||
let out = self.out.clone();
|
||||
let img = doc.images.get(key).filter(|_| !self.left_out.contains(key)).and_then(|blob| {
|
||||
let img = doc.image_blob(im).filter(|_| !self.left_out.contains(key)).and_then(|blob| {
|
||||
let cmyk_image = blob.is_cmyk();
|
||||
if !cmyk_image && self.convert && doc.color_mode == vectorcraft_doc::ColorMode::Cmyk {
|
||||
self.rgb_in_cmyk.images.insert(key.to_string());
|
||||
@@ -1904,8 +1905,8 @@ impl Exporter<'_> {
|
||||
return i.clone();
|
||||
}
|
||||
let doc = self.doc;
|
||||
let img = doc.images.get(&im.key).filter(|b| b.is_cmyk()).and_then(|blob| {
|
||||
let alpha = crate::images::mask_alpha(doc.images.get(&mi.key)?, im.width, im.height, m.clip)?;
|
||||
let img = doc.image_blob(im).filter(|b| b.is_cmyk()).and_then(|blob| {
|
||||
let alpha = crate::images::mask_alpha(doc.image_blob(mi)?, im.width, im.height, m.clip)?;
|
||||
let r = crate::images::recode_cmyk(blob, size, self.compression, self.interpolate, self.out.cmyk_pixels(), Some(alpha));
|
||||
if let (Some(w), Some(_)) = (r.warning, &r.image) {
|
||||
self.warn(w);
|
||||
@@ -1922,7 +1923,7 @@ impl Exporter<'_> {
|
||||
if !writable(s, im.xf, Rect::new(0.0, 0.0, im.width.max(1).into(), im.height.max(1).into())) {
|
||||
return;
|
||||
}
|
||||
let Some(img) = img.or_else(|| self.load_image(&im.key, placed_size(im))) else { return };
|
||||
let Some(img) = img.or_else(|| self.load_image(im, placed_size(im))) else { return };
|
||||
let Some(size) = Size::from_wh(im.width.max(1) as f32, im.height.max(1) as f32) else { return };
|
||||
s.push_transform(&xf(im.xf));
|
||||
s.draw_image(img, size);
|
||||
|
||||
@@ -1569,7 +1569,7 @@ impl<'p> Builder<'p> {
|
||||
let xf = transform * Affine::scale_non_uniform(f64::from(w) / f64::from(mw.max(1)), f64::from(h) / f64::from(mh.max(1)));
|
||||
let art = Node::new(
|
||||
self.id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key: k, width: mw, height: mh, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject { key: k, width: mw, height: mh, xf, link: None, placement: Default::default(), pixels: None })),
|
||||
);
|
||||
Some(OpacityMask::new(art, true))
|
||||
}
|
||||
@@ -1589,8 +1589,10 @@ impl<'p> Builder<'p> {
|
||||
(k, w, h)
|
||||
};
|
||||
let id = self.id();
|
||||
let mut n =
|
||||
Node::new(id, NodeKind::Image(Box::new(ImageObject { key: k, width: w, height: h, xf, link: None, placement: Default::default() })));
|
||||
let mut n = Node::new(
|
||||
id,
|
||||
NodeKind::Image(Box::new(ImageObject { key: k, width: w, height: h, xf, link: None, placement: Default::default(), pixels: None })),
|
||||
);
|
||||
n.extras_mut().mask = mask.map(Box::new);
|
||||
self.push_node(n);
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ fn art_reaching_beyond_a_billion_points_is_left_out() {
|
||||
xf: Affine::new([5.5e8, 0.0, 0.0, 10.0, far, 60.0]),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
for (what, n) in [
|
||||
("path", rect_node(Rect::new(far, 60.0, 90.0, 90.0), Color::BLACK)),
|
||||
@@ -560,6 +561,7 @@ fn image_roundtrip() {
|
||||
xf: Affine::translate((20.0, 30.0)) * Affine::scale(10.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
add(&mut d, Node::new(NodeId(0), NodeKind::Image(Box::new(im))));
|
||||
let out = roundtrip(&d);
|
||||
@@ -601,7 +603,7 @@ fn an_image_whose_pixels_cant_be_decoded_is_left_out_with_one_note() {
|
||||
for (i, (key, blob)) in blobs.into_iter().enumerate() {
|
||||
d.images.insert(key.into(), blob);
|
||||
let xf = Affine::translate((20.0, 20.0 + 50.0 * i as f64)) * Affine::scale(10.0);
|
||||
let im = ImageObject { key: key.into(), width: 4, height: 2, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: key.into(), width: 4, height: 2, xf, link: None, placement: Default::default(), pixels: None };
|
||||
add(&mut d, Node::new(NodeId(0), NodeKind::Image(Box::new(im))));
|
||||
}
|
||||
add(&mut d, rect_node(Rect::new(100.0, 100.0, 150.0, 150.0), Color::BLACK));
|
||||
@@ -634,6 +636,7 @@ fn a_group_transform_effect_exports_its_image_copies() {
|
||||
xf: Affine::translate((20.0, 30.0)) * Affine::scale(5.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let image = Arc::new(Node::new(d.alloc_id(), NodeKind::Image(Box::new(im))));
|
||||
let mut square = rect_node(Rect::new(20.0, 60.0, 30.0, 70.0), Color::BLACK);
|
||||
|
||||
@@ -306,7 +306,15 @@ fn all_images(d: &Document) -> Vec<&ImageBlob> {
|
||||
fn image_doc(blob: ImageBlob, w: u32, h: u32) -> Document {
|
||||
let mut d = Document::new_with_mode(100.0, 100.0, ColorMode::Cmyk);
|
||||
d.images.insert("img".into(), blob);
|
||||
let image = ImageObject { key: "img".into(), width: w, height: h, xf: Affine::scale(40.0 / w as f64), link: None, placement: Default::default() };
|
||||
let image = ImageObject {
|
||||
key: "img".into(),
|
||||
width: w,
|
||||
height: h,
|
||||
xf: Affine::scale(40.0 / w as f64),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image)));
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
@@ -383,6 +391,7 @@ fn export_masked(mask: Vec<u8>, clip: bool, invert: bool) -> (Vec<u8>, Vec<Optio
|
||||
xf: Affine::scale(20.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
}))
|
||||
};
|
||||
let mut mask = vectorcraft_doc::OpacityMask::new(Node::new(d.alloc_id(), image("alpha")), clip);
|
||||
|
||||
@@ -30,7 +30,15 @@ fn doc(bytes: Vec<u8>, mime: &str) -> Document {
|
||||
let xf = Affine::scale(72.0 / 600.0);
|
||||
let n = Node::new(
|
||||
d.alloc_id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key: "img".into(), width: 600, height: 600, xf, link: None, placement: Default::default() })),
|
||||
NodeKind::Image(Box::new(ImageObject {
|
||||
key: "img".into(),
|
||||
width: 600,
|
||||
height: 600,
|
||||
xf,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
|
||||
@@ -197,7 +197,8 @@ fn png(rgb: [u8; 3]) -> Vec<u8> {
|
||||
fn image_doc(bytes: Vec<u8>) -> Document {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.images.insert("img".into(), ImageBlob::new("image/png", bytes));
|
||||
let image = ImageObject { key: "img".into(), width: 4, height: 4, xf: Affine::scale(10.0), link: None, placement: Default::default() };
|
||||
let image =
|
||||
ImageObject { key: "img".into(), width: 4, height: 4, xf: Affine::scale(10.0), link: None, placement: Default::default(), pixels: None };
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(image)));
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
|
||||
@@ -125,6 +125,7 @@ fn pdfx_1a_files_have_no_rgb() {
|
||||
xf: Affine::translate((150.0, 50.0)) * Affine::scale(5.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
|
||||
@@ -51,7 +51,7 @@ fn doc(n: usize, image: bool, on_first: bool) -> Document {
|
||||
.unwrap();
|
||||
if image && (i > 0 || on_first) {
|
||||
let xf = Affine::translate((x + 60.0, 60.0)) * Affine::scale(4.0);
|
||||
let img = ImageObject { key: "img".into(), width: 8, height: 8, xf, link: None, placement: Default::default() };
|
||||
let img = ImageObject { key: "img".into(), width: 8, height: 8, xf, link: None, placement: Default::default(), pixels: None };
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(img)))).unwrap();
|
||||
}
|
||||
|
||||
@@ -264,6 +264,7 @@ fn art_bounds_take_in_the_effects_of_groups_type_and_images() {
|
||||
xf: vectorcraft_geom::Affine::translate((20.0, 50.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
let cut_off: Vec<_> = [("group", group), ("type", text), ("image", image)]
|
||||
|
||||
+18
-4
@@ -2964,8 +2964,15 @@ mod tests {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
// A PNG signature with nothing after it.
|
||||
d.images.insert("cut".into(), ImageBlob::new("image/png", b"\x89PNG\r\n\x1a\n".to_vec()));
|
||||
let im =
|
||||
ImageObject { key: "cut".into(), width: 40, height: 40, xf: Affine::translate((30.0, 30.0)), link: None, placement: Default::default() };
|
||||
let im = ImageObject {
|
||||
key: "cut".into(),
|
||||
width: 40,
|
||||
height: 40,
|
||||
xf: Affine::translate((30.0, 30.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
n.appearance.effects = vec![Effect { id: "blur.gaussian".into(), params: json!({"radius": 4.0}), visible: true }];
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
@@ -2989,8 +2996,15 @@ mod tests {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
// A PNG signature with nothing after it.
|
||||
d.images.insert("cut".into(), ImageBlob::new("image/png", b"\x89PNG\r\n\x1a\n".to_vec()));
|
||||
let im =
|
||||
ImageObject { key: "cut".into(), width: 40, height: 40, xf: Affine::translate((30.0, 30.0)), link: None, placement: Default::default() };
|
||||
let im = ImageObject {
|
||||
key: "cut".into(),
|
||||
width: 40,
|
||||
height: 40,
|
||||
xf: Affine::translate((30.0, 30.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
n.appearance.effects = [
|
||||
("rasterize", json!({"background": "white"})),
|
||||
|
||||
@@ -410,7 +410,8 @@ mod tests {
|
||||
let mut d = Document::new_with_mode(20.0, 10.0, ColorMode::Cmyk);
|
||||
d.images.insert("img".into(), ImageBlob::cmyk_tiff(&Inks::new(2, 2, ink.repeat(4)).unwrap()).unwrap());
|
||||
let l = d.layers[0].id;
|
||||
let im = ImageObject { key: "img".into(), width: 2, height: 2, xf: Affine::scale(5.0), link: None, placement: Default::default() };
|
||||
let im =
|
||||
ImageObject { key: "img".into(), width: 2, height: 2, xf: Affine::scale(5.0), link: None, placement: Default::default(), pixels: None };
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
// The same inks as a fill beside it.
|
||||
|
||||
+25
-18
@@ -2009,11 +2009,11 @@ impl Renderer {
|
||||
let unit = f.view * im.xf * Affine::scale_non_uniform(im.width as f64, im.height as f64);
|
||||
let shown = if outline && !f.doc.setup.outline_images {
|
||||
None
|
||||
} else if let Some(pm) = mips.then(|| self.kept_mip(f, &im.key, outline, unit)).flatten() {
|
||||
} else if let Some(pm) = mips.then(|| self.kept_mip(f, &im.key, im.pixels.as_deref(), outline, unit)).flatten() {
|
||||
Some(pm)
|
||||
} else {
|
||||
self.image_pixmap(f.doc, &im.key, outline, !mips).map(|(key, pm)| {
|
||||
let (key, pm) = if outline { (key, pm) } else { ink::ink_image(&key, &pm, f.ink, f.doc.images.get(&im.key), !mips) };
|
||||
self.image_pixmap(f.doc, &im.key, im.pixels.as_deref(), outline, !mips).map(|(key, pm)| {
|
||||
let (key, pm) = if outline { (key, pm) } else { ink::ink_image(&key, &pm, f.ink, f.doc.image_blob(im), !mips) };
|
||||
if mips { image_mip(&key, pm, unit) } else { pm }
|
||||
})
|
||||
};
|
||||
@@ -2075,15 +2075,22 @@ impl Renderer {
|
||||
self.adjusts.insert(derived, (key.to_string(), hash, map.clone()));
|
||||
}
|
||||
|
||||
/// The pixels of image `key` (a blob of `doc`, or a recoloured one named by
|
||||
/// [`Self::adjusted_image`]), or a greyscale copy of them, with their key in the shared store.
|
||||
/// `touch` marks them used there.
|
||||
fn image_pixmap(&mut self, doc: &Document, key: &str, grey: bool, touch: bool) -> Option<(images::ImageKey, Arc<Pixmap>)> {
|
||||
let (key, colour) = match doc.images.get(key) {
|
||||
/// The pixels of image `key` (`own`, the pixels an image holds itself, a blob of `doc`, or a
|
||||
/// recoloured one named by [`Self::adjusted_image`]), or a greyscale copy of them, with their
|
||||
/// key in the shared store. `touch` marks them used there.
|
||||
fn image_pixmap(
|
||||
&mut self,
|
||||
doc: &Document,
|
||||
key: &str,
|
||||
own: Option<&vectorcraft_doc::ImageBlob>,
|
||||
grey: bool,
|
||||
touch: bool,
|
||||
) -> Option<(images::ImageKey, Arc<Pixmap>)> {
|
||||
let (key, colour) = match own.or_else(|| doc.images.get(key)) {
|
||||
Some(blob) => blob_pixmap(blob, touch)?,
|
||||
None => {
|
||||
let (src, hash, map) = self.adjusts.get(key)?.clone();
|
||||
let (src_key, src) = self.image_pixmap(doc, &src, false, touch)?;
|
||||
let (src_key, src) = self.image_pixmap(doc, &src, None, false, touch)?;
|
||||
let key = images::ImageKey { adjust: Some(hash), ..src_key };
|
||||
match lookup(&key, touch) {
|
||||
Some(pm) => (key, pm),
|
||||
@@ -2122,28 +2129,28 @@ impl Renderer {
|
||||
Some((key, pm))
|
||||
}
|
||||
|
||||
/// The key [`Self::image_pixmap`] gives the pixels of image `key` and their size, without
|
||||
/// The key [`Self::image_pixmap`] gives the pixels of image `key` (or `own`) and their size, without
|
||||
/// making or looking at them: `None` until the image has been decoded.
|
||||
fn image_key(&self, doc: &Document, key: &str, grey: bool) -> Option<(images::ImageKey, (u16, u16))> {
|
||||
let (key, size) = match doc.images.get(key) {
|
||||
fn image_key(&self, doc: &Document, key: &str, own: Option<&vectorcraft_doc::ImageBlob>, grey: bool) -> Option<(images::ImageKey, (u16, u16))> {
|
||||
let (key, size) = match own.or_else(|| doc.images.get(key)) {
|
||||
Some(blob) => {
|
||||
let (key, _, size) = blob_key(blob);
|
||||
(key, size?)
|
||||
}
|
||||
None => {
|
||||
let (src, hash, _) = self.adjusts.get(key)?;
|
||||
let (src_key, size) = self.image_key(doc, src, false)?;
|
||||
let (src_key, size) = self.image_key(doc, src, None, false)?;
|
||||
(images::ImageKey { adjust: Some(*hash), ..src_key }, size)
|
||||
}
|
||||
};
|
||||
Some((if grey { images::ImageKey { grey: true, ..key } } else { key }, size))
|
||||
}
|
||||
|
||||
/// The mip [`image_mip`] draws image `key` (in greyscale with `grey`, the outline view's) with
|
||||
/// through `m` when the store holds it, marked used there; `None` when it draws the full pixels
|
||||
/// or the mip is yet to be made.
|
||||
fn kept_mip(&self, f: &Frame, key: &str, grey: bool, m: Affine) -> Option<Arc<Pixmap>> {
|
||||
let (key, (w, h)) = self.image_key(f.doc, key, grey)?;
|
||||
/// The mip [`image_mip`] draws image `key` (or `own`) with through `m`, in greyscale with
|
||||
/// `grey` (the outline view's), when the store holds it, marked used there; `None` when it
|
||||
/// draws the full pixels or the mip is yet to be made.
|
||||
fn kept_mip(&self, f: &Frame, key: &str, own: Option<&vectorcraft_doc::ImageBlob>, grey: bool, m: Affine) -> Option<Arc<Pixmap>> {
|
||||
let (key, (w, h)) = self.image_key(f.doc, key, own, grey)?;
|
||||
let key = if grey { key } else { ink::ink_key(&key, f.ink) };
|
||||
let level = mip_level(m, w, h);
|
||||
if level == 0 {
|
||||
|
||||
@@ -621,7 +621,8 @@ mod tests {
|
||||
let mut d = doc_with_pattern(TileType::Grid);
|
||||
// A PNG signature with nothing after it.
|
||||
d.images.insert("cut".into(), ImageBlob::new("image/png", b"\x89PNG\r\n\x1a\n".to_vec()));
|
||||
let im = ImageObject { key: "cut".into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im =
|
||||
ImageObject { key: "cut".into(), width: 10, height: 10, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.patterns[0].art = vec![Arc::new(Node::new(NodeId(901), NodeKind::Image(Box::new(im))))];
|
||||
let mut r = Renderer::new();
|
||||
let start = tiles_rasterized();
|
||||
|
||||
@@ -833,7 +833,7 @@ fn images_take_their_nearest_pixel_without_smoothing() {
|
||||
let mut d = Document::new(40.0, 20.0);
|
||||
d.images.insert("bw".into(), ImageBlob::new("image/png", png));
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: "bw".into(), width: 2, height: 1, xf: Affine::scale(20.0), link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "bw".into(), width: 2, height: 1, xf: Affine::scale(20.0), link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(d.layers[0].id), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
// How many pixels across the image are neither black nor white.
|
||||
let greys = |smooth_images| {
|
||||
@@ -905,7 +905,8 @@ fn a_gray_image_draws_its_gray_levels() {
|
||||
let mut d = Document::new(40.0, 20.0);
|
||||
d.images.insert("gray".into(), ImageBlob::new("image/png", png));
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: "gray".into(), width: 2, height: 1, xf: Affine::scale(20.0), link: None, placement: Default::default() };
|
||||
let im =
|
||||
ImageObject { key: "gray".into(), width: 2, height: 1, xf: Affine::scale(20.0), link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(d.layers[0].id), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let r = Renderer::new().render(&d, 40, 20, Affine::IDENTITY, &RenderOptions { smooth_images: false, ..Default::default() });
|
||||
assert_eq!((r.pixel(5, 10), r.pixel(35, 10)), ([40, 40, 40, 255], [200, 200, 200, 255]));
|
||||
|
||||
@@ -93,6 +93,7 @@ fn an_embedded_image_is_recoloured_and_the_original_pixels_stay() {
|
||||
xf: Affine::translate((20.0, 20.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
n.appearance.effects.push(hue());
|
||||
|
||||
@@ -45,3 +45,40 @@ fn outline_mode_draws_a_bitmap_glyph_as_its_box() {
|
||||
// The box's left edge at x 32.5 (the bearing of 2 px of 6.25 pt), from y 45 to 145.
|
||||
assert!((31..=34).any(|x| img.pixel(x, 100)[0] < 128), "the box's edge shows");
|
||||
}
|
||||
|
||||
/// 100 pt [`COLOUR_CHAR`] in the `CBDT` test font with `effects`, on a white 160×160 page.
|
||||
fn render_with(effects: Vec<vectorcraft_doc::Effect>) -> Rendered {
|
||||
let _adding = crate::tests_fontchange::adding_fonts();
|
||||
vectorcraft_text::FontDb::global().add_font(colour_bitmap_font(false).unwrap());
|
||||
let mut d = Document::new(160.0, 160.0);
|
||||
let st = CharStyle { font_family: colour_bitmap_family(false).into(), size: 100.0, ..Default::default() };
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 120.0), &COLOUR_CHAR.to_string(), st))));
|
||||
let mut appearance = (*n.appearance).clone();
|
||||
appearance.effects = effects;
|
||||
n.appearance = appearance.into();
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
Renderer::new().render(&d, 160, 160, Affine::IDENTITY, &RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() })
|
||||
}
|
||||
|
||||
fn effect(id: &str, params: serde_json::Value) -> vectorcraft_doc::Effect {
|
||||
vectorcraft_doc::Effect { id: id.into(), params, visible: true }
|
||||
}
|
||||
|
||||
/// Type with a geometry effect is drawn as its outlines: its emoji go with them, as images that
|
||||
/// hold their own pixels (here moved 40 pt by a Transform effect).
|
||||
#[test]
|
||||
fn bitmap_glyphs_go_with_type_that_a_geometry_effect_reshapes() {
|
||||
let img = render_with(vec![effect("distort.transform", serde_json::json!({"moveH": 40}))]);
|
||||
assert_eq!(img.pixel(110, 80), COLOUR_LARGE, "moved with the type");
|
||||
assert_eq!(img.pixel(50, 80), [255, 255, 255, 255], "not where it was");
|
||||
}
|
||||
|
||||
/// A colour adjustment leaves the emoji of reshaped type in their colours, as it does those of
|
||||
/// type, and as exports and Expand Appearance draw them.
|
||||
#[test]
|
||||
fn a_colour_adjustment_leaves_the_emoji_of_reshaped_type_as_they_are() {
|
||||
let moved = effect("distort.transform", serde_json::json!({"moveH": 40}));
|
||||
let img = render_with(vec![moved, effect("adjust.hueSaturation", serde_json::json!({"hue": 120}))]);
|
||||
assert_eq!(img.pixel(110, 80), COLOUR_LARGE);
|
||||
}
|
||||
|
||||
@@ -222,6 +222,7 @@ fn placed_images_are_separated_not_blackened() {
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
add(&mut d, n);
|
||||
@@ -256,7 +257,7 @@ fn rich_black_shows_black_ink_alone_as_rgb_black() {
|
||||
let mut png = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
d.images.insert("red".into(), ImageBlob::new("image/png", png));
|
||||
let im = ImageObject { key: "red".into(), width: 20, height: 20, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "red".into(), width: 20, height: 20, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
add(&mut d, n);
|
||||
assert_eq!(centre_with(&d, true), centre_with(&d, false), "an image shows as it is");
|
||||
|
||||
@@ -158,7 +158,15 @@ fn group_geometry_effects_move_and_copy_images_too() {
|
||||
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();
|
||||
d.images.insert("red".into(), ImageBlob::new("image/png", png));
|
||||
let b = rect(&mut d, Rect::new(0.0, 0.0, 20.0, 20.0), solid(0.0, 0.0, 1.0));
|
||||
let im = ImageObject { key: "red".into(), width: 30, height: 30, xf: Affine::translate((10.0, 30.0)), link: None, placement: Default::default() };
|
||||
let im = ImageObject {
|
||||
key: "red".into(),
|
||||
width: 30,
|
||||
height: 30,
|
||||
xf: Affine::translate((10.0, 30.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let image = Arc::new(Node::new(d.alloc_id(), NodeKind::Image(Box::new(im))));
|
||||
let mut g = Node::group(d.alloc_id(), vec![b, image]);
|
||||
g.appearance.effects.push(fx("distort.transform", json!({"moveH": 50, "copies": 1})));
|
||||
|
||||
@@ -27,6 +27,7 @@ fn doc(layer: impl FnOnce(&mut NodeKind)) -> Document {
|
||||
xf: Affine::translate((50.0, 10.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
im.id = d.alloc_id();
|
||||
|
||||
@@ -26,7 +26,7 @@ fn doc_with_image(key: &str, bytes: Vec<u8>, w: u32, h: u32) -> Document {
|
||||
let mut d = Document::new(w as f64, h as f64);
|
||||
d.images.insert(key.into(), ImageBlob::new("image/png", bytes));
|
||||
let (id, layer) = (d.alloc_id(), d.layers[0].id);
|
||||
let im = ImageObject { key: key.into(), width: w, height: h, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: key.into(), width: w, height: h, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
d
|
||||
}
|
||||
@@ -39,16 +39,16 @@ fn renderers_share_one_decode_of_an_image() {
|
||||
// Another document holding the same image under another key (its own copy of the bytes).
|
||||
let b = doc_with_image("photo", bytes, 16, 16);
|
||||
let (mut r1, mut r2) = (Renderer::new(), Renderer::new());
|
||||
let (k1, p1) = r1.image_pixmap(&a, "img", false, true).unwrap();
|
||||
let (k2, p2) = r2.image_pixmap(&b, "photo", false, true).unwrap();
|
||||
let (k1, p1) = r1.image_pixmap(&a, "img", None, false, true).unwrap();
|
||||
let (k2, p2) = r2.image_pixmap(&b, "photo", None, false, true).unwrap();
|
||||
assert_eq!(k1, k2);
|
||||
assert!(Arc::ptr_eq(&p1, &p2), "decoded once for both renderers");
|
||||
// Drawing goes through the store too: the pixels stay the ones decoded.
|
||||
let _ = r2.render(&b, 16, 16, Affine::IDENTITY, &RenderOptions::default());
|
||||
assert!(Arc::ptr_eq(&r1.image_pixmap(&b, "photo", false, true).unwrap().1, &p1));
|
||||
assert!(Arc::ptr_eq(&r1.image_pixmap(&b, "photo", None, false, true).unwrap().1, &p1));
|
||||
// A document using the same key for other pixels gets its own.
|
||||
let c = doc_with_image("img", png(16, 16, 102), 16, 16);
|
||||
let (k3, p3) = r1.image_pixmap(&c, "img", false, true).unwrap();
|
||||
let (k3, p3) = r1.image_pixmap(&c, "img", None, false, true).unwrap();
|
||||
assert!(k3 != k1 && !Arc::ptr_eq(&p3, &p1));
|
||||
assert!(
|
||||
r1.render(&c, 16, 16, Affine::IDENTITY, &RenderOptions::default()).pixels
|
||||
@@ -102,7 +102,7 @@ fn image_mips_are_opt_in_and_change_nothing_off() {
|
||||
// On, the quarter-size image samples the quarter-size mip: close to the full pixels' average
|
||||
// but not the same pixels.
|
||||
assert!(mipped.pixels != cold.pixels);
|
||||
let (key, _) = warm.image_pixmap(&d, "img", false, true).unwrap();
|
||||
let (key, _) = warm.image_pixmap(&d, "img", None, false, true).unwrap();
|
||||
let mip = images::store().peek(&images::ImageKey { mip: 2, ..key }).expect("quarter mip kept");
|
||||
assert_eq!((mip.width(), mip.height()), (16, 16));
|
||||
// Above half size, mips on draw the full pixels.
|
||||
@@ -122,7 +122,7 @@ fn kept_mips_draw_without_decoding_the_full_pixels_again() {
|
||||
let quarter = Affine::scale(0.25);
|
||||
let mut r = Renderer::new();
|
||||
let _ = r.render(&d, 32, 16, quarter, &on);
|
||||
let full = r.image_pixmap(&d, "img", false, false).unwrap().0;
|
||||
let full = r.image_pixmap(&d, "img", None, false, false).unwrap().0;
|
||||
images::store().remove(&full);
|
||||
// The image dragged 4 pt (a device pixel) right.
|
||||
let mut moved = d.clone();
|
||||
@@ -475,7 +475,15 @@ fn art_placed_again_in_every_frame_keeps_one_outline_per_stroke() {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
d.images.insert("dot".into(), ImageBlob::new("image/png", png(4, 4, 108)));
|
||||
let mut art = symbol_art(&mut d);
|
||||
let im = ImageObject { key: "dot".into(), width: 4, height: 4, xf: Affine::translate((-2.0, -2.0)), link: None, placement: Default::default() };
|
||||
let im = ImageObject {
|
||||
key: "dot".into(),
|
||||
width: 4,
|
||||
height: 4,
|
||||
xf: Affine::translate((-2.0, -2.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
if let Some(c) = art.children_mut() {
|
||||
c.push(Arc::new(image));
|
||||
@@ -1476,7 +1484,7 @@ fn blurred_image_doc(key: &str, seed: u8) -> Document {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
d.images.insert(key.into(), ImageBlob::new("image/png", png(16, 16, seed)));
|
||||
let xf = Affine::translate((20.0, 10.0)) * Affine::scale(5.0);
|
||||
let im = ImageObject { key: key.into(), width: 16, height: 16, xf, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: key.into(), width: 16, height: 16, xf, link: None, placement: Default::default(), pixels: None };
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
n.appearance.effects.push(effect("blur.gaussian", serde_json::json!({"radius": 1.0})));
|
||||
with_art(d, vec![n])
|
||||
|
||||
@@ -285,6 +285,7 @@ fn image_doc(effects: Vec<Effect>) -> Document {
|
||||
xf: Affine::translate((20.0, 20.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
n.appearance.effects = effects;
|
||||
|
||||
@@ -10,7 +10,7 @@ fn doc_with_red_image() -> Document {
|
||||
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();
|
||||
d.images.insert("red".into(), ImageBlob::new("image/png", png));
|
||||
let (id, layer) = (d.alloc_id(), d.layers[0].id);
|
||||
let im = ImageObject { key: "red".into(), width: 40, height: 40, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: "red".into(), width: 40, height: 40, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
@@ -546,7 +546,7 @@ impl Writer<'_> {
|
||||
/// missing), else its bytes. `None` when it has neither.
|
||||
fn image_href(&mut self, im: &vectorcraft_doc::ImageObject) -> Option<String> {
|
||||
let (doc, link) = (self.doc, self.opts.images == ImageMode::Link);
|
||||
match (im.link.as_ref().filter(|_| link), doc.images.get(&im.key)) {
|
||||
match (im.link.as_ref().filter(|_| link), doc.image_blob(im)) {
|
||||
(Some(l), _) => Some(l.path.clone()),
|
||||
(None, Some(b)) if !b.bytes.is_empty() => Some(self.blob_href(&web_image(b))),
|
||||
_ => im.link.as_ref().map(|l| l.path.clone()),
|
||||
@@ -556,17 +556,16 @@ impl Writer<'_> {
|
||||
/// Images drawn more than once in `art` (the pieces of a distorted image) go into the defs
|
||||
/// once; each piece then draws them with `<use>`.
|
||||
fn share_images(&mut self, art: &Node) {
|
||||
let mut seen: HashMap<(String, u32, u32), usize> = HashMap::new();
|
||||
// Each image's count and its first piece, which shows what they all show.
|
||||
let mut seen: HashMap<(String, u32, u32), (usize, vectorcraft_doc::ImageObject)> = HashMap::new();
|
||||
art.walk(&mut |c| {
|
||||
if let NodeKind::Image(im) = &c.kind {
|
||||
*seen.entry((im.key.clone(), im.width, im.height)).or_default() += 1;
|
||||
seen.entry((im.key.clone(), im.width, im.height)).or_insert_with(|| (0, (**im).clone())).0 += 1;
|
||||
}
|
||||
});
|
||||
let mut repeated: Vec<_> = seen.into_iter().filter(|(k, n)| *n > 1 && !self.shared_images.contains_key(k)).map(|(k, _)| k).collect();
|
||||
repeated.sort();
|
||||
for (key, width, height) in repeated {
|
||||
let link = self.linked_of(art, &key);
|
||||
let im = vectorcraft_doc::ImageObject { key: key.clone(), width, height, xf: Affine::IDENTITY, link, placement: Default::default() };
|
||||
let mut repeated: Vec<_> = seen.into_iter().filter(|(k, (n, _))| *n > 1 && !self.shared_images.contains_key(k)).collect();
|
||||
repeated.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
for ((key, width, height), (_, im)) in repeated {
|
||||
let Some(href) = self.image_href(&im) else { continue };
|
||||
let id = self.fresh_id("image");
|
||||
self.def(
|
||||
@@ -580,20 +579,6 @@ impl Writer<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The link of the first image with blob `key` in `art`.
|
||||
fn linked_of(&self, art: &Node, key: &str) -> Option<vectorcraft_doc::LinkInfo> {
|
||||
let mut link = None;
|
||||
art.walk(&mut |c| {
|
||||
if let NodeKind::Image(im) = &c.kind
|
||||
&& im.key == key
|
||||
&& link.is_none()
|
||||
{
|
||||
link = im.link.clone();
|
||||
}
|
||||
});
|
||||
link
|
||||
}
|
||||
|
||||
fn fresh_id(&mut self, prefix: &str) -> String {
|
||||
let mut i = 1;
|
||||
loop {
|
||||
|
||||
@@ -1564,7 +1564,10 @@ impl Importer {
|
||||
if link.is_some() && !missing { blob.with_proxy() } else { blob }
|
||||
});
|
||||
let xf = acc * Affine::scale_non_uniform(size.width() as f64 / pw as f64, size.height() as f64 / ph as f64);
|
||||
Some(self.named(i.id(), NodeKind::Image(Box::new(ImageObject { key, width: pw, height: ph, xf, link, placement: Default::default() }))))
|
||||
Some(self.named(
|
||||
i.id(),
|
||||
NodeKind::Image(Box::new(ImageObject { key, width: pw, height: ph, xf, link, placement: Default::default(), pixels: None })),
|
||||
))
|
||||
}
|
||||
|
||||
/// An SVG shown as an image (a data URL or a linked file) → its art, `acc` mapping its size.
|
||||
|
||||
@@ -70,7 +70,15 @@ fn group_effects_bake_into_the_members_and_filter_the_group() {
|
||||
fn a_group_transform_copies_its_images() {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
d.images.insert("red".into(), ImageBlob::new("image/png", vec![1, 2, 3]));
|
||||
let im = ImageObject { key: "red".into(), width: 20, height: 20, xf: Affine::translate((20.0, 20.0)), link: None, placement: Default::default() };
|
||||
let im = ImageObject {
|
||||
key: "red".into(),
|
||||
width: 20,
|
||||
height: 20,
|
||||
xf: Affine::translate((20.0, 20.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let image = Arc::new(Node::new(d.alloc_id(), NodeKind::Image(Box::new(im))));
|
||||
let black = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
|
||||
let square = Arc::new(Node::path(d.alloc_id(), shapes::rectangle(Rect::new(20.0, 60.0, 30.0, 70.0)), black));
|
||||
|
||||
@@ -82,7 +82,7 @@ fn image_doc(key: &str, bytes: Vec<u8>) -> Document {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.images.insert(key.into(), ImageBlob::new("image/png", bytes));
|
||||
let l = d.layers[0].id;
|
||||
let im = ImageObject { key: key.into(), width: 2, height: 2, xf: Affine::IDENTITY, link: None, placement: Default::default() };
|
||||
let im = ImageObject { key: key.into(), width: 2, height: 2, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
d.insert(Some(l), usize::MAX, n).unwrap();
|
||||
d
|
||||
|
||||
@@ -347,6 +347,7 @@ fn image_roundtrip() {
|
||||
xf: Affine::translate((10.0, 20.0)) * Affine::scale(10.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
let mut d = doc_with(vec![n]);
|
||||
|
||||
@@ -13,7 +13,8 @@ pub fn doc_json(doc: &Document) -> Value {
|
||||
|
||||
/// Tree invariants: ids unique, `node_count` equals the number of walked nodes, `next_id` above
|
||||
/// every id, top level made of layers, layers only at the top or inside layers, clip groups have
|
||||
/// a first child, compound children are paths.
|
||||
/// a first child, compound children are paths, no image holds its own pixels (the document keeps
|
||||
/// them).
|
||||
pub fn check_document(doc: &Document) -> Result<(), String> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut walked = 0usize;
|
||||
@@ -32,6 +33,9 @@ pub fn check_document(doc: &Document) -> Result<(), String> {
|
||||
NodeKind::Compound { children, .. } if children.iter().any(|c| !matches!(c.kind, NodeKind::Path { .. })) && err.is_none() => {
|
||||
err = Some(format!("compound {} has a non-path child", n.id));
|
||||
}
|
||||
NodeKind::Image(im) if im.pixels.is_some() && err.is_none() => {
|
||||
err = Some(format!("image {} holds its own pixels", n.id));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if !(n.opacity.is_finite() && (0.0..=1.0).contains(&n.opacity)) && err.is_none() {
|
||||
|
||||
@@ -138,6 +138,21 @@ impl PositionedGlyph {
|
||||
pub fn placed_image(&self) -> Option<(&Arc<GlyphImage>, Affine)> {
|
||||
self.image.as_ref().map(|i| (i, self.xf * i.xf))
|
||||
}
|
||||
/// The glyph's colour bitmap as an image object placed through `to_doc` (text space → where it
|
||||
/// goes), holding its pixels ([`vectorcraft_doc::ImageObject::pixels`]), without an id.
|
||||
pub fn placed_image_node(&self, to_doc: Affine) -> Option<vectorcraft_doc::Node> {
|
||||
let (image, to_text) = self.placed_image()?;
|
||||
let im = vectorcraft_doc::ImageObject {
|
||||
key: image.blob.content_key(),
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
xf: to_doc * to_text,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: Some(Arc::new(image.blob.clone())),
|
||||
};
|
||||
Some(vectorcraft_doc::Node::new(vectorcraft_doc::NodeId(u64::MAX), vectorcraft_doc::NodeKind::Image(Box::new(im))))
|
||||
}
|
||||
/// The glyph's colour art placed through `to_doc` (text space → where they go): a copy of
|
||||
/// [`Self::colour`], transformed, its nodes without ids.
|
||||
pub fn placed_colour(&self, to_doc: Affine) -> Option<vectorcraft_doc::Node> {
|
||||
|
||||
@@ -285,6 +285,7 @@ mod tests {
|
||||
xf: Affine::translate(at) * Affine::scale(2.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let mut s = Selection::default();
|
||||
|
||||
@@ -317,6 +317,7 @@ mod tests {
|
||||
xf: vectorcraft_geom::Affine::scale(10.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
@@ -353,6 +354,7 @@ mod tests {
|
||||
xf: vectorcraft_geom::Affine::scale(10.0),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
|
||||
@@ -9981,6 +9981,7 @@ mod tests {
|
||||
xf: Affine::IDENTITY,
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
})),
|
||||
);
|
||||
source.visible = false;
|
||||
|
||||
+4
-3
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user