O PDFium Component abre um PDF que ainda está a ser descarregado através do TPdfProgressiveDocument, uma subclasse de TPdf que embrulha a API de disponibilidade FPDFAvail_* do PDFium. O BeginProgressiveLoad arranca a sessão, o CheckDocumentAvailability comunica que intervalos de bytes o PDFium ainda precisa, o OpenProgressiveDocument abre o ficheiro logo que existam bytes chegados, e o CancelProgressiveLoad abandona um download interrompido sem fugas de handles nativos. A parte difícil não é o caminho feliz. Um visualizador numa ligação instável vai ver utilizadores fechar o separador aos 25 por cento, mudar de ideias, e abrir o mesmo link outra vez, e cada uma dessas sessões abortadas tem um handle de disponibilidade nativo, dois registos de callbacks C, um adaptador de stream e um conjunto de pedidos de intervalo em voo que têm de ser libertados pela ordem certa
Como carrega o TPdfProgressiveDocument um PDF que ainda está a ser descarregado?
O TPdfProgressiveDocument mantém um fornecedor de disponibilidade do PDFium vivo enquanto um stream de acesso aleatório é preenchido, e pergunta a esse fornecedor antes de cada passo de análise se os bytes que quer estão presentes. O BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) recebe o stream de suporte mais o tamanho lógico do ficheiro remoto, liga um callback IsDataAvail e um callback AddSegment em dois registos, e chama o FPDFAvail_Create. Quando o PDFium pergunta se um intervalo está presente, o componente responde que sim se o intervalo estiver dentro do prefixo contíguo descrito pelo AvailableByteCount ou dentro de um intervalo já completado pelo agendador RangeRequests, e o evento OnDataAvailable pode sobrepor-se ao veredicto para lojas esparsas. Cada chamada ao CheckDocumentAvailability devolve um de três valores TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) e entrega os intervalos que o PDFium pediu como uma matriz TPdfDownloadRanges ordenada e fundida, já em fila no agendador com prioridade rrpImmediate
// FetchRange é o seu transporte (HTTP Range GET, socket, leitor de blobs):
// escreve Size bytes em Offset no Store e devolve quantos chegaram
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;
// As dicas já estão em fila; escreva primeiro os bytes, depois 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;
Dois detalhes nesse loop são estruturais. O limite de rondas importa porque um link morto faz o CheckDocumentAvailability pedir os mesmos intervalos para sempre, e um loop sem limite transforma uma falha de rede numa UI pendurada. A ordem importa porque o agendador serializa o seu próprio estado com uma secção crítica mas não faz nada pelo TStream.Position na loja de suporte: uma thread de transporte tem de escrever os bytes da resposta no stream antes de chamar o CompleteRequest, porque no momento em que uma conclusão é publicada o PDFium pode ler esse intervalo, e escritores concorrentes precisam de I/O posicionado ou de um bloqueio deles próprios
Porque é que o AvailableByteCount se recusa a andar para trás?
O AvailableByteCount só cresce, e o setter dispara EPdfError com "Available byte count cannot move backwards" quando tenta encolhê-lo. Depois de o callback IsDataAvail ter dito ao PDFium que um intervalo existe, o parser pode já ter lido e posto em cache objetos dele, por isso retirar esses bytes depois tornaria as respostas de disponibilidade inconsistentes com o que o PDFium já consumiu. O mesmo setter rejeita valores maiores do que o LogicalFileSize e dispara "No progressive load is active" fora de uma sessão, razão pela qual bytes que já tem antes de a carga começar pertencem ao argumento AInitialAvailableByteCount do BeginProgressiveLoad em vez de uma atribuição de propriedade feita demasiado cedo. Se a sua loja de download enche fora de ordem, não tente expressá-lo pelo prefixo de todo: complete os intervalos pelo agendador ou responda pelo OnDataAvailable
Quando é que um PDF parcialmente descarregado abre mesmo?
Só um PDF linearizado (Anexo F da ISO 32000-1, a disposição "Fast Web View") abre antes de o ficheiro inteiro chegar; um PDF não linearizado ainda precisa de todos os bytes. O OpenProgressiveDocument confere a propriedade Linearization (plnUnknown, plnNotLinearized, plnLinearized) e encaminha em conformidade: um ficheiro linearizado abre através do FPDFAvail_GetDocument logo que a secção da primeira página e as tabelas de dicas estejam presentes, enquanto um ficheiro não linearizado é aberto através do FPDF_LoadCustomDocument no mesmo registo de acesso a ficheiro e tratado como legível apenas como um todo. O encaminhamento existe por uma razão concreta. Chamar o FPDFAvail_GetDocument num ficheiro não linearizado pode devolver um handle não nulo cuja contagem de páginas é zero, um documento que parece aberto e está vazio. Na suíte de testes do próprio componente um fixture linearizado de 51 páginas chega a pdaAvailable e abre com a sua árvore de páginas completa enquanto a loja de download esparsa ainda não cobre o ficheiro
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 é agora a página ativa
pdaError:
Exit(False);
pdaNotAvailable:
while Pdf.RangeRequests.TryDequeue(Request) do
Pdf.RangeRequests.CompleteRequest(Request,
FetchRange(Store, Request.Offset, Request.Size));
end;
end;
O LoadAvailablePage recebe um número de página de base um e impõe a ordem que o PDFium espera: antes da primeira verificação de página corre o CheckFormAvailability, que embrulha o FPDFAvail_IsFormAvail, e só depois disso chama o FPDFAvail_IsPageAvail. Um resultado pfaNotPresent é a resposta normal para um documento sem AcroForm e não bloqueia nada. Quando a página está pronta, o LoadAvailablePage torna-a a página ativa, por isso um visualizador pode renderizar a página 1 de um folheto linearizado enquanto as restantes páginas ainda estão em trânsito; o FirstAvailablePageNumber diz-lhe que página o dicionário de linearização designa como a primeira, já convertido do índice de base zero do PDFium
O que liberta o CancelProgressiveLoad, e por que ordem?
O CancelProgressiveLoad desmonta uma sessão em quatro passos que não podem ser reordenados: cancelar o agendador de intervalos, fechar o documento, destruir o handle de disponibilidade com FPDFAvail_Destroy, e depois dispensar os registos de callbacks e libertar o adaptador de stream. Cancelar o agendador primeiro incrementa o contador de geração dele, larga todos os pedidos pendentes e em voo, e dispara OnCancelRequest para cada um dos em voo, por isso uma conclusão de transporte que aterrar mais tarde transporta a geração velha e o CompleteRequest devolve False sem tocar em nada. O documento tem de fechar antes de o handle de disponibilidade e o adaptador irem embora porque o PDFium pode fazer callback ao fornecedor de acesso a ficheiro enquanto fecha um documento, e se o adaptador já se foi esse callback lê memória libertada
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
FPdf := TPdfProgressiveDocument.Create(nil);
// O agendador vive o mesmo que o FPdf, por isso ligue-o uma vez
FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;
procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
Attempt: Cardinal);
begin
FTransport.Abort(RequestId); // código seu: feche esse socket ou pedido
end;
procedure TDownloadForm.CancelButtonClick(Sender: TObject);
begin
FPdf.CancelProgressiveLoad;
// ProgressiveLoading = False, Active = False, AvailableByteCount = 0
end;
O método é idempotente e é o caminho único de limpeza para três situações: um BeginProgressiveLoad que falha a meio da construção, um cancelamento explícito do utilizador, e o destrutor. O BeginProgressiveLoad também o chama antes de arrancar, por isso reiniciar o mesmo objeto num URL novo é seguro sem cancelamento explícito. Uma decisão de posse é sua para acertar: se uma worker thread escreve no stream de suporte, passe AOwnsStream = False e liberte o stream você próprio depois de a worker parar, porque com a posse entregue o cancelamento liberta o stream enquanto uma escrita tardia ainda pode estar a caminho. Exceções disparadas dentro do OnCancelRequest são engolidas por pedido para um transporte a falhar não bloquear os cancelamentos restantes
Como prova a suíte de lifecycle que o caminho de cancelamento não tem fugas?
A suíte de stress de lifecycle do PDFium Component exercita um download interrompido ao estilo de rede em todos os ciclos mistos. Cada ciclo arranca uma carga progressiva cuja loja só tem um quarto dos bytes do fixture, exige pdaNotAvailable com uma lista de dicas não vazia, chama o CancelProgressiveLoad, e verifica que o objeto não reporta nem ProgressiveLoading nem Active; depois corre o mesmo caminho de streaming até ao fim com disponibilidade total, OpenProgressiveDocument, uma renderização e um fecho. A corrida mista predefinida cobre 100 ciclos medidos com 600 aberturas, 2300 renderizações e 100 cancelamentos progressivos, e a memória privada amostrada cresceu 8.21 MiB contra um orçamento de 32 MiB. A suíte conta cancelamentos progressivos separadamente dos cancelamentos de callbacks de renderização, porque um download abortado e um loop de renderização que pára cedo são eventos diferentes com critérios de aceitação diferentes
Onde o caminho progressivo deixa de ajudar
Vale a pena conhecer alguns limites antes de construir um visualizador por cima disto. Funcionalidades que precisam dos bytes do ficheiro original recusam uma fonte progressiva incompleta em vez de adivinhar: o ReadXmpPacket falha explicitamente e a validação de assinaturas reporta Indeterminate até o ficheiro inteiro estar presente. O teste de disponibilidade predefinido assume um prefixo contíguo, por isso um transporte que busca intervalos fora de ordem tem de os completar pelo RangeRequests ou responder pelo OnDataAvailable, ou o PDFium continuará a pedir bytes que já tem. Um ficheiro não linearizado não ganha nada no tempo até à primeira página, por isso se a primeira pintura rápida importa, linearize o ficheiro do lado do servidor. E o CancelProgressiveLoad não fecha os seus sockets por si; o OnCancelRequest é o gancho onde isso acontece
Para o caminho simples do adaptador de stream que carrega um ficheiro local completo a pedido, veja streaming de PDFs grandes a pedido com o PDFium; para abrir um PDF que vive dentro de um buffer maior, veja carregamento por intervalos de bytes para PDFs incorporados. Cancelar uma renderização lenta de uma página já carregada é um mecanismo separado, coberto em renderização progressiva de páginas cancelável. O TPdfProgressiveDocument e o seu agendador de intervalos viajam com o PDFium Component para Delphi e C++Builder