Teknisk artikel

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

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)

RecordStarter vedLukket?Aktuel position
EMR_POLYBEZIERPunkt 0NejBruges ikke, opdateres ikke
EMR_POLYLINEPunkt 0Nej (kun pen)Bruges ikke, opdateres ikke
EMR_POLYLINETOAktuel positionNej (kun pen)Bruges og opdateres
EMR_POLYPOLYLINEFørste punkt i hver polylinjeNej (kun pen)Bruges ikke, opdateres ikke
EMR_POLYDRAWFørste PT_MOVETO eller aktuel positionKun hvor PT_CLOSEFIGURE er satBruges 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

PDFlibPas-diagram over en EMR_POLYBEZIER-record med syv punkter, hvor punkt nul åbner stien med m, og punkterne et til tre og fire til seks hver danner et kubisk c-segment, med det rettede 32-bit-handle siden v3.539.41 sat op imod den gamle gruppering, der opslugte startpunktet som kontrolpunkt
Punkt 0 er startpunktet, og kun komplette treere efter det bliver til kubiske segmenter, så en PolyBezier med syv punkter renderes som m plus to c-operatorer

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_MOVETO i 16-bit-EMR_POLYDRAW16 genstartede 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 en l- eller c-operator uden en foregående m
PDFlibPas-anatomi af en EMR_POLYDRAW-typebyte, hvor PT_CLOSEFIGURE er flagbit nul OR'et ind i PT_LINETO eller PT_BEZIERTO, så gyldige typebytes 3 og 5 skal maskeres med and not PT_CLOSEFIGURE før dispatch; den gamle case-sætning sprang begge bytes over, og lukkede figurer mistede deres sidste side
Maskér lukkeflaget væk, før du dispatcher, og udsend lukningen først efter en fuldført linje eller Bezier-triple, ellers dropper PolyDraw stille punkter

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

PDFlibPas-sammenligning af en åben V-polylinje eksporteret fra EMR_POLYLINE: en korrekt konverterer afslutter stien med stroke-operatoren S og ignorerer den valgte brush, mens udsendelse af f eller B lukker den åbne understi implicit under ISO 32000-1 8.5.3 og maler buggen med den udfyldte kile i grafen
En fill-operator lukker enhver åben understi, før den tegnes, så polylinjer skal ende i S uden h, f eller B på understien

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_PEN og WHITE_BRUSH valgt, så en metafil, der tegner uden nogen EMR_SELECTOBJECT, tegner stadig sort omrids; konvertereren startede først uden pen og uden fill og skrev n (afslut sti, mal intet) for sådanne records
  • Inde i en BeginPath / EndPath-parentes bruger en Polyline hverken 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.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: åbne figurer, stroke med S, aldrig h, f eller B, fordi PDF-fill lukker åbne understi
  • EMR_POLYLINETO: start ved den aktuelle position, forbliv åben, opdatér den aktuelle position, rør aldrig pentilstanden
  • 32-bit EMR_POLYPOLYLINE: punkterne starter ved byte 32 + nPolys * 4, ikke ved aptl[0]
  • EMR_POLYDRAW: maskér PT_CLOSEFIGURE væk før dispatch, luk efter det fuldførte segment, start ved den aktuelle position, når første punkt ikke er PT_MOVETO
  • Default device context-tilstand er BLACK_PEN plus WHITE_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 / cpts mod nSize i 64-bit-regnestykke, før du kopierer punkter
  • Håndbyggede test-EMF'er behøver hele 108-byte-headeren, ellers læser TMetafile.LoadFromStream dem 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