Artigo Técnico

Colacionar Digitalizações Duplex em Delphi: Mescla PDF Intercalada

CollateDocumentsEx na biblioteca PDFlibPas para Delphi mescla vários documentos abertos em um único documento intercalado. Ela anexa GroupSize páginas de cada origem por rodada, aceita uma lista de intervalos de páginas por origem, e trata um intervalo decrescente como 3-1 como uma inversão dessa origem. Uma única chamada transforma uma pilha frontal e uma pilha de verso invertida em ordem de leitura

O cenário por trás dessa API é mundano e extremamente comum. Um scanner alimentado por folhas com percurso somente de um lado digitaliza a pilha inteira virada para baixo, depois o operador vira a pilha e a passa novamente. Você acaba com dois PDFs: as frentes em ordem, os versos em ordem inversa. O arquivo que o usuário quer é um único arquivo, página 1 frente, página 1 verso, página 2 frente, e assim por diante. Este artigo trata do problema de ordenação e da armadilha de duplicação de recursos que fica por baixo dele. Se sua preocupação é a taxa de transferência bruta de concatenação, veja mesclagem rápida de PDF por deslocamento de referências em nível de byte; se as entradas são grandes demais para caber inteiramente na memória, veja mesclagem e divisão de PDFs de gigabytes com acesso direto

O scanner produz duas pilhas, uma delas invertida

Colação não é mesclagem. Uma mesclagem concatena intervalos de páginas; uma colação os intercala, e o padrão de intercalação é uma propriedade do dispositivo físico que produziu a entrada. Errar o padrão não deixa o arquivo levemente errado, ele fica ilegível: cada segunda página pertence a uma folha diferente. Três variáveis descrevem quase todo caso real: quantas origens estão no rodízio, quantas páginas vêm de cada origem por rodada, e se alguma origem precisa ser lida de trás para frente. CollateDocuments cobre as duas primeiras com um array simples de handles de documento e um inteiro GroupSize. CollateDocumentsEx adiciona a terceira aceitando uma lista de intervalos de páginas separados por ponto e vírgula, um segmento por origem, onde um segmento vazio significa todas as páginas dessa origem e um intervalo decrescente a inverte. Ambas as funções anexam ao final do documento atualmente selecionado e retornam 1 em caso de sucesso, 0 em qualquer rejeição

Por que a colação ingênua multiplica o tamanho do arquivo?

Porque o mapa de importação que relaciona números de objeto da origem aos números de objeto do destino é reconstruído a cada chamada de cópia, e tudo que é alcançável a partir de mais de um trecho é importado uma vez por trecho. Dentro do PDFlibPas, TPDFDocument.CopyPagesFromDoc reinicia sua NewIndObjList no início de cada invocação. Essa lista é a única memória que o copiador tem daquilo que já trouxe. Chame-a uma vez com um intervalo de dez páginas e uma fonte compartilhada por todas as dez páginas é incorporada uma vez. Chame-a dez vezes com uma página cada e essa mesma fonte é incorporada dez vezes. Isso importa muito mais para digitalizações do que para documentos de texto, porque uma página digitalizada é um único XObject de imagem grande e os objetos compartilhados são os que têm peso real: um perfil ICC incorporado, uma cadeia /DecodeParms compartilhada, um carimbo ou marca d'água em forma de XObject aplicado a cada folha, a fonte da camada de texto OCR. A forma óbvia de escrever uma colação em rodízio é um laço sobre as rodadas, e esse laço é exatamente o caso patológico

// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
var
  RoundIndex: Integer;
begin
  for RoundIndex := 1 to 12 do
  begin
    PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
    PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
  end;
end;

Doze rodadas, duas origens, vinte e quatro mapas de importação. Nada avisa você. A ordem das páginas está correta, cada página renderiza, e o único sintoma é um arquivo várias vezes maior que a soma de suas entradas. Em um trabalho em lote de 300 páginas o multiplicador não é um erro de arredondamento, é a diferença entre um arquivo que cabe no orçamento de retenção e um que não cabe

Importar uma vez, depois reordenar a árvore de páginas

A correção é separar as duas preocupações que o laço ingênuo havia fundido. Copiar decide quais objetos existem no destino; ordenar decide onde as páginas ficam na árvore de páginas. CollateDocumentsEx copia cada origem exatamente uma vez, em uma única chamada CopyPagesFromDoc com o intervalo completo dessa origem, então cada origem ganha um mapa de importação e os recursos compartilhados são gravados uma vez. Só depois que todas as origens tiverem sido trazidas é que a intercalação acontece, e ela acontece inteiramente por meio de TPDFPageTree.MovePage

Mover páginas é gratuito no sentido que importa aqui. A ISO 32000-1 §7.7.3 define a árvore de páginas como uma estrutura balanceada de dicionários de nó cujos arrays /Kids guardam referências indiretas, com /Count carregando o total de folhas em cada nó. Realocar uma página significa remover uma referência indireta de um array /Kids, inseri-la em outro, ajustar os dois valores de /Count, e reapontar o /Parent da página. Nenhum content stream é tocado, nenhum recurso é duplicado, nenhum objeto é criado. O objeto de página mantém seu número de objeto, o que também é o motivo de os números de objeto permanecerem estáveis do jeito que ficam em substituição de páginas que preserva números de objeto. Há um detalhe adicional que um movimento de página ingênuo erra e que MovePage não erra. A ISO 32000-1 §7.7.3.4 permite que /Resources, /MediaBox, /CropBox e /Rotate sejam herdados de um nó ancestral em vez de declarados na própria página. Uma página que herda seus recursos do nó A e depois é movida para debaixo do nó B silenciosamente herda algo diferente, ou nada. MovePage portanto resolve o valor herdado e o grava no dicionário da página antes da realocação, de modo que a página carrega seus próprios atributos através do movimento

O que a passagem de reordenação realmente faz?

Ela executa um selection sort contra uma semântica de inserir-em. A ordem relativa de bloco desejada é calculada primeiro: percorre as origens em rodízio, pega até GroupSize índices de cada uma, pula uma origem que se esgotou, repete até que toda página esteja posicionada. Isso produz uma permutação sobre o bloco anexado. Aplicá-la é a parte trabalhosa, porque MovePage é uma inserção, não uma troca, então cada movimento desloca em um tudo entre a posição antiga e a nova

A implementação mantém um array Current modelando onde cada página anexada está atualmente, varre à frente a partir da posição K procurando a página que pertence a K, emite o movimento, depois desliza as entradas do array para espelhar o que o movimento fez na árvore. É O(n ao quadrado) em operações de array e zero em cópias de objeto, o que é a troca correta para essa carga de trabalho: uma colação de 500 páginas é um quarto de milhão de embaralhamentos de inteiros e nem um byte de dados de imagem duplicados. Intervalos decrescentes e páginas repetidas não precisam de tratamento especial nessa passagem porque PLParsePageRangeList é chamada com a ordenação desativada e duplicatas permitidas, então a ordem solicitada sobrevive intacta ao parsing

Intervalos invertidos e a mesclagem duplex de uma chamada só

Com a inversão expressa como um intervalo, o caso de digitalização de folha plana em passagem dupla se reduz a uma única chamada. As frentes querem sua ordem natural e os versos querem 12-1, e o primeiro segmento vazio antes do ponto e vírgula diz que a primeira origem contribui com todas as suas páginas

var
  PDF: TPDFlib;
  Target, Fronts, Backs: Integer;
begin
  PDF := TPDFlib.Create;
  try
    Target := PDF.NewDocument;
    if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
      Exit;
    Fronts := PDF.SelectedDocument;
    if PDF.LoadFromFile('backs.pdf', '') <> 1 then
      Exit;
    Backs := PDF.SelectedDocument;
    PDF.SelectDocument(Target);
    // fronts 1..12 in order, backs scanned in reverse: F1 B12 F2 B11 ...
    if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
      PDF.SaveToFile('duplex.pdf');
  finally
    PDF.Free;
  end;
end;

Dois comportamentos nesse trecho merecem menção explícita. As páginas colacionadas são anexadas ao documento selecionado, então um documento criado com NewDocument contribui com sua página em branco inicial antes delas, e você deve excluí-la se não a quiser. E as origens podem ser desiguais: com GroupSize 2 sobre uma origem de três páginas e uma de cinco páginas, as rodadas saem como A1 A2 B1 B2, depois A3 B3 B4 quando A está quase esgotada, depois B5 sozinha, porque uma origem esgotada é simplesmente pulada em vez de preenchida

Rollback, campos de formulário, e o que não vem junto

Todo argumento é validado antes de o destino ser tocado. Um handle de documento ausente, o documento selecionado listado como sua própria origem, um GroupSize abaixo de um, uma contagem de segmentos que não bate com a contagem de origens, um intervalo nomeando uma página que a origem não tem: todos esses retornam 0 com o destino inalterado. Falha durante a cópia é o caso mais difícil, e é tratada através do DeletePages público em vez do PageTree.DeletePages bruto. O motivo é específico. A cópia roda com MergeFormData ativado, então os campos de formulário da origem já foram anexados ao array /AcroForm /Fields do destino no momento em que uma origem posterior falha. Excluir as páginas no nível da árvore de páginas removeria as páginas de widget e deixaria essas referências de campo penduradas; o caminho público desvincula as referências de campo, esboço e cadeia de artigos junto com as páginas

if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
  // Nothing was appended and the target is byte-identical to before.
  // 412 is the copy failure; 0 means the arguments were rejected
  // during validation, before any page was touched.
  Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));

Seja honesto com seus usuários sobre os limites. A colação carrega páginas, suas anotações e seus campos de formulário, e mescla a lista de campos do AcroForm, o array de ordem de cálculo e o dicionário de recursos padrão. Ela não carrega marcadores de origem: a árvore de esboço de uma pilha frontal digitalizada é quase sempre vazia, então nada se perde no caso duplex, mas se você colacionar dois documentos autorados seus esboços ficam para trás e você reconstrói a navegação por conta própria. Destinos nomeados que existiam apenas no catálogo da origem estão na mesma posição. Planeje isso antes de prometer a um cliente uma colação sem perdas

O PDFlibPas embarca as funções de colação junto com o restante de sua superfície de montagem de páginas, então o fluxo de trabalho de digitalização, a extração baseada em intervalos e os caminhos de arquivos grandes ficam todos por trás de um único componente em Delphi e C++Builder. A referência completa da API e um build de avaliação estão na página do produto losLab Delphi PDF library