Artículo técnico

Importación vectorial EMF en Free Pascal con PDFlibPas

PDFlibPas convierte los metarchivos mejorados en contenido real de página PDF registro por registro, en lugar de rasterizarlos, que es justo lo que mantiene nítido un gráfico o un dibujo CAD importado con cualquier zoom. Ese conversor tiene unas 6500 líneas y se escribió contra la VCL, así que cuando la biblioteca ganó un destino Free Pascal se clasificó como no portable y se sustituyó por un stub. Esa clasificación estaba mal, y la forma en que estaba mal deja una lección útil sobre cómo auditar una dependencia antes de decidir reescribir a su alrededor

La superficie real de la VCL en esas 6500 líneas resultó pequeña: una clase de mapa de bits usada por su formato de píxel, su guardado en flujo, su handle, su canvas y sus scanlines; una clase de metarchivo usada por su ancho, su alto y su handle; y el tipo de color con dos constantes. Cada uno de esos elementos ya lo proporcionaba la unidad de gráficos propia de la biblioteca, que existe precisamente para que la compilación sin VCL tenga equivalentes. El conversor no estaba bloqueado por la VCL en absoluto. Estaba bloqueado por la unidad Windows de Free Pascal

Dividir según el eje del que realmente depende el código

Así que el cambio no fue una reimplementación. Fue un condicional: de "compilar el stub cuando la compilación se hace sin la VCL" a "compilar el stub cuando no se compila para Windows". Ese es el eje correcto, y enunciar el porqué hace evidente la diferencia. Un metarchivo mejorado es un contenedor de Windows. El conversor es un analizador de registros GDI de Windows de principio a fin. Que la aplicación anfitriona use la VCL, otro conjunto de widgets o ninguno no tiene nada que ver con que esos registros se puedan interpretar; que el destino sea Windows lo tiene todo que ver

Las consecuencias de elegir el eje correcto llegan gratis. Las compilaciones de C++Builder, que en esta biblioteca eliminan el símbolo de plataforma Windows, conservan el stub que lanza una excepción y se comportan exactamente como antes. macOS conserva el stub, correctamente, porque allí no hay registros GDI que interpretar. Las compilaciones Delphi VCL quedan intactas. Y una compilación Windows con un conjunto de widgets ajeno a la VCL gana la importación vectorial de EMF como efecto secundario, algo que nadie tuvo que implementar. Un condicional alineado con la dependencia real convierte el trabajo de plataforma en un cambio de una línea; uno alineado con la dependencia equivocada lo convierte en una reescritura que nunca entra en el calendario

El condicional de importación EMF cambia de eje: de pertenecer a la VCL a la plataforma Windows, conservando stubs en otros casos y dando importación vectorial a las compilaciones Windows sin VCL
Cambiar el eje de la condición del stub a la plataforma Windows conserva el comportamiento de cada compilación existente y da a los destinos Windows sin VCL la importación vectorial EMF gratis

La brecha de Free Pascal eran declaraciones, no lógica

Lo que faltaba de verdad eran las declaraciones Win32 que la unidad Windows de Delphi proporciona y la de Free Pascal no. Reunirlas en una sola unidad de compatibilidad, en lugar de dispersar condicionales por el conversor, mantuvo legible el analizador. La lista es ilustrativa porque muestra qué desigual es la cobertura de cabeceras entre los dos RTL: 113 constantes de tipos de registro de metarchivo, dos banderas de salida de texto extendida, tres constantes de modo de relleno de degradado, un tipo de puntero a tabla de handles, alias para los registros de vértices y primitivas del degradado, y tres tipos de registro que Free Pascal no declara en absoluto, que cubren la mezcla alfa, el blitting transparente y el modo de gestión de color

Nada de eso tiene interés por separado. Todo tiene que estar correcto antes de que el analizador compile, y una unidad de compatibilidad es el hogar natural porque se puede comparar contra la documentación de cabeceras como bloque

Declaraciones Win32 ausentes de la unidad Windows de Free Pascal, reunidas en una sola unidad de compatibilidad para el conversor vectorial de EMF a PDF
Constantes de registros, banderas, alias y tres tipos de registro ausentes viven en una sola unidad de compatibilidad que se puede comparar contra la documentación de cabeceras

La que dibuja en silencio la imagen equivocada

Dos de esas declaraciones no solo están ausentes: están presentes y son incorrectas para este propósito, y esta es la parte que vale la pena recordar aunque nunca toquen un metarchivo

Free Pascal declara el registro de creación de brochas con la estructura de brocha de ejecución incrustada, y el registro de pluma extendida con la estructura de pluma de ejecución incrustada. Ambas estructuras de ejecución declaran su miembro hatch como un entero del tamaño de un puntero, porque en una llamada GDI en vivo ese miembro puede llevar un handle. Un metarchivo, sin embargo, siempre almacena la forma de 32 bits, porque la disposición del registro es parte del formato de archivo serializado y no cambia con el ancho de bits del proceso

En compilaciones de 32 bits ambas coinciden y no pasa nada. En Win64 el miembro del tamaño de un puntero ocupa ocho bytes donde el archivo tiene cuatro, así que cada campo posterior al miembro hatch se lee desde el desplazamiento equivocado. No hay excepción, ni error de análisis, ni advertencia. El metarchivo simplemente se renderiza mal: colores tomados de los bytes equivocados, anchos de pluma tomados de los bytes equivocados y una imagen que parece un defecto de renderizado y no un defecto de disposición de estructura. Delphi incluye variantes explícitamente de 32 bits de ambas estructuras justo por esta razón, y la unidad de compatibilidad las redeclara de la misma manera

Disposición en bytes del registro de brocha EMF que muestra un campo hatch del tamaño de un puntero desplazando los campos posteriores cuatro bytes en Win64 frente a la disposición fija de 32 bits
El registro serializado siempre almacena un hatch de 4 bytes, así que la estructura de ejecución con tamaño de puntero lee mal en silencio todos los campos posteriores en Win64
// Incorrecto en Win64: Hatch es del tamaño de un puntero, el archivo
// almacena 32 bits y cada campo posterior se desplaza cuatro bytes
// sin ningún error
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 bytes en un proceso de 64 bits
  end;

// Correcto: la disposición serializada, ancho fijo sin importar el
// ancho de bits
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // siempre 4 bytes, tal como se guarda en el metarchivo
  end;

La regla general: toda estructura que aparece tanto como argumento de API en ejecución como disposición de campos serializada necesita dos declaraciones, y la serializada debe usar tipos de ancho fijo de principio a fin. Los miembros del tamaño de un puntero en un formato de archivo son siempre un error esperando una compilación de 64 bits

Las diferencias de firma van en un wrapper, no en cada punto de llamada

Las diferencias restantes eran desajustes de firma corrientes, y la forma de absorberlos es un wrapper de reenvío en vez de un condicional en cada punto de llamada. La función de combinación de transformaciones toma punteros bajo Free Pascal donde Delphi toma parámetros por referencia, así que el wrapper recibe referencias y pasa direcciones. También copia antes ambos argumentos de origen en variables locales, porque el conversor tiene puntos de llamada donde la matriz destino es a la vez una de las fuentes, y pasar la misma dirección dos veces a una función que escribe mientras lee produce una transformación sutilmente equivocada de un modo que solo aparece con contenido rotado

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Copiar primero: quienes llaman pasan legítimamente Dest como A o B
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

Los tipos de rectángulo y punto son el otro caso. Free Pascal trata los registros de rectángulo y punto del metarchivo como tipos distintos de los de gráficos generales, así que ocho sitios de asignación necesitaron un cast explícito entre registros de disposición idéntica. Ambos compiladores aceptan la forma del cast, así que esos sitios no llevan condicional alguno, algo que vale un poco de fealdad

Qué cambia esto en un despliegue con Free Pascal

La importación vectorial de EMF funciona en Windows bajo Free Pascal y produce el mismo contenido de página que la compilación Delphi: trazados como trazados, degradados como contenido de patrón y texto como texto. Fuera de Windows la vía rasterizada sigue siendo la respuesta, y esa es una limitación del formato y no del port. El estado de coordenadas y de recorte que alimenta el conversor se describe en el artículo sobre el rastreador de CTM y recorte del content stream, y las primitivas vectoriales que emite se tratan en gráficos vectoriales, shaders y degradados

Si están auditando su propia base de código buscando la misma oportunidad, el ejercicio útil es el que dio origen a esto: enumerar los miembros que realmente usan del marco del que creen depender. La respuesta suele ser mucho más corta de lo que sugiere la lista de importaciones, y la restricción real suele estar en otra parte por completo. Las vías de importación basadas en contextos de dispositivo en general se describen en el artículo de vista previa de impresión y contexto de dispositivo, y la cobertura de plataformas y toolchains está en la página del producto losLab PDF Developer Library