O Componente PDFium expõe a mesclagem de PDF através de um único método: ImportPages. O padrão é sempre o mesmo: criar um documento de destino vazio, abrir cada arquivo de origem, chamar ImportPages para copiar as páginas, fechar a origem e repetir. Quando o loop termina, SaveAs grava o resultado no disco. Não há nenhum modo de mesclagem especial, nenhuma configuração a alternar. A complexidade vive nos casos extremos, e existem alguns que surpreendem sem aviso
O loop principal
Duas instâncias de TPdf são tudo que você precisa. Uma contém o documento de destino, criado vazio com CreateDocument. A outra abre cada arquivo de origem por sua vez. Abaixo está um procedimento que recebe uma lista de caminhos de arquivos e grava a saída mesclada em um ú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 nesse código são fáceis de ignorar em uma primeira leitura. A primeira é como o PDFium relata falhas de carregamento. Active := True nunca levanta uma exceção: 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 corrompido sairia silenciosamente da mesclagem, sem nenhuma indicação na saída. O PDF final teria menos páginas que o esperado e você não saberia qual arquivo foi o culpado
A segunda é o contador InsertAt. O terceiro argumento de ImportPages é a posição baseada em 1 no destino onde a primeira página importada é colocada. Começar em 1 coloca o primeiro documento de origem no início de um arquivo que, de outra forma, estaria vazio. Após cada origem, o contador avança em PdfSrc.PageCount, para que o próximo lote de páginas seja anexado após a última. Esqueça de incrementá-lo e cada origem subsequente substituirá as páginas na posição 1, deixando você com o último documento da lista e nada mais
Intervalos seletivos de páginas
Você não precisa obter todas as páginas de uma origem. A string de intervalo passada como o segundo argumento segue um formato simples de vírgula e hífen: "1-3" obtém as páginas 1 a 3, "2,4,6" seleciona três páginas específicas e "1-" significa da página 1 até o final do documento. Os intervalos podem ser combinados em uma única string, de modo que "1-3,5,7-" pula as páginas 4 e 6. Uma sutileza importa aqui: os números sempre se referem às páginas no documento de origem, começando em 1, independentemente de onde essas páginas acabem no destino. Se você quiser as páginas 40 a 50 de um catálogo de 200 páginas, a string de intervalo será "40-50", e 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ê realmente importou, não a contagem de páginas da origem. Se você passar '1,3-5', você importou 4 páginas, então avance em 4. Avançar usando PdfSrc.PageCount deixaria uma lacuna de posições de destino em branco e colocaria o próximo documento de origem mais adiante no arquivo do que o pretendido
O que ImportPages preserva e o que não preserva
Páginas copiadas pelo ImportPages mantêm o 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. Anotações ao nível da página, incluindo comentários, realces e traços a tinta, também acompanham, porque são armazenadas dentro do dicionário da página em vez do nível do documento
Metadados no nível do documento são outra história. As strings de título, autor, assunto e palavra-chave no dicionário Info da origem ficam para trás. O documento de destino começa com metadados vazios após CreateDocument, portanto, se a saída mesclada precisar que esses campos sejam preenchidos, você deverá atribuí-los diretamente ao PdfDest antes de chamar SaveAs. As propriedades Title, Author, Subject, Keywords e Creator no TPdf recebem strings simples e escrevem 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 no nível do documento, e não dentro de fluxos individuais da página. Quando ImportPages copia uma página que contém campos de formulário, a aparência visual desses campos é transferida porque é renderizada no fluxo de conteúdo da página, mas os widgets do campo que os tornam interativos fazem parte da estrutura AcroForm e não seguem junto. Numa mesclagem típica, um campo de texto de um documento de origem exibirá o valor que possuía no momento da importação, mas não será editável no arquivo mesclado. Se você precisar que os campos permaneçam preenchíveis, realize um nivelamento (flatten) deles em cada documento de origem antes de importar: isso integra os valores atuais ao fluxo de conteúdo e remove a sobreposição interativa, fornecendo um resultado visual limpo, sem widgets quebrados na saída
Arquivos de origem criptografados
Documentos de origem protegidos por senha abrem da mesma maneira que aqueles sem criptografia, exigindo apenas que uma propriedade extra seja configurada primeiro. Atribua a senha a PdfSrc.Password antes de definir Active := True, e o PDFium a utilizará 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 Active = False de um arquivo ausente, portanto, a verificação explícita é igualmente necessária aqui. A criptografia não é transferida para o destino: páginas importadas de uma origem protegida aterrissam 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
SaveAs no TPdf aceita um caminho de arquivo ou um TStream. Para a maioria das mesclagens, a sobrecarga de arquivo é o que você deseja:
PdfDest.SaveAs('merged-output.pdf');
O segundo argumento, opcional, é um TSaveOption que controla o modo de salvamento. O padrão, saNone, grava uma atualização incremental se o documento tiver sido carregado a partir de um arquivo, ou uma reescrita completa se for recém-criado. Como um destino criado com CreateDocument é sempre novo, a saída será um arquivo compacto de revisão única. O terceiro argumento, TPdfVersion, permite fixar o cabeçalho da versão do PDF quando os consumidores posteriores precisarem de uma versão específica; deixá-lo como pvUnknown permite que o PDFium escolha a versão baseada no conteúdo
Os métodos ImportPages e SaveAs aqui apresentados fazem parte do Componente PDFium para Delphi e C++Builder