O PDFium VCL expõe a fusão de PDFs através de um único método: o ImportPages. O padrão é sempre o mesmo: criar um documento de destino vazio, abrir cada ficheiro de origem, chamar o ImportPages para copiar as páginas, fechar a origem e repetir. Quando o ciclo termina, o SaveAs grava o resultado no disco. Não existe um modo especial de fusão, nem configurações adicionais a alterar. A complexidade reside nos casos limite, e existem alguns que requerem atenção redobrada.
O ciclo principal
Apenas necessita de duas instâncias do TPdf. Uma armazena o documento de destino, criado vazio com o CreateDocument. A outra abre cada ficheiro de origem sequencialmente. Abaixo apresenta-se um procedimento que recebe uma lista de caminhos de ficheiros e grava o resultado fundido num único caminho:
procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
PdfDest, PdfSrc: TPdf;
InsertAt, I: Integer;
begin
PdfDest := TPdf.Create(nil);
PdfSrc := TPdf.Create(nil);
try
PdfDest.CreateDocument;
InsertAt := 1; // ImportPages uses 1-based destination position
for I := 0 to FileList.Count - 1 do
begin
PdfSrc.FileName := FileList[I];
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.CreateFmt('Cannot open: %s', [FileList[I]]);
PdfDest.ImportPages(
PdfSrc,
'1-' + IntToStr(PdfSrc.PageCount), // full document range
InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
end;
PdfDest.SaveAs(OutputPath);
finally
PdfSrc.Free;
PdfDest.Free;
end;
end;
Dois aspetos nesse código são fáceis de ignorar numa primeira leitura. O primeiro é a forma como o PDFium reporta falhas de carregamento. A atribuição Active := True nunca gera exceções: se o ficheiro estiver em falta, danificado ou protegido por palavra-passe, o PDFium captura o erro internamente e mantém o Active como False. Sem a verificação explícita na linha 20, um ficheiro incorreto seria ignorado silenciosamente na fusão sem qualquer indicação no ficheiro final. O PDF resultante conteria menos páginas do que o esperado e não saberia qual o ficheiro de origem que falhou.
O segundo é o contador InsertAt. O terceiro argumento do ImportPages é a posição (baseada em 1) no documento de destino onde a primeira página importada será colocada. Começar em 1 coloca o primeiro documento de origem no início de um ficheiro que estaria de outra forma vazio. Após cada ficheiro de origem, o contador avança de acordo com o PdfSrc.PageCount, garantindo que o lote seguinte de páginas é anexado após o anterior. Se se esquecer de incrementar este contador, cada documento de origem subsequente substituirá as páginas na posição 1, resultando num ficheiro final que contém apenas o último documento da lista.
Intervalos de páginas seletivos
Não necessita de importar todas as páginas de um ficheiro de origem. A string de intervalo passada como segundo argumento segue um formato simples de vírgulas e hífens: "1-3" copia as páginas 1 a 3, "2,4,6" escolhe três páginas específicas e "1-" representa da página 1 até ao final do documento. Os intervalos podem ser combinados numa única string, de modo que "1-3,5,7-" ignora as páginas 4 e 6. Um detalhe importante: os números referem-se sempre às páginas no documento de origem, começando em 1, independentemente de onde essas páginas venham a ficar posicionadas no destino. Se pretender as páginas 40 a 50 de um catálogo com 200 páginas, a string de intervalo a definir é "40-50", e não uma posição relativa ao que já está inserido no destino.
// Extract cover plus a three-page executive summary from a long report
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active := True;
if PdfSrc.Active then
begin
// Page 1 is the cover; pages 3-5 are the summary
PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
Inc(InsertAt, 4); // 1 cover + 3 summary pages = 4 pages added
PdfSrc.Active := False;
end;
Ao calcular o incremento para InsertAt, conte as páginas que realmente importou, e não o total de páginas da origem. Se passar '1,3-5', importou 4 páginas, pelo que deve avançar 4 posições. Avançar de acordo com o PdfSrc.PageCount criaria posições em branco no destino e colocaria o documento seguinte num ponto do ficheiro mais à frente do que o pretendido.
O que o ImportPages preserva e o que não preserva
As páginas copiadas pelo ImportPages mantêm o seu conteúdo visível intacto. Texto, gráficos vetoriais, imagens rasterizadas, fontes incorporadas e objetos de formulário XObject são transferidos como parte dos fluxos de conteúdo das páginas. As anotações ao nível da página (incluindo comentários, destaques e traços de tinta) também são importadas, pois estão armazenadas no dicionário da própria página e não ao nível do documento.
Os metadados ao nível do documento são um caso diferente. O título, autor, assunto e palavras-chave armazenados no dicionário Info da origem não são transferidos. O documento de destino começa com metadados vazios após o CreateDocument, pelo que, se necessitar que estes campos sejam preenchidos no ficheiro final, terá de os atribuir diretamente ao PdfDest antes de chamar o SaveAs. As propriedades Title, Author, Subject, Keywords e Creator no TPdf aceitam strings simples e gravam no dicionário Info ao salvar.
Os campos de formulários interativos são mais complexos. As definições dos campos AcroForm residem num dicionário ao nível do documento e não nos fluxos de páginas individuais. Quando o ImportPages copia uma página que contém campos de formulário, o aspeto visual desses campos é transferido porque está desenhado no fluxo de conteúdo da página, mas os controlos (widgets) que os tornam interativos pertencem à estrutura AcroForm e não são transferidos. Numa fusão típica, um campo de texto de um documento de origem apresentará o valor que tinha no momento da importação, mas não será editável no ficheiro final. Se necessitar que os campos permaneçam preenchíveis, aplique o achatamento (flatten) em cada documento de origem antes de os importar: isso consolida os valores atuais no fluxo de conteúdo e remove a camada interativa, fornecendo um resultado visual limpo sem controlos inativos no ficheiro final.
Ficheiros de origem encriptados
Os documentos de origem protegidos por palavra-passe abrem da mesma forma que os não encriptados, necessitando apenas de uma propriedade adicional definida previamente. Atribua a palavra-passe a PdfSrc.Password antes de alterar para Active := True e o PDFium utilizá-la-á na abertura:
PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.Create('Wrong password or file cannot be opened');
PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
Uma palavra-passe incorreta provoca o mesmo resultado silencioso Active = False que um ficheiro em falta, pelo que a verificação explícita é igualmente necessária aqui. A encriptação não é transferida para o destino: as páginas importadas de uma origem protegida ficam sem proteção no documento final. Se o ficheiro fundido de saída necessitar de encriptação, configure-a no PdfDest antes de chamar o SaveAs.
Gravar o resultado
O método SaveAs no TPdf aceita um caminho de ficheiro ou um TStream. Para a maioria das fusões, a sobrecarga de ficheiro é a pretendida:
PdfDest.SaveAs('merged-output.pdf');
O segundo argumento opcional é um TSaveOption que controla o modo de gravação. O padrão, saNone, grava uma atualização incremental se o documento foi carregado a partir de um ficheiro, ou uma reescrita completa se foi criado de raiz. Como um destino construído com o CreateDocument é sempre novo, a saída será um ficheiro compacto de revisão única. O terceiro argumento, TPdfVersion, permite fixar a versão do cabeçalho do PDF quando tem sistemas a jusante que exigem uma versão específica; manter em pvUnknown permite que o PDFium escolha de acordo com o conteúdo.
Os métodos ImportPages e SaveAs apresentados aqui fazem parte do Componente PDFium VCL para Delphi e C++Builder.