In this article, we'll explore how to implement a pure client-side PDF rotation tool that runs entirely in the browser. This tool can rotate PDF pages by any angle (90°, 180°, 270°) in both clockwise and counterclockwise directions.
Why Browser-Based PDF Rotation?
Traditional PDF rotation typically requires:
- Uploading files to a server
- Backend processing
- Downloading the result
Browser-based processing solves all these issues:
- ✅ Files never leave your computer - complete privacy
- ✅ Instant processing with no network delays
- ✅ Works offline after initial load
- ✅ Zero server costs
- ✅ No file size limits imposed by server
The Challenge: PDF Page Rotation
PDF rotation is more complex than simple image rotation because:
- PDFs contain vector graphics, text, and metadata
- Rotation must preserve text searchability and selectability
- Page dimensions may need to swap (width ↔ height for 90°/270°)
- Annotations and form fields must rotate correctly
Architecture Overview
This tool uses the same architecture as other QPDF-based features:
Key Data Structures
Rotation Options
// Rotation values mapped to degrees
const rotationOptions = [
{ name: "clockwise_0", degree: 0, title: "No Rotation" },
{ name: "clockwise_90", degree: 90, title: "Clockwise 90°" },
{ name: "clockwise_180", degree: 180, title: "Clockwise 180°" },
{ name: "clockwise_270", degree: 270, title: "Clockwise 270°" },
{ name: "counterclockwise_90", degree: -90, title: "Counterclockwise 90°" },
{ name: "counterclockwise_180", degree: -180, title: "Counterclockwise 180°" },
{ name: "counterclockwise_270", degree: -270, title: "Counterclockwise 270°" },
];
WorkerFunctions Interface
// hooks/useqpdf.ts
interface WorkerFunctions {
init: () => Promise<void>;
rotate: (file: File, rotate: number) => Promise<ArrayBuffer | null>;
// ... other operations
}
Implementation Deep Dive
1. User Interface Component
The rotate component provides an intuitive radio button interface:
// app/[locale]/_components/qpdf/rotate.tsx
export const Rotate = () => {
const [files, setFiles] = useState<File[]>([]);
const [degree, setDegree] = useState(0);
const t = useTranslations("Rotate");
const { rotate } = useQpdf();
const mergeInMain = async () => {
console.log("Rotating PDF:", files[0]?.name);
// Call rotate function with selected degree
const outputFile = await rotate(files[0]!, degree);
if (outputFile) {
autoDownloadBlob(new Blob([outputFile]), "rotated.pdf");
}
};
return (
<PdfPage
process={mergeInMain}
onFiles={onPdfFiles}
title={t("title")}
desp={t("desp")}
>
<div>
<Radio
defaultValue="clockwise_0"
values={[
{ name: "clockwise_0", title: t("clockwise_0") },
{ name: "clockwise_90", title: t("clockwise_90") },
{ name: "clockwise_180", title: t("clockwise_180") },
{ name: "clockwise_270", title: t("clockwise_270") },
{ name: "counterclockwise_90", title: t("counterclockwise_90") },
{ name: "counterclockwise_180", title: t("counterclockwise_180") },
{ name: "counterclockwise_270", title: t("counterclockwise_270") },
]}
onValueChange={(e) => {
switch (e) {
case "clockwise_0": {
setDegree(0);
break;
}
case "clockwise_90": {
setDegree(90);
break;
}
case "clockwise_180": {
setDegree(180);
break;
}
case "clockwise_270": {
setDegree(270);
break;
}
case "counterclockwise_90": {
setDegree(-90);
break;
}
case "counterclockwise_180": {
setDegree(-180);
break;
}
case "counterclockwise_270": {
setDegree(-270);
break;
}
}
}}
/>
</div>
</PdfPage>
);
};
Key features:
- Radio button group for rotation selection
- Support for both clockwise and counterclockwise directions
- Degree values: 0, ±90, ±180, ±270
- Intuitive mapping from UI selection to rotation degrees
2. Radio Button Component
A reusable radio button component for rotation selection:
// app/[locale]/_components/radio.tsx
export type RadioProps = {
values: { name: string; title: string }[];
defaultValue: string;
onValueChange: (value: string) => void;
};
export function Radio({ values, defaultValue, onValueChange }: RadioProps) {
const { value, onChange: handleSplitChange } =
useInputValue<string>(defaultValue);
useEffect(() => {
onValueChange(value);
}, [value, onValueChange]);
return (
<fieldset className="fieldset bg-base-100">
{values.map((e, i) => (
<label className="label" key={i}>
<input
type="radio"
name="splitType" // Same name for mutual exclusion
value={e.name}
className="radio"
checked={value === e.name}
onChange={handleSplitChange}
/>
{e.title}
</label>
))}
</fieldset>
);
}
3. Worker Management with Comlink
The useQpdf hook manages the Web Worker:
// hooks/useqpdf.ts
export const useQpdf = () => {
const workerRef = useRef<Comlink.Remote<WorkerFunctions>>(null);
useEffect(() => {
async function initWorker() {
if (workerRef.current) return;
const worker = new PdfWorker();
worker.onerror = (error) => {
console.error("Worker error:", error);
};
workerRef.current = Comlink.wrap<WorkerFunctions>(worker);
await workerRef.current.init();
return () => worker.terminate();
}
initWorker().catch(() => { return; });
}, []);
const rotate = async (
file: File,
rotate: number,
): Promise<ArrayBuffer | null> => {
console.log("Calling rotate in main thread", workerRef.current);
if (!workerRef.current) return null;
const r = await workerRef.current.rotate(file, rotate);
return r;
};
return { rotate };
};
4. QPDF Rotation Implementation in Web Worker
The actual rotation happens in the Web Worker using QPDF:
// hooks/pdf.worker.js
async rotate(file, rotate) {
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// Write input to virtual filesystem
qpdf.FS.writeFile(`/input.pdf`, uint8Array);
// Build QPDF rotation command
// Format: --rotate=angle:page-range
const params = [
"/input.pdf",
`--rotate=${rotate}:1-z`, // Rotate all pages (1 to last)
"--",
"/output.pdf",
];
console.log("Rotation params:", params);
// Execute QPDF
qpdf.callMain(params);
// Read output from virtual filesystem
const outputFile = qpdf.FS.readFile("/output.pdf");
return outputFile;
}
QPDF rotation command:
# Rotate all pages 90 degrees clockwise
qpdf input.pdf --rotate=90:1-z -- output.pdf
# Rotate specific pages (e.g., pages 1-5)
qpdf input.pdf --rotate=180:1-5 -- output.pdf
# Rotate different pages differently
qpdf input.pdf --rotate=90:1-3 --rotate=180:4-6 -- output.pdf
Command syntax:
--rotate=angle:page-range-
angle: Rotation in degrees (positive = clockwise, negative = counterclockwise) -
page-range: Pages to rotate (e.g.,1-zfor all pages,1-5for pages 1-5)
5. QPDF WASM Initialization
The QPDF WASM module is initialized with Emscripten:
// lib/qpdfwasm.js
import createModule from "@neslinesli93/qpdf-wasm";
const f = async () => {
const qpdf = await createModule({
locateFile: () => "/qpdf.wasm",
noInitialRun: true, // Don't run main() immediately
});
return qpdf;
};
export default f;
Complete Processing Flow
How QPDF Handles Rotation
QPDF performs rotation by modifying the PDF's rotation dictionary:
PDF Rotation Dictionary
In PDF files, each page has a /Rotate entry in its dictionary:
% PDF page dictionary
<<
/Type /Page
/Parent 3 0 R
/MediaBox [0 0 612 792]
/Rotate 90 % Rotation angle in degrees
/Contents 5 0 R
>>
Rotation Values
| Rotation | Effect |
|---|---|
| 0 | No rotation (default) |
| 90 | Rotate 90° clockwise |
| 180 | Rotate 180° (upside down) |
| 270 | Rotate 270° clockwise (or 90° counterclockwise) |
What QPDF Does
- Parses the PDF structure
- Finds all page dictionaries in the page tree
-
Updates the
/Rotateentry for specified pages - Recalculates any dependent structures (optional)
- Writes the modified PDF with new rotation values
Key Technical Decisions
1. Why QPDF for Rotation?
QPDF is ideal for rotation because:
- It modifies the PDF rotation dictionary directly
- Preserves all PDF features (text, forms, annotations)
- Handles complex PDF structures correctly
- Fast and reliable
2. Why Web Workers?
PDF processing benefits from Web Workers:
- Prevents UI freezing during processing
- Handles large PDFs without blocking
- Maintains responsive user interface
3. Clockwise vs Counterclockwise
The component supports both directions:
- Clockwise: Positive degrees (90, 180, 270)
- Counterclockwise: Negative degrees (-90, -180, -270)
QPDF accepts both positive and negative rotation values.
User Interface
The component provides a clean radio button interface:
<fieldset className="fieldset bg-base-100">
<label className="label">
<input type="radio" name="splitType" value="clockwise_0" className="radio" checked />
No Rotation
</label>
<label className="label">
<input type="radio" name="splitType" value="clockwise_90" className="radio" />
Clockwise 90°
</label>
<label className="label">
<input type="radio" name="splitType" value="clockwise_180" className="radio" />
Clockwise 180°
</label>
<label className="label">
<input type="radio" name="splitType" value="clockwise_270" className="radio" />
Clockwise 270°
</label>
<label className="label">
<input type="radio" name="splitType" value="counterclockwise_90" className="radio" />
Counterclockwise 90°
</label>
<!-- More options... -->
</fieldset>
Benefits of This Architecture
- Privacy First: Files never leave the browser
- Fast Processing: QPDF is highly optimized
- Preserves Quality: Vector graphics and text remain crisp
- Responsive UI: Web Workers prevent blocking
- No Server Required: Zero backend infrastructure
- Standard Compliant: Uses PDF rotation dictionary correctly
Try It Yourself
Want to rotate your PDF pages without uploading them to a server? Try our free browser-based tool:
All processing happens locally in your browser - your files never leave your computer!
Conclusion
Building a browser-based PDF rotation tool demonstrates how QPDF compiled to WebAssembly can handle PDF manipulation tasks efficiently. By using the PDF rotation dictionary, we ensure that rotated documents remain fully functional and editable.
This approach is ideal for:
- Correcting scanned document orientation
- Preparing documents for different viewing modes
- Fixing upside-down or sideways pages
- Batch processing multiple PDFs
The simple radio button interface makes it easy for users to select the exact rotation they need, while the underlying QPDF engine handles the complex PDF structure modifications reliably.


Top comments (0)