Source code for pdfbeaver.utils.pdf_geometry

# src/pdfbeaver/utils/pdf_geometry.py
"""
Geometry utilities for calculating cursor positions and transformations from PDF state.
"""
from typing import Any, Dict

import numpy as np


[docs] def extract_text_position(state: Dict[str, Any]) -> np.ndarray: """ Calculates the absolute (x, y, 1) position from the graphics state. Requires 'tstate' (Text State) and 'ctm' (Current Transformation Matrix). Handles both List-based CTM (pdfminer) and Numpy-based CTM. """ if not state or "tstate" not in state or "ctm" not in state: # Return origin if state is missing return np.array([0.0, 0.0, 1.0]) # Text Matrix (Tm) is typically a list/object with a .matrix attribute # [a, b, c, d, e, f] tstate = state["tstate"] if hasattr(tstate, "matrix"): tm = tstate.matrix elif isinstance(tstate, dict) and "matrix" in tstate: tm = tstate["matrix"] else: tm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] # Translation components (e, f) are at indices 4, 5 tx, ty = float(tm[4]), float(tm[5]) ctm = state["ctm"] # CASE A: CTM is a NumPy Array (3x3) # Used by StateTracker if isinstance(ctm, np.ndarray) and ctm.shape == (3, 3): # Point vector [x, y, 1] local_point = np.array([tx, ty, 1.0]) # Apply transformation: P_new = P_old @ Matrix return local_point @ ctm # CASE B: CTM is a List of 6 floats # Used by pdfminer / StreamStateIterator # CTM = [a, b, c, d, e, f] try: # x' = x*a + y*c + e # y' = x*b + y*d + f # tx corresponds to x, ty corresponds to y (in text space origin) ux = tx * ctm[0] + ty * ctm[2] + ctm[4] uy = tx * ctm[1] + ty * ctm[3] + ctm[5] return np.array([ux, uy, 1.0]) except (IndexError, TypeError): # Fallback for malformed state return np.array([tx, ty, 1.0])