From e6d6aa6873ecbc541a2b1d4dec658d122596355e Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 27 Sep 2025 17:50:12 -0700 Subject: [PATCH] Integrate into existing font cache --- node-graph/gcore/src/text.rs | 129 +------------------ node-graph/gcore/src/text/text_context.rs | 35 ++--- node-graph/gcore/src/text/to_path.rs | 17 ++- node-graph/gpath-bool/src/lib.rs | 6 +- node-graph/graph-craft/src/document/value.rs | 19 +-- node-graph/gsvg-renderer/src/renderer.rs | 2 +- 6 files changed, 51 insertions(+), 157 deletions(-) diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs index dce43239a5..7077da5a56 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -3,25 +3,16 @@ mod path_builder; mod text_context; mod to_path; -use std::{ - borrow::Cow, - collections::{HashMap, hash_map::Entry}, - fmt, - sync::{Arc, Mutex}, -}; +use std::fmt; use dyn_any::DynAny; pub use font_cache::*; use graphene_core_shaders::color::Color; -use parley::{Layout, StyleProperty}; -use rustc_hash::FxBuildHasher; -use std::hash::BuildHasher; +use parley::Layout; use std::hash::{Hash, Hasher}; pub use text_context::TextContext; pub use to_path::*; -use crate::{consts::*, table::Table, vector::Vector}; - /// Alignment of lines of type within a text block. #[repr(C)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] @@ -75,7 +66,7 @@ impl Default for TypesettingConfig { #[derive(Clone, DynAny)] pub struct Typography { pub layout: Layout<()>, - pub font_family: String, + pub family_name: String, pub color: Color, pub stroke: Option<(Color, f64)>, } @@ -83,7 +74,7 @@ pub struct Typography { impl fmt::Debug for Typography { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Typography") - .field("font_family", &self.font_family) + .field("font_family", &self.family_name) .field("color", &self.color) .field("stroke", &self.stroke) .finish() @@ -92,118 +83,12 @@ impl fmt::Debug for Typography { impl PartialEq for Typography { fn eq(&self, _other: &Self) -> bool { - true + unimplemented!("Typography data type cannot be compared") } } impl Hash for Typography { - fn hash(&self, state: &mut H) { - self.layout.len().hash(state); - } -} - -impl Typography { - pub fn to_vector(&self) -> Table { - // To implement this function, a clone of the `NewFontCacheWrapper` must be included in the typography data type - Table::new() - } -} - -#[derive(Clone)] -pub struct NewFontCacheWrapper(pub Arc>); - -impl fmt::Debug for NewFontCacheWrapper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("font cache").finish() - } -} - -impl PartialEq for NewFontCacheWrapper { - fn eq(&self, _other: &Self) -> bool { - log::error!("Font cache should not be compared"); - false - } -} - -unsafe impl dyn_any::StaticType for NewFontCacheWrapper { - type Static = NewFontCacheWrapper; -} - -pub struct NewFontCache { - pub font_context: parley::FontContext, - pub layout_context: parley::LayoutContext<()>, - pub font_mapping: HashMap, - pub hash: u64, -} - -impl NewFontCache { - pub fn new() -> Self { - let mut new = NewFontCache { - font_context: parley::FontContext::new(), - layout_context: parley::LayoutContext::new(), - font_mapping: HashMap::new(), - hash: 0, - }; - - let source_sans_font = Font::new(SOURCE_SANS_FONT_FAMILY.to_string(), SOURCE_SANS_FONT_STYLE.to_string()); - new.register_font(source_sans_font, SOURCE_SANS_FONT_DATA.to_vec()); - new - } - - pub fn register_font(&mut self, font: Font, data: Vec) { - match self.font_mapping.entry(font) { - Entry::Occupied(occupied_entry) => { - log::error!("Trying to register font that already is added: {:?}", occupied_entry.key()); - } - Entry::Vacant(vacant_entry) => { - let registered_font = self.font_context.collection.register_fonts(parley::fontique::Blob::from(data), None); - if registered_font.len() > 1 { - log::error!("Registered multiple fonts for {:?}. Only the first is accessible", vacant_entry.key()); - }; - match registered_font.into_iter().next() { - Some((family_id, font_info)) => { - let Some(family_name) = self.font_context.collection.family_name(family_id) else { - log::error!("Could not get family name for font: {:?}", vacant_entry.key()); - return; - }; - let Some(font_info) = font_info.into_iter().next() else { - log::error!("Could not get font info for font: {:?}", vacant_entry.key()); - return; - }; - // Hash the Font for a unique id and add it to the cached hash - let hash_value = FxBuildHasher.hash_one(vacant_entry.key()); - self.hash = self.hash.wrapping_add(hash_value); - - vacant_entry.insert((family_name.to_string(), font_info)); - } - None => log::error!("Could not register font for {:?}", vacant_entry.key()), - } - } - } - } - - pub fn generate_typography(&mut self, font: &Font, font_size: f32, text: &str) -> Option { - let Some((font_family, font_info)) = self.font_mapping.get(font) else { - log::error!("Font not loaded: {:?}", font); - return None; - }; - let font_family = font_family.to_string(); - - let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, 1., false); - - builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Owned(font_family.clone()))))); - builder.push_default(StyleProperty::FontSize(font_size)); - builder.push_default(StyleProperty::FontWeight(font_info.weight())); - builder.push_default(StyleProperty::FontStyle(font_info.style())); - builder.push_default(StyleProperty::FontWidth(font_info.width())); - - let mut layout: Layout<()> = builder.build(text); - layout.break_all_lines(None); - Some(Typography { - layout, - font_family, - color: Color::BLACK, - stroke: None, - }) + fn hash(&self, _: &mut H) { + unimplemented!("Typography cannot be hashed") } } diff --git a/node-graph/gcore/src/text/text_context.rs b/node-graph/gcore/src/text/text_context.rs index da7952493b..44a765a63f 100644 --- a/node-graph/gcore/src/text/text_context.rs +++ b/node-graph/gcore/src/text/text_context.rs @@ -38,34 +38,35 @@ impl TextContext { } /// Get or cache font information for a given font - fn get_font_info(&mut self, font: &Font, font_data: &Blob) -> Option<(String, FontInfo)> { + pub fn get_font_info(&mut self, font: &Font, font_cache: &FontCache) -> Option<(String, FontInfo)> { + // Note that the actual_font may not be the desired font if that font is not yet loaded. + // It is important not to cache the default font under the name of another font. + let (font_data, actual_font) = self.resolve_font_data(font, font_cache)?; + // Check if we already have the font info cached - if let Some((family_id, font_info)) = self.font_info_cache.get(font) { + if let Some((family_id, font_info)) = self.font_info_cache.get(actual_font) { if let Some(family_name) = self.font_context.collection.family_name(*family_id) { return Some((family_name.to_string(), font_info.clone())); } } // Register the font and cache the info - let families = self.font_context.collection.register_fonts(font_data.clone(), None); + let families = self.font_context.collection.register_fonts(font_data, None); - families.first().and_then(|(family_id, fonts_info)| { - fonts_info.first().and_then(|font_info| { - self.font_context.collection.family_name(*family_id).map(|family_name| { + families.into_iter().next().and_then(|(family_id, fonts_info)| { + fonts_info.into_iter().next().and_then(|font_info| { + self.font_context.collection.family_name(family_id).map(|family_name| { // Cache the font info for future use - self.font_info_cache.insert(font.clone(), (*family_id, font_info.clone())); - (family_name.to_string(), font_info.clone()) + self.font_info_cache.insert(actual_font.clone(), (family_id, font_info.clone())); + (family_name.to_string(), font_info) }) }) }) } /// Create a text layout using the specified font and typesetting configuration - fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option> { - // Note that the actual_font may not be the desired font if that font is not yet loaded. - // It is important not to cache the default font under the name of another font. - let (font_data, actual_font) = self.resolve_font_data(font, font_cache)?; - let (font_family, font_info) = self.get_font_info(actual_font, &font_data)?; + pub fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option> { + let (font_family, font_info) = self.get_font_info(font, font_cache)?; const DISPLAY_SCALE: f32 = 1.; let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false); @@ -87,17 +88,21 @@ impl TextContext { } /// Convert text to vector paths using the specified font and typesetting configuration - pub fn to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table { + pub fn text_to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table { let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else { return Table::new_from_element(Vector::default()); }; + self.layout_to_path(layout, 0., per_glyph_instances) + } + + pub fn layout_to_path(&mut self, layout: Layout<()>, tilt: f64, per_glyph_instances: bool) -> Table { let mut path_builder = PathBuilder::new(per_glyph_instances, layout.scale() as f64); for line in layout.lines() { for item in line.items() { if let PositionedLayoutItem::GlyphRun(glyph_run) = item { - path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances); + path_builder.render_glyph_run(&glyph_run, tilt, per_glyph_instances); } } } diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index 64fc4b3b5a..4f0457d179 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -1,13 +1,28 @@ use super::text_context::TextContext; use super::{Font, FontCache, TypesettingConfig}; use crate::table::Table; +use crate::text::Typography; use crate::vector::Vector; use glam::DVec2; +use graphene_core_shaders::color::Color; use parley::fontique::Blob; use std::sync::Arc; +pub fn to_typography(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option { + TextContext::with_thread_local(|ctx| { + let layout = ctx.layout_text(text, font, font_cache, typesetting)?; + let (family_name, _) = ctx.get_font_info(font, font_cache)?; + Some(Typography { + layout, + family_name, + color: Color::BLACK, + stroke: None, + }) + }) +} + pub fn to_path(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table { - TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_instances)) + TextContext::with_thread_local(|ctx| ctx.text_to_path(text, font, font_cache, typesetting, per_glyph_instances)) } pub fn bounding_box(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index 9489aab660..5abb5def2a 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -2,6 +2,7 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use graphene_core::subpath::{ManipulatorGroup, PathSegPoints, Subpath, pathseg_points}; use graphene_core::table::{Table, TableRow, TableRowRef}; +use graphene_core::text::TextContext; use graphene_core::vector::algorithms::merge_by_distance::MergeByDistanceExt; use graphene_core::vector::style::Fill; use graphene_core::vector::{PointId, Vector}; @@ -318,7 +319,10 @@ fn flatten_vector(graphic_table: &Table) -> Table { } }) .collect::>(), - Graphic::Typography(typography) => typography.into_iter().flat_map(|row| row.element.to_vector()).collect::>(), + Graphic::Typography(typography) => typography + .into_iter() + .flat_map(|row| TextContext::with_thread_local(|ctx| ctx.layout_to_path(row.element.layout, 0., false))) + .collect::>(), } }) .collect() diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 0b2bc85991..18e9533f4a 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -11,7 +11,6 @@ use graphene_brush::brush_stroke::BrushStroke; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, Raster}; use graphene_core::table::Table; -use graphene_core::text::NewFontCacheWrapper; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; use graphene_core::vector::Vector; @@ -39,9 +38,7 @@ macro_rules! tagged_value { RenderOutput(RenderOutput), SurfaceFrame(SurfaceFrame), #[serde(skip)] - EditorApi(Arc), - #[serde(skip)] - NewFontCache(NewFontCacheWrapper), + EditorApi(Arc) } // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) @@ -55,7 +52,6 @@ macro_rules! tagged_value { Self::RenderOutput(x) => x.hash(state), Self::SurfaceFrame(x) => x.hash(state), Self::EditorApi(x) => x.hash(state), - Self::NewFontCache(x) => x.hash(state), } } } @@ -68,7 +64,6 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Box::new(x), Self::SurfaceFrame(x) => Box::new(x), Self::EditorApi(x) => Box::new(x), - Self::NewFontCache(x) => Box::new(x), } } /// Converts to a Arc @@ -79,7 +74,6 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Arc::new(x), Self::SurfaceFrame(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x), - Self::NewFontCache(x) => Arc::new(x), } } /// Creates a graphene_core::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value @@ -89,8 +83,7 @@ macro_rules! tagged_value { $( Self::$identifier(_) => concrete!($ty), )* Self::RenderOutput(_) => concrete!(RenderOutput), Self::SurfaceFrame(_) => concrete!(SurfaceFrame), - Self::EditorApi(_) => concrete!(&WasmEditorApi), - Self::NewFontCache(_) => concrete!(NewFontCacheWrapper), + Self::EditorApi(_) => concrete!(&WasmEditorApi) } } /// Attempts to downcast the dynamic type to a tagged value @@ -529,14 +522,6 @@ mod fake_hash { self.1.hash(state) } } - impl FakeHash for NewFontCacheWrapper { - fn hash(&self, state: &mut H) { - match self.0.lock() { - Ok(inner) => inner.hash.hash(state), - Err(_) => log::error!("Could not lock font cache when hashing"), - } - } - } } #[test] diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 75138a131c..0ecac289c7 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -1596,7 +1596,7 @@ impl Render for Table { attributes.push("transform", matrix); } - attributes.push("font-family", table_row.element.font_family.clone()); + attributes.push("font-family", table_row.element.family_name.clone()); attributes.push("font-weight", font_attributes.weight.value().to_string()); attributes.push("font-size", glyph_run.run().font_size().to_string()); attributes.push("font-style", font_style);