Artigo Técnico

Rotação Dupla e Bugs de Zoom de Ajuste no PDFium em Delphi

A função FPDF_RenderPageBitmap do Componente PDFium aceita um argumento rotate que o PDFium sempre adiciona sobre qualquer rotação que a página já carregue em sua própria entrada /Rotate, de modo que ler a rotação armazenada de uma página e alimentar esse mesmo valor de volta na chamada de renderização gira a página duas vezes. O erro idêntico aparece na matemática de zoom de ajuste: dimensionar uma miniatura a partir da largura e altura não rotacionadas da página produz a proporção errada sempre que /Rotate é 90 ou 270 graus, porque o bitmap renderizado sai com largura e altura trocadas

A falha é fácil de identificar uma vez que você sabe o que procurar, e fácil de perder até então. Um lote de faturas digitalizadas chega com uma mistura de originais em retrato e paisagem, alguém endireita metade delas com uma rotação de 90 graus no Acrobat antes de arquivar, e a tira de miniaturas em um visualizador Delphi construído sobre o PDFium renderiza aquelas páginas específicas de lado, de cabeça para baixo, ou espremidas em uma caixa formatada para a orientação errada. Nada levanta uma exceção. Nada registra um erro. Os pixels estão simplesmente errados, e apenas para o subconjunto de páginas que alguém girou posteriormente — exatamente o tipo de bug que sobrevive a uma passada de QA completa contra um PDF de teste não rotacionado e depois aparece em produção na página 47 de um real

Por que o PDFium gira a página duas vezes?

O PDFium aplica o próprio valor /Rotate de uma página automaticamente toda vez que renderiza um bitmap, independentemente do que é passado ao renderizador. O parâmetro rotate de FPDF_RenderPageBitmap, exposto no PDFiumPas como os valores TRotation ro0, ro90, ro180 e ro270 em TPdf.RenderPage, TPdf.RenderTile e TPdf.RenderPageThumbnail, não define o ângulo em que a página deveria acabar; o parâmetro rotate define quanta rotação extra colocar em camada sobre o que quer que o dicionário de página já especifique, motivo pelo qual todos esses métodos o assumem como ro0 por padrão

TPdf.PageRotation lê esse mesmo valor /Rotate por meio de FPDFPage_GetRotation, e o código da aplicação frequentemente precisa dele por motivos que não têm nada a ver com renderização, como decidir como posicionar uma anotação no espaço da página. A armadilha é uma única linha: passar PageRotation no argumento Rotation de RenderPage, esperando que a chamada normalize a página para a posição vertical. Uma página já salva com /Rotate 90 exibe corretamente, rotacionada, em qualquer visualizador em conformidade, incluindo o PDFium; adicione ro90 novamente por cima disso e a página gira para 180 graus em vez dos 90 pretendidos, enquanto uma página sem nenhuma rotação recebe um giro indesejado de um quarto de volta sem motivo

// Wrong: PageRotation already reflects /Rotate, and PDFium applies
// it automatically on every render -- passing it again as Rotation
// doubles the angle
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, Pdf.PageRotation, []);

// Right: leave Rotation at its ro0 default and let PDFium apply the
// page's own /Rotate exactly once
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, ro0, []);

Para que o parâmetro Rotation de fato serve

O parâmetro Rotation ganha seu lugar na API para uma tarefa genuinamente diferente: adicionar uma rotação apenas de visualização que não tem nada a ver com a orientação armazenada de uma página, o tipo que um botão de barra de ferramentas de girar visualização aplica sem tocar o arquivo subjacente. TPdfView mantém os dois conceitos como duas propriedades separadas exatamente por esse motivo. TPdfView.PageRotation espelha o próprio /Rotate da página e, por meio de FPDFPage_SetRotation, pode gravar um novo valor de volta no documento; TPdfView.Rotation é uma propriedade transitória, apenas de visualização, que assume ro0 por padrão e nunca toca o arquivo. Ler a primeira propriedade e gravá-la na segunda é o bug inteiro em uma frase

// View-only: rotates what the user sees, changes nothing in the file
procedure TViewerForm.RotateViewClick(Sender: TObject);
begin
  case PdfView.Rotation of
    ro0:   PdfView.Rotation := ro90;
    ro90:  PdfView.Rotation := ro180;
    ro180: PdfView.Rotation := ro270;
    ro270: PdfView.Rotation := ro0;
  end;
end;

// Persistent: rewrites the page's own /Rotate entry in the document
procedure TViewerForm.RotatePageClick(Sender: TObject);
begin
  case PdfView.PageRotation of
    ro0:   PdfView.PageRotation := ro90;
    ro90:  PdfView.PageRotation := ro180;
    ro180: PdfView.PageRotation := ro270;
    ro270: PdfView.PageRotation := ro0;
  end;
end;

Por que o dimensionamento de zoom de ajuste quebra da mesma forma?

O dimensionamento de zoom de ajuste quebra por um motivo em imagem espelhada: o cálculo começa a partir do par errado de números, em vez do ângulo errado. Uma forma típica de dimensionar uma caixa de miniatura pede ao PDFium a largura e altura de uma página, compara essa proporção com a caixa disponível, e calcula o maior retângulo que cabe dentro dela — o que funciona de forma limpa para uma página não rotacionada. O mesmo cálculo falha silenciosamente para uma página /Rotate 90 ou /Rotate 270 quando a largura e altura vieram de uma chamada que reporta o tamanho intrínseco, não rotacionado, da página: uma página A4 retrato carregando /Rotate 90 ainda reporta aproximadamente 595 por 842 pontos, mesmo que o PDFium a renderize, corretamente, a aproximadamente 842 por 595 uma vez que a rotação entra em vigor, e uma caixa de ajuste calculada a partir do par não rotacionado acaba formatada para uma orientação completamente errada

FPDF_GetPageSizeByIndex é um exemplo concreto de uma chamada que reporta esse tamanho intrínseco, não rotacionado, por design, o que a torna conveniente para escanear dimensões de página sem carregar cada página e arriscada para matemática de zoom de ajuste que esquece de considerar isso. A correção decorre diretamente de nomear o problema: verifique a rotação da página antes de fazer a aritmética de ajuste, troque largura e altura sempre que essa rotação for 90 ou 270 graus, calcule a caixa de ajuste a partir do par trocado, e ainda passe ro0 para a chamada de renderização real, porque o PDFium continua sendo quem aplica a rotação de fato

Acertando miniaturas sem reinventar a matemática de ajuste

TPdf.RenderPageThumbnail já carrega essa correção, de modo que o caminho mais curto para uma miniatura correta é chamá-la, em vez de remontar a lógica de ajuste-e-rotação manualmente. Dado um índice de página baseado em 1 e uma largura e altura máximas, RenderPageThumbnail calcula uma caixa de ajuste, a corrige para um /Rotate de 90 ou 270 internamente, e retorna um bitmap de posse de quem chama sem perturbar a página atual do documento ou disparar um evento OnPageChange — o que importa para uma tira de miniaturas construída ao lado de um visualizador ao vivo na mesma instância de TPdf

// PageW, PageH are a page's own (unrotated) dimensions in points, for
// example from FPDF_GetPageSizeByIndex, which reports size before
// /Rotate is applied
function FitBox(PageW, PageH: Double; Rotation: TRotation;
  MaxW, MaxH: Integer; out FitW, FitH: Integer): Boolean;
var
  PgW, PgH, Swap: Integer;
begin
  PgW := Round(PageW);
  PgH := Round(PageH);
  if PgW < 1 then PgW := 1;
  if PgH < 1 then PgH := 1;

  if Rotation in [ro90, ro270] then
  begin
    Swap := PgW;
    PgW := PgH;
    PgH := Swap;
  end;

  Result := (MaxW > 0) and (MaxH > 0);
  if not Result then
    Exit;

  if PgW * MaxH > PgH * MaxW then
  begin
    FitW := MaxW;
    FitH := (MaxW * PgH) div PgW;
  end
  else
  begin
    FitH := MaxH;
    FitW := (MaxH * PgW) div PgH;
  end;
end;

O auxiliar FitBox vale a pena manter por perto de qualquer forma, porque RenderPageThumbnail só cobre o caso de um único bitmap. Um grid de miniaturas personalizado, uma tira de pré-visualização de impressão, ou um diálogo seletor de página que organiza várias páginas contra caixas independentes precisa da mesma matemática de ajuste ciente de rotação sem necessariamente querer um bitmap novo para cada ladrilho, e os próprios modos de zoom de ajustar-página e ajustar-largura de TPdfView se apoiam na mesma ideia internamente, escolhendo entre a largura e a altura de uma página para o cálculo de proporção de zoom baseado na rotação atual da visualização antes de compará-la contra a área de cliente disponível. Se o desempenho de zoom e rolagem nesse tipo de visualizador for o próximo problema na lista, o texto complementar sobre cache de renderização e zoom suave em um visualizador Delphi baseado em PDFium continua exatamente onde o dimensionamento correto para

Identificando uma rotação dupla antes de um cliente

Uma rotação dupla tem uma assinatura visual confiável: uma página que foi girada 90 graus na entrada sai parecendo girada 180 em relação ao resto do documento, não 90, porque o ro90 extra se empilhou sobre o próprio ro90 da página, em vez de substituí-lo. Um fixture de teste construído apenas a partir de páginas /Rotate 0 nunca vai capturar isso, já que adicionar ro0 a ro0 continua sendo ro0 e o bug permanece invisível; um fixture precisa de pelo menos uma página salva com /Rotate 90 e uma com /Rotate 270 antes de um caminho de código de miniatura ou zoom de ajuste poder ser considerado confiável

O pipeline básico de página para bitmap coberto em renderizando páginas de PDF para JPEG com o Componente PDFium já renderiza páginas rotacionadas corretamente sem nenhum código de caso especial, precisamente porque deixa Rotation em seu padrão ro0 e permite que o PDFium aplique /Rotate por conta própria. O bug de rotação dupla só aparece quando o código da aplicação começa a ler PageRotation de volta e a alimentá-la em algum lugar a que não pertence

As chamadas de renderização cientes de rotação e o dimensionamento de miniatura descritos aqui fazem parte do Componente PDFium para Delphi e C++Builder, ao lado do restante das APIs de renderização, visualização e extração de texto construídas sobre as mesmas classes TPdf e TPdfView