Um arquivo digitalizado pode atingir vários gigabytes num único PDF. Um visualizador que abra esse ficheiro normalmente deseja apresentar apenas uma página: talvez a tabela de conteúdos, ou a página para a qual o utilizador saltou a partir de um marcador (bookmark). Ler o ficheiro inteiro para a memória apenas para renderizar duas páginas é um desperdício em todas as vertentes: consome espaço de endereçamento, atrasa o utilizador com uma longa leitura inicial e, num processo Delphi de 32 bits, pode falhar imediatamente antes de apresentar uma única página. O PDFium foi concebido a pensar nisto. Ele consegue carregar um documento através de um callback que solicita as gamas de bytes específicas que necessita, quando necessita, nunca exigindo o ficheiro completo de uma só vez.
O componente expõe esse caminho através de um adaptador de fluxo (stream adapter). Pode fornecer-lhe qualquer TStream, e o PDFium recolhe blocos desse fluxo a pedido. O ficheiro pode residir no disco, num campo blob de base de dados, ou atrás de qualquer outro descendente do TStream, e nada disso é copiado previamente para a memória.
Como o PDFium solicita os bytes
A API C do PDFium carrega um documento a partir de um objeto fornecido pelo chamador e descrito pela estrutura FPDF_FILEACCESS. A estrutura tem três partes importantes: um campo de comprimento, um callback de leitura e um parâmetro opaco do utilizador. O ponto de entrada que a consome é FPDF_LoadCustomDocument. Uma vez na posse dessa estrutura, o PDFium analisa o trailer, localiza a tabela de referências cruzadas e, a partir daí, lê apenas o que uma determinada operação requer. A abertura do documento interage com a cauda do ficheiro e um punhado de objetos do catálogo. A renderização da página 400 lê os fluxos de conteúdo e recursos para essa página, e nada mais.
O adaptador de fluxo
O adaptador que liga um TStream do Delphi à estrutura FPDF_FILEACCESS é o TPdfStreamAdapter. O seu construtor recebe o fluxo e um sinalizador de propriedade (ownership flag), captura o comprimento do fluxo uma vez, preenche o registo FPDF_FILEACCESS e configura o callback de leitura. Quando o PDFium faz chamadas posteriores indicando um deslocamento (offset) e um tamanho, o adaptador posiciona o fluxo nesse deslocamento e copia precisamente essa gama para o buffer fornecido pelo PDFium.
// 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 liberta o fluxo. Ao passar False, o chamador retém o fluxo e deve mantê-lo ativo durante toda a vida útil do documento. Passando True, o adaptador assume o controlo, libertando o fluxo quando o documento é fechado. Em qualquer caso, o fluxo tem de sobreviver a todas as leituras que o PDFium venha a realizar, porque o PDFium retém o ponteiro de FPDF_FILEACCESS e invocará o callback a qualquer momento enquanto o documento estiver aberto, e não apenas durante o carregamento inicial.
Porque é que o callback é uma função estática
O callback de leitura que o PDFium armazena em m_GetBlock é um ponteiro simples de função C com a convenção de chamada cdecl. Um método Delphi não pode ser usado diretamente, porque os métodos contêm um argumento Self oculto que um chamador C desconhece e nunca fornecerá. Assim, o adaptador declara o callback como uma class function marcada como cdecl; static, compilando para uma função independente com a estrutura de frame C que o PDFium espera e sem Self implícito.
Trampoline do cdecl de volta para a instância
Isto resolve o problema da convenção de chamada, mas coloca uma segunda questão: sem Self, de que forma o callback acede ao fluxo específico de onde deve ler? A resposta é o parâmetro de utilizador opaco. Ao criar o registo, o adaptador armazena o seu próprio ponteiro de instância em m_Param. O PDFium devolve esse mesmo ponteiro como o primeiro argumento de cada callback. A função estática converte-o de volta para um TPdfStreamAdapter e envia a leitura para o fluxo dessa instância. Trata-se do trampolim padrão para partilhar o contexto do objeto através de uma fronteira C que não tem noção de 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 teto de 4 GiB e porque necessita de uma proteção
O campo de comprimento m_FileLen em FPDF_FILEACCESS é um valor de 32 bits sem sinal. O seu maior comprimento representável é de um byte a menos do que 4 GiB. Um TStream reporta o seu tamanho como um Int64, pelo que um fluxo pode descrever muito mais bytes do que o campo consegue conter. No momento em que o tamanho de um fluxo ultrapassa esse teto, deixa de haver uma forma correta de indicar ao PDFium qual o comprimento real do ficheiro.
A resposta errada seria atribuir o tamanho e deixar o valor dar a volta (wrap). Truncar um comprimento de 5 GiB para um campo de 32 bits produz um número pequeno e de aspeto plausível, e o PDFium analisará então o ficheiro acreditando que este termina a cerca de um gigabyte. O trailer e a tabela de referências cruzadas residem no fim real do ficheiro, muito além do comprimento truncado, de modo que a análise falha de uma forma que nada tem a ver com a causa real. Estaria a depurar um erro de referências cruzadas num ficheiro que é perfeitamente válido, sem qualquer pista de que um inteiro deu a volta duas camadas acima.
Em vez disso, o adaptador rejeita a entrada. O construtor compara o tamanho do fluxo com High(FPDF_DWORD) e gera a exceção EPdfError no instante em que o fluxo é demasiado grande para ser descrito. Um erro explícito e imediato aponta para o problema real no momento da construção. Uma truncagem silenciosa esconderia o problema por trás de um sintoma enganador que tentaria rastrear muito mais tarde. O limite de 4 GiB é uma restrição real deste caminho de carregamento, e a atitude correta é expô-la de forma audível e direta, em vez de a camuflar com aritmética que por acaso compila.
As falhas não podem atravessar a fronteira
Uma leitura pode falhar. O fluxo pode ser um objeto de rede com timeout, um handle de blob que foi fechado sem o seu conhecimento, ou um ficheiro que foi truncado após a abertura do documento. O contrato do PDFium para o callback de leitura é um valor de retorno: diferente de zero para sucesso, zero para falha. Trata-se de um frame C e não possui qualquer mecanismo para capturar ou propagar uma exceção Pascal.
Esta é a razão pela qual o trampolim envolve a procura (seek) e a leitura num bloco try/except que absorve a exceção e retorna zero. Se uma exceção do Delphi pudesse propagar-se para fora do callback, desenrolar-se-ia através dos frames de stack cdecl do PDFium, que nunca foram concebidos para serem desenrolados pelo mecanismo de exceções do Pascal. O resultado seria comportamento indefinido no melhor dos casos e um crash grave no pior, em plena análise do PDF e sem uma stack útil. Retornar zero mantém a falha dentro do contrato. O PDFium regista uma falha de leitura de bloco, aborta a operação de forma segura e o FPDF_LoadCustomDocument reporta que o documento não pôde ser carregado, que o componente expõe como um EPdfError no lado Pascal, ao qual pertence.
Abrir um documento desta forma
O método do componente que gere o caminho de streaming é o LoadCustomDocument, declarado como um método autónomo e não como outra sobrecarga de LoadDocument, para assegurar que a passagem de um TMemoryStream nunca caia acidentalmente no caminho com buffer. Ele constrói o adaptador, chama FPDF_LoadCustomDocument e mantém o adaptador ativo durante toda a vida útil 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 de blob de um dataset de base de dados, ou um descendente personalizado de TStream. O carregamento a pedido justifica o seu uso quando o ficheiro é grande e apenas uma parte será lida: um visualizador de arquivo, um gerador de miniaturas que amostra algumas páginas, ou um índice de pesquisa que extrai uma página de cada vez. Quando o ficheiro é pequeno ou se planeia ler a sua totalidade de qualquer forma, o carregamento com buffer é mais simples e o mecanismo de streaming não oferece vantagens. O fator decisivo é a proporção entre os bytes com os quais vai interagir e os bytes totais do ficheiro.
Depois de as páginas começarem a ser transmitidas a pedido, a preocupação seguinte é manter a capacidade de resposta das páginas renderizadas à medida que o utilizador faz zoom e scroll, o que é abordado na nossa nota sobre desempenho de cache de renderização e zoom. Quando o documento transmitido for do tipo que um visualizador deve apresentar mas não permitir ao utilizador exportar ou alterar, as técnicas descritas no guia de pré-visualização segura de PDF alinham-se naturalmente com este percurso de carregamento. Ambos assentam no carregamento por fluxo aqui descrito, disponibilizado como parte do PDFium Component para Delphi e C++Builder, a par das APIs de renderização, extração de texto e anotações abordadas noutras secções deste blogue.