"""
Module for Image Crop.
.. versionadded:: 0.9.0
"""
from __future__ import annotations
from typing import Any, Tuple, Type, cast, TypeVar, overload, TYPE_CHECKING
import math
from ooo.dyn.text.graphic_crop import GraphicCrop
from ooodev.events.args.cancel_event_args import CancelEventArgs
from ooodev.exceptions import ex as mEx
from ooodev.format.inner.common.props.image_crop_props import ImageCropProps
from ooodev.format.inner.direct.structs.crop_struct import CropStruct
from ooodev.format.inner.kind.format_kind import FormatKind
from ooodev.format.inner.style_base import StyleMulti
from ooodev.utils import images_lo as mImg
from ooodev.utils import props as mProps
from ooodev.utils.data_type.size import Size
from ooodev.utils.data_type.size_mm import SizeMM
if TYPE_CHECKING:
from ooodev.units.unit_obj import UnitT
from ooodev.proto.size_obj import SizeObj
else:
SizeObj = Any
UnitT = Any
_TImageCrop = TypeVar("_TImageCrop", bound="ImageCrop")
[docs]class CropOpt(CropStruct):
# region Init
[docs] def __init__(
self,
*,
left: float | UnitT = 0,
right: float | UnitT = 0,
top: float | UnitT = 0,
bottom: float | UnitT = 0,
all: float | UnitT | None = None,
keep_scale: bool = True,
) -> None:
"""
Constructor
Args:
left (float, UnitT, optional): Specifies left crop in ``mm`` units or :ref:`proto_unit_obj`. Default ``0.0``.
right (float, UnitT, optional): Specifies right crop in ``mm`` units or :ref:`proto_unit_obj`. Default ``0.0``.
top (float, UnitT, optional): Specifies top crop in ``mm`` units or :ref:`proto_unit_obj`. Default ``0.0``.
bottom (float, UnitT, optional): Specifies bottom crop in ``mm`` units or :ref:`proto_unit_obj`. Default ``0.0``.
keep_scale (bool, options): If ``True`` then crop is
all (float, UnitT, optional): Specifies ``left``, ``right``, ``top``, and ``bottom`` in ``mm`` units or :ref:`proto_unit_obj`. If set all other parameters are ignored.
keep_scale (bool, optional): If ``True`` crop is applied keeping image scale; Otherwise crop is applied keeping image size. Defaults to ``True``.
"""
super().__init__(left=left, right=right, top=top, bottom=bottom, all=all)
self._keep_scale = keep_scale
# endregion Init
# region Overrides
[docs] def copy(self, **kwargs) -> CropOpt:
cp = super().copy(**kwargs)
cp._keep_scale = self._keep_scale
return cp
# endregion Overrides
# region dunder methods
def __eq__(self, oth: object) -> bool:
result = super().__eq__(oth)
if result == False:
return False
if isinstance(oth, CropOpt):
return self.prop_keep_scale == oth.prop_keep_scale
return False
# endregion dunder methods
# region Methods
[docs] def can_crop(self) -> bool:
"""
Gets if values are valid for crop.
Returns:
bool: ``True`` if options are valid; Otherwise, ``False``.
"""
result = False
for prop in self._props:
pv = cast(int, self._get(prop))
if pv != 0:
result = True
break
return result
# endregion Methods
# region Overrides
@property
def prop_keep_scale(self) -> bool:
"""
Gets/Sets keep scale.
"""
return self._keep_scale
@prop_keep_scale.setter
def prop_keep_scale(self, value: bool):
self._keep_scale = value
# endregion Overrides
[docs]class ImageCrop(StyleMulti):
"""
Crops and/or resizes an image.
General Rules.
**Crop Rules**
Rules for ``CropOpt.keep_scale=True``.
.. cssclass:: ul-list
- If scale is passed in then image size is to be calculated from that scale using original image values, factoring in crop values.
- If scale is not passed in image size is calculated from 100% using original image values, factoring in crop values.
- If Image size is passed in then it is ignored.
Rules for ``CropOpt.keep_scale=False``.
.. cssclass:: ul-list
- If image size is passed in then it is used to set image size, factoring in crop values.
- If image size is not passed in then then the original image size is used to set image size, factoring in crop values.
**No Crop Rules**
Rules when crop is not passed to constructor.
.. cssclass:: ul-list
- If image size is present it is used to set the image size. In this case scale is ignored. Any existing Crop values are ignored.
- If scale is present but no image size is present then a new size is derived from original size using scale.
- If both image size and scale size are present then scale is ignored.
.. versionadded:: 0.9.0
"""
# region Init
[docs] def __init__(
self,
*,
crop: CropOpt | None = None,
img_size: SizeMM | None = None,
img_scale: SizeObj | None = None,
) -> None:
"""
Constructor
Args:
crop: (CropOpt, optional): Specifies crop values.
img_size (SizeMM, optional): Specifies image size.
image_scale (SizeObj, optional): Specifies image scale.
"""
super().__init__()
self._img_size = img_size
self._img_scale = None if img_scale is None else Size.from_size(img_scale)
if crop is not None:
co = crop.copy(_cattribs=self._get_struct_cattrib())
self._set_style("crop_struct", co, *co.get_attrs())
# endregion Init
# region internal methods
def _get_struct_cattrib(self) -> dict:
return {
"_property_name": self._props.crop_struct,
"_supported_services_values": self._supported_services(),
"_format_kind_prop": self.prop_format_kind,
}
def _rule_crop_keep_scale_scale(self, orig_size: Size) -> Size:
# sourcery skip: class-extract-method
# orig_size in 1/100th mm
# returns 1/100th mm
# If scale is passed in then image size is to be calculated from that scale using original image values, factoring in crop values
img_scale = self.prop_img_scale
if img_scale is None:
raise ValueError("Image Scale is required for rule crop-scale.")
crop = self.prop_crop_opt
if crop is None:
raise ValueError("Crop Options are required for rule crop-scale.")
img_width = orig_size.width
img_height = orig_size.height
# do calculations in 1/100th mm
width = mImg.ImagesLo.calc_keep_scale_len(
orig_len=img_width,
start_crop=crop.prop_top.get_value_mm100(),
end_crop=crop.prop_bottom.get_value_mm100(),
scale=img_scale.width / 100,
)
height = mImg.ImagesLo.calc_keep_scale_len(
orig_len=img_height,
start_crop=crop.prop_left.get_value_mm100(),
end_crop=crop.prop_right.get_value_mm100(),
scale=img_scale.height / 100,
)
# returns 1/100th mm
return Size(width=round(width), height=round(height))
def _rule_crop_keep_scale_no_scale(self, orig_size: Size) -> Size:
# orig_size in 1/100th mm
# returns 1/100th mm
# If scale is not passed in image size is calculated from 100% using original image values, factoring in crop values.
img_scale = Size(100, 100)
crop = self.prop_crop_opt
if crop is None:
raise ValueError("Crop Options are required for rule crop-scale.")
img_width = orig_size.width
img_height = orig_size.height
# do calculations in 1/100th mm
width = mImg.ImagesLo.calc_keep_scale_len(
orig_len=img_width,
start_crop=crop.prop_top.get_value_mm100(),
end_crop=crop.prop_bottom.get_value_mm100(),
scale=img_scale.width / 100,
)
height = mImg.ImagesLo.calc_keep_scale_len(
orig_len=img_height,
start_crop=crop.prop_left.get_value_mm100(),
end_crop=crop.prop_right.get_value_mm100(),
scale=img_scale.height / 100,
)
# returns 1/100th mm
return Size(width=round(width), height=round(height))
def _rule_no_crop_image(self) -> Size:
# returns 1/100th mm
# image size is present use it. In this case scale is ignored
if self.prop_img_size is None:
raise ValueError("Crop Image is required for rule no-crop-image.")
return self.prop_img_size.get_size_mm100()
def _rule_no_crop_scale_no_image(self, orig_size: Size) -> Size:
# orig_size in 1/100th mm
# returns 1/100th mm
# Scale No images size calculate new size from original size using scale.
if self.prop_img_scale is None:
raise ValueError("Crop Image is required for rule no-crop-scale-no-image.")
factor_width = self.prop_img_scale.width / 100
factor_height = self.prop_img_scale.height / 100
new_width = orig_size.width * factor_width
new_height = orig_size.height * factor_height
return Size(width=round(new_width), height=round(new_height))
def _rule_crop_keep_image(self, orig_size: Size) -> Size:
# orig_size in 1/100th mm
# returns 1/100th mm
# If image size is passed in then it is use that the image size
# If image is not passed in then then the original image size is used
if self.prop_img_size is None:
return orig_size
return self.prop_img_size.get_size_mm100()
def _get_keep_scale_value(self, orig_size: Size) -> Size:
# orig_size in 1/100th mm
# If scale is passed in then image size is to be calculated from that scale using original image values, factoring in crop values
# If scale is not passed in image size is calculated from 100% using original image values, scale factoring in crop values.
# If Image size is passed in then it is ignored.
if self.prop_img_scale is not None:
return self._rule_crop_keep_scale_scale(orig_size)
return self._rule_crop_keep_scale_no_scale(orig_size)
# endregion internal methods
# region Overrides
def _supported_services(self) -> Tuple[str, ...]:
try:
return self._supported_services_values
except AttributeError:
self._supported_services_values = ("com.sun.star.text.TextGraphicObject",)
return self._supported_services_values
def _on_modifying(self, source: Any, event_args: CancelEventArgs) -> None:
if self._is_default_inst:
raise ValueError("Modifying a default instance is not allowed")
return super()._on_modifying(source, event_args)
# region Copy()
@overload
def copy(self: _TImageCrop) -> _TImageCrop: ...
@overload
def copy(self: _TImageCrop, **kwargs) -> _TImageCrop: ...
[docs] def copy(self: _TImageCrop, **kwargs) -> _TImageCrop:
"""Gets a copy of instance as a new instance"""
cp = super().copy(**kwargs)
cp._img_size = self._img_size
cp._img_scale = self._img_scale
return cp
# endregion Copy(
# region apply()
@overload
def apply(self, obj: Any) -> None: ...
@overload
def apply(self, obj: Any, **kwargs) -> None: ...
[docs] def apply(self, obj: Any, **kwargs) -> None:
"""
Applies style of current instance.
Args:
obj (Any): UNO Object that styles are to be applied.
"""
# sourcery skip: de-morgan, hoist-statement-from-if, merge-else-if-into-elif, use-named-expression
if not self._is_valid_obj(obj):
self._print_not_valid_srv(method_name="apply")
return
actual_size = cast(SizeObj, mProps.Props.get(obj, self._props.actual_size))
orig_size = Size.from_size(actual_size)
apply_clear = kwargs.pop("_apply_clear", True)
if apply_clear:
self._clear()
crop = self.prop_crop_opt
if crop is None:
sz = None
if not self.prop_img_size is None:
sz = self._rule_no_crop_image()
elif not self.prop_img_scale is None:
sz = self._rule_no_crop_scale_no_image(orig_size)
else:
if crop.prop_keep_scale:
sz = self._get_keep_scale_value(orig_size)
else:
sz = self._rule_crop_keep_image(orig_size)
if not sz is None:
self._set(self._props.height, sz.height)
self._set(self._props.width, sz.width)
super().apply(obj, **kwargs)
# endregion apply()
# endregion Overrides
# region Static Methods
# region reset_to_original_size()
@overload
@classmethod
def reset_image_original_size(cls, obj: object) -> None: ...
@overload
@classmethod
def reset_image_original_size(cls, obj: object, **kwargs) -> None: ...
[docs] @classmethod
def reset_image_original_size(cls, obj: object, **kwargs) -> None:
"""
Resets the image to its original size. Resetting crop, scale and size.
Args:
obj (object): UNO Image Object
Raises:
NotSupportedError: If ``obj`` is not supported.
"""
inst = cls(crop=CropOpt(all=0), img_scale=Size(100, 100), **kwargs)
if not inst._is_valid_obj(obj):
raise mEx.NotSupportedError(f'Object is not supported for conversion to "{cls.__name__}"')
actual_size = cast(SizeObj, mProps.Props.get(obj, inst._props.actual_size))
inst.prop_img_size = SizeMM.from_size_mm100(actual_size)
inst.apply(obj)
# endregion reset_to_original_size()
# region from_obj_get_size()
@overload
@classmethod
def get_image_original_size(cls, obj: object) -> Size: ...
@overload
@classmethod
def get_image_original_size(cls, obj: object, **kwargs) -> Size: ...
[docs] @classmethod
def get_image_original_size(cls, obj: object, **kwargs) -> Size:
"""
Gets size from object in ``1/100th mm`` units.
Args:
obj (object): UNO Object.
Raises:
NotSupportedError: If ``obj`` is not supported.
Returns:
Size: Size in ``1/100th mm`` units.
"""
inst = cls(**kwargs)
if not inst._is_valid_obj(obj):
raise mEx.NotSupportedError(f'Object is not supported for conversion to "{cls.__name__}"')
actual_size = cast(SizeObj, mProps.Props.get(obj, inst._props.actual_size))
return Size.from_size(actual_size)
# endregion from_obj_get_size()
# region from_obj()
@overload
@classmethod
def from_obj(cls: Type[_TImageCrop], obj: object) -> _TImageCrop: ...
@overload
@classmethod
def from_obj(cls: Type[_TImageCrop], obj: object, **kwargs) -> _TImageCrop: ...
[docs] @classmethod
def from_obj(cls: Type[_TImageCrop], obj: object, **kwargs) -> _TImageCrop:
"""
Gets instance from object
Args:
obj (object): UNO object.
Raises:
NotSupportedError: If ``obj`` is not supported.
Returns:
Crop: Instance that represents Image crop.
"""
inst = cls(**kwargs)
if not inst._is_valid_obj(obj):
raise mEx.NotSupportedError(f'Object is not supported for conversion to "{cls.__name__}"')
actual_size = cast(SizeObj, mProps.Props.get(obj, inst._props.actual_size))
size = cast(SizeObj, mProps.Props.get(obj, inst._props.size))
inst.prop_img_size = SizeMM.from_size_mm100(size)
crop_struct = cast(GraphicCrop, mProps.Props.get(obj, inst._props.crop_struct))
cs = CropOpt.from_uno_struct(value=crop_struct, _cattribs=inst._get_struct_cattrib())
# get scale
scale_W = mImg.ImagesLo.calc_scale_crop(
orig_len=actual_size.Width, new_len=size.Width, start_crop=crop_struct.Left, end_crop=crop_struct.Right
)
scale_h = mImg.ImagesLo.calc_scale_crop(
orig_len=actual_size.Height, new_len=size.Height, start_crop=crop_struct.Top, end_crop=crop_struct.Bottom
)
inst.prop_img_scale = Size(math.ceil(scale_W), math.ceil(scale_h))
sz_actual = Size.from_size(actual_size)
sz_size = Size.from_size(size)
cs.prop_keep_scale = sz_actual != sz_size
inst._set_style("crop_struct", cs, *cs.get_attrs())
return inst
# endregion from_obj()
# endregion Static Methods
# region Properties
@property
def prop_format_kind(self) -> FormatKind:
"""Gets the kind of style"""
try:
return self._format_kind_prop
except AttributeError:
self._format_kind_prop = FormatKind.IMAGE
return self._format_kind_prop
@property
def prop_img_size(self) -> SizeMM | None:
"""
Gets or Sets image size.
"""
return self._img_size
@prop_img_size.setter
def prop_img_size(self, value: SizeMM | None):
self._img_size = value
@property
def prop_img_scale(self) -> Size | None:
"""
Gets or Sets image scale.
"""
return self._img_scale
@prop_img_scale.setter
def prop_img_scale(self, value: SizeObj | None):
if value is None:
self._img_scale = None
else:
sz = Size.from_size(value)
sz.height = max(sz.height, 1)
sz.width = max(sz.width, 1)
self._img_scale = sz
@property
def prop_crop_opt(self) -> CropOpt | None:
"""Gets or Sets Crop Struct instance"""
try:
return self._direct_inner
except AttributeError:
self._direct_inner = cast(CropOpt, self._get_style_inst("crop_struct"))
return self._direct_inner
@prop_crop_opt.setter
def prop_crop_opt(self, value: CropOpt | None) -> None:
self._del_attribs("_direct_inner")
if value is None:
self._remove_style("crop_struct")
return
self._set_style("crop_struct", value, *value.get_attrs())
@property
def _props(self) -> ImageCropProps:
try:
return self._props_internal_attributes
except AttributeError:
self._props_internal_attributes = ImageCropProps(
crop_struct="GraphicCrop", width="Width", height="Height", size="Size", actual_size="ActualSize"
)
return self._props_internal_attributes
# endregion Properties