Настройка форм презентаций в Python через Java

Обзор

В этой статье объясняется, как настраивать формы презентаций в Aspose.Slides, редактируя геометрию формы через точки редактирования и пути геометрии. Показано, как работать с GeometryPath для изменения существующих форм, выполнять базовые операции редактирования пути, добавлять или удалять точки и применять обновлённую геометрию к форме.

Также демонстрируется, как создавать пользовательские и составные формы, строить формы со скруглёнными углами, определять, закрыта ли геометрия формы, исследовать элементы формы и преобразовывать контуры текста Java AWT в пути геометрии.

Изменение формы с помощью точек редактирования

В PowerPoint вы можете использовать Точки редактирования, чтобы изменить квадрат:

  • Переместить угол внутрь или наружу.
  • Скорректировать кривизну в точке.
  • Добавить новые точки.

Редактирование точек позволяет изменять контур формы или использовать существующую форму в качестве основы для новой.

Советы по редактированию форм

overview_image

При редактировании форм PowerPoint имейте в виду следующие концепции:

  • Форма (или её путь) может быть закрытой или открытой.
  • Путь формы состоит из опорных точек, соединённых сегментами.
  • Сегмент может быть прямым или кривым. Опорные точки и управляющие ручки определяют его форму.
  • Опорные точки бывают угловыми, прямыми или сглаженными:
    • В угловой точке смежные сегменты могут встречаться под углом.
    • В сглаженной точке две управляющие ручки выровнены и находятся на одинаковом расстоянии от опорной точки.
    • В прямой точке две управляющие ручки выровнены, но могут иметь разное расстояние от опорной точки.
  • Перемещение опорных точек или регулировка их ручек меняет контур формы.

Для редактирования форм PowerPoint через точки редактирования Aspose.Slides предоставляет класс GeometryPath.

Простые операции редактирования

Используйте следующие методы для редактирования пути геометрии. Перегрузки, принимающие индекс, вставляют сегмент в указанную позицию (нумерация с нуля) массива сегментов пути.

Операция Метод
Добавить или вставить прямую линию. GeometryPath.lineTo
Добавить или вставить кубическую кривую Безье с двумя управляющими точками и конечной точкой. GeometryPath.cubicBezierTo
Добавить или вставить квадратичную кривую Безье с одной управляющей точкой и конечной точкой. GeometryPath.quadraticBezierTo
Добавить эллиптическую дугу с указанными размерами и углами в градусах. GeometryPath.arcTo
Замкнуть текущую фигуру, соединив её конечную точку с начальной. GeometryPath.closeFigure
Установить начальную точку фигуры. GeometryPath.moveTo
Удалить сегмент по индексу, нумерация с нуля. GeometryPath.removeAt

Добавление пользовательских точек к форме

В этом примере вставляются две точки в прямоугольную форму с помощью GeometryPath.lineTo и применяется отредактированный путь через GeometryShape.setGeometryPath. Координаты локальны для формы: обе новые точки находятся в её центре, на 100 пунктов от левого края и 50 пунктов от верхнего. Индексы сегментов 1 и 4 помещают их на верхний и нижний края, притягивая эти края внутрь.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 200, 100)

    geometry_path = shape.getGeometryPaths()[0]
    geometry_path.lineTo(100, 50, 1)
    geometry_path.lineTo(100, 50, 4)

    shape.setGeometryPath(geometry_path)
finally:
    presentation.dispose()

example1_image

Удаление точек из формы

В этом примере меняется контур сердцевидной формы, удаляя сегмент с индексом 2 (нумерация с нуля) с помощью GeometryPath.removeAt. Применение изменённого пути через GeometryShape.setGeometryPath обновляет контур формы.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Heart, 100, 100, 300, 300)

    geometry_path = shape.getGeometryPaths()[0]
    geometry_path.removeAt(2)

    shape.setGeometryPath(geometry_path)
finally:
    presentation.dispose()

example2_image

Создание пользовательской формы

В этом примере создаётся пятиконечная звезда с помощью GeometryPath. Чередующиеся внешние и внутренние радиусы 100 и 50 пунктов задают вершины и врезы; шаг в 72 градуса равномерно размещает пять вершин. Путь закрывается методом GeometryPath.closeFigure и применяется к прямоугольной форме через GeometryShape.setGeometryPath, заменяя её контур.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, GeometryPath

import math

points = []
outer_radius = 100
inner_radius = 50
angle_step = 72

for angle in range(-90, 270, angle_step):
    radians = math.radians(angle)
    x = outer_radius * math.cos(radians)
    y = outer_radius * math.sin(radians)
    points.append((x + outer_radius, y + outer_radius))

    radians = math.radians(angle + angle_step / 2)
    x = inner_radius * math.cos(radians)
    y = inner_radius * math.sin(radians)
    points.append((x + outer_radius, y + outer_radius))

star_path = GeometryPath()
star_path.moveTo(*points[0])
for point in points[1:]:
    star_path.lineTo(*point)
star_path.closeFigure()

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, outer_radius * 2, outer_radius * 2)
    shape.setGeometryPath(star_path)
finally:
    presentation.dispose()

example3_image

Создание составной пользовательской формы

В этом примере определяются два закрытых объекта GeometryPath для верхней и нижней трети формы. Применение обоих путей через GeometryShape.setGeometryPaths создаёт одну составную форму с промежутком через её среднюю треть.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, GeometryPath

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 200, 100)

    top_path = GeometryPath()
    top_path.moveTo(0, 0)
    top_path.lineTo(shape.getWidth(), 0)
    top_path.lineTo(shape.getWidth(), shape.getHeight() / 3)
    top_path.lineTo(0, shape.getHeight() / 3)
    top_path.closeFigure()

    bottom_path = GeometryPath()
    bottom_path.moveTo(0, shape.getHeight() / 3 * 2)
    bottom_path.lineTo(shape.getWidth(), shape.getHeight() / 3 * 2)
    bottom_path.lineTo(shape.getWidth(), shape.getHeight())
    bottom_path.lineTo(0, shape.getHeight())
    bottom_path.closeFigure()

    shape.setGeometryPaths([top_path, bottom_path])
finally:
    presentation.dispose()

example4_image

Создание пользовательской формы со скруглёнными углами

В этом примере комбинируются прямые сегменты с GeometryPath.arcTo для создания формы с внутренне‑скруглёнными углами. Размер каждого угла задаёт ширину и высоту дуги в пунктах, а разворот ‑90 градусов рисует дугу против часовой стрелки – четверть эллипса. Готовый путь применяется через GeometryShape.setGeometryPath, и презентация сохраняется в формате PPTX.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, GeometryPath, SaveFormat

shape_x = 20
shape_y = 20
shape_width = 300
shape_height = 200

left_top_size = 50
right_top_size = 20
right_bottom_size = 40
left_bottom_size = 10

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Custom, shape_x, shape_y, shape_width, shape_height)

    geometry_path = GeometryPath()
    geometry_path.moveTo(left_top_size, 0)
    geometry_path.lineTo(shape_width - right_top_size, 0)
    geometry_path.arcTo(right_top_size, right_top_size, 180, -90)
    geometry_path.lineTo(shape_width, shape_height - right_bottom_size)
    geometry_path.arcTo(right_bottom_size, right_bottom_size, -90, -90)
    geometry_path.lineTo(left_bottom_size, shape_height)
    geometry_path.arcTo(left_bottom_size, left_bottom_size, 0, -90)
    geometry_path.lineTo(0, left_top_size)
    geometry_path.arcTo(left_top_size, left_top_size, 90, -90)
    geometry_path.closeFigure()
    
    shape.setGeometryPath(geometry_path)

    presentation.save("output.pptx", SaveFormat.Pptx)
finally:
    presentation.dispose()

Определение, закрыта ли геометрия формы

Следующая вспомогательная функция проверяет, заканчивается ли каждый непустой путь геометрии закрывающим сегментом. Она читает GeometryPath.getPathData и сравнивает конечный сегмент’s PathSegment.getPathCommand с PathCommandType.Close. Возвращает False, когда нет непустых путей. Проверка касается только последней команды каждого пути; она не исследует каждый подпуть внутри пути. В примере выводится True для прямоугольника.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, PathCommandType

def is_geometry_closed(geometry_shape):
    has_nonempty_path = False
    for geometry_path in geometry_shape.getGeometryPaths():
        path_data = geometry_path.getPathData()
        if len(path_data) == 0:
            continue
        last_segment = path_data[-1]
        has_nonempty_path = True
        if last_segment.getPathCommand() != PathCommandType.Close:
            return False
    return has_nonempty_path

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)
    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 200, 100)
    closed = is_geometry_closed(shape)
    print(closed)
finally:
    presentation.dispose()

Извлечение и анализ элементов формы

Используйте GeometryShape.createShapeElements для анализа существующей формы как набора элементов, содержащих точки пути и типы точек. Преобразуйте каждый полученный элемент к типу ShapeElement с помощью jpype.JObject, чтобы получить массивы точек и типов через методы getPathPoints и getPathTypes. Такое представление полезно для анализа контура или передачи геометрии в пользовательские инструменты рисования.

В примере открывается input.pptx, находится первая геометрическая форма на первом слайде и выводятся её сегменты пути. Используйте презентацию, содержащую форму с прямыми и кривыми сегментами, чтобы увидеть оба типа вывода. Поиск внутри групп не выполняется.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, GeometryShape, ShapeElement

presentation = Presentation("input.pptx")
try:
    slide = presentation.getSlides().get_Item(0)

    shape = None
    for candidate in slide.getShapes():
        if isinstance(candidate, GeometryShape):
            shape = candidate
            break

    if shape is None:
        print("No geometry shape found on the first slide.")
    else:
        for element in shape.createShapeElements():
            shape_element = jpype.JObject(element, ShapeElement)
            points = shape_element.getPathPoints()
            types = shape_element.getPathTypes()
            print("Shape element:")

            i = 0
            while i < len(points):
                point_type = types[i] & 0x07  # Keep the segment type, excluding flags.
                if point_type == 0:
                    print(f"Start figure at {points[i]}")
                elif point_type == 1:
                    print(f"Line to {points[i]}")
                elif point_type == 3:
                    print(f"Cubic Bezier: controls {points[i]}, {points[i + 1]}; end {points[i + 2]}")
                    i += 2  # Consume both control points and the endpoint.

                if types[i] & 0x80:
                    print("Close subpath back to its starting point.")
                i += 1
finally:
    presentation.dispose()

Массивы согласованы по индексу: points[i] — точка, а types[i] описывает её роль. Тип 0 начинается фигура, тип 1 — линия от предыдущей точки к текущей. Кубический сегмент Безье начинается от предыдущей конечной точки и использует три последовательных элемента типа 3: два управляющих шага и новую конечную точку. Цикл потребляет эти три элемента совместно и проверяет конечную точку на замыкание.

Типы точек также содержат флаги. Маска 0x07 определяет тип сегмента, а отдельная проверка 0x80 обнаруживает закрытый подпуть. Например, значение 129 (0x81) означает конечную точку линии, закрывающую подпуть, а 131 (0x83) — конечную точку Безье, закрывающую его. В Java эти флаги хранятся в знаковых байтах; маскирование сохраняет нужные битовые значения. Сравнение «сырых» значений только с 1 или 3 упустило бы такие случаи. Другие флаги, такие как пунктир (0x10) и маркер (0x20), не меняют базовый тип сегмента.

Координаты и преобразования. Точки измеряются в пунктах (1/72 дюйма), X растёт вправо, Y — вниз. Для не сгруппированной формы извлечённые координаты уже включают позицию, возвращаемую методами getX и getY, а также её текущую ширину и высоту. Например, не повернутый прямоугольник с координатами (100, 200) и размером 120 × 80 имеет границы контура (100, 200)–(220, 280). Не добавляйте позицию формы ещё раз. Вычтите её X и Y, чтобы получить координаты относительно её необёрнутой рамки.

Извлечённые точки не учитывают поворот формы, возвращаемый методом getRotation, а также горизонтальное и вертикальное отражения. Чтобы воспроизвести отображаемый контур, примените эти преобразования относительно центра формы. Для сгруппированных форм также учитывайте преобразования координат группы; не предполагаете, что извлечённые точки уже являются окончательными координатами слайда, и не применяйте трансляцию или масштаб дважды, если они уже учтены в кадре дочернего объекта.

Анализ vs. редактирование. Извлечённые массивы описывают контур; изменение их не обновляет исходную форму. Выбирайте такой подход, когда ваш код анализа или рисования нуждается в массивах точек/типов. Используйте getGeometryPaths, когда нужны команды геометрии в локальной системе координат формы или вы хотите редактировать её геометрию. Применяйте отредактированные пути через setGeometryPath или setGeometryPaths.

Преобразование Java AWT Shape в GeometryPath

В этом примере комбинируется прямоугольный контур с текстом, преобразованным в геометрию. Метод setFillMode контура отключает его заливку. GlyphVector создаёт контур текста в объекте java.awt.Shape. Его PathIterator поставляет команды перемещения, линии, квадратичной, кубической и закрывающей сегменты, которые пример воспроизводит на GeometryPath.

Текст использует шрифт Arial размером 40 пунктов. Контур сдвинут на 20 пунктов по горизонтали и позиционируется так, чтобы его визуальная верхняя часть находилась на 10 пунктов ниже верхнего края формы. Текстовый путь заполняется, в то время как исходный прямоугольник остаётся контуром. Применение обоих путей через GeometryShape.setGeometryPaths создаёт единую форму, содержащую прямоугольник и текст.

import jpype
import asposeslides

if not jpype.isJVMStarted():
    jpype.startJVM()

from asposeslides.api import Presentation, ShapeType, GeometryPath, PathFillModeType

from java.awt import Font
from java.awt.geom import PathIterator
from java.awt.image import BufferedImage

presentation = Presentation()
try:
    slide = presentation.getSlides().get_Item(0)

    shape = slide.getShapes().addAutoShape(ShapeType.Rectangle, 100, 100, 300, 100)

    # Получить путь геометрии формы.
    original_path = shape.getGeometryPaths()[0]
    original_path.setFillMode(PathFillModeType.None_)

    # Создать новый графический путь с текстом.
    font = Font("Arial", Font.PLAIN, 40)
    text = "Text in shape"
    image = BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB)
    graphics = image.createGraphics()
    try:
        font_context = graphics.getFontRenderContext()
        glyph_vector = font.createGlyphVector(font_context, text)
        text_bounds = glyph_vector.getVisualBounds()
        baseline = -text_bounds.getY() + 10
        graphics_path = glyph_vector.getOutline(20.0, baseline)
    finally:
        graphics.dispose()

    # Преобразовать графический путь в путь геометрии.
    text_path = GeometryPath()
    path_iterator = graphics_path.getPathIterator(None)
    points = jpype.JArray(jpype.JFloat)(6)
    while not path_iterator.isDone():
        segment_type = path_iterator.currentSegment(points)
        if segment_type == PathIterator.SEG_MOVETO:
            text_path.moveTo(points[0], points[1])
        elif segment_type == PathIterator.SEG_LINETO:
            text_path.lineTo(points[0], points[1])
        elif segment_type == PathIterator.SEG_QUADTO:
            text_path.quadraticBezierTo(points[0], points[1], points[2], points[3])
        elif segment_type == PathIterator.SEG_CUBICTO:
            text_path.cubicBezierTo(points[0], points[1], points[2], points[3], points[4], points[5])
        elif segment_type == PathIterator.SEG_CLOSE:
            text_path.closeFigure()
        path_iterator.next()
    text_path.setFillMode(PathFillModeType.Normal)

    # Применить путь текста вместе с оригинальным путем геометрии.
    shape.setGeometryPaths([original_path, text_path])
finally:
    presentation.dispose()

example5_image

FAQ

Что произойдёт с заливкой и контурами после замены геометрии?

Стиль остаётся привязанным к форме; меняется только контур. Заливка и контур автоматически применяются к новой геометрии.

Как правильно повернуть пользовательскую форму вместе с её геометрией?

Используйте метод формы setRotation; геометрия вращается вместе с формой, потому что она привязана к её собственной системе координат.

Можно ли преобразовать пользовательскую форму в изображение, чтобы «зафиксировать» результат?

Да. Экспортируйте нужную slide область или саму shape в растровый формат; это упростит дальнейшую работу со сложными геометриями.