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