Um arquivo escaneado pode chegar a vários gigabytes num único PDF. Um visualizador que abre um arquivo desse tipo normalmente quer mostrar apenas uma página — talvez o índice, ou uma página para a qual o usuário saltou através de um marcador. Ler o arquivo inteiro para a memória a fim de renderizar duas páginas é um desperdício em todos os eixos: consome espaço de endereçamento, retém o usuário devido a uma longa leitura inicial, e, em um processo Delphi de 32 bits, pode falhar de imediato antes mesmo de exibir uma única página. O PDFium foi construído com isso em mente. Ele pode carregar um documento por meio de um callback que solicita os intervalos de bytes específicos de que precisa, exatamente quando precisa deles, e ele nunca exige o arquivo inteiro de uma só vez. Um limite, contudo, aparece logo de cara: este canal de streaming descreve o arquivo com um tamanho de 32 bits, de modo que atende a um único arquivo de até 4 GiB, o que abrange quase todos os arquivos escaneados na prática. Um arquivo que ultrapassa essa linha não é território deste artigo; ele precisa ser dividido em volumes no momento da digitalização, ou, em vez disso, aberto usando uma estratégia de acesso direto, e a proteção (guard) que impõe esse teto honestamente recebe uma seção própria logo abaixo
O componente expõe esse caminho através de um adaptador de fluxo (stream adapter). Você lhe entrega qualquer TStream, e o PDFium extrai os blocos desse fluxo sob demanda. O arquivo pode residir no disco, num campo blob de um banco de dados, ou por trás de qualquer outro descendente de TStream, e nada dele é copiado para a memória antecipadamente
Como o PDFium pede por bytes
A API C do PDFium carrega um documento a partir de um objeto fornecido pelo chamador, descrito pela estrutura FPDF_FILEACCESS. Essa estrutura possui três partes que importam aqui: um campo de tamanho (length), um callback de leitura e um parâmetro de usuário opaco. O ponto de entrada que a consome é FPDF_LoadCustomDocument. Assim que o PDFium detém essa estrutura, ele analisa o trailer, localiza a tabela de referência cruzada e, a partir de então, lê apenas o que uma determinada operação requer. Abrir o documento toca o final (tail) do arquivo e um punhado de objetos de catálogo. A renderização da página 400 lê os fluxos de conteúdo e recursos para essa página, e nada mais
Essa é a diferença entre um carregamento em buffer e um carregamento em streaming. O carregamento em buffer lê o arquivo de ponta a ponta antes mesmo de o PDFium ver o byte zero. Um carregamento em streaming inverte a relação: o PDFium conduz as leituras, e os bytes que nunca são tocados nunca são lidos. Para um arquivo de vários gigabytes visualizado página por página, isso constitui a diferença entre um carregamento inutilizável e um instantâneo
O adaptador de stream
O adaptador que interliga um TStream do Delphi à estrutura FPDF_FILEACCESS é o TPdfStreamAdapter. Seu construtor aceita o fluxo (stream) e um sinalizador de propriedade, captura o comprimento do fluxo uma vez, preenche o registro FPDF_FILEACCESS e conecta o callback de leitura. Quando o PDFium posteriormente invocar esse callback com um deslocamento (offset) e um tamanho, o adaptador buscará (seek) no fluxo para aquele deslocamento e copiará exatamente esse intervalo para o buffer que o PDFium forneceu
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
O sinalizador de propriedade decide quem libera (free) o fluxo. Passe False e o chamador ficará com o fluxo e deverá mantê-lo vivo durante toda a vida útil do documento. Passe True e o adaptador assumirá o controle, liberando o fluxo quando o documento fechar. De qualquer modo, o fluxo deve sobreviver a todas as leituras que o PDFium fará, pois o PDFium retém o ponteiro FPDF_FILEACCESS e chamará de volta em qualquer ponto enquanto o documento estiver aberto, não apenas durante o carregamento inicial
Por que o callback é uma função estática
O callback de leitura que o PDFium armazena em m_GetBlock é um ponteiro de função C simples com a convenção de chamada cdecl. Um método Delphi não pode ser usado diretamente, porque ele carrega um argumento oculto Self sobre o qual um chamador em linguagem C não sabe nada e que jamais irá prover. Consequentemente, o adaptador declara o callback como uma class function marcada como cdecl; static, o qual é compilado para uma função independente com o leiaute de quadro C (C frame) que o PDFium espera, sem nenhum Self implícito
Isso resolve a convenção de chamadas, mas levanta uma segunda questão: sem o Self, como é que o callback atinge o fluxo específico a partir do qual ele deve ler? A resposta é o parâmetro de usuário opaco. Quando o adaptador constrói o registro, ele armazena o próprio ponteiro de instância em m_Param. O PDFium devolve esse mesmo ponteiro como o primeiro argumento de todo callback. A função estática converte (cast) esse ponteiro de volta para um TPdfStreamAdapter e despacha a leitura contra o fluxo dessa instância. Este é o trampolim (trampoline) padrão para passar contexto de objeto através de um limite C que não tem a menor noção do que sejam objetos
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
O limite de 4 GiB e por que precisa de uma proteção
É daqui que vem o limite mencionado na abertura. O campo de comprimento m_FileLen em FPDF_FILEACCESS é um valor não assinado de 32 bits (unsigned). Seu maior comprimento representável equivale a 4 GiB menos um byte. Um TStream relata seu tamanho como um Int64, assim um fluxo pode descrever muito mais bytes do que o campo consegue armazenar. A partir do momento em que o tamanho do fluxo ultrapassa esse teto, não há mais nenhuma forma honesta de informar ao PDFium quão longo é o arquivo
A resposta errada seria atribuir o tamanho e deixar que ele sofra uma quebra (wrap). Truncar um comprimento de 5 GiB num campo de 32 bits produz um número pequeno e aparentemente plausível; então, o PDFium fará o parse do arquivo acreditando que ele termine em aproximadamente um gigabyte. O trailer e a tabela de referência cruzada vivem no verdadeiro final do arquivo, muito além do comprimento truncado, e por isso a análise falha de um modo que nada tem a ver com a causa real. Você estaria depurando um erro de referência cruzada em um arquivo que é perfeitamente válido, sem nenhuma pista de que houve um estouro no número inteiro, duas camadas acima
Em vez disso, o adaptador recusa a entrada. O construtor compara o tamanho do fluxo com High(FPDF_DWORD) e lança um EPdfError no momento em que constata que o fluxo é grande demais para ser descrito. Um erro explícito e imediato nomeia o problema real no ponto de construção. Um truncamento silencioso o esconde por trás de um sintoma enganoso que você investigaria muito depois. O limite de 4 GiB é uma restrição genuína desse caminho de carregamento, e o correto a se fazer é destacá-lo de forma veemente, em vez de mascará-lo por meio de uma aritmética que por acaso compila. Se um arquivo realmente cruzar esse limite, as soluções prometidas logo no início do texto residem fora desta API: dividir a digitalização em arquivos por volume, nos quais cada um se mantém abaixo do teto, ou manter o documento no disco e servi-lo por meio de uma arquitetura de acesso direto baseada em offsets de 64 bits em vez de ser através de FPDF_FILEACCESS
Falhas não devem cruzar a fronteira
Uma leitura pode falhar. O fluxo pode ser um objeto fundamentado na rede cujo tempo limite se esgote (timeout), um manipulador blob que tenha sido fechado pelas suas costas, ou um arquivo truncado após a abertura do documento. O contrato do PDFium para o callback de leitura é um valor de retorno: não-zero para sucesso, zero para falha. Trata-se de uma estrutura da linguagem C (C frame) e ela não possui qualquer mecanismo (machinery) para capturar nem propagar uma exceção Pascal
É por isso que o trampolim envolve a busca (seek) e a leitura em um bloco try/except que engole a exceção e retorna zero. Se uma exceção Delphi tivesse permissão para se propagar a partir do callback, ela desfaria a pilha de chamadas (unwind) pelos quadros de pilha cdecl do PDFium, os quais nunca foram concebidos para serem desfeitos pelo mecanismo de exceções do Pascal. O resultado seria um comportamento indefinido na melhor das hipóteses e um colapso drástico (hard crash) na pior delas, lá no fundo do analisador sintático (parser) do PDF e sem nenhuma pilha de chamadas que se pudesse utilizar. Retornar zero conserva a falha dentro do contrato. O PDFium constata que ocorreu uma falha na leitura do bloco, cancela (aborts) a operação de forma elegante e FPDF_LoadCustomDocument relata que o documento não pôde ser carregado — o que o componente expõe como um EPdfError do lado Pascal, onde ele tem que estar
Abrindo um documento dessa maneira
O método do componente que orienta a via do streaming é LoadCustomDocument, declarado como um método distinto e não como uma outra sobrecarga de LoadDocument, de modo que passar um TMemoryStream jamais o fará recair acidentalmente sobre o caminho de dados em buffer (buffered path). Ele constrói o adaptador, chama FPDF_LoadCustomDocument, e mantém o adaptador em vida por todo o tempo de vida do documento carregado
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
A mesma chamada funciona para um TMemoryStream, um fluxo blob (blob stream) vindo do dataset de um banco de dados, ou um descendente personalizado de TStream. O carregamento sob demanda se justifica quando o arquivo é imenso e apenas uma parte dele será lida: seja um visualizador de arquivos, um gerador de miniaturas (thumbnails) que extrai algumas poucas páginas como amostra, seja um indexador de buscas (search index) que puxa uma única página por vez. Quando o arquivo é pequeno ou você vai lê-lo na totalidade de um jeito ou de outro, um carregamento em buffer é muito mais fácil e a engrenagem do streaming não lhe acrescenta nenhum benefício. O que determina (the deciding factor) é a proporção (ratio) entre a quantidade de bytes a serem efetivamente manuseados e a quantidade total de bytes existentes naquele arquivo
Uma vez que as páginas chegam (stream in) sob demanda, a próxima preocupação é manter as páginas renderizadas responsivas conforme o usuário aplica zoom e rola a tela, assunto este que é abordado em nossa nota sobre cache de renderização e desempenho do zoom. No caso de o documento transmitido ser algum que um visualizador deva mostrar mas sem permitir que o usuário o exporte ou altere, as técnicas descritas em o passo a passo de visualização segura de PDF combinam perfeitamente (pair naturally) com esse caminho de carregamento. Ambas são fundamentadas no carregamento por streaming descrito aqui, o qual é enviado como parte do Componente PDFium para Delphi e C++Builder junto com as APIs de renderização, extração de texto e de anotação ensinadas em outros lugares deste blog