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

news2024/9/28 9:28:18

上次我在《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文本指令的解析

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

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指令,我的方法如下:

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

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

data = parse_operand(operand)
print(data)

结果:

b'\x80\x80\x81\x82CSDN\x83https://blog.csdn.net/as604049322'

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

编码表构建

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

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,解析函数为:

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

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

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)

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

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)

image-20240830175601761

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

去除文本水印实践

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

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

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'

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

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)

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

image-20240830180101016

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

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

相关文章

Error running tomcat: Can‘t find catalina.jar

一、错误描述&#xff1a; 在运行 java-web项目时出现报错&#xff1a;Error running tomcat: Can‘t find catalina.jar 二、错误原因&#xff1a; tomcat的路径错误&#xff0c;在idea中配置正确的tomcat路径 三、解决方法&#xff1a; 1.点击EditConfigurations 2.点…

如何通过WinRAR软件有效禁止RAR压缩包内文件的修改

RAR压缩包作为一种广泛使用的文件格式&#xff0c;凭借其高压缩比和强大的功能&#xff0c;成为了许多用户保存和传输文件的首选。然而&#xff0c;在某些情况下&#xff0c;我们可能希望确保RAR压缩包内的文件不被随意修改或删除&#xff0c;以维护文件的安全性和完整性。本文…

CANoe入门(四) :全真实节点阶段和真实节点和部分仿真节点共存阶段,读取和模拟数据

1. 前言 前篇文章我们在CANoe全仿真阶段&#xff0c;模拟数据和信号。这篇文章&#xff0c;我们来看下全真实节点阶段和真实节点和部分仿真节点共存阶段&#xff0c;怎么读取数据信号&#xff0c;和模拟发送数据信号。 2. 全真实节点阶段 全真实节点阶段&#xff0c;所有的 …

LeetCode_sql_day18(1841.联赛信息统计)

描述 表: Teams ------------------------- | Column Name | Type | ------------------------- | team_id | int | | team_name | varchar | ------------------------- team_id 是该表主键. 每一行都包含了一个参加联赛的队伍信息.表: Matches -------…

StarShip v0.5版本更新

CodeSouler更新 IDE插件&#xff08;CodeSouler&#xff09; 01 代码补全优化 &#x1f680; 解决了Tab操作与IDE自带补全的冲突。 优化代码补全机制&#xff0c;调整触发逻辑并改进防抖算法&#xff0c;减少编码干扰。 修复了JetBrains插件中的多余 ) 和 } 符号问题。 02 代…

【GPT】Coze使用开放平台接口-【4】创建机器人

在前面三篇&#xff0c;我们分别创建了插件&#xff0c;插件里面添加了多个工具。接着&#xff0c;我们把插件添加到工作流内&#xff0c;成为一个开放平台API的调用节点&#xff0c;从而创建出一条业务流。分别是&#xff0c;语音伪造检测工作流&#xff0c;以及通话语音内容分…

day14JS-正则表达式

1. 什么是正则表达式 正则表达式(Regular Expression)是一种文本模式&#xff0c;包括普通字符&#xff08;例如&#xff0c;a 到 z 之间的字母&#xff09;和特殊字符&#xff08;称为"元字符"&#xff09;&#xff0c;可以用来描述和匹配字符串的特定模式。正则表…

SRA ToolKit(v 3.1.1)安装和使用(Bioinformatics tools-032)

01 检索数据 run就是数据&#xff0c;如SRR26717485 SRA 档案数据通过 SRA 加载过程进行标准化&#xff0c;并由 SRA 工具包用于读取和生成如 FASTQ、SAM 等格式。默认的工具包配置使其能够通过登录号查找和检索 SRA 运行数据。 现在&#xff0c;公共 SRA 文件可以通过 GCP 和…

[WCT系列(四):BLASTSyncEngine

WCT系列&#xff08;一&#xff09;&#xff1a;WindowContainerTransaction类详解 WCT系列&#xff08;二&#xff09;&#xff1a;SyncTransactionQueue类详解 WCT系列&#xff08;三&#xff09;&#xff1a;WindowOrganizerController WCT系列&#xff08;四&#xff09;&a…

分治,CF 768B. Code For 1

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 https://codeforces.com/problemset/problem/768/B 二、解题报告 1、思路…

python-读取word中的内容

doc Document(rD:\xxxx\xxxx\xxx.docx) #读取word中所有内容 for p in doc.paragraphs print(p,p.text) #读取指定段落中的所有run(文字块) for run in doc.paragraphs[1].runs: print(run,run.text) #读取word中所有表格内容 for 表格 in doc.tables: print(表格) for 行 in …

什么是家庭全光组网和企业全光组网,两者有什么区别?

家庭全光组网和企业全光组网虽然都是基于光纤技术来实现高速网络连接&#xff0c;但它们在应用场景、规模、需求和技术细节上存在一些差异。 家庭全光组网 目标用户&#xff1a;面向个人家庭用户。 规模&#xff1a;一般为单个住宅内的网络覆盖&#xff0c;或者小范围内的多个房…

零基础泛微二开指南

前言 在泛微系统上开发一个自定义post接口 准备 首先准备工作要做好&#xff0c;安装一个泛微&#xff0c;之后所有的操作要在泛微的安装目录操作 参考官网安装&#xff0c;挺麻烦的&#xff1b; IDEA 1、直接新建项目 new ->Project from Existing Sources.直接打开泛…

uniapp微信小程序page-container导致滚动失效/向下偏移,返回上一页/左滑取消返回上一页

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 前提&#xff1a; 使用uniapp来做的微信小程序 有两级tab页面 要求手机的两边往中间滑时 要求&#xff08;调用手机的物理返回按钮--有震动感&#xff09; 返回上一页。具体如下图箭头所示&#xf…

数据防泄漏软件10款超好用推荐|2024数据防泄漏软件排名

在2024年&#xff0c;数据防泄漏软件市场涌现了多款优秀的产品&#xff0c;它们通过不同的技术手段和策略&#xff0c;为企业提供全面的数据安全保护。以下是10款超好用的数据防泄漏软件推荐及简要排名。 1.安企神 特点&#xff1a;专为企业设计的数据安全防护工具&#xff0c…

【零知识证明】MiMC哈希函数电路

1 哈希电路 哈希函数电路实现&#xff1a; pragma circom 2.0.0;// y (x k c) ^ 5 // 输入信号x, k &#xff0c;常量c // base x k c // base2 base * base // base4 base2 * base2 // base5 base *base4 // 输出 ytemplate MIMC5(){signal input x;signal input k…

使用Aqua进行WebUI测试(Pytest)——介绍篇

一、在创建时选择Selenium with Pytest 如果选择的是Selenium&#xff0c;则只能选择Java类语言 选择selenium with Pytest&#xff0c;则可以选择Python类语言 Environment 其中的【Environment】可选New 和 Existing New &#xff1a;选择这个选项意味着你希望工具为你创…

【Go函数详解】二、参数传递、变长参数与多返回值

文章目录 一、传递参数1. 按值传参2. 引用传参2.1 特殊情况2.1.1 切片slice2.1.2 字典map 二、变长参数1. 基本定义和传值1.1 基本定义1.2 传值1.2.1 普通传值1.2.2 传递切片 2. 任意类型的变长参数&#xff08;泛型&#xff09; 三、多返回值1. 命名返回值 一、传递参数 1. 按…

破解电商数据分析难题,优化运营策略的秘诀

在电商行业中&#xff0c;数据分析是不可或缺的一部分。它能帮助商家精准掌握市场动态&#xff0c;优化运营策略&#xff0c;从而提升销售业绩。然而&#xff0c;面对大量复杂的数据&#xff0c;许多电商运营者往往不知道从哪里开始分析。那么&#xff0c;电商运营究竟如何有效…

优可测白光干涉仪助力红外探测行业发展——晶圆衬底检测

从18世纪红外线被发现&#xff0c;到19世纪红外探测器的发明。至今&#xff0c;随着工艺更新迭代&#xff0c;红外探测器朝着多波段、大面阵、高分辨率、低成本量产快速发展。 今天&#xff0c;小优博士带您探索红外探测的奥秘。 一、红外线是什么 红外光是一种电磁波&#x…