Substituir a página 3 de um contrato já assinado não deveria mover o sumário. Exclua a página antiga, insira a nova, e todo bookmark que apontava para lá agora cai em outro lugar. A PDFlibPas Delphi PDF library evita isso mantendo o próprio objeto de página alvo e transferindo apenas as entradas que carregam conteúdo visual
Por que os bookmarks quebram depois de substituir uma página de PDF?
Os bookmarks quebram porque um destino de PDF nomeia uma página por referência indireta a objeto, não por número de página. A ISO 32000-1 §12.3.2.2 define um destino explícito como um array cujo primeiro elemento é uma referência indireta ao objeto de página. Exclua esse objeto e anexe um substituto, e a referência fica pendurada: a maioria dos leitores responde jogando o leitor de volta na página 1, que é exatamente o sintoma que as pessoas relatam depois de uma substituição via exclusão-e-inserção. A árvore de páginas parece perfeita, a contagem de páginas está certa, a renderização está certa, e a camada inteira de navegação está silenciosamente errada
Destinos nomeados também não te salvam. A §12.3.2.3 roteia um nome pela name tree /Dests no catálogo do documento, mas a folha para a qual esse nome resolve ainda é um array de destino explícito contendo a mesma referência de página. Nomear adiciona uma camada de indireção acima da referência de página, não ao redor dela. O mesmo raciocínio cobre o resto da camada interativa descrita na §12.5: uma anotação de link carrega um /Dest ou uma ação GoTo /A cujo /D é esse array, toda anotação pode carregar uma entrada /P que é uma referência indireta à sua página, e um widget de campo de formulário é uma anotação exatamente na mesma condição. Uma troca ingênua de página desconecta quatro subsistemas de uma vez, e se você quiser ver isso enumerado em um arquivo real, o mesmo grafo de objetos é o que a introspecção de outline e anotação percorre
Quais entradas de página carregam identidade e quais carregam aparência
Um dictionary de página mistura dois tipos de entrada, e uma substituição no lugar tem sucesso precisamente quando você as separa. O lado da aparência é finito e enumerável: /Contents, /Resources, as cinco caixas de página /MediaBox, /CropBox, /BleedBox, /TrimBox e /ArtBox, mais /Rotate, /Group, /UserUnit e /BoxColorInfo. Essas onze entradas decidem tudo que um rasterizador produz para a página, e mais nada no arquivo aponta para elas pelo nome
O lado da identidade é aquilo a que o resto do documento se vinculou: o número de objeto e geração da página, o link de volta /Parent para a árvore de páginas, e /Annots. A PDFlibPas mantém cada um deles intocado. ReplacePageRanges purga as onze entradas visuais do dictionary da página alvo e as reinsere a partir da página de origem importada, então o objeto de página alvo é mutado no lugar em vez de substituído. A estrutura da árvore de páginas exigida pela §7.7.3 também permanece idêntica byte a byte em forma: a ordem de /Kids, /Count, e cada /Parent sobrevivente são os mesmos antes e depois, porque nenhum nó jamais foi desvinculado
Como a PDFlibPas substitui uma página sem renumerar objetos?
A chamada recebe um documento de origem, uma página inicial alvo baseada em 1, uma expressão de intervalo de origem, e uma flag de opções. Ambos os documentos precisam estar abertos na mesma instância, e o documento alvo é o selecionado. Como a contagem de páginas do alvo nunca muda, o intervalo que você solicita precisa caber dentro do documento a partir de TargetStartPage, e isso é checado antes de qualquer coisa ser criada
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
// The document whose bookmarks and links must survive
if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
Exit;
TargetDoc := Lib.SelectedDocument;
// The revised clause page, rendered by whatever produced it
if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
Exit;
SourceDoc := Lib.SelectedDocument;
Lib.SelectDocument(TargetDoc);
// Source page 1 overwrites the visuals of target page 3.
// Page count, page 3 object number, bookmarks and annotations are kept.
if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
Lib.SaveToFile('contract-final.pdf');
finally
Lib.Free;
end;
end;
Internamente, as páginas de origem não podem simplesmente ser lidas atravessando limites de documento, porque toda referência indireta dentro delas pertence à numeração de objetos do documento de origem. Então o intervalo de origem é primeiro importado da forma comum, como páginas temporárias anexadas após a última página real, o que roda o remapeamento completo do grafo de objetos: content streams, fontes, XObjects, shadings e color spaces são todos renumerados para o documento alvo. Só então as onze entradas visuais são copiadas de cada página temporária para sua página alvo, e só então as páginas temporárias são desvinculadas da árvore de páginas. O trabalho de remapeamento acontece onde é barato e seguro, e a edição destrutiva é reduzida a uma troca no nível de dictionary em páginas que já existem
O caminho de exclusão que destruiria o que você acabou de transferir
Remover essas páginas temporárias é o passo que parece trivial e não é. O caminho comum de exclusão de página na biblioteca faz mais do que desvincular um nó: ele combina as camadas de cada página sendo excluída, esvazia o primeiro content stream, e recupera recursos que nenhuma outra página compartilha. Isso é comportamento correto para uma exclusão real, e catastrófico aqui, porque no momento em que as páginas temporárias são removidas as páginas alvo já referenciam exatamente esses content streams e objetos de recurso. Esvaziá-los deixaria em branco a página que você acabou de substituir, e a varredura de recursos coletaria fontes e imagens que agora têm um dono ativo
A correção é um modo de preservar-objetos-referenciados no caminho interno de exclusão. Quando ativado, a exclusão pula tanto a varredura de recursos não compartilhados quanto a limpeza de content stream, e não faz nada além de desanexar as páginas da árvore de páginas e corrigir a contabilidade da árvore. Os objetos transferidos sobrevivem com um novo dono, e a propriedade de objetos depois da operação é o que você desenharia em um quadro branco: um content stream, uma página dona, um número de objeto que nunca se moveu. As regras de ciclo de vida relacionadas para criar, excluir e reordenar páginas são cobertas separadamente nas notas sobre operações de ciclo de vida de documento e página
Ordenação, duplicatas, e falha tudo-ou-nada
A flag de opções seleciona como o intervalo de origem é interpretado. 0 ordena os números de página analisados e remove duplicatas, o que é o padrão sensato quando quem chama passa algo como '4-6,2' e simplesmente quer dizer essas quatro páginas. 1 preserva a ordem que você escreveu e permite que uma página se repita, então '2,1,2' genuinamente significa três substituições tiradas de duas páginas de origem. A validação roda primeiro e roda completamente: a sintaxe do intervalo, todo número de página contra a contagem de páginas de origem, o próprio valor da opção, e a capacidade do alvo são todos checados antes que um único objeto seja criado. Uma chamada rejeitada define LastErrorCode como 412, restaura a página previamente selecionada, e deixa o documento exatamente como estava
var
Replaced: Integer;
begin
Lib.SelectDocument(TargetDoc);
// Options = 1: source order is preserved and repeats are allowed, so
// target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
if Replaced = 0 then
raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
[Lib.LastErrorCode]);
// On success the selection is the first replaced page
Assert(Lib.SelectedPage = 5);
end;
A atomicidade se estende para além da validação, até a própria transferência. Antes que a primeira página de origem seja importada, as onze entradas visuais de toda página alvo no intervalo são capturadas como valores codificados. Se a importação falhar, ou se a contagem de páginas importadas não corresponder ao que foi solicitado, os snapshots são decodificados de volta nas páginas alvo e as páginas temporárias são removidas, então uma falha no meio do caminho ainda deixa os visuais originais no lugar em seus objetos originais. Isso importa mais do que parece: um intervalo de página meio substituído em um contrato é pior que uma chamada que falhou, porque nada no arquivo o marca como meio-feito
// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);
O que a substituição no lugar ainda não faz por você?
Anotações de origem, campos de formulário de origem e outlines de origem deliberadamente não são importados. Trazer um widget sem sua entrada de campo /AcroForm, ou uma anotação portadora de marked-content sem a propriedade de sua structure tree, produz um objeto interativo meio importado que nenhum visualizador consegue interpretar, então a operação transfere só a aparência. A consequência prática é que se a página de substituição deveria carregar novos campos de formulário ou novos links, você os adiciona à página alvo depois, contra o objeto de página alvo que ainda está lá esperando por eles
Mais dois limites valem a pena checar em seus próprios arquivos. Primeiro, /Annots é preservado mas a geometria da página não é, então substituir uma página de 220 mm por uma de 320 mm mantém os retângulos de anotação nas coordenadas antigas dentro de um /MediaBox de tamanho diferente; se a geometria mudar, reposicione as anotações que você manteve. Segundo, entradas fora das onze chaves visuais permanecem com a página alvo por design, o que está certo para /Trans ou /AA e desatualizado para /Thumb, então regenere thumbnails depois de uma substituição. Documentos tagueados precisam de um pensamento a mais: os elementos de estrutura ainda apontam para o objeto de página correto via /Pg, mas seus identificadores de marked-content descrevem conteúdo que não está mais lá, então uma troca de página dentro de um workflow PDF/UA é tanto uma edição de structure tree quanto uma edição de conteúdo. Se o seu trabalho é na verdade composição em vez de troca, sobrepondo arte em páginas que você mantém, a abordagem de page stitching e templates é a ferramenta mais barata
Tudo descrito aqui, incluindo a sintaxe da expressão de intervalo, os valores de opção e a API de manipulação de página ao redor, vem embutido na PDFlibPas Delphi PDF Library padrão para Delphi e C++Builder, cuja documentação de referência traz a entrada completa da chamada de substituição de página e seus códigos de erro