mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
Crop Image: trim an image to its pixels that aren't fully transparent
object.cropImage {trim: true} cuts the selected image to the box of its pixels
with any alpha, copying them as they are (no re-render, no resampling) and
placing them where they were. An image with nothing to cut is left as it is
and isn't an error (trimmed: false), so a script can trim every image it
opens. Print-ready PNGs usually carry transparent padding; with this, Fit to
Artwork Bounds can size to the art.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fc74389b5a
commit
f767403673
@@ -111,7 +111,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Crop Image",
|
||||
["Object"],
|
||||
None,
|
||||
"{rect?: [x, y, width, height]} crop the selected image (default: to the artboard it sits on) → {id, width, height}",
|
||||
"{rect?: [x, y, width, height] | trim?: true (to the pixels that aren't fully transparent, pixel for pixel; an image with none to cut is left as it is, trimmed: false)} crop the selected image (default: to the artboard it sits on) → {id, width, height, trimmed? (with trim)}",
|
||||
has_image,
|
||||
crop_image
|
||||
),
|
||||
@@ -541,6 +541,9 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.ok_or_else(|| bad(C, "select an image"))?;
|
||||
let node = st.doc.node(id).cloned().ok_or(EngineError::NoNode(id))?;
|
||||
let NodeKind::Image(im) = &node.kind else { return Err(bad(C, "select an image")) };
|
||||
if p.get("trim").and_then(Value::as_bool) == Some(true) {
|
||||
return trim_image(s, id, im.clone());
|
||||
}
|
||||
let ib = node.geometric_bounds().ok_or_else(|| bad(C, "image has no bounds"))?;
|
||||
let rect = match p.get("rect").and_then(Value::as_array) {
|
||||
Some(a) if a.len() == 4 => {
|
||||
@@ -594,6 +597,49 @@ fn crop_image(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "id": id.0, "width": w, "height": h }))
|
||||
}
|
||||
|
||||
/// Crop Image's trim: the image cut to the box of its pixels that aren't fully transparent, the
|
||||
/// pixels copied as they are (no resampling) and placed where they were.
|
||||
fn trim_image(s: &mut Session, id: NodeId, im: ImageObject) -> Result<Value> {
|
||||
const C: &str = "object.cropImage";
|
||||
let st = s.doc()?;
|
||||
let blob = st.doc.images.get(&im.key).ok_or_else(|| bad(C, "the image's pixels are missing"))?;
|
||||
let px = image::load_from_memory(&blob.bytes).map_err(|_| bad(C, "the image's pixels can't be read"))?.to_rgba8();
|
||||
let (w, h) = px.dimensions();
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0, 0);
|
||||
for (x, y, p) in px.enumerate_pixels() {
|
||||
if p[3] > 0 {
|
||||
(x0, y0, x1, y1) = (x0.min(x), y0.min(y), x1.max(x + 1), y1.max(y + 1));
|
||||
}
|
||||
}
|
||||
if x1 <= x0 || y1 <= y0 {
|
||||
return Err(bad(C, "the image is fully transparent"));
|
||||
}
|
||||
if (x0, y0, x1, y1) == (0, 0, w, h) {
|
||||
// Already trimmed: not an error, so a script can trim every image it opens.
|
||||
return Ok(json!({ "id": id.0, "width": w, "height": h, "trimmed": false }));
|
||||
}
|
||||
let cut = image::imageops::crop_imm(&px, x0, y0, x1 - x0, y1 - y0).to_image();
|
||||
let mut png = Vec::new();
|
||||
cut.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).map_err(|e| bad(C, e.to_string()))?;
|
||||
let (cw, ch) = (x1 - x0, y1 - y0);
|
||||
s.edit("Crop Image", |d, _| {
|
||||
let key = unique_key(d, "crop");
|
||||
d.images.insert(key.clone(), vectorcraft_doc::ImageBlob::new("image/png", png));
|
||||
if let Some(n) = d.node_mut(id) {
|
||||
n.kind = NodeKind::Image(ImageObject {
|
||||
key,
|
||||
width: cw,
|
||||
height: ch,
|
||||
xf: im.xf * Affine::translate((f64::from(x0), f64::from(y0))),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "id": id.0, "width": cw, "height": ch, "trimmed": true }))
|
||||
}
|
||||
|
||||
/// The Control bar's Mask for an image: a clip group of the image and a clipping path on its
|
||||
/// outline, with that path selected so dragging its handles crops the image.
|
||||
fn mask_image(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
|
||||
Reference in New Issue
Block a user