CollateDocumentsEx na biblioteca PDF PDFlibPas para Delphi combina vários documentos abertos num único documento intercalado. Anexa GroupSize páginas de cada origem por ronda, aceita uma lista de intervalos de páginas por origem, e trata um intervalo descendente como 3-1 como uma inversão dessa origem. Uma única chamada transforma uma pilha frontal e uma pilha traseira invertida em ordem de leitura
O cenário por detrás desta API é banal e extremamente comum. Um scanner de alimentação de folhas com um percurso apenas de um lado digitaliza toda a pilha virada para baixo, depois o operador inverte a pilha e digitaliza-a de novo. Ficamos com dois PDFs: frentes em ordem, versos em ordem inversa. O ficheiro que o utilizador quer é um único, 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 se esconde por baixo dele. Se a sua preocupação for o débito de concatenação em bruto, consulte combinação rápida de PDF por deslocamento de referências ao nível do byte; se as entradas forem demasiado grandes para caber em memória, consulte combinar e dividir PDFs de vários gigabytes com acesso direto
O scanner produz duas pilhas, uma delas ao contrário
Colacionar não é combinar. Uma combinação concatena intervalos de páginas; uma colação intercala-os, 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 ficheiro ligeiramente errado, torna-o ilegível: cada segunda página pertence a uma folha diferente. Três variáveis descrevem quase todos os casos reais: quantas origens estão em rotação, quantas páginas vêm de cada origem por ronda, e se alguma origem precisa de ser lida ao contrário. CollateDocuments cobre as duas primeiras com um array simples de handles de documento e um inteiro GroupSize. CollateDocumentsEx acrescenta a terceira ao aceitar 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 descendente inverte-a. Ambas as funções anexam ao final do documento atualmente selecionado e devolvem 1 em caso de sucesso, 0 em qualquer rejeição
Por que razão a colação ingénua multiplica o tamanho do ficheiro?
Porque o mapa de importação que associa números de objeto da origem a números de objeto do destino é reconstruído em cada chamada de cópia, e tudo o que é alcançável a partir de mais do que um bloco é importado uma vez por bloco. Dentro do PDFlibPas, TPDFDocument.CopyPagesFromDoc reinicia a sua NewIndObjList no início de cada invocação. Essa lista é a única memória que o copiador tem daquilo que já trouxe. Chame-o uma vez com um intervalo de dez páginas e um tipo de letra partilhado por todas as dez páginas é incorporado uma vez. Chame-o dez vezes com uma página de cada vez e esse mesmo tipo de letra é incorporado dez vezes. Isto pesa muito mais em digitalizações do que em documentos de texto, porque uma página digitalizada é um único objeto de imagem XObject grande e os objetos partilhados são os que têm peso real: um perfil ICC incorporado, uma cadeia /DecodeParms partilhada, um carimbo ou marca de água em forma de XObject aplicado a cada folha, o tipo de letra da camada de texto OCR. A forma óbvia de escrever uma colação round-robin é um ciclo sobre rondas, e esse ciclo é 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 rondas, duas origens, vinte e quatro mapas de importação. Nada avisa. A ordem das páginas está correta, cada página é renderizada, e o único sintoma é um ficheiro várias vezes maior do que a soma das suas entradas. Num trabalho por lotes 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 consiste em separar as duas preocupações que o ciclo ingénuo tinha fundido. Copiar decide que objetos existem no destino; ordenar decide onde as páginas se sentam na árvore de páginas. CollateDocumentsEx copia cada origem exatamente uma vez, numa única chamada CopyPagesFromDoc com o intervalo completo dessa origem, pelo que cada origem obtém um mapa de importação e os recursos partilhados são escritos uma vez. Só depois de todas as origens terem chegado é que ocorre a intercalação, e ocorre inteiramente através de TPDFPageTree.MovePage
As deslocações de páginas são gratuitas no sentido que aqui importa. A ISO 32000-1 §7.7.3 define a árvore de páginas como uma estrutura balanceada de dicionários de nós cujos arrays /Kids contêm referências indiretas, com /Count a transportar o total de folhas em cada nó. Relocalizar uma página significa remover uma referência indireta de um array /Kids, inseri-la noutro, ajustar ambos os valores /Count e reapontar o /Parent da página. Nenhum stream de conteúdo é tocado, nenhum recurso é duplicado, nenhum objeto é criado. O objeto de página mantém o seu número de objeto, o que é também a razão pela qual os números de objeto permanecem estáveis do mesmo modo que em substituição de páginas que preserva números de objeto. Há um detalhe adicional que uma deslocação de página ingénua trata mal e que MovePage não trata mal. A ISO 32000-1 §7.7.3.4 permite que /Resources, /MediaBox, /CropBox e /Rotate sejam herdados de um nó ancestral em vez de estarem declarados na página. Uma página que herda os seus recursos do nó A e é depois deslocada para debaixo do nó B herda silenciosamente algo diferente, ou nada de todo. MovePage resolve por isso o valor herdado e escreve-o no dicionário da página antes da relocalização, pelo que a página transporta os seus próprios atributos durante a deslocação
O que faz realmente o passo de reordenação?
Executa uma ordenação por seleção contra semântica de inserção. A ordem relativa ao bloco desejada é calculada primeiro: percorre as origens em rotação, retira até GroupSize índices de cada uma, salta uma origem esgotada, repete até todas as páginas estarem colocadas. Isso produz uma permutação sobre o bloco anexado. Aplicá-la é a parte incómoda, porque MovePage é uma inserção, não uma troca, pelo que cada deslocação desloca tudo entre a posição antiga e a nova em uma unidade
A implementação mantém um array Current a modelar onde cada página anexada está atualmente sentada, procura para a frente a partir da posição K pela página que pertence a K, emite a deslocação, depois desliza as entradas do array para espelhar aquilo que a deslocação fez à árvore. É O(n ao quadrado) em operações de array e zero em cópias de objetos, o que é a troca correta para esta carga de trabalho: uma colação de 500 páginas é um quarto de milhão de baralhamentos de inteiros e nem um único byte de dados de imagem duplicados. Intervalos descendentes e páginas repetidas não precisam de tratamento especial neste passo porque PLParsePageRangeList é chamada com a ordenação desativada e duplicados permitidos, pelo que a ordem pedida sobrevive intacta à análise
Intervalos invertidos e a colação duplex numa só chamada
Com a inversão expressa como um intervalo, o caso do scanner de planos duplos colapsa numa única chamada. As frentes querem a sua ordem natural e os versos querem 12-1, e o segmento vazio antes do primeiro 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;
Vale a pena declarar explicitamente dois comportamentos naquele excerto. As páginas colacionadas são anexadas ao documento selecionado, pelo que um documento criado com NewDocument contribui com a sua página em branco inicial antes delas, e deve apagá-la se não a quiser. E as origens podem ser desiguais: com GroupSize 2 sobre uma origem de três páginas e outra de cinco páginas, as rondas saem A1 A2 B1 B2, depois A3 B3 B4 quando A está quase esgotada, depois B5 sozinha, porque uma origem esgotada é simplesmente saltada em vez de preenchida com padding
Reversão, campos de formulário, e o que não vem incluído
Cada argumento é validado antes de o destino ser tocado. Um handle de documento em falta, o documento selecionado listado como a sua própria origem, um GroupSize abaixo de um, uma contagem de segmentos que não corresponde à contagem de origens, um intervalo que nomeia uma página que a origem não tem: tudo isto devolve 0 com o destino inalterado. Uma falha durante a cópia é o caso mais difícil, e é tratada através do DeletePages público em vez do PageTree.DeletePages em bruto. A razão é específica. A cópia executa com MergeFormData ativado, pelo que os campos de formulário da origem já foram anexados ao array /AcroForm /Fields do destino no momento em que uma origem posterior falha. Apagar as páginas ao nível da árvore de páginas retiraria as páginas de widgets e deixaria essas referências de campo penduradas; o percurso público desliga a referência do campo, dos marcadores e das linhas de artigo juntamente 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 os seus utilizadores sobre as fronteiras. A colação transporta páginas, as suas anotações e os seus campos de formulário, e combina a lista de campos do AcroForm, o array de ordem de cálculo e o dicionário de recursos predefinido. Não transporta marcadores da origem: a árvore de estrutura de uma pilha frontal digitalizada é quase sempre vazia, pelo que nada se perde no caso duplex, mas se colacionar dois documentos autorados os respetivos marcadores ficam para trás e terá de reconstruir a navegação você mesmo. Os destinos nomeados que viviam apenas no catálogo da origem estão na mesma posição. Planeie isso antes de prometer a um cliente uma colação sem perdas
O PDFlibPas disponibiliza as funções de colação juntamente com o resto da sua superfície de montagem de páginas, pelo que o fluxo de trabalho do scanner, a extração baseada em intervalos e os percursos de ficheiros grandes se encontram todos por detrás de um único componente em Delphi e C++Builder. A referência completa da API e uma versão de avaliação estão disponíveis na página do produto losLab Delphi PDF library