news 2026/9/16 3:19:35

pypdf 添加 PDF 注释(Annotations)完全指南:FreeText、图形、链接与高亮

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pypdf 添加 PDF 注释(Annotations)完全指南:FreeText、图形、链接与高亮

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.01.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,支持按位组合:

标志含义
INVISIBLE1不显示注释,除非开启显示隐藏注释选项
HIDDEN2不显示,也不打印
PRINT4打印时显示注释
NO_ZOOM8页面缩放时不缩放注释外观
NO_ROTATE16页面旋转时不旋转注释外观
NO_VIEW32屏幕上不显示
READ_ONLY64禁止用户交互
LOCKED128锁定,禁止删除或修改
TOGGLE_NO_VIEW256反向切换 NO_VIEW
LOCKED_CONTENTS512锁定注释内容

典型用法:把注释标记为"可打印"(annotation.flags = AnnotationFlag.PRINT),或组合多个标志如AnnotationFlag.PRINT | AnnotationFlag.READ_ONLYflags属性定义在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_texttest_linktest_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"字体名称
boldFalse是否加粗
italicFalse是否斜体
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。关键参数:p1p2(两个端点坐标)以及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
  • /Verticesx1, 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 类似:空verticesValueError、自动计算/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):

  • urltarget_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_pointsArrayObject,一组四边形坐标。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 提供TextFreeTextLinePolyLineRectangleEllipsePolygonPopupLinkHighlight十种公开注释类(见 pypdf/annotations/init.py 的__all__);
  • 颜色统一使用十六进制字符串(如"ff0000"),底层经hex_to_rgb转为 0.0~1.0 的浮点数组写入/C/IC;PolyLine 等默认透明,必须手动设置颜色;
  • 交互与显示行为通过AnnotationFlag/F条目)控制,如PRINTREAD_ONLYHIDDEN
  • 文本标记注释(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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 3:19:25

3000元旗舰机选购指南:准旗舰芯片与隐形配置实战解析

1. 涨价潮前的“临界采购窗口”:为什么3000元是此刻最理性的旗舰机决策锚点最近两周,我陆续接到七八个朋友的微信咨询,开头几乎一模一样:“兄弟,听说骁龙8 Gen3和天玑9300的旗舰机要集体涨价了,现在下手还来…

作者头像 李华
网站建设 2026/9/16 3:18:47

同一把 TaoToken Key,从 Claude 切到 Gemini 救急 Windsurf

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:15:38

Raft共识算法原理与工程实践:从选举到KV存储落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:15:25

51单片机温度显示与报警系统设计:从DS18B20到Proteus仿真与实物调试

简介:面向51单片机课程设计与毕业设计场景,这份资料提供了一套基于DS18B20的数字温度显示与超限报警系统参考实现。项目以汇编语言编写核心测温与显示逻辑,实时驱动1602液晶呈现温度,并通过定时器中断配合阈值判断完成报警&#x…

作者头像 李华
网站建设 2026/9/16 3:15:00

Redis核心场景实战:从缓存穿透到分布式锁的15个案例

Redis这玩意儿,我前后用了快十年。从最早只是拿它做某个后台模块的本地缓存,到后来在微服务架构里当分布式锁、扛排行榜、处理延迟任务,一路踩过的坑确实不少。一开始我也觉得它无非就是个厉害点的HashMap,但用久了才意识到&#…

作者头像 李华