Teknisk artikel

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

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)

PostBörjar påStängd?Aktuell position
EMR_POLYBEZIERPunkt 0NejAnvänds inte, uppdateras inte
EMR_POLYLINEPunkt 0Nej (endast penna)Används inte, uppdateras inte
EMR_POLYLINETOAktuell positionNej (endast penna)Används och uppdateras
EMR_POLYPOLYLINEFörsta punkten i varje polylinjeNej (endast penna)Används inte, uppdateras inte
EMR_POLYDRAWFörsta PT_MOVETO, eller aktuell positionBara där PT_CLOSEFIGURE är sattAnvä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

PDFlibPas-diagram över en EMR_POLYBEZIER-post med sju punkter där punkt noll öppnar pathen med m och punkterna ett till tre och fyra till sex vardera bildar ett kubiskt c-segment, med den fixade 32-bitarsrutinen sedan v3.539.41 som kontrast mot den gamla grupperingen som åt upp startpunkten som kontrollpunkt
Punkt 0 är startpunkten och bara kompletta trippelar därefter blir kubiska segment, så en sjupunkters PolyBezier renderas som m plus två c-operatorer

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_MOVETO i 16-bitars EMR_POLYDRAW16 startade 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_MOVETO börjar på den aktuella positionen, som postdefinitionen säger, i stället för att skriva en l- eller c-operator utan föregående m
PDFlibPas-anatomi över ett EMR_POLYDRAW-typbyte där PT_CLOSEFIGURE är flaggbit noll OR:ad in i PT_LINETO eller PT_BEZIERTO, så att giltiga typbyten 3 och 5 måste maskas med and not PT_CLOSEFIGURE före dispatch; gamla case-satsen hoppade över båda bytena och stängda figurer tappade sin sista sida
Maska bort stängflaggan före dispatch och emittera stängningen först efter en färdig linje eller Bezier-trippel, annars tappar PolyDraw tyst punkter

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

PDFlibPas-jämförelse av en öppen V-polylinje exporterad från EMR_POLYLINE: en korrekt konverterare avslutar pathen med stroke-operatorn S och ignorerar den valda penseln, medan f eller B stänger den öppna subpathen implicit enligt ISO 32000-1 8.5.3 och målar den fyllda kilen, graffelen
En fill-operator stänger varje öppen subpath före den målas, så polylinjer måste sluta med S utan h, f eller B på subpathen

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_PEN och WHITE_BRUSH valda, så en metafil som ritar utan någon EMR_SELECTOBJECT ritar ändå svarta konturer; konverteraren brukade starta utan penna och utan fyllnad och skrev n (avsluta path, måla inget) för sådana poster
  • Inuti en BeginPath / EndPath-parentes varken använder eller uppdaterar en Polyline den 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.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: öppna figurer, stroke med S, aldrig h, f eller B, för PDF-fill stänger öppna subpaths
  • EMR_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å byte 32 + nPolys * 4, inte på aptl[0]
  • EMR_POLYDRAW: maska bort PT_CLOSEFIGURE före dispatch, stäng efter det färdiga segmentet, börja på den aktuella positionen när första punkten inte är PT_MOVETO
  • Standardläge för device context är BLACK_PEN plus WHITE_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 / cpts mot nSize i 64-bitars aritmetik innan punkterna kopieras
  • Handbyggda test-EMF:er behöver den fullständiga 108-byte-headern, annars läser TMetafile.LoadFromStream dem 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