Você adiciona um novo intervalo de rótulos ao manual de um fornecedor para que o apêndice leia App-a, App-b em vez de A-1, A-2, salva e reabre o arquivo. O page box continua dizendo A-1. Abrindo o catálogo em um editor de texto, o dicionário /PageLabels mostra o motivo: ele carrega um array /Kids herdado do produtor e, colado ao lado, um array /Nums novinho guardando seu intervalo. A ISO 32000-1 deixa a raiz de uma number tree ter uma daquelas chaves ou a outra, e um leitor que encontra /Kids na raiz desce por ela e não olha mais nada
Como um PDF guarda intervalos de page labels
Page labels são as strings que um visualizador mostra em seu page box em vez do índice físico da página, e a ISO 32000-1 §12.4.2 as mantém em uma number tree sob a chave de catálogo /PageLabels. Cada chave é um índice de página base zero onde um intervalo de rotulagem começa. Cada valor é um dicionário de rótulo com até três entradas: /S para o estilo de numeração (D, R, r, A ou a), /P para uma string de prefixo e /St para o número da primeira página do intervalo, que tem padrão 1. Um intervalo vai até a próxima chave. A especificação também exige uma entrada para o índice de página 0, então toda página do documento cai dentro de algum intervalo
Na PDF Library for Delphi o escritor é TPDFlib.AddPageLabels(Start, Style, Offset, Prefix), e ele se mapeia naquele dicionário com três regras que valem memorizar. Start é base 1 como todo argumento de página da biblioteca, e vai para a árvore como Start - 1. Style vai de 0 a 5: 0 não grava /S algum, então o rótulo é só o prefixo (útil para uma única página rotulada Cover), e 1 a 5 viram D, R, r, A e a. Um estilo fora de 0..5 faz a chamada devolver 0 e deixa o documento intocado. Offset vira /St apenas quando é maior que zero; passe 0 e a chave é omitida, o que todo leitor interpreta como 1
Um livro típico vindo de uma ferramenta de diagramação precisa de três intervalos: numerais romanos em minúsculas para o prefácio, números arábicos para o corpo e um apêndice com prefixo
uses
PDFlibrary;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('handbook.pdf', '') <> 1 then
Exit;
// Páginas 1-4: i, ii, iii, iv (romanos minúsculos)
Lib.AddPageLabels(1, 3, 1, '');
// Páginas 5-120: 1, 2, 3 ... (decimal, reiniciando em 1)
Lib.AddPageLabels(5, 1, 1, '');
// Páginas 121 em diante: A-1, A-2 ... (decimal com prefixo)
Lib.AddPageLabels(121, 1, 1, 'A-');
WriteLn(Lib.GetPageLabel(5)); // 1
WriteLn(Lib.GetPageLabel(122)); // A-2
Lib.SaveToFile('handbook-labeled.pdf');
finally
Lib.Free;
end;
end;
Page labels chegaram no PDF 1.3, então cada chamada também pede ao documento para elevar sua versão de saída para pelo menos 1.3. Se você travou a versão com LockSaveVersion, esse pedido é ignorado e o travamento se mantém, porque a biblioteca não sobrepuja uma versão que você escolheu de propósito. Se vai travar algo, trave em 1.3 ou superior em qualquer job que escreva rótulos
Por que um intervalo novo desaparece quando a raiz tem /Kids?
O intervalo desaparece porque a ISO 32000-1 §7.9.7 (Tabela 37) dá à raiz de uma number tree ou /Kids ou /Nums, nunca ambos, e os leitores escolhem /Kids quando presente. Produtores que escrevem documentos longos costumam dividir a árvore em nós intermediários, cada um com um par /Limits, pendurados em uma raiz que só tem /Kids. Uma rotina de inserção que só conhece /Nums olha para essa raiz, não acha array, cria um ao lado do /Kids existente e deposita o intervalo novo lá. A gravação tem sucesso. O arquivo continua parseável. Todo visualizador então desce o /Kids, vê os três intervalos originais do produtor e nunca toca no array extraviado, que é exatamente o sintoma do manual do fornecedor do parágrafo de abertura
A saída é parar de tratar a raiz como um contêiner que pode ganhar um segundo ponto de entrada. Quando o AddPageLabels encontra /Kids na raiz, ele primeiro percorre toda folha da árvore existente da esquerda para a direita e coleta cada chave com seu dicionário de rótulo, depois constrói um array /Nums único e achatado dessa lista. /Kids, /Limits e qualquer /Nums remanescente são removidos da raiz antes que o array achatado seja anexado, e só então o intervalo novo entra com uma inserção ordenada comum. Remover /Limits também importa: a Tabela 37 permite nos nós intermediários e folhas apenas, e uma raiz que o mantém está malformada mesmo que agora tenha /Nums
Um efeito colateral passa fácil despercebido. Se um arquivo chega até você já quebrado, com as duas chaves na raiz porque alguma ferramenta anterior fez a inserção ingênua, a caminhada pelas folhas segue o /Kids exatamente como um visualizador faria, então os intervalos do array extraviado não são carregados. Um visualizador nunca mostrou esses intervalos também, então os rótulos depois da edição batem com o que os leitores já viam
A árvore achatada não é reconstruída em nós /Kids balanceados depois, e achamos que é a decisão certa para essa árvore em particular. Balancear compensa para name trees com milhares de destinos ou arquivos embutidos. Uma árvore de page labels tem uma entrada por intervalo de rotulagem, e mesmo um manual de referência grande raramente precisa de mais que algumas dúzias; uma única folha também é o que a maioria dos produtores grava quando não está dividindo por dividir. Uma passagem de rebalanceamento adicionaria código que pode dar errado a cada salvamento, por um ganho de busca que ninguém conseguiria medir em uma árvore desse tamanho
// Rerotular o apêndice em um arquivo cuja raiz /PageLabels usa /Kids
if Lib.LoadFromFile('vendor-manual.pdf', '') = 1 then
begin
WriteLn('Before: ', Lib.GetPageLabel(121)); // ex.: A-1
// Mesma página de início do intervalo existente, então seu valor é substituído
if Lib.AddPageLabels(121, 5, 1, 'App-') = 1 then
begin
WriteLn('After: ', Lib.GetPageLabel(121)); // App-a
WriteLn('Front: ', Lib.GetPageLabel(2)); // ii, sem mudança
Lib.SaveToFile('vendor-manual-relabeled.pdf');
end;
end;
Como o intervalo novo começa no mesmo índice de página de uma chave existente, a inserção substitui o dicionário dessa chave em vez de adicionar uma segunda entrada para o índice de página 120. Os intervalos romano e decimal das folhas do produtor passam com seus dicionários originais, incluindo quaisquer valores /St
A caminhada pelas folhas tem de sobreviver a árvores malformadas também. Uma entrada /Kids que aponta de volta para um dos próprios ancestrais, ou duas ramificações que compartilham uma folha, aparece em arquivos de pipelines danificados. A travessia lembra quais nós já visitou, então um ciclo é percorrido uma vez e depois deixado para trás, e uma folha compartilhada contribui com seus intervalos uma vez. As mesmas preocupações para name trees estão cobertas em lidando com ciclos de name tree, /Limits inválidos e folhas grandes
Percorrendo um array /Nums em pares
Um array /Nums é uma sequência plana de entradas alternadas, [key0 value0 key1 value1 ...], e apenas as posições pares são chaves. Esse layout convida a um erro clássico. Um loop que avança um elemento por vez e pergunta "isto é um número?" vai comparar valores como se fossem chaves sempre que um valor por acaso for numérico. Se essa comparação ganha um teste de menor, o ponto de inserção cai em um índice ímpar, o novo par vai para o meio de um existente, e toda chave e valor posteriores trocam de lugar
Valores de page labels são dicionários, então o bug se esconde no /PageLabels em si. Ele aparece no dia em que o mesmo helper é reusado para uma number tree cujos valores são números puros, e nada na sintaxe do array avisa qual dos dois você está segurando. A PDF Library for Delphi lê chaves no índice X * 2 e valores no X * 2 + 1 tanto na caminhada pelas folhas quanto na inserção, e uma correspondência exata de chave substitui o valor e para a varredura. Se você escreve seu próprio código de number tree contra objetos crus, copie esse passo antes de qualquer outra coisa
Lendo rótulos de volta com GetPageLabel
TPDFlib.GetPageLabel(Page) devolve o rótulo de uma página base 1, e seus dois fallbacks se comportam de forma diferente de propósito. Um documento sem entrada /PageLabels alguma devolve o número decimal da página, então um chamador pode usar a função incondicionalmente para exibição. Um documento que tem uma árvore mas nenhum intervalo cobrindo a página pedida devolve uma string vazia. Esse segundo caso é o que um arquivo produz quando pula a entrada obrigatória para o índice de página 0: as primeiras páginas não têm rótulo no sentido da especificação, e uma string vazia reporta isso com mais honestidade do que um número inventado. Sempre comece seu conjunto de rótulos na página 1
Estilos de letras seguem a especificação do PDF em vez das colunas de planilha. Depois de Z vem AA, depois BB, depois CC; a letra se repete em vez de transportar, então a página 53 de um intervalo A é AAA. Números romanos são gerados em maiúsculas para R e minúsculas para r, e o prefixo é prependido sem mudança nos dois casos
var
P: Integer;
Data: WideString;
begin
// Auditoria rápida do que um visualizador vai mostrar no page box
for P := 1 to Lib.PageCount do
WriteLn(P, ' -> ', Lib.GetPageLabel(P));
// O valor de opção 4 exporta apenas os intervalos de rótulos, um bloco
// PageLabelBegin por intervalo
Data := Lib.ExportDocumentData(4);
// ... edite o texto, ou leve-o para outro arquivo ...
// O primeiro registro de rótulo limpa os intervalos existentes, depois cada
// registro é reproduzido via AddPageLabels; devolve os registros aplicados
WriteLn(Lib.ImportDocumentData(Data, 4), ' label ranges applied');
end;
Para edições em massa, o ExportDocumentData com valor de opção 4 grava cada intervalo como um bloco PageLabelBegin seguido de PageLabelNewIndex (a página de início base 1), PageLabelStart, linhas opcionais de PageLabelPrefix e PageLabelNumStyle, usando nomes como LowercaseRomanNumerals e DecimalArabicNumerals. O ImportDocumentData trata o primeiro registro de rótulo que encontra como uma substituição completa: chama ClearPageLabels uma vez, o que remove a entrada /PageLabels inteira do catálogo, e então alimenta cada registro ao AddPageLabels. A árvore reconstruída é uma folha única desde o início, então uma ida e volta textual por um original em forma de /Kids volta na mesma forma plana que a rota de rerotulação produz
Ordem das folhas e movimentos de página depois da rerotulação
O achatamento mantém os intervalos na ordem em que os encontra. A §7.9.7 exige chaves em ordem ascendente, mas arquivos com folhas fora de ordem circulam, e o GetPageLabel aplica o último intervalo dessa ordem cuja chave está no índice da página ou abaixo. Para um arquivo que declara seus intervalos como [0 roman, 120 A-, 4 decimal], com o intervalo decimal começando em 1, a página 122 resolve para o intervalo decimal e lê 118 onde você esperava A-2. Se a auditoria página a página acima mostrar rótulos assim, não remendeie em volta com mais uma chamada AddPageLabels. Chame ClearPageLabels e escreva o conjunto completo de novo em ordem ascendente; quatro chamadas custam menos que raciocinar sobre qual intervalo vence
Rótulos também estão vinculados a índices de página, não a objetos de página. Qualquer coisa que mude a contagem ou ordem de páginas deixa os intervalos exatamente onde estavam, apontando para as páginas que agora sentam nesses índices. Uma troca in-place como substituir páginas preservando números de objeto mantém a contagem, então os rótulos ficam alinhados. Uma mesclagem como colar scans duplex intercalados produz uma nova sequência de páginas, e essa saída deveria receber um conjunto de intervalos recém-escrito depois da mesclagem, começando na página 1
As chamadas de rótulos, o achatamento de /Kids e a exportação e importação de dados de documento mostradas aqui fazem parte da PDF Library for Delphi, que mira Delphi, C++Builder e Lazarus; sua entrada de referência para o AddPageLabels lista os valores de estilo e códigos de retorno em uma tabela