GeoJSONcn
GeoJSONcn / 技术专栏 / 把 GeoJSON 画成能印刷的…

把 GeoJSON 画成能印刷的地图全图:投影、画布映射与三种导出格式

发布于 2026-08-02 · GeoJSONcn 技术团队 · 阅读约 11 分钟

--- title: 把 GeoJSON 画成能印刷的地图全图:投影、画布映射与三种导出格式 ---

网页地图截个屏放进 PPT,投影仪一打就糊。年报要 300dpi,设计师还想在 Illustrator 里单独改某个省的填色。这时候要的不是切片地图,是一张从 GeoJSON 直接渲染出来的静态全图。

静态出图别用 Web 墨卡托

Leaflet、MapLibre 默认的 EPSG:3857 是给瓦片拼接用的,代价是纬度越高面积放得越大,倍数是 sec²φ。50°N 附近约 2.42 倍,20°N 附近约 1.13 倍,两头差了两倍多。画全国图的结果就是黑龙江看着比实际大一圈,海南被压小。屏幕上滑来滑去没人计较,印在纸上做面积对比就是硬伤。

静态制图该用等积投影。国内常用 Albers 等积圆锥,中央经线 105°E,双标准纬线 25°N 和 47°N。d3-geo 里对应 geoConicEqualArea:

import { geoConicEqualArea, geoPath } from 'd3-geo';

const projection = geoConicEqualArea()
  .parallels([25, 47])
  .rotate([-105, 0])
  .center([0, 36]);

rotate 的第一个分量取中央经线的负值,center 是旋转之后坐标系里的位置,这组参数就是把图幅中心放在 105°E、36°N。画单个省份时,把这三个参数换成该省的中心经纬度,形变会小很多。

让边界正好填满画布

缩放比和平移量不用手算,fitExtent 会按要素的经纬度范围反推:

const width = 1600, height = 1200, pad = 40;

projection.fitExtent(
  [[pad, pad], [width - pad, height - pad]],
  geojson
);

const path = geoPath(projection).digits(2);

digits 是 d3-geo 3.1 之后才有的,控制输出路径的小数位数。全国图在 1600px 画布上留两位小数,肉眼看不出区别,SVG 体积却能省下一大截。想更狠一点,渲染前先跑一轮抽稀,见 Douglas-Peucker 抽稀。

拼 SVG 字符串:

const paths = geojson.features
  .map(f => `<path d="${path(f)}" data-adcode="${f.properties.adcode}"/>`)
  .join('\n');

const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
<g fill="#e8f0fb" stroke="#3b6fb5" stroke-width="0.8" stroke-linejoin="round" fill-rule="evenodd">
${paths}
</g>
</svg>`;

adcode 写进 data- 属性,后面用脚本按代码批量改填色就很方便,做分级设色也是这个套路。

三个容易翻车的地方

空洞被填实

面里的内环表示飞地或者空洞,SVG 默认的 nonzero 填充规则要求外环和内环绕向相反才挖得出洞。数据环向不规范时洞就被填满了,改用 fill-rule="evenodd" 能绕开这个依赖。想从源头解决,按 RFC 7946 规范纠正绕向。

远海岛屿把图拉小

fitExtent 认的是全部要素的包围盒,一个远处的小岛就能让陆地主体缩到画面中间一小块。解决办法是先按面积过滤掉零星小面,或者只拿主体要素去 fit,小岛单独画。逐个要素查包围盒用 path.bounds(feature)。

线宽随缩放变化

viewBox 一缩放,描边就跟着变粗变细。浏览器里加 vector-effect="non-scaling-stroke" 能锁死线宽,可 Office 和一部分矢量编辑器不认这个属性。交付印刷稿,还是按最终尺寸直接给绝对线宽稳妥。

栅格化到 300dpi

SVG 转 PNG 用 sharp,底层走 librsvg:

import sharp from 'sharp';
import { writeFileSync } from 'node:fs';

writeFileSync('china.svg', svg);

await sharp(Buffer.from(svg), { density: 300 })
  .png()
  .toFile('china.png');

density 的基准是 72dpi,设成 300 相当于把 SVG 的标称尺寸放大约 4.17 倍,1600×1200 出来是 6667×5000 左右。反着算目标尺寸也行:A4 幅面 210mm 宽,300dpi 下要 210 ÷ 25.4 × 300 ≈ 2480px,高度 297mm 对应 3508px。

要在 Word 和 PPT 里改颜色,导 EMF

Office 对 SVG 的支持只能算能看,取消组合之后经常丢样式。EMF 是 Windows 原生的矢量格式,插进 PPT 右键取消组合,每个省就是一个独立形状,直接用主题色填。Inkscape 命令行能批量转:

inkscape china.svg --export-type=emf --export-filename=china.emf
格式类型适用场景短板
PNG位图网页、微信文章、快速插图放大失真,300dpi 全国图动辄十几 MB
SVG矢量网页嵌入、Illustrator / Figma 二次设计Office 兼容性差
EMF矢量Word、PPT 里拆分填色主要在 Windows 生态可用
PDF矢量印刷、论文投稿需专业软件才能编辑

出图前的顺序

先合并到目标层级,再抽稀,最后投影出图。顺序反了,相邻边界会被简化成不同形状,图上就冒出细缝,成因在拓扑合并 dissolve 那篇讲过。静态出图和网页渲染终归是两条路子,网页端怎么取舍见 前端渲染大体积 GeoJSON。

省、市、区县三级的高清全图和对应的 GeoJSON 源数据,GeoJSON 中国 上就能找到,PNG、SVG、EMF 都是现成的。自己搭一套渲染链路之前,这笔账值得先算算。