PDF批量加水印 与 去除水印实践

news2024/11/20 19:30:56

本文主要目标是尝试去除水印,但是为了准备测试数据,我们需要先准备好有水印的pdf测试文件。

注意:本文的去水印只针对文字悬浮图片悬浮两种特殊情况,即使是这两种情况也不代表一定都可以去除水印。

文章目录

  • 批量添加透明图片水印
  • 批量去除悬浮图片水印
  • 批量添加文字水印
  • 批量去除文字水印
  • 总结

批量添加透明图片水印

首先按照之前文章《Office三件套批量转PDF以及PDF书签读写与加水印》提供的方法,生成带水印的PDF,完整代码如下:

import PyPDF2
import math
from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageChops


def crop_image(im):
    '''裁剪图片边缘空白'''
    bg = Image.new(mode='RGBA', size=im.size)
    bbox = ImageChops.difference(im, bg).getbbox()
    if bbox:
        return im.crop(bbox)
    return im


def set_opacity(im, opacity):
    '''设置水印透明度'''
    assert 0 <= opacity <= 1
    alpha = im.split()[3]
    alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
    im.putalpha(alpha)
    return im


def get_mark_img(text, color="#8B8B1B", size=30, opacity=0.15):
    width = len(text) * size
    mark = Image.new(mode='RGBA', size=(width, size + 20))
    ImageDraw.Draw(im=mark) \
        .text(xy=(0, 0),
              text=text,
              fill=color,
              font=ImageFont.truetype('msyhbd.ttc', size=size))
    mark = crop_image(mark)
    set_opacity(mark, opacity)
    return mark


def create_watermark_pdf(text, filename="watermark.pdf", page_size=(595, 842), color="#8B8B1B", size=30, opacity=0.3,
                         space=75, angle=30, dpi=100):
    mark = get_mark_img(text, color, size, opacity)
    img_size = tuple(map(lambda s: int(s*dpi//72), page_size))
    im = Image.new(mode='RGBA', size=img_size)
    w, h = img_size
    c = int(math.sqrt(w ** 2 + h ** 2))
    mark2 = Image.new(mode='RGBA', size=(c, c))
    y, idx = 0, 0
    mark_w, mark_h = mark.size
    while y < c:
        x = -int((mark_w + space) * 0.5 * idx)
        idx = (idx + 1) % 2
        while x < c:
            mark2.paste(mark, (x, y))
            x = x + mark_w + space
        y = y + mark_h + space
    mark2 = mark2.rotate(angle)
    im.paste(mark2, (int((w - c) / 2), int((h - c) / 2)),  # 坐标
             mask=mark2.split()[3])
    im.save(filename, "PDF", resolution=dpi, save_all=True)


def pdf_add_watermark(filename, save_filepath, watermark='watermark.pdf'):
    watermark = PyPDF2.PdfReader(watermark).pages[0]
    pdf_reader = PyPDF2.PdfReader(filename)
    pdf_writer = PyPDF2.PdfWriter()
    for page in pdf_reader.pages:
        page.merge_page(watermark)
        page.compress_content_streams()
        pdf_writer.add_page(page)
    with open(save_filepath, "wb") as out:
        pdf_writer.write(out)


if __name__ == '__main__':
    watermark = 'watermark.pdf'
    create_watermark_pdf(
        "小小明的CSDN:https://blog.csdn.net/as604049322", watermark, opacity=0.4)
    pdf_add_watermark('mysql.pdf', 'mysql【带水印】.pdf', watermark=watermark)

然后就可以得到一个全部是水印的PDF文件:

在这里插入图片描述

批量去除悬浮图片水印

对于这类水印,去除起来并不难,只需要批量删除最后一个图像图层即可。

import PyPDF2

writer = PyPDF2.PdfWriter()
reader = PyPDF2.PdfReader('mysql【带水印】.pdf')
for page in reader.pages:
    obj = page.get("/Resources").get("/XObject")
    obj.pop(list(obj)[-1])
    page[PyPDF2.generic.NameObject("/Resources")][
        PyPDF2.generic.NameObject("/XObject")] = obj
    writer.add_page(page)

output_path = "mysql【去水印】.pdf"
with open(output_path, "wb") as output_file:
    writer.write(output_file)

对于上面方法生成的水印,已经迅速一键去除。

当然水印图床可能实际并不在最后一层,这就需要调试测试,找到水印对应的层进行删除。

例如我需要查看第5页每个图片对象,可以使用jupyter执行如下代码:

from PIL import Image
import io

reader = PyPDF2.PdfReader('mysql【带水印】.pdf')
page = reader.pages[5]
print(page.get("/Resources").get("/XObject"))
for i, img in enumerate(page.images):
    img_data = Image.open(io.BytesIO(img.data))
    print(i, img)
    display(img_data)

对于一些特殊的PDF有助于找到水印图层的规律,进而批量删除水印。

一般情况下,水印都是最后添加的,所以上面的代码直接删除最后一个图层没啥问题。有时我们会遇到一些特殊的多图层pdf,PyPDF2并不能良好的支持,即使原封不动复制,也会报错。

我们需要改造一下处理函数:

import PyPDF2


def remove_image_watermark(input_pdf, output_path):
    writer = PyPDF2.PdfWriter()
    reader = PyPDF2.PdfReader(input_pdf)

    for page in reader.pages:
        obj = page.get("/Resources").get("/XObject")
        new_obj = PyPDF2.generic.DictionaryObject()
        obj.pop(list(obj)[-1])
        for k in obj:
            value = obj[PyPDF2.generic.NameObject(k)]
            if value is None:
                continue
            new_obj[PyPDF2.generic.NameObject(k)] = value
        page[PyPDF2.generic.NameObject("/Resources")][
            PyPDF2.generic.NameObject("/XObject")] = new_obj
        writer.add_page(page)

    with open(output_path, "wb") as output_file:
        writer.write(output_file)


input_pdf = "example2.pdf"
output_path = "example2【去水印】.pdf"
remove_image_watermark(input_pdf, output_path)

但这样也会不断出现异常日志,例如:Object 2763 0 not defined.,而且读取速度非常慢,一个100多页的PDF4分钟才处理完成。

这时,我们可以修改PyPDF2库的源码,修改库根目标的_reader.py文件的get_object函数:
在这里插入图片描述

表示在两个条件都不满足时,直接返回None,不再执行后面的读取和正则查找。因为对于本身不存在的对象,执行这样复杂的读取查找只是纯粹浪费时间。

经过上述修改后,再次执行代码,在1秒内处理完毕。

批量添加文字水印

不管是添加文字水印还是图片水印,我们都需要相应的水印PDF与需要添加水印的pdf进行图层合并。

首先我们需要生成文字水印PDF:

from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import math

pagesize = (595, 842)
watermark = 'watermark.pdf'
space = 120
angle = 30

pdfmetrics.registerFont(TTFont('msyhbd', 'msyhbd.ttc'))
mark = canvas.Canvas(watermark, pagesize=pagesize)
w, h = pagesize
c = int(math.sqrt(w**2+h**2))
mark.rotate(angle)
mark.setFont('msyhbd', 20)
mark.setFillColor("#8B8B1B")
mark.setFillAlpha(0.4)
for i, y in enumerate(range(-int(math.sin(math.radians(angle))*w-40), int(math.cos(math.radians(angle))*h-40), space)):
    mark.drawString(20+y*w/c+(w/2 if i%2==1 else 0), y, '小小明的CSDN:https://blog.csdn.net/as604049322')
mark.save()

注意:若缺少reportlab库,可以通过pip install reportlab安装。

然后整理一下代码,生成带有文字水印的PDF,最终完整代码为:

from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import PyPDF2
import math


def create_text_watermark_pdf(text, watermark, pagesize=(595, 842), color="#8B8B1B", font_size=20,
                              opacity=0.3, space=150, angle=30, font='msyhbd.ttc'):
    pdfmetrics.registerFont(TTFont('font', font))
    mark = canvas.Canvas(watermark, pagesize=pagesize)
    w, h = pagesize
    c = int(math.sqrt(w**2+h**2))
    mark.rotate(angle)
    mark.setFont('font', font_size)
    mark.setFillColor(color)
    mark.setFillAlpha(opacity)
    for i, y in enumerate(range(-int(math.sin(math.radians(angle))*w-40), int(math.cos(math.radians(angle))*h-40), space)):
        mark.drawString(20+y*w/c+(w/2 if i % 2 == 1 else 0), y, text)
    mark.save()


def pdf_add_watermark(filename, save_filepath, watermark='watermark.pdf'):
    watermark = PyPDF2.PdfReader(watermark).pages[0]
    pdf_reader = PyPDF2.PdfReader(filename)
    pdf_writer = PyPDF2.PdfWriter()
    for page in pdf_reader.pages:
        page.merge_page(watermark)
        page.compress_content_streams()
        pdf_writer.add_page(page)
    with open(save_filepath, "wb") as out:
        pdf_writer.write(out)


if __name__ == '__main__':
    watermark = 'watermark.pdf'
    create_text_watermark_pdf(
        "小小明的CSDN:https://blog.csdn.net/as604049322", watermark, opacity=0.3, angle=30)
    filename = 'mysql.pdf'
    save_filepath = 'mysql【带水印】.pdf'
    pdf_add_watermark(filename, save_filepath, watermark=watermark)

在这里插入图片描述

可以很清楚的看到文字水印相对图片文字的好处在于,文字链接可以直接点击访问。

批量去除文字水印

问题来了,对于这种悬浮的文字水印,能否批量去除呢?

首先我们观察一下添加水印前后,page对象的主要变化:

import PyPDF2

print(PyPDF2.PdfReader("mysql.pdf").pages[0])
print(PyPDF2.PdfReader("mysql【带水印】.pdf").pages[0])

结果示例:

{'/Type': '/Page', '/Parent': IndirectObject(2, 0, 2016175275936), '/Resources': {'/Font': {'/F1': IndirectObject(5, 0, 2016175275936), '/F2': IndirectObject(9, 0, 2016175275936), '/F3': IndirectObject(11, 0, 2016175275936), '/F4': IndirectObject(16, 0, 2016175275936), '/F5': IndirectObject(21, 0, 2016175275936), '/F6': IndirectObject(26, 0, 2016175275936), '/F7': IndirectObject(28, 0, 2016175275936)}, '/ExtGState': {'/GS7': IndirectObject(7, 0, 2016175275936), '/GS8': IndirectObject(8, 0, 2016175275936)}, '/ProcSet': ['/PDF', '/Text', '/ImageB', '/ImageC', '/ImageI']}, '/MediaBox': [0, 0, 595.32, 841.92], '/Contents': IndirectObject(4, 0, 2016175275936), '/Group': {'/Type': '/Group', '/S': '/Transparency', '/CS': '/DeviceRGB'}, '/Tabs': '/S', '/StructParents': 0}
{'/Type': '/Page', '/Resources': {'/ExtGState': {'/GS7': IndirectObject(5, 0, 2016175272768), '/GS8': IndirectObject(6, 0, 2016175272768), '/gRLs0': {'/ca': 0.3}}, '/Font': {'/F1': IndirectObject(7, 0, 2016175272768), '/F2': IndirectObject(11, 0, 2016175272768), '/F3': IndirectObject(14, 0, 2016175272768), '/F4': IndirectObject(22, 0, 2016175272768), '/F5': IndirectObject(30, 0, 2016175272768), '/F6': IndirectObject(38, 0, 2016175272768), '/F7': IndirectObject(41, 0, 2016175272768), '/F12f89c5f3-0000-4658-b1ab-21ec73871408': {'/BaseFont': '/Helvetica', '/Encoding': '/WinAnsiEncoding', '/Name': '/F1', '/Subtype': '/Type1', '/Type': '/Font'}, '/F2+0': IndirectObject(45, 0, 2016175272768)}, '/ProcSet': ['/ImageC', '/Text', '/ImageB', '/PDF', '/ImageI']}, '/MediaBox': [0, 0, 595.32, 841.92], '/Contents': IndirectObject(49, 0, 2016175272768), '/Group': {'/Type': '/Group', '/S': '/Transparency', '/CS': '/DeviceRGB'}, '/Tabs': '/S', '/Annots': [], '/Parent': IndirectObject(1, 0, 2016175272768)}

可以看到主要变化在于水印PDF的page对象增加了'/Parent'节点。

针对这种情况,我们的批量去除水印代码为:

import PyPDF2

pdf_path = "mysql【带水印】.pdf"
writer = PyPDF2.PdfWriter()
reader = PyPDF2.PdfReader(pdf_path)
for page in reader.pages:
    if '/Parent' in page:
        del page['/Parent']
    writer.add_page(page)
output_path = "mysql【去水印】.pdf"
with open(output_path, "wb") as output_file:
    writer.write(output_file)

结果发现并没有去除水印。

可以看到这个PDF,加水印前后,/Contents仅一个IndirectObject对象,正常对于普通的加过文字水印的PDF,/Contents往往都存在多个IndirectObject对象。执行如下代码进行进一步确认:

import PyPDF2

reader = PyPDF2.PdfReader(r"mysql【带水印】.pdf")
page = reader.pages[0]
page_content = page.get_contents()
print(page_content.get_data())

在这里插入图片描述

可以确认水印存在于这个对象中,预计主体内容和水印都被合并在了这一个内容对象里,这样我们就无法简单的通过删除/Contents内的某个对象达到删除水印的效果。

虽然我们自己生成的水印PDF无法轻易被删除,但最近我确实看到不少可以轻松删除文字水印的PDF。

例如这个PDF文件:

import PyPDF2

pdf_path = "工行结算卡流水.pdf"
reader = PyPDF2.PdfReader(pdf_path)
page = reader.pages[0]
page_content = page.get_contents()
print(page_content)
[IndirectObject(5, 0, 1288719316112), IndirectObject(6, 0, 1288719316112), IndirectObject(7, 0, 1288719316112), IndirectObject(8, 0, 1288719316112), IndirectObject(9, 0, 1288719316112)]

可以看到这一个PDF的第一页的内容对象存在5个对象,这样我们就可以挨个测试只要某个对象,得到的PDF是否满足要求,最终达到去除水印的目的。

首先我们将第一页的每个对象拆分成单独的一页:

import PyPDF2

pdf_path = "工行结算卡流水.pdf"
writer = PyPDF2.PdfWriter()
reader = PyPDF2.PdfReader(pdf_path)
page = reader.pages[0]
page_contents = page.get_contents()
for page_content in page_contents:
    new_page_content = PyPDF2.generic.ArrayObject()
    new_page_content.append(page_content)
    page[PyPDF2.generic.NameObject(
        "/Contents")] = new_page_content
    writer.add_page(page)
with open("第一页图层拆分.pdf", "wb") as f:
    writer.write(f)

然后我们人工检查第一页图层拆分.pdf这个文件,看哪几个图层才是我们需要的数据,目前我测试的这个文件只有第3页是我所需要的数据,那么我们可以批量只取第3个对象的内容:

import PyPDF2

pdf_path = "工行结算卡流水.pdf"
output_path = "工行结算卡流水【去水印】.pdf"

writer = PyPDF2.PdfWriter()
reader = PyPDF2.PdfReader(pdf_path)
for page in reader.pages:
    new_page_content = PyPDF2.generic.ArrayObject()
    page_content = page.get_contents()
    new_page_content.append(page_content[2])
    page[PyPDF2.generic.NameObject(
        "/Contents")] = new_page_content
    writer.add_page(page)
with open(output_path, "wb") as f:
    writer.write(f)

经检查工行结算卡流水.pdf中的水印在工行结算卡流水【去水印】.pdf文件中已经完全消除。

总结

我们可以给PDF加图片水印或文字水印,要去除图片水印,一般只需要删除最后一个图片对象即可。

要去除文字水印,需要保证主体内容和文字水印在/Contents中位于不同的对象内,这样我们只需要删除文字水印对应的IndirectObject对象即可删除水印。

而对于主体内容和文字水印已经混合在一个对象时,本文的提供的方法则无能为力,需要进一步深入分析PDF细节。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1791351.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenStreetMap部署(OSM)

参考&#xff1a;https://github.com/openstreetmap/openstreetmap-website/blob/master/DOCKER.md OpenStreeMap 部署 操作系统建议使用 Ubuntu 22 版本 安装 Docker # 更新软件包索引&#xff1a; sudo apt-get update # 允许APT使用HTTPS&#xff1a; sudo apt-get inst…

TypeScript的never类型的妙用

never类型介绍 在 TypeScript 中&#xff0c;"never" 是一个表示永远不会发生的值类型。 使用场景 "never" 类型通常用于以下几种情况&#xff1a; 1、函数返回类型&#xff1a;当一个函数永远不会返回任何值&#xff08;比如抛出异常或者无限循环&…

使用 MDC 实现日志链路跟踪,包教包会!

在微服务环境中&#xff0c;我们经常使用 Skywalking、Spring Cloud Sleut 等去实现整体请求链路的追踪&#xff0c;但是这个整体运维成本高&#xff0c;架构复杂&#xff0c;本次我们来使用 MDC 通过 Log 来实现一个轻量级的会话事务跟踪功能&#xff0c;需要的朋友可以参考一…

数据库与缓存⼀致性⽅案

数据库与缓存⼀致性⽅案 1、背景2、数据⼀致性⽅案设计3、数据⼀致性⽅案流程图4、关键代码4.1、 处理数据⼀致性的消息队列⼊⼝4.2、数据⼀致性配置的常量信息 1、背景 现有的业务场景下&#xff0c;都会涉及到数据库以及缓存双写的问题&#xff0c;⽆论是先删除缓存&#xf…

2024年度CCF-阿里云瑶池科研基金正式发布

2024年度CCF-阿里云瑶池科研基金正式发布 截止时间&#xff1a;2024年7月1日24:00&#xff08;北京时间&#xff09; 欢迎CCF会员积极申报 “CCF-阿里云瑶池科研基金”由CCF与阿里云计算有限公司于2024年联合设立&#xff0c;专注于数据库领域&#xff0c;旨在为领域学者提供…

FarmersWorld农民世界源码开发:0撸卷轴+潮玩模式

一、引言 随着科技的发展&#xff0c;游戏产业日益壮大&#xff0c;一种新型的游戏形式——零撸游戏应运而生。本文将深入探讨FarmersWorld农民世界源码开发&#xff0c;以其独特的0撸卷轴潮玩模式&#xff0c;为玩家带来全新的游戏体验。 二、源码开发的专业性和深度 Farmer…

找出字符串中出现最多次数的字符以及出现的次数

str.charAt(i) 是JavaScript中获取字符串中特定位置字符的方法&#xff0c;表示获取当前的字符。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-wi…

进入新公司有焦虑感怎么办?

前因 前两天技术交流群里有童鞋问了一个很有意思的问题&#xff0c;他问如何克服进入新公司的焦虑感&#xff1f;很多热心的童鞋都纷纷支招&#xff0c;比如 “主动干活”、“专注干活”、“让时间冲淡焦虑感”、……等等&#xff0c;这些都很有道理&#xff0c;不过&#xff…

win11右键二级菜单恢复成win10一级菜单

winr输入“cmd”回车&#xff0c;打开cmd窗口&#xff0c;输入如下命令&#xff0c;并回车。reg add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve提示cuccessfully&#xff0c;表示操作成功。重启电脑即可。 如下…

NEJM新英格兰医学期刊文献在家如何查阅下载

今天收到的求助文献中有一篇是NEJM新英格兰医学期刊中的一篇文献&#xff0c;篇名“Osimertinib after Chemoradiotherapy in Stage III EGFR -Mutated NSCLC” 首先我们先简单了解一下NEJM新英格兰医学期刊&#xff1a; NEJM新英格兰医学期刊&#xff1a;New England Journa…

排序进阶----快速排序

当我们写了插入和希尔排序后&#xff0c;我们就应该搞更难的了吧。大家看名字就知道我们这篇博客的内容了吧。而且从名字上来看。快速排序就很快吧。那么为什么这个排序怎么能叫快速排序啊。我们希尔排序不是很快嘛。那么我们的快速排序肯定是有特殊之处嘞。不然这就太自负了。…

Qt Group宣布更新许可协议

本文翻译自&#xff1a;Qt Group Launches Updates of License Agreements 原文作者&#xff1a;Qt Group产品管理总监Santtu Ahonen 为了简化Qt Group的许可协议并提升整体的可读性&#xff0c;我们对商业合同文档的结构进行了更新。这一变更对Qt Group许可其商业产品和服务的…

SpringCache 缓存 - @Cacheable、@CacheEvict、@CachePut、@Caching、CacheConfig 以及优劣分析

目录 SpringCache 缓存 环境配置 1&#xff09;依赖如下 2&#xff09;配置文件 3&#xff09;设置缓存的 value 序列化为 JSON 格式 4&#xff09;EnableCaching 实战开发 Cacheable CacheEvict CachePut Caching CacheConfig SpringCache 的优势和劣势 读操作…

出行预测:端午打车需求将上涨31%,滴滴发放超2亿司机补贴

作为上半年的“收官”小长假&#xff0c;端午假期接棒“五一”的出行热度&#xff0c;中短途周边游持续升温&#xff0c;海滨旅行、龙舟民俗体验成为新的出行看点。 滴滴出行预测&#xff0c;端午节当天&#xff08;6月10日&#xff09;打车需求将同比去年上涨约31%。今年端午…

因子区间[牛客周赛44]

思路分析: 我们可以发现125是因子个数的极限了,所以我们可以用二维数组来维护第几个数有几个因子,然后用前缀和算出来每个区间合法个数,通过一个排列和从num里面选2个 ,c num 2 来计算即可 #include<iostream> #include<cstring> #include<string> #include…

长虹智能电视55D3P(机芯:ZLH74GiR2G)海思平台固件解析打包

一、使用Hitool打包固件 接上一篇&#xff0c;尝试使用HITOOL打包固件 长虹55D3P海思平台固件破解-CSDN博客 参考ZNDS HItool备份固件&#xff1a;【玩机必看】海思机顶盒备份线刷包 制作分区表xml文件_ZNDS刷机/救砖_ZNDS HITOOL下载&#xff1a;https://cloud.189.cn/web/…

关于信号翻转模块(sig_flag_mod)的实现

关于信号翻转模块(sig_flag_mod)的实现 语言 &#xff1a;Verilg HDL 、VHDL EDA工具&#xff1a;ISE、Vivado、Quartus II 关于信号翻转模块(sig_flag_mod)的实现一、引言二、实现信号翻转模块的方法&#xff08;1&#xff09;输入接口&#xff08;2&#xff09;输出接口&…

如何跨渠道分析销售数据 - 7年制造业销售经验小结

如何跨渠道分析销售数据 - 7年制造业销售经验小结&#xff08;1&#xff09; 【前言】 在我过去7年销售工作生涯中&#xff0c;从第一年成为公司销冠后&#xff0c;我当时的确自满的一段时间&#xff0c;认为自己很了不起。但是第一年的销售业绩并没有拿到提成&#xff0c;最…

LabVIEW冲击响应谱分析系统

LabVIEW冲击响应谱分析系统 开发了一种基于LabVIEW开发的冲击响应谱分析系统&#xff0c;该系统主要用于分析在短时间内高量级输入力作用下装备的响应。通过改进的递归数字滤波法和样条函数法进行冲击响应谱的计算&#xff0c;实现了冲击有效持续时间的自动提取和响应谱的精准…

分布式任务队列系统 celery 进阶

通过前面的入门&#xff0c;我们大概了解了celery的工作原理及简单的入门代码示例&#xff08;传送门&#xff09;&#xff0c;下面进行一些稍微复杂的任务调度学习 多目录结构异步执行 在实际项目中&#xff0c;使用Celery进行异步任务处理时&#xff0c;经常需要将代码组织…