لمعرفة أين يذهب حجم ملف PDF فعليًا، تعرض مكتبة losLab PDF Library الدالة AuditDocumentSpace، التي تصنّف كل كائن غير مباشر إلى اثنتي عشرة فئة — الصور، وبرامج الخطوط، وقواميس الخطوط، ودفقات المحتوى، وكائنات النماذج XObject، ودفقات الكائنات، والملفات المضمّنة، والبيانات الوصفية، وشجرة البنية، والتعليقات التوضيحية، وشجرة الصفحات، وأخرى — وتُبلغ عن عدد الكائنات والبايتات المخزَّنة ونسبة الحصة لكل فئة
الموقف الذي وُجدت هذه الميزة من أجله مألوف. تقرير من 40 صفحة يخرج من مولّدك بحجم 80 ميجابايت، ويسأل العميل عن السبب، وكل ما يمكنك تقديمه هو تخمين. ربما الصور. ربما الخطوط. فتُفعّل تقليل عيّنات الصور، وتُسلّم الملف، فيهبط الحجم إلى 74 ميجابايت لأن الوزن الحقيقي كان في مكان آخر تمامًا. مقالنا المرافق عن تجزئة الخطوط وتقليل عيّنات الصور يغطي كيفية تصغير ملف PDF؛ أما هذا المقال فيغطي الخطوة التي يجب أن تسبق ذلك، وهي قياس ما تُوشك على تصغيره
لماذا تقيس قبل أن تضغط؟
لأن ممرات التحسين القياسية الثلاثة لها عوائد متفاوتة جدًا على أي ملف معطى، ولا شيء في الملف يخبرك أيها ينطبق حتى تعدّ. تجزئة الخطوط في مستند خطوطه بالفعل 2% من بايتاته هو وقت بعد ظهر يُقضى في تحريك خطأ تقريب. تقليل عيّنات الصور في ملف كتلته دفقات محتوى غير مضغوطة يُنتج نفس خيبة الأمل. المُحسِّن ليس الجزء الصعب — كل مكتبة تملك واحدًا. معرفة أي مُحسِّن يجب توجيهه إلى هذا الملف هي الجزء الصعب، وهذه مسألة محاسبة، لا مسألة ضغط. التدقيق يلتقط أيضًا الحالات التي لا يكون فيها أي مُحسِّن هو الحل: ملف يتبين أنه 60% مرفقات مضمّنة لا يحتاج ضغطًا أفضل، بل يحتاج نقاشًا حول ما إذا كانت تلك المرفقات تنتمي للمستند أصلًا، وملف نسبته 30% شجرة بنية يدفع ثمن وسم إمكانية الوصول، وهذه عادةً تكلفة متعمدة لا ينبغي إزالتها بصمت. بمجرد أن تُنسب البايتات، تكون تتخذ قرار منتج مبنيًا على أرقام بدلًا من التوجه إلى أقرب مفتاح
ماذا يحتوي تقرير الفئات الاثنتي عشرة؟
تُعيد AuditDocumentSpace مقبض قائمة نصية بدلًا من سجل، بحيث يبقى التقرير سليمًا عبر واجهتي DLL المسطحة وCOM دون تغيير. تحمل القائمة سطر ملخص Total,Objects,Bytes,100.0 يتبعه بالضبط اثنا عشر سطر Category,Objects,Bytes,Percent بترتيب ثابت هو جزء من العقد: الصور، وبرامج الخطوط، وقواميس الخطوط، ودفقات المحتوى، وكائنات النماذج XObject، ودفقات الكائنات، والملفات المضمّنة، والبيانات الوصفية، وشجرة البنية، والتعليقات التوضيحية، وشجرة الصفحات، وأخرى. ثلاثة عشر سطرًا، دائمًا، حتى عندما تكون فئة ما فارغة
var
Lib: TPDFlib;
ListID, I: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
ListID := Lib.AuditDocumentSpace; // 0 when no document is selected
if ListID = 0 then
Exit;
try
// GetStringListItem is 1-based: items run 1..GetStringListCount
for I := 1 to Lib.GetStringListCount(ListID) do
Memo1.Lines.Add(Lib.GetStringListItem(ListID, I));
finally
Lib.ReleaseStringList(ListID);
end;
finally
Lib.Free;
end;
end;
تفصيلة واحدة في Delphi ضمن تلك الحلقة ستُوقعك بالضبط مرة واحدة. تستخدم GetStringListItem فهارس عناصر تبدأ من واحد، بما يطابق GetStringListCount، والفهرس خارج النطاق يُعيد سلسلة فارغة بدلًا من إثارة استثناء. اكتب الحلقة for I := 0 to Count - 1 بدافع العادة وستحصل على سطر أول فارغ، وآخر سطر مُسقط بصمت، ولا استثناء في أي مكان يخبرك أن الفهرسة خاطئة. التقرير نفسه سيبدو تقريبًا صحيحًا، وهو أسوأ نمط فشل يمكن أن تحمله أداة تشخيصية
لماذا يستخدم التدقيق الطول المخزَّن بدلًا من الحجم بعد فك الترميز؟
لأن الطول المخزَّن هو في آن واحد الرقم الذي تريده والرقم الرخيص الحصول عليه. كل كائن غير مباشر يحمل TPDFIndObj.FLength، طول البايت الخام الذي يشغله الكائن في الملف كما جرى تحليله. استخدامه يعني أن صورة DCTDecode بحجم 900 كيلوبايت تُبلَّغ بأنها 900 كيلوبايت — البايتات التي تكلّفك إياها على القرص — لا 40 ميجابايت من عيّنات RGB التي تُفكّ إليها. كما يعني أن التدقيق لا يحتاج أبدًا إلى فك ترميز أي شيء: الكائنات المحمَّلة بكسل تبقى كسولة، والمرشحات تبقى غير مشغَّلة، وتدقيق ملف بحجم 500 ميجابايت هو مرور على ترويسات الكائنات وليس دورة فك ضغط كاملة
القاعدة الثانية دفاع ضد الاحتساب المزدوج. عندما يعيش كائن داخل دفق كائنات مضغوط، مُشار إليه بـ FObjStrNum غير صفري، يُسجَّل عدد بايتاته صفرًا. تخزينه قد دُفع ثمنه بالفعل مرة واحدة بواسطة دفق الحاوية، الذي تُعرّفه الفقرة §7.5.7 من ISO 32000-1 بأنه دفق /Type /ObjStm يحمل كائنات عديدة في حمولة واحدة مضغوطة بـ Flate. احتساب حصة كل عضو على حدة ثم احتساب الحاوية مرة أخرى سيُضخّم المجموع متجاوزًا حجم الملف الحقيقي. هذا له نتيجة مباشرة على كيفية قراءتك للمخرجات، مشروحة أدناه وبتعمق أكبر في مقالنا عن دفقات الكائنات ودفقات جدول المراجع المتقاطعة
لماذا لا يستطيع برنامج خط أن يصنّف نفسه؟
لأن ملف خط TrueType المضمّن في ملف PDF ليس له علامة تدل عليه. تُعرّف الفقرة §9.8.1 من ISO 32000-1 برنامج الخط المضمّن كقيمة لـ /FontFile أو /FontFile2 أو /FontFile3 في واصف خط، وقاموس الدفق في الطرف الآخر من ذلك المرجع يحمل مفاتيح /Length1 والمرشحات لكن لا /Type ولا /Subtype يُعرّفه كخط. منظورًا إليه بمعزل، هو دفق ثنائي مجهول الهوية. الواصف الذي يشير إليه وحده يعرف ماهيته. نفس عدم التناظر يظهر في التعليقات التوضيحية: تجعل الفقرة §12.5.2 من /Type /Annot اختياريًا في قاموس تعليق توضيحي، لذا فالإشارة الموثوقة هي العضوية في مصفوفة /Annots الخاصة بالصفحة، لا القاموس نفسه
لذا يُنفَّذ التصنيف مرتين. المرور الأول يقرأ /Type و/Subtype الخاصين بكل كائن ويأخذ الانتصارات السهلة: /ObjStm، و/Subtype /Image، و/Subtype /Form، و/Type /Font و/Type /FontDescriptor، و/Metadata، و/EmbeddedFile و/Filespec، و/StructTreeRoot و/StructElem، و/Annot، و/Page و/Pages. كل شيء آخر يُدرج مؤقتًا في فئة أخرى. المرور الثاني بعد ذلك يتجول في جانب المرجع ويُلغي التصنيف السابق: كل قاموس صفحة يُعيد تعيين /Contents الخاص به إلى دفقات المحتوى، ومدخلات /Annots الخاصة به إلى التعليقات التوضيحية، و/Thumb الخاص به إلى الصور، بينما يتجول كل قاموس خط عبر سلسلة واصفه الخاصة
// Shape of the second pass: the referrer names the object
Descriptor := DictOf(FontDict.FindValueByKeyName('FontDescriptor'));
if Assigned(Descriptor) then
begin
MarkRef(FontDict.FindValueByKeyName('FontDescriptor'), catFontDicts);
MarkRef(Descriptor.FindValueByKeyName('FontFile'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile2'), catFontPrograms);
MarkRef(Descriptor.FindValueByKeyName('FontFile3'), catFontPrograms);
end;
// Type0 fonts keep the descriptor one level down
Descendants := FontDict.FindValueByKeyName('DescendantFonts', True);
if (Descendants is TPDFArray) and (TPDFArray(Descendants).Count > 0) then
MarkFontProgramRefs(DictOf(TPDFArray(Descendants).Item[0]));
قراءة التقرير واختيار الخطوة التالية
اقرأ الحصص أولًا، وأعداد الكائنات ثانيًا، وعامِل أي فجوة كبيرة بينهما كإشارة. ملف PDF حديث يضع معظم قواميسه الصغيرة داخل دفقات كائنات، لذا تُظهر شجرة الصفحات وشجرة البنية بشكل روتيني عشرات الكائنات مقابل بايتات قريبة من الصفر — تكلفتها الحقيقية طُويت في سطر دفقات الكائنات. إذا كانت دفقات الكائنات نفسها كبيرة، فالملف كثيف ببنية أشبه بالبيانات الوصفية لا بالمحتوى، والرافعة هي تقليم الكائنات، لا ضغطها. دفقات مظهر التعليقات التوضيحية تتصرف بشكل مماثل: تحمل /Subtype /Form، لذا يُظهر مستند مليء بالأختام وزنه تحت كائنات النماذج XObject بينما يبقى سطر التعليقات التوضيحية صغيرًا
function CategoryShare(Lib: TPDFlib; ListID: Integer;
const Category: string): Double;
var
I: Integer;
Parts: TArray<string>;
Inv: TFormatSettings;
begin
Result := 0;
Inv := FormatSettings;
Inv.DecimalSeparator := '.'; // the report is locale-independent
for I := 2 to Lib.GetStringListCount(ListID) do // line 1 is Total
begin
Parts := string(Lib.GetStringListItem(ListID, I)).Split([',']);
if (Length(Parts) = 4) and SameText(Parts[0], Category) then
Exit(StrToFloatDef(Parts[3], 0, Inv));
end;
end;
حقيقتان تنسيقيتان مهمتان إن حللت النسب المئوية بدلًا من عرضها فقط. الفاصل العشري دائمًا نقطة حرفية بغضّ النظر عن لغة الجهاز، لذا فإن التحليل بـ FormatSettings المحيطة على محطة عمل ألمانية أو فرنسية سيفشل أو، أسوأ، يُساء تفسيره. والأصفار الزائدة تُقصّ، لذا فئة تحمل بالضبط 40% من البايتات تُطبع كـ 40، وليس 40.0 — لا تفترض أبدًا عددًا ثابتًا من المنازل العشرية. مع الحصة في اليد، يصبح التوجيه ميكانيكيًا: حصة صور مهيمنة تشير إلى DownsampleImages، وحصة برامج خطوط مهيمنة إلى SubsetEmbeddedFonts، ودفقات محتوى ضخمة إلى CompressContent
ما الذي لا يخبرك به التدقيق عمدًا؟
الإجمالي هو مجموع على الكائنات غير المباشرة، وملف PDF أكبر قليلًا من مجرد كائناته. ترويسة الملف، والتذييل، والمسافات البيضاء بين الكائنات، وجدول مراجع متقاطعة كلاسيكي ليست كائنات غير مباشرة، لذا تُنسب تلك البايتات إلى لا شيء ويهبط إجمالي التدقيق قليلًا دون الحجم الفعلي على القرص. دفق المراجع المتقاطعة مختلف — إنه كائن حقيقي بـ /Type /XRef، لذا في ملف حديث تظهر تلك البايتات فعلًا، في فئة أخرى. لا سلوك من الاثنين خلل، لكن إذا كنت توفّق التدقيق مع عدد بايتات من نظام الملفات، فهذا مصدر الفجوة
حدّان إضافيان يستحقان الذكر الصريح. أولًا، الأرقام تصف ملفًا جرى تحميله، لا ملفًا يجري تأليفه: للكائنات المبنية في الذاكرة والتي لا تملك طولًا مخزَّنًا بعد، يعود الحجم إلى الإخراج المسلسَل مع هامش اسمي لقاموس الدفق، وهذا تقدير للكتابة النهائية وليس قياسًا. دقّق بعد الحفظ وإعادة التحميل إن أردت أرقامًا دقيقة. ثانيًا، سطر "أخرى" السمين اكتشاف، لا تقرير عن خلل — عادةً ما يعني كائنات يتيمة لم يعد يشير إليها شيء، وهذه مهمة جمع القمامة بالوسم والمسح لا أي ممر ضغط
مستخدمًا بهذه الطريقة، يُغيّر التدقيق شكل المحادثة. بدلًا من التخمين حول تقرير الـ 80 ميجابايت، تفتحه، وتُشغّل استدعاء واحدًا، وتقرأ أن الصور 8%، وبرامج الخطوط 61%، وأن المستند يضمّن تسعة برامج خطوط كاملة لأسلوب مؤسسي يستخدم ثلاثة أشكال فقط. تلك إجابة قابلة للإصلاح مربوطة برقم. AuditDocumentSpace، مع ممرات التحسين التي توجّهك إليها، تُشحن في مكتبة losLab PDF Library لـ Delphi وC++Builder، حيث توثّق صفحات المرجع قائمة الفئات الكاملة وواجهة القوائم النصية البرمجية المحيطة بها