Если бы меня спросили, какой мой любимый векторный редактор, я бы, не задумываясь, ответил: PowerPoint.

Звучит неожиданно, но позвольте объяснить.
Я всегда любил делать презентации. Хоть я и не дизайнер, мне всегда хотелось, чтобы слайды были не только информативными, но и визуально приятными. Зачастую в чужих докладах я видел одно и то же: шакальные картинки, даже там, где их легко можно было заменить чистыми и векторными иллюстрациями (впрочем, я сейчас пользуюсь версией, которая, к сожалению, не поддерживает работу с svg, так что вообще не осуждаю).
В какой-то момент я решил, что в своих презентациях я буду рисовать всё сам – прямо в PowerPoint, фигурами. Прямоугольники, стрелки, круги, надписи – всё, что нужно для простых схем и иллюстраций уже заботливо подготовлено разработчиками из Microsoft.
Такой подход особенно пригодился мне в мини-семинарах для команды, где я рассказывал про различные архитектуры моделей машинного обучения. Зачастую мне нужно было добавлять на свои слайды схемы пайплайнов, визуализации блоков и концептуальные диаграммы из разбираемых к семинару статей. К сожалению, эти иллюстрации были слишком мелкими и расплывались при масштабировании. Поэтому я просто перерисовывал их руками, фигура за фигурой, прямо в PowerPoint.

Но однажды мне понадобилось нечто большее, чем десяток фигур. Нужно было вставить парочку scatter-графиков из тысячи точек и нарисовать модели нескольких полносвязных нейронных сетей впридачу, причём весовые коэффициенты хотелось нарисовать не просто одинаковыми линиями, а линиями с разной толщиной и цветом в зависимости от значений соответствующих весов. Вставлять PNG не хотелось (даже отрендеренный в 8К), а вручную расставлять тысячу точек стало слишком уж лениво (честно признаться, сначала я даже был почти готов пойти на это).
И тут мне пришла идея: а что, если программно управлять PowerPoint, как векторным движком? Я же всё-таки программист! Так появилась pptx-shapes – небольшая Python-библиотека, которая позволяет добавлять фигуры (и не только) прямо в .pptx
, полностью автоматически, не используя при этом ничего кроме обработки XML с помощью lxml
библиотеки.
Что такое pptx-shapes?
pptx-shapes
– это небольшая Python-библиотека, которая позволяет добавлять фигуры прямо в PowerPoint-презентацию, оперируя на уровне XML. Ей не требуется Windows, PowerPoint или Office. Всё работает через редактировние XML-файлов, лежащих внутри .pptx-архива.
Файл презентации, на который хочется добавить фигуры, – это всё, что нужно, чтобы начать работать с библиотекой.
Пример использования
Проще один раз увидеть, как пользоваться инструментом, чем сразу бросаться читать документацию, не так ли? Давайте посмотрим, как создать несколько простых фигур и добавить их на слайд:
from pptx_shapes import Presentation
from pptx_shapes.shapes import Ellipse, Rectangle, TextBox
from pptx_shapes.style import FillStyle, FontFormat, FontStyle, StrokeStyle
with Presentation(presentation_path="empty.pptx") as presentation:
presentation.add(shape=TextBox(
x=23, y=4,
width=12, height=2,
angle=45,
text="Hello from pptx-shapes!",
style=FontStyle(size=32),
formatting=FontFormat(bold=True)
))
presentation.add(shape=Ellipse(
x=18, y=7,
width=9, height=9,
fill=FillStyle(color="#7699d4")
), slide="slide1")
presentation.add(shape=Rectangle(
x=4, y=2,
width=8, height=16,
radius=0.25,
angle=30,
fill=FillStyle(color="#dd7373"),
stroke=StrokeStyle(color="magenta", thickness=3)
), slide="slide1")
presentation.save("result.pptx")
Давайте разберёмся, что именно здесь происходит:
редактируется презентация
empty.pptx
(содержащая как минимум один слайд);в правый верхний угол добавляется текстовый блок с жирным текстом "Hello from pptx-shapes!", повёрнутый на 45 градусов;
добавляется синий круг размером 9x9 сантиметров;
добавляется повёрнутый на 30 градусов прямоугольник красного цвета с закруглёнными углами и толстой пурпурной обводкой;
изменённая презентация сохраняется с именем
result.pptx
.
А вот, как это выглядит на слайде (хороший дизайн, ничего не скажешь):

Всё, что вам нужно – это для каждой созданной фигуры вызвать метод add
, передав фигуру и опционально номер слайда (по умолчанию фигура добавляется на первый слайд). А когда закончите, вызвать метод save
.
Как это работает изнутри
.pptx
распаковывается как ZIP-архив во временную папку (именно поэтому используется механизм контекстного менежера).При добавлении фигур на слайд ищется соответствующий XML файл в папке ppt/slides (например, для первого слайда – это обычно ppt/slides/slide1.xml).
В XML этого слайда внутри
spTree
добавляются новые элементы с тегами вроде<p:sp>
,<p:cxnSp>
и<a:prstGeom>
.При сохранении изменений модифицированные XML файлы заменяются, и все файлы внутри временной папки запаковываются обратно в
.pptx
.
Такой низкоуровневый подход особенно хорош, когда требуется автоматически генерировать слайды, визуализировать данные или строить сложные геометрические схемы, например архитектуры нейросетей, которые вручную рисовать слишком долго. Если нужно создать десятки или сотни фигур, разместить их по сетке, закодировать цвет, прозрачность или толщину линий — всё это можно сделать программно с помощью pptx-shapes.
Что можно добавлять?
Сейчас можно добавить одну из следующих фигур, доступных в модуле shapes
:
прямую линию (
Line
);стрелку с указанием типа наконечника как в начале, так и конце стрелки (
Arrow
);дугу окружности (
Arc
);арку (та же дуга, только имеющая толщину,
Arch
);эллипс (
Elipse
);прямоугольник, в т.ч. с закруглёнными углами (
Rectangle
);сектор окруности (
Pie
);полигон (
Polygon
);текстовое поле (
TextBox
).

А если нужно сгруппировать две и более фигуры в единую сущность, то существует фигура Group
.
Стилизация
Каждая фигура поддерживает собственные параметры стилизации: заливку (FillStyle
), обводку (StrokeStyle
), шрифт (FontStyle
) и форматирование текста (FontFormat
). Доступность этих параметров зависит от типа фигуры. Например, линии и стрелки не имеют заливки, а настройки шрифта и форматирования применимы только к текстовым полям.

Диаграммы
Если добавлять фигуры для вас слишком скучно и хочется чего-то посложнее, например, визуализировать данные, то pptx-shapes
тоже может быть полезен. В библиотеке доступны несколько базовых типов диаграмм, реализованных в виде составных фигур внутри модуля charts
:
Пончиковая диаграмма (
DonutChart
) – отлично подходит для представления распределений и соотношений. Можно задать значения, цвета и толщину кольца.Столбчатая диаграмма (
BarChart
) – удобна для отображения категориальных значения или временных рядов. Поддерживает настройку отступов, подписей и цветов.Scatter-график (
ScatterPlot
) – позволяет разместить на слайде облако точек, идеально подходящее, например, для визуализации распределений или результатов кластеризации.

Можно спросить: зачем вообще реализовывать собственные диаграммы, если в PowerPoint уже есть встроенные?
Дело в контроле. Иногда хочется, чтобы визуализация точно соответствовала стилю слайда: нужная толщина колец, точное позиционирование подписей, индивидуальные цвета для каждой категории. Стандартные диаграммы в PowerPoint во многом удобны, но порой слишком ограничены – особенно если вы хотите, чтобы график был не просто «вставкой», а частью иллюстрации.
Кроме того, для pptx-shapes
это естественное продолжение идеи: раз уж есть возможность программно собирать фигуры, почему бы не использовать её для создания наглядных визуализаций, прямо в коде?
Где почитать подробнее?
Если вам захотелось попробовать pptx-shapes
в деле, добро пожаловать в документацию на Read the Docs – там описаны доступные фигуры, параметры и примеры использования.
А если интересует, как всё устроено внутри, загляните в репозиторий на GitHub. Исходный код открыт и, надеюсь, читаем – вдруг вам захочется внести свой вклад или просто что-нибудь подправить под себя.
Есть ещё примеры?
Все примеры доступны в директории examples репозитория, а наиболее интересные дополнительно описаны в соответствующем разделе документации.
Вот несколько наиболее наглядных:
Галерея фигур
Пример, в котором на одном слайде собраны практически все доступные типы фигур – от простых линий и прямоугольников до текстовых полей, стрелок и секторов. Отличная отправная точка, чтобы быстро увидеть возможности библиотеки.
Код примера
from pptx_shapes import Presentation
from pptx_shapes.enums import Align, ArrowType, LineDash, VerticalAlign
from pptx_shapes.shapes import Arc, Arrow, Ellipse, Group, Line, Polygon, Rectangle, TextBox
from pptx_shapes.style import FillStyle, FontFormat, FontStyle, StrokeStyle
def main() -> None:
with Presentation(presentation_path="empty.pptx") as presentation:
presentation.add(shape=TextBox(
x=23, y=4, width=12, height=2, text="Hello from pptx-shapes!", angle=45, style=FontStyle(size=32), formatting=FontFormat(bold=True)
))
presentation.add(shape=TextBox(
x=7.5, y=17.2, width=18.5, height=1.5,
text="Python library for adding basic geometric shapes directly to PowerPoint (.pptx) slides by editing the XML structure.",
style=FontStyle(size=16, align=Align.LEFT),
auto_fit=True
))
# ellipses
presentation.add(shape=Ellipse(x=20, y=2, width=4, height=4, fill=FillStyle(color="#7699d4")))
# arrows
presentation.add(shape=Arrow(x1=10, y1=9, x2=14, y2=11, start_type=ArrowType.OVAL, end_type=ArrowType.ARROW, stroke=StrokeStyle(thickness=2)))
# arcs
presentation.add(shape=Arc(
x=24, y=9, width=5, height=8, start_angle=90, end_angle=270, angle=45, stroke=StrokeStyle(color="#f00", thickness=2.5, dash=LineDash.DASH_DOTTED)
))
presentation.add(shape=Arc(
x=19.5, y=1.5, width=5, height=5, start_angle=5, end_angle=175, stroke=StrokeStyle(color="#7699d4", thickness=2, dash=LineDash.DOTTED)
))
presentation.add(shape=Arc(
x=19.5, y=1.5, width=5, height=5, start_angle=185, end_angle=355, stroke=StrokeStyle(color="#7699d4", thickness=2, dash=LineDash.DASHED)
))
# rectangles
presentation.add(shape=Rectangle(
x=18, y=8, width=4, height=8.5, radius=0.25, fill=FillStyle(color="#dd7373"), stroke=StrokeStyle(color="#222", thickness=3), angle=30
))
presentation.add(shape=Rectangle(
x=27, y=14, width=3, height=3, radius=0, fill=FillStyle(color="#dd7373"), stroke=StrokeStyle(color="#222", thickness=1)
))
# polygons
presentation.add(shape=Polygon(
points=[(11, 12), (13, 14), (11, 16), (9, 14), (11, 12)], fill=FillStyle(color="yellow"), stroke=StrokeStyle(color="magenta", thickness=2.5)
))
presentation.add(shape=Polygon(
points=[(15, 5), (16, 6), (15, 7), (12, 7), (11, 6), (12, 5)], angle=45, fill=FillStyle(color="#88ff88")
))
# groups
presentation.add(shape=Group(shapes=[
Line(x1=1, y1=1, x2=13, y2=1, stroke=StrokeStyle(thickness=2, color="#7699d4")),
Line(x1=1, y1=1, x2=1, y2=6, stroke=StrokeStyle(thickness=2, color="#dd7373")),
Line(x1=13, y1=1, x2=1, y2=6, stroke=StrokeStyle(thickness=2, color="#89dd73")),
TextBox(x=0.7, y=3.5, width=13, height=1, text="hypotenuse", angle=-22.6, style=FontStyle(size=18, color="#89dd73", vertical_align=VerticalAlign.TOP)),
TextBox(x=-2, y=3, width=5, height=1, text="kathete", angle=90, style=FontStyle(size=18, color="#dd7373", vertical_align=VerticalAlign.TOP)),
TextBox(x=1, y=0, width=12, height=1, text="kathete", style=FontStyle(size=18, color="#7699d4", vertical_align=VerticalAlign.BOTTOM))
]))
presentation.add(shape=Group(shapes=[
Ellipse(x=4.5, y=6.0, width=2.0, height=3.5, fill=FillStyle(color="#dd7373", opacity=0.5), stroke=StrokeStyle(color="black", thickness=2, opacity=0.75)),
Ellipse(x=3.0, y=8.5, width=3.5, height=2.0, fill=FillStyle(color="#dd7373", opacity=0.25), stroke=StrokeStyle(color="black", opacity=0.25), angle=-45),
Ellipse(x=5.0, y=8.5, width=3.5, height=2.0, fill=FillStyle(color="#dd7373", opacity=0.85), stroke=StrokeStyle(color="black", opacity=0.85), angle=45)
]))
presentation.add(shape=Group(shapes=[
TextBox(x=1, y=15, width=4.8, height=1, text="little histogram", style=FontStyle(size=20, color="#7699d4")),
Rectangle(x=1, y=16, width=1.2, height=2.7, radius=0.2, fill=FillStyle(color="#7699d4"), stroke=StrokeStyle(color="#fff")),
Rectangle(x=2.2, y=16.4, width=1.2, height=2.3, radius=0.2, fill=FillStyle(color="#7699d4"), stroke=StrokeStyle(color="#fff")),
Rectangle(x=3.4, y=17, width=1.2, height=1.7, radius=0.2, fill=FillStyle(color="#7699d4"), stroke=StrokeStyle(color="#fff")),
Rectangle(x=4.6, y=16.1, width=1.2, height=2.6, radius=0.2, fill=FillStyle(color="#7699d4"), stroke=StrokeStyle(color="#fff"))
]))
presentation.save("basic.pptx")
if __name__ == "__main__":
main()

Фрактал
Пример построения фрактала из линий с помощью простой рекурсивной функции.
Скрытый текст
import math
from dataclasses import dataclass
from typing import List
from pptx_shapes import Presentation
from pptx_shapes.shapes import Group, Line, Rectangle, Shape
from pptx_shapes.style import FillStyle, StrokeStyle
@dataclass
class Config:
start_color: str
end_color: str
depth: int
start_branch_num: int
branch_num: int
branch_angle: float
length: float
def get_stroke(self, depth: int) -> StrokeStyle:
ratio = depth / self.depth
r1, g1, b1 = int(self.start_color[1:3], 16), int(self.start_color[3:5], 16), int(self.start_color[5:7], 16)
r2, g2, b2 = int(self.end_color[1:3], 16), int(self.end_color[3:5], 16), int(self.end_color[5:7], 16)
r = math.floor(r1 * (1 - ratio) + r2 * ratio)
g = math.floor(g1 * (1 - ratio) + g2 * ratio)
b = math.floor(b1 * (1 - ratio) + b2 * ratio)
return StrokeStyle(color=f"#{r:02X}{g:02X}{b:02X}", opacity=1 - math.pow(depth / (self.depth + 1), 2))
def draw_fractal_line(config: Config, start_x: float, start_y: float, depth: int, degree: float, length: float, shapes: List[Shape]) -> None:
if depth > config.depth:
return
start_angle = -(config.branch_num - 1) * config.branch_angle / 2 + degree
for _ in range(config.branch_num):
angle = start_angle * math.pi / 180
x = start_x + math.cos(angle) * length
y = start_y + math.sin(angle) * length
shapes.append(Line(x1=start_x, y1=start_y, x2=x, y2=y, stroke=config.get_stroke(depth=depth)))
draw_fractal_line(config, x, y, depth + 1, start_angle, length * math.pow(config.length, depth), shapes)
start_angle += config.branch_angle
def draw_fractal(config: Config, x0: float, y0: float, length: float) -> List[Shape]:
shapes = []
degree = 360 / config.start_branch_num
for i in range(config.start_branch_num):
angle = degree * i * math.pi / 180
x = x0 + math.cos(angle) * length
y = y0 + math.sin(angle) * length
shapes.append(Line(x1=x0, y1=y0, x2=x, y2=y, stroke=config.get_stroke(0)))
draw_fractal_line(config, x, y, 1, degree * i, length, shapes)
return shapes
def main() -> None:
config = Config(
start_color="#ff0000",
end_color="#ffff00",
depth=5,
start_branch_num=7,
branch_num=4,
branch_angle=75, # 20..90
length=0.93 # 0.6..1.3
)
width = 33.867
height = 19.05
with Presentation(presentation_path="empty.pptx") as presentation:
shapes = draw_fractal(config=config, x0=width / 2, y0=height / 2, length=2)
presentation.add(shape=Rectangle(x=0, y=0, width=width, height=height, fill=FillStyle(color="#000")))
presentation.add(shape=Group(shapes))
presentation.save("fractal.pptx")
if __name__ == "__main__":
main()

Разбиение полигонов
Пример демонстрирует, как можно делить полигоны линиями и визуализировать результат – полезно, если нужно показать взаимодействие или пересечения геометрических объектов.
Код примера
from typing import List, Tuple
from pptx_shapes import Presentation
from pptx_shapes.shapes import Group, Polygon
from pptx_shapes.style import FillStyle, StrokeStyle
def split_polygon(polygon: List[Tuple[float, float]], line: Tuple[float, float, float]) -> Tuple[List[Tuple[float, float]], List[Tuple[float, float]]]:
polygon1, polygon2 = [], []
a, b, c = line
for i, (x1, y1) in enumerate(polygon):
x2, y2 = polygon[(i + 1) % len(polygon)]
sign1 = a * x1 + b * y1 + c
sign2 = a * x2 + b * y2 + c
if sign1 <= 0:
polygon1.append((x1, y1))
if sign1 >= 0:
polygon2.append((x1, y1))
if sign1 * sign2 >= 0:
continue
t = sign1 / (sign1 - sign2)
x = x1 + t * (x2 - x1)
y = y1 + t * (y2 - y1)
polygon1.append((x, y))
polygon2.append((x, y))
return polygon1, polygon2
def mix_colors(color1: str, color2: str) -> str:
r1, g1, b1 = color1[1:3], color1[3:5], color1[5:]
r2, g2, b2 = color2[1:3], color2[3:5], color2[5:]
r = (int(r1, 16) + int(r2, 16)) // 2
g = (int(g1, 16) + int(g2, 16)) // 2
b = (int(b1, 16) + int(b2, 16)) // 2
return f"#{r:02X}{g:02X}{b:02X}"
def split_polygons(polygons: List[dict], line: Tuple[float, float, float]) -> List[dict]:
new_polygons = []
for polygon in polygons:
polygon1, polygon2 = split_polygon(polygon=polygon["points"], line=line)
if len(polygon1) > 2:
new_polygons.append({"points": polygon1, "color": mix_colors(polygon["color"], "#dd7373")})
if len(polygon2) > 2:
new_polygons.append({"points": polygon2, "color": mix_colors(polygon["color"], "#7699d4")})
return new_polygons
def view_points(points: List[Tuple[float, float]], limits: dict, x0: float, y0: float, width: float, height: float) -> List[Tuple[float, float]]:
mapped_points = []
for x, y in points:
x = x0 + (x - limits["x_min"]) / (limits["x_max"] - limits["x_min"]) * width
y = y0 + (limits["y_max"] - y) / (limits["y_max"] - limits["y_min"]) * height
mapped_points.append((x, y))
return mapped_points
def main() -> None:
lines = [
(0.04, 0.3, -0.01),
(-0.75, 0.1, -0.97),
(-0.14, 0.9, 0.96),
(1.14, 0.18, -1.05),
(1.27, -0.07, 0.04),
(-0.2, 0.24, -0.15),
(0.35, 1.34, -0.96),
(0.26, -0.9, -0.54)
]
limits = {"x_min": -1.7, "y_min": -1.7, "x_max": 1.7, "y_max": 1.7}
polygons = [
{"points": [(-1.7, -1.7), (-1.7, 1.7), (1.7, 1.7), (1.7, -1.7)], "color": "#ffffff"}
]
x0, y0 = 1, 1.5
size = 7.5
gap = 0.5
columns = 4
with Presentation(presentation_path="empty.pptx") as presentation:
for i, line in enumerate(lines):
polygons = split_polygons(polygons=polygons, line=line)
x = x0 + (size + gap) * (i % columns)
y = y0 + (size + gap) * (i // columns)
shapes = []
for polygon in polygons:
points = view_points(points=polygon["points"], limits=limits, x0=x, y0=y, width=size, height=size)
shapes.append(Polygon(points=points, fill=FillStyle(color=polygon["color"]), stroke=StrokeStyle(color="#222", thickness=0.5)))
presentation.add(shape=Group(shapes=shapes))
presentation.save("polygons.pptx")
if __name__ == "__main__":
main()

Слайды, созданные с помощью pptx-shapes
Здесь представлены лишь иллюстрации, что были сделаны с помощью библиотеки, так как сами слайды целиком показать я пока не могу.




Спасибо за внимание!
Надеюсь, pptx-shapes окажется для вас полезной – будь то для визуализации данных, генерации иллюстраций или просто красивых слайдов. Если у вас появятся идеи, вопросы или мысли, буду рад, если заглянете в репозиторий. Всегда приятно узнать, что библиотека кому-то пригодилась!
Комментарии (3)
IAMBIRD
19.05.2025 07:08Очень любил векторный редактор PowerPoint, пока не открыл для себя ультраудобный (и увы заброшенный) Expression Design. Если не изменяет память, выгруженный оттуда SVG можно было каким-нибудь сервисом (или даже инкскейпом) сконвертировать в WMF/EMF и уже такой вектор кажись офис принимал.
maxzh83
Когда у тебя в руке молоток, все становится похожим на гвозди. А когда python, везде хочется его применить. Как обучающая статья, вполне норм. А так еще есть макросы vb и трассировщики в svg.
pnmv
Vba, уже довольно давно, стало, чуть ли не ругательным словом, для многих из тех, кто никогда не пробовал им пользоваться или имел неудачный опыт применения. При этом, когда речь идёт о схожих вещах, допустим, в инструментах от google, отторжения ни у кого не возникает.