Artículo técnico

Carga por rango de bytes en Delphi para PDF incrustados con PDFium

PDFium Component puede abrir un PDF que vive dentro de un búfer más grande directamente desde un rango de bytes. La sobrecarga LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) direcciona una ventana en su sitio, así que no se necesita ningún Copy preliminar. A cambio, te pide que entiendas una regla: cuando Buffered es False, el array subyacente se toma prestado, no se copia

Este es un mecanismo distinto del enfoque guiado por retrollamadas descrito en streaming de PDF grandes bajo demanda con PDFium VCL, que le entrega a PDFium un lector FPDF_FILEACCESS y le deja extraer bloques de disco según los necesita. Ese es para documentos demasiado grandes para caber en RAM. Este es para documentos que ya están en RAM, situados en un desplazamiento conocido dentro de otra cosa. Los dos son complementarios, y la última sección explica qué situación corresponde a cuál

La copia de 40 MB que nadie pidió

El escenario aparece dondequiera que los PDF viajen dentro de otros formatos. Un almacén de correo mantiene cuerpos de mensaje y adjuntos en un único registro. Un contenedor de archivo concatena un manifiesto, algunas imágenes y un PDF. Un protocolo de red personalizado enmarca un documento detrás de una cabecera con prefijo de longitud. En todos los casos terminas sosteniendo un TBytes grande y sabiendo que el PDF empieza en el byte 1.182.336 y dura 312 kilobytes

Antes de que existiera la sobrecarga de rango de bytes, la respuesta idiomática era Copy(Data, Index, Count), que asigna un segundo array y copia con memcpy la ventana dentro de él. Después le entregas ese fragmento a LoadDocument con Buffered = True, que lo vuelve a copiar en el búfer privado del componente. Dos copias de los mismos bytes, una de ellas puro ceremonial, y en un escaneo de buzón grande repetido para cada mensaje. La sobrecarga de rango de bytes elimina la primera copia incondicionalmente y la segunda opcionalmente

Qué hace realmente la sobrecarga de rango de bytes

La sobrecarga es deliberadamente delgada: valida, calcula un puntero, y delega en la forma con puntero de LoadDocument por la que ya pasa toda la familia. Index es de base cero, Count es una longitud en bytes, y Buffered vale True por defecto exactamente igual que en las demás sobrecargas. El LoadDocument(const Data: TBytes; Buffered: Boolean) de un solo argumento ahora es en sí mismo solo una llamada a este con Index = 0 y Count = Length(Data), así que hay una ruta de validación en lugar de dos

Llamarlo se parece al código que ya estabas escribiendo, menos el fragmento

var
  Frame: TBytes;          // whole container record, tens of megabytes
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // your container parser

  // No Copy(Frame, Offset, Size) here - the window is addressed in place
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

¿Por qué Index más Count desborda la comprobación de límites?

Porque Index y Count son ambos Integer, y la suma de dos valores Integer positivos grandes no es necesariamente un Integer positivo grande. Este es el núcleo técnico de la sobrecarga, y es el único lugar donde una comprobación de aspecto natural es un agujero de seguridad de memoria. La formulación obvia está mal

// WRONG: Index + Count is evaluated in Integer and can wrap negative
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// RIGHT: reject signs first, then bound each term separately,
// with the only arithmetic done as a subtraction that cannot wrap
Check(Index >= 0,  'PDF byte range index cannot be negative');
Check(Count >= 0,  'PDF byte range count cannot be negative');
Check(Index <= Length(Data), 'PDF byte range index exceeds data length');
Check(Count <= Length(Data) - Index, 'PDF byte range exceeds data length');

Sigue el caso que falla paso a paso. Toma Index = 2000000000 y Count = 2000000000. Su suma real es cuatro mil millones, pero en aritmética con signo de 32 bits el resultado se desborda a exactamente menos 294.967.296. Ese valor es cómodamente menor que Length(Data), así que la comprobación incorrecta pasa, @Data[Index] se toma muy fuera del array, y a PDFium se le entrega un puntero descontrolado más una longitud de dos gigabytes. Lo que sigue es una violación de acceso en un buen día y el análisis silencioso de memoria de proceso no relacionada en uno malo

El orden correcto lo soluciona no sumando nunca. Los negativos se rechazan antes de indexar nada, así que @Data[Index] nunca puede tomarse por debajo del array. Luego Index se acota por su cuenta contra Length(Data), lo que garantiza que Length(Data) - Index sea un Integer no negativo. Solo entonces se compara Count contra ese resto. Cada valor intermedio permanece dentro del rango representable, así que ninguna configuración de compilación puede cambiar el resultado. Tampoco caigas en la tentación de confiar en la comprobación de desbordamiento {$Q+} como red de seguridad: los builds de release habitualmente se distribuyen con ella desactivada, e incluso cuando está activada has convertido un fallo de seguridad de memoria en un EIntOverflow que se escapa del medio de una rutina de validación. PDFium Component trata la aritmética de longitud no confiable igual que trata el resto del límite, una disciplina cubierta con más amplitud en reforzar el ABI de PDFium VCL y la seguridad de memoria en Delphi

¿Por qué una ventana de longitud cero debe pasar nil?

Porque @Data[Index] no es una expresión legal para todo Index que la validación acepta. Index = Length(Data) con Count = 0 es una ventana vacía perfectamente bien formada al final del búfer, y un TBytes vacío da Index = 0 en un array que no tiene ningún elemento cero en absoluto. Tomar la dirección en cualquiera de los dos casos indexa más allá del final, o desreferencia un array dinámico nil. Así que la sobrecarga bifurca: Count = 0 produce un puntero nil, cualquier otro conteo produce @Data[Index]. El nil fluye entonces hacia la sobrecarga con puntero, cuya propia guarda acepta un puntero nil cuando el tamaño es cero, y la carga termina en el error ordinario "Cannot load PDF document" en lugar de una violación de acceso. Un llamador que calculó una ventana de cero bytes a partir de un contenedor malformado obtiene un EPdfError limpio y detectable como cualquier otra entrada incorrecta

Prestado o copiado: lo que decide Buffered

Buffered selecciona el contrato de propiedad, y es el único parámetro aquí con consecuencias más allá de la llamada. Con Buffered = True, PDFium Component copia la ventana seleccionada, y solo la ventana, en su búfer interno antes de cargar. El contenedor de 40 MB no se copia; el PDF de 312 KB sí. Una vez que LoadDocument devuelve el control, puedes liberar, reutilizar o sobrescribir el contenedor inmediatamente, porque el componente ya no lo referencia. Esta es la opción por defecto y la correcta para casi todo el código

Buffered = False pasa @Data[Index] directamente a FPDF_LoadMemDocument64, y PDFium mantiene ese puntero durante toda la vida del documento en lugar de copiar los bytes. Eso hace que la carga sea libre de asignaciones, y hace que todo el TBytes subyacente sea un recurso prestado. Debe permanecer vivo y sin modificar hasta que se ejecute UnloadDocument o Active pase a False. No la ventana, el array entero: un array dinámico tiene conteo de referencias como unidad, y dejar que la última referencia desaparezca en cualquier parte de tu código libera la memoria que PDFium todavía está leyendo. Fijar Length sobre él es igual de fatal, porque una reasignación puede mover el bloque. Deja esto establecido en tu propia documentación de API dondequiera que expongas una carga así, en el mismo espíritu que cualquier otro límite de préstamo frente a propiedad en código Pascal; el modo de fallo es idéntico a los riesgos de aliasing descritos en la fuga de FillChar y cadena de resultado en Delphi, donde un búfer parece propio y no lo es

type
  TFrameSession = class
  private
    FFrame: TBytes;   // owns the backing storage for as long as FPdf is loaded
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame must outlive the loaded document
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // release the borrow first
  FFrame := nil;         // only now may the storage go
  inherited;
end;

Cuándo la ventana de rango de bytes es la herramienta equivocada

Sé honesto sobre el límite. La sobrecarga de rango de bytes asume que el contenedor ya está completamente en memoria, y Count es un Integer, así que una sola ventana no puede superar los dos gigabytes. Si el contenedor es un archivo de 6 GB en disco, o llega por un socket que no puedes rebobinar, esta sobrecarga no puede ayudarte, y leer todo en un TBytes solo para direccionar una ventana dentro de él anula el propósito. Ahí es precisamente donde pertenece la ruta FPDF_FILEACCESS, y el artículo de streaming bajo demanda muestra cómo exponer una vista con desplazamiento desplazado de un archivo como una fuente de documento personalizada. Igualmente, si los bytes incrustados necesitan transformación antes de que PDFium los vea, descompresión, descifrado, un paso de desenvolvimiento, entonces una copia real es inevitable y Buffered = True sobre el array transformado es la respuesta honesta. La ventana de rango de bytes compensa en exactamente una forma: bytes de PDF contiguos, sin modificar, ya residentes, en un desplazamiento conocido

Si estás evaluando esto para un visor, un panel de vista previa o un pipeline de ingesta por lotes, la sobrecarga de rango de bytes y el cargador en streaming son dos de las estrategias de carga que PDFium Component incluye junto a las cargas de archivo, flujo y puntero en bruto. La superficie completa de la API, la licencia y el soporte de versiones de Delphi y C++Builder están documentados en la página del producto PDFium Component