mirror of
https://git.lynn.is/Gwen/python-layout.git
synced 2024-01-13 01:31:55 +01:00
200 lines
8.6 KiB
Python
200 lines
8.6 KiB
Python
import unicodedata
|
|
|
|
from .. import TextWrap, TextVerticalAlign, TextAlign
|
|
|
|
|
|
class TextLayouter():
|
|
|
|
def __init__(self,
|
|
lines,
|
|
align,
|
|
vertical_align,
|
|
wrap,
|
|
font,
|
|
line_height
|
|
):
|
|
self._lines = lines
|
|
self._align = align
|
|
self._vertical_align = vertical_align
|
|
self._wrap = wrap
|
|
self._font = font
|
|
self._line_height = line_height
|
|
|
|
# returns a list of tuples (x, y, text)
|
|
def get_lines(self, available_width=None, available_height=None):
|
|
lines = self._wrap_lines(available_width)
|
|
if len(lines) == 0:
|
|
return []
|
|
|
|
content_width = max([self._font.getbbox(line)[2] for line in lines])
|
|
content_height = len(lines) * self._line_height
|
|
|
|
if available_height is None:
|
|
available_height = content_height
|
|
if available_width is None:
|
|
available_width = content_width
|
|
|
|
positioned_lines = []
|
|
y = 0
|
|
if self._vertical_align == TextVerticalAlign.MIDDLE:
|
|
y = (available_height - content_height) // 2
|
|
elif self._vertical_align == TextVerticalAlign.BOTTOM:
|
|
y = available_height - content_height
|
|
for line in lines:
|
|
ly = y
|
|
line_width = self._font.getbbox(line)[2]
|
|
if self._align == TextAlign.CENTER:
|
|
lx = (available_width - line_width) // 2
|
|
elif self._align == TextAlign.RIGHT:
|
|
lx = available_width - line_width
|
|
else:
|
|
lx = 0
|
|
positioned_lines.append((lx, ly, line))
|
|
y += self._line_height
|
|
return positioned_lines
|
|
|
|
def get_dimensions(self, max_width=None, max_height=None):
|
|
if max_width == 0:
|
|
width = 0
|
|
cnt = 0
|
|
for line in self._lines:
|
|
for ch in line:
|
|
if len(ch.strip()) > 0:
|
|
w = self._text_width(ch)
|
|
width = max(width, w)
|
|
cnt += 1
|
|
height = cnt * self._line_height
|
|
else:
|
|
lines = [l[2] for l in self.get_lines(max_width, max_height)]
|
|
if len(lines) > 0:
|
|
width = max([self._font.getbbox(line)[2] for line in lines])
|
|
else:
|
|
width = 0
|
|
height = len(lines) * self._line_height
|
|
return width, height
|
|
|
|
def _text_width(self, text):
|
|
return self._font.getbbox(text)[2]
|
|
|
|
def _trim_line(self, line):
|
|
if self._align == TextAlign.LEFT:
|
|
return line.rstrip()
|
|
elif self._align == TextAlign.CENTER:
|
|
return line.strip()
|
|
elif self._align == TextAlign.RIGHT:
|
|
return line.lstrip()
|
|
|
|
def _wrap_lines_everywhere(self, max_width):
|
|
lines = []
|
|
for line in self._lines:
|
|
remaining = self._trim_line(line)
|
|
while len(remaining) > 0:
|
|
width_remaining = self._text_width(remaining)
|
|
if width_remaining <= max_width:
|
|
lines.append(remaining)
|
|
remaining = ''
|
|
else:
|
|
part_length = len(remaining) * max_width // width_remaining
|
|
while True:
|
|
width = self._text_width(self._trim_line(remaining[:part_length]))
|
|
if width <= max_width:
|
|
break
|
|
part_length -= 1
|
|
while True:
|
|
width = self._text_width(self._trim_line(remaining[:part_length + 1]))
|
|
if width > max_width:
|
|
break
|
|
part_length += 1
|
|
# if the max width is less than fits a character, still put a character in the line
|
|
# otherwise the algorithm would never finish and no layouting would be possible
|
|
if part_length == 0:
|
|
part_length = 1
|
|
|
|
lines.append(self._trim_line(remaining[:part_length]))
|
|
remaining = self._trim_line(remaining[part_length:])
|
|
return lines
|
|
|
|
def _get_word_breakpoints(self, line):
|
|
break_after = ['SPACE', 'EN QUAD', 'EM QUAD', 'EN SPACE', 'EM SPACE', 'THREE-PER-EM SPACE',
|
|
'FOUR-PER-EM SPACE', 'SIX-PER-EM SPACE', 'PUNCTUATION SPACE', 'THIN SPACE', 'HAIR SPACE',
|
|
'MEDIUM MATHEMATICAL SPACE', 'IDEOGRAPHIC SPACE', 'TAB', 'ARMENIAN HYPHEN', 'HYPHEN',
|
|
'FIGURE DASH', 'EN DASH', 'HYPHENATION POINT', 'VERTICAL LINE']
|
|
no_break_before = ['SMALL COMMA', 'SMALL FULL STOP', 'FULLWIDTH COMMA', 'FULLWIDTH FULL STOP', 'COMMA',
|
|
'FULL STOP', 'HORIZONTAL ELLIPSIS', 'NON-BREAKING HYPHEN']
|
|
breaks = []
|
|
no_breaks = []
|
|
for i, ch in enumerate(line):
|
|
if unicodedata.name(ch) in break_after:
|
|
if i + 1 < len(line) or unicodedata.name(line[i + 1]) not in no_break_before:
|
|
breaks.append(i + 1)
|
|
if unicodedata.name(ch) in no_break_before:
|
|
no_breaks.append(i)
|
|
return breaks, no_breaks
|
|
|
|
def _wrap_lines_on_words(self, max_width, allow_inside_word):
|
|
lines = []
|
|
for line in self._lines:
|
|
breakpoints, no_breaks = self._get_word_breakpoints(line)
|
|
breakpoints.append(len(line))
|
|
# e.g. if the string is 'asdf' then a breakpoint at 3 means the line can be broken into 'asd' and 'f'
|
|
previous_break = 0
|
|
while previous_break < len(line):
|
|
last_successful_break = None
|
|
first_unsuccessful_break = None
|
|
for bp in breakpoints:
|
|
if bp <= previous_break:
|
|
continue
|
|
width = self._text_width(self._trim_line(line[previous_break:bp]))
|
|
if width <= max_width:
|
|
last_successful_break = bp
|
|
else:
|
|
first_unsuccessful_break = bp
|
|
break
|
|
if last_successful_break is not None:
|
|
lines.append(self._trim_line(line[previous_break:last_successful_break]))
|
|
previous_break = last_successful_break
|
|
else:
|
|
if self._text_width(self._trim_line(line[previous_break:])) < max_width:
|
|
lines.append(self._trim_line(line[previous_break:]))
|
|
previous_break = len(line)
|
|
else:
|
|
end = first_unsuccessful_break
|
|
if not allow_inside_word:
|
|
lines.append(self._trim_line(line[previous_break:end]))
|
|
previous_break = end
|
|
else:
|
|
word = self._trim_line(line[previous_break:end])
|
|
word_width = self._text_width(word)
|
|
beginning_length = len(word) * max_width // word_width
|
|
while True:
|
|
change = 1
|
|
while previous_break + beginning_length - change in no_breaks:
|
|
change += 1
|
|
beginning_length -= change
|
|
width = self._text_width(self._trim_line(word[:beginning_length]))
|
|
if width <= max_width:
|
|
break
|
|
while True:
|
|
change = 1
|
|
while previous_break + beginning_length + change in no_breaks:
|
|
change += 1
|
|
width = self._text_width(self._trim_line(word[:beginning_length + change]))
|
|
if width > max_width and beginning_length > 0:
|
|
break
|
|
beginning_length += change
|
|
if beginning_length == 0:
|
|
beginning_length = 1
|
|
|
|
lines.append(self._trim_line(word[:beginning_length]))
|
|
previous_break = previous_break + beginning_length
|
|
return lines
|
|
|
|
def _wrap_lines(self, max_width):
|
|
if self._wrap == TextWrap.NO_WRAP or max_width is None:
|
|
return self._lines
|
|
|
|
if self._wrap == TextWrap.EVERYWHERE:
|
|
return self._wrap_lines_everywhere(max_width)
|
|
else:
|
|
return self._wrap_lines_on_words(max_width, self._wrap == TextWrap.PREFER_WORDS)
|