Artículo técnico

Fallos de carga PDF en Delphi: el load report de PDFium

En el PDFium Component para Delphi y Lazarus, asignar TPdf.Active := True jamás lanza cuando un PDF falla al cargar: TPdf.SetActive captura toda excepción y deja el componente inactivo. Para ver el error real, llama en su lugar a TPdf.LoadDocument(Options, Report). Esa sobrecarga re-lanza la excepción original y rellena un TPdfLoadReport con el estado de carga, el código de error nativo de PDFium y si la tabla de referencias cruzadas tuvo que reconstruirse

El problema suele aflorar en código por lotes. Un trabajo de extracción de tablas recorre una carpeta de 13 PDFs reales con un único TPdf compartido, y 7 vuelven como fallos. Ninguno de los 7 archivos está realmente roto. Los bloques except alrededor de la carga nunca se disparan, el log culpa a nombres de archivo equivocados, y el primer error visible es un EPdfError desnudo sobre un componente inactivo, lanzado desde una lectura de propiedad varias líneas después de la carga que realmente falló. Dos comportamientos separados se apilan para producir ese cuadro, y ambos funcionan como fueron diseñados

¿Por qué TPdf.Active := True no lanza cuando un PDF falla al cargar?

TPdf.SetActive envuelve LoadDocument en un try..except que se traga cualquier clase de excepción y simplemente deja el componente inactivo. La absorción es deliberada: el mismo setter corre cuando un diseñador de formularios conmuta Active en el IDE, y una ruta mala no debe tumbar el IDE. En tiempo de ejecución TPdf.Active solo reporta si existe un handle de documento nativo, así que tras una carga fallida lee False y no ocurre nada más. Lo que se lanzó desapareció, fuera un EPdfError del parser, un error de stream o un EAccessViolation de un pdfium.dll a medio enlazar. Los mensajes detallados de la DLL descritos en diagnosticar fallos de carga de pdfium.dll en Delphi solo llegan a tu handler mediante una llamada que no se los trague

Dos caminos de carga en PDFium Component: asignar Active true absorbe toda excepción en el setter y aplaza el fallo a la primera llamada protegida, donde CheckActive lanza un EPdfError sobre un componente inactivo, mientras que LoadDocument con TPdfLoadOptions y un TPdfLoadReport audita la cabecera, startxref, el xref y el marcador de fin de archivo, y luego re-lanza la excepción original con la causa real adjunta
La absorción es deliberada porque el diseñador del IDE comparte el setter; el código por lotes necesita la sobrecarga que lanza, reporta y cuenta la historia real del archivo
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive se traga cualquier excepción de carga
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // nunca se ejecuta
end;
// El fallo aflora aquí en cambio, como un 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: comprueba Active justo tras la asignación;
// desde v3.122.1 LastLoadReport conserva el texto del error absorbido
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

El fallo por fin aparece en la primera llamada protegida. TPdf.PageCount, como la mayoría de propiedades de documento, empieza por CheckActive, que lanza un EPdfError que nombra al componente pero no al archivo ni a la causa. Comprobar Pdf.Active inmediatamente después de la asignación convierte un crash mal atribuido en una entrada «falló» honesta. Antes de PDFiumPas v3.122.1 la razón se perdía en ese punto; desde v3.122.1 la asignación fallida sustituye LastLoadReport por un report plsFailed que lleva el texto del error, así que la causa sobrevive. El objeto de excepción en sí y la auditoría a nivel de bytes siguen necesitando otro punto de entrada

¿Por qué reutilizar un TPdf falla desde el segundo archivo?

TPdf.FileName solo puede asignarse con el componente inactivo, así que una instancia compartida rechaza el segundo archivo antes de siquiera intentar cargarlo. TPdf.SetFileName empieza por CheckInactive, y la misma guardia protege Password y FormFill. Tras la primera carga exitosa la instancia sigue activa, la siguiente asignación lanza, y si el bucle del lote captura esa excepción y sigue, el error aterriza bajo el nombre del archivo nuevo mientras el documento viejo sigue abierto. Mezclado con las cargas fallidas absorbidas, el log deja de coincidir con la realidad. En la reproducción de 13 archivos, una instancia compartida reportó 7 fallos, mientras que un TPdf.Create(nil) fresco por documento abrió los 13. Poner Active := False entre archivos también funciona, pero una instancia por documento deja cada archivo aislado por construcción

Línea temporal de un TPdf de PDFium compartido que falla desde el segundo archivo: tras la primera carga la instancia sigue activa, la siguiente asignación de FileName lanza en CheckInactive antes de cualquier intento de carga, y el bucle del lote registra el error bajo el nombre del archivo nuevo mientras el documento viejo sigue abierto, la trampa detrás de 7 falsos fallos en un lote de 13 archivos
SetFileName se protege con CheckInactive, así que una instancia compartida rechaza el archivo dos antes de probarlo; aísla cada documento con su propio TPdf y el log vuelve a coincidir con la realidad

¿Qué te da TPdf.LoadDocument con un TPdfLoadReport?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) lanza la excepción real y además te cuenta qué pasó en forma estructurada. La sobrecarga de archivo carga FileName; las sobrecargas hermanas toman TBytes o un puntero y tamaño, y LoadCustomDocument(AStream, AOwnsStream, Options, Report) cubre streams. Cada una valida las opciones, comprueba que la instancia está inactiva, ejecuta una auditoría a nivel de bytes de la cabecera, el startxref, las secciones xref y el marcador %%EOF, y luego realiza la carga nativa. La auditoría está acotada por el mismo tipo de límites que se discuten en los presupuestos de recursos del parser para PDFs que no son de fiar: TPdfLoadOptions.Default pone AuditByteLimit a 256 MiB, MaxIssues a 256, MaxXrefSections a 1024 y MaxXrefEntries a 4.000.000. En fallo el método pone Report.Status := plsFailed y re-lanza; como Report se escribe in situ, su contenido sobrevive a la excepción, y una copia se guarda en TPdf.LastLoadReport

Los campos del report responden las preguntas que un log por lotes realmente necesita. Status es uno de plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected o plsFailed. NativeErrorCode guarda FPDF_GetLastError, así que FPDF_ERR_PASSWORD (4) separa una contraseña ausente o errónea de un archivo dañado reportado como FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid y RecoveryRoute dicen si PDFium tuvo que reconstruir la tabla xref, y Issues lista cada hallazgo de la auditoría con Code, Severity, Offset, ObjectNumber y MessageText, con IssuesTruncated activado cuando MaxIssues cortó la lista

El pipeline de LoadDocument del PDFium Component y su TPdfLoadReport: la validación de opciones y la comprobación de inactivo lanzan antes de que exista report alguno, una auditoría de bytes recorre la cabecera, startxref, las secciones xref y el marcador de fin de archivo, la carga nativa registra FPDF_GetLastError, y los desenlaces se ramifican en cargado, cargado con recuperación tras una reconstrucción del xref, rechazo estricto o fallo
Status, NativeErrorCode y la lista de issues responden lo que un log por lotes necesita; solo una sobrecarga con opciones añade la auditoría de bytes, mientras que desde v3.122.1 un Active := True fallido sigue registrando plsFailed en 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);          // una instancia por documento
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report queda relleno aunque LoadDocument haya lanzado
          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;

¿Cuándo deberías cargar con plmStrict?

Usa plmStrict siempre que un archivo reparado en silencio sea peor que uno rechazado, como la entrada de archivo, el manejo de evidencias o un pipeline de firma. PDFium reconstruye discretamente una tabla de referencias cruzadas rota (ISO 32000-1 §7.5.4) escaneando el archivo en busca de objetos, estupendo para un visor y un problema para cualquier cosa que deba procesar exactamente los bytes que recibió. Tras la carga nativa, el componente pregunta a FPDF_DocumentHasValidCrossReferenceTable. En modo plmCompatible una reconstrucción produce plsLoadedWithRecovery más un aviso plicNativeCrossReferenceRebuild. En modo plmStrict el componente descarga el documento, pone plsRejected, añade plicStrictModeRejected y lanza EPdfError con "Strict PDF load rejected the document". El modo estricto también rechaza cualquier error de auditoría, y TPdfLoadOptions.Default(plmStrict) activa RequireFinalEndOfFileMarker, que asciende un %%EOF ausente o datos tras el final (§7.5.5) de aviso a error. La auditoría del xref complementa las comprobaciones a nivel de objeto de validar streams de objeto y xref con 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, sin errores de auditoría
    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;

¿Dónde deja de decir la verdad TPdf.LastLoadReport?

TPdf.LastLoadReport está completo solo tras una sobrecarga de LoadDocument que tome opciones, porque solo esas sobrecargas ejecutan la auditoría de bytes. Un Active := True exitoso escribe un report de modo compatible sin auditoría de bytes, así que AuditAttempted se queda en False. Antes de PDFiumPas v3.122.1 uno fallido no escribía nada, lo que significaba que en una instancia compartida LastLoadReport seguía describiendo el archivo anterior, a menudo con un tranquilizador plsLoaded. Desde v3.122.1 toda carga fallida sustituye el report: un Active := True fallido, que sigue dejando el componente inactivo sin lanzar, y una llamada fallida a LoadDocument o LoadCustomDocument simple registran plsFailed con el texto del error, también sin auditoría. Otros dos huecos importan en la práctica. La validación de opciones y CheckInactive corren antes de que se inicialice el report, así que un AuditByteLimit negativo o una instancia ya activa lanzan sin producir report. Y NativeErrorCode solo significa algo cuando PDFium intentó de verdad el parseo; para un archivo ausente el wrapper lanza antes de que PDFium corra, así que registra ErrorMessage y el texto de la excepción en su lugar

La regla práctica es corta. Reserva Active := True para visores atados al diseñador donde un componente inactivo es un desenlace aceptable. En todas las demás partes, y sobre todo en código por lotes y de servidor, crea un TPdf por documento, llama a LoadDocument(Options, Report), captura la excepción que lanza y registra Report.Status, NativeErrorCode y los Issues de nivel error junto con el nombre del archivo. El coste son unas pocas líneas por punto de llamada, y cada fallo queda atribuido al archivo correcto con su causa real

La API de load report, el modo estricto y la auditoría a nivel de bytes se envían con el PDFium Component para Delphi, C++Builder y Lazarus, junto con renderizado, extracción de texto, relleno de formularios y validación PDF/A