HotPDF aplana la rotación de páginas PDF con THotPDF.FlattenLoadedPageRotation: el método envuelve el contenido de cada página rotada en una transformación cm horaria, reescribe cada page box que la página tenga de verdad, gira la geometría de anotaciones, las matrices de appearance, los destinos explícitos y la geometría estructurada por el mismo ángulo, y luego pone /Rotate a 0. La página se ve idéntica en un visor, pero su sistema de coordenadas ya está derecho. Eso importa en cuanto una herramienta aguas abajo, un RIP de impresión o su propio código de sellos ignora /Rotate y coloca cosas en user space crudo
El desencadenante típico es un escáner o una app de captura móvil que escribe páginas apaisadas como media vertical con /Rotate 90. Todos los visores las muestran bien, así que nadie se entera hasta que alguien estampa un número de página en la «esquina inferior derecha» y aterriza de lado a lo largo del borde izquierdo, o un paso de imposición que solo lee el /MediaBox maqueta un hueco vertical para una página apaisada. Aplanar suena a trabajo de matriz de una línea. En la práctica toca cinco page boxes, tres clases de geometría de anotaciones, los destinos de enlaces del documento y el árbol de estructura, y cada uno tiene su propia regla en ISO 32000-1
¿Hacia dónde gira /Rotate una página PDF?
/Rotate gira la página en sentido horario para visualización e impresión, en múltiplos de 90 grados (ISO 32000-1 §7.7.3.3, Tabla 30). A 90 grados el borde izquierdo del media pasa a ser la parte superior y el borde superior pasa al lado derecho, así que en un device space con y hacia abajo el mapeo es X = (y - Bottom) * Scale y Y = (x - Left) * Scale. A 270 grados el borde derecho pasa arriba. /Rotate es además uno de los solo cuatro atributos de página heredables, junto a /Resources, /MediaBox y /CropBox (§7.7.3.4), así que un diccionario de página sin /Rotate propio puede quedar girado por un ancestro /Pages. THotPDF.GetLoadedPageRotation recorre la cadena de /Parent y normaliza el resultado a 0–359, que es el valor que usted quiere, no la clave cruda de la página
El sentido es fácil de torcer de una manera que sobrevive al testing, y builds anteriores de HotPDF hicieron exactamente eso. La antigua matriz de página a dispositivo intercambiaba las componentes y para 90 y 270, lo que produce un reflejo por la diagonal en lugar de una rotación: la orientación de la matriz se voltea respecto al caso sin rotar. Ambos ángulos «parecían girados», el bitmap tenía el ancho y el alto intercambiados, y un round trip de página a vista y de vuelta devolvía el punto de partida, así que las comprobaciones de dimensiones y los tests de round trip pasaban todos. La única comprobación fiable es dónde acaba un marcador de esquina, comparado píxel a píxel contra un renderer de referencia. Como el modelo del visor, el backend de render SIMD y el mapeo de resaltados habían copiado la misma matriz, todos se corrigieron a la vez, y el código de aplanado usa ahora la misma convención horaria que el renderer
Cómo reescribe FlattenLoadedPageRotation una página
FlattenLoadedPageRotation(PageRange, Info) procesa cada página de PageRange cuya rotación efectiva sea 90, 180 o 270, y devuelve el número de páginas que aplanó. Un PageRange vacío significa todas las páginas; si no, la cadena usa la sintaxis habitual 1-based '1-3,7', y un número de página fuera de rango lanza excepción en lugar de saltárselo. Los content streams originales jamás se recodifican. El método antepone a los /Contents de la página un stream nuevo que contiene q 0 -1 1 0 -Bottom Width+Left cm (para 90 grados), añade al final un stream con Q, y por último escribe un /Rotate 0 explícito en el diccionario de página para que un valor heredado en un nodo /Pages no pueda girar la página una segunda vez
var
Pdf: THotPDF;
Info: THPDFRotationFlattenInfo;
Flattened: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
begin
// '' = todas las páginas; las páginas a 0 grados se escanean pero se dejan tranquilas
Flattened := Pdf.FlattenLoadedPageRotation('', Info);
Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
[Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
Info.TransformedStructureGeometryCount]));
if Flattened > 0 then
Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
end;
finally
Pdf.Free;
end;
end;
El record THPDFRotationFlattenInfo merece registrarse en el log en lugar de tirarse. ScannedPageCount es el tamaño del rango, FlattenedPageCount iguala al valor de retorno, y los tres contadores Transformed... le dicen si el documento tenía enlaces, bookmarks o geometría estructurada apuntando a las páginas giradas. Un lote donde cada archivo reporta cero destinos está bien; un archivo PDF/UA etiquetado que reporta cero geometría estructurada cuando usted esperaba bounding boxes de figuras es una señal para inspeccionarlo a mano
¿Qué page boxes reescribe el aplanado, y en qué orden?
El aplanado reescribe solo las cajas que la página ya tiene, y lee todas las cajas antes de escribir ninguna. El orden importa por la cadena de valores por defecto: GetLoadedPageBox(PageIndex, pbCropBox, ...) devuelve el /MediaBox cuando la página no tiene /CropBox, y /BleedBox, /TrimBox y /ArtBox caen por defecto a la CropBox (§14.11.2). Una versión anterior sí leía, transformaba y escribía una caja cada vez. Reescribía primero el MediaBox, luego leía la «CropBox», recibía de vuelta el MediaBox ya girado, lo giraba una segunda vez y escribía una CropBox que la página jamás tuvo, lo que recortaba una página apaisada hasta dejarla cuadrada. Las reglas de herencia se parten igual: MediaBox y CropBox se buscan por la cadena de /Parent, mientras que Bleed, Trim y ArtBox solo cuentan si están en el propio diccionario de página, así que un /TrimBox perdido en un nodo /Pages se trata como ausente y jamás se copia a la página
procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
L, B, R, T: Single;
begin
Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
// True incluso sin clave /TrimBox: el valor recurre a CropBox, luego a MediaBox
if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
Writeln(Format('TrimBox [%g %g %g %g]', [L, B, R, T]));
// Precargado Letter; GetLoadedPageVisibleBox deja las salidas intactas si falla
L := 0; B := 0; R := 612; T := 792;
Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
Writeln(Format('Visible [%g %g %g %g]', [L, B, R, T]));
end;
Ejecute ese helper antes y después del aplanado y los números se explican solos. Para una página a 90 grados con MediaBox [0 0 612 792], el MediaBox aplanado pasa a ser [0 0 792 612]; cada caja reescrita se mapea por el mismo giro horario, relativo al origen del MediaBox original, así que el nuevo MediaBox empieza siempre en el origen y las demás cajas conservan su posición dentro. GetLoadedPageVisibleBox devuelve lo que los visores muestran y las impresoras imprimen, la CropBox recortada al MediaBox y normalizada de modo que Left sea menor que Right, y el renderer de HotPDF, el export SVG, el visor y la vía de impresión usan todos esa misma caja. Cuando necesite el tamaño de página que ve un humano, llame a GetLoadedPageVisibleBox en lugar de leer el /MediaBox
¿Por qué se rompen las anotaciones si solo gira /Rect?
Las anotaciones se rompen porque un appearance stream no se dibuja directamente en el /Rect. Bajo §12.5.5 el visor primero transforma el /BBox del formulario por su /Matrix, y luego escala y traslada el bounding box de ese resultado hasta el /Rect. Gire solo el /Rect y un sello de 200 × 40 acaba exprimido en un hueco de 40 × 200, ilegible y de lado. Por eso FlattenLoadedPageRotation multiplica por la derecha el giro horario de la página en cada /Matrix de appearance (para 90 grados, [0 -1 1 0 0 0] en la convención de vectores fila), a través de las appearances /N, /R y /D y de cada estado dentro de ellas. Un mismo appearance stream puede compartirse entre varias anotaciones o estados, así que cada stream se gira exactamente una vez por llamada. El único caso sin respuesta limpia es un stream compartido entre páginas con rotaciones distintas; sigue a la primera página que lo alcanza
Dos reglas más mantienen en su sitio los campos de formulario y las notas adhesivas. La entrada /MK /R de un widget (§12.5.6.19) es un ángulo antihorario, así que al ángulo horario de la página se le resta, módulo 360; sáltese eso y la siguiente regeneración de appearance dibuja el texto del campo en la dirección equivocada. Las anotaciones con el flag NoRotate (posición de bit 5, valor 16, §12.5.3) se quedan derechas en una página girada y pivotan sobre la esquina superior izquierda de su /Rect, así que el aplanado conserva su ancho, su alto y su aspecto derecho y solo mueve esa esquina a donde el giro la deja. Más allá de las anotaciones, el método también gira /QuadPoints, /Vertices, /L y /InkList, reescribe los destinos explícitos que nombran la página (puntos /XYZ, rectángulos /FitR, y /FitH / /FitV intercambiados a 90 y 270 grados, §12.3.2.2), y transforma geometría estructurada como las entradas /BBox de atributos de elementos de estructura cuyo /Pg es la página
Qué no cubre el aplanado
El aplanado es una reescritura geométrica de los objetos propios de una página, y varias situaciones quedan fuera en silencio en lugar de a los gritos
- Las páginas cuya rotación efectiva ya es 0, o cuyo MediaBox falta o tiene ancho o alto cero, se saltan sin error; compare el valor de retorno con el número de páginas que esperaba que cambiaran
- Los Form XObjects referenciados desde los recursos de la página conservan su propio
/BBoxen form space, porque elcmexterior ya los gira; el barrido del árbol de estructura sigue solo/Ky/A, así que jamás entra una segunda vez en los recursos o las anotaciones de la página - Los destinos se encuentran barriendo cada objeto indirecto una vez por página aplanada, así que un documento grande con cientos de páginas giradas paga ese paseo en cada una
- El renderer de páginas de HotPDF no dibuja anotaciones, así que una comprobación visual de sellos girados necesita antes
FlattenLoadedAnnotations
// Hornee las appearances en el contenido para que el renderer pueda mostrarlas,
// luego renderice la página 1 antes y después de quitar su /Rotate
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Pdf.FlattenLoadedPageRotation('1', Info);
After := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Assert((Before.Width = After.Width) and (Before.Height = After.Height));
// Compare aquí los píxeles del marcador de esquina, no solo las dimensiones
finally
After.Free;
end;
finally
Before.Free;
end;
Para más trasfondo, el lado de anotaciones de esta historia continúa en sintetizar appearances de anotaciones antes de aplanarlas, el renderer detrás de la comparación de antes y después está cubierto en renderizar una página PDF cargada a un bitmap, y redacción y empalme N-up sobre PDFs cargados muestra la misma técnica de anexado a content streams de la que dependen el prefijo y el sufijo de rotación. HotPDF, incluidos FlattenLoadedPageRotation y los lectores de page boxes, está disponible para Delphi y C++Builder en la página del componente PDF HotPDF para Delphi