O PDFium Component dá-lhe um método para dividir PDF: ImportPages. Tudo o resto, quer esteja a isolar uma única página, a cortar em fronteiras arbitrárias, ou a seguir a própria estrutura de marcadores do documento, são apenas maneiras diferentes de decidir que números de página vão para cada ficheiro de saída. A mecânica mantém-se a mesma. Perceber isso cedo poupa 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 instância TPdf nova, chame nela CreateDocument para inicializar um PDF vazio em memória, importe as páginas que quer com ImportPages, guarde o resultado, e depois reponha Active a False antes da iteração seguinte. Esse último passo é o que as pessoas deixam escapar: CreateDocument não fecha implicitamente o documento que ainda está em memória, pelo que tem de guardar a saída e repor Active := False de forma explícita antes de o chamar outra vez; repor primeiro mantém o estado limpo e bem definido. A instância TPdf exterior é reutilizada em todas as iterações, o que mantém baixa a pressão de alocação em trabalhos grandes
Eis o aspeto da 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 é uma string de números de página a começar em 1; posição 1 = primeira
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Failed to import page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False; // repor antes do próximo CreateDocument
end;
finally
PdfOut.Free;
end;
end;
O parâmetro Range de ImportPages tem o mesmo formato de string que o PDFium usa internamente: uma lista separada por vírgulas de números de página ou de intervalos delimitados por hífen, todos a começar em 1. '3' importa a página 3. '1-5' importa as páginas 1 a 5 por ordem. '2,5,8' importa essas três páginas. O terceiro parâmetro é a posição de inserção no documento de destino, também a começar em 1; passar 1 coloca sempre as páginas importadas no início de um ficheiro de outro modo vazio, que é o que aqui se pretende
Dividir por intervalos de páginas
Quando quem chama fornece uma lista como 1-12,13-24,25-36, analisa-a em pares de início e fim e corre 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;
if not PdfOut.ImportPages(Source, RangeList[I], 1) then
raise Exception.Create('Invalid page range: ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
A validação antes de chegar a ImportPages importa aqui. ImportPages devolve False quando um número de página na string de intervalo excede Source.PageCount, mas não levanta exceção e não produz um ficheiro de saída parcial que consiga detetar só pelo nome. Verifique o valor de retorno de SaveAs e registe as falhas em separado; um intervalo que produz um ficheiro de saída vazio não é obviamente errado até alguém o abrir
Dividir nas fronteiras dos marcadores
A terceira abordagem usa a estrutura do próprio documento em vez de uma lista fornecida do exterior. Cada marcador de nível de topo transporta um número de página de destino; a secção que define vai dessa página até uma antes da página 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;
if not PdfOut.ImportPages(Source, RangeStr, 1) then
begin
PdfOut.Active := False;
Continue; // ignora uma secção malformada em vez de escrever ficheiro vazio
end;
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Um documento sem marcadores não é uma condição de erro que valha a pena apresentar ao utilizador como tal; significa apenas que este modo de divisão não tem por onde pegar. A guarda Length(Bm) = 0 trata disso em silêncio. O que vale a pena apresentar é quando o número de página de um marcador cai fora do intervalo do documento, o que acontece em ficheiros malformados onde a estrutura de tópicos nunca foi atualizada depois de páginas terem sido apagadas. A verificação de limites em StartPage e EndPage ignora essas entradas em vez de passar um intervalo lixo a ImportPages
Nomes dos ficheiros de saída e a reposição de Active
A segurança dos nomes de ficheiro derivados de marcadores exige atenção explícita. Os títulos de marcadores podem conter carateres válidos numa string PDF mas não num caminho do sistema de ficheiros. No mínimo, substitua a barra, 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 trata deles sem trazer uma expressão regular
A linha Active := False no fim de cada iteração merece ênfase porque é o único requisito não óbvio do padrão. CreateDocument não fecha implicitamente o que estiver aberto. Se Active ainda estiver a True quando CreateDocument voltar a correr, o documento que continua em memória nunca foi devidamente fechado nem guardado, e não pode contar com comportamento bem definido nesse estado, pelo que deve guardar e repor de forma explícita antes de começar o documento seguinte. Pense nisso como o par do try/finally: o bloco finally liberta o objeto exterior; o Active := False repõe o estado do documento interior entre iterações do ciclo
O uso de memória ao longo de um grande trabalho de divisão mantém-se plano com esta abordagem, porque nunca tem mais do que um documento de saída em memória de cada vez. O documento de origem permanece aberto e só de leitura do princípio ao fim; ImportPages copia os dados das páginas para o documento novo sem modificar a origem. Se a origem estiver cifrada, abra-a com a respetiva palavra-passe antes do ciclo e as páginas copiadas em cada ficheiro de saída ficarão sem cifra, o que costuma ser o comportamento certo para saída dividida distribuída por destinatários diferentes
Mais uma coisa sobre o SaveAs: devolve um Boolean. Um diretório de saída que não existe, um caminho com carateres que o sistema operativo rejeita, ou uma condição de disco cheio fazem todos com que SaveAs devolva False sem levantar exceção. Num trabalho em lote que divide um documento de 200 páginas em 200 ficheiros de uma página, uma falha silenciosa na página 147 passa facilmente despercebida. Verifique o valor de retorno em cada chamada e conte os sucessos contra o total esperado quando o ciclo terminar
Os métodos ImportPages e CreateDocument aqui mostrados fazem parte do PDFium Component para Delphi e C++Builder