微信小程序实现扫码并截图扫码图片转为base64的案例
实现功能为用户点击扫码会去扫描二维码,然后将扫描的场景图片相当于进行了一下拍照保存,然后将图片转为base64,然后将base64传给后端进行处理。目的是为了防止用户截图保存,翻拍以及拍照识别等作弊行为。因此在扫码的过程中需要拿到码信息和相关的场景图片。
不含精度
页面结构
两个文件
pages/index/index和pages/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();
},
});