aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/pptx/text/layout.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/pptx/text/layout.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/text/layout.py')
-rw-r--r--.venv/lib/python3.12/site-packages/pptx/text/layout.py325
1 files changed, 325 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/layout.py b/.venv/lib/python3.12/site-packages/pptx/text/layout.py
new file mode 100644
index 00000000..d2b43993
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pptx/text/layout.py
@@ -0,0 +1,325 @@
+"""Objects related to layout of rendered text, such as TextFitter."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from PIL import ImageFont
+
+if TYPE_CHECKING:
+ from pptx.util import Length
+
+
+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: str, extents: tuple[Length, Length], max_size: int, font_file: str
+ ) -> int:
+ """Return whole-number best fit point size less than or equal to `max_size`.
+
+ The return value is 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 function taking an integer point size argument.
+
+ The function 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| when text in `line_source` can be wrapped to fit.
+
+ Fit means text can be broken into lines that 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)
+ try:
+ px_width, px_height = font.getsize(text)
+ except AttributeError:
+ left, top, right, bottom = font.getbbox(text)
+ px_width, px_height = right - left, bottom - top
+
+ 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