Artigo Técnico

Progressive download e cancelamento em Delphi (FPDFAvail)

O PDFium Component abre um PDF que ainda está baixando por meio do TPdfProgressiveDocument, uma subclasse de TPdf que embrulha a API de availability FPDFAvail_* do PDFium. O BeginProgressiveLoad inicia a sessão, o CheckDocumentAvailability reporta que intervalos de bytes o PDFium ainda precisa, o OpenProgressiveDocument abre o arquivo quando bytes suficientes existem, e o CancelProgressiveLoad abandona um download interrompido sem vazar handles nativos. A parte difícil não é o caminho feliz. Um viewer numa conexão instável vai ver usuários fechar a aba em 25 por cento, mudar de ideia, e abrir o mesmo link de novo, e cada uma dessas sessões abortadas tem um handle nativo de availability, dois records de callback C, um adaptador de stream e um conjunto de range requests em voo que precisam ser liberados exatamente na ordem certa

Como o TPdfProgressiveDocument carrega um PDF que ainda está baixando?

O TPdfProgressiveDocument mantém um provider de availability do PDFium vivo enquanto um stream de acesso aleatório é preenchido, e pergunta a esse provider antes de cada passo de parse se os bytes que ele quer estão presentes. O BeginProgressiveLoad(AStream, AFileSize, AOwnsStream, AInitialAvailableByteCount) recebe o stream de apoio mais o tamanho lógico do arquivo remoto, liga um callback IsDataAvail e um callback AddSegment em dois records, e chama o FPDFAvail_Create. Quando o PDFium pergunta se um range está presente, o componente responde sim se o range está dentro do prefixo contíguo descrito pelo AvailableByteCount ou dentro de um range já completado pelo scheduler RangeRequests, e o evento OnDataAvailable pode sobrepor o veredito para stores esparsos. Cada chamada ao CheckDocumentAvailability devolve um de três valores de TPdfDataAvailability (pdaAvailable, pdaNotAvailable, pdaError) e entrega os ranges que o PDFium pediu como um array TPdfDownloadRanges ordenado e mesclado, já na fila do scheduler com prioridade rrpImmediate

// FetchRange é o seu transporte (HTTP Range GET, socket, leitor de blob):
// escreve Size bytes no Offset em 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 hints já estão na fila; escreva os bytes primeiro, 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 teto de rodadas importa porque um link morto faz o CheckDocumentAvailability pedir os mesmos ranges para sempre, e um loop sem limite transforma uma falha de rede numa UI travada. A ordem importa porque o scheduler serializa o próprio estado dele com uma critical section mas não faz nada pelo TStream.Position no store de apoio: uma thread de transporte precisa escrever os bytes da resposta no stream antes de chamar o CompleteRequest, já que no instante em que uma conclusão é publicada o PDFium pode ler aquele range, e writers concorrentes precisam de I/O posicionado ou de um lock próprio

O loop de availability do TPdfProgressiveDocument no PDFium Component: o BeginProgressiveLoad cria o provider FPDFAvail, o CheckDocumentAvailability entrega hints de download ordenados e mesclados na fila com prioridade rrpImmediate, o transporte escreve bytes no store antes do CompleteRequest publicar cada range para o PDFium, e o loop é limitado a 64 rodadas porque um link morto continua pedindo os mesmos ranges
Escreva os bytes, depois complete a requisição: no instante em que uma conclusão é publicada o PDFium pode ler aquele range, e nada protege a posição do stream por você

Por que o AvailableByteCount se recusa a andar para trás?

O AvailableByteCount só cresce, e o setter levanta EPdfError com "Available byte count cannot move backwards" quando você tenta encolhê-lo. Uma vez que o callback IsDataAvail disse ao PDFium que um range existe, o parser pode já ter lido e feito cache de objetos dele, então retirar esses bytes depois tornaria as respostas de availability inconsistentes com o que o PDFium já consumiu. O mesmo setter rejeita valores maiores que o LogicalFileSize e levanta "No progressive load is active" fora de uma sessão, razão pela qual bytes que você já tem antes do load começar pertencem ao argumento AInitialAvailableByteCount do BeginProgressiveLoad em vez de uma atribuição de propriedade feita cedo demais. Se o seu store de download enche fora de ordem, nem tente expressar isso pelo prefixo: complete os ranges pelo scheduler ou responda pelo OnDataAvailable

Quando um PDF parcialmente baixado consegue de fato abrir?

Só um PDF linearizado (ISO 32000-1 Anexo F, o layout "Fast Web View") abre antes de o arquivo inteiro chegar; um PDF não linearizado ainda precisa de todo byte. O OpenProgressiveDocument checa a propriedade Linearization (plnUnknown, plnNotLinearized, plnLinearized) e roteia de acordo: um arquivo linearizado abre pelo FPDFAvail_GetDocument assim que a seção de primeira página e as hint tables estão presentes, enquanto um arquivo não linearizado é aberto pelo FPDF_LoadCustomDocument no mesmo record de acesso a arquivo e tratado como legível apenas como um todo. O roteamento existe por uma razão concreta. Chamar o FPDFAvail_GetDocument num arquivo 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 alcança pdaAvailable e abre com a page tree completa enquanto o store de download esparso ainda não cobre o arquivo

Como o OpenProgressiveDocument roteia um download parcial no PDFium Component: um arquivo linearizado abre pelo FPDFAvail_GetDocument quando a seção de primeira página e as hint tables chegam, um arquivo não linearizado precisa do FPDF_LoadCustomDocument e de todo byte, e o LoadAvailablePage checa a availability do form com FPDFAvail_IsFormAvail antes da checagem de página, evitando a armadilha do handle de página zero não nulo
Só arquivos linearizados ganham vantagem inicial; em qualquer outra coisa o FPDFAvail_GetDocument pode devolver um documento de aparência aberta com zero páginas, que é exatamente o que o roteamento previne
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 1 e impõe a ordem que o PDFium espera: antes da primeira checagem de página ele roda 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 a torna a página ativa, então um viewer pode renderizar a página 1 de um folheto linearizado enquanto as páginas restantes ainda estão em trânsito; o FirstAvailablePageNumber diz qual página o dicionário de linearização designa como a primeira, já convertido do índice de base zero do PDFium

O que o CancelProgressiveLoad libera, e em que ordem?

O CancelProgressiveLoad desmonta uma sessão em quatro passos que não podem ser reordenados: cancelar o scheduler de ranges, fechar o documento, destruir o handle de availability com o FPDFAvail_Destroy, depois descartar os records de callback e liberar o adaptador de stream. Cancelar o scheduler primeiro incrementa o contador de geração dele, derruba toda requisição pendente e em voo, e dispara o OnCancelRequest para cada uma em voo, então uma conclusão do transporte que chegar depois carrega a geração antiga e o CompleteRequest devolve False sem tocar em nada. O documento precisa fechar antes do handle de availability e do adaptador irem embora porque o PDFium pode fazer callback no provider de acesso a arquivo enquanto fecha um documento, e se o adaptador já tiver ido embora esse callback lê memória liberada

A ordem fixa de teardown do CancelProgressiveLoad no PDFium Component: cancele o scheduler de ranges primeiro para conclusões tardias baterem no contador de geração incrementado e devolverem False, feche o documento antes do adaptador de acesso a arquivo sumir, destrua o handle de availability com FPDFAvail_Destroy, e só então descarte os records de callback e libere o adaptador de stream
Um método idempotente limpa igualmente um início falho, um cancel do usuário e o destrutor; com uma worker thread escrevendo no store, a propriedade do stream fica com você
procedure TDownloadForm.FormCreate(Sender: TObject);
begin
  FPdf := TPdfProgressiveDocument.Create(nil);
  // O scheduler vive tanto quanto FPdf, então ligue uma vez
  FPdf.RangeRequests.OnCancelRequest := RangeCancelled;
end;

procedure TDownloadForm.RangeCancelled(Sender: TObject; RequestId: UInt64;
  Attempt: Cardinal);
begin
  FTransport.Abort(RequestId);   // seu código: feche esse socket ou request
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 no meio da construção, um cancel explícito do usuário, e o destrutor. O BeginProgressiveLoad também o chama antes de começar, então reiniciar o mesmo objeto numa URL nova é seguro sem cancel explícito. Uma decisão de propriedade é sua de acertar: se uma worker thread escreve no stream de apoio, passe AOwnsStream = False e libere o stream você mesmo depois de a worker parar, porque com a propriedade entregue o cancel libera o stream enquanto uma escrita tardia ainda pode estar a caminho. Exceções levantadas dentro do OnCancelRequest são engolidas por requisição para que um transporte falhando não possa bloquear os cancelamentos restantes

Como a suíte de lifecycle prova que o caminho de cancel não vaza?

A suíte de stress de lifecycle do PDFium Component exercita um download interrompido estilo rede em todo ciclo misto. Cada ciclo inicia um progressive load cujo store guarda só um quarto dos bytes do fixture, exige pdaNotAvailable com uma lista de hints não vazia, chama o CancelProgressiveLoad, e dá assert que o objeto reporta nem ProgressiveLoading nem Active; depois roda o mesmo caminho de streaming até o fim com availability completa, OpenProgressiveDocument, um render e um close. A rodada mista padrão cobre 100 ciclos medidos com 600 opens, 2300 renders 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 separados dos cancelamentos de callback de render, porque um download abortado e um loop de render que para cedo são eventos diferentes com critérios de aceitação diferentes

Onde o caminho progressivo para de ajudar

Alguns limites valem a pena conhecer antes de construir um viewer em cima disso. Features que precisam dos bytes originais do arquivo recusam uma fonte progressiva incompleta em vez de chutar: o ReadXmpPacket falha explicitamente e a validação de assinatura reporta Indeterminate até o arquivo inteiro estar presente. O teste de availability padrão assume um prefixo contíguo, então um transporte que busca ranges fora de ordem precisa completá-los pelo RangeRequests ou responder pelo OnDataAvailable, ou o PDFium vai continuar pedindo bytes que você já tem. Um arquivo não linearizado não ganha nada no tempo até a primeira página, então se o primeiro paint rápido importa, linearize o arquivo do lado do servidor. E o CancelProgressiveLoad não fecha seus sockets sozinho; o OnCancelRequest é o hook onde isso acontece

Para o caminho simples de adaptador de stream que carrega um arquivo local completo sob demanda, veja fazer streaming de PDFs grandes sob demanda com PDFium; para abrir um PDF que está dentro de um buffer maior, veja carregamento por byte range para PDFs embutidos. Cancelar um render lento de uma página já carregada é um mecanismo separado, coberto em renderização progressiva de página cancelável. O TPdfProgressiveDocument e o scheduler de ranges dele vão com o PDFium Component para Delphi e C++Builder