El PDFium Component abre un PDF que sigue descargándose vía TPdfProgressiveDocument, una subclase de TPdf que envuelve el API de disponibilidad FPDFAvail_* de PDFium. BeginProgressiveLoad arranca la sesión, CheckDocumentAvailability reporta qué rangos de bytes todavía necesita PDFium, OpenProgressiveDocument abre el archivo en cuanto existen suficientes bytes, y CancelProgressiveLoad abandona una descarga interrumpida sin filtrar handles nativos. Lo difícil no es el camino feliz. Un visor sobre una conexión inestable va a ver usuarios cerrar la pestaña al 25 por ciento, arrepentirse, y abrir el mismo enlace de nuevo, y cada una de esas sesiones abortadas tiene un handle de disponibilidad nativo, dos records de callback C, un stream adapter y un set de range requests en vuelo que deben liberarse en exactamente el orden correcto
¿Cómo carga TPdfProgressiveDocument un PDF que sigue descargándose?
TPdfProgressiveDocument mantiene vivo un proveedor de disponibilidad de PDFium mientras se llena un stream de acceso aleatorio, y le pregunta a ese proveedor antes de cada paso de parseo si los bytes que quiere están presentes. BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) toma el stream de respaldo más el tamaño lógico del archivo remoto, cablea un callback IsDataAvail y un callback AddSegment en dos records, y llama FPDFAvail_Create. Cuando PDFium pregunta si un rango está presente, el componente responde que sí si el rango cae dentro del prefijo contiguo descrito por AvailableByteCount o dentro de un rango ya completado vía el scheduler RangeRequests, y el evento OnDataAvailable puede invalidar el veredicto para stores dispersos. Cada llamada a CheckDocumentAvailability devuelve uno de tres valores TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) y entrega los rangos que PDFium pidió como un array TPdfDownloadRanges ordenado y fusionado, ya en cola en el scheduler con prioridad rrpImmediate
// FetchRange es su transporte (HTTP Range GET, socket, lector de blob):
// escribe Size bytes en Offset dentro de Store y devuelve cuántos llegaron
function FetchRange(Store: TStream; Offset, Size: UInt64): UInt64; forward;
procedure OpenWhileDownloading(Pdf: TPdfProgressiveDocument; Store: TStream;
RemoteSize: UInt64);
const
MaxRounds = 64;
var
Hints: TPdfDownloadRanges;
State: TPdfDataAvailability;
Request: TPdfRangeRequest;
Round: Integer;
begin
Pdf.BeginProgressiveLoad(Store, RemoteSize, False);
State := pdaNotAvailable;
for Round := 1 to MaxRounds do
begin
State := Pdf.CheckDocumentAvailability(Hints);
if State <> pdaNotAvailable then
Break;
// Los hints ya están en cola; escriba primero los bytes, después complete
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
if State <> pdaAvailable then
raise EPdfError.Create('The document could not be discovered');
Pdf.OpenProgressiveDocument;
end;
Dos detalles de ese bucle son load-bearing. El tope de rondas importa porque un enlace muerto hace que CheckDocumentAvailability pida los mismos rangos para siempre, y un bucle sin tope convierte una falla de red en una UI colgada. El orden importa porque el scheduler serializa su propio estado con una critical section pero no hace nada por TStream.Position del store de respaldo: un thread de transporte debe escribir los bytes de la respuesta en el stream antes de llamar CompleteRequest, porque en el momento en que una completion se publica PDFium puede leer ese rango, y los escritores concurrentes necesitan I/O posicionado o un lock propio
¿Por qué AvailableByteCount se niega a retroceder?
AvailableByteCount solo crece, y el setter lanza EPdfError con "Available byte count cannot move backwards" cuando usted intenta encogerlo. Una vez que el callback IsDataAvail le dijo a PDFium que un rango existe, el parser ya pudo haber leído y cacheado objetos de él, así que retirar esos bytes después dejaría las respuestas de disponibilidad inconsistentes con lo que PDFium ya consumió. El mismo setter rechaza valores mayores que LogicalFileSize y lanza "No progressive load is active" fuera de una sesión, que es por qué los bytes que ya tiene antes de que arranque la carga pertenecen al argumento AInitialAvailableByteCount de BeginProgressiveLoad y no a una asignación de propiedad hecha demasiado temprano. Si su store de descarga se llena fuera de orden, no intente expresarlo por el prefijo: complete los rangos vía el scheduler o responda vía OnDataAvailable
¿Cuándo puede abrirse de verdad un PDF descargado a medias?
Solo un PDF linearizado (ISO 32000-1 Anexo F, el layout "Fast Web View") abre antes de que llegue el archivo completo; un PDF no linearizado todavía necesita cada byte. OpenProgressiveDocument chequea la propiedad Linearization (plnUnknown, plnNotLinearized, plnLinearized) y enruta en consecuencia: un archivo linearizado abre vía FPDFAvail_GetDocument apenas están presentes la sección de primera página y las tablas de hints, mientras que un archivo no linearizado se abre vía FPDF_LoadCustomDocument sobre el mismo record de acceso a archivo y se trata como legible solo como un todo. El routing existe por una razón concreta. Llamar FPDFAvail_GetDocument sobre un archivo no linearizado puede devolver un handle no nulo cuyo conteo de páginas es cero, un documento que parece abierto y está vacío. En el test suite propio del componente, un fixture linearizado de 51 páginas alcanza pdaAvailable y abre con su árbol de páginas completo mientras el store de descarga disperso todavía no cubre el archivo
function WaitForPage(Pdf: TPdfProgressiveDocument; Store: TStream;
PageNumber: Integer): Boolean;
var
Hints: TPdfDownloadRanges;
Request: TPdfRangeRequest;
Round: Integer;
begin
Result := False;
for Round := 1 to 64 do
case Pdf.LoadAvailablePage(PageNumber, Hints) of
pdaAvailable:
Exit(True); // PageNumber ya es la página activa
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
LoadAvailablePage toma un número de página base 1 y impone el orden que PDFium espera: antes del primer chequeo de página corre CheckFormAvailability, que envuelve a FPDFAvail_IsFormAvail, y solo después llama FPDFAvail_IsPageAvail. Un resultado pfaNotPresent es la respuesta normal para un documento sin AcroForm y no bloquea nada. Cuando la página está lista, LoadAvailablePage la vuelve la página activa, así que un visor puede renderizar la página 1 de un folleto linearizado mientras las restantes siguen en tránsito; FirstAvailablePageNumber le dice qué página designa el diccionario de linearización como primera, ya convertida del índice base cero de PDFium
¿Qué libera CancelProgressiveLoad, y en qué orden?
CancelProgressiveLoad desarma una sesión en cuatro pasos que no se pueden reordenar: cancelar el scheduler de rangos, cerrar el documento, destruir el handle de disponibilidad con FPDFAvail_Destroy, y después disponer los records de callback y liberar el stream adapter. Cancelar primero el scheduler incrementa su contador de generación, suelta todo request pendiente y en vuelo, y dispara OnCancelRequest por cada uno en vuelo, así que una completion de transporte que aterrice después carga la generación vieja y CompleteRequest devuelve False sin tocar nada. El documento debe cerrarse antes de que se vayan el handle de disponibilidad y el adapter porque PDFium puede hacer callback al proveedor de acceso a archivo mientras cierra un documento, y si el adapter ya no está ese callback lee memoria liberada
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// El scheduler vive tanto como FPdf, así que cablee una sola vez
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // código suyo: cierre ese socket o request
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
El método es idempotente y es el único camino de limpieza para tres situaciones: un BeginProgressiveLoad que falla a mitad de construcción, un cancel explícito del usuario, y el destructor. BeginProgressiveLoad además lo llama antes de arrancar, así que reiniciar el mismo objeto sobre una URL nueva es seguro sin un cancel explícito. Una decisión de propiedad es suya y hay que acertarla: si un worker thread escribe en el stream de respaldo, pase AOwnsStream = False y libere el stream usted mismo después de que el worker haya parado, porque con la propiedad entregada el cancel libera el stream mientras una escritura tardía todavía puede estar en camino. Las excepciones lanzadas dentro de OnCancelRequest se tragan por request, así que un transporte que falle no puede bloquear las cancelaciones restantes
¿Cómo prueba el suite de lifecycle que el camino de cancel no filtra?
El suite de estrés de lifecycle del PDFium Component ejercita una descarga interrumpida estilo red en cada ciclo mixto. Cada ciclo arranca una carga progresiva cuyo store contiene solo un cuarto de los bytes del fixture, exige pdaNotAvailable con una lista de hints no vacía, llama CancelProgressiveLoad, y afirma que el objeto no reporta ni ProgressiveLoading ni Active; después corre el mismo camino de streaming hasta el final con disponibilidad completa, OpenProgressiveDocument, un render y un close. La corrida mixta por defecto cubre 100 ciclos medidos con 600 opens, 2300 renders y 100 cancelaciones progresivas, y la memoria privada muestreada creció 8.21 MiB contra un presupuesto de 32 MiB. El suite cuenta las cancelaciones progresivas separadas de las cancelaciones de render-callback, porque una descarga abortada y un bucle de render que para temprano son eventos distintos con criterios de aceptación distintos
Dónde el camino progresivo deja de ayudar
Unos cuantos límites valen conocerlos antes de construir un visor encima de esto. Las features que necesitan los bytes del archivo original rechazan una fuente progresiva incompleta en lugar de adivinar: ReadXmpPacket falla explícitamente y la validación de firmas reporta Indeterminate hasta que el archivo completo está presente. El test de disponibilidad por defecto asume un prefijo contiguo, así que un transporte que descarga rangos fuera de orden debe completarlos vía RangeRequests o responder vía OnDataAvailable, o PDFium seguirá pidiendo bytes que usted ya tiene. Un archivo no linearizado no gana nada en tiempo hasta primera página, así que si el primer pintado rápido importa, linearice el archivo del lado del servidor. Y CancelProgressiveLoad no cierra sus sockets por sí solo; OnCancelRequest es el hook donde eso ocurre
Para el camino simple de stream adapter que carga a demanda un archivo local completo, vea hacer streaming de PDFs grandes a demanda con PDFium; para abrir un PDF que está dentro de un buffer mayor, vea carga por byte range para PDFs incrustados. Cancelar un render lento de una página ya cargada es un mecanismo aparte, cubierto en rendering progresivo de páginas cancelable. TPdfProgressiveDocument y su scheduler de rangos vienen con el PDFium Component for Delphi and C++Builder