基于golang编写一个word/excel/ppt转pdf的工具

需求

公司客户有需求,需要转换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 暂时只支持PDF,后续会支持更多

响应

根据HTTP状态码做判断

200 : ok

其他: 有错

Body:

转换的文件的二进制流

如果status_code非200,是对应的报错信息

到此这篇关于基于golang编写一个word/excel/ppt转pdf的工具的文章就介绍到这了,更多相关go word/excel/ppt转pdf内容请搜索恩蓝小号以前的文章或继续浏览下面的相关文章希望大家以后多多支持恩蓝小号!

原创文章,作者:VQKQU,如若转载,请注明出处:http://www.wangzhanshi.com/n/5584.html

(0)
VQKQU的头像VQKQU
上一篇 2024年12月17日 19:27:44
下一篇 2024年12月17日 19:27:46

相关推荐

  • 重学Go语言之基础数据类型详解

    前言 Go语言有非常强大的数据类型系统,其支持的数据类型大体上可分为四类:基础数据类型、引用数据类型、接口类型、复合类型。 基础数据类型有: 布尔型(bool) 整型(int) 浮…

    Golang 2024年12月29日
  • Golang中的信号(Signal)机制详解

    引言 Signal 是一种操作系统级别的事件通知机制,进程可以响应特定的系统信号。这些信号用于指示进程执行特定的操作,如程序终止、挂起、恢复等。Golang 的标准库 o…

    Golang 2024年12月29日
  • Golang中Options模式的使用

    在软件开发领域,选项模式(Options Pattern)是一种常见的设计模式,它允许用户通过提供一系列选项来自定义函数、类型或对象的行为。在Golang中,选项模式的应用非常广泛…

    Golang 2024年12月17日
  • Go语言标准错误error全面解析

    错误类型 errorString 错误是程序中处理逻辑和系统稳定新的重要组成部分。 在go语言中内置错误如下: // The error built-in interface ty…

    Golang 2024年12月17日
  • 一文带你了解Go语言中的I/O接口设计

    1. 引言 I/O 操作在编程中扮演着至关重要的角色。它涉及程序与外部世界之间的数据交换,允许程序从外部,如键盘、文件、网络等地方读取数据,也能够将外界输入的数据重新写入到目标位置…

    Golang 2024年12月29日
  • 一文详解Go语言中的Defer机制

    在Go语言中,defer是一个关键字,用于确保资源的清理和释放,特别是在函数中创建的资源。defer语句会将其后的函数调用推迟到包含它的函数即将返回时执行。这使得defer成为处理…

    Golang 2024年12月17日
  • Go语言中Gin框架使用JWT实现登录认证的方案

    Gin框架JWT登录认证 背景: 在如今前后端分离开发的大环境中,我们需要解决一些登陆,后期身份认证以及鉴权相关的事情,通常的方案就是采用请求头携带token的方式进行实现。 在开…

    2024年12月17日
  • 如何使用go实现创建WebSocket服务器

    使用Go语言创建WebSocket服务器可以利用现有的库来简化开发过程。gorilla/websocket 是一个非常流行且功能强大的库,适用于Go语言的WebSocket应用。下…

    Golang 2024年12月17日
  • golang标准库time时间包的使用

    time包 时间和日期是我们编程中经常会用到的,本文主要介绍了 Go 语言内置的 time 包的基本用法。 time 包提供了一些关于时间显示和测量用的函数。 time 包中日历的…

    2024年12月26日
  • Go中Vendo机制的使用

    1. 介绍 自 Go 1.6 起,vendor 机制正式启用,它允许把项目的依赖放到一个位于本项目的 vendor 目录中,这个 vendor 目录可以简单理解成私有的 GOPAT…

    Golang 2024年12月17日

发表回复

登录后才能评论