使用PyMuPdf删除PDF文档中的水印

有些网上下载的PDF文档往往被作者添加了水印,有些甚至还加密了。对于加密的PDF文档,用pdf24工具集中的解密工具可以轻松解密,在此不赘述。对于添加的水印,如果知道用于添加水印的PDF编辑软件,那么用原编辑软件也可以轻松批量删除,如果不知道用于添加水印的软件,那就要费一番周折了。通常我们可以用Acrobat Pro打开PDF文件后在导航窗格中显示其内容,从而观察页面结构,如下图:

然后在导航窗格中依次选择各个对象,同时观察页面中的高亮标记,就能找出哪一个对象是水印,如下图:

这时候只要将选择的对象删除,即可删除相应的水印。不过,如果页面较多的话,估计没有几个人能够耐着性子一页一页去删除水印。有个Acrobat Pro的插件Enfocus PitStop可以实现批量操作,但又是一个收费软件,而且激活步骤非常复杂。考虑到最常见的水印就是图片水印和文本水印,所以不妨用PyMuPdf库的功能自己手搓一个PDF水印消除程序,如下:

python 复制代码
import sys
import fitz  # PyMuPDF
import re
from collections import defaultdict


def main():
    if len(sys.argv) > 1:
        pdf_path = sys.argv[1]
    else:
        pdf_path = input("请输入 PDF 文件路径: ").strip()

    try:
        doc = fitz.open(pdf_path)
    except Exception as e:
        print(f"❌ 打开 PDF 失败: {e}")
        return

    print(f"✅ 成功打开 PDF,共 {len(doc)} 页\n")

    while True:
        print("请选择功能:")
        print("1. 自动检测并删除超过75%页面共有的相同尺寸图片")
        print("2. 输入宽度和高度,删除匹配尺寸的图片")
        print("3. 输入正则表达式,删除匹配的文本")
        print("4. 保存修改并退出")

        choice = input("请输入选择 (1-4): ").strip()

        if choice == "1":
            auto_remove_common_images(doc)
        elif choice == "2":
            try:
                w = float(input("请输入宽度 (points): "))
                h = float(input("请输入高度 (points): "))
                remove_images_by_size(doc, w, h)
            except ValueError:
                print("❌ 输入无效,请输入数字")
        elif choice == "3":
            regex_str = input("请输入正则表达式: ").strip()
            remove_text_by_regex(doc, regex_str)
        elif choice == "4":
            output_path = (
                pdf_path.replace(".pdf", "_processed.pdf")
                if pdf_path.lower().endswith(".pdf")
                else pdf_path + "_processed.pdf"
            )
            try:
                doc.save(output_path, garbage=4, deflate=True, clean=True)
                print(f"✅ 已保存处理后的文件: {output_path}")
            except Exception as e:
                print(f"❌ 保存失败: {e}")
            doc.close()
            break
        else:
            print("❌ 无效选择,请重新输入")


def auto_remove_common_images(doc):
    """
    自动检测每页中的图片,在75%以上页面中出现的尺寸相同的图片推定为图片水印并予以删除
    """
    size_to_page_count = defaultdict(int)
    total_pages = len(doc)

    for p in range(total_pages):
        page = doc[p]
        page_sizes = set()
        for img in page.get_images(full=True):
            xref = img[0]
            for rect in page.get_image_rects(xref):
                w = round(rect.width, 1)
                h = round(rect.height, 1)
                page_sizes.add((w, h))
        for sz in page_sizes:
            size_to_page_count[sz] += 1

    threshold = 0.75 * total_pages
    qualifying_sizes = [sz for sz, cnt in size_to_page_count.items() if cnt > threshold]

    if not qualifying_sizes:
        print("✅ 没有找到超过 75% 页面共有的相同尺寸图片")
        return

    print(f"发现以下尺寸在超过 75% 页面出现,将进行删除:")
    for sz in qualifying_sizes:
        print(f"   • {sz[0]} × {sz[1]} points(出现在 {size_to_page_count[sz]} 页)")

    total_removed = 0
    for sz in qualifying_sizes:
        total_removed += remove_images_by_size(doc, sz[0], sz[1], silent=True)
    print(f"✅ 共删除 {total_removed} 个图片实例")


def remove_images_by_size(doc, target_w, target_h, silent=False):
    """
    通过指定图片的精确尺寸/最小尺寸删除图片水印
    """
    target_w = round(target_w, 1)
    target_h = round(target_h, 1)
    removed = 0
    for page in doc:
        to_redact = []
        for img in page.get_images(full=True):
            xref = img[0]
            for rect in page.get_image_rects(xref):
                if (
                    abs(round(rect.width, 1) - target_w) < 0.1
                    and abs(round(rect.height, 1) - target_h)
                    < 0.1  # 删除相同尺寸图片,适用于输入精确图片尺寸
                    # round(rect.width, 1) >= target_w
                    # and round(rect.height, 1) >= target_h  # 删除宽度及高度大不小于指定值的图片,适用于输入图片尺寸的大致值
                ):
                    page.delete_image(xref)  # 将图片数据直接删除
                    removed += 1

    if not silent:
        print(f"✅ 已删除 {removed} 个尺寸为 {target_w}×{target_h} 的图片实例")
    return removed


def remove_text_by_regex(doc, regex_str):
    """
    覆盖文本水印
    """
    try:
        pattern = re.compile(regex_str)
    except re.error as e:
        print(f"❌ 正则表达式语法错误: {e}")
        return

    removed = 0
    for page in doc:
        to_redact = []
        text_dict = page.get_text("dict")
        for block in text_dict.get("blocks", []):
            if block["type"] != 0:  # 只处理文本块(type=0)
                continue
            for line in block.get("lines", []):
                for span in line.get("spans", []):
                    if pattern.search(span.get("text", "")):
                        to_redact.append(
                            fitz.Rect(span["bbox"])
                        )  # pymupdf无法删除文本,采取红action进行"视觉删除 + 内容层移除"
        if to_redact:
            for r in to_redact:
                page.add_redact_annot(
                    r, fill=(1, 1, 1)
                )  # 填充矩形进行视觉删除,如果水印下方有内容,会被白色矩形覆盖
            page.apply_redactions()  # 移除文本
            removed += len(to_redact)
    print(f"✅ 已删除 {removed} 个匹配正则表达式的文本片段")


if __name__ == "__main__":
    main()

当然,上面的程序还很有局限性,例如如果文字水印是半透明的且下层还有内容,消除文字水印后下层内容会被白色矩形覆盖,但对于大多数常见情形,还是可以取得满意的处理效果。以下是示例文件使用上述程序处理的过程及结果:

bash 复制代码
(venv) PS E:\projects\python\pdftool> & e:/projects/python/pdftool/venv/Scripts/python.exe e:/projects/python/pdftool/pdf_cleaner.py
请输入 PDF 文件路径: input.pdf
✅ 成功打开 PDF,共 32 页

请选择功能:
1. 自动检测并删除超过75%页面共有的相同尺寸图片
2. 输入宽度和高度,删除匹配尺寸的图片
3. 输入正则表达式,删除匹配的文本
4. 保存修改并退出
请输入选择 (1-4): 3
请输入正则表达式: .*慧嘉森教育.*
✅ 已删除 64 个匹配正则表达式的文本片段
请选择功能:
1. 自动检测并删除超过75%页面共有的相同尺寸图片
2. 输入宽度和高度,删除匹配尺寸的图片
3. 输入正则表达式,删除匹配的文本
4. 保存修改并退出
请输入选择 (1-4): 1
发现以下尺寸在超过 75% 页面出现,将进行删除:
   • 793.7 × 595.3 points(出现在 32 页)
✅ 共删除 32 个图片实例
请选择功能:
1. 自动检测并删除超过75%页面共有的相同尺寸图片
2. 输入宽度和高度,删除匹配尺寸的图片
3. 输入正则表达式,删除匹配的文本
4. 保存修改并退出
请输入选择 (1-4): 4
✅ 已保存处理后的文件: input_processed.pdf
相关推荐
ComPDFKit3 小时前
ComPDF的产品升级:从工具包到PDF服务
pdf
占疏6 小时前
pdf文件解析
pdf
开开心心_Every8 小时前
音频视频转文字工具,离线语音识别免费
人工智能·游戏·微信·pdf·excel·语音识别·memcache
开开心心_Every8 小时前
电脑网速加速工具,无线有线叠加网络
网络·游戏·微信·pdf·电脑·excel·语音识别
小沈同学呀8 小时前
SpringBoot 使用Docx4j实现 DOCX 转 PDF
spring boot·后端·pdf·docx4j
开开心心就好8 小时前
免费批量抠图软件大模型,复杂倒影精准去除
网络·windows·pdf·计算机外设·电脑·硬件架构·材料工程
MaiTube&Maipdf1 天前
麦瓜PDF 的安全分享流程图:从上传到权限控制
pdf·流程图
CodeCaptain1 天前
【一】dify的知识库上传过相关的文件作为待引用的文档,这样已经与[原始语料 → 按“一文档一份 PDF”存 ObjectStore]同样的概念吗
人工智能·pdf·dify
放飞自我的Coder2 天前
【PDF拆分 Python拆分左右并排PDF】
python·pdf