简介:地图导出(图片/PDF)工具面向GIS从业者、制图人员及需要向客户展示地图效果的项目成员,以QGIS工程为基础,省去学习专业地图打印工具的步骤,即可将配置好的地图快捷输出为图片或PDF,适用于打印出图、方案样例与效果展示等场景。资源包内共321个文件,核心包括dll运行库与gfs样式文件,同时配套svg、png等图形素材及csv坐标数据,整体约69.79MB,结构完整可直接使用。目前已有365人学习浏览,工具上手门槛低,适合QGIS初学者快速掌握出图流程,也可作为日常制图工作的辅助插件。解压后通过批处理方式运行,用户无需修改复杂配置,只需基于已有QGIS工程即可得到清晰、规范的图片或PDF成果,提升交付效率。
1. 地图导出不是截图:从地图窗口到可发布 PDF 的完整链路
某开发者第一次接到地图导出任务时,以为就是在 GIS 里放大地图、按一下“屏幕截图”。结果评审会上 PDF 一放大全是马赛克,比例尺和图例对不上,打印出来还偏了一块。这个标题要解决的,就是把指定地理范围、指定比例尺、带图例与注记的地图内容,完整输出成图片或 PDF 文件的整套流程。它涉及投影、DPI、瓦片拼接、字体嵌入这些看似琐碎但决定成败的细节。适合做规划评审材料、论文插图、项目汇报图,以及需要批量出图给下游系统使用的人。读完你至少能判断:自己该走桌面工具流程还是命令行流程,哪里容易翻车,以及怎么验收一张导出图“能用”。
2. 导出前的三道选择题:投影、DPI 与栅格 / 矢量 PDF 怎么选
没想清楚这三件事就动手导出,后面大概率要返工。地图数据本身带的是经纬度坐标,而图片和 PDF 是平面介质,从球面到平面的映射方式、输出分辨率、内容组织方式,决定了导出结果的可用性。这一章先把三笔“欠账”还清,下一章的操作才有意义。
2.1 投影选不对,导出图就会偏:Web 墨卡托与打印坐标系的取舍
地图导出最常见的错误,是把在线底图的 Web 墨卡托坐标系直接当成打印坐标系。Web 墨卡托(EPSG:3857)适合屏幕浏览,因为经纬线是直线、方向不变;但它对面积和距离的变形非常严重,纬度越高,地被拉得越大。如果这张 PDF 将来要被拿去做量算、圈面积或者叠加规划红线,用 3857 出图就是给自己挖坑。
我一般会根据用途先写死一个结论:
| 用途 | 推荐投影 | 理由 |
|---|---|---|
| 屏幕查看、报告插图、文档嵌入 | 随底图(常见为 EPSG:3857) | 视觉一致,无需额外重投影,出图快 |
| 打印纸质图、规划评审、量算 | 本地投影或 UTM / 高斯投影 | 距离面积变形小,图面与坐标标注匹配 |
| 跨带大范围总图 | 按中央经线选投影或等距圆锥投影 | 避免单个投影带边缘变形过大 |
操作上就是在工程或项目属性里设置目标 CRS,让 GIS 在导出时把所有图层实时重投影到该坐标系。这里有个容易被忽略的细节:重投影发生在渲染阶段,如果数据量很大,导出时间会明显变长,图面边缘偶尔出现细缝。所以对纯视觉用途,我不强求转投影;对量算用途,必须在导出前把矢量数据先另存为目标投影,而不是靠导出时的实时渲染去兜底。
2.2 DPI 与像素尺寸:算出“不糊”的最低分辨率
DPI(Dots Per Inch)决定打印时每英寸铺多少个像素点。导出图片或 PDF 中的位图内容时,像素总量 = 物理尺寸 × DPI。很多人直接截屏导出,屏幕截图通常只有 96 DPI 左右,放到 A4 纸上一放大全是锯齿。先用目标纸张倒推像素需求,是最值得养成的习惯。
A4 横向宽度约 297mm,折合 11.69 英寸。300 DPI 下需要 3508 像素宽;A3 横向要 4961 像素宽。如果目标只是屏幕阅读,150 DPI 够用;要打印就得 300 DPI 起步。简单来说:
- 输出图片:导出对话框里的“分辨率”直接填 300(单位是 DPI)
- 输出 PDF:如果是栅格内容,按同样的像素需求去算瓦片数量或比例尺
需要多少张瓦片,也可以此时粗算:像素需求 ÷ 单张瓦片边长。比如 3508 像素用 256 像素瓦片,单方向约 14 张,乘以下载范围的行列数,就能估算总量和耗时。这个计算放到了第 4 章,但决策时机是在导出前。
2.3 栅格 PDF 还是矢量 PDF:按用途二选一
PDF 导出时,地图内容有两种组织方式。栅格 PDF 本质上把整张图渲染成一张大图嵌进去,适合卫星影像、地形晕渲、带有复杂纹理和透明效果的样式;缺点是放大到 400% 时文字和线条会发虚,文件体积也大。矢量 PDF 里,线、面、文字都是矢量对象,放大不糊,文字还能被搜索和复制,适合线划图、路网图这类以线条和标注为主的内容。
选型标准我一般是这么定的:
| 判断问题 | 选栅格 PDF | 选矢量 PDF |
|---|---|---|
| 底图是影像 / 航片? | 是 | 否 |
| 样式里有大量渐变、纹理、复杂符号? | 是 | 否 |
| 导出后还要在 AI / CAD 里继续编辑? | 否 | 是 |
| 文件大小有严格限制? | 否 | 是 |
还有一个折中:很多桌面 GIS 支持“混合模式”,地图区按矢量导出,影像图层单独光栅化。这个功能适合一张图里既有线划又有影像的情况,能兼顾清晰度和体积。下一章的导出参数设置里会专门讲到这个开关。
3. 用打印布局把地图导出成图片和 PDF:桌面 GIS 的实操步骤
明确了投影、DPI 和 PDF 类型,就可以动手了。以常见的开源桌面 GIS 工具为例,完成一次带图例、比例尺、指北针和标题的完整导出,整个过程都在打印布局里完成。这一章讲的是单张出图的完整路径,顺带给出批量脚本化的方法。
3.1 打印布局与地图项:为什么不能直接对屏幕截图
打印布局是一张虚拟的“纸张”,可以在上面摆放地图内容、标题、图例、比例尺条、指北针、表格等元素。它的核心价值在于联动:布局里的地图项指向某个图层集合,地图缩放、图层可见性调整后,布局内容会自动刷新,不需要重做。这和截图有本质区别。
操作步骤按顺序走一遍:
- 在工程属性里把 CRS 设置为目标投影(按 2.1 的结论选)
- 打开“新建打印布局”,输入布局名称
- 在布局面板中设置纸张尺寸和方向,比如 A4 横向
- 从布局工具栏添加“地图”项,拖动框选出地图区
- 在地图项属性中勾选“锁定比例尺”,并输入比例尺分母,比如 10000 表示 1:10000
- 添加图例项,在属性中勾选“仅显示图层中可见的项目”,点击“更新”同步当前地图内容
- 添加比例尺条、指北针和标题框,调整位置
锁定比例尺这一步是最容易省掉但最不该省的。如果不锁定,地图项会随着画布拖动自动缩放,导出的图看似正常,图例里的比例尺分母和实际图面比例尺对不上。锁定后,图面内容固定,图例和比例尺条的数值才可信。
3.2 导出参数:分辨率、压缩与嵌入字体的设置
布局排好后,分别处理图片和 PDF 的导出参数。导出图片时,对话框里有一个“分辨率”输入框,这里填的是最终图片的 DPI。A4 横向 300 DPI 会得到约 3508×2480 像素的图,JPEG 质量建议 85~90,太高文件臃肿,太低文字边缘出毛刺。
导出 PDF 时的关键选项比较多,逐个说明:
| 选项 | 建议值 | 说明 |
|---|---|---|
| 分辨率 | 300 | 影响栅格图层的清晰度 |
| 尽可能合并矢量渲染 | 勾选 | 线条按矢量保留,文字可选中 |
| 光栅化所有内容 | 不勾选 | 勾选后整页变成位图,失去矢量优势 |
| 嵌入字体 | 勾选 | 防止换电脑后中文注记变成方块 |
| 简化几何图形 | 按需 | 可减小文件体积,但可能丢细节 |
一个常见误区是只调导出分辨率,忽略地图项本身的“渲染分辨率”。在地图项属性里,渲染分辨率控制着地图区域的栅格化精度,如果这里保持默认值,即使导出对话框填了 300,地图区仍然可能以较低精度输出。稳妥做法是:图片导出和 PDF 导出都把分辨率设为 300,同时把地图项属性里的分辨率也提到 300。
3.3 PyQGIS 批量出图:把一套布局脚本化
单张图配置好之后,如果还有十个村、二十个地块要出图,手工改范围再导出会把人逼疯。这时可以用脚本批量替换地图项范围,复用同一个布局模板。下面的脚本在桌面 GIS 的 Python 控制台里运行:
project = QgsProject.instance() layout = project.layoutManager().layoutByName("A4_标准") # 替换成你的布局名 map_item = layout.itemById("地图1") # 替换成你的地图项 ID extent = QgsRectangle(120.0, 30.0, 120.5, 30.5) # 左、下、右、上 map_item.setExtent(extent) map_item.setScale(10000) # 锁定 1:10000 exporter = QgsLayoutExporter(layout) settings = QgsLayoutExporter.PdfExportSettings() settings.dpi = 300 exporter.exportToPdf("/tmp/地块A.pdf", settings)脚本的逻辑很直接:先定位到布局对象和地图项,修改范围或比例尺,然后用给定的导出设置输出 PDF。setExtent传入的是地理范围(与工程 CRS 一致),setScale的 10000 指比例尺分母,两者二选一即可,同时设置时以 scale 生效。settings.dpi对应第 2 章说的 300 DPI。如果批量循环多个范围,注意每次修改后布局要调用layout.refresh()强制刷新,否则导出的还是上一次的缓存内容。
4. 命令行高分辨率导出:Python 瓦片下载、拼接与合成 PDF
桌面工具适合交互式操作,但遇到几百平方公里的大范围、要求指定 zoom 级别、或者需要把导出流程嵌入自动化管道时,命令行脚本是更可靠的路径。核心思路是:从瓦片服务下载网格状的小图,再按经纬度位置拼成一张大图,最后补上投影信息并合成 PDF。
4.1 范围转瓦片行列号与并发下载
Web 墨卡托瓦片把全球地图切成 2 的 zoom 次幂个网格,编号从左上角 (0,0) 开始。给定经纬度,换算成指定 zoom 下的行列号是第一步。下面是标准换算函数:
import math def deg_to_tile(lat, lon, z): n = 2 ** z x = (lon + 180.0) / 360.0 * n lat_rad = math.radians(lat) y = (1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n return int(math.floor(x)), int(math.floor(y))deg_to_tile返回的是瓦片坐标,z是缩放级别,级别越高瓦片数量按四次方增长。注意:这里必须用math.floor而不是int()强转,否则负纬度或负经度时取整方向会错,导致整张图偏移一个瓦片。这个坑在 5.2 节还会再提到。
有了换算函数,下载整个范围就变成两层循环。下面这段是可运行的下载骨架:
import urllib.request from concurrent.futures import ThreadPoolExecutor HEADERS = {"User-Agent": "my-map-export-script/1.0"} TILE_URL = "https://example-tile-server/{z}/{x}/{y}.png" # 替换为实际瓦片地址 def fetch_one(z, x, y): url = TILE_URL.format(z=z, x=x, y=y) req = urllib.request.Request(url, headers=HEADERS) for attempt in range(3): # 失败重试 3 次 try: with urllib.request.urlopen(req, timeout=15) as resp: return x, y, resp.read() except Exception: if attempt == 2: raise return x, y, None # 计算范围:西北角 tile_min,东南角 tile_max tile_min = deg_to_tile(31.0, 121.0, z=14) tile_max = deg_to_tile(30.0, 121.5, z=14) with ThreadPoolExecutor(max_workers=4) as pool: futures = [] for x in range(tile_min[0], tile_max[0] + 1): for y in range(tile_min[1], tile_max[1] + 1): futures.append(pool.submit(fetch_one, z, x, y)) tiles = [f.result() for f in futures]两个参数值得细说:max_workers=4是保守值,很多瓦片服务对单 IP 并发敏感,超过 8 很容易触发限流返回 403;timeout=15防止某个瓦片卡死拖垮整个任务。重试逻辑放在for attempt in range(3)里,失败后延迟递增会更礼貌。最终tiles列表里存的是 (x, y, 图片二进制) 三元组。
4.2 Pillow 拼接:按网格顺序合成一张大位图
下载完成后,拼图的关键是搞清瓦片坐标与像素坐标的对应关系。瓦片编号从左上角开始,所以第 x 列、第 y 行的瓦片,它的左上角像素坐标就是 (x * 256, y * 256)。用 Pillow 按这个规则把每张瓦片贴到画布上:
from PIL import Image TILE_SIZE = 256 min_x, min_y = tile_min max_x, max_y = tile_max width = (max_x - min_x + 1) * TILE_SIZE height = (max_y - min_y + 1) * TILE_SIZE canvas = Image.new("RGB", (width, height), (255, 255, 255)) for x, y, data in tiles: if data is None: continue img = Image.open(io.BytesIO(data)) px = (x - min_x) * TILE_SIZE py = (y - min_y) * TILE_SIZE canvas.paste(img, (px, py)) canvas.save("/tmp/map_raw.png")这里有个容易忽略的点:tile_min和tile_max是从经纬度换算来的,但范围边界处往往只压到了目标区域的一部分,所以循环要取到max_x + 1、max_y + 1,否则最右一列和最下一行会缺。如果追求严格对齐,拼完后用canvas.crop按目标经纬度对应的像素范围裁掉多余部分。canvas.paste的性能对几千张瓦片没问题,但到几万张时建议按行分批保存,原因见第 5 章。
4.3 重投影与 PDF 合成:给拼好的图补上地理坐标
拼出来的大图只是一张普通的 PNG,没有坐标信息。如果它最终要作为 PDF 嵌入报告,可以直接合成;如果要交给 GIS 或 CAD 继续处理,必须先写成带坐标的 GeoTIFF。用 GDAL 命令补坐标是最省事的方式:
gdal_translate -a_srs EPSG:3857 \ -a_ullr 121.0 31.5 121.5 30.5 \ /tmp/map_raw.png /tmp/map_geo.tif-a_srs声明坐标系,这里假设瓦片是 Web 墨卡托;-a_ullr依次是左上角经度、纬度、右下角经度、纬度。注意瓦片服务的经纬度边界对应 EPSG:3857 的投影坐标,但-a_ullr直接填经纬度在很多场景能自动换算,严谨做法是先转成 3857 的米制坐标再填。如果目标投影不是 3857,用gdalwarp做一次重投影:
gdalwarp -t_srs EPSG:32650 /tmp/map_geo.tif /tmp/map_final.tif再把 GeoTIFF 转成 PDF,常见做法是用 GDAL 自带的 PDF 驱动,或者用 Pillow 把 PNG 直接存成 PDF:
from PIL import Image img = Image.open("/tmp/map_raw.png") img.save("/tmp/map_result.pdf", "PDF", resolution=300)resolution=300是 Pillow 写入 PDF 时声明的 DPI,决定打印物理尺寸,和图片本身的像素量无关。如果上一步已经做了gdalwarp,这里建议直接用转换后的 GeoTIFF 转 PDF,保证 PDF 里的坐标和出图一致。另外提醒一句:瓦片服务通常有使用条款,批量下载大范围前先确认授权范围,内部项目用也要控制频率,别跑满带宽。
5. 地图导出避坑:文字变方块、偏半个瓦片与大图内存爆掉
这一章整理的是实际操作中踩过、也帮别人排查过的几个高频问题。每条按“现象 → 原因 → 解决”写,排序基本按出现频率,前两条几乎每个项目都会碰到。
5.1 打印字糊、图例对不上的两个纸面坑
现象一:PDF 在电脑上看一切正常,打印出来中文注记全变成方块或乱码。
原因是字体嵌入选项没开。桌面 GIS 导出 PDF 时默认会嵌入字体,但如果系统里缺少中文字体,或者项目样式里引用的字体名与实际安装名不一致,导出的 PDF 只记录字体名不嵌入字体文件,打印设备用自己的字体渲染时就会替换失败。解决方法是三步:先确认操作系统里安装了中文字体;在导出 PDF 选项里勾选“嵌入字体”;导出后用 PDF 阅读器打开,在“字体”属性里检查是否显示“已嵌入”。有的版本还需要额外勾选“始终使用此字体”,可以视具体情况调整。
现象二:图例里标注的“1:10000”和图面上实际比例尺对不上,用尺子一量差不少。
原因是地图项没有被锁定比例尺。布局中添加地图项后,如果拖动边框调整位置,地图内容会随之缩放,而图例中的比例尺文本是静态的,不会跟着变。解决方法是回到地图项属性,勾选“锁定比例尺”并写入目标分母,然后重新更新图例。别只顾着调图例,核心是锁地图。
5.2 瓦片 403、出图偏移半张瓦片的两个网络坑
现象一:批量下载跑到三分之一,请求开始返回 403,之后越来越多,最终整个任务中断。
原因通常是并发太高触发服务端限流,或者请求头里没有 User-Agent 被反爬识别。很多瓦片服务默认拒绝无 UA 或 UA 是库默认值的请求。解决方法是把第 4 章的HEADERS加上真实浏览器 UA,max_workers降到 4,并在重试循环里加入递增延时,比如第 n 次重试前等待2^n秒。如果还不行,检查是否有 Referer 校验,需要在请求头里补上对应来源页。
现象二:拼接出来的大图整体往左上偏移,边界正好差一个瓦片大小。
原因是瓦片坐标换算时用了int()而不是math.floor()。当经纬度落在负数区域,或者恰好压线时,int()会向零取整,导致 y 坐标偏大,整张图被顶上去一行。解决方法是换用math.floor,并验证边界瓦片的实际范围。还有一个连带问题:循环边界少算了+1,导致最右列和最下行缺瓦片,拼出来图不完整。这两个问题常同时出现,排查时先打印tile_min和tile_max,用手工验证几个角点的瓦片编号再继续。
5.3 上万张瓦片拼接内存不足:分块落盘的思路
现象:大范围高 zoom 导出,比如 2 万张 256 瓦片,拼接时程序直接内存溢出,或者系统卡死。
原因是 Pillow 全尺寸画布按像素数占内存,RGB 三通道每像素 3 字节。2 万张 256 瓦片拼成的大图约 1.3GB 像素数据,再加上解码缓存和粘贴时的临时拷贝,8GB 内存机器很容易被吃满。解决思路是避免一次性生成整张画布,改成按行条带拼接,逐行写入磁盘。每拼完一行条带就释放内存,最终用 GDAL 把条带合并:
# 按行拼接的临时文件 gdal_translate -of GTiff row_0.tif row_1.tif ... merged.vrt gdal_translate merged.vrt final.tif更快的做法是直接用 gdalbuildvrt 把所有瓦片在文件层面组织成虚拟栅格,不实际加载像素:
gdalbuildvrt mosaic.vrt tiles/*.png gdal_translate -co COMPRESS=LZW -co BIGTIFF=YES mosaic.vrt final.tifBIGTIFF=YES是给超过 4GB 的成果用的,小图不用开。这个方案的额外好处是 avirtual 文件可以随时按范围裁剪或重投影,不用重新拼图。如果你的瓦片本来就是 GeoTIFF,这一步能省掉 Pillow,内存占用和耗时都能降到原来的零头。
6. 把导出固化成工作流:一个自检清单和一个验证命令
单张图能导出了,接着要做的是把“能做”变成“稳定做”。我会在项目里维护一个固定脚本,输入一个经纬度范围、zoom 级别和输出路径,依次完成:范围转瓦片坐标 → 并发下载 → 按行条带拼接 → 写 GeoTIFF → 重投影 → 合成 PDF。然后把下面的自检清单打成表格贴在项目文档开头:
| 检查项 | 验证方法 | 通过标准 |
|---|---|---|
| 比例尺正确 | 布局中锁定比例尺分母 | 图例数值与实际图面一致 |
| 角点坐标正确 | 在 GIS 中叠加导出文件 | 与输入范围偏差不超过一个像素 |
| 文字清晰且可选中 | PDF 放大 400% 并复制文字 | 无毛刺,文字可选 |
| 文件体积合理 | 查看导出文件大小 | 大图超过 4GB 时确认已开 BIGTIFF |
最后一步验收角点坐标,我习惯用一条 GDAL 命令直接读 GeoTIFF 的地图范围:
gdalinfo -stats final.tif | grep -E "Lower Left|Upper Right|Size is"这条命令输出文件尺寸和四个角坐标,拿它和输入范围比对,偏差超过一个像素就说明瓦片边界算错了。PDF 本身不好直接读坐标,所以我会始终保留一份 GeoTIFF 作为中间产物,PDF 只当分发格式,坐标验证永远对着 TIF 做。
说个真实教训:某次给专项图出 PDF,布局里图例、指北针、比例尺都排得很满,导出后在屏幕上放大看没有任何问题。交出去之后才被反馈说投影仪上文字发虚,一查是地图项渲染分辨率没跟着调,整张图的有效分辨率只有 96 DPI。从那以后,“导出前先查地图项分辨率”写进了自检清单,每次出图都先跑一遍 gdalinfo 再看文件。这个习惯救了我好几次,希望也能帮到你。
本文还有配套的精品资源,点击获取