JS实现图片压缩比较简单,但是图片经过压缩后,压缩后的图片的元信息(拍摄时间、设备、地点)等会丢失掉,如果在特殊场景中需要使用这些元信息的话,就会出现问题了,因此需要将未压缩前的图片元信息填充至压缩后的图片中,以下是实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// 封装一个获取变量的数据类型函数
const getType = (data: unknown): string => {
const toStingResult = Object.prototype.toString.call(data);
const type = toStingResult.replace(/^\[object (\w+)\]$/, "$1");
return type.toLowerCase();
};

// 封装一个将 Base64 的字符串转换成 Blob 流的函数
const dataURLtoBlob = (dataURL: string): Blob | null => {
const dataType = getType(dataURL);
if (dataType !== "string") return null;
const arr = dataURL.split(",");
if (!arr[0] || !arr[1]) return null;
const code = window.atob(arr[1]);
const mimeExpRes = arr[0].match(/:(.*?);/);
if (!mimeExpRes) return null;
let len = code.length;
const mime = mimeExpRes[1];
if (!mime) return null;
const ia = new Uint8Array(len);
while (len--) ia[len] = code.charCodeAt(len);
return new Blob([ia], { type: mime });
};

// 利用规律编码格式把里面的标记以及值等分割开来,传原图片的 ArrayBuffer 进来
const getSegments = (arrayBuffer: ArrayBuffer): number[][] => {
if (!arrayBuffer.byteLength) return [];
let head = 0;
let length, endPoint, seg;
const segments = [];
const arr = [].slice.call(new Uint8Array(arrayBuffer), 0);
while (1) {
if (arr[head] === 0xff && arr[head + 1] === 0xda) break;
if (arr[head] === 0xff && arr[head + 1] === 0xd8) {
head += 2;
} else {
length = arr[head + 2] * 256 + arr[head + 3];
endPoint = head + length + 2;
seg = arr.slice(head, endPoint);
head = endPoint;
segments.push(seg);
}
if (head > arr.length) break;
}
return segments;
};

// 传入上面 getSegments 的返回值,取出EXIF图片元信息
const getEXIF = (segments: number[][]): Array<number> => {
if (!segments.length) return [];
let seg: Array<number> = [];
for (let i = 0; i < segments.length; i++) {
const item = segments[i];
if (item[0] === 0xff && item[1] === 0xe1) {
seg = seg.concat(item);
}
}
return seg;
};

// 将 getEXIF 获取的元信息,插入到压缩后的图片的 Blob 中,传 压缩图片后的 Blob 流
const insertEXIF = (blob: Blob, exif: number[]): Promise<Blob> => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => {
const arr = [].slice.call(new Uint8Array(fileReader.result as ArrayBuffer), 0);
if (arr[2] !== 0xff || arr[3] !== 0xe0) {
return reject(new Error("Couldn't find APP0 marker from blob data"));
}
const length = arr[4] * 256 + arr[5];
const newImage = [0xff, 0xd8].concat(exif, arr.slice(4 + length));
const uint8Array = new Uint8Array(newImage);
const newBlob = new Blob([uint8Array], { type: "image/jpeg" });
resolve(newBlob);
};
fileReader.readAsArrayBuffer(blob);
});
};

// 压缩图片逻辑
const compressImage = (file: File, quality: number): Promise<Blob | null> => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => {
const img = new Image();
img.src = fileReader.result as string;
img.onload = () => {
const { width, height } = img;
const canvas = window.document.createElement("canvas");
const ctx = <CanvasRenderingContext2D>canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
const fileData = canvas.toDataURL("image/jpeg", quality);
const fileBlob = dataURLtoBlob(fileData);
resolve(fileBlob);
};
img.onerror = (err) => reject(err);
};
fileReader.onerror = (err) => reject(err);
fileReader.readAsDataURL(file);
});
};

/**
* @description: 完整的压缩图片,最终对外暴露的函数
* @param {File} file
* @param {number} quality 0 - 1
* @return {Promise<File>}
*/
export default (file: File, quality = 0.5): Promise<File> => {
return new Promise((resolve, reject) => {
const dataType = getType(file);
if (dataType !== "file") return reject(new Error(`Expected parameter type is file, You passed in ${dataType}`));
if (file.type.indexOf("image") === -1) return resolve(file);
// 压缩图片
compressImage(file, quality)
.then((compressdBlob) => {
if (!compressdBlob) return resolve(file);
const fileReader = new FileReader();
fileReader.onload = () => {
// 获取图片元信息
const segments = getSegments(fileReader.result as ArrayBuffer);
const exif = getEXIF(segments);
// 没有元数据的时候, 直接抛出压缩后的图片
if (!exif.length) return resolve(new File([compressdBlob], file.name, { type: file.type, lastModified: file.lastModified }));
// 有元数据的时候, 将元信息合并到压缩图片里
insertEXIF(compressdBlob, exif)
.then((newBlob) => resolve(new File([newBlob], file.name, { type: file.type, lastModified: file.lastModified })))
.catch(() => resolve(file));
};
fileReader.onerror = () => resolve(file);
fileReader.readAsArrayBuffer(file);
})
.catch(() => resolve(file));
});
};