penpot/render-wasm/src/shapes/text_paths.rs
alonso.torres a641cbab5d ✨ Render Japanese text layouts in WASM
Lay out horizontal and vertical Japanese text with kinsoku, ruby,
emphasis, warichu, tate-chu-yoko and proportional font features.
Carry the layout through glyph paths, SVG export and text selection.

Update the Rust and ClojureScript binary formats together, including
the editor bridge and headless exporter. Resolve CJK fallback fonts
and add renderer tests and font fixtures.
2026-11-03 17:36:40 +01:00

101 lines
3.2 KiB
Rust

use crate::render::text::decoration_segments;
use crate::shapes::text::{vertical_align_offset, Paragraph, TextContent};
use crate::shapes::VerticalAlign;
use skia_safe::{
self as skia,
textlayout::{paragraph::VisitorInfo, Paragraph as SkiaParagraph},
Point,
};
use std::ops::Deref;
pub struct TextPaths(TextContent);
impl TextPaths {
pub fn new(text_content: TextContent) -> Self {
Self(text_content)
}
pub fn get_paths(&self, vertical_align: VerticalAlign) -> Vec<skia::Path> {
if self.0.is_vertical() {
return crate::shapes::text_vertical::vertical_text_paths(
&self.0,
vertical_align,
true,
)
.into_iter()
.map(|(path, _)| path)
.collect();
}
let layout_width = self.0.get_width(self.bounds.width());
let mut paragraph_builders = self.0.paragraph_builder_group_from_text(None);
let mut paragraphs: Vec<SkiaParagraph> = paragraph_builders
.iter_mut()
.filter_map(|group| group.first_mut())
.map(|paragraph_builder| {
let mut paragraph = paragraph_builder.build();
paragraph.layout(layout_width);
paragraph
})
.collect();
let total_height: f32 = paragraphs.iter().map(|p| p.height()).sum();
let mut offset_y = self.bounds.y()
+ vertical_align_offset(self.bounds.height(), total_height, vertical_align);
let mut paths = Vec::new();
for (paragraph, text_paragraph) in paragraphs.iter_mut().zip(self.0.paragraphs()) {
let origin = Point::new(self.bounds.x(), offset_y);
Self::collect_paragraph_paths(paragraph, text_paragraph, origin, &mut paths);
offset_y += paragraph.height();
}
paths
}
fn collect_paragraph_paths(
paragraph: &mut SkiaParagraph,
text_paragraph: &Paragraph,
origin: Point,
paths: &mut Vec<skia::Path>,
) {
let offsets = crate::shapes::HorizontalOffsets::new(text_paragraph);
for deco in decoration_segments(paragraph, text_paragraph, &offsets, origin.x, origin.y) {
let mut builder = skia::PathBuilder::new();
builder.add_rect(deco.rect(), None, None);
paths.push(builder.detach());
}
paragraph.visit(|_: usize, info: Option<&VisitorInfo>| {
let Some(info) = info else {
return;
};
let font = info.font();
let run_origin = origin + info.origin();
let mut builder = skia::PathBuilder::new();
let mut has_glyphs = false;
for (glyph, position) in info.glyphs().iter().zip(info.positions().iter()) {
let Some(glyph_path) = font.get_path(*glyph) else {
continue;
};
builder.add_path(&glyph_path.with_offset(run_origin + *position), None);
has_glyphs = true;
}
if has_glyphs {
paths.push(builder.detach());
}
});
}
}
impl Deref for TextPaths {
type Target = TextContent;
fn deref(&self) -> &Self::Target {
&self.0
}
}