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)
| Zapis | Počinje u | Zatvoren? | Trenutačna pozicija |
|---|---|---|---|
EMR_POLYBEZIER | Točka 0 | Ne | Ne koristi se, ne ažurira se |
EMR_POLYLINE | Točka 0 | Ne (samo penkalo) | Ne koristi se, ne ažurira se |
EMR_POLYLINETO | Trenutačna pozicija | Ne (samo penkalo) | Koristi se i ažurira se |
EMR_POLYPOLYLINE | Prva točka svake polyline | Ne (samo penkalo) | Ne koristi se, ne ažurira se |
EMR_POLYDRAW | Prvi PT_MOVETO, ili trenutačna pozicija | Samo tamo gdje je postavljena PT_CLOSEFIGURE | Koristi 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
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_MOVETOu 16-bitnomEMR_POLYDRAW16ponovno 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_MOVETOpočinje u trenutačnoj poziciji, kako kaže definicija zapisa, umjesto da zapiše operatorlilicbez prethodnogm
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
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_PENiWHITE_BRUSH, pa metafile koji crta bez ijednogEMR_SELECTOBJECTipak crta crne obrise; pretvarač je nekada počinjao bez penkala i bez ispune te zapisivaon(kraj putanje, ništa se ne crta) za takve zapise - Unutar zagrade
BeginPath/EndPath,Polylinene 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.41EMR_POLYLINE/EMR_POLYPOLYLINE: otvorene figure, obris saS, nikadh,fniB, jer PDF ispuna zatvara otvorena podputanjaEMR_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 bajtu32 + nPolys * 4, ne naaptl[0] EMR_POLYDRAW: izmaskirajtePT_CLOSEFIGUREprije raspodjele, zatvorite nakon dovršenog segmenta, počnite u trenutačnoj poziciji kad prva točka nijePT_MOVETO- Zadano stanje konteksta uređaja jest
BLACK_PENplusWHITE_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/cptspremanSizeu 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