Technical Article

GeoPDF in Delphi: Georuimtelijke coördinaten met PDFlibPas

De meeste ontwikkelaars zien een PDF-pagina als een vel papier met tekst en afbeeldingen erop. Een PDF met geoverwijzingen (georeferenced) is echter meer dan dat. Het bevat voldoende informatie om een punt op de pagina, gemeten in gewone paginaeenheden, te vertalen naar de breedte- en lengtegraad waarop het zich in de echte wereld bevindt. Dat feit maakt een PDF tot een bruikbaar medium voor een topografische kaart, een kadastraal perceelplan, een overstromingskaart of elke GIS-export die moet worden afgedrukt en betekenis moet behouden. De geometrie bevindt zich in het bestand; de enige vraag is of uw loader deze leest.

De reden waarom dit vaak over het hoofd wordt gezien, is dat een GeoPDF op exact dezelfde manier opent en afdrukt als elke andere PDF. Niets op de gerenderde pagina verraadt dat de kaart is gekoppeld aan een coördinatensysteem. De registratie bevindt zich in dictionaries die aan het pagina-object hangen en die nooit worden getekend. Een viewer die deze dictionaries negeert, toont de kaart desalniettemin. Om ruimtelijke bewerkingen met het bestand uit te voeren — zoals het uitlezen van landmeetkundige coördinaten, herprojectie of overlay met andere lagen — moet u die dictionaries zelf doorlopen.

Er zijn twee standaarden in omloop

Een lezer die bestanden uit de praktijk wil verwerken, moet overweg kunnen met twee georegistratiemethoden, aangezien beide in omloop zijn en een bestand een van beide kan gebruiken. De oudere is de OGC-codering die wordt beschreven in OGC 08-139r2, die een LGIDict (een georuimtelijke registratie-dictionary) aan de pagina koppelt. Deze is ouder dan de ISO-goedkeuring en was de de facto standaard voor vroege GeoPDF-uitvoer. Veel oudere kaarten bevatten dan ook uitsluitend deze methode.

De moderne methode is degene die ISO heeft gestandaardiseerd in ISO 32000-1 §8.8.2. In plaats van een enkele dictionary op paginaniveau modelleert deze methode georuimtelijke gegevens als een pagina-Viewport met een gekoppelde Measure-dictionary, en de measure-dictionary specificeert een geografisch coördinatensysteem. Dit is de codering die Acrobat en huidige GIS-exporteurs schrijven. Een robuuste importeur controleert op beide: lees de viewports voor het ISO model, and fall back to (or additionally inspect) the LGIDict for files that only carry the legacy registration.

Viewports en hun grenzen

In het ISO-model is de viewport de eenheid van georegistratie, en een pagina kan er meerdere bevatten. Een groot vel kan een hoofdkaart in de ene rechthoek plaatsen, een inzetkaart met een andere schaal in een andere rechthoek, en een legenda-paneel dat helemaal geen geoverwijzingen bevat. Elke viewport bevat een BBox, de rechthoek op de pagina die door de viewport wordt beheerd, zodat de lezer weet op welk deel van het vel een bepaald coördinatensysteem van toepassing is. Door te controleren of een aangeklikt punt binnen die rechthoeken valt, bepaalt een viewer welke measure-dictionary moet worden gebruikt.

PDFlibPas ontsluit de viewports van de geselecteerde pagina rechtstreeks. GetPageViewPortCount retourneert hoeveel er zijn, GetPageViewPortID zet een one-based index om in een ViewPortID-handle, en GetViewPortBBox leest de begrenzende rechthoek dimensie voor dimensie uit. De parameter Dimension selecteert welke zijde of omvang u wilt: 0 is Left, 1 is Top, 2 is Width, 3 is Height, 4 is Right, en 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;

Een ViewPortID van nul geretourneerd door GetPageViewPortID betekent dat de viewport op die index niet kon worden gevonden; controleer dit dus voordat u de handle doorgeeft.

Binnen de measure-dictionary

De geometrie die de pagina aan de wereld koppelt, bevindt zich in de measure-dictionary die aan een viewport is gekoppeld. GetViewPortMeasureDict retourneert een MeasureDictID voor een gegeven ViewPortID, of nul wanneer the viewport has no measure dictionary, which is the normal case for a legend or title panel. De measure-dictionary bevat drie interessante elementen: de coördinatensystemen waarnaar wordt verwezen, de arrays die paginapunten koppelen aan geografische punten, en de eenheid waarin de puntgegevens zijn uitgedrukt.

De registratie zelf bestaat uit twee parallelle arrays. GPTS is de array van geografische punten (breedte- en lengtegraadparen) binnen het geografische coördinatensysteem. LPTS is de array van punten in de paginaruimte, uitgedrukt als fracties van de BBox van de viewport, zodat ze bestand zijn tegen schaling. Element n van LPTS en element n van GPTS verwijzen naar dezelfde fysieke locatie, eenmaal in paginacoördinaten en eenmaal op de wereldbol. Drie of meer van deze paren bepalen de affiene (of in het algemene geval projectieve) transformatie die elk paginacoördinaat binnen de viewport projecteert op een wereldcoördinaat. Het uitlezen ervan is een kwestie van het synchroon doorlopen van beide arrays.

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;

De measure-dictionary rapporteert ook zijn weergave-eenheden via GetMeasureDictPDU, die een UnitIndex accepteert van 1 for linear, 2 for area, or 3 for angular units and returns a code identifying the specific unit, for example a meter or an international foot for the linear category. De Bounds-array, gelezen met GetMeasureDictBoundsItem, beschrijft de vierhoek binnen de viewport die de meting daadwerkelijk beslaat, wat niet altijd de volledige rechthoek is.

WKT versus EPSG

De breedte- en lengtegraad in GPTS zijn betekenisloos zonder te weten bij welk geografisch coördinatensysteem ze horen. Een coördinaat van 51.5, -0.1 belandt immers op een heel andere fysieke plek onder WGS 84 dan onder een ouder nationaal datum. De measure-dictionary geeft hier antwoord op via een coördinatensysteem-dictionary, die wordt bereikt via GetMeasureDictGCSDict voor het geografische systeem. PDF beschrijft dat systeem op een van twee onderling uitwisselbare manieren, en een lezer moet beide kunnen accepteren.

De eerste is WKT (Well-Known Text), een op zichzelf staande string die het datum, de ellipsoïde, de nulmeridiaan en de eenheden volledig beschrijft. Het is langdradig maar eenduidig en vereist geen externe opzoektabel. De tweede is een EPSG-code, een enkele integer die verwijst naar een coördinatensysteem in het EPSG-register; 4326 staat voor WGS 84, het referentiekader dat de meeste consumenten-GPS-gegevens gebruiken. EPSG is compact maar veronderstelt dat de lezer de code kan vertalen met behulp van een database. Bestanden bevatten de ene, de andere of beide opties, wat verklaart waarom de API alle drie de functies GetCSDictType, GetCSDictEPSG en GetCSDictWKT ontsluit. GetCSDictType rapporteert of het systeem geografisch is (een GEOGCS, retourwaarde 1) of geprojecteerd (a PROJCS, return value 2), letting you interpret the rest correctly before you trust it.

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;

De verouderde LGIDict lezen

Bestanden die ouder zijn dan het viewport-model, of die zijn geproduceerd met tools die nog de oudere codering genereren, bevatten hun registratie in een LGIDict op de pagina in plaats van in een measure-dictionary. PDFlibPas rapporteert hoeveel van dergelijke dictionaries een pagina heeft via GetPageLGIDictCount and hands back the raw content of each with GetPageLGIDictContent, indexed from one. The returned text is the dictionary as written, holding the OGC 08-139r2 registration fields, which your code then parses to recover the same kind of page-to-world mapping the measure dictionary provides. On the writing side, AddLGIDictToPage attaches an LGIDict to the current page, so a converter can round-trip the legacy form when an old consumer still expects it.

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;

De lezing samenvoegen

Een complete importeur behandelt de twee methoden als een paar stappen over elke pagina. Selecteer de pagina, vraag GetPageViewPortCount naar de ISO-viewports en haal voor elke viewport die een measure-dictionary bezit de BBox, de GPTS- en LPTS-arrays, de maateenheid en de GCS-beschrijving op via de coördinatensysteem-dictionary. Controleer vervolgens GetPageLGIDictCount op eventuele legacy-registraties die de viewport-stap niet heeft gedekt. Een kaart die beide methoden bevat moet onderling consistent zijn; een kaart die er slechts één bevat is nog steeds leesbaar, omdat u op beide plaatsen heeft gezocht. De handles die onderweg worden geretourneerd (ViewPortID, MeasureDictID, CSDictID) zijn eenvoudige integers die geldig blijven zolang het document is geladen. De gehele procedure bestaat dus uit een paar geneste lussen over de paginalijst, zonder dat u allocaties hoeft te beheren.

Once you can recover the registration, the page becomes a data source rather than a picture. The companion techniques for reading the rest of a page are covered in the article on text, image, and font extraction, and rendering a georeferenced sheet to a device for on-screen measurement is described in the print and preview device-context walkthrough. The geospatial reader described here ships as part of the losLab PDF Library for Delphi and C++Builder, alongside the loading, extraction, and rendering APIs covered elsewhere on this blog.