Techninis straipsnis

PDFlibPas EMF importas: PolyDraw, Polyline, Bezier taisyklės

PDFlibPas, losLab PDF biblioteka Delphi, EMF Poly* įrašus verčia į PDF kelius laikydamasi kiekvieno įrašo apibrėžimo iš [MS-EMF]: 32 bitų EMR_POLYBEZIER prasideda ties 0 tašku, polilinijos lieka atviros ir yra tik braukiamos, PT_CLOSEFIGURE aibėje EMR_POLYDRAW yra vėliava, o kiekvienas taškų skaičius tikrinamas pagal įrašo dydį. Tos taisyklės atsirado versijose v3.539.39, v3.539.41 ir v3.539.43. Anksčiau ataskaitos diagrama iš ImportEMFFromFile galėjo išeiti su užpildytu sektoriumi ten, kur turėtų būti tendencijos linija, su Bezier kreive, palinkusia link netinkamo valdymo taško, arba su uždaru kontūru, kuriame trūksta paskutinės kraštinės. Joks šių atvejų nekelė klaidos, o taisyklės galioja bet kuriam Delphi EMF į PDF konverteriui ar GDI įrašų analizatoriui

Kodėl EMF Poly* įrašai sugenda verčiant į PDF?

EMF Poly* įrašai sugenda todėl, kad kiekvienas dalį savo prasmės neša už savo taškų ribų: ar figūra atvira, ar ji prasideda dabartinėje pozicijoje, kuris pen ir kuris teptukas taikomi bei kur įraše prasideda taškai. Išplėstinis metafailas yra GDI kvietimų įrenginio kontekstui įrašas, tad konverteriui tenka atkurti ne vien koordinates, bet ir to įrenginio konteksto būseną. PDF jokio įrenginio konteksto neturi. Jame yra kelias, dabartinis taškas to kelio viduje ir piešimo operatorius, kuris renkasi tarp braukimo (S), užpildymo (f) ir abiejų (B). Kiekvienas abiejų modelių nesutapimas virsta nebyliu atvaizdavimo skirtumu

Poly* šeima būna dviejų pločių. Kiekvienas 32 bitų įrašas, toks kaip EMR_POLYLINE, turi 16 bitų dvynį, pvz. EMR_POLYLINE16, kuris taškus saugo kaip SmallInt poras. GDI paprastai užrašo kompaktiškąją formą, kai tik visos koordinatės telpa, todėl konverterio 32 bitų dorokliai gali būti klaidingi metų metus, o kasdieniniai testiniai piešiniai iki jų niekada nenukeliauja. Greičiausia patikra – paleisti tuos pačius taškus pro abu įrašus ir palyginti gautus kelius. Čia aptariami įrašai visi priklauso [MS-EMF] piešimo įrašų grupei (2.3.5 Drawing Record Types)

ĮrašasPrasideda tiesUždaryta?Dabartinė pozicija
EMR_POLYBEZIER0 taškasNeNenaudojama, neatnaujinama
EMR_POLYLINE0 taškasNe (tik pen)Nenaudojama, neatnaujinama
EMR_POLYLINETODabartinė pozicijaNe (tik pen)Naudojama ir atnaujinama
EMR_POLYPOLYLINEKiekvienos polilinijos pirmasis taškasNe (tik pen)Nenaudojama, neatnaujinama
EMR_POLYDRAWPirmasis PT_MOVETO arba dabartinė pozicijaTik ten, kur numatytas PT_CLOSEFIGURENaudojama ir atnaujinama

Kur iš tikrųjų prasideda EMR_POLYBEZIER kreivė?

EMR_POLYBEZIER kreivė prasideda ties 0 tašku, o tik taškai, pradedant 1 indeksu, grupuojami po tris – valdymo taškas, valdymo taškas, galutinis taškas. Įrašas su 7 taškais todėl piešia du kubinius segmentus: 0 yra pradžia, 1–3 sudaro pirmąjį segmentą, 4–6 – antrąjį. 16 bitų doroklis PDFlibPas tai jau darydavo teisingai. 32 bitų doroklis pradėdavo grupuoti ties 0 tašku, tad pradinis taškas būdavo suvalgomas kaip pirmasis valdymo taškas, o visi vėlesni segmentai pasislinkdavo per vieną. Kreivė vis tiek atvaizduodavosi – tik ne ta. Nuo v3.539.41 abi pločių versijos atveria kelią su m ties 0 tašku ir po to kiekvienai pilnai triplei išduoda po vieną c

PDFlibPas EMR_POLYBEZIER įrašo su septyniais taškais diagrama, kurioje nulinis taškas atveria kelią su m, o taškai nuo vieno iki trijų ir nuo keturių iki šešių kiekvienas sudaro po vieną kubinį c segmentą; palyginamas v3.539.41 pataisytas 32 bitų doroklis ir senoji grupavimo schema, kurioje pradinis taškas būdavo suvalgomas kaip valdymo taškas
Nulinis taškas yra pradžios taškas, o tik pilnos triples už jo tampa kubiniais segmentais, todėl septynių taškų PolyBezier atvaizduojamas kaip m ir du c operatoriai

Jei skaitote patys: taškų skaičius, nelygus 1 plius 3 kartotiniui, reiškia sugadintą įrašą, o uodegoje likusius taškus derėtų ignoruoti, o ne priausti prie kreivės

PolyDraw: PT_CLOSEFIGURE yra vėliava, o ne taško tipas

Aibėje EMR_POLYDRAW PT_CLOSEFIGURE (reikšmė 1) yra bitas, jungiamas su PT_LINETO (2) arba PT_BEZIERTO (4), todėl teisėtas tipo baitas gali būti 3 arba 5. Taško tipas yra baitas su nureikšmintu tuo bitu, o vėliava reiškia uždaryti figūrą po segmento, kuris baigiasi šiame taške. Senasis PDFlibPas doroklis baitą lygindavo su atskiromis reikšmėmis case sakinyje, tad 3 ir 5 tipo taškai nesutapdavo su niekuo ir būdavo praleidžiami visai. Stačiakampis, pieštas su PolyDraw, prarasdavo uždaromąją kraštinę, o Bezier triple, kurio paskutinis taškas nešdavo vėliavą, prarasdavo tą tašką ir taip iš eilio išstumdavo visas vėlesnes triples

Nuo v3.539.39 tipas skaitomas kaip Types[i] and not PT_CLOSEFIGURE, o uždarymas išduodamas tik po pilno segmento: po linijos, kai uždaromas PT_LINETO, ir po trečiojo Bezier grupės taško. Sugadintas failas, kuris vėliavą prirašo prie pirmojo ar antrojo triple taško, figūros anksti neuždaro. Tas pats leidimas atnešė ir dvi susijusias pataisas:

  • Kiekvienas PT_MOVETO 16 bitų EMR_POLYDRAW16 įraše perleisdavo visą kelią iš naujo, todėl įrašas su trimis figūromis išsaugodavo tik paskutinę; dabar pirmasis judesys pradeda kelią, o kiekvienas vėlesnis atveria atskirą subkelią
  • PolyDraw įrašas, kuris neprasideda PT_MOVETO, prasideda dabartinėje pozicijoje, kaip ir sako įrašo apibrėžimas, o ne rašo l ar c operatorių be prieš jį einančio m
PDFlibPas EMR_POLYDRAW tipo baito anatomija, kurioje PT_CLOSEFIGURE yra nulinis vėliavos bitas, sujungtas OR su PT_LINETO arba PT_BEZIERTO, todėl teisėti tipo baitai 3 ir 5 prieš paskirstymą turi būti nureikšminti su and not PT_CLOSEFIGURE; senasis case sakinys praleisdavo abu baitus, o uždaromos figūros prarasdavo paskutinę kraštinę
Nureikšminkite uždarymo vėliavą dar prieš paskirstymą ir išduokite uždarymą tik po pilnos linijos ar Bezier triples, kitaip PolyDraw tyliai numeta taškus

Kodėl EMF polilinijos PDF niekada negalima užpildyti?

EMF polilinijos užpildyti negalima niekada, nes EMR_POLYLINE ir EMR_POLYPOLYLINE yra atviros figūros, piešiamos vien pen, o atviro kelio užpildymas PDF jį uždaro netiesiogiai. ISO 32000-1 §8.5.3 teigia, kad užpildymo operatoriai bet kokį atvirą subkelią prieš piešdami uždaro. Konverteris, kuris trijų taškų polilinijai išduoda B arba f, todėl nupiešia užpildytą trikampį dabartine teptuko spalva – tą užpildytą sektorių po diagramos tendencijos linija. Iki v3.539.41 PDFlibPas užpildydavo abi polilinijų pločių versijas teptuku, o 32 bitų įrašas dar ir buvo aiškiai uždaromas. Dabar abi versijos baigiasi vien brauku, o GDI skirtumas išlieka: Polygon uždaro ir užpildo, Polyline – niekada

PDFlibPas atviro V polilinijos, eksportuotos iš EMR_POLYLINE, palyginimas: teisingas konverteris kelią užbaigia braukimo operatoriumi S ir ignoruoja pasirinktąjį teptuką, o išduodamas f arba B netiesiogiai uždaro atvirą subkelią pagal ISO 32000-1 8.5.3 ir nupiešia užpildyto sektoriaus diagramos klaidą
Užpildymo operatorius bet kokį atvirą subkelią uždaro prieš piešdamas, todėl polilinijos turi baigtis S be jokio h, f ar B subkelyje

PolylineTo prasideda dabartinėje pozicijoje

EMR_POLYLINETO piešia nuo dabartinės pozicijos per kiekvieną įrašo tašką, lieka atviras ir dabartinę poziciją palieka ties paskutiniu tašku. Senajame doroklyje dar buvo specialus atvejis, kuris išjungdavo pen, kai pirmi du taškai dalijosi y koordinate, o niekas jo vėl neįjungdavo – todėl visi vėlesni failo įrašai prarasdavo kontūrą. Pen būsena priklauso EMR_SELECTOBJECT ir EMR_CREATEPEN; piešimo įrašo dorokliui kištis į ją negalima. Tas specialus atvejis pašalintas v3.539.41, o vieno taško forma daugiau neskaityta už savo pačių taškus (pataisyta v3.539.39)

PolyPolyline taškai prasideda po skaičių masyvo

32 bitų EMR_POLYPOLYLINE saugo nPolys skaičių, o paskui cptl taškus, ir taškai prasideda baito poslinkyje 32 + nPolys * 4. Spąstai – RTL viduje: Windows unitas deklaruoja TEMRPolyPolyline su aPolyCounts ir aptl kaip vieno elemento masyvais, tad aptl[0] yra pirmasis taškas tik tada, kai nPolys lygus 1. Kodas, kuris indeksuoja aptl tiesiai, kiekvienam kelių linijų įrašui skaito skaičių reikšmes kaip koordinates. Senasis PDFlibPas doroklis savo ribų patikrą taip pat dėstė pagal tą neteisingą išdėstymą, todėl teisėti kelių linijų įrašai būdavo atmetami, o vienos linijos įrašai nieko nepiešdavo. Nuo v3.539.41 PDFlibPas taškų masyvą suranda pagal apskaičiuotą poslinkį – kaip visada darydavo jo PolyPolygon doroklis – ir kiekvieną poliliniją piešia kaip atskirą atvirą subkelią su vienu brauku pabaigoje. v3.539.43 tą patį išgyveno ir 16 bitų dvynys; jis piešdavo segmentas po segmento, kas gadindavo linijų sujungimus ir ignoruodavo pasirinktąjį NULL_PEN

Numatytasis pen ir teptukas bei kelio skliaustai

Dvi būsenos taisyklės užbaigia v3.539.43 polilinijų pataisas:

  • Naujas GDI įrenginio kontekstas jau turi pažymėtus BLACK_PEN ir WHITE_BRUSH, todėl metafailas, piešiantis be jokio EMR_SELECTOBJECT, vis tiek piešia juodus kontūrus; konverteris anksčiau pradėdavo be pen ir be užpildymo ir tokiems įrašams rašydavo n (baigti kelią, nieko nepiešti)
  • Skliauste BeginPath / EndPath viduje Polyline dabartinės pozicijos nei naudoja, nei atnaujina, todėl privalo savo pirmame taške atverti naują subkelią, o ne prisijungti prie ankstesnės figūros, ir niekas negali būti piešiama, kol skliaustas nebrauktas ar neužpildytas

EMF testinio failo kūrimas su TMetafileCanvas

Greičiausias būdas patikrinti konverterį pagal šias taisykles – užrašyti tris rizikingus kvietimus į vieną išplėstinį metafailą su TMetafileCanvas. Žemiau esantis piešinys kreives užrašo su tuščiu teptuku, o tada polilinijai tyčia pažymi vientisą geltoną teptuką: teisingas konverteris polilinijai tą teptuką privalo ignoruoti, tad bet kokia geltona išvesties PDF yra klaida. PolyDraw neturi TCanvas įvyniojimo, todėl kviečiamas per Windows API su drobės rankena, naudojant tipo baitus 3 ir 5, kad išbandytų uždarymo vėliavą

uses
  Winapi.Windows, System.Types, Vcl.Graphics;

procedure BuildPolyTestEmf(const FileName: string);
const
  // Uždarytas kvadratas (3 = LINETO + CLOSEFIGURE), paskui uždaryta
  // Bezier figūra, kurios paskutinė valdymo triple baigiasi 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;    // kreivėms vien kontūrai
      // 0 taškas yra pradžia; 1..3 ir 4..6 yra du kubiniai segmentai
      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));
      // Atvira V forma su pažymėtu vientisu teptuku: braukiama,
      // niekada neuždaroma į geltoną trikampį
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // baigia įrašymą
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Kadangi šios koordinatės telpa į SmallInt, GDI paprastai užrašys 16 bitų variantus. Iki 32 bitų doroklių prireikia gamintojo, kuris juos rašo, arba rankomis sudėtų įrašų. Rankomis sudėti failai turi savų spąstų: VCL TMetafile.LoadFromStream srautą laiko EMF tik tada, kai likęs ilgis griežtai didesnis už 108 baitų TEnhMetaHeader. Minimalus ranka rašytas EMF su trumpa antrašte arba tuščias, kurio ilgis lygiai 108 baitai, palaikomas WMF ir atmetamas su pranešimu "Metafile is not valid". Prieš savo testinius įrašus visada rašykite pilną 108 baitų antraštę, įskaitant išplėtimo laukus

EMF importavimas į PDF su PDFlibPas

PDFlibPas importuoja EMF su ImportEMFFromFile arba ImportEMFFromStream, kurie sėkmės atveju grąžina ne nulinį paveikslėlio ID, o nesėkmės atveju 0. GeneralOptions = 0 palieka vektorinį kelią, apie kurį šis straipsnis; 1 vietoj to metafailą paverčia bitmapu. FontOptions = 1 prideda metafailo šriftus kaip neįterptus TrueType šriftus. Srauto variantas prieš įkeldamas srautą grąžina į 0 poziciją, tad perduokite srautą, kuriame yra vien metafailas

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);              // viršutinis kairysis kampas DrawImage
    PDF.SetMeasurementUnits(0);    // taškai
    // FontOptions 1 = pridėti šriftus kaip neįterptus TrueType
    // GeneralOptions 0 = vektorinis importas, 1 = bitmap
    ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
    if ImageID = 0 then
      raise Exception.Create('The metafile could not be imported');
    PDF.SelectImage(ImageID);
    // EMF atveju ImageWidth / ImageHeight yra kadro dydis taškais
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // Puslapis tik iškviečia importuotą formą: 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;

Vektorinis EMF importas tampa formos XObject, tad GetPageContentToString grąžina vien išsaugojimo, transformavimo, Do ir atstatymo seką. Operatoriai m, l, c, h ir S, pagaminti iš Poly* įrašų, gyvena formos XObject sraute, kuris suglaudintas. Juos apžiūrėti galima išskleidus išsaugotą failą PDF objektų inspektoriuje ir perskaičius formos srautą: pirmiau pateiktame testiniame faile turėtumėte matyti poliliniją, besibaigiančią S be jokio h prieš ją, po h prie kiekvienos uždarymo vėliavos PolyDraw figūrose ir jokio f ar B šiuose subkeliuose. DrawImage importuotą EMF taip pat vienodai masteluoja mažesniuoju iš Width ir Height, tad piešinys išlaiko proporcijas, net jei jūsų perduotas stačiakampis jų neatitinka

Free Pascal aplinkoms žiūrėkite kaip PDFlibPas EMF vektorinis importuotojas susikompiliuoja Free Pascal aplinkoje; įrašų semantika tokia pat, kur importuotojas besikompiliuotų

Kaip EMF analizatorius turėtų traktuoti failo taškų skaičius?

EMF analizatorius kiekvieną taškų skaičių turėtų traktuoti kaip nepatikimą įvedimą ir tikrinti pagal įrašo dydį dar prieš nukopijuodamas bent vieną tašką. EnumEnhMetaFile garantuoja tik tai, kad kiekvieno įrašo nSize lieka faile. cptl sutapimo su nSize jis netikrina, todėl doroklis, kuris cptl taškus kopijuoja su Move, suklastotam ar sugadintam skaičiui nuskaitys sekančius įrašus ar peršoks per metafailo galą. Nuo v3.539.39 PDFlibPas PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo ir Polygon įrašams abi pločių versijose tikrina fiksuotą antraštę plius skaičių kartus baitų vienam taškam pagal nSize, su vienu papildomu baitu taškui dėl PolyDraw tipo baitų. PolyPoly įrašuose atskirų figūrų skaičiai dar turi sumuotis į ne didesnę už deklaruotąją bendrąją sumą, o nulinio taškų skaičiaus figūros praleidžiamos

Ta pati patikra trumpa, kad ją galėtumėte nukopijuoti į savo analizatorių. Ši versija patvirtina 32 bitų EMR_POLYPOLYLINE ir grąžina rodyklę į tikrąjį jo taškų masyvą:

uses
  Winapi.Windows;

// Grąžina nil, nebent įrašas tikrai turi deklaruojamus taškus.
// Taškai prasideda po skaičių masyvo: 32 + nPolys * 4 baitų gilyn,
// o ne ties aptl[0], kurį RTL deklaruoja kaip vieno elemento masyvą
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;                         // suklastotas ar nukirptas skaičius
  Total := 0;
  Count := @P^.aPolyCounts[0];    // eiti rodykle: [0..0] sukelia range checks
  for I := 1 to P^.nPolys do
  begin
    Inc(Total, Count^);
    Inc(Count);
  end;
  if Total > P^.cptl then
    Exit;                         // figūros reikalauja daugiau taškų, nei jų yra
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

Taškų tilpimo patikra paleidžiama pirma, todėl iki ciklo apėjimo žinoma, kad skaičių masyvas slypi įraše. Aritmetika Int64, nes nPolys * 4 ir cptl * 8, apskaičiuoti 32 bitais, gali perpildyti ir praeiti pro palyginimą

Trumpa atmintinė: EMF Poly* taisyklės konversijai iš EMF į PDF

  • EMR_POLYBEZIER: 0 taškas yra pradžios taškas; grupuokite po tris nuo 1 taško; 32 bitų įrašas pataisytas v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: atviros figūros, braukite su S, niekada h, f ar B, nes PDF užpildymas uždaro atvirus subkelius
  • EMR_POLYLINETO: pradėkite dabartinėje pozicijoje, likite atviri, atnaujinkite dabartinę poziciją, nelieskite pen būsenos
  • 32 bitų EMR_POLYPOLYLINE: taškai prasideda ties baitu 32 + nPolys * 4, o ne ties aptl[0]
  • EMR_POLYDRAW: nureikšminkite PT_CLOSEFIGURE prieš paskirstydami, uždarykite po pilno segmento, pradėkite dabartinėje pozicijoje, kai pirmasis taškas nėra PT_MOVETO
  • Numatytoji įrenginio konteksto būsena – BLACK_PEN plius WHITE_BRUSH; v3.539.43 ir vėlesnės ją gerbia
  • Skliauste BeginPath / EndPath viduje kiekviena polilinija atveria savo subkelią, o niekas nepiešiama, kol skliaustas nepanaudotas
  • Patvirtinkite kiekvieną cptl / cpts pagal nSize 64 bitų aritmetika prieš kopijuodami taškus
  • Rankomis sudėtiems testiniams EMF reikia pilnos 108 baitų antraštės, kitaip TMetafile.LoadFromStream juos skaito kaip WMF

Jeigu jūsų ataskaitos keliauja per kitą komponentą, įrašų semantika tokia pat; HotPDF EMF ir WMF vektorinis importas pasakoja, kaip tas komponentas gradientines ir šrafines teptukų tekstūras paverčia PDF raštais, o vektorinė grafika, shaderiai ir gradientai PDFlibPas – kaip tuos pačius figūras piešti tiesiogiai bibliotekos API, ne per metafailą

PDFlibPas v3.539.43 ar vėlesnė apima visas aukščiau išvardytas taisykles. Išsamesnė informacija ir bandomieji parsisiuntimai – PDFlibPas Delphi PDF bibliotekos produkto puslapyje