Articol tehnic

Import vectorial EMF în Free Pascal cu PDFlibPas

PDFlibPas convertește metafișierele enhanced în conținut real de pagină PDF înregistrare cu înregistrare, în loc să le rastruze, iar acest lucru menține un grafic sau un desen CAD importat clar la orice zoom. Acel convertor are circa 6500 de linii și a fost scris împotriva VCL, astfel încât, când biblioteca a căpătat o țintă Free Pascal, a fost clasificat drept inportabil și înlocuit cu un stub. Clasificarea a fost greșită, iar modul în care a fost greșită este o lecție utilă despre cum să auditați o dependență înainte să decideți să rescrieți în jurul ei

Suprafața VCL efectiv folosită din acele 6500 de linii s-a dovedit mică: o clasă bitmap folosită pentru formatul de pixel, salvarea în stream, handle, canvas și scanlines; o clasă de metafișier folosită pentru lățime, înălțime și handle; și tipul de culoare cu două constante. Fiecare dintre acestea era deja furnizată de unitatea grafică proprie a bibliotecii, care există tocmai pentru ca build-ul non-VCL să aibă echivalente. Convertorul nu a fost blocat deloc de VCL. A fost blocat de unitatea Windows a Free Pascal

Separare pe axa de care depinde efectiv codul

Deci schimbarea nu a fost o reimplementare. A fost o singură condițională: din „compilează stub-ul când se construiește fără VCL" în „compilează stub-ul când nu se construiește pentru Windows". Aceasta este axa corectă, iar enunțarea motivului face diferența evidentă. Un metafișier enhanced este un container Windows. Convertorul este, de la un capăt la altul, un parser pentru înregistrări GDI Windows. Dacă aplicația gazdă folosește VCL, alt set de widget-uri sau niciun set de widget-uri nu are nicio legătură cu posibilitatea de a interpreta acele înregistrări; faptul că ținta este Windows are tot atât de mult de-a face cu asta

Consecințele alegerii axei corecte rezultă gratis. Build-urile C++Builder, care nedefinesc simbolul de platformă Windows în această bibliotecă, păstrează stub-ul care aruncă excepție și se comportă exact ca înainte. macOS păstrează stub-ul, corect, deoarece acolo nu există înregistrări GDI de interpretat. Build-urile Delphi VCL rămân neatinse. Iar un build Windows cu un set de widget-uri non-VCL câștigă importul vectorial EMF ca efect secundar, fără ca cineva să fi trebuit să îl implementeze. O condițională aliniată cu dependența reală transformă munca de platformă într-o schimbare de o linie; una aliniată cu dependența greșită o transformă într-o rescriere care nu ajunge niciodată în planificare

Condiționala de import EMF realiniată de la apartenența la VCL la platforma Windows, păstrând stub-urile în restul cazurilor și oferind build-urilor Windows non-VCL import vectorial
Realiniarea condiției stub-ului la platforma Windows păstrează comportamentul fiecărui build existent și le oferă țintelor Windows non-VCL import vectorial EMF gratis

Lacuna Free Pascal a fost declarații, nu logică

Ce lipsea efectiv erau declarațiile Win32 pe care unitatea Windows din Delphi le furnizează, iar cea din Free Pascal nu. Adunarea lor într-o singură unitate de compatibilitate, în loc să risipiți condiționale prin convertor, a menținut parserul lizibil. Lista este edificatoare, deoarece arată cât de neuniformă este acoperirea header-elor între cele două RTL: 113 constante de tip de înregistrare de metafișier, două fanioane pentru ieșire text extinsă, trei constante de mod de umplere gradient, un tip de pointer pentru tabela de handle, aliasuri pentru înregistrările de vârf gradient și primitive, și trei tipuri de înregistrare pe care Free Pascal nu le declară deloc, care acoperă amestecarea alfa, blitting transparent și modul de gestiune a culorilor

Nimic din toate acestea nu este interesant individual. Totul trebuie să fie corect înainte ca parserul să compileze, iar o unitate de compatibilitate este căminul natural, deoarece poate fi comparată diff cu documentația header-elor ca unitate

Declarațiile Win32 lipsă din unitatea Windows a Free Pascal, adunate într-o singură unitate de compatibilitate pentru convertorul vectorial EMF în PDF
Constantele de înregistrare, fanioanele, aliasurile și cele trei tipuri de înregistrare absente trăiesc într-o singură unitate de compatibilitate, comparabilă diff cu documentația header-elor

Cea care desenează în tăcere imaginea greșită

Două dintre acele declarații nu sunt doar absente: sunt prezente și greșite pentru acest scop, iar aceasta este partea care merită reținută chiar dacă nu atingeți niciodată un metafișier

Free Pascal declară înregistrarea de creare a pensulei cu structura de pensulă de la execuție înglobată în ea, iar înregistrarea stiloului extins cu structura stiloului de la execuție înglobată în ea. Ambele structuri de execuție își declară membrul hatch ca un întreg de mărimea pointerului, deoarece într-un apel GDI real acel membru poate transporta un handle. Un metafișier, în schimb, stochează întotdeauna forma pe 32 de biți, deoarece aspectul înregistrării face parte din formatul de fișier serializat și nu se schimbă odată cu bitness-ul procesului

Pe build-urile pe 32 de biți cele două coincid și nu se întâmplă nimic. Pe Win64 membrul de mărimea pointerului are opt octeți acolo unde fișierul are patru, deci fiecare câmp de după membrul hatch este citit de la offset-ul greșit. Nu există nicio excepție, nicio eroare de parsare și niciun avertisment. Metafișierul pur și simplu se randează greșit: culori din octeții greșiți, lățimi de stilou din octeții greșiți și o imagine care pare un bug de randare, nu un bug de aspect al structurii. Delphi livrează variante explicit pe 32 de biți pentru ambele structuri tocmai din acest motiv, iar unitatea de compatibilitate le redeclară la fel

Aspectul în octeți al înregistrării de pensulă EMF, arătând un câmp hatch de mărimea pointerului care deplasează câmpurile ulterioare cu patru octeți pe Win64 față de aspectul fix pe 32 de biți
Înregistrarea serializată stochează întotdeauna un hatch pe 4 octeți, deci structura de execuție de mărimea pointerului citește greșit în tăcere fiecare câmp ulterior pe Win64
// Greșit pe Win64: Hatch este de mărimea pointerului, fișierul stochează
// 32 de biți, iar fiecare câmp următor se deplasează cu patru octeți fără eroare
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 octeți într-un proces pe 64 de biți
  end;

// Corect: aspectul serializat, lățime fixă indiferent de bitness
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // întotdeauna 4 octeți, așa cum este stocat în metafișier
  end;

Regula generală: orice structură care apare atât ca argument de API la execuție, cât și ca aspect de câmp serializat are nevoie de două declarații, iar cea serializată trebuie să folosească tipuri cu lățime fixă peste tot. Membrii de mărimea pointerului într-un format de fișier sunt întotdeauna un bug care așteaptă un build pe 64 de biți

Diferențele de semnătură aparțin unui wrapper, nu fiecărui punct de apel

Diferențele rămase erau nepotriviri obișnuite de semnătură, iar modul de a le absorbi este un wrapper de redirecționare, nu o condițională la fiecare punct de apel. Funcția de combinare a transformărilor primește pointeri sub Free Pascal, acolo unde Delphi primește parametri prin referință, deci wrapper-ul preia referințe și transmite adrese. De asemenea, copiază mai întâi ambele argumente sursă în variabile locale, deoarece convertorul are puncte de apel în care matricea destinație este simultan una dintre surse, iar transmiterea aceleiași adrese de două ori către o funcție care scrie pe măsură ce citește produce o transformare subtil greșită, într-un mod care se vede doar pe conținut rotit

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Copiere mai întâi: apelanții transmit în mod legitim Dest ca A sau B
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

Tipurile de dreptunghi și punct sunt celălalt caz. Free Pascal tratează înregistrările de dreptunghi și punct ale metafișierului drept tipuri distincte de cele grafice generale, deci opt puncte de atribuire au avut nevoie de un cast explicit între înregistrări cu aspect identic. Ambele compilatoare acceptă forma cu cast, deci acele puncte nu poartă nicio condițională, ceea ce merită puțină urâțenie

Ce schimbă acest lucru pentru o instalare Free Pascal

Importul vectorial EMF funcționează pe Windows sub Free Pascal, producând același conținut de pagină ca build-ul Delphi: trasee ca trasee, gradienți ca conținut de tipar, text ca text. În afara Windows, calea raster rămâne răspunsul, iar aceasta este o limitare a formatului, nu a portării. Starea de coordonate și decupare în care alimentează convertorul este descrisă în articolul despre trackerul CTM și decupare al content stream, iar primitivele vectoriale emise sunt tratate în grafică vectorială, shaders și gradienți

Dacă auditați propria bază de cod pentru aceeași oportunitate, exercițiul util este cel care a declanșat totul: listați membrii pe care îi folosiți efectiv din framework-ul de care credeți că depindeți. Răspunsul este adesea mult mai scurt decât sugerează lista de import, iar restricția reală se află de obicei în cu totul altă parte. Căile de import bazate pe context de dispozitiv sunt descrise în general în articolul despre print preview și context de dispozitiv, iar acoperirea de platforme și toolchain este listată pe pagina de produs losLab PDF Developer Library