Artículo técnico

Renderizar páginas PDF a imágenes JPEG en Delphi con PDFium Component

Renderizar una página PDF a un JPEG requiere de dos operaciones que la gente tiende a ejecutar juntas y luego depurar por separado. Primero usted rasteriza la página en un mapa de bits de píxeles a una resolución que usted elija. Luego le entrega ese mapa de bits a un codificador de JPEG y escoge una calidad. PDFium Component posee la primera mitad a través de RenderPage; la segunda mitad es VCL simple, TJPEGImage desde Vcl.Imaging.jpeg. La unión entre ellos es donde residen las decisiones interesantes, debido a que la resolución que escoge en el lado de la renderización y la calidad que escoge en el lado de la codificación se contraponen entre sí y en contra del tamaño del fichero de maneras que son fáciles de interpretar erróneamente

La cuestión que debe internalizar antes de cualquier código: una página PDF no tiene píxeles. Está descrita en puntos, donde un punto es 1/72 de pulgada, y la página es un dibujo vectorial medido en esos puntos. Cuando pide a PDFium que renderice, está escogiendo sobre cuántos píxeles proyectar ese dibujo, y esa elección son los DPI (puntos por pulgada). Equivoque la aritmética y, o bien renderiza una miniatura borrosa cuando quería un maestro de impresión, o bien asigna un mapa de bits de 200 megapíxeles para algo destinado a ser una previsualización de 120 píxeles

De DPI a dimensiones de píxeles

RenderPage quiere números enteros Width y Height en píxeles, no un DPI. Así que el primer trabajo es hacer la conversión. Una página reporta su tamaño en puntos a través de PageWidth y PageHeight (ambos Double), y la conversión es la misma que emplea todo rasterizador: los píxeles equivalen a los puntos multiplicados por los DPI objetivo y divididos entre 72. Una página Carta de EE. UU. (US Letter) mide 612 por 792 puntos. A 150 DPI eso se convierte en 1275 por 1650 píxeles; a 72 DPI permanece en 612 por 792, un píxel por punto, el cual 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 deciden si el código es correcto. El primero es que la forma de función de RenderPage devuelve un TBitmap del cual usted es dueño. PDFium lo asignó y se fue; si no lo libera con Free en cada iteración, un lote sobre unos cientos de páginas pierde unos cientos de mapas de bits y el proceso se hincha hasta que algo se cae. El segundo es el argumento Color, clWhite aquí. Las páginas de PDF se dibujan normalmente asumiendo un sustrato blanco opaco, y una página con transparencia renderizada en el color de fondo incorrecto produce bordes turbios o halos oscuros extraviados. El blanco es el defecto correcto para casi cualquier documento; el parámetro existe para el caso raro en el que no lo es

Los 0, 0 son los desplazamientos Left y Top dentro de la página, en el espacio de coordenadas escalado, y usted los deja a cero a menos que esté recortando. El ro0 es la rotación: déjelo en cero y PDFium respetará cualquier rotación que la página ya declare en su entrada /Rotate, de modo que una página creada en apaisado sale en apaisado sin que usted haga nada

Codificar el mapa de bits como JPEG

Una vez que el mapa de bits existe, el JPEG es la parte fácil, y es Delphi puro. TJPEGImage.Assign copia el mapa de bits dentro, CompressionQuality fija la calidad en una escala de 1 a 100, y SaveToFile escribe el fichero. La única regla de ordenamiento es que la calidad se tiene que fijar antes de que guarde, 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 parece quisquilloso para un ayudante de una página, y es exactamente lo correcto para un lote. El bloque interior libera el codificador, el bloque exterior libera el mapa de bits, y si cualquiera de los dos salta en una excepción todavía libera lo que posee. Colápselos en uno y una excepción durante la codificación puede abandonar al mapa de bits. En una carrera larga 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 memoria agotada

Elegir los DPI y la calidad conjuntamente

Las dos perillas no son independientes del propósito de la salida, y el error común es subir ambas por precaución. Una miniatura web renderizada a 300 DPI y guardada a una calidad de 95 es un archivo de varios cientos de kilobytes pretendiendo ser una imagen de 120 píxeles; el navegador desecha casi todo en la reducción. Empareje la resolución a los píxeles que la salida realmente requiere, luego escoja una calidad que sobreviva a la compresión con pérdidas del JPEG sin artefactos visibles

SalidaDPICalidad JPEG
Miniatura de lista7260-70
Previsualización en pantalla96-15080-85
Visualización de alto detalle200-30085-95
Maestro de impresión300-60090-100

Vale la pena dar una palabra de precaución sobre la calidad de JPEG por sí sola. No es un dial lineal. El salto de 70 a 85 compra una mejora visual real por un modesto crecimiento del archivo; el salto de 95 a 100 duplica aproximadamente el archivo por una diferencia que casi nadie puede ver, porque la calidad 100 sigue sin ser sin pérdidas, solo deja de desechar mucho. Para las páginas con mucho texto, la compresión basada en bloques del JPEG emborrona los bordes nítidos de los glifos en un tenue timbre, razón por la que una calidad inferior a 80 aproximadamente da lugar a un texto con aspecto de escaneado en lo que debería ser una salida nítida. Si las páginas son en su mayoría texto y usted puede cambiar de formato, el PNG renderiza ese texto sin el timbre; el JPEG se gana su lugar en contenidos fotográficos y mixtos donde su compresión es genuinamente más reducida

Miniaturas más rápidas y ligeras

Cuando el destino es una miniatura antes que una reproducción fiel, usted puede decir al renderizador que realice menos trabajo. El parámetro Options coge un conjunto de banderas TRenderOption, y unas cuantas de ellas intercambian fidelidad por velocidad de exactamente la manera que una previsualización pequeña desea. reGrayscale desecha el color, lo cual se renderiza más deprisa a la vez que produce un mapa de bits más liviano para ser codificado. reNoSmoothImage y reNoSmoothPath se saltan el suavizado anti-aliasing que de todos modos es invisible a la escala de la 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 la miniatura también muestra la forma más limpia de pensar en el dimensionamiento. En lugar de atravesar los DPI, calcule un factor de escala sencillo que encaje la página dentro del cuadro delimitador conservando su relación de aspecto, el cual es lo que hace la función Min de las dos razones. Una página vertical y una apaisada encajan sin distorsiones en el mismo cuadro, y usted no necesita razonar nunca a qué DPI corresponde "encajar en 200 por 280". Hay una advertencia con reGrayscale: convierte el contenido de la imagen rasterizada a gris, pero los rellenos vectoriales y el texto guardan sus valores de color en el motor, por lo que una página que sea en su mayoría arte vectorial puede volver menos monocromática de lo que sugiere el nombre de la bandera. Para un desenlace auténtico totalmente en escala de grises, la vía segura consiste en convertir el mapa de bits renderizado recurriendo a GrayscalePdfBitmap

Procesar por lotes un documento íntegro

Reunir todo para un documento íntegro consiste en un bucle sobre PageCount, trasladando PageNumber una página a la vez. Las páginas tienen base 1: la página uno es PageNumber := 1, y el bucle se ejecuta de modo inclusivo hacia PageCount, no PageCount - 1. La otra circunstancia que el lote debe acatar es el contrato de carga callada. Determinar Active := True nunca arroja excepciones por un fichero dañado ni ante contraseñas equivocadas; tan sólo deja Active figurando como False. Verifíquelo antes de renderizar una sola página, porque de otra forma el primer RenderPage funcionará enfocado sobre un documento el cual no llegó a abrirse

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 de ceros a través de Digits es un pequeño detalle que ahorra una tarde más adelante. Si nombra los ficheros page_1.jpg a page_10.jpg, cualquier herramienta que los clasifique como cadenas colocará page_10 justo después de page_1, alterando el orden. Rellenar hasta la anchura del número de página más alto, de modo que un documento de 300 páginas genere page_001.jpg, mantiene el orden léxico idéntico al orden de las páginas en cualquier fase posterior

Para documentos lo suficientemente grandes como para que la conversión tome un tiempo notable, ejecútela fuera del hilo de la interfaz de usuario (UI thread) o bombee los mensajes entre páginas para que la aplicación siga respondiendo, y ofrezca al usuario una forma de detenerla. Si está renderizando páginas muy grandes y desea una cancelación que tenga efecto en medio de la página y no sólo entre páginas, PDFium Component dispone de una ruta de renderización progresiva con un token de cancelación; es un mecanismo más pesado de lo que la mayoría de las exportaciones por lotes necesitan, pero está ahí para cuando una sola página a 600 DPI sea de por sí lo suficientemente lenta como para bloquear

Una última combinación que merece la pena conocer: rasterizar una página descarta su capa de texto: el JPEG está formado por píxeles, y las palabras que contiene ya no se pueden seleccionar ni buscar. Cuando se necesita tanto la imagen como el texto subyacente, renderice para obtener la imagen y extraiga el texto por separado, algo que aborda el artículo complementario sobre cómo extraer texto de documentos PDF con PDFium Component en Delphi. Las sobrecargas de RenderPage y las opciones de renderización aquí mostradas forman parte del Componente PDFium Component para Delphi y C++Builder