Teknisk artikel

Importera EMF- och WMF-vektorer till Delphi-PDF:er med HotPDF

HotPDF, den native PDF-komponenten för Delphi och C++Builder, importerar Windows EMF- och WMF-metafiler genom att tolka varje GDI-post direkt till PDF-operatorer i stället för att platta ut filen till en bitmapp: gradientfyllningar blir PDF-axialskuggningsmönster, skraffyrborstar blir PDF-kakelmönster, och en centraliserad spärr för väggtillstånd blockerar felaktiga poster från att korrumpera utdata. Varje diagram som en TChart, en GDI+-yta, eller en vanlig TCanvas kan exportera som en förbättrad metafil är en kandidat för den här vägen, och skillnaden syns i samma stund någon zoomar in på sidan eller skickar den till en högupplöst skrivare

Alternativet de flesta Delphi-utvecklare tar till som standard är att rastrera metafilen till en bitmapp innan den läggs på sidan, och kostnaden syns först senare: ett stapeldiagram som var skarpt på skärmen blir synligt kantigt så fort PDF:en skrivs ut i 600 DPI eller projiceras på en styrelserumsskärm, och en skraffyrfylld CAD-region kollapsar till en enda platt grå rektangel om fyllningsstilen inte förs vidare. Att läsa metafilen som ett program i stället för en bild är vad som undviker båda problemen, och det är den svårare vägen att implementera korrekt, vilket är varför fallgroparna nedan är värda att känna till innan en rapport skickas ut

Varför tolka en metafil i stället för att platta ut den till en bitmapp?

HotPDF håller EMF- och WMF-import på vektorvägen eftersom en Windows-metafil är en inspelad sekvens av GDI-ritanrop, inte en bild, och att spela upp dessa anrop igen som PDF-path-, text- och skuggningsoperatorer är det som gör att resultatet skalar som resten av sidan. THPDFPage.ShowMetafile och dess motsvarighet ShowMetafileEx är ingångspunkterna en applikation anropar, och båda skickar metafilen till THPDFWmf, klassen som går igenom varje GDI-post och översätter den. Skillnaden är inte absolut, och HotPDF låtsas inte annat: en metafilspost som verkligen är rasterdata, till exempel en StretchDIBits-bitmappsöverföring, bäddas in som ett riktigt PDF Image XObject via AddImage och ShowImage, samma par av anrop som vilken annan bild på sidan som helst går igenom, i stället för att tvingas in i path-operatorer som inte kan uttrycka ett fotografi. Linjer, fyllningar och text förblir vektor; pixlar som redan var pixlar i källan förblir pixlar i utdata. Det enklaste anropet behöver inget mer än den laddade metafilen:

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;

Hur omvandlar tolken GDI-koordinater till PDF-sidrymd?

HotPDF svarar på det med en enda genomgång av metafilens egen postström i stället för en andra implementation av GDI. THPDFWmf.Analyse läser metafilens header via Win32-anropet GetEnhMetaFileHeader, återställer sitt interna ritningstillstånd, och anropar EnumEnhMetafile, samma uppräkningsAPI som en metafil-visare skulle använda, så att varje EMR_*-post når THPDFWmf.ExecuteRecord i den ordning den ursprungligen spelades in. GDI uttrycker koordinater uppifrån och ner i enheter eller logiska enheter som väljs av metafilens eget mappningsläge; en PDF-sida är nerifrån och upp i user-space-punkter, koordinatsystemet som täcks i HotPDF:s canvas-ritmodell för paths och fyllningar. Varje posthanterare löser den avvikelsen genom ScaleX och ScaleY, som anropar ProjectX och ProjectY för att spela upp GDI:s egen fönster-till-viewport-formel igen för de anisotropa och isotropa mappningslägena, så att en form inspelad fem logiska enheter bred hamnar med rätt bredd i PDF-punkter oavsett vilka fönster- och viewport-utsträckningar källapplikationen satte

Hur blir en GDI-gradientfyllning ett PDF-skuggningsmönster?

En EMR_GRADIENTFILL-post blir ett riktigt PDF Type 2-axialskuggningsmönster (ISO 32000-1 §8.7.4.5) närhelst GDI spelade in det i ett av de två rektangellägena. THPDFWmf.VEMRGradientFill läser postens egen layout direkt från den råa bytebufferten, enligt strukturen i MS-EMF §2.3.1.6: en vertexarray med 16-bitars RGBA-hörn, följt av en lista med rektanglar som var och en refererar till två av dessa vertexar. För GRADIENT_FILL_RECT_H sveper färgerna vänster-till-höger längs rektangelns horisontella mittlinje; för GRADIENT_FILL_RECT_V sveper de uppifrån-och-ner längs den vertikala mittlinjen. Oavsett vilket går de två hörnfärgerna och de projicerade rektangelkoordinaterna rakt in i THotPDF.RegisterAxialGradient, som returnerar ett mönsternamn, och sidan ritar rektangeln och fyller den genom det mönstret (SetFillPattern) i stället för ett platt SetRGBFillColor-anrop, så att ett kalkylbladsliknande bandad rubrik eller ett diagrams gradientytarea behåller sin toning i stället för att kollapsa till en genomsnittsfärg

Gouraud-triangelläge är den ärliga luckan. När postens ulMode-fält rapporterar GRADIENT_FILL_TRIANGLE, känner VEMRGradientFill igen det, loggar att triangelläge ännu inte är implementerat, och hoppar över rektangeln i stället för att gissa på en tvåfärgsapproximation. Interpolering per vertex, per pixel över ett godtyckligt triangelnät reducerar inte till en tvåstegs axial- eller radialskuggning, och att uttrycka det korrekt skulle innebära att generera en PDF Type 4- eller Type 5-nätskuggning, samma skuggningsfamilj som HotPDF:s sidrenderare också lämnar omålad vid inläsning av en PDF. Två orelaterade kodvägar landar på samma gräns: nätskuggningar är luckan på både skrivsidan och läsningssidan, och ett källdiagram som använder Gouraud-trianglar för en mjuk radiell glöd faller tillbaka till vad den senaste solida penseln var, inte en renderad approximation

Skraffyrborstar blir kakelmönster, inte utplattad grå

En GDI-skraffyrborste behåller sin textur i PDF:en eftersom THPDFWmf.SetBrushColor kontrollerar CurrentBrush.lbStyle för BS_HATCHED innan den någonsin faller tillbaka till en solid fyllning, och dirigerar det fallet till SetHatchBrushPattern i stället. Den metoden skriver en 8-gånger-8-enhets PDF-innehållsström av streckade linjeoperatorer, m, l, och S, valda utifrån GDI-skraffyrstilen: ett enda horisontellt eller vertikalt streck för HS_HORIZONTAL och HS_VERTICAL, tre parallella diagonaler för HS_FDIAGONAL och HS_BDIAGONAL, och kombinationerna horisontell-plus-vertikal eller båda-diagonalerna för HS_CROSS och HS_DIAGCROSS. THotPDF.RegisterTilingPattern registrerar den innehållsströmmen som ett färgat kakelmönster (PaintType 1, ISO 32000-1 §8.7.3.1) med ett 8-enhets XStep och YStep, och sidan fyller genom SetFillPattern på samma sätt som en axialskuggning gör. En CAD-planritning eller en teknisk ritning som lutar sig mot skraffyrfyllningar för att skilja på material behåller det visuella språket i PDF:en i stället för att förlora varje region till identisk grå

Inte varje pensel förtjänar den behandlingen, och luckan är värd att känna till innan en CAD-import skickas ut. EMR_CREATEDIBPATTERNBRUSHPT, posten för en anpassad bitmappsbildmönsterpensel snarare än en av GDI:s sex standardskraffyrstilar, registrerar bara sitt handtag så att senare SELECTOBJECT- och DELETEOBJECT-poster förblir konsekventa; HotPDF exponerar ännu inte en PDF Pattern-resurspipeline för godtyckliga kakelbilder, så att välja den penseln faller igenom till en enfärgad reservlösning i stället för källtexturen. Om en fyllning renderas platt där originalet tydligt använde en upprepande bildtextur är källpenseln nästan säkert ett anpassat DIB-mönster snarare än en standardskraffyr, och det är det enda fallet värt att kontrollera för hand först. Att konfigurera en import för en ritning som den här går fortfarande genom samma alternativobjekt:

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;

Vad hindrar en felaktig metafil från att korrumpera sidan?

HotPDF:s svar är en enda spärr överst i ExecuteRecord i stället för en defensiv kontroll upprepad i vart och ett av dess omkring åttio posthanterare. En GDI-path-klammer, öppnad av EMR_BEGINPATH och stängd av EMR_ENDPATH eller EMR_ABORTPATH, spåras av en privat PathContinue-egenskap stödd av fältet FPathContinue. Medan den klammern är öppen släpper ExecuteRecord bara igenom path-konstruktionsposter, varianterna move, line, polyline, polygon, polybezier och polydraw, plus CLOSEFIGURE och en liten uppsättning transform- och DC-tillståndsposter som SETWORLDTRANSFORM, SAVEDC, och RESTOREDC. Varje annan posttyp som når ExecuteRecord medan klammern är öppen, till exempel en vilsekommen EXTTEXTOUT eller en bitmappsöverföring, kastas bort centralt med en enda Exit i samma ögonblick den anländer

Den spärren finns eftersom en path-klammer i en handskriven, verktygsgenererad, eller helt enkelt korrupt metafil inte garanterat innehåller endast det en välformad fil skulle lägga mellan sina öppnings- och stängningsposter. En textutdata-post som hamnar mellan EMR_BEGINPATH och EMR_ENDPATH skulle, utan en spärr, antingen förorena path-geometrin under konstruktion eller generera en PDF-textvisningsoperator mitt i en sekvens som ska vara ren path-konstruktion, och båda felläget är av det slag som visar sig på en felaktig indata från ett tredjepartsverktyg, inte på något en normal testsvit råkar täcka. Att centralisera kontrollen i ExecuteRecord innebär att de enskilda VEMR*-hanterarna inte var och en behöver försvara sig mot att anropas vid fel tidpunkt; spärren avgör det en gång, före dispatch, i stället för åttio gånger efteråt

Att placera ett vektordiagram bredvid text och bilder på en sida

En rapportsida rymmer sällan bara ett diagram, och ShowMetafile komponerar med HotPDF:s övriga sidoperatorer precis som vilket annat ritanrop som helst. En rubrik ritad med TextOut, ett skraffyrfyllt stapeldiagram importerat som en EMF, och en logotyp placerad med ShowImage kan alla hamna på samma sida i samma innehållsström, var och en behåller sin ursprungliga trohet, kompositionsmönstret som täcks i HotPDF:s guide till att lägga ut text, teckensnitt och bilder i en rapport:

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);

EMF- och WMF-tolken, axialskuggningsmönstren den registrerar för gradientfyllningar, och kakelmönster-mappningen för skraffyrborstar som beskrivs här levereras alla som en del av standardversionen av HotPDF-komponenten för Delphi och C++Builder, ett nativt VCL-bibliotek utan externa DLL-beroenden för något av detta