Техническая статья

GeoPDF в Delphi: географические координаты с помощью PDF Library for Delphi

Большинство разработчиков воспринимают страницу PDF как обычный лист бумаги с текстом и рисунками. Геопривязанный PDF представляет собой гораздо большее. Он содержит достаточно информации, чтобы по точке на странице, измеренной в обычных единицах разметки, определить широту и долготу этого места в реальном мире. Этот факт превращает PDF в удобный контейнер для топографических карт, кадастровых планов земельных участков, планов зон затопления или любых других данных ГИС, которые должны нести практический смысл после печати. Вся геометрия уже присутствует в файле, вопрос лишь в том, умеет ли ваш загрузчик считывать ее

Эту деталь часто упускают из виду, так как GeoPDF открывается и печатается точно так же, как любой обычный PDF. На самой странице ничто не указывает на то, что карта привязана к системе координат. Данные привязки хранятся в невидимых словарях метаданных объекта страницы, и простая программа просмотра, игнорирующая их, отобразит карту без изменений. Чтобы работать с пространственными данными (считывание геодезических координат, перепроецирование, наложение слоев), вам придется обойти эти словари вручную

Два стандарта, используемые на практике

Средство чтения, предназначенное для работы с реальными файлами, должно поддерживать две схемы георегистрации, так как обе они распространены. Более старой является кодировка OGC, описанная в OGC 08-139r2, при которой к странице прикрепляется словарь LGIDict (словарь геопространственной регистрации). Эта спецификация появилась до официальной стандартизации ISO и служила форматом по умолчанию для ранних файлов GeoPDF, поэтому множество старых карт содержат только ее

Современная схема стандартизована в ISO 32000-1 §8.8.2. Вместо одного словаря на уровне страницы она описывает пространственные данные как область просмотра страницы (Viewport) с прикрепленным словарем измерений (Measure), указывающим на географическую координатную систему. Именно это кодирование используют Acrobat и современные экспортеры ГИС. Надежный импортер должен проверять оба варианта: считывать области просмотра для модели ISO и обращаться к LGIDict для файлов, содержащих только устаревшую разметку

Диаграмма PDF Library for Delphi: модель георегистрации ISO через словари viewport и measure в противовес прежней схеме LGIDict, которую несут старые файлы GeoPDF
В реальных файлах GeoPDF встречаются две схемы геопривязки, и страница может нести любую из них или обе. Каждый путь преобразует координаты страницы в широту и долготу

Области просмотра и их границы

В модели ISO базовой единицей георегистрации является область просмотра, и на странице их может быть несколько. На одном большом листе можно расположить основную карту в одной рамке, врезку с другим масштабом во второй, а легенду без географической привязки разместить в третьей. Каждая область просмотра имеет параметр BBox (прямоугольник на странице, которым управляет эта область), благодаря чему средство чтения определяет, к какой части листа относится данная система координат. Сопоставление точки нажатия с этими рамками позволяет программе просмотра выбрать правильный словарь измерений

Библиотека PDF Library for Delphi предоставляет прямой доступ к областям просмотра выбранной страницы. Функция GetPageViewPortCount возвращает их количество, GetPageViewPortID преобразует индекс с отсчетом от единицы в дескриптор ViewPortID, а метод GetViewPortBBox считывает границы прямоугольника по очереди для каждого измерения. Аргумент Dimension задает конкретную границу: 0 для Left, 1 для Top, 2 для Width, 3 для Height, 4 для Right и 5 для Bottom

PDF Library for Delphi: макет страницы PDF с основным окном карты, врезкой и легендой без привязки, с проверкой попаданий кликов по BBox каждого окна
Каждый viewport управляет только областью внутри своего BBox, поэтому hit-testing подсказывает читателю, какой словарь измерений применим. Нулевой ViewPortID означает, что запрошенный viewport не найден
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 описывают область карты для этой области просмотра
    end;
  finally
    Pdf.Free;
  end;
end;

Возврат значения ViewPortID, равного нулю, из функции GetPageViewPortID указывает на то, что область просмотра с данным индексом не найдена, поэтому проверяйте это значение перед использованием дескриптора

Внутри словаря измерений

Геометрия, связывающая страницу с реальным миром, содержится в словаре измерений, прикрепленном к области просмотра. Функция GetViewPortMeasureDict возвращает MeasureDictID для указанного ViewPortID или ноль при отсутствии словаря измерений (что обычно наблюдается у легенды или панели заголовка). Словарь измерений хранит три группы параметров: целевые координатные системы, массивы связей точек страницы с географическими координатами, а также единицы измерения пространственных данных

Фактически привязка представлена двумя параллельными массивами. GPTS является массивом географических точек (пары широты и долготы в заданной системе координат). LPTS хранит координаты точек на странице, выраженные в виде долей от BBox области просмотра, что позволяет сохранять точность при масштабировании. Элемент n из LPTS и элемент n из GPTS описывают одну и ту же физическую точку (первый на странице, а второй на земном шаре). Три или более подобные пары определяют аффинное (или в общем случае проективное) преобразование, которое сопоставляет любую координату страницы внутри области просмотра с мировыми координатами. Чтение этих данных сводится к последовательному обходу обоих массивов

PDF Library for Delphi: парные доли пространства страницы LPTS и массивы широты-долготы GPTS, соединяемые поэлементно для фиксации аффинного преобразования страницы в мировые координаты
Элемент n в LPTS и элемент n в GPTS называют одно и то же физическое место — один раз на странице, другой на глобусе. Три и более пары фиксируют аффинное или проективное преобразование
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 хранит пары широта/долгота; LPTS хранит соответствующие доли страницы.
    // Оба массива читаются с индексами элементов от единицы.
    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) на странице соответствует (lat, lon) на местности
      Inc(j, 2);
    end;
  end;
end;

Словарь измерений также сообщает единицы отображения через функцию GetMeasureDictPDU, которая принимает индекс UnitIndex (1 для линейных, 2 для квадратных и 3 для угловых единиц) и возвращает код единицы измерения, например метр или международный фут. Массив Bounds, считываемый с помощью GetMeasureDictBoundsItem, описывает четырехугольник внутри области просмотра, к которому действительно относится измерение, поскольку оно не всегда занимает весь прямоугольник

WKT против EPSG

Широта и долгота в массиве GPTS не имеют практического смысла без знания географической системы координат, так как точка 51.5, -0.1 будет соответствовать разным местам в системе WGS 84 и в старой государственной системе координат. Словарь измерений решает эту задачу с помощью словаря координатных систем, возвращаемого функцией GetMeasureDictGCSDict. Стандарт PDF описывает эту систему одним из двух взаимозаменяемых способов, и средство чтения должно поддерживать оба

Первый способ представляет собой WKT (Well-Known Text), то есть текстовую строку, содержащую описание датума, эллипсоида, нулевого меридиана и единиц измерения. Этот формат избыточен, но точен и не требует внешних справочных таблиц. Второй способ выражается в коде 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);   // например, 4326 для WGS 84, 0 при отсутствии
    wkt    := Pdf.GetCSDictWKT(gcsID);    // полное текстовое описание, '' при отсутствии
    // Предпочитать EPSG, если он присутствует; иначе разбирать WKT.
  end;
end;

Чтение устаревшего словаря LGIDict

Файлы, созданные до внедрения модели областей просмотра или сохраненные старыми инструментами, содержат геопривязку в словаре LGIDict страницы, а не в словаре измерений. Функция GetPageLGIDictCount возвращает количество таких словарей на странице, а GetPageLGIDictContent возвращает их исходное содержимое по индексу с отсчетом от единицы. Возвращаемый текст содержит поля разметки спецификации OGC 08-139r2, которые ваш код должен разобрать для получения координатной сетки. Для записи метод AddLGIDictToPage прикрепляет словарь к текущей странице, позволяя сохранять обратную совместимость с устаревшими системами

var
  lgiCount, k: Integer;
  dictText: WideString;
begin
  lgiCount := Pdf.GetPageLGIDictCount;
  for k := 1 to lgiCount do
  begin
    dictText := Pdf.GetPageLGIDictContent(k);
    // dictText несёт регистрационные данные OGC 08-139r2 для разбора
  end;
end;

Реализация чтения в коде

Импортер обрабатывает эти две схемы как пару проходов по каждой странице. Выберите страницу, запросите области просмотра с помощью GetPageViewPortCount, и для каждой области просмотра со словарем измерений извлеките параметры BBox, массивы GPTS и LPTS, единицы измерения и спецификацию системы координат. Затем выполните проверку GetPageLGIDictCount на предмет наличия устаревших параметров привязки, которые могли быть пропущены на первом шаге. Карта, содержащая обе схемы, должна иметь согласующиеся данные, но если присутствует только одна схема, данные все равно корректно считаются. Возвращаемые в процессе обхода дескрипторы ViewPortID, MeasureDictID и CSDictID представляют собой обычные целые числа, которые остаются валидными на протяжении всего времени загрузки документа, поэтому весь процесс обхода сводится к нескольким вложенным циклам без необходимости ручного управления памятью

После считывания привязки страница превращается в источник структурированных данных, а не просто в изображение. Вспомогательные методы чтения остального содержимого страницы описаны в статье об извлечении текста, изображений и шрифтов, а рендеринг страницы с геопривязкой на устройство для измерений на экране рассматривается в руководстве по печати и предварительному просмотру с контекстом устройства. Модуль работы с геопространственными данными поставляется в составе библиотеки losLab PDF Library для Delphi и C++Builder вместе с API для загрузки, извлечения данных и рендеринга