Artigo Técnico

Importação vetorial EMF no Free Pascal com PDFlibPas

O PDFlibPas converte enhanced metafiles em conteúdo de página PDF real registro por registro, em vez de rasterizá-los, e é isso que mantém um gráfico importado ou um desenho CAD nítido em qualquer zoom. Esse conversor tem cerca de 6500 linhas e foi escrito contra a VCL, então quando a biblioteca ganhou um alvo Free Pascal ele foi classificado como não portável e substituído por um stub. Essa classificação estava errada, e o modo como estava errada é uma lição útil sobre como auditar uma dependência antes de decidir reescrever em torno dela

A superfície VCL real dessas 6500 linhas acabou sendo pequena: uma classe de bitmap usada pelo formato de pixel, salvamento em stream, handle, canvas e scanlines; uma classe de metafile usada pela largura, altura e handle; e o tipo de cor com duas constantes. Cada um deles já era fornecido pela própria unidade de gráficos da biblioteca, que existe justamente para que o build sem VCL tenha equivalentes. O conversor não estava bloqueado pela VCL. Estava bloqueado pela unidade Windows do Free Pascal

Divida pelo eixo em que o código realmente depende

Então a mudança não foi uma reimplementação. Foi um único condicional: de "compile o stub quando construído sem a VCL" para "compile o stub quando não estiver construindo para Windows". Esse é o eixo correto, e enunciar o porquê torna a diferença óbvia. Um enhanced metafile é um contêiner Windows. O conversor é um parser de registros GDI do Windows de ponta a ponta. Se o aplicativo hospedeiro usa a VCL, outro conjunto de widgets ou nenhum conjunto de widgets não tem nada a ver com se esses registros podem ser interpretados; se o alvo é Windows tem tudo a ver com isso

As consequências de escolher o eixo certo saem de graça. Builds de C++Builder, que removem a definição do símbolo da plataforma Windows nesta biblioteca, mantêm o stub que lança exceção e se comportam exatamente como antes. O macOS mantém o stub, corretamente, porque não há registros GDI para interpretar lá. Builds Delphi VCL ficam intactos. E um build Windows com um conjunto de widgets sem VCL ganha importação vetorial de EMF como efeito colateral, que ninguém precisou implementar. Um condicional alinhado com a dependência real transforma o trabalho de plataforma em uma mudança de uma linha; um condicional alinhado com o errado o transforma em uma reescrita que nunca entra na agenda

Condicional de importação EMF reaxado da filiação à VCL para a plataforma Windows, mantendo stubs nos demais ambientes e dando importação vetorial aos builds Windows sem VCL
Reaxar a condição do stub para a plataforma Windows preserva o comportamento de cada build existente e dá aos alvos Windows sem VCL importação vetorial EMF de graça

A lacuna do Free Pascal eram declarações, não lógica

O que faltava de fato eram as declarações Win32 que a unidade Windows do Delphi fornece e a do Free Pascal não. Reuni-las em uma única unidade de compatibilidade, em vez de espalhar condicionais pelo conversor, manteve o parser legível. A lista é instrutiva porque mostra o quanto a cobertura de headers é irregular entre os dois RTLs: 113 constantes de tipos de registro de metafile, dois flags de saída de texto estendida, três constantes de modo de gradient fill, um tipo de ponteiro para tabela de handles, aliases para os registros de vértice de gradiente e primitivas, e três tipos de registro que o Free Pascal não declara de forma alguma, cobrindo alpha blending, blitting transparente e modo de gerenciamento de cor

Nada disso é interessante individualmente. Tudo precisa estar certo antes de o parser compilar, e uma unidade de compatibilidade é o lar natural porque pode ser comparada com a documentação dos headers como uma unidade

Declarações Win32 ausentes na unidade Windows do Free Pascal, reunidas em uma única unidade de compatibilidade para o conversor vetorial de EMF para PDF
Constantes de registros, flags, aliases e três tipos de registro ausentes vivem em uma única unidade de compatibilidade comparável com a documentação dos headers

A que silenciosamente desenha a imagem errada

Duas dessas declarações não estão apenas ausentes, elas estão presentes e erradas para esse propósito, e esta é a parte que vale lembrar mesmo que você nunca toque em um metafile

O Free Pascal declara o registro de criação de brush com a estrutura de brush de execução embutida nele, e o registro de caneta estendida com a estrutura de caneta de execução embutida nele. Ambas essas estruturas de execução declaram seu membro hatch como um inteiro do tamanho de um ponteiro, porque em uma chamada GDI ao vivo esse membro pode carregar um handle. Um metafile, porém, sempre armazena a forma de 32 bits, porque o layout do registro é parte do formato de arquivo serializado e não muda com o bitness do processo

Em builds de 32 bits os dois concordam e nada acontece. No Win64 o membro do tamanho de um ponteiro tem oito bytes onde o arquivo tem quatro, então todo campo depois do membro hatch é lido do offset errado. Não há exceção, erro de parse ou aviso. O metafile simplesmente renderiza errado: cores dos bytes errados, espessuras de caneta dos bytes errados, e uma imagem que parece um bug de renderização em vez de um bug de layout de struct. O Delphi distribui variantes explicitamente de 32 bits de ambas as estruturas exatamente por esse motivo, e a unidade de compatibilidade as redeclara da mesma forma

Layout em bytes do registro de brush EMF mostrando um campo hatch do tamanho de um ponteiro deslocando os campos seguintes em quatro bytes no Win64 ante o layout fixo de 32 bits
O registro serializado sempre armazena um hatch de 4 bytes, então a estrutura de execução do tamanho de um ponteiro lê errado em silêncio todo campo posterior no Win64
// Errado no Win64: Hatch tem tamanho de ponteiro, o arquivo armazena
// 32 bits, e todo campo seguinte desloca quatro bytes sem erro nenhum
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 bytes em um processo de 64 bits
  end;

// Certo: o layout serializado, largura fixa independente do bitness
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // sempre 4 bytes, como armazenado no metafile
  end;

A regra geral: qualquer estrutura que aparece tanto como argumento de API em execução quanto como layout de campos serializados precisa de duas declarações, e a serializada deve usar tipos de largura fixa por completo. Membros do tamanho de um ponteiro em um formato de arquivo são sempre um bug esperando um build de 64 bits

Diferenças de assinatura pertencem a um wrapper, não a cada call site

As diferenças restantes eram incompatibilidades ordinárias de assinatura, e o modo de absorvê-las é um wrapper de encaminhamento em vez de um condicional em cada call site. A função de combinação de transformações recebe ponteiros sob o Free Pascal onde o Delphi recebe parâmetros por referência, então o wrapper recebe referências e passa endereços. Ele também copia primeiro ambos os argumentos de origem para locais, porque o conversor tem call sites em que a matriz de destino é simultaneamente uma das fontes, e passar o mesmo endereço duas vezes para uma função que escreve enquanto lê produz uma transformação sutilmente errada de um modo que só aparece em conteúdo rotacionado

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Copie primeiro: chamadores legitimamente passam Dest como A ou B
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

Os tipos de retângulo e ponto são o outro caso. O Free Pascal trata os registros de retângulo e ponto de metafile como tipos distintos dos de gráficos gerais, então oito pontos de atribuição precisaram de um cast explícito entre registros de layout idêntico. Ambos os compiladores aceitam a forma com cast, então esses pontos não carregam condicional algum, o que vale um pouco de feiúra

O que isso muda para uma implantação com Free Pascal

A importação vetorial de EMF funciona no Windows sob Free Pascal, produzindo o mesmo conteúdo de página que o build Delphi: paths como paths, gradientes como pattern content, texto como texto. Fora do Windows o caminho raster continua sendo a resposta, e isso é uma limitação do formato e não do port. O estado de coordenadas e clipping que o conversor alimenta é descrito em o artigo sobre o rastreador de CTM e clipping do content stream, e as primitivas vetoriais que ele emite são cobertas em gráficos vetoriais, shaders e gradientes

Se você está auditando sua própria base de código para a mesma oportunidade, o exercício útil é o que iniciou isto: liste os membros que você realmente usa do framework do qual você acha que depende. A resposta costuma ser muito mais curta do que a lista de imports sugere, e a restrição real geralmente está em outro lugar por completo. Caminhos de importação baseados em device context em geral são descritos em o artigo sobre print preview e device context, e a cobertura de plataformas e toolchains está listada na página de produto da losLab PDF Developer Library