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

تقليل حجم ملف PDF بالضغط وتجزئة الخطوط

تحتاج إلى أصغر ملف ⁨PDF⁩ يسمح به المحتوى، من دون أي فقد في الدقة. يوفّر لك ⁨NextPDF⁩ أداتين للتحكّم في حجم الملف، وكلتاهما مفعّلتان افتراضيًا:

  • ضغط الدفق. يغلّف الكاتب دفق محتوى كل صفحة وبرنامج كل خط مضمَّن داخل دفق ⁨FlateDecode⁩ ‏(⁨zlib⁩). تحتفظ الراية compress في NextPDF\Core\Config بهذا الإعداد. عدّله عبر أداة التعديل withCompress() عند بناء مستند تدفقي.
  • تجزئة الخط. عند تضمين خط ⁨TrueType⁩ أو ⁨CFF⁩، يعيد الكاتب بناء برنامج الخط بحيث لا يتضمن إلا المحارف الرسومية التي يستخدمها المستند، ثم يضغط الناتج بترميز ⁨FlateDecode.⁩ يحدث ذلك تلقائيًا. لا تضبط أي راية ولا تستدعِ أي طريقة. فخط ⁨CJK⁩ المؤلَّف من 20,000 محرف رسومي والذي لا يستخدم منه المستند إلا بضع مئات من المحارف يُضمَّن بجزء يسير من حجمه على القرص.

من المهم توضيح ذلك من البداية: لا يتيح ⁨NextPDF Core⁩ إعادة أخذ عينات الصور، ولا عنصر تحكّم في جودة الصور، ولا مفتاح تبديل لدفق الكائنات، ولا إعدادًا لإزالة تكرار الموارد. الأداتان أعلاه هما أداتا التحكّم الوحيدتان في الحجم. توضّح لك بقية هذه الوصفة كيفية استخدامهما بالطريقة الصحيحة، وما الذي لا تقدّمه كل منهما.

المتطلبات المسبقة: ثبّت ⁨Core⁩ ‏(composer require nextpdf/core:^3)، وجهّز ملف خط مرخّصًا لك بتضمينه لاستخدامه في مسار التجزئة.

Terminal window
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⁩ صغيرة بترك الضغط مفعّلًا (الإعداد الافتراضي) وبتضمين ملفات خطوط حقيقية (بحيث يكون للتجزئة ما تقلّصه)، لا بضبط قائمة طويلة من الخيارات.

عنصر التحكّم الوحيد في الحجم الذي تضبطه موجود على كائن الإعداد.

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⁩، لذا فإن الدفوق المضغوطة حتمية، بينما قد تظل المعرّفات على مستوى المستند مختلفة بين عمليات التشغيل ما لم تضبط أيضًا إعدادات حتمية. لمعرفة آليات التضمين التي تقف وراء التجزئة، راجع وصفة التضمين والتجزئة المرتبطة أدناه.