Las dimensiones de una página PDF se fijan en el momento de crearla, por lo que no es posible reescalar el contenido en el propio lugar del mismo modo que se redimensiona una imagen. El modelo de biblioteca que hace viable la reducción es capturar y redibujar: se extrae el contenido de cada página hacia un manejador en memoria, se crea una nueva página en blanco con el tamaño de papel original y se vuelve a dibujar el contenido capturado dentro de un rectángulo delimitador reducido. El espacio en blanco resultante actúa como margen. Con un factor de escala del 70% en una página A4, por ejemplo, el 15% del ancho queda a cada lado y el mismo porcentaje arriba y abajo, que es exactamente lo que produce el cálculo de bordes que se muestra a continuación.
Funcionamiento de CapturePage
CapturePage recibe un número de página, traslada el contenido de esa página a un objeto de captura en memoria y elimina la página del árbol de páginas del documento. Esa eliminación es intencionada y es la razón por la que el bucle siempre selecciona la página 1 independientemente del índice de iteración: una vez que la página 1 se captura y elimina, la que era la página 2 pasa a ser la nueva página 1, y así sucesivamente. Si se incrementa el selector de página junto con el contador del bucle se saltará una página de cada dos y el resultado tendrá la mitad de páginas de las esperadas.
El manejador de captura devuelto por CapturePage no es una referencia de página, sino más bien una instantánea del contenido. Permanece válido hasta que se llama a DrawCapturedPage o se libera explícitamente. DrawCapturedPage recibe ese manejador junto con un rectángulo destino expresado como desplazamiento izquierdo, desplazamiento inferior, ancho y alto, todo en puntos. La biblioteca escala el contenido capturado para ajustarlo exactamente a ese rectángulo y solo preserva la relación de aspecto si el rectángulo coincide con las proporciones originales. Para un escalado uniforme, el rectángulo debe ser el tamaño original multiplicado por el factor de escala, centrado en la página.
El cálculo del centrado
Con un factor de escala del 70%, el 30% restante de cada dimensión se reparte por igual entre los dos lados. El margen horizontal es pageWidth * (1.0 - 0.70) / 2, es decir, el 15% del ancho, y el margen vertical sigue la misma fórmula usando la altura de la página. El rectángulo destino de DrawCapturedPage comienza entonces en (horizBorder, vertBorder) y ocupa pageWidth - 2 * horizBorder por pageHeight - 2 * vertBorder. Este cálculo no es específico de ninguna biblioteca, es simplemente la geometría de encajar un rectángulo menor simétricamente dentro de uno mayor.
Conviene tener en cuenta que SetOrigin(1) sitúa el origen de coordenadas en la esquina superior izquierda en lugar de la inferior izquierda. Los valores de margen que se pasan a DrawCapturedPage se miden desde el origen que se haya establecido, de modo que si se cambia el modo de origen entre la carga y el dibujo, el centrado quedará desalineado.
Ejemplo en C#
El código siguiente procesa todas las páginas de Pages.pdf mediante el ciclo de captura y redibujo, y escribe el resultado en newpages.pdf. PDFL es el objeto envolvente ActiveX/COM añadido al proyecto desde PDFlibDLL64.dll.
private void ScalePages_Click(object sender, EventArgs e)
{
File.Delete("newpages.pdf");
double pageWidth, pageHeight, horizBorder, vertBorder;
double scaleFactor = 0.70;
int capturedPageId, ret;
PDFL.LoadFromFile("Pages.pdf", "");
PDFL.SetOrigin(1);
int numPages = PDFL.PageCount();
for (int i = 1; i <= numPages; i++)
{
// Always select page 1: CapturePage removes the page, so page 2
// becomes page 1 on the next iteration.
PDFL.SelectPage(1);
pageWidth = PDFL.PageWidth();
pageHeight = PDFL.PageHeight();
horizBorder = pageWidth * (1.0 - scaleFactor) / 2;
vertBorder = pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId = PDFL.CapturePage(1);
PDFL.NewPage();
PDFL.SetPageDimensions(pageWidth, pageHeight);
ret = PDFL.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
}
PDFL.SaveToFile("newpages.pdf");
}
Ejemplo en Delphi
La versión para Delphi utiliza TPDFlib directamente en lugar de hacerlo a través de la capa COM, pero la secuencia de llamadas es idéntica. Una diferencia práctica es la protección del fichero de salida: FileExists junto con DeleteFile en lugar de File.Delete, porque SaveToFile fallará si el destino está bloqueado por una ejecución anterior aún abierta en un visor.
procedure TForm1.ScalePagesClick(Sender: TObject);
var
PDFLib: TPDFlib;
pageWidth, pageHeight, horizBorder, vertBorder: Double;
scaleFactor: Double;
capturedPageId, ret, numPages, i: Integer;
begin
if FileExists('newpages.pdf') then
DeleteFile('newpages.pdf');
scaleFactor := 0.70;
PDFLib := TPDFlib.Create;
try
PDFLib.LoadFromFile('Pages.pdf', '');
PDFLib.SetOrigin(1);
numPages := PDFLib.PageCount();
for i := 1 to numPages do
begin
PDFLib.SelectPage(1);
pageWidth := PDFLib.PageWidth();
pageHeight := PDFLib.PageHeight();
horizBorder := pageWidth * (1.0 - scaleFactor) / 2;
vertBorder := pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId := PDFLib.CapturePage(1);
PDFLib.NewPage();
PDFLib.SetPageDimensions(pageWidth, pageHeight);
ret := PDFLib.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
end;
PDFLib.SaveToFile('newpages.pdf');
finally
PDFLib.Free;
end;
end;
Qué controla realmente el factor de escala
El valor 0,70 significa que el contenido renderizado ocupa el 70% de cada dimensión de la página, no que el fichero tenga el 70% de su tamaño original en bytes. El tamaño del fichero tras esta operación depende de la complejidad del contenido original; una página con imágenes de gran tamaño no se reducirá proporcionalmente porque los datos de píxeles se redibujan a la misma resolución dentro de un área más pequeña. Si el objetivo es la compresión a nivel de bytes, el enfoque adecuado es LinearizeFile o volver a guardar con compresión de flujo, no el escalado geométrico.
El valor del 70% tampoco es un límite fijo. Cualquier valor entre 0,0 y 1,0 funciona, y los valores superiores a 1,0 amplían el contenido más allá del borde original de la página, lo que se recorta en el borde del media box a menos que también se aumenten las dimensiones de la página. Los documentos con páginas de distintos tamaños se gestionan de forma natural porque PageWidth y PageHeight se consultan por página antes del cálculo de bordes, de modo que un documento donde las páginas impares son A4 y las pares A3 producirá una salida correctamente centrada en cada tamaño sin necesidad de ningún tratamiento especial.
Posibles fallos
En la práctica aparecen dos modos de fallo. El primero es un fichero de salida dejado abierto en un visor PDF de una ejecución anterior: SaveToFile fallará o escribirá cero bytes según la plataforma, y la nueva salida nunca se genera. La protección con borrado del fichero al inicio de la función resuelve esto en desarrollo, pero en un pipeline de producción es más seguro escribir en una ruta temporal y renombrar el fichero al completar con éxito.
El segundo es la discordancia en el recuento de páginas. Como CapturePage elimina páginas del documento a medida que las procesa, el recuento obtenido con PageCount() antes del bucle es el límite correcto para iterar. Llamar a PageCount() dentro del bucle devolvería un número decreciente en cada pasada y terminaría antes de tiempo, dejando las últimas páginas sin procesar. La variable del bucle en los ejemplos sirve únicamente como contador de iteraciones restantes; nunca se usa para seleccionar una página, porque la página a seleccionar es siempre la 1 por la razón explicada anteriormente.
Las llamadas de manipulación de páginas mostradas aquí, incluidas CapturePage, DrawCapturedPage y SetPageDimensions, forman parte de la losLab PDF Library para Delphi, C#, VB.NET y C++.