使用Rust与PrintPDF渲染阿拉伯文本异常,求解决方案
在Rust中使用PrintPDF渲染阿拉伯文本显示异常的解决方法
尝试用Rust的PrintPDF crate渲染阿拉伯文本「السلام عليكم」,但输出字符顺序颠倒且字形未正确连写,不符合预期。
环境信息
Rust版本
rustc 1.85.0 (4d91de4e4 2025-02-17)
Cargo依赖配置
[dependencies] allsorts = "0.15.1" clap = { version = "4.5.32", features = ["derive"] } fontdue = "0.9.3" harfbuzz_rs = "2.0.1" image = "0.24.9" printpdf = { version = "0.8.2", features = ["png", "jpeg"] } rustybuzz = "0.20.1" unicode-bidi = "0.3.18"
问题代码
extern crate image as image_official; extern crate printpdf; use clap::Parser; use printpdf::*; use std::path::Path; use structs::PageSize; use std::fs::{self, File}; use std::io::{BufWriter, Write}; pub mod structs; pub mod utils; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { #[arg(short, long)] name: String, #[arg(short, long)] amount: f64, #[arg(short, long)] date: String, #[arg(short, long)] logo: String, #[arg(short, long)] output_path: String, } static NOTO_SANS_TTF: &[u8] = include_bytes!("../fonts/noto_sans/NotoSans-VariableFont_wdth,wght.ttf"); static NOTO_AR_TTF: &[u8] = include_bytes!("../fonts/Noto_Sans_Arabic/NotoSansArabic-VariableFont_wdth,wght.ttf"); fn generate_receipt(_name: &str, _amount: f64, _date: &str, _logo: &str, output_path: &str) { let page_size = PageSize::<f32>::a4(); let path = Path::new(output_path); if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("Failed to create directory"); } let mut doc = PdfDocument::new("Receipt"); let font_noto = ParsedFont::from_bytes(NOTO_SANS_TTF, 0, &mut Vec::new()).unwrap(); let _font_noto_id = doc.add_font(&font_noto); let font_ar = ParsedFont::from_bytes(NOTO_AR_TTF, 0, &mut Vec::new()).unwrap(); let font_ar_id = doc.add_font(&font_ar); let ops = vec![ Op::SaveGraphicsState, Op::StartTextSection, Op::SetTextCursor { pos: Point::new(Mm(20.0), Mm(270.0)), }, Op::SetFontSizeBuiltinFont { size: Pt(24.0), font: BuiltinFont::Helvetica, }, Op::SetLineHeight { lh: Pt(24.0) }, Op::SetFillColor { col: Color::Rgb(Rgb { r: 0.0, g: 0.0, b: 0.8, icc_profile: None, }), }, Op::WriteTextBuiltinFont { items: vec![TextItem::Text("Hello from Helvetica!".to_string())], font: BuiltinFont::Helvetica, }, Op::AddLineBreak, Op::SetFontSizeBuiltinFont { size: Pt(18.0), font: BuiltinFont::TimesRoman, }, Op::SetLineHeight { lh: Pt(18.0) }, Op::SetFillColor { col: Color::Rgb(Rgb { r: 0.8, g: 0.0, b: 0.0, icc_profile: None, }), }, Op::WriteTextBuiltinFont { items: vec![TextItem::Text("This is Times Roman font".to_string())], font: BuiltinFont::TimesRoman, }, Op::AddLineBreak, Op::SetFontSize { size: Pt(14.0), font: font_ar_id.clone(), }, Op::SetLineHeight { lh: Pt(14.0) }, Op::SetFillColor { col: Color::Rgb(Rgb { r: 0.0, g: 0.6, b: 0.0, icc_profile: None, }), }, Op::SetTextRenderingMode { mode: TextRenderingMode::Fill, }, Op::WriteText { items: vec![TextItem::Text("السلام عليكم".to_string())], font: font_ar_id.clone(), }, Op::EndTextSection, Op::RestoreGraphicsState, ]; let page1 = PdfPage::new(Mm(page_size.width), Mm(page_size.height), ops); let pdf_bytes: Vec<u8> = doc .with_pages(vec![page1]) .save(&PdfSaveOptions::default(), &mut Vec::new()); let file = File::create(output_path).expect("Failed to create file"); let mut writer = BufWriter::new(file); writer .write_all(&pdf_bytes) .expect("Failed to write PDF data"); println!("Created text_example.pdf"); } fn main() { let args = Args::parse(); generate_receipt( &args.name, args.amount, &args.date, &args.logo, &args.output_path, ); println!("Receipt generated at {}", args.output_path); }
当前输出效果

解决方法
阿拉伯语属于**从右到左(RTL)**的双向语言,且存在字符连写的字形规则,PrintPDF默认不处理这些逻辑,需要手动添加文本排序和字形整形步骤:
1. 处理双向文本排序
使用unicode-bidi crate将RTL文本调整为正确的显示顺序:
use unicode_bidi::{BidiInfo, Level}; fn reorder_rtl_text(text: &str) -> String { let bidi_info = BidiInfo::new(text, None); let mut result = String::new(); for para in bidi_info.paragraphs { let line = para.range.clone(); let levels = bidi_info.reorder_line(para, line); let mut chars: Vec<_> = text.chars().collect(); unicode_bidi::reorder_indices(&mut chars, &levels); result.extend(chars); } result }
2. 字形整形(处理字符连写)
使用rustybuzz crate对阿拉伯文本进行字形整形,生成正确的字形索引和位置偏移:
use rustybuzz::{Face, FontBuffer, GlyphPosition, GlyphInfo}; fn shape_arabic_text(text: &str, font_data: &[u8]) -> (Vec<u16>, Vec<GlyphPosition>) { let face = Face::from_slice(font_data, 0).unwrap(); let mut buffer = FontBuffer::new(); buffer.set_text(text, &[]); buffer.shape(&face, &rustybuzz::ShaperOptions::default()); let glyph_infos = buffer.glyph_infos(); let glyph_positions = buffer.glyph_positions(); let glyph_ids: Vec<u16> = glyph_infos.iter().map(|info| info.glyph_id).collect(); (glyph_ids, glyph_positions.to_vec()) }
3. 修改生成PDF的代码
将上述工具函数整合到原代码中,替换直接写入文本的逻辑,手动处理字形绘制:
// 在generate_receipt函数中,替换阿拉伯文本相关的Op逻辑 let arabic_text = "السلام عليكم"; // 1. 调整文本顺序 let reordered_text = reorder_rtl_text(arabic_text); // 2. 字形整形 let (glyph_ids, glyph_positions) = shape_arabic_text(&reordered_text, NOTO_AR_TTF); // 计算文本起始位置(RTL文本需要从右侧开始定位) let start_x = Mm(200.0); // 调整合适的右侧起始位置 let start_y = Mm(270.0 - 24.0 - 18.0 - 14.0); // 对应之前的换行位置 let mut text_ops = vec![ Op::SaveGraphicsState, Op::StartTextSection, Op::SetTextCursor { pos: Point::new(start_x, start_y) }, Op::SetFontSize { size: Pt(14.0), font: font_ar_id.clone() }, Op::SetFillColor { col: Color::Rgb(Rgb { r: 0.0, g: 0.6, b: 0.0, icc_profile: None }), }, ]; // 逐个绘制整形后的字形 let mut current_x = 0.0; for (i, glyph_id) in glyph_ids.iter().enumerate() { let pos = &glyph_positions[i]; text_ops.extend([ Op::SetTextCursor { pos: Point::new(start_x + Pt(current_x + pos.x_advance as f32), start_y), }, Op::WriteGlyph { glyph_id: *glyph_id, font: font_ar_id.clone(), }, ]); current_x += pos.x_advance as f32; } text_ops.extend([Op::EndTextSection, Op::RestoreGraphicsState]); // 将text_ops合并到原ops中 let mut ops = vec![/* 原有的英文文本Op */]; ops.extend(text_ops);
关键说明
- RTL文本需要先通过
unicode-bidi调整字符顺序,确保显示方向正确; - 阿拉伯语的连写字形必须通过字形整形工具(如rustybuzz)处理,否则字符会以独立形式显示;
- 绘制RTL文本时,需要从右侧起始位置开始定位,避免文本超出页面范围。
内容的提问来源于stack exchange,提问作者Waiyl Karim
相关产品推荐
相关产品推荐

