Teknisk artikel

EMF-vektorimport i Free Pascal med PDFlibPas

PDFlibPas konverterar enhanced metafiles till verkligt PDF-sidinnehåll post för post i stället för att rastrera dem, vilket är det som håller ett importerat diagram eller en CAD-ritning skarp vid vilken inzoomning som helst. Den konverteraren är cirka 6500 rader och den skrevs mot VCL, så när biblioteket fick en Free Pascal-målplattform klassades den som oportabel och byttes ut mot en stub. Den klassificeringen var fel, och sättet den var fel på är en användbar läxa i hur man granskar ett beroende innan man bestämmer sig för att skriva om runt det

Den faktiska VCL-ytan hos dessa 6500 rader visade sig vara liten: en bitmappsklass använd för sitt pixelformat, sparande till ström, handle, canvas och scanlines; en metafileklass använd för sin bredd, höjd och sitt handle; samt färgtypen med två konstanter. Varenda en av dem tillhandahölls redan av bibliotekets egen grafikenhet, som finns just för att icke-VCL-bygget ska ha motsvarigheter. Konverteraren blockerades inte av VCL alls. Den blockerades av Free Pascal Windows-enheten

Dela på den axel koden faktiskt beror på

Ändringen var alltså inte en reimplementering. Den var ett villkor: från ”kompilera stubben när bygget sker utan VCL” till ”kompilera stubben när man inte bygger för Windows”. Det är den korrekta axeln, och att ange varför gör skillnaden uppenbar. En enhanced metafile är en Windows-container. Konverteraren är en parser för Windows GDI-poster från topp till tå. Huruvida värdapplikationen använder VCL, en annan widgetuppsättning eller ingen alls har inget med huruvida de posterna kan tolkas att göra; huruvida målet är Windows har allt med det att göra

Konsekvenserna av att välja rätt axel kommer gratis. C++Builder-byggen, som odefinierar Windows-plattformssymbolen i detta bibliotek, behåller den felkastande stubben och beter sig exakt som tidigare. macOS behåller stubben, korrekt, för där finns det inga GDI-poster att parsa. Delphi VCL-byggen är orörda. Och ett Windows-bygge med en icke-VCL-widgetuppsättning får vektor-EMF-import som biprodukt, vilket ingen behövde implementera. Ett villkor i linje med det verkliga beroendet gör plattformsarbete till en ändring på en rad; ett villkor i linje med fel beroende gör det till en omskrivning som aldrig blir schemalagd

EMF-importvillkoret får ny axel från VCL-medlemskap till Windows-plattformen, med stubbar kvar på andra ställen och vektorimport för icke-VCL-Windows-byggen
Att flytta stubbvillkoret till Windows-plattformen bevarar alla befintliga byggbeteenden och ger icke-VCL-Windows-mål EMF-vektorimport gratis

Free Pascal-bristen var deklarationer, inte logik

Det som faktiskt saknades var Win32-deklarationerna som Delphi Windows-enheten tillhandahåller och Free Pascal-enheten inte gör. Att samla dem i en enda kompatibilitetsenhet i stället för att strö villkor genom konverteraren höll parsern läsbar. Listan är lärorik eftersom den visar hur ojämn rubriktackningen är mellan de två RTL:erna: 113 konstanter för metafileposttyper, två utökade flaggor för textutmatning, tre konstanter för gradientfyllnadsläge, en pekartyp för handletabell, alias för gradientvertex- och primitivposter samt tre posttyper som Free Pascal inte deklarerar alls, täckande alfablandning, transparent blitting och färghanteringsläge

Inget av detta är intressant var för sig. Allt måste vara rätt innan parsern kompilerar, och en kompatibilitetsenhet är det naturliga hemmet eftersom den kan diffas mot rubrikdokumentationen som en enhet

Win32-deklarationer som saknas i Free Pascal Windows-enheten, samlade i en kompatibilitetsenhet för EMF-till-PDF-vektorkonverteraren
Postkonstanter, flaggor, alias och tre saknade posttyper bor alla i en kompatibilitetsenhet som kan diffas mot rubrikdokumentationen

Den som tyst ritar fel bild

Två av de deklarationerna är inte bara saknade, de finns där och är fel för detta syfte, och det är den del som är värd att minnas även om du aldrig rör en metafile

Free Pascal deklarerar posten för penselskapande med körtidsstrukturen för penslar inbäddad i sig, och posten för utökad penna med körtidsstrukturen för pennor inbäddad i sig. Båda de körtidsstrukturerna deklarerar sin hatch-medlem som ett pekarstort heltal, för i ett levande GDI-anrop kan den medlemmen bära ett handle. En metafile lagrar dock alltid 32-bitsformen, eftersom postlayouten är en del av det serialiserade filformatet och inte ändras med processens bitbredd

På 32-bitsbyggen överensstämmer de två och inget händer. På Win64 är den pekarstora medlemmen åtta byte där filen har fyra, så varje fält efter hatch-medlemmen läses från fel offset. Det finns inget undantag, inget parsningsfel och ingen varning. Metafilen renderas bara fel: färger från fel byte, pennbredder från fel byte och en bild som ser ut som en renderingsbugg i stället för en struct-layout-bugg. Delphi levererar uttryckliga 32-bitsvarianter av båda strukturerna av precis den anledningen, och kompatibilitetsenheten deklarerar om dem på samma sätt

Bytelayout för EMF-penselposten som visar ett pekarstort hatch-fält som förskjuter senare fält med fyra byte på Win64 jämfört med den fasta 32-bitslayouten
Den serialiserade posten lagrar alltid en hatch på 4 byte, så den pekarstora körtidsstrukturen läser fel på varje senare fält på Win64
// Fel på Win64: Hatch är pekarstor, filen lagrar 32 bitar,
// och varje följande fält förskjuts med fyra byte utan något fel
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 byte i en 64-bitsprocess
  end;

// Rätt: den serialiserade layouten, fast bredd oavsett bitbredd
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // alltid 4 byte, som lagrat i metafilen
  end;

Den allmänna regeln: varje struktur som uppträder både som körtidsargument till ett API och som serialiserad fältlayout behöver två deklarationer, och den serialiserade måste använda fastbreddstyper genomgående. Pekarstora medlemmar i ett filformat är alltid en bugg som väntar på ett 64-bitsbygge

Signaturskillnader hör hemma i en wrapper, inte på varje anropsplats

De återstående skillnaderna var vanliga signaturavvikelser, och sättet att absorbera dem är en vidarebefordrande wrapper i stället för ett villkor på varje anropsplats. Transformkombineringsfunktionen tar pekare under Free Pascal där Delphi tar referensparametrar, så wrappern tar referenser och skickar adresser. Den kopierar också båda källargumenten till lokala variabler först, eftersom konverteraren har anropsplatser där destinationsmatrisen samtidigt är en av källorna, och att skicka samma adress två gånger till en funktion som skriver medan den läser ger en transform som är subtilt fel på ett sätt som bara visar sig på roterat innehåll

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Kopiera först: anropare skickar med rätta Dest som A eller B
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

Rektangel- och punkttyperna är det andra fallet. Free Pascal behandlar metafilens rektangel- och punktposter som skilda typer från de allmänna grafiktyperna, så åtta tilldelningsplatser behövde en explicit typkonvertering mellan poster med identisk layout. Båda kompilatorerna accepterar typkonverteringsformen, så de platserna bär ingen villkorssats alls, vilket är värt lite fulhet

Vad detta ändrar för en Free Pascal-distribution

Vektor-EMF-import fungerar på Windows under Free Pascal och producerar samma sidinnehåll som Delphi-bygget: paths som paths, gradients som pattern-innehåll, text som text. Utanför Windows förblir rasterspåret svaret, och det är en begränsning i formatet snarare än i porteringen. Koordinat- och klippningstillståndet som konverteraren matar in i beskrivs i artikeln om innehållsströmmens CTM- och klippningsbevakare, och de vektorprimitiver den avger tas upp i vektorgrafik, shaders och gradienter

Om du granskar din egen kodbas för samma möjlighet är den användbara övningen den som startade detta: lista de medlemmar du faktiskt använder från ramverket du tror att du är beroende av. Svaret är ofta mycket kortare än importlistan antyder, och det verkliga tvånget ligger oftast någon helt annanstans. Importspår baserade på device context beskrivs i allmänhet i artikeln om förhandsgranskning och device context, och täckningen av plattformar och verktygskedjor listas på produktsidan för losLab PDF Developer Library