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,
    })
  },
});

优化上个版本问题,兼容了鸿蒙的抓拍

首页

首页wxml

js
<button bind:tap="bindViewTap">开始扫码并截取图片</button>

<button style="margin-top: 20rpx;" bind:tap="bindPhotoTap">开始拍照转base64图片</button>
<view wx:if="{{QRText}}">扫码内容:{{QRText}}</view>
<view wx:if="{{base64}}">扫码图片:</view>
<image src="{{base64}}" mode="widthFix" style="width: 100vw;" wx:if="{{base64}}" />
<view wx:if="{{pzbase64}}">拍照图片(不含有识别码内容):</view>
<image src="{{pzbase64}}" mode="widthFix" style="width: 100vw;" wx:if="{{pzbase64}}" />

首页js

js
Page({
  data: {
    base64:'', // 扫码识别图片
    pzbase64:'', // 拍照图片
    QRText:'', // 扫码内容
  },
  bindViewTap() {
    const that = this
    wx.navigateTo({
      url: "../scan/scan",
      events: {
        acceptDataFromB: (data) => {
          console.log("首页获取的扫码结果:", data);
          that.setData({
            QRText:data.QRText,
            base64: data.base64
          })
        },
      },
    });
  },
  bindPhotoTap(){
    const that = this;
    wx.chooseMedia({
      count: 1,
      mediaType: ['image'],
      sourceType: ['camera'],
      sizeType: ['compressed'],
      success: res => {
        this.setData({
          isPop_2: 0,
        })
        const tempPath = res.tempFiles[0].tempFilePath;
        const base64 = wx.getFileSystemManager().readFileSync(tempPath, 'base64'); // 2. 同步读成 base64
        const dataURL = `data:image/jpeg;base64,${base64}`; // 3. 拼前缀
        that.setData({
          pzbase64: dataURL
        })
      },
      fail: error => {

      }
    })
  }
})

扫码页面

扫码页面wxml

js
<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:100vw;height:90vh;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>

扫码页面wxss

js
.body_view {
  width: 70vw;
  height: 70vw;
  position: relative;
  top: -10vw;
}

.bg {
  width: 100vw;
  height: 90vh;
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  flex-flow: column nowrap;
  align-items: center;
  justify-content: center;
}

.view_11 {
  width: 10vw;
  height: 1vw;
  background: #02DA6B;
  position: absolute;
  top: 0;
  left: 0;
}

.view_12 {
  width: 10vw;
  height: 1vw;
  background: #02DA6B;
  position: absolute;
  top: 0;
  right: 0;
}

.view_13 {
  width: 10vw;
  height: 1vw;
  background: #02DA6B;
  position: absolute;
  bottom: 0;
  left: 0;
}

.view_14 {
  width: 10vw;
  height: 1vw;
  background: #02DA6B;
  position: absolute;
  bottom: 0;
  right: 0;
}

.view_15 {
  width: 1vw;
  height: 10vw;
  background: #02DA6B;
  position: absolute;
  top: 0;
  left: 0;
}

.view_16 {
  width: 1vw;
  height: 10vw;
  background: #02DA6B;
  position: absolute;
  top: 0;
  right: 0;
}

.view_17 {
  width: 1vw;
  height: 10vw;
  background: #02DA6B;
  position: absolute;
  bottom: 0;
  left: 0;
}

.view_18 {
  width: 1vw;
  height: 10vw;
  background: #02DA6B;
  position: absolute;
  bottom: 0;
  right: 0;
}

.text_2 {
  color: #02DA6B;
}

扫码页面js

js
let timer = null; // 用于存储节流定时器
let flag = true; // 节流阀
Page({
  data: {
    QRText: '', // 抓拍图片内容
    base64: '', // 抓拍图片base64
    flag: false, //是否继续接收媒体流
    cWidth: 0,
    cHeight: 0 // 抓拍图片宽高
  },
  onLoad(options) {

  },
  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();
  },
  async changeDataToBase64(frame) {
    const base64 = await this.frameToBase64(frame);
    // 保存base64
    this.setData({
      base64
    })
    const eventChannel = this.getOpenerEventChannel();
    eventChannel.emit("acceptDataFromB", {
      base64,
      QRText: this.data.QRText
    });
    // 返回页面再上传文件
    wx.navigateBack();
  },
  // 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);
    });
  },

  onShow() {

  },
  // 停止扫码
  cameraHiddenTap() {
    this.data.listener.stop();
  },
});