Mesclar e dividir são as duas operações de página mais procuradas, e elas cobrem bastante espaço. No entanto, elas não cobrem tudo. Existe uma outra família de tarefas que rearranja páginas em vez de mover arquivos inteiros: distribuir quatro slides em uma folha, arrastar uma página do final do documento para o início ou extrair as páginas 3, 7 e 12 em um trecho curto sem tocar no restante. O PDFium expõe três métodos exatamente para isso, e cada um deles se comporta de maneira diferente da mesclagem e divisão que você já conhece. Este artigo descreve o que eles fazem, onde os pontos de saída residem e um detalhe de propriedade que causou falhas em produção.
Os três métodos são ImportNPagesToOne para imposição N-up, MovePages para reordenação local e ImportPagesByIndex para extração de subconjuntos. A mesclagem empilha documentos de ponta a ponta e resulta em uma contagem de páginas igual à soma das entradas. A divisão grava vários arquivos de saída a partir de uma única entrada. As três operações descritas aqui situam-se no meio: uma delas altera quantas páginas de origem compartilham uma folha, outra altera a ordem dentro de um único documento e a última copia um grupo selecionado de páginas para outro documento. Saber qual é qual evita que você realize uma ginástica de mesclagem e exclusão onde uma única chamada seria suficiente.
O que a imposição N-up realmente faz
Imposição é o termo gráfico para organizar várias páginas de origem em uma folha maior, de modo que o resultado impresso e dobrado seja lido na ordem correta. A versão cotidiana é o folheto de duas páginas (2-up), o caderno de quatro páginas (4-up) ou a folha de contatos que acomoda uma dúzia de miniaturas em uma página. O PDFium gerencia essa geometria por meio de uma única chamada:
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
NumX e NumY descrevem a grade. Um valor de 2, 1 coloca duas páginas de origem lado a lado; 2, 2 agrupa quatro em um layout de quadrantes; 4, 3 cria uma folha de contatos de doze páginas. O PDFium lê as páginas de origem em ordem, reduz cada uma para caber em sua respectiva célula e preenche a grade da esquerda para a direita, de cima para baixo, iniciando uma nova folha de saída sempre que a grade atual estiver cheia. As páginas de origem não são modificadas. O que você recebe de volta é um novo documento cujas páginas são compostas.
O tamanho de saída é em pontos, não em pixels
OutputWidth e OutputHeight são unidades de usuário do PDF, e uma unidade de usuário do PDF é um ponto, o qual equivale a 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 pixels da tela ou DPI de renderização. Esse é o local mais comum onde ocorrem erros de imposição, porque um desenvolvedor acostumado a bitmaps busca uma contagem de pixels e acaba com uma folha do tamanho de um selo postal ou de um outdoor.
Os números que valem a pena memorizar são os dois tamanhos de página mais comuns. O formato Carta (US Letter) tem 612 por 792 pontos, porque 8,5 polegadas multiplicadas por 72 dá 612, e 11 polegadas multiplicadas por 72 dá 792. O formato A4 tem aproximadamente 595 por 842 pontos, a partir de suas dimensões de 210 por 297 milímetros. O próprio cabeçalho do binding declara a regra claramente: uma unidade equivale a um setenta e dois avos de polegada, e o pacote fornece uma constante PointsPerInch igual a 72 caso você prefira calcular o tamanho a partir de polegadas no código em vez de gravar 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 deve ser liberado por você
Leia a assinatura novamente. O 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 proprietário dele. O TPdf de origem sobre o qual você chamou o método permanece intocado e ainda possui seu próprio handle; o composto é um segundo objeto independente. Se você deixar o TPdf retornado sair do escopo sem liberá-lo, vazará um documento PDFium inteiro.
O erro mais perigoso ocorre na direção inversa. Internamente, o método solicita ao PDFium um novo FPDF_DOCUMENT por meio de FPDF_ImportNPagesToOne e, em seguida, envolve esse handle bruto dentro do TPdf retornado para que o tempo de vida do envelopador governe o do handle. A partir desse momento, há exatamente um proprietário do handle e exatamente um local onde ele deve ser fechado: quando você libera (Free) o objeto retornado. Um caminho de erro descuidado que tanto libera o envelopador quanto chama FPDF_CloseDocument no handle bruto capturado fecha o mesmo documento PDFium duas vezes. Isso é uma liberação dupla (double-free) e foi o bug específico que afetou um chamador anteriormente. A regra que o previne é simples. Feche o documento em apenas um caminho, liberando o TPdf que o método entregou, e nunca acesse além do envelopador para fechar o handle que ele já adotou.
Dois corolários decorrem disso. Primeiro, o método retorna nil quando o PDFium rejeita os argumentos, como um valor zero em qualquer eixo da grade ou uma falha de alocação, de modo que uma verificação de nil deve ser feita antes de ajustar o resultado. Segundo, inicialize sua variável de saída como nil antes do bloco try e libere-a no finally, como o exemplo acima faz, para que uma falha no meio do caminho não deixe você liberando uma referência indefinida ou pulando a liberação por completo.
Reordenando páginas sem gravá-las novamente
A imposição constrói um novo documento. A reordenação altera um documento localmente. O MovePages retira um conjunto de páginas de suas posições atuais e as posiciona em um destino, deslocando o restante em torno do bloco movido para que a contagem de páginas permaneça a mesma:
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
Os índices são baseados em zero. O PageIndices lista as páginas a serem movidas, na ordem em que devem ficar, e o DestPageIndex é o índice onde a primeira página movida ficará após a movimentação se estabilizar. Como o PDFium realoca as páginas em vez de copiar e compactar novamente o seu conteúdo, a operação é leve e sem perdas: os objetos de página mantêm seus fluxos, seus recursos e sua fidelidade. Esta é a chamada por trás de um painel de páginas do tipo arrastar-para-reordenar, onde um usuário puxa uma miniatura para um novo slot e você confirma a nova ordem com um único movimento. Ela retorna False quando um índice está fora do intervalo, logo valide o resultado em vez de assumir que o rearranjo funcionou.
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;
Extraindo um subconjunto por índice
A terceira operação copia um conjunto explícito de páginas de um documento para outro. O ImportPagesByIndex recebe o documento de origem e um array de índices baseado em zero, inserindo essas páginas no destino em uma posição escolhida:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
Você o chama no documento de destino e passa a origem como o primeiro argumento. O PageIndices nomeia as páginas de origem a serem extraídas, na ordem desejada; o InsertAt é a posição baseada em zero no destino onde a primeira página importada ficará, de forma que 0 as coloca antes da primeira página existente e a contagem de páginas atual do destino é incrementada. Um array vazio importa todas as páginas, o que torna a chamada uma cópia completa quando você precisa de uma. Retorna False se qualquer índice estiver fora do intervalo na origem.
É aqui que a diferença com a divisão importa. A divisão grava arquivos separados, uma operação gerando muitas saídas no disco. O ImportPagesByIndex faz o oposto: reúne um conjunto escolhido de páginas em um único documento de destino na memória, o qual você salva uma única vez. Quando a tarefa é "me dê as páginas 3, 7 e 12 como um único PDF curto", este é o caminho direto e encapsula o FPDF_ImportPagesByIndex internamente.
var
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;
Juntando tudo de forma limpa
O formato de ponta a ponta é o mesmo para as três operações: abra a origem definindo FileName e alterando Active para True, execute a operação, salve com SaveAs e libere o que você possui. O único detalhe que precisa de atenção é quais chamadas alocam um novo documento. O MovePages modifica o documento que você já possui, de modo que há apenas um objeto para liberar. O ImportPagesByIndex grava em um destino que você mesmo criou, portanto você libera a origem e o destino aberto. O ImportNPagesToOne é o caso atípico, porque o novo documento é o valor de retorno do método, e não algo que você construiu, e esquecer que se trata de um handle separado e de propriedade do chamador é a forma como o vazamento e a liberação dupla acontecem. Inicialize o resultado como nil, verifique-o após a chamada e libere-o em um único caminho.
Se o trabalho que você realmente tem é combinar arquivos inteiros em vez de reorganizar páginas, consulte mesclando múltiplos arquivos PDF em um único documento. Se for o oposto, dividir um documento em vários arquivos, consulte dividindo documentos PDF em múltiplos arquivos. Os métodos de imposição e reordenação descritos aqui são fornecidos como parte do Componente PDFium para Delphi e C++Builder, juntamente com as APIs de carregamento, renderização e edição abordadas em outras partes deste blog.