id-scanner-lib 1.2.2 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +375 -363
- package/dist/id-scanner-core.esm.js +427 -221
- package/dist/id-scanner-core.esm.js.map +1 -1
- package/dist/id-scanner-core.js +427 -221
- package/dist/id-scanner-core.js.map +1 -1
- package/dist/id-scanner-core.min.js +1 -9
- package/dist/id-scanner-core.min.js.map +1 -1
- package/dist/id-scanner-ocr.esm.js +451 -276
- package/dist/id-scanner-ocr.esm.js.map +1 -1
- package/dist/id-scanner-ocr.js +451 -276
- package/dist/id-scanner-ocr.js.map +1 -1
- package/dist/id-scanner-ocr.min.js +1 -9
- package/dist/id-scanner-ocr.min.js.map +1 -1
- package/dist/id-scanner-qr.esm.js +483 -233
- package/dist/id-scanner-qr.esm.js.map +1 -1
- package/dist/id-scanner-qr.js +482 -232
- package/dist/id-scanner-qr.js.map +1 -1
- package/dist/id-scanner-qr.min.js +1 -9
- package/dist/id-scanner-qr.min.js.map +1 -1
- package/dist/id-scanner.js +2138 -358
- package/dist/id-scanner.js.map +1 -1
- package/dist/id-scanner.min.js +1 -9
- package/dist/id-scanner.min.js.map +1 -1
- package/package.json +27 -7
- package/src/demo/demo.ts +178 -62
- package/src/id-recognition/anti-fake-detector.ts +317 -0
- package/src/id-recognition/id-detector.ts +184 -155
- package/src/id-recognition/ocr-processor.ts +193 -146
- package/src/id-recognition/ocr-worker.ts +82 -72
- package/src/index-umd.ts +347 -110
- package/src/index.ts +866 -91
- package/src/ocr-module.ts +108 -60
- package/src/qr-module.ts +104 -54
- package/src/scanner/barcode-scanner.ts +145 -58
- package/src/scanner/qr-scanner.ts +86 -47
- package/src/utils/image-processing.ts +479 -294
- package/dist/core.d.ts +0 -77
- package/dist/demo/demo.d.ts +0 -14
- package/dist/id-recognition/data-extractor.d.ts +0 -105
- package/dist/id-recognition/id-detector.d.ts +0 -100
- package/dist/id-recognition/ocr-processor.d.ts +0 -64
- package/dist/id-scanner.esm.js +0 -94656
- package/dist/id-scanner.esm.js.map +0 -1
- package/dist/index-umd.d.ts +0 -96
- package/dist/index.d.ts +0 -78
- package/dist/ocr-module.d.ts +0 -67
- package/dist/qr-module.d.ts +0 -68
- package/dist/scanner/barcode-scanner.d.ts +0 -90
- package/dist/scanner/qr-scanner.d.ts +0 -80
- package/dist/types/core.d.ts +0 -77
- package/dist/types/demo/demo.d.ts +0 -14
- package/dist/types/id-recognition/data-extractor.d.ts +0 -105
- package/dist/types/id-recognition/id-detector.d.ts +0 -100
- package/dist/types/id-recognition/ocr-processor.d.ts +0 -64
- package/dist/types/index-umd.d.ts +0 -96
- package/dist/types/index.d.ts +0 -78
- package/dist/types/ocr-module.d.ts +0 -67
- package/dist/types/qr-module.d.ts +0 -68
- package/dist/types/scanner/barcode-scanner.d.ts +0 -90
- package/dist/types/scanner/qr-scanner.d.ts +0 -80
- package/dist/types/utils/camera.d.ts +0 -81
- package/dist/types/utils/image-processing.d.ts +0 -75
- package/dist/types/utils/types.d.ts +0 -65
- package/dist/utils/camera.d.ts +0 -81
- package/dist/utils/image-processing.d.ts +0 -75
- package/dist/utils/types.d.ts +0 -65
|
@@ -1,361 +1,546 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file 图像处理工具类
|
|
3
|
-
* @description
|
|
3
|
+
* @description 提供图像预处理功能,用于提高OCR识别率
|
|
4
4
|
* @module ImageProcessor
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import imageCompression from "browser-image-compression"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 图像处理器配置选项
|
|
11
|
+
*/
|
|
12
|
+
export interface ImageProcessorOptions {
|
|
13
|
+
brightness?: number // 亮度调整,范围 -100 到 100
|
|
14
|
+
contrast?: number // 对比度调整,范围 -100 到 100
|
|
15
|
+
grayscale?: boolean // 是否转换为灰度图
|
|
16
|
+
invert?: boolean // 是否反转颜色
|
|
17
|
+
blur?: number // 模糊程度 (0-10)
|
|
18
|
+
sharpen?: boolean // 是否锐化
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 图像压缩选项
|
|
23
|
+
*/
|
|
24
|
+
export interface ImageCompressionOptions {
|
|
25
|
+
maxSizeMB?: number // 图片最大大小,MB
|
|
26
|
+
maxWidthOrHeight?: number // 图片最大宽度或高度
|
|
27
|
+
useWebWorker?: boolean // 是否使用Web Worker处理
|
|
28
|
+
maxIteration?: number // 最大压缩迭代次数
|
|
29
|
+
quality?: number // 输出质量 (0-1)
|
|
30
|
+
fileType?: string // 输出文件类型 ('image/jpeg', 'image/png' 等)
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
/**
|
|
8
34
|
* 图像处理工具类
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 这些功能可用于增强图像质量,提高OCR和扫描的识别率。
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```typescript
|
|
15
|
-
* // 使用图像处理功能增强图像
|
|
16
|
-
* const enhancedImage = ImageProcessor.adjustBrightnessContrast(
|
|
17
|
-
* originalImageData,
|
|
18
|
-
* 15, // 增加亮度
|
|
19
|
-
* 25 // 增加对比度
|
|
20
|
-
* );
|
|
21
|
-
*
|
|
22
|
-
* // 转换为灰度图像
|
|
23
|
-
* const grayImage = ImageProcessor.toGrayscale(originalImageData);
|
|
24
|
-
* ```
|
|
35
|
+
*
|
|
36
|
+
* 提供各种图像处理功能,用于优化识别效果
|
|
25
37
|
*/
|
|
26
38
|
export class ImageProcessor {
|
|
27
39
|
/**
|
|
28
40
|
* 将ImageData转换为Canvas元素
|
|
29
|
-
*
|
|
41
|
+
*
|
|
30
42
|
* @param {ImageData} imageData - 要转换的图像数据
|
|
31
43
|
* @returns {HTMLCanvasElement} 包含图像的Canvas元素
|
|
32
44
|
*/
|
|
33
45
|
static imageDataToCanvas(imageData: ImageData): HTMLCanvasElement {
|
|
34
|
-
const canvas = document.createElement(
|
|
35
|
-
canvas.width = imageData.width
|
|
36
|
-
canvas.height = imageData.height
|
|
37
|
-
const ctx = canvas.getContext(
|
|
38
|
-
|
|
46
|
+
const canvas = document.createElement("canvas")
|
|
47
|
+
canvas.width = imageData.width
|
|
48
|
+
canvas.height = imageData.height
|
|
49
|
+
const ctx = canvas.getContext("2d")
|
|
50
|
+
|
|
39
51
|
if (ctx) {
|
|
40
|
-
ctx.putImageData(imageData, 0, 0)
|
|
52
|
+
ctx.putImageData(imageData, 0, 0)
|
|
41
53
|
}
|
|
42
|
-
|
|
43
|
-
return canvas
|
|
54
|
+
|
|
55
|
+
return canvas
|
|
44
56
|
}
|
|
45
|
-
|
|
57
|
+
|
|
46
58
|
/**
|
|
47
59
|
* 将Canvas转换为ImageData
|
|
48
|
-
*
|
|
60
|
+
*
|
|
49
61
|
* @param {HTMLCanvasElement} canvas - 要转换的Canvas元素
|
|
50
62
|
* @returns {ImageData|null} Canvas的图像数据,如果获取失败则返回null
|
|
51
63
|
*/
|
|
52
64
|
static canvasToImageData(canvas: HTMLCanvasElement): ImageData | null {
|
|
53
|
-
const ctx = canvas.getContext(
|
|
54
|
-
return ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null
|
|
65
|
+
const ctx = canvas.getContext("2d")
|
|
66
|
+
return ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null
|
|
55
67
|
}
|
|
56
|
-
|
|
68
|
+
|
|
57
69
|
/**
|
|
58
70
|
* 调整图像亮度和对比度
|
|
59
|
-
*
|
|
60
|
-
* @param
|
|
61
|
-
* @param
|
|
62
|
-
* @param
|
|
63
|
-
* @returns
|
|
71
|
+
*
|
|
72
|
+
* @param imageData 原始图像数据
|
|
73
|
+
* @param brightness 亮度调整值 (-100到100)
|
|
74
|
+
* @param contrast 对比度调整值 (-100到100)
|
|
75
|
+
* @returns 处理后的图像数据
|
|
64
76
|
*/
|
|
65
|
-
static adjustBrightnessContrast(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
static adjustBrightnessContrast(
|
|
78
|
+
imageData: ImageData,
|
|
79
|
+
brightness: number = 0,
|
|
80
|
+
contrast: number = 0
|
|
81
|
+
): ImageData {
|
|
82
|
+
// 将亮度和对比度范围限制在 -100 到 100 之间
|
|
83
|
+
brightness = Math.max(-100, Math.min(100, brightness))
|
|
84
|
+
contrast = Math.max(-100, Math.min(100, contrast))
|
|
85
|
+
|
|
86
|
+
// 将范围转换为适合计算的值
|
|
87
|
+
const factor = (259 * (contrast + 255)) / (255 * (259 - contrast))
|
|
88
|
+
const briAdjust = (brightness / 100) * 255
|
|
89
|
+
|
|
90
|
+
const data = imageData.data
|
|
91
|
+
const length = data.length
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < length; i += 4) {
|
|
94
|
+
// 分别处理 RGB 三个通道
|
|
95
|
+
for (let j = 0; j < 3; j++) {
|
|
96
|
+
// 应用亮度和对比度调整公式
|
|
97
|
+
const newValue = factor * (data[i + j] + briAdjust - 128) + 128
|
|
98
|
+
data[i + j] = Math.max(0, Math.min(255, newValue))
|
|
84
99
|
}
|
|
85
|
-
|
|
86
|
-
ctx.putImageData(imgData, 0, 0);
|
|
87
|
-
return imgData;
|
|
100
|
+
// Alpha 通道保持不变
|
|
88
101
|
}
|
|
89
|
-
|
|
90
|
-
return imageData
|
|
102
|
+
|
|
103
|
+
return imageData
|
|
91
104
|
}
|
|
92
|
-
|
|
105
|
+
|
|
93
106
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* @
|
|
97
|
-
* @
|
|
98
|
-
* @returns {number} 截断后的值,范围为0-255
|
|
107
|
+
* 将图像转换为灰度图
|
|
108
|
+
*
|
|
109
|
+
* @param imageData 原始图像数据
|
|
110
|
+
* @returns 灰度图像数据
|
|
99
111
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
static toGrayscale(imageData: ImageData): ImageData {
|
|
113
|
+
const data = imageData.data
|
|
114
|
+
const length = data.length
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < length; i += 4) {
|
|
117
|
+
// 使用加权平均法将 RGB 转换为灰度值
|
|
118
|
+
const gray = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11
|
|
119
|
+
data[i] = data[i + 1] = data[i + 2] = gray
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return imageData
|
|
102
123
|
}
|
|
103
|
-
|
|
124
|
+
|
|
104
125
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* @
|
|
110
|
-
* @returns {ImageData} 转换后的灰度图像
|
|
126
|
+
* 锐化图像
|
|
127
|
+
*
|
|
128
|
+
* @param imageData 原始图像数据
|
|
129
|
+
* @param amount 锐化程度,默认为2
|
|
130
|
+
* @returns 锐化后的图像数据
|
|
111
131
|
*/
|
|
112
|
-
static
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
static sharpen(imageData: ImageData, amount: number = 2): ImageData {
|
|
133
|
+
if (!imageData || !imageData.data) return imageData
|
|
134
|
+
|
|
135
|
+
const width = imageData.width
|
|
136
|
+
const height = imageData.height
|
|
137
|
+
const data = imageData.data
|
|
138
|
+
|
|
139
|
+
const outputData = new Uint8ClampedArray(data.length)
|
|
140
|
+
|
|
141
|
+
// 锐化卷积核
|
|
142
|
+
const kernel = [
|
|
143
|
+
0,
|
|
144
|
+
-amount,
|
|
145
|
+
0,
|
|
146
|
+
-amount,
|
|
147
|
+
1 + 4 * amount,
|
|
148
|
+
-amount,
|
|
149
|
+
0,
|
|
150
|
+
-amount,
|
|
151
|
+
0,
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
// 应用卷积
|
|
155
|
+
for (let y = 1; y < height - 1; y++) {
|
|
156
|
+
for (let x = 1; x < width - 1; x++) {
|
|
157
|
+
const pos = (y * width + x) * 4
|
|
158
|
+
|
|
159
|
+
// 对每个通道应用卷积
|
|
160
|
+
for (let c = 0; c < 3; c++) {
|
|
161
|
+
let val = 0
|
|
162
|
+
for (let ky = -1; ky <= 1; ky++) {
|
|
163
|
+
for (let kx = -1; kx <= 1; kx++) {
|
|
164
|
+
const kernelPos = (ky + 1) * 3 + (kx + 1)
|
|
165
|
+
const dataPos = ((y + ky) * width + (x + kx)) * 4 + c
|
|
166
|
+
val += data[dataPos] * kernel[kernelPos]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
outputData[pos + c] = Math.max(0, Math.min(255, val))
|
|
170
|
+
}
|
|
171
|
+
outputData[pos + 3] = data[pos + 3] // 保持透明度不变
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 处理边缘像素
|
|
176
|
+
for (let y = 0; y < height; y++) {
|
|
177
|
+
for (let x = 0; x < width; x++) {
|
|
178
|
+
if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
|
|
179
|
+
const pos = (y * width + x) * 4
|
|
180
|
+
outputData[pos] = data[pos]
|
|
181
|
+
outputData[pos + 1] = data[pos + 1]
|
|
182
|
+
outputData[pos + 2] = data[pos + 2]
|
|
183
|
+
outputData[pos + 3] = data[pos + 3]
|
|
184
|
+
}
|
|
126
185
|
}
|
|
127
|
-
|
|
128
|
-
ctx.putImageData(imgData, 0, 0);
|
|
129
|
-
return imgData;
|
|
130
186
|
}
|
|
131
|
-
|
|
132
|
-
|
|
187
|
+
|
|
188
|
+
// 创建新的ImageData对象
|
|
189
|
+
return new ImageData(outputData, width, height)
|
|
133
190
|
}
|
|
134
|
-
|
|
191
|
+
|
|
135
192
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* @param
|
|
139
|
-
* @param
|
|
140
|
-
* @
|
|
141
|
-
* @returns {ImageData} 调整大小后的图像数据
|
|
193
|
+
* 对图像应用阈值操作,增强对比度
|
|
194
|
+
*
|
|
195
|
+
* @param imageData 原始图像数据
|
|
196
|
+
* @param threshold 阈值 (0-255)
|
|
197
|
+
* @returns 处理后的图像数据
|
|
142
198
|
*/
|
|
143
|
-
static
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
199
|
+
static threshold(imageData: ImageData, threshold: number = 128): ImageData {
|
|
200
|
+
// 先转换为灰度图
|
|
201
|
+
const grayscaleImage = this.toGrayscale(
|
|
202
|
+
new ImageData(
|
|
203
|
+
new Uint8ClampedArray(imageData.data),
|
|
204
|
+
imageData.width,
|
|
205
|
+
imageData.height
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
const data = grayscaleImage.data
|
|
210
|
+
const length = data.length
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < length; i += 4) {
|
|
213
|
+
// 二值化处理
|
|
214
|
+
const value = data[i] < threshold ? 0 : 255
|
|
215
|
+
data[i] = data[i + 1] = data[i + 2] = value
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return grayscaleImage
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 将图像转换为黑白图像(二值化)
|
|
223
|
+
*
|
|
224
|
+
* @param imageData 原始图像数据
|
|
225
|
+
* @returns 二值化后的图像数据
|
|
226
|
+
*/
|
|
227
|
+
static toBinaryImage(imageData: ImageData): ImageData {
|
|
228
|
+
// 先转换为灰度图
|
|
229
|
+
const grayscaleImage = this.toGrayscale(
|
|
230
|
+
new ImageData(
|
|
231
|
+
new Uint8ClampedArray(imageData.data),
|
|
232
|
+
imageData.width,
|
|
233
|
+
imageData.height
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// 使用OTSU算法自动确定阈值
|
|
238
|
+
const threshold = this.getOtsuThreshold(grayscaleImage)
|
|
239
|
+
|
|
240
|
+
return this.threshold(grayscaleImage, threshold)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 使用OTSU算法计算最佳阈值
|
|
245
|
+
*
|
|
246
|
+
* @param imageData 灰度图像数据
|
|
247
|
+
* @returns 最佳阈值
|
|
248
|
+
*/
|
|
249
|
+
private static getOtsuThreshold(imageData: ImageData): number {
|
|
250
|
+
const data = imageData.data
|
|
251
|
+
const histogram = new Array(256).fill(0)
|
|
252
|
+
|
|
253
|
+
// 统计灰度直方图
|
|
254
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
255
|
+
histogram[data[i]]++
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const total = imageData.width * imageData.height
|
|
259
|
+
let sum = 0
|
|
260
|
+
|
|
261
|
+
// 计算总灰度值和
|
|
262
|
+
for (let i = 0; i < 256; i++) {
|
|
263
|
+
sum += i * histogram[i]
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let sumB = 0
|
|
267
|
+
let wB = 0
|
|
268
|
+
let wF = 0
|
|
269
|
+
let maxVariance = 0
|
|
270
|
+
let threshold = 0
|
|
271
|
+
|
|
272
|
+
// 遍历所有可能的阈值,找到最大类间方差
|
|
273
|
+
for (let t = 0; t < 256; t++) {
|
|
274
|
+
wB += histogram[t] // 背景权重
|
|
275
|
+
if (wB === 0) continue
|
|
276
|
+
|
|
277
|
+
wF = total - wB // 前景权重
|
|
278
|
+
if (wF === 0) break
|
|
279
|
+
|
|
280
|
+
sumB += t * histogram[t]
|
|
281
|
+
|
|
282
|
+
const mB = sumB / wB // 背景平均灰度
|
|
283
|
+
const mF = (sum - sumB) / wF // 前景平均灰度
|
|
284
|
+
|
|
285
|
+
// 计算类间方差
|
|
286
|
+
const variance = wB * wF * (mB - mF) * (mB - mF)
|
|
287
|
+
|
|
288
|
+
if (variance > maxVariance) {
|
|
289
|
+
maxVariance = variance
|
|
290
|
+
threshold = t
|
|
291
|
+
}
|
|
153
292
|
}
|
|
154
|
-
|
|
155
|
-
return
|
|
293
|
+
|
|
294
|
+
return threshold
|
|
156
295
|
}
|
|
157
296
|
|
|
158
297
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* @
|
|
164
|
-
* @param {number} [maxDimension=1000] - 目标最大尺寸(宽或高)
|
|
165
|
-
* @returns {ImageData} 处理后的图像数据
|
|
298
|
+
* 批量应用图像处理
|
|
299
|
+
*
|
|
300
|
+
* @param imageData 原始图像数据
|
|
301
|
+
* @param options 处理选项
|
|
302
|
+
* @returns 处理后的图像数据
|
|
166
303
|
*/
|
|
167
|
-
static
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
304
|
+
static batchProcess(
|
|
305
|
+
imageData: ImageData,
|
|
306
|
+
options: ImageProcessorOptions
|
|
307
|
+
): ImageData {
|
|
308
|
+
let processedImage = new ImageData(
|
|
309
|
+
new Uint8ClampedArray(imageData.data),
|
|
310
|
+
imageData.width,
|
|
311
|
+
imageData.height
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
// 应用亮度和对比度调整
|
|
315
|
+
if (options.brightness !== undefined || options.contrast !== undefined) {
|
|
316
|
+
processedImage = this.adjustBrightnessContrast(
|
|
317
|
+
processedImage,
|
|
318
|
+
options.brightness || 0,
|
|
319
|
+
options.contrast || 0
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 应用灰度转换
|
|
324
|
+
if (options.grayscale) {
|
|
325
|
+
processedImage = this.toGrayscale(processedImage)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 应用锐化
|
|
329
|
+
if (options.sharpen) {
|
|
330
|
+
processedImage = this.sharpen(processedImage)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 应用颜色反转
|
|
334
|
+
if (options.invert) {
|
|
335
|
+
const data = processedImage.data
|
|
336
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
337
|
+
// 反转RGB值
|
|
338
|
+
data[i] = 255 - data[i]
|
|
339
|
+
data[i + 1] = 255 - data[i + 1]
|
|
340
|
+
data[i + 2] = 255 - data[i + 2]
|
|
341
|
+
// Alpha通道保持不变
|
|
342
|
+
}
|
|
173
343
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const scale = maxDimension / Math.max(width, height);
|
|
177
|
-
const newWidth = Math.round(width * scale);
|
|
178
|
-
const newHeight = Math.round(height * scale);
|
|
179
|
-
|
|
180
|
-
// 调整图像大小
|
|
181
|
-
return this.resize(imageData, newWidth, newHeight);
|
|
344
|
+
|
|
345
|
+
return processedImage
|
|
182
346
|
}
|
|
183
347
|
|
|
184
348
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* @param
|
|
188
|
-
* @
|
|
349
|
+
* 压缩图片文件
|
|
350
|
+
*
|
|
351
|
+
* @param file 图片文件
|
|
352
|
+
* @param options 压缩选项
|
|
353
|
+
* @returns Promise<File> 压缩后的文件
|
|
189
354
|
*/
|
|
190
|
-
static
|
|
191
|
-
|
|
192
|
-
|
|
355
|
+
static async compressImage(
|
|
356
|
+
file: File,
|
|
357
|
+
options?: ImageCompressionOptions
|
|
358
|
+
): Promise<File> {
|
|
359
|
+
const defaultOptions = {
|
|
360
|
+
maxSizeMB: 1,
|
|
361
|
+
maxWidthOrHeight: 1920,
|
|
362
|
+
useWebWorker: true,
|
|
363
|
+
quality: 0.8,
|
|
364
|
+
fileType: file.type || "image/jpeg",
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const compressOptions = { ...defaultOptions, ...options }
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
return await imageCompression(file, compressOptions)
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error("图片压缩失败:", error)
|
|
373
|
+
return file // 如果压缩失败,返回原始文件
|
|
374
|
+
}
|
|
193
375
|
}
|
|
194
376
|
|
|
195
377
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* @param
|
|
199
|
-
* @returns
|
|
378
|
+
* 从图片文件创建ImageData
|
|
379
|
+
*
|
|
380
|
+
* @param file 图片文件
|
|
381
|
+
* @returns Promise<ImageData>
|
|
200
382
|
*/
|
|
201
|
-
static async
|
|
383
|
+
static async createImageDataFromFile(file: File): Promise<ImageData> {
|
|
202
384
|
return new Promise((resolve, reject) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
385
|
+
try {
|
|
386
|
+
const img = new Image()
|
|
387
|
+
const url = URL.createObjectURL(file)
|
|
388
|
+
|
|
389
|
+
img.onload = () => {
|
|
390
|
+
try {
|
|
391
|
+
// 创建canvas元素
|
|
392
|
+
const canvas = document.createElement("canvas")
|
|
393
|
+
const ctx = canvas.getContext("2d")
|
|
394
|
+
|
|
395
|
+
if (!ctx) {
|
|
396
|
+
reject(new Error("无法创建2D上下文"))
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
canvas.width = img.width
|
|
401
|
+
canvas.height = img.height
|
|
402
|
+
|
|
403
|
+
// 绘制图片到canvas
|
|
404
|
+
ctx.drawImage(img, 0, 0)
|
|
405
|
+
|
|
406
|
+
// 获取图像数据
|
|
407
|
+
const imageData = ctx.getImageData(
|
|
408
|
+
0,
|
|
409
|
+
0,
|
|
410
|
+
canvas.width,
|
|
411
|
+
canvas.height
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
// 释放资源
|
|
415
|
+
URL.revokeObjectURL(url)
|
|
416
|
+
|
|
417
|
+
resolve(imageData)
|
|
418
|
+
} catch (e) {
|
|
419
|
+
reject(e)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
img.onerror = () => {
|
|
424
|
+
URL.revokeObjectURL(url)
|
|
425
|
+
reject(new Error("图片加载失败"))
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
img.src = url
|
|
429
|
+
} catch (error) {
|
|
430
|
+
reject(error)
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* 将ImageData转换为File对象
|
|
437
|
+
*
|
|
438
|
+
* @param imageData ImageData对象
|
|
439
|
+
* @param fileName 输出文件名
|
|
440
|
+
* @param fileType 输出文件类型
|
|
441
|
+
* @param quality 图片质量 (0-1)
|
|
442
|
+
* @returns Promise<File>
|
|
443
|
+
*/
|
|
444
|
+
static async imageDataToFile(
|
|
445
|
+
imageData: ImageData,
|
|
446
|
+
fileName: string = "image.jpg",
|
|
447
|
+
fileType: string = "image/jpeg",
|
|
448
|
+
quality: number = 0.8
|
|
449
|
+
): Promise<File> {
|
|
450
|
+
return new Promise((resolve, reject) => {
|
|
451
|
+
try {
|
|
452
|
+
const canvas = document.createElement("canvas")
|
|
453
|
+
canvas.width = imageData.width
|
|
454
|
+
canvas.height = imageData.height
|
|
455
|
+
|
|
456
|
+
const ctx = canvas.getContext("2d")
|
|
210
457
|
if (!ctx) {
|
|
211
|
-
reject(new Error(
|
|
212
|
-
return
|
|
458
|
+
reject(new Error("无法创建2D上下文"))
|
|
459
|
+
return
|
|
213
460
|
}
|
|
214
|
-
|
|
215
|
-
ctx.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
461
|
+
|
|
462
|
+
ctx.putImageData(imageData, 0, 0)
|
|
463
|
+
|
|
464
|
+
canvas.toBlob(
|
|
465
|
+
(blob) => {
|
|
466
|
+
if (!blob) {
|
|
467
|
+
reject(new Error("无法创建图片Blob"))
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
const file = new File([blob], fileName, { type: fileType })
|
|
471
|
+
resolve(file)
|
|
472
|
+
},
|
|
473
|
+
fileType,
|
|
474
|
+
quality
|
|
475
|
+
)
|
|
476
|
+
} catch (error) {
|
|
477
|
+
reject(error)
|
|
478
|
+
}
|
|
479
|
+
})
|
|
226
480
|
}
|
|
227
481
|
|
|
228
482
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* @param
|
|
233
|
-
* @param
|
|
234
|
-
* @param
|
|
235
|
-
* @returns
|
|
483
|
+
* 调整图像大小
|
|
484
|
+
*
|
|
485
|
+
* @param imageData 原始图像数据
|
|
486
|
+
* @param maxWidth 最大宽度
|
|
487
|
+
* @param maxHeight 最大高度
|
|
488
|
+
* @param maintainAspectRatio 是否保持宽高比
|
|
489
|
+
* @returns ImageData 调整大小后的图像数据
|
|
236
490
|
*/
|
|
237
|
-
static
|
|
238
|
-
imageData: ImageData,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
491
|
+
static resizeImage(
|
|
492
|
+
imageData: ImageData,
|
|
493
|
+
maxWidth: number,
|
|
494
|
+
maxHeight: number,
|
|
495
|
+
maintainAspectRatio: boolean = true
|
|
496
|
+
): ImageData {
|
|
497
|
+
const { width, height } = imageData
|
|
498
|
+
|
|
499
|
+
// 如果图像已经小于指定大小,则不需要调整
|
|
500
|
+
if (width <= maxWidth && height <= maxHeight) {
|
|
501
|
+
return imageData
|
|
245
502
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
503
|
+
|
|
504
|
+
let newWidth = maxWidth
|
|
505
|
+
let newHeight = maxHeight
|
|
506
|
+
|
|
507
|
+
// 计算新的尺寸,保持宽高比
|
|
508
|
+
if (maintainAspectRatio) {
|
|
509
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height)
|
|
510
|
+
newWidth = Math.floor(width * ratio)
|
|
511
|
+
newHeight = Math.floor(height * ratio)
|
|
255
512
|
}
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const chunkCanvas = document.createElement('canvas');
|
|
266
|
-
const chunkCtx = chunkCanvas.getContext('2d');
|
|
267
|
-
|
|
268
|
-
if (!chunkCtx) continue;
|
|
269
|
-
|
|
270
|
-
let chunkImageData;
|
|
271
|
-
|
|
272
|
-
if (isWide) {
|
|
273
|
-
// 水平分割
|
|
274
|
-
const startX = i * chunkSize;
|
|
275
|
-
const width = (i === chunks - 1) ? imageData.width - startX : chunkSize;
|
|
276
|
-
|
|
277
|
-
chunkCanvas.width = width;
|
|
278
|
-
chunkCanvas.height = imageData.height;
|
|
279
|
-
|
|
280
|
-
// 复制原图像数据到分块
|
|
281
|
-
const tempCanvas = this.imageDataToCanvas(imageData);
|
|
282
|
-
chunkCtx.drawImage(
|
|
283
|
-
tempCanvas,
|
|
284
|
-
startX, 0, width, imageData.height,
|
|
285
|
-
0, 0, width, imageData.height
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
chunkImageData = chunkCtx.getImageData(0, 0, width, imageData.height);
|
|
289
|
-
} else {
|
|
290
|
-
// 垂直分割
|
|
291
|
-
const startY = i * chunkSize;
|
|
292
|
-
const height = (i === chunks - 1) ? imageData.height - startY : chunkSize;
|
|
293
|
-
|
|
294
|
-
chunkCanvas.width = imageData.width;
|
|
295
|
-
chunkCanvas.height = height;
|
|
296
|
-
|
|
297
|
-
// 复制原图像数据到分块
|
|
298
|
-
const tempCanvas = this.imageDataToCanvas(imageData);
|
|
299
|
-
chunkCtx.drawImage(
|
|
300
|
-
tempCanvas,
|
|
301
|
-
0, startY, imageData.width, height,
|
|
302
|
-
0, 0, imageData.width, height
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
chunkImageData = chunkCtx.getImageData(0, 0, imageData.width, height);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// 使用Worker处理
|
|
309
|
-
const workerCode = `
|
|
310
|
-
self.onmessage = function(e) {
|
|
311
|
-
const imageData = e.data.imageData;
|
|
312
|
-
const processingFunction = ${processingFunction.toString()};
|
|
313
|
-
const result = processingFunction(imageData);
|
|
314
|
-
self.postMessage({ result, index: e.data.index }, [result.data.buffer]);
|
|
315
|
-
}
|
|
316
|
-
`;
|
|
317
|
-
|
|
318
|
-
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
319
|
-
const workerUrl = URL.createObjectURL(blob);
|
|
320
|
-
const worker = new Worker(workerUrl);
|
|
321
|
-
|
|
322
|
-
const promise = new Promise<{ result: ImageData, index: number }>((resolve) => {
|
|
323
|
-
worker.onmessage = function(e) {
|
|
324
|
-
resolve(e.data);
|
|
325
|
-
worker.terminate();
|
|
326
|
-
URL.revokeObjectURL(workerUrl);
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// 传输数据
|
|
330
|
-
worker.postMessage({
|
|
331
|
-
imageData: chunkImageData,
|
|
332
|
-
index: i
|
|
333
|
-
}, [chunkImageData.data.buffer]);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
promises.push(promise);
|
|
513
|
+
|
|
514
|
+
// 创建用于调整大小的Canvas
|
|
515
|
+
const canvas = document.createElement("canvas")
|
|
516
|
+
canvas.width = newWidth
|
|
517
|
+
canvas.height = newHeight
|
|
518
|
+
|
|
519
|
+
const ctx = canvas.getContext("2d")
|
|
520
|
+
if (!ctx) {
|
|
521
|
+
throw new Error("无法创建2D上下文")
|
|
337
522
|
}
|
|
338
|
-
|
|
339
|
-
//
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const { result } = results[i];
|
|
348
|
-
const tempCanvas = this.imageDataToCanvas(result);
|
|
349
|
-
|
|
350
|
-
if (isWide) {
|
|
351
|
-
const startX = i * chunkSize;
|
|
352
|
-
resultCtx.drawImage(tempCanvas, startX, 0);
|
|
353
|
-
} else {
|
|
354
|
-
const startY = i * chunkSize;
|
|
355
|
-
resultCtx.drawImage(tempCanvas, 0, startY);
|
|
356
|
-
}
|
|
523
|
+
|
|
524
|
+
// 创建临时Canvas绘制原始ImageData
|
|
525
|
+
const tempCanvas = document.createElement("canvas")
|
|
526
|
+
tempCanvas.width = width
|
|
527
|
+
tempCanvas.height = height
|
|
528
|
+
|
|
529
|
+
const tempCtx = tempCanvas.getContext("2d")
|
|
530
|
+
if (!tempCtx) {
|
|
531
|
+
throw new Error("无法创建临时2D上下文")
|
|
357
532
|
}
|
|
358
|
-
|
|
359
|
-
|
|
533
|
+
|
|
534
|
+
tempCtx.putImageData(imageData, 0, 0)
|
|
535
|
+
|
|
536
|
+
// 使用缩放平滑算法
|
|
537
|
+
ctx.imageSmoothingEnabled = true
|
|
538
|
+
ctx.imageSmoothingQuality = "high"
|
|
539
|
+
|
|
540
|
+
// 绘制调整大小的图像
|
|
541
|
+
ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, newWidth, newHeight)
|
|
542
|
+
|
|
543
|
+
// 获取新的ImageData
|
|
544
|
+
return ctx.getImageData(0, 0, newWidth, newHeight)
|
|
360
545
|
}
|
|
361
|
-
}
|
|
546
|
+
}
|