Artigo Técnico

Falhas de carga PDF silenciosas em Delphi: use o load report

No PDFium Component para Delphi e Lazarus, atribuir TPdf.Active := True nunca dispara quando um PDF falha ao carregar: o TPdf.SetActive apanha todas as exceções e deixa o componente inativo. Para ver o erro real, chame antes o TPdf.LoadDocument(Options, Report). Essa sobrecarga volta a disparar a exceção original e preenche um TPdfLoadReport com o estado de carga, o código de erro nativo do PDFium e se a tabela de referências cruzadas teve de ser reconstruída

O problema costuma aparecer em código de lote. Um trabalho de extração de tabelas percorre uma pasta de 13 PDFs reais com um único TPdf partilhado, e 7 deles voltam como falhas. Nenhum dos 7 ficheiros está de facto partido. Os blocos except à volta da carga nunca disparam, o log acusa os nomes de ficheiro errados, e o primeiro erro visível é um EPdfError nu sobre um componente inativo, disparado por uma leitura de propriedade várias linhas depois da carga que realmente falhou. Dois comportamentos separados empilham-se para produzir esse quadro, e ambos estão a funcionar como desenhados

Porque é que TPdf.Active := True não dispara quando um PDF falha ao carregar?

O TPdf.SetActive embrulha o LoadDocument num try..except que engole toda a classe de exceção e simplesmente deixa o componente inativo. O engolir é deliberado: o mesmo setter corre quando um desenhador de formulários alterna o Active no IDE, e um caminho mau não pode partir o IDE. Em tempo de execução o TPdf.Active só reporta se existe um handle de documento nativo, por isso depois de uma carga falhada lê False e não acontece mais nada. O que quer que tenha sido disparado perdeu-se, fosse um EPdfError do parser, um erro de stream ou um EAccessViolation de uma pdfium.dll meio ligada. As mensagens detalhadas de DLL descritas em diagnosticar falhas de carga do pdfium.dll em Delphi só chegam ao seu handler através de uma chamada que não as engula

Dois caminhos de carga no PDFium Component: atribuir Active true engole todas as exceções no setter e adia a falha para a primeira chamada guardada, onde o CheckActive dispara um EPdfError sobre um componente inativo, enquanto o LoadDocument com TPdfLoadOptions e um TPdfLoadReport audita o cabeçalho, o startxref, o xref e a marca de fim de ficheiro, e depois volta a disparar a exceção original com a causa real apegada
O engolir é deliberado porque o desenhador do IDE partilha o setter; código de lote precisa da sobrecarga que dispara, reporta e conta a história real do ficheiro
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive engole qualquer exceção de carga
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // nunca executa
end;
// A falha aparece aqui em vez disso, como um EPdfError genérico:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Correção mínima para código existente: teste o Active logo após a atribuição;
// desde a v3.122.1 o LastLoadReport guarda o texto do erro engolido
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

A falha finalmente aparece na primeira chamada guardada. O TPdf.PageCount, como a maioria das propriedades de documento, começa pelo CheckActive, que dispara um EPdfError a nomear o componente mas não o ficheiro e não a causa. Testar o Pdf.Active imediatamente depois da atribuição transforma uma falha mal atribuída numa entrada honesta de "falhou". Antes do PDFiumPas v3.122.1 a razão perdia-se nesse ponto; desde a v3.122.1 a atribuição falhada substitui o LastLoadReport por um relatório plsFailed que transporta o texto do erro, por isso a causa sobrevive. O objeto de exceção em si e a auditoria ao nível do byte continuam a exigir um ponto de entrada diferente

Porque é que reutilizar um TPdf falha a partir do segundo ficheiro?

O TPdf.FileName só pode ser atribuído enquanto o componente está inativo, por isso uma instância partilhada rejeita o segundo ficheiro antes de sequer tentar carregá-lo. O TPdf.SetFileName começa pelo CheckInactive, e a mesma guarda protege o Password e o FormFill. Depois da primeira carga bem-sucedida a instância fica ativa, a atribuição seguinte dispara, e se o loop de lote apanhar essa exceção e seguir em frente, o erro aterra sob o nome do ficheiro novo enquanto o documento velho continua aberto. Misturado com as falhas de carga engolidas, o log deixa de bater certo com a realidade. Na reprodução de 13 ficheiros uma instância partilhada reportou 7 falhas, enquanto um TPdf.Create(nil) fresco por documento abriu os 13. Pôr Active := False entre ficheiros também funciona, mas uma instância por documento mantém cada ficheiro isolado por construção

Cronologia de um TPdf do PDFium partilhado a falhar a partir do segundo ficheiro: depois da primeira carga a instância fica ativa, a atribuição seguinte de FileName dispara no CheckInactive antes de qualquer tentativa de carga, e o loop de lote registra o erro sob o nome do ficheiro novo enquanto o documento velho continua aberto, a armadilha por trás de 7 falsas falhas num lote de 13 ficheiros
O SetFileName guarda com o CheckInactive, por isso uma instância partilhada rejeita o segundo ficheiro antes de o tentar; isole cada documento com o seu próprio TPdf e o log volta a bater certo com a realidade

O que lhe dá o TPdf.LoadDocument com um TPdfLoadReport?

O TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) dispara a exceção real e também lhe diz o que aconteceu em forma estruturada. A sobrecarga de ficheiro carrega o FileName; sobrecargas irmãs recebem TBytes ou um ponteiro e um tamanho, e o LoadCustomDocument(AStream, AOwnsStream, Options, Report) cobre streams. Cada uma valida as opções, confere que a instância está inativa, corre uma auditoria ao nível do byte do cabeçalho, do startxref, das secções xref e da marca %%EOF, e depois executa a carga nativa. A auditoria é limitada pelo mesmo tipo de limites discutidos em orçamentos de recursos do parser para PDFs não confiáveis: o TPdfLoadOptions.Default põe o AuditByteLimit em 256 MiB, o MaxIssues em 256, o MaxXrefSections em 1024 e o MaxXrefEntries em 4.000.000. Em falha o método põe Report.Status := plsFailed e volta a disparar; como o Report é escrito no lugar, os seus conteúdos sobrevivem à exceção, e uma cópia é guardada em TPdf.LastLoadReport

Os campos do relatório respondem às perguntas que um log de lote realmente precisa. O Status é um de plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected ou plsFailed. O NativeErrorCode guarda o FPDF_GetLastError, por isso o FPDF_ERR_PASSWORD (4) separa uma palavra-passe em falta ou errada de um ficheiro danificado reportado como FPDF_ERR_FORMAT (3). O UsedRecovery, o CrossReferenceTableValid e o RecoveryRoute dizem se o PDFium teve de reconstruir a tabela xref, e o Issues lista cada achado da auditoria com Code, Severity, Offset, ObjectNumber e MessageText, com o IssuesTruncated ativo quando o MaxIssues cortou a lista

O pipeline LoadDocument do PDFium Component e o seu TPdfLoadReport: a validação de opções e a verificação de inatividade disparam antes de existir relatório algum, uma auditoria de bytes percorre o cabeçalho, o startxref, as secções xref e a marca de fim de ficheiro, a carga nativa registra o FPDF_GetLastError, e os desfechos ramificam-se em carregado, carregado com recuperação após uma reconstrução do xref, rejeição estrita ou falha
Status, NativeErrorCode e a lista de achados respondem ao que um log de lote precisa; só uma sobrecarga com opções acrescenta a auditoria de bytes, enquanto desde a v3.122.1 um Active := True falhado ainda registra plsFailed no 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);          // uma instância por documento
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // o Report é preenchido mesmo que o LoadDocument tenha disparado
          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;

Quando deve carregar com plmStrict?

Use o plmStrict sempre que um ficheiro silenciosamente reparado é pior do que um rejeitado, como na receção de arquivo, no tratamento de evidências ou num pipeline de assinatura. O PDFium reconstrói em silêncio uma tabela de referências cruzada partida (ISO 32000-1 §7.5.4) varrendo o ficheiro à procura de objetos, o que é ótimo para um visualizador e um problema para qualquer coisa que tenha de processar exatamente os bytes que lhe foram dados. Depois da carga nativa, o componente pergunta ao FPDF_DocumentHasValidCrossReferenceTable. No modo plmCompatible uma reconstrução produz plsLoadedWithRecovery mais um aviso plicNativeCrossReferenceRebuild. No modo plmStrict o componente descarrega o documento, põe plsRejected, acrescenta plicStrictModeRejected e dispara EPdfError com "Strict PDF load rejected the document". O modo estrito também rejeita qualquer erro de auditoria, e o TPdfLoadOptions.Default(plmStrict) liga o RequireFinalEndOfFileMarker, que promove um %%EOF em falta ou dados depois do final (§7.5.5) de aviso para erro. A auditoria xref complementa as verificações ao nível dos objetos em validar streams de objetos e xref com 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 válido, sem erros de auditoria
    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;

Onde é que o TPdf.LastLoadReport deixa de dizer a verdade?

O TPdf.LastLoadReport só está completo depois de uma sobrecarga LoadDocument que receba opções, porque só essas sobrecargas correm a auditoria de bytes. Um Active := True bem-sucedido escreve um relatório de modo compatível sem auditoria de bytes, por isso o AuditAttempted fica False. Antes do PDFiumPas v3.122.1 um falhado não escrevia nada, o que significava que numa instância partilhada o LastLoadReport ainda descrevia o ficheiro anterior, muitas vezes com um plsLoaded tranquilizador. Desde a v3.122.1 toda carga falhada substitui o relatório: um Active := True falhado, que continua a deixar o componente inativo sem disparar, e uma chamada simples falhada a LoadDocument ou LoadCustomDocument registam plsFailed com o texto do erro, também sem auditoria. Mais dois vazios importam na prática. A validação de opções e o CheckInactive correm antes de o relatório ser inicializado, por isso um AuditByteLimit negativo ou uma instância já ativa disparam sem produzir relatório. E o NativeErrorCode só tem significado quando o PDFium efetivamente tentou a análise; para um ficheiro em falta o wrapper dispara antes de o PDFium correr, por isso registre o ErrorMessage e o texto da exceção em vez disso

A regra prática é curta. Guarde o Active := True para visualizadores ligados ao desenhador, onde um componente inativo é um desfecho aceitável. Em todo o resto, e sobretudo em código de lote e de servidor, crie um TPdf por documento, chame o LoadDocument(Options, Report), apanhe a exceção que dispara e registre o Report.Status, o NativeErrorCode e os Issues de nível erro junto com o nome do ficheiro. O custo são umas linhas por sítio de chamada, e toda a falha fica atribuída ao ficheiro certo com a sua causa real

A API de load report, o modo estrito e a auditoria ao nível do byte viajam com o PDFium Component para Delphi, C++Builder e Lazarus, ao lado da renderização, extração de texto, preenchimento de formulários e validação PDF/A