Техническа статия

Тихи провали при отваряне на PDF: PDFium load report

В PDFium Component за Delphi и Lazarus присвояването на TPdf.Active := True никога не вдига, когато PDF се провали при зареждане: TPdf.SetActive улавя всяко exception и оставя компонента неактивен. За да видите реалната грешка, викайте вместо това TPdf.LoadDocument(Options, Report). Този overload пре-вдига оригиналното exception и пълни TPdfLoadReport със статуса на зареждането, нативния error код на PDFium и дали cross-reference таблицата е трябвало да бъде преизградена

Проблемът обикновено излиза наяве в batch код. Job за извличане на таблици обхожда папка с 13 реални PDF с един споделен TPdf и 7 от тях се връщат като провали. Нито един от 7-те файла всъщност не е повреден. except блоковете около зареждането никога не се задействат, логът обвинява грешните имена на файлове, а първата видима грешка е гол EPdfError за неактивен компонент, вдигнат от четене на свойство няколко реда след зареждането, което реално се е провалило. Два отделни поведения се наслагват, за да се получи тази картина, и и двете работят както е замислено

Защо TPdf.Active := True не вдига, когато PDF се провали при зареждане?

TPdf.SetActive опакова LoadDocument в try..except, който поглъща всеки exception клас и просто оставя компонента неактивен. Поглъщането е нарочно: същият setter върви, когато form designer превключва Active в IDE-то, а лош път не бива да срина IDE-то. По време на изпълнение TPdf.Active просто докладва дали съществува нативен document handle, така че след провалено зареждане чете False и нищо друго не става. Каквото и да е било вдигнато, изчезва — да речем EPdfError от parser-а, stream грешка или EAccessViolation от полузакачен pdfium.dll. Подробните DLL съобщения, описани в диагностицирането на pdfium.dll load провали в Delphi, стигат до вашия handler само чрез извикване, което не ги поглъща

Два пътя за зареждане в PDFium Component: присвояването на Active true поглъща всяко exception в setter-а и отлага провала до първото закрилено извикване, където CheckActive вдига EPdfError за неактивен компонент, докато LoadDocument с TPdfLoadOptions и TPdfLoadReport одитира header, startxref, xref и маркера за край на файл, после пре-вдига оригиналното exception с прикачената реална причина
Поглъщането е нарочно, защото IDE designer-ът споделя setter-а; batch кодът се нуждае от overload-а, който вдига, докладва и разказва истинската история на файла
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive поглъща всяко load 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, както повечето document свойства, започва с CheckActive, който вдига EPdfError, назоваващ компонента, но не и файла и не причината. Проверката на Pdf.Active веднага след присвояването превръща погрешно приписано срутване в честен запис „провалило се“. Преди PDFiumPas v3.122.1 причината се губеше в този момент; от v3.122.1 проваленото присвояване заменя LastLoadReport с доклад plsFailed, който носи текста на грешката, така че причината оцелява. Самият exception обект и одитът на байтово ниво все още изискват друга входна точка

Защо преизползването на един TPdf се провали от втория файл нататък?

TPdf.FileName може да бъде присвояван само докато компонентът е неактивен, така че споделена инстанция отхвърля втория файл, преди изобщо да се опита да го зареди. TPdf.SetFileName започва с CheckInactive, а същата защита пази Password и FormFill. След първото успешно зареждане инстанцията остава активна, следващото присвояване вдига, и ако batch цикълът улови това exception и продължи, грешката каца под новото име на файл, докато старият документ е все още отворен. Смесено с погълнатите load провали, логът спира да съответства на реалността. В 13-файловата репродукция споделена инстанция докладва 7 провала, докато свеж TPdf.Create(nil) на документ отвори всичките 13. Задаването на Active := False между файловете също работи, но една инстанция на документ държи всеки файл изолиран по конструкция

Хронология на споделен PDFium TPdf, провалящ се от втория файл нататък: след първото зареждане инстанцията остава активна, следващото присвояване на FileName вдига в CheckInactive преди всякакъв опит за зареждане, а batch цикълът логва грешката под новото име на файл, докато старият документ е все още отворен — капанът зад 7 фалшиви провала в 13-файлова партида
SetFileName пази с CheckInactive, така че споделена инстанция отхвърля втория файл, преди да го пробва; изолирайте всеки документ със собствен TPdf и логът пак съответства на реалността

Какво ви дава TPdf.LoadDocument с TPdfLoadReport?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) вдига реалното exception и също ви казва структурирано какво се е случило. Файловият overload зарежда FileName; съседните overload-и приемат TBytes или пойнтер и размер, а LoadCustomDocument(AStream, AOwnsStream, Options, Report) покрива stream-овете. Всяко едно валидира опциите, проверява, че инстанцията е неактивна, изпълнява одит на байтово ниво на header-а, startxref, xref секциите и маркера %%EOF, после извършва нативното зареждане. Одитът е ограничен от същия вид лимити, обсъдени в ресурсните бюджети на parser-а за недоверени PDF-и: TPdfLoadOptions.Default задава AuditByteLimit на 256 MiB, MaxIssues на 256, MaxXrefSections на 1024 и MaxXrefEntries на 4 000 000. При провал методът задава Report.Status := plsFailed и пре-вдига; понеже Report се пише на място, съдържанието му оцелява през exception-а, а копие се съхранява в TPdf.LastLoadReport

Полетата на доклада отговарят на въпросите, от които един batch лог действително се нуждае. Status е едно от plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected или plsFailed. NativeErrorCode държи FPDF_GetLastError, така че FPDF_ERR_PASSWORD (4) отделя липсваща или грешна парола от повреден файл, докладван като FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid и RecoveryRoute казват дали PDFium е трябвало да преизгради xref таблицата, а Issues изброява всеки одитен наход с Code, Severity, Offset, ObjectNumber и MessageText, като IssuesTruncated е вдигнат, когато MaxIssues е отрязал списъка

Pipeline-ът LoadDocument на PDFium Component и неговият TPdfLoadReport: валидацията на опциите и проверката за неактивност вдигат, преди да съществува някакъв доклад, байтов одит минава през header, startxref, xref секции и маркер за край на файл, нативното зареждане записва FPDF_GetLastError, а изходите се разклоняват в заредено, заредено с recovery след преизграждане на xref, строго отхвърляне или провал
Status, NativeErrorCode и списъкът с находки отговарят на нуждите на един batch лог; само options overload добавя байтовия одит, докато от 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 да е вдигнала
          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, когато тихо поправен файл е по-лош от отхвърлен — например archive intake, обработка на доказателства или signing pipeline. PDFium тихо възстановява счупена cross-reference таблица (ISO 32000-1 §7.5.4), сканирайки файла за обекти, което е чудесно за viewer и проблем за каквото и да е, което трябва да обработи точно подадените му байтове. След нативното зареждане компонентът пита FPDF_DocumentHasValidCrossReferenceTable. В режим plmCompatible преизграждане дава plsLoadedWithRecovery плюс предупреждение plicNativeCrossReferenceRebuild. В режим plmStrict компонентът разтоварва документа, задава plsRejected, добавя plicStrictModeRejected и вдига EPdfError с „Strict PDF load rejected the document“. Strict режимът отхвърля и всяка одитна грешка, а TPdfLoadOptions.Default(plmStrict) включва RequireFinalEndOfFileMarker, който повишава липсващ %%EOF или данни след последния (§7.5.5) от предупреждение до грешка. xref одитът допълва проверките на ниво обект в валидацията на object и xref stream-ове с 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, без одитни грешки
    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 е пълен само след LoadDocument overload, приемащ опции, защото само тези overload-и пускат байтовия одит. Успешно Active := True записва доклад в compatible режим без байтов одит, така че AuditAttempted остава False. Преди PDFiumPas v3.122.1 проваленото не записваше нищо, което означаваше, че на споделена инстанция LastLoadReport все още описва предишния файл, често с успокояващ plsLoaded. От v3.122.1 всяко провалено зареждане заменя доклада: провалено Active := True, което все пак оставя компонента неактивен без да вдига, и провалено обикновено извикване на LoadDocument или LoadCustomDocument записват plsFailed с текста на грешката — отново без одит. Още две дупки имат значение на практика. Валидацията на опциите и CheckInactive вървят, преди докладът да е инициализиран, така че отрицателен AuditByteLimit или вече активна инстанция вдигат, без да произведат доклад. А NativeErrorCode има значение само когато PDFium действително се е опитал да парсне; за липсващ файл обвързката вдига, преди PDFium да тръгне, така че логвайте ErrorMessage и текста на exception-а вместо това

Практичното правило е кратко. Дръжте Active := True за designer-вързани viewer-и, където неактивен компонент е приемлив изход. Навсякъде другаде и преди всичко в batch и server код създавайте по един TPdf на документ, викайте LoadDocument(Options, Report), улавяйте exception-а, който вдига, и логвайте Report.Status, NativeErrorCode и Issues на ниво грешка заедно с името на файла. Цената е няколко реда на извикващо място, а всеки провал се приписва на правилния файл с истинската му причина

API-ят за load report, strict режимът и одитът на байтово ниво пътуват с PDFium Component за Delphi, C++Builder и Lazarus, заедно с рендерирането, извличането на текст, попълването на форми и PDF/A валидацията