最近在HTML5项目中用到了上传图片功能,现在有HTML5前端网页也能访问手机相册或照相机,但是手机拍下来的图片动不动就好几M,这样上传几张图片上去,用户要骂街了,能不能在客户端压缩好图片后再上传了,节约用户的流量。那么前端如何压缩图片了?
实现图片压缩我们需要用到canvas。 好了开干吧!
如下代码使用React编写
使用input file 调用相机货相册
<input type="file" multiple accept="image/*"/>
复制代码
点击如上input file可以调起一个弹窗,让用户从相机或相册中获取图片。
默认的input file样式太丑,看着太别扭,我们先来把样式美化下吧。这里推荐两种方法,
方法一:
"uploadFile" type="file" multiple accept="image/*"/>
<div className="btn-upload">
<label htmlFor="uploadFile">上传图片label>
div>
复制代码
这种方法是把input[file]的opacity设为0,label的for属性设置为input[file]的ID,这样点击label就是点击input file是一样的效果,而input file我们完全看不见。
方法二:
"leftFile" type="file" multiple accept="image/*" style={{display: 'none'}}/>
<a className="btn" onClick={()=>{
this.refs.leftFile.click()
}}>Browsea>
复制代码
这种方法把input display设为none,点击其他元素触发input的click事件。
使用canvas压缩图片
1、获取到选取的图片
render() {
const {
prefixCls, className, children, style
} = this.props
const cls = classNames({
[prefixCls]: true,
[className]: className
})
return (
<div
className={cls}
style={style}
onClick={() => {
this.refs.inputFile.click()
}}>
<input ref="inputFile" type="file" multiple accept="image/*" onChange={this.onChange}/>
{ children } {/*自定义选取图片按钮样式*/}
div>
)
}
复制代码
不管文件域是用何种方式打开的,都可以在 change 事件中获取到选择的文件或拍摄的照片
onChange = async (e) => {
const { onChange, compress } = this.props
let files = e.target.files
const len = files.length
let newFiles = []
for(let i = 0; i < len; i++) {
const result = await this.fileReader(files[i])
const data = await this.compress(result)
newFiles.push(data)
}
}
复制代码
2、通过FileReader把图片转化为base64图像编码
通过onChange获取到图片后需要创建一个FileReader对象,我们需要调用readAsDataURL把文件转换为base64图像编码,如data:image/jpeg;base64……这种格式。让fileReader读取文件完毕后会触发onload方法,在onload方法中我们可以通过e.target.result来获取到base64的编码,如果读取失败,该值为null。onload是个异步方法,我们可以通过Promise和async,await来把异步变为同步,不至于嵌套太多callback。
// 读取文件并转化为base64
fileReader = (file) => {
return new Promise(function (resolve, reject) {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result)
};
reader.onerror = reject
reader.readAsDataURL(file);
})
}
复制代码
3、创建Image对象,给src赋值为fileReader读取的base64结果,然后同样在Image的onload方法中处理图片压缩。
compress = (res) => {
return new Promise(function (resolve, reject) {
const img = new Image()
img.onload = function() {
//压缩图片
resolve(blob) // 返回处理结果
}
img.onerror = function() {
reject(new Error('图片加载失败'))
}
img.src = res;
})
}
复制代码
4、通过canvas压缩图片
使用JS实现图片的压缩效果,原理其实很简单,核心API就是使用canvas的drawImage()方法。 canvas的drawImage()方法API如下:
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
复制代码
虽然看上去有9大参数,但不用慌,实际上可以看出就3个参数:
img就是图片对象,可以是页面上获取的DOM对象,也可以是虚拟DOM中的图片对象。
sx,sy,swidth,sheight 在canvas画布上画一片区域用来放置图片,sx,sy为左上角坐标,swidth,sheight指区域大小。如果没有指定后面4个参数,则图片会被拉伸或缩放在这片区域内。
x,y,width,height 就是图片在canvas画布上显示的大小和位置。如果这里的width,height的值就是图片的原始尺寸,则最终的表现效果是图片剪裁于swidth,sheight区域内。 对于本文的图片压缩,最后四个参数是用不到的,只需前面五个参数就可以了。 核心代码
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
// 清除画布
context.clearRect(0, 0, targetWidth, targetHeight);
// 图片压缩
context.drawImage(img, 0, 0, targetWidth, targetHeight);
// 获取base64格式信息
const dataUrl = canvas.toDataURL(imageType);
复制代码
canvas.toDataURL()方法
canvas.toDataURL(mimeType, qualityArgument)
复制代码
可以把图片转换成base64格式信息,纯字符的图片表示法。
其中:
mimeType表示canvas导出来的base64图片的类型,默认是png格式,也即是默认值是'image/png',我们也可以指定为jpg格式'image/jpeg'或者webp等格式。file对象中的file.type就是文件的mimeType类型,在转换时候正好可以直接拿来用(如果有file对象)。
qualityArgument表示导出的图片质量,只要导出为jpg和webp格式的时候此参数才有效果,默认值是0.92,是一个比较合理的图片质量输出参数,通常情况下,我们无需再设定。
canvas.toBlob()方法
canvas.toBlob(callback, mimeType, qualityArgument)
复制代码
可以把canvas转换成Blob文件,通常用在文件上传中,因为是二进制的,对后端更加友好。
toBlob方法目前iOS不支持。
和toDataURL()方法相比,toBlob()方法是异步的,因此多了个callback参数,这个callback回调方法默认的第一个参数就是转换好的blob文件信息,本文demo的文件上传就是将canvas图片转换成二进制的blob文件。
完整代码
github.com/zhangyi5628…
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import './index.less'
export default class InputImage extends Component {
constructor(props) {
super(props)
}
static defaultProps = {
prefixCls: 'yh-input-image',
compress: true,
className: '',
style: null,
onChange: ()=>{},
maxWidth: 400,
maxHeight: 400,
fileType: 'blob',
imageType: 'image/png'
}
static propTypes = {
prefixCls: PropTypes.string,
compress: PropTypes.bool,
className: PropTypes.string,
style: PropTypes.object,
onChange: PropTypes.func,
maxWidth: PropTypes.number,
maxHeight: PropTypes.number,
fileType: PropTypes.oneOf(['base64', 'blob']), // 返回压缩后的的文件类型
imageType: PropTypes.string // 指定返回的图片格式
}
compress = (res) => {
// console.log('res:', res)
let { maxWidth, maxHeight, fileType, imageType } = this.props
// imageType = `image/${imageType}`
return new Promise(function (resolve, reject) {
const img = new Image()
img.onload = function() {
let originWidth = this.width, originHeight = this.height
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
let targetWidth = originWidth, targetHeight = originHeight;
// 图片尺寸超过400x400的限制
if (originWidth > maxWidth || originHeight > maxHeight) {
if (originWidth / originHeight > maxWidth / maxHeight) {
// 更宽,按照宽度限定尺寸
targetWidth = maxWidth;
targetHeight = Math.round(maxWidth * (originHeight / originWidth));
} else {
targetHeight = maxHeight;
targetWidth = Math.round(maxHeight * (originWidth / originHeight));
}
}
// canvas对图片进行缩放
canvas.width = targetWidth;
canvas.height = targetHeight;
// 清除画布
context.clearRect(0, 0, targetWidth, targetHeight);
// 图片压缩
context.drawImage(img, 0, 0, targetWidth, targetHeight);
if (fileType === 'base64') {
// 获取base64格式信息
const dataUrl = canvas.toDataURL(imageType);
resolve(dataUrl)
} else {
// 把canvas转化为blob二进制文件
if (canvas.toBlob) {
canvas.toBlob(function(blob) {
resolve(blob)
}, imageType)
} else { // ios 不支持toB
let data = canvas.toDataURL(imageType);
//dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
data = data.split(',')[1];
data = window.atob(data);
let ia = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i);
}
//canvas.toDataURL 返回的默认格式就是 image/png
let blob = new Blob([ia], {
type: imageType
});
resolve(blob)
}
}
}
img.onerror = function() {
reject(new Error('图片加载失败'))
}
img.src = res;
});
}
// 读取文件并转化为base64
fileReader = (file) => {
console.log('file:', file)
return new Promise(function (resolve, reject) {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result)
};
reader.onerror = reject
reader.readAsDataURL(file);
})
}
onChange = async (e) => {
const { onChange, compress } = this.props
let files = e.target.files
const len = files.length
console.log('files:', files)
let newFiles = []
if (compress) {
for(let i = 0; i < len; i++) {
const result = await this.fileReader(files[i])
const data = await this.compress(result)
// console.log('data:', data)
newFiles.push(data)
}
}
console.log('newFiles:', newFiles)
onChange && onChange({
files,
newFiles
})
}
render() {
const {
prefixCls, className, children, style
} = this.props
const cls = classNames({
[prefixCls]: true,
[className]: className
})
return (
<div
className={cls}
style={style}
onClick={() => {
this.refs.inputFile.click()
}}>
<input ref="inputFile" type="file" multiple accept="image/*" onChange={this.onChange}/>
{ children } {/**/}
div>
)
}
}
复制代码