How LiteParse canonical_rotation Normalizes 90°, 180°, and 270° Text for Reading Order
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 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, 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
0and 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°stays45,357°stays357) 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 (lines 13–78)—uses that value to reconstruct the correct text flow.
The routine performs three actions:
- Groups items by their canonical rotation value so that horizontal and vertical labels are processed separately.
- Splits 90° and 270° groups into spatial clusters, preventing top labels and bottom labels from incorrectly merging into a single block.
- 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.
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:
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, and the end-to-end parsing flow is orchestrated by crates/liteparse/src/parser.rs.
Summary
canonical_rotationincrates/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_orderuses these canonical values to group, cluster, and re-orient text so that 90° and 270° labels are read in the correct order.- The
rotationfield onProjectedTextItemcarries the normalized value through the fullLiteParsepipeline, which is orchestrated incrates/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 starting at line 63.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →