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)
| Registo | Começa em | Fecha? | Posição atual |
|---|---|---|---|
EMR_POLYBEZIER | Ponto 0 | Não | Não usada, não atualizada |
EMR_POLYLINE | Ponto 0 | Não (só pen) | Não usada, não atualizada |
EMR_POLYLINETO | Posição atual | Não (só pen) | Usada e atualizada |
EMR_POLYPOLYLINE | Primeiro ponto de cada polyline | Não (só pen) | Não usada, não atualizada |
EMR_POLYDRAW | Primeiro PT_MOVETO, ou posição atual | Só onde PT_CLOSEFIGURE está definido | Usada 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
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_MOVETOnoEMR_POLYDRAW16de 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_MOVETOcomeça na posição atual, como diz a definição do registo, em vez de escrever um operadorloucsem nenhummprecedente
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
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_PENeWHITE_BRUSHselecionados, por isso um metafile que desenhe sem nenhumEMR_SELECTOBJECTcontinua a desenhar contornos pretos; o conversor começava sem pen e sem fill e escrevian(fim de path, nada pintado) para esses registos - Dentro de um parêntese
BeginPath/EndPath, umaPolylinenã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.41EMR_POLYLINE/EMR_POLYPOLYLINE: figuras abertas, stroke comS, nuncah,fouB, porque o fill do PDF fecha subpaths abertosEMR_POLYLINETO: começar na posição atual, manter aberto, atualizar a posição atual, nunca tocar no estado da penEMR_POLYPOLYLINEde 32 bits: os pontos começam no byte32 + nPolys * 4, não emaptl[0]EMR_POLYDRAW: mascarar oPT_CLOSEFIGUREantes 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_PENmaisWHITE_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/cptscontra onSizeem 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.LoadFromStreamlê-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