Image and text bug fixes (#685)

* Image and text bugfixes

* Mark only the required layer types as dirty

* Fix doctest

* Disable selection if empty

* Cleanup naming

* Simplify cache deleting on export

* Minor css style change

* Nit

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2022-06-29 18:18:01 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent ee98908d88
commit 2191b09f9f
22 changed files with 243 additions and 128 deletions
+26 -5
View File
@@ -3,9 +3,9 @@ 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::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant};
use crate::layers::shape_layer::ShapeLayer;
use crate::layers::style::ViewMode;
use crate::layers::style::RenderData;
use crate::layers::text_layer::{Font, FontCache, TextLayer};
use crate::{DocumentError, DocumentResponse, Operation};
@@ -42,10 +42,10 @@ impl Default for Document {
impl Document {
/// Wrapper around render, that returns the whole document as a Response.
pub fn render_root(&mut self, mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) -> String {
pub fn render_root(&mut self, render_data: RenderData) -> String {
let mut svg_defs = String::from("<defs>");
self.root.render(&mut vec![], mode, &mut svg_defs, font_cache, culling_bounds);
self.root.render(&mut vec![], &mut svg_defs, render_data);
svg_defs.push_str("</defs>");
@@ -375,6 +375,27 @@ impl Document {
Ok(())
}
/// Marks all decendants of the specified [Layer] of a specific [LayerDataType] as dirty
fn mark_layers_of_type_as_dirty(root: &mut Layer, data_type: LayerDataTypeDiscriminant) -> bool {
if let LayerDataType::Folder(folder) = &mut root.data {
let mut dirty = false;
for layer in folder.layers_mut() {
dirty = Self::mark_layers_of_type_as_dirty(layer, data_type) || dirty;
}
root.cache_dirty = dirty;
}
if LayerDataTypeDiscriminant::from(&root.data) == data_type {
root.cache_dirty = true;
}
root.cache_dirty
}
/// Marks all layers in the [Document] of a specific [LayerDataType] as dirty
pub fn mark_all_layers_of_type_as_dirty(&mut self, data_type: LayerDataTypeDiscriminant) -> bool {
Self::mark_layers_of_type_as_dirty(&mut self.root, data_type)
}
pub fn transforms(&self, path: &[LayerId]) -> Result<Vec<DAffine2>, DocumentError> {
let mut root = &self.root;
let mut transforms = vec![self.root.transform];
@@ -697,7 +718,7 @@ impl Document {
text.font = Font::new(font_family, font_style);
text.size = size;
text.regenerate_path(text.load_face(font_cache));
text.cached_path = Some(text.generate_path(text.load_face(font_cache)));
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
+3 -3
View File
@@ -1,5 +1,5 @@
use super::layer_info::{Layer, LayerData, LayerDataType};
use super::style::ViewMode;
use super::style::RenderData;
use crate::intersection::Quad;
use crate::layers::text_layer::FontCache;
use crate::{DocumentError, LayerId};
@@ -22,9 +22,9 @@ pub struct FolderLayer {
}
impl LayerData for FolderLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: RenderData) {
for layer in &mut self.layers {
let _ = writeln!(svg, "{}", layer.render(transforms, view_mode, svg_defs, font_cache, culling_bounds));
let _ = writeln!(svg, "{}", layer.render(transforms, svg_defs, render_data));
}
}
+14 -8
View File
@@ -1,5 +1,5 @@
use super::layer_info::LayerData;
use super::style::ViewMode;
use super::style::{RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache;
use crate::LayerId;
@@ -9,6 +9,8 @@ use kurbo::{Affine, BezPath, Shape as KurboShape};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
mod base64_serde;
fn glam_to_kurbo(transform: DAffine2) -> Affine {
Affine::new(transform.to_cols_array())
}
@@ -16,6 +18,7 @@ fn glam_to_kurbo(transform: DAffine2) -> Affine {
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ImageLayer {
pub mime: String,
#[serde(serialize_with = "base64_serde::as_base64", deserialize_with = "base64_serde::from_base64")]
pub image_data: Vec<u8>,
#[serde(skip)]
pub blob_url: Option<String>,
@@ -24,8 +27,8 @@ pub struct ImageLayer {
}
impl LayerData for ImageLayer {
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, _font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) {
let transform = self.transform(transforms, view_mode);
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse();
if !inverse.is_finite() {
@@ -47,12 +50,15 @@ impl LayerData for ImageLayer {
.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())
r#"<image width="{}" height="{}" transform="matrix({})" href=""#,
self.dimensions.x, self.dimensions.y, svg_transform,
);
if render_data.embed_images {
let _ = write!(svg, "data:{};base64,{}", self.mime, base64::encode(&self.image_data));
} else {
let _ = write!(svg, "{}", self.blob_url.as_ref().unwrap_or(&String::new()));
}
let _ = svg.write_str(r#""/>"#);
let _ = svg.write_str("</g>");
}
@@ -0,0 +1,21 @@
//! Basic wrapper for [`serde`] for [`base64`] encoding
use serde::{Deserialize, Deserializer, Serializer};
pub fn as_base64<T, S>(key: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: AsRef<[u8]>,
S: Serializer,
{
serializer.serialize_str(&base64::encode(key.as_ref()))
}
pub fn from_base64<'a, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'a>,
{
use serde::de::Error;
String::deserialize(deserializer)
.and_then(|string| base64::decode(&string).map_err(|err| Error::custom(err.to_string())))
.map_err(serde::de::Error::custom)
}
+50 -10
View File
@@ -2,13 +2,14 @@ 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::style::{PathStyle, RenderData};
use super::text_layer::TextLayer;
use crate::intersection::Quad;
use crate::layers::text_layer::FontCache;
use crate::DocumentError;
use crate::LayerId;
use core::fmt;
use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
@@ -46,6 +47,40 @@ impl LayerDataType {
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum LayerDataTypeDiscriminant {
Folder,
Shape,
Text,
Image,
}
impl fmt::Display for LayerDataTypeDiscriminant {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
LayerDataTypeDiscriminant::Folder => "Folder",
LayerDataTypeDiscriminant::Shape => "Shape",
LayerDataTypeDiscriminant::Text => "Text",
LayerDataTypeDiscriminant::Image => "Image",
};
formatter.write_str(name)
}
}
impl From<&LayerDataType> for LayerDataTypeDiscriminant {
fn from(data: &LayerDataType) -> Self {
use LayerDataType::*;
match data {
Folder(_) => LayerDataTypeDiscriminant::Folder,
Shape(_) => LayerDataTypeDiscriminant::Shape,
Text(_) => LayerDataTypeDiscriminant::Text,
Image(_) => LayerDataTypeDiscriminant::Image,
}
}
}
/// Defines shared behavior for every layer type.
pub trait LayerData {
/// Render the layer as an SVG tag to a given string.
@@ -53,7 +88,7 @@ pub trait LayerData {
/// # Example
/// ```
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode};
/// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode, RenderData};
/// # use graphite_graphene::layers::layer_info::LayerData;
/// # use std::collections::HashMap;
///
@@ -61,7 +96,9 @@ pub trait LayerData {
/// let mut svg = String::new();
///
/// // Render the shape without any transforms, in normal view mode
/// shape.render(&mut svg, &mut String::new(), &mut vec![], ViewMode::Normal, &Default::default(), None);
/// # let font_cache = Default::default();
/// let render_data = RenderData::new(ViewMode::Normal, &font_cache, None, false);
/// shape.render(&mut svg, &mut String::new(), &mut vec![], render_data);
///
/// assert_eq!(
/// svg,
@@ -70,7 +107,7 @@ pub trait LayerData {
/// </g>"
/// );
/// ```
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>);
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: RenderData);
/// Determine the layers within this layer that intersect a given quad.
/// # Example
@@ -117,8 +154,8 @@ pub trait LayerData {
}
impl LayerData for LayerDataType {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache, viewport_bounds: Option<[DVec2; 2]>) {
self.inner_mut().render(svg, svg_defs, transforms, view_mode, font_cache, viewport_bounds)
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, render_data: RenderData) {
self.inner_mut().render(svg, svg_defs, transforms, render_data)
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
@@ -223,14 +260,17 @@ impl Layer {
LayerIter { stack: vec![self] }
}
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, svg_defs: &mut String, font_cache: &FontCache, culling_bounds: Option<[DVec2; 2]>) -> &str {
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, svg_defs: &mut String, render_data: RenderData) -> &str {
if !self.visible {
return "";
}
transforms.push(self.transform);
if let Some(viewport_bounds) = culling_bounds {
if let Some(bounding_box) = self.data.bounding_box(transforms.iter().cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY), font_cache) {
if let Some(viewport_bounds) = render_data.culling_bounds {
if let Some(bounding_box) = self
.data
.bounding_box(transforms.iter().cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY), render_data.font_cache)
{
let is_overlapping =
viewport_bounds[0].x < bounding_box[1].x && bounding_box[0].x < viewport_bounds[1].x && viewport_bounds[0].y < bounding_box[1].y && bounding_box[0].y < viewport_bounds[1].y;
if !is_overlapping {
@@ -245,7 +285,7 @@ impl Layer {
if self.cache_dirty {
self.thumbnail_cache.clear();
self.svg_defs_cache.clear();
self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, view_mode, font_cache, culling_bounds);
self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, render_data);
self.cache.clear();
let _ = writeln!(self.cache, r#"<g transform="matrix("#);
+4 -4
View File
@@ -1,5 +1,5 @@
use super::layer_info::LayerData;
use super::style::{self, PathStyle, ViewMode};
use super::style::{self, PathStyle, RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache;
use crate::LayerId;
@@ -32,13 +32,13 @@ pub struct ShapeLayer {
}
impl LayerData for ShapeLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, _font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
let mut path = self.path.clone();
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
let layer_bounds = [(x0, y0).into(), (x1, y1).into()];
let transform = self.transform(transforms, view_mode);
let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse();
if !inverse.is_finite() {
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
@@ -58,7 +58,7 @@ impl LayerData for ShapeLayer {
svg,
r#"<path d="{}" {} />"#,
path.to_svg(),
self.style.render(view_mode, svg_defs, transform, layer_bounds, transformed_bounds)
self.style.render(render_data.view_mode, svg_defs, transform, layer_bounds, transformed_bounds)
);
let _ = svg.write_str("</g>");
}
+21
View File
@@ -1,5 +1,6 @@
//! Contains stylistic options for SVG elements.
use super::text_layer::FontCache;
use crate::color::Color;
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
@@ -36,6 +37,26 @@ impl Default for ViewMode {
}
}
/// Contains metadata for rendering the document as an svg
#[derive(Debug, Clone, Copy)]
pub struct RenderData<'a> {
pub view_mode: ViewMode,
pub font_cache: &'a FontCache,
pub culling_bounds: Option<[DVec2; 2]>,
pub embed_images: bool,
}
impl<'a> RenderData<'a> {
pub fn new(view_mode: ViewMode, font_cache: &'a FontCache, culling_bounds: Option<[DVec2; 2]>, embed_images: bool) -> Self {
Self {
view_mode,
font_cache,
culling_bounds,
embed_images,
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize)]
pub enum GradientType {
Linear,
+16 -19
View File
@@ -1,5 +1,5 @@
use super::layer_info::LayerData;
use super::style::{PathStyle, ViewMode};
use super::style::{PathStyle, RenderData, ViewMode};
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId;
pub use font_cache::{Font, FontCache};
@@ -33,12 +33,12 @@ pub struct TextLayer {
#[serde(skip)]
pub editable: bool,
#[serde(skip)]
cached_path: Option<BezPath>,
pub cached_path: Option<BezPath>,
}
impl LayerData for TextLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, font_cache: &FontCache, _culling_bounds: Option<[DVec2; 2]>) {
let transform = self.transform(transforms, view_mode);
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
let transform = self.transform(transforms, render_data.view_mode);
let inverse = transform.inverse();
if !inverse.is_finite() {
@@ -53,8 +53,8 @@ impl LayerData for TextLayer {
let _ = svg.write_str(r#")">"#);
if self.editable {
let font = font_cache.resolve_font(&self.font);
if let Some(url) = font.and_then(|font| font_cache.get_preview_url(font)) {
let font = render_data.font_cache.resolve_font(&self.font);
if let Some(url) = font.and_then(|font| render_data.font_cache.get_preview_url(font)) {
let _ = write!(svg, r#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, url);
}
@@ -70,7 +70,7 @@ impl LayerData for TextLayer {
font.map(|_| r#" style="font-family: local-font;""#).unwrap_or_default()
);
} else {
let buzz_face = self.load_face(font_cache);
let buzz_face = self.load_face(render_data.font_cache);
let mut path = self.to_bez_path(buzz_face);
@@ -86,7 +86,7 @@ impl LayerData for TextLayer {
svg,
r#"<path d="{}" {} />"#,
path.to_svg(),
self.path_style.render(view_mode, svg_defs, transform, bounds, transformed_bounds)
self.path_style.render(render_data.view_mode, svg_defs, transform, bounds, transformed_bounds)
);
}
let _ = svg.write_str("</g>");
@@ -139,7 +139,7 @@ impl TextLayer {
cached_path: None,
};
new.regenerate_path(new.load_face(font_cache));
new.cached_path = Some(new.generate_path(new.load_face(font_cache)));
new
}
@@ -147,8 +147,10 @@ impl TextLayer {
/// Converts to a [BezPath], populating the cache if necessary.
#[inline]
pub fn to_bez_path(&mut self, buzz_face: Option<Face>) -> BezPath {
if self.cached_path.is_none() {
self.regenerate_path(buzz_face);
if self.cached_path.as_ref().filter(|x| !x.is_empty()).is_none() {
let path = self.generate_path(buzz_face);
self.cached_path = Some(path.clone());
return path;
}
self.cached_path.clone().unwrap()
}
@@ -158,11 +160,11 @@ impl TextLayer {
pub fn to_bez_path_nonmut(&self, font_cache: &FontCache) -> BezPath {
let buzz_face = self.load_face(font_cache);
self.cached_path.clone().unwrap_or_else(|| self.generate_path(buzz_face))
self.cached_path.clone().filter(|x| !x.is_empty()).unwrap_or_else(|| self.generate_path(buzz_face))
}
#[inline]
fn generate_path(&self, buzz_face: Option<Face>) -> BezPath {
pub fn generate_path(&self, buzz_face: Option<Face>) -> BezPath {
to_kurbo::to_kurbo(&self.text, buzz_face, self.size, self.line_width)
}
@@ -172,15 +174,10 @@ impl TextLayer {
Rect::new(0., 0., far.x, far.y)
}
/// Populate the cache.
pub fn regenerate_path(&mut self, buzz_face: Option<Face>) {
self.cached_path = Some(self.generate_path(buzz_face));
}
pub fn update_text(&mut self, text: String, font_cache: &FontCache) {
let buzz_face = self.load_face(font_cache);
self.text = text;
self.regenerate_path(buzz_face);
self.cached_path = Some(self.generate_path(buzz_face));
}
}