init: 폴더구조 설계 및 인프라 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:31:53 +09:00
parent 34ad612281
commit f9c463f87a
1839 changed files with 323096 additions and 1 deletions
@@ -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