مقاله فنی

ممیزی اندازه فایل PDF در Delphi: تفکیک بایت به دسته

برای فهمیدن اینکه اندازه فایل PDF واقعاً کجا صرف می‌شود، losLab PDF Library تابع AuditDocumentSpace را ارائه می‌دهد که هر شیء indirect را در دوازده دسته طبقه‌بندی می‌کند — تصاویر، برنامه‌های فونت، دیکشنری‌های فونت، content stream، form XObject، object stream، فایل‌های embed شده، metadata، درخت ساختار، annotation، درخت صفحه، سایر — و تعداد اشیاء، بایت‌های ذخیره‌شده و سهم درصدی هر کدام را گزارش می‌دهد

موقعیتی که این تابع برایش وجود دارد آشناست. یک گزارش ۴۰صفحه‌ای از تولیدکننده شما با حجم ۸۰ مگابایت بیرون می‌آید، مشتری می‌پرسد چرا، و تنها چیزی که می‌توانید ارائه دهید یک حدس است. احتمالاً تصاویر. شاید فونت‌ها. پس downsampling را فعال می‌کنید، منتشر می‌کنید، و فایل روی ۷۴ مگابایت می‌نشیند چون وزن واقعی جای کاملاً دیگری بود. مقاله همراه ما درباره subsetting فونت و downsampling تصویر نحوه کوچک کردن یک PDF را پوشش می‌دهد؛ این یکی مرحله‌ای را پوشش می‌دهد که باید اول بیاید، یعنی اندازه‌گیری چیزی که می‌خواهید کوچکش کنید

چرا پیش از فشرده‌سازی اندازه‌گیری کنیم؟

چون سه پاس بهینه‌سازی استاندارد بازدهی بسیار متفاوتی روی هر فایل مشخصی دارند، و هیچ‌چیز درباره فایل به شما نمی‌گوید کدام یک اعمال می‌شود تا زمانی که بشمارید. subsetting فونت‌ها روی سندی که فونت‌هایش از قبل ۲٪ بایت‌هایش هستند، یک بعدازظهر است که صرف حرکت دادن یک خطای گرد کردن می‌شود. downsampling تصاویر در فایلی که حجم اصلی‌اش content streamهای فشرده‌نشده است همان ناامیدی را تولید می‌کند. بهینه‌ساز بخش سخت نیست — هر کتابخانه‌ای یکی دارد. دانستن اینکه کدام بهینه‌ساز را باید به این فایل نشانه گرفت بخش سخت است، و آن یک سؤال حسابداری است، نه یک سؤال فشرده‌سازی. یک ممیزی همچنین مواردی را می‌گیرد که هیچ بهینه‌سازی جواب نیست: فایلی که مشخص می‌شود ۶۰٪ آن پیوست‌های embed شده است به فشرده‌سازی بهتر نیاز ندارد، به یک گفت‌وگو نیاز دارد درباره اینکه آیا آن پیوست‌ها اصلاً باید در سند باشند، و فایلی که ۳۰٪ آن درخت ساختار است دارد بابت برچسب‌گذاری accessibility هزینه می‌دهد، که معمولاً هزینه‌ای عمدی است و نباید خاموش حذفش کنید. وقتی بایت‌ها نسبت داده شدند، شما دارید یک تصمیم محصول با عدد پشتش می‌گیرید نه اینکه به نزدیک‌ترین سوییچ دست بزنید

گزارش دوازده‌دسته‌ای چه چیزی دارد؟

AuditDocumentSpace یک handle فهرست رشته برمی‌گرداند نه یک رکورد، بنابراین گزارش از فاساد flat DLL و COM بدون تغییر عبور می‌کند. فهرست یک خط خلاصه Total,Objects,Bytes,100.0 نگه می‌دارد که به‌دنبال آن دقیقاً دوازده خط Category,Objects,Bytes,Percent در ترتیبی ثابت که بخشی از قرارداد است می‌آید: Images، Font programs، Font dictionaries، Content streams، Form XObjects، Object streams، Embedded files، Metadata، Structure tree، Annotations، Page tree، Other. سیزده خط، همیشه، حتی وقتی یک دسته خالی است

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 از اندیس‌های 1-based استفاده می‌کند، منطبق با GetStringListCount، و یک اندیس خارج از بازه به‌جای raise کردن یک رشته خالی برمی‌گرداند. حلقه را از روی عادت for I := 0 to Count - 1 بنویسید و یک خط اول خالی، یک خط آخر که خاموش حذف شده، و هیچ exceptionی جایی که به شما بگوید اندیس‌گذاری اشتباه است می‌گیرید. خود گزارش تقریباً درست به‌نظر می‌رسد، که بدترین حالت شکست است که یک ابزار تشخیصی می‌تواند داشته باشد

چرا ممیزی از طول ذخیره‌شده به‌جای اندازه دیکد‌شده استفاده می‌کند؟

چون طول ذخیره‌شده هم عددی است که می‌خواهید و هم عددی است که به‌دست آوردنش ارزان است. هر شیء indirect TPDFIndObj.FLength را حمل می‌کند، طول بایت خام‌ای که شیء در فایل به‌همان‌صورتی که parse شده اشغال می‌کند. استفاده از آن یعنی یک تصویر DCTDecode با ۹۰۰ کیلوبایت به‌عنوان ۹۰۰ کیلوبایت گزارش می‌شود — بایت‌هایی که روی دیسک برایتان هزینه دارند — نه ۴۰ مگابایت نمونه‌های RGB که به آن دیکد می‌شود. این همچنین یعنی ممیزی هرگز مجبور به دیکد کردن هیچ‌چیز نیست: اشیاء بارگذاری‌شده تنبل تنبل می‌مانند، فیلترها اجرا‌نشده می‌مانند، و ممیزی یک فایل ۵۰۰ مگابایتی یک پاس روی هدرهای شیء است نه یک چرخه decompression کامل

قانون دوم یک دفاع در برابر دوبار‌شمردن است. وقتی یک شیء داخل یک object stream فشرده زندگی می‌کند، که با یک FObjStrNum غیرصفر نشان داده می‌شود، تعداد بایت آن صفر ثبت می‌شود. فضای ذخیره‌سازی آن قبلاً یک‌بار توسط container stream پرداخت شده، که ISO 32000-1 §7.5.7 آن را به‌عنوان یک stream /Type /ObjStm که چندین شیء را در یک payload فشرده‌شده با Flate نگه می‌دارد تعریف می‌کند. اگر به هر عضو سهم خودش را حساب کنید و سپس دوباره container را حساب کنید، مجموع را فراتر از اندازه واقعی فایل تورم می‌دهد. این یک پیامد مستقیم برای نحوه خواندن خروجی دارد، که در ادامه و با عمق بیشتر در مقاله ما درباره object stream و cross-reference stream پوشش داده شده

چرا یک برنامه فونت نمی‌تواند خودش را طبقه‌بندی کند؟

چون یک فایل فونت TrueType که در یک PDF embed شده هیچ نشانه‌ای برای گفتن این موضوع ندارد. ISO 32000-1 §9.8.1 برنامه فونت embed‌شده را به‌عنوان مقدار /FontFile، /FontFile2 یا /FontFile3 در یک font descriptor تعریف می‌کند، و دیکشنری stream در سوی دیگر آن ارجاع کلیدهای /Length1 و فیلتر را حمل می‌کند اما هیچ /Type و هیچ /Subtypeای که آن را به‌عنوان فونت شناسایی کند ندارد. به‌تنهایی نگاه شود، یک stream باینری ناشناس است. تنها descriptorی که به آن اشاره می‌کند می‌داند چیست. همان نامتقارنی برای annotationها هم پدیدار می‌شود: §12.5.2 /Type /Annot را در یک دیکشنری annotation اختیاری می‌کند، بنابراین سیگنال قابل‌اعتماد عضویت در یک آرایه /Annots صفحه است، نه خود دیکشنری

پس طبقه‌بندی دوبار اجرا می‌شود. پاس اول /Type و /Subtype خود هر شیء را می‌خواند و پیروزی‌های آسان را می‌گیرد: /ObjStm، /Subtype /Image، /Subtype /Form، /Type /Font و /Type /FontDescriptor، /Metadata، /EmbeddedFile و /Filespec، /StructTreeRoot و /StructElem، /Annot، /Page و /Pages. همه‌چیز دیگر موقتاً در Other فرود می‌آید. پاس دوم سپس سمت ارجاع‌دهنده را پیمایش می‌کند و بازنویسی می‌کند: هر دیکشنری صفحه /Contents خودش را به content stream ها، entryهای /Annots خودش را به annotationها، و /Thumb خودش را به تصاویر بازتخصیص می‌دهد، درحالی‌که هر دیکشنری فونت زنجیره descriptor خودش را پیمایش می‌کند

// 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 مدرن اغلب دیکشنری‌های کوچک خود را داخل object stream ها قرار می‌دهد، بنابراین Page tree و Structure tree معمولاً ده‌ها شیء در برابر تقریباً صفر بایت نشان می‌دهند — هزینه واقعی آن‌ها به خط Object streams تا شده. اگر Object streams خودش بزرگ باشد، فایل با ساختار شبیه metadata متراکم است نه محتوا، و اهرم هرس اشیاء است، نه فشرده کردن آن‌ها. جریان‌های appearance مربوط به annotation رفتار مشابهی دارند: آن‌ها /Subtype /Form حمل می‌کنند، بنابراین سندی که به‌شدت مهر خورده وزن خود را زیر Form XObjects نشان می‌دهد درحالی‌که خط Annotations کوچک می‌ماند

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;

دو واقعیت قالب‌بندی مهم هستند اگر درصدها را parse کنید به‌جای نمایش آن‌ها. جداکننده اعشاری همیشه صرف‌نظر از locale ماشین یک نقطه literal است، بنابراین parse کردن با FormatSettings محیطی روی یک ایستگاه کاری آلمانی یا فرانسوی شکست می‌خورد یا، بدتر، اشتباه خوانده می‌شود. و صفرهای انتهایی حذف می‌شوند، بنابراین دسته‌ای که دقیقاً ۴۰٪ بایت‌ها را دارد به‌صورت 40 چاپ می‌شود، نه 40.0 — هرگز یک رقم اعشاری ثابت فرض نکنید. با سهم در دست، مسیریابی مکانیکی است: یک سهم غالب Images به سمت DownsampleImages اشاره می‌کند، یک سهم غالب Font programs به سمت SubsetEmbeddedFonts، و Content streams حجیم به سمت CompressContent

چیزی که ممیزی عمداً به شما نمی‌گوید

مجموع جمعی روی اشیاء indirect است، و یک فایل PDF کمی بیشتر از اشیائش است. هدر فایل، trailer، فضای خالی بین اشیاء و یک جدول cross-reference کلاسیک اشیاء indirect نیستند، بنابراین آن بایت‌ها به هیچ‌چیز نسبت داده نمی‌شوند و مجموع ممیزی کمی زیر اندازه روی دیسک می‌نشیند. یک cross-reference stream متفاوت است — آن یک شیء واقعی با /Type /XRef است، بنابراین در یک فایل مدرن آن بایت‌ها واقعاً ظاهر می‌شوند، در دسته Other. هیچ‌کدام از این رفتارها یک نقص نیست، اما اگر دارید ممیزی را با تعداد بایت از سیستم فایل تطبیق می‌دهید، این‌جاست که شکاف از آن می‌آید

دو مرز دیگر ارزش گفتن صریح دارند. اول، اعداد فایلی را توصیف می‌کنند که بارگذاری شده، نه فایلی که در حال نوشتن است: برای اشیائی که در حافظه ساخته شده‌اند و هنوز طول ذخیره‌شده ندارند، اندازه به خروجی سریالایز‌شده با یک مجاز اسمی برای دیکشنری stream برمی‌گردد، که یک برآورد از نوشتن نهایی است نه یک اندازه‌گیری. اگر ارقام دقیق می‌خواهید، پس از save-and-reload ممیزی کنید. دوم، یک خط چاق Other یک یافته است، نه یک گزارش باگ — معمولاً یعنی اشیاء یتیمی که دیگر هیچ‌چیز به آن‌ها ارجاع نمی‌دهد، که کاری برای garbage collection از نوع mark-and-sweep است نه برای هیچ پاس فشرده‌سازی

وقتی به این شکل استفاده شود، ممیزی شکل گفت‌وگو را تغییر می‌دهد. به‌جای حدس زدن روی گزارش ۸۰ مگابایتی، آن را باز می‌کنید، یک فراخوانی اجرا می‌کنید، و می‌خوانید که تصاویر ۸٪ هستند، برنامه‌های فونت ۶۱٪ هستند، و سند نه برنامه فونت کامل را برای یک استایل شرکتی که سه قلم استفاده می‌کند embed کرده. آن یک پاسخ قابل‌رفع با یک عدد چسبیده به آن است. AuditDocumentSpace، همراه با پاس‌های بهینه‌سازی‌ای که به سمت آن‌ها اشاره می‌کند، در losLab PDF Library برای Delphi و C++Builder عرضه می‌شود، جایی که صفحات مرجع فهرست کامل دسته‌ها و API فهرست رشته پیرامون آن را مستند می‌کنند