PDFlibPas puede abrir un PDF local mediante una vista de memoria acotada y de solo lectura: LoadFromMappedFile y DAOpenMappedFile mantienen exactamente una ventana deslizante sobre el archivo, la vuelven a mapear cuando hace falta y sirven cada segmento de objeto mediante lecturas con offset absoluto. La biblioteca PDF para Delphi no conserva en memoria toda la fuente, por lo que el uso del espacio de direcciones permanece estable aunque crezca el archivo. El diseño existe para una carga concreta: PDF de gigabytes cuyo parser ha terminado de cargar y sigue volviendo al disco, objeto a objeto y fragmento de stream a fragmento de stream
¿Por qué siguen siendo costosas las lecturas dispersas después de cargar el PDF?
Cargar un PDF no termina de leerlo, y en un archivo de varios gigabytes esa diferencia es donde se va el tiempo. Una tabla de referencias cruzadas o un stream de referencias cruzadas (ISO 32000-1 §7.5.4 y §7.5.8) solo registra dónde empieza cada objeto indirecto. Los bytes llegan después, cuando se renderiza una página, se decodifica un programa de fuente o se extrae un stream de archivo incrustado (ISO 32000-1 §7.11.4). Un archivo de 2 GB con decenas de miles de objetos se convierte en decenas de miles de lecturas pequeñas y desordenadas, y ninguna se conoce en el momento de la carga
La ruta que seguían antes esas lecturas era un Seek compartido seguido de Read sobre un stream posicional, y falla en dos direcciones a la vez. Cada fragmento paga una lectura de archivo aunque la página ya esté residente en la caché del sistema operativo, y el cursor es estado mutable compartido, así que un archivo local y la fuente por rangos que hay detrás de la carga progresiva de PDF por rangos con prefetch no podían ejecutar el mismo código de parser sin pelearse por la posición. PDFlibPas corrige ambos problemas convirtiendo la lectura con offset absoluto de optimización en contrato
¿Qué garantiza TPDFReadAtStream?
TPDFReadAtStream garantiza una lectura en un offset absoluto que no depende del cursor lógico del stream ni lo modifica. Es un descendiente abstracto de TStream con un único método virtual, y las dos fuentes de la biblioteca independientes del cursor derivan de él: TReadOnlyMappedFileStream para archivos locales y TByteRangeStream para los servidos por rangos remotos. El lector de segmentos de objetos pregunta una vez si su fuente es un TPDFReadAtStream y vuelve a la secuencia antigua de seek y read cuando no lo es, de modo que un file stream normal o un memory stream siguen funcionando sin cambios
type
// Streams de solo lectura cuyas lecturas absolutas evitan un Seek más Read compartido
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Acceso de solo lectura por ventanas a un único archivo local
TReadOnlyMappedFileStream = class(TPDFReadAtStream)
private
FMemoryMapped: Boolean;
public
constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
function GetStats: TPDFMappedFileStats;
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; override;
property MemoryMapped: Boolean read FMemoryMapped;
end;
La diferencia importa más de lo que sugiere la firma. ReadAt utiliza el offset que recibe y deja Position exactamente donde estaba, lo que permite que niveles anidados del parser hagan lecturas sin guardar y restaurar la posición alrededor de cada llamada. TReadOnlyMappedFileStream sigue implementando Read, Seek y Size como cualquier otro TStream, Seek limita la posición lógica al archivo y Write siempre devuelve 0 porque la fuente se abre en modo de solo lectura
Abrir un PDF mediante una vista mapeada en Delphi
Dos puntos de entrada explícitos abren una fuente mapeada, y ninguno cambia el comportamiento de los puntos de entrada que ya utiliza. LoadFromMappedFile carga y selecciona un documento; DAOpenMappedFile devuelve un handle de Direct Access sobre el mismo archivo, que es el modo adecuado al fusionar y dividir PDF de gigabytes mediante Direct Access. LoadFromFile y DAOpenFile conservan intacta su semántica de compartición de archivos, errores y compatibilidad, así que nada cambia para quienes no opten por la nueva ruta. Ambos puntos de entrada mapeados reciben un WindowSize solicitado en bytes y una máscara Options, y ambos aceptan 0 para cualquiera de los dos
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 selecciona el valor predeterminado de 64 MiB; aquí el mapeo es obligatorio
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// La extracción diferida recorre ahora ventanas mapeadas en lugar de hacer seek
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
¿Qué obliga a hacer realmente PDF_MAPPED_FILE_REQUIRE_MAPPING?
PDF_MAPPED_FILE_REQUIRE_MAPPING convierte un fallback silencioso en un fallo inmediato y diagnosticable durante la apertura. Con Options a 0, ambos puntos de entrada aceptan un fallback a un file stream de solo lectura: si la plataforma no tiene código de mapeo o falla la llamada de mapeo, el documento sigue abriéndose y cada lectura pasa por un file stream normal. Con el flag activado, PDFlibPas acepta la entrada solo cuando se ha establecido la primera vista, e informa del rechazo mediante LastErrorCode 401 en lugar de cargar un documento que ejecutaría silenciosamente exactamente como la ruta antigua
En Windows, el stream mapeado abre un segundo handle de solo lectura con FILE_SHARE_READ, FILE_SHARE_WRITE y FILE_SHARE_DELETE, además de FILE_FLAG_RANDOM_ACCESS, crea sobre él un mapeo PAGE_READONLY y mapea la primera ventana dentro del constructor. Mapear de forma inmediata es el objetivo: un fallo de «mapeo obligatorio» aparece en LoadFromMappedFile, no durante la primera lectura diferida de un objeto a mitad de un trabajo de renderizado. Conviene dejar claro dónde termina la garantía. El código de mapeo solo se compila para destinos Windows, y un archivo de cero bytes no intenta mapearse, así que PDF_MAPPED_FILE_REQUIRE_MAPPING es una petición que puede fallar legítimamente, no una promesa portable. Un WindowSize negativo, o cualquier bit de Options distinto del único valor documentado, se rechaza directamente con el mismo error 401
Una ventana, remapeada a la granularidad de asignación
Solo se conserva una vista, y eso es lo que mantiene el uso del espacio de direcciones independiente del tamaño del archivo. Un WindowSize de 0 selecciona 64 MiB; un valor inferior a la granularidad de asignación del sistema se eleva hasta ella; un valor superior a 1 GiB se limita; y el resultado se redondea hasta un número entero de unidades de granularidad, 65536 bytes en Windows salvo que GetSystemInfo informe de otro dwAllocationGranularity. Cuando una lectura cae fuera de la vista actual, PDFlibPas la desmapea, alinea el offset solicitado hacia abajo a un límite de granularidad y mapea allí una ventana nueva. La ventana final se limita al tamaño físico del archivo, por lo que la vista nunca se extiende más allá de su final
Una sola lectura puede atravesar cualquier número de ventanas: el bucle copia lo que pueda suministrar la vista actual, remapea y continúa, y una petición que se salga del final devuelve un recuento corto en lugar de fallar. Lo que PDFlibPas no hace deliberadamente es entregarle un puntero a la vista, porque la siguiente lectura que cruce una ventana lo invalidaría y ningún caller podría protegerse razonablemente contra ello. Los bytes mapeados se copian directamente a buffers de destino propiedad del parser, lo que elimina el buffer de entrada de archivo adicional y los cambios de posición, pero la biblioteca no afirma que el almacenamiento final del parser sea zero-copy. El windowing de lectura también compone con el lado de escritura, ya que el desplazamiento de referencias a nivel de byte durante una fusión rápida de PDF transmite los bytes de objeto mientras la fuente mapeada los proporciona. El compromiso del tamaño de ventana es evidente: una ventana menor ocupa menos espacio de direcciones y remapea más a menudo, lo que suele ser la decisión correcta dentro de un proceso de 32 bits
Qué protege el lock y qué informa GetMappedFileInfo
Una única sección crítica cubre la vista mapeada, el cursor del archivo de fallback, la posición lógica y las estadísticas, y la separación entre los dos métodos de lectura sale directamente de ahí. ReadAt toma el lock y llama al lector interno sin lock; Read toma el mismo lock, llama a la misma función interna con la posición lógica actual y después la avanza. Reutilizar la función interna en lugar del ReadAt público evita el locking recursivo, y mantener el lock durante todo el bucle de copia es lo que mantiene correcto un remapeo de una sola ventana bajo llamadas concurrentes. Conviene conocer un detalle de Free Pascal antes de portar: la unidad Windows de FPC declara su propio record llamado TCriticalSection, así que el campo y su construcción deben escribirse como SyncObjs.TCriticalSection. Delphi compila felizmente la forma sin calificar; FPC la resuelve como un record sin Create, Enter ni Leave
var
Pdf: TPDFlib;
Handle, PageRef: Integer;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
if Handle = 0 then
Exit;
try
PageRef := Pdf.DAFindPage(Handle, 1);
Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));
// {"memoryMapped":true,"fileSize":...,"remapCount":...}
if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
Writeln(Info);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
memoryMappedes false siempre que esté activo el fallback portable a file stream, y es el único campo que demuestra que nunca se estableció un mapeowindowSizees la ventana efectiva alineada, no el valor solicitado, ymappedByteses menor que ella en la ventana finalmappedOffsetes el inicio alineado a la asignación de la vista conservada, o -1 cuando no hay ninguna vista activareadCallscuenta las peticiones de lectura correctas dentro del rango,bytesReadcuenta los bytes copiados a los callers yremapCountincluye la vista inicial
Las regresiones específicas cubren lecturas absolutas entre ventanas, conservación del cursor lógico, lecturas cortas al final, offsets inválidos, escrituras rechazadas, remapeos entre ventanas separadas, extracción diferida de un adjunto incomprimible de 220 KB y estadísticas que se vuelven inválidas después de DACloseFile; las suites headless Win32 y Win64 descubrieron 1467 pruebas cada una y las superaron todas sin resultados ignorados, fallidos, erróneos ni fugas. Si trabaja con PDF de gigabytes en Delphi o C++Builder y su profiler sigue señalando las lecturas de archivo en lugar del parsing, los puntos de entrada de archivo mapeado merecen una tarde de mediciones, y GetMappedFileInfo le dirá si realmente obtuvo un mapeo. La referencia completa de la API y una compilación de prueba están en la página de la biblioteca PDF para Delphi PDFlibPas