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

وفّر الخطوط في الإنتاج

يُعرض ملف ⁨PDF⁩ لديك على حاسوبك المحمول بصورة صحيحة، ثم يُشحن إلى حاوية فيخرج صفًّا من مربّعات فارغة — محرف “التوفو” — أو بلكنات ومحارف غير لاتينية مفقودة. والسبب يكاد يكون نفسه دائمًا: الخط الذي اخترته غير موجود في الصورة المنشورة.

يحلّ محرّك ⁨NextPDF⁩ الأصلي العامل داخل العملية الخطوط من ملفات خطوط يستطيع سجلّ الخطوط قراءتها. وهو لا يكتشف خطوط نظام التشغيل أو fontconfig تلقائيًا — فملفات الخطوط المثبّتة على نظام التشغيل لا تنفع إلا إذا سجّلت تلك الملفات صراحةً أو أضفت دليلها الحاوي إلى مسار بحث FontRegistry. وحاوية مبنية من صورة أساس نحيلة لا خطوط مثبّتة بـapt/apk فيها، وحتى حين تكون موجودة، يتجاهلها المحرّك الأصلي ما لم توجّه السجلّ إلى ملفاتها. العلاج هو تجميع ملفات الخطوط الفعلية داخل تطبيقك أو صورتك وتسجيلها في المحرّك. يقرأ السجلّ ملفات ⁨TrueType⁩ ‏(.ttf) و⁨OpenType⁩ ‏(.otf) ومجموعة ⁨TrueType⁩ ‏(.ttc)؛ ويُقبل أيضًا ⁨Type1⁩ القديم ‏(.pfb) لكنه نادرًا ما يلزم للعمل الجديد.

قبل أن تبدأ، أكّد أن هذه العناصر في مكانها:

  • ⁨NextPDF core⁩ مثبّت.
  • لديك ملفات الخطوط الفعلية التي تنوي استخدامها، وأنت مرخّص لتضمينها. حقوق التضمين مسؤوليتك — انظر تضمين خط ⁨TrueType⁩ وتقليصه.
  • يستطيع بناؤك نسخ تلك الملفات إلى الأثر المنشور.

هذا دليل عمليّ تشغيليّ. الشيفرة فيه قليلة؛ والعمل في البناء وفي تخطيط نظام الملفات. لميكانيكا تسجيل وجه واحد وتقليصه على مستوى ⁨API،⁩ اقرأ وصفة التضمين والتقليص المرتبطة أعلاه. تغطّي هذه الصفحة إيصال الملفات إلى الجهاز وتوجيه المحرّك إليها.

لماذا لا يجد المحرّك الأصلي خطوط نظام التشغيل تلقائيًا

قسم بعنوان «لماذا لا يجد المحرّك الأصلي خطوط نظام التشغيل تلقائيًا»

هناك مساران مختلفان للعرض، وقصّة الخطوط تختلف بينهما.

  • المحرّك الأصلي داخل العملية (الافتراضي، Document / writeHtml): لا يستدعي المحرّك نظام خطوط نظام التشغيل أو fontconfig للاكتشاف. بل يحلّ وجهًا عبر سجلّ الخطوط، الذي يقرأ ملف خط محدّدًا سجّلته أو يجد ملفًا داخل دليل هيّأته بوصفه مسار بحث. لا يفعل تثبيت خط بـapt-get install fonts-noto أو تشغيل fc-cache شيئًا بذاته — فالمحرّك الأصلي لا يرى تلك الملفات إلا إذا سجّلتها أو أضفت دليلها إلى مسار بحث السجلّ.
  • جسر ⁨Chrome⁩ (عارض ⁨HTML⁩ إلى ⁨PDF⁩ الذي يقود متصفّحًا بلا واجهة): يستخدم هذا المسار خطوط المضيف المثبّتة عبر اكتشاف الخطوط الطبيعي للمتصفّح، فتهمّ حزم خطوط apt/apk وfontconfig هناك.

إذا قرأت إرشاد “ثبّت حزم خطوط النظام هذه في ⁨Dockerfile⁩ لديك” العام، فهو ينطبق على جسر ⁨Chrome،⁩ لا على المحرّك الأصلي المغطّى في هذه الصفحة. للتوليد الأصلي، جمّع الملفات وسجّلها.

الخطوة 1 — جمّع ملفات الخطوط الفعلية

قسم بعنوان «الخطوة 1 — جمّع ملفات الخطوط الفعلية»

ضع ملفات الخطوط داخل شجرة تطبيقك حتى تُدار إصداراتها وتُشحن مع كل بناء. الموقع المتعارَف عليه هو دليل resources/fonts/.

your-app/
├── resources/
│ └── fonts/
│ ├── DejaVuSans.ttf
│ ├── DejaVuSans-B.ttf
│ └── NotoSansCJK-Regular.ttc
└── src/

سمِّ الملفات بحيث يستطيع بحث المحرّك في الدليل العثور عليها بالعائلة والنمط. حين تسجّل دليلًا (بدلًا من ملف محدّد) ثم تستدعي setFont('DejaVuSans', 'B', 12)، يبحث المحرّك عن ملفات مثل DejaVuSans-B.ttf أو DejaVuSansB.ttf أو DejaVuSans.ttf في كل دليل مهيّأ. يبني بحث الدليل تلك الأسماء المرشّحة من رمز النمط أحادي الحرف نفسه الذي تمرّره إلى setFont ‏(B للعريض، I للمائل، BI للعريض المائل)، لا كلمة مكتوبة بالكامل — فالشكل الموثوق هو Family-<StyleCode>.ttf (مثلًا DejaVuSans-B.ttf أو DejaVuSans-BI.ttfلا Family-Bold.ttf. ملف باسم DejaVuSans-Bold.ttf لا يجده بحث الدليل أبدًا؛ ولاستخدام مثل هذا الملف، سجّله صراحةً بـregister() — التي تحلّل الخط وتفهرسه تحت العائلة والنمط المقروءين من جداول أسماء الملف نفسه، فلا يعود اسم الملف المكتوب بالكامل مهمًّا (انظر الخطوة 2).

الخطوة 2 — سجّل الخطوط في المحرّك

قسم بعنوان «الخطوة 2 — سجّل الخطوط في المحرّك»

لديك طريقتان متكافئتان لجعل الملفات مرئية. وكلتاهما تمرّان عبر NextPDF\Typography\FontRegistry، التي تنفّذ NextPDF\Contracts\FontRegistryInterface.

سجّل ملفًا محدّدًا تحت اسم مستعار حين تتحكّم بالوجه بدقّة:

use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$registry->register(__DIR__ . '/../resources/fonts/DejaVuSans.ttf', alias: 'DejaVuSans');

يقبل register(string $fontFile, string $alias = '', int $fontIndex = 0) ملفات .ttf و.otf و.ttc، إضافةً إلى ⁨Type1⁩ القديم .pfb (الذي يحمّل قياسات .afm المرافقة من المسار نفسه)؛ ويختار $fontIndex خطًا فرعيًا داخل مجموعة ⁨TrueType⁩ ‏(.ttc). يحلّل register() الملف ويفهرس الوجه بالعائلة والنمط المقروءين من جداول أسمائه الخاصة، فيصبح اسم الملف الفيزيائي غير ذي صلة بمجرد التسجيل. والاسم المستعار $alias الاختياري ليس إلا اسم بحث إضافيًا للوجه — وليس رمز نمط ولا يغيّر النمط الذي يوفّره الملف؛ مرّره حين تريد استدعاء setFont() باسم غير اسم العائلة المضمّن للخط. ويعيد FontInfo المُحلّل.

سجّل دليلًا حين تريد أن يحلّ المحرّك الأوجه بالاسم من مجلّد تتحكّم به:

$registry = new FontRegistry('/var/www/app/resources/fonts');
// or, equivalently, after construction:
$registry->addFontDirectory('/var/www/app/resources/fonts');

يأخذ مُنشئ FontRegistry ذلك الدليل بوصفه وسيطه الأول، ويضيف addFontDirectory() مسارات بحث أخرى. ويكشف Document المجرّد أيضًا عن addFontDirectory() للحالة المستقلّة.

لاستخدام سجلّ ملأته بنفسك، ابنِ المستندات عبر DocumentFactory، التي تربط ذلك السجلّ بالذات بكل مستند تنشئه:

use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'Réndéred wîth a bundled face — no tofu.', newLine: true);
$doc->save('/tmp/out.pdf');

يبني Document::createStandalone() سجلّه الداخلي الخاص، فالوجه الذي سجّلته على FontRegistry منفصل غير مرئيّ له. في الإنتاج، امرر عبر DocumentFactory (أو مصنع إطار عملك) ليكون السجلّ المملوء هو المستخدَم.

يكشف كل تكامل إطار عن المفهومين نفسيهما بوصفهما تهيئة، فنادرًا ما تلمس السجلّ مباشرةً. في ملف nextpdf.php لحزمة ⁨Laravel،⁩ ‏fonts_path (الافتراضي NEXTPDF_FONTS_PATH، بالرجوع إلى resource_path('fonts')) هو دليل البحث، وpreload_fonts قائمة بمسارات ملفات خطوط مطلقة تُحلَّل عند إقلاع العامل. وجّه fonts_path إلى الدليل الذي جمّعته فتُحلّ أوجهك المسجَّلة تلقائيًا.

الخطوة 3 — وفّر الخطوط في صورة ⁨Docker⁩

قسم بعنوان «الخطوة 3 — وفّر الخطوط في صورة ⁨Docker⁩»

في حاوية، يجب أن تكون ملفات الخطوط جزءًا من طبقة الصورة، منسوخةً في وقت البناء. ولأن شيفرة التطبيق والخطوط تُشحن معًا حين تجمّعها تحت resources/fonts/، فإن COPY . . العادي يحملها أصلًا. إذا أبقيت الخطوط خارج سياق البناء، فانسخها صراحةً وتأكد من أن المسار الذي تسجّله يطابق المسار داخل الصورة.

# Native engine: NO system font packages are required.
# The native engine does not discover OS-installed fonts automatically; install OS
# font packages (`apt-get install fonts-*`) only if you also register them or point
# the font registry's search directory at their files.
FROM php:8.4-cli
WORKDIR /var/www/app
# Bundle the application, including resources/fonts/, into the image.
COPY . /var/www/app
# Make the bundled directory the engine's font search path.
ENV NEXTPDF_FONTS_PATH=/var/www/app/resources/fonts
CMD ["php", "bin/generate.php"]

على نظام ملفات غير قابل للتغيير أو للقراءة فقط (حاوية readOnlyRootFilesystem أو صورة عديمة الخوادم أو مضيف مُصلَّب)، تُقرأ ملفات الخطوط في وقت التوليد ولا تُكتب أبدًا، فمَركَب القراءة فقط يفي بالغرض. والكتابة الوحيدة التي قد يريدها المحرّك هي خبيئة خطوطه المُحلّلة: إمّا أن تعطي ذلك الدليل وحدة تخزين صغيرة قابلة للكتابة، أو أن تسخّن السجلّ وتقفله عند الإقلاع (القسم التالي) فلا تُحاول أي كتابة أو تسجيل في وقت التشغيل.

في عامل طويل العمر، حلّل كل وجه مرة عند الإقلاع، ثم اقفل السجلّ فلا يحدث تسجيل لكل طلب وتفشل التهيئة الخاطئة بصوت عالٍ بدلًا من التراجع بصمت:

$registry = new FontRegistry('/var/www/app/resources/fonts');
$registry->warmup([
'/var/www/app/resources/fonts/DejaVuSans.ttf',
'/var/www/app/resources/fonts/DejaVuSans-B.ttf',
]);
$registry->lock();

بعد lock()، يرفع كل من register() وaddFontDirectory() وwarmup() استثناءً، فيحوّل خطأ “مسار خاطئ في الصورة” إلى فشل إقلاع صارم بدلًا من صفحة توفو في الإنتاج.

أضف فحص دخان نشر يعرض صفحة واحدة بكل وجه مطلوب. لا يتحقق فحص الترويسة أدناه إلا من أن المستند أنتج خرجًا — لا يثبت أن الخط تحلّل أو تضمّن أو حتى انحلّ. فالوجه الذي لا يجده المحرّك قد يتراجع إلى خط أساس قياسي (وقد يوفّر ملف تعريف مطابقة، تحت السلوك الحالي غير الصارم، بديلًا مجمَّعًا بدلًا من ذلك) مع إصدار ملف ⁨PDF⁩ صالح غير فارغ — فحتى حيث يحدث ذلك التراجع لن يلتقط هذا الفحص وحده التدهور الصامت. لا تعتمد على كون التراجع مضمونًا أو صامتًا في كل مسار؛ تحقق من البرنامج المضمّن مباشرةً كما هو موضّح أدناه:

$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'warmup check', newLine: true);
$pdf = $doc->getPdfData();
// `getPdfData()` would normally throw on a real failure; this header check only
// confirms serialization returned PDF bytes, not that any specific font resolved.
if (!str_starts_with($pdf, '%PDF')) {
throw new RuntimeException('Font warmup smoke check produced no PDF output.');
}

لإفشال النشر فعلًا حين يكون وجه ما مفقودًا، افحص ملف ⁨PDF⁩ المُصدَر بحثًا عن برنامج الخط المضمّن. فالوجه المسجَّل الذي يتحلّل يحمل قاموس خطه الخاص ببرنامج مضمّن، فتأكيد وجوده يلتقط الحالة التي لم يتحلّل فيها الوجه المطلوب أبدًا (مهما تراجع إليه المحرّك) التي يفوّتها فحص الترويسة. ويعتمد المفتاح الذي يحمل البرنامج على صيغة المخطّط: تستخدم مخطّطات ⁨TrueType⁩ ‏(.ttf و.ttc) ‏/FontFile2، وتستخدم مخطّطات ⁨CFF/OpenType⁩ ‏(.otf بمخطّطات ⁨PostScript⁩) ‏/FontFile3، ويستخدم ⁨Type1⁩ القديم ‏(.pfb) ‏/FontFile.

إذا كان كل ما تحتاجه إشارة “بعض برنامج خط مضمّن” غير معتمدة على الصيغة، فاختبر بحثًا عن /FontFile وحده — ولأن /FontFile سلسلة فرعية من كل من /FontFile2 و/FontFile3، فإن فحص سلسلة فرعية مجرّد يطابق أصلًا كل نوع مخطّط، وتكون إضافة /FontFile2//FontFile3 فرعَي || إضافيين زائدة:

if (!str_contains($pdf, '/FontFile')) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

لكن /FontFile المجرّدة لا تستطيع تمييز أنواع المخطّطات. للتمييز بينها، طابِق على الرمز الدقيق بحدّ كلمة حتى لا تشتعل /FontFile أيضًا على /FontFile2 أو /FontFile3:

$isTrueType = preg_match('~/FontFile2\b~', $pdf) === 1; // TrueType (.ttf/.ttc)
$isCffOtf = preg_match('~/FontFile3\b~', $pdf) === 1; // CFF/OpenType (.otf)
$isType1 = preg_match('~/FontFile(?![23])\b~', $pdf) === 1; // Type1 (.pfb)
if (!$isTrueType && !$isCffOtf && !$isType1) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

على أيّ حال، عامِل هذا بوصفه استدلالًا تقريبيًا فقط، لا بوّابة نشر موثوقة. فبحث البايتات الخام عبر ملف ⁨PDF⁩ المُسلسَل غير دقيق لعدّة أسباب: يمكن أن تقيم برامج الخطوط داخل مجاري كائنات مضغوطة (حيث لا تظهر /FontFile* أبدًا بوصفها بايتات صرفة)، ويمكن أن تُلحِق التحديثات التزايدية كائنات أو تستبدلها، والخطوط غير المضمّنة أو القياسية الأربعة عشر لا تحمل برنامج خط على الإطلاق بحقّ، ويمكن أن تنقل اختلافات التسلسل (ترتيب الكائنات، المسافات، ترميز الأسماء) الرمز أو تخفيه. وهو في أفضل الأحوال يؤكد أن بعض وجه ضمّن برنامجًا — لا أن الوجه المحدّد الذي أردته تحلّل.

لبوّابة نشر حقيقية، لا تعتمد على بحث البايتات. حلّل ملف ⁨PDF⁩ المُصدَر بمحلّل ⁨PDF⁩ ملائم أو مفتّش كائنات وأكّد أن كائن الخط لوجهك الهدف يحمل برنامجًا مضمّنًا /FontFile//FontFile2//FontFile3، أو استخدم تأكيد حلّ خط يوفّره المنتج إن كان متاحًا لتكاملك. التعبيرات النمطية الواعية بالرموز أعلاه مفيدة لفحص سلامة محليّ سريع، لكن الفحص البنيوي هو ما ينبغي أن يُفشِل النشر. والتضمين وبنية قاموس الخط موصوفان في تضمين خط ⁨TrueType⁩ وتقليصه.

  • createStandalone() له سجلّه الخاص. الوجه المسجَّل على FontRegistry منفصل غير مرئيّ لمستند مستقلّ. استخدم DocumentFactory (أو مصنع الإطار) ليكون سجلّك هو النشط.
  • يجب أن توجد ملفات النمط بوصفها ملفات. لا يصطنع المحرّك العريض أو المائل من وجه عاديّ. إذا استدعيت setFont('DejaVuSans', 'B')، بحث الدليل عن DejaVuSans-B.ttf أو DejaVuSansB.ttf أو DejaVuSans.ttf (وبأحرف صغيرة ومتغيّرات .otf أيضًا) — يبني المرشّح من رمز النمط B الحرفيّ، فلا يبحث أبدًا عن DejaVuSans-Bold.ttf. ملف باسم مكتوب بالكامل مثل DejaVuSans-Bold.ttf لا يُحلّ إلا حين تسجّله صراحةً بـregister()، التي تفهرسه بالعائلة والنمط المقروءين من جداول أسماء الملف نفسه بصرف النظر عن اسم الملف؛ والاعتماد على بحث الدليل لإيجاده ينتج فشلًا، وبعده قد يتراجع المحرّك إلى خط أساس (لا مسار مضمون ولا دائم الصمت) — وهو التدهور الذي تحذّر منه هذه الصفحة.
  • مسارات مغلّف المجرى والمسارات البعيدة مرفوضة. يرفض السجلّ المسارات التي تحتوي مخطّط ⁨URI⁩ أو بايت خال. سجّل الملفات المحليّة فقط؛ للخطوط المجلوبة في وقت التشغيل استخدم registerFromBinary() بالبايتات الخام.
  • السجلّ المقفل غير قابل للتغيير. بمجرد استدعاء lock()، يرفع أي register() أو addFontDirectory() أو warmup() لاحق استثناءً. وتبقى طرائق البحث متاحة. سجّل وسخّن كل شيء قبل القفل.
  • مجموعات ⁨CJK⁩ كبيرة. سجّل الخط الفرعي الصحيح من .ttc بـ$fontIndex، واحسب حسابًا لمجموعة جزئية مضمّنة أكبر. انظر ملاحظات ⁨CJK⁩ في وصفة التضمين والتقليص.
  • ملف الخط مدخل ثنائيّ غير موثوق. جمّع فقط الخطوط من مصادر تثق بها، وتحقق من منشأ أي وجه مقبول من مستخدمين نهائيين.
  • قفل السجلّ بعد التسخين يزيل سطح تغيير في وقت التشغيل ويجعل خطأ مسار يفشل عند الإقلاع بدلًا من تدهور الخرج بصمت.
  • لا تُقحِم مدخل المستخدم في مسار ملف مسجَّل. سجّل مجموعة ثابتة من الأوجه المجمَّعة؛ ولا تدع طلبًا يختار مسار نظام ملفات اعتباطيًا.

لا يدّعي هذا الدليل أي ادعاء معياري. فكل رمز معروض سطح عام متحقَّق منه: NextPDF\Typography\FontRegistry ‏(register() وaddFontDirectory() وwarmup() و lock()، ووسيط مُنشئ الدليل)، وعقدها NextPDF\Contracts\FontRegistryInterface، و NextPDF\Core\DocumentFactory::create()، وNextPDF\Core\Document::setFont() / addFontDirectory(). ومفتاحا ⁨Laravel⁩ ‏fonts_path وpreload_fonts هما التهيئة الموثّقة لحزمة nextpdf/laravel. وسلوك التضمين ووسم المجموعة الجزئية، بما له من استشهادات ⁨ISO 32000-2،⁩ موثّق في وصفة التضمين والتقليص المرتبطة تحت “انظر أيضًا”.