2025, Oct 05 21:00

Arbitrary-angle PDF rotation in PyMuPDF: use show_pdf_page to keep selectable text, vectors, and images without rasterization

Learn how to rotate PDF pages by any angle in PyMuPDF using show_pdf_page, preserving selectable text, vector graphics, and images - no rasterizing. Key notes.

Rotating a PDF page by an arbitrary angle is a surprisingly common need, yet the obvious API surface in PyMuPDF appears to block you at the door. The usual page rotation toggles only support multiples of 90 degrees and simply change viewer orientation. If you need something like 2.5 degrees while preserving selectable text, vector shapes, and images, there is a clean, library-native way to do it—in one call.

When the obvious path doesn’t work

The instinct is to try a transformation matrix directly on the page. You open the document, pick the page, set its rotation to zero, build a Matrix, prerotate it, and then try to apply that to the page contents. The catch: applying a matrix to an entire page object in this way is not available.

import pymupdf as pm
pdf_obj = pm.open("Sample.pdf")  # open input PDF
first_pg = pdf_obj[0]             # access first page
first_pg.set_rotation(0)          # 0/90/180/270 only, orientation-level
xform = pm.Matrix(1, 0, 0, 1, 0, 0)
xform.prerotate(2.5)              # aim for 2.5 degrees
first_pg.add_transformation(xform)  # this method does not exist for pages
pdf_obj.save("Sample_rotated.pdf")

Why this fails

The page rotation flag in PDF does not rotate the actual content. It stores an orientation value that viewers honor, and it is restricted to 0, 90, 180, and 270 degrees. Attempting to push a free-angle transform on the page itself runs into API limits, because that operation isn’t exposed as a page-wide transformation in PyMuPDF. If you want fine-grained angles like 2.5 degrees, you need a different approach that still preserves text and vector data.

A simple, working solution with PyMuPDF

PyMuPDF can render a page into another page with a transformation in place. The method that unlocks arbitrary rotation is show_pdf_page. It brings the source page into a target page rectangle and applies a rotation parameter—and the result keeps text selectable and vector content intact.

import pymupdf  # NOT fitz
# prepare a blank receiver page
sheet = pymupdf.open().new_page(width=595, height=841)
# import page 0 from input.pdf into the receiver, rotated by 2.5 degrees
sheet.show_pdf_page(
    pymupdf.Rect(0, 0, 595, 841),
    pymupdf.open("input.pdf"),
    pno=0,
    rotate=2.5,
    keep_proportion=True
)
# save the new document
sheet.parent.save("output.pdf")

This approach retains vectors, works with scanned images, and keeps text selectable. It does the transformation as part of the import, which avoids rasterizing the content.

What to expect and where the edges are

This method focuses on page cores. As a result, some ancillary items may not carry over. Metadata might not be fully transferred because the output is regenerated. Annotations, bookmarks, and links can disappear, similar to other libraries that rebuild page content; the original coordinates may no longer be valid after a transformed import. Fine rotation can also ignore a source page’s prior rotation, so you might need to adjust orientation on import for sources that come in landscape or contain mixed rotations. There is also a bonus side-effect: this technique may remove embedded JavaScript, which can be handy from a safety perspective, although that is proven on some files but not all. Other functions such as morph exist in the API landscape and can offer related transformations, but they tend to target specific content types. The show_pdf_page route is a good balance for whole-page handling without diving into per-object manipulation.

Clean de-rotation and sanitizing

The same approach can flatten rotations back to zero and sanitize a document by reimporting just the core page content. This can help normalize PDFs, remove unwanted extras, and compress along the way.

import pymupdf  # NOT fitz
out_pdf = pymupdf.open()            # receiver document
in_pdf = pymupdf.open("input.pdf")  # source document
for idx in range(len(in_pdf)):      # iterate source pages
    canvas = out_pdf.new_page()     # A4 upright by default
    # import the page core with a 0.0-degree rotation (acts as de-rotation)
    canvas.show_pdf_page(canvas.rect, in_pdf, idx, rotate=0.0)
# optionally clear garbage and compress core streams
out_pdf.save("output.pdf", garbage=3, deflate=True)

Why knowing this matters

Being able to rotate at fractional degrees without rasterization means you keep the quality and accessibility of your PDFs. Selectable text stays selectable, vectors stay vectors, and file sizes remain lean. At the same time, reimporting pages provides a pragmatic way to normalize documents, which can remove embedded scripts, drop fragile extras, and sidestep quirks that accumulate from prior edits.

Wrap-up

Multiple-of-90 page rotation is an orientation flag; it won’t give you a precise tilt. When you need a 2.5-degree nudge or any other fine angle while preserving the underlying PDF structure, import the page into a new one using show_pdf_page with the rotate parameter. Be mindful that you are effectively rebuilding page cores, which can leave behind some metadata and interactive elements. If that trade-off works for your use case, you get a compact, accurate, and scriptable solution entirely within Python and PyMuPDF.

The article is based on a question from StackOverflow by theozh and an answer by K J.