ayon-core/pype/scripts/slates/slate_base/items.py
2020-03-26 16:27:52 +01:00

667 lines
20 KiB
Python

import os
import re
from PIL import Image
from .base import BaseObj
from .font_factory import FontFactory
class BaseItem(BaseObj):
available_parents = ["main_frame", "layer"]
@property
def item_pos_x(self):
if self.parent.obj_type == "main_frame":
return self._pos_x
return self.parent.child_pos_x(self.id)
@property
def item_pos_y(self):
if self.parent.obj_type == "main_frame":
return self._pos_y
return self.parent.child_pos_y(self.id)
def add_item(self, *args, **kwargs):
raise Exception("Can't add item to an item, use layers instead.")
def draw(self, image, drawer):
raise NotImplementedError(
"Method `draw` is not implemented for <{}>".format(
self.__clas__.__name__
)
)
class ItemImage(BaseItem):
obj_type = "image"
def __init__(self, image_path, *args, **kwargs):
self.image_path = image_path
super(ItemImage, self).__init__(*args, **kwargs)
def fill_data_format(self):
if re.match(self.fill_data_regex, self.image_path):
self.image_path = self.image_path.format(**self.fill_data)
def draw(self, image, drawer):
source_image = Image.open(os.path.normpath(self.image_path))
paste_image = source_image.resize(
(self.value_width(), self.value_height()),
Image.ANTIALIAS
)
image.paste(
paste_image,
(self.value_pos_x, self.value_pos_y)
)
def value_width(self):
return int(self.style["width"])
def value_height(self):
return int(self.style["height"])
class ItemRectangle(BaseItem):
obj_type = "rectangle"
def draw(self, image, drawer):
bg_color = self.style["bg-color"]
fill = self.style.get("fill", False)
kwargs = {}
if fill:
kwargs["fill"] = bg_color
else:
kwargs["outline"] = bg_color
start_pos_x = self.value_pos_x
start_pos_y = self.value_pos_y
end_pos_x = start_pos_x + self.value_width()
end_pos_y = start_pos_y + self.value_height()
drawer.rectangle(
(
(start_pos_x, start_pos_y),
(end_pos_x, end_pos_y)
),
**kwargs
)
def value_width(self):
return int(self.style["width"])
def value_height(self):
return int(self.style["height"])
class ItemPlaceHolder(BaseItem):
obj_type = "placeholder"
def __init__(self, image_path, *args, **kwargs):
self.image_path = image_path
super(ItemPlaceHolder, self).__init__(*args, **kwargs)
def fill_data_format(self):
if re.match(self.fill_data_regex, self.image_path):
self.image_path = self.image_path.format(**self.fill_data)
def draw(self, image, drawer):
bg_color = self.style["bg-color"]
kwargs = {}
if bg_color != "tranparent":
kwargs["fill"] = bg_color
start_pos_x = self.value_pos_x
start_pos_y = self.value_pos_y
end_pos_x = start_pos_x + self.value_width()
end_pos_y = start_pos_y + self.value_height()
drawer.rectangle(
(
(start_pos_x, start_pos_y),
(end_pos_x, end_pos_y)
),
**kwargs
)
def value_width(self):
return int(self.style["width"])
def value_height(self):
return int(self.style["height"])
def collect_data(self):
return {
"pos_x": self.value_pos_x,
"pos_y": self.value_pos_y,
"width": self.value_width(),
"height": self.value_height(),
"path": self.image_path
}
class ItemText(BaseItem):
obj_type = "text"
def __init__(self, value, *args, **kwargs):
self.value = value
super(ItemText, self).__init__(*args, **kwargs)
def draw(self, image, drawer):
bg_color = self.style["bg-color"]
if bg_color and bg_color.lower() != "transparent":
# TODO border outline styles
drawer.rectangle(
(self.content_pos_start, self.content_pos_end),
fill=bg_color,
outline=None
)
font_color = self.style["font-color"]
font_family = self.style["font-family"]
font_size = self.style["font-size"]
font_bold = self.style.get("font-bold", False)
font_italic = self.style.get("font-italic", False)
font = FontFactory.get_font(
font_family, font_size, font_italic, font_bold
)
drawer.text(
self.value_pos_start,
self.value,
font=font,
fill=font_color
)
def value_width(self):
font_family = self.style["font-family"]
font_size = self.style["font-size"]
font_bold = self.style.get("font-bold", False)
font_italic = self.style.get("font-italic", False)
font = FontFactory.get_font(
font_family, font_size, font_italic, font_bold
)
width = font.getsize(self.value)[0]
return int(width)
def value_height(self):
font_family = self.style["font-family"]
font_size = self.style["font-size"]
font_bold = self.style.get("font-bold", False)
font_italic = self.style.get("font-italic", False)
font = FontFactory.get_font(
font_family, font_size, font_italic, font_bold
)
height = font.getsize(self.value)[1]
return int(height)
class ItemTable(BaseItem):
obj_type = "table"
def __init__(self, values, use_alternate_color=False, *args, **kwargs):
self.values_by_cords = None
self.prepare_values(values)
super(ItemTable, self).__init__(*args, **kwargs)
self.size_values = None
self.calculate_sizes()
self.use_alternate_color = use_alternate_color
def add_item(self, item):
if item.obj_type == "table-item":
return
super(ItemTable, self).add_item(item)
def fill_data_format(self):
for item in self.values:
item.fill_data_format()
def prepare_values(self, _values):
values = []
values_by_cords = []
row_count = 0
col_count = 0
for row in _values:
row_count += 1
if len(row) > col_count:
col_count = len(row)
for row_idx in range(row_count):
values_by_cords.append([])
for col_idx in range(col_count):
values_by_cords[row_idx].append([])
if col_idx <= len(_values[row_idx]) - 1:
col = _values[row_idx][col_idx]
else:
col = ""
col_item = TableField(row_idx, col_idx, col, parent=self)
values_by_cords[row_idx][col_idx] = col_item
values.append(col_item)
self.values = values
self.values_by_cords = values_by_cords
def calculate_sizes(self):
row_heights = []
col_widths = []
for row_idx, row in enumerate(self.values_by_cords):
row_heights.append(0)
for col_idx, col_item in enumerate(row):
if len(col_widths) < col_idx + 1:
col_widths.append(0)
_width = col_widths[col_idx]
item_width = col_item.width()
if _width < item_width:
col_widths[col_idx] = item_width
_height = row_heights[row_idx]
item_height = col_item.height()
if _height < item_height:
row_heights[row_idx] = item_height
self.size_values = (row_heights, col_widths)
def draw(self, image, drawer):
bg_color = self.style["bg-color"]
if bg_color and bg_color.lower() != "transparent":
# TODO border outline styles
drawer.rectangle(
(self.content_pos_start, self.content_pos_end),
fill=bg_color,
outline=None
)
for value in self.values:
value.draw(image, drawer)
def value_width(self):
row_heights, col_widths = self.size_values
width = 0
for _width in col_widths:
width += _width
if width != 0:
width -= 1
return width
def value_height(self):
row_heights, col_widths = self.size_values
height = 0
for _height in row_heights:
height += _height
if height != 0:
height -= 1
return height
def content_pos_info_by_cord(self, row_idx, col_idx):
row_heights, col_widths = self.size_values
pos_x = int(self.value_pos_x)
pos_y = int(self.value_pos_y)
width = 0
height = 0
for idx, value in enumerate(col_widths):
if col_idx == idx:
width = value
break
pos_x += value
for idx, value in enumerate(row_heights):
if row_idx == idx:
height = value
break
pos_y += value
return (pos_x, pos_y, width, height)
class TableField(BaseItem):
obj_type = "table-item"
available_parents = ["table"]
ellide_text = "..."
def __init__(self, row_idx, col_idx, value, *args, **kwargs):
super(TableField, self).__init__(*args, **kwargs)
self.row_idx = row_idx
self.col_idx = col_idx
self.value = value
def recalculate_by_width(self, value, max_width):
padding = self.style["padding"]
padding_left = self.style.get("padding-left")
if padding_left is None:
padding_left = padding
padding_right = self.style.get("padding-right")
if padding_right is None:
padding_right = padding
max_width -= (padding_left + padding_right)
if not value:
return ""
word_wrap = self.style.get("word-wrap")
ellide = self.style.get("ellide")
max_lines = self.style.get("max-lines")
font_family = self.style["font-family"]
font_size = self.style["font-size"]
font_bold = self.style.get("font-bold", False)
font_italic = self.style.get("font-italic", False)
font = FontFactory.get_font(
font_family, font_size, font_italic, font_bold
)
val_width = font.getsize(value)[0]
if val_width <= max_width:
return value
if not ellide and not word_wrap:
# TODO logging
self.log.warning((
"Can't draw text because is too long with"
" `word-wrap` and `ellide` turned off <{}>"
).format(value))
return ""
elif ellide and not word_wrap:
max_lines = 1
words = [word for word in value.split()]
words_len = len(words)
lines = []
last_index = None
while True:
start_index = 0
if last_index is not None:
start_index = int(last_index) + 1
line = ""
for idx in range(start_index, words_len):
_word = words[idx]
connector = " "
if line == "":
connector = ""
_line = connector.join([line, _word])
_line_width = font.getsize(_line)[0]
if _line_width > max_width:
break
line = _line
last_index = idx
if line:
lines.append(line)
if last_index == words_len - 1:
break
elif last_index is None:
add_message = ""
if ellide:
add_message = " String was shortened to `{}`."
line = ""
for idx, char in enumerate(words[idx]):
_line = line + char + self.ellide_text
_line_width = font.getsize(_line)[0]
if _line_width > max_width:
if idx == 0:
line = _line
break
line = line + char
lines.append(line)
# TODO logging
self.log.warning((
"Font size is too big.{} <{}>"
).format(add_message, value))
break
output = ""
if not lines:
return output
over_max_lines = (max_lines and len(lines) > max_lines)
if not over_max_lines:
return "\n".join([line for line in lines])
lines = [lines[idx] for idx in range(max_lines)]
if not ellide:
return "\n".join(lines)
last_line = lines[-1]
last_line_width = font.getsize(last_line + self.ellide_text)[0]
if last_line_width <= max_width:
lines[-1] += self.ellide_text
return "\n".join([line for line in lines])
last_line_words = last_line.split()
if len(last_line_words) == 1:
if max_lines > 1:
# TODO try previous line?
lines[-1] = self.ellide_text
return "\n".join([line for line in lines])
line = ""
for idx, word in enumerate(last_line_words):
_line = line + word + self.ellide_text
_line_width = font.getsize(_line)[0]
if _line_width > max_width:
if idx == 0:
line = _line
break
line = _line
lines[-1] = line
return "\n".join([line for line in lines])
line = ""
for idx, _word in enumerate(last_line_words):
connector = " "
if line == "":
connector = ""
_line = connector.join([line, _word + self.ellide_text])
_line_width = font.getsize(_line)[0]
if _line_width <= max_width:
line = connector.join([line, _word])
continue
if idx != 0:
line += self.ellide_text
break
if max_lines != 1:
# TODO try previous line?
line = self.ellide_text
break
for idx, char in enumerate(_word):
_line = line + char + self.ellide_text
_line_width = font.getsize(_line)[0]
if _line_width > max_width:
if idx == 0:
line = _line
break
line = line + char
break
lines[-1] = line
return "\n".join([line for line in lines])
def fill_data_format(self):
value = self.value
if re.match(self.fill_data_regex, value):
value = value.format(**self.fill_data)
self.orig_value = value
max_width = self.style.get("max-width")
max_width = self.style.get("width") or max_width
if max_width:
value = self.recalculate_by_width(value, max_width)
self.value = value
def content_width(self):
width = self.style.get("width")
if width:
return int(width)
return super(TableField, self).content_width()
def content_height(self):
return super(TableField, self).content_height()
def value_width(self):
if not self.value:
return 0
font_family = self.style["font-family"]
font_size = self.style["font-size"]
font_bold = self.style.get("font-bold", False)
font_italic = self.style.get("font-italic", False)
font = FontFactory.get_font(
font_family, font_size, font_italic, font_bold
)
width = font.getsize_multiline(self.value)[0] + 1
min_width = self.style.get("min-height")
if min_width and min_width > width:
width = min_width
return int(width)
def value_height(self):
if not self.value:
return 0
height = self.style.get("height")
if height:
return int(height)
font_family = self.style["font-family"]
font_size = self.style["font-size"]
font_bold = self.style.get("font-bold", False)
font_italic = self.style.get("font-italic", False)
font = FontFactory.get_font(
font_family, font_size, font_italic, font_bold
)
height = font.getsize_multiline(self.value)[1] + 1
min_height = self.style.get("min-height")
if min_height and min_height > height:
height = min_height
return int(height)
@property
def item_pos_x(self):
pos_x, pos_y, width, height = (
self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)
)
return pos_x
@property
def item_pos_y(self):
pos_x, pos_y, width, height = (
self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)
)
return pos_y
@property
def value_pos_x(self):
pos_x, pos_y, width, height = (
self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)
)
alignment_hor = self.style["alignment-horizontal"].lower()
if alignment_hor in ["center", "centre"]:
pos_x += (width - self.value_width()) / 2
elif alignment_hor == "right":
pos_x += width - self.value_width()
else:
padding = self.style["padding"]
padding_left = self.style.get("padding-left")
if padding_left is None:
padding_left = padding
pos_x += padding_left
return int(pos_x)
@property
def value_pos_y(self):
pos_x, pos_y, width, height = (
self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)
)
alignment_ver = self.style["alignment-vertical"].lower()
if alignment_ver in ["center", "centre"]:
pos_y += (height - self.value_height()) / 2
elif alignment_ver == "bottom":
pos_y += height - self.value_height()
else:
padding = self.style["padding"]
padding_top = self.style.get("padding-top")
if padding_top is None:
padding_top = padding
pos_y += padding_top
return int(pos_y)
def draw(self, image, drawer):
pos_x, pos_y, width, height = (
self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx)
)
pos_start = (pos_x, pos_y)
pos_end = (pos_x + width, pos_y + height)
bg_color = self.style["bg-color"]
if self.parent.use_alternate_color and (self.row_idx % 2) == 1:
bg_color = self.style["bg-alter-color"]
if bg_color and bg_color.lower() != "transparent":
# TODO border outline styles
drawer.rectangle(
(pos_start, pos_end),
fill=bg_color,
outline=None
)
font_color = self.style["font-color"]
font_family = self.style["font-family"]
font_size = self.style["font-size"]
font_bold = self.style.get("font-bold", False)
font_italic = self.style.get("font-italic", False)
font = FontFactory.get_font(
font_family, font_size, font_italic, font_bold
)
alignment_hor = self.style["alignment-horizontal"].lower()
if alignment_hor == "centre":
alignment_hor = "center"
drawer.multiline_text(
self.value_pos_start,
self.value,
font=font,
fill=font_color,
align=alignment_hor
)