Rotating a PDF page — turning content 90°, 180°, or 270° — sounds like a simple toggle. But properly rewriting a PDF page's content stream, adjusting its crop box, and updating metadata is surprisingly involved. With pdf-lib and a bit of canvas math, you can build a fully browser-based rotation tool that works correctly in every PDF viewer.
This post walks through the implementation details, including coordinate system transformation, content stream rewriting, and multi-page support.
Why client-side?
Traditional rotation tools upload your file, process it on a server, and send the result back. For documents that might contain sensitive information, this introduces an unnecessary privacy risk. A browser-based approach:
- Processes everything locally
- Keeps files on the user's device
- Works offline after loading
- Avoids server-side bandwidth costs
The stack
- Vue 3 + Composition API
- pdf-lib for PDF manipulation and rotation
-
PDF.js (
pdfjs-dist) for preview rendering (optional) - Vite for bundling
The rotation logic
pdf-lib provides a page.setRotation(degrees) method, but this only updates the rotation metadata flag. It doesn't rewrite the actual content stream, which can cause visual glitches in some viewers. For correct rotation, we need to:
- Apply an affine transformation matrix to the page's content stream
- Adjust the CropBox dimensions if necessary (90°/270° rotations swap width/height)
- Update the rotation metadata
- Optionally preview using PDF.js to show users the result before saving
Here's the core rotation function:
<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument, degreesToRadians, PageRotation } from 'pdf-lib'
const file = ref<File | null>(null)
const rotation = ref<PageRotation>(0) // 0, 90, 180, 270
const rotating = ref(false)
async function rotatePages() {
if (!file.value) return
rotating.value = true
const arrayBuffer = await file.value.arrayBuffer()
const pdf = await PDFDocument.load(arrayBuffer)
const pages = pdf.getPages()
for (const page of pages) {
// Apply rotation transformation
page.setRotation(Number(rotation.value))
// CropBox may need adjustment for 90°/270° rotations
const { width, height } = page.getSize()
if (rotation.value === 90 || rotation.value === 270) {
// Swap width and height for sideways pages
page.setSize(height, width)
}
}
const rotatedPdf = await pdf.save()
downloadFile(rotatedPdf, 'rotated.pdf')
rotating.value = false
}
</script>
Key implementation details
1. Affine transformations
When you rotate a PDF page, the content coordinates (where text and images are drawn) need to transform as well. pdf-lib handles this automatically via setRotation(), which internally:
- Updates the
/Rotateentry in the page dictionary - Applies an affine transformation matrix to the content stream operators
- Adjusts the CropBox dimensions for 90°/270° rotations
2. Handling custom angles (non-multiples of 90°)
pdf-lib only supports standard rotations (0, 90, 180, 270). For custom angles, you need to:
- Render the page to a canvas (using PDF.js) at the desired angle
- Extract the canvas content as an image
- Embed the image back into a new PDF page at the rotated dimensions
This is the "lossy" path — text and vector elements become raster images. Use it only when custom angles are absolutely required.
3. Preserving rotation state during previews
If you want to show users a preview before committing, use PDF.js to render the page with the temporary rotation applied. Don't save yet — the user may cancel. PDF.js respects the /Rotate metadata, so just pass the desired rotation to its viewport:
const viewport = page.getViewport({
scale: 1.5,
rotation: Number(rotation.value),
})
4. Batch rotation across multiple pages
Rotate all pages uniformly or select specific pages. For selective rotation, store which pages should be rotated and iterate over them only:
const pagesToRotate = Array.from({ length: pageCount }, (_, i) =>
selectedPages.has(i + 1) ? i : null
).filter(v => v !== null)
for (const pageIndex of pagesToRotate) {
const page = pages[pageIndex]
page.setRotation(Number(rotation.value))
if (rotation.value === 90 || rotation.value === 270) {
const { width, height } = page.getSize()
page.setSize(height, width)
}
}
What to watch out for
- Form fields: Rotating a page that contains interactive form fields can misalign the field coordinates. Fields won't "rotate" with the page — they stay fixed. This is expected behavior but users should be aware.
- Page order: PDF page order in the document's Page tree is unaffected by rotation. Rotation only changes how a single page is displayed.
-
Multiple rotation flags: Some PDFs already have a
/Rotateentry. CallingsetRotation()replaces it, which is the correct behavior — you want the final rotation to be the one the user requested, not compounded. - Very large pages: Architectural drawings or large-format posters may exceed canvas memory limits when rendered for preview. Use smaller preview scales or skip preview for very large pages.
Summary
Building a browser-based PDF rotation tool with pdf-lib involves:
- Loading the PDF with
PDFDocument.load() - Iterating pages and calling
setRotation(degrees) - Adjusting CropBox dimensions for 90°/270° rotations
- Saving with
pdf.save() - Downloading the result via
Blob+<a>element
Try it at en.sotool.top/rotate. It takes about 30 seconds and keeps your files completely private.
Top comments (0)