GIS 地图打印时空白问题

177 阅读4分钟

前言

之前基于 vue-print-np 改造开发了新版的 Vue 打印插件: vue-print-next,虽然只在一篇文章里提了一句,但也陆续收获了几十个 star。尽管 npm 每周下载量仅有 20-30 次,但能有人使用,我还是感到非常开心。

前段时间有一位用户提了一个 issue,表示在打印地图(如 GIS,类似百度地图、高德地图)时,打印结果是空白的。定位这个问题花费了不少时间,最终找到了原因并解决。本文将复现问题,并详细分析其原因与解决方法。

问题复现

以下是一个用于复现问题的简单 Demo:

<!--
 * @Author: zi.yang
 * @Date: 2024-12-22 23:00:26
 * @LastEditors: zi.yang
 * @LastEditTime: 2024-12-23 08:25:18
 * @Description: 地图打印问题复现
 * @FilePath: /demo/webgl-print/index.html
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>地图打印问题复现</title>
</head>

<body>
  <div id="app">
    <button @click="printMap">打印地图</button>
    <div id="map" ref="mapContainer"
      style="width: 500px; height: 400px; background-color: lightgray;">
      <!-- 模拟地图区域 -->
      <canvas id="mapCanvas" width="500" height="400"></canvas>
    </div>
  </div>

  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script type="module">
    import { VuePrintNext } from 'https://unpkg.com/vue-print-next/dist/vue-print-next.js';

    function draw() {
      const canvas = document.getElementById('mapCanvas');
      const gl = canvas.getContext('webgl');
      if (gl) {
        gl.clearColor(0.0, 0.5, 0.5, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);
      }
    }

    const app = Vue.createApp({
      mounted() {
        draw()
      },
      methods: {
        printMap() {
          new VuePrintNext({ el: '#map' })
        }
      }
    });

    app.mount('#app');
  </script>
</body>

</html>

上述代码中的 canvas 在页面显示时有一个绿色背景方块,但打印结果却是灰白的。这只是一个简单例子,实际上无论 canvas 绘制什么内容,打印结果都只会显示为灰白背景。

如下是打印后的效果:

问题分析

要分析这个问题,首先需要了解目前常用的局部打印方案及其对 canvas 的处理方式。

局部打印原理

局部打印通常通过以下步骤实现:

  1. 获取需要打印的 DOM 元素。
  2. 将该元素克隆到一个新的 iframe 中。
  3. 调用浏览器的打印功能打印该 iframe

对于 canvas 元素,为了确保打印效果,打印库一般会调用 canvas.toDataURL 方法将 canvas 内容转换为 base64 格式的图片,然后将其替换为 img 标签,从而正常显示。

由此可以推断,地图打印为空白的原因很可能是 toDataURL 输出的内容有问题。

尝试直接输出

在控制台中直接调用 toDataURL 输出地图的内容:

const canvas = document.getElementById('mapCanvas');
console.log(canvas.toDataURL());

通过工具解码后,结果是一个空白图片。这表明 toDataURL 方法并未正确转换 canvas 内容。

为了进一步排查,我们尝试使用其他绘图工具(如 ECharts)生成 canvas 并导出 base64,结果是正常的。这说明问题与 WebGL 绘图方式有关。

问题根源

通过查阅资料发现,WebGL 模式下,preserveDrawingBuffer 参数默认值为 false。这是出于性能和内存优化的考虑,在每帧渲染完成后会自动清除画布缓存。对于需要保留画布数据的场景(如打印、截图),这种行为会导致 toDataURL 方法无法获取画布内容,从而输出空白。

解决方法

有两种常见的解决方法:打印前重绘一次,或者开启 preserveDrawingBuffer 配置

方法一:打印前重绘

最简单的一种方式,如果你使用的库支持,或者本身就是你自己写的 webgl 方法。 那么可以在打印之前调用一次渲染方法,这样就可以成功打印了。

const app = Vue.createApp({
  mounted() {
	draw()
  },
  methods: {
	printMap() {
	  // 打印前重新绘制 webgl
	  draw()
	  new VuePrintNext({ el: '#map' })
	}
  }
});

在调用打印功能前调用一次绘制方法 draw(),确保 canvas 内容在打印时是完整的。

方法二:开启配置

在初始化 WebGL 上下文时,将 preserveDrawingBuffer 参数设置为 true,以确保每次绘制后画布内容不会被清除。

可以通过重写 getContext 方法实现:

// 重写 getContext 方法
HTMLCanvasElement.prototype.getContext = (function (origFn) {
  return function (type, attributes) {
    if (type === 'webgl') {
      attributes = Object.assign({}, attributes, {
        preserveDrawingBuffer: true,
      });
    }
    return origFn.call(this, type, attributes);
  };
})(HTMLCanvasElement.prototype.getContext);

在此方法中,我们拦截了 canvas.getContext 方法的调用,将 preserveDrawingBuffer 设置为 true 后再返回上下文。这样在打印时无需额外重绘即可获取正确的内容。

总结

WebGL 中 preserveDrawingBuffer 默认值为 false 是导致打印空白的根本原因。对于需要打印或截图的场景,可以根据需求选择以下方案:

  1. 打印前重绘:适合对性能要求高、不希望永久开启 preserveDrawingBuffer 的场景。
  2. 启用 preserveDrawingBuffer:适合打印或截图需求频繁、对性能要求不高的场景。

如果使用的库(webgl 地图)支持重新渲染方法,还是推荐仅在打印前重绘,避免因为开启preserveDrawingBuffer 导致造成性能问题。

相关链接