Uniapp微信小程序使用canvas绘制并下载图片

背景

最近项目中有一个下载图片和一个分享海报的功能,遇到了一些问题,比如海报图片生成失败、图片下载是空白的、多张图片下载时下载的还是前一张图片等等。觉得过程挺有意思的,记录一下。

创建容器及样式

<view class="canvas-container">
      <canvas
        :style="{ width: canvasW + 'px', height: canvasH + 'px' }"
        canvas-id="myCanvas"
        id="myCanvas01"
      >
      </canvas>
    </view>
    <view class="convas-image">
      <canvas :style="{ width: canvasW + 'px',height: canvasH + 'px'}" canvasid="myCanvas2" id="myCanvas02">
      </canvas>
    </view>

由于要做移动端适配,故容器的宽高是动态的。

.canvas-container {
  position: fixed;
  top: 9999999px;
  #myCanvas01 {
    background: rgba(255, 255, 255, 0); /*关键点*/
  }
}
.convas-image {
  position: fixed;
  top: 9999999px;
  #myCanvas02 {
    background: rgba(255, 255, 255, 0); /*关键点*/
  }
}

因为这两个容器实际上都是不显示在页面上的,如果使用hidden或者v-if隐藏的话多多少少都会有些问题,所以使用固定定位使容器脱离文档流。

准备工作

    // 获取设备信息
    getSystemInfo() {
      return new Promise((req, rej) => {
        uni.getSystemInfo({
          success: function (res) {
            req(res);
          },
        });
      });
    },
 getImageInfo(image) {
      console.log(`output->image`, image);
      return new Promise((req, rej) => {
        uni.getImageInfo({
          src: image,
          success: function (res) {
            req(res);
          },
          fail: (err) => {
             uni.showToast({
               icon: "error",
               mask: true,
               title: "获取照片失败",
             });
            console.log("****", err);
          },
        });
      });
    },

获取设备信息及辅助函数,因为canvas中使用的图片为网络方式返回,此函数确保图片被正常读取后再绘制,后文有用到。

海报的绘制

async canvas2dFun(retryCount = 3) {
      this.SystemInfo = await this.getSystemInfo();
      const w = this.SystemInfo.windowWidth / 750;
      let goodsImgPath = await this.getImageInfo(this.imgObj.urls[0]);//海报主图
      let ewmImgPath = await this.getImageInfo(this.imgObj.qrCode);//海报二维码
      let posterPath = await this.getImageInfo(
        `${this.imgpath}/static_pro/create/poster.png`
      );//海报边框
      let logoPath = await this.getImageInfo(
        `${this.imgpath}/static_pro/create/logo.png`
      );//logo

      this.canvasW = 710 * w;
      this.canvasH = 1124 * w;

      if (this.SystemInfo.errMsg == "getSystemInfo:ok") {
        console.log("读取图片信息成功");
        var ctx = uni.createCanvasContext("myCanvas", this);
        // 1.填充背景色,白色
        // 设置画布透明度
        ctx.setFillStyle("rgba(255, 255, 255, 1)"); // 默认白色
        ctx.fillRect(0, 0, this.canvasW, this.canvasH); // fillRect(x,y,宽度,高度)

        ctx.drawImage(
          goodsImgPath.path,
          5 * w,
          5 * w,
          this.canvasW - 15 * w,
          this.canvasH - 15 * w
        ); // drawImage(图片路径,x,y,绘制图像的宽度,绘制图像的高度)
        ctx.drawImage(posterPath.path, 0, 0, this.canvasW, this.canvasH); // drawImage(图片路径,x,y,绘制图像的宽度,绘制图像的高度,二维码的宽,高)
        ctx.drawImage(
          logoPath.path,
          this.canvasW - this.ewmW * w - 460 * w,
          this.canvasH - this.ewmW * w - 100 * w,
          this.logoW * w - 30 * w,
          this.logoH * w - 30 * w
        ); //logo
        ctx.drawImage(
          ewmImgPath.path,
          this.canvasW - this.ewmW * w - 460 * w,
          this.canvasH - this.ewmW * w - 16 * w,
          this.ewmW * w - 30 * w,
          this.ewmW * w - 30 * w
        ); // drawImage(图片路径,x,y,绘制图像的宽度,绘制图像的高度,二维码的宽,高)
        ctx.font = "bold 18px Arial";
        ctx.fillStyle = "#222222";
        ctx.fillText(
          "微信扫码 XX相机",
          this.canvasW - this.ewmW * w - 280 * w,
          this.canvasH - this.ewmW * w + 50 * w
        );
        ctx.font = "normal 12px Arial";
        ctx.fillText(
          "这是我的写真,你也来试一试吧",
          this.canvasW - this.ewmW * w - 280 * w,
          this.canvasH - this.ewmW * w + 100 * w
        );
        // draw方法 把以上内容画到 canvas 中
          ctx.draw(true, (ret) => {
            uni.canvasToTempFilePath(
              {
                // 保存canvas为图片
                canvasId: "myCanvas",
                quality: 1,
                success: (res) => {
                  console.log("生成海报-》", res);
                  uni.setStorageSync("filePath", res.tempFilePath); // 保存临时文件路径到缓存
                },
                fail: (error) => {
                  console.log("生成海报失败-》", error);
                  if (retryCount > 0) {
                    console.log(`重试剩余次数: ${retryCount - 1}`);
                    this.canvas2dFun(retryCount - 1)
                  }
                },
              },
              this
            );
        });
      } else {
        console.log("读取图片信息失败");
      }
    },

绘制完成之后将绘制的图片存入filePath临时路径中,方便后面下载等操作。

测试阶段发现,部分安卓机型在canvas第一次绘制时会报wx.canvasToTempFilePath:create bitmap failed,可能是不分机型的性能使得图形绘制失败,图片保存下来也是纯白色的底图。在这里fail回调处理了如果生成失败再生成一次的操作。

shareFirendCircle() {
      let posterUrl = uni.getStorageSync("filePath");
      uni.showShareImageMenu({
        path: posterUrl, //图片地址必须为本地路径或者临时路径
        success: (re) => {
          console.log({ success: re });
        },
        fail: (re) => {
          console.log({ fail: re });
        },
      });
    },

页面按钮点击后调用函数唤起分享框,函数中的posterUrl为canvas绘制的图片。

图片的下载

async canvas2dImage(retryCount = 3) {
      this.SystemInfo = await this.getSystemInfo();
      const w = this.SystemInfo.windowWidth / 750;
      let goodsImgPath = await this.getImageInfo(this.imgObj.urls[this.swiperIndex]);
      let logoPath = await this.getImageInfo(
        `${this.imgpath}/static_pro/create/logo.png`
      );

      this.canvasW = 710 * w;
      this.canvasH = 1124 * w;

      if (this.SystemInfo.errMsg == "getSystemInfo:ok") {
        console.log("读取图片信息成功");
        var ctx = uni.createCanvasContext("myCanvas2", this);
        // 1.填充背景色,白色
        // 设置画布透明度
        ctx.setFillStyle("rgba(255, 255, 255, 1)"); // 默认白色
        ctx.fillRect(0, 0, this.canvasW, this.canvasH); // fillRect(x,y,宽度,高度)

        ctx.drawImage(
          goodsImgPath.path,
          5 * w,
          5 * w,
          this.canvasW - 15 * w,
          this.canvasH - 15 * w
        ); // drawImage(图片路径,x,y,绘制图像的宽度,绘制图像的高度)
        ctx.drawImage(
          logoPath.path,
          this.canvasW - this.ewmW * w - 480 * w,
          this.canvasH - this.ewmW * w + 100 * w,
          this.logoW * w - 30 * w,
          this.logoH * w - 30 * w
        ); //logo
        // draw方法 把以上内容画到 canvas 中
        return new Promise((resolve, reject) => {
          ctx.draw(false, (ret) => {
              uni.canvasToTempFilePath(
                {
                  // 保存canvas为图片
                  canvasId: "myCanvas2",
                  quality: 1,
                  success: (res) => {
                    console.log("生成高清图-》", res);
                    uni.setStorageSync("ImagePath", res.tempFilePath); // 保存临时文件路径到缓存
                    resolve();
                  },
                  fail: (error) => {
                    console.log("生成高清图失败-》", error);
                    if (retryCount > 0) {
                      console.log(`重试剩余次数: ${retryCount - 1}`);
                      this.canvas2dImage(retryCount - 1)
                        .then(resolve)
                        .catch(reject);
                    } else {
                      reject(error);
                    }
                  },
                },
                this
              );
          });
        });
      } else {
        console.log("读取图片信息失败");
      }
    },

这里使用Promise的原因是有多张图片需要单独下载,切换图片点击下载按钮开始绘制图像,确保在图像canvas生成完毕后继续下载的步骤。

canvas绘制方面与上面大差不差,主要是下载方面需要单独配置

 async dowloadImg() {
      if (!this.$utils.getUserInfo().isVip) {
        //无会员
        this.openPlusAlert = true;
      } else {
        uni.showLoading({
          title: "照片保存中",
        });
        await this.canvas2dImage();
        let that = this;
        // 获取用户是否开启 授权保存图片到相册。
        uni.getSetting({
          success(res) {
            console.log("已知权限", res);
            uni.hideLoading();
            // 如果没有授权
            if (!res.authSetting["scope.writePhotosAlbum"]) {
              // 则拉起授权窗口
              uni.authorize({
                scope: "scope.writePhotosAlbum",
                success() {
                  that.saveImage();
                },
                fail(error) {
                  //点击了拒绝授权后--就一直会进入失败回调函数--此时就可以在这里重新拉起授权窗口
                  console.log("拒绝授权则拉起弹框", error);
                  uni.showModal({
                    title: "提示",
                    content: "若点击不授权,将无法保存图片",
                    cancelText: "不授权",
                    cancelColor: "#999",
                    confirmText: "授权",
                    confirmColor: "#f94218",
                    success(res) {
                      console.log(res);
                      if (res.confirm) {
                        // 选择弹框内授权
                        uni.openSetting({
                          success(res) {
                            console.log(res.authSetting);
                          },
                        });
                      } else if (res.cancel) {
                        // 选择弹框内 不授权
                        console.log("用户点击不授权");
                      }
                    },
                  });
                },
              });
            } else {
              // 有权限--直接保存
              console.log("有权限  直接调用相应方法");
              uni.hideLoading();
              that.saveImage();
            }
          },
          fail: (error) => {
            console.log(
              "调用微信的查取权限接口失败,并不知道有无权限!只有success调用成功才只知道有无权限",
              error
            );
            uni.hideLoading();
            uni.showToast({
              title: error.errMsg,
              icon: "none",
              duration: 1500,
            });
          },
        });
      }
    },

这里是点击下载绑定的函数,主要作用是判断是否开启了权限。

  saveImage() {
      let filePath = uni.getStorageSync("ImagePath"); //从缓存中读取临时文件路径
      wx.saveImageToPhotosAlbum({
        filePath: filePath,
        success(res) {
          uni.showToast({
            icon: "success",
            mask: true,
            title: "保存到相册了",
          });
        },
        fail(res) {
          console.log(res.errMsg);
        },
      });
    },

保存图片到本地的操作。中心思想还是把canvas生成的临时路径下载保存到本地。

小结

这样就完成了上面轮播图切换时下载对应的图片,以及海报的分享与下载功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值