# How LiteParse canonical_rotation Normalizes 90°, 180°, and 270° Text for Reading Order

> Learn how LiteParse canonical_rotation normalizes 90, 180, and 270 degree text for accurate reading order. Reliable text re orientation for PDFs.

- Repository: [LlamaIndex/liteparse](https://github.com/run-llama/liteparse)
- Tags: deep-dive
- Published: 2026-06-06

---

**LiteParse's `canonical_rotation` helper wraps raw PDFium angles to the 0°–360° range, snaps near-cardinal rotations within ±2° to exactly 0°, 90°, 180°, or 270°, and rounds all other angles to the nearest integer so downstream reading-order logic can reliably re-orient text.**

When extracting text from PDFs, even slight rotational noise from the rendering engine can break layout analysis. In the `run-llama/liteparse` repository, every text fragment's floating-point rotation is passed through the **`canonical_rotation`** function in [`crates/liteparse/src/projection.rs`](https://github.com/run-llama/liteparse/blob/main/crates/liteparse/src/projection.rs) before any reading-order decisions are made. This normalization ensures that rotated labels are grouped correctly and merged into a linear flow that humans expect.

## How canonical_rotation Works in LiteParse

The `canonical_rotation` implementation, found at lines 89–111 of [`crates/liteparse/src/projection.rs`](https://github.com/run-llama/liteparse/blob/main/crates/liteparse/src/projection.rs), executes a strict three-step pipeline on the raw angle supplied by PDFium.

### Step 1: Wrap the Angle to a 0°–360° Range

The helper begins by calling `rotation.rem_euclid(360.0)` on the raw floating-point value. This guarantees that negative angles and values greater than 360° collapse into a single positive rotation around the circle.

### Step 2: Snap to Cardinal Directions Using Circular Distance

Next, the algorithm evaluates the four cardinal candidates—**0°, 90°, 180°, and 270°**—and computes the shorter *circular* distance between the wrapped angle and each candidate. If the smallest distance is **≤ 2°**, the function snaps the angle to that cardinal direction and returns it as an **`i32`**. This means that a raw rotation of 89.8° is treated as exactly `90`, while 1.5° is normalized to `0`.

### Step 3: Round Remaining Slanted Angles

If the angle is more than 2° away from every cardinal direction, the wrapped value `r` is rounded to the nearest whole degree with `r.round() as i32`. This preserves truly slanted text—such as a 45° watermark—while still providing an integer that downstream code can consume.

These rules produce the following behaviors according to the source code:

- **0°, 360°, or -1°** → canonicalized to `0` and treated as normal horizontal text.
- **1° to 2°** → snapped to `0`, ignoring small noise.
- **88° to 92°** → snapped to `90`, triggering vertical reading-order handling.
- **179° to 181°** → snapped to `180`, flagging upside-down text to be flipped.
- **268° to 272°** → snapped to `270`, marking counter-clockwise vertical text.
- **All other angles** → rounded to the nearest integer (e.g., `45°` stays `45`, `357°` stays `357`) with no special snapping applied.

## How LiteParse Converts Canonical Rotation to Linear Reading Order

Once every fragment has an integer rotation, the **`handle_rotation_reading_order`** routine—defined in [`crates/liteparse/src/projection.rs`](https://github.com/run-llama/liteparse/blob/main/crates/liteparse/src/projection.rs) (lines 13–78)—uses that value to reconstruct the correct text flow.

The routine performs three actions:

1. **Groups items** by their canonical rotation value so that horizontal and vertical labels are processed separately.
2. **Splits 90° and 270° groups** into spatial clusters, preventing top labels and bottom labels from incorrectly merging into a single block.
3. **Re-orients each rotated group** back to normal reading direction by swapping x/y coordinates, exchanging width and height, and setting `rotation = 0`. You can see the coordinate-transformation block starting at line 63 in the same file.

This pipeline guarantees that a 90° sidebar heading is read top-to-bottom and then inserted into the main flow at the correct logical position.

## Rust Example: Inspecting canonical_rotation Output

The snippet below demonstrates how to parse a PDF and inspect the canonical rotation for each extracted item. The example assumes the `liteparse` crate is available in your workspace.

```rust
use liteparse::parser::LiteParse;
use liteparse::config::LiteParseConfig;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Basic configuration – use defaults
    let cfg = LiteParseConfig::default();

    // Create a parser for a PDF file
    let mut parser = LiteParse::new("example.pdf", cfg)?;

    // Run the full pipeline (conversion, extraction, OCR, projection)
    let result = parser.parse()?;

    // Iterate over the extracted items
    for (i, item) in result.items.iter().enumerate() {
        // The raw rotation supplied by PDFium
        let raw = item.item.rotation;
        // Canonical rotation as used by LiteParse (calls the private helper internally)
        let canonical = liteparse::projection::canonical_rotation(raw);
        println!(
            "Item {} – raw rotation = {raw:.2}°, canonical = {canonical}°",
            i + 1
        );
    }

    Ok(())
}

```

When run against a document containing rotated labels, the output will look similar to:

```text
Item 7 – raw rotation = 89.8°, canonical = 90°
Item 12 – raw rotation = 0.3°, canonical = 0°
Item 15 – raw rotation = 357.0°, canonical = 357°

```

The `canonical` value is exactly what the layout engine later feeds into `handle_rotation_reading_order` to decide whether an item must be rotated back into the normal flow.

The canonical rotation value is carried through the pipeline on the **`rotation`** field of `ProjectedTextItem`, defined in [`crates/liteparse/src/types.rs`](https://github.com/run-llama/liteparse/blob/main/crates/liteparse/src/types.rs), and the end-to-end parsing flow is orchestrated by [`crates/liteparse/src/parser.rs`](https://github.com/run-llama/liteparse/blob/main/crates/liteparse/src/parser.rs).

## Summary

- **`canonical_rotation`** in [`crates/liteparse/src/projection.rs`](https://github.com/run-llama/liteparse/blob/main/crates/liteparse/src/projection.rs) (lines 89–111) wraps, snaps, and rounds raw PDFium angles into stable integer rotations.
- Angles within **±2°** of **0°, 90°, 180°, or 270°** are snapped to those exact cardinals, while all others are rounded to the nearest degree.
- **`handle_rotation_reading_order`** uses these canonical values to group, cluster, and re-orient text so that 90° and 270° labels are read in the correct order.
- The `rotation` field on `ProjectedTextItem` carries the normalized value through the full `LiteParse` pipeline, which is orchestrated in [`crates/liteparse/src/parser.rs`](https://github.com/run-llama/liteparse/blob/main/crates/liteparse/src/parser.rs).

## Frequently Asked Questions

### What tolerance does LiteParse use when snapping rotated text?

LiteParse uses a **2° tolerance** on either side of each cardinal direction. Any angle whose shortest circular distance to 0°, 90°, 180°, or 270° is less than or equal to 2° is snapped to that exact cardinal value and returned as an integer.

### How does LiteParse handle text that is rotated exactly 45°?

Because 45° is more than 2° away from every cardinal direction, `canonical_rotation` does not snap it. Instead, it rounds the wrapped angle to the nearest whole degree—returning `45`—and leaves the item in its original slanted orientation without special reading-order re-orientation.

### Where does the rotation value come from before canonicalization?

The raw floating-point rotation originates from **PDFium**, the PDF rendering engine used by LiteParse. Each extracted text fragment carries this value in the `rotation` field of `ProjectedTextItem` before `canonical_rotation` processes it during the projection stage.

### Does canonical_rotation modify the physical coordinates of the text?

No. The `canonical_rotation` function only returns an integer angle. The actual coordinate transformation—swapping x and y, exchanging width and height, and setting `rotation = 0`—is performed later by **`handle_rotation_reading_order`** in [`crates/liteparse/src/projection.rs`](https://github.com/run-llama/liteparse/blob/main/crates/liteparse/src/projection.rs) starting at line 63.