Technical Article

Juntar e Dividir PDFs de Gigabytes em Delphi com Acesso Direto do PDFlibPas

Juntar ou dividir um PDF com dois gigabytes de forma convencional custa-lhe duas coisas ao mesmo tempo: tempo de relógio e espaço de endereçamento. A forma convencional consiste em carregar cada entrada, executar o trabalho e escrever a saída. O processo falha no carregamento. Um arquivo de digitalizações que passe de 300 para 600 DPI duplica a sua resolução linear e quadruplica no disco; assim, a mesma tarefa de montagem que processava ficheiros de 400 MB ao longo do ano começa a falhar no momento em que uma entrada ultrapassa um gigabyte, muitas vezes apenas ao contar páginas. A tarefa em si não se tornou mais difícil. Abrir, contar, selecionar intervalos e concatenar é tudo o que é necessário. O carregamento de toda a árvore de objetos deixou simplesmente de ser um padrão viável para esse volume de dados. O PDFlibPas, a biblioteca PDF da losLab para Delphi e C++Builder, resolve este obstáculo com a sua camada de Acesso Direto (Direct Access): uma família de funções com o prefixo DA apoiada por um leitor de fluxo que percorre a tabela de referências cruzadas (xref) localmente em vez de construir todo o documento na memória.

Para onde vai a memória num carregamento completo

Carregar um PDF "normalmente" significa analisar a tabela xref, resolver cada objeto indireto numa árvore em memória, descodificar os fluxos de objetos e interligar a árvore de páginas, tipos de letra e anotações em objetos manipuláveis. Para fluxos de edição, este é o compromisso correto. Para tarefas de junção, divisão e inspeção, é sobretudo desperdício. Um arquivo digitalizado de 30.000 páginas pode conter milhões de objetos indiretos, enquanto uma tarefa de divisão só precisa de ler algumas centenas deles: os nós de página no intervalo pretendido e o que esses nós referenciam.

A camada de Acesso Direto inverte este modelo. As funções DAOpenFile e DAOpenFileReadOnly analisam o trailer e a tabela xref (alguns kilobytes no final do ficheiro) e devolvem um handle. Os objetos são obtidos a pedido (lazy fetching) quando uma chamada deles necessita. A consequência prática é que abrir um ficheiro de vários gigabytes demora sensivelmente o mesmo que abrir um ficheiro pequeno, e o consumo de memória acompanha o que é acedido em vez do que o ficheiro contém.

Analisar um ficheiro enorme sem o carregar

O padrão abaixo provém do teste de desempenho de ficheiros grandes da própria biblioteca: abrir apenas para leitura, efetuar consultas e fechar. Nunca chega a existir uma árvore de documento:

var
  Lib: TPDFlib;
  Handle, Pages: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
    if Handle = 0 then
      raise Exception.Create('Direct access open failed');
    Pages := Lib.DAGetPageCount(Handle);
    Writeln('pages : ', Pages);
    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
    Lib.DACloseFile(Handle);
  finally
    Lib.Free;
  end;
end;

O modo apenas de leitura deve ser preferido sempre que possível: permite que a fase de triagem decorra enquanto outros processos acedem ao ficheiro e documenta a intenção do código. Uma fase de verificação que chame acidentalmente uma função de modificação falhará de imediato em vez de corromper o arquivo.

PageRef é um handle de objeto, não um número de página

O erro mais comum com a API DA consiste em passar o número da página onde a função espera um PageRef. Quase todas as chamadas DA por página requerem um handle de referência para o objeto da página e não um número: DAExtractPageText, DARenderPageToFile, DARotatePage e DACapturePage esperam uma referência. Obtém uma traduzindo o número visível para o utilizador através de DAFindPage:

PageRef := Lib.DAFindPage(Handle, 250);          // page number -> object handle
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Passar o número bruto 250 em sua substituição não gera erro. Irá referenciar o objeto que calhar a estar associado a esse valor de handle, o que num caso favorável falhará visivelmente e num caso desfavorável extrairá o texto da página errada para um documento enviado ao cliente. Se encapsular a camada DA no seu próprio código de serviço, torne essa tradução obrigatória: aceite números de página na interface externa, chame DAFindPage de imediato e utilize apenas referências internamente.

Juntar centenas de ficheiros com uma lista nomeada

Para dois ficheiros, o método MergeFiles(First, Second, Output) é suficiente. A montagem em lote escala melhor através de listas de ficheiros: registe as entradas sob um nome de lista e junte a lista numa única passagem.

Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Verifique o resultado de forma simples: utilizando acesso direto novamente
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

A família de junções possui três variantes e a diferença não reside apenas na velocidade. A função MergeFileListFast ignora a preservação da árvore de estrutura; MergeFileListStrict impõe o modo estrito; a versão sem sufixo é o padrão equilibrado. A regra prática que daí decorre: se alguma das entradas for um PDF Etiquetado (Tagged PDF) cuja estrutura de acessibilidade deva ser preservada, o caso óbvio de documentos produtos para PDF/UA, opte pela variante padrão ou pela Strict, dado que a versão Fast descarta a árvore de estrutura silenciosamente. Para arquivos de digitalizações simples sem etiquetas de acessibilidade, o modo Fast constitui desempenho gratuito. Decida de acordo com o fluxo de processamento e não por preferência momentânea, registando a variante utilizada no diário de tarefas.

Dividir sem carregar: extração de intervalos

A divisão adota a mesma filosofia de ausência de carregamento completo. O método ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) extrai um intervalo de páginas diretamente de ficheiro para ficheiro, através de uma lista de intervalos como '1-500', '501-1000' ou seleções separadas por vírgula, sem que a origem seja convertida numa árvore de documento. Quando um documento já estiver carregado por outros motivos, ExtractPageRanges cria um novo documento em memória a partir do atual, e CopyPageRanges copia intervalos a partir de outro documento carregado identificando o seu ID. Para a divisão por extrato em fluxos de impressão consolidados, a forma de ficheiro para ficheiro evita que uma entrada de 4 GB seja carregada para a memória RAM.

Ficheiros que mentem sobre a sua geometria

Os fluxos de processamento de ficheiros grandes deparam com ficheiros danificados a um ritmo que os fluxos de ficheiros pequenos nunca experienciam, simplesmente porque as entradas passam por mais sistemas intermediários. Duas tipologias de falha requerem tratamento explícito.

Primeiro, cabeçalhos deslocados. Os servidores de correio e os spoolers de impressão por vezes acrescentam bytes no início de um PDF, fazendo com que o marcador %PDF deixe de estar no desvio 0 e todos os desvios da xref no ficheiro fiquem incorretos pelo mesmo valor. O leitor de fluxo deteta esta situação e expõe-na (DAShiftedHeader no nível plano, ShiftedHeader no TSmartPDFReader), compensando a diferença durante as leituras. A aritmética de desvios desenvolvida internamente costuma falhar nestes casos, o que explica o sintoma clássico: "funciona com todos os ficheiros que geramos, mas falha com os ficheiros enviados pelo cliente X".

Segundo, tabelas de referências cruzadas corrompidas. A função DACopyFile(InputFileName, OutputFileName, PageCount) transfere todo o ficheiro para uma nova cópia enquanto reconstrói a xref, devolvendo a contagem de páginas como subproduto. A sua execução como etapa de normalização antes de um consumidor final exigente converte uma classe de falhas de análise intermitentes num passo de reparação previsível. E quando as suas edições precisarem de ser gravadas, DAAppendFile escreve-as como uma atualização incremental, anexando uma nova revisão em vez de reescrever gigabytes, o que mantém o custo de gravação proporcional à alteração e não ao tamanho do ficheiro.

Detalhes de entrega: linearização e composição

Duas capacidades complementares completam um fluxo de processamento de ficheiros grandes. Quando a saída montada é disponibilizada via HTTP para visualização no navegador, o método LinearizeFile reorganiza o ficheiro para streaming por intervalo de bytes, permitindo que a primeira página seja apresentada antes de terminar a transferência de um pacote de 500 MB. Execute esta operação como etapa final, após todas as junções, porque qualquer modificação posterior anula a linearização do ficheiro. Além disso, quando os pacotes necessitam de composição em vez de simples concatenação, como uma folha de rosto sobreposta a cada extrato ou a imposição de duas páginas de origem numa folha de saída, DACapturePage converte qualquer página num modelo reutilizável que DADrawCapturedPage insere na página de destino num retângulo arbitrário, ainda sem carregar todo o documento de origem de vários gigabytes.

Limites e o que se mantém apenas de leitura

As limitações do próprio formato surgem muito antes das da camada de Acesso Direto. Os desvios utilizam o tipo Int64 em toda a camada DA, pelo que os limites reais são o espaço disponível em disco e o campo de desvio da xref de 10 dígitos nas tabelas de referências cruzadas clássicas (sem fluxos). Arquivos digitalizados de vários gigabytes são comuns na prática, e o consumo de memória mantém-se limitado independentemente do tamanho do ficheiro, porque os objetos são lidos apenas quando uma chamada o exige.

Existem duas questões colocadas com frequência que convém responder diretamente. A junção através do caminho padrão transporta a estrutura do documento, pelo que os marcadores e as ligações internas sobrevivem; a variante Fast é a que prescinde da árvore de estrutura em prol da velocidade, razão pela qual deve ser reservada para entradas sem etiquetas de acessibilidade. O procedimento seguro consiste em abrir o ficheiro resultante, percorrer o seu índice (outline) e verificar algumas ligações internas antes do envio. Quanto à edição: existe um compromisso útil entre a verificação apenas de leitura e o carregamento completo. Operações ao nível da página atuam diretamente no handle, entre as quais DARotatePage, DAMovePage e DAHidePage, em conjunto com leituras de campos de formulário, e a função DAAppendFile grava essas edições como uma revisão incremental. A edição ao nível do conteúdo, ou seja, tudo o que altere os operadores gráficos de uma página, continua a pertencer à camada do documento completo.

Artigos relacionados

Caso o documento gerado necessite de manter os requisitos de acessibilidade, o enquadramento sobre a árvore de estrutura é abordado no artigo sobre a acessibilidade de PDFs Etiquetados, que detalha exatamente o que a variante Fast descartaria. Para extrair conteúdo dos intervalos que dividiu, consulte o guia de extração de texto, imagens e tipos de letra.

A lista completa de funções de Acesso Direto acompanha a biblioteca; as edições e as versões de teste estão disponíveis na página do produto PDFlibPas.