Skip to content

Instantly share code, notes, and snippets.

@allquixotic
Created June 10, 2025 05:53
Show Gist options
  • Save allquixotic/abdf1484a555e49ed99583c071ee74de to your computer and use it in GitHub Desktop.
Save allquixotic/abdf1484a555e49ed99583c071ee74de to your computer and use it in GitHub Desktop.
Claude Opus 4's own generated CLAUDE.md for PDFStitcher per-page rotation

PDFStitcher Per-Page Rotation Implementation

Overview

This document outlines the technical implementation plan for adding per-page rotation support to PDFStitcher. The feature will allow users to specify rotation values for individual pages or page ranges using syntax like 1,2,4r90,5-7r180 in the page range field.

Implementation Status (January 2025)

COMPLETED - All components of the per-page rotation feature have been successfully implemented.

What Was Implemented

  1. Enhanced Page Range Parser (utils.py):

    • ✅ Added parse_page_range_with_rotation() function at line 287
    • ✅ Supports syntax like 1,2r90,3-5r180 with regex pattern (\d+)(?:-(\d+))?(?:[rR](\d+))?
    • ✅ Validates rotation values (0, 90, 180, 270)
    • ✅ Added utility functions:
      • degrees_to_sw_rotation() - converts degrees to SW_ROTATION enum
      • get_rotation_matrix() - returns 2x2 rotation matrices
      • apply_rotation_to_dimensions() - calculates dimensions after rotation
  2. ProcessingBase API Updates (procbase.py):

    • ✅ Maintained backward compatibility with existing code
    • ✅ Added page_range_with_rotation property for full rotation info
    • ✅ Updated page_range property to return list of integers for compatibility
    • ✅ Modified page validation to handle both dict and int formats
  3. PageFilter Rotation Support (pagefilter.py):

    • ✅ Added _apply_rotation_to_page() method for applying rotation transformations
    • ✅ Updated run() method to iterate through page_range_with_rotation
    • ✅ Rotation applied using PDF transformation matrices with proper translation
  4. PageTiler Per-Page Rotation (pagetiler.py):

    • ✅ Modified _process_page() to accept page_rotation parameter
    • ✅ Updated _build_pagelist() to pass per-page rotation
    • ✅ Modified _compute_T_matrix() to use per-page rotation with fallback to global
    • ✅ Updated _calc_shift() to accept rotation parameter
  5. CLI Updates (cli/app.py):

    • ✅ Updated help text for -p/--pages to show rotation syntax
    • ✅ Clarified that global -R/--rotate is overridden by per-page rotation
  6. GUI Updates (gui/io_tab.py):

    • ✅ Added example text: "Add r and degrees to rotate pages. Example: 1-3, 4r90, 5-7r180."
  7. Comprehensive Tests (tests/test_rotation.py):

    • ✅ Created 15 tests covering all aspects of the implementation
    • ✅ All tests passing successfully

Key Design Decisions

  • Per-page rotation takes precedence over global rotation when both are specified
  • Backward compatibility maintained - existing code continues to work unchanged
  • Rotation values limited to 0, 90, 180, 270 degrees (matching existing constraints)
  • Both uppercase 'R' and lowercase 'r' accepted in rotation syntax

Current State Analysis

Existing Rotation Support

  • PageTiler: Currently supports rotation (0°, 90°, 180°, 270°) but applies to ALL tiled pages
  • PageFilter: No rotation support (simple page selection with margin addition)
  • Rotation values: Defined in SW_ROTATION enum in pagetiler.py:
    • NONE = 0
    • CLOCKWISE = 1 (90°)
    • COUNTERCLOCKWISE = 2 (270°)
    • TURNAROUND = 3 (180°)

Page Range Parsing

  • Function: parse_page_range() in utils.py:266-283
  • Current format: "1,2,4-7" returns [1, 2, 4, 5, 6, 7]
  • Supports: comma-separated values, hyphenated ranges, repeated pages, out-of-order pages

Implementation Plan

1. Enhanced Page Range Parser

File: pdfstitcher/utils.py

New Function: parse_page_range_with_rotation(ptext: str = "") -> list[dict]

def parse_page_range_with_rotation(ptext: str = "") -> list[dict]:
    """
    Parse page ranges with optional rotation suffixes.
    Format: "1,2,4r90,5-7r180"
    Returns: [
        {"page": 1, "rotation": 0},
        {"page": 2, "rotation": 0},
        {"page": 4, "rotation": 90},
        {"page": 5, "rotation": 180},
        {"page": 6, "rotation": 180},
        {"page": 7, "rotation": 180}
    ]
    """

Implementation details:

  • Parse rotation suffix using regex: (\d+)(?:-(\d+))?(?:[rR](\d+))?
  • Support both uppercase and lowercase 'r'
  • Validate rotation values (0, 90, 180, 270)
  • Map CLI values (0, 90, 180, 270) to SW_ROTATION enum values
  • Maintain backward compatibility by defaulting to rotation=0

2. API Changes

ProcessingBase Class Changes

File: pdfstitcher/processing/procbase.py

Current API:

@property
def page_range(self) -> list:
    return self._page_range

@page_range.setter
def page_range(self, page_range: Union[str, list]) -> None:
    if isinstance(page_range, str):
        self._page_range = utils.parse_page_range(page_range)
    else:
        self._page_range = page_range

New API:

@property
def page_range(self) -> list:
    """Returns list of page numbers for backward compatibility"""
    return [p["page"] if isinstance(p, dict) else p for p in self._page_range]

@property
def page_range_with_rotation(self) -> list[dict]:
    """Returns full page range with rotation info"""
    return self._page_range

@page_range.setter
def page_range(self, page_range: Union[str, list]) -> None:
    if isinstance(page_range, str):
        self._page_range = utils.parse_page_range_with_rotation(page_range)
    elif isinstance(page_range, list) and all(isinstance(p, int) for p in page_range):
        # Convert old format to new format for backward compatibility
        self._page_range = [{"page": p, "rotation": 0} for p in page_range]
    else:
        self._page_range = page_range

3. PageFilter Rotation Support

File: pdfstitcher/processing/pagefilter.py

Changes to run() method:

  1. Import rotation matrix functions from pagetiler.py or create utility functions
  2. After adding each page to out_doc, apply rotation if specified:
    for page_info in self.page_range_with_rotation:
        p = page_info["page"]
        rotation = page_info["rotation"]
        
        # ... existing page addition logic ...
        
        if rotation != 0:
            self._apply_rotation_to_page(self.out_doc.pages[-1], rotation, user_unit)

New method: _apply_rotation_to_page(page, rotation_degrees, user_unit)

  • Convert page to XObject using page.as_form_xobject()
  • Apply rotation matrix based on rotation_degrees
  • Handle page dimension swapping for 90°/270° rotations
  • Update MediaBox and CropBox accordingly

4. PageTiler Per-Page Rotation

File: pdfstitcher/processing/pagetiler.py

Changes:

  1. Modify _process_page() to accept per-page rotation:

    def _process_page(self, page: pikepdf.Page, page_num: int, 
                      trim: list = [], page_rotation: int = 0) -> dict:
  2. In run() method, pass individual page rotation:

    for page_info in self.page_range_with_rotation:
        i = page_info["page"] - 1
        page_rotation = page_info["rotation"]
        
        info = self._process_page(
            self.in_doc.pages[i], 
            i, 
            trim_amounts,
            page_rotation
        )
  3. Modify _compute_T_matrix() to use per-page rotation instead of global rotation

5. CLI Updates

File: pdfstitcher/cli/app.py

Changes:

  1. Update help text for -p/--pages argument to mention rotation syntax
  2. Add validation for rotation values in page ranges
  3. Consider deprecating global -R/--rotate when using per-page rotation

6. GUI Updates

File: pdfstitcher/gui/io_tab.py

Changes:

  1. Update tooltip/help text for page range field
  2. Update example text to show rotation syntax: "1-3, 0, 4r90, 0, 5-10r180"
  3. Add validation feedback for invalid rotation values

7. Utility Functions

File: pdfstitcher/utils.py or new pdfstitcher/processing/rotation.py

New utilities:

def degrees_to_sw_rotation(degrees: int) -> SW_ROTATION:
    """Convert degrees (0, 90, 180, 270) to SW_ROTATION enum"""
    
def get_rotation_matrix(rotation: SW_ROTATION) -> list[float]:
    """Get the 2x2 rotation matrix for given rotation"""
    
def apply_rotation_to_dimensions(width: float, height: float, 
                                rotation: SW_ROTATION) -> tuple[float, float]:
    """Calculate new dimensions after rotation"""

Testing Considerations

  1. Backward Compatibility: Ensure existing page range strings work unchanged

  2. Edge Cases:

    • Invalid rotation values (e.g., "1r45")
    • Mixed formats (e.g., "1-3r90,4,5r180")
    • Repeated pages with different rotations
    • Blank pages (page 0) with rotation
  3. Integration Tests:

    • Test with PageFilter (simple rotation)
    • Test with PageTiler (rotation + tiling)
    • Test with LayerFilter active
    • Test CLI and GUI inputs

Implementation Order

  1. Implement parse_page_range_with_rotation() and test thoroughly
  2. Update ProcessingBase API with backward compatibility
  3. Add rotation support to PageFilter
  4. Modify PageTiler for per-page rotation
  5. Update CLI with new syntax support
  6. Update GUI with examples and validation
  7. Add comprehensive tests

Notes

  • The rotation values in CLI (0, 90, 180, 270) need to map to SW_ROTATION enum values
  • Consider caching rotation matrices for performance
  • Ensure UserUnit scaling is properly handled with rotation
  • MediaBox/CropBox updates must account for dimension swapping in 90°/270° rotations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment