Die meisten Entwickler stellen sich eine PDF-Seite wie ein Blatt Papier mit Text und Bildern vor. Ein georeferenziertes PDF ist mehr als das. Es enthält genügend Informationen, um einen Punkt auf der Seite (gemessen in normalen Seiteneinheiten) zu erfassen und den Breitengrad sowie den Längengrad auszugeben, über dem er sich in der realen Welt befindet. Diese einzige Tatsache macht ein PDF zu einem brauchbaren Träger für eine topografische Karte, einen Katasterplan, einen Überschwemmungsgebietsnachweis oder jeden GIS-Export, der gedruckt werden muss und dennoch eine Bedeutung haben soll. Die Geometrie ist in der Datei vorhanden; die einzige Frage ist, ob Ihr Lader sie liest
Der Grund dafür, dass dies übersehen wird, liegt darin, dass sich ein GeoPDF genau wie jedes andere PDF öffnen und drucken lässt. Nichts auf der gerenderten Seite deutet darauf hin, dass die Karte für ein Koordinatensystem registriert ist. Die Registrierung befindet sich in Wörterbüchern (Dictionaries), die am Seitenobjekt hängen und niemals gezeichnet werden. Ein Viewer, der sie ignoriert, zeigt Ihnen die Karte dennoch an. Um räumliche Operationen mit der Datei durchzuführen - wie das Ablesen von Vermessungskoordinaten, Reprojektionen oder Überlagerungen mit anderen Ebenen -, müssen Sie diese Wörterbücher selbst durchlaufen
Zwei Standards existieren in der Praxis
Ein Reader, der praxisrelevante Dateien verarbeiten möchte, muss mit zwei Georegistrierungsschemata zurechtkommen, da beide im Umlauf sind und eine bestimmte Datei eines von beiden verwenden kann. Das ältere ist die in OGC 08-139r2 beschriebene OGC-Codierung, die ein LGIDict (ein geospatiales Registrierungswörterbuch) an die Seite anhängt. Sie stammt aus der Zeit vor jeder ISO-Zulassung und war das De-facto-Format für frühe GeoPDF-Ausgaben, weshalb ein großer Teil älterer Karten nur diese Codierung trägt
Das moderne Schema ist dasjenige, das von der ISO in ISO 32000-1 §8.8.2 standardisiert wurde. Anstelle eines einzelnen Wörterbuchs auf Seitenebene modelliert es geospatiale Daten als Viewport der Seite mit einem angehängten Measure-Wörterbuch, und dieses Measure-Wörterbuch benennt ein geografisches Koordinatensystem. Dies ist die Codierung, die Acrobat und aktuelle GIS-Exporter schreiben. Ein robuster Importer prüft auf beide: Er liest die Viewports für das ISO-Modell und fällt bei Dateien, die nur die alte Registrierung tragen, auf das LGIDict zurück (oder inspiziert es zusätzlich)
Viewports und ihre Grenzen
Im ISO-Modell ist die Einheit der Georegistrierung der Viewport, und eine Seite kann mehrere davon haben. Ein großes Blatt kann eine Hauptkarte in einem Rechteck, eine Detailkarte mit einem anderen Maßstab in einem anderen und ein Legendenfeld platzieren, das überhaupt nicht georeferenziert ist. Jeder Viewport trägt eine BBox, das Rechteck auf der Seite, das der Viewport regelt, sodass der Reader weiß, für welchen Teil des Blattes ein bestimmtes Koordinatensystem gilt. Durch das Testen eines angeklickten Punktes gegen diese Boxen (Hit-Testing) entscheidet ein Viewer, welches Measure-Wörterbuch zu verwenden ist
PDFlibPas legt die Viewports des ausgewählten Dokuments direkt offen. GetPageViewPortCount gibt an, wie viele vorhanden sind, GetPageViewPortID wandelt einen einsbasierten Index in ein ViewPortID-Handle um, und GetViewPortBBox liest das begrenzende Rechteck Dimension für Dimension aus. Das Dimension-Argument wählt aus, welche Kante oder Ausdehnung Sie wünschen: 0 ist Left, 1 ist Top, 2 ist Width, 3 ist Height, 4 ist Right und 5 ist 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;
Eine ViewPortID von Null aus GetPageViewPortID bedeutet, dass der Viewport unter diesem Index nicht gefunden werden konnte. Überprüfen Sie dies, bevor Sie das Handle weitergeben
Im Measure-Wörterbuch
Die Geometrie, die die Seite mit der Welt registriert, befindet sich im Measure-Wörterbuch, das an einen Viewport angehängt ist. GetViewPortMeasureDict gibt eine MeasureDictID für eine gegebene ViewPortID zurück oder Null, wenn der Viewport kein Measure-Wörterbuch besitzt, was der Normalfall für ein Legenden- oder Titelfeld ist. Das Measure-Wörterbuch enthält drei lesenswerte Dinge: die Koordinatensysteme, auf die es verweist, die Arrays, die Seitenpunkte mit geografischen Punkten verknüpfen, und die Einheit, in der die Punktdaten ausgedrückt werden
Die Registrierung selbst besteht aus zwei parallelen Arrays. GPTS is das Array der geografischen Punkte, also Breitengrad- und Längengradpaare im geografischen Koordinatensystem. LPTS ist das Array der Punkte im Seitenbereich, ausgedrückt als Bruchteile der BBox des Viewports, sodass sie eine Skalierung überstehen. Element n von LPTS und Element n von GPTS benennen denselben physischen Ort, einmal in Seitenkoordinaten und einmal auf dem Globus. Drei oder mehr solcher Paare bestimmen die affine, oder im allgemeinen Fall projektive Transformation, die jede Seitenkoordinate innerhalb des Viewports einer Weltkoordinate zuordnet. Das Auslesen ist eine Frage des synchronen Durchlaufens beider 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;
Das Measure-Wörterbuch meldet auch seine Anzeigeeinheiten über GetMeasureDictPDU, das einen UnitIndex von 1 für lineare, 2 für Flächen- oder 3 für Winkeleinheiten entgegennimmt und einen Code zur Identifizierung der spezifischen Einheit zurückgibt (z. B. Meter oder internationaler Fuß für die lineare Kategorie). Das Bounds-Array, ausgelesen mit GetMeasureDictBoundsItem, beschreibt das Viereck innerhalb des Viewports, das die Messung tatsächlich abdeckt - was nicht immer das vollständige Rechteck ist
Praktischer Kontext
Der Breitengrad und der Längengrad in GPTS sind bedeutungslos, ohne zu wissen, zu welchem geografischen Koordinatensystem sie gehören, da eine Koordinate von 51.5, -0.1 lands in a different physical spot under WGS 84 than under an older national datum. Das Measure-Wörterbuch beantwortet dies über ein Koordinatensystemwörterbuch, erreichbar über GetMeasureDictGCSDict für das geografische System. PDF beschreibt dieses System auf eine von zwei austauschbaren Arten, und ein Reader muss beide akzeptieren
Die erste ist WKT (Well-Known Text), ein in sich geschlossener String, der das Datum, das Ellipsoid, den Nullmeridian und die Einheiten vollständig ausschreibt. Es ist ausführlich, aber eindeutig und erfordert keine externe Nachschlagetabelle. Die zweite ist ein EPSG-Code, eine einzelne Ganzzahl, die ein Koordinatensystem im EPSG-Register indiziert; 4326 ist WGS 84, der Rahmen, den die meisten GPS-Daten für Verbraucher verwenden. EPSG ist kompakt, setzt aber voraus, dass der Reader den Code mit einer Datenbank auflösen kann. Dateien erscheinen mit dem einen, dem anderen oder beiden, weshalb die API alle drei Methoden bereitstellt: GetCSDictType, GetCSDictEPSG und GetCSDictWKT. GetCSDictType meldet, ob das System geografisch (ein GEOGCS, Rückgabewert 1) oder projiziert (ein PROJCS, Rückgabewert 2) ist, sodass Sie den Rest korrekt interpretieren können, bevor Sie ihm vertrauen
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;
Lesen des veralteten LGIDict
Dateien, die älter als das Viewport-Modell sind oder von Werkzeugen erstellt wurden, die noch die ältere Codierung ausgeben, tragen ihre Registrierung in einem LGIDict auf der Seite und nicht in einem Measure-Wörterbuch. PDFlibPas meldet über GetPageLGIDictCount, wie viele solcher Wörterbücher eine Seite hat, und gibt den Rohinhalt jedes einzelnen mit GetPageLGIDictContent (einsbasiert indiziert) zurück. Der zurückgegebene Text ist das Wörterbuch im Originalzustand und enthält die OGC 08-139r2-Registrierungsfelder, die Ihr Code dann parst, um dieselbe Art von Zuordnung zwischen Seite und realer Welt wiederherzustellen, die das Measure-Wörterbuch bereitstellt. Auf der Schreibseite hängt AddLGIDictToPage ein LGIDict an die aktuelle Seite an, sodass ein Konverter die veraltete Form im Round-Trip verarbeiten kann, wenn ein alter Empfänger dies noch erwartet
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;
Zusammenfassung des Lesevorgangs
Ein vollständiger Importer behandelt die beiden Schemata als zwei Durchgänge über jede Seite. Wählen Sie die Seite aus, fragen Sie GetPageViewPortCount nach den ISO-Viewports ab, und rufen Sie für jeden Viewport, der ein Measure-Wörterbuch besitzt, seine BBox, seine GPTS- und LPTS-Arrays, seine Punktdateneinheit sowie die GCS-Beschreibung über das Koordinatensystemwörterbuch ab. Überprüfen Sie dann GetPageLGIDictCount auf eventuelle alte Registrierungen, die der Viewport-Durchlauf nicht abgedeckt hat. Eine Karte, die beide trägt, sollte inhaltlich übereinstimmen; eine Karte, die nur eines davon trägt, lässt sich dennoch auflösen, da Sie an beiden Stellen gesucht haben. Die auf diesem Weg zurückgegebenen Handles (ViewPortID, MeasureDictID, CSDictID) sind einfache Ganzzahlen, die gültig bleiben, während das Dokument geladen ist, sodass der gesamte Durchgang aus ein paar verschachtelten Schleifen über die Seitenliste besteht, ohne dass Speicherzuweisungen zu verwalten sind
Sobald Sie die Registrierung wiederherstellen können, die Seite wird zu einer Datenquelle und nicht nur zu einem Bild. Die begleitenden Techniken zum Lesen des Rests einer Seite werden in the article on text, image, and font extraction behandelt. Das Rendern eines georeferenzierten Bogens auf ein Gerät zur Messung auf dem Bildschirm wird in the print and preview device-context walkthrough beschrieben. Der hier beschriebene geospatiale Reader wird als Teil der losLab PDF Library für Delphi und C++Builder zusammen mit den APIs zum Laden, Extrahieren und Rendern ausgeliefert, die an anderer Stelle auf diesem Blog behandelt werden