Większość programistów traktuje stronę PDF jak arkusz papieru z tekstem i obrazkami. Plik PDF z georeferencjami to coś więcej. Niesie ze sobą wystarczająco dużo informacji, aby pobrać punkt na stronie (zmierzony w zwykłych jednostkach strony) i zwrócić szerokość oraz długość geograficzną, nad którymi leży w świecie rzeczywistym. Ten jeden fakt sprawia, że PDF staje się użytecznym nośnikiem dla mapy topograficznej, szkicu katastralnego, mapy strefy powodziowej lub dowolnego eksportu GIS, który musi dać się wydrukować, a jednocześnie zachować znaczenie przestrzenne. Geometria znajduje się w pliku – jedynym pytaniem jest to, czy Twój moduł ładujący ją odczytuje.
Powodem, dla którego ten temat bywa pomijany, jest fakt, że GeoPDF otwiera się i drukuje dokładnie tak samo, jak każdy inny PDF. Nic na wyrenderowanej stronie nie zdradza, że mapa jest zarejestrowana w układzie współrzędnych. Rejestracja ta żyje w słownikach podpiętych pod obiekt strony, nigdy nie jest rysowana, a przeglądarka ignorująca te dane i tak wyświetli mapę. Aby zrobić cokolwiek przestrzennego z tym plikiem – odczytywać współrzędne geodezyjne, dokonywać reprojekcji, nakładać na inne warstwy – musisz samodzielnie przejść przez te słowniki.
Dwa standardy funkcjonujące w praktyce
Czytnik, który ma obsługiwać rzeczywiste pliki, musi radzić sobie z dwoma schematami georejestracji, ponieważ oba są w obiegu i dany plik może używać dowolnego z nich. Starszym z nich jest kodowanie OGC opisane w specyfikacji OGC 08-139r2, które dołącza do strony słownik LGIDict (słownik rejestracji geospatialnej). Poprzedza ono jakiekolwiek zatwierdzenie przez ISO i było de facto formatem wczesnych danych wyjściowych GeoPDF, przez co duża część starszych map niesie ten zapis i nic innego.
Nowoczesny schemat to ten, który ISO ustandaryzowało w normie ISO 32000-1 §8.8.2. Zamiast pojedynczego słownika na poziomie strony modeluje on dane geograficzne jako Rzutnię (Viewport) strony z dołączonym słownikiem Miar (Measure), a słownik miar wskazuje geograficzny układ współrzędnych. Jest to kodowanie, które zapisuje program Acrobat oraz obecne eksportery GIS. Solidny importer sprawdza oba schematy: odczytuje rzutnie dla modelu ISO i cofa się do (lub dodatkowo bada) słownika LGIDict w przypadku plików, które niosą tylko starszą rejestrację.
Rzutnie i ich granice
W modelu ISO jednostką georejestracji jest rzutnia (viewport), a strona może mieć ich kilka. Duży arkusz może umieścić główną mapę w jednym prostokącie, wstawkę w innej skali w drugim, a panel legendy, który nie posiada georeferencji, wcale. Każda rzutnia niesie ze sobą BBox – prostokąt na stronie, którym rządzi rzutnia, dzięki czemu czytnik wie, do której części arkusza stosuje się dany układ współrzędnych. Testowanie trafienia (hit-testing) klikniętego punktu w te ramki pozwala przeglądarce zdecydować, którego słownika miar użyć.
Biblioteka PDFlibPas udostępnia rzutnie wybranej strony bezpośrednio. Metoda GetPageViewPortCount zwraca liczbę rzutni, GetPageViewPortID zamienia indeks oznaczony od jedynki na uchwyt ViewPortID, a GetViewPortBBox odczytuje prostokąt ograniczający po jednym wymiarze na raz. Argument Dimension wybiera krawędź lub zakres: 0 to Left, 1 to Top, 2 to Width, 3 to Height, 4 to Right, a 5 to Bottom.
var
Pdf: TPDFlib;
vpCount, i, vpID: Integer;
Left, Top, Width, Height: Double;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('topo_sheet.pdf', '') <> 1 then
raise Exception.Create('load failed');
Pdf.SelectPage(1);
vpCount := Pdf.GetPageViewPortCount;
for i := 1 to vpCount do
begin
vpID := Pdf.GetPageViewPortID(i);
Left := Pdf.GetViewPortBBox(vpID, 0);
Top := Pdf.GetViewPortBBox(vpID, 1);
Width := Pdf.GetViewPortBBox(vpID, 2);
Height := Pdf.GetViewPortBBox(vpID, 3);
// Left/Top/Width/Height describe the map area for this viewport
end;
finally
Pdf.Free;
end;
end;
Wartość ViewPortID równa zero z metody GetPageViewPortID oznacza, że nie można znaleźć rzutni o danym indeksie, należy to zatem sprawdzić przed przekazaniem uchwytu dalej.
Wewnątrz słownika miar
Geometria rejestrująca stronę do świata fizycznego znajduje się w słowniku miar (measure dictionary) dołączonym do rzutni. Metoda GetViewPortMeasureDict zwraca MeasureDictID dla danego ViewPortID lub zero, gdy rzutnia nie posiada słownika miar (co jest normalne dla legendy lub panelu tytułowego). Słownik miar przechowuje trzy rzeczy warte odczytania: układy współrzędnych, do których się odwołuje, tablice wiążące punkty strony z punktami geograficznymi oraz jednostkę, w której wyrażane są dane punktów.
Sama rejestracja to dwie równoległe tablice. GPTS to tablica punktów geograficznych, czyli par szerokości i długości geograficznej podanych w geograficznym układzie współrzędnych. LPTS to tablica punktów w przestrzeni strony, wyrażonych jako ułamki BBox rzutni, dzięki czemu przeżywają skalowanie. Element n tablicy LPTS i element n tablicy GPTS wskazują tę samą fizyczną lokalizację – raz we współrzędnych strony, a raz na globie. Trzy lub więcej takich par określają transformację afiniczną (lub w ogólnym przypadku rzutową), która mapuje dowolną współrzędną strony wewnątrz rzutni na współrzędną świata. Ich odczyt sprowadza się do przejścia przez obie te tablice krok po kroku.
var
measID, gptsCount, lptsCount, j: Integer;
lat, lon, px, py: Double;
begin
measID := Pdf.GetViewPortMeasureDict(vpID);
if measID <> 0 then
begin
gptsCount := Pdf.GetMeasureDictGPTSCount(measID);
lptsCount := Pdf.GetMeasureDictLPTSCount(measID);
// GPTS holds lat/lon pairs; LPTS holds the matching page fractions.
// Both arrays are read with one-based item indices.
j := 1;
while j < gptsCount do
begin
lat := Pdf.GetMeasureDictGPTSItem(measID, j);
lon := Pdf.GetMeasureDictGPTSItem(measID, j + 1);
px := Pdf.GetMeasureDictLPTSItem(measID, j);
py := Pdf.GetMeasureDictLPTSItem(measID, j + 1);
// (px, py) on the page corresponds to (lat, lon) on the ground
Inc(j, 2);
end;
end;
end;
Słownik miar raportuje również swoje jednostki wyświetlania poprzez metodę GetMeasureDictPDU, która przyjmuje UnitIndex o wartości 1 dla jednostek liniowych, 2 dla powierzchniowych lub 3 dla jednostek kątowych i zwraca kod identyfikujący konkretną jednostkę (na przykład metr lub stopę międzynarodową dla kategorii liniowej). Tablica Bounds, odczytywana za pomocą GetMeasureDictBoundsItem, opisuje czworokąt wewnątrz rzutni, który faktycznie obejmuje pomiar (co nie zawsze jest pełnym prostokątem).
WKT kontra EPSG
Szerokość i długość geograficzna w GPTS są bezużyteczne bez wiedzy, do jakiego układu współrzędnych geograficznych należą, ponieważ współrzędna 51.5, -0.1 wskazuje inną fizyczną lokalizację w układzie WGS 84 niż w starszym krajowym układzie odniesienia. Słownik miar odpowiada na to za pomocą słownika układu współrzędnych, do którego można dotrzeć metodą GetMeasureDictGCSDict dla układu geograficznego. PDF opisuje ten układ na jeden z dwóch zamiennych sposobów, a czytnik musi akceptować oba.
Pierwszym z nich jest WKT (Well-Known Text) – samowystarczalny ciąg znaków, który szczegółowo określa układ odniesienia, elipsoidę, południk zerowy i jednostki. Jest on gadatliwy, ale jednoznaczny i nie wymaga zewnętrznej tabeli wyszukiwania. Drugim sposobem jest kod EPSG – pojedyncza liczba całkowita, która indeksuje układ współrzędnych w rejestrze EPSG (np. 4326 to WGS 84, układ używany przez większość konsumenckich urządzeń GPS). Kod EPSG jest kompaktowy, ale zakłada, że czytnik potrafi powiązać kod z bazą danych. Pliki pojawiają się z jednym, drugim lub oboma zapisami, dlatego API udostępnia metody GetCSDictType, GetCSDictEPSG oraz GetCSDictWKT. Metoda GetCSDictType informuje, czy układ jest geograficzny (GEOGCS, wartość zwracana 1) czy rzutowany (PROJCS, wartość zwracana 2), pozwalając na poprawną interpretację przed zaufaniem danym.
var
gcsID, csType, epsg: Integer;
wkt: WideString;
begin
gcsID := Pdf.GetMeasureDictGCSDict(measID);
if gcsID <> 0 then
begin
csType := Pdf.GetCSDictType(gcsID); // 1 = GEOGCS, 2 = PROJCS
epsg := Pdf.GetCSDictEPSG(gcsID); // e.g. 4326 for WGS 84, 0 if absent
wkt := Pdf.GetCSDictWKT(gcsID); // full text description, '' if absent
// Prefer EPSG when present; fall back to parsing WKT otherwise.
end;
end;
Odczytywanie dziedzictwa LGIDict
Pliki, które powstały przed modelem rzutni lub zostały wygenerowane przez narzędzia nadal emitujące starsze kodowanie, przenoszą swoją rejestrację w słowniku LGIDict na stronie, a nie w słowniku miar. PDFlibPas zgłasza, ile takich słowników posiada strona, za pomocą GetPageLGIDictCount i zwraca surową zawartość każdego z nich za pomocą GetPageLGIDictContent, indeksowanego od jedynki. Zwrócony tekst to słownik w postaci zapisanej, trzymający pola rejestracji OGC 08-139r2, który Twój kod następnie analizuje w celu odzyskania tego samego rodzaju mapowania strony na świat, jakie zapewnia słownik miar. Po stronie zapisu metoda AddLGIDictToPage dołącza słownik LGIDict do bieżącej strony, dzięki czemu konwerter może obsłużyć starszą formę w obie strony, gdy starszy odbiorca wciąż tego oczekuje.
var
lgiCount, k: Integer;
dictText: WideString;
begin
lgiCount := Pdf.GetPageLGIDictCount;
for k := 1 to lgiCount do
begin
dictText := Pdf.GetPageLGIDictContent(k);
// dictText carries the OGC 08-139r2 registration to parse
end;
end;
Łączenie odczytu w całość
Kompletny importer traktuje te dwa schematy jako parę przejść nad każdą stroną. Wybierz stronę, zapytaj GetPageViewPortCount o rzutnie ISO i dla każdej rzutni posiadającej słownik miar pobierz jej BBox, tablice GPTS i LPTS, jednostkę danych punktów oraz opis GCS poprzez słownik układu współrzędnych. Następnie sprawdź GetPageLGIDictCount pod kątem ewentualnej starszej rejestracji, której nie objęło przejście rzutni. Mapa niosąca oba zapisy powinna być między nimi spójna; mapa niosąca tylko jeden wciąż zostanie odczytana, ponieważ sprawdziłeś oba miejsca. Uchwyty zwracane po drodze (ViewPortID, MeasureDictID, CSDictID) to zwykłe liczby całkowite, które pozostają ważne tak długo, jak dokument jest załadowany, więc całe przejście to kilka zagnieżdżonych pętli na liście stron bez konieczności zarządzania alokacją pamięci.
Gdy potrafisz już odzyskać rejestrację, strona staje się źródłem danych, a nie tylko obrazem. Towarzyszące techniki odczytu reszty strony zostały opisane w artykule na temat ekstrakcji tekstu, obrazów i czcionek, a renderowanie georeferencyjnego arkusza na urządzenie w celu pomiarów na ekranie przedstawiono w przewodniku po kontekście urządzenia drukowania i podglądu. Czytnik geospatialny opisany tutaj jest dostarczany jako część biblioteki losLab PDF Library dla Delphi i C++Builder, obok interfejsów API do ładowania, ekstrakcji i renderowania omówionych w innych miejscach tego bloga.