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

579 阅读6分钟

需求背景

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

实现步骤

  • 在 wxml 中放一个 canvas 的承载标签 imgCanvas
  • 在海报的分享中,改变的部分大多数是产品信息和二维码,这里我们可以吧绘制图片写成一个方法函数然后需要的数据以参数的方式传递进来。
export function setCanvasImage(productInfo, qrCode) {
  // 具体的业务实现
}
  • 更具需求获取到海报的元信息,比如:背景图、商品图、logo 图、市场价、活动价等,然后对海报的样式进行绘制
  • 首先我们先从参数中获取到我们的元信息
const { productName, url, price, marketPrice, logo } = productInfo
  • 图片获取的是一个 url,我们在绘制海报的时候,需要把图片下载下来,然后才能进行绘制,因为图片获取的接口是个异步行为,我们用 promise 进行封装,获取图片完成之后才能进行绘制。
  • 封装了一个单位转换函数,因为样式是基于px的
export function rpxToPx(data = 0) {
  // return (data / 750 * wx.getSystemInfoSync().windowWidth)
  return (data * wx.getSystemInfoSync().windowWidth) / 750
}
  • 获取背景图片
const bgImgPromise = new Promise((resolve, reject) => {
  wx.downloadFile({
    url: 'https://xxxx.com/a.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)
    },
  })
})
  • 获取二维码信息
// 由于数据是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')
  }
})
  • 接下来就是在画布上对获取的元素进行拼图和美化操作了
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)
    })
})

完整代码

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

/**
 * 拼团海报
 * @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)
      })
  })
}