Download server/tools/pptx_tools.py from Berlm/ppt_agent: direct link, hf CLI and curl.
- Browser
- Download file 13.7 kB
-
https://huggingface.co/spaces/Berlm/ppt_agent/resolve/main/server/tools/pptx_tools.py
- Command line
-
hf download hf://spaces/Berlm/ppt_agent/server/tools/pptx_tools.py
-
curl -L -o pptx_tools.py https://huggingface.co/spaces/Berlm/ppt_agent/resolve/main/server/tools/pptx_tools.py
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", | |
| ] | |