Artigo Técnico

Falha silenciosa de load em Delphi: o load report do PDFium

No PDFium Component para Delphi e Lazarus, atribuir TPdf.Active := True nunca levanta quando um PDF falha ao carregar: o TPdf.SetActive captura toda exceção e deixa o componente inativo. Para ver o erro real, chame o TPdf.LoadDocument(Options, Report) em vez. Essa sobrecarga relança a exceção original e preenche um TPdfLoadReport com o status de load, o código de erro nativo do PDFium e se a tabela de cross-reference precisou ser reconstruída

O problema costuma aparecer em código batch. Um job de extração de tabelas percorre uma pasta de 13 PDFs do mundo real com um TPdf compartilhado, e 7 deles voltam como falhas. Nenhum dos 7 arquivos está de fato quebrado. Os blocos except em volta do load nunca disparam, o log culpa os nomes de arquivo errados, e o primeiro erro visível é um EPdfError pelado sobre componente inativo, levantado de uma leitura de propriedade várias linhas depois do load que de fato falhou. Dois comportamentos separados se empilham para produzir esse quadro, e ambos estão funcionando como projetados

Por que TPdf.Active := True não levanta quando um PDF falha ao carregar?

O TPdf.SetActive embrulha o LoadDocument num try..except que engole toda classe de exceção e simplesmente deixa o componente inativo. O engolir é deliberado: o mesmo setter roda quando um designer de formulários alterna o Active na IDE, e um caminho ruim não pode derrubar a IDE. Em run time o TPdf.Active só reporta se existe um handle nativo de documento, então depois de um load falho ele lê False e nada mais acontece. O quer que tenha sido levantado se foi, fosse um EPdfError do parser, um erro de stream ou um EAccessViolation de uma pdfium.dll meio vinculada. As mensagens detalhadas de DLL descritas em diagnosticar falhas de load de pdfium.dll em Delphi só chegam ao seu handler por uma chamada que não as engole

Dois caminhos de load no PDFium Component: atribuir Active true engole toda exceção no setter e adia a falha para a primeira chamada vigiada, onde o CheckActive levanta um EPdfError sobre componente inativo, enquanto o LoadDocument com TPdfLoadOptions e um TPdfLoadReport audita o header, o startxref, o xref e o marcador de fim de arquivo, e depois relança a exceção original com a causa real anexada
O engolir é deliberado porque o designer da IDE compartilha o setter; código batch precisa da sobrecarga que levanta, reporta e conta a história real do arquivo
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive engole qualquer exceção de load
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // nunca executa
end;
// A falha aparece aqui em vez, como um EPdfError genérico:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Fix mínimo para código existente: teste 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 vigiada. O TPdf.PageCount, como a maioria das propriedades de documento, começa com o CheckActive, que levanta um EPdfError nomeando o componente mas não o arquivo e não a causa. Testar o Pdf.Active imediatamente depois da atribuição transforma um crash mal atribuído numa entrada honesta de "falhou". Antes do PDFiumPas v3.122.1 a razão se perdia nesse ponto; desde a v3.122.1 a atribuição falha substitui o LastLoadReport por um relatório plsFailed que carrega o texto do erro, então a causa sobrevive. O objeto de exceção em si e a auditoria a nível de byte ainda exigem um ponto de entrada diferente

Por que reutilizar um TPdf falha a partir do segundo arquivo?

O TPdf.FileName só pode ser atribuído enquanto o componente está inativo, então uma instância compartilhada rejeita o segundo arquivo antes mesmo de tentar carregá-lo. O TPdf.SetFileName começa com o CheckInactive, e o mesmo guard protege Password e FormFill. Depois do primeiro load bem-sucedido a instância fica ativa, a próxima atribuição levanta, e se o loop batch captura essa exceção e segue em frente, o erro cai sob o nome do arquivo novo enquanto o documento velho ainda está aberto. Misturado com as falhas de load engolidas, o log para de bater com a realidade. Na reprodução de 13 arquivos uma instância compartilhada reportou 7 falhas, enquanto um TPdf.Create(nil) novo por documento abriu todos os 13. Setar Active := False entre arquivos também funciona, mas uma instância por documento mantém cada arquivo isolado por construção

Linha do tempo de um TPdf do PDFium compartilhado falhando a partir do segundo arquivo: depois do primeiro load a instância fica ativa, a próxima atribuição de FileName levanta no CheckInactive antes de qualquer tentativa de load, e o loop batch loga o erro sob o nome do arquivo novo enquanto o documento velho ainda está aberto, a armadilha por trás de 7 falhas falsas num batch de 13 arquivos
O SetFileName protege com CheckInactive, então uma instância compartilhada rejeita o arquivo dois antes de tentá-lo; isole cada documento com o próprio TPdf e o log volta a bater com a realidade

O que o TPdf.LoadDocument com um TPdfLoadReport dá a você?

O TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) levanta a exceção real e também conta o que aconteceu em forma estruturada. A sobrecarga de arquivo carrega o FileName; sobrecargas irmãs recebem TBytes ou um ponteiro e tamanho, e o LoadCustomDocument(AStream, AOwnsStream, Options, Report) cobre streams. Cada uma valida as opções, checa que a instância está inativa, roda uma auditoria a nível de byte do header, do startxref, das seções de xref e do marcador %%EOF, e então executa o load nativo. 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 seta AuditByteLimit para 256 MiB, MaxIssues para 256, MaxXrefSections para 1024 e MaxXrefEntries para 4.000.000. Em falha o método seta Report.Status := plsFailed e relança; como o Report é escrito in place, o conteúdo dele sobrevive à exceção, e uma cópia fica guardada no TPdf.LastLoadReport

Os campos do relatório respondem às perguntas que um log batch de fato precisa. O Status é um de plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected ou plsFailed. O NativeErrorCode guarda o FPDF_GetLastError, então FPDF_ERR_PASSWORD (4) separa uma senha ausente ou errada de um arquivo danificado reportado como FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid e RecoveryRoute dizem se o PDFium precisou reconstruir a tabela de xref, e o Issues lista cada achado da auditoria com Code, Severity, Offset, ObjectNumber e MessageText, com IssuesTruncated setado quando o MaxIssues cortou a lista

O pipeline do LoadDocument do PDFium Component e o TPdfLoadReport dele: a validação de opções e a checagem de inativo levantam antes de qualquer relatório existir, uma auditoria de bytes percorre o header, o startxref, as seções de xref e o marcador de fim de arquivo, o load nativo registra o FPDF_GetLastError, e os desfechos se ramificam em carregado, carregado com recuperação após reconstrução de xref, rejeição estrita ou falha
Status, NativeErrorCode e a lista de issues respondem o que um log batch precisa; só uma sobrecarga com opções adiciona a auditoria de bytes, enquanto desde a v3.122.1 um Active := True falho 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
          // Report é preenchido mesmo que o LoadDocument tenha levantado
          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 carregar com plmStrict?

Use o plmStrict sempre que um arquivo silenciosamente reparado é pior que um rejeitado, como triagem de acervo, manuseio de evidências ou um pipeline de assinatura. O PDFium reconstrói silenciosamente uma tabela de cross-reference quebrada (ISO 32000-1 §7.5.4) varrendo o arquivo atrás de objetos, o que é ótimo para um viewer e um problema para qualquer coisa que precise processar exatamente os bytes que recebeu. Depois do load nativo, o componente pergunta ao FPDF_DocumentHasValidCrossReferenceTable. No modo plmCompatible uma reconstrução rende plsLoadedWithRecovery mais um aviso plicNativeCrossReferenceRebuild. No modo plmStrict o componente descarrega o documento, seta plsRejected, adiciona plicStrictModeRejected e levanta 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 ausente ou dados depois do final (§7.5.5) de aviso para erro. A auditoria de xref complementa as checagens a nível de objeto em validar object e xref streams 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 o TPdf.LastLoadReport para de dizer a verdade?

O TPdf.LastLoadReport só está completo depois de uma sobrecarga do LoadDocument que recebe opções, porque só essas sobrecargas rodam a auditoria de bytes. Um Active := True bem-sucedido grava um relatório em modo compatível sem auditoria de bytes, então o AuditAttempted fica False. Antes do PDFiumPas v3.122.1 um falho não gravava nada, o que significava que numa instância compartilhada o LastLoadReport ainda descrevia o arquivo anterior, muitas vezes com um tranquilizador plsLoaded. Desde a v3.122.1 todo load falho substitui o relatório: um Active := True falho, que ainda deixa o componente inativo sem levantar, e uma chamada falha de LoadDocument ou LoadCustomDocument simples registram plsFailed com o texto do erro, de novo sem auditoria. Mais dois vazios importam na prática. A validação de opções e o CheckInactive rodam antes de o relatório ser inicializado, então um AuditByteLimit negativo ou uma instância já ativa levanta sem produzir relatório. E o NativeErrorCode só significa algo quando o PDFium de fato tentou o parse; para um arquivo ausente o wrapper levanta antes de o PDFium rodar, então logue o ErrorMessage e o texto da exceção em vez

A regra prática é curta. Guarde o Active := True para viewers ligados a designer em que um componente inativo é um desfecho aceitável. Em todo o resto, e sobretudo em código batch e de servidor, crie um TPdf por documento, chame o LoadDocument(Options, Report), capture a exceção que ele levanta e logue Report.Status, NativeErrorCode e os Issues de nível erro junto com o nome do arquivo. O custo é umas linhas por call site, e toda falha é atribuída ao arquivo certo com a causa real

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