Artykuł techniczny

Import wektorów EMF i WMF do PDF w Delphi za pomocą HotPDF

HotPDF, natywny komponent PDF dla Delphi i C++Buildera, importuje metapliki Windows EMF i WMF, interpretując każdy rekord GDI bezpośrednio jako operatory PDF, zamiast spłaszczać plik do bitmapy: wypełnienia gradientowe stają się wzorcami cieniowania osiowego PDF, pędzle kreskowane stają się wzorcami kafelkowymi PDF, a scentralizowana bramka stanu ścieżki blokuje wadliwe rekordy przed zniekształceniem wyjścia. Każdy wykres, który TChart, powierzchnia GDI+ lub zwykły TCanvas potrafi wyeksportować jako rozszerzony metaplik, jest kandydatem do tej ścieżki, a różnica ujawnia się w chwili, gdy ktoś powiększy stronę albo wyśle ją na drukarkę wysokiej rozdzielczości

Alternatywą, po którą większość programistów Delphi sięga domyślnie, jest zrasteryzowanie metapliku do bitmapy przed umieszczeniem go na stronie, a koszt ujawnia się dopiero później: wykres słupkowy, który na ekranie był ostry, staje się widocznie klockowaty w chwili wydruku PDF przy 600 DPI albo wyświetlenia na ekranie w sali konferencyjnej, a kreskowany obszar CAD zapada się do jednego płaskiego szarego prostokąta, jeśli styl wypełnienia nie zostanie zachowany. Odczytanie metapliku jako programu, a nie jako obrazka, pozwala uniknąć obu problemów i jest trudniejszą ścieżką do poprawnego wdrożenia, dlatego poniższe pułapki warto znać, zanim raport trafi do klienta

Dlaczego interpretować metaplik zamiast spłaszczać go do bitmapy?

HotPDF trzyma import EMF i WMF na ścieżce wektorowej, ponieważ metaplik Windows to zarejestrowana sekwencja wywołań rysujących GDI, a nie obrazek, a odtworzenie tych wywołań jako operatorów ścieżki, tekstu i cieniowania PDF jest tym, co pozwala wynikowi skalować się tak samo jak reszta strony. THPDFPage.ShowMetafile i jego odpowiednik ShowMetafileEx to punkty wejścia wywoływane przez aplikację, a oba przekazują metaplik do THPDFWmf, klasy, która przechodzi przez każdy rekord GDI i go tłumaczy. Rozróżnienie nie jest absolutne i HotPDF wcale tego nie udaje: rekord metapliku, który jest rzeczywiście danymi rastrowymi, na przykład blit bitmapy StretchDIBits, jest osadzany jako prawdziwy obraz PDF Image XObject przez AddImage i ShowImage, tę samą parę wywołań, przez którą przechodzi każdy inny obrazek na stronie, zamiast na siłę zamieniać go na operatory ścieżki, które nie potrafią wyrazić fotografii. Linie, wypełnienia i tekst pozostają wektorowe; piksele, które w źródle były już pikselami, pozostają pikselami w wyniku. Najprostsze wywołanie nie wymaga niczego poza wczytanym metaplikiem:

var
  Pdf: THotPDF;
  Chart: TMetafile;
begin
  Pdf := THotPDF.Create(nil);
  Chart := TMetafile.Create;
  try
    Chart.LoadFromFile('quarterly-revenue.emf');  // exported from TChart or GDI+
    Pdf.FileName := 'quarterly-report.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafile(Chart);
    Pdf.EndDoc;
  finally
    Chart.Free;
    Pdf.Free;
  end;
end;

Jak interpreter przekształca współrzędne GDI na przestrzeń strony PDF?

HotPDF odpowiada na to jednym przejściem po własnym strumieniu rekordów metapliku, zamiast tworzyć drugą implementację GDI. THPDFWmf.Analyse odczytuje nagłówek metapliku przez wywołanie Win32 GetEnhMetaFileHeader, resetuje swój wewnętrzny stan rysowania i wywołuje EnumEnhMetafile, to samo API enumeracyjne, którego użyłaby przeglądarka metaplików, więc każdy rekord EMR_* trafia do THPDFWmf.ExecuteRecord w kolejności, w jakiej został pierwotnie zarejestrowany. GDI wyraża współrzędne odgórnie (top-down) w jednostkach urządzenia lub logicznych wybranych przez własny tryb mapowania metapliku; strona PDF jest oddolna (bottom-up), w punktach przestrzeni użytkownika — układ współrzędnych opisany w modelu rysowania kanwy HotPDF dla ścieżek i wypełnień. Każdy program obsługi rekordu rozwiązuje tę rozbieżność przez ScaleX i ScaleY, które wywołują ProjectX i ProjectY, odtwarzające własny wzór GDI okno-do-widoku dla anizotropowych i izotropowych trybów mapowania, więc kształt zarejestrowany o szerokości pięciu jednostek logicznych ląduje z poprawną szerokością w punktach PDF, niezależnie od tego, jakie zasięgi okna i widoku ustawiła aplikacja źródłowa

Jak gradientowe wypełnienie GDI staje się wzorcem cieniowania PDF?

Rekord EMR_GRADIENTFILL staje się prawdziwym osiowym wzorcem cieniowania PDF typu 2 (ISO 32000-1 §8.7.4.5), ilekroć GDI zarejestrowało go w jednym z dwóch trybów prostokątnych. THPDFWmf.VEMRGradientFill odczytuje własny układ rekordu bezpośrednio z surowego bufora bajtów, zgodnie ze strukturą MS-EMF §2.3.1.6: tablica wierzchołków 16-bitowych kolorów RGBA, po której następuje lista prostokątów, z których każdy odwołuje się do dwóch z tych wierzchołków. Dla GRADIENT_FILL_RECT_H kolory przechodzą od lewej do prawej wzdłuż poziomej linii środkowej prostokąta; dla GRADIENT_FILL_RECT_V przechodzą od góry do dołu wzdłuż pionowej linii środkowej. W obu przypadkach dwa kolory narożne i rzutowane współrzędne prostokąta trafiają wprost do THotPDF.RegisterAxialGradient, które zwraca nazwę wzorca, a strona rysuje prostokąt i wypełnia go przez ten wzorzec (SetFillPattern) zamiast płaskim wywołaniem SetRGBFillColor, więc pasmowy nagłówek w stylu arkusza kalkulacyjnego albo gradientowy obszar wykresu zachowuje swoje przejście barwne zamiast zapadać się do jednego uśrednionego koloru

Tryb trójkątów Gourauda to szczera luka. Gdy pole ulMode rekordu zgłasza GRADIENT_FILL_TRIANGLE, VEMRGradientFill rozpoznaje to, loguje, że tryb trójkątów nie jest jeszcze zaimplementowany, i pomija prostokąt, zamiast zgadywać dwukolorowe przybliżenie. Interpolacja na wierzchołek, na piksel w dowolnej siatce trójkątów nie sprowadza się do dwuprzystankowego cieniowania osiowego ani promienistego, a poprawne jej wyrażenie oznaczałoby emisję cieniowania siatkowego PDF typu 4 lub 5 — tej samej rodziny cieniowań, którą renderer stron HotPDF również pozostawia niewymalowaną przy odczycie PDF z powrotem. Dwie niezwiązane ze sobą ścieżki kodu trafiają na tę samą granicę: cieniowania siatkowe to luka zarówno po stronie zapisu, jak i odczytu, a diagram źródłowy, który używa trójkątów Gourauda do gładkiej poświaty promienistej, cofa się do tego, jaki był ostatni pędzel jednolity, a nie do renderowanego przybliżenia

Pędzle kreskowane stają się wzorcami kafelkowymi, nie spłaszczoną szarością

Pędzel kreskowany GDI zachowuje swoją teksturę w PDF, ponieważ THPDFWmf.SetBrushColor sprawdza CurrentBrush.lbStyle pod kątem BS_HATCHED, zanim jeszcze w ogóle cofnie się do wypełnienia jednolitego, kierując ten przypadek do SetHatchBrushPattern. Ta metoda zapisuje strumień treści PDF o rozmiarze 8 na 8 jednostek złożony z operatorów kreślonej linii — m, l i S — dobranych według stylu kreskowania GDI: pojedyncza kreska pozioma lub pionowa dla HS_HORIZONTAL i HS_VERTICAL, trzy równoległe przekątne dla HS_FDIAGONAL i HS_BDIAGONAL oraz kombinacje pozioma-plus-pionowa lub obie przekątne dla HS_CROSS i HS_DIAGCROSS. THotPDF.RegisterTilingPattern rejestruje ten strumień treści jako kolorowy wzorzec kafelkowy (PaintType 1, ISO 32000-1 §8.7.3.1) z 8-jednostkowym XStep i YStep, a strona wypełnia go przez SetFillPattern tak samo, jak robi to cieniowanie osiowe. Rzut kondygnacji CAD albo rysunek techniczny, który opiera się na kreskowaniach do rozróżniania materiałów, zachowuje ten język wizualny w PDF, zamiast tracić każdy obszar na rzecz identycznej szarości

Nie każdy pędzel zasługuje na takie traktowanie, i warto znać tę lukę, zanim import CAD trafi do produkcji. EMR_CREATEDIBPATTERNBRUSHPT, rekord dla niestandardowego pędzla wzorca bitmapowego, a nie jednego z sześciu standardowych stylów kreskowania GDI, jedynie rejestruje swój uchwyt, aby późniejsze rekordy SELECTOBJECT i DELETEOBJECT pozostały spójne; HotPDF nie udostępnia jeszcze potoku zasobów PDF Pattern dla dowolnych obrazów kafelkowych, więc wybranie tego pędzla przechodzi w dół do zastępczego wypełnienia jednolitym kolorem zamiast tekstury źródłowej. Jeśli wypełnienie renderuje się płasko tam, gdzie oryginał wyraźnie używał powtarzającej się tekstury obrazu, pędzel źródłowy niemal na pewno jest niestandardowym wzorcem DIB, a nie standardowym kreskowaniem, i to właśnie ten jeden przypadek warto najpierw sprawdzić ręcznie. Konfigurowanie importu dla takiego rysunku nadal przebiega przez ten sam obiekt opcji:

var
  Pdf: THotPDF;
  Drawing: TMetafile;
  Options: THPDFEmfOptions;
begin
  Pdf := THotPDF.Create(nil);
  Drawing := TMetafile.Create;
  Options := THPDFEmfOptions.Create;
  try
    Drawing.LoadFromFile('floor-plan.emf');
    Options.Assign(Pdf.EmfOptions);   // start from the document-wide defaults
    Options.Redraw := False;          // interpret the original EMF bytes, no GDI re-record pass
    Options.ShowNullBrush := True;    // keep explicitly unfilled CAD regions visible
    Options.UseFrame := True;         // clip output to the frame the EMF header declares
    Pdf.FileName := 'floor-plan.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
    Pdf.EndDoc;
  finally
    Options.Free;
    Drawing.Free;
    Pdf.Free;
  end;
end;

Co powstrzymuje wadliwy metaplik przed zepsuciem strony?

Odpowiedzią HotPDF jest jedna bramka na szczycie ExecuteRecord, zamiast defensywnego sprawdzenia powtarzanego w każdym z około osiemdziesięciu programów obsługi rekordów. Nawias ścieżki GDI, otwierany przez EMR_BEGINPATH i zamykany przez EMR_ENDPATH lub EMR_ABORTPATH, jest śledzony przez prywatną właściwość PathContinue wspieraną przez pole FPathContinue. Dopóki ten nawias jest otwarty, ExecuteRecord przepuszcza wyłącznie rekordy budujące ścieżkę: warianty przesunięcia, linii, polilinii, wielokąta, polibeziera i polirysowania, a także CLOSEFIGURE oraz niewielki zbiór rekordów transformacji i stanu kontekstu urządzenia, takich jak SETWORLDTRANSFORM, SAVEDC i RESTOREDC. Każdy inny typ rekordu docierający do ExecuteRecord przy otwartym nawiasie, na przykład zabłąkany EXTTEXTOUT albo blit bitmapy, jest odrzucany centralnie jednym Exit w chwili nadejścia

Ta bramka istnieje, ponieważ nawias ścieżki w ręcznie napisanym, wygenerowanym przez narzędzie albo po prostu uszkodzonym metapliku nie ma gwarancji, że zawiera wyłącznie to, co dobrze sformowany plik umieściłby między swoimi rekordami otwierającym a zamykającym. Rekord wyprowadzania tekstu, który wylądowałby między EMR_BEGINPATH a EMR_ENDPATH, bez bramki albo zanieczyściłby budowaną geometrię ścieżki, albo wyemitowałby operator wyświetlania tekstu PDF w środku sekwencji, która ma być czystym budowaniem ścieżki, a oba te tryby awarii to rodzaj błędu, który ujawnia się na jednym wadliwym wejściu z narzędzia zewnętrznego, a nie na czymś, co zwykły zestaw testów akurat pokrywa. Scentralizowanie tego sprawdzenia w ExecuteRecord oznacza, że poszczególne programy obsługi VEMR* nie muszą się każdy z osobna bronić przed wywołaniem w niewłaściwym momencie; bramka decyduje o tym raz, przed dyspozycją, zamiast osiemdziesiąt razy po niej

Umieszczanie wykresu wektorowego obok tekstu i obrazów na jednej stronie

Strona raportu rzadko zawiera wyłącznie wykres, a ShowMetafile współdziała z innymi operatorami stron HotPDF dokładnie tak, jak każde inne wywołanie rysujące. Nagłówek narysowany przez TextOut, kreskowany wykres słupkowy zaimportowany jako EMF i logo umieszczone przez ShowImage mogą wszystkie wylądować na tej samej stronie w tym samym strumieniu treści, każde zachowując swoją natywną wierność — wzorzec kompozycji opisany w przewodniku HotPDF po układaniu tekstu, czcionek i obrazów w raporcie:

Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart);   // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);

Interpreter EMF i WMF, wzorce cieniowania osiowego, które rejestruje dla wypełnień gradientowych, oraz mapowanie na wzorce kafelkowe dla pędzli kreskowanych opisane tutaj — wszystko to jest częścią standardowego komponentu HotPDF dla Delphi i C++Buildera, natywnej biblioteki VCL bez żadnej zewnętrznej zależności DLL