PDFlibPas, losLabs PDF-bibliotek för Delphi, gör om EMF:s Poly*-poster till PDF-paths genom att följa varje postdefinition i [MS-EMF]: en 32-bitars EMR_POLYBEZIER börjar på punkt 0, polylinjer förblir öppna och ritas bara med stroke, PT_CLOSEFIGURE i EMR_POLYDRAW är en flagga, och varje punktantal kontrolleras mot poststorleken. Reglerna kom på plats över v3.539.39, v3.539.41 och v3.539.43. Innan dess kunde en rapportgraf komma ut ur ImportEMFFromFile med en fylld kil där trendlinjen skulle vara, en Bezier-kurva böjd mot fel kontrollpunkt, eller en sluten kontur som saknade sin sista sida. Inget av detta gav något fel, och reglerna gäller vilken Delphi-konverterare från EMF till PDF eller GDI-postparser som helst
Varför går EMF:s Poly*-poster fel vid PDF-konvertering?
EMF:s Poly*-poster går fel för att var och en bär en del av sin betydelse utanför sina punkter: om figuren är öppen, om den börjar på den aktuella positionen, vilken penna och pensel som gäller och var i posten punkterna börjar. En enhanced metafile är en inspelning av GDI-anrop mot en device context, så en konverterare måste spela upp både det tillståndet och koordinaterna. PDF har ingen device context. Den har en path, en aktuell punkt i den pathen och en ritoperator som bestämmer mellan stroke (S), fill (f) och båda (B). Varje missmatchning mellan de två modellerna blir en tyst renderingsskillnad
Poly*-familjen finns också i två bredder. Varje 32-bitarspost som EMR_POLYLINE har en 16-bitars tvilling som EMR_POLYLINE16 som lagrar punkter som SmallInt-par. GDI spelar vanligen in den kompakta formen när varje koordinat ryms, så en konverterares 32-bitarsrutiner kan ligga fel i åratal medan vardagliga testritningar aldrig når dit. Den snabbaste granskningen är att skicka samma punkter genom båda posterna och jämföra de resulterande pathsen. Posterna som tas upp här ligger alla i [MS-EMF]:s grupp av ritningsposter (2.3.5 Drawing Record Types)
| Post | Börjar på | Stängd? | Aktuell position |
|---|---|---|---|
EMR_POLYBEZIER | Punkt 0 | Nej | Används inte, uppdateras inte |
EMR_POLYLINE | Punkt 0 | Nej (endast penna) | Används inte, uppdateras inte |
EMR_POLYLINETO | Aktuell position | Nej (endast penna) | Används och uppdateras |
EMR_POLYPOLYLINE | Första punkten i varje polylinje | Nej (endast penna) | Används inte, uppdateras inte |
EMR_POLYDRAW | Första PT_MOVETO, eller aktuell position | Bara där PT_CLOSEFIGURE är satt | Används och uppdateras |
Var börjar en EMR_POLYBEZIER-kurva egentligen?
En EMR_POLYBEZIER-kurva börjar på punkt 0, och bara punkterna från index 1 och framåt grupperas tre i taget som kontrollpunkt, kontrollpunkt, slutpunkt. En post med 7 punkter ritar alltså två kubiska segment: 0 är startpunkten, 1 till 3 bildar första segmentet, 4 till 6 bildar det andra. 16-bitarsrutinen i PDFlibPas gjorde redan detta. 32-bitarsrutinen började gruppera på punkt 0, så startpunkten åts upp som första kontrollpunkt och varje senare segment försköts med ett. Kurvan renderades fortfarande, bara fel kurva. Sedan v3.539.41 öppnar båda bredderna pathen med m på punkt 0 och emitterar en c per komplett trippel därefter
För din egen parser: ett antal som inte är 1 plus en multipel av 3 är felformat, och de avslutande punkterna bör ignoreras i stället för att sys in i en kurva
PolyDraw: PT_CLOSEFIGURE är en flagga, inte en punkttyp
I EMR_POLYDRAW är PT_CLOSEFIGURE (värde 1) en bit som kombineras med PT_LINETO (2) eller PT_BEZIERTO (4), så ett giltigt typbyte kan vara 3 eller 5. Punkttypen är byten med den biten utmaskerad, och flaggan betyder att figuren stängs efter segmentet som slutar på den punkten. Den gamla PDFlibPas-rutinen matchade byten mot enskilda värden i en case-sats, så punkter av typ 3 och 5 matchade ingenting och hoppades över helt. En rektangel ritad med PolyDraw tappade sin avslutande sida, och en Bezier-trippel vars sista punkt bar flaggan tappade den punkten, vilket knuffade alla senare trippelar ur fas
Sedan v3.539.39 läses typen som Types[i] and not PT_CLOSEFIGURE, och stängningen emitteras bara efter ett helt segment: efter linjen för en stängd PT_LINETO, och efter den tredje punkten i en Bezier-grupp. En felformad fil som sätter flaggan på första eller andra punkten i en trippel stänger inte figuren i förtid. Två relaterade fixar kom i samma release:
- Varje
PT_MOVETOi 16-bitarsEMR_POLYDRAW16startade om hela pathen, så en post med tre figurer behöll bara den sista; nu startar första flytten pathen och senare flyttar öppnar subpaths - En PolyDraw-post som inte börjar med
PT_MOVETObörjar på den aktuella positionen, som postdefinitionen säger, i stället för att skriva enl- ellerc-operator utan föregåendem
Varför får en EMF-polylinje aldrig fyllas i PDF?
En EMF-polylinje får aldrig fyllas för att EMR_POLYLINE och EMR_POLYPOLYLINE är öppna figurer som ritas bara med pennan, och att fylla en öppen path i PDF stänger den implicit. ISO 32000-1 §8.5.3 säger att fill-operatorerna stänger varje öppen subpath före den ritas. En konverterare som emitterar B eller f för en trepunkters polylinje målar alltså en fylld triangel i aktuell penselfärg: den fyllda kilen under en grafs trendlinje. Före v3.539.41 fyllde PDFlibPas båda polylinjebredderna med penseln, och 32-bitarsposten stängdes dessutom explicit. Idag slutar båda bredder med enbart stroke, och GDI:s åtskillnad bevaras: Polygon stänger och fyller, Polyline gör det aldrig
PolylineTo börjar på den aktuella positionen
EMR_POLYLINETO ritar från den aktuella positionen genom varje punkt i posten, förblir öppen och lämnar den aktuella positionen på sista punkten. Gamla rutinen innehöll dessutom ett specialfall som stängde av pennan när de två första punkterna delade y-koordinat, och inget slog någonsin på den igen, så varje senare post i filen tappade sin kontur. Pennläget hör till EMR_SELECTOBJECT och EMR_CREATEPEN; en rutin för ritningsposter har inget att göra där. Det specialfallet togs bort i v3.539.41, och postens enpunktsform läser inte längre förbi sina egna punkter (fixat i v3.539.39)
PolyPolyline-punkter börjar efter antalsarrayen
32-bitars EMR_POLYPOLYLINE lagrar nPolys antal och sedan cptl punkter, och punkterna börjar på byteoffset 32 + nPolys * 4. Fällan ligger i RTL: uniten Windows deklarerar TEMRPolyPolyline med aPolyCounts och aptl som arrayer med ett element, så aptl[0] är första punkten bara när nPolys är 1. Kod som indexerar aptl direkt läser antalsvärden som koordinater för varje post med flera linjer. Den gamla PDFlibPas-rutinen storleksatte dessutom sin gränskontroll efter den felaktiga layouten, så giltiga flerlinjeposter avslogs och enkelinjeposter ritade ingenting. Sedan v3.539.41 hittar PDFlibPas punktarrayen från beräknad offset, som dess PolyPolygon-rutin alltid gjort, och ritar varje polylinje som sin egen öppna subpath med en stroke på slutet. I v3.539.43 fick 16-bitars tvillingen samma behandling; den hade ritat segment för segment, vilket bröt linjefogar och ignorerade en vald NULL_PEN
Standardpennan och standardpenseln, samt path-parenteser
Två tillståndsregler rundar av polylinjefixarna i v3.539.43:
- En färsk GDI device context har redan
BLACK_PENochWHITE_BRUSHvalda, så en metafil som ritar utan någonEMR_SELECTOBJECTritar ändå svarta konturer; konverteraren brukade starta utan penna och utan fyllnad och skrevn(avsluta path, måla inget) för sådana poster - Inuti en
BeginPath/EndPath-parentes varken använder eller uppdaterar enPolylineden aktuella positionen, så den måste öppna en ny subpath på sin första punkt i stället för att ansluta till föregående figur, och inget får målas förrän parentesen har strokeats eller fyllts
Bygga en EMF-testfil med TMetafileCanvas
Det snabbaste sättet att testa en konverterare mot de här reglerna är att spela in de tre riskabla anropen i en och samma enhanced metafile med TMetafileCanvas. Ritningen nedan spelar in kurvorna med en ihålig pensel och väljer sedan med flit en heldragen gul pensel för polylinjen: en korrekt konverterare måste ignorera den penseln för polylinjen, så gult i utdata-PDF:en är en bugg. PolyDraw har inget TCanvas-omslag, så den anropas via Windows API med canvas-hantelet, med typbytena 3 och 5 för att träna stängflaggan
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// En sluten kvadrat (3 = LINETO + CLOSEFIGURE), sedan en sluten Bezier
// figur vars sista kontrolltrippel slutar med 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; // endast konturer för kurvorna
// Punkt 0 är starten; 1..3 och 4..6 är två kubiska segment
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));
// Öppen V-form med heldragen pensel vald: streckas, stängs aldrig
// till en gul triangel
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // avslutar inspelningen
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Eftersom de här koordinaterna ryms i en SmallInt lagrar GDI normalt 16-bitarsvarianterna. För att nå 32-bitarsrutinerna behöver du en producent som skriver dem, eller poster du bygger för hand. Handbyggda filer har en egen fälla: VCL:s TMetafile.LoadFromStream behandlar strömmen som en EMF bara när återstående längd är strikt större än TEnhMetaHeader:s 108 byte. En minimal handskriven EMF med kort header, eller en tom som är exakt 108 byte lång, tas för en WMF och avslås med "Metafile is not valid". Skriv alltid den fullständiga 108-byte-headern, inklusive tilläggsfälten, före dina testposter
Importera EMF:en till en PDF med PDFlibPas
PDFlibPas importerar en EMF med ImportEMFFromFile eller ImportEMFFromStream, som returnerar ett bild-ID skilt från noll vid lyckat resultat och 0 vid misslyckande. GeneralOptions = 0 behåller de vektorpaths den här artikeln handlar om; 1 rastrerar i stället metafilen till en bitmapp. FontOptions = 1 lägger till metafilens fonter som icke inbäddade TrueType-fonts. Strömvarianten spolar tillbaka strömmen till position 0 före inläsning, så skicka en ström som bara innehåller metafilen
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); // ursprung i övre vänster för DrawImage
PDF.SetMeasurementUnits(0); // punkter
// FontOptions 1 = lägg till fonter som icke inbäddade TrueType
// GeneralOptions 0 = vektorimport, 1 = bitmapp
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// För en EMF är ImageWidth / ImageHeight ramstorleken i punkter
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// Sidan anropar bara den importerade formen: 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;
En vektor-EMF-import blir en form XObject, så GetPageContentToString returnerar bara sekvensen save, transform, Do och restore. Operatorerna m, l, c, h och S som produceras ur Poly*-posterna bor i form XObject-strömmen, som är komprimerad. För att granska dem, dekomprimera den sparade filen i en PDF-objektinspektör och läs form-strömmen: för testfilen ovan ska du se polylinjen sluta med S utan något h före, ett h vid varje stängflagga i PolyDraw-formerna, och ingen f eller B på någon av dessa subpaths. DrawImage skalar dessutom en importerad EMF enhetligt med det minsta av Width och Height, så ritningen behåller sina proportioner även om rutan du skickar in inte matchar dem
För Free Pascal-mål, se hur PDFlibPas EMF-vektorimporteraren byggs under Free Pascal; postsemantiken är densamma oavsett var importeren kompilerar
Hur ska en EMF-parser behandla punktantal från filen?
En EMF-parser ska behandla varje punktantal som opålitlig indata och kontrollera det mot poststorleken innan en enda punkt kopieras. EnumEnhMetaFile garanterar bara att varje posts nSize stannar inne i filen. Den kontrollerar inte att cptl stämmer med nSize, så en rutin som kopierar cptl punkter med Move läser följande poster, eller förbi metafilens slut, när talet är förfalskat eller skadat. Sedan v3.539.39 kontrollerar PDFlibPas fast header plus antal gånger byte per punkt mot nSize för PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo och Polygon i båda bredder, med en extra byte per punkt för PolyDraw:s typbyten. För PolyPoly-posterna måste antalen per figur dessutom summera till högst det deklarerade totala, och figurer med noll punkter hoppas över
Samma kontroll är kort nog att kopiera in i din egen parser. Den här versionen validerar en 32-bitars EMR_POLYPOLYLINE och returnerar en pekare till dess verkliga punktarray:
uses
Winapi.Windows;
// Returnerar nil om inte posten verkligen håller de punkter den deklarerar.
// Punkterna börjar efter antalsarrayen: 32 + nPolys * 4 byte in, inte på
// aptl[0], som RTL:n deklarerar som en array med ett element
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; // förfalskat eller avklippt antal
Total := 0;
Count := @P^.aPolyCounts[0]; // stega med pekare: [0..0] triggar range checks
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // figurerna utger fler punkter än som finns
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Testet att punkterna ryms körs först, så antalsarrayen är känd att ligga inne i posten innan loopen stegar den. Aritmetiken är Int64 för att nPolys * 4 och cptl * 8 beräknade i 32 bitar kan runna över och passera jämförelsen
Snabbreferens: EMF Poly*-regler för konvertering från EMF till PDF
EMR_POLYBEZIER: punkt 0 är startpunkten; gruppera från punkt 1, tre i taget; fixat för 32-bitarsposten i v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: öppna figurer, stroke medS, aldrigh,fellerB, för PDF-fill stänger öppna subpathsEMR_POLYLINETO: börja på den aktuella positionen, förbli öppen, uppdatera den aktuella positionen, rör aldrig pennläget- 32-bitars
EMR_POLYPOLYLINE: punkterna börjar på byte32 + nPolys * 4, inte påaptl[0] EMR_POLYDRAW: maska bortPT_CLOSEFIGUREföre dispatch, stäng efter det färdiga segmentet, börja på den aktuella positionen när första punkten inte ärPT_MOVETO- Standardläge för device context är
BLACK_PENplusWHITE_BRUSH; v3.539.43 och senare respekterar det - Inuti
BeginPath/EndPathöppnar varje polylinje sin egen subpath och inget målas förrän parentesen används - Validera varje
cptl/cptsmotnSizei 64-bitars aritmetik innan punkterna kopieras - Handbyggda test-EMF:er behöver den fullständiga 108-byte-headern, annars läser
TMetafile.LoadFromStreamdem som WMF
Om dina rapporter går genom en annan komponent gäller samma postsemantik; HotPDF EMF- och WMF-vektorimport tar upp hur den komponenten gör om gradient- och hatchpenslar till PDF-mönster, och vektorgrafik, shaders och gradienter i PDFlibPas tar upp att rita samma former direkt med biblioteks-API:et i stället för via en metafil
PDFlibPas v3.539.43 eller senare innehåller alla regler ovan. Detaljer och testnedladdningar finns på produktsidan för PDFlibPas Delphi PDF library