Technical Article

GeoPDF i Delphi: Geografiske koordinater med PDFlibPas

De fleste udviklere tænker på en PDF-side som et stykke papir med tekst og billeder på. En georefereret PDF er mere end det. Den bærer nok information til at tage et punkt på siden, målt i almindelige sideenheder, og rapportere den breddegrad og længdegrad, det sidder over i den virkelige verden. Dette enkelte faktum er, hvad der gør en PDF til en anvendelig bærer for et topografisk kort, et matrikulært opmålingskort, en oversvømmelseszone-udstilling eller enhver GIS-eksport, der skal udskrives og still betyde noget. Geometrien er der i filen; det eneste spørgsmål er, om din indlæser læser den.

Grunden til, at dette overses, er, at en GeoPDF åbnes og udskrives præcis som enhver anden PDF. Intet på den renderede side fortæller, at kortet er registreret til et koordinatsystem. Registreringen lever i ordbøger (dictionaries), der hænger på sideobjektet, aldrig tegnet, og en fremviser, der ignorerer dem, viser dig kortet alligevel. For at gøre noget rumligt med filen, opmålingskoordinataflæsninger, reprojektering, overlejring mod andre lag, skal du selv gennemgå disse ordbøger.

To standarder lever i det fri

En læser, der ønsker at håndtere filer fra den virkelige verden, skal klare to georegistreringsordninger, fordi begge er i cirkulation, og en given fil kan bruge begge. Den ældre er OGC-kodningen beskrevet i OGC 08-139r2, som knytder en LGIDict (en geografisk registreringsordbog) til siden. Den går forud for enhver ISO-godkendelse og var de facto-formatet for tidligt GeoPDF-output, så en stor mængde ældre kort bærer den og intet andet.

Det moderne system er det, som ISO standardiserede i ISO 32000-1 §8.8.2. I stedet for en enkelt ordbog på sideniveau modellerer det geografiske data som en side-Viewport med en tilknyttet Measure-ordbog, og målingsordbogen navngiver et geografisk koordinatsystem. Dette er den kodning, som Acrobat og nuværende GIS-eksportører skriver. En robust importør kontrollerer for begge: Læs viewports til ISO-modellen, og fald tilbage til (eller inspicer yderligere) LGIDict for filer, der kun bærer den ældre registrering.

Viewports og deres grænser

I ISO-modellen er enheden for georegistrering en viewport, og en side kan have flere. Et stort ark kan placere et hovedkort i ét rektangel, en indsats i en anden skala i et andet, og et tegnforklaringspanel, der slet ikke er georefereret. Hver viewport bærer en BBox, rektanglet på siden, som viewporten styrer, så læseren ved, hvilken del af arket et givet koordinatsystem gælder for. At teste et klikket punkt mod disse bokse er, hvordan en fremviser beslutter, hvilken målingsordbog der skal bruges.

PDFlibPas eksponerer den valgte sides viewports direkte. GetPageViewPortCount returnerer, hvor mange der er, GetPageViewPortID forvandler et 1-baseret indeks til et ViewPortID-håndtag, og GetViewPortBBox læser det afgrænsende rektangel én dimension ad gangen. The Dimension argument vælger, hvilken kant eller udstrækning du ønsker: 0 er Left, 1 is Top, 2 is Width, 3 is Height, 4 is Right, og 5 er 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;

Et ViewPortID på nul fra GetPageViewPortID betyder, at viewporten på dette indeks ikke kunne findes, så kontroller det, før du sender håndtaget videre.

Inde i målingsordbogen

Geometrien, der registrerer side til verden, lever i målingsordbogen (measure dictionary) tilknyttet en viewport. GetViewPortMeasureDict returnerer et MeasureDictID for et givet ViewPortID, eller nul, når viewporten ikke har nogen målingsordbog, hvilket er det normale tilfælde for et tegnforklarings- eller titelpanel. Målingsordbogen indeholder tre ting, der er værd at læse: de koordinatsystemer, den refererer til, de arrays, der binder sidepunkter til geografiske punkter, og enheden, som punktdatane udtrykkes i.

Selve registreringen er to parallelle arrays. GPTS is the array of geographic points, latitude and longitude pairs given in the geographic coordinate system. LPTS is the array of page-space points, expressed as fractions of the viewport's BBox so they survive scaling. Element n af LPTS og element n af GPTS navngiver den samme fysiske placering, én gang i sidekoordinater og én gang på jorden. Tre eller flere af disse par fastlægger den affine, eller i det generelle tilfælde projektive, transformation, der kortlægger enhver sidekoordinat inde i viewporten til en verdenskoodinat. At læse dem handler om at gennemgå begge arrays 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ålingsordbogen rapporterer også sine visningsenheder via GetMeasureDictPDU, som tager et UnitIndex på 1 for lineære, 2 for areal- eller 3 for vinkelenheder og returnerer en kode, der identificerer den specifikke enhed, f.eks. en meter eller en international fod for den lineære kategori. Bounds-arrayet, læst med GetMeasureDictBoundsItem, beskriver den firkant i viewporten, som målingen faktisk dækker, hvilket ikke altid er hele rektanglet.

WKT mod EPSG

Breddegraden og længdegraden i GPTS er meningsløse uden at vide, hvilket geografisk koordinatsystem de tilhører, da en koordinat på 51,5, -0,1 lander på et andet fysisk sted under WGS 84 end under et ældre nationalt datum. Målingsordbogen besvarer dette via en ordbog over koordinatsystemer, nået med GetMeasureDictGCSDict for det geografiske system. PDF beskriver dette system på en af to udskiftelige måder, og en læser skal acceptere begge.

Den første er WKT, Well-Known Text, en selvstændig streng, der staver datum, ellipsoide, nulmeridian og enheder fuldt ud. Den er ordrig, men entydig og behøver ingen ekstern opslagstabel. Den anden er en EPSG-kode, et enkelt heltal, der indekserer et koordinatsystem i EPSG-registret; 4326 er WGS 84, den ramme, som de fleste forbruger-GPS-data bruger. EPSG er kompakt, men forudsætter, at læseren kan slå koden op i en database. Filer optræder med den ene, den anden eller begge dele, hvilket er grunden til, at API'en eksponerer alle tre: GetCSDictType, GetCSDictEPSG og GetCSDictWKT. GetCSDictType rapporterer, om systemet er geografisk (en GEOGCS, returværdi 1) eller projekteret (en PROJCS, returværdi 2), så du kan tolke resten korrekt, før du stoler 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;

Læsning af den ældre LGIDict

Filer, der går forud for viewport-modellen, eller som blev produceret af værktøjer, der stadig afgiver den ældre kodning, bærer deres registrering i en LGIDict på siden i stedet for i en målingsordbog. PDFlibPas rapporterer, hvor mange af sådanne ordbøger en side har via GetPageLGIDictCount og leverer det rå indhold af hver med GetPageLGIDictContent, indekseret fra et. Den returnerede tekst er ordbogen som skrevet, der indeholder OGC 08-139r2-registreringsfelterne, som din kode derefter fortolker for at gendanne den samme slags side-til-verden-kortlægning, som målingsordbogen giver. På skriverens side knytter AddLGIDictToPage en LGIDict til den aktuelle side, så en konverter kan behandle den ældre form fuldstændigt, når en gammel forbruger 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;

Samling af læsningen

En komplet importør behandler de to systemer som et par gennemgange over hver side. Vælg siden, spørg GetPageViewPortCount efter ISO-viewports, og for hver viewport, der ejer en målingsordbog, træk dens BBox, dens GPTS- og LPTS-arrays, dens punktdataenhed og GCS-beskrivelsen via koordinatsystemsordbogen. Kontroller derefter GetPageLGIDictCount for enhver ældre registrering, som viewport-gennemgangen ikke dækkede. Et kort, der bærer begge, bør være enige indbyrdes; et kort, der kun bærer ét, løses stadig, fordi du kiggede begge steder. Håndtagene returneret undervejs, ViewPortID, MeasureDictID, CSDictID, are plain integers that stay valid while the document is loaded, so the whole walk is a few nested loops over the page list with no allocation to manage.

Når du kan gendanne registreringen, siden bliver en datakilde frem for et billede. De tilhørende teknikker 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.