Skip to content
微信小程序实现扫码并截图扫码图片转为base64的案例

实现功能为用户点击扫码会去扫描二维码,然后将扫描的场景图片相当于进行了一下拍照保存,然后将图片转为base64,然后将base64传给后端进行处理。目的是为了防止用户截图保存,翻拍以及拍照识别等作弊行为。因此在扫码的过程中需要拿到码信息和相关的场景图片。

页面结构

两个文件pages/index/indexpages/scan/scan分别为首页和扫码页面。

首页代码

pages/index/index.wxml代码如下

html
<!--pages/index/index.wxml-->
<button bind:tap="goScan">去扫码</button>
<text wx:if="{{data!=''}}">扫码结果: {{data}}</text>
<text wx:if="{{filePath}}">扫码的base64图片</text>
<image wx:if="{{filePath}}" src="{{filePath}}"></image>

page/index/index.js代码如下

js
Page({
    data: {
        filePath: "",
        data:""
    },
    goScan() {
        const that = this;
        wx.navigateTo({
            url: "../scan/scan",
            events: {
                acceptDataFromB: (data) => {
                    console.log("首页获取的扫码结果:", data);
                    const fs = wx.getFileSystemManager();
                    fs.readFile({
                        filePath: data.filePath,
                        encoding: 'base64', // 指定读取文件的编码为base64
                        success(res) {
                            // 下面打印的这个base64就是完整的截图的图片
                            console.log("data:image/jpeg;base64," + res.data);
                            // 上传图片...
                            that.setData({
                                filePath: "data:image/jpeg;base64," + res.data,
                                data:JSON.stringify(data)
                            })
                        },
                        fail(err) {
                            console.error("文件读取失败:", err);
                        }
                    })
                },
            },
        });
    },
});

扫码页面代码

pages/scan/scan.wxml代码如下

html
<camera device-position="back" flash="off" binderror="error" 
style="height: 100vh;width: 100vw;" mode="scanCode" bindscancode="onGetCode" frame-size="small"></camera>
<!-- 做一个扫码框框 -->
<view class="qrCodeKuang"></view>

pages/scan/scan.wxss代码如下

css
/* subF/pages/scanCode/scanCode.wxss */
.qrCodeKuang {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 500rpx;
    height: 500rpx;
    background: linear-gradient(#ffffff, #ffffff) left top,
        linear-gradient(#ffffff, #ffffff) left top,
        linear-gradient(#ffffff, #ffffff) right top,
        linear-gradient(#ffffff, #ffffff) right top,
        linear-gradient(#ffffff, #ffffff) right bottom,
        linear-gradient(#ffffff, #ffffff) right bottom,
        linear-gradient(#ffffff, #ffffff) left bottom,
        linear-gradient(#ffffff, #ffffff) left bottom;
    background-repeat: no-repeat;
    background-size: 6rpx 60rpx, 60rpx 6rpx;
}

pages/scan/scan.js代码如下

js
Page({
    onLoad() {
      // 帧数组
      this.frameList = [];
      this.isEnd = false;
  
      // 相机上下文对象
      this.cameraContext = wx.createCameraContext();
      this.cameraListener = this.cameraContext.onCameraFrame((frame) => {
        // 存储一张帧图片
        if (this.frameList.length === 0) {
          this.frameList.push({
            data: frame.data,
            width: frame.width,
            height: frame.height,
          });
          this.stopGetFrame();
        }
      });
    },
    // 获取到扫码结果
    onGetCode(e) {
      console.log("扫码页面获取到扫码结果", e);
      this.qrCodeResult = e.detail.result;
      if (this.frameList.length === 0 && !this.isEnd) this.startGetFrame();
    },
    // 开始获取帧图片
    startGetFrame() {
      this.cameraListener.start();
    },
    // 停止监听帧数据
    stopGetFrame() {
      this.cameraListener.stop();
      this.transferFrame();
    },
    // frame转base64
    frameToBase64(frameInfo) {
      const frameWidth = frameInfo.width;
      const frameHeight = frameInfo.height;
      return new Promise((resolve, reject) => {
        if (!frameInfo) {
          reject(new Error("No frame data available"));
          return;
        }
        // 创建离屏 canvas
        const offscreenCanvas = wx.createOffscreenCanvas({
          type: "2d",
          width: frameWidth,
          height: frameHeight,
        });
        const ctx = offscreenCanvas.getContext("2d");
        // 创建 ImageData 对象
        const imgData = ctx.createImageData(frameWidth, frameHeight);
        // 将 ArrayBuffer 数据复制到 ImageData
        const uint8Array = new Uint8Array(frameInfo.data);
        imgData.data.set(uint8Array);
        // 将 ImageData 绘制到 canvas
        ctx.putImageData(imgData, 0, 0);
        // 将 canvas 内容转换为 base64
        const base64 = offscreenCanvas.toDataURL("image/png");
        resolve(base64);
      });
    },
    // 去掉前缀
    removeBase64Prefix(base64Data) {
      return base64Data.replace(/^data:image\/[a-z]+;base64,/, "");
    },
    // 处理帧数据
    async transferFrame() {
      console.log("获取到帧数据", this.frameList);
      const fs = wx.getFileSystemManager();
      let filePath = `${wx.env.USER_DATA_PATH}/example.png`;
      const base64 = this.removeBase64Prefix(
        await this.frameToBase64(this.frameList[0])
      );
      fs.writeFile({
        filePath,
        encoding: "base64",
        data: base64,
        success: (res) => {
          console.log("写入成功", res);
          const eventChannel = this.getOpenerEventChannel();
          eventChannel.emit("acceptDataFromB", {
            data: this.qrCodeResult,
            filePath,
          });
          this.frameList = [];
          this.frameList.length = 0;
          this.isEnd = true;
          // 返回页面再上传文件
          wx.navigateBack();
        },
        fail(err) {
          console.error("写入失败", err);
          this.frameList = [];
          this.frameList.length = 0;
        },
      });
    },
  });

扫码页面代码(优化后的版本,上面那个版本抓拍图片模糊,这个版本解决了模糊问题)

wxml代码如下

html
<camera bindscancode="bindscancode" binderror="binderror" bindinitdone="bindinitdone" id="cameraId" mode="scanCode" device-position="back" frame-size="large" resolution="high" flash="off" style="width: 100vw; height: 100vh;"></camera>

<!-- 抓拍图片,定位到外面防止用户看到,抓拍图片要使用canvas -->
<canvas canvas-id="myCanvas" id="myCanvas" style="width:{{cWidth}}px;height:{{cHeight}}px;position: fixed;left:200vw;top:200vh;" />

<!-- 做一个扫码框框 -->
<view class="bg">
    <view class="body_view">
        <view class="view_11"></view>
        <view class="view_12"></view>
        <view class="view_13"></view>
        <view class="view_14"></view>
        <view class="view_15"></view>
        <view class="view_16"></view>
        <view class="view_17"></view>
        <view class="view_18"></view>
    </view>
    <view style="height: 3vw;"></view>
    <view class="text_2">请把二维码置于框内</view>
</view>

js代码如下

js
const message = require("../../../utils/message");
const {
  cherishedPrizePool,
  serenaPrizePool,
  logData,
  sendData
} = require('../../../utils/theme')
const app = getApp()
let timer = null; // 用于存储节流定时器
let flag = true; // 节流阀
Page({
  data: {
    QRText: '', // 抓拍图片内容
    base64: '', // 抓拍图片base64
    flag: false, //是否继续接收媒体流
    cWidth: 0,
    cHeight: 0 // 抓拍图片宽高
  },
  onLoad(options) {
    if (options.CRMID && options.unionid) {
      this.setData({
        CRMID: options.CRMID,
        unionid: options.unionid
      })
    }
  },
  binderror(e) {
    wx.getPrivacySetting({
      success: res => {
        if (res.needAuthorization) {
          this.cameraHiddenTap();
        } else {
          message.showModalWithoutCancel('提示', '亲,请在手机设置中打开微信的「相机」授权然后就可以正常参与活动啦!', () => {
            this.cameraHiddenTap();
          });
        }
      },
      fail: res => {},
    })
  },
  bindinitdone(e) {
    console.log('初始化完成');
    this.toScan()
  },
  bindscancode(e) {
    if (this.data.QRText) return;

    this.data.QRText = e.detail.result;
    if (this.data.QRText) {
      this.setData({
        QRText: e.detail.result,
      })
    }
  },
  toScan() {
    this.setData({
      QRText: '',
      base64: '',
      flag: false, //是否继续接收媒体流
    })
    this.createCameraContext();
  },
  createCameraContext() {
    const context = wx.createCameraContext()
    let that = this
    this.data.listener = context.onCameraFrame((frame) => {
      if (that.data.flag) return;
      if (that.data.QRText) {
        that.setData({
          flag: true,
        })
        that.changeDataToBase64(frame);
      }
    })
    this.data.listener.start();
  },
  // 节流函数
  sleep(ms) {
    // 返回promise
    return new Promise((resolve, reject) => {
      // 节流
      if (flag) {
        flag = false;
        timer = setTimeout(() => {
          flag = true;
          clearTimeout(timer);
          timer = null;
          resolve();
        }, ms);
      }
    });
  },
  async changeDataToBase64(frame) {
    var fwith = Math.trunc(frame.width);
    var fheight = Math.trunc(frame.height);
    this.setData({
      cWidth: fwith,
      cHeight: fheight,
    })
    await this.sleep(1000) // 停留一秒获取相机的宽高
    var data = new Uint8Array(frame.data.slice(0));
    var clamped = new Uint8ClampedArray(data);
    let that = this
    wx.canvasPutImageData({
      canvasId: 'myCanvas',
      x: 0,
      y: 0,
      width: fwith,
      height: fheight,
      data: clamped,
      success: (res) => {
        // 转换临时文件
        wx.canvasToTempFilePath({
          x: 0,
          y: 0,
          width: fwith,
          height: fheight,
          canvasId: 'myCanvas',
          fileType: 'jpg',
          destWidth: fwith,
          destHeight: fheight,
          // 精度修改
          quality: 1,
          success: (res) => {
            // 临时文件转base64
            wx.getFileSystemManager().readFile({
              filePath: res.tempFilePath, //选择图片返回的相对路径
              encoding: 'base64', //编码格式
              success: res => {
                // 保存base64
                that.data.base64 = res.data;
                // 这里拿到的base64就是不模糊的,手机拍的啥样就啥样
                console.log("data:image/jpeg;base64," + res.data)
              }
            })
          },
          fail(res) {
            this.setData({
              flag: false,
            })
            console.log(err, 2)
          }
        }, that)
      },
      fail: (err => {
        console.log(err, 1)
        this.setData({
          flag: false,
        })
      })
    })
  },
  onShow() {

  },
  cameraHiddenTap() {
    this.data.listener.stop();
    this.setData({
      isScanCode: false,
    })
  },
});