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
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, 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
// 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 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
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 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