富文本编辑器复制word文档中的图片
文章目錄
- 富文本編輯器復制 word 文檔中的圖片
- 與 ckeditor 相見恨晚
- 獲取圖片的前奏
- 獲取剪貼板內容
- 獲取 word 文檔中的圖片
- replaceImagesSourceWithBase64 方法
- extractImageDataFromRtf 方法
- replaceImagesFileSourceWithInlineRepresentation
- 動手實踐,獲取圖片信息并展示
- 錦上添花,實現圖片上傳
- base64 轉換為 blod 對象
- 優化顯示的 URL
- blod 轉 base64
- 總結
- 流程總結
- 彩蛋 - 下集預告
文章有點長,感覺每次寫文章都特別啰嗦,如果不想看過程的話直接跳到*動手實踐那一步,那邊有核心的方法~
富文本編輯器復制 word 文檔中的圖片
- 問題點:從 word 文檔復制進來的內容的圖片都是 file:/// 協議,這時候如果我們的頁面是 http://或者 https:// 協議的話,就不允許讀取圖片了。
除非頁面也是本地文件打開的(但是實際項目中基本上是不可能的了):
與 ckeditor 相見恨晚
paste-from-word demo
看,ckeditor 就支持!然而這時候的項目已經有太多歷史包袱(包括后面新開發的插件,我用的是 tinymce )
倒不是說 tinymce 不好,只是用多了你會發現。。。真的很不好(說來話長,后面記錄 tinymce 的時候在吐槽把)
如果你也有編輯器需求,而且沒有歷史包袱,直接嘗試 ckeditor 把
獲取圖片的前奏
要獲取圖片,先從剪貼板入手,因為我們的數據源最后是從剪貼板復制過來的。
先了解幾個知識點,才能更好理解后面的內容
ckeditor 在怎么強大也不可能從 http/https 協議下的網址讀取 file:/// 的文件。原因也很簡單,如果能讀取的話,豈不是網站能把我們全部的資料都讀到?
word 文檔其實只需要把后綴改為 .zip。然后打開對應的目錄,你會發現圖片就存在里面,而且 word 目錄下還有一個 webSettings.xml 里面就存放著 word 文檔的信息。感興趣的就自己找一個看看把
我們經常用到的復制某一段字的功能,其實核心就是用到了 window 子對象 clipboardData 的一個方法:setData()
clipboardData.setData(sDataFormat, sData)- sDataFormat:要復制的內容的格式;
- sData:要復制的內容。
只是因為 clipboardData 還是實驗性功能,所以平時用的不多。接下來要說的東西就和 sDataFormat 息息相關。
獲取剪貼板內容
-
缺點:
- 只能在 https 域名下使用(見下圖 1)
- 頁面必須聚焦,鼠標在控制臺都不行(見下圖 2)
- 還會被人發現,甚至被人拒絕(見下圖 3)
-
優點:
- 他能讓你獲取剪貼板內容。。。
使用 event 中的 clipboardData 調用 getData 方法,其中的參數目前我知道的有如下幾個
- text 獲取文本
- text/html 獲取 html 文本
- text/plain 獲取普通文本,效果和 text 一樣
- text/rtf 獲取 rtf 信息 (不懂就問,啥是 rtf)
PS:復制后到頁面上隨便粘貼一下,不一定要找到輸入框,按下 ctrl+v 就行
輸出如下:上面還有一大堆亂七八糟的標簽,wps 就比 office 干凈多了,這個是從 office 復制進來的。
- clipdata.getData('text/html') 也就是我們富文本用的方法,獲取粘貼的內容的 html 代碼 注意是 text/html 這里有個坑,后面會說到
- clipdata.getData('text/rtf') 獲取的東西更加亂了,不過里面就記載著我們的圖片信息(我的文檔就 2 張圖片,11mb.可怕)
有了上面的基礎知識,我們就能拋開富文本編輯器,先來實現一個文章最前面的截圖,粘貼顯示 word 文檔的功能。
<body><p>請按下ctrl+v粘貼內容</p><div id="preview"></div><script>window.addEventListener("paste", function (e) {const clipdata = e.clipboardData || window.clipboardData;document.querySelector('#preview').innerHTML = clipdata.getData("text/html")});</script> </body> </html>獲取 word 文檔中的圖片
下面根據 ckeditor 的源碼來學習,具體的代碼是在
GitHub:ckeditor5-paste-from-office
或者從 npm 下載:@ckeditor/ckeditor5-paste-from-office
分析源碼:
src/index.js -> src/pastefromoffice.js (在 init 函數中,執行了一個 activeNormalizer.execute方法)-> src/normalizers/mswordnormalizer.js
到這里就看到了一個 replaceImagesSourceWithBase64 方法,這就是今天學習的核心
replaceImagesSourceWithBase64 方法
該方法在:src/filters/image.js
在 replaceImagesSourceWithBase64 函數中,和圖片相關的方法是:
- findAllImageElementsWithLocalSource 查找全部的 file:/// 開頭的圖片
createRangeIn、new Matcher、這些方法都不用太過于關注,因為復制進來的都是文本,這些可能是 ckeditor 核心代碼中轉換為 dom 節點的方法
我們直接粗暴點渲染為真實 dom,然后在操作真實 dom 就是了
第 12 行,獲取 src 是 file:// 開頭的 dom 節點
- 接著執行 replaceImagesFileSourceWithInlineRepresentation 方法。在這之前還會執行 extractImageDataFromRtf
extractImageDataFromRtf 方法
同樣是在 src/filters/image.js
這部分代碼是把我們從剪貼板中 getData('text/rtf') 獲取到的值做一個加工,提取里面的圖片信息(我承認沒看懂提取的是啥,我對 rtf 也不那么了解,哈哈哈哈)
更新一點點東西(關于正則無法匹配到最新的圖片節點)
regexPictureHeader 這段正則中,在以前的時候還是可以用的,可能最近 rtf 又更新了,導致匹配失敗,無法生成圖片
于是進過一番探索,根據舊的正則自己刪減了一部分匹配規則,進過測試 office 和 wps 都能識別。
舊的寫法: const regexPictureHeader = /{\pict[\s\S]+?\bliptag-?\d+(\blipupi-?\d+)?({\*\blipuid\s?[\da-fA-F]+)?[\s}]?/;
新的寫法:const regexPictureHeader = /{\pict[\s\S]+?({\*\blipuid\s?[\da-fA-F]+)[\s}]/;
replaceImagesFileSourceWithInlineRepresentation
同文件下的方法
傳入的參數第一個是 src 為file://的圖片節點數組,第二個從 rtf 提取的圖片信息數組,第三個就是 ckeditor 自己的方法了,用來顯示文本的,不用管他
還用到了一個 _convertHexToBase64 方法,把 hex 轉換為 base64
接著就是一頓循環了,對應的節點替換為對應的 base64,設置到圖片節點的的 src 上,只是這里他們用了自身封裝的 writer。
function replaceImagesFileSourceWithInlineRepresentation(imageElements, imagesHexSources, writer) {// Assume there is an equal amount of image elements and images HEX sources so they can be matched accordingly based on existing order.if (imageElements.length === imagesHexSources.length) {for (let i = 0; i < imageElements.length; i++) {const newSrc = `data:${imagesHexSources[i].type};base64,${_convertHexToBase64(imagesHexSources[i].hex)}`writer.setAttribute('src', newSrc, imageElements[i])}} }function _convertHexToBase64(hexString) {return btoa(hexString.match(/\w{2}/g).map(char => {return String.fromCharCode(parseInt(char, 16))}).join('')) }動手實踐,獲取圖片信息并展示
上面分析了一些 ckeditor 代碼之后,其實我們要用的也就是
- findAllImageElementsWithLocalSource
- 這個方法被改造了一下,直接讀取實際的 dom 節點,拿到圖片節點
- replaceImagesFileSourceWithInlineRepresentation
- 這個方法在最后賦值的時候也改了下,因為我們已經記錄了實際的 dom 節點,所以直接使用 .setAttribute(‘src’,newSrc)
- extractImageDataFromRtf
- _convertHexToBase64
整理過后的代碼如下:
<body><p>請按下ctrl+v粘貼內容</p><div id="preview"></div><script>window.addEventListener("paste", function (e) {const clipdata = e.clipboardData || window.clipboardData;document.querySelector('#preview').innerHTML = clipdata.getData("text/html")let rtf = clipdata.getData('text/rtf')let imgs = findAllImageElementsWithLocalSource()replaceImagesFileSourceWithInlineRepresentation(imgs, extractImageDataFromRtf(rtf))});function findAllImageElementsWithLocalSource() {let imgs = document.querySelectorAll('img')return imgs;}function extractImageDataFromRtf(rtfData) {if (!rtfData) {return [];}// 舊的寫法// const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/// 新刪減后的寫法const regexPictureHeader = /{\\pict[\s\S]+?({\\\*\\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 = false;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 _convertHexToBase64(hexString) {return btoa(hexString.match(/\w{2}/g).map(char => {return String.fromCharCode(parseInt(char, 16));}).join(''));}function replaceImagesFileSourceWithInlineRepresentation(imageElements, imagesHexSources, writer) {// Assume there is an equal amount of image elements and images HEX sources so they can be matched accordingly based on existing order.if (imageElements.length === imagesHexSources.length) {for (let i = 0; i < imageElements.length; i++) {const newSrc = `data:${imagesHexSources[i].type};base64,${_convertHexToBase64(imagesHexSources[i].hex)}`;imageElements[i].setAttribute('src',newSrc)}}}</script></body></html>錦上添花,實現圖片上傳
進過上面一系列方法后,我們確實是拿到了 base64 格式的圖片,可是這顯示未免也太長了一些,如果要實現上傳,還得后端給我們重新起一個 base64 圖片上傳的方法。。。
base64 轉換為 blod 對象
blod 就是我們平時用 input 選擇圖片后拿到的 File 類型(不知道有沒有解釋錯,大概就是這個意思)
方法如下:
/** 將base64轉換為文件對象* @param {String} base64 base64字符串**/ function convertBase64ToBlob(base64) {var base64Arr = base64.split(',')var imgtype = ''var base64String = ''if (base64Arr.length > 1) {//如果是圖片base64,去掉頭信息base64String = base64Arr[1]imgtype = base64Arr[0].substring(base64Arr[0].indexOf(':') + 1, base64Arr[0].indexOf(';'))}// 將base64解碼var bytes = atob(base64String)//var bytes = base64;var bytesCode = new ArrayBuffer(bytes.length)// 轉換為類型化數組var byteArray = new Uint8Array(bytesCode)// 將base64轉換為ascii碼for (var i = 0; i < bytes.length; i++) {byteArray[i] = bytes.charCodeAt(i)}// 生成Blob對象(文件對象)return new Blob([bytesCode], { type: imgtype }) }效果如下
優化顯示的 URL
上傳問題是解決了,可是那么長的 base64 看著實在是糟心,還好我們還有 ObjectURL
一下子清爽多了:
blod 轉 base64
既然都說到這里了,還有一個轉換就順便說了把
function readBlobAsDataURL(blob, callback) {var a = new FileReader()a.onload = function(e) {callback(e.target.result)}a.readAsDataURL(blob) }readBlobAsDataURL('blod文件對象', function(base64) {console.log(base64) })圖片讀取,圖片顯示,包括圖片轉換為 blod 對象也有了,只要圖片上傳后,在回顯一下,就齊活了~
總結
核心原理包括 ckeditor 部分源碼解讀就結束了,當然還有很多細節沒考慮,包括一些標簽的轉換,標簽過濾,樣式過濾,最主要的是要判斷復制進來的到底是不是 word 文檔,還有如果拿不到 rtf 等各種情況,都可以研究下 ckeditor 的代碼
流程總結
彩蛋 - 下集預告
上面說到有一個坑,就是我們獲取的 getData('text/html') 和 getData('text/rtf')
這 2 個東西并不是憑空出現的,而且人為設置的(不要覺得復制的任何東西都有 text/html)
這些東西都是在設置剪貼板的時候 setData('text/html')。設置了有什么,才能拿到什么(因為我在富文本的另一個功能中踩到這坑了,包括 safari 瀏覽器也有坑!)
下一篇文章就來寫寫這個剪貼板的坑!
復制 word 文檔圖片原理的文章真的好少~希望我這篇能幫到你
總結
以上是生活随笔為你收集整理的富文本编辑器复制word文档中的图片的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据库技术基础:查询优化相关知识笔记
- 下一篇: VirtualBox安装增强功能时报错: