Tehnični članak

PDFlibPas in EMF: pravila za PolyDraw, Polyline in Bezier

PDFlibPas, losLabova knjižnica PDF za Delphi, pretvarja EMF zapise Poly* v PDF poti po definiciji vsakega zapisa v [MS-EMF]: 32-bitni EMR_POLYBEZIER se začne pri točki 0, zapisi Polyline ostanejo odprti in se samo obrišejo, PT_CLOSEFIGURE v EMR_POLYDRAW je zastavica, vsako število točk pa se preveri proti velikosti zapisa. Ta pravila so pristala skozi v3.539.39, v3.539.41 in v3.539.43. Pred njimi je lahko iz ImportEMFFromFile izšel graf poročila z zapolnjenim klinom tam, kjer naj bi bila trendna črta, Bezierjeva krivulja, upognjena proti napačni kontrolni točki, ali zaprt obris, ki mu je manjkala zadnja stranica. Nobeno od teh stanj ni javilo napake, pravila pa veljajo za kateri koli pretvornik EMF v PDF za Delphi ali razčlenjevalnik GDI zapisov

Zakaj se EMF zapisi Poly* pri pretvorbi v PDF pokvarijo?

Zapisi EMF Poly* se pokvarijo, ker vsak del svojega pomena nosi zunaj svojih točk: ali je figura odprta, ali se začne pri trenutnem položaju, kateri pisalo in čopič veljata ter kje v zapisu se točke začnejo. Enhanced metafile je posnetek klicev GDI nad device contextom, zato mora pretvornik predvajati tudi stanje tega device contexta, ne samo koordinate. PDF nima device contexta. Ima pot, trenutno točko znotraj te poti in risalni operator, ki se odloča med obrisom (S), polnjenjem (f) in obojim (B). Vsako neujemanje med tema modeloma postane tiha razlika v izrisu

Družina Poly* obstaja tudi v dveh širinah. Vsak 32-bitni zapis, kot je EMR_POLYLINE, ima 16-bitnega dvojčka, kot je EMR_POLYLINE16, ki shranjuje točke kot pare SmallInt. GDI običajno zabeleži kompaktno obliko, kadar se prilega vsaka koordinata, zato lahko 32-bitni handlerji pretvornika ostanejo pokvarjeni leta, medtem ko vsakodnevne testne risbe nikoli ne pridejo do njih. Najhitrejši pregled je, da iste točke posredujete skozi oba zapisa in primerjate nastali poti. Zapisi, obravnavani tukaj, so vsi v skupini risalnih zapisov [MS-EMF] (2.3.5 Drawing Record Types)

ZapisSe začne priZaprt?Trenutni položaj
EMR_POLYBEZIERTočka 0NeNi v uporabi, se ne posodablja
EMR_POLYLINETočka 0Ne (samo pisalo)Ni v uporabi, se ne posodablja
EMR_POLYLINETOTrenutni položajNe (samo pisalo)V uporabi in se posodablja
EMR_POLYPOLYLINEPrva točka vsake polylineNe (samo pisalo)Ni v uporabi, se ne posodablja
EMR_POLYDRAWPrvi PT_MOVETO ali trenutni položajSamo kjer je nastavljena PT_CLOSEFIGUREV uporabi in se posodablja

Kje se krivulja EMR_POLYBEZIER sploh začne?

Krivulja EMR_POLYBEZIER se začne pri točki 0, točke od indeksa 1 naprej pa se zbirajo po tri v trojčke kontrolna točka, kontrolna točka, končna točka. Zapis s 7 točkami torej nariše dva kubična odseka: 0 je začetek, 1 do 3 tvorijo prvi odsek, 4 do 6 drugi. To je 16-bitni handler v PDFlibPas že počel. 32-bitni handler je pričel združevati pri točki 0, zato je bila začetna točka porabljena kot prva kontrolna točka in vsak kasnejši odsek je zdrsnil za ena. Krivulja se je izrisala, le napačna. Od v3.539.41 obe širini odpreta pot z m pri točki 0 in izdatata en c za vsak popoln trojček za njo

PDFlibPas shema zapisa EMR_POLYBEZIER s sedmimi točkami, kjer točka nič odpre pot z m, točke ena do tri in štiri do šest pa vsake tvorijo po en kubični odsek c, v kontrastu med popavljenim 32-bitnim handlerjem od v3.539.41 in starim združevanjem, ki je začetno točko porabilo kot kontrolno točko
Točka 0 je začetna točka in samo popolni trojčki za njo postanejo kubični odseki, zato se PolyBezier s sedmimi točkami izriše kot m plus dva operatorja c

Za vaš lasten razčlenjevalnik: kadar število točk ni 1 plus mnogokratnik 3, je zapis pokvarjen, presežne točke na koncu pa je treba prezreti, namesto da bi jih zašili v krivuljo

PolyDraw: PT_CLOSEFIGURE je zastavica, ne tip točke

V EMR_POLYDRAW je PT_CLOSEFIGURE (vrednost 1) bit, ki se združi z PT_LINETO (2) ali PT_BEZIERTO (4), zato je veljaven bajt tipa lahko 3 ali 5. Tip točke je ta bajt z maskiranim tem bitom, zastavica pa pomeni, da se figura zapre za odsekom, ki se konča na tej točki. Stari handler v PDFlibPas je primerjal bajt s posameznimi vrednostmi v stavku case, zato točki tipa 3 in 5 nista zadeli ničesar in sta bili popolnoma preskočeni. Pravokotnik, narejen s PolyDraw, je izgubil zaključno stranico, trojček Bezierja, katerega zadnja točka je nosila zastavico, pa je izgubil to točko, s čimer je vsak kasnejši trojček zdrsnil iz koraka

Od v3.539.39 se tip prebere kot Types[i] and not PT_CLOSEFIGURE, zaprtje pa se izda šele po celotnem odseku: po črti pri zaprtem PT_LINETO in po tretji točki Bezierjeve skupine. Pokvarjena datoteka, ki nastavi zastavico na prvi ali drugi točki trojčka, figure ne zapre predčasno. V isti izdaji sta pristala še dva povezana popravka:

  • Vsak PT_MOVETO v 16-bitnem EMR_POLYDRAW16 je znova zagnal celotno pot, zato je zapis s tremi figurami obdržal samo zadnjo; zdaj prvi premik zažene pot, kasnejši pa odprejo podpoti
  • Zapis PolyDraw, ki se ne začne s PT_MOVETO, se začne pri trenutnem položaju, kot pravi definicija zapisa, namesto da bi zapisal operator l ali c brez predhodnega m
PDFlibPas anatomija bajta tipa EMR_POLYDRAW, kjer je PT_CLOSEFIGURE zastavični bit nič, z OR vpleten v PT_LINETO ali PT_BEZIERTO, zato morata biti veljavna bajta tipa 3 in 5 maskirana z and not PT_CLOSEFIGURE pred razporeditvijo; stari stavek case je preskočil oba bajta in zaprte figure so izgubile zadnjo stranico
Zapiralno zastavico maskirajte, preden razporedite, zaprtje pa izdajte šele po zaključeni črti ali Bezierjevem trojčku, sicer PolyDraw tiho izgublja točke

Zakaj EMF polyline v PDF-ju nikoli ne sme biti zapolnjena?

EMF polyline se nikoli ne sme zapolniti, ker sta EMR_POLYLINE in EMR_POLYPOLYLINE odprti figuri, narisani samo s pisalom, polnjenje odprte poti v PDF-ju pa jo implicitno zapre. ISO 32000-1 §8.5.3 pravi, da operatorji polnjenja zaprejo vsako odprto podpot, preden jo narišejo. Pretvornik, ki izda B ali f za polyline s tremi točkami, torej nariše zapolnjen trikotnik v barvi trenutnega čopiča: zapolnjeni klin pod trendno črto grafa. Pred v3.539.41 je PDFlibPas zapolnjeval obe širini polyline s čopičem, 32-bitni zapis pa je bil izrecno še zaprt. Danes obe širini končata samo z obrisom, GDI razlikovanje pa je ohranjeno: Polygon zapre in zapolni, Polyline tega nikoli ne stori

PDFlibPas primerjava odprte polyline v obliki V, izvožene iz EMR_POLYLINE: pravilen pretvornik konča pot z obrisnim operatorjem S in ignorira izbrani čopič, medtem ko izdaja f ali B implicitno zapre odprto podpot po ISO 32000-1 8.5.3 in nariše zapolnjeni klin, znano grafično napako
Operator polnjenja zapre vsako odprto podpot, preden jo nariše, zato se polyline morajo končati z S, brez h, f ali B na podpoti

PolylineTo se začne pri trenutnem položaju

EMR_POLYLINETO riše od trenutnega položaja skozi vsako točko v zapisu, ostane odprt in pusti trenutni položaj na zadnji točki. Stari handler je vseboval tudi poseben primer, ki je izklopil pisalo, kadar sta si prvi dve točki delili y koordinato, in nič ga nikoli ni znova vklopilo, zato je vsak kasnejši zapis v datoteki izgubil svoj obris. Stanje pisala spada k EMR_SELECTOBJECT in EMR_CREATEPEN; handler risalnega zapisa nima kaj spreminjati. Ta poseben primer je odstranjen v v3.539.41, enotočkovna oblika zapisa pa ne bere več čez svoje točke (popravljeno v v3.539.39)

Točke PolyPolyline se začnejo za poljem števil

32-bitni EMR_POLYPOLYLINE shrani nPolys števil in nato cptl točk, točke pa se začnejo pri bajtnem odmiku 32 + nPolys * 4. Past je v RTL: enota Windows deklarira TEMRPolyPolyline s aPolyCounts in aptl kot enoelementnima poljema, zato je aptl[0] prva točka samo, kadar je nPolys enak 1. Koda, ki indeksira aptl neposredno, bere vrednosti števil kot koordinate za vsak veččrtovni zapis. Stari handler v PDFlibPas je velikost svojega preverjanja meja prav tako osnoval na tej napačni postavitvi, zato so bili veljavni veččrtovni zapisi zavrnjeni, enočrtovni pa niso narisali nič. Od v3.539.41 PDFlibPas poišče polje točk na izračunanem odmiku, kot je njegov handler PolyPolygon počel vselej, in nariše vsako polyline kot njeno lastno odprto podpot z enim obrisom na koncu. V v3.539.43 je enako obravnavo dobil 16-bitni dvojček; ta je risal odsek za odsekom, kar je pokvarilo spoje črt in ignoriralo izbrani NULL_PEN

Privzeti pisalo in čopič ter oklepaji poti

Z dvema praviloma o stanju se zaokrožita popravka polyline v v3.539.43:

  • Svež GDI device context že ima izbrana BLACK_PEN in WHITE_BRUSH, zato metafile, ki riše brez kakršnega koli EMR_SELECTOBJECT, vseeno riše črne obrise; pretvornik je začenjal brez pisala in brez polnila ter je za take zapise zapisal n (konec poti, brez risanja)
  • Znotraj oklepaja BeginPath / EndPath Polyline ne uporablja in ne posodablja trenutnega položaja, zato mora odpreti novo podpot pri svoji prvi točki, namesto da bi se povezala s prejšnjo figuro, in nič se ne sme narisati, dokler oklepaja ne obrišete ali zapolnite

Izdelava preskusne datoteke EMF s TMetafileCanvas

Najhitrejši način, da pretvornik preverite proti tem pravilom, je, da tri tvegane klice posnamete v en enhanced metafile s TMetafileCanvas. Risba spodaj posname krivulje z votlim čopičem, za polyline pa namenoma izbere poln rumen čopič: pravilen pretvornik mora ta čopič za polyline ignorirati, zato je vsak rumeni odtenek v izhodnem PDF-ju hrošč. PolyDraw nima ovoja TCanvas, zato se kliče prek Windows API s ročajem canvasa, pri čemer bajta tipa 3 in 5 preizkušata zapiralno zastavico

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

procedure BuildPolyTestEmf(const FileName: string);
const
  // Zaprta figura: kvadrat (3 = LINETO + CLOSEFIGURE), potem še zaprta
  // Bezierjeva figura, katere zadnji kontrolni trojček konča s 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;    // samo obrisi za krivulje
      // Točka 0 je začetek; 1..3 in 4..6 so dva kubična odseka
      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));
      // Odprta oblika V z izbranim polnim čopičem: samo obris,
      // nikoli zaprta v rumen trikotnik
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // konča snemanje
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Ker te koordinate spravijo v SmallInt, GDI običajno shrani 16-bitne različice. Do 32-bitnih handlerjev pridete le s producentom, ki jih zapisuje, ali z zapisi, ki jih zgradite ročno. Ročno zgrajene datoteke prinašajo svojo past: VCL TMetafile.LoadFromStream tok obravnava kot EMF samo, kadar je preostala dolžina strogo večja od 108-bajtnega TEnhMetaHeader. Minimalni ročno napisani EMF s kratko glavo ali prazna datoteka, dolga natančno 108 bajtov, je sprejet za WMF in zavrnjen z "Metafile is not valid". Pred testnimi zapisi vedno zapišite celotno 108-bajtno glavo, vključno s polji razširitve

Uvoz EMF v PDF s PDFlibPas

PDFlibPas uvozi EMF s ImportEMFFromFile ali ImportEMFFromStream, ki vrneta neničelni ID slike ob uspehu in 0 ob napaki. GeneralOptions = 0 ohrani vektorsko pot, o kateri govori ta članek; 1 namesto tega rasterizira metafile v bitmapo. FontOptions = 1 doda pisave metafilea kot nevgrajene pisave TrueType. Različica s tokom tok pred nalaganjem prevrne na položaj 0, zato podajte tok, ki vsebuje samo 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);              // izhodišče zgoraj levo za DrawImage
    PDF.SetMeasurementUnits(0);    // točke
    // FontOptions 1 = doda pisave kot nevgrajene TrueType
    // GeneralOptions 0 = vektorski uvoz, 1 = bitmapa
    ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
    if ImageID = 0 then
      raise Exception.Create('The metafile could not be imported');
    PDF.SelectImage(ImageID);
    // Za EMF sta ImageWidth / ImageHeight velikost okvirja v točkah
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // Stran samo prikliče uvoženo formo: 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;

Vektorski uvoz EMF postane form XObject, zato GetPageContentToString vrne samo zaporedje shrani, transformiraj, Do in povrni. Operatorji m, l, c, h in S, izdelani iz zapisov Poly*, živijo v toku form XObject, ki je stisnjen. Za pregled razstisnite shranjeno datoteko v PDF objektnem inšpektorju in preberite tok forme: pri zgornji preskusni datoteki bi morali videti, da se polyline konča s S brez h pred njim, po en h pri vsaki zapiralni zastavici v figurah PolyDraw ter brez f ali B na kateri koli teh podpoti. DrawImage uvožen EMF tudi enakomerno skalira po manjšem od Width in Height, zato risba ohrani razmerje stranic, tudi kadar polje, ki ga podate, temu ne ustreza

Za tarče Free Pascal poglejte, kako se uvoznik vektorske grafike EMF v PDFlibPas zgradi pod Free Pascal; semantika zapisov je ista, kjer koli se uvoznik prevede

Kako naj EMF razčlenjevalnik ravna s števili točk iz datoteke?

EMF razčlenjevalnik naj vsako število točk obravnava kot nezaupljiv vhod in ga preveri proti velikosti zapisa, preden kopira eno samo točko. EnumEnhMetaFile jamči le, da ostane nSize vsakega zapisa znotraj datoteke. Ne preverja, da se cptl ujema s nSize, zato bo handler, ki s Move kopira cptl točk, bral naslednje zapise ali čez konec metafilea, kadar je število ponarejeno ali pokvarjeno. Od v3.539.39 PDFlibPas preverja fiksno glavo plus število krat bajti na točko proti nSize za PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo in Polygon v obeh širinah, z enim dodatnim bajtom na točko za bajte tipa PolyDraw. Pri zapisih PolyPoly morajo števila na figuro sešteti v največ deklarirano skupno vsoto, figure brez točk pa se preskočijo

Isti pregled je dovolj kratek, da ga skopirate v svoj razčlenjevalnik. Ta različica potrdi 32-bitni EMR_POLYPOLYLINE in vrne kazalec na njegovo pravo polje točk:

uses
  Winapi.Windows;

// Vrne nil, razen če zapis resnično vsebuje točke, ki jih deklarira.
// Točke se začnejo za poljem števil: 32 + nPolys * 4 bajtov znotraj,
// ne pri aptl[0], ki ga RTL deklarira kot enoelementno polje
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;                         // ponarejeno ali skrajšano število
  Total := 0;
  Count := @P^.aPolyCounts[0];    // hoja po kazalcu: [0..0] sproži preverjanja obsegov
  for I := 1 to P^.nPolys do
  begin
    Inc(Total, Count^);
    Inc(Count);
  end;
  if Total > P^.cptl then
    Exit;                         // figure zahtevajo več točk, kot jih je
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

Preizkus, ali točke gredo noter, teče najprej, zato je znano, da je polje števil znotraj zapisa, preden ga zanka prehodi. Aritmetika je Int64, ker se nPolys * 4 in cptl * 8, izračunana v 32 bitih, lahko prevrnteta in uidejo primerjavi

Hiter pregled: pravila EMF Poly* za pretvorbo EMF v PDF

  • EMR_POLYBEZIER: točka 0 je začetna točka; združujte od točke 1 naprej po tri; popravljeno za 32-bitni zapis v v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: odprte figure, obris s S, nikoli h, f ali B, ker polnjenje v PDF zapre odprte podpoti
  • EMR_POLYLINETO: začetek pri trenutnem položaju, ostane odprt, posodablja trenutni položaj, stanja pisala nikoli ne dotakne
  • 32-bitni EMR_POLYPOLYLINE: točke se začnejo na bajtu 32 + nPolys * 4, ne pri aptl[0]
  • EMR_POLYDRAW: maskirajte PT_CLOSEFIGURE, preden razporedite, zaprite po zaključenem odseku, začnite pri trenutnem položaju, kadar prva točka ni PT_MOVETO
  • Privzeto stanje device contexta je BLACK_PEN plus WHITE_BRUSH; v3.539.43 in novejši ga upoštevajo
  • Znotraj BeginPath / EndPath vsaka polyline odpre svojo podpot in nič se ne nariše, dokler oklepaja ne uporabite
  • Potrdite vsak cptl / cpts proti nSize v 64-bitni aritmetiki, preden kopirate točke
  • Ročno zgrajene preskusne EMF datoteke potrebujejo celotno 108-bajtno glavo, sicer jih TMetafile.LoadFromStream bere kot WMF

Če vaša poročila gredo skozi drugačno komponento, velja ista semantika zapisov; HotPDF EMF in WMF vektorski uvoz pokriva, kako ta komponenta pretvori čopiče gradientov in šrafur v vzorce PDF, vektorska grafika, shaderji in gradienti v PDFlibPas pa risanje istih likov neposredno z API knjižnice, namesto prek metafilea

PDFlibPas v3.539.43 ali novejši vključuje vsa zgoraj navedena pravila. Podrobnosti in preskusni prenosi so na strani izdelka PDFlibPas Delphi PDF knjižnice