Las dimensiones de la página PDF se fijan en el momento en que se crea una página, por lo que no se puede simplemente reescalar el contenido en el lugar de la forma en que se redimensionaría una imagen. El modelo de la biblioteca que hace práctica la reducción es capturar y redibujar: extraer el contenido de cada página del documento a un identificador, crear una nueva página en blanco con el tamaño del medio original, y luego volver a dibujar el contenido capturado en un cuadro delimitador reducido. El espacio en blanco circundante se convierte en el margen. A un 70% de escala en una página A4, por ejemplo, el 15% del ancho queda a cada lado y la misma fracción arriba y abajo, que es exactamente lo que produce la aritmética de bordes a continuación
Cómo funciona 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 intencional 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 se captura y elimina la página 1, lo que era la página 2 se convierte en la nueva página 1, y así sucesivamente. Si incrementa el selector de páginas junto con el contador del bucle, omitirá una de cada dos páginas y terminará con la mitad de la salida esperada
El identificador de captura devuelto por CapturePage no es una referencia de página; es más bien una instantánea de contenido. Sigue válido después de dibujar, así que la misma captura puede colocarse más de una vez, hasta que la libere con ReleaseCapturedPage o el documento desaparezca. DrawCapturedPage toma ese identificador más un rectángulo de destino dado como borde izquierdo, borde superior, ancho y alto, todo en puntos por defecto. La biblioteca escala el contenido capturado para que encaje exactamente en ese rectángulo, preservando la relación de aspecto solo si su rectángulo resulta coincidir con las proporciones originales. Para un escalado uniforme, querrá que el rectángulo sea el tamaño original multiplicado por el factor de escala, centrado en la página
Las matemáticas del centrado
Con un factor de escala del 70%, el 30% restante de cada dimensión se divide en partes iguales entre los dos lados. Por lo que el margen insertado horizontal es pageWidth * (1.0 - 0.70) / 2, que es el 15% del ancho, y el margen vertical sigue la misma fórmula usando el alto de la página. El rectángulo de destino para DrawCapturedPage entonces comienza en (horizBorder, vertBorder) y abarca pageWidth - 2 * horizBorder por pageHeight - 2 * vertBorder. Esa aritmética no es específica de la biblioteca; es solo la geometría de encajar un rectángulo más pequeño simétricamente dentro de uno más grande
Una cosa que vale la pena notar: SetOrigin(1) coloca el origen de coordenadas en la parte superior izquierda en lugar de la parte inferior izquierda. El segundo valor que pasa a DrawCapturedPage es el borde superior del rectángulo, medido desde el origen que haya establecido. Por eso los ejemplos llaman a SetOrigin(1) primero: con el origen por defecto en la parte inferior izquierda, el mismo vertBorder pondría el borde superior cerca del final de la página y empujaría la mayor parte del contenido fuera de ella
Ejemplo en C#
El siguiente código procesa cada página de Pages.pdf a través del ciclo de captura y redibujado y escribe el resultado en newpages.pdf. PDFL es una instancia de la clase de import PDFlibrary que viene con la edición DLL, creada con new PDFlibrary("PDFlibDLL64.dll"); la edición ActiveX expone los mismos métodos por medio 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++)
{
// Siempre selecciona la página 1: CapturePage elimina la página, así que la página 2
// se convierte en 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. una fuente 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 de Delphi usa TPDFlib directamente en lugar de a través de la clase de import DLL, pero la secuencia de llamadas es idéntica. Una diferencia práctica es la protección del archivo de salida: FileExists más DeleteFile en lugar de File.Delete, porque SaveToFile fallará si el destino está bloqueado por una ejecución anterior que aún está 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. una fuente 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 aquí significa que el contenido renderizado ocupa el 70% de cada dimensión de la página, no que el archivo tenga el 70% de su tamaño original en bytes. El tamaño del archivo después de 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íxeles se vuelven a dibujar a la misma resolución en un área más pequeña. Si el objetivo es un archivo más pequeño, mire CompressContent para los content streams de página que salieron sin comprimir y la recompresión de imágenes (OptimizeImages en la edición Delphi), no el escalado geométrico. LinearizeFile es otra herramienta: reordena el archivo para carga de página por página (Fast Web View), lo que ayuda a los visores a abrir archivos grandes por red pero no los encoge
La cifra del 70% tampoco es un límite estricto. Cualquier valor entre 0.0 y 1.0 funciona, y los valores por encima de 1.0 agrandan el contenido más allá del límite original de la página, lo cual se recorta en el borde del cuadro del medio (media box) a menos que también aumente las dimensiones de la página. Los documentos de tamaños mixtos se manejan de forma natural porque PageWidth y PageHeight se consultan por página antes del cálculo de los bordes, por lo que un documento donde las páginas impares son A4 y las pares son A3 producirá una salida correctamente centrada en cada tamaño de página sin ningún caso especial
Dónde pueden fallar las cosas
Dos modos de fallo surgen en la práctica. El primero es un archivo de salida que queda abierto en un visor de PDF de una ejecución anterior: SaveToFile fallará o escribirá cero bytes dependiendo de la plataforma, y la nueva salida nunca aterriza. La protección de eliminación de archivos en la parte superior de la función maneja eso para el desarrollo, pero en una tubería de producción, escribir en una ruta temporal y cambiar el nombre en caso de éxito es más seguro
El segundo es el desajuste en el conteo de páginas. Debido a que CapturePage elimina las 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 cual iterar. Llamar a PageCount() dentro del bucle tampoco ayuda: cada pasada elimina una página y agrega una, así que el conteo se queda plano y no dice nada sobre cuántas páginas de origen quedan. La variable del bucle en los ejemplos solo sirve como un contador de iteraciones restantes; nunca se usa para seleccionar una página, porque la página a seleccionar siempre es la 1 por la razón explicada anteriormente
Las llamadas de manipulación de páginas mostradas aquí, incluyendo CapturePage, DrawCapturedPage y SetPageDimensions, son parte de la losLab PDF Library para Delphi, C#, VB.NET y C++