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}')

 

posted on 2026-06-08 12:03  sw-lab  阅读(24)  评论(0)    收藏  举报

导航