mirror of
https://git.lynn.is/Gwen/python-layout.git
synced 2024-01-13 01:31:55 +01:00
401 lines
16 KiB
Python
401 lines
16 KiB
Python
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()
|