مقاله فنی

جمع‌آوری زباله PDF در Delphi: علامت‌گذاری و جاروب

حذف یک صفحه از یک PDF فونت‌ها، تصاویر یا جریان‌های محتوایش را حذف نمی‌کند. کتابخانه losLab PDF Library آن‌ها را با یک جمع‌کننده علامت‌گذاری-و-جاروب بازمی‌گیرد که گراف آبجکت را به‌جلو از ریشه‌های trailer می‌پیماید و هر آبجکت غیرمستقیمی که هیچ‌چیز به آن دسترسی ندارد را حذف می‌کند. این روی یک ذخیره کامل اجرا می‌شود، به‌طور پیش‌فرض خاموش است، و تعداد آبجکت‌هایی که حذف کرده را برمی‌گرداند

چرا حذف صفحات PDF فایل را کوچک نمی‌کند؟

چون حذف صفحه یک ویرایش ارجاعی است، نه یک عملیات ذخیره‌سازی. DeletePages(StartPage, PageCount) آبجکت‌های صفحه را از درخت صفحه جدا می‌کند و مدخل‌های outline که به آن‌ها اشاره می‌کردند را تعمیر می‌کند. کاری که نمی‌تواند انجام دهد تصمیم‌گیری این است که برنامه فونت، جریان محتوا و XObject تصویری که آن صفحات استفاده می‌کردند اکنون مرده‌اند، چون در لحظه حذف هیچ‌چیز در فایل ثبت نمی‌کند که چه کس دیگری ممکن است هنوز به آن‌ها اشاره کند. آن آبجکت‌ها در فهرست آبجکت سند باقی می‌مانند، و یک ذخیره کامل هرکدام را دوباره می‌نویسد. نتیجه شکایتی است که اکثر این رشته‌های پشتیبانی را شروع می‌کند: یک مشتری نود درصد صفحات را حذف می‌کند، ذخیره می‌کند، و فایل دو درصد کوچک می‌شود. بدتر، نشتی مرکب می‌شود. بارگذاری کنید، حذف کنید، ذخیره کنید، دوباره بارگذاری کنید، دوباره حذف کنید، دوباره ذخیره کنید، و فایل به‌طور یکنواخت رشد می‌کند در حالی که شمارش صفحه سقوط می‌کند. این مسئله‌ای متفاوت از آنی است که با زیرمجموعه‌سازی فونت و کاهش نمونه تصویر حل می‌شود، که آبجکت‌های زنده را کوچک‌تر می‌کند. اینجا آبجکت‌ها خیلی بزرگ نیستند. آن‌ها به‌سادگی دیگر بخشی از سند نیستند

مجموعه ریشه trailer است، نه درخت صفحه

گراف آبجکت PDF هیچ فیلد ارجاع-معکوس ندارد. فرمت هیچ شمارنده ارجاع و هیچ فهرست بازپیوند تعریف نمی‌کند، و کلیدهای /Parent که وجود دارند به ساختارهای خاصی مانند درخت صفحه تعلق دارند، نه به کل گراف آبجکت. هیچ‌چیز در یک آبجکت غیرمستقیم به شما نمی‌گوید چه کسی به آن اشاره می‌کند، پس پرسش «آیا کسی هنوز از آبجکت ۴۷ استفاده می‌کند» دقیقاً یک پاسخ دارد: پیمایش به‌جلو از یک ریشه شناخته‌شده و بررسی اینکه آیا می‌رسید. به همین دلیل جمع‌کننده در losLab PDF Library یک جمع‌کننده علامت‌گذاری-و-جاروب است نه یک طرح شمارنده-ارجاع

ریشه‌ها از trailer فایل می‌آیند (ISO 32000-1 §7.5.5). سه کلید آن‌ها را حمل می‌کنند: /Root، فهرست سند §7.7.2 که از آن درخت صفحه، نام‌ها، outline ها، AcroForm و فراداده همه آویزان‌اند؛ /Info، دیکشنری اطلاعات سند؛ و /Encrypt، دیکشنری رمزنگاری. دو کلید trailer باقی‌مانده دام‌اند. /ID یک آرایه از دو رشته بایتی است، و /Prev یک عدد صحیح آفست بایتی به بخش مرجع متقابل قبلی است. هیچ‌کدام یک ارجاع غیرمستقیم نیست، پس هیچ‌کدام یک ریشه مشارکت نمی‌کند. losLab PDF Library کل دیکشنری trailer را در صف می‌گذارد به‌جای سه کلید نام‌دار، که چیزی هزینه نمی‌کند و هر توسعه trailer خصوصی را زنده نگه می‌دارد

خود پیمایش تکراری است نه بازگشتی. وقتی پیمایش به یک ارجاع غیرمستقیم می‌رسد، فقط شماره آبجکت و نسل را ثبت می‌کند، جای‌گاه متناظر را علامت می‌زند و آن را روی یک صف FIFO فشار می‌دهد به‌جای بلافاصله دیرفرنس‌کردن، که درخت‌های عمیق صفحه و زنجیره‌های outline بلند را بیرون از پشته فراخوانی نگه می‌دارد و از دوبار رمزگشایی‌شدن همان آبجکت جلوگیری می‌کند. دیکشنری‌ها، آرایه‌ها و دیکشنری‌های جریان مستقیم روی یک صف دوم می‌روند که با یک مجموعه بازدیدشده نگهبانی می‌شود، چون اسناد واقعی چرخه‌های واقعی دارند: /Parent یک صفحه به گره درخت صفحه‌اش برمی‌گردد، و آیتم‌های outline از راه /Prev و /Next در هر دو جهت زنجیر می‌شوند. شماره‌های نسل بخشی از تطبیق‌اند، نه تزئین. یک ارجاع فقط وقتی حل می‌شود که شماره آبجکت و نسل هر دو توافق کنند؛ ارجاعی به شماره‌ای که در نسل متفاوتی وجود دارد به‌عنوان آبجکت null الزام‌شده توسط مشخصات رفتار می‌شود، هرگز به‌عنوان یک یال زنده

چگونه جمع‌آوری زباله را روی یک ذخیره فعال می‌کنید؟

جمع‌آوری زباله opt-in است و به رکورد گزینه‌های ذخیره تعلق دارد. به‌طور پیش‌فرض False است چون جمع‌کننده یک گذر مخرب روی گراف آبجکت است و هیچ کتابخانه‌ای نباید در سکوت آبجکت‌هایی را که یک فراخوان‌کننده هرگز نخواسته بررسی شوند حذف کند

var
  Pdf: TPDFlib;
  Opt: TPDFlibSaveOptions;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report-500pages.pdf', '') <> 1 then
      Exit;
    Pdf.DeletePages(11, 490);          // keep the first ten pages

    FillChar(Opt, SizeOf(Opt), 0);
    Opt.CompressContent := True;
    Opt.CompressFonts := True;
    Opt.OptimizeContentStreams := True;
    Opt.PackObjectStreams := True;
    Opt.GarbageCollect := True;        // drop everything the pages left behind
    Pdf.SaveToFileOptions('report-10pages.pdf', Opt);
  finally
    Pdf.Free;
  end;
end;

دو نقطه ورودی دیگر به همان جمع‌کننده می‌رسند. SetGarbageCollect(1) پرچم را روی سند انتخاب‌شده ست می‌کند پس یک SaveToFile معمولی آن را رعایت می‌کند، و GarbageCollectObjects بلافاصله گذر را اجرا می‌کند و تعداد آبجکت‌های غیرمستقیم یتیم حذف‌شده را برمی‌گرداند. فرم بلافاصله آنی است که وقتی می‌خواهید یک عدد را لاگ یا assert کنید استفاده می‌کنید، و ارزش بررسی دارد، چون یک بازگشت منفی یک شمارش نیست

var
  Removed: Integer;
begin
  Pdf.DeletePages(11, 490);
  Removed := Pdf.GarbageCollectObjects;
  if Removed < 0 then
    // The graph could not be fully decoded. Nothing was swept and the
    // document is unchanged; save it without GC or reject the input.
    LogWarning('object graph incomplete, GC skipped')
  else
    LogInfo(Format('reclaimed %d orphaned objects', [Removed]));
end;

آن مسیر شکست بیشتر از آنچه به‌نظر می‌رسد اهمیت دارد. آبجکت‌ها تنبل رمزگشایی می‌شوند، و آبجکتی که هرگز رمزگشایی نشده هیچ ارجاعی نمایش نمی‌دهد. اگر جمع‌کننده یک آبجکت رمزگشایی‌نشده را به‌عنوان یک گره خالی رفتار کند، هرچیز قابل‌دسترسی فقط از راه آن را جاروب می‌کند. پس پیمایش رمزگشایی را همان‌طور که هر آبجکت را لمس می‌کند اجبار می‌کند، و یک خطای رمزگشایی منفرد کل گذر را با یک نتیجه منفی می‌بندد و سند را بایت‌به‌بایت دست‌نخورده می‌گذارد. جاروب‌کردن گرافی که فقط بخشی از آن را می‌فهمید همان‌طوری است که یک جمع‌کننده یک فایل خراب را به یک فایل نابودشده تبدیل می‌کند

چه چیزی یک جمع‌کننده ساده‌لوح PDF را می‌شکند؟

دو جزئیات، و هر دو در سکوت شکست می‌خورند نه با صدا. اولی جریان‌های آبجکت‌اند. از PDF 1.5 یک آبجکت غیرجریانی می‌تواند فشرده درون یک ظرف /ObjStm زندگی کند (§7.5.7)، و مدخل مرجع متقابلش یک مدخل نوع ۲ است که ظرف به‌علاوه یک نمایه درونش را نام می‌برد. یک آبجکت فشرده بنابراین فقط از راه ظرفش قابل‌دسترسی است. عضو را علامت بزنید، ظرف را جاروب کنید چون هیچ‌چیز به آن به‌عنوان یک آبجکت سند ارجاع نداده، و شما یک فایل نوشته‌اید که xrefش به یک آبجکتی اشاره می‌کند که دیگر وجود ندارد. ظرف ذخیره‌سازی ساختاری است، نه داده سند، پس هرگز به‌عنوان یک یال در گراف آبجکتی که می‌پیمایید ظاهر نمی‌شود. losLab PDF Library این را با جداکردن هر عضو فشرده جان‌سالم‌مانده از ظرف منبعش پیش از رفتن ظرف‌ها مدیریت می‌کند، پس از آن ذخیره بازماندگان را به جریان‌های آبجکت تازه بازبسته‌بندی می‌کند. جزئیات دوم چیزی است که یک آبجکت جریان واقعاً به آن ارجاع می‌دهد. بایت‌ها بخشی از گراف نیستند. یک جریان محتوا که متن را با /F1 12 Tf رسم می‌کند یک فونت را با نام منبع می‌نامد، و آن نام از راه دیکشنری /Resources صفحه حل می‌شود، پس یال دسترس‌پذیری از صفحه → /Resources/Font → آبجکت فونت می‌رود، هرگز از راه بار محتوایی جریان. تنها ارجاعاتی که یک جریان مشارکت می‌دهد از دیکشنری‌اش می‌آیند، جایی که /Length، /Filter و /DecodeParms همه اجازه دارند غیرمستقیم باشند. جمع‌کننده‌ای که بایت‌های جریان را برای یافتن ارجاعات تجزیه می‌کند دارد کار پرهزینه‌ای بی‌فایده انجام می‌دهد؛ جمع‌کننده‌ای که دیکشنری‌های جریان را رد می‌کند آبجکت طول را از‌دست‌می‌دهد و فایل را فاسد می‌کند

روی شماره‌های آبجکتی که آزاد می‌کنید چه اتفاقی می‌افتد

آن‌ها مدخل‌های آزاد می‌شوند، و در همان ذخیره دوباره استفاده نمی‌شوند. جاروب فهرست آبجکت را به‌ترتیب نزولی می‌پیماید پس حذف‌ها پایدار-نمایه می‌مانند، فهرست جست‌وجو را یک‌بار در انتها بازمی‌سازد به‌جای پس از هر حذف، و برای هر آبجکت حذف‌شده شماره را در فهرست آزاد با نسلش افزایش‌یافته یک ثبت می‌کند، دقیقاً همان‌طور که §7.5.4 برای مدخلی که ممکن است بعداً دوباره استفاده شود مشخص می‌کند. نسلی که از قبل روی ۶۵۵۳۵ است همان‌جا می‌ماند، و علامت می‌زند آن شماره دائماً بازنشسته شده. شماره‌های آبجکت عمداً فشرده نمی‌شوند. پس از یک جمع‌آوری، فایل سوراخ‌ها را نگه می‌دارد: آبجکت ۱۲ ممکن است آزاد باشد در حالی که ۱۳ و ۱۴ در حال استفاده‌اند، و /Size trailer همچنان بالاترین شماره به‌علاوه یک را گزارش می‌کند نه شمارش بازمانده. آن قانونی و عادی است. شماره‌گذاری‌مجدد چند بایت در جدول مرجع متقابل صرفه‌جویی می‌کرد و می‌خواست هر ارجاع در سند بازنویسی شود، که نوع تغییری است که در سکوت هرچیزی که شماره‌های آبجکت را از بیرون نگه داشته باطل می‌کند. اندازه‌ای که پس می‌گیرید از بدنه‌های آبجکت می‌آید، نه از جدول xref

چه زمانی نباید جمع‌کننده را اجرا کنید

هرگز روی یک به‌روزرسانی افزایشی. جمع‌کننده به ذخیره‌های کامل دروازه‌بندی شده و پرچم به‌سادگی وقتی سند در حال ضمیمه‌شدن است خوانده نمی‌شود، و آن دروازه یک محدودیت برای دورزدن نیست. یک به‌روزرسانی افزایشی (§7.5.6) بایت‌های اصلی را دست‌نخورده می‌گذارد و یک بخش مرجع متقابل جدید را که از راه /Prev به قبلی زنجیر شده ضمیمه می‌کند. هر نسخه قبلی همچنان به همان آبجکت‌هایی که همیشه اشاره می‌کرد اشاره می‌کند، پس آبجکتی که در نسخه جاری غیرقابل‌دسترسی است در یک نسخه قدیمی‌تر بسیار قابل‌دسترسی است. حذفش هر نسخه جز آخری را می‌شکند، و مکانیک اینکه چرا در مقاله به‌روزرسانی‌های افزایشی و ذخیره‌های حالت-ضمیمه پوشش داده شده. همان استدلال جمع‌آوری زباله روی یک سند امضاشده را رد می‌کند، چون بازنویسی کاملی که جمع‌آوری را ممکن می‌کند خودش چیزی است که امضا را باطل می‌کند

ارزش صراحت داشتن نیز درباره اینکه جمع‌آوری چه چیزی نیست. این یک پاک‌ساز نیست. جمع‌کننده آبجکت‌هایی را حذف می‌کند که هیچ‌چیز به آن‌ها ارجاع نمی‌دهد؛ هیچ نظری درباره اینکه آیا محتوایشان حساس بوده ندارد، و آبجکتی که همچنان ارجاع‌شده هرچه بوده می‌ماند. اگر هدف غیرقابل‌بازیابی‌کردن اطلاعات است نه کوچک‌کردن فایل، گراف آبجکت لایه اشتباه است و حذف سطح-دستورالعمل و پاکسازی سند لایه درست است. آن دو خوب با هم در این ترتیب ترکیب می‌شوند: ابتدا حذف و پاکسازی کنید، سپس جمع‌آوری کنید، پس آبجکت‌هایی که حذف جدا کرد واقعاً فایل را ترک می‌کنند. همان جفت‌شدن در API پاک‌سازی منابع نیز وجود دارد، جایی که عبور گزینه جمع‌آوری-زباله باعث می‌شود پاک‌سازی یک جمع‌آوری پس از خودش اجرا کند و یتیم‌هایی که حذف کرده را در OrphanObjectsRemoved گزارش دهد

یک عادت آخر ارزش پذیرفتن دارد. مقدار برگشتی GarbageCollectObjects را در هر کار دسته‌ای که حذف صفحاتتان را انجام می‌دهد لاگ کنید، و آن را روی چند هفته اسناد واقعی تماشا کنید. یک صفر روی فایلی که تازه نصفش کرده‌اید یعنی چیزی بالادست هنوز یک ارجاعی که انتظار نداشتید نگه داشته، معمولاً یک مدخل درخت نام، یک مقصد outline یا یک فیلد AcroForm که از صفحه‌ای که رویش بود جان سالم به‌در برده. جمع‌کننده ارزان‌ترین دیباگر دسترس‌پذیری است که هرگز خواهید داشت، چون به پرسشی پاسخ می‌دهد که خود فرمت PDF از پاسخ‌دادن به آن امتناع می‌کند

جمع‌کننده زباله، رکورد گزینه‌های ذخیره و API پاک‌سازی منابع که در اینجا شرح داده شد بخشی از losLab PDF Library برای Delphi و C++Builder هستند، که صفحه محصولش مرجع کامل خط‌لوله ذخیره شامل تعامل میان جمع‌آوری، بسته‌بندی جریان آبجکت و خطی‌سازی را در بر دارد