跳到內容
getnextpdf.com

讓 CJK 與阿拉伯文 PDF 正確支援複製貼上

建立一份 Portable Document Format(PDF)檔案,讓其中的中文、日文、韓文(CJK)與阿拉伯文文字能以原始邏輯字元複製貼上。引擎會透過 /ToUnicode CMap 把每個字形對映到 Unicode,以 Normalization Form Compatibility Composition(NFKC)正規化結果,並把塑形後或加了字距的文字段落包裹在帶有 /ActualText/Span 中。只要註冊字型並寫入內容,擷取結果就能維持正確。請以 pdftotext/Poppler 驗證擷取,而非 PyMuPDF;veraPDF 驗證的是 PDF/Universal Accessibility 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)。引擎會以 Unicode UAX #15 所定義的 Normalization Form Compatibility Composition(NFKC)正規化這些值。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 建構器會拒絕代理對半碼(surrogate halves)與超出碼空間的字碼,因此格式錯誤的對映無法建立出損毀的擷取資源。引擎不會執行任何指令稿,也不會為本地字型取得任何遠端資源。

陳述規範條款
/ToUnicode CMap 會將字元碼對映到 Unicode,以供擷取。ISO 32000-2§9.10
/ActualText 是所包圍內容的確切替換。ISO 32000-2§14.9
NFKC 是先進行相容分解,再進行正規組合。Unicode UAX #15§1.2

不適用。