PDFlibPas, losLabs PDF-bibliotek for Delphi, gjør om EMF Poly*-postene til paths i PDF-en ved å følge hver postdefinisjon i [MS-EMF]: en 32-bits EMR_POLYBEZIER starter i punkt 0, polylinjer forblir åpne og blir bare stroket, PT_CLOSEFIGURE i EMR_POLYDRAW er et flagg, og alle punktantall sjekkes mot poststørrelsen. Disse reglene kom i v3.539.39, v3.539.41 og v3.539.43. Før dem kunne en rapportgraf komme ut av ImportEMFFromFile med en fylt kile der trendlinjen skulle vært, en Bezier-kurve bøyd mot feil kontrollpunkt, eller en lukket omkrets som manglet siste side. Ingen av delene ga noen feilmelding, og reglene gjelder enhver Delphi EMF-til-PDF-konverterer eller GDI-postparser
Hvorfor går EMF Poly*-postene galt i PDF-konvertering?
EMF Poly*-postene går galt fordi hver av dem bærer en del av betydningen sin utenfor punktene: om figuren er åpen, om den starter i gjeldende posisjon, hvilken penn og pensel som gjelder, og hvor i posten punktene begynner. En enhanced metafile er et opptak av GDI-kall mot en device context, så en konverterer må spille av device context-tilstanden like godt som koordinatene. PDF har ingen device context. Den har en path, et gjeldende punkt i den pathen og en tegneoperator som velger mellom stroke (S), fill (f) og begge (B). Hver mismatch mellom de to modellene ender som en rendering som avviker, helt uten feilmelding
Poly*-familien finnes også i to bredder. Hver 32-bits post som EMR_POLYLINE, har en 16-bits tvilling som EMR_POLYLINE16, som lagrer punktene som SmallInt-par. GDI tar normalt opp den kompakte formen når alle koordinatene passer, så en konverterers 32-bits handlere kan ligke galt i årevis mens hverdagslige testtegninger aldri når dem. Den raskeste revisjonen er å mate de samme punktene gjennom begge postene og sammenligne paths-en som kommer ut. Postene som dekkes her, ligger alle i tegnepostgruppen i [MS-EMF] (2.3.5 Drawing Record Types)
| Post | Starter i | Lukket? | Gjeldende posisjon |
|---|---|---|---|
EMR_POLYBEZIER | Punkt 0 | Nei | Brukes ikke, oppdateres ikke |
EMR_POLYLINE | Punkt 0 | Nei (bare pennen) | Brukes ikke, oppdateres ikke |
EMR_POLYLINETO | Gjeldende posisjon | Nei (bare pennen) | Brukes og oppdateres |
EMR_POLYPOLYLINE | Første punkt i hver polylinje | Nei (bare pennen) | Brukes ikke, oppdateres ikke |
EMR_POLYDRAW | Første PT_MOVETO, ellers gjeldende posisjon | Bare der PT_CLOSEFIGURE er satt | Brukes og oppdateres |
Hvor starter en EMR_POLYBEZIER-kurve egentlig?
En EMR_POLYBEZIER-kurve starter i punkt 0, og bare punktene fra indeks 1 og utover grupperes i treere som kontrollpunkt, kontrollpunkt, endepunkt. En post med 7 punkter tegner dermed to kubiske segmenter: 0 er starten, 1 til 3 utgjør det første segmentet, 4 til 6 det andre. 16-bits-handleren i PDFlibPas hadde allerede dette riktig. 32-bits-handleren begynte grupperingen i punkt 0, så startpunktet ble spist som første kontrollpunkt, og alle senere segmenter skiftet med én. Kurven ble fortsatt rendret, bare feil en. Siden v3.539.41 åpner begge bredder pathen med m i punkt 0 og sender ut én c per komplett trippel etterpå
Til din egen parser: et antall som ikke er 1 pluss et multiplum av 3, er misdannet, og punktene på slutten bør ignoreres i stedet for å sys inn i en kurve
PolyDraw: PT_CLOSEFIGURE er et flagg, ikke en punkttype
I EMR_POLYDRAW er PT_CLOSEFIGURE (verdi 1) en bit som kombineres med PT_LINETO (2) eller PT_BEZIERTO (4), så en gyldig typebyte kan være 3 eller 5. Punkttypen er byten med den biten maskert bort, og flagget betyr lukk figuren etter segmentet som ender i dette punktet. Den gamle PDFlibPas-handleren sammenlignet byten med enkeltverdier i en case-setning, så punkter av type 3 og 5 matchet ingenting og ble hoppet fullstendig over. Et rektangel tegnet med PolyDraw mistet sin avsluttende side, og en Bezier-trippel hvis siste punkt bar flagget mistet det punktet, noe som skjøv alle senere tripler ut av takt
Siden v3.539.39 leses typen som Types[i] and not PT_CLOSEFIGURE, og lukkingen sendes bare ut etter et fullt segment: etter linjen for en lukket PT_LINETO, og etter tredje punkt i en Bezier-gruppe. En misdannet fil som setter flagget på første eller andre punkt i en trippel, lukker ikke figuren tidlig. To relaterte fikser kom i samme utgivelse:
- Hver
PT_MOVETOi 16-bitsEMR_POLYDRAW16startet hele pathen på nytt, så en post med tre figurer beholdt bare den siste; nå starter første move pathen, og senere moves åpner subpaths - En PolyDraw-post som ikke begynner med
PT_MOVETO, starter i gjeldende posisjon, slik postdefinisjonen sier, i stedet for å skrive enl- ellerc-operator uten foregåendem
Hvorfor må en EMF-polylinje aldri fylles i PDF?
En EMF-polylinje må aldri fylles fordi EMR_POLYLINE og EMR_POLYPOLYLINE er åpne figurer tegnet bare med pennen, og å fylle en åpen path i PDF-en lukker den implisitt. ISO 32000-1 §8.5.3 sier at fylloperatorene lukker enhver åpen subpath før den tegnes. En konverterer som sender ut B eller f for en polylinje med tre punkter, maler dermed en fylt trekant i gjeldende penselfarge: den fylte kilen under en grafs trendlinje. Før v3.539.41 fylte PDFlibPas begge polylinje-breddene med penselen, og 32-bits-posten ble også lukket eksplisitt. I dag ender begge bredder med bare stroke, og GDI-skillet er bevart: Polygon lukker og fyller, Polyline gjør aldri det
PolylineTo starter i gjeldende posisjon
EMR_POLYLINETO tegner fra gjeldende posisjon gjennom hvert punkt i posten, forblir åpen, og etterlater gjeldende posisjon i siste punkt. Den gamle handleren inneholdt også et spesialtilfelle som slo av pennen når de to første punktene delte y-koordinat, og ingenting slo den på igjen, så hver senere post i filen mistet sin omkrets. Pennetilstand hører hjemme hos EMR_SELECTOBJECT og EMR_CREATEPEN; en tegnepost-handler har intet å gjøre med å endre den. Det spesialtilfellet ble fjernet i v3.539.41, og en-punkt-formen av posten leser ikke lenger forbi egne punkter (fikset i v3.539.39)
PolyPolyline-punktene starter etter antallstabellen
Den 32-bits EMR_POLYPOLYLINE-posten lagrer nPolys antall og deretter cptl punkter, og punktene begynner på byte-offset 32 + nPolys * 4. Fellen ligger i RTL-en: Windows-uniten deklarerer TEMRPolyPolyline med aPolyCounts og aptl som arrayer med ett element, så aptl[0] er første punkt bare når nPolys er 1. Kode som indekserer aptl direkte, leser antallsverdier som koordinater for hver flerlinjet post. Den gamle PDFlibPas-handleren dimensjonerte også bounds-sjekken sin etter det galge layoutet, så gyldige flerlinjede poster ble avvist og enslinjede tegnet ingenting. Siden v3.539.41 finner PDFlibPas punkt-arrayen fra beregnet offset, slik PolyPolygon-handleren alltid gjorde, og tegner hver polylinje som sin egen åpne subpath med én stroke på slutten. I v3.539.43 fikk 16-bits-tvillingen samme behandling; den hadde tegnet segment for segment, noe som ødela linjeskjøter og ignorerte en valgt NULL_PEN
Standardpennen og penselen, og path-parentesene
To tilstandsregler fullfører polylinjefiksene i v3.539.43:
- En fersk GDI device context har allerede
BLACK_PENogWHITE_BRUSHvalgt, så en metafil som tegner uten noenEMR_SELECTOBJECT, tegner fortsatt svarte omkretser; konvertereren startet før med ingen penn og ingen fyll og skrevn(avslutt path, mal ingenting) for slike poster - Innenfor en
BeginPath/EndPath-parentes bruker enPolylineverken eller oppdaterer gjeldende posisjon, så den må åpne en ny subpath i sitt første punkt i stedet for å koble seg til forrige figur, og ingenting kan males før parentesen strokes eller fylles
Bygge en EMF-testfil med TMetafileCanvas
Den raskeste måten å sjekke en konverterer mot disse reglene på, er å ta opp de tre risikofylte kallene i én enhanced metafile med TMetafileCanvas. Tegningen nedenfor tar opp kurvene med en hul pensel og velger deretter med vilje en helfylt gul pensel til polylinjen: en korrekt konverterer må ignorere den penselen for polylinjen, så all gul i PDF-en som kommer ut, er en bug. PolyDraw har ingen TCanvas-innpakning, så den kalles gjennom Windows API med canvas-håndtaket, med typebyter 3 og 5 for å teste lukkeflagget
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Et lukket kvadrat (3 = LINETO + CLOSEFIGURE), deretter en lukket
// Bezier-figur hvis siste kontrolltrippel 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; // bare omkretser for kurvene
// 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));
// Åpen V-form med helfylt pensel valgt: strokes, aldri lukket
// 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; // avslutter opptaket
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Siden disse koordinatene passer i en SmallInt, lagrer GDI normalt 16-bits-variantene. For å nå 32-bits-handlerne trenger du en produsent som skriver dem, eller poster du bygger for hånd. Håndbygde filer har sin egen felle: VCL TMetafile.LoadFromStream behandler strømmen som en EMF bare når gjenværende lengde er strengt større enn de 108 bytene i TEnhMetaHeader. En minimal håndskrevet EMF med kort header, eller en tom én som er nøyaktig 108 byte lang, tas for en WMF og avvises med «Metafile is not valid». Skriv alltid hele 108-byte-headeren, inkludert utvidelsesfeltene, før testpostene dine
Importere EMF-en inn i en PDF med PDFlibPas
PDFlibPas importerer en EMF med ImportEMFFromFile eller ImportEMFFromStream, som returnerer en image-ID ulik null ved suksess og 0 ved feil. GeneralOptions = 0 beholder vektorpathen denne artikkelen handler om; 1 rasteriserer metafilen til et bitmap i stedet. FontOptions = 1 legger til metafilfontene som ikke-innebygde TrueType-fonts. Strømvarianten spoler strømmen tilbake til posisjon 0 før lasting, så send inn en strøm som bare inneholder 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); // øvre venstre origo for DrawImage
PDF.SetMeasurementUnits(0); // punkter
// FontOptions 1 = legg til fonts som ikke-innebygd 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 rammestørrelsen i punkter
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// Siden kaller bare opp den importerte 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 returnerer bare save-, transform-, Do- og restore-sekvensen. m-, l-, c-, h- og S-operatorene som produseres fra Poly*-postene, ligger i form XObject-strømmen, som er komprimert. For å revidere dem, dekomprimer den lagrede filen i en PDF-object inspector og les form-strømmen: for testfilen over skal du se polylinjen ende i S uten h før, en h ved hvert lukkeflagg i PolyDraw-figurene, og ingen f eller B på noen av disse subpaths. DrawImage skalerer også en importert EMF uniformt etter den minste av Width og Height, så tegningen beholder sideforholdet selv om boksen du sender inn, ikke matcher det
For Free Pascal-mål, se hvordan PDFlibPas EMF-vektorimportøren bygger under Free Pascal; postsemantikken er den samme uansett hvor importøren kompilerer
Hvordan bør en EMF-parser behandle punktantall fra filen?
En EMF-parser bør behandle hvert punktantall som uklarert input og sjekke det mot poststørrelsen før ett eneste punkt kopieres. EnumEnhMetaFile garanterer bare at hver posts nSize holdes innenfor filen. Den sjekker ikke at cptl stemmer med nSize, så en handler som kopierer cptl punkter med Move, vil lese de følgende postene, eller forbi slutten av metafilen, når antallet er forfalsket eller ødelagt. Siden v3.539.39 sjekker PDFlibPas fast header pluss antall ganger byte per punkt mot nSize for PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo og Polygon i begge bredder, med én ekstra byte per punkt for PolyDraw-typebyter. For PolyPoly-postene må antallene per figur også summere til høyst det deklarerte totalen, og figurer uten punkter hoppes over
Samme sjekk er kort nok til å kopiere inn i din egen parser. Denne versjonen validerer en 32-bits EMR_POLYPOLYLINE og returnerer en peker til den virkelige punkt-arrayen:
uses
Winapi.Windows;
// Returnerer nil med mindre posten virkelig inneholder punktene den deklarerer.
// Punkter starter etter antallstabellen: 32 + nPolys * 4 byte inn, ikke ved
// aptl[0], som RTL-en deklarerer som et 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; // forfalsket eller avkuttet antall
Total := 0;
Count := @P^.aPolyCounts[0]; // gå via peker: [0..0] utløser range checks
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // figurene krever flere punkter enn det finnes
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Punkt-pass-formen-testen kjøres først, så antallstabellen er kjent for å ligge inne i posten før løkken går gjennom den. Regnestykket er Int64 fordi nPolys * 4 og cptl * 8 beregnet i 32 bit kan wrappe rundt og slippe gjennom sammenligningen
Hurtigreferanse: EMF Poly*-regler for EMF-til-PDF-konvertering
EMR_POLYBEZIER: punkt 0 er startpunktet; grupper fra punkt 1 i treere; fikset for 32-bits-posten i v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: åpne figurer, stroke medS, aldrih,fellerB, fordi PDF-fill lukker åpne subpathsEMR_POLYLINETO: start i gjeldende posisjon, forblir åpen, oppdater gjeldende posisjon, rør aldri pennetilstanden- 32-bits
EMR_POLYPOLYLINE: punktene starter på byte32 + nPolys * 4, ikke vedaptl[0] EMR_POLYDRAW: masker bortPT_CLOSEFIGUREfør dispatch, lukk etter det fullførte segmentet, start i gjeldende posisjon når første punkt ikke erPT_MOVETO- Standardtilstanden for device context er
BLACK_PENplussWHITE_BRUSH; v3.539.43 og senere respekterer den - Innenfor
BeginPath/EndPathåpner hver polylinje sin egen subpath, og ingenting males før parentesen brukes - Valider hvert
cptl/cptsmotnSizei 64-bits aritmetikk før du kopierer punkter - Håndbygde test-EMF-er trenger hele 108-byte-headeren, ellers leser
TMetafile.LoadFromStreamdem som WMF
Går rapportene dine gjennom en annen komponent, gjelder samme postsemantikk; HotPDF EMF- og WMF-vektorimport dekker hvordan den komponenten gjør gradient- og hatch-pensler om til PDF-mønstre, og vektorgrafikk, shaders og gradienter i PDFlibPas dekker å tegne de samme formene direkte med bibliotek-API-en i stedet for gjennom en metafil
PDFlibPas v3.539.43 eller senere inneholder alle reglene over. Detaljer og prøvenedlastinger ligger på PDFlibPas Delphi PDF library-produktsiden