如何计算文本像素宽度?Rust SVG文本换行实现求助
Rust实现文本换行并生成SVG 标签的解决方案
现有ttf-parser代码的问题分析
- 直接用
unwrap()处理glyph_index和glyph_bounding_box,遇到空格等无明确边界盒的字符会崩溃 - 用glyph的边界盒宽度计算总宽度错误,应该使用水平前进量(horizontal advance)——这才是字符实际占用的水平空间(包含字符宽度和间距)
- 缩放计算时类型转换顺序错误,导致精度丢失
修复后的ttf-parser文本宽度计算函数
use ttf_parser as ttf; use anyhow::{Context, Result}; pub fn get_text_width( text: &str, font_size: f32, ttf_path: &str, ) -> Result<f32> { let font_data = std::fs::read(ttf_path).context("读取TTF文件失败")?; let face = ttf::Face::parse(&font_data, 0).context("解析TTF文件失败")?; let units_per_em = face.units_per_em() as f32; let scale = font_size / units_per_em; let mut total_width = 0.0; for c in text.chars() { let glyph_id = face.glyph_index(c) .with_context(|| format!("无法获取字符'{}'的Glyph ID", c))?; // 优先使用水平前进量,无值时回退到边界盒宽度 let advance = if let Some(adv) = face.glyph_hor_advance(glyph_id) { adv as f32 } else { face.glyph_bounding_box(glyph_id) .with_context(|| format!("无法获取字符'{}'的尺寸信息", c))? .width() as f32 }; total_width += advance * scale; } Ok(total_width) }
使用cosmic-text简化文本布局与换行
cosmic-text专门处理文本布局、换行、度量,无需手动计算字符宽度,以下是完整实现:
依赖配置(Cargo.toml)
cosmic-text = "0.9" anyhow = "1.0"
实现函数
use cosmic_text::{Attrs, Buffer, Family, FontSystem, Layout, LineHeight, Style, Weight}; use anyhow::{Context, Result}; pub fn generate_wrapped_svg_text( text: &str, ttf_path: &str, font_size: u32, line_width: u32, x: f32, y: f32, ) -> Result<String> { // 初始化字体系统 let mut font_system = FontSystem::new(); // 加载自定义TTF字体 let _font = font_system.load_font_file(std::path::Path::new(ttf_path)) .context("加载TTF字体失败")?; // 设置文本属性 let attrs = Attrs::new() .family(Family::Name("Custom Font")) .font_size(font_size as f32) .weight(Weight::NORMAL) .style(Style::Normal); // 创建文本缓冲区并启用自动换行 let mut buffer = Buffer::new(&mut font_system); buffer.set_size(line_width as f32, f32::INFINITY); buffer.set_text(text, attrs, Layout::Wrap); buffer.shape_until_scroll(); // 生成SVG标签组 let mut svg_text = format!("<text x=\"{x}\" y=\"{y}\" font-family=\"Custom Font\" font-size=\"{font_size}\">"); // 计算行高(1.2倍字号为默认行间距,可调整) let line_height = LineHeight::Relative(1.2).to_absolute(font_size as f32); // 遍历行生成<tspan>标签(SVG标准换行方式) for (i, line) in buffer.lines().enumerate() { let line_text = line.text(); if i == 0 { svg_text.push_str(line_text); } else { svg_text.push_str(&format!("<tspan x=\"{x}\" dy=\"{line_height}\">{line_text}</tspan>")); } } svg_text.push_str("</text>"); Ok(svg_text) }
说明
- cosmic-text自动处理换行、字符间距、字体度量等细节,无需手动实现换行逻辑
- 用
<tspan>配合x/dy属性是SVG规范的文本换行方式,比多个<text>标签更简洁
手动换行逻辑(基于修复后的ttf-parser)
若不想引入额外依赖,可通过修复后的宽度计算函数手动实现换行:
use anyhow::Result; pub fn wrap_text( text: &str, font_size: f32, ttf_path: &str, line_width: u32, ) -> Result<Vec<String>> { let mut lines = Vec::new(); let mut current_line = String::new(); let line_width_f32 = line_width as f32; let space_width = get_text_width(" ", font_size, ttf_path)?; for word in text.split_whitespace() { let word_width = get_text_width(word, font_size, ttf_path)?; let current_line_width = get_text_width(¤t_line, font_size, ttf_path)?; let new_width = if current_line.is_empty() { word_width } else { current_line_width + space_width + word_width }; if new_width <= line_width_f32 { if !current_line.is_empty() { current_line.push(' '); } current_line.push_str(word); } else { lines.push(current_line); current_line = word.to_string(); } } if !current_line.is_empty() { lines.push(current_line); } Ok(lines) } // 生成SVG标签 pub fn generate_svg_text_from_lines( lines: &[String], x: f32, y: f32, font_size: u32, font_family: &str, ) -> String { let mut svg = String::new(); let line_height = font_size as f32 * 1.2; for (i, line) in lines.iter().enumerate() { let current_y = y + (i as f32 * line_height); svg.push_str(&format!( "<text x=\"{x}\" y=\"{current_y}\" font-family=\"{font_family}\" font-size=\"{font_size}\">{line}</text>\n" )); } svg }
内容的提问来源于stack exchange,提问作者Shazamo Morebucks
相关产品推荐
相关产品推荐

