|
@@ -421,19 +421,14 @@ export default () => {
|
|
|
throw new Error('Unsupported data type')
|
|
throw new Error('Unsupported data type')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
|
|
+ /*
|
|
|
const makeWhiteTransparent = async (
|
|
const makeWhiteTransparent = async (
|
|
|
data: string | Blob,
|
|
data: string | Blob,
|
|
|
filename: string,
|
|
filename: string,
|
|
|
options?: { tolerance?: number }
|
|
options?: { tolerance?: number }
|
|
|
): Promise<File> => {
|
|
): Promise<File> => {
|
|
|
- // const tolerance = options?.tolerance ?? 30 // 容差值,控制哪些颜色被视为白色
|
|
|
|
|
-
|
|
|
|
|
- // 容差:颜色到白色的欧几里得距离阈值
|
|
|
|
|
- const distThreshold = options?.tolerance ?? 50 // 50 是经验值,可根据效果调整
|
|
|
|
|
-
|
|
|
|
|
- // 是否启用边缘恢复(去除白边)
|
|
|
|
|
- const removeWhiteMatte = true // 可改为配置项
|
|
|
|
|
|
|
+ const tolerance = options?.tolerance ?? 30 // 容差值,控制哪些颜色被视为白色
|
|
|
|
|
+ const distThreshold = options?.tolerance ?? 50;
|
|
|
|
|
|
|
|
// 1. 将输入数据统一为 Blob 或可直接用于加载的 URL
|
|
// 1. 将输入数据统一为 Blob 或可直接用于加载的 URL
|
|
|
let imageUrl: string
|
|
let imageUrl: string
|
|
@@ -472,74 +467,127 @@ export default () => {
|
|
|
// 4. 获取像素数据并处理
|
|
// 4. 获取像素数据并处理
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
|
const dataArray = imageData.data
|
|
const dataArray = imageData.data
|
|
|
- /*
|
|
|
|
|
- for (let i = 0; i < dataArray.length; i += 4) {
|
|
|
|
|
- const r = dataArray[i]
|
|
|
|
|
- const g = dataArray[i + 1]
|
|
|
|
|
- const b = dataArray[i + 2]
|
|
|
|
|
-
|
|
|
|
|
- // 判断颜色是否接近白色(RGB 都大于 255 - tolerance)
|
|
|
|
|
- if (r > 255 - tolerance && g > 255 - tolerance && b > 255 - tolerance) {
|
|
|
|
|
- dataArray[i + 3] = 0 // 设置 Alpha 为 0(完全透明)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- */
|
|
|
|
|
-
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < dataArray.length; i += 4) {
|
|
for (let i = 0; i < dataArray.length; i += 4) {
|
|
|
const r = dataArray[i]
|
|
const r = dataArray[i]
|
|
|
const g = dataArray[i + 1]
|
|
const g = dataArray[i + 1]
|
|
|
const b = dataArray[i + 2]
|
|
const b = dataArray[i + 2]
|
|
|
- const a = dataArray[i + 3] // 当前 alpha(可能是255,也可能小于255)
|
|
|
|
|
-
|
|
|
|
|
- // 计算到白色的距离
|
|
|
|
|
- const dr = r - 255
|
|
|
|
|
- const dg = g - 255
|
|
|
|
|
- const db = b - 255
|
|
|
|
|
- const dist = Math.sqrt(dr * dr + dg * dg + db * db)
|
|
|
|
|
|
|
|
|
|
- if (dist <= distThreshold) {
|
|
|
|
|
- // 完全视为背景,设为全透明
|
|
|
|
|
- dataArray[i + 3] = 0
|
|
|
|
|
- }
|
|
|
|
|
- else if (removeWhiteMatte && a === 255) {
|
|
|
|
|
- // 尝试恢复边缘(仅针对完全不透明但受背景影响的像素)
|
|
|
|
|
- // 估计背景混合度:用最小通道值近似计算 alpha
|
|
|
|
|
- const minChannel = Math.min(r, g, b)
|
|
|
|
|
- const bgWeight = 1 - minChannel / 255 // 背景混合权重
|
|
|
|
|
- if (bgWeight > 0.01 && bgWeight < 0.99) {
|
|
|
|
|
- // 存在混合,反推前景色
|
|
|
|
|
- const alpha = 1 - bgWeight // 前景透明度
|
|
|
|
|
- // 反推公式:前景色 = (当前颜色 - 背景色*(1-alpha)) / alpha
|
|
|
|
|
- const newR = (r - 255 * (1 - alpha)) / alpha
|
|
|
|
|
- const newG = (g - 255 * (1 - alpha)) / alpha
|
|
|
|
|
- const newB = (b - 255 * (1 - alpha)) / alpha
|
|
|
|
|
-
|
|
|
|
|
- // 将计算结果写回,并设置alpha
|
|
|
|
|
- dataArray[i] = Math.max(0, Math.min(255, newR))
|
|
|
|
|
- dataArray[i + 1] = Math.max(0, Math.min(255, newG))
|
|
|
|
|
- dataArray[i + 2] = Math.max(0, Math.min(255, newB))
|
|
|
|
|
- dataArray[i + 3] = Math.round(alpha * 255)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 判断颜色是否接近白色(RGB 都大于 255 - tolerance)
|
|
|
|
|
+ if (r > 255 - tolerance && g > 255 - tolerance && b > 255 - tolerance) {
|
|
|
|
|
+ dataArray[i + 3] = 0 // 设置 Alpha 为 0(完全透明)
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 5. 将修改后的像素放回 Canvas
|
|
|
|
|
- ctx.putImageData(imageData, 0, 0)
|
|
|
|
|
|
|
+ // 5. 将修改后的像素放回 Canvas
|
|
|
|
|
+ ctx.putImageData(imageData, 0, 0)
|
|
|
|
|
|
|
|
- // 6. 将 Canvas 转换为 PNG Blob
|
|
|
|
|
- const outputBlob = await new Promise<Blob>((resolve) =>
|
|
|
|
|
- canvas.toBlob((blob) => resolve(blob!), 'image/png')
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ // 6. 将 Canvas 转换为 PNG Blob
|
|
|
|
|
+ const outputBlob = await new Promise<Blob>((resolve) =>
|
|
|
|
|
+ canvas.toBlob((blob) => resolve(blob!), 'image/png')
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- // 7. 清理对象 URL(如果之前创建过)
|
|
|
|
|
- if (typeof data !== 'string') {
|
|
|
|
|
- URL.revokeObjectURL(imageUrl)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 7. 清理对象 URL(如果之前创建过)
|
|
|
|
|
+ if (typeof data !== 'string') {
|
|
|
|
|
+ URL.revokeObjectURL(imageUrl)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ // 8. 返回 File 对象
|
|
|
|
|
+ return new File([outputBlob], filename, { type: 'image/png' })
|
|
|
|
|
+ }
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将图片中的白色背景变为透明
|
|
|
|
|
+ * @param data 图片数据(Base64字符串 或 Blob)
|
|
|
|
|
+ * @param filename 输出文件名
|
|
|
|
|
+ * @param options 可选配置
|
|
|
|
|
+ * @param options.tolerance 颜色距离容差(默认50,值越大越多的浅色被变透明)
|
|
|
|
|
+ * @param options.removeMatte 是否去除白色边缘(默认true,可改善白边)
|
|
|
|
|
+ * @returns 处理后的 PNG 格式 File 对象
|
|
|
|
|
+ */
|
|
|
|
|
+ const makeWhiteTransparent = async (
|
|
|
|
|
+ data: string | Blob,
|
|
|
|
|
+ filename: string,
|
|
|
|
|
+ options?: { tolerance?: number }
|
|
|
|
|
+ ): Promise<File> => {
|
|
|
|
|
+ const tolerance = options?.tolerance ?? 15; // 只处理非常接近白色的像素
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 将输入统一转为可加载的 URL
|
|
|
|
|
+ let imageUrl: string;
|
|
|
|
|
+ let needRevoke = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof data === 'string') {
|
|
|
|
|
+ imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`;
|
|
|
|
|
+ } else if (data instanceof Blob) {
|
|
|
|
|
+ imageUrl = URL.createObjectURL(data);
|
|
|
|
|
+ needRevoke = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error('Unsupported data type: expected string or Blob');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 2. 加载图像
|
|
|
|
|
+ const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
|
|
+ const image = new Image();
|
|
|
|
|
+ image.onload = () => resolve(image);
|
|
|
|
|
+ image.onerror = reject;
|
|
|
|
|
+ if (typeof data === 'string' && !data.startsWith('data:')) {
|
|
|
|
|
+ image.crossOrigin = 'anonymous';
|
|
|
|
|
+ }
|
|
|
|
|
+ image.src = imageUrl;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 绘制到 Canvas
|
|
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
|
|
+ canvas.width = img.width;
|
|
|
|
|
+ canvas.height = img.height;
|
|
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
|
|
+ ctx.drawImage(img, 0, 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 获取像素数据
|
|
|
|
|
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
|
|
+ const dataArray = imageData.data;
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 遍历像素:只将白色背景区域设为透明,其他颜色原样保留
|
|
|
|
|
+ for (let i = 0; i < dataArray.length; i += 4) {
|
|
|
|
|
+ const r = dataArray[i];
|
|
|
|
|
+ const g = dataArray[i + 1];
|
|
|
|
|
+ const b = dataArray[i + 2];
|
|
|
|
|
+
|
|
|
|
|
+ // 计算与纯白色 (255,255,255) 的欧几里得距离
|
|
|
|
|
+ const dr = r - 255;
|
|
|
|
|
+ const dg = g - 255;
|
|
|
|
|
+ const db = b - 255;
|
|
|
|
|
+ const dist = Math.sqrt(dr * dr + dg * dg + db * db);
|
|
|
|
|
+
|
|
|
|
|
+ if (dist <= tolerance) {
|
|
|
|
|
+ // 非常接近白色 → 设为全透明
|
|
|
|
|
+ dataArray[i + 3] = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 其他所有像素(包括浅蓝、灰色、黑色等)保持原样,不修改颜色和透明度
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 将修改后的像素放回 Canvas
|
|
|
|
|
+ ctx.putImageData(imageData, 0, 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 导出为 PNG Blob
|
|
|
|
|
+ const outputBlob = await new Promise<Blob>((resolve, reject) => {
|
|
|
|
|
+ canvas.toBlob((blob) => {
|
|
|
|
|
+ if (blob) resolve(blob);
|
|
|
|
|
+ else reject(new Error('Canvas toBlob failed'));
|
|
|
|
|
+ }, 'image/png');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
// 8. 返回 File 对象
|
|
// 8. 返回 File 对象
|
|
|
- return new File([outputBlob], filename, { type: 'image/png' })
|
|
|
|
|
|
|
+ return new File([outputBlob], filename, { type: 'image/png' });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (needRevoke) {
|
|
|
|
|
+ URL.revokeObjectURL(imageUrl);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 上传 File 到 S3,返回公开访问的 URL
|
|
* 上传 File 到 S3,返回公开访问的 URL
|
|
|
*/
|
|
*/
|
|
@@ -1636,13 +1684,13 @@ export default () => {
|
|
|
|
|
|
|
|
const firstCell = el.data[0][0]
|
|
const firstCell = el.data[0][0]
|
|
|
const border = firstCell.borders.top ||
|
|
const border = firstCell.borders.top ||
|
|
|
- firstCell.borders.bottom ||
|
|
|
|
|
- el.borders.top ||
|
|
|
|
|
- el.borders.bottom ||
|
|
|
|
|
- firstCell.borders.left ||
|
|
|
|
|
- firstCell.borders.right ||
|
|
|
|
|
- el.borders.left ||
|
|
|
|
|
- el.borders.right
|
|
|
|
|
|
|
+ firstCell.borders.bottom ||
|
|
|
|
|
+ el.borders.top ||
|
|
|
|
|
+ el.borders.bottom ||
|
|
|
|
|
+ firstCell.borders.left ||
|
|
|
|
|
+ firstCell.borders.right ||
|
|
|
|
|
+ el.borders.left ||
|
|
|
|
|
+ el.borders.right
|
|
|
const borderWidth = border?.borderWidth || 0
|
|
const borderWidth = border?.borderWidth || 0
|
|
|
const borderStyle = border?.borderType || 'solid'
|
|
const borderStyle = border?.borderType || 'solid'
|
|
|
const borderColor = border?.borderColor || '#eeece1'
|
|
const borderColor = border?.borderColor || '#eeece1'
|