Skip to content

Commit 5732915

Browse files
winddiesnighca
winddies
authored andcommitted
添加图片压缩功能 (#363)
1 parent db0cd8a commit 5732915

File tree

9 files changed

+477
-8
lines changed

9 files changed

+477
-8
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ Qiniu-JavaScript-SDK 的示例 [Demo](http://jssdk-v2.demo.qiniu.io) 中的服
105105

106106
### Example
107107

108+
文件上传:
109+
108110
```JavaScript
109111

110112
var observable = qiniu.upload(file, key, token, putExtra, config)
@@ -115,7 +117,20 @@ var subscription = observable.subscribe(next, error, complete) // 这样传参
115117

116118
subscription.unsubscribe() // 上传取消
117119
```
120+
图片上传前压缩:
118121

122+
```JavaScript
123+
let options = {
124+
quality: 0.92,
125+
noCompressIfLarger: true
126+
// maxWidth: 1000,
127+
// maxHeight: 618
128+
}
129+
qiniu.compressImage(file, options).then(data => {
130+
var observable = qiniu.upload(data.dist, key, token, putExtra, config)
131+
var subscription = observable.subscribe(observer) // 上传开始
132+
})
133+
```
119134
## API Reference Interface
120135

121136
### qiniu.upload(file: blob, key: string, token: string, putExtra: object, config: object): observable
@@ -251,6 +266,25 @@ subscription.unsubscribe() // 上传取消
251266
multipart_params_obj[k[0]] = k[1]
252267
}
253268
```
269+
### qiniu.compressImage(file: blob, options: object) : Promise (上传前图片压缩)
270+
271+
```JavaScript
272+
var imgLink = qiniu.compressImage(file, options).then(res => {
273+
// res : {
274+
// dist: 压缩后输出的 blob 对象,或原始的 file,具体看下面的 options 配置
275+
// width: 压缩后的图片宽度
276+
// height: 压缩后的图片高度
277+
// }
278+
}
279+
})
280+
```
281+
* file: 要压缩的源图片,为 `blob` 对象,支持 `image/png``image/jpeg``image/bmp``image/webp` 这几种图片类型
282+
* options: `object`
283+
* options.quality: `number`,图片压缩质量,在图片格式为 `image/jpeg``image/webp` 的情况下生效,其他格式不会生效,可以从 01 的区间内选择图片的质量。默认值 0.92
284+
* options.maxWidh: `number`,压缩图片的最大宽度值
285+
* options.maxHeight: `number`,压缩图片的最大高度值
286+
(注意:当 `maxWidth``maxHeight` 都不设置时,则采用原图尺寸大小)
287+
* options.noCompressIfLarger: `boolean`,为 `true` 时如果发现压缩后图片大小比原来还大,则返回源图片(即输出的 dist 直接返回了输入的 file);默认 `false`,即保证图片尺寸符合要求,但不保证压缩后的图片体积一定变小
254288

255289
### qiniu.watermark(options: object, key: string, domain: string): string(水印)
256290

@@ -428,7 +462,7 @@ subscription.unsubscribe() // 上传取消
428462
"Domain": "<Your Bucket Domain>" // Bucket 的外链默认域名,在 Bucket 的内容管理里查看,如:'http://xxx.bkt.clouddn.com/'
429463
}
430464
```
431-
2. 进入项目根目录,执行 `npm install` 安装依赖库,然后打开两个终端,一个执行 `npm run serve` 跑 server, 一个执行 `npm run dev` 运行服务 demo1demo2 为测试es6语法的 demo,进入 demo2 目录,执行 `npm install`,然后 `npm start` 运行 demo2demo1 和 demo2 都共用一个 server,请注意 server 文件里的 `region` 设置跟 `config` 里的` region` 设置要保持一致。
465+
2. 进入项目根目录,执行 `npm install` 安装依赖库,然后打开两个终端,一个执行 `npm run serve` 跑 server, 一个执行 `npm run dev` 运行服务demo1`http://0.0.0.0:8080/test/demo1`;demo3:`http://0.0.0.0:8080/test/demo3`;demo1为测试上传功能的示例,demo3为测试图片压缩功能的示例;demo2 为测试 es6 语法的示例,进入 demo2 目录,执行 `npm install`,然后 `npm start` 运行 demo2demo1、demo2demo3 都共用一个 server,请注意 server 文件里的 `region` 设置跟 `config` 里的` region` 设置要保持一致。
432466

433467

434468
<a id="note"></a>

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "qiniu-js",
33
"jsName": "qiniu",
4-
"version": "2.2.2",
4+
"version": "2.3.0",
55
"private": false,
66
"description": "Javascript SDK for Qiniu Resource (Cloud) Storage AP",
77
"main": "dist/qiniu.min.js",
@@ -53,7 +53,8 @@
5353
"uglifyjs-webpack-plugin": "^1.1.6",
5454
"webpack": "^3.6.0",
5555
"webpack-dev-server": "^2.9.1",
56-
"webpack-merge": "^4.1.1"
56+
"webpack-merge": "^4.1.1",
57+
"exif-js": "^2.3.0"
5758
},
5859
"license": "MIT"
5960
}

src/compress.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { EXIF } from "exif-js";
2+
import { createObjectURL, getTransform } from "./utils";
3+
4+
let mimeTypes = {
5+
PNG: "image/png",
6+
JPEG: "image/jpeg",
7+
WEBP: "image/webp",
8+
BMP: "image/bmp"
9+
};
10+
11+
let maxSteps = 4;
12+
let scaleFactor = Math.log(2);
13+
let supportMimeTypes = Object.keys(mimeTypes).map(type => mimeTypes[type]);
14+
let defaultType = mimeTypes.JPEG;
15+
16+
function isSupportedType(type) {
17+
return supportMimeTypes.includes(type);
18+
}
19+
20+
class Compress {
21+
constructor(file, option) {
22+
this.config = Object.assign(
23+
{
24+
quality:0.92,
25+
noCompressIfLarger:false
26+
},
27+
option
28+
);
29+
this.file = file;
30+
}
31+
32+
process() {
33+
this.outputType = this.file.type;
34+
let srcDimension = {};
35+
if (!isSupportedType(this.file.type)) {
36+
return Promise.reject(new Error(`unsupported file type: ${this.file.type}`));
37+
}
38+
39+
return this.getOriginImage()
40+
.then(img => {
41+
return this.getCanvas(img);
42+
})
43+
.then(canvas => {
44+
// 计算图片缩小比例,取最小值,如果大于1则保持图片原尺寸
45+
let scale = 1;
46+
if (this.config.maxWidth) {
47+
scale = Math.min(1, this.config.maxWidth / canvas.width);
48+
}
49+
if (this.config.maxHeight) {
50+
scale = Math.min(1, scale, this.config.maxHeight / canvas.height);
51+
}
52+
srcDimension.width = canvas.width;
53+
srcDimension.height = canvas.height;
54+
return this.doScale(canvas, scale);
55+
})
56+
.then(result => {
57+
let distBlob = this.toBlob(result);
58+
if (distBlob.size > this.file.size && this.config.noCompressIfLarger){
59+
return {
60+
dist: this.file,
61+
width: srcDimension.width,
62+
height: srcDimension.height
63+
};
64+
}
65+
return ({
66+
dist: distBlob,
67+
width: result.width,
68+
height: result.height
69+
});
70+
});
71+
}
72+
73+
clear(ctx, width, height) {
74+
// jpeg 没有 alpha 通道,透明区间会被填充成黑色,这里把透明区间填充为白色
75+
if (this.outputType === defaultType) {
76+
ctx.fillStyle = "#fff";
77+
ctx.fillRect(0, 0, width, height);
78+
} else {
79+
ctx.clearRect(0, 0, width, height);
80+
}
81+
}
82+
// 通过 file 初始化 image 对象
83+
getOriginImage() {
84+
return new Promise((resolve, reject) => {
85+
let url = createObjectURL(this.file);
86+
let img = new Image();
87+
img.onload = () => {
88+
resolve(img);
89+
};
90+
img.onerror = () => {
91+
reject("image load error");
92+
};
93+
img.src = url;
94+
});
95+
}
96+
97+
getCanvas(img) {
98+
return new Promise((resolve, reject) => {
99+
// 通过得到图片的信息来调整显示方向以正确显示图片,主要解决 ios 系统上的图片会有旋转的问题
100+
EXIF.getData(img, () => {
101+
let orientation = EXIF.getTag(img, "Orientation") || 1;
102+
let { width, height, matrix } = getTransform(img, orientation);
103+
let canvas = document.createElement("canvas");
104+
let context = canvas.getContext("2d");
105+
canvas.width = width;
106+
canvas.height = height;
107+
this.clear(context, width, height);
108+
context.transform(...matrix);
109+
context.drawImage(img, 0, 0);
110+
resolve(canvas);
111+
});
112+
});
113+
}
114+
115+
doScale(source, scale) {
116+
if (scale === 1) {
117+
return Promise.resolve(source);
118+
}
119+
// 不要一次性画图,通过设定的 step 次数,渐进式的画图,这样可以增加图片的清晰度,防止一次性画图导致的像素丢失严重
120+
let sctx = source.getContext("2d");
121+
let steps = Math.min(maxSteps, Math.ceil((1 / scale) / scaleFactor));
122+
123+
let factor = Math.pow(scale, 1 / steps);
124+
125+
let mirror = document.createElement("canvas");
126+
let mctx = mirror.getContext("2d");
127+
128+
let { width, height } = source;
129+
let originWidth = width;
130+
let originHeight = height;
131+
mirror.width = width;
132+
mirror.height = height;
133+
let src, context;
134+
135+
for (let i = 0; i < steps; i++) {
136+
137+
let dw = width * factor | 0;
138+
let dh = height * factor | 0;
139+
// 到最后一步的时候 dw, dh 用 目标缩放尺寸,否则会出现最后尺寸偏小的情况
140+
if (i === steps - 1) {
141+
dw = originWidth * scale;
142+
dh = originHeight * scale;
143+
}
144+
145+
if (i % 2 === 0) {
146+
src = source;
147+
context = mctx;
148+
} else {
149+
src = mirror;
150+
context = sctx;
151+
}
152+
// 每次画前都清空,避免图像重叠
153+
this.clear(context, width, height);
154+
context.drawImage(src, 0, 0, width, height, 0, 0, dw, dh);
155+
width = dw;
156+
height = dh;
157+
}
158+
159+
let canvas = src === source ? mirror : source;
160+
// save data
161+
let data = context.getImageData(0, 0, width, height);
162+
163+
// resize
164+
canvas.width = width;
165+
canvas.height = height;
166+
167+
// store image data
168+
context.putImageData(data, 0, 0);
169+
170+
return Promise.resolve(canvas);
171+
}
172+
173+
// 这里把 base64 字符串转为 blob 对象
174+
toBlob(result) {
175+
let dataURL = result.toDataURL(this.outputType, this.config.quality);
176+
let buffer = atob(dataURL.split(",")[1]).split("").map(char => char.charCodeAt(0));
177+
let blob = new Blob([new Uint8Array(buffer)], { type: this.outputType });
178+
return blob;
179+
}
180+
}
181+
182+
let compressImage = (file, options) => {
183+
return new Compress(file, options).process();
184+
};
185+
186+
export default compressImage;

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { UploadManager } from "./upload";
1010
import { imageMogr2, watermark, imageInfo, exif, pipeline } from "./image";
1111
import { Observable } from "./observable";
1212
import { StatisticsLogger } from "./statisticsLog";
13-
13+
import compressImage from "./compress";
1414
let statisticsLogger = new StatisticsLogger();
1515

1616
function upload(file, key, token, putExtra, config) {
@@ -46,5 +46,6 @@ export {
4646
watermark,
4747
imageInfo,
4848
exif,
49+
compressImage,
4950
pipeline
5051
};

src/upload.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export class UploadManager {
6666
if (!isContainFileMimeType(this.file.type, this.putExtra.mimeType)){
6767
let err = new Error("file type doesn't match with what you specify");
6868
this.onError(err);
69-
return Promise.reject(err);
69+
return;
7070
}
7171
}
7272
let upload = getUploadUrl(this.config, this.token).then(res => {
@@ -80,7 +80,7 @@ export class UploadManager {
8080
this.sendLog(res.reqId, 200);
8181
}
8282
}, err => {
83-
83+
8484
this.clear();
8585
if (err.isRequestError && !this.config.disableStatisticsReport) {
8686
let reqId = this.aborted ? "" : err.reqId;
@@ -203,13 +203,13 @@ export class UploadManager {
203203
if (savedReusable && !shouldCheckMD5) {
204204
return reuseSaved();
205205
}
206-
206+
207207
return computeMd5(chunk).then(md5 => {
208208

209209
if (savedReusable && md5 === info.md5) {
210210
return reuseSaved();
211211
}
212-
212+
213213
let headers = getHeadersForChunkUpload(this.token);
214214
let onProgress = data => {
215215
this.updateChunkProgress(data.loaded, index);

src/utils.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,67 @@ function getUpHosts(token) {
253253
export function isContainFileMimeType(fileType, mimeType){
254254
return mimeType.indexOf(fileType) > -1;
255255
}
256+
257+
export function createObjectURL(file) {
258+
let URL = window.URL || window.webkitURL || window.mozURL;
259+
return URL.createObjectURL(file);
260+
}
261+
262+
export function getTransform(image, orientation) {
263+
let { width, height } = image;
264+
265+
switch (orientation) {
266+
case 1:
267+
// default
268+
return {
269+
width, height,
270+
matrix: [1, 0, 0, 1, 0, 0]
271+
};
272+
case 2:
273+
// horizontal flip
274+
return {
275+
width, height,
276+
matrix: [-1, 0, 0, 1, width, 0]
277+
};
278+
case 3:
279+
// 180° rotated
280+
return {
281+
width, height,
282+
matrix: [-1, 0, 0, -1, width, height]
283+
};
284+
case 4:
285+
// vertical flip
286+
return {
287+
width, height,
288+
matrix: [1, 0, 0, -1, 0, height]
289+
};
290+
case 5:
291+
// vertical flip + -90° rotated
292+
return {
293+
width: height,
294+
height: width,
295+
matrix: [0, 1, 1, 0, 0, 0]
296+
};
297+
case 6:
298+
// -90° rotated
299+
return {
300+
width: height,
301+
height: width,
302+
matrix: [0, 1, -1, 0, height, 0]
303+
};
304+
case 7:
305+
// horizontal flip + -90° rotate
306+
return {
307+
width: height,
308+
height: width,
309+
matrix: [0, -1, -1, 0, height, width]
310+
};
311+
case 8:
312+
// 90° rotated
313+
return {
314+
width: height,
315+
height: width,
316+
matrix: [0, -1, 1, 0, 0, width]
317+
};
318+
}
319+
}

0 commit comments

Comments
 (0)