說起富文本編輯器,我們大都遇到過,甚至使用過,這種所見即所得的書寫方式,以及它靈活的排版,讓我們的創作更加流暢和美觀。其實你可以把它理解成是把word等軟件的功能轉成在瀏覽器里面使用,這樣就能通過其他的一些手段進行管理,并融入到相應系統中。但是由于實現方式和語言等的不同,存在著一些出入。
比如我現在正在使用的,也就是此刻我寫這篇文章的工具,就是一個富文本編輯器。其實富文本編輯器有很多種,它們的功能類似、產出目的類似、使用方式也類似,只不過在豐富程度上稍有差別,今天的CKEditor5就是其中的一款。
示意圖
可以看到,還是很好看的,美而不失實用。它的功能特別多,只不過有一些功能是要收費的,也就是說它只開源了一部分,或者說對于一些更高級的吊吊的功能你需要少買一點零食或者玩具。不過這些基礎功能已經足夠用了,它的可插拔式插件集成功能非常強大。
示意圖
就像上面所示,你可以隨意的添加或刪除一個擴展功能,下面有非常多的待繼承插件供你選擇。
示意圖
但是像上面這種的,帶有premium的插件,那你就需要支付一定的費用才可以使用啦。
細心的你相信一眼就看出來了,這就是我們今天要講的內容:從word中導入。
這是一個高級功能,雖然不是很常用,但是有一些特殊的場景或者需求,我們可能希望從編輯好的word中,通過導入的方式來讓用戶在網頁中繼續編輯它,并盡可能的保留內容和格式。
一個是自己資金不是很充裕,再一個是想自己去動手做做,因此就決定獨立實現這樣一個功能。自己做的,當然可以隨便免費用。
在開始之前,我們先看下做這個功能在完成之后需要滿足的效果,雖然這個功能官網是收費的,但是為了給大家演示,官網也提供了示例,我們先看下官網的成品:
效果圖
我們先根據提示,在官網示例上面下載了它提供的一個word,然后用CKEditor5的導入word功能,把這個word導入到編輯器中,解析完成之后就看到了效果,它的還原度很高了,官網應該是特意制作的示例word文件,里邊包含了段落、列表、圖片、表格等等多個技術點。這些都是我們接下來要實現的內容,官網這復雜程度,錢花的挺值。
為了能讓大家有一個對比,這里我把原版word也展示出來給你們看一下:
效果圖
可以對比著感受下,不過還是有一些地方不太一樣的,比如我對這個原文檔做一點點更改。體現就稍微有一點略微的不同,但是這個不是毛病,只是看著有點別扭,我給兩張圖,先來原word的圖,這是我改過的列表:
示意圖
再來一張官網導入之后渲染的效果圖:
示意圖
主要有:1.列表距左邊的距離。2.列表項之間多出空白。3.不能顯示中文序號。
我們要想實現這樣一個插件,首先想到有沒有現成的word轉html的前端或者后端插件,因為富文本編輯器是可以設置內容的,并且這個內容實質就是html代碼,然后再在這個基礎上進行集成開發。
因為我有自己的node后端,所以如果用后端做的話就找了一些關于node的word轉html插件,一共找到了docx2html、mammoth、word2html等,但是經過測試都不太理想,于是決定放棄,換一個思路,我們可以解析word,然后根據word規范,自己生成出html。
word是流式文件,能任意編輯并且回顯,那么肯定有一套約定在里邊,能夠保存格式并重新讀取,就看它有沒有開放給我們,幸好,docx這個x就是告訴我們,可以的,因為它就是xml的意思,符合xml規范。
好了,我們可以找出兩個輔助插件:
第一個就是用來解壓縮用的adm-zip包。
第二個就是用來解析xml文件的xml-js包。
為什么這樣呢?這是因為一個docx文件,就是一個壓縮包,我們把docx文件重命名為zip格式。然后就可以解壓看下里面的內容:
示意圖
這就是解壓之后的目錄,里面包含著所有的word內容,我們一會揭開它的面紗。其中一個關鍵目錄就是word文件夾:
示意圖
可以看到有很多的xml文件,它們就規定了word的回顯機制和渲染邏輯。
還有一個media文件夾,我們看下它里面有什么:
示意圖
可以明顯的看到有兩張圖片,這兩張圖片就是我們在原word中使用的圖片,它就隱藏在這里。
另外,其中document.xml文件存儲了整個word的結構和內容,numbering.xml文件規定了列表如何渲染,styles.xml告訴了需要應用哪些樣式。
我們就以document.xml文件做一個簡單的說明,其余不做過多展開:
示意圖
文件前面是對該xml的一些聲明,body中包含了一個個的段落,也就是w:p。其中又包含了多個系列w:r,系列中就存儲著我們的文本,比如上圖紅框中我圈出的部分。
而且里面還存儲著段落屬性w:pPr和系列屬性w:rPr。我們就是通過對這些一對對的xml標簽,來對word進行解析,找出它的渲染規則。
首先使用上面提到的兩個包,非常簡單:
const dir = join(process.cwd(), 'public/temp/word/' + fn)
const zip = new AdmZip(dir)
let contentXml = zip.readAsText('word/document.xml')
const documentData = xml2js(contentXml)
contentXml = zip.readAsText('word/numbering.xml')
const numberingData = contentXml ? xml2js(contentXml) : {
elements: ''
}
contentXml = zip.readAsText('word/_rels/document.xml.rels')
const relsData = xml2js(contentXml)
contentXml = zip.readAsText('word/styles.xml')
const styleData = xml2js(contentXml)
let ent = zip.getEntries()
let ind = fn.lastIndexOf('.')
let flag = false
for(let i = 0; i < ent.length; i++) {
let n = ent[i].entryName
if(n.substring(0, 11) === 'word/media/') {
flag = true
zip.extractEntryTo(n, join(process.cwd(), 'public/temp/word/' + fn.substring(0, ind)), false, true)
}
}
return {
documentXML: documentData?.elements[0]?.elements[0]?.elements,
numberingXML: numberingData?.elements[0]?.elements,
relsXML: relsData?.elements[0]?.elements,
styleXML: styleData?.elements[0]?.elements.slice(2),
imagePath: fn.substring(0, ind),
}
簡單對上面的代碼做一下說明:
至此,我們看一下目前解析完成之后,形成的數據結構。
示意圖
很好,現在開始集成:
import { Editor } from '/lib/ckeditor5/ckeditor'
import loadConfig from './config'
import filePlugin from './file'
import './style.scss'
loadConfig(Editor)
const container: any = ref(null)
let richEditor: any = null
onMounted(() => {
Editor.create(container.value, {
extraPlugins: [filePlugin]
}).then((editor: any) => {
richEditor = editor
}).catch((error: any) => {
console.log(error.stack)
})
})
第1行,導入Editor,也就是我們一會要用的富文本編輯器,然后第9行通過create方法創建它,接收的兩個參數分別表示:渲染的容器與配置的插件。
因為CKEditor5填入圖片的時候,需要自己手動實現一個插件方法,因此我們要把它配置進來,因為跟咱們要講的內容無關,就不展開了,官方文檔說的很清楚了。
第5行,我在初始化編輯器之前,先去加載了一些配置,其中一個就是引入word轉pdf的功能,由于CKEditor5插件擴展很容易,直接在Editor的builtinPlugins屬性數據里面加上我們實現的插件就可以,所以我們直接講插件的開發:
import { ButtonView, Plugin } from '/lib/ckeditor5/ckeditor'
import { postData } from '@/request'
import { DocumentWordProcessorReference } from '@/common/svg'
import { serverUrl } from '@/company'
import { ElMessage } from 'element-plus'
import { arrayToMapByKey } from '@/utils'
let numberingList: any = null
let relsList: any = null
let styleList: any = null
let imageUrl: any = null
let docInfo: any = {
author: {},
currentAuthor: '',
currentIndex: -1
}
const colorList = ['#d13438', '#0078d4', '#5c2e91', 'chocolate', 'aquamarine', 'lawngreen', 'hotpink', 'darkblue', 'darkslateblue', 'blueviolet', 'firebrick', 'coral', 'darkcyan', 'indigo', 'greenyellow', 'deeppink', 'indianred', 'blue', 'darkgray', 'darkmagenta', 'darkgreen', 'chartreuse', 'darksalmon', 'dimgray', 'crimson', 'darkolivegreen', 'gold', 'aqua', 'lightcoral', 'goldenrod', 'burlywood', 'green', 'darkkhaki', 'forestgreen', 'fushcia', 'darkorchid', 'deepskyblue', 'darkgoldenrod', 'cyan', 'cornflowerblue', 'brown', 'cadetblue', 'darkviolet', 'dodgerblue', 'darkred', 'gray', 'khaki', 'bisque', 'darkorange', 'darkslategray', 'lightblue', 'darkturquoise', 'darkseagreen']
let BlockType = ''
引入一些必要的組件和方法等,然后定義我們的插件,一定要繼承ckeditor5的Plugin:
export default class importFromWord extends Plugin {
}
然后首先在里面實現它的init方法,做一些初始化操作:
init() {
const editor = this.editor
editor.ui.componentFactory.add('importFromWord', () => {
const button = new ButtonView()
button.set({
label: '從word導入',
icon: DocumentWordProcessorReference,
tooltip: true
})
button.on('execute', () => {
this.input.click()
})
return button
})
}
this.editor就是我們之前使用create創建好的編輯器,通過editor.ui.componentFactory.add給工具欄添加一個按鈕,也就是我們要點擊導入word的按鈕。
示意圖
這里面用到了ckeditor5的ButtonView按鈕組件生成器,設置它的名稱和圖標,然后添加一個暴露出來的事件,當點擊按鈕的時候,觸發選擇文件彈窗,這個input是我自己寫的一個文件上傳輸入框。
接下來,我們去構造函數中做一些事情,當實例化這個組件的時候,初始化好我們需要的東西:
constructor(editor: any) {
super(editor)
this.editor = editor
this.input = document.createElement('input')
this.input.type = 'file'
this.input.style.opacity = 0
this.input.style.display = 'none'
this.input.addEventListener('change', (e: any) => {
const formData: any = new FormData()
formData.append("upload", this.input.files[0])
formData.Headers = {'Content-Type':'multipart/form-data'}
let ms = ElMessage({
message: "正在解析...",
type: "info",
})
postData({
service: "lc",
url: `file/word`,
data: formData,
}).then(res => {
ms.close()
if (res.data) {
ElMessage({
message: "上傳文件成功",
type: "success",
})
const { documentXML, numberingXML, relsXML, styleXML, imagePath } = res.data
numberingList = numberingXML
relsList = relsXML
styleList = styleXML
imageUrl = imagePath
markList(documentXML)
const html = listToHTML(documentXML)
const ckC = this.editor.ui.view?.editable?.element
const ckP = this.editor.ui.view?.stickyPanel?.element
if(ckC) {
let rt = ckC.parentNode.parentNode.parentNode
rt.style.setProperty('--content-top', docInfo.paddingTop + 'px')
rt.style.setProperty('--content-right', docInfo.paddingRight + 'px')
rt.style.setProperty('--content-bottom', docInfo.paddingBottom + 'px')
rt.style.setProperty('--content-left', docInfo.paddingLeft + 'px')
rt.style.setProperty('--content-width', docInfo.pageWidth - docInfo.paddingLeft - docInfo.paddingRight + 'px')
}
if(ckP) {
let rt = ckP.parentNode.parentNode.parentNode
rt.style.setProperty('--sticky-width', docInfo.pageWidth + 'px')
}
const div = document.createElement('div')
div.style.display = 'none'
div.innerHTML = html
splitList(div.firstElementChild)
insertDivToList(div)
document.body.appendChild(div)
document.body.removeChild(div)
this.editor.setData(div.innerHTML)
} else {
ElMessage({
message: "上傳文件失敗",
type: "error",
})
}
})
})
}
在這里我們主要做了幾件事:
首先第4行到第7行定義了一個文件選擇器。
然后給這個輸入框添加了一個事件。
第9行到第20行我們讀取到選擇的文件并上傳到服務器進行解析。
對返回回來的文檔數據,我們首先做一個標記,以方便我們接下來的操作:
function markList(list: any) {
let cache: any = []
list.forEach((item: any, index: number) => {
let isList = false
if(item.name === 'w:p') {
let pPr = findByName(item.elements, 'w:pPr')
if(pPr) {
let numPr = findByName(pPr.elements, 'w:numPr')
if(numPr) {
isList = true
let ilvl = numPr.elements[0].attributes['w:val']
let numId = numPr.elements[1].attributes['w:val']
let c = cache.at(-1)
numPr.level = ilvl
if(c) {
if(c.ilvl === ilvl && c.numId === numId) {
cache.pop()
}else if(c.ilvl === ilvl && c.numId !== numId) {
numPr.start = true
c.numPr.end = true
cache.pop()
}else if(c.ilvl < ilvl && c.numId === numId) {
numPr.start = true
cache.pop()
}else if(c.ilvl > ilvl && c.numId === numId) {
c.numPr.end = true
cache.pop()
}else if(c.numId !== numId) {
while(c.ilvl >= ilvl) {
c.numPr.end = true
c = cache.pop()
if(!c) {
break
}
}
}
}else {
numPr.start = true
}
cache.push({
ilvl,
numId,
index,
numPr
})
}
}
}
})
cache.forEach((c: any) => {
c.numPr.end = true
})
}
主要就是對列表進行標記,因為它要做一些特殊化的處理。
拿到數據之后,我們的核心邏輯都在第33行,實現listToHtml進行處理:
function listToHTML(list: any) {
let html = ''
list.forEach((item: any, index: number) => {
let info = getContainer(item)
html += info
})
return html
}
遍歷每一項,然后把它們生成的html拼接起來:
function getContainer(item: any) {
let html = ''
if(item.name === 'w:p') {
let n = findByName(item.elements, 'w:pPr')
let el: any = null
let pEl: any = null
let attr: any = {}
let style = null
if(n) {
let ps = findByName(n.elements, 'w:pStyle')
if(ps) {
let styleId = getAttributeVal(ps)
let sy = styleList.find((item: any) => {
return item.attributes['w:styleId'] === styleId
})
let ppr = findByName(sy.elements, 'w:pPr')
let rpr = findByName(sy.elements, 'w:rPr')
if(ppr) {
ppr.elements.forEach((p: any) => {
if(!findByName(n.elements, p.name)) {
n.elements.push(p)
}
})
}
if(rpr) {
let rs = findsByName(item.elements, 'w:r')
rs.forEach((r: any) => {
let rr = findByName(r.elements, 'w:rPr')
rpr.elements.forEach((p: any) => {
if(!findByName(rr.elements, p.name)) {
rr.elements.push(p)
}
})
})
}
}
let info = getPAttribute(n.elements)
attr = info.attr
style = info.style
if(attr.list) {
let s1: any = {}
let s2: any = {}
for(let t in info.style) {
if(t === 'list-style-type') {
s1[t] = info.style[t]
}else{
s2[t] = info.style[t]
}
}
for(let t in info.liStyle) {
s1[t] = info.liStyle[t]
}
if(attr.order) {
if(attr.start) {
if(attr.level !== '0') {
html += '<li style="list-style-type:none;">'
}
html += '<ol'
html += addStyle(s1)
html += '<li>'
html += '<p'
html += addStyle(s2)
}else {
html += '<li>'
html += '<p'
html += addStyle(s2)
}
}else{
if(attr.start) {
if(attr.level !== '0') {
html += '<li style="list-style-type:none;">'
}
html += '<ul'
html += addStyle(s1)
html += '<li>'
html += '<p'
html += addStyle(s2)
}else {
html += '<li>'
html += '<p'
html += addStyle(s2)
}
}
}else{
html += '<p'
html += addStyle(info.style)
}
}else{
el = document.createElement('p')
}
item.elements.forEach((r: any) => {
if(r.name === 'w:ins') {
setAuthor(r.attributes['w:author'])
r.elements.forEach((ins: any) => {
html += dealWr(ins, 'ins')
})
}else if(r.name === 'w:hyperlink') {
r.elements.forEach((hyp: any) => {
html += dealWr(hyp)
})
}else if(r.name === 'w:r') {
html += dealWr(r)
}else if(r.name === 'w:commentRangeStart') {
BlockType = 'comment'
}else if(r.name === 'w:commentRangeEnd') {
BlockType = ''
}else if(r.name === 'w:del') {
setAuthor(r.attributes['w:author'])
r.elements.forEach((hyp: any) => {
html += dealWr(hyp, 'del')
})
}
})
if(attr.list) {
if(attr.order) {
if(attr.end) {
html += '</p></li></ol>'
if(attr.level !== '0') {
html += '</li>'
}
}else {
html += '</p></li>'
}
}else{
if(attr.end) {
html += '</p></li></ul>'
if(attr.level !== '0') {
html += '</li>'
}
}else {
html += '</p></li>'
}
}
}else {
html += '</p>'
}
}else if(item.name === 'w:tbl') {
let n = findByName(item.elements, 'w:tblPr')
if(n) {
let info = getTableAttribute(n.elements)
html += '<figure class="table"'
html += addStyle(info.figureStyle)
html += '<table'
html += addStyle(info.tableStyle)
html += '<tbody>'
}
item.elements.forEach((r: any) => {
if(r.name === 'w:tr') {
html += dealWtr(r)
}
})
html += '</tbody></table></figure>'
}else if(item.name === 'w:sectPr') {
let ps = findByName(item.elements, 'w:pgSz')
let pm = findByName(item.elements, 'w:pgMar')
if(ps) {
docInfo.pageWidth = Math.ceil(ps.attributes['w:w'] / 20 * 96 / 72) + 1
}
if(pm) {
docInfo.paddingTop = pm.attributes['w:top'] / 1440 * 96
docInfo.paddingRight = pm.attributes['w:right'] / 1440 * 96
docInfo.paddingBottom = pm.attributes['w:bottom'] / 1440 * 96
docInfo.paddingLeft = pm.attributes['w:left'] / 1440 * 96
}
}
return html
}
做了一些邏輯判斷,和不同標簽的特殊處理。
在剛才input事件中的第34行到47行,主要是做一些編輯器大小等外觀設置,因為要配置成word中的寬度與邊距。
還需要考慮到,列表可能不是連續的,中間可能被一些段落所隔開,因此到這里還需要對生成的html中的列表進行分割,并修復索引問題:
function splitList(el: any) {
while(el) {
if(el.tagName === 'OL' || el.tagName === 'UL') {
let a = el.querySelectorAll('ol > p, ul > p')
let path: any = []
a.forEach((item: any) => {
let p: any = []
while(item) {
p.push(item)
item = item.parentNode
if(item === el) {
break
}
}
path.push(p.reverse())
})
let cur = el
let t: number = 0
path.forEach((p: any) => {
let list = cur.cloneNode(false)
let list2 = list
cur.parentNode.insertBefore(list, cur)
p.forEach((l: any, ind: number) => {
let chi = cur.children
let t = 0
for(let i = 0; i < chi.length; i++) {
if(chi[i] !== l) {
list.append(chi[i])
t++
i--
}else{
if(cur.tagName === 'OL') {
let s = cur.getAttribute('start')
cur.setAttribute('start', s ? (+s + t) : (t + 1))
}
if(ind === p.length - 1) {
let par = chi[i].parentNode
el.parentNode.insertBefore(chi[i], el)
if(par.children.length === 0) {
par.remove()
}
cur = el
}else{
cur.setAttribute('start', cur.getAttribute('start') - 1)
let cl = chi[i].cloneNode(false)
list.append(cl)
list = cl
cur = chi[i]
}
break
}
}
})
})
}
el = el.nextElementSibling
}
}
并且由于CKEditor5會對相鄰的列表進行合并等處理,這不是我們想要的,可以在它們中間插入一些div:
function insertDivToList(div: any) {
let f = div.firstElementChild
let k = f.nextElementSibling
while(k) {
if(f.tagName === 'UL' && k.tagName === 'UL') {
let d = document.createElement('div')
f = k
div.insertBefore(d, f)
k = f.nextElementSibling
}else if(f.tagName === 'OL' && k.tagName === 'OL') {
let d = document.createElement('p')
d.setAttribute('list-separator', "true")
f = k
div.insertBefore(d, f)
k = f.nextElementSibling
}else {
f = k
k = f.nextElementSibling
}
}
}
最后我們用this.editor.setData方法,將剛才生成的html設置到編輯器中去。
到此我們基本就已經把需要的功能實現了。
該來看一下我們所做的工作成果了,首先同樣導入CKEditor5官網中的文檔:
效果圖
可以看到,內容與格式等,基本跟原word一樣,與CKEditor5官網的示例也相同。然后我們再用另一個剛才修改過的文件測試一下:
效果圖
這個是用咱們剛才開發的插件導入的word的效果圖,幾乎與原word一模一樣,也沒有了CKEditor官網中的那幾個小問題。
至此,我們針對CKEditor5導入word的功能已經開發完畢,同時我又找了各種類型的word測試,均未發現問題,還原度都非常高。
感謝docx的規范,使得我們自己解析word成為可能,雖然不可能100%還原word的格式,但是能夠將它導入到我們的富文本編輯器中,以進行二次創作,這對我們來說是非常方便的。
本次word轉html,并導入富文本編輯器的開發過程,希望能給大家帶來啟發。
每一次創作都是快樂的,每一次分享也都是有益的,希望能夠幫助到你!
謝謝
近有一個業務是前端要上傳word格式的文稿,然后用戶上傳完之后,可以用瀏覽器直接查看該文稿,并且可以在富文本框直接引用該文稿,所以上傳word文稿之后,后端保存到db的必須是html格式才行,所以涉及到word格式轉html格式。
通過調查,這個word和html的處理,有兩種方案,方案1是前端做這個轉換。方案2是把word文檔上傳給后臺,后臺轉換好之后再返回給前端。至于方案1,看到大家的反饋都說很多問題,所以就沒采用前端轉的方案,最終決定是后端轉化為html格式并返回給前段預覽,待客戶預覽的時候,確認格式沒問題之后,再把html保存到后臺(因為word涉及到的格式太多,比如圖片,visio圖,表格,圖片等等之類的復雜元素,轉html的時候,可能會很多格式問題,所以要有個預覽的過程)。
對于word中普通的文字,問題倒不大,主要是文本之外的元素的處理,比如圖片,視頻,表格等。針對我本次的文章,只處理了圖片,處理的方式是:后臺從word中找出圖片(當然引入的jar包已經帶了獲取word中圖片的功能),上傳到服務器,拿到絕對路徑之后,放入到html里面,這樣,返回給前端的html內容,就可以直接預覽了。
maven引入相關依賴包如下:
<poi-scratchpad.version>3.14</poi-scratchpad.version>
<poi-ooxml.version>3.14</poi-ooxml.version>
<xdocreport.version>1.0.6</xdocreport.version>
<poi-ooxml-schemas.version>3.14</poi-ooxml-schemas.version>
<ooxml-schemas.version>1.3</ooxml-schemas.version>
<jsoup.version>1.11.3</jsoup.version>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>${poi-scratchpad.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi-ooxml.version}</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>xdocreport</artifactId>
<version>${xdocreport.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>${poi-ooxml-schemas.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>${ooxml-schemas.version}</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>
word轉html,對于word2003和word2007轉換方式不一樣,因為word2003和word2007的格式不一樣,工具類如下:
使用方法如下:
public String uploadSourceNews(MultipartFile file) {
String fileName = file.getOriginalFilename();
String suffixName = fileName.substring(fileName.lastIndexOf("."));
if (!".doc".equals(suffixName) && !".docx".equals(suffixName)) {
throw new UploadFileFormatException();
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");
String dateDir = formatter.format(LocalDate.now());
String directory = imageDir + "/" + dateDir + "/";
String content = null;
try {
InputStream inputStream = file.getInputStream();
if ("doc".equals(suffixName)) {
content = wordToHtmlUtil.Word2003ToHtml(inputStream, imageBucket, directory, Constants.HTTPS_PREFIX + imageVisitHost);
} else {
content = wordToHtmlUtil.Word2007ToHtml(inputStream, imageBucket, directory, Constants.HTTPS_PREFIX + imageVisitHost);
}
} catch (Exception ex) {
logger.error("word to html exception, detail:", ex);
return null;
}
return content;
}
關于doc和docx的一些存儲格式介紹:
docx 是微軟開發的基于 xml 的文字處理文件。docx 文件與 doc 文件不同, 因為 docx 文件將數據存儲在單獨的壓縮文件和文件夾中。早期版本的 microsoft office (早于 office 2007) 不支持 docx 文件, 因為 docx 是基于 xml 的, 早期版本將 doc 文件另存為單個二進制文件。
DOCX is an XML based word processing file developed by Microsoft. DOCX files are different than DOC files as DOCX files store data in separate compressed files and folders. Earlier versions of Microsoft Office (earlier than Office 2007) do not support DOCX files because DOCX is XML based where the earlier versions save DOC file as a single binary file.
可能你會問了,明明是docx結尾的文檔,怎么成了xml格式了?
很簡單:你隨便選擇一個docx文件,右鍵使用壓縮工具打開,就能得到一個這樣的目錄結構:
所以你以為docx是一個完整的文檔,其實它只是一個壓縮文件。
參考:
https://www.cnblogs.com/ct-csu/p/8178932.html
啥要做這個軟件
軟件獲取方式:私信“word”即可獲取
*請認真填寫需求信息,我們會在24小時內與您取得聯系。