Canvas根据纯文本绘制图片,并生成file二进制流


接到个需求,要求根据给出的纯文本商标,智能生成商标图样(即纯文本图片),并自动上传服务器。不多说,开始动手

思路

  • canvas绘制文字,并在合适的地方切断换行,获得base64字符串
  • 将base64转换file二进制流

方案

根据最大宽度寻找canvas文字的切换断点方法

使用ctx2D.measureText()来获取canvas中的字串的宽度等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 寻找canvas文字的切换断点
* @param {string} text 要寻找断点的文本
* @param {number} width 需要切断的最大宽度
* @param {any} context canvas画布
* @return {number} 断点位置
*/
function findTextBreakPoint(text, width, context) {
let [min, max] = [0, text.length - 1]

while (min <= max) {
// 取文本中值
const middle = Math.floor((min + max) / 2)
// 通过 measureText(text).width 获取一半文本在画布中的像素宽度
const middleWidth = context.measureText(text.substr(0, middle)).width
// 获取一半+1字符文本在画布中的像素宽度
const middleOneCharWidth = context.measureText(text.substr(0, middle + 1)).width
// 如果加1字符后刚好超出最大宽度,说明切断点就在middle这里
if (middleWidth <= width && middleOneCharWidth > width) {
return middle
}
// 如果一半宽度小于最大宽度,往前推进一个字符,反之后退
if (middleWidth < width) {
min = middle + 1
} else {
max = middle - 1
}
}
// 未找到断点,直接返回-1
return -1
}

获得根据最大宽度切割成的字串数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 将文字分割,存入数组返回
* @param {any} context canvas画布
* @param {string} text 要切断为数组的文本
* @param {number} width 需要切断的最大宽度
* @param {string} [font = ''] canvas文本样式
* @return {string[]} 切割后的字串数组
*/
function breakLinesForCanvas(context, text, width, font = '') {
const result = []
let breakPoint = 0

if (font) {
context.font = font
}
// 遍历寻找断点,直到找不到断点
while ((breakPoint = findTextBreakPoint(text, width, context)) !== -1) {
// 切割断点前的字串,存入数组
result.push(text.substr(0, breakPoint))
// 将text设置为切割剩余的字串,继续遍历
text = text.substr(breakPoint)
}
// 如果还有剩余字串,丢入数组
if (text) {
result.push(text)
}
return result
}

base64转换file二进制流(这方面不太熟,参考:图片URL转Base64,Base64转二进制文件流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function base64ToFile(url) {
// 截取并获取mine
const arr = url.split(',')
if (!arr || !arr[0]) throw new Error('获取文件mime失败')

const mimeMatch = arr[0].match(/:(.*);/)
if (!mimeMatch) throw new Error('获取文件mime失败')

const mime = mimeMatch[1]
// base64解码为Unicode编码
const bstr = window.atob(arr[1])
// 把每个unicode编码放入Unit8Array
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], '商标图样.jpg', {type: mime})
}

将给定文字绘制到canvas画布,并获得生成的File文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function intelGenerate(text) {
const canvas = document.createElement('canvas')
// 设置生成的图片宽高
const [width, height] = [800, 800]
canvas.width = width
canvas.height = height
const context = canvas.getContext('2d')
context.fillStyle = '#fff'
context.fillRect(0, 0, width, height)

// 设置文字样式
const lineHeight = 90
context.font = 'bold 80px SimSun'
context.textAlign = 'center'
context.textBaseline = 'middle'
context.fillStyle = '#000'

// 获取切断目标文本后的数组
const result = this.breakLinesForCanvas(context, text, width)
// 设置文字开始绘制的高度,保证生成文字垂直居中
const startTop = (height - result.length * lineHeight + lineHeight) / 2
// 遍历逐行填充文本
result.forEach((item, index) => {
context.fillText(item, width / 2, lineHeight * index + startTop)
})

// 将canvas画布内容转为base64
const imgBaseData = canvas.toDataURL('image/jpeg')
// base64转换file二进制流
const file = this.base64ToFile(imgBaseData)

return file
}

补充

手动组装element-ui上传所需的file对象

1
2
3
4
5
6
7
8
9
10
function makeUploadFile(file) {
return {
file,
uid: new Date().getTime(),
size: file.size,
name: file.name,
raw: file,
url: URL.createObjectURL(file)
}
}