تقليل حجم ملف PDF بالضغط وتجزئة الخطوط
لمحة سريعة
قسم بعنوان «لمحة سريعة»تحتاج إلى أصغر ملف PDF يسمح به المحتوى، من دون أي فقد في الدقة. يوفّر لك NextPDF أداتين للتحكّم في حجم الملف، وكلتاهما مفعّلتان افتراضيًا:
- ضغط الدفق. يغلّف الكاتب دفق محتوى كل صفحة وبرنامج كل خط مضمَّن داخل دفق FlateDecode (zlib). تحتفظ الراية
compressفيNextPDF\Core\Configبهذا الإعداد. عدّله عبر أداة التعديلwithCompress()عند بناء مستند تدفقي. - تجزئة الخط. عند تضمين خط TrueType أو CFF، يعيد الكاتب بناء برنامج الخط بحيث لا يتضمن إلا المحارف الرسومية التي يستخدمها المستند، ثم يضغط الناتج بترميز FlateDecode. يحدث ذلك تلقائيًا. لا تضبط أي راية ولا تستدعِ أي طريقة. فخط CJK المؤلَّف من
20,000محرف رسومي والذي لا يستخدم منه المستند إلا بضع مئات من المحارف يُضمَّن بجزء يسير من حجمه على القرص.
من المهم توضيح ذلك من البداية: لا يتيح NextPDF Core إعادة أخذ عينات الصور، ولا عنصر تحكّم في جودة الصور، ولا مفتاح تبديل لدفق الكائنات، ولا إعدادًا لإزالة تكرار الموارد. الأداتان أعلاه هما أداتا التحكّم الوحيدتان في الحجم. توضّح لك بقية هذه الوصفة كيفية استخدامهما بالطريقة الصحيحة، وما الذي لا تقدّمه كل منهما.
المتطلبات المسبقة: ثبّت Core (composer require nextpdf/core:^3)، وجهّز ملف خط مرخّصًا لك بتضمينه لاستخدامه في مسار التجزئة.
التثبيت
قسم بعنوان «التثبيت»composer require nextpdf/core:^3نظرة مفاهيمية عامة
قسم بعنوان «نظرة مفاهيمية عامة»ملف PDF شجرة من الكائنات. أكبر الكائنات عادةً هي دفوق المحتوى (مُعامِلات الرسم لكل صفحة) وبرامج الخطوط (مخططات المحارف الرسومية المضمَّنة). ينضغط كلاهما جيدًا، لذا فإن أكثر أدوات التحكّم في الحجم فعاليةً هي ضغطهما بترميز FlateDecode. FlateDecode هو الاسم المعتمد في PDF 2.0 لدفق DEFLATE مغلَّف بترميز zlib (ISO 32000-2:2020 §7.4.4)، وهو المرشِّح الذي يُصدره NextPDF.
يثبّت الكاتب مستوى ضغط DEFLATE عند 9، وهو الحد الأقصى في RFC 1951، عبر NextPDF\Writer\PinnedZlibCompressor. يقايض المستوى 9 قليلًا من وقت المعالج الإضافي مقابل أصغر دفق. كما يجعل تثبيت المستوى المخرجات حتمية، لأن ترويسة zlib تُرمّز المستوى، وأي تغيير في المستوى سيغيّر البايتات. أنت لا تختار المستوى — فالمحرك يثبّته بحيث يُنتج تشغيلان على المدخل نفسه دفوقًا متطابقة بايتًا ببايت.
الأداة الثانية هي تجزئة الخط. يحمل ملف الخط على القرص كل محرف رسومي يُعرّفه الوجه الطباعي، لكن مستندًا يطبع “Invoice 2026” لا يحتاج إلا إلى قلة منها. يجتاز NextPDF\Typography\FontSubsetter (لخطوط TrueType) وNextPDF\Typography\CffSubsetter (لخطوط CFF / OpenType) النقاط الرمزية التي عرضها المستند فعليًا، ويحلّان تبعيات المحارف المركّبة، ويعيدان بناء جداول الخط المطلوبة فقط. ويُصدِران ملفًا ثنائيًا صالحًا للخط المُجزَّأ يحمل وسمًا حتميًا لبادئة التجزئة من ستة أحرف (ISO 32000-2:2020 §9.9). يطبّق الكاتب ذلك كلما عُرفت مجموعة المحارف المستخدمة لخط مضمَّن، ثم يضغط الجزء بترميز FlateDecode. إذا كانت تجزئة وجه معيّن ستوفّر أقل من عشرة بالمئة، فإن أداة التجزئة تُعيد البرنامج الأصلي بدلًا منه، لأن تكلفة إعادة البناء لا تستحق مكسبًا هامشيًا.
الخلاصة: أبقِ ملفات PDF صغيرة بترك الضغط مفعّلًا (الإعداد الافتراضي) وبتضمين ملفات خطوط حقيقية (بحيث يكون للتجزئة ما تقلّصه)، لا بضبط قائمة طويلة من الخيارات.
واجهة API
قسم بعنوان «واجهة API»عنصر التحكّم الوحيد في الحجم الذي تضبطه موجود على كائن الإعداد.
NextPDF\Core\Config هو كائن قيمة غير قابل للتغيير من نوع final readonly مزوَّد بطرائق تعديل مُحدَّدة الأنواع. العضو المتعلق بالحجم هو:
compress(bool، الافتراضيtrue) — يفعّل ضغط FlateDecode. غيّره عبرwithCompress(bool $compress): self، الذي يُعيدConfigجديدًا بالراية مُغيَّرة مع الحفاظ على كل حقل آخر.
أرفِق Config بمستند عند الإنشاء:
- يبني
NextPDF\Core\Document::createStandalone(?Config $config = null): selfمستندًا بسجلّات عابرة مناسبًا لنص برمجي عبر CLI أو لعملية قصيرة العمر، مع تطبيقConfigالخاص بك.
يُحدِّد عضوان ما تتعامل معه أدوات الحجم، لكن ليس أيٌّ منهما في حد ذاته أداة تحكّم في الضغط:
imageCacheBytes(int، الافتراضي52_428_800) يضع حدًا أقصى لذاكرة التخزين المؤقت للصور في الذاكرة، وwithImageCacheBytes(int $bytes): selfيغيّره. يحدّ هذا من ذروة الذاكرة أثناء البناء. إنه لا يعيد أخذ العينات، ولا يعيد الضغط، ولا يقلّص بأي شكل آخر الصور التي تُضمّنها — إنه سقف للذاكرة، لا أداة تحكّم في حجم المخرجات.- يضبط
fontsDirectory(string) وwithFontsDirectory(string $dir): selfمسار البحث الافتراضي عن ملفات الخطوط، الذي يغذّي مسار التجزئة.
تتعامل مع الخطوط عبر واجهة الطباعة على المستند:
- يختار
setFont(string $family, string $style = '', float $size = 12.0): staticوجهًا. عندما تُحلّ العائلة إلى ملف خط قابل للتضمين، يسجّل الكاتب النقاط الرمزية التي تعرضها لكي يتمكّن من تجزئة ذلك الوجه عند الحفظ. - يسجّل
addFontDirectory(string $directory): staticدليلًا إضافيًا للبحث عن ملفات الخطوط.
المخرجات هي الثلاثي القياسي: يُعيد getPdfData(): string البايتات، ويكتبها save(string $path): void كتابةً ذرّية، ويتولّى output(?string $filename, OutputDestination $dest): string التسليم عبر HTTP.
ليست للتجزئة طريقة عامة ولا راية. إنها خاصية تنشأ عن تضمين خط وعرض نص. يشغّل الكاتب FontSubsetter / CffSubsetter نيابةً عنك داخل NextPDF\Writer\PdfFontWriter.
مثال برمجي — بداية سريعة
قسم بعنوان «مثال برمجي — بداية سريعة»يبني هذا المثال مستندًا مع تفعيل الضغط صراحةً واستخدام خط مضمَّن ومُجزَّأ، ثم يكتب البايتات. يتجاوز المثال معالجة الأخطاء لإبقاء شكل الاستدعاء واضحًا. يضيف المثال الإنتاجي أدناه كامل الضمانات.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);$doc->addFontDirectory(__DIR__ . '/fonts');$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer// subsets this font automatically when the document is built.$doc->setFont('LiberationSans', '', 12);$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));مثال برمجي — إنتاجي
قسم بعنوان «مثال برمجي — إنتاجي»هذا برنامج قائم بذاته. يبني مستندًا مع تفعيل الضغط، ويُضمّن خطًا من دليل تتحكّم فيه، ويعرض نصًا لكي تتوفر لأداة التجزئة مجموعة من المحارف المستخدمة، ثم يكتب الناتج بصورة ذرّية. يلتقط البرنامج أكثر استثناءات NextPDF تحديدًا مما يثيره مسارا البناء والحفظ، ثم يعيد إثارة كل منها مع سياقه بدلًا من ابتلاعه. وجّه NEXTPDF_FONT_DIR إلى دليل يحتوي على وجه TrueType أو CFF مرخّص لك بتضمينه؛ يتحقق البرنامج من المسار قبل التضمين.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\Exception\CompressionException;use NextPDF\Exception\InvalidConfigException;
/** * Resolve and validate the font directory from a server-controlled source. * * Reading the directory from the environment keeps the path off the request * surface. The function rejects a missing or unreadable directory so the * embedding path never runs against untrusted or absent input. */function resolveFontDirectory(): string{ $configured = getenv('NEXTPDF_FONT_DIR'); $dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir); if ($real === false || !is_dir($real) || !is_readable($real)) { throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir)); }
return $real;}
/** * Build a compressed, font-subsetted document and return its bytes. * * @param non-empty-string $fontDirectory Validated directory of embeddable fonts. * * @return string Raw PDF bytes. */function buildCompactPdf(string $fontDirectory): string{ // compress is true by default; pin it so the intent is explicit and the // streaming writer path honours it regardless of any wrapper defaults. $config = (new Config()) ->withCompress(true) ->withFontsDirectory($fontDirectory) // Bound the image cache so a build cannot exhaust memory. This is a // memory ceiling, not an output-size control. ->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config); $doc->addFontDirectory($fontDirectory); $doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the // writer turns into a font subset at build time. $doc->setFont('LiberationSans', '', 12); $doc->cell(0, 10, 'Invoice 2026', newLine: true); $doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program // are FlateDecode-compressed before the bytes are returned. return $doc->getPdfData();}
try { $fontDirectory = resolveFontDirectory(); $pdf = buildCompactPdf($fontDirectory);} catch (CompressionException $e) { // Raised if the zlib encoder hard-fails while compressing a stream. throw new RuntimeException( sprintf('Compression failed for a %s stream.', $e->getAlgorithm()), previous: $e, );} catch (InvalidConfigException $e) { // Raised by the output path for an invalid destination configuration. throw new RuntimeException( sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()), previous: $e, );}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) { throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);مخرجات STDOUT المتوقَّعة (يعتمد عدد البايتات على الخط وعلى عملية البناء):
Wrote <n> bytes to <path>.الحالات الحدّية والمزالق
قسم بعنوان «الحالات الحدّية والمزالق»- الضغط مفعّل افتراضيًا. يكون
compressفيConfigجديد مضبوطًا علىtrue. نادرًا ما تحتاج إلىwithCompress()أصلًا. اضبطه صراحةً فقط لتوثيق القصد، أو لإلغاء التفعيل في بناء تصحيحي تريد فيه قراءة الدفوق الخام. - إيقاف الضغط يجعل الملفات أكبر، لا أصغر.
withCompress(false)أداة تشخيصية لفحص الدفوق غير المضغوطة. وهي ليست أبدًا تحسينًا للحجم. انشر المنتج والضغط مفعّل. - تحتاج التجزئة إلى خط مضمَّن حقيقي. يُشار إلى خطوط Base14 القياسية (Helvetica وTimes وCourier وما يماثلها) بالاسم، ولا تحمل برنامجًا مضمَّنًا في مستند عادي، لذا ليس هناك ما يُجزَّأ. لا تقلّص التجزئة سوى الوجوه التي تُضمّنها من ملف خط.
- التجزئة تلقائية وصامتة. لا توجد راية ولا طريقة ولا تأكيد. إذا ضمّنت خطًا وعرضت نصًا به، فسيجزّئه الكاتب. يحمل البرنامج المضمَّن وسم بادئة تجزئة من ستة أحرف (مثل
ABCDEF+LiberationSans) لكي يتمكّن القارئ من التمييز بين الجزء والتضمين الكامل. - التوفير الضئيل يُبقي الخط كاملًا. عندما يوفّر الجزء أقل من عشرة بالمئة من حجم البرنامج، تُعيد أداة التجزئة البرنامج الأصلي. هذا حدّ أدنى متعمَّد: فتكلفة إعادة البناء لا تستحق مكسبًا هامشيًا. قد يحدث ذلك عند تضمين وجه صغير الحجم أصلًا، أو عند عرض كل محارفه الرسومية تقريبًا.
imageCacheBytesليس عنصر تحكّم في حجم الصور. إنه يحدّ سقف الذاكرة، لا بايتات المخرجات. يُضمّن NextPDF Core بيانات الصور التي تمنحه إياها؛ ولا توجد خطوة إعادة أخذ عينات ولا خفض دقة ولا إعادة ترميز. إذا احتجت صورًا أصغر، فأعِد تحجيمها وترميزها قبل تضمينها.- لا يوجد إعداد لدفق الكائنات ولا لإزالة التكرار. لا يتيح NextPDF Core مفتاح تبديل لدفوق كائنات PDF 2.0 ولا لإزالة تكرار الموارد. لا تبحث عن إعداد كهذا — فأداتا الحجم هما ضغط الدفق وتجزئة الخط.
الأداء
قسم بعنوان «الأداء»الضغط عند المستوى 9 هو أكبر تكلفة على المعالج عند كتابة دفق. وهو يقايض نسبة ضئيلة من وقت البناء مقابل أصغر مخرجات. التكلفة خطية بالنسبة لعدد البايتات غير المضغوطة، لذا يحدّد عدد الصفحات وكمية بيانات الخطوط المضمَّنة الميزانية. تضيف التجزئة تمريرة واحدة لكل وجه مضمَّن تحلّل دليل جداول الخط، وتحلّ انغلاق المحارف المستخدمة، وتعيد بناء الجداول المطلوبة. بالنسبة إلى وجه CJK كبير، تُعدّ هذه الأداة الأغلى بين الأداتين، لكنها تُنفَّذ مرة واحدة لكل خط، لا مرة واحدة لكل صفحة. يوجد حدّ التوفير الأدنى البالغ عشرة بالمئة جزئيًا لإبقاء تلك التمريرة خارج المسار الساخن عندما لا تكون مُجدية. يبقى مستند صغير بجزء مضمَّن واحد بسهولة ضمن حدّ زمني قدره 1500 ms وميزانية ذروة قدرها 96 MB. اضبط imageCacheBytes عند سقفك الحقيقي لكي يفشل البناء الذي يُضمّن صورًا كثيرة سريعًا بسبب الذاكرة بدلًا من التبديل إلى القرص.
ملاحظات أمنية
قسم بعنوان «ملاحظات أمنية»يجري البناء داخل العملية؛ ولا تغادر أي بايتات من المستند إلى المضيف ولا يُجرى أي استدعاء شبكي. عامِل أي خط أو صورة مُورَّدة خارجيًا على أنها مدخل غير موثوق:
- تحقّق من دليل الخطوط. يقرأ المثال الإنتاجي مسار الخط من متغير بيئة يتحكّم فيه الخادم، ويرفض دليلًا مفقودًا أو غير قابل للقراءة قبل التضمين. لا تشتق أبدًا مسار خط من حقل في الطلب.
- لا تُضمّن سوى الخطوط المرخّص لك بإعادة توزيعها. يبقى الجزء برنامج خط مضمَّنًا. تأكّد من أن الترخيص يسمح بالتضمين قبل نشر مستند يحمل الوجه.
- الخطوط المشوَّهة تثير استثناءً، ولا تُفسد بصمت. يثير ملف الخط الذي يفشل تحليله
NextPDF\Exception\FontParsingException، ويثير فشل zlib القاسيNextPDF\Exception\CompressionException. التقط أكثر الاستثناءات تحديدًا وتصرّف بناءً عليها. لا تغلّف البناء أبدًا داخلcatchفارغ. - لا تُدرج أبدًا مدخلات المستخدم في مسار المخرجات. يكتب المثال إلى مسار ثابت أو قناة جانبية يتحكّم فيها الخادم، ويرفض أغلفة الدفق والبايتات الخالية عبر الكاتب الذرّي في
save(). اشتق مسارات المخرجات من قيم يتحكّم فيها الخادم لتجنّب اجتياز المسارات. - لا أسرار في المستند. لا تُضمّن بيانات اعتماد أو رموزًا أو معرّفات داخلية في مستند مُولَّد تُعيده إلى العميل.
المطابقة
قسم بعنوان «المطابقة»لا تقدّم هذه الوصفة أي ادعاء معياري مستقل يتعلق بالمعايير. الآليات التي تستخدمها مُعرَّفة في مواصفة PDF 2.0: ضغط دفق FlateDecode (ISO 32000-2:2020 §7.4.4) وتسمية جزء الخط ببادئة تجزئة من ستة أحرف (ISO 32000-2:2020 §9.9). يُصدر NextPDF كلتيهما كجزء من مسار الكتابة القياسي لديه؛ ولا تضبطهما أبعد من الراية compress. يعكس ملف قابلية الاستنساخ structural الذي تعلنه هذه الصفحة أن الكاتب يثبّت مستوى DEFLATE، لذا فإن الدفوق المضغوطة حتمية، بينما قد تظل المعرّفات على مستوى المستند مختلفة بين عمليات التشغيل ما لم تضبط أيضًا إعدادات حتمية. لمعرفة آليات التضمين التي تقف وراء التجزئة، راجع وصفة التضمين والتجزئة المرتبطة أدناه.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- تضمين خط TrueType وتجزئته — سجّل وجهًا، واعرض به، وافحص وسم الجزء المضمَّن.
- تأليف النصوص والخطوط — واجهة تأليف النصوص والخطوط الأوسع التي تغذّي مسار التجزئة.
- مرجع وحدة الإعداد — كائن القيمة
Configالكامل، وطرائق التعديل الخاصة به، وقيمها الافتراضية. - معالجة الأخطاء المدركة للاستثناءات — تسلسل استثناءات NextPDF الذي يقف وراء
CompressionExceptionوFontParsingExceptionوInvalidConfigException.