主要方法:
- 找到需要替换的文本块,(位置,block尺寸)然后在改位置添加修订注释(redaction annotation)
- 添加注释后,步骤1的文本块会被删除
- 在原位置添加新的文本块
向 PDF 添加内容的各种方法,分两大类:直接写入内容流(content stream) 和 添加注释/对象(annotation/widget)。不同方式对 page.get_text("blocks")
的可见性也不一样。
insert_text
/ insert_textbox
/ draw_text/(add_redact_annot
+ apply_redactions)/insert_image
/ draw_image/draw_rect
、draw_line
、draw_circle
常用的添加接口?
1. 直接写入内容流(Content Stream)
方法 | 功能 |
---|---|
page.insert_text(point, text, ...) | 在指定坐标插入单行文字 |
page.insert_textbox(rect, text, ...) | 在给定矩形内自动换行并插入文字 |
page.insert_image(rect, filename=…) | 在矩形区域插入位图 |
page.insert_pdf(src_doc, from_page, to_page, ...) | 将另一个 PDF 的页面或部分页面拷贝过来 |
page.draw_rect(rect, ...) | 画矩形(可用于填充背景或边框) |
page.draw_line(pt1, pt2, ...) | 画直线 |
page.draw_circle(center, radius, ...) | 画圆 |
page.draw_oval(rect, ...) | 画椭圆 |
page.draw_path(path_commands, ...) | 画任意矢量路径 |
page.insert_svg(svg_string, ...) | 按 SVG 字符串绘制矢量图 |
这些操作都直接修改或追加到页面的内容流里,新增的文字和图片一般会被 get_text("blocks")
和 page.get_drawings()
(图片除外)检索到。
2. 添加注释/表单控件(Annotations & Widgets)
注释/表单控件(add_*_annot、add_link、add_widget) → 属于注释层,不在 get_text("blocks")
,需用相应注释 API 读取。
方法 | 功能 |
---|---|
page.add_redact_annot(rect, ...) | 创建涂改(redaction)注释 |
page.add_freetext_annot(rect, ...) | 创建可编辑的文本注释 |
page.add_highlight_annot(quads, ...) | 创建高亮注释(文字上方的标注) |
page.add_underline_annot(quads, ...) | 创建下划线注释 |
page.add_squiggly_annot(quads, ...) | 创建波浪线注释 |
page.add_strikeout_annot(quads, ...) | 创建删除线注释 |
page.add_stamp_annot(point, stamp_name) | 添加印章注释 |
page.add_popup_annot(rect, text, ...) | 添加弹出式注释 |
page.add_link(rect, uri="…") | 添加链接(跳转到 URL 或文档内位置) |
page.add_widget(widget_dict) | 添加或修改表单字段(文本框、下拉、复选) |
-
注释:不会加入正文内容流;需用
page.annots()
或特定 API(如.widgets()
)读取。 -
表单字段:交互式控件,既有注释属性,也能含有值;用
page.widgets()
管理。
1. 什么是“Block”
-
逻辑段落
一段连续的文字(如一段正文、一行标题),在 PDF 内容流中往往对应多个“span”(文本片段),PyMuPDF 将它们合并为一个 block,方便你一次性处理整段文字。 -
图片或矢量
除了文字,block 也可以表示一张图片或一段矢量图形,它们在版式上属于同一个区域。
可把 block 理解成“内容的容器”,每个容器包含:
-
矩形边界(bounding box)——
(x0, y0, x1, y1)
-
内容摘要——对于文本,是拼接后的字符串;对于图片/矢量,是空字符串或特殊标记。
-
类型标识——告诉你这是文本、图片还是其它。
2. 在 PyMuPDF 中如何获得 Blocks
blocks = page.get_text("blocks")
返回一个列表,每个元素都是一个 6 元组:
(x0, y0, x1, y1, content, block_type)
元素 | 含义 |
---|---|
x0, y0 | 坐标:左上角(PyMuPDF 坐标原点在页面左上,y 向下为正) |
x1, y1 | 坐标:右下角 |
content | 文本内容(若 block 是文本)。对于图片/矢量,通常为空字符串。 |
block_type | 类型编号: • 0 =文本• 1 =图片• 2 =矢量• 3 =其他注释(如表单) |
示例:
import fitz
doc = fitz.open("example.pdf")
page = doc[0]
blocks = page.get_text("blocks")
for b in blocks:
x0, y0, x1, y1, text, btype = b
print(f"- Block({btype}) @[{x0:.1f},{y0:.1f} → {x1:.1f},{y1:.1f}]:")
if btype == 0:
print(" 文本段落:", text[:30].replace("\n"," "), "…")
else:
print(" 非文本 block")
在 PyMuPDF 里,page.get_text("blocks")
只会把可提取的“文本块”和“图片块”当作 Block 返回,不会把 PDF 中的线条(table 边框)、矢量图形或者表单输入框当成 Block。具体来说:
-
block_type == 0
文本块,content
字段里是拼接好的字符串。PyMuPDF -
block_type == 1
图片块,content
字段里是一段图片元信息(位置、格式等),不是真正的像素数据。
如果你的 PDF 页面上有:
线条、表格边框、矩形(vector graphics)
路径、曲线(vector drawings)
可交互的表单字段(AcroForm input boxes)
这些都 不 会出现在
get_text("blocks")
的输出中。
要处理它们,你可以分别使用:
drawings = page.get_drawings()
get_drawings()
会返回一个列表,每项都是一个字典,详细描述该笔划(线段、曲线、多边形等)的类型、坐标、颜色、宽度……
表单字段(widgets)
for widget in page.widgets():
print(widget.field_name, widget.rect, widget.field_type)
page.widgets()
会让你遍历所有表单控件(包括文本框、下拉框、复选框等),并能读取或写入它们的值。
若你还需要处理“注释/高亮/红线标记”“链接”“真正的图片流”“附件”等,就要配合 page.annots()
、page.get_links()
、page.get_images()
、doc.attachments()
等 API 一起使用,才能拿到 PDF 中所有可视(或可交互)元素。
-----------------------------------------------
add_redact_annot
是 PyMuPDF(fitz
)中用来在页面上标记“涂改区域”(redaction annotation)的方法,它并不直接修改内容,而是创建一条注释,待你调用 apply_redactions()
时,再真正将标记区域内的原始内容移除,并可按注释设置绘制新的矩形和文字。下面分几部分详细介绍。
annot = page.add_redact_annot(
rect,
text=None,
fill=None,
text_color=None,
fontsize=None,
fontname=None,
align=None,
stroke_color=None,
overlay_text=False,
flags=0
)
使用步骤
打开文档
doc = fitz.open("template.pdf")
为每个要替换的区域添加 redact 注释
r = page.search_for("{{PLACEHOLDER}}")[0] # 找到第一个占位符
page.add_redact_annot(
r,
text="新文字",
fill=(1,1,1),
text_color=(0,0,0),
fontsize=r.height*0.8,
align=fitz.TEXT_ALIGN_LEFT
)
为每个要替换的区域添加 redact 注释
page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_NONE)
这一步会:
-
删除注释区域内所有原始文字、图形、图像
-
按注释的
fill
颜色填充背景 -
把注释里的
text
按fontname
、fontsize
、text_color
、align
绘制上去
保存并清理
doc.save("output.pdf", garbage=4, deflate=True)
doc.close()
返回值
返回一个 Annot
对象,代表这个 Redact 注释;真正的“涂改”效果要等你调用
page.apply_redactions()
。
参数详解
参数 | 类型 | 说明 |
---|---|---|
rect | Rect | 必需。红线注释的矩形区域;可用 fitz.Rect(x0, y0, x1, y1) 创建。也可传 Quad 或 Quad 列表以适应不规则排版。 |
text | str | 可选。涂改后要写入的新文字。若不提供,则仅覆盖,不输出文字。 |
fill | (r,g,b) 或 None | 可选。矩形内部填充色,RGB 三元组,取值 0~1;例如 (1,1,1) 表示白色。 |
stroke_color | (r,g,b) 或 None | 可选。矩形边框颜色;若不设置则无边框。 |
text_color | (r,g,b) | 可选。覆盖文字的颜色,默认黑色 (0,0,0) 。 |
fontsize | float | 可选。覆盖文字的大小(磅)。如果不设置,PyMuPDF 会尝试自动选择合适大小。 |
fontname | str | 可选。内置字体名称(如 "helv" 、"courier" )或外部字体路径。 |
align | 整数常量 | 可选。文字对齐方式:fitz.TEXT_ALIGN_LEFT 、_CENTER 、_RIGHT 、_JUSTIFY 。 |
overlay_text | bool | 可选。True 表示文字绘制在矩形之上;False 表示先画文字再覆盖矩形底色(通常设 False )。 |
flags | int | 可选。注释标志,通常不用改。 |
使
PyMuPDF库支持多种文档格式的内容读取,如PDF、XPS、CBZ等,支持将文档转换为其他格式,如HTML、SVG、PDF和CBZ等。
PyMuPDF可以修改pdf文件的内容。其他文件类型用PyMuPDF是只读的。但可以将任何文档(包括图像)转换为PDF(Document.convert_to_pdf()),然后将再使用PyMuPDF的功能进行操作。
参考文档:https://pymupdf.readthedocs.io/en/latest/page.html
文档操作
打开文档
open()没有参数时是打开新的文档,有参数时是加载指定文档
fitz和pymupdf 是同一个库,操作相同
import fitz # fitz就是PyMuPDF的别名
# import pymupdf # 同fitz
# new_pdf = pymupdf.open()
# pdf_document = pymupdf.open(pdf_path) # 打开文档,获取文档对象
new_pdf = fitz.open()
pdf_document = fitz.open(pdf_path) # 打开文档,获取文档对象
获取文档信息
print(pdf_document.metadata) # 获取文档信息
print(pdf_document.get_toc()) # 获取目录大纲
print(pdf_document.page_count) # 获取页数
文档信息如下:
{'format': 'PDF 1.7', 'title': '', 'author': '', 'subject': '', 'keywords': '7e1d6144af9e0ffb0HJ_0924E1RQy4S3U_uCQ-ernv_VMhNm', 'creator': 'Microsoft® Word 2021', 'producer': 'Microsoft® Word 2021; modified using iText® 5.5.13 ©2000-2018 iText Group NV (AGPL-version)', 'creationDate': "D:20240322202301+08'00'", 'modDate': "D:20240423092659+08'00'", 'trapped': '', 'encryption': None}
pdf_document.delete_page(-1)
删除页
delete_page 删除指定页,一次只删除一页,参数为对应页的索引
delete_pages 删除多页,传入参数如果为列表/元组/范围,可删除对应页,如果是两个整数则删除从第n页到第m页(关键字'from_page'/'to_page')
pdf_document.delete_pages((2,4,7))
pdf_document.delete_pages(3,5)
移动页
pdf_document.move_page(0,2) # move_page(n,m)将第n+1页移动到第m+1页,m默认为-1(最后一页)
选择重构合并
在列表中建立带有页码的子pdf。参数为需要重新创建指定页的页码列表,页码必须是在范围内,会根据列表中的顺序选择整合文档,这里演示只合并奇数页。
pdf_document.select([i for i in range(0,pdf_document.page_count,2)])
保存关闭
def save(
self,
filename,
garbage=0,
clean=0,
deflate=0,
deflate_images=0,
deflate_fonts=0,
incremental=0,
ascii=0,
expand=0,
linear=0,
no_new_id=0,
appearance=0,
pretty=0,
encryption=1,
permissions=4095,
owner_pw=None,
user_pw=None,
preserve_metadata=1,
use_objstms=0,
compression_effort=0,
):
pdf_document.save(rf'{save_img_path}\{pdf_file_name}-副本{int(time())}.pdf')
pdf_document.close()
页对象操作
插入页
pdf_document.new_page(3) # 指定位置插入新的空白页
内容读取
PyMuPDF支持将读取到的内容转为多种格式的数据,默认为text纯文本内容
"text":(默认)带换行符的纯文本(不包含格式、文字位置详细信息和图像)。
pdf_document = fitz.open(pdf_path) # 打开文档,获取文档对象
for page_num in range(len(pdf_document)):
page = pdf_document.load_page(page_num) # 获取页对象
text = page.get_text() # 获取页面文本内容
print(text)
"blocks":生成文本块(段落)的列表。
"words":生成不包含空格的字符串单词列表。
"html":创建包括任何图像的html数据。
def fitz_pdf(pdf_path):
pdf_document = fitz.open(pdf_path) # 打开文档,获取文档对象
for page_num in range(len(pdf_document)):
page = pdf_document.load_page(page_num) # 获取页对象
html = page.get_text("html") # 获取页面内容
with open(f'test-{page_num}.html', 'w') as f:
f.write(html)
pdf_document.close()
"dict" 或 "json":
"rawdict"或 "rawjson":包含XML之类字符详细信息的"dict"及"json"的超级集合。
"xhtml":包含图像及文本信息级别的html数据。
"xml":不包含图像,只有每个文本字符的完整位置和字体信息。
获取页对象的字体样式
page = pdf_document.load_page(page_num) # 获取页对象
print(page.get_fonts()) # 获取字体样式
[(14, 'ttf', 'TrueType', 'BCDEEE+Cambria', 'F1', 'WinAnsiEncoding'), (15, 'ttf', 'Type0', 'BCDFEE+MS-Gothic', 'F2', 'Identity-H'), (16, 'ttf', 'Type0', 'BCDGEE+MicrosoftYaHei', 'F3', 'Identity-H'), (17, 'n/a', 'TrueType', 'ArialMT', 'F4', 'WinAnsiEncoding'), (18, 'ttf', 'Type0', 'BCDHEE+SimHei', 'F5', 'Identity-H'), (19, 'ttf', 'Type0', 'BCDIEE+MicrosoftYaHei-Bold', 'F6', 'Identity-H'), (20, 'ttf', 'TrueType', 'BCDJEE+SimHei', 'F7', 'WinAnsiEncoding'), (21, 'ttf', 'TrueType', 'BCDKEE+MicrosoftYaHei', 'F8', 'WinAnsiEncoding'), (22, 'ttf', 'TrueType', 'BCDLEE+Cambria-Bold', 'F9', 'WinAnsiEncoding'), (23, 'n/a', 'TrueType', 'Arial-BoldMT', 'F10', 'WinAnsiEncoding'), (24, 'ttf', 'Type0', 'BCDMEE+Wingdings-Regular', 'F11', 'Identity-H'), (25, 'ttf', 'TrueType', 'BCDNEE+ArialUnicodeMS', 'F12', 'WinAnsiEncoding'), (26, 'ttf', 'Type0', 'BCDOEE+ArialUnicodeMS', 'F13', 'Identity-H'), (1, 'n/a', 'Type1', 'Helvetica', 'Xi0', 'WinAnsiEncoding')]
插入文本标签
page.add_text_annot((50, 150), f'文本便利贴测试,这是{page_num + 1}页')
插入文本内容
字体设置
如果写入内容时不指定字体时,中文内容会乱码。
内置字体:china-s 黑体 china-ss 宋体 china-t 繁体黑体 china-ts 繁体宋体。
自定义字体添加如下,很多网上分享者都用 fitz.Font() 添加,根本没有用。
page.insert_font(fontname="三极妙漫体",
fontfile=r"C:\Users\DELL\AppData\Local\JianyingPro\三极妙漫体.ttf",
fontbuffer=None, set_simple=False) # 自定义字体添加
insert_text添加文本
def insert_text(
self,
point: point_like,
buffer_: typing.Union[str, list],
fontsize: float = 11,
lineheight: OptFloat = None,
fontname: str = "helv",
fontfile: OptStr = None,
set_simple: bool = 0,
encoding: int = 0,
color: OptSeq = None,
fill: OptSeq = None,
render_mode: int = 0,
border_width: float = 1,
rotate: int = 0,
morph: OptSeq = None,
stroke_opacity: float = 1,
fill_opacity: float = 1,
oc: int = 0,
) -> int:
page.insert_text((50, 50), "这是中文测试", fontsize=15, fontname='china-s')
insert_textbox添加文本
def insert_textbox(
self,
rect: rect_like,
buffer: typing.Union[str, list],
fontname: OptStr = "helv",
fontfile: OptStr = None,
fontsize: float = 11,
lineheight: OptFloat = None,
set_simple: bool = 0,
encoding: int = 0,
color: OptSeq = None,
fill: OptSeq = None,
expandtabs: int = 1,
border_width: float = 0.05,
align: int = 0,
render_mode: int = 0,
rotate: int = 0,
morph: OptSeq = None,
stroke_opacity: float = 1,
fill_opacity: float = 1,
oc: int = 0,
) -> float:
text_rect = fitz.Rect(80, 80, 500, 100) # 定义文本框位置
page.insert_textbox(text_rect, "测试文本框添加操作", fontsize=12,
align=fitz.TEXT_ALIGN_LEFT, fontname='三极妙漫体',
fill=(200 / 255, 250 / 255, 100 / 255), rotate=90, fill_opacity=.2)
插入图片
insert_image(rect, *, alpha=-1, filename=None, height=0, keep_proportion=True, mask=None, oc=0, overlay=True, pixmap=None, rotate=0, stream=None, width=0, xref=0)
img_rect = fitz.Rect((50, 50, 150, 100))
page.insert_image(img_rect, filename=r'E:\桌面\99\测试图片\1.jpg') # 可设置位置和图片大小
获取页面注释、链接、表单字段
for ant in page.annots(): # 获取注释
print(ant)
for link in page.links(): # 获取链接
print(link)
for widget in page.widgets(): # 获取表单字段
print(widget)
获取页面RGB图像数据并将页面保存为图片
get_pixmap(*, matrix=pymupdf.Identity, dpi=None, colorspace=pymupdf.csRGB, clip=None, alpha=False, annots=True)
获取页面RGB图像,参数包含分辨率、颜色空间(可生成灰度图像或具有减色方案的图像)、透明度、旋转、镜像、移位、剪切等。可设置宽度、高度等。
mat = fitz.Matrix(1, 1) # 这里可以调整缩放比例,zoom_x 和zoom_y
pix = page.get_pixmap(matrix=mat,alpha=0) # alpha=0 白色背景
pix.save('test.png')
获取页面的矢量图(转svg)
svg_img = page.get_svg_image()
with open('test.svg', 'w') as f:
f.write(svg_img)
创建新页面
pdf_document.new_page()
图片提取
import fitz # fitz就是PyMuPDF的别名
def get_save_image(pdf_path):
pdf_document = fitz.open(pdf_path) # 打开文档,获取文档对象
pdf_file_name = pdf_path.split('\\')[-1].split('.')[0]
save_img_path = pdf_path.replace(pdf_path.split('\\')[-1], '')
for page_num in range(len(pdf_document)):
for img in pdf_document.get_page_images(page_num):
xref = img[0]
pix = fitz.Pixmap(pdf_document, xref)
if pix.n < 5: # GRAY or RGB
# print(pix.width,pix.height)
pix.save("%s%s-page%s-%s.png" % (save_img_path, pdf_file_name, page_num, xref))
else: # CMYK: convert to RGB first
pix1 = fitz.Pixmap(fitz.csRGB, pix)
pix1.save("%s%s-page%s-%s.png" % (save_img_path, pdf_file_name, page_num, xref))
pdf_document.close()
修改或清空匹配内容
def replace_pdftext(pdf_path, search_text, is_empty: bool = False, replacement_text: str = None):
pdf_document = fitz.open(pdf_path) # 打开文档,获取文档对象
for page_num in range(len(pdf_document)):
page = pdf_document.load_page(page_num) # 获取页对象,或pdf_document[page_num]
text_instances = page.search_for(search_text) # 搜索关键字所在位置
if text_instances:
info = page.get_text('dict')
for block in info['blocks']:
# 图片不存在lines,可以通过是否存在lines确定处理文本和图片
if block.get('lines'): # 图片没有lines,不判断时会报错
for line in block.get('lines'):
for span in line['spans']:
if search_text == span.get('text'):
if is_empty and not replacement_text:
page.add_redact_annot(span['bbox']) # 清除匹配到文本
page.apply_redactions()
if search_text != span.get('text') and search_text in span.get('text'):
if is_empty and not replacement_text:
replacement_text = ' ' * len(search_text)
new_text = span['text'].replace(search_text, replacement_text)
font = span.get('font') # 获取字体
if 'SimHei' in font:
fontname = 'china-s'
elif 'SimSun' in font:
fontname = 'china-ss'
else:
fontname = 'china-s'
# 方法1:使用遮罩替换原内容
page.add_redact_annot(span['bbox'], new_text, fontname=fontname,
fontsize=span['size'], align=fitz.TEXT_ALIGN_LEFT, fill=(1, 1, 1),
text_color=(0, 0, 0), cross_out=True) # 清除匹配到文本
page.apply_redactions()
# 方法2:通过在原内容上添加同背景色的矩形再插入新的文字内容,看不见数据,但是通过查询还是可以查到数据
page.draw_rect(span['bbox'], color=(1, 1, 1), fill=(1, 1, 1),
width=0) # 绘制白色无框矩形遮罩,可根据具体颜色设置遮罩颜色
page.insert_text((span['bbox'][0], span['bbox'][-1]), new_text, fontname=fontname,
fontsize=span['size'], color=(0, 0, 0, 1),
fill=None, render_mode=0, border_width=1, rotate=0, morph=None,
overlay=True)
pdf_document.save(rf'{pdf_path.split(".")[0]}-副本{int(time())}.pdf')
pdf_document.close()
修改图片
修改图片时无法直接将目标图片替换成和原pdf图片一样的大小,会出现空白,边框等,这里使用PIL单独写了一个方法将目标图片修改成和要求一样的大小。
import fitz
from PIL import Image
import os
def change_img_size(img_name, w, h):
new_img_name = fr'{img_name.split(".")[0]}-new.{img_name.split(".")[-1]}'
img = Image.open(img_name)
new_img = img.resize((w, h))
new_img.save(new_img_name)
return new_img_name
def replace_pdf_img(pdf_path, replace_img_path: list = None):
pdf_document = fitz.open(pdf_path) # 打开文档,获取文档对象
for page_num in range(len(pdf_document)):
page = pdf_document.load_page(page_num) # 获取页对象,或pdf_document[page_num]
info = page.get_text('dict')
img_num = len(page.get_images())
if len(replace_img_path) >= img_num:
i = 0
for block in info['blocks']:
# 图片不存在lines,可以通过是否存在lines确定处理文本和图片
if not block.get('lines'): # 图片没有lines,不判断时会报错
page.add_redact_annot(block['bbox']) # 给原图片位置设置遮罩
page.apply_redactions() # 清空原图片
image_name = change_img_size(replace_img_path[i], block['width'],
block['height']) # 修改图片大小为指定位置图片大小
img_rect = fitz.Rect(block['bbox'])
pix = fitz.Pixmap(image_name)
page.insert_image(img_rect, pixmap=pix)
os.remove(image_name)
i += 1
else:
print(
rf'第{page_num + 1}页共有{img_num}张图片,当前图片数量不够,还需要增加{img_num - len(replace_img_path)}张')
pdf_document.save(rf'{pdf_path.split(".")[0]}-副本{int(time())}.pdf')
pdf_document.close()