最近项目有给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
这个IDE
对JAVA
代码进行编译打成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
代码里加水印成功后会输出加水印成功
,只要输出里存在这个字符串就是调用成功了。