Renderizar una página PDF a JPEG son dos operaciones que la gente suele encadenar y luego depurar por separado. Primero se rasteriza la página en un mapa de bits de píxeles a la resolución elegida. Luego ese mapa de bits se entrega a un codificador JPEG y se elige una calidad. PDFium VCL es responsable de la primera mitad mediante RenderPage; la segunda es Delphi puro, TJPEGImage de Vcl.Imaging.jpeg. La unión entre ambas es donde viven las decisiones interesantes, porque la resolución que se elige en el lado de renderizado y la calidad en el lado de codificación se compensan entre sí y con el tamaño del fichero de maneras fáciles de errar.
Lo fundamental antes de cualquier código: una página PDF no tiene píxeles. Se describe en puntos, donde un punto es 1/72 de pulgada, y la página es un dibujo vectorial medido en esos puntos. Cuando se pide a PDFium que renderice, se está eligiendo cuántos píxeles proyectar ese dibujo, y esa elección es el DPI. Un cálculo incorrecto da como resultado una miniatura borrosa cuando se quería un original de impresión, o la asignación de un mapa de bits de 200 megapíxeles para algo que acabará siendo una previsualización de 120 píxeles.
De DPI a dimensiones en píxeles
RenderPage necesita Width y Height en píxeles enteros, no un DPI. Por tanto, el primer paso es convertir. Una página informa de su tamaño en puntos mediante PageWidth y PageHeight (ambos Double), y la conversión es la misma que usa cualquier rasterizador: los píxeles son iguales a los puntos multiplicados por el DPI objetivo dividido entre 72. Una página US Letter mide 612 por 792 puntos. A 150 DPI se convierte en 1275 por 1650 píxeles; a 72 DPI se queda en 612 por 792, un píxel por punto, que es el caso que la gente olvida que es simplemente la identidad.
// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free; // the function-form RenderPage hands you ownership
Dos detalles en esas cuatro líneas determinan si el código es correcto. El primero es que la forma funcional de RenderPage devuelve un TBitmap del que el llamante es responsable. PDFium lo asignó y se desentendió; si no se libera con Free en cada iteración, un lote de unos cientos de páginas pierde cientos de mapas de bits y el proceso crece hasta que algo falla. El segundo es el argumento Color, aquí clWhite. Las páginas PDF suelen dibujarse asumiendo un substrato blanco opaco, y una página con transparencia renderizada sobre el color de fondo incorrecto produce bordes turbios o halos oscuros extraños. El blanco es el valor por defecto adecuado para casi cualquier documento; el parámetro existe para el caso excepcional en que no lo sea.
El 0, 0 son los desplazamientos Left y Top dentro de la página, en el espacio de coordenadas escalado, y se dejan a cero a menos que se recorte. El ro0 es la rotación: dejarlo a cero hace que PDFium respete la rotación que la página declara en su entrada /Rotate, de modo que una página creada en horizontal sale en horizontal sin ninguna intervención adicional.
Codificar el mapa de bits como JPEG
Una vez que existe el mapa de bits, JPEG es la parte sencilla y es Delphi puro. TJPEGImage.Assign copia el mapa de bits, CompressionQuality establece la calidad en una escala del 1 al 100, y SaveToFile escribe el fichero. La única regla de orden es que la calidad debe establecerse antes de guardar, porque gobierna la codificación que desencadena SaveToFile.
uses
Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;
procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
const FileName: string);
var
Bitmap: TBitmap;
Jpeg: TJPEGImage;
begin
Pdf.PageNumber := PageNumber;
Bitmap := Pdf.RenderPage(0, 0,
Round(Pdf.PageWidth * Dpi / 72),
Round(Pdf.PageHeight * Dpi / 72),
ro0, [], clWhite);
try
Jpeg := TJPEGImage.Create;
try
Jpeg.Assign(Bitmap);
Jpeg.CompressionQuality := Quality; // 1..100
Jpeg.SaveToFile(FileName);
finally
Jpeg.Free;
end;
finally
Bitmap.Free;
end;
end;
Ese try/finally anidado puede parecer excesivo para un helper de una sola página, pero es exactamente lo correcto para un lote. El bloque interno libera el codificador, el externo libera el mapa de bits, y cualquiera de los dos que se dispare ante una excepción sigue liberando lo que le corresponde. Si se colapsan en uno y se produce una excepción durante la codificación, el mapa de bits puede quedarse sin liberar. A largo plazo esa es la diferencia entre un convertidor que termina y uno que muere en la página 300 con un fichero corrupto y un diálogo de falta de memoria.
Elegir DPI y calidad conjuntamente
Los dos parámetros no son independientes del propósito de la salida, y el error habitual es subirlos ambos por precaución. Una miniatura web renderizada a 300 DPI y guardada con calidad 95 son varios cientos de kilobytes haciéndose pasar por una imagen de 120 píxeles; el navegador descarta casi todo en la reducción. Hay que ajustar la resolución a los píxeles que la salida necesita realmente, y luego elegir una calidad que sobreviva a la compresión con pérdida de JPEG sin artefactos visibles.
| Uso | DPI | Calidad JPEG |
|---|---|---|
| Miniatura de lista | 72 | 60-70 |
| Previsualización en pantalla | 96-150 | 80-85 |
| Visualización de alto detalle | 200-300 | 85-95 |
| Original de impresión | 300-600 | 90-100 |
La calidad JPEG merece una advertencia propia. No es un dial lineal. El salto de 70 a 85 aporta una mejora visual real a cambio de un crecimiento modesto del fichero; el salto de 95 a 100 aproximadamente dobla el fichero para una diferencia que casi nadie puede ver, porque la calidad 100 sigue sin ser sin pérdida, simplemente deja de descartar mucho. En páginas con mucho texto, la compresión por bloques de JPEG difumina los bordes nítidos de los glifos en un leve ruido, que es por lo que con una calidad por debajo de 80 el texto aparece granulado en lo que debería ser una salida nítida. Si las páginas son principalmente texto y se puede cambiar de formato, PNG renderiza ese texto sin el ruido; JPEG se gana su lugar en contenido fotográfico y mixto donde su compresión es genuinamente menor.
Miniaturas más rápidas y pequeñas
Cuando el objetivo es una miniatura en lugar de una reproducción fiel, se puede indicar al renderizador que haga menos trabajo. El parámetro Options acepta un conjunto de indicadores TRenderOption, y algunos de ellos intercambian fidelidad por velocidad exactamente de la manera que quiere una previsualización pequeña. reGrayscale elimina el color, lo que tanto renderiza más rápido como produce un mapa de bits más pequeño para codificar. reNoSmoothImage y reNoSmoothPath omiten el suavizado que de todas formas es invisible a escala de miniatura.
function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
Scale: Double;
begin
Pdf.PageNumber := PageNumber;
// Fit the page inside MaxW x MaxH while preserving aspect ratio.
Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
Result := Pdf.RenderPage(0, 0,
Round(Pdf.PageWidth * Scale),
Round(Pdf.PageHeight * Scale),
ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;
El caso de miniatura también muestra la forma más clara de pensar en el tamaño. En lugar de pasar por DPI, se calcula un único factor de escala que hace caber la página dentro de un rectángulo delimitador y preserva la relación de aspecto, que es lo que hace el Min de los dos ratios. Una página vertical y una horizontal acaban ambas dentro del mismo rectángulo sin distorsión, y no hay que razonar sobre qué DPI corresponde a "caber en 200 por 280". Una advertencia con reGrayscale: convierte el contenido raster a gris, pero los rellenos vectoriales y el texto mantienen sus valores de color en el motor, de modo que una página con mayoritariamente arte vectorial puede volver menos monocromática de lo que el nombre del indicador sugiere. Para un resultado verdaderamente en escala de grises, GrayscalePdfBitmap posterior al renderizado es la vía fiable.
Procesar un documento completo por lotes
Para un documento completo se usa un bucle sobre PageCount avanzando PageNumber de una en una. Las páginas van en base 1: la página uno es PageNumber := 1, y el bucle llega hasta PageCount inclusive, no PageCount - 1. Lo otro que debe respetar el lote es el contrato de carga silenciosa. Asignar Active := True nunca lanza excepción ante un fichero dañado o una contraseña incorrecta; simplemente deja Active en False. Hay que comprobarlo antes de renderizar una sola página, de lo contrario el primer RenderPage trabaja contra un documento que nunca se abrió.
procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
Pdf: TPdf;
I, Digits: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := PdfPath;
Pdf.Active := True;
if not Pdf.Active then
raise Exception.Create('Could not open ' + PdfPath);
Digits := Length(IntToStr(Pdf.PageCount)); // zero-pad so files sort right
for I := 1 to Pdf.PageCount do
SavePageAsJpeg(Pdf, I, Dpi, Quality,
Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
El relleno con ceros mediante Digits es algo pequeño que ahorra una tarde más adelante. Si se nombran los ficheros de page_1.jpg a page_10.jpg, cualquier herramienta que los ordene como cadenas coloca page_10 justo después de page_1, desordenando la secuencia. Rellenar hasta el ancho del número de página más alto, de modo que un documento de 300 páginas produzca page_001.jpg, mantiene el orden léxico y el orden de páginas idénticos en toda la cadena de procesamiento.
Para documentos lo suficientemente grandes como para que la conversión tome un tiempo apreciable, conviene ejecutarla fuera del hilo de la interfaz de usuario o procesar mensajes entre páginas para que la aplicación siga respondiendo, y dar al usuario una forma de detenerla. Si se renderizan páginas muy grandes y se quiere cancelación que actúe a mitad de página y no solo entre páginas, PDFium VCL tiene una vía de renderizado progresivo con un token de cancelación; es un mecanismo más pesado del que la mayoría de exportaciones por lotes necesitan, pero está disponible cuando una sola página a 600 DPI es en sí misma suficientemente lenta como para bloquear.
Un último emparejamiento que conviene conocer. Rasterizar una página descarta su capa de texto: el JPEG son píxeles, y las palabras que contiene ya no son seleccionables ni buscables. Cuando se necesitan tanto la imagen como el texto subyacente, hay que renderizar para la imagen y extraer el texto por separado, algo que cubre el artículo complementario sobre extracción de texto de documentos PDF con PDFium VCL. Las sobrecargas de RenderPage y las opciones de renderizado mostradas aquí forman parte del PDFium VCL Component para Delphi y C++Builder.