Technical Article

GeoPDF dans Delphi : Coordonnées géospatiales avec PDFlibPas

La plupart des développeurs voient une page PDF comme une feuille de papier contenant du texte et des images. Un PDF géoréférencé est bien plus que cela. Il intègre suffisamment d'informations pour qu'un point de la page, mesuré en unités de page ordinaires, corresponde à sa latitude et sa longitude réelles sur Terre. C'est ce qui fait du PDF un support adapté pour une carte topographique, un plan cadastral, une carte des zones inondables ou tout export SIG devant être imprimé tout en restant exploitable géographiquement. Les données géométriques figurent dans le fichier ; la seule question est de savoir si votre chargeur sait les interpréter.

Si cet aspect est souvent ignoré, c'est parce qu'un GeoPDF s'ouvre et s'imprime exactement comme n'importe quel autre PDF. Rien sur la page affichée n'indique que la carte est calée sur un système de coordonnées. Le calage réside dans des dictionnaires rattachés à l'objet page, jamais dessinés, et un lecteur qui les ignore affichera la carte normalement. Pour effectuer la moindre opération spatiale sur le fichier (lecture de coordonnées d'arpentage, reprojection, superposition à d'autres couches), vous devez parcourir ces dictionnaires par vous-même.

Deux normes coexistent sur le terrain

Un lecteur conçu pour gérer les fichiers réels doit prendre en charge deux schémas de géoréférencement, car tous deux sont en circulation et un fichier donné peut utiliser l'un ou l'autre. Le plus ancien est l'encodage OGC décrit dans la note OGC 08-139r2, qui associe un dictionnaire LGIDict (dictionnaire d'enregistrement géospatial) à la page. Précédant toute validation ISO, il était le format de fait des premières générations de fichiers GeoPDF, de sorte qu'une grande quantité de cartes historiques l'exploite à l'exclusion de tout autre.

Le schéma moderne est celui normalisé par l'ISO dans la norme ISO 32000-1 §8.8.2. Au lieu d'un dictionnaire unique au niveau de la page, il modélise les données géospatiales sous forme de fenêtres d'affichage (Viewports) de pages dotées d'un dictionnaire Measure associé, lequel désigne un système de coordonnées géographiques. C'est l'encodage qu'écrivent Acrobat et les outils d'export SIG actuels. Un importateur robuste recherche les deux : il lit les fenêtres d'affichage pour le modèle ISO et se rabat sur (ou inspecte parallèlement) l'LGIDict pour les fichiers qui ne comportent que le calage hérité.

Les fenêtres d'affichage et leurs limites

Dans le modèle ISO, l'unité de géoréférencement est la fenêtre d'affichage (viewport), et une page peut en comporter plusieurs. Une grande feuille peut positionner la carte principale dans un rectangle, un encart à une échelle différente dans un autre et un panneau de légende non géoréférencé à côté. Chaque fenêtre d'affichage possède une boîte englobante BBox (le rectangle sur la page qu'elle régit), afin que le lecteur sache à quelle zone s'applique un système de coordonnées. Le test de positionnement d'un clic de l'utilisateur dans ces boîtes permet au lecteur de choisir le dictionnaire Measure approprié.

PDFlibPas expose directement les fenêtres d'affichage de la page sélectionnée. GetPageViewPortCount renvoie leur nombre, GetPageViewPortID convertit un index à base un en un identifiant de fenêtre d'affichage (ViewPortID), et GetViewPortBBox lit le rectangle de délimitation une dimension à la fois. L'officiel paramètre Dimension sélectionne le côté ou la dimension souhaitée : 0 pour Left, 1 pour Top, 2 pour Width, 3 pour Height, 4 pour Right et 5 pour 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;

Un ViewPortID égal à zéro renvoyé par GetPageViewPortID signifie que la fenêtre d'affichage à cet index n'a pas pu être trouvée. Il convient donc de le vérifier avant de transmettre ce descripteur.

Dans le dictionnaire de mesure

La géométrie qui associe la page au monde physique réside dans le dictionnaire Measure associé à une fenêtre d'affichage. GetViewPortMeasureDict renvoie un identifiant de dictionnaire Measure (MeasureDictID) pour un ViewPortID donné, ou zéro si la fenêtre n'en dispose pas, ce qui est habituel pour une légende ou un titre. Ce dictionnaire de mesure contient trois éléments d'intérêt : les systèmes de coordonnées de référence, les tableaux reliant les points de la page aux coordonnées géographiques et l'unité dans laquelle les données de points sont exprimées.

Le calage lui-même s'effectue via deux tableaux parallèles. GPTS est le tableau des points géographiques, à savoir les paires de latitude et longitude définies dans le système de coordonnées géographiques. LPTS est le tableau des points dans l'espace de la page, exprimés sous forme de fractions de la boîte de délimitation (BBox) de la fenêtre d'affichage pour résister aux mises à l'échelle. L'élément n de LPTS et l'élément n de GPTS désignent le même emplacement géographique, une fois dans les coordonnées de la page et une fois sur le globe. Trois couples de ce type ou plus définissent la transformation affine (ou projective dans le cas général) qui associe chaque coordonnée de page de la fenêtre d'affichage à des coordonnées réelles. Leur lecture s'effectue en parcourant les deux tableaux en parallèle.

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;

Le dictionnaire de mesure indique également ses unités d'affichage via GetMeasureDictPDU, qui accepte un UnitIndex valant 1 pour les unités linéaires, 2 pour les surfaces ou 3 pour les unités angulaires, et renvoie un code désignant l'unité (par exemple le mètre ou le pied international pour les mesures linéaires). Le tableau Bounds, lu avec GetMeasureDictBoundsItem, décrit le quadrilatère au sein de la fenêtre d'affichage auquel s'applique réellement la mesure, ce qui ne correspond pas toujours au rectangle complet.

WKT contre EPSG

La latitude et la longitude de GPTS n'ont pas de sens si l'on ignore le système de coordonnées géographiques auquel elles appartiennent, une coordonnée de 51.5, -0.1 désignant un lieu physique différent sous WGS 84 par rapport à un système de référence national plus ancien. Le dictionnaire Measure y répond via un dictionnaire de système de coordonnées, accessible avec GetMeasureDictGCSDict pour le système géographique. Le PDF décrit ce système de deux manières équivalentes, et un lecteur doit savoir gérer les deux.

La première est le WKT (Well-Known Text), une chaîne de caractères autonome qui décrit en détail le datum, l'ellipsoïde, le méridien d'origine et les unités. Elle est verbeuse mais sans ambiguïté et n'exige aucune table de correspondance externe. La seconde est un code EPSG, un entier qui indexe un système de coordonnées dans le registre EPSG ; 4326 correspond à WGS 84, le référentiel utilisé par la plupart des récepteurs GPS grand public. L'EPSG est compact mais suppose que le lecteur sache associer ce code à une base de données. Les fichiers peuvent comporter l'une, l'autre ou les deux informations, c'est pourquoi l'API propose les fonctions GetCSDictType, GetCSDictEPSG et GetCSDictWKT. GetCSDictType indique si le système est géographique (GEOGCS, valeur de retour 1) ou projeté (PROJCS, valeur de retour 2), vous permettant d'interpréter le reste de manière adéquate.

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;

Lire le dictionnaire historique LGIDict

Les fichiers antérieurs au modèle de fenêtre d'affichage, ou produits par des outils écrivant encore l'ancien format, intègrent leur géoréférencement dans un LGIDict sur la page plutôt que dans un dictionnaire Measure. PDFlibPas signale le nombre de ces dictionnaires sur une page via GetPageLGIDictCount et renvoie le contenu brut de chacun d'eux avec GetPageLGIDictContent, indexé à partir de un. Le texte renvoyé correspond au dictionnaire tel qu'il est écrit, contenant les champs de géoréférencement de la note OGC 08-139r2, que votre code peut ensuite analyser pour retrouver la même correspondance page/coordonnées réelles. Côté écriture, AddLGIDictToPage associe un LGIDict à la page courante, permettant à un convertisseur de restituer la forme historique si un lecteur plus ancien l'exige.

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;

Mise en œuvre globale de la lecture

Un importateur complet traite les deux schémas en effectuant deux passes sur chaque page. Sélectionnez la page, lisez les fenêtres d'affichage ISO avec GetPageViewPortCount, et pour chaque fenêtre disposant d'un dictionnaire Measure, extrayez sa BBox, ses tableaux GPTS et LPTS, son unité de mesure et la description GCS du dictionnaire de coordonnées. Vérifiez ensuite avec GetPageLGIDictCount s'il existe un géoréférencement historique non pris en compte par l'étape précédente. Si une carte comporte les deux encodages, leurs valeurs doivent coïncider ; si elle n'en comporte qu'un, la résolution fonctionne tout de même car vous avez vérifié les deux emplacements. Les descripteurs renvoyés en chemin (ViewPortID, MeasureDictID, CSDictID) sont de simples entiers valides tant que le document reste chargé, de sorte que l'ensemble du traitement se résume à quelques boucles imbriquées sur les pages, sans allocation à gérer.

Dès lors que le géoréférencement est récupéré, la page devient une source de données et plus une simple image. Les techniques complémentaires pour lire le reste de la page sont présentées dans l'article sur l'extraction de texte, d'images et de polices, et le rendu d'une feuille géoréférencée sur un périphérique pour des mesures à l'écran est décrit dans le guide sur le contexte graphique d'impression et d'aperçu. Le lecteur géospatial décrit ici fait partie de la bibliothèque PDF losLab pour Delphi et C++Builder, aux côtés des API de chargement, d'extraction et de rendu couvertes ailleurs sur ce blog.