基于golang編寫一個(gè)word/excel/ppt轉(zhuǎn)pdf的工具
需求
公司客戶有需求,需要轉(zhuǎn)換doc文件為pdf文件,并且保持格式完全不變。
工程師用各種Java類庫(kù),無(wú)論是doc4j、POI還是Aspose.Doc、Libreoffice組件還是各種線上API服務(wù),轉(zhuǎn)換結(jié)果都不甚滿意。
于是我這邊接手這個(gè)活了。
調(diào)研
其實(shí),最符合客戶需求的莫過于原生Windows Office Word的導(dǎo)出功能了。
需要能夠操作Windows的Office Word程序,那么需要能夠直接訪問其系統(tǒng)組件,需要類似COM/OLE系統(tǒng)庫(kù),說干就干。
1、運(yùn)維做弄了一個(gè)配置比較低的EC2機(jī)器,windows10系統(tǒng)。
2、我這邊找了一些庫(kù),python的comtypes.client,但是有點(diǎn)問題,單跑沒問題,做成服務(wù),在web線程中做這個(gè)事情,就有問題,具體找了下,應(yīng)該還是線程問題,想了想,不做了(因?yàn)楸旧砭筒幌胗胮ython寫, )
3、趕緊找了下golang中對(duì)應(yīng)的OLE庫(kù),找到了一個(gè),看了下文檔,直接寫了出來(lái)。
實(shí)現(xiàn)
話不多說,直接上核心代碼看看:
下面是基礎(chǔ)的解析過程,其實(shí)就是模擬以下四個(gè)步驟:
1、打開Office對(duì)應(yīng)的程序(Word/Excel/PPT)
2、導(dǎo)出為PDF文件
3、關(guān)閉文件
4、退出Office程序
基礎(chǔ)邏輯
package office import ( ole "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" log "github.com/sirupsen/logrus" ) /// 更多內(nèi)容請(qǐng)參考官方COM文檔 https://docs.microsoft.com/zh-cn/office/vba/api/word.application type Operation struct { OpType string Arguments []interface{} } /// 部分應(yīng)用不允許隱藏 ,比如ppt,所以Visible需要設(shè)定下 type ConvertHandler struct { FileInPath string FileOutPath string ApplicationName string WorkspaceName string Visible bool DisplayAlerts int OpenFileOp Operation ExportOp Operation CloseOp Operation QuitOp Operation } type DomConvertObject struct { Application *ole.IDispatch Workspace *ole.IDispatch SingleFile *ole.IDispatch } func (handler ConvertHandler) Convert() { ole.CoInitialize(0) defer ole.CoUninitialize() log.Println("handle open start") dom := handler.Open() log.Println("handle open end") log.Println("handler in file path is " + handler.FileInPath) log.Println("handler out file path is " + handler.FileOutPath) defer dom.Application.Release() defer dom.Workspace.Release() defer dom.SingleFile.Release() handler.Export(dom) log.Println("handle export end") handler.Close(dom) log.Println("handle close end") handler.Quit(dom) log.Println("handle quit end") } func (handler ConvertHandler) Open() DomConvertObject { var dom DomConvertObject unknown, err := oleutil.CreateObject(handler.ApplicationName) if err != nil { panic(err) } dom.Application = unknown.MustQueryInterface(ole.IID_IDispatch) oleutil.MustPutProperty(dom.Application, "Visible", handler.Visible) oleutil.MustPutProperty(dom.Application, "DisplayAlerts", handler.DisplayAlerts) dom.Workspace = oleutil.MustGetProperty(dom.Application, handler.WorkspaceName).ToIDispatch() dom.SingleFile = oleutil.MustCallMethod(dom.Workspace, handler.OpenFileOp.OpType, handler.OpenFileOp.Arguments...).ToIDispatch() return dom } func (handler ConvertHandler) Export(dom DomConvertObject) { oleutil.MustCallMethod(dom.SingleFile, handler.ExportOp.OpType, handler.ExportOp.Arguments...) } func (handler ConvertHandler) Close(dom DomConvertObject) { if handler.ApplicationName == "PowerPoint.Application" { oleutil.MustCallMethod(dom.SingleFile, handler.CloseOp.OpType, handler.CloseOp.Arguments...) } else { oleutil.MustCallMethod(dom.Workspace, handler.CloseOp.OpType, handler.CloseOp.Arguments...) } } func (handler ConvertHandler) Quit(dom DomConvertObject) { oleutil.MustCallMethod(dom.Application, handler.QuitOp.OpType, handler.QuitOp.Arguments...)
不同格式的適配
支持Word/Excel/PPT轉(zhuǎn)pdf,下面是Word轉(zhuǎn)pdf的代碼:
package office func ConvertDoc2Pdf(fileInputPath string, fileOutputPath string) { openArgs := []interface{}{fileInputPath} /// https://docs.microsoft.com/zh-cn/office/vba/api/word.document.exportasfixedformat exportArgs := []interface{}{fileOutputPath, 17} closeArgs := []interface{}{} quitArgs := []interface{}{} convertHandler := ConvertHandler{ FileInPath: fileInputPath, FileOutPath: fileOutputPath, ApplicationName: "Word.Application", WorkspaceName: "Documents", Visible: false, DisplayAlerts: 0, OpenFileOp: Operation{ OpType: "Open", Arguments: openArgs, }, ExportOp: Operation{ OpType: "ExportAsFixedFormat", Arguments: exportArgs, }, CloseOp: Operation{ OpType: "Close", Arguments: closeArgs, }, QuitOp: Operation{ OpType: "Quit", Arguments: quitArgs, }, } convertHandler.Convert() }
提供web service接口
package web import ( "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "office-convert/office" "os" "path" "path/filepath" "runtime/debug" "strconv" log "github.com/sirupsen/logrus" ) const PORT = 10000 const SAVED_DIR = "files" type ConvertRequestInfo struct { FileInUrl string `json:"file_in_url"` SourceType string `json:"source_type"` TargetType string `json:"target_type"` } func logStackTrace(err ...interface{}) { log.Println(err) stack := string(debug.Stack()) log.Println(stack) } func convertHandler(w http.ResponseWriter, r *http.Request) { defer func() { if r := recover(); r != nil { w.WriteHeader(503) fmt.Fprintln(w, r) logStackTrace(r) } }() if r.Method != "POST" { w.WriteHeader(400) fmt.Fprintf(w, "Method not support") return } var convertRequestInfo ConvertRequestInfo reqBody, err := ioutil.ReadAll(r.Body) if err != nil { log.Println(err) } json.Unmarshal(reqBody, &convertRequestInfo) log.Println(convertRequestInfo) log.Println(convertRequestInfo.FileInUrl) downloadFile(convertRequestInfo.FileInUrl) fileOutAbsPath := getFileOutAbsPath(convertRequestInfo.FileInUrl, convertRequestInfo.TargetType) convert(convertRequestInfo) w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/octet-stream") //文件過大的話考慮使用io.Copy進(jìn)行流式拷貝 outFileBytes, err := ioutil.ReadFile(fileOutAbsPath) if err != nil { panic(err) } w.Write(outFileBytes) } func convert(convertRequestInfo ConvertRequestInfo) { fileOutAbsPath := getFileOutAbsPath(convertRequestInfo.FileInUrl, convertRequestInfo.TargetType) switch convertRequestInfo.SourceType { case "doc", "docx": office.ConvertDoc2Pdf(getFileInAbsPath(convertRequestInfo.FileInUrl), fileOutAbsPath) break case "xls", "xlsx": office.ConvertXsl2Pdf(getFileInAbsPath(convertRequestInfo.FileInUrl), fileOutAbsPath) break case "ppt", "pptx": office.ConvertPPT2Pdf(getFileInAbsPath(convertRequestInfo.FileInUrl), fileOutAbsPath) break } } func getNameFromUrl(inputUrl string) string { u, err := url.Parse(inputUrl) if err != nil { panic(err) } return path.Base(u.Path) } func getCurrentWorkDirectory() string { cwd, err := os.Getwd() if err != nil { panic(err) } return cwd } func getFileInAbsPath(url string) string { fileName := getNameFromUrl(url) currentWorkDirectory := getCurrentWorkDirectory() absPath := filepath.Join(currentWorkDirectory, SAVED_DIR, fileName) return absPath } func getFileOutAbsPath(fileInUrl string, targetType string) string { return getFileInAbsPath(fileInUrl) + "." + targetType } func downloadFile(url string) { log.Println("Start download file url :", url) resp, err := http.Get(url) if err != nil { panic(err) } defer resp.Body.Close() fileInAbsPath := getFileInAbsPath(url) dir := filepath.Dir(fileInAbsPath) // log.Println("dir is " + dir) if _, err := os.Stat(dir); os.IsNotExist(err) { log.Println("dir is not exists") os.MkdirAll(dir, 0644) } out, err := os.Create(fileInAbsPath) log.Println("save file to " + fileInAbsPath) if err != nil { panic(err) } defer out.Close() _, err = io.Copy(out, resp.Body) if err != nil { panic(err) } log.Println("Download file end url :", url) } func StartServer() { log.Println("start service ...") http.HandleFunc("/convert", convertHandler) http.ListenAndServe("127.0.0.1:"+strconv.Itoa(PORT), nil) }
部署/使用
編譯 (可跳過)
如果要編譯源碼,得到exe文件,可以執(zhí)行命令go build -ldflags "-H windowsgui" 生成 office-convert.exe 。不想編譯的話,可以在prebuilt下找到對(duì)應(yīng)exe文件。
運(yùn)行
方法一:普通運(yùn)行
雙擊執(zhí)行 office-convert.exe 即可,但是如果程序報(bào)錯(cuò),或者電腦異常關(guān)機(jī),不會(huì)重啟
方法二:后臺(tái)運(yùn)行(定時(shí)任務(wù)啟動(dòng),可以自動(dòng)恢復(fù))
windows要做到定時(shí)啟動(dòng)/自動(dòng)恢復(fù),還挺麻煩的。。。
1、復(fù)制文件
將prebuilt下兩個(gè)文件復(fù)制到 C:\Users\Administrator\OfficeConvert\ 目錄下
2、修改COM訪問權(quán)限
當(dāng)我們以服務(wù)、定時(shí)任務(wù)啟動(dòng)程序的時(shí)候,會(huì)報(bào)錯(cuò),提示空指針錯(cuò)誤。
原因就是微軟限制了COM組件在非UI Session的情況下使用(防止惡意病毒之類),如果要允許,需要做如下處理:
參考這里
- Open Component Services (Start -> Run, type in dcomcnfg)
- Drill down to Component Services -> Computers -> My Computer and click on DCOM Config
- Right-click on Microsoft Excel Application and choose Properties
- In the Identity tab select This User and enter the ID and password of an interactive user account (domain or local) and click Ok
注意,上圖是演示,賬號(hào)密碼填寫該機(jī)器的Administrator賬號(hào)密碼
3、定時(shí)任務(wù)
創(chuàng)建windows定時(shí)任務(wù),每1分鐘調(diào)用check_start.bat文件,該文件自動(dòng)檢查office-convert.exe是否運(yùn)行,沒有就啟動(dòng)。
注意: 上圖只是演示,具體位置填寫 C:\Users\Administrator\OfficeConvert\check_start.bat
Web部署
使用nginx作為反向代理,具體位置在 C:\Users\Administrator\nginx-1.20.2\nginx-1.20.2下,修改conf/nginx.conf文件,代理127.0.0.1:10000即可,
有公網(wǎng)IP(比如xxx.com)的話,配置DNS解析convert-tools.xxx.com到此機(jī)器ip。
server { listen 80; server_name convert-tools.xxx.net; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; proxy_pass http://127.0.0.1:10000; } # ...其他設(shè)置 }
請(qǐng)求
已部署到Windows機(jī)器,訪問URL:http://127.0.0.1:10000 (如果上面配置了域名,則訪問 http://convert-tools.xxx.com/convert)
請(qǐng)求相關(guān)
Method : POST
Content-Type: application/json
Body:
{ "file_in_url":"https://your_docx_file_url", "source_type":"docx", "target_type":"pdf" }
參數(shù) | 是否必須 | 取值范圍 | 說明 |
---|---|---|---|
file_in_url | 是 | 滿足下面source_type的各類文檔url | 待轉(zhuǎn)換的文檔的網(wǎng)絡(luò)連接 |
source_type | 是 | [doc,docx,xls,xlsx,ppt,pptx] | 文檔類型 |
target_type | 是 | 暫時(shí)只支持PDF,后續(xù)會(huì)支持更多 |
響應(yīng)
根據(jù)HTTP狀態(tài)碼做判斷
200 : ok
其他: 有錯(cuò)
Body:
轉(zhuǎn)換的文件的二進(jìn)制流
如果status_code非200,是對(duì)應(yīng)的報(bào)錯(cuò)信息
到此這篇關(guān)于基于golang編寫一個(gè)word/excel/ppt轉(zhuǎn)pdf的工具的文章就介紹到這了,更多相關(guān)go word/excel/ppt轉(zhuǎn)pdf內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
簡(jiǎn)單聊聊Golang中defer預(yù)計(jì)算參數(shù)
在golang當(dāng)中defer代碼塊會(huì)在函數(shù)調(diào)用鏈表中增加一個(gè)函數(shù)調(diào)用,下面這篇文章主要給大家介紹了關(guān)于Golang中defer預(yù)計(jì)算參數(shù)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03使用Golang的gomail庫(kù)實(shí)現(xiàn)郵件發(fā)送功能
本篇博客詳細(xì)介紹了如何使用Golang語(yǔ)言中的gomail庫(kù)來(lái)實(shí)現(xiàn)郵件發(fā)送的功能,首先,需要準(zhǔn)備工作,包括安裝Golang環(huán)境、gomail庫(kù),以及申請(qǐng)126郵箱的SMTP服務(wù)和獲取授權(quán)碼,其次,介紹了在config文件中配置SMTP服務(wù)器信息的步驟2024-10-10go語(yǔ)言版的ip2long函數(shù)實(shí)例
這篇文章主要介紹了go語(yǔ)言版的ip2long函數(shù),實(shí)例分析了Go語(yǔ)言實(shí)現(xiàn)的ip2long函數(shù)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02Go語(yǔ)言實(shí)現(xiàn)的簡(jiǎn)單網(wǎng)絡(luò)端口掃描方法
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)的簡(jiǎn)單網(wǎng)絡(luò)端口掃描方法,實(shí)例分析了Go語(yǔ)言網(wǎng)絡(luò)程序的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02Air實(shí)現(xiàn)Go程序?qū)崟r(shí)熱重載使用過程解析示例
這篇文章主要為大家介紹了Air實(shí)現(xiàn)Go程序?qū)崟r(shí)熱重載使用過程解析示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04golang?run時(shí)報(bào)undefined錯(cuò)誤的解決
這篇文章主要介紹了golang?run時(shí)報(bào)undefined錯(cuò)誤的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03