مقاله فنی

ایندکس شیء PDF sparse و lazy در Delphi با PDFiumPas

یک دیکشنری از یک PDF دو گیگابایتی می‌خواهید و ابزار اول کل جدول ارجاع متقابل را به یک آرایه اندازه‌گیریافته با /Size trailer باز می‌کند. PDFiumPas آن مرحله را با یک ایندکس شیء sparse و lazy جایگزین می‌کند: فقط توصیف‌گرهای سکشن xref را نگه می‌دارد، یک شماره شیء تنها را در صورت نیاز از طریق پنجره‌های کراندار حل می‌کند و فقط مدخل‌هایی را که واقعاً لمس کرده‌اید کش می‌کند

شکل قدیمی این کد در FPdfCompress صادقانه اما پرهزینه بود. ApplyDefaultOpenAction فایل کامل را داخل یک TBytes می‌خواند، بعد یک آرایه متراکم TPdfActiveXrefEntries با یک اسلات به ازای هر شماره شیء تا /Size تخصیص می‌داد. دو چیز در مقیاس خراب شد. هزینه خواندن با اندازه سند خطی رشد می‌کرد حتی وقتی فراخواننده چهار دیکشنری می‌خواست، و آرایه متراکم با بودجه پارسر برخورد می‌کرد: TPdfParserResourceBudget.Default مقدار MaxObjects را روی 4,000,000 ست می‌کند، پس فایلی کاملاً معتبر که بیشترین شماره شیءش بالای آن سقف می‌نشست به استدلال حافظه رد می‌شد نه به استدلال درستی

ایندکس شیء sparse و lazy در PDFiumPas در Delphi در مقایسه با یک آرایه ارجاع متقابل متراکم: مسیر متراکم کل فایل را می‌خواند و یک اسلات به ازای هر شماره شیء تا اندازه trailer تخصیص می‌دهد، در حالی که مسیر sparse فقط توصیف‌گرهای سکشن را نگه می‌دارد
فقط توصیف‌گرها در حافظه می‌مانند، مدخل‌ها در فایل می‌مانند و هر خواندن از یک پنجره کراندار یک مبی‌بایتی می‌گذرد

چرا 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 خروجی حفظ می‌کند

چگونگی حل یک شماره شیء در Delphi توسط PDFiumPas: یک جدول ارجاع متقابل کلاسیک عرض سطر اندازه‌گیری‌شده را ضرب می‌کند، در حالی که یک استریم ارجاع متقابل تعداد زیرسکشن‌های قبلی را پیش از ضرب در عرض فیلدهای جمع‌شده از آرایه /W انباشته می‌کند
هر دو lookup حساب محض‌اند، پس هیچ‌کدام متناسب با تعداد اشیاء اعلام‌شده در trailer نیست

بازنگری‌های ترکیبی، زنجیره‌های /Prev و گاردهای دورشان

تقدم بازنگری همان‌جایی است که یک ایندکس lazy ساده‌لوحانه خراب می‌شود. PDFiumPas زنجیره را از startxref به ترتیب جدید-اول پیمایش می‌کند و یک lookup را در اولین سکشنی که جواب بدهد متوقف می‌کند، که قاعده تقدم را بدون عینیت بخشیدن به یک جدول ادغام‌شده بازتولید می‌کند. فایل‌های hybrid-reference (§7.5.8.4) داخل شاخه کلاسیک هندل می‌شوند: وقتی trailer یک /XRefStm حمل می‌کند، سکشن استریم مکمل قبل از سکشن کلاسیکی که به آن ارجاع داده ثبت می‌شود، پس اشیاء فشرده‌ای که به جدول ساده نامرئی‌اند همچنان پیدا می‌شوند در حالی که مدخل‌های کلاسیک جایگاهشان را نگه می‌دارند. بازنگری‌های قدیمی‌تر بعد از طریق /Prev دنبال می‌شوند

دو گارد آن پیمایش را کران می‌دهند و هر دو روی فایل‌های خراب اهمیت دارند. هر آفست ملاقات‌شده ثبت می‌شود، پس یک /Prev که به درون زنجیره برمی‌گردد به‌جای چرخیدن خاتمه می‌یابد، و عمق پیمایش با MaxRecursionDepth کران می‌خورد که به 1024 دیفالت می‌شود. فلگ رمزگذاری در سراسر زنجیره کامل انباشته می‌شود نه اینکه فقط از جدیدترین trailer خوانده شود، چون سندی که جدیدترین trailer‌اش /Encrypt را حذف کرده هنوز می‌تواند عقب‌تر رمز شده باشد؛ فراخواننده‌هایی که بازنگری اضافه می‌کنند روی آن فلگ تکیه می‌کنند که از نوشتن اشیاء متنی آشکار در یک فایل رمزشده امتناع کنند

چگونگی پیمایش زنجیره بازنگری PDF ترکیبی در Delphi توسط PDFiumPas: سکشن‌ها از startxref جدید اول ثبت می‌شوند، یک سکشن XRefStm مکمل جلوتر از جدول کلاسیکی که نامش را برده می‌رود، و پیمایش /Prev با آفست‌های ملاقات‌شده و یک سقف عمق کران می‌خورد
یک lookup در اولین سکشنی که جواب بدهد متوقف می‌شود، که تقدم بازنگری را بدون عینیت بخشیدن به جدول ادغام‌شده بازتولید می‌کند

مدخل‌های تایپ 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 عرضه می‌شوند