|
|
@@ -11,7 +11,9 @@ import useSlideHandler from '@/hooks/useSlideHandler'
|
|
|
import useHistorySnapshot from './useHistorySnapshot'
|
|
|
import message from '@/utils/message'
|
|
|
import { getSvgPathRange } from '@/utils/svgPathParser'
|
|
|
-//import utifUrl from '/UTIF.js';
|
|
|
+import { EMFJS, WMFJS } from 'rtf.js';
|
|
|
+import * as UTIF from 'utif2';
|
|
|
+
|
|
|
import type {
|
|
|
Slide,
|
|
|
TableCellStyle,
|
|
|
@@ -567,106 +569,377 @@ export default () => {
|
|
|
* @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;
|
|
|
+
|
|
|
+ // ----- 辅助函数:将输入统一转换为 { blob, mime } -----
|
|
|
+ async function getBlobAndMime(input: string | Blob): Promise<{ blob: Blob; mime: string }> {
|
|
|
+ if (input instanceof Blob) {
|
|
|
+ return { blob: input, mime: input.type };
|
|
|
+ }
|
|
|
+ if (input.startsWith('data:')) {
|
|
|
+ const response = await fetch(input);
|
|
|
+ const blob = await response.blob();
|
|
|
+ return { blob, mime: blob.type };
|
|
|
+ }
|
|
|
+ // 纯 base64 字符串,默认当作 PNG
|
|
|
+ const binary = atob(input);
|
|
|
+ const bytes = new Uint8Array(binary.length);
|
|
|
+ for (let i = 0; i < binary.length; i++) {
|
|
|
+ bytes[i] = binary.charCodeAt(i);
|
|
|
+ }
|
|
|
+ const blob = new Blob([bytes], { type: 'image/png' });
|
|
|
+ return { blob, mime: 'image/png' };
|
|
|
+ }
|
|
|
+
|
|
|
+ // ----- 辅助函数:通过 MIME 或文件扩展名判断格式 -----
|
|
|
+ function getFormat(mime: string, filename: string): string {
|
|
|
+ const ext = filename.split('.').pop()?.toLowerCase();
|
|
|
+ if (mime.startsWith('image/')) {
|
|
|
+ const sub = mime.split('/')[1];
|
|
|
+ if (sub === 'vnd.microsoft.icon') return 'ico';
|
|
|
+ if (sub === 'x-emf' || sub === 'x-msmetafile') return 'emf';
|
|
|
+ if (sub === 'tiff' || sub === 'x-tiff') return 'tiff';
|
|
|
+ return sub;
|
|
|
+ }
|
|
|
+ // 兜底扩展名判断
|
|
|
+ if (ext === 'emf' || ext === 'wmf') return 'emf';
|
|
|
+ if (ext === 'tif' || ext === 'tiff') return 'tiff';
|
|
|
+ return 'unknown';
|
|
|
+ }
|
|
|
+
|
|
|
+ // ----- 格式转换器 -----
|
|
|
+ // 1. 浏览器原生支持的格式:通过 Image + Canvas 转为 PNG
|
|
|
+ async function convertBrowserImageToPng(blob: Blob): Promise<Blob> {
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ try {
|
|
|
+ const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
+ const image = new Image();
|
|
|
+ image.onload = () => resolve(image);
|
|
|
+ image.onerror = reject;
|
|
|
+ image.src = url;
|
|
|
+ });
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = img.width;
|
|
|
+ canvas.height = img.height;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+ ctx.drawImage(img, 0, 0);
|
|
|
+ return await new Promise((resolve, reject) => {
|
|
|
+ canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error('Canvas toBlob failed'))), 'image/png');
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. EMF/WMF 转 PNG(需要 wmf2png 库)
|
|
|
+ async function convertEmfToPng(blob: Blob): Promise<Blob> {
|
|
|
+ const arrayBuffer = await blob.arrayBuffer();
|
|
|
+ const pngBuffer = await wmf2png(arrayBuffer);
|
|
|
+ return new Blob([pngBuffer], { type: 'image/png' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. TIFF 转 PNG(需要 UTIF.js 库)
|
|
|
+ async function convertTiffToPng(blob: Blob): Promise<Blob> {
|
|
|
+ const arrayBuffer = await blob.arrayBuffer();
|
|
|
+ const ifds = UTIF.decode(arrayBuffer);
|
|
|
+ if (!ifds || ifds.length === 0) throw new Error('No TIFF image found');
|
|
|
+ const rgba = UTIF.toRGBA8(ifds[0]);
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = ifds[0].width;
|
|
|
+ canvas.height = ifds[0].height;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+ const imageData = new ImageData(rgba, ifds[0].width, ifds[0].height);
|
|
|
+ ctx.putImageData(imageData, 0, 0);
|
|
|
+ return await new Promise((resolve, reject) => {
|
|
|
+ canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error('TIFF to PNG failed'))), 'image/png');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // ----- 主逻辑:获取原始 blob 和格式 -----
|
|
|
+ let { blob, mime } = await getBlobAndMime(data);
|
|
|
+ let format = getFormat(mime, filename);
|
|
|
+
|
|
|
+ // 统一转为 PNG(除已经是 PNG 的以外,其它都转换)
|
|
|
+ let pngBlob: Blob;
|
|
|
+ if (format === 'png') {
|
|
|
+ pngBlob = blob; // 直接复用,稍后做白色透明
|
|
|
+ } else {
|
|
|
+ // 根据格式选择转换器
|
|
|
+ if (format === 'emf' || format === 'wmf') {
|
|
|
+ pngBlob = await convertEmfToPng(blob);
|
|
|
+ } else if (format === 'tiff' || format === 'x-tiff') {
|
|
|
+ pngBlob = await convertTiffToPng(blob);
|
|
|
+ } else {
|
|
|
+ // 其它所有格式(jpeg, bmp, gif, webp, ico 等)都尝试用浏览器原生方法转换
|
|
|
+ pngBlob = await convertBrowserImageToPng(blob);
|
|
|
+ }
|
|
|
+ // 更新文件名后缀为 .png
|
|
|
+ filename = filename.replace(/\.[^.]*$/, '') + '.png';
|
|
|
+ }
|
|
|
+
|
|
|
+ // ----- 白色变透明处理(仅对 PNG 执行)-----
|
|
|
+ const imageUrl = URL.createObjectURL(pngBlob);
|
|
|
+ try {
|
|
|
+ const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
+ const image = new Image();
|
|
|
+ image.onload = () => resolve(image);
|
|
|
+ image.onerror = reject;
|
|
|
+ image.src = imageUrl;
|
|
|
+ });
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = img.width;
|
|
|
+ canvas.height = img.height;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+ ctx.drawImage(img, 0, 0);
|
|
|
+
|
|
|
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
+ 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];
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ctx.putImageData(imageData, 0, 0);
|
|
|
+
|
|
|
+ const outputBlob = await new Promise<Blob>((resolve, reject) => {
|
|
|
+ canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error('Canvas toBlob failed'))), 'image/png');
|
|
|
+ });
|
|
|
+ return new File([outputBlob], filename, { type: 'image/png' });
|
|
|
+ } finally {
|
|
|
+ URL.revokeObjectURL(imageUrl);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ */
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将图片统一处理:
|
|
|
+ * - 对于浏览器原生支持的格式(JPEG, BMP, GIF, WebP 等):直接返回原始文件
|
|
|
+ * - 对于 PNG:执行白色变透明处理后返回
|
|
|
+ * - 对于 TIFF / EMF / WMF:先转换为 PNG,再执行白色变透明处理后返回
|
|
|
+ */
|
|
|
const makeWhiteTransparent = async (
|
|
|
data: string | Blob,
|
|
|
filename: string,
|
|
|
options?: { tolerance?: number }
|
|
|
): Promise<File> => {
|
|
|
- const tolerance = options?.tolerance ?? 15
|
|
|
+ const tolerance = options?.tolerance ?? 15;
|
|
|
|
|
|
- // ----- 辅助函数:将输入统一转换为 { blob, mime } -----
|
|
|
- async function getBlobAndMime(input: string | Blob): Promise<{ blob: Blob; mime: string }> {
|
|
|
- // 1. 已经是 Blob
|
|
|
- if (input instanceof Blob) {
|
|
|
- return { blob: input, mime: input.type }
|
|
|
- }
|
|
|
+ // 1. 统一输入为 Blob 和 MIME
|
|
|
+ const { blob, mime } = await getBlobAndMime(data);
|
|
|
+ const format = getFormat(mime, filename);
|
|
|
|
|
|
- // 2. 处理字符串
|
|
|
- if (input.startsWith('data:')) {
|
|
|
- // data URL → 通过 fetch 获取 Blob(自动获得正确的 MIME 类型)
|
|
|
- const response = await fetch(input)
|
|
|
- const blob = await response.blob()
|
|
|
- return { blob, mime: blob.type }
|
|
|
- }
|
|
|
- // 纯 base64 字符串 → 按原逻辑默认当作 PNG
|
|
|
- const binary = atob(input)
|
|
|
- const bytes = new Uint8Array(binary.length)
|
|
|
- for (let i = 0; i < binary.length; i++) {
|
|
|
- bytes[i] = binary.charCodeAt(i)
|
|
|
- }
|
|
|
- // 默认 MIME 为 image/png(与原函数行为一致)
|
|
|
- const blob = new Blob([bytes], { type: 'image/png' })
|
|
|
- return { blob, mime: 'image/png' }
|
|
|
+ // 2. 浏览器原生支持的格式直接返回
|
|
|
+ if (format === 'browser') {
|
|
|
+ return new File([blob], filename, { type: mime });
|
|
|
+ }
|
|
|
|
|
|
+ // 3. 需要转换成 PNG 的格式
|
|
|
+ let pngBlob: Blob;
|
|
|
+ if (format === 'tiff') {
|
|
|
+ pngBlob = await convertTiffToPng(blob);
|
|
|
+ } else if (format === 'emf') {
|
|
|
+ pngBlob = await convertEmfToPng(blob);
|
|
|
+ } else if (format === 'wmf') {
|
|
|
+ pngBlob = await convertWmfToPng(blob);
|
|
|
+ } else {
|
|
|
+ // format === 'png' 的情况
|
|
|
+ pngBlob = blob;
|
|
|
}
|
|
|
|
|
|
- // 获取统一的 blob 和实际 MIME 类型
|
|
|
- const { blob, mime } = await getBlobAndMime(data)
|
|
|
+ // 4. 对 PNG 执行白色变透明处理
|
|
|
+ const transparentPngBlob = await makeWhiteTransparentFromPng(pngBlob, tolerance);
|
|
|
+ const finalFilename = format === 'png' ? filename : filename.replace(/\.[^.]*$/, '') + '.png';
|
|
|
+ return new File([transparentPngBlob], finalFilename, { type: 'image/png' });
|
|
|
+ };
|
|
|
+
|
|
|
+ // ================== 辅助函数 ==================
|
|
|
|
|
|
- // ----- 非 PNG 格式:直接返回原始文件(不处理透明)-----
|
|
|
- if (mime !== 'image/png') {
|
|
|
- return new File([blob], filename, { type: mime })
|
|
|
+ async function getBlobAndMime(input: string | Blob): Promise<{ blob: Blob; mime: string }> {
|
|
|
+ if (input instanceof Blob) return { blob: input, mime: input.type };
|
|
|
+ if (input.startsWith('data:') || input.startsWith('blob:')) {
|
|
|
+ const res = await fetch(input);
|
|
|
+ const blob = await res.blob();
|
|
|
+ return { blob, mime: blob.type };
|
|
|
}
|
|
|
+ const binary = atob(input);
|
|
|
+ const bytes = new Uint8Array(binary.length);
|
|
|
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
|
+ const blob = new Blob([bytes], { type: 'image/png' });
|
|
|
+ return { blob, mime: 'image/png' };
|
|
|
+ }
|
|
|
|
|
|
- // ----- PNG 格式:执行白色变透明处理 -----
|
|
|
- // 1. 创建对象 URL 用于加载图片
|
|
|
- const imageUrl = URL.createObjectURL(blob)
|
|
|
- const needRevoke = true
|
|
|
+ function getFormat(mime: string, filename: string): string {
|
|
|
+ const ext = filename.split('.').pop()?.toLowerCase();
|
|
|
+ if (mime === 'image/png') return 'png';
|
|
|
+ if (mime === 'image/tiff' || mime === 'image/x-tiff' || ext === 'tiff' || ext === 'tif') return 'tiff';
|
|
|
+ if (mime === 'image/x-emf' || mime === 'application/x-emf' || ext === 'emf') return 'emf';
|
|
|
+ if (mime === 'image/x-wmf' || mime === 'application/x-wmf' || ext === 'wmf') return 'wmf';
|
|
|
+ return 'browser';
|
|
|
+ }
|
|
|
|
|
|
- // 2. 加载图像
|
|
|
- const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
- const image = new Image()
|
|
|
- image.onload = () => resolve(image)
|
|
|
- image.onerror = reject
|
|
|
- // Blob URL 不需要设置 crossOrigin
|
|
|
- image.src = imageUrl
|
|
|
- })
|
|
|
+ // TIFF 转 PNG(使用 UTIF.js)
|
|
|
+ async function convertTiffToPng(blob: Blob): Promise<Blob> {
|
|
|
+ const arrayBuffer = await blob.arrayBuffer();
|
|
|
+ const ifds = UTIF.decode(arrayBuffer);
|
|
|
+ if (!ifds || ifds.length === 0) throw new Error('No TIFF image found');
|
|
|
+ UTIF.decodeImage(arrayBuffer, ifds[0]);
|
|
|
+ const rgba = UTIF.toRGBA8(ifds[0]);
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = ifds[0].width;
|
|
|
+ canvas.height = ifds[0].height;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+ ctx.putImageData(new ImageData(rgba, ifds[0].width, ifds[0].height), 0, 0);
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error('TIFF to PNG failed'))), 'image/png');
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- const canvas = document.createElement('canvas')
|
|
|
+ // 通用函数:将 EMF/WMF 通过 Renderer 转换为 PNG
|
|
|
+ // 参考示例:https://github.com/wood/rtf.js/blob/master/demo/WMFJS.html
|
|
|
+ async function convertMetafileToPng(
|
|
|
+ arrayBuffer: ArrayBuffer,
|
|
|
+ RendererClass: any // new (data: ArrayBuffer) => { render(settings: any): SVGElement }
|
|
|
+ ): Promise<Blob> {
|
|
|
+ // 1. 创建 Renderer 实例
|
|
|
+ const renderer = new RendererClass(arrayBuffer);
|
|
|
+
|
|
|
+ // 2. 先尝试获取图片的真实尺寸(通过临时渲染并解析 SVG 的 viewBox)
|
|
|
+ let width = 800, height = 600; // 默认值
|
|
|
try {
|
|
|
- canvas.width = img.width
|
|
|
- canvas.height = img.height
|
|
|
- const ctx = canvas.getContext('2d')!
|
|
|
- ctx.drawImage(img, 0, 0)
|
|
|
-
|
|
|
- // 3. 获取像素数据,将接近白色的像素设为透明
|
|
|
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
|
- 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]
|
|
|
-
|
|
|
- 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 // 完全透明
|
|
|
+ // 使用一个较大的临时尺寸进行第一次渲染,以获取 SVG 的 viewBox
|
|
|
+ const tempSettings = {
|
|
|
+ width: '100%',
|
|
|
+ height: '100%',
|
|
|
+ xExt: 1000,
|
|
|
+ yExt: 1000,
|
|
|
+ mapMode: 8, // 保持宽高比
|
|
|
+ };
|
|
|
+ const tempSvg = renderer.render(tempSettings);
|
|
|
+ const viewBox = tempSvg.getAttribute('viewBox');
|
|
|
+ if (viewBox) {
|
|
|
+ const parts = viewBox.split(/[\s,]+/);
|
|
|
+ if (parts.length >= 4) {
|
|
|
+ width = parseFloat(parts[2]);
|
|
|
+ height = parseFloat(parts[3]);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 尝试从 width/height 属性获取
|
|
|
+ const svgWidth = tempSvg.getAttribute('width');
|
|
|
+ const svgHeight = tempSvg.getAttribute('height');
|
|
|
+ if (svgWidth && svgHeight) {
|
|
|
+ width = parseFloat(svgWidth);
|
|
|
+ height = parseFloat(svgHeight);
|
|
|
}
|
|
|
}
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('Failed to get dimensions from SVG, using default', e);
|
|
|
+ }
|
|
|
|
|
|
- ctx.putImageData(imageData, 0, 0)
|
|
|
+ // 3. 使用实际尺寸重新渲染
|
|
|
+ const settings = {
|
|
|
+ width: width + 'px',
|
|
|
+ height: height + 'px',
|
|
|
+ xExt: width,
|
|
|
+ yExt: height,
|
|
|
+ mapMode: 8, // 保持宽高比
|
|
|
+ };
|
|
|
+ const svg = renderer.render(settings);
|
|
|
+
|
|
|
+ // 4. 将 SVG 转为 data URL 并用 Image 加载
|
|
|
+ const serializer = new XMLSerializer();
|
|
|
+ let svgString = serializer.serializeToString(svg);
|
|
|
+ // 确保有命名空间
|
|
|
+ if (!svgString.includes('xmlns="http://www.w3.org/2000/svg"')) {
|
|
|
+ svgString = svgString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
|
}
|
|
|
- finally {
|
|
|
- if (needRevoke) {
|
|
|
- URL.revokeObjectURL(imageUrl)
|
|
|
- }
|
|
|
+ const blob = new Blob([svgString], { type: 'image/svg+xml' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ try {
|
|
|
+ const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
+ const image = new Image();
|
|
|
+ image.onload = () => resolve(image);
|
|
|
+ image.onerror = reject;
|
|
|
+ image.src = url;
|
|
|
+ });
|
|
|
+ // 5. 绘制到 canvas
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = img.width;
|
|
|
+ canvas.height = img.height;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+ ctx.drawImage(img, 0, 0);
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error('Metafile to PNG failed'))), 'image/png');
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 4. 导出为 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')
|
|
|
- })
|
|
|
+ // EMF 转 PNG(使用 EMFJS.Renderer)
|
|
|
+ async function convertEmfToPng(blob: Blob): Promise<Blob> {
|
|
|
+ const arrayBuffer = await blob.arrayBuffer();
|
|
|
+ return convertMetafileToPng(arrayBuffer, EMFJS.Renderer);
|
|
|
+ }
|
|
|
|
|
|
- return new File([outputBlob], filename, { type: 'image/png' })
|
|
|
+ // WMF 转 PNG(使用 WMFJS.Renderer)
|
|
|
+ async function convertWmfToPng(blob: Blob): Promise<Blob> {
|
|
|
+ const arrayBuffer = await blob.arrayBuffer();
|
|
|
+ return convertMetafileToPng(arrayBuffer, WMFJS.Renderer);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对 PNG 执行白色变透明
|
|
|
+ async function makeWhiteTransparentFromPng(pngBlob: Blob, tolerance: number): Promise<Blob> {
|
|
|
+ const url = URL.createObjectURL(pngBlob);
|
|
|
+ try {
|
|
|
+ const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
+ const image = new Image();
|
|
|
+ image.onload = () => resolve(image);
|
|
|
+ image.onerror = reject;
|
|
|
+ image.src = url;
|
|
|
+ });
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = img.width;
|
|
|
+ canvas.height = img.height;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+ ctx.drawImage(img, 0, 0);
|
|
|
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
+ const data = imageData.data;
|
|
|
+ for (let i = 0; i < data.length; i += 4) {
|
|
|
+ const r = data[i];
|
|
|
+ const g = data[i + 1];
|
|
|
+ const b = data[i + 2];
|
|
|
+ const dr = r - 255;
|
|
|
+ const dg = g - 255;
|
|
|
+ const db = b - 255;
|
|
|
+ if (Math.sqrt(dr * dr + dg * dg + db * db) <= tolerance) {
|
|
|
+ data[i + 3] = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ctx.putImageData(imageData, 0, 0);
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ canvas.toBlob(blob => (blob ? resolve(blob) : reject(new Error('Canvas toBlob failed'))), 'image/png');
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+
|
|
|
/**
|
|
|
* 上传 File 到 S3,返回公开访问的 URL
|
|
|
*/
|
|
|
@@ -1469,7 +1742,7 @@ export default () => {
|
|
|
}
|
|
|
|
|
|
// 如果 src 是 base64,触发上传
|
|
|
- if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
|
|
|
+ if (el.src && typeof el.src === 'string' && el.src.startsWith('blob:')) {
|
|
|
const uploadTask = (async () => {
|
|
|
try {
|
|
|
const file = await makeWhiteTransparent(el.src, `image_${Date.now()}.png`)
|
|
|
@@ -1502,27 +1775,27 @@ export default () => {
|
|
|
fixedRatio: true,
|
|
|
rotate: 0,
|
|
|
}
|
|
|
- /*
|
|
|
- // 如果 src 是 base64,触发上传
|
|
|
- if (el.picBase64 && typeof el.picBase64 === 'string' && el.picBase64.startsWith('data:')) {
|
|
|
- const uploadTask = (async () => {
|
|
|
- try {
|
|
|
- const file = makeWhiteTransparent(el.picBase64, `image_${Date.now()}.png`)
|
|
|
- if (file) {
|
|
|
- const url = await uploadFileToS3(file)
|
|
|
- element.src = url // 替换为远程 URL
|
|
|
- const slidesStore = useSlidesStore()
|
|
|
- slidesStore.updateElement({ id: element.id, props: { src: url } })
|
|
|
- }
|
|
|
- }
|
|
|
- catch (error) {
|
|
|
- console.error('Image upload failed:', error)
|
|
|
- // 失败时保留原 base64(或可置空)
|
|
|
- }
|
|
|
- })()
|
|
|
- uploadTasks.push(uploadTask)
|
|
|
- }
|
|
|
- */
|
|
|
+
|
|
|
+ // 如果 src 是 base64,触发上传
|
|
|
+ if (el.picBase64 && typeof el.picBase64 === 'string' && el.picBase64.startsWith('blob:')) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = makeWhiteTransparent(el.picBase64, `image_${Date.now()}.png`)
|
|
|
+ if (file) {
|
|
|
+ const url = await uploadFileToS3(file)
|
|
|
+ element.src = url // 替换为远程 URL
|
|
|
+ const slidesStore = useSlidesStore()
|
|
|
+ slidesStore.updateElement({ id: element.id, props: { src: url } })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('Image upload failed:', error)
|
|
|
+ // 失败时保留原 base64(或可置空)
|
|
|
+ }
|
|
|
+ })()
|
|
|
+ uploadTasks.push(uploadTask)
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
slide.elements.push(element)
|
|
|
|
|
|
@@ -1579,26 +1852,26 @@ export default () => {
|
|
|
rotate: 0,
|
|
|
autoplay: false,
|
|
|
}
|
|
|
- /*
|
|
|
- const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null)
|
|
|
- if (localData) {
|
|
|
- const uploadTask = (async () => {
|
|
|
- try {
|
|
|
- const file = await dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4')
|
|
|
- if (file) {
|
|
|
- const url = await uploadFileToS3(file)
|
|
|
- element.src = url
|
|
|
- const slidesStore = useSlidesStore()
|
|
|
- slidesStore.updateElement({ id: element.id, props: { src: url } })
|
|
|
- }
|
|
|
- }
|
|
|
- catch (error) {
|
|
|
- console.error('Video upload failed:', error)
|
|
|
- }
|
|
|
- })()
|
|
|
- uploadTasks.push(uploadTask)
|
|
|
- }
|
|
|
- */
|
|
|
+
|
|
|
+ const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null)
|
|
|
+ if (localData) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = await dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4')
|
|
|
+ if (file) {
|
|
|
+ const url = await uploadFileToS3(file)
|
|
|
+ element.src = url
|
|
|
+ const slidesStore = useSlidesStore()
|
|
|
+ slidesStore.updateElement({ id: element.id, props: { src: url } })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('Video upload failed:', error)
|
|
|
+ }
|
|
|
+ })()
|
|
|
+ uploadTasks.push(uploadTask)
|
|
|
+ }
|
|
|
+
|
|
|
slide.elements.push(element)
|
|
|
}
|
|
|
|