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