Artigo Técnico

Handles de Objeto de Página PDFium Obsoletos Após Transformação em Delphi

Quando FPDFPage_TransFormWithClip reescreve uma página, cada handle FPDF_PAGEOBJECT que já detém continua a descrever a análise de antes da transformação. O PDFium Component para Delphi e C++Builder resolve isto dentro de TransformPageContent, que descarrega a página de texto, regenera o conteúdo, depois recarrega a página para que consultas posteriores vejam as novas coordenadas

O sintoma é silencioso. Aplica uma escala de 0,9 para acrescentar uma margem de impressão, depois lê PageObjectInfo e obtém exatamente os números que tinha antes da chamada. Sem exceção, sem código de erro, nada num registo. Esta é uma falha diferente da cache de página de texto descrita em o artigo sobre páginas de texto obsoletas após uma edição: ali a cache é um único handle FPDF_TEXTPAGE que pode largar e reconstruir, aqui o problema é cada handle de objeto de página nas suas próprias variáveis, mais uma classe de getters que reportam falha através de um código de retorno que a maioria de quem chama descarta

Por que ficam os limites de objeto de página obsoletos sem erro?

Porque um handle de objeto de página é um ponteiro para uma representação analisada de um stream de conteúdo específico, e uma transformação de página inteira substitui esse stream de conteúdo por um novo. O PDFium não percorre a sua pilha de chamadas à procura de handles a corrigir. Constrói um novo grafo de objetos e deixa o antigo exatamente como estava, pelo que uma leitura contra o handle antigo é uma leitura perfeitamente válida de uma estrutura que já não corresponde ao que o ficheiro diz

A ISO 32000-1 §7.8.2 define o stream de conteúdo como a sequência de operadores que desenha uma página, e a §8.3.3 define como a matriz de transformação atual mapeia o espaço do utilizador sobre o espaço do dispositivo. Uma transformação ao nível da página é expressa envolvendo e reescrevendo esses operadores, não editando coordenadas por objeto no lugar. Assim, as coordenadas que os objetos transportam podem não mudar de todo; o que muda é a matriz em vigor quando são desenhados. Qualquer handle que foi analisado sob a matriz antiga responde a perguntas de geometria sob a matriz antiga, e responde sem qualquer queixa

O que reescreve realmente FPDFPage_TransFormWithClip

Reescreve a página, não os seus instantâneos. FPDFPage_TransFormWithClip recebe uma FS_MATRIX e um retângulo de clip FS_RECTF e aplica ambos a todo o conteúdo da página. É a chamada certa para margens, escala de imposição, e normalizar uma página de tamanho estranho contra uma caixa alvo. É a chamada errada a que recorrer se espera que os handles existentes acompanhem, e vale também a pena lembrar que só toca no conteúdo da página: as anotações são uma camada separada e precisam de TransformPageAnnotations, que encaminha os mesmos seis coeficientes de matriz para FPDFPage_TransformAnnots

Diagrama mostrando como os handles FPDF_PAGEOBJECT Delphi ficam obsoletos quando o FPDFPage_TransFormWithClip substitui a stream de conteúdo da página PDF analisada
Um handle de objeto de página aponta para um fluxo de conteúdo analisado, e FPDFPage_TransFormWithClip substitui esse fluxo por inteiro — o handle antigo continua a responder sob uma matriz que já não existe
var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // instantâneo tirado antes da transformação

  Scale.a:= 0.9;   Scale.b:= 0.0;
  Scale.c:= 0.0;   Scale.d:= 0.9;
  Scale.e:= 29.7;  Scale.f:= 42.0;        // margem de 5%, A4 em pontos
  Clip:= Pdf.GetPageBox(pbMedia);
  Pdf.TransformPageContent(Scale, Clip);

  // Info.Bounds ainda contém a geometria pré-transformação, e Info.Handle agora
  // aponta para uma página que TransformPageContent já substituiu
end;

A ordem de atualização que TransformPageContent usa

Quatro passos, por esta ordem: descarregar a página de texto, transformar, gerar conteúdo, recarregar a página. TPdf.TransformPageContent executa exatamente essa sequência. Chama CheckPageActive, copia a matriz e o clip para as suas formas de registo nativas, chama UnloadTextPage, depois FPDFPage_TransFormWithClip, depois UpdatePage, que é o wrapper à volta de FPDFPage_GenerateContent, e finalmente ReloadPage

Cada passo ganha o seu lugar. UnloadTextPage vai primeiro porque a FPDF_TEXTPAGE em cache contém caixas de carateres calculadas sob a matriz antiga, e também descarta a lista de hiperligações web derivada e qualquer sessão de procura em curso que foram construídas a partir dela. FPDFPage_GenerateContent tem de correr antes do recarregamento, porque a transformação vive na página em memória até ser serializada de volta para o stream de conteúdo, e um recarregamento de outro modo voltaria a analisar o stream não modificado. ReloadPage fecha com FPDF_LoadPage contra o índice de página atual, que é a única coisa que realmente lhe dá um grafo de objetos novo

Diagrama da ordem de atualização em quatro passos no TransformPageContent: descarregar a página de texto, transformar, gerar conteúdo e recarregar a página PDFium
TransformPageContent corre quatro passos numa ordem fixa, e só o FPDF_LoadPage final produz um grafo de objetos fresco para consultar
// Depois da transformação, reenumere. Não reutilize nada capturado anteriormente.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // descarregar a página de texto, transformar,
                                           // gerar conteúdo, recarregar a página
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // handle e limites da nova análise
    if Info.Bounds.Right> PageWidth then
      Log('object '+ IntToStr(I)+ ' still overflows after scaling');
  end;
end;

Um detalhe em ReloadPage vale a pena copiar se algum dia escrever esta sequência você mesmo. Carrega a nova página primeiro e só a compromete ao campo depois, pelo que um carregamento de página que falhe deixa a página nativa atual e todas as suas caches derivadas intactas em vez de o largar num estado meio desmantelado. Recarregar não é grátis — está a pagar por uma nova análise completa da página — mas é pago uma vez por transformação, não uma vez por consulta, e não há alternativa correta mais barata

Não transporte handles através do recarregamento

Após o recarregamento, os handles antigos não estão meramente obsoletos, estão pendurados. A FPDF_PAGE anterior foi fechada, e os valores FPDF_PAGEOBJECT que lhe pertenciam são ponteiros para memória libertada. TPdfPageObjectInfo expõe o handle nativo no seu campo Handle, que é genuinamente útil para passar um objeto diretamente para uma chamada de nível mais baixo, e igualmente genuinamente perigoso de manter num campo de formulário ou numa lista através de uma operação que recarrega a página. Trate um registo de instantâneo como válido apenas até à próxima chamada que regenera conteúdo, no mesmo espírito das regras de propriedade discutidas em as notas sobre ABI e segurança de memória na fronteira PDFium

Pode um getter falhar e continuar a parecer dados válidos?

Sim, e esta é a segunda metade do mesmo problema. FPDFPageObj_GetRotatedBounds e FPDFPageObj_GetIsActive são getters de parâmetro de saída: devolvem uma flag de sucesso int e escrevem a resposta real num argumento de referência. Ambos podem devolver FALSE para um objeto que foi criado mas cuja página ainda não foi reanalisada. Quando isso acontece, o parâmetro de saída fica intocado, e um registo Pascal inicializado com Default(TPdfPageObjectInfo) é todo zeros, pelo que quem chama vê um quadrilátero com quatro pontos na origem e uma flag Active de False. Uma chamada falhada foi silenciosamente promovida a dados plausíveis

TPdfPageObjectInfo responde a isto com sentinelas explícitas. HasRotatedBounds transporta o resultado da chamada FPDFPageObj_GetRotatedBounds, HasActiveState transporta o resultado da chamada FPDFPageObj_GetIsActive, e os campos de geometria e estado só são escritos quando a sentinela correspondente é True. A mesma forma repete-se ao longo do registo para os outros getters de parâmetro de saída, pelo que HasMatrix, HasFillColor, HasStrokeColor, e HasStrokeWidth significam todos a mesma coisa: a chamada nativa teve sucesso e o campo vizinho é significativo

Diagrama comparando getters raw de parâmetros de saída do PDFium que devolvem silenciosamente registos com predefinições com os campos sentinela Has de TPdfPageObjectInfo em Delphi
Os getters de parâmetro de saída reportam falha através de um código de retorno, pelo que TPdfPageObjectInfo emparelha cada campo derivado com um sentinela Has explícito
Info:= Pdf.PageObjectInfo(I);

if Info.HasRotatedBounds then
  // RotatedBounds é array [1..4] de TPdfPoint, em ordem de desenho
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // a chamada nativa falhou; recorre ao retângulo alinhado aos eixos
  UseRect(Info.Bounds);

if Info.HasActiveState and (not Info.Active) then
  SkipObject(I);         // genuinamente inativo
// se HasActiveState for False, o estado do objeto é desconhecido, não inativo

O padrão generaliza-se a cada getter do PDFium que segue a convenção de código de retorno mais parâmetro de saída, e há muitos deles. Se um wrapper colapsa essa convenção num resultado de função simples, deitou fora o único sinal que distingue "a resposta é zero" de "não há resposta". Transportar um booleano extra por campo custa um byte e remove uma categoria inteira de bugs em que um registo predefinido é confundido com uma medição

Onde isto ainda morde

Três limites honestos. Primeiro, a atualização é por página: transforme a página dois e quaisquer handles que esteja a manter para a página um permanecem inafetados, mas agora tem duas páginas analisadas em momentos diferentes e cabe-lhe a si lembrar quais instantâneos vieram de quais. Segundo, a estabilidade de índice não é garantida através de uma regeneração de conteúdo — depois do recarregamento, o índice 3 é seja o que for que o índice 3 é na nova análise, pelo que deve reidentificar objetos pelo seu tipo e geometria em vez de assumir que as posições se mantiveram. Terceiro, o retângulo de clip em FPDFPage_TransFormWithClip é aplicado ao conteúdo da página e não redimensiona nenhuma das caixas de página; se reduzir o conteúdo em escala para criar uma margem, a MediaBox continua a ser o tamanho que sempre foi, e um visualizador vai mostrar a folha original com o desenho encolhido lá dentro. Nada disto é exótico — é a consequência comum de uma API C que entrega ponteiros para estado analisado e deixa o tempo de vida a cargo de quem chama. A correção é a que funciona em todo o lado: defina exatamente quando um instantâneo expira, atualize nessa fronteira, e nunca deixe uma chamada falhada passar por um valor

Se estiver a trabalhar com o comportamento de matrizes mais em geral, a ordem de multiplicação que decide onde uma transformação aterra está coberta em o artigo sobre prepend, append e pivot com matrizes. As APIs de transformação e objeto de página aqui descritas são disponibilizadas com o PDFium Component para Delphi e C++Builder, cuja página de produto contém a referência completa para o registo de instantâneo de objeto de página e os seus campos sentinela