pypdf 添加 PDF 注释(Annotations)完全指南:FreeText、图形、链接与高亮
【免费下载链接】pypdfA pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files项目地址: https://gitcode.com/GitHub_Trending/py/pypdf
本指南以 pypdf 官方文档 docs/user/adding-pdf-annotations.md 为骨架,系统讲解如何使用pypdf在 PDF 页面中创建附件、自由文本、线段、折线、矩形、椭圆、多边形、弹出窗、内外链接与文本高亮等注释。读完本文,你将掌握pypdf.annotations模块中所有公开注释类的构造参数、PdfWriter.add_annotation()的调用方式,以及颜色、字体、AnnotationFlag 等底层 PDF 对象的工作原理,可直接在真实项目中落地使用。
核心前置知识:注释字典、颜色与可见性
PDF 中的注释(annotation)本质上是一个字典对象(Dictionary Object)。pypdf 的pypdf/annotations/__init__.py明确说明:所有注释类型的内核都是DictionaryObject,因此如果 pypdf 没有实现某个特性,你可以直接像操作字典一样扩展已生成的功能。
所有注释类都继承自AnnotationDictionary(见 pypdf/annotations/_base.py),其构造函数自动写入:
/Type=/Annot:标识这是一个注释对象;- 默认
flags为 0,即没有任何标记。构造函数刻意不添加 flags,如果你需要改变默认行为,请使用flags属性(见下文"AnnotationFlag"小节)。
颜色/C条目:为什么有些注释"看不见"
官方文档特别提醒了一个常见陷阱:默认情况下,某些注释可能是不可见的,例如 PolyLine(折线),因为默认颜色是"transparent"(透明)。
解决办法是显式给注释添加/C条目,它是一个数组,每个元素取值在0.0到1.0之间:
- 1 个元素:灰度值(grayscale);
- 3 个元素:RGB 颜色定义;
- 4 个元素:CMYK 颜色定义。
例如给折线设置橙红色(RGB 0.9, 0.1, 0):
annotation[NameObject("/C")] = ArrayObject( [FloatObject(0.9), FloatObject(0.1), FloatObject(0)] )从源码看,pypdf/annotations/_markup_annotations.py中的hex_to_rgb工具函数负责把"00ff00"这类十六进制颜色字符串解析为 RGB 浮点数组(每个分量除以 255),最终写入/C或/IC条目,因此多数注释类(FreeText、Highlight、Rectangle、Ellipse)的十六进制颜色参数在底层都会被转换为/C数组。
AnnotationFlag:控制注释的显示与交互
AnnotationFlag定义在 pypdf/constants.py,对应 PDF 规范 §12.5.3 "Annotation Flags",是一个IntFlag,支持按位组合:
| 标志 | 值 | 含义 |
|---|---|---|
INVISIBLE | 1 | 不显示注释,除非开启显示隐藏注释选项 |
HIDDEN | 2 | 不显示,也不打印 |
PRINT | 4 | 打印时显示注释 |
NO_ZOOM | 8 | 页面缩放时不缩放注释外观 |
NO_ROTATE | 16 | 页面旋转时不旋转注释外观 |
NO_VIEW | 32 | 屏幕上不显示 |
READ_ONLY | 64 | 禁止用户交互 |
LOCKED | 128 | 锁定,禁止删除或修改 |
TOGGLE_NO_VIEW | 256 | 反向切换 NO_VIEW |
LOCKED_CONTENTS | 512 | 锁定注释内容 |
典型用法:把注释标记为"可打印"(annotation.flags = AnnotationFlag.PRINT),或组合多个标志如AnnotationFlag.PRINT | AnnotationFlag.READ_ONLY。flags属性定义在AnnotationDictionary上(见 pypdf/annotations/_base.py),读取时返回AnnotationFlag类型,写入时转换为/F数值条目。
将注释写入 PDF 的统一入口:add_annotation()
无论创建哪种注释,最终都通过PdfWriter.add_annotation()写入文档。其签名与行为见 pypdf/_writer.py:
writer.add_annotation(page_number, annotation) -> DictionaryObject要点:
page_number可以传**页面索引(int)**或PageObject;- 注释必须新建,不能复用/回收旧注释;
- 方法会为注释自动写入
/P(父页面引用)、把注释追加到页面的/Annots数组,并通过self._add_object()注册为间接对象; - 返回被插入的注释对象——这个返回值很重要,创建 Popup 弹出窗时必须使用它(见下文);
- 内部链接注释(
/Subtype == "/Link"且含/Dest)会被自动转换为Destination目标数组,供阅读器跳转; - Popup 注释会自动把自身引用写入父注释的
/Popup条目。
测试 tests/test_annotations.py 中所有用例(test_free_text、test_link、test_popup等)均验证了这一调用链路。
Attachments:把任意文件附加到 PDF
附件注释(FileAttachment)是 PDF 文档中嵌入文件的通用方式。官方示例:
from pypdf import PdfWriter writer = PdfWriter() writer.add_blank_page(width=200, height=200) data = b"any bytes - typically read from a file" writer.add_attachment("smile.png", data) writer.write("out-attachment.pdf")add_attachment与注释的关系在于:附件在 PDF 内部以 FileAttachment 形式关联(读取侧可通过/Subtype == "/FileAttachment"的注释取回文件数据,见 docs/user/reading-pdf-annotations.md 中的读取示例)。writer.add_blank_page(width=200, height=200)用于先创建一页空白页承载附件。
FreeText:在矩形框中添加自由文本
FreeText 注释用于在页面上放置一段富文本,效果见 free-text-annotation.png。构造参数(源码见 pypdf/annotations/_markup_annotations.py):
| 参数 | 默认值 | 说明 |
|---|---|---|
text | 必填 | 注释文本内容,支持\n换行 |
rect | 必填 | 矩形区域(xLL, yLL, xUR, yUR) |
font | "Helvetica" | 字体名称 |
bold | False | 是否加粗 |
italic | False | 是否斜体 |
font_size | "14pt" | 字号(字符串,如"20pt") |
font_color | "000000" | 字体颜色(十六进制,如"00ff00") |
border_color | "000000" | 边框颜色;传None则无边框(写入/BS且宽度为 0) |
background_color | "ffffff" | 背景颜色;传None则透明 |
官方完整示例:
from pypdf import PdfReader, PdfWriter from pypdf.annotations import FreeText from pypdf.constants import AnnotationFlag # Fill the writer with the pages you want reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) # Create the annotation and add it annotation = FreeText( text="Hello World\nThis is the second line!", rect=(50, 550, 200, 650), font="Arial", bold=True, italic=True, font_size="20pt", font_color="00ff00", border_color="0000ff", background_color="cdcdcd", ) # Mark the annotation as printable. # See "AnnotationFlag" for other options, e.g. hidden etc. annotation.flags = AnnotationFlag.PRINT writer.add_annotation(page_number=0, annotation=annotation) # Write the annotated file to disk writer.write("out-free-text.pdf")底层原理:FreeText 构造函数把italic/bold/font_size/font/font_color拼装成 CSS2 风格的富文本字符串写入/DS条目(参照 PDF 1.7 规范 Table 225 "CSS2 style attributes used in rich text strings"),测试 tests/test_annotations.py 直接断言了生成的/DS值,例如italic bold 20pt Arial;text-align:left;color:#00ff00。边框颜色经hex_to_rgb转为r g b rg字符串写入/DA(默认外观);border_color=None时写入/BS且宽度为 0(即无边框);背景颜色写入/C数组。
Text:经典文本注释
Text 注释是 PDF 中最常见的"便签"式注释,形如一个小图标,点击后显示文本,效果见 text-annotation.png。构造参数(pypdf/annotations/_markup_annotations.py):rect(可点击区域)、text(内容)、open(默认False,是否默认展开)、flags(默认 0)。示例:
from pypdf import PdfReader, PdfWriter from pypdf.annotations import Text reader = PdfReader("crazyones.pdf") writer = PdfWriter() writer.add_page(reader.pages[0]) annotation = Text( text="Hello World\nThis is the second line!", rect=(50, 550, 200, 650), open=True, ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-text.pdf")底层会写入/Subtype=/Text、/Rect、/Contents、/Open与/Flags条目。
Line:两点连线
Line 注释用于绘制一条直线,效果见 annotation-line.png。关键参数:p1、p2(两个端点坐标)以及rect(包围矩形)、text(可选说明文字,默认""):
from pypdf import PdfReader, PdfWriter from pypdf.annotations import Line reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) # Add the line annotation = Line( text="Hello World\nLine2", rect=(50, 550, 200, 650), p1=(50, 550), p2=(200, 650), ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-line.pdf")源码层面(pypdf/annotations/_markup_annotations.py),Line 会额外写入:
/L:端点数组[p1.x, p1.y, p2.x, p2.y];/LE:两端线帽样式,默认[/None, /None](无箭头);/IC:默认填充色[0.5, 0.5, 0.5](中灰色,可选改);/Contents:说明文本。
PolyLine:多段折线
PolyLine 绘制连续折线,效果见 annotation-polyline.png。注意:官方文档强调默认颜色是透明的,必须显式设置/C颜色:
from pypdf import PdfReader, PdfWriter from pypdf.annotations import PolyLine from pypdf.generic import ArrayObject, FloatObject, NameObject reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) # Add the polyline # By default, the line will be transparent. Set an explicit color. annotation = PolyLine( vertices=[(50, 550), (200, 650), (70, 750), (50, 700)], ) annotation[NameObject("/C")] = ArrayObject( [FloatObject(0.9), FloatObject(0.1), FloatObject(0)] ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-polyline.pdf")源码要点(pypdf/annotations/_markup_annotations.py):
vertices为空列表时抛出ValueError;/Vertices按x1, y1, x2, y2, ...扁平化写入;/Rect自动由_get_bounding_rectangle()根据所有顶点计算包围盒,因此无需手动传rect。
Rectangle:矩形框
Rectangle 绘制矩形,效果见 annotation-square.png。其 PDF 子类型实际是/Square——pypdf 故意命名为Rectangle是因为 PDF 规范中的 "Square" 注释并不要求是正方形(见 pypdf/annotations/init.py 模块文档):
from pypdf import PdfReader, PdfWriter from pypdf.annotations import Rectangle reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) # Add the rectangle annotation = Rectangle( rect=(50, 550, 200, 650), ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-rectangle.pdf")如需填充,使用interiour_color="ff0000"参数(源码参数名为interior_color,见 pypdf/annotations/_markup_annotations.py,十六进制颜色会被转换为/IC数组):
annotation = Rectangle( rect=(50, 550, 200, 650), interior_color="ff0000", )Ellipse:椭圆与圆
Ellipse 绘制椭圆(圆是长宽相等的椭圆),效果见 annotation-circle.png。其 PDF 子类型为/Circle:
from pypdf import PdfReader, PdfWriter from pypdf.annotations import Ellipse reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) annotation = Ellipse( rect=(50, 550, 200, 650), ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-ellipse.pdf")与 Rectangle 相同,也支持interior_color="..."填充参数(写入/IC)。
Polygon:多边形
Polygon 绘制闭合多边形,效果见 annotation-polygon.png:
from pypdf import PdfReader, PdfWriter from pypdf.annotations import Polygon reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) # Add the line annotation = Polygon( vertices=[(50, 550), (200, 650), (70, 750), (50, 700)], ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-polygon.pdf")源码(pypdf/annotations/_markup_annotations.py)中 Polygon 与 PolyLine 类似:空vertices抛ValueError、自动计算/Rect包围盒,并额外写入/IT = /PolygonCloud(标注该多边形可作为"云朵"形状)。Polygon 与 PolyLine 的区别在于闭合与否以及/IT条目。
Popup:弹出窗
Popup 注释用于管理标记注释的弹出窗口,效果见 annotation-popup.png。关键要求:必须使用add_annotation()的返回值作为parent,因为返回的是已注册的父注释:
from pypdf import PdfWriter from pypdf.annotations import Popup, Text # Arrange writer = PdfWriter() writer.append("crazyones.pdf", [0]) # Act text_annotation = writer.add_annotation( 0, Text( text="Hello World\nThis is the second line!", rect=(50, 550, 200, 650), open=True, ), ) popup_annotation = Popup( rect=(50, 550, 200, 650), open=True, parent=text_annotation, # use the output of add_annotation ) writer.write("out-popup.pdf")底层行为(pypdf/_writer.py 与 pypdf/annotations/_non_markup_annotations.py):
- Popup 构造时通过
parent.indirect_reference写入/Parent条目;若 parent 未注册(没有indirect_reference),会触发logger_warning,且不设置 Parent 字段; add_annotation检测到/Subtype == "/Popup"且含/Parent时,会自动把 popup 的引用写回父注释的/Popup条目,建立双向关联。
Link:外部链接与内部跳转
Link 注释支持两类目标:外部 URL与文档内部页面跳转,效果与常规可点击链接一致。
外部链接
from pypdf import PdfReader, PdfWriter from pypdf.annotations import Link reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) # Add the link annotation = Link( rect=(50, 550, 200, 650), url="https://martin-thoma.com/", ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-link.pdf")内部链接
内部链接通过target_page_index指定目标页,并用Fit控制跳转后的视图:
from pypdf import PdfReader, PdfWriter from pypdf.annotations import Link from pypdf.generic import Fit reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) # Add the link annotation = Link( rect=(50, 550, 200, 650), target_page_index=3, fit=Fit(fit_type="/FitH", fit_args=(123,)), ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-internal-link.pdf")源码要点(pypdf/annotations/_non_markup_annotations.py):
url与target_page_index必须且只能提供一个,否则抛ValueError;- 外部链接写入
/Aaction 字典(/S=/URI、/Type=/Action、/URI=...); - 内部链接先把目标暂存为"延迟字典",由
add_annotation内部结合Fit转换为真正的Destination目标数组(pypdf/_writer.py); Fit类(pypdf/generic/_fit.py)支持多种视图方式:Fit(fit_type="/FitH", fit_args=(123,))表示以纵坐标 123 为基准水平适配;此外还提供Fit.xyz()、Fit.fit()、Fit.fit_horizontally()等便捷类方法,fit_args中的None会被转换为NullObject。
Text Markup Annotations:高亮等文本标记
文本标记注释(Text Markup Annotations)用于标记文档中的一段具体文本,比上述注释更复杂:你必须知道文本的确切位置——即所谓的QuadPoints(四边形点)。该类注释的 PDF 子类型包括 Highlight、Underline、Squiggly、StrikeOut 等,pypdf 目前公开实现的是Highlight。
Highlight:文本高亮
效果见 annotation-highlight.png:
from pypdf import PdfReader, PdfWriter from pypdf.annotations import Highlight from pypdf.generic import ArrayObject, FloatObject reader = PdfReader("crazyones.pdf") page = reader.pages[0] writer = PdfWriter() writer.add_page(page) rect = (50, 550, 200, 650) quad_points = [rect[0], rect[1], rect[2], rect[1], rect[0], rect[3], rect[2], rect[3]] # Add the highlight annotation = Highlight( rect=rect, quad_points=ArrayObject([FloatObject(quad_point) for quad_point in quad_points]), ) writer.add_annotation(page_number=0, annotation=annotation) writer.write("out-highlight.pdf")参数说明(pypdf/annotations/_markup_annotations.py):
rect:高亮区域的包围矩形;quad_points:ArrayObject,一组四边形坐标。PDF 规范要求四边形按"左上、右上、左下、右下"顺序给出 8 个浮点数,上例用rect的坐标顺序[x1,y1, x2,y1, x1,y2, x2,y2]构造;highlight_color:默认"ff0000"(红色),十六进制字符串,会被转换为/C颜色数组;printing:默认False;置True时自动设置AnnotationFlag.PRINT。
与读取侧配合:验证与闭环
创建的注释可通过 docs/user/reading-pdf-annotations.md 中的通用读取方式验证。PDF 2.0 定义了 Text、Link、FreeText、Line、Square、Circle、Polygon、PolyLine、Highlight、Underline、Squiggly、StrikeOut、Popup、FileAttachment 等二十余种注释类型,读取代码按/Annots遍历即可:
from pypdf import PdfReader reader = PdfReader("example.pdf") for page in reader.pages: if "/Annots" in page: for annotation in page["/Annots"]: obj = annotation.get_object() print({"subtype": obj["/Subtype"], "location": obj["/Rect"]})创建侧与读取侧形成闭环:例如 PolyLine 写入的/C颜色数组、FreeText 写入的/DS富文本、Link 写入的/Aaction、Highlight 写入的/QuadPoints,都能在读取侧以同样的字典键访问(obj["/Contents"]、obj["/QuadPoints"]、obj["/FS"]等),这也是AnnotationDictionary本质是DictionaryObject的实践体现。
小结
- 所有注释统一通过
PdfWriter.add_annotation(page_number, annotation)写入,返回的注释对象可用于 Popup 关联; - pypdf 提供
Text、FreeText、Line、PolyLine、Rectangle、Ellipse、Polygon、Popup、Link、Highlight十种公开注释类(见 pypdf/annotations/init.py 的__all__); - 颜色统一使用十六进制字符串(如
"ff0000"),底层经hex_to_rgb转为 0.0~1.0 的浮点数组写入/C或/IC;PolyLine 等默认透明,必须手动设置颜色; - 交互与显示行为通过
AnnotationFlag(/F条目)控制,如PRINT、READ_ONLY、HIDDEN; - 文本标记注释(Highlight)需要精确的 QuadPoints,建议结合文本提取工具先定位目标文本坐标;
- 所有示例均可在 pypdf 仓库的 tests/test_annotations.py 中找到对应的自动化测试用例,作为可复现的运行参考。
【免费下载链接】pypdfA pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files项目地址: https://gitcode.com/GitHub_Trending/py/pypdf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考