Artigo Técnico

Importação EMF no PDFlibPas: regras de PolyDraw e Bézier

O PDFlibPas, a biblioteca PDF da losLab para Delphi, converte os registos EMF Poly* em paths PDF seguindo a definição de cada registo no [MS-EMF]: um EMR_POLYBEZIER de 32 bits começa no ponto 0, as polylines permanecem abertas e só recebem stroke, o PT_CLOSEFIGURE no EMR_POLYDRAW é uma flag, e cada contagem de pontos é validada contra o tamanho do registo. Essas regras chegaram ao longo das versões v3.539.39, v3.539.41 e v3.539.43. Antes delas, um gráfico de relatório podia sair do ImportEMFFromFile com uma cunha preenchida onde devia estar uma linha de tendência, uma curva Bézier vergada para o ponto de controlo errado, ou um contorno fechado a perder o último lado. Nenhum destes casos levantava um erro, e as regras aplicam-se a qualquer conversor Delphi de EMF para PDF ou parser de registos GDI

Porque é que os registos EMF Poly* correm mal na conversão para PDF?

Os registos EMF Poly* correm mal porque cada um transporta parte do seu significado fora dos seus pontos: se a figura está aberta, se começa na posição atual, qual a pen e a brush a aplicar, e onde é que os pontos começam dentro do registo. Um enhanced metafile é uma gravação de chamadas GDI contra um device context, por isso um conversor tem de reproduzir o estado desse device context além das coordenadas. O PDF não tem device context. Tem um path, um ponto atual dentro desse path, e um operador de pintura que decide entre stroke (S), fill (f) e ambos (B). Cada desajuste entre os dois modelos torna-se uma diferença de renderização silenciosa

A família Poly* existe também em duas larguras. Cada registo de 32 bits como o EMR_POLYLINE tem um gémeo de 16 bits como o EMR_POLYLINE16 que guarda os pontos como pares de SmallInt. O GDI normalmente grava a forma compacta quando todas as coordenadas cabem, por isso os handlers de 32 bits de um conversor podem ficar anos errados enquanto os desenhos de teste do dia a dia nunca chegam a eles. A auditoria mais rápida é passar os mesmos pontos pelos dois registos e comparar os paths resultantes. Os registos cobertos aqui estão todos no grupo de registos de desenho do [MS-EMF] (2.3.5 Drawing Record Types)

RegistoComeça emFecha?Posição atual
EMR_POLYBEZIERPonto 0NãoNão usada, não atualizada
EMR_POLYLINEPonto 0Não (só pen)Não usada, não atualizada
EMR_POLYLINETOPosição atualNão (só pen)Usada e atualizada
EMR_POLYPOLYLINEPrimeiro ponto de cada polylineNão (só pen)Não usada, não atualizada
EMR_POLYDRAWPrimeiro PT_MOVETO, ou posição atualSó onde PT_CLOSEFIGURE está definidoUsada e atualizada

Onde é que a curva de um EMR_POLYBEZIER começa afinal?

Uma curva de EMR_POLYBEZIER começa no ponto 0, e só os pontos a partir do índice 1 são agrupados em trios como ponto de controlo, ponto de controlo, ponto final. Um registo com 7 pontos desenha por isso dois segmentos cúbicos: o 0 é o início, 1 a 3 formam o primeiro segmento, 4 a 6 formam o segundo. O handler de 16 bits do PDFlibPas já fazia isto. O handler de 32 bits começava a agrupar no ponto 0, por isso o ponto inicial era consumido como primeiro ponto de controlo e cada segmento seguinte deslocava-se um ponto. A curva continuava a renderizar, só que a errada. Desde a v3.539.41 as duas larguras abrem o path com m no ponto 0 e emitem um c por cada trio completo a seguir

Diagrama PDFlibPas de um registo EMR_POLYBEZIER com sete pontos em que o ponto zero abre o path com m e os pontos um a três e quatro a seis formam cada um um segmento cúbico c, contrastando o handler de 32 bits corrigido desde a v3.539.41 com o agrupamento antigo que consumia o ponto inicial como ponto de controlo
O ponto 0 é o ponto inicial e só os trios completos a seguir se tornam segmentos cúbicos, por isso um PolyBezier de sete pontos renderiza como m mais dois operadores c

Para o seu próprio parser: uma contagem que não seja 1 mais um múltiplo de 3 está malformada, e os pontos excedentários devem ser ignorados em vez de cosidos numa curva

PolyDraw: PT_CLOSEFIGURE é uma flag, não um tipo de ponto

No EMR_POLYDRAW, o PT_CLOSEFIGURE (valor 1) é um bit que se combina com PT_LINETO (2) ou PT_BEZIERTO (4), por isso um byte de tipo válido pode ser 3 ou 5. O tipo do ponto é o byte com esse bit mascarado, e a flag significa fechar a figura depois do segmento que termina neste ponto. O handler antigo do PDFlibPas comparava o byte com valores únicos numa instrução case, por isso os pontos de tipo 3 e 5 não correspondiam a nada e eram saltados por completo. Um retângulo desenhado com PolyDraw perdia o lado de fecho, e um trio Bézier cujo último ponto levava a flag perdia esse ponto, o que descarrilava todos os trios seguintes

Desde a v3.539.39 o tipo é lido como Types[i] and not PT_CLOSEFIGURE, e o fecho só é emitido depois de um segmento completo: depois da linha num PT_LINETO com flag de fecho, e depois do terceiro ponto de um grupo Bézier. Um ficheiro malformado que ponha a flag no primeiro ou segundo ponto de um trio não fecha a figura cedo demais. Duas correções relacionadas chegaram na mesma versão:

  • Cada PT_MOVETO no EMR_POLYDRAW16 de 16 bits reiniciava o path inteiro, por isso um registo com três figuras mantinha só a última; agora o primeiro move inicia o path e os moves seguintes abrem subpaths
  • Um registo PolyDraw que não comece por PT_MOVETO começa na posição atual, como diz a definição do registo, em vez de escrever um operador l ou c sem nenhum m precedente
Anatomia PDFlibPas de um byte de tipo do EMR_POLYDRAW em que o PT_CLOSEFIGURE é o bit zero da flag em OR com PT_LINETO ou PT_BEZIERTO, por isso os bytes de tipo válidos 3 e 5 têm de ser mascarados com and not PT_CLOSEFIGURE antes do despacho; o case antigo saltava ambos os bytes e as figuras fechadas perdiam o último lado
Mascare a flag de fecho antes do despacho e emita o fecho só depois de uma linha ou de um trio Bézier completos, ou o PolyDraw deixa cair pontos em silêncio

Porque é que uma polyline EMF nunca deve ser preenchida em PDF?

Uma polyline EMF nunca deve ser preenchida porque o EMR_POLYLINE e o EMR_POLYPOLYLINE são figuras abertas desenhadas só com a pen, e preencher um path aberto em PDF fecha-o implicitamente. A ISO 32000-1 §8.5.3 declara que os operadores de fill fecham qualquer subpath aberto antes de o pintar. Um conversor que emita B ou f para uma polyline de três pontos pinta por isso um triângulo preenchido na cor atual da brush: a cunha preenchida por baixo da linha de tendência de um gráfico. Antes da v3.539.41, o PDFlibPas preenchia as duas larguras de polyline com a brush, e o registo de 32 bits era ainda fechado explicitamente. Hoje as duas larguras acabam só em stroke, e a distinção do GDI é preservada: o Polygon fecha e preenche, a Polyline nunca o faz

Comparação PDFlibPas de uma polyline em V aberta exportada do EMR_POLYLINE: um conversor correto termina o path com o operador de stroke S e ignora a brush selecionada, enquanto emitir f ou B fecha o subpath aberto implicitamente segundo a ISO 32000-1 8.5.3 e pinta o bug da cunha preenchida no gráfico
Um operador de fill fecha qualquer subpath aberto antes de pintar, por isso as polylines têm de acabar em S sem nenhum h, f ou B no subpath

PolylineTo começa na posição atual

O EMR_POLYLINETO desenha da posição atual por todos os pontos do registo, permanece aberto, e deixa a posição atual no último ponto. O handler antigo continha ainda um caso especial que desligava a pen quando os dois primeiros pontos partilhavam uma coordenada y, e nada a voltava a ligar, por isso todos os registos seguintes do ficheiro perdiam o contorno. O estado da pen pertence ao EMR_SELECTOBJECT e ao EMR_CREATEPEN; um handler de registo de desenho não tem nada lá a fazer. Esse caso especial foi removido na v3.539.41, e a forma de um ponto do registo já não lê para além dos seus próprios pontos (corrigido na v3.539.39)

Os pontos do PolyPolyline começam depois do array de contagens

O EMR_POLYPOLYLINE de 32 bits guarda nPolys contagens e depois cptl pontos, e os pontos começam no offset de bytes 32 + nPolys * 4. A armadilha está na RTL: a unidade Windows declara o TEMRPolyPolyline com aPolyCounts e aptl como arrays de um elemento, por isso o aptl[0] só é o primeiro ponto quando nPolys é 1. Código que indexe aptl diretamente lê valores de contagem como coordenadas em cada registo de múltiplas linhas. O handler antigo do PDFlibPas dimensionava também a sua verificação de limites sobre esse layout errado, por isso registos válidos de múltiplas linhas eram rejeitados e os de linha única não desenhavam nada. Desde a v3.539.41 o PDFlibPas localiza o array de pontos a partir do offset calculado, à maneira que o seu handler de PolyPolygon sempre fez, e desenha cada polyline como o seu próprio subpath aberto com um único stroke no fim. Na v3.539.43 o gémeo de 16 bits recebeu o mesmo tratamento; vinha a desenhar segmento a segmento, o que partia as junções de linha e ignorava um NULL_PEN selecionado

A pen e a brush por omissão, e os parênteses de path

Duas regras de estado completam as correções de polyline na v3.539.43:

  • Um device context GDI novo já tem BLACK_PEN e WHITE_BRUSH selecionados, por isso um metafile que desenhe sem nenhum EMR_SELECTOBJECT continua a desenhar contornos pretos; o conversor começava sem pen e sem fill e escrevia n (fim de path, nada pintado) para esses registos
  • Dentro de um parêntese BeginPath / EndPath, uma Polyline não usa nem atualiza a posição atual, por isso tem de abrir um novo subpath no seu primeiro ponto em vez de se ligar à figura anterior, e nada pode ser pintado até o parêntese receber stroke ou fill

Construir um ficheiro EMF de teste com TMetafileCanvas

A maneira mais rápida de verificar um conversor contra estas regras é gravar as três chamadas de risco num único enhanced metafile com TMetafileCanvas. O desenho abaixo grava as curvas com uma brush vazia e depois seleciona de propósito uma brush amarela sólida para a polyline: um conversor correto tem de ignorar essa brush para a polyline, por isso qualquer amarelo no PDF de saída é um bug. O PolyDraw não tem wrapper de TCanvas, por isso é chamado através da API do Windows com o handle do canvas, usando os bytes de tipo 3 e 5 para exercitar a flag de fecho

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

procedure BuildPolyTestEmf(const FileName: string);
const
  // Um quadrado fechado (3 = LINETO + CLOSEFIGURE), depois uma figura
  // Bézier fechada cujo último trio de controlo acaba em 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;    // só contornos para as curvas
      // O ponto 0 é o início; 1..3 e 4..6 são dois 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 aberta com uma brush sólida selecionada: recebe stroke, nunca é
      // fechada num triângulo amarelo
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // termina a gravação
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Como estas coordenadas cabem num SmallInt, o GDI normalmente guarda as variantes de 16 bits. Para chegar aos handlers de 32 bits é preciso um produtor que os escreva, ou registos construídos à mão. Os ficheiros feitos à mão trazem a sua própria armadilha: o TMetafile.LoadFromStream da VCL só trata o stream como um EMF quando o comprimento restante é estritamente maior que o TEnhMetaHeader de 108 bytes. Um EMF mínimo escrito à mão com um cabeçalho curto, ou um vazio com exatamente 108 bytes, é tomado por um WMF e rejeitado com "Metafile is not valid". Escreva sempre o cabeçalho completo de 108 bytes, incluindo os campos de extensão, antes dos seus registos de teste

Importar o EMF para um PDF com o PDFlibPas

O PDFlibPas importa um EMF com ImportEMFFromFile ou ImportEMFFromStream, que devolvem um ID de imagem diferente de zero em caso de sucesso e 0 em caso de falha. GeneralOptions = 0 mantém o path vetorial de que este artigo fala; 1 rasteriza o metafile para um bitmap. FontOptions = 1 acrescenta as fontes do metafile como fontes TrueType não incorporadas. A variante de stream rebobina o stream para a posição 0 antes de carregar, por isso passe um stream que contenha só o 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);              // origem no topo esquerdo para o DrawImage
    PDF.SetMeasurementUnits(0);    // pontos
    // FontOptions 1 = acrescenta fontes como TrueType não incorporadas
    // GeneralOptions 0 = importação vetorial, 1 = bitmap
    ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
    if ImageID = 0 then
      raise Exception.Create('The metafile could not be imported');
    PDF.SelectImage(ImageID);
    // Num EMF, ImageWidth / ImageHeight são o tamanho da frame em pontos
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // A página apenas invoca o 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;

Uma importação EMF vetorial torna-se um form XObject, por isso o GetPageContentToString devolve apenas a sequência de save, transform, Do e restore. Os operadores m, l, c, h e S produzidos a partir dos registos Poly* vivem no stream do form XObject, que está comprimido. Para os auditar, descomprima o ficheiro gravado num inspetor de objetos PDF e leia o stream do form: no ficheiro de teste acima deve ver a polyline acabar em S sem nenhum h antes, um h em cada flag de fecho nas figuras do PolyDraw, e nenhum f ou B em nenhum destes subpaths. O DrawImage também escala um EMF importado de forma uniforme pelo menor de Width e Height, por isso o desenho mantém a proporção mesmo que a caixa que passar não a respeite

Para alvos Free Pascal, veja como o importador vetorial EMF do PDFlibPas compila em Free Pascal; a semântica dos registos é a mesma onde quer que o importador compile

Como deve um parser EMF tratar as contagens de pontos vindas do ficheiro?

Um parser EMF deve tratar cada contagem de pontos como input não confiável e validá-la contra o tamanho do registo antes de copiar um único ponto. O EnumEnhMetaFile só garante que cada nSize de registo se mantém dentro do ficheiro. Não verifica que o cptl está de acordo com o nSize, por isso um handler que copie cptl pontos com Move vai ler os registos seguintes, ou passar do fim do metafile, quando a contagem é forjada ou corrompida. Desde a v3.539.39 o PDFlibPas valida cabeçalho fixo mais contagem vezes bytes por ponto contra o nSize para PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo e Polygon nas duas larguras, com um byte extra por ponto nos bytes de tipo do PolyDraw. Nos registos PolyPoly as contagens por figura também têm de somar não mais do que o total declarado, e figuras de zero pontos são saltadas

A mesma verificação é curta o bastante para copiar para o seu próprio parser. Esta versão valida um EMR_POLYPOLYLINE de 32 bits e devolve um apontador para o seu array de pontos real:

uses
  Winapi.Windows;

// Devolve nil a menos que o registo realmente contenha os pontos que declara.
// Os pontos começam depois do array de contagens: 32 + nPolys * 4 bytes para dentro,
// não em aptl[0], que a RTL declara como array de um 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;                         // contagem forjada ou truncada
  Total := 0;
  Count := @P^.aPolyCounts[0];    // avançar por apontador: [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;                         // as figuras reclamam mais pontos do que existem
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

O teste de cabimento dos pontos corre primeiro, por isso se sabe que o array de contagens está dentro do registo antes de o loop o percorrer. A aritmética é Int64 porque nPolys * 4 e cptl * 8 calculados em 32 bits podem fazer wrap around e passar a comparação

Referência rápida: regras EMF Poly* para conversão de EMF para PDF

  • EMR_POLYBEZIER: o ponto 0 é o ponto inicial; agrupar a partir do ponto 1 em trios; corrigido no registo de 32 bits na v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: figuras abertas, stroke com S, nunca h, f ou B, porque o fill do PDF fecha subpaths abertos
  • EMR_POLYLINETO: começar na posição atual, manter aberto, atualizar a posição atual, nunca tocar no estado da pen
  • EMR_POLYPOLYLINE de 32 bits: os pontos começam no byte 32 + nPolys * 4, não em aptl[0]
  • EMR_POLYDRAW: mascarar o PT_CLOSEFIGURE antes do despacho, fechar depois do segmento completo, começar na posição atual quando o primeiro ponto não é PT_MOVETO
  • O estado por omissão do device context é BLACK_PEN mais WHITE_BRUSH; a v3.539.43 e posteriores respeitam-no
  • Dentro de BeginPath / EndPath, cada polyline abre o seu próprio subpath e nada é pintado até o parêntese ser usado
  • Validar cada cptl / cpts contra o nSize em aritmética de 64 bits antes de copiar pontos
  • EMFs de teste feitos à mão precisam do cabeçalho completo de 108 bytes, ou o TMetafile.LoadFromStream lê-los como WMF

Se os seus relatórios passam por um componente diferente, a mesma semântica de registos aplica-se; o import vetorial de EMF e WMF do HotPDF cobre como esse componente transforma brushes de gradiente e hatch em patterns PDF, e gráficos vetoriais, shaders e gradientes no PDFlibPas cobre desenhar as mesmas formas diretamente com a API da biblioteca em vez de passar por um metafile

O PDFlibPas v3.539.43 ou posterior inclui todas as regras acima. Detalhes e downloads de trial estão na página de produto da biblioteca PDF Delphi PDFlibPas