یک دیکشنری از یک PDF دو گیگابایتی میخواهید و ابزار اول کل جدول ارجاع متقابل را به یک آرایه اندازهگیریافته با /Size trailer باز میکند. PDFiumPas آن مرحله را با یک ایندکس شیء sparse و lazy جایگزین میکند: فقط توصیفگرهای سکشن xref را نگه میدارد، یک شماره شیء تنها را در صورت نیاز از طریق پنجرههای کراندار حل میکند و فقط مدخلهایی را که واقعاً لمس کردهاید کش میکند
شکل قدیمی این کد در FPdfCompress صادقانه اما پرهزینه بود. ApplyDefaultOpenAction فایل کامل را داخل یک TBytes میخواند، بعد یک آرایه متراکم TPdfActiveXrefEntries با یک اسلات به ازای هر شماره شیء تا /Size تخصیص میداد. دو چیز در مقیاس خراب شد. هزینه خواندن با اندازه سند خطی رشد میکرد حتی وقتی فراخواننده چهار دیکشنری میخواست، و آرایه متراکم با بودجه پارسر برخورد میکرد: TPdfParserResourceBudget.Default مقدار MaxObjects را روی 4,000,000 ست میکند، پس فایلی کاملاً معتبر که بیشترین شماره شیءش بالای آن سقف مینشست به استدلال حافظه رد میشد نه به استدلال درستی
چرا API عمومی PDFium به این پرسش جواب نمیدهد؟
چون اطلاعات داخل PDFium وجود دارد اما هرگز از مرز C عبور نمیکند. CPDF_Parser جدول ارجاع متقابل و عضویت استریم شیء و تقدم بازنگری را داخلی نگه میدارد، اما هدرهای منتشرشده هیچ مدخلهایی آشکار نمیکنند که یک شماره شیء بگیرد و آفست خامش، نسلش، اینکه کدام بازنگری برده یا در کدام ObjStm زندگی میکند را برگرداند. سمت ذخیره هم به همان اندازه بسته است: FPDF_SaveAsCopy و FPDF_SaveWithVersion فقط یک کالبک نوشتن ترتیبی به شما میدهند. پس هر وصله سطح بایتی به یک کاتالوگ بعد از یک ذخیره نیتیو باید در لایه Pascal ساخته شود، به همین دلیل است که PDFiumPas این ساختارها را خودش parse میکند بهجای استفاده مجدد از DLL
ایندکس sparse واقعاً چه چیزی را در حافظه نگه میدارد؟
توصیفگرها، نه مدخلها. برای یک جدول کلاسیک (ISO 32000-1 §7.5.4) یک TPdfSparseXrefSubsection اولین شماره شیء، تعداد اشیاء، آفست بایتی که سطرهای مدخل از آنجا شروع میشوند و عرض اندازهگیریشده مدخل را ذخیره میکند. خود مدخلها در فایل میمانند. عرض از اولین سطر اندازهگیری میشود نه اینکه 20 بایت فرض شود، چون تولیدکنندهها درباره line endingها اختلاف دارند؛ PDFiumPas از 18 تا 64 را قبول میکند و هر چیزی بیرون از آن باند را رد میکند، همراه با هر زیرسکشنی که تعداد اعلامشدهاش از انتهای استریم میگذشت. برای یک استریم ارجاع متقابل (§7.5.8) سکشن سه عرض فیلد /W را نگه میدارد، هر یک محدود به 0 تا 8، جفتهای /Index تختشده، و بایتهای مدخل دیکودشده، که طول مورد انتظارشان از /W و /Index پیش از تورم حتی یک بایت محاسبه میشود
کل ایندکس توسط Initialize از یک پنجره دنباله حداکثر 1 مبیبایتی ساخته میشود، که همانجاست که startxref پیدا میشود، و هر خواندن شیء بعدی از یک پنجره شیء 1 مبیبایتی استفاده میکند. سقف استریم خام 64 مبیبایت است و یک خط xref تنها نباید از 1024 بایت بگذرد. اگر یادداشت ما درباره اعتبارسنجی استریمهای شیء و ارجاع متقابل با PDFiumPas را خوانده باشید، همان انضباط عرض فیلد اینجا هم اعمال میشود، فقط حالا برای آدرس دادن یک مدخل استفاده میشود نه ممیزی یک جدول کامل
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ فقط startxref و زنجیره /Prev و کاتالوگ را پیمایش میکند }
if ReadPdfSparseRevisionInfo(Source, Revision) then
begin
Writeln('root ', Revision.RootObjectNumber, ' ',
Revision.RootGeneration);
Writeln('max obj ', Revision.MaximumObjectNumber);
Writeln('xref str ', Revision.UsesXrefStream);
Writeln('encrypted ', Revision.HasEncrypt);
Writeln(string(Revision.CatalogDictionary));
end;
finally
Source.Free;
end;
end;
یک lookup چطور به یک شیء میرسد؟
با حساب، در هر دو چیدمان. یک زیرسکشن کلاسیک سطرهای عرض ثابت دارد، پس آدرس یک مدخل برابر شروع زیرسکشن بهعلاوه آفست شیء ضربدر عرض اندازهگیریشده است؛ PDFiumPas بعد آن یک خط را میخواند، آفست دهرقمی و نسل پنجرقمی را parse میکند، نسل را با سقف 65535 از §7.5.4 چک میکند و کلیدواژه انتهایی را بهشکل axkDirect یا axkFree دستهبندی میکند. یک استریم ارجاع متقابل یک مرحله بیشتر میخواهد چون زیرسکشنهای /Index در رشته بایت دیکودشده به هم چسبیدهاند، پس ایندکس تعداد زیرسکشنهای قبلی را پیش از ضرب در عرض جمعشده /W انباشته میکند. تایپ 1 یک آفست میدهد، تایپ 2 یک شماره استریم شیء و یک ایندکس عضو میدهد، و هر چیز دیگری axkUnknown میشود نه یک حدس
{ جدول کلاسیک، ISO 32000-1 بخش 7.5.4 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ استریم ارجاع متقابل، ISO 32000-1 بخش 7.5.8 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
هیچ چیز در هیچکدام از دو مسیر متناسب با /Size نیست. کل نقطه این بازنویسی همین است: مقدار اندازه trailer بهشکل متادیتا جلو برده میشود و هنگام نوشتن بازنگری افزایشی استفاده میشود، اما هرگز یک تخصیص را نمیراند. suite رگرسیون این را با یک fixture قفل میکند که درخت صفحهاش در شیء 1,000,000,000 و 1,000,000,001 زیر یک trailer که /Size 1000000002 اعلام میکند زندگی میکند. پیادهسازی متراکم قدیمی آن فایل را رد میکرد؛ ایندکس sparse هر دو ارجاع را حل و اندازه اعلامشده را در trailer خروجی حفظ میکند
بازنگریهای ترکیبی، زنجیرههای /Prev و گاردهای دورشان
تقدم بازنگری همانجایی است که یک ایندکس lazy سادهلوحانه خراب میشود. PDFiumPas زنجیره را از startxref به ترتیب جدید-اول پیمایش میکند و یک lookup را در اولین سکشنی که جواب بدهد متوقف میکند، که قاعده تقدم را بدون عینیت بخشیدن به یک جدول ادغامشده بازتولید میکند. فایلهای hybrid-reference (§7.5.8.4) داخل شاخه کلاسیک هندل میشوند: وقتی trailer یک /XRefStm حمل میکند، سکشن استریم مکمل قبل از سکشن کلاسیکی که به آن ارجاع داده ثبت میشود، پس اشیاء فشردهای که به جدول ساده نامرئیاند همچنان پیدا میشوند در حالی که مدخلهای کلاسیک جایگاهشان را نگه میدارند. بازنگریهای قدیمیتر بعد از طریق /Prev دنبال میشوند
دو گارد آن پیمایش را کران میدهند و هر دو روی فایلهای خراب اهمیت دارند. هر آفست ملاقاتشده ثبت میشود، پس یک /Prev که به درون زنجیره برمیگردد بهجای چرخیدن خاتمه مییابد، و عمق پیمایش با MaxRecursionDepth کران میخورد که به 1024 دیفالت میشود. فلگ رمزگذاری در سراسر زنجیره کامل انباشته میشود نه اینکه فقط از جدیدترین trailer خوانده شود، چون سندی که جدیدترین trailerاش /Encrypt را حذف کرده هنوز میتواند عقبتر رمز شده باشد؛ فراخوانندههایی که بازنگری اضافه میکنند روی آن فلگ تکیه میکنند که از نوشتن اشیاء متنی آشکار در یک فایل رمزشده امتناع کنند
مدخلهای تایپ 2: چرا استریم شیء صبر میکند
یک مدخل تایپ 2 یک استریم شیء را نام میبرد، و PDFiumPas تا وقتی فراخوانندهای عضوی از آن را نخواهد به آن استریم دست نمیزند. وقتی سرانجام دست میزند، /Type /ObjStm راستیآزمایی میشود، /N با بودجه شیء و /First با سقف بایت دیکودشده چک میشود، و /N با /First sanity-check میشود چون هر جفت هدر دستکم چهار بایت میخواهد. فقط بعد استریم توریده میشود، و اسکن هدر در عضو درخواستی و جانشینش متوقف میشود بهجای ساختن یک جدول عضو کامل. یک استریم شیء دیکودشده در هر لحظه نگه داشته میشود، که وقتی یک شاخه درخت صفحه در یک ObjStm تنها خوشه میشود معامله درستی است؛ گزارش ما درباره دیکود استریم شیء و predictor در Delphi آنچه را داخل آن مرحله تورم میگذرد (§7.5.7) پوشش میدهد
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ یک ایندکس نگهداشتهشده، خواندنهای آگاه از نسل متعدد }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { Source مال شما میماند }
end;
end;
کجا کش وعده دادن را کنار میگذارد
ایندکس یک snapshot است، و ارزش دارد دربارهاش رک گفت. سکشنها یک بار در Initialize parse میشوند؛ اگر استریم زیرین بعداً جهش یابد، هر مدخل کششده منقضی است و کلاس متوجه نمیشود. TPdfSparseDictionaryReader ایندکس را برای عمر متعلق به فراخواننده مبدأ نگه میدارد، که دقیقاً همان چیزی است که یک پیمایش بازگشتی روی درخت صفحه میخواهد و دقیقاً همان چیزی است که نباید در سراسر یک بازنویسی انجام دهید. کش مدخل یک آرایه تخت است که خطی جستجو میشود و نتایج منفی را هم ذخیره میکند، پس چند صد lookup ارزان است و چند صد هزار ارزان نیست. ReadDictionary تطابق نسل دقیق میخواهد در حالی که ReadLatestDictionary نسخه فعال را حل میکند، و این تفاوت عمدی است: حل ارجاع مورد اول را میخواهد، بازرسی کاتالوگ مورد دوم را. جایی که این کرانها قابل رعایت نیستند، واحدهای اطراف به پارسر کلفایل legacy فالبک میکنند بهجای باریک کردن مجموعه فایلهایی که هنوز کار میکنند، الگویی که ما برای streaming درخواستی PDFهای بزرگ هم استفاده میکنیم
رگرسیونهای بینکامپایلری همان رفتار را روی هر سه toolchain پوشش میدهند، شامل یک assertion که یک مبدأ 2 مبیبایتی هرگز یک خواندن بزرگتر از 1 مبیبایت نمیبیند. اگر کد Delphi یا C++Builder یا Lazarus نگه میدارید که مستقیماً ساختار PDF را لمس میکند و از پرداخت هزینه parse کلفایل برای چهار دیکشنری خسته شدهاید، ایندکس sparse و درز عمومی دورش در کامپوننت PDFium برای Delphi از PDFiumPas عرضه میشوند