import math from PIL import Image, ImageDraw import layout from layout.internal.helpers import line_intersection class Layout: def __init__( self, width=None, height=None, min_width=None, min_height=None, max_width=None, max_height=None, fg_color=None, bg_color=None, border_color=None, border_width=0, border_radius=0, padding=0, font=None, overflow=None, left=None, top=None, right=None, bottom=None, flex_grow=0, flex_shrink=0, debug_layout=False, ): self.width = width self.height = height self.min_width = min_width self.min_height = min_height self.max_width = max_width self.max_height = max_height self.fg_color = fg_color self.bg_color = bg_color self.border_color = border_color self.border_width = border_width self.border_radius = border_radius self.padding = padding self.font = font self.overflow = overflow self.left = left self.top = top self.right = right self.bottom = bottom self.flex_grow = flex_grow self.flex_shrink = flex_shrink self.debug_layout = debug_layout self.container = None def complete_init(self, container): self.container = container for c in self.children(): c.complete_init(self) def children(self): return [] def get_document_size(self): if isinstance(self, layout.Document): return self.actual_size else: return self.container.get_document_size() def get_fg_color(self): if self.fg_color is not None: return self.fg_color elif self.container is not None: return self.container.get_fg_color() else: return None def get_font(self): if self.font is not None: return self.font elif self.container is not None: return self.container.get_font() else: raise Exception('no font defined in {0}'.format(self.__class__)) def get_overflow(self): if self.overflow is not None: return self.overflow elif self.container is not None: return self.container.get_overflow() else: return None def get_tuple_property(self, name, default, allow_tuple=True): value = getattr(self, name) if isinstance(value, list) or (allow_tuple and isinstance(value, tuple)): if len(value) == 0: return default, default, default, default elif len(value) == 1: return value[0], value[0], value[0], value[0] elif len(value) == 2: return tuple(value + value) elif len(value) == 3: return value[0], value[1], value[2], value[1] else: return tuple(value[:4]) elif value is None: return default, default, default, default else: return value, value, value, value def get_padding(self): return self.get_tuple_property('padding', 0) def get_border_width(self): return self.get_tuple_property('border_width', 0) def get_border_color(self): return self.get_tuple_property('border_color', None, False) def get_border_radius(self): return self.get_tuple_property('border_radius', None) def get_min_inner_width(self, max_height=None): return self.min_width def get_min_inner_height(self, max_width=None): return self.min_height def get_ideal_inner_dimensions(self, min_width=None, min_height=None, available_width=None, available_height=None): return self.width, self.height def get_x_padding_border_size(self): border, padding = self.get_border_width(), self.get_padding() return border[0] + border[2] + padding[0] + padding[2] def get_y_padding_border_size(self): border, padding = self.get_border_width(), self.get_padding() return border[1] + border[3] + padding[1] + padding[3] def get_min_outer_width(self, max_outer_height=None): if max_outer_height is not None: max_height = max_outer_height - self.get_y_padding_border_size() else: max_height = None return self.get_min_inner_width(max_height) + self.get_x_padding_border_size() def get_min_outer_height(self, max_outer_width=None): if max_outer_width is not None: max_width = max_outer_width - self.get_x_padding_border_size() else: max_width = None return self.get_min_inner_height(max_width) + self.get_y_padding_border_size() def get_ideal_outer_dimensions(self, min_width=None, min_height=None, available_width=None, available_height=None): x = self.get_x_padding_border_size() y = self.get_y_padding_border_size() if min_width is not None: min_width -= x if available_width is not None: available_width -= x if min_height is not None: min_height -= y if available_height is not None: available_height -= y w, h = self.get_ideal_inner_dimensions(min_width, min_height, available_width, available_height) return w + x, h + y def make_canvas(self): return Image.new('RGBA', self.get_document_size(), (0, 0, 0, 0)) def make_border_corner_mask(self, radius_x, radius_y, x, y): mask = Image.new('1', (radius_x, radius_y), 1) d = ImageDraw.Draw(mask) x1 = x * -radius_x y1 = y * -radius_y x2 = (2 - x) * radius_x y2 = (2 - y) * radius_y d.ellipse((x1, y1, x2 - 1, y2 - 1), fill=0) return mask def make_border_outside_mask(self, rect, border_radii): x1, y1, x2, y2 = rect mask = Image.new('1', self.get_document_size(), 0) d = ImageDraw.Draw(mask) d.rectangle(rect, fill=1) radius_top_left, radius_top_right, radius_bottom_right, radius_bottom_left = border_radii if radius_top_left > 0: mask_top_left = self.make_border_corner_mask(radius_top_left, radius_top_left, 0, 0) mask.paste(0, (x1, y1), mask=mask_top_left) if radius_top_right > 0: mask_top_right = self.make_border_corner_mask(radius_top_right, radius_top_right, 1, 0) mask.paste(0, (x2 - radius_top_right + 1, y1), mask=mask_top_right) if radius_bottom_right > 0: mask_bottom_right = self.make_border_corner_mask(radius_bottom_right, radius_bottom_right, 1, 1) mask.paste(0, (x2 - radius_bottom_right + 1, y2 - radius_bottom_right + 1), mask=mask_bottom_right) if radius_bottom_left > 0: mask_bottom_left = self.make_border_corner_mask(radius_bottom_left, radius_bottom_left, 0, 1) mask.paste(0, (x1, y2 - radius_bottom_left + 1), mask=mask_bottom_left) return mask def make_border_inside_mask(self, rect, border_radii): mask = Image.new('1', self.get_document_size(), 0) x1, y1, x2, y2 = rect width_left, width_top, width_right, width_bottom = self.get_border_width() radius_top_left, radius_top_right, radius_bottom_right, radius_bottom_left = border_radii inner_rect = (x1 + width_left, y1 + width_top, x2 - width_right, y2 - width_bottom) d = ImageDraw.Draw(mask) d.rectangle(inner_rect, fill=1) if radius_top_left > width_top and radius_top_left > width_left: mask_top_left = self.make_border_corner_mask(radius_top_left - width_left, radius_top_left - width_top, 0, 0) mask.paste(0, (x1 + width_left, y1 + width_top), mask=mask_top_left) if radius_top_right > width_top and radius_top_right > width_right: mask_top_right = self.make_border_corner_mask(radius_top_right - width_right, radius_top_right - width_top, 1, 0) mask.paste(0, (x2 - radius_top_right + 1, y1 + width_top), mask=mask_top_right) if radius_bottom_right > width_bottom and radius_bottom_right > width_right: mask_bottom_right = self.make_border_corner_mask(radius_bottom_right - width_right, radius_bottom_right - width_bottom, 1, 1) mask.paste(0, (x2 - radius_bottom_right + 1, y2 - radius_bottom_right + 1), mask=mask_bottom_right) if radius_bottom_left > width_bottom and radius_bottom_left > width_left: mask_bottom_right = self.make_border_corner_mask(radius_bottom_left - width_left, radius_bottom_left - width_bottom, 0, 1) mask.paste(0, (x1 + width_left, y2 - radius_bottom_left + 1), mask=mask_bottom_right) return mask def make_border_color_image(self, rect, border_radii): x1, y1, x2, y2 = rect color_left, color_top, color_right, color_bottom = self.get_border_color() width_left, width_top, width_right, width_bottom = self.get_border_width() radius_top_left, radius_top_right, radius_bottom_right, radius_bottom_left = border_radii image = self.make_canvas() d = ImageDraw.Draw(image) if width_left > 0: p1 = (x1, y1) p1_ = (x1 + width_left, y1 + width_top) p2 = (x1, y2) p2_ = (x1 + width_left, y2 - width_bottom) left = x1 + max(width_left, radius_top_left, radius_bottom_left) intersect = line_intersection((p1, p1_), (p2, p2_)) left = min(left, intersect[0]) left_line = ((left, 0), (left, 1)) intersect_1 = line_intersection((p1, p1_), left_line) intersect_2 = line_intersection((p2, p2_), left_line) d.polygon([p1, intersect_1, intersect_2, p2], fill=color_left) if width_top > 0: p1 = (x1, y1) p1_ = (x1 + width_left, y1 + width_top) p2 = (x2, y1) p2_ = (x2 - width_right, y1 + width_top) top = y1 + max(width_top, radius_top_left, radius_top_right) intersect = line_intersection((p1, p1_), (p2, p2_)) top = min(top, intersect[1]) top_line = ((0, top), (1, top)) intersect_1 = line_intersection((p1, p1_), top_line) intersect_2 = line_intersection((p2, p2_), top_line) d.polygon([p1, intersect_1, intersect_2, p2], fill=color_top) if width_right > 0: p1 = (x2, y1) p1_ = (x2 - width_right, y1 + width_top) p2 = (x2, y2) p2_ = (x2 - width_right, y2 - width_bottom) right = x2 - max(width_right, radius_top_right, radius_bottom_right) intersect = line_intersection((p1, p1_), (p2, p2_)) right = max(right, intersect[0]) right_line = ((right, 0), (right, 1)) intersect_1 = line_intersection((p1, p1_), right_line) intersect_2 = line_intersection((p2, p2_), right_line) d.polygon([p1, intersect_1, intersect_2, p2], fill=color_right) if width_bottom > 0: p1 = (x1, y2) p1_ = (x1 + width_left, y2 - width_bottom) p2 = (x2, y2) p2_ = (x2 - width_right, y2 - width_bottom) bottom = y2 - max(width_bottom, radius_bottom_right, radius_bottom_left) intersect = line_intersection((p1, p1_), (p2, p2_)) bottom = max(bottom, intersect[1]) bottom_line = ((0, bottom), (1, bottom)) intersect_1 = line_intersection((p1, p1_), bottom_line) intersect_2 = line_intersection((p2, p2_), bottom_line) d.polygon([p1, intersect_1, intersect_2, p2], fill=color_bottom) return image def actual_border_radii(self, rect): x1, y1, x2, y2 = rect width, height = x2 - x1 + 1, y2 - y1 + 1 border_radii = self.get_border_radius() radius_sum_top = border_radii[0] + border_radii[1] radius_sum_right = border_radii[1] + border_radii[2] radius_sum_bottom = border_radii[2] + border_radii[3] radius_sum_left = border_radii[3] + border_radii[0] radius_factor = 1 if radius_sum_top > width: radius_factor = min(radius_factor, width / radius_sum_top) if radius_sum_bottom > width: radius_factor = min(radius_factor, width / radius_sum_bottom) if radius_sum_left > height: radius_factor = min(radius_factor, height / radius_sum_left) if radius_sum_right > height: radius_factor = min(radius_factor, height / radius_sum_right) return tuple([int(r * radius_factor) for r in border_radii]) def render_base(self, image, rect): pass def render_after_background(self, image, rect): pass def render_after_border(self, image, rect): pass def render_after_content(self, image, rect): pass def modify_outside_mask(self, outside_mask, rect): pass def modify_inside_mask(self, inside_mask, rect): pass def modify_border_mask(self, border_mask, rect): pass def render(self, rect): image = self.make_canvas() self.render_base(image, rect) border_radii = self.actual_border_radii(rect) outside_mask = self.make_border_outside_mask(rect, border_radii) inside_mask = self.make_border_inside_mask(rect, border_radii) # this should composite the outside mask over a black background, with inside_mask as the alpha channel it is # somewhat unclear to me why this works, as I would expect (intuitively and from the documentation of related # functions) that pixels that are 0 in the alpha channel get copied from the black background and pixels that # are 1 in the alpha channel get copied from the outside mask, but apparently it is the other way round? border_mask = Image.composite(0, outside_mask, mask=inside_mask) self.modify_outside_mask(outside_mask, rect) self.modify_inside_mask(inside_mask, rect) self.modify_border_mask(border_mask, rect) if self.bg_color is not None: image.paste(self.bg_color, mask=outside_mask) self.render_after_background(image, rect) border_color = self.make_border_color_image(rect, border_radii) border_image = self.make_canvas() border_image.paste(border_color, mask=border_mask) image.alpha_composite(border_image) self.render_after_border(image, rect) border_width = self.get_border_width() padding = self.get_padding() content_x1 = rect[0] + border_width[0] + padding[0] content_y1 = rect[1] + border_width[1] + padding[1] content_x2 = rect[2] - border_width[2] - padding[2] content_y2 = rect[3] - border_width[3] - padding[3] content_rect = (content_x1, content_y1, content_x2, content_y2) content_image = self.render_content(content_rect) if not self.get_overflow(): actual_content_image = content_image content_image = self.make_canvas() content_image.paste(actual_content_image, mask=inside_mask) image.alpha_composite(content_image) self.render_after_content(image, rect) if self.debug_layout: dc = Image.new('RGBA', image.size, (0, 0, 0, 0)) d = ImageDraw.Draw(dc) # outside margin d.rectangle(rect, outline=(255, 0, 0, 50), width=1) # border (this does not account for actual border width) d.rectangle((rect[0] + border_width[0], rect[1] + border_width[1], rect[2] - border_width[2], rect[3] - border_width[3]), outline=(0, 0, 255, 50), width=1) # inside padding d.rectangle(content_rect, outline=(0, 255, 0, 50), width=1) image.alpha_composite(dc) return image def render_content(self, rect): return self.make_canvas()