跳转到内容
getnextpdf.com

让 CJK 和阿拉伯语 PDF 的复制粘贴结果正确

制作 PDF 文件,让其中的中文、日文、韩文(CJK)和阿拉伯语文本在复制粘贴时保留原始逻辑字符。引擎会通过 /ToUnicode CMap 把每个字形码映射到 Unicode,再按规范化形式兼容组合(NFKC)规范化结果,并把经过塑形或带字母间距的文本运行包裹在携带 /ActualText/Span 中。注册字体并写入内容后,提取结果就会保持正确。请用 pdftotext/Poppler 验证提取,而非 PyMuPDF;veraPDF 验证的是 PDF/通用无障碍 2(PDF/UA-2)符合性,而不是文本提取结果。

Terminal window
composer require nextpdf/core

注册一款 CJK 字体(例如 Noto Sans CJK),以及一款支持阿拉伯语、且字符映射表覆盖 Arabic Presentation Forms-B 区块的字体(例如 Noto Naskh Arabic)。只嵌入你拥有嵌入授权的字体。

内容流中的字形码并不等同于 Unicode。/ToUnicode CMap 会把每个码映射回 Unicode,以便阅读器提取文本(ISO 32000-2 §9.10)。引擎会按规范化形式兼容组合(NFKC,由 Unicode UAX #15 定义)对这些值进行规范化。CJK 兼容表意文字和阿拉伯语呈现形式都会映射为各自的基础字符,因此搜索和复制得到的是规范文本,而不是兼容码位。

有两种情况仅靠 /ToUnicode 还不够:带字母间距的拉丁文,以及经过塑形的从右到左阿拉伯语。引擎会把它们绘制成带间距或经过重排序的字形,因此仅凭字形顺序无法还原逻辑字符串。引擎会把每个文本运行包裹在携带 /ActualText/Span 标记内容序列中,/ActualText 是所包裹内容的精确替代(ISO 32000-2 §14.9)。遵循 /ActualText 的提取器会返回逻辑字符串。

符号位置作用
FontRegistry::register(string $fontFile, string $alias = ''): FontInfoNextPDF\Typography\FontRegistry注册 CJK 和阿拉伯语字体。
DocumentFactory::create(): DocumentNextPDF\Core\DocumentFactory构建会使用你的注册表的文档。
Document::writeHtml(string $html): staticNextPDF\Core\Concerns\HasTextOutput渲染多语言内容。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
$fonts = new FontRegistry();
$fonts->register(__DIR__ . '/NotoSansCJK-Regular.ttf', alias: 'CJK');
$fonts->register(__DIR__ . '/NotoNaskhArabic-Regular.ttf', alias: 'Arabic');
$doc = (new DocumentFactory($fonts, new ImageRegistry(maxCacheBytes: 0)))->create();
$doc->addPage();
$doc->writeHtml(
'<p style="font-family: \'CJK\';">PDF 2.0 引擎 — 量子</p>'
. '<p style="direction: rtl; font-family: \'Arabic\';">فاتورة</p>'
);
$doc->save(__DIR__ . '/multilingual.pdf');
Terminal window
pdftotext multilingual.pdf - | head
# Extracts the logical text: "PDF 2.0 引擎 — 量子" and the logical Arabic "فاتورة",
# not compatibility code points or reversed presentation forms.

这个自包含示例会为文档添加标签,加入一个带字母间距的标题,并写入测试载体路径。

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
$fonts = new FontRegistry();
$fonts->register(__DIR__ . '/NotoSansCJK-Regular.ttf', alias: 'CJK');
$fonts->register(__DIR__ . '/NotoNaskhArabic-Regular.ttf', alias: 'Arabic');
$doc = (new DocumentFactory($fonts, new ImageRegistry(maxCacheBytes: 0)))->create();
$doc->setTitle('Multilingual extraction');
$doc->enableTaggedPdf('en');
$doc->addPage();
$html = <<<'HTML'
<h1 style="font-family: 'CJK'; letter-spacing: 3px;">CODE WORD</h1>
<p style="font-family: 'CJK';">中文 · 日本語 · 한국어 · 量子 (compatibility ideograph)</p>
<p style="direction: rtl; font-family: 'Arabic';">المبلغ الإجمالي 380.00</p>
HTML;
$doc->writeHtml($html);
$out = getenv('NEXTPDF_OUT');
$doc->save($out !== false ? $out : __DIR__ . '/multilingual-copy-paste-extraction.pdf');
echo "Wrote the multilingual PDF\n";

对输出文件运行 pdftotext。带字母间距的标题会提取为 CODE WORD,并且不会插入任何空格;CJK 行会提取为对应的基础字符;阿拉伯语行会提取为逻辑字符串。

  • 请用 pdftotext 验证提取,而非 PyMuPDF。 PyMuPDF 的原始文本模式会忽略行内 /ActualText,并返回视觉字形,因此可能低估结果的正确性。Poppler(pdftotext)会遵循 /ActualText;veraPDF 验证的是 PDF/UA-2,而不是提取。
  • 提取需要 /ToUnicode 注册并嵌入字体,让写入器输出 /ToUnicode CMap。未嵌入的非标准字体无法保证 Unicode 映射。
  • /ActualText 覆盖塑形后与带字母间距的运行。 对于普通、未塑形、无间距的文本,仅靠 /ToUnicode 就能正确提取;/Span 包裹器则保留带间距或经过重排序的文本运行。
  • 已加标签的 HTML 表格能通过 PDF/UA-2 检查。 提取是正确的,而且已加标签的 HTML <table> 现在能通过 veraPDF --flavour ua2 且零失败;参见无障碍

构建 /ToUnicode CMap 与 /Span 包裹器的开销随字形数量线性增长。本示例预算为 wall_ms: 1500, peak_mb: 96

请校验用户提供的多语言字符串长度,确保输出大小有界。/ToUnicode 构建器会拒绝代理半区和超出码空间的码,因此格式错误的映射无法生成损坏的提取资源。引擎不会运行任何脚本,也不会为本地字体抓取任何远程资源。

陈述规范条款
/ToUnicode CMap 会将字符码映射到 Unicode,以供提取。ISO 32000-2§9.10
/ActualText 是所包裹内容的精确替代。ISO 32000-2§14.9
NFKC 是先做兼容分解、再做规范组合。Unicode UAX #15§1.2

不适用。