Artigo Técnico

Imposição N-up e Reordenação de Páginas com PDFium

Juntar e dividir são as duas operações de páginas a que toda a gente recorre primeiro, e cobrem muito terreno. Não cobrem tudo. Há uma família de trabalho à parte que rearranja páginas em vez de mover ficheiros inteiros: colocar quatro diapositivos numa folha para um documento de apoio, arrastar uma página do fim de um documento para o início, ou extrair as páginas 3, 7 e 12 para um breve excerto sem tocar no resto. O PDFium expõe três métodos exatamente para isto, e cada um comporta-se de forma diferente da junção e da divisão que já conhece. Este artigo percorre o que fazem, onde vivem as saídas, e um pormenor de propriedade que já provocou um crash no terreno

Os três são ImportNPagesToOne para imposição N-up, MovePages para reordenação no lugar, e ImportPagesByIndex para extração de subconjuntos. Juntar empilha documentos uns a seguir aos outros e deixa a contagem de páginas igual à soma das entradas. Dividir escreve vários ficheiros de saída a partir de uma entrada. As três operações aqui ficam pelo meio: uma delas muda quantas páginas de origem partilham uma folha, outra muda a ordem dentro de um único documento, e outra copia um punhado escolhido de páginas para outro documento. Saber qual é qual poupa-lhe forçar uma dança de juntar e apagar onde bastava uma única chamada

O que a imposição N-up faz realmente

Imposição é o termo de pré-impressão para dispor várias páginas de origem numa folha maior de modo a que o resultado impresso e dobrado se leia pela ordem certa. A versão do dia a dia é o documento de apoio 2-up, o caderno de encadernação 4-up, ou a folha de contactos que junta uma dúzia de miniaturas numa página. O PDFium trata da geometria com uma só 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 arruma quatro numa disposição em quadrantes; 4, 3 constrói uma folha de contactos de doze. O PDFium lê as páginas de origem por ordem, reduz cada uma até caber na sua célula, e preenche a grelha da esquerda para a direita e de cima para baixo, começando uma folha de saída nova sempre que a grelha atual fica cheia. As páginas de origem não são modificadas. O que recebe de volta é um documento novo cujas páginas são compósitos

Diagrama da imposição N-up do PDFium a arrumar seis páginas de origem em folhas compósitas US Letter de dois e de quatro por folha
O ImportNPagesToOne arruma NumX por NumY páginas de origem 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 pixels

OutputWidth e OutputHeight são unidades de utilizador do PDF, e uma unidade de utilizador do PDF é um ponto, que é um setenta e dois avos de polegada. A unidade declara o tamanho físico da folha de saída, e nada tem que ver com pixels de ecrã ou DPI de renderização. Este é isoladamente o sítio mais comum onde uma imposição corre mal, porque um programador habituado a bitmaps recorre a uma contagem de pixels e acaba com uma folha do tamanho de um selo ou de um outdoor

Os números que vale a pena decorar são os dois tamanhos de página que mais vai usar. O US Letter tem 612 por 792 pontos, porque 8,5 polegadas vezes 72 dá 612 e 11 polegadas vezes 72 dá 792. O A4 tem cerca de 595 por 842 pontos, a partir das suas dimensões de 210 por 297 milímetros. O cabeçalho da própria camada de ligação enuncia a regra com clareza, que uma unidade é um setenta e dois avos de polegada, e a unit disponibiliza uma constante PointsPerInch igual a 72 se preferir calcular um tamanho a partir de polegadas em código em vez de escrever o literal

const
  LetterW = 612.0;   // 8,5 pol * 72
  LetterH = 792.0;   // 11  pol * 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, grelha 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;   // ver a secção seguinte: isto é obrigatório
    Source.Free;
  end;
end;

O handle devolvido é seu para libertar

Leia de novo a assinatura. ImportNPagesToOne devolve um TPdf, não um Boolean. Esse valor de retorno é um handle de documento novo em folha, alocado à parte da origem, e quem chama é o seu dono. O TPdf de origem sobre o qual chamou o método fica intocado e continua dono do seu próprio handle; o compósito é um segundo objeto independente. Se deixar o TPdf devolvido sair de âmbito sem o libertar, fuga um documento PDFium inteiro

O erro mais perigoso vai no sentido contrário. Por baixo, o método pede ao PDFium um FPDF_DOCUMENT novo através de FPDF_ImportNPagesToOne, e depois embrulha esse handle em bruto dentro do TPdf devolvido, para que o tempo de vida do invólucro governe o do handle. A partir desse ponto há exatamente um dono do handle, e exatamente um sítio onde deve ser fechado: quando faz Free ao objeto devolvido. Um caminho de erro descuidado que liberte o invólucro e também chame FPDF_CloseDocument sobre o handle em bruto que capturou fecha o mesmo documento PDFium duas vezes. Isso é uma dupla libertação, e é o bug concreto que aqui mordeu uma vez quem chamava. A regra que o evita é curta. Feche o documento por um só caminho, libertando o TPdf que o método lhe entregou, e nunca passe por cima do invólucro para fechar o handle que ele já adotou

Daqui caem dois corolários. Primeiro, o método devolve nil quando o PDFium rejeita os argumentos, como um zero em qualquer dos eixos da grelha ou uma falha de alocação, pelo que uma verificação de nil pertence a antes de tocar no resultado. Segundo, inicialize a sua variável de saída a nil antes do try e liberte-a no finally, como o exemplo acima faz, para que uma falha a meio não o deixe a libertar uma referência indefinida ou a saltar a libertação por completo

Diagrama do ciclo de vida do handle TPdf devolvido pelo ImportNPagesToOne do PDFium, com o caminho seguro de uma só libertação frente ao caminho de crash por dupla libertação
O TPdf devolvido é um segundo documento cuja propriedade é de quem chama: inicialize-o a nil, verifique-o, e liberte o invólucro por um só caminho

Reordenar páginas sem as reescrever

A imposição constrói um documento novo. A reordenação altera um documento no lugar. MovePages levanta um conjunto de páginas das suas posições atuais e deixa-as cair num destino, deslocando tudo o resto à volta do bloco movido para que a contagem de páginas se mantenha:

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Os índices começam em zero. PageIndices lista as páginas a mover, pela ordem em que devem ficar, e DestPageIndex é o índice em que a primeira página movida aterra depois de a mudança assentar. Como o PDFium relocaliza as páginas em vez de copiar e voltar a comprimir o seu conteúdo, a operação é barata 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 com reordenação por arrasto, onde um utilizador puxa uma miniatura para uma nova posição e confirma a nova ordem com um único movimento. Devolve False quando um índice está fora do intervalo, pelo que deve validar o resultado em vez de assumir que o rearranjo pegou

PDFium Component: mapa de reordenação do MovePages a mover a página quatro do PDF para o índice zero de um relatório de cinco páginas enquanto as restantes páginas deslizam uma posição para a direita
O MovePages levanta a página 4 para o índice 0 enquanto as restantes páginas deslizam uma posição e a contagem de páginas continua 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 ficheiro de 5 páginas) para a 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. ImportPagesByIndex recebe o documento de origem e um vetor de índices baseado em zero, e insere essas páginas no destino numa posição escolhida:

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

Chama-o sobre o documento de destino e passa a origem como primeiro argumento. PageIndices nomeia as páginas de origem a extrair, pela ordem em que as quer; InsertAt é a posição baseada em zero no destino onde entra a primeira página importada, pelo que 0 coloca-as antes da primeira página existente e a contagem de páginas atual do destino acrescenta ao fim. Um vetor vazio importa todas as páginas, o que torna a chamada uma cópia completa quando precisa de uma. Devolve False se algum índice estiver fora do intervalo na origem

É aqui que o contraste com a divisão importa. Dividir escreve ficheiros separados, uma operação a produzir muitas saídas em disco. ImportPagesByIndex faz a forma oposta de trabalho: reúne um conjunto escolhido de páginas num único documento de destino em memória, que depois guarda uma só vez. Quando o trabalho é "dá-me as páginas 3, 7 e 12 como um PDF curto", este é o caminho direto, e envolve por baixo 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 um destino vazio

    // Extrai as páginas 3, 7 e 12 (2, 6, 11 em base zero) para o excerto.
    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 forma de ponta a ponta é a mesma nas três: abrir a origem definindo FileName e passando Active a True, executar a operação, guardar com SaveAs, e libertar o que lhe pertence. O ramo que exige cuidado é saber que chamadas alocam um documento novo. MovePages altera o documento que já tem em mão, pelo que há um objeto a libertar. ImportPagesByIndex escreve num destino que criou por si, pelo que liberta a origem e o destino que abriu. ImportNPagesToOne é a exceção, porque o documento novo é o valor de retorno do método e não algo que tenha construído, e esquecer que é um handle separado cuja propriedade é de quem chama é como acontecem tanto a fuga como a dupla libertação. Inicialize o resultado a nil, verifique-o depois da chamada, e liberte-o num só caminho

Se o trabalho que tem em mãos é combinar ficheiros inteiros em vez de rearranjar páginas, veja juntar vários ficheiros PDF num só documento. Se é o inverso, partir um documento em vários ficheiros, veja dividir documentos PDF em vários ficheiros. Os métodos de imposição e reordenação aqui descritos são fornecidos como parte do PDFium Component para Delphi e C++Builder, a par das APIs de carregamento, renderização e edição abordadas noutros pontos deste blogue