PDFlibPas, PDF knihovna losLab pro Delphi, převádí záznamy EMF Poly* na cesty PDF tak, že se drží definice každého záznamu v [MS-EMF]: 32bitový EMR_POLYBEZIER začíná v bodě 0, polylinie zůstávají otevřené a kreslí se jen tahem, PT_CLOSEFIGURE v EMR_POLYDRAW je příznak a každý počet bodů se kontroluje proti velikosti záznamu. Tahle pravidla přibyla postupně ve v3.539.39, v3.539.41 a v3.539.43. Před nimi mohl graf v reportu vylézt z ImportEMFFromFile s vyplněným klínem tam, kde měla být trendová linie, s Bezier křivkou zahnutou ke špatnému řídícímu bodu, nebo s uzavřeným obrysem, kterému chyběla poslední strana. Ani jedna z těchto chyb nevyhodila chybovou hlášku a pravidla platí pro jakýkoli převodník EMF do PDF v Delphi i pro parser GDI záznamů
Proč se záznamy EMF Poly* při převodu do PDF pokazí?
Záznamy EMF Poly* se pokazí proto, že část svého významu nesou mimo své body: jestli je útvar otevřený, jestli začíná v aktuální pozici, které pero a štětec se uplatní a kde v záznamu body začínají. Enhanced metafile je záznam volání GDI proti device contextu, takže převodník musí přehrát nejen souřadnice, ale i stav toho device contextu. PDF žádný device context nemá. Má cestu, aktuální bod uvnitř té cesty a kreslicí operátor, který rozhoduje mezi stroke (S), fill (f) a obojím (B). Každá neshoda obou modelů se promění v tichý rozdíl ve vykreslení
Rodina Poly* navíc existuje ve dvou šířkách. Každý 32bitový záznam jako EMR_POLYLINE má 16bitového dvojčete jako EMR_POLYLINE16, které ukládá body jako páry SmallInt. GDI obvykle zaznamená kompaktní formu, když se všechny souřadnice vejdou, takže 32bitové handlery převodníku můžou roky zůstat rozbité, zatímco běžné testovací kresby se k nim nikdy nedostanou. Nejrychlejší audit je pustit stejné body oběma záznamy a porovnat výsledné cesty. Záznamy popsané tady patří všechny do skupiny kreslicích záznamů [MS-EMF] (2.3.5 Drawing Record Types)
| Záznam | Začíná v | Uzavřený? | Aktuální pozice |
|---|---|---|---|
EMR_POLYBEZIER | Bod 0 | Ne | Nepoužívá se, neaktualizuje se |
EMR_POLYLINE | Bod 0 | Ne (jen pero) | Nepoužívá se, neaktualizuje se |
EMR_POLYLINETO | Aktuální pozice | Ne (jen pero) | Používá se a aktualizuje se |
EMR_POLYPOLYLINE | První bod každé polylinie | Ne (jen pero) | Nepoužívá se, neaktualizuje se |
EMR_POLYDRAW | První PT_MOVETO, nebo aktuální pozice | Jen kde je nastaven PT_CLOSEFIGURE | Používá se a aktualizuje se |
Kde vlastně začíná křivka EMR_POLYBEZIER?
Křivka EMR_POLYBEZIER začíná v bodě 0 a jen body od indexu 1 výš se sdružují po trojicích řídící bod, řídící bod, koncový bod. Záznam se 7 body nakreslí dva kubické segmenty: 0 je start, 1 až 3 tvoří první segment, 4 až 6 druhý. 16bitový handler v PDFlibPas to už dělal správně. 32bitový handler začal sdružovat od bodu 0, takže startovní bod se spolkl jako první řídící bod a každý další segment se posunul o jedničku. Křivka se vykreslila, jen úplně jiná. Od v3.539.41 obě šířky otevírají cestu m v bodě 0 a pro každou úplnou trojici za ním vypustí jedno c
Pro vlastní parser: počet, který není 1 plus násobek 3, je poškozený a přebytečné body na konci je potřeba ignorovat, ne došívat do křivky
PolyDraw: PT_CLOSEFIGURE je příznak, ne typ bodu
V EMR_POLYDRAW je PT_CLOSEFIGURE (hodnota 1) bit kombinovaný s PT_LINETO (2) nebo PT_BEZIERTO (4), takže platný typový bajt může být 3 nebo 5. Typ bodu je bajt s tímto bitem zamaskovaným pryč a příznak znamená uzavřít útvar po segmentu, který na tomto bodu končí. Starý handler v PDFlibPas porovnával bajt s jednotlivými hodnotami v case větvi, takže body typu 3 a 5 na nic nenarazily a celé se přeskočily. Obdélník kreslený přes PolyDraw ztratil zavírací stranu a Bezier trojice, jejíž poslední bod nesl příznak, ztratila ten bod, což rozchodilo všechny další trojice
Od v3.539.39 se typ čte jako Types[i] and not PT_CLOSEFIGURE a uzavření se vypustí až po celém segmentu: za přímkou u uzavřeného PT_LINETO a za třetím bodem Bezier skupiny. Poškozený soubor, který nastaví příznak na prvním nebo druhém bodu trojice, útvar zavře předčasně ne. Ve stejném vydání vyjely dvě související opravy:
- Každý
PT_MOVETOv 16bitovémEMR_POLYDRAW16restartoval celou cestu, takže záznam se třemi útvary ponechal jen ten poslední; teď první pohyb cestu zahájí a další pohyby otevírají podcesty - Záznam PolyDraw, který nezačíná
PT_MOVETO, začíná v aktuální pozici, jak říká definice záznamu, místo aby zapsal operátorlnebocbez předchozíhom
Proč se polylinie z EMF nikdy nesmí v PDF vyplňovat?
Polylinie z EMF se nikdy nesmí vyplnit, protože EMR_POLYLINE a EMR_POLYPOLYLINE jsou otevřené útvary kreslené jen perem a vyplnění otevřené cesty v PDF ji implicitně zavře. ISO 32000-1 §8.5.3 říká, že výplňové operátory zavřou jakýkoli otevřený subpath před jeho vykreslením. Převodník, který pro tříbodovou polylinii vypustí B nebo f, tedy vykreslí vyplněný trojúhelník aktuální barvou štětce: vyplněný klín pod trendovou linií grafu. Před v3.539.41 plnil PDFlibPas obě šířky polylinií štětcem a 32bitový záznam byl navíc explicitně uzavřený. Dnes obě šířky končí jen tahem a GDI rozdíl zůstává zachován: Polygon zavře a vyplní, Polyline nikdy
PolylineTo začíná v aktuální pozici
EMR_POLYLINETO kreslí od aktuální pozice přes každý bod v záznamu, zůstává otevřený a aktuální pozici nechá na posledním bodu. Starý handler navíc obsahoval zvláštní případ, který vypnul pero, když první dva body sdílely y souřadnici, a nic ho nikdy nezapnulo zpátky, takže každý další záznam v souboru ztratil obrys. Stav pera patří k EMR_SELECTOBJECT a EMR_CREATEPEN; handler kreslicího záznamu do něj nemá co mluvit. Ten zvláštní případ zmizel ve v3.539.41 a jedno-bodová forma záznamu už nečte za vlastními body (opraveno ve v3.539.39)
Body PolyPolyline začínají za polem počtů
32bitový EMR_POLYPOLYLINE ukládá nPolys počtů a pak cptl bodů, přičemž body začínají na bajtovém offsetu 32 + nPolys * 4. Past je v RTL: unit Windows deklaruje TEMRPolyPolyline s aPolyCounts a aptl jako jednoprvková pole, takže aptl[0] je první bod jen tehdy, když nPolys je 1. Kód, který indexuje aptl přímo, čte hodnoty počtů jako souřadnice u každého víceřádkového záznamu. Starý handler PDFlibPas měl navíc kontrolu mezí dimenzovanou na tohle špatné rozložení, takže platné víceřádkové záznamy byly odmítnuty a jednořádkové nenakreslily nic. Od v3.539.41 hledá PDFlibPas pole bodů z vypočteného offsetu, tak jako jeho handler PolyPolygon vždycky, a kreslí každou polylinii jako vlastní otevřený subpath s jedním tahem na konci. Ve v3.539.43 dostalo 16bitové dvojče stejnou péči; kreslilo segment po segmentu, což rozbilo spoje linek a ignorovalo vybraný NULL_PEN
Výchozí pero a štětec a závorky cesty
Opravy polylinií ve v3.539.43 doplňují dvě pravidla o stavu:
- Čerstvý GDI device context už má vybrané
BLACK_PENaWHITE_BRUSH, takže metafile, který kreslí bez jedinéhoEMR_SELECTOBJECT, stejně kreslí černé obrysy; převodník dřív startoval bez pera a bez výplně a pro takové záznamy psaln(konec cesty, nic se nevykreslí) - Uvnitř závorky
BeginPath/EndPathaniPolylinenepoužívá, ani neaktualizuje aktuální pozici, takže musí otevřít nový subpath ve svém prvním bodě místo napojení na předchozí útvar, a dokud se závorka neobtáhne nebo nevyplní, nesmí se nic vykreslit
Vytvoření testovacího EMF souboru přes TMetafileCanvas
Nejrychlejší cesta, jak převodník proti těmto pravidlům vyzkoušet, je zaznamenat tři riziková volání do jednoho enhanced metafile přes TMetafileCanvas. Kresba níže zaznamená křivky s dutým štětcem a pak záměrně vybere plný žlutý štětec pro polylinii: správný převodník musí ten štětec u polylinie ignorovat, takže jakákoli žlutá ve výstupním PDF je bug. PolyDraw nemá wrapper na TCanvas, takže se volá přes Windows API s handle canvasu, s typovými bajty 3 a 5, aby se zavírací příznak procvičil
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Uzavřený čtverec (3 = LINETO + CLOSEFIGURE), pak uzavřený Bezier
// útvar, jehož poslední řídicí trojice končí 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; // pro křivky jen obrysy
// Bod 0 je start; 1..3 a 4..6 jsou dva kubické segmenty
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));
// Otevřený tvar V s vybraným plným štětcem: jen tah, nikdy se
// neuzavře do žlutého trojúhelníku
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // ukončí záznam
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Protože se tyhle souřadnice vejdou do SmallIntu, GDI je bude normálně ukládat v 16bitových variantách. K 32bitovým handlerům se dostanete jen s producentem, který je umí zapsat, nebo se záznamy postavenými ručně. Ručně poskládané soubory přinášejí vlastní past: VCL TMetafile.LoadFromStream bere stream jako EMF jen tehdy, když zbývající délka je přísně větší než 108bajtový TEnhMetaHeader. Minimální ručně psaný EMF s krátkou hlavičkou, nebo prázdný soubor dlouhý přesně 108 bajtů, se bere za WMF a je odmítnut hlášením „Metafile is not valid“. Vždy zapište plnou 108bajtovou hlavičku včetně extenzních polí, než přijdou vaše testovací záznamy
Import EMF do PDF přes PDFlibPas
PDFlibPas importuje EMF přes ImportEMFFromFile nebo ImportEMFFromStream, které vrací nenulové image ID při úspěchu a 0 při selhání. GeneralOptions = 0 ponechá vektorovou cestu, o které tenhle článek je; 1 místo toho rasterizuje metafile do bitmapy. FontOptions = 1 přidá písma metafile jako neembeddovaná TrueType písma. Streamová varianta přetočí stream na pozici 0 před načtením, takže pošlete stream, který obsahuje jen 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); // počátek vlevo nahoře pro DrawImage
PDF.SetMeasurementUnits(0); // body (points)
// FontOptions 1 = přidá písma jako neembeddovaná TrueType
// GeneralOptions 0 = vektorový import, 1 = bitmapa
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// Pro EMF jsou ImageWidth / ImageHeight rozměry rámu v bodech
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// Stránka jen vyvolá importovaný formulář: 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;
Vektorový import EMF se stane form XObjectem, takže GetPageContentToString vrátí jen sekvenci uložení, transformace, Do a obnovení. Operátory m, l, c, h a S vyrobené ze záznamů Poly* bydlí ve streamu form XObjectu, který je komprimovaný. Pro audit je dekomprimujte v PDF object inspektoru a přečtěte form stream: u testovacího souboru výš byste měli vidět polylinii končící S bez žádného h před ní, h u každého zavíracího příznaku v útvarech PolyDraw a žádné f ani B na žádném z těchto subpathů. DrawImage zároveň škáluje importovaný EMF uniformně podle menšího z Width a Height, takže kresba drží poměr stran, i když rámeček, který pošlete, neodpovídá
Pro targety Free Pascalu se podívejte na jak se vektorový EMF importer PDFlibPas sestavuje pod Free Pascalem; sémantika záznamů je stejná všude, kde se importer zkompiluje
Jak má parser EMF zacházet s počty bodů ze souboru?
Parser EMF by měl brát každý počet bodů jako nedůvěryhodný vstup a zkontrolovat ho proti velikosti záznamu, než zkopíruje jediný bod. EnumEnhMetaFile garantuje jen to, že nSize každého záznamu zůstane uvnitř souboru. Nekontroluje, že cptl souhlasí s nSize, takže handler, který kopíruje cptl bodů přes Move, si přečte následující záznamy, nebo zajde za konec metafile, když je počet zfalšovaný nebo poškozený. Od v3.539.39 kontroluje PDFlibPas pevnou hlavičku plus počet krát bajty na bod proti nSize u PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo a Polygon v obou šířkách, s jedním extra bajtem na bod u typových bajtů PolyDraw. U záznamů PolyPoly se navíc počty na útvar musí sečíst na nejvýše deklarovaný celkový součet a útvary s nulou bodů se přeskočí
Ta samá kontrola je krátká na to, abyste si ji zkopírovali do vlastního parseru. Tahle verze validuje 32bitový EMR_POLYPOLYLINE a vrací ukazatel na jeho skutečné pole bodů:
uses
Winapi.Windows;
// Vrací nil, dokud záznam doopravdy neobsahuje body, které deklaruje.
// Body začínají za polem počtů: 32 + nPolys * 4 bajtů od začátku, ne na
// aptl[0], které RTL deklaruje jako jednoprvkové pole
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; // zfalšovaný nebo useknutý počet
Total := 0;
Count := @P^.aPolyCounts[0]; // chůze přes ukazatel: [0..0] spustí range check
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // útvary si nárokují víc bodů, než existuje
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Test, zda se body vejdou, běží první, takže pole počtů je před smyčkou známé jako uvnitř záznamu. Aritmetika je v Int64, protože nPolys * 4 a cptl * 8 spočítané v 32 bitech se můžou přetáhnout přes mez a projít porovnáním
Přehled: pravidla EMF Poly* pro převod EMF do PDF
EMR_POLYBEZIER: bod 0 je startovní bod; sdružujte od bodu 1 po trojicích; opraveno u 32bitového záznamu ve v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: otevřené útvary, tah sS, nikdyh,faniB, protože PDF výplň zavírá otevřené subpathyEMR_POLYLINETO: začíná v aktuální pozici, zůstává otevřený, aktualizuje aktuální pozici, nikdy nesahá na stav pera- 32bitový
EMR_POLYPOLYLINE: body začínají na bajtu32 + nPolys * 4, ne naaptl[0] EMR_POLYDRAW: zamaskujtePT_CLOSEFIGUREpřed rozesláním, zavřete po dokončeném segmentu, začněte v aktuální pozici, když první bod neníPT_MOVETO- Výchozí stav device contextu je
BLACK_PENplusWHITE_BRUSH; v3.539.43 a novější ho respektují - Uvnitř
BeginPath/EndPathsi každá polylinie otevírá vlastní subpath a dokud se závorka nepoužije, nic se nevykreslí - Validujte každý
cptl/cptsprotinSizev 64bitové aritmetice, než zkopírujete body - Ručně stavěné testovací EMF potřebují plnou 108bajtovou hlavičku, jinak je
TMetafile.LoadFromStreampřečte jako WMF
Pokud vaše reporty jdou přes jinou komponentu, sémantika záznamů je stejná; vektorový import EMF a WMF v HotPDF popisuje, jak ta komponenta mění gradientní a hatch štětce na PDF patterny, a vektorová grafika, shadery a gradienty v PDFlibPas popisuje kreslení stejných tvarů přímo přes API knihovny místo přes metafile
PDFlibPas ve verzi v3.539.43 a novější obsahuje všechna výše uvedená pravidla. Detaily a trial ke stažení najdete na produktové stránce PDFlibPas Delphi PDF library