Skip to content

ENH: Add /IRT (in-reply-to) support for markup annotations#3631

Merged
stefan6419846 merged 12 commits intopy-pdf:mainfrom
costajohnt:feat/add-irt-annotation-support-2065
Mar 4, 2026
Merged

ENH: Add /IRT (in-reply-to) support for markup annotations#3631
stefan6419846 merged 12 commits intopy-pdf:mainfrom
costajohnt:feat/add-irt-annotation-support-2065

Conversation

@costajohnt
Copy link
Copy Markdown
Contributor

Summary

Adds first-class support for PDF annotation replies (PDF spec 1.5, Table 170) to the MarkupAnnotation base class.

New parameters on MarkupAnnotation

  • in_reply_to: Reference to a parent annotation (the object returned by writer.add_annotation()). Sets the /IRT entry.
  • reply_type: Either "R" (reply, default) or "Group" (grouped). Sets the /RT entry.
  • annotation_name: Sets the /NM entry for annotation threading. Auto-generated as a UUID when in_reply_to is set and no name is provided.

Usage example

from pypdf import PdfWriter
from pypdf.annotations import Text

writer = PdfWriter()
writer.add_blank_page(width=612, height=792)

# Create parent annotation
parent = Text(text="Original comment", rect=(50, 700, 200, 750))
parent_ref = writer.add_annotation(0, parent)

# Create reply annotation
reply = Text(
    text="Reply to the comment",
    rect=(50, 700, 200, 750),
    in_reply_to=parent_ref,
)
writer.add_annotation(0, reply)

with open("annotated.pdf", "wb") as f:
    writer.write(f)

Design notes

  • Follows the same pattern as Popup(parent=...) for referencing other annotations via indirect_reference
  • Warns (without crashing) when in_reply_to receives an unregistered annotation object, matching the existing Popup behavior
  • All existing MarkupAnnotation subclasses require rect, so the workaround discovered in the issue thread (reply annotations need /Rect to be visible) is handled naturally

Tests

5 new tests covering:

  • Basic reply with /IRT, /RT, /NM verification + round-trip read
  • reply_type="Group" variant
  • Explicit annotation_name without in_reply_to
  • Custom annotation_name with in_reply_to
  • Graceful warning for unregistered parent annotations

Closes #2065

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.36%. Comparing base (6d0fa2f) to head (e3b8311).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3631   +/-   ##
=======================================
  Coverage   97.36%   97.36%           
=======================================
  Files          55       55           
  Lines        9925     9942   +17     
  Branches     1817     1823    +6     
=======================================
+ Hits         9663     9680   +17     
  Misses        152      152           
  Partials      110      110           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@costajohnt costajohnt force-pushed the feat/add-irt-annotation-support-2065 branch from 9f0ca7e to b65774c Compare February 8, 2026 07:13
Comment thread pypdf/annotations/_markup_annotations.py Outdated
Comment thread pypdf/annotations/_markup_annotations.py Outdated
Comment thread pypdf/annotations/_markup_annotations.py Outdated
Comment thread pypdf/annotations/_markup_annotations.py Outdated
Comment thread pypdf/annotations/_markup_annotations.py Outdated
Comment thread tests/test_annotations.py Outdated
Comment thread tests/test_annotations.py Outdated
@costajohnt
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! I've addressed all 7 comments in the latest commit:

  1. Accept IndirectObject directlyin_reply_to now accepts both DictionaryObject and IndirectObject, so you can reply to an object by its reference directly.

  2. Literal["R", "Group"] — Replaced the raw str type with a proper Literal type for reply_type.

  3. Raise ValueError instead of logging — Unregistered in_reply_to now raises ValueError immediately rather than silently logging and continuing without setting /IRT.

  4. Only write /RT when not the default — Per PDF spec Table 170, /RT defaults to /R, so we now only write it explicitly when reply_type="Group".

  5. /NM scoping — The auto-generated /NM assignment stays properly scoped inside the in_reply_to block.

  6. Simplified test setup — Using PdfWriter(clone_from=...) directly instead of the manual PdfReaderadd_page flow.

  7. BytesIO instead of disk I/O — Tests now use in-memory buffers instead of writing to disk.

@stefan6419846
Copy link
Copy Markdown
Collaborator

Could you please have a look at the coverage?

  1. Only write /RT when not the default — Per PDF spec Table 170, /RT defaults to /R, so we now only write it explicitly when reply_type="Group".

I am not aware of having requested such a change. We should write it in all cases, even if it is considered the default here.

@costajohnt
Copy link
Copy Markdown
Contributor Author

Good catch — reverted to always writing /RT explicitly. The conditional was my own addition, not from your feedback. This should also resolve the coverage gap since the removed branch was the uncovered path.

Copy link
Copy Markdown
Collaborator

@stefan6419846 stefan6419846 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay. I have added some further remarks I stumbled upon.

Comment thread pypdf/annotations/_markup_annotations.py Outdated
Comment thread pypdf/annotations/_markup_annotations.py Outdated
Comment thread tests/test_annotations.py Outdated
Comment thread tests/test_annotations.py Outdated
Comment thread tests/test_annotations.py Outdated
Comment thread tests/test_annotations.py Outdated
Comment thread tests/test_annotations.py Outdated
@costajohnt
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough follow-up review! Addressed all points in the latest push:

  1. Simplified in_reply_to docstring — removed the concrete type names (DictionaryObject, IndirectObject) since they're already in the type hint. Added a Sphinx :meth: cross-reference for add_annotation().

  2. Simplified annotation_name docstring — removed the /NM and UUID implementation details. Users just need to know it's auto-generated when omitted with in_reply_to.

  3. & 4. Exact test assertions — replaced the presence-only checks ("/IRT" in reply_obj) with value assertions: /IRT now verifies it resolves to the correct parent via /Contents, and /NM checks the exact value survives the write/read cycle.

  4. Removed unnecessary write — dropped the BytesIO write from the group type test since it wasn't being re-read.

  5. Test naming — renamed all 6 test functions to use test_markup_annotation_* prefix.

  6. Docstrings — converted inline # comments to proper docstrings across all test functions.

Re the /NM scoping from the first review — it's intentionally at the outer scope to support setting annotation_name independently of in_reply_to (tested in test_markup_annotation_name). Happy to move it inside the in_reply_to block if you'd prefer to restrict it.

@costajohnt
Copy link
Copy Markdown
Contributor Author

@stefan6419846 Thanks for the thorough second round of feedback! I've addressed all comments in 8b39b2e:

  • Simplified in_reply_to and annotation_name docstrings (removed internal details like /NM and UUID)
  • Added :meth: cross-reference to PdfWriter.add_annotation()
  • Checking exact expected values (/Contents, /NM) instead of just key presence
  • Removed unnecessary write in the group type test
  • Renamed test functions to test_markup_annotation_* for clarity
  • Converted inline comments to docstrings

Ready for another look when you have time.

Add first-class support for annotation replies (PDF 1.5) by extending
MarkupAnnotation with three new parameters:

- in_reply_to: reference to the parent annotation, sets /IRT
- reply_type: "R" (reply, default) or "Group", sets /RT
- annotation_name: sets /NM for annotation threading (auto-generated
  UUID when in_reply_to is set)

Closes py-pdf#2065
Raise ValueError if reply_type is not 'R' or 'Group' (the only
values allowed by PDF spec Table 170), rather than silently writing
an invalid /RT entry.
- Use Literal["R", "Group"] for reply_type instead of runtime string check
- Accept IndirectObject directly for in_reply_to (not just DictionaryObject)
- Raise ValueError for unregistered in_reply_to instead of logging warning
- Only write /RT when reply_type is "Group" (per PDF spec, /R is the default)
- Simplify tests: use PdfWriter(clone_from=...) and BytesIO instead of disk I/O
Use isinstance(indirect_ref, IndirectObject) instead of direct attribute
access to handle both the missing-attribute case and mypy type narrowing.
Per maintainer feedback, always write /RT explicitly even when
reply_type is the default "R", rather than omitting it.
Update test assertion and docstring accordingly.
- Simplify in_reply_to and annotation_name docstrings to remove
  implementation details (DictionaryObject, /NM, UUID)
- Use Sphinx :meth: cross-reference for add_annotation()
- Rename test functions to test_markup_annotation_* prefix
- Assert exact /IRT and /NM values after write/read cycle
- Remove unnecessary BytesIO write in group type test
- Convert inline comments to proper docstrings
@costajohnt costajohnt force-pushed the feat/add-irt-annotation-support-2065 branch from 8b39b2e to 462f82a Compare February 13, 2026 18:23
Move /NM assignment inside the in_reply_to block and add ValueError
guards for annotation_name and reply_type when provided without
in_reply_to. This prevents silent parameter discard and keeps the
reply-specific parameters properly scoped.
@costajohnt
Copy link
Copy Markdown
Contributor Author

Hey @stefan6419846, sorry for the late reply, totally missed your last comment. You're right, I've moved /NM inside the in_reply_to block so it's properly scoped to the reply context. Also added ValueError guards for both annotation_name and reply_type when passed without in_reply_to, so they don't just silently get ignored. Ready for another look whenever you get a chance.

@stefan6419846
Copy link
Copy Markdown
Collaborator

It seems like you forgot to push your latest changes?

@costajohnt
Copy link
Copy Markdown
Contributor Author

Hey, sorry about that. Pushed the changes just now. The pypy3.11 failure is a runner timeout (shutdown signal mid-test), not related to our changes. All other jobs pass. Could you re-trigger that one when you get a chance?

Copy link
Copy Markdown
Collaborator

@stefan6419846 stefan6419846 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

@stefan6419846 stefan6419846 merged commit 0e9792d into py-pdf:main Mar 4, 2026
30 of 32 checks passed
@costajohnt costajohnt deleted the feat/add-irt-annotation-support-2065 branch March 5, 2026 03:17
stefan6419846 added a commit that referenced this pull request Mar 9, 2026
## What's new

### Security (SEC)
- Limit allowed `/Length` value of stream  (#3675) by @stefan6419846

### New Features (ENH)
- Add /IRT (in-reply-to) support for markup annotations (#3631) by @costajohnt

### Documentation (DOC)
- Avoid using `PageObject.replace_contents` on PdfReader (#3669) by @stefan6419846
- Document how to disable jbig2dec calls by @stefan6419846

[Full Changelog](6.7.5...6.8.0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ENH: Add PDF annotation /IRT ("in reply to")

2 participants