Artykuł techniczny

Import grafiki wektorowej EMF na Free Pascal z PDFlibPas

PDFlibPas konwertuje rozszerzone metafili na prawdziwą zawartość stron PDF rekord po rekordzie, zamiast ich rastrować, i właśnie dlatego zaimportowany wykres albo rysunek CAD pozostaje ostry przy każdym powiększeniu. Ten konwerter ma około 6500 linii i powstał pod kątem VCL, więc gdy biblioteka zyskała cel Free Pascal, uznano go za nieprzenośny i zamieniono na stub. Ta klasyfikacja okazała się błędna, a sposób, w jaki była błędna, to użyteczna lekcja, jak audytować zależność, zanim podejmiesz decyzję o przepisywaniu kodu wokół niej

Rzeczywista powierzchnia VCL w tych 6500 liniach okazała się niewielka: klasa bitmapy używana dla formatu pikseli, zapisu do strumienia, uchwytu, canvasu i linii skanowania; klasa metafila używana dla szerokości, wysokości i uchwytu; oraz typ koloru z dwiema stałymi. Każdą z tych rzeczy dostarczała już własna jednostka graficzna biblioteki, która istnieje właśnie po to, by build bez VCL miał odpowiedniki. Konwerter w ogóle nie blokował się na VCL. Blokował się na jednostce Windows z Free Pascala

Podziel wzdłuż osi, od której kod faktycznie zależy

Zmiana nie była więc reimplementacją. Był to jeden warunek: z „kompiluj stub, gdy build idzie bez VCL” na „kompiluj stub, gdy nie budujesz pod Windows”. To właściwa oś, a wskazanie dlaczego robi różnicę oczywistą. Rozszerzony metafil jest kontenerem Windows. Konwerter jest parserem rekordów GDI Windows od pierwszej do ostatniej linii. To, czy aplikacja gospodarza używa VCL, innego zestawu widgetów albo żadnego, nie ma nic wspólnego z tym, czy te rekordy dają się zinterpretować; to, czy celem jest Windows, ma z tym wszystko do czynienia

Konsekwencje trafnego wyboru osi przychodzą same. Buildy C++Builder, które w tej bibliotece oddefiniowują symbol platformy Windows, zachowują stub zgłaszający wyjątek i działają dokładnie jak wcześniej. macOS zachowuje stub, słusznie, bo nie ma tam rekordów GDI do sparsowania. Buildy Delphi z VCL zostają nietknięte. A build Windows z zestawem widgetów innym niż VCL zyskuje wektorowy import EMF przy okazji, którego nikt nie musiał implementować. Warunek wyrównany z rzeczywistą zależnością zamienia pracę platformową w zmianę jednej linii; warunek wyrównany ze złą zamienia ją w przepisywanie, które nigdy nie trafia do harmonogramu

Warunek importu EMF przepięty z przynależności do VCL na platformę Windows, z zachowaniem stubów gdzie indziej i wektorowym importem dla buildów Windows bez VCL
Przepięcie warunku stubu na platformę Windows zachowuje zachowanie wszystkich istniejących buildów i daje celom Windows bez VCL wektorowy import EMF bez dodatkowej pracy

Luka Free Pascala dotyczyła deklaracji, nie logiki

Na braku znalazły się deklaracje Win32, które dostarcza jednostka Windows z Delphi, a nie dostarcza jednostka z Free Pascala. Zebranie ich w jednej jednostce kompatybilności zamiast rozsypywania warunków po konwerterze utrzymało parser czytelnym. Lista jest pouczająca, bo pokazuje, jak nierównomierne jest pokrycie nagłówków między tymi dwoma RTL: 113 stałych typów rekordów metafila, dwie flagi rozszerzonego wyjścia tekstowego, trzy stałe trybu wypełniania gradientem, typ wskaźnika tablicy uchwytów, aliasy dla rekordów wierzchołków gradientu i prymitywów oraz trzy typy rekordów, których Free Pascal w ogóle nie deklaruje, obejmujące mieszanie alfa, przezroczyste blitowanie i tryb zarządzania kolorem

Żaden z tych elementów nie jest ciekawy osobno. Wszystkie muszą być poprawne, zanim parser się skompiluje, a jednostka kompatybilności jest ich naturalnym domem, bo można ją porównać diffem z dokumentacją nagłówków w całości

Deklaracje Win32 nieobecne w jednostce Windows Free Pascala, zebrane w jedną jednostkę kompatybilności dla wektorowego konwertera EMF do PDF
Stałe rekordów, flagi, aliasy i trzy nieobecne typy rekordów żyją w jednej jednostce kompatybilności, którą można porównać diffem z dokumentacją nagłówków

Ten, który po cichu rysuje zły obraz

Dwie z tych deklaracji nie tylko nie istnieją — są obecne i złe na ten cel, i to jest część warta zapamiętania, nawet jeśli nigdy nie tkniesz metafila

Free Pascal deklaruje rekord tworzenia pędzla z osadzoną w nim strukturą pędzla czasu wykonania, a rekord rozszerzonego pióra z osadzoną strukturą pióra czasu wykonania. Obie te struktury czasu wykonania deklarują pole hatch jako liczbę całkowitą wielkości wskaźnika, bo w żywym wywołaniu GDI pole to może nieść uchwyt. Metafil zawsze jednak przechowuje formę 32-bitową, bo układ rekordu jest częścią serializowanego formatu pliku i nie zmienia się wraz z bitowością procesu

W buildach 32-bitowych obie się zgadzają i nic się nie dzieje. Na Win64 pole wielkości wskaźnika ma osiem bajtów tam, gdzie plik ma cztery, więc każde pole po polu hatch jest czytane ze złego offsetu. Nie ma wyjątku, błędu parsowania ani ostrzeżenia. Metafil po prostu renderuje się źle: kolory ze złych bajtów, szerokości piór ze złych bajtów i obraz, który wygląda jak błąd renderowania, a nie błąd układu struktury. Delphi dostarcza jawnie 32-bitowe warianty obu struktur dokładnie z tego powodu, a jednostka kompatybilności redeklaruje je tak samo

Układ bajtów rekordu pędzla EMF pokazujący, że pole hatch wielkości wskaźnika przesuwa pola późniejsze o cztery bajty na Win64 względem stałego układu 32-bit
Serializowany rekord zawsze przechowuje 4-bajtowy hatch, więc struktura czasu wykonania wielkości wskaźnika po cichu błędnie czyta każde późniejsze pole na Win64
// Źle na Win64: Hatch ma rozmiar wskaźnika, plik przechowuje 32 bity,
// a każde kolejne pole przesuwa się o cztery bajty bez żadnego błędu
type
  TLogBrushRuntime = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: ULONG_PTR;      // 8 bajtów w procesie 64-bitowym
  end;

// Dobrze: układ serializowany, stała szerokość niezależnie od bitowości
type
  TLogBrush32 = record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: DWORD;          // zawsze 4 bajty, tak jak zapisano w metafilu
  end;

Reguła ogólna: każda struktura, która występuje i jako argument API czasu wykonania, i jako układ pól serializowanych, potrzebuje dwóch deklaracji, a serializowana musi używać typów o stałej szerokości na całej długości. Pola wielkości wskaźnika w formacie pliku to zawsze błąd czekający na build 64-bitowy

Różnice sygnatur należą do opakowania, nie do każdego miejsca wywołania

Pozostałe różnice to zwykłe niezgodności sygnatur, a sposób ich wchłonięcia to opakowanie przekazujące, a nie warunek w każdym miejscu wywołania. Funkcja łącząca transformacje przyjmuje wskaźniki pod Free Pascalem, tam gdzie Delphi przyjmuje parametry referencyjne, więc opakowanie przyjmuje referencje i przekazuje adresy. Kopiuje też najpierw oba argumenty źródłowe do zmiennych lokalnych, bo konwerter ma miejsca wywołań, w których macierz docelowa jest jednocześnie jednym ze źródeł, a przekazanie tego samego adresu dwukrotnie do funkcji, która pisze, jak czyta, daje transformację subtelnie złą w sposób ujawniający się dopiero na obróconej treści

function CombineTransformCompat(var Dest: TXForm;
  const A, B: TXForm): BOOL;
var
  SrcA, SrcB: TXForm;
begin
  // Najpierw kopia: wywołujący w pełni legalnie przekazują Dest jako A lub B
  SrcA := A;
  SrcB := B;
{$IFDEF FPC}
  Result := Windows.CombineTransform(@Dest, @SrcA, @SrcB);
{$ELSE}
  Result := Windows.CombineTransform(Dest, SrcA, SrcB);
{$ENDIF}
end;

Typy prostokąta i punktu to drugi przypadek. Free Pascal traktuje rekordy prostokąta i punktu metafila jako odrębne typy względem ogólnych graficznych, więc osiem miejsc przypisania potrzebowało jawnego rzutowania między rekordami o identycznym układzie. Oba kompilatory akceptują formę z rzutowaniem, więc te miejsca nie niosą żadnego warunku, co warte jest odrobiny brzydoty

Co to zmienia dla wdrożenia na Free Pascalu

Wektorowy import EMF działa na Windows pod Free Pascalem, produkując tę samą zawartość stron co build Delphi: ścieżki jako ścieżki, gradienty jako zawartość wzorca, tekst jako tekst. Poza Windows odpowiedzią pozostaje ścieżka rastrowa i to jest ograniczenie formatu, a nie portu. Stan współrzędnych i przycinania, który konwerter zasiluje, opisuje artykuł o śledzeniu CTM i przycinania w strumieniu zawartości, a emitowane prymitywy wektorowe omawia grafika wektorowa, shadery i gradienty

Jeśli audytujesz własną bazę kodu pod kątem tej samej okazji, użytecznym ćwiczeniem jest to, które to wszystko zapoczątkowało: wypisz elementy, których faktycznie używasz z frameworka, od którego, jak myślisz, zależysz. Odpowiedź jest często znacznie krótsza, niż sugeruje lista importów, a rzeczywiste ograniczenie zwykle leży gdzie indziej całkowicie. Ścieżki importu oparte na kontekście urządzenia opisuje ogólnie artykuł o podglądzie wydruku i kontekście urządzenia, a pokrycie platform i toolchainów jest wypisane na stronie produktu losLab PDF Developer Library