上传图片是小程序常见的功能,例如点评类小程序邀请用户分享照片、电商类小程序要求商家上传商品照片。
伴随着照片像素越来越高,图片体积越来越大,小程序开发者需要压缩图片,否则将导致用户上传图片失败或加载时间过长等影响体验的情况。
小程序提供 wx.chooseMedia、wx.canvasToTempFilePath、wx.compressImage 3 个图片类接口,便于开发者在不同应用场景下处理图片。除此以外,这 3 个接口的巧妙结合能够满足更多元化的图片压缩需求。下面就来看看怎样使用吧!
wx.chooseMedia
wx.chooseMedia 支持在使用小程序过程中拍摄或从手机相册选择图片或视频,其 sizeType 属性支持是否上传缩略图。该接口应用简便,接入即可实现压缩图片效果,省时省力。
wx.chooseMedia({
count: 9,
mediaType: ['image'], // 只允许选择图片
sourceType: ['album', 'camera'], // 可以拍摄或从相册中选择
sizeType:['compressed'], // 选择压缩图
camera: 'back', // 后置摄像头
success(res) {
console.log(res)
}
});
然而,该接口在压缩图片方面也有一定的限制:
-
无法指定压缩质量
-
部分安卓机型存在压缩失效的情况
-
iOS 和安卓的压缩机制不同,需要进行合理兼容
wx.canvasToTempFilePath
开发者可以通过控制 Canvas.createImage
绘制图片到 Canvas,然后利用 wx.canvasToTempFilePath
接口转换成图片。
这种方法能够高效控制图片宽高尺寸以及压缩质量,非常适用于有图片要求的场景。
wx.canvasToTempFilePath({
width: 50, // 画布区域的宽度
height: 50, // 画布区域的高度
destWidth: 100, // 输出图片的宽度
destHeight: 100, // 输出图片的高度
canvasId: 'myCanvas',
quality: 1, // 图片质量0-1
success(res) {
console.log(res.tempFilePath)
}
});
但是这种方式也会存在一定的限制:
-
iOS 和安卓的压缩机制不同,需要进行合理兼容
-
通过 Canvas 转换的图片存在略微色差
wx.compressImage
开发者可以调用wx.compressImage
接口直接压缩图片,而且支持选择压缩质量,不限制图片宽高尺寸,非常适用于处理特殊大小的图片。
wx.compressImage({
src: '', // 图片路径
quality: 80 // 压缩质量 0-100
});
同时这种方式也需要考虑不同系统的压缩差异:
-
在压缩到极限值时,iOS 压缩图画质不会随着压缩质量变小而变化
-
在压缩质量小于 1 时,安卓系统输出的画质将不再变小
多方式结合处理
回顾常见的小程序业务场景,图片处理主要聚焦于用户上传图片、列表展示这 2 个环节,可以结合以上 3 个接口实现最佳图片处理方式,既能够利用接口自带的压缩功能,省时省力;又能够解决图片太大造成的压缩难题。
- 判断系统类型
判断当前系统是 iOS 系统还是安卓系统
function isIOS(){
return wx.getSystemInfo().then(res => {
return /IOS/ig.test(res.system);
});
}
- 根据系统选择上传方式
iOS 系统:设置 sizeType 为 [‘compressed’],利用 iOS 压缩体系自动压缩
安卓系统:设置 sizeType 为 [‘original’, ‘compressed’],让用户自主选择上传原图或压缩图。
这种方式一方面利用接口自带的压缩能力; 另一方面如果图片宽高大于安卓能清晰压缩的值(例如40000),用户会预览到比较模糊的照片而选择上传原图
- 验证大小,手动压缩
当用户选择图片后,wx.chooseMedia
返回的 tempFiles 显示对应图片的大小。如果该图片大小大于限制值,则进行手动压缩。
- 根据宽高选择压缩方式
通过 wx.getImageInfo
获取图片的宽高:
如果宽度或高度大于 4096,调用 wx.compressImage
强制压缩
如果宽度和高度都小于 4096,绘制 Canvas 实现压缩,设置压缩基础宽高为 1280
代码如下:
// compressImage.js
/**
* @param {object} img 包含path:图片的path,size:图片的大小
* @param {object} canvas canvas对象
* @param {number} fileLimit 文件大小限制
* @returns {Promise} 返回Promise对象
*/
function _compressImage(img, canvas, fileLimit) {
return wx.getSystemInfo().then(res => {
let {
// 设备像素比
pixelRatio,
// 设备品牌
system
} = res;
// 是否是IOS系统
let isIOS = /(ios)/ig.test(system);
// 文件限制
fileLimit = fileLimit || 2 * 1024 * 1024;
// 基础大小
let baseSize = 1280;
// 大于文件限制,手动压缩
if (img.size > fileLimit) {
return compressImg({src:img.path, size:img.size, canvas, baseSize, isIOS, pixelRatio}).then(response => {
return Promise.resolve(response);
});
}
return Promise.resolve(img.path);
});
}
/**
* @description 根据图片的大小选择压缩的方式
* @param {string} src 图片的path
* @param {number} size 图片的大小
* @param {object} canvas canvas对象
* @param {number} baseSize 基础尺寸
* @param {boolean} isIOS 是否是IOS系统
* @returns {Promise} 返回Promise对象
*/
function compressImg({src, size, canvas, baseSize, isIOS, pixelRatio}) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src
}).then(res => {
let imgWidth = res.width;
let imgHeight = res.height;
if (imgWidth <= 4096 && imgHeight <= 4096) {
// 小于4096使用canvas压缩
canvasToImage({src, size, imgWidth, imgHeight, canvas, baseSize, isIOS, pixelRatio}).then(response => {
resolve(response);
});
} else {
// 超过4096使用强制压缩
compressImage(src, size, isIOS).then(response => {
resolve(response);
});
}
}).catch(err => {
// 使用强制压缩
compressImage(src, size, isIOS).then(response => {
resolve(response);
});
});
});
}
/**
* @description 使用wx.compressImage压缩图片
* @param {string} src 图片的path
* @param {number} size 图片的大小
* @param {boolean} isIOS 是否是IOS系统
* @returns {Promise} 返回Promise对象
*/
function compressImage(src, size, isIOS) {
return new Promise((resolve, reject) => {
let quality = 100;
if (isIOS) {
quality = 0.1;
} else {
let temp = 30 - (size / 1024 / 1024);
quality = temp < 10 ? 10 : temp;
}
wx.compressImage({
src,
quality,
success: (res) => {
resolve(res.tempFilePath);
},
fail: () => {
// 压缩失败返回传递的图片src
resolve(src);
}
});
});
}
/**
* @description 使用canvans压缩图片
* @param {string} src 图片的path
* @param {number} size 图片的大小
* @param {number} imgWidth 图片的宽度
* @param {number} imgHeight 图片的高度
* @param {object} canvas canvas对象
* @param {number} baseSize 基础尺寸
* @param {boolean} isIOS 是否是IOS系统
* @param {number} pixelRatio 设备像素比
* @returns {Promise} 返回Promise对象
*/
function canvasToImage({src, size, imgWidth, imgHeight, canvas, baseSize, isIOS, pixelRatio}) {
return new Promise((resolve, reject) => {
if (!canvas) {
compressImage(src, size).then(res => {
resolve(res);
});
return;
}
// 设置canvas宽度和高度
let canvasWidth = 0;
let canvasHeight = 0;
let quality = 1;
// 图片的宽度和高度都小于baseSize,宽高不变
if (imgWidth <= baseSize && imgHeight <= baseSize) {
canvasWidth = imgWidth;
canvasHeight = imgHeight;
quality = 0.3;
} else {
let compareFlag = true;
// 图片的一边大于baseSize,宽高不变
if (pixelRatio > 2 && (imgWidth > baseSize || imgHeight > baseSize) && (imgWidth < baseSize || imgHeight < baseSize)) {
canvasWidth = imgWidth;
canvasHeight = imgHeight;
quality = 0.3;
} else {
// 按照原图的宽高比压缩
compareFlag = pixelRatio > 2 ? (imgWidth > imgHeight) : (imgWidth > imgHeight);
// 宽比高大,宽按基准比例缩放,高设置为基准值,高比宽大,高按基准比例缩放,宽设置为基准值。
canvasWidth = compareFlag ? parseInt(imgWidth / (imgHeight / baseSize)) : baseSize;
canvasHeight = compareFlag ? baseSize : parseInt(imgHeight / (imgwidth / baseSize));
quality = 0.9;
}
}
let pic = canvas.createImage();
pic.src = src;
pic.onerror = function () {
// 加载失败使用强制压缩
compressImage(src, size, isIOS).then(response => {
resolve(response);
});
}
pic.onload = function () {
// 获取绘画上下文
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.drawImage(pic, 0, 0, canvasWidth, canvasHeight);
// 导出图片
wx.canvasToTempFilePath({
canvas,
width: canvasWidth,
height: canvasHeight,
destHeight: canvasHeight,
destWidth: canvasWidth,
fileType:'jpg',
quality,
success: (res) => {
resolve(res.tempFilePath);
},
fail: (err) => {
// 压缩失败使用强制压缩
compressImage(src, size, isIOS).then(response => {
resolve(response);
});
}
});
}
});
}
/**
* @description 循环压缩图片
* @param {object} img 包含path:图片的path,size:图片的大小
* @param {object} canvas canvas对象
* @param {number} fileLimit 文件大小限制
* @returns {Promise} 返回Promise对象
*/
async function cycleCompressImg(img, canvas, fileLimit) {
let fileSystemManager = wx.getFileSystemManager();
function getFileInfoPromise(src) {
return new Promise((resolve, reject) => {
fileSystemManager.getFileInfo({
filePath: src,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
}
let size = await getFileInfoPromise(img.path).size;
let path = img.path;
while (size > fileLimit) {
path = await _compressImage(img, canvas, fileLimit);
}
return path;
}
module.exports = {
_compressImage,
cycleCompressImg
};
使用
在需要使用的页面中,引入上面的代码。
由于在压缩中使用canvas因此需要在wxml文件中添加canvas元素
<view class="upload">
<view class="imageList">
<block wx:if="{{uploadedFilePaths && uploadedFilePaths.length}}">
<view class="imageList_item" wx:for="{{uploadedFilePaths}}" wx:key="index">
<image src="{{item}}" class="upload_file"></image>
<view class="close" bindtap="handleClose" data-index="{{index}}">
<text class="flow">x</text>
</view>
</view>
</block>
<view wx:if="{{uploadedFilePaths.length < maxCount}}" class="camera" bindtap="handleCameraTap">
<canvas type="2d" id="uploadCanvas" class="canvas"></canvas>
</view>
</view>
</view>
样式
/* components/upload/index.wxss */
.upload{
margin-top:20rpx;
}
.camera {
width: 100rpx;
height: 100rpx;
border: 1px solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
background-color: #fff;
position: relative;
}
.imageList {
display: flex;
align-items: center;
}
.imageList_item {
border-radius: 12rpx;
position: relative;
width: 100rpx;
height: 100rpx;
margin-right: 20rpx;
}
.close {
width: 32rpx;
height: 32rpx;
background-color: rgba(90, 90, 90, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
top: 0;
font-size: 24rpx;
color: #fff;
}
.flow {
display: flow-root;
height: 100%;
line-height: 100%;
}
.canvas {
width: 100rpx;
height: 100rpx;
opacity: 0;
position: absolute;
z-index: -1;
display: none;
}
.upload_img{
width: 48rpx;
height: 48rpx;
border-radius:12rpx;
}
.upload_file{
width: 100rpx;
height: 100rpx;
border-radius: 12rpx;
}
然后在js文件中,调用选择图片的api,由于版本的更迭使用的api也在变化,因此为了能够方便使用把微信小程序中目前常用的选择图片的api封装成一个函数,代码如下:
// chooseImage.js
/**
* @description 选择图片
* @param {object} options 选择图片的参数对象
* @returns {Promise} 返回Promise对象
*/
function chooseImage(options) {
options = options || {};
return wx.getSystemInfo().then(res => {
let {
system
} = res;
let isIOS = /ios/gi.test(system);
let sizeType = isIOS ? ['compressed'] : ['origin', 'compressed'];
if (wx.canIUse('chooseImage')) {
return new Promise((resolve, reject) => {
wx.chooseImage({
count: options.count || 9,
sizeType: options.sizeType || sizeType,
sourceType: options.sourceType || ['album', 'camera'],
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
} else if (wx.canIUse('chooseMedia')) {
return new Promise((resolve, reject) => {
wx.chooseMedia({
count: options.count || 9,
mediaType: ['image'],
sourceType: options.sourceType || ['album', 'camera'],
success: (res) => {
res.tempFiles = res.tempFiles.map(item => {
item.path = item.tempFilePath;
return item;
});
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
}
});
}
module.exports = chooseImage;
引入上面的compressImage.js
和chooseImage.js
的 代码
const chooseImage = require('./chooseImage');
const {
_compressImage
} = require('./compressImage');
Page({
data:{
// 保存上传的文件路径
uploadFilePaths:[],
// 上传的最大文件数量
maxCount:5,
// 文件大小限制,单位kb,超过限制压缩文件
limitSize:1024
},
// 上传图片
uploadImg() {
wx.getSetting().then(res => {});
if (this.data.uploadedFilePaths.length < this.data.maxCount) {
const appAuthorizeSetting = wx.getSystemInfoSync(),
cameraAuthorized = appAuthorizeSetting.cameraAuthorized,
albumAuthorized = appAuthorizeSetting.albumAuthorized;
// 相册或相机没有授权,先设置权限
if ((albumAuthorized !== undefined && !albumAuthorized) || !cameraAuthorized) {
wx.openAppAuthorizeSetting().then(auth => {}).catch(e => {});
} else {
let {
maxCount
} = this.data;
// 授权后选择图片
chooseImage({
count: maxCount
}).then(res => {
let {
tempFiles,
} = res;
if (tempFiles.length <= maxCount) {
if ((tempFiles.length + this.data.uploadedFilePaths.length) > maxCount) {
showToast({
title: '最多上传' + maxCount + '张图片'
});
return ;
}
} else {
showToast({
title: '最多上传' + maxCount + '张图片'
});
return ;
}
this.getCanvas('uploadCanvas').then(canvas => {
if (canvas) {
let proArr = tempFiles.map(item => {
return compressImage._compressImage(item, canvas, this.data.limitSize);
});
Promise.all(proArr).then(res => {
this.data.uploadedFilePaths = this.data.uploadedFilePaths.concat(res);
this.setData({
uploadedFilePaths: this.data.uploadedFilePaths
});
this.triggerEvent('upload', {
value: this.data.uploadedFilePaths
});
});
}
});
}).catch(err => {});
}
}
},
// 获取canvas,用于压缩图片
getCanvas(id) {
return new Promise((resolve, reject) => {
let query = wx.createSelectorQuery();
query.in(this);
query.select(`#${id}`).fields({
node: true,
size: true
}).exec((res) => {
if (res[0]) {
resolve(res[0].node);
} else {
resolve(null);
}
});
});
},
})
每种图片处理方式都有其突出的优势,结合多种方式能够最优地解决问题,适用于目标场景,便利用户上传图片的体验。