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.
Files changed (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +375 -363
  3. package/dist/id-scanner-core.esm.js +427 -221
  4. package/dist/id-scanner-core.esm.js.map +1 -1
  5. package/dist/id-scanner-core.js +427 -221
  6. package/dist/id-scanner-core.js.map +1 -1
  7. package/dist/id-scanner-core.min.js +1 -9
  8. package/dist/id-scanner-core.min.js.map +1 -1
  9. package/dist/id-scanner-ocr.esm.js +451 -276
  10. package/dist/id-scanner-ocr.esm.js.map +1 -1
  11. package/dist/id-scanner-ocr.js +451 -276
  12. package/dist/id-scanner-ocr.js.map +1 -1
  13. package/dist/id-scanner-ocr.min.js +1 -9
  14. package/dist/id-scanner-ocr.min.js.map +1 -1
  15. package/dist/id-scanner-qr.esm.js +483 -233
  16. package/dist/id-scanner-qr.esm.js.map +1 -1
  17. package/dist/id-scanner-qr.js +482 -232
  18. package/dist/id-scanner-qr.js.map +1 -1
  19. package/dist/id-scanner-qr.min.js +1 -9
  20. package/dist/id-scanner-qr.min.js.map +1 -1
  21. package/dist/id-scanner.js +2138 -358
  22. package/dist/id-scanner.js.map +1 -1
  23. package/dist/id-scanner.min.js +1 -9
  24. package/dist/id-scanner.min.js.map +1 -1
  25. package/package.json +27 -7
  26. package/src/demo/demo.ts +178 -62
  27. package/src/id-recognition/anti-fake-detector.ts +317 -0
  28. package/src/id-recognition/id-detector.ts +184 -155
  29. package/src/id-recognition/ocr-processor.ts +193 -146
  30. package/src/id-recognition/ocr-worker.ts +82 -72
  31. package/src/index-umd.ts +347 -110
  32. package/src/index.ts +866 -91
  33. package/src/ocr-module.ts +108 -60
  34. package/src/qr-module.ts +104 -54
  35. package/src/scanner/barcode-scanner.ts +145 -58
  36. package/src/scanner/qr-scanner.ts +86 -47
  37. package/src/utils/image-processing.ts +479 -294
  38. package/dist/core.d.ts +0 -77
  39. package/dist/demo/demo.d.ts +0 -14
  40. package/dist/id-recognition/data-extractor.d.ts +0 -105
  41. package/dist/id-recognition/id-detector.d.ts +0 -100
  42. package/dist/id-recognition/ocr-processor.d.ts +0 -64
  43. package/dist/id-scanner.esm.js +0 -94656
  44. package/dist/id-scanner.esm.js.map +0 -1
  45. package/dist/index-umd.d.ts +0 -96
  46. package/dist/index.d.ts +0 -78
  47. package/dist/ocr-module.d.ts +0 -67
  48. package/dist/qr-module.d.ts +0 -68
  49. package/dist/scanner/barcode-scanner.d.ts +0 -90
  50. package/dist/scanner/qr-scanner.d.ts +0 -80
  51. package/dist/types/core.d.ts +0 -77
  52. package/dist/types/demo/demo.d.ts +0 -14
  53. package/dist/types/id-recognition/data-extractor.d.ts +0 -105
  54. package/dist/types/id-recognition/id-detector.d.ts +0 -100
  55. package/dist/types/id-recognition/ocr-processor.d.ts +0 -64
  56. package/dist/types/index-umd.d.ts +0 -96
  57. package/dist/types/index.d.ts +0 -78
  58. package/dist/types/ocr-module.d.ts +0 -67
  59. package/dist/types/qr-module.d.ts +0 -68
  60. package/dist/types/scanner/barcode-scanner.d.ts +0 -90
  61. package/dist/types/scanner/qr-scanner.d.ts +0 -80
  62. package/dist/types/utils/camera.d.ts +0 -81
  63. package/dist/types/utils/image-processing.d.ts +0 -75
  64. package/dist/types/utils/types.d.ts +0 -65
  65. package/dist/utils/camera.d.ts +0 -81
  66. package/dist/utils/image-processing.d.ts +0 -75
  67. 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('canvas');
35
- canvas.width = imageData.width;
36
- canvas.height = imageData.height;
37
- const ctx = canvas.getContext('2d');
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('2d');
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 {ImageData} imageData - 要处理的图像数据
61
- * @param {number} [brightness=0] - 亮度调整值,正值增加亮度,负值降低亮度,范围建议为-100到100
62
- * @param {number} [contrast=0] - 对比度调整值,正值增加对比度,负值降低对比度,范围建议为-100到100
63
- * @returns {ImageData} 处理后的图像数据
71
+ *
72
+ * @param imageData 原始图像数据
73
+ * @param brightness 亮度调整值 (-100到100)
74
+ * @param contrast 对比度调整值 (-100到100)
75
+ * @returns 处理后的图像数据
64
76
  */
65
- static adjustBrightnessContrast(imageData: ImageData, brightness: number = 0, contrast: number = 0): ImageData {
66
- const canvas = this.imageDataToCanvas(imageData);
67
- const ctx = canvas.getContext('2d');
68
-
69
- if (ctx) {
70
- const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
71
- const data = imgData.data;
72
-
73
- // 调整对比度算法
74
- const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
75
-
76
- for (let i = 0; i < data.length; i += 4) {
77
- // 红色
78
- data[i] = this.truncate(factor * (data[i] - 128) + 128 + brightness);
79
- // 绿色
80
- data[i + 1] = this.truncate(factor * (data[i + 1] - 128) + 128 + brightness);
81
- // 蓝色
82
- data[i + 2] = this.truncate(factor * (data[i + 2] - 128) + 128 + brightness);
83
- // Alpha不变
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
- * 确保值在0-255范围内
95
- *
96
- * @private
97
- * @param {number} value - 要截断的值
98
- * @returns {number} 截断后的值,范围为0-255
107
+ * 将图像转换为灰度图
108
+ *
109
+ * @param imageData 原始图像数据
110
+ * @returns 灰度图像数据
99
111
  */
100
- private static truncate(value: number): number {
101
- return Math.min(255, Math.max(0, value));
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
- * 灰度转换可以简化图像,提高OCR和条形码识别的准确率
108
- *
109
- * @param {ImageData} imageData - 要转换的彩色图像
110
- * @returns {ImageData} 转换后的灰度图像
126
+ * 锐化图像
127
+ *
128
+ * @param imageData 原始图像数据
129
+ * @param amount 锐化程度,默认为2
130
+ * @returns 锐化后的图像数据
111
131
  */
112
- static toGrayscale(imageData: ImageData): ImageData {
113
- const canvas = this.imageDataToCanvas(imageData);
114
- const ctx = canvas.getContext('2d');
115
-
116
- if (ctx) {
117
- const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
118
- const data = imgData.data;
119
-
120
- for (let i = 0; i < data.length; i += 4) {
121
- const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
122
- data[i] = avg; // 红色
123
- data[i + 1] = avg; // 绿色
124
- data[i + 2] = avg; // 蓝色
125
- // Alpha不变
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
- return imageData;
187
+
188
+ // 创建新的ImageData对象
189
+ return new ImageData(outputData, width, height)
133
190
  }
134
-
191
+
135
192
  /**
136
- * 调整图像大小
137
- *
138
- * @param {ImageData} imageData - 原图像数据
139
- * @param {number} newWidth - 新宽度
140
- * @param {number} newHeight - 新高度
141
- * @returns {ImageData} 调整大小后的图像数据
193
+ * 对图像应用阈值操作,增强对比度
194
+ *
195
+ * @param imageData 原始图像数据
196
+ * @param threshold 阈值 (0-255)
197
+ * @returns 处理后的图像数据
142
198
  */
143
- static resize(imageData: ImageData, newWidth: number, newHeight: number): ImageData {
144
- const canvas = document.createElement('canvas');
145
- canvas.width = newWidth;
146
- canvas.height = newHeight;
147
- const ctx = canvas.getContext('2d');
148
-
149
- if (ctx) {
150
- const tempCanvas = this.imageDataToCanvas(imageData);
151
- ctx.drawImage(tempCanvas, 0, 0, imageData.width, imageData.height, 0, 0, newWidth, newHeight);
152
- return ctx.getImageData(0, 0, newWidth, newHeight);
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 imageData;
293
+
294
+ return threshold
156
295
  }
157
296
 
158
297
  /**
159
- * 降低图像分辨率以提高处理速度
160
- *
161
- * 对于OCR和图像分析,降低分辨率可以在保持识别率的同时大幅提升处理速度
162
- *
163
- * @param {ImageData} imageData - 原图像数据
164
- * @param {number} [maxDimension=1000] - 目标最大尺寸(宽或高)
165
- * @returns {ImageData} 处理后的图像数据
298
+ * 批量应用图像处理
299
+ *
300
+ * @param imageData 原始图像数据
301
+ * @param options 处理选项
302
+ * @returns 处理后的图像数据
166
303
  */
167
- static downsampleForProcessing(imageData: ImageData, maxDimension: number = 1000): ImageData {
168
- const { width, height } = imageData;
169
-
170
- // 如果图像尺寸已经小于或等于目标尺寸,则无需处理
171
- if (width <= maxDimension && height <= maxDimension) {
172
- return imageData;
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
- * 转换图像为Base64格式,方便在Worker线程中传递
186
- *
187
- * @param {ImageData} imageData - 原图像数据
188
- * @returns {string} base64编码的图像数据
349
+ * 压缩图片文件
350
+ *
351
+ * @param file 图片文件
352
+ * @param options 压缩选项
353
+ * @returns Promise<File> 压缩后的文件
189
354
  */
190
- static imageDataToBase64(imageData: ImageData): string {
191
- const canvas = this.imageDataToCanvas(imageData);
192
- return canvas.toDataURL('image/jpeg', 0.7); // 使用较低质量的JPEG格式减少数据量
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
- * 从Base64字符串还原图像数据
197
- *
198
- * @param {string} base64 - base64编码的图像数据
199
- * @returns {Promise<ImageData>} 还原的图像数据
378
+ * 从图片文件创建ImageData
379
+ *
380
+ * @param file 图片文件
381
+ * @returns Promise<ImageData>
200
382
  */
201
- static async base64ToImageData(base64: string): Promise<ImageData> {
383
+ static async createImageDataFromFile(file: File): Promise<ImageData> {
202
384
  return new Promise((resolve, reject) => {
203
- const img = new Image();
204
- img.onload = () => {
205
- const canvas = document.createElement('canvas');
206
- canvas.width = img.width;
207
- canvas.height = img.height;
208
- const ctx = canvas.getContext('2d');
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('无法创建Canvas上下文'));
212
- return;
458
+ reject(new Error("无法创建2D上下文"))
459
+ return
213
460
  }
214
-
215
- ctx.drawImage(img, 0, 0);
216
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
217
- resolve(imageData);
218
- };
219
-
220
- img.onerror = () => {
221
- reject(new Error('图像加载失败'));
222
- };
223
-
224
- img.src = base64;
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
- * 使用Web Worker并行处理图像
230
- * 此方法将图像分割为多个部分,并行处理以提高性能
231
- *
232
- * @param {ImageData} imageData - 原图像数据
233
- * @param {Function} processingFunction - 处理函数,接收ImageData返回ImageData
234
- * @param {number} [chunks=4] - 分割的块数
235
- * @returns {Promise<ImageData>} 处理后的图像数据
483
+ * 调整图像大小
484
+ *
485
+ * @param imageData 原始图像数据
486
+ * @param maxWidth 最大宽度
487
+ * @param maxHeight 最大高度
488
+ * @param maintainAspectRatio 是否保持宽高比
489
+ * @returns ImageData 调整大小后的图像数据
236
490
  */
237
- static async processImageInParallel(
238
- imageData: ImageData,
239
- processingFunction: (imgData: ImageData) => ImageData,
240
- chunks: number = 4
241
- ): Promise<ImageData> {
242
- // 如果不支持Worker或图像太小,直接处理
243
- if (typeof Worker === 'undefined' || imageData.width * imageData.height < 100000) {
244
- return processingFunction(imageData);
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
- // 创建结果canvas
248
- const resultCanvas = document.createElement('canvas');
249
- resultCanvas.width = imageData.width;
250
- resultCanvas.height = imageData.height;
251
- const resultCtx = resultCanvas.getContext('2d');
252
-
253
- if (!resultCtx) {
254
- throw new Error('无法创建Canvas上下文');
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 isWide = imageData.width > imageData.height;
259
- const chunkSize = Math.floor((isWide ? imageData.width : imageData.height) / chunks);
260
-
261
- // 创建Worker处理每个块
262
- const promises = [];
263
-
264
- for (let i = 0; i < chunks; i++) {
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
- // 等待所有Worker完成并组合结果
340
- const results = await Promise.all(promises);
341
-
342
- // 按索引排序结果
343
- results.sort((a, b) => a.index - b.index);
344
-
345
- // 将处理后的块绘制到结果canvas
346
- for (let i = 0; i < results.length; i++) {
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
- return resultCtx.getImageData(0, 0, imageData.width, imageData.height);
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
+ }