Переглянути джерело

word复制的图文混合内容,把图片替换为dataUrl,解决复制word粘贴后图片丢失的问题。 (#1830)

将dataUrl上传到服务器,并返回获取数据的url。解决md内容中由于大量base64而太长的问题。
hz 3 місяців тому
батько
коміт
f3943112c1
2 змінених файлів з 91 додано та 0 видалено
  1. 88 0
      src/ts/util/fixBrowserBehavior.ts
  2. 3 0
      types/index.d.ts

+ 88 - 0
src/ts/util/fixBrowserBehavior.ts

@@ -1404,6 +1404,10 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
         if (textHTML.trim() !== "") {
             const tempElement = document.createElement("div");
             tempElement.innerHTML = textHTML;
+            // word复制的图文混合,替换为dataUrl: <v:imagedata src="file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png" o:title="">
+            replaceVmlImage(tempElement, ("clipboardData" in event ? event.clipboardData : event.dataTransfer).getData("text/rtf"))
+            await uploadBase64Img(tempElement, vditor)
+
             tempElement.querySelectorAll("[style]").forEach((e) => {
                 e.removeAttribute("style");
             });
@@ -1499,3 +1503,87 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
         scrollCenter(vditor);
     }
 };
+
+//replace <v:imagedata src=...> to <img>, get image data by text/rtf
+function replaceVmlImage(root:Element, rtfData: string) {
+    const images = extractImageDataFromRtf(rtfData)
+    const shapes : Array<{shape:Element,img:Element}> = []
+    walk(root, (child : Element) => {
+        if (child.tagName === 'V:SHAPE') {
+            walk(child, (sub) => {
+                if (sub.tagName === 'V:IMAGEDATA') shapes.push({ shape: child, img: sub })
+            })
+            return false
+        }
+    })
+    for (let i = 0; i < shapes.length; i++) {
+        const img = document.createElement('img')
+        const newSrc = 'data:' + images[i].type + ';base64,' + hexToBase64(images[i].hex)
+        img.src = newSrc
+        img.title = shapes[i].img.getAttribute('title')
+        shapes[i].shape.parentNode.replaceChild(img, shapes[i].shape)
+    }
+
+    function walk(el:Element, fn: (el:Element)=> boolean| void) {
+        const goNext = fn(el)
+        if (goNext !== false)
+            for (let i=0;i<  el.children.length;i++) {
+                walk(el.children[i], fn)
+            }
+    }
+}
+function extractImageDataFromRtf( rtfData:string ) {
+    if ( !rtfData ) {
+        return [];
+    }
+
+    const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/;
+    const regexPicture = new RegExp( '(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}', 'g' );
+    const images = rtfData.match( regexPicture );
+    const result = [];
+
+    if ( images ) {
+        for ( const image of images ) {
+            let imageType;
+
+            if ( image.includes( '\\pngblip' ) ) {
+                imageType = 'image/png';
+            } else if ( image.includes( '\\jpegblip' ) ) {
+                imageType = 'image/jpeg';
+            }
+
+            if ( imageType ) {
+                result.push( {
+                    hex: image.replace( regexPictureHeader, '' ).replace( /[^\da-fA-F]/g, '' ),
+                    type: imageType
+                } );
+            }
+        }
+    }
+
+    return result;
+}
+function hexToBase64(hexString :string) {
+    const ms = hexString.match(/\w{2}/g) || []
+    return btoa(ms.map(char => {
+        return String.fromCharCode(parseInt(char, 16));
+    }).join(''));
+}
+async function uploadBase64Img(root:Element, vditor: IVditor) {
+    if (vditor.options.upload.handleDataUrl) {
+        const imgs = root.querySelectorAll('img')
+        for (let i = 0; i < imgs.length; i++) {
+            const src = imgs[i].src || ''
+            if(src) imgs[i].src = await vditor.options.upload.handleDataUrl(src)
+        }
+    }
+}
+function base64ToBlob(base64: string, contentType: string) {
+    const raw = atob(base64 || '');
+    const rawLength = raw.length;
+    const uInt8Array = new Uint8Array(rawLength);
+    for (let i = 0; i < rawLength; ++i) {
+        uInt8Array[i] = raw.charCodeAt(i);
+    }
+    return new Blob([uInt8Array], { type: contentType });
+}

+ 3 - 0
types/index.d.ts

@@ -400,6 +400,9 @@ interface IUpload {
     /** 自定义上传,当发生错误时返回错误信息 */
     handler?(files: File[]): string | null | Promise<string> | Promise<null>;
 
+    //将dataUrl上传到服务器,并返回获取数据的url
+    handleDataUrl?(dataUrl: string): string | Promise<string>;
+
     /** 对服务端返回的数据进行转换,以满足内置的数据结构 */
     format?(files: File[], responseText: string): string;