微信小程序图片内容审核功能云函数+小程序端代码完整实现【填坑】

背景

开发微信小程序的同学如果涉及了社交类目,应该都会碰到由于缺少内容审核机制导致不过审的情况。微信小程序官方实际上提供了图片、文本、视频的审核接口,可以通过https和云函数两种方式调用,对于一些小项目的前端开发者而言,云函数是非常方便的一种选择。一般来说,文本检测的接口没什么难度,比较容易出问题的是图片检测的实现(视频接口本人没有涉及,不在考虑范围内)。基本集中在以下几个问题中:

  1. 接口要求media是formdata格式不一致
  2. media中value参数的Buffer类型不一致
  3. 图片过大导致的接口调用异常
  4. 前端图片缩放+裁剪实现
  5. 多张图片检测的并行策略问题,有兴趣可以私聊
  6. 找不到涉黄图片进行测试[滑稽],愿你有一个喜欢开车的盆友

如果你也遇到上述的问题,阅读本文将是你解决问题的最快方法。目前网上的各种资料质量参差不齐,很可能会带坑里去,我经过一天的摸索,总结出一套稳定可用的方法,供大家参考。

实现

技术栈

本人使用的是uniapp开发+云函数,如果是原生开发,写法基本一致,也可以参考思路。以下为实现过程。

小程序端

基本思路

小程序端的实现思路是:通过canvas接口,实现图片的缩放/裁剪;将canvas转为临时文件;将临时文件读取为string或者arraybuffer;调用云函数接口,上传图片数据,回调中根据errcode判断图片是否违规。
用Mermaid图表示如下:

chooseImage
getImageInfo
图片符合要求
drawImage
readFileSysc
canvasToTempFilePath
imgSecCheck

主要代码

/*
** @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])中的编码方式对应关系、以及实际测试结果如下:(测试通过表示,涉黄图片可以检测出来!)

readFileSyscNode备注测试通过
nullnullArrayBuffer格式
asciiascii仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。
base64Base64 编码。
binarylatin1 的别名。
hex将每个字节编码为两个十六进制字符。
ucs2/ucs-2ucs2utf16le 的别名。
utf16le/utf-16leutf16le2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
utf8/utf-8utf8多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。
latin1latin1一种把 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] 图片缩放思路来源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HouGISer

HouGiser需要你的鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值