import math
from typing import Optional, TypeVar, Dict, Tuple
import xml.etree.ElementTree as ET
import logging
logger = logging.getLogger(__name__)
# Although it is actually 360000, 1000000 is adopted here for convenience
# EMU_TO_CM = 1 / 360000
EMU_TO_CM = 1 / 1000000
T = TypeVar("T")
[docs]
def ensure_not_none(value: Optional[T], arg_name: str = "value") -> T:
if value is None:
raise ValueError(f"Invalid argument '{arg_name}': expected a non-None value")
return value
[docs]
def get_required_attribute(element: ET.Element, attr_name: str) -> str:
"""Retrieve a required attribute from an XML element.
Args:
element (ET.Element): The XML element to retrieve the attribute from.
attr_name (str): The name of the attribute to retrieve.
Returns:
str: The value of the attribute.
Raises:
ValueError: If the attribute is missing.
"""
value = ensure_not_none(element.get(attr_name), attr_name)
return value
[docs]
def get_required_element(
element: ET.Element, path: str, namespaces: Optional[Dict[str, str]] = None
) -> ET.Element:
"""Retrieve a required child element from an XML element.
Args:
element (ET.Element): The XML element to retrieve the child from.
path (str): The XPath to the child element.
namespaces (Optional[Dict[str, str]]): XML namespaces to use during the search.
Returns:
ET.Element: The found child element.
Raises:
ValueError: If the child element is missing.
"""
child = element.find(path, namespaces)
if child is None:
raise ValueError(f"Required element '{path}' is missing.")
return child
[docs]
def get_required_text(element: ET.Element) -> str:
text = element.text
if text is None:
raise ValueError(f"Required element '{element}' is missing.")
return text
[docs]
def get_element_text(
element: ET.Element, path: str, namespaces: Optional[Dict[str, str]] = None
) -> str:
child = get_required_element(element, path, namespaces)
text = get_required_text(child)
return text
[docs]
def get_attribute_or_none(
element: ET.Element, attr_name: str, default: Optional[str] = None
) -> Optional[str]:
return element.get(attr_name) or default
[docs]
def get_attribute_or_default(element: ET.Element, attr_name: str, default: str) -> str:
return element.get(attr_name) or default
[docs]
def get_element_or_none(
element: ET.Element, path: str, namespaces: Optional[Dict[str, str]] = None
) -> Optional[ET.Element]:
child = element.find(path, namespaces)
return child or None
[docs]
def emu_to_cm(emu: float) -> float:
"""Convert EMU (English Metric Units) to centimeters.
Args:
emu (float): The value in EMU to be converted.
Returns:
float: The converted value in centimeters, rounded to two decimal places.
"""
return round(emu * EMU_TO_CM, 2)
[docs]
def apply_rotation(
p: Tuple[float, float], c: Tuple[float, float], rotation: float
) -> Tuple[float, float]:
"""Apply rotation to a point around a center.
Args:
p (Tuple[float, float]): The point to be rotated.
c (Tuple[float, float]): The center of rotation.
rotation (float): The rotation angle in degrees.
Returns:
Tuple[float, float]: The new coordinates of the point after rotation.
"""
# Calculate the center coordinates
cx = c[0]
cy = c[1]
x = p[0]
y = p[1]
# Convert degrees to radians
# Change to clockwise
rad = math.radians(rotation)
# Calculate new x, y
new_x = cx + (x - cx) * math.cos(rad) - (y - cy) * math.sin(rad)
new_y = cy + (x - cx) * math.sin(rad) + (y - cy) * math.cos(rad)
return (new_x, new_y)
[docs]
def apply_scale(x: float, y: float, w: float, h: float, scale: float) -> Tuple[float, float, float, float]:
"""Apply scaling to a rectangle defined by its top-left corner and dimensions.
Args:
x (float): The x-coordinate of the top-left corner.
y (float): The y-coordinate of the top-left corner.
w (float): The width of the rectangle.
h (float): The height of the rectangle.
scale (float): The scaling factor.
Returns:
Tuple[float, float, float, float]: The new coordinates and dimensions after scaling.
"""
return x * scale, y * scale, w * scale, h * scale
[docs]
def apply_flip(
p: Tuple[float, float], c: Tuple[float, float], flip_h: bool, flip_v: bool
) -> Tuple[float, float]:
"""Apply horizontal and/or vertical flip to a point around a center.
Args:
p (Tuple[float, float]): The point to be flipped.
c (Tuple[float, float]): The center of flipping.
flip_h (bool): Whether to apply horizontal flip.
flip_v (bool): Whether to apply vertical flip.
Returns:
Tuple[float, float]: The new coordinates of the point after flipping.
"""
logger.info(f"flip_h: {flip_h}, flip_v: {flip_v}")
if flip_h:
logger.info(f"flip_h: {p}")
p = (c[0] - (p[0] - c[0]), p[1])
if flip_v:
logger.info(f"flip_v: {p}")
p = (p[0], c[1] - (p[1] - c[1]))
return p