欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

揭开 dom-to-image 的神秘面纱:原理解析

最编程 2024-08-10 11:36:17
...

背景


最近接到需求要做海报,UI给一张背景图然后内容自己填充最后生成一张图片。刚开始做没多想就拿canvas画,一些简单的还好,能够hold的住。遇到元素多的就麻烦了????。后来发现有dom-to-image的方案,不错,解决的我的问题????????。话不多说,下面就来说说他是怎样实现的。

Api介绍


看了看代码,一共887行,代码量不多,每个方法都比较易懂。核心Api有以下几个。

    toSvg
    toPng
    toJpeg
    toBlob
    toPixelData

顾名思义了,这里就不多说,其实这几个方法实现方式都一样的。第一个方法toSvgdom节点转成svg,然后其他的方法都是拿到svg后处理为dataUrl再处理为cavnas再操作的。下面就拿 toPng 去展开说明。

代码解读


1. toPng

  function toPng(node, options) {
    return draw(node, options || {}).then(function (canvas) {
      return canvas.toDataURL();
    });
  }

可见,是通过draw方法将dom节点转为canvas,然后通过canvas获取图片资源。

2.draw

  function draw(domNode, options) {
    // 将dom节点转为svg
    return toSvg(domNode, options)
      // 拿到的svg是image data URL,这里进一步通过svg创建图片
      .then(util.makeImage)
      .then(util.delay(100))
      .then(function (image) {
        // 通过图片创建canvas并返回
        var canvas = newCanvas(domNode);
        canvas.getContext("2d").drawImage(image, 0, 0);
        return canvas;
      });
    // 新建canvas节点,处理dataUrl资源,和options参数
    function newCanvas(domNode) {
      var canvas = document.createElement("canvas");
      canvas.width = options.width || util.width(domNode);
      canvas.height = options.height || util.height(domNode);

      if (options.bgcolor) {
        var ctx = canvas.getContext("2d");
        ctx.fillStyle = options.bgcolor;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
      }

      return canvas;
    }
  }

3.toSvg

  function toSvg(node, options) {
    options = options || {};
    copyOptions(options);
    return Promise.resolve(node)
      .then(function (node) {
       // 递归克隆dom节点
        return cloneNode(node, options.filter, true);
      })
       // 嵌入字体,找出所有font-face样式,添加入一个新的style里面
      .then(embedFonts)
       // 将图片链接转换为dataUrl形式使用
      .then(inlineImages)
       // 将options里面的一些style放进style里面
      .then(applyOptions)
      .then(function (clone) {
        // 创建svg,将dom节点通过 XMLSerializer().serializeToString() 序列化为字符串
        // 然后用 foreignObject 包裹,就能将dom转为svg。
        return makeSvgDataUri(
          clone,
          options.width || util.width(node),
          options.height || util.height(node)
        );
      });
    // 处理一些options的样式
    function applyOptions(clone) {
      if (options.bgcolor) clone.style.backgroundColor = options.bgcolor;

      if (options.width) clone.style.width = options.width + "px";
      if (options.height) clone.style.height = options.height + "px";

      if (options.style)
        Object.keys(options.style).forEach(function (property) {
          clone.style[property] = options.style[property];
        });

      return clone;
    }
  }

4. cloneNode

  function cloneNode(node, filter, root) {
    if (!root && filter && !filter(node)) return Promise.resolve();

    return Promise.resolve(node)
      .then(makeNodeCopy)
      .then(function (clone) {
        return cloneChildren(node, clone, filter);
      })
      .then(function (clone) {
        return processClone(node, clone);
      });

    function makeNodeCopy(node) {
      // 遇到canvas转为image对象
      if (node instanceof HTMLCanvasElement)
        return util.makeImage(node.toDataURL());
      // 克隆第一层
      return node.cloneNode(false);
    }
    // 克隆子节点
    function cloneChildren(original, clone, filter) {
      var children = original.childNodes;
      if (children.length === 0) return Promise.resolve(clone);
    
      return cloneChildrenInOrder(clone, util.asArray(children), filter).then(
        function () {
          return clone;
        }
      );
      // 递归克隆
      function cloneChildrenInOrder(parent, children, filter) {
        var done = Promise.resolve();
        children.forEach(function (child) {
          done = done
            .then(function () {
              return cloneNode(child, filter);
            })
            .then(function (childClone) {
              if (childClone) parent.appendChild(childClone);
            });
        });
        return done;
      }
    }

    function processClone(original, clone) {
      if (!(clone instanceof Element)) return clone;

      return Promise.resolve()
        .then(cloneStyle)
        .then(clonePseudoElements)
        .then(copyUserInput)
        .then(fixSvg)
        .then(function () {
          return clone;
        });
      // 克隆节点上面所有使用的样式。
      function cloneStyle() {
        // 顺便提提,为什么不用style,因为如果什么样式也没有设置的话,style是光秃秃的
        // 而getComputedStyle则能获取到应用在节点上面所有样式
        copyStyle(window.getComputedStyle(original), clone.style);

        function copyStyle(source, target) {
          if (source.cssText) target.cssText = source.cssText;
          else copyProperties(source, target);

          function copyProperties(source, target) {
            util.asArray(source).forEach(function (name) {
              target.setProperty(
                name,
                source.getPropertyValue(name),
                source.getPropertyPriority(name)
              );
            });
          }
        }
      }
      // 提取伪类样式,放到css
      function clonePseudoElements() {
        [":before", ":after"].forEach(function (element) {
          clonePseudoElement(element);
        });

        function clonePseudoElement(element) {
          var style = window.getComputedStyle(original, element);
          var content = style.getPropertyValue("content");

          if (content === "" || content === "none") return;

          var className = util.uid();
          clone.className = clone.className + " " + className;
          var styleElement = document.createElement("style");
          styleElement.appendChild(
            formatPseudoElementStyle(className, element, style)
          );
          clone.appendChild(styleElement);
          function formatPseudoElementStyle(className, element, style) {
            var selector = "." + className + ":" + element;
            var cssText = style.cssText
              ? formatCssText(style)
              : formatCssProperties(style);
            return document.createTextNode(selector + "{" + cssText + "}");

            function formatCssText(style) {
              var content = style.getPropertyValue("content");
              return style.cssText + " content: " + content + ";";
            }

            function formatCssProperties(style) {
              return util.asArray(style).map(formatProperty).join("; ") + ";";

              function formatProperty(name) {
                return (
                  name +
                  ": " +
                  style.getPropertyValue(name) +
                  (style.getPropertyPriority(name) ? " !important" : "")
                );
              }
            }
          }
        }
      }
      // 处理输入内容
      function copyUserInput() {
      ...
      }
      // 处理svg,创建命名空间
      function fixSvg() {
        if (!(clone instanceof SVGElement)) return;
        clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
        ...
      }
    }
  }

5.makeSvgDataUri

  function makeSvgDataUri(node, width, height) {
    return (
      Promise.resolve(node)
        .then(function (node) {
          // 将dom转换为字符串
          node.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
          return new XMLSerializer().serializeToString(node);
        })
        .then(util.escapeXhtml)
        .then(function (xhtml) {
          return (
            '<foreignObject x="0" y="0" width="100%" height="100%">' +
            xhtml +
            "</foreignObject>"
          );
        })
        /**
         * 顺带提一提
         * 不指定xmlns命名空间是不会渲染的
         * xmlns="http://www.w3.org/2000/svg"
         */

        .then(function (foreignObject) {
          return (
            '<svg xmlns="http://www.w3.org/2000/svg" width="' +
            width +
            '" height="' +
            height +
            '">' +
            foreignObject +
            "</svg>"
          );
        })
        .then(function (svg) {
          return "data:image/svg+xml;charset=utf-8," + svg;
        })
    );
  }

总结


总结下上面几个核心方法流程
  1. 递归去克隆dom节点,其中
    ① 遇到canvas转为image对象
    ② 提取元素computed样式,并且插到新建的style标签上面,对于":before,:after"这些伪元素,会提取其样式,放到新建样式名中并且插入到新建的style标签中,供所属的节点使用。
    ③ 处理输入内容和svg。

  2. 插入字体
    ① 获取所有样式表并处理为数组,提取包含 rule.type === CSSRule.FONT_FACE_RULE 规则,再提取包含 src 的 rules。
    ② 下载资源,将资源转为dataUrl并给 src 使用。

  3. 处理图片
    ① 图片都处理为dataUrl

  4. 序列化dom节点为字符串,然后在 foreignObject 嵌入转换好的字符串,foreignObject 能够在 svg 内部嵌入XHTML,再将svg处理为dataUrl数据。

  5. 用 canvas 渲染出处理好的 dataUrl 数据。

  6. 最后,拿到canvas了,想怎样处理都行了。

参考

SVG 简介与截图等应用

gitlab提取了源文件出来,并写了注释,有兴趣可以自己研究下,看看其中的工作方式。