Artículo técnico

Importación EMF en PDFlibPas: PolyDraw, Polyline y Bezier

PDFlibPas, la biblioteca PDF de losLab para Delphi, convierte los registros EMF Poly* en trazados PDF siguiendo la definición de cada registro en [MS-EMF]: un EMR_POLYBEZIER de 32 bits empieza en el punto 0, las polylines quedan abiertas y solo se trazan, PT_CLOSEFIGURE en EMR_POLYDRAW es un flag, y cada recuento de puntos se comprueba contra el tamaño del registro. Esas reglas llegaron repartidas entre v3.539.39, v3.539.41 y v3.539.43. Antes de ellas, el gráfico de un informe podía salir de ImportEMFFromFile con un sector relleno donde debía ir una línea de tendencia, una curva Bezier doblada hacia el punto de control equivocado o un contorno cerrado al que le faltaba su último lado. Ninguno de estos fallos lanzaba un error, y las reglas valen para cualquier conversor Delphi de EMF a PDF o para cualquier parser de registros GDI

¿Por qué fallan los registros EMF Poly* en la conversión a PDF?

Los registros EMF Poly* fallan porque cada uno lleva parte de su significado fuera de sus puntos: si la figura queda abierta, si arranca en la posición actual, qué lápiz y qué pincel se aplican y en qué byte del registro empiezan los puntos. Un enhanced metafile es una grabación de llamadas GDI contra un device context, así que un conversor tiene que reproducir ese estado del device context además de las coordenadas. PDF no tiene device context: tiene un trazado, un punto actual dentro de ese trazado y un operador de pintado que decide entre stroke (S), fill (f) o ambos (B). Cada desajuste entre los dos modelos se convierte en una diferencia de renderizado silenciosa

La familia Poly* existe además en dos anchos. Cada registro de 32 bits como EMR_POLYLINE tiene un gemelo de 16 bits como EMR_POLYLINE16 que guarda los puntos como pares SmallInt. GDI suele grabar la forma compacta cuando todas las coordenadas caben, de modo que los manejadores de 32 bits de un conversor pueden llevar años equivocados mientras los dibujos de prueba de siempre nunca llegan a ellos. La auditoría más rápida es pasar los mismos puntos por ambos registros y comparar los trazados resultantes. Los registros que cubre este artículo están todos en el grupo de registros de dibujo de [MS-EMF] (2.3.5 Drawing Record Types)

RegistroEmpieza en¿Cerrado?Posición actual
EMR_POLYBEZIERPunto 0NoNo se usa, no se actualiza
EMR_POLYLINEPunto 0No (solo lápiz)No se usa, no se actualiza
EMR_POLYLINETOPosición actualNo (solo lápiz)Se usa y se actualiza
EMR_POLYPOLYLINEPrimer punto de cada polylineNo (solo lápiz)No se usa, no se actualiza
EMR_POLYDRAWPrimer PT_MOVETO, o posición actualSolo donde PT_CLOSEFIGURE está activoSe usa y se actualiza

¿Dónde empieza realmente una curva EMR_POLYBEZIER?

Una curva EMR_POLYBEZIER empieza en el punto 0, y solo los puntos a partir del índice 1 se agrupan de tres en tres como punto de control, punto de control y punto final. Un registro de 7 puntos dibuja por tanto dos segmentos cúbicos: el 0 es el inicio, del 1 al 3 va el primer segmento y del 4 al 6 el segundo. El manejador de 16 bits de PDFlibPas ya lo hacía así. El de 32 bits empezaba a agrupar en el punto 0, de manera que el punto de inicio se consumía como primer punto de control y todos los segmentos posteriores quedaban desplazados uno. La curva se renderizaba igual, solo que era la equivocada. Desde v3.539.41 ambos anchos abren el trazado con m en el punto 0 y emiten una c por cada terna completa posterior

Diagrama de PDFlibPas de un registro EMR_POLYBEZIER con siete puntos donde el punto cero abre el trazado con m y los puntos uno a tres y cuatro a seis forman cada uno un segmento cúbico c, contrastando el manejador de 32 bits corregido desde v3.539.41 con la agrupación antigua que consumía el punto de inicio como punto de control
El punto 0 es el punto de inicio y solo las ternas completas posteriores se convierten en segmentos cúbicos, así que una PolyBezier de siete puntos se renderiza como m más dos operadores c

Para su propio parser: un recuento que no sea 1 más un múltiplo de 3 está malformado, y los puntos sobrantes conviene ignorarlos en lugar de coserlos a la curva

PolyDraw: PT_CLOSEFIGURE es un flag, no un tipo de punto

En EMR_POLYDRAW, PT_CLOSEFIGURE (valor 1) es un bit que se combina con PT_LINETO (2) o con PT_BEZIERTO (4), de modo que un byte de tipo válido puede ser 3 o 5. El tipo de punto es el byte con ese bit enmascarado, y el flag significa cerrar la figura tras el segmento que termina en ese punto. El antiguo manejador de PDFlibPas comparaba el byte con valores sueltos en un case, así que los puntos de tipo 3 y 5 no coincidían con nada y se saltaban por completo. Un rectángulo dibujado con PolyDraw perdía su lado de cierre, y a una terna Bezier cuyo último punto llevaba el flag se le perdía ese punto, lo que desalineaba todas las ternas posteriores

Desde v3.539.39 el tipo se lee como Types[i] and not PT_CLOSEFIGURE, y el cierre solo se emite tras un segmento completo: tras la línea en un PT_LINETO cerrado, y tras el tercer punto de un grupo Bezier. Un archivo malformado que ponga el flag en el primer o segundo punto de una terna no cierra la figura antes de tiempo. En la misma versión entraron dos correcciones relacionadas:

  • Cada PT_MOVETO del EMR_POLYDRAW16 de 16 bits reiniciaba el trazado entero, de modo que un registro con tres figuras conservaba solo la última; ahora el primer move arranca el trazado y los moves posteriores abren subtrazados
  • Un registro PolyDraw que no empiece con PT_MOVETO arranca en la posición actual, como dice la definición del registro, en lugar de escribir un operador l o c sin ningún m previo
Anatomía en PDFlibPas de un byte de tipo de EMR_POLYDRAW donde PT_CLOSEFIGURE es el bit de flag cero combinado con OR en PT_LINETO o PT_BEZIERTO, de modo que los bytes de tipo válidos 3 y 5 deben enmascararse con and not PT_CLOSEFIGURE antes del dispatch; el case antiguo saltaba ambos bytes y las figuras cerradas perdían su último lado
Enmascare el flag de cierre antes del dispatch y emita el cierre solo tras una línea o una terna Bezier completas, o PolyDraw descartará puntos en silencio

¿Por qué una polyline EMF nunca debe rellenarse en PDF?

Una polyline EMF nunca debe rellenarse porque EMR_POLYLINE y EMR_POLYPOLYLINE son figuras abiertas dibujadas solo con el lápiz, y rellenar un trazado abierto en PDF lo cierra implícitamente. ISO 32000-1 §8.5.3 establece que los operadores de relleno cierran cualquier subtrazado abierto antes de pintarlo. Un conversor que emita B o f para una polyline de tres puntos pinta por tanto un triángulo relleno con el color del pincel en vigor: el sector relleno bajo una línea de tendencia de un gráfico. Antes de v3.539.41, PDFlibPas rellenaba ambos anchos de polyline con el pincel, y el registro de 32 bits además se cerraba explícitamente. Hoy ambos anchos terminan solo con stroke, y la distinción GDI se conserva: Polygon cierra y rellena, Polyline nunca lo hace

Comparación en PDFlibPas de una polyline abierta en V exportada desde EMR_POLYLINE: un conversor correcto termina el trazado con el operador stroke S e ignora el pincel seleccionado, mientras que emitir f o B cierra el subtrazado abierto implícitamente según ISO 32000-1 8.5.3 y pinta el sector relleno típico del fallo en gráficos
Un operador de relleno cierra cualquier subtrazado abierto antes de pintarlo, así que las polylines deben terminar en S sin ningún h, f o B sobre el subtrazado

PolylineTo empieza en la posición actual

EMR_POLYLINETO dibuja desde la posición actual por cada punto del registro, queda abierta y deja la posición actual en el último punto. El antiguo manejador contenía además un caso especial que apagaba el lápiz cuando los dos primeros puntos compartían la coordenada y, y nada volvía a encenderlo, así que todos los registros posteriores del archivo perdían su contorno. El estado del lápiz pertenece a EMR_SELECTOBJECT y EMR_CREATEPEN; el manejador de un registro de dibujo no tiene nada que hacer cambiándolo. Ese caso especial se eliminó en v3.539.41, y la forma de un solo punto del registro ya no lee más allá de sus propios puntos (corregido en v3.539.39)

Los puntos de PolyPolyline empiezan tras el array de recuentos

El EMR_POLYPOLYLINE de 32 bits guarda nPolys recuentos y luego cptl puntos, y los puntos empiezan en el offset de byte 32 + nPolys * 4. La trampa está en la RTL: la unidad Windows declara TEMRPolyPolyline con aPolyCounts y aptl como arrays de un elemento, así que aptl[0] solo es el primer punto cuando nPolys vale 1. El código que indexa aptl directamente lee valores de recuento como coordenadas en cada registro multilínea. El antiguo manejador de PDFlibPas dimensionaba además su comprobación de límites sobre esa disposición equivocada, de modo que rechazaba registros multilínea válidos y los de una sola línea no dibujaban nada. Desde v3.539.41 PDFlibPas localiza el array de puntos desde el offset calculado, como siempre hizo su manejador de PolyPolygon, y dibuja cada polyline como su propio subtrazado abierto con un único stroke al final. En v3.539.43 el gemelo de 16 bits recibió el mismo trato; venía dibujando segmento a segmento, lo que rompía las uniones de línea e ignoraba un NULL_PEN seleccionado

El lápiz y el pincel por defecto, y los corchetes de trazado

Dos reglas de estado completan las correcciones de polyline de v3.539.43:

  • Un device context GDI recién creado ya tiene BLACK_PEN y WHITE_BRUSH seleccionados, de modo que un metafile que dibuje sin ningún EMR_SELECTOBJECT sigue dibujando contornos negros; el conversor arrancaba sin lápiz ni relleno y escribía n (fin de trazado, nada pintado) para esos registros
  • Dentro de un corchete BeginPath / EndPath, una Polyline ni usa ni actualiza la posición actual, así que debe abrir un subtrazado nuevo en su primer punto en lugar de conectarse a la figura anterior, y nada puede pintarse hasta que el corchete se trace o se rellene

Construir un archivo EMF de prueba con TMetafileCanvas

La manera más rápida de comprobar un conversor contra estas reglas es grabar las tres llamadas de riesgo en un solo enhanced metafile con TMetafileCanvas. El dibujo de abajo graba las curvas con un pincel hueco y luego selecciona a propósito un pincel amarillo sólido para la polyline: un conversor correcto debe ignorar ese pincel para la polyline, de modo que cualquier amarillo en el PDF resultante es un bug. PolyDraw no tiene envoltorio en TCanvas, así que se llama por la API de Windows con el handle del canvas, usando bytes de tipo 3 y 5 para ejercitar el flag de cierre

uses
  Winapi.Windows, System.Types, Vcl.Graphics;

procedure BuildPolyTestEmf(const FileName: string);
const
  // Un cuadrado cerrado (3 = LINETO + CLOSEFIGURE) y luego una
  // figura Bezier cerrada cuya última terna termina en 5 = BEZIERTO + CLOSEFIGURE
  DrawPts: array[0..7] of TPoint = (
    (X: 300; Y: 40), (X: 380; Y: 40), (X: 380; Y: 120), (X: 300; Y: 120),
    (X: 420; Y: 120), (X: 440; Y: 40), (X: 520; Y: 40), (X: 540; Y: 120));
  DrawTypes: array[0..7] of Byte = (
    PT_MOVETO, PT_LINETO, PT_LINETO, PT_LINETO or PT_CLOSEFIGURE,
    PT_MOVETO, PT_BEZIERTO, PT_BEZIERTO, PT_BEZIERTO or PT_CLOSEFIGURE);
var
  Mf: TMetafile;
  Canvas: TMetafileCanvas;
begin
  Mf := TMetafile.Create;
  try
    Mf.Enhanced := True;
    Mf.Width := 600;
    Mf.Height := 260;
    Canvas := TMetafileCanvas.Create(Mf, 0);
    try
      Canvas.Pen.Color := clNavy;
      Canvas.Pen.Width := 2;
      Canvas.Brush.Style := bsClear;    // solo contornos para las curvas
      // El punto 0 es el inicio; 1..3 y 4..6 son dos segmentos cúbicos
      Canvas.PolyBezier([Point(20, 120), Point(60, 20), Point(100, 220),
        Point(140, 120), Point(180, 20), Point(220, 220), Point(260, 120)]);
      PolyDraw(Canvas.Handle, DrawPts[0], DrawTypes[0], Length(DrawPts));
      // V abierta con un pincel sólido seleccionado: se traza y nunca
      // se cierra en un triángulo amarillo
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // termina la grabación
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Como estas coordenadas caben en un SmallInt, GDI normalmente guardará las variantes de 16 bits. Para llegar a los manejadores de 32 bits necesita un productor que los escriba, o registros construidos a mano. Los archivos hechos a mano traen su propia trampa: TMetafile.LoadFromStream de la VCL solo trata el stream como un EMF cuando la longitud restante es estrictamente mayor que los 108 bytes del TEnhMetaHeader. Un EMF mínimo escrito a mano con cabecera corta, o uno vacío de exactamente 108 bytes, se toma por un WMF y se rechaza con "Metafile is not valid". Escriba siempre la cabecera completa de 108 bytes, campos de extensión incluidos, antes de sus registros de prueba

Importar el EMF a un PDF con PDFlibPas

PDFlibPas importa un EMF con ImportEMFFromFile o ImportEMFFromStream, que devuelven un ID de imagen distinto de cero si tienen éxito y 0 si fallan. GeneralOptions = 0 conserva el trazado vectorial del que trata este artículo; 1 rasteriza el metafile a un bitmap. FontOptions = 1 añade las fuentes del metafile como fuentes TrueType no incrustadas. La variante de stream rebobina el stream a la posición 0 antes de cargar, así que pase un stream que contenga solo el metafile

uses
  System.SysUtils, PDFlibrary;

procedure EmfToPdf(const EmfFile, PdfFile: WideString);
var
  PDF: TPDFlib;
  ImageID: Integer;
  PageOps: AnsiString;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);              // origen arriba a la izquierda para DrawImage
    PDF.SetMeasurementUnits(0);    // puntos
    // FontOptions 1 = añade las fuentes como TrueType no incrustadas
    // GeneralOptions 0 = importación vectorial, 1 = bitmap
    ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
    if ImageID = 0 then
      raise Exception.Create('The metafile could not be imported');
    PDF.SelectImage(ImageID);
    // En un EMF, ImageWidth / ImageHeight son el tamaño del marco en puntos
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // La página solo invoca el formulario importado: q ... cm /Name Do Q
    PageOps := PDF.GetPageContentToString;
    if Pos(AnsiString(' Do'), PageOps) = 0 then
      raise Exception.Create('Expected a form XObject invocation');

    if PDF.SaveToFile(PdfFile) <> 1 then
      raise Exception.Create('The PDF could not be saved');
  finally
    PDF.Free;
  end;
end;

Una importación vectorial de EMF se convierte en un form XObject, así que GetPageContentToString devuelve solo la secuencia save, transform, Do y restore. Los operadores m, l, c, h y S producidos a partir de los registros Poly* viven en el stream del form XObject, que va comprimido. Para auditarlos, descomprima el archivo guardado en un inspector de objetos PDF y lea el stream del formulario: en el archivo de prueba de arriba debería ver la polyline terminar en S sin ningún h delante, un h en cada flag de cierre de las figuras PolyDraw, y ningún f ni B sobre estos subtrazados. DrawImage también escala un EMF importado de forma uniforme por el menor de Width y Height, de modo que el dibujo conserva su proporción aunque la caja que usted pase no la respete

Para objetivos Free Pascal, vea cómo compila el importador vectorial EMF de PDFlibPas bajo Free Pascal; la semántica de los registros es la misma dondequiera que compile el importador

¿Cómo debe tratar un parser EMF los recuentos de puntos del archivo?

Un parser EMF debe tratar cada recuento de puntos como entrada no confiable y comprobarlo contra el tamaño del registro antes de copiar un solo punto. EnumEnhMetaFile solo garantiza que cada nSize de registro quede dentro del archivo. No comprueba que cptl concuerde con nSize, así que un manejador que copie cptl puntos con Move leerá los registros siguientes, o pasará del final del metafile, cuando el recuento esté falsificado o corrompido. Desde v3.539.39 PDFlibPas comprueba cabecera fija más recuento por bytes por punto contra nSize para PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo y Polygon en ambos anchos, con un byte extra por punto para los bytes de tipo de PolyDraw. En los registros PolyPoly los recuentos por figura deben además sumar como mucho el total declarado, y las figuras de cero puntos se saltan

La misma comprobación es lo bastante corta para copiarla a su propio parser. Esta versión valida un EMR_POLYPOLYLINE de 32 bits y devuelve un puntero a su array real de puntos:

uses
  Winapi.Windows;

// Devuelve nil salvo que el registro contenga de verdad los puntos que
// declara. Los puntos empiezan tras el array de recuentos: 32 + nPolys * 4
// bytes hacia dentro, no en aptl[0], que la RTL declara como array de un elemento
function PolyPolylinePoints(Rec: PEnhMetaRecord): PPoint;
var
  P: PEMRPolyPolyline;
  Count: PDWORD;
  PointsOffset, Total: Int64;
  I: Cardinal;
begin
  Result := nil;
  if (Rec^.iType <> EMR_POLYPOLYLINE) or (Rec^.nSize < 32) then
    Exit;
  P := PEMRPolyPolyline(Rec);
  if P^.nPolys = 0 then
    Exit;
  PointsOffset := 32 + Int64(P^.nPolys) * SizeOf(DWORD);
  if PointsOffset + Int64(P^.cptl) * SizeOf(TPoint) > Rec^.nSize then
    Exit;                         // recuento falsificado o truncado
  Total := 0;
  Count := @P^.aPolyCounts[0];    // recorrer por puntero: [0..0] dispara range checks
  for I := 1 to P^.nPolys do
  begin
    Inc(Total, Count^);
    Inc(Count);
  end;
  if Total > P^.cptl then
    Exit;                         // las figuras reclaman más puntos de los que hay
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

El test de cabida de los puntos se ejecuta primero, así que el array de recuentos ya se sabe que está dentro del registro cuando el bucle lo recorre. La aritmética es Int64 porque nPolys * 4 y cptl * 8 calculados en 32 bits pueden dar la vuelta y pasar la comparación

Referencia rápida: reglas EMF Poly* para la conversión de EMF a PDF

  • EMR_POLYBEZIER: el punto 0 es el punto de inicio; agrupe desde el punto 1 de tres en tres; corregido para el registro de 32 bits en v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: figuras abiertas, stroke con S, nunca h, f ni B, porque el fill de PDF cierra los subtrazados abiertos
  • EMR_POLYLINETO: empiece en la posición actual, quede abierta, actualice la posición actual y no toque nunca el estado del lápiz
  • EMR_POLYPOLYLINE de 32 bits: los puntos empiezan en el byte 32 + nPolys * 4, no en aptl[0]
  • EMR_POLYDRAW: enmascare PT_CLOSEFIGURE antes del dispatch, cierre tras el segmento completado y arranque en la posición actual cuando el primer punto no sea PT_MOVETO
  • El estado por defecto del device context es BLACK_PEN más WHITE_BRUSH; v3.539.43 y posteriores lo respetan
  • Dentro de BeginPath / EndPath, cada polyline abre su propio subtrazado y nada se pinta hasta que se usa el corchete
  • Valide cada cptl / cpts contra nSize en aritmética de 64 bits antes de copiar puntos
  • Los EMF de prueba hechos a mano necesitan la cabecera completa de 108 bytes, o TMetafile.LoadFromStream los leerá como WMF

Si sus informes pasan por otro componente, la semántica de los registros es la misma; la importación vectorial EMF y WMF de HotPDF explica cómo ese componente convierte pinceles de degradado y de tramado en patterns de PDF, y gráficos vectoriales, shaders y degradados en PDFlibPas cubre dibujar las mismas formas directamente con la API de la biblioteca en lugar de pasar por un metafile

PDFlibPas v3.539.43 o posterior incluye todas las reglas anteriores. Detalles y descargas de prueba en la página de producto de la biblioteca PDF PDFlibPas para Delphi