- 这些 Canvas 小技巧,保证你新年用得上
来自「微信开发者」公众号,作者为微信小程序技术研发工程师binnie。 本文主要介绍了3个隐藏的 Canvas 小技巧: - 绘制并生成图片 - Video 绘制 Canvas / webgl - 视频解码并绘制到 webgl - 录制并导出 webgl 视频 一键加滤镜 快速合成音视频 轻松挑选视频封面 …… Canvas 能够做这些? 作为资深的开发者,相信大家对 Canvas 都不陌生。这项能力在绘制图形方面发挥着极大的作用,高效支持图片编辑、数据可视化等应用场景。但是只局限于一般能力应用,那格局就小了。 Canvas 的应用场景非常丰富!赶紧往下看看这些隐藏的 Canvas 小技巧,保证你新年用得上!还有手把手教程以及文末彩蛋哟。 -- • 绘制并生成图片 • -- [图片] 示例:新年模板长按保存祝福 适用场景:图片分享海报 相关 API:RenderingContext/Canvas/wx.canvasToTempFilePath Step 1: 创建实例获取对象 创建 Canvas 实例,获取 CanvasRenderingContext2D 对象(Canvas 绘图上下文)来绘制形状、文本、图像等。 const query = wx.createSelectorQuery() let canvas = null query.select('#myCanvas') .fields({ node: true, size: true }) .exec((res) => { // 通过 wx.createSelectorQuery 获取到 canvas 实例 canvas = res[0].node // 通过 canvas.getContext('2d') 获取 CanvasRenderingContext2D 对象 const ctx = canvas.getContext('2d') }) Step 2: 设置宽高调整图片 获取 Canvas 绘图上下文后,将 Canvas 的宽高设置为节点宽高 * 设备像素比,绘制出来的图片更清晰 // 获取设备像素比 const dpr = wx.getSystemInfoSync().pixelRatio // 将 canvas 宽高设置为 canvas.width = res[0].width * dpr canvas.height = res[0].height * dpr Step 3: 绘制内容 使用 CanvasRenderingContext2D 绘制,根据业务需要在画布中绘制头像、文字、背景等 // 矩形 ctx.fillStyle = '#FFFFFF' ctx.fillRect(0, 0, canvas.width , canvas.height ) // 图片 var image = canvas.createImage() himage.src = 'https://example.com/example.jpg' headImage.onload = (res) => { ctx.drawImage(himage 0, 0, 32, 32; } // 文本 ctx.font = "18px SimHei"; ctx.textAlgin = "left" ctx.fillStyle = "#07c160"; ctx.fillText("这是我的名字", 0, 0); Step 4: 生成并保存本地 使用 wx.canvasToTempFilePath 将画布生成图片,wx.saveImageToPhotosAlbum 将图片保存到本地。 wx.canvasToTempFilePath({ canvas: canvas, // canvas 实例 success(res) { // canvas 生成图片成功 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success(res) { // 保存成功 } }) } }) -- • Video 绘制 Canvas / webgl • -- [图片] 示例:视频文件绘制 Canvas 适用场景:制作 Video 滤镜、挑选 Video 封面等 相关 API:RenderingContext/Canvas Step 1: 获取实例 通过 wx.createSelectorQuery 获取 VideoContext 实例 let video = null wx.createSelectorQuery().select('#video').context(res => { // 通过 wx.createSelectorQuery 获取 VideoContext 实例 video = res.context; }) Step 2: 绘制内容 获取 VideoContext 实例后,将 VideoContext 传递给 Canvas 进行绘制。开发者根据业务需求选择绘制类型: Canvas 2d 写法:canvas.drawImage(video, ...)webgl 写法:gl.texImage2D(..., video) wx.createSelectorQuery().selectAll('#myCanvas,#webglCanvas').node(res => { const ctx = res[0].node.getContext('2d') const gl = res[1].node.getContext('webgl') setInterval(() => { // canvas 2d // 将 video 纹理对象传入 drawImage 进行绘制 ctx1.drawImage(video, 0, 0, w * dpr, h * dpr); // 添加一个蒙层 ctx1.fillStyle = 'rgba(0, 0, 0, 0.3)' ctx1.fillRect(0, 0, w * dpr, h * dpr); // webgl const render = createRenderer(res[1].node, w, h) render(new Uint8Array(ctx1.getImageData(0, 0, w * dpr, h * dpr).data), w * dpr, h * dpr) }, 1000 / 24) }).exec() function createRenderer(canvas, width, height) { const gl = canvas.getContext("webgl") ... return (arrayBuffer, width, height) => { ... // 指定二维纹理图像 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, arrayBuffer) gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) } } -- • 视频解码并绘制到 webgl • -- [图片] 示例:视频一键解码并绘制到 webgl 适用场景:添加特效、贴图等视频编辑场景 相关 API:wx.createVideoDecoder/VideoDecoder/RenderingContext/Canvas.requestAnimationFrame/wx.createMediaAudioPlayer/MediaAudioPlayer Step 1: 创建视频解码器进行解码 1. 调用 createVideoDecoder 对视频进行解码 2. 使用 videodecoder.start 启动解码,视频源文件不限制本地或远程路径 3. 通过 videodecoder.on('start', res => {}) 监听解码,通过 videodecoder.getFrameData() 获取到解码数据 // 获取视频解码器 getVideoDecoder(source, abortAudio) { return new Promise((resolve, reject) => { // 创建视频解码器 videodecoder = wx.createVideoDecoder() // 开始解码 videodecoder.start({ abortAudio: abortAudio, source: source, // 视频源文件,支持本地路径&远程路径 mode: 0 // 按pts解码,保证音画同步 }) // 监听解码 开始 videodecoder.on('start', res => { console.log('videodecoder start', res) // 状态初始化 isStop = false resolve(videodecoder) }) // 监听解码 结束 videodecoder.on('ended', res => { // 状态设置为结束,停止画面录制器 isStop = true }) }) }, Step 2: 解码数据绘制到 webgl 1. 通过 gl.texImage2D(..., image) 将解码数据绘制到 webgl 2. 使用 webgl.requestAnimationFrame 继续绘制,效果更加流畅 // 将解码数据绘制到 webgl 中 const query = wx.createSelectorQuery() query.select('#webglCanvas').node().exec((res) => { const webgl = res[0].node const requestAnimationFrame = webgl.requestAnimationFrame; // 初始化webgl let render = null if (!render) { render = createRenderer(webgl, 600, 400) } /** * 绘制视频帧到 canvas */ let i = 1 let loop = () => { // 解码结束,停止循环 if (isStop) { return } // 获取解码数据,绘制到 webgl 中 const imageData = videodecoder.getFrameData() if (imageData) { // render 的高宽需要设置为图片的宽高才可以绘制出来 render(new Uint8Array(imageData.data), imageData.width, imageData.height) } // 继续绘制 console.log('绘制帧数:', i++) requestAnimationFrame(loop) } // 启动录制循环 requestAnimationFrame(loop) }) Step 3: 添加音频播放器同步播放音频 完成 Step2 后,webgl 只有视频播放,缺少音频。因此使用 wx.createMediaAudioPlayer(),支持 addAudioSource 传入 videodecoder,保证视频帧渲染音画同步 /** * 创建媒体音频播放器 */ let mediaAudioPlayer = null let addAudio = () => { if (mediaAudioPlayer) return mediaAudioPlayer = wx.createMediaAudioPlayer() mediaAudioPlayer.start().then(() => { // 添加播放器音频来源 mediaAudioPlayer.addAudioSource(videodecoder).then(res => { console.log('add mediaAudioPlayer: ',) }) }) } // render 绘制视频同时添加音频 render(new Uint8Array(imageData.data), imageData.width, imageData.height) addAudio() -- • 录制并导出 webgl 视频 • -- [图片] 示例:录制并一键导出 webgl 视频 适用场景:将动画、编辑过的视频导出视频文件保存 相关 API:wx.createMediaRecorder/MediaRecorder/wx.createMediaContainer/MediaContainer/MediaTrack Step 1: 创建 webgl 画面录制器进行录制 通过 createMediaRecorder 创建页面录制器,并且绑定 webgl(建议离屏状态,效果更好)进行录制 /** * 获取画面录制器 */ getRecorder() { let canvas = this.getMainCanvasNode() let recorder = wx.createMediaRecorder(canvas, { fps: choosedVideoInfo.fps, // 实际视频的 fps videoBitsPerSecond: choosedVideoInfo.bitrate, // 实际视频的 bitrate gop: 12 }) // 监听录制事件 recorder.on("timeupdate", (res) => { console.log('recorder 录制中,当前时间:', res.currentTime) }) recorder.on("stop", (res) => { console.log('recorder停止') this.saveMedia(res.tempFilePath) }) // 开始录制 recorder.start() this.recorder = recorder return recorder }, // 初始化 画面录制器 并进行录制 await this.initRenderer() this.getDecoder().then((decoder) => { let recorder = this.getRecorder() var self = this function loop() { if (self.stopped) { return } let frameData = decoder.getFrameData() if (!frameData) { console.log('没取到帧') setTimeout(() => { loop() }, 1000/60) } else { self.renderFrame(frameData) recorder.requestFrame(() => { console.log('录制帧数:', i++) loop() }) } } loop() }) Step 2: 添加音频合成音视频 1. 通过 createMediaContainer 创建音视频处理容器来合成音视频 2. 通过 MediaContainer.extractDataSource 将视频源分离出视频轨道和音频轨道,将需要的轨道通过 MediaContainer.addTrack 添加到容器中 3. 通过 MediaContainer.export 导出即可获得合成后的视频文件 /** * 将视频和音频合到一起并保存到本地 * @param {*} videoTempFilePath */ saveMedia(videoTempFilePath) { const self = this let choosedFile = this.choosedFile const MediaContainer = wx.createMediaContainer() // webgl的取视频 MediaContainer.extractDataSource({ source: videoTempFilePath, success(res) { MediaContainer.addTrack(res.tracks[0]) // 源视频取音频 MediaContainer.extractDataSource({ source: choosedFile, success(res) { // 拿到音频轨道并加入到容器 res.tracks[0].kind == 'audio' && MediaContainer.addTrack(res.tracks[0]) res.tracks[1].kind == 'audio' && MediaContainer.addTrack(res.tracks[1]) // 合成视频并导出视频文件 MediaContainer.export({ success(res) { // 保存视频到本地 wx.saveVideoToPhotosAlbum({ filePath: res.tempFilePath, success() { wx.showToast({ title: '导出成功', icon: 'success', duration: 2000 }) self.destroy() } }) } }) } }) } }) }, -- •高效图像处理彩蛋 • -- 学会以上这些 Canvas 小技巧,还担心新年的美图美照美视频处理不过来?赶紧码下这个 Canvas 代码包,保证你就是家里最闪耀的靓女靓仔。 预祝大家新的一年 Canvas 在手,红包一直有!
2022-03-24 - 云函数特约商户进件
// 云函数入口文件 var cloud = require('wx-server-sdk') crypto = require('crypto') request = require('request') NodeRSA = require('node-rsa')//npm安装 Form = require('./Form.class') //Form 源自北望大佬https://https://developers.weixin.qq.com/community/develop/article/doc/000c24f0390ff8b5d91b2489059413 cloud.init({ env: 'xxxxxx' }) const db = cloud.database() // 云函数入口函数 exports.main = async (event, context) => { const rq = options =>new Promise((resolve, reject) => { request(options, (error, response, body) => { if (error) { reject(error); } resolve(response); }) }) // 基本参数 var mchid ='160xxxxx92'//服务商商户号 var appId = 'wx41xxxxxxfd6' //服务商和直连商户的appId var APIv3Key = 'xxxxxxxxx'//服务商APIv3密钥 var cert_Doc = 'certificates' //存在数据库的证书记录的_id var timeStamp = parseInt(Date.now()/1000) var nonce_str = Math.random().toString(36).substr(2, 13) var url = 'https://api.mch.weixin.qq.com'+event.url //证书与私钥 var certificates = (await db.collection('a-data').doc(cert_Doc).get()).data //存在数据库的证书 /* 证书记录json如下: "_id":"certificates", "apiclient_key":"-----BEGIN PRIVATE KEY---- xxxxxxx",//商户证书私钥 "serial_no":"3A008xxxxxxxxxxxxxxxx335DABA0",//商户证书序列号 "wx_publicKey":"-----BEGIN PUBLIC KEY-----xxxxxxxxx",//商户对应微信支付平台证书公钥 "wx_serial_no":"32EDFA40A4xxxxxxxxxxx3FAFC91C",////商户对应微信支付平台证书序列号 */ var {serial_no} = certificates var {wx_serial_no} = certificates // 敏感信息加密 var RSAoaep = (e)=>{ var plaintext = crypto.publicEncrypt({ key: certificates.wx_publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },Buffer.from(e)) return plaintext.toString('base64') } // getHeaders const getHeaders = (method,meta)=>{ var signature = new NodeRSA(certificates.apiclient_key).sign(`${method}\n${event.url}\n${timeStamp}\n${nonce_str}\n${meta}\n`, 'base64', 'utf8') var Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${mchid}",nonce_str="${nonce_str}",signature="${signature}",timestamp="${timeStamp}",serial_no="${serial_no}"` return {Authorization,"Content-Type": "application/json",Accept: "application/json","User-Agent": "xxxx.com",} } // 特约商户进件:提交申请单 if(event.url=='/v3/applyment4sub/applyment/'){ /*入参 upload:{ _id:'xxx',//记录_id id_card_number:'xxx',/超管身份证号,注:默认法人为超管 mobile_phone:'xxx',/超管电话 contact_email:'xxx',//超管邮箱 account_number:'xxx',//对公/经营者银行账户号 post:{..}//待提交表单 } */ var {upload} = event var {post} = upload post.business_code = upload._id var {legal_person} = post.subject_info.business_license_info legal_person = RSAoaep(legal_person) var id_card_number = RSAoaep(upload.id_card_number) //超管信息 post.contact_info.contact_name = legal_person post.contact_info.contact_id_number = id_card_number post.contact_info.mobile_phone = RSAoaep(upload.mobile_phone) post.contact_info.contact_email = RSAoaep(upload.contact_email) // 法人信息 post.subject_info.identity_info.id_card_info.id_card_name = legal_person post.subject_info.identity_info.id_card_info.id_card_number = id_card_number //银行卡信息 post.bank_account_info.account_name = (post.bank_account_info.bank_account_type=='BANK_ACCOUNT_TYPE_PERSONAL'?legal_person:RSAoaep(post.subject_info.business_license_info.merchant_name)) post.bank_account_info.account_number =RSAoaep(upload.account_number) console.log(post) var method = 'POST' var meta = JSON.stringify(post) var res = await rq({url, method, headers:{ ...getHeaders(method,meta), "Wechatpay-Serial":wx_serial_no }, body:meta, }) return res } // 特约商户进件:图片上传接口 if(event.url=='/v3/merchant/media/upload'){ var file = (await rq({method: 'GET',url:event.imgUrl,encoding: null})).body //event.imgUrl 图片地址 var filename = `${nonce_str}${/\.\w+$/.exec(event.imgUrl)[0]}` var sha256 = crypto.createHash('sha256').update(file).digest('hex') var meta = JSON.stringify({filename,sha256}) var form = new Form form.append('file',file,filename).append('meta',meta,'meta.json') var method = 'POST' var res = await rq({url, method, headers:{ ...getHeaders(method,meta), ...form.getHeaders(), }, body:form.getBuffer(), }) return JSON.parse(res.body) } // 特约商户进件:查询申请单状态 if(event.url.indexOf('/v3/applyment4sub/applyment/business_code')>-1||event.url.indexOf('/v3/applyment4sub/applyment/applyment_id')>-1){ var method = 'GET' var res = await rq({url,method, json:true, headers:{...getHeaders(method,'')} }) return res.body } })
2022-02-22 - 云调用能力—客服消息
在前面的章节,我们已经在小程序端将 button 组件 open-type 的值设置为 contact ,点击 button 就可以进入客服消息。不过这个客服消息使用的是官方的后台,没法进行深度的定制,我们可以使用云开发作为后台来自定义客服消息来实现快捷回复、添加常用回答等功能。 如果是使用传统的开发方式,需要填写服务器地址(URL)、令牌(Token) 和 消息加密密钥(EncodingAESKey)等信息,然后结合将 token、timestamp、nonce 三个参数进行字典序排序、拼接、并进行 sha1 加密,然后将加密后的字符串与 signature 对比来验证消息的确来自微信服务器,之后再来进行接收消息和事件的处理,可谓十分繁琐,而使用云开发相对简单很多。 13.8.1 客服消息的配置与说明使用开发者工具新建一个云函数,比如 customer,在 config.json 里,设置以下权限后部署上传到服务端。 { "permissions": { "openapi": [ "customerServiceMessage.send", "customerServiceMessage.getTempMedia", "customerServiceMessage.setTyping", "customerServiceMessage.uploadTempMedia" ] } } 然后再打开云开发控制台,点击右上角的设置,选择全局设置,开启云函数接收消息推送,添加消息推送配置。为了学习方便我们将所有的消息类型都指定推送到 customer 云函数里。 text,文本消息image,图片消息miniprogram,小程序卡片event,事件类型 user_enter_tempsession,进入客服消息时就会触发以上有四种消息类型,但是发送客服消息的 customerServiceMessage.send 的 msgtype 属性的合法值有 text、image、link(图文链接消息)、miniprogrampage 四种,也就是我们还可以发图文链接消息。 13.8.2 自动回复文本消息和链接1、自动回复文本消息使用开发者工具新建一个页面,比如 customer,然后在 customer.wxml 里输入以下按钮, 进入客服button> 当用户通过 button 进入到客服消息之后,在聊天界面回复信息,就能触发设置好的 customer 云函数,比如下面的例子就是当用户发一条消息(包括表情)到客服消息会话界面,云函数就会给调用 customerServiceMessage.send 接口给用户回复两条文本消息(一次性可以回复多条),内容分别为[代码]等候您多时啦[代码]和[代码]欢迎关注云开发技术训练营[代码],一个云函数里也是可以多次调用接口的: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); try { const result = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "text", text: { content: "等候您多时啦", }, }); const result2 = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "text", text: { content: "欢迎关注云开发技术训练营", }, }); return event; } catch (err) { console.log(err); return err; } }; 发送文本消息时,支持插入跳小程序的文字链接的,比如我们把上面的文本消息改为以下代码: content: '欢迎浏览点击跳小程序a>'; data-miniprogram-appid 项,填写小程序 appid,则表示该链接跳小程序;data-miniprogram-path 项,填写小程序路径,路径与 app.json 中保持一致,可带参数;对于不支持 data-miniprogram-appid 项的客户端版本,如果有 herf 项,则仍然保持跳 href 中的网页链接;data-miniprogram-appid 对应的小程序必须与公众号有绑定关系。 2、自动回复链接我们还可以给用户回复链接,我们可以把 customer 云函数修改为以下代码,当用户向微信聊天对话界面发送一条消息时,就会回复给用户一个链接,这个链接可以是外部链接哦。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); try { const result = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "link", link: { title: "快来加入云开发技术训练营", description: "零基础也能在10天内学会开发一个小程序", url: "https://cloud.tencent.com/", thumbUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/love.png", }, }); return event; } catch (err) { console.log(err); return err; } }; 3、根据关键词来回复用户将上面的云函数部署之后,当用户向客服消息的聊天会话里输入内容时,不管用户发送的是什么内容,云函数都会回给用户相同的内容,这未免有点过于死板,客服消息能否根据用户发送的关键词回复用户不同的内容呢?要做到这一点我们需要能够获取到用户发送的内容。 我们可以留意云开发控制台云函数日志里看到,customer 云函数返回的 event 对象里的 Content 属性就会记录用户发到聊天会话里的内容: {"Content":"请问怎么加入云开发训练营", "CreateTime":1582877109, "FromUserName":"oUL-mu...XbuEDsn8", "MsgId":22661351901594052, "MsgType":"text", "ToUserName":"gh_b2bbe22535e4", "userInfo":{"appId":"wxda99ae4531b57046","openId":"oUL-m5FuRmuVmxvbYOGuXbuEDsn8"}} 由于 Content 是字符串,那这个关键词既可以是非常精准的,比如“训练营”,或“云开发训练营”,还可以是非常模糊的“请问怎么加入云开发训练营”,我们只需要对字符串进行正则匹配处理即可,比如当用户只要发的内容包含“训练营”,就会收到链接: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); const keyword = event.Content; try { if (keyword.search(/训练营/i) != -1) { const result = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "link", link: { title: "快来加入云开发技术训练营", description: "零基础也能在10天内学会开发一个小程序", url: "https://cloud.tencent.com/", thumbUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/love.png", }, }); } return event; } catch (err) { console.log(err); return err; } }; 在前面的案例里,我们都是使用[代码]touser: wxContext.OPENID,[代码], 13.8.2 自动触发 event 事件要触发 event 事件,我们可以将 customer.wxml 的按钮改为如下代码,这里的 session-from 是用户从该按钮进入客服消息会话界面时,开发者将收到带上本参数的事件推送,可用于区分用户进入客服会话的来源。 进入客服button> 由于我们开启了 event 类型的客服消息,事件类型的值为 user_enter_tempsession,当用户点击 button 进入客服时,就会触发云函数,不用用户发消息就能触发,同时我们返回 event 对象. const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); try { const result = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "text", text: { content: "欢迎来到等候您多时啦", }, }); return event; } catch (err) { console.log(err); return err; } }; 我们可以去云开发控制台查看返回的 event 对象 {"CreateTime":1582876587, "Event":"user_enter_tempsession", "FromUserName":"oUL-m5F...8", "MsgType":"event", "SessionFrom":"文章详情的客服按钮", "ToUserName":"gh_b2bbe22535e4", "userInfo":{"appId":"wxda9...57046", "openId":"oUL-m5FuRmuVmx...sn8"}} 在云函数端,我们是可以通过 event.SessionFrom 来获取到用户到底是点击了哪个按钮从而进入客服对话的,也可以根据用户进入客服会话的来源不同,给用户推送不同类型,比如我们可以给 session-from 的值设置为“训练营”,当用户进入客服消息会话就能推送相关的信息给到用户。 还有一点就是,bindcontact 是给客服按钮绑定了了一个事件处理函数,这里为 onCustomerServiceButtonClick,通过事件处理函数我们可以在小程序端做很多事情,比如记录用户点击了多少次带有标记(比如 session-from 的值设置为“训练营”)的客服消息的按钮等功能。 13.8.3 自动回复图片要在客服消息里给用户回复图片,这个图片的来源只能是来源于微信服务器,我们需要先使用 customerServiceMessage.uploadTempMedia,把图片文件上传到微信服务器,获取到 mediaId(有点类似于微信服务器的 fileID),然后才能在客服消息里使用。 在 customer 云函数的 index.js 里输入以下代码并部署上线,我们将获取到的 mediaId 使用 cloud.openapi.customerServiceMessage.send 发给用户: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); try { //我们通常会将云存储的图片作为客服消息媒体文件的素材 const fileID = "cloud://xly-xrlur.786c-xly-xrlur-1300446086/1572315793628-366.png"; //uploadTempMedia的图片类型为Buffer,而从存储下载的图片格式也是Buffer const res = await cloud.downloadFile({ fileID: fileID, }); const Buffer = res.fileContent; const result = await cloud.openapi.customerServiceMessage.uploadTempMedia({ type: "image", media: { contentType: "image/png", value: Buffer, }, }); console.log(result.mediaId); const mediaId = result.mediaId; const wxContext = cloud.getWXContext(); const result2 = await cloud.openapi.customerServiceMessage.send({ touser: wxContext.OPENID, msgtype: "image", image: { mediaId: mediaId, }, }); return event; } catch (err) { console.log(err); return err; } }; 客服消息还能给用户回复小程序消息卡片,以及客服当前的输入状态给用户(使用 customerServiceMessage.setTyping 接口)。
2021-09-10 - 云调用能力—图像处理和OCR
云调用有些接口属于 AI 服务的范畴,比如借助于人工智能来进行智能裁剪、扫描条码/二维码、图片的高清化等图像处理和识别银行卡、营业执照、驾驶证、身份证、印刷体、驾驶证等 OCR,有了这些接口我们也能在小程序里使用人工智能了。接下来我们以小程序的条码/二维码识别和识别印刷体为例来介绍一下云调用。 13.3.1 图像处理使用开发者工具新建一个云函数,如 scancode,然后在 config.json 里添加 img.scanQRCode 云调用的权限,使用 npm install 安装依赖之后,上传并部署所有文件(此时也会更新权限)。 { "permissions": { "openapi": [ "img.scanQRCode" ] } } 然后再在 index.js 里输入以下代码,注意[代码]cloud.openapi.img.scanQRCode[代码]方法和[代码]img.scanQRCode[代码]权限的对应写法,不然会报 604100 的错误。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { try { const result = await cloud.openapi.img.scanQRCode({ imgUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/demo/qrcodetest.png", //注意二维码必须是条码/二维码,不能是小程序码 }); return result; } catch (err) { console.log(err); return err; } }; 调用该云函数之后,返回的 result 对象里包含 result 对象,在 codeResults 的 data 里可以得到二维码里包含的内容。 codeResults: [{ data: "使用云开发来开发微信小程序可以免费。。。", pos: {leftTop: {…}, rightTop: {…}, rightBottom: {…}, leftBottom: {…}},typeName: "QR_CODE"}] errCode: 0 errMsg: "openapi.img.scanQRCode:ok" imgSize: {w: 260, h: 260} 13.3.2 OCR 人工智能识别使用开发者工具新建一个云函数,如 ocrprint,然后在 config.json 里添加 ocr.printedText 云调用的权限,使用 npm install 安装依赖之后,上传并部署所有文件(此时也会更新权限)。 { "permissions": { "openapi": [ "ocr.printedText" ] } } 调用该云函数之后,返回的 result 对象里包含 result 对象,在 codeResults 的 data 里可以得到二维码里包含的内容。 const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); exports.main = async (event, context) => { try { const result = await cloud.openapi.ocr.printedText({ imgUrl: "https://tcb-1251009918.cos.ap-guangzhou.myqcloud.com/demo/ocrprint.png", }); console.log(result); return result; } catch (err) { console.log(err); return err; } }; 调用该云函数之后,返回的 result 对象里包含 result 对象,在的 items 里可以返回图片包含的文字内容。 items: Array(4) 0: {text: "JavaScript入门", pos: {…}} 1: {text: "JavaScript是目前世界上最流行的编程语言之一,它也是小程序开发最重要的基础语言。要做出一个功能复杂的小程序,除了需要掌握JavaScript的基本语", pos: {…}} 2: {text: "法,还要了解如何使用JavaScript来操作小程序(通过API接口)", pos: {…}} 3: {text: "过API接口)。", pos: {…}} 13.3.3 图像处理拓展能力图片是小程序非常重要的元素,尤其是旅游照片、社交图片、电商产品图片、相册类小程序、媒体图文等,图片的加载速度、清晰度、图片的交互、图片效果的处理以及图片加载的 CDN 消耗都是一个不得不需要去关注的问题。而云开发图像处理拓展能力结合云存储则可以非常有效的解决很多问题。 强烈建议所有有图片处理需求的用户都应该安装图像处理拓展能力,这个能力大大弥补和增强了云存储在图片处理能力,尤其是图片按照需求的规格进行缩放可以大大减少 CDN 的消耗以及图片的加载速度以及我们可以按照不同的业务场景使用快速缩略模板,而这一切的操作和云存储的结合都是非常实用且易用的。 1、图像处理能力介绍云开发图像处理能力结合的是腾讯云数据万象的图片解决方案,图像处理提供多种图像处理功能,包含智能裁剪、无损压缩、水印、格式转换等,图像处理拓展能力所包含的功能非常丰富,使用如下图片处理的费用是按量计费的,计费周期为月,10TB 以内免费,超出 10TB,按 0.025 元/GB 来计费,省事而便宜: 缩放:等比缩放、设定目标宽高缩放等多种方式;裁剪:普通裁剪、缩放裁剪、内切圆、人脸智能裁剪;旋转:普通旋转、自适应旋转;格式转换:jpg、bmp、gif、png、webp、yjpeg 格式转换,gif 格式优化,渐进显示功能;质量变换:针对 JPG 和 WEBP 图片进行质量变换;高斯模糊:对图片进行模糊处理;锐化:对图片进行锐化处理;图片水印:提供图片水印处理功能;文字水印:提供实时文字水印处理功能;获取图片基本信息:查询图片基本信息,包括格式、长、宽等;获取图片 EXIF:查询图片 EXIF 信息,如照片的拍摄参数、缩略图等;获取图片主色调:获取图片主色调信息;去除元信息:去除图片元信息,减小图像体积;快速缩略模板:快速实现图片格式转换、缩略、剪裁等功能,生成缩略图;管道操作符:对图片按顺序进行多种处理当我们在腾讯云云开发网页控制台(注意要使用微信公众号的方式登录)添加完图像处理的拓展能力之后,我们可以在腾讯云的数据万象存储桶里看到云开发的云存储,而关于图像处理能力的深入使用,也可以参考腾讯云数据万象的技术文档。在小程序云开发里使用图像处理能力的方法有三种: 图像地址的拼接,只需要在图片的下载地址 url 里拼接一些简单的参数(API 管道操作符),就能够使用到图像处理的能力,非常方便易用,这个不会把图片处理的结果存储到云存储,不会占用云存储的空间;在获取图片基本信息、获取图片 EXIF、获取图片主色调等方面非常方便;在前端(小程序端)做持久化图像处理,支持有结果图输出的处理操作,也就是我们可以把缩放、裁剪、格式转换、质量变换等处理之后的图片存储到云存储方便以后使用;在云函数端做持久化图像处理,支持有结果图输出的处理操作 01图像地址的拼接在了解图像处理能力之前,我们需要先了解一下云存储文件的 fileID、下载地址以及下载地址携带的权限参数 sign(图像处理能力的参数拼接就是基于下载地址的),如下图所示: [图片] 在安装了图像处理拓展能力的情况下,我们可以直接拿云存储的下载地址进行拼接,拼接之后的链接我们既可以在小程序里使用,也可以用于图床,比如原始图片下载地址为: https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049 而相关的图像处理能力的拼接案例如下,具体的操作可以看技术文档,实际的效果,可以复制粘贴链接到浏览器或小程序里体验(换成自己的地址),注意拼接方式就是在下载地址后面加了一个[代码]&imageMogr2/thumbnail/!20p[代码](注意这里由于已经有了一个 sign 参数,所以拼接时用的是[代码]$[代码],不能写成[代码]?[代码],否则不会生效),直接就可以啦,非常易用: //将图片等比例缩小到原来的20% https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049&imageMogr2/thumbnail/!20p 后面为了方便,我们将[代码]https://786c-xly-xrlur-1300446086.tcb.qcloud.la/hehe.jpg?sign=b8ac757538940ead8eed4786449b4cd7&t=1591752049[代码]简写为 download_url: //缩放宽度,高度不变,下面案例为宽度为原图50%,高度不变 download_url&imageMogr2/thumbnail/!50px //缩放高度,宽度不变,下面案例为高度为原图50%,宽度不变 download_url&imageMogr2/thumbnail/!x50p //指定目标图片的宽度(单位为px),高度等比压缩,注意下面的是x,不是px,p与x在拼接里代表着不同的意思 download_url&imageMogr2/thumbnail/640x //指定目标图片的高度(单位为px),宽度等比压缩: download_url&imageMogr2/thumbnail/x355 //限定缩略图的宽度和高度的最大值分别为 Width 和 Height,进行等比缩放 download_url&imageMogr2/thumbnail/640x355 //限定缩略图的宽度和高度的最小值分别为 Width 和 Height,进行等比缩放 download_url&imageMogr2/thumbnail/640x355r //忽略原图宽高比例,指定图片宽度为 Width,高度为 Height ,强行缩放图片,可能导致目标图片变形 download_url&imageMogr2/thumbnail/640x355! //等比缩放图片,缩放后的图像,总像素数量不超过 Area download_url&imageMogr2/thumbnail/150000@ //取半径为300,进行内切圆裁剪 download_url&imageMogr2/iradius/300 //取半径为100px,进行圆角裁剪 download_url&imageMogr2/rradius/100 //顺时针旋转90度 download_url&imageMogr2/rotate/90 //将jpg格式的原图片转换为 png 格式 download_url&imageMogr2/format/png //模糊半径取8,sigma 值取5,进行高斯模糊处理 download_url&imageMogr2/blur/8x5 //获取图片的基础信息,返回的是json格式,我们可以使用https请求来查看图片的format格式,width宽度、height高度,size大小,photo_rgb主色调 download_url&imageInfo 2、小程序端持久化图像处理当我们希望把缩放、裁剪、旋转、格式变换等图像处理的结果(也就是处理之后的图片)存储到云存储,这个就叫做持久化图像处理,在安装了图像处理能力之后,我们也可以在小程序端做图像处理。 当用户把原始图片上传到小程序端时,我们需要对该图片进行一定的处理,比如图片过大就对图片进行裁剪缩小;比如图片需要进行一定的高斯模糊、旋转等处理,这些虽然在图像处理之前,也是可以使用 js 来做的,但是小程序端图像处理的效果并没有那么好或者过于复杂,使用图像处理的拓展能力就非常实用了。在小程序端构建图像拓展依赖 首先在开发者工具小程序根目录(一般为 miniprogram),右键“在终端中打开”,然后在终端里输入以下代码,也就是在小程序端安装图像拓展依赖,安装完时,我们就可以在 miniprogram 文件夹下看到 node_modules: npm install --save @cloudbase/extension-ci-wxmp@latest 然后点击开发者工具工具栏里的工具-构建 npm,构建成功之后,就可以在 miniprogram 文件夹下看到 minprogram_npm 里有@cloubase 文件夹,里面有 extension-ci-wxmp,说明图像拓展依赖就构建完成。 在小程序端进行图像处理 使用开发者工具新建一个 imgprocess 的页面,然后在 imgprocess.wmxl 里输入如下代码,我们新建一个 button 按钮: 处理图片button> 然后再在 imgprocess.js 的 Page()函数的上面(外面)引入图像处理依赖,代码如下: const extCi = require("./../../miniprogram_npm/@cloudbase/extension-ci-wxmp"); 然后再在 imgprocess.js 的 Page()函数的里面写一个 imgprocess 的事件处理函数,点击 button 之后会先执行 readFile()函数,也就是获取图片上传到小程序临时文件的结果(是一个对象),然后再调用 imageProcess()函数,这个函数会对图片进行处理,图片会保存为[代码]tcbdemo.jpg[代码],而处理之后的图片会保存为 image_process 文件夹下的 tcbdemo.png,相当于保存了两张图片: async imgprocess(){ const readFile = async function() { let res = await new Promise(resolve=>{ wx.chooseImage({ success: function(res) { let filePath = res.tempFilePaths[0] let fm = wx.getFileSystemManager() fm.readFile({ filePath, success(res){ resolve(res) } }) } }) }) return res } let fileResult = await readFile(); //获取图像的临时文件上传结果 const fileContent = fileResult.data //获取上传到临时文件的图像,为Uint8Array或Buffer格式 async function imageProcess() { extCi.invoke({ action: "ImageProcess", cloudPath: "tcbdemo.jpg", // 图像在云存储中的路径,有点类似于wx.cloud.uploadFile接口里的cloudPath,上传的文件会保存为云存储根目录下的hehe.jpg operations: { rules: [ { fileid: "/image_process/tcbdemo.png", //将图片存储到云存储目录下的image_process文件夹里,也就是我们用image_process存储处理之后的图片 rule: "imageMogr2/format/png", // 处理样式参数,我们可以在这里写图片处理的参数拼接 } ] }, fileContent }).then(res => { console.log(res); }).catch(err => { console.log(err); }) } await imageProcess() } 可能你的开发者工具会报以下错误:[代码]https://786c-xly-xrlur-1300446086.pic.ap-shanghai.myqcloud.com 不在以下 request 合法域名列表中,请参考文档:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html[代码],这个要按照参考文档将链接加入到合法域名当中,不然不会生成图片;[代码]action[代码]是操作类型,它的值可以为:ImageProcess 图像处理,DetectType 图片安全审核(后面会介绍),WaterMark 图片忙水印、DetectLabel 图像标签等。[代码]operations[代码]是图像处理参数,尤其是 rule 和我们之前 url 的拼接是一致的,比如[代码]imageMogr2/blur/8x5[代码]、[代码]imageMogr2/rradius/100[代码]等参数仍然有效。上面函数里的 fileContent 不是必要的,也就是说我们可以不在小程序端上传图片,而是直接修改云存储里面已有的图片,并将图片处理后的照片保存,这种情况代码可以写成如下: async imgprocess(){ extCi.invoke({ action: "ImageProcess", cloudPath: "tcbdemo.jpg", // 会直接处理这张图片 operations: { rules: [ { fileid: "/image_process/tcbdemo.png", rule: "imageMogr2/format/png", // 处理样式参数,与下载时处理图像在url拼接的参数一致 } ] }, }).then(res => { console.log(res); }).catch(err => { console.log(err); }) } 3、云函数端持久化图像处理在云函数端的处理和小程序端的处理,使用的方法大体上是一致的,不过云函数的处理图片的场景和小程序端处理图片的场景会有所不同,小程序端主要用于当用于上传图片时就对图片进行处理,云函数则主要用于从第三方下载图片之后进行处理或者对云存储里面的图片进行处理(比如使用定时触发器对云存储里指定文件夹的图片进行处理)。不建议把图片传输到云函数端再来对图片进行处理。 使用开发者工具新建一个 imgprocess 的云函数,然后在 package.json 里添加 latest 最新版的[代码]@cloudbase/extension-ci[代码],并右键云函数目录选择在终端中打开输入命令 npm install 安装依赖: "dependencies": { "wx-server-sdk": "latest", "@cloudbase/extension-ci": "latest" } 然后再在 index.js 里输入以下代码,代码的具体含义可以参考小程序端的内容讲解: const cloud = require("wx-server-sdk"); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const extCi = require("@cloudbase/extension-ci"); cloud.registerExtension(extCi); async function process() { try { const opts = { rules: [ { fileid: "/image_process/tcbdemo.jpeg", rule: "imageMogr2/format/png", }, ], }; const res = await app.invokeExtension("CloudInfinite", { action: "ImageProcess", cloudPath: "tcbdemo.jpg", fileContent, operations: opts, }); console.log(res); return res; } catch (err) { console.log(err); } }
2021-09-10