背景
开发微信小程序的同学如果涉及了社交类目,应该都会碰到由于缺少内容审核机制导致不过审的情况。微信小程序官方实际上提供了图片、文本、视频的审核接口,可以通过https和云函数两种方式调用,对于一些小项目的前端开发者而言,云函数是非常方便的一种选择。一般来说,文本检测的接口没什么难度,比较容易出问题的是图片检测的实现(视频接口本人没有涉及,不在考虑范围内)。基本集中在以下几个问题中:
- 接口要求media是formdata格式不一致
- media中value参数的Buffer类型不一致
- 图片过大导致的接口调用异常
- 前端图片缩放+裁剪实现
- 多张图片检测的并行策略问题,有兴趣可以私聊
- 找不到涉黄图片进行测试[滑稽],愿你有一个喜欢开车的盆友
如果你也遇到上述的问题,阅读本文将是你解决问题的最快方法。目前网上的各种资料质量参差不齐,很可能会带坑里去,我经过一天的摸索,总结出一套稳定可用的方法,供大家参考。
实现
技术栈
本人使用的是uniapp开发+云函数,如果是原生开发,写法基本一致,也可以参考思路。以下为实现过程。
小程序端
基本思路
小程序端的实现思路是:通过canvas接口,实现图片的缩放/裁剪;将canvas转为临时文件;将临时文件读取为string或者arraybuffer;调用云函数接口,上传图片数据,回调中根据errcode判断图片是否违规。
用Mermaid图表示如下:
主要代码
/*
** @parms{imgPath: 图片URL,index:第几张图片(有一个imgs存所有图片的路径)}
** 用async/await写法,try/catch捕获异常
** author: Yu Li
** date: 2020/05/01
*/
async imageSecCheck(imgPath, index) {
try {
// 对wx.getImageInfo接口转promise写法,主要目的是获取图像的宽、高
let img = await wxGetImageInfo({
src: imgPath,
})
let info = await this.imgBoundCheckOpr(index, img, imgPath);
let path = await this.canvasToFileOpr(index, info);
await this.checkImgOpr(imgPath, path);
} catch (err) {
console.error(err)
}
}
/*
** 图片尺寸判断,满足则直接转buffer,不满足则利用canvas.drawImage函数缩放图片
** @parms{index:图片索引,img:图片信息,imgPath:图片URL}
*/
imgBoundCheckOpr(index, img, imgPath) {
let cW = img.width,
cH = img.height,
cHeight = cH,
cWidth = cW;
if ((cW / cH) < 0.56) { //说明 要依高为缩放比例--0.56是 750/1334的结果,这个是官方要求,但是实际上按照这个尺寸,有时还是会太大,因此按等比例规定最大尺寸为560/1000
if (cH > 1000) {
cHeight = 1000;
cWidth = parseInt((cW * 1000) / cH);
}
} else { //要依宽为缩放比例
if (cW > 560) {
cWidth = 560;
cHeight = parseInt((cH * 560) / cW);
}
}
// 图片尺寸满足要求,直接检测
if (cW == cWidth && cH == cHeight) {
this.checkImgOpr(imgPath, imgPath)
throw 'no need zoom img'
}
this.canvasList[index].save()
// set canvas w,h
this.imgCheck.cWidth.splice(index, 1, cWidth)
this.imgCheck.cHeight.splice(index, 1, cHeight)
return new Promise((resolve, reject) => {
// 此处timeout如果设成0,那么手机端第一次canvastotempfilepath就会失败,nextTick也会失败,设置一定时间解决问题
setTimeout(() => {
this.canvasList[index].drawImage(img.path, 0, 0, cW, cH, 0, 0, cWidth, cHeight);
this.canvasList[index].draw(false, function() {
resolve([cWidth, cHeight])
})
}, 200)
})
},
/*
** canvas转临时文件
** @parms{index:图片索引,用于组装canvasId,imgInfo:[width,height]}
*/
canvasToFileOpr(index, imgInfo) {
return new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvasId: 'canvasCur' + index,
x: 0,
y: 0,
width: imgInfo[0],
height: imgInfo[1],
destWidth: imgInfo[0],
destHeight: imgInfo[1],
quality: .8,
fileType: 'jpg',
success: rr => {
resolve(rr.tempFilePath)
},
fail: err => {
console.log(err)
}
}, this)
})
},
readFileSysc函数的编码方式和Node.js(云函数)中Buffer.from(string[,encode])中的编码方式对应关系、以及实际测试结果如下:(测试通过表示,涉黄图片可以检测出来!)
| readFileSysc | Node | 备注 | 测试通过 |
|---|---|---|---|
| null | null | ArrayBuffer格式 | ✅ |
| ascii | ascii | 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。 | ❌ |
| base64 | 同 | Base64 编码。 | ✅ |
| binary | 同 | latin1 的别名。 | ✅ |
| hex | 同 | 将每个字节编码为两个十六进制字符。 | ✅ |
| ucs2/ucs-2 | ucs2 | utf16le 的别名。 | ❌ |
| utf16le/utf-16le | utf16le | 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。 | ❌ |
| utf8/utf-8 | utf8 | 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。 | ❌ |
| latin1 | latin1 | 一种把 Buffer 编码成一字节编码的字符串的方式。 | ✅ |
五种可行编码方式的体积测试,用一张2248*1080图片进行测试,缩放后的尺寸为560*269,结果如下:
- binary: 37.6kb
- base64: 50.1kb
- latin1: 37.6kb
- hex: 75.2kb
- arraybuffer: 18.8kb
因此得出结论,在该尺寸约束下,无编码(ArrayBuffer)结构的图片体积最小,推荐使用。
/*
** 图片buffer获取,调用云函数
** @parms{originPath:图片原始URL,path:缩放后的图片URL}
*/
checkImgOpr(originPath, path) {
// 根据上述表格,encode可以设置为null,base64,binary,hex,latin1五种,设置为null表示转为ArrayBuffer
let encode = null
let buffer = wx.getFileSystemManager().readFileSync(path, encode);
let id = this.imgs.indexOf(originPath)
imgSecCheck({
contentType: 'image/jpeg',
buffer: buffer,
encode: encode
})
.then(res => {
console.log('图片合规')
this.imgsCheck.splice(id, 1, 'safe')
})
.catch(err => {
console.error(err)
this.imgsCheck.splice(id, 1, 'unsafe')
this.alertImgUnSafe()
})
},
/*
** 调用云函数,回调处理
** @parms{media:{contentType,buffer,encode}}
*/
export function imgSecCheck(media) {
return new Promise((resolve, reject) => {
wx.cloud.callFunction({
name: 'imgSecCheck',
data: {
media: media
},
complete: ress => { //异步结果
if (ress.result.errCode == 87014) { //内容不正常
reject(ress.result)
} else {
resolve()
}
}
})
})
}
云函数
云函数的config.json和index.js需要修改:
// config.json
{
"permissions": {
"openapi": [
"security.imgSecCheck"
]
}
}
// index.js
// 云函数入口文件
const cloud = require('wx-server-sdk');
cloud.init()
// 云函数入口函数
exports.main = async(event, context) => {
const wxContext = cloud.getWXContext()
try {
const result = await cloud.openapi.security.imgSecCheck({
media:{
contentType:event.media.contentType,
value:Buffer.from(event.media.buffer,event.media.encode)// Buffer.from()是必须的
}
})
// result 结构
// { errCode: 0, errMsg: 'openapi.templateMessage.send:ok' }
return result
} catch (err) {
// 错误处理
// err.errCode !== 0
return err
}
}
思考
以上是我5.1劳动节的主要成果,当然另外一部分成果在于如何处理多张图片异步检测,与保存操作之间的逻辑关系处理,不是本文重点,我就不详述了。
按照官方文档,主要的问题在于media是FormData类型的,导致先入为主地找了很多小程序传Formdata格式的文章,浪费了很多时间。其实仔细往下看,可以看到media中的value是Buffer类型的,所以关键要理解Buffer类型的图片怎么生成。可以很快找到readFile这个接口可以生成Buffer,有string和arraybuffer两种类型,面对这么多的编码,你可能就会有疑问要选哪一种。另一方面,云函数环境中,有一个Buffer.from()的函数,那么这个函数要和readFile怎么才能对应起来?你可能很容易忽略Buffer.from()到底有几种参数类型,其实细心一点你就可以发现,这俩函数完全就是一一对应的关系啊!
到这里,问题就迎刃而解了。
如果这篇博文帮助到你了,请你点个赞,谢谢
主要参考
[1] wx.canvasToTempFilePath接口报fail:create bitmap failed"问题
[2] security.imgSecCheck接口返回-404012错误
[3] Buffer用法
[4] 图片Buffer获取思路来源(该文结论是错误的,因为在云函数Buffer.from()中没有设置编码)
[5] 图片缩放思路来源

3549

被折叠的 条评论
为什么被折叠?



