mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 03:58:12 +08:00
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:
co-authored by
Keavon Chambers
parent
ee98908d88
commit
2191b09f9f
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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("#);
|
||||
|
||||
@@ -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>");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user