Bài viết kỹ thuật

Lỗi nạp PDF âm thầm trong Delphi: dùng PDFium Load Report

Trong PDFium Component cho Delphi và Lazarus, việc gán TPdf.Active := True không bao giờ raise khi một PDF nạp thất bại: TPdf.SetActive bắt mọi exception và để component ở trạng thái inactive. Để thấy lỗi thật, hãy gọi TPdf.LoadDocument(Options, Report) thay vào. Overload đó re-raise exception gốc và đổ đầy một TPdfLoadReport với trạng thái nạp, mã lỗi native của PDFium và việc bảng cross-reference có buộc phải dựng lại hay không

Vấn đề thường lộ diện trong code batch. Một job trích bảng đi qua một thư mục 13 PDF thực tế với một TPdf dùng chung, và 7 trong số đó quay về như thất bại. Chẳng file nào trong 7 file đó thực sự hỏng. Các khối except quanh phép nạp chẳng bao giờ bắn, log đổ tội cho các tên file sai, và lỗi nhìn thấy đầu tiên là một EPdfError trần trụi về một component inactive, raise từ một phép đọc property vài dòng sau phép nạp thực sự gãy. Hai hành vi riêng biệt chồng lên nhau tạo nên bức tranh đó, và cả hai đều vận hành đúng thiết kế

Vì sao TPdf.Active := True không raise khi một PDF nạp thất bại?

TPdf.SetActive bọc LoadDocument trong một try..except nuốt gọn mọi lớp exception và đơn giản để component inactive. Việc nuốt là có chủ ý: cùng setter đó chạy khi một form designer bật tắt Active trong IDE, và một path xấu không được làm gãy IDE. Ở run time, TPdf.Active chỉ báo có handle tài liệu native tồn tại hay không, nên sau một lần nạp thất bại nó đọc False và chẳng có gì khác xảy ra. Cái gì được raise thì mất sạch, dù đó là một EPdfError từ parser, một lỗi stream hay một EAccessViolation từ một pdfium.dll gắn dở dang. Các thông báo DLL chi tiết mô tả trong chẩn đoán lỗi nạp pdfium.dll trong Delphi chỉ đến tay handler của bạn qua một lời gọi không nuốt chúng

Hai đường nạp trong PDFium Component: gán Active true nuốt mọi exception trong setter và dời thất bại sang lần gọi có canh đầu tiên, nơi CheckActive raise một EPdfError về component inactive, trong khi LoadDocument với TPdfLoadOptions và một TPdfLoadReport audit header, startxref, xref và marker cuối file, rồi re-raise exception gốc kèm nguyên nhân thật
Việc nuốt là có chủ ý vì IDE designer dùng chung setter; code batch cần overload raise, report và kể câu chuyện thật của file
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive nuốt mọi exception nạp
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // chẳng bao giờ chạy
end;
// Thất bại lộ diện ở đây thay vào, như một EPdfError chung chung:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Bản sửa tối thiểu cho code hiện có: kiểm Active ngay sau phép gán;
// từ v3.122.1, LastLoadReport giữ văn bản của lỗi đã bị nuốt
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Thất bại cuối cùng lộ diện tại lần gọi có canh đầu tiên. TPdf.PageCount, giống hầu hết các property tài liệu, bắt đầu bằng CheckActive, thứ raise một EPdfError gọi tên component nhưng không gọi tên file và không nói nguyên nhân. Kiểm Pdf.Active ngay sau phép gán biến một cú gãy bị gán nhầm thành một entry “failed” trung thực. Trước PDFiumPas v3.122.1, nguyên nhân mất tại điểm đó; từ v3.122.1, phép gán thất bại thay LastLoadReport bằng một report plsFailed mang văn bản lỗi, nên nguyên nhân sống sót. Bản thân exception object và phép audit cấp byte vẫn đòi một điểm vào khác

Vì sao tái dùng một TPdf gãy từ file thứ hai trở đi?

TPdf.FileName chỉ có thể được gán khi component inactive, nên một instance dùng chung từ chối file thứ hai trước cả khi nó kịp thử nạp. TPdf.SetFileName bắt đầu bằng CheckInactive, và cùng phép canh đó bảo vệ Password và FormFill. Sau lần nạp thành công đầu tiên, instance giữ trạng thái active, phép gán kế tiếp raise, và nếu vòng batch bắt exception đó rồi đi tiếp, lỗi đáp xuống dưới tên file mới trong khi tài liệu cũ vẫn còn mở. Trộn với các lần nạp thất bại bị nuốt, log ngừng khớp với thực tế. Trong bản tái hiện 13 file, một instance dùng chung báo 7 thất bại, trong khi một TPdf.Create(nil) mới cho mỗi tài liệu mở được cả 13. Đặt Active := False giữa các file cũng được, nhưng một instance mỗi tài liệu giúp mọi file cách ly nhau ngay từ thiết kế

Dòng thời gian của một TPdf dùng chung gãy từ file thứ hai trở đi: sau lần nạp đầu, instance giữ trạng thái active, phép gán FileName kế tiếp raise trong CheckInactive trước mọi nỗ lực nạp, và vòng batch ghi lỗi dưới tên file mới trong khi tài liệu cũ vẫn còn mở — cái bẫy đứng sau 7 thất bại giả trong một batch 13 file
SetFileName canh bằng CheckInactive, nên một instance dùng chung từ chối file hai trước cả khi thử; hãy cách ly mỗi tài liệu bằng TPdf riêng và log lại khớp thực tế

TPdf.LoadDocument với một TPdfLoadReport cho bạn gì?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) raise exception thật và còn cho bạn biết chuyện gì xảy ra dưới dạng có cấu trúc. Overload file nạp FileName; các overload anh em nhận TBytes hay một pointer và kích thước, và LoadCustomDocument(AStream, AOwnsStream, Options, Report) phủ các stream. Mỗi cái validate các tùy chọn, kiểm instance đang inactive, chạy một phép audit cấp byte trên header, startxref, các phần xref và marker %%EOF, rồi thực hiện nạp native. Phép audit bị chặn bởi cùng kiểu giới hạn đã bàn trong ngân sách tài nguyên parser cho PDF không đáng tin: TPdfLoadOptions.Default đặt AuditByteLimit là 256 MiB, MaxIssues là 256, MaxXrefSections là 1024 và MaxXrefEntries là 4.000.000. Khi thất bại, method đặt Report.Status := plsFailed và re-raise; vì Report được ghi tại chỗ, nội dung của nó sống sót qua exception, và một bản copy được lưu vào TPdf.LastLoadReport

Các field của report trả lời đúng những câu hỏi mà một batch log cần. Status là một trong plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected hay plsFailed. NativeErrorCode giữ FPDF_GetLastError, nên FPDF_ERR_PASSWORD (4) tách một password thiếu hay sai khỏi một file hỏng được báo là FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid và RecoveryRoute cho biết PDFium có phải dựng lại bảng xref hay không, và Issues liệt kê từng phát hiện audit với Code, Severity, Offset, ObjectNumber và MessageText, kèm IssuesTruncated được đặt khi MaxIssues cắt ngắn danh sách

Pipeline LoadDocument của PDFium Component và TPdfLoadReport của nó: validation tùy chọn và phép kiểm inactive raise trước khi bất kỳ report nào tồn tại, một byte audit đi qua header, startxref, các phần xref và marker cuối file, phép nạp native ghi lại FPDF_GetLastError, và các kết quả rẽ nhánh thành loaded, loaded with recovery sau một lần dựng lại xref, strict rejection hay failure
Status, NativeErrorCode và danh sách issue trả lời thứ mà một batch log cần; chỉ một overload có options mới thêm byte audit, trong khi từ v3.122.1, một Active := True thất bại vẫn ghi plsFailed vào 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);          // một instance mỗi tài liệu
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report được đổ đầy dù LoadDocument đã raise
          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;

Khi nào nên nạp với plmStrict?

Hãy dùng plmStrict bất cứ khi nào một file được sửa lặng lẽ còn tệ hơn một file bị từ chối, chẳng hạn tiếp nhận lưu trữ, xử lý chứng cứ hay một pipeline ký. PDFium lặng lẽ dựng lại một bảng cross-reference hỏng (ISO 32000-1 §7.5.4) bằng cách quét file tìm object, điều tuyệt vời cho một viewer và là vấn đề với bất cứ thứ gì phải xử lý đúng từng byte mà nó được giao. Sau lần nạp native, component hỏi FPDF_DocumentHasValidCrossReferenceTable. Trong mode plmCompatible, một lần dựng lại cho ra plsLoadedWithRecovery cộng một cảnh báo plicNativeCrossReferenceRebuild. Trong mode plmStrict, component unload tài liệu, đặt plsRejected, thêm plicStrictModeRejected và raise EPdfError kèm thông báo “Strict PDF load rejected the document”. Strict mode còn từ chối mọi lỗi audit, và TPdfLoadOptions.Default(plmStrict) bật RequireFinalEndOfFileMarker, thứ nâng một %%EOF bị thiếu hay dữ liệu nằm sau marker cuối (§7.5.5) từ cảnh báo thành lỗi. Phép audit xref bổ trợ cho các phép kiểm cấp object trong validate object stream và xref stream với 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 hợp lệ, chẳng lỗi audit nào
    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 ngừng nói thật ở đâu?

TPdf.LastLoadReport chỉ hoàn chỉnh sau một overload LoadDocument nhận options, vì chỉ những overload đó chạy byte audit. Một Active := True thành công ghi một report compatible-mode không có byte audit, nên AuditAttempted giữ False. Trước PDFiumPas v3.122.1, một lần thất bại chẳng ghi gì, nghĩa là trên một instance dùng chung, LastLoadReport vẫn mô tả file trước đó, thường kèm một plsLoaded trấn an. Từ v3.122.1, mọi lần nạp thất bại thay report: một Active := True thất bại — vẫn để component inactive mà không raise — và một lời gọi LoadDocument hay LoadCustomDocument trơn thất bại ghi plsFailed kèm văn bản lỗi, cũng chẳng có audit. Hai khoảng hổng nữa đáng lưu ý trong thực tế. Validation tùy chọn và CheckInactive chạy trước khi report được khởi tạo, nên một AuditByteLimit âm hay một instance đã active raise mà không sinh report nào. Và NativeErrorCode chỉ có ý nghĩa khi PDFium thực sự đã thử parse; với một file mất tích, wrapper raise trước khi PDFium chạy, nên hãy log ErrorMessage và văn bản exception thay vào

Luật thực dụng ngắn gọn. Giữ Active := True cho các viewer gắn designer, nơi một component inactive là kết quả chấp nhận được. Ở mọi chỗ khác, và trên hết là trong code batch lẫn server, hãy tạo một TPdf mỗi tài liệu, gọi LoadDocument(Options, Report), bắt exception nó raise và log Report.Status, NativeErrorCode cùng các Issues cấp lỗi kèm tên file. Cái giá là vài dòng mỗi call site, và mọi thất bại đều được quy về đúng file với nguyên nhân thật của nó

API load report, strict mode và phép audit cấp byte được giao kèm với PDFium Component for Delphi, C++Builder and Lazarus, cạnh rendering, trích văn bản, điền form và validation PDF/A