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")
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")
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")
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")
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
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 existingwidget.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 bymat *= 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 beforeself.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 pymupdfout_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")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")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")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")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