Artigo Técnico

Imposição N-up e reordenação de páginas com PDFium

Merge e split são as duas operações de página que todo mundo procura primeiro, e elas cobrem muito terreno. Não cobrem tudo. Existe uma família separada de trabalho que reorganiza páginas em vez de mover arquivos inteiros: dispor quatro slides em uma folha para um handout, arrastar uma página do fim de um documento para o início, ou puxar as páginas 3, 7 e 12 para um extrato curto sem tocar no resto. O PDFium expõe três métodos exatamente para isso, e cada um se comporta de forma diferente do merge e do split que você já conhece. Este artigo percorre o que eles fazem, onde ficam os pontos de saída e um detalhe de ownership que é fácil de errar

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 arquivos 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 contatos 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 grade. Um valor de 2, 1 coloca duas páginas de origem lado a lado; 2, 2 acomoda quatro em um layout em quadrantes; 4, 3 monta uma folha de contato doze-up. O PDFium lê as páginas de origem em ordem, reduz cada uma para caber em sua 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 fica 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 compostos

Diagrama da imposição N-up do PDFium empacotando seis páginas de origem em folhas compostas US Letter two-up e four-up
ImportNPagesToOne empacota páginas de origem NumX por NumY em folhas dimensionadas em pontos, preenchendo da esquerda para a direita e de cima para baixo antes de começar uma folha nova

O tamanho de saída é em pontos, não em píxeis

OutputWidth e OutputHeight são unidades de usuário do PDF, e uma unidade de usuário 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 tela 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;

    // Quatro páginas de origem por folha Letter, grade 2 por 2
    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;   // veja a próxima seção: isto é obrigatório
    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 corre no outro sentido. Por baixo, o método pede ao PDFium um FPDF_DOCUMENT novo via FPDF_ImportNPagesToOne e então embrulha esse handle bruto dentro do TPdf retornado, de modo que o tempo de vida do wrapper governa o do handle. A partir daí existe exatamente um dono do handle, e exatamente um lugar em que ele deve ser fechado: quando você dá Free no objeto retornado. Um caminho de erro desatento que tanto libera o wrapper quanto chama FPDF_CloseDocument no handle bruto que capturou fecha o mesmo documento PDFium duas vezes. Isso é um double-free. A regra que evita isso é curta. Feche o documento em um único caminho, liberando o TPdf que o método entregou, e nunca estenda a mão além do wrapper para fechar o handle que ele já adotou

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

Diagrama de ciclo de vida do handle TPdf retornado por PDFium ImportNPagesToOne com o caminho seguro de liberação única versus o caminho de crash de double-free
O TPdf retornado é um segundo documento de propriedade do chamador: inicialize-o como nil, verifique-o e libere o wrapper em apenas um caminho

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 zero-based. PageIndices lista as páginas a mover, na ordem em que devem acabar, e DestPageIndex é o índice em que a primeira página movida pousa depois que a movimentação se assenta. Como o PDFium realoca as páginas em vez de copiar e recomprimir seu conteúdo, a operação é barata e sem perdas: os objetos de página mantêm seus streams, seus recursos e sua fidelidade. Essa é a chamada por trás de um painel de reordenação por arrastar, em que o usuário puxa uma miniatura para um novo slot e você confirma a nova ordem com um único move. Ele retorna False quando um índice está fora da faixa, então valide o resultado em vez de assumir que a reordenação aconteceu

PDFium Component: mapa de reordenação MovePages movendo a página quatro do PDF para o índice zero de um relatório de cinco páginas, enquanto as páginas restantes se deslocam um slot para a direita
MovePages eleva a página 4 ao índice 0 enquanto as páginas restantes se deslocam um slot e a contagem de páginas permanece em cinco
var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move a última página (índice 4 num arquivo de 5) para bem na frente
    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 arquivos 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;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // começa com um destino vazio

    // Puxa as páginas 3, 7 e 12 (base 0: 2, 6, 11) para o extrato
    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 atually 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