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 på tillräckligt med information för att ta en punkt på sidan, mätt i vanliga sidenheter (page units), och rapportera den latitud och longitud den sitter över i den verkliga världen. Det enskilda faktumet är vad som gör en PDF till en användbar bärare för en topografisk karta, en fastighetskartläggning (cadastral survey plot), en utställning över översvämningszoner (flood-zone exhibit), eller vilken GIS-export som helst som måste skrivas ut och fortfarande betyda något. Geometrin finns där i filen; den enda frågan är ifall din inläsare (loader) läser den
Anledningen till att detta missas är att en GeoPDF öppnas och skrivs ut exakt som vilken annan PDF som helst. Ingenting i den renderade sidan tillkännager att kartan är registrerad mot ett koordinatsystem. Registreringen lever i lexikon (dictionaries) som hänger på sidobjektet (page object), ritas aldrig ut, och en visare (viewer) som ignorerar dem visar dig kartan ändå. För att göra något spatialt med filen, som mätutläsningar (surveying coordinate readouts), omprojicering (reprojection) eller överlägg (overlay) mot andra lager, måste du vandra genom de lexikonen själv
Två standarder lever i det vilda
En läsare som vill hantera verkliga filer måste klara av två georegistreringsscheman (georegistration schemes), eftersom båda är i omlopp och en given fil kan använda vilket som helst. Det äldre är OGC-kodningen beskriven i OGC 08-139r2, som fäster en LGIDict (ett geospatialt registreringslexikon) till sidan. Det föregick (predates) varje ISO-välsignelse och var de facto-formatet för tidig GeoPDF-utmatning, så en stor mängd legacy-kartor bär på det och ingenting annat
Det moderna schemat är det som ISO standardiserade i ISO 32000-1 §8.8.2. I stället för ett enda lexikon på sidnivå (page-level dictionary) modellerar den geospatiala data som en sidvyport (page Viewport) med ett vidfäst måttlexikon (Measure dictionary), och måttlexikonet namnger ett geografiskt koordinatsystem. Detta är den kodning Acrobat och nuvarande GIS-exporterare skriver. En robust importerare kontrollerar efter båda: läs vyportarna för ISO-modellen, och fall tillbaka på (eller inspektera ytterligare) LGIDict för filer som enbart bär på den äldre registreringen
Vyportar och deras gränser
I ISO-modellen är enheten för georegistrering vyporten (viewport), och en sida kan ha flera. Ett stort ark kan placera en huvudkarta i en rektangel, en infällning (inset) i en annan skala i en annan, och en teckenförklaringspanel (legend panel) som inte är georefererad alls. Varje vyport bär på en BBox, den rektangel på sidan som vyporten styr, så läsaren vet vilken del av arket som ett givet koordinatsystem gäller för. Träffprovning (Hit-testing) av en klickad punkt mot de boxarna är hur en visare beslutar vilket måttlexikon som ska användas
PDFlibPas exponerar vyportarna på den valda sidan direkt. GetPageViewPortCount returnerar hur många de är, GetPageViewPortID förvandlar ett ettbaserat (one-based) index till ett ViewPortID-handtag, och GetViewPortBBox läser den avgränsande rektangeln (bounding rectangle) en dimension i taget. Dimension-argumentet väljer vilken kant (edge) eller utsträckning (extent) du vill ha: 0 är Vänster (Left), 1 är Topp (Top), 2 är Bredd (Width), 3 är Höjd (Height), 4 är Höger (Right), och 5 är Botten (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;
Ett ViewPortID på noll från GetPageViewPortID betyder att vyporten på det indexet inte kunde hittas, så kontrollera det innan du skickar vidare handtaget
Inuti måttlexikonet
Geometrin som registrerar sida till värld (page to world) lever i det måttlexikon (measure dictionary) som är fäst vid en vyport. GetViewPortMeasureDict returnerar ett MeasureDictID för ett givet ViewPortID, eller noll när vyporten inte har något måttlexikon, vilket är det normala fallet för en teckenförklaring eller titelpanel (title panel). Måttlexikonet rymmer tre saker värda att läsa: koordinatsystemen den refererar till, arrayerna som knyter sidpunkter till geografiska punkter, och enheten i vilken punktdatan uttrycks
Registreringen i sig utgörs av två parallella arrayer. GPTS är arrayen av geografiska punkter, latitud- och longitud-par givna i det geografiska koordinatsystemet. LPTS är arrayen av sidrymdspunkter (page-space points), uttryckta som bråkdelar (fractions) av vyportens BBox så att de överlever skalning. Objekt n i LPTS och objekt n i GPTS namnger samma fysiska plats, en gång i sidkoordinater och en gång på jordklotet (globe). Tre eller fler sådana par spikar fast (pin down) den affina, eller i det allmänna fallet projektiva, transformen (transform) som mappar varje sidkoordinat inuti vyporten till en världskoordinat. Att läsa dem är en fråga om att vandra genom båda arrayerna i takt (in step)
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åttlexikonet rapporterar också sina visningsenheter genom 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 (international foot) för den linjära kategorin. Arrayen Bounds, läst med GetMeasureDictBoundsItem, beskriver den fyrsiding (quadrilateral) inuti vyporten som mätningen faktiskt täcker, vilken inte alltid är hela rektangeln
WKT kontra EPSG
Latituden och longituden i GPTS är meningslösa utan att man vet vilket geografiskt koordinatsystem de tillhör, eftersom en koordinat på 51.5, -0.1 landar på en annan fysisk plats under WGS 84 än under ett äldre nationellt datum (datum). Måttlexikonet besvarar detta genom ett koordinatsystemslexikon (coordinate system dictionary), nådd med GetMeasureDictGCSDict för det geografiska systemet. PDF beskriver det systemet på ett av två utbytbara (interchangeable) sätt, och en läsare måste acceptera vilket som
Det första är WKT, Well-Known Text, en innesluten (self-contained) sträng som bokstaverar datum, ellipsoid, nollmeridian (prime meridian) och enheter i sin helhet. Den är ordrik (verbose) men entydig och behöver ingen extern uppslagstabell. Den andra är en EPSG-kod, ett enskilt heltal som indexerar ett koordinatsystem i EPSG-registret; 4326 är WGS 84, den ram (frame) de flesta konsument-GPS-data använder. EPSG är kompakt men förutsätter att läsaren kan slå upp koden mot en databas. Filer dyker upp med det ena, det andra, eller båda, vilket är anledningen till varför API:et exponerar alla tre av GetCSDictType, GetCSDictEPSG och GetCSDictWKT. GetCSDictType rapporterar huruvida 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å den
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;
Att läsa det äldre LGIDict
Filer som kom före (predate) vyportsmodellen, eller som producerades av verktyg som fortfarande genererar (emitting) den äldre kodningen, bär sin registrering i ett LGIDict på sidan snarare än i ett måttlexikon. PDFlibPas rapporterar hur många sådana lexikon en sida har genom GetPageLGIDictCount och lämnar tillbaka det råa innehållet för varje genom GetPageLGIDictContent, indexerat från ett. Den returnerade texten är lexikonet så som den skrevs, som rymmer OGC 08-139r2-registreringsfälten, vilka din kod därefter tolkar (parses) för att återvinna (recover) samma typ av sida-till-värld-mappning som måttlexikonet tillhandahåller. På skrivsidan (writing side) fäster AddLGIDictToPage ett LGIDict till den aktuella sidan, så en konverterare kan tur-och-retur-hantera (round-trip) det äldre formatet när en gammal konsument 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;
Att sätta ihop inläsningen
En komplett importerare (importer) behandlar de två schemana som ett par passeringar (passes) över varje sida. Välj sidan, be GetPageViewPortCount om ISO-vyportarna, och dra för varje vyport som äger ett måttlexikon in (pull) dess BBox, dess GPTS- och LPTS-arrayer, dess punktdatasenhet (point data unit), och GCS-beskrivningen genom koordinatsystemslexikonet. Kontrollera därefter GetPageLGIDictCount för all äldre registrering som vyportspasseringen inte täckte. En karta som bär på båda bör stämma överens mellan dem; en karta som enbart bär på ena löser sig ändå (still resolves), för att du letade på båda ställena. Handtagen som returneras längs vägen, ViewPortID, MeasureDictID, CSDictID, är vanliga heltal (plain integers) som förblir giltiga medan dokumentet är laddat, så hela vandringen är ett par nästlade loopar (nested loops) över sidlistan utan någon allokering att hantera
När du väl kan återvinna registreringen blir sidan en datakälla snarare än en bild. Medföljande tekniker för att läsa resten av en sida täcks in i artikeln om extrahering av text, bild och typsnitt, och renderandet av ett georefererat ark till en enhet (device) för mätning på skärm (on-screen measurement) beskrivs i utskrifts- och förhandsgransknings-genomgången för device-context. Den geospatiala inläsaren (reader) som beskrivs här skeppas som en del av losLab PDF Library för Delphi och C++Builder, jämsides med de inläsnings- (loading), extraherings- och renderande API:er som täcks på andra ställen i den här bloggen