Artículo técnico

Linearizar PDF para Fast Web View con PDFium en Delphi

PDFium Component produce PDF Fast Web View (linearizados) desde Delphi mediante TPdf.SaveAsLinearized. El método guarda el documento actual con PDFium y luego reescribe el resultado conforme al Anexo F de ISO 32000-1: los objetos de la primera página pasan al frente, se escriben dos secciones de cross-reference y las hint tables de offsets de página y de objetos compartidos le dicen a un visor qué rangos de bytes pedir para cualquier página

La razón por la que a alguien le importa es la red. Un catálogo de 40 MB servido desde object storage o un portal documental debería mostrar la página uno tras los primeros kilobytes, y saltar a la página 380 debería costar un par de HTTP Range requests, no una descarga completa. Los visores solo pueden hacer eso cuando el archivo está linealizado. La mayoría de los PDF que salen de un pipeline de edición no lo están, porque la forma natural de escribir un PDF pone la tabla de cross-reference al final y siembra los objetos de página donde al escritor le pillara emitirlos

¿Por qué PDFium lee archivos linealizados pero no los escribe?

PDFium tiene un lector completo de linealización y ningún escritor para ella. La API de disponibilidad FPDFAvail_* detecta el diccionario de linealización, decodifica hint tables y le pide al anfitrión exactamente los rangos de bytes que necesita, que es sobre lo que está construido el flujo de descarga progresiva y cancelación en PDFium Component. El ABI público de guardado, por su parte, solo ofrece un guardado de copia simple y un guardado con versión de PDF objetivo. Nada en él reordena objetos ni emite hint streams

PDFium Component cierra ese hueco sin re-implementar lo que PDFium ya hace bien. TPdf.SaveAsLinearized llama primero al SaveAs existente, así que PDFium sigue siendo dueño del guardado semántico: páginas editadas, estado de campos de formulario y manejo de versión pasan todos por la ruta de código que llevan años ejerciéndose. El componente ejecuta entonces una pasada de reestructuración del Anexo F sobre ese PDF intermedio estable y escribe el resultado linealizado en tu destino. Partir el trabajo así significa que la linealización jamás tiene que entender una anotación o un formulario, solo objetos, referencias y offsets de bytes

Llamar a TPdf.SaveAsLinearized desde Delphi

TPdf.SaveAsLinearized tiene dos sobrecargas, una para un TStream y otra para un nombre de archivo, y ambas toman los mismos parámetros opcionales Option: TSaveOption y PdfVersion: TPdfVersion que SaveAs. Ambas devuelven True en caso de éxito. La sobrecarga de archivo escribe a un archivo temporal en el directorio de destino y sustituye el destino de forma atómica, así que un guardado fallido nunca deja un PDF a medio escribir donde antes había uno bueno

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.Active := True;
    // ... edita páginas, rellena formularios, estampa, lo que haga el pipeline ...
    if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

Las reglas de parámetros son estrictas a propósito. saIncremental lanza un error, porque una actualización incremental añade una revisión nueva al final del archivo y destrozaría de inmediato la maquetación con todo al frente. Una versión objetivo por debajo de PDF 1.2 también se rechaza; pvUnknown conserva la versión del origen. La entrada cifrada necesita saRemoveSecurity, ya que el escritor renumera objetos y las claves de cifrado a nivel de objeto de PDF se derivan del número de objeto. Y como la linealización es una reescritura completa, toda firma digital existente queda invalidada. Si tienes que entregar firmado y linealizado, linealiza primero y firma después

Cómo agrupa objetos el escritor para Fast Web View

La linealización no es solo mover la página uno al principio del archivo. El escritor parsea la revisión actual, sea su información de cross-reference una tabla clásica, un archivo híbrido o un cross-reference stream, y expande cada miembro de un object stream activo en un objeto independiente. Luego construye un grafo de referencias y ordena los objetos en cinco grupos: objetos de nivel documento necesarios para abrir el archivo, objetos de la primera página, objetos privados de una página posterior, objetos compartidos por varias páginas posteriores y todo lo demás

La trampa en ese recorrido del grafo es /Parent. Cada diccionario de página apunta hacia arriba en el árbol de páginas, y un recorrido ingenuo sigue ese puntero hasta el nodo padre, luego baja por /Kids hasta cada página hermana y sus content streams. Un paso después, el grupo de la primera página contiene el documento entero, y el archivo está técnicamente linealizado pero es inútil para acceso de rango primero. PDFium Component ignora el enlace /Parent de la página y los nodos del árbol de páginas fuera de la página que está recolectando, que es lo que mantiene pequeña la sección de primera página. En una muestra de regresión de 51 páginas con un árbol de páginas profundo, la sección de primera página son 5289 bytes, un 0,48% de la salida de 1105354 bytes

Los object streams y cross-reference streams de la entrada se aplanan en objetos indirectos ordinarios de generación 0, cada referencia se reescribe a los números nuevos, y la salida usa secciones de cross-reference clásicas. Eso cuesta algo de tamaño de archivo comparado con object streams comprimidos, pero simplifica la cobertura de las hint tables y es la maquetación que todo visor consciente de la linealización maneja. Si la compacidad importa más que Fast Web View para una salida concreta, PDFium Component tiene un escritor de object streams aparte para ese trabajo

¿Qué entra en el diccionario de linealización y las dos secciones xref?

Un archivo linealizado escrito por PDFium Component lleva dos secciones de cross-reference clásicas y un diccionario de linealización en el primerísimo objeto. La primera sección xref sigue al diccionario de linealización y cubre los objetos de nivel documento y de primera página, así que un visor puede renderizar la página uno solo desde el frente del archivo. La sección xref principal se sienta al final y cubre todo lo demás

El propio diccionario de linealización guarda los números que un visor necesita antes de leer cualquier otra cosa: /L la longitud del archivo, /H el offset y la longitud del hint stream, /O el número de objeto de la primera página, /E el final de la sección de primera página, /N el recuento de páginas, y /T el offset de la primera entrada de la tabla xref principal. Todo eso depende de la maquetación final, y el diccionario se sienta antes de la maquetación. Escribirlo con números de anchura natural crea un bucle: una longitud que crece de 999999 a 1000000 añade un byte, desplaza cada offset posterior, lo que puede cambiar la anchura de otro número, y así

PDFium Component rompe el bucle reservando 320 bytes fijos para el diccionario de linealización. Cada objeto, el hint stream, ambas secciones xref y los trailers se escriben primero; cuando todos los offsets son finales, el diccionario se rellena en su sitio y se acolcha hasta la anchura reservada. Un detalle sutil es /T: debe apuntar a la entrada del objeto 0 de la tabla xref principal, no a la palabra clave xref que la precede. Los validadores estrictos marcan el offset de la palabra clave aunque los visores indulgentes abran el archivo igualmente

¿Por qué los offsets de las hint tables ignoran el hint stream?

Los offsets de las hint tables en un PDF linealizado se interpretan como si el hint stream no existiera en el archivo. El Anexo F lo define así para que el hint stream pueda sentarse entre la sección de primera página y el resto sin que su propio tamaño realimente los números que contiene. La consecuencia práctica para un escritor es fácil de torcer: todo offset real que quede después del hint stream tiene que reducirse antes de entrar en la hint table de offsets de página o de objetos compartidos

La pregunta es reducido por cuánto. La respuesta tentadora es la longitud de los datos del stream, porque esa es la parte que huele a contenido de hint. La respuesta correcta es la longitud del objeto indirecto del hint entero, incluidos N 0 obj, el diccionario del stream, las palabras clave stream y endstream, y endobj. Resta solo la longitud de los datos y todo offset posterior queda desviado unas pocas decenas de bytes. La página uno se sigue renderizando porque vive antes del hint stream, así que el bug sobrevive a una comprobación visual rápida y solo aparece cuando un visor salta a una página posterior y pide un rango que empieza en mitad del objeto equivocado

Ambas hint tables van empaquetadas por bits con el bit más significativo primero, otra vez según el Anexo F. El componente verifica el resultado como lo haría un cliente real: un almacén de rangos disperso solo suministra los primeros 1 KiB de la muestra de 51 páginas, y el manejo de hints nativo de PDFium pide el resto. La última página abre sin que el archivo completo esté presente nunca

Verificar un PDF linealizado en Delphi

La comprobación más barata es preguntarle al propio PDFium. TPdf.InspectAvailability corre la API de disponibilidad contra un stream sin cargar el documento, y su campo Linearization reporta plnLinearized, plnNotLinearized o plnUnknown

function IsFastWebView(const FileName: string): Boolean;
var
  Stream: TFileStream;
  Info: TPdfAvailability;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Info := TPdf.InspectAvailability(Stream);
    Result := Info.Linearization = plnLinearized;
  finally
    Stream.Free;
  end;
end;

Eso confirma que PDFium reconoce la maquetación; no prueba que las hints sean correctas. Para un test real de extremo a extremo, sirve el archivo a través de un transporte limitado por rangos y abre una página tardía con TPdfProgressiveDocument, igual que hace la guía de streaming de PDF grandes a demanda con PDFium VCL. Si la página se renderiza tras pedir solo una pequeña fracción del archivo, las hint tables están haciendo su trabajo. Añadir un segundo comprobador de linealización independiente al pipeline de releases también merece la pena, porque los visores difieren en lo indulgentes que son con un /T mal colocado o un offset ligeramente equivocado

Dos límites que conviene tener en la cabeza. El materializador de objetos aún rechaza arrays de 2 GiB o más, así que los documentos extremadamente grandes necesitan por ahora otra estrategia. Y la linealización solo rinde cuando el servidor honra los HTTP Range requests; detrás de un proxy que pele Accept-Ranges, un archivo linealizado se descarga exactamente igual que cualquier otro

FAQ

¿TPdf.SaveAsLinearized mantiene válidas las firmas digitales?

No. TPdf.SaveAsLinearized renumera y reescribe cada objeto, así que las firmas existentes quedan invalidadas. Linealiza primero y aplica la firma después

¿Puede PDFium Component linealizar un PDF cifrado?

Solo cuando pasas saRemoveSecurity. Las claves de cifrado a nivel de objeto dependen de los números de objeto, que el escritor cambia, así que la salida se escribe sin cifrado

¿Qué versiones de PDF soporta la salida linealizada?

TPdf.SaveAsLinearized acepta pvUnknown para conservar la versión del origen o cualquier objetivo desde PDF 1.2 en adelante, y rechaza saIncremental porque una revisión añadida rompería la maquetación

La salida Fast Web View, la carga progresiva y la API de disponibilidad son parte del PDFium Component para Delphi y C++Builder, que envuelve PDFium para proyectos Delphi, C++Builder y Lazarus