init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user