O PDFium VCL disponibiliza um único método para divisão de PDF: ImportPages. Tudo o resto, quer esteja a isolar uma única página, a cortar em limites arbitrários ou a seguir a própria estrutura de marcadores do documento, resume-se a diferentes formas de decidir quais os números de página que vão para cada ficheiro de saída. A mecânica permanece a mesma. Compreender isto desde o início evita muitos caminhos errados.
Como funciona o ciclo de divisão
O padrão é o mesmo independentemente de como divide o documento de origem. Crie uma nova instância de TPdf, chame CreateDocument nela para inicializar um PDF vazio em memória, importe as páginas pretendidas com ImportPages, guarde o resultado e, em seguida, redefina Active para False antes da próxima iteração. Este último passo é o que as pessoas costumam esquecer: sem este reinício, a chamada seguinte a CreateDocument anexa as páginas ao documento ainda em memória em vez de começar do zero. A instância externa de TPdf é reutilizada em todas as iterações, o que mantém a pressão de alocação de memória reduzida em tarefas grandes.
Eis como se parece a divisão página a página reduzida ao essencial:
procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument;
// Range is a 1-based page number string; insertion point 1 = first position
PdfOut.ImportPages(Source, IntToStr(I), 1);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
PdfOut.SaveAs(OutFile);
PdfOut.Active := False; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
O parâmetro Range de ImportPages utiliza o mesmo formato de string que o PDFium usa internamente: uma lista de números de página separados por vírgulas ou intervalos delimitados por hífenes, todos baseados em 1. '3' importa a página 3. '1-5' importa as páginas de 1 a 5 por ordem. '2,5,8' importa essas três páginas. O terceiro parâmetro é a posição de inserção baseada em 1 no documento de destino; passar 1 coloca sempre as páginas importadas no início de um ficheiro que, de outra forma, estaria vazio, que é o pretendido neste caso.
Dividir por intervalos de páginas
Quando o chamador fornece uma lista como 1-12,13-24,25-36, analisa-a em pares de início/fim e executa o mesmo ciclo, construindo a string de intervalo a partir de cada par:
procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(RangeList) do
begin
PdfOut.CreateDocument;
PdfOut.ImportPages(Source, RangeList[I], 1);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
PdfOut.SaveAs(OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
A validação antes de chegar a ImportPages é importante neste caso. O método ImportPages devolve False quando um número de página na string de intervalo excede Source.PageCount, mas não lança uma exceção e não gera um ficheiro de saída parcial que possa detetar apenas pelo nome. Verifique o valor de retorno de SaveAs e registe as falhas separadamente; um intervalo que gera um ficheiro de saída vazio não parece obviamente errado até que alguém o abra.
Dividir nos limites dos marcadores
A terceira abordagem utiliza a própria estrutura do documento em vez de uma lista fornecida externamente. Cada marcador de nível superior possui um número de página de destino; a secção que ele define estende-se desde essa página até à página imediatamente anterior à do marcador seguinte, ou até ao fim do documento no caso da última entrada.
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
I, StartPage, EndPage: Integer;
PdfOut: TPdf;
RangeStr, OutFile, SafeTitle: string;
begin
Bm := Source.Bookmarks;
if Length(Bm) = 0 then
Exit;
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(Bm) do
begin
StartPage := Bm[I].PageNumber;
if I < High(Bm) then
EndPage := Bm[I + 1].PageNumber - 1
else
EndPage := Source.PageCount;
if (StartPage < 1) or (EndPage < StartPage) then
Continue;
RangeStr := Format('%d-%d', [StartPage, EndPage]);
PdfOut.CreateDocument;
PdfOut.ImportPages(Source, RangeStr, 1);
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
PdfOut.SaveAs(OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Um documento que não possua marcadores não constitui uma condição de erro que valha a pena apresentar ao utilizador; significa apenas que este modo de divisão não tem elementos a partir dos quais trabalhar. A verificação Length(Bm) = 0 trata disso silenciosamente. O que vale a pena assinalar é quando o número de página de um marcador está fora do intervalo do documento, o que acontece em ficheiros malformados cujo índice nunca foi atualizado após a eliminação de páginas. A verificação de limites em StartPage e EndPage ignora essas entradas em vez de passar um intervalo inválido para ImportPages.
Nomes dos ficheiros de saída e a redefinição de Active
A segurança dos nomes de ficheiro derivados de marcadores requer atenção explícita. Os títulos dos marcadores podem conter caracteres que são válidos numa string PDF, mas não no caminho de um sistema de ficheiros. No mínimo, substitua a barra normal, a barra invertida e os dois pontos antes de construir o caminho de saída. No Windows, *, ?, ", <, > e | também são proibidos; um ciclo simples sobre um conjunto fixo resolve o problema sem necessidade de recorrer a expressões regulares.
A linha Active := False no final de cada iteração merece destaque por ser o único requisito não óbvio no padrão. O método CreateDocument não fecha implicitamente o que estiver aberto. Se Active continuar a ser True quando CreateDocument for executado novamente, o PDFium descarta o documento atual e inicia um novo sem erro, mas o comportamento é definido pela implementação em casos limite e a intenção torna-se mais clara quando faz a redefinição explicitamente. Pense nisso como o par de try/finally: o bloco finally liberta o objeto externo; a instrução Active := False redefine o estado do documento interno entre as iterações do ciclo.
O uso de memória ao longo de uma grande tarefa de divisão permanece estável com esta abordagem, uma vez que nunca mantém mais do que um documento de saída em memória em simultâneo. O documento de origem permanece aberto e apenas para leitura durante todo o processo; o método ImportPages copia os dados da página para o novo documento sem modificar a origem. Se a origem estiver encriptada, abra-a com a respetiva palavra-passe antes do ciclo e as páginas copiadas em cada ficheiro de saída ficarão desencriptadas, o que costuma ser o comportamento correto para saídas divididas a serem distribuídas por diferentes destinatários.
Mais um detalhe sobre o SaveAs: este devolve um valor Boolean. Um diretório de saída que não exista, um caminho com caracteres que o sistema operativo rejeite ou uma condição de disco cheio farão com que o SaveAs devolva False sem lançar uma exceção. Numa tarefa em lote que divide um documento de 200 páginas em 200 ficheiros individuais, uma falha silenciosa na página 147 é fácil de passar despercebida. Verifique o valor de retorno em cada chamada e conte os sucessos em relação ao total esperado quando o ciclo terminar.
Os métodos ImportPages e CreateDocument apresentados aqui fazem parte do PDFium VCL para Delphi e C++Builder.