Οι περισσότεροι προγραμματιστές σκέφτονται μια σελίδα PDF ως ένα φύλλο χαρτιού με κείμενο και εικόνες πάνω του. Ένα γεωαναφερμένο (georeferenced) PDF είναι κάτι παραπάνω από αυτό. Μεταφέρει αρκετές πληροφορίες ώστε να πάρει ένα σημείο στη σελίδα, μετρημένο σε συνηθισμένες μονάδες σελίδας, και να αναφέρει το γεωγραφικό πλάτος και μήκος στο οποίο βρίσκεται στον πραγματικό κόσμο. Αυτό το απλό γεγονός είναι που μετατρέπει ένα PDF σε έναν χρήσιμο φορέα για έναν τοπογραφικό χάρτη, ένα κτηματολογικό διάγραμμα, ένα σχέδιο πλημμυρικής ζώνης ή οποιαδήποτε εξαγωγή GIS που πρέπει να εκτυπωθεί και να συνεχίσει να σημαίνει κάτι. Η γεωμετρία υπάρχει στο αρχείο. Το μόνο ερώτημα είναι αν ο φορτωτής σας τη διαβάζει.
Ο λόγος που αυτό παραβλέπεται είναι ότι ένα GeoPDF ανοίγει και εκτυπώνεται ακριβώς όπως οποιοδήποτε άλλο PDF. Τίποτα στην αποδιδόμενη σελίδα δεν αναγγέλλει ότι ο χάρτης είναι καταχωρημένος σε ένα σύστημα συντεταγμένων. Η καταχώριση ζει σε λεξικά (dictionaries) που κρέμονται από το αντικείμενο της σελίδας, δεν σχεδιάζεται ποτέ, και ένα πρόγραμμα προβολής που τα αγνοεί σας δείχνει τον χάρτη παρ' όλα αυτά. Για να κάνετε οτιδήποτε χωρικό με το αρχείο, αναγνώσεις συντεταγμένων αποτύπωσης, επαναπροβολή, επικάλυψη με άλλα επίπεδα, πρέπει να περιηγηθείτε σε αυτά τα λεξικά μόνοι σας.
Δύο πρότυπα υπάρχουν σε κυκλοφορία
Ένας αναγνώστης που θέλει να χειρίζεται αρχεία του πραγματικού κόσμου πρέπει να αντιμετωπίσει δύο σχήματα γεωγραφικής καταχώρισης (georegistration), επειδή και τα δύο βρίσκονται σε κυκλοφορία και ένα δεδομένο αρχείο μπορεί να χρησιμοποιεί οποιοδήποτε από αυτά. Το παλαιότερο είναι η κωδικοποίηση OGC που περιγράφεται στο OGC 08-139r2, η οποία προσαρτά ένα LGIDict (ένα λεξικό γεωχωρικής καταχώρισης) στη σελίδα. Προηγείται οποιασδήποτε έγκρισης ISO και ήταν η de facto μορφή για τις πρώτες εξόδους GeoPDF, επομένως ένας μεγάλος όγκος παλαιών χαρτών το φέρει και τίποτα άλλο.
Το σύγχρονο σχήμα είναι αυτό που ο ISO τυποποίησε στο ISO 32000-1 §8.8.2. Αντί για ένα ενιαίο λεξικό σε επίπεδο σελίδας, μοντελοποιεί τα γεωχωρικά δεδομένα ως μια σελίδα Viewport με ένα προσαρτημένο λεξικό Measure, και το λεξικό measure ονομάζει ένα γεωγραφικό σύστημα συντεταγμένων. Αυτή είναι η κωδικοποίηση που γράφουν το Acrobat και οι τρέχοντες εξαγωγείς GIS. Ένας ισχυρός εισαγωγέας ελέγχει και για τα δύο: διαβάζει τα viewports για το μοντέλο ISO, και επιστρέφει (ή επιθεωρεί επιπλέον) στο LGIDict για αρχεία που φέρουν μόνο την παλιά καταχώριση.
Viewports και τα όριά τους
Στο μοντέλο ISO, η μονάδα γεωγραφικής καταχώρισης είναι το viewport, και μια σελίδα μπορεί να έχει πολλά. Ένα μεγάλο φύλλο μπορεί να τοποθετήσει έναν κύριο χάρτη σε ένα ορθογώνιο, ένα ένθετο σε διαφορετική κλίμακα σε ένα άλλο, και ένα πάνελ υπομνήματος που δεν είναι καθόλου γεωαναφερμένο. Κάθε viewport φέρει ένα BBox, το ορθογώνιο στη σελίδα που διέπει το viewport, ώστε ο αναγνώστης να γνωρίζει σε ποιο μέρος του φύλλου εφαρμόζεται ένα δεδομένο σύστημα συντεταγμένων. Ο έλεγχος πρόσκρουσης (hit-testing) ενός σημείου που έχει γίνει κλικ έναντι αυτών των πλαισίων είναι ο τρόπος με τον οποίο ένα πρόγραμμα προβολής αποφασίζει ποιο λεξικό measure θα χρησιμοποιήσει.
PDFlibPas εκθέτει τα viewports της επιλεγμένης σελίδας απευθείας. Η GetPageViewPortCount επιστρέφει πόσα υπάρχουν, η GetPageViewPortID μετατρέπει έναν δείκτη μονοσήμαντης βάσης (one-based index) σε λαβή ViewPortID, και η GetViewPortBBox διαβάζει το ορθογώνιο οριοθέτησης μία διάσταση τη φορά. Το όρισμα Dimension επιλέγει ποιο όριο ή έκταση θέλετε: 0 είναι Left, 1 είναι Top, 2 είναι Width, 3 είναι Height, 4 είναι Right, και 5 είναι 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;
Μια τιμή ViewPortID ίση με μηδέν από την GetPageViewPortID σημαίνει ότι το viewport σε αυτόν τον δείκτη δεν μπόρεσε να βρεθεί, οπότε ελέγξτε το πριν μεταβιβάσετε τη λαβή.
Μέσα στο λεξικό measure
Η γεωμετρία που καταχωρεί τη σελίδα στον κόσμο ζει στο λεξικό measure που είναι προσαρτημένο σε ένα viewport. Η GetViewPortMeasureDict επιστρέφει ένα MeasureDictID για ένα δεδομένο ViewPortID, ή μηδέν όταν το viewport δεν έχει λεξικό measure, που είναι η συνηθισμένη περίπτωση για ένα πάνελ υπομνήματος ή τίτλου. Το λεξικό measure κρατά τρία πράγματα που αξίζει να διαβαστούν: τα συστήματα συντεταγμένων στα οποία αναφέρεται, τους πίνακες που συνδέουν τα σημεία της σελίδας με γεωγραφικά σημεία, και τη μονάδα στην οποία εκφράζονται τα δεδομένα των σημείων.
Η ίδια η καταχώριση είναι δύο παράλληλοι πίνακες. Ο GPTS είναι ο πίνακας γεωγραφικών σημείων, ζεύγη γεωγραφικού πλάτους και μήκους που δίνονται στο γεωγραφικό σύστημα συντεταγμένων. Ο LPTS είναι ο πίνακας σημείων χώρου σελίδας, εκφρασμένα ως κλάσματα του BBox του viewport, ώστε να επιβιώνουν από την κλιμάκωση. Το στοιχείο n του LPTS και το στοιχείο n του GPTS ονομάζουν την ίδια φυσική τοποθεσία, μία φορά σε συντεταγμένες σελίδας και μία φορά στην υδρόγειο. Τρία ή περισσότερα τέτοια ζεύγη καθορίζουν τον συγγενή (affine), ή στη γενική περίπτωση προβολικό (projective), μετασχηματισμό που χαρτογραφεί οποιαδήποτε συντεταγμένη σελίδας μέσα στο viewport σε μια παγκόσμια συντεταγμένη. Η ανάγνωσή τους είναι θέμα διάσχισης και των δύο πινάκων παράλληλα.
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;
Το λεξικό measure αναφέρει επίσης τις μονάδες εμφάνισής του μέσω της GetMeasureDictPDU, η οποία λαμβάνει έναν δείκτη UnitIndex με 1 για γραμμικές, 2 για εμβαδικές ή 3 για γωνιακές μονάδες και επιστρέφει έναν κώδικα που προσδιορίζει τη συγκεκριμένη μονάδα, για παράδειγμα το μέτρο ή το διεθνές πόδι για τη γραμμική κατηγορία. Ο πίνακας Bounds, που διαβάζεται με την GetMeasureDictBoundsItem, περιγράφει το τετράπλευρο εντός του viewport που καλύπτει πραγματικά η μέτρηση, το οποίο δεν είναι πάντα ολόκληρο το ορθογώνιο.
WKT έναντι EPSG
Το γεωγραφικό πλάτος και μήκος στον GPTS είναι άσκοπα χωρίς να γνωρίζουμε σε ποιο γεωγραφικό σύστημα συντεταγμένων ανήκουν, καθώς μια συντεταγμένη 51.5, -0.1 lands σε διαφορετικό φυσικό σημείο υπό το WGS 84 από ό,τι υπό ένα παλαιότερο εθνικό σύστημα (datum). Το λεξικό measure απαντά σε αυτό μέσω ενός λεξικού συστήματος συντεταγμένων, το οποίο προσεγγίζεται με την GetMeasureDictGCSDict για το γεωγραφικό σύστημα. Το PDF περιγράφει αυτό το σύστημα με έναν από δύο εναλλάξιμους τρόπους, και ένας αναγνώστης πρέπει να αποδέχεται οποιονδήποτε από τους δύο.
Ο πρώτος είναι το WKT (Well-Known Text), μια αυτοτελής συμβολοσειρά που περιγράφει πλήρως το datum, το ελλειψοειδές, τον πρώτο μεσημβρινό και τις μονάδες. Είναι αναλυτικός αλλά ξεκάθαρος και δεν χρειάζεται εξωτερικό πίνακα αναζήτησης. Ο δεύτερος είναι ένας κώδικας EPSG, ένας μεμονωμένος ακέραιος που ευρετηριάζει ένα σύστημα συντεταγμένων στο μητρώο EPSG. Ο κώδικας 4326 είναι το WGS 84, το πλαίσιο που χρησιμοποιούν τα περισσότερα καταναλωτικά δεδομένα GPS. Το EPSG είναι συμπαγές αλλά υποθέτει ότι ο αναγνώστης μπορεί να επιλύσει τον κώδικα έναντι μιας βάσης δεδομένων. Τα αρχεία εμφανίζονται με το ένα, το άλλο ή και τα δύο, γι' αυτό και το API εμφανίζει και τα τρία: GetCSDictType, GetCSDictEPSG και GetCSDictWKT. Η GetCSDictType αναφέρει εάν το σύστημα είναι γεωγραφικό (GEOGCS, τιμή επιστροφής 1) ή προβεβλημένο (PROJCS, τιμή επιστροφής 2), επιτρέποντάς σας να ερμηνεύσετε τα υπόλοιπα σωστά προτού τα εμπιστευτείτε.
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;
Ανάγνωση του παλαιού LGIDict
Αρχεία που προηγούνται του μοντέλου viewport, ή που παρήχθησαν από εργαλεία που εξακολουθούν να εκπέμπουν την παλαιότερη κωδικοποίηση, μεταφέρουν την καταχώρισή τους σε ένα LGIDict στη σελίδα αντί για ένα λεξικό measure. Το PDFlibPas αναφέρει πόσα τέτοια λεξικά έχει μια σελίδα μέσω της GetPageLGIDictCount και επιστρέφει το ακατέργαστο περιεχόμενο καθενός με την GetPageLGIDictContent, με ευρετηρίαση από το ένα. Το επιστρεφόμενο κείμενο είναι το λεξικό όπως γράφτηκε, κρατώντας τα πεδία καταχώρισης OGC 08-139r2, τα οποία ο κώδικάς σας στη συνέχεια αναλύει για να ανακτήσει την ίδια χαρτογράφηση σελίδας προς κόσμο που παρέχει το λεξικό measure. Στην πλευρά της εγγραφής, η AddLGIDictToPage προσαρτά ένα LGIDict στην τρέχουσα σελίδα, ώστε ένας μετατροπέας να μπορεί να επεξεργαστεί πλήρως (round-trip) την παλιά μορφή όταν ένας παλαιός καταναλωτής την αναμένει ακόμα.
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;
Σύνθεση της διαδικασίας ανάγνωσης
Ένας ολοκληρωμένος εισαγωγέας αντιμετωπίζει τα δύο σχήματα ως ένα ζευγάρι περασμάτων πάνω από κάθε σελίδα. Επιλέξτε τη σελίδα, ζητήστε από την GetPageViewPortCount τα viewports ISO, και για κάθε viewport που κατέχει ένα λεξικό measure τραβήξτε το BBox του, τους πίνακες GPTS και LPTS, τη μονάδα δεδομένων σημείου του, και την περιγραφή GCS μέσω του λεξικού συστήματος συντεταγμένων. Στη συνέχεια, ελέγξτε την GetPageLGIDictCount για τυχόν παλιές καταχωρίσεις που δεν κάλυψε το πέρασμα του viewport. Ένας χάρτης που μεταφέρει και τα δύο θα πρέπει να συμφωνεί μεταξύ τους. Ένας χάρτης που μεταφέρει μόνο το ένα εξακολουθεί να επιλύεται, επειδή κοιτάξατε και στα δύο μέρη. Οι λαβές που επιστρέφονται κατά τη διάρκεια της διαδικασίας, ViewPortID, MeasureDictID, CSDictID, είναι απλοί ακέραιοι που παραμένουν έγκυροι όσο το έγγραφο είναι φορτωμένο, οπότε ολόκληρη η περιήγηση είναι μερικοί φωλιασμένοι βρόχοι πάνω στη λίστα σελίδων χωρίς ανάγκη διαχείρισης κατανομής μνήμης.
Μολις μπορέσετε να ανακτήσετε την καταχώριση, η σελίδα γίνεται πηγή δεδομένων αντί για εικόνα. Οι συνοδευτικές τεχνικές για την ανάγνωση του υπόλοιπου περιεχομένου μιας σελίδας καλύπτονται στο άρθρο μας σχετικά με την εξαγωγή κειμένου, εικόνας και γραμματοσειρών, και η απόδοση ενός γεωαναφερμένου φύλλου σε μια συσκευή για μέτρηση επί της οθόνης περιγράφεται στον οδηγό για το πλαίσιο συσκευής εκτύπωσης και προεπισκόπησης (device-context). Ο γεωχωρικός αναγνώστης που περιγράφεται εδώ παρέχεται ως μέρος της losLab PDF Library για Delphi και C++Builder, μαζί με τα API φόρτωσης, εξαγωγής και απόδοσης που καλύπτονται σε άλλα σημεία αυτού του ιστολογίου.