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

