You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何计算文本像素宽度?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(&current_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 11:44:51