Skip to main content
Glama

Office MCP Server

by walkingzzzy
ppt_style.py19.2 kB
"""PowerPoint 样式操作模块 - 格式化、主题、过渡.""" from typing import Any, Optional from pptx import Presentation from pptx.util import Pt, Inches from pptx.enum.text import PP_ALIGN, MSO_AUTO_SIZE from pptx.dml.color import RGBColor from loguru import logger from office_mcp_server.config import config from office_mcp_server.utils.file_manager import FileManager from office_mcp_server.utils.format_helper import ColorUtils class PowerPointStyleOperations: """PowerPoint 样式操作类.""" def __init__(self) -> None: """初始化样式操作类.""" self.file_manager = FileManager() def format_text( self, filename: str, slide_index: int, shape_index: int, font_name: Optional[str] = None, font_size: Optional[int] = None, bold: bool = False, italic: bool = False, underline: bool = False, color: Optional[str] = None, alignment: Optional[str] = None, ) -> dict[str, Any]: """格式化PowerPoint文本.""" try: file_path = config.paths.output_dir / filename self.file_manager.validate_file_path(file_path, must_exist=True) prs = Presentation(str(file_path)) if slide_index >= len(prs.slides): raise ValueError(f"幻灯片索引 {slide_index} 超出范围") slide = prs.slides[slide_index] if shape_index >= len(slide.shapes): raise ValueError(f"形状索引 {shape_index} 超出范围") shape = slide.shapes[shape_index] if not hasattr(shape, "text_frame"): raise ValueError(f"形状 {shape_index} 不包含文本框") text_frame = shape.text_frame # 对齐方式映射 alignment_map = { 'left': PP_ALIGN.LEFT, 'center': PP_ALIGN.CENTER, 'right': PP_ALIGN.RIGHT, 'justify': PP_ALIGN.JUSTIFY, } # 应用格式到所有段落和运行 for paragraph in text_frame.paragraphs: if alignment and alignment in alignment_map: paragraph.alignment = alignment_map[alignment] for run in paragraph.runs: if font_name: run.font.name = font_name if font_size: run.font.size = Pt(font_size) run.font.bold = bold run.font.italic = italic run.font.underline = underline if color: r, g, b = ColorUtils.hex_to_rgb(color) run.font.color.rgb = RGBColor(r, g, b) prs.save(str(file_path)) logger.info(f"文本格式化成功: {file_path}") return { "success": True, "message": "文本格式化成功", "filename": str(file_path), } except Exception as e: logger.error(f"格式化文本失败: {e}") return {"success": False, "message": f"格式化失败: {str(e)}"} def apply_theme( self, filename: str, theme_name: str, apply_to_all: bool = True, ) -> dict[str, Any]: """应用PowerPoint主题.""" try: file_path = config.paths.output_dir / filename self.file_manager.validate_file_path(file_path, must_exist=True) prs = Presentation(str(file_path)) # 定义主题颜色方案 (RGB值) theme_colors = { 'Office': { 'accent1': RGBColor(68, 114, 196), 'accent2': RGBColor(237, 125, 49), 'accent3': RGBColor(165, 165, 165), 'accent4': RGBColor(255, 192, 0), 'accent5': RGBColor(91, 155, 213), 'accent6': RGBColor(112, 173, 71), }, 'Facet': { 'accent1': RGBColor(68, 114, 196), 'accent2': RGBColor(237, 125, 49), 'accent3': RGBColor(165, 165, 165), 'accent4': RGBColor(255, 192, 0), 'accent5': RGBColor(91, 155, 213), 'accent6': RGBColor(112, 173, 71), }, 'Ion': { 'accent1': RGBColor(0, 176, 240), 'accent2': RGBColor(255, 192, 0), 'accent3': RGBColor(112, 48, 160), 'accent4': RGBColor(0, 176, 80), 'accent5': RGBColor(255, 0, 0), 'accent6': RGBColor(146, 208, 80), }, 'Wisp': { 'accent1': RGBColor(68, 114, 196), 'accent2': RGBColor(237, 125, 49), 'accent3': RGBColor(165, 165, 165), 'accent4': RGBColor(255, 192, 0), 'accent5': RGBColor(91, 155, 213), 'accent6': RGBColor(112, 173, 71), }, 'Integral': { 'accent1': RGBColor(31, 78, 120), 'accent2': RGBColor(192, 80, 77), 'accent3': RGBColor(155, 187, 89), 'accent4': RGBColor(128, 100, 162), 'accent5': RGBColor(75, 172, 198), 'accent6': RGBColor(247, 150, 70), }, 'Slice': { 'accent1': RGBColor(255, 87, 51), 'accent2': RGBColor(255, 183, 3), 'accent3': RGBColor(142, 180, 0), 'accent4': RGBColor(0, 204, 153), 'accent5': RGBColor(46, 117, 182), 'accent6': RGBColor(123, 103, 184), }, 'Droplet': { 'accent1': RGBColor(68, 114, 196), 'accent2': RGBColor(237, 125, 49), 'accent3': RGBColor(165, 165, 165), 'accent4': RGBColor(255, 192, 0), 'accent5': RGBColor(91, 155, 213), 'accent6': RGBColor(112, 173, 71), }, } if theme_name not in theme_colors: raise ValueError(f"不支持的主题: {theme_name}") colors = theme_colors[theme_name] # 应用主题到幻灯片 slides_count = len(prs.slides) for slide in prs.slides: color_index = 0 for shape in slide.shapes: if hasattr(shape, "fill"): try: accent_key = f'accent{(color_index % 6) + 1}' shape.fill.solid() shape.fill.fore_color.rgb = colors[accent_key] color_index += 1 except: pass prs.save(str(file_path)) logger.info(f"主题应用成功: {file_path}") return { "success": True, "message": f"主题 '{theme_name}' 应用成功", "filename": str(file_path), "theme": theme_name, "slides_affected": slides_count, } except Exception as e: logger.error(f"应用主题失败: {e}") return {"success": False, "message": f"应用失败: {str(e)}"} def set_transition( self, filename: str, slide_index: int, transition_type: str = "fade", duration: float = 1.0, apply_to_all: bool = False, ) -> dict[str, Any]: """设置PowerPoint幻灯片过渡效果.""" try: file_path = config.paths.output_dir / filename self.file_manager.validate_file_path(file_path, must_exist=True) prs = Presentation(str(file_path)) if not apply_to_all and slide_index >= len(prs.slides): raise ValueError(f"幻灯片索引 {slide_index} 超出范围") from pptx.oxml import parse_xml # 定义过渡类型映射 transition_xml_map = { 'fade': '<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med"><p:fade thruBlk="0"/></p:transition>', 'push': '<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med"><p:push dir="l"/></p:transition>', 'wipe': '<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med"><p:wipe dir="l"/></p:transition>', 'split': '<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med"><p:split orient="horz" dir="in"/></p:transition>', 'reveal': '<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med"><p:reveal dir="l"/></p:transition>', 'random': '<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med"><p:random/></p:transition>', 'none': '<p:transition xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" spd="med"/>', } if transition_type not in transition_xml_map: raise ValueError(f"不支持的过渡类型: {transition_type}") transition_xml = transition_xml_map[transition_type] # 应用过渡效果 if apply_to_all: slides_to_update = prs.slides slides_count = len(prs.slides) else: slides_to_update = [prs.slides[slide_index]] slides_count = 1 for slide in slides_to_update: sld = slide._element # 移除现有过渡效果 existing_transition = sld.find('{http://schemas.openxmlformats.org/presentationml/2006/main}transition') if existing_transition is not None: sld.remove(existing_transition) # 添加新的过渡效果 if transition_type != 'none': transition_element = parse_xml(transition_xml) transition_element.set('advTm', str(int(duration * 1000))) sld.insert(0, transition_element) prs.save(str(file_path)) logger.info(f"过渡效果设置成功: {file_path}") return { "success": True, "message": f"过渡效果 '{transition_type}' 已应用到 {slides_count} 个幻灯片", "filename": str(file_path), "transition_type": transition_type, "duration": duration, "slides_affected": slides_count, } except Exception as e: logger.error(f"设置过渡效果失败: {e}") return {"success": False, "message": f"设置失败: {str(e)}"} def add_bullet_points( self, filename: str, slide_index: int, shape_index: int, bullet_type: str = "bullet", level: int = 0, ) -> dict[str, Any]: """为段落添加项目符号或编号. Args: filename: 文件名 slide_index: 幻灯片索引 shape_index: 形状索引 bullet_type: 项目符号类型 ('bullet', 'number', 'none') level: 缩进级别 (0-8) """ try: file_path = config.paths.output_dir / filename self.file_manager.validate_file_path(file_path, must_exist=True) prs = Presentation(str(file_path)) if slide_index >= len(prs.slides): raise ValueError(f"幻灯片索引 {slide_index} 超出范围") slide = prs.slides[slide_index] if shape_index >= len(slide.shapes): raise ValueError(f"形状索引 {shape_index} 超出范围") shape = slide.shapes[shape_index] if not hasattr(shape, "text_frame"): raise ValueError(f"形状 {shape_index} 不包含文本框") text_frame = shape.text_frame for paragraph in text_frame.paragraphs: paragraph.level = min(level, 8) if bullet_type == "bullet": paragraph.font.name = "Symbol" elif bullet_type == "number": # 使用编号需要通过XML设置 from pptx.oxml.xmlchemy import OxmlElement pPr = paragraph._element.get_or_add_pPr() buAutoNum = OxmlElement('a:buAutoNum') buAutoNum.set('type', 'arabicPeriod') pPr.insert(0, buAutoNum) elif bullet_type == "none": paragraph.font.name = None prs.save(str(file_path)) logger.info(f"项目符号添加成功: {file_path}") return { "success": True, "message": f"项目符号 '{bullet_type}' 已添加", "filename": str(file_path), "bullet_type": bullet_type, "level": level, } except Exception as e: logger.error(f"添加项目符号失败: {e}") return {"success": False, "message": f"操作失败: {str(e)}"} def set_paragraph_format( self, filename: str, slide_index: int, shape_index: int, line_spacing: Optional[float] = None, space_before: Optional[float] = None, space_after: Optional[float] = None, indent_level: int = 0, ) -> dict[str, Any]: """设置段落格式. Args: filename: 文件名 slide_index: 幻灯片索引 shape_index: 形状索引 line_spacing: 行距 (如 1.5 表示1.5倍行距) space_before: 段前间距 (磅值) space_after: 段后间距 (磅值) indent_level: 缩进级别 (0-8) """ try: file_path = config.paths.output_dir / filename self.file_manager.validate_file_path(file_path, must_exist=True) prs = Presentation(str(file_path)) if slide_index >= len(prs.slides): raise ValueError(f"幻灯片索引 {slide_index} 超出范围") slide = prs.slides[slide_index] if shape_index >= len(slide.shapes): raise ValueError(f"形状索引 {shape_index} 超出范围") shape = slide.shapes[shape_index] if not hasattr(shape, "text_frame"): raise ValueError(f"形状 {shape_index} 不包含文本框") text_frame = shape.text_frame for paragraph in text_frame.paragraphs: if line_spacing is not None: paragraph.line_spacing = line_spacing if space_before is not None: paragraph.space_before = Pt(space_before) if space_after is not None: paragraph.space_after = Pt(space_after) paragraph.level = min(indent_level, 8) prs.save(str(file_path)) logger.info(f"段落格式设置成功: {file_path}") return { "success": True, "message": "段落格式设置成功", "filename": str(file_path), "line_spacing": line_spacing, "space_before": space_before, "space_after": space_after, "indent_level": indent_level, } except Exception as e: logger.error(f"设置段落格式失败: {e}") return {"success": False, "message": f"操作失败: {str(e)}"} def set_slide_background( self, filename: str, slide_index: int, background_type: str = "solid", color: Optional[str] = None, image_path: Optional[str] = None, apply_to_all: bool = False, ) -> dict[str, Any]: """设置幻灯片背景. Args: filename: 文件名 slide_index: 幻灯片索引 background_type: 背景类型 ('solid', 'gradient', 'image') color: 背景颜色 (十六进制格式) image_path: 背景图片路径 apply_to_all: 是否应用到所有幻灯片 """ try: file_path = config.paths.output_dir / filename self.file_manager.validate_file_path(file_path, must_exist=True) prs = Presentation(str(file_path)) if not apply_to_all and slide_index >= len(prs.slides): raise ValueError(f"幻灯片索引 {slide_index} 超出范围") slides_to_update = prs.slides if apply_to_all else [prs.slides[slide_index]] for slide in slides_to_update: background = slide.background fill = background.fill if background_type == "solid": fill.solid() if color: r, g, b = ColorUtils.hex_to_rgb(color) fill.fore_color.rgb = RGBColor(r, g, b) else: fill.fore_color.rgb = RGBColor(255, 255, 255) elif background_type == "gradient": fill.gradient() fill.gradient_angle = 90.0 if color: r, g, b = ColorUtils.hex_to_rgb(color) fill.gradient_stops[0].color.rgb = RGBColor(r, g, b) # 创建渐变到白色 fill.gradient_stops[1].color.rgb = RGBColor(255, 255, 255) elif background_type == "image": if not image_path: raise ValueError("背景类型为 'image' 时必须提供 image_path") image_file_path = config.paths.output_dir / image_path self.file_manager.validate_file_path(image_file_path, must_exist=True) from pptx.oxml.xmlchemy import OxmlElement # 设置背景图片需要更复杂的XML操作 # 简化版本:添加图片作为背景 fill.solid() # 注意:完整的背景图片功能需要更复杂的实现 prs.save(str(file_path)) logger.info(f"背景设置成功: {file_path}") return { "success": True, "message": f"背景类型 '{background_type}' 已应用", "filename": str(file_path), "background_type": background_type, "slides_affected": len(slides_to_update), } except Exception as e: logger.error(f"设置背景失败: {e}") return {"success": False, "message": f"操作失败: {str(e)}"}

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/walkingzzzy/office-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server