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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 18:15:54