Artículo técnico

Importar vectores EMF y WMF en 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 en operadores PDF en lugar de aplanar el archivo a un mapa de bits: los rellenos degradados se convierten en patrones de sombreado axial de PDF, los pinceles de trama se convierten en patrones de mosaico de PDF, y una puerta centralizada de estado de trazado bloquea que los registros malformados 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 ruta, y la diferencia se nota en cuanto alguien hace zoom en 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 recurre por defecto es rasterizar el metarchivo a un mapa de bits antes de colocarlo en la página, y el costo solo aparece después: un gráfico de barras que se veía nítido en pantalla se vuelve visiblemente pixelado en cuanto el PDF se imprime a 600 DPI o se proyecta en una pantalla de sala de juntas, y una región de CAD rellena con trama colapsa en un solo rectángulo gris plano si el estilo de relleno no se conserva. 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, por lo que vale la pena conocer las trampas siguientes antes de que un informe salga a producción

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

HotPDF mantiene la importación de EMF y WMF en la ruta vectorial porque un metarchivo de Windows es una secuencia grabada de llamadas de dibujo GDI, no una imagen, y reproducir esas llamadas como operadores de trazado, texto y sombreado de PDF es lo que permite que el resultado escale igual que el resto de la página. THPDFPage.ShowMetafile y su contraparte 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 pretende lo contrario: un registro de metarchivo que es genuinamente datos ráster, un blit de mapa de bits StretchDIBits por ejemplo, se incrusta como un verdadero 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 forzarse a operadores de trazado que no pueden expresar una fotografía. Las líneas, rellenos y texto permanecen vectoriales; los píxeles que ya eran píxeles en el origen siguen siendo píxeles en la salida. La llamada más simple 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 espacio de página PDF?

HotPDF responde a eso con una sola pasada sobre el propio flujo de registros del metarchivo, en lugar de una segunda implementación de GDI. THPDFWmf.Analyse lee el encabezado del metarchivo mediante la llamada Win32 GetEnhMetaFileHeader, reinicia su estado de dibujo interno, 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 lienzo de HotPDF para trazados y rellenos. Cada manejador 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 llega con el ancho correcto en puntos PDF sin importar qué extensiones de ventana y viewport haya fijado la aplicación de origen

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

Un registro EMR_GRADIENTFILL se convierte en un verdadero patrón de sombreado axial de PDF tipo 2 (ISO 32000-1 §8.7.4.5) cada vez que GDI lo grabó en uno de los dos modos de rectángulo. THPDFWmf.VEMRGradientFill lee directamente el diseño propio del registro desde el búfer de bytes crudo, siguiendo la estructura de MS-EMF §2.3.1.6: un arreglo de vértices de esquinas RGBA de 16 bits, seguido de una lista de rectángulos que cada uno referencia dos de esos vértices. Para GRADIENT_FILL_RECT_H, los colores se degradan de izquierda a derecha a lo largo de la línea media horizontal del rectángulo; para GRADIENT_FILL_RECT_V, se degradan de arriba a abajo a lo largo de la línea media vertical. En ambos casos, los dos colores de las esquinas y las coordenadas proyectadas del rectángulo van directo 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 hoja de cálculo o el área de trazado degradada de un gráfico conserva su mezcla de colores en lugar de colapsar a un solo color promedio

El modo de triángulos Gouraud es la brecha honesta. Cuando el campo ulMode del registro reporta 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 aproximar con 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 de color, y expresarla correctamente significaría emitir un sombreado de malla PDF 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 un PDF de vuelta. Dos rutas de código sin relación llegan al mismo límite: los sombreados de malla son la brecha tanto en el lado de escritura como en el de lectura, y un diagrama de origen que usa triángulos Gouraud para un brillo radial suave cae de vuelta al último pincel sólido usado, no a una aproximación renderizada

Los pinceles de trama se vuelven patrones de mosaico, no 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 a un relleno sólido, dirigiendo ese caso a SetHatchBrushPattern en su lugar. Ese método escribe un flujo de contenido PDF de 8 por 8 unidades formado por operadores de línea trazada, m, l y S, elegidos según el estilo de trama de GDI: un solo 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 a color (PaintType 1, ISO 32000-1 §8.7.3.1) con un XStep y YStep de 8 unidades, y la página rellena a través de SetFillPattern de la misma manera que lo hace un sombreado axial. Un plano de planta de CAD o un dibujo de ingeniería que se apoya en rellenos de trama para distinguir materiales conserva ese lenguaje visual en el PDF en lugar de perder cada región a un gris idéntico

No todos los pinceles reciben ese trato, y la brecha vale la pena conocerla antes de que una importación de CAD salga a producción. EMR_CREATEDIBPATTERNBRUSHPT, el registro para un pincel de patrón de imagen de mapa de bits personalizado en lugar de uno de los seis estilos de trama estándar de GDI, solo registra su handle para que los registros SELECTOBJECT y DELETEOBJECT posteriores permanezcan consistentes; HotPDF todavía no expone un pipeline de recurso de patrón PDF para imágenes de mosaico arbitrarias, así que seleccionar ese pincel cae en un color sólido de respaldo en lugar de la textura de origen. Si un relleno se renderiza plano donde el original claramente usaba una textura de imagen repetida, el pincel de origen es casi con certeza un patrón DIB personalizado en lugar de una trama estándar, y ese es el único caso que vale la pena verificar a mano primero. Configurar una importación para un dibujo así todavía pasa 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 malformado corrompa la página?

La respuesta de HotPDF es una única puerta al inicio de ExecuteRecord en lugar de una comprobación defensiva repetida en cada uno de sus aproximadamente ochenta manejadores 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, poli-bézier y poli-dibujo, más CLOSEFIGURE y un pequeño conjunto de registros de transformación y estado de DC como SETWORLDTRANSFORM, SAVEDC y RESTOREDC. Cualquier otro tipo de registro que llegue a ExecuteRecord mientras el corchete está abierto, un EXTTEXTOUT perdido o un blit de mapa de bits por ejemplo, se descarta de forma centralizada con un único Exit en el instante en que llega

Esa puerta existe porque un corchete de trazado en un metarchivo escrito a mano, generado por una herramienta, o simplemente corrupto, no tiene garantizado contener solo lo que un archivo bien formado pondría entre sus registros de apertura y cierre. Un registro de salida de texto que caiga entre EMR_BEGINPATH y EMR_ENDPATH, sin una puerta, contaminaría la geometría del trazado en construcción o emitiría un operador de PDF de mostrar texto en medio de una secuencia que se supone es construcción de trazado pura, y ambos modos de fallo son del tipo que aparece con una sola entrada malformada de una herramienta de terceros, no con nada que una suite de pruebas normal suela cubrir. Centralizar la comprobación en ExecuteRecord significa que los manejadores VEMR* individuales no necesitan cada uno defenderse de ser llamados en el momento equivocado; la puerta 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 página

Una página de informe rara vez contiene solo un gráfico, y ShowMetafile se combina con los demás 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 coexistir en la misma página en el mismo flujo de contenido, cada uno conservando su fidelidad nativa, el patrón de composición que cubre 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í se incluyen todos como 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