Un archivo escaneado puede alcanzar varios gigabytes en un solo PDF. Un visor que abre dicho archivo generalmente desea mostrar una sola página, tal vez la tabla de contenido o una página a la que el usuario saltó desde un marcador. Leer todo el archivo en memoria para renderizar dos páginas es un desperdicio en todos los aspectos: consume espacio de direcciones, retrasa al usuario detrás de una larga lectura inicial y, en un proceso Delphi de 32 bits, puede fallar por completo antes de que aparezca una sola página. PDFium fue construido con esto en mente. Puede cargar un documento a través de una devolución de llamada que solicita los rangos de bytes específicos que necesita, cuando los necesita, y nunca exige todo el archivo a la vez
El componente expone esa ruta a través de un adaptador de flujo. Le entrega cualquier TStream, y PDFium extrae bloques de ese flujo bajo demanda. El archivo puede estar en el disco, en un campo blob de base de datos o detrás de cualquier otro descendiente de TStream, y nada de ello se copia en la memoria de antemano
Cómo solicita bytes PDFium
La API de C de PDFium carga un documento desde un objeto suministrado por el llamador descrito por la estructura FPDF_FILEACCESS. La estructura tiene tres partes importantes aquí: un campo de longitud, una devolución de llamada de lectura y un parámetro de usuario opaco. El punto de entrada que la consume es FPDF_LoadCustomDocument. Una vez que PDFium tiene esa estructura, analiza el trailer, localiza la tabla de referencias cruzadas y, a partir de ese momento, lee solo lo que requiere una operación determinada. Abrir el documento toca la cola del archivo y un puñado de objetos de catálogo. Renderizar la página 400 lee los flujos de contenido y recursos para esa página y nada más
Esta es la diferencia entre una carga con almacenamiento en búfer y una carga por transmisión. Una carga con almacenamiento en búfer lee el archivo de extremo a extremo antes de que PDFium vea el byte cero. Una carga por transmisión invierte la relación: PDFium dirige las lecturas, y los bytes que nunca se tocan nunca se leen. Para un archivo de varios gigabytes visto una página a la vez, esa es la diferencia entre una carga inutilizable y una instantánea
El adaptador de flujo
El adaptador que sirve de puente entre un TStream de Delphi y FPDF_FILEACCESS es TPdfStreamAdapter. Su constructor toma el flujo y una bandera de propiedad, captura la longitud del flujo una vez, llena el registro FPDF_FILEACCESS y conecta la devolución de llamada de lectura. Cuando PDFium vuelve a llamar más tarde con un desplazamiento y un tamaño, el adaptador posiciona el flujo en ese desplazamiento y copia exactamente ese rango en el búfer proporcionado por PDFium
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
La bandera de propiedad decide quién libera el flujo. Pase False y el llamador conservará el flujo y deberá mantenerlo vivo durante toda la vida útil del documento. Pase True y el adaptador se hará cargo, liberando el flujo cuando se cierre el documento. De cualquier manera, el flujo tiene que sobrevivir a cada lectura que PDFium realice, porque PDFium conserva el puntero FPDF_FILEACCESS y volverá a llamar en cualquier punto mientras el documento esté abierto, no solo durante la carga inicial
Por qué la devolución de llamada es una función estática
La devolución de llamada de lectura que PDFium almacena en m_GetBlock es un puntero de función de C simple con la convención de llamadas cdecl. No se puede utilizar un método de Delphi directamente, porque un método lleva un argumento Self oculto del cual el llamador de C no sabe nada y nunca suministrará. Por lo tanto, el adaptador declara la devolución de llamada como una función de clase class function marcada como cdecl; static, que se compila como una función independiente con el diseño de marco de C que espera PDFium y sin Self implícito
Eso resuelve la convención de llamadas pero plantea una segunda pregunta: sin Self, ¿cómo llega la devolución de llamada al flujo específico del que se supone que debe leer? La respuesta es el parámetro de usuario opaco. Cuando el adaptador construye el registro, almacena su propio puntero de instancia en m_Param. PDFium devuelve ese mismo puntero como primer argumento de cada devolución de llamada. La función estática lo convierte de nuevo en un TPdfStreamAdapter y envía la lectura contra el flujo de esa instancia. Este es el trampolín estándar para transferir el contexto del objeto a través de un límite de C que no tiene noción de objetos
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
El límite de 4 GiB y por qué necesita una protección
El campo de longitud m_FileLen en FPDF_FILEACCESS es un valor sin signo de 32 bits. Su longitud representable más grande es un byte menos que 4 GiB. Un TStream reporta su tamaño como un Int64, lo que un flujo puede describir muchos más bytes de los que el campo puede contener. En el momento en que el tamaño de un flujo excede ese límite, no hay una forma honesta de decirle a PDFium qué tan largo es el archivo
La respuesta incorrecta es asignar el tamaño y dejar que se desborde. Truncar una longitud de 5 GiB a un campo de 32 bits produce un número pequeño y de apariencia plausible, y PDFium analizará el archivo creyendo que termina aproximadamente a un gigabyte. El trailer y la tabla de referencias cruzadas se encuentran al final real del archivo, mucho más allá de la longitud truncada, por lo que el análisis falla de una manera que no tiene nada que ver con la causa real. Estaría depurando un error de referencia cruzada en un archivo que es válido, sin ninguna pista de que un entero se desbordó dos capas más arriba
En su lugar, el adaptador rechaza la entrada. El constructor compara el tamaño del flujo con High(FPDF_DWORD) y genera EPdfError en el instante en que el flujo es demasiado grande para describirlo. Un error explícito e inmediato nombra el problema real en el punto de construcción. Un truncamiento silencioso lo oculta detrás de un síntoma engañoso que investigaría mucho más tarde. El límite de 4 GiB es una restricción real de esta ruta de carga, y lo honesto es presentarlo con fuerza en lugar de disimularlo con operaciones aritméticas que resultan compilar
Los fallos no deben cruzar el límite
Una lectura puede fallar. El flujo puede ser un objeto respaldado por la red que agota el tiempo de espera, un controlador blob que se cerró debajo de usted o un archivo que se truncó después de abrir el documento. El contrato de PDFium para la devolución de llamada de lectura es un valor de retorno: distinto de cero para éxito, cero para fallo. Es un marco de C y no tiene ningún mecanismo para capturar o propagar una excepción de Pascal
Es por esto que el trampolín envuelve la búsqueda y la lectura en un bloque try/except que absorbe la excepción y devuelve cero. Si se permitiera que una excepción de Delphi se propagara fuera de la devolución de llamada, se desenrollaría a través de los marcos de pila cdecl de PDFium, que nunca fueron diseñados para ser desenrollados por el mecanismo de excepciones de Pascal. El resultado es un comportamiento indefinido en el mejor de los casos y una caída dura en el peor, profundamente dentro del analizador PDF sin una pila utilizable. Devolver cero mantiene el fallo dentro del contrato. PDFium ve una lectura de bloque fallida, aborta la operación limpiamente y FPDF_LoadCustomDocument reporta que el documento no se pudo cargar, lo cual el componente presenta como un EPdfError en el lado de Pascal al que pertenece
Apertura de un documento de esta manera
El método del componente que dirige la ruta de transmisión es LoadCustomDocument, declarado como un método distinto en lugar de otra sobrecarga de LoadDocument para que pasar un TMemoryStream nunca termine accidentalmente en la ruta con almacenamiento en búfer. Construye el adaptador, llama a FPDF_LoadCustomDocument y mantiene vivo el adaptador durante la vida útil del documento cargado
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
La misma llamada funciona para un TMemoryStream, un flujo blob de un conjunto de datos de base de datos o un descendiente de TStream personalizado. La carga bajo demanda gana su lugar cuando el archivo es grande y solo se leerá una parte de él: un visor de archivos, un generador de miniaturas que toma muestras de unas pocas páginas o un índice de búsqueda que extrae una página a la vez. Cuando el archivo es pequeño o va a leer todo de todos modos, una carga con almacenamiento en búfer es más simple y el mecanismo de transmisión no aporta nada. El factor decisivo es la relación entre los bytes que realmente tocará y los bytes que contiene el archivo
Una vez que las páginas se transmiten bajo demanda, la siguiente preocupación es mantener la capacidad de respuesta de las páginas procesadas a medida que el usuario hace zoom y se desplaza, lo cual se cubre en nuestra nota sobre el rendimiento del zoom y el almacenamiento en caché de procesamiento. Cuando el documento transmitido es uno que un visor debe mostrar pero no permitir que el usuario lo exporte o altere, las técnicas en la guía práctica de vista previa de PDF segura se complementan naturalmente con esta ruta de carga. Ambos se basean en la carga por transmisión descrita aquí, que se incluye como parte de PDFium Component para Delphi y C++Builder, junto con las API de procesamiento, extracción de texto y anotaciones cubiertas en otras partes de este blog