前期搞了个pdf加页脚,但是搞了半天中文加不了,就换了个思路。
直接说结论,pdf拆成图片,加中文,再合成pdf,会导致pdf模糊。
import os
import fitz # PyMuPDF
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime
from typing import List, Tuple, Optional
import tempfile
import shutildef pdf_to_images(input_pdf: str, output_dir: str) -> int:"""将PDF转换为图片Args:input_pdf: 输入PDF文件路径output_dir: 输出图片目录Returns:转换后的图片数量"""os.makedirs(output_dir, exist_ok=True)try:# 打开PDF文档doc = fitz.open(input_pdf)page_count = len(doc)# 转换每一页为图片for page_num in range(page_count):page = doc[page_num]pix = page.get_pixmap()image_path = os.path.join(output_dir, f"page_{page_num + 1}.png")pix.save(image_path)doc.close()return page_countexcept Exception as e:print(f"PDF转图片时出错: {str(e)}")return 0def add_watermark_to_image(input_image: str, output_image: str, watermark_text: str,font_size: int = 10,font_color: Tuple[int, int, int] = (0, 0, 0),font_path: str = None) -> None:"""为图片添加水印Args:input_image: 输入图片路径output_image: 输出图片路径watermark_text: 水印文本font_size: 字体大小font_color: 字体颜色,RGB元组font_path: 字体文件路径,用于支持中文等特殊字符"""try:# 打开图片img = Image.open(input_image).convert("RGBA")width, height = img.size# 创建水印图层watermark = Image.new('RGBA', img.size, (0, 0, 0, 0))draw = ImageDraw.Draw(watermark)# 设置字体font = Noneif font_path:try:font = ImageFont.truetype(font_path, font_size)except Exception:print(f"无法加载指定字体: {font_path},将尝试系统字体")# 如果指定字体无法加载或未指定字体,尝试系统字体if font is None:system_fonts = {"win32": ["simhei.ttf", "msyh.ttc"],"darwin": ["/System/Library/Fonts/PingFang.ttc"],"linux": ["/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"]}for font_name in system_fonts.get(os.name, []):try:font = ImageFont.truetype(font_name, font_size)print(f"使用系统字体: {font_name}")breakexcept Exception:continue# 如果仍未找到合适的字体,使用默认字体if font is None:font = ImageFont.load_default()print("使用默认字体")# 计算水印位置(右下角,留出边距)# 兼容旧版本和新版本的 Pillow 库try:# 新版本 Pillow 使用 getbbox()bbox = draw.textbbox((0, 0), watermark_text, font=font)text_width = bbox[2] - bbox[0]text_height = bbox[3] - bbox[1]except AttributeError:# 旧版本 Pillow 使用 textsize()text_width, text_height = draw.textsize(watermark_text, font=font)margin = 10position = (width - text_width - margin, height - text_height - margin)# 添加水印draw.text(position, watermark_text, font=font, fill=font_color + (128,)) # 半透明# 合并水印图层combined = Image.alpha_composite(img.convert('RGBA'), watermark)combined = combined.convert(img.mode) # 转换回原始模式# 保存图片combined.save(output_image)except Exception as e:print(f"图片添加水印时出错: {str(e)}")raise # 重新抛出异常,便于调试def images_to_pdf(image_dir: str, output_pdf: str, page_count: int) -> None:"""将图片合并为PDFArgs:image_dir: 图片目录output_pdf: 输出PDF文件路径page_count: 页面数量"""try:# 创建一个新的PDF文档doc = fitz.open()# 按顺序添加图片到PDFfor i in range(1, page_count + 1):image_path = os.path.join(image_dir, f"page_{i}.png")if os.path.exists(image_path):# 打开图片img = fitz.open(image_path)rect = img[0].rectpdfbytes = img.convert_to_pdf()img.close()# 将图片PDF添加到新文档imgpdf = fitz.open("pdf", pdfbytes)page = doc.new_page(width=rect.width, height=rect.height)page.show_pdf_page(rect, imgpdf, 0)imgpdf.close()# 保存PDFdoc.save(output_pdf)doc.close()except Exception as e:print(f"图片合并为PDF时出错: {str(e)}")def add_watermark(image_path, watermark_text, output_path=None, font_size=24, opacity=80):"""给图片右下角添加文字水印参数:image_path: 图片路径watermark_text: 水印文字output_path: 输出路径,默认与原图同目录并添加"_watermarked"后缀font_size: 字体大小opacity: 水印透明度(0-100)"""# 打开图片image = Image.open(image_path)# 如果未指定输出路径,生成默认路径if output_path is None:file_name, file_ext = os.path.splitext(image_path)output_path = f"{file_name}_watermarked{file_ext}"# 创建水印图像watermark = Image.new('RGBA', image.size, (0, 0, 0, 0))draw = ImageDraw.Draw(watermark)# 设置字体font = Nonesystem_fonts = {"win32": ["simhei.ttf", "msyh.ttc"],"darwin": ["/System/Library/Fonts/PingFang.ttc"],"linux": ["/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"]}for font_name in system_fonts.get(os.name, []):try:font = ImageFont.truetype(font_name, font_size)print(f"使用系统字体: {font_name}")breakexcept Exception:continue# 如果仍未找到合适的字体,使用默认字体if font is None:font = ImageFont.load_default()print("使用默认字体")# 计算水印文字尺寸 - 兼容新旧版本try:# 新版本 Pillow 使用 getbbox()bbox = draw.textbbox((0, 0), watermark_text, font=font)text_width = bbox[2] - bbox[0]text_height = bbox[3] - bbox[1]except AttributeError:# 旧版本 Pillow 使用 textsize()text_width, text_height = draw.textsize(watermark_text, font=font)# 确定右下角位置(留出10像素边距)position = (image.width - text_width - 10, image.height - text_height - 10)# 绘制水印文字draw.text(position, watermark_text, font=font, fill=(255, 255, 255, int(opacity * 2.55)))# 合并水印到原图if image.mode != 'RGBA':image = image.convert('RGBA')watermarked_image = Image.alpha_composite(image, watermark)# 获取输出文件的扩展名,确定保存格式output_ext = os.path.splitext(output_path)[1].lower()# 根据文件扩展名处理保存逻辑if output_ext in ['.jpg', '.jpeg']:# 对于JPEG格式,移除透明度通道并转换为RGBwatermarked_image = watermarked_image.convert('RGB')watermarked_image.save(output_path, quality=95)elif output_ext == '.png':# 对于PNG格式,保留RGBA模式watermarked_image.save(output_path)else:# 其他格式,尝试转换为RGB(如果可能)try:watermarked_image.convert('RGB').save(output_path)except:watermarked_image.save(output_path)print(f"水印已添加,保存至: {output_path}")return output_pathdef add_watermark_to_pdf(input_pdf: str, output_pdf: str, watermark_text: str,font_size: int = 10,font_color: Tuple[int, int, int] = (0, 0, 0),font_path: str = None,pages_to_skip: Optional[List[int]] = None) -> None:"""为PDF文件添加水印Args:input_pdf: 输入PDF文件路径output_pdf: 输出PDF文件路径watermark_text: 水印文本font_size: 字体大小font_color: 字体颜色,RGB元组font_path: 字体文件路径,用于支持中文等特殊字符pages_to_skip: 跳过添加水印的页码列表(从0开始)"""if pages_to_skip is None:pages_to_skip = []# 创建临时目录with tempfile.TemporaryDirectory() as temp_dir:# 转换PDF为图片page_count = pdf_to_images(input_pdf, temp_dir)if page_count == 0:print(f"无法转换PDF文件: {input_pdf}")return# 为每张图片添加水印for i in range(1, page_count + 1):if (i - 1) in pages_to_skip:continueimage_path = os.path.join(temp_dir, f"page_{i}.png")temp_image_path = os.path.join(temp_dir, f"page_{i}_watermarked.png")add_watermark_to_image(image_path,temp_image_path,watermark_text,font_size,font_color,font_path)# 替换原图if os.path.exists(temp_image_path):os.replace(temp_image_path, image_path)# 将图片合并回PDFimages_to_pdf(temp_dir, output_pdf, page_count)def batch_add_watermark(input_dir: str, output_dir: str, watermark_text: str,font_size: int = 10,font_color: Tuple[int, int, int] = (0, 0, 0),font_path: str = None,pages_to_skip: Optional[List[int]] = None,overwrite: bool = False) -> None:"""批量为目录中的PDF文件添加水印,保持目录结构Args:input_dir: 输入目录路径output_dir: 输出目录路径watermark_text: 水印文本font_size: 字体大小font_color: 字体颜色,RGB元组font_path: 字体文件路径,用于支持中文等特殊字符pages_to_skip: 跳过添加水印的页码列表(从0开始)overwrite: 是否覆盖已存在的输出文件"""# 确保输出目录存在os.makedirs(output_dir, exist_ok=True)# 遍历输入目录中的所有文件和子目录for root, dirs, files in os.walk(input_dir):# 计算相对路径relative_path = os.path.relpath(root, input_dir)# 如果是根目录,相对路径会是'.',此时设为空if relative_path == '.':relative_path = ''# 计算对应的输出目录current_output_dir = os.path.join(output_dir, relative_path)# 确保输出子目录存在os.makedirs(current_output_dir, exist_ok=True)# 处理当前目录中的PDF文件for file in files:if file.lower().endswith('.pdf'):input_path = os.path.join(root, file)output_path = os.path.join(current_output_dir, file)# 检查是否需要跳过已存在的文件if os.path.exists(output_path) and not overwrite:print(f"跳过已存在的文件: {output_path}")continueprint(f"正在处理: {input_path}")add_watermark_to_pdf(input_path,output_path,watermark_text,font_size,font_color,font_path,pages_to_skip)if __name__ == "__main__":# 配置参数INPUT_DIR = "./input_pdfs" # 输入PDF文件目录OUTPUT_DIR = "./output_pdfs" # 输出PDF文件目录# 水印文本示例,包含当前日期WATERMARK_TEXT = '更多资料请+wx:zz34742'# 字体设置FONT_SIZE = 12FONT_COLOR = (0, 0, 0) # 黑色# 指定支持中文的字体# Windows系统常用中文字体FONT_PATH = r"C:\Windows\Fonts\simhei.ttf" # 黑体# FONT_PATH = r"C:\Windows\Fonts\msyh.ttc" # 微软雅黑# macOS系统常用中文字体# FONT_PATH = "/System/Library/Fonts/PingFang.ttc" # 苹方# Linux系统常用中文字体# FONT_PATH = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc" # Noto Sans CJK# 根据您的系统选择合适的字体# FONT_PATH = None # 使用默认字体# 跳过添加水印的页码(从0开始)PAGES_TO_SKIP = [] # 例如: [0] 表示跳过第一页# 是否覆盖已存在的文件OVERWRITE = False# 执行批量处理batch_add_watermark(INPUT_DIR,OUTPUT_DIR,WATERMARK_TEXT,font_size=FONT_SIZE,font_color=FONT_COLOR,font_path=FONT_PATH,pages_to_skip=PAGES_TO_SKIP,overwrite=OVERWRITE)
这个PDF水印添加工具的核心逻辑可以概括为以下几个步骤:
1. PDF转图片
- 读取输入PDF文件
- 使用PyMuPDF库将PDF的每一页转换为PNG图片
- 图片保存在临时目录中,文件名格式为
page_1.png
,page_2.png
等
2. 图片添加水印
- 打开图片并创建一个透明的水印图层
- 检测系统字体,优先使用支持中文的字体(如黑体、微软雅黑等)
- 计算水印文本的大小和位置(右下角留出边距)
- 将水印文本绘制到图层上(半透明效果)
- 合并水印图层到原图
3. 图片合并回PDF
- 按顺序读取带水印的图片
- 使用PyMuPDF创建新的PDF文档
- 将每张图片添加为PDF的一页
- 保存为新的PDF文件
4. 批量处理
- 遍历指定目录中的所有PDF文件
- 保持原有的目录结构
- 对每个PDF文件执行上述处理流程
- 支持跳过指定页码、覆盖已处理文件等选项
关键技术细节
-
字体处理:
- 优先使用用户指定的字体
- 自动检测系统中可用的中文字体
- 回退到默认字体确保兼容性
-
版本兼容:
- 同时支持Pillow库的新旧API
- 使用
textbbox()
(新)和textsize()
(旧)方法计算文本尺寸
-
临时文件管理:
- 使用
tempfile.TemporaryDirectory
创建临时工作目录 - 处理完成后自动清理临时文件
- 使用
-
错误处理:
- 每层处理都有异常捕获
- 关键步骤输出详细日志
- 保留原始错误信息便于调试
使用流程
- 将需要添加水印的PDF文件放入
input_pdfs
目录 - 运行程序,配置水印文本、字体等参数
- 处理后的PDF文件会保存在
output_pdfs
目录中,保持原有目录结构 - 程序会自动检测系统并选择合适的中文字体,确保水印文本正常显示
这个工具通过将PDF转换为图片、添加水印后再转回PDF的方式,实现了对任意PDF文件添加文本水印的功能,特别适合批量处理大量文档。