基于golang編寫一個word/excel/ppt轉(zhuǎn)pdf的工具
需求
公司客戶有需求,需要轉(zhuǎn)換doc文件為pdf文件,并且保持格式完全不變。
工程師用各種Java類庫,無論是doc4j、POI還是Aspose.Doc、Libreoffice組件還是各種線上API服務(wù),轉(zhuǎn)換結(jié)果都不甚滿意。
于是我這邊接手這個活了。
調(diào)研
其實,最符合客戶需求的莫過于原生Windows Office Word的導(dǎo)出功能了。
需要能夠操作Windows的Office Word程序,那么需要能夠直接訪問其系統(tǒng)組件,需要類似COM/OLE系統(tǒng)庫,說干就干。
1、運維做弄了一個配置比較低的EC2機器,windows10系統(tǒng)。
2、我這邊找了一些庫,python的comtypes.client,但是有點問題,單跑沒問題,做成服務(wù),在web線程中做這個事情,就有問題,具體找了下,應(yīng)該還是線程問題,想了想,不做了(因為本身就不想用python寫, )
3、趕緊找了下golang中對應(yīng)的OLE庫,找到了一個,看了下文檔,直接寫了出來。
實現(xiàn)
話不多說,直接上核心代碼看看:
下面是基礎(chǔ)的解析過程,其實就是模擬以下四個步驟:
1、打開Office對應(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)容請參考官方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進行流式拷貝 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下找到對應(yīng)exe文件。
運行
方法一:普通運行
雙擊執(zhí)行 office-convert.exe 即可,但是如果程序報錯,或者電腦異常關(guān)機,不會重啟
方法二:后臺運行(定時任務(wù)啟動,可以自動恢復(fù))
windows要做到定時啟動/自動恢復(fù),還挺麻煩的。。。
1、復(fù)制文件
將prebuilt下兩個文件復(fù)制到 C:\Users\Administrator\OfficeConvert\ 目錄下
2、修改COM訪問權(quán)限
當我們以服務(wù)、定時任務(wù)啟動程序的時候,會報錯,提示空指針錯誤。
原因就是微軟限制了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
注意,上圖是演示,賬號密碼填寫該機器的Administrator賬號密碼
3、定時任務(wù)
創(chuàng)建windows定時任務(wù),每1分鐘調(diào)用check_start.bat文件,該文件自動檢查office-convert.exe是否運行,沒有就啟動。
注意: 上圖只是演示,具體位置填寫 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到此機器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è)置 }
請求
已部署到Windows機器,訪問URL:http://127.0.0.1:10000 (如果上面配置了域名,則訪問 http://convert-tools.xxx.com/convert)
請求相關(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 | 是 | 暫時只支持PDF,后續(xù)會支持更多 |
響應(yīng)
根據(jù)HTTP狀態(tài)碼做判斷
200 : ok
其他: 有錯
Body:
轉(zhuǎn)換的文件的二進制流
如果status_code非200,是對應(yīng)的報錯信息
到此這篇關(guān)于基于golang編寫一個word/excel/ppt轉(zhuǎn)pdf的工具的文章就介紹到這了,更多相關(guān)go word/excel/ppt轉(zhuǎn)pdf內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
簡單聊聊Golang中defer預(yù)計算參數(shù)
在golang當中defer代碼塊會在函數(shù)調(diào)用鏈表中增加一個函數(shù)調(diào)用,下面這篇文章主要給大家介紹了關(guān)于Golang中defer預(yù)計算參數(shù)的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-03-03使用Golang的gomail庫實現(xiàn)郵件發(fā)送功能
本篇博客詳細介紹了如何使用Golang語言中的gomail庫來實現(xiàn)郵件發(fā)送的功能,首先,需要準備工作,包括安裝Golang環(huán)境、gomail庫,以及申請126郵箱的SMTP服務(wù)和獲取授權(quán)碼,其次,介紹了在config文件中配置SMTP服務(wù)器信息的步驟2024-10-10Go語言實現(xiàn)的簡單網(wǎng)絡(luò)端口掃描方法
這篇文章主要介紹了Go語言實現(xiàn)的簡單網(wǎng)絡(luò)端口掃描方法,實例分析了Go語言網(wǎng)絡(luò)程序的實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-02-02Air實現(xiàn)Go程序?qū)崟r熱重載使用過程解析示例
這篇文章主要為大家介紹了Air實現(xiàn)Go程序?qū)崟r熱重載使用過程解析示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪2022-04-04