PDFlibPas, biblioteka PDF losLab dla Delphi, zamienia rekordy EMF Poly* na ścieżki PDF, trzymając się definicji każdego rekordu w [MS-EMF]: 32-bitowy EMR_POLYBEZIER startuje w punkcie 0, polilinie zostają otwarte i są tylko obrysowywane, PT_CLOSEFIGURE w EMR_POLYDRAW to flaga, a każda liczba punktów jest sprawdzana względem rozmiaru rekordu. Te reguły weszły w wersjach v3.539.39, v3.539.41 i v3.539.43. Wcześniej wykres z raportu mógł wyjść z ImportEMFFromFile z wypełnionym klinem tam, gdzie powinna być linia trendu, krzywą Beziera zgiętą w stronę złego punktu kontrolnego albo zamkniętym konturem bez ostatniego boku. Żaden z tych błędów nie zgłaszał wyjątku, a reguły dotyczą każdego konwertera EMF na PDF w Delphi i każdego parsera rekordów GDI
Dlaczego rekordy EMF Poly* psują się przy konwersji do PDF?
Rekordy EMF Poly* psują się, bo każdy z nich niesie część swojego znaczenia poza punktami: czy figura jest otwarta, czy startuje od bieżącej pozycji, jakie pióro i pędzel obowiązują i gdzie w rekordzie zaczynają się punkty. Rozszerzony metafil to zapis wywołań GDI względem kontekstu urządzenia, więc konwerter musi odtworzyć nie tylko współrzędne, ale i stan tego kontekstu. PDF nie ma kontekstu urządzenia. Ma ścieżkę, punkt bieżący wewnątrz tej ścieżki i operator rysujący, który rozstrzyga między obrysem (S), wypełnieniem (f) i jednym i drugim (B). Każda niezgodność obu modeli kończy się jako cicha różnica w renderingu
Rodzina Poly* występuje też w dwóch szerokościach. Każdy 32-bitowy rekord, taki jak EMR_POLYLINE, ma 16-bitowego bliźniaka, taki jak EMR_POLYLINE16, który trzyma punkty jako pary SmallInt. GDI zwykle zapisuje postać kompaktową, gdy każda współrzędna się mieści, więc 32-bitowe handlery konwertera potrafią latać z błędami latami, bo codzienne rysunki testowe do nich nigdy nie docierają. Najszybszy audyt to przepuszczenie tych samych punktów przez oba rekordy i porównanie wynikowych ścieżek. Rekordy omówione tutaj wszystkie należą do grupy rekordów rysujących w [MS-EMF] (2.3.5 Drawing Record Types)
| Rekord | Startuje w | Zamknięty? | Pozycja bieżąca |
|---|---|---|---|
EMR_POLYBEZIER | Punkt 0 | Nie | Nieużywana, nieaktualizowana |
EMR_POLYLINE | Punkt 0 | Nie (tylko pióro) | Nieużywana, nieaktualizowana |
EMR_POLYLINETO | Bieżąca pozycja | Nie (tylko pióro) | Używana i aktualizowana |
EMR_POLYPOLYLINE | Pierwszy punkt każdej polilinii | Nie (tylko pióro) | Nieużywana, nieaktualizowana |
EMR_POLYDRAW | Pierwszy PT_MOVETO albo bieżąca pozycja | Tylko tam, gdzie ustawiono PT_CLOSEFIGURE | Używana i aktualizowana |
Gdzie naprawdę zaczyna się krzywa EMR_POLYBEZIER?
Krzywa w EMR_POLYBEZIER startuje w punkcie 0, a dopiero punkty od indeksu 1 wzwyż są grupowane po trzy jako punkt kontrolny, punkt kontrolny, punkt końcowy. Rekord z 7 punktami rysuje więc dwa segmenty sześcienne: 0 to start, 1–3 tworzą pierwszy segment, 4–6 drugi. Handler 16-bitowy w PDFlibPas robił to już dobrze. Handler 32-bitowy zaczynał grupowanie od punktu 0, więc punkt startowy był zjadany jako pierwszy punkt kontrolny, a każdy dalszy segment przesuwał się o jeden. Krzywa się renderowała — tylko nie ta. Od v3.539.41 obie szerokości otwierają ścieżkę operatorem m w punkcie 0 i emitują jedno c na każdą kompletną trójkę po nim
Dla własnego parsera: liczba punktów, która nie jest równa 1 plus wielokrotność 3, oznacza uszkodzony rekord, a końcowe punkty należy zignorować, zamiast zszywać je w krzywą
PolyDraw: PT_CLOSEFIGURE to flaga, a nie typ punktu
W EMR_POLYDRAW wartość PT_CLOSEFIGURE (1) to bit łączony z PT_LINETO (2) albo PT_BEZIERTO (4), więc poprawny bajt typu może wynosić 3 albo 5. Typ punktu to bajt z zamaskowanym tym bitem, a flaga znaczy: zamknij figurę po segmencie kończącym się na tym punkcie. Stary handler PDFlibPas porównywał bajt z pojedynczymi wartościami w instrukcji case, więc punkty typu 3 i 5 niczego nie łapały i były w całości pomijane. Prostokąt rysowany przez PolyDraw tracił zamykający bok, a trójka Beziera, której ostatni punkt niósł flagę, traciła ten punkt, co wybijało z rytmu wszystkie dalsze trójki
Od v3.539.39 typ jest czytany jako Types[i] and not PT_CLOSEFIGURE, a zamknięcie jest emitowane dopiero po pełnym segmencie: po linii dla zamkniętego PT_LINETO i po trzecim punkcie grupy Beziera. Uszkodzony plik, który ustawi flagę na pierwszym albo drugim punkcie trójki, nie zamknie figury za wcześnie. W tym samym wydaniu pojechały dwie powiązane poprawki:
- Każdy
PT_MOVETOw 16-bitowymEMR_POLYDRAW16restartował całą ścieżkę, więc rekord trzymający trzy figury zachowywał tylko ostatnią; teraz pierwszy move startuje ścieżkę, a dalsze otwierają podścieżki - Rekord PolyDraw, który nie zaczyna się od
PT_MOVETO, startuje od bieżącej pozycji, zgodnie z definicją rekordu, zamiast pisać operatorlalbocbez poprzedzającegom
Dlaczego polilinii EMF nie wolno nigdy wypełniać w PDF?
Polilinii EMF nie wolno nigdy wypełniać, bo EMR_POLYLINE i EMR_POLYPOLYLINE to otwarte figury rysowane wyłącznie piórem, a wypełnienie otwartej ścieżki w PDF domyka ją z automatu. ISO 32000-1 §8.5.3 mówi wprost, że operatory wypełniania domykają każdą otwartą podścieżkę przed malowaniem. Konwerter, który wyemituje B albo f dla trzypunktowej polilinii, zamaluje więc wypełniony trójkąt aktualnym kolorem pędzla: dokładnie ten wypełniony klin pod linią trendu na wykresie. Przed v3.539.41 PDFlibPas wypełniał pędzlem obie szerokości polilinii, a rekord 32-bitowy był dodatkowo domykany wprost. Dziś obie szerokości kończą się samym obrysem, a rozróżnienie z GDI zostaje zachowane: Polygon domyka i wypełnia, Polyline nigdy
PolylineTo startuje od bieżącej pozycji
EMR_POLYLINETO rysuje od bieżącej pozycji przez każdy punkt rekordu, zostaje otwarta i zostawia bieżącą pozycję w ostatnim punkcie. Stary handler miał w dodatku szczególny przypadek, który wyłączał pióro, gdy pierwsze dwa punkty dzieliły współrzędną y — i nic go już nie włączało z powrotem, więc każdy dalszy rekord w pliku tracił obrys. Stan pióra należy do EMR_SELECTOBJECT i EMR_CREATEPEN; handler rekordu rysującego nie ma tu nic do gadania. Ten szczególny przypadek wylądował w koszu w v3.539.41, a forma rekordu z jednym punktem nie czyta już dalej poza własne punkty (naprawione w v3.539.39)
Punkty PolyPolyline zaczynają się za tablicą liczników
32-bitowy EMR_POLYPOLYLINE przechowuje nPolys liczników, a potem cptl punktów, przy czym punkty zaczynają się na bajtowym przesunięciu 32 + nPolys * 4. Pułapka siedzi w RTL: jednostka Windows deklaruje TEMRPolyPolyline z aPolyCounts i aptl jako tablicami jednoelementowymi, więc aptl[0] jest pierwszym punktem tylko przy nPolys równym 1. Kod indeksujący aptl bezpośrednio czyta wartości liczników jako współrzędne dla każdego rekordu wielolinowego. Stary handler PDFlibPas dodatkowo kalibrował swój test graniczny na tym złym układzie, więc poprawne rekordy wielolinowe były odrzucane, a jednolinijne nic nie rysowały. Od v3.539.41 PDFlibPas lokalizuje tablicę punktów z obliczonego przesunięcia, tak jak jego handler PolyPolygon zawsze robił, i rysuje każdą polilinię jako własną otwartą podścieżkę z jednym obrysem na końcu. W v3.539.43 ta sama obróbka trafiła na bliźniaka 16-bitowego; ten rysował dotąd segment po segmencie, co łamało łączenia linii i ignorowało wybrany NULL_PEN
Domyślne pióro i pędzel oraz nawiasy ścieżek
Dwie reguły stanu dopełniają poprawki polilinii w v3.539.43:
- Świeży kontekst urządzenia GDI ma już wybrane
BLACK_PENiWHITE_BRUSH, więc metafil rysujący bez żadnegoEMR_SELECTOBJECTi tak rysuje czarne obrysy; konwerter startował wcześniej bez pióra i bez wypełnienia i pisałn(koniec ścieżki, nic nie maluj) dla takich rekordów - Wewnątrz nawiasu
BeginPath/EndPathPolylineani nie używa, ani nie aktualizuje bieżącej pozycji, więc musi otworzyć nową podścieżkę w swoim pierwszym punkcie zamiast łączyć się z poprzednią figurą, a nic nie może być zamalowane, dopóki nawias nie zostanie obrysowany albo wypełniony
Budowanie testowego pliku EMF z TMetafileCanvas
Najszybszy sposób sprawdzenia konwertera pod kątem tych reguł to zapisanie trzech ryzykownych wywołań w jednym rozszerzonym metafilu przez TMetafileCanvas. Poniższy rysunek zapisuje krzywe pędzlem przezroczystym, a potem celowo wybiera pełny żółty pędzel dla polilinii: poprawny konwerter musi zignorować ten pędzel przy polilinii, więc każdy żółty piksel w wyjściowym PDF to błąd. PolyDraw nie ma opakowania w TCanvas, więc wołamy je przez Windows API z uchwytem płótna, używając bajtów typu 3 i 5, żeby ćwiczyć flagę zamknięcia
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Zamknięty kwadrat (3 = LINETO + CLOSEFIGURE), a potem zamknięta
// figura Beziera, której ostatnia trójka kontrolna kończy się 5 = BEZIERTO + CLOSEFIGURE
DrawPts: array[0..7] of TPoint = (
(X: 300; Y: 40), (X: 380; Y: 40), (X: 380; Y: 120), (X: 300; Y: 120),
(X: 420; Y: 120), (X: 440; Y: 40), (X: 520; Y: 40), (X: 540; Y: 120));
DrawTypes: array[0..7] of Byte = (
PT_MOVETO, PT_LINETO, PT_LINETO, PT_LINETO or PT_CLOSEFIGURE,
PT_MOVETO, PT_BEZIERTO, PT_BEZIERTO, PT_BEZIERTO or PT_CLOSEFIGURE);
var
Mf: TMetafile;
Canvas: TMetafileCanvas;
begin
Mf := TMetafile.Create;
try
Mf.Enhanced := True;
Mf.Width := 600;
Mf.Height := 260;
Canvas := TMetafileCanvas.Create(Mf, 0);
try
Canvas.Pen.Color := clNavy;
Canvas.Pen.Width := 2;
Canvas.Brush.Style := bsClear; // tylko obrysy dla krzywych
// Punkt 0 to start; 1..3 oraz 4..6 to dwa segmenty sześcienne
Canvas.PolyBezier([Point(20, 120), Point(60, 20), Point(100, 220),
Point(140, 120), Point(180, 20), Point(220, 220), Point(260, 120)]);
PolyDraw(Canvas.Handle, DrawPts[0], DrawTypes[0], Length(DrawPts));
// Otwarte V z wybranym pełnym pędzlem: tylko obrys, nigdy domknięte
// do żółtego trójkąta
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // kończy zapis
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Te współrzędne mieszczą się w SmallInt, więc GDI normalnie zapisze warianty 16-bitowe. Do 32-bitowych handlerów trzeba dotrzeć producentem, który takie rekordy pisze, albo rekordami zbudowanymi ręcznie. Pliki składane ręcznie niosą własną pułapkę: VCL TMetafile.LoadFromStream traktuje strumień jako EMF tylko wtedy, gdy pozostała długość jest ściśle większa od 108-bajtowego TEnhMetaHeader. Minimalny ręcznie pisany EMF ze skróconym nagłówkiem albo pusty plik mający dokładnie 108 bajtów zostanie wzięty za WMF i odrzucony komunikatem „Metafile is not valid". Zawsze zapisuj pełny 108-bajtowy nagłówek, łącznie z polami rozszerzenia, zanim pojawią się twoje testowe rekordy
Import EMF do PDF przez PDFlibPas
PDFlibPas importuje EMF przez ImportEMFFromFile albo ImportEMFFromStream, które na sukces zwracają niezerowe ID obrazu, a na porażkę 0. GeneralOptions = 0 zachowuje ścieżkę wektorową, o której jest ten artykuł; wartość 1 rasteryzuje metafil do bitmapy. FontOptions = 1 dodaje fonty metafila jako nieosadzone fonty TrueType. Wariant strumieniowy przewija strumień na pozycję 0 przed wczytaniem, więc podaj strumień zawierający wyłącznie metafil
uses
System.SysUtils, PDFlibrary;
procedure EmfToPdf(const EmfFile, PdfFile: WideString);
var
PDF: TPDFlib;
ImageID: Integer;
PageOps: AnsiString;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1); // lewy górny róg jako początek dla DrawImage
PDF.SetMeasurementUnits(0); // punkty
// FontOptions 1 = dodaj fonty jako nieosadzone TrueType
// GeneralOptions 0 = import wektorowy, 1 = bitmapa
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// Dla EMF ImageWidth / ImageHeight to rozmiar ramki w punktach
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// Strona tylko wywołuje zaimportowaną formę: q ... cm /Name Do Q
PageOps := PDF.GetPageContentToString;
if Pos(AnsiString(' Do'), PageOps) = 0 then
raise Exception.Create('Expected a form XObject invocation');
if PDF.SaveToFile(PdfFile) <> 1 then
raise Exception.Create('The PDF could not be saved');
finally
PDF.Free;
end;
end;
Wektorowy import EMF staje się form XObject, więc GetPageContentToString zwraca tylko sekwencję save, transform, Do i restore. Operatory m, l, c, h i S wyprodukowane z rekordów Poly* siedzą w strumieniu form XObject, który jest skompresowany. Żeby je przejrzeć, zdekompresuj zapisany plik w inspektorze obiektów PDF i poczytaj strumień formy: w powyższym pliku testowym polilinia powinna kończyć się S bez h przed nim, każda flaga zamknięcia w figurach PolyDraw powinna mieć swoje h, a na żadnej z tych podścieżek nie może być f ani B. DrawImage przy okazji skaluje zaimportowany EMF jednolicie według mniejszej z wartości Width i Height, więc rysunek zachowuje proporcje, nawet gdy podana ramka im nie sprzyja
Dla celów Free Pascal zobacz jak wektorowy importer EMF w PDFlibPas buduje się pod Free Pascal; semantyka rekordów jest ta sama, gdziekolwiek importer się kompiluje
Jak parser EMF powinien traktować liczby punktów z pliku?
Parser EMF powinien traktować każdą liczbę punktów jak dane niezaufane i sprawdzać ją względem rozmiaru rekordu, zanim skopiuje choć jeden punkt. EnumEnhMetaFile gwarantuje tylko, że nSize każdego rekordu mieści się w pliku. Nie sprawdza, czy cptl zgadza się z nSize, więc handler kopiujący cptl punktów przez Move przy sfabrykowanej albo uszkodzonej liczbie przeczyta kolejne rekordy albo wyjedzie poza koniec metafila. Od v3.539.39 PDFlibPas sprawdza względem nSize stały nagłówek plus liczbę punktów razy bajty na punkt dla PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo i Polygon w obu szerokościach, z dodatkowym bajtem na punkt dla bajtów typu PolyDraw. Dla rekordów PolyPoly liczniki poszczególnych figur muszą też sumować się do nie więcej niż zadeklarowana całość, a figury zeropunktowe są pomijane
Ten sam test jest krótki na tyle, żeby skopiować go do własnego parsera. Ta wersja waliduje 32-bitowy EMR_POLYPOLYLINE i zwraca wskaźnik na jego prawdziwą tablicę punktów:
uses
Winapi.Windows;
// Zwraca nil, chyba że rekord naprawdę trzyma punkty, które deklaruje.
// Punkty zaczynają się za tablicą liczników: 32 + nPolys * 4 bajta w głąb,
// nie na aptl[0], które RTL deklaruje jako tablicę jednoelementową
function PolyPolylinePoints(Rec: PEnhMetaRecord): PPoint;
var
P: PEMRPolyPolyline;
Count: PDWORD;
PointsOffset, Total: Int64;
I: Cardinal;
begin
Result := nil;
if (Rec^.iType <> EMR_POLYPOLYLINE) or (Rec^.nSize < 32) then
Exit;
P := PEMRPolyPolyline(Rec);
if P^.nPolys = 0 then
Exit;
PointsOffset := 32 + Int64(P^.nPolys) * SizeOf(DWORD);
if PointsOffset + Int64(P^.cptl) * SizeOf(TPoint) > Rec^.nSize then
Exit; // sfabrykowana albo obcięta liczba
Total := 0;
Count := @P^.aPolyCounts[0]; // chodzenie wskaźnikiem: [0..0] wyzwala testy zakresu
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // figury żądają więcej punktów, niż istnieje
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Test, czy punkty się mieszczą, biegnie jako pierwszy, więc w momencie, gdy pętla chodzi po tablicy liczników, wiadomo już, że siedzi wewnątrz rekordu. Arytmetyka jest na Int64, bo nPolys * 4 i cptl * 8 policzone na 32 bitach potrafią się przepełnić i prześlizgnąć przez porównanie
Ściąga: reguły EMF Poly* przy konwersji EMF na PDF
EMR_POLYBEZIER: punkt 0 to punkt startowy; grupuj od punktu 1 po trzy; w rekordzie 32-bitowym naprawione w v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: figury otwarte, obrysS, nigdyh,faniB, bo wypełnienie w PDF domyka otwarte podścieżkiEMR_POLYLINETO: start od bieżącej pozycji, zostaje otwarta, aktualizuje bieżącą pozycję, nigdy nie rusza stanu pióra- 32-bitowy
EMR_POLYPOLYLINE: punkty zaczynają się na bajcie32 + nPolys * 4, nie naaptl[0] EMR_POLYDRAW: zamaskujPT_CLOSEFIGUREprzed rozdziałem, zamykaj po ukończonym segmencie, startuj od bieżącej pozycji, gdy pierwszy punkt to niePT_MOVETO- Domyślny stan kontekstu urządzenia to
BLACK_PENplusWHITE_BRUSH; v3.539.43 i nowsze to respektują - Wewnątrz
BeginPath/EndPathkażda polilinia otwiera własną podścieżkę, a nic nie jest malowane, dopóki nawias nie zostanie użyty - Waliduj każde
cptl/cptswzględemnSizew arytmetyce 64-bitowej, zanim skopiujesz punkty - Ręcznie składane testowe EMF-y potrzebują pełnego 108-bajtowego nagłówka, inaczej
TMetafile.LoadFromStreamprzeczyta je jako WMF
Jeśli twoje raporty idą przez inny komponent, semantyka rekordów jest ta sama; import wektorowy EMF i WMF w HotPDF opisuje, jak tamten komponent zamienia pędzle gradientowe i kreskowe na wzorce PDF, a grafika wektorowa, shadery i gradienty w PDFlibPas pokazuje rysowanie tych samych kształtów bezpośrednio API biblioteki zamiast przez metafil
PDFlibPas w wersji v3.539.43 lub nowszej zawiera każdą z powyższych reguł. Szczegóły i wersje próbne są na stronie produktu PDFlibPas Delphi PDF library