富文本編輯器復制word文檔中的圖片
文章有點長,感覺每次寫文章都特別啰嗦,如果不想看過程的話直接跳到*動手實踐那一步,那邊有核心的方法~
富文本編輯器復制 word 文檔中的圖片
問題點:從 word 文檔復制進來的內容的圖片都是 file:/// 協議,這時候如果我們的頁面是 http://或者 https:// 協議的話,就不允許讀取圖片了。

除非頁面也是本地文件打開的(但是實際項目中基本上是不可能的了):

與 ckeditor 相見恨晚
paste-from-word demo

看,ckeditor 就支持!然而這時候的項目已經有太多歷史包袱(包括后面新開發的插件,我用的是 tinymce )
倒不是說 tinymce 不好,只是用多了你會發現。。。真的很不好(說來話長,后面記錄 tinymce 的時候在吐槽把)
如果你也有編輯器需求,而且沒有歷史包袱,直接嘗試 ckeditor 把
獲取圖片的前奏
要獲取圖片,先從剪貼板入手,因為我們的數據源最后是從剪貼板復制過來的。
先了解幾個知識點,才能更好理解后面的內容
為什么網站不能直接讀取圖片?因為安全性:
ckeditor 在怎么強大也不可能從 http/https 協議下的網址讀取 file:/// 的文件。原因也很簡單,如果能讀取的話,豈不是網站能把我們全部的資料都讀到?
word 文檔內部的東西
word 文檔其實只需要把后綴改為 .zip。然后打開對應的目錄,你會發現圖片就存在里面,而且 word 目錄下還有一個 webSettings.xml 里面就存放著 word 文檔的信息。感興趣的就自己找一個看看把

關于系統剪貼板/JS 中的 clipboardData
我們經常用到的復制某一段字的功能,其實核心就是用到了 window 子對象 clipboardData 的一個方法:setData()
clipboardData.setData(sDataFormat, sData)
sDataFormat:要復制的內容的格式;
sData:要復制的內容。
只是因為 clipboardData 還是實驗性功能,所以平時用的不多。接下來要說的東西就和 sDataFormat 息息相關。
獲取剪貼板內容
主動獲取
缺點:
只能在 https 域名下使用(見下圖 1)
頁面必須聚焦,鼠標在控制臺都不行(見下圖 2)
還會被人發現,甚至被人拒絕(見下圖 3)
優點:
他能讓你獲取剪貼板內容。。。



navigator.clipboard
.readText()
.then(v => {
console.log('獲取剪貼板成功:', v)
})
.catch(v => {
console.log('獲取剪貼板失敗: ', v)
})
被控獲取 監聽 ctrl + v / 粘貼事件
使用 event 中的 clipboardData 調用 getData 方法,其中的參數目前我知道的有如下幾個
text 獲取文本
text/html 獲取 html 文本
text/plain 獲取普通文本,效果和 text 一樣
text/rtf 獲取 rtf 信息 (不懂就問,啥是 rtf)
window.addEventListener('paste', function(e) {
const clipdata = e.clipboardData || window.clipboardData
let data = clipdata.getData('text/html')
console.log(data)
})
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 節點
function findAllImageElementsWithLocalSource(documentFragment, writer) {
const range = writer.createRangeIn(documentFragment)
const imageElementsMatcher = new Matcher({
name: 'img'
})
const imgs = []
for (const value of range) {
if (imageElementsMatcher.match(value.item)) {
if (value.item.getAttribute('src').startsWith('file://')) {
imgs.push(value.item)
}
}
}
return imgs
}
接著執行 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}]/;
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
}
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
一下子清爽多了:

let boldFile = convertBase64ToBlob('base64的字符串')
// 直接使用 URL.createObjectURL 生成
imageElements[i].setAttribute('src', URL.createObjectURL(boldFile))
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 的代碼
流程總結
監聽粘貼事件,獲取剪貼板的數據(包括 text/html和text/rtf)
拿到 html 后把 file:// 開頭的 img 節點找出來,然后使用轉換方法把 rtf 對應的圖片信息也一一對應的找出來
使用 hex 轉 base64 的方法獲取到圖片的 base64 信息,然后在看需要進行轉換
彩蛋 - 下集預告
上面說到有一個坑,就是我們獲取的 getData('text/html') 和 getData('text/rtf')
這 2 個東西并不是憑空出現的,而且人為設置的(不要覺得復制的任何東西都有 text/html)
這些東西都是在設置剪貼板的時候 setData('text/html')。設置了有什么,才能拿到什么(因為我在富文本的另一個功能中踩到這坑了,包括 safari 瀏覽器也有坑!)
下一篇文章就來寫寫這個剪貼板的坑!
復制 word 文檔圖片原理的文章真的好少~希望我這篇能幫到你
參考文章:http://blog.ncmem.com/wordpress/2023/12/27/%e5%af%8c%e6%96%87%e6%9c%ac%e7%bc%96%e8%be%91%e5%99%a8%e5%a4%8d%e5%88%b6word%e6%96%87%e6%a1%a3%e4%b8%ad%e7%9a%84%e5%9b%be%e7%89%87/
歡迎入群一起討論

浙公網安備 33010602011771號