مقال تقني

تقليل حجم ملف PDF في دلفي: الخطوط والصور وLZW

لتقليل حجم ملف PDF في دلفي، توفر مكتبة losLab PDF ثلاث واجهات برمجة تطبيقات تهاجم أكبر ثلاثة مصادر للتضخم: تعيد SubsetEmbeddedFonts كتابة كل برنامج خط TrueType مضمن وصولاً إلى الصور الرمزية التي يعرضها المستند بالفعل، وتقوم DownsampleImages بإعادة معاينة الصور النقطية التي تتجاوز دقة DPI المستهدفة، وتستبدل NormalizeLZWStreams ضغط LZWDecode القديم بـ FlateDecode. وترجع كل منها عدد الكائنات التي غيرتها، وبالتالي تخبرك القيمة صفر أن التمريرة كانت غير فعالة (no-op) بدلاً من كونها فشلاً صامتًا

لماذا يكون ملف PDF المدمج الخاص بي أكبر من ملفاته المصدر؟

عادة ما يكون حجم ملف PDF المدمج أو المنشأ برمجياً أكبر من اللازم لأحد ثلاثة أسباب: الخطوط المضمنة بالكامل، أو معاينة الصور بدقة أعلى بكثير من دقة عرضها، أو التدفقات التي لا تزال مضغوطة باستخدام مرشح LZW القديم. ويسمح معيار ISO 32000-1 §9.9 للمنتج بتضمين برنامج الخط الكامل، ويفعل معظم المنتجين ذلك بالضبط لأنه الخيار الافتراضي الآمن. ويصل حجم ملف Arial FontFile2 الكامل إلى مئات الكيلوبايتات؛ وتضمينه في اثني عشر ملفًا مصدرًا ودمجها يعني أنك تحمل اثنتي عشرة نسخة من الخطوط الخارجية للصور الرمزية لأحرف لم يكتبها أحد. والدمج في حد ذاته لا يخلق الهدر، بل يركزه فقط في ملف واحد حيث يصبح المجموع مرئيًا في النهاية

تعد الصور هي الجاني الثاني. فشحن مسح ضوئي بعرض 4800 بكسل موضوع في إطار ربع صفحة يرسل حوالي 40 ضعفًا من بيانات البكسل مقارنة بما يمكن لخط أنابيب طباعة بدقة 300 DPI استخدامه. أما الثالث فهو أكثر هدوءًا: التدفقات المرشحة بـ LZWDecode. يحدد معيار ISO 32000-1 §7.4.4 كلا من LZWDecode و FlateDecode، ويشير إلى أن Flate عادة ما يضغط على الأقل بنفس الكفاءة؛ وعملياً، تكون مخرجات Flate أصغر باستمرار على نفس البيانات، ويبقى LZW غالباً في الملفات التي مرت عبر أدوات من حقبة التسعينيات في مرحلة ما من تاريخها. يستعرض باقي هذا المقال التمريرات الثلاث لمكتبة losLab PDF التي توفر حلاً لكل مشكلة، ثم يجمعها في خط تدفق واحد

تجزئة الخطوط باستخدام SubsetEmbeddedFonts

تقوم SubsetEmbeddedFonts بتقليص كل خط TrueType مضمن في مستند محمل إلى الأحرف التي يستخدمها المستند بالفعل، ولا تحتاج إلى أي معاملات لأنها تشتق قائمة الحفظ من تدفقات المحتوى نفسها. وتقوم التمريرة داخلياً بالمرور عبر تدفق محتوى كل صفحة باستخدام GetTextRuns، وتجمع رموز الأحرف المشار إليها تحت كل مورد خط، وتبني قائمة حفظ، وتسلم برنامج الخط الأصلي إلى محرك Windows FontSub (أي CreateFontPackage) لإنتاج جزء فرعي (subset). ويحل البرنامج المعاد كتابته محل تدفق FontFile2 في مكانه، ويكتسب اسم BaseFont بادئة LOSABC+، وهي اتفاقية الأحرف الستة الكبيرة وعلامة الزائد التي يحددها معيار ISO 32000-1 §9.6.4 للخطوط المجزأة. وتلك البادئة هي أيضًا ما يجعل الاستدعاء متساوي القوة (idempotent): قم بتشغيل التمريرة مرتين وسيتم التعرف على الخطوط المجزأة بالفعل وتجاوزها، مما يجعل إدراجها في وظيفة دفعة (batch job) قد تعيد زيارة الملفات أمرًا آمنًا

var
  Lib: TPDFlib;
  Fonts: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
    begin
      Fonts := Lib.SubsetEmbeddedFonts;
      // Fonts = number of FontFile2 programs rewritten;
      // 0 means nothing embedded, or everything already subsetted
      Lib.SaveToFile('merged-report-subset.pdf');
    end;
  finally
    Lib.Free;
  end;
end;

هناك تفصيلان في التنفيذ جديران بالمعرفة لأنهما يشرحان حدود واجهة برمجة التطبيقات. أولاً، تستهدف التمريرة FontFile2، لذا فهي تغطي برامج TrueType المضمنة؛ أما الخطوط المضمنة كـ Type 1 أو CFF مجرد فتدع دون مساس بدلاً من المخاطرة بها. ثانياً، تعتمد على FontSub، مما يجعل SubsetEmbeddedFonts حصرية بنظام تشغيل Windows فقط. ونقطة أكثر دقة من التنفيذ: يتم تحديد ما إذا كان الخط مؤهلاً من خلال حل سلسلة المراجع FontDescriptorFontFile2 فعلياً، وليس بالاعتماد على خوارزمية تحديد علامة التضمين، لأن الخطوط في مستند محمل لم تمر أبداً بعملية مسك الدفاتر من جانب الإنشاء التي تحدد مثل هذه العلامات. وإذا كان الدفق المحلول موجودًا، يكون الخط مرشحًا؛ وإذا لم يكن كذلك، يتم تجاوزه دون حدوث خطأ

المقايضة النزيهة: يحتوي الخط المجزأ فقط على الصور الرمزية الموجودة في وقت التجزئة. وإذا قامت أداة لاحقة، أو الكود الخاص بك، بإضافة نص لاحقًا بنفس الخط، فلن يكون لأي حرف خارج التجزئة خط خارجي وسيظهر كشكل رمزي مفقود. قم بالتجزئة كخطوة أخيرة لتغيير المحتوى، وليس قبل مرحلة التحرير أبدًا. وينطبق الحذر نفسه إذا كنت تخطط لسحب الخط مرة أخرى لاحقًا لإعادة الاستخدام؛ ويغطي المقال الخاص بـ استخراج النصوص والصور والخطوط باستخدام PDFlibPas ما يمكن وما لا يمكن لبرنامج التجزئة المستخرج أن يقدمه لك

كيف تقرر DownsampleImages الصور التي يجب تقليصها؟

تقوم DownsampleImages(MaxDPI, Quality, Filter) بإعادة معاينة الصور التي يمكنها بثقة اعتبارها ذات دقة زائدة فقط، باستخدام تقدير متحفظ عمدًا لدقة DPI. يخزن كائن الصورة XObject في PDF أبعاد البكسل ولكن ليس هناك دقة مادية موثوقة، ونادرًا ما تنجو أي علامة DPI من الصورة المصدر من دورة التحميل والتحرير والحفظ. لذلك تقدر التمريرة SrcDPI = PixelWidth / 8.5، وتسأل في الواقع: إذا امتدت هذه الصورة على العرض الكامل لصفحة بمقاس Letter، فما هي دقتها؟ ويتم التعامل فقط مع الصور التي يتجاوز تقديرها MaxDPI. وهذا التحيز مقصود: فالصورة الموضوعة بحجم صغير على الصفحة لها دقة DPI حقيقية أعلى من التقدير، لذا تعمل التمريرة بأقل من طاقتها بدلاً من خفض جودة أصل ذي جودة طباعة لا يمكنها قياسه

تحدد المعلمة Quality من 1 إلى 100 جودة إعادة تشفير JPEG، بينما تبقي القيمة 0 المخرجات في وضع Flate غير الفاقد للجودة بنمط PNG؛ وتختار المعلمة Filter نواة إعادة المعاينة، 0 لمتوسط الصندوق (box average) و 1 للثنائي الخطي (bilinear). وبالنسبة للمستندات المكتبية الممسوحة ضوئيًا، يعد DownsampleImages(150, 75, 1) نقطة بداية معقولة؛ وبالنسبة لأي شيء قد يعاد طباعته، ارفع MaxDPI إلى 300 أو تخطى التمريرة بالكامل. وتعد إعادة المعاينة لتقليل الدقة هي الخطوة الوحيدة الفاقدة للجودة من بين الخطوات الثلاث، لذا يجب وضعها خلف إعداد يمكن للمستخدمين إيقاف تشغيله

تحويل تدفقات LZW القديمة باستخدام NormalizeLZWStreams

تعد NormalizeLZWStreams فوزًا مجانيًا: فهي تلغي ضغط كل دفق LZWDecode دون فقدان للبيانات وتعيد ضغطه باستخدام FlateDecode في مكانه، وترجع عدد التدفقات التي تم تحويلها. وتتعامل مع كل من إدخال /Filter /LZWDecode الفردي وظهور LZW داخل مصفوفة سلسلة مرشحات، حيث يتم استبدال ارتباط LZW فقط ويتم الحفاظ على بقية السلسلة. وتُقرأ معلمات التنبؤ (Predictor, Columns, Colors, BitsPerComponent) من DecodeParms الخاصة بالدفق وتُمرر إلى أداة إلغاء الضغط، بحيث تدور بيانات الصور المشفرة بالتنبؤ بشكل صحيح. ونظرًا لأن كلا المرشحين هما برامج ترميز دقيقة على مستوى البت، فإن البايتات المفككة تكون متطابقة قبل وبعد؛ ويتغير فقط ضغط الحاوية، ولهذا السبب تكون هذه التمريرة آمنة للتشغيل دون شروط على كل ملف

في مستند لا يحتوي على تدفقات LZW، يرجع الاستدعاء ببساطة 0 ولا يلمس شيئًا، وهو ما تختبره مجموعة اختبارات التراجع الخاصة بالمكتبة بشكل صريح: يجب أن يبلغ ملف Flate فقط والمنشأ حديثًا عن صفر تحويلات. وضمان عدم الفعالية (no-op) هذا مهم عندما تقع التمريرة في خط تدفق يعالج آلاف الملفات المتنوعة، بعضها من عام 2024 وبعضها من عام 1998

خط تدفق تحسين الحجم الكامل في دلفي

تتحد التمريرات الثلاث في وظيفة واحدة للتحميل والتحسين والحفظ، والترتيب يهم أقل مما قد تتوقع لأنها تعمل على أنواع كائنات منفصلة: الخطوط، وكائنات الصور XObjects، ومرشحات التدفق. ويظل تشغيل التجزئة أولاً هو الخيار المنظم، نظرًا لأنها التمريرة التي تفرض قيدًا على ترتيب التحرير

function OptimizePDF(const Src, Dst: string): Boolean;
var
  Lib: TPDFlib;
  Fonts, Images, Streams: Integer;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      Exit;
    Fonts   := Lib.SubsetEmbeddedFonts;        // TrueType FontFile2 -> subset
    Images  := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
    Streams := Lib.NormalizeLZWStreams;        // LZWDecode -> FlateDecode
    Result := Lib.SaveToFile(Dst) = 1;
    // Log Fonts/Images/Streams: three zeros mean the file was already lean
  finally
    Lib.Free;
  end;
end;

تحقق من خط التدفق بالطريقة التي تتحقق بها المكتبة من نفسها: رحلة ذهاب وعودة. تنشئ اختبارات التراجع للإصدار v3.130 مستندًا، وتحفظه، وتعيد تحميله، وتجري التحسين، وتحفظه مرة أخرى، ثم تؤكد ثلاثة أشياء: المخرجات أصغر، والأعداد المرجعة تطابق التوقعات، وإعادة تحميل الملف المحسن لا يزال يحلل ويعرض بشكل سليم. وإعادة إنتاج حلقة الإنشاء والتحسين وإعادة التحميل هذه مقابل عينة من ملفات الإنتاج الخاصة بك، ومقارنة النص المستخرج قبل وبعد، هو استثمار لمدة ساعة يكتشف أخطاء التكامل قبل وقت طويل من قيام العميل بفتح فاتورة معطلة

// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
  Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
  Assert(Lib.GetPageCount > 0);
finally
  Lib.Free;
end;

أين يقع خط التدفق في سير عمل الدمج؟ بعد الدمج، وليس أثناءه. إن الدمج أولاً وتحسين النتيجة الفردية يعني أن كل خط مضمن يتم تجزئته مرة واحدة مقابل اتحاد جميع الأحرف المستخدمة، بدلاً من تجزئته لكل ملف مصدر. وإذا كان معدل نقل الدمج هو عنق الزجاجة، فإن PDFlibPas يقدم مسارًا سريعًا على مستوى البايت يتجنب تحليل الكائنات الكامل، وهو موضح في المقال الخاص بدمج PDF السريع مع نقل مرجع البايت؛ وبالنسبة للمدخلات الكبيرة جدًا بحيث لا يمكن الاحتفاظ بها بالكامل في الذاكرة، يغطي دليل الدمج والتقسيم ذو الوصول المباشر لملفات PDF الكبيرة مسار الدفق. وكلاهما يقترن بشكل طبيعي مع تمريرة تحسين نهائية على المخرجات المدمجة

ما لن تفعله التمريرات الثلاث

يستبعد ثلاثي تحسين مكتبة losLab PDF عمدًا أي شيء يغير دلالات المستند. لا تقوم SubsetEmbeddedFonts بتوحيد الخطوط المكررة عبر المصادر المدمجة في برنامج واحد، بل تقص كل منها بشكل مستقل؛ وإلغاء التكرار هو تحول مختلف وأكثر خطورة. وسوف تتجاوز DownsampleImages أي صورة يقل تقديرها المتحفظ لدقة DPI عن الحد الأدنى حتى عندما يمكن للإنسان معرفة أنها أكبر من حجم إطارها. ولا تلمس أي من التمريرات بنية المستند، لذا فإن الملف المتضخم بآلاف الكائنات اليتيمة يحتاج إلى حفظ بنمط إعادة الكتابة بدلاً من هذه التمريرات على مستوى الدفق. وضمن تلك الحدود، فإن الجمع بين تجزئة الخطوط، وتقليل دقة الصور، وتطبيع LZW إلى Flate يزيل المصادر الثلاثة الكلاسيكية لتضخم PDF باستدعاء واجهة برمجة تطبيقات واحد يمكن التنبؤ به لكل منها. وتشحن الوظائف الثلاث كجزء من مكتبة losLab PDF لدلفي، وC# وVB.NET، إلى جانب واجهات برمجة تطبيقات الدمج والاستخراج والعرض الموضحة أعلاه