Artículo técnico

Importar vectores EMF y WMF a PDF de Delphi con HotPDF

HotPDF, el componente PDF nativo para Delphi y C++Builder, importa metarchivos EMF y WMF de Windows interpretando cada registro GDI directamente como operadores PDF en lugar de aplanar el archivo a un mapa de bits: los rellenos degradados se convierten en patrones de sombreado axial PDF, los pinceles de trama se convierten en patrones de mosaico PDF, y una compuerta centralizada de estado de trazado impide que los registros mal formados corrompan la salida. Cualquier gráfico que un TChart, una superficie GDI+ o un simple TCanvas pueda exportar como metarchivo mejorado es candidato para esta vía, y la diferencia se nota en cuanto alguien hace zoom sobre la página o la envía a una impresora de alta resolución

La alternativa a la que la mayoría de los desarrolladores Delphi recurren por defecto es rasterizar el metarchivo a un mapa de bits antes de colocarlo en la página, y el coste solo se manifiesta más tarde: un gráfico de barras que era nítido en pantalla se vuelve visiblemente pixelado en cuanto el PDF se imprime a 600 PPP o se proyecta en la pantalla de una sala de juntas, y una región CAD rellena con trama colapsa en un único rectángulo gris plano si el estilo de relleno no se traslada correctamente. Leer el metarchivo como un programa en lugar de como una imagen es lo que evita ambos problemas, y es el camino más difícil de implementar correctamente, razón por la cual merece la pena conocer las trampas que siguen antes de publicar un informe

¿Por qué interpretar un metarchivo en vez de aplanarlo a un mapa de bits?

HotPDF mantiene la importación de EMF y WMF en la vía vectorial porque un metarchivo de Windows es una secuencia grabada de llamadas de dibujo GDI, no una imagen, y reproducir esas llamadas como operadores PDF de trazado, texto y sombreado es lo que permite que el resultado escale igual que el resto de la página. THPDFPage.ShowMetafile y su homólogo ShowMetafileEx son los puntos de entrada que llama una aplicación, y ambos entregan el metarchivo a THPDFWmf, la clase que recorre cada registro GDI y lo traduce. La distinción no es absoluta, y HotPDF no finge lo contrario: un registro del metarchivo que es realmente datos raster, por ejemplo una transferencia de mapa de bits StretchDIBits, se incrusta como un auténtico XObject de imagen PDF mediante AddImage y ShowImage, el mismo par de llamadas por el que pasa cualquier otra imagen de la página, en lugar de forzarlo a operadores de trazado que no pueden expresar una fotografía. Las líneas, los rellenos y el texto siguen siendo vectoriales; los píxeles que ya eran píxeles en el origen siguen siendo píxeles en la salida. La llamada más sencilla no necesita nada más que el metarchivo cargado:

var
  Pdf: THotPDF;
  Chart: TMetafile;
begin
  Pdf := THotPDF.Create(nil);
  Chart := TMetafile.Create;
  try
    Chart.LoadFromFile('quarterly-revenue.emf');  // exported from TChart or GDI+
    Pdf.FileName := 'quarterly-report.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafile(Chart);
    Pdf.EndDoc;
  finally
    Chart.Free;
    Pdf.Free;
  end;
end;

¿Cómo convierte el intérprete las coordenadas GDI en el espacio de página PDF?

HotPDF responde a esto con una única pasada sobre el propio flujo de registros del metarchivo, en lugar de una segunda implementación de GDI. THPDFWmf.Analyse lee la cabecera del metarchivo mediante la llamada Win32 GetEnhMetaFileHeader, reinicia su estado interno de dibujo y llama a EnumEnhMetafile, la misma API de enumeración que usaría un visor de metarchivos, de modo que cada registro EMR_* llega a THPDFWmf.ExecuteRecord en el orden en que fue grabado originalmente. GDI expresa las coordenadas de arriba hacia abajo en unidades de dispositivo o lógicas elegidas por el propio modo de mapeo del metarchivo; una página PDF va de abajo hacia arriba en puntos de espacio de usuario, el sistema de coordenadas que cubre el modelo de dibujo de canvas de HotPDF para trazados y rellenos. Cada gestor de registro resuelve ese desajuste mediante ScaleX y ScaleY, que llaman a ProjectX y ProjectY para reproducir la propia fórmula de ventana a viewport de GDI para los modos de mapeo anisotrópico e isotrópico, de modo que una forma grabada con cinco unidades lógicas de ancho aterriza con el ancho correcto en puntos PDF, sin importar qué extensiones de ventana y viewport estableciera la aplicación de origen

¿Cómo se convierte un relleno degradado GDI en un patrón de sombreado PDF?

Un registro EMR_GRADIENTFILL se convierte en un auténtico patrón de sombreado axial PDF de Tipo 2 (ISO 32000-1 §8.7.4.5) siempre que GDI lo haya grabado en uno de los dos modos de rectángulo. THPDFWmf.VEMRGradientFill lee la disposición propia del registro directamente del búfer de bytes en bruto, siguiendo la estructura de MS-EMF §2.3.1.6: un array de vértices de esquinas RGBA de 16 bits, seguido de una lista de rectángulos que hacen referencia cada uno a dos de esos vértices. Para GRADIENT_FILL_RECT_H, los colores se desplazan de izquierda a derecha a lo largo de la línea media horizontal del rectángulo; para GRADIENT_FILL_RECT_V, se desplazan de arriba a abajo a lo largo de la línea media vertical. En ambos casos, los dos colores de esquina y las coordenadas proyectadas del rectángulo pasan directamente a THotPDF.RegisterAxialGradient, que devuelve un nombre de patrón, y la página dibuja el rectángulo y lo rellena a través de ese patrón (SetFillPattern) en lugar de una llamada plana a SetRGBFillColor, de modo que un encabezado con bandas al estilo de una hoja de cálculo o el área de trazado degradada de un gráfico conserva su mezcla de color en lugar de colapsar en un único color promedio

El modo de triángulos de Gouraud es la laguna honesta. Cuando el campo ulMode del registro indica GRADIENT_FILL_TRIANGLE, VEMRGradientFill lo reconoce, registra en el log que el modo de triángulos aún no está implementado y omite el rectángulo en lugar de arriesgar una aproximación de dos colores. La interpolación por vértice y por píxel a través de una malla de triángulos arbitraria no se reduce a un sombreado axial o radial de dos paradas, y expresarla correctamente supondría emitir un sombreado de malla PDF de Tipo 4 o Tipo 5, la misma familia de sombreados que el renderizador de páginas de HotPDF también deja sin pintar al leer de vuelta un PDF. Dos vías de código sin relación entre sí acaban en el mismo límite: los sombreados de malla son la laguna tanto en el lado de escritura como en el de lectura, y un diagrama de origen que use triángulos de Gouraud para un resplandor radial suave termina reduciéndose al último pincel sólido usado, no a una aproximación renderizada

Los pinceles de trama se convierten en patrones de mosaico, no en gris aplanado

Un pincel de trama de GDI conserva su textura en el PDF porque THPDFWmf.SetBrushColor comprueba CurrentBrush.lbStyle en busca de BS_HATCHED antes de recurrir jamás a un relleno sólido, encaminando ese caso hacia SetHatchBrushPattern. Ese método escribe un flujo de contenido PDF de 8 por 8 unidades formado por operadores de trazo de línea, m, l y S, elegidos según el estilo de trama de GDI: un único trazo horizontal o vertical para HS_HORIZONTAL y HS_VERTICAL, tres diagonales paralelas para HS_FDIAGONAL y HS_BDIAGONAL, y las combinaciones horizontal más vertical o ambas diagonales para HS_CROSS y HS_DIAGCROSS. THotPDF.RegisterTilingPattern registra ese flujo de contenido como un patrón de mosaico coloreado (PaintType 1, ISO 32000-1 §8.7.3.1) con un XStep y un YStep de 8 unidades, y la página rellena a través de SetFillPattern del mismo modo que lo haría un sombreado axial. Un plano de planta CAD o un dibujo de ingeniería que se apoye en rellenos de trama para distinguir materiales conserva ese lenguaje visual en el PDF en lugar de perder todas las regiones en un mismo gris

No todos los pinceles reciben ese tratamiento, y merece la pena conocer esta laguna antes de publicar una importación CAD. EMR_CREATEDIBPATTERNBRUSHPT, el registro de un pincel de patrón de imagen de mapa de bits personalizado en lugar de uno de los seis estilos de trama de fábrica de GDI, solo registra su handle para que los posteriores registros SELECTOBJECT y DELETEOBJECT se mantengan coherentes; HotPDF todavía no expone un flujo de recursos de patrón PDF para imágenes de mosaico arbitrarias, así que seleccionar ese pincel recae en un respaldo de color sólido en lugar de en la textura de origen. Si un relleno se muestra plano donde el original usaba claramente una textura de imagen repetida, el pincel de origen es casi con toda seguridad un patrón DIB personalizado en lugar de una trama estándar, y ese es el único caso que merece la pena comprobar a mano primero. Configurar una importación para un dibujo de ese tipo sigue pasando por el mismo objeto de opciones:

var
  Pdf: THotPDF;
  Drawing: TMetafile;
  Options: THPDFEmfOptions;
begin
  Pdf := THotPDF.Create(nil);
  Drawing := TMetafile.Create;
  Options := THPDFEmfOptions.Create;
  try
    Drawing.LoadFromFile('floor-plan.emf');
    Options.Assign(Pdf.EmfOptions);   // start from the document-wide defaults
    Options.Redraw := False;          // interpret the original EMF bytes, no GDI re-record pass
    Options.ShowNullBrush := True;    // keep explicitly unfilled CAD regions visible
    Options.UseFrame := True;         // clip output to the frame the EMF header declares
    Pdf.FileName := 'floor-plan.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
    Pdf.EndDoc;
  finally
    Options.Free;
    Drawing.Free;
    Pdf.Free;
  end;
end;

¿Qué impide que un metarchivo mal formado corrompa la página?

La respuesta de HotPDF es una única compuerta en lo alto de ExecuteRecord, en lugar de una comprobación defensiva repetida en cada uno de sus aproximadamente ochenta gestores de registro. Un corchete de trazado GDI, abierto por EMR_BEGINPATH y cerrado por EMR_ENDPATH o EMR_ABORTPATH, se rastrea mediante una propiedad privada PathContinue respaldada por el campo FPathContinue. Mientras ese corchete está abierto, ExecuteRecord solo deja pasar registros de construcción de trazado, las variantes de mover, línea, polilínea, polígono, polibézier y polidibujo, además de CLOSEFIGURE y un pequeño conjunto de registros de transformación y estado del contexto de dispositivo como SETWORLDTRANSFORM, SAVEDC y RESTOREDC. Cualquier otro tipo de registro que llegue a ExecuteRecord mientras el corchete está abierto, un EXTTEXTOUT perdido o una transferencia de mapa de bits, por ejemplo, se descarta de forma centralizada con un único Exit en el instante en que llega

Esa compuerta existe porque un corchete de trazado en un metarchivo escrito a mano, generado por una herramienta o simplemente corrupto no tiene garantizado contener únicamente lo que un archivo bien formado colocaría entre sus registros de apertura y cierre. Un registro de salida de texto que aterrizara entre EMR_BEGINPATH y EMR_ENDPATH, sin una compuerta, contaminaría la geometría del trazado en construcción o emitiría un operador PDF de mostrar texto en medio de una secuencia que se supone que es construcción de trazado pura, y ambos modos de fallo son del tipo que aflora con una entrada mal formada concreta de una herramienta de terceros, no con nada que una batería de pruebas normal suela cubrir. Centralizar la comprobación en ExecuteRecord significa que los gestores individuales VEMR* no necesitan defenderse cada uno por separado de ser invocados en el momento equivocado; la compuerta lo decide una sola vez, antes del despacho, en lugar de ochenta veces después

Colocar un gráfico vectorial junto a texto e imágenes en una misma página

Una página de informe rara vez contiene solo un gráfico, y ShowMetafile se compone con el resto de operadores de página de HotPDF exactamente igual que cualquier otra llamada de dibujo. Un encabezado dibujado con TextOut, un gráfico de barras con relleno de trama importado como EMF y un logotipo colocado con ShowImage pueden convivir en la misma página y en el mismo flujo de contenido, cada uno conservando su fidelidad nativa, el patrón de composición cubierto en la guía de HotPDF para maquetar texto, fuentes e imágenes en un informe:

Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart);   // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);

El intérprete de EMF y WMF, los patrones de sombreado axial que registra para los rellenos degradados y el mapeo a patrones de mosaico para los pinceles de trama descritos aquí forman parte del componente HotPDF estándar para Delphi y C++Builder, una biblioteca VCL nativa sin dependencia de ninguna DLL externa para nada de esto