PDFlibPas, la librería de PDF de losLab para Delphi, convierte los registros Poly* de EMF en paths de PDF siguiendo la definición de cada registro en [MS-EMF]: un EMR_POLYBEZIER de 32 bits arranca en el punto 0, las polilíneas quedan abiertas y solo reciben stroke, PT_CLOSEFIGURE en EMR_POLYDRAW es un flag, y cada conteo de puntos se valida contra el tamaño del registro. Esas reglas llegaron entre v3.539.39, v3.539.41 y v3.539.43. Antes de ellas, el gráfico de un reporte podía salir de ImportEMFFromFile con una cuña rellena 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 casos levantaba un error, y las reglas valen para cualquier conversor de EMF a PDF en Delphi o parser de registros GDI
¿Por qué los registros Poly* de EMF fallan al convertir a PDF?
Los registros Poly* de EMF fallan porque cada uno carga parte de su significado fuera de sus puntos: si la figura queda abierta, si arranca en la posición actual, qué pluma y qué pincel aplican, y en qué parte 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 path, un punto actual dentro de ese path, 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 render silenciosa
La familia Poly* además viene 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 de SmallInt. GDI normalmente graba la forma compacta cuando cada coordenada cabe, así que los handlers de 32 bits de un conversor pueden estar rotos durante años mientras los dibujos de prueba de todos los días nunca llegan a ellos. La auditoría más rápida es pasar los mismos puntos por ambos registros y comparar los paths 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)
| Registro | Arranca en | ¿Cerrado? | Posición actual |
|---|---|---|---|
EMR_POLYBEZIER | Punto 0 | No | No se usa, no se actualiza |
EMR_POLYLINE | Punto 0 | No (solo pluma) | No se usa, no se actualiza |
EMR_POLYLINETO | Posición actual | No (solo pluma) | Se usa y se actualiza |
EMR_POLYPOLYLINE | Primer punto de cada polilínea | No (solo pluma) | No se usa, no se actualiza |
EMR_POLYDRAW | Primer PT_MOVETO, o posición actual | Solo donde PT_CLOSEFIGURE está activo | Se usa y se actualiza |
¿Dónde arranca en realidad la curva de un EMR_POLYBEZIER?
La curva de un EMR_POLYBEZIER arranca en el punto 0, y solo los puntos desde el índice 1 en adelante se agrupan de a tres como punto de control, punto de control, punto final. Un registro con 7 puntos dibuja entonces dos segmentos cúbicos: el 0 es el arranque, el 1 al 3 forman el primer segmento, el 4 al 6 el segundo. El handler de 16 bits de PDFlibPas ya lo hacía así. El de 32 bits empezaba a agrupar en el punto 0, de modo que el punto de arranque se consumía como primer punto de control y cada segmento posterior quedaba corrido uno. La curva se dibujaba igual, solo que era la equivocada. Desde v3.539.41 ambos anchos abren el path con m en el punto 0 y emiten un c por cada terceto completo posterior
Para su propio parser: un conteo que no sea 1 más un múltiplo de 3 está mal formado, y los puntos sobrantes conviene ignorarlos en vez 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 PT_BEZIERTO (4), así 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 después del segmento que termina en ese punto. El handler viejo de PDFlibPas comparaba el byte contra 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 un terceto Bezier cuyo último punto traía el flag perdía ese punto, lo que desincronizaba todos los tercetos posteriores
Desde v3.539.39 el tipo se lee como Types[i] and not PT_CLOSEFIGURE, y el cierre se emite solo después de un segmento completo: tras la línea para un PT_LINETO cerrado, y tras el tercer punto de un grupo Bezier. Un archivo mal formado que ponga el flag en el primer o segundo punto de un terceto no cierra la figura antes de tiempo. Dos correcciones relacionadas salieron en la misma versión:
- Cada
PT_MOVETOdelEMR_POLYDRAW16de 16 bits reiniciaba el path entero, así que un registro con tres figuras conservaba solo la última; ahora el primer move arranca el path y los moves posteriores abren subpaths - Un registro PolyDraw que no empiece con
PT_MOVETOarranca en la posición actual, como dice la definición del registro, en vez de escribir un operadorlocsin unmprevio
¿Por qué una polilínea EMF jamás debe rellenarse en PDF?
Una polilínea EMF jamás debe rellenarse porque EMR_POLYLINE y EMR_POLYPOLYLINE son figuras abiertas que se dibujan solo con la pluma, y rellenar un path abierto en PDF lo cierra implícitamente. ISO 32000-1 §8.5.3 dice que los operadores de fill cierran cualquier subpath abierto antes de pintarlo. Un conversor que emita B o f para una polilínea de tres puntos pinta entonces un triángulo relleno con el color del pincel actual: la cuña rellena bajo la línea de tendencia de un gráfico. Antes de v3.539.41, PDFlibPas rellenaba ambos anchos de polilínea 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 de GDI se preserva: Polygon cierra y rellena, Polyline nunca lo hace
PolylineTo arranca en la posición actual
EMR_POLYLINETO dibuja desde la posición actual pasando por cada punto del registro, queda abierta, y deja la posición actual en el último punto. El handler viejo además traía un caso especial que apagaba la pluma cuando los primeros dos puntos compartían la coordenada y, y nada volvía a encenderla, así que todos los registros posteriores del archivo perdían su contorno. El estado de la pluma es asunto de EMR_SELECTOBJECT y EMR_CREATEPEN; un handler de registro de dibujo no tiene por qué tocarlo. Ese caso especial se eliminó en v3.539.41, y la forma de un punto del registro ya no lee más allá de sus propios puntos (corregido en v3.539.39)
Los puntos de PolyPolyline empiezan después del array de conteos
El EMR_POLYPOLYLINE de 32 bits guarda nPolys conteos 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] es el primer punto solo cuando nPolys vale 1. Código que indexa aptl directo lee valores de conteo como coordenadas en cada registro multilínea. El handler viejo de PDFlibPas además dimensionaba su chequeo de límites sobre ese layout equivocado, así 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 handler de PolyPolygon, y dibuja cada polilínea como su propio subpath abierto con un solo stroke al final. En v3.539.43 el gemelo de 16 bits recibió el mismo tratamiento; venía dibujando segmento por segmento, lo que rompía las uniones de línea e ignoraba un NULL_PEN seleccionado
La pluma y el pincel por defecto, y los brackets de path
Dos reglas de estado completan las correcciones de polilínea en v3.539.43:
- Un device context GDI recién creado ya trae
BLACK_PENyWHITE_BRUSHseleccionados, así que un metafile que dibuja sin ningúnEMR_SELECTOBJECTsigue dibujando contornos negros; el conversor arrancaba sin pluma ni fill y escribían(fin de path, no pintar nada) para esos registros - Dentro de un bracket
BeginPath/EndPath, unaPolylineni usa ni actualiza la posición actual, así que debe abrir un subpath nuevo en su primer punto en vez de conectarse a la figura anterior, y nada se puede pintar hasta que el bracket reciba stroke o fill
Cómo armar un archivo EMF de prueba con TMetafileCanvas
La forma más rápida de probar un conversor contra estas reglas es grabar las tres llamadas riesgosas en un solo enhanced metafile con TMetafileCanvas. El dibujo de abajo graba las curvas con un pincel hueco y después selecciona a propósito un pincel amarillo sólido para la polilínea: un conversor correcto debe ignorar ese pincel para la polilínea, así que cualquier amarillo en el PDF de salida es un bug. PolyDraw no tiene wrapper en TCanvas, así que se llama vía 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), luego una figura
// Bezier cuyo último terceto de control termina con 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 arranque; 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 pincel sólido seleccionado: se traza, 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 handlers de 32 bits necesita un productor que los escriba, o registros armados 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 el TEnhMetaHeader de 108 bytes. Un EMF minimalista escrito a mano con header corto, o uno vacío de exactamente 108 bytes, se toma por un WMF y se rechaza con "Metafile is not valid". Escriba siempre el header completo 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 en caso de éxito y 0 en caso de falla. GeneralOptions = 0 conserva el path vectorial del que trata este artículo; 1 rasteriza el metafile a un bitmap. FontOptions = 1 agrega las fuentes del metafile como fuentes TrueType no embebidas. La variante por 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 = agrega las fuentes como TrueType no embebidas
// GeneralOptions 0 = import 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);
// Para un EMF, ImageWidth / ImageHeight son el tamaño del frame en puntos
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// La página solo invoca el form 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 de 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 está comprimido. Para auditarlos, descomprima el archivo guardado en un inspector de objetos PDF y lea el stream del form: para el archivo de prueba de arriba debería ver la polilínea terminar en S sin ningún h antes, un h en cada flag de cierre de las figuras PolyDraw, y ningún f ni B en esos subpaths. DrawImage además escala un EMF importado de forma uniforme por el menor entre Width y Height, así que el dibujo conserva su relación de aspecto aunque la caja que le pase no la respete
Para targets de Free Pascal, vea cómo compila el importador vectorial de 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 de EMF los conteos de puntos que vienen del archivo?
Un parser de EMF debe tratar cada conteo de puntos como entrada no confiable y validarlo contra el tamaño del registro antes de copiar un solo punto. EnumEnhMetaFile solo garantiza que el nSize de cada registro quede dentro del archivo. No chequea que cptl concuerde con nSize, así que un handler que copie cptl puntos con Move leerá los registros siguientes, o más allá del final del metafile, cuando el conteo esté falsificado o corrupto. Desde v3.539.39 PDFlibPas valida header fijo más conteo 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 conteos por figura además deben sumar como máximo el total declarado, y las figuras de cero puntos se saltan
El mismo chequeo es tan corto que puede copiarlo a su propio parser. Esta versión valida un EMR_POLYPOLYLINE de 32 bits y devuelve un puntero a su array de puntos real:
uses
Winapi.Windows;
// Devuelve nil salvo que el registro realmente contenga los puntos que declara.
// Los puntos empiezan tras el array de conteos: a 32 + nPolys * 4 bytes,
// 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; // conteo 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 que los puntos caben corre primero, así que el array de conteos se sabe dentro del registro antes de que el loop lo recorra. 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 de los Poly* de EMF para la conversión de EMF a PDF
EMR_POLYBEZIER: el punto 0 es el punto de arranque; agrupe desde el punto 1 de a tres; corregido para el registro de 32 bits en v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: figuras abiertas, stroke conS, nuncah,fniB, porque el fill de PDF cierra los subpaths abiertosEMR_POLYLINETO: arrancar en la posición actual, quedar abierta, actualizar la posición actual, jamás tocar el estado de la plumaEMR_POLYPOLYLINEde 32 bits: los puntos empiezan en el byte32 + nPolys * 4, no enaptl[0]EMR_POLYDRAW: enmascarePT_CLOSEFIGUREantes del dispatch, cierre después del segmento completo, y arranque en la posición actual cuando el primer punto no seaPT_MOVETO- El estado por defecto del device context es
BLACK_PENmásWHITE_BRUSH; v3.539.43 y posteriores lo respetan - Dentro de
BeginPath/EndPath, cada polilínea abre su propio subpath y nada se pinta hasta que el bracket se usa - Valide cada
cptl/cptscontranSizeen aritmética de 64 bits antes de copiar puntos - Los EMF de prueba hechos a mano necesitan el header completo de 108 bytes, o
TMetafile.LoadFromStreamlos lee como WMF
Si sus reportes pasan por otro componente, la semántica de los registros es la misma; la importación vectorial de EMF y WMF de HotPDF cubre cómo ese componente convierte pinceles de gradiente y hatch en patterns de PDF, y gráficos vectoriales, shaders y gradientes en PDFlibPas cubre cómo dibujar las mismas formas directo con la API de la librería en vez de vía un metafile
PDFlibPas v3.539.43 o posterior incluye todas las reglas de arriba. Los detalles y las descargas de prueba están en la página del producto de la librería PDFlibPas Delphi PDF