Technical Article

GeoPDF i Delphi: Geospatiala koordinater med PDFlibPas

De flesta utvecklare tänker på en PDF-sida som ett pappersark med text och bilder på. En georefererad PDF är mer än så. Den bär tillräckligt med information för att ta en punkt på sidan, mätt i vanliga sidenheter, och rapportera latituden och longituden den ligger över i den verkliga världen. Det enda faktumet är vad som förvandlar en PDF till en användbar bärare av en topografisk karta, en fastighetskarta, en översvämningsutredning eller någon GIS-export som måste kunna skrivas ut och ändå betyda något. Geometrin finns där i filen; den enda frågan är om din laddare läser den

Anledningen till att detta missas är att en GeoPDF öppnas och skrivs ut exakt som vilken annan PDF som helst. Ingenting på den renderade sidan meddelar att kartan är registrerad till ett koordinatsystem. Registreringen lever i dicionärer som hänger på sidobjektet, ritas aldrig ut, och ett visningsprogram som ignorerar dem visar dig kartan ändå. För att göra något rumsligt med filen, som att läsa av lantmäterikoordinater, omprojicera eller lägga över mot andra lager, måste du gå igenom dessa dicionärer själv

Praktiskt sammanhang

En läsare som vill hantera filer från den verkliga världen måste klara av två georegistreringssystem, eftersom båda är i omlopp och en given fil kan använda vilket som helst. Den äldre är OGC-kodningen som beskrivs i OGC 08-139r2, vilken bifogar en LGIDict (en geospatial registreringsdicionär) till sidan. Den är äldre än något ISO-godkännande och var de facto-formatet för tidiga GeoPDF-utdata, så en stor mängd äldre kartor bär på den och ingenting annat

Det moderna systemet är det som ISO standardiserade i ISO 32000-1 §8.8.2. Istället för en enda dicionär på sidnivå modellerar det geospatiala data som en sidas Viewport (vyport) med en tillhörande Measure-dicionär (måttdicionär), och måttdicionären namnger ett geografiskt koordinatsystem. Detta är den kodning som Acrobat och nuvarande GIS-exportörer skriver. En robust importör letar efter båda: läser vyportarna för ISO-modellen, och faller tillbaka på (eller dessutom inspekterar) LGIDict för filer som endast bär på den äldre registreringen

Implementeringssteg

I ISO-modellen är enheten för georegistrering vyporten, och en sida kan ha flera. Ett stort ark kan placera en huvudkarta i en rektangel, en infälld karta i en annan skala i en annan, och en teckenförklaringspanel som inte är georefererad alls. Varje vyport bär en BBox, rektangeln på sidan som vyporten styr, så att läsaren vet vilken del av arket som ett givet koordinatsystem gäller för. Att testa en klickad punkt mot dessa rutor är hur ett visningsprogram bestämmer vilken måttdicionär som ska användas

PDFlibPas exponerar vyportarna för den valda sidan direkt. GetPageViewPortCount returnerar hur många det finns, GetPageViewPortID gör om ett ettbaserat index till en ViewPortID-pekare, och GetViewPortBBox läser avgränsningsrektangeln en dimension i taget. Argumentet Dimension väljer vilken kant eller utsträckning du vill ha: 0 är Vänster, 1 är Topp, 2 är Bredd, 3 är Höjd, 4 är Höger och 5 är Botten

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;

Ett ViewPortID på noll från GetPageViewPortID innebär att vyporten på det indexet inte kunde hittas, så kontrollera det innan du skickar pekaren vidare

Kontrollpunkter

Geometrin som registrerar sida till värld lever i måttdicionären kopplad till en vyport. GetViewPortMeasureDict returnerar ett MeasureDictID för ett givet ViewPortID, eller noll när vyporten saknar måttdicionär, vilket är det normala fallet för en teckenförklaring eller titelpanel. Måttdicionären innehåller tre saker värda att läsa: koordinatsystemen den refererar till, arrayerna som kopplar sidpunkter till geografiska punkter och enheten som punktdata uttrycks i

Registreringen i sig är två parallella arrayer. GPTS är arrayen av geografiska punkter, latitud- och longitudpar angivna i det geografiska koordinatsystemet. LPTS är arrayen av sidrymdspunkter, uttryckta som bråkdelar av vyportens BBox så att de överlever skalning. Element n i LPTS och element n i GPTS namnger samma fysiska plats, en gång i sidkoordinater och en gång på jordklotet. Tre eller fler sådana par fastställer den affina, eller i det allmänna fallet projektiva, transformeringen som mappar valfri sidkoordinat inuti vyportet till en världskoordinat. Att läsa dem handlar om att stega igenom båda arrayerna i takt

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;

Måttdicionären rapporterar också sina visningsenheter via GetMeasureDictPDU, som tar ett UnitIndex på 1 för linjära, 2 för area-, eller 3 för vinkelenheter och returnerar en kod som identifierar den specifika enheten, till exempel en meter eller en internationell fot för den linjära kategorin. Arrayen Bounds, läst med GetMeasureDictBoundsItem, beskriver fyrhörningen inom vyporten som mätningen faktiskt täcker, vilket inte alltid är hela rektangeln

Praktiskt sammanhang

Latituden och longituden i GPTS är meningslösa utan att veta vilket geografiskt koordinatsystem de tillhör, eftersom koordinaten 51,5, -0,1 hamnar på en annan fysisk plats under WGS 84 än under ett äldre nationellt datum. Måttdicionären besvarar detta genom en koordinatsystemdicionär, som nås med GetMeasureDictGCSDict för det geografiska systemet. PDF beskriver det systemet på ett av två utbytbara sätt, och en läsare måste acceptera båda

Det första är WKT, Well-Known Text, en fristående sträng som stavar ut datum, ellipsoid, nollmeridian och enheter i sin helhet. Den är utförlig men entydig och kräver ingen extern uppslagstabell. Det andra är en EPSG-kod, ett enskilt heltal som indexerar ett koordinatsystem i EPSG-registret; 4326 är WGS 84, ramen som de flesta konsument-GPS-data använder. EPSG är kompakt men förutsätter att läsaren kan matcha koden mot en databas. Filer förekommer med den ena, den andra eller båda, vilket är anledningen till att API:et lyfter fram alla tre GetCSDictType, GetCSDictEPSG och GetCSDictWKT. GetCSDictType rapporterar om systemet är geografiskt (en GEOGCS, returvärde 1) eller projicerat (en PROJCS, returvärde 2), vilket låter dig tolka resten korrekt innan du litar på det

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;

Implementeringssteg

Filer som är äldre än vyportsmodellen, eller som producerades av verktyg som fortfarande skickar ut den äldre kodningen, bär sin registrering i en LGIDict på sidan snarare än i en måttdicionär. PDFlibPas rapporterar hur många sådana dicionärer en sida har genom GetPageLGIDictCount och lämnar tillbaka det råa innehållet i varje med GetPageLGIDictContent, indexerat från ett. Den returnerade texten är dicionären som den är skriven, innehållande OGC 08-139r2-registreringsfälten, som din kod sedan tolkar för att återställa samma typ av sida-till-värld-mappning som måttdicionären ger. På skrivsidan bifogar AddLGIDictToPage en LGIDict till den aktuella sidan, så att en konverterare kan hantera den äldre formen fram och tillbaka när en äldre mottagare fortfarande förväntar sig det

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;

Kontrollpunkter

En komplett importör behandlar de två systemen som ett par genomgångar över varje sida. Välj sidan, fråga GetPageViewPortCount efter ISO-vyportarna, och för varje vyport som äger en måttdicionär hämtar du dess BBox, dess GPTS- och LPTS-arrayer, dess punktdatalängd och GCS-beskrivningen via koordinatsystemdicionären. Kontrollera sedan GetPageLGIDictCount för eventuell äldre registrering som vyportsgenomgången inte täckte. En karta som bär på båda bör stämma överens mellan dem; en karta som endast bär på den ena går ändå att lösa, eftersom du letade på båda ställena. Pekarna som returneras längs vägen, ViewPortID, MeasureDictID, CSDictID, är vanliga heltal som förblir giltiga medan dokumentet är laddat, så hela genomgången är några nästlade loopar över sidlistan utan någon allokering att hantera

När du väl kan återställa registreringen blir sidan en datakälla snarare än en bild. De tillhörande teknikerna för att läsa resten av en sida beskrivs i artikeln om text-, bild- och teckensnittsutdragning, och rendering av ett georefererat ark till en enhet för mätning på skärmen beskrivs i genomgången av enhetskontext för utskrift och förhandsgranskning. Den geospatiala läsaren som beskrivs här levereras som en del av losLab PDF Library för Delphi och C++Builder, tillsammans med de API:er för inläsning, utdragning och rendering som beskrivs på andra ställen i denna blogg