微信小程序中前端生成海报图片

需求背景

一般在促销活动中,都会有一个活动海报进行宣传。有时候的海报是针对产品的,那么分享出去的海报是实时更新的。这里就以前端拿到相应的数据然后进行图片的绘制然后在进行分享,简单记录下代码的实现。

实现步骤

  • 在 wxml 中放一个 canvas 的承载标签 imgCanvas

  • 在海报的分享中,改变的部分大多数是产品信息和二维码,这里我们可以吧绘制图片写成一个方法函数然后需要的数据以参数的方式传递进来。

1
2
3
export function setCanvasImage(productInfo, qrCode) {
// 具体的业务实现
}
  • 更具需求获取到海报的元信息,比如:背景图、商品图、logo 图、市场价、活动价等,然后对海报的样式进行绘制
  • 首先我们先从参数中获取到我们的元信息
1
const { productName, url, price, marketPrice, logo } = productInfo
  • 图片获取的是一个 url,我们在绘制海报的时候,需要把图片下载下来,然后才能进行绘制,因为图片获取的接口是个异步行为,我们用 promise 进行封装,获取图片完成之后才能进行绘制。

  • 封装了一个单位转换函数,因为样式是基于px的

1
2
3
4
export function rpxToPx(data = 0) {
// return (data / 750 * wx.getSystemInfoSync().windowWidth)
return (data * wx.getSystemInfoSync().windowWidth) / 750
}
  • 获取背景图片
1
2
3
4
5
6
7
8
9
10
11
const bgImgPromise = new Promise((resolve, reject) => {
wx.downloadFile({
url: 'https://xxxx.com/a.jpg',
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
reject(err)
},
})
})
  • 获取产品图片
1
2
3
4
5
6
7
8
9
10
11
const prdImgPromise = new Promise((resolve, reject) => {
wx.getImageInfo({
src: url,
success: res => {
resolve(res)
},
fail: err => {
reject(err)
},
})
})
  • 获取品牌 logo 图
1
2
3
4
5
6
7
8
9
10
11
const logoImgPromise = new Promise((resolve, reject) => {
wx.downloadFile({
url: logo,
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
reject(err)
},
})
})
  • 获取二维码信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 由于数据是base64所以是一下方法如果是url可以使用上面获取图片的方式拿到图片
const qrImgPromise = new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(qrCode) || []
if (format) {
const filePath = `${wx.env.USER_DATA_PATH}/${productInfo.productId}.png`
const fileManager = wx.getFileSystemManager()
fileManager.writeFile({
filePath,
data: bodyData,
encoding: 'base64',
success: () => {
resolve(filePath)
},
fail: err => {
reject(err)
},
})
} else {
return new Error('xxxxxxxxx')
}
})
  • 接下来就是在画布上对获取的元素进行拼图和美化操作了
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
return new Promise((resolve, reject) => {
Promise.all([bgImgPromise, prdImgPromise, logoImgPromise, qrImgPromise])
.then(results => {
const context = wx.createCanvasContext('imgCanvas')
// 背景图片
context.drawImage(results[0], 0, 0, rpxToPx(750), rpxToPx(1334))

// 产品图片 做了兼容处理,修改样式api可以查看官方文档
const { width, height, path } = results[1]
if (width === height) {
// 产品图片:宽度等于高度
context.drawImage(path, rpxToPx(80), rpxToPx(100), rpxToPx(590), rpxToPx(590))
} else if (width > height) {
// 产品图片:宽度大于高度
const hwScale = height / width // 高宽比
context.drawImage(path, rpxToPx(80), rpxToPx(100 + (590 * (1 - hwScale)) / 2), rpxToPx(590), rpxToPx(590 * hwScale))
} else {
// 产品图片:宽度小于高度
const whScale = width / height // 高宽比
context.drawImage(path, rpxToPx(80 + (590 * (1 - whScale)) / 2), rpxToPx(100), rpxToPx(590 * whScale), rpxToPx(590))
}

// 产品名称
context.setFontSize(rpxToPx(30))
context.setTextBaseline('top')
context.setFillStyle('rgba(0, 0, 0, 0.9)')
let productNameDrawY = rpxToPx(730) // 绘制文本的y坐标
const productNameMaxWidth = rpxToPx(570) // 绘制文本最大宽度
const productNameLineHeight = rpxToPx(40) // 产品名称单行高度
let productNameDrawTxt = '' // 当前绘制的内容
let productNameDrawIndex = 0 // 当前绘制内容的索引
let productNameLineNum = 1 // 产品名称展示行数
if (context.measureText(productName).width <= productNameMaxWidth) {
// 单行绘制
context.fillText(productName, rpxToPx(80), productNameDrawY)
} else {
// 多行绘制
for (var i = 0; i < productName.length; i++) {
productNameDrawTxt += productName[i]
if (context.measureText(productNameDrawTxt).width >= productNameMaxWidth) {
context.fillText(productName.substring(productNameDrawIndex, i + 1), rpxToPx(80), productNameDrawY)
productNameDrawIndex = i + 1
productNameDrawY += productNameLineHeight
productNameDrawTxt = ''
productNameLineNum += 1
} else {
if (i === productName.length - 1) {
// 剩下不足 maxWidth 内容的绘制
context.fillText(productName.substring(productNameDrawIndex), rpxToPx(80), productNameDrawY)
}
}
}
}

// 拼团价整数部分
const showPriceInt = `¥${Math.floor(price / 100)}`
context.setFontSize(rpxToPx(50))
context.setFillStyle('#f5770a')
context.setTextBaseline('normal')
context.fillText(showPriceInt, rpxToPx(70), rpxToPx(795 - 0.5) + productNameLineHeight * productNameLineNum)
context.fillText(showPriceInt, rpxToPx(70 - 0.5), rpxToPx(795) + productNameLineHeight * productNameLineNum)
context.fillText(showPriceInt, rpxToPx(70), rpxToPx(795) + productNameLineHeight * productNameLineNum)

// 拼团价小数部分
const showPriceIntWidth = context.measureText(showPriceInt).width
const decimal = price % 100
const showPriceDecimal = decimal > 9 ? `.${decimal}` : `.0${decimal}`
context.setFontSize(rpxToPx(40))
context.setFillStyle('#f5770a')
context.setTextBaseline('normal')
context.fillText(showPriceDecimal, rpxToPx(70) + showPriceIntWidth, rpxToPx(795 - 0.5) + productNameLineHeight * productNameLineNum)
context.fillText(showPriceDecimal, rpxToPx(70 - 0.5) + showPriceIntWidth, rpxToPx(795) + productNameLineHeight * productNameLineNum)
context.fillText(showPriceDecimal, rpxToPx(70) + showPriceIntWidth, rpxToPx(795) + productNameLineHeight * productNameLineNum)

// 拼团价标识背景
const showPriceDecimalWidth = context.measureText(showPriceDecimal).width
context.setFillStyle('#f5770a')
context.fillRect(
rpxToPx(80) + showPriceIntWidth + showPriceDecimalWidth,
rpxToPx(765) + productNameLineHeight * productNameLineNum,
rpxToPx(88),
rpxToPx(30)
)

// 拼团价标识
context.setFontSize(rpxToPx(22))
context.setTextBaseline('top')
context.setFillStyle('rgba(255, 255, 255, 0.9)')
context.fillText('拼团价', rpxToPx(91) + showPriceIntWidth + showPriceDecimalWidth, rpxToPx(766) + productNameLineHeight * productNameLineNum)

// 市场价
const showMarketPrice = `¥${Number(marketPrice / 100).toFixed(2)}`
context.setFontSize(rpxToPx(26))
context.setTextBaseline('top')
context.setFillStyle('rgba(0, 0, 0, 0.6)')
context.fillText(showMarketPrice, rpxToPx(80), rpxToPx(816) + productNameLineHeight * productNameLineNum)

// 市场价删除线
const showMarketPriceWidth = context.measureText(showMarketPrice).width
context.beginPath()
context.moveTo(rpxToPx(80), rpxToPx(830) + productNameLineHeight * productNameLineNum)
context.lineTo(rpxToPx(80) + showMarketPriceWidth, rpxToPx(830) + productNameLineHeight * productNameLineNum)
context.setStrokeStyle('rgba(0, 0, 0, 0.6)')
context.stroke()

// 扫描/长按识别
context.setFontSize(rpxToPx(40))
context.setTextBaseline('top')
context.setFillStyle('rgba(0, 0, 0, 0.9)')
// 由于api没有加粗效果的,故用叠加达到加粗效果
context.fillText('扫描/长按识别', rpxToPx(60), rpxToPx(1137 - 0.5))
context.fillText('扫描/长按识别', rpxToPx(60 - 0.5), rpxToPx(1137))
context.fillText('扫描/长按识别', rpxToPx(60), rpxToPx(1137))

// 即刻参与拼团
context.setFontSize(rpxToPx(26))
context.setTextBaseline('top')
context.setFillStyle('rgba(0, 0, 0, 0.6)')
context.fillText('即刻参与拼团', rpxToPx(60), rpxToPx(1195))

// 品牌log和二维码
context.drawImage(results[2], rpxToPx(380), rpxToPx(1129), rpxToPx(100), rpxToPx(100))
context.drawImage(results[3], rpxToPx(515), rpxToPx(1099), rpxToPx(160), rpxToPx(160))

context.draw(true, res => {
if (res.errMsg === 'drawCanvas:ok') {
setTimeout(() => {
wx.canvasToTempFilePath({
canvasId: 'imgCanvas',
success: result => {
// 图片生成成功,删除本地二维码文件
const filePath = `${wx.env.USER_DATA_PATH}/${productInfo.productId}.png`
const fileManager = wx.getFileSystemManager()
fileManager.unlinkSync(filePath)
// 返回图片临时路径
resolve(result.tempFilePath)
},
fail: err => {
reject(err)
},
})
}, 100)
} else {
reject(res.errMsg)
}
})
})
.catch(err => {
reject(err)
})
})

完整代码

整体实现一个分享海报的图片就上面这几步,可能根据具体的业务有相应的修改,但大体的步骤没有变化,代码有注释,不清楚的可以看下注释因该就懂了,贴上完整代码。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/**
* 拼团海报
* @param {产品信息} productInfo
* @param {二维码} qrCode
*/
export function setCanvasImage(productInfo, qrCode) {
const {
productName, // 产品名称
url, // 产品图片
price, //
marketPrice,
logo,
} = productInfo
// 背景图片
const bgImgPromise = new Promise((resolve, reject) => {
wx.downloadFile({
url: 'https://g.wopuwulian.com/zpk/assets/static/img_assemble_share_b.jpg',
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
reject(err)
},
})
})
// 产品图片
const prdImgPromise = new Promise((resolve, reject) => {
wx.getImageInfo({
src: url,
success: res => {
resolve(res)
},
fail: err => {
reject(err)
},
})
})
// 品牌logo图片
const logoImgPromise = new Promise((resolve, reject) => {
wx.downloadFile({
url: logo,
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
reject(err)
},
})
})
// 二维码图片
const qrImgPromise = new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(qrCode) || []
if (format) {
const filePath = `${wx.env.USER_DATA_PATH}/${productInfo.productId}.png`
const fileManager = wx.getFileSystemManager()
fileManager.writeFile({
filePath,
data: bodyData,
encoding: 'base64',
success: () => {
resolve(filePath)
},
fail: err => {
reject(err)
},
})
} else {
return new Error('xxxxxxxxx')
}
})
return new Promise((resolve, reject) => {
Promise.all([bgImgPromise, prdImgPromise, logoImgPromise, qrImgPromise])
.then(results => {
const context = wx.createCanvasContext('imgCanvas')
// 背景图片
context.drawImage(results[0], 0, 0, rpxToPx(750), rpxToPx(1334))

// 产品图片
const { width, height, path } = results[1]
if (width === height) {
// 产品图片:宽度等于高度
context.drawImage(path, rpxToPx(80), rpxToPx(100), rpxToPx(590), rpxToPx(590))
} else if (width > height) {
// 产品图片:宽度大于高度
const hwScale = height / width // 高宽比
context.drawImage(path, rpxToPx(80), rpxToPx(100 + (590 * (1 - hwScale)) / 2), rpxToPx(590), rpxToPx(590 * hwScale))
} else {
// 产品图片:宽度小于高度
const whScale = width / height // 高宽比
context.drawImage(path, rpxToPx(80 + (590 * (1 - whScale)) / 2), rpxToPx(100), rpxToPx(590 * whScale), rpxToPx(590))
}

// 产品名称
context.setFontSize(rpxToPx(30))
context.setTextBaseline('top')
context.setFillStyle('rgba(0, 0, 0, 0.9)')
let productNameDrawY = rpxToPx(730) // 绘制文本的y坐标
const productNameMaxWidth = rpxToPx(570) // 绘制文本最大宽度
const productNameLineHeight = rpxToPx(40) // 产品名称单行高度
let productNameDrawTxt = '' // 当前绘制的内容
let productNameDrawIndex = 0 // 当前绘制内容的索引
let productNameLineNum = 1 // 产品名称展示行数
if (context.measureText(productName).width <= productNameMaxWidth) {
// 单行绘制
context.fillText(productName, rpxToPx(80), productNameDrawY)
} else {
// 多行绘制
for (var i = 0; i < productName.length; i++) {
productNameDrawTxt += productName[i]
if (context.measureText(productNameDrawTxt).width >= productNameMaxWidth) {
context.fillText(productName.substring(productNameDrawIndex, i + 1), rpxToPx(80), productNameDrawY)
productNameDrawIndex = i + 1
productNameDrawY += productNameLineHeight
productNameDrawTxt = ''
productNameLineNum += 1
} else {
if (i === productName.length - 1) {
// 剩下不足 maxWidth 内容的绘制
context.fillText(productName.substring(productNameDrawIndex), rpxToPx(80), productNameDrawY)
}
}
}
}

// 拼团价整数部分
const showPriceInt = `¥${Math.floor(price / 100)}`
context.setFontSize(rpxToPx(50))
context.setFillStyle('#f5770a')
context.setTextBaseline('normal')
context.fillText(showPriceInt, rpxToPx(70), rpxToPx(795 - 0.5) + productNameLineHeight * productNameLineNum)
context.fillText(showPriceInt, rpxToPx(70 - 0.5), rpxToPx(795) + productNameLineHeight * productNameLineNum)
context.fillText(showPriceInt, rpxToPx(70), rpxToPx(795) + productNameLineHeight * productNameLineNum)

// 拼团价小数部分
const showPriceIntWidth = context.measureText(showPriceInt).width
const decimal = price % 100
const showPriceDecimal = decimal > 9 ? `.${decimal}` : `.0${decimal}`
context.setFontSize(rpxToPx(40))
context.setFillStyle('#f5770a')
context.setTextBaseline('normal')
context.fillText(showPriceDecimal, rpxToPx(70) + showPriceIntWidth, rpxToPx(795 - 0.5) + productNameLineHeight * productNameLineNum)
context.fillText(showPriceDecimal, rpxToPx(70 - 0.5) + showPriceIntWidth, rpxToPx(795) + productNameLineHeight * productNameLineNum)
context.fillText(showPriceDecimal, rpxToPx(70) + showPriceIntWidth, rpxToPx(795) + productNameLineHeight * productNameLineNum)

// 拼团价标识背景
const showPriceDecimalWidth = context.measureText(showPriceDecimal).width
context.setFillStyle('#f5770a')
context.fillRect(
rpxToPx(80) + showPriceIntWidth + showPriceDecimalWidth,
rpxToPx(765) + productNameLineHeight * productNameLineNum,
rpxToPx(88),
rpxToPx(30)
)

// 拼团价标识
context.setFontSize(rpxToPx(22))
context.setTextBaseline('top')
context.setFillStyle('rgba(255, 255, 255, 0.9)')
context.fillText('拼团价', rpxToPx(91) + showPriceIntWidth + showPriceDecimalWidth, rpxToPx(766) + productNameLineHeight * productNameLineNum)

// 市场价
const showMarketPrice = `¥${Number(marketPrice / 100).toFixed(2)}`
context.setFontSize(rpxToPx(26))
context.setTextBaseline('top')
context.setFillStyle('rgba(0, 0, 0, 0.6)')
context.fillText(showMarketPrice, rpxToPx(80), rpxToPx(816) + productNameLineHeight * productNameLineNum)

// 市场价删除线
const showMarketPriceWidth = context.measureText(showMarketPrice).width
context.beginPath()
context.moveTo(rpxToPx(80), rpxToPx(830) + productNameLineHeight * productNameLineNum)
context.lineTo(rpxToPx(80) + showMarketPriceWidth, rpxToPx(830) + productNameLineHeight * productNameLineNum)
context.setStrokeStyle('rgba(0, 0, 0, 0.6)')
context.stroke()

// 扫描/长按识别
context.setFontSize(rpxToPx(40))
context.setTextBaseline('top')
context.setFillStyle('rgba(0, 0, 0, 0.9)')
// 由于api没有加粗效果的,故用叠加达到加粗效果
context.fillText('扫描/长按识别', rpxToPx(60), rpxToPx(1137 - 0.5))
context.fillText('扫描/长按识别', rpxToPx(60 - 0.5), rpxToPx(1137))
context.fillText('扫描/长按识别', rpxToPx(60), rpxToPx(1137))

// 即刻参与拼团
context.setFontSize(rpxToPx(26))
context.setTextBaseline('top')
context.setFillStyle('rgba(0, 0, 0, 0.6)')
context.fillText('即刻参与拼团', rpxToPx(60), rpxToPx(1195))

// 品牌logo和二维码
context.drawImage(results[2], rpxToPx(380), rpxToPx(1129), rpxToPx(100), rpxToPx(100))
context.drawImage(results[3], rpxToPx(515), rpxToPx(1099), rpxToPx(160), rpxToPx(160))

context.draw(true, res => {
if (res.errMsg === 'drawCanvas:ok') {
setTimeout(() => {
wx.canvasToTempFilePath({
canvasId: 'imgCanvas',
success: result => {
// 图片生成成功,删除本地二维码文件
const filePath = `${wx.env.USER_DATA_PATH}/${productInfo.productId}.png`
const fileManager = wx.getFileSystemManager()
fileManager.unlinkSync(filePath)
// 返回图片临时路径
resolve(result.tempFilePath)
},
fail: err => {
reject(err)
},
})
}, 100)
} else {
reject(res.errMsg)
}
})
})
.catch(err => {
reject(err)
})
})
}
-------------本文结束感谢您的阅读-------------

本文标题:微信小程序中前端生成海报图片

文章作者:Water

发布时间:2020年07月27日 - 09:07

最后更新:2023年08月01日 - 06:08

原始链接:https://water.buging.cn/2020/07/27/微信小程序中前端生成海报图片/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!