Skip to content

ENH: AppearanceStream: Allow arbitrary rotations and apply rotations for annotation appearance streams#3917

Open
PJBrs wants to merge 4 commits into
py-pdf:mainfrom
PJBrs:2737
Open

ENH: AppearanceStream: Allow arbitrary rotations and apply rotations for annotation appearance streams#3917
PJBrs wants to merge 4 commits into
py-pdf:mainfrom
PJBrs:2737

Conversation

@PJBrs

@PJBrs PJBrs commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

This PR adds a "rotation" attribute to class BaseStreamConfig. When instantiating a class BaseStreamAppearance, we apply the rotation to the appearance stream in the form of a Transformation, both rotating and translating the BaseStreamAppearance object so that its origin remains (0, 0).

When we parse an annotation to generate an appearance stream, we now read its ["/MK"]["/R"] field to determine rotation, and pass the rotation along with the other layout options. In some cases, we also need to take into account page rotation (in particular, this happens in issue #2737.

Finally this PR adds one patch to change the typing of BaseStreamConfig so that rectangle is always a RectangleObject. This to prevent mypy warnings or coverage problems.

Fixes: #2737

I made extensive use of Google Gemini preparing this PR. Especially the Transformation code I could not have produced myself. The resulting logic, though, is mainly my own.

PJBrs added 4 commits July 14, 2026 08:27
Now that we need BaseStreamConfig.rectangle in more places, just
have it always be a RectangleObject. This is more accurate and it
prevents code coverage / mypy problems where lines needed to check
whether rectangle is a RectangleObject are either detected as missing
by mypy or untested by coverage.
This patch adds rotation to BaseStreamConfig and applies it
to BaseStreamAppearance by adding a "/Matrix" key when rotation
is 90, 180 or 270 degrees.
When an annotation includes a ["/MK"]["/R"] field for rotation,
detect it and rotate the appearance stream accordingly.
When a page has a rotation set of 90 or 270 degrees (for instance,
portrait to landscape), also rotate the axes of the annotation's
appearance stream rectangle.
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.87%. Comparing base (efc511b) to head (b3505aa).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3917   +/-   ##
=======================================
  Coverage   97.86%   97.87%           
=======================================
  Files          57       57           
  Lines       10738    10758   +20     
  Branches     2006     2009    +3     
=======================================
+ Hits        10509    10529   +20     
  Misses        127      127           
  Partials      102      102           

☔ View full report in Codecov by Harness.
📢 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.

@PJBrs

PJBrs commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Here's some example code and associated screen shot of the result.

image
from pypdf import PdfWriter
from pypdf.generic import RectangleObject
from pypdf.generic._appearance_stream import BaseStreamConfig, TextStreamAppearance

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

for rotation in range(0, 360, 30):

    layout = BaseStreamConfig(
        rectangle=RectangleObject([0, 0, 400, 20]),
        border_width=1,
        rotation=rotation
    )
    text=f"        Rotation {rotation}: Hello World!"

    appearance = TextStreamAppearance(
        layout=layout,
        text=text,
        font_size=12.0,
        font_color="1 0 0 rg",
        is_multiline=False,
        alignment=0
    )

    writer._add_apstream_object(
        page,
        appearance,
        f"HelloWorldStream{rotation}",
        x_offset=100,
        y_offset=350
    )

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

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.

Form fill font extra \x00 and font size

1 participant