Las dimensiones de una página PDF se fijan en el momento de crearla, así que no se puede reescalar el contenido en el propio sitio como se redimensiona una imagen. El modelo de biblioteca que hace práctica la reducción es capturar-y-redibujar: se eleva el contenido de cada página fuera del documento hacia un handle, se crea una página nueva en blanco con el tamaño de media original y se vuelve a dibujar el contenido capturado dentro de un rectángulo delimitador reducido. El espacio en blanco sobrante se convierte en el margen. Con una escala del 70% en una página A4, por ejemplo, el 15% del ancho cae a cada lado y la misma fracción arriba y abajo, que es exactamente lo que produce la aritmética de bordes de abajo
Funcionamiento de CapturePage
CapturePage toma un número de página, promueve 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 sin importar el índice de iteración: una vez que la página 1 se captura y se elimina, la que era la página 2 pasa a ser la nueva página 1, y así sucesivamente. Si incrementa el selector de página junto con el contador del bucle se saltará una página de cada dos y acabará con la mitad de la salida esperada
El handle de captura que devuelve CapturePage no es una referencia de página; se parece más a una instantánea de contenido. Sigue válido después de dibujar, así que la misma captura se puede colocar más de una vez, hasta que la libere con ReleaseCapturedPage o el documento desaparezca. DrawCapturedPage toma ese handle más un rectángulo destino dado como borde izquierdo, borde superior, ancho y alto, todo en puntos por defecto. La biblioteca escala el contenido capturado para ajustarlo exactamente a ese rectángulo, preservando la relación de aspecto solo si su rectángulo coincide por casualidad con las proporciones originales. Para un escalado uniforme quiere que el rectángulo sea 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. Así 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 con la altura de la página. El rectángulo destino de DrawCapturedPage empieza entonces en (horizBorder, vertBorder) y abarca pageWidth - 2 * horizBorder por pageHeight - 2 * vertBorder. Esa aritmética no es específica de ninguna biblioteca; es simplemente la geometría de encajar un rectángulo menor simétricamente dentro de uno mayor
Algo que conviene notar: SetOrigin(1) pone el origen de coordenadas en la esquina superior izquierda en lugar de la inferior izquierda. El segundo valor que se pasa a DrawCapturedPage es el borde superior del rectángulo, medido desde el origen que haya fijado. Por eso los ejemplos llaman primero a SetOrigin(1): con el origen por defecto de abajo a la izquierda, el mismo vertBorder pondría el borde superior cerca de la parte baja de la página y empujaría la mayor parte del contenido fuera de ella
Ejemplo en C#
El código siguiente procesa todas las páginas de Pages.pdf por el ciclo de captura y redibujo y escribe el resultado en newpages.pdf. PDFL es una instancia de la clase de importación PDFlibrary que viene con la edición DLL, creada con new PDFlibrary("PDFlibDLL64.dll"); la edición ActiveX expone los mismos métodos a través de PDFlibAX.PDFlibrary
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++)
{
// Seleccione siempre la página 1: CapturePage elimina la página, así que la página 2
// pasa a ser la página 1 en la siguiente iteración.
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);
// 0 = nada capturado, p. ej. un origen de una sola página: la única página no se puede eliminar
if (capturedPageId == 0)
throw new InvalidOperationException("CapturePage failed on source page " + i);
PDFL.NewPage();
PDFL.SetPageDimensions(pageWidth, pageHeight);
ret = PDFL.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
if (ret != 1)
throw new InvalidOperationException("DrawCapturedPage failed on source page " + i);
}
PDFL.SaveToFile("newpages.pdf");
}
Ejemplo en Delphi
La versión Delphi usa TPDFlib directamente en lugar de hacerlo a través de la clase de importación de la DLL, pero la secuencia de llamadas es idéntica. Una diferencia práctica es la guarda del fichero de salida: FileExists más 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);
// 0 = nada capturado, p. ej. un origen de una sola página: la única página no se puede eliminar
if capturedPageId = 0 then
raise Exception.CreateFmt('CapturePage failed on source page %d', [i]);
PDFLib.NewPage();
PDFLib.SetPageDimensions(pageWidth, pageHeight);
ret := PDFLib.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
if ret <> 1 then
raise Exception.CreateFmt('DrawCapturedPage failed on source page %d', [i]);
end;
PDFLib.SaveToFile('newpages.pdf');
finally
PDFLib.Free;
end;
end;
Qué controla realmente el factor de escala
El valor 0.70 significa aquí que el contenido renderizado ocupa el 70% de cada dimensión de 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 grandes no se encogerá proporcionalmente porque los datos de píxel se redibujan a la misma resolución dentro de un área menor. Si el objetivo es un fichero más pequeño, mire a CompressContent para los streams de contenido de página que salieron sin comprimir y a la recompresión de imágenes (OptimizeImages en la edición Delphi), no al escalado geométrico. LinearizeFile es otra herramienta: reordena el fichero para la carga página a página (Fast Web View), lo que ayuda a los visores a abrir ficheros grandes por red pero no los encoge
La cifra del 70% tampoco es un límite fijo. Cualquier valor entre 0.0 y 1.0 funciona, y los valores por encima de 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 salvo que también aumente las dimensiones de la página. Los documentos con páginas de tamaños mixtos se gestionan de forma natural porque PageWidth y PageHeight se consultan por página antes del cálculo de bordes, así que un documento donde las páginas impares son A4 y las pares A3 producirá salida correctamente centrada en cada tamaño de página sin 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 aterriza. La guarda de borrado de fichero al principio de la función resuelve eso en desarrollo, pero en un pipeline de producción es más seguro escribir en una ruta temporal y renombrar al tener éxito
El segundo es el desajuste del conteo de páginas. Como CapturePage elimina páginas del documento a medida que las procesa, el conteo que lee de PageCount() antes del bucle es el límite correcto contra el que iterar. Llamar a PageCount() dentro del bucle tampoco ayuda: cada pasada elimina una página y añade una, así que el conteo se mantiene plano y no dice nada sobre cuántas páginas de origen quedan. La variable del bucle en los ejemplos sirve solo 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 antes
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++