Artigo Técnico

Carregamento por Intervalo de Bytes de PDF Embutido no PDFium

O PDFium Component consegue abrir um PDF que vive dentro de um buffer maior diretamente a partir de um intervalo de bytes. A sobrecarga LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) endereça uma janela no lugar, então nenhum Copy preliminar é necessário. Em troca ela pede que você entenda uma regra: quando Buffered é False, o array de suporte é emprestado, não copiado

Este é um mecanismo diferente da abordagem orientada a callback descrita em streaming de PDFs grandes sob demanda com PDFium VCL, que entrega ao PDFium um leitor FPDF_FILEACCESS e o deixa puxar blocos do disco conforme precisa. Aquele é para documentos grandes demais para caber em RAM. Este é para documentos já em RAM, sentados em um offset conhecido dentro de outra coisa. Os dois são complementares, e a última seção explica qual situação pertence a qual

A cópia de 40 MB que ninguém pediu

O cenário aparece onde quer que PDFs viajem dentro de outros formatos. Um armazenamento de correio mantém corpos de mensagem e anexos em um único registro. Um contêiner de arquivo concatena um manifesto, algumas imagens e um PDF. Um protocolo de fio personalizado enquadra um documento atrás de um cabeçalho prefixado por comprimento. Em todo caso você acaba segurando um TBytes grande e sabendo que o PDF começa no byte 1.182.336 e dura 312 kilobytes

Antes de a sobrecarga de intervalo de bytes existir, a resposta idiomática era Copy(Data, Index, Count), que aloca um segundo array e faz memcpy da janela para dentro dele. Você então entrega essa fatia a LoadDocument com Buffered = True, que a copia novamente para o buffer privado do componente. Duas cópias dos mesmos bytes, uma delas puro cerimonial, e em uma varredura de caixa de correio grande repetida para cada mensagem. A sobrecarga de intervalo de bytes remove a primeira cópia incondicionalmente e a segunda opcionalmente

O que a sobrecarga de intervalo de bytes realmente faz

A sobrecarga é fina por design: ela valida, calcula um ponteiro, e delega para a forma de ponteiro de LoadDocument pela qual toda a família já passa. Index é baseado em zero, Count é um comprimento em bytes, e Buffered tem padrão True exatamente como nas outras sobrecargas. O LoadDocument(const Data: TBytes; Buffered: Boolean) de um único argumento agora é ele mesmo apenas uma chamada a esta com Index = 0 e Count = Length(Data), então há um único caminho de validação em vez de dois

Chamá-la se parece com o código que você já estava escrevendo, menos a fatia

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 que Index mais Count estoura a verificação de limites?

Porque Index e Count são ambos Integer, e a soma de dois valores Integer positivos grandes não é necessariamente um Integer positivo grande. Este é o núcleo técnico da sobrecarga, e é o único lugar onde uma verificação de aparência natural é um buraco de segurança de memória. A formulação óbvia está errada

// 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');

Trabalhe o caso de falha. Pegue Index = 2000000000 e Count = 2000000000. Sua soma verdadeira é quatro bilhões, mas em aritmética assinada de 32 bits o resultado dá a volta para exatamente menos 294.967.296. Esse valor é confortavelmente menor que Length(Data), então a verificação errada passa, @Data[Index] é tomado bem fora do array, e o PDFium recebe um ponteiro selvagem mais um comprimento de dois gigabytes. O que se segue é uma violação de acesso em um bom dia e análise silenciosa de memória de processo não relacionada em um dia ruim

A ordem correta corrige isso nunca somando. Negativos são rejeitados antes de qualquer coisa ser indexada, então @Data[Index] nunca pode ser tomado abaixo do array. Depois Index é limitado sozinho contra Length(Data), o que garante que Length(Data) - Index é um Integer não negativo. Só então Count é comparado contra esse restante. Todo valor intermediário permanece dentro do intervalo representável, então nenhuma configuração de build pode mudar o resultado. Não se deixe tentar a confiar na verificação de overflow {$Q+} como rede de segurança também: builds de release rotineiramente saem com isso desligado, e mesmo quando está ligado você converteu um bug de segurança de memória em um EIntOverflow escapando do meio de uma rotina de validação. O PDFium Component trata aritmética de comprimento não confiável da mesma forma que trata o resto da fronteira, uma disciplina coberta mais amplamente em endurecendo o ABI do PDFium VCL e segurança de memória em Delphi

Por que uma janela de comprimento zero precisa passar nil?

Porque @Data[Index] não é uma expressão legal para todo Index que a validação aceita. Index = Length(Data) com Count = 0 é uma janela vazia perfeitamente bem formada no final do buffer, e um TBytes vazio dá Index = 0 em um array que não tem nenhum elemento zero de forma alguma. Tomar o endereço em qualquer um dos casos indexa além do fim, ou desreferencia um array dinâmico nil. Então a sobrecarga se ramifica: Count = 0 produz um ponteiro nil, qualquer outra contagem produz @Data[Index]. O nil então flui para a sobrecarga de ponteiro, cuja própria proteção aceita um ponteiro nil quando o tamanho é zero, e o carregamento termina no erro comum "Cannot load PDF document" em vez de uma violação de acesso. Um chamador que calculou uma janela de zero bytes a partir de um contêiner malformado recebe um EPdfError limpo e capturável como qualquer outra entrada ruim

Emprestado ou copiado: o que Buffered decide

Buffered seleciona o contrato de propriedade, e é o único parâmetro aqui com consequências além da chamada. Com Buffered = True, o PDFium Component copia a janela selecionada, e só a janela, para seu buffer interno antes de carregar. O contêiner de 40 MB não é copiado; o PDF de 312 KB é. Assim que LoadDocument retorna você pode liberar, reutilizar ou sobrescrever o contêiner imediatamente, porque o componente não o referencia mais. Este é o padrão e a escolha certa para quase todo código

Buffered = False passa @Data[Index] diretamente para FPDF_LoadMemDocument64, e o PDFium mantém esse ponteiro pela vida do documento em vez de copiar os bytes. Isso torna o carregamento livre de alocação, e torna todo o TBytes de suporte um recurso emprestado. Ele deve permanecer vivo e imodificado até que UnloadDocument rode ou Active se torne False. Não a janela, o array inteiro: um array dinâmico tem contagem de referência como uma unidade, e deixar a última referência ir embora em qualquer lugar do seu código libera a memória que o PDFium ainda está lendo. Definir Length nele é igualmente fatal, porque uma realocação pode mover o bloco. Declare isso na sua própria documentação de API onde quer que você exponha tal carregamento, no mesmo espírito de qualquer outra fronteira emprestar-versus-possuir em código Pascal; o modo de falha é idêntico aos perigos de aliasing descritos em o vazamento de FillChar e string de resultado em Delphi, onde um buffer parece possuído e não é

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;

Quando a janela de intervalo de bytes é a ferramenta errada

Seja honesto sobre a fronteira. A sobrecarga de intervalo de bytes assume que o contêiner já está totalmente em memória, e Count é um Integer, então uma única janela não pode exceder dois gigabytes. Se o contêiner é um arquivo de 6 GB em disco, ou chega por um socket que você não pode rebobinar, esta sobrecarga não pode ajudar você e ler a coisa toda em TBytes só para endereçar uma janela dentro dela derrota o propósito. É precisamente ali que o caminho FPDF_FILEACCESS pertence, e o artigo de streaming sob demanda mostra como expor uma visão deslocada por offset de um arquivo como uma fonte de documento personalizada. Igualmente, se os bytes embutidos precisam de transformação antes de o PDFium os ver, descompressão, descriptografia, um passo de desempacotamento, então uma cópia real é inevitável e Buffered = True no array transformado é a resposta honesta. A janela de intervalo de bytes compensa em exatamente uma forma: bytes de PDF contíguos, não modificados, já residentes, em um offset conhecido

Se você está avaliando isso para um visualizador, um painel de pré-visualização ou um pipeline de ingestão em lote, a sobrecarga de intervalo de bytes e o carregador de streaming são duas das estratégias de carregamento que o PDFium Component fornece junto com carregamentos de arquivo, stream e ponteiro bruto. A superfície completa de API, licenciamento e suporte de versão Delphi e C++Builder estão documentados na página do produto PDFium Component