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)
| Registro | Empieza en | ¿Cerrado? | Posición actual |
|---|---|---|---|
EMR_POLYBEZIER | Punto 0 | No | No se usa, no se actualiza |
EMR_POLYLINE | Punto 0 | No (solo lápiz) | No se usa, no se actualiza |
EMR_POLYLINETO | Posición actual | No (solo lápiz) | Se usa y se actualiza |
EMR_POLYPOLYLINE | Primer punto de cada polyline | No (solo lápiz) | 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 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
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_MOVETOdelEMR_POLYDRAW16de 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_MOVETOarranca en la posición actual, como dice la definición del registro, en lugar de escribir un operadorlocsin ningúnmprevio
¿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
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_PENyWHITE_BRUSHseleccionados, de modo que un metafile que dibuje sin ningúnEMR_SELECTOBJECTsigue dibujando contornos negros; el conversor arrancaba sin lápiz ni relleno y escribían(fin de trazado, nada pintado) para esos registros - Dentro de un corchete
BeginPath/EndPath, unaPolylineni 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.41EMR_POLYLINE/EMR_POLYPOLYLINE: figuras abiertas, stroke conS, nuncah,fniB, porque el fill de PDF cierra los subtrazados abiertosEMR_POLYLINETO: empiece en la posición actual, quede abierta, actualice la posición actual y no toque nunca el estado del lápizEMR_POLYPOLYLINEde 32 bits: los puntos empiezan en el byte32 + nPolys * 4, no enaptl[0]EMR_POLYDRAW: enmascarePT_CLOSEFIGUREantes del dispatch, cierre tras el segmento completado 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 polyline abre su propio subtrazado y nada se pinta hasta que se usa el corchete - Valide cada
cptl/cptscontranSizeen 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.LoadFromStreamlos 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