O PDFium VCL expõe a mesclagem de PDF através de um único método: ImportPages. O padrão é sempre o mesmo: crie um documento de destino vazio, abra cada arquivo de origem, chame ImportPages para copiar as páginas, feche a origem e repita. Quando o loop termina, o SaveAs grava o resultado no disco. Não há nenhum modo de mesclagem especial, nem configuração para alternar. A complexidade vive nos casos extremos, e existem alguns que mordem sem aviso prévio.
O loop principal
Duas instâncias de TPdf são tudo o que você precisa. Uma detém o documento de destino, criado vazio com CreateDocument. A outra abre cada arquivo de origem por vez. Abaixo está uma procedure que pega uma lista de caminhos de arquivo e escreve a saída mesclada 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;
Duas coisas naquele código são fáceis de passar despercebidas numa primeira leitura. A primeira é como o PDFium relata falhas de carregamento. Active := True nunca levanta uma exceção (raises): se o arquivo estiver ausente, danificado ou protegido por senha, o PDFium captura o erro internamente e deixa Active como False. Sem a verificação explícita na linha 10, um arquivo ruim cairia silenciosamente da mesclagem sem nenhuma indicação na saída. O PDF final teria menos páginas do que o esperado e você não saberia qual arquivo foi o culpado.
A segunda é o contador InsertAt. O terceiro argumento para ImportPages é a posição baseada em 1 no destino onde a primeira página importada aterrissa. Começar no 1 coloca o primeiro documento de origem no início de um arquivo de outra forma vazio. Após cada origem, o contador avança por PdfSrc.PageCount, de modo que o próximo lote de páginas é anexado após o último. Esqueça de incrementá-lo e toda origem subsequente sobrescreve páginas na posição 1, dando a você o último documento na lista e nada mais.
Intervalos seletivos de páginas
Você não precisa pegar todas as páginas de uma origem. A string de intervalo passada como segundo argumento segue um formato simples de vírgula e hífen: "1-3" pega as páginas de 1 até 3, "2,4,6" escolhe três páginas específicas, e "1-" significa a página 1 até o final do documento. Os intervalos podem ser combinados em uma única string, de forma que "1-3,5,7-" pula as páginas 4 e 6. Uma sutileza importa aqui: os números sempre se referem a páginas no documento de origem, começando no 1, independente de onde essas páginas acabem no destino. Se você quiser das páginas 40 até 50 de um catálogo de 200 páginas, a string de intervalo é "40-50", não uma posição relativa ao que já está 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 você efetivamente importou, não a contagem de páginas da origem. Se você passar '1,3-5', você importou 4 páginas, então avance por 4. Avançar por PdfSrc.PageCount deixaria uma lacuna de posições de destino em branco e colocaria o documento de origem seguinte mais adiante no arquivo do que o pretendido.
O que ImportPages preserva e o que não preserva
As páginas copiadas pelo ImportPages carregam seu conteúdo visível intacto. Texto, gráficos vetoriais, imagens rasterizadas, fontes incorporadas e XObjects de formulário são todos transferidos como parte dos fluxos de conteúdo da página (content streams). Anotações em nível de página, incluindo comentários, destaques e traços de tinta, também vêm, porque são armazenados dentro do dicionário da página em vez de no nível do documento.
Os metadados em nível de documento são uma história diferente. O título, autor, assunto e strings de palavras-chave no dicionário Info da origem ficam para trás. O documento de destino começa com metadados vazios após CreateDocument, então, se a saída mesclada precisar que esses campos sejam preenchidos, você deve atribuí-los a PdfDest de forma direta antes de chamar SaveAs. As propriedades Title, Author, Subject, Keywords e Creator no TPdf recebem strings simples e gravam no dicionário Info ao salvar.
Campos de formulário interativos são mais complicados. Definições de campo AcroForm vivem em um dicionário de nível de documento em vez de dentro de streams de página individuais. Quando o ImportPages copia uma página que contém campos de formulário, a aparência visual desses campos é transferida porque ela é renderizada no fluxo de conteúdo da página, porém os widgets de campo que os tornam interativos são parte da estrutura AcroForm e não os seguem. Numa mesclagem típica, um campo de texto de um documento de origem exibirá o valor que ele tinha na hora da importação, mas não será editável no arquivo mesclado. Se você precisa que os campos permaneçam preenchíveis, achate-os (flatten) em cada documento de origem antes de importar: isso assa (bakes) os valores atuais para dentro do fluxo de conteúdo e remove a sobreposição interativa, fornecendo a você um resultado visual limpo, sem widgets quebrados na saída.
Arquivos de origem criptografados
Documentos de origem protegidos por senha abrem do mesmo jeito que os não criptografados, havendo uma propriedade extra para definir primeiro. Atribua a senha a PdfSrc.Password antes de alterar para Active := True, e o PDFium a usará durante a 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 senha incorreta causa o mesmo resultado silencioso de Active = False de um arquivo ausente, portanto a verificação explícita é tão necessária aqui quanto. A criptografia não é transferida para o destino: páginas importadas de uma origem protegida pousam no destino como conteúdo não protegido. Se a saída mesclada também precisar de criptografia, configure-a no PdfDest antes de chamar SaveAs.
Salvando o resultado
O SaveAs no TPdf aceita tanto um caminho de arquivo quanto um TStream. Para a maior parte das mesclagens, a sobrecarga de arquivo é o que você quer:
PdfDest.SaveAs('merged-output.pdf');
O segundo argumento opcional é um TSaveOption que controla o modo de salvar. O padrão, saNone, grava uma atualização incremental caso o documento tenha sido carregado por um arquivo ou uma reescrita completa caso ele tenha sido criado do zero. Visto que um destino construído com o CreateDocument sempre é recém-criado, a saída será num arquivo com única revisão, compacto. O terceiro argumento, o TPdfVersion, te permite fixar (pin) o cabeçalho de versão em PDF para momentos quando você dispõe de consumidores downstream com requerimentos estipuladores perante um de específica de versão; de maneira à em largar isso perante à uma correspondente pvUnknown é permitir perante as orientações estipulando ao predefinir formato por um referenciado do de PDFium com de escolhas englobando a de perante aos arranjos estipulados de referenciada em baseado sobre de o conteúdo.
Os métodos ImportPages e SaveAs mostrados aqui são parte do Componente PDFium VCL para o Delphi e C++Builder.