PDFlibPas, de losLab PDF-library voor Delphi, zet de EMF-Poly*-records om naar PDF-paden door elke recorddefinitie in [MS-EMF] te volgen: een 32-bit EMR_POLYBEZIER begint bij punt 0, polylines blijven open en krijgen uitsluitend een stroke, PT_CLOSEFIGURE in EMR_POLYDRAW is een vlag, en elk puntaantal wordt tegen de recordgrootte gecontroleerd. Die regels zijn verschenen in v3.539.39, v3.539.41 en v3.539.43. Daarvoor kon een grafiek in een rapport uit ImportEMFFromFile rollen met een gevulde wig waar een trendlijn hoorde, een Bezier-curve die naar het verkeerde control point boog, of een gesloten omtrek waarvan de laatste zijde ontbrak. Geen van die dingen gaf een foutmelding, en de regels gelden voor elke Delphi EMF-naar-PDF-converter en elke GDI-recordparser
Waarom gaan EMF-Poly*-records mis bij PDF-conversie?
EMF-Poly*-records gaan mis omdat elk record een deel van zijn betekenis buiten zijn punten vervoert: of de figuur open is, of hij bij de huidige positie begint, welke pen en brush gelden, en waar in het record de punten beginnen. Een enhanced metafile is een opname van GDI-aanroepen tegen een device context, dus een converter moet die device-context-status net zo goed meespelen als de coördinaten. PDF heeft geen device context. Het heeft een path, een current point daarin en een tekenoperator die kiest tussen stroke (S), fill (f) en beide (B). Elke mismatch tussen de twee modellen wordt een stil weergaveverschil
De Poly*-familie bestaat bovendien in twee breedtes. Elk 32-bit record zoals EMR_POLYLINE heeft een 16-bit tweeling zoals EMR_POLYLINE16 die punten opslaat als SmallInt-paren. GDI schrijft meestal de compacte vorm weg zodra elke coördinaat past, dus de 32-bit handlers van een converter kunnen jarenlang fout blijven terwijl alledaagse testtekeningen ze nooit bereiken. De snelste controle is dezelfde punten door beide records heen te halen en de resulterende paden te vergelijken. De records hier behandeld zitten allemaal in de groep van tekenrecords van [MS-EMF] (2.3.5 Drawing Record Types)
| Record | Begint bij | Gesloten? | Huidige positie |
|---|---|---|---|
EMR_POLYBEZIER | Punt 0 | Nee | Niet gebruikt, niet bijgewerkt |
EMR_POLYLINE | Punt 0 | Nee (alleen pen) | Niet gebruikt, niet bijgewerkt |
EMR_POLYLINETO | Huidige positie | Nee (alleen pen) | Gebruikt en bijgewerkt |
EMR_POLYPOLYLINE | Eerste punt van elke polyline | Nee (alleen pen) | Niet gebruikt, niet bijgewerkt |
EMR_POLYDRAW | Eerste PT_MOVETO, of huidige positie | Alleen waar PT_CLOSEFIGURE is gezet | Gebruikt en bijgewerkt |
Waar begint een EMR_POLYBEZIER-curve eigenlijk?
Een EMR_POLYBEZIER-curve begint bij punt 0, en alleen de punten vanaf index 1 worden in drietallen gegroepeerd als control point, control point, eindpunt. Een record met 7 punten tekent dus twee cubic segmenten: 0 is de start, 1 tot 3 vormen het eerste segment, 4 tot 6 het tweede. De 16-bit handler in PDFlibPas deed dat al. De 32-bit handler begon pas te groeperen bij punt 0, zodat het startpunt als eerste control point werd opgebruikt en elk later segment één positie opschoof. De curve werd nog steeds gerenderd, alleen de verkeerde. Sinds v3.539.41 openen beide breedtes het path met m op punt 0 en geven ze één c per volledig drietal daarna uit
Voor uw eigen parser: een aantal dat niet gelijk is aan 1 plus een veelvoud van 3 is misvormd, en de punten die overblijven hoort u te negeren in plaats van ze tot een curve aan elkaar te plakken
PolyDraw: PT_CLOSEFIGURE is een vlag, geen punttype
In EMR_POLYDRAW is PT_CLOSEFIGURE (waarde 1) een bit dat wordt gecombineerd met PT_LINETO (2) of PT_BEZIERTO (4), dus een geldig typebyte kan 3 of 5 zijn. Het punttype is de byte met dat bit uitgemaskeerd, en de vlag betekent: sluit de figuur af na het segment dat op dit punt eindigt. De oude PDFlibPas-handler vergeleek de byte met losse waarden in een case-statement, dus punten van type 3 en 5 matchten nergens en werden volledig overgeslagen. Een rechthoek die met PolyDraw werd getekend verloor zijn sluitzijde, en een Bezier-drietal waarvan het laatste punt de vlag droeg verloor dat punt, wat elk later drietal uit de pas liet lopen
Sinds v3.539.39 wordt het type gelezen als Types[i] and not PT_CLOSEFIGURE, en de sluiting wordt pas na een volledig segment gegeven: na de lijn bij een gesloten PT_LINETO, en na het derde punt van een Bezier-groep. Een misvormd bestand dat de vlag op het eerste of tweede punt van een drietal zet, sluit de figuur niet voortijdig. Twee verwante fixes zijn in dezelfde release verschenen:
- Elke
PT_MOVETOin de 16-bitEMR_POLYDRAW16herstartte het hele path, dus een record met drie figuren hield alleen de laatste over; nu start de eerste move het path en openen latere moves subpaths - Een PolyDraw-record dat niet met
PT_MOVETObegint, start op de huidige positie, zoals de recorddefinitie zegt, in plaats van eenl- ofc-operator te schrijven zonder voorafgaandem
Waarom mag een EMF-polyline nooit gevuld worden in PDF?
Een EMF-polyline mag nooit gevuld worden, want EMR_POLYLINE en EMR_POLYPOLYLINE zijn open figuren die alleen met de pen worden getekend, en het vullen van een open path in PDF sluit het impliciet. ISO 32000-1 §8.5.3 stelt dat de fill-operators elke open subpath sluiten voordat ze die tekenen. Een converter die B of f geeft voor een polyline van drie punten, tekent dus een gevulde driehoek in de huidige brushkleur: de gevulde wig onder een trendlijn in een grafiek. Vóór v3.539.41 vulde PDFlibPas beide polyline-breedtes met de brush, en het 32-bit record werd bovendien expliciet gesloten. Tegenwoordig eindigen beide breedtes in een stroke en het GDI-onderscheid blijft behouden: Polygon sluit en vult, Polyline doet dat nooit
PolylineTo begint op de huidige positie
EMR_POLYLINETO tekent vanaf de huidige positie langs elk punt in het record, blijft open en laat de huidige positie op het laatste punt achter. De oude handler bevatte bovendien een special case die de pen uitzette zodra de eerste twee punten een y-coördinaat deelden, en niets zette hem ooit weer aan, dus elk later record in het bestand verloor zijn omtrek. Pen-status hoort bij EMR_SELECTOBJECT en EMR_CREATEPEN; een tekenrecord-handler heeft niets te zoeken bij het veranderen ervan. Die special case is in v3.539.41 verwijderd, en de vorm met één punt leest niet langer voorbij zijn eigen punten (opgelost in v3.539.39)
De punten van PolyPolyline beginnen na de counts-array
Het 32-bit EMR_POLYPOLYLINE slaat nPolys aantallen en daarna cptl punten op, en de punten beginnen op byte-offset 32 + nPolys * 4. De valkuil zit in de RTL: de unit Windows declareert TEMRPolyPolyline met aPolyCounts en aptl als arrays met één element, dus aptl[0] is het eerste punt alleen als nPolys 1 is. Code die aptl direct indexeert, leest bij elk multi-line record aantalwaarden als coördinaten. De oude PDFlibPas-handler baseerde bovendien zijn bounds check op die verkeerde layout, dus geldige multi-line records werden geweigerd en single-line records tekenden niets. Sinds v3.539.41 lokaliseert PDFlibPas de puntarray vanaf de berekende offset, zoals zijn PolyPolygon-handler altijd al deed, en tekent hij elke polyline als eigen open subpath met één stroke op het eind. In v3.539.43 kreeg de 16-bit tweeling dezelfde behandeling; die tekende segment voor segment, wat line joins brak en een geselecteerde NULL_PEN negeerde
De standaard pen en brush, en path-brackets
Twee statusregels maken de polyline-fixes in v3.539.43 compleet:
- Een verse GDI device context heeft al
BLACK_PENenWHITE_BRUSHgeselecteerd, dus een metafile die zonder enigeEMR_SELECTOBJECTtekent, tekent toch zwarte omtrekken; de converter begon voorheen zonder pen en zonder fill en schreefn(end path, niets tekenen) voor zulke records - Binnen een
BeginPath/EndPath-bracket gebruikt eenPolylinede huidige positie niet en werkt ze niet bij, dus ze moet op haar eerste punt een nieuwe subpath openen in plaats van door te lopen naar de vorige figuur, en er mag niets worden getekend totdat de bracket is gestroked of gevuld
Een EMF-testbestand bouwen met TMetafileCanvas
De snelste manier om een converter aan deze regels te toetsen is de drie risicovolle aanroepen in één enhanced metafile op te nemen met TMetafileCanvas. De tekening hieronder neemt de curves op met een holle brush en selecteert daarna opzettelijk een effen gele brush voor de polyline: een correcte converter moet die brush voor de polyline negeren, dus elk geel in de uitgaande PDF is een bug. PolyDraw heeft geen TCanvas-wrapper, dus hij wordt via de Windows API met de canvas-handle aangeroepen, met typebytes 3 en 5 om de close-vlag te testen
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Een gesloten vierkant (3 = LINETO + CLOSEFIGURE), daarna een gesloten
// Bezier-figuur waarvan het laatste control-drietal eindigt op 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; // alleen omtrekken voor de curves
// Punt 0 is de start; 1..3 en 4..6 zijn twee cubic segmenten
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));
// Open V-vorm met een effen brush geselecteerd: gestroked, nooit
// gesloten tot een gele driehoek
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // beëindigt de opname
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Omdat deze coördinaten in een SmallInt passen, slaat GDI normaal de 16-bit varianten op. Om bij de 32-bit handlers te komen heeft u een producent nodig die ze schrijft, of records die u met de hand bouwt. Handgebouwde bestanden brengen hun eigen valkuil mee: VCL TMetafile.LoadFromStream behandelt de stream pas als EMF wanneer de resterende lengte strikt groter is dan de 108 bytes van TEnhMetaHeader. Een minimaal handgeschreven EMF met een korte header, of een leeg exemplaar van precies 108 bytes, wordt voor een WMF aangezien en geweigerd met "Metafile is not valid". Schrijf altijd de volledige header van 108 bytes, inclusief de extensievelden, vóór uw testrecords
De EMF in een PDF importeren met PDFlibPas
PDFlibPas importeert een EMF met ImportEMFFromFile of ImportEMFFromStream, die bij succes een niet-nul image ID teruggeven en bij falen 0. GeneralOptions = 0 houdt het vectorpad vast waar dit artikel over gaat; 1 rasteriseert de metafile in plaats daarvan naar een bitmap. FontOptions = 1 voegt de metafile-fonts toe als niet-ingebedde TrueType-fonts. De streamvariant spoelt de stream eerst terug naar positie 0 vóór het laden, dus geef een stream door die alleen de metafile bevat
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); // linkerbovenhoek als origine voor DrawImage
PDF.SetMeasurementUnits(0); // punten
// FontOptions 1 = fonts toevoegen als niet-ingebedde TrueType
// GeneralOptions 0 = vectorimport, 1 = bitmap
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// Bij een EMF zijn ImageWidth / ImageHeight de framemaat in punten
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// De pagina roept alleen de geïmporteerde form aan: 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;
Een vector-EMF-import wordt een form XObject, dus GetPageContentToString geeft alleen de save-, transform-, Do- en restore-reeks terug. De operators m, l, c, h en S die uit de Poly*-records voortkomen, zitten in de form-XObject-stream, die gecomprimeerd is. Om ze te controleren, decomprimeert u het opgeslagen bestand in een PDF object inspector en leest de form-stream: in het testbestand hierboven hoort u de polyline te zien eindigen in S zonder h ervoor, een h bij elke close-vlag in de PolyDraw-figuren, en geen f of B op een van deze subpaths. DrawImage schaalt een geïmporteerde EMF ook uniform met het kleinste van Width en Height, dus de tekening behoudt zijn beeldverhouding zelfs als het kader dat u doorgeeft daar niet aan voldoet
Voor Free Pascal-doelen, zie hoe de PDFlibPas EMF-vectorimporteur onder Free Pascal bouwt; de record-semantiek is overal gelijk waar de importeur compileert
Hoe moet een EMF-parser puntaantallen uit het bestand behandelen?
Een EMF-parser moet elk puntaantal als niet-vertrouwde invoer behandelen en het tegen de recordgrootte controleren voordat er ook maar één punt wordt gekopieerd. EnumEnhMetaFile garandeert alleen dat elke record-nSize binnen het bestand blijft. Hij controleert niet of cptl met nSize klopt, dus een handler die cptl punten met Move kopieert, leest de volgende records of voorbij het einde van de metafile zodra het aantal is vervalst of corrupt. Sinds v3.539.39 controleert PDFlibPas vaste header plus aantal keer bytes per punt tegen nSize voor PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo en Polygon in beide breedtes, met één extra byte per punt voor PolyDraw-typebytes. Voor de PolyPoly-records moeten de aantallen per figuur bovendien samen niet meer uitkomen dan het opgegeven totaal, en figuren met nul punten worden overgeslagen
Dezelfde controle is kort genoeg om in uw eigen parser over te nemen. Deze versie valideert een 32-bit EMR_POLYPOLYLINE en geeft een pointer naar zijn echte puntarray terug:
uses
Winapi.Windows;
// Geeft nil terug tenzij het record echt de punten bevat die het aangeeft.
// De punten beginnen na de counts-array: 32 + nPolys * 4 bytes naar binnen,
// niet op aptl[0], dat de RTL als array met één element declareert
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; // vervalst of afgekapt aantal
Total := 0;
Count := @P^.aPolyCounts[0]; // met de pointer lopen: [0..0] triggert range checks
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // figuren claimen meer punten dan er zijn
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
De punten-passen-test draait eerst, dus de counts-array ligt bekend binnen het record voordat de lus hem doorloopt. De rekenkunde is Int64 omdat nPolys * 4 en cptl * 8 in 32 bits berekend kunnen omslaan en de vergelijking toch laten passeren
Snelnaslag: EMF-Poly*-regels voor EMF-naar-PDF-conversie
EMR_POLYBEZIER: punt 0 is het startpunt; groepeer vanaf punt 1 in drietallen; opgelost voor het 32-bit record in v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: open figuren, stroke metS, nooith,fofB, want PDF-fill sluit open subpathsEMR_POLYLINETO: begin op de huidige positie, blijf open, werk de huidige positie bij, raak de pen-status nooit aan- 32-bit
EMR_POLYPOLYLINE: de punten beginnen op byte32 + nPolys * 4, niet opaptl[0] EMR_POLYDRAW: maskeerPT_CLOSEFIGUREaf vóór de dispatch, sluit pas na het voltooide segment, begin op de huidige positie als het eerste punt geenPT_MOVETOis- De default-status van de device context is
BLACK_PENplusWHITE_BRUSH; v3.539.43 en later houden die aan - Binnen
BeginPath/EndPathopent elke polyline zijn eigen subpath en er wordt niets getekend totdat de bracket wordt gebruikt - Valideer elke
cptl/cptstegennSizein 64-bit rekenkunde voordat u punten kopieert - Met de hand gebouwde test-EMF's hebben de volledige header van 108 bytes nodig, anders leest
TMetafile.LoadFromStreamze als WMF
Als uw rapporten via een andere component lopen, geldt dezelfde record-semantiek; HotPDF EMF- en WMF-vectorimport beschrijft hoe die component gradient- en hatch-brushes omzet in PDF-patterns, en vectorkunst, shaders en gradients in PDFlibPas beschrijft hoe u dezelfde vormen rechtstreeks met de library-API tekent in plaats van via een metafile
PDFlibPas v3.539.43 of later bevat alle bovenstaande regels. Details en proefdownloads staan op de productpagina van de PDFlibPas Delphi PDF-library