Java實現(xiàn)在線編輯預(yù)覽office文檔詳解
1 在線編輯
1.1 PageOffice簡介
PageOffice是一款在線的office編輯軟件,幫助Web應(yīng)用系統(tǒng)或Web網(wǎng)站實現(xiàn)用戶在線編輯Word、Excel、PowerPoint文檔??梢酝昝缹崿F(xiàn)在線公文流轉(zhuǎn),領(lǐng)導批閱,蓋章??梢越o文件添加水印,在線安全預(yù)覽防止用戶下載和復制文件等
1.2 前端項目
由于pageoffice瀏覽器是ie內(nèi)核,vue3不兼容ie。所以需要把頁面放在后端
1.2.1 配置
在 vue.config.js 中配置代理
devServer: { proxy: { '/api': { target: 'http://localhost:8081/samples-springboot-back', //"/api"對應(yīng)后端項目"http://localhost:8081/samples-springboot-back"地址 ws: true, changeOrigin: true, // 允許跨域 pathRewrite: { '^/api': '' // 標識替換,使用 '/api' 代替真實的接口地址 } } } }
1.2.2 頁面部分
在index.html頁面引用后端項目(samples-springboot-back)根目錄下的pageoffice.js
<script type="text/javascript" src="http://localhost:8081/samples-springboot-back/pageoffice.js"></script>
在index.vue頁面添加一個按鈕,調(diào)用POBrowser.openWindowModeless請求后端。http://localhost:8081/springboot-pageoffice-demo/SimpleWord/Word2 是后端打開文件的controller
POBrowser.openWindowModeless('http://localhost:8081/springboot-pageoffice-demo/SimpleWord/Word2', 'width=1150px;height=900px;');
在Word.vue頁面created中通過axios請求后臺獲取pageoffice控件(注意:后臺返回string字符串,前端需要使用v-html解析)
這里給后臺發(fā)請求的是axios,如果需要添加token可以在main.js中配置攔截器給請求添加token
Word.vue頁面,可以直接復制后修改url
<template> <div class="Word"> <div style="height: 800px; width: auto" v-html="poHtmlCode" /> </div> </template> <script> const axios = require("axios"); export default { name: "Word", data() { return { poHtmlCode: "", }; }, created: function () { axios .post("/api/SimpleWord/Word") .then((response) => { this.poHtmlCode = response.data; }) .catch(function (err) { console.log(err); }); }, methods: { //控件中的一些常用方法都在這里調(diào)用,比如保存,打印等等 /** * Save()方法是/api/SimpleWord/Word這個后臺controller中PageOfficeCtrl控件通過poCtrl.addCustomToolButton定義的方法,除了保存還有另存到本地、打印等功能。 */ Save() { document.getElementById("PageOfficeCtrl1").WebSave(); } }, mounted: function () { // 將PageOffice控件中的方法通過mounted掛載到window對象上,只有掛載后才能被vue組件識別 window.Save = this.Save; }, }; </script>
1.3 后端項目
1.3.1 pom.xml
<dependency> <groupId>com.zhuozhengsoft</groupId> <artifactId>pageoffice</artifactId> <version>5.4.0.3</version> </dependency>
1.3.2 添加配置
在啟動類中配置servlet bean,poSysPath 是在 properites 中配置的磁盤路徑(注意:pageoffice的poserver.zz等這些請求不要攔截,get和post請求都放出來)
@Bean public ServletRegistrationBean pageofficeRegistrationBean() { com.zhuozhengsoft.pageoffice.poserver.Server poserver = new com.zhuozhengsoft.pageoffice.poserver.Server(); poserver.setSysPath(poSysPath);//設(shè)置PageOffice注冊成功后,license.lic文件存放的目錄 ServletRegistrationBean srb = new ServletRegistrationBean(poserver); srb.addUrlMappings("/poserver.zz"); srb.addUrlMappings("/posetup.exe"); srb.addUrlMappings("/pageoffice.js"); srb.addUrlMappings("/jquery.min.js"); srb.addUrlMappings("/pobstyle.css"); srb.addUrlMappings("/sealsetup.exe"); return srb; }
1.3.3 controller
打開文件的controller(webopen第一個參數(shù)是當前文件的磁盤路徑,磁盤路徑必須反向雙斜杠)。
setServerPage和setSaveFilePage中的api是前端代理,前后端分離項目必須配置代理
@RestController @RequestMapping(value = "/SimpleWord") public class SimpleWordController { @RequestMapping(value="/Word") public String showWord(HttpServletRequest request) { PageOfficeCtrl poCtrl = new PageOfficeCtrl(request); poCtrl.setServerPage("/api/poserver.zz");//設(shè)置服務(wù)頁面 poCtrl.addCustomToolButton("保存", "Save", 1); poCtrl.setSaveFilePage("/api/SimpleWord/save");//設(shè)置保存方法的url //打開word poCtrl.webOpen("D:\\doc\\test.docx", OpenModeType.docNormalEdit, "張三"); return poCtrl.getHtmlCode("PageOfficeCtrl1"); } @RequestMapping("save") public void save(HttpServletRequest request, HttpServletResponse response) { FileSaver fs = new FileSaver(request, response); fs.saveToFile("D:\\doc\\" + fs.getFileName()); fs.close(); } }
2 在線預(yù)覽
2.1 引言
最近遇到了文件預(yù)覽的需求,但一搜索發(fā)現(xiàn),這還不是一個簡單的功能。于是又去查詢了很多資料,調(diào)研了一些方案,也踩了好多坑。最后總結(jié)方案如下:
花錢解決(使用市面上現(xiàn)有的文件預(yù)覽服務(wù))
微軟,google,阿里云 IMM,XDOC,Office Web 365,wps開放平臺
前端方案
pptx的預(yù)覽方案,pdf的預(yù)覽方案,docx的預(yù)覽方案,xlsx(excel)的預(yù)覽方案
服務(wù)端方案
openOffice,kkFileView,onlyOffice
2.2 市面上現(xiàn)有的文件預(yù)覽服務(wù)
2.2.1 微軟
docx,pptx,xlsx可以說是office三件套,那自然得看一下微軟官方提供的文件預(yù)覽服務(wù)。使用方法特別簡單,只需要將文件鏈接,拼接到參數(shù)后面即可。
記得encodeURL
https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}
對于docx,pptx,xlsx都有較好的支持,pdf不行。
還有一個坑點是:這個服務(wù)是否穩(wěn)定,有什么限制,是否收費,都查不到一個定論。在office官方網(wǎng)站上甚至找不到介紹這個東西的地方。
目前只能找到一個Q&A:
微軟官方人員回答表示:
翻譯翻譯,就是:幾乎永久使用,沒有收費計劃,不會存儲預(yù)覽的文件數(shù)據(jù),限制文件10MB,建議用于 查看互聯(lián)網(wǎng)上公開的文件。
但經(jīng)過某些用戶測試發(fā)現(xiàn),使用了微軟的文件預(yù)覽服務(wù),然后刪除了文件地址,仍然可訪問,但過一段時間才會失效。
2.2.2 Google Drive查看器
接入簡單,同 Office Web Viewer,只需要把 src 改為https://drive.google.com/viewer?url=${encodeURIComponent(url)}
即可。
限制25MB,支持以下格式:
測試效果,支持docx,pptx,xlsx,pdf預(yù)覽,但pptx預(yù)覽的效果不如微軟,沒有動畫效果,樣式有小部分會錯亂。
2.2.3 阿里云 IMM
付費使用
2.2.4 XDOC 文檔預(yù)覽
說了一些大廠的,在介紹一些其他的,需要自行分辨
2.2.5 Office Web 365
需要注意的是,雖然名字很像office
,但我們看網(wǎng)頁的Copyright
可以發(fā)現(xiàn),其實是一個西安的公司,不是微軟,但畢竟也提供了文件預(yù)覽的服務(wù)
官網(wǎng)地址:www.officeweb365.com/
2.2.6 WPS開放平臺
官方地址:solution.wps.cn
付費使用,價格如下:
2.3 前端處理方案
2.3.1 pptx的預(yù)覽方案
先查一下有沒有現(xiàn)成的輪子,目前 pptx 的開源預(yù)覽方案能找到的只有這個:github.com/g21589/PPTX…[6] 。但已經(jīng)六七年沒有更新,也沒有維護,筆者使用的時候發(fā)現(xiàn)有很多兼容性問題。
簡單來說就是,沒有。
對于這種情況,我們可以自行解析,主要步驟如下:
- 查詢pptx的國際標準
- 解析pptx文件
- 渲染成html或者canvas進行展示
我們先去找一下pptx的國際標準
先解釋下什么是officeopenxml:
Office OpenXML,也稱為OpenXML或OOXML,是一種基于XML的辦公文檔格式,包括文字處理文檔、電子表格、演示文稿以及圖表、圖表、形狀和其他圖形材料。該規(guī)范由微軟開發(fā),并于2006年被ECMA國際采用為ECMA-376。第二個版本于2008年12月發(fā)布,第三個版本于2011年6月發(fā)布。該規(guī)范已被ISO和IEC采用為ISO/IEC 29500。
雖然Microsoft繼續(xù)支持較舊的二進制格式(.doc、.xls和.ppt),但OOXML現(xiàn)在是所有Microsoft Office文檔(.docx、.xlsx和.pptx)的默認格式。
由此可見,Office OpenXML由微軟開發(fā),目前已經(jīng)是國際標準。
接下來我們看一下pptx里面有哪些內(nèi)容,具體可以看pptx的官方標準:officeopenxml-pptx[8]
PresentationML或.pptx文件是一個zip文件,其中包含許多“部分”(通常是UTF-8或UTF-16編碼)或XML文件。該包還可能包含其他媒體文件,例如圖像。該結(jié)構(gòu)根據(jù) OOXML 標準 ECMA-376 第 2 部分中概述的開放打包約定進行組織。
根據(jù)國際標準,我們知道,pptx文件本質(zhì)就是一個zip文件,其中包含許多部分:
部件的數(shù)量和類型將根據(jù)演示文稿中的內(nèi)容而有所不同,但始終會有一個 [Content_Types].xml、一個或多個關(guān)系 (.rels) 部件和一個演示文稿部件(演示文稿.xml),它位于 ppt 文件夾中,用于Microsoft Powerpoint 文件。通常,還將至少有一個幻燈片部件,以及一張母版幻燈片和一張版式幻燈片,從中形成幻燈片。
那么js如何讀取zip呢?
找到一個工具: www.npmjs.com
于是我們可以開始嘗試解析pptx了。
import JSZip from 'jszip' // 加載pptx數(shù)據(jù) const zip = await JSZip.loadAsync(pptxData)
解析[Content_Types].xml
每個pptx必然會有一個 [Content_Types].xml。此文件包含包中部件的所有內(nèi)容類型的列表。每個部件及其類型都必須列在 [Content_Types].xml 中。通過它里面的內(nèi)容,可以解析其他的文件數(shù)據(jù)
const filesInfo = await getContentTypes(zip) ???????async function getContentTypes(zip: JSZip) { const ContentTypesJson = await readXmlFile(zip, '[Content_Types].xml') const subObj = ContentTypesJson['Types']['Override'] const slidesLocArray = [] const slideLayoutsLocArray = [] for (let i = 0; i < subObj.length; i++) { switch (subObj[i]['attrs']['ContentType']) { case 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml': slidesLocArray.push(subObj[i]['attrs']['PartName'].substr(1)) break case 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml': slideLayoutsLocArray.push(subObj[i]['attrs']['PartName'].substr(1)) break default: } } return { slides: slidesLocArray, slideLayouts: slideLayoutsLocArray, } }
解析演示文稿
先獲取ppt目錄下的presentation.xml演示文稿的大小
由于演示文稿是xml格式,要真正的讀取內(nèi)容需要執(zhí)行 readXmlFile
const slideSize = await getSlideSize(zip) async function getSlideSize(zip: JSZip) { const content = await readXmlFile(zip, 'ppt/presentation.xml') const sldSzAttrs = content['p:presentation']['p:sldSz']['attrs'] return { width: (parseInt(sldSzAttrs['cx']) * 96) / 914400, height: (parseInt(sldSzAttrs['cy']) * 96) / 914400, } }
加載主題
根據(jù) officeopenxml的標準解釋
每個包都包含一個關(guān)系部件,用于定義其他部件之間的關(guān)系以及與包外部資源的關(guān)系。這樣可以將關(guān)系與內(nèi)容分開,并且可以輕松地更改關(guān)系,而無需更改引用目標的源。
除了包的關(guān)系部分之外,作為一個或多個關(guān)系源的每個部件都有自己的關(guān)系部分。每個這樣的關(guān)系部件都可以在部件的_rels子文件夾中找到,并通過在部件名稱后附加“.rels”來命名。
其中主題的相關(guān)信息就在ppt/_rels/presentation.xml.rels中
async function loadTheme(zip: JSZip) { const preResContent = await readXmlFile( zip, 'ppt/_rels/presentation.xml.rels', ) const relationshipArray = preResContent['Relationships']['Relationship'] let themeURI if (relationshipArray.constructor === Array) { for (let i = 0; i < relationshipArray.length; i++) { if ( relationshipArray[i]['attrs']['Type'] === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme' ) { themeURI = relationshipArray[i]['attrs']['Target'] break } } } else if ( relationshipArray['attrs']['Type'] === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme' ) { themeURI = relationshipArray['attrs']['Target'] } if (themeURI === undefined) { throw Error("Can't open theme file.") } ??????? return readXmlFile(zip, 'ppt/' + themeURI) }
2.3.2 pdf的預(yù)覽方案
2.3.2.1 iframe和embed
pdf 比較特別,一般的瀏覽器默認支持預(yù)覽pdf。因此,我們可以使用瀏覽器的能力:<iframe src="viewFileUrl" />
但這樣就完全依賴瀏覽器,對PDF的展示,交互,是否支持全看瀏覽器的能力,且不同的瀏覽器展示和交互往往不同,如果需要統(tǒng)一的話,最好還是嘗試其他方案。
embed的解析方式也是一樣,這里不舉例子了
2.3.2.2 pdfjs
由mozilla出品,就是我們常見的MDN的老大。而且目前 火狐瀏覽器 使用的 PDF 預(yù)覽就是采用這個,我們可以用火狐瀏覽器打開pdf文件,查看瀏覽器使用的js就能發(fā)現(xiàn)
需要注意的是,最新版 pdf.js 限制了 node 版本,需要大于等于18
如果你項目node版本小于這個情況,可能會無法使用。
具體使用情況如下:
import * as pdfjs from 'pdfjs-dist' import * as pdfjsWorker from 'pdfjs-dist/build/pdf.work.entry' interface Viewport { width: number height: number viewBox: Array<number> } interface RenderContext { canvasContext: CanvasRenderingContext2D | null transform: Array<number> viewport: Viewport } interface PDFPageProxy { pageNumber: number getViewport: () => Viewport render: (options: RenderContext) => void } interface PDFDocumentProxy { numPages: number getPage: (x: number) => Promise<PDFPageProxy> } class PdfPreview { private pdfDoc: PDFDocumentProxy | undefined pageNumber: number total: number dom: HTMLElement pdf: string | ArrayBuffer constructor(pdf: string | ArrayBuffer, dom: HTMLElement | undefined) { this.pageNumber = 1 this.total = 0 this.pdfDoc = undefined this.pdf = pdf this.dom = dom ? dom : document.body } private getPdfPage = (number: number) => { return new Promise((resolve, reject) => { if (this.pdfDoc) { this.pdfDoc.getPage(number).then((page: PDFPageProxy) => { const viewport = page.getViewport() const canvas = document.createElement('canvas') this.dom.appendChild(canvas) const context = canvas.getContext('2d') const [_, __, width, height] = viewport.viewBox canvas.width = width canvas.height = height viewport.width = width viewport.height = height canvas.style.width = Math.floor(viewport.width) + 'px' canvas.style.height = Math.floor(viewport.height) + 'px' const renderContext = { canvasContext: context, viewport: viewport, transform: [1, 0, 0, -1, 0, viewport.height], } page.render(renderContext) resolve({ success: true, data: page }) }) } else { reject({ success: false, data: null, message: 'pdfDoc is undefined' }) } }) } pdfPreview = () => { window.pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker window.pdfjsLib .getDocument(this.pdf) .promise.then(async (doc: PDFDocumentProxy) => { this.pdfDoc = doc this.total = doc.numPages for (let i = 1; i <= this.total; i++) { await this.getPdfPage(i) } }) } prevPage = () => { if (this.pageNumber > 1) { this.pageNumber -= 1 } else { this.pageNumber = 1 } this.getPdfPage(this.pageNumber) } nextPage = () => { if (this.pageNumber < this.total) { this.pageNumber += 1 } else { this.pageNumber = this.total } this.getPdfPage(this.pageNumber) } } const createReader = (file: File): Promise<string | ArrayBuffer | null> => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.readAsDataURL(file) reader.onload = () => { resolve(reader.result) } reader.onerror = (error) => { reject(error) } reader.onabort = (abort) => { reject(abort) } }) } export const renderPdf = async ( file: File, dom?: HTMLElement, ): Promise<void> => { try { if (typeof window !== 'undefined') { const pdf = await createReader(file) if (pdf) { const PDF = new PdfPreview(pdf, dom) PDF.pdfPreview() } } } catch (error) { console.log('renderPdf', error) } }
2.3.3 docx的預(yù)覽方案
我們可以去查看docx的國際標準,去解析文件格式,渲染成html和canvas,不過比較好的是,已經(jīng)有人這么做了,還開源了
使用方法如下:
import { renderAsync } from 'docx-preview' interface DocxOptions { bodyContainer?: HTMLElement | null styleContainer?: HTMLElement buffer: Blob docxOptions?: Partial<Record<string, string | boolean>> } export const renderDocx = (options: DocxOptions): Promise<void> | undefined => { if (typeof window !== 'undefined') { const { bodyContainer, styleContainer, buffer, docxOptions = {} } = options const defaultOptions = { className: 'docx', ignoreLastRenderedPageBreak: false, } const configuration = Object.assign({}, defaultOptions, docxOptions) if (bodyContainer) { return renderAsync(buffer, bodyContainer, styleContainer, configuration) } else { const contain = document.createElement('div') document.body.appendChild(contain) return renderAsync(buffer, contain, styleContainer, configuration) } } }
2.3.4 前端預(yù)覽方案總結(jié)
我們對以上找到的優(yōu)秀的解決方案,進行改進和總結(jié),并封裝成一個web components組件:preview組件
為什么是web components組件?
因為它跟框架無關(guān),可以在任何框架中使用,且使用起來跟原生的div標簽一樣方便。并編寫使用文檔: preview組件文檔, 文檔支持交互體驗。
目前docx,pdf,xlsx預(yù)覽基本可以了,都是最好的方案。pptx預(yù)覽效果不太好,因為需要自行解析。
2.4 服務(wù)端預(yù)覽方案
2.4.1 openOffice
由于瀏覽器不能直接打開 docx,pptx,xlsx 等格式文件,但可以直接打開pdf和圖片,因此,我們可以換一個思路,用服務(wù)端去轉(zhuǎn)換下文件的格式,轉(zhuǎn)換成瀏覽器能識別的格式,然后再讓瀏覽器打開,這不就OK了嗎,甚至不需要前端處理了。
我們可以借助openOffice的能力,先介紹一下openOffice:
Apache OpenOffice是領(lǐng)先的開源辦公軟件套件,用于文字處理,電子表格,演示文稿,圖形,數(shù)據(jù)庫等。它有多種語言版本,適用于所有常用計算機。它以國際開放標準格式存儲您的所有數(shù)據(jù),還可以從其他常見的辦公軟件包中讀取和寫入文件。它可以出于任何目的完全免費下載和使用。
官網(wǎng)如下:www.openoffice.org
完整示例如下:
package org.example; import org.artofsolving.jodconverter.OfficeDocumentConverter; import org.artofsolving.jodconverter.office.DefaultOfficeManagerConfiguration; import org.artofsolving.jodconverter.office.OfficeManager; import java.io.File; public class OfficeUtil { private static OfficeManager officeManager; private static int port[] = {8100}; /** * start openOffice service. */ public static void startService() { DefaultOfficeManagerConfiguration configuration = new DefaultOfficeManagerConfiguration(); try { System.out.println("準備啟動office轉(zhuǎn)換服務(wù)...."); configuration.setOfficeHome("這里的路徑一般為C:\\Program Files (x86)\\OpenOffice 4的bin目錄"); configuration.setPortNumbers(port); // 設(shè)置轉(zhuǎn)換端口,默認為8100 configuration.setTaskExecutionTimeout(1000 * 60 * 30L);// 設(shè)置任務(wù)執(zhí)行超時為30分鐘 configuration.setTaskQueueTimeout(1000 * 60 * 60 * 24L);// 設(shè)置任務(wù)隊列超時為24小時 officeManager = configuration.buildOfficeManager(); officeManager.start(); // 啟動服務(wù) System.out.println("office轉(zhuǎn)換服務(wù)啟動成功!"); } catch (Exception e) { System.out.println("office轉(zhuǎn)換服務(wù)啟動失敗!詳細信息:" + e); } } /** * stop openOffice service. */ public static void stopService() { System.out.println("準備關(guān)閉office轉(zhuǎn)換服務(wù)...."); if (officeManager != null) { officeManager.stop(); } System.out.println("office轉(zhuǎn)換服務(wù)關(guān)閉成功!"); } public static void convertToPDF(String inputFile, String outputFile) { startService(); System.out.println("進行文檔轉(zhuǎn)換轉(zhuǎn)換:" + inputFile + " --> " + outputFile); OfficeDocumentConverter converter = new OfficeDocumentConverter(officeManager); converter.convert(new File(inputFile), new File(outputFile)); stopService(); } public static void main(String[] args) { convertToPDF("/Users/koolearn/Desktop/asdf.docx", "/Users/koolearn/Desktop/adsf.pdf"); } }
2.4.2 kkFileView
支持的文件預(yù)覽格式非常豐富圖片
安裝下libreoffice :
kkFileView明確要求的額外依賴 libreoffice,否則無法啟動
啟動項目
找到主文件,主函數(shù)mian,即可執(zhí)行
2.4.3 onlyOffice
官網(wǎng)地址:https://www.onlyoffice.com/zh
開發(fā)者版本和社區(qū)版免費,企業(yè)版付費:www.onlyoffice.com/zh/docs-ent
預(yù)覽的文件種類沒有kkFileView多,但對office三件套有很好的支持,甚至支持多人編輯。
以上就是Java實現(xiàn)在線編輯預(yù)覽office文檔詳解的詳細內(nèi)容,更多關(guān)于Java在線預(yù)覽office的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Cloud Config實現(xiàn)分布式配置中心
這篇文章主要介紹了Spring Cloud Config實現(xiàn)分布式配置中心,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04MyBatisPlus中使用or()和and()遇到的問題及細節(jié)處理
這篇文章主要介紹了MyBatisPlus中使用or()和and()遇到的問題,本文通過多種寫法實例代碼相結(jié)合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08springboot+mybaties項目中掃描不到@mapper注解的解決方法
本文主要介紹了springboot+mybaties項目中掃描不到@mapper注解的解決方法,該報錯表明掃描不到Mapper層,具有一定的參考價值,感興趣的可以了解一下2024-05-05java實現(xiàn)簡單的學生信息管理系統(tǒng)代碼實例
這篇文章主要介紹了java實現(xiàn)簡單的學生信息管理系統(tǒng),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-04-04java自帶命令行工具jmap、jhat與jinfo的使用實例代碼詳解
本篇文章主要通過代碼實例對java自帶命令行工具jmap、jhat與jinfo的使用做出了詳解,需要的朋友可以參考下2017-04-04