From 6ef5853b65cb86cbdd94ae8d713ddb1fac753bdf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 16:33:15 +0100 Subject: [PATCH] reorganized slates a little bit --- pype/scripts/slate/base.py | 1459 ----------------- pype/scripts/slate/default_style.json | 44 - pype/scripts/slate/lib.py | 114 ++ pype/scripts/slate/slate.py | 355 ---- pype/scripts/slate/slate_base/__init__.py | 0 pype/scripts/slate/slate_base/base.py | 373 +++++ .../slate/slate_base/default_style.json | 58 + pype/scripts/slate/slate_base/font_factory.py | 93 ++ pype/scripts/slate/slate_base/items.py | 666 ++++++++ pype/scripts/slate/slate_base/layer.py | 139 ++ pype/scripts/slate/slate_base/main_frame.py | 77 + pype/scripts/slate/style_schema.json | 44 - 12 files changed, 1520 insertions(+), 1902 deletions(-) delete mode 100644 pype/scripts/slate/base.py delete mode 100644 pype/scripts/slate/default_style.json create mode 100644 pype/scripts/slate/lib.py delete mode 100644 pype/scripts/slate/slate.py create mode 100644 pype/scripts/slate/slate_base/__init__.py create mode 100644 pype/scripts/slate/slate_base/base.py create mode 100644 pype/scripts/slate/slate_base/default_style.json create mode 100644 pype/scripts/slate/slate_base/font_factory.py create mode 100644 pype/scripts/slate/slate_base/items.py create mode 100644 pype/scripts/slate/slate_base/layer.py create mode 100644 pype/scripts/slate/slate_base/main_frame.py delete mode 100644 pype/scripts/slate/style_schema.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py deleted file mode 100644 index 1f90f92992..0000000000 --- a/pype/scripts/slate/base.py +++ /dev/null @@ -1,1459 +0,0 @@ -import os -import sys -import re -import copy -import json -import collections -from queue import Queue -sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") -from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor -from uuid import uuid4 - - -class BaseObj: - """Base Object for slates.""" - - obj_type = None - available_parents = [] - all_style_keys = [ - "font-family", "font-size", "font-color", "font-bold", "font-italic", - "bg-color", "bg-alter-color", - "alignment-horizontal", "alignment-vertical", - "padding", "padding-left", "padding-right", - "padding-top", "padding-bottom", - "margin", "margin-left", "margin-right", - "margin-top", "margin-bottom", "width", "height", - "fill" - ] - fill_data_regex = r"{[^}]+}" - - def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): - if not self.obj_type: - raise NotImplementedError( - "Class don't have set object type <{}>".format( - self.__class__.__name__ - ) - ) - - parent_obj_type = None - if parent: - parent_obj_type = parent.obj_type - - if parent_obj_type not in self.available_parents: - expected_parents = ", ".join(self.available_parents) - raise Exception(( - "Invalid parent <{}> for <{}>. Expected <{}>" - ).format( - parent.__class__.__name__, self.obj_type, expected_parents - )) - - self.parent = parent - self._style = style - - self.id = uuid4() - self.name = name - self.items = {} - - self._pos_x = pos_x or 0 - self._pos_y = pos_y or 0 - - if parent: - parent.add_item(self) - - def fill_data_format(self): - return - - @property - def fill_data(self): - return self.parent.fill_data - - @property - def main_style(self): - default_style_v1 = { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": False, - "font-italic": False, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top" - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "padding": 0, - "margin": 0, - "bg-color": "#252525" - }, - "table": { - "padding": 0, - "margin": 0, - "bg-color": "transparent" - }, - "table-item": { - "padding": 5, - "padding-bottom": 10, - "margin": 0, - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - } - } - return default_style_v1 - - def height(self): - raise NotImplementedError( - "Attribute `height` is not implemented for <{}>".format( - self.__clas__.__name__ - ) - ) - - def width(self): - raise NotImplementedError( - "Attribute `width` is not implemented for <{}>".format( - self.__clas__.__name__ - ) - ) - - def collect_data(self): - return None - - def find_item(self, obj_type=None, name=None): - obj_type_fits = False - name_fits = False - if obj_type is None or self.obj_type == obj_type: - obj_type_fits = True - - if name is None or self.name != name: - name_fits = True - - output = [] - if obj_type_fits and name_fits: - output.append(self) - - if not self.items: - return output - - for item in self.items.values(): - output.extend( - item.find_item(obj_type=obj_type, name=name) - ) - return output - - @property - def full_style(self): - if self.parent is not None: - style = dict(val for val in self.parent.full_style.items()) - else: - style = self.main_style - - for key, value in self._style.items(): - if key in self.all_style_keys: - # TODO which variant is right? - style[self.obj_type][key] = value - # style["*"][key] = value - else: - if key not in style: - style[key] = {} - - if isinstance(style[key], dict): - style[key].update(value) - else: - style[key] = value - - return style - - def get_style_for_obj_type(self, obj_type, style=None): - if not style: - style = copy.deepcopy(self.full_style) - - base = style.get("*") or {} - obj_specific = style.get(obj_type) or {} - name_specific = {} - if self.name: - name = str(self.name) - if not name.startswith("#"): - name = "#" + name - name_specific = style.get(name) or {} - - if obj_type == "table-item": - col_regex = r"table-item-col\[([\d\-, ]+)*\]" - row_regex = r"table-item-row\[([\d\-, ]+)*\]" - field_regex = ( - r"table-item-field\[(([ ]+)?\d+([ ]+)?:([ ]+)?\d+([ ]+)?)*\]" - ) - # STRICT field regex (not allowed spaces) - # fild_regex = r"table-item-field\[(\d+:\d+)*\]" - - def get_indexes_from_regex_match(result, field=False): - group = result.group(1) - indexes = [] - if field: - return [ - int(part.strip()) for part in group.strip().split(":") - ] - - parts = group.strip().split(",") - for part in parts: - part = part.strip() - if "-" not in part: - indexes.append(int(part)) - continue - - sub_parts = [ - int(sub.strip()) for sub in part.split("-") - ] - if len(sub_parts) != 2: - # TODO logging - print("invalid range '{}'".format(part)) - continue - - for idx in range(sub_parts[0], sub_parts[1]+1): - indexes.append(idx) - return indexes - - for key, value in style.items(): - if not key.startswith(obj_type): - continue - - result = re.search(col_regex, key) - if result: - indexes = get_indexes_from_regex_match(result) - if self.col_idx in indexes: - obj_specific.update(value) - continue - - result = re.search(row_regex, key) - if result: - indexes = get_indexes_from_regex_match(result) - if self.row_idx in indexes: - obj_specific.update(value) - continue - - result = re.search(field_regex, key) - if result: - col_idx, row_idx = get_indexes_from_regex_match( - result, True - ) - if self.col_idx == col_idx and self.row_idx == row_idx: - obj_specific.update(value) - - output = {} - output.update(base) - output.update(obj_specific) - output.update(name_specific) - - return output - - @property - def style(self): - return self.get_style_for_obj_type(self.obj_type) - - @property - def item_pos_x(self): - if self.parent.obj_type == "main_frame": - return int(self._pos_x) - return 0 - - @property - def item_pos_y(self): - if self.parent.obj_type == "main_frame": - return int(self._pos_y) - return 0 - - @property - def content_pos_x(self): - pos_x = self.item_pos_x - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - pos_x += margin_left - - return pos_x - - @property - def content_pos_y(self): - pos_y = self.item_pos_y - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - return pos_y + margin_top - - @property - def value_pos_x(self): - pos_x = int(self.content_pos_x) - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - pos_x += padding_left - - return pos_x - - @property - def value_pos_y(self): - pos_y = int(self.content_pos_y) - padding = self.style["padding"] - padding_top = self.style.get("padding-top") - if padding_top is None: - padding_top = padding - - pos_y += padding_top - - return pos_y - - @property - def value_pos_start(self): - return (self.value_pos_x, self.value_pos_y) - - @property - def value_pos_end(self): - pos_x, pos_y = self.value_pos_start - pos_x += self.width() - pos_y += self.height() - return (pos_x, pos_y) - - @property - def content_pos_start(self): - return (self.content_pos_x, self.content_pos_y) - - @property - def content_pos_end(self): - pos_x, pos_y = self.content_pos_start - pos_x += self.content_width() - pos_y += self.content_height() - return (pos_x, pos_y) - - def value_width(self): - raise NotImplementedError( - "Attribute is not implemented <{}>".format( - self.__class__.__name__ - ) - ) - - def value_height(self): - raise NotImplementedError( - "Attribute is not implemented for <{}>".format( - self.__class__.__name__ - ) - ) - - def content_width(self): - width = self.value_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 - - return width + padding_left + padding_right - - def content_height(self): - height = self.value_height() - padding = self.style["padding"] - padding_top = self.style.get("padding-top") - if padding_top is None: - padding_top = padding - - padding_bottom = self.style.get("padding-bottom") - if padding_bottom is None: - padding_bottom = padding - - return height + padding_top + padding_bottom - - def width(self): - width = self.content_width() - - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - - return width + margin_left + margin_right - - def height(self): - height = self.content_height() - - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin - - return height + margin_bottom + margin_top - - def add_item(self, item): - self.items[item.id] = item - item.fill_data_format() - - - def reset(self): - for item in self.items.values(): - item.reset() - - -class MainFrame(BaseObj): - - obj_type = "main_frame" - available_parents = [None] - - def __init__( - self, width, height, destination_path, fill_data={}, *args, **kwargs - ): - kwargs["parent"] = None - super(MainFrame, self).__init__(*args, **kwargs) - self._width = width - self._height = height - self.dst_path = destination_path - self._fill_data = fill_data - self.fill_data_format() - - def fill_data_format(self): - if re.match(self.fill_data_regex, self.dst_path): - self.dst_path = self.dst_path.format(**self.fill_data) - - @property - def fill_data(self): - return self._fill_data - - def value_width(self): - width = 0 - for item in self.items.values(): - width += item.width() - return width - - def value_height(self): - height = 0 - for item in self.items.values(): - height += item.height() - return height - - def width(self): - return self._width - - def height(self): - return self._height - - def draw(self, path=None): - dir_path = os.path.dirname(self.dst_path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width(), self.height()), color=bg_color) - drawer = ImageDraw.Draw(image) - for item in self.items.values(): - item.draw(image, drawer) - - image.save(self.dst_path) - self.reset() - - def collect_data(self): - output = {} - output["width"] = self.width() - output["height"] = self.height() - output["slate_path"] = self.dst_path - - placeholders = self.find_item(obj_type="placeholder") - placeholders_data = [] - for placeholder in placeholders: - placeholders_data.append(placeholder.collect_data()) - - output["placeholders"] = placeholders_data - - return output - - -class Layer(BaseObj): - obj_type = "layer" - available_parents = ["main_frame", "layer"] - - # Direction can be 0=vertical/ 1=horizontal - def __init__(self, direction=0, *args, **kwargs): - super(Layer, self).__init__(*args, **kwargs) - self._direction = direction - - @property - def item_pos_x(self): - if self.parent.obj_type == self.obj_type: - pos_x = self.parent.child_pos_x(self.id) - elif self.parent.obj_type == "main_frame": - pos_x = self._pos_x - else: - pos_x = self.parent.value_pos_x - return int(pos_x) - - @property - def item_pos_y(self): - if self.parent.obj_type == self.obj_type: - pos_y = self.parent.child_pos_y(self.id) - elif self.parent.obj_type == "main_frame": - pos_y = self._pos_y - else: - pos_y = self.parent.value_pos_y - - return int(pos_y) - - @property - def direction(self): - if self._direction not in (0, 1): - print( - "Direction must be 0 or 1 (0 is horizontal / 1 is vertical)!" - ) - return 0 - return self._direction - - def child_pos_x(self, item_id): - pos_x = self.value_pos_x - alignment_hor = self.style["alignment-horizontal"].lower() - - item = None - for id, _item in self.items.items(): - if item_id == id: - item = _item - break - - if self.direction == 1: - for id, _item in self.items.items(): - if item_id == id: - break - - pos_x += _item.width() - if _item.obj_type not in ["image", "placeholder"]: - pos_x += 1 - - else: - if alignment_hor in ["center", "centre"]: - pos_x += (self.content_width() - item.content_width()) / 2 - - elif alignment_hor == "right": - pos_x += self.content_width() - item.content_width() - - else: - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - pos_x += margin_left - - return int(pos_x) - - def child_pos_y(self, item_id): - pos_y = self.value_pos_y - alignment_ver = self.style["alignment-horizontal"].lower() - - item = None - for id, _item in self.items.items(): - if item_id == id: - item = _item - break - - if self.direction != 1: - for id, item in self.items.items(): - if item_id == id: - break - pos_y += item.height() - if item.obj_type not in ["image", "placeholder"]: - pos_y += 1 - - else: - if alignment_ver in ["center", "centre"]: - pos_y += (self.content_height() - item.content_height()) / 2 - - elif alignment_ver == "bottom": - pos_y += self.content_height() - item.content_height() - - return int(pos_y) - - def value_height(self): - height = 0 - for item in self.items.values(): - if self.direction == 1: - if height > item.height(): - continue - # times 1 because won't get object pointer but number - height = item.height() - else: - height += item.height() - - # TODO this is not right - min_height = self.style.get("min-height") - if min_height and min_height > height: - return min_height - return height - - def value_width(self): - width = 0 - for item in self.items.values(): - if self.direction == 0: - if width > item.width(): - continue - # times 1 because won't get object pointer but number - width = item.width() - else: - if self.name == "LeftSide": - print(item, item.width()) - width += item.width() - - min_width = self.style.get("min-width") - if min_width and min_width > width: - return min_width - return width - - def draw(self, image, drawer): - for item in self.items.values(): - item.draw(image, drawer) - - -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): - super(ItemImage, self).__init__(*args, **kwargs) - self.image_path = image_path - - 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): - super(ItemText, self).__init__(*args, **kwargs) - self.value = value - - 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 - print(( - "Can't draw text because is too long with" - " `word-wrap` and `ellide` turned off" - )) - 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: - if ellide: - 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 - print("Font size is too big.") - 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 - ) - - -class FontFactory: - fonts = None - default = None - - @classmethod - def get_font(cls, family, font_size=None, italic=False, bold=False): - if cls.fonts is None: - cls.load_fonts() - - styles = [] - if bold: - styles.append("Bold") - - if italic: - styles.append("Italic") - - if not styles: - styles.append("Regular") - - style = " ".join(styles) - family = family.lower() - family_styles = cls.fonts.get(family) - if not family_styles: - return cls.default - - font = family_styles.get(style) - if font: - if font_size: - font = font.font_variant(size=font_size) - return font - - # Return first found - for font in family_styles: - if font_size: - font = font.font_variant(size=font_size) - return font - - return cls.default - - @classmethod - def load_fonts(cls): - - cls.default = ImageFont.load_default() - - available_font_ext = [".ttf", ".ttc"] - dirs = [] - if sys.platform == "win32": - # check the windows font repository - # NOTE: must use uppercase WINDIR, to work around bugs in - # 1.5.2's os.environ.get() - windir = os.environ.get("WINDIR") - if windir: - dirs.append(os.path.join(windir, "fonts")) - - elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS", "") - if not lindirs: - # According to the freedesktop spec, XDG_DATA_DIRS should - # default to /usr/share - lindirs = "/usr/share" - dirs += [ - os.path.join(lindir, "fonts") for lindir in lindirs.split(":") - ] - - elif sys.platform == "darwin": - dirs += [ - "/Library/Fonts", - "/System/Library/Fonts", - os.path.expanduser("~/Library/Fonts") - ] - - available_fonts = collections.defaultdict(dict) - for directory in dirs: - for walkroot, walkdir, walkfilenames in os.walk(directory): - for walkfilename in walkfilenames: - ext = os.path.splitext(walkfilename)[1] - if ext.lower() not in available_font_ext: - continue - - fontpath = os.path.join(walkroot, walkfilename) - font_obj = ImageFont.truetype(fontpath) - family = font_obj.font.family.lower() - style = font_obj.font.style - available_fonts[family][style] = font_obj - - cls.fonts = available_fonts - -def main(): - cur_folder = os.path.dirname(os.path.abspath(__file__)) - # input_json = os.path.join(cur_folder, "netflix_v01.json") - # input_json = os.path.join(cur_folder, "netflix_v02.json") - input_json = os.path.join(cur_folder, "netflix_v03.json") - with open(input_json) as json_file: - slate_data = json.load(json_file) - - fill_data = { - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", - "project": { - "name": "Project name" - }, - "intent": "WIP", - "version_name": "mei_101_001_0020_slate_NFX_v001", - "date": "2019-08-09", - "shot_type": "2d comp", - "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", - "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "vendor": "DAZZLE", - "shot_name": "SLATE_SIMPLE", - "frame_start": 1001, - "frame_end": 1004, - "duration": 3 - } - width = slate_data["width"] - height = slate_data["height"] - dst_path = slate_data["destination_path"] - style = slate_data.get("style") or {} - - main = MainFrame(width, height, dst_path, fill_data, style=style) - - load_queue = Queue() - for item in slate_data["items"]: - load_queue.put((item, main)) - - while not load_queue.empty(): - item_data, parent = load_queue.get() - - item_type = item_data["type"].lower() - item_style = item_data.get("style", {}) - item_name = item_data.get("name") - - pos_x = None - pos_y = None - if parent.obj_type == "main_frame": - pos_x = item_data.get("pos_x", {}) - pos_y = item_data.get("pos_y", {}) - - kwargs = { - "parent": parent, - "style": item_style, - "name": item_name, - "pos_x": pos_x, - "pos_y": pos_y - } - - if item_type == "layer": - direction = item_data.get("direction", 0) - item_obj = Layer(direction, **kwargs) - for item in item_data.get("items", []): - load_queue.put((item, item_obj)) - - elif item_type == "table": - use_alternate_color = item_data.get("use_alternate_color", False) - values = item_data.get("values") or [] - ItemTable(values, use_alternate_color, **kwargs) - - elif item_type == "image": - path = item_data["path"] - ItemImage(path, **kwargs) - - elif item_type == "rectangle": - ItemRectangle(**kwargs) - - elif item_type == "placeholder": - path = item_data["path"] - ItemPlaceHolder(path, **kwargs) - - else: - # TODO logging - print( - "Slate item not implemented <{}> - skipping".format(item_type) - ) - - main.draw() - print(main.collect_data()) - print("*** Finished") - - -if __name__ == "__main__": - main() diff --git a/pype/scripts/slate/default_style.json b/pype/scripts/slate/default_style.json deleted file mode 100644 index c0f1006a4a..0000000000 --- a/pype/scripts/slate/default_style.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#777777", - "bg-alter-color": "#666666", - "__alignment-vertical__values": ["left", "center", "right"], - "alignment-vertical": "left", - "__alignment-horizontal__values": ["top", "center", "bottom"], - "alignment-horizontal": "top", - "__padding__variants": [ - "padding-top", "padding-right", "padding-bottom", "padding-left" - ], - "padding": 0, - "__margin__variants": [ - "margin-top", "margin-right", "margin-bottom", "margin-left" - ], - "margin": 0, - }, - "layer": { - - }, - "image": { - - }, - "text": { - - }, - "table": { - - }, - "table-item": { - - }, - "table-item-col-0": { - - }, - "#MyName": { - - } -} diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slate/lib.py new file mode 100644 index 0000000000..ca3c0f2e41 --- /dev/null +++ b/pype/scripts/slate/lib.py @@ -0,0 +1,114 @@ +import os +import json +from queue import Queue + +# --- Lines for debug purpose --------------------------------- +import sys +sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") +# ------------------------------------------------------------- + +from slate_base.main_frame import MainFrame +from slate_base.layer import Layer +from slate_base.items import ( + ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder +) + + +def main(fill_data): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + input_json = os.path.join(cur_folder, "netflix_v03.json") + with open(input_json) as json_file: + slate_data = json.load(json_file) + + + width = slate_data["width"] + height = slate_data["height"] + dst_path = slate_data["destination_path"] + style = slate_data.get("style") or {} + + main = MainFrame(width, height, dst_path, fill_data, style=style) + + load_queue = Queue() + for item in slate_data["items"]: + load_queue.put((item, main)) + + while not load_queue.empty(): + item_data, parent = load_queue.get() + + item_type = item_data["type"].lower() + item_style = item_data.get("style", {}) + item_name = item_data.get("name") + + pos_x = item_data.get("pos_x") + pos_y = item_data.get("pos_y") + if parent.obj_type != "main_frame": + if pos_x or pos_y: + # TODO logging + self.log.warning(( + "You have specified `pos_x` and `pos_y` but won't be used." + " Possible only if parent of an item is `main_frame`." + )) + pos_x = None + pos_y = None + + kwargs = { + "parent": parent, + "style": item_style, + "name": item_name, + "pos_x": pos_x, + "pos_y": pos_y + } + + if item_type == "layer": + direction = item_data.get("direction", 0) + item_obj = Layer(direction, **kwargs) + for item in item_data.get("items", []): + load_queue.put((item, item_obj)) + + elif item_type == "table": + use_alternate_color = item_data.get("use_alternate_color", False) + values = item_data.get("values") or [] + ItemTable(values, use_alternate_color, **kwargs) + + elif item_type == "image": + path = item_data["path"] + ItemImage(path, **kwargs) + + elif item_type == "rectangle": + ItemRectangle(**kwargs) + + elif item_type == "placeholder": + path = item_data["path"] + ItemPlaceHolder(path, **kwargs) + + else: + # TODO logging + self.log.warning( + "Slate item not implemented <{}> - skipping".format(item_type) + ) + + main.draw() + print("Slate creation finished") + + +if __name__ == "__main__": + fill_data = { + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", + "project": { + "name": "Project name" + }, + "intent": "WIP", + "version_name": "mei_101_001_0020_slate_NFX_v001", + "date": "2019-08-09", + "shot_type": "2d comp", + "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", + "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "vendor": "DAZZLE", + "shot_name": "SLATE_SIMPLE", + "frame_start": 1001, + "frame_end": 1004, + "duration": 3 + } + main(fill_data) + # raise NotImplementedError("Slates don't have Implemented args running") diff --git a/pype/scripts/slate/slate.py b/pype/scripts/slate/slate.py deleted file mode 100644 index 4e66f68d21..0000000000 --- a/pype/scripts/slate/slate.py +++ /dev/null @@ -1,355 +0,0 @@ -import textwrap -from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor - - -class TableDraw: - def __init__( - self, image_width, image_height, - rel_pos_x, rel_pos_y, rel_width, - col_fonts=None, col_font_colors=None, - default_font=None, default_font_color=None, - col_alignments=None, rel_col_widths=None, bg_color=None, - alter_bg_color=None, pad=20, - pad_top=None, pad_bottom=None, pad_left=None, pad_right=None - ): - self.image_width = image_width - - pos_x = image_width * rel_pos_x - pos_y = image_height * rel_pos_y - width = image_width * rel_width - - self.pos_x_start = pos_x - self.pos_y_start = pos_y - self.pos_x_end = pos_x + width - self.width = width - - self.rel_col_widths = list(rel_col_widths) - self._col_widths = None - self._col_alignments = col_alignments - - if bg_color and isinstance(bg_color, str): - bg_color = ImageColor.getrgb(bg_color) - - if alter_bg_color and isinstance(alter_bg_color, str): - alter_bg_color = ImageColor.getrgb(alter_bg_color) - - self._bg_color = bg_color - self._alter_bg_color = alter_bg_color - - self.alter_use = False - - if col_fonts: - _col_fonts = [] - for col_font in col_fonts: - font_name, font_size = col_font - font = ImageFont.truetype(font_name, font_size) - _col_fonts.append(font) - col_fonts = _col_fonts - - self._col_fonts = col_fonts - - self._col_font_colors = col_font_colors - - if not default_font: - default_font = ImageFont.truetype("times", 26) - self.default_font = default_font - - if not default_font_color: - default_font_color = "#ffffff" - self.default_font_color = default_font_color - - self.texts = [] - - if pad is None: - pad = 5 - - _pad_top = pad - if pad_top is not None: - _pad_top = pad_top - - _pad_bottom = pad - if pad_bottom is not None: - _pad_bottom = pad_bottom - - _pad_left = pad - if pad_left is not None: - _pad_left = pad_left - - _pad_right = pad - if pad_right is not None: - _pad_right = pad_right - - self.pad_top = _pad_top - self.pad_bottom = _pad_bottom - self.pad_left = _pad_left - self.pad_right = _pad_right - - @property - def col_widths(self): - if self._col_widths is None: - sum_width = 0 - for w in self.rel_col_widths: - sum_width += w - - one_piece = self.width / sum_width - self._col_widths = [] - for w in self.rel_col_widths: - self._col_widths.append(one_piece * w) - - return self._col_widths - - @property - def col_fonts(self): - if self._col_fonts is None: - self._col_fonts = [] - for _ in range(len(self.col_widths)): - self._col_fonts.append(self.default_font) - - elif len(self._col_fonts) < len(self.col_widths): - if isinstance(self._col_fonts, tuple): - self._col_fonts = list(self._col_fonts) - - while len(self._col_fonts) < len(self.col_widths): - self._col_fonts.append(self.default_font) - - return self._col_fonts - - @property - def col_font_colors(self): - if self._col_font_colors is None: - self._col_font_colors = [] - for _ in range(len(self.col_widths)): - self._col_font_colors.append(self.default_font_color) - - elif len(self._col_font_colors) < len(self.col_widths): - if isinstance(self._col_font_colors, tuple): - self._col_font_colors = list(self._col_font_colors) - - while len(self._col_font_colors) < len(self.col_widths): - self._col_font_colors.append(self.default_font_color) - - return self._col_font_colors - - @property - def col_alignments(self): - if self._col_alignments is None: - self._col_alignments = [] - for _ in range(len(self.col_widths)): - self._col_alignments.append("left") - - elif len(self._col_alignments) < len(self.col_widths): - if isinstance(self._col_alignments, tuple): - self._col_alignments = list(self._col_alignments) - - while len(self._col_alignments) < len(self.col_widths): - self._col_alignments.append("left") - - return self._col_alignments - - @property - def bg_color(self): - if self.alter_use is True: - value = self.alter_bg_color - self.alter_use = False - else: - value = self._bg_color - self.alter_use = True - return value - - @property - def alter_bg_color(self): - if self._alter_bg_color: - return self._alter_bg_color - return self.bg_color - - def add_texts(self, texts): - if isinstance(texts, str): - texts = [texts] - - for text in texts: - if isinstance(text, str): - text = [text] - - if len(text) > len(self.rel_col_widths): - for _ in (len(text) - len(self.rel_col_widths)): - self.rel_col_widths.append(1) - for _t in self.texts: - _t.append("") - - self.texts.append(text) - - def draw(self, drawer): - y_pos = self.pos_y_start - for texts in self.texts: - max_height = None - cols_data = [] - for _idx, col in enumerate(texts): - width = self.col_widths[_idx] - font = self.col_fonts[_idx] - lines, line_height = self.lines_height_by_width( - drawer, col, width - self.pad_left - self.pad_right, font - ) - row_height = line_height * len(lines) - if max_height is None or row_height > max_height: - max_height = row_height - - cols_data.append({ - "lines": lines, - "line_height": line_height - }) - - drawer.rectangle( - ( - (self.pos_x_start, y_pos), - ( - self.pos_x_end, - y_pos + max_height + self.pad_top + self.pad_bottom - ) - ), - fill=self.bg_color - ) - - pos_x_start = self.pos_x_start + self.pad_left - for col, col_data in enumerate(cols_data): - lines = col_data["lines"] - line_height = col_data["line_height"] - alignment = self.col_alignments[col] - x_offset = self.col_widths[col] - font = self.col_fonts[col] - font_color = self.col_font_colors[col] - for idx, line_data in enumerate(lines): - line = line_data["text"] - line_width = line_data["width"] - if alignment == "left": - x_start = pos_x_start + self.pad_left - elif alignment == "right": - x_start = ( - pos_x_start + x_offset - line_width - - self.pad_right - self.pad_left - ) - else: - # TODO else - x_start = pos_x_start + self.pad_left - - drawer.text( - ( - x_start, - y_pos + (idx * line_height) + self.pad_top - ), - line, - font=font, - fill=font_color - ) - pos_x_start += x_offset - - y_pos += max_height + self.pad_top + self.pad_bottom - - def lines_height_by_width(self, drawer, text, width, font): - lines = [] - lines.append([part for part in text.split() if part]) - - line = 0 - while True: - thistext = lines[line] - line = line + 1 - if not thistext: - break - newline = [] - - while True: - _textwidth = drawer.textsize(" ".join(thistext), font)[0] - if ( - _textwidth <= width - ): - break - elif _textwidth > width and len(thistext) == 1: - # TODO raise error? - break - - val = thistext.pop(-1) - - if not val: - break - newline.insert(0, val) - - if len(newline) > 0: - lines.append(newline) - else: - break - - _lines = [] - height = None - for line_items in lines: - line = " ".join(line_items) - (width, _height) = drawer.textsize(line, font) - if height is None or height < _height: - height = _height - - _lines.append({ - "width": width, - "text": line - }) - - return (_lines, height) - - -width = 1920 -height = 1080 -# width = 800 -# height = 600 -bg_color_hex = "#242424" -bg_color = ImageColor.getrgb(bg_color_hex) - -base = Image.new('RGB', (width, height), color=bg_color) - -texts = [ - ("Version name:", "mei_101_001_0020_slate_NFX_v001"), - ("Date:", "2019-08-09"), - ("Shot Types:", "2d comp"), - # ("Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.") -] -text_widths_rel = (2, 8) -col_alignments = ("right", "left") -fonts = (("arial", 20), ("times", 26)) -font_colors = ("#999999", "#ffffff") - -table_color_hex = "#212121" -table_alter_color_hex = "#272727" - -drawer = ImageDraw.Draw(base) -table_d = TableDraw( - width, height, - 0.1, 0.1, 0.5, - col_fonts=fonts, - col_font_colors=font_colors, - rel_col_widths=text_widths_rel, - col_alignments=col_alignments, - bg_color=table_color_hex, - alter_bg_color=table_alter_color_hex, - pad_top=20, pad_bottom=20, pad_left=5, pad_right=5 -) - -table_d.add_texts(texts) -table_d.draw(drawer) - -image_path = r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\image.jpg" -image = Image.open(image_path) -img_width, img_height = image.size - -rel_image_width = 0.3 -rel_image_pos_x = 0.65 -rel_image_pos_y = 0.1 -image_pos_x = int(width * rel_image_pos_x) -image_pos_y = int(width * rel_image_pos_y) - -new_width = int(width * rel_image_width) -new_height = int(new_width * img_height / img_width) -image = image.resize((new_width, new_height), Image.ANTIALIAS) - -# mask = Image.new("L", image.size, 255) - -base.paste(image, (image_pos_x, image_pos_y)) -base.save(r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\test{}x{}.jpg".format( - width, height -)) -base.show() diff --git a/pype/scripts/slate/slate_base/__init__.py b/pype/scripts/slate/slate_base/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/scripts/slate/slate_base/base.py b/pype/scripts/slate/slate_base/base.py new file mode 100644 index 0000000000..35a3b6af6d --- /dev/null +++ b/pype/scripts/slate/slate_base/base.py @@ -0,0 +1,373 @@ +import os +import re +import logging +import copy +import json +from uuid import uuid4 + + +def load_default_style(): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + default_json_path = os.path.join(cur_folder, "default_style.json") + with open(default_json_path, "r") as _file: + data = _file.read() + return json.loads(data) + + +class BaseObj: + """Base Object for slates.""" + + obj_type = None + available_parents = [] + all_style_keys = [ + "font-family", "font-size", "font-color", "font-bold", "font-italic", + "bg-color", "bg-alter-color", + "alignment-horizontal", "alignment-vertical", + "padding", "padding-left", "padding-right", + "padding-top", "padding-bottom", + "margin", "margin-left", "margin-right", + "margin-top", "margin-bottom", "width", "height", + "fill", "word-wrap", "ellide", "max-lines" + ] + fill_data_regex = r"{[^}]+}" + + def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): + if not self.obj_type: + raise NotImplementedError( + "Class don't have set object type <{}>".format( + self.__class__.__name__ + ) + ) + + parent_obj_type = None + if parent: + parent_obj_type = parent.obj_type + + if parent_obj_type not in self.available_parents: + expected_parents = ", ".join(self.available_parents) + raise Exception(( + "Invalid parent <{}> for <{}>. Expected <{}>" + ).format( + parent.__class__.__name__, self.obj_type, expected_parents + )) + + self.parent = parent + self._style = style + + self.id = uuid4() + self.name = name + self.items = {} + + self._pos_x = pos_x or 0 + self._pos_y = pos_y or 0 + + log_parts = [] + module = self.__class__.__module__ + if module and module != "__main__": + log_parts.append(module) + log_parts.append(self.__class__.__name__) + self.log = logging.getLogger(".".join(log_parts)) + + if parent: + parent.add_item(self) + + def fill_data_format(self): + return + + @property + def fill_data(self): + return self.parent.fill_data + + @property + def main_style(self): + return load_default_style() + + def height(self): + raise NotImplementedError( + "Attribute `height` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + def width(self): + raise NotImplementedError( + "Attribute `width` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + def collect_data(self): + return None + + def find_item(self, obj_type=None, name=None): + obj_type_fits = False + name_fits = False + if obj_type is None or self.obj_type == obj_type: + obj_type_fits = True + + if name is None or self.name != name: + name_fits = True + + output = [] + if obj_type_fits and name_fits: + output.append(self) + + if not self.items: + return output + + for item in self.items.values(): + output.extend( + item.find_item(obj_type=obj_type, name=name) + ) + return output + + @property + def full_style(self): + if self.parent is not None: + style = dict(val for val in self.parent.full_style.items()) + else: + style = self.main_style + + for key, value in self._style.items(): + if key in self.all_style_keys: + # TODO which variant is right? + style[self.obj_type][key] = value + # style["*"][key] = value + else: + if key not in style: + style[key] = {} + + if isinstance(style[key], dict): + style[key].update(value) + else: + style[key] = value + + return style + + def get_style_for_obj_type(self, obj_type, style=None): + if not style: + style = copy.deepcopy(self.full_style) + + base = style.get("*") or {} + obj_specific = style.get(obj_type) or {} + name_specific = {} + if self.name: + name = str(self.name) + if not name.startswith("#"): + name = "#" + name + name_specific = style.get(name) or {} + + if obj_type == "table-item": + col_regex = r"table-item-col\[([\d\-, ]+)*\]" + row_regex = r"table-item-row\[([\d\-, ]+)*\]" + field_regex = ( + r"table-item-field\[(([ ]+)?\d+([ ]+)?:([ ]+)?\d+([ ]+)?)*\]" + ) + # STRICT field regex (not allowed spaces) + # fild_regex = r"table-item-field\[(\d+:\d+)*\]" + + def get_indexes_from_regex_match(result, field=False): + group = result.group(1) + indexes = [] + if field: + return [ + int(part.strip()) for part in group.strip().split(":") + ] + + parts = group.strip().split(",") + for part in parts: + part = part.strip() + if "-" not in part: + indexes.append(int(part)) + continue + + sub_parts = [ + int(sub.strip()) for sub in part.split("-") + ] + if len(sub_parts) != 2: + # TODO logging + self.log.warning("Invalid range '{}'".format(part)) + continue + + for idx in range(sub_parts[0], sub_parts[1]+1): + indexes.append(idx) + return indexes + + for key, value in style.items(): + if not key.startswith(obj_type): + continue + + result = re.search(col_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.col_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(row_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.row_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(field_regex, key) + if result: + col_idx, row_idx = get_indexes_from_regex_match( + result, True + ) + if self.col_idx == col_idx and self.row_idx == row_idx: + obj_specific.update(value) + + output = {} + output.update(base) + output.update(obj_specific) + output.update(name_specific) + + return output + + @property + def style(self): + return self.get_style_for_obj_type(self.obj_type) + + @property + def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return int(self._pos_x) + return 0 + + @property + def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return int(self._pos_y) + return 0 + + @property + def content_pos_x(self): + pos_x = self.item_pos_x + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + pos_x += margin_left + + return pos_x + + @property + def content_pos_y(self): + pos_y = self.item_pos_y + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + return pos_y + margin_top + + @property + def value_pos_x(self): + pos_x = int(self.content_pos_x) + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + pos_x += padding_left + + return pos_x + + @property + def value_pos_y(self): + pos_y = int(self.content_pos_y) + padding = self.style["padding"] + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + pos_y += padding_top + + return pos_y + + @property + def value_pos_start(self): + return (self.value_pos_x, self.value_pos_y) + + @property + def value_pos_end(self): + pos_x, pos_y = self.value_pos_start + pos_x += self.width() + pos_y += self.height() + return (pos_x, pos_y) + + @property + def content_pos_start(self): + return (self.content_pos_x, self.content_pos_y) + + @property + def content_pos_end(self): + pos_x, pos_y = self.content_pos_start + pos_x += self.content_width() + pos_y += self.content_height() + return (pos_x, pos_y) + + def value_width(self): + raise NotImplementedError( + "Attribute is not implemented <{}>".format( + self.__class__.__name__ + ) + ) + + def value_height(self): + raise NotImplementedError( + "Attribute is not implemented for <{}>".format( + self.__class__.__name__ + ) + ) + + def content_width(self): + width = self.value_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 + + return width + padding_left + padding_right + + def content_height(self): + height = self.value_height() + padding = self.style["padding"] + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + padding_bottom = self.style.get("padding-bottom") + if padding_bottom is None: + padding_bottom = padding + + return height + padding_top + padding_bottom + + def width(self): + width = self.content_width() + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + return width + margin_left + margin_right + + def height(self): + height = self.content_height() + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + return height + margin_bottom + margin_top + + def add_item(self, item): + self.items[item.id] = item + item.fill_data_format() + + + def reset(self): + for item in self.items.values(): + item.reset() diff --git a/pype/scripts/slate/slate_base/default_style.json b/pype/scripts/slate/slate_base/default_style.json new file mode 100644 index 0000000000..d0748846a5 --- /dev/null +++ b/pype/scripts/slate/slate_base/default_style.json @@ -0,0 +1,58 @@ +{ + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": true, + "ellide": true, + "max-lines": null + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "fill": true + }, + "image": { + "padding": 0, + "margin": 0, + "fill": true + }, + "placeholder": { + "padding": 0, + "margin": 0, + "fill": true + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 0, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 + } +} diff --git a/pype/scripts/slate/slate_base/font_factory.py b/pype/scripts/slate/slate_base/font_factory.py new file mode 100644 index 0000000000..77df9a40a7 --- /dev/null +++ b/pype/scripts/slate/slate_base/font_factory.py @@ -0,0 +1,93 @@ +import os +import sys +import collections + +from PIL import ImageFont + + +class FontFactory: + fonts = None + default = None + + @classmethod + def get_font(cls, family, font_size=None, italic=False, bold=False): + if cls.fonts is None: + cls.load_fonts() + + styles = [] + if bold: + styles.append("Bold") + + if italic: + styles.append("Italic") + + if not styles: + styles.append("Regular") + + style = " ".join(styles) + family = family.lower() + family_styles = cls.fonts.get(family) + if not family_styles: + return cls.default + + font = family_styles.get(style) + if font: + if font_size: + font = font.font_variant(size=font_size) + return font + + # Return first found + for font in family_styles: + if font_size: + font = font.font_variant(size=font_size) + return font + + return cls.default + + @classmethod + def load_fonts(cls): + + cls.default = ImageFont.load_default() + + available_font_ext = [".ttf", ".ttc"] + dirs = [] + if sys.platform == "win32": + # check the windows font repository + # NOTE: must use uppercase WINDIR, to work around bugs in + # 1.5.2's os.environ.get() + windir = os.environ.get("WINDIR") + if windir: + dirs.append(os.path.join(windir, "fonts")) + + elif sys.platform in ("linux", "linux2"): + lindirs = os.environ.get("XDG_DATA_DIRS", "") + if not lindirs: + # According to the freedesktop spec, XDG_DATA_DIRS should + # default to /usr/share + lindirs = "/usr/share" + dirs += [ + os.path.join(lindir, "fonts") for lindir in lindirs.split(":") + ] + + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts") + ] + + available_fonts = collections.defaultdict(dict) + for directory in dirs: + for walkroot, walkdir, walkfilenames in os.walk(directory): + for walkfilename in walkfilenames: + ext = os.path.splitext(walkfilename)[1] + if ext.lower() not in available_font_ext: + continue + + fontpath = os.path.join(walkroot, walkfilename) + font_obj = ImageFont.truetype(fontpath) + family = font_obj.font.family.lower() + style = font_obj.font.style + available_fonts[family][style] = font_obj + + cls.fonts = available_fonts diff --git a/pype/scripts/slate/slate_base/items.py b/pype/scripts/slate/slate_base/items.py new file mode 100644 index 0000000000..ea31443f80 --- /dev/null +++ b/pype/scripts/slate/slate_base/items.py @@ -0,0 +1,666 @@ +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): + super(ItemImage, self).__init__(*args, **kwargs) + self.image_path = image_path + + 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): + super(ItemText, self).__init__(*args, **kwargs) + self.value = value + + 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 + ) diff --git a/pype/scripts/slate/slate_base/layer.py b/pype/scripts/slate/slate_base/layer.py new file mode 100644 index 0000000000..ea3a3de53e --- /dev/null +++ b/pype/scripts/slate/slate_base/layer.py @@ -0,0 +1,139 @@ +from .base import BaseObj + + +class Layer(BaseObj): + obj_type = "layer" + available_parents = ["main_frame", "layer"] + + # Direction can be 0=vertical/ 1=horizontal + def __init__(self, direction=0, *args, **kwargs): + super(Layer, self).__init__(*args, **kwargs) + self._direction = direction + + @property + def item_pos_x(self): + if self.parent.obj_type == self.obj_type: + pos_x = self.parent.child_pos_x(self.id) + elif self.parent.obj_type == "main_frame": + pos_x = self._pos_x + else: + pos_x = self.parent.value_pos_x + return int(pos_x) + + @property + def item_pos_y(self): + if self.parent.obj_type == self.obj_type: + pos_y = self.parent.child_pos_y(self.id) + elif self.parent.obj_type == "main_frame": + pos_y = self._pos_y + else: + pos_y = self.parent.value_pos_y + + return int(pos_y) + + @property + def direction(self): + if self._direction not in (0, 1): + self.log.warning(( + "Direction of Layer must be 0 or 1 " + "(0 is horizontal / 1 is vertical)! Setting to 0." + )) + return 0 + return self._direction + + def child_pos_x(self, item_id): + pos_x = self.value_pos_x + alignment_hor = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break + + if self.direction == 1: + for id, _item in self.items.items(): + if item_id == id: + break + + pos_x += _item.width() + if _item.obj_type not in ["image", "placeholder"]: + pos_x += 1 + + else: + if alignment_hor in ["center", "centre"]: + pos_x += (self.content_width() - item.content_width()) / 2 + + elif alignment_hor == "right": + pos_x += self.content_width() - item.content_width() + + else: + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + pos_x += margin_left + + return int(pos_x) + + def child_pos_y(self, item_id): + pos_y = self.value_pos_y + alignment_ver = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break + + if self.direction != 1: + for id, item in self.items.items(): + if item_id == id: + break + pos_y += item.height() + if item.obj_type not in ["image", "placeholder"]: + pos_y += 1 + + else: + if alignment_ver in ["center", "centre"]: + pos_y += (self.content_height() - item.content_height()) / 2 + + elif alignment_ver == "bottom": + pos_y += self.content_height() - item.content_height() + + return int(pos_y) + + def value_height(self): + height = 0 + for item in self.items.values(): + if self.direction == 1: + if height > item.height(): + continue + # times 1 because won't get object pointer but number + height = item.height() + else: + height += item.height() + + # TODO this is not right + min_height = self.style.get("min-height") + if min_height and min_height > height: + return min_height + return height + + def value_width(self): + width = 0 + for item in self.items.values(): + if self.direction == 0: + if width > item.width(): + continue + # times 1 because won't get object pointer but number + width = item.width() + else: + width += item.width() + + min_width = self.style.get("min-width") + if min_width and min_width > width: + return min_width + return width + + def draw(self, image, drawer): + for item in self.items.values(): + item.draw(image, drawer) diff --git a/pype/scripts/slate/slate_base/main_frame.py b/pype/scripts/slate/slate_base/main_frame.py new file mode 100644 index 0000000000..837e752aae --- /dev/null +++ b/pype/scripts/slate/slate_base/main_frame.py @@ -0,0 +1,77 @@ +import os +import re +from PIL import Image, ImageDraw + +from .base import BaseObj + + +class MainFrame(BaseObj): + + obj_type = "main_frame" + available_parents = [None] + + def __init__( + self, width, height, destination_path, fill_data={}, *args, **kwargs + ): + kwargs["parent"] = None + super(MainFrame, self).__init__(*args, **kwargs) + self._width = width + self._height = height + self.dst_path = destination_path + self._fill_data = fill_data + self.fill_data_format() + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.dst_path): + self.dst_path = self.dst_path.format(**self.fill_data) + + @property + def fill_data(self): + return self._fill_data + + def value_width(self): + width = 0 + for item in self.items.values(): + width += item.width() + return width + + def value_height(self): + height = 0 + for item in self.items.values(): + height += item.height() + return height + + def width(self): + return self._width + + def height(self): + return self._height + + def draw(self, path=None): + dir_path = os.path.dirname(self.dst_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + bg_color = self.style["bg-color"] + image = Image.new("RGB", (self.width(), self.height()), color=bg_color) + drawer = ImageDraw.Draw(image) + for item in self.items.values(): + item.draw(image, drawer) + + image.save(self.dst_path) + self.reset() + + def collect_data(self): + output = {} + output["width"] = self.width() + output["height"] = self.height() + output["slate_path"] = self.dst_path + + placeholders = self.find_item(obj_type="placeholder") + placeholders_data = [] + for placeholder in placeholders: + placeholders_data.append(placeholder.collect_data()) + + output["placeholders"] = placeholders_data + + return output diff --git a/pype/scripts/slate/style_schema.json b/pype/scripts/slate/style_schema.json deleted file mode 100644 index dab6151df5..0000000000 --- a/pype/scripts/slate/style_schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "alignment": { - "description": "Alignment of item", - "type": "string", - "enum": ["left", "right", "center"], - "example": "left" - }, - "font-family": { - "description": "Font type", - "type": "string", - "example": "Arial" - }, - "font-size": { - "description": "Font size", - "type": "integer", - "example": 26 - }, - "font-color": { - "description": "Font color", - "type": "string", - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#ffffff" - }, - "font-bold": { - "description": "Font boldness", - "type": "boolean", - "example": True - }, - "bg-color": { - "description": "Background color, None means transparent", - "type": ["string", None], - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#333" - }, - "bg-alter-color": None, -}