关于PDFCPU的使用,以及go的IO流 | 豆包MarsCode AI刷题

489 阅读5分钟

在本次项目中,主要采取PDFCPU这个库来完成在上传、导出过程中的PDF的拆分与合并。同时,此库也由api、model、type等库组合而成,每一个库都使用中有自己独特的用处。

1、关于io

io流在对于PDF的编码过程中起着决定性的作用,在本次项目中应用的对象方法主要是由Read、Close、Seek三种方法组成,由此衍生出io.Reader、io.ReadCloser、io.ReadSeeker等多种对象。

Read 方法读取长度为 len(p)的字节到 p 中,它返回读取到的字节长度(0<=n<=len(p))和遇到的任何错误。Close方法一般用于关闭数据读写的通道。Seek方法可以根据给定的偏移量,基于数据的起始位置、末尾位置、或者当前读写位置去寻找新的读写位置。这个新的读写位置用于表明下一次读或写时的起始索引。

2、关于PDFCPU库的介绍

PDFcpu 是一个多功能的 PDF 解决方案,适用于各种 PDF 相关的需求,PDFCPU库不仅可以用来在程序中编辑、合成、分割、加密、解密PDF,也可以在命令行中进行PDF的编辑处理。

3、关于PDF合成的操作介绍

所有的PDF操作均是基于将文件转化成二进制的字节流,再进行内部的编辑,最后输出。对于×1的合成,需要把所有文件转化成io.ReadSeeker的类型。输入的文件仅有两种格式,一种是图片格式,例如:jpg、png、jpeg等,另一种则是PDF格式。对于PDF合成的本质就是将多个PDF文件进行统一合成,所以对于图片格式的文件,我们需要先进行一套处理,将图片格式的文件转化成PDF的格式文件,对于所有PDF文件,则先将它们储存在一个切片中。

转化PDF方法,为避免增加服务器的压力,应尽可能避免产生临时文件,所以全部以缓冲区的方式处理。首先需要把所有图片文件进行读取,为避免对原文件造成损坏,所以再将所有文件信息进行复制给一个新的变量data,再将data信息传送给imageReader,然后利用PDFCPU库下的api.ImportImages,将所有信息写入到缓冲区,进行处理,最后将缓冲区返回至前端,代码如下所示:


func convertToPDF(reader io.ReadSeeker) (io.ReadSeeker, error) {
	data, err := ioutil.ReadAll(reader)
	if err != nil {
		return nil, err
	}
	imageReader := bytes.NewReader(data)
	// 创建一个内存中的缓冲区来存储 PDF
	tempPDF := new(bytes.Buffer)
	// 使用 api.ImportImages 将图片插入到PDF
	conf := model.NewDefaultConfiguration()
	err = api.ImportImages(nil, tempPDF, []io.Reader{imageReader}, nil, conf)
	if err != nil {
		return nil, err
	}
	// 返回 bytes.NewReader,它实现了 io.ReadSeeker 接口
	return bytes.NewReader(tempPDF.Bytes()), nil
}

对于最终一步的合成,infiles切片中存储了大量PDF格式文件的信息,通过api的方法,将合成文件全部输出到outfile中,最终通过SSE将数据传输给前端。代码如下所示:

err = api.MergeRaw(infiles, &outfile, false, nil)

对于上述两次使用的方法,第一个方法的源代码如下

func ImportImages(rs io.ReadSeeker, w io.Writer, imgs []io.Reader, imp *pdfcpu.Import, conf *model.Configuration) error 

w是将要被写入的对象,imgs是要写入对象的信息,imp是指代页面布局(主要是与纸张大小、间隔相关),如果传入的值为空,则会采用默认设定,设定如下。conf同理,则是关于格式的相关设置。

func DefaultImportConfig() *Import {
	return &Import{
		PageDim:  types.PaperSize["A4"],
		PageSize: "A4",
		Pos:      types.Full,
		Scale:    0.5,
		InpUnit:  types.POINTS,
	}
}

对于×2的排版布局,则是采用PDFCPU库下的nup结构体下的grid属性来实现,将一张纸进行网格的划分。但将grid应用到纸张上,则需要先将grid进行解析构造。所以先采用pdfcpu.ParseNUpGridDefinition的方法对nup进行定义重写,再将第一次输出(×1)的结果传入,最后通过api.NUp的方法,将其成功应用到文件上。×4布局的操作方法与此同理。代码如下

if err := pdfcpu.ParseNUpGridDefinition(1, 2, nup); err != nil {return err}
var outFileNew bytes.Buffer
err = api.NUp(bytes.NewReader(outfile.Bytes()), &outFileNew, nil, nil, nup, nil)

对于第一个函数,源代码主要目的是将nup,Grid进行定义,进行网格的划分。Grid属性即是由type包下的Dim来进行划分

func ParseNUpGridDefinition(rows, cols int, nUp *model.NUp) error {
	m := cols
	if m <= 0 {
		return errInvalidGridDims
	}

	n := rows
	if n <= 0 {
		return errInvalidGridDims
	}

	nUp.Grid = &types.Dim{Width: float64(m), Height: float64(n)}

	return nil
}

对于第二个函数,源代码的主要目的是根据nup的属性,将outfile重组,最后输出新的布局PDF文件。rs是需要写入的文件,w则是被写入的目标文件,imgFiles、selectedPages则是对应用页数的选择,nup是对页面布局的设定,conf同上述所述。

func NUp(rs io.ReadSeeker, w io.Writer, imgFiles, selectedPages []string, nup *model.NUp, conf *model.Configuration) error

4、关于PDF拆解的操作介绍

与合成相似,大多数所应用的文件类型是相似的,操作也是大同小异,所以针对此操作,主要进行一下函数的操作

modelCtx, err := api.ReadValidateAndOptimize(bytes.NewReader(body), model.NewDefaultConfiguration())

先将文件读取,并同时新建conf将其格式化,返回modelCtx(也就是*model.Context结构)的内容,下一步将其内容进行拆分,每一页进行一次分割(第一个参数传入的是要分割的内容,第二个参数是对特定页数进行分割)

pageContent, err := api.ExtractPage(modelCtx, i)
func ExtractPage(ctx *model.Context, pageNr int) (io.Reader, error)

其中第一个函数的目的是为了将文件转化成PDFCPU的context结构,第二个函数正式对其进行操作,将其进行分割

5、结语

关于PDFCPU库的使用,在本次项目中仅仅是使用了其解析、拆分、合成的功能,但其加密、解密等功能并未涉足,期待在下一次项目中,可以更加深入学习PDFCPU