pythonPDF去除镶嵌在文本中的文本水印
方法一:基于库的方式去除
整个处理流程由 5 个函数串联完成:
remove_pass 使用 pikepdf 对加密 PDF 进行解密,将无密码版本保存到 decrypted/ 目录。
split_pdf 使用 PyMuPDF (fitz) 将解密后的 PDF 按每 10 页一份进行拆分,输出到 out1/ 目录。拆分的目的是降低后续 Spire.PDF 处理单文件时的页数,避免大文件处理不稳定。
remove_inner_text 使用 Spire.PDF 的正则文本替换能力,逐页匹配并删除形如"2024-01-01 12:00:00"格式的内部水印文本,每个分块独立处理后保存到 out1/。
merge_pdfs 使用 PyMuPDF 将所有处理后的分块按顺序合并为一个 PDF。保存时启用了 garbage=4(去重共享资源)、deflate=True(压缩流数据)、clean=True(清理冗余操作符),以控制合并后的文件体积。
remove_uppder_text 使用 PyMuPDF 的 redaction 功能,在合并后的完整 PDF 上查找 "Evaluation Warning" 水印,通过白色填充涂销将其彻底移除,最终结果输出到 res/ 目录。
#encoding=utf-8
from spire.pdf import *
import fitz
import pikepdf
import time
def remove_pass(input_path):
output_path = f'decrypted/{time.time()}.pdf'
password = '' # 替换为实际密码
try:
# 使用密码打开加密的 PDF
with pikepdf.open(input_path, password=password) as pdf:
# 保存时设置 encryption=False 即可去除密码
pdf.save(output_path, encryption=False)
print("✅ PDF 解密成功!")
return output_path
except pikepdf.PasswordError:
print("❌ 密码错误,请检查密码是否正确。")
except Exception as e:
print(f"❌ 解密失败: {e}")
def remove_inner_text(input_path):
"""
使用正则表达式匹配并删除 PDF 中的文本
:param regex_pattern: 正则表达式字符串
:param input_path: 输入的 PDF 文件路径
:return: 处理后的 PDF 文件路径
"""
doc = PdfDocument()
doc.LoadFromFile(input_path)
out_path = f'out1/{time.time()}.pdf'
regex_pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})" #可自定义正则表达式
# 遍历文档中的每一页
for i in range(doc.Pages.Count):
page = doc.Pages.get_Item(i)
# 1. 创建文本替换器
replacer = PdfTextReplacer(page)
# 2. 将替换模式设置为正则表达式 (Regex)
replacer.Options.ReplaceType = ReplaceActionType.Regex
# 3. 使用正则表达式匹配并将所有实例替换为空字符串(即删除)
replacer.ReplaceAllText(regex_pattern, "")
doc.SaveToFile(out_path)
doc.Close()
return out_path
def remove_uppder_text(input_path, output_path, old_text="Evaluation Warning : The document was created with Spire.PDF for Python."):
doc = fitz.open(input_path)
for page in doc:
# 1. 查找目标文本的精确位置(底层引擎已处理编码映射)
text_instances = page.search_for(old_text)
if not text_instances:
continue
for inst in text_instances:
# 2. 添加涂销注释(Redaction),用白色背景覆盖旧文本
# 注意:可以获取旧文本的字体大小和颜色进行精确还原
page.add_redact_annot(inst, fill=(1, 1, 1))
# 3. 应用涂销,彻底从底层删除旧文本
page.apply_redactions()
doc.save(output_path)
doc.close()
def split_pdf(input_path, max_pages=10):
"""
将 PDF 拆分为多个小文件,每个文件最多 max_pages 页
:param input_path: 输入的 PDF 文件路径
:param max_pages: 每个拆分文件的最大页数
:return: 拆分后的 PDF 文件路径列表
"""
doc = fitz.open(input_path)
total_pages = len(doc)
split_paths = []
for start in range(0, total_pages, max_pages):
end = min(start + max_pages, total_pages)
new_doc = fitz.open()
new_doc.insert_pdf(doc, from_page=start, to_page=end - 1)
out_path = f'out1/split_{start}_{end}_{time.time()}.pdf'
new_doc.save(out_path)
new_doc.close()
split_paths.append(out_path)
print(f"📄 拆分: 第 {start + 1}-{end} 页 → {out_path}")
doc.close()
print(f"✅ 共拆分为 {len(split_paths)} 个文件")
return split_paths
def merge_pdfs(pdf_paths, output_path):
"""
合并多个 PDF 文件为一个
:param pdf_paths: PDF 文件路径列表
:param output_path: 合并后的输出路径
:return: 合并后的 PDF 文件路径
"""
merged = fitz.open()
for path in pdf_paths:
doc = fitz.open(path)
merged.insert_pdf(doc)
doc.close()
merged.save(output_path, garbage=4, deflate=True, clean=True)
merged.close()
print(f"✅ 合并完成: {len(pdf_paths)} 个文件 → {output_path}")
return output_path
if __name__ == '__main__':
input_path = "example"
# Step 1: 解密 PDF
decrypted_path = remove_pass(input_path)
# Step 2: 拆分为每份最多 10 页的小文件
split_paths = split_pdf(decrypted_path, max_pages=10)
# Step 3: 对每个拆分后的 PDF 执行 remove_inner_text
processed_paths = []
for path in split_paths:
processed_path = remove_inner_text(path)
processed_paths.append(processed_path)
print(f"✅ 已处理内部文本: {path} → {processed_path}")
# Step 4: 合并所有处理后的 PDF
merged_path = merge_pdfs(processed_paths, f'out1/merged_{time.time()}.pdf')
# Step 5: 对合并后的 PDF 执行 remove_uppder_text
remove_uppder_text(merged_path, "res/" + input_path)
方法一的方式比较直观易懂,但是缺点很明显:生成的文件过大,容易出现底层错误,速度较慢。
方法二:基于内容流的方式去除
PDF 每一页的可见内容是由若干个"内容流"(Content Stream)按顺序叠加渲染的。你可以把它理解为 Photoshop 里的图层概念——一页 PDF 可以有多层内容流,从底层到顶层依次绘制,最终合成出你看到的效果。
import fitz import re import sys import shutil sys.stdout.reconfigure(encoding='utf-8') INPUT_PDF = r'input.pdf' OUTPUT_PDF = r'clean.pdf' doc = fitz.open(INPUT_PDF) print(f'Total pages: {len(doc)}') total_removed = 0 for page_num in range(len(doc)): page = doc[page_num] contents = page.get_contents() page_modified = False for idx, xref in enumerate(contents): stream = doc.xref_stream(xref) if stream is None: continue stream_str = stream.decode('latin-1') # Check if this stream is a watermark stream # Watermark indicators: # 1. Uses Xi font/state references (/Xi2, /Xi3) # 2. Contains STSong-Light related text with rotation matrix # 3. Contains repeated text pattern with dates (2026-06-04) is_watermark = False # Check for Xi references (watermark transparency and font) if b'/Xi2' in stream or b'/Xi3' in stream: is_watermark = True # Also check for the rotated text pattern with date if not is_watermark and b'0.88295 0.46947 -0.46947 0.88295' in stream: is_watermark = True # Check for STSong-Light watermark text pattern in the stream if not is_watermark and len(stream) < 5000: # Small streams that contain text with rotation are likely watermarks if b'16 Tf' in stream and b'Tm' in stream: # Count text operations - watermark has many Tm/Tj pairs tm_count = stream.count(b'Tm') if tm_count >= 4: # Watermark has 5+ Tm operations # Check if it also has the gray color typical of watermark if b'0.25098' in stream: is_watermark = True if is_watermark: # Replace the watermark stream with a no-op # Use "q Q" which is a valid empty graphics state save/restore doc.update_stream(xref, b"q Q\n") total_removed += 1 page_modified = True doc.save(OUTPUT_PDF, garbage=4, deflate=True) doc.close() print(f'\nDone! Total watermark streams neutralized: {total_removed}') print(f'Output saved to: {OUTPUT_PDF}')
浙公网安备 33010602011771号