jack 1 week ago
parent
commit
c4205a8a34
4 changed files with 693 additions and 121 deletions
  1. 215 0
      package-lock.json
  2. 4 0
      package.json
  3. 391 118
      src/hooks/useImport.ts
  4. 83 3
      src/utils/prosemirror/schema/nodes.ts

+ 215 - 0
package-lock.json

@@ -41,15 +41,19 @@
         "prosemirror-state": "^1.4.3",
         "prosemirror-view": "^1.33.9",
         "qs": "^6.14.0",
+        "rtf.js": "^3.0.9",
         "svg-arc-to-cubic-bezier": "^3.2.0",
         "svg-pathdata": "^7.1.0",
         "tinycolor2": "^1.6.0",
         "tippy.js": "^6.3.7",
+        "utif": "^3.1.0",
+        "utif2": "^4.1.0",
         "uuid": "^13.0.0",
         "vue": "^3.5.17",
         "vuedraggable": "^4.1.0",
         "wangeditor": "^4.7.15",
         "webcodecs-encoder": "^0.3.2",
+        "wmf2png": "^1.0.0",
         "y-websocket": "^3.0.0",
         "yjs": "^13.6.27"
       },
@@ -1996,6 +2000,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
@@ -2235,6 +2248,15 @@
         "node": ">=12"
       }
     },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -2942,6 +2964,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/extsprintf": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.4.1.tgz",
+      "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
+      "engines": [
+        "node >=0.6.0"
+      ],
+      "license": "MIT"
+    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4518,6 +4549,15 @@
       "resolved": "https://registry.npmmirror.com/orderedmap/-/orderedmap-2.1.1.tgz",
       "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
     },
+    "node_modules/os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/p-limit": {
       "version": "3.1.0",
       "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
@@ -5341,6 +5381,15 @@
       "resolved": "https://registry.npmmirror.com/rope-sequence/-/rope-sequence-1.3.4.tgz",
       "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
     },
+    "node_modules/rtf.js": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmmirror.com/rtf.js/-/rtf.js-3.0.9.tgz",
+      "integrity": "sha512-I1GpDat4i548WzmeZXv27f/743984fvEeeBS8BC01/Sop17pMlUl3M7DYcdcB3PUvOZTrFIMxGZx8qw7cSMAKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "codepage": "^1.15.0"
+      }
+    },
     "node_modules/run-parallel": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -5876,6 +5925,24 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/utif": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/utif/-/utif-3.1.0.tgz",
+      "integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
+      "license": "MIT",
+      "dependencies": {
+        "pako": "^1.0.5"
+      }
+    },
+    "node_modules/utif2": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/utif2/-/utif2-4.1.0.tgz",
+      "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
+      "license": "MIT",
+      "dependencies": {
+        "pako": "^1.0.11"
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -5911,6 +5978,38 @@
         "spdx-expression-parse": "^3.0.0"
       }
     },
+    "node_modules/vasync": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmmirror.com/vasync/-/vasync-2.2.1.tgz",
+      "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
+      "engines": [
+        "node >=0.6.0"
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "verror": "1.10.0"
+      }
+    },
+    "node_modules/verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
+      "engines": [
+        "node >=0.6.0"
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
+    "node_modules/verror/node_modules/core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
+      "license": "MIT"
+    },
     "node_modules/vite": {
       "version": "5.3.5",
       "resolved": "https://registry.npmmirror.com/vite/-/vite-5.3.5.tgz",
@@ -6093,6 +6192,34 @@
         "node": ">= 8"
       }
     },
+    "node_modules/wmf2png": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/wmf2png/-/wmf2png-1.0.0.tgz",
+      "integrity": "sha512-sPVTVKcDdH+oSv9WEkHB3DFUGUfUwy5HH2liQqN8Elqj6YObYac0bkz/L6FeYCgvuBjZicn23CVBBEbNo65I+A==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "BSD-2-Clause",
+      "os": [
+        "win32"
+      ],
+      "dependencies": {
+        "tmp": "0.0.33",
+        "vasync": "^2.2.0"
+      }
+    },
+    "node_modules/wmf2png/node_modules/tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "license": "MIT",
+      "dependencies": {
+        "os-tmpdir": "~1.0.2"
+      },
+      "engines": {
+        "node": ">=0.6.0"
+      }
+    },
     "node_modules/wrap-ansi": {
       "version": "7.0.0",
       "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -7610,6 +7737,11 @@
       "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==",
       "dev": true
     },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
+    },
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
@@ -7784,6 +7916,11 @@
         "wrap-ansi": "^7.0.0"
       }
     },
+    "codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="
+    },
     "color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -8325,6 +8462,11 @@
         "strip-final-newline": "^2.0.0"
       }
     },
+    "extsprintf": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.4.1.tgz",
+      "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="
+    },
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -9525,6 +9667,11 @@
       "resolved": "https://registry.npmmirror.com/orderedmap/-/orderedmap-2.1.1.tgz",
       "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
     },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="
+    },
     "p-limit": {
       "version": "3.1.0",
       "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
@@ -10169,6 +10316,14 @@
       "resolved": "https://registry.npmmirror.com/rope-sequence/-/rope-sequence-1.3.4.tgz",
       "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
     },
+    "rtf.js": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmmirror.com/rtf.js/-/rtf.js-3.0.9.tgz",
+      "integrity": "sha512-I1GpDat4i548WzmeZXv27f/743984fvEeeBS8BC01/Sop17pMlUl3M7DYcdcB3PUvOZTrFIMxGZx8qw7cSMAKQ==",
+      "requires": {
+        "codepage": "^1.15.0"
+      }
+    },
     "run-parallel": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -10584,6 +10739,22 @@
         "punycode": "^2.1.0"
       }
     },
+    "utif": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/utif/-/utif-3.1.0.tgz",
+      "integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
+      "requires": {
+        "pako": "^1.0.5"
+      }
+    },
+    "utif2": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/utif2/-/utif2-4.1.0.tgz",
+      "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
+      "requires": {
+        "pako": "^1.0.11"
+      }
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -10612,6 +10783,31 @@
         "spdx-expression-parse": "^3.0.0"
       }
     },
+    "vasync": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmmirror.com/vasync/-/vasync-2.2.1.tgz",
+      "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
+      "requires": {
+        "verror": "1.10.0"
+      }
+    },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      },
+      "dependencies": {
+        "core-util-is": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
+          "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
+        }
+      }
+    },
     "vite": {
       "version": "5.3.5",
       "resolved": "https://registry.npmmirror.com/vite/-/vite-5.3.5.tgz",
@@ -10718,6 +10914,25 @@
         "isexe": "^2.0.0"
       }
     },
+    "wmf2png": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/wmf2png/-/wmf2png-1.0.0.tgz",
+      "integrity": "sha512-sPVTVKcDdH+oSv9WEkHB3DFUGUfUwy5HH2liQqN8Elqj6YObYac0bkz/L6FeYCgvuBjZicn23CVBBEbNo65I+A==",
+      "requires": {
+        "tmp": "0.0.33",
+        "vasync": "^2.2.0"
+      },
+      "dependencies": {
+        "tmp": {
+          "version": "0.0.33",
+          "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.0.33.tgz",
+          "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+          "requires": {
+            "os-tmpdir": "~1.0.2"
+          }
+        }
+      }
+    },
     "wrap-ansi": {
       "version": "7.0.0",
       "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

+ 4 - 0
package.json

@@ -48,15 +48,19 @@
     "prosemirror-state": "^1.4.3",
     "prosemirror-view": "^1.33.9",
     "qs": "^6.14.0",
+    "rtf.js": "^3.0.9",
     "svg-arc-to-cubic-bezier": "^3.2.0",
     "svg-pathdata": "^7.1.0",
     "tinycolor2": "^1.6.0",
     "tippy.js": "^6.3.7",
+    "utif": "^3.1.0",
+    "utif2": "^4.1.0",
     "uuid": "^13.0.0",
     "vue": "^3.5.17",
     "vuedraggable": "^4.1.0",
     "wangeditor": "^4.7.15",
     "webcodecs-encoder": "^0.3.2",
+    "wmf2png": "^1.0.0",
     "y-websocket": "^3.0.0",
     "yjs": "^13.6.27"
   },

+ 391 - 118
src/hooks/useImport.ts

@@ -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)
             }
 

+ 83 - 3
src/utils/prosemirror/schema/nodes.ts

@@ -94,11 +94,91 @@ const bulletList: NodeSpec = {
   },
 }
 
+/*
 const listItem: NodeSpec = {
   ..._listItem,
   content: 'paragraph block*',
   group: 'block',
 }
+*/
+
+const listItem: NodeSpec = {
+  attrs: {
+    align: { default: '' },
+    textIndent: { default: '' },
+    marginTop: { default: '' },
+    marginBottom: { default: '' },
+    marginLeft: { default: '' },
+    marginRight: { default: '' },
+  },
+  content: 'paragraph block*',
+  group: 'block',
+  parseDOM: [
+    {
+      tag: 'li',
+      getAttrs(dom) {
+        const el = dom as HTMLElement;
+        const style = el.style;
+
+        const textAlign = style.textAlign || '';
+        const textAlignLast = style.textAlignLast || '';
+        const textIndent = style.textIndent || '';
+        const marginTop = style.marginTop || '';
+        const marginBottom = style.marginBottom || '';
+        const marginLeft = style.marginLeft || '';
+        const marginRight = style.marginRight || '';
+
+        let align = textAlign || textAlignLast || '';
+        align = /^(left|right|center|justify)$/.test(align) ? align : '';
+
+        return {
+          align,
+          textIndent,
+          marginTop,
+          marginBottom,
+          marginLeft,
+          marginRight,
+        };
+      },
+    },
+  ],
+  toDOM(node) {
+    const {
+      align,
+      textIndent,
+      marginTop,
+      marginBottom,
+      marginLeft,
+      marginRight,
+    } = node.attrs;
+
+    let style = '';
+
+    if (align) {
+      style += `text-align: ${align}; text-align-last: ${align};`;
+    }
+    if (textIndent) {
+      style += `text-indent: (100% - ${textIndent});`;
+    }
+    if (marginTop) {
+      style += `margin-top: ${marginTop};`;
+    }
+    if (marginBottom) {
+      style += `margin-bottom: ${marginBottom};`;
+    }
+    if (marginLeft) {
+      style += `margin-left: ${marginLeft};`;
+    }
+    if (marginRight) {
+      style += `margin-right: ${marginRight};`;
+    }
+
+    const attrs: { style?: string } = {};
+    if (style) attrs.style = style;
+
+    return ['li', attrs, 0];
+  },
+};
 
 const paragraph: NodeSpec = {
   attrs: {
@@ -136,7 +216,7 @@ const paragraph: NodeSpec = {
 
         const indent = +((dom as HTMLElement).getAttribute('data-indent') || 0)
       
-        return { align, indent, textIndent: textIndentLevel }
+        return { align, indent, textIndent }
       }
     },
     {
@@ -152,10 +232,10 @@ const paragraph: NodeSpec = {
     const { align, indent, textIndent } = node.attrs
     let style = ''
     if (align && align !== 'left') style += `text-align: ${align};text-align-last: ${align};`
-    if (textIndent) style += `text-indent: ${textIndent}em;`
+    if (textIndent) style += `text-indent: ${textIndent};`
 
     const attr: Attr = { style }
-    if (indent) attr['data-indent'] = indent
+    //if (indent) attr['data-indent'] = indent
 
     return ['p', attr, 0]
   },