mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 12:48:12 +08:00
Add embedable images (#564)
* Add embedable bitmaps * Initial work on blob urls * Finish implementing data url * Fix some bugs * Rename bitmap to image * Fix loading image on document load * Add transform properties for image * Remove some logging * Add image dimensions * Implement system copy and paste * Fix pasting images * Fix test * Address code review Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
0ee492a857
commit
51c31f042b
+26
-10
@@ -2,6 +2,7 @@ use crate::boolean_ops::boolean_operation;
|
||||
use crate::intersection::Quad;
|
||||
use crate::layers;
|
||||
use crate::layers::folder_layer::FolderLayer;
|
||||
use crate::layers::image_layer::ImageLayer;
|
||||
use crate::layers::layer_info::{Layer, LayerData, LayerDataType};
|
||||
use crate::layers::shape_layer::ShapeLayer;
|
||||
use crate::layers::style::ViewMode;
|
||||
@@ -268,23 +269,17 @@ impl Document {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Visit each layer recursively, applies modify_shape to each non-overlay Shape
|
||||
pub fn visit_all_shapes<F: FnMut(&mut ShapeLayer)>(layer: &mut Layer, modify_shape: &mut F) -> bool {
|
||||
/// Visit each layer recursively, marks all children as dirty
|
||||
pub fn mark_children_as_dirty(layer: &mut Layer) -> bool {
|
||||
match layer.data {
|
||||
LayerDataType::Shape(ref mut shape) => {
|
||||
modify_shape(shape);
|
||||
|
||||
// This layer should be updated on next render pass
|
||||
layer.cache_dirty = true;
|
||||
}
|
||||
LayerDataType::Folder(ref mut folder) => {
|
||||
for sub_layer in folder.layers_mut() {
|
||||
if Document::visit_all_shapes(sub_layer, modify_shape) {
|
||||
if Document::mark_children_as_dirty(sub_layer) {
|
||||
layer.cache_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
LayerDataType::Text(_) => layer.cache_dirty = true,
|
||||
_ => layer.cache_dirty = true,
|
||||
}
|
||||
layer.cache_dirty
|
||||
}
|
||||
@@ -502,6 +497,19 @@ impl Document {
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::AddImage {
|
||||
path,
|
||||
transform,
|
||||
insert_index,
|
||||
image_data,
|
||||
mime,
|
||||
} => {
|
||||
let layer = Layer::new(LayerDataType::Image(ImageLayer::new(mime, image_data)), transform);
|
||||
|
||||
self.set_layer(&path, layer, insert_index)?;
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetTextEditability { path, editable } => {
|
||||
self.layer_mut(&path)?.as_text_mut()?.editable = editable;
|
||||
self.mark_as_dirty(&path)?;
|
||||
@@ -690,6 +698,14 @@ impl Document {
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetImageBlobUrl { path, blob_url, dimensions } => {
|
||||
let image = self.layer_mut(&path).expect("Blob url for invalid layer").as_image_mut().unwrap();
|
||||
image.blob_url = Some(blob_url);
|
||||
image.dimensions = dimensions.into();
|
||||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
|
||||
Operation::SetLayerTransformInViewport { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
self.set_transform_relative_to_viewport(&path, transform)?;
|
||||
|
||||
@@ -10,6 +10,7 @@ pub enum DocumentError {
|
||||
NonReorderableSelection,
|
||||
NotAShape,
|
||||
NotText,
|
||||
NotAnImage,
|
||||
InvalidFile(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use super::layer_info::LayerData;
|
||||
use super::style::ViewMode;
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::LayerId;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Shape as KurboShape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ImageLayer {
|
||||
pub mime: String,
|
||||
pub image_data: Vec<u8>,
|
||||
#[serde(skip)]
|
||||
pub blob_url: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub dimensions: DVec2,
|
||||
}
|
||||
|
||||
impl LayerData for ImageLayer {
|
||||
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
|
||||
let transform = self.transform(transforms, view_mode);
|
||||
let inverse = transform.inverse();
|
||||
|
||||
if !inverse.is_finite() {
|
||||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = svg.write_str(r#")">"#);
|
||||
|
||||
let svg_transform = transform
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," })
|
||||
.collect::<String>();
|
||||
let _ = write!(
|
||||
svg,
|
||||
r#"<image width="{}" height="{}" transform="matrix({})" xlink:href="{}" />"#,
|
||||
self.dimensions.x,
|
||||
self.dimensions.y,
|
||||
svg_transform,
|
||||
self.blob_url.as_ref().unwrap_or(&String::new())
|
||||
);
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
|
||||
let mut path = self.bounds();
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
|
||||
if intersect_quad_bez_path(quad, &self.bounds(), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageLayer {
|
||||
pub fn new(mime: String, image_data: Vec<u8>) -> Self {
|
||||
let blob_url = None;
|
||||
let dimensions = DVec2::ONE;
|
||||
Self {
|
||||
mime,
|
||||
image_data,
|
||||
blob_url,
|
||||
dimensions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
|
||||
let start = match mode {
|
||||
ViewMode::Outline => 0,
|
||||
_ => (transforms.len() as i32 - 1).max(0) as usize,
|
||||
};
|
||||
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
|
||||
}
|
||||
|
||||
fn bounds(&self) -> BezPath {
|
||||
kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(self.dimensions.x, self.dimensions.y)).to_path(0.)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::blend_mode::BlendMode;
|
||||
use super::folder_layer::FolderLayer;
|
||||
use super::image_layer::ImageLayer;
|
||||
use super::shape_layer::ShapeLayer;
|
||||
use super::style::{PathStyle, ViewMode};
|
||||
use super::text_layer::TextLayer;
|
||||
@@ -16,6 +17,7 @@ pub enum LayerDataType {
|
||||
Folder(FolderLayer),
|
||||
Shape(ShapeLayer),
|
||||
Text(TextLayer),
|
||||
Image(ImageLayer),
|
||||
}
|
||||
|
||||
impl LayerDataType {
|
||||
@@ -24,6 +26,7 @@ impl LayerDataType {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
LayerDataType::Text(t) => t,
|
||||
LayerDataType::Image(i) => i,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +35,7 @@ impl LayerDataType {
|
||||
LayerDataType::Shape(s) => s,
|
||||
LayerDataType::Folder(f) => f,
|
||||
LayerDataType::Text(t) => t,
|
||||
LayerDataType::Image(i) => i,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,6 +197,13 @@ impl Layer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_image_mut(&mut self) -> Result<&mut ImageLayer, DocumentError> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Image(img) => Ok(img),
|
||||
_ => Err(DocumentError::NotAnImage),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Ok(&s.style),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod blend_mode;
|
||||
pub mod folder_layer;
|
||||
pub mod image_layer;
|
||||
pub mod layer_info;
|
||||
pub mod shape_layer;
|
||||
pub mod style;
|
||||
|
||||
@@ -53,6 +53,18 @@ pub enum Operation {
|
||||
style: style::PathStyle,
|
||||
size: f64,
|
||||
},
|
||||
AddImage {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
insert_index: isize,
|
||||
mime: String,
|
||||
image_data: Vec<u8>,
|
||||
},
|
||||
SetImageBlobUrl {
|
||||
path: Vec<LayerId>,
|
||||
blob_url: String,
|
||||
dimensions: (f64, f64),
|
||||
},
|
||||
SetTextEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
|
||||
Reference in New Issue
Block a user