Technický článek

Import vektorů EMF ve Free Pascalu s PDFlibPas

PDFlibPas převádí rozšířené metasoubory na skutečný obsah stránek PDF záznam po záznamu, namísto jejich rasterizace, a právě proto zůstává importovaný graf nebo CAD výkres ostrý při libovolném přiblížení. Tento konvertor má kolem 6500 řádků a byl napsán proti VCL, takže když knihovna získala cíl Free Pascal, byl označen za nepřenositelný a nahrazen stubem. Toto označení bylo chybné a způsob, jakým bylo chybné, je užitečnou lekcí o tom, jak auditovat závislost dříve, než se rozhodnete psát kolem ní znovu

Skutečný povrch VCL těchto 6500 řádků se ukázal jako malý: třída bitmapy využívaná pro svůj pixelový formát, ukládání do streamu, handle, plátno a scanlines; třída metasouboru využívaná pro šířku, výšku a handle; a barevný typ se dvěma konstantami. Všechny tyto položky už poskytovala vlastní grafická jednotka knihovny, která existuje právě proto, aby sestavení bez VCL měla ekvivalenty. Konvertor nebyl na VCL nijak blokován. Blokovala ho jednotka Windows ve Free Pascalu

Rozdělte podle osy, na které kód skutečně závisí

Změna tedy nebyla reimplementací. Byla to jediná podmínka: z „kompilovat stub, když se sestavuje bez VCL“ na „kompilovat stub, když se nesestavuje pro Windows“. To je správná osa a vyslovení důvodu činí rozdíl zjevným. Rozšířený metasoubor je kontejner Windows. Konvertor je shora dolů parser záznamů Windows GDI. To, zda hostitelská aplikace používá VCL, jinou sadu widgetů nebo žádnou, s tím, zda lze tyto záznamy interpretovat, nesouvisí vůbec; to, zda cílem je Windows, souvisí se vším

Důsledky správné volby osy přicházejí samy. Sestavení C++Builder, která v této knihovně nedefinují symbol platformy Windows, si ponechávají stub vyhazující výjimku a chovají se přesně jako dříve. macOS stub správně drží, protože zde nejsou žádné záznamy GDI k parsování. Sestavení Delphi VCL se nedotčena. A sestavení Windows se sadou widgetů mimo VCL získává vektorový import EMF jako vedlejší efekt, který nikdo nemusel implementovat. Podmínka zarovnaná se skutečnou závislostí mění práci s platformami na změnu jednoho řádku; podmínka zarovnaná se špatnou z ní dělá přepis, na který se nikdy nedostane fronta

Podmínka importu EMF přesunuta z příslušnosti k VCL na platformu Windows; stuby zůstávají jinde a sestavení Windows bez VCL získávají vektorový import
Přesunutí podmínky stubu na platformu Windows zachovává chování všech stávajících sestavení a dává cílům Windows bez VCL vektorový import EMF bez práce

Mezera ve Free Pascalu byla v deklaracích, nikoli v logice

Skutečně chyběly deklarace Win32, které poskytuje jednotka Windows v Delphi a neposkytuje jednotka ve Free Pascalu. Shromáždění do jediné kompatibilní jednotky namísto rozhazování podmínek konvertorem udrželo parser čitelný. Seznam je poučný, protože ukazuje, jak nerovnoměrné je pokrytí hlaviček mezi oběma RTL: 113 konstant typů záznamů metasouboru, dva příznaky rozšířeného textového výstupu, tři konstanty režimu gradient fill, typ ukazatele tabulky handle, aliasy pro záznamy gradientních vrcholů a primitiv a tři typy záznamů, které Free Pascal nedeklaruje vůbec, pokrývající alpha blending, transparentní blitting a režim správy barev

Žádná z těch položek není sama o sobě zajímavá. Všechny musí být správně, než se parser zkompiluje, a kompatibilní jednotka je přirozené místo, protože ji lze porovnat s dokumentací hlaviček jako celek

Deklarace Win32 chybějící v jednotce Windows Free Pascalu, shromážděné do jedné kompatibilní jednotky pro vektorový konvertor EMF do PDF
Konstanty záznamů, příznaky, aliasy a tři chybějící typy záznamů žijí všechny v jedné kompatibilní jednotce porovnatelné s dokumentací hlaviček

Ten, který potichu kreslí špatný obrázek

Dvě z těchto deklarací nejen že chybí, jsou přítomné a pro tento účel chybné, a právě tato část stojí za zapamatování, i kdybyste se metasouboru nikdy nedotkli

Free Pascal deklaruje záznam pro vytváření štětce s vloženou runtime strukturou štětce a záznam rozšířeného pera s vloženou runtime strukturou pera. Obě tyto runtime struktury deklarují svůj člen hatch jako celé číslo velikosti ukazatele, protože v živém volání GDI může tento člen nést handle. Metasoubor však vždy ukládá 32bitovou podobu, protože rozložení záznamu je součástí serializovaného formátu souboru a nemění se s bitovou šířkou procesu

Na 32bitových sestaveních se obě shodnou a nic se neděje. Na Win64 má člen velikosti ukazatele osm bajtů tam, kde soubor má čtyři, takže každé pole za členem hatch se čte z nesprávného offsetu. Žádná výjimka, žádná chyba parsování, žádné varování. Metasoubor se prostě vykreslí chybně: barvy ze špatných bajtů, šířky per ze špatných bajtů a obrázek, který vypadá jako chyba vykreslování, nikoli jako chyba rozložení struktury. Delphi právě z tohoto důvodu dodává explicitní 32bitové varianty obou struktur a kompatibilní jednotka je deklaruje stejným způsobem

Bajtové rozložení záznamu štětce EMF ukazující, že pole hatch velikosti ukazatele posouvá následující pole o čtyři bajty na Win64 oproti pevnému 32bitovému rozložení
Serializovaný záznam vždy ukládá 4bajtový hatch, takže runtime struktura velikosti ukazatele potichu špatně čte všechna následující pole na Win64
// Na Win64 špatně: Hatch má velikost ukazatele, soubor ukládá
// 32 bitů, a každé následující pole se posune o čtyři bajty bez chyby
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 bajtů v 64bitovém procesu
  end;

// Správně: serializované rozložení, pevná šířka bez ohledu na bitovou šířku
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // vždy 4 bajty, tak jak je uloženo v metasouboru
  end;

Obecné pravidlo: každá struktura, která se objevuje jak jako runtime argument API, tak jako serializované rozložení polí, potřebuje dvě deklarace a ta serializovaná musí používat typy s pevnou šířkou od začátku do konce. Členy velikosti ukazatele ve formátu souboru jsou vždy chyba čekající na 64bitové sestavení

Rozdíly signatur patří do wrapperu, ne ke každému místu volání

Zbývající rozdíly byly běžné nesouhlasy signatur a způsobem, jak je pohltit, je forwarding wrapper namísto podmínky na každém místě volání. Funkce kombinující transformace bere pod Free Pascalem ukazatele tam, kde je Delphi bere referenčními parametry, takže wrapper přijímá reference a předává adresy. Navíc nejprve zkopíruje oba zdrojové argumenty do lokálních proměnných, protože konvertor má místa volání, kde cílová matice je zároveň jedním ze zdrojů, a předání téže adresy dvakrát funkci, která zapisuje, jak čte, produkuje transformaci jemně chybnou způsobem, který se projeví pouze na otočeném obsahu

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Nejprve kopie: volající legitimně předávají Dest jako A nebo B
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

Typy obdélníku a bodu jsou druhý případ. Free Pascal chápe záznamy obdélníku a bodu metasouboru jako odlišné typy od obecných grafických, takže osm míst přiřazení potřebovalo explicitní přetypování mezi záznamy identického rozložení. Oba kompilátory přijímají formu s přetypováním, takže tato místa nenese žádnou podmínku, což stojí za trochu ošklivosti

Co to mění pro nasazení ve Free Pascalu

Vektorový import EMF funguje na Windows pod Free Pascalem a produkuje stejný obsah stránek jako sestavení Delphi: cesty jako cesty, přechody jako obsah vzorků, text jako text. Mimo Windows zůstává odpovědí rastrová cesta a to je limit formátu, nikoli portu. Souřadnicový a ořezový stav, do něhož konvertor zapisuje, popisuje článek o sledování CTM a ořezů ve content streamu a vektorové primitivy, které vydává, pokrývá vektorová grafika, shadery a přechody

Pokud auditujete vlastní kódovou základnu za stejnou příležitostí, užitečné cvičení je to, které to všechno odstartovalo: vyjmenujte členy, které ze frameworku, o němž si myslíte, že na něm závisíte, skutečně používáte. Odpověď bývá mnohem kratší, než naznačuje seznam importů, a skutečné omezení je obvykle úplně jinde. Importní cesty založené na device contextu obecně popisuje článek o náhledu tisku a device contextu a pokrytí platforem a toolchainů uvádí stránka produktu losLab PDF Developer Library