开源图片编辑器, 如何把psd解析成fabric画布模板, 提高效率

背景

最近一直在开发图片编辑器vue-design-editor, 基于fabric实现. psd解析基于psd.js实现

github地址

预览地址

图片编辑器如果能直接把psd解析到fabric画布中, 在画布中编辑操作, 然后导出自己需要类型的图片, 可以提高图片制作的效率,同时看github上,目前没有大佬开源出该功能, 所以实现了psd解析成fabric画布模板功能.

阅读本文能学习到

  • 策略模式实现不同类型文件解析成模板逻辑
  • psd解析逻辑封装
  • psd.js库解析psd
  • psd生成支持fabric渲染的json格式
  • 异步实现n层级下组内图片上传, 不阻塞, 保证所有图片上传完成
  • psd.js踩坑
  • fabric渲染json

策略模式实现不同类型文件解析成模板逻辑

为什么要用策略模式, 目的是方便后期扩展, 在未来规划中, 不仅支持psd导入生成模板, 也支持 Sketch / Ai / PPTX / PDF 以及 图片 / 视频格式(希望大家能一起加入), 使用策略模式就可以通过判断文件类型执行不同模式下的逻辑, 方便管理和提高代码的可读性

如何正确判断文件类型, 上文有讲

图片编辑器中实现文件上传的三种方式和二进制流及文件头校验文件类型

代码实现

js 复制代码
const mapStrategyType: any = {
  psd: (file: File) => {
    return new Psd(file);
  },
  sketch: (file: File) => {
    return new sketch(file);
  },
  ai: (file: File) => {
    return new Ai(file);
  },
  ...
};
const handler = mapStrategyType[fileType]();

fileType文件类型, 就会匹配mapStrategyType对应类型的类, 执行方法创建实例

psd解析逻辑封装

通过创建类的形式对psd解析逻辑进行封装

js 复制代码
Class Psd {}
export default Psd

这样的话每次创建一个实例都可以单独进行psd解析, 在不同模块下都能直接使用该方法, 互不影响

支持配置两个参数 uploadUrl 图片上传的接口 uploadCallback 图片上传后解析逻辑, 目的是可以高度自定义解析接口返回的数据返回图片链接, 给图片元素设置src字段

psd.js库解析psd

psd解析

js 复制代码
 import PSD from 'psd.js'
 // 文件转为url
 const url = URL.createObjectURL(file);
 // 通过psd.js库中fromURL方法解析成js数据
 PSD.fromURL(url).then(async (psd) => {}

psd导出为png

作用是可以当做模板的预览图,缩略图, 而不需要单独生成

js 复制代码
 getPsdBgImage(psd) {
    return new Promise((resolve) => {
      const l_background = psd.image.toBase64();
      let img = new Image();
      img.src = l_background;
      img.setAttribute("crossOrigin", "Anonymous");
      img.onload = () => {
        resolve({
          backgroundImage: l_background,
          width: img.width,
          height: img.height,
        });
      };
    });
  }

获取图层数据

js 复制代码
const childrens = psd.tree().children();

图层类型判断

每个图层都是一个js对象

每个对象有个原型属性type

type: group | layer 两种类型

group是组, layer是普通元素, 包括图片和文本

如何判断图片和文本?

js 复制代码
const typeTool = e.get("typeTool");
if (typeof typeTool !== "undefined") {
    // 文本
}else{
    // 图片
}

图层属性获取

下文的e代指psd解析后的图层数据

基础元素

fabric基础元素组成包括位置

json 复制代码
['width', 'height', 'left', 'top', 'opacity', 'visible']

opactiy和visible 是每个fabric元素都有的属性, 指元素的透明度和是否可见

js 复制代码
const left = e.left;
const top = e.top;
const width = e.width;
const height = e.height;
const opacity = e.export().opacity;
const visible = e.export().visible;

图片独有属性

src 图片链接

获取方法

js 复制代码
i.layer.image.toBase64() // i指图层

文本独有属性

fill 文本颜色

js 复制代码
const color = e.export().text.font.colors[0];
const fill = `rgb(${color[0]},${color[1]},${color[2]})`;

fontWeight 文本字重

js 复制代码
const fontWeight = e.export().text.font.weights[0];

fontSize 字体大小

js 复制代码
const fontSize = e.export().text.font.sizes[0];

fontSize 字体大小

js 复制代码
const size = e.export().text.font.sizes[0];
const transY = exportObj.text.transform.yy;
const fontSize =  Math.round(size * transY * 100) * 0.01; 

fontStyle 字体样式 (是否斜体)

js 复制代码
if (e.export().text.font.styles[0] != "normal") {
   const fontStyle = e.export().text.font.styles[0];
}

fontFamily 字体

js 复制代码
const fontFamily = e.export().text.font.names[0];

text 文本

js 复制代码
const text = e.export().text.value;

textAlign 文本对齐

js 复制代码
if (e.export().text.font.alignment[0] != "left") {
    const textAlign = e.export().text.font.alignment[0];
}

charSpacing 文字间距

js 复制代码
if (e.tracking != 0) {
  const charSpacing = e.tracking;
}

angle 文本旋转角度

js 复制代码
  function getRotation(transform) {
    let rotation = Math.round(
      Math.atan(transform.xy / transform.xx) * (180 / Math.PI)
    );

    if (transform.xx < 0) {
      rotation += 180;
    } else if (rotation < 0) {
      rotation += 360;
    }

    return rotation;
  }
let angleR = this.getRotation(e.export().text.transform);
if (angleR != 0) {
  const angle = angleR;
}

group 独有属性

js 复制代码
 e.children(); // 子图层

psd生成支持fabric渲染的json格式

从上文已经知道如何解析出group、image、text指定属性

我们首先创建一个数组

js 复制代码
let result = [];

存储解析出的psd图层, 根据图层指定类型赋值不同属性

比如group

js 复制代码
if (e.type == "group") {
          var i_child = e.children(); // 子图层
          let newGroupObj = {};
          newGroupObj.type = "group";
          newGroupObj.left = e.left;
          newGroupObj.top = e.top;
          newGroupObj.width = e.width;
          newGroupObj.height = e.height;
          newGroupObj.opacity = e.export().opacity;
          newGroupObj.visible = e.export().visible;
          newGroupObj.id = uuid();
          newGroupObj.name = e.name;
          newGroupObj.objects = [];// 子图层, 相当于上文的result
          e = newGroupObj;
          result[i] = e;
          return this.getPsdJson(i_child, res, e.objects);
}

赋值完成后给result赋值就可以把每个图层解析出来, 不过需要注意的是上文group代码, 因为group包括子图层, 所以需要递归遍历图层赋值

最后需要注意一点

psd解析出来的图层顺序和fabric的图层顺序相反

比如psd的最底图层解析出来是数组的最后一个, fabric是第一个

翻转顺序, 组递归翻转

js 复制代码
 function resReverse(group) {
    return group.reverse().map((item) => {
      if (item.type == "group") {
        item.objects = this.resReverse(item.objects);
        return item;
      } else {
        return item;
      }
    });
  }
  result = this.resReverse(result);

异步实现n层级下组内图片上传, 不阻塞, 保证所有图片上传完成

如果按同步的思维方法, 上传完成一张图片才解析下一个图层, 假设一张图片上传耗时100ms, 100张图片就要耗时10s, 耗时太长, 体验差, 所以需要异步来实现图片同时上传

异步的方法, 上百张同时上传, 需要保证同时上传完成后拿到最终解析出的json结果, 否则个别图层对象的src还没有获取到, 就拿解析结果后渲染画布, 导致数据不准确

如果psd的最大层级为1, 那每个图层创建一个Promise, 最后使用Promise.all一下不就能保证上传完成吗

但是如果psd的最大层级为100、 1000呢, 怎么保证第1000图层下的图片上传完成

我是如何实现的

getPsdJson 方法的第二个图层就是上级图层的Pomise的resolve,

每个层级都有一个Promise数组, 在保证子层级解析完成后才执行父层级的resolve, 这样就能实现子图层解析完成后, 才会认为父图层的组模块解析完成

js 复制代码
const childrens = psd.tree().children();
let result = [];
const outProArr = this.getPsdJson(childrens, null, result);

 Promise.all(outProArr)
.then(() => {
  console.log(result)
})

function getPsdJson(childrenList, resolve, list) {
    let outProArr = [];
    Array.from(childrenList).forEach((e, i) => {
      let outPro = new Promise((res) => {
        // 顶级图层/文件夹
        if (e.type == "group") {
          var i_child = e.children(); // 子图层
          return this.getPsdJson(i_child, res, e.objects);
        } else {
          let itemObj = {};
          itemObj = e;
          this.getChildData(childrenList[i])
            .then((a) => {
              if (a) {
                itemObj.type = a.type;
                if (a.type == "text") {
                  itemObj = newTextObj;
                } else if (a.type == "image") {}
                res(itemObj);
              } else {
                res();
              }
            })
            .catch((e) => {
              console.error(childrenList[i].name, e);
            });
        }
      });
      outProArr.push(outPro);
    });
    if (resolve) {
      return Promise.all(outProArr).then(resolve);
    } else {
      return outProArr;
    }
  }

以上思路,我们也能实现图片上传的进度, 遍历完图层后,可以得到图片的总数, 每上传完成一张图片, 数量+1, 就能得到已上传数量/总数量的比例, 展示图片上传的进度

psd.js踩坑

问题一

psd.js依赖Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

但是该类只存在node.js中, 客户端不支持, 所以需要在浏览器端兼容一下

js 复制代码
npm i Buffer -S
if (typeof window.Buffer === "undefined") {
  window.Buffer = Buffer.Buffer;
}

问题二 psd.js最新版本3.9.0, 解析出来的group数据有问题

宽高和位置信息都是0

所以需要安装低版本才行, 目前安装的是3.6.3

fabric渲染json

ts 复制代码
  importJSON = async (json: any) => {
    console.log("json", json);
    if (!this.canvas.contextTop) return;
    this.isimporting = true;
    try {
      if (!this.isEmptyCanvas()) {
        this.canvas.clear();
      }
      if (typeof json === "string") {
        json = JSON.parse(json);
      }
      const workarea = json.find((obj: any) => obj.id == "workarea");
      // this.workareaHandler.initialize();
      if (workarea && this.workareaHandler.workspace) {
        this.workareaHandler.setSize(workarea.width, workarea.height);
      } else {
        this.workareaHandler.initialize();
        this.workareaHandler.setSize(workarea.width, workarea.height);
      }
      for (let i = 0; i < json.length; i++) {
        const obj = json[i];
        if (obj.id == "workarea") continue;
        if (obj.id == "background") {
          await this.workareaHandler.setBgImage(obj);
          continue;
        } else if (obj.type == "group") {
          await this.importGroupJSON(obj);
          continue;
        }
        if (!obj.id) {
          obj.id = uuid();
        }
        await this.add(obj, true);
      }
      this.canvas.renderAll();
    } catch (e) {
      console.error(e);
    }
  };

遍历json, 按type创建元素, 需要注意group元素创建需要递归, 详细代码github地址

简介

vue-design-editor 是仿搞定设计的一款开源图片编辑器, 支持多种格式的导入,包括png、jpg、gif、mp4, 也可以一键psd转模板(后续开发)

github地址 预览

上个开源库是 starfish-vue3-lowcode

github地址 预览

相关推荐
黄尚圈圈30 分钟前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水1 小时前
简洁之道 - React Hook Form
前端
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch5 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光5 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   5 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   5 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web5 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常5 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇6 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器