Tehnički članak

PDFlibPas EMF uvoz: pravila za PolyDraw, Polyline i Bezier

PDFlibPas, losLabova PDF biblioteka za Delphi, pretvara EMF zapise Poly* u PDF putanje slijedeći definiciju svakog zapisa u [MS-EMF]: 32-bitni EMR_POLYBEZIER počinje u točki 0, polylines ostaju otvoreni i crtaju se samo obrisom, PT_CLOSEFIGURE u EMR_POLYDRAW jest zastavica, a svaki brojač točaka provjerava se prema veličini zapisa. Ta su pravila stigla kroz v3.539.39, v3.539.41 i v3.539.43. Prije njih grafikon u izvještaju mogao je iz ImportEMFFromFile izaći s ispunjenim klinom tamo gdje treba stajati linija trenda, s Bezierovom krivinom savinutom prema pogrešnoj kontrolnoj točki ili sa zatvorenim obrisom kojem nedostaje posljednja stranica. Ništa od toga nije prijavilo pogrešku, a pravila vrijede za svaki Delphi EMF-u-PDF pretvarač i parser GDI zapisa

Zašto EMF zapisi Poly* pođu po zlu u pretvorbi u PDF?

EMF zapisi Poly* pođu po zlu jer svaki dio svog značenja nosi izvan svojih točaka: je li figura otvorena, počinje li u trenutačnoj poziciji, koje se penkalo i četka primjenjuju i gdje u zapisu točke počinju. Enhanced metafile snimak je GDI poziva nad kontekstom uređaja, pa pretvarač mora reproducirati i stanje tog konteksta uređaja, a ne samo koordinate. PDF nema kontekst uređaja. Ima putanju, trenutačnu točku unutar te putanje i operator crtanja koji odlučuje između obrisa (S), ispune (f) i obojega (B). Svako nepoklapanje ta dva modela postaje tiha razlika u renderiranju

Obitelj Poly* dolazi i u dvije širine. Svaki 32-bitni zapis poput EMR_POLYLINE ima 16-bitnog blizanca poput EMR_POLYLINE16 koji točke sprema kao parove vrijednosti SmallInt. GDI obično snima kompaktni oblik kad se svaka koordinata uklopi, pa 32-bitni rukovatelji pretvarača mogu godinama ostati pogrešni dok svakodnevni testni crteži nikad ne dođu do njih. Najbrža provjera jest propustiti iste točke kroz oba zapisa i usporediti dobivene putanje. Zapisi obrađeni ovdje svi su u skupini crtačkih zapisa [MS-EMF-a] (2.3.5 Drawing Record Types)

ZapisPočinje uZatvoren?Trenutačna pozicija
EMR_POLYBEZIERTočka 0NeNe koristi se, ne ažurira se
EMR_POLYLINETočka 0Ne (samo penkalo)Ne koristi se, ne ažurira se
EMR_POLYLINETOTrenutačna pozicijaNe (samo penkalo)Koristi se i ažurira se
EMR_POLYPOLYLINEPrva točka svake polylineNe (samo penkalo)Ne koristi se, ne ažurira se
EMR_POLYDRAWPrvi PT_MOVETO, ili trenutačna pozicijaSamo tamo gdje je postavljena PT_CLOSEFIGUREKoristi se i ažurira se

Gdje krivina EMR_POLYBEZIER zapravo počinje?

Krivina EMR_POLYBEZIER počinje u točki 0, a samo se točke od indeksa 1 nadalje grupiraju u trojke: kontrolna točka, kontrolna točka, završna točka. Zapis sa 7 točaka stoga iscrtava dva kubična segmenta: 0 je početak, 1 do 3 čine prvi segment, 4 do 6 drugi. 16-bitni rukovatelj u PDFlibPasu to je već radio. 32-bitni rukovatelj počeo je grupirati od točke 0, pa je početna točka potrošena kao prva kontrolna točka i svaki je kasniji segment pomaknut za jedno mjesto. Krivina se i dalje renderirala, samo pogrešna. Od v3.539.41 obje širine otvaraju putanju s m u točki 0 i emitiraju jedan c za svaku dovršenu trojku iza nje

PDFlibPas dijagram zapisa EMR_POLYBEZIER sa sedam točaka gdje točka nula otvara putanju s m, a točke jedan do tri i četiri do šest svaka čine jedan kubični c segment, u kontrastu s ispravljenim 32-bitnim rukovateljem od v3.539.41 i starim grupiranjem koje je potrošilo početnu točku kao kontrolnu točku
Točka 0 jest početna točka i samo dovršene trojke iza nje postaju kubični segmenti, pa PolyBezier sa sedam točaka renderira se kao operator m plus dva operatora c

Za vlastiti parser: brojač koji nije 1 plus višekratnik broja 3 neispravan je oblik, a točke viška treba ignorirati, a ne zašiti ih u krivinu

PolyDraw: PT_CLOSEFIGURE je zastavica, a ne tip točke

U EMR_POLYDRAWu PT_CLOSEFIGURE (vrijednost 1) jest bit koji se kombinira s PT_LINETO (2) ili PT_BEZIERTO (4), pa valjani bajt tipa može biti 3 ili 5. Tip točke jest taj bajt s tim izmaskiranim bitom, a zastavica znači zatvori figuru nakon segmenta koji završava na ovoj točki. Stari PDFlibPas rukovatelj uspoređivao je bajt s pojedinačnim vrijednostima u case naredbi, pa točke tipa 3 i 5 nisu odgovarale ničemu i potpuno su preskačene. Pravokutnik nacrtan s PolyDrawom gubio je zatvarajuću stranicu, a Bezierova trojka čija je posljednja točka nosila zastavicu gubila je tu točku, što je svaku kasniju trojku izbacivalo iz koraka

Od v3.539.39 tip se čita kao Types[i] and not PT_CLOSEFIGURE, a zatvaranje se emitira tek nakon cijelog segmenta: nakon linije za zatvoreni PT_LINETO i nakon treće točke Bezierove grupe. Neispravna datoteka koja postavi zastavicu na prvoj ili drugoj točki trojke ne zatvara figuru prerano. Dva povezana popravka stigla su u istom izdanju:

  • Svaki PT_MOVETO u 16-bitnom EMR_POLYDRAW16 ponovno je pokretao cijelu putanju, pa je zapis s tri figure zadržavao samo posljednju; sada prvi move pokreće putanju, a kasniji otvaraju podputanja
  • PolyDraw zapis koji ne počinje s PT_MOVETO počinje u trenutačnoj poziciji, kako kaže definicija zapisa, umjesto da zapiše operator l ili c bez prethodnog m
PDFlibPas anatomija bajta tipa EMR_POLYDRAW gdje je PT_CLOSEFIGURE nulta zastavična bit ILI-irana u PT_LINETO ili PT_BEZIERTO, pa valjane bajtove tipa 3 i 5 treba maskirati s and not PT_CLOSEFIGURE prije raspodjele; stara case naredba preskakala je oba bajta pa su zatvorene figure gubile posljednju stranicu
Izmaskirajte zastavicu zatvaranja prije raspodjele i emitirajte zatvaranje tek nakon dovršene linije ili Bezierove trojke, ili će PolyDraw tiho gubiti točke

Zašto EMF polyline nikad ne smije biti ispunjen u PDF-u?

EMF polyline nikad se ne smije ispuniti jer su EMR_POLYLINE i EMR_POLYPOLYLINE otvorene figure crtane samo penkalom, a ispuna otvorene putanje u PDF-u zatvara je implicitno. ISO 32000-1 §8.5.3 kaže da operatori ispune zatvaraju svako otvoreno podputanje prije nego što ga nacrtaju. Pretvarač koji emitira B ili f za polyline od tri točke stoga crta ispunjeni trokut u boji trenutačne četke: ispunjeni klin ispod linije trenda u grafikonu. Prije v3.539.41 PDFlibPas ispunjavao je obje polyline širine četkom, a 32-bitni zapis i eksplicitno zatvarao. Danas obje širine završavaju samo crtanjem obrisa, a GDI razlika je očuvana: Polygon zatvara i ispunjava, Polyline nikad

PDFlibPas usporedba otvorene V polyline izvezene iz EMR_POLYLINE: ispravan pretvarač završava putanju stroke operatorom S i ignorira odabranu četku, dok emitiranje f ili B implicitno zatvara otvoreno podputanje pod ISO 32000-1 8.5.3 i crta ispunjeni klin, bug grafikona
Operator ispune zatvara svako otvoreno podputanje prije crtanja, pa polylinei moraju završiti sa S, bez h, f ili B na podputanju

PolylineTo počinje u trenutačnoj poziciji

EMR_POLYLINETO crta od trenutačne pozicije kroz svaku točku u zapisu, ostaje otvoren i ostavlja trenutačnu poziciju na posljednjoj točki. Stari rukovatelj sadržavao je i poseban slučaj koji je isključivao penkalo kad su prve dvije točke dijelile y koordinatu, a ništa ga nikad nije vratilo, pa je svaki kasniji zapis u datoteci gubio obris. Stanje penkala pripada EMR_SELECTOBJECTu i EMR_CREATEPENu; rukovatelj crtačkog zapisa nema što tu mijenjati. Taj je poseban slučaj uklonjen u v3.539.41, a oblik zapisa s jednom točkom više ne čita iza vlastitih točaka (popravljeno u v3.539.39)

Točke PolyPolylinea počinju iza polja brojača

32-bitni EMR_POLYPOLYLINE sprema nPolys brojača, a zatim cptl točaka, i točke počinju na bajtnom pomaku 32 + nPolys * 4. Zamka je u RTL-u: jedinica Windows deklarira TEMRPolyPolyline s aPolyCounts i aptl kao poljima od jednog elementa, pa je aptl[0] prva točka samo kad je nPolys 1. Kod koji izravno indeksira aptl čita vrijednosti brojača kao koordinate za svaki višelinijski zapis. Stari PDFlibPas rukovatelj svoju provjeru granica bazirao je i na tom pogrešnom rasporedu, pa su valjani višelinijski zapisi odbacivani, a jednolinijski nisu crtali ništa. Od v3.539.41 PDFlibPas locira polje točaka iz izračunatog pomaka, kao što je njegov PolyPolygon rukovatelj oduvijek radio, i crta svaku polyline kao vlastito otvoreno podputanje s jednim iscrtavanjem na kraju. U v3.539.43 dobio je 16-bitni blizanac isto tretiranje; crtao je segment po segment, što je lomilo spajanje linija i ignoriralo odabrani NULL_PEN

Zadano penkalo i četka te zagrade putanje

Dva pravila stanja zaokružuju polyline popravke u v3.539.43:

  • Svježi GDI kontekst uređaja već ima odabrane BLACK_PEN i WHITE_BRUSH, pa metafile koji crta bez ijednog EMR_SELECTOBJECT ipak crta crne obrise; pretvarač je nekada počinjao bez penkala i bez ispune te zapisivao n (kraj putanje, ništa se ne crta) za takve zapise
  • Unutar zagrade BeginPath / EndPath, Polyline ne koristi ni ne ažurira trenutačnu poziciju, pa mora otvoriti novo podputanje u svojoj prvoj točki umjesto da se spoji na prethodnu figuru, i ništa se ne smije nacrtati dok se zagrada ne upotrijebi obrism ili ispunom

Izgradnja EMF testne datoteke s TMetafileCanvasom

Najbrži način da pretvarač provjerite protiv ovih pravila jest snimiti tri rizična poziva u jedan enhanced metafile s TMetafileCanvasom. Crtež dolje snima krivine sa šupljom četkom, a zatim namjerno bira punu žutu četku za polyline: ispravan pretvarač tu četku mora za polyline ignorirati, pa svaka žuta boja u izlaznom PDF-u jest bug. PolyDraw nema TCanvas omot, pa se poziva preko Windows API-ja s handleom platna, s bajtovima tipa 3 i 5 kako bi se iskušala zastavica zatvaranja

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

procedure BuildPolyTestEmf(const FileName: string);
const
  // Zatvoren kvadrat (3 = LINETO + CLOSEFIGURE), pa zatvorena Bezierova
  // figura čija posljednja kontrolna trojka završava 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 krivine
      // Točka 0 je početak; 1..3 i 4..6 su dva kubična segmenta
      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));
      // Otvoreni V oblik s odabranom punom četkom: samo obris, nikad
      // zatvoren u žuti trokut
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // završava snimanje
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Budući da se te koordinate uklope u SmallInt, GDI će obično spremiti 16-bitne varijante. Da dođete do 32-bitnih rukovatelja, treba vam proizvođač koji ih piše ili zapisi koje gradite ručno. Ručno građene datoteke dolaze sa svojom zamkom: VCL TMetafile.LoadFromStream tok tretira kao EMF samo kad je preostala duljina strogo veća od 108-bajtnog TEnhMetaHeadera. Minimalni ručno pisani EMF s kratkim zaglavljem ili prazan točno 108 bajtova dugačak uzet je za WMF i odbijen s porukom „Metafile is not valid". Uvijek zapišite cijelo 108-bajtno zaglavlje, uključujući polja proširenja, prije svojih testnih zapisa

Uvoz EMF-a u PDF s PDFlibPasom

PDFlibPas uvozi EMF s ImportEMFFromFile ili ImportEMFFromStream, koji vraćaju nenulti ID slike na uspjehu, a 0 na neuspjehu. GeneralOptions = 0 zadržava vektorsku putanju o kojoj se ovdje radi; 1 umjesto toga metafile rasterizira u bitmapu. FontOptions = 1 dodaje fontove metafilea kao neugrađene TrueType fontove. Varijanta s tokom vraća tok na poziciju 0 prije učitavanja, pa prenesite tok koji sadrži 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);              // ishodište gore lijevo za DrawImage
    PDF.SetMeasurementUnits(0);    // točke
    // FontOptions 1 = dodaj fontove kao neugrađene 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, ImageWidth / ImageHeight su veličina okvira u točkama
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // Stranica samo poziva uvezeni form XObject: 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 EMF uvoz postaje form XObject, pa GetPageContentToString vraća samo niz save, transformacija, Do i restore. Operatori m, l, c, h i S proizvedeni iz Poly* zapisa žive u toku form XObjecta, koji je komprimiran. Da ih pregledate, dekomprimirajte spremljenu datoteku u PDF inspektoru objekata i pročitajte form tok: za gornju testnu datoteku trebali biste vidjeti polyline koji završava sa S, bez h prije njega, h kod svake zastavice zatvaranja u PolyDraw figurama, te nijedan f ni B na tim podputanjima. DrawImage uvezeni EMF skalira uniformno, prema manjoj od vrijednosti Width i Height, pa crtež zadržava omjer stranica i ako ga okvir koji prenesete ne poštuje

Za Free Pascal ciljeve pogledajte kako se PDFlibPas EMF vektorski uvoznik gradi pod Free Pascalom; semantika zapista ista je gdje god se uvoznik kompilira

Kako EMF parser treba tretirati brojače točaka iz datoteke?

EMF parser svaki brojač točaka treba tretirati kao nepouzdani ulaz i provjeriti ga prema veličini zapisa prije nego što kopira ijednu točku. EnumEnhMetaFile jamči samo da nSize svakog zapisa ostaje unutar datoteke. Ne provjerava slaže li se cptl s nSizeom, pa rukovatelj koji kopira cptl točaka s Move čitat će sljedeće zapise, ili izaći iza kraja metafilea, kad je brojač krivotvoren ili oštećen. Od v3.539.39 PDFlibPas provjerava fiksno zaglavlje plus brojač puta bajtova po točki prema nSizeu za PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo i Polygon u obje širine, s dodatnim bajtom po točki za PolyDraw bajtove tipa. Za PolyPoly zapise brojači po figuri moraju se u zbroju zaustaviti na najviše deklariranom totalu, a figure bez točaka preskaču se

Ista je provjera dovoljno kratka da je kopirate u vlastiti parser. Ova verzija validira 32-bitni EMR_POLYPOLYLINE i vraća pokazivač na njegovo stvarno polje točaka:

uses
  Winapi.Windows;

// Vraća nil osim ako zapis stvarno drži točke koje deklarira.
// Točke počinju iza polja brojača: 32 + nPolys * 4 bajta ulaska, ne na
// aptl[0], koji RTL deklarira kao polje od jednog elementa
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;                         // krivotvoren ili skraćen brojač
  Total := 0;
  Count := @P^.aPolyCounts[0];    // hodaj pokazivačem: [0..0] okida provjere raspona
  for I := 1 to P^.nPolys do
  begin
    Inc(Total, Count^);
    Inc(Count);
  end;
  if Total > P^.cptl then
    Exit;                         // figure traže više točaka nego što postoji
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

Test uklapanja točaka izvodi se prvi, pa je poznato da je polje brojača unutar zapisa prije nego što ga petlja prošeće. Aritmetika je Int64 jer se nPolys * 4 i cptl * 8 izračunati u 32 bita mogu omotati i proći usporedbu

Brza referenca: pravila EMF Poly* zapisa za pretvorbu EMF-a u PDF

  • EMR_POLYBEZIER: točka 0 jest početna točka; grupirajte od točke 1 u trojkama; popravljeno za 32-bitni zapis u v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: otvorene figure, obris sa S, nikad h, f ni B, jer PDF ispuna zatvara otvorena podputanja
  • EMR_POLYLINETO: počni u trenutačnoj poziciji, ostani otvoren, ažuriraj trenutačnu poziciju, nikad ne diraj stanje penkala
  • 32-bitni EMR_POLYPOLYLINE: točke počinju na bajtu 32 + nPolys * 4, ne na aptl[0]
  • EMR_POLYDRAW: izmaskirajte PT_CLOSEFIGURE prije raspodjele, zatvorite nakon dovršenog segmenta, počnite u trenutačnoj poziciji kad prva točka nije PT_MOVETO
  • Zadano stanje konteksta uređaja jest BLACK_PEN plus WHITE_BRUSH; v3.539.43 i kasnije to poštuju
  • Unutar BeginPath / EndPath, svaka polyline otvara vlastito podputanje i ništa se ne crta dok se zagrada ne upotrijebi
  • Validirajte svaki cptl / cpts prema nSizeu u 64-bitnoj aritmetici prije kopiranja točaka
  • Ručno građene testne EMF datoteke trebaju cijelo 108-bajtno zaglavlje, ili ih TMetafile.LoadFromStream čita kao WMF

Ako vaši izvještaji idu kroz drugu komponentu, ista semantika zapista vrijedi; HotPDF EMF i WMF vektorski uvoz pokriva kako ta komponenta gradijentne i šrafirane četke pretvara u PDF obrasce, a vektorska grafika, shaderi i gradijenti u PDFlibPasu pokriva crtanje istih oblika izravno bibliotečkim API-jem umjesto preko metafilea

PDFlibPas v3.539.43 ili kasniji uključuje svako gore navedeno pravilo. Detalji i probna preuzimanja nalaze se na stranici proizvoda PDFlibPas Delphi PDF biblioteke