Technical Article

GeoPDF en Delphi: Coordenadas geoespaciales con PDFlibPas

La mayoría de los desarrolladores piensan en una página PDF como una hoja de papel con texto e imágenes. Un PDF georreferenciado es más que eso. Transporta suficiente información para tomar un punto en la página, medido en unidades de página ordinarias, y reportar la latitud y longitud sobre la que se sitúa en el mundo real. Ese único hecho es lo que convierte a un PDF en un soporte útil para un mapa topográfico, un plano de catastro, una exhibición de zonas de inundación o cualquier exportación de SIG (GIS) que deba imprimirse y aún conservar su significado. La geometría está en el archivo; la única pregunta es si su cargador la lee.

La razón por la que esto se pasa por alto es que un GeoPDF se abre e imprime exactamente como cualquier otro PDF. Nada en la página renderizada anuncia que el mapa esté registrado en un sistema de coordenadas. El registro reside en diccionarios asociados al objeto de página, nunca dibujados, y un visor que los ignora le mostrará el mapa de todos modos. Para hacer cualquier tarea espacial con el archivo (lecturas de coordenadas de agrimensura, reproyección o superposición con otras capas), debe recorrer esos diccionarios usted mismo.

Dos estándares coexisten en la práctica

Un lector que desee manejar archivos del mundo real tiene que lidiar con dos esquemas de georregistro, ya que ambos están en circulación y un archivo determinado puede utilizar cualquiera de ellos. El más antiguo es la codificación OGC descrita en OGC 08-139r2, que asocia un LGIDict (un diccionario de registro geoespacial) a la página. Es anterior a cualquier aprobación de ISO y fue el formato de facto para las primeras salidas de GeoPDF, por lo que un gran volumen de mapas heredados lo incluye y nada más.

El esquema moderno es el que la ISO estandarizó en la norma ISO 32000-1 §8.8.2. En lugar de un único diccionario a nivel de página, modela los datos geoespaciales como un Viewport (ventana gráfica) de página con un diccionario Measure (medida) asociado, y el diccionario de medida designa un sistema de coordenadas geográficas. Esta es la codificación que escriben Acrobat y los exportadores de SIG actuales. Un importador sólido comprueba ambos casos: lee los viewports para el modelo ISO y recurre a (o inspecciona adicionalmente) el LGIDict para archivos que solo contienen el registro heredado.

Viewports y sus límites

En el modelo ISO, la unidad de georregistro es el viewport, y una página puede tener varios. Una hoja grande puede colocar un mapa principal en un rectángulo, un mapa insertado a una escala diferente en otro y un panel de leyenda que no esté georreferenciado en absoluto. Cada viewport contiene un BBox, el rectángulo en la página que el viewport rige, para que el lector sepa a qué parte de la hoja se aplica un sistema de coordenadas determinado. Realizar pruebas de colisión (hit-testing) de un punto en el que se hizo clic contra esos cuadros es la forma en que un visor decide qué diccionario de medida utilizar.

PDFlibPas expone directamente los viewports de la página seleccionada. GetPageViewPortCount devuelve cuántos hay, GetPageViewPortID convierte un índice con base uno en un identificador ViewPortID y GetViewPortBBox lee el rectángulo de límites una dimensión a la vez. El argumento Dimension selecciona cuál borde o extensión desea: 0 es Left, 1 es Top, 2 es Width, 3 es Height, 4 es Right y 5 es 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 de cero proveniente de GetPageViewPortID significa que no se pudo encontrar el viewport en ese índice, por lo que debe verificarlo antes de transferir el identificador.

Dentro del diccionario de medida

La geometría que registra la página con el mundo reside en el diccionario de medida asociado a un viewport. GetViewPortMeasureDict devuelve un MeasureDictID para un ViewPortID dado, o cero cuando el viewport no tiene diccionario de medida, lo cual es el caso normal para una leyenda o un panel de título. El diccionario de medida contiene tres elementos que vale la pena leer: los sistemas de coordenadas que referencia, los arreglos que vinculan puntos de página con puntos geográficos y la unidad en la que se expresan los datos del punto.

El registro en sí consiste en dos arreglos paralelos. GPTS es el arreglo de puntos geográficos, pares de latitud y longitud indicados en el sistema de coordenadas geográficas. LPTS es el arreglo de puntos del espacio de página, expresados como fracciones del BBox del viewport para que sobrevivan a los cambios de escala. El elemento n de LPTS y el elemento n de GPTS designan la misma ubicación física, una vez en coordenadas de página y otra en el globo. Tres o más de estos pares determinan la transformación afín, o proyectiva en el caso general, que mapea cualquier coordenada de página dentro del viewport a una coordenada mundial. Leerlos es cuestión de recorrer ambos arreglos al mismo paso.

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;

El diccionario de medida también reporta sus unidades de visualización a través de GetMeasureDictPDU, que toma un UnitIndex de 1 para unidades lineales, 2 para unidades de área o 3 para unidades angulares y devuelve un código que identifica la unidad específica, por ejemplo, un metro o un pie internacional para la categoría lineal. El arreglo Bounds, leído con GetMeasureDictBoundsItem, describe el cuadrilátero dentro del viewport que la medición realmente cubre, que no siempre es el rectángulo completo.

WKT frente a EPSG

La latitud y la longitud en GPTS no tienen sentido sin saber a qué sistema de coordenadas geográficas pertenecen, ya que una coordenada de 51.5, -0.1 lands en un punto físico diferente bajo WGS 84 que bajo un datum nacional más antiguo. El diccionario de medida responde a esto a través de un diccionario de sistema de coordenadas, al que se accede con GetMeasureDictGCSDict para el sistema geográfico. PDF describe ese sistema en una de dos formas intercambiables, y un lector debe aceptar cualquiera de ellas.

La primera es WKT (Well-Known Text), una cadena independiente que detalla el datum, el elipsoide, el meridiano de origen y las unidades en su totalidad. Es detallada pero inequívoca y no requiere una tabla de búsqueda externa. La segunda es un código EPSG, un único entero que indexa un sistema de coordenadas en el registro EPSG; 4326 es WGS 84, el marco que utiliza la mayoría de los datos de GPS de consumo. EPSG es compacto pero asume que el lector puede resolver el código contra una base de datos. Los archivos aparecen con uno, the other o ambos, razón por la cual la API expone los tres métodos GetCSDictType, GetCSDictEPSG y GetCSDictWKT. GetCSDictType reporta si el sistema es geográfico (un GEOGCS, valor de retorno 1) o proyectado (un PROJCS, valor de retorno 2), lo que le permite interpretar el resto correctamente antes de confiar en él.

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;

Lectura del LGIDict heredado

Los archivos anteriores al modelo de viewport, o aquellos producidos por herramientas que aún emiten la codificación antigua, transportan su registro en un LGIDict en la página en lugar de en un diccionario de medida. PDFlibPas reporta cuántos de estos diccionarios tiene una página a través de GetPageLGIDictCount y devuelve el contenido directo de cada uno con GetPageLGIDictContent, indexado desde uno. El texto devuelto es el diccionario tal como se escribió, que contiene los campos de registro de OGC 08-139r2, que luego su código analiza para recuperar el mismo tipo de mapeo de página a mundo que proporciona el diccionario de medida. En el lado de la escritura, AddLGIDictToPage asocia un LGIDict a la página actual, de modo que un convertidor puede procesar de ida y vuelta la forma heredada cuando un consumidor antiguo todavía la espera.

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;

Estructuración completa de la lectura

Un importador completo trata los dos esquemas como un par de pasadas sobre cada página. Seleccione la página, solicite los viewports ISO a GetPageViewPortCount y, para cada viewport que posea un diccionario de medida, extraiga su BBox, sus arreglos GPTS y LPTS, su unidad de datos de punto y la descripción GCS a través del diccionario del sistema de coordenadas. Luego, compruebe GetPageLGIDictCount para cualquier registro heredado que la pasada de viewports no haya cubierto. A mapa que contenga ambos debería mostrar coherencia entre ellos; un mapa que solo contenga uno seguirá resolviéndose, porque usted buscó en ambos lugares. Los identificadores devueltos en el proceso (ViewPortID, MeasureDictID, CSDictID) son enteros simples que siguen siendo válidos mientras el documento esté cargado, por lo que todo el recorrido se reduce a unos pocos bucles anidados sobre la lista de páginas sin asignaciones de memoria que gestionar.

Una vez que puede recuperar el registro, la página se convierte en una fuente de datos en lugar de una imagen. Las técnicas complementarias para leer el resto de una página se cubren en el artículo sobre extracción de texto, imágenes y fuentes, y la renderización de una hoja georreferenciada en un dispositivo para mediciones en pantalla se describe en la guía del contexto de dispositivo para impresión y vista previa. El lector geoespacial descrito aquí se distribuye como parte de la losLab PDF Library para Delphi y C++Builder, junto con las API de carga, extracción y renderizado cubiertas en otras partes de este blog.