PDFlibPas, losLab PDF knižnica pre Delphi, prevádza EMF záznamy Poly* na PDF cesty tak, že sa drží definície každého záznamu v [MS-EMF]: 32-bitový EMR_POLYBEZIER začína v bode 0, polylines ostanú otvorené a kreslia sa len obrysom, PT_CLOSEFIGURE v EMR_POLYDRAW je príznak a každý počet bodov sa kontroluje proti veľkosti záznamu. Tieto pravidlá pribudli priebežne vo v3.539.39, v3.539.41 a v3.539.43. Pred nimi mohol z ImportEMFFromFile vyjsť graf s vyplneným klinom tam, kde mala byť trendová línia, Bezier krivka ohnutá k nesprávnemu riadiacemu bodu alebo uzavretý obrys bez poslednej strany. Žiadna z týchto chýb nespadla výnimkou a pravidlá platia pre akýkoľvek konvertor EMF do PDF v Delphi či parser GDI záznamov
Prečo sa EMF záznamy Poly* pri konverzii do PDF pokazia?
Záznamy Poly* sa pokazia preto, lebo časť svojho významu nesú mimo svojich bodov: či je útvar otvorený, či začína na aktuálnej pozícii, ktoré pero a štetec sa použijú a kde v zázname body vlastne začínajú. Enhanced metafile je záznam volaní GDI voči device contextu, takže konvertor musí prehrať nielen súradnice, ale aj ten stav device contextu. PDF žiadny device context nemá. Má cestu, aktuálny bod vnútri tej cesty a maľovací operátor, ktorý rozhoduje medzi obrysom (S), výplňou (f) a obojím (B). Každá nezrovnalosť medzi oboma modelmi sa zmení na tichý rozdiel vo vykreslení
Rodina Poly* existuje aj v dvoch šírkach. Každý 32-bitový záznam ako EMR_POLYLINE má 16-bitové dvojča ako EMR_POLYLINE16, ktoré ukladá body ako dvojice SmallInt. GDI zvyčajne zaznamená kompaktnú formu, keď sa zmestia všetky súradnice, takže 32-bitové handlery konvertora môžu roky zostať pokazené, zatiaľ čo bežné testovacie kresby sa k nim nikdy nedostanú. Najrýchlejší audit je pustiť tie isté body cez oba záznamy a porovnať vzniknuté cesty. Záznamy pokryté v tomto článku patria všetky do skupiny kresliacich záznamov [MS-EMF] (2.3.5 Drawing Record Types)
| Záznam | Začína v | Uzavretý? | Aktuálna pozícia |
|---|---|---|---|
EMR_POLYBEZIER | Bod 0 | Nie | Nepoužíva sa, neaktualizuje sa |
EMR_POLYLINE | Bod 0 | Nie (len pero) | Nepoužíva sa, neaktualizuje sa |
EMR_POLYLINETO | Aktuálna pozícia | Nie (len pero) | Používa sa a aktualizuje |
EMR_POLYPOLYLINE | Prvý bod každej polyline | Nie (len pero) | Nepoužíva sa, neaktualizuje sa |
EMR_POLYDRAW | Prvý PT_MOVETO alebo aktuálna pozícia | Len kde je nastavený PT_CLOSEFIGURE | Používa sa a aktualizuje |
Kde vlastne začína krivka EMR_POLYBEZIER?
Krivka EMR_POLYBEZIER začína v bode 0 a len body od indexu 1 sa zoskupujú po trojiciach: riadiaci bod, riadiaci bod, koncový bod. Záznam so 7 bodmi teda kreslí dva kubické segmenty: 0 je štart, 1 až 3 tvoria prvý segment, 4 až 6 druhý. 16-bitový handler v PDFlibPas toto robil správne odjakživa. 32-bitový handler začal zoskupovať od bodu 0, takže štartovací bod skončil ako prvý riadiaci bod a každý ďalší segment sa posunul o jedno. Krivka sa vykreslila, len bola zlá. Od v3.539.41 obe šírky otvárajú cestu operátorom m v bode 0 a po ňom vypustia jedno c za každú kompletnú trojicu
Pre vlastný parser: počet, ktorý nie je 1 plus násobok 3, je deformovaný a prebytočné body treba ignorovať, nie došiť ich do krivky
PolyDraw: PT_CLOSEFIGURE je príznak, nie typ bodu
V EMR_POLYDRAW je PT_CLOSEFIGURE (hodnota 1) bit kombinovaný s PT_LINETO (2) alebo PT_BEZIERTO (4), takže platný typový bajt môže byť 3 alebo 5. Typ bodu je ten istý bajt s maskovaným príznakom a príznak znamená uzavrieť útvar po segmente, ktorý na tomto bode končí. Starý handler PDFlibPas porovnával bajt s jednotlivými hodnotami v case, takže body typu 3 a 5 nič nezabodli a preskočili sa úplne. Obdĺžnik nakreslený cez PolyDraw stratil zatváraciu stranu a Bezier trojica, ktorej posledný bod niesol príznak, stratila ten bod, čím sa každá ďalšia trojica rozhodila z kroku
Od v3.539.39 sa typ číta ako Types[i] and not PT_CLOSEFIGURE a uzáver sa vypúšťa až po celom segmente: po línii pre uzatvorený PT_LINETO a po treťom bode Bezier skupiny. Deformovaný súbor, ktorý nastaví príznak na prvom alebo druhom bode trojice, útvar predčasne neuzavrie. V tom istom release odišli dve súvisiace opravy:
- Každý
PT_MOVETOv 16-bitovomEMR_POLYDRAW16reštartoval celú cestu, takže záznam s tromi útvarmi si ponechal len ten posledný; teraz prvý pohyb začína cestu a ďalšie pohyby otvárajú podcesty - Záznam PolyDraw, ktorý nezačína
PT_MOVETO, začína na aktuálnej pozícii, ako hovorí definícia záznamu, namiesto zápisu operátoralalebocbez predchádzajúcehom
Prečo sa EMF polyline v PDF nesmie nikdy vyplniť?
EMF polyline sa nesmie nikdy vyplniť, pretože EMR_POLYLINE a EMR_POLYPOLYLINE sú otvorené útvary kreslené len perom a vyplnenie otvorenej cesty v PDF ju implicitne uzavrie. ISO 32000-1 §8.5.3 hovorí, že výplňové operátory uzavrú každý otvorený subpath ešte pred jeho vykreslením. Konvertor, ktorý vypustí B alebo f pre trojbodovú polyline, teda nakreslí vyplnený trojuholník aktuálnou farbou štetca: ten vyplnený klin pod trendovou líniou grafu. Pred v3.539.41 PDFlibPas vyplňoval obe šírky polyline štetcom a 32-bitový záznam bol navyše explicitne uzavretý. Dnes obe šírky končia len obrysom a GDI rozdiel ostáva zachovaný: Polygon uzavrie a vyplní, Polyline nikdy
PolylineTo začína na aktuálnej pozícii
EMR_POLYLINETO kreslí od aktuálnej pozície cez každý bod v zázname, ostáva otvorený a aktuálnu pozíciu nechá na poslednom bode. Starý handler navyše obsahoval špeciálny prípad, ktorý vypol pero, keď prvé dva body zdieľali y súradnicu, a nič ho už nikdy nezaplo, takže každý ďalší záznam v súbore stratil obrys. Stav pera riadia výhradne EMR_SELECTOBJECT a EMR_CREATEPEN; handler kresliaceho záznamu si k tomu nemá čo povedať. Ten špeciálny prípad odišiel vo v3.539.41 a jedno-bodová forma záznamu už nečíta za svoje vlastné body (opravené vo v3.539.39)
Body PolyPolyline začínajú až za poľom počtov
32-bitový EMR_POLYPOLYLINE ukladá nPolys počtov a potom cptl bodov a body začínajú na bajtovom ofsete 32 + nPolys * 4. Past je v RTL: jednotka Windows deklaruje TEMRPolyPolyline s aPolyCounts a aptl ako jednoprvkové polia, takže aptl[0] je prvý bod len vtedy, keď je nPolys 1. Kód, ktorý indexuje aptl priamo, číta hodnoty počtov ako súradnice pri každom viac polylinovom zázname. Starý handler PDFlibPas si navyše nastavil kontrolu hraníc podľa toho zlého rozloženia, takže platné záznamy s viacerými polylinami boli odmietané a záznamy s jedinou nenakreslili nič. Od v3.539.41 PDFlibPas hľadá pole bodov z vypočítaného ofsetu, rovnako ako to robil handler PolyPolygon odjakživa, a kreslí každú polyline ako vlastný otvorený subpath s jediným obrysom na konci. Vo v3.539.43 dostala rovnaké ošetrenie aj 16-bitové dvojča; dovtedy kreslila segment po segmente, čo rozbilo line joins a ignorovalo vybraný NULL_PEN
Predvolené pero a štetec a zátvorky cesty
Polyline opravy vo v3.539.43 uzatvárajú dve pravidlá o stave:
- Čerstvý GDI device context už má vybrané
BLACK_PENaWHITE_BRUSH, takže metafile, ktorý kreslí bez akéhokoľvekEMR_SELECTOBJECT, aj tak kreslí čierne obrysy; konvertor predtým štartoval bez pera a bez výplne a pre také záznamy písaln(koniec cesty, nič nenamaľuj) - Vnútri zátvorky
BeginPath/EndPathPolylineaktuálnu pozíciu ani nepoužíva, neaktualizuje, takže musí otvoriť nový subpath na svojom prvom bode namiesto napojenia sa na predchádzajúci útvar a nič sa nesmie namaľovať, kým sa zátvorka neobkreslí alebo nevyplní
Stavba testovacieho EMF súboru s TMetafileCanvas
Najrýchlejší spôsob, ako prevrieť konvertor proti týmto pravidlám, je zaznamenať tri rizikové volania do jedného enhanced metafile s TMetafileCanvas. Kresba nižšie zaznamená krivky s dutým štetcom a potom zámerne vyberie plný žltý štetec pre polyline: správny konvertor musí ten štetec pri polyline ignorovať, takže akákoľvek žltá vo výstupnom PDF je bug. PolyDraw nemá wrapper v TCanvas, takže sa volá cez Windows API s canvas handlom, s typovými bajtmi 3 a 5, aby sa príznak uzáveru dostal na skúšku
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Uzavretý štvorec (3 = LINETO + CLOSEFIGURE), potom uzavretý
// Bezier útvar, ktorej posledná riadiaca trojica končí 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; // pre krivky len obrysy
// Bod 0 je štart; 1..3 a 4..6 sú dva kubické segmenty
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));
// Otvorené V s vybraným plným štetcom: len obrys, nikdy neuzavreté
// do žltého trojuholníka
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // ukončí zaznamenávanie
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Keďže sa tieto súradnice zmestia do SmallInt, GDI ich bežne uloží v 16-bitových variantoch. K 32-bitovým handlerom sa dostanete len s producentom, ktorý ich zapisuje, alebo so záznamami postavenými ručne. Ručne stavané súbory prinášajú vlastnú pasť: VCL TMetafile.LoadFromStream berie stream ako EMF len vtedy, keď zostávajúca dĺžka je striktne väčšia ako 108-bajtový TEnhMetaHeader. Minimálny ručne písaný EMF s krátkou hlavičkou alebo prázdny presne 108 bajtov dlhý sa vezme za WMF a odmietne sa s hláškou „Metafile is not valid“. Hlavičku vždy zapisujte celú, 108 bajtov vrátane rozširujúcich polí, ešte pred testovacími záznamami
Import EMF do PDF s PDFlibPas
PDFlibPas importuje EMF cez ImportEMFFromFile alebo ImportEMFFromStream, ktoré vrátia nenulové image ID pri úspechu a 0 pri zlyhaní. GeneralOptions = 0 ponechá vektorovú cestu, o ktorú sa baví tento článok; 1 namiesto toho rasterizuje metafile na bitmapu. FontOptions = 1 pridá fonty metafilu ako nevložené TrueType fonty. Streamová varianta pretáča stream na pozíciu 0 pred načítaním, takže pošlite stream, ktorý obsahuje len 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); // ľavý horný roh ako počiatok pre DrawImage
PDF.SetMeasurementUnits(0); // body (points)
// FontOptions 1 = pridá fonty ako nevložené TrueType
// GeneralOptions 0 = vektorový import, 1 = bitmapa
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// Pre EMF sú ImageWidth / ImageHeight veľkosť rámu v bodoch
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// Strana len vyvoláva importovanú formu: 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;
Vektorový EMF import sa stane form XObjectom, takže GetPageContentToString vráti len sekvenciu save, transform, Do a restore. Operátory m, l, c, h a S vyrobené z Poly* záznamov bývajú v streame form XObjectu, ktorý je komprimovaný. Na audit ich rozbaľte uložený súbor v PDF object inspektore a prečítajte form stream: v testovacom súbore vyššie uvidíte polyline končiť S bez h pred ním, h na každom uzatváracom príznaku v útvaroch PolyDraw a žiadne f ani B na žiadnom z týchto subpathov. DrawImage navyše škáluje importovaný EMF uniformne menším z Width a Height, takže kresba si zachová pomer strán, aj keď box, ktorý podáte, mu nezodpovedá
Pre Free Pascal ciele pozrite, ako sa EMF vektorový importer PDFlibPas zostavuje pod Free Pascal; sémantika záznamov je tá istá, kamkoľvek sa importer skompiluje
Ako má EMF parser brať počty bodov zo súboru?
EMF parser má brať každý počet bodov ako nedôveryhodný vstup a skontrolovať ho proti veľkosti záznamu skôr, než skopíruje jediný bod. EnumEnhMetaFile garantuje len to, že nSize každého záznamu ostane vnútri súboru. Nekontroluje, že cptl sedí s nSize, takže handler, ktorý kopíruje cptl bodov cez Move, prečíta nasledujúce záznamy alebo zaletí za koniec metafilu, keď je počet sfalšovaný alebo poškodený. Od v3.539.39 PDFlibPas kontroluje pevnú hlavičku plus počet krát bajtov na bod proti nSize pre PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo a Polygon v oboch šírkach, s jedným prídavným bajtom na bod pre typové bajty PolyDraw. Pri záznamoch PolyPoly sa navyše počty jednotlivých útvarov musia sčítať na nanajvýš deklarovaný celok a útvary s nulou bodov sa preskočia
Rovnaká kontrola je krátka na to, aby ste si ju skopírovali do vlastného parsera. Táto verzia validuje 32-bitový EMR_POLYPOLYLINE a vracia pointer na jeho skutočné pole bodov:
uses
Winapi.Windows;
// Vracia nil, pokiaľ záznam naozaj nedrží body, ktoré deklaruje.
// Body začínajú za poľom počtov: 32 + nPolys * 4 bajtov od začiatku,
// nie na aptl[0], ktoré RTL deklaruje ako jednoprvkové pole
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; // sfalšovaný alebo skrátený počet
Total := 0;
Count := @P^.aPolyCounts[0]; // chodenie pointerom: [0..0] spustí range checky
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // útvary si nárokujú viac bodov, než existuje
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Test, či sa body zmestia, beží ako prvý, takže pole počtov je pred prechodom slučky už známe ako vnútri záznamu. Aritmetika je Int64, pretože nPolys * 4 a cptl * 8 počítané v 32 bitoch sa môžu pretočiť a porovnaním prejsť
Rýchla referencia: pravidlá EMF Poly* pre konverziu EMF do PDF
EMR_POLYBEZIER: bod 0 je štartovací bod; zoskupujte od bodu 1 po trojiciach; pre 32-bitový záznam opravené vo v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: otvorené útvary, obrys cezS, nikdyh,faniB, pretože PDF výplň uzavrie otvorené subpathyEMR_POLYLINETO: štart na aktuálnej pozícii, ostáva otvorený, aktualizuje aktuálnu pozíciu, do stavu pera nikdy nesiaha- 32-bitový
EMR_POLYPOLYLINE: body začínajú na bajte32 + nPolys * 4, nie naaptl[0] EMR_POLYDRAW: zamaskujtePT_CLOSEFIGUREpred dispatchom, uzavrite po dokončenom segmente, štartujte na aktuálnej pozícii, keď prvý bod nie jePT_MOVETO- Predvolený stav device contextu je
BLACK_PENplusWHITE_BRUSH; v3.539.43 a novšie ho rešpektujú - Vnútri
BeginPath/EndPathsi každá polyline otvára vlastný subpath a nič sa nenamaľuje, kým sa zátvorka nespotrebuje - Validujte každý
cptl/cptsprotinSizev 64-bitovej aritmetike skôr, než skopírujete body - Ručne stavané testovacie EMF potrebujú plnú 108-bajtovú hlavičku, inak ich
TMetafile.LoadFromStreamčíta ako WMF
Ak vaše reporty idú cez iný komponent, sémantika záznamov platí tá istá; HotPDF EMF a WMF vektorový import popisuje, ako ten komponent mení gradient a hatch štetce na PDF patterny, a vektorová grafika, shadery a gradienty v PDFlibPas pokrýva kreslenie tých istých tvarov priamo cez API knižnice namiesto cez metafile
PDFlibPas vo verzii v3.539.43 a novšej obsahuje každé pravidlo uvedené vyššie. Detaily a skúšobné stiahnutia nájdete na produktovej stránke PDFlibPas Delphi PDF library