تخطَّ إلى المحتوى

الطباعة: سجل الخطوط، التجزئة الفرعية، CMap، الترميز، BiDi

تحوّل وحدة الطباعة ملف خط وسلسلة ⁨Unicode⁩ إلى البايتات اللازمة لدفق محتوى ⁨Portable Document Format⁩ (⁨PDF⁩). وتتولى تحليل الخطوط، وسجل الخطوط طوال عمر العملية، والتجزئة الفرعية للحروف الرسومية، وإخراج ⁨ToUnicode CMap⁩، واستراتيجيات الترميز الواعية بـ ⁨cmap⁩، ومحرك ⁨Unicode⁩ ثنائي الاتجاه.

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 غير قابل للتغيير. استخدمه مع العمليات العاملة طويلة الأمد: سخّن مجموعة الخطوط عند الإقلاع، ثم استدعِ lock(). بعد ذلك يرفض السجل أي تعديل، بينما تستمر عمليات البحث في خدمة حركة المرور. ولا يحتفظ إلا ببيانات ⁨PHP⁩ خالصة: البيانات الوصفية المحلَّلة وبايتات الخط الخام. يمكن لمجموعة العمليات العاملة مشاركة نسخة واحدة. يقبل registerFromBinary() بايتات خط خام، وهو ما يستخدمه جسر @font-face الخاص بـ ⁨HyperText Markup Language⁩ (⁨HTML⁩) لخط جُلب من مصدر بعيد أو من ⁨data URI.⁩

يُضمّن المحرك كل خط يستخدمه ويجري له تجزئة فرعية. ينتقل برنامج الخط المضمَّن داخل ملف ⁨PDF⁩، لذلك يُعرض المستند بالشكل نفسه في أي عارض ولا يعتمد على خطوط النظام المثبَّتة — ⁨ISO 32000-2⁩ §9. لا تحمل التجزئة الفرعية إلا الحروف الرسومية التي يشير إليها المستند، وهذا مهم للمحتوى الصيني والياباني والكوري (⁨CJK⁩) أو المحتوى الغني بـ ⁨Unicode⁩ — ⁨ISO 32000-2⁩ §9. يحلّل FontSubsetter دليل الجداول الأصلي، ويستخرج ⁨cmap⁩، ويحل تبعيات الحروف الرسومية المركَّبة بوصفها إغلاقاً متعدياً، ويعيد بناء جداول head وhhea وmaxp وcmap وloca وglyf وhmtx. ويحافظ على ترقيم معرّف الحرف الرسومي الأصلي ويملأ الفتحات غير المستخدمة بالأصفار، بحيث يبقى CIDToGIDMap من النوع /Identity صالحاً. ويُعيد الخط الأصلي دون تغيير عندما تكون التجزئة الفرعية ستوفّر أقل من عشرة بالمئة، تجنباً لعمل لا يبرر نفسه. ينفّذ CffSubsetter العملية نفسها لخطوط ⁨OpenType⁩ التي تحمل جدول مخططات ⁨Compact Font Format.⁩

لإصدار النص ثلاث ترجمات: نقطة رمز ⁨Unicode⁩، ورمز الحرف في دفق المحتوى، ومعرّف الحرف الرسومي داخل الخط. تُبقي الوحدة هذا المسار صريحاً. FontInfo::encodeText() هي الواجهة؛ ويختار FontEncodingStrategyResolver الاستراتيجية لكل خط على حدة. يُوجَّه خط ⁨TrueType⁩ أو ⁨OpenType⁩ مضمَّن ومزوَّد بـ ⁨cmap⁩ من نوع ⁨Unicode⁩ إلى TrueTypeCmapStrategy، الذي يُصدر دفقاً سداسي عشري ⁨Identity-H⁩ من بايتين. وهذا هو الشكل المطلوب لخط ⁨Type 0⁩ مزوَّد بـ ⁨Identity-H CMap⁩ وسليل CIDFontType2 (⁨ISO 32000-2⁩ §9.7.4؛ أُعيد ملخّص مقطع ⁨retrieval-augmented generation⁩ (⁨RAG⁩) المطابق مقتطعاً بسبب حدّ الترخيص، وقد سُجِّل في _downgraded-claims-o3.md). أما كل خط آخر — خطوط ⁨Base 14⁩ القياسية، و⁨Type 1⁩ من نوعي ⁨PFB⁩ و⁨AFM⁩ — فيُوجَّه إلى Base14EncodingStrategy، الذي يُصدر سلسلة ⁨WinAnsi⁩ حرفية من بايت واحد. يغطّي ذلك الدفق ذخيرة ⁨WinAnsiEncoding⁩ ‏(⁨Windows code page 1252⁩) الكاملة — اللاتينية المُشكَّلة، وعلامة اليورو، وعلامات الترقيم الطباعية الشائعة. وتُسقَط نقاط الرمز الواقعة خارجها من الدفق أحادي البايت، ويُستخدم بديل الخط لكل عنقود عندما يُسجَّل خط يغطّيها (⁨ISO 32000-2 Annex D.2⁩). يغطي المُحلِّل كامل فضاء قيم FontInfo؛ ولا يوجد مسار يقبل قيمة فارغة. يبني ToUnicodeCMapBuilder مورد /ToUnicode الذي يتيح للقارئ استرجاع ⁨Unicode⁩ الأصلي من خط ⁨Identity-H.⁩ ويطبّق دمج bfrange الجشع وحدّاً أقصاه 100 إدخال لكل كتلة.

BidiEngine هو خدمة الحدّ لخوارزمية ⁨Unicode⁩ ثنائية الاتجاه، المعرَّفة في ⁨Unicode Standard Annex⁩ #9 (⁨UAX⁩ #9)، ⁨Unicode 16.⁩ عند إيقاف دعم العزل، يفوّض إلى المُحلِّل القديم بحيث يرى المستدعون الحاليون السلوك نفسه. وعند تشغيل دعم العزل، يشغّل خط الأنابيب الواعي بالعزل: مكدّس العزل الصريح بعمق أقصى يبلغ 125، وتمريرات النوع الضعيف، وتمريرات النوع المحايد بما في ذلك حل الأقواس المزدوجة، وتمريرات المستوى الضمني وإعادة ترتيب السطر. أما تغطية الحروف الرسومية لـ ⁨CJK⁩ في خط مرشَّح فهي تشخيص منفصل: يأخذ CjkFontValidator عينات من كتل ⁨Unicode⁩ المطلوبة لكل نص ويبلّغ عن نسبة تغطية مئوية.

النوعالصنفالأعضاء الرئيسيةالاستقرارمنذ
FontRegistryفئة نهائيةregister(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage()مستقر1.7.0
FontInfoفئة نهائية للقراءة فقط$family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText()مستقر1.0.0
FontSubsetterفئة نهائيةsubset(string, array<int>, int): stringمستقر1.0.0
CffSubsetterفئة نهائيةالتجزئة الفرعية لمخططات ⁨OpenType/CFF⁩مستقر1.0.0
FontEncodingStrategyResolverفئة نهائيةresolve(FontInfo): FontEncodingStrategyمستقر2.7.0
ToUnicodeCMapBuilderفئة نهائيةbuildFromRun(), buildFromMap(), encodeUnicodeUtf16Be()مستقر2.7.0
BidiEngineفئة نهائيةحل مدرك للعزل وفق ⁨UAX⁩ #9مستقر3.1.0
CjkFontValidatorفئة نهائيةvalidateCoverage(), detectScript(), isCjkCodepoint()مستقر1.0.0

FontInfo غير قابل للتغيير: توقيع باني الكائن وخصائصه العامة مجمَّدة. استراتيجيات الترميز دوال خالصة لـ (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 مزوَّداً بدفق البايتات، ومُعامل سلسلة ⁨PDF⁩، وعروض التقدّم لكل حرف رسومي، وخريطة معرّف الحرف الرسومي (⁨GID⁩) إلى ⁨Unicode⁩ التي تستهلكها ⁨CMap⁩ من نوع /ToUnicode.

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(). بعد lock()، يُطلق كل تعديل استثناءً، وتستمر عمليات البحث في خدمة حركة المرور. يُعيد memoryUsage() كائن MemoryReport، بحيث تستطيع العملية العاملة تتبّع ذاكرة الخطوط المؤقتة مقابل ميزانيتها.

  • عندما يكون السجل مقفلاً، فإنه يرفض register() وregisterFromBinary() وaddFontDirectory() وwarmup(). سخّن السجل واقفله عند الإقلاع؛ لا تسجّل قط أثناء معالجة الطلبات.
  • يُعيد FontSubsetter::subset() البايتات الأصلية دون تغيير عندما يكون التوفير أقل من عشرة بالمئة أو عندما يكون جدول أساسي مفقوداً. الخط المُعاد الذي يساوي المُدخل هو مسار انعدام الكسب الموثَّق، لا فشل.
  • يحافظ مُجزّئ التجزئة الفرعية على ترقيم معرّف الحرف الرسومي الأصلي ويملأ الحروف الرسومية غير المستخدمة بالأصفار. وهذا يُبقي CIDToGIDMap /Identity صالحاً؛ لا تفترض أن معرّفات الحروف الرسومية يُعاد ترقيمها ضمن نطاق متجاور.
  • يكتب registerFromBinary() البايتات إلى ملف مؤقت للتحليل، ويحذف كلاً من ملف الامتداد وملف tempnam() الأساسي في كتلة finally. بيانات الخط غير الموثوقة هي سطح هجوم على المُحلِّل؛ تحكّم بها قبل أن تصل إليه (انظر ملاحظات الأمان).
  • يفوّض BidiEngine حرفياً إلى المُحلِّل القديم عندما يكون دعم العزل متوقفاً. عندئذٍ تمرّ أحرف تنسيق العزل بوصفها محايدة عند الحدود. شغّل دعم العزل عبر سياسة المطابقة للحصول على سلوك ⁨UAX⁩ #9 الكامل.
  • يأخذ CjkFontValidator عينات من نقاط الرمز على خطوة بدلاً من اختبار كل نقطة، لذا فإن رقم التغطية الخاص به تقدير كافٍ إحصائياً، لا عدّ شامل.

يهيمن تحليل الخطوط على الاستخدام الأول؛ ويوزّع السجل تلك التكلفة لتصبح مرة واحدة لكل عملية. بعد التسخين، يكون get() وhas() عمليتي بحث في خريطة بترتيب ⁨O⁩(1). تتناسب تكلفة التجزئة الفرعية مع عدد الحروف الرسومية التي يستخدمها المستند، لا مع جدول الحروف الرسومية الكامل للخط. لذلك تحسّن التجزئة الفرعية الحجم والسرعة معاً لمحتوى ⁨CJK⁩: يتعامل مُجزّئ التجزئة الفرعية مع خطوط تتجاوز 20,000 حرف رسومي عبر البحث الثنائي، والمخازن المؤقتة المخصَّصة مسبقاً، وعمليات السلاسل المجمّعة. حل الحروف الرسومية المركَّبة محدود؛ إذ يبلغ حدّه الأقصى 100 تكرار إغلاق للدفاع ضد المراجع الدائرية للمكوّنات. يحدّ محلّل ⁨cmap⁩ من نوع ⁨Format 12⁩ من أعداد المجموعات والإدخالات للحدّ من استخدام الذاكرة مع مُدخلات الخطوط العدائية. يغطي performance_budget البالغ 1500 ⁨ms⁩ للزمن الجداري و64 ⁨MB⁩ للذروة تسخين خط نموذجي إضافة إلى عرض المستند.

هناك سطحان يحملان ثقلاً أمنياً. الأول هو مُدخل الخط. يحلّل register() وregisterFromBinary() بايتات عشوائية. وينشئ registerFromBinary() ملفاً مؤقتاً. يرفض الحدّ مُغلّفات الدفق وبايتات ⁨null⁩ في المسارات. يجب أن تجتاز بيانات الخط غير الموثوقة سياسة موارد خارجية تحدّ من حجم الملف وعدد الحروف الرسومية قبل أن تصل إلى المُحلِّل. تتحقق القارئات الثنائية لمُجزّئ التجزئة الفرعية من حدود كل إزاحة. تحدّ محلّلات ⁨cmap⁩ من أعداد المجموعات والإدخالات والجداول (numGroups > 31000 وحدّ إدخالات يبلغ 200,000 في ⁨Format 12⁩) بحيث لا يستطيع خط مُصمَّم خصيصاً دفع تخصيص غير محدود. السطح الثاني هو استرجاع النص: يتحقق ToUnicodeCMapBuilder من أن كل رمز حرف داخل فضاء الرموز 16 بت وأن كل قيمة ⁨Unicode⁩ عددية صالحة. ويرفض أنصاف البدائل، بحيث لا تستطيع خريطة مشوَّهة إنتاج مورد استخراج تالف. عامِل أي خط أو نص يُورَّد من الخارج بوصفه غير موثوق.

الادّعاءالمعيارالبندالدليل
كل خط يستخدمه المستند مضمَّن بحيث يُعرض المستند دون الاعتماد على خطوط النظام.⁨ISO 32000-2⁩§9
الخط المضمَّن مُجزّأ فرعياً إلى الحروف الرسومية التي يشير إليها المستند.⁨ISO 32000-2⁩§9
يُصدَر خط ⁨TrueType⁩ من نوع ⁨CJK⁩ مضمَّن بوصفه خط ⁨Type 0⁩ مزوَّداً بـ ⁨Identity-H CMap⁩ وسليل CIDFontType2.⁨ISO 32000-2⁩§9.7.4ملخّص ⁨RAG⁩ مقتطع بسبب حدّ الترخيص؛ البادئة 7a5258772f508e3b، انظر _downgraded-claims-o3.md

البندان الأولان مُعاد صياغتهما ومُثبَّتان بالملخّص. لم يُعَد ملخّص ⁨RAG⁩ الكامل للبند الثالث (اقتطاع بسبب حدّ الترخيص)؛ ويؤكده ⁨ADR-013⁩ والنظرة العامة للمطوّر حول مُرمِّز ⁨cmap⁩، وقد سُجِّل بوصفه مخفَّضاً. لا يُعيد ⁨NextPDF⁩ إنتاج نص معياري. مطابقة ⁨PDF/A-4⁩ و⁨PDF/UA-2⁩ لمحتوى ⁨CJK⁩ مشروطة بالتجزئة الفرعية على جانب الكاتب وتوصيل /ToUnicode المتتبَّعَين هناك.

تبني حزمة ميزات ⁨OpenType⁩ التجارية وسلاسل البديل المتميّزة للخطوط على سجل ⁨Core⁩ وطبقة الترميز. تُضمّن وحدة طباعة ⁨Core⁩ كل خط وتجري له تجزئة فرعية وترميزاً دون ترخيص؛ وتضيف الحزمة المدفوعة حلّ بديل منسَّقاً. إغفال رابط التحويل مقصود: هذه الصفحة وثائق، وليست مسار مبيعات.