Juntar (merge) e dividir (split) são as duas operações de página a que todos recorrem primeiro, e cobrem bastante terreno. Mas não abrangem tudo. Existe uma família distinta de tarefas que reorganiza páginas em vez de mover ficheiros inteiros: dispor quatro diapositivos (slides) numa única folha para um folheto, arrastar uma página do fim de um documento para a frente, ou extrair as páginas 3, 7 e 12 num pequeno excerto sem tocar no resto. O PDFium expõe três métodos precisamente para isto, e cada um deles comporta-se de forma diferente da junção e divisão que já conhece. Este artigo explica o que fazem, onde residem os pontos de saída e um detalhe de propriedade que causou um crash em produção.
Os três são ImportNPagesToOne para imposição N-up, MovePages para reordenação local e ImportPagesByIndex para extração de subconjuntos. A junção empilha documentos de ponta a ponta e resulta numa contagem de páginas igual à soma das entradas. A divisão escreve vários ficheiros de saída a partir de uma entrada. As três operações aqui descritas situam-se no meio: uma delas altera quantas páginas de origem partilham uma folha, outra altera a ordem dentro de um único documento e a última copia um grupo escolhido de páginas para outro documento. Saber distingui-las evita que force uma dança de junção e eliminação quando uma única chamada resolveria o problema.
O que a imposição N-up realmente faz
Imposição é o termo de pré-impressão para organizar várias páginas de origem numa folha maior, de modo que o resultado impresso e dobrado seja lido na ordem correta. A versão do dia a dia é o folheto de 2 páginas por folha (2-up), o caderno de 4 páginas de um folheto, ou a folha de contactos que acomoda uma dúzia de miniaturas numa página. O PDFium gere a geometria através de uma única chamada:
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
NumX e NumY descrevem a grelha. Um valor de 2, 1 coloca duas páginas de origem lado a lado; 2, 2 agrupa quatro num esquema de quadrantes; 4, 3 cria uma folha de contactos de doze páginas. O PDFium lê as páginas de origem por ordem, redimensiona cada uma para caber na sua célula e preenche a grelha da esquerda para a direita, de cima para baixo, iniciando uma nova folha de saída sempre que a grelha atual estiver cheia. As páginas de origem não são modificadas. O que recebe de volta é um novo documento cujas páginas são compostas.
O tamanho de saída é em pontos, não em píxeis
OutputWidth e OutputHeight são unidades de utilizador do PDF, e uma unidade de utilizador do PDF equivale a um ponto, que é um setenta e dois avos de polegada. A unidade declara o tamanho físico da folha de saída e não tem relação com píxeis do ecrã ou DPI de renderização. Este é o local mais comum para cometer erros de imposição, porque um programador habituado a bitmaps tende a indicar uma contagem de píxeis e acaba com uma folha do tamanho de um selo postal ou de um cartaz publicitário.
Os números que vale a pena memorizar são os dois tamanhos de página mais utilizados. O formato US Letter tem 612 por 792 pontos, porque 8.5 polegadas vezes 72 é 612 e 11 polegadas vezes 72 é 792. O A4 tem aproximadamente 595 por 842 pontos, derivados das suas dimensões de 210 por 297 milímetros. O próprio cabeçalho do binding indica a regra de forma clara: uma unidade equivale a um setenta e dois avos de polegada, e a unidade inclui uma constante PointsPerInch igual a 72 caso prefira calcular o tamanho a partir de polegadas no código em vez de escrever o valor literal.
const
LetterW = 612.0; // 8.5 in * 72
LetterH = 792.0; // 11 in * 72
var
Source, Composite: TPdf;
begin
Source := TPdf.Create(nil);
Composite := nil;
try
Source.FileName := 'slides.pdf';
Source.Active := True;
// Four source pages per Letter sheet, 2 by 2 grid.
Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
if Composite = nil then
raise Exception.Create('PDFium rejected the imposition arguments');
Composite.SaveAs('slides-4up.pdf');
finally
Composite.Free; // see the next section: this is mandatory
Source.Free;
end;
end;
O handle retornado é seu para libertar
Leia a assinatura novamente. ImportNPagesToOne retorna um TPdf, não um Boolean. Esse valor de retorno é um handle de documento totalmente novo, alocado separadamente da origem, e o chamador é o seu proprietário. O TPdf de origem sobre o qual chamou o método permanece inalterado e continua a possuir o seu próprio handle; o composto é um segundo objeto independente. Se permitir que o TPdf retornado saia do âmbito (scope) sem o libertar, originará uma fuga de um documento PDFium completo.
O erro mais perigoso ocorre na direção oposta. Internamente, o método solicita ao PDFium um novo FPDF_DOCUMENT através de FPDF_ImportNPagesToOne, envolvendo depois esse handle simples dentro do TPdf retornado para que o tempo de vida do wrapper governe o do handle. A partir desse momento, existe exatamente um proprietário do handle, e exatamente um local onde ele deve ser fechado: ao libertar (Free) o objeto retornado. Um caminho de erro descuidado que liberte o wrapper e também chame FPDF_CloseDocument no handle simples capturado fecha o mesmo documento PDFium duas vezes. Trata-se de uma libertação dupla (double-free), sendo o erro específico que afetou um chamador no passado. A regra que o previne é simples. Feche o documento apenas num caminho, libertando o TPdf que o método lhe entregou, e nunca aceda ao handle através do wrapper para o fechar após este já ter sido adotado.
Disto resultam dois corolários. Primeiro, o método retorna nil quando o PDFium rejeita os argumentos, como um zero em qualquer dos eixos da grelha ou uma falha de alocação, pelo que deve realizar uma verificação de nil antes de interagir com o resultado. Segundo, inicialize a sua variável de saída para nil antes do bloco try e liberte-a no finally, como faz o exemplo acima, para que uma falha a meio do processo não o deixe a tentar libertar uma referência indefinida ou a saltar a libertação por completo.
Reordenar páginas sem as reescrever
A imposição constrói um documento novo. A reordenação altera um documento localmente. O método MovePages retira um conjunto de páginas das suas posições atuais e coloca-as num destino, deslocando todos os restantes elementos em torno do bloco movido para que a contagem de páginas se mantenha inalterada:
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
Os índices são baseados em zero. PageIndices lista as páginas a mover, na ordem em que devem ficar, e DestPageIndex é o índice onde a primeira página movida aterra após a reordenação se consolidar. Como o PDFium recoloca as páginas em vez de copiar e recompactar o seu conteúdo, a operação é económica e sem perdas: os objetos de página mantêm os seus fluxos, os seus recursos e a sua fidelidade. Esta é a chamada por trás de um painel de páginas do tipo arrastar para reordenar, onde um utilizador puxa uma miniatura para uma nova posição e o código consolida a nova ordem com uma única chamada. Retorna False quando um índice está fora de alcance, por isso valide o resultado em vez de assumir que a reorganização foi bem-sucedida.
var
Doc: TPdf;
begin
Doc := TPdf.Create(nil);
try
Doc.FileName := 'report.pdf';
Doc.Active := True;
// Move the last page (index 4 in a 5-page file) to the very front.
if not Doc.MovePages([4], 0) then
raise Exception.Create('MovePages rejected the index');
Doc.SaveAs('report-reordered.pdf');
finally
Doc.Free;
end;
end;
Extrair um subconjunto por índice
A terceira operação copia um conjunto explícito de páginas de um documento para outro. A função ImportPagesByIndex recebe o documento de origem e um array de índices baseado em zero, inserindo essas páginas no destino numa posição escolhida:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
Esta função é invocada no documento de destino, passando-se a origem como primeiro argumento. PageIndices indica as páginas de origem a extrair, na ordem pretendida; InsertAt é a posição baseada em zero no destino onde a primeira página importada será inserida, pelo que 0 as coloca antes da primeira página existente e a contagem de páginas atual do destino aumenta. Um array vazio importa todas as páginas, tornando a chamada uma cópia completa se precisar de uma. Retorna False se algum índice estiver fora de intervalo na origem.
É aqui que o contraste com a divisão (split) é importante. A divisão escreve ficheiros separados, com uma operação a produzir várias saídas no disco. ImportPagesByIndex realiza o trabalho oposto: reúne um conjunto selecionado de páginas num único documento de destino em memória, que depois guarda uma vez. Quando o objetivo é "obter as páginas 3, 7 e 12 num único PDF curto", esta é a via direta, envolvendo internamente a função FPDF_ImportPagesByIndex.
var
Source, Excerpt: TPdf;
Source, Excerpt: TPdf;
begin
Source := TPdf.Create(nil);
Excerpt := TPdf.Create(nil);
try
Source.FileName := 'manual.pdf';
Source.Active := True;
Excerpt.CreateDocument; // start an empty target
// Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
raise Exception.Create('A requested page index is out of range');
Excerpt.SaveAs('manual-excerpt.pdf');
finally
Excerpt.Free;
Source.Free;
end;
end;
Juntar tudo de forma limpa
A estrutura de ponta a ponta é semelhante nas três operações: abre-se a origem definindo o FileName e mudando o Active para True, executa-se a operação, guarda-se com SaveAs e liberta-se o que se possui. O único aspeto que requer cuidado é identificar quais as chamadas que alocam um documento novo. MovePages altera o documento que já possui, pelo que há apenas um objeto para libertar. ImportPagesByIndex escreve num destino criado por si, pelo que liberta a origem e o destino que abriu. O método ImportNPagesToOne é a exceção, porque o novo documento é o valor de retorno do método e não algo construído por si, e esquecer que se trata de um handle separado pertencente ao chamador é como a fuga de memória e a libertação dupla acontecem. Inicialize o resultado para nil, verifique-o após a chamada e liberta-o apenas num caminho.
Se the work you actually have is combining whole files rather than rearranging pages, see merging multiple PDF files into one document. If it is the reverse, breaking one document into several files, see splitting PDF documents into multiple files. The imposition and reordering methods described here ship as part of the PDFium Component for Delphi and C++Builder alongside the loading, rendering, and editing APIs covered elsewhere on this blog.