需求
公司客户有需求,需要转换doc文件为pdf文件,并且保持格式完全不变。
工程师用各种java类库,无论是doc4j、poi还是aspose.doc、libreoffice组件还是各种线上api服务,转换结果都不甚满意。
于是我这边接手这个活了。
调研
其实,最符合客户需求的莫过于原生windows office word的导出功能了。
需要能够操作windows的office word程序,那么需要能够直接访问其系统组件,需要类似com/ole系统库,说干就干。
1、运维做弄了一个配置比较低的ec2机器,windows10系统。
2、我这边找了一些库,python的comtypes.client,但是有点问题,单跑没问题,做成服务,在web线程中做这个事情,就有问题,具体找了下,应该还是线程问题,想了想,不做了(因为本身就不想用python写, )
3、赶紧找了下golang中对应的ole库,找到了一个,看了下文档,直接写了出来。
实现
话不多说,直接上核心代码看看:
下面是基础的解析过程,其实就是模拟以下四个步骤:
1、打开office对应的程序(word/excel/ppt)
2、导出为pdf文件
3、关闭文件
4、退出office程序
基础逻辑
package office
import (
ole "github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
log "github.com/sirupsen/logrus"
)
/// 更多内容请参考官方com文档 https://docs.microsoft.com/zh-cn/office/vba/api/word.application
type operation struct {
optype string
arguments []interface{}
}
/// 部分应用不允许隐藏 ,比如ppt,所以visible需要设定下
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转pdf,下面是word转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文件,可以执行命令go build -ldflags "-h windowsgui" 生成 office-convert.exe 。不想编译的话,可以在prebuilt下找到对应exe文件。
运行
方法一:普通运行
双击执行 office-convert.exe 即可,但是如果程序报错,或者电脑异常关机,不会重启
方法二:后台运行(定时任务启动,可以自动恢复)
windows要做到定时启动/自动恢复,还挺麻烦的。。。
1、复制文件
将prebuilt下两个文件复制到 c:\users\administrator\officeconvert\ 目录下
2、修改com访问权限
当我们以服务、定时任务启动程序的时候,会报错,提示空指针错误。
原因就是微软限制了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、定时任务
创建windows定时任务,每1分钟调用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即可,
有公网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;
}
# ...其他设置
}
请求
已部署到windows机器,访问url:http://127.0.0.1:10000 (如果上面配置了域名,则访问 http://convert-tools.xxx.com/convert)
请求相关
method : post
content-type: application/json
body:
{
"file_in_url":"https://your_docx_file_url",
"source_type":"docx",
"target_type":"pdf"
}
| 参数 | 是否必须 | 取值范围 | 说明 |
|---|---|---|---|
| file_in_url | 是 | 满足下面source_type的各类文档url | 待转换的文档的网络连接 |
| source_type | 是 | [doc,docx,xls,xlsx,ppt,pptx] | 文档类型 |
| target_type | 是 | 暂时只支持pdf,后续会支持更多 |
响应
根据http状态码做判断
200 : ok
其他: 有错
body:
转换的文件的二进制流
如果status_code非200,是对应的报错信息
到此这篇关于基于golang编写一个word/excel/ppt转pdf的工具的文章就介绍到这了,更多相关go word/excel/ppt转pdf内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论