[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 4\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n    parser: 'babel-eslint',\n    extends: [\n        '@ecomfe/eslint-config'\n    ],\n    parserOptions: {\n        babelOptions: {\n            configFile: './babel.config.js'\n        }\n    },\n    rules: {\n        'comma-dangle': ['error', {\n            objects: 'never'\n        }]\n    },\n    overrides: [\n        {\n            files: [\n                '**/*.test.js'\n            ],\n            env: {\n                jest: true\n            }\n        }\n    ]\n};\n"
  },
  {
    "path": ".gitignore",
    "content": "# Referenced from https://github.com/github/gitignore/blob/master/Node.gitignore\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n\n# other stuff\n.DS_Store\nThumbs.db\n\n# IDE configurations\n.idea\n.vscode\n\n# build assets\noutput\ndist\ndll\nmock\n"
  },
  {
    "path": ".stylelintrc",
    "content": "{\n    \"extends\": \"@ecomfe/stylelint-config/baidu/default\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 更新日志\n\n### 1.0.6\n[变更] 背景图backgroundImg可不传。若fileType为jpeg则背景为黑色，为png则背景透明\n[修改] 绘制二维码时，需传入有效的宽高\n\n### 1.0.5\n[修复] 绘制多组文字样式不正确问题\n\n### 1.0.4\n[修改] 更新文档\n\n### 1.0.3\n[修改] 修改参数名称以表达正确意义\n\n### 1.0.2\n[修改] 配置文件整理\n\n### 1.0.1\n[修复] 修复notUseCache字段取值错误\n\n### 1.0.0\n[修改] 多个参数名称更新: baseConfig => base、dynamicConfig => dynamic、 imageTimeOut => loadImgTimeOut\n[修改] 统一使用img表示图片，弃用image\n\n### 0.0.7\n[新增] 新增weight属性控制动态元素绘制层级\n[新增] 支持文字设置fontFamily属性\n\n### 0.0.6\n[新增] 增加图片超时时间设置\n[修改] 修改draw仅绘制逻辑\n\n### 0.0.5\n[修改] 修改合成成功返回数据格式\n\n### 0.0.4\n[修改] 输出方法名称变更为mix-image\n\n### 0.0.3\n[修改] 统一输出数据格式\n\n### 0.0.2\n[修复] 修改依赖配置\n\n### 0.0.1\n[发布] 图片合成工具库\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Baidu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# mix-img\nmix-img图片合成工具，通过调用canvas API实现包括图片和文字的合成并最终生成图片base64，合成成功后向用户展示和分享。\n\n![image](https://efe-h2.cdn.bcebos.com/ceug/resource/res/2021-03/1616594872570/aol5g54dm8p7.jpg)\n## Install\n\n```js\nnpm install --save mix-img\n```\n## Quick Start\n```js\nimport {mixImg} from 'mix-img';\nimport {mixConfig} from './mixConfig';  // 配置文件路径自定义\nasync function getImg() {\n    const res = await mixImg(mixConfig);\n    console.log('图片合成结束', res);\n}\n```\n\n> mixConfig参数配置可参见参数说明文档；Lib库使用者可以通过调试工具在本地进行预览调试，生成配置。\n\n## Document\n- [Start](https://github.com/baidu/mix-img/blob/master/README.md)\n- [Example](https://github.com/baidu/mix-img/blob/master/test/e2e/index.js)\n- [mixImg方法使用说明](https://github.com/baidu/mix-img/blob/master/docs/mixImg.md)\n- [mixConfig参数说明文档](https://github.com/baidu/mix-img/blob/master/docs/mixConfig.md)\n- [参数调试工具](https://github.com/baidu/mix-img/blob/master/docs/tool.md)\n- [本库开发者阅读](https://github.com/baidu/mix-img/blob/master/docs/dev.md)\n\n## ChangeLog\nPlease visit document [ChangeLog](https://github.com/baidu/mix-img/blob/master/CHANGELOG.md)\n"
  },
  {
    "path": "babel.config.js",
    "content": "/**\n * @file: babel配置文件\n * @author: zhw\n * @Date: 2021-01-09 14:16:42\n * @Last Modified by: zhw\n * @Last Modified time: 2021-05-31 22:08:36\n */\nmodule.exports = function (api) {\n    api.cache(true);\n    const presets = [];\n    const plugins = [\n        '@babel/plugin-proposal-optional-chaining',\n        '@babel/plugin-proposal-async-generator-functions',\n        ['@babel/plugin-proposal-pipeline-operator', {proposal: 'smart'}]\n    ];\n    // 打包umd模块包含完备的polyfill\n    if (process.env.NODE_ENV === 'build:umd') {\n        presets.push([\n            '@babel/preset-env',\n            {\n                useBuiltIns: 'usage',\n                corejs: 3\n            }\n        ]);\n    }\n    // 单测需要转一下es modules至commonjs\n    if (process.env.NODE_ENV === 'test') {\n        plugins.push('@babel/plugin-transform-modules-commonjs');\n    }\n    return {\n        presets,\n        plugins\n    };\n};\n"
  },
  {
    "path": "ci.yml",
    "content": ""
  },
  {
    "path": "docs/dev.md",
    "content": "## 开发\n```\n# 从本项目中fork代码后，将代码clone到本地。\n\n# 安装依赖\nnpm i\n# 运行项目\nnpm run dev\n# 构建成功后会自动打开浏览器，访问本地IP:8899/test.html，进行调试。\n```\n## 测试\n```\n# 自动化单元测试\nnpm run test\n\n# e2e测试, 同dev开发\nnpm run test:e2e\n```\n"
  },
  {
    "path": "docs/mixConfig.md",
    "content": "## mixConfig 图片合成参数\n\n### 参数说明\n#### mixConfig 参数说明\n|参数名|类型|必填|说明|备注|\n|---|---|---|---|---|\n|replaceText|Object|否|替换字段配置|需要替换的属性设置为 `{变量}` <br>支持替换的内容：动态配置中文本的 text、动态配置中图片的 imgUrl、二维码配置的 text|\n|base|Object|是|基本配置|具体配置参见 base 参数说明|\n|qrCode|Object|否|二维码配置|具体配置参见 qrCode 参数说明|\n|dynamic|Array.&lt;object&gt;|否|动态配置|具体配置参见 dynamic 参数说明|\n|dev|Object|否|开发配置|具体配置参见 dev 参数说明|\n\n#### base 参数说明\n\n|参数名|类型|必填|默认值|说明|备注|\n|---|---|---|---|---|---|\n|backgroundImg|String|否|-|合成图片的背景图，支持 url 或 base64<br>若不传，fileType 为 jpeg 时背景为黑色，为 png 时背景透明|图片过大影响性能，建议压缩至 100k 以下|\n|width|Number|是|300|合成图片的宽度|单位 px|\n|height|Number|是|300|合成图片的高度|单位 px|\n|quality|Number|否|0.8|生成图片的质量，取值范围为 (0, 1]|建议设置为0.9及以下|\n|fileType|String|否|jpeg|生成图片的数据类型，值为 jpeg 或 png|-|\n|dataType|String|否|base64|最终返回的数据格式，值为 base64 或 canvas|默认返回 base64 字符；若指定为 canvas，则返回绘制完毕的 canvas 对象|\n|loadingTimeout|Number|否|5000|加载图片响应超时时间|单位 ms，背景图、动态图片超过该时间未加载完毕则返回超时信息|\n\n#### qrCode 参数说明\n\n|参数名|类型|必填|默认值|说明|备注|\n|---|---|---|---|---|---|\n|width|Number|是|70|生成二维码宽度|单位 px|\n|height|Number|是|70|生成二维码高度|单位 px|\n|text|String|是|-|要转换成二维码的文本|支持变量，如：`{qrcodeUrl}`|\n|x|Number|是|0|横坐标信息|起始点为背景图片左上角|\n|y|Number|是|0|纵坐标信息|起始点为背景图片左上角|\n|background|String|否|#ffffff|二维码背景色|-|\n|foreground|String|否|#000000|二维码前景色|-|\n|correctLevel|Number|否|1|容错级别，值为 1、0、3、2|对应容错率为：L (7%)、M (15%)、Q (25%)、H (30%)<br>级别越高，二维码图片允许遮挡的部分越多，二维码信息越复杂|\n\n#### dynamic 参数说明\n\n* 动态信息为图片时：type = 1\n\n|参数名|类型|必填|默认值|说明|备注|\n|---|---|---|---|---|---|\n|type|Number|是|-|动态信息类型，1 为图片、2 为文字||\n|imgUrl|String|是|-|图片地址，支持 url 或 base64|支持变量，如：`{avatarUrl}`|\n|size|Object|是|-|-|-|\n|size.dWidth|Number|是|图片实际宽度|绘制图片的宽度|单位 px|\n|size.dHeight|Number|是|图片实际高度|绘制图片的高度|单位 px|\n|position|Object|是|-|-|-|\n|position.x|Number|是|0|横坐标信息|起始点为背景图片左上角|\n|position.y|Number|是|0|纵坐标信息|起始点为背景图片左上角|\n|isRound|Boolean|否|false|是否绘制成圆形|-|\n|weight|Number|否|0|绘制权重|权重越大，绘制越晚，层级越高|\n\n* 动态信息为文字时：type = 2\n\n|参数名|类型|必填|默认值|说明|备注|\n|---|---|---|---|---|---|\n|type|Number|是|-|动态信息类型，1 为图片、2 为文字|-|\n|text|String|是|-|文字内容|支持变量，如：`{userName}`|\n|style|Object|是|-|-|-|\n|style.color|String|是|#000000|文字颜色|-|\n|style.fontSize|Number|是|20|文字大小|单位px|\n|style.fontWeight|String|否|normal|文字粗细，值为 normal、bold、lighter|-|\n|style.fontFamily|String|否|PingFang SC / Roboto|文字字体|IOS 为`PingFang SC`，Android 为`Roboto`|\n|style.textAlign|String|是|left|文字水平方向对齐方式，值为 left、center、right 等|[textAlign 文档](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/textAlign)|\n|style.textBaseline|String|是|alphabetic|文字垂直方向对齐方式|[textBaseline 文档](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/textBaseline)|\n|position|Object|是|-|-|-|\n|position.x|Number|是|0|横坐标信息|起始点为背景图片左上角|\n|position.y|Number|是|0|纵坐标信息|起始点为背景图片左上角|\n|weight|Number|否|0|绘制权重|权重越大，绘制越晚，层级越高|\n\n#### dev 参数说明\n|参数名|类型|必填|默认值|说明|备注|\n|---|---|---|---|---|---|\n|notUseCache|Boolean|否|false|是否禁用缓存|默认启用缓存策略|\n> 缓存策略：当 dataType 为默认值返回 base64 时，会使用当前传入配置 mixConfig 的 md5 值做键名，缓存合成后的 base64 字符（仅缓存最新的两条）。当用户传入相同配置项时，会从缓存中直接读取 base64 字符。\n\n\n### 注意事项\n#### 1. 动态元素 weight 属性\nweight 属性可以控制动态元素的绘制层级，当动态配置中两个元素存在覆盖关系时，上方元素的 weight 属性需要设置更大的值。若 weight 属性值相同，则绘制层级随机。\n- 图片示例\n![图片](https://efe-h2.cdn.bcebos.com/ceug/resource/res/2021-03/1615882302636/q7eb8uj36ww2.png)\n\n\n#### 2. 文字使用自定义字体\n>【注意】请确认使用的自定义字体已获得授权，增强法律意识，避免字体侵权行为。\n\na. 声明自定义字体\n- css\n```css\n@font-face {\n    font-family: 'myFont';\n    src: url(\"https://efe-h2.cdn.bcebos.com/ceug/resource/res/2021-1/1611891166782/c4964f1209aa.ttf\") format('truetype');\n}\n```\n\nb. 预下载自定义字体\n\n通常情况下，当页面元素用到了 font-face 中定义的字体，则会执行下载。\n- css\n```css\n#font-loaded {\n    font-size: 0;\n    font-family: 'myFont', sans-serif;\n}\n```\n\n- html\n```html\n<div id=\"font-loaded\">.</div>\n```\n\nc. 动态配置中 fontFamily 设置为自定义字体\n\n- 动态配置\n```\n'dynamic': [\n    {\n        'type': 2,\n        'position': {\n            'x': 187,\n            'y': 353\n        },\n        'style': {\n            'fontFamily': 'myFont'\n            'fontSize': 22,\n            'color': '#ffebc0',\n            'textAlign': 'center'\n        },\n        'text': '『自定义字体abc123』'\n    }\n]\n```\n\n> 在绘制文字前，使用了 [FontFaceSet.load()](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/load) 方法等待自定义字体下载完毕再进行后续操作。由于该API存在兼容性问题，若当前环境不可用，且绘制文字时找不到已加载的自定义字体，本次将使用默认字体进行绘制，同时触发自定义字体的加载。\n"
  },
  {
    "path": "docs/mixImg.md",
    "content": "## mixImg 图片合成方法\n\n### 使用\n```js\nimport {mixImg} from 'mix-img';\nimport {mixConfig} from './mixConfig';  // 配置文件路径自定义\nasync function getImg() {\n    const res = await mixImg(mixConfig);\n    console.log('图片合成结束', res);\n}\n```\n\n### 参数示例\n```js\nexport const mixConfig = {\n    'replaceText': {\n        'submitName': '朱雀号',\n        'userName': '百度网友123',\n        'avatarUrl': 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-07/1594717976441/idyexeq1u92w.png',\n        'qrCodeUrl': 'https://www.baidu.com'\n    },\n    'base': {\n        'backgroundImg': 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-07/1594797097021/ml9v716tnxoc.jpg',\n        'width': 375,\n        'height': 667,\n        'quality': 0.8,\n        'fileType': 'jpeg'\n    },\n    'qrCode': {\n        'width': 74,\n        'height': 74,\n        'text': '{qrCodeUrl}',\n        'x': 279,\n        'y': 576,\n        'correctLevel': 1\n    },\n    'dynamic': [\n        {\n            'type': 2,\n            'position': {\n                'x': 187,\n                'y': 353\n            },\n            'style': {\n                'fontSize': 22,\n                'color': '#ffebc0',\n                'textAlign': 'center',\n                'fontWeight': 'bold'\n            },\n            'text': '『{submitName}』'\n        },\n        {\n            'type': 1,\n            'position': {\n                'x': 169,\n                'y': 207\n            },\n            'size': {\n                'dWidth': 40,\n                'dHeight': 40\n            },\n            'imgUrl': '{avatarUrl}',\n            'isRound': true\n        },\n        {\n            'type': 2,\n            'position': {\n                'x': 187,\n                'y': 268\n            },\n            'style': {\n                'textAlign': 'center',\n                'fontSize': 16,\n                'color': '#ffebc0',\n                'fontWeight': 'normal'\n            },\n            'text': '{userName}'\n        }\n    ]\n};\n```\n\n> 参数含义可参见 [mixConfig参数说明文档](https://github.com/baidu/mix-img/blob/master/docs/mixConfig.md)\n\n### 返回数据\n\n#### 合成成功\n\n1.dataType 为 base64\n\n- 返回参数说明\n\n| 参数 | 类型 | 说明 |\n| ------ | ------ | ------ |\n| errno | Number | 错误码，合成成功时为 0 |\n| data | Object | 数据对象 |\n| data.base64 | String | 图片的 base64 字符 |\n\n- 返回示例\n```json5\n{\n    errno: 0,\n    data: {\n        base64: 'data:image/jpeg;base64,000'\n    }\n}\n```\n2.dataType 为 draw\n- 返回参数说明\n\n| 参数 | 类型 | 说明 |\n| ------ | ------ | ------ |\n| errno | Number | 错误码，合成成功时为 0 |\n| data | Object | 数据对象 |\n| data.canvas | Object | 绘制完成的 canvas 对象 |\n\n- 返回示例\n```json5\n{\n    errno: 0,\n    data: {\n        canvas: '<canvas id=\"_mixImgCanvas\"></canvas>'  // 绘制完成的 canvas 对象\n    }\n}\n```\n\n#### 合成失败\n- 返回参数说明\n\n| 参数 | 类型 | 说明 |\n| ------ | ------ | ------ |\n| errno | Number | 错误码 |\n| errmsg | String | 错误描述 |\n| err | Object / String | 错误信息 |\n\n- 返回示例\n```json5\n{\n    errno: 90002,\n    errmsg: '[mix img err] 创建img标签超时！',\n    err: 'img response time more than 5000 ms'\n}\n```\n"
  },
  {
    "path": "docs/tool.md",
    "content": "## 调试工具\n### 产生背景\n图片合成的配置项包含 base（基本配置）、replaceText（替换字段配置）、qrCode（二维码配置）、dynamic（动态元素配置）四大项。 其中动态元素配置更是会有很多的情况，调试配置参数很困难。为了减少开发人员工作量，内置了参数调试工具。用户可以在平台内更改参数，预览合成图片效果。调试完毕后，复制最终配置到项目中使用。\n\n### 如何启动\n```\n# 将本库代码clone到本地\n# 安装依赖\nnpm i\n# 启动配置调试工具\nnpm run tool\n```\n\n### 工具界面\n![图片](https://efe-h2.cdn.bcebos.com/ceug/resource/res/2021-05/1620978160538/b0006kpaixoi.png)\n\n### 使用步骤\n1. 修改 JSON 配置\n2. 点击「生成预览」按钮，进行预览\n3. 参数调试完毕，点击「复制配置」按钮\n"
  },
  {
    "path": "jest.config.js",
    "content": "/**\n * @file: 测试配置文件\n * @author: zhw\n * @Date: 2020-06-30 13:45:06\n * @Last Modified by: zhw\n * @Last Modified time: 2021-01-09 16:44:09\n */\n\nmodule.exports = {\n    setupFiles: ['jest-canvas-mock'],\n    rootDir: __dirname,\n    testMatch: ['<rootDir>/test/unit/*.test.js']\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"mix-img\",\n    \"version\": \"1.0.6\",\n    \"description\": \"A fast mix image javascript tool libary\",\n    \"module\": \"dist/index.min.js\",\n    \"browser\": \"dist/umd/index.umd.js\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"scripts\": {\n        \"dev\": \"npm run test:e2e\",\n        \"lint\": \"eslint  src/** --ignore-path .gitignore\",\n        \"build\": \"rollup -c scripts/rollup.config.js\",\n        \"build:umd\": \"cross-env NODE_ENV=build:umd  rollup -c scripts/rollup.umd.config.js\",\n        \"test\": \"cross-env NODE_ENV=test  jest --coverage\",\n        \"test:e2e\": \"rollup -c scripts/rollup.e2e.config.js -w\",\n        \"prepublishOnly\": \"npm run build && npm run build:umd\",\n        \"tool\": \"sh ./scripts/tool.sh\"\n    },\n    \"husky\": {\n        \"hooks\": {\n            \"pre-commit\": \"lint-staged\"\n        }\n    },\n    \"lint-staged\": {\n        \"linters\": {\n            \"*.js\": [\n                \"eslint\"\n            ]\n        }\n    },\n    \"author\": \"zhenghaiwang\",\n    \"devDependencies\": {\n        \"@babel/core\": \"^7.11.1\",\n        \"@babel/eslint-parser\": \"^7.13.4\",\n        \"@babel/eslint-plugin\": \"^7.12.1\",\n        \"@babel/plugin-proposal-async-generator-functions\": \"^7.12.1\",\n        \"@babel/plugin-proposal-optional-chaining\": \"^7.12.7\",\n        \"@babel/plugin-proposal-pipeline-operator\": \"^7.5.0\",\n        \"@babel/plugin-transform-modules-commonjs\": \"^7.13.0\",\n        \"@babel/preset-env\": \"^7.11.0\",\n        \"@ecomfe/eslint-config\": \"^7.0.0\",\n        \"@ecomfe/stylelint-config\": \"^1.1.1\",\n        \"babel-eslint\": \"^11.0.0-beta.0\",\n        \"core-js\": \"^3.6.5\",\n        \"cross-env\": \"^5.2.0\",\n        \"eslint\": \"^7.17.0\",\n        \"husky\": \"^4.3.7\",\n        \"internal-ip\": \"^6.2.0\",\n        \"jest\": \"^26.6.3\",\n        \"jest-canvas-mock\": \"^2.3.0\",\n        \"lint-staged\": \"^8.1.0\",\n        \"rollup\": \"^2.39.1\",\n        \"rollup-plugin-babel\": \"^4.4.0\",\n        \"rollup-plugin-commonjs\": \"^10.1.0\",\n        \"rollup-plugin-node-resolve\": \"^5.2.0\",\n        \"rollup-plugin-serve\": \"^1.1.0\",\n        \"rollup-plugin-terser\": \"^7.0.0\",\n        \"san\": \"^3.10.1\",\n        \"stylelint\": \"^13.13.1\"\n    },\n    \"engine\": {\n        \"node\": \">= 8\"\n    },\n    \"dependencies\": {\n        \"md5\": \"^2.3.0\",\n        \"qrcodejs2-fixes\": \"0.0.2\"\n    },\n    \"license\": \"MIT\",\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"https://github.com/baidu/mix-img.git\"\n    }\n}\n"
  },
  {
    "path": "scripts/build.sh",
    "content": "export PATH=$NODEJS_BIN_LATEST:$PATH\n\necho \"node: $(node -v)\"\necho \"npm: $(npm -v)\"\n\n\nout_dir=\"output\"\n\n# 安装依赖\nexport NODE_ENV=development\nnpm install\n\n# 跑一遍build\nexport NODE_ENV=production\nnpm run build\n\n\nmkdir -p $out_dir\nmv ./dist $out_dir\nif [ $? -eq 0 ]; then\n    echo '[publish] done'\n    exit 0\nelse\n    echo '[publish] fail'\n    exit 1\nfi\n"
  },
  {
    "path": "scripts/rollup.config.js",
    "content": "/**\n * @file: rollup配置文件\n * @author: zhw\n * @Date: 2020-07-06 16:16:49\n * @Last Modified by: zhw\n * @Last Modified time: 2021-01-09 16:27:16\n */\n// rollup.config.js\nimport babel from 'rollup-plugin-babel';\nimport {terser} from 'rollup-plugin-terser';\nexport default [\n    {\n        input: 'src/index.js',\n        output: {\n            file: './dist/index.min.js',\n            format: 'es'\n        },\n        plugins: [\n            babel({\n                runtimeHelpers: true,\n                extensions: ['.js'],\n                exclude: 'node_modules/**'\n            }),\n            terser()\n        ],\n        watch: {\n            include: 'src/**'\n        }\n    },\n    {\n        input: 'src/index.js',\n        output: {\n            file: './dist/index.js',\n            format: 'es'\n        },\n        plugins: [\n            babel({\n                runtimeHelpers: true,\n                extensions: ['.js'],\n                exclude: 'node_modules/**'\n            })\n        ],\n        watch: {\n            include: 'src/**'\n        }\n    }\n];\n\n"
  },
  {
    "path": "scripts/rollup.e2e.config.js",
    "content": "import babel from 'rollup-plugin-babel';\nimport serve from 'rollup-plugin-serve';\nimport resolve from 'rollup-plugin-node-resolve';\nimport commonjs from 'rollup-plugin-commonjs';\nconst internalIp = require('internal-ip');\nconst devHost = internalIp.v4.sync();\nexport default {\n    input: 'test/e2e/index.js',\n    output: {\n        file: 'dist/e2e/test.js',\n        format: 'iife'\n    },\n    plugins: [\n        babel({\n            include: ['src/**', 'test/**']\n        }),\n        resolve(),\n        commonjs(),\n        serve({\n            host: devHost,\n            open: true,\n            openPage: '/test.html',\n            contentBase: ['test/e2e', 'dist'],\n            port: 8899\n        })\n    ]\n};\n"
  },
  {
    "path": "scripts/rollup.umd.config.js",
    "content": "/**\n * @file: rollup umd配置文件\n * @author: haoxin\n */\n// rollup.umd.config.js\nimport babel from 'rollup-plugin-babel';\nimport resolve from 'rollup-plugin-node-resolve';\nimport commonjs from 'rollup-plugin-commonjs';\nimport {terser} from 'rollup-plugin-terser';\nexport default [\n    {\n        input: 'src/index.js',\n        output: {\n            file: './dist/umd/index.umd.js',\n            format: 'umd',\n            name: 'mixImg'\n        },\n        plugins: [\n            babel({\n                runtimeHelpers: true,\n                extensions: ['.js'],\n                exclude: 'node_modules/**'\n            }),\n            resolve(),\n            commonjs(),\n            terser()\n        ],\n        watch: {\n            include: 'src/**'\n        }\n    }\n];\n\n"
  },
  {
    "path": "scripts/tool.sh",
    "content": "cd toolkit || exit\n\nnpm i\n\nnpm run start\n"
  },
  {
    "path": "src/config/errorMap.js",
    "content": "/**\n * @file errorMap\n * @author haoxin\n */\n\nexport const errorMap = {\n    CREATE_CANVAS_ERROR: {errno: 10001, errmsg: '[mix img err] 创建canvas标签出错！'},\n    ADD_BG_ERROR: {errno: 20001, errmsg: '[mix img err] 绘制背景图出错！'},\n    ADD_DYNAMIC_ERROR: {errno: 30001, errmsg: '[mix img err] 添加动态元素错误！'},\n    ADD_TEXT_ERROR: {errno: 300011, errmsg: '[mix img err] 添加文字错误！'},\n    ADD_IMG_ERROR: {errno: 300012, errmsg: '[mix img err] 添加图片错误！'},\n    ADD_QRCODE_ERROR: {errno: 30002, errmsg: '[mix img err] 添加二维码错误！'},\n    TO_BASE64_ERROR: {errno: 40001, errmsg: '[mix img err] canvas转base64出错！'},\n    CREATE_IMG_ERROR: {errno: 90001, errmsg: '[mix img err] 创建img标签出错！请排查该图片是否跨域'},\n    CREATE_IMG_TIMEOUT: {errno: 90002, errmsg: '[mix img err] 创建img标签超时！'}\n};\n"
  },
  {
    "path": "src/index.js",
    "content": "/**\n * @file: 图片合成方法\n * @author: haoxin\n */\n\nimport {\n    addImgToCanvas,\n    addTextToCanvas,\n    addQrCodeToCanvas,\n    createCanvas,\n    canvasToBase64\n} from './utils/canvasUtils';\nimport {createImg} from './utils/createImg';\nimport {errorMap} from './config/errorMap';\nimport {splitArr} from './utils/tools';\nimport md5 from 'md5';\n\nlet hash = '';\n\n/**\n * 绘制背景图\n * @param {Object} config 总配置项\n * @return {Promise<Object>} config 总配置项\n */\nexport const addBackgroundImg = async config => {\n    try {\n        const {base, ctx} = config;\n        const width = base.width || 300;\n        const height = base.height || 300;\n        if (base.backgroundImg) {\n            const img = await createImg(base.backgroundImg, base.loadingTimeout);\n            ctx.drawImage(img, 0, 0, width, height);\n        }\n        return config;\n    }\n    catch (err) {\n        return Promise.reject(Object.assign({}, errorMap.ADD_BG_ERROR, {err}));\n    }\n};\n\n/**\n * 添加动态元素\n * @param {Object} config 总配置项\n * @return {Promise<Object>} config 总配置项\n */\nexport const addDynamicElementToCanvas = async config => {\n    try {\n        const {ctx, dynamic = [], replaceText} = config;\n        const timeout = config.base.loadingTimeout;\n\n        // 动态配置按weight属性分组\n        let weightConfig = splitArr(dynamic, 'weight', 0);\n        let weightKeys = Object.keys(weightConfig);\n        weightKeys.sort(function (a, b) {\n            return a - b;\n        });\n\n        // 分组绘制动态元素\n        for (let item of weightKeys) {\n            let dynamicPromises = [];\n            let currWeightConfig = weightConfig[item];\n            for (let i = 0; i < currWeightConfig.length; i++) {\n                if (currWeightConfig[i].type === 1) {\n                    dynamicPromises.push(addImgToCanvas(ctx, currWeightConfig[i], replaceText, timeout));\n                }\n                else {\n                    dynamicPromises.push(addTextToCanvas(ctx, currWeightConfig[i], replaceText));\n                }\n            }\n            await Promise.all(dynamicPromises);\n        }\n        return config;\n    }\n    catch (err) {\n        return Promise.reject(Object.assign({}, errorMap.ADD_DYNAMIC_ERROR, {err}));\n    }\n};\n\n/**\n * 缓存base64文件\n * @param {string} base64Img 图片base64字符\n */\nexport const cacheFile = base64Img => {\n    try {\n        let base64Queue = JSON.parse(localStorage.getItem('mix_img_base64_queue')) || [];\n        base64Queue.push(`mix_img_base64_${hash}`);\n\n        // 缓存超过2个出队 && 删除对应的item\n        if (base64Queue.length > 2) {\n            localStorage.removeItem(base64Queue.shift());\n        }\n\n        localStorage.setItem('mix_img_base64_queue', JSON.stringify(base64Queue));\n        localStorage.setItem(`mix_img_base64_${hash}`, base64Img);\n\n    }\n    catch (e) {\n        console.log(`[mix img log] ${e}`);\n    }\n};\n\n/**\n * 生成base64图片\n * @param {Object} config 总配置项\n * @return {Promise<Object>} base64字符对象\n */\nexport const getBase64 = async config => {\n    const {canvasImg, base, dev} = config;\n    const base64Img = await canvasToBase64(canvasImg, {\n        fileType: base.fileType,\n        quality: base.quality\n    });\n    if (!dev?.notUseCache) {\n        cacheFile(base64Img);\n    }\n    return {\n        base64: base64Img\n    };\n};\n\n/**\n * 获取canvas处理流程\n * @param {Object} config 总配置项\n * @return {Promise<Object>} base64字符对象\n */\nexport const processCanvas = async config => {\n    return config\n        |> await createCanvas(#)\n        |> await addBackgroundImg(#)\n        |> await addDynamicElementToCanvas(#)\n        |> await addQrCodeToCanvas(#);\n};\n\n/**\n * 获取base64处理流程\n * @param {Object} config 总配置项\n * @return {Promise<Object>} canvas对象\n */\nexport const processBase64 = async config => {\n    hash = md5(JSON.stringify(config));\n    const localBase64Img = config.dev?.notUseCache ? ''\n        : localStorage.getItem(`mix_img_base64_${hash}`);\n    // 有缓存 直接读取 | 无缓存 重新获取\n    return localBase64Img ? {base64: localBase64Img}\n        : config\n            |> await createCanvas(#)\n            |> await addBackgroundImg(#)\n            |> await addDynamicElementToCanvas(#)\n            |> await addQrCodeToCanvas(#)\n            |> await getBase64(#);\n};\n\n/**\n * 图片合成函数\n * @param {Object} mixConfig\n * @param {string} mixConfig.base.dataType 合成类型  默认 'base64' 返回base64图片字符 | 'canvas' 返回canvas对象\n * @return {Promise<Object>} 合成结果\n */\nexport const mixImg = async mixConfig => {\n    const start = (new Date()).getTime();\n    try {\n        // 深拷贝配置项\n        let config = JSON.parse(JSON.stringify(mixConfig));\n        let data = config.base.dataType === 'canvas' ? await processCanvas(config) : await processBase64(config);\n        console.log(`[mix img time] ${(new Date().getTime() - start)} ms`);\n        return {\n            errno: 0,\n            data\n        };\n    }\n    catch (err) {\n        return err;\n    }\n};\n"
  },
  {
    "path": "src/utils/canvasUtils.js",
    "content": "/**\n * @file: 向canvas中增加元素\n * @author: haoxin\n */\n\nimport {getQrCodeImg} from './qrcode';\nimport {clientType, renameKey} from './tools';\nimport {errorMap} from '../config/errorMap';\nimport {createImg} from './createImg';\n\n/**\n * 变量替换\n * @param {string} text 带变量的原内容\n * @param {Object} replaceText 待替换的变量对象\n * @return {string} targetText 替换后的内容\n */\nexport const replaceVariable = (text, replaceText = {}) => {\n    const reg = /(.*)({(.*)})(.*)/g;\n    const matchTextArr = reg.exec(text);\n    return matchTextArr ? matchTextArr[1] + replaceText[matchTextArr[3]] + matchTextArr[4] : text;\n};\n\n/**\n * 向canvas中添加文本\n * @param {Object} ctx 上下文对象\n * @param {Object} config 文本配置\n * @param {number} config.position.x 距上距离\n * @param {number} config.position.y 距左距离\n * @param {string} config.style.color 颜色\n * @param {number} config.style.fontSize 大小\n * @param {string} config.style.fontWeight 粗细\n * @param {string} config.style.textAlign 水平对齐方式\n * @param {string} config.style.textBaseline 垂直对齐方式\n * @param {string} config.font 字体设置 优先级高于style中的配置\n * @param {Object} replaceText 替换项对象\n */\nexport const addTextToCanvas = async (ctx, config = {}, replaceText) => {\n    try {\n        if (!config.text) {\n            return;\n        }\n        // 读取字体配置\n        const fontSize = config.style?.fontSize ? `${config.style.fontSize}px` : '20px';\n        const fontWeight = config.style?.fontWeight ? `${config.style.fontWeight}` : 'normal';\n        const initFF = clientType() === 'ios' ? 'PingFang SC' : 'Roboto';\n        const fontFamily = config.style?.fontFamily ? `${config.style.fontFamily}, ${initFF}` : initFF;\n        const font = config.font ? config.font : `${fontWeight} ${fontSize} ${fontFamily}`;\n        // 等待字体加载完毕\n        try {\n            document?.fonts?.load && await document.fonts.load(font);\n        } catch (e) {\n            console.error('[Font loading failed]', e);\n        }\n        // 颜色\n        ctx.fillStyle = config.style?.color;\n        // 字体\n        ctx.font = font;\n        // 水平对齐\n        ctx.textAlign = config.style?.textAlign ? config.style.textAlign : 'left';\n        // 垂直对齐\n        ctx.textBaseline = config.style?.textBaseline ? config.style.textBaseline : 'alphabetic';\n        // 文本\n        const text = replaceVariable(config.text, replaceText);\n        ctx.fillText(text, config.position?.x || 0, config.position?.y || 0);\n    }\n    catch (err) {\n        return Promise.reject(Object.assign({}, errorMap.ADD_TEXT_ERROR, {err}));\n    }\n};\n\n/**\n * 创建圆形裁剪区\n * @param {Object} ctx 上下文对象\n * @param {number} halfWidth 图形半宽\n * @param {number} halfHeight 图形半高\n * @param {number} x 图形距上距离\n * @param {number} y 图形距左距离\n */\nexport const setClipZone = (ctx, halfWidth, halfHeight, x, y) => {\n    ctx.beginPath();\n    ctx.arc(halfWidth + x, halfHeight + y, halfWidth, 0, 2 * Math.PI);\n    ctx.clip();\n};\n\n/**\n * 向canvas中添加图片\n * @param {Object} ctx 上下文对象\n * @param {Object} config 图片配置\n * @param {number} config.position.x 距上距离\n * @param {number} config.position.y 距左距离\n * @param {number} config.size.dWidth 图片宽\n * @param {number} config.size.dHeight 图片高\n * @param {number} config.isRound 是否裁剪为圆形\n * @param {Object} replaceText 替换项对象\n * @param {number} timeout 请求图片的超时时间\n */\nexport const addImgToCanvas = async (ctx, config = {}, replaceText, timeout) => {\n    try {\n        if (!config.imgUrl) {\n            return;\n        }\n        // 传入图片为base64不进行变量替换\n        const isBase64 = config.imgUrl.startsWith('data:image');\n        const src = isBase64 ? config.imgUrl : replaceVariable(config.imgUrl, replaceText);\n        const img = await createImg(src, timeout);\n        let width = img.width;\n        let height = img.height;\n        if (config.size && config.size.dWidth && config.size.dHeight) {\n            width = config.size.dWidth;\n            height = config.size.dHeight;\n        }\n        ctx.save();\n        if (config.isRound) {\n            setClipZone(ctx, width / 2, height / 2, config.position?.x, config.position?.y);\n        }\n        ctx.drawImage(img, config.position?.x, config.position?.y, width, height);\n        ctx.restore();\n    }\n    catch (err) {\n        return Promise.reject(Object.assign({}, errorMap.ADD_IMG_ERROR, {err}));\n    }\n};\n\n/**\n * 向canvas中添加二维码\n * @param {Object} config 配置项\n * @return {Promise<Object>} config 配置项\n */\nexport const addQrCodeToCanvas = async config => {\n    try {\n        let {ctx, qrCode, replaceText} = config;\n        if (qrCode?.text && qrCode.width && qrCode.height) {\n            qrCode.text = replaceVariable(qrCode.text, replaceText);\n            qrCode = renameKey(qrCode, 'foreground', 'colorDark');\n            qrCode = renameKey(qrCode, 'background', 'colorLight');\n            const qrCodeCanvas = await getQrCodeImg(qrCode);\n            ctx.drawImage(qrCodeCanvas, qrCode.x || 0, qrCode.y || 0, qrCode.width, qrCode.height);\n        }\n        return config.base.dataType === 'canvas' ? {canvas: config.canvasImg} : config;\n    }\n    catch (err) {\n        return Promise.reject(Object.assign({}, errorMap.ADD_QRCODE_ERROR, {err}));\n    }\n};\n\n/**\n * 创建canvas标签\n * @param {Object} config 配置项\n * @return {Object} canvas对象、canvas上下文与配置项合并后的总配置项\n */\nexport const createCanvas = config => {\n    try {\n        const canvasId = '_mixImgCanvas';\n        const {width, height} = config.base;\n        let canvas = document.getElementById(canvasId);\n        if (!canvas) {\n            canvas = document.createElement('canvas');\n            canvas.id = canvasId;\n        }\n        canvas.width = width;\n        canvas.height = height;\n        const ctx = canvas.getContext('2d');\n        ctx.clearRect(0, 0, canvas.width, canvas.height);\n        return Object.assign({canvasImg: canvas, ctx}, config);\n    }\n    catch (err) {\n        return Promise.reject(Object.assign({}, errorMap.CREATE_CANVAS_ERROR, {err}));\n    }\n};\n\n/**\n * canvas转base64文件\n * @param {Object} canvas canvas对象\n * @param {Object} fileConfig 文件配置\n * @param {string} fileConfig.fileType 文件类型\n * @param {number} fileConfig.quality 文件质量 0-1\n * @return {string} base64图片的 data URI\n */\nexport const canvasToBase64 = (canvas, fileConfig = {}) => {\n    try {\n        const fileType = fileConfig.fileType ? `image/${fileConfig.fileType}` : 'image/jpeg';\n        const quality = fileConfig.quality || 0.8;\n        return canvas.toDataURL(fileType, quality);\n    }\n    catch (err) {\n        return Promise.reject(Object.assign({}, errorMap.TO_BASE64_ERROR, {err}));\n    }\n};\n\n"
  },
  {
    "path": "src/utils/createImg.js",
    "content": "/**\n * 创建启用了CORS的图片\n * @param {string} src 图片的src\n * @param {number} timeout 请求图片的超时时间\n * @return {Promise<Element>} HTMLImageElement img对象\n */\nimport {errorMap} from '../config/errorMap';\n\nexport const createImg = (src, timeout = 5000) => {\n    return new Promise((resolve, reject) => {\n        const img = new Image();\n        img.crossOrigin = 'Anonymous';\n        img.src = src;\n        setTimeout(() => {\n            reject(Object.assign({}, errorMap.CREATE_IMG_TIMEOUT, {\n                err: `img response time more than ${timeout} ms`,\n                errSrc: src\n            }));\n        }, timeout);\n        img.onload = () => {\n            resolve(img);\n        };\n        img.onerror = err => {\n            reject(Object.assign({}, errorMap.CREATE_IMG_ERROR, {err, errSrc: src}));\n        };\n    });\n};\n"
  },
  {
    "path": "src/utils/qrcode.js",
    "content": "/**\n * @file: 二维码生成\n * @author: haoxin\n */\n\nimport QRCode from 'qrcodejs2-fixes';\n\n/**\n * 获取二维码canvas对象\n * @param {Object} config 二维码配置项\n * @return {Promise<Element>} HTMLCanvasElement 绘制了二维码的canvas对象\n */\nexport const getQrCodeImg = config => {\n    return new Promise((resolve, reject) => {\n        const options = Object.assign({\n            width: 70,\n            height: 70,\n            colorDark: '#000000',\n            colorLight: '#ffffff',\n            correctLevel: QRCode.CorrectLevel.L\n        }, config, {text: ''});\n\n        const qrWrap = document.createElement('div');\n        const qrCode = new QRCode(qrWrap, options);\n        qrCode.makeCode(config.text);\n        const qrCodeCanvas = qrWrap.getElementsByTagName('canvas')[0];\n        resolve(qrCodeCanvas);\n    });\n};\n"
  },
  {
    "path": "src/utils/tools.js",
    "content": "/**\n * @file: 工具函数\n * @author: haoxin\n */\n\n/**\n * 判断客户端\n * @return {string} 宿主类型 ios | android | pc\n */\nexport const clientType = () => {\n    let client = '';\n    if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {\n        client = 'ios';\n    }\n    else if (/(Android)/i.test(navigator.userAgent)) {\n        client = 'android';\n    }\n    else {\n        client = 'pc';\n    }\n    return client;\n};\n\n/**\n * JSON对象key值重命名\n * @param {Object} object 配置项\n * @param {string} key 键名\n * @param {string} newKey 新键名\n * @return {Object} key值重命名后的对象\n */\nexport const renameKey = (object, key, newKey) => {\n    const clonedObj = Object.assign({}, object);\n    const targetKey = clonedObj[key];\n    if (targetKey && newKey) {\n        delete clonedObj[key];\n        clonedObj[newKey] = targetKey;\n    }\n    return clonedObj;\n};\n\n/**\n * 根据某属性将对象数组进行分组\n * @param {Array} array 数组对象\n * @param {string} key 键名\n * @param {number} init 分组对象键名默认值\n * @return {Object} 返回已分组的对象\n */\nexport const splitArr = (array, key, init) => {\n    return array.reduce((acc, item) => {\n        let currentVal = item[key] || init;\n        acc[currentVal] || (acc[currentVal] = []);\n        acc[currentVal].push(item);\n        return acc;\n    }, {});\n};\n"
  },
  {
    "path": "test/config/allConfig.js",
    "content": "export const mixConfig = {\n    'dev': {\n        'notUseCache': false\n    },\n    'replaceText': {\n        'submitName': '朱雀号abc123',\n        'userName': '百度网友abc123',\n        'avatarUrl': 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-07/1594717976441/idyexeq1u92w.png',\n        'qrCodeUrl': 'https://www.baidu.com'\n    },\n    'base': {\n        'backgroundImg': 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-07/1594797097021/ml9v716tnxoc.jpg',\n        'width': 375,\n        'height': 667,\n        'quality': 0.8,\n        'fileType': 'jpeg',\n        'loadingTimeout': 3000,\n        'dataType': 'base64'\n    },\n    'qrCode': {\n        'width': 74,\n        'height': 74,\n        'text': '{qrCodeUrl}',\n        'x': 279,\n        'y': 576,\n        'correctLevel': 1\n    },\n    'dynamic': [\n        {\n            'type': 2,\n            'position': {\n                'x': 187,\n                'y': 353\n            },\n            'style': {\n                'fontSize': 20,\n                'color': '#ffebc0',\n                'textAlign': 'center',\n                'fontWeight': 'bold',\n                'fontFamily': 'myFont'\n            },\n            'text': '『{submitName}』'\n        },\n        {\n            'type': 2,\n            'position': {\n                'x': 187,\n                'y': 254\n            },\n            'style': {\n                'textAlign': 'center',\n                'fontSize': 26,\n                'color': '#ff0000',\n                'fontWeight': 'normal',\n                'fontFamily': 'myFont2'\n            },\n            'text': '{userName}',\n            'weight': 1\n        },\n        {\n            'type': 2,\n            'position': {\n                'x': 187,\n                'y': 204\n            },\n            'style': {\n                'textAlign': 'center',\n                'fontSize': 26,\n                'color': '#ffff00',\n                'fontWeight': 'normal'\n            },\n            'text': '{userName}'\n        },\n        {\n            'type': 1,\n            'position': {\n                'x': 169,\n                'y': 207\n            },\n            'size': {\n                'dWidth': 40,\n                'dHeight': 40\n            },\n            'imgUrl': '{avatarUrl}',\n            'isRound': true\n        }\n    ]\n};\n\nexport const base64Config = Object.assign({}, JSON.parse(JSON.stringify(mixConfig)));\nexport const canvasConfig = Object.assign({}, JSON.parse(JSON.stringify(mixConfig)));\ncanvasConfig.base.dataType = 'canvas';\n"
  },
  {
    "path": "test/config/index.js",
    "content": "/**\n * @file: 配置\n * @author: zhw\n * @Date: 2020-06-30 13:45:06\n * @Last Modified by: zhw\n * @Last Modified time: 2020-12-13 17:44:24\n */\n\nexport const addTextToCanvasConfig = {\n    type: 2,\n    position: {\n        x: 187,\n        y: 200\n    },\n    style: {\n        fontSize: 34,\n        color: '#ffebc0',\n        textAlign: 'center',\n        fontWeight: 'normal'\n    },\n    text: '名字叫小芳'\n};\n\nexport const addTextToCanvasConfigWithReplaceText = {\n    type: 2,\n    position: {\n        x: 187,\n        y: 200\n    },\n    style: {\n        fontSize: 34,\n        color: '#ffebc0',\n        textAlign: 'center',\n        fontWeight: 'normal'\n    },\n    text: '名字叫{name}',\n    replaceText: {\n        name: '小明'\n    }\n};\n\nexport const addImgToCanvasConfig = {\n    type: 1,\n    imgUrl: 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-09/1599455753277/pu26hccjgzoj.png',\n    position: {\n        x: 39,\n        y: 200\n    },\n    size: {\n        dWidth: 150,\n        dHeight: 50\n    }\n};\n\nexport const dynamicConfig = [addTextToCanvasConfig, addImgToCanvasConfig];\n\nexport const backgroundImgConfig = {\n    backgroundImg: 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-07/1594797097021/ml9v716tnxoc.jpg',\n    width: 375,\n    height: 667,\n    quality: 0.8,\n    fileType: 'jpeg'\n};\n\nexport const qrCodeConfig = {\n    width: 80,\n    height: 80,\n    text: 'https://www.baidu.com',\n    x: 275,\n    y: 573,\n    background: '#cccccc',\n    foreground: '#000d54',\n    correctLevel: 1\n};\n\nexport const userAgentMap = {\n    pc: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',\n    android: 'Mozilla/5.0 (Linux; Android 10; YAL-AL10 Build/HUAWEIYAL-AL10; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/76.0.3809.89 Mobile Safari/537.36 T7/12.5 SP-engine/2.26.0 baiduboxapp/12.5.1.10 (Baidu; P1 10) NABar/1.0',\n    ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) BaiduBoxApp/12.5.0 Mobile/18B92 Safari/602.1 SP-engine/2.26.0 main%2F1.0 baiduboxapp/12.5.0.11 (Baidu; P2 14.2) NABar/1.0 webCore=0x12c6b1320'\n};\n"
  },
  {
    "path": "test/e2e/index.js",
    "content": "/**\n * @file: e2e入口文件\n * @author: zhw\n * @Date: 2021-01-09 13:34:21\n * @Last Modified by: zhw\n * @Last Modified time: 2021-05-31 22:08:08\n */\nimport san from 'san';\nimport {mixImg} from '../../src/index';\nimport {canvasConfig, base64Config} from '../config/allConfig';\n\nconst App = san.defineComponent({\n    template: ` <div class=\"wrap\">\n                    <div>合成图片测试</div>\n                    <div class=\"mi-btn\" on-click=\"getImg\">合成图片</div>\n                    <div id=\"show-img-wrap\"></div>\n                    <div id=\"font-load-one\">.</div>\n                    <div id=\"font-load-two\">.</div>\n                </div>`,\n    async getImg() {\n        const res = await mixImg(canvasConfig);\n        // 返回了canvas则置入页面\n        if (res.errno === 0 && res.data.canvas) {\n            document.getElementById('show-img-wrap').appendChild(res.data.canvas);\n        }\n        console.log('图片合成结束~~', res);\n    }\n});\n\nnew App().attach(document.body);\n"
  },
  {
    "path": "test/e2e/test.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>合成图片测试</title>\n    <style>\n        @font-face {\n            font-family: 'myFont';\n            src: url(\"https://efe-h2.cdn.bcebos.com/ceug/resource/res/2021-1/1611891166782/c4964f1209aa.ttf\") format('truetype');\n        }\n\n        @font-face {\n            font-family: 'myFont2';\n            src: url(\"https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-8/1597241411459/ba2b12610e62.ttf\") format('truetype');\n        }\n\n        html {\n            line-height: 1.15;\n            font-size: 5vw;\n        }\n\n        .wrap {\n            display: flex;\n            flex-direction: column;\n            justify-content: center;\n            align-items: center;\n            width: 100vw;\n        }\n\n        .mi-btn {\n            width: 100vw;\n            height: 100px;\n            line-height: 100px;\n            text-align: center;\n            background: #ccc;\n        }\n\n        #font-load-one {\n            font-size: 0;\n            font-family: 'myFont', serif;\n        }\n\n        #font-load-two {\n            font-size: 0;\n            font-family: 'myFont2', serif;\n        }\n    </style>\n</head>\n<body>\n<script src=\"/e2e/test.js\"></script>\n<script src=\"https://cdn.bootcdn.net/ajax/libs/vConsole/1.0.1/vconsole.min.js\"></script>\n<script>\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "test/unit/__snapshots__/canvasUtils.test.js.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`向canvas中添加二维码 绘制二维码 1`] = `\nArray [\n  Object {\n    \"props\": Object {\n      \"dHeight\": 150,\n      \"dWidth\": 300,\n      \"dx\": 275,\n      \"dy\": 573,\n      \"img\": <canvas />,\n      \"sHeight\": 150,\n      \"sWidth\": 300,\n      \"sx\": 0,\n      \"sy\": 0,\n    },\n    \"transform\": Array [\n      1,\n      0,\n      0,\n      1,\n      0,\n      0,\n    ],\n    \"type\": \"drawImage\",\n  },\n]\n`;\n\nexports[`向canvas中添加图片 添加图片 1`] = `\nArray [\n  Object {\n    \"props\": Object {\n      \"dHeight\": 0,\n      \"dWidth\": 0,\n      \"dx\": 39,\n      \"dy\": 200,\n      \"img\": <img\n        src=\"https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-09/1599455753277/pu26hccjgzoj.png\"\n      />,\n      \"sHeight\": 0,\n      \"sWidth\": 0,\n      \"sx\": 0,\n      \"sy\": 0,\n    },\n    \"transform\": Array [\n      1,\n      0,\n      0,\n      1,\n      0,\n      0,\n    ],\n    \"type\": \"drawImage\",\n  },\n]\n`;\n\nexports[`向canvas中添加文本 没有替换字段的文本 1`] = `\nArray [\n  Object {\n    \"props\": Object {\n      \"value\": \"#ffebc0\",\n    },\n    \"transform\": Array [\n      1,\n      0,\n      0,\n      1,\n      0,\n      0,\n    ],\n    \"type\": \"fillStyle\",\n  },\n  Object {\n    \"props\": Object {\n      \"value\": \"34px Roboto\",\n    },\n    \"transform\": Array [\n      1,\n      0,\n      0,\n      1,\n      0,\n      0,\n    ],\n    \"type\": \"font\",\n  },\n  Object {\n    \"props\": Object {\n      \"value\": \"center\",\n    },\n    \"transform\": Array [\n      1,\n      0,\n      0,\n      1,\n      0,\n      0,\n    ],\n    \"type\": \"textAlign\",\n  },\n  Object {\n    \"props\": Object {\n      \"value\": \"alphabetic\",\n    },\n    \"transform\": Array [\n      1,\n      0,\n      0,\n      1,\n      0,\n      0,\n    ],\n    \"type\": \"textBaseline\",\n  },\n  Object {\n    \"props\": Object {\n      \"maxWidth\": null,\n      \"text\": \"名字叫小芳\",\n      \"x\": 187,\n      \"y\": 200,\n    },\n    \"transform\": Array [\n      1,\n      0,\n      0,\n      1,\n      0,\n      0,\n    ],\n    \"type\": \"fillText\",\n  },\n]\n`;\n\nexports[`设置clipZone 添加图片 1`] = `Array []`;\n"
  },
  {
    "path": "test/unit/canvasUtils.test.js",
    "content": "/**\n * @file: 测试h5\n * @author: zhw\n * @Date: 2020-06-30 13:45:06\n * @Last Modified by: zhw\n * @Last Modified time: 2021-01-09 16:48:33\n */\n\nlet mockImg = {};\nlet mockCanvas = {};\n\n// createImg方法调用了onload, 但jest-canvas-mock暂时还没找到可以开启这个方法的地方，故mock\njest.mock('../../src/utils/createImg', () => {\n    return {\n        createImg: jest.fn(() => {\n            const img = global.document.createElement('img');\n            img.src = 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-09/1599455753277/pu26hccjgzoj.png';\n            mockImg = img;\n            return img;\n        }),\n        __esModule: true\n    };\n});\n\n// 生成二维码canvas引用外部库，故mock\njest.mock('../../src/utils/qrcode', () => {\n    return {\n        getQrCodeImg: jest.fn(() => {\n            const qrcodeCanvas = global.document.createElement('canvas');\n            mockCanvas = qrcodeCanvas;\n            return qrcodeCanvas;\n        }),\n        __esModule: true\n    };\n});\n\nimport {\n    replaceVariable,\n    addTextToCanvas,\n    addImgToCanvas,\n    setClipZone,\n    addQrCodeToCanvas\n} from '../../src/utils/canvasUtils';\nimport {\n    addTextToCanvasConfig,\n    addTextToCanvasConfigWithReplaceText,\n    addImgToCanvasConfig,\n    qrCodeConfig\n} from '../config';\n\n// const canvas = document.createElement('canvas');\n// const events = canvas.__getEvents();\ndescribe('replaceVariable替换方法', () => {\n    it('replaceVariable变量替换返回是否正常', () => {\n        const text = replaceVariable('{prize}元奖金', {prize: 100});\n        expect(text).toBe('100元奖金');\n    });\n    it('replaceVariable传入空和null返回是否正常', () => {\n        const text = replaceVariable('', null);\n        expect(text).toBe('');\n    });\n    it('replaceVariable仅传了变量文本返回是否正常', () => {\n        const text = replaceVariable('{prize}元奖金');\n        expect(text).toBe('undefined元奖金');\n    });\n});\n\ndescribe('向canvas中添加文本', () => {\n    Object.defineProperty(document, 'fonts', {\n        writable: true,\n        value: {\n            load: jest.fn().mockImplementation(() => Promise.resolve())\n        }\n    });\n    it('没有替换字段的文本', async () => {\n        const canvas = document.createElement('canvas').getContext('2d');\n        await addTextToCanvas(canvas, addTextToCanvasConfig);\n        const events = canvas.__getEvents();\n        expect(events).toMatchSnapshot();\n    });\n    it('有替换字段的文本', async () => {\n        const canvas = document.createElement('canvas').getContext('2d');\n        await addTextToCanvas(\n            canvas, addTextToCanvasConfigWithReplaceText, addTextToCanvasConfigWithReplaceText.replaceText\n        );\n        expect(canvas.fillText).toBeCalledWith('名字叫小明', 187, 200);\n    });\n});\n\ndescribe('向canvas中添加图片', () => {\n    it('添加图片', async () => {\n        const canvas = document.createElement('canvas').getContext('2d');\n        await addImgToCanvas(canvas, addImgToCanvasConfig);\n        const calls = canvas.__getDrawCalls();\n        expect(calls).toMatchSnapshot();\n        expect(canvas.drawImage).toBeCalledWith(mockImg, 39, 200, 150, 50);\n    });\n});\n\ndescribe('设置clipZone', () => {\n    it('添加图片', () => {\n        const canvas = document.createElement('canvas').getContext('2d');\n        setClipZone(canvas, 4, 4, 80, 80);\n        const calls = canvas.__getDrawCalls();\n        expect(calls).toMatchSnapshot();\n        expect(canvas.arc).toBeCalledWith(84, 84, 4, 0, 6.283185307179586);\n    });\n});\n\ndescribe('向canvas中添加二维码', () => {\n    it('绘制二维码', async () => {\n        const canvas = document.createElement('canvas').getContext('2d');\n        await addQrCodeToCanvas({ctx: canvas, qrCode: qrCodeConfig, base: {}});\n        const calls = canvas.__getDrawCalls();\n        expect(calls).toMatchSnapshot();\n        expect(canvas.drawImage).toBeCalledWith(mockCanvas, 275, 573, 80, 80);\n    });\n});\n"
  },
  {
    "path": "test/unit/index.test.js",
    "content": "/**\n * @file: 测试index文件\n * @author: haoxin\n */\n\n// createImg方法调用了onload, 但jest-canvas-mock暂时还没找到可以开启这个方法的地方，故mock\njest.mock('../../src/utils/createImg', () => {\n    return {\n        createImg: jest.fn(() => {\n            const img = global.document.createElement('img');\n            img.src = '';\n            return img;\n        }),\n        __esModule: true\n    };\n});\n\nimport {\n    addBackgroundImg,\n    addDynamicElementToCanvas,\n    getBase64,\n    mixImg\n} from '../../src/index';\n// config\nimport {backgroundImgConfig, dynamic} from '../config';\nimport {base64Config, canvasConfig} from '../config/allConfig';\n\ndescribe('绘制背景图', () => {\n    it('绘制背景图成功', async () => {\n        const canvas = document.createElement('canvas').getContext('2d');\n        const data = await addBackgroundImg({ctx: canvas, base: backgroundImgConfig});\n        expect(data).toHaveProperty('base');\n    });\n    it('绘制背景图失败', async () => {\n        expect.assertions(1);\n        try {\n            await addBackgroundImg();\n        }\n        catch (e) {\n            expect(e).toHaveProperty('errno', 20001);\n        }\n    });\n});\n\ndescribe('添加动态元素', () => {\n    Object.defineProperty(document, 'fonts', {\n        writable: true,\n        value: {\n            load: jest.fn().mockImplementation(() => Promise.resolve())\n        }\n    });\n    it('添加动态元素成功', async () => {\n        const canvas = document.createElement('canvas').getContext('2d');\n        const data = await addDynamicElementToCanvas({ctx: canvas, dynamic, base: {}});\n        expect(data).toHaveProperty('dynamic');\n    });\n    it('添加动态元素失败', async () => {\n        expect.assertions(1);\n        try {\n            await addDynamicElementToCanvas();\n        }\n        catch (e) {\n            expect(e).toHaveProperty('errno', 30001);\n        }\n    });\n});\n\ndescribe('生成base64图片对象', () => {\n    it('生成jpeg图片', async () => {\n        const canvas = document.createElement('canvas');\n\n        const {base64} = await getBase64({\n            canvasImg: canvas,\n            base: {\n                fileType: 'jpeg',\n                quality: '0.8'\n            }\n        });\n        expect(base64).toMatch(/data:image\\/jpeg;base64,/);\n    });\n\n    it('生成png图片', async () => {\n        let canvas = document.createElement('canvas');\n        const {base64} = await getBase64({\n            canvasImg: canvas,\n            base: {\n                fileType: 'png',\n                quality: '1'\n            }\n        });\n        expect(base64).toMatch(/data:image\\/png;base64,/);\n    });\n});\n\ndescribe('图片合成函数是否正常', () => {\n    it('dataType传参为base64', async () => {\n        const res = await mixImg(base64Config);\n        expect(res.data.base64).toMatch(/data:image\\/jpeg;base64,/);\n    });\n\n    it('dataType传参为canvas', async () => {\n        const res = await mixImg(canvasConfig);\n        expect(res.data.canvas).toHaveProperty('id', '_mixImgCanvas');\n\n    });\n});\n\n"
  },
  {
    "path": "test/unit/tools.test.js",
    "content": "/**\n * @file: 测试utils方法\n * @author: haoxin\n */\n\nimport {clientType, renameKey, splitArr} from '../../src/utils/tools';\nimport {userAgentMap, qrCodeConfig, dynamicConfig} from '../config';\n\ndescribe('判断clientType方法', () => {\n    it('clientType为android', () => {\n        Object.defineProperty(navigator, 'userAgent', {\n            writable: true,\n            value: userAgentMap.android\n        });\n        expect(clientType()).toBe('android');\n    });\n\n    it('clientType为ios', () => {\n        Object.defineProperty(navigator, 'userAgent', {\n            writable: true,\n            value: userAgentMap.ios\n        });\n        expect(clientType()).toBe('ios');\n    });\n\n    it('clientType为pc', () => {\n        Object.defineProperty(navigator, 'userAgent', {\n            writable: true,\n            value: userAgentMap.pc\n        });\n        expect(clientType()).toBe('pc');\n    });\n});\n\ndescribe('renameKey方法', () => {\n    it('重写对象属性名成功', () => {\n        const obj = renameKey(qrCodeConfig, 'foreground', 'colorDark');\n        expect(obj).toHaveProperty('colorDark');\n    });\n\n    it('被重写的属性不存在时是否不做处理', () => {\n        const obj = renameKey(qrCodeConfig, 'noExistKey', 'newName');\n        expect(obj).not.toHaveProperty('newName');\n    });\n\n\n    it('新属性名为空时是否不做处理', () => {\n        const obj = renameKey(qrCodeConfig, 'foreground', '');\n        expect(obj).toHaveProperty('foreground');\n    });\n});\n\ndescribe('splitArr方法', () => {\n    it('根据weight属性分组成功', () => {\n        const obj = splitArr(dynamicConfig, 'weight', 0);\n        expect(obj[0]).toHaveLength(2);\n    });\n});\n"
  },
  {
    "path": "toolkit/.editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 4\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "toolkit/.eslintrc.js",
    "content": "module.exports = {\n    extends: [\n        // 代码规范检查\n        '@ecomfe/eslint-config',\n        // 支持vue文件\n        '@ecomfe/eslint-config/vue'\n    ],\n    rules: {\n        'comma-dangle': ['error', {\n            objects: 'never'\n        }]\n    }\n};\n"
  },
  {
    "path": "toolkit/.gitignore",
    "content": "# Referenced from https://github.com/github/gitignore/blob/master/Node.gitignore\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nfis.conf.js\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n\n# other stuff\n.DS_Store\nThumbs.db\n\n# IDE configurations\n.idea\n.vscode\n\n# build assets\n/output\n/dist\n/dll\n"
  },
  {
    "path": "toolkit/.stylelintrc",
    "content": "{\n    \"extends\": \"@ecomfe/stylelint-config/baidu/default\"\n}\n"
  },
  {
    "path": "toolkit/babel.config.js",
    "content": "/**\n * @file: babel配置\n * @author: zhw\n * @Date: 2020-03-25 18:57:09\n * @Last Modified by: zhw\n * @Last Modified time: 2020-12-27 14:04:50\n */\nmodule.exports = {\n    presets: ['@vue/cli-plugin-babel/preset'],\n    plugins: ['@babel/plugin-proposal-optional-chaining']\n};\n"
  },
  {
    "path": "toolkit/package.json",
    "content": "{\n    \"name\": \"mix-img-toolkit\",\n    \"version\": \"0.0.1\",\n    \"private\": true,\n    \"scripts\": {\n        \"serve\": \"vue-cli-service serve\",\n        \"build\": \"vue-cli-service build\",\n        \"lint\": \"eslint --ext .js,.vue src/  --ignore-path .gitignore\",\n        \"start\": \"npm run serve\"\n    },\n    \"dependencies\": {\n        \"mix-img\": \"^1.0.6\",\n        \"clipboard\": \"^2.0.6\",\n        \"core-js\": \"^3.6.4\",\n        \"jsoneditor\": \"^8.6.3\",\n        \"view-design\": \"^4.3.2\",\n        \"vue\": \"^2.6.11\",\n        \"vue-router\": \"^3.0.6\"\n    },\n    \"devDependencies\": {\n        \"@babel/core\": \"^7.12.10\",\n        \"@babel/eslint-parser\": \"^7.13.10\",\n        \"@babel/eslint-plugin\": \"^7.13.10\",\n        \"@babel/plugin-proposal-optional-chaining\": \"^7.11.0\",\n        \"@ecomfe/eslint-config\": \"^7.0.0\",\n        \"@ecomfe/stylelint-config\": \"^1.1.1\",\n        \"@vue/cli-plugin-babel\": \"~4.5.6\",\n        \"@vue/cli-service\": \"~4.2.0\",\n        \"eslint\": \"^7.27.0\",\n        \"less\": \"^3.11.1\",\n        \"less-loader\": \"^5.0.0\",\n        \"stylelint\": \"^13.13.1\",\n        \"vue-template-compiler\": \"^2.6.11\"\n    }\n}\n"
  },
  {
    "path": "toolkit/public/index.html",
    "content": "<!-- eslint-disable -->\n<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <title>图片合成配置平台</title>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n</head>\n\n<body>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n</body>\n\n</html>\n"
  },
  {
    "path": "toolkit/src/App.vue",
    "content": "<template>\n    <div class=\"app\">\n        <router-view/>\n    </div>\n</template>\n\n<script>\nexport default {\n    data() {\n        return {};\n    }\n};\n</script>\n<style lang=\"less\"></style>\n"
  },
  {
    "path": "toolkit/src/components/layout/default/Layout.vue",
    "content": "<template>\n    <Layout class=\"layout-container\">\n        <appHeader/>\n        <Layout class=\"layout-body ivu-layout-has-sider\">\n            <Layout class=\"layout-body-right\">\n                <Content class=\"layout-body-right-content\">\n                    <router-view/>\n                </Content>\n            </Layout>\n        </Layout>\n        <Footer>\n            <div class=\"footer-content clear\">\n                <div class=\"footer-content-more clear\">\n                    <div class=\"footer-content-more-title\">\n                        <Icon type=\"ios-link\"/>\n                        更多资源\n                    </div>\n                    <a\n                        class=\"footer-content-more-content\" href=\"https://smartprogram.baidu.com/docs/develop/tutorial/intro/\"\n                        target=\"_blank\" rel=\"noopener noreferrer\"\n                    >\n                        <div class=\"footer-content-more-content-main\">百度智能小程序</div>\n                        <div class=\"footer-content-more-content-sub\"></div>\n                    </a>\n                    <a\n                        class=\"footer-content-more-content\" href=\"https://efe.baidu.com/\"\n                        target=\"_blank\" rel=\"noopener noreferrer\"\n                    >\n                        <div class=\"footer-content-more-content-main\">百度EFE（Excellent FrontEnd）技术体系</div>\n                        <div class=\"footer-content-more-content-sub\"></div>\n                    </a>\n                </div>\n                <div class=\"footer-content-more clear\">\n                    <div class=\"footer-content-more-title\">\n                        <Icon type=\"ios-people\"/>\n                        开发团队&招聘\n                    </div>\n                    <a\n                        class=\"footer-content-more-content\" href=\"https://talent.baidu.com/external/baidu/index.html#/jobDetail/2/150801\"\n                        target=\"_blank\" rel=\"noopener noreferrer\"\n                    >\n                        <div class=\"footer-content-more-content-main\">百度用户增长研发部</div>\n                    </a>\n                </div>\n                <div class=\"footer-content-slogan clear\">\n                    <div class=\"footer-content-slogan-logo\"></div>\n                    <div class=\"footer-content-slogan-title\">图片合成平台</div>\n                    <div class=\"footer-content-slogan-main\">致力于提供方便快捷的图片合成服务</div>\n                </div>\n            </div>\n        </Footer>\n    </Layout>\n</template>\n\n<script>\nimport appHeader from './components/header/appHeader';\nexport default {\n    components: {\n        appHeader\n    }\n};\n</script>\n<style lang=\"less\" scoped>\n.ivu-layout-header {\n  padding: 0 30px;\n}\n</style>\n<style lang=\"less\" scoped>\n.layout {\n    border: 1px solid #d7dde4;\n    background: #f5f7f9;\n    position: relative;\n    border-radius: 4px;\n    overflow: hidden;\n    &-container{\n        height: 100%;\n        &-body {\n            display: flex;\n            flex-direction: row;\n            &-right {\n            padding: 10px;\n            &-content {\n                padding: 10px 120px;\n                min-height: 700px;\n                background: #fff;\n            }\n            }\n            &-breadcrumb {\n                padding-left: 130px;\n            }\n        }\n    }\n}\n.ivu-layout-footer {\n    height: 227px;\n    background: #15273b;\n    .footer-content {\n        margin: 0 75px;\n        color: #fff;\n        height: 143px;\n        &-more {\n            float: left;\n            margin-right: 116px;\n            &-title {\n                .ivu-icon {\n                    font-size: 20px;\n                }\n                font-size: 18px;\n                margin-bottom: 18px;\n            }\n            &-content{\n                color: #ffffff;\n                display: flex;\n                margin-top: 5px;\n                &-main {\n                    font-size: 15px;\n                    line-height: 25px;\n                }\n                &-sub {\n                    font-size: 12px;\n                    line-height: 25px;\n                    margin-left: 5px;\n                }\n            }\n        }\n        &-slogan {\n            float: right;\n            font-size: 16px;\n            color: #ffffff;\n            width: 320px;\n            // display: flex;\n            // flex-direction: column;\n            &-logo {\n                width: 105px;\n                height: 86px;\n                background-image: url(\"../../../assets/logo.png\");\n                background-size: 100% 100%;\n                margin-bottom: 11px;\n                float: right;\n\n            }\n            &-title,&-main {\n                width: 100%;\n                float: right;\n                text-align: right;\n            }\n\n        }\n    }\n}\n.footer {\n  width: 100%;\n  text-align: center;\n  margin-top: 8px;\n  color: #ffffff;\n}\n</style>\n"
  },
  {
    "path": "toolkit/src/components/layout/default/components/header/appHeader.vue",
    "content": "<template>\n    <Header>\n        <Menu\n            mode=\"horizontal\" class=\"header-theme\"\n            theme=\"dark\"\n        >\n            <div class=\"layout-logo\"></div>\n            <div class=\"layout-nav\">\n                <div class=\"layout-title\">\n                    <div class=\"layout-title-line\"></div>\n                    <span class=\"layout-title-content\">图片合成配置平台</span>\n                </div>\n            </div>\n        </Menu>\n    </Header>\n</template>\n\n<script>\nexport default {\n};\n</script>\n<style lang=\"less\" scoped>\n.layout-logo {\n    width: 46px;\n    height: 38px;\n    float: left;\n    position: relative;\n    top: 20px;\n    left: 150px;\n    background-repeat: no-repeat;\n    background-size: contain;\n    background-image: url(\"../../../../../assets/logo.png\");\n}\n.layout-nav {\n  margin-left: 200px;\n  color: #fff;\n  font-size: 24px;\n  .layout-title {\n    float: left;\n    position: relative;\n    margin-right: 10px;\n\n    &-line {\n      position: absolute;\n      width: 50px;\n      height: 1px;\n      background: #ffffff;\n      top: 50%;\n      margin-top: -0.5px;\n    }\n    &-content {\n        margin-left: 50px;\n      }\n  }\n}\n.layout-user {\n    float: right;\n    margin: 0 120px;\n    color: #fff;\n}\n\n.ivu-layout-header {\n  padding: 0 !important;\n  height: 78px;\n  line-height: 78px;\n}\n.ivu-menu-dark {\n  background: #15273b;\n}\n.ivu-menu-horizontal {\n  height: 78px;\n  line-height: 78px;\n}\n.layout-router {\n    position: absolute;\n    left: 0;\n    top: 0;\n    right: 0;\n    bottom: 0;\n}\n</style>\n"
  },
  {
    "path": "toolkit/src/components/layout/default/index.js",
    "content": "/**\n * @file 路径配置配置\n * @author zhw(zhw)\n */\n\nimport Layout from './Layout';\nimport './layout.less';\nexport default Layout;\n"
  },
  {
    "path": "toolkit/src/components/layout/default/layout.less",
    "content": "body {\n    height: 100vh;\n}\n\n.app {\n    height: 100%;\n}\n\n.font-weight {\n    font-weight: bold;\n}\n\n\n\n* {\n    margin: 0;\n    padding: 0;\n    list-style: none;\n}\n\n/*\nKISSY CSS Reset\n理念：1. reset 的目的不是清除浏览器的默认样式，这仅是部分工作。清除和重置是紧密不可分的。\n2. reset 的目的不是让默认样式在所有浏览器下一致，而是减少默认样式有可能带来的问题。\n3. reset 期望提供一套普适通用的基础样式。但没有银弹，推荐根据具体需求，裁剪和修改后再使用。\n特色：1. 适应中文；2. 基于最新主流浏览器。\n维护：玉伯<lifesinger@gmail.com>, 正淳<ragecarrier@gmail.com>\n */\n\n/** 清除内外边距 **/\nbody,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\np,\nblockquote,\n/* structural elements 结构元素 */\ndl,\ndt,\ndd,\nul,\nol,\nli,\n/* list elements 列表元素 */\npre,\n/* text formatting elements 文本格式元素 */\nform,\nfieldset,\nlegend,\nbutton,\ninput,\ntextarea,\n/* form elements 表单元素 */\nth,\ntd\n\n/* table elements 表格元素 */\n    {\n    margin: 0;\n    padding: 0;\n}\n\n/** 设置默认字体 **/\nbody,\nbutton,\ninput,\nselect,\ntextarea\n\n/* for ie */\n    {\n    font: 12px/1.5 tahoma, arial, \\5b8b\\4f53, sans-serif;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n    font-size: 100%;\n}\n\naddress,\ncite,\ndfn,\nem,\nvar {\n    font-style: normal;\n}\n\n/* 将斜体扶正 */\ncode,\nkbd,\npre,\nsamp {\n    font-family: courier new, courier, monospace;\n}\n\n/* 统一等宽字体 */\nsmall {\n    font-size: 12px;\n}\n\n/* 小于 12px 的中文很难阅读，让 small 正常化 */\n\n/** 重置列表元素 **/\nul,\nol {\n    list-style: none;\n}\n\n/** 重置文本格式元素 **/\na {\n    text-decoration: none;\n}\n\na:hover {\n    text-decoration: underline;\n}\n\n\n/** 重置表单元素 **/\nlegend {\n    color: #000;\n}\n\n/* for ie6 */\nfieldset,\nimg {\n    border: 0;\n}\n\n/* img 搭车：让链接里的 img 无边框 */\nbutton,\ninput,\nselect,\ntextarea {\n    font-size: 100%;\n}\n\n/* 使得表单元素在 ie 下能继承字体大小 */\n/* 注：optgroup 无法扶正 */\n\n/** 重置表格元素 **/\ntable {\n    border-collapse: collapse;\n    border-spacing: 0;\n}\n\n/* 清除浮动 */\n.ks-clear:after,\n.clear:after {\n    content: '\\20';\n    display: block;\n    height: 0;\n    clear: both;\n}\n\n.ks-clear,\n.clear {\n    *zoom: 1;\n}\n\n.main {\n    padding: 30px 100px;\n    width: 960px;\n    margin: 0 auto;\n}\n\n.main h1 {\n    font-size: 36px;\n    color: #333;\n    text-align: left;\n    margin-bottom: 30px;\n    border-bottom: 1px solid #eee;\n}\n\n.helps {\n    margin-top: 40px;\n}\n\n.helps pre {\n    padding: 20px;\n    margin: 10px 0;\n    border: solid 1px #e7e1cd;\n    background-color: #fffdef;\n    overflow: auto;\n}\n\n.icon_lists {\n    width: 100% !important;\n\n}\n\n.icon_lists li {\n    float: left;\n    width: 100px;\n    height: 180px;\n    text-align: center;\n    list-style: none !important;\n}\n\n.icon_lists .icon {\n    font-size: 42px;\n    line-height: 100px;\n    margin: 10px 0;\n    color: #333;\n    -webkit-transition: font-size 0.25s ease-out 0s;\n    -moz-transition: font-size 0.25s ease-out 0s;\n    transition: font-size 0.25s ease-out 0s;\n\n}\n\n.icon_lists .icon:hover {\n    font-size: 100px;\n}\n\n\n\n.markdown {\n    color: #666;\n    font-size: 14px;\n    line-height: 1.8;\n}\n\n.highlight {\n    line-height: 1.5;\n}\n\n.markdown img {\n    vertical-align: middle;\n    max-width: 100%;\n}\n\n.markdown h1 {\n    color: #404040;\n    font-weight: 500;\n    line-height: 40px;\n    margin-bottom: 24px;\n}\n\n.markdown h2,\n.markdown h3,\n.markdown h4,\n.markdown h5,\n.markdown h6 {\n    color: #404040;\n    margin: 1.6em 0 0.6em 0;\n    font-weight: 500;\n    clear: both;\n}\n\n.markdown h1 {\n    font-size: 28px;\n}\n\n.markdown h2 {\n    font-size: 22px;\n}\n\n.markdown h3 {\n    font-size: 16px;\n}\n\n.markdown h4 {\n    font-size: 14px;\n}\n\n.markdown h5 {\n    font-size: 12px;\n}\n\n.markdown h6 {\n    font-size: 12px;\n}\n\n.markdown hr {\n    height: 1px;\n    border: 0;\n    background: #e9e9e9;\n    margin: 16px 0;\n    clear: both;\n}\n\n.markdown p,\n.markdown pre {\n    margin: 1em 0;\n}\n\n.markdown>p,\n.markdown>blockquote,\n.markdown>.highlight,\n.markdown>ol,\n.markdown>ul {\n    width: 80%;\n}\n\n.markdown ul>li {\n    list-style: circle;\n}\n\n.markdown>ul li,\n.markdown blockquote ul>li {\n    margin-left: 20px;\n    padding-left: 4px;\n}\n\n.markdown>ul li p,\n.markdown>ol li p {\n    margin: 0.6em 0;\n}\n\n.markdown ol>li {\n    list-style: decimal;\n}\n\n.markdown>ol li,\n.markdown blockquote ol>li {\n    margin-left: 20px;\n    padding-left: 4px;\n}\n\n.markdown code {\n    margin: 0 3px;\n    padding: 0 5px;\n    background: #eee;\n    border-radius: 3px;\n}\n\n.markdown pre {\n    border-radius: 6px;\n    background: #f7f7f7;\n    padding: 20px;\n}\n\n.markdown pre code {\n    border: none;\n    background: #f7f7f7;\n    margin: 0;\n}\n\n.markdown strong,\n.markdown b {\n    font-weight: 600;\n}\n\n.markdown>table {\n    border-collapse: collapse;\n    border-spacing: 0px;\n    empty-cells: show;\n    border: 1px solid #e9e9e9;\n    width: 95%;\n    margin-bottom: 24px;\n}\n\n.markdown>table th {\n    white-space: nowrap;\n    color: #333;\n    font-weight: 600;\n\n}\n\n.markdown>table th,\n.markdown>table td {\n    border: 1px solid #e9e9e9;\n    padding: 8px 16px;\n    text-align: left;\n}\n\n.markdown>table th {\n    background: #F7F7F7;\n}\n\n.markdown blockquote {\n    font-size: 90%;\n    color: #999;\n    border-left: 4px solid #e9e9e9;\n    padding-left: 0.8em;\n    margin: 1em 0;\n    font-style: italic;\n}\n\n.markdown blockquote p {\n    margin: 0;\n}\n\n.markdown .anchor {\n    opacity: 0;\n    transition: opacity 0.3s ease;\n    margin-left: 8px;\n}\n\n.markdown .waiting {\n    color: #ccc;\n}\n\n.markdown h1:hover .anchor,\n.markdown h2:hover .anchor,\n.markdown h3:hover .anchor,\n.markdown h4:hover .anchor,\n.markdown h5:hover .anchor,\n.markdown h6:hover .anchor {\n    opacity: 1;\n    display: inline-block;\n}\n\n.markdown>br,\n.markdown>p>br {\n    clear: both;\n}\n\n\n.hljs {\n    display: block;\n    background: white;\n    padding: 0.5em;\n    color: #333333;\n    overflow-x: auto;\n}\n\n.hljs-comment,\n.hljs-meta {\n    color: #969896;\n}\n\n.hljs-string,\n.hljs-variable,\n.hljs-template-variable,\n.hljs-strong,\n.hljs-emphasis,\n.hljs-quote {\n    color: #df5000;\n}\n\n.hljs-keyword,\n.hljs-selector-tag,\n.hljs-type {\n    color: #a71d5d;\n}\n\n.hljs-literal,\n.hljs-symbol,\n.hljs-bullet,\n.hljs-attribute {\n    color: #0086b3;\n}\n\n.hljs-section,\n.hljs-name {\n    color: #63a35c;\n}\n\n.hljs-tag {\n    color: #333333;\n}\n\n.hljs-title,\n.hljs-attr,\n.hljs-selector-id,\n.hljs-selector-class,\n.hljs-selector-attr,\n.hljs-selector-pseudo {\n    color: #795da3;\n}\n\n.hljs-addition {\n    color: #55a532;\n    background-color: #eaffea;\n}\n\n.hljs-deletion {\n    color: #bd2c00;\n    background-color: #ffecec;\n}\n\n.hljs-link {\n    text-decoration: underline;\n}\n\npre {\n    background: #fff;\n}"
  },
  {
    "path": "toolkit/src/main.js",
    "content": "/**\n * @file: 入口文件\n * @author: zhw\n * @Date: 2020-03-25 00:04:33\n * @Last Modified by: zhw\n * @Last Modified time: 2020-12-27 14:04:18\n */\nimport Vue from 'vue';\nimport ViewUI from 'view-design';\nimport App from './App.vue';\nimport router from './router';\nimport 'view-design/dist/styles/iview.css';\n\nVue.use(ViewUI);\n\n// 开启debug模式\nVue.config.debug = true;\n\n// eslint-disable-next-line no-new\nnew Vue({\n    el: '#app',\n    router,\n    render: h => h(App)\n});\n\n"
  },
  {
    "path": "toolkit/src/router/index.js",
    "content": "/**\n * @file: 路由处理\n * @author: zhw(zhenghaiwang)\n * @Date: 2020-03-24 23:36:24\n * @Last Modified by: zhw\n * @Last Modified time: 2020-12-18 17:42:27\n */\nimport Vue from 'vue';\nimport Router from 'vue-router';\nimport routes from './routers';\n\nVue.use(Router);\nconst router = new Router({\n    routes,\n    mode: 'hash'\n});\n\nexport default router;\n"
  },
  {
    "path": "toolkit/src/router/routers.js",
    "content": "/**\n * @file 路径配置配置\n * @author zhw(zhw)\n */\n\nimport Main from '../components/layout/default';\nexport default [\n    {\n        path: '/',\n        component: Main,\n        redirect: 'imageViewFe',\n        children: [\n            {\n                path: 'imageViewFe',\n                component: () => import(/* webpackChunkName: 'dynamicFe' */ '../template/dynamicFe/index')\n            }\n        ]\n    }\n];\n"
  },
  {
    "path": "toolkit/src/template/dynamicFe/index.js",
    "content": "/**\n * @file: zhw(zhenghaiwang)\n * @author: zhw(zhenghaiwang)\n * @Date: 2020-03-24 23:21:07\n * @Last Modified by: zhw\n * @Last Modified time: 2020-03-25 23:54:12\n */\nimport index from './index.vue';\n\nexport default index;\n"
  },
  {
    "path": "toolkit/src/template/dynamicFe/index.vue",
    "content": "<template>\n    <div class=\"dynamic-template\">\n        <Tabs value=\"JSON_EDITOR\" class=\"tabs\">\n            <TabPane label=\"JSON配置\" name=\"JSON_EDITOR\">\n                <div ref=\"json\" class=\"json-contain\">\n                </div>\n            </TabPane>\n        </Tabs>\n        <Card\n            class=\"image-contain\"\n            :padding=\"10\"\n        >\n            <p slot=\"title\">图片预览</p>\n            <div class=\"image-box\">\n            </div>\n        </Card>\n        <div class=\"button-list\">\n            <Button\n                class=\"button-item\" type=\"primary\"\n                @click=\"preview\"\n            >生成预览</Button>\n            <Button\n                class=\"button-item copy-button\"\n                type=\"warning\"\n                :data-clipboard-text=\"copyText\"\n                @click=\"copy\"\n            >复制配置</Button>\n        </div>\n    </div>\n</template>\n<script>\nimport JSONEditor from 'jsoneditor';\nimport 'jsoneditor/dist/jsoneditor.min.css';\nimport Clipboard from 'clipboard';\nimport {mixImg} from 'mix-img';\n\nexport default {\n    data() {\n        return {\n            editor: null,\n            basicFormModel: {\n                backgroundImg: 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-07/1594797097021/ml9v716tnxoc.jpg',\n                width: 375,\n                height: 667,\n                quality: 0.8,\n                fileType: 'jpeg',\n                dataType: 'canvas'\n            },\n            qrCodeFormModel: {\n                width: 100,\n                height: 100,\n                text: 'https://www.baidu.com',\n                x: 270,\n                y: 573,\n                correctLevel: 1\n            },\n            dynamicFormModel: [\n                {\n                    type: 2,\n                    position: {\n                        x: 187,\n                        y: 350\n                    },\n                    style: {\n                        fontSize: 34,\n                        color: '#ffebc0',\n                        textAlign: 'center',\n                        fontWeight: 'bold'\n                    },\n                    text: '{variate}'\n                },\n                {\n                    type: 1,\n                    position: {\n                        x: 162,\n                        y: 200\n                    },\n                    size: {\n                        dWidth: 50,\n                        dHeight: 50\n                    },\n                    imgUrl: 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-07/1594717976441/idyexeq1u92w.png',\n                    isRound: true\n                }\n            ],\n            replaceFinalText: {\n                variate: '小明',\n                avatarUrl: 'https://efe-h2.cdn.bcebos.com/ceug/resource/res/2020-12/1608522700040/yq3fma464m5y.png'\n            },\n            copyText: ''\n        };\n    },\n    computed: {\n        // 表单数据\n        formData() {\n            return Object.assign(\n                {},\n                {\n                    base: this.basicFormModel,\n                    qrCode: this.qrCodeFormModel,\n                    dynamic: this.dynamicFormModel,\n                    replaceText: this.replaceFinalText\n                }\n            );\n        }\n    },\n    mounted() {\n        this.editor = new JSONEditor(this.$refs.json, {\n            modes: ['code', 'text', 'preview']\n        });\n        this.editor.set(this.formData);\n        this.preview();\n    },\n    methods: {\n        async preview() {\n            const res = await mixImg(this.editor.get());\n            console.log('图片合成结束~~', res);\n            if (res.errno === 0 && res.data.canvas) {\n                document.getElementsByClassName('image-box')[0].appendChild(res.data.canvas);\n            }\n        },\n\n        // 复制配置\n        copy() {\n            this.copyText = this.editor && JSON.stringify(this.editor.get());\n            const clipboard = new Clipboard('.copy-button');\n            clipboard.on('success', e => {\n                this.$Message.info('复制成功');\n                e.clearSelection();\n                clipboard.destroy();\n            });\n        }\n    }\n};\n</script>\n<style lang=\"less\">\n.dynamic-template {\n    display: flex;\n    justify-content: space-between;\n    .tabs {\n        width: 1000px;\n        .form-pannel {\n            position: relative;\n            &-dele {\n                position: absolute;\n                right: 10px;\n                top: 50%;\n                margin-top: -9px;\n            }\n        }\n    }\n    .button-list {\n        width: 100px;\n        text-align: center;\n        padding-top: 100px;\n        .button-item {\n            margin-top: 50px;\n        }\n    }\n    .image-contain {\n        width: 434px;\n        height: 100%;\n        margin: 30px;\n        .image-box {\n            #_mixImgCanvas {\n                width: 100%;\n            }\n        }\n    }\n    .json-contain {\n        width: 100%;\n        height: 780px;\n    }\n    .title-btn-right {\n        margin: 20px 0 0 50px;\n    }\n}\n</style>\n"
  },
  {
    "path": "toolkit/vue.config.js",
    "content": "/**\n * @file: vue conf\n * @author: zhw(zhenghaiwang)\n * @Date: 2020-03-24 23:46:04\n * @Last Modified by: zhw\n * @Last Modified time: 2020-12-31 15:57:50\n */\n\nmodule.exports = {\n    publicPath: '/mix-img-toolkit',\n    assetsDir: '',\n    // outputDir: 'dist',\n    devServer: {\n        compress: true,\n        port: 9000,\n        // host: 'ug.baidu-int.com',\n        host: '0.0.0.0',\n        historyApiFallback: true,\n        hot: true,\n        inline: true,\n        open: true\n    }\n};\n"
  }
]