Substituir a página 3 de um contrato já assinado não devia mover o índice. Eliminar a página antiga, inserir a nova, e cada marcador que antes apontava para lá agora aterra noutro sítio. A biblioteca PDFlibPas Delphi PDF evita isto mantendo o próprio objeto de página alvo e transferindo apenas as entradas que transportam conteúdo visual
Porque é que os marcadores partem depois de substituir uma página de PDF?
Os marcadores partem porque um destino de PDF nomeia uma página por referência indireta a objeto, não por número de página. O 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. Elimine esse objeto e acrescente um substituto, e a referência fica pendurada: a maioria dos leitores responde deixando cair o leitor na página 1, que é exatamente o sintoma que as pessoas relatam depois de uma substituição por eliminar-e-inserir. A árvore de páginas parece perfeita, o número de páginas está certo, a renderização está certa, e toda a camada de navegação está silenciosamente errada
Os destinos nomeados também não o salvam. O §12.3.2.3 encaminha um nome através da árvore de nomes /Dests no catálogo do documento, mas a folha para a qual esse nome resolve continua a ser um array de destino explícito com a mesma referência de página. Nomear acrescenta uma camada de indireção por cima da referência de página, não à volta dela. O mesmo raciocínio cobre o resto da camada interativa descrita no §12.5: uma anotação de link transporta um /Dest ou uma ação GoTo /A cujo /D é esse array, qualquer anotação pode transportar uma entrada /P que é uma referência indireta à sua página, e o widget de um campo de formulário é uma anotação exatamente nos mesmos termos. Uma troca de página ingénua desliga quatro subsistemas de uma só vez, e se quiser vê-los enumerados num ficheiro real, o mesmo grafo de objetos é o que a introspeção de outline, anotações e ações percorre
Que entradas de página transportam identidade e quais transportam aparência
Um dicionário de página mistura dois tipos de entradas, e uma substituição no próprio lugar só é bem-sucedida quando se separam. 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 o que um rasterizador produz para a página, e mais nada no ficheiro lhes aponta pelo nome
O lado da identidade é aquilo a que o resto do documento se ligou: o número de objeto e a geração da página, a ligação de retorno /Parent para a árvore de páginas, e /Annots. A PDFlibPas mantém cada um deles intocado. ReplacePageRanges purga as onze entradas visuais do dicionário da página alvo e volta a adicioná-las a partir da página de origem importada, pelo que o objeto da página alvo é alterado no próprio lugar em vez de substituído. A estrutura da árvore de páginas exigida pelo §7.7.3 também se mantém idêntica byte a byte na forma: a ordem de /Kids, o /Count e cada /Parent sobrevivente são os mesmos antes e depois, porque nenhum nó foi alguma vez desligado
Como é que a PDFlibPas substitui uma página sem renumerar objetos?
A chamada recebe um documento de origem, uma página inicial alvo com base em 1, uma expressão de intervalo de origem, e uma flag de opções. Ambos os documentos têm de estar abertos na mesma instância, e o documento alvo é o selecionado. Como o número de páginas do alvo nunca muda, o intervalo pedido tem de caber dentro do documento a partir de TargetStartPage, e isso é verificado 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 através das fronteiras de documento, porque cada referência indireta dentro delas pertence à numeração de objetos da origem. Por isso o intervalo de origem é primeiro importado da forma normal, como páginas temporárias acrescentadas depois da última página real, o que corre o remapeamento completo do grafo de objetos: content streams, tipos de letra, XObjects, shadings e espaços de cor são todos renumerados para o documento alvo. Só depois é que as onze entradas visuais são copiadas de cada página temporária para a sua página alvo, e só depois as páginas temporárias são desligadas da árvore de páginas. O trabalho de remapeamento acontece onde é barato e seguro, e a edição destrutiva reduz-se a uma troca ao nível do dicionário em páginas que já existem
O caminho de eliminação que destruiria o que acabou de transferir
Remover essas páginas temporárias é o passo que parece trivial e não é. O caminho normal de eliminação de páginas na biblioteca faz mais do que desligar um nó: combina as camadas de cada página a eliminar, esvazia o primeiro content stream, e recupera recursos que nenhuma outra página partilha. Isso é comportamento correto para uma eliminação real, e catastrófico aqui, porque quando as páginas temporárias são removidas as páginas alvo já referenciam exatamente esses content streams e objetos de recursos. Esvaziá-los apagaria a página que acabou de substituir, e a varredura de recursos recolheria tipos de letra e imagens que agora têm um dono ativo
A correção é um modo de preservar-objetos-referenciados no caminho interno de eliminação. Quando está ativo, a eliminação salta tanto a varredura de recursos não partilhados como a limpeza do content stream, e não faz nada além de desligar as páginas da árvore de páginas e corrigir a contabilidade da árvore. Os objetos transferidos sobrevivem com um novo dono, e a posse dos objetos depois da operação é o que se desenharia num 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, eliminar e reordenar páginas estão cobertas em separado nas notas sobre operações de ciclo de vida de documentos e páginas
Ordenação, duplicados, 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 duplicados, o que é a predefinição sensata quando o chamador passa algo como '4-6,2' e simplesmente quer dizer essas quatro páginas. 1 preserva a ordem escrita e permite que uma página se repita, pelo que '2,1,2' significa genuinamente três substituições retiradas de duas páginas de origem. A validação corre primeiro e corre por completo: a sintaxe do intervalo, cada número de página contra o número de páginas da origem, o próprio valor da opção, e a capacidade do alvo são todos verificados antes de sequer um objeto ser 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 estende-se para além da validação até à própria transferência. Antes de a primeira página de origem ser importada, as onze entradas visuais de cada página alvo no intervalo são capturadas como valores codificados. Se a importação falhar, ou o número de páginas importadas não coincidir com o que foi pedido, as capturas são descodificadas de volta para as páginas alvo e as páginas temporárias são removidas, pelo que uma falha a meio caminho ainda deixa os visuais originais no lugar, nos seus objetos originais. Isso importa mais do que parece: um intervalo de páginas meio-substituído num contrato é pior do que uma chamada falhada, porque nada no ficheiro 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 próprio lugar continua a não fazer por si?
As anotações, campos de formulário e outlines de origem não são importados de propósito. Trazer um widget sem a sua entrada de campo /AcroForm, ou uma anotação portadora de conteúdo marcado sem a sua posse na árvore de estrutura, produz um objeto interativo meio-importado que nenhum leitor consegue interpretar, pelo que a operação transfere apenas a aparência. A consequência prática é que, se a página de substituição deve trazer novos campos de formulário ou novos links, adicione-os à página alvo depois, contra o objeto de página alvo que continua ali à espera deles
Há mais duas fronteiras que vale a pena verificar nos seus próprios ficheiros. Primeiro, o /Annots é preservado mas a geometria da página não, pelo que substituir uma página de 220 mm por uma de 320 mm mantém os retângulos de anotação nas suas coordenadas antigas dentro de um /MediaBox de tamanho diferente; se a geometria mudar, reposicione as anotações que manteve. Segundo, as entradas fora das onze chaves visuais ficam com a página alvo por design, o que está certo para /Trans ou /AA e desatualizado para /Thumb, pelo que deve regenerar as miniaturas depois de uma substituição. Os documentos com tags precisam de um pensamento extra: os elementos de estrutura continuam a apontar para o objeto de página correto através de /Pg, mas os seus identificadores de conteúdo marcado descrevem conteúdo que já lá não está, pelo que uma troca de página num fluxo de trabalho PDF/UA é também uma edição da árvore de estrutura e não só uma edição de conteúdo. Se a sua tarefa é na verdade composição em vez de troca, sobrepor artwork em páginas que mantém, a abordagem de junção de páginas e templates é a ferramenta mais barata
Tudo o que aqui se descreve, incluindo a sintaxe da expressão de intervalo, os valores de opção e a API de manipulação de páginas que os rodeia, vem incluído na PDFlibPas Delphi PDF Library padrão para Delphi e C++Builder, cuja documentação de referência contém a entrada completa para a chamada de substituição de páginas e os seus códigos de erro