diff options
| author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
|---|---|---|
| committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
| commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
| tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/pptx/text | |
| parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
| download | gn-ai-master.tar.gz | |
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/text')
4 files changed, 1405 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/text/__init__.py b/.venv/lib/python3.12/site-packages/pptx/text/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/text/__init__.py diff --git a/.venv/lib/python3.12/site-packages/pptx/text/fonts.py b/.venv/lib/python3.12/site-packages/pptx/text/fonts.py new file mode 100644 index 00000000..5ae054a8 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/text/fonts.py @@ -0,0 +1,399 @@ +"""Objects related to system font file lookup.""" + +from __future__ import annotations + +import os +import sys +from struct import calcsize, unpack_from + +from pptx.util import lazyproperty + + +class FontFiles(object): + """A class-based singleton serving as a lazy cache for system font details.""" + + _font_files = None + + @classmethod + def find(cls, family_name: str, is_bold: bool, is_italic: bool) -> str: + """Return the absolute path to an installed OpenType font. + + File is matched by `family_name` and the styles `is_bold` and `is_italic`. + """ + if cls._font_files is None: + cls._font_files = cls._installed_fonts() + return cls._font_files[(family_name, is_bold, is_italic)] + + @classmethod + def _installed_fonts(cls): + """ + Return a dict mapping a font descriptor to its font file path, + containing all the font files resident on the current machine. The + font descriptor is a (family_name, is_bold, is_italic) 3-tuple. + """ + fonts = {} + for d in cls._font_directories(): + for key, path in cls._iter_font_files_in(d): + fonts[key] = path + return fonts + + @classmethod + def _font_directories(cls): + """ + Return a sequence of directory paths likely to contain fonts on the + current platform. + """ + if sys.platform.startswith("darwin"): + return cls._os_x_font_directories() + if sys.platform.startswith("win32"): + return cls._windows_font_directories() + raise OSError("unsupported operating system") + + @classmethod + def _iter_font_files_in(cls, directory): + """ + Generate the OpenType font files found in and under *directory*. Each + item is a key/value pair. The key is a (family_name, is_bold, + is_italic) 3-tuple, like ('Arial', True, False), and the value is the + absolute path to the font file. + """ + for root, dirs, files in os.walk(directory): + for filename in files: + file_ext = os.path.splitext(filename)[1] + if file_ext.lower() not in (".otf", ".ttf"): + continue + path = os.path.abspath(os.path.join(root, filename)) + with _Font.open(path) as f: + yield ((f.family_name, f.is_bold, f.is_italic), path) + + @classmethod + def _os_x_font_directories(cls): + """ + Return a sequence of directory paths on a Mac in which fonts are + likely to be located. + """ + os_x_font_dirs = [ + "/Library/Fonts", + "/Network/Library/Fonts", + "/System/Library/Fonts", + ] + home = os.environ.get("HOME") + if home is not None: + os_x_font_dirs.extend( + [os.path.join(home, "Library", "Fonts"), os.path.join(home, ".fonts")] + ) + return os_x_font_dirs + + @classmethod + def _windows_font_directories(cls): + """ + Return a sequence of directory paths on Windows in which fonts are + likely to be located. + """ + return [r"C:\Windows\Fonts"] + + +class _Font(object): + """ + A wrapper around an OTF/TTF font file stream that knows how to parse it + for its name and style characteristics, e.g. bold and italic. + """ + + def __init__(self, stream): + self._stream = stream + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_tb): + self._stream.close() + + @property + def is_bold(self): + """ + |True| if this font is marked as a bold style of its font family. + """ + try: + return self._tables["head"].is_bold + except KeyError: + # some files don't have a head table + return False + + @property + def is_italic(self): + """ + |True| if this font is marked as an italic style of its font family. + """ + try: + return self._tables["head"].is_italic + except KeyError: + # some files don't have a head table + return False + + @classmethod + def open(cls, font_file_path): + """ + Return a |_Font| instance loaded from *font_file_path*. + """ + return cls(_Stream.open(font_file_path)) + + @property + def family_name(self): + """ + The name of the typeface family for this font, e.g. 'Arial'. The full + typeface name includes optional style names, such as 'Regular' or + 'Bold Italic'. This attribute is only the common base name shared by + all fonts in the family. + """ + return self._tables["name"].family_name + + @lazyproperty + def _fields(self): + """5-tuple containing the fields read from the font file header. + + Also known as the offset table. + """ + # sfnt_version, tbl_count, search_range, entry_selector, range_shift + return self._stream.read_fields(">4sHHHH", 0) + + def _iter_table_records(self): + """ + Generate a (tag, offset, length) 3-tuple for each of the tables in + this font file. + """ + count = self._table_count + bufr = self._stream.read(offset=12, length=count * 16) + tmpl = ">4sLLL" + for i in range(count): + offset = i * 16 + tag, checksum, off, len_ = unpack_from(tmpl, bufr, offset) + yield tag.decode("utf-8"), off, len_ + + @lazyproperty + def _tables(self): + """ + A mapping of OpenType table tag, e.g. 'name', to a table object + providing access to the contents of that table. + """ + return dict( + (tag, _TableFactory(tag, self._stream, off, len_)) + for tag, off, len_ in self._iter_table_records() + ) + + @property + def _table_count(self): + """ + The number of tables in this OpenType font file. + """ + return self._fields[1] + + +class _Stream(object): + """A thin wrapper around a binary file that facilitates reading C-struct values.""" + + def __init__(self, file): + self._file = file + + @classmethod + def open(cls, path): + """Return |_Stream| providing binary access to contents of file at `path`.""" + return cls(open(path, "rb")) + + def close(self): + """ + Close the wrapped file. Using the stream after closing raises an + exception. + """ + self._file.close() + + def read(self, offset, length): + """ + Return *length* bytes from this stream starting at *offset*. + """ + self._file.seek(offset) + return self._file.read(length) + + def read_fields(self, template, offset=0): + """ + Return a tuple containing the C-struct fields in this stream + specified by *template* and starting at *offset*. + """ + self._file.seek(offset) + bufr = self._file.read(calcsize(template)) + return unpack_from(template, bufr) + + +class _BaseTable(object): + """ + Base class for OpenType font file table objects. + """ + + def __init__(self, tag, stream, offset, length): + self._tag = tag + self._stream = stream + self._offset = offset + self._length = length + + +class _HeadTable(_BaseTable): + """ + OpenType font table having the tag 'head' and containing certain header + information for the font, including its bold and/or italic style. + """ + + def __init__(self, tag, stream, offset, length): + super(_HeadTable, self).__init__(tag, stream, offset, length) + + @property + def is_bold(self): + """ + |True| if this font is marked as having emboldened characters. + """ + return bool(self._macStyle & 1) + + @property + def is_italic(self): + """ + |True| if this font is marked as having italicized characters. + """ + return bool(self._macStyle & 2) + + @lazyproperty + def _fields(self): + """ + A 17-tuple containing the fields in this table. + """ + return self._stream.read_fields(">4s4sLLHHqqhhhhHHHHH", self._offset) + + @property + def _macStyle(self): + """ + The unsigned short value of the 'macStyle' field in this head table. + """ + return self._fields[12] + + +class _NameTable(_BaseTable): + """ + An OpenType font table having the tag 'name' and containing the + name-related strings for the font. + """ + + def __init__(self, tag, stream, offset, length): + super(_NameTable, self).__init__(tag, stream, offset, length) + + @property + def family_name(self): + """ + The name of the typeface family for this font, e.g. 'Arial'. + """ + + def find_first(dict_, keys, default=None): + for key in keys: + value = dict_.get(key) + if value is not None: + return value + return default + + # keys for Unicode, Mac, and Windows family name, respectively + return find_first(self._names, ((0, 1), (1, 1), (3, 1))) + + @staticmethod + def _decode_name(raw_name, platform_id, encoding_id): + """ + Return the unicode name decoded from *raw_name* using the encoding + implied by the combination of *platform_id* and *encoding_id*. + """ + if platform_id == 1: + # reject non-Roman Mac font names + if encoding_id != 0: + return None + return raw_name.decode("mac-roman") + elif platform_id in (0, 3): + return raw_name.decode("utf-16-be") + else: + return None + + def _iter_names(self): + """Generate a key/value pair for each name in this table. + + The key is a (platform_id, name_id) 2-tuple and the value is the unicode text + corresponding to that key. + """ + table_format, count, strings_offset = self._table_header + table_bytes = self._table_bytes + + for idx in range(count): + platform_id, name_id, name = self._read_name(table_bytes, idx, strings_offset) + if name is None: + continue + yield ((platform_id, name_id), name) + + @staticmethod + def _name_header(bufr, idx): + """ + The (platform_id, encoding_id, language_id, name_id, length, + name_str_offset) 6-tuple encoded in each name record C-struct. + """ + name_hdr_offset = 6 + idx * 12 + return unpack_from(">HHHHHH", bufr, name_hdr_offset) + + @staticmethod + def _raw_name_string(bufr, strings_offset, str_offset, length): + """ + Return the *length* bytes comprising the encoded string in *bufr* at + *str_offset* in the strings area beginning at *strings_offset*. + """ + offset = strings_offset + str_offset + tmpl = "%ds" % length + return unpack_from(tmpl, bufr, offset)[0] + + def _read_name(self, bufr, idx, strings_offset): + """Return a (platform_id, name_id, name) 3-tuple for name at `idx` in `bufr`. + + The triple looks like (0, 1, 'Arial'). `strings_offset` is the for the name at + `idx` position in `bufr`. `strings_offset` is the index into `bufr` where actual + name strings begin. The returned name is a unicode string. + """ + platform_id, enc_id, lang_id, name_id, length, str_offset = self._name_header(bufr, idx) + name = self._read_name_text(bufr, platform_id, enc_id, strings_offset, str_offset, length) + return platform_id, name_id, name + + def _read_name_text( + self, bufr, platform_id, encoding_id, strings_offset, name_str_offset, length + ): + """ + Return the unicode name string at *name_str_offset* or |None| if + decoding its format is not supported. + """ + raw_name = self._raw_name_string(bufr, strings_offset, name_str_offset, length) + return self._decode_name(raw_name, platform_id, encoding_id) + + @lazyproperty + def _table_bytes(self): + """ + The binary contents of this name table. + """ + return self._stream.read(self._offset, self._length) + + @property + def _table_header(self): + """ + The (table_format, name_count, strings_offset) 3-tuple contained + in the header of this table. + """ + return unpack_from(">HHH", self._table_bytes) + + @lazyproperty + def _names(self): + """A mapping of (platform_id, name_id) keys to string names for this font.""" + return dict(self._iter_names()) + + +def _TableFactory(tag, stream, offset, length): + """ + Return an instance of |Table| appropriate to *tag*, loaded from + *font_file* with content of *length* starting at *offset*. + """ + TableClass = {"head": _HeadTable, "name": _NameTable}.get(tag, _BaseTable) + return TableClass(tag, stream, offset, length) 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 diff --git a/.venv/lib/python3.12/site-packages/pptx/text/text.py b/.venv/lib/python3.12/site-packages/pptx/text/text.py new file mode 100644 index 00000000..e139410c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/text/text.py @@ -0,0 +1,681 @@ +"""Text-related objects such as TextFrame and Paragraph.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterator, cast + +from pptx.dml.fill import FillFormat +from pptx.enum.dml import MSO_FILL +from pptx.enum.lang import MSO_LANGUAGE_ID +from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.oxml.simpletypes import ST_TextWrappingType +from pptx.shapes import Subshape +from pptx.text.fonts import FontFiles +from pptx.text.layout import TextFitter +from pptx.util import Centipoints, Emu, Length, Pt, lazyproperty + +if TYPE_CHECKING: + from pptx.dml.color import ColorFormat + from pptx.enum.text import ( + MSO_TEXT_UNDERLINE_TYPE, + MSO_VERTICAL_ANCHOR, + PP_PARAGRAPH_ALIGNMENT, + ) + from pptx.oxml.action import CT_Hyperlink + from pptx.oxml.text import ( + CT_RegularTextRun, + CT_TextBody, + CT_TextCharacterProperties, + CT_TextParagraph, + CT_TextParagraphProperties, + ) + from pptx.types import ProvidesExtents, ProvidesPart + + +class TextFrame(Subshape): + """The part of a shape that contains its text. + + Not all shapes have a text frame. Corresponds to the `p:txBody` element that can + appear as a child element of `p:sp`. Not intended to be constructed directly. + """ + + def __init__(self, txBody: CT_TextBody, parent: ProvidesPart): + super(TextFrame, self).__init__(parent) + self._element = self._txBody = txBody + self._parent = parent + + def add_paragraph(self): + """ + Return new |_Paragraph| instance appended to the sequence of + paragraphs contained in this text frame. + """ + p = self._txBody.add_p() + return _Paragraph(p, self) + + @property + def auto_size(self) -> MSO_AUTO_SIZE | None: + """Resizing strategy used to fit text within this shape. + + Determins the type of automatic resizing used to fit the text of this shape within its + bounding box when the text would otherwise extend beyond the shape boundaries. May be + |None|, `MSO_AUTO_SIZE.NONE`, `MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT`, or + `MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`. + """ + return self._bodyPr.autofit + + @auto_size.setter + def auto_size(self, value: MSO_AUTO_SIZE | None): + self._bodyPr.autofit = value + + def clear(self): + """Remove all paragraphs except one empty one.""" + for p in self._txBody.p_lst[1:]: + self._txBody.remove(p) + p = self.paragraphs[0] + p.clear() + + def fit_text( + self, + font_family: str = "Calibri", + max_size: int = 18, + bold: bool = False, + italic: bool = False, + font_file: str | None = None, + ): + """Fit text-frame text entirely within bounds of its shape. + + Make the text in this text frame fit entirely within the bounds of its shape by setting + word wrap on and applying the "best-fit" font size to all the text it contains. + + :attr:`TextFrame.auto_size` is set to :attr:`MSO_AUTO_SIZE.NONE`. The font size will not + be set larger than `max_size` points. If the path to a matching TrueType font is provided + as `font_file`, that font file will be used for the font metrics. If `font_file` is |None|, + best efforts are made to locate a font file with matchhing `font_family`, `bold`, and + `italic` installed on the current system (usually succeeds if the font is installed). + """ + # ---no-op when empty as fit behavior not defined for that case--- + if self.text == "": + return # pragma: no cover + + font_size = self._best_fit_font_size(font_family, max_size, bold, italic, font_file) + self._apply_fit(font_family, font_size, bold, italic) + + @property + def margin_bottom(self) -> Length: + """|Length| value representing the inset of text from the bottom text frame border. + + :meth:`pptx.util.Inches` provides a convenient way of setting the value, e.g. + `text_frame.margin_bottom = Inches(0.05)`. + """ + return self._bodyPr.bIns + + @margin_bottom.setter + def margin_bottom(self, emu: Length): + self._bodyPr.bIns = emu + + @property + def margin_left(self) -> Length: + """Inset of text from left text frame border as |Length| value.""" + return self._bodyPr.lIns + + @margin_left.setter + def margin_left(self, emu: Length): + self._bodyPr.lIns = emu + + @property + def margin_right(self) -> Length: + """Inset of text from right text frame border as |Length| value.""" + return self._bodyPr.rIns + + @margin_right.setter + def margin_right(self, emu: Length): + self._bodyPr.rIns = emu + + @property + def margin_top(self) -> Length: + """Inset of text from top text frame border as |Length| value.""" + return self._bodyPr.tIns + + @margin_top.setter + def margin_top(self, emu: Length): + self._bodyPr.tIns = emu + + @property + def paragraphs(self) -> tuple[_Paragraph, ...]: + """Sequence of paragraphs in this text frame. + + A text frame always contains at least one paragraph. + """ + return tuple([_Paragraph(p, self) for p in self._txBody.p_lst]) + + @property + def text(self) -> str: + """All text in this text-frame as a single string. + + Read/write. The return value contains all text in this text-frame. A line-feed character + (`"\\n"`) separates the text for each paragraph. A vertical-tab character (`"\\v"`) appears + for each line break (aka. soft carriage-return) encountered. + + The vertical-tab character is how PowerPoint represents a soft carriage return in clipboard + text, which is why that encoding was chosen. + + Assignment replaces all text in the text frame. A new paragraph is added for each line-feed + character (`"\\n"`) encountered. A line-break (soft carriage-return) is inserted for each + vertical-tab character (`"\\v"`) encountered. + + Any control character other than newline, tab, or vertical-tab are escaped as plain-text + like "_x001B_" (for ESC (ASCII 32) in this example). + """ + return "\n".join(paragraph.text for paragraph in self.paragraphs) + + @text.setter + def text(self, text: str): + txBody = self._txBody + txBody.clear_content() + for p_text in text.split("\n"): + p = txBody.add_p() + p.append_text(p_text) + + @property + def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None: + """Represents the vertical alignment of text in this text frame. + + |None| indicates the effective value should be inherited from this object's style hierarchy. + """ + return self._txBody.bodyPr.anchor + + @vertical_anchor.setter + def vertical_anchor(self, value: MSO_VERTICAL_ANCHOR | None): + bodyPr = self._txBody.bodyPr + bodyPr.anchor = value + + @property + def word_wrap(self) -> bool | None: + """`True` when lines of text in this shape are wrapped to fit within the shape's width. + + Read-write. Valid values are True, False, or None. True and False turn word wrap on and + off, respectively. Assigning None to word wrap causes any word wrap setting to be removed + from the text frame, causing it to inherit this setting from its style hierarchy. + """ + return { + ST_TextWrappingType.SQUARE: True, + ST_TextWrappingType.NONE: False, + None: None, + }[self._txBody.bodyPr.wrap] + + @word_wrap.setter + def word_wrap(self, value: bool | None): + if value not in (True, False, None): + raise ValueError( # pragma: no cover + "assigned value must be True, False, or None, got %s" % value + ) + self._txBody.bodyPr.wrap = { + True: ST_TextWrappingType.SQUARE, + False: ST_TextWrappingType.NONE, + None: None, + }[value] + + def _apply_fit(self, font_family: str, font_size: int, is_bold: bool, is_italic: bool): + """Arrange text in this text frame to fit inside its extents. + + This is accomplished by setting auto size off, wrap on, and setting the font of + all its text to `font_family`, `font_size`, `is_bold`, and `is_italic`. + """ + self.auto_size = MSO_AUTO_SIZE.NONE + self.word_wrap = True + self._set_font(font_family, font_size, is_bold, is_italic) + + def _best_fit_font_size( + self, family: str, max_size: int, bold: bool, italic: bool, font_file: str | None + ) -> int: + """Return font-size in points that best fits text in this text-frame. + + The best-fit font size is the largest integer point size not greater than `max_size` that + allows all the text in this text frame to fit inside its extents when rendered using the + font described by `family`, `bold`, and `italic`. If `font_file` is specified, it is used + to calculate the fit, whether or not it matches `family`, `bold`, and `italic`. + """ + if font_file is None: + font_file = FontFiles.find(family, bold, italic) + return TextFitter.best_fit_font_size(self.text, self._extents, max_size, font_file) + + @property + def _bodyPr(self): + return self._txBody.bodyPr + + @property + def _extents(self) -> tuple[Length, Length]: + """(cx, cy) 2-tuple representing the effective rendering area of this text-frame. + + Margins are taken into account. + """ + parent = cast("ProvidesExtents", self._parent) + return ( + Length(parent.width - self.margin_left - self.margin_right), + Length(parent.height - self.margin_top - self.margin_bottom), + ) + + def _set_font(self, family: str, size: int, bold: bool, italic: bool): + """Set the font properties of all the text in this text frame.""" + + def iter_rPrs(txBody: CT_TextBody) -> Iterator[CT_TextCharacterProperties]: + for p in txBody.p_lst: + for elm in p.content_children: + yield elm.get_or_add_rPr() + # generate a:endParaRPr for each <a:p> element + yield p.get_or_add_endParaRPr() + + def set_rPr_font( + rPr: CT_TextCharacterProperties, name: str, size: int, bold: bool, italic: bool + ): + f = Font(rPr) + f.name, f.size, f.bold, f.italic = family, Pt(size), bold, italic + + txBody = self._element + for rPr in iter_rPrs(txBody): + set_rPr_font(rPr, family, size, bold, italic) + + +class Font(object): + """Character properties object, providing font size, font name, bold, italic, etc. + + Corresponds to `a:rPr` child element of a run. Also appears as `a:defRPr` and + `a:endParaRPr` in paragraph and `a:defRPr` in list style elements. + """ + + def __init__(self, rPr: CT_TextCharacterProperties): + super(Font, self).__init__() + self._element = self._rPr = rPr + + @property + def bold(self) -> bool | None: + """Get or set boolean bold value of |Font|, e.g. `paragraph.font.bold = True`. + + If set to |None|, the bold setting is cleared and is inherited from an enclosing shape's + setting, or a setting in a style or master. Returns None if no bold attribute is present, + meaning the effective bold value is inherited from a master or the theme. + """ + return self._rPr.b + + @bold.setter + def bold(self, value: bool | None): + self._rPr.b = value + + @lazyproperty + def color(self) -> ColorFormat: + """The |ColorFormat| instance that provides access to the color settings for this font.""" + if self.fill.type != MSO_FILL.SOLID: + self.fill.solid() + return self.fill.fore_color + + @lazyproperty + def fill(self) -> FillFormat: + """|FillFormat| instance for this font. + + Provides access to fill properties such as fill color. + """ + return FillFormat.from_fill_parent(self._rPr) + + @property + def italic(self) -> bool | None: + """Get or set boolean italic value of |Font| instance. + + Has the same behaviors as bold with respect to None values. + """ + return self._rPr.i + + @italic.setter + def italic(self, value: bool | None): + self._rPr.i = value + + @property + def language_id(self) -> MSO_LANGUAGE_ID | None: + """Get or set the language id of this |Font| instance. + + The language id is a member of the :ref:`MsoLanguageId` enumeration. Assigning |None| + removes any language setting, the same behavior as assigning `MSO_LANGUAGE_ID.NONE`. + """ + lang = self._rPr.lang + if lang is None: + return MSO_LANGUAGE_ID.NONE + return self._rPr.lang + + @language_id.setter + def language_id(self, value: MSO_LANGUAGE_ID | None): + if value == MSO_LANGUAGE_ID.NONE: + value = None + self._rPr.lang = value + + @property + def name(self) -> str | None: + """Get or set the typeface name for this |Font| instance. + + Causes the text it controls to appear in the named font, if a matching font is found. + Returns |None| if the typeface is currently inherited from the theme. Setting it to |None| + removes any override of the theme typeface. + """ + latin = self._rPr.latin + if latin is None: + return None + return latin.typeface + + @name.setter + def name(self, value: str | None): + if value is None: + self._rPr._remove_latin() # pyright: ignore[reportPrivateUsage] + else: + latin = self._rPr.get_or_add_latin() + latin.typeface = value + + @property + def size(self) -> Length | None: + """Indicates the font height in English Metric Units (EMU). + + Read/write. |None| indicates the font size should be inherited from its style hierarchy, + such as a placeholder or document defaults (usually 18pt). |Length| is a subclass of |int| + having properties for convenient conversion into points or other length units. Likewise, + the :class:`pptx.util.Pt` class allows convenient specification of point values:: + + >>> font.size = Pt(24) + >>> font.size + 304800 + >>> font.size.pt + 24.0 + """ + sz = self._rPr.sz + if sz is None: + return None + return Centipoints(sz) + + @size.setter + def size(self, emu: Length | None): + if emu is None: + self._rPr.sz = None + else: + sz = Emu(emu).centipoints + self._rPr.sz = sz + + @property + def underline(self) -> bool | MSO_TEXT_UNDERLINE_TYPE | None: + """Indicaties the underline setting for this font. + + Value is |True|, |False|, |None|, or a member of the :ref:`MsoTextUnderlineType` + enumeration. |None| is the default and indicates the underline setting should be inherited + from the style hierarchy, such as from a placeholder. |True| indicates single underline. + |False| indicates no underline. Other settings such as double and wavy underlining are + indicated with members of the :ref:`MsoTextUnderlineType` enumeration. + """ + u = self._rPr.u + if u is MSO_UNDERLINE.NONE: + return False + if u is MSO_UNDERLINE.SINGLE_LINE: + return True + return u + + @underline.setter + def underline(self, value: bool | MSO_TEXT_UNDERLINE_TYPE | None): + if value is True: + value = MSO_UNDERLINE.SINGLE_LINE + elif value is False: + value = MSO_UNDERLINE.NONE + self._element.u = value + + +class _Hyperlink(Subshape): + """Text run hyperlink object. + + Corresponds to `a:hlinkClick` child element of the run's properties element (`a:rPr`). + """ + + def __init__(self, rPr: CT_TextCharacterProperties, parent: ProvidesPart): + super(_Hyperlink, self).__init__(parent) + self._rPr = rPr + + @property + def address(self) -> str | None: + """The URL of the hyperlink. + + Read/write. URL can be on http, https, mailto, or file scheme; others may work. + """ + if self._hlinkClick is None: + return None + return self.part.target_ref(self._hlinkClick.rId) + + @address.setter + def address(self, url: str | None): + # implements all three of add, change, and remove hyperlink + if self._hlinkClick is not None: + self._remove_hlinkClick() + if url: + self._add_hlinkClick(url) + + def _add_hlinkClick(self, url: str): + rId = self.part.relate_to(url, RT.HYPERLINK, is_external=True) + self._rPr.add_hlinkClick(rId) + + @property + def _hlinkClick(self) -> CT_Hyperlink | None: + return self._rPr.hlinkClick + + def _remove_hlinkClick(self): + assert self._hlinkClick is not None + self.part.drop_rel(self._hlinkClick.rId) + self._rPr._remove_hlinkClick() # pyright: ignore[reportPrivateUsage] + + +class _Paragraph(Subshape): + """Paragraph object. Not intended to be constructed directly.""" + + def __init__(self, p: CT_TextParagraph, parent: ProvidesPart): + super(_Paragraph, self).__init__(parent) + self._element = self._p = p + + def add_line_break(self): + """Add line break at end of this paragraph.""" + self._p.add_br() + + def add_run(self) -> _Run: + """Return a new run appended to the runs in this paragraph.""" + r = self._p.add_r() + return _Run(r, self) + + @property + def alignment(self) -> PP_PARAGRAPH_ALIGNMENT | None: + """Horizontal alignment of this paragraph. + + The value |None| indicates the paragraph should 'inherit' its effective value from its + style hierarchy. Assigning |None| removes any explicit setting, causing its inherited + value to be used. + """ + return self._pPr.algn + + @alignment.setter + def alignment(self, value: PP_PARAGRAPH_ALIGNMENT | None): + self._pPr.algn = value + + def clear(self): + """Remove all content from this paragraph. + + Paragraph properties are preserved. Content includes runs, line breaks, and fields. + """ + for elm in self._element.content_children: + self._element.remove(elm) + return self + + @property + def font(self) -> Font: + """|Font| object containing default character properties for the runs in this paragraph. + + These character properties override default properties inherited from parent objects such + as the text frame the paragraph is contained in and they may be overridden by character + properties set at the run level. + """ + return Font(self._defRPr) + + @property + def level(self) -> int: + """Indentation level of this paragraph. + + Read-write. Integer in range 0..8 inclusive. 0 represents a top-level paragraph and is the + default value. Indentation level is most commonly encountered in a bulleted list, as is + found on a word bullet slide. + """ + return self._pPr.lvl + + @level.setter + def level(self, level: int): + self._pPr.lvl = level + + @property + def line_spacing(self) -> int | float | Length | None: + """The space between baselines in successive lines of this paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. A numeric value, e.g. `2` or `1.5`, + indicates spacing is applied in multiples of line heights. A |Length| value such as + `Pt(12)` indicates spacing is a fixed height. The |Pt| value class is a convenient way to + apply line spacing in units of points. + """ + pPr = self._p.pPr + if pPr is None: + return None + return pPr.line_spacing + + @line_spacing.setter + def line_spacing(self, value: int | float | Length | None): + pPr = self._p.get_or_add_pPr() + pPr.line_spacing = value + + @property + def runs(self) -> tuple[_Run, ...]: + """Sequence of runs in this paragraph.""" + return tuple(_Run(r, self) for r in self._element.r_lst) + + @property + def space_after(self) -> Length | None: + """The spacing to appear between this paragraph and the subsequent paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. |Length| objects provide convenience + properties, such as `.pt` and `.inches`, that allow easy conversion to various length + units. + """ + pPr = self._p.pPr + if pPr is None: + return None + return pPr.space_after + + @space_after.setter + def space_after(self, value: Length | None): + pPr = self._p.get_or_add_pPr() + pPr.space_after = value + + @property + def space_before(self) -> Length | None: + """The spacing to appear between this paragraph and the prior paragraph. + + A value of |None| indicates no explicit value is assigned and its effective value is + inherited from the paragraph's style hierarchy. |Length| objects provide convenience + properties, such as `.pt` and `.cm`, that allow easy conversion to various length units. + """ + pPr = self._p.pPr + if pPr is None: + return None + return pPr.space_before + + @space_before.setter + def space_before(self, value: Length | None): + pPr = self._p.get_or_add_pPr() + pPr.space_before = value + + @property + def text(self) -> str: + """Text of paragraph as a single string. + + Read/write. This value is formed by concatenating the text in each run and field making up + the paragraph, adding a vertical-tab character (`"\\v"`) for each line-break element + (`<a:br>`, soft carriage-return) encountered. + + While the encoding of line-breaks as a vertical tab might be surprising at first, doing so + is consistent with PowerPoint's clipboard copy behavior and allows a line-break to be + distinguished from a paragraph boundary within the str return value. + + Assignment causes all content in the paragraph to be replaced. Each vertical-tab character + (`"\\v"`) in the assigned str is translated to a line-break, as is each line-feed + character (`"\\n"`). Contrast behavior of line-feed character in `TextFrame.text` setter. + If line-feed characters are intended to produce new paragraphs, use `TextFrame.text` + instead. Any other control characters in the assigned string are escaped as a hex + representation like "_x001B_" (for ESC (ASCII 27) in this example). + """ + return "".join(elm.text for elm in self._element.content_children) + + @text.setter + def text(self, text: str): + self.clear() + self._element.append_text(text) + + @property + def _defRPr(self) -> CT_TextCharacterProperties: + """The element that defines the default run properties for runs in this paragraph. + + Causes the element to be added if not present. + """ + return self._pPr.get_or_add_defRPr() + + @property + def _pPr(self) -> CT_TextParagraphProperties: + """Contains the properties for this paragraph. + + Causes the element to be added if not present. + """ + return self._p.get_or_add_pPr() + + +class _Run(Subshape): + """Text run object. Corresponds to `a:r` child element in a paragraph.""" + + def __init__(self, r: CT_RegularTextRun, parent: ProvidesPart): + super(_Run, self).__init__(parent) + self._r = r + + @property + def font(self): + """|Font| instance containing run-level character properties for the text in this run. + + Character properties can be and perhaps most often are inherited from parent objects such + as the paragraph and slide layout the run is contained in. Only those specifically + overridden at the run level are contained in the font object. + """ + rPr = self._r.get_or_add_rPr() + return Font(rPr) + + @lazyproperty + def hyperlink(self) -> _Hyperlink: + """Proxy for any `a:hlinkClick` element under the run properties element. + + Created on demand, the hyperlink object is available whether an `a:hlinkClick` element is + present or not, and creates or deletes that element as appropriate in response to actions + on its methods and attributes. + """ + rPr = self._r.get_or_add_rPr() + return _Hyperlink(rPr, self) + + @property + def text(self): + """Read/write. A unicode string containing the text in this run. + + Assignment replaces all text in the run. The assigned value can be a 7-bit ASCII + string, a UTF-8 encoded 8-bit string, or unicode. String values are converted to + unicode assuming UTF-8 encoding. + + Any other control characters in the assigned string other than tab or newline + are escaped as a hex representation. For example, ESC (ASCII 27) is escaped as + "_x001B_". Contrast the behavior of `TextFrame.text` and `_Paragraph.text` with + respect to line-feed and vertical-tab characters. + """ + return self._r.text + + @text.setter + def text(self, text: str): + self._r.text = text |
