using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Drawing; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Presentation; namespace AxCopilot.Services.Agent; public class PptxSkill : IAgentTool { private static readonly Dictionary Themes = new Dictionary { ["professional"] = ("2B579A", "4B5EFC", "1A1A2E", "FFFFFF", "FFFFFF"), ["modern"] = ("0D9488", "06B6D4", "1A1A2E", "FFFFFF", "FFFFFF"), ["dark"] = ("374151", "6366F1", "F9FAFB", "FFFFFF", "1F2937"), ["minimal"] = ("6B7280", "3B82F6", "111827", "FFFFFF", "FAFAFA") }; public string Name => "pptx_create"; public string Description => "Create a PowerPoint (.pptx) presentation. Supports slide layouts: title (title+subtitle), content (title+body text), two_column (title+left+right), table (title+headers+rows), blank. No external runtime required (native OpenXML)."; public ToolParameterSchema Parameters { get { ToolParameterSchema toolParameterSchema = new ToolParameterSchema(); Dictionary obj = new Dictionary { ["path"] = new ToolProperty { Type = "string", Description = "Output file path (.pptx). Relative to work folder." }, ["title"] = new ToolProperty { Type = "string", Description = "Presentation title (used on first slide if no explicit title slide)." }, ["slides"] = new ToolProperty { Type = "array", Description = "Array of slide objects. Each slide: {\"layout\": \"title|content|two_column|table|blank\", \"title\": \"Slide Title\", \"subtitle\": \"...\", \"body\": \"...\", \"left\": \"...\", \"right\": \"...\", \"headers\": [...], \"rows\": [[...]], \"notes\": \"Speaker notes\"}", Items = new ToolProperty { Type = "object" } } }; ToolProperty obj2 = new ToolProperty { Type = "string", Description = "Color theme: professional (blue), modern (teal), dark (dark gray), minimal (light). Default: professional" }; int num = 4; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "professional"; span[1] = "modern"; span[2] = "dark"; span[3] = "minimal"; obj2.Enum = list; obj["theme"] = obj2; toolParameterSchema.Properties = obj; num = 2; List list2 = new List(num); CollectionsMarshal.SetCount(list2, num); Span span2 = CollectionsMarshal.AsSpan(list2); span2[0] = "path"; span2[1] = "slides"; toolParameterSchema.Required = list2; return toolParameterSchema; } } public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { string path = args.GetProperty("path").GetString() ?? ""; if (!args.TryGetProperty("title", out var tt)) { } else if (tt.GetString() == null) { } JsonElement th; string theme = (args.TryGetProperty("theme", out th) ? (th.GetString() ?? "professional") : "professional"); if (!args.TryGetProperty("slides", out var slidesEl) || slidesEl.ValueKind != JsonValueKind.Array) { return ToolResult.Fail("slides 배열이 필요합니다."); } string fullPath = FileReadTool.ResolvePath(path, context.WorkFolder); if (context.ActiveTab == "Cowork") { fullPath = AgentContext.EnsureTimestampedPath(fullPath); } if (!fullPath.EndsWith(".pptx", StringComparison.OrdinalIgnoreCase)) { fullPath += ".pptx"; } if (!context.IsPathAllowed(fullPath)) { return ToolResult.Fail("경로 접근 차단: " + fullPath); } if (!(await context.CheckWritePermissionAsync(Name, fullPath))) { return ToolResult.Fail("쓰기 권한 거부: " + fullPath); } string dir = System.IO.Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(dir)) { Directory.CreateDirectory(dir); } if (!Themes.TryGetValue(theme, out var colors)) { colors = Themes["professional"]; } try { using PresentationDocument pres = PresentationDocument.Create(fullPath, PresentationDocumentType.Presentation); PresentationPart presPart = pres.AddPresentationPart(); presPart.Presentation = new Presentation(); presPart.Presentation.SlideIdList = new SlideIdList(); presPart.Presentation.SlideSize = new SlideSize { Cx = 12192000, Cy = 6858000 }; presPart.Presentation.NotesSize = new NotesSize { Cx = 6858000L, Cy = 9144000L }; uint slideId = 256u; int slideCount = 0; foreach (JsonElement slideEl in slidesEl.EnumerateArray()) { JsonElement lay; string layout = (slideEl.TryGetProperty("layout", out lay) ? (lay.GetString() ?? "content") : "content"); JsonElement st; string slideTitle = (slideEl.TryGetProperty("title", out st) ? (st.GetString() ?? "") : ""); JsonElement sub; string subtitle = (slideEl.TryGetProperty("subtitle", out sub) ? (sub.GetString() ?? "") : ""); JsonElement bd; string body = (slideEl.TryGetProperty("body", out bd) ? (bd.GetString() ?? "") : ""); JsonElement lf; string left = (slideEl.TryGetProperty("left", out lf) ? (lf.GetString() ?? "") : ""); JsonElement rt; string right = (slideEl.TryGetProperty("right", out rt) ? (rt.GetString() ?? "") : ""); if (!slideEl.TryGetProperty("notes", out var nt)) { } else if (nt.GetString() == null) { } SlidePart slidePart = presPart.AddNewPart(); slidePart.Slide = new Slide(new CommonSlideData(new ShapeTree(new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeProperties(new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = 1u, Name = "" }, new DocumentFormat.OpenXml.Presentation.NonVisualGroupShapeDrawingProperties(), new ApplicationNonVisualDrawingProperties()), new GroupShapeProperties(new TransformGroup())))); ShapeTree shapeTree = slidePart.Slide.CommonSlideData.ShapeTree; uint shapeId = 2u; switch (layout) { case "title": AddRectangle(shapeTree, ref shapeId, 0L, 0L, 12192000L, 6858000L, colors.Primary); AddTextBox(shapeTree, ref shapeId, 600000L, 2000000L, 10992000L, 1400000L, slideTitle, 3600, colors.TextLight, bold: true); if (!string.IsNullOrEmpty(subtitle)) { AddTextBox(shapeTree, ref shapeId, 600000L, 3600000L, 10992000L, 800000L, subtitle, 2000, colors.TextLight, bold: false); } break; case "two_column": AddTextBox(shapeTree, ref shapeId, 600000L, 300000L, 10992000L, 800000L, slideTitle, 2800, colors.Primary, bold: true); AddTextBox(shapeTree, ref shapeId, 600000L, 1300000L, 5200000L, 5000000L, left, 1600, colors.TextDark, bold: false); AddTextBox(shapeTree, ref shapeId, 6400000L, 1300000L, 5200000L, 5000000L, right, 1600, colors.TextDark, bold: false); break; case "table": { AddTextBox(shapeTree, ref shapeId, 600000L, 300000L, 10992000L, 800000L, slideTitle, 2800, colors.Primary, bold: true); string tableText = FormatTableAsText(slideEl, colors); AddTextBox(shapeTree, ref shapeId, 600000L, 1300000L, 10992000L, 5000000L, tableText, 1400, colors.TextDark, bold: false); break; } default: AddTextBox(shapeTree, ref shapeId, 600000L, 300000L, 10992000L, 800000L, slideTitle, 2800, colors.Primary, bold: true); AddTextBox(shapeTree, ref shapeId, 600000L, 1300000L, 10992000L, 5000000L, body, 1600, colors.TextDark, bold: false); break; case "blank": break; } presPart.Presentation.SlideIdList.AppendChild(new SlideId { Id = slideId++, RelationshipId = presPart.GetIdOfPart(slidePart) }); slideCount++; lay = default(JsonElement); st = default(JsonElement); sub = default(JsonElement); bd = default(JsonElement); lf = default(JsonElement); rt = default(JsonElement); nt = default(JsonElement); } presPart.Presentation.Save(); return ToolResult.Ok($"✅ PPTX 생성 완료: {System.IO.Path.GetFileName(fullPath)} ({slideCount}슬라이드, 테마: {theme})", fullPath); } catch (Exception ex) { return ToolResult.Fail("PPTX 생성 실패: " + ex.Message); } } private static void AddTextBox(ShapeTree tree, ref uint id, long x, long y, long cx, long cy, string text, int fontSize, string color, bool bold) { DocumentFormat.OpenXml.Presentation.Shape shape = new DocumentFormat.OpenXml.Presentation.Shape(); shape.NonVisualShapeProperties = new DocumentFormat.OpenXml.Presentation.NonVisualShapeProperties(new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = id++, Name = $"TextBox{id}" }, new DocumentFormat.OpenXml.Presentation.NonVisualShapeDrawingProperties(new ShapeLocks { NoGrouping = true }), new ApplicationNonVisualDrawingProperties()); shape.ShapeProperties = new DocumentFormat.OpenXml.Presentation.ShapeProperties(new Transform2D(new Offset { X = x, Y = y }, new Extents { Cx = cx, Cy = cy }), new PresetGeometry(new AdjustValueList()) { Preset = ShapeTypeValues.Rectangle }); DocumentFormat.OpenXml.Presentation.TextBody textBody = new DocumentFormat.OpenXml.Presentation.TextBody(new BodyProperties { Wrap = TextWrappingValues.Square }, new ListStyle()); string[] array = text.Split('\n'); string[] array2 = array; foreach (string text2 in array2) { Paragraph paragraph = new Paragraph(); Run run = new Run(); RunProperties runProperties = new RunProperties { Language = "ko-KR", FontSize = fontSize, Dirty = false }; runProperties.AppendChild(new SolidFill(new RgbColorModelHex { Val = color })); if (bold) { runProperties.Bold = true; } run.AppendChild(runProperties); run.AppendChild(new DocumentFormat.OpenXml.Drawing.Text(text2)); paragraph.AppendChild(run); textBody.AppendChild(paragraph); } shape.TextBody = textBody; tree.AppendChild(shape); } private static void AddRectangle(ShapeTree tree, ref uint id, long x, long y, long cx, long cy, string fillColor) { DocumentFormat.OpenXml.Presentation.Shape shape = new DocumentFormat.OpenXml.Presentation.Shape(); shape.NonVisualShapeProperties = new DocumentFormat.OpenXml.Presentation.NonVisualShapeProperties(new DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties { Id = id++, Name = $"Rect{id}" }, new DocumentFormat.OpenXml.Presentation.NonVisualShapeDrawingProperties(), new ApplicationNonVisualDrawingProperties()); shape.ShapeProperties = new DocumentFormat.OpenXml.Presentation.ShapeProperties(new Transform2D(new Offset { X = x, Y = y }, new Extents { Cx = cx, Cy = cy }), new PresetGeometry(new AdjustValueList()) { Preset = ShapeTypeValues.Rectangle }, new SolidFill(new RgbColorModelHex { Val = fillColor })); tree.AppendChild(shape); } private static string FormatTableAsText(JsonElement slideEl, (string Primary, string Accent, string TextDark, string TextLight, string Bg) colors) { StringBuilder stringBuilder = new StringBuilder(); if (slideEl.TryGetProperty("headers", out var value)) { List list = new List(); foreach (JsonElement item in value.EnumerateArray()) { list.Add(item.GetString() ?? ""); } stringBuilder.AppendLine(string.Join(" | ", list)); stringBuilder.AppendLine(new string('─', list.Sum((string h) => h.Length + 5))); } if (slideEl.TryGetProperty("rows", out var value2)) { foreach (JsonElement item2 in value2.EnumerateArray()) { List list2 = new List(); foreach (JsonElement item3 in item2.EnumerateArray()) { list2.Add(item3.GetString() ?? item3.ToString()); } stringBuilder.AppendLine(string.Join(" | ", list2)); } } return stringBuilder.ToString(); } }