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

使用lopdf添加PDF文本在多阅读器显示异常及特殊字符问题求助

解决PDF添加特殊字符跨阅读器显示问题

核心原因

你遇到的问题本质是:

  • Base 14标准字体(如Courier)的默认编码(WinAnsiEncoding或MacRomanEncoding)并不包含所有你用到的特殊字符(比如°),不同阅读器对未嵌入字体的字符 fallback 逻辑不同:Brave会自动用系统字体补全缺失字符,而Firefox、Evince等严格遵循PDF规范,无嵌入子集或正确映射时直接显示方块。
  • Ghostscript的自动嵌入处理会重构字体编码,但默认逻辑不会保留你自定义的UTF-16BE映射,导致特殊字符的编码映射错乱,出现缺失或错误。

直接解决方案:正确嵌入字体子集+设置ToUnicode映射

用lopdf添加文本时,不要依赖系统默认的Courier字体,而是主动嵌入包含目标特殊字符的Courier字体子集,并定义ToUnicode CMap确保阅读器能正确解析字符编码。

步骤1:准备Courier字体文件

获取包含完整扩展拉丁字符的Courier字体文件(比如系统自带的Courier New.ttf,或开源的Courier Prime.ttf),将其作为资源引入项目。

步骤2:用lopdf创建带嵌入子集和ToUnicode的字体字典

use lopdf::{Document, Object, ObjectId, Dictionary, Stream};
use std::fs;

// 加载现有PDF文档
let mut doc = Document::load("input.pdf")?;

// 加载字体文件
let font_data = fs::read("Courier New.ttf")?;

// 创建字体描述符字典
let font_descriptor = Dictionary::from_iter(vec![
    ("Type", Object::Name("FontDescriptor".to_string())),
    ("FontName", Object::Name("CourierCustom".to_string())),
    ("FontFile2", doc.add_object(Stream::new(Dictionary::new(), font_data))),
    ("Flags", Object::Integer(32)), // 标记为固定宽度字体
    ("FontBBox", Object::Array(vec![
        Object::Integer(-166), Object::Integer(-226),
        Object::Integer(1000), Object::Integer(890)
    ])),
    ("ItalicAngle", Object::Integer(0)),
    ("Ascent", Object::Integer(890)),
    ("Descent", Object::Integer(-226)),
]);
let font_descriptor_id = doc.add_object(font_descriptor);

// 创建ToUnicode CMap,映射字符编码到UTF-16BE
// 针对需要的字符:é(U+00E9)、è(U+00E8)、°(U+00B0)
let to_unicode_cmap = Stream::new(
    Dictionary::new(),
    b"""
    /CIDInit /ProcSet findresource begin
    12 dict begin
    begincmap
    /CIDSystemInfo <<
    /Registry (Adobe)
    /Ordering (UCS)
    /Supplement 0
    >> def
    /CMapName /CourierCustom-UCS def
    /CMapType 2 def
    1 begincodespacerange
    <00> <FF>
    endcodespacerange
    3 beginbfchar
    <E9> <00E9>
    <E8> <00E8>
    <B0> <00B0>
    endbfchar
    endcmap
    CMapName currentdict /CMap defineresource pop
    end
    end
    """.to_vec(),
);
let to_unicode_id = doc.add_object(to_unicode_cmap);

// 创建字体字典
let font_dict = Dictionary::from_iter(vec![
    ("Type", Object::Name("Font".to_string())),
    ("Subtype", Object::Name("TrueType".to_string())),
    ("BaseFont", Object::Name("CourierNew".to_string())),
    ("FontDescriptor", Object::Reference(font_descriptor_id)),
    ("ToUnicode", Object::Reference(to_unicode_id)),
    ("Encoding", Object::Name("WinAnsiEncoding".to_string())),
]);
let font_id = doc.add_object(font_dict);

// 将字体添加到页面的资源字典中
let page_id = doc.get_pages()[0]; // 假设操作第一页
let mut page = doc.get_page_mut(page_id)?;
let resources = page.get_mut_resources()?;
let fonts = resources.get_mut("Font").unwrap_or_else(|| {
    let dict = Dictionary::new();
    resources.insert("Font", Object::Dictionary(dict)).unwrap();
    resources.get_mut("Font").unwrap()
});
if let Object::Dictionary(ref mut fonts_dict) = fonts {
    fonts_dict.insert("F1", Object::Reference(font_id));
}

步骤3:添加文本时使用正确的编码

不再用UTF-16BE转十六进制,而是直接使用WinAnsi编码对应的字符(é是\xE9,è是\xE8,°是\xB0),用Tj运算符:

// 构造文本内容的十六进制字符串(WinAnsi编码)
let text_content = "Hello é è ° World";
let hex_text = text_content.as_bytes().iter().map(|b| format!("{:02X}", b)).collect::<String>();

// 创建文本流
let text_stream = format!("BT /F1 12 Tf 100 700 Td <{}> Tj ET", hex_text);
let content_stream = Stream::new(Dictionary::new(), text_stream.as_bytes().to_vec());
let content_id = doc.add_object(content_stream);

// 更新页面内容
page.set("Contents", Object::Reference(content_id));

// 保存文档
doc.save("output.pdf")?;

Ghostscript后续处理的补救方案

如果必须用Ghostscript处理已生成的PDF,需要添加参数强制保留ToUnicode映射,避免编码错乱:

gs -sDEVICE=pdfwrite -dEmbedAllFonts=true -dPreserveCopyPage=false -dPreserveToUnicode=true -o output_gs.pdf input.pdf

但这种方式不如在生成PDF时直接处理可靠。

内容的提问来源于stack exchange,提问作者C Taque

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 14:02:01