Files
sci-gui-agent-benchmark/mm_agents/autoglm/tools/package/libreoffice_impress.py
Adam Yanxiao Zhao aa05f6cc26 Add AutoGLM-OS agent (#309)
* autoglm-os initialize

* clean code

* chore: use proxy for download setup

* feat(autoglm-os): add parameter to toggle images

* fix: use temporary directory for files pulled from the vm to prevent potential collision when running multiple instances of the same task in parallel

* update

* add client_password

* update multienv

* fix

* fix prompt

* fix prompt

* fix prompt

* fix sys prompt

* feat: use proxy in file evaluator

* fix client_password

* fix note_prompt

* fix autoglm agent cmd type

* fix

* revert: fix: use temporary directory for files pulled from the vm to prevent potential collision when running multiple instances of the same task in parallel

reverts commit bab5473eea1de0e61b0e1d68b23ce324a5b0ee57

* feat(autoglm): setup tools

* fix(autoglm): remove second time of get a11y tree

* add osworld server restart

* Revert "add osworld server restart"

This reverts commit 7bd9d84122e246ce2a26de0e49c25494244c2b3d.

* fix _launch_setup

* fix autoglm agent tools & xml tree

* fix desktop_env

* fix bug for tool name capitalization

* fix: always use proxy for setup download

* add fail after exceeding max turns

* fix(autoglm): avoid adding image to message when screenshot is empty

* fix maximize_window

* fix maximize_window

* fix maximize_window

* fix import browsertools module bug

* fix task proxy config bug

* restore setup

* refactor desktop env

* restore image in provider

* restore file.py

* refactor desktop_env

* quick fix

* refactor desktop_env.step

* fix our env reset

* add max truns constraint

* clean run script

* clean lib_run_single.py

---------

Co-authored-by: hanyullai <hanyullai@outlook.com>
Co-authored-by: JingBh <jingbohao@yeah.net>
2025-08-17 12:08:40 +08:00

1425 lines
66 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import os
import uno
from com.sun.star.awt.FontSlant import ITALIC, NONE
from com.sun.star.awt.FontWeight import BOLD, NORMAL
from com.sun.star.beans import PropertyValue
from com.sun.star.drawing.TextHorizontalAdjust import CENTER, LEFT, RIGHT
class ImpressTools:
localContext = uno.getComponentContext()
resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext)
ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
desktop = ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
doc = desktop.getCurrentComponent()
ret = ""
@classmethod
def close_other_window(cls):
"""关闭除当前文档外的所有文档"""
components = cls.desktop.getComponents().createEnumeration()
current_url = cls.doc.getURL()
while components.hasMoreElements():
doc = components.nextElement()
if doc.getURL() != current_url:
doc.close(True)
@classmethod
def save(cls):
"""保存文档到当前位置"""
try:
if cls.doc.hasLocation():
cls.doc.store()
cls.ret = "Success"
else:
cls.ret = "Error: Document has no save location"
return True
except Exception as e:
cls.ret = f"Error: {e}"
return False
@classmethod
def maximize_window(cls):
"""
将窗口设置为工作区最大尺寸
使用工作区域大小(考虑任务栏等)
"""
window = cls.doc.getCurrentController().getFrame().getContainerWindow()
toolkit = window.getToolkit()
device = toolkit.createScreenCompatibleDevice(0, 0)
workarea = toolkit.getWorkArea()
window.setPosSize(workarea.X, workarea.Y, workarea.Width, workarea.Height, 15)
@classmethod
def print_result(cls):
print(cls.ret)
@classmethod
def env_info(cls, page_indices=None):
"""
获取指定页面的内容
:param page_indices: 页码列表如果为None则获取所有页面
:return: 包含各页面内容的列表
"""
try:
pages = cls.doc.getDrawPages()
content_str = ""
if page_indices is None:
page_indices = range(pages.getCount())
for page_index in page_indices:
if 0 <= page_index < pages.getCount():
page = pages.getByIndex(page_index)
page_content = []
for i in range(page.getCount()):
shape = page.getByIndex(i)
if hasattr(shape, "getText"):
text = shape.getText()
if text:
page_content.append("- Box " + str(i) + ": " + text.getString().strip())
c = "\n".join(page_content)
content_str += f"Slide {page_index+1}:\n{c}\n\n"
cur_idx = cls.get_current_slide_index() + 1
content_str = content_str + f"Current Slide Index: {cur_idx}"
cls.ret = content_str
return content_str
except Exception as e:
cls.ret = f"Error: {str(e)}"
return []
@classmethod
def get_current_slide_index(cls):
"""
Gets the index of the currently active slide in the presentation.
:return: The index of the currently active slide (0-based)
"""
try:
controller = cls.doc.getCurrentController()
current_page = controller.getCurrentPage()
pages = cls.doc.getDrawPages()
for i in range(pages.getCount()):
if pages.getByIndex(i) == current_page:
cls.ret = i
return i
cls.ret = "Current slide not found"
return -1
except Exception as e:
cls.ret = f"Error: {str(e)}"
return -1
@classmethod
def go_to_slide(cls, slide_index):
"""
Navigates to a specific slide in the presentation based on its index.
Args:
slide_index (int): The index of the slide to navigate to (1-based indexing)
Returns:
bool: True if navigation was successful, False otherwise
"""
try:
zero_based_index = slide_index - 1
controller = cls.doc.getCurrentController()
if not controller:
cls.ret = "Error: Could not get document controller"
return False
pages = cls.doc.getDrawPages()
if zero_based_index < 0 or zero_based_index >= pages.getCount():
cls.ret = f"Error: Slide index {slide_index} is out of range. Valid range is 1-{pages.getCount()}"
return False
target_slide = pages.getByIndex(zero_based_index)
controller.setCurrentPage(target_slide)
cls.ret = f"Successfully navigated to slide {slide_index}"
return True
except Exception as e:
cls.ret = f"Error navigating to slide: {str(e)}"
return False
@classmethod
def get_slide_count(cls):
"""
Gets the total number of slides in the current presentation.
:return: The total number of slides as an integer
"""
try:
pages = cls.doc.getDrawPages()
count = pages.getCount()
cls.ret = count
return count
except Exception as e:
cls.ret = f"Error: {str(e)}"
return 0
@classmethod
def duplicate_slide(cls, slide_index):
"""
Creates a duplicate of a specific slide and places it at the end of the presentation.
:param slide_index: The index of the slide to duplicate (1-based indexing)
:return: True if successful, False otherwise
"""
try:
zero_based_index = slide_index - 1
draw_pages = cls.doc.getDrawPages()
if zero_based_index < 0 or zero_based_index >= draw_pages.getCount():
cls.ret = f"Error: Invalid slide index {slide_index}. Valid range is 1 to {draw_pages.getCount()}"
return False
controller = cls.doc.getCurrentController()
controller.setCurrentPage(draw_pages.getByIndex(zero_based_index))
dispatcher = cls.ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.DispatchHelper", cls.ctx)
frame = controller.getFrame()
dispatcher.executeDispatch(frame, ".uno:DuplicatePage", "", 0, ())
duplicated_slide_index = zero_based_index + 1
slide_count = draw_pages.getCount()
if duplicated_slide_index < slide_count - 1:
controller.setCurrentPage(draw_pages.getByIndex(duplicated_slide_index))
moves_needed = slide_count - duplicated_slide_index - 1
for _ in range(moves_needed):
dispatcher.executeDispatch(frame, ".uno:MovePageDown", "", 0, ())
cls.ret = f"Slide {slide_index} duplicated successfully and moved to the end"
return True
except Exception as e:
cls.ret = f"Error: {str(e)}"
return False
@classmethod
def set_slide_font(cls, slide_index, font_name):
"""
Sets the font style for all text elements in a specific slide, including the title.
Args:
slide_index (int): The index of the slide to modify (1-based indexing)
font_name (str): The name of the font to apply (e.g., 'Arial', 'Times New Roman', 'Calibri')
Returns:
bool: True if successful, False otherwise
"""
try:
zero_based_index = slide_index - 1
slides = cls.doc.getDrawPages()
if zero_based_index < 0 or zero_based_index >= slides.getCount():
cls.ret = f"Error: Slide index {slide_index} is out of range. Valid range is 1 to {slides.getCount()}."
return False
slide = slides.getByIndex(zero_based_index)
for i in range(slide.getCount()):
shape = slide.getByIndex(i)
if hasattr(shape, "getText"):
text = shape.getText()
if text:
cursor = text.createTextCursor()
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.setPropertyValue("CharFontName", font_name)
cls.ret = f"Successfully set font to '{font_name}' for all text elements in slide {slide_index}."
return True
except Exception as e:
cls.ret = f"Error setting font: {str(e)}"
return False
@classmethod
def write_text(cls, content, page_index, box_index, bold=False, italic=False, size=None, append=False):
"""
Writes text to a specific textbox on a slide
:param content: The text content to add
:param page_index: The index of the slide (1-based indexing)
:param box_index: The index of the textbox to modify (0-based indexing)
:param bold: Whether to make the text bold, default is False
:param italic: Whether to make the text italic, default is False
:param size: The size of the text. If None, uses the box's current font size.
:param append: Whether to append the text, default is False. If you want to observe some formats(like a bullet at the beginning) or keep the original text, you should set up it.
:return: True if successful, False otherwise
"""
try:
zero_based_page_index = page_index - 1
pages = cls.doc.getDrawPages()
if zero_based_page_index < 0 or zero_based_page_index >= pages.getCount():
cls.ret = f"Error: Page index {page_index} is out of range"
return False
page = pages.getByIndex(zero_based_page_index)
if box_index < 0 or box_index >= page.getCount():
cls.ret = f"Error: Box index {box_index} is out of range"
return False
shape = page.getByIndex(box_index)
if not hasattr(shape, "String"):
cls.ret = f"Error: The shape at index {box_index} cannot contain text"
return False
if append:
shape.String = shape.String + content
else:
shape.String = content
if hasattr(shape, "getCharacterProperties"):
char_props = shape.getCharacterProperties()
if bold:
char_props.CharWeight = BOLD
else:
char_props.CharWeight = NORMAL
if italic:
char_props.CharPosture = ITALIC
else:
char_props.CharPosture = NONE
if size is not None:
char_props.CharHeight = size
cls.ret = f"Text successfully written to page {page_index}, box {box_index}"
return True
except Exception as e:
cls.ret = f"Error: {str(e)}"
return False
@classmethod
def set_style(cls, slide_index, box_index, bold=None, italic=None, underline=None):
"""
Sets the style properties for the specified textbox on a slide.
:param slide_index: The index of the slide to modify (1-based indexing)
:param box_index: The index of the textbox to modify (0-based indexing)
:param bold: Whether to make the text bold
:param italic: Whether to make the text italic
:param underline: Whether to underline the text
:return: True if successful, False otherwise
"""
try:
pages = cls.doc.getDrawPages()
if slide_index < 1 or slide_index > pages.getCount():
cls.ret = f"Error: Invalid slide index {slide_index}. Valid range is 1 to {pages.getCount()}"
return False
page = pages.getByIndex(slide_index - 1)
if box_index < 0 or box_index >= page.getCount():
cls.ret = f"Error: Invalid box index {box_index}. Valid range is 0 to {page.getCount() - 1}"
return False
shape = page.getByIndex(box_index)
if not hasattr(shape, "getText"):
cls.ret = "Error: The specified shape does not contain text"
return False
text = shape.getText()
cursor = text.createTextCursor()
cursor.gotoStart(False)
cursor.gotoEnd(True)
if bold is not None:
cursor.setPropertyValue("CharWeight", BOLD if bold else NORMAL)
if italic is not None:
cursor.setPropertyValue("CharPosture", ITALIC if italic else NONE)
if underline is not None:
cursor.setPropertyValue("CharUnderline", 1 if underline else 0)
cls.ret = "Style applied successfully"
return True
except Exception as e:
cls.ret = f"Error: {str(e)}"
return False
@classmethod
def configure_auto_save(cls, enabled, interval_minutes):
"""
Enables or disables auto-save functionality for the current document and sets the auto-save interval.
:param enabled: Whether to enable (True) or disable (False) auto-save
:param interval_minutes: The interval in minutes between auto-saves (minimum 1 minute)
:return: True if successful, False otherwise
"""
try:
if interval_minutes < 1:
interval_minutes = 1
config_provider = cls.ctx.ServiceManager.createInstanceWithContext(
"com.sun.star.configuration.ConfigurationProvider", cls.ctx
)
prop = PropertyValue()
prop.Name = "nodepath"
prop.Value = "/org.openoffice.Office.Common/Save/Document"
config_access = config_provider.createInstanceWithArguments(
"com.sun.star.configuration.ConfigurationUpdateAccess", (prop,)
)
config_access.setPropertyValue("AutoSave", enabled)
config_access.setPropertyValue("AutoSaveTimeIntervall", interval_minutes)
config_access.commitChanges()
cls.ret = f"Auto-save {'enabled' if enabled else 'disabled'} with interval of {interval_minutes} minutes"
return True
except Exception as e:
cls.ret = f"Error configuring auto-save: {str(e)}"
return False
@classmethod
def set_background_color(cls, slide_index, box_index, color):
"""
Sets the background color for the specified textbox on a slide.
Args:
slide_index (int): The index of the slide containing the textbox (1-based indexing)
box_index (int): The index of the textbox to modify (0-based indexing)
color (str): The color to apply to the textbox (e.g., 'red', 'green', 'blue', 'yellow', or hex color code)
Returns:
bool: True if successful, False otherwise
"""
try:
zero_based_slide_index = slide_index - 1
slides = cls.doc.getDrawPages()
if zero_based_slide_index < 0 or zero_based_slide_index >= slides.getCount():
cls.ret = f"Error: Slide index {slide_index} is out of range"
return False
slide = slides.getByIndex(zero_based_slide_index)
if box_index < 0 or box_index >= slide.getCount():
cls.ret = f"Error: Box index {box_index} is out of range"
return False
shape = slide.getByIndex(box_index)
color_int = 0
color_map = {
"red": 16711680,
"green": 65280,
"blue": 255,
"yellow": 16776960,
"black": 0,
"white": 16777215,
"purple": 8388736,
"orange": 16753920,
"pink": 16761035,
"gray": 8421504,
"brown": 10824234,
"cyan": 65535,
"magenta": 16711935,
}
if color.lower() in color_map:
color_int = color_map[color.lower()]
elif color.startswith("#") and len(color) == 7:
color_int = int(color[1:], 16)
else:
cls.ret = f"Error: Invalid color format: {color}"
return False
shape.FillStyle = uno.Enum("com.sun.star.drawing.FillStyle", "SOLID")
shape.FillColor = color_int
cls.ret = f"Background color of textbox {box_index} on slide {slide_index} set to {color}"
return True
except Exception as e:
cls.ret = f"Error: {str(e)}"
return False
@classmethod
def set_text_color(cls, slide_index, box_index, color):
"""
Sets the text color for the specified textbox on a slide.
Args:
slide_index (int): The index of the slide to modify (1-based indexing)
box_index (int): The index of the textbox to modify (0-based indexing)
color (str): The color to apply to the text (e.g., 'red', 'green', 'blue', 'black', or hex color code)
Returns:
bool: True if successful, False otherwise
"""
try:
zero_based_slide_index = slide_index - 1
slides = cls.doc.getDrawPages()
if zero_based_slide_index < 0 or zero_based_slide_index >= slides.getCount():
cls.ret = f"Error: Slide index {slide_index} is out of range"
return False
slide = slides.getByIndex(zero_based_slide_index)
if box_index < 0 or box_index >= slide.getCount():
cls.ret = f"Error: Box index {box_index} is out of range"
return False
shape = slide.getByIndex(box_index)
if not hasattr(shape, "getText"):
cls.ret = f"Error: Shape at index {box_index} does not contain text"
return False
color_int = 0
if color.startswith("#"):
color_int = int(color[1:], 16)
else:
color_map = {
"red": 16711680,
"green": 43315,
"blue": 255,
"black": 0,
"white": 16777215,
"yellow": 16776960,
"cyan": 65535,
"magenta": 16711935,
"gray": 8421504,
}
if color.lower() in color_map:
color_int = color_map[color.lower()]
else:
cls.ret = f"Error: Unsupported color '{color}'"
return False
text = shape.getText()
cursor = text.createTextCursor()
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.setPropertyValue("CharColor", color_int)
cls.ret = f"Successfully set text color to {color} for textbox {box_index} on slide {slide_index}"
return True
except Exception as e:
cls.ret = f"Error: {str(e)}"
return False
@classmethod
def delete_content(cls, slide_index, box_index):
"""
Deletes the specified textbox from a slide.
:param slide_index: The index of the slide to modify (1-based indexing)
:param box_index: The index of the textbox to modify (0-based indexing)
:return: True if successful, False otherwise
"""
try:
pages = cls.doc.getDrawPages()
zero_based_slide_index = slide_index - 1
if zero_based_slide_index < 0 or zero_based_slide_index >= pages.getCount():
cls.ret = f"Error: Invalid slide index {slide_index}. Valid range is 1 to {pages.getCount()}"
return False
slide = pages.getByIndex(zero_based_slide_index)
if box_index < 0 or box_index >= slide.getCount():
cls.ret = f"Error: Invalid box index {box_index}. Valid range is 0 to {slide.getCount() - 1}"
return False
shape = slide.getByIndex(box_index)
slide.remove(shape)
cls.ret = f"Successfully deleted textbox {box_index} from slide {slide_index}"
return True
except Exception as e:
cls.ret = f"Error: {str(e)}"
return False
@classmethod
def set_slide_orientation(cls, orientation):
"""
Changes the orientation of slides in the presentation between portrait (upright) and landscape (sideways).
:param orientation: The desired orientation for the slides ('portrait' or 'landscape')
:return: True if successful, False otherwise
"""
try:
draw_pages = cls.doc.getDrawPages()
first_page = draw_pages.getByIndex(0)
current_width = first_page.Width
current_height = first_page.Height
if orientation == "portrait" and current_width > current_height:
new_width, new_height = current_height, current_width
elif orientation == "landscape" and current_width < current_height:
new_width, new_height = current_height, current_width
else:
cls.ret = f"Slides are already in {orientation} orientation"
return True
for i in range(draw_pages.getCount()):
page = draw_pages.getByIndex(i)
page.Width = new_width
page.Height = new_height
cls.ret = f"Changed slide orientation to {orientation}"
return True
except Exception as e:
cls.ret = f"Error changing slide orientation: {str(e)}"
return False
@classmethod
def position_box(cls, slide_index, box_index, position):
"""
Positions a textbox or image on a slide at a specific location or predefined position.
:param slide_index: The index of the slide containing the box (1-based indexing)
:param box_index: The index of the box to position (0-based indexing)
:param position: Predefined position on the slide (left, right, center, top, bottom, etc.)
:return: True if successful, False otherwise
"""
try:
pages = cls.doc.getDrawPages()
if slide_index < 1 or slide_index > pages.getCount():
cls.ret = f"Error: Invalid slide index {slide_index}"
return False
page = pages.getByIndex(slide_index - 1)
if box_index < 0 or box_index >= page.getCount():
cls.ret = f"Error: Invalid box index {box_index}"
return False
shape = page.getByIndex(box_index)
controller = cls.doc.getCurrentController()
slide_width = 28000
slide_height = 21000
shape_width = shape.Size.Width
shape_height = shape.Size.Height
margin = 500
if position == "left":
new_x = margin
new_y = (slide_height - shape_height) / 2
elif position == "right":
new_x = slide_width - shape_width - margin
new_y = (slide_height - shape_height) / 2
elif position == "center":
new_x = (slide_width - shape_width) / 2
new_y = (slide_height - shape_height) / 2
elif position == "top":
new_x = (slide_width - shape_width) / 2
new_y = margin
elif position == "bottom":
new_x = (slide_width - shape_width) / 2
new_y = slide_height - shape_height - margin
elif position == "top-left":
new_x = margin
new_y = margin
elif position == "top-right":
new_x = slide_width - shape_width - margin
new_y = margin
elif position == "bottom-left":
new_x = margin
new_y = slide_height - shape_height - margin
elif position == "bottom-right":
new_x = slide_width - shape_width - margin
new_y = slide_height - shape_height - margin
else:
cls.ret = f"Error: Invalid position '{position}'"
return False
try:
shape.Position.X = int(new_x)
shape.Position.Y = int(new_y)
except:
try:
shape.setPropertyValue("PositionX", int(new_x))
shape.setPropertyValue("PositionY", int(new_y))
except:
point = uno.createUnoStruct("com.sun.star.awt.Point", int(new_x), int(new_y))
shape.setPosition(point)
cls.ret = f"Box positioned at {position} (X: {new_x}, Y: {new_y})"
return True
except Exception as e:
cls.ret = f"Error positioning box: {str(e)}"
return False
@classmethod
def insert_file(cls, file_path, slide_index=None, position=None, size=None, autoplay=False):
"""
Inserts a video file into the current or specified slide in the presentation.
Args:
file_path (str): The full path to the video file to be inserted
slide_index (int, optional): The index of the slide to insert the video into (1-based indexing).
If not provided, inserts into the current slide.
position (dict, optional): The position coordinates for the video as percentages of slide dimensions
{'x': float, 'y': float}
size (dict, optional): The size dimensions for the video as percentages of slide dimensions
{'width': float, 'height': float}
autoplay (bool, optional): Whether the video should automatically play when the slide is shown
Returns:
bool: True if successful, False otherwise
"""
try:
expanded_file_path = os.path.expanduser(file_path)
if not os.path.exists(expanded_file_path):
cls.ret = f"Error: File not found: {expanded_file_path}"
return False
file_url = uno.systemPathToFileUrl(os.path.abspath(expanded_file_path))
pages = cls.doc.getDrawPages()
if slide_index is not None:
zero_based_index = slide_index - 1
if zero_based_index < 0 or zero_based_index >= pages.getCount():
cls.ret = f"Error: Invalid slide index: {slide_index}"
return False
slide = pages.getByIndex(zero_based_index)
else:
controller = cls.doc.getCurrentController()
slide = controller.getCurrentPage()
slide_width = 21000
slide_height = 12750
if position is None:
position = {"x": 10, "y": 10}
if size is None:
size = {"width": 80, "height": 60}
x = int(position["x"] * slide_width / 100)
y = int(position["y"] * slide_height / 100)
width = int(size["width"] * slide_width / 100)
height = int(size["height"] * slide_height / 100)
media_shape = cls.doc.createInstance("com.sun.star.presentation.MediaShape")
slide.add(media_shape)
media_shape.setPosition(uno.createUnoStruct("com.sun.star.awt.Point", x, y))
media_shape.setSize(uno.createUnoStruct("com.sun.star.awt.Size", width, height))
media_shape.setPropertyValue("MediaURL", file_url)
if autoplay:
try:
media_shape.setPropertyValue("MediaIsAutoPlay", True)
except:
pass
cls.ret = f"Video inserted successfully from {expanded_file_path}"
return True
except Exception as e:
cls.ret = f"Error inserting video: {str(e)}"
return False
@classmethod
def set_slide_background(cls, slide_index=None, color=None, image_path=None):
"""
Sets the background color or image for a specific slide or all slides.
Args:
slide_index (int, optional): The index of the slide to modify (1-based indexing).
If not provided, applies to all slides.
color (str, optional): The background color to apply (e.g., 'red', 'green', 'blue', or hex color code)
image_path (str, optional): Path to an image file to use as background. If provided, overrides color.
Returns:
bool: True if successful, False otherwise
"""
try:
if not color and not image_path:
cls.ret = "Error: Either color or image_path must be provided"
return False
pages = cls.doc.getDrawPages()
page_count = pages.getCount()
rgb_color = None
if color:
if color.startswith("#"):
color = color.lstrip("#")
rgb_color = int(color, 16)
else:
color_map = {
"red": 16711680,
"green": 43315,
"blue": 255,
"black": 0,
"white": 16777215,
"yellow": 16776960,
"cyan": 65535,
"magenta": 16711935,
"gray": 8421504,
}
rgb_color = color_map.get(color.lower(), 0)
if slide_index is not None:
slide_index = slide_index - 1
if slide_index < 0 or slide_index >= page_count:
cls.ret = f"Error: Slide index {slide_index + 1} is out of range (1-{page_count})"
return False
slides_to_modify = [pages.getByIndex(slide_index)]
else:
slides_to_modify = [pages.getByIndex(i) for i in range(page_count)]
for slide in slides_to_modify:
fill_props = cls.ctx.ServiceManager.createInstanceWithContext(
"com.sun.star.drawing.FillProperties", cls.ctx
)
if image_path and os.path.exists(image_path):
abs_path = os.path.abspath(image_path)
file_url = uno.systemPathToFileUrl(abs_path)
fill_props.FillStyle = uno.Enum("com.sun.star.drawing.FillStyle", "BITMAP")
fill_props.FillBitmapURL = file_url
fill_props.FillBitmapMode = uno.Enum("com.sun.star.drawing.BitmapMode", "STRETCH")
elif rgb_color is not None:
fill_props.FillStyle = uno.Enum("com.sun.star.drawing.FillStyle", "SOLID")
fill_props.FillColor = rgb_color
slide.setPropertyValue("Background", fill_props)
cls.ret = "Background set successfully"
return True
except Exception as e:
cls.ret = f"Error setting background: {str(e)}"
return False
@classmethod
def save_as(cls, file_path, overwrite=False):
"""
Saves the current document to a specified location with a given filename.
:param file_path: The full path where the file should be saved, including the filename and extension
:param overwrite: Whether to overwrite the file if it already exists (default: False)
:return: True if successful, False otherwise
"""
try:
if os.path.exists(file_path) and not overwrite:
cls.ret = f"File already exists and overwrite is set to False: {file_path}"
return False
abs_path = os.path.abspath(file_path)
if os.name == "nt":
url = "file:///" + abs_path.replace("\\", "/")
else:
url = "file://" + abs_path
properties = []
overwrite_prop = PropertyValue()
overwrite_prop.Name = "Overwrite"
overwrite_prop.Value = overwrite
properties.append(overwrite_prop)
extension = os.path.splitext(file_path)[1].lower()
if extension == ".odp":
filter_name = "impress8"
elif extension == ".ppt":
filter_name = "MS PowerPoint 97"
elif extension == ".pptx":
filter_name = "Impress MS PowerPoint 2007 XML"
elif extension == ".pdf":
filter_name = "impress_pdf_Export"
else:
filter_name = "impress8"
filter_prop = PropertyValue()
filter_prop.Name = "FilterName"
filter_prop.Value = filter_name
properties.append(filter_prop)
cls.doc.storeAsURL(url, tuple(properties))
cls.ret = f"Document saved successfully to {file_path}"
return True
except Exception as e:
cls.ret = f"Error saving document: {str(e)}"
return False
@classmethod
def insert_image(cls, slide_index, image_path, width=None, height=None, position=None):
"""
Inserts an image to a specific slide in the presentation.
Args:
slide_index (int): The index of the slide to add the image to (1-based indexing)
image_path (str): The full path to the image file to be added
width (float, optional): The width of the image in centimeters
height (float, optional): The height of the image in centimeters
position (dict, optional): The position coordinates for the image as percentages
{
'x': float, # The x-coordinate as a percentage of slide width
'y': float # The y-coordinate as a percentage of slide height
}
Returns:
bool: True if successful, False otherwise
"""
try:
if not os.path.exists(image_path):
cls.ret = f"Error: Image file not found at {image_path}"
return False
zero_based_index = slide_index - 1
slides = cls.doc.getDrawPages()
if zero_based_index < 0 or zero_based_index >= slides.getCount():
cls.ret = f"Error: Slide index {slide_index} is out of range. Valid range is 1 to {slides.getCount()}"
return False
slide = slides.getByIndex(zero_based_index)
bitmap = cls.doc.createInstance("com.sun.star.drawing.BitmapTable")
image_url = uno.systemPathToFileUrl(os.path.abspath(image_path))
shape = cls.doc.createInstance("com.sun.star.drawing.GraphicObjectShape")
shape.setPropertyValue("GraphicURL", image_url)
slide.add(shape)
x_pos = 0
y_pos = 0
slide_width = slide.Width
slide_height = slide.Height
if position:
if "x" in position:
x_pos = int(position["x"] / 100 * slide_width)
if "y" in position:
y_pos = int(position["y"] / 100 * slide_height)
current_width = shape.Size.Width
current_height = shape.Size.Height
new_width = int(width * 1000) if width is not None else current_width
new_height = int(height * 1000) if height is not None else current_height
size = uno.createUnoStruct("com.sun.star.awt.Size")
size.Width = new_width
size.Height = new_height
point = uno.createUnoStruct("com.sun.star.awt.Point")
point.X = x_pos
point.Y = y_pos
shape.Size = size
shape.Position = point
cls.ret = f"Image inserted successfully on slide {slide_index}"
return True
except Exception as e:
cls.ret = f"Error inserting image: {str(e)}"
return False
@classmethod
def configure_display_settings(
cls, use_presenter_view=None, primary_monitor_only=None, monitor_for_presentation=None
):
"""
Configures the display settings for LibreOffice Impress presentations.
Args:
use_presenter_view (bool, optional): Whether to use presenter view. Set to false to disable presenter view.
primary_monitor_only (bool, optional): Whether to use only the primary monitor for the presentation.
monitor_for_presentation (int, optional): Specify which monitor to use (1 for primary, 2 for secondary, etc.)
Returns:
bool: True if settings were successfully applied, False otherwise
"""
try:
controller = cls.doc.getCurrentController()
if not hasattr(controller, "getPropertyValue"):
cls.ret = "Error: Not an Impress presentation or controller not available"
return False
if use_presenter_view is not None:
try:
controller.setPropertyValue("IsPresentationViewEnabled", use_presenter_view)
except Exception as e:
cls.ret = f"Warning: Could not set presenter view: {str(e)}"
if primary_monitor_only is not None:
try:
controller.setPropertyValue("UsePrimaryMonitorOnly", primary_monitor_only)
except Exception as e:
cls.ret = f"Warning: Could not set primary monitor usage: {str(e)}"
if monitor_for_presentation is not None:
try:
controller.setPropertyValue("MonitorForPresentation", monitor_for_presentation - 1)
except Exception as e:
cls.ret = f"Warning: Could not set presentation monitor: {str(e)}"
cls.ret = "Display settings configured successfully"
return True
except Exception as e:
cls.ret = f"Error configuring display settings: {str(e)}"
return False
@classmethod
def set_text_strikethrough(cls, slide_index, box_index, line_numbers, apply):
"""
Applies or removes strike-through formatting to specific text content in a slide.
Args:
slide_index (int): The index of the slide containing the text (1-based indexing)
box_index (int): The index of the textbox containing the text (0-based indexing)
line_numbers (list): The line numbers to apply strike-through formatting to (1-based indexing)
apply (bool): Whether to apply (true) or remove (false) strike-through formatting
Returns:
bool: True if successful, False otherwise
"""
try:
slides = cls.doc.getDrawPages()
slide = slides.getByIndex(slide_index - 1)
shape = slide.getByIndex(box_index)
if not hasattr(shape, "getText"):
cls.ret = f"Error: Shape at index {box_index} does not contain text"
return False
text = shape.getText()
cursor = text.createTextCursor()
text_content = text.getString()
lines = text_content.split("\n")
for line_number in line_numbers:
if 1 <= line_number <= len(lines):
start_pos = 0
for i in range(line_number - 1):
start_pos += len(lines[i]) + 1
end_pos = start_pos + len(lines[line_number - 1])
cursor.gotoStart(False)
cursor.goRight(start_pos, False)
cursor.goRight(len(lines[line_number - 1]), True)
cursor.CharStrikeout = apply
cls.ret = f"Strike-through {'applied' if apply else 'removed'} successfully"
return True
except Exception as e:
cls.ret = f"Error: {str(e)}"
return False
@classmethod
def set_textbox_alignment(cls, slide_index, box_index, alignment):
"""
Sets the text alignment for the specified textbox on a slide.
:param slide_index: The index of the slide to modify (1-based indexing)
:param box_index: The index of the textbox to modify (0-based indexing)
:param alignment: The text alignment to apply ('left', 'center', 'right', or 'justify')
:return: True if successful, False otherwise
"""
try:
zero_based_slide_index = slide_index - 1
slides = cls.doc.getDrawPages()
if zero_based_slide_index < 0 or zero_based_slide_index >= slides.getCount():
cls.ret = f"Error: Slide index {slide_index} out of range"
return False
slide = slides.getByIndex(zero_based_slide_index)
if box_index < 0 or box_index >= slide.getCount():
cls.ret = f"Error: Box index {box_index} out of range"
return False
shape = slide.getByIndex(box_index)
if not hasattr(shape, "getText"):
cls.ret = "Error: Selected shape does not support text"
return False
if alignment == "left":
shape.TextHorizontalAdjust = LEFT
elif alignment == "center":
shape.TextHorizontalAdjust = CENTER
elif alignment == "right":
shape.TextHorizontalAdjust = RIGHT
elif alignment == "justify":
text = shape.getText()
cursor = text.createTextCursor()
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.ParaAdjust = 3
else:
cls.ret = f"Error: Invalid alignment value: {alignment}"
return False
cls.ret = f"Successfully set text alignment to {alignment} for textbox {box_index} on slide {slide_index}"
return True
except Exception as e:
cls.ret = f"Error: {str(e)}"
return False
@classmethod
def set_slide_number_properties(
cls, color=None, font_size=None, visible=None, position=None, apply_to="all", slide_indices=None
):
"""
Modifies the properties of slide numbers in the presentation.
Args:
color (str, optional): The color to apply to slide numbers (e.g., 'red', 'green', 'blue', 'black', or hex color code)
font_size (float, optional): The font size for slide numbers (in points)
visible (bool, optional): Whether slide numbers should be visible or hidden
position (str, optional): The position of slide numbers ('bottom-left', 'bottom-center', 'bottom-right',
'top-left', 'top-center', 'top-right')
apply_to (str, optional): Whether to apply changes to 'all', 'current', or 'selected' slides
slide_indices (list, optional): Indices of specific slides to change (1-based indexing)
Returns:
bool: True if successful, False otherwise
"""
try:
draw_pages = cls.doc.getDrawPages()
master_pages = cls.doc.getMasterPages()
pages_to_modify = []
if apply_to == "all":
for i in range(draw_pages.getCount()):
pages_to_modify.append(draw_pages.getByIndex(i))
elif apply_to == "current":
current_page = cls.doc.getCurrentController().getCurrentPage()
pages_to_modify.append(current_page)
elif apply_to == "selected" and slide_indices:
for idx in slide_indices:
if 1 <= idx <= draw_pages.getCount():
pages_to_modify.append(draw_pages.getByIndex(idx - 1))
for i in range(master_pages.getCount()):
master_page = master_pages.getByIndex(i)
page_number_shape = None
for j in range(master_page.getCount()):
shape = master_page.getByIndex(j)
if hasattr(shape, "TextType"):
try:
if shape.TextType == 5:
page_number_shape = shape
break
except:
pass
if hasattr(shape, "getText"):
try:
text = shape.getText()
if text and text.getTextFields().getCount() > 0:
fields = text.getTextFields().createEnumeration()
while fields.hasMoreElements():
field = fields.nextElement()
if "PageNumber" in field.getImplementationName():
page_number_shape = shape
break
if page_number_shape:
break
except:
pass
if page_number_shape:
if color is not None:
color_int = 0
if color.startswith("#"):
color_int = int(color[1:], 16)
elif color == "red":
color_int = 16711680
elif color == "green":
color_int = 65280
elif color == "blue":
color_int = 255
elif color == "black":
color_int = 0
text = page_number_shape.getText()
cursor = text.createTextCursor()
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.CharColor = color_int
if font_size is not None:
text = page_number_shape.getText()
cursor = text.createTextCursor()
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.CharHeight = font_size
if position is not None:
page_width = master_page.Width
page_height = master_page.Height
width = page_number_shape.Size.Width
height = page_number_shape.Size.Height
new_x = 0
new_y = 0
if position.startswith("bottom"):
new_y = page_height - height - 100
elif position.startswith("top"):
new_y = 100
if position.endswith("left"):
new_x = 100
elif position.endswith("center"):
new_x = (page_width - width) / 2
elif position.endswith("right"):
new_x = page_width - width - 100
page_number_shape.Position = uno.createUnoStruct("com.sun.star.awt.Point", new_x, new_y)
if position.endswith("left"):
page_number_shape.ParaAdjust = LEFT
elif position.endswith("center"):
page_number_shape.ParaAdjust = CENTER
elif position.endswith("right"):
page_number_shape.ParaAdjust = RIGHT
if visible is not None:
try:
page_number_shape.Visible = visible
except:
if not visible:
page_number_shape.Size = uno.createUnoStruct("com.sun.star.awt.Size", 1, 1)
page_number_shape.Position = uno.createUnoStruct("com.sun.star.awt.Point", -1000, -1000)
elif (
visible is True
or visible is None
and (color is not None or font_size is not None or position is not None)
):
page_number_shape = cls.doc.createInstance("com.sun.star.drawing.TextShape")
master_page.add(page_number_shape)
default_width = 2000
default_height = 400
page_number_shape.Size = uno.createUnoStruct("com.sun.star.awt.Size", default_width, default_height)
page_width = master_page.Width
page_height = master_page.Height
pos_x = page_width - default_width - 100
pos_y = page_height - default_height - 100
if position is not None:
if position.startswith("bottom"):
pos_y = page_height - default_height - 100
elif position.startswith("top"):
pos_y = 100
if position.endswith("left"):
pos_x = 100
page_number_shape.ParaAdjust = LEFT
elif position.endswith("center"):
pos_x = (page_width - default_width) / 2
page_number_shape.ParaAdjust = CENTER
elif position.endswith("right"):
pos_x = page_width - default_width - 100
page_number_shape.ParaAdjust = RIGHT
page_number_shape.Position = uno.createUnoStruct("com.sun.star.awt.Point", pos_x, pos_y)
text = page_number_shape.getText()
cursor = text.createTextCursor()
try:
page_field = cls.doc.createInstance("com.sun.star.text.TextField.PageNumber")
text.insertTextContent(cursor, page_field, False)
except:
text.setString("<#>")
if color is not None:
color_int = 0
if color.startswith("#"):
color_int = int(color[1:], 16)
elif color == "red":
color_int = 16711680
elif color == "green":
color_int = 65280
elif color == "blue":
color_int = 255
elif color == "black":
color_int = 0
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.CharColor = color_int
if font_size is not None:
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.CharHeight = font_size
if visible is not None:
try:
page_number_shape.Visible = visible
except:
if not visible:
page_number_shape.Position = uno.createUnoStruct("com.sun.star.awt.Point", -1000, -1000)
else:
try:
page_number_shape.Visible = True
except:
pass
try:
controller = cls.doc.getCurrentController()
view_data = controller.getViewData()
controller.restoreViewData(view_data)
except:
pass
cls.ret = "Slide number properties updated successfully"
return True
except Exception as e:
cls.ret = f"Error setting slide number properties: {str(e)}"
return False
@classmethod
def set_slide_number(cls, color=None, font_size=None, visible=None, position=None):
"""
Sets the slide number in the presentation.
:param color: The color to apply to slide numbers (e.g., 'red', 'green', 'blue', 'black', or hex color code)
:param font_size: The font size for slide numbers (in points)
:param visible: Whether slide numbers should be visible or hidden
:param position: The position of slide numbers on the slides (bottom-left, bottom-center, bottom-right, top-left, top-center, top-right)
:return: True if successful, False otherwise
"""
try:
controller = cls.doc.getCurrentController()
dispatcher = cls.ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.DispatchHelper", cls.ctx)
if visible is False:
pages = cls.doc.getDrawPages()
for i in range(pages.getCount()):
page = pages.getByIndex(i)
for j in range(page.getCount()):
try:
shape = page.getByIndex(j)
if hasattr(shape, "Presentation") and shape.Presentation == "Number":
page.remove(shape)
except:
pass
master_pages = cls.doc.getMasterPages()
for i in range(master_pages.getCount()):
master_page = master_pages.getByIndex(i)
for j in range(master_page.getCount()):
try:
shape = master_page.getByIndex(j)
if hasattr(shape, "Presentation") and shape.Presentation == "Number":
master_page.remove(shape)
except:
pass
cls.ret = "Slide numbers hidden successfully"
return True
if visible is True or color is not None or font_size is not None or position is not None:
current_slide = controller.getCurrentPage()
master_pages = cls.doc.getMasterPages()
if master_pages.getCount() == 0:
cls.ret = "No master pages found"
return False
master_page = master_pages.getByIndex(0)
slide_number_shape = cls.doc.createInstance("com.sun.star.drawing.TextShape")
slide_number_shape.setSize(uno.createUnoStruct("com.sun.star.awt.Size", 2000, 500))
pos = position or "bottom-right"
page_width = master_page.Width
page_height = master_page.Height
x, y = 0, 0
if "bottom" in pos:
y = page_height - 1000
elif "top" in pos:
y = 500
if "left" in pos:
x = 500
elif "center" in pos:
x = (page_width - 2000) / 2
elif "right" in pos:
x = page_width - 2500
slide_number_shape.setPosition(uno.createUnoStruct("com.sun.star.awt.Point", x, y))
master_page.add(slide_number_shape)
text = slide_number_shape.getText()
cursor = text.createTextCursor()
page_number = cls.doc.createInstance("com.sun.star.text.TextField.PageNumber")
text.insertTextContent(cursor, page_number, False)
if "center" in pos:
slide_number_shape.setPropertyValue("TextHorizontalAdjust", CENTER)
elif "right" in pos:
slide_number_shape.setPropertyValue("TextHorizontalAdjust", RIGHT)
elif "left" in pos:
slide_number_shape.setPropertyValue("TextHorizontalAdjust", LEFT)
if font_size is not None:
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.setPropertyValue("CharHeight", font_size)
if color is not None:
cursor.gotoStart(False)
cursor.gotoEnd(True)
if color.startswith("#") and len(color) == 7:
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
cursor.setPropertyValue("CharColor", (r << 16) + (g << 8) + b)
else:
color_map = {
"red": 16711680,
"green": 65280,
"blue": 255,
"black": 0,
"white": 16777215,
"yellow": 16776960,
"cyan": 65535,
"magenta": 16711935,
"gray": 8421504,
}
if color.lower() in color_map:
cursor.setPropertyValue("CharColor", color_map[color.lower()])
cls.ret = "Slide numbers added and configured successfully"
return True
except Exception as e:
cls.ret = f"Error setting slide number: {str(e)}"
return False
@classmethod
def set_slide_number_color(cls, color):
"""
Sets the color of the slide number in the presentation.
Args:
color (str): The color to apply to slide numbers (e.g., 'red', 'green', 'blue', 'black', or hex color code)
Returns:
bool: True if successful, False otherwise
"""
try:
color_map = {
"black": 0,
"white": 16777215,
"red": 16711680,
"green": 65280,
"blue": 255,
"yellow": 16776960,
"cyan": 65535,
"magenta": 16711935,
"gray": 8421504,
"orange": 16753920,
"purple": 8388736,
}
if color.lower() in color_map:
rgb_color = color_map[color.lower()]
else:
if color.startswith("#"):
color = color[1:]
try:
if len(color) == 6:
rgb_color = int(color, 16)
else:
rgb_color = 0
except ValueError:
rgb_color = 0
found = False
master_pages = cls.doc.getMasterPages()
for i in range(master_pages.getCount()):
master_page = master_pages.getByIndex(i)
for j in range(master_page.getCount()):
shape = master_page.getByIndex(j)
if hasattr(shape, "getText") and shape.getText() is not None:
text = shape.getText()
try:
enum = text.createEnumeration()
while enum.hasMoreElements():
para = enum.nextElement()
if hasattr(para, "createEnumeration"):
para_enum = para.createEnumeration()
while para_enum.hasMoreElements():
portion = para_enum.nextElement()
if (
hasattr(portion, "TextPortionType")
and portion.TextPortionType == "TextField"
):
if hasattr(portion, "TextField") and portion.TextField is not None:
field = portion.TextField
if hasattr(field, "supportsService") and (
field.supportsService(
"com.sun.star.presentation.TextField.PageNumber"
)
or field.supportsService("com.sun.star.text.TextField.PageNumber")
):
portion.CharColor = rgb_color
found = True
except Exception as e:
continue
draw_pages = cls.doc.getDrawPages()
for i in range(draw_pages.getCount()):
page = draw_pages.getByIndex(i)
for j in range(page.getCount()):
shape = page.getByIndex(j)
if hasattr(shape, "getText") and shape.getText() is not None:
text = shape.getText()
try:
enum = text.createEnumeration()
while enum.hasMoreElements():
para = enum.nextElement()
if hasattr(para, "createEnumeration"):
para_enum = para.createEnumeration()
while para_enum.hasMoreElements():
portion = para_enum.nextElement()
if (
hasattr(portion, "TextPortionType")
and portion.TextPortionType == "TextField"
):
if hasattr(portion, "TextField") and portion.TextField is not None:
field = portion.TextField
if hasattr(field, "supportsService") and (
field.supportsService(
"com.sun.star.presentation.TextField.PageNumber"
)
or field.supportsService("com.sun.star.text.TextField.PageNumber")
):
portion.CharColor = rgb_color
found = True
except Exception as e:
continue
for i in range(draw_pages.getCount()):
page = draw_pages.getByIndex(i)
for j in range(page.getCount()):
shape = page.getByIndex(j)
if hasattr(shape, "getText") and shape.getText() is not None:
text = shape.getText()
text_string = text.getString()
if text_string.isdigit() and len(text_string) <= 3:
try:
cursor = text.createTextCursor()
cursor.gotoStart(False)
cursor.gotoEnd(True)
cursor.CharColor = rgb_color
found = True
except Exception as e:
continue
if found:
cls.ret = f"Slide number color set to {color}"
return True
else:
cls.ret = "Could not find slide numbers to change color"
return False
except Exception as e:
cls.ret = f"Error setting slide number color: {str(e)}"
return False
@classmethod
def export_to_image(cls, file_path, format, slide_index=None):
"""
Exports the current presentation or a specific slide to an image file format.
Args:
file_path (str): The full path where the image file should be saved, including the filename and extension
format (str): The image format to export to (e.g., 'png', 'jpeg', 'gif')
slide_index (int, optional): The index of the specific slide to export (1-based indexing).
If not provided, exports the entire presentation as a series of images.
Returns:
bool: True if export was successful, False otherwise
"""
try:
format = format.lower()
valid_formats = ["png", "jpeg", "jpg", "gif", "bmp", "tiff"]
if format not in valid_formats:
cls.ret = f"Error: Invalid format '{format}'. Valid formats are: {', '.join(valid_formats)}"
return False
if format == "jpg":
format = "jpeg"
pages = cls.doc.getDrawPages()
page_count = pages.getCount()
if slide_index is not None:
slide_index = slide_index - 1
if slide_index < 0 or slide_index >= page_count:
cls.ret = f"Error: Invalid slide index {slide_index + 1}. Valid range is 1 to {page_count}"
return False
controller = cls.doc.getCurrentController()
filter_name = f"draw_{format}_Export"
filter_data = PropertyValue(Name="FilterData", Value=())
if slide_index is not None:
controller.setCurrentPage(pages.getByIndex(slide_index))
props = PropertyValue(Name="FilterName", Value=filter_name), filter_data
cls.doc.storeToURL(uno.systemPathToFileUrl(file_path), props)
cls.ret = f"Successfully exported slide {slide_index + 1} to {file_path}"
return True
else:
base_name, ext = os.path.splitext(file_path)
for i in range(page_count):
controller.setCurrentPage(pages.getByIndex(i))
if page_count == 1:
current_file = f"{base_name}.{format}"
else:
current_file = f"{base_name}_{i + 1}.{format}"
props = PropertyValue(Name="FilterName", Value=filter_name), filter_data
cls.doc.storeToURL(uno.systemPathToFileUrl(current_file), props)
if page_count == 1:
cls.ret = f"Successfully exported {page_count} slides to {base_name}.{format}"
else:
cls.ret = f"Successfully exported {page_count} slides to {base_name}_[1-{page_count}].{format}"
return True
except Exception as e:
cls.ret = f"Error exporting to image: {str(e)}"
return False