Artigo Técnico

Carregamento por Intervalo de Bytes de PDF Incorporado em Delphi

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, pelo que não é necessário um Copy preliminar. Em troca, pede-lhe que entenda uma regra: quando Buffered é False, o array subjacente é emprestado, não copiado

Este é um mecanismo diferente da abordagem orientada por 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 à medida que precisa deles. Esse é para documentos demasiado grandes para caber em RAM. Este é para documentos já em RAM, sentados num deslocamento conhecido dentro de outra coisa. Os dois são complementares, e a última secção explica que situação pertence a que

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

O cenário aparece onde quer que os PDFs viajem dentro de outros formatos. Um repositório de correio mantém corpos de mensagem e anexos num único registo. Um contentor de arquivo concatena um manifesto, algumas imagens e um PDF. Um protocolo de rede personalizado enquadra um documento atrás de um cabeçalho com prefixo de comprimento. Em todos os casos, acaba por manter um único TBytes grande e saber que o PDF começa no byte 1.182.336 e dura 312 quilobytes

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 lá. Depois entrega essa fatia a LoadDocument com Buffered = True, que a copia de novo para o buffer privado do componente. Duas cópias dos mesmos bytes, uma delas puro cerimonial, e repetida em cada mensagem numa análise de caixa de correio grande. A sobrecarga de intervalo de bytes remove a primeira cópia incondicionalmente e a segunda opcionalmente

Diagrama de comparação mostrando o antigo caminho de cópia dupla para um PDF incorporado dentro de um contentor em Delphi versus a sobrecarga LoadDocument por intervalo de bytes do PDFium Component que endereça a janela no local
Sem o overload, a mesma janela de 312 KB foi copiada por memcpy duas vezes; a chamada byte-range endereça-a no lugar, e Buffered decide se algo é sequer copiado

O que faz realmente a sobrecarga de intervalo de bytes

A sobrecarga é fina por conceção: 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 por predefinição True exatamente como nas outras sobrecargas. O único argumento LoadDocument(const Data: TBytes; Buffered: Boolean) é agora ele próprio apenas uma chamada a esta com Index = 0 e Count = Length(Data), pelo que há um único percurso de validação em vez de dois

Chamá-lo parece o código que já estava a escrever, menos a fatia

var
  Frame: TBytes;          // registo do contentor completo, dezenas de megabytes
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // o seu analisador do contentor

  // Sem Copy(Frame, Offset, Size) aqui - a janela é endereçada no lugar
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

Por que faz Index mais Count transbordar 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 local onde uma verificação de aspeto natural é uma falha de segurança de memória. A formulação óbvia está errada

// ERRADO: Index + Count é avaliado em Integer e pode dar a volta para negativo
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// CERTO: rejeite os sinais primeiro, depois limite cada termo separadamente,
// com a única aritmética feita como uma subtração que não pode dar a volta
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 até ao fim. Tome Index = 2000000000 e Count = 2000000000. A sua soma verdadeira é quatro mil milhões, mas em aritmética assinada de 32 bits o resultado dá a volta para exatamente menos 294.967.296. Esse valor é confortavelmente menor do que Length(Data), pelo que 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 num bom dia e análise silenciosa de memória de processo não relacionada num mau dia

A ordem correta corrige isto nunca somando. Os negativos são rejeitados antes de qualquer indexação, pelo que @Data[Index] nunca pode ser tomado abaixo do array. Depois Index é limitado por si só contra Length(Data), o que garante que Length(Data) - Index é um Integer não negativo. Só então Count é comparado contra esse remanescente. Cada valor intermédio permanece dentro do intervalo representável, pelo que nenhuma configuração de build pode alterar o resultado. Não seja tentado a confiar na verificação de overflow {$Q+} como rede de segurança: os builds de release rotineiramente enviam-na desligada, e mesmo quando está ligada converteu um bug de segurança de memória num EIntOverflow que escapa do meio de uma rotina de validação. O PDFium Component trata a aritmética de comprimento não fiável da mesma forma que trata o resto da fronteira, uma disciplina coberta mais amplamente em reforçar o ABI PDFium VCL e a segurança de memória em Delphi

Diagrama de fluxo contrastando a verificação de limites com overflow de Index mais Count com a ordem de validação só por subtração que mantém o carregamento por intervalo de bytes do PDFium seguro em memória em Delphi
Adicionar primeiro transborda para além de MaxInt e derrota a proteção; rejeitar sinais e limitar termos antes de uma única subtração sem transbordo mantém cada intermediário representável

Por que tem uma janela de comprimento zero de passar nil?

Porque @Data[Index] não é uma expressão legal para todo o 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 num array que não tem elemento zero de todo. Tomar o endereço em qualquer dos casos indexa além do final, ou desreferencia um array dinâmico nil. Então a sobrecarga ramifica-se: Count = 0 produz um ponteiro nil, qualquer outra contagem produz @Data[Index]. O nil flui então 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 contentor malformado obtém um EPdfError limpo e detetável como qualquer outra entrada errada

Emprestado ou copiado: o que decide Buffered

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

Buffered = False passa @Data[Index] diretamente para FPDF_LoadMemDocument64, e o PDFium mantém esse ponteiro durante a vida do documento em vez de copiar os bytes. Isso torna o carregamento livre de alocação, e torna todo o TBytes subjacente um recurso emprestado. Tem de permanecer vivo e inalterado até UnloadDocument correr ou Active passar a False. Não a janela, o array inteiro: um array dinâmico é referenciado por contagem como uma unidade, e deixar a última referência ir seja onde for no seu código liberta a memória que o PDFium ainda está a ler. Definir Length nele é igualmente fatal, porque uma realocação pode mover o bloco. Declare isto na sua própria documentação de API onde quer que 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 riscos de aliasing descritos em a fuga de FillChar e string de resultado em Delphi, onde um buffer parece possuído e não é

type
  TFrameSession = class
  private
    FFrame: TBytes;   // detém o armazenamento de suporte durante o tempo em que o FPdf estiver carregado
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: o FFrame tem de sobreviver ao documento carregado
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // liberta primeiro o empréstimo
  FFrame := nil;         // só agora o armazenamento pode desaparecer
  inherited;
end;

Quando a janela de intervalo de bytes é a ferramenta errada

Seja honesto quanto à fronteira. A sobrecarga de intervalo de bytes assume que o contentor já está inteiramente em memória, e Count é um Integer, pelo que uma única janela não pode exceder dois gigabytes. Se o contentor for um arquivo de 6 GB em disco, ou chegar através de um socket que não pode rebobinar, esta sobrecarga não o consegue ajudar e ler tudo para TBytes só para endereçar uma janela lá dentro derrota o objetivo. É precisamente aí que pertence o percurso FPDF_FILEACCESS, e o artigo sobre streaming sob demanda mostra como expor uma vista de ficheiro deslocada como uma fonte de documento personalizada. Igualmente, se os bytes incorporados precisarem de transformação antes de o PDFium os ver, descompressão, desencriptação, um passo de desembrulhar, então uma cópia real é inevitável e Buffered = True sobre o array transformado é a resposta honesta. A janela de intervalo de bytes compensa exatamente numa forma: bytes de PDF contíguos, não modificados, já residentes, num deslocamento conhecido

Se estiver a avaliar isto para um visualizador, um painel de pré-visualização ou um pipeline de ingestão por lotes, a sobrecarga de intervalo de bytes e o carregador de streaming são duas das estratégias de carregamento que o PDFium Component disponibiliza ao lado de carregamentos de ficheiro, stream e ponteiro em bruto. A superfície completa da API, o licenciamento e o suporte de versões Delphi e C++Builder estão documentados na página do produto PDFium Component

Diagrama de cronologia dos dois modos Buffered ao carregar um PDF incorporado a partir de um intervalo de bytes em Delphi: copiar apenas a janela do PDF e libertar o contentor imediatamente versus pedir emprestado todo o TBytes de suporte até o UnloadDocument correr
Buffered = True copia apenas a janela, para que o contentor é descartável, enquanto Buffered = False deixa todo o array de suporte emprestado até ao descarregamento