Technical Article

GeoPDF v Delphi: Geopriestorové súradnice s PDFlibPas

Väčšina vývojárov si pod PDF stránkou predstaví list papiera s textom a obrázkami. Georeferencované PDF je však viac než to. Nesie dostatok informácií na to, aby zobralo bod na stránke, nameraný v bežných jednotkách stránky, a nahlásilo zemepisnú šírku a dĺžku, nad ktorou leží v reálnom svete. Táto jediná skutočnosť je to, čo mení PDF na použiteľný nosič pre topografickú mapu, parcelný katastrálny plán, náčrt záplavovej zóny alebo akýkoľvek export GIS, ktorý sa musí vytlačiť a stále mať zmysel. Geometria je prítomná v súbore; jedinou otázkou je, či ju váš loader číta.

Dôvodom, prečo sa to prehliada, je to, že GeoPDF sa otvára a tlačí presne ako akékoľvek iné PDF. Nič na vykreslenej stránke neoznamuje, že mapa je registrovaná v súradnicovom systéme. Registrácia žije v slovníkoch visiacich z objektu stránky, ktoré sa nikdy nevykresľujú, a prehliadač, ktorý ich ignoruje, vám napriek tomu zobrazí mapu. Ak chcete so súborom robiť čokoľvek priestorové – meranie súradníc, reprojekciu, prekrývanie s inými vrstvami – musíte tieto slovníky prejsť sami.

V praxi žijú dva štandardy

Čítačka, ktorá chce pracovať so skutočnými súbormi, sa musí vysporiadať s dvoma schémami georegistrácie, pretože obe sú v obehu a daný súbor môže používať ktorúkoľvek z nich. Staršia je kódovanie OGC popísané v OGC 08-139r2, ktoré k stránke pripája LGIDict (slovník geopriestorovej registrácie). Predchádza akémukoľvek schváleniu ISO a bol de facto formátom pre skoré GeoPDF výstupy, so veľké množstvo starších máp nesie práve tento formát a nič iné.

Moderná schéma je tá, ktorú ISO štandardizovalo v ISO 32000-1 §8.8.2. Namiesto jediného slovníka na úrovni stránky modeluje geopriestorové dáta ako stránkový Viewport s pripojeným slovníkom Measure, pričom slovník merania definuje geografický súradnicový systém. Toto je kódovanie, ktoré zapisuje Acrobat a súčasné exportéry GIS. Robustný importér kontroluje oboje: číta viewports pre ISO model a vracia sa k (alebo dodatočne kontroluje) LGIDict pre súbory, ktoré nesú iba staršiu registráciu.

Viewports a ich hranice

V modeli ISO je jednotkou georegistrácie viewport a stránka ich môže mať niekoľko. Veľký hárok môže umiestniť hlavnú mapu do jedného obdĺžnika, výsek v inej mierke do druhého a panel legendy, ktorý nie je vôbec georeferencovaný, do tretieho. Každý viewport nesie BBox, obdĺžnik na stránke, ktorý viewport riadi, so čítačka vie, na ktorú časť hárku sa daný súradnicový systém vzťahuje. Testovanie zasiahnutia (hit-testing) kliknutého bodu voči týmto boxom je spôsob, akým prehliadač rozhoduje, ktorý slovník merania použiť.

PDFlibPas odhaľuje viewports vybranej stránky priamo. GetPageViewPortCount vracia, koľko ich je, GetPageViewPortID mení index od 1 na handle ViewPortID a GetViewPortBBox číta ohraničujúci obdĺžnik po jednej dimenzii naraz. Lex Dimension argument selects which edge or extent you want: 0 is Left, 1 is Top, 2 is Width, 3 is Height, 4 is Right, and 5 is 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;

Hodnota ViewPortID rovná nule z GetPageViewPortID znamená, že viewport na tomto indexe nebolo možné nájsť, preto ju pred odovzdaním handlu ďalej skontrolujte.

Vnútri slovníka meraní

Geometria, ktorá registruje stránku k svetu, žije v slovníku meraní (measure dictionary) pripojenom k viewportu. GetViewPortMeasureDict vracia MeasureDictID pre daný ViewPortID, alebo nulu, keď viewport nemá žiadny slovník meraní, čo je bežný prípad pre legendu alebo panel s názvom. Slovník merania obsahuje tri veci, ktoré stojí za to čítať: súradnicové systémy, na ktoré odkazuje, polia, ktoré spájajú body stránky s geografickými bodmi, a jednotku, v ktorej sú dáta bodov vyjadrené.

Samotná registrácia pozostáva z dvoch paralelných polí. GPTS je pole geografických bodov, teda dvojíc zemepisnej šírky a dĺžky zadaných v geografickom súradnicovom systéme. LPTS je pole bodov v priestore stránky vyjadrených ako zlomky BBoxu viewportu, so prežijú zmenu mierky. Položka n z LPTS a položka n z GPTS pomenúvajú rovnaké fyzické miesto, raz v súradniciach stránky a raz na glóbuse. Tri alebo viac takýchto dvojíc definujú afinnú alebo vo všeobecnom prípade projektívnu transformáciu, ktorá mapuje akúkoľvek súradnicu stránky vnútri viewportu na svetovú súradnicu. Ich čítanie je otázkou prechodu oboch polí krok za krokom.

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;

Slovník merania tiež hlási svoje zobrazovacie jednotky cez GetMeasureDictPDU, ktoré prijíma UnitIndex s hodnotou 1 pre lineárne, 2 pre plošné alebo 3 pre uhlové jednotky a vracia kód identifikujúci konkrétnu jednotku, napríklad meter alebo medzinárodnú stopu pre lineárnu kategóriu. Pole Bounds, čítané pomocou GetMeasureDictBoundsItem, popisuje štvoruholník vnútri viewportu, ktorý meranie skutočne pokrýva, čo nie je vždy celý obdĺžnik.

WKT verzus EPSG

Zemepisná šírka a dĺžka v GPTS sú bezvýznamné bez znalosti toho, do ktorého geografického súradnicového systému patria, keďže súradnica 51,5, -0,1 pristane na inom fyzickom mieste pod WGS 84 než pod starším národným dátumom (datum). Slovník merania na to odpovedá prostredníctvom slovníka súradnicového systému (coordinate system dictionary), dosiahnutého pomocou GetMeasureDictGCSDict pre geografický systém. PDF popisuje tento systém jedným z dvoch zameniteľných spôsobov a čítačka musí akceptovať oba.

Prvým je WKT (Well-Known Text), samostatný reťazec, ktorý plne popisuje dátum, elipsoid, hlavný poludník a jednotky. Je podrobný, ale jednoznačný a nepotrebuje žiadnu externú vyhľadávaciu tabuľku. Druhým je kód EPSG, jedno celé číslo, ktoré indexuje súradnicový systém v registri EPSG; 4326 je WGS 84, rámec, ktorý používa väčšina spotrebiteľských GPS dát. EPSG je kompaktný, ale predpokladá, že čítačka dokáže kód vyriešiť voči databáze. Súbory sa objavujú s jedným, druhým alebo oboma, preto API sprístupňuje všetky tri funkcie: GetCSDictType, GetCSDictEPSG a GetCSDictWKT. GetCSDictType hlási, či je systém geografický (GEOGCS, návratová hodnota 1) alebo projektovaný (PROJCS, návratová hodnota 2), čo vám umožňuje správne interpretovať zvyšok pred tým, ako mu začnete dôverovať.

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;

Čítanie staršieho LGIDict

Súbory, ktoré predchádzajú modelu viewport, alebo boli vytvorené nástrojmi stále generujúcimi staršie kódovanie, nesú svoju registráciu v LGIDict na stránke a nie v slovníku meraní. PDFlibPas hlási, koľko takýchto slovníkov stránka má, cez GetPageLGIDictCount a odovzdáva späť surový obsah každého z nich pomocou GetPageLGIDictContent s indexovaním od jednej. Vrátený text je slovník v zapísanej podobe, obsahujúci registračné polia OGC 08-139r2, ktoré váš kód následne analyzuje na obnovenie rovnakého mapovania stránky k svetu, aké poskytuje slovník meraní. Na strane zápisu AddLGIDictToPage pripája LGIDict k aktuálnej stránke, takže konvertor môže spracovať staršiu formu obojsmerne, keď ju starý spotrebiteľ stále očakáva.

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;

Ako spojiť čítanie dokopy

Kompletný importér pristupuje k obom schémam ako k dvojici prechodov nad každou stránkou. Vyberte stránku, opýtajte sa GetPageViewPortCount na ISO viewports a pre každý viewport, ktorý vlastní slovník meraní, vytiahnite jeho BBox, polia GPTS a LPTS, jednotku dát bodu a popis GCS cez slovník súradnicového systému. Potom skontrolujte GetPageLGIDictCount pre akúkoľvek staršiu registráciu, ktorú prechod viewportov nepokryl. Mapa, ktorá nesie obe schémy, by mala byť v zhode; mapa, ktorá nesie iba jednu, sa stále vyrieši, pretože ste hľadali na oboch miestach. Handly vrátené počas postupu (ViewPortID, MeasureDictID, CSDictID) sú obyčajné celé čísla, ktoré zostávajú platné po dobu načítania dokumentu, takže celý priechod je len niekoľkými vnorenými slučkami nad zoznamom stránok bez nutnosti správy alokácií.

Akonáhle dokážete obnoviť registráciu, stránka sa stáva skôr zdrojom dát než len obrázkom. Sprievodné techniky na čítanie zvyšku stránky sú popísané v článku o extrakcii textu, obrázkov a písiem a vykreslenie georeferencovaného hárku na zariadenie na meranie na obrazovke je popísané v návode na tlač a náhľad s kontextom zariadenia (device context). Geopriestorová čítačka opísaná tu sa dodáva ako súčasť losLab PDF Library pre Delphi a C++Builder spolu s API na načítanie, extrakciu a vykresľovanie popísanými inde na tomto blogu.