#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub use oxitext_core::{
Bitmap, ColorBitmap, Decoration, DecorationLine, DecorationRect, FlowDirection,
FontVerticalMetrics, GlyphCluster, GlyphMetrics, LayoutConstraints, LineSpacing, OxiTextError,
ParagraphStyle, PositionedGlyph, RenderOutput, Rgba8, ShapedGlyph, ShapedRun, TextAlignment,
TextDecoration, TextRun, TextStyle, WritingMode,
};
pub use oxitext_layout::{LayoutEngine, LayoutResult, Line, LineMetrics, ParagraphMetrics};
#[cfg(feature = "sdf")]
pub mod sdf {
pub use oxitext_sdf::*;
}
#[cfg(feature = "font-subset")]
pub mod pdf_subset;
#[cfg(feature = "pure")]
pub use oxitext_raster::RasterBackend;
#[cfg(feature = "pure")]
pub use oxitext_shape::{ShapeBackend, ShapeDirection, ShapeFeature, ShapeRequest};
pub use oxitext_layout::bidi::{BidiParagraph, BidiRun};
pub use oxitext_layout::linebreak::{LineBreak, LineBreaker};
pub use oxitext_layout::vertical::{is_upright_in_vertical, VerticalMetrics};
pub use oxitext_layout::{detect_runs, GlyphEntry, TateChuYokoRun};
#[cfg(feature = "icu")]
pub mod icu {
pub use oxitext_icu::{
CaseMapper, CharProperties, CollateError, IcuCollator, IcuSegmenter, NormalizationForm,
Normalizer, ScriptRun, SegmentKind, TextScript,
};
}
#[cfg(feature = "pure")]
pub use oxitext_layout::SimpleLayouter;
#[cfg(feature = "pure")]
pub use oxitext_raster::FontdueRasterizer;
#[cfg(feature = "pure")]
pub use oxitext_shape::SwashShaper;
pub struct RenderResult {
pub glyphs: Vec<PositionedGlyph>,
pub bitmaps: Vec<Bitmap>,
pub outputs: Vec<RenderOutput>,
pub lines: Vec<Line>,
pub metrics: ParagraphMetrics,
pub decoration_rects: Vec<DecorationRect>,
}
impl RenderResult {
pub fn composite_to_rgba(
&self,
width: u32,
height: u32,
bg_color: Rgba8,
text_color: Rgba8,
) -> ColorBitmap {
let mut rgba = vec![0u8; (width as usize) * (height as usize) * 4];
for px in rgba.chunks_exact_mut(4) {
px[0] = bg_color.r;
px[1] = bg_color.g;
px[2] = bg_color.b;
px[3] = bg_color.a;
}
for (i, glyph) in self.glyphs.iter().enumerate() {
let ox = glyph.pos.0.round() as i32;
let oy = glyph.pos.1.round() as i32;
match self.outputs.get(i) {
Some(RenderOutput::Greyscale(bm)) => {
if bm.is_empty() {
continue;
}
blit_coverage(
&mut rgba,
width as i32,
height as i32,
bm,
ox,
oy,
text_color,
);
}
Some(RenderOutput::Color(cbm)) => {
if cbm.is_empty() {
continue;
}
blit_color(&mut rgba, width as i32, height as i32, cbm, ox, oy);
}
Some(RenderOutput::Lcd(lcd_bm)) => {
if lcd_bm.is_empty() {
continue;
}
let synthetic_bm = lcd_to_greyscale(lcd_bm);
blit_coverage(
&mut rgba,
width as i32,
height as i32,
&synthetic_bm,
ox,
oy,
text_color,
);
}
Some(RenderOutput::Sdf { .. }) | Some(RenderOutput::Msdf { .. }) => {
continue;
}
None => {
continue;
}
}
}
for rect in &self.decoration_rects {
let x0 = rect.x.max(0.0) as u32;
let y0 = rect.y.max(0.0) as u32;
let x1 = (rect.x + rect.width).ceil() as u32;
let y1 = (rect.y + rect.height).ceil() as u32;
for row in y0..y1.min(height) {
for col in x0..x1.min(width) {
let idx = (row * width + col) as usize * 4;
if idx + 3 < rgba.len() {
rgba[idx] = rect.color.r;
rgba[idx + 1] = rect.color.g;
rgba[idx + 2] = rect.color.b;
rgba[idx + 3] = rect.color.a;
}
}
}
}
ColorBitmap {
width,
height,
rgba,
}
}
#[cfg(feature = "png-output")]
pub fn to_png(
&self,
path: &std::path::Path,
width: u32,
height: u32,
bg: Rgba8,
fg: Rgba8,
) -> Result<(), OxiTextError> {
let canvas = self.composite_to_rgba(width, height, bg, fg);
let file = std::fs::File::create(path)
.map_err(|e| OxiTextError::Other(format!("png write: {e}")))?;
let w = std::io::BufWriter::new(file);
let mut encoder = png::Encoder::new(w, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder
.write_header()
.map_err(|e| OxiTextError::Other(format!("png write: {e}")))?;
writer
.write_image_data(&canvas.rgba)
.map_err(|e| OxiTextError::Other(format!("png write: {e}")))
}
}
fn blit_coverage(canvas: &mut [u8], cw: i32, ch: i32, bm: &Bitmap, ox: i32, oy: i32, color: Rgba8) {
for gy in 0..bm.height as i32 {
for gx in 0..bm.width as i32 {
let dx = ox + gx;
let dy = oy + gy;
if dx < 0 || dy < 0 || dx >= cw || dy >= ch {
continue;
}
let cov = bm.pixels[(gy as u32 * bm.width + gx as u32) as usize];
if cov == 0 {
continue;
}
let sa = (cov as u32 * color.a as u32 / 255) as u8;
if sa == 0 {
continue;
}
let idx = ((dy * cw + dx) * 4) as usize;
source_over(&mut canvas[idx..idx + 4], color.r, color.g, color.b, sa);
}
}
}
fn blit_color(canvas: &mut [u8], cw: i32, ch: i32, cbm: &ColorBitmap, ox: i32, oy: i32) {
for gy in 0..cbm.height as i32 {
for gx in 0..cbm.width as i32 {
let dx = ox + gx;
let dy = oy + gy;
if dx < 0 || dy < 0 || dx >= cw || dy >= ch {
continue;
}
let src_idx = ((gy as u32 * cbm.width + gx as u32) * 4) as usize;
if src_idx + 3 >= cbm.rgba.len() {
continue;
}
let sr = cbm.rgba[src_idx];
let sg = cbm.rgba[src_idx + 1];
let sb = cbm.rgba[src_idx + 2];
let sa = cbm.rgba[src_idx + 3];
if sa == 0 {
continue;
}
let dst_idx = ((dy * cw + dx) * 4) as usize;
source_over(&mut canvas[dst_idx..dst_idx + 4], sr, sg, sb, sa);
}
}
}
fn lcd_to_greyscale(lcd: &oxitext_core::LcdBitmap) -> Bitmap {
let pixel_count = (lcd.width * lcd.height) as usize;
let mut pixels = Vec::with_capacity(pixel_count);
for i in 0..pixel_count {
let base = i * 3;
if base + 2 < lcd.rgb.len() {
let avg =
(lcd.rgb[base] as u16 + lcd.rgb[base + 1] as u16 + lcd.rgb[base + 2] as u16) / 3;
pixels.push(avg as u8);
} else {
pixels.push(0);
}
}
Bitmap {
width: lcd.width,
height: lcd.height,
pixels,
}
}
fn source_over(dst: &mut [u8], sr: u8, sg: u8, sb: u8, sa: u8) {
let sa_f = sa as f32 / 255.0;
let da_f = dst[3] as f32 / 255.0;
let out_a = sa_f + da_f * (1.0 - sa_f);
if out_a < 1e-6 {
return;
}
let blend = |s: u8, d: u8| -> u8 {
let s_f = s as f32 / 255.0;
let d_f = d as f32 / 255.0;
let out = (s_f * sa_f + d_f * da_f * (1.0 - sa_f)) / out_a;
(out.clamp(0.0, 1.0) * 255.0).round() as u8
};
dst[0] = blend(sr, dst[0]);
dst[1] = blend(sg, dst[1]);
dst[2] = blend(sb, dst[2]);
dst[3] = (out_a * 255.0).round() as u8;
}
#[cfg(feature = "pure")]
fn extract_vertical_metrics(font_bytes: &[u8]) -> Option<FontVerticalMetrics> {
use oxifont::{FontFace as _, ParsedFace};
let arc: std::sync::Arc<[u8]> = font_bytes.to_vec().into();
let parsed = ParsedFace::parse(arc, 0).ok()?;
let m = parsed.metrics()?;
Some(FontVerticalMetrics {
units_per_em: m.units_per_em,
ascender: m.ascender,
descender: m.descender,
line_gap: m.line_gap,
})
}
#[cfg(feature = "pure")]
fn validate_font(font_bytes: &[u8]) -> Result<(), OxiTextError> {
use oxifont::ParsedFace;
let arc: std::sync::Arc<[u8]> = font_bytes.to_vec().into();
ParsedFace::parse(arc, 0).map_err(|e| OxiTextError::Other(format!("invalid font: {e}")))?;
Ok(())
}
#[cfg(feature = "pure")]
fn font_has_glyph(font_data: &[u8], ch: char) -> bool {
ttf_parser::Face::parse(font_data, 0)
.map(|face| face.glyph_index(ch).is_some())
.unwrap_or(false)
}
#[cfg(feature = "pure")]
pub fn best_font_for_char(ch: char, primary: &[u8], fallbacks: &[Vec<u8>]) -> usize {
if font_has_glyph(primary, ch) {
return 0;
}
for (i, fallback) in fallbacks.iter().enumerate() {
if font_has_glyph(fallback, ch) {
return i + 1;
}
}
0
}
#[cfg(feature = "pure")]
fn rasterize_single(
gid: u16,
font_data: &std::sync::Arc<[u8]>,
px_size: f32,
rasterizer: &FontdueRasterizer,
) -> RenderOutput {
use oxitext_raster::{detect_color_glyph_type, render_colr_v0, ColorGlyphType};
let color_type = detect_color_glyph_type(font_data, gid);
match color_type {
ColorGlyphType::ColrV0 | ColorGlyphType::ColrV1 => {
let dim = px_size.ceil() as u32;
let glyph_id = ttf_parser::GlyphId(gid);
if let Some(cbm) = render_colr_v0(font_data, glyph_id, dim, dim) {
return RenderOutput::Color(ColorBitmap {
width: cbm.width,
height: cbm.height,
rgba: cbm.rgba,
});
}
}
_ => {}
}
match rasterizer.raster(gid, font_data, px_size) {
Ok(bm) => RenderOutput::Greyscale(bm),
Err(_) => RenderOutput::Greyscale(Bitmap {
width: 0,
height: 0,
pixels: Vec::new(),
}),
}
}
#[cfg(all(feature = "pure", feature = "icu"))]
fn script_to_opentype_tag(script: oxitext_icu::TextScript) -> [u8; 4] {
use oxitext_icu::TextScript;
match script {
TextScript::Latin => *b"latn",
TextScript::Greek => *b"grek",
TextScript::Cyrillic => *b"cyrl",
TextScript::Arabic => *b"arab",
TextScript::Hebrew => *b"hebr",
TextScript::Han => *b"hani",
TextScript::Hiragana | TextScript::Katakana => *b"kana",
TextScript::Hangul => *b"hang",
TextScript::Thai => *b"thai",
TextScript::Devanagari => *b"deva",
TextScript::Common | TextScript::Inherited | TextScript::Other => *b"DFLT",
}
}
#[cfg(all(feature = "pure", feature = "icu"))]
fn itemize_by_script(text: &str) -> Vec<(usize, usize, [u8; 4])> {
use oxitext_icu::CharProperties;
let props = CharProperties::new();
props
.itemize(text)
.into_iter()
.map(|r| (r.start, r.end, script_to_opentype_tag(r.script)))
.collect()
}
#[cfg(feature = "pure")]
pub struct PipelineBuilder {
font_data: Option<Vec<u8>>,
}
#[cfg(feature = "pure")]
impl PipelineBuilder {
pub fn font(mut self, data: Vec<u8>) -> Self {
self.font_data = Some(data);
self
}
pub fn build(self) -> Result<Pipeline, OxiTextError> {
let data = self.font_data.ok_or(OxiTextError::FontNotFound)?;
Pipeline::from_bytes(&data)
}
}
#[cfg(feature = "pure")]
enum ShaperKind {
Default(Box<SwashShaper>),
Custom(Box<dyn ShapeBackend + Send + Sync>),
}
#[cfg(feature = "pure")]
pub struct Pipeline {
shaper: ShaperKind,
engine: LayoutEngine,
#[allow(dead_code)]
rasterizer: FontdueRasterizer,
font_data: std::sync::Arc<[u8]>,
vmetrics: Option<FontVerticalMetrics>,
fallback_fonts: Vec<std::sync::Arc<[u8]>>,
shape_cache_text: String,
shape_cache_style_hash: u64,
shape_cache_runs: Vec<ShapedRun>,
}
#[cfg(feature = "pure")]
impl Pipeline {
pub fn builder() -> PipelineBuilder {
PipelineBuilder { font_data: None }
}
pub fn available_features() -> &'static [&'static str] {
const FEATURES: &[&str] = &[
#[cfg(feature = "pure")]
"pure",
#[cfg(feature = "sdf")]
"sdf",
#[cfg(feature = "icu")]
"icu",
#[cfg(feature = "parallel")]
"parallel",
];
FEATURES
}
pub fn renders_color_glyphs(&self) -> bool {
true
}
fn style_hash(style: &TextStyle) -> u64 {
let mut h: u64 = 14_695_981_039_346_656_037;
let mix = |h: &mut u64, v: u64| {
*h ^= v;
*h = h.wrapping_mul(1_099_511_628_211);
};
mix(&mut h, style.font_size.to_bits() as u64);
mix(&mut h, style.flow_direction as u64);
h
}
fn invalidate_shape_cache(&mut self) {
self.shape_cache_text.clear();
self.shape_cache_style_hash = 0;
self.shape_cache_runs.clear();
}
pub fn new(font_db: &oxifont::FontDatabase) -> Result<Self, OxiTextError> {
use oxifont::FontCatalog as _;
let faces = font_db.faces();
let first = faces.first().ok_or(OxiTextError::FontNotFound)?;
let bytes = std::fs::read(&first.path)
.map_err(|e| OxiTextError::Other(format!("font read error: {e}")))?;
Self::from_bytes(&bytes)
}
pub fn new_with_system_font(family: &str) -> Result<Self, OxiTextError> {
use oxifont::{FontCatalog as _, FontDatabase, FontQuery};
let db = FontDatabase::system()
.map_err(|e| OxiTextError::Other(format!("font db scan failed: {e}")))?;
let face = db
.find_css(&FontQuery::new().family(family))
.or_else(|| db.faces().first())
.ok_or(OxiTextError::FontNotFound)?;
let bytes = std::fs::read(&face.path)
.map_err(|e| OxiTextError::Other(format!("read font {:?}: {e}", face.path)))?;
Self::from_bytes(&bytes)
}
pub fn from_bytes(font_bytes: &[u8]) -> Result<Self, OxiTextError> {
validate_font(font_bytes)?;
let vmetrics = extract_vertical_metrics(font_bytes);
Ok(Self {
shaper: ShaperKind::Default(Box::new(SwashShaper::new())),
engine: LayoutEngine::new(),
rasterizer: FontdueRasterizer::new(),
font_data: std::sync::Arc::from(font_bytes),
vmetrics,
fallback_fonts: Vec::new(),
shape_cache_text: String::new(),
shape_cache_style_hash: 0,
shape_cache_runs: Vec::new(),
})
}
pub fn set_fallback_fonts(&mut self, fonts: Vec<Vec<u8>>) {
self.fallback_fonts = fonts
.into_iter()
.map(|v| std::sync::Arc::from(v.as_slice()) as std::sync::Arc<[u8]>)
.collect();
self.invalidate_shape_cache();
}
pub fn shape_with_fallback(
&mut self,
text: &str,
px_size: f32,
) -> Result<oxitext_shape::ShapeResult, OxiTextError> {
let primary_data: Vec<u8> = self.font_data.as_ref().to_vec();
let fallback_owned: Vec<Vec<u8>> = self
.fallback_fonts
.iter()
.map(|arc| arc.as_ref().to_vec())
.collect();
if fallback_owned.is_empty() {
let glyphs = self.shape_segment(text, px_size, &primary_data, 0)?;
return Ok(oxitext_shape::ShapeResult {
glyphs,
script_detected: None,
direction: oxitext_shape::ShapeDirection::default(),
missing_codepoints: vec![],
cluster_boundaries: vec![],
});
}
let mut runs: Vec<(usize, usize, usize)> = Vec::new();
let mut current_font_idx: usize = best_font_for_char(
text.chars().next().unwrap_or('\0'),
&primary_data,
&fallback_owned,
);
let mut run_start: usize = 0;
for (byte_pos, ch) in text.char_indices() {
let font_idx = best_font_for_char(ch, &primary_data, &fallback_owned);
if font_idx != current_font_idx {
if byte_pos > run_start {
runs.push((run_start, byte_pos, current_font_idx));
}
current_font_idx = font_idx;
run_start = byte_pos;
}
}
if run_start < text.len() {
runs.push((run_start, text.len(), current_font_idx));
}
let mut all_glyphs: Vec<ShapedGlyph> = Vec::new();
for (start, end, font_idx) in runs {
let segment = &text[start..end];
let font_data_for_run: &Vec<u8> = if font_idx == 0 {
&primary_data
} else {
&fallback_owned[font_idx - 1]
};
let glyphs = self.shape_segment(segment, px_size, font_data_for_run, start)?;
all_glyphs.extend(glyphs);
}
Ok(oxitext_shape::ShapeResult {
glyphs: all_glyphs,
script_detected: None,
direction: oxitext_shape::ShapeDirection::default(),
missing_codepoints: vec![],
cluster_boundaries: vec![],
})
}
fn shape_segment(
&mut self,
segment: &str,
px_size: f32,
font_data: &[u8],
byte_offset: usize,
) -> Result<Vec<ShapedGlyph>, OxiTextError> {
let font_arc: std::sync::Arc<[u8]> = std::sync::Arc::from(font_data);
let mut glyphs: Vec<ShapedGlyph> = match &mut self.shaper {
ShaperKind::Default(s) => s
.shape(segment, std::sync::Arc::clone(&font_arc), px_size)
.map(|r| r.glyphs.into_vec())?,
ShaperKind::Custom(s) => s.shape_with_direction(&font_arc, segment, px_size, false),
};
let offset_u32 = byte_offset as u32;
for g in &mut glyphs {
g.cluster += offset_u32;
}
Ok(glyphs)
}
pub fn with_backend(
font_data: Vec<u8>,
shaper: Box<dyn ShapeBackend + Send + Sync>,
rasterizer: FontdueRasterizer,
) -> Result<Self, OxiTextError> {
validate_font(&font_data)?;
let vmetrics = extract_vertical_metrics(&font_data);
Ok(Self {
shaper: ShaperKind::Custom(shaper),
engine: LayoutEngine::new(),
rasterizer,
font_data: std::sync::Arc::from(font_data.as_slice()),
vmetrics,
fallback_fonts: Vec::new(),
shape_cache_text: String::new(),
shape_cache_style_hash: 0,
shape_cache_runs: Vec::new(),
})
}
pub fn font_metrics(&self) -> Option<&FontVerticalMetrics> {
self.vmetrics.as_ref()
}
pub fn has_rtl(&self, text: &str) -> bool {
let para = BidiParagraph::new(text, None);
para.is_rtl() || para.runs().iter().any(|r| r.level % 2 == 1)
}
fn shape_run_with_notdef_fallback(
&mut self,
text_slice: &str,
px_size: f32,
rtl: bool,
cluster_offset: u32,
) -> Result<ShapedRun, OxiTextError> {
let mut run = match &mut self.shaper {
ShaperKind::Default(s) => s.shape_with_direction(
text_slice,
std::sync::Arc::clone(&self.font_data),
px_size,
rtl,
)?,
ShaperKind::Custom(s) => {
let glyphs = s.shape_with_direction(&self.font_data, text_slice, px_size, rtl);
ShapedRun {
glyphs: glyphs.into(),
font_data: std::sync::Arc::clone(&self.font_data),
}
}
};
for g in &mut run.glyphs {
g.cluster += cluster_offset;
}
if self.fallback_fonts.is_empty() || run.glyphs.iter().all(|g| g.gid != 0) {
return Ok(run);
}
let cluster_offset_u32 = cluster_offset;
let fallbacks: Vec<std::sync::Arc<[u8]>> = self.fallback_fonts.clone();
let n = run.glyphs.len();
let mut idx = 0;
while idx < n {
if run.glyphs[idx].gid != 0 {
idx += 1;
continue;
}
let notdef_cluster = run.glyphs[idx].cluster;
let cluster_start = (notdef_cluster.saturating_sub(cluster_offset_u32)) as usize;
let next_cluster = run
.glyphs
.iter()
.skip(idx + 1)
.find(|g2| g2.cluster != notdef_cluster)
.map(|g2| (g2.cluster.saturating_sub(cluster_offset_u32)) as usize);
let cluster_end = next_cluster
.unwrap_or(text_slice.len())
.min(text_slice.len());
if cluster_start >= cluster_end {
idx += 1;
continue;
}
let slice = match text_slice.get(cluster_start..cluster_end) {
Some(s) if !s.is_empty() => s,
_ => {
idx += 1;
continue;
}
};
'fallback_loop: for fb_data in &fallbacks {
let fb_glyphs: Vec<ShapedGlyph> = match &mut self.shaper {
ShaperKind::Default(s) => {
match s.shape_with_direction(
slice,
std::sync::Arc::clone(fb_data),
px_size,
rtl,
) {
Ok(r) => r.glyphs.into_vec(),
Err(_) => continue,
}
}
ShaperKind::Custom(s) => s.shape_with_direction(fb_data, slice, px_size, rtl),
};
if let Some(winner) = fb_glyphs.into_iter().find(|g| g.gid != 0) {
run.glyphs[idx].gid = winner.gid;
run.glyphs[idx].x_advance = winner.x_advance;
run.glyphs[idx].y_advance = winner.y_advance;
run.glyphs[idx].x_offset = winner.x_offset;
run.glyphs[idx].y_offset = winner.y_offset;
run.font_data = std::sync::Arc::clone(fb_data);
break 'fallback_loop;
}
}
idx += 1;
}
Ok(run)
}
fn layout_dispatch(
&mut self,
text: &str,
runs: &[ShapedRun],
constraints: &LayoutConstraints,
alignment: TextAlignment,
) -> Result<LayoutResult, OxiTextError> {
#[cfg(feature = "icu")]
{
self.engine
.layout_cldr(text, runs, constraints, alignment, self.vmetrics.as_ref())
}
#[cfg(not(feature = "icu"))]
{
self.engine
.layout(text, runs, constraints, alignment, self.vmetrics.as_ref())
}
}
pub fn shape_and_layout(
&mut self,
text: &str,
style: &TextStyle,
) -> Result<LayoutResult, OxiTextError> {
let constraints = LayoutConstraints {
max_width: style.max_width,
font_size: style.font_size,
};
if matches!(self.shaper, ShaperKind::Custom(_)) {
let run = self.shape_run_with_notdef_fallback(text, style.font_size, false, 0)?;
return self.layout_dispatch(text, &[run], &constraints, style.alignment);
}
if style.flow_direction == FlowDirection::Vertical {
let run = self.shape_run_with_notdef_fallback(text, style.font_size, false, 0)?;
return self.engine.layout_vertical(
text,
&[run],
style.max_width, style.font_size,
self.vmetrics.as_ref(),
);
}
let style_hash = Self::style_hash(style);
if text == self.shape_cache_text
&& style_hash == self.shape_cache_style_hash
&& !self.shape_cache_runs.is_empty()
{
let cached: Vec<ShapedRun> = self.shape_cache_runs.clone();
return self.layout_dispatch(text, &cached, &constraints, style.alignment);
}
if !oxitext_layout::needs_bidi(text) {
#[cfg(feature = "icu")]
{
let script_runs = itemize_by_script(text);
if script_runs.len() > 1 {
let mut runs: Vec<ShapedRun> = Vec::with_capacity(script_runs.len());
for (run_start, run_end, script_tag) in &script_runs {
let run_text = &text[*run_start..*run_end];
if run_text.is_empty() {
continue;
}
if let ShaperKind::Default(s) = &mut self.shaper {
let req = oxitext_shape::ShapeRequest::builder()
.text(run_text)
.font_data(&self.font_data)
.px_size(style.font_size)
.script(*script_tag)
.build()
.map_err(|e| OxiTextError::Shaping(e.to_string()))?;
let mut glyphs = s.shape_request(&req)?;
for g in &mut glyphs {
g.cluster += *run_start as u32;
}
runs.push(ShapedRun {
glyphs: glyphs.into(),
font_data: std::sync::Arc::clone(&self.font_data),
});
}
}
self.shape_cache_text = text.to_owned();
self.shape_cache_style_hash = style_hash;
self.shape_cache_runs = runs.clone();
return self.layout_dispatch(text, &runs, &constraints, style.alignment);
}
}
let run = self.shape_run_with_notdef_fallback(text, style.font_size, false, 0)?;
let runs = vec![run];
self.shape_cache_text = text.to_owned();
self.shape_cache_style_hash = style_hash;
self.shape_cache_runs = runs.clone();
return self.layout_dispatch(text, &runs, &constraints, style.alignment);
}
let para = oxitext_layout::bidi::BidiParagraph::new(text, None);
let mut bidi_runs: Vec<oxitext_layout::bidi::BidiRun> = para.runs().to_vec();
bidi_runs.sort_by_key(|r| r.start);
let mut runs: Vec<ShapedRun> = Vec::with_capacity(bidi_runs.len());
for br in &bidi_runs {
let slice = &text[br.start..br.end];
if slice.is_empty() {
continue;
}
let rtl = br.level % 2 == 1;
let run =
self.shape_run_with_notdef_fallback(slice, style.font_size, rtl, br.start as u32)?;
runs.push(run);
}
self.shape_cache_text = text.to_owned();
self.shape_cache_style_hash = style_hash;
self.shape_cache_runs = runs.clone();
self.layout_dispatch(text, &runs, &constraints, style.alignment)
}
pub fn render_paragraph(
&mut self,
paragraphs: &[&str],
style: &TextStyle,
) -> Result<RenderResult, OxiTextError> {
let para_spacing = style.font_size * 0.5;
let mut all_glyphs: Vec<PositionedGlyph> = Vec::new();
let mut all_bitmaps: Vec<Bitmap> = Vec::new();
let mut all_outputs: Vec<RenderOutput> = Vec::new();
let mut all_lines: Vec<Line> = Vec::new();
let mut y_offset = 0.0_f32;
let mut total_width = 0.0_f32;
let mut total_lines: usize = 0;
let mut has_overflow = false;
for ¶_text in paragraphs {
if para_text.is_empty() {
y_offset += style.font_size + para_spacing;
total_lines += 1;
continue;
}
let result = self.render(para_text, style)?;
let glyph_base = all_glyphs.len();
for mut g in result.glyphs {
g.pos.1 += y_offset;
all_glyphs.push(g);
}
all_bitmaps.extend(result.bitmaps);
all_outputs.extend(result.outputs);
for mut line in result.lines {
line.metrics.baseline_y += y_offset;
line.glyph_start += glyph_base;
line.glyph_end += glyph_base;
all_lines.push(line);
}
total_width = total_width.max(result.metrics.total_width);
has_overflow |= result.metrics.overflow;
total_lines += result.metrics.line_count;
y_offset += result.metrics.total_height + para_spacing;
}
let metrics = ParagraphMetrics {
total_width,
total_height: y_offset,
line_count: total_lines,
overflow: has_overflow,
truncated: false,
};
Ok(RenderResult {
glyphs: all_glyphs,
bitmaps: all_bitmaps,
outputs: all_outputs,
lines: all_lines,
metrics,
decoration_rects: Vec::new(),
})
}
pub fn render_styled(
&mut self,
runs: &[TextRun],
max_width: f32,
) -> Result<RenderResult, OxiTextError> {
let mut unified_text = String::new();
let mut run_offsets: Vec<(usize, usize)> = Vec::with_capacity(runs.len()); for run in runs {
let start = unified_text.len();
unified_text.push_str(&run.text);
run_offsets.push((start, unified_text.len()));
}
let mut shaped_runs: Vec<ShapedRun> = Vec::with_capacity(runs.len());
for (run, &(byte_start, _byte_end)) in runs.iter().zip(run_offsets.iter()) {
if run.text.is_empty() {
continue;
}
let mut shaped = match &mut self.shaper {
ShaperKind::Default(s) => s.shape(
&run.text,
std::sync::Arc::clone(&run.font_data),
run.style.font_size,
)?,
ShaperKind::Custom(s) => {
let glyphs = s.shape(&run.font_data, &run.text, run.style.font_size);
ShapedRun {
glyphs: glyphs.into(),
font_data: std::sync::Arc::clone(&run.font_data),
}
}
};
for g in &mut shaped.glyphs {
g.cluster += byte_start as u32;
}
shaped_runs.push(shaped);
}
let first_style = runs.first().map(|r| &r.style).cloned().unwrap_or_default();
let constraints = LayoutConstraints {
max_width,
font_size: first_style.font_size,
};
let layout = self.engine.layout(
&unified_text,
&shaped_runs,
&constraints,
first_style.alignment,
self.vmetrics.as_ref(),
)?;
let (bitmaps, outputs) = self.rasterize_glyphs(&layout.glyphs)?;
Ok(RenderResult {
glyphs: layout.glyphs,
bitmaps,
outputs,
lines: layout.lines,
metrics: layout.metrics,
decoration_rects: Vec::new(),
})
}
pub fn measure(
&mut self,
text: &str,
style: &TextStyle,
) -> Result<ParagraphMetrics, OxiTextError> {
Ok(self.shape_and_layout(text, style)?.metrics)
}
fn rasterize_glyphs(
&self,
glyphs: &[PositionedGlyph],
) -> Result<(Vec<Bitmap>, Vec<RenderOutput>), OxiTextError> {
use std::collections::HashMap;
type DedupeKey = (u16, u32, usize);
let mut key_order: Vec<DedupeKey> = Vec::new();
let mut key_set: HashMap<DedupeKey, ()> = HashMap::new();
for g in glyphs {
let key: DedupeKey = (
g.gid,
g.font_size.to_bits(),
std::sync::Arc::as_ptr(&g.font_data) as *const u8 as usize,
);
if key_set.insert(key, ()).is_none() {
key_order.push(key);
}
}
let key_to_font: HashMap<DedupeKey, std::sync::Arc<[u8]>> = glyphs
.iter()
.map(|g| {
let key: DedupeKey = (
g.gid,
g.font_size.to_bits(),
std::sync::Arc::as_ptr(&g.font_data) as *const u8 as usize,
);
(key, std::sync::Arc::clone(&g.font_data))
})
.collect();
#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
let results: Vec<(DedupeKey, RenderOutput)> = {
use rayon::prelude::*;
key_order
.par_iter()
.map_init(FontdueRasterizer::new, |thread_rast, &key| {
key_to_font.get(&key).map(|font_data| {
let (gid, px_bits, _) = key;
let px_size = f32::from_bits(px_bits);
(key, rasterize_single(gid, font_data, px_size, thread_rast))
})
})
.filter_map(|x| x)
.collect()
};
#[cfg(not(all(feature = "parallel", not(target_arch = "wasm32"))))]
let results: Vec<(DedupeKey, RenderOutput)> = key_order
.iter()
.filter_map(|&key| {
let font_data = key_to_font.get(&key)?;
let (gid, px_bits, _) = key;
let px_size = f32::from_bits(px_bits);
let output = rasterize_single(gid, font_data, px_size, &self.rasterizer);
Some((key, output))
})
.collect();
let dedup_map: HashMap<DedupeKey, RenderOutput> = results.into_iter().collect();
let mut bitmaps: Vec<Bitmap> = Vec::with_capacity(glyphs.len());
let mut outputs: Vec<RenderOutput> = Vec::with_capacity(glyphs.len());
for g in glyphs {
let key: DedupeKey = (
g.gid,
g.font_size.to_bits(),
std::sync::Arc::as_ptr(&g.font_data) as *const u8 as usize,
);
let output = dedup_map
.get(&key)
.cloned()
.unwrap_or(RenderOutput::Greyscale(Bitmap {
width: 0,
height: 0,
pixels: Vec::new(),
}));
let bm = match &output {
RenderOutput::Greyscale(b) => b.clone(),
_ => Bitmap {
width: 0,
height: 0,
pixels: Vec::new(),
},
};
bitmaps.push(bm);
outputs.push(output);
}
Ok((bitmaps, outputs))
}
pub fn render(&mut self, text: &str, style: &TextStyle) -> Result<RenderResult, OxiTextError> {
let layout = self.shape_and_layout(text, style)?;
let (bitmaps, outputs) = self.rasterize_glyphs(&layout.glyphs)?;
Ok(RenderResult {
glyphs: layout.glyphs,
bitmaps,
outputs,
lines: layout.lines,
metrics: layout.metrics,
decoration_rects: Vec::new(),
})
}
pub fn render_to_image(
&mut self,
text: &str,
style: &TextStyle,
bg_color: Rgba8,
text_color: Rgba8,
) -> Result<ColorBitmap, OxiTextError> {
let result = self.render(text, style)?;
let width = if style.max_width > 0.0 {
style.max_width.ceil() as u32
} else {
result.metrics.total_width.ceil() as u32
}
.max(1);
let height = result.metrics.total_height.ceil() as u32 + style.font_size.ceil() as u32;
let height = height.max(1);
Ok(result.composite_to_rgba(width, height, bg_color, text_color))
}
pub fn benchmark(
&mut self,
text: &str,
style: &TextStyle,
iterations: usize,
) -> std::time::Duration {
let iterations = iterations.max(1);
let start = std::time::Instant::now();
for _ in 0..iterations {
let _ = self.measure(text, style);
}
start.elapsed() / iterations as u32
}
pub fn profile(
&mut self,
text: &str,
style: &TextStyle,
) -> (
std::time::Duration,
std::time::Duration,
std::time::Duration,
) {
let t0 = std::time::Instant::now();
let _ = self.shape_and_layout(text, style);
let t1 = std::time::Instant::now();
let _ = self.measure(text, style);
let t2 = std::time::Instant::now();
let shape_layout = t1 - t0;
let total = t2 - t0;
let remainder = total.saturating_sub(shape_layout);
(shape_layout, remainder, total)
}
}
#[cfg(all(feature = "pure", feature = "sdf"))]
impl Pipeline {
pub fn render_to_sdf_atlas(
&mut self,
text: &str,
style: &TextStyle,
atlas: &mut oxitext_sdf::SdfAtlas,
) -> Result<(oxitext_layout::LayoutResult, Vec<u16>), OxiTextError> {
let layout_result = self.shape_and_layout(text, style)?;
let mut seen = std::collections::HashSet::<(u16, u32)>::new();
let mut glyph_set: Vec<(u16, f32)> = Vec::new();
for g in &layout_result.glyphs {
if seen.insert((g.gid, g.font_size.to_bits())) {
glyph_set.push((g.gid, g.font_size));
}
}
let font_bytes: &[u8] = &self.font_data;
let mut packed_ids: Vec<u16> = Vec::new();
for (glyph_id, px_size) in glyph_set {
if atlas.uv_map.contains_key(&glyph_id) {
continue;
}
let maybe_tile = oxitext_sdf::glyph_to_sdf_tile_analytic(
font_bytes, glyph_id, px_size, 64, 4.0, )
.map_err(|e| OxiTextError::Other(format!("sdf tile error: {e}")))?;
if let Some(tile) = maybe_tile {
if atlas.add_tile(&tile).is_some() {
packed_ids.push(glyph_id);
}
}
}
Ok((layout_result, packed_ids))
}
}
pub mod prelude {
pub use oxitext_core::{
Bitmap, ColorBitmap, Decoration, FlowDirection, GlyphMetrics, LayoutConstraints,
OxiTextError, ParagraphStyle, PositionedGlyph, RenderOutput, Rgba8, TextAlignment,
TextStyle, WritingMode,
};
pub use oxitext_layout::{LayoutResult, Line, ParagraphMetrics};
#[cfg(feature = "pure")]
pub use crate::{Pipeline, RenderResult};
}