From 5e3f0ab337dec07f6a76f10267241f1ba1daa40a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:11:05 +0100 Subject: [PATCH 01/86] initial commit --- pype/scripts/slate/base.py | 799 +++++++++++++++++++++++++++++++++++++ 1 file changed, 799 insertions(+) create mode 100644 pype/scripts/slate/base.py diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py new file mode 100644 index 0000000000..cd51c94aab --- /dev/null +++ b/pype/scripts/slate/base.py @@ -0,0 +1,799 @@ +import sys +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 = [] + + def __init__(self, parent, style={}, name=None): + if not self.obj_type: + raise NotImplemented( + "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.parent = parent + self.name = name + self._style = style + self.items = {} + self.final_style = None + + @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": None, + "bg-alter-color": None, + "alignment-vertical": "left", #left, center, right + "alignment-horizontal": "top", #top, center, bottom + "padding": 0, + # "padding-left": 0, + # "padding-right": 0, + # "padding-top": 0, + # "padding-bottom": 0 + "margin": 0, + # "margin-left": 0, + # "margin-right": 0, + # "margin-top": 0, + # "margin-bottom": 0 + }, + "layer": {}, + "image": {}, + "text": {}, + "table": {}, + "table-item": {}, + "table-item-col-0": {}, + "#MyName": {} + } + return default_style_v1 + + @property + def is_drawing(self): + return self.parent.is_drawing + + @property + def height(self): + raise NotImplementedError( + "Attribute `height` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + @property + def width(self): + raise NotImplementedError( + "Attribute `width` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + @property + def full_style(self): + if self.is_drawing and self.final_style is not None: + return self.final_style + + 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 style: + style[key] = value + style.update() + + if self.is_drawing: + self.final_style = style + + return style + + @property + def style(self): + style = self.full_style + style.update(self._style) + + base = style.get("*", style.get("global")) or {} + obj_specific = style.get(self.obj_type) or {} + name_specific = {} + if self.name: + name = str(self.name) + if not name.startswith("#"): + name += "#" + name_specific = style.get(name) or {} + + output = {} + output.update(base) + output.update(obj_specific) + output.update(name_specific) + + return output + + @property + def pos_x(self): + return 0 + + @property + def pos_y(self): + return 0 + + @property + def pos_start(self): + return (self.pos_x, self.pos_y) + + @property + def pos_end(self): + pos_x, pos_y = self.pos_start + pos_x += self.width + pos_y += self.height + return (pos_x, pos_y) + + @property + def max_width(self): + return self.style.get("max-width") or (self.width * 1) + + @property + def max_height(self): + return self.style.get("max-height") or (self.height * 1) + + @property + def max_content_width(self): + width = self.max_width + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + return (width - (padding_left + padding_right)) + + @property + def max_content_height(self): + height = self.max_height + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + return (height - (padding_top + padding_bottom)) + + def add_item(self, item): + self.items[item.id] = item + + def reset(self): + self.final_style = None + for item in self.items.values(): + item.reset() + + +class MainFrame(BaseObj): + + obj_type = "main_frame" + available_parents = [None] + + def __init__(self, width, height, style={}, parent=None, name=None): + super(MainFrame, self).__init__(parent, style, name) + self._width = width + self._height = height + self._is_drawing = False + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def is_drawing(self): + return self._is_drawing + + def draw(self, path): + self._is_drawing = True + bg_color = self.style["bg-color"] + image = Image.new("RGB", (self.width, self.height))#, color=bg_color) + for item in self.items.values(): + item.draw(image) + + image.save(path) + self._is_drawing = False + self.reset() + + + +class Layer(BaseObj): + obj_type = "layer" + available_parents = ["main_frame", "layer"] + + # Direction can be 0=horizontal/ 1=vertical + def __init__(self, *args, **kwargs): + super(Layer, self).__init__(*args, **kwargs) + + @property + def pos_x(self): + if self.parent.obj_type == self.obj_type: + pos_x = self.parent.item_pos_x(self.id) + else: + pos_x = self.parent.pos_x + return pos_x + + @property + def pos_y(self): + if self.parent.obj_type == self.obj_type: + pos_y = self.parent.item_pos_y(self.id) + else: + pos_y = self.parent.pos_y + return pos_y + + @property + def direction(self): + direction = self.style.get("direction", 0) + if direction not in (0, 1): + raise Exception( + "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" + ) + return direction + + def item_pos_x(self, item_id): + pos_x = self.pos_x + if self.direction != 0: + for id, item in self.items.items(): + if id == item_id: + break + pos_x += item.width + if item.obj_type != "image": + pos_x += 1 + + if pos_x != self.pos_x: + pos_x += 1 + return pos_x + + def item_pos_y(self, item_id): + pos_y = self.pos_y + if self.direction != 1: + for id, item in self.items.items(): + if item_id == id: + break + pos_y += item.height + if item.obj_type != "image": + pos_y += 1 + + return pos_y + + @property + def height(self): + height = 0 + for item in self.items.values(): + if self.direction == 0: + if height > item.height: + continue + + # times 1 because won't get object pointer but number + height = item.height * 1 + else: + height += item.height + + min_height = self.style.get("min-height") + if min_height > height: + return min_height + return height + + @property + def 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 * 1 + else: + width += item.width + + min_width = self.style.get("min-width") + if min_width > width: + return min_width + return width + + def draw(self, image, drawer=None): + if drawer is None: + drawer = ImageDraw.Draw(image) + + for item in self.items.values(): + item.draw(image, drawer) + + +class BaseItem(BaseObj): + available_parents = ["layer"] + + def __init__(self, *args, **kwargs): + super(BaseItem, self).__init__(*args, **kwargs) + + @property + def pos_x(self): + return self.parent.item_pos_x(self.id) + + @property + def pos_y(self): + return self.parent.item_pos_y(self.id) + + @property + def content_pos_x(self): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + return self.pos_x + margin_left + padding_left + + @property + def content_pos_y(self): + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + + return self.pos_y + margin_top + padding_top + + @property + def content_width(self): + raise NotImplementedError( + "Attribute is not implemented" + ) + + @property + def content_height(self): + raise NotImplementedError( + "Attribute is not implemented" + ) + + @property + def width(self): + width = self.content_width + + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + + 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 + + padding_left + padding_right + ) + + @property + def height(self): + height = self.content_height + + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + + 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 + + padding_top + padding_bottom + ) + + 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 draw(self, image, drawer): + paste_image = Image.open(self.image_path) + paste_image = paste_image.resize( + (self.content_width, self.content_height), + Image.ANTIALIAS + ) + image.paste( + paste_image, + (self.content_pos_x, self.content_pos_y) + ) + + @property + def content_width(self): + return self.style.get("width") + + @property + def content_height(self): + return self.style.get("height") + + +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": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (self.pos_start, self.pos_end), + fill=bg_color, + outline=None + ) + + text_pos_start = (self.content_pos_x, self.content_pos_y) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + drawer.text( + text_pos_start, + self.value, + font=font, + fill=font_color + ) + + @property + def content_width(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + width = font.getsize(self.value)[0] + return width + + @property + def content_height(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + height = font.getsize(self.value)[1] + return height + + +class ItemTable(BaseItem): + + obj_type = "table" + + def __init__(self, values, *args, **kwargs): + super(ItemTable, self).__init__(*args, **kwargs) + self.size_values = None + self.values_by_cords = None + + self.prepare_values(values) + self.calculate_sizes() + + def prepare_values(self, _values): + values = [] + values_by_cords = [] + for row_idx, row in enumerate(_values): + if len(values_by_cords) < row_idx + 1: + for i in range(len(values_by_cords), row_idx + 1): + values_by_cords.append([]) + + for col_idx, col in enumerate(_values[row_idx]): + if len(values_by_cords[row_idx]) < col_idx + 1: + for i in range(len(values_by_cords[row_idx]), col_idx + 1): + values_by_cords[row_idx].append("") + + if not col: + col = "" + col_item = TableField(row_idx, col_idx, col, 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": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (self.pos_start, self.pos_end), + fill=bg_color, + outline=None + ) + + for value in self.values: + value.draw(image, drawer) + + @property + def content_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 + + @property + def content_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 pos_info_by_cord(self, cord_x, cord_y): + row_heights, col_widths = self.size_values + pos_x = self.pos_x + pos_y = self.pos_y + width = 0 + height = 0 + for idx, value in enumerate(col_widths): + if cord_y == idx: + width = value + break + pos_x += value + + for idx, value in enumerate(row_heights): + if cord_x == idx: + height = value + break + pos_y += value + + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_top = self.style.get("padding-top") or padding + pos_x += padding_left + pos_y += padding_top + return (pos_x, pos_y, width, height) + + def filled_style(self, cord_x, cord_y): + # TODO CHANGE STYLE BY CORDS + return self.style + + +class TableField(BaseItem): + + obj_type = "table-item" + available_parents = ["table"] + + def __init__(self, cord_x, cord_y, value, *args, **kwargs): + super(TableField, self).__init__(*args, **kwargs) + self.cord_x = cord_x + self.cord_y = cord_y + self.value = value + + @property + def content_width(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + width = font.getsize(self.value)[0] + 1 + return width + + @property + def content_height(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + height = font.getsize(self.value)[1] + 1 + return height + + def content_pos_x(self, pos_x, width): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + return pos_x + padding_left + + def content_pos_y(self, pos_y, height): + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + return pos_y + padding_top + + def draw(self, image, drawer): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + pos_start = (pos_x, pos_y) + pos_end = (pos_x + width, pos_y + height) + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (pos_start, pos_end), + fill=bg_color, + outline=None + ) + + text_pos_start = ( + self.content_pos_x(pos_x, width), + self.content_pos_y(pos_y, height) + ) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + drawer.text( + text_pos_start, + self.value, + font=font, + fill=font_color + ) + +if __name__ == "__main__": + main_style = { + "bg-color": "#777777" + } + text_1_style = { + "padding": 10, + "bg-color": "#00ff77" + } + text_2_style = { + "padding": 8, + "bg-color": "#ff0066" + } + text_3_style = { + "padding": 0, + "bg-color": "#ff5500" + } + main = MainFrame(1920, 1080, style=main_style) + layer = Layer(parent=main) + main.add_item(layer) + + text_1 = ItemText("Testing message 1", layer, text_1_style) + text_2 = ItemText("Testing message 2", layer, text_2_style) + text_3 = ItemText("Testing message 3", layer, text_3_style) + table_1_items = [["0", "Output text 1"], ["1", "Output text 2"], ["2", "Output text 3"]] + table_1_style = { + "padding": 8, + "bg-color": "#0077ff" + } + table_1 = ItemTable(table_1_items, layer, table_1_style) + + image_1_style = { + "width": 240, + "height": 120, + "bg-color": "#7733aa" + } + image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" + image_1 = ItemImage(image_1_path, layer, image_1_style) + + layer.add_item(text_1) + layer.add_item(text_2) + layer.add_item(table_1) + layer.add_item(image_1) + layer.add_item(text_3) + dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output2.png" + main.draw(dst) + + print("*** Drawing done :)") + + +style_schema = { + "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, +} From 89a0dd05fd325053df72163ed4727acfd16b8d77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:11:50 +0100 Subject: [PATCH 02/86] added set.json need rework --- pype/scripts/slate/set.json | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 pype/scripts/slate/set.json diff --git a/pype/scripts/slate/set.json b/pype/scripts/slate/set.json new file mode 100644 index 0000000000..e8bba8ac41 --- /dev/null +++ b/pype/scripts/slate/set.json @@ -0,0 +1,59 @@ +{ + "width": 1920, + "height": 1020, + "bg-color": "#000000", + "__bg-color__": "For setting constant color of background. May cause issues if not set when there are not painted spaces.", + "bg-image": null, + "__bg-image__": "May be path to static image???", + "defaults": { + "font-family": "Arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "bg-color": null, + "bg-alter-color": null, + "alignment": "left", + "__alignment[enum]__": ["left", "right", "center"] + }, + "items": [{ + "rel_pos_x": 0.1, + "rel_pos_y": 0.1, + "rel_width": 0.5, + "type": "table", + "col-format": [ + { + "font-size": 12, + "font-color": "#777777", + "alignment": "right" + }, + { + "alignment": "left" + } + ], + "rows": [ + [ + { + "name": "Version", + }, + { + "value": "mei_101_001_0020_slate_NFX_v001", + "font-bold": true + } + ], [ + { + "value": "Date:" + }, + { + "value": "{date}" + } + ] + ] + }, { + "rel_pos_x": 0.1, + "rel_pos_y": 0.1, + "rel_width": 0.5, + "rel_height": 0.5, + "type": "image", + } + ] +} From 18db21c82837ea80f1f6e93f1290443b8489784a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:12:03 +0100 Subject: [PATCH 03/86] first successfull image inspiration --- pype/scripts/slate/slate.py | 355 ++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 pype/scripts/slate/slate.py diff --git a/pype/scripts/slate/slate.py b/pype/scripts/slate/slate.py new file mode 100644 index 0000000000..4e66f68d21 --- /dev/null +++ b/pype/scripts/slate/slate.py @@ -0,0 +1,355 @@ +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() From 449fb26aa796681ed77ffb62260ff0dcebc666b4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 12 Jan 2020 23:14:40 +0100 Subject: [PATCH 04/86] added margins and modified style --- pype/scripts/slate/base.py | 117 +++++++++----------------- pype/scripts/slate/default_style.json | 44 ++++++++++ pype/scripts/slate/style_schema.json | 44 ++++++++++ 3 files changed, 130 insertions(+), 75 deletions(-) create mode 100644 pype/scripts/slate/default_style.json create mode 100644 pype/scripts/slate/style_schema.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index cd51c94aab..5ae7109ed4 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -3,6 +3,7 @@ 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.""" @@ -11,7 +12,7 @@ class BaseObj: def __init__(self, parent, style={}, name=None): if not self.obj_type: - raise NotImplemented( + raise NotImplementedError( "Class don't have set object type <{}>".format( self.__class__.__name__ ) @@ -50,18 +51,10 @@ class BaseObj: "font-italic": False, "bg-color": None, "bg-alter-color": None, - "alignment-vertical": "left", #left, center, right - "alignment-horizontal": "top", #top, center, bottom + "alignment-vertical": "left", + "alignment-horizontal": "top", "padding": 0, - # "padding-left": 0, - # "padding-right": 0, - # "padding-top": 0, - # "padding-bottom": 0 "margin": 0, - # "margin-left": 0, - # "margin-right": 0, - # "margin-top": 0, - # "margin-bottom": 0 }, "layer": {}, "image": {}, @@ -105,8 +98,9 @@ class BaseObj: for key, value in self._style.items(): if key in style: - style[key] = value - style.update() + style[key].update(value) + else: + style[self.obj_type][key] = value if self.is_drawing: self.final_style = style @@ -134,6 +128,7 @@ class BaseObj: return output + @property def pos_x(self): return 0 @@ -191,8 +186,8 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, style={}, parent=None, name=None): - super(MainFrame, self).__init__(parent, style, name) + def __init__(self, width, height, *args, **kwargs): + super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height self._is_drawing = False @@ -212,7 +207,7 @@ class MainFrame(BaseObj): def draw(self, path): self._is_drawing = True bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width, self.height))#, color=bg_color) + image = Image.new("RGB", (self.width, self.height), color=bg_color) for item in self.items.values(): item.draw(image) @@ -221,14 +216,14 @@ class MainFrame(BaseObj): self.reset() - class Layer(BaseObj): obj_type = "layer" available_parents = ["main_frame", "layer"] # Direction can be 0=horizontal/ 1=vertical - def __init__(self, *args, **kwargs): + def __init__(self, direction=0, *args, **kwargs): super(Layer, self).__init__(*args, **kwargs) + self._direction = direction @property def pos_x(self): @@ -248,15 +243,20 @@ class Layer(BaseObj): @property def direction(self): - direction = self.style.get("direction", 0) - if direction not in (0, 1): - raise Exception( + if self._direction not in (0, 1): + print( "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" ) - return direction + return 0 + return self._direction def item_pos_x(self, item_id): pos_x = self.pos_x + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + pos_x += margin_left if self.direction != 0: for id, item in self.items.items(): if id == item_id: @@ -265,12 +265,15 @@ class Layer(BaseObj): if item.obj_type != "image": pos_x += 1 - if pos_x != self.pos_x: - pos_x += 1 return pos_x def item_pos_y(self, item_id): pos_y = self.pos_y + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + + pos_y += margin_top if self.direction != 1: for id, item in self.items.items(): if item_id == id: @@ -283,7 +286,11 @@ class Layer(BaseObj): @property def height(self): - height = 0 + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + height = (margin_top + margin_bottom) for item in self.items.values(): if self.direction == 0: if height > item.height: @@ -294,6 +301,7 @@ class Layer(BaseObj): else: height += item.height + min_height = self.style.get("min-height") if min_height > height: return min_height @@ -301,7 +309,11 @@ class Layer(BaseObj): @property def width(self): - width = 0 + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + width = (margin_left + margin_right) for item in self.items.values(): if self.direction == 0: if width > item.width: @@ -402,9 +414,9 @@ class BaseItem(BaseObj): margin_bottom = self.style.get("margin-bottom") or margin return ( - height + - margin_bottom + margin_top + - padding_top + padding_bottom + height + + margin_bottom + margin_top + + padding_top + padding_bottom ) def add_item(self, *args, **kwargs): @@ -417,6 +429,7 @@ class BaseItem(BaseObj): ) ) + class ItemImage(BaseItem): obj_type = "image" @@ -751,49 +764,3 @@ if __name__ == "__main__": main.draw(dst) print("*** Drawing done :)") - - -style_schema = { - "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, -} diff --git a/pype/scripts/slate/default_style.json b/pype/scripts/slate/default_style.json new file mode 100644 index 0000000000..c0f1006a4a --- /dev/null +++ b/pype/scripts/slate/default_style.json @@ -0,0 +1,44 @@ +{ + "*": { + "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/style_schema.json b/pype/scripts/slate/style_schema.json new file mode 100644 index 0000000000..dab6151df5 --- /dev/null +++ b/pype/scripts/slate/style_schema.json @@ -0,0 +1,44 @@ +{ + "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, +} From 43fbdbf8fe95809b46f95d361cfd817364197a64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jan 2020 12:45:46 +0100 Subject: [PATCH 05/86] added margins --- pype/scripts/slate/base.py | 395 +++++++++++++++++++------------------ 1 file changed, 205 insertions(+), 190 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 5ae7109ed4..bb5d127a2d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -9,6 +9,15 @@ class BaseObj: obj_type = None available_parents = [] + all_style_keys = [ + "font-family", "font-size", "font-color", "font-bold", "font-italic", + "bg-color", "bg-alter-color", + "alignment-vertical", "alignment-horizontal", + "padding", "padding-left", "padding-right", + "padding-top", "padding-bottom", + "margin", "margin-left", "margin-right", + "margin-top", "margin-bottom", "width", "height" + ] def __init__(self, parent, style={}, name=None): if not self.obj_type: @@ -34,9 +43,7 @@ class BaseObj: self._style = style self.id = uuid4() - self.parent = parent self.name = name - self._style = style self.items = {} self.final_style = None @@ -51,18 +58,41 @@ class BaseObj: "font-italic": False, "bg-color": None, "bg-alter-color": None, - "alignment-vertical": "left", + "alignment-vertical": "right", "alignment-horizontal": "top", "padding": 0, "margin": 0, }, - "layer": {}, - "image": {}, - "text": {}, - "table": {}, - "table-item": {}, - "table-item-col-0": {}, - "#MyName": {} + "main_frame": { + "padding": 0, + "margin": 0 + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "image": { + "padding": 0, + "margin": 0 + }, + "text": { + # "alignment-vertical": "left", + # "alignment-horizontal": "top", + "padding": 0, + "margin": 0 + }, + "table": { + "padding": 0, + "margin": 0 + }, + "table-item": { + "padding": 0, + "margin": 0 + }, + "__not_implemented__": { + "table-item-col-0": {}, + "#MyName": {} + } } return default_style_v1 @@ -97,10 +127,18 @@ class BaseObj: style = self.main_style for key, value in self._style.items(): - if key in style: - style[key].update(value) - else: + 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 if self.is_drawing: self.final_style = style @@ -110,9 +148,8 @@ class BaseObj: @property def style(self): style = self.full_style - style.update(self._style) - base = style.get("*", style.get("global")) or {} + base = style.get("*") or {} obj_specific = style.get(self.obj_type) or {} name_specific = {} if self.name: @@ -128,49 +165,121 @@ class BaseObj: return output - @property - def pos_x(self): + def item_pos_x(self): return 0 @property - def pos_y(self): + def item_pos_y(self): return 0 @property - def pos_start(self): - return (self.pos_x, self.pos_y) + def content_pos_x(self): + pos_x = self.item_pos_x + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + return pos_x + margin_left @property - def pos_end(self): - pos_x, pos_y = self.pos_start + def content_pos_y(self): + pos_y = self.item_pos_y + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + return pos_y + margin_top + + @property + def value_pos_x(self): + pos_x = self.content_pos_x * 1 + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + pos_x += padding_left + + return pos_x + + @property + def value_pos_y(self): + pos_y = self.content_pos_y * 1 + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or 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 max_width(self): - return self.style.get("max-width") or (self.width * 1) + def content_pos_start(self): + return (self.content_pos_x, self.content_pos_y) @property - def max_height(self): - return self.style.get("max-height") or (self.height * 1) + 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) @property - def max_content_width(self): - width = self.max_width + def value_width(self): + raise NotImplementedError( + "Attribute is not implemented <{}>".format( + self.__class__.__name__ + ) + ) + + @property + def value_height(self): + raise NotImplementedError( + "Attribute is not implemented for <{}>".format( + self.__class__.__name__ + ) + ) + + @property + def content_width(self): + width = self.value_width padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding padding_right = self.style.get("padding-right") or padding - return (width - (padding_left + padding_right)) + return width + padding_left + padding_right @property - def max_content_height(self): - height = self.max_height + def content_height(self): + height = self.value_height padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding padding_bottom = self.style.get("padding-bottom") or padding - return (height - (padding_top + padding_bottom)) + return height + padding_top + padding_bottom + + @property + 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 + + @property + 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 @@ -187,11 +296,26 @@ class MainFrame(BaseObj): available_parents = [None] def __init__(self, width, height, *args, **kwargs): + kwargs["parent"] = None super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height self._is_drawing = False + @property + def value_width(self): + width = 0 + for item in self.items.values(): + width += item.width + return width + + @property + def value_height(self): + height = 0 + for item in self.items.values(): + height += item.height + return height + @property def width(self): return self._width @@ -226,19 +350,19 @@ class Layer(BaseObj): self._direction = direction @property - def pos_x(self): + def item_pos_x(self): if self.parent.obj_type == self.obj_type: - pos_x = self.parent.item_pos_x(self.id) + pos_x = self.parent.child_pos_x(self.id) else: - pos_x = self.parent.pos_x + pos_x = self.parent.value_pos_x return pos_x @property - def pos_y(self): + def item_pos_y(self): if self.parent.obj_type == self.obj_type: - pos_y = self.parent.item_pos_y(self.id) + pos_y = self.parent.child_pos_y(self.id) else: - pos_y = self.parent.pos_y + pos_y = self.parent.value_pos_y return pos_y @property @@ -250,30 +374,21 @@ class Layer(BaseObj): return 0 return self._direction - def item_pos_x(self, item_id): - pos_x = self.pos_x + def child_pos_x(self, item_id): + pos_x = self.value_pos_x - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - pos_x += margin_left if self.direction != 0: for id, item in self.items.items(): - if id == item_id: + if item_id == id: break - pos_x += item.width + pos_x += item.height if item.obj_type != "image": pos_x += 1 - return pos_x - def item_pos_y(self, item_id): - pos_y = self.pos_y + def child_pos_y(self, item_id): + pos_y = self.value_pos_y - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - - pos_y += margin_top if self.direction != 1: for id, item in self.items.items(): if item_id == id: @@ -281,21 +396,15 @@ class Layer(BaseObj): pos_y += item.height if item.obj_type != "image": pos_y += 1 - return pos_y @property - def height(self): - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin - - height = (margin_top + margin_bottom) + def value_height(self): + height = 0 for item in self.items.values(): if self.direction == 0: if height > item.height: continue - # times 1 because won't get object pointer but number height = item.height * 1 else: @@ -308,24 +417,19 @@ class Layer(BaseObj): return height @property - def width(self): - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - - width = (margin_left + margin_right) + 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 * 1 else: width += item.width min_width = self.style.get("min-width") - if min_width > width: + if min_width and min_width > width: return min_width return width @@ -344,80 +448,12 @@ class BaseItem(BaseObj): super(BaseItem, self).__init__(*args, **kwargs) @property - def pos_x(self): - return self.parent.item_pos_x(self.id) + def item_pos_x(self): + return self.parent.child_pos_x(self.id) @property - def pos_y(self): - return self.parent.item_pos_y(self.id) - - @property - def content_pos_x(self): - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - return self.pos_x + margin_left + padding_left - - @property - def content_pos_y(self): - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - - return self.pos_y + margin_top + padding_top - - @property - def content_width(self): - raise NotImplementedError( - "Attribute is not implemented" - ) - - @property - def content_height(self): - raise NotImplementedError( - "Attribute is not implemented" - ) - - @property - def width(self): - width = self.content_width - - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - - 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 + - padding_left + padding_right - ) - - @property - def height(self): - height = self.content_height - - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding - - 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 - + padding_top + padding_bottom - ) + def item_pos_y(self): + 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.") @@ -440,20 +476,20 @@ class ItemImage(BaseItem): def draw(self, image, drawer): paste_image = Image.open(self.image_path) paste_image = paste_image.resize( - (self.content_width, self.content_height), + (self.value_width, self.value_height), Image.ANTIALIAS ) image.paste( paste_image, - (self.content_pos_x, self.content_pos_y) + (self.value_pos_x, self.value_pos_y) ) @property - def content_width(self): + def value_width(self): return self.style.get("width") @property - def content_height(self): + def value_height(self): return self.style.get("height") @@ -467,35 +503,27 @@ class ItemText(BaseItem): def draw(self, image, drawer): bg_color = self.style["bg-color"] if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding # TODO border outline styles drawer.rectangle( - (self.pos_start, self.pos_end), + (self.content_pos_start, self.content_pos_end), fill=bg_color, outline=None ) - text_pos_start = (self.content_pos_x, self.content_pos_y) - font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] font = ImageFont.truetype(font_family, font_size) drawer.text( - text_pos_start, + self.value_pos_start, self.value, font=font, fill=font_color ) @property - def content_width(self): + def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -504,7 +532,7 @@ class ItemText(BaseItem): return width @property - def content_height(self): + def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -571,15 +599,9 @@ class ItemTable(BaseItem): def draw(self, image, drawer): bg_color = self.style["bg-color"] if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding # TODO border outline styles drawer.rectangle( - (self.pos_start, self.pos_end), + (self.content_pos_start, self.content_pos_end), fill=bg_color, outline=None ) @@ -588,7 +610,7 @@ class ItemTable(BaseItem): value.draw(image, drawer) @property - def content_width(self): + def value_width(self): row_heights, col_widths = self.size_values width = 0 for _width in col_widths: @@ -599,7 +621,7 @@ class ItemTable(BaseItem): return width @property - def content_height(self): + def value_height(self): row_heights, col_widths = self.size_values height = 0 for _height in row_heights: @@ -611,8 +633,8 @@ class ItemTable(BaseItem): def pos_info_by_cord(self, cord_x, cord_y): row_heights, col_widths = self.size_values - pos_x = self.pos_x - pos_y = self.pos_y + pos_x = self.value_pos_x + pos_y = self.value_pos_y width = 0 height = 0 for idx, value in enumerate(col_widths): @@ -627,12 +649,6 @@ class ItemTable(BaseItem): break pos_y += value - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_top = self.style.get("padding-top") or padding - pos_x += padding_left - pos_y += padding_top return (pos_x, pos_y, width, height) def filled_style(self, cord_x, cord_y): @@ -652,7 +668,7 @@ class TableField(BaseItem): self.value = value @property - def content_width(self): + def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -661,7 +677,7 @@ class TableField(BaseItem): return width @property - def content_height(self): + def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -669,15 +685,19 @@ class TableField(BaseItem): height = font.getsize(self.value)[1] + 1 return height - def content_pos_x(self, pos_x, width): - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - return pos_x + padding_left + @property + def item_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + return pos_x - def content_pos_y(self, pos_y, height): - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - return pos_y + padding_top + @property + def item_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + return pos_y def draw(self, image, drawer): pos_x, pos_y, width, height = ( @@ -700,18 +720,13 @@ class TableField(BaseItem): outline=None ) - text_pos_start = ( - self.content_pos_x(pos_x, width), - self.content_pos_y(pos_y, height) - ) - font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] font = ImageFont.truetype(font_family, font_size) drawer.text( - text_pos_start, + self.value_pos_start, self.value, font=font, fill=font_color From 3ef67998f7b3c0c2e80453e0d3dc18bb116bd66c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jan 2020 16:34:15 +0100 Subject: [PATCH 06/86] added initial alignment --- pype/scripts/slate/base.py | 231 +++++++++++++++++++++++++++---------- 1 file changed, 173 insertions(+), 58 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index bb5d127a2d..84b8faf0da 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -12,7 +12,7 @@ class BaseObj: all_style_keys = [ "font-family", "font-size", "font-color", "font-bold", "font-italic", "bg-color", "bg-alter-color", - "alignment-vertical", "alignment-horizontal", + "alignment-horizontal", "alignment-vertical", "padding", "padding-left", "padding-right", "padding-top", "padding-bottom", "margin", "margin-left", "margin-right", @@ -56,10 +56,10 @@ class BaseObj: "font-color": "#ffffff", "font-bold": False, "font-italic": False, - "bg-color": None, - "bg-alter-color": None, - "alignment-vertical": "right", - "alignment-horizontal": "top", + "bg-color": "#0077ff", + "bg-alter-color": "#0055dd", + "alignment-horizontal": "center", + "alignment-vertical": "bottom", "padding": 0, "margin": 0, }, @@ -76,8 +76,6 @@ class BaseObj: "margin": 0 }, "text": { - # "alignment-vertical": "left", - # "alignment-horizontal": "top", "padding": 0, "margin": 0 }, @@ -86,6 +84,7 @@ class BaseObj: "margin": 0 }, "table-item": { + "alignment-horizontal": "right", "padding": 0, "margin": 0 }, @@ -178,15 +177,16 @@ class BaseObj: pos_x = self.item_pos_x margin = self.style["margin"] margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - return pos_x + margin_left + + 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 - margin_bottom = self.style.get("margin-bottom") or margin return pos_y + margin_top @property @@ -281,6 +281,30 @@ class BaseObj: return height + margin_bottom + margin_top + # @property + # def max_width(self): + # return self.style.get("max-width") or self.width + # + # @property + # def max_height(self): + # return self.style.get("max-height") or self.height + # + # @property + # def max_content_width(self): + # width = self.max_width + # padding = self.style["padding"] + # padding_left = self.style.get("padding-left") or padding + # padding_right = self.style.get("padding-right") or padding + # return (width - (padding_left + padding_right)) + # + # @property + # def max_content_height(self): + # height = self.max_height + # padding = self.style["padding"] + # padding_top = self.style.get("padding-top") or padding + # padding_bottom = self.style.get("padding-bottom") or padding + # return (height - (padding_top + padding_bottom)) + def add_item(self, item): self.items[item.id] = item @@ -344,7 +368,7 @@ class Layer(BaseObj): obj_type = "layer" available_parents = ["main_frame", "layer"] - # Direction can be 0=horizontal/ 1=vertical + # Direction can be 0=vertical/ 1=horizontal def __init__(self, direction=0, *args, **kwargs): super(Layer, self).__init__(*args, **kwargs) self._direction = direction @@ -369,25 +393,51 @@ class Layer(BaseObj): def direction(self): if self._direction not in (0, 1): print( - "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" + "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 != 0: - for id, item in self.items.items(): + for id, _item in self.items.items(): if item_id == id: break - pos_x += item.height - if item.obj_type != "image": + pos_x += _item.height + if _item.obj_type != "image": pos_x += 1 - return pos_x + + 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(): @@ -396,7 +446,19 @@ class Layer(BaseObj): pos_y += item.height if item.obj_type != "image": pos_y += 1 - return pos_y + + 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 + + else: + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + pos_y += margin_top + return int(pos_y) @property def value_height(self): @@ -545,7 +607,7 @@ class ItemTable(BaseItem): obj_type = "table" - def __init__(self, values, *args, **kwargs): + def __init__(self, values, use_alternate_color=False, *args, **kwargs): super(ItemTable, self).__init__(*args, **kwargs) self.size_values = None self.values_by_cords = None @@ -553,22 +615,28 @@ class ItemTable(BaseItem): self.prepare_values(values) self.calculate_sizes() + self.use_alternate_color = use_alternate_color + def prepare_values(self, _values): values = [] values_by_cords = [] - for row_idx, row in enumerate(_values): - if len(values_by_cords) < row_idx + 1: - for i in range(len(values_by_cords), row_idx + 1): - values_by_cords.append([]) + row_count = 0 + col_count = 0 + for row in _values: + row_count += 1 + if len(row) > col_count: + col_count = len(row) - for col_idx, col in enumerate(_values[row_idx]): - if len(values_by_cords[row_idx]) < col_idx + 1: - for i in range(len(values_by_cords[row_idx]), col_idx + 1): - values_by_cords[row_idx].append("") - - if not col: + 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, self) + + col_item = TableField(row_idx, col_idx, col, parent=self) values_by_cords[row_idx][col_idx] = col_item values.append(col_item) @@ -631,7 +699,7 @@ class ItemTable(BaseItem): height -= 1 return height - def pos_info_by_cord(self, cord_x, cord_y): + def content_pos_info_by_cord(self, cord_x, cord_y): row_heights, col_widths = self.size_values pos_x = self.value_pos_x pos_y = self.value_pos_y @@ -669,6 +737,9 @@ class TableField(BaseItem): @property def value_width(self): + if not self.value: + return 0 + font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -678,6 +749,8 @@ class TableField(BaseItem): @property def value_height(self): + if not self.value: + return 0 font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -688,31 +761,68 @@ class TableField(BaseItem): @property def item_pos_x(self): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) return pos_x @property def item_pos_y(self): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) return pos_y + @property + def value_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + ) + + 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") or 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.cord_x, self.cord_y) + ) + + 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") or padding + pos_y += padding_top + + return int(pos_y) + def draw(self, image, drawer): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) pos_start = (pos_x, pos_y) pos_end = (pos_x + width, pos_y + height) bg_color = self.style["bg-color"] - if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] + if self.parent.use_alternate_color and (self.cord_x % 2) == 1: + bg_color = self.style["bg-alter-color"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding + if bg_color and bg_color.lower() != "transparent": # TODO border outline styles drawer.rectangle( (pos_start, pos_end), @@ -734,48 +844,53 @@ class TableField(BaseItem): if __name__ == "__main__": main_style = { - "bg-color": "#777777" + "bg-color": "#777777", + "margin": 0 } text_1_style = { - "padding": 10, + "padding": 0, "bg-color": "#00ff77" } text_2_style = { - "padding": 8, + "padding": 0, "bg-color": "#ff0066" } text_3_style = { "padding": 0, "bg-color": "#ff5500" } - main = MainFrame(1920, 1080, style=main_style) - layer = Layer(parent=main) - main.add_item(layer) - - text_1 = ItemText("Testing message 1", layer, text_1_style) - text_2 = ItemText("Testing message 2", layer, text_2_style) - text_3 = ItemText("Testing message 3", layer, text_3_style) - table_1_items = [["0", "Output text 1"], ["1", "Output text 2"], ["2", "Output text 3"]] - table_1_style = { - "padding": 8, - "bg-color": "#0077ff" - } - table_1 = ItemTable(table_1_items, layer, table_1_style) - image_1_style = { "width": 240, "height": 120, "bg-color": "#7733aa" } + table_1_style = { + "padding": 0, + "bg-color": "#0077ff" + } + + main = MainFrame(1920, 1080, style=main_style) + layer = Layer(parent=main) + main.add_item(layer) + + text_1 = ItemText("Testing message 1", layer, text_1_style) + text_2 = ItemText("Testing 2", layer, text_2_style) + text_3 = ItemText("Testing message 3", layer, text_3_style) + + table_1_items = [["0", "Output text 1", "ha"], ["1", "Output 2"], ["2", "Output text 3"]] + table_1 = ItemTable(table_1_items, True, parent=layer, style=table_1_style) + image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" image_1 = ItemImage(image_1_path, layer, image_1_style) layer.add_item(text_1) layer.add_item(text_2) + layer.add_item(text_3) + layer.add_item(table_1) layer.add_item(image_1) - layer.add_item(text_3) - dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output2.png" + + dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" main.draw(dst) print("*** Drawing done :)") From 8899ce974fbfe91c69e87b5a390910a71e044a75 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:33:59 +0100 Subject: [PATCH 07/86] added important imports --- pype/scripts/slate/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 84b8faf0da..33266bf62d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -1,4 +1,9 @@ +import os import sys +import re +import copy +import json +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 From de2d156fc4955f1d4e246a5a0b74b4e5b263d58b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:34:26 +0100 Subject: [PATCH 08/86] base object may have pos_x and pos_y --- pype/scripts/slate/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 33266bf62d..1aca5062be 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -24,7 +24,7 @@ class BaseObj: "margin-top", "margin-bottom", "width", "height" ] - def __init__(self, parent, style={}, name=None): + 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( @@ -50,7 +50,9 @@ class BaseObj: self.id = uuid4() self.name = name self.items = {} - self.final_style = None + + self._pos_x = pos_x or 0 + self._pos_y = pos_y or 0 @property def main_style(self): From 549898730a60c099b52f958fed5b24d242f59284 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:35:51 +0100 Subject: [PATCH 09/86] style property use get_style_for_obj_type method --- pype/scripts/slate/base.py | 77 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1aca5062be..6430e5d8f3 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -151,12 +151,12 @@ class BaseObj: return style - @property - def style(self): - style = self.full_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(self.obj_type) or {} + obj_specific = style.get(obj_type) or {} name_specific = {} if self.name: name = str(self.name) @@ -164,6 +164,67 @@ class BaseObj: 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) + if self.row_idx == col_idx and self.row_idx == row_idx: + obj_specific.update(value) + output = {} output.update(base) output.update(obj_specific) @@ -171,12 +232,20 @@ class BaseObj: 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 self._pos_x return 0 @property def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return self._pos_x return 0 @property From 7d40f571d58455d724a03f37fa190ed9ccc80184 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:39:02 +0100 Subject: [PATCH 10/86] removed final_style, is_drawing and cleaned up little bit --- pype/scripts/slate/base.py | 54 +++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 6430e5d8f3..737d92de67 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -102,10 +102,6 @@ class BaseObj: } return default_style_v1 - @property - def is_drawing(self): - return self.parent.is_drawing - @property def height(self): raise NotImplementedError( @@ -124,9 +120,6 @@ class BaseObj: @property def full_style(self): - if self.is_drawing and self.final_style is not None: - return self.final_style - if self.parent is not None: style = dict(val for val in self.parent.full_style.items()) else: @@ -146,9 +139,6 @@ class BaseObj: else: style[key] = value - if self.is_drawing: - self.final_style = style - return style def get_style_for_obj_type(self, obj_type, style=None): @@ -385,7 +375,6 @@ class BaseObj: self.items[item.id] = item def reset(self): - self.final_style = None for item in self.items.values(): item.reset() @@ -395,12 +384,12 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, *args, **kwargs): + def __init__(self, width, height, destination_path=None, *args, **kwargs): kwargs["parent"] = None super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height - self._is_drawing = False + self.dst_path = destination_path @property def value_width(self): @@ -424,19 +413,27 @@ class MainFrame(BaseObj): def height(self): return self._height - @property - def is_drawing(self): - return self._is_drawing + def draw(self, path=None): + if not path: + path = self.dst_path + + if not path: + raise TypeError(( + "draw() missing 1 required positional argument: 'path'" + " if 'destination_path is not specified'" + )) + + dir_path = os.path.dirname(path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) - def draw(self, path): - self._is_drawing = True bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width, self.height), color=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) + item.draw(image, drawer) image.save(path) - self._is_drawing = False self.reset() @@ -571,26 +568,27 @@ class Layer(BaseObj): return min_width return width - def draw(self, image, drawer=None): - if drawer is None: - drawer = ImageDraw.Draw(image) - + def draw(self, image, drawer): for item in self.items.values(): item.draw(image, drawer) class BaseItem(BaseObj): - available_parents = ["layer"] + available_parents = ["main_frame", "layer"] def __init__(self, *args, **kwargs): super(BaseItem, self).__init__(*args, **kwargs) @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): @@ -795,10 +793,6 @@ class ItemTable(BaseItem): return (pos_x, pos_y, width, height) - def filled_style(self, cord_x, cord_y): - # TODO CHANGE STYLE BY CORDS - return self.style - class TableField(BaseItem): From 2b199538c461381d5939b8291d1fc1f1653988c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:26:47 +0100 Subject: [PATCH 11/86] return self._pos_x if parent in main_frame --- pype/scripts/slate/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 737d92de67..e985d9fd04 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -450,6 +450,8 @@ class Layer(BaseObj): 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 pos_x @@ -458,6 +460,8 @@ class Layer(BaseObj): 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 pos_y From ef609344dfddc2b0dd356362eac8c295b7c2bf5f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:30:12 +0100 Subject: [PATCH 12/86] initial netflix version json --- pype/scripts/slate/netflix_v01.1.json | 141 ++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 pype/scripts/slate/netflix_v01.1.json diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.1.json new file mode 100644 index 0000000000..a2be6d13e0 --- /dev/null +++ b/pype/scripts/slate/netflix_v01.1.json @@ -0,0 +1,141 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v001.png", + "style": { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top", + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": true + }, + "main_frame": { + "bg-color": "#252525" + }, + "table": { + "bg-color": "transparent" + }, + "table-item": { + "bg-color": "#272727", + "bg-alter-color": "#212121", + "font-color": "#ffffff", + "font-bold": true, + "font-italic": false, + "padding": 5, + "padding-bottom": 10, + "alignment-vertical": "top", + "alignment-horizontal": "left", + "word-wrap": true, + "ellide": true, + "max-lines": 3 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989" + }, + "table-item-col[1]": { + "font-size": 30, + "padding-left": 20 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "pos_x": 40, + "pos_y": 40, + "style": { + "width": 1094, + "height": 1000 + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ], + "style": { + "table-item-col[0]": { + "width": 300 + } + } + }, { + "type": "rectangle", + "style": { + "bg-color": "#d40914", + "width": 1094, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["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."] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 300 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 786 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "pos_x": 1174, + "pos_y": 40, + "style": { + "width": 733, + "height": 1000 + }, + "items": [{ + "type": "image", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "style": { + "width": 733, + "height": 414 + } + }, { + "type": "image", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "style": { + "width": 733, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "DAZZLE"], + ["Shot Name:", "SLATE_SIMPLE"], + ["Frames:", "0 - 1 (2)"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left" + }, + "table-item-col[1]": { + "alignment-horizontal": "right" + } + } + }] + }] +} From b872ee29919dd568e9ae935870e6ea6f708028b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:30:54 +0100 Subject: [PATCH 13/86] added argument when processing field style --- pype/scripts/slate/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index e985d9fd04..467b504ca1 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -211,7 +211,9 @@ class BaseObj: result = re.search(field_regex, key) if result: - col_idx, row_idx = get_indexes_from_regex_match(result) + col_idx, row_idx = get_indexes_from_regex_match( + result, True + ) if self.row_idx == col_idx and self.row_idx == row_idx: obj_specific.update(value) From 8c5fd923fe5e4581e53febb930b7d1ee422b54a8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:32:17 +0100 Subject: [PATCH 14/86] created FontFactory --- pype/scripts/slate/base.py | 201 ++++++++++++++++++++++++++++++++----- 1 file changed, 174 insertions(+), 27 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 467b504ca1..37d59f13bf 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -918,32 +918,102 @@ class TableField(BaseItem): fill=font_color ) -if __name__ == "__main__": - main_style = { - "bg-color": "#777777", - "margin": 0 - } - text_1_style = { - "padding": 0, - "bg-color": "#00ff77" - } - text_2_style = { - "padding": 0, - "bg-color": "#ff0066" - } - text_3_style = { - "padding": 0, - "bg-color": "#ff5500" - } - image_1_style = { - "width": 240, - "height": 120, - "bg-color": "#7733aa" - } - table_1_style = { - "padding": 0, - "bg-color": "#0077ff" - } + +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_v01(): + main_style = {"bg-color": "#777777", "margin": 0} + text_1_style = {"padding": 0, "bg-color": "#00ff77"} + text_2_style = {"padding": 0, "bg-color": "#ff0066"} + text_3_style = {"padding": 0, "bg-color": "#ff5500"} + image_1_style = {"width": 240, "height": 120, "bg-color": "#7733aa"} + table_1_style = {"padding": 0, "bg-color": "#0077ff"} main = MainFrame(1920, 1080, style=main_style) layer = Layer(parent=main) @@ -969,4 +1039,81 @@ if __name__ == "__main__": dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" main.draw(dst) - print("*** Drawing done :)") + +def main_v02(): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + input_json = os.path.join(cur_folder, "netflix_v01.1.json") + with open(input_json) as json_file: + slate_data = json.load(json_file) + + width = slate_data["width"] + height = slate_data["height"] + style = slate_data.get("style") or {} + dst_path = slate_data.get("destination_path") + main = MainFrame(width, height, destination_path=dst_path, style=style) + + load_queue = Queue() + for item in slate_data["items"]: + load_queue.put((item, main)) + + all_objs = [] + 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, + "pos_x": pos_x, + "pos_y": pos_y + } + + item_obj = None + 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 [] + item_obj = ItemTable(values, use_alternate_color, **kwargs) + + elif item_type == "image": + path = item_data["path"] + item_obj = ItemImage(path, **kwargs) + + elif item_type == "rectangle": + item_obj = ItemRectangle(**kwargs) + + if not item_obj: + print( + "Slate item not implemented <{}> - skipping".format(item_type) + ) + continue + + all_objs.append(item_obj) + + parent.add_item(item_obj) + + main.draw() + # for item in all_objs: + # print(item.style.get("width"), item.style.get("height")) + # print(item.width, item.height) + # print(item.content_pos_x, item.content_pos_y) + # print(item.value_pos_x, item.value_pos_y) + + +if __name__ == "__main__": + main_v02() + print("*** Drawing is done") From 7a800142077bc2c580366344e3dd739070ae2185 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:37:32 +0100 Subject: [PATCH 15/86] all heights and widths are callable and not property --- pype/scripts/slate/base.py | 82 ++++++++++++++------------------------ 1 file changed, 30 insertions(+), 52 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 37d59f13bf..750061a844 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -102,7 +102,6 @@ class BaseObj: } return default_style_v1 - @property def height(self): raise NotImplementedError( "Attribute `height` is not implemented for <{}>".format( @@ -110,7 +109,6 @@ class BaseObj: ) ) - @property def width(self): raise NotImplementedError( "Attribute `width` is not implemented for <{}>".format( @@ -282,8 +280,8 @@ class BaseObj: @property def value_pos_end(self): pos_x, pos_y = self.value_pos_start - pos_x += self.width - pos_y += self.height + pos_x += self.width() + pos_y += self.height() return (pos_x, pos_y) @property @@ -293,11 +291,10 @@ class BaseObj: @property def content_pos_end(self): pos_x, pos_y = self.content_pos_start - pos_x += self.content_width - pos_y += self.content_height + pos_x += self.content_width() + pos_y += self.content_height() return (pos_x, pos_y) - @property def value_width(self): raise NotImplementedError( "Attribute is not implemented <{}>".format( @@ -305,7 +302,6 @@ class BaseObj: ) ) - @property def value_height(self): raise NotImplementedError( "Attribute is not implemented for <{}>".format( @@ -313,25 +309,22 @@ class BaseObj: ) ) - @property def content_width(self): - width = self.value_width + width = self.value_width() padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding padding_right = self.style.get("padding-right") or padding return width + padding_left + padding_right - @property def content_height(self): - height = self.value_height + height = self.value_height() padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding padding_bottom = self.style.get("padding-bottom") or padding return height + padding_top + padding_bottom - @property def width(self): - width = self.content_width + width = self.content_width() margin = self.style["margin"] margin_left = self.style.get("margin-left") or margin @@ -339,9 +332,8 @@ class BaseObj: return width + margin_left + margin_right - @property def height(self): - height = self.content_height + height = self.content_height() margin = self.style["margin"] margin_top = self.style.get("margin-top") or margin @@ -393,25 +385,21 @@ class MainFrame(BaseObj): self._height = height self.dst_path = destination_path - @property def value_width(self): width = 0 for item in self.items.values(): - width += item.width + width += item.width() return width - @property def value_height(self): height = 0 for item in self.items.values(): - height += item.height + height += item.height() return height - @property def width(self): return self._width - @property def height(self): return self._height @@ -491,16 +479,17 @@ class Layer(BaseObj): for id, _item in self.items.items(): if item_id == id: break - pos_x += _item.height + + pos_x += _item.height() if _item.obj_type != "image": pos_x += 1 else: if alignment_hor in ["center", "centre"]: - pos_x += (self.content_width - item.content_width) / 2 + pos_x += (self.content_width() - item.content_width()) / 2 elif alignment_hor == "right": - pos_x += self.content_width - item.content_width + pos_x += self.content_width() - item.content_width() else: margin = self.style["margin"] @@ -522,16 +511,16 @@ class Layer(BaseObj): for id, item in self.items.items(): if item_id == id: break - pos_y += item.height + pos_y += item.height() if item.obj_type != "image": pos_y += 1 else: if alignment_ver in ["center", "centre"]: - pos_y += (self.content_height - item.content_height) / 2 + pos_y += (self.content_height() - item.content_height()) / 2 elif alignment_ver == "bottom": - pos_y += self.content_height - item.content_height + pos_y += self.content_height() - item.content_height() else: margin = self.style["margin"] @@ -539,35 +528,33 @@ class Layer(BaseObj): pos_y += margin_top return int(pos_y) - @property def value_height(self): height = 0 for item in self.items.values(): if self.direction == 0: - if height > item.height: + if height > item.height(): continue # times 1 because won't get object pointer but number - height = item.height * 1 + height = item.height() else: - height += item.height + height += item.height() min_height = self.style.get("min-height") - if min_height > height: + if min_height and min_height > height: return min_height return height - @property def value_width(self): width = 0 for item in self.items.values(): if self.direction == 0: - if width > item.width: + if width > item.width(): continue # times 1 because won't get object pointer but number - width = item.width * 1 + width = item.width() else: - width += item.width + width += item.width() min_width = self.style.get("min-width") if min_width and min_width > width: @@ -626,11 +613,9 @@ class ItemImage(BaseItem): (self.value_pos_x, self.value_pos_y) ) - @property def value_width(self): return self.style.get("width") - @property def value_height(self): return self.style.get("height") @@ -664,7 +649,6 @@ class ItemText(BaseItem): fill=font_color ) - @property def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -673,7 +657,6 @@ class ItemText(BaseItem): width = font.getsize(self.value)[0] return width - @property def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -733,12 +716,12 @@ class ItemTable(BaseItem): col_widths.append(0) _width = col_widths[col_idx] - item_width = col_item.width + 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 + item_height = col_item.height() if _height < item_height: row_heights[row_idx] = item_height @@ -757,7 +740,6 @@ class ItemTable(BaseItem): for value in self.values: value.draw(image, drawer) - @property def value_width(self): row_heights, col_widths = self.size_values width = 0 @@ -768,7 +750,6 @@ class ItemTable(BaseItem): width -= 1 return width - @property def value_height(self): row_heights, col_widths = self.size_values height = 0 @@ -811,7 +792,6 @@ class TableField(BaseItem): self.cord_y = cord_y self.value = value - @property def value_width(self): if not self.value: return 0 @@ -823,7 +803,6 @@ class TableField(BaseItem): width = font.getsize(self.value)[0] + 1 return width - @property def value_height(self): if not self.value: return 0 @@ -853,13 +832,12 @@ class TableField(BaseItem): pos_x, pos_y, width, height = ( self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) - alignment_hor = self.style["alignment-horizontal"].lower() if alignment_hor in ["center", "centre"]: - pos_x += (width - self.value_width) / 2 + pos_x += (width - self.value_width()) / 2 elif alignment_hor == "right": - pos_x += width - self.value_width + pos_x += width - self.value_width() else: padding = self.style["padding"] @@ -876,10 +854,10 @@ class TableField(BaseItem): alignment_ver = self.style["alignment-vertical"].lower() if alignment_ver in ["center", "centre"]: - pos_y += (height - self.value_height) / 2 + pos_y += (height - self.value_height()) / 2 elif alignment_ver == "bottom": - pos_y += height - self.value_height + pos_y += height - self.value_height() else: padding = self.style["padding"] From 25cb9b82896e343ca76793f47582fcfb7107fc54 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:38:30 +0100 Subject: [PATCH 16/86] added rectangle item --- pype/scripts/slate/base.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 750061a844..4029379655 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -618,6 +618,35 @@ class ItemImage(BaseItem): def value_height(self): return self.style.get("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 ItemText(BaseItem): From 184e1f17615fbfd3b35f3565ab3262b023cedcde Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:39:37 +0100 Subject: [PATCH 17/86] cleanup --- pype/scripts/slate/base.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 4029379655..7912b93026 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -21,7 +21,8 @@ class BaseObj: "padding", "padding-left", "padding-right", "padding-top", "padding-bottom", "margin", "margin-left", "margin-right", - "margin-top", "margin-bottom", "width", "height" + "margin-top", "margin-bottom", "width", "height", + "fill" ] def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): @@ -229,13 +230,13 @@ class BaseObj: @property def item_pos_x(self): if self.parent.obj_type == "main_frame": - return self._pos_x + return int(self._pos_x) return 0 @property def item_pos_y(self): if self.parent.obj_type == "main_frame": - return self._pos_x + return int(self._pos_y) return 0 @property @@ -257,7 +258,7 @@ class BaseObj: @property def value_pos_x(self): - pos_x = self.content_pos_x * 1 + pos_x = int(self.content_pos_x) padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding pos_x += padding_left @@ -266,7 +267,7 @@ class BaseObj: @property def value_pos_y(self): - pos_y = self.content_pos_y * 1 + pos_y = int(self.content_pos_y) padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding pos_y += padding_top @@ -444,7 +445,7 @@ class Layer(BaseObj): pos_x = self._pos_x else: pos_x = self.parent.value_pos_x - return pos_x + return int(pos_x) @property def item_pos_y(self): @@ -454,7 +455,7 @@ class Layer(BaseObj): pos_y = self._pos_y else: pos_y = self.parent.value_pos_y - return pos_y + return int(pos_y) @property def direction(self): @@ -569,8 +570,6 @@ class Layer(BaseObj): class BaseItem(BaseObj): available_parents = ["main_frame", "layer"] - def __init__(self, *args, **kwargs): - super(BaseItem, self).__init__(*args, **kwargs) @property def item_pos_x(self): @@ -603,9 +602,9 @@ class ItemImage(BaseItem): self.image_path = image_path def draw(self, image, drawer): - paste_image = Image.open(self.image_path) - paste_image = paste_image.resize( - (self.value_width, self.value_height), + 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( @@ -614,10 +613,12 @@ class ItemImage(BaseItem): ) def value_width(self): - return self.style.get("width") + return int(self.style["width"]) def value_height(self): - return self.style.get("height") + return int(self.style["height"]) + + class ItemRectangle(BaseItem): obj_type = "rectangle" @@ -684,7 +685,7 @@ class ItemText(BaseItem): font = ImageFont.truetype(font_family, font_size) width = font.getsize(self.value)[0] - return width + return int(width) def value_height(self): font_family = self.style["font-family"] @@ -692,7 +693,7 @@ class ItemText(BaseItem): font = ImageFont.truetype(font_family, font_size) height = font.getsize(self.value)[1] - return height + return int(height) class ItemTable(BaseItem): From 9dd0079b28669454a1ff446c8459d47ae4a2866d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:41:08 +0100 Subject: [PATCH 18/86] use font factory for fonts --- pype/scripts/slate/base.py | 44 +++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 7912b93026..1724984fce 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -670,8 +670,12 @@ class ItemText(BaseItem): 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 = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) drawer.text( self.value_pos_start, self.value, @@ -682,16 +686,24 @@ class ItemText(BaseItem): 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 = ImageFont.truetype(font_family, font_size) + 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 = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) height = font.getsize(self.value)[1] return int(height) @@ -828,20 +840,28 @@ class TableField(BaseItem): 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 = ImageFont.truetype(font_family, font_size) - width = font.getsize(self.value)[0] + 1 - return width + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + width = font.getsize_multiline(self.value)[0] + 1 + return int(width) def value_height(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 = ImageFont.truetype(font_family, font_size) - height = font.getsize(self.value)[1] + 1 - return height + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + height = font.getsize_multiline(self.value)[1] + 1 + return int(height) @property def item_pos_x(self): @@ -917,8 +937,12 @@ class TableField(BaseItem): 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 = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) drawer.text( self.value_pos_start, self.value, From 435713a076dd2e4d644a1b071472d6ce976df5eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 13:50:42 +0100 Subject: [PATCH 19/86] added recalculate method for word wrapping --- pype/scripts/slate/base.py | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1724984fce..c83394ce54 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -834,6 +834,108 @@ class TableField(BaseItem): self.cord_y = cord_y self.value = value + def recalculate_by_width(self, value, max_width): + if not value: + return "" + + word_wrap = self.style.get("word-wrap") + ellide = self.style.get("ellide") + max_lines = self.style.get("max-lines") + + 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 + + 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 + ) + + words = [word for word in value.split()] + words_len = len(words) + lines = [] + last_index = 0 + while True: + line = "" + for idx in range(last_index, words_len): + _word = words[idx] + _line = " ".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 == 0: + 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 + + lines.append(line) + # TODO logging + print("Font size is too big.") + break + + output = "" + if not lines: + return output + + if max_lines and len(lines) > max_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: + lines[-1] = self.ellide_text + return "\n".join([line for line in lines]) + + _line = "" + for idx, char in enumerate(last_line): + _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 + lines[-1] = line + return "\n".join([line for line in lines]) + + return "\n".join([line for line in lines]) + + def value_width(self): if not self.value: return 0 From 96d22f8a9be81ceb7bcf094b77e7f67bf568a201 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:01:20 +0100 Subject: [PATCH 20/86] added default ellide text --- pype/scripts/slate/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index c83394ce54..b6e4bd2701 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -827,6 +827,7 @@ class TableField(BaseItem): obj_type = "table-item" available_parents = ["table"] + ellide_text = "..." def __init__(self, cord_x, cord_y, value, *args, **kwargs): super(TableField, self).__init__(*args, **kwargs) From 643ad0fcde771b3d86b6077633c43e93e9277bb9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:01:56 +0100 Subject: [PATCH 21/86] added import for font factory --- pype/scripts/slate/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index b6e4bd2701..1cea093929 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -3,6 +3,7 @@ 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 a4f2464583f30a7a9cb9f1ca94e0588c84006e51 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:10 +0100 Subject: [PATCH 22/86] replaced cord_x and cord_y with col_idx and row_idx --- pype/scripts/slate/base.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1cea093929..5c81d11190 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -803,20 +803,20 @@ class ItemTable(BaseItem): height -= 1 return height - def content_pos_info_by_cord(self, cord_x, cord_y): + def content_pos_info_by_cord(self, row_idx, col_idx): row_heights, col_widths = self.size_values - pos_x = self.value_pos_x - pos_y = self.value_pos_y + 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 cord_y == idx: + if col_idx == idx: width = value break pos_x += value for idx, value in enumerate(row_heights): - if cord_x == idx: + if row_idx == idx: height = value break pos_y += value @@ -830,10 +830,11 @@ class TableField(BaseItem): available_parents = ["table"] ellide_text = "..." - def __init__(self, cord_x, cord_y, value, *args, **kwargs): + def __init__(self, row_idx, col_idx, value, *args, **kwargs): super(TableField, self).__init__(*args, **kwargs) - self.cord_x = cord_x - self.cord_y = cord_y + self.row_idx = row_idx + self.col_idx = col_idx + self.value = value def recalculate_by_width(self, value, max_width): @@ -970,21 +971,21 @@ class TableField(BaseItem): @property def item_pos_x(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + 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.cord_x, self.cord_y) + 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.cord_x, self.cord_y) + 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"]: @@ -1003,7 +1004,7 @@ class TableField(BaseItem): @property def value_pos_y(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) alignment_ver = self.style["alignment-vertical"].lower() @@ -1022,12 +1023,12 @@ class TableField(BaseItem): def draw(self, image, drawer): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + 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.cord_x % 2) == 1: + 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": From 3526c14c44af95c45912a6d0b481259a3811d679 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:27 +0100 Subject: [PATCH 23/86] added few changes for width and height --- pype/scripts/slate/base.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 5c81d11190..40e507209f 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -835,6 +835,13 @@ class TableField(BaseItem): self.row_idx = row_idx self.col_idx = col_idx + 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 recalculate_by_width(self, value, max_width): @@ -943,6 +950,10 @@ class TableField(BaseItem): if not self.value: return 0 + width = self.style.get("width") + if width: + return int(width) + font_family = self.style["font-family"] font_size = self.style["font-size"] font_bold = self.style.get("font-bold", False) @@ -952,11 +963,21 @@ class TableField(BaseItem): 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) @@ -966,6 +987,11 @@ class TableField(BaseItem): 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 From 83edea59360fca36240603c9076b529ddb22a3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:50 +0100 Subject: [PATCH 24/86] remove set.json which is deprecated --- pype/scripts/slate/set.json | 59 ------------------------------------- 1 file changed, 59 deletions(-) delete mode 100644 pype/scripts/slate/set.json diff --git a/pype/scripts/slate/set.json b/pype/scripts/slate/set.json deleted file mode 100644 index e8bba8ac41..0000000000 --- a/pype/scripts/slate/set.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "width": 1920, - "height": 1020, - "bg-color": "#000000", - "__bg-color__": "For setting constant color of background. May cause issues if not set when there are not painted spaces.", - "bg-image": null, - "__bg-image__": "May be path to static image???", - "defaults": { - "font-family": "Arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "bg-color": null, - "bg-alter-color": null, - "alignment": "left", - "__alignment[enum]__": ["left", "right", "center"] - }, - "items": [{ - "rel_pos_x": 0.1, - "rel_pos_y": 0.1, - "rel_width": 0.5, - "type": "table", - "col-format": [ - { - "font-size": 12, - "font-color": "#777777", - "alignment": "right" - }, - { - "alignment": "left" - } - ], - "rows": [ - [ - { - "name": "Version", - }, - { - "value": "mei_101_001_0020_slate_NFX_v001", - "font-bold": true - } - ], [ - { - "value": "Date:" - }, - { - "value": "{date}" - } - ] - ] - }, { - "rel_pos_x": 0.1, - "rel_pos_y": 0.1, - "rel_width": 0.5, - "rel_height": 0.5, - "type": "image", - } - ] -} From fc86e3fb19a5e0be7084e5d212aec4a9d8099f53 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:05:00 +0100 Subject: [PATCH 25/86] code cleanup --- pype/scripts/slate/base.py | 49 ++++---------------------------------- 1 file changed, 4 insertions(+), 45 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 40e507209f..6376554e2a 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -1169,41 +1169,7 @@ class FontFactory: cls.fonts = available_fonts - -def main_v01(): - main_style = {"bg-color": "#777777", "margin": 0} - text_1_style = {"padding": 0, "bg-color": "#00ff77"} - text_2_style = {"padding": 0, "bg-color": "#ff0066"} - text_3_style = {"padding": 0, "bg-color": "#ff5500"} - image_1_style = {"width": 240, "height": 120, "bg-color": "#7733aa"} - table_1_style = {"padding": 0, "bg-color": "#0077ff"} - - main = MainFrame(1920, 1080, style=main_style) - layer = Layer(parent=main) - main.add_item(layer) - - text_1 = ItemText("Testing message 1", layer, text_1_style) - text_2 = ItemText("Testing 2", layer, text_2_style) - text_3 = ItemText("Testing message 3", layer, text_3_style) - - table_1_items = [["0", "Output text 1", "ha"], ["1", "Output 2"], ["2", "Output text 3"]] - table_1 = ItemTable(table_1_items, True, parent=layer, style=table_1_style) - - image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" - image_1 = ItemImage(image_1_path, layer, image_1_style) - - layer.add_item(text_1) - layer.add_item(text_2) - layer.add_item(text_3) - - layer.add_item(table_1) - layer.add_item(image_1) - - dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" - main.draw(dst) - - -def main_v02(): +def main(): cur_folder = os.path.dirname(os.path.abspath(__file__)) input_json = os.path.join(cur_folder, "netflix_v01.1.json") with open(input_json) as json_file: @@ -1219,7 +1185,6 @@ def main_v02(): for item in slate_data["items"]: load_queue.put((item, main)) - all_objs = [] while not load_queue.empty(): item_data, parent = load_queue.get() @@ -1260,23 +1225,17 @@ def main_v02(): item_obj = ItemRectangle(**kwargs) if not item_obj: + # TODO logging print( "Slate item not implemented <{}> - skipping".format(item_type) ) continue - all_objs.append(item_obj) - parent.add_item(item_obj) main.draw() - # for item in all_objs: - # print(item.style.get("width"), item.style.get("height")) - # print(item.width, item.height) - # print(item.content_pos_x, item.content_pos_y) - # print(item.value_pos_x, item.value_pos_y) + print("*** Drawing is done") if __name__ == "__main__": - main_v02() - print("*** Drawing is done") + main() From a2470faa64627036d3b8ae9f1d0e2b9b57b958bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 15:29:36 +0100 Subject: [PATCH 26/86] col row attribute name fix --- pype/scripts/slate/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 6376554e2a..457618327c 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -214,7 +214,7 @@ class BaseObj: col_idx, row_idx = get_indexes_from_regex_match( result, True ) - if self.row_idx == col_idx and self.row_idx == row_idx: + if self.col_idx == col_idx and self.row_idx == row_idx: obj_specific.update(value) output = {} From e9d2bab7910a1cca0553e218fb036356e54fdea7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 16:33:43 +0100 Subject: [PATCH 27/86] fixes for working slate --- pype/scripts/slate/base.py | 14 ++++--- pype/scripts/slate/netflix_v01.1.json | 57 +++++++++++++++++---------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 457618327c..bd090b2556 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -477,12 +477,12 @@ class Layer(BaseObj): item = _item break - if self.direction != 0: + if self.direction == 1: for id, _item in self.items.items(): if item_id == id: break - pos_x += _item.height() + pos_x += _item.width() if _item.obj_type != "image": pos_x += 1 @@ -497,6 +497,7 @@ class Layer(BaseObj): 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): @@ -537,11 +538,11 @@ class Layer(BaseObj): if height > item.height(): continue # times 1 because won't get object pointer but number - height = item.height() - else: 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 @@ -550,7 +551,7 @@ class Layer(BaseObj): def value_width(self): width = 0 for item in self.items.values(): - if self.direction == 0: + if self.direction == 1: if width > item.width(): continue # times 1 because won't get object pointer but number @@ -1201,6 +1202,7 @@ def main(): kwargs = { "parent": parent, "style": item_style, + "name": item_name, "pos_x": pos_x, "pos_y": pos_y } diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.1.json index a2be6d13e0..91058ddb50 100644 --- a/pype/scripts/slate/netflix_v01.1.json +++ b/pype/scripts/slate/netflix_v01.1.json @@ -15,6 +15,10 @@ "padding": 0, "margin": 0 }, + "layer": { + "padding": 0, + "margin": 0 + }, "rectangle": { "padding": 0, "margin": 0, @@ -37,9 +41,9 @@ "padding-bottom": 10, "alignment-vertical": "top", "alignment-horizontal": "left", - "word-wrap": true, + "word-wrap": false, "ellide": true, - "max-lines": 3 + "max-lines": 1 }, "table-item-col[0]": { "font-size": 20, @@ -60,20 +64,30 @@ "height": 1000 }, "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], + "type": "layer", + "direction": 1, "style": { + "layer": {"padding": 0}, "table-item-col[0]": { - "width": 300 + "width": 200 } - } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ] + }, { + "type": "table", + "values": [ + ["Submitting For:", "SAMPLE"] + ] + }] }, { "type": "rectangle", "style": { "bg-color": "#d40914", - "width": 1094, + "width": 1108, "height": 5, "fill": true } @@ -87,6 +101,11 @@ ["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."] ], "style": { + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 4 + }, "table-item-col[0]": { "alignment-horizontal": "right", "width": 300 @@ -100,24 +119,20 @@ }, { "type": "layer", "name": "RightSide", - "pos_x": 1174, - "pos_y": 40, - "style": { - "width": 733, - "height": 1000 - }, + "pos_x": 1164, + "pos_y": 26, "items": [{ "type": "image", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", "style": { - "width": 733, - "height": 414 + "width": 730, + "height": 412 } }, { "type": "image", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", "style": { - "width": 733, + "width": 730, "height": 55 } }, { @@ -130,10 +145,12 @@ ], "style": { "table-item-col[0]": { - "alignment-horizontal": "left" + "alignment-horizontal": "left", + "width": 200 }, "table-item-col[1]": { - "alignment-horizontal": "right" + "alignment-horizontal": "right", + "width": 530 } } }] From db1e578cd80bdda0f89a11addf64bd748fd55057 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 18:48:38 +0100 Subject: [PATCH 28/86] fixed cases when value of tablefield does not need to ellide or word wrap --- pype/scripts/slate/base.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index bd090b2556..cb72d9af1c 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -853,6 +853,18 @@ class TableField(BaseItem): 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(( From 9cc2c4d5844e4442256bee204286cb475ce5174e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 18:52:43 +0100 Subject: [PATCH 29/86] few padding fixes --- pype/scripts/slate/base.py | 65 ++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index cb72d9af1c..a5404f318d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -261,7 +261,10 @@ class BaseObj: def value_pos_x(self): pos_x = int(self.content_pos_x) padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + pos_x += padding_left return pos_x @@ -270,7 +273,10 @@ class BaseObj: def value_pos_y(self): pos_y = int(self.content_pos_y) padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + pos_y += padding_top return pos_y @@ -314,15 +320,27 @@ class BaseObj: def content_width(self): width = self.value_width() padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or 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") or padding - padding_bottom = self.style.get("padding-bottom") or 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): @@ -343,30 +361,6 @@ class BaseObj: return height + margin_bottom + margin_top - # @property - # def max_width(self): - # return self.style.get("max-width") or self.width - # - # @property - # def max_height(self): - # return self.style.get("max-height") or self.height - # - # @property - # def max_content_width(self): - # width = self.max_width - # padding = self.style["padding"] - # padding_left = self.style.get("padding-left") or padding - # padding_right = self.style.get("padding-right") or padding - # return (width - (padding_left + padding_right)) - # - # @property - # def max_content_height(self): - # height = self.max_height - # padding = self.style["padding"] - # padding_top = self.style.get("padding-top") or padding - # padding_bottom = self.style.get("padding-bottom") or padding - # return (height - (padding_top + padding_bottom)) - def add_item(self, item): self.items[item.id] = item @@ -846,6 +840,17 @@ class TableField(BaseItem): 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 "" From ac202b9fbd3fbe1eb3cbf179a06655723f6dc0ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 14:28:20 +0100 Subject: [PATCH 30/86] implemented placeholder --- pype/scripts/slate/base.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index a5404f318d..4fc7cdb4d9 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -646,6 +646,53 @@ class ItemRectangle(BaseItem): 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" From 4a8ab6fac8dbe9137304d1be036f8e01ebf32846 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 14:29:19 +0100 Subject: [PATCH 31/86] added collect_data and fill_data abilities --- pype/scripts/slate/base.py | 382 ++++++++++++------ .../{netflix_v01.1.json => netflix_v01.json} | 87 +++- pype/scripts/slate/netflix_v02.json | 213 ++++++++++ pype/scripts/slate/netflix_v03.json | 213 ++++++++++ 4 files changed, 758 insertions(+), 137 deletions(-) rename pype/scripts/slate/{netflix_v01.1.json => netflix_v01.json} (65%) create mode 100644 pype/scripts/slate/netflix_v02.json create mode 100644 pype/scripts/slate/netflix_v03.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 4fc7cdb4d9..1f90f92992 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -25,6 +25,7 @@ class BaseObj: "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: @@ -56,6 +57,16 @@ class BaseObj: 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 = { @@ -66,40 +77,43 @@ class BaseObj: "font-bold": False, "font-italic": False, "bg-color": "#0077ff", - "bg-alter-color": "#0055dd", - "alignment-horizontal": "center", - "alignment-vertical": "bottom", - "padding": 0, - "margin": 0, - }, - "main_frame": { - "padding": 0, - "margin": 0 + "alignment-horizontal": "left", + "alignment-vertical": "top" }, "layer": { "padding": 0, "margin": 0 }, - "image": { + "rectangle": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "#E9324B", + "fill": true }, - "text": { + "main_frame": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "#252525" }, "table": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "transparent" }, "table-item": { - "alignment-horizontal": "right", - "padding": 0, - "margin": 0 - }, - "__not_implemented__": { - "table-item-col-0": {}, - "#MyName": {} + "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 @@ -118,6 +132,31 @@ class BaseObj: ) ) + 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: @@ -151,10 +190,9 @@ class BaseObj: if self.name: name = str(self.name) if not name.startswith("#"): - name += "#" + 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\-, ]+)*\]" @@ -363,6 +401,8 @@ class BaseObj: def add_item(self, item): self.items[item.id] = item + item.fill_data_format() + def reset(self): for item in self.items.values(): @@ -374,12 +414,24 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, destination_path=None, *args, **kwargs): + 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 @@ -400,16 +452,7 @@ class MainFrame(BaseObj): return self._height def draw(self, path=None): - if not path: - path = self.dst_path - - if not path: - raise TypeError(( - "draw() missing 1 required positional argument: 'path'" - " if 'destination_path is not specified'" - )) - - dir_path = os.path.dirname(path) + dir_path = os.path.dirname(self.dst_path) if not os.path.exists(dir_path): os.makedirs(dir_path) @@ -419,9 +462,24 @@ class MainFrame(BaseObj): for item in self.items.values(): item.draw(image, drawer) - image.save(path) + 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" @@ -450,6 +508,7 @@ class Layer(BaseObj): pos_y = self._pos_y else: pos_y = self.parent.value_pos_y + return int(pos_y) @property @@ -477,7 +536,7 @@ class Layer(BaseObj): break pos_x += _item.width() - if _item.obj_type != "image": + if _item.obj_type not in ["image", "placeholder"]: pos_x += 1 else: @@ -509,7 +568,7 @@ class Layer(BaseObj): if item_id == id: break pos_y += item.height() - if item.obj_type != "image": + if item.obj_type not in ["image", "placeholder"]: pos_y += 1 else: @@ -519,22 +578,18 @@ class Layer(BaseObj): elif alignment_ver == "bottom": pos_y += self.content_height() - item.content_height() - else: - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - pos_y += margin_top return int(pos_y) def value_height(self): height = 0 for item in self.items.values(): - if self.direction == 0: + 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() + else: + height += item.height() # TODO this is not right min_height = self.style.get("min-height") @@ -545,12 +600,14 @@ class Layer(BaseObj): def value_width(self): width = 0 for item in self.items.values(): - if self.direction == 1: + 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") @@ -566,7 +623,6 @@ class Layer(BaseObj): class BaseItem(BaseObj): available_parents = ["main_frame", "layer"] - @property def item_pos_x(self): if self.parent.obj_type == "main_frame": @@ -597,6 +653,10 @@ class ItemImage(BaseItem): 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( @@ -756,15 +816,25 @@ 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.values_by_cords = None - - self.prepare_values(values) 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 = [] @@ -876,14 +946,6 @@ class TableField(BaseItem): super(TableField, self).__init__(*args, **kwargs) self.row_idx = row_idx self.col_idx = col_idx - - 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 recalculate_by_width(self, value, max_width): @@ -928,24 +990,23 @@ class TableField(BaseItem): elif ellide and not word_wrap: max_lines = 1 - 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 - ) - words = [word for word in value.split()] words_len = len(words) lines = [] - last_index = 0 + 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(last_index, words_len): + for idx in range(start_index, words_len): _word = words[idx] - _line = " ".join([line, _word]) + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word]) _line_width = font.getsize(_line)[0] if _line_width > max_width: break @@ -958,7 +1019,7 @@ class TableField(BaseItem): if last_index == words_len - 1: break - elif last_index == 0: + elif last_index is None: if ellide: line = "" for idx, char in enumerate(words[idx]): @@ -968,7 +1029,7 @@ class TableField(BaseItem): if idx == 0: line = _line break - line = _line + line = line + char lines.append(line) # TODO logging @@ -979,46 +1040,103 @@ class TableField(BaseItem): if not lines: return output - if max_lines and len(lines) > max_lines: - lines = [lines[idx] for idx in range(max_lines)] - if not ellide: - return "\n".join(lines) + over_max_lines = (max_lines and len(lines) > max_lines) + if not over_max_lines: + return "\n".join([line for line in 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 + 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]) - last_line_words = last_line.split() - if len(last_line_words) == 1: - if max_lines > 1: - 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 - _line = "" - for idx, char in enumerate(last_line): - _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 - lines[-1] = line - return "\n".join([line for line in lines]) + 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 - width = self.style.get("width") - if width: - return int(width) - font_family = self.style["font-family"] font_size = self.style["font-size"] font_bold = self.style.get("font-bold", False) @@ -1087,7 +1205,10 @@ class TableField(BaseItem): else: padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + pos_x += padding_left return int(pos_x) @@ -1107,7 +1228,10 @@ class TableField(BaseItem): else: padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + pos_y += padding_top return int(pos_y) @@ -1139,11 +1263,17 @@ class TableField(BaseItem): font = FontFactory.get_font( font_family, font_size, font_italic, font_bold ) - drawer.text( + + 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 + fill=font_color, + align=alignment_hor ) @@ -1236,15 +1366,36 @@ class FontFactory: def main(): cur_folder = os.path.dirname(os.path.abspath(__file__)) - input_json = os.path.join(cur_folder, "netflix_v01.1.json") + # 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 {} - dst_path = slate_data.get("destination_path") - main = MainFrame(width, height, destination_path=dst_path, style=style) + + main = MainFrame(width, height, dst_path, fill_data, style=style) load_queue = Queue() for item in slate_data["items"]: @@ -1271,7 +1422,6 @@ def main(): "pos_y": pos_y } - item_obj = None if item_type == "layer": direction = item_data.get("direction", 0) item_obj = Layer(direction, **kwargs) @@ -1281,26 +1431,28 @@ def main(): elif item_type == "table": use_alternate_color = item_data.get("use_alternate_color", False) values = item_data.get("values") or [] - item_obj = ItemTable(values, use_alternate_color, **kwargs) + ItemTable(values, use_alternate_color, **kwargs) elif item_type == "image": path = item_data["path"] - item_obj = ItemImage(path, **kwargs) + ItemImage(path, **kwargs) elif item_type == "rectangle": - item_obj = ItemRectangle(**kwargs) + ItemRectangle(**kwargs) - if not item_obj: + elif item_type == "placeholder": + path = item_data["path"] + ItemPlaceHolder(path, **kwargs) + + else: # TODO logging print( "Slate item not implemented <{}> - skipping".format(item_type) ) - continue - - parent.add_item(item_obj) main.draw() - print("*** Drawing is done") + print(main.collect_data()) + print("*** Finished") if __name__ == "__main__": diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.json similarity index 65% rename from pype/scripts/slate/netflix_v01.1.json rename to pype/scripts/slate/netflix_v01.json index 91058ddb50..9a5974839d 100644 --- a/pype/scripts/slate/netflix_v01.1.json +++ b/pype/scripts/slate/netflix_v01.json @@ -1,7 +1,7 @@ { "width": 1920, "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v001.png", + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v01.png", "style": { "*": { "font-family": "arial", @@ -32,33 +32,40 @@ "bg-color": "transparent" }, "table-item": { - "bg-color": "#272727", - "bg-alter-color": "#212121", - "font-color": "#ffffff", - "font-bold": true, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, "font-italic": false, "padding": 5, "padding-bottom": 10, - "alignment-vertical": "top", "alignment-horizontal": "left", + "alignment-vertical": "top", "word-wrap": false, "ellide": true, "max-lines": 1 }, "table-item-col[0]": { "font-size": 20, - "font-color": "#898989" + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null }, "table-item-col[1]": { - "font-size": 30, - "padding-left": 20 + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" } }, "items": [{ "type": "layer", "name": "LeftSide", - "pos_x": 40, - "pos_y": 40, + "pos_x": 27, + "pos_y": 27, "style": { "width": 1094, "height": 1000 @@ -67,26 +74,52 @@ "type": "layer", "direction": 1, "style": { - "layer": {"padding": 0}, + "table-item": { + "bg-color": "transparent" + }, "table-item-col[0]": { - "width": 200 + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 } }, "items": [{ "type": "table", "values": [ ["Show:", "First Contact"] - ] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } }, { "type": "table", "values": [ ["Submitting For:", "SAMPLE"] - ] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } }] }, { "type": "rectangle", "style": { - "bg-color": "#d40914", + "bg-color": "#bc1015", "width": 1108, "height": 5, "fill": true @@ -101,18 +134,24 @@ ["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."] ], "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, "table-item-field[1:3]": { "word-wrap": true, "ellide": true, - "max-lines": 4 + "max-lines": 2 }, "table-item-col[0]": { "alignment-horizontal": "right", - "width": 300 + "width": 150 }, "table-item-col[1]": { "alignment-horizontal": "left", - "width": 786 + "width": 958 } } }] @@ -122,15 +161,18 @@ "pos_x": 1164, "pos_y": 26, "items": [{ - "type": "image", + "type": "placeholder", + "name": "thumbnail", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", "style": { "width": 730, "height": 412 } }, { - "type": "image", + "type": "placeholder", + "name": "colorbar", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "return_data": true, "style": { "width": 730, "height": 55 @@ -150,7 +192,8 @@ }, "table-item-col[1]": { "alignment-horizontal": "right", - "width": 530 + "width": 530, + "font-size": 30 } } }] diff --git a/pype/scripts/slate/netflix_v02.json b/pype/scripts/slate/netflix_v02.json new file mode 100644 index 0000000000..f373ed8134 --- /dev/null +++ b/pype/scripts/slate/netflix_v02.json @@ -0,0 +1,213 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v02.png", + "style": { + "*": { + "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 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "SAMPLE"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["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."] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 2 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "return_data": true, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "DAZZLE"], + ["Shot Name:", "SLATE_SIMPLE"], + ["Frames:", "0 - 1 (2)"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] +} diff --git a/pype/scripts/slate/netflix_v03.json b/pype/scripts/slate/netflix_v03.json new file mode 100644 index 0000000000..975cb60133 --- /dev/null +++ b/pype/scripts/slate/netflix_v03.json @@ -0,0 +1,213 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "{destination_path}", + "style": { + "*": { + "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 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "{project[name]}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "{intent}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Version name:", "{version_name}"], + ["Date:", "{date}"], + ["Shot Types:", "{shot_type}"], + ["Submission Note:", "{submission_note}"] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 4 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "{thumbnail_path}", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "{color_bar_path}", + "return_data": true, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "{vendor}"], + ["Shot Name:", "{shot_name}"], + ["Frames:", "{frame_start} - {frame_end} ({duration})"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] +} From 6ef5853b65cb86cbdd94ae8d713ddb1fac753bdf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 16:33:15 +0100 Subject: [PATCH 32/86] 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, -} From f54a3e981a12ff078fa70b0e6f8fa7f5e55a79a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:07:59 +0100 Subject: [PATCH 33/86] created basic slate usage --- pype/scripts/slate/lib.py | 87 ++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slate/lib.py index ca3c0f2e41..750046269e 100644 --- a/pype/scripts/slate/lib.py +++ b/pype/scripts/slate/lib.py @@ -1,26 +1,46 @@ import os import json +import logging 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 ( +from .slate_base.main_frame import MainFrame +from .slate_base.layer import Layer +from .slate_base.items import ( ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder ) +from pypeapp import config -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) +log = logging.getLogger(__name__) +RequiredSlateKeys = ["width", "height", "destination_path"] + + +def create_slates(fill_data, slate_name): + presets = config.get_presets() + slate_presets = ( + presets + .get("tools", {}) + .get("slates") + ) or {} + slate_data = slate_presets.get(slate_name) + + if not slate_data: + log.error("Slate data of <{}> does not exists.") + return False + + missing_keys = [] + for key in RequiredSlateKeys: + if key not in slate_data: + missing_keys.append("`{}`".format(key)) + + if missing_keys: + log.error("Slate data of <{}> miss required keys: {}".format( + slate_name, ", ".join(missing_keys) + )) + return False + width = slate_data["width"] height = slate_data["height"] dst_path = slate_data["destination_path"] @@ -44,7 +64,7 @@ def main(fill_data): if parent.obj_type != "main_frame": if pos_x or pos_y: # TODO logging - self.log.warning(( + 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`." )) @@ -83,32 +103,43 @@ def main(fill_data): else: # TODO logging - self.log.warning( - "Slate item not implemented <{}> - skipping".format(item_type) + log.warning( + "Not implemented object type `{}` - skipping".format(item_type) ) main.draw() - print("Slate creation finished") + log.debug("Slate creation finished") -if __name__ == "__main__": +def example(): fill_data = { - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", + "destination_path": "PATH/TO/OUTPUT/FILE", "project": { - "name": "Project name" + "name": "Testing project" }, "intent": "WIP", - "version_name": "mei_101_001_0020_slate_NFX_v001", + "version_name": "seq01_sh0100_compositing_v01", "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", + "submission_note": ( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." + " Aenean commodo ligula eget dolor. Aenean massa." + " Cum sociis natoque penatibus et magnis dis parturient montes," + " nascetur ridiculus mus. Donec quam felis, ultricies nec," + " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" + " enim. Donec pede justo, fringilla vel," + " aliquet nec, vulputate eget, arcu." + ), + "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", + "color_bar_path": "PATH/TO/COLOR/BAR/FILE", + "vendor": "Our Studio", + "shot_name": "sh0100", "frame_start": 1001, "frame_end": 1004, "duration": 3 } - main(fill_data) - # raise NotImplementedError("Slates don't have Implemented args running") + create_slates(fill_data, "example_HD") + + +if __name__ == "__main__": + raise NotImplementedError("Slates don't have Implemented args running") From 5b0e8fe8d0dac8c8e8a70e3557cc53a866932c7f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:08:17 +0100 Subject: [PATCH 34/86] remove netflix examples --- pype/scripts/slate/netflix_v01.json | 201 -------------------------- pype/scripts/slate/netflix_v02.json | 213 ---------------------------- pype/scripts/slate/netflix_v03.json | 213 ---------------------------- 3 files changed, 627 deletions(-) delete mode 100644 pype/scripts/slate/netflix_v01.json delete mode 100644 pype/scripts/slate/netflix_v02.json delete mode 100644 pype/scripts/slate/netflix_v03.json diff --git a/pype/scripts/slate/netflix_v01.json b/pype/scripts/slate/netflix_v01.json deleted file mode 100644 index 9a5974839d..0000000000 --- a/pype/scripts/slate/netflix_v01.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v01.png", - "style": { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top", - "padding": 0, - "margin": 0 - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "bg-color": "#252525" - }, - "table": { - "bg-color": "transparent" - }, - "table-item": { - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "padding": 5, - "padding-bottom": 10, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "pos_x": 27, - "pos_y": 27, - "style": { - "width": 1094, - "height": 1000 - }, - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent" - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "SAMPLE"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["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."] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 2 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "pos_x": 1164, - "pos_y": 26, - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "DAZZLE"], - ["Shot Name:", "SLATE_SIMPLE"], - ["Frames:", "0 - 1 (2)"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] -} diff --git a/pype/scripts/slate/netflix_v02.json b/pype/scripts/slate/netflix_v02.json deleted file mode 100644 index f373ed8134..0000000000 --- a/pype/scripts/slate/netflix_v02.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v02.png", - "style": { - "*": { - "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 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "direction": 1, - "name": "MainLayer", - "style": { - "#MainLayer": { - "width": 1094, - "height": 1000, - "margin": 25, - "padding": 0 - }, - "#LeftSide": { - "margin-right": 25 - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent", - "padding-bottom": 20 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "SAMPLE"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["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."] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 2 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "DAZZLE"], - ["Shot Name:", "SLATE_SIMPLE"], - ["Frames:", "0 - 1 (2)"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] - }] -} diff --git a/pype/scripts/slate/netflix_v03.json b/pype/scripts/slate/netflix_v03.json deleted file mode 100644 index 975cb60133..0000000000 --- a/pype/scripts/slate/netflix_v03.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "{destination_path}", - "style": { - "*": { - "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 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "direction": 1, - "name": "MainLayer", - "style": { - "#MainLayer": { - "width": 1094, - "height": 1000, - "margin": 25, - "padding": 0 - }, - "#LeftSide": { - "margin-right": 25 - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent", - "padding-bottom": 20 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "{project[name]}"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "{intent}"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Version name:", "{version_name}"], - ["Date:", "{date}"], - ["Shot Types:", "{shot_type}"], - ["Submission Note:", "{submission_note}"] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 4 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "{thumbnail_path}", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "{color_bar_path}", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "{vendor}"], - ["Shot Name:", "{shot_name}"], - ["Frames:", "{frame_start} - {frame_end} ({duration})"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] - }] -} From b3f4673248a8ec06dbc44d6457dbb095688aaa20 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:09:16 +0100 Subject: [PATCH 35/86] renamed slate folder to slates folder --- pype/scripts/{slate => slates}/lib.py | 0 pype/scripts/{slate => slates}/slate_base/__init__.py | 0 pype/scripts/{slate => slates}/slate_base/base.py | 0 pype/scripts/{slate => slates}/slate_base/default_style.json | 0 pype/scripts/{slate => slates}/slate_base/font_factory.py | 0 pype/scripts/{slate => slates}/slate_base/items.py | 0 pype/scripts/{slate => slates}/slate_base/layer.py | 0 pype/scripts/{slate => slates}/slate_base/main_frame.py | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename pype/scripts/{slate => slates}/lib.py (100%) rename pype/scripts/{slate => slates}/slate_base/__init__.py (100%) rename pype/scripts/{slate => slates}/slate_base/base.py (100%) rename pype/scripts/{slate => slates}/slate_base/default_style.json (100%) rename pype/scripts/{slate => slates}/slate_base/font_factory.py (100%) rename pype/scripts/{slate => slates}/slate_base/items.py (100%) rename pype/scripts/{slate => slates}/slate_base/layer.py (100%) rename pype/scripts/{slate => slates}/slate_base/main_frame.py (100%) diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slates/lib.py similarity index 100% rename from pype/scripts/slate/lib.py rename to pype/scripts/slates/lib.py diff --git a/pype/scripts/slate/slate_base/__init__.py b/pype/scripts/slates/slate_base/__init__.py similarity index 100% rename from pype/scripts/slate/slate_base/__init__.py rename to pype/scripts/slates/slate_base/__init__.py diff --git a/pype/scripts/slate/slate_base/base.py b/pype/scripts/slates/slate_base/base.py similarity index 100% rename from pype/scripts/slate/slate_base/base.py rename to pype/scripts/slates/slate_base/base.py diff --git a/pype/scripts/slate/slate_base/default_style.json b/pype/scripts/slates/slate_base/default_style.json similarity index 100% rename from pype/scripts/slate/slate_base/default_style.json rename to pype/scripts/slates/slate_base/default_style.json diff --git a/pype/scripts/slate/slate_base/font_factory.py b/pype/scripts/slates/slate_base/font_factory.py similarity index 100% rename from pype/scripts/slate/slate_base/font_factory.py rename to pype/scripts/slates/slate_base/font_factory.py diff --git a/pype/scripts/slate/slate_base/items.py b/pype/scripts/slates/slate_base/items.py similarity index 100% rename from pype/scripts/slate/slate_base/items.py rename to pype/scripts/slates/slate_base/items.py diff --git a/pype/scripts/slate/slate_base/layer.py b/pype/scripts/slates/slate_base/layer.py similarity index 100% rename from pype/scripts/slate/slate_base/layer.py rename to pype/scripts/slates/slate_base/layer.py diff --git a/pype/scripts/slate/slate_base/main_frame.py b/pype/scripts/slates/slate_base/main_frame.py similarity index 100% rename from pype/scripts/slate/slate_base/main_frame.py rename to pype/scripts/slates/slate_base/main_frame.py From 16bc4dcc6098dd1e514aac06851a83630a7fdfef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:16:02 +0100 Subject: [PATCH 36/86] exchange col_idx and row_idx for field style --- pype/scripts/slates/slate_base/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/scripts/slates/slate_base/base.py b/pype/scripts/slates/slate_base/base.py index 35a3b6af6d..35ef46769c 100644 --- a/pype/scripts/slates/slate_base/base.py +++ b/pype/scripts/slates/slate_base/base.py @@ -213,7 +213,7 @@ class BaseObj: result = re.search(field_regex, key) if result: - col_idx, row_idx = get_indexes_from_regex_match( + row_idx, col_idx = get_indexes_from_regex_match( result, True ) if self.col_idx == col_idx and self.row_idx == row_idx: From f72a19457bafe003ebc1c7a5deb12fdfbeb414bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jan 2020 10:28:54 +0100 Subject: [PATCH 37/86] added example for testing --- pype/scripts/slates/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/lib.py index 750046269e..a73f87e82f 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/lib.py @@ -1,5 +1,3 @@ -import os -import json import logging from queue import Queue @@ -112,6 +110,10 @@ def create_slates(fill_data, slate_name): def example(): + # import sys + # sys.append(r"PATH/TO/PILLOW/PACKAGE") + # sys.append(r"PATH/TO/PYPE-SETUP") + fill_data = { "destination_path": "PATH/TO/OUTPUT/FILE", "project": { @@ -142,4 +144,4 @@ def example(): if __name__ == "__main__": - raise NotImplementedError("Slates don't have Implemented args running") + example() From 09c9f66e4c79c1d7ed4b5185c912647be8e0825e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 19 Feb 2020 19:44:05 +0100 Subject: [PATCH 38/86] basic hook system --- pype/ftrack/lib/ftrack_app_handler.py | 5 +++++ pype/lib.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 9dc735987d..ea769ad167 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -273,6 +273,11 @@ class AppAction(BaseHandler): # Full path to executable launcher execfile = None + if application.get("launch_hook"): + hook = application.get("launch_hook") + self.log.info("launching hook: {}".format(hook)) + pypelib.execute_hook(application.get("launch_hook")) + if sys.platform == "win32": for ext in os.environ["PATHEXT"].split(os.pathsep): diff --git a/pype/lib.py b/pype/lib.py index 2235efa2f4..73bc16e97a 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -6,6 +6,8 @@ import contextlib import subprocess import inspect +import six + from avalon import io import avalon.api import avalon @@ -585,3 +587,21 @@ class CustomNone: def __repr__(self): """Representation of custom None.""" return "".format(str(self.identifier)) + + +def execute_hook(hook, **kwargs): + class_name = hook.split("/")[-1] + + abspath = os.path.join(os.getenv('PYPE_ROOT'), + 'repos', 'pype', **hook.split("/")[:-1]) + + try: + with open(abspath) as f: + six.exec_(f.read()) + + except Exception as exp: + log.exception("loading hook failed: {}".format(exp), + exc_info=True) + + hook_obj = globals()[class_name]() + hook_obj.execute(**kwargs) From 69c396ec3d3c3a3f5dbbfb9678e60e876bdc5a0e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 20 Feb 2020 17:35:38 +0100 Subject: [PATCH 39/86] polishing hooks --- pype/ftrack/lib/ftrack_app_handler.py | 8 ---- pype/hooks/unreal/unreal_prelaunch.py | 8 ++++ pype/lib.py | 54 +++++++++++++++++++++----- res/app_icons/ue4.png | Bin 0 -> 46503 bytes 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 pype/hooks/unreal/unreal_prelaunch.py create mode 100644 res/app_icons/ue4.png diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index ea769ad167..825a0a1985 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -256,14 +256,6 @@ class AppAction(BaseHandler): env = acre.merge(env, current_env=dict(os.environ)) env = acre.append(dict(os.environ), env) - - # - # tools_env = acre.get_tools(tools) - # env = acre.compute(dict(tools_env)) - # env = acre.merge(env, dict(os.environ)) - # os.environ = acre.append(dict(os.environ), env) - # os.environ = acre.compute(os.environ) - # Get path to execute st_temp_path = os.environ['PYPE_CONFIG'] os_plat = platform.system().lower() diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py new file mode 100644 index 0000000000..05d95a0b2a --- /dev/null +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -0,0 +1,8 @@ +from pype.lib import PypeHook + + +class UnrealPrelaunch(PypeHook): + + def execute(**kwargs): + print("I am inside!!!") + pass diff --git a/pype/lib.py b/pype/lib.py index 73bc16e97a..d1062e468f 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1,10 +1,13 @@ import os +import sys +import types import re import logging import itertools import contextlib import subprocess import inspect +from abc import ABCMeta, abstractmethod import six @@ -123,7 +126,8 @@ def modified_environ(*remove, **update): is sure to work in all situations. :param remove: Environment variables to remove. - :param update: Dictionary of environment variables and values to add/update. + :param update: Dictionary of environment variables + and values to add/update. """ env = os.environ update = update or {} @@ -347,8 +351,8 @@ def switch_item(container, "parent": version["_id"]} ) - assert representation, ("Could not find representation in the database with" - " the name '%s'" % representation_name) + assert representation, ("Could not find representation in the database " + "with the name '%s'" % representation_name) avalon.api.switch(container, representation) @@ -481,7 +485,9 @@ def get_subsets(asset_name, """ Query subsets with filter on name. - The method will return all found subsets and its defined version and subsets. Version could be specified with number. Representation can be filtered. + The method will return all found subsets and its defined version + and subsets. Version could be specified with number. Representation + can be filtered. Arguments: asset_name (str): asset (shot) name @@ -498,8 +504,8 @@ def get_subsets(asset_name, asset_io = io.find_one({"type": "asset", "name": asset_name}) # check if anything returned - assert asset_io, "Asset not existing. \ - Check correct name: `{}`".format(asset_name) + assert asset_io, ( + "Asset not existing. Check correct name: `{}`").format(asset_name) # create subsets query filter filter_query = {"type": "subset", "parent": asset_io["_id"]} @@ -513,7 +519,9 @@ def get_subsets(asset_name, # query all assets subsets = [s for s in io.find(filter_query)] - assert subsets, "No subsets found. Check correct filter. Try this for start `r'.*'`: asset: `{}`".format(asset_name) + assert subsets, ("No subsets found. Check correct filter. " + "Try this for start `r'.*'`: " + "asset: `{}`").format(asset_name) output_dict = {} # Process subsets @@ -593,15 +601,43 @@ def execute_hook(hook, **kwargs): class_name = hook.split("/")[-1] abspath = os.path.join(os.getenv('PYPE_ROOT'), - 'repos', 'pype', **hook.split("/")[:-1]) + 'repos', 'pype', *hook.split("/")[:-1]) + + mod_name, mod_ext = os.path.splitext(os.path.basename(abspath)) + + if not mod_ext == ".py": + return + + module = types.ModuleType(mod_name) + module.__file__ = abspath + + log.info("-" * 80) + print(module) try: with open(abspath) as f: - six.exec_(f.read()) + six.exec_(f.read(), module.__dict__) + + sys.modules[abspath] = module except Exception as exp: log.exception("loading hook failed: {}".format(exp), exc_info=True) + from pprint import pprint + print("-" * 80) + pprint(dir(module)) + hook_obj = globals()[class_name]() hook_obj.execute(**kwargs) + + +@six.add_metaclass(ABCMeta) +class PypeHook: + + def __init__(self): + pass + + @abstractmethod + def execute(**kwargs): + pass diff --git a/res/app_icons/ue4.png b/res/app_icons/ue4.png new file mode 100644 index 0000000000000000000000000000000000000000..39201de6643767b8b24c821316b910e2bd41b1ba GIT binary patch literal 46503 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
>Di8y z!@gK({d>>Xt`hOa68X2X+l_C|Tj_&JGiEE8b|z`NOrV{Q?w>mS^@5XmQR3Wiom+7V zzl*d45?)f-bh5u2<^4&5ly!31iZKv$pWpM6`DDRaunOM!>;9)8xhqKaK)utMVmS`B zL4?3KuGrGw4|Q{KEG8rWfr*9CA(1~?Za>OZ#@ZriUw4yDUjq=~!cRHCluc(3x(*d_ zg#5S?k%{+85;caV4~o<7+;^BX6+)c7yc?yywcPFl}cTv{kc!?AMu0Xt^q z;|ee{1wGVQlI~p@Bplf@qF9&DMA26g!j~F`74fKfM=Ur^;5^NC?A}e^Q$FZ>fo97PWZk$2!=F`t;ELPdyL1nI+ zcZ{1%J(zawB4-7`lYf zy8OmEA+Qw(uPbFTVU{RHXz>U0@ZSR9C6h*C4(muAv9uSPq4d(q*n5@whl?P9%Sl6V zqftEMLTNaTMH{!5kqZ4m|6BMoPyCmXvosv(+jPXT8-<~U)}1R*PN(FMOvkKGlX%G9 zL1LD-TQLVC_Gz!2{OlTiQ~jHjx^`uMUtiRDEfaQw3#zJ?)2#WLIz_gWM~9+BQB~8T z0|jT9KlZj;P-Q=_wM?cTsg85f);_Lf++@NYyAYvm+04vPZs{Ejey@~&>``EQ=2%$D z`qWGUMeNq%vDZ4b#&LwOwZs#6U!Iv%D3Ie0nqeWCiu?B1aV%`u zr6PH=1A4t6I=S=La+AwVDV4xh2a{vB5{Il=oxCcZzwFFc>mLN-xnf_`Kf`aW88D}M z@DAGy7OkCSYkXjM!7_Iptg78=Mk8XkFz3LTI22>fa_nJ&>-Q8*(0%@FPTgwbqnoeqx#*Q|KNQVeAGCb# z*CI>3Xw+oJZt#R$@upB^aAAgk+qBTzu*;WWp93nKPoG>8Rp=({B0CI@YUZ@@#UNZc zTexalH-C)D;B{|#ok&jRtl^eRgDOQXY>=(L^|5Yhj#(v(Z8PP2sOdi*swk7srMk7O zH%~5&#_|!!Z`z?9X2QTw>^`9yAq0w~jc=ZlJEJJNWgukeuS=$c<5d zk-oxHI~(kdC2u}29Oak60)?6&ArHs$#TU5TaJ*pttEbam8oG7*k*VLF{m0AIS}nJz z8DM%xx)BBIYu|E#I(fD-R3~WGqKRekPhlsne&{Xj>-w@NlGtbr9g5R;lw}p`MOU{? zsFu@H!69i&b^Ec#Lv}bt2Y5Gxd%fYSgPY6utnhJ+X2v7ZVZ?hb99UHLMsMwL+$Zgs z${3UxUKiK5bS}UxnD(82P0o<2Ltg0cEp3T~vF-BwvRhwDLKa@)9QtYT?j3Zyy(ivK zKW|3`=7id(!TBFoN27K}U}lA$Ek1i6<=`6WweREa)=n>JXmT>|@h#Tum$H0AgHH(8 z_6XV}a3(5aSm((U26Dptm=W|QPU+H%-cm=Rqn0_lf!&LbirXmaY+3a~mCS-PdeXYr zdTj*l6xG_yjgyH3A>isa=MeKSM1@7sJ@vJS;=n?#SXGH)`_*>xTXFkJzVbLj{mG^G zg8EX-Nr~Sz`^XE@P`h{$ThRT8pG`rICvxR@deWe$?&lC8&&-&jE}=R1RynSid;$;( z7&1~ha8~&_ScyqFBpjO9U~c#0HZ?{rv!RD7tsPGkr0@=>L7(ey<4Mn70+Y`OCza<~ z*vQ0GeYPZG{#n3I`kCA|KH{f#0O!W4Yij5BNNA~^yBhKmuavlE3$4_|IxeFp}fu)*bVAc3<|(Y z6!Mga_Gd9SQDAtP^Ludu%>`560i4aCxU6g@#E!>h#l1iJJaf3pw&LjBL7Finy!8=5 zP+!H7TDS5HJB(61uwXR&dRK3;kbbIGX7`{prfRyM(1?b=O=qKg2l3`7?>tn*(vODs zw;WrI*$v1~8}KHMKOWY#Zr;287Uq%Yfwk!)$&w2e=1j?pWqX6}aAltblRqMbmBt*T zpp0b;+^~@(%G8NJZVkdPD+#~M11(3FJ^^B(MnF8XERFtVw7<9!ark~+?$evP+rxg+ z2B+w^M(cLX6&geOrK(Q>uP_$R!Tap;6;Udskt_Df<%rw8tT&==cGLV_RWtM?%5x)n zkRGP!Vi(t~j5v8~*gxpA4*`f6eN!E%Cs$P2`3{He$uM$Ipdh4u`q8Iq;z}p)xg6Z> ztQC<~J+$4xaG!%*5At{!>jZaPr65eZvw$TN^Hmme&`uN*jAb6$?^u@(^{RaOuy>pP zppMIN~kZX1$x4^L=!;T~aD`A5jl_@w!1+X~{DrZ~ydG*lV zzyz*Xx_ipw9Mq?8oh#CbvQkXH>CN|ja|`8`)7{*%nMYUj4($pREHQ<3j}{2v2F9>r zE1FhfsWZoA)L|7FP%r;y^4#d-Y}Q&oio{qWumn!r1aVkhX4PfsO8{9)U zZ>>PJoi>IQm`k|h=c=zcgbX|HzdfDXdUCnR=;IX@ps>CQ_kPqEJ+FTG zL6)EHUI7!xEC9tC!!~r$H3q=SLeeE+3*Q`7-&=ycQRe%^v+YrAAS*`xXMGi?gNx=0!k z!HRxmPvnNGrt1!pdk!8QGF4p#= zSVYgKVXFl{+oDY=@mb4r0M%^v-C6tOv*Skc3)*)uMlJ$`o)5OZIiZyr93EK`YHK+% z<=7$YzPG~3B7~_e8^sPg-ifGG;1m{9Uzt+gZEQB@Bukk@Ad2-<5$!VoHeWC&T>W(4XNem&U@9eIk!m0h zxx)=m`{cV-x(WNpY$J70+l(8FTCf+kU+ggb0UnhOWv?;Qo{VE@AAQG&&5eMysoZS4 zH5y*0haPFh41Upbaz&gDA2iq8xNynmaG3IecFtmEwv^E+{ArhN-6vekN~t& zVg_~GnOA^_x3%B~$>d(aw(G!d()Z{JMMxF{*g9-}10@J8dlWpLW>|mTz~ksfk-+2) zx7|YVkr&*VNVJ>gBytXrXHJn+)#RN>9dHpHX3?ITi{IxD#L}(Bp@EdkyyNT-N%1{Q8jm@W;(F($LSh(T>QSbK_=@E^DZZpfzl5 zP6@O>apV`waQ$G4?7#Y?;hPPPKPd(>FQ!g-)El4MP!2B#8Y^)iQ_f*@5vaOI0BqB)?Abr{}ZzTOMp4kwMd z><`Wky4VIBQLY?_>BmC@2;$+=ZelOgzg;xS(fG9F7V=Sjtb%|&5RKPmtPd()i4BEu zA)0M0n6f0qA$MNZLI}KjwBP$MPP-T@_PXHQqI>xcn!K{#{IjfO@!~UX#1VLhp4vzF zHfsH|kzyV%eD^Bz&1iTE#e5qR&`Z{~(U$+@1gN)Gz{PxvUhR`>1rb{PW4(bI2bIIf zi9J$7{9uCX=ywcTe#lk^Iaq>}VqW>Xnb5iE?rCmPA-noF^&DLy;-;OjxZI`Noh@hC zV^3au)8jeZN1)OYQqNBV_u4nJS$2Mkja+zdb0lkpd=@*cZ1@d1yA$lLZRbDEP-(l! zXbb!1Xu2d4VKluhv|Xr+6WU%7gt*s4RX!l{SHh%apY>p6?lAp@ID*m$2`wBNzTZBC zrtihP^7KTSBP zbH$%Xew)5-$P}i|OxMlOwYNu!I6aZ2B6KuC&x_^vb zKNAyWg31(#S}JSePdHZ{bh_CjV_!Obhte1CBh1jk`U%ktXULem<95!>v! z;r(fnn><(Y*cF(qUrviF2xi_Bb-hzPT#@*Yqtk5s5L_Uv%lGxl5!2yt{R<*6D<`$^ zB05mTp!|N0`d}EcoZx&1l}g$EEN*Mxev{%}2{6+{T!|aVSyV6bXRdK^hOTq2dlpZx21KlV)ck71Bi-qUlVVP{GMXe&s82oOlt7OivZ1;x zM!-+TH0$vXW)omt&rMsty{NJ|*hfgA`-`ytkwc%zc)HSNK{fthOpzTh{m5Nvc}*+n z$G*ig0w^qJvAyu4QcIMq^-lOSfA5xQ*0{UQ!%oZHdFQCq=m&FgW)(U_J|UQu_LX@uI{eq*}ol^AvveK)|_=f}P7Z)I$b+W$fT4&A%z$G9=m zI)tf14p}WIrhoSx5t8dh{GH>>(Ki^B>+4Ecd9#MyVG@34Xdk9_o=73RA;kxN=78$B zRUfheonX?g1qu`+Oo!PK?^B^l<;^g?_3@YfP8+J-QeZh&Q^%(_LMAnLRNtdTGLwgi zU~B_Gb@6K}dGUFFbqoB%u8HY72byW$_NXG`aHHGFNf7=crf7^o(FcOvgt&q%$8i#n^-7_#F(GGW&G zej2y%q_H|h>1=QNk8(r-#YGk)jU8+5A8J+cGvgOTWy(aJ5bHYW{po%+=_WwPHep*v zsrmQlDWjC4Qb6B#&Q$L9>gH|JG<5@Qu-@3*JX&p>xrn_Hd*L*myD?~ulmsu?~$n{lK1_fp#aD5>K20QWKBPQX|9~VW3FUHzZZey|D_;jUj5i2 zsykKiVJK|gK2TTyAqC0r>EMX904s3T%nfZ}+~I4q{^?6t3sv{fP_^gA!f>74y7$Rt?*nr+GxV}s?b%q3mUR>IW>tx$ z90r;@#6y}wZ!CTvEL3%mZ@m0EK7DI3Pr>$Vi1Jgkpd%6nkr41$3wR)D+>YyFS|geb zi?C3hFh13gapTCvLSUP4+v@x%FPyyOB)`Xe|>8f#liKrBp2qIN6YqacOT zugKYYg(yiIsor1t5K0=K-d>otOg+UsMGx3BRUbAtShL*~Orw8bL@hjVkjBu(i}iIk zw%2ClTF^o0GS8vT5(QH!sg2{ zSuKTu;Thox>hFfqs@r8X^<1&<0&muY?=!+0WJi3?*{a(G5G+QV{3>r$b7OvnK{a=w zpI3_6&_zKW@ru!6HToE8c?(K@ouBPf|M`XK{(dUw9|j1=zRmtZPV%p9R;3e&7~VZBrA_L%mJa`*TnbP?U|gx;ca5HKFv-k?(&h2E>$Wl(COu>18HxuWThMJF{k7%;XSNbY!&@g)cZQ^1r> zF1`A*WXioUs*4pOPlaUOd#issLj2|Bk*DtJ-|O%o(Ojhc<1F(bq}O-}+`isVNNS!z@x8%_2uU^R5C(#gx*S zhDxQEMKLH6V?2^{Ugf3|*^e=$N6#yDsfxskn%yPhbXL8^mt~eBf5~8_aibI$A#&k? zO)D%Ssd}U<0@oV7bs?#&!5PH=8Bqr#_N;6wkm^L4ciCelsdb0vLOzEiZv}#6ZnK#< zJ{1Vy;4A`N+sH_PT|ePJi9bj6N!&K~-cC5hG&H64UbH`w6mDeiZl@oSRGp;z6@vh0O$0lT^YJ= zPG8TI5{b1%Y4OcX{5ZpIp!Om>pH7HbJ+lF&HX`~*`K57sKkHKH8_D{r=T$F?Sw1e@Y6ooQYS80t^BYL}H+cHpQ^3nN+Or%I{uP!XNHq?1uFCgPZH*!JvW%KBR8)^g zh!n}K+g&%P6(hdgv-CFwA39V4(&~ym%afhWe%b8oJX_4jS+^}F*|m#^<6axSO@S3r z5;_^ydRW~lqZGc4eM*<|sIDeo5HU}6Aa zrZ=}>x*bo%y8iTjw`TOTwrMi`I_ff>Ure>*E=DJMb8(<=|2tZWI{gcWfyAVx&IdGG zno8^ec69*3O>BdzxS$g)EfI}7kt^5!WEFW5V74Oz`BhfgC_kfYkGLK0Ref=7J|Tq$ zaPyY4TT`1IUG&>}&uv<`b|axeCMGDAFhlj9q5M9aTd})8ck}g5_WGxO(i0wO++YsM ziGuP+?E1VovqmoFn+(Q-A%g?x)RqYQ}KBCPaOdlIjx zjwRC#vyp$i^4jU{ljU(o{l?lY#`aFuwKgK&fZgQWGYdO28FHXF;hO!{@~@(w6*I-r zwNW=mD(=($e=*~OS<1KcQ9f}bBSYmeBc&d!sk0`)K>TtuI;9smPea}E`SdTH4B5Kz z(3ftgF;hMAoF7YX>*ti07dUS&=hT_p)%3=Lj9g_*gA4@ZJuY0MWI0m zWQ5QgM|}HY$-iA@rf`mz?b7A*$Ev>$QBuMgCvMU{o(mY?P;I|SH=o%CPD2h$FQjfv zMLX-=I?LO7+|LB(Zp!rQVLEfKnZm|SvO~*02V`CweftKG_GmNZ^Z8<3$NdTd4!|NC zqfr(JoaE|oAEG;rQ#iNxSyNy+PxEOp8%gF29C?)PmiH5TX{_zz0{+KqOa84Xu7gqf zphTu)n{nn#Rns*3qz56n5s>EGhkjIQb^z8}$r|cmH$aIZ4y@nqd293)5YRU!`4^_p zzBA`skA3=(pzEtnetdw}BgNfWKu6u4weTBf!Q4A(`N-W;c(xm@(~sueni{`$#|TtH zvgm8)7tQC_Idk@`%E2z{koqg=LETcUODW*M?_?2v%gkr!(^rg!0|v89M4&=2yWI!W z%&M+rhkQA*-6f#bGOYI6vVUvJclPO>KPLHK-M4Yl_UX{eIKlQnxNc~2vM?Y}_+ulhO?UlAKcLgnqJ$!3Q zxns>STHzfuPzvQx$ll64+=!NAJ$=qFzA(LE+lw$Vy`0w)w($3l`-4g@60d1Z6I8O& zzQhjU;bw<%&&m;hHx|A!44biupJ*Ek14ufIfTJ;VR{pxWf{g^fDi*rQQ2b}os7 zP5N13KE}NCuykqYWia`5^L%y9P_p4y+-)M``mMIAGPnD#Ok<{2!tSZ5RmZvDZ=8sj zi6Wj zi@oWMjCd*ZLBtd ztoDOPZLgpH0Lvh4(6{7Z0TMxlpEgGCQ3V98G*J?aA`}jL)z`Z6J3klU6wA<-8M>;X z$z!!76LTg@AulEl@v(^(Q#(vTrMRvL;@fykThWiYMJ#!oXJJ<>PE3a|F)o08GDMJ<$nx0Xa$ z%i)~9qD)Ye**K*Gn|2u>3Ih<%u%jKs)JZqtL$~No&6_K95{_jn4ofWRQN{$FLHT9l zv2AX3>!xOV*i4JDRG&Joj2}?f=X;MJn>`SkL*furev368SB{p(Ndug@!GoJ0N~Ljh z5&`vlyq%>w&8dyk4S7@zjF8%F&pu|qE${^E+VVVs183x#L!IQN?IGgQ54O;E3Ke3l zENLOsK@zX;a#tS$k#p`%P*c4}Pn3xx0<)g8N~9=2`kY7reL$fLlG#*_$fqRe;(jg6 zBw>xCR5w>BtAWB{zdMuO5^U$Zav# zocj<3^o=VhTW{2AaL@<-(`-h0=`Y&otLMC~Rq;c+50Ti#4nfcEkK)fx<|l7=>Ec>H zhS;? z10~q5G>ym4{++qTPkA=HQ+=p-S?v$Q-=6jI9ji2gcAp}Z$<-9lRGhWv@aPd0_~moM zk|iKlX|jJ$c983jwYP^ctz~X`XR!1bRHmxrffwe=^FZ317K_5oPQ z3>F1HNLh<(a&50FLx?m#FgdX~3&0Ga=7rFhkWIiegYBg&>!l$|Ln`}5K^rh17Ibzx zs&-D$RN}W?+9gmc6XN~)fM&qsOtU*%ghqk(EY<$;JQAn;)(EI=s++9=HLr>sr{GXx zV(UW$#}EzHbg>VSv~FI0kp0~kQ%1{DxBxjN2PG2zK#MP0&GJcwlvcgN`h|8wdloeZ zWxIiMHKNO+32ejZvA!t_=?dw!d6AS6`33l8ZG^?1{CeY5*s7tJtqVWh5H%eEZjvGG|~ zyKVt3Z*zvxo)^*jLjnMGsY4d3RAHAz>Aa}zZGi%n$v1xZR47b6;Dv^1s}6hr@~_d$ zglRA}v#Ad!`i1|x5oaYdl8QFUVr5|ylg*e>tAn=TdcVYHx_94cdxR?zFp-~aw?vG; zKLg)_8&mq^2-DXI?uklc;#tRRMhII}xz$GQ*x){Wz|z{`2?iXw&_+tc+`|Ir<|Pz` zVtbn-*7glfYYTtVD)A>)qRvfalWjja2E`TE%er+=S@7HN{Fy*LC-g2)MfPd)IOq8R zZ)u#9lS@%**WWh_ZZq+*8S%)Rj&?WnvXQarFS($bSmySO^HTl(6IUZZ>D6uZb+pcE zMG6a;TE`WT$;7~=uQMsow=G8bm1ND0+)?V0_~s{Th75dQ*q##2pqTa0 z`9Qgk^YSs_9|Jk4L?%#ddM|>66DXi(Oe}>=a?0O&wnIb8^ClnBewOTM5B;1SFiO0E zSM@`7mI~s8SLH18QW%DOLq9ZYWNd0$1L=Z?c6Ghxg((ot;jtC)^=k%ui$OUnuuX&V zE*5mpn5w{ZaVKe{koHKg>@~jDs$NSuoPgA(_vlOikfJM{D+eU0{A>`EaX;>7s2y_#L~p{&APX=<+wj2}GZ$)K)mA@h1MzR- z9NRZ;x%*a%>}n5TAg{?Arz%}{HQKdst4?#|HLY_-HCrn_dD%xmZ}xFYB-eYDcB0On zxP^|my5B4^L;k~{)4;AhkG`a(nI&L3iSG10#+$26V>lKj%q6JRkShtr8}dOQNM@ble%(#zM^r!~1DpsTA{JH^hsW{IHEXAB_lGFj;?Nxe%V7JERn z6ut7AvsF?rDnisPrapW!O)l!@0d<>xG3oPmo+cp+UwU`77xXIf_z1lI!d(2>91^f4 zGM^_;vb1L%f+A)Kys;(oEZskwWqVOMZ8s@t>)MsaK6@3o{C#(Ua>y5cIr|*HO=G~X zv!<+P-yYUU3^Z=6HRhlqnRYKuC%3q@V>_1x_vyepwK`?~R=_AZ1}^M(Qb*Rqin?

r@KoO^&9yCj+{ zND@+LI5aOO@V5=rpAUtcQeaQK943t{Jp$h@bLAe6KgF^RVZ9{cB#&(8B0(jML|(va z{pi^TDbC;bp*?qGxS5cBSi?&&m5@Sfp0Nb!%&Go9geSy*p|_p*4~AMuhDlAmFN2)9 zMsy~UxD!+btv<2q+x|wVP^Ws96mTp^t|iH2CaekF^yN`CKis z>t-}75)^Kx(@V2KPBW5OQ`goq?}Lh1x$M4n+yNvL^S?kr!Gf3f!XH^gJo*&jE?qKE z|7xAKvBuPZ86@9bEmx(x;TU11!$Gg(ImSShech%abJyl!ds_J7$g$z**&n`j_9USz zoYVbXEW<8rsPMyE?LSr9)7JaeI0Ki?%$9QJ$}GeTb959d>zxNTKB|BCeU)XXfr2=l z?S(;lXg}gDLj2%IzidLjJ@X#GZwCw#1T-2d9=^M=7xpq)lRCU{Ev{HP^>~1$-*p6O zsg6J!wuR^v#C|uO;~XAoUwXe@)ch5ioBnu$RFdIcEXs3)tRM8OGmq8?T(Bu?_5NsG zA|-5KIE`T71Epl$u?Tj7wDRw~0=Q@BZ@MB>eTMoKl%n*yp;tV3XbI5leHKUr%I_}j zh)*rGenON)KHksAC3>2ZmcP34D9_O9iNTO{t98$!jbMp6v=TC?`WReksd}f50Z9xZ z(8*skuuLJX;g>4{mB{C9W>NAaMCfvRZ!;s9h&hy$E(g?XNp!;CD{GPHN*;dzQ~Nbm8uv(!-W)TO3<_Z*P}!=z0m* zLy0+U{?oNpT-o7cQworn*I0^V9fDQRs6lMF7dPPXk$qv#112Ty10w zQ8i3tcQvPc3MCR&;i$254p8~uGe&z+zV6IJ!L&cg z+veSt2E13fLVKvC5=*5}pe!8~LkaYHYVa;{b<~do;Mm6X*w@2Cd~2KlB-(^W6K0tfXWO=n%h>HRmotMh1!JHpL1*Iq#KrlYzOy?ir0Ix9!u78$1b@P$AZte$+1exuu&rj{$m;{iZ{KQTLayG`&$jSmSn{PR!w+v21z2YLzWz$bKX2H zi&Gu4TSHbsYn|oV^BO(EdqOb_Z zau1>|^N|9H6t#m6HTnr#8=7GhwlkNzu1vWGzUH_RcH(KETTo2)uJM(u_?Pkq56jW# zV1a@iuh>>?VUOgYq`J9l6W}(+^xNyaMSSMdXkV((5JirE40~S{XVc739q8%X%@=y^ zjb2E((|m*jd+do;?PJBlr+K;yIngNj831LlL-5u)mp?BN&N+AuH75Ci2 zgtP&P@7g?n34sSGo!grm>HZg1yT>ez-5}2gXWff>-^{pfziP@&9!*93(u3(#f3LEY zb9iaZCd%HyRbZ#CfgrLTN7Hn+3)30#y@=D|g&UxHPsq3MK@u&)Bi@=L*8izhb2s|P zuH7d^E}uM*`tqgxb^6X7k-yK9lpg~2jL53Ii8~KB__PEvxI%LaCGqB_Z`sH1zJvm= zuaoL~Bx`nixu$|-hr2m8RpO~NzxK31{R?doD5k{fRyjfz^5ckmxyw+PBYO}GaVm6Jn@g=f??1VZn3%_19@{V46hW8o}hHg4XmIQ7h z3dN0!lFl-6eXFpmmzI(0gG&%Z6-W zsql_K?B;jx-~>^XdlG#z(4#Wk%jYadpZ>xNcTkT@K&eFn2$sb=Z<>L~kJq3h!Cue|jPL2Y>-4(A8X(;|4{4TqJO?jOt>=u`*2|U6O zN#gDBh1E9yJ<$9*W7v@M;0rtU*c6ue8{8w0Sd+N{hnuBsmF1QODHgFU8lUcK4<=Y)4O+`Q@ zp$XjYAUyIwY}PCH@pqSk3C)md5-Us)*!iK((K@w8w5s7xzJFULAkgZb@(tEr8MwQZ9K}r8cMnT_@$W<iUEwg)!xLE`&u zB_~C;yas?H=#ljZep(l87*B;wJL-Y!jGe_C0#KCH91XX@jExtaz=_~jbkYL&1E(x}1J&nTM6#n!TgSHj!elD!8 z0lL_gA|xV^D`$D3oVzqMZGB1r)xpc?>?LGsWh!a>e$qIm_~EVxOdxb`$06tzxQ;?z zfOpvKzIJkt!SkEW(AF zjXcY?q63{}yC2xeoz&I?YGaFeSP6mW)uAZfi9@I8%InkNdOKMvu>p<_8n`Xznk!=- zw^{QL3OwD}xN~dbpfdFA6kT?8CKBj|E#JOEQl71a;M)%mppKC*TJVx@-E|0#%pXEf zV%%0Ti=Y>M!<|I-t!sT$?3SHp4hgIeLO#=*QyPnDAd{}PSal%)rAQXK4XVh)5T==d zrTdq|8iXOYmX)lE^LIX(3{-lD{>Q`{bFZ) z4?`M3e+*A;p~NJz>uZ`ZLOTr1!HvN_#A#8&VB|cg0^)YscOGIFfq%QTzCmxV68J0& zU0$zGaRZlvJIAJ^jFKyTV}0FyIX-YeqTs~6{ePJH?s%&A`2Y7g$Vz6Bb!>ai7Q1o#1GWzel;fWwWP_-n{(WaO*wq z_Wax!I06+hWOIW8bG$igPa~_~PolZoLEMdn4|M|`LWU!vL}Zv;>Pgg<7`iG?_+f$1 zD=T72*wb(3HrAY=a zB6w3F9@Wa7V*l~Pi<|h>IVds>Xx5Ss&R=HxfSg+D_M;#C8Fx>M1Btx?D_mVHn;^2A zM*bLE?v4p}f(i=clFF6lTRm{>nyE7=h#V~tQ8xhNX35s9meva*zepS$wC`e%`e)R8 ze3~)C_aY^c&ikT6whiaAdc_b7R@liD42&BCV(1+ zT>WZ2EYJM{XY>jyQHiHk7UP7nl3PB6~ls$3ZO{+!lN%fSeLQ3jDU-zJm(rh$-;_R_gh44n%KF*yg3~ z3gQ-LON2~1s&)_yPm4aJ7Tz1UZw$I_)o$RoL$jti%F~Nyx4pXgT>vRlC_f&Gi)#2d zwBq-UfI5Pg*7t2gHr2|M5G%en;o%#U5rndhYNg<_j7*IA3ZU%QwLq0UcDiZ-I4iTx z{Ae%$Ha3T4MTy^WmQ*J+M59qg<2-$f=6Co7j9}d~9VIiqJw23ve@o ztOY+TJfaEFf}?Bymvo0q zc<`+N5Q{-uCI6Vj-`MoxASO8np63=;A&Y8U%-tH#&D{J@(C3ce?mp?ImR|aG>WM9@ z!PG7{cxP6=gRG}1uB^2kGb1soDka!bQ>N0~$l&Vc_m^X;5#+_xt3zpNR~mP!TzoB? zFO3SX4nMswap=TORlZl=12xdurQ(YypqBIrYyWP0;)?JcnY?Ida*PV9LsKSv1yQy= zGTniIt#pCadZ7Zf*<-Hg(cbxOmR#yda*!v;H?*>$V-?$8#pJFOhWLWEJ0rKidmzYqj)D(nh&H%d zM3PRKh8B*kX7WM38|VY1nRQ4EPvKoesSA&oc-MK<{-au9&}Q&$9PJgUV3N0S=V5^q z_OJKPMj_bLr=>v*mJ3pRqj=PI9wX$8G9oxRkpM;)>n4aPxzMWYP7lnAJO9ui0Ew)n zE=4xdY~B>c*b<)lebb))kG*1P7s`l1^q{@U&~T-fFt7fiJLCXZe+EE!JNNv1NKpx$ zdcP10`%%oalq2)~$m~pUIDKU*4CF-jSrVkIb_gS>FK|aa8mQq(r3UdLezem4-}$tY z0tgAC)Y^b0n^B63z(AOK#A4PsII=c_A;_V;`0G)NV~FfOECE`YQ#*IY`<}}3l?_W; z^G%sEYbsf28IK(&;RBTjs2I&=T6^PW{Qj?Ah97Dxj8K?jC{!XU|n#kllyIXO0> z;NGcRq&BMttm#Y>?Z`92jacgrD2I-Sz1io{7zpY?rE1(Z-7d9VXXD*iDU8vXUzGvN z%y@%WI78IKg{fyXfWi#O8Z*?!Z@`bzpy%E^HlZbe2^J#WAzJWdJ?*pE{l#@g_Qv>c zP^aUc0oLc~`&Khi03CgYzy?-Y#>}AL?cLs?$+O1ky~;B5?3TNUH4yILzpi3H=7d+O zvd=YLf3-iQ?($XPN<13UhN$id=<3u6M8u)P=_ZK4uL~rAy`U673W(h*3`ZY;Q$bKq zo%0>M18-SB;1l`vH31+|ge8-&j5aDQMch0Bel>?b=e<{r2k?=&El*C-R%iP({qD|y z)cyF~ryAN^Nf$>gGr#O@>YluQUu2(A&*14}tQ>Xst_sC-6Hg_D#iQZ?j1Xoh$fvK= z86*!mSaWn)+P0xTtl}ZnlOO*KEIuk1OG1qq(@R; zPV#OM#l6;cLvP*z{Wf(h$4Hk?ZR~6hC*`_`|2}lAwtF$eohT<@D~zdKiIhcN{3CHP zXki1zzvn>E%Z(*jVGV3pCQWaOf6pDWd4AGnVE6iG`Os~;f+}ZI68V_#Sn}Y7`2A8E z9YyMkpcU^7bd@3K?%H+ci=`mU=;be+ldr$lLzhE*^t~oT7 zzZo;YsM-(sgRJA_N8f)wf@m;M&7IY`XoEl*j<9mUSE(;;1f4zwwfb=G%6TFen<1KolS3DMh^DAZ7|CG+bqpfB1u4kH5EZr@1BU33Y+&oJr+Rs;`yY)?5jg!n5Yd5WWMS+>ryhp$6b&hI;T4``kD#lJLO znjkH^Pg-Z2`Oaw>^pvcwT7L{vSkerqd$zSg7d!T9-v-=>UEVo69;x*TC2EEPE2aT*QM>_&k&$2=*YgdvL4o z+LA2;nFvRWYQs5ioD^vs605SMnIGvn1-iBQN}9QW$dJh`L7}sJ>0;m$ zHxB1N0X7BZFXi_s5|X-oMUAZ*j+(@+wYm$k_}`Btl24&5>4l*h@#II&(33^beXVK?J^H812h6%3sap1b|GE#Do`JO4z#3K_# zJ@+z`2<2r-b*Oz$x=T8=7VHKBNbNU;@!SjPDbAQ%NIkKoDl^!15w0@K@dpz$|=`p3;87b?kxVLI#VL9|L#43d*Iy3#SD zGfI2dY#lH}+F?K85f0+1gqoLcGY95QuF)+}+drrIQB!venoNM7yelWyrjq=Ex97uy zZW)}|Ls?XMW!U`#pOnb9_-lKkTzNNB^5_BcxVzglL&5TUR7CWkt&#A)C8`fds-y|k z&8>&&I#V-gga<+h&jpHr8k8%)IFnTrL0?(G^PmwdG4`n?tJ{kgfHO*iT=pWiRZX|fNuhf{L$*8L$Aa!LnfP*&=0V45wk4PjfGMFrsDq0Eqem*tNDvOo58NjUTpTG z-*}U{chFa7L{Vk6GV8G!fm9W$oE=aXy^U2z^~nkIu{l~;y_pciI^ zt7LUwFnRJ_S%1_x2T?&qcMnrKxnKM{*J7WW3?Xf8Vt{02O#oF~ed0uGJ>77fo?*!- zo(>b6N{PkXeTwD7xa~k1i#y~{-t6p*3l!DCbLr8Ha#XtpYuKnKSWYSzNAjQY6Wa*+ z7+>;^HhGn`wgsM}Wyb08kI&zJ>^P%pfmbg2ummO2uF|tG4p0AQCp#FFs>hO~7`dV7 z$bF&->T>nxLLq5Ku?_sFL@M+tC1@bskq&ylc)VyZ*x?0H&qO5}8dDhv(};2<4E}n5 z-6rJoii#O>8m_DF037q2t(Pe^_-%Sd9e$3+B;LNy9K15p5X?rnk?6{|-WR(rZwl?$ za7ZvJEqSS!J>E}(S>DF&3xp?VmzQEK zm?u@YyfS^_%DcAwx*g8!F;#5sa_>7&`)G8zC(@;k`cpP6Y;W>G3%wM2@_B785b11W z)AIK5&$Suz9>4ptH_JDV(p}HcS-;(I`S!kisRA}xOFIso3EvsX9LLQj&jtH!Q+>bD z6IS$M&yajwB$(&9k&y!?{_GQgmUPL!T#RKZo)g`Gi^5>$N5OaXTKzKqN)?Vf;LKlN$hq{IcsVv*#}$uV==Co`!3J%%I4~vk_FL#R?t5C z{DN&GrPL9(8&QTJGh>AfZO2Tn9XtYLTiA<%nz@yv37dzE{>>&oqcIy#xB;zLQ9@w6 zNPK_THFI3X43NK1t%5@5JqF?B&c1V1|7If~eZ2m!*qq!NN3$E)uDSL+M#XY=<4?`z z+>7kpSH7)BST9*6QZgl>1a*r;PolMbuo=nHVPcER#lWj;%%@i#8`(5LuZo4%`x=Pg zl1|cBguS?M(Jk9LnwvnK)WW)%;*vVTY$5SQ?_8Hb!*{u`BqX(@xt)Hq%B_|!c@yfk zV-*@^AoV?FKYGNS(5DSTukmOiD9gFg94&_VY6DvhtN(&B;0C0Ywtyi{H=(4?2wq&_y{+cjEq`UWL4*M&bu&(9#{PW@@a!&h zZn2WfyeP{k9-v>zL|4r5d*H5fn+mTMF-3Ry!+--*0P7Zgw2=OK?youn|4 z>UXq-n0V#)`f#xyT>*kLMif~&k^VLj_cQ$H1C}Ve*VlDzyRdX7R&N6bfI9NFd(1>bQ+<=0*D_90?;dm2gCoMe6UVoMi?N{R zH}S3DXMlmD?x&r-jT^m2jO_NB5}D8>lu^klU@wEdV3CO^KtjMNT-{UvPbaLSC93r> zK>$(c&$*Dl<~UYL>prZ`b`{-SB5e0wmwuND)gO;bA`OruLKtfIdJyD7 zoU$HZKuc&40vqGa#^5T7Gn_~ChnUaLam%=Wg{~8 z$U;@pQJt4^=vK2Sq?G_kydp;KO$%N9`L4{l72ahDH}5A%L(JHESRKv*n$I&r+9upv zZ#lh6pw#&%`Kjb%rjYu0U!Ud&1(eGX&jGfSr--P?bL{&JB0CtUo4dBo!X@ZJZrY^# zVTk*_^O5B)gGgagr0D+HhQ90h%(81AZw}eA<{P+TfeNp&3A9 zIeGK)71_TOV*}gan-+HHAZ!O{td)6XJUb=|!UC2i9y0vr za3CWq{f46N)2DQ4gDeh&Jcc`53ab_0xsk`SnR&l_5f-=1WAR^E7yEyFjCAKKI0%!i7Ug5 zW#kS8pt9Rla7;t7#fq}eAG>1=mXS9C9n6pD^#p%0*+VqcgG(MgPYipqR>b@2nT&eB z6II_~yjzAE6JH)h^?awAaKUyCZ|$3|Db>M-(oZQex4JYJ1BY zzz$wEXq{Een!g4t@rHO{9MmkrnD{#=_YT?mJpFbo{igraA4YS6Z2qeLYwhM;$-C)F zWZu7)1*Y(V55wo z1O!Bo3#Nq4fJi9T#gyjsH7a~S0o3Ea`NzJxoujB~$+G%6-u|=W61~2TUa~~aObipp z7)b7^GUG=C+f5hX2oDk^d7zNh9H*|dffNOf$M-R!7%k9<-DllZ5n2|zaNLsmM1`c?u!2h-RjXe@T^4@$gFFfzZY4x|IpGY0+y!!*(?#kIf)3=x2NUf5d9LA;Cs zMnCrfS$Sm@SlZ?p_S|VW^7lu|AiC5*SS<6Z122iUIyx%OV!}$~CR#=TrXNTO{zU8K zSMRR^kl@|!`T2!M;8nS6-SQ#z4DWh8qb9 zL2!?3nJeew@&m^ou^63pE(#cl?IcZJWce=BgXRFz&-_8kNelKGnU6`P)tWCY_m|2N z6fk_@)J}!afelAigCK0J5GOE5_nA9CyBLmjUf_-fcjuwWp4XXtq*11TB+h6fxILfc zDe@@6>bJwa2$^CI!X?K#x?8C;6@=<}DAup{Uom?=&)Lo@;%z^P|CB&dTPg~;#MG<< zJu>#zP%W@0Mg?7#AnzioV9hzYZjo1_!gQeYfzvMkIYhNQ!O#2^tJrd7obntKnc1f5 zJxWxCNZw2YN}`odgwu<-W=@l>`|A!%sCzj)pZA2TgKO_bXEi7h=M&3ZV^N0X>T_=N z$1a)WnHk1kIwR($7Eegx!fH@CH~|$|np9ZQx#V$NF-cZ0SblKaysGdD@pGChe zoc)D&ci{@Ffjm?8+V_*dfi6ml<6j*fz?tMEk>+U2Z2;o8hJ)a7v5ytuuyTK|c_E+y z6G8X_`AgfWQ)%!XxmREPF--C2GHo?FeQ#tuifFKU1 za`tGpyPR)F5JALNZyu*H;;2Y)tf$(sqBZ)80_5&YbEC8^YTsEV&txPI)I?D#o*g(& zoq0wOaZZlC+YPNQ$Y+E(7AL;+Y3gx1p{-&XduDyEJBwqABjEP?b$w9cBy8{}jL}k>b*z2iGK&ajKp9qDj0adF3ORGgoovB< z@6+5)1=r~%0M-Hrm}V+7iulEzB)P@Tw?>;ZFqIz->2ux*efwzL?T#{E*MLRPKyQP< z6iQqOgCl?BcMTxXQB`q=?~6X%paL+Y|65aKJ%@j!j319Read|5I}=%2{-Q3Q4ZqQ8((ZTxAX4Y zZ?aQP6Gm=S{weMRo|X_m_CnTgO`V7~^f z14`I2aBUWT@bMd2^k_U~h{g1Q)Z42TgV*Lfz20N6Uaw(6pg7a@Pv*ml~B(hw0z1LIGRA@XN%6u@5;V)^K=Ov2W9Wk5^ISL-a0fOzY>yE3N zcElMg8Rck191a4pCzr*xo=hQ)uKP#g`67*}z>3sI4nA5|1jABvCZNMWbtm?<2L3&h zF5}!r36OYouuS~!Pw^d&OzziRdx*#)wC{lxsQ3)dHdhn)4r*wan8|wsE`309{q1)8 ze@pa}tE)lHuB|jU17zfL*D{L#gYO|5MU?wC+q44AIK0b=Mi*8CXM$kMYwOFhyN;>T8Jz*6S@ zeV%w>pY_JufXzCaX3^m+&Cc|kOO4T)=BO5F&$=*Hy@r?~yV;5D*p z{y-Q!!-v?yEI(b*WsmJ24UQ{+!9|b*S_cmc0W}PGWAWJP0duOmx>mMqWx1I(Uy+@yM~w6N&@Dh6=05t!^3PceF*J{xxa)9!>Szc^8A>nX*%K_=*=j+>tD1S;`{2Et zV@#8JaQD&ve2~t*s2-PGetpCVbNLN7@8_N8Q=e&AICV^L2q0YsuRtA72&7@1Asx;8 zGyaT`VL|Ac2ixEyuIA@uAn(wjLHZ=%Gyv7B{)&IWKLXL++XO4%P3lsHZo``_hCRDc z<)Jtyb_$DEp3Nb%cTXnANh1w8k3ARJ^=%kKulbwPDk8WolwJel7gCe#5&Qx2jVM#`Pu ztMNxkv&=W;OUq5teQ#oHsZvoRpoS98f&aAeZ%dPIKBThPcZ#vJfXBz)WnXyp_FFWy zmrtp7e*bp*Vn3;1+!xHKKmQK|Ix0D8SXb}NTs)T8+A+4DTQijhYWDttGOQ&SCIw_A z4t`K02xQqL96{aNOL=)_kGULrB#Y>_?7G zdRupAneSl76SMvc)Qeqhq$!H)Bfm{h4TnPy*!`XXl@G5ZClNSup+s@1V-<+wz`Meb zO~9gDfWSJM!3@y(c~`Gw0hYJmWkP$i%J@nUUsD7p@jb9WU^mXz@Pl|pHXw08F{2g0 z_b8E>y?P(cx5+_&vT7`Xt=|gf0|<$+oczuRTrfd<_|cVnz8w>*`Gy;*RL>uVfTS}g z13W+gBWggOz;r958ubrC4DtTi+0`(4?9dru$tNz=!7n4SoR&0K7~M9gjYn+%S@WOa zAoC8ufEUZ#f_lYo6G}h};CGeCt<}&TsYycfQT)LDH|xBSooht{K(yi*c*w@YsxouK zCf%>6g#s1QF>uKSl3X;BhtWYW@EpctB2$#YL1r6|+g?MoG`EF;g=(n4eE4;xK%^Z?lfQ#Z52z z!vq?jzME*UsU55h5Ic13VsBr;d3mgx82mmav*hd$e?sUl{>q8^)pR2>&EiUHVl(6P zD;+3idJJo4MqMm@T)qqk>-`zQe&v5Ir%UgA4)=q4)hnh?hMNe^qB~q@XJgWhdP%`4 zN73K#I<+v}Y#QA+B~cFi&dWE3%H9orfH?4 zV~19V?(1xw?W+pi_U3kZ8FO5T;5pNW9B7C|c);|rigTCgUVxrXA=r#vQbWET9#@ul zlPOJrRf=Tf^MpJHT`|?C0jMPHb$d#BGme)4O!^DtU;mBq^xO#C%!=r8Q5|$LyVteX zXRsL^mh@Bce?e%S>vO5ACpJMg`H+h!4TL^zW5u4A&0LRb5$^cY6o7*@_%HPW67YDV z5Biexdh__{TAaO*R173TE>ZwRVsDdCcFi=rcRxGqtLGqcG@dty$(g3+=z3zaD2-)(W zO8>=x&Dpa2qRxLgm+31U1`e1ZuS+CxUsqnp_Y(}9z^5@H42gDvn1u*|pX}kc-afeujMvRj!5O7N^y+KF?!Q=CqtAF>``==uVbg*Arh2-l!zJs)WghyAk0~b!~bN+V1ttItPld&@=?B;2>Z@Hgp7wTAe_N z;nd?b0qT$wON-Uq^Vy)YeW62?>fjYb4y@+GqWw3L2O17(K3gevS;vj{gGxL}W!mbw zmFo*h($sJVsJ*4>LtR2BYTFDD#S=4GZ_z zjAB+oAK(D2J+A%TP-b?A7hE$Dz@ork;n#2fhx`Sk?on3k-e=2#hxalFqVq^Au+R3W zFEoK7qk_;Kw_qwy*K!9E#cxSdf|kiAeUFEe2qf^u@53h*VEg=w;Ztu--#(&fA|2X; zf=R`kl@qW5Kulq=zYJ$Wtzy|THv=``yorU~F>pO9hZB2>)e`L>34mdf<&}rYn6hR{ zVmD6n?F^p7Pq&j!C4?0yua73`M@VkuL_BTAH9}mwGa9=|h7R{6#BT-YVfeZhT2@p- zvlsLjr})-KMWs5JTD9EjS3n2`F)U*?61kQkaJP1C=wt}k96ATixS9;F=3;B3UJ@ri zLOO?aj4>UWy5KH*E}~9=dwu)eH$1dqxNUfd+;FJxomPHKwst4{S_Xlqx;a0l_~K;z zlRW}(Om0A(0&5=f=;N$8p48Fa3IELrfS|@{KY;sqX^xq>MFJfYc5c*5PVbqK-LrdF zFD7D#_JGn#+wXTEbnw-^e0Fur=SQO`YB2`vxHA$Ve_pRB(0^9a7gKPKsQf6kw7+J` z+?8)}hKCS1;HpSK4JVj&62FF=!e>}*8JCzdAWr5D8rwGGxt%xa?w~cQCt<{2SfN;e znYLLyR3rd-Eb;`Q5zU`IPL4wP;1mtA>GjC+SAyDx+s4)=lCo{N#a#ZJE4=D zbA3H#|Cw$D6uH%N7oUchFkA32E)LxZVpxo?c0!A(1V3Jv+=j2M29Vi`n^?sUAD|K< zV?$2n=wF>5{OGQ`u`@J9q|Fp5GTzVSecn9?f7A3Jxr|JOquBfSSKRN#l#BXBvQ|M7 zNm&TFTpM;~mOMTe`k_*bgRc^R48?X&PNsXxmTTP0IWbUEa4rWpk^y`a5>e+z?Q8}= z%t8N}7yuSkQ7BIa(@)3~{&A&CP2#OuugwosKh6tpJH!B(MR1*Ik0RiPt<=ry*}`==5F_nG1^Q8-OF() z<>K`R3ckfDL#dDfo?Tr%5cg>Y@P6$=U0sp4k@p3gHoL3AviZb>s6X^!#|H22-!Y@J zGewG15VRu5G<59W0_eAtu>!NR8Vjd_7VEwy*z?tjNI#^*I@Kimh?2Dba-_XcaDH1d z#PfSzh)p{zwNw^POVTQX2}-~vH)xih2k9q?yKg2IbjF9_*f;IPWxuwiy zh0v+j&lc$Us)K1ylITJhD*^naUG^0yb{)T-A%ndrM z*zoi~a7&J!FBs_{wYt`O;NKLVDhM{nsxApze(h4iTKlz7*MKR6P1zL-txAYp)ak*0 zN_P>hoHsf^+Yt#$8Nu=iRjd>VEI6NS;la<0BS6nI8|hjArSEHN#2U_6tXYtRF1xI2 zlg?DT%_z=8+6o--AvBAmFk66RK*Q!5A#v>q_vx9BTB(wb=%C*?#z@^0D0nVLU-ViX z7n#hs{*=k$SXbzYE%x0@RQdz=LkmYV>X9phbIh$}y6dGqD71|jrI!inew)FAp?#!) zkPkLnhV(wH0>A9oxEA{30=o)Zyx}5+i z-+g!s3&M|>+Fai^Ja#SV!J8hDAj9aqHy;Fa)wEu66+vu0CDvUL?$eP)GYKs-He&EI z4@>VjcydYTmn*~0XOf2NJp39Zs`I&tTEo#3+GA_nXZ)(8u%v-KHAD4jTU*> z-MJ(v`J8OflihS57ea|Seyc*?O~R=9aP>i0wF`RSjI|BLF(k5qjamEdg&S6mAae%( z;?vKywhH=6e*LdHG>BafBXX@%G+rDu&=#cZmSLJk!oR^@oWtCH!lmD5uB=yyV=li|=52@J5?vo{&R|^uCIxd{mb*Go|%stuBt)Yz{Yy)u%LE zdOusbQpLC`#0aPpez3Kxa-6zXc1sr@;*N&WH8nKNAy^CC`Et}{^ObK=^ z_!hrSh?pV{Mr>+;VHtjzJ~)<|_kFoKZrBTz()NUM-Qez${;-nB!5>BaLJ&sGVC6?C zW6>0lCv<<+9CV6C3vKuWU{K}8XW6B9k>d!XB|`!ADqw)nAcuxAXltLc?Z)wnj9uCU z$0(e^0B)ZBt*2V1!npd+rv31=+RZ{;bEc;JE1S6&`I00E@8PY4hXsEFFdXvCA%ip? zRMoZg-EUAi7|gA(wI|3;dJHc?P!H8Ih66^_nKsh_7>6i26`B_G`@a=X7>tIwvvfOg zK**@#MX&=*S36&Akyp9+a^myw(p9$Fz3@!iXMqOAQExPYHGQ+7NHd|Qf^q8~Q1fm# zfu$0}6lG2N!Ii5pJT*Wn7%EE6!u{)=>!W%ZEKY~R;yfDfDSbUu901q1k9x~pz47?J z895UD0Dj-CD1K4-;}{Yj>ZpGQ@eBFZzhxE`f2EZrrcpNx(z2p}=`1ZK&8~%0@%6`t26n7H+=6YNFFj6$jP$EC6#+bZ8c)9&`-!5fK~(~n zww8wxp6$;~2VlVE^GJoy^?|qQs1LV|zPtw5G}%X+@KlZYwJOv|VZ0z=AvFQYdqE6- z)Nk5MCuIaOobC`WMJxnNPqX)}Laythm4Ty&X5R%rBji!Qz_>&Ws&Q>y&Ux4uhjqe{ zH7GFj%ccC$ICIC^f58!VR`}bi>7Z?ROsfHGD=&OOmf9GjS3{$x9SaT8^+PQ^k#N-D54TKnriH6=t+l>h2ECb@1s0=6j5+P}6=G%s zS$2PTv>68}1k4=9rf$zI?5mP@EabV(SYO&88hD^Veg~r5ei#`A4(3t_oo$xj^DZ7J zBGxMlAi_VXA(zjR?trIjB?7*irsZfWN2!@E7v~2?ZCua`wl+(G0dk0}aT z8f#Ix_`(}3+GdqxTW6SY=Eah@HvdU2?G>2t{@M{zCP3%2m6snA&*M9I&DUp2A~1@X z!1|1>7Nkti3pwY^QaKFzi%C-+ydO9l@7_3 zX9VEsRqZY9+HbB|*rSWkC9Xg|Q?9kOn95f^!T{4TgnI}m3GsRq_-_pMDc|#WR2AB% z_k$7Va>y&-?DzWza=rzV?}2b+!~}C>Fk;ZSwFl-`%!1)Z=k?lK2y%H8&0gP}K0zD( zy^(*sOBl2Yr+rEu2B$MQQSLsDUriEkR#$pn%xrRry$6UQfCfecxJ3_Qpy6nTpjdw);w|j9a^6Z1xHNg%@<1V*q zPccl--Q8|Ws$tgnup{kbT=o7Gr7!*+FGz3lTWt`u(c)iNeO<$iMU*cu*uB6ar}7It{lb(mq4%7`DKSsBNh|)X-p)*PI;UEUYyPnR=?XLW3pqk~jRSR}dD8XOduIU@QbFc( zKcRj}Ve7fnG%C>SZTvNs-)2FMuhF6Un3DNBBuN1vtrg+Cdjb4I$z?X1;d5fEm9oR_ zPOttua6TBME77}qZ=1zo4l(fruizgf0%0Cl?-dxe8{y#62cke1)lYeGtLnhSpQka0 zlUBL>VFoAudN_L}*9wb7COYbkhYwyn(k;3=HqPM+IR;FNPh~)xA^S%G%y?=a{2^0c zG^=Zv#`^i=C+Nb@d4x7{gz}GqcBv?Rr4>81+>gJw4h!t}r@fR8Wxowqb+%e#6`u~K zs(PUP!K-@%JY37?wE=i#u9v66cZatMB2Wa%goPF2?uJ1odtEf!Nf`Wj*z+zX0kC5T zp7dD{dmIIKSk(mB!z!%nwB#4nY;rF#mkd<08WmFB^t$@-y{%4H)C!ca}Nzu*ah znx#9%;;Gyel)1vbz-m7TKR{M1l*2wZ!6eFsx%}cy1@!UQm}71D@;q?IW3-3_g1ies zw1Kx3*ATbX|J;<@@lyRBoOi`;bm^LB{qrVubI~n{CI$s!t7GbAu6toay1Br7fdBU2 z@eduz#(t6xm7H#x05)=wDZ8!M$>zkXyI}CL?Z9Mn`cxI6Vk~_ON_WU68)OTKe(**% z-!U=u={uy=SaLr`$?@Q`-QGMQP7oxa3@ekbYR8^`Y^V7RB2QC<<-fEZY93y`%OjRI zto|mt0))cvJCHP>N+*>eXAgwCLK_tZR1{19aCr1TzYFrmXs&xP#Z_@d&XuPow{pmC z8F`=*dMOxKjFG(udxmMhuKc>KOW!q6Ghn*~A7RUYF<@i2gl{khOv^9v?%wERr%ryS z7c%3%IECmln1aqWc#DaI7GII&-?$z?zLWsjh*@uRZ@|#m_X9_+3$OrL z%FyJU_ZylVe%q>_&sB4_#b{J4FZ0dQHD2yb_5_l57Vr=-(SkQ3nxSHyTm(+r)oVV2 zcyK^^3Eoa!KGya4T78-qKgk4*6adJS`}+{H_1oMt7jW0mj1(sH<@q<5qJHY2i^OVd z2;*TQ34%q`Fi7k=km>LJt>~)U5Wm@pbnm_3Mvv>LrYoz~6!0W}#5Os-J>R;zg-0|? zwB(&n--(hjy+6hE4;-KRK!o$);d8Grc-|c5ev`s(BS_vwVpFb!avQ z4>|_uk+B294c|cY=TspdMWz=$MCG+5Jj{%oM}U--Lr3UZl6?N?n~$M?uXPl ze!TBNS*u5{dW*oR`DlqQ73xA3_V16Yq&^gzv$@T5%$35#?{i5ou98vdf4cD7b97;< z_xIt`$H^=iJT9ywtz1)n85DEoYV1jbWQL>gbM*3Sd{!p;*tDZ3VM$ zcOfEYp%08WCRIJya=9W`HLik|yUuubIQ3KF@viXpdZ}|_(6li!R0zNXb85}=O0Ds3 z-ad2k@vZ&e$0i{bAm6!f5{5vkCh!O&Q_<=f(J4PC=#kIdKi87C=+%{*@Om;Xg03u2 z=&ph@1zX1`-vSt9nN?Y~`y;m6LX}I`ym-Y;sHV8aq%GG#`}Mlv&DrmP%wkWE5FE!$ zwa6jmj?JHW0x-7>!9P+D#1&4J>Pgj=bCv&XN41?Qci$KPd_4t219^rdn|~5czVUi5 z3HNK_0ai#r!ha*Cze><=0vIl*us(F{*F?vxc789_i5t`Q<>URhx$`EvM%1Y!l0O|j zb?MA)Uj1D>VI9l#E3upW>*o_FEt_Wfqj2ZAsCQmH`*nG-TbF~V9!eeS{oQm&`5;T( z?^dfzq#X@$d_QlpOFrqD?}K!Ws(BK_<<;vwC)J(sG;)&6#;)y*?{bWTQRyri8_yyn zuTJgYd40R8yLo2G#2K!?D9Gr>OD!-odtB;H)bE4XoITdCTZNbdmm#DwX zBWShT>O|uS;HE4y+bvMfw~d+YjPZDq)d0{+D4Y=KB6+o0y6{(a=khKn>nR2qhonxi zj6RE(z0rQtUZ|tk?9r<~d+y!Hetvr3_hZm^|1J|>D*E?v4RhW?VH4n3e>!}UZH zzC`Hmdy1RXBgok+`jM3G&K#Z!jCkvp1bs^yS(#TcN)-p|alK{j&<^-%LkA z^1Mf;E;n*Zjp1z>jl;Xhs4uRL4ueceKv~)9(QR&)Fc>2FwdXv|$0PLZk&XUMv#;F{ zLOIksJAKMq|EMu?8kw$t1Lu8r43=Z@Gb{e&n>Ur0f8ZKoCj`^Ny4q_DCFo9{XPczM z@;1!QveXX^1WibEUpo0cn_eM@G{}qjF}z=xgREo@O49;sH?J<-g!K^bO{cC$|GC8+ zXSG~~U!%UAo}Hnrkr)MBRWN0y;vzXy>GGA`STy-(`gZ4=bDxii8bsnHGxFe^_Iq9P z=Io0q(SsLkljnrXKI4Mu5ASR`%Z72$jHyG_fZ}{DIb9l*z*?)L|L=Rsked__e{y^n z7{vyZ?|lJ;>?8Jb;=>Lvn1T7Soi`G1nTZ2{KUD@VY=N`mtJ`+yPe$Qq(DqLC?hVnA zmpp2X&|1X?dY1cD0egq?SRsrSTAxXQ4&tN0=fvZq-%EY^j-|(*N+8WN@Jf;6&NM^Z zedou%QX77@+QI5d58iJD5L@l6C+@&4)qp#)k?oB<_Iv->^ST+%iwo^{Ist|6GpFm$ z7e&w@s{9QkIZ_Bci!f@W{950ZtmY3dxzG^5BTm69l|P&uJv|%YEGV*bYE#wf#EK&% z=3DL!Gu%*E>Wk~`So&N;k-n84zcyKr?t;GpCKe3%2(lKWx zPR#yZV);?x?I=w{CQ7kc7&({@B^0yQ>O`G8Ve5g>^R&@;(!eiO-mKPqdiy+~+xdFH z;;P;RTGNq6plb+ggVvu{I*et?Ok3KJVMZu|8PjJf;1AgK$7am+XbHh#sa13_X(Dbd zs+mKyO1u0T1h26kDyJ6nh-n;kpjGPPs^xeNjyyxhekle0oCrW@>xIfX4c5cD9l zzPwtau~fEwTlz=KPftjcAxR6JY5J+>^CiIE8 z0$=%2K5UG})FB4^W@lqlFf$e`rR4bO@)B2|SKCK;V7Vd#tw;C!j4%rd_v4N$8>=ul{L%7j7)*ZQ$>h@QsO3MV+m`zx zQ)NG`@tuIdvcV;o+w8nM{@qi%u|AP*#d=@|KSihcw|+BH3}OTB*a6!QvX`*8y)>``K>_`j3=_{vwA`E1xe$y`sKis+V{72Hc8x z%uA$bEvVD=C?~1g;s>Gedm1wX!@tXN*Y0%kjjgk9-&`yvz3hPe(6>jZtnW6%?^nY7 zSm){j)k9xiALT|vJG*hcKtLa_3I*OtM(WRuhO|LDx?=(^xa01v_liywoO&qwbF!h8 z`Iyueiv;YmKhHGcdrg2@w+K6v^fYGGL_x3k$%6egJTk=U8Nj=3d zzBJAS;*_5NiyU^rw^xigDeiY+3p6G|H`@b-=YRS)LYh5T>wkRH|u$N-mPvGX$f${OmNX67(mX+cqBWI;W%NE4o^j`|z5g2gHFkFxhPjrG>|W zF`$fjX*KIw$>r2ech}sub8o%Kw#YZdcN1Q_|DUAOJ^_y(A#Z$(L?C)xKK_7v*kgE( zHK~e=UmdA4P#=oh&EN=w{Y(I?QF!zGy)2yey8+?B^0Vh5uN-q_9vq3wOWZM@1)F~j z8w-W*eW&eHAST|Z+F;h1x}Kf6qcaNP560ZRwx*i2Zitj2R^AS+uH-(I)hrSd}x|hh)p)2ii$Fsr$>B!srVJOq26 ztGMc)h8GWc5w(}C6++V7S$zIIgj(7QX(UACc4b&e@o&8oSR+jBmO2doRJfndcb9)k zP%cIC%&3O8w<(>bW9Dj8cSlBx%ig?$AOnha_1vPWAWB2gkrye6UQl#lFWkU=u6bjXw$W^!; zZTSi@3u#UnMw}M1UH*+t<7ETaTE|-n(J%t)C?tsKmIl%MMiAY zco1s+&X5h^1{3BU@}GmEgyJ>fWw56{h-57SEl1Y(^%+;XX$rl-bVrT{jLYTrYX#gpJvr|JcTueZ6bJ51#P&7gf#<~T_0n)(mPi zCG}iqH{7i6y#nE2>h0wih+6B8PGN-95Xg~;IHaajErny6K#pg6U2LBAU^F~Gn=Qus z#Lj!&nv(M$0@;>lm_zZQhc;(^mp-4N)Lz>+*7lVhXlm#8NXDZ+tMhseoZig8%c8Qu zlvf-T7lBVE7cY-QC|qFdZ^Vi8K)PC{``-bn+=WFlLJN|Z9Js#w3 z4}|{!)8@)lO0&PVp_;L>#T;_}hLGO}t^C~LMEdgh9JCAY2*->6D6DNH?(w~v$BbBT zIlo70o<3%50Wus`(IIe`tA1{ZGUe}%M25<)B#*bWn?HmN%SARN>md%=5}ivK^i|)T zJo~lLjdV1GDN?5r<=9u=l1_uHLSqr0s)1N zc)a6iqVQLh={lfMrkq4^TCR zUm2y&GOr*?0Qut6%!sJ%%(l z=n@%kqJ><;p@EeaVwR+!5XVa-VLL})sk&kxyr}QA-N{d{R?mISR|gV2yvU(Ke}q@G3)$j&d-R6v zKO=9A>mpnZ36T9EVLTPBZPOo8A-*G74a8t+$5U>WsfXFiRkFJbn#u!SX*05DAZA|B z2Jz$fO+u*H8uv?|vcMgS&j4u+C!jXHb?ney<_Z0<9cZV0IO;fA@UrMoZX#af)F|%V zgBoJ<^Pi|1Xat?SyTYb2L%EI)N=-&Os3#!2W-PVyY(C6^jwhGutJ?oz<{sS}Oft%5 z&$#3>>Q@xo*CnO67D(lCwtF0-gTVh?=#tzOWv=?JbS$$*cHrB~ZPg7!w)NiHxU+)H z?|Tq#;v)Q<*_RBowM3VGn#jMXY4Y9HiMrziNhGqE!Z*|TJ8ipAU55q>H`L513)UE`)>&h*Um1SL#3eAPyg9dWQ z3O{*$#j}0=5n9*Msn(%bY###YH{J1>QBL;N-*L?hyb?#>sMsv`@MsYa-M<(UM^17p8O-JHMH&vXmZts&rK87{}e(n zj>QXAPK+iuDCSRQ#U2M6H=f^P^ATs>0FS;9Iu6atPB#*#>I?0H*10EWG}*8txuQ@G zc30BWR+Z6W!L3H|T^Zm_p-x>xo?XUom*cg{DAi=mqOwKVrWtPGKk53z@(@6Fb@fmD?qR4yLF*o>)P-{l>Y%8aCe`2hpZGDjD&zzy7%WA3`BdZ>~UwU+r4_= zUUDbCSmU89azPN+UU~ra<7~!(xR10e&^H0B(;WtWm@jU|l^Wrw#`j(Q6LVaIF*B-P zt4LN5UCynLZ7AX$$^MZdbhIuVlxpAB1_wVzm=HQrmy09mFZDO`epTc(Yj-E>*zKA8 zv0wO{EIiy!6N&;7EU-b)M{NNeqGV)2NbUIgeSpzs1}lSM3vGS05%f$rUwH)iaH$N6)`!qg0iD{* z$Gyc3y%UL7?&3r28!jK39nLPE8OTXsu~X4Y6l;jvJLqd0R2@y;Y{lK4e9{jgBYER{ zA-JxYv6ubbhUy@gQlU!3qffHXr7OuhNC#Axw5+Wvq(w%Y z6iL(5F}Jvsq<|k3TS)$de#Wc)^=elT7%GFfHCK|oSH zy{yU3HVrs^{>aBdB?3ncRyRxgGRw{Yb)#vSLR#9Gc&PT|Pd29S0za~(lJdfuA88rK z$&DSIb9+F9CWS>@mSCX8YMK2)cK1=v*Hgxrs-U0Td z&E@;O-0WMo$b!yPajD|AmqMQVzTp&bP_*2xG5EyiVc;5;_mV4 zFX`)vT{Tz2O|cDeym|sG-go2;c-Z(g2fH-mCqVf0Tz}JvP`kETQnT-TrvpFG4>ph+ z62p-Tp{a{sxL!@Le~O8ML9O0e04cLV;6V^>Re&(FnpB-%Y|B8`(DSW?+;#CXHrV^R zu>|&0DDeUrOyysTX00RgfB#vOj=~Si3r|6Kue;VGdhK3ta@zMpmHUm0eT{X-Ay85H z7S1~Bp4UexUVUPOFgy>)+n5cQw#In1iqR8)MgR-9rWzMfZV{1vRVPg)wwLu|$3%Ze z*ML%+yh(%Sbgk9*R<^}19<5|8g!)?;%v?Rzn4{_LXSL;yfRuM<*4gH~er?)OXJZ0l z)QGUI-j;>B*K)#A?&)@*+G(HuY4z>t2e+gT zQ^DmSH#BB<>mgl4k;Q^?xTnGL0%Z%oR1CV{yH7S%WNaRM@QKZe-WDQt*UYgRW-v6>$pih_|7r&wN^rYnELFR3nsV=FjPqVP0-f`ef*+M-}E`r+6DHe3dh5JJ&aoE!;R#w{QLB#8w15y`i_&l|ekOg>HROroW0HB<6m5Rx|2s zIHbXYP=m_+Sy+$)5XnRut=%VgYpUf;Y|R{!pj3oSBKPi5K6Nn&KAG)Yiq-pObE5(Q%$hAJg{)D{e}Dt1TH-D zbSuiq5&4K+@jrWK;AW;4=kNn;=p2>wc!h0pGA2v^R!sz#n#^_5_J-O;mD6Z#p*!n7 zCHTtVYcrB8}%onDEZmX%d*@Qt!l9x`1XFB?rV{}gU!3UZ8fQDX*S>H9&p%mjCcaR!21mwC28iwo9v^NhuAKO|lyUoKsb_e#T% zjoYN<35a88>`n}(){waS2Uw27bJ8YseFAB5?XX11mv!gXC(pH(wrh4auiyVBdgox_ zLAn$Q&!&+g$g=>KdjN!OdR=>_V59*Own$nOe8JPyhGSfX-{~?q<-fl5@tX;6$0Ce` za;PG&z^->D4#7_&Q&XmYJ{VUJ6ExC#rBIVl9{_exyPp%I+=C6+wZGl3ol9@@{224J z8-g9vN#}jI`LUDeJvgH6>QY`MLRk7@N8g8lEwBS$1jYUn++W4aU9kB6PFMD{|7F^< zPE-;o;zgRv!YCD-sfT!Un^A$xjSI(nO#PI8CNI7t>N7Ey(nQhKO44EvYZDxWt$Itu z!SpfbpdlF^L?=s~i9>+uMWG2c9!8k;{ZGw3%?>_Wx^&+0RB+=0BC95W0G2ftg2(tV0ZVZ@%t3 z{?K*&v)`O#h9o2l)bgYhFC9QB*rW-wK3P}BSk-~+Zo*s0tiPJ{cXdQ!nv~QNH2$zp z#7(!YPOeU$fcRNw6L^Z%@uQJ>D*}UPw+&oei&~Ltn9nOSgEg6Ehi`o8HUo#L>_6Q> zP0QaPuJLm5BJqmIb2bCP%ZXj(-p*2}^MHKSZ$23amx+l~&AGuiIsC1+%aac>f2Q*!c@hrSE&(UIsG2EbELe16Wi7&j1eBSfZ$1f2J!E73pFY4df8 z1cYtl60v{Lhng9>6X>st&_y&c>Er=Jx*A55SO=p4QO~UJt2uQz<`AgsX550>aL_T= z?_blWHoi6uL^|ssT#&mhr5Nqw*#Q1FMjt8t`cM1?ooRS+u@1s(9iMt947W<86{@+J zL*=Vi&WM?Ev1}DePzfl!$>hXXFoSv=B`S_qP#1}0dwSnEgl*9zwx+VdY$$|(qHcK} zR8_y8um9ewCh&?73{OfZZ#i6B32M@6m>sJuCBaD~9s*xGE^)Y_kR&Z1dVb@zUxg2q zL+CW>qx)8oC*TZ4t@w4&&>gK;|64v{%*Ub=j~~&9*?{H91Bu`p(HnvOT5FxKhb{QK z48@0Z*OaAluNVxOn*G=+X%z}dK@5*FFDjP3)-j3=`FSpZfqeZq6hf62nW;x)frjWd zLu;7%&FIz;2s#7nll6I!SEcEo_c-kHyaI)9I${MRDO%LZpMp_9ee5WW`#J`gF}B%Y z_55dke8hGL1fYS9A@*Uv0Mk+U?_Lc-4DzjHZilv5Yz^;Ev+F_>Y|U1KS>O(*a-X^4 zAVZg+43tSi+P}Z*cnFB4JMXGRPV?@u0XpKV0`A&mofSz#G`;`}F5#N`0Le1X@5?zY z2W)7=vAE3}{Ep&TuhYFEFl55exnjrb&;sqZFci&wic~2(B?(%kstX;`6)+g(WK6^; zL~DOiybU<2giCvW=x)o`0nQ>mHmDj^1Yx!Jde~23%=NUXB!Ji7Ei)6P4=#?V)?R09 zw=HvCg&&DjK93cHqL+*9q<|kMtf1A0VnIr=u7q6 z2hEK!D$M;WQ|$>Wv~}KId6c$!Ono(~82p1lV42J+t7AVLaQM~RyKV%2Ni>k$4umYO z)yLsXg9o>B;(kGaH-4=-OIFDNP0l7NOb(|LIKp7a4rC}1*<(oQVB@d&dq^U!y`y9Qu?zn1{5$vGXC4b{-*#*_RX+FZVE>2FH85dH)KKC> zs9aw^YoFVNB#gq!^(STq$SzjmFa9k#ls5KGf`&CiVlZJBFckHLtc%GOXNNV2+&vgv zU~X7g;pusPzq-51HN!hv8D#RF04>lbrOxxGGoH;Ivq{r;Av45VpQ`}(lDZg!B|9~C zJ7G)xruaMu1UzwB&O*Nqy)1m%KQHybcDNekX!@bS7ZYR!k$4&InU6es+04tvzdxP< zE!4%RGi`QWzb`*H$MT@rrjD*lhpZIuL{Oq?j?Lvnk45jlvB2o|Jk=`FqIKK2mtzn* zW0T@5HGYq^At$@Ti(gnnuR+#n=gUcl=tXqQ#tY{GC{Y&aT}{|4OXZO;XpXQh%j8Ly zMQJA;;aZ-yx$@=Gb+CB!!<6^(-+v+zPf=<2xGp6gVv%IrG2qCDKR?iAb6xgcbPo)e zmU?bqQV%ZS<*Z#}U+qQW^SP9kkJ`I`{mH?6{%Tfy_~A#ovB!~a%-ZF=jP*iA1<7`R zQ>j~fY|VSORdqr{agO)kLF-&*0ZBu;9s=V=PH*h*7F;S;?hnYh1scVd77bV?BZ2G( z2z?gDG#No9Q4Jz#;skLS06~tgPPWx=K^D*$^5ShyZ2A$n&@MXO{-yC+^W$}p9QT8* zyvT_8N$=SGi{H0q9CmF8er(GV5i2Q^EfA~wGloihv6ltE8oii z2)KzR1{42&W{fdLp@X$y{ooM3oYUmcmEZxV)y$&c=lpMX;TJ(+F8}%dKkA@|vs6?u zVs=j0rB7(R3_UBU4L>HLG6k`;!8=i-P!!rv>Rv2IGEU- z^=SjIFUB%z3n0-iARHCp$Z@_TBctb>#uz(Sg%C$gq(YY(xVdoy5i)yD`_xt$Ucdzg zTh21@R0VB9|LRY+UccV45K#Xf))?1S&?Bl?R{Z}(ua6mE!t5vM#CX zBQQ%52Z+xSP6`N)Qzb5YL0K=z%GNvT7TB@^V?=ynrv)hS%b-57=Glkg)W6nz_Jk0> zY6GR!n^SwG`~cRx+(72n2@}_VwdWZ=@m;wlf&zhoU?p++ae8&{9c@3}iwEJ{&5lfr z5MTSgv&#zEd~t|aZ!`i#ngFnCE-{eVQV8vvSQacsCFa9aUV4Fu(o>dvd&3TV3k-=d z>*V$Db@Wri0yNn5hw5{hy4HI=wThw9HZ95WX_5#+jDA=zHkKCusY7HbHX6huu%o#2 zO(AI|`Rb?x#{i#Fv2=C5e1r>V72lU1DR~sId?hQ0b0zh%SPUBx(5MEK9!leKY@}>i z-x&e%Gj31r-=ot)Fr0Lc(NUdQtbFW^LB-D2gh4q=ql|D+f@5s-cgUql+GOB4sWm4VgmqX!{A6Hl`_g6$d5G_M zd!>^S@c7xm5sBA{Z(PQ1nl85)xj)v1n>IRvL_hNW%>6>!SH~LOv%!e@6vcAl6DP=^ z=KNDA0u7mrpM813U2fr?f5iIX#chpiLx-_QNL^A_J1soTEB6TDl_lvy5RmVh^diXOr5;rZ-C&?C>$5I~&)CVoOKOXolyFIV=wHm&i%C=D>TQZ5~uO>1>5l z-bg!`A^Gyli~z1Fz<;i2Y=e(+aw5n>h1@INag$<}vjXCFNuZM*~Pqu@qD^p~A+|B;`oyz%zwuYt@B5~E_%#%W?8EKp+t^8Bdm8C)q zt2Po~7#+r9jAj5IshDRAqH zJ^z1a`QlPV7|5-YI%c?+@}*%g*T4e+YNH|XIv4gx?mJs zh;#_@TwJG4Id>0diN3iIxnfVRYCAGPZ45149PTQayM&amTSou)F2j-kdkauu@CXUt opy3f*F#kPbd<=|71&O Date: Fri, 21 Feb 2020 11:36:33 +0100 Subject: [PATCH 40/86] unreal project creation and launching --- pype/ftrack/lib/ftrack_app_handler.py | 9 +- pype/hooks/unreal/unreal_prelaunch.py | 80 ++++++- pype/lib.py | 33 ++- pype/unreal/__init__.py | 0 pype/unreal/lib.py | 305 ++++++++++++++++++++++++++ 5 files changed, 411 insertions(+), 16 deletions(-) create mode 100644 pype/unreal/__init__.py create mode 100644 pype/unreal/lib.py diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 825a0a1985..5dd33c1492 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -268,7 +268,14 @@ class AppAction(BaseHandler): if application.get("launch_hook"): hook = application.get("launch_hook") self.log.info("launching hook: {}".format(hook)) - pypelib.execute_hook(application.get("launch_hook")) + ret_val = pypelib.execute_hook( + application.get("launch_hook"), env=env) + if not ret_val: + return { + 'success': False, + 'message': "Hook didn't finish successfully {0}" + .format(self.label) + } if sys.platform == "win32": diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index 05d95a0b2a..83ba4bf8aa 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -1,8 +1,82 @@ +import logging +import os + from pype.lib import PypeHook +from pype.unreal import lib as unreal_lib +from pypeapp import Logger + +log = logging.getLogger(__name__) class UnrealPrelaunch(PypeHook): + """ + This hook will check if current workfile path has Unreal + project inside. IF not, it initialize it and finally it pass + path to the project by environment variable to Unreal launcher + shell script. + """ - def execute(**kwargs): - print("I am inside!!!") - pass + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + if not env: + env = os.environ + asset = env["AVALON_ASSET"] + task = env["AVALON_TASK"] + workdir = env["AVALON_WORKDIR"] + engine_version = env["AVALON_APP_NAME"].split("_")[-1] + project_name = f"{asset}_{task}" + + # Unreal is sensitive about project names longer then 20 chars + if len(project_name) > 20: + self.log.warning((f"Project name exceed 20 characters " + f"[ {project_name} ]!")) + + # Unreal doesn't accept non alphabet characters at the start + # of the project name. This is because project name is then used + # in various places inside c++ code and there variable names cannot + # start with non-alpha. We append 'P' before project name to solve it. + # :scream: + if not project_name[:1].isalpha(): + self.log.warning(f"Project name doesn't start with alphabet " + f"character ({project_name}). Appending 'P'") + project_name = f"P{project_name}" + + project_path = os.path.join(workdir, project_name) + + self.log.info((f"{self.signature} requested UE4 version: " + f"[ {engine_version} ]")) + + detected = unreal_lib.get_engine_versions() + detected_str = ', '.join(detected.keys()) or 'none' + self.log.info((f"{self.signature} detected UE4 versions: " + f"[ {detected_str} ]")) + del(detected_str) + engine_version = ".".join(engine_version.split(".")[:2]) + if engine_version not in detected.keys(): + self.log.error((f"{self.signature} requested version not " + f"detected [ {engine_version} ]")) + return False + + os.makedirs(project_path, exist_ok=True) + + project_file = os.path.join(project_path, f"{project_name}.uproject") + if not os.path.isfile(project_file): + self.log.info((f"{self.signature} creating unreal " + f"project [ {project_name} ]")) + if env.get("AVALON_UNREAL_PLUGIN"): + os.environ["AVALON_UNREAL_PLUGIN"] = env.get("AVALON_UNREAL_PLUGIN") # noqa: E501 + unreal_lib.create_unreal_project(project_name, + engine_version, project_path) + + self.log.info((f"{self.signature} preparing unreal project ... ")) + unreal_lib.prepare_project(project_file, detected[engine_version]) + + env["PYPE_UNREAL_PROJECT_FILE"] = project_file + return True diff --git a/pype/lib.py b/pype/lib.py index d1062e468f..87c206e758 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -597,7 +597,20 @@ class CustomNone: return "".format(str(self.identifier)) -def execute_hook(hook, **kwargs): +def execute_hook(hook, *args, **kwargs): + """ + This will load hook file, instantiate class and call `execute` method + on it. Hook must be in a form: + + `$PYPE_ROOT/repos/pype/path/to/hook.py/HookClass` + + This will load `hook.py`, instantiate HookClass and then execute_hook + `execute(*args, **kwargs)` + + :param hook: path to hook class + :type hook: str + """ + class_name = hook.split("/")[-1] abspath = os.path.join(os.getenv('PYPE_ROOT'), @@ -606,14 +619,11 @@ def execute_hook(hook, **kwargs): mod_name, mod_ext = os.path.splitext(os.path.basename(abspath)) if not mod_ext == ".py": - return + return False module = types.ModuleType(mod_name) module.__file__ = abspath - log.info("-" * 80) - print(module) - try: with open(abspath) as f: six.exec_(f.read(), module.__dict__) @@ -623,13 +633,12 @@ def execute_hook(hook, **kwargs): except Exception as exp: log.exception("loading hook failed: {}".format(exp), exc_info=True) + return False - from pprint import pprint - print("-" * 80) - pprint(dir(module)) - - hook_obj = globals()[class_name]() - hook_obj.execute(**kwargs) + obj = getattr(module, class_name) + hook_obj = obj() + ret_val = hook_obj.execute(*args, **kwargs) + return ret_val @six.add_metaclass(ABCMeta) @@ -639,5 +648,5 @@ class PypeHook: pass @abstractmethod - def execute(**kwargs): + def execute(self, *args, **kwargs): pass diff --git a/pype/unreal/__init__.py b/pype/unreal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py new file mode 100644 index 0000000000..8217e834a3 --- /dev/null +++ b/pype/unreal/lib.py @@ -0,0 +1,305 @@ +import os +import platform +import json +from distutils import dir_util +import subprocess + + +def get_engine_versions(): + """ + This will try to detect location and versions of installed Unreal Engine. + Location can be overridden by `UNREAL_ENGINE_LOCATION` environment + variable. + + Returns dictionary with version as a key and dir as value. + """ + try: + engine_locations = {} + root, dirs, files = next(os.walk(os.environ["UNREAL_ENGINE_LOCATION"])) + + for dir in dirs: + if dir.startswith("UE_"): + ver = dir.split("_")[1] + engine_locations[ver] = os.path.join(root, dir) + except KeyError: + # environment variable not set + pass + except OSError: + # specified directory doesn't exists + pass + + # if we've got something, terminate autodetection process + if engine_locations: + return engine_locations + + # else kick in platform specific detection + if platform.system().lower() == "windows": + return _win_get_engine_versions() + elif platform.system().lower() == "linux": + # on linux, there is no installation and getting Unreal Engine involves + # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. + pass + elif platform.system().lower() == "darwin": + return _darwin_get_engine_version() + + return {} + + +def _win_get_engine_versions(): + """ + If engines are installed via Epic Games Launcher then there is: + `%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat` + This file is JSON file listing installed stuff, Unreal engines + are marked with `"AppName" = "UE_X.XX"`` like `UE_4.24` + """ + install_json_path = os.path.join( + os.environ.get("PROGRAMDATA"), + "Epic", + "UnrealEngineLauncher", + "LauncherInstalled.dat", + ) + + return _parse_launcher_locations(install_json_path) + + +def _darwin_get_engine_version(): + """ + It works the same as on Windows, just JSON file location is different. + """ + install_json_path = os.path.join( + os.environ.get("HOME"), + "Library", + "Application Support", + "Epic", + "UnrealEngineLauncher", + "LauncherInstalled.dat", + ) + + return _parse_launcher_locations(install_json_path) + + +def _parse_launcher_locations(install_json_path): + engine_locations = {} + if os.path.isfile(install_json_path): + with open(install_json_path, "r") as ilf: + try: + install_data = json.load(ilf) + except json.JSONDecodeError: + raise Exception( + "Invalid `LauncherInstalled.dat file. `" + "Cannot determine Unreal Engine location." + ) + + for installation in install_data.get("InstallationList", []): + if installation.get("AppName").startswith("UE_"): + ver = installation.get("AppName").split("_")[1] + engine_locations[ver] = installation.get("InstallLocation") + + return engine_locations + + +def create_unreal_project(project_name, ue_version, dir): + """ + This will create `.uproject` file at specified location. As there is no + way I know to create project via command line, this is easiest option. + Unreal project file is basically JSON file. If we find + `AVALON_UNREAL_PLUGIN` environment variable we assume this is location + of Avalon Integration Plugin and we copy its content to project folder + and enable this plugin. + """ + + if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): + # copy plugin to correct path under project + plugin_path = os.path.join(dir, "Plugins", "Avalon") + if not os.path.isdir(plugin_path): + os.makedirs(plugin_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), + plugin_path) + + data = { + "FileVersion": 3, + "EngineAssociation": ue_version, + "Category": "", + "Description": "", + "Modules": [ + { + "Name": project_name, + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": ["Engine"], + } + ], + "Plugins": [ + {"Name": "PythonScriptPlugin", "Enabled": True}, + {"Name": "EditorScriptingUtilities", "Enabled": True}, + {"Name": "Avalon", "Enabled": True}, + ], + } + + project_file = os.path.join(dir, "{}.uproject".format(project_name)) + with open(project_file, mode="w") as pf: + json.dump(data, pf, indent=4) + + +def prepare_project(project_file: str, engine_path: str): + """ + This function will add source files needed for project to be + rebuild along with the avalon integration plugin. + + There seems not to be automated way to do it from command line. + But there might be way to create at least those target and build files + by some generator. This needs more research as manually writing + those files is rather hackish. :skull_and_crossbones: + + :param project_file: path to .uproject file + :type project_file: str + :param engine_path: path to unreal engine associated with project + :type engine_path: str + """ + + project_name = os.path.splitext(os.path.basename(project_file))[0] + project_dir = os.path.dirname(project_file) + targets_dir = os.path.join(project_dir, "Source") + sources_dir = os.path.join(targets_dir, project_name) + + os.makedirs(sources_dir, exist_ok=True) + os.makedirs(os.path.join(project_dir, "Content"), exist_ok=True) + + module_target = ''' +using UnrealBuildTool; +using System.Collections.Generic; + +public class {0}Target : TargetRules +{{ + public {0}Target( TargetInfo Target) : base(Target) + {{ + Type = TargetType.Game; + ExtraModuleNames.AddRange( new string[] {{ "{0}" }} ); + }} +}} +'''.format(project_name) + + editor_module_target = ''' +using UnrealBuildTool; +using System.Collections.Generic; + +public class {0}EditorTarget : TargetRules +{{ + public {0}EditorTarget( TargetInfo Target) : base(Target) + {{ + Type = TargetType.Editor; + + ExtraModuleNames.AddRange( new string[] {{ "{0}" }} ); + }} +}} +'''.format(project_name) + + module_build = ''' +using UnrealBuildTool; +public class {0} : ModuleRules +{{ + public {0}(ReadOnlyTargetRules Target) : base(Target) + {{ + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicDependencyModuleNames.AddRange(new string[] {{ "Core", + "CoreUObject", "Engine", "InputCore" }}); + PrivateDependencyModuleNames.AddRange(new string[] {{ }}); + }} +}} +'''.format(project_name) + + module_cpp = ''' +#include "{0}.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, {0}, "{0}" ); +'''.format(project_name) + + module_header = ''' +#pragma once +#include "CoreMinimal.h" +''' + + game_mode_cpp = ''' +#include "{0}GameModeBase.h" +'''.format(project_name) + + game_mode_h = ''' +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "{0}GameModeBase.generated.h" + +UCLASS() +class {1}_API A{0}GameModeBase : public AGameModeBase +{{ + GENERATED_BODY() +}}; +'''.format(project_name, project_name.upper()) + + with open(os.path.join( + targets_dir, f"{project_name}.Target.cs"), mode="w") as f: + f.write(module_target) + + with open(os.path.join( + targets_dir, f"{project_name}Editor.Target.cs"), mode="w") as f: + f.write(editor_module_target) + + with open(os.path.join( + sources_dir, f"{project_name}.Build.cs"), mode="w") as f: + f.write(module_build) + + with open(os.path.join( + sources_dir, f"{project_name}.cpp"), mode="w") as f: + f.write(module_cpp) + + with open(os.path.join( + sources_dir, f"{project_name}.h"), mode="w") as f: + f.write(module_header) + + with open(os.path.join( + sources_dir, f"{project_name}GameModeBase.cpp"), mode="w") as f: + f.write(game_mode_cpp) + + with open(os.path.join( + sources_dir, f"{project_name}GameModeBase.h"), mode="w") as f: + f.write(game_mode_h) + + if platform.system().lower() == "windows": + u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" + "UnrealBuildTool.exe") + u_header_tool = (f"{engine_path}/Engine/Binaries/Win64/" + f"UnrealHeaderTool.exe") + elif platform.system().lower() == "linux": + # WARNING: there is no UnrealBuildTool on linux? + u_build_tool = "" + u_header_tool = "" + elif platform.system().lower() == "darwin": + # WARNING: there is no UnrealBuildTool on Mac? + u_build_tool = "" + u_header_tool = "" + + u_build_tool = u_build_tool.replace("\\", "/") + u_header_tool = u_header_tool.replace("\\", "/") + + command1 = [u_build_tool, "-projectfiles", f"-project={project_file}", + "-progress"] + + subprocess.run(command1) + + command2 = [u_build_tool, f"-ModuleWithSuffix={project_name},3555" + "Win64", "Development", "-TargetType=Editor" + f'-Project="{project_file}"', f'"{project_file}"' + "-IgnoreJunk"] + + subprocess.run(command2) + + uhtmanifest = os.path.join(os.path.dirname(project_file), + f"{project_name}.uhtmanifest") + + command3 = [u_header_tool, f'"{project_file}"', f'"{uhtmanifest}"', + "-Unattended", "-WarningsAsErrors", "-installed"] + + subprocess.run(command3) From 1537b2ba571e0e614277907377e30f303d4b48c4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Feb 2020 21:06:30 +0100 Subject: [PATCH 41/86] switch to UnrealEnginePython --- pype/hooks/unreal/unreal_prelaunch.py | 1 + pype/unreal/lib.py | 49 ++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index 83ba4bf8aa..cb3b6e8e64 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -79,4 +79,5 @@ class UnrealPrelaunch(PypeHook): unreal_lib.prepare_project(project_file, detected[engine_version]) env["PYPE_UNREAL_PROJECT_FILE"] = project_file + env["AVALON_CURRENT_UNREAL_ENGINE"] = detected[engine_version] return True diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 8217e834a3..6130b38764 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -1,3 +1,4 @@ +import sys import os import platform import json @@ -107,15 +108,52 @@ def create_unreal_project(project_name, ue_version, dir): of Avalon Integration Plugin and we copy its content to project folder and enable this plugin. """ + import git if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project - plugin_path = os.path.join(dir, "Plugins", "Avalon") - if not os.path.isdir(plugin_path): - os.makedirs(plugin_path, exist_ok=True) + plugins_path = os.path.join(dir, "Plugins") + avalon_plugin_path = os.path.join(plugins_path, "Avalon") + if not os.path.isdir(avalon_plugin_path): + os.makedirs(avalon_plugin_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), - plugin_path) + avalon_plugin_path) + + # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to + # support offline installation. + # Otherwise clone UnrealEnginePython to Plugins directory + # https://github.com/20tab/UnrealEnginePython.git + uep_path = os.path.join(plugins_path, "UnrealEnginePython") + if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + + os.makedirs(uep_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree(os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + uep_path) + else: + git.Repo.clone_from("https://github.com/20tab/UnrealEnginePython.git", + uep_path) + + # now we need to fix python path in: + # `UnrealEnginePython.Build.cs` + # to point to our python + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="r") as f: + build_file = f.read() + + fix = build_file.replace( + 'private string pythonHome = "";', + 'private string pythonHome = "{}";'.format( + sys.base_prefix.replace("\\", "/"))) + + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="w") as f: + f.write(fix) data = { "FileVersion": 3, @@ -134,6 +172,7 @@ def create_unreal_project(project_name, ue_version, dir): {"Name": "PythonScriptPlugin", "Enabled": True}, {"Name": "EditorScriptingUtilities", "Enabled": True}, {"Name": "Avalon", "Enabled": True}, + {"Name": "UnrealEnginePython", "Enabled": True} ], } @@ -296,6 +335,7 @@ class {1}_API A{0}GameModeBase : public AGameModeBase subprocess.run(command2) + """ uhtmanifest = os.path.join(os.path.dirname(project_file), f"{project_name}.uhtmanifest") @@ -303,3 +343,4 @@ class {1}_API A{0}GameModeBase : public AGameModeBase "-Unattended", "-WarningsAsErrors", "-installed"] subprocess.run(command3) + """ From 5a56df384d0cdb4fd3c19bf64841269a26b8c025 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 2 Mar 2020 12:25:15 +0100 Subject: [PATCH 42/86] set cpp project mode on demand --- pype/hooks/unreal/unreal_prelaunch.py | 12 +- pype/unreal/lib.py | 173 ++++++++++++++++++-------- 2 files changed, 127 insertions(+), 58 deletions(-) diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index cb3b6e8e64..efb5d9157b 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -42,7 +42,7 @@ class UnrealPrelaunch(PypeHook): # of the project name. This is because project name is then used # in various places inside c++ code and there variable names cannot # start with non-alpha. We append 'P' before project name to solve it. - # :scream: + # 😱 if not project_name[:1].isalpha(): self.log.warning(f"Project name doesn't start with alphabet " f"character ({project_name}). Appending 'P'") @@ -67,17 +67,17 @@ class UnrealPrelaunch(PypeHook): os.makedirs(project_path, exist_ok=True) project_file = os.path.join(project_path, f"{project_name}.uproject") + engine_path = detected[engine_version] if not os.path.isfile(project_file): self.log.info((f"{self.signature} creating unreal " f"project [ {project_name} ]")) if env.get("AVALON_UNREAL_PLUGIN"): os.environ["AVALON_UNREAL_PLUGIN"] = env.get("AVALON_UNREAL_PLUGIN") # noqa: E501 unreal_lib.create_unreal_project(project_name, - engine_version, project_path) - - self.log.info((f"{self.signature} preparing unreal project ... ")) - unreal_lib.prepare_project(project_file, detected[engine_version]) + engine_version, + project_path, + engine_path=engine_path) env["PYPE_UNREAL_PROJECT_FILE"] = project_file - env["AVALON_CURRENT_UNREAL_ENGINE"] = detected[engine_version] + env["AVALON_CURRENT_UNREAL_ENGINE"] = engine_path return True diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 6130b38764..be6314b09b 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -4,6 +4,7 @@ import platform import json from distutils import dir_util import subprocess +from pypeapp import config def get_engine_versions(): @@ -63,7 +64,7 @@ def _win_get_engine_versions(): return _parse_launcher_locations(install_json_path) -def _darwin_get_engine_version(): +def _darwin_get_engine_version() -> dict: """ It works the same as on Windows, just JSON file location is different. """ @@ -79,7 +80,16 @@ def _darwin_get_engine_version(): return _parse_launcher_locations(install_json_path) -def _parse_launcher_locations(install_json_path): +def _parse_launcher_locations(install_json_path: str) -> dict: + """ + This will parse locations from json file. + + :param install_json_path: path to `LauncherInstalled.dat` + :type install_json_path: str + :returns: returns dict with unreal engine versions as keys and + paths to those engine installations as value. + :rtype: dict + """ engine_locations = {} if os.path.isfile(install_json_path): with open(install_json_path, "r") as ilf: @@ -99,7 +109,11 @@ def _parse_launcher_locations(install_json_path): return engine_locations -def create_unreal_project(project_name, ue_version, dir): +def create_unreal_project(project_name: str, + ue_version: str, + pr_dir: str, + engine_path: str, + dev_mode: bool = False) -> None: """ This will create `.uproject` file at specified location. As there is no way I know to create project via command line, this is easiest option. @@ -107,12 +121,30 @@ def create_unreal_project(project_name, ue_version, dir): `AVALON_UNREAL_PLUGIN` environment variable we assume this is location of Avalon Integration Plugin and we copy its content to project folder and enable this plugin. + + :param project_name: project name + :type project_name: str + :param ue_version: unreal engine version (like 4.23) + :type ue_version: str + :param pr_dir: path to directory where project will be created + :type pr_dir: str + :param engine_path: Path to Unreal Engine installation + :type engine_path: str + :param dev_mode: Flag to trigger C++ style Unreal project needing + Visual Studio and other tools to compile plugins from + sources. This will trigger automatically if `Binaries` + directory is not found in plugin folders as this indicates + this is only source distribution of the plugin. Dev mode + is also set by preset file `unreal/project_setup.json` in + **PYPE_CONFIG**. + :type dev_mode: bool + :returns: None """ - import git + preset = config.get_presets()["unreal"]["project_setup"] if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project - plugins_path = os.path.join(dir, "Plugins") + plugins_path = os.path.join(pr_dir, "Plugins") avalon_plugin_path = os.path.join(plugins_path, "Avalon") if not os.path.isdir(avalon_plugin_path): os.makedirs(avalon_plugin_path, exist_ok=True) @@ -120,68 +152,105 @@ def create_unreal_project(project_name, ue_version, dir): dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), avalon_plugin_path) - # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to - # support offline installation. - # Otherwise clone UnrealEnginePython to Plugins directory - # https://github.com/20tab/UnrealEnginePython.git - uep_path = os.path.join(plugins_path, "UnrealEnginePython") - if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): - - os.makedirs(uep_path, exist_ok=True) - dir_util._path_created = {} - dir_util.copy_tree(os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), - uep_path) - else: - git.Repo.clone_from("https://github.com/20tab/UnrealEnginePython.git", - uep_path) - - # now we need to fix python path in: - # `UnrealEnginePython.Build.cs` - # to point to our python - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="r") as f: - build_file = f.read() - - fix = build_file.replace( - 'private string pythonHome = "";', - 'private string pythonHome = "{}";'.format( - sys.base_prefix.replace("\\", "/"))) - - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="w") as f: - f.write(fix) + if (not os.path.isdir(os.path.join(avalon_plugin_path, "Binaries")) + or not os.path.join(avalon_plugin_path, "Intermediate")): + dev_mode = True + # data for project file data = { "FileVersion": 3, "EngineAssociation": ue_version, "Category": "", "Description": "", - "Modules": [ - { + "Plugins": [ + {"Name": "PythonScriptPlugin", "Enabled": True}, + {"Name": "EditorScriptingUtilities", "Enabled": True}, + {"Name": "Avalon", "Enabled": True} + ] + } + + if preset["install_unreal_python_engine"]: + # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to + # support offline installation. + # Otherwise clone UnrealEnginePython to Plugins directory + # https://github.com/20tab/UnrealEnginePython.git + uep_path = os.path.join(plugins_path, "UnrealEnginePython") + if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + + os.makedirs(uep_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree( + os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + uep_path) + else: + # WARNING: this will trigger dev_mode, because we need to compile + # this plugin. + dev_mode = True + import git + git.Repo.clone_from( + "https://github.com/20tab/UnrealEnginePython.git", + uep_path) + + data["Plugins"].append( + {"Name": "UnrealEnginePython", "Enabled": True}) + + if (not os.path.isdir(os.path.join(uep_path, "Binaries")) + or not os.path.join(uep_path, "Intermediate")): + dev_mode = True + + if dev_mode or preset["dev_mode"]: + # this will add project module and necessary source file to make it + # C++ project and to (hopefully) make Unreal Editor to compile all + # sources at start + + data["Modules"] = [{ "Name": project_name, "Type": "Runtime", "LoadingPhase": "Default", "AdditionalDependencies": ["Engine"], - } - ], - "Plugins": [ - {"Name": "PythonScriptPlugin", "Enabled": True}, - {"Name": "EditorScriptingUtilities", "Enabled": True}, - {"Name": "Avalon", "Enabled": True}, - {"Name": "UnrealEnginePython", "Enabled": True} - ], - } + }] - project_file = os.path.join(dir, "{}.uproject".format(project_name)) + if preset["install_unreal_python_engine"]: + # now we need to fix python path in: + # `UnrealEnginePython.Build.cs` + # to point to our python + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="r") as f: + build_file = f.read() + + fix = build_file.replace( + 'private string pythonHome = "";', + 'private string pythonHome = "{}";'.format( + sys.base_prefix.replace("\\", "/"))) + + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="w") as f: + f.write(fix) + + # write project file + project_file = os.path.join(pr_dir, "{}.uproject".format(project_name)) with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) + # ensure we have PySide installed in engine + # TODO: make it work for other platforms 🍎 🐧 + if platform.system().lower() == "windows": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python", "Win64", + "python.exe") -def prepare_project(project_file: str, engine_path: str): + subprocess.run([python_path, "-m", + "pip", "install", "pyside"]) + + if dev_mode or preset["dev_mode"]: + _prepare_cpp_project(pr_dir, engine_path) + + +def _prepare_cpp_project(project_file: str, engine_path: str) -> None: """ This function will add source files needed for project to be rebuild along with the avalon integration plugin. From cf43bbb18e1b6bf453241132a283a847d06e3ef1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:34:12 +0100 Subject: [PATCH 43/86] moved imports to top of lib --- pype/lib.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index ad3a863854..39d8533be8 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1,14 +1,19 @@ import os import re +import uuid +import json +import getpass +import collections import logging import itertools import contextlib import subprocess import inspect +import platform -from avalon import io +from avalon import io, pipeline import avalon.api -import avalon +from pypeapp import config log = logging.getLogger(__name__) @@ -489,7 +494,6 @@ def filter_pyblish_plugins(plugins): `discover()` method. :type plugins: Dict """ - from pypeapp import config from pyblish import api host = api.current_host() @@ -546,7 +550,6 @@ def get_subsets(asset_name, Returns: dict: subsets with version and representaions in keys """ - from avalon import io # query asset from db asset_io = io.find_one({"type": "asset", "name": asset_name}) @@ -620,7 +623,6 @@ class CustomNone: def __init__(self): """Create uuid as identifier for custom None.""" - import uuid self.identifier = str(uuid.uuid4()) def __bool__(self): From 116a16eb4c0ab04bd3f29c6d39889455e08ddfaf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:34:48 +0100 Subject: [PATCH 44/86] get_workfile_build_presets method implemented to load workfile variants per host and current task --- pype/lib.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 39d8533be8..06f3540177 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -643,3 +643,30 @@ class CustomNone: def __repr__(self): """Representation of custom None.""" return "".format(str(self.identifier)) + + +def get_workfile_build_presets(task_name): + host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1] + presets = config.get_presets(io.Session["AVALON_PROJECT"]) + # Get presets for host + workfile_presets = presets["plugins"].get(host_name, {}).get( + "workfile_build" + ) + if not workfile_presets: + return + + task_name_low = task_name.lower() + per_task_preset = None + for variant in workfile_presets: + variant_tasks = variant.get("tasks") + if not variant_tasks: + continue + + variant_tasks_low = [task.lower() for task in variant_tasks] + if task_name_low not in variant_tasks_low: + continue + + per_task_preset = variant + break + + return per_task_preset From 99ea83939a8707babe8ffe803a1bc78e870f1c17 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:36:24 +0100 Subject: [PATCH 45/86] collect_last_version_repres implemented to get all representations of latest versions for all subsets of entered assets --- pype/lib.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 06f3540177..5cdcf82d4d 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -670,3 +670,98 @@ def get_workfile_build_presets(task_name): break return per_task_preset + + +def collect_last_version_repres(asset_entities): + """Collect subsets, versions and representations for asset_entities. + + :param asset_entities: Asset entities for which want to find data + :type asset_entities: list + :return: collected entities + :rtype: dict + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(io.find({ + "type": "subset", + "parent": {"$in": asset_entity_by_ids.keys()} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(io.find({ + "type": "version", + "parent": {"$in": subset_entity_by_ids.keys()} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = io.find({ + "type": "representation", + "parent": {"$in": last_versions_by_id.keys()} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output From a415e2255af906e940124eb8715fb253531690a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:36:56 +0100 Subject: [PATCH 46/86] added get_link_assets, not yet implemented since we dont have the logic --- pype/lib.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 5cdcf82d4d..3e92349d10 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -672,6 +672,12 @@ def get_workfile_build_presets(task_name): return per_task_preset +def get_link_assets(asset_entity): + """Return linked assets for `asset_entity`.""" + # TODO implement + return [test_asset] + + def collect_last_version_repres(asset_entities): """Collect subsets, versions and representations for asset_entities. From 57f6f9b87d8280aedff63f0c3155fc897181ff27 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:37:47 +0100 Subject: [PATCH 47/86] implemented load_containers_by_asset_data which loads all containers for specific asset by entered workfile variants --- pype/lib.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 3e92349d10..30c222ac57 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -672,6 +672,215 @@ def get_workfile_build_presets(task_name): return per_task_preset +def load_containers_by_asset_data( + asset_entity_data, workfile_presets, loaders_by_name +): + if not asset_entity_data or not workfile_presets or not loaders_by_name: + return + + asset_entity = asset_entity_data["asset_entity"] + # Filter workfile presets by available loaders + valid_variants = [] + for variant in workfile_presets: + variant_loaders = variant.get("loaders") + if not variant_loaders: + log.warning(( + "Workfile variant has missing loaders configuration: {0}" + ).format(json.dumps(variant, indent=4))) + continue + + found = False + for loader_name in variant_loaders: + if loader_name in loaders_by_name: + valid_variants.append(variant) + found = True + break + + if not found: + log.warning( + "Loaders in Workfile variant are not available: {0}".format( + json.dumps(variant, indent=4) + ) + ) + + if not valid_variants: + log.warning("There are not valid Workfile variants. Skipping process.") + return + + log.debug("Valid Workfile variants: {}".format(valid_variants)) + + subsets = [] + version_by_subset_id = {} + repres_by_version_id = {} + for subset_id, in_data in asset_entity_data["subsets"].items(): + subsets.append(in_data["subset_entity"]) + version_data = in_data["version"] + version_entity = version_data["version_entity"] + version_by_subset_id[subset_id] = version_entity + repres_by_version_id[version_entity["_id"]] = version_data["repres"] + + if not subsets: + log.warning("There are not subsets for asset {0}".format( + asset_entity["name"] + )) + return + + subsets_by_family = collections.defaultdict(list) + for subset in subsets: + family = subset["data"].get("family") + if not family: + families = subset["data"].get("families") + if not families: + continue + family = families[0] + + subsets_by_family[family].append(subset) + + valid_subsets_by_id = {} + variants_per_subset_id = {} + for family, subsets in subsets_by_family.items(): + family_low = family.lower() + for variant in valid_variants: + # Family filtering + variant_families = variant.get("families") or [] + if not variant_families: + continue + + variant_families_low = [fam.lower() for fam in variant_families] + if family_low not in variant_families_low: + continue + + # Regex filtering (optional) + variant_regexes = variant.get("subset_filters") + for subset in subsets: + if variant_regexes: + valid = False + for pattern in variant_regexes: + if re.match(pattern, subset["name"]): + valid = True + break + + if not valid: + continue + + subset_id = subset["_id"] + valid_subsets_by_id[subset_id] = subset + variants_per_subset_id[subset_id] = variant + + # break variants loop if got here + break + + if not valid_subsets_by_id: + log.warning("There are not valid subsets.") + return + + log.debug("Valid subsets: {}".format(valid_subsets_by_id.values())) + + valid_repres_by_subset_id = collections.defaultdict(list) + for subset_id, subset_entity in valid_subsets_by_id.items(): + variant = variants_per_subset_id[subset_id] + variant_repre_names = variant.get("repre_names") + if not variant_repre_names: + continue + + # Lower names + variant_repre_names = [name.lower() for name in variant_repre_names] + + version_entity = version_by_subset_id[subset_id] + version_id = version_entity["_id"] + repres = repres_by_version_id[version_id] + for repre in repres: + repre_name_low = repre["name"].lower() + if repre_name_low in variant_repre_names: + valid_repres_by_subset_id[subset_id].append(repre) + + # DEBUG message + msg = "Valid representations for Asset: `{}`".format(asset_entity["name"]) + for subset_id, repres in valid_repres_by_subset_id.items(): + subset = valid_subsets_by_id[subset_id] + msg += "\n# Subset Name/ID: `{}`/{}".format(subset["name"], subset_id) + for repre in repres: + msg += "\n## Repre name: `{}`".format(repre["name"]) + + log.debug(msg) + + loaded_containers = { + "asset_entity": asset_entity, + "containers": [] + } + + for subset_id, repres in valid_repres_by_subset_id.items(): + subset = valid_subsets_by_id[subset_id] + subset_name = subset["name"] + + variant = variants_per_subset_id[subset_id] + + variant_loader_names = variant["loaders"] + variant_loader_count = len(variant_loader_names) + + variant_repre_names = variant["repre_names"] + variant_repre_count = len(variant_repre_names) + + is_loaded = False + for repre_name_idx, variant_repre_name in enumerate( + variant_repre_names + ): + found_repre = None + for repre in repres: + repre_name = repre["name"] + if repre_name == variant_repre_name: + found_repre = repre + break + + if not found_repre: + continue + + for loader_idx, loader_name in enumerate(variant_loader_names): + if is_loaded: + break + + loader = loaders_by_name.get(loader_name) + if not loader: + continue + try: + container = avalon.api.load( + loader, + found_repre["_id"], + name=subset_name + ) + loaded_containers["containers"].append(container) + is_loaded = True + + except Exception as exc: + if exc == pipeline.IncompatibleLoaderError: + log.info(( + "Loader `{}` is not compatible with" + " representation `{}`" + ).format(loader_name, repre["name"])) + + else: + log.error( + "Unexpected error happened during loading", + exc_info=True + ) + + msg = "Loading failed." + if loader_idx < (variant_loader_count - 1): + msg += " Trying next loader." + elif repre_name_idx < (variant_repre_count - 1): + msg += ( + " Loading of subset `{}` was not successful." + ).format(subset_name) + else: + msg += " Trying next representation." + log.info(msg) + + if is_loaded: + break + + return loaded_containers + + def get_link_assets(asset_entity): """Return linked assets for `asset_entity`.""" # TODO implement From 808510d856fb5ba4562630b9673dcff630b4b88f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:39:12 +0100 Subject: [PATCH 48/86] implemented load_containers_for_workfile which loads and trigger loading by current context --- pype/lib.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 30c222ac57..f37da5096a 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -980,3 +980,138 @@ def collect_last_version_repres(asset_entities): ) return output + + +def load_containers_for_workfile(): + """Load containers for (first) workfile. + + Loads latest versions of current and linked assets to workfile by logic + stored in Workfile variants from presets. Variants are set by host, + filtered by current task name and used by families. + + Each family can specify representation names and loaders for + representations and first available and successful loaded representation is + returned as container. + + At the end you'll get list of loaded containers per each asset. + + loaded_containers [{ + "asset_entity": , + "containers": [, , ...] + }, { + "asset_entity": , + "containers": [, ...] + }, { + ... + }] + """ + io.install() + + # Get current asset name and entity + current_asset_name = io.Session["AVALON_ASSET"] + current_asset_entity = io.find_one({ + "type": "asset", + "name": current_asset_name + }) + + # Skip if asset was not found + if not current_asset_entity: + print("Asset entity with name `{}` was not found".format( + current_asset_name + )) + return + + # Prepare available loaders + loaders_by_name = {} + for loader in avalon.api.discover(avalon.api.Loader): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError("Duplicated loader name {0}!".format(loader_name)) + loaders_by_name[loader_name] = loader + + # Skip if there are any loaders + if not loaders_by_name: + print("There are not registered loaders.") + return + + # Get current task name + current_task_name = os.environ["AVALON_TASK"] + current_task_name_low = current_task_name.lower() + # Load workfile presets for task + workfile_presets = get_workfile_build_presets(current_task_name_low) + + # Skip if there are any presets for task + if not workfile_presets: + log.warning( + "For current task `{}` is not set any loading preset.".format( + current_task_name + ) + ) + return + + # Get presets for loading current asset + current_context = workfile_presets.get("current_context") + # Get presets for loading linked assets + link_context = workfile_presets.get("linked_assets") + # Skip if both are missing + if not current_context and not link_context: + log.warning("Current task `{}` has empty loading preset.".format( + current_task_name + )) + return + + elif not current_context: + log.warning(( + "Current task `{}` don't have set loading preset for it's context." + ).format(current_task_name)) + + elif not link_context: + log.warning(( + "Current task `{}` don't have set " + "loading preset for it's linked assets." + ).format(current_task_name)) + + # Prepare assets to process by workfile presets + assets = [] + current_asset_id = None + if current_context: + # Add current asset entity if preset has current context set + assets.append(current_asset_entity) + current_asset_id = current_asset_entity["_id"] + + if link_context: + # Find and append linked assets if preset has set linked mapping + link_assets = get_link_assets(current_asset_entity) + if link_assets: + assets.extend(link_assets) + + # Skip if there are any assets + # - this may happend if only linked mapping is set and there are not links + if not assets: + log.warning("Asset does not have linked assets. Nothing to process.") + return + + # Prepare entities from database for assets + prepared_entities = collect_last_version_repres(assets) + + # Load containers by prepared entities and presets + loaded_containers = [] + # - Current asset containers + if current_asset_id and current_asset_id in prepared_entities: + current_context_data = prepared_entities.pop(current_asset_id) + loaded_data = load_containers_by_asset_data( + current_context_data, current_context, loaders_by_name + ) + if loaded_data: + loaded_containers.append(loaded_data) + + # - Linked assets container + for linked_asset_data in prepared_entities.values(): + loaded_data = load_containers_by_asset_data( + linked_asset_data, link_context, loaders_by_name + ) + if loaded_data: + loaded_containers.append(loaded_data) + + # Return list of loaded containers + return loaded_containers From 149a65f1e7f5c371c3f51bfc06932d05b6bcd60a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:39:36 +0100 Subject: [PATCH 49/86] implemented methods for creating first version workfile --- pype/lib.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index f37da5096a..4e7d88947d 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1115,3 +1115,126 @@ def load_containers_for_workfile(): # Return list of loaded containers return loaded_containers + + +def get_last_workfile_path(root, template, file_ext): + template = re.sub("<.*?>", ".*?", template) + template = re.sub("{version.*}", "([0-9]+)", template) + template = re.sub("{comment.*?}", ".+?", template) + # template = pipeline._format_work_template(template) + template = "^" + template + "$" + + all_file_names = [] + if os.path.exists(root): + all_file_names = os.listdir(root) + + filtered_file_names = [ + file_name for file_name in all_file_names + if os.path.splitext(file_name)[1] == file_ext + ] + + kwargs = {} + if platform.system() == "Windows": + kwargs["flags"] = re.IGNORECASE + + version = None + last_file_name = None + for file_name in sorted(filtered_file_names): + match = re.match(template, file_name, **kwargs) + if not match: + continue + + file_version = int(match.group(1)) + if file_version >= version: + last_file_name = file_name + version = file_version + 1 + + last_file_path = None + if last_file_name: + last_file_path = os.path.join(root, last_file_name) + + return last_file_path + + +def create_first_workfile(file_ext=None): + """Builds first workfile and load containers for it. + + :param file_ext: Work file extension may be specified otherwise first + extension in host's registered extensions is used. + :type file_ext: str + :return: Workfile path and loaded containers by Asset entity + :rtype: tuple + """ + # Get host + host = avalon.api.registered_host() + + # Workfile extension + if file_ext is None: + if not host.file_extensions(): + raise AssertionError( + "Host doesn't have set file extensions. Can't create workfile." + ) + file_ext = host.file_extensions()[0] + + workfile_root = host.work_root(io.Session) + + # make sure extension has dot + if not file_ext.startswith("."): + file_ext = ".{}".format(file_ext) + + # Create new workfile + project_doc = io.find_one({"type": "project"}) + asset_name = io.Session["AVALON_ASSET"] + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) + if not asset_doc: + raise AssertionError( + "Asset with name `{}` was not found.".format(asset_name) + ) + + root = avalon.api.registered_root() + template = project_doc["config"]["template"]["workfile"] + file_path = get_last_workfile_path(root, template, file_ext) + # TODO what should do if already exists? + # 1.) create new + # 2.) override + # 3.) raise exception + if file_path is not None: + log.warning("There already exist workfile `{}`.".format(file_path)) + return file_path + + hierarchy = "" + parents = asset_doc["data"].get("parents") + if parents: + hierarchy = "/".join(parents) + + # Use same data as Workfiles tool + template_data = { + "root": root, + "project": { + "name": project_doc["name"], + "code": project_doc["data"].get("code") + }, + "asset": asset_name, + "task": io.Session["AVALON_TASK"], + "hierarchy": hierarchy, + "version": 1, + "user": getpass.getuser(), + "ext": file_ext + } + + # Use same template as in Workfiles Tool + template_filled = pipeline.format_template_with_optional_keys( + template_data, template + ) + + # make sure filled template does not have more dots due to extension + while ".." in template_filled: + template_filled = template_filled.replace("..", ".") + + workfile_path = os.path.join(workfile_root, template_filled) + host.save_file(workfile_path) + + return workfile_path From d38df643a837ecc47c86362421ae85ee7c635e4f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:39:57 +0100 Subject: [PATCH 50/86] added build first workfile (probably for testing) --- pype/lib.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 4e7d88947d..0698e2bbba 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1238,3 +1238,12 @@ def create_first_workfile(file_ext=None): host.save_file(workfile_path) return workfile_path + + +def build_first_workfile(file_ext=None): + # DEBUG this should probably be host specific + # Create workfile + workfile_path = create_first_workfile(file_ext) + # Load containers + loaded_containers = load_containers_for_workfile() + return (workfile_path, loaded_containers) From 4428ccc04a286e9a4d3028ac90340ffe662c0cbc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:40:18 +0100 Subject: [PATCH 51/86] added build first workfile to maya menu --- pype/maya/menu.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/pype/maya/menu.py b/pype/maya/menu.py index 806944c117..efe338b4af 100644 --- a/pype/maya/menu.py +++ b/pype/maya/menu.py @@ -3,7 +3,9 @@ import os import logging from avalon.vendor.Qt import QtWidgets, QtCore, QtGui - +from avalon.maya import pipeline +import avalon.api +from ..lib import build_first_workfile import maya.cmds as cmds self = sys.modules[__name__] @@ -21,8 +23,15 @@ def _get_menu(): return menu - def deferred(): + def add_build_workfiles_item(): + # Add build first workfile + cmds.menuItem(divider=True, parent=pipeline._menu) + cmds.menuItem( + "Build First Workfile", + parent=pipeline._menu, + command=lambda *args: build_first_workfile() + ) log.info("Attempting to install scripts menu..") @@ -30,8 +39,11 @@ def deferred(): import scriptsmenu.launchformaya as launchformaya import scriptsmenu.scriptsmenu as scriptsmenu except ImportError: - log.warning("Skipping studio.menu install, because " - "'scriptsmenu' module seems unavailable.") + log.warning( + "Skipping studio.menu install, because " + "'scriptsmenu' module seems unavailable." + ) + add_build_workfiles_item() return # load configuration of custom menu @@ -39,15 +51,16 @@ def deferred(): config = scriptsmenu.load_configuration(config_path) # run the launcher for Maya menu - studio_menu = launchformaya.main(title=self._menu.title(), - objectName=self._menu) + studio_menu = launchformaya.main( + title=self._menu.title(), + objectName=self._menu + ) # apply configuration studio_menu.build_from_configuration(studio_menu, config) def uninstall(): - menu = _get_menu() if menu: log.info("Attempting to uninstall..") @@ -60,9 +73,8 @@ def uninstall(): def install(): - if cmds.about(batch=True): - print("Skipping pype.menu initialization in batch mode..") + log.info("Skipping pype.menu initialization in batch mode..") return uninstall() From c11092252f2b455fb4a7922d6661a46b08f9c74d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:45:55 +0100 Subject: [PATCH 52/86] fixed not existing variable --- pype/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index 0698e2bbba..d39902f5cf 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -884,7 +884,7 @@ def load_containers_by_asset_data( def get_link_assets(asset_entity): """Return linked assets for `asset_entity`.""" # TODO implement - return [test_asset] + return [] def collect_last_version_repres(asset_entities): From 7d0d0f4270c8dd0f0e62ad0a63033e3d4ea0dc75 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Mar 2020 17:48:07 +0100 Subject: [PATCH 53/86] renamed key `subset_filters` to `subset_name_filters` --- pype/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index d39902f5cf..0264653f0f 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -751,7 +751,7 @@ def load_containers_by_asset_data( continue # Regex filtering (optional) - variant_regexes = variant.get("subset_filters") + variant_regexes = variant.get("subset_name_filters") for subset in subsets: if variant_regexes: valid = False From 9a8655be1da5a8939a34cf66f71a036b13141fb2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 14 Mar 2020 00:41:53 +0100 Subject: [PATCH 54/86] publishing unreal static mesh from maya --- pype/hooks/unreal/unreal_prelaunch.py | 2 +- .../global/publish/collect_scene_version.py | 3 + pype/plugins/global/publish/integrate_new.py | 3 +- .../maya/create/create_unreal_staticmesh.py | 11 ++ .../maya/publish/collect_unreal_staticmesh.py | 33 ++++ pype/plugins/maya/publish/extract_fbx.py | 5 +- .../validate_unreal_mesh_triangulated.py | 33 ++++ .../validate_unreal_staticmesh_naming.py | 120 ++++++++++++++ .../maya/publish/validate_unreal_up_axis.py | 25 +++ pype/plugins/unreal/create/create_fbx.py | 14 ++ .../plugins/unreal/load/load_staticmeshfbx.py | 53 ++++++ .../unreal/publish/collect_instances.py | 152 ++++++++++++++++++ pype/unreal/__init__.py | 45 ++++++ pype/unreal/plugin.py | 9 ++ 14 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 pype/plugins/maya/create/create_unreal_staticmesh.py create mode 100644 pype/plugins/maya/publish/collect_unreal_staticmesh.py create mode 100644 pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py create mode 100644 pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py create mode 100644 pype/plugins/maya/publish/validate_unreal_up_axis.py create mode 100644 pype/plugins/unreal/create/create_fbx.py create mode 100644 pype/plugins/unreal/load/load_staticmeshfbx.py create mode 100644 pype/plugins/unreal/publish/collect_instances.py create mode 100644 pype/unreal/plugin.py diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index efb5d9157b..5b6b8e08e0 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -36,7 +36,7 @@ class UnrealPrelaunch(PypeHook): # Unreal is sensitive about project names longer then 20 chars if len(project_name) > 20: self.log.warning((f"Project name exceed 20 characters " - f"[ {project_name} ]!")) + f"({project_name})!")) # Unreal doesn't accept non alphabet characters at the start # of the project name. This is because project name is then used diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 02e913199b..314a64f550 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -16,6 +16,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if "standalonepublisher" in context.data.get("host", []): return + if "unreal" in context.data.get("host", []): + return + filename = os.path.basename(context.data.get('currentFile')) if '' in filename: diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 1d061af173..8935127e9e 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -80,7 +80,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "matchmove", "image" "source", - "assembly" + "assembly", + "fbx" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/pype/plugins/maya/create/create_unreal_staticmesh.py b/pype/plugins/maya/create/create_unreal_staticmesh.py new file mode 100644 index 0000000000..5a74cb22d5 --- /dev/null +++ b/pype/plugins/maya/create/create_unreal_staticmesh.py @@ -0,0 +1,11 @@ +import avalon.maya + + +class CreateUnrealStaticMesh(avalon.maya.Creator): + name = "staticMeshMain" + label = "Unreal - Static Mesh" + family = "unrealStaticMesh" + icon = "cube" + + def __init__(self, *args, **kwargs): + super(CreateUnrealStaticMesh, self).__init__(*args, **kwargs) diff --git a/pype/plugins/maya/publish/collect_unreal_staticmesh.py b/pype/plugins/maya/publish/collect_unreal_staticmesh.py new file mode 100644 index 0000000000..5ab9643f4b --- /dev/null +++ b/pype/plugins/maya/publish/collect_unreal_staticmesh.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from maya import cmds +import pyblish.api + + +class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): + """Collect unreal static mesh + + Ensures always only a single frame is extracted (current frame). This + also sets correct FBX options for later extraction. + + Note: + This is a workaround so that the `pype.model` family can use the + same pointcache extractor implementation as animation and pointcaches. + This always enforces the "current" frame to be published. + + """ + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Model Data" + families = ["unrealStaticMesh"] + + def process(self, instance): + # add fbx family to trigger fbx extractor + instance.data["families"].append("fbx") + # set fbx overrides on instance + instance.data["smoothingGroups"] = True + instance.data["smoothMesh"] = True + instance.data["triangulate"] = True + + frame = cmds.currentTime(query=True) + instance.data["frameStart"] = frame + instance.data["frameEnd"] = frame diff --git a/pype/plugins/maya/publish/extract_fbx.py b/pype/plugins/maya/publish/extract_fbx.py index 01b58241c2..6a75bfce0e 100644 --- a/pype/plugins/maya/publish/extract_fbx.py +++ b/pype/plugins/maya/publish/extract_fbx.py @@ -212,12 +212,11 @@ class ExtractFBX(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'mov', - 'ext': 'mov', + 'name': 'fbx', + 'ext': 'fbx', 'files': filename, "stagingDir": stagingDir, } instance.data["representations"].append(representation) - self.log.info("Extract FBX successful to: {0}".format(path)) diff --git a/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py b/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py new file mode 100644 index 0000000000..77f7144c4e --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api + + +class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): + """Validate if mesh is made of triangles for Unreal Engine""" + + order = pype.api.ValidateMeshOder + hosts = ["maya"] + families = ["unrealStaticMesh"] + category = "geometry" + label = "Mesh is Triangulated" + actions = [pype.maya.action.SelectInvalidAction] + + @classmethod + def get_invalid(cls, instance): + invalid = [] + meshes = cmds.ls(instance, type="mesh", long=True) + for mesh in meshes: + faces = cmds.polyEvaluate(mesh, f=True) + tris = cmds.polyEvaluate(mesh, t=True) + if faces != tris: + invalid.append(mesh) + + return invalid + + def process(self, instance): + invalid = self.get_invalid(instance) + assert len(invalid) == 0, ( + "Found meshes without triangles") diff --git a/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py b/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py new file mode 100644 index 0000000000..b62a855da9 --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api +import pype.maya.action +import re + + +class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin): + """Validate name of Unreal Static Mesh + + Unreals naming convention states that staticMesh sould start with `SM` + prefix - SM_[Name]_## (Eg. SM_sube_01). This plugin also validates other + types of meshes - collision meshes: + + UBX_[RenderMeshName]_##: + Boxes are created with the Box objects type in + Max or with the Cube polygonal primitive in Maya. + You cannot move the vertices around or deform it + in any way to make it something other than a + rectangular prism, or else it will not work. + + UCP_[RenderMeshName]_##: + Capsules are created with the Capsule object type. + The capsule does not need to have many segments + (8 is a good number) at all because it is + converted into a true capsule for collision. Like + boxes, you should not move the individual + vertices around. + + USP_[RenderMeshName]_##: + Spheres are created with the Sphere object type. + The sphere does not need to have many segments + (8 is a good number) at all because it is + converted into a true sphere for collision. Like + boxes, you should not move the individual + vertices around. + + UCX_[RenderMeshName]_##: + Convex objects can be any completely closed + convex 3D shape. For example, a box can also be + a convex object + + This validator also checks if collision mesh [RenderMeshName] matches one + of SM_[RenderMeshName]. + + """ + optional = True + order = pype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["unrealStaticMesh"] + label = "Unreal StaticMesh Name" + actions = [pype.maya.action.SelectInvalidAction] + regex_mesh = r"SM_(?P.*)_(\d{2})" + regex_collision = r"((UBX)|(UCP)|(USP)|(UCX))_(?P.*)_(\d{2})" + + @classmethod + def get_invalid(cls, instance): + + # find out if supplied transform is group or not + def is_group(groupName): + try: + children = cmds.listRelatives(groupName, children=True) + for child in children: + if not cmds.ls(child, transforms=True): + return False + return True + except Exception: + return False + + invalid = [] + content_instance = instance.data.get("setMembers", None) + if not content_instance: + cls.log.error("Instance has no nodes!") + return True + pass + descendants = cmds.listRelatives(content_instance, + allDescendents=True, + fullPath=True) or [] + + descendants = cmds.ls(descendants, noIntermediate=True, long=True) + trns = cmds.ls(descendants, long=False, type=('transform')) + + # filter out groups + filter = [node for node in trns if not is_group(node)] + + # compile regex for testing names + sm_r = re.compile(cls.regex_mesh) + cl_r = re.compile(cls.regex_collision) + + sm_names = [] + col_names = [] + for obj in filter: + sm_m = sm_r.match(obj) + if sm_m is None: + # test if it matches collision mesh + cl_r = sm_r.match(obj) + if cl_r is None: + cls.log.error("invalid mesh name on: {}".format(obj)) + invalid.append(obj) + else: + col_names.append((cl_r.group("renderName"), obj)) + else: + sm_names.append(sm_m.group("renderName")) + + for c_mesh in col_names: + if c_mesh[0] not in sm_names: + cls.log.error(("collision name {} doesn't match any " + "static mesh names.").format(obj)) + invalid.append(c_mesh[1]) + + return invalid + + def process(self, instance): + + invalid = self.get_invalid(instance) + + if invalid: + raise RuntimeError("Model naming is invalid. See log.") diff --git a/pype/plugins/maya/publish/validate_unreal_up_axis.py b/pype/plugins/maya/publish/validate_unreal_up_axis.py new file mode 100644 index 0000000000..6641edb4a5 --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_up_axis.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api + + +class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): + """Validate if Z is set as up axis in Maya""" + + optional = True + order = pype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["unrealStaticMesh"] + label = "Unreal Up-Axis check" + actions = [pype.api.RepairAction] + + def process(self, context): + assert cmds.upAxis(q=True, axis=True) == "z", ( + "Invalid axis set as up axis" + ) + + @classmethod + def repair(cls, instance): + cmds.upAxis(axis="z", rotateView=True) diff --git a/pype/plugins/unreal/create/create_fbx.py b/pype/plugins/unreal/create/create_fbx.py new file mode 100644 index 0000000000..0d5b0bf316 --- /dev/null +++ b/pype/plugins/unreal/create/create_fbx.py @@ -0,0 +1,14 @@ +from pype.unreal.plugin import Creator + + +class CreateFbx(Creator): + """Static FBX geometry""" + + name = "modelMain" + label = "Model" + family = "model" + icon = "cube" + asset_types = ["StaticMesh"] + + def __init__(self, *args, **kwargs): + super(CreateFbx, self).__init__(*args, **kwargs) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py new file mode 100644 index 0000000000..056c81d54d --- /dev/null +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -0,0 +1,53 @@ +from avalon import api +from avalon import unreal as avalon_unreal +import unreal +import time + + +class StaticMeshFBXLoader(api.Loader): + """Load Unreal StaticMesh from FBX""" + + families = ["unrealStaticMesh"] + label = "Import FBX Static Mesh" + representations = ["fbx"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, data): + + tools = unreal.AssetToolsHelpers().get_asset_tools() + temp_dir, temp_name = tools.create_unique_asset_name( + "/Game/{}".format(name), "_TMP" + ) + + # asset_path = "/Game/{}".format(namespace) + unreal.EditorAssetLibrary.make_directory(temp_dir) + + task = unreal.AssetImportTask() + + task.filename = self.fname + task.destination_path = temp_dir + task.destination_name = name + task.replace_existing = False + task.automated = True + task.save = True + + # set import options here + task.options = unreal.FbxImportUI() + task.options.import_animations = False + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + + imported_assets = unreal.EditorAssetLibrary.list_assets( + temp_dir, recursive=True, include_folder=True + ) + new_dir = avalon_unreal.containerise( + name, namespace, imported_assets, context, self.__class__.__name__) + + asset_content = unreal.EditorAssetLibrary.list_assets( + new_dir, recursive=True, include_folder=True + ) + + unreal.EditorAssetLibrary.delete_directory(temp_dir) + + return asset_content diff --git a/pype/plugins/unreal/publish/collect_instances.py b/pype/plugins/unreal/publish/collect_instances.py new file mode 100644 index 0000000000..fa604f79d3 --- /dev/null +++ b/pype/plugins/unreal/publish/collect_instances.py @@ -0,0 +1,152 @@ +import unreal + +import pyblish.api + + +class CollectInstances(pyblish.api.ContextPlugin): + """Gather instances by objectSet and pre-defined attribute + + This collector takes into account assets that are associated with + an objectSet and marked with a unique identifier; + + Identifier: + id (str): "pyblish.avalon.instance" + + Limitations: + - Does not take into account nodes connected to those + within an objectSet. Extractors are assumed to export + with history preserved, but this limits what they will + be able to achieve and the amount of data available + to validators. An additional collector could also + append this input data into the instance, as we do + for `pype.rig` with collect_history. + + """ + + label = "Collect Instances" + order = pyblish.api.CollectorOrder + hosts = ["unreal"] + + def process(self, context): + + objectset = cmds.ls("*.id", long=True, type="objectSet", + recursive=True, objectsOnly=True) + + context.data['objectsets'] = objectset + for objset in objectset: + + if not cmds.attributeQuery("id", node=objset, exists=True): + continue + + id_attr = "{}.id".format(objset) + if cmds.getAttr(id_attr) != "pyblish.avalon.instance": + continue + + # The developer is responsible for specifying + # the family of each instance. + has_family = cmds.attributeQuery("family", + node=objset, + exists=True) + assert has_family, "\"%s\" was missing a family" % objset + + members = cmds.sets(objset, query=True) + if members is None: + self.log.warning("Skipped empty instance: \"%s\" " % objset) + continue + + self.log.info("Creating instance for {}".format(objset)) + + data = dict() + + # Apply each user defined attribute as data + for attr in cmds.listAttr(objset, userDefined=True) or list(): + try: + value = cmds.getAttr("%s.%s" % (objset, attr)) + except Exception: + # Some attributes cannot be read directly, + # such as mesh and color attributes. These + # are considered non-essential to this + # particular publishing pipeline. + value = None + data[attr] = value + + # temporarily translation of `active` to `publish` till issue has + # been resolved, https://github.com/pyblish/pyblish-base/issues/307 + if "active" in data: + data["publish"] = data["active"] + + # Collect members + members = cmds.ls(members, long=True) or [] + + # `maya.cmds.listRelatives(noIntermediate=True)` only works when + # `shapes=True` argument is passed, since we also want to include + # transforms we filter afterwards. + children = cmds.listRelatives(members, + allDescendents=True, + fullPath=True) or [] + children = cmds.ls(children, noIntermediate=True, long=True) + + parents = [] + if data.get("includeParentHierarchy", True): + # If `includeParentHierarchy` then include the parents + # so they will also be picked up in the instance by validators + parents = self.get_all_parents(members) + members_hierarchy = list(set(members + children + parents)) + + if 'families' not in data: + data['families'] = [data.get('family')] + + # Create the instance + instance = context.create_instance(objset) + instance[:] = members_hierarchy + + # Store the exact members of the object set + instance.data["setMembers"] = members + + + # Define nice label + name = cmds.ls(objset, long=False)[0] # use short name + label = "{0} ({1})".format(name, + data["asset"]) + + # Append start frame and end frame to label if present + if "frameStart" and "frameEnd" in data: + label += " [{0}-{1}]".format(int(data["frameStart"]), + int(data["frameEnd"])) + + instance.data["label"] = label + + instance.data.update(data) + + # Produce diagnostic message for any graphical + # user interface interested in visualising it. + self.log.info("Found: \"%s\" " % instance.data["name"]) + self.log.debug("DATA: \"%s\" " % instance.data) + + + def sort_by_family(instance): + """Sort by family""" + return instance.data.get("families", instance.data.get("family")) + + # Sort/grouped by family (preserving local index) + context[:] = sorted(context, key=sort_by_family) + + return context + + def get_all_parents(self, nodes): + """Get all parents by using string operations (optimization) + + Args: + nodes (list): the nodes which are found in the objectSet + + Returns: + list + """ + + parents = [] + for node in nodes: + splitted = node.split("|") + items = ["|".join(splitted[0:i]) for i in range(2, len(splitted))] + parents.extend(items) + + return list(set(parents)) diff --git a/pype/unreal/__init__.py b/pype/unreal/__init__.py index e69de29bb2..bb8a765a43 100644 --- a/pype/unreal/__init__.py +++ b/pype/unreal/__init__.py @@ -0,0 +1,45 @@ +import os +import logging + +from avalon import api as avalon +from pyblish import api as pyblish + +logger = logging.getLogger("pype.unreal") + +PARENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(PARENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "unreal", "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "unreal", "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "unreal", "create") + + +def install(): + """Install Unreal configuration for Avalon.""" + print("-=" * 40) + logo = '''. +. + ____________ + / \\ __ \\ + \\ \\ \\/_\\ \\ + \\ \\ _____/ ______ + \\ \\ \\___// \\ \\ + \\ \\____\\ \\ \\_____\\ + \\/_____/ \\/______/ PYPE Club . +. +''' + print(logo) + print("installing Pype for Unreal ...") + print("-=" * 40) + logger.info("installing Pype for Unreal") + pyblish.register_plugin_path(str(PUBLISH_PATH)) + avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH)) + avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH)) + + +def uninstall(): + """Uninstall Unreal configuration for Avalon.""" + pyblish.deregister_plugin_path(str(PUBLISH_PATH)) + avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH)) + avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH)) diff --git a/pype/unreal/plugin.py b/pype/unreal/plugin.py new file mode 100644 index 0000000000..d403417ad1 --- /dev/null +++ b/pype/unreal/plugin.py @@ -0,0 +1,9 @@ +from avalon import api + + +class Creator(api.Creator): + pass + + +class Loader(api.Loader): + pass From ada2cc0f4e9b6a351c9cb9ae6b1d56ed1d37e631 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 16 Mar 2020 21:43:47 +0100 Subject: [PATCH 55/86] base of unreal staticMesh pipeline --- .../global/publish/collect_scene_version.py | 2 +- pype/plugins/unreal/create/create_fbx.py | 14 -- .../unreal/create/create_staticmeshfbx.py | 33 ++++ .../plugins/unreal/load/load_staticmeshfbx.py | 52 +++++- .../unreal/publish/collect_instances.py | 149 ++++-------------- 5 files changed, 112 insertions(+), 138 deletions(-) delete mode 100644 pype/plugins/unreal/create/create_fbx.py create mode 100644 pype/plugins/unreal/create/create_staticmeshfbx.py diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 314a64f550..8c2bacf6e1 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -16,7 +16,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if "standalonepublisher" in context.data.get("host", []): return - if "unreal" in context.data.get("host", []): + if "unreal" in pyblish.api.registered_hosts(): return filename = os.path.basename(context.data.get('currentFile')) diff --git a/pype/plugins/unreal/create/create_fbx.py b/pype/plugins/unreal/create/create_fbx.py deleted file mode 100644 index 0d5b0bf316..0000000000 --- a/pype/plugins/unreal/create/create_fbx.py +++ /dev/null @@ -1,14 +0,0 @@ -from pype.unreal.plugin import Creator - - -class CreateFbx(Creator): - """Static FBX geometry""" - - name = "modelMain" - label = "Model" - family = "model" - icon = "cube" - asset_types = ["StaticMesh"] - - def __init__(self, *args, **kwargs): - super(CreateFbx, self).__init__(*args, **kwargs) diff --git a/pype/plugins/unreal/create/create_staticmeshfbx.py b/pype/plugins/unreal/create/create_staticmeshfbx.py new file mode 100644 index 0000000000..8002299f0a --- /dev/null +++ b/pype/plugins/unreal/create/create_staticmeshfbx.py @@ -0,0 +1,33 @@ +import unreal +from pype.unreal.plugin import Creator +from avalon.unreal import ( + instantiate, +) + + +class CreateStaticMeshFBX(Creator): + """Static FBX geometry""" + + name = "unrealStaticMeshMain" + label = "Unreal - Static Mesh" + family = "unrealStaticMesh" + icon = "cube" + asset_types = ["StaticMesh"] + + root = "/Game" + suffix = "_INS" + + def __init__(self, *args, **kwargs): + super(CreateStaticMeshFBX, self).__init__(*args, **kwargs) + + def process(self): + + name = self.data["subset"] + + selection = [] + if (self.options or {}).get("useSelection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + unreal.log("selection: {}".format(selection)) + instantiate(self.root, name, self.data, selection, self.suffix) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py index 056c81d54d..61e765f7c2 100644 --- a/pype/plugins/unreal/load/load_staticmeshfbx.py +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -1,7 +1,6 @@ from avalon import api from avalon import unreal as avalon_unreal import unreal -import time class StaticMeshFBXLoader(api.Loader): @@ -14,13 +13,33 @@ class StaticMeshFBXLoader(api.Loader): color = "orange" def load(self, context, name, namespace, data): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ tools = unreal.AssetToolsHelpers().get_asset_tools() temp_dir, temp_name = tools.create_unique_asset_name( "/Game/{}".format(name), "_TMP" ) - # asset_path = "/Game/{}".format(namespace) unreal.EditorAssetLibrary.make_directory(temp_dir) task = unreal.AssetImportTask() @@ -51,3 +70,32 @@ class StaticMeshFBXLoader(api.Loader): unreal.EditorAssetLibrary.delete_directory(temp_dir) return asset_content + + def update(self, container, representation): + node = container["objectName"] + source_path = api.get_representation_path(representation) + destination_path = container["namespace"] + + task = unreal.AssetImportTask() + + task.filename = source_path + task.destination_path = destination_path + # strip suffix + task.destination_name = node[:-4] + task.replace_existing = True + task.automated = True + task.save = True + + task.options = unreal.FbxImportUI() + task.options.import_animations = False + + # do import fbx and replace existing data + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + avalon_unreal.imprint( + container_path, {"_id": str(representation["_id"])}) + + def remove(self, container): + unreal.EditorAssetLibrary.delete_directory(container["namespace"]) diff --git a/pype/plugins/unreal/publish/collect_instances.py b/pype/plugins/unreal/publish/collect_instances.py index fa604f79d3..766a73028c 100644 --- a/pype/plugins/unreal/publish/collect_instances.py +++ b/pype/plugins/unreal/publish/collect_instances.py @@ -4,23 +4,14 @@ import pyblish.api class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by objectSet and pre-defined attribute + """Gather instances by AvalonPublishInstance class - This collector takes into account assets that are associated with - an objectSet and marked with a unique identifier; + This collector finds all paths containing `AvalonPublishInstance` class + asset Identifier: id (str): "pyblish.avalon.instance" - Limitations: - - Does not take into account nodes connected to those - within an objectSet. Extractors are assumed to export - with history preserved, but this limits what they will - be able to achieve and the amount of data available - to validators. An additional collector could also - append this input data into the instance, as we do - for `pype.rig` with collect_history. - """ label = "Collect Instances" @@ -29,124 +20,40 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): - objectset = cmds.ls("*.id", long=True, type="objectSet", - recursive=True, objectsOnly=True) + ar = unreal.AssetRegistryHelpers.get_asset_registry() + instance_containers = ar.get_assets_by_class( + "AvalonPublishInstance", True) - context.data['objectsets'] = objectset - for objset in objectset: + for container_data in instance_containers: + asset = container_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = container_data.asset_name + # convert to strings + data = {str(key): str(value) for (key, value) in data.items()} + assert data.get("family"), ( + "instance has no family" + ) - if not cmds.attributeQuery("id", node=objset, exists=True): - continue + # content of container + members = unreal.EditorAssetLibrary.list_assets( + asset.get_path_name(), recursive=True, include_folder=True + ) + self.log.debug(members) + self.log.debug(asset.get_path_name()) + # remove instance container + members.remove(asset.get_path_name()) + self.log.info("Creating instance for {}".format(asset.get_name())) - id_attr = "{}.id".format(objset) - if cmds.getAttr(id_attr) != "pyblish.avalon.instance": - continue - - # The developer is responsible for specifying - # the family of each instance. - has_family = cmds.attributeQuery("family", - node=objset, - exists=True) - assert has_family, "\"%s\" was missing a family" % objset - - members = cmds.sets(objset, query=True) - if members is None: - self.log.warning("Skipped empty instance: \"%s\" " % objset) - continue - - self.log.info("Creating instance for {}".format(objset)) - - data = dict() - - # Apply each user defined attribute as data - for attr in cmds.listAttr(objset, userDefined=True) or list(): - try: - value = cmds.getAttr("%s.%s" % (objset, attr)) - except Exception: - # Some attributes cannot be read directly, - # such as mesh and color attributes. These - # are considered non-essential to this - # particular publishing pipeline. - value = None - data[attr] = value - - # temporarily translation of `active` to `publish` till issue has - # been resolved, https://github.com/pyblish/pyblish-base/issues/307 - if "active" in data: - data["publish"] = data["active"] - - # Collect members - members = cmds.ls(members, long=True) or [] - - # `maya.cmds.listRelatives(noIntermediate=True)` only works when - # `shapes=True` argument is passed, since we also want to include - # transforms we filter afterwards. - children = cmds.listRelatives(members, - allDescendents=True, - fullPath=True) or [] - children = cmds.ls(children, noIntermediate=True, long=True) - - parents = [] - if data.get("includeParentHierarchy", True): - # If `includeParentHierarchy` then include the parents - # so they will also be picked up in the instance by validators - parents = self.get_all_parents(members) - members_hierarchy = list(set(members + children + parents)) - - if 'families' not in data: - data['families'] = [data.get('family')] - - # Create the instance - instance = context.create_instance(objset) - instance[:] = members_hierarchy + instance = context.create_instance(asset.get_name()) + instance[:] = members # Store the exact members of the object set instance.data["setMembers"] = members + instance.data["families"] = [data.get("family")] - - # Define nice label - name = cmds.ls(objset, long=False)[0] # use short name - label = "{0} ({1})".format(name, + label = "{0} ({1})".format(asset.get_name()[:-4], data["asset"]) - # Append start frame and end frame to label if present - if "frameStart" and "frameEnd" in data: - label += " [{0}-{1}]".format(int(data["frameStart"]), - int(data["frameEnd"])) - instance.data["label"] = label instance.data.update(data) - - # Produce diagnostic message for any graphical - # user interface interested in visualising it. - self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.debug("DATA: \"%s\" " % instance.data) - - - def sort_by_family(instance): - """Sort by family""" - return instance.data.get("families", instance.data.get("family")) - - # Sort/grouped by family (preserving local index) - context[:] = sorted(context, key=sort_by_family) - - return context - - def get_all_parents(self, nodes): - """Get all parents by using string operations (optimization) - - Args: - nodes (list): the nodes which are found in the objectSet - - Returns: - list - """ - - parents = [] - for node in nodes: - splitted = node.split("|") - items = ["|".join(splitted[0:i]) for i in range(2, len(splitted))] - parents.extend(items) - - return list(set(parents)) From a1a27784ae85f5c7da925059c4ff3f0b4ef66429 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 17 Mar 2020 00:01:35 +0100 Subject: [PATCH 56/86] updated docs --- pype/unreal/lib.py | 12 +++++++++++- pype/unreal/plugin.py | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index be6314b09b..faf3a6e99f 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -13,7 +13,17 @@ def get_engine_versions(): Location can be overridden by `UNREAL_ENGINE_LOCATION` environment variable. - Returns dictionary with version as a key and dir as value. + Returns: + + dict: dictionary with version as a key and dir as value. + + Example: + + >>> get_engine_version() + { + "4.23": "C:/Epic Games/UE_4.23", + "4.24": "C:/Epic Games/UE_4.24" + } """ try: engine_locations = {} diff --git a/pype/unreal/plugin.py b/pype/unreal/plugin.py index d403417ad1..0c00eb77d6 100644 --- a/pype/unreal/plugin.py +++ b/pype/unreal/plugin.py @@ -2,8 +2,10 @@ from avalon import api class Creator(api.Creator): + """This serves as skeleton for future Pype specific functionality""" pass class Loader(api.Loader): + """This serves as skeleton for future Pype specific functionality""" pass From 3bf653b006e336734cfecb421964cc2226e61717 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Mar 2020 11:25:13 +0100 Subject: [PATCH 57/86] fixed bug with cpp project setup --- pype/unreal/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index faf3a6e99f..8f87fdbf4e 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -257,7 +257,7 @@ def create_unreal_project(project_name: str, "pip", "install", "pyside"]) if dev_mode or preset["dev_mode"]: - _prepare_cpp_project(pr_dir, engine_path) + _prepare_cpp_project(project_file, engine_path) def _prepare_cpp_project(project_file: str, engine_path: str) -> None: From f80e4e123546de2cf40d2c1f0d5c488cc9055f25 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 11:13:54 +0100 Subject: [PATCH 58/86] clean(nuke): pep8 improvments --- pype/nuke/lib.py | 29 +++++++++++++++-------------- setup/nuke/nuke_path/menu.py | 10 +++++----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index ad2d576da3..ccb4f343db 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -28,7 +28,7 @@ self = sys.modules[__name__] self._project = None -def onScriptLoad(): +def on_script_load(): ''' Callback for ffmpeg support ''' if nuke.env['LINUX']: @@ -39,7 +39,7 @@ def onScriptLoad(): nuke.tcl('load movWriter') -def checkInventoryVersions(): +def check_inventory_versions(): """ Actiual version idetifier of Loaded containers @@ -180,8 +180,8 @@ def format_anatomy(data): padding = int(anatomy.templates['render']['padding']) except KeyError as e: msg = ("`padding` key is not in `render` " - "Anatomy template. Please, add it there and restart " - "the pipeline (padding: \"4\"): `{}`").format(e) + "Anatomy template. Please, add it there and restart " + "the pipeline (padding: \"4\"): `{}`").format(e) log.error(msg) nuke.message(msg) @@ -701,7 +701,8 @@ class WorkfileSettings(object): def set_reads_colorspace(self, reads): """ Setting colorspace to Read nodes - Looping trought all read nodes and tries to set colorspace based on regex rules in presets + Looping trought all read nodes and tries to set colorspace based + on regex rules in presets """ changes = dict() for n in nuke.allNodes(): @@ -873,10 +874,10 @@ class WorkfileSettings(object): if any(x for x in data.values() if x is None): msg = ("Missing set shot attributes in DB." - "\nContact your supervisor!." - "\n\nWidth: `{width}`" - "\nHeight: `{height}`" - "\nPixel Asspect: `{pixel_aspect}`").format(**data) + "\nContact your supervisor!." + "\n\nWidth: `{width}`" + "\nHeight: `{height}`" + "\nPixel Asspect: `{pixel_aspect}`").format(**data) log.error(msg) nuke.message(msg) @@ -895,8 +896,9 @@ class WorkfileSettings(object): ) except Exception as e: bbox = None - msg = ("{}:{} \nFormat:Crop need to be set with dots, example: " - "0.0.1920.1080, /nSetting to default").format(__name__, e) + msg = ("{}:{} \nFormat:Crop need to be set with dots, " + "example: 0.0.1920.1080, " + "/nSetting to default").format(__name__, e) log.error(msg) nuke.message(msg) @@ -1037,7 +1039,8 @@ class BuildWorkfile(WorkfileSettings): """ Building first version of workfile. - Settings are taken from presets and db. It will add all subsets in last version for defined representaions + Settings are taken from presets and db. It will add all subsets + in last version for defined representaions Arguments: variable (type): description @@ -1265,8 +1268,6 @@ class BuildWorkfile(WorkfileSettings): representation (dict): avalon db entity """ - context = representation["context"] - loader_name = "LoadLuts" loader_plugin = None diff --git a/setup/nuke/nuke_path/menu.py b/setup/nuke/nuke_path/menu.py index 15702fa364..be4f39b542 100644 --- a/setup/nuke/nuke_path/menu.py +++ b/setup/nuke/nuke_path/menu.py @@ -4,8 +4,8 @@ import KnobScripter from pype.nuke.lib import ( writes_version_sync, - onScriptLoad, - checkInventoryVersions + on_script_load, + check_inventory_versions ) import nuke @@ -15,9 +15,9 @@ log = Logger().get_logger(__name__, "nuke") # nuke.addOnScriptSave(writes_version_sync) -nuke.addOnScriptSave(onScriptLoad) -nuke.addOnScriptLoad(checkInventoryVersions) -nuke.addOnScriptSave(checkInventoryVersions) +nuke.addOnScriptSave(on_script_load) +nuke.addOnScriptLoad(check_inventory_versions) +nuke.addOnScriptSave(check_inventory_versions) # nuke.addOnScriptSave(writes_version_sync) log.info('Automatic syncing of write file knob to script version') From 3d0b7e49c7cadc849b3d165443e16923ae33309f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 25 Mar 2020 13:52:40 +0100 Subject: [PATCH 59/86] PEP fixes --- .flake8 | 2 +- pype/ftrack/lib/ftrack_app_handler.py | 7 +++---- pype/plugins/unreal/load/load_staticmeshfbx.py | 4 ++-- pype/unreal/lib.py | 10 +++++----- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.flake8 b/.flake8 index b04062ceab..f9c81de232 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] # ignore = D203 -ignore = BLK100, W504 +ignore = BLK100, W504, W503 max-line-length = 79 exclude = .git, diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 58c550b3dd..b5576ae046 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -277,7 +277,7 @@ class AppAction(BaseHandler): 'success': False, 'message': "Hook didn't finish successfully {0}" .format(self.label) - } + } if sys.platform == "win32": @@ -290,7 +290,7 @@ class AppAction(BaseHandler): # Run SW if was found executable if execfile is not None: - popen = avalonlib.launch( + avalonlib.launch( executable=execfile, args=[], environment=env ) else: @@ -298,8 +298,7 @@ class AppAction(BaseHandler): 'success': False, 'message': "We didn't found launcher for {0}" .format(self.label) - } - pass + } if sys.platform.startswith('linux'): execfile = os.path.join(path.strip('"'), self.executable) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py index 61e765f7c2..4c27f9aa92 100644 --- a/pype/plugins/unreal/load/load_staticmeshfbx.py +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -37,7 +37,7 @@ class StaticMeshFBXLoader(api.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() temp_dir, temp_name = tools.create_unique_asset_name( - "/Game/{}".format(name), "_TMP" + "/Game/{}".format(name), "_TMP" ) unreal.EditorAssetLibrary.make_directory(temp_dir) @@ -95,7 +95,7 @@ class StaticMeshFBXLoader(api.Loader): container["objectName"]) # update metadata avalon_unreal.imprint( - container_path, {"_id": str(representation["_id"])}) + container_path, {"_id": str(representation["_id"])}) def remove(self, container): unreal.EditorAssetLibrary.delete_directory(container["namespace"]) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 8f87fdbf4e..0b049c8b1d 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -214,11 +214,11 @@ def create_unreal_project(project_name: str, # sources at start data["Modules"] = [{ - "Name": project_name, - "Type": "Runtime", - "LoadingPhase": "Default", - "AdditionalDependencies": ["Engine"], - }] + "Name": project_name, + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": ["Engine"], + }] if preset["install_unreal_python_engine"]: # now we need to fix python path in: From 599a227359e1bc57271215441af8e22f957bdd90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 13:18:26 +0100 Subject: [PATCH 60/86] fix queue import --- pype/scripts/slates/lib.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/lib.py index a73f87e82f..154c689349 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/lib.py @@ -1,5 +1,9 @@ import logging -from queue import Queue + +try: + from queue import Queue +except Exception: + from Queue import Queue from .slate_base.main_frame import MainFrame from .slate_base.layer import Layer From 9ca3541c827353f4da8656853f6469f2ea7ab320 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 15:30:46 +0100 Subject: [PATCH 61/86] slates are more like package, fixed few bugs and example is in specific file with more data of example --- pype/scripts/slates/__init__.py | 2 + pype/scripts/slates/__main__.py | 10 + pype/scripts/slates/slate_base/api.py | 15 ++ pype/scripts/slates/slate_base/example.py | 254 ++++++++++++++++++++ pype/scripts/slates/slate_base/items.py | 1 + pype/scripts/slates/{ => slate_base}/lib.py | 69 ++---- 6 files changed, 300 insertions(+), 51 deletions(-) create mode 100644 pype/scripts/slates/__init__.py create mode 100644 pype/scripts/slates/__main__.py create mode 100644 pype/scripts/slates/slate_base/api.py create mode 100644 pype/scripts/slates/slate_base/example.py rename pype/scripts/slates/{ => slate_base}/lib.py (63%) diff --git a/pype/scripts/slates/__init__.py b/pype/scripts/slates/__init__.py new file mode 100644 index 0000000000..52937708ea --- /dev/null +++ b/pype/scripts/slates/__init__.py @@ -0,0 +1,2 @@ +from . import slate_base +from .slate_base import api diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py new file mode 100644 index 0000000000..29282d3226 --- /dev/null +++ b/pype/scripts/slates/__main__.py @@ -0,0 +1,10 @@ +from slate_base import api + + +def main(in_args=None): + # TODO proper argument handling + api.example() + + +if __name__ == "__main__": + main() diff --git a/pype/scripts/slates/slate_base/api.py b/pype/scripts/slates/slate_base/api.py new file mode 100644 index 0000000000..cd64c68134 --- /dev/null +++ b/pype/scripts/slates/slate_base/api.py @@ -0,0 +1,15 @@ +from .font_factory import FontFactory +from .base import BaseObj, load_default_style +from .main_frame import MainFrame +from .layer import Layer +from .items import ( + BaseItem, + ItemImage, + ItemRectangle, + ItemPlaceHolder, + ItemText, + ItemTable, + TableField +) +from .lib import create_slates +from .example import example diff --git a/pype/scripts/slates/slate_base/example.py b/pype/scripts/slates/slate_base/example.py new file mode 100644 index 0000000000..560f9ec02d --- /dev/null +++ b/pype/scripts/slates/slate_base/example.py @@ -0,0 +1,254 @@ +# import sys +# sys.append(r"PATH/TO/PILLOW/PACKAGE") + +from . import api + + +def example(): + """Example data to demontrate function. + + It is required to fill "destination_path", "thumbnail_path" + and "color_bar_path" in `example_fill_data` to be able to execute. + """ + + example_fill_data = { + "destination_path": "PATH/TO/OUTPUT/FILE", + "project": { + "name": "Testing project" + }, + "intent": "WIP", + "version_name": "seq01_sh0100_compositing_v01", + "date": "2019-08-09", + "shot_type": "2d comp", + "submission_note": ( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." + " Aenean commodo ligula eget dolor. Aenean massa." + " Cum sociis natoque penatibus et magnis dis parturient montes," + " nascetur ridiculus mus. Donec quam felis, ultricies nec," + " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" + " enim. Donec pede justo, fringilla vel," + " aliquet nec, vulputate eget, arcu." + ), + "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", + "color_bar_path": "PATH/TO/COLOR/BAR/FILE", + "vendor": "Our Studio", + "shot_name": "sh0100", + "frame_start": 1001, + "frame_end": 1004, + "duration": 3 + } + + example_presets = {"example_HD": { + "width": 1920, + "height": 1080, + "destination_path": "{destination_path}", + "style": { + "*": { + "font-family": "arial", + "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 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": True, + "ellide": False, + "word-wrap": True, + "max-lines": None + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": True, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "{project[name]}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[0:1]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "{intent}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[0:1]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": True + } + }, { + "type": "table", + "use_alternate_color": True, + "values": [ + ["Version name:", "{version_name}"], + ["Date:", "{date}"], + ["Shot Types:", "{shot_type}"], + ["Submission Note:", "{submission_note}"] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[0:1]": { + "font-bold": True + }, + "table-item-field[3:0]": { + "word-wrap": True, + "ellide": True, + "max-lines": 4 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "{thumbnail_path}", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "{color_bar_path}", + "return_data": True, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": True, + "values": [ + ["Vendor:", "{vendor}"], + ["Shot Name:", "{shot_name}"], + ["Frames:", "{frame_start} - {frame_end} ({duration})"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] + }} + + api.create_slates(example_fill_data, "example_HD", example_presets) diff --git a/pype/scripts/slates/slate_base/items.py b/pype/scripts/slates/slate_base/items.py index ea31443f80..1183d73305 100644 --- a/pype/scripts/slates/slate_base/items.py +++ b/pype/scripts/slates/slate_base/items.py @@ -1,3 +1,4 @@ +import os import re from PIL import Image diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/slate_base/lib.py similarity index 63% rename from pype/scripts/slates/lib.py rename to pype/scripts/slates/slate_base/lib.py index 154c689349..3c7a465e98 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/slate_base/lib.py @@ -1,17 +1,19 @@ import logging - try: from queue import Queue except Exception: from Queue import Queue -from .slate_base.main_frame import MainFrame -from .slate_base.layer import Layer -from .slate_base.items import ( +from .main_frame import MainFrame +from .layer import Layer +from .items import ( ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder ) -from pypeapp import config +try: + from pypeapp.config import get_presets +except Exception: + get_presets = dict log = logging.getLogger(__name__) @@ -19,17 +21,20 @@ log = logging.getLogger(__name__) RequiredSlateKeys = ["width", "height", "destination_path"] -def create_slates(fill_data, slate_name): - presets = config.get_presets() - slate_presets = ( - presets - .get("tools", {}) - .get("slates") - ) or {} +def create_slates(fill_data, slate_name, slate_presets=None): + if slate_presets is None: + presets = get_presets() + slate_presets = ( + presets + .get("tools", {}) + .get("slates") + ) or {} slate_data = slate_presets.get(slate_name) if not slate_data: - log.error("Slate data of <{}> does not exists.") + log.error( + "Name \"{}\" was not found in slate presets.".format(slate_name) + ) return False missing_keys = [] @@ -111,41 +116,3 @@ def create_slates(fill_data, slate_name): main.draw() log.debug("Slate creation finished") - - -def example(): - # import sys - # sys.append(r"PATH/TO/PILLOW/PACKAGE") - # sys.append(r"PATH/TO/PYPE-SETUP") - - fill_data = { - "destination_path": "PATH/TO/OUTPUT/FILE", - "project": { - "name": "Testing project" - }, - "intent": "WIP", - "version_name": "seq01_sh0100_compositing_v01", - "date": "2019-08-09", - "shot_type": "2d comp", - "submission_note": ( - "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." - " Aenean commodo ligula eget dolor. Aenean massa." - " Cum sociis natoque penatibus et magnis dis parturient montes," - " nascetur ridiculus mus. Donec quam felis, ultricies nec," - " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" - " enim. Donec pede justo, fringilla vel," - " aliquet nec, vulputate eget, arcu." - ), - "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", - "color_bar_path": "PATH/TO/COLOR/BAR/FILE", - "vendor": "Our Studio", - "shot_name": "sh0100", - "frame_start": 1001, - "frame_end": 1004, - "duration": 3 - } - create_slates(fill_data, "example_HD") - - -if __name__ == "__main__": - example() From a5d313492f7f157513c124894289f297a7ec237c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 16:27:52 +0100 Subject: [PATCH 62/86] hopefully fix creating items --- pype/scripts/slates/slate_base/items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/scripts/slates/slate_base/items.py b/pype/scripts/slates/slate_base/items.py index 1183d73305..6d19fc6a0c 100644 --- a/pype/scripts/slates/slate_base/items.py +++ b/pype/scripts/slates/slate_base/items.py @@ -36,8 +36,8 @@ class ItemImage(BaseItem): obj_type = "image" def __init__(self, image_path, *args, **kwargs): - super(ItemImage, self).__init__(*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): @@ -143,8 +143,8 @@ class ItemText(BaseItem): obj_type = "text" def __init__(self, value, *args, **kwargs): - super(ItemText, self).__init__(*args, **kwargs) self.value = value + super(ItemText, self).__init__(*args, **kwargs) def draw(self, image, drawer): bg_color = self.style["bg-color"] From 3e216dc3b39026515aa8ff2d86b25224ce68bb8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Mar 2020 12:04:00 +0100 Subject: [PATCH 63/86] slates can receive data though command line like burnins and it is possible to save slates metadata to json --- pype/scripts/slates/__main__.py | 13 +++++-- pype/scripts/slates/slate_base/lib.py | 56 +++++++++++++++++++++------ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py index 29282d3226..43efcfbb06 100644 --- a/pype/scripts/slates/__main__.py +++ b/pype/scripts/slates/__main__.py @@ -1,10 +1,17 @@ +import json from slate_base import api def main(in_args=None): - # TODO proper argument handling - api.example() + data_arg = in_args[-1] + in_data = json.loads(data_arg) + api.create_slates( + in_data["fill_data"], + in_data.get("slate_name"), + in_data.get("slate_data"), + in_data.get("data_output_json") + ) if __name__ == "__main__": - main() + main(sys.argv) diff --git a/pype/scripts/slates/slate_base/lib.py b/pype/scripts/slates/slate_base/lib.py index 3c7a465e98..d9f8ad6d42 100644 --- a/pype/scripts/slates/slate_base/lib.py +++ b/pype/scripts/slates/slate_base/lib.py @@ -1,3 +1,5 @@ +import os +import json import logging try: from queue import Queue @@ -21,21 +23,36 @@ log = logging.getLogger(__name__) RequiredSlateKeys = ["width", "height", "destination_path"] -def create_slates(fill_data, slate_name, slate_presets=None): - if slate_presets is None: - presets = get_presets() +# TODO proper documentation +def create_slates( + fill_data, slate_name=None, slate_data=None, data_output_json=None +): + """Implmentation for command line executing. + + Data for slates are by defaule taken from presets. That requires to enter, + `slate_name`. If `slate_data` are entered then they are used. + + `data_output` should be path to json file where data will be collected. + """ + if slate_data is None and slate_name is None: + raise TypeError( + "`create_slates` expects to enter data for slates or name" + " of slate preset." + ) + + elif slate_data is None: slate_presets = ( - presets + get_presets() .get("tools", {}) .get("slates") ) or {} - slate_data = slate_presets.get(slate_name) - - if not slate_data: - log.error( - "Name \"{}\" was not found in slate presets.".format(slate_name) - ) - return False + slate_data = slate_presets.get(slate_name) + if slate_data is None: + raise ValueError( + "Preset name \"{}\" was not found in slate presets.".format( + slate_name + ) + ) missing_keys = [] for key in RequiredSlateKeys: @@ -116,3 +133,20 @@ def create_slates(fill_data, slate_name, slate_presets=None): main.draw() log.debug("Slate creation finished") + + if not data_output_json: + return + + if not data_output_json.endswith(".json"): + raise ValueError("Output path must be .json file.") + + data_output_json_dir = os.path.dirname(data_output_json) + if not os.path.exists(data_output_json_dir): + log.info("Creating folder \"{}\"".format(data_output_json_dir)) + os.makedirs(data_output_json_dir) + + output_data = main.collect_data() + with open(data_output_json, "w") as json_file: + json_file.write(json.dumps(output_data, indent=4)) + + log.info("Metadata collected in \"{}\".".format(data_output_json)) From 9a749a03a950afbcccb8467f6308f0d7456de4ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Mar 2020 12:06:04 +0100 Subject: [PATCH 64/86] added important import --- pype/scripts/slates/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py index 43efcfbb06..bd49389d84 100644 --- a/pype/scripts/slates/__main__.py +++ b/pype/scripts/slates/__main__.py @@ -1,3 +1,4 @@ +import sys import json from slate_base import api From 66f7367524dbb3d749cf6564fbd48fbe00b74a34 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 30 Mar 2020 17:35:16 +0200 Subject: [PATCH 65/86] minor refactor --- ...ract_quicktime.py => extract_playblast.py} | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) rename pype/plugins/maya/publish/{extract_quicktime.py => extract_playblast.py} (87%) diff --git a/pype/plugins/maya/publish/extract_quicktime.py b/pype/plugins/maya/publish/extract_playblast.py similarity index 87% rename from pype/plugins/maya/publish/extract_quicktime.py rename to pype/plugins/maya/publish/extract_playblast.py index 29d6b78051..cc307d6afc 100644 --- a/pype/plugins/maya/publish/extract_quicktime.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -3,24 +3,23 @@ import glob import contextlib import clique import capture -# + import pype.maya.lib as lib import pype.api -# -from maya import cmds, mel + +from maya import cmds import pymel.core as pm -# TODO: move codec settings to presets -class ExtractQuicktime(pype.api.Extractor): - """Extract Quicktime from viewport capture. +class ExtractPlayblast(pype.api.Extractor): + """Extract viewport playblast. Takes review camera and creates review Quicktime video based on viewport capture. """ - label = "Quicktime" + label = "Extract Playblast" hosts = ["maya"] families = ["review"] optional = True @@ -29,7 +28,7 @@ class ExtractQuicktime(pype.api.Extractor): self.log.info("Extracting capture..") # get scene fps - fps = mel.eval('currentTimeUnitToFPS()') + fps = instance.data.get("fps") or instance.context.data.get("fps") # if start and end frames cannot be determined, get them # from Maya timeline @@ -39,6 +38,7 @@ class ExtractQuicktime(pype.api.Extractor): start = cmds.playbackOptions(query=True, animationStartTime=True) if end is None: end = cmds.playbackOptions(query=True, animationEndTime=True) + self.log.info("start: {}, end: {}".format(start, end)) # get cameras @@ -47,7 +47,7 @@ class ExtractQuicktime(pype.api.Extractor): try: preset = lib.load_capture_preset(data=capture_preset) - except: + except Exception: preset = {} self.log.info('using viewport preset: {}'.format(preset)) @@ -55,19 +55,10 @@ class ExtractQuicktime(pype.api.Extractor): preset['format'] = "image" # preset['compression'] = "qt" preset['quality'] = 95 - preset['compression'] = "jpg" + preset['compression'] = "png" preset['start_frame'] = start preset['end_frame'] = end preset['camera_options'] = { - "displayGateMask": False, - "displayResolution": False, - "displayFilmGate": False, - "displayFieldChart": False, - "displaySafeAction": False, - "displaySafeTitle": False, - "displayFilmPivot": False, - "displayFilmOrigin": False, - "overscan": 1.0, "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), } @@ -90,8 +81,8 @@ class ExtractQuicktime(pype.api.Extractor): filename = preset.get("filename", "%TEMP%") # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between playblast - # and viewer + # viewer opening call to allow a signal to trigger between + # playblast and viewer preset['viewer'] = False # Remove panel key since it's internal value to capture_gui @@ -112,8 +103,8 @@ class ExtractQuicktime(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'mov', - 'ext': 'mov', + 'name': 'png', + 'ext': 'png', 'files': collected_frames, "stagingDir": stagingdir, "frameStart": start, @@ -133,7 +124,6 @@ class ExtractQuicktime(pype.api.Extractor): To workaround this we just glob.glob() for any file extensions and assume the latest modified file is the correct file and return it. - """ # Catch cancelled playblast if filepath is None: @@ -164,7 +154,6 @@ class ExtractQuicktime(pype.api.Extractor): return filepath - @contextlib.contextmanager def maintained_time(): ct = cmds.currentTime(query=True) From 17622b046d3e4fd814ad45d4f2e4cdd554d6a39a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 1 Apr 2020 19:16:54 +0200 Subject: [PATCH 66/86] fixed camera options --- pype/plugins/maya/publish/extract_playblast.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index cc307d6afc..579712018c 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -58,9 +58,9 @@ class ExtractPlayblast(pype.api.Extractor): preset['compression'] = "png" preset['start_frame'] = start preset['end_frame'] = end - preset['camera_options'] = { - "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), - } + camera_option = preset.get("camera_option", {}) + camera_option["depthOfField"] = cmds.getAttr( + "{0}.depthOfField".format(camera)) stagingdir = self.staging_dir(instance) filename = "{0}".format(instance.name) From 7d48a0322d3df9b7fdc8e8e14d774890c173b884 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 2 Apr 2020 16:54:33 +0200 Subject: [PATCH 67/86] function names and gramar --- pype/lib.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 5410c2eba1..78fe8b6c69 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -843,7 +843,7 @@ def load_containers_by_asset_data( log.debug("Valid subsets: {}".format(valid_subsets_by_id.values())) valid_repres_by_subset_id = collections.defaultdict(list) - for subset_id, subset_entity in valid_subsets_by_id.items(): + for subset_id, _subset_entity in valid_subsets_by_id.items(): variant = variants_per_subset_id[subset_id] variant_repre_names = variant.get("repre_names") if not variant_repre_names: @@ -889,7 +889,7 @@ def load_containers_by_asset_data( is_loaded = False for repre_name_idx, variant_repre_name in enumerate( - variant_repre_names + variant_repre_names ): found_repre = None for repre in repres: @@ -947,7 +947,7 @@ def load_containers_by_asset_data( return loaded_containers -def get_link_assets(asset_entity): +def get_linked_assets(asset_entity): """Return linked assets for `asset_entity`.""" # TODO implement return [] @@ -1048,7 +1048,7 @@ def collect_last_version_repres(asset_entities): return output -def load_containers_for_workfile(): +def load_containers_to_workfile(): """Load containers for (first) workfile. Loads latest versions of current and linked assets to workfile by logic @@ -1097,7 +1097,7 @@ def load_containers_for_workfile(): # Skip if there are any loaders if not loaders_by_name: - print("There are not registered loaders.") + print("There are no registered loaders.") return # Get current task name @@ -1109,7 +1109,7 @@ def load_containers_for_workfile(): # Skip if there are any presets for task if not workfile_presets: log.warning( - "For current task `{}` is not set any loading preset.".format( + "Current task `{}` does not have any loading preset.".format( current_task_name ) ) @@ -1128,12 +1128,12 @@ def load_containers_for_workfile(): elif not current_context: log.warning(( - "Current task `{}` don't have set loading preset for it's context." + "Current task `{}` doesn't have any loading preset for it's context." ).format(current_task_name)) elif not link_context: log.warning(( - "Current task `{}` don't have set " + "Current task `{}` doesn't have any" "loading preset for it's linked assets." ).format(current_task_name)) @@ -1147,7 +1147,7 @@ def load_containers_for_workfile(): if link_context: # Find and append linked assets if preset has set linked mapping - link_assets = get_link_assets(current_asset_entity) + link_assets = get_linked_assets(current_asset_entity) if link_assets: assets.extend(link_assets) @@ -1268,7 +1268,7 @@ def create_first_workfile(file_ext=None): # 2.) override # 3.) raise exception if file_path is not None: - log.warning("There already exist workfile `{}`.".format(file_path)) + log.warning("Workfile already exists`{}`.".format(file_path)) return file_path hierarchy = "" @@ -1311,5 +1311,5 @@ def build_first_workfile(file_ext=None): # Create workfile workfile_path = create_first_workfile(file_ext) # Load containers - loaded_containers = load_containers_for_workfile() + loaded_containers = load_containers_to_workfile() return (workfile_path, loaded_containers) From f81e2e9ca3c5ff119a60e09b51f3027f40f9265f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 2 Apr 2020 18:10:40 +0200 Subject: [PATCH 68/86] Update validate_rig_output_ids.py --- pype/plugins/maya/publish/validate_rig_output_ids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/validate_rig_output_ids.py b/pype/plugins/maya/publish/validate_rig_output_ids.py index aefe883149..b6b3735196 100644 --- a/pype/plugins/maya/publish/validate_rig_output_ids.py +++ b/pype/plugins/maya/publish/validate_rig_output_ids.py @@ -38,7 +38,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if compute: out_set = next(x for x in instance if x.endswith("out_SET")) instance_nodes = pc.sets(out_set, query=True) - instance_nodes.extend([x.getShape() for x in instance_nodes]) + instance_nodes.extend([x.getShape() for x in instance_nodes if x.getShape()]) scene_nodes = pc.ls(type="transform") + pc.ls(type="mesh") scene_nodes = set(scene_nodes) - set(instance_nodes) From fe5733771a6418f9bc47e03fe5055e21f71b3e26 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Apr 2020 09:53:25 +0200 Subject: [PATCH 69/86] Update pype/lib.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit precompile regex Co-Authored-By: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- pype/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index 78fe8b6c69..108351ee1b 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1205,8 +1205,9 @@ def get_last_workfile_path(root, template, file_ext): version = None last_file_name = None + re_template = re.compile(template) for file_name in sorted(filtered_file_names): - match = re.match(template, file_name, **kwargs) + match = re.match(re_template, file_name, **kwargs) if not match: continue From d8d5ccb2f0544f6fa83108d3fe0643ba87496c78 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Apr 2020 10:15:04 +0200 Subject: [PATCH 70/86] PR comments based changes --- pype/lib.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 108351ee1b..e863fcac77 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -711,7 +711,17 @@ class PypeHook: pass -def get_workfile_build_presets(task_name): +def get_workfile_build_preset(task_name): + """ Returns preset variant to build workfile for task name. + + Presets are loaded for current project set in io.Session["AVALON_PROJECT"], + filtered by registered host and entered task name. + + :param task_name: Task name used for filtering build presets. + :type task_name: str + :return: preset per eneter task + :rtype: dict | None + """ host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1] presets = config.get_presets(io.Session["AVALON_PROJECT"]) # Get presets for host @@ -739,15 +749,15 @@ def get_workfile_build_presets(task_name): def load_containers_by_asset_data( - asset_entity_data, workfile_presets, loaders_by_name + asset_entity_data, workfile_preset, loaders_by_name ): - if not asset_entity_data or not workfile_presets or not loaders_by_name: + if not asset_entity_data or not workfile_preset or not loaders_by_name: return asset_entity = asset_entity_data["asset_entity"] # Filter workfile presets by available loaders valid_variants = [] - for variant in workfile_presets: + for variant in workfile_preset: variant_loaders = variant.get("loaders") if not variant_loaders: log.warning(( @@ -755,14 +765,15 @@ def load_containers_by_asset_data( ).format(json.dumps(variant, indent=4))) continue - found = False + valid_variant = None for loader_name in variant_loaders: if loader_name in loaders_by_name: - valid_variants.append(variant) - found = True + valid_variant = variant break - if not found: + if valid_variant: + valid_variants.append(valid_variant) + else: log.warning( "Loaders in Workfile variant are not available: {0}".format( json.dumps(variant, indent=4) @@ -833,7 +844,7 @@ def load_containers_by_asset_data( valid_subsets_by_id[subset_id] = subset variants_per_subset_id[subset_id] = variant - # break variants loop if got here + # break variants loop on finding the first matching variant break if not valid_subsets_by_id: @@ -1071,8 +1082,6 @@ def load_containers_to_workfile(): ... }] """ - io.install() - # Get current asset name and entity current_asset_name = io.Session["AVALON_ASSET"] current_asset_entity = io.find_one({ @@ -1101,13 +1110,13 @@ def load_containers_to_workfile(): return # Get current task name - current_task_name = os.environ["AVALON_TASK"] + current_task_name = io.Session["AVALON_TASK"] current_task_name_low = current_task_name.lower() # Load workfile presets for task - workfile_presets = get_workfile_build_presets(current_task_name_low) + workfile_preset = get_workfile_build_preset(current_task_name_low) # Skip if there are any presets for task - if not workfile_presets: + if not workfile_preset: log.warning( "Current task `{}` does not have any loading preset.".format( current_task_name @@ -1116,9 +1125,9 @@ def load_containers_to_workfile(): return # Get presets for loading current asset - current_context = workfile_presets.get("current_context") + current_context = workfile_preset.get("current_context") # Get presets for loading linked assets - link_context = workfile_presets.get("linked_assets") + link_context = workfile_preset.get("linked_assets") # Skip if both are missing if not current_context and not link_context: log.warning("Current task `{}` has empty loading preset.".format( From a8d1fc9cc67c4955a71866b97ab6fc70251e7f70 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Apr 2020 10:32:55 +0200 Subject: [PATCH 71/86] variables shuffle --- pype/lib.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index e863fcac77..67c5e82611 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -887,20 +887,15 @@ def load_containers_by_asset_data( } for subset_id, repres in valid_repres_by_subset_id.items(): - subset = valid_subsets_by_id[subset_id] - subset_name = subset["name"] + subset_name = valid_subsets_by_id[subset_id]["name"] variant = variants_per_subset_id[subset_id] - - variant_loader_names = variant["loaders"] - variant_loader_count = len(variant_loader_names) - - variant_repre_names = variant["repre_names"] - variant_repre_count = len(variant_repre_names) + loaders_last_idx = len(variant["loaders"]) - 1 + repre_names_last_idx = len(variant["repre_names"]) - 1 is_loaded = False for repre_name_idx, variant_repre_name in enumerate( - variant_repre_names + variant["repre_names"] ): found_repre = None for repre in repres: @@ -912,7 +907,7 @@ def load_containers_by_asset_data( if not found_repre: continue - for loader_idx, loader_name in enumerate(variant_loader_names): + for loader_idx, loader_name in enumerate(variant["loaders"]): if is_loaded: break @@ -942,9 +937,9 @@ def load_containers_by_asset_data( ) msg = "Loading failed." - if loader_idx < (variant_loader_count - 1): + if loader_idx < loaders_last_idx: msg += " Trying next loader." - elif repre_name_idx < (variant_repre_count - 1): + elif repre_name_idx < repre_names_last_idx: msg += ( " Loading of subset `{}` was not successful." ).format(subset_name) @@ -1106,7 +1101,7 @@ def load_containers_to_workfile(): # Skip if there are any loaders if not loaders_by_name: - print("There are no registered loaders.") + log.warning("There are no registered loaders.") return # Get current task name @@ -1137,7 +1132,8 @@ def load_containers_to_workfile(): elif not current_context: log.warning(( - "Current task `{}` doesn't have any loading preset for it's context." + "Current task `{}` doesn't have any loading" + " preset for it's context." ).format(current_task_name)) elif not link_context: @@ -1160,8 +1156,8 @@ def load_containers_to_workfile(): if link_assets: assets.extend(link_assets) - # Skip if there are any assets - # - this may happend if only linked mapping is set and there are not links + # Skip if there are no assets. This can happen if only linked mapping is + # set and there are no links for his asset. if not assets: log.warning("Asset does not have linked assets. Nothing to process.") return From 305ba756ba7ad52e0db200ff3718d4df4eb773b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Apr 2020 11:32:13 +0200 Subject: [PATCH 72/86] Subprocess in app launch is again stored to variable. Now it is `_popen` and added comment why it is there. --- pype/ftrack/lib/ftrack_app_handler.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index b5576ae046..56196d15f9 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -290,7 +290,10 @@ class AppAction(BaseHandler): # Run SW if was found executable if execfile is not None: - avalonlib.launch( + # Store subprocess to varaible. This is due to Blender launch + # bug. Please make sure Blender >=2.81 can be launched before + # remove `_popen` variable. + _popen = avalonlib.launch( executable=execfile, args=[], environment=env ) else: @@ -323,7 +326,7 @@ class AppAction(BaseHandler): 'message': "No executable permission - {}".format( execfile) } - pass + else: self.log.error('Launcher doesn\'t exist - {}'.format( execfile)) @@ -331,10 +334,13 @@ class AppAction(BaseHandler): 'success': False, 'message': "Launcher doesn't exist - {}".format(execfile) } - pass + # Run SW if was found executable if execfile is not None: - avalonlib.launch( + # Store subprocess to varaible. This is due to Blender launch + # bug. Please make sure Blender >=2.81 can be launched before + # remove `_popen` variable. + _popen = avalonlib.launch( '/usr/bin/env', args=['bash', execfile], environment=env ) else: @@ -343,7 +349,6 @@ class AppAction(BaseHandler): 'message': "We didn't found launcher for {0}" .format(self.label) } - pass # Change status of task to In progress presets = config.get_presets()["ftrack"]["ftrack_config"] From f9655b0e2156b8d151436a9c575f56a3c5221aaf Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 3 Apr 2020 19:47:12 +0200 Subject: [PATCH 73/86] fixed handling of fursettings and representation paths --- pype/plugins/maya/load/load_yeti_cache.py | 35 +++++++++++++++---- .../maya/publish/collect_yeti_cache.py | 4 +++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/load/load_yeti_cache.py b/pype/plugins/maya/load/load_yeti_cache.py index a1793cd67d..286b154ca2 100644 --- a/pype/plugins/maya/load/load_yeti_cache.py +++ b/pype/plugins/maya/load/load_yeti_cache.py @@ -6,10 +6,11 @@ from collections import defaultdict from maya import cmds -from avalon import api +from avalon import api, io from avalon.maya import lib as avalon_lib, pipeline from pype.maya import lib from pypeapp import config +from pprint import pprint class YetiCacheLoader(api.Loader): @@ -101,12 +102,26 @@ class YetiCacheLoader(api.Loader): def update(self, container, representation): + io.install() namespace = container["namespace"] container_node = container["objectName"] + # import web_pdb; web_pdb.set_trace() + + fur_settings = io.find_one( + {"parent": representation["parent"], "name": "fursettings"} + ) + + pprint({"parent": representation["parent"], "name": "fursettings"}) + pprint(fur_settings) + assert fur_settings is not None, ( + "cannot find fursettings representation" + ) + + settings_fname = api.get_representation_path(fur_settings) path = api.get_representation_path(representation) # Get all node data - fname, ext = os.path.splitext(path) - settings_fname = "{}.fursettings".format(fname) + # fname, ext = os.path.splitext(path) + # settings_fname = "{}.fursettings".format(fname) with open(settings_fname, "r") as fp: settings = json.load(fp) @@ -147,13 +162,14 @@ class YetiCacheLoader(api.Loader): cmds.delete(to_remove) + # replace frame in filename with %04d + RE_frame = re.compile(r"(\d+)(\.fur)$") + file_name = re.sub(RE_frame, r"%04d\g<2>", os.path.basename(path)) for cb_id, data in meta_data_lookup.items(): # Update cache file name - file_name = data["name"].replace(":", "_") - cache_file_path = "{}.%04d.fur".format(file_name) data["attrs"]["cacheFileName"] = os.path.join( - path, cache_file_path) + os.path.dirname(path), file_name) if cb_id not in scene_lookup: @@ -197,6 +213,13 @@ class YetiCacheLoader(api.Loader): yeti_node = yeti_nodes[0] for attr, value in data["attrs"].items(): + # import web_pdb; web_pdb.set_trace() + # handle empty attribute strings. Those are reported + # as None, so their type is NoneType and this is not + # supported on attributes in Maya. We change it to + # empty string. + if value is None: + value = "" lib.set_attribute(attr, value, yeti_node) cmds.setAttr("{}.representation".format(container_node), diff --git a/pype/plugins/maya/publish/collect_yeti_cache.py b/pype/plugins/maya/publish/collect_yeti_cache.py index 2365162c05..c5300ff0ee 100644 --- a/pype/plugins/maya/publish/collect_yeti_cache.py +++ b/pype/plugins/maya/publish/collect_yeti_cache.py @@ -49,6 +49,10 @@ class CollectYetiCache(pyblish.api.InstancePlugin): attr_data = {} for attr in SETTINGS: current = cmds.getAttr("%s.%s" % (shape, attr)) + # change None to empty string as Maya doesn't support + # NoneType in attributes + if current is None: + current = "" attr_data[attr] = current # Get transform data From 4bd3d0652e9b56ccbdf1c929d0e05c891b3ac666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 3 Apr 2020 19:54:14 +0200 Subject: [PATCH 74/86] shutting up hound --- pype/plugins/maya/publish/validate_rig_output_ids.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/validate_rig_output_ids.py b/pype/plugins/maya/publish/validate_rig_output_ids.py index b6b3735196..89cd37fe64 100644 --- a/pype/plugins/maya/publish/validate_rig_output_ids.py +++ b/pype/plugins/maya/publish/validate_rig_output_ids.py @@ -38,7 +38,8 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if compute: out_set = next(x for x in instance if x.endswith("out_SET")) instance_nodes = pc.sets(out_set, query=True) - instance_nodes.extend([x.getShape() for x in instance_nodes if x.getShape()]) + instance_nodes.extend( + [x.getShape() for x in instance_nodes if x.getShape()]) scene_nodes = pc.ls(type="transform") + pc.ls(type="mesh") scene_nodes = set(scene_nodes) - set(instance_nodes) From 0c99fc849d131e01a4cf4f25c22414bdd2f1378c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Apr 2020 10:04:05 +0200 Subject: [PATCH 75/86] renamed `load_containers_to_workfile` to `build_workfile` and removed `create_first_workfile` --- pype/lib.py | 97 +---------------------------------------------- pype/maya/menu.py | 7 ++-- 2 files changed, 5 insertions(+), 99 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 67c5e82611..d38e76a5ee 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1054,8 +1054,8 @@ def collect_last_version_repres(asset_entities): return output -def load_containers_to_workfile(): - """Load containers for (first) workfile. +def build_workfile(): + """Load representations for current context as containers into workfile. Loads latest versions of current and linked assets to workfile by logic stored in Workfile variants from presets. Variants are set by host, @@ -1226,96 +1226,3 @@ def get_last_workfile_path(root, template, file_ext): last_file_path = os.path.join(root, last_file_name) return last_file_path - - -def create_first_workfile(file_ext=None): - """Builds first workfile and load containers for it. - - :param file_ext: Work file extension may be specified otherwise first - extension in host's registered extensions is used. - :type file_ext: str - :return: Workfile path and loaded containers by Asset entity - :rtype: tuple - """ - # Get host - host = avalon.api.registered_host() - - # Workfile extension - if file_ext is None: - if not host.file_extensions(): - raise AssertionError( - "Host doesn't have set file extensions. Can't create workfile." - ) - file_ext = host.file_extensions()[0] - - workfile_root = host.work_root(io.Session) - - # make sure extension has dot - if not file_ext.startswith("."): - file_ext = ".{}".format(file_ext) - - # Create new workfile - project_doc = io.find_one({"type": "project"}) - asset_name = io.Session["AVALON_ASSET"] - asset_doc = io.find_one({ - "type": "asset", - "name": asset_name - }) - if not asset_doc: - raise AssertionError( - "Asset with name `{}` was not found.".format(asset_name) - ) - - root = avalon.api.registered_root() - template = project_doc["config"]["template"]["workfile"] - file_path = get_last_workfile_path(root, template, file_ext) - # TODO what should do if already exists? - # 1.) create new - # 2.) override - # 3.) raise exception - if file_path is not None: - log.warning("Workfile already exists`{}`.".format(file_path)) - return file_path - - hierarchy = "" - parents = asset_doc["data"].get("parents") - if parents: - hierarchy = "/".join(parents) - - # Use same data as Workfiles tool - template_data = { - "root": root, - "project": { - "name": project_doc["name"], - "code": project_doc["data"].get("code") - }, - "asset": asset_name, - "task": io.Session["AVALON_TASK"], - "hierarchy": hierarchy, - "version": 1, - "user": getpass.getuser(), - "ext": file_ext - } - - # Use same template as in Workfiles Tool - template_filled = pipeline.format_template_with_optional_keys( - template_data, template - ) - - # make sure filled template does not have more dots due to extension - while ".." in template_filled: - template_filled = template_filled.replace("..", ".") - - workfile_path = os.path.join(workfile_root, template_filled) - host.save_file(workfile_path) - - return workfile_path - - -def build_first_workfile(file_ext=None): - # DEBUG this should probably be host specific - # Create workfile - workfile_path = create_first_workfile(file_ext) - # Load containers - loaded_containers = load_containers_to_workfile() - return (workfile_path, loaded_containers) diff --git a/pype/maya/menu.py b/pype/maya/menu.py index efe338b4af..c25166f210 100644 --- a/pype/maya/menu.py +++ b/pype/maya/menu.py @@ -2,10 +2,9 @@ import sys import os import logging -from avalon.vendor.Qt import QtWidgets, QtCore, QtGui +from avalon.vendor.Qt import QtWidgets, QtGui from avalon.maya import pipeline -import avalon.api -from ..lib import build_first_workfile +from ..lib import build_workfile import maya.cmds as cmds self = sys.modules[__name__] @@ -30,7 +29,7 @@ def deferred(): cmds.menuItem( "Build First Workfile", parent=pipeline._menu, - command=lambda *args: build_first_workfile() + command=lambda *args: build_workfile() ) log.info("Attempting to install scripts menu..") From c002890772e0e95a9e27c108f8d5758368bfc73a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Apr 2020 10:27:47 +0200 Subject: [PATCH 76/86] name `variant` changed to `profile` --- pype/lib.py | 95 ++++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index d38e76a5ee..11491fe250 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -4,7 +4,6 @@ import types import re import uuid import json -import getpass import collections import logging import itertools @@ -712,7 +711,7 @@ class PypeHook: def get_workfile_build_preset(task_name): - """ Returns preset variant to build workfile for task name. + """ Returns preset profile to build workfile for task name. Presets are loaded for current project set in io.Session["AVALON_PROJECT"], filtered by registered host and entered task name. @@ -733,16 +732,16 @@ def get_workfile_build_preset(task_name): task_name_low = task_name.lower() per_task_preset = None - for variant in workfile_presets: - variant_tasks = variant.get("tasks") - if not variant_tasks: + for profile in workfile_presets: + profile_tasks = profile.get("tasks") + if not profile_tasks: continue - variant_tasks_low = [task.lower() for task in variant_tasks] - if task_name_low not in variant_tasks_low: + profile_tasks_low = [task.lower() for task in profile_tasks] + if task_name_low not in profile_tasks_low: continue - per_task_preset = variant + per_task_preset = profile break return per_task_preset @@ -756,35 +755,35 @@ def load_containers_by_asset_data( asset_entity = asset_entity_data["asset_entity"] # Filter workfile presets by available loaders - valid_variants = [] - for variant in workfile_preset: - variant_loaders = variant.get("loaders") - if not variant_loaders: + valid_profiles = [] + for profile in workfile_preset: + profile_loaders = profile.get("loaders") + if not profile_loaders: log.warning(( - "Workfile variant has missing loaders configuration: {0}" - ).format(json.dumps(variant, indent=4))) + "Workfile profile has missing loaders configuration: {0}" + ).format(json.dumps(profile, indent=4))) continue - valid_variant = None - for loader_name in variant_loaders: + valid_profile = None + for loader_name in profile_loaders: if loader_name in loaders_by_name: - valid_variant = variant + valid_profile = profile break - if valid_variant: - valid_variants.append(valid_variant) + if valid_profile: + valid_profiles.append(valid_profile) else: log.warning( - "Loaders in Workfile variant are not available: {0}".format( - json.dumps(variant, indent=4) + "Loaders in Workfile profile are not available: {0}".format( + json.dumps(profile, indent=4) ) ) - if not valid_variants: - log.warning("There are not valid Workfile variants. Skipping process.") + if not valid_profiles: + log.warning("There are not valid Workfile profiles. Skipping process.") return - log.debug("Valid Workfile variants: {}".format(valid_variants)) + log.debug("Valid Workfile profiles: {}".format(valid_profiles)) subsets = [] version_by_subset_id = {} @@ -814,25 +813,25 @@ def load_containers_by_asset_data( subsets_by_family[family].append(subset) valid_subsets_by_id = {} - variants_per_subset_id = {} + profiles_per_subset_id = {} for family, subsets in subsets_by_family.items(): family_low = family.lower() - for variant in valid_variants: + for profile in valid_profiles: # Family filtering - variant_families = variant.get("families") or [] - if not variant_families: + profile_families = profile.get("families") or [] + if not profile_families: continue - variant_families_low = [fam.lower() for fam in variant_families] - if family_low not in variant_families_low: + profile_families_low = [fam.lower() for fam in profile_families] + if family_low not in profile_families_low: continue # Regex filtering (optional) - variant_regexes = variant.get("subset_name_filters") + profile_regexes = profile.get("subset_name_filters") for subset in subsets: - if variant_regexes: + if profile_regexes: valid = False - for pattern in variant_regexes: + for pattern in profile_regexes: if re.match(pattern, subset["name"]): valid = True break @@ -842,9 +841,9 @@ def load_containers_by_asset_data( subset_id = subset["_id"] valid_subsets_by_id[subset_id] = subset - variants_per_subset_id[subset_id] = variant + profiles_per_subset_id[subset_id] = profile - # break variants loop on finding the first matching variant + # break profiles loop on finding the first matching profile break if not valid_subsets_by_id: @@ -855,20 +854,20 @@ def load_containers_by_asset_data( valid_repres_by_subset_id = collections.defaultdict(list) for subset_id, _subset_entity in valid_subsets_by_id.items(): - variant = variants_per_subset_id[subset_id] - variant_repre_names = variant.get("repre_names") - if not variant_repre_names: + profile = profiles_per_subset_id[subset_id] + profile_repre_names = profile.get("repre_names") + if not profile_repre_names: continue # Lower names - variant_repre_names = [name.lower() for name in variant_repre_names] + profile_repre_names = [name.lower() for name in profile_repre_names] version_entity = version_by_subset_id[subset_id] version_id = version_entity["_id"] repres = repres_by_version_id[version_id] for repre in repres: repre_name_low = repre["name"].lower() - if repre_name_low in variant_repre_names: + if repre_name_low in profile_repre_names: valid_repres_by_subset_id[subset_id].append(repre) # DEBUG message @@ -889,25 +888,25 @@ def load_containers_by_asset_data( for subset_id, repres in valid_repres_by_subset_id.items(): subset_name = valid_subsets_by_id[subset_id]["name"] - variant = variants_per_subset_id[subset_id] - loaders_last_idx = len(variant["loaders"]) - 1 - repre_names_last_idx = len(variant["repre_names"]) - 1 + profile = profiles_per_subset_id[subset_id] + loaders_last_idx = len(profile["loaders"]) - 1 + repre_names_last_idx = len(profile["repre_names"]) - 1 is_loaded = False - for repre_name_idx, variant_repre_name in enumerate( - variant["repre_names"] + for repre_name_idx, profile_repre_name in enumerate( + profile["repre_names"] ): found_repre = None for repre in repres: repre_name = repre["name"] - if repre_name == variant_repre_name: + if repre_name == profile_repre_name: found_repre = repre break if not found_repre: continue - for loader_idx, loader_name in enumerate(variant["loaders"]): + for loader_idx, loader_name in enumerate(profile["loaders"]): if is_loaded: break @@ -1058,7 +1057,7 @@ def build_workfile(): """Load representations for current context as containers into workfile. Loads latest versions of current and linked assets to workfile by logic - stored in Workfile variants from presets. Variants are set by host, + stored in Workfile profiles from presets. Variants are set by host, filtered by current task name and used by families. Each family can specify representation names and loaders for From 29d63e4f179d5a7fe19952fbac5c2331b73833b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Apr 2020 12:05:01 +0200 Subject: [PATCH 77/86] build workile methods wrapped into one class and methods were split or reorganized a little bit --- pype/lib.py | 977 ++++++++++++++++++++++++---------------------- pype/maya/menu.py | 4 +- 2 files changed, 512 insertions(+), 469 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 11491fe250..48d9cb5965 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -710,97 +710,13 @@ class PypeHook: pass -def get_workfile_build_preset(task_name): - """ Returns preset profile to build workfile for task name. - - Presets are loaded for current project set in io.Session["AVALON_PROJECT"], - filtered by registered host and entered task name. - - :param task_name: Task name used for filtering build presets. - :type task_name: str - :return: preset per eneter task - :rtype: dict | None - """ - host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1] - presets = config.get_presets(io.Session["AVALON_PROJECT"]) - # Get presets for host - workfile_presets = presets["plugins"].get(host_name, {}).get( - "workfile_build" - ) - if not workfile_presets: - return - - task_name_low = task_name.lower() - per_task_preset = None - for profile in workfile_presets: - profile_tasks = profile.get("tasks") - if not profile_tasks: - continue - - profile_tasks_low = [task.lower() for task in profile_tasks] - if task_name_low not in profile_tasks_low: - continue - - per_task_preset = profile - break - - return per_task_preset +def get_linked_assets(asset_entity): + """Return linked assets for `asset_entity`.""" + # TODO implement + return [] -def load_containers_by_asset_data( - asset_entity_data, workfile_preset, loaders_by_name -): - if not asset_entity_data or not workfile_preset or not loaders_by_name: - return - - asset_entity = asset_entity_data["asset_entity"] - # Filter workfile presets by available loaders - valid_profiles = [] - for profile in workfile_preset: - profile_loaders = profile.get("loaders") - if not profile_loaders: - log.warning(( - "Workfile profile has missing loaders configuration: {0}" - ).format(json.dumps(profile, indent=4))) - continue - - valid_profile = None - for loader_name in profile_loaders: - if loader_name in loaders_by_name: - valid_profile = profile - break - - if valid_profile: - valid_profiles.append(valid_profile) - else: - log.warning( - "Loaders in Workfile profile are not available: {0}".format( - json.dumps(profile, indent=4) - ) - ) - - if not valid_profiles: - log.warning("There are not valid Workfile profiles. Skipping process.") - return - - log.debug("Valid Workfile profiles: {}".format(valid_profiles)) - - subsets = [] - version_by_subset_id = {} - repres_by_version_id = {} - for subset_id, in_data in asset_entity_data["subsets"].items(): - subsets.append(in_data["subset_entity"]) - version_data = in_data["version"] - version_entity = version_data["version_entity"] - version_by_subset_id[subset_id] = version_entity - repres_by_version_id[version_entity["_id"]] = version_data["repres"] - - if not subsets: - log.warning("There are not subsets for asset {0}".format( - asset_entity["name"] - )) - return - +def map_subsets_by_family(subsets): subsets_by_family = collections.defaultdict(list) for subset in subsets: family = subset["data"].get("family") @@ -811,417 +727,544 @@ def load_containers_by_asset_data( family = families[0] subsets_by_family[family].append(subset) + return subsets_by_family - valid_subsets_by_id = {} - profiles_per_subset_id = {} - for family, subsets in subsets_by_family.items(): - family_low = family.lower() - for profile in valid_profiles: - # Family filtering - profile_families = profile.get("families") or [] - if not profile_families: + +class BuildWorkfile: + """Wrapper for build workfile process. + + Load representations for current context by build presets. Build presets + are host related, since each host has it's loaders. + """ + + @classmethod + def build_workfile(cls): + """Main method of this wrapper. + + Loads latest versions of current and linked assets to workfile by logic + stored in Workfile profiles from presets. Profiles are set by host, + filtered by current task name and used by families. + + Each family can specify representation names and loaders for + representations and first available and successful loaded + representation is returned as container. + + At the end you'll get list of loaded containers per each asset. + + loaded_containers [{ + "asset_entity": , + "containers": [, , ...] + }, { + "asset_entity": , + "containers": [, ...] + }, { + ... + }] + """ + # Get current asset name and entity + current_asset_name = io.Session["AVALON_ASSET"] + current_asset_entity = io.find_one({ + "type": "asset", + "name": current_asset_name + }) + + # Skip if asset was not found + if not current_asset_entity: + print("Asset entity with name `{}` was not found".format( + current_asset_name + )) + return + + # Prepare available loaders + loaders_by_name = {} + for loader in avalon.api.discover(avalon.api.Loader): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {0}!".format(loader_name) + ) + loaders_by_name[loader_name] = loader + + # Skip if there are any loaders + if not loaders_by_name: + log.warning("There are no registered loaders.") + return + + # Get current task name + current_task_name = io.Session["AVALON_TASK"] + + # Load workfile presets for task + build_presets = cls.get_build_presets(current_task_name) + + # Skip if there are any presets for task + if not build_presets: + log.warning( + "Current task `{}` does not have any loading preset.".format( + current_task_name + ) + ) + return + + # Get presets for loading current asset + current_context_profiles = build_presets.get("current_context") + # Get presets for loading linked assets + link_context_profiles = build_presets.get("linked_assets") + # Skip if both are missing + if not current_context_profiles and not link_context_profiles: + log.warning("Current task `{}` has empty loading preset.".format( + current_task_name + )) + return + + elif not current_context_profiles: + log.warning(( + "Current task `{}` doesn't have any loading" + " preset for it's context." + ).format(current_task_name)) + + elif not link_context_profiles: + log.warning(( + "Current task `{}` doesn't have any" + "loading preset for it's linked assets." + ).format(current_task_name)) + + # Prepare assets to process by workfile presets + assets = [] + current_asset_id = None + if current_context_profiles: + # Add current asset entity if preset has current context set + assets.append(current_asset_entity) + current_asset_id = current_asset_entity["_id"] + + if link_context_profiles: + # Find and append linked assets if preset has set linked mapping + link_assets = get_linked_assets(current_asset_entity) + if link_assets: + assets.extend(link_assets) + + # Skip if there are no assets. This can happen if only linked mapping + # is set and there are no links for his asset. + if not assets: + log.warning( + "Asset does not have linked assets. Nothing to process." + ) + return + + # Prepare entities from database for assets + prepared_entities = cls._collect_last_version_repres(assets) + + # Load containers by prepared entities and presets + loaded_containers = [] + # - Current asset containers + if current_asset_id and current_asset_id in prepared_entities: + current_context_data = prepared_entities.pop(current_asset_id) + loaded_data = cls.load_containers_by_asset_data( + current_context_data, current_context_profiles, loaders_by_name + ) + if loaded_data: + loaded_containers.append(loaded_data) + + # - Linked assets container + for linked_asset_data in prepared_entities.values(): + loaded_data = cls.load_containers_by_asset_data( + linked_asset_data, link_context_profiles, loaders_by_name + ) + if loaded_data: + loaded_containers.append(loaded_data) + + # Return list of loaded containers + return loaded_containers + + @classmethod + def get_build_presets(cls, task_name): + """ Returns presets to build workfile for task name. + + Presets are loaded for current project set in + io.Session["AVALON_PROJECT"], filtered by registered host + and entered task name. + + :param task_name: Task name used for filtering build presets. + :type task_name: str + :return: preset per eneter task + :rtype: dict | None + """ + host_name = avalon.api.registered_host().__name__.rsplit(".", 1)[-1] + presets = config.get_presets(io.Session["AVALON_PROJECT"]) + # Get presets for host + build_presets = ( + presets["plugins"] + .get(host_name, {}) + .get("workfile_build") + ) + if not build_presets: + return + + task_name_low = task_name.lower() + per_task_preset = None + for preset in build_presets: + preset_tasks = preset.get("tasks") or [] + preset_tasks_low = [task.lower() for task in preset_tasks] + if task_name_low in preset_tasks_low: + per_task_preset = preset + break + + return per_task_preset + + @classmethod + def _filter_workfile_profiles(cls, workfile_profiles, loaders_by_name): + """ Filter and prepare workfile presets by available loaders. + + Valid profile must have "loaders", "families" and "repre_names" keys + with valid values. + - "loaders" expects list of strings representing possible loaders. + - "families" expects list of strings for filtering + by main subset family. + - "repre_names" expects list of strings for filtering by + representation name. + + Lowered "families" and "repre_names" are prepared for each profile with + all required keys. + """ + valid_profiles = [] + for profile in workfile_profiles: + # Check loaders + profile_loaders = profile.get("loaders") + if not profile_loaders: + log.warning(( + "Workfile profile has missing loaders configuration: {0}" + ).format(json.dumps(profile, indent=4))) continue - profile_families_low = [fam.lower() for fam in profile_families] - if family_low not in profile_families_low: - continue - - # Regex filtering (optional) - profile_regexes = profile.get("subset_name_filters") - for subset in subsets: - if profile_regexes: - valid = False - for pattern in profile_regexes: - if re.match(pattern, subset["name"]): - valid = True - break - - if not valid: - continue - - subset_id = subset["_id"] - valid_subsets_by_id[subset_id] = subset - profiles_per_subset_id[subset_id] = profile - - # break profiles loop on finding the first matching profile - break - - if not valid_subsets_by_id: - log.warning("There are not valid subsets.") - return - - log.debug("Valid subsets: {}".format(valid_subsets_by_id.values())) - - valid_repres_by_subset_id = collections.defaultdict(list) - for subset_id, _subset_entity in valid_subsets_by_id.items(): - profile = profiles_per_subset_id[subset_id] - profile_repre_names = profile.get("repre_names") - if not profile_repre_names: - continue - - # Lower names - profile_repre_names = [name.lower() for name in profile_repre_names] - - version_entity = version_by_subset_id[subset_id] - version_id = version_entity["_id"] - repres = repres_by_version_id[version_id] - for repre in repres: - repre_name_low = repre["name"].lower() - if repre_name_low in profile_repre_names: - valid_repres_by_subset_id[subset_id].append(repre) - - # DEBUG message - msg = "Valid representations for Asset: `{}`".format(asset_entity["name"]) - for subset_id, repres in valid_repres_by_subset_id.items(): - subset = valid_subsets_by_id[subset_id] - msg += "\n# Subset Name/ID: `{}`/{}".format(subset["name"], subset_id) - for repre in repres: - msg += "\n## Repre name: `{}`".format(repre["name"]) - - log.debug(msg) - - loaded_containers = { - "asset_entity": asset_entity, - "containers": [] - } - - for subset_id, repres in valid_repres_by_subset_id.items(): - subset_name = valid_subsets_by_id[subset_id]["name"] - - profile = profiles_per_subset_id[subset_id] - loaders_last_idx = len(profile["loaders"]) - 1 - repre_names_last_idx = len(profile["repre_names"]) - 1 - - is_loaded = False - for repre_name_idx, profile_repre_name in enumerate( - profile["repre_names"] - ): - found_repre = None - for repre in repres: - repre_name = repre["name"] - if repre_name == profile_repre_name: - found_repre = repre + # Check if any loader is available + loaders_match = False + for loader_name in profile_loaders: + if loader_name in loaders_by_name: + loaders_match = True break - if not found_repre: + if not loaders_match: + log.warning(( + "All loaders from Workfile profile are not available: {0}" + ).format(json.dumps(profile, indent=4))) continue - for loader_idx, loader_name in enumerate(profile["loaders"]): + # Check families + profile_families = profile.get("families") + if not profile_families: + log.warning(( + "Workfile profile is missing families configuration: {0}" + ).format(json.dumps(profile, indent=4))) + continue + + # Check representation names + profile_repre_names = profile.get("repre_names") + if not profile_repre_names: + log.warning(( + "Workfile profile is missing" + " representation names filtering: {0}" + ).format(json.dumps(profile, indent=4))) + continue + + # Prepare lowered families and representation names + profile["families_lowered"] = [ + fam.lower() for fam in profile_families + ] + profile["repre_names_lowered"] = [ + name.lower() for name in profile_repre_names + ] + + valid_profiles.append(profile) + + return valid_profiles + + @classmethod + def _prepare_profile_for_subsets(cls, subsets, profiles): + # Prepare subsets + subsets_by_family = map_subsets_by_family(subsets) + + profiles_per_subset_id = {} + for family, subsets in subsets_by_family.items(): + family_low = family.lower() + for profile in profiles: + # Skip profile if does not contain family + if family_low not in profile["families_lowered"]: + continue + + # Precompile name filters as regexes + profile_regexes = profile.get("subset_name_filters") + if profile_regexes: + _profile_regexes = [] + for regex in profile_regexes: + _profile_regexes.append(re.compile(regex)) + profile_regexes = _profile_regexes + + # TODO prepare regex compilation + for subset in subsets: + # Verify regex filtering (optional) + if profile_regexes: + valid = False + for pattern in profile_regexes: + if re.match(pattern, subset["name"]): + valid = True + break + + if not valid: + continue + + profiles_per_subset_id[subset["_id"]] = profile + + # break profiles loop on finding the first matching profile + break + return profiles_per_subset_id + + @classmethod + def load_containers_by_asset_data( + cls, asset_entity_data, build_profiles, loaders_by_name + ): + # Make sure all data are not empty + if not asset_entity_data or not build_profiles or not loaders_by_name: + return + + asset_entity = asset_entity_data["asset_entity"] + + valid_profiles = cls._filter_workfile_profiles( + build_profiles, loaders_by_name + ) + if not valid_profiles: + log.warning( + "There are not valid Workfile profiles. Skipping process." + ) + return + + log.debug("Valid Workfile profiles: {}".format(valid_profiles)) + + subsets_by_id = {} + version_by_subset_id = {} + repres_by_version_id = {} + for subset_id, in_data in asset_entity_data["subsets"].items(): + subset_entity = in_data["subset_entity"] + subsets_by_id[subset_entity["_id"]] = subset_entity + + version_data = in_data["version"] + version_entity = version_data["version_entity"] + version_by_subset_id[subset_id] = version_entity + repres_by_version_id[version_entity["_id"]] = ( + version_data["repres"] + ) + + if not subsets_by_id: + log.warning("There are not subsets for asset {0}".format( + asset_entity["name"] + )) + return + + profiles_per_subset_id = cls._prepare_profile_for_subsets( + subsets_by_id.values(), valid_profiles + ) + if not profiles_per_subset_id: + log.warning("There are not valid subsets.") + return + + valid_repres_by_subset_id = collections.defaultdict(list) + for subset_id, profile in profiles_per_subset_id.items(): + profile_repre_names = profile["repre_names_lowered"] + + version_entity = version_by_subset_id[subset_id] + version_id = version_entity["_id"] + repres = repres_by_version_id[version_id] + for repre in repres: + repre_name_low = repre["name"].lower() + if repre_name_low in profile_repre_names: + valid_repres_by_subset_id[subset_id].append(repre) + + # DEBUG message + msg = "Valid representations for Asset: `{}`".format( + asset_entity["name"] + ) + for subset_id, repres in valid_repres_by_subset_id.items(): + subset = subsets_by_id[subset_id] + msg += "\n# Subset Name/ID: `{}`/{}".format( + subset["name"], subset_id + ) + for repre in repres: + msg += "\n## Repre name: `{}`".format(repre["name"]) + + log.debug(msg) + + containers = cls._load_containers( + valid_repres_by_subset_id, subsets_by_id, + profiles_per_subset_id, loaders_by_name + ) + + return { + "asset_entity": asset_entity, + "containers": containers + } + + @classmethod + def _load_containers( + cls, repres_by_subset_id, subsets_by_id, + profiles_per_subset_id, loaders_by_name + ): + loaded_containers = [] + for subset_id, repres in repres_by_subset_id.items(): + subset_name = subsets_by_id[subset_id]["name"] + + profile = profiles_per_subset_id[subset_id] + loaders_last_idx = len(profile["loaders"]) - 1 + repre_names_last_idx = len(profile["repre_names_lowered"]) - 1 + + is_loaded = False + for repre_name_idx, profile_repre_name in enumerate( + profile["repre_names_lowered"] + ): + # Break iteration if representation was already loaded if is_loaded: break - loader = loaders_by_name.get(loader_name) - if not loader: + found_repre = None + for repre in repres: + repre_name = repre["name"].lower() + if repre_name == profile_repre_name: + found_repre = repre + break + + if not found_repre: continue - try: - container = avalon.api.load( - loader, - found_repre["_id"], - name=subset_name - ) - loaded_containers["containers"].append(container) - is_loaded = True - except Exception as exc: - if exc == pipeline.IncompatibleLoaderError: - log.info(( - "Loader `{}` is not compatible with" - " representation `{}`" - ).format(loader_name, repre["name"])) + for loader_idx, loader_name in enumerate(profile["loaders"]): + if is_loaded: + break - else: - log.error( - "Unexpected error happened during loading", - exc_info=True + loader = loaders_by_name.get(loader_name) + if not loader: + continue + try: + container = avalon.api.load( + loader, + found_repre["_id"], + name=subset_name ) + loaded_containers.append(container) + is_loaded = True - msg = "Loading failed." - if loader_idx < loaders_last_idx: - msg += " Trying next loader." - elif repre_name_idx < repre_names_last_idx: - msg += ( - " Loading of subset `{}` was not successful." - ).format(subset_name) - else: - msg += " Trying next representation." - log.info(msg) + except Exception as exc: + if exc == pipeline.IncompatibleLoaderError: + log.info(( + "Loader `{}` is not compatible with" + " representation `{}`" + ).format(loader_name, repre["name"])) - if is_loaded: - break + else: + log.error( + "Unexpected error happened during loading", + exc_info=True + ) - return loaded_containers + msg = "Loading failed." + if loader_idx < loaders_last_idx: + msg += " Trying next loader." + elif repre_name_idx < repre_names_last_idx: + msg += ( + " Loading of subset `{}` was not successful." + ).format(subset_name) + else: + msg += " Trying next representation." + log.info(msg) + return loaded_containers -def get_linked_assets(asset_entity): - """Return linked assets for `asset_entity`.""" - # TODO implement - return [] + @classmethod + def _collect_last_version_repres(cls, asset_entities): + """Collect subsets, versions and representations for asset_entities. + :param asset_entities: Asset entities for which want to find data + :type asset_entities: list + :return: collected entities + :rtype: dict -def collect_last_version_repres(asset_entities): - """Collect subsets, versions and representations for asset_entities. - - :param asset_entities: Asset entities for which want to find data - :type asset_entities: list - :return: collected entities - :rtype: dict - - Example output: - ``` - { - {Asset ID}: { - "asset_entity": , - "subsets": { - {Subset ID}: { - "subset_entity": , - "version": { - "version_entity": , - "repres": [ - , , ... - ] - } - }, - ... - } - }, - ... - } - output[asset_id]["subsets"][subset_id]["version"]["repres"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(io.find({ - "type": "subset", - "parent": {"$in": asset_entity_by_ids.keys()} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(io.find({ - "type": "version", - "parent": {"$in": subset_entity_by_ids.keys()} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = io.find({ - "type": "representation", - "parent": {"$in": last_versions_by_id.keys()} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... } - } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) + if not asset_entities: + return {} - return output + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + subsets = list(io.find({ + "type": "subset", + "parent": {"$in": asset_entity_by_ids.keys()} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} -def build_workfile(): - """Load representations for current context as containers into workfile. + sorted_versions = list(io.find({ + "type": "version", + "parent": {"$in": subset_entity_by_ids.keys()} + }).sort("name", -1)) - Loads latest versions of current and linked assets to workfile by logic - stored in Workfile profiles from presets. Variants are set by host, - filtered by current task name and used by families. + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version - Each family can specify representation names and loaders for - representations and first available and successful loaded representation is - returned as container. + repres = io.find({ + "type": "representation", + "parent": {"$in": last_versions_by_id.keys()} + }) - At the end you'll get list of loaded containers per each asset. + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] - loaded_containers [{ - "asset_entity": , - "containers": [, , ...] - }, { - "asset_entity": , - "containers": [, ...] - }, { - ... - }] - """ - # Get current asset name and entity - current_asset_name = io.Session["AVALON_ASSET"] - current_asset_entity = io.find_one({ - "type": "asset", - "name": current_asset_name - }) + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] - # Skip if asset was not found - if not current_asset_entity: - print("Asset entity with name `{}` was not found".format( - current_asset_name - )) - return + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] - # Prepare available loaders - loaders_by_name = {} - for loader in avalon.api.discover(avalon.api.Loader): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError("Duplicated loader name {0}!".format(loader_name)) - loaders_by_name[loader_name] = loader + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } - # Skip if there are any loaders - if not loaders_by_name: - log.warning("There are no registered loaders.") - return + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } - # Get current task name - current_task_name = io.Session["AVALON_TASK"] - current_task_name_low = current_task_name.lower() - # Load workfile presets for task - workfile_preset = get_workfile_build_preset(current_task_name_low) - - # Skip if there are any presets for task - if not workfile_preset: - log.warning( - "Current task `{}` does not have any loading preset.".format( - current_task_name + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre ) - ) - return - # Get presets for loading current asset - current_context = workfile_preset.get("current_context") - # Get presets for loading linked assets - link_context = workfile_preset.get("linked_assets") - # Skip if both are missing - if not current_context and not link_context: - log.warning("Current task `{}` has empty loading preset.".format( - current_task_name - )) - return - - elif not current_context: - log.warning(( - "Current task `{}` doesn't have any loading" - " preset for it's context." - ).format(current_task_name)) - - elif not link_context: - log.warning(( - "Current task `{}` doesn't have any" - "loading preset for it's linked assets." - ).format(current_task_name)) - - # Prepare assets to process by workfile presets - assets = [] - current_asset_id = None - if current_context: - # Add current asset entity if preset has current context set - assets.append(current_asset_entity) - current_asset_id = current_asset_entity["_id"] - - if link_context: - # Find and append linked assets if preset has set linked mapping - link_assets = get_linked_assets(current_asset_entity) - if link_assets: - assets.extend(link_assets) - - # Skip if there are no assets. This can happen if only linked mapping is - # set and there are no links for his asset. - if not assets: - log.warning("Asset does not have linked assets. Nothing to process.") - return - - # Prepare entities from database for assets - prepared_entities = collect_last_version_repres(assets) - - # Load containers by prepared entities and presets - loaded_containers = [] - # - Current asset containers - if current_asset_id and current_asset_id in prepared_entities: - current_context_data = prepared_entities.pop(current_asset_id) - loaded_data = load_containers_by_asset_data( - current_context_data, current_context, loaders_by_name - ) - if loaded_data: - loaded_containers.append(loaded_data) - - # - Linked assets container - for linked_asset_data in prepared_entities.values(): - loaded_data = load_containers_by_asset_data( - linked_asset_data, link_context, loaders_by_name - ) - if loaded_data: - loaded_containers.append(loaded_data) - - # Return list of loaded containers - return loaded_containers - - -def get_last_workfile_path(root, template, file_ext): - template = re.sub("<.*?>", ".*?", template) - template = re.sub("{version.*}", "([0-9]+)", template) - template = re.sub("{comment.*?}", ".+?", template) - # template = pipeline._format_work_template(template) - template = "^" + template + "$" - - all_file_names = [] - if os.path.exists(root): - all_file_names = os.listdir(root) - - filtered_file_names = [ - file_name for file_name in all_file_names - if os.path.splitext(file_name)[1] == file_ext - ] - - kwargs = {} - if platform.system() == "Windows": - kwargs["flags"] = re.IGNORECASE - - version = None - last_file_name = None - re_template = re.compile(template) - for file_name in sorted(filtered_file_names): - match = re.match(re_template, file_name, **kwargs) - if not match: - continue - - file_version = int(match.group(1)) - if file_version >= version: - last_file_name = file_name - version = file_version + 1 - - last_file_path = None - if last_file_name: - last_file_path = os.path.join(root, last_file_name) - - return last_file_path + return output diff --git a/pype/maya/menu.py b/pype/maya/menu.py index c25166f210..eee4347a72 100644 --- a/pype/maya/menu.py +++ b/pype/maya/menu.py @@ -4,7 +4,7 @@ import logging from avalon.vendor.Qt import QtWidgets, QtGui from avalon.maya import pipeline -from ..lib import build_workfile +from ..lib import BuildWorkfile import maya.cmds as cmds self = sys.modules[__name__] @@ -29,7 +29,7 @@ def deferred(): cmds.menuItem( "Build First Workfile", parent=pipeline._menu, - command=lambda *args: build_workfile() + command=lambda *args: BuildWorkfile.build_workfile() ) log.info("Attempting to install scripts menu..") From 20e3a6022404e4f481170e7106fdabc939a7cf48 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Apr 2020 13:20:45 +0200 Subject: [PATCH 78/86] removed unused modules --- pype/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index 48d9cb5965..e16643a472 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -11,7 +11,6 @@ import contextlib import subprocess import inspect from abc import ABCMeta, abstractmethod -import platform from avalon import io, pipeline import six From 871bfde9c8cea0296893aa062efe480ecccc54ab Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Apr 2020 14:14:30 +0200 Subject: [PATCH 79/86] added some documentation --- pype/lib.py | 91 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index e16643a472..965e738d1f 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -910,8 +910,8 @@ class BuildWorkfile: return per_task_preset @classmethod - def _filter_workfile_profiles(cls, workfile_profiles, loaders_by_name): - """ Filter and prepare workfile presets by available loaders. + def _filter_build_profiles(cls, build_profiles, loaders_by_name): + """ Filter build profiles by loaders and prepare process data. Valid profile must have "loaders", "families" and "repre_names" keys with valid values. @@ -923,14 +923,21 @@ class BuildWorkfile: Lowered "families" and "repre_names" are prepared for each profile with all required keys. + + :param build_profiles: Profiles for building workfile. + :type build_profiles: dict + :param loaders_by_name: Available loaders per name. + :type loaders_by_name: dict + :return: Filtered and prepared profiles. + :rtype: list """ valid_profiles = [] - for profile in workfile_profiles: + for profile in build_profiles: # Check loaders profile_loaders = profile.get("loaders") if not profile_loaders: log.warning(( - "Workfile profile has missing loaders configuration: {0}" + "Build profile has missing loaders configuration: {0}" ).format(json.dumps(profile, indent=4))) continue @@ -943,7 +950,7 @@ class BuildWorkfile: if not loaders_match: log.warning(( - "All loaders from Workfile profile are not available: {0}" + "All loaders from Build profile are not available: {0}" ).format(json.dumps(profile, indent=4))) continue @@ -951,7 +958,7 @@ class BuildWorkfile: profile_families = profile.get("families") if not profile_families: log.warning(( - "Workfile profile is missing families configuration: {0}" + "Build profile is missing families configuration: {0}" ).format(json.dumps(profile, indent=4))) continue @@ -959,7 +966,7 @@ class BuildWorkfile: profile_repre_names = profile.get("repre_names") if not profile_repre_names: log.warning(( - "Workfile profile is missing" + "Build profile is missing" " representation names filtering: {0}" ).format(json.dumps(profile, indent=4))) continue @@ -978,6 +985,22 @@ class BuildWorkfile: @classmethod def _prepare_profile_for_subsets(cls, subsets, profiles): + """Select profile for each subset byt it's data. + + Profiles are filtered for each subset individually. + Profile is filtered by subset's family, optionally by name regex and + representation names set in profile. + It is possible to not find matching profile for subset, in that case + subset is skipped and it is possible that none of subsets have + matching profile. + + :param subsets: Subset documents. + :type subsets: list + :param profiles: Build profiles. + :type profiles: dict + :return: Profile by subset's id. + :rtype: dict + """ # Prepare subsets subsets_by_family = map_subsets_by_family(subsets) @@ -1020,13 +1043,26 @@ class BuildWorkfile: def load_containers_by_asset_data( cls, asset_entity_data, build_profiles, loaders_by_name ): + """Load containers for entered asset entity by Build profiles. + + :param asset_entity_data: Prepared data with subsets, last version + and representations for specific asset. + :type asset_entity_data: dict + :param build_profiles: Build profiles. + :type build_profiles: dict + :param loaders_by_name: Available loaders per name. + :type loaders_by_name: dict + :return: Output contains asset document and loaded containers. + :rtype: dict + """ + # Make sure all data are not empty if not asset_entity_data or not build_profiles or not loaders_by_name: return asset_entity = asset_entity_data["asset_entity"] - valid_profiles = cls._filter_workfile_profiles( + valid_profiles = cls._filter_build_profiles( build_profiles, loaders_by_name ) if not valid_profiles: @@ -1105,6 +1141,29 @@ class BuildWorkfile: cls, repres_by_subset_id, subsets_by_id, profiles_per_subset_id, loaders_by_name ): + """Real load by collected data happens here. + + Loading of representations per subset happens here. Each subset can + loads one representation. Loading is tried in specific order. + Representations are tried to load by names defined in configuration. + If subset has representation matching representation name each loader + is tried to load it until any is successful. If none of them was + successful then next reprensentation name is tried. + Subset process loop ends when any representation is loaded or + all matching representations were already tried. + + :param repres_by_subset_id: Available representations mapped + by their parent (subset) id. + :type repres_by_subset_id: dict + :param subsets_by_id: Subset documents mapped by their id. + :type subsets_by_id: dict + :param profiles_per_subset_id: Build profiles mapped by subset id. + :type profiles_per_subset_id: dict + :param loaders_by_name: Available loaders per name. + :type loaders_by_name: dict + :return: Objects of loaded containers. + :rtype: list + """ loaded_containers = [] for subset_id, repres in repres_by_subset_id.items(): subset_name = subsets_by_id[subset_id]["name"] @@ -1113,6 +1172,10 @@ class BuildWorkfile: loaders_last_idx = len(profile["loaders"]) - 1 repre_names_last_idx = len(profile["repre_names_lowered"]) - 1 + repre_by_low_name = { + repre["name"].lower(): repre for repre in repres + } + is_loaded = False for repre_name_idx, profile_repre_name in enumerate( profile["repre_names_lowered"] @@ -1121,14 +1184,8 @@ class BuildWorkfile: if is_loaded: break - found_repre = None - for repre in repres: - repre_name = repre["name"].lower() - if repre_name == profile_repre_name: - found_repre = repre - break - - if not found_repre: + repre = repre_by_low_name.get(profile_repre_name) + if not repre: continue for loader_idx, loader_name in enumerate(profile["loaders"]): @@ -1141,7 +1198,7 @@ class BuildWorkfile: try: container = avalon.api.load( loader, - found_repre["_id"], + repre["_id"], name=subset_name ) loaded_containers.append(container) From 59e2f2a36d5aeb2ef26f2fac33be78d69209be63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Apr 2020 15:01:05 +0200 Subject: [PATCH 80/86] classmethods replaced with object methods and added process method where post processing can be implemented by host --- pype/lib.py | 45 ++++++++++++++++++++++++--------------------- pype/maya/menu.py | 2 +- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 965e738d1f..d3ccbc8589 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -736,10 +736,19 @@ class BuildWorkfile: are host related, since each host has it's loaders. """ - @classmethod - def build_workfile(cls): + def process(self): """Main method of this wrapper. + Building of workfile is triggered and is possible to implement + post processing of loaded containers if necessary. + """ + containers = self.build_workfile() + + return containers + + def build_workfile(self): + """Prepares and load containers into workfile. + Loads latest versions of current and linked assets to workfile by logic stored in Workfile profiles from presets. Profiles are set by host, filtered by current task name and used by families. @@ -793,7 +802,7 @@ class BuildWorkfile: current_task_name = io.Session["AVALON_TASK"] # Load workfile presets for task - build_presets = cls.get_build_presets(current_task_name) + build_presets = self.get_build_presets(current_task_name) # Skip if there are any presets for task if not build_presets: @@ -850,14 +859,14 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = cls._collect_last_version_repres(assets) + prepared_entities = self._collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] # - Current asset containers if current_asset_id and current_asset_id in prepared_entities: current_context_data = prepared_entities.pop(current_asset_id) - loaded_data = cls.load_containers_by_asset_data( + loaded_data = self.load_containers_by_asset_data( current_context_data, current_context_profiles, loaders_by_name ) if loaded_data: @@ -865,7 +874,7 @@ class BuildWorkfile: # - Linked assets container for linked_asset_data in prepared_entities.values(): - loaded_data = cls.load_containers_by_asset_data( + loaded_data = self.load_containers_by_asset_data( linked_asset_data, link_context_profiles, loaders_by_name ) if loaded_data: @@ -874,8 +883,7 @@ class BuildWorkfile: # Return list of loaded containers return loaded_containers - @classmethod - def get_build_presets(cls, task_name): + def get_build_presets(self, task_name): """ Returns presets to build workfile for task name. Presets are loaded for current project set in @@ -909,8 +917,7 @@ class BuildWorkfile: return per_task_preset - @classmethod - def _filter_build_profiles(cls, build_profiles, loaders_by_name): + def _filter_build_profiles(self, build_profiles, loaders_by_name): """ Filter build profiles by loaders and prepare process data. Valid profile must have "loaders", "families" and "repre_names" keys @@ -983,8 +990,7 @@ class BuildWorkfile: return valid_profiles - @classmethod - def _prepare_profile_for_subsets(cls, subsets, profiles): + def _prepare_profile_for_subsets(self, subsets, profiles): """Select profile for each subset byt it's data. Profiles are filtered for each subset individually. @@ -1039,9 +1045,8 @@ class BuildWorkfile: break return profiles_per_subset_id - @classmethod def load_containers_by_asset_data( - cls, asset_entity_data, build_profiles, loaders_by_name + self, asset_entity_data, build_profiles, loaders_by_name ): """Load containers for entered asset entity by Build profiles. @@ -1062,7 +1067,7 @@ class BuildWorkfile: asset_entity = asset_entity_data["asset_entity"] - valid_profiles = cls._filter_build_profiles( + valid_profiles = self._filter_build_profiles( build_profiles, loaders_by_name ) if not valid_profiles: @@ -1093,7 +1098,7 @@ class BuildWorkfile: )) return - profiles_per_subset_id = cls._prepare_profile_for_subsets( + profiles_per_subset_id = self._prepare_profile_for_subsets( subsets_by_id.values(), valid_profiles ) if not profiles_per_subset_id: @@ -1126,7 +1131,7 @@ class BuildWorkfile: log.debug(msg) - containers = cls._load_containers( + containers = self._load_containers( valid_repres_by_subset_id, subsets_by_id, profiles_per_subset_id, loaders_by_name ) @@ -1136,9 +1141,8 @@ class BuildWorkfile: "containers": containers } - @classmethod def _load_containers( - cls, repres_by_subset_id, subsets_by_id, + self, repres_by_subset_id, subsets_by_id, profiles_per_subset_id, loaders_by_name ): """Real load by collected data happens here. @@ -1230,8 +1234,7 @@ class BuildWorkfile: return loaded_containers - @classmethod - def _collect_last_version_repres(cls, asset_entities): + def _collect_last_version_repres(self, asset_entities): """Collect subsets, versions and representations for asset_entities. :param asset_entities: Asset entities for which want to find data diff --git a/pype/maya/menu.py b/pype/maya/menu.py index eee4347a72..12eca6d4d9 100644 --- a/pype/maya/menu.py +++ b/pype/maya/menu.py @@ -29,7 +29,7 @@ def deferred(): cmds.menuItem( "Build First Workfile", parent=pipeline._menu, - command=lambda *args: BuildWorkfile.build_workfile() + command=lambda *args: BuildWorkfile().build_workfile() ) log.info("Attempting to install scripts menu..") From 1b28e58c62e62ada29a004f60e40edaecf8c6aa1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Apr 2020 15:10:19 +0200 Subject: [PATCH 81/86] changed method which is called in maya --- pype/maya/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/maya/menu.py b/pype/maya/menu.py index 12eca6d4d9..70df50b9e6 100644 --- a/pype/maya/menu.py +++ b/pype/maya/menu.py @@ -29,7 +29,7 @@ def deferred(): cmds.menuItem( "Build First Workfile", parent=pipeline._menu, - command=lambda *args: BuildWorkfile().build_workfile() + command=lambda *args: BuildWorkfile().process() ) log.info("Attempting to install scripts menu..") From 05b33a20ce2e9cd905a92665f0f9f2339d1aedc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 8 Apr 2020 11:45:51 +0200 Subject: [PATCH 82/86] code cleanup --- pype/plugins/maya/load/load_yeti_cache.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pype/plugins/maya/load/load_yeti_cache.py b/pype/plugins/maya/load/load_yeti_cache.py index 286b154ca2..412c2bd558 100644 --- a/pype/plugins/maya/load/load_yeti_cache.py +++ b/pype/plugins/maya/load/load_yeti_cache.py @@ -105,7 +105,6 @@ class YetiCacheLoader(api.Loader): io.install() namespace = container["namespace"] container_node = container["objectName"] - # import web_pdb; web_pdb.set_trace() fur_settings = io.find_one( {"parent": representation["parent"], "name": "fursettings"} @@ -120,8 +119,6 @@ class YetiCacheLoader(api.Loader): settings_fname = api.get_representation_path(fur_settings) path = api.get_representation_path(representation) # Get all node data - # fname, ext = os.path.splitext(path) - # settings_fname = "{}.fursettings".format(fname) with open(settings_fname, "r") as fp: settings = json.load(fp) @@ -213,7 +210,6 @@ class YetiCacheLoader(api.Loader): yeti_node = yeti_nodes[0] for attr, value in data["attrs"].items(): - # import web_pdb; web_pdb.set_trace() # handle empty attribute strings. Those are reported # as None, so their type is NoneType and this is not # supported on attributes in Maya. We change it to From f6e329fac6347080ad936dca203f81837b78d763 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 16 Apr 2020 23:55:43 +0200 Subject: [PATCH 83/86] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 31 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..6ed6ae428c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. windows] + - Host: [e.g. Maya, Nuke, Houdini] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..11fc491ef1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From f1765e5aa146aeccd0d88c5273ade09c14f29501 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 20 Apr 2020 22:14:07 +0200 Subject: [PATCH 84/86] bump version --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 2614ce9d96..892994aa6c 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.7.0" +__version__ = "2.8.0" From f23fd2ab1bd20516f3d7b8774cccbd0e4b284f3e Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 20 Apr 2020 22:17:12 +0200 Subject: [PATCH 85/86] fix possibility of version missing from review --- pype/plugins/nukestudio/publish/collect_reviews.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/publish/collect_reviews.py b/pype/plugins/nukestudio/publish/collect_reviews.py index 7cf8d77de4..b91d390e2e 100644 --- a/pype/plugins/nukestudio/publish/collect_reviews.py +++ b/pype/plugins/nukestudio/publish/collect_reviews.py @@ -151,13 +151,16 @@ class CollectReviews(api.InstancePlugin): "handleStart", "handleEnd", "sourceIn", "sourceOut", "frameStart", "frameEnd", "sourceInH", "sourceOutH", "clipIn", "clipOut", "clipInH", "clipOutH", "asset", - "track", "version" + "track" ] version_data = dict() # pass data to version version_data.update({k: instance.data[k] for k in transfer_data}) + if 'version' in instance.data: + version_data["version"] = instance.data[version] + # add to data of representation version_data.update({ "colorspace": item.sourceMediaColourTransform(), From 3ce24febff017a1677cde6ac6634112619aead7d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 20 Apr 2020 22:26:12 +0200 Subject: [PATCH 86/86] remove hardcoded render tempalte --- pype/plugins/global/publish/extract_burnin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 01fdaed54b..7668eafd2a 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -132,13 +132,14 @@ class ExtractBurnin(pype.api.Extractor): slate_duration = duration_cp # exception for slate workflow - if ("slate" in instance.data["families"]): + if "slate" in instance.data["families"]: if "slate-frame" in repre.get("tags", []): slate_frame_start = frame_start_cp - 1 slate_frame_end = frame_end_cp slate_duration = duration_cp + 1 - self.log.debug("__1 slate_frame_start: {}".format(slate_frame_start)) + self.log.debug("__1 slate_frame_start: {}".format( + slate_frame_start)) _prep_data.update({ "slate_frame_start": slate_frame_start, @@ -192,7 +193,6 @@ class ExtractBurnin(pype.api.Extractor): self.log.debug("Output: {}".format(output)) repre_update = { - "anatomy_template": "render", "files": movieFileBurnin, "name": repre["name"], "tags": [x for x in repre["tags"] if x != "delete"]