Font selection for text layers (#585)

* Add font dropdown

* Add fonts

* Font tool options

* Fix tests

* Replace http with https

* Add variant selection

* Do not embed default font

* Use proxied font list API

* Change default font to Merriweather

* Remove outdated comment

* Specify font once & load font into foreignobject

* Fix tests

* Rename variant to font_style

* Change TextAreaInput to use FieldInput (WIP, breaks functionality)

* Fix textarea functionality

* Fix types

* Add weight name mapping

* Change labeling of "Italic"

* Remove commented HTML node

* Rename font "name" to "font_family" and "file" "font_file"

* Fix errors

* Fix fmt

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2022-04-21 09:50:44 +01:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 916a575446
commit 239aa03453
36 changed files with 1029 additions and 253 deletions
+99 -12
View File
@@ -14,12 +14,50 @@ use kurbo::Affine;
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
/// A number that identifies a layer.
/// This does not technically need to be unique globally, only within a folder.
pub type LayerId = u64;
/// A cache of all loaded fonts along with a string of the name of the default font (sent from js)
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct FontCache {
data: HashMap<String, Vec<u8>>,
default_font: Option<String>,
}
impl FontCache {
/// Returns the font family name if the font is cached, otherwise returns the default font family name if that is cached
pub fn resolve_font<'a>(&'a self, font: Option<&'a String>) -> Option<&'a String> {
font.filter(|font| self.loaded_font(font))
.map_or(self.default_font.as_ref().filter(|font| self.loaded_font(font)), Some)
}
/// Try to get the bytes for a font
pub fn get<'a>(&'a self, font: Option<&String>) -> Option<&'a Vec<u8>> {
self.resolve_font(font).and_then(|font| self.data.get(font))
}
/// Check if the font is already loaded
pub fn loaded_font(&self, font: &str) -> bool {
self.data.contains_key(font)
}
/// Insert a new font into the cache
pub fn insert(&mut self, font: String, data: Vec<u8>, is_default: bool) {
if is_default {
self.default_font = Some(font.clone());
}
self.data.insert(font, data);
}
/// Checks if the font cache has a default font
pub fn has_default(&self) -> bool {
self.default_font.is_some()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Document {
/// The root layer, usually a [FolderLayer](layers::folder_layer::FolderLayer) that contains all other [Layers](layers::layer_info::Layer).
@@ -28,6 +66,7 @@ pub struct Document {
/// This identifier is not a hash and is not guaranteed to be equal for equivalent documents.
#[serde(skip)]
pub state_identifier: DefaultHasher,
pub font_cache: FontCache,
}
impl Default for Document {
@@ -35,6 +74,7 @@ impl Default for Document {
Self {
root: Layer::new(LayerDataType::Folder(FolderLayer::default()), DAffine2::IDENTITY.to_cols_array()),
state_identifier: DefaultHasher::new(),
font_cache: FontCache::default(),
}
}
}
@@ -44,7 +84,7 @@ impl Document {
pub fn render_root(&mut self, mode: ViewMode) -> String {
let mut svg_defs = String::from("<defs>");
self.root.render(&mut vec![], mode, &mut svg_defs);
self.root.render(&mut vec![], mode, &mut svg_defs, &self.font_cache);
svg_defs.push_str("</defs>");
@@ -58,7 +98,7 @@ impl Document {
/// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`.
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
self.layer(path).unwrap().intersects_quad(quad, path, intersections);
self.layer(path).unwrap().intersects_quad(quad, path, intersections, &self.font_cache);
}
/// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers.
@@ -321,13 +361,13 @@ impl Document {
pub fn viewport_bounding_box(&self, path: &[LayerId]) -> Result<Option<[DVec2; 2]>, DocumentError> {
let layer = self.layer(path)?;
let transform = self.multiply_transforms(path)?;
Ok(layer.data.bounding_box(transform))
Ok(layer.data.bounding_box(transform, &self.font_cache))
}
pub fn bounding_box_and_transform(&self, path: &[LayerId]) -> Result<Option<([DVec2; 2], DAffine2)>, DocumentError> {
let layer = self.layer(path)?;
let transform = self.multiply_transforms(&path[..path.len() - 1])?;
Ok(layer.data.bounding_box(layer.transform).map(|bounds| (bounds, transform)))
Ok(layer.data.bounding_box(layer.transform, &self.font_cache).map(|bounds| (bounds, transform)))
}
pub fn visible_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
@@ -491,11 +531,13 @@ impl Document {
insert_index,
transform,
text,
style,
size,
font_name,
font_style,
font_file,
} => {
let layer = Layer::new(LayerDataType::Text(TextLayer::new(text, style, size)), transform);
let layer = Layer::new(LayerDataType::Text(TextLayer::new(text, style, size, font_name, font_style, font_file, &self.font_cache)), transform);
self.set_layer(&path, layer, insert_index)?;
@@ -520,7 +562,20 @@ impl Document {
Some(vec![DocumentChanged])
}
Operation::SetTextContent { path, new_text } => {
self.layer_mut(&path)?.as_text_mut()?.update_text(new_text);
// Not using Document::layer_mut is necessary because we alson need to borrow the font cache
let mut current_folder = &mut self.root;
let (layer_path, id) = split_path(&path)?;
for id in layer_path {
current_folder = current_folder.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(layer_path.into()))?;
}
current_folder
.as_folder_mut()?
.layer_mut(id)
.ok_or_else(|| DocumentError::LayerNotFound(path.clone()))?
.as_text_mut()?
.update_text(new_text, &self.font_cache);
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
@@ -679,6 +734,30 @@ impl Document {
return Err(DocumentError::IndexOutOfBounds);
}
}
Operation::ModifyFont {
path,
font_family,
font_style,
font_file,
size,
} => {
// Not using Document::layer_mut is necessary because we alson need to borrow the font cache
let mut current_folder = &mut self.root;
let (folder_path, id) = split_path(&path)?;
for id in folder_path {
current_folder = current_folder.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
}
let layer_mut = current_folder.as_folder_mut()?.layer_mut(id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
let text = layer_mut.as_text_mut()?;
text.font_family = font_family;
text.font_style = font_style;
text.font_file = font_file;
text.size = size;
text.regenerate_path(text.load_face(&self.font_cache));
self.mark_as_dirty(&path)?;
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
}
Operation::RenameLayer { layer_path: path, new_name: name } => {
self.layer_mut(&path)?.name = Some(name);
Some(vec![LayerChanged { path }])
@@ -729,12 +808,20 @@ impl Document {
self.set_transform_relative_to_viewport(&path, transform)?;
self.mark_as_dirty(&path)?;
if let LayerDataType::Text(t) = &mut self.layer_mut(&path)?.data {
let bezpath = t.to_bez_path();
self.layer_mut(&path)?.data = layers::layer_info::LayerDataType::Shape(ShapeLayer::from_bez_path(bezpath, t.style.clone(), true));
// Not using Document::layer_mut is necessary because we alson need to borrow the font cache
let mut current_folder = &mut self.root;
let (folder_path, id) = split_path(&path)?;
for id in folder_path {
current_folder = current_folder.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
}
let layer_mut = current_folder.as_folder_mut()?.layer_mut(id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
if let LayerDataType::Text(t) = &mut layer_mut.data {
let bezpath = t.to_bez_path(t.load_face(&self.font_cache));
layer_mut.data = layers::layer_info::LayerDataType::Shape(ShapeLayer::from_bez_path(bezpath, t.path_style.clone(), true));
}
if let LayerDataType::Shape(shape) = &mut self.layer_mut(&path)?.data {
if let LayerDataType::Shape(shape) = &mut layer_mut.data {
shape.path = bez_path;
}
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
@@ -795,7 +882,7 @@ impl Document {
let layer = self.layer_mut(&path)?;
match &mut layer.data {
LayerDataType::Shape(s) => s.style = style,
LayerDataType::Text(text) => text.style = style,
LayerDataType::Text(text) => text.path_style = style,
_ => return Err(DocumentError::NotAShape),
}
self.mark_as_dirty(&path)?;
+7 -6
View File
@@ -1,5 +1,6 @@
use super::layer_info::{Layer, LayerData, LayerDataType};
use super::style::ViewMode;
use crate::document::FontCache;
use crate::intersection::Quad;
use crate::{DocumentError, LayerId};
@@ -21,24 +22,24 @@ 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) {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache) {
for layer in &mut self.layers {
let _ = writeln!(svg, "{}", layer.render(transforms, view_mode, svg_defs));
let _ = writeln!(svg, "{}", layer.render(transforms, view_mode, svg_defs, font_cache));
}
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
path.push(*layer_id);
layer.intersects_quad(quad, path, intersections);
layer.intersects_quad(quad, path, intersections, font_cache);
path.pop();
}
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
self.layers
.iter()
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform))
.filter_map(|layer| layer.data.bounding_box(transform * layer.transform, font_cache))
.reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])])
}
}
+4 -3
View File
@@ -1,5 +1,6 @@
use super::layer_info::LayerData;
use super::style::ViewMode;
use crate::document::FontCache;
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId;
@@ -23,7 +24,7 @@ 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) {
fn render(&mut self, svg: &mut String, _svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, _font_cache: &FontCache) {
let transform = self.transform(transforms, view_mode);
let inverse = transform.inverse();
@@ -55,7 +56,7 @@ impl LayerData for ImageLayer {
let _ = svg.write_str("</g>");
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
let mut path = self.bounds();
if transform.matrix2 == DMat2::ZERO {
@@ -67,7 +68,7 @@ impl LayerData for ImageLayer {
Some([(x0, y0).into(), (x1, y1).into()])
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
if intersect_quad_bez_path(quad, &self.bounds(), true) {
intersections.push(path.clone());
}
+31 -26
View File
@@ -4,6 +4,7 @@ use super::image_layer::ImageLayer;
use super::shape_layer::ShapeLayer;
use super::style::{PathStyle, ViewMode};
use super::text_layer::TextLayer;
use crate::document::FontCache;
use crate::intersection::Quad;
use crate::DocumentError;
use crate::LayerId;
@@ -54,12 +55,13 @@ pub trait LayerData {
/// # use graphite_graphene::layers::shape_layer::ShapeLayer;
/// # use graphite_graphene::layers::style::{Fill, PathStyle, ViewMode};
/// # use graphite_graphene::layers::layer_info::LayerData;
/// # use std::collections::HashMap;
///
/// let mut shape = ShapeLayer::rectangle(PathStyle::new(None, Fill::None));
/// 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);
/// shape.render(&mut svg, &mut String::new(), &mut vec![], ViewMode::Normal, &Default::default());
///
/// assert_eq!(
/// svg,
@@ -68,7 +70,7 @@ pub trait LayerData {
/// </g>"
/// );
/// ```
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode);
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache);
/// Determine the layers within this layer that intersect a given quad.
/// # Example
@@ -78,6 +80,7 @@ pub trait LayerData {
/// # use graphite_graphene::layers::layer_info::LayerData;
/// # use graphite_graphene::intersection::Quad;
/// # use glam::f64::{DAffine2, DVec2};
/// # use std::collections::HashMap;
///
/// let mut shape = ShapeLayer::ellipse(PathStyle::new(None, Fill::None));
/// let shape_id = 42;
@@ -86,11 +89,11 @@ pub trait LayerData {
/// let quad = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
/// let mut intersections = vec![];
///
/// shape.intersects_quad(quad, &mut vec![shape_id], &mut intersections);
/// shape.intersects_quad(quad, &mut vec![shape_id], &mut intersections, &Default::default());
///
/// assert_eq!(intersections, vec![vec![shape_id]]);
/// ```
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>);
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache);
// TODO: this doctest fails because 0 != 1e-32, maybe assert difference < epsilon?
/// Calculate the bounding box for the layer's contents after applying a given transform.
@@ -100,29 +103,30 @@ pub trait LayerData {
/// # use graphite_graphene::layers::style::{Fill, PathStyle};
/// # use graphite_graphene::layers::layer_info::LayerData;
/// # use glam::f64::{DAffine2, DVec2};
/// # use std::collections::HashMap;
/// let shape = ShapeLayer::ellipse(PathStyle::new(None, Fill::None));
///
/// // Calculate the bounding box without applying any transformations.
/// // (The identity transform maps every vector to itself.)
/// let transform = DAffine2::IDENTITY;
/// let bounding_box = shape.bounding_box(transform);
/// let bounding_box = shape.bounding_box(transform, &Default::default());
///
/// assert_eq!(bounding_box, Some([DVec2::ZERO, DVec2::ONE]));
/// ```
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>;
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]>;
}
impl LayerData for LayerDataType {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode) {
self.inner_mut().render(svg, svg_defs, transforms, view_mode)
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<glam::DAffine2>, view_mode: ViewMode, font_cache: &FontCache) {
self.inner_mut().render(svg, svg_defs, transforms, view_mode, font_cache)
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
self.inner().intersects_quad(quad, path, intersections)
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
self.inner().intersects_quad(quad, path, intersections, font_cache)
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
self.inner().bounding_box(transform)
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
self.inner().bounding_box(transform, font_cache)
}
}
@@ -219,7 +223,7 @@ impl Layer {
LayerIter { stack: vec![self] }
}
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, svg_defs: &mut String) -> &str {
pub fn render(&mut self, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, svg_defs: &mut String, font_cache: &FontCache) -> &str {
if !self.visible {
return "";
}
@@ -228,7 +232,7 @@ impl Layer {
transforms.push(self.transform);
self.thumbnail_cache.clear();
self.svg_defs_cache.clear();
self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, view_mode);
self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, view_mode, font_cache);
self.cache.clear();
let _ = writeln!(self.cache, r#"<g transform="matrix("#);
@@ -250,13 +254,13 @@ impl Layer {
self.cache.as_str()
}
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
pub fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
if !self.visible {
return;
}
let transformed_quad = self.transform.inverse() * quad;
self.data.intersects_quad(transformed_quad, path, intersections)
self.data.intersects_quad(transformed_quad, path, intersections, font_cache)
}
/// Compute the bounding box of the layer after applying a transform to it.
@@ -268,31 +272,32 @@ impl Layer {
/// # use graphite_graphene::layers::style::PathStyle;
/// # use glam::DVec2;
/// # use glam::f64::DAffine2;
/// # use std::collections::HashMap;
/// // Create a rectangle with the default dimensions, from `(0|0)` to `(1|1)`
/// let layer: Layer = ShapeLayer::rectangle(PathStyle::default()).into();
///
/// // Apply the Identity transform, which leaves the points unchanged
/// assert_eq!(
/// layer.aabounding_box_for_transform(DAffine2::IDENTITY),
/// layer.aabounding_box_for_transform(DAffine2::IDENTITY, &Default::default()),
/// Some([DVec2::ZERO, DVec2::ONE]),
/// );
///
/// // Apply a transform that scales every point by a factor of two
/// let transform = DAffine2::from_scale(DVec2::ONE * 2.);
/// assert_eq!(
/// layer.aabounding_box_for_transform(transform),
/// layer.aabounding_box_for_transform(transform, &Default::default()),
/// Some([DVec2::ZERO, DVec2::ONE * 2.]),
/// );
pub fn aabounding_box_for_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.data.bounding_box(transform)
pub fn aabounding_box_for_transform(&self, transform: DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
self.data.bounding_box(transform, font_cache)
}
pub fn aabounding_box(&self) -> Option<[DVec2; 2]> {
self.aabounding_box_for_transform(self.transform)
pub fn aabounding_box(&self, font_cache: &FontCache) -> Option<[DVec2; 2]> {
self.aabounding_box_for_transform(self.transform, font_cache)
}
pub fn bounding_transform(&self) -> DAffine2 {
let scale = match self.aabounding_box_for_transform(DAffine2::IDENTITY) {
pub fn bounding_transform(&self, font_cache: &FontCache) -> DAffine2 {
let scale = match self.aabounding_box_for_transform(DAffine2::IDENTITY, font_cache) {
Some([a, b]) => {
let dimensions = b - a;
DAffine2::from_scale(dimensions)
@@ -360,7 +365,7 @@ impl Layer {
pub fn style(&self) -> Result<&PathStyle, DocumentError> {
match &self.data {
LayerDataType::Shape(s) => Ok(&s.style),
LayerDataType::Text(t) => Ok(&t.style),
LayerDataType::Text(t) => Ok(&t.path_style),
_ => Err(DocumentError::NotAShape),
}
}
@@ -368,7 +373,7 @@ impl Layer {
pub fn style_mut(&mut self) -> Result<&mut PathStyle, DocumentError> {
match &mut self.data {
LayerDataType::Shape(s) => Ok(&mut s.style),
LayerDataType::Text(t) => Ok(&mut t.style),
LayerDataType::Text(t) => Ok(&mut t.path_style),
_ => Err(DocumentError::NotAShape),
}
}
+4 -3
View File
@@ -1,5 +1,6 @@
use super::layer_info::LayerData;
use super::style::{self, PathStyle, ViewMode};
use crate::document::FontCache;
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId;
@@ -30,7 +31,7 @@ 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) {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, _font_cache: &FontCache) {
let mut path = self.path.clone();
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
@@ -61,7 +62,7 @@ impl LayerData for ShapeLayer {
let _ = svg.write_str("</g>");
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
use kurbo::Shape;
let mut path = self.path.clone();
@@ -74,7 +75,7 @@ impl LayerData for ShapeLayer {
Some([(x0, y0).into(), (x1, y1).into()])
}
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
if intersect_quad_bez_path(quad, &self.path, self.style.fill().is_some()) {
intersections.push(path.clone());
}
@@ -1,93 +0,0 @@
Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name Source.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+51 -31
View File
@@ -1,10 +1,12 @@
use super::layer_info::LayerData;
use super::style::{PathStyle, ViewMode};
use crate::document::FontCache;
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId;
use glam::{DAffine2, DMat2, DVec2};
use kurbo::{Affine, BezPath, Rect, Shape};
use rustybuzz::Face;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
@@ -17,16 +19,18 @@ fn glam_to_kurbo(transform: DAffine2) -> Affine {
/// A line, or multiple lines, of text drawn in the document.
/// Like [ShapeLayers](super::shape_layer::ShapeLayer), [TextLayer] are rendered as
/// [`<path>`s](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path).
/// Currently, the only supported font is `SourceSansPro-Regular`.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct TextLayer {
/// The string of text, encompassing one or multiple lines.
pub text: String,
/// Fill color and stroke used to render the text.
pub style: PathStyle,
pub path_style: PathStyle,
/// Font size in pixels.
pub size: f64,
pub line_width: Option<f64>,
pub font_family: String,
pub font_style: String,
pub font_file: Option<String>,
#[serde(skip)]
pub editable: bool,
#[serde(skip)]
@@ -34,7 +38,7 @@ pub struct TextLayer {
}
impl LayerData for TextLayer {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode) {
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, view_mode: ViewMode, font_cache: &FontCache) {
let transform = self.transform(transforms, view_mode);
let inverse = transform.inverse();
@@ -50,18 +54,26 @@ impl LayerData for TextLayer {
let _ = svg.write_str(r#")">"#);
if self.editable {
let font = font_cache.resolve_font(self.font_file.as_ref());
if let Some(url) = font {
let _ = write!(svg, r#"<style>@font-face {{font-family: local-font;src: url({});}}")</style>"#, url);
}
let _ = write!(
svg,
r#"<foreignObject transform="matrix({})"></foreignObject>"#,
r#"<foreignObject transform="matrix({})"{}></foreignObject>"#,
transform
.to_cols_array()
.iter()
.enumerate()
.map(|(i, entry)| { entry.to_string() + if i == 5 { "" } else { "," } })
.collect::<String>(),
font.map(|_| r#" style="font-family: local-font;""#).unwrap_or_default()
);
} else {
let mut path = self.to_bez_path();
let buzz_face = self.load_face(font_cache);
let mut path = self.to_bez_path(buzz_face);
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
let bounds = [(x0, y0).into(), (x1, y1).into()];
@@ -75,14 +87,16 @@ impl LayerData for TextLayer {
svg,
r#"<path d="{}" {} />"#,
path.to_svg(),
self.style.render(view_mode, svg_defs, transform, bounds, transformed_bounds)
self.path_style.render(view_mode, svg_defs, transform, bounds, transformed_bounds)
);
}
let _ = svg.write_str("</g>");
}
fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
let mut path = self.bounding_box(&self.text).to_path(0.1);
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
let buzz_face = Some(self.load_face(font_cache)?);
let mut path = self.bounding_box(&self.text, buzz_face).to_path(0.1);
if transform.matrix2 == DMat2::ZERO {
return None;
@@ -93,14 +107,20 @@ impl LayerData for TextLayer {
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.bounding_box(&self.text).to_path(0.), true) {
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
let buzz_face = self.load_face(font_cache);
if intersect_quad_bez_path(quad, &self.bounding_box(&self.text, buzz_face).to_path(0.), true) {
intersections.push(path.clone());
}
}
}
impl TextLayer {
pub fn load_face<'a>(&self, font_cache: &'a FontCache) -> Option<Face<'a>> {
font_cache.get(self.font_file.as_ref()).map(|data| rustybuzz::Face::from_slice(data, 0).expect("Loading font failed"))
}
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
let start = match mode {
ViewMode::Outline => 0,
@@ -109,61 +129,61 @@ impl TextLayer {
transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY)
}
pub fn new(text: String, style: PathStyle, size: f64) -> Self {
pub fn new(text: String, style: PathStyle, size: f64, font_family: String, font_style: String, font_file: Option<String>, font_cache: &FontCache) -> Self {
let mut new = Self {
text,
style,
path_style: style,
size,
line_width: None,
font_family,
font_style,
font_file,
editable: false,
cached_path: None,
};
new.regenerate_path();
new.regenerate_path(new.load_face(font_cache));
new
}
/// Converts to a [BezPath], populating the cache if necessary.
#[inline]
pub fn to_bez_path(&mut self) -> BezPath {
pub fn to_bez_path(&mut self, buzz_face: Option<Face>) -> BezPath {
if self.cached_path.is_none() {
self.regenerate_path();
self.regenerate_path(buzz_face);
}
self.cached_path.clone().unwrap()
}
/// Converts to a [BezPath], without populating the cache.
#[inline]
pub fn to_bez_path_nonmut(&self) -> BezPath {
self.cached_path.clone().unwrap_or_else(|| self.generate_path())
}
pub fn to_bez_path_nonmut(&self, font_cache: &FontCache) -> BezPath {
let buzz_face = self.load_face(font_cache);
/// Get the font face for `SourceSansPro-Regular`.
/// For now, the font is hardcoded in the wasm binary.
#[inline]
fn font_face() -> rustybuzz::Face<'static> {
rustybuzz::Face::from_slice(include_bytes!("SourceSansPro/SourceSansPro-Regular.ttf"), 0).unwrap()
self.cached_path.clone().unwrap_or_else(|| self.generate_path(buzz_face))
}
#[inline]
fn generate_path(&self) -> BezPath {
to_kurbo::to_kurbo(&self.text, Self::font_face(), self.size, self.line_width)
fn generate_path(&self, buzz_face: Option<Face>) -> BezPath {
to_kurbo::to_kurbo(&self.text, buzz_face, self.size, self.line_width)
}
#[inline]
pub fn bounding_box(&self, text: &str) -> Rect {
let far = to_kurbo::bounding_box(text, Self::font_face(), self.size, self.line_width);
pub fn bounding_box(&self, text: &str, buzz_face: Option<Face>) -> Rect {
let far = to_kurbo::bounding_box(text, buzz_face, self.size, self.line_width);
Rect::new(0., 0., far.x, far.y)
}
/// Populate the cache.
pub fn regenerate_path(&mut self) {
self.cached_path = Some(self.generate_path());
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) {
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();
self.regenerate_path(buzz_face);
}
}
+14 -2
View File
@@ -67,7 +67,13 @@ fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_
false
}
pub fn to_kurbo(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_width: Option<f64>) -> BezPath {
pub fn to_kurbo(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> BezPath {
let buzz_face = match buzz_face {
Some(face) => face,
// Show blank layer if font has not loaded
None => return BezPath::default(),
};
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
let mut builder = Builder {
@@ -106,7 +112,13 @@ pub fn to_kurbo(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_widt
builder.path
}
pub fn bounding_box(str: &str, buzz_face: rustybuzz::Face, font_size: f64, line_width: Option<f64>) -> DVec2 {
pub fn bounding_box(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> DVec2 {
let buzz_face = match buzz_face {
Some(face) => face,
// Show blank layer if font has not loaded
None => return DVec2::ZERO,
};
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
let mut pos = DVec2::ZERO;
+10
View File
@@ -53,6 +53,9 @@ pub enum Operation {
text: String,
style: style::PathStyle,
size: f64,
font_name: String,
font_style: String,
font_file: Option<String>,
},
AddImage {
path: Vec<LayerId>,
@@ -119,6 +122,13 @@ pub enum Operation {
DuplicateLayer {
path: Vec<LayerId>,
},
ModifyFont {
path: Vec<LayerId>,
font_family: String,
font_style: String,
font_file: Option<String>,
size: f64,
},
RenameLayer {
layer_path: Vec<LayerId>,
new_name: String,