Műszaki cikk

PDFlibPas EMF import: PolyDraw, Polyline és Bezier szabályok

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)

RekordKezdeteZárt?Aktuális pozíció
EMR_POLYBEZIERNulladik pontNemNem használt, nem frissül
EMR_POLYLINENulladik pontNem (csak toll)Nem használt, nem frissül
EMR_POLYLINETOAktuális pozícióNem (csak toll)Használt és frissül
EMR_POLYPOLYLINEMinden polyline első pontjaNem (csak toll)Nem használt, nem frissül
EMR_POLYDRAWElső PT_MOVETO, vagy aktuális pozícióCsak ott, ahol a PT_CLOSEFIGURE be van állítvaHaszná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

PDFlibPas ábra egy hét pontos EMR_POLYBEZIER rekordról, ahol a nulladik pont m-mel nyitja az útvonalat, az egy-három és a négy-hat pontok pedig egy-egy köbös c szegmenst alkotnak; szembeállítja a v3.539.41 óta javított 32 bites kezelőt a régi csoportosítással, ami a kezdőpontot első kontrollpontként fogyasztotta el
A nulladik pont a kezdőpont, és csak az utána következő teljes hármasok válnak köbös szegmenssé, így egy hétpontos PolyBezier m plusz két c operátorként renderelődik

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 bites EMR_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üli l-t vagy c-t írna m nélkül
PDFlibPas anatómiája egy EMR_POLYDRAW típusbájtnak, ahol a PT_CLOSEFIGURE a nulladik jelzőbit, amit PT_LINETO-ba vagy PT_BEZIERTO-ba OR-olnak, így a 3-as és 5-ös érvényes típusbájtokat and not PT_CLOSEFIGURE-jel kell maszkolni az elágaztatás előtt; a régi case ág mindkét bájtot átugrotta, a zárt alakzatokból pedig hiányzott az utolsó oldal
Maszkold le a záró jelzőt az elágaztatás előtt, és a zárást csak befejezett vonal vagy Bezier hármas után add ki, különben a PolyDraw néma csendben ejt el pontokat

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

PDFlibPas összehasonlítás egy EMR_POLYLINE-ból exportált nyitott V polyline-ról: a helyes konverter az útvonalat az S stroke operátorral zárja, és figyelmen kívül hagyja a kiválasztott ecsetet, az f vagy B kibocsátása pedig az ISO 32000-1 8.5.3 szerint implicit módon bezárja a nyitott alpathot, és kifesti a kitöltött ék diagramhibát
A kitöltő operátor bármely nyitott alpathot lezár, mielőtt kirajzolná, ezért a polyline-nak S-sel kell véget érnie, h, f vagy B nélkül az alpathon

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 a WHITE_BRUSH, így egy metafile, ami semmilyen EMR_SELECTOBJECT né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 rekordokra n-t írt (path vége, semmi sem festődik)
  • Egy BeginPath / EndPath zárójelben egy Polyline sem 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ítva
  • EMR_POLYLINE / EMR_POLYPOLYLINE: nyitott alakzatok, stroke S-sel, sosem h, f vagy B, mert a PDF kitöltés bezárja a nyitott alpathokat
  • EMR_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 a 32 + nPolys * 4 bájtnál kezdődnek, nem az aptl[0]-nál
  • EMR_POLYDRAW: maszkold le a PT_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 nem PT_MOVETO
  • Az alapértelmezett device context állapot BLACK_PEN plusz WHITE_BRUSH; a v3.539.43 és újabbak ezt tiszteletben tartják
  • BeginPath / EndPath kö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 az nSize-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.LoadFromStream WMF-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