Type: emoji go with type that effects reshape and Expand Appearance

Squashed from #3879 onto main.
This commit is contained in:
bflatastic
2026-10-24 04:49:06 +07:00
parent 94e1fa3ecc
commit c2ad9f8bbb
91 changed files with 519 additions and 168 deletions
+1 -1
View File
@@ -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())?;
}
+8 -7
View File
@@ -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
View File
@@ -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()
+1 -1
View File
@@ -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
View File
@@ -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.)
+1
View File
@@ -377,6 +377,7 @@ mod tests {
xf: Affine::IDENTITY,
link,
placement: Default::default(),
pixels: None,
})),
),
)
+7 -1
View File
@@ -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)];
+3 -2
View File
@@ -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) });
+3 -1
View File
@@ -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;
+1
View File
@@ -103,6 +103,7 @@ mod tests {
xf: Affine::IDENTITY,
link: None,
placement: Default::default(),
pixels: None,
})),
);
source.visible = false;
+8 -6
View File
@@ -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
+5
View File
@@ -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)]);
+9 -2
View File
@@ -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)?;
+6 -4
View File
@@ -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;
}
}
}
+9 -1
View File
@@ -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,
})))
}
}
+2 -1
View File
@@ -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());
+4 -2
View File
@@ -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();
+9 -1
View File
@@ -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)
+2
View File
@@ -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;
+3 -1
View File
@@ -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(())
+1
View File
@@ -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)))
}
+1 -1
View File
@@ -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));
+9 -1
View File
@@ -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)
+1
View File
@@ -92,6 +92,7 @@ mod tests {
xf: Affine::scale(72.0 / ppi),
link: None,
placement: Default::default(),
pixels: None,
})),
)
}
+5 -13
View File
@@ -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.
+1
View File
@@ -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]);
+9 -1
View File
@@ -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]);
+60
View File
@@ -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();
}
+1
View File
@@ -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]);
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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();
+1
View File
@@ -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| {
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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();
+9 -1
View File
@@ -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);
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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(())
+6 -3
View File
@@ -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 ----------------------------------------------------------------------
+2 -2
View File
@@ -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))
}
}
+1
View File
@@ -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();
+4 -2
View File
@@ -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()?;
+1
View File
@@ -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);
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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
+1
View File
@@ -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;
+27 -7
View File
@@ -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();
+1 -1
View File
@@ -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));
+1 -1
View File
@@ -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();
}
+1
View File
@@ -42,6 +42,7 @@ fn image_node(d: &mut Document, key: &str) -> Node {
xf: Affine::IDENTITY,
link: None,
placement: Default::default(),
pixels: None,
})),
)
}
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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();
+1
View File
@@ -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]);
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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);
+5 -4
View File
@@ -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()))
+4 -1
View File
@@ -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 {
+7 -6
View File
@@ -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);
+5 -3
View File
@@ -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);
}
+4 -1
View File
@@ -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);
+10 -1
View File
@@ -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);
+9 -1
View File
@@ -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();
+2 -1
View File
@@ -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();
+1
View File
@@ -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();
+1 -1
View File
@@ -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();
}
+1
View File
@@ -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
View File
@@ -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"})),
+2 -1
View File
@@ -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
View File
@@ -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 {
+2 -1
View File
@@ -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();
+3 -2
View File
@@ -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]));
+1
View File
@@ -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());
+37
View File
@@ -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);
}
+2 -1
View File
@@ -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");
+9 -1
View File
@@ -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})));
+1
View File
@@ -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();
+17 -9
View File
@@ -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])
+1
View File
@@ -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;
+1 -1
View File
@@ -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
}
+7 -22
View File
@@ -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 {
+4 -1
View File
@@ -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.
+9 -1
View File
@@ -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));
+1 -1
View File
@@ -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
+1
View File
@@ -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]);
+5 -1
View File
@@ -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() {
+15
View File
@@ -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> {
+1
View File
@@ -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();
+2
View File
@@ -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());
+1
View File
@@ -9981,6 +9981,7 @@ mod tests {
xf: Affine::IDENTITY,
link: None,
placement: Default::default(),
pixels: None,
})),
);
source.visible = false;
+4 -3
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long