ข้ามไปยังเนื้อหา

การจัดวางตัวอักษร: รีจิสทรีฟอนต์ การทำซับเซ็ต CMap และการเข้ารหัส BiDi

โมดูล typography แปลงไฟล์ฟอนต์และสตริง Unicode ให้เป็นลำดับไบต์ที่ content stream ของ Portable Document Format (PDF) ต้องใช้ โมดูลนี้ดูแลการแยกวิเคราะห์ฟอนต์ รีจิสทรีที่คงอยู่ตลอดอายุของกระบวนการ การทำซับเซ็ต glyph การสร้าง ToUnicode CMap กลยุทธ์การเข้ารหัสแบบรับรู้ cmap และเอนจิน Unicode bidirectional

Terminal window
composer require nextpdf/core:^3

FontRegistry เก็บฟอนต์ไว้ตลอดอายุของกระบวนการและนำ FontRegistryInterface ไปใช้ รีจิสทรีนี้แยกวิเคราะห์ไฟล์ TrueType, OpenType, TrueType Collection (TTC) หรือ Type 1 (Printer Font Binary (PFB) และ Adobe Font Metrics (AFM)) เพียงครั้งเดียว แล้วคืนค่า FontInfo ที่ไม่สามารถเปลี่ยนแปลงได้ ใช้รีจิสทรีนี้กับ worker ที่ทำงานยาวนาน: เตรียมชุดฟอนต์ล่วงหน้าตอนบูต แล้วเรียก lock() หลังจากนั้นรีจิสทรีจะปฏิเสธการเปลี่ยนแปลงทุกครั้ง ขณะที่การค้นหายังคงให้บริการทราฟฟิกต่อไป รีจิสทรีเก็บเฉพาะข้อมูล PHP ล้วน ได้แก่ เมตาดาตาที่แยกวิเคราะห์แล้วและไบต์ฟอนต์ดิบ worker pool สามารถใช้อินสแตนซ์เดียวร่วมกันได้ registerFromBinary() รับไบต์ฟอนต์ดิบ ซึ่งบริดจ์ @font-face ของ HyperText Markup Language (HTML) ใช้สำหรับฟอนต์ที่ดึงมาจากแหล่งระยะไกลหรือ data URI

เอนจินฝังและทำซับเซ็ตฟอนต์ทุกตัวที่ใช้ โปรแกรมฟอนต์ที่ฝังจะติดไปกับ PDF ดังนั้นเอกสารจะเรนเดอร์เหมือนกันในโปรแกรมดูทุกตัวและไม่ต้องพึ่งฟอนต์ของระบบที่ติดตั้งไว้ — ISO 32000-2 §9 ซับเซ็ตบรรจุเฉพาะ glyph ที่เอกสารอ้างอิงเท่านั้น ซึ่งสำคัญต่อเนื้อหาภาษาจีน ญี่ปุ่น และเกาหลี (CJK) หรือเนื้อหาที่ใช้ Unicode จำนวนมาก — ISO 32000-2 §9 FontSubsetter แยกวิเคราะห์ table directory เดิม ดึง cmap ออกมา แก้ไขการพึ่งพา composite-glyph เป็น transitive closure และสร้างตาราง head, hhea, maxp, cmap, loca, glyf และ hmtx ขึ้นใหม่ รีจิสทรีรักษาการกำหนดหมายเลข glyph identifier เดิมไว้และเติมศูนย์ในช่องที่ไม่ได้ใช้ ดังนั้น CIDToGIDMap แบบ /Identity จึงยังคงใช้ได้ ระบบจะคืนค่าฟอนต์เดิมโดยไม่เปลี่ยนแปลงเมื่อการทำซับเซ็ตประหยัดได้น้อยกว่าสิบเปอร์เซ็นต์ เพื่อหลีกเลี่ยงงานที่ไม่คุ้มค่า CffSubsetter ทำงานแบบเดียวกันสำหรับฟอนต์ OpenType ที่มีตารางเส้นโครงร่าง Compact Font Format

การส่งออกข้อความมีการแปลงสามขั้น: code point ของ Unicode, character code ใน content stream และ glyph identifier ภายในฟอนต์ โมดูลทำให้เส้นทางนี้ชัดเจน FontInfo::encodeText() เป็นฟาซาด ส่วน FontEncodingStrategyResolver เลือกกลยุทธ์ตามฟอนต์ ฟอนต์ TrueType หรือ OpenType ที่ฝังและมี Unicode cmap จะถูกส่งไปยัง TrueTypeCmapStrategy ซึ่งปล่อย Identity-H hex stream ขนาดสองไบต์ นั่นคือรูปแบบที่ฟอนต์ Type 0 ซึ่งมี Identity-H CMap และส่วนสืบทอด CIDFontType2 ต้องการ (ISO 32000-2 §9.7.4; ไดเจสต์ของ retrieval-augmented generation (RAG) chunk ที่ตรงกันถูกส่งกลับมาแบบตัดทอนโดย license cap และบันทึกไว้ใน _downgraded-claims-o3.md) ฟอนต์อื่นทุกตัว — ฟอนต์มาตรฐาน Base 14, Type 1 PFB และ AFM — จะถูกส่งไปยัง Base14EncodingStrategy ซึ่งปล่อย WinAnsi literal string ขนาดหนึ่งไบต์ สตรีมนั้นครอบคลุมชุดอักขระของ WinAnsiEncoding (Windows code page 1252) ทั้งหมด — อักขระละตินที่มีเครื่องหมายกำกับ เครื่องหมายยูโร และเครื่องหมายวรรคตอนเชิงการพิมพ์ทั่วไป code point ที่อยู่นอกชุดนี้จะถูกตัดออกจากสตรีมแบบหนึ่งไบต์ และใช้การสำรองฟอนต์ต่อคลัสเตอร์เมื่อมีการลงทะเบียนฟอนต์ที่ครอบคลุมไว้ (ISO 32000-2 Annex D.2) รีโซลเวอร์ครอบคลุมพื้นที่ค่าของ FontInfo ทั้งหมด จึงไม่มีเส้นทางที่เป็น null ToUnicodeCMapBuilder สร้างทรัพยากร /ToUnicode ที่ทำให้โปรแกรมอ่านกู้คืน Unicode เดิมจากฟอนต์ Identity-H ได้ โดยใช้การรวม bfrange แบบ greedy และจำกัดบล็อกไว้ที่ 100 รายการ

BidiEngine เป็นบริการแบบมีขอบเขตสำหรับ Unicode Bidirectional Algorithm ซึ่งกำหนดโดย Unicode Standard Annex #9 (UAX #9), Unicode 16 เมื่อปิดการรองรับ isolate จะมอบหมายงานให้รีโซลเวอร์รุ่นเดิม เพื่อให้ผู้เรียกเดิมเห็นพฤติกรรมเดียวกัน เมื่อเปิดการรองรับ isolate จะรันไพป์ไลน์แบบรับรู้ isolate ได้แก่ สแต็ก explicit-isolate ที่มีความลึกสูงสุด 125 พาส weak-type พาส neutral-type รวมถึงการแก้ไข paired-bracket และพาส implicit-level กับ line-reordering ความครอบคลุม glyph ของ CJK สำหรับฟอนต์ตัวเลือกเป็นการวินิจฉัยแยกต่างหาก: CjkFontValidator สุ่มตัวอย่างบล็อก Unicode ที่จำเป็นต่อ script และรายงานเปอร์เซ็นต์ความครอบคลุม

ชนิดประเภทสมาชิกสำคัญเสถียรภาพตั้งแต่
FontRegistryfinal classregister(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage()stable1.7.0
FontInfofinal readonly class$family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText()stable1.0.0
FontSubsetterfinal classsubset(string, array<int>, int): stringstable1.0.0
CffSubsetterfinal classการทำซับเซ็ตเส้นโครงร่าง OpenType/CFFstable1.0.0
FontEncodingStrategyResolverfinal classresolve(FontInfo): FontEncodingStrategystable2.7.0
ToUnicodeCMapBuilderfinal classbuildFromRun(), buildFromMap(), encodeUnicodeUtf16Be()stable2.7.0
BidiEnginefinal classการแก้ไขแบบรับรู้ isolate ตาม UAX #9stable3.1.0
CjkFontValidatorfinal classvalidateCoverage(), detectScript(), isCjkCodepoint()stable1.0.0

FontInfo ไม่สามารถเปลี่ยนแปลงได้: ลายเซ็นของ constructor และคุณสมบัติสาธารณะถูกตรึงไว้ กลยุทธ์การเข้ารหัสเป็นฟังก์ชันบริสุทธิ์ของ (FontInfo, UTF-8 text): อินพุตเดียวกันจะคืนค่า EncodedGlyphRun เดียวกันทุกครั้งที่เรียก

examples/35-cjk-cmap-demo.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Typography\Encoding\EncodingMode;
use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$cjkFont = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
$encoded = $cjkFont->encodeText('PDF 2.0 引擎 — 使用 CMap 編碼');
// An embedded CJK TrueType face resolves to the two-byte Identity-H path.
assert($encoded->mode === EncodingMode::TwoByteCid);

register() แยกวิเคราะห์ฟอนต์เพียงครั้งเดียวและคืนค่า FontInfo ที่ไม่สามารถเปลี่ยนแปลงได้ encodeText() ส่งผ่านรีโซลเวอร์และคืนค่า EncodedGlyphRun พร้อม byte stream, PDF string operand, ความกว้าง advance ต่อ glyph และแมป glyph identifier (GID) ไปยัง Unicode ที่ /ToUnicode CMap นำไปใช้

examples/typography/registry-warmup.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Exception\NextPdfException;
use NextPDF\Typography\FontRegistry;
use Psr\Log\LoggerInterface;
final readonly class FontBootstrap
{
public function __construct(
private FontRegistry $registry,
private LoggerInterface $logger,
) {}
/**
* Warm a font set at worker boot, then lock the registry for the
* lifetime of the process.
*
* @param list<string> $fontFiles Absolute paths to font files.
*/
public function boot(array $fontFiles): void
{
try {
$this->registry->warmup($fontFiles);
$this->registry->lock();
} catch (NextPdfException $e) {
$this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e;
}
$report = $this->registry->memoryUsage();
$this->logger->info('Font cache primed', [
'fonts' => $report->entryCount,
'bytes' => $report->currentBytes,
]);
}
}

warmup() ตามด้วย lock() คือลำดับการบูตของ worker หลังจาก lock() การเปลี่ยนแปลงทุกครั้งจะ throw และการค้นหายังคงให้บริการทราฟฟิกต่อไป memoryUsage() คืนค่า MemoryReport ดังนั้น worker แต่ละตัวจึงติดตามแคชฟอนต์เทียบกับงบประมาณของตนได้

  • เมื่อรีจิสทรีถูกล็อก จะปฏิเสธ register(), registerFromBinary(), addFontDirectory() และ warmup() ให้เตรียมและล็อกตอนบูต อย่าลงทะเบียนระหว่างการจัดการคำขอ
  • FontSubsetter::subset() คืนค่าไบต์เดิมโดยไม่เปลี่ยนแปลงเมื่อการประหยัดน้อยกว่าสิบเปอร์เซ็นต์หรือเมื่อตารางที่จำเป็นขาดหายไป ฟอนต์ที่คืนค่ามาซึ่งเท่ากับอินพุตคือเส้นทาง no-gain ที่มีการบันทึกไว้ ไม่ใช่ความล้มเหลว
  • ตัวทำซับเซ็ตรักษาการกำหนดหมายเลข glyph identifier เดิมไว้และเติมศูนย์ใน glyph ที่ไม่ได้ใช้ สิ่งนี้ทำให้ CIDToGIDMap /Identity ยังคงใช้ได้ อย่าสันนิษฐานว่า glyph identifier จะถูกกำหนดหมายเลขใหม่ให้อยู่ในช่วงต่อเนื่อง
  • registerFromBinary() เขียนไบต์ลงไฟล์ชั่วคราวเพื่อแยกวิเคราะห์ และลบทั้งไฟล์ส่วนขยายและไฟล์ฐานของ tempnam() ในบล็อก finally ข้อมูลฟอนต์ที่ไม่น่าเชื่อถือเป็นพื้นผิวการโจมตีจากการแยกวิเคราะห์ จงกั้นข้อมูลนั้นก่อนที่จะถึงตัวแยกวิเคราะห์ (ดูหมายเหตุด้านความปลอดภัย)
  • BidiEngine มอบหมายงานให้รีโซลเวอร์รุ่นเดิมแบบคำต่อคำเมื่อปิดการรองรับ isolate อักขระจัดรูปแบบ isolate จะผ่านต่อไปแบบ boundary-neutral เปิดการรองรับ isolate ผ่านนโยบายความสอดคล้องเพื่อให้ได้พฤติกรรม UAX #9 อย่างเต็มรูปแบบ
  • CjkFontValidator สุ่มตัวอย่าง code point เป็นช่วงก้าวแทนที่จะทดสอบทุกตัว ดังนั้นตัวเลขความครอบคลุมจึงเป็นค่าประมาณที่เพียงพอทางสถิติ ไม่ใช่การนับแบบครบถ้วน

การแยกวิเคราะห์ฟอนต์เป็นต้นทุนหลักในการใช้งานครั้งแรก รีจิสทรีกระจายต้นทุนนั้นให้เหลือครั้งเดียวต่อกระบวนการ หลังการเตรียมล่วงหน้า get() และ has() เป็นการค้นหาในแมปแบบ O(1) ต้นทุนการทำซับเซ็ตแปรตามจำนวน glyph ที่เอกสารใช้ ไม่ใช่ตามตาราง glyph ทั้งหมดของฟอนต์ นั่นคือเหตุผลที่การทำซับเซ็ตช่วยปรับปรุงทั้งขนาดและความเร็วสำหรับเนื้อหา CJK: ตัวทำซับเซ็ตจัดการฟอนต์ที่มี glyph มากกว่า 20,000 ตัวผ่านการค้นหาแบบไบนารี บัฟเฟอร์ที่จัดสรรล่วงหน้า และการดำเนินการสตริงแบบกลุ่ม การแก้ไข composite-glyph มีขอบเขตจำกัด โดยจำกัดที่ 100 รอบ closure เพื่อป้องกันการอ้างอิงคอมโพเนนต์แบบวนรอบ ตัวแยกวิเคราะห์ cmap Format 12 จำกัดจำนวน group และ entry เพื่อจำกัดการใช้หน่วยความจำสำหรับอินพุตฟอนต์ที่เป็นอันตราย performance_budget ที่ 1500 ms wall และ 64 MB peak ครอบคลุมการเตรียมฟอนต์ล่วงหน้าโดยทั่วไปบวกกับการเรนเดอร์เอกสาร

มีพื้นผิวสองส่วนที่มีนัยสำคัญด้านความปลอดภัย ส่วนแรกคืออินพุตฟอนต์ register() และ registerFromBinary() แยกวิเคราะห์ไบต์จากอินพุตที่กำหนดได้โดยอิสระ registerFromBinary() สร้างไฟล์ชั่วคราวจริงบนดิสก์ ชั้นขอบเขตจะปฏิเสธ stream wrapper และ null byte ในพาธ ข้อมูลฟอนต์ที่ไม่น่าเชื่อถือต้องผ่านนโยบายทรัพยากรภายนอกที่จำกัดขนาดไฟล์และจำนวน glyph ก่อนที่จะถึงตัวแยกวิเคราะห์ ตัวอ่านไบนารีของตัวทำซับเซ็ตตรวจสอบขอบเขตของทุก offset ตัวแยกวิเคราะห์ cmap จำกัดจำนวน group, entry และ table (numGroups > 31000 และจำกัด entry ที่ 200,000 ใน Format 12) ดังนั้นฟอนต์ที่ถูกสร้างขึ้นเป็นพิเศษจึงไม่สามารถบังคับให้จัดสรรหน่วยความจำแบบไม่จำกัดได้ พื้นผิวที่สองคือการกู้คืนข้อความ: ToUnicodeCMapBuilder ตรวจสอบว่าทุก character code อยู่ภายใน codespace ขนาด 16 บิตและทุกค่า Unicode เป็น scalar ที่ถูกต้อง จะปฏิเสธ surrogate half ดังนั้นแมปที่ผิดรูปแบบจึงไม่สามารถสร้างทรัพยากรการแยกข้อมูลที่เสียหายได้ ให้ถือว่าฟอนต์หรือข้อความใดก็ตามที่จัดหามาจากภายนอกเป็นสิ่งที่ไม่น่าเชื่อถือ

การกล่าวอ้างมาตรฐานข้อกำหนดหลักฐาน
ฟอนต์ทุกตัวที่เอกสารใช้ถูกฝัง ดังนั้นเอกสารจึงเรนเดอร์ได้โดยไม่ต้องพึ่งฟอนต์ของระบบISO 32000-2§9
ฟอนต์ที่ฝังถูกทำซับเซ็ตให้เหลือเฉพาะ glyph ที่เอกสารอ้างอิงISO 32000-2§9
ฟอนต์ TrueType ของ CJK ที่ฝังถูกปล่อยเป็นฟอนต์ Type 0 ที่มี Identity-H CMap และมี CIDFontType2 เป็นฟอนต์สืบทอดISO 32000-2§9.7.4ไดเจสต์ RAG ถูกตัดทอนโดย licence cap คำนำหน้า 7a5258772f508e3b ดู _downgraded-claims-o3.md

สองข้อกำหนดแรกเป็นการถอดความและตรึงด้วยไดเจสต์ ไดเจสต์ RAG แบบเต็มของข้อกำหนดที่สามไม่ได้ถูกคืนค่ามา (ถูกตัดทอนด้วย license cap) ADR-013 และภาพรวมสำหรับนักพัฒนาเรื่อง cmap-encoder ช่วยยืนยันสนับสนุน และมีการบันทึกไว้ว่าถูกลดระดับ NextPDF ไม่ทำซ้ำข้อความเชิงบรรทัดฐาน ความสอดคล้อง PDF/A-4 และ PDF/UA-2 สำหรับเนื้อหา CJK ถูกกำกับด้วยการทำซับเซ็ตฝั่ง writer และการเชื่อมต่อ /ToUnicode ที่ติดตามไว้ที่นั่น

ฟีเจอร์แพ็ก OpenType เชิงพาณิชย์และเชน font-fallback ระดับพรีเมียมต่อยอดจากชั้นรีจิสทรีและการเข้ารหัสของ Core โมดูล typography ของ Core ฝัง ทำซับเซ็ต และเข้ารหัสฟอนต์ทุกตัวโดยไม่ต้องมีไลเซนส์ ส่วนแพ็กแบบเสียเงินเพิ่มการแก้ไข fallback ที่คัดสรรมาแล้ว การละเว้นลิงก์ conversion เป็นความตั้งใจ: หน้านี้เป็นเอกสาร ไม่ใช่เส้นทางการขาย