Artículo técnico

Fallos de carga PDF en Delphi: use un load report de PDFium

En el PDFium Component for Delphi and Lazarus, asignar TPdf.Active := True jamás lanza cuando un PDF falla al cargar: TPdf.SetActive atrapa toda excepción y deja el componente inactivo. Para ver el error de verdad, llame TPdf.LoadDocument(Options, Report). Esa sobrecarga relanza la excepción original y llena 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 salir a la luz en código por lotes. Un trabajo de extracción de tablas recorre una carpeta de 13 PDFs reales con un solo TPdf compartido, y 7 de ellos vuelven como fallas. Ninguno de esos 7 archivos está roto de verdad. Los bloques except alrededor de la carga nunca se disparan, el log culpa a los nombres de archivo equivocados, y el primer error visible es un EPdfError pelado sobre un componente inactivo, lanzado desde una lectura de propiedad varias líneas después de la carga que realmente falló. Dos comportamientos distintos 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 a LoadDocument en un try..except que se traga toda clase de excepción y simplemente deja el componente inactivo. La digestió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 pasa nada más. Lo que se lanzó se perdió, fuera un EPdfError del parser, un error de stream o un EAccessViolation de una pdfium.dll medio enlazada. Los mensajes detallados de la DLL descritos en diagnosticar fallos de carga de pdfium.dll en Delphi solo llegan a su handler por una llamada que no se los trague

Dos caminos de carga en PDFium Component: asignar Active true se traga toda excepción en el setter y difiere la falla a la primera llamada protegida, donde CheckActive lanza un EPdfError sobre un componente inactivo, mientras LoadDocument con TPdfLoadOptions y un TPdfLoadReport audita el header, startxref, el xref y el marcador de fin de archivo, y después relanza la excepción original con la causa real adjunta
La digestió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;
// La falla sale 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: pruebe Active justo tras la asignación;
// desde v3.122.1 LastLoadReport guarda el texto del error tragado
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

La falla por fin aparece en la primera llamada protegida. TPdf.PageCount, como casi toda propiedad de documento, arranca con CheckActive, que lanza un EPdfError nombrando al componente pero ni al archivo ni a la causa. Probar Pdf.Active inmediatamente después de la asignación convierte un crash mal atribuido en una entrada honesta de "falló". Antes de PDFiumPas v3.122.1 la razón se perdía en ese punto; desde la v3.122.1 la asignación fallida reemplaza LastLoadReport con un reporte plsFailed que carga el texto del error, así que la causa sobrevive. El objeto de excepción en sí y la auditoría a nivel de bytes todavía requieren un punto de entrada distinto

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

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

Línea de tiempo de un TPdf de PDFium compartido que falla desde el segundo archivo en adelante: tras la primera carga la instancia se queda 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 fallas falsas en un lote de 13 archivos
SetFileName protege con CheckInactive, así que una instancia compartida rechaza el archivo dos antes de probarlo; aísle cada documento con su propio TPdf y el log vuelve a cuadrar con la realidad

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

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) lanza la excepción real y además le dice 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, chequea que la instancia esté inactiva, corre una auditoría a nivel de bytes del header, startxref, las secciones xref y el marcador %%EOF, y después ejecuta la carga nativa. La auditoría está acotada por el mismo tipo de límites discutidos en presupuestos de recursos del parser para PDFs no confiables: TPdfLoadOptions.Default fija AuditByteLimit en 256 MiB, MaxIssues en 256, MaxXrefSections en 1024 y MaxXrefEntries en 4.000.000. En fallo el método fija Report.Status := plsFailed y relanza; como Report se escribe in situ, su contenido sobrevive a la excepción, y una copia queda guardada en TPdf.LastLoadReport

Los campos del reporte responden las preguntas que un log por lotes realmente necesita. Status es uno de plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected o plsFailed. NativeErrorCode trae FPDF_GetLastError, así que FPDF_ERR_PASSWORD (4) separa una contraseña faltante o equivocada 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 auditoría con Code, Severity, Offset, ObjectNumber y MessageText, con IssuesTruncated fijado cuando MaxIssues cortó la lista

El pipeline de LoadDocument de PDFium Component y su TPdfLoadReport: la validación de opciones y el chequeo de inactivo lanzan antes de que exista reporte alguno, una auditoría de bytes recorre el header, 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 de xref, rechazo estricto o falla
Status, NativeErrorCode y la lista de issues responden lo que un log por lotes necesita; solo una sobrecarga con opciones agrega la auditoría de bytes, mientras que desde la v3.122.1 un Active := True fallido igual registra 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 se llena 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 conviene cargar con plmStrict?

Use plmStrict siempre que un archivo reparado en silencio sea peor que uno rechazado, como admisión de archivos, manejo de evidencia o un pipeline de firma. PDFium reconstruye calladamente una tabla de referencias cruzadas rota (ISO 32000-1 §7.5.4) escaneando el archivo en busca de objetos, lo que es genial para un visor y un problema para cualquier cosa que deba procesar exactamente los bytes que le dieron. Tras la carga nativa, el componente pregunta a FPDF_DocumentHasValidCrossReferenceTable. En modo plmCompatible una reconstrucción produce plsLoadedWithRecovery más un warning plicNativeCrossReferenceRebuild. En modo plmStrict el componente descarga el documento, fija plsRejected, suma plicStrictModeRejected y lanza EPdfError con "Strict PDF load rejected the document". El modo estricto además rechaza cualquier error de auditoría, y TPdfLoadOptions.Default(plmStrict) activa RequireFinalEndOfFileMarker, que promueve un %%EOF faltante o datos después del final (§7.5.5) de warning a error. La auditoría de xref complementa los chequeos a nivel de objeto de validar object y xref streams 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 TPdf.LastLoadReport de decir la verdad?

TPdf.LastLoadReport está completo solo tras una sobrecarga de LoadDocument que tome opciones, porque solo esas sobrecargas corren la auditoría de bytes. Un Active := True exitoso escribe un reporte en 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, muchas veces con un tranquilizador plsLoaded. Desde la v3.122.1 toda carga fallida reemplaza el reporte: un Active := True fallido, que sigue dejando el componente inactivo sin lanzar, y una llamada fallida a LoadDocument o LoadCustomDocument plana registran plsFailed con el texto del error, también sin auditoría. Dos huecos más importan en la práctica. La validación de opciones y CheckInactive corren antes de que el reporte se inicialice, así que un AuditByteLimit negativo o una instancia ya activa lanzan sin producir reporte. Y NativeErrorCode solo significa algo cuando PDFium de verdad intentó el parseo; para un archivo faltante el wrapper lanza antes de que PDFium corra, así que registre ErrorMessage y el texto de la excepción en su lugar

La regla práctica es corta. Reserve Active := True para visores atados al diseñador donde un componente inactivo es un desenlace aceptable. En todo lo demás, y sobre todo en código por lotes y de servidor, cree un TPdf por documento, llame LoadDocument(Options, Report), atrape la excepción que lanza y registre Report.Status, NativeErrorCode y los Issues de nivel error junto con el nombre del archivo. El costo son unas pocas líneas por sitio de llamada, y cada falla queda atribuida al archivo correcto con su causa real

El API de load report, el modo estricto y la auditoría a nivel de bytes vienen con el PDFium Component for Delphi, C++Builder and Lazarus, junto con rendering, extracción de texto, llenado de formularios y validación PDF/A