Tehnički članak

PDFlibPas EMF uvoz: pravila za PolyDraw, Polyline i Bezier

PDFlibPas, losLab PDF biblioteka za Delphi, EMF Poly* zapise prevodi u PDF putanje prateći definiciju svakog zapisa u [MS-EMF]: 32-bitni EMR_POLYBEZIER kreće od tačke 0, polilinije ostaju otvorene i crta se samo njihova linija, PT_CLOSEFIGURE u EMR_POLYDRAW je zastavica, a svaki broj tačaka proverava se prema veličini zapisa. Ta pravila su stizala kroz v3.539.39, v3.539.41 i v3.539.43. Pre njih, grafikon u izveštaju mogao je iz ImportEMFFromFile izaći sa popunjenim klinom tamo gde treba da bude linija trenda, sa Bezier krivom savijenom ka pogrešnoj kontrolnoj tački ili sa zatvorenom konturom kome nedostaje poslednja strana. Ništa od toga nije podizalo grešku, a pravila važe za bilo koji Delphi EMF to PDF konverter ili parser GDI zapisa

Zašto se EMF Poly* zapisi pokvare pri PDF konverziji?

EMF Poly* zapisi zavaravaju jer svaki deo svog značenja nosi van svojih tačaka: da li je figura otvorena, da li kreće od trenutne pozicije, koji pen i brush važe i gde u zapisu počinju tačke. Enhanced metafile je snimak GDI poziva nad device context-om, pa konverter mora da reprodukuje i stanje tog device context-a, a ne samo koordinate. PDF nema device context. Ima putanju, trenutnu tačku unutar nje i operator iscrtavanja koji odlučuje između stroke (S), fill (f) i oba (B). Svako neslaganje ta dva modela postaje tiha razlika u renderu

Porodica Poly* dolazi i u dve širine. Svaki 32-bitni zapis poput EMR_POLYLINE ima 16-bitnog blizanca poput EMR_POLYLINE16 koji tačke čuva kao parove SmallInt. GDI obično snima kompaktni oblik kad se svaka koordinata uklapa, pa 32-bitni handler-i konvertera mogu godinama ostati pogrešni dok svakodnevni test crteži nikada ne dođu do njih. Najbrža provera je da iste tačke propustite kroz oba zapisa i uporedite dobijene putanje. Zapisi obrađeni ovde svi su u grupi crtačkih zapisa [MS-EMF]-a (2.3.5 Drawing Record Types)

ZapisPočinje odZatvoren?Trenutna pozicija
EMR_POLYBEZIERTačka 0NeNe koristi se, ne ažurira se
EMR_POLYLINETačka 0Ne (samo linija)Ne koristi se, ne ažurira se
EMR_POLYLINETOTrenutna pozicijaNe (samo linija)Koristi se i ažurira se
EMR_POLYPOLYLINEPrva tačka svake polilinijeNe (samo linija)Ne koristi se, ne ažurira se
EMR_POLYDRAWPrvi PT_MOVETO ili trenutna pozicijaSamo tamo gde je PT_CLOSEFIGURE postavljenKoristi se i ažurira se

Gde zapravo počinje kriva EMR_POLYBEZIER zapisa?

Kriva zapisa EMR_POLYBEZIER počinje od tačke 0, i samo se tačke od indeksa 1 nadalje grupišu u trojke: kontrolna tačka, kontrolna tačka, krajnja tačka. Zapis sa 7 tačaka tako crta dva kubna segmenta: 0 je početak, 1 do 3 čine prvi segment, 4 do 6 drugi. 16-bitni handler u PDFlibPas-u to je radio ispravno. 32-bitni handler počinjao je grupisanje od tačke 0, pa je početna tačka potrošena kao prva kontrolna tačka i svaki kasniji segment pomerio se za jedan. Kriva se iscrtala, samo pogrešna. Od v3.539.41 obe širine otvaraju putanju sa m na tački 0 i emituju po jedan c za svaku kompletnu trojku iza nje

PDFlibPas dijagram EMR_POLYBEZIER zapisa sa sedam tačaka gde tačka nula otvara putanju sa m, a tačke jedan do tri i četiri do šest svaka čine jedan kubni c segment, sa poređenjem ispravljenog 32-bitnog handlera od v3.539.41 i starog grupisanja koje je početnu tačku trošilo kao kontrolnu tačku
Tačka 0 je početna tačka i samo kompletne trojke iza nje postaju kubni segmenti, pa sedamtačkasti PolyBezier daje m plus dva c operatora

Za sopstveni parser: broj tačaka koji nije 1 plus višekratnik broja 3 je neispravan, a zaostale tačke treba ignorisati, a ne ušivati ih u krivu

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

U EMR_POLYDRAW je PT_CLOSEFIGURE (vrednost 1) bit koji se kombinuje sa PT_LINETO (2) ili PT_BEZIERTO (4), pa validan tip-bajt može biti 3 ili 5. Tip tačke je bajt sa tom maskiranim bitom, a zastavica znači zatvori figuru posle segmenta koji se završava na toj tački. Stari PDFlibPas handler poredio je bajt sa pojedinačnim vrednostima u case naredbi, pa tačke tipa 3 i 5 nisu poklapale ništa i potpuno su preskakane. Pravougaonik crtan PolyDraw-om gubio je zatvarajuću stranu, a Bezier trojka čija je poslednja tačka nosila zastavicu gubila je tu tač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 emituje tek posle kompletnog segmenta: posle linije kod zatvorenog PT_LINETO i posle treće tačke Bezier grupe. Neispravan fajl koji postavi zastavicu na prvu ili drugu tačku trojke ne zatvara figuru prerano. Dve srodne popravke stigle su u istom izdanju:

  • Svaki PT_MOVETO u 16-bitnom EMR_POLYDRAW16 restartovao je celu putanju, pa je zapis sa tri figure čuvao samo poslednju; sada prvi potez pokreće putanju, a kasniji potezi otvaraju nove subpath-ove
  • PolyDraw zapis koji ne počinje sa PT_MOVETO kreće od trenutne pozicije, kako kaže definicija zapisa, umesto da upiše l ili c operator bez prethodećeg m
PDFlibPas anatomija tip-bajta EMR_POLYDRAW zapisa gde je PT_CLOSEFIGURE flag bit nula spojen OR operacijom sa PT_LINETO ili PT_BEZIERTO, pa tip-bajtove 3 i 5 treba maskirati sa and not PT_CLOSEFIGURE pre raspodele; stara case naredba preskakala je oba bajta i zatvorene figure gubile su poslednju stranu
Maskirajte close zastavicu pre raspodele i emitujte zatvaranje tek posle završene linije ili Bezier trojke, ili će PolyDraw tiho gubiti tačke

Zašto EMF poliliniju u PDF-u nikada ne treba popunjavati?

EMF poliliniju nikada ne treba popunjavati jer su EMR_POLYLINE i EMR_POLYPOLYLINE otvorene figure crtane samo linijom, a popuna otvorene putanje u PDF-u implicira njeno zatvaranje. ISO 32000-1 §8.5.3 kaže da fill operatori zatvaraju svaki otvoreni subpath pre nego što ga iscrta. Konverter koji za trotačkastu poliliniju emituje B ili f zato iscrta popunjen trougao bojom trenutnog brush-a: popunjeni klin ispod linije trenda na grafikonu. Pre v3.539.41, PDFlibPas je popunjavao obe širine polilinije brush-om, a 32-bitni zapis je i eksplicitno zatvaran. Danas obe širine završavaju samo stroke-om, i GDI razlika je očuvana: Polygon zatvara i popunjava, Polyline nikada

PDFlibPas poređenje otvorene V polilinije izveštane iz EMR_POLYLINE: ispravan konverter završava putanju stroke operatorom S i ignoriše izabrani brush, dok emitovanje f ili B implicira zatvaranje otvorenog subpath-a po ISO 32000-1 8.5.3 i iscrta popunjeni klin, poznatu grafikon grešku
Fill operator zatvara svaki otvoreni subpath pre iscrtavanja, pa polilinije moraju da se završe sa S, bez h, f ili B na subpath-u

PolylineTo kreće od trenutne pozicije

EMR_POLYLINETO crta od trenutne pozicije kroz svaku tačku u zapisu, ostaje otvoren i ostavlja trenutnu poziciju na poslednjoj tački. Stari handler sadržao je i specijalan slučaj koji je isključivao pen kad su prve dve tačke delile y koordinatu, i ništa ga nikada nije vraćalo, pa je svaki kasniji zapis u fajlu gubio konturu. Stanje pena pripada EMR_SELECTOBJECT i EMR_CREATEPEN; handler crtačkog zapisa nema šta da ga menja. Taj specijalan slučaj uklonjen je u v3.539.41, a jedno-tačkasti oblik zapisa više ne čita iza sopstvenih tačaka (popravljeno u v3.539.39)

PolyPolyline tačke počinju iza niza brojača

32-bitni EMR_POLYPOLYLINE čuva nPolys brojača pa zatim cptl tačaka, i tačke počinju na bajt ofsetu 32 + nPolys * 4. Zamka je u RTL-u: jedinica Windows deklariše TEMRPolyPolyline sa aPolyCounts i aptl kao nizovima od jednog elementa, pa je aptl[0] prva tačka samo kad je nPolys 1. Kod koji direktno indeksira aptl čita vrednosti brojača kao koordinate kod svakog višelinijskog zapisa. Stari PDFlibPas handler merio je svoju proveru granica po tom pogrešnom rasporedu, pa su validni višelinijski zapisi odbacivani, a jednostavni nisu crtali ništa. Od v3.539.41 PDFlibPas locira niz tačaka iz izračunatog ofseta, kako je njegov PolyPolygon handler oduvek radio, i crta svaku poliliniju kao sopstveni otvoreni subpath sa jednim stroke-om na kraju. U v3.539.43 isti tretman dobio je i 16-bitni blizanac; crtao je segment po segment, što je lomilo spajanje linija i ignorišalo izabrani NULL_PEN

Podrazumevani pen i brush i path zagrade

Dva pravila o stanju dopunjuju polilinijske popravke u v3.539.43:

  • Svež GDI device context već ima izabrane BLACK_PEN i WHITE_BRUSH, pa metafile koji crta bez ijednog EMR_SELECTOBJECT ipak crta crne konture; konverter je ranije počinjao bez pena i bez popune i upisivao n (kraj putanje, ništa se ne crta) za takve zapise
  • Unutar BeginPath / EndPath zagrade, Polyline trenutnu poziciju ni ne koristi ni ne ažurira, pa mora da otvori novi subpath na svojoj prvoj tački umesto da se veže na prethodnu figuru, i ništa se ne sme iscrta dok zagrada ne dobije stroke ili popunu

Izgradnja EMF test fajla sa TMetafileCanvas

Najbrži način da proverite konverter protiv ovih pravila jeste da tri rizična poziva snimite u jedan enhanced metafile pomoću TMetafileCanvas. Crtež ispod snima krive sa šupljim brush-om, a zatim namerno bira pun žuti brush za poliliniju: ispravan konverter mora taj brush da ignoriše kod polilinije, pa je svaka žuta boja u izlaznom PDF-u bug. PolyDraw nema TCanvas omotač, pa se poziva kroz Windows API sa handle-om canvas-a, koristeći tip-bajtove 3 i 5 da bi se ispitala close zastavica

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

procedure BuildPolyTestEmf(const FileName: string);
const
  // Zatvoren kvadrat (3 = LINETO + CLOSEFIGURE), pa zatvorena Bezier
  // figura cija poslednja kontrolna trojka se zavrsava sa 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 konture za krive
      // Tačka 0 je početak; 1..3 i 4..6 su dva kubna 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 oblik V sa izabranim punim brush-om: stroke, bez zatvaranja
      // u žuti trougao
      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;

Pošto se ove koordinate uklapaju u SmallInt, GDI će obično snimiti 16-bitne varijante. Da biste došli do 32-bitnih handler-a, treba vam proizvođač koji ih upisuje ili zapisi koje gradite ručno. Ručno građeni fajlovi nose sopstvenu zamku: VCL TMetafile.LoadFromStream tretira tok kao EMF samo kad je preostala dužina strogo veća od 108-bajtnog TEnhMetaHeader. Minimalan ručno napisan EMF sa kratkim header-om, ili prazan tačno 108 bajtova dug, uzet je kao WMF i odbačen sa „Metafile is not valid“. Uvek upišite kompletan 108-bajtni header, polja za proširenje uključeno, pre svojih test zapisa

Uvoz EMF-a u PDF pomoću PDFlibPas-a

PDFlibPas uvozi EMF pomoću ImportEMFFromFile ili ImportEMFFromStream, koji vraćaju ne-nula image ID na uspeh a 0 na neuspeh. GeneralOptions = 0 čuva vektorsku putanju o kojoj je ovaj članak; 1 umesto toga rasterizuje metafile u bitmapu. FontOptions = 1 dodaje fontove iz metafile-a kao ne-ugrađene TrueType fontove. Varijanta sa tokom namota tok na poziciju 0 pre učitavanja, pa prosledite 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);              // koordinatni početak gore-levo za DrawImage
    PDF.SetMeasurementUnits(0);    // tačke
    // FontOptions 1 = dodaje fontove kao ne-ugrađ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 tačkama
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // Stranica samo poziva uveženi 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 sekvencu save, transform, Do i restore. Operatori m, l, c, h i S proizvedeni iz Poly* zapisa žive u toku form XObject-a, koji je komprimovan. Da biste ih pregledali, dekompresujte sačuvani fajl u PDF object inspector-u i pročitajte form tok: za gore navedeni test fajl trebalo bi da vidite da se polilinija završava sa S bez h ispred nje, po jedan h na svakoj close zastavici u PolyDraw figurama, i bez f ili B na bilo kom od ovih subpath-ova. DrawImage takođe ujednačeno skalira uvezeni EMF po manjoj od Width i Height, pa crtež zadržava omere i kad kvadrat koji prosledite ne odgovara njemu

Za Free Pascal mete, pogledajte kako se PDFlibPas EMF vektorski uvoznik gradi pod Free Pascal-om; semantika zapisa je ista gde god se uvoznik kompajlirao

Kako EMF parser treba da tretira brojeve tačaka iz fajla?

EMF parser treba svaki broj tačaka da tretira kao untrusted ulaz i da ga proveri prema veličini zapisa pre kopiranja ijedne tačke. EnumEnhMetaFile garantuje samo da svaki nSize zapisa ostaje unutar fajla. Ne proverava da li se cptl slaže sa nSize, pa handler koji kopira cptl tačaka sa Move pročitaće sledeće zapise, ili iza kraja metafile-a, kad je broj frifikovan ili oštećen. Od v3.539.39 PDFlibPas proverava fiksni header plus broj puta bajtova po tački prema nSize za PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo i Polygon u obe širine, sa dodatnim bajtom po tački za PolyDraw tip-bajtove. Za PolyPoly zapise brojači po figuri takođe moraju u zbiru da ne pređu deklarisani ukupan zbir, a figure bez tačaka se preskaču

Ista provera dovoljno je kratka da je iskopirate u sopstveni parser. Ova verzija validira 32-bitni EMR_POLYPOLYLINE i vraća pokazivač na njegov stvarni niz tačaka:

uses
  Winapi.Windows;

// Vraća nil osim ako zapis zaista sadrži tačke koje deklariše.
// Tačke počinju iza niza brojača: 32 + nPolys * 4 bajtova, ne na
// aptl[0], koji RTL deklariše kao niz 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;                         // frifikovan ili odsečen broj
  Total := 0;
  Count := @P^.aPolyCounts[0];    // šetnja pokazivačem: [0..0] aktivira range provere
  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 tačaka nego što postoji
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

Provera da tačke staju izvršava se prva, pa je poznato da je niz brojača unutar zapisa pre nego što ga petlja prošeta. Aritmetika je Int64 jer se nPolys * 4 i cptl * 8 izračunati u 32 bita mogu preliti i provući kroz poređenje

Brzi pregled: EMF Poly* pravila za EMF to PDF konverziju

  • EMR_POLYBEZIER: tačka 0 je početna tačka; grupišite od tačke 1 u trojkama; popravljeno za 32-bitni zapis u v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: otvorene figure, stroke sa S, nikada h, f ili B, jer PDF fill zatvara otvorene subpath-ove
  • EMR_POLYLINETO: krenite od trenutne pozicije, ostanite otvoreni, ažurirajte trenutnu poziciju, nikada ne dirajte stanje pena
  • 32-bitni EMR_POLYPOLYLINE: tačke počinju na bajtu 32 + nPolys * 4, a ne na aptl[0]
  • EMR_POLYDRAW: maskirajte PT_CLOSEFIGURE pre raspodele, zatvorite posle kompletnog segmenta, krenite od trenutne pozicije kad prva tačka nije PT_MOVETO
  • Podrazumevano stanje device context-a je BLACK_PEN plus WHITE_BRUSH; v3.539.43 i noviji ga poštuju
  • Unutar BeginPath / EndPath, svaka polilinija otvara sopstveni subpath i ništa se ne crta dok se zagrada ne iskoristi
  • Validirajte svaki cptl / cpts prema nSize u 64-bitnoj aritmetici pre kopiranja tačaka
  • Ručno građeni test EMF-ovi traže kompletan 108-bajtni header, ili će ih TMetafile.LoadFromStream čitati kao WMF

Ako vaši izveštaji idu kroz drugu komponentu, ista semantika zapisa važi; HotPDF EMF i WMF vektorski uvoz pokriva kako ta komponenta pretvara gradient i hatch brush-ove u PDF pattern-e, a vektorska grafika, shader-i i gradijenti u PDFlibPas-u pokriva crtanje istih oblika direktno API-jem biblioteke umesto preko metafile-a

PDFlibPas v3.539.43 ili noviji uključuje svako gore navedeno pravilo. Detalji i trial preuzimanja su na stranici proizvoda PDFlibPas Delphi PDF library