教你如何将svg元素写入到系统剪切板中

774 阅读4分钟

本文已开启[新人创作礼]活动,一起开启掘金创作之路。

前言

两周前,我的研发领导告诉我说:小波,咱们产品需要添加一个新功能,对于咱们这种建模的产品(使用鼠标拖拽在画布区域创建svg元素),BOSS需要能够直接选中图形之后,通过复制之后,可以直接在word文档中粘贴出来,这两天实现一下。

我:好的没问题,马上安排!🤓

思路

分解问题

要实现这种需求,就需要对需求做出分解:

  1. 如何将选中的svg元素单独抽离出来制作成图片?
  2. 如何才能调用系统剪切板,将在剪切板中写入图片的数据?

实现

  1. 首先获取到选中的svg元素,如果没有选中直接将画布中的svg元素全部获取出来
  2. 获取到的svg元素通过XMLSerializer对象将DOM树转化为xml字符串
  3. 将转化出来的xml字符串通过原生APIbtoa编码为 Base64编码的ASCII 字符串。
  4. 将编码好的图片通过二进制的数据写入到系统剪切板中,就完成了从浏览器复制svg元素到浏览器的操作啦

好啦,思路有了,开始实现~;

具体实现

这里通过写一个demo,最简短的代码实现这个需求

我们公司中建模工具使用的是mxgraph这个插件,我们就以此来实现

首先新建一个文件夹,进入文件夹中下载mxgraph

npm install mxgraph

新建一个html文件,写一个mxgraph的hello,world!

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <title>hello, world!</title>
  <style>
      * {
          margin: 0;
          padding: 0;
      }
      #image-con {
          width: 500px;
          height: 500px;
      }
  </style>
</head>
<body onload='main(document.getElementById("graphContainer"))'>
<div id='graphContainer'
     style="position:relative;overflow:hidden;width:300px;height:300px;background:#eee;cursor:default;">
</div>
<button id='copyBtn'>
  复制
</button>
<div id='image-con'>

</div>
<script>
  mxBasePath = './node_modules/mxgraph/javascript/src';
</script>
<script src='./node_modules/mxgraph/javascript/mxClient.js'></script>
<script>
function main(container) {
  if (!mxClient.isBrowserSupported()) {
    mxUtils.error('Browser is not supported!', 200, false);
  } else {
    const graph = new mxGraph(container);
    const parent = graph.getDefaultParent();
    graph.getModel().beginUpdate();
    try {
      const v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
      const v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
      graph.insertEdge(parent, null, '', v1, v2);
    } finally {
      graph.getModel().endUpdate();
    }
  }
}
</script>
</body>
</html>

显示界面如图所示, 通过点击复制按钮触发操作 image.png 接下来开始重点部分,通过实例化出来的graph中可以获取到当前灰色区域中的两个块和连接线,首先通过graph获取内容的区域大小

let { width, height, x, y } = graph.getGraphBounds();

结构出来区域的x,y的位置,如果直接靠结构出来的想,y的值去生成图片的话,由于生成出来的图片是以左上角未为顶点去生成的,就可能会造成图片内容显示不全,那么就需要对位置做出优化,我们可以通过平移灰色区域的内容的位置来优化

// 通过减去界面的偏移量来让内容区域回到0,0点
x -= graph.view.translate.x;
y -= graph.view.translate.y;
graph.view.setTranslate(-x, -y);

获取到svg元素,然后通过XMLSerializer转码为xml字符串, 通过btoa将xml字符串转为base64的字符串;

const divContent = graph.container.firstElementChild;
const xmlString = new XMLSerializer().serializeToString(divContent);
const base64 = btoa(unescape(encodeURIComponent(xmlString)));

注意看,通过btoa编码的字符串通过了encodeURIComponentunescape, 由于btoa无法将中文转换为对应的base64的ASCLL字符串,就需要通过encodeURIComponent将字符串加密为转义序列,中文会转换为·%xx,之后通过unescape将含有%xx十六进制形式编码的字符都用ASCLL字符集中等价的字符代替,简单来说就是将其中的中文或特殊字符通过这两个方法编码为ASCLL字符。 base64编码完成之后就完成一半了,然后新实例化image,将转换好的base64拼接为image可以识别的字符串

const image = new Image();
image.width = width;
image.height = height;
image.src = 'data:image/svg+xml;base64,' + base64;

等待image加载完毕之后将image加载到创建的canvas中,通过canvas转为blob数据

image.onload = function() {
  const canvas = document.createElement('canvas');
  canvas.width = width
  canvas.height = height
  const ctx = canvas.getContext('2d');
  ctx.drawImage(image, 0, 0, width, height)
  document.getElementById('image-con').appendChild(image);
  canvas.toBlob(blob => {
      writeImage(blob)
  })
};

明明也可以使用svg+xml也可以转化为blob,这里需要特别说明为什么需要使用canvas,由于系统剪切板目前只能写入png的二进制图片数据,canvas转blob之后默认类型就是png

⭐️将blob二进制数据写入到剪切板,调用navigatorclipboard.write方法,写入一个数组,数组中实例化ClipboardItem写入数据之后就可以将图片写入系统剪切板

async function writeImage(blob) {
  try {
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
    console.log('Image copied.');
  } catch (err) {
    console.error(err.name, err.message);
  }
}

全量代码

<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <title>hello, world!</title>
  <style>
      * {
          margin: 0;
          padding: 0;
      }
      #image-con {
          width: 500px;
          height: 500px;
      }
  </style>
</head>
<body onload='main(document.getElementById("graphContainer"))'>
<div id='graphContainer'
     style="position:relative;overflow:hidden;width:300px;height:300px;background:#eee;cursor:default;">
</div>
<button id='copyBtn'>
  复制
</button>
<div id='image-con'>

</div>
<script>
  mxBasePath = './node_modules/mxgraph/javascript/src';
</script>
<script src='./node_modules/mxgraph/javascript/mxClient.js'></script>
<script>
  function main(container) {
    if (!mxClient.isBrowserSupported()) {
      mxUtils.error('Browser is not supported!', 200, false);
    } else {
      const graph = new mxGraph(container);
      const parent = graph.getDefaultParent();
      graph.getModel().beginUpdate();
      try {
        const v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
        const v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
        graph.insertEdge(parent, null, '', v1, v2);
      } finally {
        graph.getModel().endUpdate();
      }
      const btn = document.getElementById('copyBtn');
      btn.onclick = function() {
        let { width, height, x, y } = graph.getGraphBounds();
        x -= graph.view.translate.x;
        y -= graph.view.translate.y;
        graph.view.setTranslate(-x, -y);
        const divContent = graph.container.firstElementChild;
        const xmlString = new XMLSerializer().serializeToString(divContent);
        const base64 = btoa(unescape(encodeURIComponent(xmlString)));
        const image = new Image();
        image.width = width;
        image.height = height;
        image.src = 'data:image/svg+xml;base64,' + base64;
        image.onload = function() {
          const canvas = document.createElement('canvas');
          canvas.width = width
          canvas.height = height
          const ctx = canvas.getContext('2d');
          ctx.drawImage(image, 0, 0, width, height)
          document.getElementById('image-con').appendChild(image);
          canvas.toBlob(blob => {
            writeImage(blob)
          })
        };
      };
    }
  }

  async function writeImage(blob) {
    try {
      await navigator.clipboard.write([
        new ClipboardItem({
          [blob.type]: blob,
        }),
      ]);
      console.log('Image copied.');
    } catch (err) {
      console.error(err.name, err.message);
    }
  }
</script>
</body>
</html>

注意

首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。 image.png 其次使用clipboard要注意兼容性,比如火狐的read和write方法需要再浏览器中开启特定的开关,需要被用户须知的一个操作等

image.png

总结

svg元素转为图片写入剪切板需要4步,其中的难点就是在于将svg元素转码为base64,其中需要注意如果有中文或是特殊字符会转义失败或是显示乱码,其次就是剪切板的使用的一些特定条件必须是安全环境https或是localhost才能使用

参考

  1. canvas
  2. XMLSerializer
  3. btoa
  4. 阮一峰老师Clipboard的介绍文档

最後

以上,便是本次分享~ 感谢大家能够看到这里,谢谢各位的支持~