🎉 Store custom fonts (ttfs) and use them to write texts (wasm) (#6050)

This commit is contained in:
Belén Albeza 2025-03-14 12:45:15 +01:00 committed by GitHub
parent e4c9b736f7
commit eb6d2fb0eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 386 additions and 140 deletions

View file

@ -293,31 +293,6 @@ pub extern "C" fn add_shape_fill_stops() {
mem::free_bytes();
}
#[no_mangle]
pub extern "C" fn store_font(family_name_size: u32, font_size: u32) {
with_state!(state, {
unsafe {
let font_bytes = Vec::<u8>::from_raw_parts(
mem::buffer_ptr(),
font_size as usize,
font_size as usize,
);
let family_name = String::from_raw_parts(
mem::buffer_ptr().add(font_size as usize),
family_name_size as usize,
family_name_size as usize,
);
match state.render_state().add_font(family_name, &font_bytes) {
Err(msg) => {
eprintln!("{}", msg);
}
_ => {}
}
mem::free_bytes();
}
});
}
#[no_mangle]
pub extern "C" fn store_image(a: u32, b: u32, c: u32, d: u32) {
with_state!(state, {

View file

@ -38,14 +38,6 @@ pub extern "C" fn free_bytes() {
std::mem::drop(guard);
}
pub fn buffer_ptr() -> *mut u8 {
let guard = BUFFERU8.lock().unwrap();
guard
.as_ref()
.map_or(std::ptr::null_mut(), |buffer| buffer.as_ptr() as *mut u8)
}
pub fn bytes() -> Vec<u8> {
let mut guard = BUFFERU8.lock().unwrap();

View file

@ -1,4 +1,4 @@
use skia_safe::{self as skia, Contains, FontMgr, Matrix, RRect, Rect};
use skia_safe::{self as skia, Contains, Matrix, RRect, Rect};
use std::collections::HashMap;
use uuid::Uuid;
@ -8,6 +8,7 @@ mod blend;
mod cache;
mod debug;
mod fills;
mod fonts;
mod gpu_state;
mod images;
mod options;
@ -23,11 +24,9 @@ use options::RenderOptions;
use surfaces::{SurfaceId, Surfaces};
pub use blend::BlendMode;
pub use fonts::*;
pub use images::*;
const DEFAULT_FONT_BYTES: &[u8] =
include_bytes!("../../frontend/resources/fonts/RobotoMono-Regular.ttf");
const MAX_BLOCKING_TIME_MS: i32 = 32;
const NODE_BATCH_THRESHOLD: i32 = 10;
@ -86,9 +85,10 @@ pub(crate) struct RenderState {
gpu_state: GpuState,
pub options: RenderOptions,
pub surfaces: Surfaces,
fonts: FontStore,
// TODO: we should probably have only one of these
pub font_provider: skia::textlayout::TypefaceFontProvider,
pub font_collection: skia::textlayout::FontCollection,
// pub font_provider: skia::textlayout::TypefaceFontProvider,
// pub font_collection: skia::textlayout::FontCollection,
// ----
pub cached_surface_image: Option<CachedSurfaceImage>,
pub viewbox: Viewbox,
@ -112,15 +112,8 @@ impl RenderState {
let sampling_options =
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
let surfaces = Surfaces::new(&mut gpu_state, (width, height), sampling_options);
let mut font_provider = skia::textlayout::TypefaceFontProvider::new();
let default_font = skia::FontMgr::default()
.new_from_data(DEFAULT_FONT_BYTES, None)
.expect("Failed to load font");
font_provider.register_typeface(default_font, "robotomono-regular");
let mut font_collection = skia::textlayout::FontCollection::new();
let font_manager = FontMgr::from(font_provider.clone());
font_collection.set_default_font_manager(FontMgr::default(), None);
font_collection.set_dynamic_font_manager(font_manager);
let fonts = FontStore::new();
// This is used multiple times everywhere so instead of creating new instances every
// time we reuse this one.
@ -129,8 +122,9 @@ impl RenderState {
gpu_state,
surfaces,
cached_surface_image: None,
font_provider,
font_collection,
// font_provider,
// font_collection,
fonts,
options: RenderOptions::default(),
viewbox: Viewbox::new(width as f32, height as f32),
images: ImageStore::new(),
@ -143,13 +137,11 @@ impl RenderState {
}
}
pub fn add_font(&mut self, family_name: String, font_data: &[u8]) -> Result<(), String> {
let typeface = skia::FontMgr::default()
.new_from_data(font_data, None)
.expect("Failed to add font");
self.font_provider
.register_typeface(typeface, family_name.as_ref());
Ok(())
pub fn fonts(&self) -> &FontStore {
&self.fonts
}
pub fn fonts_mut(&mut self) -> &mut FontStore {
&mut self.fonts
}
pub fn add_image(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> {
@ -353,7 +345,7 @@ impl RenderState {
if let Some(svg) = shape.svg.as_ref() {
svg.render(self.surfaces.canvas(SurfaceId::Fills))
} else {
let font_manager = skia::FontMgr::from(self.font_provider.clone());
let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone());
let dom_result = skia::svg::Dom::from_str(sr.content.to_string(), font_manager);
match dom_result {
Ok(dom) => {

View file

@ -23,18 +23,17 @@ fn render_debug_view(render_state: &mut RenderState) {
}
pub fn render_wasm_label(render_state: &mut RenderState) {
let canvas = render_state.surfaces.canvas(SurfaceId::Current);
let font_provider = render_state.fonts().font_provider();
let typeface = font_provider
.match_family_style("robotomono-regular", skia::FontStyle::default())
.unwrap();
let canvas = render_state.surfaces.canvas(SurfaceId::Current);
let skia::ISize { width, height } = canvas.base_layer_size();
let p = skia::Point::new(width as f32 - 100.0, height as f32 - 25.0);
let mut paint = skia::Paint::default();
paint.set_color(skia::Color::from_argb(100, 0, 0, 0));
let font_provider = &render_state.font_provider;
let typeface = font_provider
.match_family_style("robotomono-regular", skia::FontStyle::default())
.unwrap();
let font = skia::Font::new(typeface, 10.0);
canvas.draw_str("WASM RENDERER", p, &font, &paint);
}

View file

@ -0,0 +1,70 @@
use skia_safe::{self as skia, textlayout, FontMgr};
use crate::shapes::FontFamily;
const DEFAULT_FONT_BYTES: &[u8] = include_bytes!("../fonts/RobotoMono-Regular.ttf");
pub struct FontStore {
// TODO: we should probably have just one of those
font_provider: textlayout::TypefaceFontProvider,
font_collection: textlayout::FontCollection,
}
impl FontStore {
pub fn new() -> Self {
let mut font_provider = skia::textlayout::TypefaceFontProvider::new();
let default_font = skia::FontMgr::default()
.new_from_data(DEFAULT_FONT_BYTES, None)
.expect("Failed to load font");
font_provider.register_typeface(default_font, "robotomono-regular");
let mut font_collection = skia::textlayout::FontCollection::new();
font_collection.set_default_font_manager(FontMgr::default(), None);
font_collection.set_dynamic_font_manager(FontMgr::from(font_provider.clone()));
Self {
font_provider,
font_collection,
}
}
pub fn font_provider(&self) -> &textlayout::TypefaceFontProvider {
&self.font_provider
}
pub fn font_collection(&self) -> &textlayout::FontCollection {
&self.font_collection
}
pub fn add(&mut self, family: FontFamily, font_data: &[u8]) -> Result<(), String> {
if self.has_family(&family) {
return Ok(());
}
let alias = format!("{}", family);
let typeface = skia::FontMgr::default()
.new_from_data(font_data, None)
.ok_or("Failed to create typeface")?;
self.font_provider
.register_typeface(typeface, alias.as_str());
self.refresh_font_collection();
Ok(())
}
pub fn has_family(&self, family: &FontFamily) -> bool {
let serialized = format!("{}", family);
self.font_provider.family_names().any(|x| x == serialized)
}
fn refresh_font_collection(&mut self) {
self.font_collection = skia::textlayout::FontCollection::new();
self.font_collection
.set_default_font_manager(FontMgr::default(), None);
self.font_collection
.set_dynamic_font_manager(FontMgr::from(self.font_provider.clone()));
}
}

View file

@ -3,8 +3,9 @@ use crate::shapes::TextContent;
pub fn render(render_state: &mut RenderState, text: &TextContent) {
let mut offset_y = 0.0;
for mut skia_paragraph in text.to_paragraphs(&render_state.font_collection) {
for mut skia_paragraph in text.to_paragraphs(&render_state.fonts().font_collection()) {
skia_paragraph.layout(text.width());
let xy = (text.x(), text.y() + offset_y);
skia_paragraph.paint(render_state.surfaces.canvas(SurfaceId::Fills), xy);
offset_y += skia_paragraph.height();

View file

@ -9,6 +9,7 @@ mod blurs;
mod bools;
mod corners;
mod fills;
mod fonts;
mod frames;
mod groups;
mod layouts;
@ -25,6 +26,7 @@ pub use blurs::*;
pub use bools::*;
pub use corners::*;
pub use fills::*;
pub use fonts::*;
pub use frames::*;
pub use groups::*;
pub use layouts::*;
@ -588,10 +590,15 @@ impl Shape {
}
}
pub fn add_text_leaf(&mut self, text_str: &str) -> Result<(), String> {
pub fn add_text_leaf(
&mut self,
text_str: String,
font_family: FontFamily,
font_size: f32,
) -> Result<(), String> {
match self.shape_type {
Type::Text(ref mut text) => {
text.add_leaf(text_str)?;
text.add_leaf(text_str, font_family, font_size)?;
Ok(())
}
_ => Err("Shape is not a text".to_string()),

View file

@ -0,0 +1,48 @@
use std::fmt;
use uuid::Uuid;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum FontStyle {
Normal,
Italic,
}
impl From<u8> for FontStyle {
fn from(value: u8) -> Self {
match value {
0 => Self::Normal,
1 => Self::Italic,
_ => Self::Normal,
}
}
}
impl fmt::Display for FontStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let txt = match self {
Self::Normal => "normal",
Self::Italic => "italic",
};
write!(f, "{}", txt)
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub struct FontFamily {
id: Uuid,
style: FontStyle,
weight: u32,
}
impl FontFamily {
pub fn new(id: Uuid, weight: u32, style: FontStyle) -> Self {
Self { id, style, weight }
}
}
impl fmt::Display for FontFamily {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {} {}", self.id, self.weight, self.style)
}
}

View file

@ -4,6 +4,8 @@ use skia_safe::{
textlayout::{FontCollection, ParagraphBuilder},
};
use super::FontFamily;
#[derive(Debug, PartialEq, Clone)]
pub struct TextContent {
paragraphs: Vec<Paragraph>,
@ -38,15 +40,18 @@ impl TextContent {
self.paragraphs.push(p);
}
pub fn add_leaf(&mut self, text: &str) -> Result<(), String> {
pub fn add_leaf(
&mut self,
text: String,
font_family: FontFamily,
font_size: f32,
) -> Result<(), String> {
let paragraph = self
.paragraphs
.last_mut()
.ok_or("No paragraph to add text leaf to")?;
paragraph.add_leaf(TextLeaf {
text: text.to_owned(),
});
paragraph.add_leaf(TextLeaf::new(text, font_family, font_size));
Ok(())
}
@ -100,15 +105,29 @@ impl Paragraph {
#[derive(Debug, PartialEq, Clone)]
pub struct TextLeaf {
text: String,
font_family: FontFamily,
font_size: f32,
}
impl TextLeaf {
pub fn new(text: String, font_family: FontFamily, font_size: f32) -> Self {
Self {
text,
font_family,
font_size,
}
}
pub fn to_style(&self) -> skia::textlayout::TextStyle {
let mut style = skia::textlayout::TextStyle::default();
// TODO: read text style info from the shape
style.set_color(skia::Color::BLACK);
style.set_font_size(16.0);
style.set_font_size(self.font_size);
style.set_font_families(&[self.serialized_font_family()]);
style
}
fn serialized_font_family(&self) -> String {
format!("{}", self.font_family)
}
}

View file

@ -1 +1,2 @@
pub mod fonts;
pub mod text;

View file

@ -0,0 +1,35 @@
use crate::mem;
use crate::utils::uuid_from_u32_quartet;
use crate::with_state;
use crate::STATE;
use crate::shapes::FontFamily;
#[no_mangle]
pub extern "C" fn store_font(a: u32, b: u32, c: u32, d: u32, weight: u32, style: u8) {
with_state!(state, {
let id = uuid_from_u32_quartet(a, b, c, d);
let font_bytes = mem::bytes();
let family = FontFamily::new(id, weight, style.into());
let res = state.render_state().fonts_mut().add(family, &font_bytes);
match res {
Err(msg) => {
eprintln!("{}", msg);
}
_ => {}
}
});
}
#[no_mangle]
pub extern "C" fn is_font_uploaded(a: u32, b: u32, c: u32, d: u32, weight: u32, style: u8) -> bool {
with_state!(state, {
let id = uuid_from_u32_quartet(a, b, c, d);
let family = FontFamily::new(id, weight, style.into());
let res = state.render_state().fonts().has_family(&family);
return res;
});
}

View file

@ -1,4 +1,6 @@
use crate::mem;
use crate::shapes::FontFamily;
use crate::utils::uuid_from_u32_quartet;
use crate::with_current_shape;
use crate::STATE;
@ -20,14 +22,25 @@ pub extern "C" fn add_text_paragraph() {
}
#[no_mangle]
pub extern "C" fn add_text_leaf() {
pub extern "C" fn add_text_leaf(
a: u32,
b: u32,
c: u32,
d: u32,
weight: u32,
style: u8,
font_size: f32,
) {
let font_id = uuid_from_u32_quartet(a, b, c, d);
let font_family = FontFamily::new(font_id, weight, style.into());
let bytes = mem::bytes();
let text = unsafe {
String::from_utf8_unchecked(bytes) // TODO: handle this error
};
with_current_shape!(state, |shape: &mut Shape| {
let res = shape.add_text_leaf(&text);
let res = shape.add_text_leaf(text, font_family, font_size);
if let Err(err) = res {
eprintln!("{}", err);
}