A PDFlibPas, a losLab Delphi PDF-könyvtára az [MS-EMF] egyes rekorddefinícióit követve alakítja az EMF Poly* rekordokat PDF útvonalakká: egy 32 bites EMR_POLYBEZIER a nulladik pontnál kezdődik, a polyline-ok nyitva maradnak és csak körvonalasan húzódnak meg, az EMR_POLYDRAW-ban a PT_CLOSEFIGURE egy jelzőbit, és minden pontszámot a rekord méretéhez kell viszonyítani. Ezek a szabályok három lépésben, a v3.539.39, a v3.539.41 és a v3.539.43 alatt érkeztek meg. Előtte előfordulhatott, hogy egy riportdiagram az ImportEMFFromFile-ból úgy került ki, hogy a trendvonal helyén kitöltött ék állt, egy Bezier-görbe rossz kontrollpont felé hajlott, vagy egy zárt körvonalból hiányzott az utolsó oldal. Egyik sem dobott hibát, és a szabályok minden Delphi EMF-PDF konverterre és GDI rekordparserre igazak
Miért csúsznak félre az EMF Poly* rekordok PDF-konverziónál?
Az EMF Poly* rekordok azért csúsznak félre, mert mindegyik a jelentésének egy részét a pontjain kívül hordozza: nyitva van-e az alakzat, az aktuális pozíciónál indul-e, melyik toll és ecset érvényes rá, és hol kezdődnek a pontok a rekordon belül. Egy enhanced metafile GDI hívások felvétele egy device contextre, így a konverternek nemcsak a koordinátákat, hanem azt a device context állapotot is le kell játszania. A PDF-ben nincs device context. Van egy path, abban egy aktuális pont, és egy rajzoló operátor, ami a stroke (S), a kitöltés (f) és a kettő együtt (B) között dönt. A két modell minden eltérése néma renderelési különbséggé válik
A Poly* család két szélességben is létezik. Minden 32 bites rekordnak, mint amilyen az EMR_POLYLINE, van egy 16 bites ikerpárja, mint az EMR_POLYLINE16, ami a pontokat SmallInt párokként tárolja. A GDI általában akkor veszi fel a kompakt formát, ha minden koordináta befér, így egy konverter 32 bites kezelői éveken át maradhatnak tévesek, miközben a mindennapi tesztrajzok sosem érnek el odáig. A leggyorsabb audit, ha ugyanazokat a pontokat mindkét rekordon átengeded, és összehasonlítod az eredő útvonalakat. Az itt tárgyalt rekordok mind az [MS-EMF] drawing rekordcsoportjában vannak (2.3.5 Drawing Record Types)
| Rekord | Kezdete | Zárt? | Aktuális pozíció |
|---|---|---|---|
EMR_POLYBEZIER | Nulladik pont | Nem | Nem használt, nem frissül |
EMR_POLYLINE | Nulladik pont | Nem (csak toll) | Nem használt, nem frissül |
EMR_POLYLINETO | Aktuális pozíció | Nem (csak toll) | Használt és frissül |
EMR_POLYPOLYLINE | Minden polyline első pontja | Nem (csak toll) | Nem használt, nem frissül |
EMR_POLYDRAW | Első PT_MOVETO, vagy aktuális pozíció | Csak ott, ahol a PT_CLOSEFIGURE be van állítva | Használt és frissül |
Hol kezdődik valójában egy EMR_POLYBEZIER görbe?
Egy EMR_POLYBEZIER görbe a nulladik pontnál kezdődik, és csak az 1. indextől kezdődő pontok csoportosulnak hármasokba: kontrollpont, kontrollpont, végpont. Egy 7 pontos rekord ezért két köbös szegmenst rajzol: a 0 a kezdő, az 1–3 az első szegmens, a 4–6 a második. Ezt a 16 bites kezelő a PDFlibPasban már így csinálta. A 32 bites kezelő viszont a nulladik pontnál kezdte a csoportosítást, így a kezdőpont az első kontrollpontként fogyott el, és minden későbbi szegmens eggyel csúszott. A görbe renderelődött, csak épp a rossz rajzolódott ki. A v3.539.41 óta mindkét szélesség m-mel nyitja az útvonalat a nulladik ponton, és minden teljes hármas után egy c-t ad ki
Ha saját parsert írsz: amelyik darabszám nem 1 plusz 3 többszöröse, az hibás, és a feleslegben maradt pontokat inkább hagyd figyelmen kívül, mint görbébe varrd
PolyDraw: a PT_CLOSEFIGURE jelzőbit, nem ponttípus
Az EMR_POLYDRAW-ban a PT_CLOSEFIGURE (értéke 1) olyan bit, ami a PT_LINETO-val (2) vagy a PT_BEZIERTO-val (4) kombinálódik, így egy érvényes típusbájt lehet 3 vagy 5 is. A ponttípus a bit maszkolása utáni bájt, a jelző pedig azt jelenti, hogy a ponton végződő szegmens után zárd be az alakzatot. A régi PDFlibPas kezelő egy case ágban egyetlen értékekre hasonlította a bájtot, így a 3-as és 5-ös típusú pontok semmihez sem illeszkedtek, és teljes egészében kiugrottak. Egy PolyDraw-val rajzolt téglalapból hiányzott a záró oldala, és egy Bezier hármas utolsó pontja, ha rajta ült a jelző, elveszett, ami az összes későbbi hármas szinkronját is eltolta
A v3.539.39 óta a típus olvasása Types[i] and not PT_CLOSEFIGURE formában történik, és a zárás csak egy teljes szegmens után kerül ki: zárt PT_LINETO-nál a vonal után, Bezier csoportnál a harmadik pont után. Egy hibás fájl, ami egy hármas első vagy második pontjára teszi a jelzőt, nem zárja be korán az alakzatot. Ugyanabban a kiadásban érkezett két kapcsolódó javítás is:
- Minden
PT_MOVETOújraindította a teljes útvonalat a 16 bitesEMR_POLYDRAW16-ban, így egy három alakzatot tartalmazó rekordból csak az utolsó maradt meg; most az első mozgás indítja az útvonalat, a későbbi mozik pedig alpathoz nyitnak - Egy PolyDraw rekord, ami nem
PT_MOVETO-val kezdődik, az aktuális pozíciónál indul, ahogy a rekorddefiníció írja, ahelyett hogy előzmény nélkülil-t vagyc-t írnamnélkül
Miért sosem szabad kitölteni egy EMF polyline-t PDF-ben?
Egy EMF polyline-t sosem szabad kitölteni, mert az EMR_POLYLINE és az EMR_POLYPOLYLINE nyitott alakzatok, amiket kizárólag a tollal rajzolnak, a PDF pedig egy nyitott path kitöltésekor implicit módon bezárja azt. Az ISO 32000-1 §8.5.3 kimondja, hogy a kitöltő operátorok minden nyitott alpathot lezárnak, mielőtt kirajzolnák. Egy konverter, ami egy hárompontos polyline-ra B-t vagy f-et ad ki, ezért egy kitöltött háromszöget fest az aktuális ecetszínnel: ez az a kitöltött ék a diagram trendvonala alatt. A v3.539.41 előtt a PDFlibPas mindkét polyline szélességet kitöltötte az ecsettel, a 32 bites rekordot pedig ráadásul explicit módon is bezárta. Ma mindkét szélesség sima stroke-dal ér véget, és a GDI különbsége megőrződik: a Polygon bezár és kitölt, a Polyline sosem teszi
A PolylineTo az aktuális pozíciónál indul
Az EMR_POLYLINETO az aktuális pozíciótól húz a rekord minden pontján át, nyitva marad, és az aktuális pozíciót az utolsó pontnál hagyja. A régi kezelőben ráadásul ott lapult egy külön eset, ami kikapcsolta a tollat, ha az első két pont y koordinátája megegyezett, és semmi nem kapcsolta vissza, így a fájl minden későbbi rekordja elvesztette a körvonalát. A toll állapota az EMR_SELECTOBJECT-hez és az EMR_CREATEPEN-hez tartozik; egy rajzoló rekord kezelőjének nincs dolga azzal. Ezt a külön esetet a v3.539.41 eltávolította, és a rekord egypontos formája már nem olvas a saját pontjain túlra (javítva a v3.539.39-ben)
A PolyPolyline pontjai a darabszám tömb után kezdődnek
A 32 bites EMR_POLYPOLYLINE nPolys darab darabszámot, majd cptl pontot tárol, és a pontok a 32 + nPolys * 4 bájtos offsetnél kezdődnek. A csapda az RTL-ben lapul: a Windows unit a TEMRPolyPolyline-t úgy deklarálja, hogy benne az aPolyCounts és az aptl egyelemű tömb, így az aptl[0] csak akkor az első pont, ha az nPolys értéke 1. Az aptl-t közvetlenül indexelő kód minden többvonalas rekordnál darabszámokat olvas koordinátaként. A régi PDFlibPas kezelő a tartományellenőrzését is erre a hibás elrendezésre alapozta, így az érvényes többvonalas rekordok elutasítódtak, az egyvonalasak pedig nem rajzoltak semmit. A v3.539.41 óta a PDFlibPas a kiszámolt offsetről keresi meg a ponttömböt, ahogy a PolyPolygon kezelője mindig is tette, és minden polyline-t saját, nyitott alpathként rajzol egyetlen lezáró stroke-dal. A v3.539.43-ban a 16 bites iker is ugyanígy járt; korábban szegmensenként rajzolt, ami széttörte a vonalcsatlakozásokat, és figyelmen kívül hagyott egy kiválasztott NULL_PEN-t
Az alapértelmezett toll és ecset, meg a path zárójelek
Két állapotszabály egészíti ki a polyline javításokat a v3.539.43-ban:
- Egy friss GDI device contexten már ki van választva a
BLACK_PENés aWHITE_BRUSH, így egy metafile, ami semmilyenEMR_SELECTOBJECTnélkül rajzol, ugyanúgy fekete körvonalat kap; a konverter korábban toll és kitöltés nélkül indult, és az ilyen rekordokran-t írt (path vége, semmi sem festődik) - Egy
BeginPath/EndPathzárójelben egyPolylinesem használja, sem nem frissíti az aktuális pozíciót, ezért az első pontjánál új alpathot kell nyitnia az előző alakzathoz csatlakozás helyett, és a zárójel lezárásáig, meg a stroke-olásáig vagy kitöltéséig semmi sem festődhet
EMF tesztfájl építése TMetafileCanvas-szel
A leggyorsabb módja, hogy e szabályok szerint ellenőrizd a konvertert, ha a TMetafileCanvas-szel egyetlen enhanced metafile-be veszed fel a három kockázatos hívást. Az alábbi rajz a görbéket üres ecsettel veszi fel, majd a polyline-hoz szándékosan tömör sárga ecsetet választ: egy helyes konverternek a polyline-nál figyelmen kívül kell hagynia azt az ecsetet, így a kimeneti PDF bármilyen sárgája hiba. A PolyDraw-nak nincs TCanvas csomagolója, ezért a Windows API-n át hívódik a canvas handle-lel, 3-as és 5-ös típusbájtokkal gyakoroltatva a záró jelzőt
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Zárt négyzet (3 = LINETO + CLOSEFIGURE), majd egy zárt Bezier
// alakzat, aminek az utolsó kontrollhármasa 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; // a görbékre csak körvonal jár
// A nulladik pont a kezdő; az 1..3 és a 4..6 két köbös szegmens
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));
// Nyitott V alakzat kiválasztott tömör ecsettel: stroke jár, sosem
// záródik sárga háromszöggé
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // lezárja a felvételt
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Mivel ezek a koordináták beférnek egy SmallInt-be, a GDI általában a 16 bites változatokat tárolja. A 32 bites kezelőkhöz kell egy olyan előállító, ami azokat írja, vagy kézzel épített rekordok. A kézzel épített fájloknak megvan a maguk csapdája: a VCL TMetafile.LoadFromStream-je csak akkor tekinti EMF-nek az adatfolyamot, ha a maradék hossz szigorúan nagyobb, mint a 108 bájtos TEnhMetaHeader. Egy rövidített fejlécű, kézzel írt minimális EMF, vagy egy épp 108 bájtos üres fájl WMF-ként kezelődik, és „Metafile is not valid” üzenettel utasítódik el. Mindig írd meg a teljes 108 bájtos fejlécet a kiterjesztő mezőkkel együtt, még a tesztrekordjaid előtt
Az EMF importálása PDF-be a PDFlibPasszal
A PDFlibPas az ImportEMFFromFile-lal vagy az ImportEMFFromStream-mel importál EMF-et, amik siker esetén nem nulla képazonosítót adnak vissza, bukásnál 0-t. A GeneralOptions = 0 megtartja a vektoros útvonalat, amiről ez a cikk szól; az 1 a metafile-t inkább bittérképpé raszterezi. A FontOptions = 1 a metafile fontjait be nem ágyazott TrueType fontokként veszi fel. A streames változat betöltés előtt a 0. pozícióra tekeri vissza az adatfolyamot, ezért olyan streamet adj át, ami csak a metafile-t tartalmazza
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); // bal felső origó a DrawImage-hez
PDF.SetMeasurementUnits(0); // pontok
// FontOptions 1 = fontok felvétele be nem ágyazott TrueType-ként
// GeneralOptions 0 = vektoros import, 1 = bittérkép
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// EMF esetén az ImageWidth / ImageHeight a keretméret pontokban
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// Az oldal csak a betöltött formot hívja meg: 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;
Egy vektoros EMF import form XObject lesz, így a GetPageContentToString csak a mentés-transzformáció-Do-visszaállítás sorozatot adja vissza. A Poly* rekordokból származó m, l, c, h és S operátorok a form XObject streamjében élnek, ami tömörített. Auditálásukhoz bontsd ki a mentett fájlt egy PDF objektuminspektorban, és olvasd el a form streamet: a fenti tesztfájlban a polyline-nak S-sel kell véget érnie, előtte h nélkül, a PolyDraw alakzatokban minden záró jelzőnél egy h-nak kell állnia, és ezen alpathok egyetlen egyén sem lehet f vagy B. A DrawImage az importált EMF-et a Width és a Height közül a kisebbivel skálázza arányosan, így a rajz megtartja képarányát, akkor is, ha az átadott doboz nem egyezik vele
Free Pascal céloknál nézd meg, hogyan fordul a PDFlibPas EMF vektorimportálója Free Pascal alatt; a rekordszemantika ugyanaz, bárhová is fordul az importáló
Hogyan kezelje egy EMF parser a fájlból jövő pontszámokat?
Egy EMF parser minden pontszámot megbízhatatlan bemenetként kezeljen, és még egyetlen pont másolása előtt ellenőrizze a rekord méretéhez viszonyítva. Az EnumEnhMetaFile csak azt garantálja, hogy minden rekord nSize-e a fájlon belül marad. Azt nem ellenőrzi, hogy a cptl egyezik-e az nSize-szel, így egy kezelő, ami cptl pontot másol Move-dal, a következő rekordokat olvassa be, vagy túlmegy a metafile végén, ha a darabszám hamisított vagy sérült. A v3.539.39 óta a PDFlibPas PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo és Polygon esetén mindkét szélességben ellenőrzi az nSize-et: rögzített fejléc plusz darabszám szorozva a pontonkénti bájtokkal, PolyDraw-nál pontonként még egy bájt a típusbájtokért. A PolyPoly rekordoknál az alakzatonkénti darabszámok összege sem haladhatja meg a deklarált totalt, a nullapontos alakzatok pedig kiugranak
Ugyanez az ellenőrzés elég rövid, hogy átmásold a saját parseredbe. Ez a változat egy 32 bites EMR_POLYPOLYLINE-t validál, és mutatót ad vissza a valódi ponttömbjére:
uses
Winapi.Windows;
// Nil-t ad vissza, hacsak a rekord nem valóban tartalmazza a deklarált pontokat.
// A pontok a counts tömb után kezdődnek: 32 + nPolys * 4 bájtra beljebb,
// nem az aptl[0]-nál, amit az RTL egyelemű tömbként deklarál
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; // hamisított vagy csonkolt darabszám
Total := 0;
Count := @P^.aPolyCounts[0]; // mutatóval lépegetve: a [0..0] range checket provokál
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // több pontot ígérnek az alakzatok, mint amennyi van
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
A pontbeférési teszt fut először, így a ciklus rálépése előtt már tudott, hogy a counts tömb a rekordon belül van. A számítás Int64, mert a 32 bitben számolt nPolys * 4 és cptl * 8 túlcsordulhat, és átmehet az összehasonlításon
Gyorsreferencia: EMF Poly* szabályok EMF-PDF konverzióhoz
EMR_POLYBEZIER: a nulladik pont a kezdőpont; az 1. ponttól hármasokban csoportosíts; a 32 bites rekord a v3.539.41-ben javítvaEMR_POLYLINE/EMR_POLYPOLYLINE: nyitott alakzatok, strokeS-sel, sosemh,fvagyB, mert a PDF kitöltés bezárja a nyitott alpathokatEMR_POLYLINETO: az aktuális pozíciónál indul, nyitva marad, frissíti az aktuális pozíciót, a toll állapotához sosem nyúl- 32 bites
EMR_POLYPOLYLINE: a pontok a32 + nPolys * 4bájtnál kezdődnek, nem azaptl[0]-nál EMR_POLYDRAW: maszkold le aPT_CLOSEFIGURE-t az elágaztatás előtt, zárj a befejezett szegmens után, és indulj az aktuális pozíciótól, ha az első pont nemPT_MOVETO- Az alapértelmezett device context állapot
BLACK_PENpluszWHITE_BRUSH; a v3.539.43 és újabbak ezt tiszteletben tartják BeginPath/EndPathközött minden polyline saját alpathot nyit, és a zárójel felhasználásáig semmi sem festődik- Minden
cptl/cptsértéket validálj 64 bites számítással aznSize-hez képest, mielőtt pontokat másolnál - A kézzel épített teszt EMF-eknek kell a teljes 108 bájtos fejléc, különben a
TMetafile.LoadFromStreamWMF-ként olvassa őket
Ha a riportjaid egy másik komponensen át készülnek, ugyanezek a rekordszemantikák érvényesek; a HotPDF EMF és WMF vektorimport bemutatja, hogy az a komponens hogyan varrja a gradiens és hatch ecseteket PDF mintákká, a vektorgrafika, shaderek és gradiensek a PDFlibPasban pedig azt, hogy hogyan rajzold ugyanazokat az alakzatokat közvetlenül a könyvtár API-jával metafile helyett
A PDFlibPas v3.539.43 vagy újabb minden fenti szabályt tartalmaz. Részletek és trial letöltések a PDFlibPas Delphi PDF library termékoldalon