use crate::{
builders::TextSpan,
layout::{
FontRegistry,
RenderContext,
Stencil,
StencilScope,
StencilType,
Typography,
BASE_FONT_SIZE,
BASE_LINE_HEIGHT,
DEFAULT_FONT_FAMILY,
},
paint::{
write_fill_path,
Iri,
ResourceIri,
ScaledRadii,
},
primitives::{
Color,
FontStyle,
FontWeight,
Mask,
PaintStack,
},
utils::{
encode_image,
ElementWriter,
PathWriter,
},
};
use base64::{
engine::general_purpose::STANDARD as BASE64,
Engine,
};
use cosmic_text::{
Attrs,
Buffer,
Command,
Family,
Metrics,
Shaping,
};
use parking_lot::Mutex;
use png::EncodingError;
use std::{
fmt::{
Formatter,
Write,
},
sync::Arc,
};
use swash::scale::image::Content;
use taffy::prelude::*;
use thiserror::Error;
use zeno::Point;
const DEFAULT_COLOR: Color = Color::rgb(0, 0, 0);
#[derive(Error, Debug)]
pub enum TextVectorizeError {
#[error("failed to write to the output stream")]
Write(#[from] std::fmt::Error),
#[error("failed to encode emoji image")]
EncodeEmoji(#[from] EncodingError),
}
#[derive(Debug, Default)]
pub(crate) enum GlyphRenderMode {
#[default]
All,
Vector,
Bitmap,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct TextMeta {
spans: Vec<TextSpan>,
buffer: Option<Buffer>,
typography: Typography,
stencil: Stencil,
}
impl TextMeta {
pub(crate) fn new(spans: Vec<TextSpan>) -> Self {
Self {
spans,
..Default::default()
}
}
pub(crate) fn typography(&mut self, typography: Typography) {
self.typography = typography;
}
pub(crate) fn stencil_paint(&mut self, value: PaintStack) {
self.stencil.paint = value;
}
pub(crate) fn stencil_scope(&mut self, value: StencilScope) {
self.stencil.scope = value;
}
pub(crate) fn stencil_type(&mut self, value: StencilType) {
self.stencil.r#type = value;
}
pub(crate) fn measure(
&mut self,
known_dimensions: Size<Option<f32>>,
available_space: Size<AvailableSpace>,
fonts: Arc<Mutex<FontRegistry>>,
) -> Size<f32> {
if let Size {
width: Some(width),
height: Some(height),
} = known_dimensions
{
return Size { width, height };
}
let mut fonts = fonts.lock();
self.init_buffer(&mut fonts);
let Some(ref mut buffer) = self.buffer else {
return Size::zero();
};
let width_constraint = known_dimensions.width.or(match available_space.width {
AvailableSpace::MinContent => Some(0.0),
AvailableSpace::MaxContent => None,
AvailableSpace::Definite(width) => Some(width),
});
buffer.set_size(&mut fonts.system, width_constraint, None);
buffer.shape_until_scroll(&mut fonts.system, false);
let (width, total_lines) = buffer
.layout_runs()
.fold((0.0, 0usize), |(width, total_lines), run| {
(run.line_w.max(width), total_lines + 1)
});
let height = total_lines as f32 * buffer.metrics().line_height;
Size { width, height }
}
pub(crate) fn render<W>(
&self,
ctx: &mut RenderContext<W>,
layout: Layout,
) -> Result<(), TextVectorizeError>
where
W: Write,
{
if self.stencil.is_none() {
self.render_text(ctx.out, &ctx.scene.fonts, GlyphRenderMode::All)
} else {
let Size { width, height } = layout.size;
let mask = {
Mask::build(|out| {
self.render_text(
out,
&ctx.scene.fonts,
if matches!(self.stencil.scope, StencilScope::VectorGlyphs) {
GlyphRenderMode::Vector
} else {
GlyphRenderMode::All
},
)
.map_err(|_| std::fmt::Error)
})?
.r#type(self.stencil.r#type.into())
};
self.render_stencil(ctx, mask.iri(), width, height)?;
ctx.scene.resources.lock().get_or_add_resource(mask.into());
if matches!(self.stencil.scope, StencilScope::VectorGlyphs) {
self.render_text(ctx.out, &ctx.scene.fonts, GlyphRenderMode::Bitmap)?;
}
Ok(())
}
}
fn init_buffer(&mut self, fonts: &mut FontRegistry) {
if self.buffer.is_some() {
return;
}
let mut spans = Vec::with_capacity(self.spans.len());
for (idx, span) in self.spans.iter_mut().enumerate() {
if span.hidden {
continue;
}
span.typography.cascade_from(&self.typography);
let (attrs, _) = typography_to_attrs(&mut span.typography, fonts);
spans.push((span.content.as_str(), attrs.metadata(idx)));
}
let mut root_tp = self.typography.clone();
let (root_attrs, root_metrics) = typography_to_attrs(&mut root_tp, fonts);
let mut buf = Buffer::new_empty(root_metrics);
let mut brw = buf.borrow_with(&mut fonts.system);
if let Some(wrap) = self.typography.wrap {
brw.set_wrap(wrap.into());
}
if let Some(ellipsize) = self.typography.ellipsize {
brw.set_ellipsize(ellipsize.into());
}
brw.set_rich_text(
spans,
&root_attrs,
Shaping::Advanced,
self.typography.align.map(Into::into),
);
self.buffer = Some(brw.to_owned());
}
fn render_text<W>(
&self,
out: &mut W,
font_registry: &Arc<Mutex<FontRegistry>>,
mode: GlyphRenderMode,
) -> Result<(), TextVectorizeError>
where
W: Write,
{
let Some(ref buffer) = self.buffer else {
return Ok(());
};
let mut font_registry = font_registry.lock();
let FontRegistry {
swash_cache: cache,
system: font_system,
..
} = &mut *font_registry;
let skip_vector = matches!(mode, GlyphRenderMode::Bitmap);
let skip_bitmap = matches!(mode, GlyphRenderMode::Vector);
for run in buffer.layout_runs() {
let line_y = run.line_y;
for glyph in run.glyphs.iter() {
let physical = glyph.physical((0.0, 0.0), 1.0);
let glyph_x = physical.x as f32;
let glyph_y = physical.y as f32;
let cache_key = physical.cache_key;
if let Some(outline_commands) = cache
.get_outline_commands(font_system, cache_key)
.filter(|x| is_drawable(*x))
{
if skip_vector {
continue;
}
ElementWriter::new(out, "path")?
.attr(
"fill",
(self
.spans
.get(glyph.metadata)
.and_then(|span| span.typography.color.clone())
.unwrap_or(DEFAULT_COLOR.into()),),
)?
.write_attr("d", |out| {
let mut d = PathWriter::new(out);
for command in outline_commands.iter() {
match *command {
Command::MoveTo(Point { x, y }) => {
d.move_to(x + glyph_x, line_y + glyph_y - y)?;
}
Command::LineTo(Point { x, y }) => {
d.line_to(x + glyph_x, line_y + glyph_y - y)?;
}
Command::CurveTo(
Point { x: x1, y: y1 },
Point { x: x2, y: y2 },
Point { x, y },
) => {
d.curve_to(
x1 + glyph_x,
line_y + glyph_y - y1,
x2 + glyph_x,
line_y + glyph_y - y2,
x + glyph_x,
line_y + glyph_y - y,
)?;
}
Command::QuadTo(Point { x: cx, y: cy }, Point { x, y }) => {
d.quad_to(
cx + glyph_x,
line_y + glyph_y - cy,
x + glyph_x,
line_y + glyph_y - y,
)?;
}
Command::Close => d.close()?,
}
}
Ok(())
})?
.close()?;
} else if let Some(image) = cache.get_image(font_system, cache_key) {
if !skip_bitmap && image.content == Content::Color {
ElementWriter::new(out, "image")?
.attr(
"href",
(format_args!(
"data:image/png;base64,{}",
BASE64.encode(encode_image(&image)?)
),),
)?
.attrs([
("x", glyph_x + image.placement.left as f32),
("y", line_y + glyph_y - image.placement.top as f32),
("width", image.placement.width as f32),
("height", image.placement.height as f32),
])?
.close()?;
}
}
}
}
Ok(())
}
fn render_stencil<W>(
&self,
ctx: &mut RenderContext<W>,
mask_iri: Iri,
width: f32,
height: f32,
) -> std::fmt::Result
where
W: Write,
{
self.stencil.paint.render(
ctx,
|out| write_fill_path(out, width, height, ScaledRadii::default()),
|out| write_fill_path(out, width, height, ScaledRadii::default()),
|layer, is_use_element| {
if is_use_element {
Ok(layer)
} else {
layer.attr("mask", (format_args!("url(#{mask_iri})"),))
}
},
|group| group.attr("mask", (format_args!("url(#{mask_iri})"),)),
)
}
}
impl std::fmt::Display for TextMeta {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(
&self
.spans
.iter()
.map(|x| x.content.clone())
.collect::<Vec<_>>()
.join(""),
)
}
}
fn is_drawable(cmds: &[Command]) -> bool {
for cmd in cmds {
match cmd {
Command::LineTo(_) | Command::QuadTo(_, _) | Command::CurveTo(_, _, _) => return true,
_ => {}
}
}
false
}
fn typography_to_attrs<'a>(
tp: &'a mut Typography,
fonts: &mut FontRegistry,
) -> (Attrs<'a>, Metrics) {
let metrics = Metrics {
font_size: tp.size.unwrap_or(BASE_FONT_SIZE),
line_height: tp.line_height.unwrap_or(BASE_LINE_HEIGHT),
};
let alias = tp.family.clone().unwrap_or(fonts.get_default_family());
tp.resolved_family = fonts
.resolve_family_name(alias.as_str())
.unwrap_or(DEFAULT_FONT_FAMILY.to_string());
let family = match tp.resolved_family.as_str() {
"sans-serif" => Family::SansSerif,
"serif" => Family::Serif,
"mono" | "monospace" => Family::Monospace,
name => Family::Name(name),
};
let mut attrs = Attrs::new()
.family(family)
.metrics(metrics)
.style(tp.style.unwrap_or(FontStyle::Normal).into())
.weight(tp.weight.unwrap_or(FontWeight::Normal).into());
if let Some(letter_spacing) = tp.letter_spacing {
attrs = attrs.letter_spacing(letter_spacing);
}
(attrs, metrics)
}