Skip to content

Annotations disappear or move when using Page.remove_rotation() for 90 or 270 rotated pages #5059

Description

@secrawford

Description of the bug

Page.remove_rotation() as documented is expected to maintain the visual appearance of a page including annotations. When using Page.remove_rotation() on a Page with rotation=90 or rotation=270 that contains annotations, those annotations tend to move unexpectedly or off the page entirely.

The case of 90 is straightforward to fix. Annot.update(rotate=0) must be called at some point after page rotation is removed to fully reflect the unrotated page. Adding annot.update(rotate=0) after line src/__init__.py:12078 would fix this issue on the library side and would mirror the existing widget.update() at src/__init__.py:12092.

The case of 270 is harder to address. I suspect this has something to do with the multiple coordinate systems involved, but the matrix used to derotate the page is unsatisfactory for derotating 270-degree annotations. I'm not sure I understand the math but inserting a if rot == 270: followed by mat *= Matrix(1, 0, 0, 1, -mb.x1 + mb.y1 +mb.y0 +mb.x0, -mb.x1 + mb.y1 + mb.y0 + mb.x0) on line src/__init__.py:12071 before self.set_rotation(0) has experimentally fixed the issue, though this particular solution is a band-aid at most.

How to reproduce the bug

The following code may be run to generate demonstration PDFs. A path must be defined to save the PDFs to, but all other code may be run as-is. We expect "orig90.pdf", "derotate90.pdf", and "update90.pdf" to be visually similar, but this is not currently the case. Similarly, "orig270.pdf", "derotate270.pdf", "update270.pdf", and "manual270.pdf" should all be visually similar, but with the current library are not.

import pymupdf
out_dir = "YOUR/FAVORITE/PATH/HERE/"
Create a new doc with a single 90 rotated page and draw an upwards arrow and annotation "90" onto the page.
doc_90 = pymupdf.open()
doc_90.insert_page(0, width=200, height=100)
page_90 = doc_90[0]
page_90.set_rotation(90)
page_90.draw_polyline([(200, 50), (0, 50), (50, 0), (50, 100), (0, 50)])
annot_90 = page_90.add_freetext_annot((180, 80, 200, 100), "90", text_color=[255, 0, 0], rotate=90)
Create a similar document with a 270 rotated page, with arrow-drawing coordinates appropriately adjusted.
doc_270 = pymupdf.open()
doc_270.insert_page(0, width=200, height=100)
page_270 = doc_270[0]
page_270.set_rotation(270)
page_270.draw_polyline([(0, 50), (200, 50), (150, 0), (150, 100), (200, 50)])
annot_270 = page_270.add_freetext_annot((180, 80, 200, 100), "270", text_color=[255, 0, 0], rotate=270)
Save both documents to the output directory for reference (shown below).
doc_90.save(out_dir + "orig90.pdf")
doc_270.save(out_dir + "orig270.pdf")

Image Image

Save copies of the original rectangle of the 270 annotation and the derotation matrix for later and remove each page's rotation then save documents to see the result.
dr_270 = pymupdf.Matrix(page_270.derotation_matrix)
or_270 = pymupdf.Rect(annot_270.rect)
page_90.remove_rotation()
page_270.remove_rotation()
doc_90.save(out_dir + "derotate90.pdf")
doc_270.save(out_dir + "derotate270.pdf")

Image Image

Calling update on the annotation fixes the 90 document, but not the 270.
annot_90.update(rotate=0)
annot_270.update(rotate=0)
doc_90.save(out_dir + "update90.pdf")
doc_270.save(out_dir + "update270.pdf")

Image Image

Manually set the annotation rectangle to what it should be to see that the annotation still exists, just misplaced.
annot_270.set_rect(or_270 * ~dr_270)
doc_270.save(out_dir + "manual270.pdf")

Image

Note that if the suggested changes as set out in the bug description are made to the library, all 3 *90.pdf docs become visually similar to one another, and all 4 *270.pdf docs become visually similar to one another.

PyMuPDF version

1.28.0

Operating system

Windows

Python version

3.11

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions