web图片编辑器生图原理,原来如此简单...

770 阅读7分钟

前言

今天是国庆节前的倒数第二天,办公室已经能明显感受到一种假期的“松弛感”。身在办公桌前,但心已飘到了我将要攀登的四姑娘山大峰。是的,我已无心工作。一眼扫过书签栏的掘金图标,点开一看,不由得一声唏嘘感慨! 原来自己距离上次更新已过 9 个月有余,说好的矢志不渝,活到老,更到老呢?hh,掐指一算,今日正是更文吉日。遂,更。言归正传,今天聊聊这段时间主要在跟进的一个技术方向- 在线图片编辑器

图片编辑器的用途我就不做过多介绍了,相信大家或多或少都接触过。在国外知名的有 Adobe Spark 、Fotor 等设计平台,专注于专业的在线化图像制作工具领域,还有的就是一些不追求专业化,但力争做到简洁易用的在线设计平台:比如国内的稿定设计、美图设计室等,我下边讲的就是这类在线编辑器,比较容易嵌入到大家各自的业务中快速落地。

image.png

方案

各家采取的技术方案上,虽然都有各自业务的考量,但是也无外乎这三种技术方案:

纯 canvas 方案

canvas 方案其实是顺理成章的选择。毕竟 canvas 本身就是在 web 领域绘制图像用的,最终导出为图片文件也很便捷。但是由于原生的 api 实在过于基础,难以应对图片编辑器这种复杂的开发场景。所以大家一般会选择一些上层框架来做。有点类似于js原生开发网页与更上层的现代前端框架的区别。整体是对象化、链式化的封装思路,最终都可以序列化为 json,方便保存在数据库里。比如老牌的fabric.js、以及后出的 konva.js 方案。两种我都深入调研过,都可使用,基本都不会有大坑,fabric因为出现较早,碰到问题时,网上的解决方案会多一些,整体会更加成熟,而 konva.js 的使用上会更加的简洁一些,TS友好,文档也更加友好。如果让我二选一,我会选 konva.js,因为 fabric.js 的文档非常不稳定,且文档的讲解顺序非常不利于新人上手。

纯 DOM 方案

这种方案很好解释,就像正常开发页面一样,利用react、vue等框架渲染出 DOM,浏览器根据 DOM 绘制图片内容。下图的美图设计室就是这样做的: image.png 但采用这种方案有个关键的问题要解决,如何将 DOM 转为图片呢?这里提供两种思路:

  1. 将 DOM 样式转换为 canvas 样式。最具代表的是 html2canvas 这个库,但这种方案基本只适用样式比较常规且简单的业务场景,比如 H5 海报生成等。针对样式复杂的编辑器场景,基本不可能做到完美的转换,会有很多样式转换不完全或者转换错误的 bug,所以这个库到现在还是 beta 版本。
  2. 调用第三方无头浏览器进行截图。 主要是浏览器本身并没有提供截图的api,但第三方的 Puppeteer 是有这个能力的。所以可以启动一个 Node 服务器,提供一个http接口,调用 Puppeteer 渲染出图片内容,进行页面截图。

image.png

具体代码可以参考下方:

            export class ImageEdtorService {
              @Inject()
              ctx: Context;
              @Inject()
              puppeteerService: PuppeteerService;
              @Inject()
              httpService: HttpService;
              @Config('egg')
              eggConfig: MidwayConfig['egg'];
              async schema2Image(schema: ImageSchema) {
                const page = await this.puppeteerService.newPage();

                // const deviceScaleFactor = 2;
                page.setViewport({
                  width: schema.imageSize?.width,
                  height: schema.imageSize?.height,
                  // deviceScaleFactor,
                });
                await page.evaluateOnNewDocument(schema => {
                  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                  // @ts-ignore
                  window.schema = schema;
                }, schema);

                await page.goto(
                  `http://127.0.0.1:9001/editor/previewPage`,
                  {
                    waitUntil: 'networkidle0',
                  }
                );
                const screenshotBuffer = await page.screenshot({
                  // path: path.join(__dirname, `./screenshot_${Date.now()}.jpg`),
                  type: 'jpeg',
                  encoding: 'binary',
                  quality: 100,
                  omitBackground: true,
                  // optimizeForSpeed: true,
                  fullPage: false,
                });
                await this.puppeteerService.closePage(page);
                return screenshotBuffer;
              }
            }

两种方案的组合

  • 纯 DOM 方案:
    • 缺点:
      • 性能上存在劣势。受限于浏览器 DOM 的渲染性能,在一些高性能要求的场景,比如绘制大量内容节点时,会遇到一些性能问题。虽然可以进行优化,比如利用 tranform 代替传统的绝对定位,会触发 GPU 加速,从而在拖拽性能上表现更流畅,减少掉帧现象。但是还是不能比 canvas 的绘制性能相提并论。
      • 生图需要额外使用 Node 调用无头浏览器进行截图,增加了工程上的复杂性。
    • 优点:
      • 易于开发。前端框架直接开发,上手简单,DOM 调试起来也比 canvas 方便。
      • 拓展性强。基础元素不仅可以支持 SVG 等矢量元素,还可以支持 React 组件,那意味着物料可以支持运营动态的上传。特别适合实际业务中的开发。这也是我最终采用纯DOM的一个原因之一,还有另外一个因素是,我们希望生图可以在服务器端完成,这样其他合作方比如算法侧在AIGC的环节就可以复用前端这套合图方案。纯客户端的方案肯定是难以做到,所以的缺点1可以接受,缺点2在我们看来并没有增加额外的成本。
  • 纯 canvas 方案:
    • 缺点:
      • 开发成本高。这一点不仅体现在编辑器本身的技术实现,也体现在物料未来的扩展性上。还有就是这种方案需要解决一个导出需要隐藏编辑态的问题:图片编辑器最重要的是编辑功能,会在画板上有很多的编辑状态,比如选中元素出现的选择框以及各种操作手柄、功能菜单。如果这些编辑态也用 canvas 来实现,一来开发成本比较高,二来在最终导出为图片的时候,如果此时正在编辑,会把当前的编辑态也保存到最终的图片内容上。就像下图的例子,需要在导出时额外隐藏一下编辑态。 image.png
      • 生图可能会存在不一致的问题。这种纯客户端生图的方案可能导致机型、浏览器的不同产生的样式效果会有差异。
    • 优点:
      • 性能高。 拖拽基本不会出现掉帧的现象,而且canvas本身的原理决定了在绘制大量图形时,性能也不会明显衰减,除非是 js 阻塞,但即使这种情况,也可以像 Figma 那样使用 WebAssembly 技术进行性能优化。
      • 生图方便。可直接导出图片内容。
  • 编辑态使用DOM、图片渲染使用Canvas
    稿定设计就是采用的这种方案,好处可以集两种方案的优点为一身。巧妙的地方在于,因为编辑态交互是DOM 来承担的,所以 canvas 可以直接导出没有编辑态的图片。但需要注意一点,需要同步好 DOM 编辑态与 canvas 渲染内容的位置、缩放等状态,毕竟是两套渲染体系。如下图所示那样: image.png

最后

我实现的编辑器长这个样子:

image.png

生图采用了服务端DOM转图片的生图方案。整个开发过程,我觉得最难的不是代码实现,而是在内部 linux 机器上部署 puputter 无头浏览器的过程,耗费了很多精力才搞定。如果你有碰到类似问题,可以私信或者评论,看我的踩坑经验能不能帮到大家。

整体的开发过程,最有趣的,其实是研究的 CSS 知识越来越深了。因为要实现各种图片内容的效果嘛,设计师会提很多合理但有挑战的需求,比如 CSS 的蒙版属性,我就是第一次接触。

image.png

好了,今天的文章就到这里,小伙伴们可以按需选择适合自己业务场景的方案。如果你有更好的方案,欢迎评论区积极贡献分享哦~