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

var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // snapshot taken before the transform

  Scale.a:= 0.9;   Scale.b:= 0.0;
  Scale.c:= 0.0;   Scale.d:= 0.9;
  Scale.e:= 29.7;  Scale.f:= 42.0;        // 5% margin, A4 in points
  Clip:= Pdf.GetPageBox(pbMedia);
  Pdf.TransformPageContent(Scale, Clip);

  // Info.Bounds still holds pre-transform geometry, and Info.Handle now
  // points into a page that TransformPageContent has already replaced
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

// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // unload text page, transform,
                                           // generate content, reload page
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // handle and bounds from the new parse
    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

Info:= Pdf.PageObjectInfo(I);

if Info.HasRotatedBounds then
  // RotatedBounds is array [1..4] of TPdfPoint, in draw order
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // the native call failed; fall back to the axis-aligned rectangle
  UseRect(Info.Bounds);

if Info.HasActiveState and (not Info.Active) then
  SkipObject(I);         // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive

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