Artigo Técnico

Importar Vetores EMF e WMF para PDFs Delphi com o HotPDF

O HotPDF, o componente PDF nativo para Delphi e C++Builder, importa metaficheiros EMF e WMF do Windows interpretando diretamente cada registo GDI em operadores PDF, em vez de achatar o ficheiro numa imagem de bitmap: os preenchimentos em gradiente tornam-se padrões de shading axial PDF, os pincéis de tramado (hatch) tornam-se padrões de mosaico (tiling patterns) PDF, e um portão centralizado de estado de percurso (path-state) impede que registos malformados corrompam a saída. Qualquer gráfico que um TChart, uma superfície GDI+, ou um simples TCanvas consiga exportar como metaficheiro melhorado é candidato a este caminho, e a diferença nota-se assim que alguém amplia a página ou a envia para uma impressora de alta resolução

A alternativa a que a maioria dos programadores Delphi recorre por predefinição é rasterizar o metaficheiro para bitmap antes de o colocar na página, e o custo só aparece mais tarde: um gráfico de barras que era nítido no ecrã torna-se visivelmente pixelizado assim que o PDF é impresso a 600 DPI ou projetado num ecrã de sala de reuniões, e uma região de CAD preenchida com tramado colapsa num único retângulo cinzento uniforme se o estilo de preenchimento não for transportado. Ler o metaficheiro como um programa, e não como uma imagem, é o que evita ambos os problemas, e é o caminho mais difícil de implementar corretamente, razão pela qual vale a pena conhecer as armadilhas abaixo antes de um relatório ser publicado

Porquê interpretar um metaficheiro em vez de o achatar numa imagem de bitmap?

O HotPDF mantém a importação de EMF e WMF no caminho vetorial porque um metaficheiro do Windows é uma sequência gravada de chamadas de desenho GDI, não uma imagem, e reproduzir essas chamadas como operadores PDF de percurso, texto e shading é o que permite que o resultado escale como o resto da página. THPDFPage.ShowMetafile e o seu equivalente ShowMetafileEx são os pontos de entrada que uma aplicação chama, e ambos entregam o metaficheiro a THPDFWmf, a classe que percorre cada registo GDI e o traduz. A distinção não é absoluta, e o HotPDF não finge o contrário: um registo de metaficheiro que é genuinamente dados de raster, por exemplo um blit de bitmap StretchDIBits, é incorporado como um verdadeiro Image XObject PDF através de AddImage e ShowImage, o mesmo par de chamadas por que passa qualquer outra imagem na página, em vez de ser forçado em operadores de percurso que não conseguem representar uma fotografia. Linhas, preenchimentos e texto mantêm-se vetoriais; pixels que já eram pixels na origem continuam a ser pixels na saída. A chamada mais simples não precisa de mais nada além do metaficheiro carregado:

var
  Pdf: THotPDF;
  Chart: TMetafile;
begin
  Pdf := THotPDF.Create(nil);
  Chart := TMetafile.Create;
  try
    Chart.LoadFromFile('quarterly-revenue.emf');  // exported from TChart or GDI+
    Pdf.FileName := 'quarterly-report.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafile(Chart);
    Pdf.EndDoc;
  finally
    Chart.Free;
    Pdf.Free;
  end;
end;

Como converte o intérprete as coordenadas GDI no espaço de página PDF?

O HotPDF resolve isso com uma única passagem sobre o próprio fluxo de registos do metaficheiro, em vez de uma segunda implementação do GDI. THPDFWmf.Analyse lê o cabeçalho do metaficheiro através da chamada Win32 GetEnhMetaFileHeader, repõe o seu estado interno de desenho, e chama EnumEnhMetafile, a mesma API de enumeração que um visualizador de metaficheiros usaria, pelo que cada registo EMR_* chega a THPDFWmf.ExecuteRecord pela ordem em que foi originalmente gravado. O GDI expressa coordenadas de cima para baixo em unidades de dispositivo ou lógicas escolhidas pelo próprio modo de mapeamento do metaficheiro; uma página PDF é de baixo para cima em pontos de espaço do utilizador, o sistema de coordenadas abordado em o modelo de desenho em canvas do HotPDF para percursos e preenchimentos. Cada manipulador de registo resolve essa discrepância através de ScaleX e ScaleY, que chamam ProjectX e ProjectY para reproduzir a própria fórmula janela-para-viewport do GDI para os modos de mapeamento anisotrópico e isotrópico, pelo que uma forma gravada com cinco unidades lógicas de largura chega à largura correta em pontos PDF, independentemente das extensões de janela e viewport definidas pela aplicação de origem

Como se torna um preenchimento em gradiente GDI num padrão de shading PDF?

Um registo EMR_GRADIENTFILL torna-se um verdadeiro padrão de shading axial PDF Tipo 2 (ISO 32000-1 §8.7.4.5) sempre que o GDI o gravou num dos dois modos de retângulo. THPDFWmf.VEMRGradientFill lê o próprio layout do registo diretamente do buffer de bytes em bruto, seguindo a estrutura MS-EMF §2.3.1.6: um array de vértices de cantos RGBA de 16 bits, seguido de uma lista de retângulos que cada um referencia dois desses vértices. Para GRADIENT_FILL_RECT_H, as cores variam da esquerda para a direita ao longo da linha média horizontal do retângulo; para GRADIENT_FILL_RECT_V, variam de cima para baixo ao longo da linha média vertical. Em ambos os casos, as duas cores de canto e as coordenadas do retângulo projetado passam diretamente para THotPDF.RegisterAxialGradient, que devolve um nome de padrão, e a página desenha o retângulo e preenche-o através desse padrão (SetFillPattern) em vez de uma chamada plana SetRGBFillColor, pelo que um cabeçalho em faixas ao estilo de folha de cálculo ou a área de traçado em gradiente de um gráfico mantém o seu esbatimento em vez de colapsar numa única cor média

O modo de triângulos Gouraud é a lacuna assumida com honestidade. Quando o campo ulMode do registo reporta GRADIENT_FILL_TRIANGLE, VEMRGradientFill reconhece-o, regista em log que o modo de triângulos ainda não está implementado, e ignora o retângulo em vez de arriscar uma aproximação a duas cores. A interpolação por vértice e por pixel ao longo de uma malha triangular arbitrária não se reduz a um shading axial ou radial de dois pontos de cor, e representá-la corretamente significaria emitir um shading de malha PDF Tipo 4 ou Tipo 5, a mesma família de shading que o motor de renderização de páginas do HotPDF também deixa por pintar ao ler um PDF de volta. Dois caminhos de código sem relação entre si acabam no mesmo limite: os shadings de malha são a lacuna tanto do lado da escrita como do lado da leitura, e um diagrama de origem que use triângulos Gouraud para um brilho radial suave recua para o que quer que fosse o último pincel sólido, e não uma aproximação renderizada

Pincéis de tramado tornam-se padrões de mosaico, não cinzento achatado

Um pincel de tramado (hatch) GDI mantém a sua textura no PDF porque THPDFWmf.SetBrushColor verifica CurrentBrush.lbStyle em busca de BS_HATCHED antes de recuar para um preenchimento sólido, encaminhando esse caso para SetHatchBrushPattern. Esse método escreve um fluxo de conteúdo PDF de 8 por 8 unidades composto por operadores de linha traçada, m, l, e S, escolhidos consoante o estilo de tramado GDI: um único traço horizontal ou vertical para HS_HORIZONTAL e HS_VERTICAL, três diagonais paralelas para HS_FDIAGONAL e HS_BDIAGONAL, e as combinações horizontal-mais-vertical ou ambas-as-diagonais para HS_CROSS e HS_DIAGCROSS. THotPDF.RegisterTilingPattern regista esse fluxo de conteúdo como um padrão de mosaico colorido (PaintType 1, ISO 32000-1 §8.7.3.1) com XStep e YStep de 8 unidades, e a página preenche através de SetFillPattern da mesma forma que um shading axial. Uma planta de CAD ou um desenho de engenharia que se apoie em preenchimentos de tramado para distinguir materiais mantém essa linguagem visual no PDF em vez de perder todas as regiões para um cinzento idêntico

Nem todos os pincéis merecem esse tratamento, e vale a pena conhecer a lacuna antes de uma importação de CAD ser publicada. EMR_CREATEDIBPATTERNBRUSHPT, o registo de um pincel de padrão de imagem bitmap personalizado, em vez de um dos seis estilos de tramado predefinidos do GDI, apenas regista o seu handle para que os registos SELECTOBJECT e DELETEOBJECT posteriores permaneçam consistentes; o HotPDF ainda não expõe um pipeline de recurso de Padrão (Pattern) PDF para imagens de mosaico arbitrárias, pelo que selecionar esse pincel recai num preenchimento de cor sólida em vez da textura de origem. Se um preenchimento surgir achatado onde o original usava claramente uma textura de imagem repetida, o pincel de origem é quase certamente um padrão DIB personalizado em vez de um tramado padrão, e é esse o único caso que vale a pena verificar manualmente primeiro. Configurar uma importação para um desenho desse tipo continua a passar pelo mesmo objeto de opções:

var
  Pdf: THotPDF;
  Drawing: TMetafile;
  Options: THPDFEmfOptions;
begin
  Pdf := THotPDF.Create(nil);
  Drawing := TMetafile.Create;
  Options := THPDFEmfOptions.Create;
  try
    Drawing.LoadFromFile('floor-plan.emf');
    Options.Assign(Pdf.EmfOptions);   // start from the document-wide defaults
    Options.Redraw := False;          // interpret the original EMF bytes, no GDI re-record pass
    Options.ShowNullBrush := True;    // keep explicitly unfilled CAD regions visible
    Options.UseFrame := True;         // clip output to the frame the EMF header declares
    Pdf.FileName := 'floor-plan.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
    Pdf.EndDoc;
  finally
    Options.Free;
    Drawing.Free;
    Pdf.Free;
  end;
end;

O que impede um metaficheiro malformado de corromper a página?

A resposta do HotPDF é um único portão no topo de ExecuteRecord, em vez de uma verificação defensiva repetida em cada um dos cerca de oitenta manipuladores de registo. Um parêntesis de percurso GDI, aberto por EMR_BEGINPATH e fechado por EMR_ENDPATH ou EMR_ABORTPATH, é rastreado por uma propriedade privada PathContinue, suportada pelo campo FPathContinue. Enquanto esse parêntesis estiver aberto, ExecuteRecord só deixa passar registos de construção de percurso, as variantes de mover, linha, polilinha, polígono, polybezier e polydraw, mais CLOSEFIGURE e um pequeno conjunto de registos de transformação e de estado do contexto de dispositivo, como SETWORLDTRANSFORM, SAVEDC, e RESTOREDC. Qualquer outro tipo de registo que chegue a ExecuteRecord enquanto o parêntesis está aberto, um EXTTEXTOUT perdido ou um blit de bitmap por exemplo, é descartado de forma centralizada com um único Exit no instante em que chega

Esse portão existe porque um parêntesis de percurso num metaficheiro escrito à mão, gerado por ferramenta, ou simplesmente corrompido, não tem garantia de conter apenas aquilo que um ficheiro bem formado colocaria entre os seus registos de abertura e fecho. Um registo de saída de texto a surgir entre EMR_BEGINPATH e EMR_ENDPATH, sem um portão, ou poluiria a geometria de percurso em construção, ou emitiria um operador PDF de exibição de texto no meio de uma sequência que deveria ser construção de percurso pura, e ambos os modos de falha são do tipo que só surge numa entrada malformada vinda de uma ferramenta de terceiros, não em algo que uma suite de testes normal costume cobrir. Centralizar a verificação em ExecuteRecord significa que os manipuladores individuais VEMR* não precisam cada um de se defender contra ser chamado no momento errado; o portão decide isso uma vez, antes do despacho, em vez de oitenta vezes depois dele

Colocar um gráfico vetorial junto de texto e imagens numa página

Uma página de relatório raramente contém apenas um gráfico, e ShowMetafile combina-se com os outros operadores de página do HotPDF exatamente como qualquer outra chamada de desenho. Um título desenhado com TextOut, um gráfico de barras preenchido com tramado importado como EMF, e um logótipo colocado com ShowImage podem todos surgir na mesma página no mesmo fluxo de conteúdo, cada um mantendo a sua fidelidade nativa, o padrão de composição abordado em o guia do HotPDF para dispor texto, fontes e imagens num relatório:

Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart);   // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);

O intérprete de EMF e WMF, os padrões de shading axial que regista para preenchimentos em gradiente, e o mapeamento de padrões de mosaico para pincéis de tramado aqui descritos são todos incluídos no componente HotPDF standard para Delphi e C++Builder, uma biblioteca VCL nativa sem qualquer dependência de DLL externa para nada disto