Artigo Técnico

Handles de Objeto de Página do PDFium Obsoletos Após Transform

Quando FPDFPage_TransFormWithClip reescreve uma página, todo handle FPDF_PAGEOBJECT que você já segura ainda descreve a análise de antes da transformação. O PDFium Component para Delphi e C++Builder resolve isso 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. Você aplica uma escala de 0,9 para adicionar uma margem de impressão, depois lê PageObjectInfo e obtém exatamente os números que obteve antes da chamada. Nenhuma exceção, nenhum código de erro, nada em um log. Esta é uma falha diferente da página de texto em cache descrita em o artigo sobre páginas de texto obsoletas após uma edição: ali o cache é um único handle FPDF_TEXTPAGE que você pode descartar e reconstruir, aqui o problema é todo 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 dos chamadores descarta

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

Porque um handle de objeto de página é um ponteiro para uma representação analisada de um content stream específico, e uma transformação de página inteira substitui esse content stream por um novo. O PDFium não percorre sua pilha de chamadas procurando handles para corrigir. Ele constrói um grafo de objetos novo e deixa o antigo exatamente como estava, então uma leitura contra o handle antigo é uma leitura perfeitamente válida de uma estrutura que não corresponde mais ao que o arquivo diz

A ISO 32000-1 §7.8.2 define o content stream 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 usuário no espaço do dispositivo. Uma transformação em nível de página é expressa envolvendo e reescrevendo esses operadores, não editando coordenadas por objeto no local. Então as coordenadas que os objetos carregam podem não mudar de forma alguma; 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 as responde sem reclamar

O que FPDFPage_TransFormWithClip realmente reescreve

Ele reescreve a página, não seus instantâneos. FPDFPage_TransFormWithClip recebe uma FS_MATRIX e um retângulo de recorte FS_RECTF e aplica ambos ao conteúdo da página inteira. É a chamada certa para margens, escala de imposição, e normalizar uma página de tamanho estranho contra uma caixa alvo. É a chamada errada para buscar se você espera que handles existentes acompanhem, e também vale a pena lembrar que ela toca apenas o conteúdo da página: 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 handles FPDF_PAGEOBJECT Delphi ficam obsoletos quando FPDFPage_TransFormWithClip substitui o fluxo de conteúdo da página PDF analisada
Um handle de objeto de página aponta para um content stream interpretado, e FPDFPage_TransFormWithClip substitui esse stream por inteiro — o handle antigo continua respondendo sob uma matriz que não existe mais
var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // snapshot 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, nesta 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 recorte para suas formas de registro nativas, chama UnloadTextPage, depois FPDFPage_TransFormWithClip, depois UpdatePage, que é o wrapper em torno de FPDFPage_GenerateContent, e finalmente ReloadPage

Cada passo ganha seu lugar. UnloadTextPage vai primeiro porque o FPDF_TEXTPAGE em cache mantém caixas de caractere calculadas sob a matriz antiga, e também descarta a lista de links web derivada e qualquer sessão de busca em andamento que foi construída a partir dela. FPDFPage_GenerateContent precisa rodar antes da recarga, porque a transformação vive na página em memória até ser serializada de volta no content stream, e uma recarga de outra forma reanalisaria 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 etapas em TransformPageContent: descarregar a página de texto, transformar, gerar conteúdo, então recarregar a página do PDFium
TransformPageContent roda quatro passos em uma ordem fixa, e apenas o FPDF_LoadPage final produz um grafo de objetos novo para consultar
// Depois da transformação, reenumere. Não reutilize nada capturado antes.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // descarrega a página de texto, transforma,
                                           // gera o conteúdo, recarrega a página
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // handle e bounds 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 você algum dia escrever essa sequência sozinho. Ele carrega a página nova primeiro e só a comita ao campo depois, então um carregamento de página que falha deixa a página nativa atual e todos os seus caches derivados intactos em vez de te jogar em um estado meio desmontado. Recarregar não é grátis — você está pagando por uma reaná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 carregue handles através da recarga

Depois da recarga, os handles antigos não estão apenas obsoletos, estão pendurados. A FPDF_PAGE anterior foi fechada, e os valores FPDF_PAGEOBJECT que pertenciam a ela são ponteiros para memória liberada. TPdfPageObjectInfo expõe o handle nativo em 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 em um campo de formulário ou em uma lista através de uma operação que recarrega a página. Trate um registro de instantâneo como válido apenas até a próxima chamada que regenera o conteúdo, no mesmo espírito das regras de propriedade discutidas em as notas sobre ABI e segurança de memória na fronteira do PDFium

Um getter pode falhar e ainda parecer dados válidos?

Sim, e essa é a segunda metade do mesmo problema. FPDFPageObj_GetRotatedBounds e FPDFPageObj_GetIsActive são getters de parâmetro de saída: eles retornam um sinalizador de sucesso int e escrevem a resposta real em um argumento de referência. Ambos podem retornar 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 registro Pascal inicializado com Default(TPdfPageObjectInfo) é todo zeros, então o chamador vê um quadrilátero com quatro pontos na origem e um sinalizador Active de False. Uma chamada falha foi silenciosamente promovida a dados de aparência plausível

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

Diagrama comparando getters de out-parameters brutos do PDFium que silenciosamente retornam records com valores padrão com os campos sentinel Has de TPdfPageObjectInfo em Delphi
Getters de parâmetro de saída reportam falha através de um código de retorno, então 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, na ordem de desenho
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // a chamada nativa falhou; recorra 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 se generaliza para todo 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 em um simples resultado de função, ele descartou o único sinal que distingue "a resposta é zero" de "não há resposta". Carregar um booleano extra por campo custa um byte e remove uma categoria inteira de bug onde um registro padronizado é confundido com uma medição

Onde isso ainda morde

Três limites honestos. Primeiro, a atualização é por página: transformar a página dois e quaisquer handles que você esteja segurando para a página um não são afetados, mas você agora tem duas páginas analisadas em momentos diferentes e cabe a você lembrar quais instantâneos vieram de quais. Segundo, estabilidade de índice não é garantida através de uma regeneração de conteúdo — depois da recarga, o índice 3 é o que quer que o índice 3 seja na nova análise, então reidentifique objetos por seu tipo e geometria em vez de assumir que posições se mantiveram. Terceiro, o retângulo de recorte em FPDFPage_TransFormWithClip é aplicado ao conteúdo da página e não redimensiona nenhuma das caixas de página; se você reduzir o conteúdo em escala para criar uma margem, a MediaBox ainda é do tamanho que sempre foi, e um visualizador vai mostrar a folha original com o desenho encolhido dentro dela. Nada disso é exótico — é a consequência ordinária de uma API C que entrega ponteiros para estado analisado e deixa a vida útil para o chamador. A correção é a que funciona em todo lugar: defina exatamente quando um instantâneo expira, atualize nessa fronteira, e nunca deixe uma chamada falha se disfarçar de valor

Se você está trabalhando com comportamento de matriz de forma mais geral, a ordem de multiplicação que decide onde uma transformação cai é coberta em o artigo sobre prepend, append e pivot com matrizes. As APIs de transformação e objeto de página descritas aqui são fornecidas com o PDFium Component para Delphi e C++Builder, cuja página de produto traz a referência completa para o registro de instantâneo de objeto de página e seus campos sentinela