PCQRSCANER/venv/Lib/site-packages/pptx/text/layout.py

322 lines
9.7 KiB
Python
Raw Permalink Normal View History

2019-12-22 21:51:47 +01:00
# encoding: utf-8
"""
Objects related to layout of rendered text, such as TextFitter.
"""
from __future__ import absolute_import, print_function
from PIL import ImageFont
class TextFitter(tuple):
"""
Value object that knows how to fit text into given rectangular extents.
"""
def __new__(cls, line_source, extents, font_file):
width, height = extents
return tuple.__new__(cls, (line_source, width, height, font_file))
@classmethod
def best_fit_font_size(cls, text, extents, max_size, font_file):
"""
Return the largest whole-number point size less than or equal to
*max_size* that allows *text* to fit completely within *extents* when
rendered using font defined in *font_file*.
"""
line_source = _LineSource(text)
text_fitter = cls(line_source, extents, font_file)
return text_fitter._best_fit_font_size(max_size)
def _best_fit_font_size(self, max_size):
"""
Return the largest whole-number point size less than or equal to
*max_size* that this fitter can fit.
"""
predicate = self._fits_inside_predicate
sizes = _BinarySearchTree.from_ordered_sequence(range(1, int(max_size) + 1))
return sizes.find_max(predicate)
def _break_line(self, line_source, point_size):
"""
Return a (line, remainder) pair where *line* is the longest line in
*line_source* that will fit in this fitter's width and *remainder* is
a |_LineSource| object containing the text following the break point.
"""
lines = _BinarySearchTree.from_ordered_sequence(line_source)
predicate = self._fits_in_width_predicate(point_size)
return lines.find_max(predicate)
def _fits_in_width_predicate(self, point_size):
"""
Return a function taking a text string value and returns |True| if
that text fits in this fitter when rendered at *point_size*. Used as
predicate for _break_line()
"""
def predicate(line):
"""
Return |True| if *line* fits in this fitter when rendered at
*point_size*.
"""
cx = _rendered_size(line.text, point_size, self._font_file)[0]
return cx <= self._width
return predicate
@property
def _fits_inside_predicate(self):
"""
Return a function taking an integer point size argument that returns
|True| if the text in this fitter can be wrapped to fit entirely
within its extents when rendered at that point size.
"""
def predicate(point_size):
"""
Return |True| if the text in *line_source* can be wrapped to fit
entirely within *extents* when rendered at *point_size* using the
font defined in *font_file*.
"""
text_lines = self._wrap_lines(self._line_source, point_size)
cy = _rendered_size("Ty", point_size, self._font_file)[1]
return (cy * len(text_lines)) <= self._height
return predicate
@property
def _font_file(self):
return self[3]
@property
def _height(self):
return self[2]
@property
def _line_source(self):
return self[0]
@property
def _width(self):
return self[1]
def _wrap_lines(self, line_source, point_size):
"""
Return a sequence of str values representing the text in
*line_source* wrapped within this fitter when rendered at
*point_size*.
"""
text, remainder = self._break_line(line_source, point_size)
lines = [text]
if remainder:
lines.extend(self._wrap_lines(remainder, point_size))
return lines
class _BinarySearchTree(object):
"""
A node in a binary search tree. Uniform for root, subtree root, and leaf
nodes.
"""
def __init__(self, value):
self._value = value
self._lesser = None
self._greater = None
def find_max(self, predicate, max_=None):
"""
Return the largest item in or under this node that satisfies
*predicate*.
"""
if predicate(self.value):
max_ = self.value
next_node = self._greater
else:
next_node = self._lesser
if next_node is None:
return max_
return next_node.find_max(predicate, max_)
@classmethod
def from_ordered_sequence(cls, iseq):
"""
Return the root of a balanced binary search tree populated with the
values in iterable *iseq*.
"""
seq = list(iseq)
# optimize for usually all fits by making longest first
bst = cls(seq.pop())
bst._insert_from_ordered_sequence(seq)
return bst
def insert(self, value):
"""
Insert a new node containing *value* into this tree such that its
structure as a binary search tree is preserved.
"""
side = "_lesser" if value < self.value else "_greater"
child = getattr(self, side)
if child is None:
setattr(self, side, _BinarySearchTree(value))
else:
child.insert(value)
def tree(self, level=0, prefix=""):
"""
A string representation of the tree rooted in this node, useful for
debugging purposes.
"""
text = "%s%s\n" % (prefix, self.value.text)
prefix = "%s└── " % (" " * level)
if self._lesser:
text += self._lesser.tree(level + 1, prefix)
if self._greater:
text += self._greater.tree(level + 1, prefix)
return text
@property
def value(self):
"""
The value object contained in this node.
"""
return self._value
@staticmethod
def _bisect(seq):
"""
Return a (medial_value, greater_values, lesser_values) 3-tuple
obtained by bisecting sequence *seq*.
"""
if len(seq) == 0:
return [], None, []
mid_idx = int(len(seq) / 2)
mid = seq[mid_idx]
greater = seq[mid_idx + 1 :]
lesser = seq[:mid_idx]
return mid, greater, lesser
def _insert_from_ordered_sequence(self, seq):
"""
Insert the new values contained in *seq* into this tree such that
a balanced tree is produced.
"""
if len(seq) == 0:
return
mid, greater, lesser = self._bisect(seq)
self.insert(mid)
self._insert_from_ordered_sequence(greater)
self._insert_from_ordered_sequence(lesser)
class _LineSource(object):
"""
Generates all the possible even-word line breaks in a string of text,
each in the form of a (line, remainder) 2-tuple where *line* contains the
text before the break and *remainder* the text after as a |_LineSource|
object. Its boolean value is |True| when it contains text, |False| when
its text is the empty string or whitespace only.
"""
def __init__(self, text):
self._text = text
def __bool__(self):
"""
Gives this object boolean behaviors (in Python 3). bool(line_source)
is False if it contains the empty string or whitespace only.
"""
return self._text.strip() != ""
def __eq__(self, other):
return self._text == other._text
def __iter__(self):
"""
Generate a (text, remainder) pair for each possible even-word line
break in this line source, where *text* is a str value and remainder
is a |_LineSource| value.
"""
words = self._text.split()
for idx in range(1, len(words) + 1):
line_text = " ".join(words[:idx])
remainder_text = " ".join(words[idx:])
remainder = _LineSource(remainder_text)
yield _Line(line_text, remainder)
def __nonzero__(self):
"""
Gives this object boolean behaviors (in Python 2). bool(line_source)
is False if it contains the empty string or whitespace only.
"""
return self._text.strip() != ""
def __repr__(self):
return "<_LineSource('%s')>" % self._text
class _Line(tuple):
"""
A candidate line broken at an even word boundary from a string of text,
and a |_LineSource| value containing the text that remains after the line
is broken at this spot.
"""
def __new__(cls, text, remainder):
return tuple.__new__(cls, (text, remainder))
def __gt__(self, other):
return len(self.text) > len(other.text)
def __lt__(self, other):
return not self.__gt__(other)
def __len__(self):
return len(self.text)
def __repr__(self):
return "'%s' => '%s'" % (self.text, self.remainder)
@property
def remainder(self):
return self[1]
@property
def text(self):
return self[0]
class _Fonts(object):
"""
A memoizing cache for ImageFont objects.
"""
fonts = {}
@classmethod
def font(cls, font_path, point_size):
if (font_path, point_size) not in cls.fonts:
cls.fonts[(font_path, point_size)] = ImageFont.truetype(
font_path, point_size
)
return cls.fonts[(font_path, point_size)]
def _rendered_size(text, point_size, font_file):
"""
Return a (width, height) pair representing the size of *text* in English
Metric Units (EMU) when rendered at *point_size* in the font defined in
*font_file*.
"""
emu_per_inch = 914400
px_per_inch = 72.0
font = _Fonts.font(font_file, point_size)
px_width, px_height = font.getsize(text)
emu_width = int(px_width / px_per_inch * emu_per_inch)
emu_height = int(px_height / px_per_inch * emu_per_inch)
return emu_width, emu_height