大多数开发人员将 PDF 页面视为一张带有文本和图片的纸张。地理参考 PDF 不仅仅如此。它携带了足够的信息,可以通过获取页面上的某个点(以普通页面单位测量),并报告其在现实世界中所处的纬度和经度。正是这一事实将 PDF 转化为地形图、地籍调查图、洪水区展示或任何必须打印且仍有意义的 GIS 导出的可用载体。几何形状就在文件中;唯一的问题是你的加载器是否能读取它。
这之所以被忽略,是因为 GeoPDF 的打开和打印方式与其他任何 PDF 完全相同。渲染的页面中没有任何内容表明地图已注册到坐标系中。注册关系存在于挂在页面对象下的字典中,它们从不被绘制,而忽略它们的查看器仍会向你显示地图。要对该文件进行任何空间处理(测量坐标读数、重投影、与其他图层叠加),你必须自己遍历这些字典。
野外存在两种标准
想要处理现实世界文件的读取器必须应对两种地理注册方案,因为这两种方案都在流通,并且给定的文件可能使用其中任何一种。较早的一种是 OGC 08-139r2 中描述 of OGC 编码,它将 LGIDict(地理空间注册字典)附加到页面上。它早于任何 ISO 的认可,并且是早期 GeoPDF 输出的行业标准格式,因此大量遗留地图仅携带它。
视口及其边界
在 ISO 模型中,地理注册的单位是视口,一个页面可能包含几个视口。一张大纸可以将主地图放在一个矩形中,将不同比例的插图放在另一个矩形中,以及一个完全没有地理参考的图例面板。每个视口携带一个 BBox(视口管辖的页面矩形),以便读取器知道给定坐标系适用于图纸的哪个部分。将点击的点与这些方框进行碰撞测试,就是查看器决定使用哪个测量字典的方式。
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;
如果从 GetPageViewPortID 返回的 ViewPortID 为零,代表无法找到该索引处的视口,因此在传递句柄之前应先对其进行检查。
深入了解 measure 字典
将页面注册到世界的几何形状存在于附加到视口的测量(measure)字典中。GetViewPortMeasureDict 为给定的 ViewPortID 返回一个 MeasureDictID,如果视口没有测量字典,则返回零(这对于图例或标题面板来说是正常情况)。测量字典包含三个值得读取的内容:它引用的坐标系、将页面上的点与地理点联系起来的数组,以及表达点数据所用的单位。
注册本身是两个平行的数组。GPTS 是地理点数组,即地理坐标系中给出的经纬度对。LPTS 是页面空间点数组,以视口 BBox 的分数形式表示,以便在缩放时幸存。LPTS 的第 n 项和 GPTS 的第 n 项命名了相同的物理位置(一次在页面坐标中,一次在地球上)。三个或更多这样的对可以确定仿射(或者在一般情况下的投影)变换,该变换将视口内的任何页面坐标映射到世界坐标。读取它们就是同步遍历这两个数组的问题。
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;
测量字典还通过 GetMeasureDictPDU 报告其显示单位,该函数接受线性单位的 UnitIndex 值为 1,面积单位为 2,或角度单位为 3,并返回标识特定单位的代码,例如线性类别的米或国际英尺。通过 GetMeasureDictBoundsItem 读取的 Bounds 数组描述了视口内测量实际覆盖的四边形,这并不总是完整的矩形。
WKT 与 EPSG 的对比
GPTS 中的纬度和经度如果在不知道它们属于哪个地理坐标系的情况下是没有意义的,因为在 WGS 84 下,51.5, -0.1 的坐标与在较旧的国家基准面下落入不同的物理位置。测量字典通过坐标系字典回答了这个问题,对于地理系统,可以通过 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); // 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
早于视口模型的文件,或者是由仍然输出较旧编码的工具生成的文件,其注册信息携带在页面上的 LGIDict 中,而不是在测量字典中。PDFlibPas 通过 GetPageLGIDictCount 报告一个页面有多少个此类字典,并通过 GetPageLGIDictContent(从 1 开始索引)交回每个字典的原始内容。返回的文本是原样写入的字典,包含 OGC 08-139r2 注册字段,然后你的代码解析这些字段以恢复测量字典提供的同种页面到世界的映射。在写入端,AddLGIDictToPage 将 LGIDict 附加到当前页面,以便在旧客户端仍然预期它时,转换器可以往返处理遗留形式。
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 请求 ISO 视口,对于拥有测量字典的每个视口,拉取其 BBox、GPTS 和 LPTS 数组、点数据单位,以及通过坐标系字典获取的 GCS 描述。然后检查 GetPageLGIDictCount 以获取视口遍历未涵盖的任何遗留注册。携带两者的地图应当在这两者之间保持一致;仅携带其中一种的地图仍能解析,因为你在两个地方都进行了查找。在此过程中返回的句柄(ViewPortID、MeasureDictID、CSDictID)是普通整数,在文档加载期间保持有效,因此整个遍历是页面列表上的几个嵌套循环,没有要管理的内存分配。
一旦你能够恢复注册,页面就变成了一个数据源,而力求精确的图像渲染已成为一种标准。阅读页面其余部分的配套技术已在关于文本、图像和字体提取的文章中介绍,而将带有地理参考的图纸渲染到设备以进行屏幕测量则已在打印和预览设备上下文指南中描述。此处介绍的地理空间读取器作为 Delphi 和 C++Builder 的 losLab PDF Library 的一部分提供,同时还包括本博客其他地方介绍的加载、提取和渲染 API。