Skip to content

添加图片压缩功能 #363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ Qiniu-JavaScript-SDK 的示例 [Demo](http://jssdk-v2.demo.qiniu.io) 中的服

### Example

文件上传:

```JavaScript

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

subscription.unsubscribe() // 上传取消
```
图片上传前压缩:

```JavaScript
let options = {
quality: 0.92,
noCompressIfLarger: true
// maxWidth: 1000,
// maxHeight: 618
}
qiniu.compressImage(file, options).then(data => {
var observable = qiniu.upload(data.dist, key, token, putExtra, config)
var subscription = observable.subscribe(observer) // 上传开始
})
```
## API Reference Interface

### qiniu.upload(file: blob, key: string, token: string, putExtra: object, config: object): observable
Expand Down Expand Up @@ -251,6 +266,25 @@ subscription.unsubscribe() // 上传取消
multipart_params_obj[k[0]] = k[1]
}
```
### qiniu.compressImage(file: blob, options: object) : Promise (上传前图片压缩)

```JavaScript
var imgLink = qiniu.compressImage(file, options).then(res => {
// res : {
// dist: 压缩后输出的 blob 对象,或原始的 file,具体看下面的 options 配置
// width: 压缩后的图片宽度
// height: 压缩后的图片高度
// }
}
})
```
* file: 要压缩的源图片,为 `blob` 对象,支持 `image/png`、`image/jpeg`、`image/bmp`、`image/webp` 这几种图片类型
* options: `object`
* options.quality: `number`,图片压缩质量,在图片格式为 `image/jpeg` 或 `image/webp` 的情况下生效,其他格式不会生效,可以从 0 到 1 的区间内选择图片的质量。默认值 0.92
* options.maxWidh: `number`,压缩图片的最大宽度值
* options.maxHeight: `number`,压缩图片的最大高度值
(注意:当 `maxWidth` 和 `maxHeight` 都不设置时,则采用原图尺寸大小)
* options.noCompressIfLarger: `boolean`,为 `true` 时如果发现压缩后图片大小比原来还大,则返回源图片(即输出的 dist 直接返回了输入的 file);默认 `false`,即保证图片尺寸符合要求,但不保证压缩后的图片体积一定变小

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

Expand Down Expand Up @@ -428,7 +462,7 @@ subscription.unsubscribe() // 上传取消
"Domain": "<Your Bucket Domain>" // Bucket 的外链默认域名,在 Bucket 的内容管理里查看,如:'http://xxx.bkt.clouddn.com/'
}
```
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` 设置要保持一致。
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` 设置要保持一致。


<a id="note"></a>
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "qiniu-js",
"jsName": "qiniu",
"version": "2.2.2",
"version": "2.3.0",
"private": false,
"description": "Javascript SDK for Qiniu Resource (Cloud) Storage AP",
"main": "dist/qiniu.min.js",
Expand Down Expand Up @@ -53,7 +53,8 @@
"uglifyjs-webpack-plugin": "^1.1.6",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.1"
"webpack-merge": "^4.1.1",
"exif-js": "^2.3.0"
},
"license": "MIT"
}
186 changes: 186 additions & 0 deletions src/compress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { EXIF } from "exif-js";
import { createObjectURL, getTransform } from "./utils";

let mimeTypes = {
PNG: "image/png",
JPEG: "image/jpeg",
WEBP: "image/webp",
BMP: "image/bmp"
};

let maxSteps = 4;
let scaleFactor = Math.log(2);
let supportMimeTypes = Object.keys(mimeTypes).map(type => mimeTypes[type]);
let defaultType = mimeTypes.JPEG;

function isSupportedType(type) {
return supportMimeTypes.includes(type);
}

class Compress {
constructor(file, option) {
this.config = Object.assign(
{
quality:0.92,
noCompressIfLarger:false
},
option
);
this.file = file;
}

process() {
this.outputType = this.file.type;
let srcDimension = {};
if (!isSupportedType(this.file.type)) {
return Promise.reject(new Error(`unsupported file type: ${this.file.type}`));
}

return this.getOriginImage()
.then(img => {
return this.getCanvas(img);
})
.then(canvas => {
// 计算图片缩小比例,取最小值,如果大于1则保持图片原尺寸
let scale = 1;
if (this.config.maxWidth) {
scale = Math.min(1, this.config.maxWidth / canvas.width);
}
if (this.config.maxHeight) {
scale = Math.min(1, scale, this.config.maxHeight / canvas.height);
}
srcDimension.width = canvas.width;
srcDimension.height = canvas.height;
return this.doScale(canvas, scale);
})
.then(result => {
let distBlob = this.toBlob(result);
if (distBlob.size > this.file.size && this.config.noCompressIfLarger){
return {
dist: this.file,
width: srcDimension.width,
height: srcDimension.height
};
}
return ({
dist: distBlob,
width: result.width,
height: result.height
});
});
}

clear(ctx, width, height) {
// jpeg 没有 alpha 通道,透明区间会被填充成黑色,这里把透明区间填充为白色
if (this.outputType === defaultType) {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
} else {
ctx.clearRect(0, 0, width, height);
}
}
// 通过 file 初始化 image 对象
getOriginImage() {
return new Promise((resolve, reject) => {
let url = createObjectURL(this.file);
let img = new Image();
img.onload = () => {
resolve(img);
};
img.onerror = () => {
reject("image load error");
};
img.src = url;
});
}

getCanvas(img) {
return new Promise((resolve, reject) => {
// 通过得到图片的信息来调整显示方向以正确显示图片,主要解决 ios 系统上的图片会有旋转的问题
EXIF.getData(img, () => {
let orientation = EXIF.getTag(img, "Orientation") || 1;
let { width, height, matrix } = getTransform(img, orientation);
let canvas = document.createElement("canvas");
let context = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
this.clear(context, width, height);
context.transform(...matrix);
context.drawImage(img, 0, 0);
resolve(canvas);
});
});
}

doScale(source, scale) {
if (scale === 1) {
return Promise.resolve(source);
}
// 不要一次性画图,通过设定的 step 次数,渐进式的画图,这样可以增加图片的清晰度,防止一次性画图导致的像素丢失严重
let sctx = source.getContext("2d");
let steps = Math.min(maxSteps, Math.ceil((1 / scale) / scaleFactor));

let factor = Math.pow(scale, 1 / steps);

let mirror = document.createElement("canvas");
let mctx = mirror.getContext("2d");

let { width, height } = source;
let originWidth = width;
let originHeight = height;
mirror.width = width;
mirror.height = height;
let src, context;

for (let i = 0; i < steps; i++) {

let dw = width * factor | 0;
let dh = height * factor | 0;
// 到最后一步的时候 dw, dh 用 目标缩放尺寸,否则会出现最后尺寸偏小的情况
if (i === steps - 1) {
dw = originWidth * scale;
dh = originHeight * scale;
}

if (i % 2 === 0) {
src = source;
context = mctx;
} else {
src = mirror;
context = sctx;
}
// 每次画前都清空,避免图像重叠
this.clear(context, width, height);
context.drawImage(src, 0, 0, width, height, 0, 0, dw, dh);
width = dw;
height = dh;
}

let canvas = src === source ? mirror : source;
// save data
let data = context.getImageData(0, 0, width, height);

// resize
canvas.width = width;
canvas.height = height;

// store image data
context.putImageData(data, 0, 0);

return Promise.resolve(canvas);
}

// 这里把 base64 字符串转为 blob 对象
toBlob(result) {
let dataURL = result.toDataURL(this.outputType, this.config.quality);
let buffer = atob(dataURL.split(",")[1]).split("").map(char => char.charCodeAt(0));
let blob = new Blob([new Uint8Array(buffer)], { type: this.outputType });
return blob;
}
}

let compressImage = (file, options) => {
return new Compress(file, options).process();
};

export default compressImage;
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { UploadManager } from "./upload";
import { imageMogr2, watermark, imageInfo, exif, pipeline } from "./image";
import { Observable } from "./observable";
import { StatisticsLogger } from "./statisticsLog";

import compressImage from "./compress";
let statisticsLogger = new StatisticsLogger();

function upload(file, key, token, putExtra, config) {
Expand Down Expand Up @@ -46,5 +46,6 @@ export {
watermark,
imageInfo,
exif,
compressImage,
pipeline
};
8 changes: 4 additions & 4 deletions src/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class UploadManager {
if (!isContainFileMimeType(this.file.type, this.putExtra.mimeType)){
let err = new Error("file type doesn't match with what you specify");
this.onError(err);
return Promise.reject(err);
return;
}
}
let upload = getUploadUrl(this.config, this.token).then(res => {
Expand All @@ -80,7 +80,7 @@ export class UploadManager {
this.sendLog(res.reqId, 200);
}
}, err => {

this.clear();
if (err.isRequestError && !this.config.disableStatisticsReport) {
let reqId = this.aborted ? "" : err.reqId;
Expand Down Expand Up @@ -203,13 +203,13 @@ export class UploadManager {
if (savedReusable && !shouldCheckMD5) {
return reuseSaved();
}

return computeMd5(chunk).then(md5 => {

if (savedReusable && md5 === info.md5) {
return reuseSaved();
}

let headers = getHeadersForChunkUpload(this.token);
let onProgress = data => {
this.updateChunkProgress(data.loaded, index);
Expand Down
64 changes: 64 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,67 @@ function getUpHosts(token) {
export function isContainFileMimeType(fileType, mimeType){
return mimeType.indexOf(fileType) > -1;
}

export function createObjectURL(file) {
let URL = window.URL || window.webkitURL || window.mozURL;
return URL.createObjectURL(file);
}

export function getTransform(image, orientation) {
let { width, height } = image;

switch (orientation) {
case 1:
// default
return {
width, height,
matrix: [1, 0, 0, 1, 0, 0]
};
case 2:
// horizontal flip
return {
width, height,
matrix: [-1, 0, 0, 1, width, 0]
};
case 3:
// 180° rotated
return {
width, height,
matrix: [-1, 0, 0, -1, width, height]
};
case 4:
// vertical flip
return {
width, height,
matrix: [1, 0, 0, -1, 0, height]
};
case 5:
// vertical flip + -90° rotated
return {
width: height,
height: width,
matrix: [0, 1, 1, 0, 0, 0]
};
case 6:
// -90° rotated
return {
width: height,
height: width,
matrix: [0, 1, -1, 0, height, 0]
};
case 7:
// horizontal flip + -90° rotate
return {
width: height,
height: width,
matrix: [0, -1, -1, 0, height, width]
};
case 8:
// 90° rotated
return {
width: height,
height: width,
matrix: [0, -1, 1, 0, 0, width]
};
}
}
Loading