La función FPDF_RenderPageBitmap del componente PDFium acepta un argumento de rotación que PDFium siempre añade encima de cualquiera que sea la rotación que la página ya lleve en su propia entrada /Rotate, así que leer la rotación almacenada de una página y volver a entregar ese mismo valor a la llamada de renderizado rota la página dos veces. El error idéntico aparece en la aritmética del zoom ajustado: dimensionar una miniatura a partir del ancho y alto sin rotar de la página produce la proporción de aspecto equivocada siempre que /Rotate sea 90 o 270 grados, porque el mapa de bits renderizado sale con el ancho y el alto intercambiados
El fallo es fácil de detectar en cuanto sabéis qué buscar, y fácil de pasar por alto hasta entonces. Llega un lote de facturas escaneadas con una mezcla de originales en vertical y en horizontal, alguien endereza la mitad de ellas con una rotación de 90 grados en Acrobat antes de archivarlas, y la tira de miniaturas de un visor Delphi construido sobre PDFium renderiza esas páginas concretas de lado, boca abajo, o comprimidas en una caja con la forma equivocada de orientación. Nada lanza una excepción. Nada registra un error. Los píxeles simplemente están mal, y solo para el subconjunto de páginas que alguien rotó después, exactamente el tipo de fallo que sobrevive a una pasada de control de calidad completa contra un PDF de prueba sin rotar y luego aparece en producción en la página 47 de uno real
¿Por qué rota PDFium la página dos veces?
PDFium aplica automáticamente el propio valor /Rotate de una página cada vez que renderiza un mapa de bits, independientemente de lo que se le pase al renderizador. El parámetro de rotación de FPDF_RenderPageBitmap, expuesto en PDFiumPas como los valores TRotation ro0, ro90, ro180 y ro270 en TPdf.RenderPage, TPdf.RenderTile y TPdf.RenderPageThumbnail, no establece el ángulo en el que debería acabar una página; el parámetro de rotación establece cuánta rotación extra superponer encima de lo que ya especifique el diccionario de página, razón por la cual cada uno de esos métodos lo deja por defecto en ro0
TPdf.PageRotation lee ese mismo valor /Rotate mediante FPDFPage_GetRotation, y el código de aplicación a menudo lo necesita por razones que no tienen nada que ver con el renderizado, como decidir cómo maquetar una anotación en el espacio de página. La trampa es una sola línea: pasar PageRotation al argumento Rotation de RenderPage, esperando que la llamada normalice la página a vertical. Una página ya guardada con /Rotate 90 se muestra correctamente, rotada, en cualquier visor conforme, PDFium incluido; añadid ro90 otra vez encima de eso y la página gira a 180 grados en lugar de a los 90 previstos, mientras que una página sin ninguna rotación en absoluto recibe un giro no deseado de un cuarto de vuelta sin ningún 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 qué sirve realmente el parámetro Rotation?
El parámetro Rotation se gana su sitio en la API para un trabajo genuinamente distinto: añadir una rotación exclusiva de vista que no tiene nada que ver con la orientación almacenada de una página, del tipo que aplica un botón de barra de herramientas de rotar vista sin tocar el archivo subyacente. TPdfView mantiene los dos conceptos como dos propiedades separadas exactamente por esta razón. TPdfView.PageRotation refleja el propio /Rotate de la página y, mediante FPDFPage_SetRotation, puede escribir un valor nuevo de vuelta en el documento; TPdfView.Rotation es una propiedad transitoria, exclusiva de vista, que por defecto es ro0 y nunca toca el archivo. Leer la primera propiedad y escribirla en la segunda es todo el fallo en una 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 qué se rompe del mismo modo el dimensionado por zoom ajustado?
El dimensionado por zoom ajustado se rompe por una razón que es la imagen especular de la anterior: el cálculo parte del par de números equivocado en lugar del ángulo equivocado. Una forma típica de dimensionar una caja de miniatura pide a PDFium el ancho y el alto de una página, compara esa proporción de aspecto con la caja disponible, y calcula el rectángulo más grande que cabe dentro de ella, lo cual funciona con limpieza para una página sin rotar. El mismo cálculo falla silenciosamente para una página /Rotate 90 o /Rotate 270 cuando el ancho y el alto proceden de una llamada que informa del tamaño intrínseco, sin rotar, de la página: una página A4 vertical que lleva /Rotate 90 sigue informando aproximadamente 595 por 842 puntos, aunque PDFium la renderiza, correctamente, a aproximadamente 842 por 595 en cuanto la rotación surte efecto, y una caja de ajuste calculada a partir del par sin rotar acaba con una forma completamente equivocada de orientación
FPDF_GetPageSizeByIndex es un ejemplo concreto de una llamada que informa de ese tamaño intrínseco, sin rotar, por diseño, lo que la hace conveniente para escanear dimensiones de página sin cargar cada página y arriesgada para la aritmética de zoom ajustado que se olvida de tenerlo en cuenta. La solución se deriva directamente de nombrar el problema: comprobad la rotación de la página antes de hacer la aritmética de ajuste, intercambiad ancho y alto siempre que esa rotación sea de 90 o 270 grados, calculad la caja de ajuste a partir del par intercambiado, y aun así pasad ro0 a la llamada de renderizado real, porque PDFium sigue siendo quien aplica la rotación real
Conseguir miniaturas correctas sin reinventar la aritmética de ajuste
TPdf.RenderPageThumbnail ya lleva incorporada esta solución, así que el camino más corto hacia una miniatura correcta es llamarlo en lugar de reensamblar a mano la lógica de ajuste y rotación. Dado un índice de página basado en 1 y un ancho y alto máximos, RenderPageThumbnail calcula una caja de ajuste, la corrige internamente para un /Rotate de 90 o 270, y devuelve un mapa de bits propiedad de quien llama sin alterar la página actual del documento ni disparar un evento OnPageChange, algo que importa para una tira de miniaturas construida junto a un visor en vivo sobre la misma instancia 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;
Merece la pena conservar el ayudante FitBox de todos modos, porque RenderPageThumbnail solo cubre el caso de un único mapa de bits. Una cuadrícula de miniaturas personalizada, una tira de vista previa de impresión, o un diálogo selector de páginas que maqueta varias páginas contra cajas independientes necesita la misma aritmética de ajuste consciente de la rotación sin necesariamente querer un mapa de bits nuevo para cada mosaico, y los propios modos de zoom de ajustar a página y ajustar al ancho de TPdfView se apoyan internamente en la misma idea, eligiendo entre el ancho y el alto de una página para el cálculo de proporción de zoom según la rotación actual de la vista antes de compararla con el área de cliente disponible. Si el rendimiento del zoom y el desplazamiento en ese tipo de visor es el siguiente problema en la lista, el artículo complementario sobre la caché de renderizado y el zoom fluido en un visor Delphi basado en PDFium retoma justo donde lo deja el dimensionado correcto
Detectar una doble rotación antes de que lo haga un cliente
Una doble rotación tiene una firma visual fiable: una página que se rotó 90 grados al entrar sale con aspecto de haber rotado 180 respecto al resto del documento, no 90, porque el ro90 extra se apiló encima del propio ro90 de la página en lugar de sustituirlo. Un banco de pruebas construido solo con páginas /Rotate 0 nunca detectará esto, ya que sumar ro0 a ro0 sigue siendo ro0 y el fallo permanece invisible; un banco de pruebas necesita al menos una página guardada con /Rotate 90 y otra con /Rotate 270 antes de que se pueda confiar en una vía de código de miniatura o zoom ajustado
El pipeline básico de página a mapa de bits cubierto en el renderizado de páginas PDF a JPEG con el componente PDFium ya renderiza correctamente las páginas rotadas sin ningún código de caso especial, precisamente porque deja Rotation en su valor por defecto ro0 y deja que PDFium aplique /Rotate por sí mismo. El fallo de doble rotación solo aparece en cuanto el código de aplicación empieza a leer PageRotation de vuelta y a entregarlo en algún sitio donde no pertenece
Las llamadas de renderizado conscientes de la rotación y el dimensionado de miniaturas descritos aquí forman parte del componente PDFium para Delphi y C++Builder, junto con el resto de las API de renderizado, visualización y extracción de texto construidas sobre las mismas clases TPdf y TPdfView