Skip to content

Commit

Permalink
First pass implementing Canvas from template (#16)
Browse files Browse the repository at this point in the history
* added an ids property to TypedCollection
* Added support for getting items by index
* Create framing decision based on framing intent
* Added a common rounding function.
* Added scale_by and copy methods to Dimension classes
* added adjust_effective_anchor_point method
* added fit_source_to_target and get_desqueezed_width methods as part of implementing canvas templates in canvas
* added methods for adjusting anchor points based on current dimensions
* fixed typo in object map resulting in round not getting initialized
added rounding method
* set required key for pad_to_maximum
* added test for RoundStrategy
* Still WIP but has all but alignment in place.
* added fixture for RoundingStrategy
* Moved rounding logic to the RoundingStrategy class
* Anchor points now support alignment. Only to be used by canvas templates..
* Added a global variable to choose if we want to be precise and keep floating values or round them to even numbers.
* calculate size within target dimensions
* added docstrings
* Suuuuuuuper rudimentary test of creating a canvass based on template. Need to add more test

---------

Signed-off-by: apetrynet <flehnerheener@gmail.com>
  • Loading branch information
apetrynet authored Jun 21, 2024
1 parent 9dab1be commit 6945c28
Show file tree
Hide file tree
Showing 9 changed files with 617 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_pyfdl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 108 additions & 1 deletion src/pyfdl/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
import uuid
from abc import ABC, abstractmethod
from typing import Any, Union
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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}")'
188 changes: 143 additions & 45 deletions src/pyfdl/canvas.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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}")'
Loading

0 comments on commit 6945c28

Please sign in to comment.