مقاله فنی

شکست بی‌صدای بارگذاری PDF در دلفی و گزارش بارگذاری PDFium

در PDFium Component for Delphi and Lazarus، انتساب TPdf.Active := True وقتی بارگذاری PDF شکست می‌خورد هرگز exception نمی‌دهد: TPdf.SetActive هر exceptionی را می‌گیرد و کامپوننت را غیرفعال رها می‌کند. برای دیدن خطای واقعی، به‌جایش TPdf.LoadDocument(Options, Report) را صدا بزنید. آن overload استثنای اصلی را دوباره پرتاب می‌کند و یک TPdfLoadReport را با وضعیت بارگذاری، کد خطای بومی PDFium و این‌که جدول ارجاع متقابل مجبور به بازسازی شده یا نه پر می‌کند

مشکل معمولاً در کد دسته‌ای سر باز می‌کند. یک کار استخراج جدول پوشه‌ای از 13 PDF واقعی را با یک TPdf مشترک می‌گردد و 7 تای آن‌ها شکست برمی‌گردانند. هیچ‌کدام از آن 7 فایل واقعاً خراب نیست. بلوک‌های except دور بارگذاری هرگز آتش نمی‌گیرند، لاگ نام فایل‌های اشتباه را مقصر می‌کند و اولین خطای دیداری یک EPdfError برهنه دربارهٔ کامپوننت غیرفعال است که چند خط بعد از بارگذاریِ واقعاً شکسته، از یک خواندن پراپرتی بلند می‌شود. دو رفتار جدا روی هم انباشته می‌شوند تا این تصویر ساخته شود، و هر دو طبق طراحی کار می‌کنند

چرا TPdf.Active := True وقتی بارگذاری PDF شکست می‌خورد exception نمی‌دهد؟

TPdf.SetActive LoadDocument را درون یک try..except می‌پیچد که هر کلاس استثنا را می‌بلعد و کامپوننت را صرفاً غیرفعال می‌گذارد. بلعیدن عمدی است: همان setter وقتی طراح فرم در IDE مقدار Active را عوض می‌کند هم اجرا می‌شود و مسیر بدی نباید IDE را بترکاند. در زمان اجرا TPdf.Active فقط گزارش می‌دهد آیا handle بومی سندی وجود دارد، پس بعد از بارگذاری شکسته False می‌خواند و بیشتر از آن هیچ اتفاقی نمی‌افتد. هرچه پرتاب شده بود گم شده، چه EPdfErrorای از پارسر بوده، چه خطای استریم، چه EAccessViolationای از یک pdfium.dll نیم‌بسته. پیام‌های تفصیلی DLL که در عیب‌یابی شکست‌های بارگذاری pdfium.dll در دلفی توضیح داده شد فقط از طریق فراخوانی‌ای که آن‌ها را نمی‌بلعد به هندلر شما می‌رسند

دو مسیر بارگذاری در PDFium Component: انتساب Active برابر true هر exceptionی را در setter می‌بلعد و شکست را به اولین فراخوانی گارددار پس می‌فرستد که در آن CheckActive یک EPdfError دربارهٔ کامپوننت غیرفعال می‌دهد، در حالی که LoadDocument با TPdfLoadOptions و یک TPdfLoadReport هدر و startxref و xref و نشانگر انتهای فایل را audit می‌کند و بعد استثنای اصلی را با علت واقعی دوباره پرتاب می‌کند
بلعیدن عمدی است چون طراح IDE همان setter را شریک است؛ کد دسته‌ای به overloadی نیاز دارد که پرتاب کند، گزارش بدهد و داستان واقعی فایل را بگوید
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive هر exception بارگذاری را می‌بلعد
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // هرگز اجرا نمی‌شود
end;
// خرابی به‌جایش این‌جا سر باز می‌کند، به‌شکل یک EPdfError عمومی:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// fix حداقلی برای کد موجود: بعد از انتساب Active را آزمایش کنید؛
// از v3.122.1 به بعد LastLoadReport متن خطای بلعیده‌شده را نگه می‌دارد
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

شکست سرانجام در اولین فراخوانی گارددار خودش را نشان می‌دهد. TPdf.PageCount مثل بیشتر پراپرتی‌های سند با CheckActive شروع می‌شود که EPdfErrorای می‌دهد که نام کامپوننت را می‌برد اما نه نام فایل را و نه علت را. آزمایش Pdf.Active بلافاصله بعد از انتساب، یک کرش نادرست‌نسبت‌یافته را به یک ثبت صادقانهٔ «شکست خورد» تبدیل می‌کند. پیش از PDFiumPas v3.122.1 علت در آن نقطه گم می‌شد؛ از v3.122.1 انتساب شکسته LastLoadReport را با گزارشی plsFailed جایگزین می‌کند که متن خطا را حمل می‌کند، پس علت زنده می‌ماند. خود شیء استثنا و audit سطح بایتی هنوز نقطهٔ ورود دیگری می‌خواهند

چرا استفادهٔ دوباره از یک TPdf از فایل دوم به بعد شکست می‌خورد؟

TPdf.FileName فقط وقتی کامپوننت غیرفعال است قابل انتساب است، پس یک نمونهٔ مشترک فایل دوم را پیش از هر تلاش برای بارگذاری رد می‌کند. TPdf.SetFileName با CheckInactive شروع می‌شود و همان گارد از Password و FormFill هم محافظت می‌کند. بعد از اولین بارگذاری موفق نمونه فعال می‌ماند، انتساب بعدی exception می‌دهد، و اگر حلقهٔ دسته‌ای آن استثنا را بگیرد و ادامه بدهد، خطا زیر نام فایل جدید می‌نشیند در حالی که سند قدیمی هنوز باز است. مخلوط با شکست‌های بلعیده‌شدهٔ بارگذاری، لاگ از واقعیت فاصله می‌گیرد. در بازتولید 13 فایلی، یک نمونهٔ مشترک 7 شکست گزارش می‌کرد، در حالی که یک TPdf.Create(nil) تازه به‌ازای هر سند هر 13 را باز کرد. ست کردن Active := False بین فایل‌ها هم جواب می‌دهد، اما یک نمونه به‌ازای هر سند هر فایل را به‌صورت ساختاری ایزوله نگه می‌دارد

خط زمانی یک TPdf مشترک PDFium که از فایل دوم به بعد شکست می‌خورد: بعد از اولین بارگذاری نمونه فعال می‌ماند، انتساب FileName بعدی پیش از هر تلاش بارگذاری در CheckInactive exception می‌دهد، و حلقهٔ دسته‌ای خطا را زیر نام فایل جدید لاگ می‌کند در حالی که سند قدیمی هنوز باز است؛ همان دام پشت 7 شکست کاذب در یک بستهٔ 13 فایلی
SetFileName با CheckInactive گارد دارد، پس نمونهٔ مشترک فایل دوم را قبل از امتحان کردنش رد می‌کند؛ هر سند را با TPdf مخصوص خودش ایزوله کنید تا لاگ دوباره با واقعیت یکی شود

TPdf.LoadDocument با یک TPdfLoadReport چه چیزی به شما می‌دهد؟

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) استثنای واقعی را پرتاب می‌کند و هم‌زمان به شکل ساخت‌یافته می‌گوید چه اتفاقی افتاده. overload فایل FileName را بار می‌کند؛ overloadهای خواهر TBytes یا اشاره‌گر و اندازه می‌گیرند و LoadCustomDocument(AStream, AOwnsStream, Options, Report) استریم‌ها را پوشش می‌دهد. هر کدام گزینه‌ها را اعتبارسنجی می‌کند، بررسی می‌کند نمونه غیرفعال است، یک audit سطح بایتی روی هدر، startxref، بخش‌های xref و نشانگر %%EOF اجرا می‌کند و بعد بارگذاری بومی را انجام می‌دهد. audit با همان جنس محدودهایی کف‌وبسته است که در بودجه‌های منابع پارسر برای PDFهای غیرقابل‌اعتماد بحث شد: TPdfLoadOptions.Default مقدار AuditByteLimit را 256 MiB، MaxIssues را 256، MaxXrefSections را 1024 و MaxXrefEntries را 4,000,000 می‌گذارد. در شکست، متد Report.Status := plsFailed را ست و دوباره پرتاب می‌کند؛ چون Report در جا نوشته می‌شود محتوایش از استثنا جان سالم به در می‌برد و یک کپی در TPdf.LastLoadReport ذخیره می‌شود

فیلدهای گزارش به سؤال‌هایی جواب می‌دهند که یک لاگ دسته‌ای واقعاً لازم دارد. Status یکی از plsNotAttempted، plsLoaded، plsLoadedWithRecovery، plsRejected یا plsFailed است. NativeErrorCode مقدار FPDF_GetLastError را حمل می‌کند، پس FPDF_ERR_PASSWORD (4) نبودن یا غلط بودن رمز را از فایل آسیب‌دیده‌ای که با FPDF_ERR_FORMAT (3) گزارش می‌شود جدا می‌کند. UsedRecovery، CrossReferenceTableValid و RecoveryRoute می‌گویند PDFium مجبور شده جدول xref را بازسازی کند یا نه، و Issues هر یافتهٔ audit را با Code، Severity، Offset، ObjectNumber و MessageText فهرست می‌کند، با IssuesTruncated ست‌شده وقتی MaxIssues فهرست را کوتاه کرده باشد

pipeline مربوط به LoadDocument در PDFium Component و TPdfLoadReport آن: اعتبارسنجی گزینه‌ها و بررسی غیرفعال‌بودن پیش از وجود هر گزارش exception می‌دهند، audit بایتی هدر و startxref و بخش‌های xref و نشانگر انتهای فایل را می‌گردد، بارگذاری بومی مقدار FPDF_GetLastError را ثبت می‌کند و پیامدها به بار شدن، بار شدن با بازیابی بعد از بازسازی xref، رد شدن سخت‌گیرانه یا شکست شاخه می‌زنند
Status و NativeErrorCode و فهرست issueها همان را جواب می‌دهند که یک لاگ دسته‌ای لازم دارد؛ فقط overload دارای گزینه‌ها audit بایتی اضافه می‌کند، و از v3.122.1 یک Active := True شکسته هم plsFailed را در LastLoadReport ثبت می‌کند
uses
  SysUtils, Classes, TypInfo, FPdfView, PDFium;

procedure ProcessBatch(Files, Log: TStrings);
var
  I: Integer;
  Pdf: TPdf;
  Options: TPdfLoadOptions;
  Report: TPdfLoadReport;
begin
  Options := TPdfLoadOptions.Default(plmCompatible);
  for I := 0 to Files.Count - 1 do
  begin
    Pdf := TPdf.Create(nil);          // یک نمونه به‌ازای هر سند
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report پر شده حتی وقتی LoadDocument exception می‌دهد
          if Report.NativeErrorCode = FPDF_ERR_PASSWORD then
            Log.Add(Files[I] + ': password required')
          else
            Log.Add(Format('%s: %s (%s)', [Files[I],
              GetEnumName(TypeInfo(TPdfLoadStatus), Ord(Report.Status)),
              E.Message]));
          Continue;
        end;
      end;
      if Report.UsedRecovery then
        Log.Add(Files[I] + ': opened after PDFium rebuilt the xref table');
      ExtractTables(Pdf, Log);
    finally
      Pdf.Free;
    end;
  end;
end;

کی باید با plmStrict بارگذاری کرد؟

هر وقت فایل ترمیم‌شدهٔ بی‌سروصدا از فایل ردشده بدتر است plmStrict را به کار ببرید؛ مثل پذیرش آرشیو، مدیریت ادله یا pipeline امضا. PDFium جدول ارجاع متقابل شکسته (ISO 32000-1 §7.5.4) را بی‌سروصدا با گشتن دنبال شیءها در فایل بازسازی می‌کند، که برای یک نمایشگر عالی و برای هر چیزی که باید دقیقاً همان بایت‌های داده‌شده را پردازش کند یک مسئله است. بعد از بارگذاری بومی، کامپوننت FPDF_DocumentHasValidCrossReferenceTable را می‌پرسد. در حالت plmCompatible بازسازی، plsLoadedWithRecovery به‌علاوهٔ یک هشدار plicNativeCrossReferenceRebuild می‌دهد. در حالت plmStrict کامپوننت سند را unload می‌کند، plsRejected را ست، plicStrictModeRejected را اضافه و EPdfError با پیام «Strict PDF load rejected the document» می‌دهد. حالت سخت‌گیرانه هر خطای audit را هم رد می‌کند و TPdfLoadOptions.Default(plmStrict) مقدار RequireFinalEndOfFileMarker را روشن می‌کند، که %%EOF غایب یا داده بعد از آخرین آن (§7.5.5) را از هشدار به خطا ارتقا می‌دهد. audit مربوط به xref مکمل بررسی‌های سطح شیء در اعتبارسنجی استریم‌های شیء و xref با PDFium VCL است

function AcceptForArchive(const FileName: string; out Reason: string): Boolean;
var
  Pdf: TPdf;
  Report: TPdfLoadReport;
  I: Integer;
begin
  Result := False;
  Reason := '';
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    try
      Pdf.LoadDocument(TPdfLoadOptions.Default(plmStrict), Report);
      Result := True;               // xref معتبر، بدون خطای audit
    except
      on E: EPdfError do
      begin
        Reason := E.Message;
        for I := 0 to High(Report.Issues) do
          if Report.Issues[I].Severity = plisError then
            Reason := Reason + sLineBreak + Format('  at offset %d: %s',
              [Report.Issues[I].Offset, Report.Issues[I].MessageText]);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

TPdf.LastLoadReport کجا دیگر حقیقت را نمی‌گوید؟

TPdf.LastLoadReport فقط بعد از یک overload از LoadDocument که گزینه‌ها می‌گیرد کامل است، چون فقط همان overloadها audit بایتی را اجرا می‌کنند. یک Active := True موفق گزارش حالت سازگار بدون audit بایتی می‌نویسد، پس AuditAttempted روی False می‌ماند. پیش از PDFiumPas v3.122.1 نسخهٔ شکسته هیچ چیزی نمی‌نوشت، یعنی روی یک نمونهٔ مشترک LastLoadReport همچنان فایل قبلی را توصیف می‌کرد، اغلب با یک plsLoaded دلگرم‌کننده. از v3.122.1 به بعد هر بارگذاری شکسته گزارش را جایگزین می‌کند: یک Active := True شکسته — که همچنان بدون پرتاب کامپوننت را غیرفعال می‌گذارد — و یک فراخوانی سادهٔ شکستهٔ LoadDocument یا LoadCustomDocument مقدار plsFailed را با متن خطا ثبت می‌کنند، باز هم بدون audit. دو شکاف دیگر در عمل مهم‌اند. اعتبارسنجی گزینه‌ها و CheckInactive پیش از مقداردهی اولیهٔ گزارش اجرا می‌شوند، پس یک AuditByteLimit منفی یا نمونه‌ای که از قبل فعال است بدون تولید گزارش exception می‌دهد. و NativeErrorCode فقط وقتی معنادار است که PDFium واقعاً parse را شروع کرده باشد؛ برای فایل غایب wrapper پیش از اجرای PDFium exception می‌دهد، پس به‌جایش ErrorMessage و متن استثنا را لاگ کنید

قاعدهٔ عملی کوتاه است. Active := True را برای نمایشگرهای متصل به طراح نگه دارید که در آن‌ها کامپوننت غیرفعال نتیجهٔ قابل قبولی است. هر جای دیگر، و بالاتر از همه در کد دسته‌ای و سروری، به‌ازای هر سند یک TPdf بسازید، LoadDocument(Options, Report) را صدا بزنید، استثنایی که پرتاب می‌کند را بگیرید و Report.Status، NativeErrorCode و Issuesهای سطح خطا را همراه نام فایل لاگ کنید. هزینه چند خط در هر نقطهٔ فراخوانی است، و هر شکستی به فایل درست با علت واقعی‌اش نسبت داده می‌شود

API گزارش بارگذاری، حالت سخت‌گیرانه و audit سطح بایتی همراه PDFium Component برای دلفی، C++Builder و Lazarus عرضه می‌شوند، در کنار رندر، استخراج متن، پر کردن فرم و اعتبارسنجی PDF/A