1
0
Fork 0
mirror of https://git.lynn.is/Gwen/python-layout.git synced 2024-01-13 01:31:55 +01:00
python-layout/pillow_layout/layout.py
2023-02-07 23:54:13 +01:00

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