PDFlibPas, losLabs PDF-bibliotek til Delphi, konverterer EMF'ens Poly*-records til PDF-stier ved at følge hver record-definition i [MS-EMF]: en 32-bit EMR_POLYBEZIER starter ved punkt 0, polylinjer forbliver åbne og tegnes kun med stroke, PT_CLOSEFIGURE i EMR_POLYDRAW er et flag, og hvert punktantal tjekkes mod record-størrelsen. Reglerne landede fordelt på v3.539.39, v3.539.41 og v3.539.43. Før dem kunne en rapportsgrafik komme ud af ImportEMFFromFile med en udfyldt kile, hvor en trendlinje skulle have været, en Bezier-kurve bøjet mod det forkerte kontrolpunkt eller en lukket omrids, der manglede sin sidste side. Ingen af delene rejste en fejl, og reglerne gælder for enhver Delphi EMF-til-PDF-konverterer eller GDI-record-parser
Hvorfor går EMF'ens Poly*-records galt i PDF-konvertering?
EMF'ens Poly*-records går galt, fordi hver enkelt bærer en del af sin betydning uden for sine punkter: om figuren er åben, om den starter ved den aktuelle position, hvilken pen og brush der gælder, og hvor i recorden punkterne begynder. En enhanced metafile er en optagelse af GDI-kald mod en device context, så en konverterer skal afspille både den tilstand og koordinaterne. PDF har ingen device context. Den har en sti, et aktuelt punkt inde i stien og en tegneoperator, der vælger mellem stroke (S), fill (f) eller begge dele (B). Enhver uoverensstemmelse mellem de to modeller bliver til en lydløs rendering-forskel
Poly*-familien findes også i to breddeformater. Hver 32-bit record som EMR_POLYLINE har en 16-bit tvilling som EMR_POLYLINE16, der gemmer punkterne som SmallInt-par. GDI optager normalt den kompakte form, når alle koordinater er tilpas, så en konverterers 32-bit-handlere kan blive ved med at tage fejl i årevis, mens de almindelige testtegninger aldrig rammer dem. Den hurtigste revision er at føre de samme punkter igennem begge records og sammenligne de stier, der kommer ud. Records i denne artikel hører alle til [MS-EMF]'s drawing record-gruppe (2.3.5 Drawing Record Types)
| Record | Starter ved | Lukket? | Aktuel position |
|---|---|---|---|
EMR_POLYBEZIER | Punkt 0 | Nej | Bruges ikke, opdateres ikke |
EMR_POLYLINE | Punkt 0 | Nej (kun pen) | Bruges ikke, opdateres ikke |
EMR_POLYLINETO | Aktuel position | Nej (kun pen) | Bruges og opdateres |
EMR_POLYPOLYLINE | Første punkt i hver polylinje | Nej (kun pen) | Bruges ikke, opdateres ikke |
EMR_POLYDRAW | Første PT_MOVETO eller aktuel position | Kun hvor PT_CLOSEFIGURE er sat | Bruges og opdateres |
Hvor starter en EMR_POLYBEZIER-kurve egentlig?
En EMR_POLYBEZIER-kurve starter ved punkt 0, og først fra indeks 1 og frem grupperes punkterne i treere som kontrolpunkt, kontrolpunkt, slutpunkt. En record med 7 punkter tegner derfor to kubiske segmenter: 0 er starten, 1 til 3 danner det første segment, 4 til 6 det andet. 16-bit-handleren i PDFlibPas gjorde det allerede. 32-bit-handleren begyndte grupperingen ved punkt 0, så startpunktet blev opslugt som første kontrolpunkt, og alle senere segmenter skød én frem. Kurven blev stadig renderet, bare den forkerte. Siden v3.539.41 åbner begge breddeformater stien med m ved punkt 0 og udsender én c pr. komplet triple efter den
Til din egen parser: et antal, der ikke er 1 plus et multiplum af 3, er misdannet, og de overskydende punkter skal ignoreres frem for at syes ind i en kurve
PolyDraw: PT_CLOSEFIGURE er et flag, ikke en punkttype
I EMR_POLYDRAW er PT_CLOSEFIGURE (værdi 1) en bit, der kombineres med PT_LINETO (2) eller PT_BEZIERTO (4), så en gyldig typebyte kan være 3 eller 5. Punkttypen er byten med den bit maskeret væk, og flaget betyder: luk figuren efter det segment, der ender på dette punkt. PDFlibPas' gamle handler matchede byten mod enkelte værdier i en case-sætning, så punkter af type 3 og 5 matchede ingenting og blev sprunget helt over. Et rektangel tegnet med PolyDraw mistede sin afsluttende side, og en Bezier-triple, hvis sidste punkt bar flaget, mistede det punkt, hvilket skød alle senere treere ud af trit
Siden v3.539.39 læses typen som Types[i] and not PT_CLOSEFIGURE, og lukningen udsendes først efter et komplet segment: efter linjen for en lukket PT_LINETO, og efter tredje punkt i en Bezier-gruppe. En misdannet fil, der sætter flaget på første eller andet punkt i en triple, lukker ikke figuren i førtid. To relaterede fixes kom i samme release:
- Hver
PT_MOVETOi 16-bit-EMR_POLYDRAW16genstartede hele stien, så en record med tre figurer kun beholdt den sidste; nu starter det første move stien, og senere moves åbner understi (subpaths) - En PolyDraw-record, der ikke begynder med
PT_MOVETO, starter ved den aktuelle position, som record-definitionen siger, i stedet for at skrive enl- ellerc-operator uden en foregåendem
Hvorfor må en EMF-polylinje aldrig blive udfyldt i PDF?
En EMF-polylinje må aldrig blive udfyldt, fordi EMR_POLYLINE og EMR_POLYPOLYLINE er åbne figurer, der tegnes med pennen alene, og en åben sti i PDF lukkes implicit, når den udfyldes. ISO 32000-1 §8.5.3 fastslår, at fill-operatorerne lukker enhver åben understi, før den tegnes. En konverterer, der udsender B eller f for en polylinje med tre punkter, maler derfor en udfyldt trekant i den aktuelle brush-farve: den udfyldte kile under en grafs trendlinje. Før v3.539.41 udfyldte PDFlibPas begge polylinjebredder med brushen, og 32-bit-recorden blev desuden lukket eksplicit. I dag ender begge med stroke alene, og GDI's skel bevares: Polygon lukker og udfylder, Polyline gør aldrig
PolylineTo starter ved den aktuelle position
EMR_POLYLINETO tegner fra den aktuelle position gennem hvert punkt i recorden, forbliver åben og efterlader den aktuelle position ved sidste punkt. Den gamle handler indeholdt desuden et specialtilfælde, der slog pennen fra, når de to første punkter delte en y-koordinate, og intet slog den til igen, så alle senere records i filen mistede deres omrids. Pentilstand hører hjemme hos EMR_SELECTOBJECT og EMR_CREATEPEN; en tegnerecord-handler har ingen sag i at ændre den. Det specialtilfælde blev fjernet i v3.539.41, og recordens ét-punkts-variant læser ikke længere forbi sine egne punkter (rettet i v3.539.39)
PolyPolyline-punkter starter efter counts-arrayet
32-bit-EMR_POLYPOLYLINE gemmer nPolys antalværdier og derefter cptl punkter, og punkterne begynder ved byte offset 32 + nPolys * 4. Fælden ligger i RTL'en: uniten Windows erklærer TEMRPolyPolyline med aPolyCounts og aptl som ét-elements-arrays, så aptl[0] kun er det første punkt, når nPolys er 1. Kode, der indekserer aptl direkte, læser antalværdier som koordinater for hver multi-linje-record. PDFlibPas' gamle handler dimensionerede desuden sin bounds check efter det forkerte layout, så gyldige multi-linje-records blev afvist, og enkelt-linje-records tegnede intet. Siden v3.539.41 finder PDFlibPas punktarrayet ud fra det beregnede offset, ligesom dens PolyPolygon-handler altid har gjort, og tegner hver polylinje som sin egen åbne understi med én stroke til sidst. I v3.539.43 fik 16-bit-tvillingen samme behandling; den havde tegnet segment for segment, hvilket brød line joins og ignorerede en valgt NULL_PEN
Standard-pen og -brush samt sti-parenteserne
To tilstandsregler afrunder polylinje-fixene i v3.539.43:
- En frisk GDI device context har allerede
BLACK_PENogWHITE_BRUSHvalgt, så en metafil, der tegner uden nogenEMR_SELECTOBJECT, tegner stadig sort omrids; konvertereren startede først uden pen og uden fill og skrevn(afslut sti, mal intet) for sådanne records - Inde i en
BeginPath/EndPath-parentes bruger enPolylinehverken den aktuelle position eller opdaterer den, så den skal åbne en ny understi ved sit første punkt i stedet for at forbinde til den foregående figur, og intet må males, før parentesen strokes eller udfyldes
Byg en EMF-testfil med TMetafileCanvas
Den hurtigste måde at teste en konverterer mod disse regler på er at optage de tre risikable kald i én enhanced metafile med TMetafileCanvas. Tegningen nedenfor optager kurverne med en hul brush og vælger derefter med vilje en solid gul brush til polylinjen: en korrekt konverterer skal ignorere den brush til polylinjen, så enhver gul farve i output-PDF'en er en bug. PolyDraw har ingen TCanvas-wrapper, så den kaldes gennem Windows API'et med canvas-handlen, med typebytes 3 og 5 for at afprøve lukkeflaget
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Et lukket kvadrat (3 = LINETO + CLOSEFIGURE), derefter en lukket
// Bezier-figur, hvis sidste kontroltriple ender 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; // kun omrids til kurverne
// Punkt 0 er starten; 1..3 og 4..6 er to kubiske segmenter
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));
// Åben V-form med en solid brush valgt: strokes, lukkes aldrig
// om til en gul trekant
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // afslutter optagelsen
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Fordi disse koordinater er plads i en SmallInt, gemmer GDI normalt 16-bit-varianterne. For at nå 32-bit-handlerne skal du bruge en producent, der skriver dem, eller records, du bygger i hånden. Håndbyggede filer har deres egen fælde: VCL's TMetafile.LoadFromStream behandler streamen som en EMF, kun når den resterende længde er strengt større end den 108-byte store TEnhMetaHeader. En minimal håndskrevet EMF med kort header, eller en tom på præcis 108 bytes, tages for en WMF og afvises med "Metafile is not valid". Skriv altid hele 108-byte-headeren, inklusive extensionsfelterne, inden dine testrecords
Importér EMF'en ind i en PDF med PDFlibPas
PDFlibPas importerer en EMF med ImportEMFFromFile eller ImportEMFFromStream, som returnerer et image-ID forskelligt fra nul ved succes og 0 ved fejl. GeneralOptions = 0 bevarer den vektorsti, denne artikel handler om; 1 rasteriserer i stedet metafilen til en bitmap. FontOptions = 1 tilføjer metafilens fonte som ikke-indlejrede TrueType-fonte. Stream-varianten spoler streamen tilbage til position 0, før den indlæser, så giv den en stream, der kun indeholder 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); // oprindelse øverst til venstre for DrawImage
PDF.SetMeasurementUnits(0); // punkter
// FontOptions 1 = tilføj fonte som ikke-indlejrede TrueType
// GeneralOptions 0 = vektorimport, 1 = bitmap
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// For en EMF er ImageWidth / ImageHeight frammeastørrelsen i punkter
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// Siden kalder kun den importerede form: 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 bliver til et form XObject, så GetPageContentToString returnerer kun sekvensen save, transform, Do og restore. Operatorerne m, l, c, h og S, der produceres ud fra Poly*-records, bor i form XObject-streamen, som er komprimeret. For at revidere dem: dekomprimér den gemte fil i en PDF object inspector og læs form-streamen; i testfilen ovenfor skal du se polylinjen ende i S uden nogen h forinden, et h ved hvert lukkeflag i PolyDraw-figurerne og ingen f eller B på nogen af disse understi. DrawImage skalerer desuden en importeret EMF uniformt efter den mindste af Width og Height, så tegningen bevarer sit formatforhold, selv hvis boksen, du giver med, ikke matcher det
Til Free Pascal-targets, se hvordan PDFlibPas' EMF-vektorimporter bygger under Free Pascal; record-semantikken er den samme, uanset hvor importeren compiler
Hvordan skal en EMF-parser behandle punktantal fra filen?
En EMF-parser skal behandle hvert punktantal som utroværdigt input og tjekke det mod record-størrelsen, før et eneste punkt kopieres. EnumEnhMetaFile garanterer kun, at hver records nSize holder sig inde i filen. Den tjekker ikke, at cptl stemmer med nSize, så en handler, der kopierer cptl punkter med Move, vil læse de efterfølgende records eller forbi metafilens slutning, når antallet er forfalsket eller korrupt. Siden v3.539.39 tjekker PDFlibPas fast header plus antal gange bytes pr. punkt mod nSize for PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo og Polygon i begge breddeformater, med én ekstra byte pr. punkt til PolyDraw-typebytes. For PolyPoly-records skal antallerne pr. figur desuden tilsammen højst svare til det deklarerede total, og figurer med nul punkter springes over
Samme tjek er kort nok til at kopiere ind i din egen parser. Denne version validerer en 32-bit EMR_POLYPOLYLINE og returnerer en pointer til dens rigtige punktarray:
uses
Winapi.Windows;
// Returnerer nil, medmindre recorden virkelig rummer de punkter, den deklarerer.
// Punkterne starter efter antalarrayet: 32 + nPolys * 4 bytes inde, ikke ved
// aptl[0], som RTL'en deklarerer som ét-elements-array
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; // forfalsket eller afkortet antal
Total := 0;
Count := @P^.aPolyCounts[0]; // gå via pointer: [0..0] udløser range checks
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // figurerne kræver flere punkter, end der findes
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Testen på, at punkterne er plads, kører først, så antalarrayet kendes til at ligge inde i recorden, før løkken går det igennem. Regnestykket er i Int64, fordi nPolys * 4 og cptl * 8 beregnet i 32 bit kan rende rundt og alligevel slippe igennem sammenligningen
Hurtig reference: EMF Poly*-regler til EMF-til-PDF-konvertering
EMR_POLYBEZIER: punkt 0 er startpunktet; gruppér fra punkt 1 i treere; rettet for 32-bit-recorden i v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: åbne figurer, stroke medS, aldrigh,fellerB, fordi PDF-fill lukker åbne understiEMR_POLYLINETO: start ved den aktuelle position, forbliv åben, opdatér den aktuelle position, rør aldrig pentilstanden- 32-bit
EMR_POLYPOLYLINE: punkterne starter ved byte32 + nPolys * 4, ikke vedaptl[0] EMR_POLYDRAW: maskérPT_CLOSEFIGUREvæk før dispatch, luk efter det fuldførte segment, start ved den aktuelle position, når første punkt ikke erPT_MOVETO- Default device context-tilstand er
BLACK_PENplusWHITE_BRUSH; v3.539.43 og senere respekterer den - Inde i
BeginPath/EndPathåbner hver polylinje sin egen understi, og intet males, før parentesen bruges - Validér hvert
cptl/cptsmodnSizei 64-bit-regnestykke, før du kopierer punkter - Håndbyggede test-EMF'er behøver hele 108-byte-headeren, ellers læser
TMetafile.LoadFromStreamdem som WMF
Går dine rapporter gennem en anden komponent, gælder samme record-semantik; HotPDF EMF- og WMF-vektorimport handler om, hvordan den komponent omsætter gradient- og hatch-brushes til PDF-patterns, og vektorgrafik, shaders og gradients i PDFlibPas handler om at tegne de samme figurer direkte med bibliotekets API i stedet for gennem en metafil
PDFlibPas v3.539.43 eller senere indeholder hver af reglerne ovenfor. Detaljer og trial-downloads findes på PDFlibPas Delphi PDF library-produktsiden