reorganized slates a little bit

This commit is contained in:
iLLiCiTiT 2020-01-16 16:33:15 +01:00
parent 4a8ab6fac8
commit 6ef5853b65
12 changed files with 1520 additions and 1902 deletions

File diff suppressed because it is too large Load diff

View file

@ -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": {
}
}

114
pype/scripts/slate/lib.py Normal file
View file

@ -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")

View file

@ -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()

View file

@ -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 <content_width> is not implemented <{}>".format(
self.__class__.__name__
)
)
def value_height(self):
raise NotImplementedError(
"Attribute <content_width> 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()

View file

@ -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
}
}

View file

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

View file

@ -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
)

View file

@ -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)

View file

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

View file

@ -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,
}