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
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
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
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