Teknisk artikkel

PDFlibPas EMF-import: PolyDraw-, Polyline- og Bezier-regler

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)

PostStarter iLukket?Gjeldende posisjon
EMR_POLYBEZIERPunkt 0NeiBrukes ikke, oppdateres ikke
EMR_POLYLINEPunkt 0Nei (bare pennen)Brukes ikke, oppdateres ikke
EMR_POLYLINETOGjeldende posisjonNei (bare pennen)Brukes og oppdateres
EMR_POLYPOLYLINEFørste punkt i hver polylinjeNei (bare pennen)Brukes ikke, oppdateres ikke
EMR_POLYDRAWFørste PT_MOVETO, ellers gjeldende posisjonBare der PT_CLOSEFIGURE er sattBrukes 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å

PDFlibPas-diagram over en EMR_POLYBEZIER-post med syv punkter der punkt null åpner pathen med m og punkt én til tre og fire til seks hver danner ett kubisk c-segment, med den fikserte 32-bits-handleren siden v3.539.41 opp mot den gamle grupperingen som spiste startpunktet som kontrollpunkt
Punkt 0 er startpunktet, og bare komplette treere etter det blir kubiske segmenter, så en PolyBezier med syv punkter rendres som m pluss to c-operatorer

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_MOVETO i 16-bits EMR_POLYDRAW16 startet 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 en l- eller c-operator uten foregående m
PDFlibPas-anatomi av en EMR_POLYDRAW-typebyte der PT_CLOSEFIGURE er flaggbit null OR-et inn i PT_LINETO eller PT_BEZIERTO, så gyldige typebyter 3 og 5 må maskeres med and not PT_CLOSEFIGURE før dispatch; den gamle case-setningen hoppet over begge bytene, og lukkede figurer mistet sin siste side
Masker bort lukkeflagget før dispatch og send lukkingen først etter en fullført linje eller Bezier-trippel, ellers mister PolyDraw punkter i stillhet

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

PDFlibPas-sammenligning av en åpen V-polylinje eksportert fra EMR_POLYLINE: en korrekt konverterer avslutter pathen med stroke-operatoren S og ignorerer valgt pensel, mens f eller B lukker den åpne subpathen implisitt etter ISO 32000-1 8.5.3 og maler den fylte kilen som grafikkfeil
En fylloperator lukker enhver åpen subpath før den tegnes, så polylinjer må ende i S uten h, f eller B på subpathen

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_PEN og WHITE_BRUSH valgt, så en metafil som tegner uten noen EMR_SELECTOBJECT, tegner fortsatt svarte omkretser; konvertereren startet før med ingen penn og ingen fyll og skrev n (avslutt path, mal ingenting) for slike poster
  • Innenfor en BeginPath / EndPath-parentes bruker en Polyline verken 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.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: åpne figurer, stroke med S, aldri h, f eller B, fordi PDF-fill lukker åpne subpaths
  • EMR_POLYLINETO: start i gjeldende posisjon, forblir åpen, oppdater gjeldende posisjon, rør aldri pennetilstanden
  • 32-bits EMR_POLYPOLYLINE: punktene starter på byte 32 + nPolys * 4, ikke ved aptl[0]
  • EMR_POLYDRAW: masker bort PT_CLOSEFIGURE før dispatch, lukk etter det fullførte segmentet, start i gjeldende posisjon når første punkt ikke er PT_MOVETO
  • Standardtilstanden for device context er BLACK_PEN pluss WHITE_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 / cpts mot nSize i 64-bits aritmetikk før du kopierer punkter
  • Håndbygde test-EMF-er trenger hele 108-byte-headeren, ellers leser TMetafile.LoadFromStream dem 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