ppt_agent / server /tools /pptx_tools.py
Berlm's picture
Upload folder using huggingface_hub
ba2cf79 verified
Raw History Blame Contribute Delete
13.7 kB
from os import PathLike
from pathlib import Path
from typing import Any, Dict, Optional
from pptx.util import Inches
from ..utils.pptx_functions import PptxEditor
_SHAPE_ID_UNSET = object()
def create_presentation(path: Optional[str | PathLike[str]] = None) -> PptxEditor:
if path is None:
return PptxEditor()
return PptxEditor(str(path))
def save_presentation(editor: PptxEditor, path: str | PathLike[str]) -> Dict[str, Any]:
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
editor.prs.save(output_path)
return {
"path": str(output_path.resolve()),
"slide_count": len(editor.prs.slides),
}
def register_theme(
editor: PptxEditor, theme: Dict[str, Any], name: str = "default"
) -> str:
return editor.register_theme(name, theme)
def update_theme(
editor: PptxEditor, updates: Dict[str, Any], name: str = "default"
) -> None:
editor.update_theme(name, updates)
def _remove_bindings(
editor: PptxEditor,
*,
slide_id: Optional[int] = None,
shape_id: Any = _SHAPE_ID_UNSET,
apply_method: Optional[str] = None,
theme_name: Optional[str] = None,
) -> None:
theme_names = (
[theme_name] if theme_name is not None else list(editor._theme_bindings)
)
for current_theme_name in theme_names:
remaining = []
for binding in editor._theme_bindings.get(current_theme_name, []):
if slide_id is not None and binding.slide_id != slide_id:
remaining.append(binding)
continue
if shape_id is not _SHAPE_ID_UNSET and binding.shape_id != shape_id:
remaining.append(binding)
continue
if apply_method is not None and binding.apply_method != apply_method:
remaining.append(binding)
continue
editor._theme_bindings[current_theme_name] = remaining
def _get_required(spec: Dict[str, Any], key: str) -> Any:
if key not in spec:
raise ValueError(f"Shape spec missing required field '{key}'")
return spec[key]
def _get_optional_geometry(spec: Dict[str, Any]) -> Dict[str, float]:
geometry = {}
for source_key, target_key in (
("x", "left"),
("y", "top"),
("w", "width"),
("h", "height"),
):
if source_key in spec:
geometry[target_key] = spec[source_key]
return geometry
def _apply_geometry_updates(
editor: PptxEditor, slide_id: int, shape_id: int, spec: Dict[str, Any]
) -> None:
geometry = _get_optional_geometry(spec)
if not geometry:
return
shape = editor._get_shape_by_id(slide_id, shape_id)
if "left" in geometry:
shape.left = Inches(geometry["left"])
if "top" in geometry:
shape.top = Inches(geometry["top"])
if "width" in geometry:
shape.width = Inches(geometry["width"])
if "height" in geometry:
shape.height = Inches(geometry["height"])
def _record_named_shape(
result: Dict[str, Any], spec: Dict[str, Any], shape_id: int
) -> None:
name = spec.get("name")
if name is None:
return
if name in result["named_shapes"]:
raise ValueError(f"Duplicate shape name '{name}' in one tool call")
result["named_shapes"][name] = shape_id
def _create_accent_bar(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
return editor.add_accent_bar_by_id(
slide_id,
_get_required(spec, "color_hex"),
height=spec.get("height", 0.35),
theme_name=theme_name,
bind_theme=True,
)
def _create_text(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
shape_id = editor.add_textbox_by_id(
slide_id,
_get_required(spec, "x"),
_get_required(spec, "y"),
_get_required(spec, "w"),
_get_required(spec, "h"),
)
editor.insert_text_by_id(slide_id, shape_id, _get_required(spec, "text"))
if spec.get("style"):
editor.style_text_by_id(
slide_id,
shape_id,
theme_name=theme_name,
bind_theme=True,
**spec["style"],
)
return shape_id
def _create_chart(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
return editor.add_chart_by_id(
slide_id,
_get_required(spec, "chart_type"),
_get_required(spec, "chart_data"),
_get_required(spec, "x"),
_get_required(spec, "y"),
_get_required(spec, "w"),
_get_required(spec, "h"),
style=spec.get("style"),
theme_name=theme_name,
bind_theme=True,
)
def _create_table(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
table_data = _get_required(spec, "table_data")
rows = len(table_data)
cols = max((len(row) for row in table_data), default=0)
if rows == 0 or cols == 0:
raise ValueError("table_data must include at least one row and one column")
return editor.add_table_by_id(
slide_id,
rows,
cols,
table_data,
_get_required(spec, "x"),
_get_required(spec, "y"),
_get_required(spec, "w"),
_get_required(spec, "h"),
style=spec.get("style"),
theme_name=theme_name,
bind_theme=True,
)
def _create_image(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
del theme_name
return editor.add_image_by_id(
slide_id,
_get_required(spec, "image_path"),
_get_required(spec, "x"),
_get_required(spec, "y"),
cx=spec.get("w"),
cy=spec.get("h"),
)
def _update_text(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
shape_id = _get_required(spec, "shape_id")
_apply_geometry_updates(editor, slide_id, shape_id, spec)
if "text" in spec:
editor.insert_text_by_id(slide_id, shape_id, spec["text"])
if "style" in spec:
_remove_bindings(
editor,
slide_id=slide_id,
shape_id=shape_id,
apply_method="style_text_by_id",
)
editor.style_text_by_id(
slide_id,
shape_id,
theme_name=theme_name,
bind_theme=True,
**spec["style"],
)
return shape_id
def _update_chart(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
shape_id = _get_required(spec, "shape_id")
_apply_geometry_updates(editor, slide_id, shape_id, spec)
if "style" in spec:
_remove_bindings(
editor,
slide_id=slide_id,
shape_id=shape_id,
apply_method="style_chart_by_id",
)
editor.style_chart_by_id(
slide_id,
shape_id,
theme_name=theme_name,
bind_theme=True,
**spec["style"],
)
return shape_id
def _update_table(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
shape_id = _get_required(spec, "shape_id")
_apply_geometry_updates(editor, slide_id, shape_id, spec)
if "style" in spec:
_remove_bindings(
editor,
slide_id=slide_id,
shape_id=shape_id,
apply_method="style_table_by_id",
)
editor.style_table_by_id(
slide_id,
shape_id,
theme_name=theme_name,
bind_theme=True,
**spec["style"],
)
return shape_id
def _update_accent_bar(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
shape_id = _get_required(spec, "shape_id")
_apply_geometry_updates(editor, slide_id, shape_id, spec)
if "height" in spec:
shape = editor._get_shape_by_id(slide_id, shape_id)
shape.height = Inches(spec["height"])
if "color_hex" in spec:
_remove_bindings(
editor,
slide_id=slide_id,
shape_id=shape_id,
apply_method="style_shape_fill_by_id",
)
editor.style_shape_fill_by_id(
slide_id,
shape_id,
fill_color_hex=spec["color_hex"],
theme_name=theme_name,
bind_theme=True,
)
return shape_id
def _update_image(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
del theme_name
shape_id = _get_required(spec, "shape_id")
if "image_path" in spec:
raise ValueError(
"Updating image content is not supported; delete and re-add the image"
)
_apply_geometry_updates(editor, slide_id, shape_id, spec)
return shape_id
_CREATE_DISPATCH = {
"accent_bar": _create_accent_bar,
"text": _create_text,
"chart": _create_chart,
"table": _create_table,
"image": _create_image,
}
_UPDATE_DISPATCH = {
"accent_bar": _update_accent_bar,
"text": _update_text,
"chart": _update_chart,
"table": _update_table,
"image": _update_image,
}
def _create_shape_from_spec(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
shape_type = _get_required(spec, "type")
if shape_type not in _CREATE_DISPATCH:
raise ValueError(f"Unsupported shape type '{shape_type}'")
return _CREATE_DISPATCH[shape_type](editor, slide_id, spec, theme_name)
def _update_shape_from_spec(
editor: PptxEditor, slide_id: int, spec: Dict[str, Any], theme_name: str
) -> int:
shape_type = _get_required(spec, "type")
if shape_type not in _UPDATE_DISPATCH:
raise ValueError(f"Unsupported shape type '{shape_type}'")
return _UPDATE_DISPATCH[shape_type](editor, slide_id, spec, theme_name)
def _delete_shape(editor: PptxEditor, slide_id: int, shape_id: int) -> None:
shape = editor._get_shape_by_id(slide_id, shape_id)
shape._element.getparent().remove(shape._element)
_remove_bindings(editor, slide_id=slide_id, shape_id=shape_id)
def delete_slide(editor: PptxEditor, slide_id: int) -> Dict[str, Any]:
slide_index = editor.get_slide_index(slide_id)
editor.delete_slide_by_id(slide_id)
_remove_bindings(editor, slide_id=slide_id)
return {
"slide_id": slide_id,
"deleted_slide_index": slide_index + 1,
"remaining_slide_count": len(editor.prs.slides),
}
def create_slide(
editor: PptxEditor,
*,
theme_name: str = "default",
background_color: Optional[str] = None,
shapes: Optional[list[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
slide_index = editor.add_slide()
slide_id = editor.get_slide_id(slide_index)
result = {"slide_id": slide_id, "shape_ids": [], "named_shapes": {}}
if background_color is not None:
editor.set_slide_background_by_id(
slide_id,
background_color,
theme_name=theme_name,
bind_theme=True,
)
for spec in shapes or []:
shape_id = _create_shape_from_spec(editor, slide_id, spec, theme_name)
result["shape_ids"].append(shape_id)
_record_named_shape(result, spec, shape_id)
return result
def update_slide(
editor: PptxEditor,
slide_id: int,
*,
theme_name: str = "default",
background_color: Optional[str] = None,
delete_shape_ids: Optional[list[int]] = None,
add_shapes: Optional[list[Dict[str, Any]]] = None,
update_shapes: Optional[list[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
editor._get_slide_by_id(slide_id)
delete_shape_ids = delete_shape_ids or []
add_shapes = add_shapes or []
update_shapes = update_shapes or []
delete_set = set(delete_shape_ids)
if len(delete_set) != len(delete_shape_ids):
raise ValueError("delete_shape_ids contains duplicates")
update_shape_ids = []
for spec in update_shapes:
shape_id = _get_required(spec, "shape_id")
if shape_id in delete_set:
raise ValueError(
f"Shape id {shape_id} cannot be both updated and deleted in one call"
)
update_shape_ids.append(shape_id)
if len(set(update_shape_ids)) != len(update_shape_ids):
raise ValueError("update_shapes contains duplicate shape_id values")
result = {
"slide_id": slide_id,
"deleted_shape_ids": [],
"updated_shape_ids": [],
"created_shape_ids": [],
"named_shapes": {},
}
if background_color is not None:
_remove_bindings(
editor,
slide_id=slide_id,
shape_id=None,
apply_method="set_slide_background_by_id",
)
editor.set_slide_background_by_id(
slide_id,
background_color,
theme_name=theme_name,
bind_theme=True,
)
for spec in update_shapes:
shape_id = _update_shape_from_spec(editor, slide_id, spec, theme_name)
result["updated_shape_ids"].append(shape_id)
_record_named_shape(result, spec, shape_id)
for spec in add_shapes:
shape_id = _create_shape_from_spec(editor, slide_id, spec, theme_name)
result["created_shape_ids"].append(shape_id)
_record_named_shape(result, spec, shape_id)
for shape_id in delete_shape_ids:
_delete_shape(editor, slide_id, shape_id)
result["deleted_shape_ids"].append(shape_id)
return result
__all__ = [
"create_presentation",
"save_presentation",
"register_theme",
"update_theme",
"create_slide",
"delete_slide",
"update_slide",
]