diff --git a/.github/workflows/test_pyfdl.yml b/.github/workflows/test_pyfdl.yml index cd12750..1c857fa 100644 --- a/.github/workflows/test_pyfdl.yml +++ b/.github/workflows/test_pyfdl.yml @@ -35,7 +35,7 @@ jobs: - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --select=E9,F63,F7,F82 --ignore=F821 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest diff --git a/src/pyfdl/base.py b/src/pyfdl/base.py index 1924a3b..c26a4ad 100644 --- a/src/pyfdl/base.py +++ b/src/pyfdl/base.py @@ -1,3 +1,4 @@ +import math import uuid from abc import ABC, abstractmethod from typing import Any, Union @@ -8,6 +9,27 @@ FDL_SCHEMA_MINOR = 0 FDL_SCHEMA_VERSION = {'major': FDL_SCHEMA_MAJOR, 'minor': FDL_SCHEMA_MINOR} +# Global variable determining if we round values to even numbers or not +BE_PRECISE = False + + +def round_to_even(value: float) -> Union[int, float]: + """ + This will make sure we always end up with an even number + + Args: + value: initial value to round + + Returns: + value: even number + """ + global BE_PRECISE + if BE_PRECISE: + return value + + half = value / 2 + return round(half) * 2 + class Base(ABC): # Holds a list of known attributes @@ -180,6 +202,10 @@ def __init__(self, cls: Any): self._cls = cls self._data = {} + @property + def ids(self): + return list(self._data.keys()) + def add_item(self, item: Any): """Add an item to the collection. All items added to a collection get associated to the collection by passing itself @@ -230,7 +256,16 @@ def remove_item(self, item_id: str): if item_id in self._data: del self._data[item_id] - def _get_item_id(self, item): + def _get_item_id(self, item: Any) -> str: + """ + Get the "id" of the item based on the item's `id_attribute` + + Args: + item: + + Returns: + id: + """ return getattr(item, self._cls.id_attribute) def __len__(self): @@ -240,6 +275,9 @@ def __iter__(self): for item in self._data.values(): yield item + def __getitem__(self, item): + return self.get_item(self.ids[item]) + def __contains__(self, item: Any) -> bool: # We support both looking for an item by item.id and "string" for future use of collection try: @@ -264,6 +302,25 @@ def __init__(self, width: float, height: float): self.width = width self.height = height + def scale_by(self, factor: float) -> None: + """ + Scale the dimensions by the provider factor + + Args: + factor: + """ + self.width = round_to_even(self.width * factor) + self.height = round_to_even(self.height * factor) + + def copy(self) -> 'DimensionsFloat': + """ + Create a copy of these dimensions + + Returns: + copy: of these dimensions + """ + return DimensionsFloat(width=self.width, height=self.height) + def __eq__(self, other): return self.width == other.width and self.height == other.height @@ -285,6 +342,25 @@ def __init__(self, width: int, height: int): self.width = width.__int__() self.height = height.__int__() + def scale_by(self, factor: float) -> None: + """ + Scale the dimensions by the provider factor + + Args: + factor: + """ + self.width = round_to_even(self.width * factor).__int__() + self.height = round_to_even(self.height * factor).__int__() + + def copy(self) -> 'DimensionsInt': + """ + Create a copy of these dimensions + + Returns: + copy: of these dimensions + """ + return DimensionsInt(width=self.width, height=self.height) + def __eq__(self, other): return self.width == other.width and self.height == other.height @@ -366,5 +442,36 @@ def mode(self, value): self._mode = value + def round_dimensions(self, dimensions: DimensionsInt) -> DimensionsInt: + """ + Round the provided dimensions based on the rules defined in this object + + Args: + dimensions: + + Returns: + dimensions: rounded based on rules + + """ + even = self.even + mode = self.mode + + mode_map = { + 'up': math.ceil, + 'down': math.floor, + 'round': round + } + + width = mode_map[mode](dimensions.width) + height = mode_map[mode](dimensions.height) + + if even == 'even': + # width = round_to_even(width) + # height = round_to_even(height) + width = round(width / 2) * 2 + height = round(height / 2) * 2 + + return DimensionsInt(width=width, height=height) + def __repr__(self): return f'{self.__class__.__name__}(even="{self.even}", mode="{self.mode}")' diff --git a/src/pyfdl/canvas.py b/src/pyfdl/canvas.py index 20b2a1b..721b110 100644 --- a/src/pyfdl/canvas.py +++ b/src/pyfdl/canvas.py @@ -1,6 +1,8 @@ -from typing import Union, Tuple, Type +import math +from typing import Tuple, Type, Union, List from pyfdl import Base, DimensionsInt, Point, DimensionsFloat, FramingDecision, TypedCollection, FramingIntent +from pyfdl.base import round_to_even from pyfdl.errors import FDLError @@ -59,8 +61,8 @@ def place_framing_intent(self, framing_intent: FramingIntent) -> str: collection of framing decisions. The framing decision's properties are calculated for you. - If the canvas has effective dimensions set, these will be used for the calculations. Otherwise, we use the - dimensions + If the canvas has effective dimensions set, these will be used for the calculations. + Otherwise, we use the dimensions Args: framing_intent: framing intent to place in canvas @@ -69,49 +71,10 @@ def place_framing_intent(self, framing_intent: FramingIntent) -> str: framing_decision_id: id of the newly created framing decision """ - active_dimensions, active_anchor_point = self.get_dimensions() - - # Compare aspect ratios of canvas and framing intent - intent_quotient = framing_intent.aspect_ratio.width / framing_intent.aspect_ratio.height - canvas_quotient = active_dimensions.width / active_dimensions.height - if intent_quotient >= canvas_quotient: - # Need to calculate height - aspect_quotient = framing_intent.aspect_ratio.height / framing_intent.aspect_ratio.width - width = active_dimensions.width - # This trick was mentioned in a ASC MITC FDL meeting by someone, but I can't recall by whom - height = round((width * self.anamorphic_squeeze * aspect_quotient) / 2) * 2 - - else: - # Need to calculate width - width = round((active_dimensions.height * intent_quotient) / 2) * 2 - height = active_dimensions.height - - decision_id = f'{self.id}-{framing_intent.id}' - protection_dimensions = DimensionsFloat(width=width, height=height) - protection_anchor_point = Point( - x=(active_dimensions.width - protection_dimensions.width) / 2, - y=(active_dimensions.height - protection_dimensions.height) / 2 - ) - dimensions = DimensionsFloat( - width=round(protection_dimensions.width * (1 - framing_intent.protection) / 2) * 2, - height=round(protection_dimensions.height * (1 - framing_intent.protection) / 2) * 2 - ) - anchor_point = Point( - x=protection_anchor_point.x + (protection_dimensions.width - dimensions.width) / 2, - y=protection_anchor_point.y + (protection_dimensions.height - dimensions.height) / 2 - ) - framing_decision = FramingDecision( - _id=decision_id, - label=framing_intent.label, - framing_intent_id=framing_intent.id, - dimensions=dimensions, - anchor_point=anchor_point, - protection_dimensions=protection_dimensions, - protection_anchor_point=protection_anchor_point - ) + framing_decision = FramingDecision.from_framing_intent(self, framing_intent) self.framing_decisions.add_item(framing_decision) - return decision_id + return framing_decision.id def get_dimensions(self) -> Tuple[DimensionsInt, Point]: """ Get the most relevant dimensions and anchor point for the canvas. @@ -121,10 +84,145 @@ def get_dimensions(self) -> Tuple[DimensionsInt, Point]: (dimensions, anchor_point): """ - if self.effective_dimensions: + if self.effective_dimensions is not None: return self.effective_dimensions, self.effective_anchor_point return self.dimensions, Point(x=0, y=0) + @classmethod + def from_canvas_template( + cls, + canvas_template: 'CanvasTemplate', + source_canvas: 'Canvas', + source_framing_decision: Union[FramingDecision, int] = 0 + ) -> 'Canvas': + """ + Create a new `Canvas` from the provided `source_canvas` and `framing_decision` + based on a [CanvasTemplate](canvas_template.md#Canvas Template) + + Args: + canvas_template: + source_canvas: + source_framing_decision: + + Returns: + canvas: based on the provided canvas template and sources + + """ + if type(source_framing_decision) is int: + source_framing_decision = source_canvas.framing_decisions[source_framing_decision] + + canvas = Canvas( + label=canvas_template.label, + _id=Base.generate_uuid().strip('-'), + source_canvas_id=source_canvas.id, + anamorphic_squeeze=canvas_template.target_anamorphic_squeeze + ) + + framing_decision = FramingDecision( + label=source_framing_decision.label, + _id=f'{canvas.id}-{source_framing_decision.framing_intent_id}', + framing_intent_id=source_framing_decision.framing_intent_id + ) + canvas.framing_decisions.add_item(framing_decision) + + source_map = { + 'framing_decision': source_framing_decision, + 'canvas': source_canvas + } + + dest_map = { + 'framing_decision': framing_decision, + 'canvas': canvas + } + + # Figure out what dimensions to use + fit_source = canvas_template.fit_source + source_type, source_attribute = fit_source.split('.') + preserve = canvas_template.preserve_from_source_canvas or fit_source + if preserve in [None, 'none']: + preserve = fit_source + + # Get the scale factor + source_dimensions = getattr(source_map[source_type], source_attribute) + scale_factor = canvas_template.get_scale_factor( + source_dimensions, + source_canvas.anamorphic_squeeze + ) + + # Dummy dimensions to test against if we received a proper value + dummy_dimensions = DimensionsFloat(width=0, height=0) + + # Copy and scale dimensions from source to target + for transfer_key in canvas_template.get_transfer_keys(): + if transfer_key == fit_source: + target_size = canvas_template.fit_source_to_target( + source_dimensions, + source_canvas.anamorphic_squeeze + ) + setattr(dest_map[source_type], source_attribute, target_size) + continue + + source_type, dimension_source_attribute = transfer_key.split('.') + dimensions = getattr( + source_map[source_type], + dimension_source_attribute, + dummy_dimensions + ).copy() + + if dimensions == dummy_dimensions: + # Source canvas/framing decision is missing this dimension. Let's move on + continue + + dimensions.width = canvas_template.get_desqueezed_width( + dimensions.width, + source_canvas.anamorphic_squeeze + ) + dimensions.scale_by(scale_factor) + setattr(dest_map[source_type], dimension_source_attribute, dimensions) + + # Make sure the canvas has dimensions + if canvas.dimensions is None: + preserve_source_type, preserve_source_attribute = preserve.split('.') + canvas.dimensions = getattr(dest_map[preserve_source_type], preserve_source_attribute).copy() + + # Round values according to rules defined in the template + if canvas_template.round is not None: + canvas.dimensions = canvas_template.round.round_dimensions(canvas.dimensions) + + # Override canvas dimensions to maximum defined in template + if canvas_template.maximum_dimensions is not None: + canvas.dimensions = min(canvas_template.maximum_dimensions, canvas.dimensions) + if canvas_template.pad_to_maximum: + canvas.dimensions = canvas_template.maximum_dimensions + + # Make sure all anchor points are correct according to new sizes + canvas.adjust_effective_anchor_point() + framing_decision.adjust_protection_anchor_point( + canvas, + canvas_template.alignment_method_horizontal, + canvas_template.alignment_method_vertical + ) + framing_decision.adjust_anchor_point( + canvas, + canvas_template.alignment_method_horizontal, + canvas_template.alignment_method_vertical + ) + + return canvas + + def adjust_effective_anchor_point(self) -> None: + """ + Adjust the `effective_anchor_point` of this `Canvas` if `effective_dimensions` are set + """ + + if self.effective_dimensions is None: + return + + self.effective_anchor_point = Point( + x=(self.dimensions.width - self.effective_dimensions.width) / 2, + y=(self.dimensions.height - self.effective_dimensions.height) / 2 + ) + def __repr__(self): return f'{self.__class__.__name__}(label="{self.label}", id="{self.id}")' diff --git a/src/pyfdl/canvas_template.py b/src/pyfdl/canvas_template.py index 6d83f8c..fb6d481 100644 --- a/src/pyfdl/canvas_template.py +++ b/src/pyfdl/canvas_template.py @@ -1,6 +1,7 @@ -from typing import Union +from typing import Union, NamedTuple, Tuple, List -from pyfdl import Base, DimensionsInt, RoundStrategy, TypedCollection +from pyfdl import Base, DimensionsInt, DimensionsFloat, RoundStrategy +from pyfdl.base import round_to_even from pyfdl.errors import FDLError @@ -24,9 +25,16 @@ class CanvasTemplate(Base): object_map = { 'target_dimensions': DimensionsInt, 'maximum_dimensions': DimensionsInt, - 'rounding': RoundStrategy + 'round': RoundStrategy } - required = ['id', 'target_dimensions', 'target_anamorphic_squeeze', 'fit_source', 'fit_method'] + required = [ + 'id', + 'target_dimensions', + 'target_anamorphic_squeeze', + 'fit_source', + 'fit_method', + 'pad_to_maximum.maximum_dimensions' + ] defaults = { 'target_anamorphic_squeeze': 1, 'fit_source': 'framing_decision.dimensions', @@ -48,7 +56,7 @@ def __init__( alignment_method_horizontal: str = None, preserve_from_source_canvas: str = None, maximum_dimensions: DimensionsInt = None, - pad_to_maximum: bool = False, + pad_to_maximum: bool = None, _round: RoundStrategy = None ): self.label = label @@ -150,5 +158,182 @@ def preserve_from_source_canvas(self, value): self._preserve_from_source_canvas = value + def get_desqueezed_width( + self, + source_width: Union[float, int], + squeeze_factor: float + ) -> Union[float, int]: + """ + Get the de-squeezed width also considering the `target_anamorphic_squeeze`. + Used to calculate scaling of canvases and framing decisions. + If `target_anamorphic_squeeze` is 0, it's considered "same as source" and no de-squeeze + is applied. + + Args: + source_width: from source `Canvas` or `FramingDecision` + squeeze_factor: source `Canvas.anamorphic_squeeze` + + Returns: + width: scaled to size + """ + + width = source_width + + # target_anamorphic_squeeze of 0 is considered "same as source" + if self.target_anamorphic_squeeze > 0: + width = width * squeeze_factor / self.target_anamorphic_squeeze + + return width + + def get_scale_factor( + self, + source_dimensions: Union[DimensionsInt, DimensionsFloat], + source_anamorphic_squeeze: float + ) -> float: + """ + Calculate the scale factor used when creating a new `Canvas` and `FramingDecision` + + Args: + source_dimensions: + source_anamorphic_squeeze: + + Returns: + scale_factor: + """ + + # We default to fit_method "width" + source_width = self.get_desqueezed_width(source_dimensions.width, source_anamorphic_squeeze) + scale_factor = self.target_dimensions.width / source_width + + target_aspect = self.target_dimensions.width / self.target_dimensions.height + source_aspect = source_width / source_dimensions.height + + if self.fit_method == 'height': + scale_factor = self.target_dimensions.height / source_dimensions.height + + elif self.fit_method == 'fit_all': + if target_aspect > source_aspect: + # Target wider than source + scale_factor = self.target_dimensions.height / source_dimensions.height + + elif self.fit_method == 'fill': + # What's left outside the target dimensions due to fill? + if target_aspect < source_aspect: + # Source wider than target + scale_factor = self.target_dimensions.height / source_dimensions.height + + return scale_factor + + def fit_source_to_target( + self, + source_dimensions: Union[DimensionsInt, DimensionsFloat], + source_anamorphic_squeeze: float + ) -> Union[DimensionsInt, DimensionsFloat]: + """ + Calculate the dimensions of `fit_source` inside `target_dimensions` based on `fit_mode` + + Args: + source_dimensions: + source_anamorphic_squeeze: + + Returns: + size: + """ + # TODO: Add tests to see if this method actually does the right thing + + scale_factor = self.get_scale_factor(source_dimensions, source_anamorphic_squeeze) + source_width = self.get_desqueezed_width(source_dimensions.width, source_anamorphic_squeeze) + + # In case of fit_mode == fill + width = self.target_dimensions.width + height = self.target_dimensions.height + + if self.fit_method == 'width': + width = self.target_dimensions.width + # If scaled height exceeds target height, we crop the excess + height = min( + # round_to_even(source_dimensions.height * scale_factor), + source_dimensions.height * scale_factor, + self.target_dimensions.height + ) + # height = source_dimensions.height * scale_factor + # if height > self.target_dimensions.height: + # print("CROPPING HEIGHT", height) + + elif self.fit_method == 'height': + height = self.target_dimensions.height + scale_factor = height / source_dimensions.height + # If scaled width exceeds target width, we crop the excess + width = min( + # round_to_even(source_width * scale_factor), + source_width * scale_factor, + self.target_dimensions.width + ) + # width = source_width * scale_factor + # if width > self.target_dimensions.width: + # print("CROPPING WIDTH", width) + + elif self.fit_method == 'fit_all': + height = self.target_dimensions.height + width = source_width * scale_factor + if width > self.target_dimensions.width: + adjustment_scale = self.target_dimensions.width / width + height *= adjustment_scale + width *= adjustment_scale + + size = type(self.target_dimensions)(width=width, height=height) + # TODO consider returning crop True/False + # or at least coordinates outside of frame like data window vs display window + + return size + + def get_transfer_keys(self) -> List[str]: + """ + Get a list of attributes to transfer from source to destination in the order that + preserves all attributes between `fit_source` and `preserve_from_canvas` + + Returns: + keys: + """ + + dimension_routing_map = { + "framing_decision.dimensions": [ + "framing_decision.dimensions", + "framing_decision.protection_dimensions", + "canvas.effective_dimensions", + "canvas.dimensions" + ], + "framing_decision.protection_dimensions": [ + "framing_decision.protection_dimensions", + "framing_decision.dimensions", + "canvas.effective_dimensions", + "canvas.dimensions" + ], + "canvas.effective_dimensions": [ + "canvas.effective_dimensions", + "framing_decision.protection_dimensions", + "framing_decision.dimensions", + "canvas.dimensions" + ], + "canvas.dimensions": [ + "canvas.dimensions", + "framing_decision.protection_dimensions", + "framing_decision.dimensions", + "canvas.effective_dimensions" + ] + } + keys = dimension_routing_map[self.fit_source] + preserve = self.preserve_from_source_canvas + + if preserve in [None, 'none']: + preserve = self.fit_source + + first = keys.index(self.fit_source) + last = keys.index(preserve) + 1 + if first == last: + return [keys[first]] + + return keys[first:last] + def __repr__(self): return f'{self.__class__.__name__}(label="{self.label}", id="{self.id})"' diff --git a/src/pyfdl/framing_decision.py b/src/pyfdl/framing_decision.py index 240fd69..1ce35c0 100644 --- a/src/pyfdl/framing_decision.py +++ b/src/pyfdl/framing_decision.py @@ -1,6 +1,7 @@ from typing import Union from pyfdl import Base, DimensionsFloat, Point, TypedCollection +from pyfdl.base import round_to_even class FramingDecision(Base): @@ -40,6 +41,149 @@ def __init__( self.protection_dimensions = protection_dimensions self.protection_anchor_point = protection_anchor_point + @classmethod + def from_framing_intent(cls, canvas: 'Canvas', framing_intent: 'FramingIntent') -> 'FramingDecision': + """ + Create a new [FramingDecision](framing_decision.md#Framing Decision) based on the provided + [Canvas](canvas.md#Canvas) and [FramingIntent](framing_intent.md#Framing Intent) + + The framing decision's properties are calculated for you. + If the canvas has effective dimensions set, these will be used for the calculations. + Otherwise, we use the dimensions + + Args: + canvas: canvas to base framing decision on + framing_intent: framing intent to place in canvas + + Returns: + framing_decision: + + """ + framing_decision = FramingDecision( + _id=f'{canvas.id}-{framing_intent.id}', + label=framing_intent.label, + framing_intent_id=framing_intent.id + ) + + active_dimensions, active_anchor_point = canvas.get_dimensions() + + # Compare aspect ratios of framing intent and canvas + intent_aspect = framing_intent.aspect_ratio.width / framing_intent.aspect_ratio.height + canvas_aspect = active_dimensions.width / active_dimensions.height + if intent_aspect >= canvas_aspect: + width = active_dimensions.width + height = round_to_even((width * canvas.anamorphic_squeeze) / intent_aspect) + # height = (width * canvas.anamorphic_squeeze) / intent_aspect + + else: + width = round_to_even(active_dimensions.height * intent_aspect) + # width = active_dimensions.height * intent_aspect + height = active_dimensions.height + + if framing_intent.protection > 0: + protection_dimensions = DimensionsFloat(width=width, height=height) + framing_decision.protection_dimensions = protection_dimensions + framing_decision.adjust_protection_anchor_point(canvas) + + # We use the protection dimensions as base for dimensions if they're set + if framing_decision.protection_dimensions is not None: + width = framing_decision.protection_dimensions.width + height = framing_decision.protection_dimensions.height + + dimensions = DimensionsFloat( + width=round_to_even(width * (1 - framing_intent.protection)), + # width=width * (1 - framing_intent.protection), + height=round_to_even(height * (1 - framing_intent.protection)) + # height=height * (1 - framing_intent.protection) + ) + framing_decision.dimensions = dimensions + framing_decision.adjust_anchor_point(canvas) + + return framing_decision + + def adjust_anchor_point( + self, canvas: 'Canvas', + h_method: str = 'center', + v_method: str = 'center' + ) -> None: + """ + Adjust this object's `anchor_point` either relative to `protection_anchor_point` + or `canvas.effective_anchor_point` + Please note that the `h_method` and `v_method` arguments only apply if no + `protection_anchor_point` is present. + + Args: + canvas: to fetch anchor point from in case protection_anchor_point is not set + h_method: horizontal alignment ('left', 'center', 'right') + v_method: vertical alignment ('top', 'center', 'bottom') + """ + + # TODO check if anchor point is shifted before centering + _, active_anchor_point = canvas.get_dimensions() + + offset_point = self.protection_anchor_point or active_anchor_point + offset_dimensions = self.protection_dimensions or self.dimensions + + x = offset_point.x + y = offset_point.y + + if self.protection_anchor_point: + x += (offset_dimensions.width - self.dimensions.width) / 2 + y += (offset_dimensions.height - self.dimensions.height) / 2 + + else: + if h_method == 'center': + x += (offset_dimensions.width - self.dimensions.width) / 2 + + elif h_method == 'right': + x += (offset_dimensions.width - self.dimensions.width) + + if v_method == 'center': + y += (offset_dimensions.height - self.dimensions.height) / 2 + + elif v_method == 'bottom': + y += (offset_dimensions.height - self.dimensions.height) + + self.anchor_point = Point(x=x, y=y) + + def adjust_protection_anchor_point( + self, + canvas: 'Canvas', + h_method: str = 'center', + v_method: str = 'center' + ) -> None: + """ + Adjust this object's `protection_anchor_point` if `protection_dimensions` are set. + Please note that the `h_method` and `v_method` are primarily used when creating a canvas based on + a [canvas template](canvas.md#from_canvas_template) + + Args: + canvas: to fetch anchor point from in case protection_anchor_point is not set + h_method: horizontal alignment ('left', 'center', 'right') + v_method: vertical alignment ('top', 'center', 'bottom') + """ + + if self.protection_dimensions is None: + return + + active_dimensions, active_anchor_point = canvas.get_dimensions() + x = active_anchor_point.x + y = active_anchor_point.y + + if h_method == 'center': + x += (active_dimensions.width - self.protection_dimensions.width) / 2 + + elif h_method == 'right': + x += (active_dimensions.width - self.protection_dimensions.width) + + if v_method == 'center': + y += (active_dimensions.height - self.protection_dimensions.height) / 2 + + elif v_method == 'bottom': + y += (active_dimensions.height - self.protection_dimensions.height) + + self.protection_anchor_point = Point(x=x, y=y) + def __eq__(self, other): return ( self.id == other.id and diff --git a/tests/conftest.py b/tests/conftest.py index 06b2f3e..78dba1a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -169,3 +169,8 @@ def sample_canvas_template_kwargs() -> dict: "_round": {"even": "even", "mode": "up"} } return canvas_template + + +@pytest.fixture +def sample_rounding_strategy() -> dict: + return {"even": "even", "mode": "up"} diff --git a/tests/test_canvas.py b/tests/test_canvas.py index b2301f5..47eeed9 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -60,3 +60,17 @@ def test_place_framing_intent(sample_framing_intent, sample_canvas, sample_frami decision = canvas.framing_decisions.get_item(decision_id) facit_decision = pyfdl.FramingDecision.from_dict(sample_framing_decision) assert decision == facit_decision + + +def test_from_canvas_template(sample_canvas, sample_framing_decision, sample_canvas_template): + canvas = pyfdl.Canvas.from_dict(sample_canvas) + canvas_template = pyfdl.CanvasTemplate.from_dict(sample_canvas_template) + framing_decision = pyfdl.FramingDecision.from_dict(sample_framing_decision) + new_canvas = pyfdl.Canvas.from_canvas_template( + canvas_template, + canvas, + framing_decision + ) + + assert isinstance(new_canvas, pyfdl.Canvas) + assert new_canvas.dimensions != canvas.dimensions diff --git a/tests/test_canvas_template.py b/tests/test_canvas_template.py index 39a238d..1e9856e 100644 --- a/tests/test_canvas_template.py +++ b/tests/test_canvas_template.py @@ -78,3 +78,5 @@ def test_preserve_from_source_canvas_enum_validation(sample_canvas_template): canvas_template.preserve_from_source_canvas = faulty_value assert f'"{faulty_value}" is not a valid option for "preserve_from_source_canvas"' in str(err.value) + +# TODO: Add more tests for all the variations of fit_methods etc. diff --git a/tests/test_common.py b/tests/test_common.py index 6c9aedd..14c34fc 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -108,11 +108,20 @@ def test_rounding_strategy_default_values(): assert rs.check_required() == [] -def test_typed_container(sample_framing_intent, sample_framing_intent_kwargs): +def test_rounding_strategy_from_dict(sample_rounding_strategy): + rs = pyfdl.RoundStrategy.from_dict(sample_rounding_strategy) + assert isinstance(rs, pyfdl.RoundStrategy) + assert rs.even == "even" + assert rs.mode == "up" + + +def test_typed_collection(sample_framing_intent, sample_framing_intent_kwargs): td = pyfdl.TypedCollection(pyfdl.FramingIntent) fi = pyfdl.FramingIntent.from_dict(sample_framing_intent) td.add_item(fi) + assert td.ids == [f"{fi.id}"] + assert fi in td assert fi.id in td assert td.get_item(fi.id) == fi