PDFlibPas, a biblioteca de PDF da losLab para Delphi, converte os records Poly* do EMF em paths de PDF seguindo a definição de cada record no [MS-EMF]: um EMR_POLYBEZIER de 32 bits começa no ponto 0, polylines ficam abertas e recebem só stroke, PT_CLOSEFIGURE em EMR_POLYDRAW é uma flag, e toda contagem de pontos é conferida contra o tamanho do record. Essas regras chegaram ao longo da v3.539.39, v3.539.41 e v3.539.43. Antes delas, um gráfico de relatório podia sair do ImportEMFFromFile com uma fatia preenchida onde deveria haver uma linha de tendência, uma curva Bezier dobrando para o control point errado, ou um contorno fechado faltando o último lado. Nada disso levantava erro, e as regras valem para qualquer conversor de EMF para PDF em Delphi ou parser de records GDI
Por que os records Poly* do EMF dão errado na conversão para PDF?
Os records Poly* do EMF dão errado porque cada um carrega parte do significado dele fora dos pontos: se a figura é aberta, se começa na posição atual, qual caneta e qual pincel se aplicam, e onde no record os pontos começam. Um enhanced metafile é uma gravação de chamadas GDI contra um device context, então o conversor precisa reproduzir o estado desse device context além das coordenadas. O PDF não tem device context. Ele tem um path, um current point dentro desse path, e um operador de pintura que decide entre stroke (S), fill (f) e ambos (B). Todo desencontro entre os dois modelos vira uma diferença de renderização silenciosa
A família Poly* também vem em duas larguras. Cada record 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, então os handlers de 32 bits de um conversor podem ficar errados por anos enquanto desenhos de teste do dia a dia nunca chegam até eles. A auditoria mais rápida é passar os mesmos pontos pelos dois records e comparar os paths resultantes. Os records cobertos aqui estão todos no grupo de records de desenho do [MS-EMF] (2.3.5 Drawing Record Types)
| Record | Começa em | Fechada? | Posição atual |
|---|---|---|---|
EMR_POLYBEZIER | Ponto 0 | Não | Não usada, não atualizada |
EMR_POLYLINE | Ponto 0 | Não (só caneta) | Não usada, não atualizada |
EMR_POLYLINETO | Posição atual | Não (só caneta) | Usada e atualizada |
EMR_POLYPOLYLINE | Primeiro ponto de cada polyline | Não (só caneta) | Não usada, não atualizada |
EMR_POLYDRAW | Primeiro PT_MOVETO, ou posição atual | Só onde PT_CLOSEFIGURE está setado | Usada e atualizada |
Onde uma curva EMR_POLYBEZIER realmente começa?
Uma curva EMR_POLYBEZIER começa no ponto 0, e só os pontos do índice 1 em diante são agrupados em trios como control point, control point, end point. Um record com 7 pontos desenha portanto dois segmentos cúbicos: 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 isso. O handler de 32 bits começava a agrupar no ponto 0, então o start point era consumido como primeiro control point e todo segmento seguinte deslocava em um. A curva ainda renderizava, só que a errada. Desde a v3.539.41 ambas as larguras abrem o path com m no ponto 0 e emitem um c por trio completo depois dele
Para o seu próprio parser: uma contagem que não é 1 mais múltiplo de 3 está malformada, e os pontos finais devem ser ignorados em vez de costurados numa curva
PolyDraw: PT_CLOSEFIGURE é uma flag, não um tipo de ponto
Em EMR_POLYDRAW, PT_CLOSEFIGURE (valor 1) é um bit combinado com PT_LINETO (2) ou PT_BEZIERTO (4), então 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 nesse ponto. O handler antigo do PDFlibPas comparava o byte com valores únicos num case, então pontos de tipo 3 e 5 não casavam com nada e eram pulados por completo. Um retângulo desenhado com PolyDraw perdia o lado que o fechava, e um trio Bezier cujo último ponto carregava a flag perdia esse ponto, o que empurrava todo trio seguinte para fora de sintonia
Desde a v3.539.39 o tipo é lido como Types[i] and not PT_CLOSEFIGURE, e o fechamento é emitido só depois de um segmento completo: depois da linha para um PT_LINETO fechado, e depois do terceiro ponto de um grupo Bezier. Um arquivo malformado que seta a flag no primeiro ou no segundo ponto de um trio não fecha a figura cedo demais. Duas correções relacionadas saíram na mesma release:
- Cada
PT_MOVETOnoEMR_POLYDRAW16de 16 bits reiniciava o path inteiro, então um record com três figuras mantinha só a última; agora o primeiro move inicia o path e os moves seguintes abrem subpaths - Um record PolyDraw que não começa com
PT_MOVETOcomeça na posição atual, como a definição do record manda, em vez de escrever um operadorloucsemmprecedente
Por que uma polyline EMF nunca deve ser preenchida em PDF?
Uma polyline EMF nunca deve ser preenchida porque EMR_POLYLINE e EMR_POLYPOLYLINE são figuras abertas desenhadas só com a caneta, e preencher um path aberto em PDF o fecha implicitamente. A ISO 32000-1 §8.5.3 declara que os operadores de fill fecham qualquer subpath aberto antes de pintá-lo. Um conversor que emite B ou f para uma polyline de três pontos pinta portanto um triângulo preenchido na cor do pincel atual: a fatia preenchida sob uma linha de tendência de gráfico. Antes da v3.539.41, o PDFlibPas preenchia ambas as larguras de polyline com o pincel, e o record de 32 bits ainda era fechado explicitamente. Hoje ambas as larguras terminam só com stroke, e a distinção do GDI é preservada: Polygon fecha e preenche, Polyline nunca faz isso
PolylineTo começa na posição atual
EMR_POLYLINETO desenha da posição atual por todos os pontos do record, fica aberto e deixa a posição atual no último ponto. O handler antigo ainda continha um caso especial que desligava a caneta quando os dois primeiros pontos compartilhavam o y, e nada nunca a ligava de volta, então todo record seguinte do arquivo perdia o contorno. O estado da caneta pertence a EMR_SELECTOBJECT e EMR_CREATEPEN; um handler de record de desenho não tem nada a ver com mudá-lo. Esse caso especial saiu na v3.539.41, e a forma de um ponto só do record não lê mais além dos 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 byte 32 + nPolys * 4. A armadilha está na RTL: a unit Windows declara TEMRPolyPolyline com aPolyCounts e aptl como arrays de um elemento, então aptl[0] é o primeiro ponto só quando nPolys é 1. Código que indexa aptl diretamente lê valores de contagem como coordenadas em todo record de múltiplas linhas. O handler antigo do PDFlibPas também dimensionava a checagem de limites com base nesse layout errado, então records 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, do mesmo jeito que o handler de PolyPolygon dele sempre fez, e desenha cada polyline como o próprio subpath aberto dela com um stroke no final. Na v3.539.43 o gêmeo de 16 bits recebeu o mesmo tratamento; ele desenhava segmento por segmento, o que quebrava as junções de linha e ignorava um NULL_PEN selecionado
A caneta e o pincel default, e os brackets de path
Duas regras de estado completam as correções de polyline na v3.539.43:
- Um device context GDI recém-criado já tem
BLACK_PENeWHITE_BRUSHselecionados, então um metafile que desenha sem nenhumEMR_SELECTOBJECTainda desenha contornos pretos; o conversor começava sem caneta e sem fill e escrevian(fecha o path, não pinta nada) para esses records - Dentro de um bracket
BeginPath/EndPath, umaPolylinenão usa nem atualiza a posição atual, então precisa abrir um subpath novo no primeiro ponto dela em vez de conectar com a figura anterior, e nada pode ser pintado até o bracket receber stroke ou fill
Construindo um arquivo de teste EMF com TMetafileCanvas
O jeito mais rápido de conferir um conversor contra essas regras é gravar as três chamadas arriscadas num único enhanced metafile com TMetafileCanvas. O desenho abaixo grava as curvas com um pincel vazado e depois seleciona um pincel amarelo sólido para a polyline de propósito: um conversor correto precisa ignorar esse pincel para a polyline, então qualquer amarelo no PDF de saída é um bug. PolyDraw não tem wrapper de TCanvas, então é chamado pela API do Windows com o handle do canvas, usando bytes de tipo 3 e 5 para exercitar a flag de fechamento
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Um quadrado fechado (3 = LINETO + CLOSEFIGURE), depois uma
// figura Bezier fechada cujo último trio de controle termina 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 pincel sólido selecionado: 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; // encerra a gravação
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Como essas coordenadas cabem num SmallInt, o GDI normalmente vai gravar as variantes de 16 bits. Para alcançar os handlers de 32 bits você precisa de um produtor que os escreva, ou de records construídos à mão. Arquivos feitos à mão têm armadilha própria: o TMetafile.LoadFromStream da VCL trata o stream como EMF só quando o comprimento restante é estritamente maior que o TEnhMetaHeader de 108 bytes. Um EMF minimalista escrito à mão com header curto, ou um vazio de exatamente 108 bytes, é tomado por WMF e rejeitado com "Metafile is not valid". Escreva sempre o header completo de 108 bytes, incluindo os campos de extensão, antes dos seus records de teste
Importando o EMF para um PDF com o PDFlibPas
O PDFlibPas importa um EMF com ImportEMFFromFile ou ImportEMFFromStream, que retornam um image ID não zero em caso de sucesso e 0 em caso de falha. GeneralOptions = 0 mantém o path vetorial de que este artigo trata; 1 rasteriza o metafile num bitmap. FontOptions = 1 adiciona as fontes do metafile como fontes TrueType não embutidas. A variante de stream rebobina o stream para a posição 0 antes de carregar, então 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 à esquerda para DrawImage
PDF.SetMeasurementUnits(0); // pontos
// FontOptions 1 = adiciona fontes como TrueType não embutidas
// 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);
// Para um EMF, ImageWidth / ImageHeight são o tamanho do frame em pontos
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// A página só 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 vetorial de EMF vira um form XObject, então o GetPageContentToString retorna só a sequência de save, transform, Do e restore. Os operadores m, l, c, h e S produzidos a partir dos records Poly* vivem no stream do form XObject, que é comprimido. Para auditar, descomprima o arquivo salvo num inspetor de objetos PDF e leia o stream do form: para o arquivo de teste acima você deve ver a polyline terminar em S sem nenhum h antes, um h a cada flag de fechamento nas figuras PolyDraw, e nenhum f ou B em nenhum desses subpaths. O DrawImage também escala um EMF importado uniformemente pelo menor entre Width e Height, então o desenho mantém o aspect ratio dele mesmo que a caixa que você passar não bata com ele
Para alvos Free Pascal, veja como o importador vetorial de EMF do PDFlibPas compila sob Free Pascal; a semântica dos records é a mesma onde quer que o importador compile
Como um parser de EMF deve tratar contagens de pontos vindas do arquivo?
Um parser de EMF deve tratar toda contagem de pontos como input não confiável e conferi-la contra o tamanho do record antes de copiar um único ponto. O EnumEnhMetaFile só garante que cada nSize de record fica dentro do arquivo. Ele não confere se o cptl bate com o nSize, então um handler que copia cptl pontos com Move vai ler os records seguintes, ou passar do fim do metafile, quando a contagem for forjada ou estiver corrompida. Desde a v3.539.39 o PDFlibPas confere header fixo mais contagem vezes bytes por ponto contra o nSize para PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo e Polygon em ambas as larguras, com um byte extra por ponto para os bytes de tipo do PolyDraw. Para os records PolyPoly as contagens por figura também precisam somar no máximo o total declarado, e figuras de zero pontos são puladas
A mesma checagem é curta o bastante para copiar para o seu próprio parser. Esta versão valida um EMR_POLYPOLYLINE de 32 bits e retorna um ponteiro para o array real de pontos dele:
uses
Winapi.Windows;
// Retorna nil a menos que o record realmente contenha os pontos que declara.
// Os pontos começam depois do array de contagens: 32 + nPolys * 4 bytes à
// frente, e 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]; // caminha por ponteiro: [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 pedem mais pontos do que existem
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
O teste de pontos que cabem roda primeiro, então o array de contagens já é sabido estar dentro do record antes de o loop caminhar por ele. A aritmética é Int64 porque nPolys * 4 e cptl * 8 calculados em 32 bits podem dar wrap around e passar na comparação
Referência rápida: regras Poly* do EMF para conversão de EMF para PDF
EMR_POLYBEZIER: o ponto 0 é o start point; agrupe a partir do ponto 1 em trios; corrigido no record 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: comece na posição atual, fique aberto, atualize a posição atual, nunca mexa no estado da canetaEMR_POLYPOLYLINEde 32 bits: os pontos começam no byte32 + nPolys * 4, não emaptl[0]EMR_POLYDRAW: mascarePT_CLOSEFIGUREantes do dispatch, feche depois do segmento completo, comece na posição atual quando o primeiro ponto não forPT_MOVETO- O estado default do device context é
BLACK_PENmaisWHITE_BRUSH; v3.539.43 e posteriores o respeitam - Dentro de
BeginPath/EndPath, cada polyline abre o próprio subpath dela e nada é pintado até o bracket ser usado - Valide todo
cptl/cptscontra onSizeem aritmética de 64 bits antes de copiar pontos - EMFs de teste construídos à mão precisam do header completo de 108 bytes, ou o
TMetafile.LoadFromStreamos lê como WMF
Se os seus relatórios passam por outro componente, a mesma semântica de records se aplica; o import vetorial de EMF e WMF do HotPDF cobre como esse componente transforma pincéis de gradiente e hatch em patterns de PDF, e vetores, shaders e gradientes no PDFlibPas cobre desenhar as mesmas formas diretamente pela 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 teste estão na página de produto da biblioteca de PDF PDFlibPas para Delphi