uni-image-editor 1.0.0
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/README.md +98 -0
- package/components/edit-graffiti-config.vue +151 -0
- package/components/edit-text-config.vue +112 -0
- package/components/image-clipper/image-clipper.vue +1009 -0
- package/components/image-clipper/img/photo.svg +19 -0
- package/components/image-clipper/img/rotate.svg +15 -0
- package/components/image-clipper/index.scss +184 -0
- package/components/image-clipper/utils.js +280 -0
- package/components/input-text-modal.vue +431 -0
- package/image-editor.vue +971 -0
- package/js/const.js +242 -0
- package/js/editor.js +1179 -0
- package/js/utils.js +316 -0
- package/package.json +29 -0
package/js/utils.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Author: msc 862078729@qq.com
|
|
3
|
+
* @Date: 2024-04-29 12:52:27
|
|
4
|
+
* @LastEditors: msc 862078729@qq.com
|
|
5
|
+
* @LastEditTime: 2024-06-14 11:52:23
|
|
6
|
+
* @FilePath: \code\components\image-editor\js\utils.js
|
|
7
|
+
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// 获取自适应尺寸
|
|
11
|
+
export const getAutoFitSize = (width, height) => {
|
|
12
|
+
const systemInfo = uni.getSystemInfoSync();
|
|
13
|
+
|
|
14
|
+
// 600为底部和头部navbar高度之和外加一点
|
|
15
|
+
const canvasMaxHeight = systemInfo.windowHeight - uni.upx2px(600);
|
|
16
|
+
|
|
17
|
+
const size = {
|
|
18
|
+
width: "",
|
|
19
|
+
height: "",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
size.width = uni.upx2px(750);
|
|
23
|
+
|
|
24
|
+
const ratio = size.width / width;
|
|
25
|
+
|
|
26
|
+
size.height =
|
|
27
|
+
ratio * height > canvasMaxHeight ? canvasMaxHeight : ratio * height;
|
|
28
|
+
|
|
29
|
+
return size;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// 导出一个函数,用于验证图片是否部分为空(即透明)
|
|
33
|
+
export const validImgHasEmpty = (
|
|
34
|
+
data,
|
|
35
|
+
totalPixels,
|
|
36
|
+
maxTransparencyRate = 5
|
|
37
|
+
) => {
|
|
38
|
+
let transparentPixels = 0;
|
|
39
|
+
|
|
40
|
+
let valid = true;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
43
|
+
// 检查Alpha通道是否为0(即完全透明)
|
|
44
|
+
if (data[i + 3] === 0) {
|
|
45
|
+
transparentPixels++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const transparencyRate = (transparentPixels / totalPixels) * 100;
|
|
50
|
+
|
|
51
|
+
console.log("图片透明占比",`当前图片最大透明占比${maxTransparencyRate}`, transparencyRate);
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if (transparencyRate >= maxTransparencyRate) {
|
|
55
|
+
valid = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return valid;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 导出一个函数,用于判断图片空白部分过大
|
|
62
|
+
export const validImgRenderOverBlank = (
|
|
63
|
+
imageData,
|
|
64
|
+
threshold = 240,
|
|
65
|
+
minConsecutivePixels = 100
|
|
66
|
+
) => {
|
|
67
|
+
let currentBlankCount = 0;
|
|
68
|
+
let hasBlankArea = false;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < imageData.length; i += 4) {
|
|
71
|
+
const r = imageData[i];
|
|
72
|
+
const g = imageData[i + 1];
|
|
73
|
+
const b = imageData[i + 2];
|
|
74
|
+
|
|
75
|
+
// 检查像素是否接近白色
|
|
76
|
+
if (r > threshold && g > threshold && b > threshold) {
|
|
77
|
+
currentBlankCount++;
|
|
78
|
+
|
|
79
|
+
// 如果连续接近白色的像素超过阈值,则认为有空白部分
|
|
80
|
+
if (currentBlankCount >= minConsecutivePixels) {
|
|
81
|
+
hasBlankArea = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// 如果遇到非接近白色的像素,重置连续计数
|
|
86
|
+
currentBlankCount = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return !hasBlankArea;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// 定义一个函数renderAwait,用于渲染一个等待动画,参数duration表示等待时间,默认值为100毫秒
|
|
93
|
+
export const renderAwait = (duration = 100) => {
|
|
94
|
+
// 返回一个新的Promise对象
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
// 使用setTimeout函数,在给定的等待时间后执行一个回调函数
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
// 执行resolve函数,表示等待结束
|
|
99
|
+
resolve();
|
|
100
|
+
}, duration);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 将Base64编码的数据转换为Blob对象
|
|
107
|
+
*
|
|
108
|
+
* @param {string} base64Data - Base64编码的数据
|
|
109
|
+
* @returns {Blob} - 转换后的Blob对象
|
|
110
|
+
*/
|
|
111
|
+
export const base64ToBlob = (base64Data) => {
|
|
112
|
+
// 获取MIME类型,例如 "image/jpeg", "text/plain" 等
|
|
113
|
+
// 分割字符串,以分号";"为分隔符,取第一部分,再以冒号":"为分隔符,取第二部分
|
|
114
|
+
const contentType = base64Data.split(";")[0].split(":")[1]; // 获取MIME类型
|
|
115
|
+
|
|
116
|
+
// 去除Base64编码的头部信息,只保留数据部分,通常是以逗号","分隔后的第二部分
|
|
117
|
+
// 使用atob函数将Base64字符串解码为二进制字符串
|
|
118
|
+
const byteCharacters = atob(base64Data.split(",")[1]);
|
|
119
|
+
|
|
120
|
+
// 创建一个空数组,用于存放字节数组
|
|
121
|
+
const byteArrays = [];
|
|
122
|
+
|
|
123
|
+
// 循环处理二进制字符串,每次处理512个字符(这是一个常见的处理大小,可根据需要调整)
|
|
124
|
+
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
|
125
|
+
// 从二进制字符串中切分出一个片段
|
|
126
|
+
const slice = byteCharacters.slice(offset, offset + 512);
|
|
127
|
+
|
|
128
|
+
// 创建一个与片段长度相同的数组,用于存放字符的ASCII码值
|
|
129
|
+
const byteNumbers = new Array(slice.length);
|
|
130
|
+
|
|
131
|
+
// 遍历片段中的每个字符,将其ASCII码值存入数组中
|
|
132
|
+
for (let i = 0; i < slice.length; i++) {
|
|
133
|
+
byteNumbers[i] = slice.charCodeAt(i);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 将ASCII码值数组转换为Uint8Array类型的字节数组
|
|
137
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
138
|
+
|
|
139
|
+
// 将字节数组添加到总数组中
|
|
140
|
+
byteArrays.push(byteArray);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 使用总数组创建一个Blob对象,并指定MIME类型
|
|
144
|
+
return new Blob(byteArrays, { type: contentType });
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 将Base64编码的数据转换为Blob对象,并生成对应的URL对象
|
|
149
|
+
*
|
|
150
|
+
* @param {string} base64Data - Base64编码的数据
|
|
151
|
+
* @returns {string} - 转换后的Blob对象的URL
|
|
152
|
+
*/
|
|
153
|
+
export const base64ToUrl = (base64Data) => {
|
|
154
|
+
// 调用base64ToBlob函数将Base64数据转换为Blob对象
|
|
155
|
+
const blob = base64ToBlob(base64Data);
|
|
156
|
+
|
|
157
|
+
// 使用URL.createObjectURL方法将Blob对象转换为一个表示该Blob对象内容的URL
|
|
158
|
+
return URL.createObjectURL(blob);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 清除由createObjectURL创建的Blob对象的URL缓存
|
|
163
|
+
*
|
|
164
|
+
* @param {Array<string>|string} urls - 需要清除的URL或URL数组
|
|
165
|
+
* @returns {void} - 无返回值
|
|
166
|
+
*/
|
|
167
|
+
export const clearUrlBlobCache = (urls) => {
|
|
168
|
+
// 如果传入的urls不是数组,则将其转换为数组
|
|
169
|
+
if (!Array.isArray(urls)) {
|
|
170
|
+
urls = [urls];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 遍历urls数组,对每个URL调用revokeObjectURL方法来释放缓存
|
|
174
|
+
urls.forEach((url) => URL.revokeObjectURL(url));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// 是否为图片的base64数据
|
|
178
|
+
export const isBase64Image = (url) =>{
|
|
179
|
+
const base64Regex = /^data:image\/(png|jpeg|jpg|gif|svg\+xml);base64,/;
|
|
180
|
+
return base64Regex.test(url);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 判断给定的URL是否为网络图片URL(以http或https开头)
|
|
184
|
+
export const isNetImageUrl = (imageUrl) => {
|
|
185
|
+
// 检查是否以 http 或 https 开头
|
|
186
|
+
if (/^(https?:\/\/)/.test(imageUrl)) {
|
|
187
|
+
return true; // 是远程图片 URL
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 在这里可以添加其他判断条件,例如检查是否以 file:// 开头(针对某些特定平台)
|
|
191
|
+
// 或者检查是否以应用内部的相对路径开头(例如 /static/images/)
|
|
192
|
+
|
|
193
|
+
// 如果没有其他明确的本地 URL 格式,可以认为不是远程的就是本地的
|
|
194
|
+
return false; // 不是远程图片 URL,可能是本地 URL 或其他格式
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 使用示例:将网络图片URL转换为Base64编码的字符串(仅H5平台)
|
|
198
|
+
export const convertNetImageUrlToBase64OfH5 = async (imageUrl) => {
|
|
199
|
+
// #ifdef H5
|
|
200
|
+
try {
|
|
201
|
+
const response = await fetch(imageUrl);
|
|
202
|
+
const blob = await response.blob();
|
|
203
|
+
const base64 = await blobToBase64(blob);
|
|
204
|
+
return base64;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error("Error fetching image:", error);
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// #endif
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
// 将网络图片URL转换为本地临时文件路径(使用uni.downloadFile方法)
|
|
215
|
+
export const convertNetImgUrlToLocal = (imageUrl) => {
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
console.log('下载图片中...');
|
|
218
|
+
uni.downloadFile({
|
|
219
|
+
url: imageUrl,
|
|
220
|
+
success: (downloadResult) => {
|
|
221
|
+
if (downloadResult.statusCode === 200) {
|
|
222
|
+
resolve(downloadResult.tempFilePath);
|
|
223
|
+
console.log('下载图片成功');
|
|
224
|
+
} else {
|
|
225
|
+
reject(
|
|
226
|
+
"转化网络图片到本地临时图片失败:" + downloadResult.statusCode
|
|
227
|
+
);
|
|
228
|
+
console.log( "转化网络图片到本地临时图片失败:" + downloadResult.statusCode);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
fail: (error) => {
|
|
232
|
+
reject("转化网络图片到本地临时图片失败:" + error.message);
|
|
233
|
+
console.log("转化网络图片到本地临时图片失败:" + error.message);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
// 从对象中提取指定属性
|
|
241
|
+
export function pick(object, keys) {
|
|
242
|
+
// 创建一个新对象
|
|
243
|
+
const result = {};
|
|
244
|
+
|
|
245
|
+
// 确保 keys 是数组
|
|
246
|
+
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
247
|
+
|
|
248
|
+
// 遍历 keys 数组
|
|
249
|
+
for (const key of keysArray) {
|
|
250
|
+
// 检查对象是否有该属性
|
|
251
|
+
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
|
|
252
|
+
// 将属性添加到结果对象
|
|
253
|
+
result[key] = object[key];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 从数组末尾开始查找符合条件的元素的索引
|
|
261
|
+
export function findLastIndex(array, predicate, fromIndex) {
|
|
262
|
+
// 处理边界情况
|
|
263
|
+
if (!Array.isArray(array)) {
|
|
264
|
+
return -1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const length = array.length;
|
|
268
|
+
if (length === 0) {
|
|
269
|
+
return -1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 确定起始索引
|
|
273
|
+
let index = length - 1;
|
|
274
|
+
if (fromIndex !== undefined) {
|
|
275
|
+
index = fromIndex < 0 ? Math.max(length + fromIndex, 0) : Math.min(fromIndex, length - 1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 从后向前遍历
|
|
279
|
+
for (; index >= 0; index--) {
|
|
280
|
+
// 检查当前元素是否符合条件
|
|
281
|
+
if (typeof predicate === 'function') {
|
|
282
|
+
if (predicate(array[index], index, array)) {
|
|
283
|
+
return index;
|
|
284
|
+
}
|
|
285
|
+
} else if (predicate === array[index]) {
|
|
286
|
+
return index;
|
|
287
|
+
} else if (typeof predicate === 'object' && predicate !== null) {
|
|
288
|
+
let matches = true;
|
|
289
|
+
for (const key in predicate) {
|
|
290
|
+
if (predicate[key] !== array[index][key]) {
|
|
291
|
+
matches = false;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (matches) {
|
|
296
|
+
return index;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return -1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 生成唯一标识符
|
|
305
|
+
let uniqueIdCounter = 0;
|
|
306
|
+
export function uniqueId(prefix) {
|
|
307
|
+
// 增加计数器
|
|
308
|
+
uniqueIdCounter++;
|
|
309
|
+
|
|
310
|
+
// 添加前缀(如果提供)
|
|
311
|
+
if (prefix !== undefined) {
|
|
312
|
+
return `${prefix}${uniqueIdCounter}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return `${uniqueIdCounter}`;
|
|
316
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uni-image-editor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "UniApp Vue2 图片编辑插件",
|
|
5
|
+
"main": "image-editor.vue",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"uniapp",
|
|
11
|
+
"vue2",
|
|
12
|
+
"image-editor",
|
|
13
|
+
"图片编辑",
|
|
14
|
+
"涂鸦",
|
|
15
|
+
"裁剪",
|
|
16
|
+
"添加文字"
|
|
17
|
+
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": ""
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": ""
|
|
27
|
+
},
|
|
28
|
+
"homepage": ""
|
|
29
|
+
}
|