Teknisk artikel

EMF-vektorimport på Free Pascal med PDFlibPas

PDFlibPas konverterer enhanced metafiles til ægte PDF-sideindhold record for record i stedet for at rasterisere dem, og det er netop dét, der holder et importeret diagram eller en CAD-tegning skarp ved enhver zoom. Den konverter er på omkring 6500 linjer og var skrevet mod VCL, så da biblioteket fik et Free Pascal-mål, blev den klassificeret som uportérbar og erstattet af en stub. Den klassifikation var forkert, og måden den var forkert på er en nyttig lektie i, hvordan man reviderer en afhængighed, før man beslutter sig til at omskrive rundt om den

Den faktiske VCL-overflade af de 6500 linjer viste sig at være lille: en bitmapklasse brugt for sin pixelformat, stream-gemning, handle, canvas og scanlines; en metafilklasse brugt for sin bredde, højde og handle; og farvetypen med to konstanter. Hver eneste af dem blev allerede leveret af bibliotekets egen grafikunit, som netop findes, for at non-VCL-bygget skal have ækvivalenter. Konverteren blokerede slet ikke på VCL. Den blokerede på Free Pascal Windows-uniten

Del langs den akse, koden reelt afhænger af

Ændringen var altså ingen reimplementering. Den var én betinget kompilering: fra "kompilér stubben, når der bygges uden VCL" til "kompilér stubben, når der ikke bygges til Windows". Det er den korrekte akse, og at sige hvorfor gør forskellen oplagt. En enhanced metafile er en Windows-container. Konverteren er fra top til bund en parser for Windows GDI-records. Hvidt værtsapplikationen bruger VCL, et andet widget-sæt eller slet ingen widget-sæt har intet med at gøre, om de records kan fortolkes; hvorvidt målet er Windows har alt med det at gøre

Konsekvenserne af at vælge den rigtige akse falder gratis ud af det. C++Builder-byg, som underfiner Windows-platformsymbolet i dette bibliotek, beholder den kastende stub og opfører sig præcis som før. macOS beholder stubben, korrekt, for der er ingen GDI-records at parse dér. Delphi VCL-byg berøres ikke. Og et Windows-build med et non-VCL widget-sæt får vektor-EMF-import som sidegevinst, som ingen behøvede at implementere. En betinget kompilering, der er justeret efter den reelle afhængighed, gør platformsarbejde til en ændring på én linje; en, der er justeret efter den forkerte, gør det til en omskrivning, der aldrig bliver planlagt

EMF-import-betingelsen re-akset fra VCL-medlemskab til Windows-platformen, stubbe beholdes andre steder, og non-VCL Windows-byg får vektorimport
At re-akse stubbetingelsen til Windows-platformen bevarer al eksisterende build-adfærd og giver non-VCL Windows-mål EMF-vektorimport gratis

Free Pascal-hullet var deklarationer, ikke logik

Det, der reelt manglede, var Win32-deklarationerne, som Delphi Windows-uniten leverer, og Free Pascal-uniten ikke gør. At samle dem i én kompatibilitetsunit frem for at sprede betingede kompileringer gennem konverteren holdt parseren læsbar. Listen er oplærnde, fordi den viser, hvor ujævn header-dækningen er mellem de to RTL'er: 113 konstanter for metafilrecord-typer, to flags for extended text-output, tre konstanter for gradient fill mode, en pointertype til handle-tabellen, aliaser for gradient vertex- og primitive-records og tre record-typer, som Free Pascal slet ikke deklarerer, dækkende alpha blending, transparent blitting og colour-management mode

Ingen af det er interessant hver for sig. Det hele skal være korrekt, før parseren kompilerer, og en kompatibilitetsunit er det naturlige hjem, fordi den kan diffes mod header-dokumentationen som én helhed

Win32-deklarationer, der mangler i Free Pascal Windows-uniten, samlet i én kompatibilitetsunit til EMF-til-PDF-vektorkonverteren
Record-konstanter, flags, aliaser og tre manglende record-typer bor alle i én kompatibilitetsunit, der kan diffes mod header-dokumentationen

Den, der i stilhed tegner det forkerte billede

To af de deklarationer er ikke blot manglende, de er til stede og forkerte til dette formål, og det er den del, der er værd at huske, selv hvis du aldrig rører en metafil

Free Pascal deklarerer brush-oprettelsesrecorden med run-time brush-strukturen indlejret i den, og extended-pen-recorden med run-time pen-strukturen indlejret i den. Begge de run-time-strukturer deklarerer deres hatch-medlem som et heltal i pointerstørrelse, for i et levende GDI-kald kan det medlem bære et handle. En metafil gemmer derimod altid 32-bit-formen, for record-layoutet er en del af det serialiserede filformat og ændrer sig ikke med processens bitvidde

På 32-bit-byg er de to enige, og der sker intet. På Win64 er det pointerstørrelse medlem otte bytes, hvor filen har fire, så hvert felt efter hatch-medlemmet læses fra forkert offset. Der er ingen exception, ingen parsefejl og ingen advarsel. Metafilen renderes bare forkert: farver fra de forkerte bytes, pennebredder fra de forkerte bytes og et billede, der ligner en renderingsfejl snarere end en struct-layout-fejl. Delphi leverer netop af denne grund eksplicitte 32-bit-varianter af begge strukturer, og kompatibilitetsuniten redeclarerer dem på samme måde

Byte-layout af EMF brush-recorden, der viser et pointerstørrelse hatch-felt, som forskubber senere felter med fire bytes på Win64 i forhold til det faste 32-bit-layout
Den serialiserede record gemmer altid en 4-byte hatch, så run-time-strukturen i pointerstørrelse mislæser i stilhed alle senere felter på Win64
// Forkert på Win64: Hatch er i pointerstørrelse, filen gemmer 32 bits,
// og hvert efterfølgende felt forskubbes med fire bytes uden fejl
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 bytes i en 64-bit-proces
  end;

// Rigtigt: det serialiserede layout, fast bredde uanset bitvidde
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // altid 4 bytes, som gemt i metafilen
  end;

Den generelle regel: enhver struktur, der optræder både som run-time API-argument og som serialiseret felt-layout, behøver to deklarationer, og den serialiserede skal bruge typer med fast bredde hele vejen igennem. Medlemmer i pointerstørrelse i et filformat er altid en fejl, der venter på et 64-bit-build

Signaturforskelle hører hjemme i en wrapper, ikke ved hvert call site

De resterende forskelle var almindelige signatur-uoverensstemmelser, og måden at absorbere dem på er en videresendende wrapper frem for en betinget kompilering ved hvert call site. Transformkombinationsfunktionen tager pointere under Free Pascal, hvor Delphi tager referenceparametre, så wrapperen tager referencer og sender adresser. Den kopierer også begge kildeargumenter ind i lokale variabler først, for konverteren har call sites, hvor destinationsmatricen samtidig er en af kilderne, og at sende den samme adresse to gange til en funktion, der skriver, mens den læser, giver en transform, der er diskret forkert på en måde, som kun viser sig på roteret indhold

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Kopiér først: kaldere sender med rette 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- og punkttyperne er det andet tilfælde. Free Pascal behandler metafilens rektangel- og punktrecords som adskilte typer fra de generelle grafiktyper, så otte tildelingssteder behøvede en eksplicit cast mellem records med identisk layout. Begge compilere accepterer cast-formen, så de steder bærer slet ingen betinget kompilering, hvilket er lidt grimhed værd

Hvad det ændrer for en Free Pascal-udrulning

Vektor-EMF-import virker på Windows under Free Pascal og producerer det samme sideindhold som Delphi-bygget: paths som paths, gradients som pattern content, tekst som tekst. Uden for Windows forbliver rastervejen svaret, og det er en begrænsning i formatet snarere end i porteringen. Koordinat- og clipping-tilstanden, som konverteren føder ind i, er beskrevet i artiklen om content stream CTM- og clipping-trackeren, og de vektorprimitiver, den udsender, er dækket i vektorgrafik, shaders og gradients

Hvis du reviderer din egen kodebase for samme mulighed, er den nyttige øvelse den, der startede det hele: list de medlemmer, du reelt bruger, fra det framework, du tror, du afhænger af. Svaret er ofte meget kortere, end importlisten antyder, og den reelle begrænsning ligger som regel et helt andet sted. Device-context-baserede importveje beskrives generelt i artiklen om print preview og device context, og platform- og toolchain-dækning er opført på produktsiden losLab PDF Developer Library