微信小程序生成海报并保存到本地系统相册

2,836 阅读5分钟

先来效果图:

根据不同的页面详情生成不同的海报,可以保存到本地或者选择分享出去.

最终效果图

思路:

用户点击生成海报,执行canvas绘制,所以页面需要一个canvas容器,用来放后面绘制的结果,以及生成的图片;考虑到绘制可能需要点时间,可以写一个假的海报样式,先展示给用户,而后保存的时候保存canvas生成即可.

微信小程序部分相关接口:

接口的详情以及参数示例等可以参考微信官方文档;

  • wx.downloadFile(Object object): 下载文件资源到本地,返回文件的本地临时路径(本地路径),单词允许下载的最大文件为50MB;
  • wx.saveImageToPhotosAlbum(Object object): 保存图片到系统相册;
  • wx.getSystemInfo(Object object): 获取系统信息;
  • wx.canvasToTempFilePath(Object object, Object this): 把当前画布指定区域的内容导出生成指定大小的图片(在 draw() 回调里调用该方法才能保证图片导出成功)
  • wx.createCanvasContext(string canvasId, Object this): 创建 canvas 的绘图上下文 CanvasContext 对象(canvasId就是canvas组件的canvas-id属性); CanvasContext部分相关绘制指令(参数太多了,还是去官方文档去看吧):
名称 作用
ctx.drawImage() 绘制图像到画布
ctx.save() 保存绘图上下文
ctx.beginPath() 开始创建一个路径,需要调用 fill 或者 stroke 才会使用路径进行填充或描边
ctx.rect() 创建一个矩形路径
ctx.setLineJoin() 设置线条的交点样式
ctx.setLineWidth() 设置线条的宽度
ctx.arc() 创建一条弧线(可以画圆)
ctx.clip() 从原始画布中裁剪出来一块,后面绘图的内容不会超出裁剪出来的区域(可以在裁剪前先用save方法保存当前画布区域,后面可以通过restore方法恢复)
ctx.restore() 恢复之前保存的绘图上下文
ctx.setFontSize() 设置字体的字号
ctx.setFillStyle() 设置填充色
ctx.fillText() 在画布上绘制被填充的文本
ctx.draw() 将之前在绘图上下文中的描述(路径、变形、样式)画到canvas中

实现步骤:

  1. 放一个canvas容器,canvas-id用于后面的标识,以及海报样式;
# detail.wxml
<canvas class="sharePoster" canvas-id="poster"></canvas>
<view>
    <view>这里写了一个假的海报样式,展示给用户看</view>
    <view class="sava-img" catchtap='savePoster'>保存到本地</view>
</view>
  1. 用户点击生成海报,获取海报的详细信息,并且开始下载对应的网络图片资源,然后绘制canvas并保存图片;
const app = getApp();
getPosterInfo(id) {
    let _this = this;
    app.vote.getPosterInfo({id: id}).then(res => {
      //存入相关信息以及展示相应的状态
      this.setData({
        shareFlag: false,
        sharePoster: true,
        shareInfo: res.data
      });
      //下载对应的网络图片资源,并绘画canvas
      wx.downloadFile({
          url: this.data.shareInfo.url,
          success: function (res) {
            _this.setData({
              shareImg: res.tempFilePath
            });
          }
      })
      //设置延时器,确保图片下载完成
      setTimeout(() => {
        wx.getSystemInfo({
          success: function (res) {
            var v = 750 / res.windowWidth; //获取手机比例
            _this.drawPoster(v);    //开始绘制canvas
          }
        });
      }, 1000);
    });
}

绘制canvas, 代码较长,可以不用细看:

drawPoster(v) { //v-- 手机比例 ratio -- 像素比例
    let ratio = 0.5;
    let _this = this;
    //创建 canvas 的绘图上下文CanvasContext 对象
    let ctx = wx.createCanvasContext("poster", this);
    //绘制背景图片
    ctx.drawImage(this.data.imgs, 0, 0, 630 / v, 812 / v);
    ctx.save();
    ctx.beginPath();
    ctx.save();
    // 圆的圆心的 x 坐标和 y 坐标,48 / v 是半径,后面的两个参数就是起始弧度和结束弧度,这样就能画好一个圆
    ctx.arc(78 / v, 78 / v, 48 / v, 0, 2 * Math.PI);
    ctx.clip();
    //绘制头像
    ctx.drawImage(this.data.userImg, 30 / v, 31 / v, 96 / v, 96 / v);
    ctx.restore();
    ctx.setFontSize(30 / v);
    ctx.setFillStyle("white");
    ctx.fillText('正在参赛...', 150 / v, 65 / v);
    ctx.restore(); //恢复限制
    ctx.rect(30 / v, 157 / v, 570 / v, 380 / v);
    ctx.setLineJoin = "round";
    ctx.setLineWidth = 20 / v;
    //作品图片
    ctx.drawImage(worksImg, 30 / v, 157 / v, 570 / v, 380 / v);
    //作品名称
    ctx.setFontSize(40 / v);
    ctx.setFillStyle("white");
    ctx.fillText('作品名称', 30 / v, 598 / v, 560 / v);
    ctx.save();

    ctx.setFontSize(36 * ratio);
    ctx.setFillStyle("white");
    //可以尝试切割字符串,循环数组,达到换行的效果
    let info = this.data.shareInfo.opus_content;
    let len = 0;
    if (info.length > 15 && info.length < 30) {
      //两行以内
      for (var a = 0; a < 2; a++) {
        let content = info.substr(len, 15);
        len += 15;
        ctx.fillText(content, 30 * ratio, (658 + a * 48) / v);
      }
    } else if (info.length > 30) {
      //超过三行
      let con1 = info.substr(len, 15);
      let con2 = info.substr(15, 14) + "...";
      ctx.fillText(con1, 30 * ratio, 658 / v);
      ctx.fillText(con2, 30 * ratio, (658 + 48) / v);
    } else {
      //就一行
      ctx.fillText(info, 30 * ratio, 658 / v);
    }

    ctx.restore();
    //二维码
    ctx.save();
    ctx.fillRect(0 / v, 740 / v, 630 / v, 235 / v);
    ctx.drawImage(this.data.code, 30 / v, 761 / v, 153 / v, 153 / v);
    ctx.setFontSize(36 / v);
    ctx.setFillStyle("#666666");
    ctx.fillText("长按识别二维码", 210 / v, 826 / v);
    ctx.fillText("为好友加油,一起参赛!", 210 / v, 877 / v);
    ctx.restore();
    
    let width = 375;
    let height = 600;
    ctx.draw(true, () => {
      //仍然是图片保存下来,有些图片是空白的原因
      let timer = setTimeout(() => {
        //把当前画布指定区域的内容导出生成指定大小的图片
        wx.canvasToTempFilePath(
          {
            x: 0,
            y: 0,
            width: width,
            height: height,
            destWidth: width * v,
            destHeight: height * v,
            canvasId: "poster",
            // fileType: 'jpg',  //如果png的话,图片存到手机可能有黑色背景部分
            success(res) {
              //生成成功
              _this.setData({
                tempImg: res.tempFilePath
              });
              clearTimeout(timer);
            },
            fail: res => {
              //生成失败
              clearTimeout(timer);
            }
          },
          this
        );
      }, 100);
    });
  },
  1. 保存图片到本地,本来以为分分钟,没想到忘记了授权,不可粗心!!!
let _this = this;
wx.saveImageToPhotosAlbum({
    filePath: _this.data.tempImg,
    success(res) {
      wx.showToast(
        {
          title: "保存成功",
          icon: "success"
        },
        1000
      );
      _this.unshare();
      _this.shareOff(); //关闭海报窗口
    },
    fail(err) {
      //授权问题报错
      if (
        err.errMsg === "saveImageToPhotosAlbum:fail:auth denied" ||
        err.errMsg === "saveImageToPhotosAlbum:fail auth deny" ||
        err.errMsg === "saveImageToPhotosAlbum:fail authorize no response"
      ) {
        wx.showModal({
          title: "提示",
          content: "需要您授权保存相册",
          showCancel: false,
          success: modalSuccess => {
            wx.openSetting({
              success(settingdata) {
                //授权状态
                if (settingdata.authSetting["scope.writePhotosAlbum"]) {
                  wx.showToast(
                    {
                      title: "获取权限成功,再次点击即可保存",
                      icon: "none"
                    },
                    500
                  );
                } else {
                  wx.showToast(
                    {
                      title: "获取权限失败,将无法保存到相册哦~",
                      icon: "none"
                    },
                    500
                  );
                }
              },
              fail(failData) {
                console.log("failData", failData);
              }
            });
          }
        });
      }
    }
});

问题总结:

  1. 文本多行显示,超出隐藏: canvas好像不能自动截取,所以就自己计算文本长度,自己拼接省略号,然后通过for循环绘制;
  2. 测试生成的海报在部分手机生成清晰度不足,所以我就又重新设置宽高,计算比例;
  3. wx.canvasToTempFilePath()需要在draw()的回调里调用该方法才能保证图片导出成功.
  4. 授权相册的时候,如果一开始用户没有授权成功,后面要用wx.openSetting()去手动开启授权