PDF文本指令解析与文本水印去除

上次我在《PDF批量加水印 与 去除水印实践》一文中完成了对图片水印和文字水印的去除。

链接:https://xxmdmst.blog.csdn.net/article/details/139483535

但是对于页面对象的内容对象是单层,不是数组的情况,无法去除水印。今天我们专门研究PDF的文本绘制指令,并尝试去除这种水印。

PDF文本显示操作符

文本显示操作符有TJTj两种,还有单引号和双引号两种。引号类的指令表示移动到下一行并显示文本,对于水印文本不可能使用这类指令。所以今天我们仅研究TJTj两种指令。

TJ指令(或称操作符)用于显示一个数组中的文本字符串,每个字符串可能有插值调整。

例如:

复制代码
[ (\\0319\\047) -3 <00180102> 14 (\\001\\232) 17 (\\001\\002\\001\\017) 4 (\\001\\002\\001\\220) 6 (\\001\\036) 9 <037f> ] TJ
  • [] 包围的区域表示一个数组。
  • 数组中的元素可以是文本字符串或者数字,其中数字表示字符间距调整,单位是千分之一的字体单位。
  • 括号 () 和括号<>包围的内容表示字符串。

括号 () 内的字符串,反斜杠 \表示后面三位数是8进制字符,\\0319\\047可以理解为\\031,9,\\047三部分组成。

括号<>内的内容是用十六进制表示的字符串。

Tj指令则表示单个文本,对于TJ指令数组中其中一个文本元素。

PDF文本指令的解析

首先我们打印一下指令的完整内容:

python 复制代码
import PyPDF2

reader = PyPDF2.PdfReader(r"mysql【带水印】.pdf")
page = reader.pages[0]
page_content = page.get_contents()
page_data = page_content.get_data()
for line in page_data.splitlines():
    i = line.rfind(b" ")
    operator = line[i+1:]
    operand = line[:i]
    if operator in (b'TJ', b'Tj'):
        print(line)

截取一部分指令展示一下:

复制代码
b'[ (\\033\\240\\031\\236\\024\xc3) 11 <2cb40a16> 11 (\\017F\\0040) 11 (\\057\xfd\\011j) 11 (\\0106\\033\xe9) 11 (\\025\\077\\002\xd6) ] TJ'
b'[ (\\016\\052) 11 (\\004\xbe\\021\\210) 11 (\\006\xd8\\004\xfb) 11 (CX) ] TJ'
b'[ (\\021\\210\\006\xd8\\004\xfb) 11 (CX\\0106) 11 (\\004j\\004T) 11 (\\057\xfd\\002\xd6) 11 (\\056\xf1\\055\\010) 11 (\\012\xbc\\007\xb5\\021\\210) ] TJ'
b'[ (\\007\\2433\\053) ] TJ'
b'[ (\\015\\273) 11 (\\033\\240\\031\\236) 11 (\\024\xc3) ] TJ'
b'[ (M\\216\\007\\2433\\053) 11 (\\015\\273\\033\\240) 11 (\\031\\236\\024\xc3) ] TJ'
b'[ (\\002\xd6\\021\\210) 11 (\\006\xd8\\015X) 11 (\\007\xb5\\021\\210\\004\\135) ] TJ'
b'(\\200\\200\\201\\202CSDN\\203https\\072\\057\\057blog\\056csdn\\056net\\057as604049322) Tj'

解析TJTj指令,我的方法如下:

python 复制代码
def parse_operand(operand):
    data = b""
    for part in re.findall(b"\(.+?\)|<.+?>", operand):
        s = part[1:-1]
        if chr(part[0]) == "(":
            data += re.sub(rb'\\([0-7]{3})',
                           lambda m: chr(int(m.group(1), 8)).encode("charmap"), s)
        elif chr(part[0]) == "<":
            data += bytes.fromhex(s.decode())
    return data

尝试解析上面展示的最后一个指令:

python 复制代码
data = parse_operand(operand)
print(data)

结果:

复制代码
b'\x80\x80\x81\x82CSDN\x83https://blog.csdn.net/as604049322'

这样我们就解析出来原始的字节,要解析出原始的文本还需要解析Tf指令,使用对应的charmap编码表进行二次转换。

编码表构建

首先我们得到所有的编码表:

python 复制代码
from PyPDF2._cmap import build_char_map

cmaps = {}
for f in page['/Resources']['/Font']:
    cmaps[f] = build_char_map(f, 200.0, page)

这里build_char_map如何实现,本文不作深究。

然后需要解析Tf指令,构建当前文本所使用的char_map,解析函数为:

python 复制代码
if operator == b'Tf':
    cmap_name = operand.split()[0].decode()
    charMapTuple = cmaps[cmap_name]
    cmap = (charMapTuple[2], charMapTuple[3],
            cmap_name, charMapTuple[4])

然后就可以使用下面的函数,对文本指令的内容进行解析获取文本:

python 复制代码
def pdf_decode_text(tt):
    encoding = cmap[0]
    if isinstance(encoding, str):
        try:
            t = tt.decode(encoding, "surrogatepass")
        except Exception:
            fallback_encoding = "utf-16-be" if encoding == "charmap" else "charmap"
            t = tt.decode(fallback_encoding, "surrogatepass")
    else:
        t = "".join(encoding.get(x, chr(x)) for x in tt)
    return "".join(cmap[1].get(x, x) for x in t)

完整的文本解析代码如下:

python 复制代码
import PyPDF2
from PyPDF2._cmap import build_char_map
import re


def parse_operand(operand):
    data = b""
    for part in re.findall(b"\(.+?\)|<.+?>", operand):
        s = part[1:-1]
        if chr(part[0]) == "(":
            data += re.sub(rb'\\([0-7]{3})',
                           lambda m: chr(int(m.group(1), 8)).encode("charmap"), s)
        elif chr(part[0]) == "<":
            data += bytes.fromhex(s.decode())
    return data


def pdf_decode_text(tt):
    encoding = cmap[0]
    if isinstance(encoding, str):
        try:
            t = tt.decode(encoding, "surrogatepass")
        except Exception:
            fallback_encoding = "utf-16-be" if encoding == "charmap" else "charmap"
            t = tt.decode(fallback_encoding, "surrogatepass")
    else:
        t = "".join(encoding.get(x, chr(x)) for x in tt)
    return "".join(cmap[1].get(x, x) for x in t)


reader = PyPDF2.PdfReader(r"mysql【带水印】.pdf")
page = reader.pages[0]
cmaps = {}
for f in page['/Resources']['/Font']:
    cmaps[f] = build_char_map(f, 200.0, page)
page_content = page.get_contents()
page_data = page_content.get_data()
for line in page_data.splitlines():
    i = line.rfind(b" ")
    operator = line[i+1:]
    operand = line[:i]
    if operator == b'Tf':
        cmap_name = operand.split()[0].decode()
        charMapTuple = cmaps[cmap_name]
        cmap = (charMapTuple[2], charMapTuple[3],
                cmap_name, charMapTuple[4])
    elif operator in (b'TJ', b'Tj'):
        data = parse_operand(operand)
        text = pdf_decode_text(data)
        print(operand, text)

可以看到每条文本指令都已完美的解析出原始的文本内容。

去除文本水印实践

我的思路是寻找前10页,非空白文本出现次数最多的对应的文本指令,然后删除这些文本指令即可。

寻找前10页出现次数最多文本指令:

python 复制代码
import PyPDF2
from collections import Counter
from PyPDF2._cmap import build_char_map
import re


def parse_operand(operand):
    data = b""
    for part in re.findall(b"\(.+?\)|<.+?>", operand):
        s = part[1:-1]
        if chr(part[0]) == "(":
            data += re.sub(rb'\\([0-7]{3})',
                           lambda m: chr(int(m.group(1), 8)).encode("charmap"), s)
        elif chr(part[0]) == "<":
            data += bytes.fromhex(s.decode())
    return data


def pdf_decode_text(tt):
    encoding = cmap[0]
    if isinstance(encoding, str):
        try:
            t = tt.decode(encoding, "surrogatepass")
        except Exception:
            fallback_encoding = "utf-16-be" if encoding == "charmap" else "charmap"
            t = tt.decode(fallback_encoding, "surrogatepass")
    else:
        t = "".join(encoding.get(x, chr(x)) for x in tt)
    return "".join(cmap[1].get(x, x) for x in t)


reader = PyPDF2.PdfReader(r"mysql【带水印】.pdf")
counter = Counter()
for page in reader.pages[:10]:
    cmaps = {}
    for f in page['/Resources']['/Font']:
        cmaps[f] = build_char_map(f, 200.0, page)
    page_content = page.get_contents()
    page_data = page_content.get_data()
    for line in page_data.splitlines():
        i = line.rfind(b" ")
        operator = line[i+1:]
        operand = line[:i]
        if operator == b'Tf':
            cmap_name = operand.split()[0].decode()
            charMapTuple = cmaps[cmap_name]
            cmap = (charMapTuple[2], charMapTuple[3],
                    cmap_name, charMapTuple[4])
        elif operator in (b'TJ', b'Tj'):
            data = parse_operand(operand)
            text = pdf_decode_text(data)
            if text.strip():
                counter[(text, line)] += 1
watermark_command = counter.most_common(1)[0][0][1]
watermark_command
复制代码
b'(\\200\\200\\201\\202CSDN\\203https\\072\\057\\057blog\\056csdn\\056net\\057as604049322) Tj'

然后我们批量删除所有页的这行指令,并保存:

python 复制代码
writer = PyPDF2.PdfWriter()
for page in reader.pages:
    page_content = page.get_contents()
    page_data = page_content.get_data()
    page_data_without_logo = page_data.replace(watermark_command+b"\n", b"")
    if page_content.decoded_self is not None:
        page_content.decoded_self.set_data(page_data_without_logo)
    else:
        page_content.set_data(page_data_without_logo)
    page[PyPDF2.generic.NameObject(
        "/Contents")] = page_content
    page.compress_content_streams()
    writer.add_page(page)
output_path = "mysql【去水印】.pdf"
with open(output_path, "wb") as output_file:
    writer.write(output_file)

最终已经成功的完成了对这类文本水印的去除:

相关推荐
拓端研究室15 小时前
专题:2025人形机器人、工业机器人、智能焊接机器人、扫地机器人产业洞察报告 | 附158+份报告PDF、数据仪表盘汇总下载
microsoft·机器人·pdf
TextIn智能文档云平台17 小时前
复杂PDF文档结构化提取全攻略——从OCR到大模型知识库构建
pdf·ocr
会飞的小菠菜17 小时前
PDF文件中的广告二维码图片该怎么批量删除
pdf·删除·二维码·批量
一只花里胡哨的程序猿1 天前
odoo打印pdf速度慢问题
pdf·odoo
灵海之森1 天前
Python将md转html,转pdf
pdf
阿幸软件杂货间2 天前
最新PDF版本!Acrobat Pro DC 2025,解压即用版
pdf·adobe acrobat·acrobat
星空的资源小屋2 天前
网易UU远程,免费电脑远程控制软件
人工智能·python·pdf·电脑
会飞的小菠菜2 天前
如何一次性将多个PPT幻灯片批量转换成PDF文档
pdf·powerpoint·ppt·批量·格式转换
somethingGoWay2 天前
wpf .netcore 导出pdf文件
pdf·wpf·.netcore
小白电脑技术2 天前
PDF教程|如何把想要的网页保存下来?
pdf·电脑