Python PDF 加水印 和 Java PDF 加水印

祁增
2023-12-01

最近项目有给PDF加水印的需求,目前使用的方法是:首先生成一个水印 PDF,再通过 PyPDF4 来把原件的每一页和 水印 PDF 合并,但耗时和页数成正比,耗时太长。

后来通过 JAVA 实现的方案是:读取原 PDF 后,在每一页的最外层直接添加文字,并且可以调整角度和透明度。

JAVA 方案耗时大概4000页在500毫秒,而相同文件在使用 Python 方案时耗时大概在 500 秒,JAVA 方案比 Python 方案快了 1000 倍。

然后就想到可能是 Merge 方案操作太耗时,就找了找 Python 里有没有 Java 方案的实现,目前还没找到一样的方案,但通过 PymuPDF 实现了个类似的方案:在每一页创建一个透明矩形,矩形可调整角度,然后在矩形里填充文字。

因为目前项目已经选用 JAVA 方案了,所以 PymuPDF 方案就没再找调整文字透明度的方法,诸君可以找找试试。

对了,PymuPDF 方案果然在耗时上提升了一大截,但还没 JAVA 方案那么变态的快,4000页大概耗时在3秒,耗时和创建几个矩形成正比。

好了,话不多说,给诸君上菜。

Python

import fitz
from PyPDF2 import PdfFileReader, PdfFileWriter


def add_watermark_by_merge_pdf(input_pdf, output, watermark):
    """PyPDF2 Merge 方案"""
    watermark_obj = PdfFileReader(watermark)
    watermark_page = watermark_obj.getPage(0)
    pdf_reader = PdfFileReader(input_pdf)
    pdf_writer = PdfFileWriter()

    for page in range(pdf_reader.getNumPages()):
        p = pdf_reader.getPage(page)
        p.mergePage(watermark_page)
        pdf_writer.addPage(page)

    with open(output, 'wb') as f:
        pdf_writer.write(f)


def add_watermark_by_text(input_path, output_path):
    """PymuPDF 矩形方案"""
    doc = fitz.open(input_path)
    text1 = "rotate=-90"
    red = (1, 0, 0)
    gray = (0, 0, 1)
    for page in doc:
        p1 = fitz.Point(page.rect.width - 25, page.rect.height - 25)
        shape = page.newShape()
        shape.drawCircle(p1, 1)
        shape.finish(width=0.3, color=red, fill=red)
        shape.insertText(p1, text1, rotate=-90, color=gray)
        shape.commit()
    doc.save(output_path)


if __name__ == "__main__":
    add_watermark_by_merge_pdf(
        './douluo.pdf', './watermark.pdf', './output.pdf')
    add_watermark_by_text('./douluo.pdf', './output.pdf')

JAVA

package test;

        import java.io.*;
        import java.net.URL;
        import java.net.URLConnection;

        import com.itextpdf.text.BaseColor;
        import com.itextpdf.text.Element;
        import com.itextpdf.text.pdf.BaseFont;
        import com.itextpdf.text.pdf.PdfContentByte;
        import com.itextpdf.text.pdf.PdfGState;
        import com.itextpdf.text.pdf.PdfReader;
        import com.itextpdf.text.pdf.PdfStamper;
        import test.utils.ConvectorUtils;

public class Watermark {
    /**
     * @param inputFile     你的PDF文件地址
     * @param outputFile    添加水印后生成PDF存放的地址
     * @param waterMarkName 你的水印
     * @return
     */
    public static boolean waterMark(String inputFile, String outputFile, String waterMarkName) {
        try {
            long s1 = System.currentTimeMillis();
            PdfReader reader = new PdfReader(inputFile);
            PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputFile));
            // 这里的字体设置比较关键,这个设置是支持中文的写法
            BaseFont base = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);// 使用系统字体
            int total = reader.getNumberOfPages() + 1;

            PdfContentByte under;
            // Rectangle pageRect = null;
            long s2 = System.currentTimeMillis();
            System.out.println("读文件耗时:" + (s2 - s1));
            for (int i = 1; i < total; i++) {
                // 获得PDF最顶层
                under = stamper.getOverContent(i);
                // set Transparency
                PdfGState gs = new PdfGState();
                // 设置透明度为0.2
                gs.setFillOpacity(0.5f);
                under.setGState(gs);
                under.saveState();
                under.restoreState();
                under.beginText();
                under.setFontAndSize(base, 35);
                under.setTextMatrix(30, 30);
                under.setColorFill(BaseColor.GRAY);
                for (int y = 0; y < 3; y++) {
                    for (int x = 0; x < 2; x++) {
                        // 水印文字成45度角倾斜
                        under.showTextAligned(Element.ALIGN_LEFT, waterMarkName, 40+250 * x, 300 * y+ x * 50, 45);
                    }
                }
                // 添加水印文字
                under.endText();
                under.setLineWidth(1f);
                under.stroke();
            }
            stamper.close();
            System.out.println("转换文件耗时:" + (System.currentTimeMillis() - s2));
            return true;

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public static void main(String[] args) {
        String inputFile = args[0];
        String outputFile = args[1];
        String watermark = args[2];
        boolean flag = waterMark(inputFile, outputFile, watermark);
        if (flag){
            System.out.println("加水印成功");
        }else{
            System.out.println("加水印失败");
        }
    }
}

JAVA POM

<dependencies>
    <dependency>
      <groupId>com.itextpdf</groupId>
      <artifactId>itextpdf</artifactId>
      <version>5.5.10</version>
  	</dependency>
  	<dependency>
      <groupId>com.itextpdf</groupId>
      <artifactId>itext-asian</artifactId>
      <version>5.2.0</version>
    </dependency>
  </dependencies>

有君想要Python 调用 Java的代码,如下:

import os

import jpype

# 一些基础路径
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LIB_PATH = os.path.join(BASE_PATH, 'lib')
# 使用的java包所在路径
WATERMARK_JAR_PATH = os.path.join(LIB_PATH, 'jwatermark.jar')


class JavaRunner:
    def __init__(self):
        jpype.startJVM(jpype.getDefaultJVMPath(), "-ea",
                       "-Djava.class.path=%s" % WATERMARK_JAR_PATH, convertStrings = False)
        Test = jpype.JClass('test.Test')
        self.t = Test()

    def addwatermark(self, input_path: str, output_path: str, watermark: str) -> bool:
        return self.t.addWarterMark(input_path, output_path, watermark)

    def tif2pdf(self, input_path:str, output_path:str) -> bool:
        return self.t.convertTifToPdf(input_path, output_path)

    # def __del__(self):
    #     jpype.shutdownJVM()

这块需要诸君使用IntelliJ IDEA这个IDEJAVA代码进行编译打成jar包,然后使用JPython启动JAVA虚拟机从而可以直接调用jar包中的Class,我这块把jar包放在了主目录的lib文件夹下,为了方便代码观察,我把从config导入的几个基础路径和jar路径COPY过来了,方便诸君进行自配

诸君若不想用JAVA虚拟机,可以尝试第二种方式,如下:

import subprocess


class File:
	……	……
	……	……
	@staticmethod
	def subcommand(args, pattern, timeout, error=TimeoutError):
	    p = subprocess.Popen(args, stderr=subprocess.PIPE,
	                         stdout=subprocess.PIPE, shell=False, encoding="utf-8")
	    p_start = time.time()
	    while True:
	        if p.poll() is not None:
	            break
	        if (time.time() - p_start) > timeout:
	            p.terminate()
	            raise error(f'{args}, {timeout}')
	        time.sleep(0.1)
	    command_return = p.stdout.read()
	    logger.info(f'[command_return] {command_return}')
	    success_str_find = re.findall(pattern, command_return)
	    return True if success_str_find else False

    def addwatermark(self):
        args = ["java", "-classpath", WATERMARK_JAR_PATH,
                "test.Watermark", self.convert_path,  WATERMARK_PATH, self.watermark]
        flag = self.subcommand(
            args, r'(加水印成功)', WATERMARK_TIMEOUT_NUM, WatermarkTimeoutError)
        if flag:
            self.watermark_path = WATERMARK_PATH
        else:
            raise WatermarkError('not get watermark_path')

这块代码也只是给个参考,具体方案是使用Python标准库subprocess开启子进程来运行java命令,就和在命令行直接运行java一样,如:java -classpath jwatermark.jar test.Watermark test.pdf output.pdf "www"
然后我因为有其他地方也都需要通过这种方式运行命令调用JAVA代码,而且设置超时返回是必须的,所有在这里把调用封成了subcommand方法,pattern参数的作用是捕获JAVA代码的输出里是不是存在调用成功的字符串,就像前面写的JAVA代码里加水印成功后会输出加水印成功,只要输出里存在这个字符串就是调用成功了。

 类似资料: